jackherd 1.12.7 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/INSTALL.md +1 -1
  3. package/README.md +17 -17
  4. package/dist/config.d.ts +35 -11
  5. package/dist/config.js +49 -83
  6. package/herdr-plugin/herdr-plugin.toml +1 -1
  7. package/install.sh +1 -1
  8. package/kit/hooks/hooks/merge-guard/board-settings.ts +7 -32
  9. package/kit/hooks/hooks/merge-guard/lane-plan.ts +2 -29
  10. package/kit/hooks/hooks/plain-question/code-cache.ts +70 -0
  11. package/kit/hooks/hooks/plain-question/index.ts +80 -0
  12. package/kit/hooks/hooks/plain-question/run.ts +50 -0
  13. package/kit/hooks/hooks/shared/board-lanes.ts +19 -0
  14. package/kit/hooks/hooks/shared/boards.ts +96 -0
  15. package/kit/hooks/hooks/shared/plan-codes.ts +68 -0
  16. package/kit/hooks/hooks.json +9 -0
  17. package/kit/review/skills/diagnose/SKILL.md +2 -2
  18. package/kit/review/skills/diagnose/references/feedback-loops.md +4 -13
  19. package/kit/rules/rules/kit/code-style.md +15 -18
  20. package/kit/rules/rules/kit/testing.md +7 -28
  21. package/kit/workflow/skills/implement/SKILL.md +3 -3
  22. package/kit/workflow/skills/orchestrate/SKILL.md +4 -1
  23. package/kit/workflow/skills/orchestrate/references/events.md +6 -1
  24. package/kit/workflow/skills/orchestrate/references/plan-commit.md +2 -1
  25. package/kit/workflow/skills/start/SKILL.md +9 -6
  26. package/kit/workflow/skills/start/references/missing-commands.md +17 -0
  27. package/package.json +1 -1
  28. package/schemas/jackherd.schema.json +38 -11
  29. package/src/check/command.ts +167 -35
  30. package/src/check/file-kinds.ts +142 -0
  31. package/src/check/plan-structure-check.ts +34 -1
  32. package/src/check/plan.ts +230 -45
  33. package/src/check/report.ts +111 -3
  34. package/src/check/result.ts +13 -3
  35. package/src/check/roots.ts +47 -10
  36. package/src/check/schema.ts +20 -3
  37. package/src/check/settings.ts +25 -4
  38. package/src/check/skill-budget-defaults.ts +7 -4
  39. package/src/config/base-branch.ts +2 -0
  40. package/src/config/command.ts +32 -13
  41. package/src/config/detect.ts +242 -74
  42. package/src/config/location.ts +6 -0
  43. package/src/config/repository-facts.ts +32 -7
  44. package/src/config/repository-values.ts +162 -5
  45. package/src/config/schema.ts +34 -16
  46. package/src/lane-prompt/texts.ts +2 -2
  47. package/src/plan-format/bare-codes.ts +28 -0
  48. package/src/plans/lane-plans.ts +2 -1
  49. package/src/sync/command-context.ts +4 -2
  50. package/src/sync/herdr-plugin.ts +30 -6
  51. package/src/sync/init.ts +17 -16
  52. package/src/sync/render.ts +4 -6
  53. package/src/watch/command.ts +32 -1
  54. package/src/watch/lane-progress.ts +6 -1
  55. package/src/watch/lane-report.ts +10 -0
  56. package/src/watch/plan-missing-read.ts +47 -0
  57. package/src/watch/plan-missing.ts +43 -0
  58. package/src/watch/reference-paths.ts +1 -0
  59. package/src/watch/texts.ts +4 -0
  60. package/src/watch/watch-events.ts +7 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  What changed for people who use jackherd, newest first.
4
4
 
