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.
Files changed (41) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +17 -17
  3. package/dist/{chunk-V4HOLGFP.js → chunk-4LUFR7RC.js} +19 -1
  4. package/dist/config.d.ts +47 -2
  5. package/dist/config.js +65 -4
  6. package/dist/plan-format.js +3 -19
  7. package/herdr-plugin/herdr-plugin.toml +1 -1
  8. package/install.sh +49 -4
  9. package/kit/hooks/hooks/merge-guard/decision.ts +3 -1
  10. package/kit/rules/rules/kit/pull-requests.md +1 -1
  11. package/package.json +1 -1
  12. package/schemas/jackherd.schema.json +12 -0
  13. package/schemas/sources.schema.json +141 -0
  14. package/src/cli.ts +1 -0
  15. package/src/config/json-schema.ts +7 -3
  16. package/src/config/location.ts +7 -0
  17. package/src/config/personal-sources.ts +83 -0
  18. package/src/config/schema.ts +29 -0
  19. package/src/plans/herd-claim.ts +9 -31
  20. package/src/plans/settings.ts +20 -7
  21. package/src/plans/snapshot.ts +26 -19
  22. package/src/secrets/command.ts +163 -0
  23. package/src/secrets/environment.ts +45 -0
  24. package/src/secrets/hidden-input.ts +51 -0
  25. package/src/secrets/listing.ts +63 -0
  26. package/src/secrets/options.ts +45 -0
  27. package/src/secrets/store.ts +74 -0
  28. package/src/shared/process-info.ts +89 -0
  29. package/src/sync/command-context.ts +26 -4
  30. package/src/sync/render.ts +9 -4
  31. package/src/watch/command.ts +44 -12
  32. package/src/watch/plan-badges-read.ts +28 -10
  33. package/src/watch/source-dry-run-command.ts +23 -12
  34. package/src/watch/source-dry-run.ts +19 -2
  35. package/src/watch/source-reload.ts +100 -0
  36. package/src/watch/source-run.ts +63 -7
  37. package/src/watch/source-tickets.ts +15 -0
  38. package/src/watch/sources.ts +21 -0
  39. package/src/watch/watch-events.ts +9 -0
  40. package/src/watch/watched-source.ts +39 -0
  41. 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.0/assets/branding/jackherd-logo-outline.svg" width="340">
2
+ <img alt="jackherd: a black and white Shih Tzu in a turquoise bandana, three lanes running from him to three agent nodes" src="https://unpkg.com/jackherd@1.15.2/assets/branding/jackherd-logo-outline.svg" width="340">
3
3
  <br>
4
4
  </p>
5
5
 
