jackherd 1.13.0 → 1.15.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 (95) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +28 -38
  3. package/assets/icons/skills.svg +1 -0
  4. package/dist/config.d.ts +12 -1
  5. package/dist/config.js +35 -2
  6. package/dist/plan-format.d.ts +24 -1
  7. package/dist/plan-format.js +25 -5
  8. package/docs/plan-contract.md +84 -0
  9. package/docs/skills-dark.svg +15 -2
  10. package/docs/skills.svg +15 -2
  11. package/herdr-plugin/herdr-plugin.toml +1 -1
  12. package/herdr-plugin/restore-plans.ts +56 -6
  13. package/install.sh +1 -1
  14. package/kit/hooks/hooks/merge-guard/board-settings.ts +5 -2
  15. package/kit/hooks/hooks/merge-guard/pull-request.ts +28 -15
  16. package/kit/hooks/hooks/merge-guard/run.ts +6 -3
  17. package/kit/hooks/hooks/merge-guard/settings.ts +12 -1
  18. package/kit/hooks/hooks/model-log/transcript.ts +4 -7
  19. package/kit/hooks/hooks/open-question/record.ts +4 -3
  20. package/kit/hooks/hooks/shared/dialog-answers.ts +7 -3
  21. package/kit/hooks/hooks/shared/log-vocabulary.ts +13 -0
  22. package/kit/hooks/hooks/shared/transcript.ts +7 -4
  23. package/kit/hooks/hooks/task-hint/decision.ts +3 -2
  24. package/kit/hooks/hooks/task-hint/run.ts +2 -4
  25. package/kit/rules/rules/kit/pull-requests.md +1 -0
  26. package/kit/workflow/skills/orchestrate/SKILL.md +24 -10
  27. package/kit/workflow/skills/orchestrate/handover/index.ts +8 -2
  28. package/kit/workflow/skills/orchestrate/references/adopt-plan.md +51 -0
  29. package/kit/workflow/skills/orchestrate/references/events.md +6 -1
  30. package/kit/workflow/skills/orchestrate/references/plan-commit.md +7 -6
  31. package/kit/workflow/skills/start/SKILL.md +6 -2
  32. package/kit/workflow/skills/start/references/missing-commands.md +17 -0
  33. package/package.json +1 -1
  34. package/schemas/jackherd.schema.json +43 -0
  35. package/src/check/command.ts +53 -7
  36. package/src/check/file-kinds.ts +144 -0
  37. package/src/check/parallel-report.ts +10 -3
  38. package/src/check/plan.ts +118 -16
  39. package/src/check/report.ts +44 -1
  40. package/src/check/roots.ts +9 -0
  41. package/src/check/schema.ts +12 -0
  42. package/src/check/settings.ts +11 -3
  43. package/src/check/skill-budget-defaults.ts +21 -7
  44. package/src/check/skill-budgets.ts +49 -19
  45. package/src/claude/paths.ts +12 -0
  46. package/src/cli.ts +1 -0
  47. package/src/config/schema.ts +27 -0
  48. package/src/context/fill.ts +2 -1
  49. package/src/context/log-vocabulary.ts +22 -0
  50. package/src/context/session-log.ts +2 -1
  51. package/src/lane-prompt/build.ts +10 -5
  52. package/src/lane-prompt/command.ts +6 -1
  53. package/src/lane-prompt/input.ts +4 -4
  54. package/src/lane-prompt/texts.ts +18 -10
  55. package/src/plan-contract/command.ts +69 -0
  56. package/src/plan-contract/contract.ts +144 -0
  57. package/src/plan-format/dependencies.ts +1 -4
  58. package/src/plan-format/index.ts +4 -0
  59. package/src/plan-format/lanes.ts +11 -0
  60. package/src/plan-format/markdown.ts +6 -5
  61. package/src/plan-format/model-table.ts +27 -0
  62. package/src/plans/activity-read.ts +98 -0
  63. package/src/plans/ask-coordinator.ts +20 -11
  64. package/src/plans/lane-plans.ts +2 -1
  65. package/src/plans/layout.ts +49 -4
  66. package/src/plans/live.ts +17 -4
  67. package/src/plans/plan-panes.ts +25 -5
  68. package/src/plans/snapshot.ts +18 -8
  69. package/src/plans/subagents-read.ts +4 -3
  70. package/src/plans/subagents.ts +2 -1
  71. package/src/plans/texts.ts +27 -1
  72. package/src/settled/send.ts +80 -4
  73. package/src/shared/parse-json.ts +8 -0
  74. package/src/sync/command.ts +17 -10
  75. package/src/sync/foreign-skills.ts +40 -0
  76. package/src/sync/global-kit.ts +31 -21
  77. package/src/sync/init.ts +2 -2
  78. package/src/sync/kit-exclude.ts +39 -0
  79. package/src/sync/plan-file.ts +1 -1
  80. package/src/sync/render.ts +2 -1
  81. package/src/sync/report.ts +37 -2
  82. package/src/sync/sync-result.ts +5 -2
  83. package/src/sync/sync.ts +13 -5
  84. package/src/task-signals/claude-signals.ts +6 -5
  85. package/src/watch/command.ts +88 -18
  86. package/src/watch/lane-message.ts +1 -1
  87. package/src/watch/lane-progress.ts +57 -3
  88. package/src/watch/lane-report.ts +67 -2
  89. package/src/watch/lane-watch.ts +2 -8
  90. package/src/watch/plan-missing-read.ts +47 -0
  91. package/src/watch/plan-missing.ts +43 -0
  92. package/src/watch/reference-paths.ts +1 -0
  93. package/src/watch/task-parallel-touched.ts +9 -2
  94. package/src/watch/texts.ts +8 -0
  95. package/src/watch/watch-events.ts +7 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  What changed for people who use jackherd, newest first.
4
4
 
