jackherd 1.26.1 → 1.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +18 -18
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/workflow/skills/orchestrate/references/lane-session.md +1 -1
- package/package.json +1 -1
- package/src/brief/lane-facts.ts +30 -18
- package/src/context/command.ts +3 -1
- package/src/lane/close-command.ts +5 -3
- package/src/lane/close.ts +1 -1
- package/src/lane/send-options.ts +3 -2
- package/src/lane/send.ts +8 -1
- package/src/lane/subagent-view.ts +21 -0
- package/src/lane-prompt/command.ts +4 -1
- package/src/plans/active-tasks.ts +4 -2
- package/src/plans/activity-read.ts +3 -2
- package/src/plans/command.ts +3 -1
- package/src/plans/coordinator-row.ts +7 -2
- package/src/plans/ensure-panes.ts +2 -2
- package/src/plans/lane-discovery.ts +42 -3
- package/src/plans/lane-worktrees.ts +56 -0
- package/src/plans/sent-files.ts +10 -5
- package/src/plans/settings.ts +9 -1
- package/src/plans/snapshot.ts +23 -8
- package/src/plans/subagents-read.ts +17 -2
- package/src/plans/subagents.ts +3 -2
- package/src/plans/unlisted-lanes.ts +13 -0
- package/src/resume/command.ts +1 -1
- package/src/resume/coordinator-read.ts +12 -12
- package/src/resume/decision.ts +3 -0
- package/src/resume/record.ts +24 -5
- package/src/sync/command.ts +16 -2
- package/src/sync/downgrade.ts +9 -0
- package/src/sync/if-changed.ts +6 -7
- package/src/update/command.ts +17 -0
- package/src/update/registry-wait.ts +44 -0
- package/src/update/update.ts +23 -0
- package/src/version/registry.ts +8 -0
- package/src/watch/command.ts +28 -14
- package/src/watch/obsolete-proposals.ts +8 -6
- package/src/watch/obsolete-records.ts +3 -1
- package/src/watch/running-subagents.ts +3 -2
- package/src/watch/task-signal-read.ts +4 -1
- package/src/watch/unlinked-artifacts-read.ts +3 -1
package/src/watch/command.ts
CHANGED
|
@@ -56,6 +56,7 @@ import { ensurePlanPanes } from '../plans/ensure-panes'
|
|
|
56
56
|
import { errorText } from '../plans/error-text'
|
|
57
57
|
import { baseReaderOf, mainCheckout, worktreeNames, worktreeRoot } from '../plans/git'
|
|
58
58
|
import { discoverLanes, isUnchanged, laneSetChange } from '../plans/lane-discovery'
|
|
59
|
+
import { type PlacedLanes, placeLanes } from '../plans/lane-worktrees'
|
|
59
60
|
import { readerOncePerRef } from '../plans/plan-heads'
|
|
60
61
|
import { readQuestionRecords } from '../plans/questions-read'
|
|
61
62
|
import { closeSentRecords, readSentRecords } from '../plans/sent-files-read'
|
|
@@ -172,6 +173,7 @@ import { branchOf, fetchBaseWhenMoved, fetchPlanHeadsWhenMoved, readMainCheckout
|
|
|
172
173
|
import { type LaneStalledEvent, laneStalledEvents, STALLED_HEARTBEATS, type StallState } from './lane-stalled'
|
|
173
174
|
import {
|
|
174
175
|
advanceBaseline,
|
|
176
|
+
agentPlacesOf,
|
|
175
177
|
armBaseline,
|
|
176
178
|
baselineWithNewAgents,
|
|
177
179
|
departedLanes,
|
|
@@ -302,6 +304,7 @@ type Watch = {
|
|
|
302
304
|
boardNotice: string | null
|
|
303
305
|
options: WatchOptions
|
|
304
306
|
files: Files
|
|
307
|
+
/** Set at the start and again at every poll, once the lanes are placed by where their agents run (`placeLanes`). */
|
|
305
308
|
settings: PlansSettings
|
|
306
309
|
/** `watch.deploy`, `feedback`, `previews` and `checks` as read at the start; the loop reads them from `Watching.live`. */
|
|
307
310
|
live: LiveValues
|
|
@@ -1158,13 +1161,16 @@ const readSprintLanes = (watch: Watch): SprintLane[] | null => {
|
|
|
1158
1161
|
const sprintLaneNames = (watch: Watch, previous: string[]): string[] =>
|
|
1159
1162
|
readSprintLanes(watch)?.map((lane) => lane.name) ?? previous
|
|
1160
1163
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1164
|
+
/** Places the lanes by where their agents run (`placeLanes`), named lanes included; the caller takes up `settings` into `watch.settings`. */
|
|
1165
|
+
const placeLanesOf = (watch: Watch, agents: readonly HerdrAgent[], sprintLanes: readonly string[]): PlacedLanes =>
|
|
1166
|
+
placeLanes(watch.root, watch.settings, agentPlacesOf(agents), [...sprintLanes, ...(watch.options.lanes ?? [])])
|
|
1167
|
+
|
|
1168
|
+
const lanesFor = (
|
|
1169
|
+
watch: Watch,
|
|
1170
|
+
agents: readonly HerdrAgent[],
|
|
1171
|
+
sprintLanes: readonly string[],
|
|
1172
|
+
worktrees: readonly string[],
|
|
1173
|
+
): string[] => watch.options.lanes ?? discoverLanes({ worktrees, agentNames: agentNamesOf(agents), sprintLanes })
|
|
1168
1174
|
|
|
1169
1175
|
/** Prints the `lane <name> not watched` lines that are new since the last poll; named lanes are never discovered, so they have none. */
|
|
1170
1176
|
const reportMisplaced = (watch: Watch, watching: Watching) => {
|
|
@@ -1203,7 +1209,9 @@ const keepBackendPane = async (watch: Watch, watching: Watching) => {
|
|
|
1203
1209
|
const followLaneSet = (watch: Watch, watching: Watching): string[] => {
|
|
1204
1210
|
if (watch.options.lanes !== null) return []
|
|
1205
1211
|
watching.sprintLanes = sprintLaneNames(watch, watching.sprintLanes)
|
|
1206
|
-
const
|
|
1212
|
+
const placed = placeLanesOf(watch, watching.agents, watching.sprintLanes)
|
|
1213
|
+
watch.settings = placed.settings
|
|
1214
|
+
const lanes = lanesFor(watch, watching.agents, watching.sprintLanes, placed.worktrees)
|
|
1207
1215
|
const change = laneSetChange(watching.lanes, lanes)
|
|
1208
1216
|
if (isUnchanged(change)) return []
|
|
1209
1217
|
watching.departed = [...watching.departed, ...departedLanes(change.removed, watching.baseline, watching.current)]
|
|
@@ -1333,6 +1341,7 @@ const noTaskSignalEventsOf = (
|
|
|
1333
1341
|
const signalled = signalledLanes(watching.lanes, {
|
|
1334
1342
|
root: watch.root,
|
|
1335
1343
|
worktreeDirectory: watch.settings.worktreeDirectory,
|
|
1344
|
+
laneWorktrees: watch.settings.laneWorktrees,
|
|
1336
1345
|
now,
|
|
1337
1346
|
})
|
|
1338
1347
|
const counts = currentPhaseCountsOf(progress)
|
|
@@ -1400,23 +1409,25 @@ const waitingObsoleteEventsOf = (
|
|
|
1400
1409
|
if (!watch.inMainCheckout) return []
|
|
1401
1410
|
try {
|
|
1402
1411
|
const worktreeDirectory = join(watch.root, watch.settings.worktreeDirectory)
|
|
1412
|
+
const { laneWorktrees } = watch.settings
|
|
1403
1413
|
if (!existsSync(worktreeDirectory)) return []
|
|
1404
1414
|
const sends = readSentRecords(watch.root)
|
|
1405
1415
|
const questions = readQuestionRecords(watch.root)
|
|
1406
1416
|
const boardText = readBoardText(watch)
|
|
1407
1417
|
const board = boardText === null ? null : openWaitingItems(boardText, watch.settings.boardLanguage)
|
|
1408
1418
|
const { missing, gone } = laneAbsenceOf(
|
|
1409
|
-
waitingLanesOf({ sends, questions, board, worktreeDirectory }),
|
|
1410
|
-
new Set(worktreeNames(worktreeDirectory)),
|
|
1419
|
+
waitingLanesOf({ sends, questions, board, worktreeDirectory, laneWorktrees }),
|
|
1420
|
+
new Set([...worktreeNames(worktreeDirectory), ...Object.keys(laneWorktrees)]),
|
|
1411
1421
|
watching.waitingMissingLanes,
|
|
1412
1422
|
)
|
|
1413
|
-
const closings = closeObsoleteSends(watch.root, obsoleteSendsOf(sends, worktreeDirectory, gone), now)
|
|
1423
|
+
const closings = closeObsoleteSends(watch.root, obsoleteSendsOf(sends, worktreeDirectory, gone, laneWorktrees), now)
|
|
1414
1424
|
const closed = new Set(closings.map(({ id }) => id))
|
|
1415
1425
|
const { events, state } = obsoleteProposalEvents(watching.waitingProposals, {
|
|
1416
1426
|
sends: sends.filter(({ id }) => !closed.has(id)),
|
|
1417
1427
|
questions,
|
|
1418
1428
|
board,
|
|
1419
1429
|
worktreeDirectory,
|
|
1430
|
+
laneWorktrees,
|
|
1420
1431
|
gone,
|
|
1421
1432
|
...proposalContextOf(watching),
|
|
1422
1433
|
})
|
|
@@ -1477,10 +1488,10 @@ const subagentCountsOf = (watch: Watch, now: number): Record<string, number> =>
|
|
|
1477
1488
|
try {
|
|
1478
1489
|
const worktreeDirectory = join(watch.root, watch.settings.worktreeDirectory)
|
|
1479
1490
|
const running = readRunningSubagents(
|
|
1480
|
-
subagentSourceOf(watch.root, worktreeDirectory, watch.settings.skillPrefix),
|
|
1491
|
+
subagentSourceOf(watch.root, worktreeDirectory, watch.settings.skillPrefix, watch.settings.laneWorktrees),
|
|
1481
1492
|
now,
|
|
1482
1493
|
)
|
|
1483
|
-
return runningSubagentCountsOf(running, worktreeDirectory)
|
|
1494
|
+
return runningSubagentCountsOf(running, worktreeDirectory, watch.settings.laneWorktrees)
|
|
1484
1495
|
} catch {
|
|
1485
1496
|
return {}
|
|
1486
1497
|
}
|
|
@@ -1719,7 +1730,9 @@ const watchLanes = async (watch: Watch): Promise<boolean> => {
|
|
|
1719
1730
|
const state = await readState(watch.files)
|
|
1720
1731
|
const agents = listAgents()
|
|
1721
1732
|
const sprintLanes = watch.options.lanes ? [] : sprintLaneNames(watch, [])
|
|
1722
|
-
const
|
|
1733
|
+
const placed = placeLanesOf(watch, agents, sprintLanes)
|
|
1734
|
+
watch.settings = placed.settings
|
|
1735
|
+
const lanes = lanesFor(watch, agents, sprintLanes, placed.worktrees)
|
|
1723
1736
|
const armed = toSnapshot(agents, lanes)
|
|
1724
1737
|
const arm = armNotice(state.armed, lanes, armed, takeOver, relative(watch.root, watch.sprintFile))
|
|
1725
1738
|
if (arm.line !== null)
|
|
@@ -1742,6 +1755,7 @@ const watchLanes = async (watch: Watch): Promise<boolean> => {
|
|
|
1742
1755
|
process.env.HERDR_PANE_ID,
|
|
1743
1756
|
),
|
|
1744
1757
|
laneModels: laneModelColumnsOf(watching.laneModelReadings),
|
|
1758
|
+
laneWorktrees: { ...watch.settings.laneWorktrees },
|
|
1745
1759
|
})
|
|
1746
1760
|
resumeRecord.follow(armed, Date.now(), observed())
|
|
1747
1761
|
process.on('SIGTERM', () => {
|
|
@@ -4,7 +4,7 @@ import type { WaitingItem } from '../plan-format/waiting'
|
|
|
4
4
|
import { CLOSE_REASON, closeReasonOf } from '../plans/close-reason'
|
|
5
5
|
import { type QuestionRecord, questionTextOf } from '../plans/question-records'
|
|
6
6
|
import { type SentFileRecord, sentLaneOf, sentTextOf } from '../plans/sent-files'
|
|
7
|
-
import {
|
|
7
|
+
import { type LaneWorktrees, laneOfCwd } from '../plans/unlisted-lanes'
|
|
8
8
|
import { isRecord } from '../shared/is-record'
|
|
9
9
|
import { PLAN_LANGUAGE, type PlanLanguage } from '../vocabulary'
|
|
10
10
|
import { latestPullRequestOf, PULL_REQUEST_STATE, type WatchedPullRequest } from './pull-requests'
|
|
@@ -33,6 +33,8 @@ export type ProposalInput = {
|
|
|
33
33
|
/** Lane → branch, as the watcher keeps it after a lane was closed. */
|
|
34
34
|
laneBranches: Readonly<Record<string, string>>
|
|
35
35
|
worktreeDirectory: string
|
|
36
|
+
/** The lanes placed in worktrees of another name (`PlansSettings.laneWorktrees`). */
|
|
37
|
+
laneWorktrees?: LaneWorktrees
|
|
36
38
|
/** The lanes whose directory was missing in two heartbeats in a row (`laneAbsenceOf`). */
|
|
37
39
|
gone: ReadonlySet<string>
|
|
38
40
|
/** The lanes known to have existed; a board row naming a lane that never had a worktree proposes nothing. */
|
|
@@ -64,13 +66,13 @@ export const textReferencesOf = (text: string): TextReferences => ({
|
|
|
64
66
|
|
|
65
67
|
/** The lanes the open points name: a send's lane, a question's lane by its directory, a board row's lanes in its text. */
|
|
66
68
|
export const waitingLanesOf = (
|
|
67
|
-
input: Pick<ProposalInput, 'sends' | 'questions' | 'board' | 'worktreeDirectory'>,
|
|
69
|
+
input: Pick<ProposalInput, 'sends' | 'questions' | 'board' | 'worktreeDirectory' | 'laneWorktrees'>,
|
|
68
70
|
): string[] => [
|
|
69
71
|
...input.sends.flatMap((record) =>
|
|
70
|
-
record.closedAt === null ? (sentLaneOf(record, input.worktreeDirectory) ?? []) : [],
|
|
72
|
+
record.closedAt === null ? (sentLaneOf(record, input.worktreeDirectory, input.laneWorktrees) ?? []) : [],
|
|
71
73
|
),
|
|
72
74
|
...input.questions.flatMap((record) =>
|
|
73
|
-
record.closedAt === null ? (
|
|
75
|
+
record.closedAt === null ? (laneOfCwd(record.cwd, input.worktreeDirectory, input.laneWorktrees) ?? []) : [],
|
|
74
76
|
),
|
|
75
77
|
...(input.board ?? []).flatMap((row) => textReferencesOf(rowText(row)).lanes),
|
|
76
78
|
]
|
|
@@ -87,7 +89,7 @@ const laneRemovedReason = (lane: string): string => closeReasonOf(CLOSE_REASON.l
|
|
|
87
89
|
|
|
88
90
|
const sendProposals = (input: ProposalInput): Proposal[] =>
|
|
89
91
|
input.sends.flatMap((record) => {
|
|
90
|
-
const lane = sentLaneOf(record, input.worktreeDirectory)
|
|
92
|
+
const lane = sentLaneOf(record, input.worktreeDirectory, input.laneWorktrees)
|
|
91
93
|
const branch = lane === null ? undefined : input.laneBranches[lane]
|
|
92
94
|
const pullRequest = branch === undefined ? null : latestPullRequestOf(input.pullRequests ?? [], branch)
|
|
93
95
|
return record.closedAt === null && pullRequest !== null && hasEnded(pullRequest)
|
|
@@ -96,7 +98,7 @@ const sendProposals = (input: ProposalInput): Proposal[] =>
|
|
|
96
98
|
})
|
|
97
99
|
|
|
98
100
|
const questionReasons = (record: QuestionRecord, input: ProposalInput): string[] => {
|
|
99
|
-
const lane =
|
|
101
|
+
const lane = laneOfCwd(record.cwd, input.worktreeDirectory, input.laneWorktrees)
|
|
100
102
|
const superseded = record.supersededBy ? [`${SUPERSEDED}: ${record.supersededBy.question}`] : []
|
|
101
103
|
const ended = typeof record.sessionEndedAt === 'number' ? [SESSION_ENDED] : []
|
|
102
104
|
return [...superseded, ...ended, ...(lane !== null && input.gone.has(lane) ? [laneRemovedReason(lane)] : [])]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { terminalSafe } from '../plan-format/terminal-text'
|
|
2
2
|
import { CLOSE_REASON, closeReasonOf } from '../plans/close-reason'
|
|
3
3
|
import { latestPerFile, type SentFileRecord, sentLaneOf } from '../plans/sent-files'
|
|
4
|
+
import type { LaneWorktrees } from '../plans/unlisted-lanes'
|
|
4
5
|
|
|
5
6
|
export const WAITING_CLOSED_EVENT = 'waiting closed'
|
|
6
7
|
|
|
@@ -49,9 +50,10 @@ export const obsoleteSendsOf = (
|
|
|
49
50
|
records: readonly SentFileRecord[],
|
|
50
51
|
worktreeDirectory: string,
|
|
51
52
|
gone: ReadonlySet<string>,
|
|
53
|
+
placed: LaneWorktrees = {},
|
|
52
54
|
): Closing[] => {
|
|
53
55
|
const open = records.flatMap((record) => {
|
|
54
|
-
const lane = sentLaneOf(record, worktreeDirectory)
|
|
56
|
+
const lane = sentLaneOf(record, worktreeDirectory, placed)
|
|
55
57
|
return record.closedAt === null ? [{ id: record.id, lane }] : []
|
|
56
58
|
})
|
|
57
59
|
const openIds = new Set(open.map(({ id }) => id))
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { RunningSubagent } from '../plans/subagents'
|
|
2
|
-
import {
|
|
2
|
+
import { type LaneWorktrees, laneOfCwd } from '../plans/unlisted-lanes'
|
|
3
3
|
import { type LaneNotes, type PhaseCount, progressNotes } from './heartbeat-progress'
|
|
4
4
|
|
|
5
5
|
/** The running subagents of each worktree below `worktreeDirectory`, by lane name; one that works elsewhere is left out. */
|
|
6
6
|
export const runningSubagentCountsOf = (
|
|
7
7
|
running: readonly Pick<RunningSubagent, 'cwd'>[],
|
|
8
8
|
worktreeDirectory: string,
|
|
9
|
+
placed: LaneWorktrees = {},
|
|
9
10
|
): Record<string, number> => {
|
|
10
11
|
const counts: Record<string, number> = {}
|
|
11
12
|
for (const { cwd } of running) {
|
|
12
|
-
const lane = cwd === null ? null :
|
|
13
|
+
const lane = cwd === null ? null : laneOfCwd(cwd, worktreeDirectory, placed)
|
|
13
14
|
if (lane !== null) counts[lane] = (counts[lane] ?? 0) + 1
|
|
14
15
|
}
|
|
15
16
|
return counts
|
|
@@ -2,12 +2,15 @@ import { join } from 'node:path'
|
|
|
2
2
|
import { defaultProjectsDirectory } from '../context/session-log'
|
|
3
3
|
import { laneOfDirectory } from '../plans/active-tasks'
|
|
4
4
|
import { readAllSignals } from '../plans/signals-read'
|
|
5
|
+
import type { LaneWorktrees } from '../plans/unlisted-lanes'
|
|
5
6
|
|
|
6
7
|
export type SignalSources = {
|
|
7
8
|
/** The main checkout, which holds `.jackherd/tasks/`. */
|
|
8
9
|
root: string
|
|
9
10
|
/** The lanes' worktree directory, relative to `root`. */
|
|
10
11
|
worktreeDirectory: string
|
|
12
|
+
/** The lanes placed in worktrees of another name (`PlansSettings.laneWorktrees`). */
|
|
13
|
+
laneWorktrees?: LaneWorktrees
|
|
11
14
|
now: number
|
|
12
15
|
/** Where Claude Code keeps its session logs; the user's by default. */
|
|
13
16
|
projectsDirectory?: string
|
|
@@ -24,6 +27,6 @@ export const signalledLanes = (lanes: readonly string[], sources: SignalSources)
|
|
|
24
27
|
{ projects: sources.projectsDirectory ?? defaultProjectsDirectory(), root: sources.root, worktrees },
|
|
25
28
|
sources.now,
|
|
26
29
|
)
|
|
27
|
-
const signalled = new Set(signals.flatMap(({ cwd }) => laneOfDirectory(cwd, worktrees) ?? []))
|
|
30
|
+
const signalled = new Set(signals.flatMap(({ cwd }) => laneOfDirectory(cwd, worktrees, sources.laneWorktrees) ?? []))
|
|
28
31
|
return new Set(lanes.filter((lane) => signalled.has(lane)))
|
|
29
32
|
}
|
|
@@ -35,7 +35,9 @@ export const readUnlinkedArtifactEvents = (
|
|
|
35
35
|
const ids = new Set(artifacts.map(({ id }) => id))
|
|
36
36
|
const worktrees = join(root, settings.worktreeDirectory)
|
|
37
37
|
const lanes = new Set(
|
|
38
|
-
records
|
|
38
|
+
records
|
|
39
|
+
.filter(({ id }) => ids.has(id))
|
|
40
|
+
.flatMap((record) => sentLaneOf(record, worktrees, settings.laneWorktrees) ?? []),
|
|
39
41
|
)
|
|
40
42
|
const checkouts = [root, ...[...lanes].map((lane) => worktreeOf(root, settings, lane))]
|
|
41
43
|
const texts = checkouts.flatMap((checkout) => markdownTextsUnder(join(checkout, settings.planDirectory)))
|