5
+ ## 1.14.0
6
+
7
+ - **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.
8
+ - **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.
9
+
10
+ ## 1.13.0
11
+
12
+ - **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/`.
13
+ - **An unset command stays unset.** `commands.*`, `lanes.lockFile` and `check.lintFilesCommand` have no Bun default any more. A kit text shows "not set: `commands.<key>` in `.jackherd/config.json`" in place of the command, and `models` has a default, so a config written by hand without it no longer stops `init`.
14
+ - **`jh config` names what is missing.** A missing command, or one the lock files contradict (`bun test` beside only `composer.lock`), goes to stderr with the line to set; `/jh-start` passes those lines on. `jh config <key>` for an unset value exits 3 instead of printing "not set" as if it were the command.
15
+ - **No green without a test.** `jh check` finds roots by the stack's manifest (`composer.json` as well as `package.json`), counts the repository root unless it gathers workspaces, and lints only the files the stack lints. It fails when a changed source file is reached by no test, judged per file (kit copies are tested where the kit keeps them), and when `commands.test` belongs to another stack than the lock file. A `check.checks` entry with `"kind": "tests"` counts as a test for the files its globs match.
16
+ - **`jh init` in another repository keeps the herdr link.** Run from a checkout of jackherd for a repository that is not the checkout's own, `init` and `sync` leave a working herdr link where it is.
17
+ - **Kit rules without a language.** The kit's rules and the diagnose and implement skills no longer name Bun, Zod, Biome, `tsconfig` or `mock.module()`; a team writes its stack's conventions into a rule of its own beside `.claude/rules/kit/`. The README has a section on other languages.
18
+ - **A dialog that names a plan code bare is sent back.** The new hook `plain-question` (`PreToolUse` on `AskUserQuestion`) reads the codes the plans linked from the boards define (phases as `P<id>`, codes that open a table row, a heading or a bold list item; else `F`/`P`/`D`/`E` plus a number) and denies a question, header or option naming one outside brackets, so the user reads words, not shorthand. `jh check` warns about a changed "Waiting for you" row doing the same. The orchestrate skill and the lane prompt say it too.
19
+
5
20
  ## 1.12.7
6
21
 
7
22
  - **Newest first in the herd.** The rows of Your turn that wait on your answer (the board's open questions, a session's questions, the files and Artifacts sent to you) stand newest first; what frees a lane still comes before the rest, newest first within it. A since without a year counts in the year nearest to now, so a row of 31 December stands under one of 1 January. With more than five rows, the oldest are the ones under `▸ n more`. An opened source lists its entries newest first by the time each shows: before, an entry with its own time from the source (say, when an error was last seen) could stand among entries in the order the watcher saw them change. Rows of the same time keep their order. Appointments keep their order, the ones without a time first, then the next due.
package/INSTALL.md CHANGED
@@ -54,7 +54,7 @@ When `init` creates a new config, it asks:
54
54
  | Use it when | the team shares the kit through the repository | the repository must stay untouched, not even untracked files |
55
55
  | Lives in | the project's `.claude/` | `~/.claude/` |
56
56
  | Skill names | `/plan`, `/implement`, `/orchestrate`, … | `/jh-plan`, `/jh-implement`, `/jh-orchestrate`, … |
57
- | Hooks | `git-guard`, `watch-guard`, `merge-guard`, `approval-gate`, `model-gate`, `model-log`, `ask-hint`, `open-question`, `sent-file`, `format-on-edit` | none; `init --global` offers only a `SessionStart` update notice |
57
+ | Hooks | `git-guard`, `watch-guard`, `merge-guard`, `approval-gate`, `model-gate`, `model-log`, `ask-hint`, `open-question`, `plain-question`, `sent-file`, `format-on-edit` | none; `init --global` offers only a `SessionStart` update notice |
58
58
  | Config | `.jackherd/config.json` | `~/.jackherd/config.json`, used where a repository has none |
59
59
  | Update | `jh sync` | `jh update` |
60
60
 
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.12.7/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.14.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.12.7/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.14.0/assets/icons/space.svg">
7
7
 
8
8
  # Jack herds your coding agents.
9
9
 
@@ -15,25 +15,25 @@ 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.12.7/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.14.0/assets/icons/space.svg">
19
19
 
20
20
  <p>
21
- <img src="https://unpkg.com/jackherd@1.12.7/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.14.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.12.7/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.14.0/assets/icons/space.svg">
24
24
  </p>
25
25
 
26
- <p><img src="https://unpkg.com/jackherd@1.12.7/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.14.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.12.7/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.14.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>
29
29
 
30
- <p><img src="https://unpkg.com/jackherd@1.12.7/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.14.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>
31
31
 
32
- <p><img src="https://unpkg.com/jackherd@1.12.7/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.14.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>
33
33
 
34
- <p><img src="https://unpkg.com/jackherd@1.12.7/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.14.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>
35
35
 
36
- <p><img src="https://unpkg.com/jackherd@1.12.7/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.12.7/assets/icons/space.svg"></p>
36
+ <p><img src="https://unpkg.com/jackherd@1.14.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.14.0/assets/icons/space.svg"></p>
37
37
 
38
38
  ## Quickstart
39
39
 
@@ -69,22 +69,22 @@ Then `/jh-start` in a repository, and Jack offers your first plan. → [Quicksta
69
69
  /jh-orchestrate docs/plans/board-<name>/board.md
70
70
  ```
71
71
 
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.12.7/assets/icons/space.svg">
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.14.0/assets/icons/space.svg">
73
73
 