5
+ ## 1.15.0
6
+
7
+ - **Any plan, once it meets the contract.** `docs/plan-contract.md` writes down what Jack needs from a plan: phases with an id, checkbox tasks, a model per phase, and from two phases on a dependency table. `jh plan check <file>` says per part whether it is there, missing or malformed, and exits 0 when the plan meets it. Without a dependency table, phases run one after another, and `jh check --parallel` says so.
8
+ - **Jack adopts a plan from another planner.** When a plan a lane starts from misses the contract, or lives only in an issue or a `.scratch/` folder, Jack mirrors it into a plan file and adds only what is missing: phase headings, tasks, model rows, the dependency table, short task names and a line saying the source planned no review. No word of the author's goes, which a word diff proves, and you see that diff before the plan is committed.
9
+ - **A lane builds with any skill.** The lane prompt names what every lane does, whatever skill it builds with (`/jh-implement`, someone else's, or your own): report each task with `jh task`, write the report, send questions as text, leave git to Jack. A prompt to a lane that does not start working comes back as an error instead of passing as sent, and Jack's own prompts wait the same way.
10
+ - **Other people's skills sit beside jackherd.** A skill installed with `npx skills add` (listed in `skills-lock.json`) only warns when it exceeds a word budget, it never turns `jh check` red. When one lands on the path of a jackherd skill, `jh sync` names it, also when it kept your file, with the way out: `"skillPrefix": "jh-"` moves jackherd's skills to `/jh-…` and leaves the other skill where it is.
11
+ - **Leave out jackherd skills you don't use.** List them in `kit.exclude` in `.jackherd/config.json`, by name without the prefix (`"exclude": ["devops", "grill"]`); a name takes its skill and its agent. `jh sync` stops writing them, removes a copy you never changed, keeps one you changed, and names what it left out. Where another jackherd skill calls one you left out, it reads `/devops (not installed)`, so the agent knows instead of calling a skill that isn't there. A name jackherd doesn't ship stops the sync before it writes anything; `orchestrate` and `start` cannot be left out.
12
+ - **The herd shows what a working lane does.** Under the line of a working lane, a dim `↳` line shows its agent's last action from the session log, a tool with its target or the start of its answer, and one more for each running subagent. The log is read again only when it grew, never per frame.
13
+ - **Jack starts a ready phase at once.** When a lane reports `done` or `idle` and its plan has a phase whose dependencies are ticked and that nobody works on, the watcher prints `<lane> ready: P9 (after P7): start it now`, so a pull request waiting for your approval no longer holds the next phase back, only the commit.
14
+ - **Word budgets follow the skill prefix.** With a `skillPrefix`, `jh check` measured every jackherd skill against the default 2000 words; it now measures each against its own budget.
15
+ - **Tests by name find a private module's test.** `_result.py` counts as tested by `test_result.py`.
16
+ - **A folded lane shows its checks.** Behind each pull request badge the folded lane line shows `✓ checks`, `checks running`, `✗ test +1` with the first red check, or `draft`, also while the lane rests, so a red check or a draft no longer hides until you open the lane. The open lane says `checks running` too, where it said `… checks`.
17
+ - **Pull requests stay drafts until their merge.** With `"merge": { "openAsDraft": true }` a lane opens every pull request with `gh pr create --draft`, so its pushes start no workflow that skips drafts. Jack runs `gh pr ready` when he reports the pull request to you to merge, or as the first step of his own merge, and waits for the runs that start then. Off by default.
18
+ - **The merge guard never merges a draft, and a required check needs a success.** A draft always asks. A check in `guard.requiredChecks` counts only with a run that succeeded: a skipped run, as a draft leaves it right after `gh pr ready`, no longer lets a merge pass. That holds for a self-merge too, which waited for no required check before; a `guard.requiredChecks` that does not read turns the self-merge answer off. Without required checks, at least one run must have succeeded: a pull request whose checks were all skipped asks.
19
+
20
+ ## 1.14.0
21
+
22
+ - **Any language, once its commands are set.** PHP and Node are recognised on their own, as before. For any other language, set its test command once, or let `/jh-start` in Claude Code propose the commands from your CI: it writes only the ones you approve. From then on `jh check` runs your tests before a commit. In a language jackherd does not know, a changed file without a test of its name gets a warning, not a block, since some languages keep tests in the source file.
23
+ - **The herd shows the plan a lane really works on.** When a lane's plan is not on the main branch yet, Jack hears about it once and gets it there, so the herd no longer shows an older plan for that lane.
24
+
5
25
  ## 1.13.0
6
26
 
7
27
  - **Repositories without Node.** `jh init` reads the stack from the lock file: next to the Node managers, `composer.lock` writes the tools `composer.json` names (Pest or PHPUnit, PHPStan or Psalm, Pint or PHP-CS-Fixer, each as `vendor/bin/…`) and `composer install`. Before the first install, `composer.json` alone decides Composer, with no `lanes.lockFile`; a `package.json` without a lock file names no package manager, so none is guessed. A repository without a known stack gets no Bun commands any more; `init` leaves them out and says so, and the config gets a `$schema` only where `node_modules` can sit beside `.jackherd/`.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <p>
2
- <img alt="jackherd: a black and white Shih Tzu in a turquoise bandana, three lanes running from him to three agent nodes" src="https://unpkg.com/jackherd@1.13.0/assets/branding/jackherd-logo-outline.svg" width="340">
2
+ <img alt="jackherd: a black and white Shih Tzu in a turquoise bandana, three lanes running from him to three agent nodes" src="https://unpkg.com/jackherd@1.15.0/assets/branding/jackherd-logo-outline.svg" width="340">
3
3
  <br>
4
4
  </p>
5
5
 
6
- [![npm](https://img.shields.io/npm/v/jackherd)](https://www.npmjs.com/package/jackherd) [![license: MIT](https://img.shields.io/npm/l/jackherd)](https://www.npmjs.com/package/jackherd) [![2 dependencies](https://img.shields.io/badge/dependencies-2-brightgreen)](https://www.npmjs.com/package/jackherd?activeTab=dependencies)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.13.0/assets/icons/space.svg">
6
+ [![npm](https://img.shields.io/npm/v/jackherd)](https://www.npmjs.com/package/jackherd) [![license: MIT](https://img.shields.io/npm/l/jackherd)](https://www.npmjs.com/package/jackherd) [![2 dependencies](https://img.shields.io/badge/dependencies-2-brightgreen)](https://www.npmjs.com/package/jackherd?activeTab=dependencies)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.0/assets/icons/space.svg">
7
7
 
8
8
  # Jack herds your coding agents.
9
9
 
@@ -15,25 +15,27 @@ For developers who'd rather decide than supervise.
15
15
  curl -fsSL https://unpkg.com/jackherd/install.sh | sh
16
16
  ```
17
17
 
18
- Then `/jh-start` in a repository, and Jack offers your first plan. → [Quickstart](#quickstart)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.13.0/assets/icons/space.svg">
18
+ Then `/jh-start` in a repository, and Jack offers your first plan. → [Quickstart](#quickstart)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.0/assets/icons/space.svg">
19
19
 
20
20
  <p>
21
- <img src="https://unpkg.com/jackherd@1.13.0/docs/workflow-dark.svg" width="720" alt="jackherd in herdr: a lane hits a question, Jack asks you as a choice, you pick, and the lane works on while the herd view counts its progress.">
21
+ <img src="https://unpkg.com/jackherd@1.15.0/docs/workflow-dark.svg" width="720" alt="jackherd in herdr: a lane hits a question, Jack asks you as a choice, you pick, and the lane works on while the herd view counts its progress.">
22
22
  <br>
23
- <sub>Jack and his lanes in <a href="https://herdr.dev/">herdr</a>, a terminal for coding agents (recommended). Without it, one Claude Code session builds with subagents.</sub><br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.13.0/assets/icons/space.svg">
23
+ <sub>Jack and his lanes in <a href="https://herdr.dev/">herdr</a>, a terminal for coding agents (recommended). Without it, one Claude Code session builds with subagents.</sub><br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.0/assets/icons/space.svg">
24
24
  </p>
25
25
 
26
- <p><img src="https://unpkg.com/jackherd@1.13.0/assets/icons/lanes.svg" width="24" height="34" align="middle" alt=""> <b>Parallel where it pays.</b><br>Phases that don't collide run side by side, each lane on its own branch and worktree. Jack keeps the merge order and has the others rebase.</p>
26
+ <p><img src="https://unpkg.com/jackherd@1.15.0/assets/icons/lanes.svg" width="24" height="34" align="middle" alt=""> <b>Parallel where it pays.</b><br>Phases that don't collide run side by side, each lane on its own branch and worktree. Jack keeps the merge order and has the others rebase.</p>
27
27
 
28
- <p><img src="https://unpkg.com/jackherd@1.13.0/assets/icons/bark.svg" width="24" height="34" align="middle" alt=""> <b>Jack barks.</b><br>A blocked lane, a red check, a review comment: Jack wakes, and the herd view shows where every lane stands.</p>
28
+ <p><img src="https://unpkg.com/jackherd@1.15.0/assets/icons/decide.svg" width="24" height="34" align="middle" alt=""> <b>You make the calls.</b><br>Nothing is committed, pushed or merged without your approval. Jack merges on green checks only where you said so: one pull request, or a plan's for up to 24 hours.</p>
29
29
 
30
- <p><img src="https://unpkg.com/jackherd@1.13.0/assets/icons/ear.svg" width="24" height="34" align="middle" alt=""> <b>Jack listens.</b><br>Review comments go to the lane that fixes them, with a test. <code>/jh-follow</code> brings anything with a CLI or an API into the herd: tickets, error logs, status pages, deploys, and whatever else you point it at.</p>
30
+ <p><img src="https://unpkg.com/jackherd@1.15.0/assets/icons/bark.svg" width="24" height="34" align="middle" alt=""> <b>Jack barks.</b><br>A blocked lane, a red check, a review comment: Jack wakes, and the herd view shows where every lane stands.</p>
31
31
 
32
- <p><img src="https://unpkg.com/jackherd@1.13.0/assets/icons/bowl.svg" width="24" height="34" align="middle" alt=""> <b>Picks up where you left off.</b><br>Plan and board hold the state, not the chat.</p>
32
+ <p><img src="https://unpkg.com/jackherd@1.15.0/assets/icons/skills.svg" width="24" height="34" align="middle" alt=""> <b>Works with your skills.</b><br>Keep your own skills: a lane builds with whichever one you pick. Plans written elsewhere run too, once Jack fills in what they're missing.</p>
33
33
 
34
- <p><img src="https://unpkg.com/jackherd@1.13.0/assets/icons/plan.svg" width="24" height="34" align="middle" alt=""> <b>Lives in your repository.</b><br>Plans, board and every decision are Markdown, reviewed like code. <code>jh init</code> gives your team the same setup.</p>
34
+ <p><img src="https://unpkg.com/jackherd@1.15.0/assets/icons/ear.svg" width="24" height="34" align="middle" alt=""> <b>Jack listens.</b><br>Review comments go to the lane that fixes them, with a test. <code>/jh-follow</code> brings anything with a CLI or an API into the herd: tickets, error logs, status pages, deploys, and whatever else you point it at.</p>
35
35
 
36
- <p><img src="https://unpkg.com/jackherd@1.13.0/assets/icons/decide.svg" width="24" height="34" align="middle" alt=""> <b>You make the calls.</b><br>Nothing is committed, pushed or merged without your approval. Jack merges on green checks only where you said so: one pull request, or a plan's for up to 24 hours.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.13.0/assets/icons/space.svg"></p>
36
+ <p><img src="https://unpkg.com/jackherd@1.15.0/assets/icons/bowl.svg" width="24" height="34" align="middle" alt=""> <b>Picks up where you left off.</b><br>Plan and board hold the state, not the chat.</p>
37
+
38
+ <p><img src="https://unpkg.com/jackherd@1.15.0/assets/icons/plan.svg" width="24" height="34" align="middle" alt=""> <b>Lives in your repository.</b><br>Plans, board and every decision are Markdown, reviewed like code. <code>jh init</code> gives your team the same setup.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.0/assets/icons/space.svg"></p>
37
39
 
38
40
  ## Quickstart
39
41
 
@@ -69,22 +71,22 @@ Then `/jh-start` in a repository, and Jack offers your first plan. → [Quicksta
69
71
  /jh-orchestrate docs/plans/board-<name>/board.md
70
72
  ```
71
73
 
72
- By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks. → [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.13.0/assets/icons/space.svg">
74
+ By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks. → [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.0/assets/icons/space.svg">
73
75
 
74
76
  ## Follow anything outside GitHub
75
77
 
76
78
  Whatever you check by hand, Jack can watch: tickets, error logs, status pages, deploys. One `/jh-follow`, one yes, and every change comes to you like this:
77
79
 
78
- <img src="https://unpkg.com/jackherd@1.13.0/docs/follow-dark.svg" width="720" alt="A source (ticket, error log or status page) has a new or changed entry; jh watch reports it to Jack; Jack puts a row into Your turn, prefixed with the source's label; its [ask] button makes Jack ask you, and then a lane works it.">
80
+ <img src="https://unpkg.com/jackherd@1.15.0/docs/follow-dark.svg" width="720" alt="A source (ticket, error log or status page) has a new or changed entry; jh watch reports it to Jack; Jack puts a row into Your turn, prefixed with the source's label; its [ask] button makes Jack ask you, and then a lane works it.">
79
81
 
80
- The herd sums up your sources in one `Sources` line: per source its new entries, or when it last ran, red while it fails; opened, a click on an entry opens it. A ticket shows on the lane that works it, and a row in Your turn that came from a source starts with that source's label.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.13.0/assets/icons/space.svg">
82
+ The herd sums up your sources in one `Sources` line: per source its new entries, or when it last ran, red while it fails; opened, a click on an entry opens it. A ticket shows on the lane that works it, and a row in Your turn that came from a source starts with that source's label.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.0/assets/icons/space.svg">
81
83
 
82
84
  ## In detail
83
85
 
84
86
  <details>
85
87
  <summary><b>What Jack does on his own, and what waits for you</b></summary>
86
88
 
87
- <img src="https://unpkg.com/jackherd@1.13.0/docs/lanes-dark.svg" alt="Jack prompts two lanes and asks you for decisions and approvals; the lanes send questions back as text; jh watch reads lane state, pull requests and checks and wakes Jack.">
89
+ <img src="https://unpkg.com/jackherd@1.15.0/docs/lanes-dark.svg" alt="Jack prompts two lanes and asks you for decisions and approvals; the lanes send questions back as text; jh watch reads lane state, pull requests and checks and wakes Jack.">
88
90
 
89
91
  | On his own | After your approval | Never |
90
92
  | --- | --- | --- |
@@ -108,16 +110,18 @@ A project install adds two hooks to Claude Code's [permission rules](https://cod
108
110
 
109
111
  A merge passes without a question on green checks after self-merge, or under a standing approval: one plan's lanes until a time you pick, at most 24 hours; workflows, migrations, env files and the guards' own files still ask. A pull request whose files all match `merge.withoutChecks`, such as docs only, skips question and checks, unless a file lies on a path that always asks.
110
112
 
113
+ `"merge": { "openAsDraft": true }` has lanes open every pull request as a draft, so pushes while it is worked on start no workflow that skips drafts; Jack runs `gh pr ready` before the merge. The guard never merges a draft, and a check in `guard.requiredChecks` counts only with a run that succeeded, not with the skipped run a draft leaves behind; without that list, at least one run must have succeeded.
114
+
111
115
  The guards catch mistakes, not intent; for that, use branch protection. The hooks need Bun; a global install ships none.
112
116
 
113
117
  </details>
114
118
 
115
119
  <details>
116
- <summary><b>Plans, boards and the skills</b></summary>
120
+ <summary><b>Plans, boards and the skills, yours included</b></summary>
117
121
 
118
122
  A **plan** is Markdown with phases, checkbox tasks, a model and a review gate per phase. A **board** lists the lanes of one or more plans: which phases each lane builds, on which model, after what. A **lane** is one agent session in its own git worktree (a second checkout of your repository), and **Jack** is the session in the main checkout that runs `/jh-orchestrate` on a board.
119
123
 
120
- <img src="https://unpkg.com/jackherd@1.13.0/docs/skills-dark.svg" alt="/jh-grill hands decisions to /jh-plan; /jh-plan feeds /jh-orchestrate or a single /jh-implement; /jh-implement ends in /jh-pre-commit, which runs the reviewers; /jh-diagnose hands a red command to /jh-implement.">
124
+ <img src="https://unpkg.com/jackherd@1.15.0/docs/skills-dark.svg" alt="Your own plan or an issue is adopted (Jack adds what is missing, you see the diff) and goes to /jh-orchestrate; /jh-grill hands decisions to /jh-plan; /jh-plan feeds /jh-orchestrate or a single /jh-implement, which your own skill can replace; /jh-implement ends in /jh-pre-commit, which runs the reviewers; /jh-diagnose hands a red command to /jh-implement.">
121
125
 
122
126
  | Skill | Does |
123
127
  | --- | --- |
@@ -130,7 +134,10 @@ A **plan** is Markdown with phases, checkbox tasks, a model and a review gate pe
130
134
  | `/jh-follow` | follows something outside GitHub: a ticket, error logs, a status page |
131
135
  | `/jh-diagnose` | turns a bug into a failing command, fixes it with a regression test |
132
136
 
133
- The global install names the skills with a `jh-` prefix; a project install uses none, unless the project already has skills of the same names.
137
+ - **Your own skills stay.** `jh init` keeps them and gives jackherd's skills a `jh-` prefix where a name is taken, as a global install always does; `jh sync` never writes over a file you changed, it leaves `<file>.kit-new` beside it. If a skill from `npx skills add` later takes the path of one of jackherd's, `jh sync` names it and the way out: `"skillPrefix": "jh-"`. Such skills only warn in `jh check`'s word budgets, never red.
138
+ - **Leave out what you don't use.** `"kit": { "exclude": ["devops", "grill"] }` in `.jackherd/config.json` keeps those skills and agents out on the next `jh sync`: a copy you never changed goes, one you changed stays, and the skills that call one show it as not installed. `/jh-orchestrate` and `/jh-start` always stay.
139
+ - **A lane builds with any skill.** The lane's rules hold over it: report each task with `jh task`, send questions as text, leave git to Jack.
140
+ - **A plan from another planner**, or one in an issue, runs once it meets the [plan contract](https://unpkg.com/jackherd/docs/plan-contract.md): Jack adds what is missing and shows you the diff before committing it.
134
141
 
135
142
  </details>
136
143
 
@@ -181,9 +188,10 @@ How the herd reads the board:
181
188
  - **Sources** shows, per source that is no ticket, its entries and how many are new, or when it last ran: amber with a new entry that is not in Your turn yet, red while it fails. Opened, its entries stand newest first by the time they show, and a click on an entry opens it; `↑ in your turn` marks one a row of Your turn asks about.
182
189
  - **Lanes** opens at the start. A lane that works or is blocked opens by itself only when it first appears; after that, opening and folding are yours. A lane symbol says the state (`●` working, `◆` blocked, `◉` the agent finished its answer, `○` idle, `(·) no agent` a worktree nobody works in); only blocked wears its word. A resting lane names what it waits on: `waits on you` with the open questions of its report, `waits on` a row of Your turn, `after` its board row's starts-after cell, or `idle · no reason`.
183
190
  - **Waiting** lists the lanes not started yet: no worktree yet, something left to do, and what each starts after; it starts folded. Finished lanes (cleaned up, or every phase they name ticked) are not shown.
184
- - **An open lane** shows, top down: its branch, the model and how long it idles; its link chips on one line, `+n` for the rest; its pull requests; the phases it works on, each with its bar and its first four open tasks, the running ones first, `▸ n more open · k done` to list them all; `▸ next` with the phases that wait, as ranges; `▸ done` with the finished ones. A lane shows only the phases its board rows name; a lane without a worktree reads its plan from the main checkout. A folded lane line names the phase moved last, `+n` more active ones and its count, and a reported task below it.
191
+ - **An open lane** shows, top down: its branch, the model and how long it idles; its link chips on one line, `+n` for the rest; its pull requests; the phases it works on, each with its bar and its first four open tasks, the running ones first, `▸ n more open · k done` to list them all; `▸ next` with the phases that wait, as ranges; `▸ done` with the finished ones. A lane shows only the phases its board rows name; a lane without a worktree reads its plan from the main checkout. A folded lane line names the phase moved last, `+n` more active ones and its count, and a reported task below it; behind each pull request badge it shows the checks, also while the lane rests: `✓ checks`, `checks running`, `✗ test +1` with the first red check, `draft` for a draft.
185
192
  - **A phase runs** when an agent reports working on it, one of its boxes was ticked in the last 20 minutes, or some of its tasks are ticked. A phase whose run was merged and that the lane's newest board row no longer names waits behind the others in `▸ next`, its open boxes left open on purpose. `n min ago` counts since the last tick or report; `· gate` means only the review gate is open, `docs open` only the documentation box.
186
193
  - **A running task** carries `●` when an agent reported it, from its task list or `jh task`; its text stands in teal and a light band runs over it (`plans.shimmer: false`, `NO_COLOR` and `--once` keep it still). `◌` in dim is only guessed from the words of the lane's log. `now: …` is a reported task no plan task names, `Subagent: …` a running subagent on no phase; a running review sits on the gate box that names it, in the phase it names or else the lane's current one. With several agents in a lane, each task carries its agent's name, dim, at the right edge.
194
+ - **What a working lane does right now:** under its lane line, open or folded, a dim `↳` line shows the last action from the agent's session log, a tool with its target (`↳ Read plans/layout.ts`) or the start of its answer, and each running subagent adds one, indented and led by its task (where `Subagent: …` says what it works on, `↳` says what it does); the lines go when the lane rests.
187
195
  - **Counts and colours:** the bar is the progress over the lane's plans; `n/m` counts checkboxes, `n/m ph` finished phases, green when all are ticked; `–` is a phase without checkboxes, `?` one not in the plan. `🥣` is the share of the context window used, on a folded lane only once it warns. `Sonnet ≠ Opus` in amber says a phase's subagent ran on another model than the plan's. `stale?` in Backlog marks a plan file unchanged on the base branch for 14 days.
188
196
  - **Chips:** `🔗` an Artifact, `⑂` a pull request or issue, `◈` Figma, `📡` a ticket with its status (`📡 n` in amber on a folded lane when tickets are new), `private` where the plan says so. After the lane: `↗` a pull request with its checks, `💬` new feedback, `✋` changes requested, `🌐` a preview. ctrl+click opens a chip's link.
189
197
  - **Mouse and keys:** `▸` `▾` marks a row that opens and folds, and only such a row has one; it turns teal under the mouse or the arrows. `❯` and a light background mark the row under the mouse or the arrows; the mark leaves with the mouse, while a row the arrows chose keeps it; the footer says what a click or a key does there. A click or enter on a task opens it and nothing else: what it asks, up to its first ` — `, one sentence per line behind a thin `│`, only whole sentences in at most five lines, code in teal and the origin dim at the end; `▸ n more lines` opens the rest. Buttons in square brackets do something, bold under the mouse. `s` opens every lane and plan as one HTML page.
@@ -222,7 +230,7 @@ Each phase in the plan names its model: the planning model (Opus by default) whe
222
230
  | `jh preview <file>` | renders a Markdown file through the GitHub API into a preview in GitHub and npm style |
223
231
  | `jh waiting` | the files and Artifacts sent to you that you have not answered yet (`close <id>…` or `close --all` once you have) |
224
232
  | `jh context` | how full an agent's context is, as JSON |
225
- | `jh config` | the commands and the base branch the repository uses; a missing value or one the lock files contradict goes to stderr with the line to set, and `jh config <key>` for an unset value exits 3 |
233
+ | `jh config` | the commands and the base branch the repository uses; a missing value goes to stderr with the line to set, and `jh config <key>` for an unset value exits 3 |
226
234
 
227
235
  Without a global install, run them as `bunx jackherd <command>`. The config is `.jackherd/config.json` in a project and `~/.jackherd/config.json` globally, checked against [one schema](https://unpkg.com/jackherd/schemas/jackherd.schema.json), which your editor completes in the project config. What usually needs setting:
228
236
 
@@ -242,24 +250,6 @@ Without a global install, run them as `bunx jackherd <command>`. The config is `
242
250
 
243
251
  </details>
244
252
 
245
- <details>
246
- <summary><b>Other languages</b></summary>
247
-
248
- jackherd runs on Bun; your project does not need Node. `jh init` reads the lock file in the repository root and writes the commands of that stack:
249
-
250
- | Lock file | Commands |
251
- | --- | --- |
252
- | `bun.lock`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` | the manager's own (`bun test`, `npm test`, …), TypeScript and Biome |
253
- | `composer.lock`, or `composer.json` before the first install | from `composer.json`: Pest or PHPUnit, PHPStan or Psalm, Pint or PHP-CS-Fixer, each as `vendor/bin/…`; `composer install` |
254
-
255
- A tool the manifest does not name gets no command, and a repository without a known lock file or a `composer.json` gets none at all: jackherd never guesses one, so a `package.json` without a lock file names no package manager. `jh config` names each missing value with the line to set, and `/jh-start` passes those lines on. Set a command yourself under `commands` in `.jackherd/config.json`; a value there always wins.
256
-
257
- `jh check` tests from the nearest directory with the stack's manifest (`package.json`, `composer.json`), type-checks where a type config is (`tsconfig.json`, `phpstan.neon`), and lints only the files the stack lints. It fails when a changed source file is reached by no test, and when `commands.test` belongs to another stack than the lock file (`bun test` beside `composer.lock`), since such a run finds no test. A suite that runs some other way is a `check.checks` entry with `"kind": "tests"`.
258
-
259
- The kit rules name no language, framework or test runner. Put your team's conventions (PSR-12, the PHPStan level, how Pest tests are written) into a rule of your own beside `.claude/rules/kit/`; `jh sync` never touches it.
260
-
261
- </details>
262
-
263
253
  <details>
264
254
  <summary><b>Troubleshooting</b></summary>
265
255
 
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26.18 37.09" width="24" height="34" fill="none" stroke-linecap="round" stroke-linejoin="round"><g transform="translate(-0.822 -0.552) scale(1.019)" stroke-width="1.718"><path stroke="#6F818A" d="M10.35 5.40L10.44 2.12L13.56 2.12L13.65 5.40L15.50 6.17L17.88 3.91L20.09 6.12L17.83 8.50L18.60 10.35L21.88 10.44L21.88 13.56L18.60 13.65L17.83 15.50L20.09 17.88L17.88 20.09L15.50 17.83L13.65 18.60L13.56 21.88L10.44 21.88L10.35 18.60L8.50 17.83L6.12 20.09L3.91 17.88L6.17 15.50L5.40 13.65L2.12 13.56L2.12 10.44L5.40 10.35L6.17 8.50L3.91 6.12L6.12 3.91L8.50 6.17Z"/><circle stroke="#04A5AD" cx="12" cy="12" r="3.3"/></g></svg>
package/dist/config.d.ts CHANGED
@@ -21,6 +21,10 @@ declare const isTerminalAdapter: (value: string) => value is TerminalAdapter;
21
21
  declare const isModelRole: (value: string) => value is ModelRole;
22
22
  /** Lower-case letters and digits ending in a dash, so a prefixed name stays one plain path segment. */
23
23
  declare const SKILL_PREFIX: RegExp;
24
+ /** What a global install, and a project that already has skills of kit names, puts before every kit name. */
25
+ declare const DEFAULT_SKILL_PREFIX = "jh-";
26
+ /** The skills every lane and the coordinator run on; `kit.exclude` cannot take them away. */
27
+ declare const KIT_CORE: readonly string[];
24
28
  /**
25
29
  * The project's own commands. Each is optional: detection fills what the repository's stack decides, and a command
26
30
  * nobody set stays out instead of a guessed one (a kit text then says where to set it).
@@ -101,6 +105,9 @@ declare const jackherdConfigSchema: z.ZodObject<{
101
105
  install: z.ZodOptional<z.ZodString>;
102
106
  dev: z.ZodOptional<z.ZodString>;
103
107
  }, z.core.$strict>>;
108
+ kit: z.ZodPrefault<z.ZodObject<{
109
+ exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
110
+ }, z.core.$strict>>;
104
111
  lanes: z.ZodPrefault<z.ZodObject<{
105
112
  copy: z.ZodDefault<z.ZodArray<z.ZodString>>;
106
113
  lockFile: z.ZodOptional<z.ZodString>;
@@ -288,6 +295,7 @@ declare const jackherdConfigSchema: z.ZodObject<{
288
295
  }>>>;
289
296
  merge: z.ZodPrefault<z.ZodObject<{
290
297
  withoutChecks: z.ZodDefault<z.ZodArray<z.ZodString>>;
298
+ openAsDraft: z.ZodDefault<z.ZodBoolean>;
291
299
  }, z.core.$strict>>;
292
300
  hints: z.ZodPrefault<z.ZodObject<{
293
301
  askUserQuestion: z.ZodDefault<z.ZodBoolean>;
@@ -321,6 +329,9 @@ declare const jackherdConfigSchema: z.ZodObject<{
321
329
  }, z.core.$strict>>;
322
330
  skillBudgets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
323
331
  looseTests: z.ZodDefault<z.ZodBoolean>;
332
+ roots: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
333
+ sourceFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
334
+ testFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
324
335
  }, z.core.$strict>>;
325
336
  }, z.core.$strict>;
326
337
  type JackherdConfig = z.infer<typeof jackherdConfigSchema>;
@@ -330,4 +341,4 @@ declare const parseConfigFile: (raw: unknown, label: string) => JackherdConfig;
330
341
  /** One `watch.sources` entry outside the config, such as a draft to try; invalid throws with `label` and the issues. */
331
342
  declare const parseSourceDraft: (raw: unknown, label: string) => z.infer<typeof sourceSchema>;
332
343
 
333
- export { COMMAND_KEYS, type CommandKey, DEFAULT_CONTEXT_WINDOW, DEFAULT_MODELS, type JackherdConfig, MODEL_ROLE, type ModelRole, SKILL_PREFIX, TERMINAL_ADAPTER, type TerminalAdapter, isModelRole, isTerminalAdapter, jackherdConfigSchema, parseConfigFile, parseJackherdConfig, parseSourceDraft, renderConfigJsonSchema };
344
+ export { COMMAND_KEYS, type CommandKey, DEFAULT_CONTEXT_WINDOW, DEFAULT_MODELS, DEFAULT_SKILL_PREFIX, type JackherdConfig, KIT_CORE, MODEL_ROLE, type ModelRole, SKILL_PREFIX, TERMINAL_ADAPTER, type TerminalAdapter, isModelRole, isTerminalAdapter, jackherdConfigSchema, parseConfigFile, parseJackherdConfig, parseSourceDraft, renderConfigJsonSchema };
package/dist/config.js CHANGED
@@ -36,6 +36,7 @@ var isTokenizable = (command3) => {
36
36
  var command = nonEmpty.refine(isTokenizable, "Unterminated quote in command");
37
37
  var glob = nonEmpty.refine((value) => !/[{}[\]]/.test(value), "Braces and brackets are not supported in globs");
38
38
  var globs = z.array(glob).min(1);
39
+ var directory = nonEmpty.transform((value) => value.replace(/^(\.\/)+/, "").replace(/\/+$/, "") || ".");
39
40
  var DEFAULT_BUILD_ARTIFACT_GLOBS = [
40
41
  "**/tsup.config.bundled_*",
41
42
  "**/dist/**",
@@ -90,12 +91,25 @@ var checkConfigSchema = z.strictObject({
90
91
  */
91
92
  skillBudgets: skillBudgets.optional(),
92
93
  /** Also run tests in directories that hold test files but belong to no package. */
93
- looseTests: z.boolean().default(false)
94
+ looseTests: z.boolean().default(false),
95
+ /**
96
+ * Package roots, as directories from the repository root, read only where no stack is detected: a changed file
97
+ * belongs to the nearest one above it, else to the repository root `.`.
98
+ */
99
+ roots: z.array(directory).min(1).optional(),
100
+ /** The files a test covers; replaces the stack's pattern, or the rule of thumb where no stack is detected. */
101
+ sourceFiles: globs.optional(),
102
+ /** The test files; replaces the stack's pattern, or the rule of thumb where no stack is detected. */
103
+ testFiles: globs.optional()
94
104
  });
95
105
 
96
106
  // src/claude/paths.ts
97
107
  var CLAUDE_DIRECTORY = ".claude";
108
+ var SKILLS_DIRECTORY = "skills";
109
+ var AGENTS_DIRECTORY = "agents";
98
110
  var WORKTREES_DIRECTORY = "worktrees";
111
+ var SKILL_PATH = new RegExp(`^${SKILLS_DIRECTORY}/([^/]+)/(.+)$`);
112
+ var AGENT_PATH = new RegExp(`^${AGENTS_DIRECTORY}/([^/]+)\\.md$`);
99
113
  var claudePath = (...segments) => [CLAUDE_DIRECTORY, ...segments].join("/");
100
114
 
101
115
  // src/watch/source-fields.ts
@@ -265,10 +279,16 @@ var DEFAULT_MODELS = {
265
279
  var isTerminalAdapter = (value) => Object.values(TERMINAL_ADAPTER).includes(value);
266
280
  var isModelRole = (value) => Object.values(MODEL_ROLE).includes(value);
267
281
  var SKILL_PREFIX = /^[a-z0-9]+-$/;
282
+ var DEFAULT_SKILL_PREFIX = "jh-";
283
+ var KIT_CORE = ["orchestrate", "start"];
284
+ var KIT_ENTRY_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
268
285
  var FULL_MODEL_ID = /^claude-[a-z0-9][a-z0-9.-]*$/;
269
286
  var language = z2.enum(Object.values(PLAN_LANGUAGE));
270
287
  var modelId = z2.string().regex(FULL_MODEL_ID, "Use a full model ID such as claude-opus-5-5, not an alias like opus or sonnet");
271
288
  var command2 = z2.string().min(1);
289
+ var excludedKitEntry = z2.string().regex(KIT_ENTRY_NAME, "a kit skill or agent name as the kit names it, e.g. devops").refine((name) => !KIT_CORE.includes(name), {
290
+ error: (issue) => `${issue.input} is the kit's core and cannot be excluded`
291
+ });
272
292
  var tokenCount = z2.number().int().positive();
273
293
  var commandsSchema = z2.strictObject({
274
294
  test: command2.optional(),
@@ -457,7 +477,13 @@ var mergeSchema = z2.strictObject({
457
477
  * of them, `/orchestrate` merges at once and the merge guard does not ask, for a merge bound to the head commit with
458
478
  * `--match-head-commit`. Empty means every merge waits for checks.
459
479
  */
460
- withoutChecks: z2.array(checkFreePattern).default([])
480
+ withoutChecks: z2.array(checkFreePattern).default([]),
481
+ /**
482
+ * Whether a lane opens every pull request as a draft (`gh pr create --draft`), so pushes while it is worked on start
483
+ * no workflow that skips drafts. `/orchestrate` runs `gh pr ready` before the merge and waits for the checks that
484
+ * start then; the merge guard never lets a draft through. Off opens pull requests ready, as before.
485
+ */
486
+ openAsDraft: z2.boolean().default(false)
461
487
  });
462
488
  var guardFields = z2.strictObject({
463
489
  /**
@@ -551,6 +577,11 @@ var jackherdConfigSchema = z2.strictObject({
551
577
  */
552
578
  skillPrefix: z2.union([z2.literal(""), z2.string().regex(SKILL_PREFIX, "lower-case letters and digits ending in a dash, e.g. kit-")]).default(""),
553
579
  commands: commandsSchema.prefault({}),
580
+ /**
581
+ * Kit skills and agents `jh sync` leaves out, by the kit's name without `skillPrefix`; a name takes its skill and
582
+ * its agent alike. An unchanged copy already installed is removed, a changed one stays untracked.
583
+ */
584
+ kit: z2.strictObject({ exclude: z2.array(excludedKitEntry).default([]) }).prefault({}),
554
585
  /** How `/orchestrate` sets up a lane's worktree. */
555
586
  lanes: z2.strictObject({
556
587
  /** Untracked files copied into every new worktree; empty means the files `.worktreeinclude` lists. */
@@ -626,6 +657,8 @@ export {
626
657
  COMMAND_KEYS,
627
658
  DEFAULT_CONTEXT_WINDOW,
628
659
  DEFAULT_MODELS,
660
+ DEFAULT_SKILL_PREFIX,
661
+ KIT_CORE,
629
662
  MODEL_ROLE,
630
663
  SKILL_PREFIX,
631
664
  TERMINAL_ADAPTER,
@@ -382,6 +382,12 @@ type OpenLaneRow = {
382
382
  };
383
383
  /** Sprint tables name a phase `P4`; the sidebar title prints it the same way. */
384
384
  declare const PHASE_ID_PREFIX = "P";
385
+ /** The phase a table's first cell names (`1 — Title`, `P1`, `10c`), lower-cased; null when it names none. */
386
+ declare const phaseIdOf: (cell: string) => string | null;
387
+ /** What a report says when the plan has no dependency table. */
388
+ declare const NO_DEPENDENCY_TABLE = "no dependency table: phases run one after another";
389
+ /** `1 phase`, `3 phases`. */
390
+ declare const phaseCount: (count: number) => string;
385
391
  /** A phase as plans and reports name it: `P4`. */
386
392
  declare const phaseLabel: (id: string) => string;
387
393
  /** Phases as plans and reports name them, joined by `separator`: `P3 P4`, `P19 ∥ P20`. */
@@ -418,6 +424,23 @@ declare const columnModels: (column: string) => ColumnModel[];
418
424
  */
419
425
  declare const differsFromColumn: (column: string, modelId: string) => boolean;
420
426
 
427
+ /**
428
+ * A row of the model table: the phase it names, the models its model cell names (none for a dash), the model cell as
429
+ * written, and the row's 1-based line.
430
+ */
431
+ type ModelRow = {
432
+ id: string;
433
+ models: ColumnModel[];
434
+ cell: string;
435
+ line: number;
436
+ };
437
+ /**
438
+ * The rows of the plan's model table (`### Model-Empfehlungen pro Phase`): the phase from the first cell (`1 — Title`,
439
+ * `P1`, `10c`), the models from the second, read like a board's model column. A row whose first cell names no phase is
440
+ * skipped; null when the plan has no such section or the section holds no table.
441
+ */
442
+ declare const parseModelTable: (markdown: string, language: PlanLanguage) => ModelRow[] | null;
443
+
421
444
  /** Per heading that carries checkboxes or table status cells: how many are done and open. */
422
445
  type PhaseTally = {
423
446
  title: string;
@@ -591,4 +614,4 @@ declare const readSprintDocument: (markdown: string, language: PlanLanguage) =>
591
614
  */
592
615
  declare const readPhaseDependencies: (planMarkdown: string, language: PlanLanguage) => Record<string, string>;
593
616
 
594
- export { ADDRESS_FORM, APPOINTMENT_VOCABULARY, APPROVAL_TEXTS, type ChecklistItem, type ColumnModel, type DeferredItem, type DriftFinding, LANE_PROMPT_OPENING, LANGUAGE_NAME, type LaneProgress, type LongPhaseTitle, type MergedPhaseWithOpenBoxes, type MisplacedDeferral, NEXT_VOCABULARY, PHASE_CELL_LIMIT, PHASE_ID_PREFIX, PHASE_TITLE_LIMIT, PHASE_WORK, PLAN_LANGUAGE, PLAN_STATUS, PLAN_VOCABULARY, PROSE_REASON, type PhaseDependency, type PhaseHeading, type PhaseProgress, type PhaseSchedule, type PhaseTally, type PhaseWithoutGate, type PhaseWithoutRow, type PhaseWork, type PhaseWorkFields, type PlanHeader, type PlanLanguage, type PlanPhase, type PlanStatus, type PlanStep, type PlanVocabulary, type PlansByHref, type ProseReason, RESPONSE_ADDRESS, type ResponseAddress, type RowWithProse, type RowWithoutPhase, SELF_MERGE_LABEL, SHORT_NAME_LIMIT, SHORT_NAME_PROBLEM, STANDING_APPROVAL_TEXTS, type ShortNameFinding, type ShortNameProblem, type SprintDocument, type SprintLane, type SprintPlanRow, type SprintPlanTable, type StatusWord, type StepProgress, WAITING_VOCABULARY, activeItem, boardRowsWithProse, boardRowsWithoutPhase, classifyStatus, columnModels, countDecisions, criticalPath, currentPhase, currentPhaseOf, deferralTarget, differsFromColumn, endsPhase, findDrift, hasLanesTable, isCheckboxLine, isDocumentationGateItem, isOpenCheckboxLine, isPhaseFinished, isPhaseOpen, isPlanLanguage, isReviewGateLine, isTickedUnmerged, laneNameOf, laneProgress, longPhaseTitles, mergedPhasesWithOpenBoxes, misplacedDeferrals, newPhasesWithoutReviewGate, onlyDocumentationOpen, parallelGroups, parallelGroupsOfPlan, parseDependencyTable, parsePlanPhases, parseSprintLanes, phaseHeading, phaseHeadingPattern, phaseHeadings, phaseIdsOfCell, phaseIdsOfPlan, phaseLabel, phaseLabels, phaseProgress, phaseWaves, phaseWorkFields, phasesText, phasesWithoutDependencyRow, readPhaseDependencies, readPlanHeader, readSprintDocument, resolvePlanPath, sectionHeading, shortNameFindings, shortTitle, sprintTitleOf, sumPhases, tallyPhases, wavesOfPlan, withoutRepeats };
617
+ export { ADDRESS_FORM, APPOINTMENT_VOCABULARY, APPROVAL_TEXTS, type ChecklistItem, type ColumnModel, type DeferredItem, type DriftFinding, LANE_PROMPT_OPENING, LANGUAGE_NAME, type LaneProgress, type LongPhaseTitle, type MergedPhaseWithOpenBoxes, type MisplacedDeferral, type ModelRow, NEXT_VOCABULARY, NO_DEPENDENCY_TABLE, PHASE_CELL_LIMIT, PHASE_ID_PREFIX, PHASE_TITLE_LIMIT, PHASE_WORK, PLAN_LANGUAGE, PLAN_STATUS, PLAN_VOCABULARY, PROSE_REASON, type PhaseDependency, type PhaseHeading, type PhaseProgress, type PhaseSchedule, type PhaseTally, type PhaseWithoutGate, type PhaseWithoutRow, type PhaseWork, type PhaseWorkFields, type PlanHeader, type PlanLanguage, type PlanPhase, type PlanStatus, type PlanStep, type PlanVocabulary, type PlansByHref, type ProseReason, RESPONSE_ADDRESS, type ResponseAddress, type RowWithProse, type RowWithoutPhase, SELF_MERGE_LABEL, SHORT_NAME_LIMIT, SHORT_NAME_PROBLEM, STANDING_APPROVAL_TEXTS, type ShortNameFinding, type ShortNameProblem, type SprintDocument, type SprintLane, type SprintPlanRow, type SprintPlanTable, type StatusWord, type StepProgress, WAITING_VOCABULARY, activeItem, boardRowsWithProse, boardRowsWithoutPhase, classifyStatus, columnModels, countDecisions, criticalPath, currentPhase, currentPhaseOf, deferralTarget, differsFromColumn, endsPhase, findDrift, hasLanesTable, isCheckboxLine, isDocumentationGateItem, isOpenCheckboxLine, isPhaseFinished, isPhaseOpen, isPlanLanguage, isReviewGateLine, isTickedUnmerged, laneNameOf, laneProgress, longPhaseTitles, mergedPhasesWithOpenBoxes, misplacedDeferrals, newPhasesWithoutReviewGate, onlyDocumentationOpen, parallelGroups, parallelGroupsOfPlan, parseDependencyTable, parseModelTable, parsePlanPhases, parseSprintLanes, phaseCount, phaseHeading, phaseHeadingPattern, phaseHeadings, phaseIdOf, phaseIdsOfCell, phaseIdsOfPlan, phaseLabel, phaseLabels, phaseProgress, phaseWaves, phaseWorkFields, phasesText, phasesWithoutDependencyRow, readPhaseDependencies, readPlanHeader, readSprintDocument, resolvePlanPath, sectionHeading, shortNameFindings, shortTitle, sprintTitleOf, sumPhases, tallyPhases, wavesOfPlan, withoutRepeats };
@@ -69,9 +69,10 @@ var headerField = (markdown, name) => {
69
69
  };
70
70
  var firstHeading = (markdown, fallback) => terminalSafe(markdown.match(/^#\s+(.+)$/m)?.[1] ?? fallback).trim();
71
71
  var HEADING = /^(#{2,4})\s+(.*?)\s*$/;
72
+ var subsectionStart = (markdown, headingText) => markdown.split("\n").findIndex((line) => HEADING.exec(line)?.[2].startsWith(headingText));
72
73
  var subsectionLines = (markdown, headingText) => {
73
74
  const lines = markdown.split("\n");
74
- const start = lines.findIndex((line) => HEADING.exec(line)?.[2].startsWith(headingText));
75
+ const start = subsectionStart(markdown, headingText);
75
76
  if (start === -1) return [];
76
77
  const level = HEADING.exec(lines[start])?.[1].length ?? 2;
77
78
  const rest = lines.slice(start + 1);
@@ -102,6 +103,10 @@ var LANE_NAME = /`([^`]+)`/;
102
103
  var CONTINUATION = "\u21B3";
103
104
  var PLAN_LINK = /\[([^\]]+)\]\(([^)]+)\)/;
104
105
  var PHASE_ID_PREFIX = "P";
106
+ var LEADING_PHASE_ID = new RegExp(`^\\s*${PHASE_ID_PREFIX}?(\\d+[a-z]?)\\b`, "i");
107
+ var phaseIdOf = (cell) => LEADING_PHASE_ID.exec(cell)?.[1]?.toLowerCase() ?? null;
108
+ var NO_DEPENDENCY_TABLE = "no dependency table: phases run one after another";
109
+ var phaseCount = (count) => `${count} ${count === 1 ? "phase" : "phases"}`;
105
110
  var phaseLabel = (id) => `${PHASE_ID_PREFIX}${id}`;
106
111
  var MAX_RANGE = 50;
107
112
  var spannedPhaseIds = (first, last) => first > last || last - first >= MAX_RANGE ? [] : Array.from({ length: last - first + 1 }, (_, offset) => String(first + offset));
@@ -467,7 +472,6 @@ var shortTitle = (progress) => {
467
472
 
468
473
  // src/plan-format/dependencies.ts
469
474
  var ID = "(\\d+[a-z]?)";
470
- var PHASE_ID = new RegExp(`^\\s*${PHASE_ID_PREFIX}?${ID}\\b`, "i");
471
475
  var PHASE_RANGE = new RegExp(`^${PHASE_ID_PREFIX}?(\\d+)\\s*[\u2013-]\\s*${PHASE_ID_PREFIX}?(\\d+)$`, "i");
472
476
  var BACKTICKED = /`([^`]+)`/g;
473
477
  var NO_VALUE = /^[—–-]?$/;
@@ -476,7 +480,6 @@ var GLOB_CHARACTER = /[*?{[]/;
476
480
  var VERSION = /^v?\d+(?:[.\-+][\w.-]*)?$/i;
477
481
  var PACKAGE_SCOPE = /^@[^/]+\//;
478
482
  var RELEASE_FILE = /(?:^|\/)(?:CHANGELOG\.md|package\.json)$/;
479
- var phaseIdOf = (cell) => PHASE_ID.exec(cell)?.[1]?.toLowerCase() ?? null;
480
483
  var splitOutsideParentheses = (cell, separator) => {
481
484
  const entries = [];
482
485
  let depth = 0;
@@ -749,11 +752,24 @@ var differsFromColumn = (column, modelId) => {
749
752
  return models.length > 0 && !models.some((model) => fits(tokens, model));
750
753
  };
751
754
 
755
+ // src/plan-format/model-table.ts
756
+ var parseModelTable = (markdown, language) => {
757
+ const heading = PLAN_VOCABULARY[language].sections.modelRecommendations;
758
+ const rows = firstTableRows(subsectionLines(markdown, heading));
759
+ if (rows.length === 0) return null;
760
+ const firstBodyLine = subsectionStart(markdown, heading) + 2;
761
+ return rows.flatMap(({ index, cells }) => {
762
+ const id = phaseIdOf(cells[0] ?? "");
763
+ const cell = cells[1] ?? "";
764
+ return id === null ? [] : [{ id, models: columnModels(cell), cell, line: firstBodyLine + index }];
765
+ });
766
+ };
767
+
752
768
  // src/plan-format/phase-tally.ts
753
769
  var DONE_MARK = /^\s*[-*]\s\[x\]|\|\s*✅\s*\|/gim;
754
770
  var OPEN_MARK = /^\s*[-*]\s\[ \]|\|\s*⬜\s*\|/gm;
755
771
  var TRAILING_SEPARATOR = /[:—–-]\s*$/;
756
- var PHASE_ID2 = /^(\d+[a-z]?)/;
772
+ var PHASE_ID = /^(\d+[a-z]?)/;
757
773
  var modelFor = (markdown, phaseWord, phaseTitle) => {
758
774
  const prefix = new RegExp(`^${escapeRegExp(phaseWord)}\\s*`, "i");
759
775
  const number = /^(\d+)/.exec(phaseTitle.replace(prefix, ""))?.[1];
@@ -765,7 +781,7 @@ var sectionOf = (section, heading, markdown, phaseWord) => {
765
781
  const match = heading.exec(section);
766
782
  if (!match) return null;
767
783
  const title = match[1].replace(TRAILING_SEPARATOR, "").trim();
768
- const phaseId = PHASE_ID2.exec(title)?.[1]?.toLowerCase() ?? "";
784
+ const phaseId = PHASE_ID.exec(title)?.[1]?.toLowerCase() ?? "";
769
785
  const targets = section.split("\n").flatMap((line) => deferralTarget(line, phaseId) ?? []);
770
786
  const done = (section.match(DONE_MARK) ?? []).length;
771
787
  const open = (section.match(OPEN_MARK) ?? []).length - targets.length;
@@ -911,6 +927,7 @@ export {
911
927
  LANE_PROMPT_OPENING,
912
928
  LANGUAGE_NAME,
913
929
  NEXT_VOCABULARY,
930
+ NO_DEPENDENCY_TABLE,
914
931
  PHASE_CELL_LIMIT,
915
932
  PHASE_ID_PREFIX,
916
933
  PHASE_TITLE_LIMIT,
@@ -957,11 +974,14 @@ export {
957
974
  parallelGroups,
958
975
  parallelGroupsOfPlan,
959
976
  parseDependencyTable,
977
+ parseModelTable,
960
978
  parsePlanPhases,
961
979
  parseSprintLanes,
980
+ phaseCount,
962
981
  phaseHeading,
963
982
  phaseHeadingPattern,
964
983
  phaseHeadings,
984
+ phaseIdOf,
965
985
  phaseIdsOfCell,
966
986
  phaseIdsOfPlan,
967
987
  phaseLabel,
@@ -0,0 +1,84 @@
1
+ # The plan contract
2
+
3
+ jackherd reads a plan from its Markdown. This is what a plan has to hold for the readers to find it, and how to check that with one command.
4
+
5
+ ## The four parts
6
+
7
+ | Part | Form | Required | Read by |
8
+ | ---- | ---- | -------- | ------- |
9
+ | Phases | a heading per phase: `### Phase 1: Title` (also `## 1 — Title` or `## P1 — Title`) | yes | herd, watcher, `jh check`, lane start |
10
+ | Tasks | at least one checkbox (`- [ ]`, `- [x]`) or a ✅/⬜ table row in every phase; a phase that points at a section (``See `## Documentation` ``) counts that section's checkboxes | yes | herd (progress), watcher, `jh check` |
11
+ | Model | a table under `### Model recommendations per phase`: the phase in the first cell (`1 — Title`, `P1`, `10c`), the model in the second (`claude-opus-5-5`, `Opus 5.5`); a dash marks a phase that needs no model | yes | lane start (the model a lane runs on), `/implement` |
12
+ | Dependencies | a table under `### Dependencies and parallelization` with the columns `Depends on`, `Writes`, `Serial marker` and one row per phase | from two phases on, optional | `jh check --parallel`, `/implement` |
13
+
14
+ Details that decide a verdict:
15
+
16
+ - A sub-phase (`3b`) is covered by the row of its base phase (`3`) in the model and the dependency table, or by a row of its own.
17
+ - A model cell that names no model and is no dash is a gap: the lane would start on a guess.
18
+ - With fewer than two phases the dependency part is reported `present` ("not needed"), never missing.
19
+ - With two or more phases and no dependency table the plan is still valid: the phases run one after another. A table that is there but lacks a row for a phase is a gap.
20
+ - The headings come from the plan language in `.jackherd/config.json` (`planLanguage`, `de` or `en`); the English ones are shown here.
21
+
22
+ ## Minimal valid plan
23
+
24
+ ```markdown
25
+ # Search filter
26
+
27
+ **Date:** YYYY-MM-DD
28
+ **Status:** Draft
29
+
30
+ ## Implementation
31
+
32
+ ### Model recommendations per phase
33
+
34
+ | Phase | Model | Rationale |
35
+ | ----- | ----- | --------- |
36
+ | 1 — Query parser | claude-sonnet-5-5 | pure functions, cheap to review |
37
+ | 2 — Filter bar | claude-opus-5-5 | touches the shared state |
38
+
39
+ ### Dependencies and parallelization
40
+
41
+ | Phase | Depends on | Writes | Serial marker |
42
+ | ----- | ---------- | ------ | ------------- |
43
+ | 1 | — | `src/query/` | — |
44
+ | 2 | 1 | `src/ui/filter-bar/` | — |
45
+
46
+ ### Phase 1: Query parser
47
+
48
+ - [ ] **Parse filters**: `parseQuery(text)` returns the filter list
49
+ - [ ] **Test it**: every operator, one malformed input
50
+
51
+ ### Phase 2: Filter bar
52
+
53
+ - [ ] **Render the bar**: one chip per filter
54
+ ```
55
+
56
+ ## Checking a plan
57
+
58
+ ```
59
+ jh plan check <plan.md>
60
+ ```
61
+
62
+ Prints one line per part, `file:line` where the part is incomplete. For the plan above:
63
+
64
+ ```
65
+ phases: present plan.md:22 2 phases
66
+ tasks: present plan.md:22 every phase has tasks
67
+ model: present plan.md:12 every phase has a model
68
+ dependencies: present every phase has a row
69
+ ```
70
+
71
+ If the model table lacked the row for phase 2, and the dependency table were left out, the lines would read:
72
+
73
+ ```
74
+ phases: present plan.md:22 2 phases
75
+ tasks: present plan.md:22 every phase has tasks
76
+ model: malformed plan.md:22 no model for P2
77
+ dependencies: missing no dependency table: phases run one after another
78
+ ```
79
+
80
+ | Exit | Meaning |
81
+ | ---- | ------- |
82
+ | 0 | the contract is met (a missing dependency table is a note, not a gap) |
83
+ | 1 | at least one part is missing or malformed |
84
+ | 2 | wrong command line: no subcommand, unknown subcommand, no file, unreadable file, unreadable config |