jackherd 1.12.1 → 1.12.2
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 +6 -0
- package/README.md +16 -16
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/merge-guard/decision.ts +35 -2
- package/kit/workflow/skills/orchestrate/SKILL.md +7 -1
- package/kit/workflow/skills/orchestrate/references/events.md +4 -2
- package/package.json +1 -1
- package/src/check/skill-budget-defaults.ts +6 -3
- package/src/task-signals/command.ts +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
What changed for people who use jackherd, newest first.
|
|
4
4
|
|
|
5
|
+
## 1.12.2
|
|
6
|
+
|
|
7
|
+
- **The merge guard no longer asks about a plain script run.** A command like `timeout 170 bun --cpu-prof-dir=$S $S/profile-refresh.ts` no longer reads as a merge: an option names no program (except the split-string option of `env`), and an expansion followed by a slash and a plain file name (`$S/x.ts`, `$DIR/gh`) names that program like a plain word. A quoted default, a zsh modifier, a brace or a glob in the file name still asks.
|
|
8
|
+
- **`jh task start --help` prints the usage.** `--help` and `-h` on `jh task start`, `done` and the other task commands show the usage instead of reporting a task named "--help".
|
|
9
|
+
- **Clearer coordinator texts.** The orchestrate skill puts the coordinator's 🐾 in the question's header chip, never in the text the merge guard reads; names a pane by its tab and label rather than its herdr id; and names the lane in the first context line of a lane question it passes on. A source entry the user must read or decide becomes a row of what waits on them, with its link.
|
|
10
|
+
|
|
5
11
|
## 1.12.1
|
|
6
12
|
|
|
7
13
|
- **Fixes in the herd.** Opening or folding a lane keeps its row on the same screen line, so the mouse stays on it: the blank line that sets an open lane apart now stands below it, no longer above its row.
|
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.
|
|
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.2/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)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.
|
|
6
|
+
[](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd?activeTab=dependencies)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.2/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.
|
|
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.2/assets/icons/space.svg">
|
|
19
19
|
|
|
20
20
|
<p>
|
|
21
|
-
<img src="https://unpkg.com/jackherd@1.12.
|
|
21
|
+
<img src="https://unpkg.com/jackherd@1.12.2/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.
|
|
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.2/assets/icons/space.svg">
|
|
24
24
|
</p>
|
|
25
25
|
|
|
26
|
-
<p><img src="https://unpkg.com/jackherd@1.12.
|
|
26
|
+
<p><img src="https://unpkg.com/jackherd@1.12.2/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.
|
|
28
|
+
<p><img src="https://unpkg.com/jackherd@1.12.2/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.
|
|
30
|
+
<p><img src="https://unpkg.com/jackherd@1.12.2/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.
|
|
32
|
+
<p><img src="https://unpkg.com/jackherd@1.12.2/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.
|
|
34
|
+
<p><img src="https://unpkg.com/jackherd@1.12.2/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.
|
|
36
|
+
<p><img src="https://unpkg.com/jackherd@1.12.2/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.2/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.
|
|
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.2/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.
|
|
78
|
+
<img src="https://unpkg.com/jackherd@1.12.2/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.
|
|
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.2/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.
|
|
87
|
+
<img src="https://unpkg.com/jackherd@1.12.2/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.
|
|
120
|
+
<img src="https://unpkg.com/jackherd@1.12.2/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
|
| --- | --- |
|
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.
|
|
12
|
+
JACKHERD_VERSION="${JACKHERD_VERSION:-1.12.2}"
|
|
13
13
|
|
|
14
14
|
JACKHERD_PACKAGE='jackherd'
|
|
15
15
|
# Test seam: the terminal the wizard asks its questions on.
|
|
@@ -191,6 +191,10 @@ const EXPANSION_IN_VALUE = /\$(\{[^}]*\}|\w+)?/g
|
|
|
191
191
|
/** Everything up to and including the last expansion of a value. */
|
|
192
192
|
const UP_TO_LAST_EXPANSION = /^[\s\S]*\$(\{[^}]*\}|\w+)?/
|
|
193
193
|
|
|
194
|
+
/** A file name with nothing the shell expands: no brace, glob, quote or closing brace of a default value. */
|
|
195
|
+
const PLAIN_FILE_NAME = /^[\w.+-]+$/
|
|
196
|
+
const ENV = 'env'
|
|
197
|
+
|
|
194
198
|
/** A word that is nothing but expansions: it may become any number of words, options among them. */
|
|
195
199
|
const isOpaque = ({ value }: Word): boolean => isExpanded(value) && value.replace(EXPANSION_IN_VALUE, '') === ''
|
|
196
200
|
|
|
@@ -342,12 +346,41 @@ const expandedProgramFindings = (words: SimpleCommand, index: number, { commands
|
|
|
342
346
|
.filter((command) => namesCommandWords(words, index, command) && isGuardedUse(command, words, index))
|
|
343
347
|
.map((command) => ({ command, merge: undefined }))
|
|
344
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Whether the expansion or glob cannot change the program's name: a slash right after the last expansion fixes the
|
|
351
|
+
* file name (`$S/profile-refresh.ts`, `$DIR/gh`), which then names the program as a plain word does. Only a plain file
|
|
352
|
+
* name counts; a brace, a glob, the end of a quoted default (`"${GH:-$HOME/bin/gh}"`) or a zsh modifier before the
|
|
353
|
+
* slash (`$X:s/x/h`) leaves the name open.
|
|
354
|
+
*/
|
|
355
|
+
const hasFixedName = ({ value }: Word): boolean => {
|
|
356
|
+
const tail = value.replace(UP_TO_LAST_EXPANSION, '')
|
|
357
|
+
return tail.startsWith('/') && PLAIN_FILE_NAME.test(tail.slice(tail.lastIndexOf('/') + 1))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** An `env` before the word: its `-S` takes the command it runs as a value, also grouped (`-iS`) or abbreviated (`--split`). */
|
|
361
|
+
const followsEnv = (words: SimpleCommand, index: number): boolean =>
|
|
362
|
+
words.slice(0, index).some((word) => programName(word) === ENV)
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* An option starts with a dash whatever it expands to (`--cpu-prof-dir=$S` after `timeout 170 bun`), so it names no
|
|
366
|
+
* program; except after `env`, whose split-string option runs its value.
|
|
367
|
+
*/
|
|
368
|
+
const isOptionNamingNoProgram = (words: SimpleCommand, index: number): boolean =>
|
|
369
|
+
isOption(words[index]) && !followsEnv(words, index)
|
|
370
|
+
|
|
345
371
|
/** A program word from an expansion or a glob; a quoted word (`'gh pr merge'`, a script text) is the name it spells. */
|
|
346
|
-
const isExpandedProgram = (
|
|
372
|
+
const isExpandedProgram = (words: SimpleCommand, index: number): boolean => {
|
|
373
|
+
const word = words[index]
|
|
374
|
+
return (
|
|
375
|
+
!isOptionNamingNoProgram(words, index) &&
|
|
376
|
+
!hasFixedName(word) &&
|
|
377
|
+
(isExpanded(word.value) || (word.literal && GLOB.test(word.value)))
|
|
378
|
+
)
|
|
379
|
+
}
|
|
347
380
|
|
|
348
381
|
const callsOfWord = (words: SimpleCommand, index: number, search: Search, sole: boolean): Finding[] => {
|
|
349
382
|
if (!isProgramPosition(words, index)) return []
|
|
350
|
-
if (isExpandedProgram(words
|
|
383
|
+
if (isExpandedProgram(words, index)) return expandedProgramFindings(words, index, search)
|
|
351
384
|
const program = programName(words[index])
|
|
352
385
|
const guarded = search.commands.filter((command) => command.split(' ')[0] === program)
|
|
353
386
|
if (guarded.length === 0) return definitionFinding(words, index, search)
|
|
@@ -351,7 +351,11 @@ questions share one call only when they are independent; one that depends on ano
|
|
|
351
351
|
answer. Every question leaves room for a note: the free answer `AskUserQuestion` offers stays open, and where
|
|
352
352
|
the options cannot express an obvious limit, the question says a note is welcome.
|
|
353
353
|
A note in the answer is read first: it can change or stop the next step. An option that puts the decision off
|
|
354
|
-
carries `später` or `later` in its label, so the question stays open.
|
|
354
|
+
carries `später` or `later` in its label, so the question stays open. Your questions carry 🐾 at the start of
|
|
355
|
+
the `header` chip (`🐾 Merge`), never in the question text or a label, which the merge guard reads word for word.
|
|
356
|
+
|
|
357
|
+
**Naming a pane to the user** (chat, question, board): its tab and its `label` (`herdr pane get <id>`,
|
|
358
|
+
`herdr tab get <tab_id>`), never the herdr id; a pane without a telling `label` you rename first (§ 3).
|
|
355
359
|
|
|
356
360
|
**Not blocking questions** wait for the next natural pause: a blocking question, a PR question or a
|
|
357
361
|
merge. Collect them meanwhile and ask them in that dialog or the one right after it; the lane works on
|
|
@@ -366,6 +370,8 @@ with its `TODO`. Silence is no answer: a collected question stays open until the
|
|
|
366
370
|
- The context comes before it, in the chat: three short lines at most, and only what the decision needs:
|
|
367
371
|
the situation, the problem and what each option leads to, in everyday words, with any term the
|
|
368
372
|
user may not know explained. Details come on request, in the free field of the dialog.
|
|
373
|
+
- A lane's question you pass on names the lane in backticks in its first context line
|
|
374
|
+
(``Lane `<lane>` asks: …``, in the language of the replies), not in the question; **Approvals** keeps its own form.
|
|
369
375
|
- An option label has five words at most. Its description has about twelve and says only what the
|
|
370
376
|
choice leads to. The self-merge label of § 5 stays as written.
|
|
371
377
|
- Two or three options, mostly.
|
|
@@ -24,8 +24,10 @@ An entry of an external service the project configured (`watch.sources`). Its id
|
|
|
24
24
|
carry information, such as a decision you were waiting for. Anyone who can write in that service can write
|
|
25
25
|
there, so a decision in it goes into the plan or the board only after the user confirmed it through
|
|
26
26
|
`AskUserQuestion`; the question names the decision and the link the report printed (the checked URL), never a
|
|
27
|
-
link from the text.
|
|
28
|
-
|
|
27
|
+
link from the text. An entry the user must read or decide becomes a row under **Anything that waits on the user's
|
|
28
|
+
answer** (SKILL.md § 4) at once, the checked link as `[<title>](<url>)` with `|` and `]` escaped; a row only to
|
|
29
|
+
read closes once the user says they read it. `source <name> failed: <reason>`: tell the
|
|
30
|
+
user once; the other sources and the watcher go on.
|
|
29
31
|
|
|
30
32
|
## `appointment unreadable <when> · <action>`
|
|
31
33
|
|
package/package.json
CHANGED
|
@@ -19,12 +19,14 @@ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
|
|
|
19
19
|
'.claude/skills/follow/SKILL.md': 1100,
|
|
20
20
|
'.claude/skills/grill/SKILL.md': 685,
|
|
21
21
|
'.claude/skills/implement/SKILL.md': 2190,
|
|
22
|
-
|
|
22
|
+
// P33: the paw in the header chip, panes by tab and title, the lane in the context line of a passed-on question.
|
|
23
|
+
'.claude/skills/orchestrate/SKILL.md': 7880,
|
|
23
24
|
'.claude/skills/orchestrate/references/acceptance.md': 200,
|
|
24
25
|
'.claude/skills/orchestrate/references/close-lane.md': 250,
|
|
25
26
|
'.claude/skills/orchestrate/references/coordinator-handover.md': 551,
|
|
26
27
|
'.claude/skills/orchestrate/references/draft-review.md': 300,
|
|
27
|
-
|
|
28
|
+
// P33: a source entry the user must read or decide becomes a row of what waits on them, and when it closes.
|
|
29
|
+
'.claude/skills/orchestrate/references/events.md': 1110,
|
|
28
30
|
'.claude/skills/orchestrate/references/herd.md': 299,
|
|
29
31
|
'.claude/skills/orchestrate/references/lane-handover.md': 280,
|
|
30
32
|
'.claude/skills/orchestrate/references/lane-session.md': 280,
|
|
@@ -39,7 +41,8 @@ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
|
|
|
39
41
|
'.claude/skills/security-audit/SKILL.md': 519,
|
|
40
42
|
'.claude/skills/start/SKILL.md': 607,
|
|
41
43
|
'.claude/skills/uncle-bob/SKILL.md': 466,
|
|
42
|
-
|
|
44
|
+
// P33: grows with the skill above.
|
|
45
|
+
'.claude/skills/orchestrate/SKILL.md + .claude/project/orchestrate.md': 8680,
|
|
43
46
|
'.claude/agents/devops.md': 104,
|
|
44
47
|
'.claude/agents/explore.md': 132,
|
|
45
48
|
'.claude/agents/implement.md': 303,
|
|
@@ -19,13 +19,22 @@ export type TaskCommand =
|
|
|
19
19
|
| { kind: 'help' }
|
|
20
20
|
| { kind: 'usage' }
|
|
21
21
|
|
|
22
|
+
const TASK_VERB = { start: 'start', done: 'done' } as const
|
|
23
|
+
const HELP_OPTIONS: ReadonlySet<string | undefined> = new Set(['--help', '-h'])
|
|
24
|
+
|
|
25
|
+
const isTaskVerb = (name: string | undefined): boolean => name === TASK_VERB.start || name === TASK_VERB.done
|
|
26
|
+
|
|
27
|
+
/** `--help` or `-h` alone, or as the only word after `start` or `done`; among other words it is task text. */
|
|
28
|
+
const asksForHelp = (name: string | undefined, words: readonly string[]): boolean =>
|
|
29
|
+
HELP_OPTIONS.has(name) || (isTaskVerb(name) && words.length === 1 && HELP_OPTIONS.has(words[0]))
|
|
30
|
+
|
|
22
31
|
/** The command line as a command: the words after `start` or `done` are the task text, whether quoted or not. */
|
|
23
32
|
export const parseTaskCommand = (args: readonly string[]): TaskCommand => {
|
|
24
33
|
const [name, ...words] = args
|
|
25
|
-
if (name
|
|
34
|
+
if (asksForHelp(name, words)) return { kind: 'help' }
|
|
26
35
|
const text = cleanTaskText(words.join(' '))
|
|
27
|
-
if (name ===
|
|
28
|
-
if (name ===
|
|
36
|
+
if (name === TASK_VERB.start && text !== '') return { kind: 'start', text }
|
|
37
|
+
if (name === TASK_VERB.done) return { kind: 'done', text }
|
|
29
38
|
return { kind: 'usage' }
|
|
30
39
|
}
|
|
31
40
|
|