@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.
- package/README.it.md +82 -25
- package/README.md +82 -25
- package/bin/base-preflight-docs.test.js +4 -1
- package/bin/bulk-cross-reference-docs.test.js +36 -0
- package/bin/capture-action.test.js +167 -0
- package/bin/capture-docs.test.js +32 -0
- package/bin/engine-route-docs.test.js +155 -0
- package/bin/facet-stamp-docs.test.js +79 -0
- package/bin/forced-mutation-docs.test.js +38 -0
- package/bin/grill-model-docs.test.js +155 -0
- package/bin/grilling-holistic-review-docs.test.js +7 -3
- package/bin/guard-axis-docs.test.js +69 -0
- package/bin/inbox-needs-info-docs.test.js +68 -0
- package/bin/lib/codex-projection.js +28 -19
- package/bin/lib/codex-projection.test.js +77 -20
- package/bin/lib/host-descriptor.js +1 -1
- package/bin/lib/host-descriptor.test.js +6 -0
- package/bin/needs-you-docs.test.js +9 -2
- package/bin/options-in-chat-docs.test.js +65 -0
- package/bin/pick-stress-test-docs.test.js +1 -1
- package/bin/pipeline-review-docs.test.js +118 -0
- package/bin/plan-review-budget-docs.test.js +63 -0
- package/bin/repo-target-docs.test.js +41 -0
- package/bin/report-status-docs.test.js +10 -2
- package/bin/seam-coverage-docs.test.js +46 -0
- package/bin/session-trust-docs.test.js +37 -0
- package/bin/standards-brief-docs.test.js +112 -0
- package/bin/surfaced-scope-docs.test.js +57 -0
- package/bin/test-once-docs.test.js +101 -0
- package/bin/test-run-docs.test.js +47 -0
- package/bin/trailhead.js +15 -8
- package/bin/trailhead.test.js +71 -17
- package/bin/unblock-docs.test.js +56 -0
- package/bin/yes-accepts-recommended-docs.test.js +54 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +2 -1
- package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
- package/plugins/trailhead/agents/trailhead-grill.md +41 -0
- package/plugins/trailhead/agents/trailhead-plan.md +1 -1
- package/plugins/trailhead/agents/trailhead-verify.md +1 -0
- package/plugins/trailhead/hooks/lib/audit-walk.test.js +62 -0
- package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
- package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
- package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
- package/plugins/trailhead/hooks/lib/git-env-mod.test.js +55 -0
- package/plugins/trailhead/hooks/lib/git-env-spawn.test.js +73 -0
- package/plugins/trailhead/hooks/lib/git-env.test.js +51 -0
- package/plugins/trailhead/hooks/lib/global-config-dir.test.js +48 -0
- package/plugins/trailhead/hooks/lib/heredoc-mod.test.js +85 -0
- package/plugins/trailhead/hooks/lib/inbox-needs-info.test.js +179 -0
- package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +165 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +22 -1
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +64 -1
- package/plugins/trailhead/hooks/lib/model-route.js +207 -0
- package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +45 -1
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +272 -0
- package/plugins/trailhead/hooks/lib/repo-target.test.js +256 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +56 -1
- package/plugins/trailhead/hooks/lib/statusline-git-env.test.js +47 -0
- package/plugins/trailhead/hooks/lib/test-run.test.js +757 -0
- package/plugins/trailhead/hooks/lib/trailhead-git-env.js +40 -0
- package/plugins/trailhead/hooks/mod/README.md +42 -17
- package/plugins/trailhead/hooks/mod/auto-run.ts +2 -1
- package/plugins/trailhead/hooks/mod/git-env.ts +31 -0
- package/plugins/trailhead/hooks/mod/heredoc.ts +79 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +3 -1
- package/plugins/trailhead/hooks/mod/map-pane.tsx +204 -49
- package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
- package/plugins/trailhead/hooks/mod/repo-pin.ts +113 -9
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +2 -1
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +28 -14
- package/plugins/trailhead/hooks/trailhead-check-update.js +20 -2
- package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +2 -2
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +1 -1
- package/plugins/trailhead/hooks/trailhead-test-guard.js +232 -0
- package/plugins/trailhead/hooks/trailhead-test-guard.test.js +172 -0
- package/plugins/trailhead/skills/_shared/capture-core.md +123 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +30 -16
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +19 -6
- package/plugins/trailhead/skills/_shared/substrate.md +4 -3
- package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
- package/plugins/trailhead/skills/_shared/teamwork.md +11 -5
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review-pipeline.md +24 -0
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +22 -6
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +6 -3
- package/plugins/trailhead/skills/_shared/techniques/debug.md +5 -2
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +28 -3
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +16 -4
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/tdd.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +5 -1
- package/plugins/trailhead/skills/_shared/techniques.md +8 -6
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
- 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 +11 -6
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +18 -3
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +12 -9
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +3 -1
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +13 -13
- package/plugins/trailhead/templates/statusline-trailhead.sh +10 -3
- package/plugins/trailhead/templates/trailhead-audit-walk.js +56 -4
- package/plugins/trailhead/templates/trailhead-auto-merge.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-queue.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-slot.js +2 -1
- package/plugins/trailhead/templates/trailhead-base-preflight.js +10 -5
- package/plugins/trailhead/templates/trailhead-capture.js +419 -0
- package/plugins/trailhead/templates/trailhead-checkout-find.js +257 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +78 -21
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +2 -1
- package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
- package/plugins/trailhead/templates/trailhead-git-env.js +40 -0
- package/plugins/trailhead/templates/trailhead-inbox-needs-info.js +161 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +2 -1
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +9 -4
- package/plugins/trailhead/templates/trailhead-issue-body.js +161 -12
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +2 -1
- package/plugins/trailhead/templates/trailhead-needs-you.js +38 -4
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +261 -7
- package/plugins/trailhead/templates/trailhead-repo-target.js +302 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +57 -9
- package/plugins/trailhead/templates/trailhead-test-run.js +752 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-followup.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-setup.js +9 -4
- package/plugins/trailhead/templates/trailhead-unblock.js +2 -1
- package/plugins/trailhead/tests/auto-run-ui.test.ts +2 -1
- package/plugins/trailhead/tests/git-env.test.ts +20 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +386 -50
- package/plugins/trailhead/tests/map-pane-view.test.ts +6 -0
- package/plugins/trailhead/tests/map-pane-write.test.ts +57 -1
- package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +3 -2
- package/plugins/trailhead/tests/repo-pin-relocate.test.ts +175 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
- package/plugins/trailhead/tests/unscrub.ts +8 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +3 -2
- package/plugins/trailhead/tests/work-state-worktree.test.ts +2 -1
- package/plugins/trailhead/types/index.d.ts +7 -0
|
@@ -5,6 +5,8 @@ import { atom, read, update } from 'claude-code'
|
|
|
5
5
|
import type { EngineInterface } from 'claude-code'
|
|
6
6
|
|
|
7
7
|
import type { RepoPin } from '../../types'
|
|
8
|
+
import { gitArgv } from './git-env'
|
|
9
|
+
import { parseWorktreeBranches } from './worktree-porcelain'
|
|
8
10
|
import { sameValue } from './work-state-view'
|
|
9
11
|
|
|
10
12
|
type RepoRead = { root: string; remote: string | null }
|
|
@@ -46,13 +48,16 @@ export const nextPin = (current: RepoPin | null, repo: RepoRead | null, toplevel
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
// The pinned checkout as the trailing word of a verb's args: `--in=` plus base64url of the root (alphabet
|
|
49
|
-
// [A-Za-z0-9_-], no padding), so a path with spaces, quotes or a leading `-` stays one shell-safe word
|
|
50
|
-
//
|
|
51
|
+
// [A-Za-z0-9_-], no padding), so a path with spaces, quotes or a leading `-` stays one shell-safe word, then `.` and
|
|
52
|
+
// base64url of `host/owner/name` (`.` is outside the alphabet, so it separates unambiguously). The verb decodes it
|
|
53
|
+
// with templates/trailhead-checkout-in.js and moves there first; when the root is gone, the repo part lets the
|
|
54
|
+
// helper find the checkout by its remote (#312, #329).
|
|
51
55
|
// No Buffer in the mod's runtime: UTF-8 bytes through btoa, then the url-safe alphabet.
|
|
52
|
-
|
|
53
|
-
const binary = Array.from(new TextEncoder().encode(
|
|
54
|
-
return
|
|
56
|
+
const base64url = (text: string): string => {
|
|
57
|
+
const binary = Array.from(new TextEncoder().encode(text), byte => String.fromCharCode(byte)).join('')
|
|
58
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
55
59
|
}
|
|
60
|
+
export const checkoutArg = (pin: RepoPin): string => `--in=${base64url(pin.root)}.${base64url(pinKey(pin))}`
|
|
56
61
|
|
|
57
62
|
// A repo-relative path under the pinned root, absolute whatever the session cwd is.
|
|
58
63
|
export const pinPath = (pin: RepoPin, relative: string): string => `${pin.root.replace(/\/+$/, '')}/${relative}`
|
|
@@ -68,7 +73,7 @@ let remembered: RepoPin | null = null
|
|
|
68
73
|
// the caller falls back.
|
|
69
74
|
export async function toplevelOf($: EngineInterface): Promise<string | null> {
|
|
70
75
|
try {
|
|
71
|
-
const out = await $.process.run(
|
|
76
|
+
const out = await $.process.run(gitArgv('rev-parse', '--show-toplevel'), { cwd: await $.session.cwd(), timeoutMs: 5_000 })
|
|
72
77
|
const top = out.stdout.trim()
|
|
73
78
|
return out.exitCode === 0 && top.startsWith('/') ? top : null
|
|
74
79
|
} catch {
|
|
@@ -92,9 +97,9 @@ async function gitRepo($: EngineInterface): Promise<{ repo: RepoRead; toplevel:
|
|
|
92
97
|
try {
|
|
93
98
|
const cwd = await $.session.cwd()
|
|
94
99
|
const run = (argv: string[]) => $.process.run(argv, { cwd, timeoutMs: 5_000 })
|
|
95
|
-
const top = await run(
|
|
100
|
+
const top = await run(gitArgv('rev-parse', '--show-toplevel'))
|
|
96
101
|
if (repoFromGit(top, null) === null) return null
|
|
97
|
-
const repo = repoFromGit(top, await run(
|
|
102
|
+
const repo = repoFromGit(top, await run(gitArgv('remote', 'get-url', 'origin')))
|
|
98
103
|
return repo === null ? null : { repo, toplevel: repo.root }
|
|
99
104
|
} catch {
|
|
100
105
|
return null
|
|
@@ -136,10 +141,107 @@ async function isServed($: EngineInterface, host: string, cwd: string): Promise<
|
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
// The pinned checkout deleted under the session (a finished worktree, a removed clone): the pin moves to a live
|
|
145
|
+
// checkout of the same repo (#347). Preferred in order: the main working tree, another worktree, the sibling
|
|
146
|
+
// clone itself. Candidates are the repo's root as the host knows it and the folders beside the dead root; every
|
|
147
|
+
// probe is `git -C <path>` from `/`, never a spawn in the dead directory. null when nothing is left.
|
|
148
|
+
const MAX_SIBLINGS = 100
|
|
149
|
+
const parentOf = (path: string): string => path.replace(/\/+$/, '').replace(/\/[^/]*$/, '') || '/'
|
|
150
|
+
|
|
151
|
+
async function checkoutOf($: EngineInterface, path: string, key: string): Promise<string | null> {
|
|
152
|
+
try {
|
|
153
|
+
const run = (args: string[]) => $.process.run(gitArgv('-C', path, ...args), { cwd: '/', timeoutMs: 5_000 })
|
|
154
|
+
const top = await run(['rev-parse', '--show-toplevel'])
|
|
155
|
+
if (top.exitCode !== 0 || !top.stdout.trim().startsWith('/')) return null
|
|
156
|
+
const origin = await run(['remote', 'get-url', 'origin'])
|
|
157
|
+
const id = remoteId(origin.exitCode === 0 ? origin.stdout : null)
|
|
158
|
+
return id !== null && `${id.host}/${id.slug}` === key ? top.stdout.trim() : null
|
|
159
|
+
} catch {
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function exists($: EngineInterface, path: string): Promise<boolean> {
|
|
165
|
+
try {
|
|
166
|
+
return await $.fs.exists(path)
|
|
167
|
+
} catch {
|
|
168
|
+
return true // unreadable: do not move the pin on a guess
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function siblingsOf($: EngineInterface, dir: string): Promise<string[]> {
|
|
173
|
+
try {
|
|
174
|
+
const entries = await $.fs.list(dir)
|
|
175
|
+
return entries.filter(e => e.kind === 'dir' && !e.name.startsWith('.')).slice(0, MAX_SIBLINGS).map(e => `${dir.replace(/\/+$/, '')}/${e.name}`)
|
|
176
|
+
} catch {
|
|
177
|
+
return []
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function liveCheckout($: EngineInterface, pin: RepoPin): Promise<string | null> {
|
|
182
|
+
const key = pinKey(pin)
|
|
183
|
+
const seeds: string[] = []
|
|
184
|
+
try {
|
|
185
|
+
const host = await $.session.repo()
|
|
186
|
+
if (host !== null && host.root !== pin.root) seeds.push(host.root)
|
|
187
|
+
} catch {
|
|
188
|
+
// The host read failed: the neighbours still answer.
|
|
189
|
+
}
|
|
190
|
+
seeds.push(...(await siblingsOf($, parentOf(pin.root))))
|
|
191
|
+
for (const seed of seeds) {
|
|
192
|
+
if (seed === pin.root || !(await exists($, seed))) continue
|
|
193
|
+
const top = await checkoutOf($, seed, key)
|
|
194
|
+
if (top === null) continue
|
|
195
|
+
// The seed's own worktree list names the main tree first, then the others.
|
|
196
|
+
const listed = await $.process.run(gitArgv('-C', top, 'worktree', 'list', '--porcelain'), { cwd: '/', timeoutMs: 5_000 }).catch(() => null)
|
|
197
|
+
const paths = listed !== null && listed.exitCode === 0 ? parseWorktreeBranches(listed.stdout).map(w => w.path) : []
|
|
198
|
+
for (const path of [...paths, top]) {
|
|
199
|
+
if (path === pin.root || !(await exists($, path))) continue
|
|
200
|
+
const checkout = path === top ? top : await checkoutOf($, path, key)
|
|
201
|
+
if (checkout !== null) return checkout
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The pin as it stands, or the live checkout of its repo when its root is gone. Both outcomes are cached per
|
|
208
|
+
// repo and dead root, since the host's memoized repo keeps naming the dead root and every refresh asks again: a hit
|
|
209
|
+
// is revalidated by one exists check, a miss is not searched again within RELOCATE_RETRY_MS (a search spawns git
|
|
210
|
+
// per sibling). Reset at a session start.
|
|
211
|
+
const RELOCATE_RETRY_MS = 60_000
|
|
212
|
+
const missed = new Map<string, number>()
|
|
213
|
+
const relocated = new Map<string, string>()
|
|
214
|
+
const relocationKey = (pin: RepoPin): string => `${pinKey(pin)}|${pin.root}`
|
|
215
|
+
|
|
216
|
+
export async function livePin($: EngineInterface, pin: RepoPin): Promise<RepoPin> {
|
|
217
|
+
if (await exists($, pin.root)) return pin
|
|
218
|
+
const key = relocationKey(pin)
|
|
219
|
+
const known = relocated.get(key)
|
|
220
|
+
if (known !== undefined) {
|
|
221
|
+
if (await exists($, known)) return { ...pin, root: known }
|
|
222
|
+
relocated.delete(key)
|
|
223
|
+
}
|
|
224
|
+
const now = await $.clock.now().catch(() => null)
|
|
225
|
+
const last = missed.get(key)
|
|
226
|
+
if (now !== null && last !== undefined && now - last < RELOCATE_RETRY_MS) return pin
|
|
227
|
+
const root = await liveCheckout($, pin).catch(() => null)
|
|
228
|
+
if (root === null) {
|
|
229
|
+
if (now !== null) missed.set(key, now)
|
|
230
|
+
return pin
|
|
231
|
+
}
|
|
232
|
+
missed.delete(key)
|
|
233
|
+
relocated.set(key, root)
|
|
234
|
+
return { ...pin, root }
|
|
235
|
+
}
|
|
236
|
+
|
|
139
237
|
// Checks the working copy now and pins what it finds; a failed read keeps the last pin. The host's
|
|
140
238
|
// `session.repo()` can stay null for a repo created mid-session, so git in the cwd answers then (#299). A session
|
|
141
239
|
// start begins from nothing: the pin of an earlier session is not this one's.
|
|
142
240
|
export async function refreshPin($: EngineInterface, init: { isStart?: boolean; onMove?: (pin: RepoPin | null) => Promise<void> } = {}): Promise<RepoPin | null> {
|
|
241
|
+
if (init.isStart === true) {
|
|
242
|
+
missed.clear()
|
|
243
|
+
relocated.clear()
|
|
244
|
+
}
|
|
143
245
|
const stored = await read($, repoPin)
|
|
144
246
|
const current = init.isStart === true ? null : (stored ?? remembered)
|
|
145
247
|
let repo: RepoRead | null = null
|
|
@@ -156,7 +258,9 @@ export async function refreshPin($: EngineInterface, init: { isStart?: boolean;
|
|
|
156
258
|
} catch {
|
|
157
259
|
// Unreadable repo: keep the last pin.
|
|
158
260
|
}
|
|
159
|
-
const
|
|
261
|
+
const next = nextPin(current, repo, toplevel)
|
|
262
|
+
// The pin, kept or freshly read (the host's memoized repo can keep naming a deleted root), may be gone.
|
|
263
|
+
const pin = next === null ? null : await livePin($, next)
|
|
160
264
|
remembered = pin
|
|
161
265
|
if (!sameValue(stored, pin)) {
|
|
162
266
|
// Right before the atom changes, whoever moves it, so what was read for the old pin goes no later than the change.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// may sit in the main checkout. Pure parts first, then the engine part that looks the state up.
|
|
4
4
|
import type { EngineInterface } from 'claude-code'
|
|
5
5
|
|
|
6
|
+
import { gitArgv } from './git-env'
|
|
6
7
|
import { isMissing, parseWorkState } from './work-state-view'
|
|
7
8
|
import { MAX_FAILURES, parseWorktreeBranches } from './worktree-porcelain'
|
|
8
9
|
|
|
@@ -91,7 +92,7 @@ async function nowOf($: EngineInterface): Promise<number | null> {
|
|
|
91
92
|
// null: the listing itself failed (not the same as an empty one). Path and branch of each checkout.
|
|
92
93
|
export async function listWorktreeBranches($: EngineInterface, root: string): Promise<{ path: string; branch: string | null }[] | null> {
|
|
93
94
|
try {
|
|
94
|
-
const out = await $.process.run(
|
|
95
|
+
const out = await $.process.run(gitArgv('worktree', 'list', '--porcelain'), { cwd: root, timeoutMs: LOOKUP_TIMEOUT_MS })
|
|
95
96
|
return out.exitCode === 0 ? parseWorktreeBranches(out.stdout) : null
|
|
96
97
|
} catch {
|
|
97
98
|
return null
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// trailhead-bash-guards.js: PreToolUse(Bash) hook.
|
|
3
|
-
// One process for the
|
|
4
|
-
// instead of
|
|
3
|
+
// One process for the seven Bash guards (commit, secret, install, search, body, labels, test)
|
|
4
|
+
// instead of seven: each Bash call pays a single node start-up. The guards keep
|
|
5
5
|
// their own modules and verdicts; this runner only calls each one's pure
|
|
6
|
-
// evaluate(cmd) in a fixed order (commit, secret, install, search, body, labels).
|
|
6
|
+
// evaluate(cmd) in a fixed order (commit, secret, install, search, body, labels, test).
|
|
7
7
|
// - First block wins: its payload goes to stdout and the hook exits 2; later
|
|
8
8
|
// guards do not run. An earlier advisory keeps its stderr line.
|
|
9
9
|
// - No block: advisories (secret-guard's "could not scan") surface on stderr
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
// - Failure posture is unchanged per guard: a throwing guard counts as allow
|
|
12
12
|
// and the others still run; a guard module that fails to load is skipped
|
|
13
13
|
// with a one-line stderr warning.
|
|
14
|
-
// - --guards=<name,...> picks the guards (default: all
|
|
15
|
-
// ships no search-guard and registers --guards=commit,secret,install,body,labels.
|
|
16
|
-
// A missing, empty, malformed or unknown-name list falls back to all
|
|
14
|
+
// - --guards=<name,...> picks the guards (default: all seven). The Codex install
|
|
15
|
+
// ships no search-guard and registers --guards=commit,secret,install,body,labels,test.
|
|
16
|
+
// A missing, empty, malformed or unknown-name list falls back to all seven, so
|
|
17
17
|
// a config typo never disables a guard.
|
|
18
18
|
// - TRAILHEAD_MOD_GUARDS=<name,...>@<epoch-ms>: the mod (which has ported those
|
|
19
19
|
// guards) stamps the guards it already ran; a stamp no older than
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// Crash-safe: unparseable input or any internal error -> exit 0.
|
|
23
23
|
// Self-contained, no dependencies. Pure functions are exported for tests.
|
|
24
24
|
|
|
25
|
-
const GUARD_ORDER = ['commit', 'secret', 'install', 'search', 'body', 'labels'];
|
|
25
|
+
const GUARD_ORDER = ['commit', 'secret', 'install', 'search', 'body', 'labels', 'test'];
|
|
26
26
|
const MOD_GUARDS_FRESH_MS = 5000;
|
|
27
27
|
const MOD_GUARDS_RE = /^([a-z][a-z0-9-]*(?:,[a-z][a-z0-9-]*)*)@(\d+)$/;
|
|
28
28
|
|
|
@@ -37,7 +37,7 @@ function parseModGuards(value, now = Date.now()) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Guards selected by --guards=<name,...>, in canonical order. Anything but a
|
|
40
|
-
// well-formed list of known names means all
|
|
40
|
+
// well-formed list of known names means all seven.
|
|
41
41
|
function parseGuardsArg(argv) {
|
|
42
42
|
const arg = argv.find((a) => a.startsWith('--guards='));
|
|
43
43
|
if (!arg) return GUARD_ORDER;
|
|
@@ -19,7 +19,7 @@ const eq = (name, actual, expected) => { assert.deepStrictEqual(actual, expected
|
|
|
19
19
|
// Run the merged hook end-to-end: tool_input JSON on stdin, capture {code, out, err}.
|
|
20
20
|
function runHook(command, { env = {}, args = [], hook = HOOK, cwd } = {}) {
|
|
21
21
|
const input = JSON.stringify({ tool_input: { command }, ...(cwd ? { cwd } : {}) });
|
|
22
|
-
const r = spawnSync('node', [hook, ...args], { input, env: { ...process.env, ...env } });
|
|
22
|
+
const r = spawnSync('node', [hook, ...args], { input, env: { ...process.env, CLAUDE_CONFIG_DIR: NO_GLOBAL, ...env } });
|
|
23
23
|
return { code: r.status, out: r.stdout.toString(), err: r.stderr.toString() };
|
|
24
24
|
}
|
|
25
25
|
const codeOf = (r) => (r.out ? JSON.parse(r.out).code : undefined);
|
|
@@ -31,9 +31,20 @@ const BAD_INSTALL = 'npm install left-pad';
|
|
|
31
31
|
const BAD_SEARCH = 'cd x && grep y z';
|
|
32
32
|
const BAD_BODY = 'gh issue edit 5 --body ""';
|
|
33
33
|
const BAD_LABELS = 'gh issue create --title x --body y --label trailhead:bug';
|
|
34
|
+
const BAD_TEST = 'npm test 2>&1 | tail -20';
|
|
35
|
+
|
|
36
|
+
// A scratch trailhead repo whose config sets testing.full, and an empty global config dir so
|
|
37
|
+
// the developer's own config never leaks into the probes.
|
|
38
|
+
const SCRATCH = fs.mkdtempSync(path.join(os.tmpdir(), 'tb-test-guard-'));
|
|
39
|
+
const TEST_REPO = path.join(SCRATCH, 'repo');
|
|
40
|
+
fs.mkdirSync(path.join(TEST_REPO, '.git'), { recursive: true });
|
|
41
|
+
fs.mkdirSync(path.join(TEST_REPO, '.trailhead'));
|
|
42
|
+
fs.writeFileSync(path.join(TEST_REPO, '.trailhead', 'config.json'), JSON.stringify({ testing: { full: 'npm test' } }));
|
|
43
|
+
const NO_GLOBAL = path.join(SCRATCH, 'no-global');
|
|
44
|
+
fs.mkdirSync(NO_GLOBAL);
|
|
34
45
|
|
|
35
46
|
// --- unit: constants ---
|
|
36
|
-
eq('guard order is fixed', GUARD_ORDER, ['commit', 'secret', 'install', 'search', 'body', 'labels']);
|
|
47
|
+
eq('guard order is fixed', GUARD_ORDER, ['commit', 'secret', 'install', 'search', 'body', 'labels', 'test']);
|
|
37
48
|
ok('freshness window is 5s', MOD_GUARDS_FRESH_MS === 5000);
|
|
38
49
|
|
|
39
50
|
// --- unit: parseModGuards ---
|
|
@@ -49,10 +60,10 @@ for (const bad of ['commit', '@123', 'commit@abc', ',@1', '', undefined, 'commit
|
|
|
49
60
|
eq('unknown names are ignored', [...parseModGuards(`commit,bogus@${NOW}`, NOW)], ['commit']);
|
|
50
61
|
|
|
51
62
|
// --- unit: parseGuardsArg ---
|
|
52
|
-
eq('no --guards -> all
|
|
63
|
+
eq('no --guards -> all seven', parseGuardsArg([]), GUARD_ORDER);
|
|
53
64
|
eq('valid subset keeps canonical order', parseGuardsArg(['--guards=body,commit']), ['commit', 'body']);
|
|
54
|
-
eq('empty --guards -> all
|
|
55
|
-
eq('malformed --guards -> all
|
|
65
|
+
eq('empty --guards -> all seven', parseGuardsArg(['--guards=']), GUARD_ORDER);
|
|
66
|
+
eq('malformed --guards -> all seven', parseGuardsArg(['--guards=commit,,body']), GUARD_ORDER);
|
|
56
67
|
eq('any unknown name invalidates the whole list', parseGuardsArg(['--guards=commit,secert']), GUARD_ORDER);
|
|
57
68
|
|
|
58
69
|
// --- unit: runGuards with fake evaluators ---
|
|
@@ -66,7 +77,7 @@ const blocker = (code) => () => ({ kind: 'block', payload: { decision: 'block',
|
|
|
66
77
|
evaluators: {
|
|
67
78
|
commit: spy('commit', () => { throw new Error('boom'); }),
|
|
68
79
|
secret: spy('secret', allow), install: spy('install', blocker('I')),
|
|
69
|
-
search: spy('search', blocker('S')), body: spy('body', allow), labels: spy('labels', allow),
|
|
80
|
+
search: spy('search', blocker('S')), body: spy('body', allow), labels: spy('labels', allow), test: spy('test', allow),
|
|
70
81
|
},
|
|
71
82
|
});
|
|
72
83
|
eq('a throwing guard counts as allow and the next one still blocks', r.code, 2);
|
|
@@ -76,7 +87,7 @@ const blocker = (code) => () => ({ kind: 'block', payload: { decision: 'block',
|
|
|
76
87
|
{
|
|
77
88
|
const r = runGuards('x', {
|
|
78
89
|
now: NOW, env: { TRAILHEAD_MOD_GUARDS: `commit@${NOW}` },
|
|
79
|
-
evaluators: { commit: blocker('C'), secret: allow, install: allow, search: allow, body: allow, labels: allow },
|
|
90
|
+
evaluators: { commit: blocker('C'), secret: allow, install: allow, search: allow, body: allow, labels: allow, test: allow },
|
|
80
91
|
});
|
|
81
92
|
eq('a fresh stamp skips the listed guard', r.code, 0);
|
|
82
93
|
}
|
|
@@ -84,7 +95,7 @@ const blocker = (code) => () => ({ kind: 'block', payload: { decision: 'block',
|
|
|
84
95
|
const adv = (t) => () => ({ kind: 'advise', stderr: `${t}\n`, payload: { hookSpecificOutput: { additionalContext: t } } });
|
|
85
96
|
const r = runGuards('x', {
|
|
86
97
|
now: NOW, env: {},
|
|
87
|
-
evaluators: { commit: adv('a'), secret: adv('b'), install: allow, search: allow, body: allow, labels: allow },
|
|
98
|
+
evaluators: { commit: adv('a'), secret: adv('b'), install: allow, search: allow, body: allow, labels: allow, test: allow },
|
|
88
99
|
});
|
|
89
100
|
eq('advisories exit 0', r.code, 0);
|
|
90
101
|
eq('advisory stderr lines are all written', r.stderr, 'a\nb\n');
|
|
@@ -98,11 +109,13 @@ const { evaluate: evInstall } = require('./trailhead-install-guard.js');
|
|
|
98
109
|
const { evaluate: evSearch } = require('./trailhead-search-guard.js');
|
|
99
110
|
const { evaluate: evBody } = require('./trailhead-body-guard.js');
|
|
100
111
|
const { evaluate: evLabels } = require('./trailhead-labels-guard.js');
|
|
112
|
+
const { evaluate: evTest } = require('./trailhead-test-guard.js');
|
|
101
113
|
for (const [name, cmd, ev] of [
|
|
102
114
|
['commit', BAD_COMMIT, evCommit], ['secret', BAD_SECRET, evSecret], ['install', BAD_INSTALL, evInstall],
|
|
103
115
|
['search', BAD_SEARCH, evSearch], ['body', BAD_BODY, evBody], ['labels', BAD_LABELS, evLabels],
|
|
116
|
+
['test', BAD_TEST, (c) => evTest(c, { cwd: TEST_REPO, env: { CLAUDE_CONFIG_DIR: NO_GLOBAL } })],
|
|
104
117
|
]) {
|
|
105
|
-
const r = runHook(cmd);
|
|
118
|
+
const r = runHook(cmd, name === 'test' ? { cwd: TEST_REPO } : {});
|
|
106
119
|
const want = ev(cmd);
|
|
107
120
|
ok(`${name}: evaluator blocks the probe`, want.kind === 'block');
|
|
108
121
|
eq(`${name}: exit 2`, r.code, 2);
|
|
@@ -171,19 +184,19 @@ for (const [name, cmd, ev] of [
|
|
|
171
184
|
const hook = path.join(tmp, 'trailhead-bash-guards.js');
|
|
172
185
|
fs.rmSync(path.join(tmp, 'trailhead-search-guard.js'));
|
|
173
186
|
|
|
174
|
-
const codex = runHook(BAD_SEARCH, { hook, args: ['--guards=commit,secret,install,body,labels'] });
|
|
187
|
+
const codex = runHook(BAD_SEARCH, { hook, args: ['--guards=commit,secret,install,body,labels,test'] });
|
|
175
188
|
eq('--guards without search never loads search-guard', codex.code, 0);
|
|
176
189
|
eq('--guards without search: no warning', codex.err, '');
|
|
177
|
-
const still = runHook(BAD_INSTALL, { hook, args: ['--guards=commit,secret,install,body,labels'] });
|
|
190
|
+
const still = runHook(BAD_INSTALL, { hook, args: ['--guards=commit,secret,install,body,labels,test'] });
|
|
178
191
|
eq('--guards subset still blocks with its guards', still.code, 2);
|
|
179
192
|
const only = runHook(BAD_INSTALL, { hook, args: ['--guards=commit'] });
|
|
180
193
|
eq('unselected guards do not run', only.code, 0);
|
|
181
194
|
|
|
182
195
|
for (const bad of ['--guards=', '--guards=commit,secert', '--guards=commit,,body']) {
|
|
183
196
|
const r = runHook(BAD_INSTALL, { hook, args: [bad] });
|
|
184
|
-
eq(`${bad}: falls back to all
|
|
197
|
+
eq(`${bad}: falls back to all seven (install blocks)`, r.code, 2);
|
|
185
198
|
const clean = runHook('git status', { hook, args: [bad] });
|
|
186
|
-
ok(`${bad}: all
|
|
199
|
+
ok(`${bad}: all seven are selected (the missing search module is reported)`, /search/.test(clean.err));
|
|
187
200
|
}
|
|
188
201
|
|
|
189
202
|
const missing = runHook(`${BAD_BODY} && ${BAD_SEARCH}`, { hook });
|
|
@@ -217,13 +230,14 @@ for (const [name, cmd, ev] of [
|
|
|
217
230
|
// Run from a scratch dir outside any trailhead repo: those suites open unlabelled issues, which
|
|
218
231
|
// the labels guard (rightly) blocks inside a trailhead repo.
|
|
219
232
|
const SUITE_CWD = fs.mkdtempSync(path.join(os.tmpdir(), 'tb-suites-'));
|
|
220
|
-
for (const name of ['commit', 'secret', 'install', 'search', 'body', 'labels']) {
|
|
233
|
+
for (const name of ['commit', 'secret', 'install', 'search', 'body', 'labels', 'test']) {
|
|
221
234
|
const file = path.join(__dirname, `trailhead-${name}-guard.test.js`);
|
|
222
235
|
const r = spawnSync('node', [file], { cwd: SUITE_CWD, env: { ...process.env, TRAILHEAD_GUARD_HOOK: HOOK } });
|
|
223
236
|
if (r.status !== 0) process.stderr.write(r.stdout.toString() + r.stderr.toString());
|
|
224
237
|
eq(`${name}-guard suite passes through the merged hook`, r.status, 0);
|
|
225
238
|
}
|
|
226
239
|
fs.rmSync(SUITE_CWD, { recursive: true, force: true });
|
|
240
|
+
fs.rmSync(SCRATCH, { recursive: true, force: true });
|
|
227
241
|
|
|
228
242
|
// --- registration: one Bash entry, one command (the merged hook) ---
|
|
229
243
|
{
|
|
@@ -11,6 +11,8 @@ const fs = require('fs');
|
|
|
11
11
|
const os = require('os');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { execFileSync } = require('child_process');
|
|
14
|
+
// Un GIT_DIR ereditato batterebbe -C: le chiamate git non lo seguono (#349).
|
|
15
|
+
const { gitEnv } = require('./lib/trailhead-git-env.js');
|
|
14
16
|
|
|
15
17
|
const REPO = 'ToRvaLDz/trailhead';
|
|
16
18
|
const NPM_PKG = '@marcomigozzi/trailhead';
|
|
@@ -33,7 +35,7 @@ function readJSON(p) {
|
|
|
33
35
|
}
|
|
34
36
|
function run(cmd, args) {
|
|
35
37
|
try {
|
|
36
|
-
return execFileSync(cmd, args, { encoding: 'utf8', timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
38
|
+
return execFileSync(cmd, args, { encoding: 'utf8', env: gitEnv(), timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
37
39
|
} catch { return ''; }
|
|
38
40
|
}
|
|
39
41
|
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
@@ -186,6 +188,21 @@ function labelGuardNotice(cwd) {
|
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
190
|
|
|
191
|
+
// Preflight dell'auth di gh (#328): un GH_TOKEN/GITHUB_TOKEN non valido che
|
|
192
|
+
// oscura il login gh funzionante fa fallire ogni chiamata gh; lo segnaliamo
|
|
193
|
+
// una volta, all'avvio (la lib salta resume e compact). Come reviewNotice: il
|
|
194
|
+
// require e' nel try, ogni errore -> null.
|
|
195
|
+
function authNotice(input) {
|
|
196
|
+
try {
|
|
197
|
+
return require('./lib/gh-auth-preflight.js').check({
|
|
198
|
+
env: process.env,
|
|
199
|
+
source: input && typeof input.source === 'string' ? input.source : undefined,
|
|
200
|
+
}) || null;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
189
206
|
// Legge il JSON di SessionStart da stdin in modo asincrono, con timeout, come
|
|
190
207
|
// gli altri hook: una lettura sincrona di fd 0 si bloccherebbe per sempre su
|
|
191
208
|
// un TTY o su una pipe mai chiusa. Su TTY o input assente si prosegue con {}.
|
|
@@ -212,11 +229,12 @@ function readInput(done) {
|
|
|
212
229
|
function main(input) {
|
|
213
230
|
const cwd = (input && typeof input.cwd === 'string' && input.cwd) || process.cwd();
|
|
214
231
|
|
|
232
|
+
const auth = authNotice(input);
|
|
215
233
|
const review = reviewNotice(cwd);
|
|
216
234
|
const guard = labelGuardNotice(cwd);
|
|
217
235
|
const update = updateNotice();
|
|
218
236
|
|
|
219
|
-
const parts = [review, guard, update].filter(Boolean);
|
|
237
|
+
const parts = [auth, review, guard, update].filter(Boolean);
|
|
220
238
|
if (!parts.length) return;
|
|
221
239
|
|
|
222
240
|
process.stdout.write(JSON.stringify({
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Test dell'hook SessionStart trailhead-check-update.js (notice auth di gh).
|
|
3
|
+
// Run: node trailhead-check-update.test.js
|
|
4
|
+
// Un gh finto in una bin dir tmp (prima nel PATH) applica la precedenza reale.
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
let passed = 0;
|
|
12
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
13
|
+
|
|
14
|
+
const hookPath = path.join(__dirname, 'trailhead-check-update.js');
|
|
15
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'trailhead-check-update-test-'));
|
|
16
|
+
const bin = path.join(tmp, 'bin');
|
|
17
|
+
fs.mkdirSync(bin);
|
|
18
|
+
const fakeGh = `#!${process.execPath}
|
|
19
|
+
const a = process.argv.slice(2);
|
|
20
|
+
if (a[0] !== 'api' || a[1] !== 'user') process.exit(1); // altre chiamate (update check): innocue
|
|
21
|
+
const t = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
22
|
+
const v = t ? t : (process.env.FAKE_GH_STORED || 'bad');
|
|
23
|
+
if (v === 'good') { console.log('octo'); process.exit(0); }
|
|
24
|
+
console.log('{"message":"Bad credentials","status":"401"}');
|
|
25
|
+
console.error('gh: Bad credentials (HTTP 401)');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
`;
|
|
28
|
+
fs.writeFileSync(path.join(bin, 'gh'), fakeGh, { mode: 0o755 });
|
|
29
|
+
|
|
30
|
+
const repo = path.join(tmp, 'repo');
|
|
31
|
+
execFileSync('git', ['init', '-q', repo]);
|
|
32
|
+
|
|
33
|
+
function run(source, extraEnv) {
|
|
34
|
+
const env = { ...process.env, ...extraEnv };
|
|
35
|
+
delete env.GH_TOKEN; delete env.GITHUB_TOKEN;
|
|
36
|
+
Object.assign(env, extraEnv, {
|
|
37
|
+
PATH: `${bin}${path.delimiter}${process.env.PATH}`,
|
|
38
|
+
XDG_CACHE_HOME: path.join(tmp, 'cache'),
|
|
39
|
+
CLAUDE_CONFIG_DIR: path.join(tmp, 'cfg'),
|
|
40
|
+
});
|
|
41
|
+
const input = { cwd: repo };
|
|
42
|
+
if (source) input.source = source;
|
|
43
|
+
const out = execFileSync('node', [hookPath], { input: JSON.stringify(input), encoding: 'utf8', env });
|
|
44
|
+
return out ? JSON.parse(out).hookSpecificOutput.additionalContext : '';
|
|
45
|
+
}
|
|
46
|
+
const AUTH = 'GitHub rejects the';
|
|
47
|
+
|
|
48
|
+
for (const source of ['startup', 'clear', undefined]) {
|
|
49
|
+
const ctx = run(source, { GITHUB_TOKEN: 'bad', FAKE_GH_STORED: 'good' });
|
|
50
|
+
ok(`source ${source}: auth notice surfaced`, ctx.includes(`${AUTH} GITHUB_TOKEN`) && ctx.includes('GITHUB_TOKEN= gh'));
|
|
51
|
+
ok(`source ${source}: token value not leaked`, !ctx.includes('bad'));
|
|
52
|
+
}
|
|
53
|
+
for (const source of ['resume', 'compact']) {
|
|
54
|
+
ok(`source ${source}: no auth notice`, !run(source, { GITHUB_TOKEN: 'bad', FAKE_GH_STORED: 'good' }).includes(AUTH));
|
|
55
|
+
}
|
|
56
|
+
ok('no token var: no auth notice', !run('startup', { FAKE_GH_STORED: 'good' }).includes(AUTH));
|
|
57
|
+
ok('valid token: no auth notice', !run('startup', { GITHUB_TOKEN: 'good' }).includes(AUTH));
|
|
58
|
+
|
|
59
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
60
|
+
console.log(`trailhead-check-update: ${passed} passed`);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// prototype | build | bug | task, and exactly one of `trailhead:map-<n>` |
|
|
11
11
|
// `trailhead:whiteboard` (never both, never two maps). Non-ticket trailhead issues
|
|
12
12
|
// (`trailhead:map`, `trailhead:dashboard`, `trailhead:codebase`,
|
|
13
|
-
// `trailhead:conventions`, and `trailhead:fog` / `trailhead:unverified`) are exempt.
|
|
13
|
+
// `trailhead:conventions`, and `trailhead:fog` / `trailhead:needs-info` / `trailhead:unverified`) are exempt.
|
|
14
14
|
// State labels (blocked, seed, paused, ...) are allowed extras and never count
|
|
15
15
|
// toward the shape. This rule applies in any repo.
|
|
16
16
|
// - A create with NO `trailhead:*` label is blocked too, but only inside a trailhead
|
|
@@ -36,7 +36,7 @@ const { words, parseRedirects } = require('./lib/body-sources.js');
|
|
|
36
36
|
|
|
37
37
|
const PLAIN_SENTINEL = /(?:^|\s)TRAILHEAD_PLAIN_ISSUE=1(?:\s|$)/;
|
|
38
38
|
const TYPES = ['decision', 'research', 'prototype', 'build', 'bug', 'task'];
|
|
39
|
-
const EXEMPT = ['trailhead:map', 'trailhead:dashboard', 'trailhead:codebase', 'trailhead:conventions', 'trailhead:fog', 'trailhead:unverified'];
|
|
39
|
+
const EXEMPT = ['trailhead:map', 'trailhead:dashboard', 'trailhead:codebase', 'trailhead:conventions', 'trailhead:fog', 'trailhead:needs-info', 'trailhead:unverified'];
|
|
40
40
|
const MAP_RE = /^trailhead:map-\d+$/;
|
|
41
41
|
const WHITEBOARD = 'trailhead:whiteboard';
|
|
42
42
|
const TYPE_LIST = TYPES.map((t) => `trailhead:${t}`).join(' | ');
|
|
@@ -104,7 +104,7 @@ ok('a seed without type is blocked', blocked(create(['trailhead:ticket', 'trailh
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// --- non-ticket trailhead issues are exempt ---
|
|
107
|
-
for (const l of ['trailhead:map', 'trailhead:dashboard', 'trailhead:codebase', 'trailhead:conventions', 'trailhead:fog', 'trailhead:unverified']) {
|
|
107
|
+
for (const l of ['trailhead:map', 'trailhead:dashboard', 'trailhead:codebase', 'trailhead:conventions', 'trailhead:fog', 'trailhead:needs-info', 'trailhead:unverified']) {
|
|
108
108
|
ok(`${l} create passes`, !blocked(create([l]), TH));
|
|
109
109
|
}
|
|
110
110
|
ok('map issue with extra labels passes', !blocked(create(['trailhead:map', 'trailhead:paused']), TH));
|