@marcomigozzi/trailhead 0.13.0 → 0.14.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 +28 -3
- package/README.md +38 -3
- package/bin/lib/codex-projection.js +16 -9
- package/bin/lib/codex-projection.test.js +39 -11
- package/bin/lib/install-verify.js +10 -5
- package/bin/lib/install-verify.test.js +51 -0
- package/bin/trailhead.js +37 -25
- package/bin/trailhead.test.js +162 -5
- package/package.json +4 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +3 -2
- package/plugins/trailhead/commands/panel.md +10 -0
- package/plugins/trailhead/hooks/hooks.json +3 -18
- package/plugins/trailhead/hooks/lib/body-sources.js +795 -0
- package/plugins/trailhead/hooks/lib/gh-subcommand.js +92 -10
- package/plugins/trailhead/hooks/lib/gh-subcommand.test.js +55 -0
- package/plugins/trailhead/hooks/lib/mockup-link.js +21 -5
- package/plugins/trailhead/hooks/lib/shell-scan.js +496 -64
- package/plugins/trailhead/hooks/lib/shell-scan.test.js +420 -0
- package/plugins/trailhead/hooks/lib/unmodelled.js +345 -0
- package/plugins/trailhead/hooks/lib/unmodelled.test.js +173 -0
- package/plugins/trailhead/hooks/lib/work-state.test.js +252 -0
- package/plugins/trailhead/hooks/mod/README.md +187 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +453 -0
- package/plugins/trailhead/hooks/mod/map-pane.tsx +663 -0
- package/plugins/trailhead/hooks/mod/register.tsx +280 -0
- package/plugins/trailhead/hooks/mod/work-state-prompt.ts +69 -0
- package/plugins/trailhead/hooks/mod/work-state-view.ts +93 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +140 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +233 -0
- package/plugins/trailhead/hooks/trailhead-body-guard.js +145 -61
- package/plugins/trailhead/hooks/trailhead-body-guard.test.js +224 -3
- package/plugins/trailhead/hooks/trailhead-commit-guard.js +41 -14
- package/plugins/trailhead/hooks/trailhead-commit-guard.test.js +2 -1
- package/plugins/trailhead/hooks/trailhead-install-guard.js +22 -12
- package/plugins/trailhead/hooks/trailhead-install-guard.test.js +2 -1
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.js +48 -11
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +75 -3
- package/plugins/trailhead/hooks/trailhead-search-guard.js +19 -9
- package/plugins/trailhead/hooks/trailhead-search-guard.test.js +2 -1
- package/plugins/trailhead/hooks/trailhead-secret-guard.js +541 -57
- package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +1000 -4
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +171 -6
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +170 -0
- package/plugins/trailhead/skills/_shared/choices.md +3 -1
- package/plugins/trailhead/skills/_shared/multi-map.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +1 -1
- package/plugins/trailhead/skills/trailhead/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +3 -2
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +3 -1
- package/plugins/trailhead/templates/trailhead-work-state.js +199 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +1374 -0
- package/plugins/trailhead/tests/map-pane-view.test.ts +565 -0
- package/plugins/trailhead/tests/mod.test.ts +14 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +207 -0
- package/plugins/trailhead/tests/work-state-prompt.test.ts +109 -0
- package/plugins/trailhead/tests/work-state-ui.test.ts +365 -0
- package/plugins/trailhead/tests/work-state-view.test.ts +86 -0
- package/plugins/trailhead/types/index.d.ts +69 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { expect, test } from 'claude-code/testing'
|
|
2
|
+
|
|
3
|
+
import { fit, parseWorkState, pipeline, sameValue, statusLine } from '../hooks/mod/work-state-view'
|
|
4
|
+
|
|
5
|
+
const valid = {
|
|
6
|
+
map: 198,
|
|
7
|
+
ticket: 203,
|
|
8
|
+
title: 'Status entry',
|
|
9
|
+
step: 'plan',
|
|
10
|
+
auto: false,
|
|
11
|
+
autoDone: 0,
|
|
12
|
+
session: 's1',
|
|
13
|
+
updatedAt: '2026-10-11T00:00:00.000Z',
|
|
14
|
+
}
|
|
15
|
+
const parse = (over: Record<string, unknown>) => parseWorkState(JSON.stringify({ ...valid, ...over }))
|
|
16
|
+
|
|
17
|
+
test('parseWorkState accepts a full state and rejects what has no ticket or step', () => {
|
|
18
|
+
expect(parse({})).toEqual(valid)
|
|
19
|
+
expect(parseWorkState('{not json')).toBeNull()
|
|
20
|
+
expect(parseWorkState('[1]')).toBeNull()
|
|
21
|
+
expect(parseWorkState('null')).toBeNull()
|
|
22
|
+
expect(parse({ ticket: 0 })).toBeNull()
|
|
23
|
+
expect(parse({ ticket: 'x' })).toBeNull()
|
|
24
|
+
expect(parse({ step: 'nope' })).toBeNull()
|
|
25
|
+
expect(parse({ step: undefined })).toBeNull()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('parseWorkState defaults autoDone and normalises invalid optional fields', () => {
|
|
29
|
+
const { autoDone: _drop, ...old } = valid
|
|
30
|
+
expect(parseWorkState(JSON.stringify(old))?.autoDone).toBe(0)
|
|
31
|
+
expect(parse({ autoDone: -1 })?.autoDone).toBe(0)
|
|
32
|
+
expect(parse({ autoDone: 1.5 })?.autoDone).toBe(0)
|
|
33
|
+
expect(parse({ autoDone: 4, auto: true })).toMatchObject({ auto: true, autoDone: 4 })
|
|
34
|
+
expect(parse({ map: 'zz' })?.map).toBeNull()
|
|
35
|
+
expect(parse({ map: null })?.map).toBeNull()
|
|
36
|
+
expect(parse({ title: 42 })?.title).toBeNull()
|
|
37
|
+
expect(parse({ title: ' a\nb ' })?.title).toBe('a b')
|
|
38
|
+
expect(parse({ title: ' ' })?.title).toBeNull()
|
|
39
|
+
expect(parse({ auto: 'yes' })?.auto).toBe(false)
|
|
40
|
+
expect(parse({ session: 7 })?.session).toBeNull()
|
|
41
|
+
expect(parse({ session: null })?.session).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('statusLine builds the entry for each shape', () => {
|
|
45
|
+
const s = parse({})
|
|
46
|
+
expect(statusLine(s, false)).toBe('map #198 · #203 Status entry · plan')
|
|
47
|
+
expect(statusLine(parse({ map: null }), false)).toBe('whiteboard · #203 Status entry · plan')
|
|
48
|
+
expect(statusLine(parse({ title: null }), false)).toBe('map #198 · #203 · plan')
|
|
49
|
+
expect(statusLine(parse({ auto: true, autoDone: 3 }), false)).toBe(
|
|
50
|
+
'map #198 · #203 Status entry · plan · auto 3 done',
|
|
51
|
+
)
|
|
52
|
+
expect(statusLine(s, true)).toBe('map #198 · #203 Status entry · plan · orphaned, resume?')
|
|
53
|
+
expect(statusLine(parse({ auto: true, autoDone: 1 }), true)).toBe(
|
|
54
|
+
'map #198 · #203 Status entry · plan · auto 1 done · orphaned, resume?',
|
|
55
|
+
)
|
|
56
|
+
expect(statusLine(null, false)).toBeUndefined()
|
|
57
|
+
expect(statusLine(null, true)).toBeUndefined()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('pipeline marks the stages for each step', () => {
|
|
61
|
+
const status = (step: string) => pipeline(step).stages.map(s => s.status)
|
|
62
|
+
expect(pipeline('plan').stages.map(s => s.name)).toEqual(['discuss', 'plan', 'execute', 'verify'])
|
|
63
|
+
expect(status('discuss')).toEqual(['current', 'todo', 'todo', 'todo'])
|
|
64
|
+
expect(status('plan')).toEqual(['done', 'current', 'todo', 'todo'])
|
|
65
|
+
expect(status('execute')).toEqual(['done', 'done', 'current', 'todo'])
|
|
66
|
+
expect(status('verify')).toEqual(['done', 'done', 'done', 'current'])
|
|
67
|
+
expect(status('resolve')).toEqual(['done', 'done', 'done', 'done'])
|
|
68
|
+
expect(status('paused')).toEqual(['todo', 'todo', 'todo', 'todo'])
|
|
69
|
+
expect(pipeline('paused').isPaused).toBe(true)
|
|
70
|
+
expect(pipeline('plan').isPaused).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('sameValue compares by content, not by key order or reference', () => {
|
|
74
|
+
expect(sameValue({ a: 1, b: null }, { b: null, a: 1 })).toBe(true)
|
|
75
|
+
expect(sameValue(parse({}), parse({}))).toBe(true)
|
|
76
|
+
expect(sameValue(parse({}), parse({ step: 'execute' }))).toBe(false)
|
|
77
|
+
expect(sameValue(null, null)).toBe(true)
|
|
78
|
+
expect(sameValue(null, parse({}))).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('fit cuts with an ellipsis by code point and leaves a short text whole', () => {
|
|
82
|
+
expect(fit('short', 10)).toBe('short')
|
|
83
|
+
expect(fit('abcdef', 4)).toBe('abc…')
|
|
84
|
+
expect(fit('abcdef', 1)).toBe('…')
|
|
85
|
+
expect(fit('a' + '\u{1F680}'.repeat(5), 3)).toBe('a\u{1F680}…')
|
|
86
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Typed contract of the trailhead mod: the shared work state (see #200).
|
|
2
|
+
export type WorkState = {
|
|
3
|
+
map: number | null
|
|
4
|
+
ticket: number | null
|
|
5
|
+
title: string | null
|
|
6
|
+
step: string | null
|
|
7
|
+
auto: boolean
|
|
8
|
+
autoDone: number
|
|
9
|
+
session: string | null
|
|
10
|
+
updatedAt: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// The latched system-prompt section: the key it was built for and its text (null: no section).
|
|
14
|
+
export type PromptSnapshot = { key: string; text: string | null }
|
|
15
|
+
|
|
16
|
+
// The map pane (#208): what the last `gh` read of the active map gave.
|
|
17
|
+
export type TicketType = 'decision' | 'research' | 'prototype' | 'build' | 'bug' | 'task'
|
|
18
|
+
export type TicketState = 'frontier' | 'in-progress' | 'blocked'
|
|
19
|
+
|
|
20
|
+
export type PaneTicket = {
|
|
21
|
+
number: number
|
|
22
|
+
title: string
|
|
23
|
+
type: TicketType | null
|
|
24
|
+
state: TicketState
|
|
25
|
+
assignees: readonly string[]
|
|
26
|
+
url: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// One open map of the repo, as the maps list shows it.
|
|
30
|
+
export type MapSummary = { number: number; title: string }
|
|
31
|
+
|
|
32
|
+
export type MapInfo = { number: number; title: string; destination: string | null; url: string }
|
|
33
|
+
|
|
34
|
+
// One ticket of the map's history, in epoch ms; `closedAt` null while open.
|
|
35
|
+
export type BurndownIssue = { openedAt: number; closedAt: number | null; isRemoved: boolean }
|
|
36
|
+
// `isCapped`: gh returned as many rows as it was asked for, so the oldest tickets may be missing.
|
|
37
|
+
export type BurndownHistory = { issues: readonly BurndownIssue[]; isCapped: boolean }
|
|
38
|
+
|
|
39
|
+
// `map` and `tickets` keep the last good read through a failure; `error` is its reason (null: healthy).
|
|
40
|
+
// `history` has its own last good read and time (`historyAt`): a history-only failure keeps it.
|
|
41
|
+
export type MapPaneData = {
|
|
42
|
+
map: MapInfo | null
|
|
43
|
+
tickets: readonly PaneTicket[]
|
|
44
|
+
unverifiedCount: number
|
|
45
|
+
isCapped: boolean
|
|
46
|
+
// The repo's open maps (empty until a first good read, then the last good one).
|
|
47
|
+
maps: readonly MapSummary[]
|
|
48
|
+
history: BurndownHistory | null
|
|
49
|
+
historyAt: number | null
|
|
50
|
+
fetchedAt: number | null
|
|
51
|
+
error: string | null
|
|
52
|
+
isLoading: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
declare module 'claude-code' {
|
|
56
|
+
interface PluginState {
|
|
57
|
+
trailhead: {
|
|
58
|
+
workState: WorkState | null
|
|
59
|
+
isOrphaned: boolean
|
|
60
|
+
promptSnapshot: PromptSnapshot | null
|
|
61
|
+
mapPane: MapPaneData
|
|
62
|
+
activeMap: number | null
|
|
63
|
+
// The map the pane shows when it is not the active one; null follows the active map.
|
|
64
|
+
viewedMap: number | null
|
|
65
|
+
pendingWork: number | null
|
|
66
|
+
paneAutoOpened: boolean
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|