jackherd 1.15.0 → 1.15.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 +15 -0
- package/README.md +17 -17
- package/dist/{chunk-V4HOLGFP.js → chunk-4LUFR7RC.js} +19 -1
- package/dist/config.d.ts +47 -2
- package/dist/config.js +65 -4
- package/dist/plan-format.js +3 -19
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +49 -4
- package/kit/hooks/hooks/merge-guard/decision.ts +3 -1
- package/kit/rules/rules/kit/pull-requests.md +1 -1
- package/package.json +1 -1
- package/schemas/jackherd.schema.json +12 -0
- package/schemas/sources.schema.json +141 -0
- package/src/cli.ts +1 -0
- package/src/config/json-schema.ts +7 -3
- package/src/config/location.ts +7 -0
- package/src/config/personal-sources.ts +83 -0
- package/src/config/schema.ts +29 -0
- package/src/plans/herd-claim.ts +9 -31
- package/src/plans/settings.ts +20 -7
- package/src/plans/snapshot.ts +26 -19
- package/src/secrets/command.ts +163 -0
- package/src/secrets/environment.ts +45 -0
- package/src/secrets/hidden-input.ts +51 -0
- package/src/secrets/listing.ts +63 -0
- package/src/secrets/options.ts +45 -0
- package/src/secrets/store.ts +74 -0
- package/src/shared/process-info.ts +89 -0
- package/src/sync/command-context.ts +26 -4
- package/src/sync/render.ts +9 -4
- package/src/watch/command.ts +44 -12
- package/src/watch/plan-badges-read.ts +28 -10
- package/src/watch/source-dry-run-command.ts +23 -12
- package/src/watch/source-dry-run.ts +19 -2
- package/src/watch/source-reload.ts +100 -0
- package/src/watch/source-run.ts +63 -7
- package/src/watch/source-tickets.ts +15 -0
- package/src/watch/sources.ts +21 -0
- package/src/watch/watch-events.ts +9 -0
- package/src/watch/watched-source.ts +39 -0
- package/src/watch/watcher-lock.ts +8 -1
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.15.2
|
|
6
|
+
|
|
7
|
+
- **The installer no longer crashes on macOS.** `install.sh` redirected `jh init --global` from `/dev/tty`, which the macOS kqueue that Bun reads input through rejects (`EINVAL: invalid argument, kqueue`) at the first question. It now names the terminal's own device (such as `/dev/ttys003`) first, through `tty` on its input or, since macOS prints `/dev/tty` for `tty </dev/tty`, through `ps`, and reads from that.
|
|
8
|
+
- **An unreadable terminal takes the default.** If the terminal cannot be read during a question, `jh init` prints one note on stderr and takes the default answer instead of stopping with an error.
|
|
9
|
+
|
|
10
|
+
- **The herd recognises an older herd on macOS.** The herd view found out whether an earlier view of the same terminal was still running by reading `/proc`, which macOS does not have, so it never took one over and two herds could draw into one terminal. It now asks `ps` there and keeps `/proc` on Linux.
|
|
11
|
+
- **Copying env files into a lane works on macOS.** The copy command in the lane setup used `cp --parents` and `grep -z`, which only the GNU tools know. It now uses `git ls-files` with an exclude pattern and `tar`, which keep folders and file modes (an env file stays `600`) with the tools of both systems. Run `jh sync` to get the new text.
|
|
12
|
+
- **Every `ps` call is bounded.** A `ps` that hangs no longer holds up the herd or the watcher.
|
|
13
|
+
- **The installer refuses a bun under a relative `PATH` entry on macOS too.** `install.sh` asked `command -v` for the path, which bash 3.2 (macOS `/bin/sh`) answers with an absolute path even for a relative entry, so the refusal never fired. It now walks `PATH` itself and names the entry as written.
|
|
14
|
+
|
|
15
|
+
## 1.15.1
|
|
16
|
+
|
|
17
|
+
- **A test with a variable no longer asks.** The merge guard read the `[` of `[ -f $P ]` as a glob program, so any variable after it could be `gh pr merge` and the guard asked, also for a plain grep. `[` and `[[` now count as the test builtin; a merge after a test, and a program from a variable, still ask.
|
|
18
|
+
- **A draft says when it turns ready.** With `"merge": { "openAsDraft": true }` the pull request rule tells an agent outside `/jh-orchestrate` as well when to run `gh pr ready`: before it hands the pull request to you to merge, or before it waits on the checks that start then for its own `gh pr merge`.
|
|
19
|
+
|
|
5
20
|
## 1.15.0
|
|
6
21
|
|
|
7
22
|
- **Any plan, once it meets the contract.** `docs/plan-contract.md` writes down what Jack needs from a plan: phases with an id, checkbox tasks, a model per phase, and from two phases on a dependency table. `jh plan check <file>` says per part whether it is there, missing or malformed, and exits 0 when the plan meets it. Without a dependency table, phases run one after another, and `jh check --parallel` says so.
|
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.15.
|
|
2
|
+
<img alt="jackherd: a black and white Shih Tzu in a turquoise bandana, three lanes running from him to three agent nodes" src="https://unpkg.com/jackherd@1.15.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.15.
|
|
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.15.2/assets/icons/space.svg">
|
|
7
7
|
|
|
8
8
|
# Jack herds your coding agents.
|
|
9
9
|
|
|
@@ -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.15.
|
|
18
|
+
Then `/jh-start` in a repository, and Jack offers your first plan. → [Quickstart](#quickstart)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.2/assets/icons/space.svg">
|
|
19
19
|
|
|
20
20
|
<p>
|
|
21
|
-
<img src="https://unpkg.com/jackherd@1.15.
|
|
21
|
+
<img src="https://unpkg.com/jackherd@1.15.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.15.
|
|
23
|
+
<sub>Jack and his lanes in <a href="https://herdr.dev/">herdr</a>, a terminal for coding agents (recommended). Without it, one Claude Code session builds with subagents.</sub><br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.2/assets/icons/space.svg">
|
|
24
24
|
</p>
|
|
25
25
|
|
|
26
|
-
<p><img src="https://unpkg.com/jackherd@1.15.
|
|
26
|
+
<p><img src="https://unpkg.com/jackherd@1.15.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.15.
|
|
28
|
+
<p><img src="https://unpkg.com/jackherd@1.15.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.</p>
|
|
29
29
|
|
|
30
|
-
<p><img src="https://unpkg.com/jackherd@1.15.
|
|
30
|
+
<p><img src="https://unpkg.com/jackherd@1.15.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>
|
|
31
31
|
|
|
32
|
-
<p><img src="https://unpkg.com/jackherd@1.15.
|
|
32
|
+
<p><img src="https://unpkg.com/jackherd@1.15.2/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.15.
|
|
34
|
+
<p><img src="https://unpkg.com/jackherd@1.15.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>
|
|
35
35
|
|
|
36
|
-
<p><img src="https://unpkg.com/jackherd@1.15.
|
|
36
|
+
<p><img src="https://unpkg.com/jackherd@1.15.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>
|
|
37
37
|
|
|
38
|
-
<p><img src="https://unpkg.com/jackherd@1.15.
|
|
38
|
+
<p><img src="https://unpkg.com/jackherd@1.15.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.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.2/assets/icons/space.svg"></p>
|
|
39
39
|
|
|
40
40
|
## Quickstart
|
|
41
41
|
|
|
@@ -71,22 +71,22 @@ Then `/jh-start` in a repository, and Jack offers your first plan. → [Quicksta
|
|
|
71
71
|
/jh-orchestrate docs/plans/board-<name>/board.md
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks. → [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.
|
|
74
|
+
By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks. → [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.2/assets/icons/space.svg">
|
|
75
75
|
|
|
76
76
|
## Follow anything outside GitHub
|
|
77
77
|
|
|
78
78
|
Whatever you check by hand, Jack can watch: tickets, error logs, status pages, deploys. One `/jh-follow`, one yes, and every change comes to you like this:
|
|
79
79
|
|
|
80
|
-
<img src="https://unpkg.com/jackherd@1.15.
|
|
80
|
+
<img src="https://unpkg.com/jackherd@1.15.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.">
|
|
81
81
|
|
|
82
|
-
The herd sums up your sources in one `Sources` line: per source its new entries, or when it last ran, red while it fails; opened, a click on an entry opens it. A ticket shows on the lane that works it, and a row in Your turn that came from a source starts with that source's label.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.
|
|
82
|
+
The herd sums up your sources in one `Sources` line: per source its new entries, or when it last ran, red while it fails; opened, a click on an entry opens it. A ticket shows on the lane that works it, and a row in Your turn that came from a source starts with that source's label.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.2/assets/icons/space.svg">
|
|
83
83
|
|
|
84
84
|
## In detail
|
|
85
85
|
|
|
86
86
|
<details>
|
|
87
87
|
<summary><b>What Jack does on his own, and what waits for you</b></summary>
|
|
88
88
|
|
|
89
|
-
<img src="https://unpkg.com/jackherd@1.15.
|
|
89
|
+
<img src="https://unpkg.com/jackherd@1.15.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.">
|
|
90
90
|
|
|
91
91
|
| On his own | After your approval | Never |
|
|
92
92
|
| --- | --- | --- |
|
|
@@ -121,7 +121,7 @@ The guards catch mistakes, not intent; for that, use branch protection. The hook
|
|
|
121
121
|
|
|
122
122
|
A **plan** is Markdown with phases, checkbox tasks, a model and a review gate per phase. A **board** lists the lanes of one or more plans: which phases each lane builds, on which model, after what. A **lane** is one agent session in its own git worktree (a second checkout of your repository), and **Jack** is the session in the main checkout that runs `/jh-orchestrate` on a board.
|
|
123
123
|
|
|
124
|
-
<img src="https://unpkg.com/jackherd@1.15.
|
|
124
|
+
<img src="https://unpkg.com/jackherd@1.15.2/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
125
|
|
|
126
126
|
| Skill | Does |
|
|
127
127
|
| --- | --- |
|
|
@@ -160,6 +160,23 @@ var classifyStatus = (language, statusText) => {
|
|
|
160
160
|
var phaseHeading = (language, id, title) => `### ${PLAN_VOCABULARY[language].phase} ${id}: ${title}`;
|
|
161
161
|
var sectionHeading = (language, section) => `## ${PLAN_VOCABULARY[language].sections[section]}`;
|
|
162
162
|
|
|
163
|
+
// src/plan-format/terminal-text.ts
|
|
164
|
+
var UNSAFE_RANGES = [
|
|
165
|
+
[0, 31],
|
|
166
|
+
[127, 159],
|
|
167
|
+
[1564, 1564],
|
|
168
|
+
[8206, 8207],
|
|
169
|
+
[8232, 8233],
|
|
170
|
+
[8234, 8238],
|
|
171
|
+
[8294, 8297]
|
|
172
|
+
];
|
|
173
|
+
var escapeCodePoint = (codePoint) => `\\u${codePoint.toString(16).padStart(4, "0")}`;
|
|
174
|
+
var UNSAFE_TERMINAL_TEXT = new RegExp(
|
|
175
|
+
`[${UNSAFE_RANGES.map(([from, to]) => `${escapeCodePoint(from)}-${escapeCodePoint(to)}`).join("")}]`,
|
|
176
|
+
"g"
|
|
177
|
+
);
|
|
178
|
+
var terminalSafe = (text) => text.replace(UNSAFE_TERMINAL_TEXT, "");
|
|
179
|
+
|
|
163
180
|
export {
|
|
164
181
|
PLAN_LANGUAGE,
|
|
165
182
|
LANGUAGE_NAME,
|
|
@@ -177,5 +194,6 @@ export {
|
|
|
177
194
|
PLAN_VOCABULARY,
|
|
178
195
|
classifyStatus,
|
|
179
196
|
phaseHeading,
|
|
180
|
-
sectionHeading
|
|
197
|
+
sectionHeading,
|
|
198
|
+
terminalSafe
|
|
181
199
|
};
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
3
|
declare const renderConfigJsonSchema: () => string;
|
|
4
|
+
declare const renderSourcesJsonSchema: () => string;
|
|
4
5
|
|
|
5
6
|
declare const TERMINAL_ADAPTER: {
|
|
6
7
|
readonly none: "none";
|
|
@@ -72,6 +73,8 @@ declare const sourceSchema: z.ZodObject<{
|
|
|
72
73
|
magenta: "magenta";
|
|
73
74
|
}>>;
|
|
74
75
|
reportOn: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
76
|
+
secrets: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
77
|
+
show: z.ZodOptional<z.ZodLiteral<"entries">>;
|
|
75
78
|
}, z.core.$strict>;
|
|
76
79
|
declare const jackherdConfigSchema: z.ZodObject<{
|
|
77
80
|
$schema: z.ZodOptional<z.ZodString>;
|
|
@@ -113,9 +116,9 @@ declare const jackherdConfigSchema: z.ZodObject<{
|
|
|
113
116
|
lockFile: z.ZodOptional<z.ZodString>;
|
|
114
117
|
}, z.core.$strict>>;
|
|
115
118
|
models: z.ZodDefault<z.ZodRecord<z.ZodEnum<{
|
|
119
|
+
review: "review";
|
|
116
120
|
plan: "plan";
|
|
117
121
|
implement: "implement";
|
|
118
|
-
review: "review";
|
|
119
122
|
explore: "explore";
|
|
120
123
|
orchestrate: "orchestrate";
|
|
121
124
|
}>, z.ZodString>>;
|
|
@@ -249,6 +252,8 @@ declare const jackherdConfigSchema: z.ZodObject<{
|
|
|
249
252
|
magenta: "magenta";
|
|
250
253
|
}>>;
|
|
251
254
|
reportOn: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
255
|
+
secrets: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
256
|
+
show: z.ZodOptional<z.ZodLiteral<"entries">>;
|
|
252
257
|
}, z.core.$strict>>>;
|
|
253
258
|
}, z.core.$strict>>;
|
|
254
259
|
guard: z.ZodPrefault<z.ZodPipe<z.ZodObject<{
|
|
@@ -338,7 +343,47 @@ type JackherdConfig = z.infer<typeof jackherdConfigSchema>;
|
|
|
338
343
|
declare const parseJackherdConfig: (raw: unknown) => JackherdConfig;
|
|
339
344
|
/** `parseJackherdConfig` for a file: an invalid one throws with `label` (its path) and the issues in words. */
|
|
340
345
|
declare const parseConfigFile: (raw: unknown, label: string) => JackherdConfig;
|
|
346
|
+
/** The personal sources file (`~/.jackherd/sources.json`): the same entries as `watch.sources`. */
|
|
347
|
+
declare const personalSourcesSchema: z.ZodObject<{
|
|
348
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
349
|
+
sources: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
350
|
+
name: z.ZodString;
|
|
351
|
+
command: z.ZodArray<z.ZodString>;
|
|
352
|
+
every: z.ZodDefault<z.ZodNumber>;
|
|
353
|
+
items: z.ZodOptional<z.ZodString>;
|
|
354
|
+
map: z.ZodObject<{
|
|
355
|
+
id: z.ZodString;
|
|
356
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
357
|
+
title: z.ZodOptional<z.ZodString>;
|
|
358
|
+
url: z.ZodOptional<z.ZodString>;
|
|
359
|
+
body: z.ZodOptional<z.ZodString>;
|
|
360
|
+
author: z.ZodOptional<z.ZodString>;
|
|
361
|
+
status: z.ZodOptional<z.ZodString>;
|
|
362
|
+
count: z.ZodOptional<z.ZodString>;
|
|
363
|
+
}, z.core.$catchall<z.ZodString>>;
|
|
364
|
+
ignoreAuthors: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
365
|
+
reportLimit: z.ZodDefault<z.ZodNumber>;
|
|
366
|
+
label: z.ZodOptional<z.ZodString>;
|
|
367
|
+
lane: z.ZodOptional<z.ZodString>;
|
|
368
|
+
ticket: z.ZodOptional<z.ZodBoolean>;
|
|
369
|
+
tone: z.ZodOptional<z.ZodEnum<{
|
|
370
|
+
amber: "amber";
|
|
371
|
+
teal: "teal";
|
|
372
|
+
grey: "grey";
|
|
373
|
+
red: "red";
|
|
374
|
+
green: "green";
|
|
375
|
+
blue: "blue";
|
|
376
|
+
magenta: "magenta";
|
|
377
|
+
}>>;
|
|
378
|
+
reportOn: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
379
|
+
secrets: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
380
|
+
show: z.ZodOptional<z.ZodLiteral<"entries">>;
|
|
381
|
+
}, z.core.$strict>>>;
|
|
382
|
+
}, z.core.$strict>;
|
|
383
|
+
type PersonalSourcesFile = z.infer<typeof personalSourcesSchema>;
|
|
384
|
+
/** The personal sources file as read from JSON; invalid throws with `label` (its path) and the issues in words. */
|
|
385
|
+
declare const parsePersonalSourcesFile: (raw: unknown, label: string) => PersonalSourcesFile;
|
|
341
386
|
/** One `watch.sources` entry outside the config, such as a draft to try; invalid throws with `label` and the issues. */
|
|
342
387
|
declare const parseSourceDraft: (raw: unknown, label: string) => z.infer<typeof sourceSchema>;
|
|
343
388
|
|
|
344
|
-
export { COMMAND_KEYS, type CommandKey, DEFAULT_CONTEXT_WINDOW, DEFAULT_MODELS, DEFAULT_SKILL_PREFIX, type JackherdConfig, KIT_CORE, MODEL_ROLE, type ModelRole, SKILL_PREFIX, TERMINAL_ADAPTER, type TerminalAdapter, isModelRole, isTerminalAdapter, jackherdConfigSchema, parseConfigFile, parseJackherdConfig, parseSourceDraft, renderConfigJsonSchema };
|
|
389
|
+
export { COMMAND_KEYS, type CommandKey, DEFAULT_CONTEXT_WINDOW, DEFAULT_MODELS, DEFAULT_SKILL_PREFIX, type JackherdConfig, KIT_CORE, MODEL_ROLE, type ModelRole, type PersonalSourcesFile, SKILL_PREFIX, TERMINAL_ADAPTER, type TerminalAdapter, isModelRole, isTerminalAdapter, jackherdConfigSchema, parseConfigFile, parseJackherdConfig, parsePersonalSourcesFile, parseSourceDraft, personalSourcesSchema, renderConfigJsonSchema, renderSourcesJsonSchema };
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PLAN_LANGUAGE,
|
|
3
3
|
RESPONSE_ADDRESS
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-4LUFR7RC.js";
|
|
5
5
|
|
|
6
6
|
// src/config/json-schema.ts
|
|
7
7
|
import { z as z3 } from "zod";
|
|
@@ -112,6 +112,15 @@ var SKILL_PATH = new RegExp(`^${SKILLS_DIRECTORY}/([^/]+)/(.+)$`);
|
|
|
112
112
|
var AGENT_PATH = new RegExp(`^${AGENTS_DIRECTORY}/([^/]+)\\.md$`);
|
|
113
113
|
var claudePath = (...segments) => [CLAUDE_DIRECTORY, ...segments].join("/");
|
|
114
114
|
|
|
115
|
+
// src/version/release.ts
|
|
116
|
+
var CLI_NAME = "jh";
|
|
117
|
+
|
|
118
|
+
// src/secrets/options.ts
|
|
119
|
+
var SECRET_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
120
|
+
var SECRET_USAGE = `Usage: ${CLI_NAME} secret set <NAME> store a value in the keychain, typed hidden in a terminal
|
|
121
|
+
${CLI_NAME} secret list the names the sources need, set or missing, never a value
|
|
122
|
+
${CLI_NAME} secret delete <NAME> remove the value from the keychain`;
|
|
123
|
+
|
|
115
124
|
// src/watch/source-fields.ts
|
|
116
125
|
var SOURCE_FIELD = {
|
|
117
126
|
id: "id",
|
|
@@ -141,6 +150,38 @@ var pathSteps = (path) => {
|
|
|
141
150
|
};
|
|
142
151
|
var isSourcePath = (path) => pathSteps(path) !== null;
|
|
143
152
|
|
|
153
|
+
// src/watch/sources.ts
|
|
154
|
+
import { createHash } from "crypto";
|
|
155
|
+
|
|
156
|
+
// src/watch/feedback-item.ts
|
|
157
|
+
var FEEDBACK_FORM = { review: "review", inline: "inline", comment: "comment" };
|
|
158
|
+
|
|
159
|
+
// src/watch/no-finding.ts
|
|
160
|
+
var ROUND_WINDOW_MS = 5 * 6e4;
|
|
161
|
+
|
|
162
|
+
// src/watch/pull-requests.ts
|
|
163
|
+
var WATCHED_PULL_REQUEST_FIELDS = "number,title,url,headRefName,isDraft,state,headRefOid,mergeCommit";
|
|
164
|
+
var OPEN_PULL_REQUEST_FIELDS = `${WATCHED_PULL_REQUEST_FIELDS},statusCheckRollup,files`;
|
|
165
|
+
|
|
166
|
+
// src/watch/feedback.ts
|
|
167
|
+
var REVIEW_STATE = {
|
|
168
|
+
approved: "APPROVED",
|
|
169
|
+
changesRequested: "CHANGES_REQUESTED",
|
|
170
|
+
commented: "COMMENTED",
|
|
171
|
+
dismissed: "DISMISSED"
|
|
172
|
+
};
|
|
173
|
+
var DECISIVE_REVIEW_STATES = [
|
|
174
|
+
REVIEW_STATE.approved,
|
|
175
|
+
REVIEW_STATE.changesRequested,
|
|
176
|
+
REVIEW_STATE.dismissed
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
// src/watch/previews.ts
|
|
180
|
+
var PREVIEW_SOURCE = { deployment: "deployment", comment: FEEDBACK_FORM.comment };
|
|
181
|
+
|
|
182
|
+
// src/watch/sources.ts
|
|
183
|
+
var SOURCE_SHOW = { entries: "entries" };
|
|
184
|
+
|
|
144
185
|
// src/watch/watch-defaults.ts
|
|
145
186
|
var CLEAN_REVIEW_TEXT = "## Code review\n\nNo issues found. Checked for bugs and CLAUDE.md compliance.";
|
|
146
187
|
var BUN_FAILED_TEST_PATTERN = "^\\(fail\\) (.+?)(?: \\[[\\d.]+m?s\\])?$";
|
|
@@ -434,13 +475,23 @@ var sourceSchema = z2.strictObject({
|
|
|
434
475
|
* The mapped fields whose change reports, with the old and the new value, in place of the content of `title`,
|
|
435
476
|
* `author`, `body` and `url`; without it a change of that content reports.
|
|
436
477
|
*/
|
|
437
|
-
reportOn: z2.array(z2.string().min(1)).min(1).max(10).optional()
|
|
478
|
+
reportOn: z2.array(z2.string().min(1)).min(1).max(10).optional(),
|
|
479
|
+
/**
|
|
480
|
+
* Names of secrets the command needs, handed to it as environment variables of the same name; each is looked up
|
|
481
|
+
* in the keychain, then in the watcher's environment.
|
|
482
|
+
*/
|
|
483
|
+
secrets: z2.array(z2.string().regex(SECRET_NAME, "capital letters, digits and `_`, starting with a letter")).max(10).refine((names) => new Set(names).size === names.length, "secret names must be unique").optional(),
|
|
484
|
+
/** `entries` shows the source's entries open, in a herd block of its own; not for a ticket source, whose entries are badges. */
|
|
485
|
+
show: z2.literal(SOURCE_SHOW.entries).optional()
|
|
438
486
|
}).refine(canChange, {
|
|
439
487
|
message: "map needs updatedAt, a content field (title, author, body, url) or reportOn",
|
|
440
488
|
path: ["map"]
|
|
441
489
|
}).refine((source) => source.ignoreAuthors.length === 0 || source.map.author !== void 0, {
|
|
442
490
|
message: "ignoreAuthors needs map.author",
|
|
443
491
|
path: ["ignoreAuthors"]
|
|
492
|
+
}).refine((source) => source.show === void 0 || source.ticket !== true, {
|
|
493
|
+
message: "show cannot be set on a ticket source",
|
|
494
|
+
path: ["show"]
|
|
444
495
|
}).refine((source) => !source.reportOn?.includes(SOURCE_FIELD.id), {
|
|
445
496
|
message: "reportOn cannot name id",
|
|
446
497
|
path: ["reportOn"]
|
|
@@ -648,11 +699,18 @@ ${z2.prettifyError(parsed.error)}`);
|
|
|
648
699
|
return parsed.data;
|
|
649
700
|
};
|
|
650
701
|
var parseConfigFile = (raw, label) => parseLabelled(jackherdConfigSchema, raw, label);
|
|
702
|
+
var personalSourcesSchema = z2.strictObject({
|
|
703
|
+
$schema: z2.string().optional(),
|
|
704
|
+
sources: z2.array(sourceSchema).refine(hasUniqueNames, "source names must be unique").default([])
|
|
705
|
+
});
|
|
706
|
+
var parsePersonalSourcesFile = (raw, label) => parseLabelled(personalSourcesSchema, raw, label);
|
|
651
707
|
var parseSourceDraft = (raw, label) => parseLabelled(sourceSchema, raw, label);
|
|
652
708
|
|
|
653
709
|
// src/config/json-schema.ts
|
|
654
|
-
var
|
|
710
|
+
var renderJsonSchema = (schema) => `${JSON.stringify(z3.toJSONSchema(schema, { io: "input" }), null, 2)}
|
|
655
711
|
`;
|
|
712
|
+
var renderConfigJsonSchema = () => renderJsonSchema(jackherdConfigSchema);
|
|
713
|
+
var renderSourcesJsonSchema = () => renderJsonSchema(personalSourcesSchema);
|
|
656
714
|
export {
|
|
657
715
|
COMMAND_KEYS,
|
|
658
716
|
DEFAULT_CONTEXT_WINDOW,
|
|
@@ -667,6 +725,9 @@ export {
|
|
|
667
725
|
jackherdConfigSchema,
|
|
668
726
|
parseConfigFile,
|
|
669
727
|
parseJackherdConfig,
|
|
728
|
+
parsePersonalSourcesFile,
|
|
670
729
|
parseSourceDraft,
|
|
671
|
-
|
|
730
|
+
personalSourcesSchema,
|
|
731
|
+
renderConfigJsonSchema,
|
|
732
|
+
renderSourcesJsonSchema
|
|
672
733
|
};
|
package/dist/plan-format.js
CHANGED
|
@@ -15,29 +15,13 @@ import {
|
|
|
15
15
|
classifyStatus,
|
|
16
16
|
isPlanLanguage,
|
|
17
17
|
phaseHeading,
|
|
18
|
-
sectionHeading
|
|
19
|
-
|
|
18
|
+
sectionHeading,
|
|
19
|
+
terminalSafe
|
|
20
|
+
} from "./chunk-4LUFR7RC.js";
|
|
20
21
|
|
|
21
22
|
// src/plan-format/lanes.ts
|
|
22
23
|
import { posix } from "path";
|
|
23
24
|
|
|
24
|
-
// src/plan-format/terminal-text.ts
|
|
25
|
-
var UNSAFE_RANGES = [
|
|
26
|
-
[0, 31],
|
|
27
|
-
[127, 159],
|
|
28
|
-
[1564, 1564],
|
|
29
|
-
[8206, 8207],
|
|
30
|
-
[8232, 8233],
|
|
31
|
-
[8234, 8238],
|
|
32
|
-
[8294, 8297]
|
|
33
|
-
];
|
|
34
|
-
var escapeCodePoint = (codePoint) => `\\u${codePoint.toString(16).padStart(4, "0")}`;
|
|
35
|
-
var UNSAFE_TERMINAL_TEXT = new RegExp(
|
|
36
|
-
`[${UNSAFE_RANGES.map(([from, to]) => `${escapeCodePoint(from)}-${escapeCodePoint(to)}`).join("")}]`,
|
|
37
|
-
"g"
|
|
38
|
-
);
|
|
39
|
-
var terminalSafe = (text) => text.replace(UNSAFE_TERMINAL_TEXT, "");
|
|
40
|
-
|
|
41
25
|
// src/plan-format/markdown.ts
|
|
42
26
|
var SECTION_END = /^##\s/;
|
|
43
27
|
var TABLE_SEPARATOR = /^\|[\s|:-]+\|$/;
|
package/install.sh
CHANGED
|
@@ -9,11 +9,14 @@ 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.15.
|
|
12
|
+
JACKHERD_VERSION="${JACKHERD_VERSION:-1.15.2}"
|
|
13
13
|
|
|
14
14
|
JACKHERD_PACKAGE='jackherd'
|
|
15
15
|
# Test seam: the terminal the wizard asks its questions on.
|
|
16
16
|
JACKHERD_TEST_TTY="${JACKHERD_TEST_TTY:-/dev/tty}"
|
|
17
|
+
# Test seam: a packed tarball (absolute path) installed instead of the published package, so CI can prove a build
|
|
18
|
+
# before it is released. Unset in every real run.
|
|
19
|
+
JACKHERD_TEST_PACKAGE="${JACKHERD_TEST_PACKAGE:-}"
|
|
17
20
|
BUN_INSTALLER_URL='https://bun.com/install'
|
|
18
21
|
BUN_MIN_MAJOR=1
|
|
19
22
|
BUN_MIN_MINOR=3
|
|
@@ -55,10 +58,29 @@ bun_bin_dir() {
|
|
|
55
58
|
printf '%s/bin\n' "${BUN_INSTALL:-$HOME/.bun}"
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
# The first executable bun along PATH, spelled the way PATH names it. Walked here and not asked of
|
|
62
|
+
# 'command -v', which some shells (bash 3.2 as sh on macOS) answer with an absolute path even for a
|
|
63
|
+
# relative entry, so the check for a relative one in main would never fire. An empty entry means the
|
|
64
|
+
# current directory and counts as relative.
|
|
65
|
+
bun_on_path() {
|
|
66
|
+
rest="$PATH:"
|
|
67
|
+
while [ -n "$rest" ]; do
|
|
68
|
+
entry=${rest%%:*}
|
|
69
|
+
rest=${rest#*:}
|
|
70
|
+
[ -n "$entry" ] || entry=.
|
|
71
|
+
if [ -f "$entry/bun" ] && [ -x "$entry/bun" ]; then
|
|
72
|
+
printf '%s/bun\n' "$entry"
|
|
73
|
+
return 0
|
|
74
|
+
fi
|
|
75
|
+
done
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
# The bun on PATH, else the one the official installer leaves in ~/.bun/bin; empty when there is none.
|
|
59
80
|
find_bun() {
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
found=$(bun_on_path) || found=''
|
|
82
|
+
if [ -n "$found" ]; then
|
|
83
|
+
printf '%s\n' "$found"
|
|
62
84
|
elif [ -x "$(bun_bin_dir)/bun" ]; then
|
|
63
85
|
printf '%s\n' "$(bun_bin_dir)/bun"
|
|
64
86
|
fi
|
|
@@ -111,10 +133,32 @@ terminal_available() {
|
|
|
111
133
|
(: <"$JACKHERD_TEST_TTY") 2>/dev/null
|
|
112
134
|
}
|
|
113
135
|
|
|
136
|
+
# The controlling terminal's own device node, else the path as given. macOS kqueue, which Bun reads stdin through,
|
|
137
|
+
# rejects /dev/tty with EINVAL but accepts the real device (/dev/ttys003). On macOS 'tty </dev/tty' prints
|
|
138
|
+
# /dev/tty itself, so the device comes from 'tty' on the inherited stdin when that is a terminal, else from the
|
|
139
|
+
# shell's own TTY column of 'ps' (ttys003 on macOS, pts/3 on Linux; ? or ?? when there is none).
|
|
140
|
+
terminal_device() {
|
|
141
|
+
device=$(tty 2>/dev/null) || device=''
|
|
142
|
+
if [ "$device" = /dev/tty ] || [ ! -c "$device" ]; then
|
|
143
|
+
name=$(ps -o tty= -p $$ 2>/dev/null) || name=''
|
|
144
|
+
# shellcheck disable=SC2086 # word splitting trims the padding ps puts around the column
|
|
145
|
+
set -- $name
|
|
146
|
+
case ${1:-} in
|
|
147
|
+
'' | '?' | '??') device='' ;;
|
|
148
|
+
*) device="/dev/$1" ;;
|
|
149
|
+
esac
|
|
150
|
+
fi
|
|
151
|
+
if [ -c "$device" ] && [ "$device" != /dev/tty ]; then
|
|
152
|
+
printf '%s\n' "$device"
|
|
153
|
+
else
|
|
154
|
+
printf '%s\n' "$JACKHERD_TEST_TTY"
|
|
155
|
+
fi
|
|
156
|
+
}
|
|
157
|
+
|
|
114
158
|
run_setup() {
|
|
115
159
|
if terminal_available; then
|
|
116
160
|
say "starting the setup: jh init --global"
|
|
117
|
-
"$1" init --global <"$
|
|
161
|
+
"$1" init --global <"$(terminal_device)" || fail "jh init --global failed"
|
|
118
162
|
else
|
|
119
163
|
say "no terminal to ask questions on, setting up with the defaults: jh init --global --yes"
|
|
120
164
|
"$1" init --global --yes </dev/null || fail "jh init --global --yes failed"
|
|
@@ -145,6 +189,7 @@ main() {
|
|
|
145
189
|
export PATH
|
|
146
190
|
|
|
147
191
|
spec="$JACKHERD_PACKAGE@$JACKHERD_VERSION"
|
|
192
|
+
[ -z "$JACKHERD_TEST_PACKAGE" ] || spec="$JACKHERD_TEST_PACKAGE"
|
|
148
193
|
say "installing $spec: bun add -g --ignore-scripts $spec"
|
|
149
194
|
"$bun" add -g --ignore-scripts "$spec" || fail "bun add -g failed"
|
|
150
195
|
|
|
@@ -194,6 +194,8 @@ const UP_TO_LAST_EXPANSION = /^[\s\S]*\$(\{[^}]*\}|\w+)?/
|
|
|
194
194
|
/** A file name with nothing the shell expands: no brace, glob, quote or closing brace of a default value. */
|
|
195
195
|
const PLAIN_FILE_NAME = /^[\w.+-]+$/
|
|
196
196
|
const ENV = 'env'
|
|
197
|
+
/** The test builtin's brackets: as a program word they name `test`, not a glob pattern. */
|
|
198
|
+
const TEST_BRACKETS = new Set(['[', '[['])
|
|
197
199
|
|
|
198
200
|
/** A word that is nothing but expansions: it may become any number of words, options among them. */
|
|
199
201
|
const isOpaque = ({ value }: Word): boolean => isExpanded(value) && value.replace(EXPANSION_IN_VALUE, '') === ''
|
|
@@ -374,7 +376,7 @@ const isExpandedProgram = (words: SimpleCommand, index: number): boolean => {
|
|
|
374
376
|
return (
|
|
375
377
|
!isOptionNamingNoProgram(words, index) &&
|
|
376
378
|
!hasFixedName(word) &&
|
|
377
|
-
(isExpanded(word.value) || (word.literal && GLOB.test(word.value)))
|
|
379
|
+
(isExpanded(word.value) || (word.literal && GLOB.test(word.value) && !TEST_BRACKETS.has(word.value)))
|
|
378
380
|
)
|
|
379
381
|
}
|
|
380
382
|
|
|
@@ -8,7 +8,7 @@ How an agent opens a pull request so every PR reads consistently and is review-
|
|
|
8
8
|
- **Base = `{{kit.baseBranch}}`** unless the user names another.
|
|
9
9
|
- **Work on a branch.** If the working branch is `{{kit.baseBranch}}`, branch first.
|
|
10
10
|
- **Push before creating, with the user's go-ahead for the push.** The head branch must be on the remote so `gh pr create` has a target.
|
|
11
|
-
- **Drafts
|
|
11
|
+
- **Drafts** (`merge.openAsDraft`: **{{kit.merge.openAsDraft}}**). When `on`, open every PR with `gh pr create --draft`; run `gh pr ready <n>` before handing it to the user to merge, or before waiting on its checks for your own `gh pr merge`.
|
|
12
12
|
- **Check for an existing PR first** (`gh pr view <branch>`). If a prior PR for the same branch is already **merged**, the new one is a continuation — mark it in the title.
|
|
13
13
|
|
|
14
14
|
## Title
|
package/package.json
CHANGED
|
@@ -633,6 +633,18 @@
|
|
|
633
633
|
"type": "string",
|
|
634
634
|
"minLength": 1
|
|
635
635
|
}
|
|
636
|
+
},
|
|
637
|
+
"secrets": {
|
|
638
|
+
"maxItems": 10,
|
|
639
|
+
"type": "array",
|
|
640
|
+
"items": {
|
|
641
|
+
"type": "string",
|
|
642
|
+
"pattern": "^[A-Z][A-Z0-9_]*$"
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
"show": {
|
|
646
|
+
"type": "string",
|
|
647
|
+
"const": "entries"
|
|
636
648
|
}
|
|
637
649
|
},
|
|
638
650
|
"required": [
|