jackherd 1.8.1 → 1.10.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 (261) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/INSTALL.md +3 -1
  3. package/README.md +75 -64
  4. package/dist/{chunk-OU2KALXP.js → chunk-5MQBCUQ5.js} +24 -4
  5. package/dist/config.d.ts +45 -4
  6. package/dist/config.js +84 -27
  7. package/dist/plan-format.d.ts +63 -7
  8. package/dist/plan-format.js +66 -12
  9. package/docs/workflow-dark.svg +67 -63
  10. package/docs/workflow.svg +67 -63
  11. package/herdr-plugin/herdr-plugin.toml +1 -1
  12. package/install.sh +1 -1
  13. package/kit/hooks/ask-rules.json +15 -0
  14. package/kit/hooks/hooks/approval-gate/index.ts +22 -4
  15. package/kit/hooks/hooks/approval-gate/run.ts +29 -6
  16. package/kit/hooks/hooks/approval-gate/subagent.ts +18 -0
  17. package/kit/hooks/hooks/ask-hint/decision.ts +82 -14
  18. package/kit/hooks/hooks/ask-hint/index.ts +15 -19
  19. package/kit/hooks/hooks/ask-hint/settings.ts +3 -18
  20. package/kit/hooks/hooks/git-guard/decision.ts +50 -102
  21. package/kit/hooks/hooks/merge-guard/always-ask.ts +63 -0
  22. package/kit/hooks/hooks/merge-guard/{plan-approval-settings.ts → board-settings.ts} +21 -67
  23. package/kit/hooks/hooks/merge-guard/check-free.ts +20 -0
  24. package/kit/hooks/hooks/merge-guard/decision.ts +127 -140
  25. package/kit/hooks/hooks/merge-guard/index.ts +6 -3
  26. package/kit/hooks/hooks/merge-guard/lane-plan.ts +79 -0
  27. package/kit/hooks/hooks/merge-guard/run.ts +59 -54
  28. package/kit/hooks/hooks/merge-guard/settings.ts +13 -1
  29. package/kit/hooks/hooks/merge-guard/standing-approval.ts +142 -0
  30. package/kit/hooks/hooks/open-question/index.ts +26 -0
  31. package/kit/hooks/hooks/open-question/record.ts +135 -0
  32. package/kit/hooks/hooks/open-question/run.ts +52 -0
  33. package/kit/hooks/hooks/open-question/store.ts +39 -0
  34. package/kit/hooks/hooks/sent-file/index.ts +19 -0
  35. package/kit/hooks/hooks/sent-file/record.ts +128 -0
  36. package/kit/hooks/hooks/sent-file/run.ts +48 -0
  37. package/kit/hooks/hooks/sent-file/store.ts +9 -0
  38. package/kit/hooks/hooks/shared/board-lanes.ts +15 -2
  39. package/kit/hooks/hooks/shared/config.ts +7 -0
  40. package/kit/hooks/hooks/shared/dialog-answers.ts +9 -9
  41. package/kit/hooks/hooks/shared/files.ts +7 -0
  42. package/kit/hooks/hooks/shared/lane.ts +1 -1
  43. package/kit/hooks/hooks/shared/newest-verdict.ts +10 -0
  44. package/kit/hooks/hooks/shared/private-directory.ts +56 -9
  45. package/kit/hooks/hooks/shared/program-position.ts +2 -75
  46. package/kit/hooks/hooks/shared/shell-aliases.ts +1 -71
  47. package/kit/hooks/hooks/{ask-hint → shared}/transcript.ts +37 -6
  48. package/kit/hooks/hooks/{merge-guard → shared}/xargs-program.ts +10 -8
  49. package/kit/hooks/hooks/task-hint/decision.ts +49 -0
  50. package/kit/hooks/hooks/task-hint/index.ts +79 -0
  51. package/kit/hooks/hooks/task-hint/run.ts +62 -0
  52. package/kit/hooks/hooks/task-hint/store.ts +51 -0
  53. package/kit/hooks/hooks.json +21 -4
  54. package/kit/hooks/statusline.sh +1 -1
  55. package/kit/review/agents/skill-review.md +34 -0
  56. package/kit/review/skills/pre-commit/SKILL.md +1 -1
  57. package/kit/workflow/skills/follow/SKILL.md +1 -8
  58. package/kit/workflow/skills/implement/SKILL.md +10 -6
  59. package/kit/workflow/skills/orchestrate/SKILL.md +213 -412
  60. package/kit/workflow/skills/orchestrate/handover/decision.ts +43 -8
  61. package/kit/workflow/skills/orchestrate/handover/index.ts +32 -4
  62. package/kit/workflow/skills/orchestrate/handover/standing-approval.ts +84 -0
  63. package/kit/workflow/skills/orchestrate/references/acceptance.md +15 -0
  64. package/kit/workflow/skills/orchestrate/references/close-lane.md +17 -0
  65. package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +43 -17
  66. package/kit/workflow/skills/orchestrate/references/draft-review.md +19 -0
  67. package/kit/workflow/skills/orchestrate/references/events.md +86 -0
  68. package/kit/workflow/skills/orchestrate/references/herd.md +6 -6
  69. package/kit/workflow/skills/orchestrate/references/lane-handover.md +18 -0
  70. package/kit/workflow/skills/orchestrate/references/lane-session.md +25 -0
  71. package/kit/workflow/skills/orchestrate/references/lane-setup.md +37 -0
  72. package/kit/workflow/skills/orchestrate/references/plan-commit.md +22 -10
  73. package/kit/workflow/skills/orchestrate/references/sensitive-input.md +5 -7
  74. package/kit/workflow/skills/plan/SKILL.md +6 -5
  75. package/kit/workflow/skills/plan/references/template-English.md +4 -4
  76. package/kit/workflow/skills/plan/references/template-German.md +4 -4
  77. package/package.json +4 -1
  78. package/recipes/README.md +2 -1
  79. package/recipes/sources.md +1 -1
  80. package/schemas/jackherd.schema.json +85 -7
  81. package/src/check/agent-text-reviewers.ts +23 -0
  82. package/src/check/command.ts +2 -0
  83. package/src/check/plan-structure-check.ts +49 -2
  84. package/src/check/plan.ts +8 -1
  85. package/src/check/schema.ts +9 -0
  86. package/src/check/skill-budget-defaults.ts +55 -0
  87. package/src/check/skill-budgets.ts +185 -0
  88. package/src/cli.ts +4 -0
  89. package/src/config/auto-window.ts +8 -0
  90. package/src/config/link-kinds.ts +7 -1
  91. package/src/config/schema.ts +80 -26
  92. package/src/context/fill.ts +12 -0
  93. package/src/context/session-log.ts +43 -1
  94. package/src/lane/close-board.ts +106 -0
  95. package/src/lane/close-check.ts +183 -0
  96. package/src/lane/close-command-line.ts +11 -0
  97. package/src/lane/close-command.ts +217 -0
  98. package/src/lane/close-options.ts +43 -0
  99. package/src/lane/close-survey.ts +83 -0
  100. package/src/lane/close.ts +141 -0
  101. package/src/lane/command.ts +13 -0
  102. package/src/lane-prompt/build.ts +74 -0
  103. package/src/lane-prompt/command.ts +87 -0
  104. package/src/lane-prompt/input.ts +116 -0
  105. package/src/lane-prompt/options.ts +68 -0
  106. package/src/lane-prompt/texts.ts +101 -0
  107. package/src/plan-format/clock.ts +12 -0
  108. package/src/plan-format/dependencies.ts +9 -9
  109. package/src/plan-format/index.ts +1 -0
  110. package/src/plan-format/lanes.ts +30 -5
  111. package/src/plan-format/short-names.ts +71 -0
  112. package/src/plan-format/terminal-text.ts +4 -0
  113. package/src/plan-format/waiting.ts +53 -0
  114. package/src/plans/active-tasks.ts +60 -0
  115. package/src/plans/appointment-rows.ts +59 -0
  116. package/src/plans/ask-coordinator.ts +77 -0
  117. package/src/plans/ask-target.ts +25 -0
  118. package/src/plans/child-stop.ts +51 -0
  119. package/src/plans/closable-records.ts +34 -0
  120. package/src/plans/command.ts +3 -2
  121. package/src/plans/current-phase.ts +17 -7
  122. package/src/plans/current-task.ts +63 -5
  123. package/src/plans/fetch.ts +26 -3
  124. package/src/plans/git.ts +19 -5
  125. package/src/plans/herd-claim.ts +173 -0
  126. package/src/plans/herd-memory.ts +36 -25
  127. package/src/plans/lane-plans.ts +44 -13
  128. package/src/plans/lane-standing.ts +9 -4
  129. package/src/plans/lane-summary.ts +367 -92
  130. package/src/plans/lane-tickets.ts +15 -0
  131. package/src/plans/layout-phase.ts +80 -19
  132. package/src/plans/layout-summary.ts +163 -74
  133. package/src/plans/layout.ts +472 -114
  134. package/src/plans/link-chips.ts +1 -0
  135. package/src/plans/live.ts +119 -10
  136. package/src/plans/phase-ids.ts +8 -0
  137. package/src/plans/phase-models.ts +1 -1
  138. package/src/plans/phase-ranges.ts +45 -0
  139. package/src/plans/phase-source.ts +15 -0
  140. package/src/plans/plan-cache.ts +28 -0
  141. package/src/plans/plan-heads.ts +54 -0
  142. package/src/plans/pull-requests.ts +23 -2
  143. package/src/plans/question-records.ts +48 -0
  144. package/src/plans/questions-read.ts +13 -0
  145. package/src/plans/read-cache.ts +8 -0
  146. package/src/plans/record-id.ts +4 -0
  147. package/src/plans/restart-decision.ts +13 -4
  148. package/src/plans/selection.ts +117 -34
  149. package/src/plans/sent-files-read.ts +13 -0
  150. package/src/plans/sent-files.ts +114 -0
  151. package/src/plans/settings.ts +2 -1
  152. package/src/plans/signalled-phase.ts +42 -0
  153. package/src/plans/signals-read.ts +62 -0
  154. package/src/plans/snapshot.ts +279 -60
  155. package/src/plans/sprint-export.ts +11 -11
  156. package/src/plans/sprint-page.ts +5 -5
  157. package/src/plans/subagent-phases.ts +95 -0
  158. package/src/plans/subagent-tasks.ts +51 -0
  159. package/src/plans/subagents-read.ts +131 -0
  160. package/src/plans/subagents.ts +190 -0
  161. package/src/plans/supervisor.ts +14 -4
  162. package/src/plans/symbols.ts +3 -0
  163. package/src/plans/task-name.ts +34 -0
  164. package/src/plans/texts.ts +36 -14
  165. package/src/plans/unlisted-lanes.ts +10 -5
  166. package/src/plans/view.ts +20 -12
  167. package/src/plans/waiting-click.ts +231 -0
  168. package/src/plans/waiting-records.ts +39 -0
  169. package/src/plans/waiting-window.ts +8 -0
  170. package/src/preview/command.ts +105 -0
  171. package/src/preview/embed.ts +209 -0
  172. package/src/preview/images.ts +89 -0
  173. package/src/preview/open.ts +12 -0
  174. package/src/preview/options.ts +32 -0
  175. package/src/preview/page.ts +89 -0
  176. package/src/preview/render.ts +41 -0
  177. package/src/settled/board-row.ts +1 -2
  178. package/src/shared/is-record.ts +2 -0
  179. package/src/shared/private-location.ts +89 -0
  180. package/src/shared/private-records.ts +48 -0
  181. package/src/shared/stat-cache.ts +48 -0
  182. package/src/sync/kit-source.ts +29 -10
  183. package/src/sync/kit.ts +2 -0
  184. package/src/sync/manifest.ts +2 -0
  185. package/src/sync/plan-ask-rules.ts +94 -0
  186. package/src/sync/plan-hooks.ts +2 -0
  187. package/src/sync/render.ts +3 -3
  188. package/src/sync/settings-hooks.ts +4 -1
  189. package/src/sync/sync-plan.ts +1 -0
  190. package/src/sync/sync.ts +24 -3
  191. package/src/task-signals/agent.ts +15 -0
  192. package/src/task-signals/claude-read.ts +89 -0
  193. package/src/task-signals/claude-signals.ts +112 -0
  194. package/src/task-signals/command.ts +71 -0
  195. package/src/task-signals/fixtures/session.jsonl +7 -0
  196. package/src/task-signals/matcher.ts +96 -0
  197. package/src/task-signals/record-store.ts +40 -0
  198. package/src/task-signals/record.ts +68 -0
  199. package/src/task-signals/text.ts +13 -0
  200. package/src/task-signals/types.ts +14 -0
  201. package/src/terminal/open.ts +13 -6
  202. package/src/terminal/text.ts +16 -4
  203. package/src/terminal/width.ts +2 -1
  204. package/src/version/skill-diff.ts +46 -0
  205. package/src/vocabulary/index.ts +44 -6
  206. package/src/waiting/command.ts +50 -0
  207. package/src/watch/appointments.ts +2 -4
  208. package/src/watch/approvals.ts +2 -2
  209. package/src/watch/asking-lanes.ts +40 -0
  210. package/src/watch/base-tip.ts +11 -0
  211. package/src/watch/command.ts +401 -90
  212. package/src/watch/failed-tests.ts +56 -10
  213. package/src/watch/failure-read.ts +18 -6
  214. package/src/watch/failures.ts +29 -5
  215. package/src/watch/feedback.ts +1 -1
  216. package/src/watch/gh-process.ts +9 -3
  217. package/src/watch/heartbeat-progress.ts +6 -18
  218. package/src/watch/json-guards.ts +0 -3
  219. package/src/watch/kit-update.ts +28 -9
  220. package/src/watch/lane-fill.ts +1 -1
  221. package/src/watch/lane-model.ts +1 -1
  222. package/src/watch/lane-name.ts +6 -0
  223. package/src/watch/lane-parallel.ts +1 -1
  224. package/src/watch/lane-progress.ts +58 -14
  225. package/src/watch/lane-report-file.ts +20 -5
  226. package/src/watch/lane-report.ts +37 -5
  227. package/src/watch/lane-rest.ts +28 -0
  228. package/src/watch/lane-sources.ts +31 -3
  229. package/src/watch/main-checkout.ts +34 -0
  230. package/src/watch/plan-badges-read.ts +13 -7
  231. package/src/watch/plan-badges.ts +9 -2
  232. package/src/watch/previews.ts +1 -1
  233. package/src/watch/pull-requests.ts +8 -3
  234. package/src/watch/reference-paths.ts +87 -0
  235. package/src/watch/running-subagents.ts +43 -0
  236. package/src/watch/skill-change.ts +124 -32
  237. package/src/watch/source-change.ts +48 -0
  238. package/src/watch/source-path.ts +1 -1
  239. package/src/watch/sources.ts +7 -4
  240. package/src/watch/stream-output.ts +14 -8
  241. package/src/watch/task-parallel-auto.ts +208 -0
  242. package/src/watch/task-parallel-data-paths.ts +46 -0
  243. package/src/watch/task-parallel-input.ts +108 -0
  244. package/src/watch/task-parallel-read.ts +151 -0
  245. package/src/watch/task-parallel-touched.ts +39 -0
  246. package/src/watch/task-parallel.ts +235 -0
  247. package/src/watch/task-signal-missing.ts +69 -0
  248. package/src/watch/task-signal-read.ts +29 -0
  249. package/src/watch/texts.ts +10 -0
  250. package/src/watch/update-available.ts +6 -3
  251. package/src/watch/waiting-line.ts +19 -0
  252. package/src/watch/watch-events.ts +36 -1
  253. package/src/watch/watcher-lock.ts +25 -29
  254. package/src/watch/workflow-runs.ts +2 -1
  255. package/kit/hooks/hooks/merge-guard/plan-approval-record.ts +0 -23
  256. package/kit/hooks/hooks/merge-guard/plan-approval-sources.ts +0 -196
  257. package/kit/hooks/hooks/merge-guard/plan-approval.ts +0 -168
  258. package/kit/hooks/hooks/merge-guard/plan-growth.ts +0 -40
  259. package/kit/hooks/hooks/merge-guard/plan-pointers.ts +0 -194
  260. package/kit/hooks/hooks/shared/command-text-runners.ts +0 -71
  261. package/kit/hooks/hooks/shared/plan-open-phases.ts +0 -114
