jackherd 1.15.1 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +22 -19
- 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 +17 -2
- package/kit/workflow/skills/follow/SKILL.md +18 -10
- package/kit/workflow/skills/orchestrate/SKILL.md +9 -6
- package/package.json +2 -1
- package/recipes/README.md +3 -2
- package/recipes/google-calendar.md +30 -0
- package/recipes/sources/google-calendar-today.ts +32 -0
- package/recipes/sources/google-calendar.ts +368 -0
- package/recipes/sources.md +15 -3
- package/schemas/jackherd.schema.json +12 -0
- package/schemas/sources.schema.json +141 -0
- package/src/check/skill-budget-defaults.ts +11 -4
- 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/layout.ts +135 -18
- package/src/plans/live.ts +54 -7
- package/src/plans/row-actions.ts +17 -10
- package/src/plans/settings.ts +20 -7
- package/src/plans/snapshot.ts +34 -22
- package/src/plans/source-rows.ts +35 -2
- 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/lane-report.ts +2 -1
- 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 +20 -3
- 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/texts.ts +1 -1
- 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,28 @@
|
|
|
2
2
|
|
|
3
3
|
What changed for people who use jackherd, newest first.
|
|
4
4
|
|
|
5
|
+
## 1.16.0
|
|
6
|
+
|
|
7
|
+
- **Sources of your own.** A source that belongs to you alone, such as your calendar, goes into `~/.jackherd/sources.json` instead of the repository's config: same shape as `watch.sources`. Its command runs in `~/.jackherd/`, not in the repository, in every repository where you start the watcher, and for nobody else. The watcher, the herd and `jh watch source <name> --dry-run` take it up after the repository's sources. If the repository has a source of the same name, the repository's wins and yours is skipped with a note. A broken personal file never stops the watcher. The file has a JSON schema, `schemas/sources.schema.json`.
|
|
8
|
+
- **Secrets in the keychain: `jh secret`.** A source lists the variables it needs in its `"secrets"` field. `jh secret set <NAME>` asks for the value hidden, from a terminal only, and keeps it in the system keychain; `jh secret list` shows which names your sources need and whether each is set, never a value; `jh secret delete <NAME>` removes one. A repository source, which anyone with a commit can change, gets only the watcher's environment, never a keychain value. A value reaches the environment of its source's command and nowhere else: no argument, output, state or log. A missing one fails the run with `secret missing: <NAME>` before the command starts.
|
|
9
|
+
- **Sources change without a restart.** The watcher reads the repository's config and your personal file again when one of them changed; a new source runs at once, a removed one stops. A changed file that does not read keeps the sources as they were and says so once. The herd takes the sources from the running watcher, so a new one shows there too without restarting either.
|
|
10
|
+
- **Entries in a block of their own.** `"show": "entries"` opens a source's entries in a block under "Your turn", headed by its label, in its colour and linked where an entry has an address. Together such blocks show at most five rows, at least one per block, and `+<n>` opens the rest of a block. The source's status stays in the `Sources` line.
|
|
11
|
+
- **`/jh-follow` asks where a source belongs.** It asks whether the source is yours or the repository's and writes exactly that file. For a personal source with a secret, you set the value with `jh secret set` in a terminal of your own; the skill then checks it with `jh secret list` and dry-runs the source by name.
|
|
12
|
+
- **A recipe for today's appointments.** The Google Calendar recipe shows the day's appointments of one or more calendars from their secret iCal addresses, kept in the keychain, recurring events included, each linked to the calendar's day view. No Google login and no API project. A failed request prints nothing of the address.
|
|
13
|
+
- **`[done]` hides its row at once.** A click on `[done]` in "Your turn" removes the row right away instead of at the next read; if the write fails, the row stays and the footer says why.
|
|
14
|
+
- **A cut activity line opens on a click.** The `↳` line of a working lane or subagent that did not fit unfolds when you click it.
|
|
15
|
+
- **The merge guard says why it asks.** An approved merge passes without a question only as a plain `gh pr merge <n>` standing alone. When it asks anyway, the question now says why: run a chained or piped merge alone, and a merge with other flags is let through only in its plain form.
|
|
16
|
+
|
|
17
|
+
## 1.15.2
|
|
18
|
+
|
|
19
|
+
- **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.
|
|
20
|
+
- **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.
|
|
21
|
+
|
|
22
|
+
- **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.
|
|
23
|
+
- **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.
|
|
24
|
+
- **Every `ps` call is bounded.** A `ps` that hangs no longer holds up the herd or the watcher.
|
|
25
|
+
- **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.
|
|
26
|
+
|
|
5
27
|
## 1.15.1
|
|
6
28
|
|
|
7
29
|
- **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.
|
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.
|
|
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.16.0/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.
|
|
6
|
+
[](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd?activeTab=dependencies) [](https://unpkg.com/jackherd/INSTALL.md) [](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.16.0/assets/icons/space.svg">
|
|
7
7
|
|
|
8
8
|
# Jack herds your coding agents.
|
|
9
9
|
|
|
@@ -15,31 +15,31 @@ For developers who'd rather decide than supervise.
|
|
|
15
15
|
curl -fsSL https://unpkg.com/jackherd/install.sh | sh
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Then `/jh-start` in a repository, and Jack offers your first plan. → [Quickstart](#quickstart)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.
|
|
18
|
+
Then `/jh-start` in a repository, and Jack offers your first plan. → [Quickstart](#quickstart)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.16.0/assets/icons/space.svg">
|
|
19
19
|
|
|
20
20
|
<p>
|
|
21
|
-
<img src="https://unpkg.com/jackherd@1.
|
|
21
|
+
<img src="https://unpkg.com/jackherd@1.16.0/docs/workflow-dark.svg" width="720" alt="jackherd in herdr: a lane hits a question, Jack asks you as a choice, you pick, and the lane works on while the herd view counts its progress.">
|
|
22
22
|
<br>
|
|
23
|
-
<sub>Jack and his lanes in <a href="https://herdr.dev/">herdr</a>, a terminal for coding agents (recommended). Without it, one Claude Code session builds with subagents.</sub><br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.
|
|
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.16.0/assets/icons/space.svg">
|
|
24
24
|
</p>
|
|
25
25
|
|
|
26
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
26
|
+
<p><img src="https://unpkg.com/jackherd@1.16.0/assets/icons/lanes.svg" width="24" height="34" align="middle" alt=""> <b>Parallel where it pays.</b><br>Phases that don't collide run side by side, each lane on its own branch and worktree. Jack keeps the merge order and has the others rebase.</p>
|
|
27
27
|
|
|
28
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
28
|
+
<p><img src="https://unpkg.com/jackherd@1.16.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>
|
|
29
29
|
|
|
30
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
30
|
+
<p><img src="https://unpkg.com/jackherd@1.16.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>
|
|
31
31
|
|
|
32
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
32
|
+
<p><img src="https://unpkg.com/jackherd@1.16.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>
|
|
33
33
|
|
|
34
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
34
|
+
<p><img src="https://unpkg.com/jackherd@1.16.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>
|
|
35
35
|
|
|
36
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
36
|
+
<p><img src="https://unpkg.com/jackherd@1.16.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>
|
|
37
37
|
|
|
38
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
38
|
+
<p><img src="https://unpkg.com/jackherd@1.16.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.16.0/assets/icons/space.svg"></p>
|
|
39
39
|
|
|
40
40
|
## Quickstart
|
|
41
41
|
|
|
42
|
-
1. **Install** (
|
|
42
|
+
1. **Install** (needs Claude Code and git). → [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md)
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
curl -fsSL https://unpkg.com/jackherd/install.sh | sh
|
|
@@ -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.
|
|
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.16.0/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.
|
|
80
|
+
<img src="https://unpkg.com/jackherd@1.16.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.">
|
|
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.
|
|
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.16.0/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.
|
|
89
|
+
<img src="https://unpkg.com/jackherd@1.16.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.">
|
|
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.
|
|
124
|
+
<img src="https://unpkg.com/jackherd@1.16.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.">
|
|
125
125
|
|
|
126
126
|
| Skill | Does |
|
|
127
127
|
| --- | --- |
|
|
@@ -203,7 +203,9 @@ How the herd reads the board:
|
|
|
203
203
|
|
|
204
204
|
Jack doesn't watch the terminals himself. `jh watch`, which he starts, wakes him when a lane is blocked or done, a check turns red, a pull request gets feedback, a lane's context fills up, a lane waits for an approval or works for ten minutes without reporting its task; deploys and previews too, when configured.
|
|
205
205
|
|
|
206
|
-
`/jh-follow PROJ-123` adds anything a command can print as JSON: a ticket, an issue, error logs, a status page. It checks the tool and its login, shows you a dry run and writes the source to `watch.sources` after your yes. A token stays in the environment, read by a small Bun script, never in an argument. Each source runs on its own interval, kept across a restart of the watcher. Recipes for Jira, Linear, GitHub issues and Projects, Sentry
|
|
206
|
+
`/jh-follow PROJ-123` adds anything a command can print as JSON: a ticket, an issue, error logs, a status page. It checks the tool and its login, shows you a dry run and writes the source to `watch.sources` after your yes. A token stays in the environment, read by a small Bun script, never in an argument. Each source runs on its own interval, kept across a restart of the watcher. Recipes for Jira, Linear, GitHub issues and Projects, Sentry, status pages and today's Google Calendar appointments come with it. The reports need herdr; a source can be set up and dry-run without it.
|
|
207
|
+
|
|
208
|
+
A source that is yours alone, such as your calendar, goes into `~/.jackherd/sources.json` instead: it runs in every repository where you start the watcher, and for nobody else. Its secrets live in the system keychain: `jh secret set <NAME>` asks for the value hidden, and only the command of the source that lists it receives it. The watcher takes up a changed source without a restart, and any source can show its entries in a block of their own under Your turn with `"show": "entries"`.
|
|
207
209
|
|
|
208
210
|
</details>
|
|
209
211
|
|
|
@@ -225,6 +227,7 @@ Each phase in the plan names its model: the planning model (Opus by default) whe
|
|
|
225
227
|
| `jh check` | types, lint and tests of what you touched, plus the plan and board checks (`--skip-slow`, `--parallel <plan>`) |
|
|
226
228
|
| `jh herd` | the live view (`--once`, `--lane <name>`) |
|
|
227
229
|
| `jh watch` | the watcher Jack runs |
|
|
230
|
+
| `jh secret` | keeps a personal source's secrets in the system keychain (`set <NAME>`, `list`, `delete <NAME>`); `list` shows which are set, never a value |
|
|
228
231
|
| `jh lane prompt <lane>` | writes a lane's first prompt from its board row and its plan (`--out`, `--handover`); your additions below the marker line at its end stay |
|
|
229
232
|
| `jh lane close <lane>` | removes a finished lane: agent, worktree, herdr workspace and branch, and marks the board; first checks that nothing is lost, and refuses with every reason; `--dry-run` only checks. Claude Code asks before each call |
|
|
230
233
|
| `jh preview <file>` | renders a Markdown file through the GitHub API into a preview in GitHub and npm style |
|
|
@@ -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.
|
|
12
|
+
JACKHERD_VERSION="${JACKHERD_VERSION:-1.16.0}"
|
|
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
|
|
|
@@ -46,7 +46,8 @@ export type GuardPolicy = {
|
|
|
46
46
|
* A guarded command the script runs or could run, and the pull request a merge names, when it names one plainly; or
|
|
47
47
|
* the shell alias or function that may run one (`definition`, whose name is then the `command`).
|
|
48
48
|
*/
|
|
49
|
-
|
|
49
|
+
/** A guarded command found in the script; `sole` where it was the one command of the call, read as written by `gh`. */
|
|
50
|
+
type Finding = { command: string; merge: PlainMerge | undefined; definition?: ShellDefinitionHit; sole?: boolean }
|
|
50
51
|
|
|
51
52
|
/** What the search looks for: the guarded commands, and the shell definitions that may stand for one of them. */
|
|
52
53
|
type Search = { commands: readonly string[]; definitions: () => ShellDefinitions }
|
|
@@ -326,7 +327,7 @@ const findingOf = (command: string, words: SimpleCommand, index: number, sole: b
|
|
|
326
327
|
if (!namesCommandWords(words, index, command) || isReading(words, index, command)) return undefined
|
|
327
328
|
if (!isGuardedUse(command, words, index)) return undefined
|
|
328
329
|
const plain = sole && words[index].literal && words[index].value === GH
|
|
329
|
-
return { command, merge: plain ? plainMerge(words.slice(index), commandWords(command)) : undefined }
|
|
330
|
+
return { command, sole: plain, merge: plain ? plainMerge(words.slice(index), commandWords(command)) : undefined }
|
|
330
331
|
}
|
|
331
332
|
|
|
332
333
|
const programsOf = (commands: readonly string[]): string[] => [
|
|
@@ -447,6 +448,19 @@ const guardedListSentence = (commands: string[]): string[] =>
|
|
|
447
448
|
`${commands.map((command) => `\`${command}\``).join(' and ')} is on the project's guarded list (guard.commands in .jackherd/config.json), so the user confirms it.`,
|
|
448
449
|
]
|
|
449
450
|
|
|
451
|
+
const SOLE_MERGE_SENTENCE = `A \`${MERGE_COMMAND}\` is checked against the approval and the checks only as a plain \`${MERGE_COMMAND} <number>\` that is the one command of its Bash call: no pipe, \`;\`, \`&&\`, wrapper or substitution around it. Run it alone, then read the result with \`gh pr view\`.`
|
|
452
|
+
|
|
453
|
+
const PLAIN_MERGE_SENTENCE = `A \`${MERGE_COMMAND}\` is checked against the approval and the checks only in its plain form: the number, merge-method and delete-branch flags and at most one \`--match-head-commit <full sha>\`; any other flag or a word the shell expands is confirmed by the user.`
|
|
454
|
+
|
|
455
|
+
/** Why an unchecked merge was not checked: it did not stand alone in its call, or it did but not in its plain form. */
|
|
456
|
+
const soleMergeSentence = (findings: Finding[]): string[] => {
|
|
457
|
+
const unchecked = findings.filter(
|
|
458
|
+
({ command, merge, definition }) => command === MERGE_COMMAND && merge === undefined && definition === undefined,
|
|
459
|
+
)
|
|
460
|
+
if (unchecked.some(({ sole }) => !sole)) return [SOLE_MERGE_SENTENCE]
|
|
461
|
+
return unchecked.length > 0 ? [PLAIN_MERGE_SENTENCE] : []
|
|
462
|
+
}
|
|
463
|
+
|
|
450
464
|
const askBecause = (findings: Finding[], notes: string[], script: string): MergeGuardDecision => {
|
|
451
465
|
const guarded = findings.filter(({ definition }) => definition === undefined).map(({ command }) => command)
|
|
452
466
|
const definitions = [
|
|
@@ -462,6 +476,7 @@ const askBecause = (findings: Finding[], notes: string[], script: string): Merge
|
|
|
462
476
|
(definition) =>
|
|
463
477
|
`${shellDefinitionHint(definition)} It may run a command of guard.commands in .jackherd/config.json, so the user confirms it.`,
|
|
464
478
|
),
|
|
479
|
+
...soleMergeSentence(findings),
|
|
465
480
|
...notes,
|
|
466
481
|
expandingHereDocumentAdvice(script),
|
|
467
482
|
]
|