jackherd 1.12.3 → 1.12.4
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 +5 -0
- package/README.md +16 -16
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/workflow/skills/orchestrate/SKILL.md +7 -7
- package/package.json +1 -1
- package/src/watch/command.ts +15 -10
- package/src/watch/lane-report.ts +17 -1
- package/src/watch/stream-output.ts +6 -0
- package/src/watch/watch-events.ts +8 -0
- package/src/watch/watcher-lock.ts +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
What changed for people who use jackherd, newest first.
|
|
4
4
|
|
|
5
|
+
## 1.12.4
|
|
6
|
+
|
|
7
|
+
- **A quiet stream when nothing changed.** `jh watch --stream` sends a heartbeat only when its state (lanes, their progress, what waits on you, the main checkout) changed since the last one it sent, so a coordinator's chat no longer fills with identical heartbeats over hours. The events a heartbeat brings (parallel tasks, a lane without a task signal, an unlinked Artifact) come as before.
|
|
8
|
+
- **A quiet restart.** A restart prints its `armed: …` line only when the lanes, their states or the board differ from the last start, or when it replaced a running watcher. The orchestrate skill checks the start by the Monitor call that started the stream, and the coordinator restarts an expired stream without a word to the user.
|
|
9
|
+
|
|
5
10
|
## 1.12.3
|
|
6
11
|
|
|
7
12
|
- **One read for every herd of a board.** A running watcher (`jh watch --stream`) reads the board, its plans and the lanes once and publishes the read; each herd of that board, the lane herds included, takes it, woken by the file, instead of reading everything itself every few seconds. It writes the sprint page as well, so no herd builds it. Without a running watcher, after a read older than a minute, or with a herd that runs other code than the watcher, a herd reads for itself as before. Measured with all lanes idle: a herd that follows the watcher takes about 1 % CPU, the watcher about 4-5 %. One herd plus its watcher costs a little more than in 1.12.2 (~5.6 % against ~4.3 %); from two herds on one watcher on, the whole costs less.
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<p>
|
|
2
|
-
<img alt="jackherd: a black and white Shih Tzu in a turquoise bandana, three lanes running from him to three agent nodes" src="https://unpkg.com/jackherd@1.12.
|
|
2
|
+
<img alt="jackherd: a black and white Shih Tzu in a turquoise bandana, three lanes running from him to three agent nodes" src="https://unpkg.com/jackherd@1.12.4/assets/branding/jackherd-logo-outline.svg" width="340">
|
|
3
3
|
<br>
|
|
4
4
|
</p>
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd?activeTab=dependencies)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.
|
|
6
|
+
[](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd?activeTab=dependencies)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.4/assets/icons/space.svg">
|
|
7
7
|
|
|
8
8
|
# Jack herds your coding agents.
|
|
9
9
|
|
|
@@ -15,25 +15,25 @@ For developers who'd rather decide than supervise.
|
|
|
15
15
|
curl -fsSL https://unpkg.com/jackherd/install.sh | sh
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Then `/jh-start` in a repository, and Jack offers your first plan. → [Quickstart](#quickstart)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.
|
|
18
|
+
Then `/jh-start` in a repository, and Jack offers your first plan. → [Quickstart](#quickstart)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.4/assets/icons/space.svg">
|
|
19
19
|
|
|
20
20
|
<p>
|
|
21
|
-
<img src="https://unpkg.com/jackherd@1.12.
|
|
21
|
+
<img src="https://unpkg.com/jackherd@1.12.4/docs/workflow-dark.svg" width="720" alt="jackherd in herdr: a lane hits a question, Jack asks you as a choice, you pick, and the lane works on while the herd view counts its progress.">
|
|
22
22
|
<br>
|
|
23
|
-
<sub>Jack and his lanes in <a href="https://herdr.dev/">herdr</a>, a terminal for coding agents (recommended). Without it, one Claude Code session builds with subagents.</sub><br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.
|
|
23
|
+
<sub>Jack and his lanes in <a href="https://herdr.dev/">herdr</a>, a terminal for coding agents (recommended). Without it, one Claude Code session builds with subagents.</sub><br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.4/assets/icons/space.svg">
|
|
24
24
|
</p>
|
|
25
25
|
|
|
26
|
-
<p><img src="https://unpkg.com/jackherd@1.12.
|
|
26
|
+
<p><img src="https://unpkg.com/jackherd@1.12.4/assets/icons/lanes.svg" width="24" height="34" align="middle" alt=""> <b>Parallel where it pays.</b><br>Phases that don't collide run side by side, each lane on its own branch and worktree. Jack keeps the merge order and has the others rebase.</p>
|
|
27
27
|
|
|
28
|
-
<p><img src="https://unpkg.com/jackherd@1.12.
|
|
28
|
+
<p><img src="https://unpkg.com/jackherd@1.12.4/assets/icons/bark.svg" width="24" height="34" align="middle" alt=""> <b>Jack barks.</b><br>A blocked lane, a red check, a review comment: Jack wakes, and the herd view shows where every lane stands.</p>
|
|
29
29
|
|
|
30
|
-
<p><img src="https://unpkg.com/jackherd@1.12.
|
|
30
|
+
<p><img src="https://unpkg.com/jackherd@1.12.4/assets/icons/ear.svg" width="24" height="34" align="middle" alt=""> <b>Jack listens.</b><br>Review comments go to the lane that fixes them, with a test. <code>/jh-follow</code> brings anything with a CLI or an API into the herd: tickets, error logs, status pages, deploys, and whatever else you point it at.</p>
|
|
31
31
|
|
|
32
|
-
<p><img src="https://unpkg.com/jackherd@1.12.
|
|
32
|
+
<p><img src="https://unpkg.com/jackherd@1.12.4/assets/icons/bowl.svg" width="24" height="34" align="middle" alt=""> <b>Picks up where you left off.</b><br>Plan and board hold the state, not the chat.</p>
|
|
33
33
|
|
|
34
|
-
<p><img src="https://unpkg.com/jackherd@1.12.
|
|
34
|
+
<p><img src="https://unpkg.com/jackherd@1.12.4/assets/icons/plan.svg" width="24" height="34" align="middle" alt=""> <b>Lives in your repository.</b><br>Plans, board and every decision are Markdown, reviewed like code. <code>jh init</code> gives your team the same setup.</p>
|
|
35
35
|
|
|
36
|
-
<p><img src="https://unpkg.com/jackherd@1.12.
|
|
36
|
+
<p><img src="https://unpkg.com/jackherd@1.12.4/assets/icons/decide.svg" width="24" height="34" align="middle" alt=""> <b>You make the calls.</b><br>Nothing is committed, pushed or merged without your approval. Jack merges on green checks only where you said so: one pull request, or a plan's for up to 24 hours.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.4/assets/icons/space.svg"></p>
|
|
37
37
|
|
|
38
38
|
## Quickstart
|
|
39
39
|
|
|
@@ -69,22 +69,22 @@ Then `/jh-start` in a repository, and Jack offers your first plan. → [Quicksta
|
|
|
69
69
|
/jh-orchestrate docs/plans/board-<name>/board.md
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks. → [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.
|
|
72
|
+
By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks. → [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.4/assets/icons/space.svg">
|
|
73
73
|
|
|
74
74
|
## Follow anything outside GitHub
|
|
75
75
|
|
|
76
76
|
Whatever you check by hand, Jack can watch: tickets, error logs, status pages, deploys. One `/jh-follow`, one yes, and every change comes to you like this:
|
|
77
77
|
|
|
78
|
-
<img src="https://unpkg.com/jackherd@1.12.
|
|
78
|
+
<img src="https://unpkg.com/jackherd@1.12.4/docs/follow-dark.svg" width="720" alt="A source (ticket, error log or status page) has a new or changed entry; jh watch reports it to Jack; Jack puts a row into Your turn, prefixed with the source's label; its [ask] button makes Jack ask you, and then a lane works it.">
|
|
79
79
|
|
|
80
|
-
The herd sums up your sources in one `Sources` line: per source its new entries, or when it last ran, red while it fails; opened, a click on an entry opens it. A ticket shows on the lane that works it, and a row in Your turn that came from a source starts with that source's label.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.
|
|
80
|
+
The herd sums up your sources in one `Sources` line: per source its new entries, or when it last ran, red while it fails; opened, a click on an entry opens it. A ticket shows on the lane that works it, and a row in Your turn that came from a source starts with that source's label.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.12.4/assets/icons/space.svg">
|
|
81
81
|
|
|
82
82
|
## In detail
|
|
83
83
|
|
|
84
84
|
<details>
|
|
85
85
|
<summary><b>What Jack does on his own, and what waits for you</b></summary>
|
|
86
86
|
|
|
87
|
-
<img src="https://unpkg.com/jackherd@1.12.
|
|
87
|
+
<img src="https://unpkg.com/jackherd@1.12.4/docs/lanes-dark.svg" alt="Jack prompts two lanes and asks you for decisions and approvals; the lanes send questions back as text; jh watch reads lane state, pull requests and checks and wakes Jack.">
|
|
88
88
|
|
|
89
89
|
| On his own | After your approval | Never |
|
|
90
90
|
| --- | --- | --- |
|
|
@@ -117,7 +117,7 @@ The guards catch mistakes, not intent; for that, use branch protection. The hook
|
|
|
117
117
|
|
|
118
118
|
A **plan** is Markdown with phases, checkbox tasks, a model and a review gate per phase. A **board** lists the lanes of one or more plans: which phases each lane builds, on which model, after what. A **lane** is one agent session in its own git worktree (a second checkout of your repository), and **Jack** is the session in the main checkout that runs `/jh-orchestrate` on a board.
|
|
119
119
|
|
|
120
|
-
<img src="https://unpkg.com/jackherd@1.12.
|
|
120
|
+
<img src="https://unpkg.com/jackherd@1.12.4/docs/skills-dark.svg" alt="/jh-grill hands decisions to /jh-plan; /jh-plan feeds /jh-orchestrate or a single /jh-implement; /jh-implement ends in /jh-pre-commit, which runs the reviewers; /jh-diagnose hands a red command to /jh-implement.">
|
|
121
121
|
|
|
122
122
|
| Skill | Does |
|
|
123
123
|
| --- | --- |
|
package/install.sh
CHANGED
|
@@ -9,7 +9,7 @@ set -eu
|
|
|
9
9
|
|
|
10
10
|
# The default is the version this file ships with. The release bump changes it together with
|
|
11
11
|
# package.json; install.test.ts fails while the two differ. Overridable, but only by a plain release.
|
|
12
|
-
JACKHERD_VERSION="${JACKHERD_VERSION:-1.12.
|
|
12
|
+
JACKHERD_VERSION="${JACKHERD_VERSION:-1.12.4}"
|
|
13
13
|
|
|
14
14
|
JACKHERD_PACKAGE='jackherd'
|
|
15
15
|
# Test seam: the terminal the wizard asks its questions on.
|
|
@@ -192,8 +192,8 @@ watcher finds you by the default name of § 0, or by `--coordinator <name>`. In
|
|
|
192
192
|
arrives as one message, and the watcher keeps running.
|
|
193
193
|
|
|
194
194
|
The heartbeat (default 15 minutes, due after that long without a report) is one line,
|
|
195
|
-
`=== heartbeat <time> · <lane> <status> ·
|
|
196
|
-
(`<lane> working: P12 7/12`)
|
|
195
|
+
`=== heartbeat <time> · <lane> <status> · …`, sent only on a change. Each working lane carries its current phase and its ticks
|
|
196
|
+
(`<lane> working: P12 7/12`) and the subagents running in its worktree
|
|
197
197
|
(`P12 7/12, 3 subagents`). A lane whose count stays put with no subagent running is
|
|
198
198
|
one to read yourself. `lane added: <name>` and `lane removed: <name>` mark a change of the found lanes.
|
|
199
199
|
An agent is watched from the state it first shows; one that exits fires as `no longer listed`.
|
|
@@ -210,9 +210,9 @@ twice.
|
|
|
210
210
|
- **The stream runs under the Monitor tool.** A Bash call never ends with it, not even in the
|
|
211
211
|
background, so its reports go unseen; the kit's `watch-guard` hook refuses such a call where the
|
|
212
212
|
project installed it, and this rule holds where it did not.
|
|
213
|
-
- **Check the start.**
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
- **Check the start.** A start under the Monitor tool is right; its first line, `armed: …`, comes
|
|
214
|
+
only when lanes, states or board changed. A start as a Bash call went wrong: stop that task
|
|
215
|
+
(`TaskStop`) and start it under the Monitor tool.
|
|
216
216
|
- **Only when the session has no Monitor tool**, even after `ToolSearch`, the watcher runs without
|
|
217
217
|
`--stream` as its own Bash call with `run_in_background: true`. It returns after one report and is
|
|
218
218
|
started again after every wake-up. Tell the user in one sentence that this session watches without a stream
|
|
@@ -248,7 +248,7 @@ On wake-up:
|
|
|
248
248
|
| `checks passed #<n>` | One line to the user: ready to merge, with the board's merge order |
|
|
249
249
|
| `deploy …` | Result and target as the run states them, to the user; a target `unknown (…)` is reported as unknown, never guessed. A failed run goes to the user as an `AskUserQuestion`: read the logs now or later |
|
|
250
250
|
| `due <when> · <action>` | An open appointment fell due (**An agreed time**, below); the action is cut to 160 characters, the board row holds it whole. Carry it out as agreed, under the approvals it needs (§ 5), then write the outcome into its status cell in the same turn |
|
|
251
|
-
| `heartbeat` | One line of status to the user
|
|
251
|
+
| `heartbeat` | One line of status to the user; a resting lane carries `waits on: <question>, <n> open questions` from its report, and `for you (<n>): …` names what still waits on the user (**Anything that waits on the user's answer**, below): the `for you` part of that line is the count only (`3 wait for you, see the herd`, in the language of the replies); the herd shows the items, and no reply of yours ends with a list of the rows of the board section |
|
|
252
252
|
| `gh unavailable` | Tell the user once; herdr watching continues |
|
|
253
253
|
| `lane <name> warn` | One line to the user: the lane's fill and that it hands over at a phase boundary if it rises further; no step in the lane |
|
|
254
254
|
| `lane <name> alert` | § 8: send the lane once the request for its hand-over at the end of its current phase |
|
|
@@ -258,7 +258,7 @@ On wake-up:
|
|
|
258
258
|
| `lane removed: <name>` | Nothing after you closed it (§ 7). Otherwise its agent exited or its worktree is gone: read why, then tell the user |
|
|
259
259
|
| `kit updated <old> → <new>` | The kit was updated under the running watcher, which ended. After the `jh update` call has finished (with `(source changed)` there is none: the kit's source moved under a watcher run from it), read the change of this skill the next line names (`orchestrate skill: § … changed · diff <path>`; the diff shows the kit's placeholders, not your values), and the change of each reference a line names (`orchestrate reference <name> changed · diff <path>`, or `new · read <path>`), so the new text is the one you follow; read this skill file again (`Read` on the file you were given, no session restart) only when the line says `diff unavailable`; `orchestrate skill unchanged` needs no reading. Then start the stream again |
|
|
260
260
|
| an event line ending in `· read <path>` | A rare event (`feedback`, `preview`, `source`, `parallel`, an update, …): read the file it names, mostly [`references/events.md`](references/events.md), and handle the event as it says there; read its `parallel tasks` section also before you switch `watch.parallelTasks.auto` on |
|
|
261
|
-
| monitor expired | Start the stream again |
|
|
261
|
+
| monitor expired | Start the stream again, without a word to the user |
|
|
262
262
|
| `superseded by <pid>` / `stopped by SIGTERM` | Nothing for `superseded`: the newer watcher runs. After `stopped by SIGTERM` from outside, start it again |
|
|
263
263
|
| `watcher stopped: <reason>` | Tell the user, fix the cause (an unreadable state file, say), then start it again |
|
|
264
264
|
|
package/package.json
CHANGED
package/src/watch/command.ts
CHANGED
|
@@ -99,7 +99,7 @@ import { boardCheckoutRows, watchedLaneParallel, watchedLaneProgress, watchedPla
|
|
|
99
99
|
import {
|
|
100
100
|
appointmentEventLine,
|
|
101
101
|
approvalDutyLines,
|
|
102
|
-
|
|
102
|
+
armNotice,
|
|
103
103
|
blockedDutyLines,
|
|
104
104
|
coordinatorStatusLine,
|
|
105
105
|
feedbackEventLines,
|
|
@@ -172,7 +172,7 @@ import { sourceDryRun } from './source-dry-run-command'
|
|
|
172
172
|
import { runSource, SOURCE_LIMITS } from './source-run'
|
|
173
173
|
import { recordWatchedSources } from './source-tickets'
|
|
174
174
|
import { isSourceDue, type SourceRun, type SourceSettings, type SourceState, sourceEvents } from './sources'
|
|
175
|
-
import { streamHeartbeatLine } from './stream-output'
|
|
175
|
+
import { heartbeatStateOf, isHeartbeatNews, streamHeartbeatLine } from './stream-output'
|
|
176
176
|
import {
|
|
177
177
|
formatTaskParallelLine,
|
|
178
178
|
TASK_PARALLEL_EVENT,
|
|
@@ -1162,10 +1162,11 @@ const subagentCountsOf = (watch: Watch, now: number): Record<string, number> =>
|
|
|
1162
1162
|
|
|
1163
1163
|
/**
|
|
1164
1164
|
* The heartbeat of a stream: one line, a working lane with its phase count at every heartbeat and the subagents running
|
|
1165
|
-
* in its worktree
|
|
1166
|
-
* count, and the next report names the failure.
|
|
1165
|
+
* in its worktree, printed only when its state changed since the last one sent. The plans are read only when a lane
|
|
1166
|
+
* works; a board that cannot be read leaves the lines without a count, and the next report names the failure. Returns
|
|
1167
|
+
* the state of the heartbeat it printed, null when it printed none.
|
|
1167
1168
|
*/
|
|
1168
|
-
const printStreamHeartbeat = async (watch: Watch, watching: Watching, now: number): Promise<
|
|
1169
|
+
const printStreamHeartbeat = async (watch: Watch, watching: Watching, now: number): Promise<string | null> => {
|
|
1169
1170
|
const working = workingLanes(watching)
|
|
1170
1171
|
const progress = await workingProgressOf(watch, watching)
|
|
1171
1172
|
const counts = currentPhaseCountsOf(progress)
|
|
@@ -1177,13 +1178,16 @@ const printStreamHeartbeat = async (watch: Watch, watching: Watching, now: numbe
|
|
|
1177
1178
|
}),
|
|
1178
1179
|
)
|
|
1179
1180
|
const standing = [waitingLineOf(watch), mainCheckoutLineOf(watch)].filter((line) => line !== null)
|
|
1180
|
-
|
|
1181
|
+
const line = streamHeartbeatLine(clock(now), watching.lanes, watching.current, notes, rests, standing)
|
|
1182
|
+
const sent = isHeartbeatNews(watching.state.heartbeat, line) ? heartbeatStateOf(line) : null
|
|
1183
|
+
if (sent !== null) console.log(line)
|
|
1181
1184
|
const events = [
|
|
1182
1185
|
...taskParallelEventsOf(watch, watching, now),
|
|
1183
1186
|
...noTaskSignalEventsOf(watch, watching, progress, now),
|
|
1184
1187
|
...unlinkedArtifactEventsOf(watch, watching),
|
|
1185
1188
|
]
|
|
1186
1189
|
for (const event of events) console.log(`${watchEventLine(event, null)}${hintFor(watch, eventReference(event))}`)
|
|
1190
|
+
return sent
|
|
1187
1191
|
}
|
|
1188
1192
|
|
|
1189
1193
|
/**
|
|
@@ -1236,8 +1240,8 @@ const pollOnce = async (watch: Watch, watching: Watching): Promise<boolean> => {
|
|
|
1236
1240
|
})
|
|
1237
1241
|
} else {
|
|
1238
1242
|
if (heartbeat) {
|
|
1239
|
-
await printStreamHeartbeat(watch, watching, now)
|
|
1240
|
-
next = recordedOf(watch, watching)
|
|
1243
|
+
const sent = await printStreamHeartbeat(watch, watching, now)
|
|
1244
|
+
next = { ...recordedOf(watch, watching), ...(sent !== null && { heartbeat: sent }) }
|
|
1241
1245
|
}
|
|
1242
1246
|
if (due) writePrivateFile(watch.files.state, JSON.stringify(next))
|
|
1243
1247
|
watching.baseline = advanceBaseline(watching.baseline, watching.current)
|
|
@@ -1300,10 +1304,11 @@ const watchLanes = async (watch: Watch) => {
|
|
|
1300
1304
|
const sprintLanes = watch.options.lanes ? [] : sprintLaneNames(watch, [])
|
|
1301
1305
|
const lanes = lanesFor(watch, agents, sprintLanes)
|
|
1302
1306
|
const armed = toSnapshot(agents, lanes)
|
|
1303
|
-
|
|
1307
|
+
const arm = armNotice(state.armed, lanes, armed, takeOver, relative(watch.root, watch.sprintFile))
|
|
1308
|
+
if (arm.line !== null) console.log(arm.line)
|
|
1304
1309
|
if (watch.boardNotice) console.log(watch.boardNotice)
|
|
1305
1310
|
|
|
1306
|
-
const watching = initialWatching(watch, { lanes, sprintLanes, state, armed, agents })
|
|
1311
|
+
const watching = initialWatching(watch, { lanes, sprintLanes, state: { ...state, armed: arm.state }, armed, agents })
|
|
1307
1312
|
keepViews(watch, watching, { kind: ENSURE_TRIGGER.start })
|
|
1308
1313
|
process.on('SIGTERM', () => {
|
|
1309
1314
|
saveState(watch, watching)
|
package/src/watch/lane-report.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
WATCH_EVENT,
|
|
26
26
|
type WatchEvent,
|
|
27
27
|
} from './watch-events'
|
|
28
|
-
import type
|
|
28
|
+
import { NO_TAKE_OVER, type TakeOver } from './watcher-lock'
|
|
29
29
|
|
|
30
30
|
export type LaneReport = {
|
|
31
31
|
lane: string
|
|
@@ -363,3 +363,19 @@ export const armLine = (lanes: readonly string[], armed: LaneSnapshot, takeOver:
|
|
|
363
363
|
'no lanes'
|
|
364
364
|
return `armed: ${states}${takeOverNote(takeOver)} board ${terminalSafe(board)}`
|
|
365
365
|
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The `armed:` line a start prints, null when it would repeat the last start's (`previous`), and the line to remember:
|
|
369
|
+
* the one without a take-over note, so a start that replaced a watcher speaks and the one after it compares as usual.
|
|
370
|
+
*/
|
|
371
|
+
export const armNotice = (
|
|
372
|
+
previous: string | null,
|
|
373
|
+
lanes: readonly string[],
|
|
374
|
+
armed: LaneSnapshot,
|
|
375
|
+
takeOver: TakeOver,
|
|
376
|
+
board: string,
|
|
377
|
+
): { line: string | null; state: string } => {
|
|
378
|
+
const state = armLine(lanes, armed, NO_TAKE_OVER, board)
|
|
379
|
+
const line = armLine(lanes, armed, takeOver, board)
|
|
380
|
+
return { line: line === previous ? null : line, state }
|
|
381
|
+
}
|
|
@@ -35,3 +35,9 @@ export const streamHeartbeatLine = (
|
|
|
35
35
|
...standing.map((line) => terminalSafe(line).replaceAll(SEPARATOR, ' / ')),
|
|
36
36
|
].join(SEPARATOR)
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/** A heartbeat line without its head and time: what two heartbeats are compared by. */
|
|
40
|
+
export const heartbeatStateOf = (line: string): string => line.slice(line.indexOf(SEPARATOR) + SEPARATOR.length)
|
|
41
|
+
|
|
42
|
+
/** Whether a heartbeat brings news against the state of the last one sent (`heartbeatStateOf`); the first one always does. */
|
|
43
|
+
export const isHeartbeatNews = (previous: string | null, line: string): boolean => previous !== heartbeatStateOf(line)
|
|
@@ -128,6 +128,10 @@ export type WatchState = {
|
|
|
128
128
|
reportQuestions: Record<string, string>
|
|
129
129
|
/** The lane lines of the status block last printed; null before the first report. */
|
|
130
130
|
statusLines: string[] | null
|
|
131
|
+
/** The state of the last heartbeat a stream sent (`heartbeatStateOf`); null before the first one. */
|
|
132
|
+
heartbeat: string | null
|
|
133
|
+
/** The `armed:` line of the last start; a start that would print the same line stays silent. Null before the first. */
|
|
134
|
+
armed: string | null
|
|
131
135
|
/** The board's appointments reported as due and its unreadable appointment rows reported, each once (`appointments.ts`). */
|
|
132
136
|
appointments: AppointmentState
|
|
133
137
|
}
|
|
@@ -219,6 +223,8 @@ export const emptyWatchState = (): WatchState => ({
|
|
|
219
223
|
reportMtimes: {},
|
|
220
224
|
reportQuestions: {},
|
|
221
225
|
statusLines: null,
|
|
226
|
+
heartbeat: null,
|
|
227
|
+
armed: null,
|
|
222
228
|
appointments: emptyAppointmentState(),
|
|
223
229
|
})
|
|
224
230
|
|
|
@@ -271,6 +277,8 @@ const isWatchState = (value: Record<string, unknown>): value is WatchState =>
|
|
|
271
277
|
everyValue(value.reportQuestions, (entry) => typeof entry === 'string') &&
|
|
272
278
|
(value.statusLines === null ||
|
|
273
279
|
(Array.isArray(value.statusLines) && value.statusLines.every((line) => typeof line === 'string'))) &&
|
|
280
|
+
(value.heartbeat === null || typeof value.heartbeat === 'string') &&
|
|
281
|
+
(value.armed === null || typeof value.armed === 'string') &&
|
|
274
282
|
isAppointmentState(value.appointments)
|
|
275
283
|
|
|
276
284
|
/**
|
|
@@ -18,6 +18,9 @@ export type CommandLineOf = (pid: number) => string | null
|
|
|
18
18
|
/** What taking the lock did to a watcher that held it: stopped it, or found it still running after the wait. */
|
|
19
19
|
export type TakeOver = { replaced: number | null; stillRunning: number | null }
|
|
20
20
|
|
|
21
|
+
/** A start that found no watcher to take over from. */
|
|
22
|
+
export const NO_TAKE_OVER: TakeOver = { replaced: null, stillRunning: null }
|
|
23
|
+
|
|
21
24
|
/** The CLI's entry by source path or bin link (`jh`, and `jackherd` for `bunx jackherd`); `agent-kit` until 2.0.0, so a watcher of the old CLI is replaced. */
|
|
22
25
|
const CLI_ENTRIES = ['cli.ts', CLI_NAME, PACKAGE_NAME, 'agent-kit']
|
|
23
26
|
const WATCH_COMMAND = 'watch'
|
|
@@ -165,11 +168,11 @@ export const isWatcherRunning = (lockFile: string, lookUp: CommandLineOf = reade
|
|
|
165
168
|
export const takeOverLock = async (lockFile: string): Promise<TakeOver> => {
|
|
166
169
|
const previous = supersedeTarget(readPrivateText(lockFile), process.pid, commandLineOf)
|
|
167
170
|
writePrivateFile(lockFile, String(process.pid))
|
|
168
|
-
if (previous === null || !isAlive(previous)) return
|
|
171
|
+
if (previous === null || !isAlive(previous)) return NO_TAKE_OVER
|
|
169
172
|
try {
|
|
170
173
|
process.kill(previous, 'SIGTERM')
|
|
171
174
|
} catch {
|
|
172
|
-
return
|
|
175
|
+
return NO_TAKE_OVER
|
|
173
176
|
}
|
|
174
177
|
const deadline = Date.now() + STOP_WAIT_MS
|
|
175
178
|
while (isAlive(previous) && Date.now() < deadline) await Bun.sleep(STOP_POLL_MS)
|