@@ -35,7 +35,9 @@ Skip this when the request already carries its decisions (a plan for a bug fix w
35
35
 
36
36
  The closing documentation section reads `## N — …` with rows `N.1` to `N.5`: replace `N` with the number of that phase (`## 10 — …`, `10.1`). A literal `N` is no phase to the herd, which shows it as "not in the plan".
37
37
 
38
- A checkbox moved on purpose to a later phase stays where it is, unticked, and ends in the target: `- [ ] Update the documentation → P9`. The herd counts it neither as open nor as done in its own phase and lists it as open in phase 9, so a phase whose only open box was moved on reads as finished. The arrow counts only at the end of the line; `jh check` refuses an open box in a changed plan that carries it anywhere else (`- [ ] → P9 Update …`). Once a phase's tasks are ticked on the base branch and the change does not tick it further, every box still open in it, gate boxes included, is ticked or moved on this way: `jh check` warns on a changed plan that leaves one.
38
+ A checkbox moved on purpose to a later phase stays where it is, unticked, and ends in the target: `- [ ] Update the documentation → P9`. The herd counts it neither as open nor as done in its own phase and lists it as open in phase 9. The arrow stands only at the end of the line. Once a phase's tasks are ticked on the base branch, every box still open in it, gate boxes included, is ticked or moved on this way; `jh check` checks both.
39
+
40
+ A task starts with its short name: `- [ ] **Name**: …` (a review-gate item keeps its own form). The name has 40 characters at most and names the subject and the result, what is different afterwards (`Heartbeat shows progress without ticks`, `` `ask-hint` finds questions anywhere ``); the how, the conditions, the origin and the evidence follow the colon. A phase's title is its name, its context goes into the origin line. A task the user adds later: `- [ ] **Name**: … (User <DD.MM.> <HH:MM>)`.
39
41
 
40
42
  What the template's tables mean:
41
43
 
@@ -67,7 +69,7 @@ Every phase gets the review gate block from the template at its end. Add the rev
67
69
  - **Shared code checked:** For every planned component it is documented why it is built new instead of reused or extended.
68
70
  - **Tests defined:** The tests section is filled in or carries an explicit rationale why none are needed (decision tree in `.claude/rules/kit/testing.md`).
69
71
  - **Model recommendations defined:** `## {{kit.heading.implementation}}` starts with the model table, one row per phase, justified by error cost × complexity.
70
- - **Parallelism defined (3+ phases):** the dependencies table follows it. Check it against the phase bodies: every migration, schema bump and new event in a phase appears as a serial marker; every shared file appears under {{kit.heading.writes}}. A dependency that exists only in your head lets `/implement` start two phases that collide. Every phase gets a row, a phase added later too: `jh check` warns about a phase without one.
72
+ - **Parallelism defined (3+ phases):** the dependencies table follows it, a row for every phase, one added later too. Every migration, schema bump and new event of a phase is a serial marker, every shared file stands under {{kit.heading.writes}}; otherwise `/implement` starts two phases that collide.
71
73
  - **arc42 check (after the plan exists):** apply the filter in `.claude/rules/kit/arc42.md` to the finished plan and **ask the user concretely** which sections should be extended. On "yes", add a concrete task to `## {{kit.heading.documentation}}`.
72
74
  - **Architecture check (after the plan exists):** apply the filter in `.claude/rules/kit/architecture-files.md` — new ownership, a new invariant or an accepted risk each trigger a concrete question about the unit's `ARCHITECTURE.md`.
73
75
  - **arc42 closing phase (3+ phases or a new service/package):** end the plan with the architecture documentation phase from the template. Add arc42 sections beyond it only when they deliver real value.
@@ -94,9 +96,8 @@ Derive the rows from the dependencies table and the model table, nothing else:
94
96
  - **Plan:** a Markdown link to the plan, its path relative to the board's directory (`../<file>.md` for a plan
95
97
  directly under `{{kit.planDirectory}}`).