6
- [![npm](https://img.shields.io/npm/v/jackherd)](https://www.npmjs.com/package/jackherd) [![license: MIT](https://img.shields.io/npm/l/jackherd)](https://www.npmjs.com/package/jackherd) [![2 dependencies](https://img.shields.io/badge/dependencies-2-brightgreen)](https://www.npmjs.com/package/jackherd?activeTab=dependencies)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.0/assets/icons/space.svg">
6
+ [![npm](https://img.shields.io/npm/v/jackherd)](https://www.npmjs.com/package/jackherd) [![license: MIT](https://img.shields.io/npm/l/jackherd)](https://www.npmjs.com/package/jackherd) [![2 dependencies](https://img.shields.io/badge/dependencies-2-brightgreen)](https://www.npmjs.com/package/jackherd?activeTab=dependencies)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.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.0/assets/icons/space.svg">
18
+ Then `/jh-start` in a repository, and Jack offers your first plan. → [Quickstart](#quickstart)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.2/assets/icons/space.svg">
19
19
 
20
20
  <p>
21
- <img src="https://unpkg.com/jackherd@1.15.0/docs/workflow-dark.svg" width="720" alt="jackherd in herdr: a lane hits a question, Jack asks you as a choice, you pick, and the lane works on while the herd view counts its progress.">
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.0/assets/icons/space.svg">
23
+ <sub>Jack and his lanes in <a href="https://herdr.dev/">herdr</a>, a terminal for coding agents (recommended). Without it, one Claude Code session builds with subagents.</sub><br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.2/assets/icons/space.svg">
24
24
  </p>
25
25
 
26
- <p><img src="https://unpkg.com/jackherd@1.15.0/assets/icons/lanes.svg" width="24" height="34" align="middle" alt=""> <b>Parallel where it pays.</b><br>Phases that don't collide run side by side, each lane on its own branch and worktree. Jack keeps the merge order and has the others rebase.</p>
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.0/assets/icons/decide.svg" width="24" height="34" align="middle" alt=""> <b>You make the calls.</b><br>Nothing is committed, pushed or merged without your approval. Jack merges on green checks only where you said so: one pull request, or a plan's for up to 24 hours.</p>
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.0/assets/icons/bark.svg" width="24" height="34" align="middle" alt=""> <b>Jack barks.</b><br>A blocked lane, a red check, a review comment: Jack wakes, and the herd view shows where every lane stands.</p>
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.0/assets/icons/skills.svg" width="24" height="34" align="middle" alt=""> <b>Works with your skills.</b><br>Keep your own skills: a lane builds with whichever one you pick. Plans written elsewhere run too, once Jack fills in what they're missing.</p>
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.0/assets/icons/ear.svg" width="24" height="34" align="middle" alt=""> <b>Jack listens.</b><br>Review comments go to the lane that fixes them, with a test. <code>/jh-follow</code> brings anything with a CLI or an API into the herd: tickets, error logs, status pages, deploys, and whatever else you point it at.</p>
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.0/assets/icons/bowl.svg" width="24" height="34" align="middle" alt=""> <b>Picks up where you left off.</b><br>Plan and board hold the state, not the chat.</p>
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.0/assets/icons/plan.svg" width="24" height="34" align="middle" alt=""> <b>Lives in your repository.</b><br>Plans, board and every decision are Markdown, reviewed like code. <code>jh init</code> gives your team the same setup.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.0/assets/icons/space.svg"></p>
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.0/assets/icons/space.svg">
74
+ By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks. → [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.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.0/docs/follow-dark.svg" width="720" alt="A source (ticket, error log or status page) has a new or changed entry; jh watch reports it to Jack; Jack puts a row into Your turn, prefixed with the source's label; its [ask] button makes Jack ask you, and then a lane works it.">
80
+ <img src="https://unpkg.com/jackherd@1.15.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.0/assets/icons/space.svg">
82
+ The herd sums up your sources in one `Sources` line: per source its new entries, or when it last ran, red while it fails; opened, a click on an entry opens it. A ticket shows on the lane that works it, and a row in Your turn that came from a source starts with that source's label.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.15.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.0/docs/lanes-dark.svg" alt="Jack prompts two lanes and asks you for decisions and approvals; the lanes send questions back as text; jh watch reads lane state, pull requests and checks and wakes Jack.">
89
+ <img src="https://unpkg.com/jackherd@1.15.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.0/docs/skills-dark.svg" alt="Your own plan or an issue is adopted (Jack adds what is missing, you see the diff) and goes to /jh-orchestrate; /jh-grill hands decisions to /jh-plan; /jh-plan feeds /jh-orchestrate or a single /jh-implement, which your own skill can replace; /jh-implement ends in /jh-pre-commit, which runs the reviewers; /jh-diagnose hands a red command to /jh-implement.">
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-V4HOLGFP.js";
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 renderConfigJsonSchema = () => `${JSON.stringify(z3.toJSONSchema(jackherdConfigSchema, { io: "input" }), null, 2)}
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
- renderConfigJsonSchema
730
+ personalSourcesSchema,
731
+ renderConfigJsonSchema,
732
+ renderSourcesJsonSchema
672
733
  };
@@ -15,29 +15,13 @@ import {
15
15
  classifyStatus,
16
16
  isPlanLanguage,
17
17
  phaseHeading,
18
- sectionHeading
19
- } from "./chunk-V4HOLGFP.js";
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|:-]+\|$/;
@@ -1,6 +1,6 @@
1
1
  id = "crossdelta.jackherd"
2
2
  name = "jackherd"
3
- version = "1.15.0"
3
+ version = "1.15.2"
4
4
  min_herdr_version = "0.8.2"
5
5
  description = "Show the herd, the state of a board's lanes, in a herdr pane"
6
6
  platforms = ["linux", "macos"]
package/install.sh CHANGED
@@ -9,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.0}"
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
- if command -v bun >/dev/null 2>&1; then
61
- command -v bun
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 <"$JACKHERD_TEST_TTY" || fail "jh init --global failed"
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:** Opening as a draft is **{{kit.merge.openAsDraft}}** (`merge.openAsDraft`); `on` opens every PR with `gh pr create --draft`; you turn it ready (`gh pr ready <n>`) once it goes to merge.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jackherd",
3
- "version": "1.15.0",
3
+ "version": "1.15.2",
4
4
  "keywords": [
5
5
  "claude-code",
6
6
  "coding-agents",
@@ -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": [