jackherd 1.11.2 → 1.12.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 +17 -0
- package/README.md +69 -51
- package/dist/{chunk-RMMXD4G5.js → chunk-V4HOLGFP.js} +2 -2
- package/dist/config.d.ts +10 -0
- package/dist/config.js +15 -5
- package/dist/plan-format.d.ts +11 -6
- package/dist/plan-format.js +8 -2
- package/docs/follow-dark.svg +2 -2
- package/docs/follow.svg +2 -2
- package/docs/workflow-dark.svg +56 -64
- package/docs/workflow.svg +56 -64
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/workflow/skills/implement/SKILL.md +1 -1
- package/kit/workflow/skills/orchestrate/SKILL.md +1 -1
- package/kit/workflow/skills/orchestrate/references/draft-review.md +4 -0
- package/kit/workflow/skills/orchestrate/references/events.md +8 -1
- package/kit/workflow/skills/plan/SKILL.md +2 -0
- package/package.json +2 -2
- package/schemas/jackherd.schema.json +18 -1
- package/src/config/link-kinds.ts +1 -1
- package/src/config/schema.ts +5 -2
- package/src/config/tones.ts +9 -2
- package/src/context/fill.ts +4 -10
- package/src/context/session-log.ts +108 -2
- package/src/plan-format/lanes.ts +10 -1
- package/src/plan-format/phases.ts +11 -3
- package/src/plan-format/progress.ts +4 -2
- package/src/plan-format/waiting.ts +12 -0
- package/src/plans/active-tasks.ts +31 -8
- package/src/plans/appointment-rows.ts +42 -53
- package/src/plans/board-rows.ts +14 -0
- package/src/plans/calendar-days.ts +29 -0
- package/src/plans/click-hint.ts +94 -0
- package/src/plans/command.ts +8 -9
- package/src/plans/current-phase.ts +22 -10
- package/src/plans/current-task.ts +18 -3
- package/src/plans/frame.ts +18 -26
- package/src/plans/git.ts +90 -31
- package/src/plans/groomed.ts +13 -7
- package/src/plans/lane-reason.ts +61 -0
- package/src/plans/lane-summary.ts +75 -24
- package/src/plans/layout-phase.ts +123 -76
- package/src/plans/layout-summary.ts +133 -83
- package/src/plans/layout.ts +451 -345
- package/src/plans/legend.ts +29 -0
- package/src/plans/link-chips.ts +26 -8
- package/src/plans/live.ts +238 -84
- package/src/plans/next-rows.ts +5 -37
- package/src/plans/phase-source.ts +8 -3
- package/src/plans/plan-chips.ts +6 -3
- package/src/plans/plan-heads.ts +1 -1
- package/src/plans/planned.ts +4 -1
- package/src/plans/process-slots.ts +36 -0
- package/src/plans/pull-requests.ts +9 -1
- package/src/plans/read-cache.ts +18 -3
- package/src/plans/refresh-pace.ts +13 -0
- package/src/plans/row-focus.ts +86 -15
- package/src/plans/selection.ts +124 -89
- package/src/plans/sent-files.ts +1 -1
- package/src/plans/settings.ts +5 -2
- package/src/plans/shimmer.ts +124 -0
- package/src/plans/signals-read.ts +38 -10
- package/src/plans/slice-gate.ts +51 -0
- package/src/plans/snapshot.ts +185 -84
- package/src/plans/source-asked.ts +1 -1
- package/src/plans/source-rows.ts +60 -18
- package/src/plans/sprint-html.ts +1 -1
- package/src/plans/status-pace.ts +45 -0
- package/src/plans/subagent-tasks.ts +1 -1
- package/src/plans/subagents-read.ts +61 -24
- package/src/plans/subagents.ts +2 -2
- package/src/plans/symbols.ts +4 -1
- package/src/plans/task-brief.ts +143 -0
- package/src/plans/texts.ts +181 -39
- package/src/plans/view.ts +198 -32
- package/src/plans/waiting-click.ts +54 -110
- package/src/plans/watch-burst.ts +25 -0
- package/src/plans/worktree-stamp.ts +35 -0
- package/src/plans/your-turn-rows.ts +102 -0
- package/src/plans/your-turn.ts +226 -0
- package/src/terminal/herdr.ts +25 -5
- package/src/terminal/restore.ts +44 -0
- package/src/terminal/text.ts +2 -2
- package/src/terminal/width.ts +9 -0
- package/src/vocabulary/index.ts +6 -5
- package/src/watch/command.ts +28 -2
- package/src/watch/lane-report.ts +2 -0
- package/src/watch/lane-rest.ts +7 -1
- package/src/watch/reference-paths.ts +1 -0
- package/src/watch/unlinked-artifacts-read.ts +43 -0
- package/src/watch/unlinked-artifacts.ts +76 -0
- package/src/watch/watch-events.ts +12 -0
- package/src/watch/watcher-lock.ts +47 -4
|
@@ -30,6 +30,12 @@ import {
|
|
|
30
30
|
type NoSignalState,
|
|
31
31
|
type NoTaskSignalEvent,
|
|
32
32
|
} from './task-signal-missing'
|
|
33
|
+
import {
|
|
34
|
+
isUnlinkedState,
|
|
35
|
+
UNLINKED_ARTIFACT_EVENT,
|
|
36
|
+
type UnlinkedArtifactEvent,
|
|
37
|
+
type UnlinkedState,
|
|
38
|
+
} from './unlinked-artifacts'
|
|
33
39
|
import { RUN_EVENT, RUN_STATUS, type WorkflowRun } from './workflow-runs'
|
|
34
40
|
|
|
35
41
|
/** The event `updateAvailableLine` prints; it is no `WatchEvent`, the announcer logs it on its own. */
|
|
@@ -51,6 +57,7 @@ export const WATCH_EVENT = {
|
|
|
51
57
|
laneParallel: LANE_PARALLEL_EVENT,
|
|
52
58
|
taskParallel: TASK_PARALLEL_EVENT,
|
|
53
59
|
noTaskSignal: NO_TASK_SIGNAL_EVENT,
|
|
60
|
+
unlinkedArtifact: UNLINKED_ARTIFACT_EVENT,
|
|
54
61
|
approval: APPROVAL_EVENT,
|
|
55
62
|
source: SOURCE_EVENT,
|
|
56
63
|
due: APPOINTMENT_EVENT.due,
|
|
@@ -109,6 +116,8 @@ export type WatchState = {
|
|
|
109
116
|
parallelAutoEnded: string | null
|
|
110
117
|
/** Per watched lane: since when it works without a task signal and the phase last reported as `no task signal`. */
|
|
111
118
|
noTaskSignals: NoSignalState
|
|
119
|
+
/** Per open artifact send no plan or board links: in its grace or already reported as `unlinked artifact`. */
|
|
120
|
+
unlinkedArtifacts: UnlinkedState
|
|
112
121
|
/** The ids of the approval requests on disk when they were last read, each reported once while it is open. */
|
|
113
122
|
approvals: string[]
|
|
114
123
|
/** Per configured source of `watch.sources`: the entries seen (`[id, fingerprint]`) and the start of a failure streak. */
|
|
@@ -166,6 +175,7 @@ export type WatchEvent =
|
|
|
166
175
|
| LaneParallelEvent
|
|
167
176
|
| TaskParallelEvent
|
|
168
177
|
| NoTaskSignalEvent
|
|
178
|
+
| UnlinkedArtifactEvent
|
|
169
179
|
| ApprovalEvent
|
|
170
180
|
| SourceEvent
|
|
171
181
|
| AppointmentEvent
|
|
@@ -203,6 +213,7 @@ export const emptyWatchState = (): WatchState => ({
|
|
|
203
213
|
taskParallels: {},
|
|
204
214
|
parallelAutoEnded: null,
|
|
205
215
|
noTaskSignals: {},
|
|
216
|
+
unlinkedArtifacts: {},
|
|
206
217
|
approvals: [],
|
|
207
218
|
sources: emptySourceState(),
|
|
208
219
|
reportMtimes: {},
|
|
@@ -252,6 +263,7 @@ const isWatchState = (value: Record<string, unknown>): value is WatchState =>
|
|
|
252
263
|
isTaskParallelFingerprints(value.taskParallels) &&
|
|
253
264
|
(value.parallelAutoEnded === null || typeof value.parallelAutoEnded === 'string') &&
|
|
254
265
|
isNoSignalState(value.noTaskSignals) &&
|
|
266
|
+
isUnlinkedState(value.unlinkedArtifacts) &&
|
|
255
267
|
Array.isArray(value.approvals) &&
|
|
256
268
|
value.approvals.every((id) => typeof id === 'string') &&
|
|
257
269
|
isSourceState(value.sources) &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import { rmSync } from 'node:fs'
|
|
2
|
+
import { readFileSync, rmSync } from 'node:fs'
|
|
3
3
|
import { basename, dirname, join } from 'node:path'
|
|
4
4
|
import { writePrivateFile } from '../../kit/hooks/hooks/shared/private-directory'
|
|
5
5
|
import {
|
|
@@ -79,11 +79,51 @@ export const repositoryKey = (root: string): string => {
|
|
|
79
79
|
return `${name}-${hash}`
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
/** How long the herd trusts a command line `ps` gave for a process ID that is still alive. */
|
|
83
|
+
const PS_CACHE_MS = 30_000
|
|
84
|
+
|
|
85
|
+
/** The command line from `/proc/<pid>/cmdline`, its arguments blank-separated; null where there is no `/proc` or no such process. */
|
|
86
|
+
export const procCommandLine: CommandLineOf = (pid) => {
|
|
87
|
+
try {
|
|
88
|
+
return readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').filter(Boolean).join(' ') || null
|
|
89
|
+
} catch {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const psCommandLine: CommandLineOf = (pid) => {
|
|
83
95
|
const result = Bun.spawnSync(['ps', '-o', 'command=', '-p', String(pid)], { stdout: 'pipe', stderr: 'ignore' })
|
|
84
96
|
return result.exitCode === 0 ? result.stdout.toString().trim() || null : null
|
|
85
97
|
}
|
|
86
98
|
|
|
99
|
+
/**
|
|
100
|
+
* `lookUp` with each found command line kept per process ID for `ttlMs`; a miss is asked again. Only for readers:
|
|
101
|
+
* a process ID is reused after its process ends, so what may be signalled is always looked up afresh.
|
|
102
|
+
*/
|
|
103
|
+
export const cachedCommandLineOf = (
|
|
104
|
+
lookUp: CommandLineOf,
|
|
105
|
+
ttlMs: number = PS_CACHE_MS,
|
|
106
|
+
clock: () => number = Date.now,
|
|
107
|
+
): CommandLineOf => {
|
|
108
|
+
const found = new Map<number, { line: string; at: number }>()
|
|
109
|
+
return (pid) => {
|
|
110
|
+
const kept = found.get(pid)
|
|
111
|
+
if (kept && clock() - kept.at < ttlMs) return kept.line
|
|
112
|
+
const line = lookUp(pid)
|
|
113
|
+
if (line === null) found.delete(pid)
|
|
114
|
+
else found.set(pid, { line, at: clock() })
|
|
115
|
+
return line
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** `/proc` where it exists (no process started), else `ps`: what decides whom `takeOverLock` signals. */
|
|
120
|
+
const commandLineOf: CommandLineOf = (pid) => procCommandLine(pid) ?? psCommandLine(pid)
|
|
121
|
+
|
|
122
|
+
const cachedPsCommandLine = cachedCommandLineOf(psCommandLine)
|
|
123
|
+
|
|
124
|
+
/** What the herd asks every few seconds: `/proc`, else `ps` kept per process ID, since one `ps` blocks for tens of milliseconds. */
|
|
125
|
+
const readerCommandLineOf: CommandLineOf = (pid) => procCommandLine(pid) ?? cachedPsCommandLine(pid)
|
|
126
|
+
|
|
87
127
|
const isAlive = (pid: number): boolean => {
|
|
88
128
|
try {
|
|
89
129
|
process.kill(pid, 0)
|
|
@@ -109,8 +149,11 @@ export const ensureWatcherDirectory = (root: string, system: LocationSystem = sy
|
|
|
109
149
|
return ensurePrivateLevels(dirname(directory), directory)
|
|
110
150
|
}
|
|
111
151
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Whether the lock names a live process that is a watcher, so its saved state is being kept up to date. The process
|
|
154
|
+
* is checked to be alive on every call, so a kept command line never shows a watcher that has ended.
|
|
155
|
+
*/
|
|
156
|
+
export const isWatcherRunning = (lockFile: string, lookUp: CommandLineOf = readerCommandLineOf): boolean => {
|
|
114
157
|
const pid = lockedPid(readPrivateText(lockFile))
|
|
115
158
|
return pid !== null && isAlive(pid) && isWatcherCommand(lookUp(pid) ?? '')
|
|
116
159
|
}
|