96
98
  Plans stay where they are; the row only links them.
97
- - **Lane name:** short kebab case, unique on the board. `/orchestrate` names the worktree directory and the herdr
98
- agent after it, so the worktree directory and the herdr agent carry the same name; a name that differs leaves the
99
- watcher reporting the lane as `no agent`.
99
+ - **Lane name:** short kebab case, unique on the board; the worktree directory and the herdr agent carry the same name
100
+ (`/orchestrate` § 2).
100
101
  - **Port:** `–`; the coordinator sets it when the lane starts, unless the project supplement assigns ports here.
101
102
 
102
103
  Show the rows as the table they become: `## {{kit.heading.lanes}}` with the columns lane, port, plan, phases, starts
@@ -42,8 +42,8 @@ What changes concretely? Files, modules, flows.
42
42
 
43
43
  ### {{kit.heading.phase}} 1: ...
44
44
 
45
- - [ ] Step 1
46
- - [ ] Step 2
45
+ - [ ] **Name of step 1**: how, conditions, origin, evidence
46
+ - [ ] **Name of step 2**: how, conditions, origin, evidence
47
47
 
48
48
  **Review gate {{kit.heading.phase}} 1:**
49
49
 
@@ -67,13 +67,13 @@ Which tests are needed? Decide with the decision tree in `.claude/rules/kit/test
67
67
 
68
68
  What gets documented after completion?
69
69
 
70
- - [ ] ...
70
+ - [ ] **Name**: ...
71
71
 
72
72
  ## Follow-ups
73
73
 
74
74
  > Ideas that come up during the work: noted here, not built in this plan, presented together once the release PR is merged.
75
75
 
76
- - [ ] The idea in one sentence (User <DD.MM.> <HH:MM>, where it came up)
76
+ - [ ] **Name**: the idea in one sentence (User <DD.MM.> <HH:MM>, where it came up)
77
77
 
78
78
  ## Rejected Alternatives
79
79
 
@@ -42,8 +42,8 @@ Was ändert sich konkret? Dateien, Module, Flows.
42
42
 
43
43
  ### {{kit.heading.phase}} 1: ...
44
44
 
45
- - [ ] Schritt 1
46
- - [ ] Schritt 2
45
+ - [ ] **Name von Schritt 1**: Wie, Bedingungen, Herkunft, Beleg
46
+ - [ ] **Name von Schritt 2**: Wie, Bedingungen, Herkunft, Beleg
47
47
 
48
48
  **Review-Gate {{kit.heading.phase}} 1:**
49
49
 
@@ -67,13 +67,13 @@ Welche Tests sind nötig? Entscheidung nach dem Entscheidungsbaum in `.claude/ru
67
67
 
