jackherd 1.17.0 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +25 -30
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/merge-guard/approval.ts +1 -1
- package/kit/hooks/hooks/merge-guard/lane-plan.ts +2 -3
- package/kit/hooks/hooks/plain-question/arguments.ts +8 -0
- package/kit/hooks/hooks/plain-question/index.ts +17 -5
- package/kit/hooks/hooks/plain-question/self-merge-files.ts +35 -0
- package/kit/hooks/hooks/plain-question/staged-files.ts +24 -0
- package/kit/hooks/hooks/shared/worktree-list.ts +8 -0
- package/kit/hooks/hooks/watch-guard/decision.ts +6 -4
- package/kit/hooks/hooks/watch-guard/index.ts +2 -0
- package/kit/review/skills/pre-commit/SKILL.md +16 -22
- package/kit/rules/rules/kit/code-style.md +22 -50
- package/kit/rules/rules/kit/pull-requests.md +17 -40
- package/kit/rules/rules/kit/testing.md +6 -12
- package/kit/workflow/skills/implement/SKILL.md +27 -42
- package/kit/workflow/skills/orchestrate/SKILL.md +158 -235
- package/kit/workflow/skills/orchestrate/references/approvals.md +23 -0
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +2 -1
- package/kit/workflow/skills/plan/SKILL.md +27 -47
- package/package.json +1 -1
- package/src/check/skill-budget-defaults.ts +20 -13
- package/src/check/skill-budgets.ts +42 -12
- package/src/watch/command.ts +7 -3
- package/src/watch/feedback.ts +30 -12
- package/src/watch/lane-report.ts +4 -4
- package/src/watch/preview-read.ts +3 -3
- package/src/watch/previews.ts +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
What changed for people who use jackherd, newest first.
|
|
4
4
|
|
|
5
|
+
## 1.18.1
|
|
6
|
+
|
|
7
|
+
- **Starting the watcher under Monitor works again.** The watch guard refused `jh watch --stream` under the Monitor tool, the very place its message points to, so the coordinator fell back to single background runs. The stream refusal now applies only to a Bash call with `run_in_background`, where a never-ending stream would stay silent; Monitor and a foreground call pass.
|
|
8
|
+
- **No self-merge option where it cannot take effect.** A pull request that changes a guard file (`.claude/hooks/**`, `.jackherd/lock.json` and the like) is never covered by a self-merge approval, yet the coordinator still offered the option and you had to confirm the merge anyway. The question is now sent back when it offers the self-merge label for such a pull request; when the files cannot be determined, it passes and the merge guard still asks at merge time.
|
|
9
|
+
- **Review feedback on the coordinator's own pull requests reaches Jack.** The watcher read reviews and comments only for pull requests of lane branches, so an inline finding on a pull request without a lane reached nobody. It now reads feedback for your open pull requests without a lane too, the same set it already read checks for; the event names them `(branch <name>)`. Previews stay lane-only.
|
|
10
|
+
|
|
11
|
+
## 1.18.0
|
|
12
|
+
|
|
13
|
+
- **`jh check` measures every file your budget names.** A path in `check.skillBudgets` (no glob, no sum) is now measured even when `jh check` would not read it on its own, such as `CLAUDE.md` or a rule with `paths:`; a named file that is missing warns, it never fails. The number of measured files in the `Skill budgets` line can rise after the update.
|
|
14
|
+
- **Shorter kit texts.** The code style, testing and pull request rules, `/jh-implement`, `/jh-plan` and `/jh-pre-commit` are cut to the rules an agent must keep, about 1,500 words fewer in total; the orchestrate skill loses about 1,200 words, what a reference, a hook message or a watcher line already says included, and its approval questions moved into `references/approvals.md`, which the `approval` row links. Word budgets and ceilings fall with them. Run `jh sync` to get the new texts.
|
|
15
|
+
- **A clearer README.** The intro, the follow section and the Quickstart say what you decide, what Jack does and what a source needs.
|
|
16
|
+
|
|
5
17
|
## 1.17.0
|
|
6
18
|
|
|
7
19
|
- **Jack resumes after a crash.** After a machine or herdr crash, herdr restored the agents but nobody restarted the watcher, and every lane whose command was cut off stood still. The watcher now keeps a record in `~/.jackherd/resume/`, and a second `[[startup]]` entry of the herdr plugin runs `jh resume`: it wakes the coordinator with a fixed `/orchestrate` line and every lane that was working with a fixed resume text, at most once per crash. Nothing resumes after a clean stop, for a record older than 12 hours, while a watcher runs, or when a target is ambiguous; `~/.jackherd/logs/resume.log` says why.
|
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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.
|
|
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.18.1/assets/branding/jackherd-logo-outline.svg" width="340">
|
|
3
3
|
<br>
|
|
4
4
|
</p>
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd?activeTab=dependencies) [](https://unpkg.com/jackherd/INSTALL.md) [](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.
|
|
6
|
+
[](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd?activeTab=dependencies) [](https://unpkg.com/jackherd/INSTALL.md) [](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.18.1/assets/icons/space.svg">
|
|
7
7
|
|
|
8
8
|
# Jack herds your coding agents.
|
|
9
9
|
|
|
10
|
-
You
|
|
10
|
+
You decide what gets built. Jack, the coordinator, plans it with you, runs it in parallel lanes, answers what the plan already settles and asks you only what needs you. The herd view shows what's running, what's blocked and what waits for you.
|
|
11
11
|
|
|
12
12
|
For developers who'd rather decide than supervise.
|
|
13
13
|
|
|
@@ -15,27 +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.
|
|
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.18.1/assets/icons/space.svg">
|
|
19
19
|
|
|
20
20
|
<p>
|
|
21
|
-
<img src="https://unpkg.com/jackherd@1.
|
|
21
|
+
<img src="https://unpkg.com/jackherd@1.18.1/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.
|
|
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.18.1/assets/icons/space.svg">
|
|
24
24
|
</p>
|
|
25
25
|
|
|
26
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
26
|
+
<p><img src="https://unpkg.com/jackherd@1.18.1/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.
|
|
28
|
+
<p><img src="https://unpkg.com/jackherd@1.18.1/assets/icons/decide.svg" width="24" height="34" align="middle" alt=""> <b>You make the calls.</b><br>Commits, pushes and merges wait for your approval. Jack merges on green checks only where you said so: one pull request, or a plan's for up to 24 hours. The guards catch mistakes, not intent; a hard boundary takes branch protection.</p>
|
|
29
29
|
|
|
30
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
30
|
+
<p><img src="https://unpkg.com/jackherd@1.18.1/assets/icons/bark.svg" width="24" height="34" align="middle" alt=""> <b>Jack barks.</b><br>A lane gets stuck, a check goes red, a decision needs you: Jack steps in and brings you only what's yours to decide. The herd view shows where every lane stands.</p>
|
|
31
31
|
|
|
32
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
32
|
+
<p><img src="https://unpkg.com/jackherd@1.18.1/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.
|
|
34
|
+
<p><img src="https://unpkg.com/jackherd@1.18.1/assets/icons/ear.svg" width="24" height="34" align="middle" alt=""> <b>Jack listens.</b><br>Review comments on a lane's pull request go back to that lane, which fixes them with a test. <code>/jh-follow</code> brings in anything a CLI or an API can read as JSON: tickets, error logs, status pages.</p>
|
|
35
35
|
|
|
36
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
36
|
+
<p><img src="https://unpkg.com/jackherd@1.18.1/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
37
|
|
|
38
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
38
|
+
<p><img src="https://unpkg.com/jackherd@1.18.1/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.18.1/assets/icons/space.svg"></p>
|
|
39
39
|
|
|
40
40
|
## Quickstart
|
|
41
41
|
|
|
@@ -50,43 +50,38 @@ Then `/jh-start` in a repository, and Jack offers your first plan. → [Quicksta
|
|
|
50
50
|
```text
|
|
51
51
|
Install jackherd by following https://unpkg.com/jackherd/INSTALL.md. Ask me the setup questions it lists; herdr is recommended, not required.
|
|
52
52
|
```
|
|
53
|
-
2. **Start Claude Code in a repository.**
|
|
53
|
+
2. **Start Claude Code in a repository.** `/jh-start` checks your setup and offers your first plan, written to `docs/plans/`.
|
|
54
54
|
|
|
55
55
|
```text
|
|
56
56
|
/jh-start
|
|
57
57
|
```
|
|
58
|
-
3. **
|
|
59
|
-
|
|
60
|
-
```text
|
|
61
|
-
/jh-plan Add product search
|
|
62
|
-
```
|
|
63
|
-
4. **Build the first phase.**
|
|
58
|
+
3. **Build the first phase,** in one session, no herdr needed.
|
|
64
59
|
|
|
65
60
|
```text
|
|
66
61
|
/jh-implement docs/plans/<plan>.md phase 1
|
|
67
62
|
```
|
|
68
|
-
|
|
63
|
+
4. **Run phases in parallel with herdr and `gh` (recommended).** Lanes, the full herd view, review-comment fixes and ticket reports need them. In Claude Code inside herdr, hand Jack the board:
|
|
69
64
|
|
|
70
65
|
```text
|
|
71
66
|
/jh-orchestrate docs/plans/board-<name>/board.md
|
|
72
67
|
```
|
|
73
68
|
|
|
74
|
-
Linux and macOS are supported (Windows through WSL); the requirements are in [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md). 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
|
|
69
|
+
Linux and macOS are supported (Windows through WSL); the requirements are in [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md). 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.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.18.1/assets/icons/space.svg">
|
|
75
70
|
|
|
76
|
-
## Follow anything
|
|
71
|
+
## Follow anything you can query
|
|
77
72
|
|
|
78
|
-
|
|
73
|
+
Tickets, error logs, status pages: if a command or an API call can read it as JSON with your login, Jack can watch it. Recipes cover Jira, Linear, GitHub issues and Projects, Sentry, Google Calendar and status pages; for anything else, `/jh-follow` writes the command with you. One yes, and every change shows up like this:
|
|
79
74
|
|
|
80
|
-
<img src="https://unpkg.com/jackherd@1.
|
|
75
|
+
<img src="https://unpkg.com/jackherd@1.18.1/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.">
|
|
81
76
|
|
|
82
|
-
The herd sums up your sources in one `Sources` line: per source
|
|
77
|
+
The herd sums up your sources in one `Sources` line: new entries per source, or its last run, red while it fails; opened, a click opens an entry. One that needs you lands in Your turn under the source's label, [ask] has Jack look at it, and a ticket shows on the lane that works it.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.18.1/assets/icons/space.svg">
|
|
83
78
|
|
|
84
79
|
## In detail
|
|
85
80
|
|
|
86
81
|
<details>
|
|
87
82
|
<summary><b>What Jack does on his own, and what waits for you</b></summary>
|
|
88
83
|
|
|
89
|
-
<img src="https://unpkg.com/jackherd@1.
|
|
84
|
+
<img src="https://unpkg.com/jackherd@1.18.1/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.">
|
|
90
85
|
|
|
91
86
|
| On his own | After your approval | Never |
|
|
92
87
|
| --- | --- | --- |
|
|
@@ -121,7 +116,7 @@ The guards catch mistakes, not intent; for that, use branch protection. The hook
|
|
|
121
116
|
|
|
122
117
|
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.
|
|
123
118
|
|
|
124
|
-
<img src="https://unpkg.com/jackherd@1.
|
|
119
|
+
<img src="https://unpkg.com/jackherd@1.18.1/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.">
|
|
125
120
|
|
|
126
121
|
| Skill | Does |
|
|
127
122
|
| --- | --- |
|
|
@@ -131,7 +126,7 @@ A **plan** is Markdown with phases, checkbox tasks, a model and a review gate pe
|
|
|
131
126
|
| `/jh-implement` | builds and tests one phase and ticks it off; independent phases run as parallel subagents |
|
|
132
127
|
| `/jh-orchestrate` | runs a board's lanes in herdr, as Jack |
|
|
133
128
|
| `/jh-pre-commit` | runs `jh check` and the reviewers, proposes the commit message and asks before committing |
|
|
134
|
-
| `/jh-follow` | follows
|
|
129
|
+
| `/jh-follow` | follows a source: a ticket, error logs, a status page |
|
|
135
130
|
| `/jh-diagnose` | turns a bug into a failing command, fixes it with a regression test |
|
|
136
131
|
|
|
137
132
|
- **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.
|
|
@@ -250,7 +245,7 @@ Without a global install, run them as `bunx jackherd <command>`. The config is `
|
|
|
250
245
|
}
|
|
251
246
|
```
|
|
252
247
|
|
|
253
|
-
`jh check` counts the words of every skill, agent and always-loaded rule an agent reads, and warns above its budget; `check.skillBudgets` sets your own, by path or glob. It also reads the rule blocks you added or changed in `**/rules/**/*.md`, `CLAUDE.md` and `AGENTS.md`, and warns (`Lint pointers`) where a block carries a rule your linter could check (a banned import path, a literal comparison, a comment form, a banned call) and names no lint rule. Name it with `` `lint:<rule id>` ``, or write `` `lint:none` `` and a reason where the rule stays prose. `check.lintPointers.level` is `warn` (default), `fail` or `off`; `check.lintPointers.exclude` lists globs that are never read. `watch.parallelTasks.auto` (`since`, `until`, at most seven days) lets Jack start up to two extra subagents in a running lane without asking, under the cost limit and away from paths that always ask; he switches it on only after asking you.
|
|
248
|
+
`jh check` counts the words of every skill, agent and always-loaded rule an agent reads, and warns above its budget; `check.skillBudgets` sets your own, by path or glob. A file it names by its exact path is measured too, even one `jh check` would not read on its own (`CLAUDE.md`, a rule with `paths:`); a named file that is missing warns. It also reads the rule blocks you added or changed in `**/rules/**/*.md`, `CLAUDE.md` and `AGENTS.md`, and warns (`Lint pointers`) where a block carries a rule your linter could check (a banned import path, a literal comparison, a comment form, a banned call) and names no lint rule. Name it with `` `lint:<rule id>` ``, or write `` `lint:none` `` and a reason where the rule stays prose. `check.lintPointers.level` is `warn` (default), `fail` or `off`; `check.lintPointers.exclude` lists globs that are never read. `watch.parallelTasks.auto` (`since`, `until`, at most seven days) lets Jack start up to two extra subagents in a running lane without asking, under the cost limit and away from paths that always ask; he switches it on only after asking you.
|
|
254
249
|
|
|
255
250
|
</details>
|
|
256
251
|
|
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
|
+
JACKHERD_VERSION="${JACKHERD_VERSION:-1.18.1}"
|
|
13
13
|
|
|
14
14
|
JACKHERD_PACKAGE='jackherd'
|
|
15
15
|
# Test seam: the terminal the wizard asks its questions on.
|
|
@@ -29,7 +29,7 @@ const onlyOf = (values: string[]): string | undefined => {
|
|
|
29
29
|
* The one branch a question names as ``Branch `name` ``, with the one change it names after it; a question that names
|
|
30
30
|
* none or several of either approves none.
|
|
31
31
|
*/
|
|
32
|
-
const namedChange = (question: string): ApprovedChange | undefined => {
|
|
32
|
+
export const namedChange = (question: string): ApprovedChange | undefined => {
|
|
33
33
|
const branch = onlyOf([...question.matchAll(BRANCH_MENTION)].map(([, name]) => name))
|
|
34
34
|
const change = onlyOf([...question.matchAll(CHANGE_MENTION)].map(([, id]) => id))
|
|
35
35
|
return branch !== undefined && change !== undefined && BRANCH_NAME.test(branch) ? { branch, change } : undefined
|
|
@@ -7,6 +7,7 @@ import { relative, resolve } from 'node:path'
|
|
|
7
7
|
import { type BoardLanguage, lanePlansOf } from '../shared/board-lanes'
|
|
8
8
|
import { type Board, boardsIn } from '../shared/boards'
|
|
9
9
|
import { laneOf } from '../shared/lane'
|
|
10
|
+
import { worktreePathOf } from '../shared/worktree-list'
|
|
10
11
|
import type { BoardSettings } from './board-settings'
|
|
11
12
|
|
|
12
13
|
const GIT_TIMEOUT_MS = 3000
|
|
@@ -18,9 +19,7 @@ export const laneInWorktreeList = (
|
|
|
18
19
|
worktreeDirectory: string,
|
|
19
20
|
branch: string,
|
|
20
21
|
): string | undefined => {
|
|
21
|
-
const
|
|
22
|
-
const block = blocks.find((lines) => lines.includes(`branch refs/heads/${branch}`))
|
|
23
|
-
const path = block?.find((line) => line.startsWith('worktree '))?.slice('worktree '.length)
|
|
22
|
+
const path = worktreePathOf(worktreeList, branch)
|
|
24
23
|
if (path === undefined) return undefined
|
|
25
24
|
const lane = laneOf({ cwd: path, main, worktreeDirectory })
|
|
26
25
|
return lane !== null && relative(resolve(main, worktreeDirectory), resolve(path)) === lane ? lane : undefined
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The positions of the values `sync` renders into the hook entry's command (`kit/hooks/hooks.json`), after the script
|
|
3
|
+
* path. `arguments.test.ts` checks the entry passes them in this order.
|
|
4
|
+
*/
|
|
5
|
+
export const ARGUMENT = { recommended: 2, approvalAllow: 3, approvalDeny: 4, selfMergeLabel: 5, standingApprove: 6 } as const
|
|
6
|
+
|
|
7
|
+
/** The first position of a label a guard reads word for word. */
|
|
8
|
+
export const FIRST_GUARDED_LABEL = ARGUMENT.approvalAllow
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// pattern; a cache keyed by each file's time and size spares reading the unchanged ones. A dialog whose question
|
|
6
6
|
// lacks its recommendation as the first option, marked with argv[2], is denied as well, and so is a coordinator's
|
|
7
7
|
// question that breaks the form of `form.ts` (🐾 header, short question and labels); argv[3…] are the labels a
|
|
8
|
-
// guard reads word for word, which carry no mark.
|
|
8
|
+
// guard reads word for word, which carry no mark. A PR question that offers the self-merge label (argv[5]) for a branch
|
|
9
|
+
// whose staged change carries a guard file is denied too, since the merge guard never lets that approval cover it
|
|
10
|
+
// (`self-merge-files.ts`; the positions are named in `arguments.ts`). Anything else, and any failure, answers nothing, so the dialog opens.
|
|
9
11
|
import { existsSync, statSync } from 'node:fs'
|
|
10
12
|
import { join } from 'node:path'
|
|
11
13
|
import { boardLocationOf, boardsIn } from '../shared/boards'
|
|
@@ -17,13 +19,19 @@ import { isCoordinatorAgent } from '../shared/lane'
|
|
|
17
19
|
import { ownPaneAgent } from '../shared/own-pane'
|
|
18
20
|
import { planCodesOf } from '../shared/plan-codes'
|
|
19
21
|
import { ensurePrivateDirectory, writePrivateFile } from '../shared/private-directory'
|
|
22
|
+
import { ARGUMENT, FIRST_GUARDED_LABEL } from './arguments'
|
|
20
23
|
import { type CodeCache, lookUpCodes, type PlanFileStat, parseCodeCache, serializeCodeCache } from './code-cache'
|
|
21
24
|
import { formReasonOf } from './form'
|
|
22
25
|
import { type RecommendationWords, recommendationReasonOf } from './recommendation'
|
|
23
26
|
import { denyReasonOf, dialogTextsOf, planPathsOfBoards, planRootsOfBoards } from './run'
|
|
27
|
+
import { selfMergeGuardFileReason } from './self-merge-files'
|
|
28
|
+
import { stagedFilesOfBranch } from './staged-files'
|
|
24
29
|
|
|
25
30
|
const HOOK_EVENT = 'PreToolUse'
|
|
26
31
|
|
|
32
|
+
/** argv[5] is the self-merge label, in the order the hook entry passes its arguments; an older install passes none. */
|
|
33
|
+
const selfMergeLabelOf = (argv: readonly string[]): string => argv[ARGUMENT.selfMergeLabel] ?? ''
|
|
34
|
+
|
|
27
35
|
/** Where the cache lies: in the main checkout beside `.jackherd/approvals`, ignored by git. */
|
|
28
36
|
const cacheDirectory = (main: string): string => join(main, '.jackherd', 'plan-codes')
|
|
29
37
|
const CACHE_FILE = 'codes.json'
|
|
@@ -35,8 +43,8 @@ const statOf = (path: string): PlanFileStat | null => {
|
|
|
35
43
|
|
|
36
44
|
/** argv[2] is the recommended mark, argv[3…] the labels a guard reads word for word; an older install passes none. */
|
|
37
45
|
const recommendationWordsOf = (argv: readonly string[]): RecommendationWords => ({
|
|
38
|
-
mark: argv[
|
|
39
|
-
guardedLabels: argv.slice(
|
|
46
|
+
mark: argv[ARGUMENT.recommended] ?? '',
|
|
47
|
+
guardedLabels: argv.slice(FIRST_GUARDED_LABEL),
|
|
40
48
|
})
|
|
41
49
|
|
|
42
50
|
/** Keeps the cache for the next dialog; a cache that cannot be written only costs that dialog a full read. */
|
|
@@ -96,13 +104,17 @@ const decide = async (): Promise<string | null> => {
|
|
|
96
104
|
try {
|
|
97
105
|
const input: unknown = JSON.parse(await Bun.stdin.text())
|
|
98
106
|
if (!isRecord(input) || typeof input.cwd !== 'string') return null
|
|
107
|
+
const { cwd } = input
|
|
99
108
|
const texts = dialogTextsOf(input.tool_input)
|
|
100
109
|
if (texts.length === 0) return null
|
|
101
110
|
const words = recommendationWordsOf(process.argv)
|
|
102
111
|
const reasons = [
|
|
103
|
-
denyReasonOf(texts, planCodesFor(
|
|
112
|
+
denyReasonOf(texts, planCodesFor(cwd)),
|
|
104
113
|
recommendationReasonOf(input.tool_input, words),
|
|
105
|
-
formReasonFor(
|
|
114
|
+
formReasonFor(cwd, input.session_id, input.tool_input, words.guardedLabels),
|
|
115
|
+
selfMergeGuardFileReason(input.tool_input, selfMergeLabelOf(process.argv), (branch) =>
|
|
116
|
+
stagedFilesOfBranch(cwd, branch),
|
|
117
|
+
),
|
|
106
118
|
].filter((reason): reason is string => reason !== null)
|
|
107
119
|
return reasons.length === 0 ? null : reasons.join(' ')
|
|
108
120
|
} catch {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { alwaysAskingFiles, STANDING_ALWAYS_ASK } from '../merge-guard/always-ask'
|
|
2
|
+
import { namedChange } from '../merge-guard/approval'
|
|
3
|
+
import { questionsOf, recordsOf } from './run'
|
|
4
|
+
|
|
5
|
+
/** The staged files of a branch's change; undefined when they cannot be determined. */
|
|
6
|
+
export type StagedFilesOf = (branch: string) => readonly string[] | undefined
|
|
7
|
+
|
|
8
|
+
const offersLabel = (question: Record<string, unknown>, label: string): boolean =>
|
|
9
|
+
recordsOf(question.options).some((option) => option.label === label)
|
|
10
|
+
|
|
11
|
+
const refusal = (files: readonly string[]): string =>
|
|
12
|
+
`The change carries a guard file (${files.map((file) => `\`${file}\``).join(', ')}), which a self-merge approval never covers, so the user confirms this merge in the merge dialog. Ask with the plain approval and the change option only.`
|
|
13
|
+
|
|
14
|
+
const branchOffering = (question: Record<string, unknown>, label: string): string | undefined =>
|
|
15
|
+
typeof question.question === 'string' && offersLabel(question, label) ? namedChange(question.question)?.branch : undefined
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The reason to send a PR question back: it offers the self-merge label for a branch whose staged change carries a
|
|
19
|
+
* guard file, the same files (`STANDING_ALWAYS_ASK`) the merge guard refuses a self-merge for. Null for a question
|
|
20
|
+
* without the label or without a branch mark, and when the files cannot be determined: the merge guard still asks at
|
|
21
|
+
* merge time, so a doubt here only costs the user a merge dialog.
|
|
22
|
+
*/
|
|
23
|
+
export const selfMergeGuardFileReason = (
|
|
24
|
+
toolInput: unknown,
|
|
25
|
+
selfMergeLabel: string,
|
|
26
|
+
stagedFilesOf: StagedFilesOf,
|
|
27
|
+
): string | null => {
|
|
28
|
+
if (selfMergeLabel === '') return null
|
|
29
|
+
for (const question of questionsOf(toolInput)) {
|
|
30
|
+
const branch = branchOffering(question, selfMergeLabel)
|
|
31
|
+
const guarded = branch === undefined ? [] : alwaysAskingFiles(stagedFilesOf(branch) ?? [], STANDING_ALWAYS_ASK)
|
|
32
|
+
if (guarded.length > 0) return refusal(guarded)
|
|
33
|
+
}
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { baseBranchOf } from '../shared/base-branch'
|
|
2
|
+
import { mainCheckoutOf } from '../shared/checkout'
|
|
3
|
+
import { commandOutput } from '../shared/command'
|
|
4
|
+
import { readConfig } from '../shared/config'
|
|
5
|
+
import { worktreePathOf } from '../shared/worktree-list'
|
|
6
|
+
|
|
7
|
+
const gitOutput = (cwd: string, ...args: string[]): string => commandOutput(['git', ...args], cwd)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The files staged in the worktree of `branch` against the merge base with the base branch, the base the branch mark's
|
|
11
|
+
* change id is taken from; undefined when the branch has no worktree or git fails.
|
|
12
|
+
*/
|
|
13
|
+
export const stagedFilesOfBranch = (cwd: string, branch: string): readonly string[] | undefined => {
|
|
14
|
+
try {
|
|
15
|
+
const main = mainCheckoutOf(cwd)
|
|
16
|
+
const base = baseBranchOf(readConfig([main]))
|
|
17
|
+
const worktree = worktreePathOf(gitOutput(main, 'worktree', 'list', '--porcelain'), branch)
|
|
18
|
+
if (base === undefined || worktree === undefined) return undefined
|
|
19
|
+
const mergeBase = gitOutput(worktree, 'merge-base', `origin/${base}`, 'HEAD')
|
|
20
|
+
return gitOutput(worktree, 'diff', '--cached', '--name-only', mergeBase).split('\n').filter(Boolean)
|
|
21
|
+
} catch {
|
|
22
|
+
return undefined
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The path of the worktree that has `branch` checked out, from `git worktree list --porcelain`; undefined for none. */
|
|
2
|
+
export const worktreePathOf = (porcelain: string, branch: string): string | undefined => {
|
|
3
|
+
const block = porcelain
|
|
4
|
+
.split('\n\n')
|
|
5
|
+
.map((entry) => entry.split('\n'))
|
|
6
|
+
.find((lines) => lines.includes(`branch refs/heads/${branch}`))
|
|
7
|
+
return block?.find((line) => line.startsWith('worktree '))?.slice('worktree '.length)
|
|
8
|
+
}
|
|
@@ -6,7 +6,7 @@ export type BashToolInput = { command?: string; run_in_background?: boolean }
|
|
|
6
6
|
const LINE_CONTINUATION = /\\\n\s*/g
|
|
7
7
|
|
|
8
8
|
const STREAM_REFUSAL = [
|
|
9
|
-
'
|
|
9
|
+
'A background Bash call never reports a jh watch --stream watcher: the stream never ends.',
|
|
10
10
|
'Start the same command with the Monitor tool (timeout_ms 1800000), where every line it prints reaches you.',
|
|
11
11
|
].join('\n')
|
|
12
12
|
|
|
@@ -40,14 +40,16 @@ export const startsOf = (command: string, parse: typeof startsIn = startsIn): Wa
|
|
|
40
40
|
* the package, or `bun` with the CLI's script) with the subcommand `watch`, also behind wrappers, inside
|
|
41
41
|
* `bash -c`, `eval`, a substitution, or a here-document or here-string fed to a shell. Text that only
|
|
42
42
|
* names the command (an argument, a quoted string, a data here-document) is no start. A `--stream` start
|
|
43
|
-
* never finishes, so
|
|
44
|
-
*
|
|
43
|
+
* never finishes, so a background call (`run_in_background: true`) never reports it and is refused; without
|
|
44
|
+
* that key it passes, as Monitor sends it (Monitor calls reach this hook as Bash calls without the key) or
|
|
45
|
+
* as a foreground call that visibly runs into its timeout. A single run reports only as its own background
|
|
46
|
+
* task, in a call that sends nothing off with `&`.
|
|
45
47
|
*/
|
|
46
48
|
export const watcherStartViolation = (input: BashToolInput): string | null => {
|
|
47
49
|
if (!input.command) return null
|
|
48
50
|
const starts = startsOf(input.command)
|
|
49
51
|
if (starts.length === 0) return null
|
|
50
|
-
if (starts.some((start) => start.stream)) return STREAM_REFUSAL
|
|
52
|
+
if (starts.some((start) => start.stream)) return input.run_in_background === true ? STREAM_REFUSAL : null
|
|
51
53
|
if (input.run_in_background !== true) return BACKGROUND_REFUSAL
|
|
52
54
|
return starts.some((start) => start.callHasBackgroundJob) ? BACKGROUND_JOB_REFUSAL : null
|
|
53
55
|
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { watcherStartViolation } from './decision'
|
|
5
5
|
|
|
6
6
|
const payload = await Bun.stdin.json()
|
|
7
|
+
// Claude Code passes Monitor calls to Bash-matcher hooks as tool_name "Bash" without run_in_background,
|
|
8
|
+
// which is how the decision tells them apart.
|
|
7
9
|
const violation = watcherStartViolation(payload.tool_input ?? {})
|
|
8
10
|
if (violation) {
|
|
9
11
|
console.error(violation)
|
|
@@ -13,42 +13,38 @@ You are the **Pre-Commit Orchestrator**. You measure what a command can measure,
|
|
|
13
13
|
bunx jackherd check
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Its output is **the evidence for steps 1, 3 and 4**: the changed files (untracked included), the affected package roots, the reviewers the changed paths call for, the files a generator owns, staged build artifacts, lint, types and tests per package root, and the project's own checks (the `check` block of `.jackherd/config.json`).
|
|
17
17
|
|
|
18
|
-
**Quote its results; restate nothing from your own reading of the diff.**
|
|
18
|
+
**Quote its results; restate nothing from your own reading of the diff.** An unmeasured verdict is a guess, and its failure mode is a confident false PASS. Where the command says `❌`, the summary says FIX FIRST.
|
|
19
19
|
|
|
20
20
|
`--skip-slow` drops types and tests. Use it only when the user asks for a quick pass, and say so in the summary.
|
|
21
21
|
|
|
22
22
|
## Step 2: Judgement Review
|
|
23
23
|
|
|
24
|
-
Run `/review` for what the command cannot decide: dead exports, naming, function size, the design review.
|
|
24
|
+
Run `/review` for what the command cannot decide: dead exports, naming, function size, the design review.
|
|
25
25
|
|
|
26
26
|
## Step 3: Specialized Reviewers
|
|
27
27
|
|
|
28
|
-
**Step 1 printed the reviewer list** (`Reviewers:
|
|
28
|
+
**Step 1 printed the reviewer list** (`Reviewers: …`) — dispatch exactly those, each as a subagent via the `Agent` tool with its `subagent_type`:
|
|
29
29
|
|
|
30
|
-
- Pass
|
|
30
|
+
- Pass the `git diff` of the relevant files and their paths; to `skill-review`, also the `Skill budgets` and `Lint pointers` results of the latest `jh check` run.
|
|
31
31
|
- Start all of them **in parallel**, in a single message, and wait for every result before step 4.
|
|
32
32
|
|
|
33
|
-
`security-audit`
|
|
33
|
+
Add `security-audit` when the diff *does* something security-relevant in a path no route lists: auth middleware, API key handling, a schema at an HTTP endpoint, a raw database query, an access policy.
|
|
34
34
|
|
|
35
|
-
**`spec-review` starts in the same message.** It
|
|
35
|
+
**`spec-review` starts in the same message.** It applies whichever paths changed. Pass it the full `git diff`, the commit list (`git log <base>..HEAD --oneline`, base `{{kit.baseBranch}}`), the base ref and the branch name.
|
|
36
36
|
|
|
37
37
|
## Step 4: Generated Files
|
|
38
38
|
|
|
39
|
-
Step 1 lists the changed files a generator owns
|
|
39
|
+
Step 1 lists the changed files a generator owns. Ask when one looks hand-edited rather than regenerated.
|
|
40
40
|
|
|
41
41
|
## Step 5: Spec Alignment
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Steps 2 and 3 judge whether the code is written well (**Standards**), `spec-review` whether it does what was asked (**Spec**). Report the two side by side, each under its own heading.
|
|
44
44
|
|
|
45
|
-
## Step 6: arc42
|
|
45
|
+
## Step 6: arc42 and Architecture
|
|
46
46
|
|
|
47
|
-
Apply the filter in `.claude/rules/kit/arc42.md` to the changed files. On a hit, **ask the user concretely**. No hit for bug fixes, styling
|
|
48
|
-
|
|
49
|
-
## Step 7: Architecture Check
|
|
50
|
-
|
|
51
|
-
For changes in a service, a domain package or a data layer, apply the filter in `.claude/rules/kit/architecture-files.md`. On a hit, **ask the user concretely** whether the unit's `ARCHITECTURE.md` needs an update.
|
|
47
|
+
Apply the filter in `.claude/rules/kit/arc42.md` to the changed files; for a service, a domain package or a data layer, also the one in `.claude/rules/kit/architecture-files.md`. On a hit, **ask the user concretely**. No hit for bug fixes, styling or refactorings without decision character.
|
|
52
48
|
|
|
53
49
|
## Output Format
|
|
54
50
|
|
|
@@ -75,19 +71,17 @@ Active reviewers: Review + Spec + [routed reviewers]
|
|
|
75
71
|
### Verdict: COMMIT / FIX FIRST
|
|
76
72
|
```
|
|
77
73
|
|
|
78
|
-
On FIX FIRST, apply what a tool fixes on its own
|
|
79
|
-
|
|
80
|
-
A second review round after fixes dispatches only the reviewers whose findings were applied, plus the reviewers of any path the fixes newly touched (the reviewer list of the new `check` run); `jh check` itself always runs in full.
|
|
74
|
+
On FIX FIRST, apply what a tool fixes on its own (`{{kit.command.format}}`, lint auto-fixes), then re-run `bunx jackherd check` and quote the new result. A second review round dispatches only the reviewers whose findings were applied, plus those of any path the fixes newly touched; `jh check` always runs in full.
|
|
81
75
|
|
|
82
|
-
Architecture problems, missing tests, domain logic errors and **every Spec finding** are reported for the user to decide:
|
|
76
|
+
Architecture problems, missing tests, domain logic errors and **every Spec finding** are reported for the user to decide: a dropped requirement or a grown scope is the user's call.
|
|
83
77
|
|
|
84
78
|
## Commit
|
|
85
79
|
|
|
86
80
|
On COMMIT, propose a Conventional Commit message (`type(scope): description`, imperative, subject at most 72 characters; the body explains cause and consequence in prose).
|
|
87
81
|
|
|
88
|
-
Ask the commit question with the `AskUserQuestion` tool,
|
|
82
|
+
Ask the commit question with the `AskUserQuestion` tool, in {{kit.responseLanguage}}: commit with this message / adjust the message / cancel.
|
|
89
83
|
|
|
90
|
-
On confirmation, write the message to a file in your scratchpad with the `Write` tool, stage **the files step 1 listed** by name, and commit from the file:
|
|
84
|
+
On confirmation, write the message to a file in your scratchpad with the `Write` tool, stage **the files step 1 listed** by name, and commit from the file, each as one Bash call:
|
|
91
85
|
|
|
92
86
|
```bash
|
|
93
87
|
git add -- "<path from step 1>" "<path from step 1>"
|
|
@@ -97,4 +91,4 @@ git add -- "<path from step 1>" "<path from step 1>"
|
|
|
97
91
|
git commit -F "<message file>"
|
|
98
92
|
```
|
|
99
93
|
|
|
100
|
-
|
|
94
|
+
A chained command cannot be approved as one; a message passed through the shell (`-m "…"`, a heredoc) can run the backticks and `$(…)` it quotes. Let the repository's own commit hooks run.
|