74
74
  ## Follow anything outside GitHub
75
75
 
76
76
  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
77
 
78
- <img src="https://unpkg.com/jackherd@1.12.7/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.">
78
+ <img src="https://unpkg.com/jackherd@1.14.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
79
 
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.12.7/assets/icons/space.svg">
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.14.0/assets/icons/space.svg">
81
81
 
82
82
  ## In detail
83
83
 
84
84
  <details>
85
85
  <summary><b>What Jack does on his own, and what waits for you</b></summary>
86
86
 
87
- <img src="https://unpkg.com/jackherd@1.12.7/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.">
87
+ <img src="https://unpkg.com/jackherd@1.14.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
88
 
89
89
  | On his own | After your approval | Never |
90
90
  | --- | --- | --- |
@@ -117,7 +117,7 @@ The guards catch mistakes, not intent; for that, use branch protection. The hook
117
117
 
118
118
  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
119
 
120
- <img src="https://unpkg.com/jackherd@1.12.7/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.">
120
+ <img src="https://unpkg.com/jackherd@1.14.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.">
121
121
 
122
122
  | Skill | Does |
123
123
  | --- | --- |
@@ -222,7 +222,7 @@ Each phase in the plan names its model: the planning model (Opus by default) whe
222
222
  | `jh preview <file>` | renders a Markdown file through the GitHub API into a preview in GitHub and npm style |
223
223
  | `jh waiting` | the files and Artifacts sent to you that you have not answered yet (`close <id>…` or `close --all` once you have) |
224
224
  | `jh context` | how full an agent's context is, as JSON |
