jackherd 0.0.1 → 1.0.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 (250) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +38 -0
  4. package/README.md +1121 -2
  5. package/assets/branding/README.md +62 -0
  6. package/assets/branding/jackherd-icon-256.png +0 -0
  7. package/assets/branding/jackherd-icon-512.png +0 -0
  8. package/assets/branding/jackherd-icon-64.png +0 -0
  9. package/assets/branding/jackherd-icon-small-32.png +0 -0
  10. package/assets/branding/jackherd-icon-small-64.png +0 -0
  11. package/assets/branding/jackherd-icon-small.svg +1 -0
  12. package/assets/branding/jackherd-icon.svg +1 -0
  13. package/assets/branding/jackherd-logo-1024.png +0 -0
  14. package/assets/branding/jackherd-logo-512.png +0 -0
  15. package/assets/branding/jackherd-logo-dark-1024.png +0 -0
  16. package/assets/branding/jackherd-logo-dark-512.png +0 -0
  17. package/assets/branding/jackherd-logo-dark.svg +1 -0
  18. package/assets/branding/jackherd-logo-mark-1024.png +0 -0
  19. package/assets/branding/jackherd-logo-mark-512.png +0 -0
  20. package/assets/branding/jackherd-logo-mark.svg +1 -0
  21. package/assets/branding/jackherd-logo.svg +1 -0
  22. package/dist/chunk-R6MGHCD7.js +93 -0
  23. package/dist/config.d.ts +167 -0
  24. package/dist/config.js +401 -0
  25. package/dist/plan-format.d.ts +325 -0
  26. package/dist/plan-format.js +636 -0
  27. package/docs/lanes.mmd +9 -0
  28. package/docs/lanes.svg +1 -0
  29. package/docs/skills.mmd +10 -0
  30. package/docs/skills.svg +1 -0
  31. package/docs/workflow.svg +172 -0
  32. package/herdr-plugin/herdr-plugin.toml +22 -0
  33. package/herdr-plugin/open-plans.ts +43 -0
  34. package/herdr-plugin/restore-plans.ts +107 -0
  35. package/install.sh +157 -0
  36. package/kit/hooks/hooks/git-guard/decision.ts +438 -0
  37. package/kit/hooks/hooks/git-guard/index.ts +38 -0
  38. package/kit/hooks/hooks/git-guard/shell.ts +407 -0
  39. package/kit/hooks/hooks/watch-guard/background.ts +36 -0
  40. package/kit/hooks/hooks/watch-guard/decision.ts +53 -0
  41. package/kit/hooks/hooks/watch-guard/index.ts +11 -0
  42. package/kit/hooks/hooks/watch-guard/shell-input.ts +32 -0
  43. package/kit/hooks/hooks/watch-guard/starts.ts +154 -0
  44. package/kit/hooks/hooks/watch-guard/text-fallback.ts +40 -0
  45. package/kit/hooks/hooks.json +30 -0
  46. package/kit/hooks/statusline.sh +72 -0
  47. package/kit/review/agents/devops.md +11 -0
  48. package/kit/review/agents/public-content.md +39 -0
  49. package/kit/review/agents/security-audit.md +11 -0
  50. package/kit/review/agents/uncle-bob.md +11 -0
  51. package/kit/review/skills/devops/SKILL.md +50 -0
  52. package/kit/review/skills/diagnose/SKILL.md +140 -0
  53. package/kit/review/skills/diagnose/references/feedback-loops.md +59 -0
  54. package/kit/review/skills/diagnose/scripts/hitl-loop.template.sh +71 -0
  55. package/kit/review/skills/pre-commit/SKILL.md +100 -0
  56. package/kit/review/skills/review/SKILL.md +43 -0
  57. package/kit/review/skills/security-audit/SKILL.md +65 -0
  58. package/kit/review/skills/uncle-bob/SKILL.md +43 -0
  59. package/kit/rules/rules/kit/arc42.md +89 -0
  60. package/kit/rules/rules/kit/architecture-files.md +87 -0
  61. package/kit/rules/rules/kit/code-style.md +159 -0
  62. package/kit/rules/rules/kit/pull-requests.md +59 -0
  63. package/kit/rules/rules/kit/testing.md +91 -0
  64. package/kit/rules/rules/kit/writing-for-agents.md +110 -0
  65. package/kit/workflow/agents/explore.md +25 -0
  66. package/kit/workflow/agents/implement.md +16 -0
  67. package/kit/workflow/agents/plan.md +13 -0
  68. package/kit/workflow/agents/spec-review.md +96 -0
  69. package/kit/workflow/skills/follow/SKILL.md +48 -0
  70. package/kit/workflow/skills/grill/SKILL.md +60 -0
  71. package/kit/workflow/skills/implement/SKILL.md +107 -0
  72. package/kit/workflow/skills/orchestrate/SKILL.md +565 -0
  73. package/kit/workflow/skills/orchestrate/handover/decision.ts +247 -0
  74. package/kit/workflow/skills/orchestrate/handover/index.ts +171 -0
  75. package/kit/workflow/skills/plan/SKILL.md +118 -0
  76. package/kit/workflow/skills/plan/references/template-English.md +92 -0
  77. package/kit/workflow/skills/plan/references/template-German.md +92 -0
  78. package/kit/workflow/skills/start/SKILL.md +47 -0
  79. package/package.json +63 -5
  80. package/schemas/jackherd.schema.json +601 -0
  81. package/src/check/changes.ts +45 -0
  82. package/src/check/command-line.ts +12 -0
  83. package/src/check/command.ts +136 -0
  84. package/src/check/deferral-arrows.ts +41 -0
  85. package/src/check/execute.ts +29 -0
  86. package/src/check/glob.ts +15 -0
  87. package/src/check/plan-structure-check.ts +135 -0
  88. package/src/check/plan.ts +106 -0
  89. package/src/check/public-content-check.ts +32 -0
  90. package/src/check/public-content-facts.ts +177 -0
  91. package/src/check/public-content.ts +216 -0
  92. package/src/check/report.ts +55 -0
  93. package/src/check/result.ts +42 -0
  94. package/src/check/roots.ts +36 -0
  95. package/src/check/routing.ts +22 -0
  96. package/src/check/schema.ts +83 -0
  97. package/src/check/settings.ts +44 -0
  98. package/src/claude/paths.ts +14 -0
  99. package/src/cli.ts +46 -0
  100. package/src/config/command.ts +45 -0
  101. package/src/config/detect.ts +196 -0
  102. package/src/config/index.ts +2 -0
  103. package/src/config/json-schema.ts +5 -0
  104. package/src/config/legacy-names.ts +56 -0
  105. package/src/config/link-kinds.ts +25 -0
  106. package/src/config/location.ts +46 -0
  107. package/src/config/read-config.ts +42 -0
  108. package/src/config/repository-facts.ts +19 -0
  109. package/src/config/repository-values.ts +46 -0
  110. package/src/config/schema.ts +302 -0
  111. package/src/context/command.ts +39 -0
  112. package/src/context/coordinator-name.ts +16 -0
  113. package/src/context/fill.ts +69 -0
  114. package/src/context/index.ts +10 -0
  115. package/src/context/level.ts +22 -0
  116. package/src/context/options.ts +29 -0
  117. package/src/context/report.ts +42 -0
  118. package/src/context/session-log.ts +91 -0
  119. package/src/plan-format/drift.ts +69 -0
  120. package/src/plan-format/header.ts +27 -0
  121. package/src/plan-format/index.ts +9 -0
  122. package/src/plan-format/lanes.ts +128 -0
  123. package/src/plan-format/markdown.ts +98 -0
  124. package/src/plan-format/phase-tally.ts +55 -0
  125. package/src/plan-format/phases.ts +224 -0
  126. package/src/plan-format/plan-structure.ts +150 -0
  127. package/src/plan-format/progress.ts +202 -0
  128. package/src/plan-format/sprint-document.ts +69 -0
  129. package/src/plan-format/terminal-text.ts +28 -0
  130. package/src/plans/board-choice.ts +60 -0
  131. package/src/plans/board-pick.ts +72 -0
  132. package/src/plans/board-rows.ts +62 -0
  133. package/src/plans/command.ts +60 -0
  134. package/src/plans/ensure-panes.ts +71 -0
  135. package/src/plans/error-text.ts +5 -0
  136. package/src/plans/fetch.ts +96 -0
  137. package/src/plans/git.ts +145 -0
  138. package/src/plans/github.ts +35 -0
  139. package/src/plans/lane-discovery.ts +34 -0
  140. package/src/plans/lane-plans.ts +80 -0
  141. package/src/plans/layout.ts +881 -0
  142. package/src/plans/link-chips.ts +74 -0
  143. package/src/plans/live.ts +336 -0
  144. package/src/plans/markdown-html.ts +140 -0
  145. package/src/plans/options.ts +15 -0
  146. package/src/plans/phase-source.ts +71 -0
  147. package/src/plans/plan-chips.ts +75 -0
  148. package/src/plans/plan-panes.ts +206 -0
  149. package/src/plans/planned.ts +117 -0
  150. package/src/plans/pull-requests.ts +107 -0
  151. package/src/plans/restart-decision.ts +42 -0
  152. package/src/plans/selection.ts +352 -0
  153. package/src/plans/settings.ts +68 -0
  154. package/src/plans/single-flight.ts +25 -0
  155. package/src/plans/snapshot.ts +300 -0
  156. package/src/plans/sprint-export.ts +103 -0
  157. package/src/plans/sprint-html.ts +301 -0
  158. package/src/plans/sprint-page.ts +287 -0
  159. package/src/plans/supervisor.ts +124 -0
  160. package/src/plans/symbols.ts +54 -0
  161. package/src/plans/texts.ts +103 -0
  162. package/src/plans/unlisted-lanes.ts +45 -0
  163. package/src/plans/view.ts +68 -0
  164. package/src/plans/worked-phases.ts +53 -0
  165. package/src/sync/checksum.ts +13 -0
  166. package/src/sync/choice.ts +14 -0
  167. package/src/sync/command-context.ts +113 -0
  168. package/src/sync/command.ts +94 -0
  169. package/src/sync/error-message.ts +1 -0
  170. package/src/sync/gitignore.ts +45 -0
  171. package/src/sync/global-kit.ts +176 -0
  172. package/src/sync/herdr-plugin.ts +175 -0
  173. package/src/sync/if-changed.ts +109 -0
  174. package/src/sync/init-command.ts +239 -0
  175. package/src/sync/init.ts +136 -0
  176. package/src/sync/kit-hooks.ts +18 -0
  177. package/src/sync/kit-path.ts +77 -0
  178. package/src/sync/kit-source.ts +169 -0
  179. package/src/sync/kit.ts +9 -0
  180. package/src/sync/legacy-files.ts +116 -0
  181. package/src/sync/manifest.ts +44 -0
  182. package/src/sync/next-steps.ts +42 -0
  183. package/src/sync/plan-file.ts +154 -0
  184. package/src/sync/plan-hooks.ts +165 -0
  185. package/src/sync/project-fs.ts +132 -0
  186. package/src/sync/render.ts +60 -0
  187. package/src/sync/report.ts +96 -0
  188. package/src/sync/settings-hooks.ts +90 -0
  189. package/src/sync/settings-migration.ts +94 -0
  190. package/src/sync/settings-offer.ts +168 -0
  191. package/src/sync/setup-questions.ts +70 -0
  192. package/src/sync/setup.ts +52 -0
  193. package/src/sync/sync-plan.ts +120 -0
  194. package/src/sync/sync-result.ts +9 -0
  195. package/src/sync/sync.ts +307 -0
  196. package/src/sync/tools.ts +52 -0
  197. package/src/terminal/herdr.ts +91 -0
  198. package/src/terminal/index.ts +13 -0
  199. package/src/terminal/none.ts +7 -0
  200. package/src/terminal/open.ts +39 -0
  201. package/src/terminal/text.ts +159 -0
  202. package/src/terminal/types.ts +30 -0
  203. package/src/terminal/width.ts +90 -0
  204. package/src/update/command.ts +65 -0
  205. package/src/update/package-manager.ts +56 -0
  206. package/src/update/update.ts +105 -0
  207. package/src/version/installed-version.ts +32 -0
  208. package/src/version/release.ts +22 -0
  209. package/src/version/skill-diff.ts +63 -0
  210. package/src/version/started-kit.ts +14 -0
  211. package/src/vocabulary/index.ts +132 -0
  212. package/src/watch/base-tip.ts +18 -0
  213. package/src/watch/command.ts +919 -0
  214. package/src/watch/coordinator-fill.ts +76 -0
  215. package/src/watch/feedback-read.ts +73 -0
  216. package/src/watch/feedback.ts +308 -0
  217. package/src/watch/gh.ts +22 -0
  218. package/src/watch/github-read.ts +175 -0
  219. package/src/watch/herdr.ts +11 -0
  220. package/src/watch/json-guards.ts +9 -0
  221. package/src/watch/kit-update.ts +18 -0
  222. package/src/watch/lane-fill.ts +36 -0
  223. package/src/watch/lane-message.ts +99 -0
  224. package/src/watch/lane-progress.ts +44 -0
  225. package/src/watch/lane-report-file.ts +53 -0
  226. package/src/watch/lane-report.ts +270 -0
  227. package/src/watch/lane-search.ts +41 -0
  228. package/src/watch/lane-sources.ts +46 -0
  229. package/src/watch/lane-watch.ts +121 -0
  230. package/src/watch/monitor-tool.ts +19 -0
  231. package/src/watch/options.ts +79 -0
  232. package/src/watch/plan-badges-read.ts +30 -0
  233. package/src/watch/plan-badges.ts +66 -0
  234. package/src/watch/plan-views.ts +59 -0
  235. package/src/watch/preview-read.ts +60 -0
  236. package/src/watch/previews.ts +234 -0
  237. package/src/watch/pull-requests.ts +117 -0
  238. package/src/watch/single-report.ts +15 -0
  239. package/src/watch/skill-change.ts +61 -0
  240. package/src/watch/source-dry-run-command.ts +35 -0
  241. package/src/watch/source-dry-run.ts +124 -0
  242. package/src/watch/source-fields.ts +9 -0
  243. package/src/watch/source-path.ts +40 -0
  244. package/src/watch/source-run.ts +90 -0
  245. package/src/watch/sources.ts +344 -0
  246. package/src/watch/stream-output.ts +14 -0
  247. package/src/watch/texts.ts +76 -0
  248. package/src/watch/watch-events.ts +399 -0
  249. package/src/watch/watcher-lock.ts +145 -0
  250. package/src/watch/workflow-runs.ts +96 -0