68
68
  Was wird nach Abschluss dokumentiert?
69
69
 
70
- - [ ] ...
70
+ - [ ] **Name**: ...
71
71
 
72
72
  ## Follow-ups
73
73
 
74
74
  > Ideen, die während der Umsetzung aufkommen: hier vorgemerkt, nicht in diesem Plan gebaut, gesammelt vorgelegt, sobald der Release-PR gemergt ist.
75
75
 
76
- - [ ] Idee in einem Satz (User <TT.MM.> <HH:MM>, wo sie aufkam)
76
+ - [ ] **Name**: Idee in einem Satz (User <TT.MM.> <HH:MM>, wo sie aufkam)
77
77
 
78
78
  ## Verworfene Alternativen
79
79
 
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "jackherd",
3
- "version": "1.8.1",
3
+ "version": "1.10.0",
4
4
  "keywords": [
5
5
  "claude-code",
6
6
  "coding-agents",
7
7
  "ai-agents",
8
8
  "agent-skills",
9
9
  "subagents",
10
+ "multi-agent",
10
11
  "orchestration",
12
+ "plan-driven",
11
13
  "git-worktree",
12
14
  "herdr",
13
15
  "planning",
@@ -48,6 +50,7 @@
48
50
  "!docs/gen",
49
51
  "assets",
50
52
  "!assets/icons/build.ts",
53
+ "!assets/icons/version.ts",
51
54
  "!assets/branding/outline.ts",
52
55
  "recipes",
53
56
  "README.md",
package/recipes/README.md CHANGED
@@ -15,7 +15,8 @@ Each recipe is one `watch.sources` entry for the jackherd watcher: put in your k
15
15
  ## Rules every recipe follows
16
16
 
17
17
  - **A token never goes into an argument:** an argument shows in the process list. Where a tool keeps its own login, it is used; where a token sits in an environment variable, a small TypeScript script run by Bun (which runs wherever `jh` does) puts it from `process.env` straight into the request header.
18
- - **A script fails with an exit code**, never with an empty answer the watcher would take for a source with nothing new, and it prints neither the token nor the body of a failed answer. Every request and every tool call is bounded with `AbortSignal.timeout(20_000)`, well under the 30 seconds a source may run.
18
+ - **A script prints one JSON value on stdout and nothing else.**
19
+ - **A script fails with an exit code**, never with an empty answer the watcher would take for a source with nothing new, and it prints neither the token nor the body of a failed answer. A 200 that carries an `errors` list, as GraphQL answers, is a failure too. Every request and every tool call is bounded with `AbortSignal.timeout(20_000)`, well under the 30 seconds a source may run.
19
20
  - **The scripts are files of their own** under [`sources/`](sources/). Copy one to `scripts/sources/<name>.ts` in your repository, the path the entry's `command` names. The closing `export {}` makes the file a module, so the type check accepts top-level `await` and the names do not clash between scripts.
20
21
  - **A recipe that needs no script** (`gh …`, `curl` on a public page, `curl --netrc`) stays a direct command.
21
22
  - **`bun` as the first word, never a path to it.** The watcher starts a source without a shell and reads `bun` to mean the Bun that runs the watcher itself, so the entry needs no absolute path, which would differ from machine to machine, even when Bun is not on the `PATH` (installed in `~/.bun/bin` only, say).
@@ -36,7 +36,7 @@ Source reports are off unless the config lists `watch.sources`. The watcher cann
36
36
 
37
37
  `items` is the path of the list in the output (without it the output is the list), `map` picks each entry's fields: `id` is required, `updatedAt` too unless a content field (`title`, `author`, `body`, `url`) or `reportOn` can tell a change, and `title`, `url`, `body`, `author` and `status` (a ticket's status, below) are optional; any further field you name (`priority`, `assignee`; a letter, then letters, digits and `_`) is read for `reportOn`. A path is `.` for the whole value or a chain of `.key`, `[n]` and `[-n]` (counted from the end), nothing more; for anything else, put a script in between.
