jackherd 1.26.2 → 1.26.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +18 -18
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -0
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/workflow/skills/orchestrate/references/close-lane.md +8 -8
- package/package.json +1 -1
- package/schemas/jackherd.schema.json +8 -0
- package/src/config/schema.ts +2 -0
- package/src/lane/close-check.ts +51 -15
- package/src/lane/close-command.ts +36 -5
- package/src/lane/close-survey.ts +9 -0
- package/src/lane/close.ts +53 -5
- package/src/plans/settings.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
What changed for people who use jackherd, newest first.
|
|
4
4
|
|
|
5
|
+
## 1.26.3
|
|
6
|
+
|
|
7
|
+
- **`jh lane close` no longer refuses a lane for files a project deletes on every close.** `lanes.disposable` in `.jackherd/config.json` lists ignored paths in `.gitignore` syntax (scratch folders, stored test logins, generated assets) that go with the worktree without a refusal; any other ignored file still refuses.
|
|
8
|
+
- **`jh lane close` archives the lane's handover files instead of refusing.** Before the worktree is removed, `.claude/handover/` is copied to `.claude/handover/closed/<lane>-<date>/` in the main checkout; when the copy fails, nothing is removed. The orchestrate skill's `close-lane.md` says so. Run `jh sync` to get the text.
|
|
9
|
+
- **`jh lane close` deletes the lane's branch on origin when nothing was pushed after the merge.** The branch goes only while it still points to the merged pull request's head, as the last step so a failed push leaves the rest done; otherwise one line says why it stays. `jh lane close <lane> --branch <branch>` finishes it for a lane closed before.
|
|
10
|
+
|
|
5
11
|
## 1.26.2
|
|
6
12
|
|
|
7
13
|
- **A board lane is placed by its agent's worktree.** The herd and the watcher find a lane by the worktree its agent runs in, not only by the lane's name.
|
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.26.
|
|
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.26.3/assets/branding/jackherd-logo-outline.svg" width="340">
|
|
3
3
|
<br>
|
|
4
4
|
</p>
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd?activeTab=dependencies) [](https://unpkg.com/jackherd/INSTALL.md) [](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.26.
|
|
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.26.3/assets/icons/space.svg">
|
|
7
7
|
|
|
8
8
|
# Jack herds your coding agents.
|
|
9
9
|
|
|
@@ -15,29 +15,29 @@ 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.26.
|
|
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.26.3/assets/icons/space.svg">
|
|
19
19
|
|
|
20
20
|
<p>
|
|
21
|
-
<img src="https://unpkg.com/jackherd@1.26.
|
|
21
|
+
<img src="https://unpkg.com/jackherd@1.26.3/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.26.
|
|
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.26.3/assets/icons/space.svg">
|
|
24
24
|
</p>
|
|
25
25
|
|
|
26
|
-
<p><img src="https://unpkg.com/jackherd@1.26.
|
|
26
|
+
<p><img src="https://unpkg.com/jackherd@1.26.3/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.26.
|
|
28
|
+
<p><img src="https://unpkg.com/jackherd@1.26.3/assets/icons/decide.svg" width="24" height="34" align="middle" alt=""> <b>You make the calls.</b><br>Commits, pushes and merges wait for your approval. Jack merges on green checks only where you said so: one pull request, or a plan's for up to 24 hours. The guards catch mistakes, not intent; a hard boundary takes branch protection.</p>
|
|
29
29
|
|
|
30
|
-
<p><img src="https://unpkg.com/jackherd@1.26.
|
|
30
|
+
<p><img src="https://unpkg.com/jackherd@1.26.3/assets/icons/bark.svg" width="24" height="34" align="middle" alt=""> <b>Jack barks.</b><br>A lane gets stuck, a check goes red, a decision needs you: Jack steps in and brings you only what's yours to decide. The herd view shows where every lane stands.</p>
|
|
31
31
|
|
|
32
|
-
<p><img src="https://unpkg.com/jackherd@1.26.
|
|
32
|
+
<p><img src="https://unpkg.com/jackherd@1.26.3/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.26.
|
|
34
|
+
<p><img src="https://unpkg.com/jackherd@1.26.3/assets/icons/ear.svg" width="24" height="34" align="middle" alt=""> <b>Jack listens.</b><br>Review comments on a lane's pull request go back to that lane, which fixes them with a test. <code>/jh-follow</code> brings in anything a CLI or an API can read as JSON: tickets, error logs, status pages.</p>
|
|
35
35
|
|
|
36
|
-
<p><img src="https://unpkg.com/jackherd@1.26.
|
|
36
|
+
<p><img src="https://unpkg.com/jackherd@1.26.3/assets/icons/model.svg" width="24" height="34" align="middle" alt=""> <b>The right model for each phase.</b><br>Every phase names its model: the stronger one where a mistake is expensive (data, security, a migration), the cheaper one for the rest. Lanes start on it, and a hook refuses to start a phase's subagent on a weaker one.</p>
|
|
37
37
|
|
|
38
|
-
<p><img src="https://unpkg.com/jackherd@1.26.
|
|
38
|
+
<p><img src="https://unpkg.com/jackherd@1.26.3/assets/icons/bowl.svg" width="24" height="34" align="middle" alt=""> <b>A crash or a full context doesn't cost you the work.</b><br>Plan, board and a hand-over file hold the state, not the chat: the next session picks up where the last one stopped.</p>
|
|
39
39
|
|
|
40
|
-
<p><img src="https://unpkg.com/jackherd@1.26.
|
|
40
|
+
<p><img src="https://unpkg.com/jackherd@1.26.3/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.26.3/assets/icons/space.svg"></p>
|
|
41
41
|
|
|
42
42
|
## Quickstart
|
|
43
43
|
|
|
@@ -68,22 +68,22 @@ Then `/jh-start` in a repository, and Jack offers your first plan. → [Quicksta
|
|
|
68
68
|
/jh-orchestrate docs/plans/board-<name>/board.md
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Linux and macOS are supported (Windows through WSL); the requirements are in [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md). By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.26.
|
|
71
|
+
Linux and macOS are supported (Windows through WSL); the requirements are in [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md). By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.26.3/assets/icons/space.svg">
|
|
72
72
|
|
|
73
73
|
## Follow anything you can query
|
|
74
74
|
|
|
75
75
|
Tickets, error logs, status pages: if a command or an API call can read it as JSON with your login, Jack can watch it. Recipes cover Jira, Linear, GitHub issues and Projects, Sentry, Google Calendar and status pages; for anything else, `/jh-follow` writes the command with you. One yes, and every change shows up like this:
|
|
76
76
|
|
|
77
|
-
<img src="https://unpkg.com/jackherd@1.26.
|
|
77
|
+
<img src="https://unpkg.com/jackherd@1.26.3/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.">
|
|
78
78
|
|
|
79
|
-
The herd sums up your sources in one `Sources` line: new entries per source, or its last run, red while it fails; opened, a click opens an entry. One that needs you lands in Your turn under the source's label, [ask] has Jack look at it, and a ticket shows on the lane that works it.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.26.
|
|
79
|
+
The herd sums up your sources in one `Sources` line: new entries per source, or its last run, red while it fails; opened, a click opens an entry. One that needs you lands in Your turn under the source's label, [ask] has Jack look at it, and a ticket shows on the lane that works it.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.26.3/assets/icons/space.svg">
|
|
80
80
|
|
|
81
81
|
## In detail
|
|
82
82
|
|
|
83
83
|
<details>
|
|
84
84
|
<summary><b>What Jack does on his own, and what waits for you</b></summary>
|
|
85
85
|
|
|
86
|
-
<img src="https://unpkg.com/jackherd@1.26.
|
|
86
|
+
<img src="https://unpkg.com/jackherd@1.26.3/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.">
|
|
87
87
|
|
|
88
88
|
| On his own | After your approval | Never |
|
|
89
89
|
| --- | --- | --- |
|
|
@@ -118,7 +118,7 @@ The guards catch mistakes, not intent; for that, use branch protection. The hook
|
|
|
118
118
|
|
|
119
119
|
A **plan** is Markdown with phases, checkbox tasks, a model and a review gate per phase, and under each phase heading one sentence on what is different afterwards and for whom. 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.
|
|
120
120
|
|
|
121
|
-
<img src="https://unpkg.com/jackherd@1.26.
|
|
121
|
+
<img src="https://unpkg.com/jackherd@1.26.3/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.">
|
|
122
122
|
|
|
123
123
|
| Skill | Does |
|
|
124
124
|
| --- | --- |
|
package/dist/config.d.ts
CHANGED
|
@@ -122,6 +122,7 @@ declare const jackherdConfigSchema: z.ZodObject<{
|
|
|
122
122
|
}, z.core.$strict>>;
|
|
123
123
|
lanes: z.ZodPrefault<z.ZodObject<{
|
|
124
124
|
copy: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
125
|
+
disposable: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
125
126
|
lockFile: z.ZodOptional<z.ZodString>;
|
|
126
127
|
minFreeGb: z.ZodOptional<z.ZodNumber>;
|
|
127
128
|
basePort: z.ZodOptional<z.ZodNumber>;
|
package/dist/config.js
CHANGED
|
@@ -747,6 +747,8 @@ var jackherdConfigSchema = z3.strictObject({
|
|
|
747
747
|
lanes: z3.strictObject({
|
|
748
748
|
/** Untracked files copied into every new worktree; empty means the files `.worktreeinclude` lists. */
|
|
749
749
|
copy: z3.array(laneCopyPath).default([]),
|
|
750
|
+
/** Ignored paths `jh lane close` deletes with a worktree without asking, in `.gitignore` syntax (`apps/web/tmp`); a pattern without a slash matches at any depth. Everything else ignored still refuses the close. */
|
|
751
|
+
disposable: z3.array(laneCopyPath).default([]),
|
|
750
752
|
/** The dependency lock file a rebase takes from the base branch and regenerates, never merges by hand; detected from the repository when left out. */
|
|
751
753
|
lockFile: lockFilePath.optional(),
|
|
752
754
|
/** Free gigabytes the disk must hold before `jh lane space` lets a new lane install its dependencies; 5 when left out. */
|
package/install.sh
CHANGED
|
@@ -9,7 +9,7 @@ set -eu
|
|
|
9
9
|
|
|
10
10
|
# The default is the version this file ships with. The release bump changes it together with
|
|
11
11
|
# package.json; install.test.ts fails while the two differ. Overridable, but only by a plain release.
|
|
12
|
-
JACKHERD_VERSION="${JACKHERD_VERSION:-1.26.
|
|
12
|
+
JACKHERD_VERSION="${JACKHERD_VERSION:-1.26.3}"
|
|
13
13
|
|
|
14
14
|
JACKHERD_PACKAGE='jackherd'
|
|
15
15
|
# Test seam: the terminal the wizard asks its questions on.
|
|
@@ -4,16 +4,16 @@ Read when a lane's status line says `plan finished` and names `jh lane close <la
|
|
|
4
4
|
or when the user names a lane to close. Run both commands from the main checkout.
|
|
5
5
|
|
|
6
6
|
1. `jh lane close <lane> --dry-run` checks and lists the steps; it changes nothing. It proves nothing is lost: a
|
|
7
|
-
merged PR whose tree equals the branch (`git diff <branch-head> <merge-commit>` empty
|
|
8
|
-
|
|
7
|
+
merged PR whose tree equals the branch (`git diff <branch-head> <merge-commit>` empty), no file that removing the worktree would delete (dependencies, build output and `lanes.disposable` paths go with it; `.claude/handover/` is
|
|
8
|
+
copied to `.claude/handover/closed/` first), no agent at work in it. A refusal changes
|
|
9
9
|
nothing: resolve each reason it prints (a named file goes to the user first, a working agent is waited for, a lock
|
|
10
|
-
is lifted once its session ended
|
|
11
|
-
A worktree on no branch closes when its head is on the base branch and only staged plan changes
|
|
10
|
+
is lifted once its session ended) and repeat until it exits 0.
|
|
11
|
+
A worktree on no branch closes when its head is on the base branch and only staged plan changes remain.
|
|
12
12
|
Handed-over service: [`backend.md`](backend.md).
|
|
13
|
-
2. `jh lane close <lane>` Run it without asking the user; its checks are the safeguard. It repeats the step 1 checks
|
|
13
|
+
2. `jh lane close <lane>` Run it without asking the user; its checks are the safeguard. It repeats the step 1 checks and runs the listed steps, one line each. After a failure,
|
|
14
14
|
run it again: with the worktree gone and the branch left, `jh lane close <lane> --branch <branch>` finishes the
|
|
15
|
-
branch and the board; a lane closed in full says so
|
|
15
|
+
branch and the board; a lane closed in full says so.
|
|
16
16
|
3. The command leaves the rows marked `{{kit.word.laneClosed}} <date>` and the `{{kit.heading.history}}` row as a
|
|
17
17
|
change in the main checkout's board, which the herd shows at once. It reaches `{{kit.baseBranch}}` bundled (§ 4), the
|
|
18
|
-
marked rows in the same documentation PR as the history entry. Done when the board shows the marks and the history row.
|
|
19
|
-
|
|
18
|
+
marked rows in the same documentation PR as the history entry. Done when the board shows the marks and the history row. The command deletes the branch on origin itself when it still
|
|
19
|
+
holds the merged head; otherwise it says why it stays.
|
package/package.json
CHANGED
|
@@ -147,6 +147,14 @@
|
|
|
147
147
|
"pattern": "^(?![/-])(?!(?:.*\\/)?\\.\\.(?:\\/|$))[^'\\x00-\\x1f\\x7f]+$"
|
|
148
148
|
}
|
|
149
149
|
},
|
|
150
|
+
"disposable": {
|
|
151
|
+
"default": [],
|
|
152
|
+
"type": "array",
|
|
153
|
+
"items": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"pattern": "^(?![/-])(?!(?:.*\\/)?\\.\\.(?:\\/|$))[^'\\x00-\\x1f\\x7f]+$"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
150
158
|
"lockFile": {
|
|
151
159
|
"type": "string",
|
|
152
160
|
"pattern": "^(?![/-])(?!(?:.*\\/)?\\.\\.(?:\\/|$))[A-Za-z0-9._/-]+$"
|
package/src/config/schema.ts
CHANGED
|
@@ -603,6 +603,8 @@ export const jackherdConfigSchema = z.strictObject({
|
|
|
603
603
|
.strictObject({
|
|
604
604
|
/** Untracked files copied into every new worktree; empty means the files `.worktreeinclude` lists. */
|
|
605
605
|
copy: z.array(laneCopyPath).default([]),
|
|
606
|
+
/** Ignored paths `jh lane close` deletes with a worktree without asking, in `.gitignore` syntax (`apps/web/tmp`); a pattern without a slash matches at any depth. Everything else ignored still refuses the close. */
|
|
607
|
+
disposable: z.array(laneCopyPath).default([]),
|
|
606
608
|
/** The dependency lock file a rebase takes from the base branch and regenerates, never merges by hand; detected from the repository when left out. */
|
|
607
609
|
lockFile: lockFilePath.optional(),
|
|
608
610
|
/** Free gigabytes the disk must hold before `jh lane space` lets a new lane install its dependencies; 5 when left out. */
|
package/src/lane/close-check.ts
CHANGED
|
@@ -27,8 +27,9 @@ export type StatusEntry = { code: string; path: string }
|
|
|
27
27
|
/**
|
|
28
28
|
* What git, GitHub and the terminal answered about one lane, gathered before anything is removed. `treeDiff` lists the
|
|
29
29
|
* paths in which the branch head differs from the merge commit; `status` holds the worktree's status with ignored
|
|
30
|
-
* files (`statusEntriesOf`); `setupFiles` the patterns of the lane setup's copy list; `
|
|
31
|
-
*
|
|
30
|
+
* files (`statusEntriesOf`); `setupFiles` the patterns of the lane setup's copy list; `disposablePaths` the patterns
|
|
31
|
+
* of ignored paths the project lets a close delete (`lanes.disposable`); `handoverEntries` every entry of the
|
|
32
|
+
* worktree's handover directory, which git shows only as the directory and which the close archives.
|
|
32
33
|
*/
|
|
33
34
|
export type CloseFacts = {
|
|
34
35
|
lane: string
|
|
@@ -42,7 +43,10 @@ export type CloseFacts = {
|
|
|
42
43
|
treeDiff: readonly string[]
|
|
43
44
|
status: readonly StatusEntry[]
|
|
44
45
|
setupFiles: readonly string[]
|
|
46
|
+
disposablePaths: readonly string[]
|
|
45
47
|
handoverEntries: readonly string[]
|
|
48
|
+
/** The commit the branch points to on origin, read live; null when the branch is gone there or none is named. */
|
|
49
|
+
remoteHead: string | null
|
|
46
50
|
/** Whether the head of a worktree on no branch is an ancestor of the base branch on the remote. */
|
|
47
51
|
headOnBase: boolean
|
|
48
52
|
/** The staged paths of a worktree on no branch that the base branch on the remote holds with the same content. */
|
|
@@ -74,6 +78,7 @@ const BUILD_OUTPUT = [
|
|
|
74
78
|
/** Build output only as a directory entry: a package's `bin/` that an ignore rule hides as a whole. */
|
|
75
79
|
const BUILD_OUTPUT_DIRECTORY_ENTRIES = [new Bun.Glob('packages/*/bin')]
|
|
76
80
|
const HANDOVER = claudePath(HANDOVER_DIRECTORY)
|
|
81
|
+
const SHORT_COMMIT_LENGTH = 9
|
|
77
82
|
|
|
78
83
|
/** A change that is staged and nothing more: the worktree file equals the index. */
|
|
79
84
|
const isStagedOnly = (code: string): boolean =>
|
|
@@ -81,22 +86,27 @@ const isStagedOnly = (code: string): boolean =>
|
|
|
81
86
|
|
|
82
87
|
const withoutSlashes = (path: string): string => path.replace(/^\/+|\/+$/g, '')
|
|
83
88
|
|
|
84
|
-
|
|
89
|
+
/** The handover directory or anything in it: the close archives it, so removing the worktree loses none of it. */
|
|
90
|
+
const isHandoverPath = (path: string): boolean => {
|
|
91
|
+
const target = withoutSlashes(path)
|
|
92
|
+
return target === HANDOVER || target.startsWith(`${HANDOVER}/`)
|
|
93
|
+
}
|
|
85
94
|
|
|
86
95
|
/** The patterns of a `.gitignore`-style list that name files; comments, blanks and negations name none. */
|
|
87
96
|
const patternsOf = (lines: readonly string[]): string[] =>
|
|
88
97
|
lines.map((line) => line.trim()).filter((line) => line !== '' && !line.startsWith('#') && !line.startsWith('!'))
|
|
89
98
|
|
|
90
99
|
/**
|
|
91
|
-
* Whether `path` (relative to the worktree, a directory with its trailing slash) is
|
|
92
|
-
* pattern with an inner slash is anchored at the root, one without matches
|
|
100
|
+
* Whether `path` (relative to the worktree, a directory with its trailing slash) is named by a `.gitignore`-style
|
|
101
|
+
* list, or lies in a directory it names: a pattern with an inner slash is anchored at the root, one without matches
|
|
102
|
+
* the name at any depth.
|
|
93
103
|
*/
|
|
94
|
-
export const
|
|
104
|
+
export const matchesPatterns = (path: string, patterns: readonly string[]): boolean => {
|
|
95
105
|
const target = withoutSlashes(path)
|
|
96
|
-
return patternsOf(
|
|
106
|
+
return patternsOf(patterns).some((pattern) => {
|
|
97
107
|
const bare = withoutSlashes(pattern)
|
|
98
108
|
const glob = bare.includes('/') ? bare : `**/${bare}`
|
|
99
|
-
return
|
|
109
|
+
return [glob, bare, `${glob}/**`].some((candidate) => new Bun.Glob(candidate).match(target))
|
|
100
110
|
})
|
|
101
111
|
}
|
|
102
112
|
|
|
@@ -134,25 +144,51 @@ export const isBuildOutput = (path: string): boolean => {
|
|
|
134
144
|
|
|
135
145
|
/**
|
|
136
146
|
* The paths removing the worktree would delete for good: every tracked change and untracked file, and every ignored
|
|
137
|
-
* path except dependencies (`node_modules` at any depth), the setup files,
|
|
147
|
+
* path except dependencies (`node_modules` at any depth), the setup files, the project's disposable paths and build
|
|
148
|
+
* output. The handover directory is no loss either: the close archives it first.
|
|
138
149
|
*/
|
|
139
150
|
export const lostPathsOf = (
|
|
140
151
|
status: readonly StatusEntry[],
|
|
141
152
|
setupFiles: readonly string[],
|
|
142
153
|
baseHeldPaths: readonly string[] = [],
|
|
154
|
+
disposablePaths: readonly string[] = [],
|
|
143
155
|
): string[] => {
|
|
144
156
|
const kept = (path: string) =>
|
|
145
|
-
isDependencyPath(path) ||
|
|
157
|
+
isDependencyPath(path) ||
|
|
158
|
+
matchesPatterns(path, setupFiles) ||
|
|
159
|
+
matchesPatterns(path, disposablePaths) ||
|
|
160
|
+
isBuildOutput(path)
|
|
146
161
|
const held = ({ code, path }: StatusEntry) => isStagedOnly(code) && baseHeldPaths.includes(path)
|
|
147
162
|
return status
|
|
163
|
+
.filter(({ path }) => !isHandoverPath(path))
|
|
148
164
|
.filter((entry) => (entry.code !== IGNORED_CODE || !kept(entry.path)) && !held(entry))
|
|
149
165
|
.map(({ path }) => path)
|
|
150
166
|
}
|
|
151
167
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
const deletedPathsOf = ({ status, setupFiles, baseHeldPaths, disposablePaths }: CloseFacts): string[] =>
|
|
169
|
+
lostPathsOf(status, setupFiles, baseHeldPaths, disposablePaths)
|
|
170
|
+
|
|
171
|
+
/** What closing does with the lane's branch on origin: delete it at the head the check saw, leave it with the reason, or nothing to do. */
|
|
172
|
+
export type RemoteBranchPlan =
|
|
173
|
+
| { kind: 'delete'; branch: string; head: string }
|
|
174
|
+
| { kind: 'keep'; branch: string; reason: string }
|
|
175
|
+
| { kind: 'none' }
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The branch on origin goes only when it points to the head of the merged pull request, so nothing pushed after the
|
|
179
|
+
* merge is lost; otherwise it stays and the plan says why.
|
|
180
|
+
*/
|
|
181
|
+
export const remoteBranchPlanOf = ({ branch, pullRequest, remoteHead }: CloseFacts): RemoteBranchPlan => {
|
|
182
|
+
if (branch === null || remoteHead === null) return { kind: 'none' }
|
|
183
|
+
if (pullRequest === null) return { kind: 'keep', branch, reason: 'it has no merged pull request' }
|
|
184
|
+
if (remoteHead !== pullRequest.headCommit)
|
|
185
|
+
return {
|
|
186
|
+
kind: 'keep',
|
|
187
|
+
branch,
|
|
188
|
+
reason: `origin has ${remoteHead.slice(0, SHORT_COMMIT_LENGTH)}, not the merged head ${pullRequest.headCommit.slice(0, SHORT_COMMIT_LENGTH)} of #${pullRequest.number}`,
|
|
189
|
+
}
|
|
190
|
+
return { kind: 'delete', branch, head: remoteHead }
|
|
191
|
+
}
|
|
156
192
|
|
|
157
193
|
/** A lane the board marks cleaned up already may still have its worktree left over; only a lane it never named is refused. */
|
|
158
194
|
const boardRefusals = ({ lane, boardLane }: CloseFacts): string[] =>
|
|
@@ -183,7 +219,7 @@ const lockRefusals = ({ lane, locked }: CloseFacts): string[] =>
|
|
|
183
219
|
* Every reason not to close the lane, all at once; empty when nothing would be lost. Closing needs an open board row,
|
|
184
220
|
* the worktree on a branch whose newest merged pull request left the same tree on the base (a squash merge leaves the
|
|
185
221
|
* branch unmerged in git's eyes, so the trees are compared), nothing in the worktree beyond dependencies, the setup
|
|
186
|
-
* files and build output (`isBuildOutput`), an
|
|
222
|
+
* files, the project's disposable paths and build output (`isBuildOutput`), an agent that is gone or rests, and no lock
|
|
187
223
|
* on the worktree (a Claude Code session may still hold it).
|
|
188
224
|
*/
|
|
189
225
|
export const closeRefusals = (facts: CloseFacts): string[] => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
1
|
+
import { cpSync, existsSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { join, relative, resolve } from 'node:path'
|
|
3
3
|
import { claudePath, HANDOVER_DIRECTORY } from '../claude/paths'
|
|
4
4
|
import { TERMINAL_ADAPTER } from '../config'
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
baseHeldPathsOf,
|
|
22
22
|
mergedPullRequestOf,
|
|
23
23
|
openWorkspaceOf,
|
|
24
|
+
remoteHeadOf,
|
|
24
25
|
stagedPlanPathsOf,
|
|
25
26
|
type WorktreeEntry,
|
|
26
27
|
worktreeEntryOf,
|
|
@@ -180,19 +181,29 @@ const workspaceOf = (context: CloseContext): string | null =>
|
|
|
180
181
|
const absentWorktreeFactsOf = (context: CloseContext, branch: string | null): Promise<WorktreeFacts> | WorktreeFacts =>
|
|
181
182
|
branch === null ? ABSENT_WORKTREE : branchFactsOf(context, branch)
|
|
182
183
|
|
|
184
|
+
/** The commit the branch points to on origin, asked live; null when there is no branch to ask for or origin lists none. */
|
|
185
|
+
const remoteHeadFor = (context: CloseContext, branch: string | null): string | null =>
|
|
186
|
+
branch === null
|
|
187
|
+
? null
|
|
188
|
+
: remoteHeadOf(runGit(context.root, ['ls-remote', '--heads', 'origin', `${BRANCH_REF}${branch}`]), branch)
|
|
189
|
+
|
|
183
190
|
/** What git, GitHub and the terminal say about the lane now; throws when one of them cannot be asked. */
|
|
184
191
|
const surveyOf = async (lane: string, branch: string | null, context: CloseContext): Promise<CloseSurvey> => {
|
|
185
192
|
const entry = worktreeEntryOf(runGit(context.root, ['worktree', 'list', '--porcelain']), context.worktree)
|
|
186
193
|
const present = entry !== null && existsSync(context.worktree)
|
|
187
|
-
const facts: CloseFacts = {
|
|
194
|
+
const facts: Omit<CloseFacts, 'remoteHead'> = {
|
|
188
195
|
lane,
|
|
189
196
|
boardLane: boardLaneOf(readFileSync(context.boardFile, 'utf8'), lane, context.settings.boardLanguage),
|
|
190
197
|
setupFiles: setupFilesOf(context.root),
|
|
198
|
+
disposablePaths: context.settings.disposablePaths,
|
|
191
199
|
...agentFactsOf(laneStatusSourceFor(context.settings.adapter).listAgents(), lane, context.worktree),
|
|
192
200
|
runsInside: realPath(worktreeRoot(context.cwd)) === realPath(context.worktree),
|
|
193
201
|
...(present ? await worktreeFactsOf(context, entry) : await absentWorktreeFactsOf(context, branch)),
|
|
194
202
|
}
|
|
195
|
-
return {
|
|
203
|
+
return {
|
|
204
|
+
facts: { ...facts, remoteHead: remoteHeadFor(context, facts.branch) },
|
|
205
|
+
workspace: present ? workspaceOf(context) : null,
|
|
206
|
+
}
|
|
196
207
|
}
|
|
197
208
|
|
|
198
209
|
const sleep = (ms: number): Promise<void> => new Promise((done) => setTimeout(done, ms))
|
|
@@ -212,7 +223,14 @@ const exitAgentIn =
|
|
|
212
223
|
throw new Error(`the agent in pane ${pane} still runs after ${EXIT_WAIT_MS / 1000} s`)
|
|
213
224
|
}
|
|
214
225
|
|
|
215
|
-
/**
|
|
226
|
+
/** Copies the handover directory of the worktree below the main checkout; throws when the copy fails, so nothing is removed after it. */
|
|
227
|
+
const archiveHandoverIn =
|
|
228
|
+
(context: CloseContext) =>
|
|
229
|
+
(worktree: string, destination: string): void => {
|
|
230
|
+
cpSync(join(worktree, claudePath(HANDOVER_DIRECTORY)), join(context.root, destination), { recursive: true })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Git removes the worktree and refuses on any change itself (ignored files go with it, never needing `--force`); herdr then only closes the emptied workspace. */
|
|
216
234
|
const removeWorktreeIn =
|
|
217
235
|
(context: CloseContext) =>
|
|
218
236
|
(worktree: string, workspace: string | null): void => {
|
|
@@ -221,6 +239,17 @@ const removeWorktreeIn =
|
|
|
221
239
|
}
|
|
222
240
|
|
|
223
241
|
/** Deletes the branch only while it points to `head`; git refuses when it moved since the check. */
|
|
242
|
+
const deleteRemoteBranchIn =
|
|
243
|
+
(context: CloseContext) =>
|
|
244
|
+
(branch: string, head: string): void => {
|
|
245
|
+
runGit(context.root, [
|
|
246
|
+
'push',
|
|
247
|
+
'origin',
|
|
248
|
+
`--force-with-lease=${BRANCH_REF}${branch}:${head}`,
|
|
249
|
+
`:${BRANCH_REF}${branch}`,
|
|
250
|
+
])
|
|
251
|
+
}
|
|
252
|
+
|
|
224
253
|
const deleteBranchIn =
|
|
225
254
|
(context: CloseContext) =>
|
|
226
255
|
(branch: string, head: string): void => {
|
|
@@ -230,7 +259,9 @@ const deleteBranchIn =
|
|
|
230
259
|
const portsFor = (lane: string, branch: string | null, context: CloseContext): ClosePorts => ({
|
|
231
260
|
survey: () => surveyOf(lane, branch, context),
|
|
232
261
|
exitAgent: exitAgentIn(context),
|
|
262
|
+
archiveHandover: archiveHandoverIn(context),
|
|
233
263
|
removeWorktree: removeWorktreeIn(context),
|
|
264
|
+
deleteRemoteBranch: deleteRemoteBranchIn(context),
|
|
234
265
|
deleteBranch: deleteBranchIn(context),
|
|
235
266
|
readBoard: () => readFileSync(context.boardFile, 'utf8'),
|
|
236
267
|
writeBoard: (text) => writeFileSync(context.boardFile, text),
|
|
@@ -241,7 +272,7 @@ const portsFor = (lane: string, branch: string | null, context: CloseContext): C
|
|
|
241
272
|
/** The main checkout, its settings, the lane's worktree from those settings, and the board, with the pick's notice. */
|
|
242
273
|
const contextOf = (options: CloseOptions, cwd: string): { context: CloseContext; notice: string | null } => {
|
|
243
274
|
const root = mainCheckout(cwd)
|
|
244
|
-
const settings = readSettings([
|
|
275
|
+
const settings = readSettings([worktreeRoot(cwd), root])
|
|
245
276
|
const boardPath = options.boardPath === null ? null : resolve(cwd, options.boardPath)
|
|
246
277
|
const board = boardFor(boardPath, [root], settings, () => [options.lane])
|
|
247
278
|
const placed = settingsPlacingBoardLanes(root, settings, board.file, options.lane)
|
package/src/lane/close-survey.ts
CHANGED
|
@@ -62,6 +62,15 @@ export const mergedPullRequestOf = (
|
|
|
62
62
|
return merged.sort((left, right) => right.number - left.number)[0] ?? null
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** The commit `branch` points to in the output of `git ls-remote --heads origin`; null when the remote lists no such branch. */
|
|
66
|
+
export const remoteHeadOf = (lsRemote: string, branch: string): string | null => {
|
|
67
|
+
const entry = lsRemote
|
|
68
|
+
.split('\n')
|
|
69
|
+
.map((line) => line.split('\t'))
|
|
70
|
+
.find(([, ref]) => ref === `${BRANCH_REF}${branch}`)
|
|
71
|
+
return entry !== undefined && isObjectId(entry[0]) ? entry[0] : null
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
const isInside = (cwd: string | null, worktree: string): boolean =>
|
|
66
75
|
cwd !== null && (cwd === worktree || cwd.startsWith(`${worktree}${sep}`))
|
|
67
76
|
|
package/src/lane/close.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { claudePath, HANDOVER_DIRECTORY } from '../claude/paths'
|
|
1
2
|
import { twoDigits } from '../plan-format/clock'
|
|
2
3
|
import { terminalSafe } from '../plan-format/terminal-text'
|
|
3
4
|
import { errorText } from '../plans/error-text'
|
|
4
5
|
import { PLAN_LANGUAGE, type PlanLanguage } from '../vocabulary'
|
|
5
6
|
import { closedMark, historyDate, withHistoryRow, withLaneClosed } from './close-board'
|
|
6
|
-
import { BOARD_LANE, type ClosableLane, type CloseFacts, closeVerdict } from './close-check'
|
|
7
|
+
import { BOARD_LANE, type ClosableLane, type CloseFacts, closeVerdict, remoteBranchPlanOf } from './close-check'
|
|
7
8
|
import { LANE_CLOSE_COMMAND } from './close-command-line'
|
|
8
9
|
|
|
9
10
|
/** The facts the check reads, and the herdr workspace the worktree is open in, null when none is. */
|
|
@@ -27,6 +28,10 @@ export type ClosePorts = {
|
|
|
27
28
|
exitAgent: (pane: string) => Promise<void>
|
|
28
29
|
/** Removes the worktree without force, then closes the terminal workspace `workspace` names, if any. */
|
|
29
30
|
removeWorktree: (worktree: string, workspace: string | null) => void
|
|
31
|
+
/** Copies the handover directory of `worktree` to `destination`, a path relative to the main checkout; merges into one that exists. */
|
|
32
|
+
archiveHandover: (worktree: string, destination: string) => void
|
|
33
|
+
/** Deletes the branch on origin only while it still points to `head`; throws when it moved or the push fails. */
|
|
34
|
+
deleteRemoteBranch: (branch: string, head: string) => void
|
|
30
35
|
/** Deletes the branch only while it still points to `head`, the commit the check compared. */
|
|
31
36
|
deleteBranch: (branch: string, head: string) => void
|
|
32
37
|
readBoard: () => string
|
|
@@ -58,6 +63,10 @@ const DETACHED_HISTORY_TEXT: Record<PlanLanguage, (lane: string, head: string) =
|
|
|
58
63
|
`Lane \`${lane}\` cleaned up with \`${LANE_CLOSE_COMMAND}\`: worktree on no branch, head \`${head}\` is on the base branch, worktree removed`,
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
/** Where the lane's handover directory is kept once its worktree is gone: below the main checkout's own handover directory. */
|
|
67
|
+
export const handoverArchiveOf = (lane: string, now: Date): string =>
|
|
68
|
+
claudePath(HANDOVER_DIRECTORY, 'closed', `${lane}-${historyDate(now)}`)
|
|
69
|
+
|
|
61
70
|
const clockOf = (time: Date): string => `${twoDigits(time.getHours())}:${twoDigits(time.getMinutes())}`
|
|
62
71
|
|
|
63
72
|
/** The history row's text: the time, then what was closed and proven, in the board's language. */
|
|
@@ -101,12 +110,44 @@ const removalSteps = (closed: ClosedLane, survey: CloseSurvey, request: CloseReq
|
|
|
101
110
|
]
|
|
102
111
|
: []
|
|
103
112
|
return [
|
|
113
|
+
...archiveSteps(survey, request, ports),
|
|
104
114
|
...worktree,
|
|
105
115
|
...branchSteps(closed.closable, ports),
|
|
106
116
|
{ label: boardLabel(facts, request), run: () => ports.writeBoard(closedBoard(ports.readBoard(), closed, request)) },
|
|
117
|
+
...remoteBranchSteps(facts, ports),
|
|
107
118
|
]
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
/** The handover files are copied before the worktree goes; when the copy fails, the worktree stays. */
|
|
122
|
+
const archiveSteps = ({ facts }: CloseSurvey, request: CloseRequest, ports: ClosePorts): Step[] => {
|
|
123
|
+
if (!facts.worktreeExists || facts.handoverEntries.length === 0) return []
|
|
124
|
+
const destination = handoverArchiveOf(request.lane, request.now)
|
|
125
|
+
return [
|
|
126
|
+
{
|
|
127
|
+
label: `archive the handover files of ${request.lane} in ${destination}`,
|
|
128
|
+
run: () => ports.archiveHandover(request.worktree, destination),
|
|
129
|
+
},
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Last, so a failed push on origin leaves everything else done: a rerun with `--branch` finishes it. */
|
|
134
|
+
const remoteBranchSteps = (facts: CloseFacts, ports: ClosePorts): Step[] => {
|
|
135
|
+
const plan = remoteBranchPlanOf(facts)
|
|
136
|
+
if (plan.kind !== 'delete') return []
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
label: `delete branch ${plan.branch} on origin`,
|
|
140
|
+
run: () => ports.deleteRemoteBranch(plan.branch, plan.head),
|
|
141
|
+
},
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The one line saying why a branch on origin stays; null when it goes or there is none. */
|
|
146
|
+
const remoteKeptNote = (facts: CloseFacts): string | null => {
|
|
147
|
+
const plan = remoteBranchPlanOf(facts)
|
|
148
|
+
return plan.kind === 'keep' ? `keeping branch ${plan.branch} on origin: ${plan.reason}` : null
|
|
149
|
+
}
|
|
150
|
+
|
|
110
151
|
/** Deleting the branch is a step only while the branch exists: a lane resumed after a partial failure may have lost it. */
|
|
111
152
|
const branchSteps = (closable: ClosableLane, ports: ClosePorts): Step[] => {
|
|
112
153
|
if (closable.kind === 'detached' || closable.head === null) return []
|
|
@@ -123,8 +164,8 @@ const exitSteps = ({ facts }: CloseSurvey, request: CloseRequest, ports: ClosePo
|
|
|
123
164
|
}
|
|
124
165
|
|
|
125
166
|
/** Nothing is left to close: no worktree, no branch, and the board marks the lane. */
|
|
126
|
-
const isClosedInFull = ({ worktreeExists, head, boardLane }: CloseFacts): boolean =>
|
|
127
|
-
!worktreeExists && head === null && boardLane === BOARD_LANE.closed
|
|
167
|
+
const isClosedInFull = ({ worktreeExists, head, boardLane, remoteHead }: CloseFacts): boolean =>
|
|
168
|
+
!worktreeExists && head === null && boardLane === BOARD_LANE.closed && remoteHead === null
|
|
128
169
|
|
|
129
170
|
/** The lane closable from `survey`, or null after printing every reason against it under `heading`. */
|
|
130
171
|
const checked = (survey: CloseSurvey, heading: string, request: CloseRequest, ports: ClosePorts): ClosedLane | null => {
|
|
@@ -135,6 +176,11 @@ const checked = (survey: CloseSurvey, heading: string, request: CloseRequest, po
|
|
|
135
176
|
return null
|
|
136
177
|
}
|
|
137
178
|
|
|
179
|
+
const printKept = (facts: CloseFacts, ports: ClosePorts): void => {
|
|
180
|
+
const note = remoteKeptNote(facts)
|
|
181
|
+
if (note !== null) ports.print(note)
|
|
182
|
+
}
|
|
183
|
+
|
|
138
184
|
/** Runs the steps in order, one line each; at the first failure, names it and every step left undone. */
|
|
139
185
|
const runSteps = async (steps: readonly Step[], ports: ClosePorts): Promise<boolean> => {
|
|
140
186
|
for (const [index, step] of steps.entries()) {
|
|
@@ -153,8 +199,8 @@ const runSteps = async (steps: readonly Step[], ports: ClosePorts): Promise<bool
|
|
|
153
199
|
/**
|
|
154
200
|
* Closes a lane in one run, or refuses with every reason and changes nothing. Checks first (`closeVerdict`); a dry run
|
|
155
201
|
* stops there and prints the steps. Otherwise it ends the lane's agent, checks again, since the agent may have written
|
|
156
|
-
* until it exited, then removes the worktree and its workspace, deletes the local branch,
|
|
157
|
-
* the board never records a removal that failed. Exit 0 closed or dry run, 1 refused or failed.
|
|
202
|
+
* until it exited, then archives the handover files, removes the worktree and its workspace, deletes the local branch, marks the board, and deletes the branch on origin last when it
|
|
203
|
+
* still holds the merged head (`remoteBranchPlanOf`), so the board never records a removal that failed. Exit 0 closed or dry run, 1 refused or failed.
|
|
158
204
|
*/
|
|
159
205
|
export const closeLane = async (request: CloseRequest, ports: ClosePorts): Promise<number> => {
|
|
160
206
|
const first = await ports.survey()
|
|
@@ -168,12 +214,14 @@ export const closeLane = async (request: CloseRequest, ports: ClosePorts): Promi
|
|
|
168
214
|
if (request.dryRun) {
|
|
169
215
|
ports.print(`dry run, nothing changed; closing ${request.lane} would:`)
|
|
170
216
|
for (const step of [...exit, ...removalSteps(closed, first, request, ports)]) ports.print(` ${step.label}`)
|
|
217
|
+
printKept(first.facts, ports)
|
|
171
218
|
return 0
|
|
172
219
|
}
|
|
173
220
|
if (!(await runSteps(exit, ports))) return 1
|
|
174
221
|
const settled = exit.length === 0 ? first : await ports.survey()
|
|
175
222
|
const late = checked(settled, `not closing ${request.lane} after its agent exited:`, request, ports)
|
|
176
223
|
if (late === null) return 1
|
|
224
|
+
printKept(settled.facts, ports)
|
|
177
225
|
if (!(await runSteps(removalSteps(late, settled, request, ports), ports))) return 1
|
|
178
226
|
ports.print(`closed ${request.lane}`)
|
|
179
227
|
return 0
|
package/src/plans/settings.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type PlansSettings = Pick<
|
|
|
23
23
|
minFreeGb: number
|
|
24
24
|
/** The port `jh lane port` starts from (`lanes.basePort`). */
|
|
25
25
|
basePort: number
|
|
26
|
+
/** The ignored paths `jh lane close` deletes with a worktree without asking (`lanes.disposable`). */
|
|
27
|
+
disposablePaths: JackherdConfig['lanes']['disposable']
|
|
26
28
|
/** The command `jh lane dev` starts a lane's dev server with (`lanes.devCommand`); undefined when none is set. */
|
|
27
29
|
devCommand: string | undefined
|
|
28
30
|
/** The shared backend of the lanes (`lanes.backend`); undefined when none is set. */
|
|
@@ -75,6 +77,7 @@ export const plansSettingsOf = (config: JackherdConfig): PlansSettings => ({
|
|
|
75
77
|
adapter: config.terminal.adapter,
|
|
76
78
|
minFreeGb: config.lanes.minFreeGb ?? DEFAULT_MIN_FREE_GB,
|
|
77
79
|
basePort: config.lanes.basePort ?? DEFAULT_BASE_PORT,
|
|
80
|
+
disposablePaths: config.lanes.disposable,
|
|
78
81
|
devCommand: config.lanes.devCommand,
|
|
79
82
|
backend: config.lanes.backend,
|
|
80
83
|
fetchIntervalMinutes: config.plans.fetchIntervalMinutes,
|