@@ -0,0 +1,154 @@
1
+ import { basename } from 'node:path'
2
+ import { parseShell } from '../git-guard/shell'
3
+ import { hasBackgroundJob } from './background'
4
+ import { SHELLS, shellHeredocBodies, shellHereStrings, withoutHeredocBodies } from './shell-input'
5
+
6
+ /**
7
+ * One start of the watcher in a call: whether it streams, and whether the call (or the script around the
8
+ * start) sends any command off with `&`, which ends a background call before the watcher reports.
9
+ */
10
+ export type WatcherStart = { stream: boolean; callHasBackgroundJob: boolean }
11
+
12
+ /** Thrown past `MAX_DEPTH` nested scripts; the caller answers it like an unreadable call. */
13
+ export class NestingTooDeep extends Error {}
14
+
15
+ const WATCH = 'watch'
16
+ /** `watch source <name> --dry-run` runs one source once and ends; it starts no watcher. */
17
+ const SOURCE = 'source'
18
+ const STREAM = '--stream'
19
+ /** The program names of the CLI: `jh`, and `agent-kit` until 2.0.0 (an old install and the shell around it still say it). */
20
+ const BINS = new Set(['jh', 'agent-kit'])
21
+ const MAX_DEPTH = 8
22
+
23
+ /** Words that may stand before the program of a simple command without being it. */
24
+ const KEYWORDS = new Set(['!', '{', 'if', 'then', 'elif', 'else', 'while', 'until', 'do', 'function', 'coproc'])
25
+ const WRAPPERS = new Set([
26
+ 'exec',
27
+ 'nohup',
28
+ 'env',
29
+ 'time',
30
+ 'setsid',
31
+ 'command',
32
+ 'nice',
33
+ 'stdbuf',
34
+ 'sudo',
35
+ 'timeout',
36
+ 'busybox',
37
+ ])
38
+ /** Programs besides the shells that run their `-c` argument: `su -c`, `script -c`, `flock <file> -c`. */
39
+ const COMMAND_RUNNERS = new Set([...SHELLS, 'su', 'runuser', 'script', 'flock'])
40
+ const PACKAGE_RUNNERS = new Set(['bunx', 'npx', 'pnpx'])
41
+ /** Package managers that run a package after `dlx` or `exec`; `yarn` also runs one directly. */
42
+ const PACKAGE_MANAGERS = new Set(['pnpm', 'yarn', 'npm'])
43
+ const PACKAGE_SUBCOMMANDS = new Set(['dlx', 'exec'])
44
+
45
+ const ASSIGNMENT = /^\w+=/
46
+ const DURATION = /^\d+(?:\.\d+)?[smhd]?$/
47
+ const PACKAGE = /^(?:(?:@crossdelta\/)?agent-kit|jackherd|jh)(?:@\S+)?$/
48
+ const SCRIPT = /(?:^|\/)(?:cli\.ts|agent-kit|jh)$/
49
+ const COMMAND_FLAG = /^-[a-zA-Z]*c[a-zA-Z]*$/
50
+
51
+ const isOption = (word: string | undefined): boolean => word?.startsWith('-') === true
52
+
53
+ /**
54
+ * A word before the program: a keyword, a wrapper (`nohup`, `timeout`, …), an assignment, an option, a
55
+ * duration, the word after an option, which may be its value (`timeout -s INT`, `nice -n 10`), or the
56
+ * name after `function`.
57
+ */
58
+ const isPrefixWord = (words: readonly string[], index: number): boolean => {
59
+ const word = words[index] ?? ''
60
+ const previous = words[index - 1]
61
+ return (
62
+ KEYWORDS.has(word) ||
63
+ WRAPPERS.has(basename(word)) ||
64
+ ASSIGNMENT.test(word) ||
65
+ isOption(word) ||
66
+ DURATION.test(word) ||
67
+ isOption(previous) ||
68
+ previous === 'function'
69
+ )
70
+ }
71
+
72
+ const afterOptions = (words: readonly string[], from: number): number => {
73
+ let index = from
74
+ while (isOption(words[index])) index++
75
+ return index
76
+ }
77
+
78
+ const subcommandAt = (words: readonly string[], index: number): number =>
79
+ words[index] === WATCH && words[index + 1] !== SOURCE ? index : -1
80
+
81
+ /** `<package> watch`, also `<package> <bin> watch` as `npx -p <package> <bin>` spells it. */
82
+ const packageAt = (words: readonly string[], index: number): number => {
83
+ if (!PACKAGE.test(words[index] ?? '')) return -1
84
+ return subcommandAt(words, BINS.has(words[index + 1] ?? '') ? index + 2 : index + 1)
85
+ }
86
+
87
+ /** `bun x <package> watch`, or `bun [flags] [run] <…/cli.ts | …/jh | …/agent-kit> watch` with the script anywhere after `bun`. */
88
+ const bunWatchIndex = (words: readonly string[], from: number): number => {
89
+ const first = afterOptions(words, from)
90
+ if (words[first] === 'x') return packageAt(words, afterOptions(words, first + 1))
91
+ for (let index = from; index < words.length; index++) {
92
+ if (SCRIPT.test(words[index] ?? '') && words[index + 1] === WATCH) return subcommandAt(words, index + 1)
93
+ }
94
+ return -1
95
+ }
96
+
97
+ const packageManagerWatchIndex = (words: readonly string[], from: number): number =>
98
+ PACKAGE_SUBCOMMANDS.has(words[from] ?? '') ? packageAt(words, afterOptions(words, from + 1)) : packageAt(words, from)
99
+
100
+ /** The index of `watch` when the program at `index` runs the watcher, else -1. */
101
+ const watchIndexOf = (words: readonly string[], index: number): number => {
102
+ const program = basename(words[index] ?? '')
103
+ if (BINS.has(program)) return subcommandAt(words, index + 1)
104
+ if (PACKAGE_RUNNERS.has(program)) return packageAt(words, afterOptions(words, index + 1))
105
+ if (PACKAGE_MANAGERS.has(program)) return packageManagerWatchIndex(words, index + 1)
106
+ if (program === 'bun') return bunWatchIndex(words, index + 1)
107
+ return -1
108
+ }
109
+
110
+ /** The script a program at `index` runs as a command line: `bash -c '<script>'`, `su -c …`, `eval <words>`, else null. */
111
+ const nestedScriptOf = (words: readonly string[], index: number): string | null => {
112
+ const program = basename(words[index] ?? '')
113
+ if (program === 'eval') return words.slice(index + 1).join(' ')
114
+ if (!COMMAND_RUNNERS.has(program)) return null
115
+ const flag = words.findIndex((word, position) => position > index && COMMAND_FLAG.test(word))
116
+ return flag === -1 ? null : (words[afterOptions(words, flag + 1)] ?? null)
117
+ }
118
+
119
+ /**
120
+ * The starts in one simple command. The program is tried at every position the words before it allow
121
+ * (wrappers, assignments, options and their values), so `env -i bun …` finds `bun` behind the option.
122
+ */
123
+ const startsOfCommand = (words: readonly string[], callHasBackgroundJob: boolean, depth: number): WatcherStart[] => {
124
+ for (let index = 0; index < words.length; index++) {
125
+ const watch = watchIndexOf(words, index)
126
+ if (watch !== -1) return [{ stream: words.slice(watch + 1).includes(STREAM), callHasBackgroundJob }]
127
+ const nested = nestedScriptOf(words, index)
128
+ if (nested !== null) return startsIn(nested, callHasBackgroundJob, depth + 1)
129
+ if (!isPrefixWord(words, index)) return []
130
+ }
131
+ return []
132
+ }
133
+
134
+ /**
135
+ * Every start of the watcher a shell would run from `script`, at any nesting the script spells out: its
136
+ * simple commands (substitutions included, which `parseShell` lists), `bash -c` and `eval` arguments, and
137
+ * here-documents or here-strings fed to a shell. Throws `ShellParseError` or `NestingTooDeep`.
138
+ */
139
+ export const startsIn = (script: string, callHasBackgroundJob: boolean, depth = 0): WatcherStart[] => {
140
+ if (depth > MAX_DEPTH) throw new NestingTooDeep(`more than ${MAX_DEPTH} nested scripts`)
141
+ const backgroundJob = callHasBackgroundJob || hasBackgroundJob(withoutHeredocBodies(script))
142
+ return [
143
+ ...parseShell(script).flatMap((command) =>
144
+ startsOfCommand(
145
+ command.map((word) => word.value),
146
+ backgroundJob,
147
+ depth,
148
+ ),
149
+ ),
150
+ ...[...shellHeredocBodies(script), ...shellHereStrings(script)].flatMap((body) =>
151
+ startsIn(body, backgroundJob, depth + 1),
152
+ ),
153
+ ]
154
+ }
@@ -0,0 +1,40 @@
1
+ import { hasBackgroundJob } from './background'
2
+ import { withoutHeredocBodies } from './shell-input'
3
+ import type { WatcherStart } from './starts'
4
+
5
+ /**
6
+ * A path in front of a program is at most 256 characters (`\S{0,256}` below): an unbounded `\S*` rescans a long
7
+ * run of characters without a space from every position, which is quadratic and can outlast the hook's timeout.
8
+ */
9
+ const COMMAND_POSITION = /(?:^|[;&|({`'"!\n]|\b(?:then|do|else|elif|if|while|until)\b)\s*/.source
10
+ const ENV_ASSIGNMENT = /\w+=\S*\s+/.source
11
+ const WRAPPER = /(?:exec|nohup|env|time|setsid|command|nice|stdbuf|sudo)\s+(?:-\S+\s+(?:[\w.=:+]+\s+)?)*/.source
12
+ const TIMEOUT = /timeout\s+(?:-\S+\s+(?:[\w.=:+]+\s+)?)*\d\S*\s+/.source
13
+ const PACKAGE_RUNNER =
14
+ /(?:\S{0,256}\/)?(?:bunx|npx|pnpx|bun\s+x|pnpm\s+dlx|yarn\s+dlx)\s+(?:-\S+\s+)*['"]?(?:(?:@crossdelta\/)?agent-kit|jackherd|jh)(?:@[^\s'"]+)?['"]?/
15
+ .source
16
+ const BUN_SCRIPT = /(?:\S{0,256}\/)?bun\s+[^;&|\n]*?\S*(?:\/|\b)(?:cli\.ts|agent-kit|jh)['"]?/.source
17
+ const BIN_PATH = /(?:\S{0,256}\/)?(?:agent-kit|jh)/.source
18
+ /** `watch source …` runs one source once and starts no watcher. */
19
+ const WATCH_WORD = /\s+watch(?=\s|$|[;&|)`'"])(?!\s+source(?=\s|$))/.source
20
+
21
+ /** Any start-shaped text, at a command position or right after a quote, so a quoted start counts too. */
22
+ const TEXT_START = new RegExp(
23
+ `${COMMAND_POSITION}(?:${ENV_ASSIGNMENT}|${WRAPPER}|${TIMEOUT})*(?:${PACKAGE_RUNNER}|${BUN_SCRIPT}|${BIN_PATH})${WATCH_WORD}`,
24
+ 'g',
25
+ )
26
+ const ARGUMENTS_TO_COMMAND_END = /^[^;&|\n)`'"]*/
27
+ const STREAM_FLAG = /(?:^|\s)--stream(?=\s|$)/
28
+
29
+ /**
30
+ * The starts in a call the parser cannot read (an unterminated quote, a substitution inside an expanding
31
+ * here-document, nesting past the limit): a match of the start's text anywhere, so a doubtful call is
32
+ * refused rather than passed.
33
+ */
34
+ export const startsInText = (script: string): WatcherStart[] => {
35
+ const callHasBackgroundJob = hasBackgroundJob(withoutHeredocBodies(script))
36
+ return [...script.matchAll(TEXT_START)].map((match) => {
37
+ const rest = script.slice((match.index ?? 0) + match[0].length)
38
+ return { stream: STREAM_FLAG.test(rest.match(ARGUMENTS_TO_COMMAND_END)?.[0] ?? ''), callHasBackgroundJob }
39
+ })
40
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "format-on-edit": {
3
+ "event": "PostToolUse",
4
+ "matcher": "Edit|Write",
5
+ "handler": {
6
+ "type": "command",
7
+ "command": "file=$(jq -r '.tool_input.file_path // empty'); [ -n \"$file\" ] || exit 0; file=$(realpath -- \"$file\" 2>/dev/null) || exit 0; project=$(realpath -- \"$CLAUDE_PROJECT_DIR\" 2>/dev/null) || exit 0; case \"$file\" in \"$project\"/*) ;; *) exit 0;; esac; biome=\"$project/node_modules/.bin/biome\"; [ -x \"$biome\" ] || exit 0; case \"$file\" in *.ts|*.tsx|*.js|*.jsx|*.json|*.jsonc) \"$biome\" format --write -- \"$file\" 2>/dev/null || true;; esac; exit 0",
8
+ "timeout": 10
9
+ }
10
+ },
11
+ "git-guard": {
12
+ "event": "PreToolUse",
13
+ "matcher": "Bash",
14
+ "handler": {
15
+ "type": "command",
16
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/git-guard/index.ts",
17
+ "timeout": 10,
18
+ "onFailure": "block"
19
+ }
20
+ },
21
+ "watch-guard": {
22
+ "event": "PreToolUse",
23
+ "matcher": "Bash",
24
+ "handler": {
25
+ "type": "command",
26
+ "command": "input=$(cat); case \"$input\" in *watch*) printf '%s' \"$input\" | bun \"$CLAUDE_PROJECT_DIR/.claude/hooks/watch-guard/index.ts\";; esac",
27
+ "timeout": 10
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # Status line for Claude Code: model, context use and session cost. Shipped by jackherd.
3
+ # Enable it in .claude/settings.json: "statusLine": { "type": "command", "command": ".claude/statusline.sh" }
4
+ # Colours follow plans.context of the jackherd config: the lane thresholds inside a worktree, the
5
+ # coordinator's elsewhere; without a config, yellow from 60 % and red from 85 %.
6
+ input=$(cat)
7
+
8
+ model=$(jq -r '.model.display_name // .model.id // "?"' <<<"$input")
9
+ pct=$(jq -r '.context_window.used_percentage // 0' <<<"$input")
10
+ cost=$(jq -r '.cost.total_cost_usd // 0' <<<"$input")
11
+ cwd=$(jq -r '.workspace.current_dir // .cwd // empty' <<<"$input")
12
+ pct_int=${pct%.*}
13
+ [ -z "$pct_int" ] && pct_int=0
14
+
15
+ case '{{kit.responseLanguage}}' in
16
+ German) label='Kontext'; compact_hint='/compact oder /clear' ;;
17
+ *) label='Context'; compact_hint='/compact or /clear' ;;
18
+ esac
19
+
20
+ # The nearest .jackherd/config.json above the working directory, never the one in the home directory
21
+ # (that is the global config), else the global ~/.jackherd/config.json. Until 2.0.0 the old
22
+ # .claude/agent-kit.json is read where the new file is missing.
23
+ config_in() {
24
+ if [ -f "$1/.jackherd/config.json" ]; then
25
+ printf '%s' "$1/.jackherd/config.json"
26
+ elif [ -f "$1/.claude/agent-kit.json" ]; then
27
+ printf '%s' "$1/.claude/agent-kit.json"
28
+ else
29
+ return 1
30
+ fi
31
+ }
32
+
33
+ config_file() {
34
+ local dir=$1
35
+ while [ -n "$dir" ] && [ "$dir" != "/" ]; do
36
+ if [ "$dir" != "$HOME" ] && config_in "$dir"; then
37
+ return
38
+ fi
39
+ dir=$(dirname "$dir")
40
+ done
41
+ config_in "$HOME"
42
+ }
43
+
44
+ warn_pct=60
45
+ alert_pct=85
46
+ config=$(config_file "${cwd:-$PWD}")
47
+ if [ -n "$config" ]; then
48
+ worktrees=$(jq -r '.worktreeDirectory // ".claude/worktrees"' "$config" 2>/dev/null)
49
+ case "${cwd:-$PWD}/" in
50
+ */"$worktrees"/*) block='.plans.context' ;;
51
+ *) block='.plans.context.coordinator' ;;
52
+ esac
53
+ # A share of the window (0.6) as a whole percent; empty when the config does not set it.
54
+ warn=$(jq -r "($block.warnAt // empty) * 100 | floor" "$config" 2>/dev/null)
55
+ alert=$(jq -r "($block.alertAt // empty) * 100 | floor" "$config" 2>/dev/null)
56
+ [[ "$warn" =~ ^[0-9]+$ ]] && warn_pct=$warn
57
+ [[ "$alert" =~ ^[0-9]+$ ]] && alert_pct=$alert
58
+ fi
59
+
60
+ reset='\033[0m'
61
+ if [ "$pct_int" -ge "$alert_pct" ]; then
62
+ color='\033[31m'
63
+ hint=" ⚠ ${compact_hint}"
64
+ elif [ "$pct_int" -ge "$warn_pct" ]; then
65
+ color='\033[33m'
66
+ hint=''
67
+ else
68
+ color='\033[32m'
69
+ hint=''
70
+ fi
71
+
72
+ LC_NUMERIC=C printf "%s | ${color}%s %s%%${reset} | \$%.2f%s" "$model" "$label" "$pct_int" "$cost" "$hint"
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: devops
3
+ description: 'Infrastructure Review — Security, Resources, Reliability. Use for infrastructure-as-code changes.'
4
+ model: {{kit.model.review}}
5
+ tools: Read, Grep, Glob
6
+ user-invocable: false
7
+ ---
8
+
9
+ Read [`.claude/skills/devops/SKILL.md`](../skills/devops/SKILL.md) and follow it, including its first step: the project supplement in `.claude/project/devops.md` if it exists. The skill carries the checks and the output format and is the single source of truth for both — this file only fixes the model and the tool grant for a dispatched run.
10
+
11
+ You receive a `git diff` or a list of changed files in your prompt. Your output is the report; applying a fix is the caller's decision.
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: public-content
3
+ description: 'Public Content Review: does a change to files a package publishes give away a specific project, client, team or person? Use when /pre-commit routes a change to published files (check.publicContent).'
4
+ model: {{kit.model.review}}
5
+ tools: Read
6
+ user-invocable: false
7
+ ---
8
+
9
+ If `.claude/project/public-content.md` exists, read it first. It extends this agent for this project and wins where the two disagree.
10
+
11
+ You receive the `git diff` of files a package publishes: what strangers will read once it is released. Judge the diff **as one of those strangers**: open no other file of the repository (the supplement above is the only exception). What you would need the repository to understand is exactly what the published text must not lean on.
12
+
13
+ `jh check` has already scanned the added lines for the names of neighbouring repositories, their organisations and ticket keys, and the names of this project; it reports where, never the name. You judge what no list of names catches.
14
+
15
+ ## What is a finding
16
+
17
+ An added line is a finding when a reader could tell from it, without the repository:
18
+
19
+ - **A project or product:** its name, a feature set or screen only it has, its domain or hosts, its table, event or service names.
20
+ - **A client, employer, team or person:** names, handles, initials in an example, an e-mail address, a role that points at one person.
21
+ - **An incident:** a date, a PR or ticket number, a version, "last week", a number of orders or users that ties a lesson to one event.
22
+ - **Internal infrastructure:** hosts, environment names that are not placeholders, account, project or tenant identifiers, machine paths.
23
+ - **An example copied from a real codebase:** a file path, a query, a log line or an error message that is plainly someone's code, not an illustration.
24
+
25
+ Not a finding: generic illustrations (`apps/web`, `orders`, `search-index`, `example.com`), placeholders (`<owner>`, `<port>`), and the tools a published package names openly (its runtime, its package manager, the editor or agent it extends).
26
+
27
+ A lesson stays; its origin goes. Rewrite "the export broke on 3 March in the shop" as the rule it taught, with an invented example.
28
+
29
+ ## Output
30
+
31
+ ```markdown
32
+ ## Public Content — PASS | FIX FIRST
33
+
34
+ | File:line | Gives away | Line | Rewrite |
35
+ |---|---|---|---|
36
+ | kit/rules/x.md:14 | a client (name) | "…" | "…" |
37
+ ```
38
+
39
+ `PASS` when no added line gives anything away; one row per finding otherwise. Quote the line so the author can find it, and offer a neutral rewrite. Your output is the report; applying a rewrite is the caller's decision.
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: security-audit
3
+ description: 'Use when a change touches auth, tenant isolation, access policies, API keys, secrets, user input, database queries, message payloads or infrastructure: OWASP Top 10, secrets exposure, authz, input validation, injection, path traversal.'
4
+ model: {{kit.model.review}}
5
+ tools: Read, Grep, Glob
6
+ user-invocable: false
7
+ ---
8
+
9
+ Read [`.claude/skills/security-audit/SKILL.md`](../skills/security-audit/SKILL.md) and follow it, including its first step: the project supplement in `.claude/project/security-audit.md` if it exists. The skill carries the checks and the output format and is the single source of truth for both — this file only fixes the model and the tool grant for a dispatched run.
10
+
11
+ You receive a `git diff` or a list of changed files in your prompt. Your output is the report; applying a fix is the caller's decision.
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: uncle-bob
3
+ description: 'Clean Architecture Review — SOLID, Dependency Direction, Small Functions. Use for backend and service code review.'
4
+ model: {{kit.model.review}}
5
+ tools: Read, Grep, Glob
6
+ user-invocable: false
7
+ ---
8
+
9
+ Read [`.claude/skills/uncle-bob/SKILL.md`](../skills/uncle-bob/SKILL.md) and follow it, including its first step: the project supplement in `.claude/project/uncle-bob.md` if it exists. The skill carries the checks and the output format and is the single source of truth for both — this file only fixes the model and the tool grant for a dispatched run.
10
+
11
+ You receive a `git diff` or a list of changed files in your prompt. Your output is the report; applying a fix is the caller's decision.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: devops
3
+ description: 'Infrastructure Review — Security, Resources, Reliability. Use for infrastructure-as-code changes.'
4
+ ---
5
+
6
+ If `.claude/project/devops.md` exists, read it first. It adds this project's infrastructure conventions (sizing rules, stream migrations, stack names) and wins where the two disagree.
7
+
8
+ You are a **DevOps Engineer** — pragmatic, security-conscious, thinking in failure modes.
9
+
10
+ ## Your Task
11
+
12
+ Review the infrastructure changes through these lenses:
13
+
14
+ ### Security
15
+
16
+ 1. **Secrets**: Credentials stay out of code, config and terminal output; they come from the secret store or encrypted stack config.
17
+ 2. **Network Policies**: The service is reachable only on the ports it needs.
18
+ 3. **RBAC**: Minimal rights. Service accounts get only the permissions they need.
19
+ 4. **Image Tags**: Pinned versions or SHA digests, never `latest`.
20
+
21
+ ### Resources
22
+
23
+ 5. **Requests vs Limits**: Sensible for the workload — not too generous (waste), not too tight (OOM).
24
+ 6. **Runtime Heap**: A Node.js heap limit (`--max-old-space-size`) fits inside the container memory limit with headroom (about 75 %).
25
+ 7. **Replicas**: At least 2 for production. Rolling update strategy configured.
26
+
27
+ ### Reliability
28
+
29
+ 8. **Idempotency**: Can the deployment (`pulumi up` or equivalent) run repeatedly without side effects?
30
+ 9. **Rollback Plan**: What happens if the deployment fails? Is there a rollback strategy?
31
+ 10. **Health Checks**: Readiness and liveness probes configured and sensible.
32
+ 11. **Message Streams**: A change to stream subjects or retention ships with its migration.
33
+
34
+ ### Infrastructure as Code
35
+
36
+ 12. **Stack Isolation**: Staging and production configs properly separated.
37
+ 13. **No hardcoded values**: Everything via stack config or config objects.
38
+ 14. **Dependencies**: Resource dependencies explicit where needed (`dependsOn`).
39
+
40
+ ## Your Output Format
41
+
42
+ In {{kit.responseLanguage}}. For each finding:
43
+
44
+ - **Category** (Security / Resources / Reliability / Infrastructure as Code)
45
+ - **Severity** (CRITICAL / WARNING / INFO)
46
+ - **File + line**
47
+ - **Problem** (one sentence)
48
+ - **Fix** (concrete proposal)
49
+
50
+ At the end: DEPLOY / DEPLOY WITH FIXES / BLOCK with rationale.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: diagnose
3
+ description: 'Diagnosis loop for hard bugs, flaky failures and performance regressions. Use when the user says "debug"/"diagnose", reports something broken, throwing, hanging or slow, or when a bug does not reproduce reliably.'
4
+ ---
5
+
6
+ <!--
7
+ Adapted from https://github.com/mattpocock/skills (skills/engineering/diagnosing-bugs),
8
+ commit 8b78b53, MIT License, Copyright (c) 2026 Matt Pocock. See NOTICE in jackherd.
9
+ Phase structure and the feedback-loop discipline are his; the loop recipes, redaction rules
10
+ and hand-off targets are the kit's.
11
+ -->
12
+
13
+ If `.claude/project/diagnose.md` exists, read it first. It adds this project's loop recipes (its database, its client state, its hardware) and wins where the two disagree.
14
+
15
+ You are the **Diagnostician**. A discipline for hard bugs. Skip a phase only with an explicit reason.
16
+
17
+ Read the relevant plan in `{{kit.planDirectory}}/` and any `ARCHITECTURE.md` of the touched unit before you form a picture of the system. Recalled memory and past incidents are leads, never evidence — a bug that looks like a known one still has to go red in a loop of its own.
18
+
19
+ ## Redaction
20
+
21
+ This skill has you show commands, outputs and captured artifacts. **Redact every secret before you show it** — write `<REDACTED>` in its place.
22
+
23
+ Build loops against environment variables so the credential stays in the environment: load the env file in the shell and reference `$VAR`; a secret is never a literal command argument. Captured artifacts carry auth headers and tokens — quote only the lines that carry the signal. Database rows carry customer data: quote the columns you are reasoning about, not the row.
24
+
25
+ If the redacted output is not enough to diagnose the bug, say so and ask the user.
26
+
27
+ ## Phase 1 — Build a feedback loop
28
+
29
+ **This is the skill.** Everything else is mechanical. With a **tight** pass/fail signal that goes red on _this_ bug, you will find the cause; bisection, hypothesis-testing and instrumentation all just consume it. Without one, no amount of reading code will save you.
30
+
31
+ Spend disproportionate effort here. Be aggressive, be creative, refuse to give up.
32
+
33
+ **The loop recipes live in [`references/feedback-loops.md`](references/feedback-loops.md)** — Bun tests, a real browser via Playwright, HTTP via curl, message subjects, differential runs. Read it before you build a loop; it also carries the two Bun invocation traps that make a healthy suite look broken.
34
+
35
+ ### Tighten the loop
36
+
37
+ Treat the loop as a product. Once you have _a_ loop, **tighten** it:
38
+
39
+ - Faster — cache setup, skip unrelated init, narrow the test scope to one module directory.
40
+ - Sharper — assert the specific symptom, not "did not crash".
41
+ - More deterministic — pin time, seed randomness, isolate storage, freeze the network.
42
+
43
+ A 30-second flaky loop is barely better than no loop. A 2-second deterministic one is a superpower.
44
+
45
+ ### Non-deterministic bugs
46
+
47
+ The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow the timing window, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it is.
48
+
49
+ ### When you genuinely cannot build a loop
50
+
51
+ Stop and say so. List what you tried, then ask the user for one of: access to the environment that reproduces it, a redacted artifact (HAR, log dump, screen recording with timestamps), or permission to add temporary instrumentation to a deployed environment. Hypotheses wait until a loop exists.
52
+
53
+ ### Completion criterion — a loop that goes red
54
+
55
+ Phase 1 is done when you can name **one command** — a test invocation, a script path, a curl — that you have **already run at least once** (show the invocation and its output, redacted), and that is:
56
+
57
+ - [ ] **Red-capable** — it drives the actual bug code path and asserts the **user's exact symptom**, so it goes red on this bug and green once fixed. Not "runs without erroring".
58
+ - [ ] **Deterministic** — same verdict every run. For flaky bugs: a pinned, high reproduction rate.
59
+ - [ ] **Fast** — seconds, not minutes.
60
+ - [ ] **Agent-runnable** — you can run it unattended. A human enters the loop only through `scripts/hitl-loop.template.sh`.
61
+
62
+ If you catch yourself reading code to build a theory before this command exists, **stop**. Jumping to a hypothesis is the exact failure this skill prevents. No red command, no Phase 2.
63
+
64
+ ## Phase 2 — Reproduce and minimise
65
+
66
+ Run the loop. Watch it go red.
67
+
68
+ Confirm:
69
+
70
+ - [ ] The loop produces the failure the **user** described, not a different one that happens to live nearby. Wrong bug, wrong fix.
71
+ - [ ] It reproduces across runs, or at a rate high enough to debug against.
72
+ - [ ] You captured the exact symptom — error message, wrong value, measured timing — so later phases can verify the fix addresses it.
73
+
74
+ Then shrink the repro to the **smallest scenario that still goes red**. Cut inputs, callers, config, data and steps **one at a time**, re-running after each cut. A minimal repro shrinks the hypothesis space in Phase 3 and becomes the regression test in Phase 5.
75
+
76
+ Done when every remaining element is load-bearing: removing any one of them turns the loop green.
77
+
78
+ ## Phase 3 — Hypothesise
79
+
80
+ Generate **3 to 5 ranked hypotheses before testing any of them**. Single-hypothesis generation anchors on the first plausible idea.
81
+
82
+ Each one is **falsifiable** — state the prediction it makes:
83
+
84
+ > "If X is the cause, then changing Y makes the bug disappear / changing Z makes it worse."
85
+
86
+ A hypothesis with no stated prediction is a vibe. Sharpen it or drop it.
87
+
88
+ **Show the ranked list to the user before testing.** They re-rank it instantly when they know something you cannot see ("we deployed exactly that yesterday", "we already ruled out the second one"). Cheap checkpoint, large saving. Proceed with your ranking if the user is away.
89
+
90
+ ## Phase 4 — Instrument
91
+
92
+ Every probe maps to a specific prediction from Phase 3. **Change one variable at a time.**
93
+
94
+ Order of preference:
95
+
96
+ 1. Inspect state directly where the environment allows it — a breakpoint or a REPL beats ten logs.
97
+ 2. Targeted logs at the boundary that distinguishes two hypotheses.
98
+ 3. Never "log everything and grep".
99
+
100
+ **Tag every debug log** with a unique prefix, `[DEBUG-a4f2]`. Cleanup becomes a single grep. Untagged logs survive the fix; tagged ones die with it.
101
+
102
+ **Performance branch.** For a regression in speed, logs are usually the wrong tool. Establish a baseline measurement first — a timing harness, `performance.now()`, a profiler run, the query plan — then bisect against it. Measure first, fix second.
103
+
104
+ ## Phase 5 — Fix and regression test
105
+
106
+ Write the regression test **before the fix** — but only where a **correct seam** exists.
107
+
108
+ A correct seam exercises the **real bug pattern as it occurs at the call site**. If the only reachable seam is too shallow — a unit test where the bug needs two collaborating callers, a mocked adapter where the bug lives in the real I/O — a test there buys false confidence.
109
+
110
+ **If no correct seam exists, that is itself the finding.** Note it: the architecture is preventing the bug from being locked down. Carry it into Phase 6.
111
+
112
+ Where a seam exists, follow the decision tree in `.claude/rules/kit/testing.md` (a bug fix always earns a regression test) and:
113
+
114
+ 1. Turn the minimised repro into a failing `*.test.ts` next to the source.
115
+ 2. Watch it fail.
116
+ 3. Apply the fix — the smallest change that turns it green, no opportunistic refactoring.
117
+ 4. Watch it pass.
118
+ 5. Re-run the Phase 1 loop against the **original**, un-minimised scenario.
119
+
120
+ For a fix inside a shared package, run the tests of the packages that consume it too. Shared code has blast radius.
121
+
122
+ ## Phase 6 — Cleanup and post-mortem
123
+
124
+ Required before you declare it done:
125
+
126
+ - [ ] The original repro no longer reproduces — re-run the Phase 1 loop, show the output
127
+ - [ ] The regression test passes, or the absence of a seam is written down
128
+ - [ ] All `[DEBUG-...]` instrumentation removed — grep the prefix to prove it
129
+ - [ ] Throwaway harnesses and fixtures deleted
130
+ - [ ] The hypothesis that turned out correct is stated in the commit body, in prose, so the next person to hit this learns the cause and not just the diff
131
+
132
+ **Then ask: what would have prevented this bug?**
133
+
134
+ - A design problem — no test seam, tangled callers, hidden coupling → hand the specifics to `/uncle-bob`.
135
+ - A rule that should hold project-wide → propose a line for the matching file in `.claude/rules/`.
136
+ - More than a single change can carry → propose a plan in `{{kit.planDirectory}}/` rather than widening this fix.
137
+
138
+ Make the recommendation **after** the fix is in. You know more now than when you started.
139
+
140
+ Report in {{kit.responseLanguage}}.
@@ -0,0 +1,59 @@
1
+ # Feedback Loops
2
+
3
+ Recipes for Phase 1 of `/diagnose`. Pick the one that reaches the bug's code path, then tighten it. The project supplement in `.claude/project/diagnose.md` adds the recipes only this project has.
4
+
5
+ Ordered by how tight the loop tends to be. Reach for the first one that can actually go red on the bug — a Bun test that cannot see the bug is worse than a slower loop that can.
6
+
7
+ ## 1. Bun test — the default
8
+
9
+ The tightest loop available. Milliseconds, deterministic, unattended.
10
+
11
+ ```bash
12
+ cd packages/<package> && bun test src/<module>/<file>.test.ts
13
+ ```
14
+
15
+ Two invocation traps make a healthy suite look broken:
16
+
17
+ - **`EMFILE` / `ProcessFdQuotaExceeded` while dev servers run.** From the workspace root Bun walks the whole monorepo and can exhaust the kernel's descriptor table while watchers hold it. It reports `0 pass / 1 error`, not an assertion failure. Fix: `cd` into the package or module directory first; leave the user's dev servers running.
18
+ - **Run from the package root, not below it.** Bun reads `tsconfig.json` — and with it `jsxImportSource` — from the **working directory**. One level too deep and every `.tsx` a test touches compiles against React's runtime: `Cannot find module 'react/jsx-dev-runtime'`. It reads like a missing dependency in untouched code. Re-run from the package root before concluding anything.
19
+
20
+ Both failures are environmental. Neither is evidence about the bug.
21
+
22
+ ## 2. The rendered app — Playwright
23
+
24
+ For anything that needs the real page: client state, offline behaviour, gestures, timing between requests.
25
+
26
+ - **Fresh auth state.** A stored login that has expired produces a silent redirect to the login page, which presents as a data bug.
27
+ - **One login per run.** Specs inherit a stored state; parallel live logins race each other and hit auth rate limits.
28
+ - **Wait for content, not for load.** Data that arrives after boot needs a poll for real content with a generous timeout; an assertion on emptiness right after navigation proves nothing.
29
+
30
+ For exploring a live page, a browser the user is already logged into is faster. Move to a spec file once the loop has to be repeatable or needs isolation or network control.
31
+
32
+ ## 3. HTTP against a running service — curl
33
+
34
+ ```bash
35
+ curl -sS -H "Authorization: Bearer $SERVICE_TOKEN" http://localhost:<port>/<route> | jq .
36
+ ```
37
+
38
+ Load the token from the environment; never inline it as a literal. Redact bearer headers out of anything you show.
39
+
40
+ ## 4. Events that never arrive — NATS subjects
41
+
42
+ When a handler does not fire, check whether the event landed in a different stream before you read the handler's code. The NATS monitoring endpoint answers this in one call:
43
+
44
+ ```bash
45
+ curl -sS http://localhost:8222/jsz | jq '.account_details[].stream_detail[] | {name, subjects: .config.subjects, messages: .state.messages}'
46
+ curl -sS http://localhost:8222/healthz
47
+ ```
48
+
49
+ A subject that sits in the wrong stream is a configuration fault, not a code fault, and restarting the container does not fix it.
50
+
51
+ ## 5. Differential loop — two environments, before versus after
52
+
53
+ When the same input behaves differently in two places, run both and diff the outputs. This is the fastest route for "works on staging, wrong in production" and for regressions that appeared between two deploys.
54
+
55
+ For a regression between two known commits, automate the check and let `git bisect run` consume it.
56
+
57
+ ## 6. A human in the loop
58
+
59
+ When a trigger genuinely needs a person — a device, a physical button, a native build — drive them with `scripts/hitl-loop.template.sh` so the run stays structured and its output feeds back. Script the reachability probes first, and note which state you asked them to observe **before** they change it — a restart that clears the symptom also destroys the evidence.