38
38
 
39
- What counts as news: an unknown `id` reports `new`; `updated` only when what you read changed, a short hash over `title`, `author`, `body` and `url`. A change of the time alone (a label, a sprint, a status) wakes nobody; where none of the four is mapped, `updatedAt` is the content, and then it is required. An entry whose `author` is in `ignoreAuthors` (case ignored) is recorded but wakes nobody, so your own comment does not. The first successful run of a source is a baseline, and the saved state holds per source only each id with its hash (with `reportOn` also the values of those fields, which may name people, such as an assignee), the 1000 seen most recently, so a restart repeats nothing and an entry that falls out of a time window and comes back is not new. Everything one GitHub read finds goes into one report, at most `reportLimit` entries per source (`5`), the rest as one line `=== source tickets +3 more`. The body shows its first line with text that is no Markdown heading (a bot's `## Code review` is skipped; a body of headings only shows the first), at most 160 characters, and the URL only as an `https:` URL without user name or password. `every` is in minutes, at least 1, `5` by default; the sources run with the GitHub read, so at most once a minute, and after a restart each runs at once.
39
+ What counts as news: an unknown `id` reports `new`; `updated` only when what you read changed, a short hash over `title`, `author`, `body` and `url`. A change of the time alone (a label, a sprint, a status) wakes nobody; where none of the four is mapped, `updatedAt` is the content, and then it is required. An entry whose `author` is in `ignoreAuthors` (case ignored) is recorded but wakes nobody, so your own comment does not. The first successful run of a source is a baseline, and the saved state holds per source only each id with its hash (with `reportOn` also the values of those fields, which may name people, such as an assignee), the 1000 seen most recently, so a restart repeats nothing and an entry that falls out of a time window and comes back is not new. Everything one GitHub read finds goes into one report, at most `reportLimit` entries per source (`5`), the rest as one line `=== source tickets +3 more`. The body shows its first line with text that is no Markdown heading (a bot's `## Code review` is skipped; a body of headings only shows the first), at most 160 characters, and the URL only as an `https:` URL without user name or password. `every` is in minutes, at least 1, `5` by default; the sources run with the GitHub read, so at most once a minute. The saved state keeps when each source last ran, failed or not, so a restart runs a source only once its `every` has passed; a source the state does not know yet, a new one or one from a state written before that time was kept, runs at once.
40
40
 
41
41
  **Report a status change.** `reportOn` names the mapped fields whose change reports, in place of the content hash: with `"reportOn": ["status"]` a ticket that moves from one column to the next wakes the coordinator, and a new comment alone does not. The report names each changed field with its old and new value, cut to 60 characters, `(empty)` for none, in one line; the body follows only when `body` is one of the changed fields. An unknown `id` still reports `new`, and `ignoreAuthors`, `reportLimit` and the one report per read stay as they are. `status` is just a field name: any mapped field but `id` may be listed, including `title` or `body`.
42
42
 
@@ -178,6 +178,17 @@
178
178
  },
179
179
  "additionalProperties": false
180
180
  },