225
- | `jh config` | the commands and the base branch the repository uses |
225
+ | `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
226
 
227
227
  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
228
 
package/dist/config.d.ts CHANGED
@@ -15,10 +15,27 @@ declare const MODEL_ROLE: {
15
15
  readonly orchestrate: "orchestrate";
16
16
  };
17
17
  type ModelRole = (typeof MODEL_ROLE)[keyof typeof MODEL_ROLE];
18
+ /** The model of each role when the config names none: a new project's, and one written by hand without `models`. */
19
+ declare const DEFAULT_MODELS: Record<ModelRole, string>;
18
20
  declare const isTerminalAdapter: (value: string) => value is TerminalAdapter;
19
21
  declare const isModelRole: (value: string) => value is ModelRole;
20
22
  /** Lower-case letters and digits ending in a dash, so a prefixed name stays one plain path segment. */
21
23
  declare const SKILL_PREFIX: RegExp;
24
+ /**
25
+ * The project's own commands. Each is optional: detection fills what the repository's stack decides, and a command
26
+ * nobody set stays out instead of a guessed one (a kit text then says where to set it).
27
+ */
28
+ declare const commandsSchema: z.ZodObject<{
29
+ test: z.ZodOptional<z.ZodString>;
30
+ typecheck: z.ZodOptional<z.ZodString>;
31
+ lint: z.ZodOptional<z.ZodString>;
32
+ format: z.ZodOptional<z.ZodString>;
33
+ install: z.ZodOptional<z.ZodString>;
34
+ dev: z.ZodOptional<z.ZodString>;
35
+ }, z.core.$strict>;
36
+ type CommandKey = keyof z.infer<typeof commandsSchema>;
37
+ /** Every key of the `commands` block, as the schema names them. */
38
+ declare const COMMAND_KEYS: CommandKey[];
22
39
  /** The window `plans.context.defaultWindow` assumes when the config leaves it out. */
23
40
  declare const DEFAULT_CONTEXT_WINDOW = 200000;
24
41
  declare const sourceSchema: z.ZodObject<{
@@ -77,24 +94,24 @@ declare const jackherdConfigSchema: z.ZodObject<{
77
94
  baseBranch: z.ZodDefault<z.ZodString>;
78
95
  skillPrefix: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<"">, z.ZodString]>>;
79
96
  commands: z.ZodPrefault<z.ZodObject<{
80
- test: z.ZodDefault<z.ZodString>;
81
- typecheck: z.ZodDefault<z.ZodString>;
82
- lint: z.ZodDefault<z.ZodString>;
83
- format: z.ZodDefault<z.ZodString>;
84
- install: z.ZodDefault<z.ZodString>;
85
- dev: z.ZodDefault<z.ZodString>;
97
+ test: z.ZodOptional<z.ZodString>;
98
+ typecheck: z.ZodOptional<z.ZodString>;
99
+ lint: z.ZodOptional<z.ZodString>;
100
+ format: z.ZodOptional<z.ZodString>;
101
+ install: z.ZodOptional<z.ZodString>;
102
+ dev: z.ZodOptional<z.ZodString>;
86
103
  }, z.core.$strict>>;
87
104
  lanes: z.ZodPrefault<z.ZodObject<{
88
105
  copy: z.ZodDefault<z.ZodArray<z.ZodString>>;
89
- lockFile: z.ZodDefault<z.ZodString>;
106
+ lockFile: z.ZodOptional<z.ZodString>;
90
107
  }, z.core.$strict>>;
91
- models: z.ZodRecord<z.ZodEnum<{
108
+ models: z.ZodDefault<z.ZodRecord<z.ZodEnum<{
92
109
  plan: "plan";
93
110
  implement: "implement";
94
111
  review: "review";
95
112
  explore: "explore";
96
113
  orchestrate: "orchestrate";
97
- }>, z.ZodString>;
114
+ }>, z.ZodString>>;
98
115
  terminal: z.ZodPrefault<z.ZodObject<{
99
116
  adapter: z.ZodDefault<z.ZodEnum<{
100
117
  none: "none";
@@ -277,11 +294,15 @@ declare const jackherdConfigSchema: z.ZodObject<{
277
294
  taskSignal: z.ZodDefault<z.ZodBoolean>;
278
295
  }, z.core.$strict>>;
279
296
  check: z.ZodOptional<z.ZodObject<{
280
- lintFilesCommand: z.ZodDefault<z.ZodString>;
297
+ lintFilesCommand: z.ZodOptional<z.ZodString>;
281
298
  checks: z.ZodDefault<z.ZodArray<z.ZodObject<{
282
299
  name: z.ZodString;
283
300
  command: z.ZodString;
284
301
  globs: z.ZodOptional<z.ZodArray<z.ZodString>>;
302
+ kind: z.ZodOptional<z.ZodEnum<{
303
+ types: "types";
304
+ tests: "tests";
305
+ }>>;
285
306
  }, z.core.$strict>>>;
286
307
  generatorOwned: z.ZodDefault<z.ZodArray<z.ZodObject<{
287
308
  glob: z.ZodString;
@@ -300,6 +321,9 @@ declare const jackherdConfigSchema: z.ZodObject<{
300
321
  }, z.core.$strict>>;
301
322
  skillBudgets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
302
323
  looseTests: z.ZodDefault<z.ZodBoolean>;
324
+ roots: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
325
+ sourceFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
326
+ testFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
303
327
  }, z.core.$strict>>;
304
328
  }, z.core.$strict>;
305
329
  type JackherdConfig = z.infer<typeof jackherdConfigSchema>;
@@ -309,4 +333,4 @@ declare const parseConfigFile: (raw: unknown, label: string) => JackherdConfig;
309
333
  /** One `watch.sources` entry outside the config, such as a draft to try; invalid throws with `label` and the issues. */
310
334
  declare const parseSourceDraft: (raw: unknown, label: string) => z.infer<typeof sourceSchema>;
311
335
 
312
- export { DEFAULT_CONTEXT_WINDOW, type JackherdConfig, MODEL_ROLE, type ModelRole, SKILL_PREFIX, TERMINAL_ADAPTER, type TerminalAdapter, isModelRole, isTerminalAdapter, jackherdConfigSchema, parseConfigFile, parseJackherdConfig, parseSourceDraft, renderConfigJsonSchema };
336
+ 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 };
package/dist/config.js CHANGED
@@ -12,72 +12,6 @@ import { z as z2 } from "zod";
12
12
  // src/check/schema.ts
13
13
  import { z } from "zod";
14
14
 
15
- // src/version/release.ts
16
- var PACKAGE_NAME = "jackherd";
17
-
18
- // src/config/location.ts
19
- import { dirname, join, resolve } from "path";
20
- var TOOL_DIRECTORY = ".jackherd";
21
- var CONFIG_FILE = "config.json";
22
- var CONFIG_SCHEMA_REFERENCE = `../node_modules/${PACKAGE_NAME}/schemas/${PACKAGE_NAME}.schema.json`;
23
- var PROJECT_CONFIG_PATH = `${TOOL_DIRECTORY}/${CONFIG_FILE}`;
24
-
25
- // src/config/detect.ts
26
- var PACKAGE_MANAGER = { bun: "bun", yarn: "yarn", pnpm: "pnpm", npm: "npm" };
27
- var LOCK_FILES = {
28
- "bun.lock": PACKAGE_MANAGER.bun,
29
- "bun.lockb": PACKAGE_MANAGER.bun,
30
- "yarn.lock": PACKAGE_MANAGER.yarn,
31
- "pnpm-lock.yaml": PACKAGE_MANAGER.pnpm,
32
- "package-lock.json": PACKAGE_MANAGER.npm
33
- };
34
- var DEFAULT_BASE_BRANCH = "main";
35
- var DERIVED_COMMANDS = {
36
- bun: {
37
- test: "bun test",
38
- typecheck: "bunx -p typescript tsc --noEmit",
39
- lint: "bunx @biomejs/biome check .",
40
- format: "bunx @biomejs/biome format --write .",
41
- install: "bun install",
42
- dev: "bun run dev",
43
- lintFiles: "bunx @biomejs/biome check --no-errors-on-unmatched"
44
- },
45
- yarn: {
46
- test: "yarn test",
47
- typecheck: "yarn tsc --noEmit",
48
- lint: "yarn biome check .",
49
- format: "yarn biome format --write .",
50
- install: "yarn install",
51
- dev: "yarn dev",
52
- lintFiles: "yarn biome check --no-errors-on-unmatched"
53
- },
54
- pnpm: {
55
- test: "pnpm test",
56
- typecheck: "pnpm exec tsc --noEmit",
57
- lint: "pnpm exec biome check .",
58
- format: "pnpm exec biome format --write .",
59
- install: "pnpm install",
60
- dev: "pnpm dev",
61
- lintFiles: "pnpm exec biome check --no-errors-on-unmatched"
62
- },
63
- npm: {
64
- test: "npm test",
65
- typecheck: "npx tsc --noEmit",
66
- lint: "npx @biomejs/biome check .",
67
- format: "npx @biomejs/biome format --write .",
68
- install: "npm install",
69
- dev: "npm run dev",
70
- lintFiles: "npx @biomejs/biome check --no-errors-on-unmatched"
71
- }
72
- };
73
- var FROZEN_INSTALL = {
74
- [DERIVED_COMMANDS.bun.install]: "bun install --frozen-lockfile",
75
- [DERIVED_COMMANDS.yarn.install]: "yarn install --immutable",
76
- [DERIVED_COMMANDS.pnpm.install]: "pnpm install --frozen-lockfile",
77
- [DERIVED_COMMANDS.npm.install]: "npm ci"
78
- };
79
- var BUN_COMMANDS = DERIVED_COMMANDS.bun;
80
-
81
15
  // src/check/command-line.ts
82
16
  var TOKEN = /(?:"[^"]*"|'[^']*'|\S)+/g;
83
17
  var QUOTED = /"([^"]*)"|'([^']*)'/g;
@@ -86,6 +20,9 @@ var tokenize = (command3) => (command3.match(TOKEN) ?? []).map((token) => {
86
20
  return token.replace(QUOTED, "$1$2");
87
21
  });
88
22
 
23
+ // src/check/result.ts
24
+ var CHECK_KIND = { lint: "lint", project: "project", types: "types", tests: "tests" };
25
+
89
26
  // src/check/schema.ts
90
27
  var nonEmpty = z.string().min(1);
91
28
  var isTokenizable = (command3) => {
@@ -99,6 +36,7 @@ var isTokenizable = (command3) => {
99
36
  var command = nonEmpty.refine(isTokenizable, "Unterminated quote in command");
100
37
  var glob = nonEmpty.refine((value) => !/[{}[\]]/.test(value), "Braces and brackets are not supported in globs");
101
38
  var globs = z.array(glob).min(1);
39
+ var directory = nonEmpty.transform((value) => value.replace(/^(\.\/)+/, "").replace(/\/+$/, "") || ".");
102
40
  var DEFAULT_BUILD_ARTIFACT_GLOBS = [
103
41
  "**/tsup.config.bundled_*",
104
42
  "**/dist/**",
@@ -115,7 +53,12 @@ var projectCheck = z.strictObject({
115
53
  name: nonEmpty,
116
54
  command,
117
55
  /** Without globs the check runs on every change; with them, only when a changed path matches. */
118
- globs: globs.optional()
56
+ globs: globs.optional(),
57
+ /**
58
+ * `tests` for a check that runs the project's tests, `types` for its type check; without one, a plain project check.
59
+ * A test run counts towards "no green without a test", and both run with the slow checks.
60
+ */
61
+ kind: z.enum([CHECK_KIND.tests, CHECK_KIND.types]).optional()
119
62
  });
120
63
  var generatorOwned = z.strictObject({
121
64
  glob,
@@ -133,9 +76,9 @@ var skillBudgets = z.record(glob, z.number().int().positive());
133
76
  var checkConfigSchema = z.strictObject({
134
77
  /**
135
78
  * Run once over the changed files, which are appended as arguments. Separate from `commands.lint`,
136
- * which lints the whole project.
79
+ * which lints the whole project. Detected from the repository when left out; without one, no lint runs.
137
80
  */
138
- lintFilesCommand: command.default(BUN_COMMANDS.lintFiles),
81
+ lintFilesCommand: command.optional(),
139
82
  /** Project checks, run alongside lint; add repository-specific scripts here. */
140
83
  checks: z.array(projectCheck).default([]),
141
84
  generatorOwned: z.array(generatorOwned).default([]),
@@ -148,7 +91,16 @@ var checkConfigSchema = z.strictObject({
148
91
  */
149
92
  skillBudgets: skillBudgets.optional(),
150
93
  /** Also run tests in directories that hold test files but belong to no package. */
151
- 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()
152
104
  });
153
105
 
154
106
  // src/claude/paths.ts
@@ -196,6 +148,9 @@ var DAY_MS = 864e5;
196
148
  var MAX_AUTO_WINDOW_DAYS = 7;
197
149
  var windowDays = (since, until) => Math.round((Date.parse(`${until}T00:00:00Z`) - Date.parse(`${since}T00:00:00Z`)) / DAY_MS);
198
150
 
151
+ // src/config/base-branch.ts
152
+ var DEFAULT_BASE_BRANCH = "main";
153
+
199
154
  // src/config/link-kinds.ts
200
155
  var ARTIFACT_LINK_KIND = {
201
156
  pattern: "^https://claude\\.ai/(code/)?artifact/",
@@ -310,6 +265,13 @@ var MODEL_ROLE = {
310
265
  explore: "explore",
311
266
  orchestrate: "orchestrate"
312
267
  };
268
+ var DEFAULT_MODELS = {
269
+ [MODEL_ROLE.plan]: "claude-opus-5-5",
270
+ [MODEL_ROLE.implement]: "claude-sonnet-5-5",
271
+ [MODEL_ROLE.review]: "claude-sonnet-5-5",
272
+ [MODEL_ROLE.explore]: "claude-sonnet-5-5",
273
+ [MODEL_ROLE.orchestrate]: "claude-opus-5-5"
274
+ };
313
275
  var isTerminalAdapter = (value) => Object.values(TERMINAL_ADAPTER).includes(value);
314
276
  var isModelRole = (value) => Object.values(MODEL_ROLE).includes(value);
315
277
  var SKILL_PREFIX = /^[a-z0-9]+-$/;
@@ -318,6 +280,17 @@ var language = z2.enum(Object.values(PLAN_LANGUAGE));
318
280
  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");
319
281
  var command2 = z2.string().min(1);
320
282
  var tokenCount = z2.number().int().positive();
283
+ var commandsSchema = z2.strictObject({
284
+ test: command2.optional(),
285
+ typecheck: command2.optional(),
286
+ lint: command2.optional(),
287
+ format: command2.optional(),
288
+ /** Installs a new lane's dependencies. */
289
+ install: command2.optional(),
290
+ /** Starts a lane's dev server, run as `PORT=<port> <dev>`; a project supplement may name another. */
291
+ dev: command2.optional()
292
+ });
293
+ var COMMAND_KEYS = Object.keys(commandsSchema.shape);
321
294
  var DEFAULT_CONTEXT_WINDOW = 2e5;
322
295
  var laneCopyPath = z2.string().regex(
323
296
  // biome-ignore lint/suspicious/noControlCharactersInRegex: the class exists to refuse control characters
@@ -587,24 +560,15 @@ var jackherdConfigSchema = z2.strictObject({
587
560
  * kit name (`implement`, `plan`, …) apart from the kit's.
588
561
  */
589
562
  skillPrefix: z2.union([z2.literal(""), z2.string().regex(SKILL_PREFIX, "lower-case letters and digits ending in a dash, e.g. kit-")]).default(""),
590
- commands: z2.strictObject({
591
- test: command2.default(BUN_COMMANDS.test),
592
- typecheck: command2.default(BUN_COMMANDS.typecheck),
593
- lint: command2.default(BUN_COMMANDS.lint),
594
- format: command2.default(BUN_COMMANDS.format),
595
- /** Installs a new lane's dependencies. */
596
- install: command2.default(BUN_COMMANDS.install),
597
- /** Starts a lane's dev server, run as `PORT=<port> <dev>`; a project supplement may name another. */
598
- dev: command2.default(BUN_COMMANDS.dev)
599
- }).prefault({}),
563
+ commands: commandsSchema.prefault({}),
600
564
  /** How `/orchestrate` sets up a lane's worktree. */
601
565
  lanes: z2.strictObject({
602
566
  /** Untracked files copied into every new worktree; empty means the files `.worktreeinclude` lists. */
603
567
  copy: z2.array(laneCopyPath).default([]),
604
- /** The dependency lock file a rebase takes from the base branch and regenerates, never merges by hand. */
605
- lockFile: lockFilePath.default("bun.lock")
568
+ /** The dependency lock file a rebase takes from the base branch and regenerates, never merges by hand; detected from the repository when left out. */
569
+ lockFile: lockFilePath.optional()
606
570
  }).prefault({}),
607
- models: z2.record(z2.enum(Object.values(MODEL_ROLE)), modelId),
571
+ models: z2.record(z2.enum(Object.values(MODEL_ROLE)), modelId).default(DEFAULT_MODELS),
608
572
  terminal: z2.strictObject({ adapter: z2.enum(Object.values(TERMINAL_ADAPTER)).default(TERMINAL_ADAPTER.none) }).prefault({}),
609
573
  /** How the coordinator shows a draft for review (`orchestrate` § 4). */
610
574
  preview: z2.strictObject({
@@ -669,7 +633,9 @@ var parseSourceDraft = (raw, label) => parseLabelled(sourceSchema, raw, label);
669
633
  var renderConfigJsonSchema = () => `${JSON.stringify(z3.toJSONSchema(jackherdConfigSchema, { io: "input" }), null, 2)}