181
+ "preview": {
182
+ "default": {},
183
+ "type": "object",
184
+ "properties": {
185
+ "publish": {
186
+ "default": true,
187
+ "type": "boolean"
188
+ }
189
+ },
190
+ "additionalProperties": false
191
+ },
181
192
  "plans": {
182
193
  "default": {},
183
194
  "type": "object",
@@ -270,13 +281,13 @@
270
281
  "type": "object",
271
282
  "properties": {
272
283
  "warnTokens": {
273
- "default": 200000,
284
+ "default": 350000,
274
285
  "type": "integer",
275
286
  "exclusiveMinimum": 0,
276
287
  "maximum": 9007199254740991
277
288
  },
278
289
  "alertTokens": {
279
- "default": 300000,
290
+ "default": 500000,
280
291
  "type": "integer",
281
292
  "exclusiveMinimum": 0,
282
293
  "maximum": 9007199254740991
@@ -361,6 +372,45 @@
361
372
  },
362
373
  "additionalProperties": false
363
374
  },
375
+ "parallelTasks": {
376
+ "default": {},
377
+ "type": "object",
378
+ "properties": {
379
+ "maxTokens": {
380
+ "default": 100000,
381
+ "type": "integer",
382
+ "exclusiveMinimum": 0,
383
+ "maximum": 9007199254740991
384
+ },
385
+ "auto": {
386
+ "type": "object",
387
+ "properties": {
388
+ "since": {
389
+ "type": "string",
390
+ "format": "date",
391
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
392
+ },
393
+ "until": {
394
+ "type": "string",
395
+ "format": "date",
396
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
397
+ }
398
+ },
399
+ "required": [
400
+ "since",
401
+ "until"
402
+ ],
403
+ "additionalProperties": false
404
+ }
405
+ },
406
+ "additionalProperties": false
407
+ },
408
+ "taskSignalMinutes": {
409
+ "default": 10,
410
+ "type": "integer",
411
+ "exclusiveMinimum": 0,
412
+ "maximum": 9007199254740991
413
+ },
364
414
  "previews": {
365
415
  "type": "object",
366
416
  "properties": {
@@ -491,9 +541,7 @@
491
541
  "commands": {
492
542
  "default": [
493
543
  "gh pr merge",
494
- "gh workflow run",
495
- "gh run rerun",
496
- "gh api"
544
+ "gh workflow run"
497
545
  ],
498
546
  "type": "array",
499
547
  "items": {
@@ -531,12 +579,26 @@
531
579
  },
532
580
  "additionalProperties": false
533
581
  },
582
+ "alwaysAsk": {
583
+ "default": [],
584
+ "type": "array",
585
+ "items": {
586
+ "type": "string",
587
+ "pattern": "^[A-Za-z0-9_.*/-]+$"
588
+ }
589
+ },
590
+ "requiredChecks": {
591
+ "type": "array",
592
+ "items": {
593
+ "type": "string",
594
+ "maxLength": 200
595
+ }
596
+ },
534
597
  "planApproval": {
535
- "default": {},
598
+ "deprecated": true,
536
599
  "type": "object",
537
600
  "properties": {
538
601
  "exceptPaths": {
539
- "default": [],
540
602
  "type": "array",
541
603
  "items": {
542
604
  "type": "string",
@@ -578,6 +640,10 @@
578
640
  "askUserQuestion": {
579
641
  "default": true,
580
642
  "type": "boolean"
643
+ },
644
+ "taskSignal": {
645
+ "default": true,
646
+ "type": "boolean"
581
647
  }
582
648
  },
583
649
  "additionalProperties": false
@@ -717,6 +783,18 @@
717
783
  ],
718
784
  "additionalProperties": false
719
785
  },
786
+ "skillBudgets": {
787
+ "type": "object",
788
+ "propertyNames": {
789
+ "type": "string",
790
+ "minLength": 1
791
+ },
792
+ "additionalProperties": {
793
+ "type": "integer",
794
+ "exclusiveMinimum": 0,
795
+ "maximum": 9007199254740991
796
+ }
797
+ },
720
798
  "looseTests": {
721
799
  "default": false,
722
800
  "type": "boolean"
@@ -0,0 +1,23 @@
1
+ import { matchesAnyGlob } from './glob'
2
+
3
+ /** The kit agent that judges how a changed agent text steers its reader. */
4
+ export const AGENT_TEXT_REVIEWER = 'skill-review'
5
+
6
+ /**
7
+ * Texts an agent reads as instructions, wherever they sit: the project's `.claude/` and a kit source tree alike.
8
+ * Only Markdown counts, so a script beside a skill or a source directory named `agents` stays out.
9
+ */
10
+ const AGENT_TEXT_GLOBS = [
11
+ '**/skills/**/*.md',
12
+ '**/agents/**/*.md',
13
+ '**/rules/**/*.md',
14
+ '.claude/project/**/*.md',
15
+ '**/CLAUDE.md',
16
+ '**/AGENTS.md',
17
+ ] as const
18
+
19
+ export const isAgentTextPath = (path: string): boolean => matchesAnyGlob(path, AGENT_TEXT_GLOBS)
20
+
21
+ /** The agent-text reviewer when a change touches an agent text. */
22
+ export const agentTextReviewers = (paths: readonly string[]): string[] =>
23
+ paths.some(isAgentTextPath) ? [AGENT_TEXT_REVIEWER] : []
@@ -15,6 +15,7 @@ import { publicContentCheck } from './public-content-check'
15
15
  import { artifactResult, formatPlanHeader, formatResults, skippedSlowResult, summarize } from './report'
16
16
  import { type CheckResult, SKIP_SLOW_FLAG } from './result'
17
17
  import { type CheckSettings, CONFIG_PATH, parseCheckSettings } from './settings'
18
+ import { skillBudgetCheck } from './skill-budgets'
18
19
 
19
20
  type CheckTarget = { root: string; configPath: string }
20
21
 
@@ -154,6 +155,7 @@ export const run = async (args: string[], home: string = homedir()): Promise<num
154
155
  const results = await executePlan(plan, root, args.includes(SKIP_SLOW_FLAG), [
155
156
  publicContent,
156
157
  deferralArrows,
158
+ skillBudgetCheck(root, settings.check.skillBudgets),
157
159
  ...planStructure,
158
160
  ])
159
161
  print(formatResults(results))
@@ -4,10 +4,13 @@ import {
4
4
  boardRowsWithoutPhase,
5
5
  boardRowsWithProse,
6
6
  hasLanesTable,
7
+ type LongPhaseTitle,
8
+ longPhaseTitles,
7
9
  type MergedPhaseWithOpenBoxes,
8
10
  mergedPhasesWithOpenBoxes,
9
11
  newPhasesWithoutReviewGate,
10
12
  PHASE_CELL_LIMIT,
13
+ PHASE_TITLE_LIMIT,
11
14
  type PhaseWithoutGate,
12
15
  type PhaseWithoutRow,
13
16
  PLAN_VOCABULARY,
@@ -17,6 +20,10 @@ import {
17
20
  phasesWithoutDependencyRow,
18
21
  type RowWithoutPhase,
19
22
  type RowWithProse,
23
+ SHORT_NAME_LIMIT,
24
+ SHORT_NAME_PROBLEM,
25
+ type ShortNameFinding,
26
+ shortNameFindings,
20
27
  } from '../plan-format'
21
28
  import { changedPlansOf } from './deferral-arrows'
22
29
  import { CHECK_STATUS, type CheckResult } from './result'
@@ -129,6 +136,30 @@ const unlistedReport: Report<PhaseWithoutRow> = {
129
136
  clean: 'every phase has a row in the dependency table',
130
137
  }
131
138
 
139
+ const shortNameText = (found: Found<ShortNameFinding>): string =>
140
+ found.problem === SHORT_NAME_PROBLEM.long
141
+ ? `${found.path}:${found.line} Phase ${found.phase}: the short name has ${found.length} characters (limit ${SHORT_NAME_LIMIT})`
142
+ : `${found.path}:${found.line} Phase ${found.phase}: open task has no short name; start it with - [ ] **Name**: (${SHORT_NAME_LIMIT} characters at most)`
143
+
144
+ const shortNameReport: Report<ShortNameFinding> = {
145
+ name: 'Task short names',
146
+ noun: 'plan',
147
+ rule: 'open tasks the change wrote start with a short name',
148
+ unmet: CHECK_STATUS.warn,
149
+ text: shortNameText,
150
+ clean: 'every open task the change wrote has a short name',
151
+ }
152
+
153
+ const titleReport: Report<LongPhaseTitle> = {
154
+ name: 'Phase title length',
155
+ noun: 'plan',
156
+ rule: `phase titles the change wrote have ${PHASE_TITLE_LIMIT} characters at most`,
157
+ unmet: CHECK_STATUS.warn,
158
+ text: ({ path, line, id, title, length }) =>
159
+ `${path}:${line} Phase ${id} ${title} has ${length} characters (limit ${PHASE_TITLE_LIMIT}); the title is the phase's name, its context goes into the origin line`,
160
+ clean: `every phase title the change wrote has ${PHASE_TITLE_LIMIT} characters at most`,
161
+ }
162
+
132
163
  const withPath =
133
164
  <T>(path: string) =>
134
165
  (found: T): Found<T> => ({ ...found, path })
@@ -150,6 +181,12 @@ const mergedOf = ({ path, current, base }: Changed, language: PlanLanguage): Fou
150
181
  const unlistedOf = ({ path, current }: Changed, language: PlanLanguage): Found<PhaseWithoutRow>[] | null =>
151
182
  phasesWithoutDependencyRow(current, language)?.map(withPath(path)) ?? null
152
183
 
184
+ const shortNamesOf = ({ path, current, previousLines }: Changed, language: PlanLanguage): Found<ShortNameFinding>[] =>
185
+ shortNameFindings(current, language, previousLines).map(withPath(path))
186
+
187
+ const titlesOf = ({ path, current, previousLines }: Changed, language: PlanLanguage): Found<LongPhaseTitle>[] =>
188
+ longPhaseTitles(current, language, previousLines).map(withPath(path))
189
+
153
190
  const changedContents = ({ root, previousOf, baseOf, language }: PlanCheckInput, plans: readonly string[]): Changed[] =>
154
191
  plans
155
192
  .filter((path) => existsSync(join(root, path)))
@@ -166,10 +203,12 @@ const changedContents = ({ root, previousOf, baseOf, language }: PlanCheckInput,
166
203
  * - a lane row the change added or edited that names no phase warns, and so does one whose phases cell is longer
167
204
  * than `PHASE_CELL_LIMIT` or records a side order of the user;
168
205
  * - a phase whose tasks are all ticked on the base branch and that still has open boxes without `→ P<n>` warns;
169
- * - a phase of a plan with a dependency table that has no row in it warns.
206
+ * - a phase of a plan with a dependency table that has no row in it warns;
207
+ * - an open task the change added or edited without a short name, or with one over `SHORT_NAME_LIMIT`, warns, and so
208
+ * does a phase title it wrote over `PHASE_TITLE_LIMIT`.
170
209
  *
171
210
  * Empty when no plan changed. The row and prose results are left out when no changed file has a lanes table, the
172
- * merged result when every one has, the dependency result when no changed plan has the table.
211
+ * merged, short-name and phase-title results when every one has, the dependency result when no changed plan has the table.
173
212
  */
174
213
  export const planStructureChecks = (input: PlanCheckInput): CheckResult[] => {
175
214
  const files = changedPlansOf(input.paths, input.planDirectory)
@@ -185,6 +224,8 @@ export const planStructureChecks = (input: PlanCheckInput): CheckResult[] => {
185
224
  const prose = boards.flatMap((file) => proseOf(file, language))
186
225
  const merged = plans.flatMap((file) => mergedOf(file, language))
187
226
  const unlisted = unlistedPerPlan.flat()
227
+ const shortNames = plans.flatMap((file) => shortNamesOf(file, language))
228
+ const titles = plans.flatMap((file) => titlesOf(file, language))
188
229
  const durationMs = Date.now() - started
189
230
  return [
190
231
  findingsResult(gateReport(language), gates, files.length, durationMs),
@@ -198,5 +239,11 @@ export const planStructureChecks = (input: PlanCheckInput): CheckResult[] => {
198
239
  ...(unlistedPerPlan.length > 0
199
240
  ? [findingsResult(unlistedReport, unlisted, unlistedPerPlan.length, durationMs)]
200
241
  : []),
242
+ ...(plans.length > 0
243
+ ? [
244
+ findingsResult(shortNameReport, shortNames, plans.length, durationMs),
245
+ findingsResult(titleReport, titles, plans.length, durationMs),
246
+ ]
247
+ : []),
201
248
  ]
202
249
  }
package/src/check/plan.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { JackherdConfig } from '../config'
2
+ import { agentTextReviewers } from './agent-text-reviewers'
2
3
  import { type ChangedFile, generatorOwnedFiles, stagedBuildArtifacts } from './changes'
3
4
  import { tokenize } from './command-line'
4
5
  import { looseDirectories, packageRootsFor } from './roots'
@@ -90,7 +91,13 @@ export const planChecks = (
90
91
  return {
91
92
  paths,
92
93
  roots,
93
- reviewers: [...reviewersFor(paths, config.reviewerRoutes), ...publicContentReviewers(paths, config.publicContent)],
94
+ reviewers: [
95
+ ...new Set([
96
+ ...reviewersFor(paths, config.reviewerRoutes),
97
+ ...publicContentReviewers(paths, config.publicContent),
98
+ ...agentTextReviewers(paths),
99
+ ]),
100
+ ],
94
101
  generated: generatorOwnedFiles(paths, config.generatorOwned),
95
102
  artifacts: stagedBuildArtifacts(files, config.buildArtifacts),
96
103
  checks:
@@ -57,6 +57,9 @@ const publicContent = z.strictObject({
57
57
  reviewer: nonEmpty.default('public-content'),
58
58
  })
59
59
 
60
+ /** A budget in words per path or glob from the project root; a key joining two paths with ` + ` budgets them together. */
61
+ const skillBudgets = z.record(glob, z.number().int().positive())
62
+
60
63
  /**
61
64
  * The `check` block of `.jackherd/config.json`. Test and type commands come from the top-level
62
65
  * `commands` block; this block holds what only the check runner reads.
@@ -73,6 +76,11 @@ export const checkConfigSchema = z.strictObject({
73
76
  buildArtifacts: z.array(glob).default([...DEFAULT_BUILD_ARTIFACT_GLOBS]),
74
77
  reviewerRoutes: z.array(reviewerRoute).default([]),
75
78
  publicContent: publicContent.optional(),
79
+ /**
80
+ * Word budgets for the files an agent loads again and again. An entry replaces the kit's budget for the same
81
+ * path and sets one for the project's own skills; a file with none is measured against the kit's default.
82
+ */
83
+ skillBudgets: skillBudgets.optional(),
76
84
  /** Also run tests in directories that hold test files but belong to no package. */
77
85
  looseTests: z.boolean().default(false),
78
86
  })
@@ -81,3 +89,4 @@ export type CheckConfig = z.infer<typeof checkConfigSchema>
81
89
  export type ReviewerRoute = CheckConfig['reviewerRoutes'][number]
82
90
  export type ProjectCheck = CheckConfig['checks'][number]
83
91
  export type PublicContentConfig = NonNullable<CheckConfig['publicContent']>
92
+ export type SkillBudgets = NonNullable<CheckConfig['skillBudgets']>
@@ -0,0 +1,55 @@
1
+ /** Words allowed to a skill the kit did not install and the project gave no budget. */
2
+ export const OWN_SKILL_BUDGET = 2000
3
+
4
+ /** Words allowed to a file the kit installed when neither the project nor `KIT_SKILL_BUDGETS` names it. */
5
+ export const KIT_FILE_FALLBACK_BUDGET = 2000
6
+
7
+ /** Words allowed to a project supplement, `.claude/project/<skill>.md`. */
8
+ export const SUPPLEMENT_BUDGET = 800
9
+
10
+ /**
11
+ * The kit's word budgets, by path from the project root. A key joining two paths with ` + ` budgets what is
12
+ * loaded together. A budget is the measured size plus at most 10 %, so a file has room for a sentence, not for a
13
+ * section; a file that needs more justifies the growth where it raises the number.
14
+ */
15
+ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
16
+ '.claude/skills/devops/SKILL.md': 321,
17
+ '.claude/skills/diagnose/SKILL.md': 1521,
18
+ '.claude/skills/diagnose/references/feedback-loops.md': 636,
19
+ '.claude/skills/follow/SKILL.md': 1100,
20
+ '.claude/skills/grill/SKILL.md': 685,
21
+ '.claude/skills/implement/SKILL.md': 2190,
22
+ '.claude/skills/orchestrate/SKILL.md': 7820,
23
+ '.claude/skills/orchestrate/references/acceptance.md': 200,
24
+ '.claude/skills/orchestrate/references/close-lane.md': 250,
25
+ '.claude/skills/orchestrate/references/coordinator-handover.md': 551,
26
+ '.claude/skills/orchestrate/references/draft-review.md': 300,
27
+ '.claude/skills/orchestrate/references/events.md': 1070,
28
+ '.claude/skills/orchestrate/references/herd.md': 299,
29
+ '.claude/skills/orchestrate/references/lane-handover.md': 280,
30
+ '.claude/skills/orchestrate/references/lane-session.md': 280,
31
+ '.claude/skills/orchestrate/references/lane-setup.md': 380,
32
+ '.claude/skills/orchestrate/references/plan-commit.md': 330,
33
+ '.claude/skills/orchestrate/references/sensitive-input.md': 269,
34
+ '.claude/skills/plan/SKILL.md': 1920,
35
+ '.claude/skills/plan/references/template-English.md': 536,
36
+ '.claude/skills/plan/references/template-German.md': 501,
37
+ '.claude/skills/pre-commit/SKILL.md': 1003,
38
+ '.claude/skills/review/SKILL.md': 319,
39
+ '.claude/skills/security-audit/SKILL.md': 519,
40
+ '.claude/skills/start/SKILL.md': 607,
41
+ '.claude/skills/uncle-bob/SKILL.md': 466,
42
+ '.claude/skills/orchestrate/SKILL.md + .claude/project/orchestrate.md': 8620,
43
+ '.claude/agents/devops.md': 104,
44
+ '.claude/agents/explore.md': 132,
45
+ '.claude/agents/implement.md': 303,
46
+ '.claude/agents/plan.md': 209,
47
+ '.claude/agents/public-content.md': 449,
48
+ '.claude/agents/security-audit.md': 128,
49
+ '.claude/agents/skill-review.md': 468,
50
+ '.claude/agents/spec-review.md': 1137,
51
+ '.claude/agents/uncle-bob.md': 111,
52
+ '.claude/rules/kit/code-style.md': 1664,
53
+ '.claude/rules/kit/pull-requests.md': 601,
54
+ '.claude/rules/kit/testing.md': 741,
55
+ }