670
634
  `;
671
635
  export {
636
+ COMMAND_KEYS,
672
637
  DEFAULT_CONTEXT_WINDOW,
638
+ DEFAULT_MODELS,
673
639
  MODEL_ROLE,
674
640
  SKILL_PREFIX,
675
641
  TERMINAL_ADAPTER,
@@ -1,6 +1,6 @@
1
1
  id = "crossdelta.jackherd"
2
2
  name = "jackherd"
3
- version = "1.12.7"
3
+ version = "1.14.0"
4
4
  min_herdr_version = "0.8.2"
5
5
  description = "Show the herd, the state of a board's lanes, in a herdr pane"
6
6
  platforms = ["linux", "macos"]
package/install.sh CHANGED
@@ -9,7 +9,7 @@ set -eu
9
9
 
10
10
  # The default is the version this file ships with. The release bump changes it together with
11
11
  # package.json; install.test.ts fails while the two differ. Overridable, but only by a plain release.
12
- JACKHERD_VERSION="${JACKHERD_VERSION:-1.12.7}"
12
+ JACKHERD_VERSION="${JACKHERD_VERSION:-1.14.0}"
13
13
 
14
14
  JACKHERD_PACKAGE='jackherd'
15
15
  # Test seam: the terminal the wizard asks its questions on.
@@ -1,42 +1,24 @@
1
1
  import { baseBranchOf } from '../shared/base-branch'
2
- import { BOARD_WORDS, type BoardLanguage } from '../shared/board-lanes'
2
+ import { type BoardLocation, boardLocationValues } from '../shared/boards'
3
3
  import { isRecord } from '../shared/json'
4
- import { isRepositoryDirectory } from '../shared/repository-directory'
5
4
  import { isControlCharacter } from './plain-path'
6
5
 
7
6
  /**
8
7
  * Where plans, boards and lanes live, the base branch, and the checks a standing approval waits for. `null` from
9
8
  * `boardSettingsOf` means the standing approval is off and every merge asks.
10
9
  */
11
- export type BoardSettings = {
12
- planLanguage: BoardLanguage
13
- planDirectory: string
14
- board: { pattern: string; file: string }
15
- worktreeDirectory: string
10
+ export type BoardSettings = BoardLocation & {
16
11
  baseBranch: string
17
12
  /** The checks that must be among the head commit's and green; undefined: at least one check, every one green. */
18
13
  requiredChecks: readonly string[] | undefined
19
14
  }
20
15
 
21
16
  const MAX_CHECK_NAME = 200
22
- const BOARD_PATTERN = /^(?![-.])[A-Za-z0-9._*?-]+$/
23
- const BOARD_FILE = /^(?![-.])[A-Za-z0-9._-]+\.md$/
24
-
25
- /** The value at `key`, its default when absent, `undefined` when present but invalid. */
26
- const valueOr = <T>(section: Record<string, unknown>, key: string, isValid: (value: unknown) => value is T, fallback: T) =>
27
- !(key in section) ? fallback : isValid(section[key]) ? section[key] : undefined
28
17
 
29
18
  /** An optional value: `{ value }` with `value` undefined when absent, `undefined` when present but invalid. */
30
19
  const optionalValue = <T>(section: Record<string, unknown>, key: string, isValid: (value: unknown) => value is T) =>
31
20
  !(key in section) ? { value: undefined } : isValid(section[key]) ? { value: section[key] } : undefined
32
21
 
33
- const matches =
34
- (pattern: RegExp) =>
35
- (value: unknown): value is string =>
36
- typeof value === 'string' && pattern.test(value)
37
-
38
- const isPlanLanguage = (value: unknown): value is BoardLanguage => typeof value === 'string' && Object.hasOwn(BOARD_WORDS, value)
39
-
40
22
  const isSectionOrAbsent = (config: Record<string, unknown>, key: string): boolean =>
41
23
  !(key in config) || isRecord(config[key])
42
24
 
@@ -64,18 +46,11 @@ const requiredChecksOf = (guard: Record<string, unknown>) =>
64
46
  : optionalValue(sectionAt(guard, 'planApproval'), 'requiredChecks', isCheckList)
65
47
 
66
48
  /** Every value the standing approval reads, with its default where absent; `undefined` stands for a present but invalid one. */
67
- const boardValues = (config: Record<string, unknown>) => {
68
- const board = sectionAt(config, 'board')
69
- return {
70
- requiredChecks: requiredChecksOf(sectionAt(config, 'guard')),
71
- planLanguage: valueOr(config, 'planLanguage', isPlanLanguage, 'en'),
72
- planDirectory: valueOr(config, 'planDirectory', isRepositoryDirectory, 'docs/plans'),
73
- pattern: valueOr(board, 'pattern', matches(BOARD_PATTERN), 'board-*'),
74
- file: valueOr(board, 'file', matches(BOARD_FILE), 'board.md'),
75
- worktreeDirectory: valueOr(config, 'worktreeDirectory', isRepositoryDirectory, '.claude/worktrees'),
76
- baseBranch: baseBranchOf(config),
77
- }
78
- }
49
+ const boardValues = (config: Record<string, unknown>) => ({
50
+ requiredChecks: requiredChecksOf(sectionAt(config, 'guard')),
51
+ ...boardLocationValues(config, sectionAt(config, 'board')),
52
+ baseBranch: baseBranchOf(config),
53
+ })
79
54
 
80
55
  type Values = ReturnType<typeof boardValues>
81
56
  type ValidValues = { [Key in keyof Values]: Exclude<Values[Key], undefined> }
@@ -3,15 +3,12 @@
3
3
  * link on the boards, as the main checkout holds them (the coordinator keeps its board there, committed or not). The
4
4
  * git call runs without a shell and with a timeout; any failure answers nothing, so the guard asks.
5
5
  */
6
- import { readdirSync, readFileSync } from 'node:fs'
7
- import { join, posix, relative, resolve } from 'node:path'
6
+ import { relative, resolve } from 'node:path'
8
7
  import { type BoardLanguage, lanePlansOf } from '../shared/board-lanes'
8
+ import { type Board, boardsIn } from '../shared/boards'
9
9
  import { laneOf } from '../shared/lane'
10
10
  import type { BoardSettings } from './board-settings'
11
11
 
12
- /** A board's directory relative to the repository, and its text. */
13
- export type Board = { directory: string; text: string }
14
-
15
12
  const GIT_TIMEOUT_MS = 3000
16
13
 
17
14
  /** The lane whose worktree, directly below the worktree directory, has `branch` checked out. */
@@ -47,30 +44,6 @@ const worktreeListOf = (main: string): string | undefined => {
47
44
  return result.exitCode === 0 ? result.stdout.toString() : undefined
48
45
  }
49
46
 
50
- const readTextOrUndefined = (path: string): string | undefined => {
51
- try {
52
- return readFileSync(path, 'utf8')
53
- } catch {
54
- return undefined
55
- }
56
- }
57
-
58
- /** The boards under the plan directory of the main checkout whose directory name matches the board pattern. */
59
- const boardsIn = (main: string, settings: BoardSettings): Board[] => {
60
- const pattern = new Bun.Glob(settings.board.pattern)
61
- try {
62
- return readdirSync(join(main, settings.planDirectory), { withFileTypes: true })
63
- .filter((entry) => entry.isDirectory() && pattern.match(entry.name))
64
- .flatMap((entry) => {
65
- const directory = posix.join(settings.planDirectory, entry.name)
66
- const text = readTextOrUndefined(join(main, directory, settings.board.file))
67
- return text === undefined ? [] : [{ directory, text }]
68
- })
69
- } catch {
70
- return []
71
- }
72
- }
73
-
74
47
  /** The one plan the lane of `branch` links on the boards of the main checkout; undefined when it cannot tell. */
75
48
  export const planOfBranch = (main: string, settings: BoardSettings, branch: string): string | undefined => {
76
49
  const list = worktreeListOf(main)