jackherd 1.11.1 → 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.
Files changed (94) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +76 -58
  3. package/dist/{chunk-RMMXD4G5.js → chunk-V4HOLGFP.js} +2 -2
  4. package/dist/config.d.ts +10 -0
  5. package/dist/config.js +15 -5
  6. package/dist/plan-format.d.ts +11 -6
  7. package/dist/plan-format.js +8 -2
  8. package/docs/follow-dark.svg +2 -2
  9. package/docs/follow.svg +2 -2
  10. package/docs/workflow-dark.svg +56 -64
  11. package/docs/workflow.svg +56 -64
  12. package/herdr-plugin/herdr-plugin.toml +1 -1
  13. package/install.sh +1 -1
  14. package/kit/workflow/skills/implement/SKILL.md +1 -1
  15. package/kit/workflow/skills/orchestrate/SKILL.md +1 -1
  16. package/kit/workflow/skills/orchestrate/references/draft-review.md +4 -0
  17. package/kit/workflow/skills/orchestrate/references/events.md +8 -1
  18. package/kit/workflow/skills/plan/SKILL.md +2 -0
  19. package/package.json +2 -2
  20. package/schemas/jackherd.schema.json +18 -1
  21. package/src/config/link-kinds.ts +1 -1
  22. package/src/config/schema.ts +5 -2
  23. package/src/config/tones.ts +9 -2
  24. package/src/context/fill.ts +4 -10
  25. package/src/context/session-log.ts +108 -2
  26. package/src/plan-format/lanes.ts +10 -1
  27. package/src/plan-format/phases.ts +11 -3
  28. package/src/plan-format/progress.ts +4 -2
  29. package/src/plan-format/waiting.ts +12 -0
  30. package/src/plans/active-tasks.ts +31 -8
  31. package/src/plans/appointment-rows.ts +42 -53
  32. package/src/plans/board-rows.ts +14 -0
  33. package/src/plans/calendar-days.ts +29 -0
  34. package/src/plans/click-hint.ts +94 -0
  35. package/src/plans/command.ts +8 -9
  36. package/src/plans/current-phase.ts +22 -10
  37. package/src/plans/current-task.ts +18 -3
  38. package/src/plans/frame.ts +18 -26
  39. package/src/plans/git.ts +90 -31
  40. package/src/plans/groomed.ts +13 -7
  41. package/src/plans/lane-reason.ts +61 -0
  42. package/src/plans/lane-summary.ts +75 -24
  43. package/src/plans/layout-phase.ts +123 -76
  44. package/src/plans/layout-summary.ts +133 -83
  45. package/src/plans/layout.ts +451 -345
  46. package/src/plans/legend.ts +29 -0
  47. package/src/plans/link-chips.ts +26 -8
  48. package/src/plans/live.ts +238 -84
  49. package/src/plans/next-rows.ts +5 -37
  50. package/src/plans/phase-source.ts +8 -3
  51. package/src/plans/plan-chips.ts +6 -3
  52. package/src/plans/plan-heads.ts +1 -1
  53. package/src/plans/planned.ts +4 -1
  54. package/src/plans/process-slots.ts +36 -0
  55. package/src/plans/pull-requests.ts +9 -1
  56. package/src/plans/read-cache.ts +18 -3
  57. package/src/plans/refresh-pace.ts +13 -0
  58. package/src/plans/row-focus.ts +86 -15
  59. package/src/plans/selection.ts +124 -89
  60. package/src/plans/sent-files.ts +1 -1
  61. package/src/plans/settings.ts +5 -2
  62. package/src/plans/shimmer.ts +124 -0
  63. package/src/plans/signals-read.ts +38 -10
  64. package/src/plans/slice-gate.ts +51 -0
  65. package/src/plans/snapshot.ts +185 -84
  66. package/src/plans/source-asked.ts +1 -1
  67. package/src/plans/source-rows.ts +60 -18
  68. package/src/plans/sprint-html.ts +1 -1
  69. package/src/plans/status-pace.ts +45 -0
  70. package/src/plans/subagent-tasks.ts +1 -1
  71. package/src/plans/subagents-read.ts +61 -24
  72. package/src/plans/subagents.ts +2 -2
  73. package/src/plans/symbols.ts +4 -1
  74. package/src/plans/task-brief.ts +143 -0
  75. package/src/plans/texts.ts +181 -39
  76. package/src/plans/view.ts +198 -32
  77. package/src/plans/waiting-click.ts +54 -110
  78. package/src/plans/watch-burst.ts +25 -0
  79. package/src/plans/worktree-stamp.ts +35 -0
  80. package/src/plans/your-turn-rows.ts +102 -0
  81. package/src/plans/your-turn.ts +226 -0
  82. package/src/terminal/herdr.ts +25 -5
  83. package/src/terminal/restore.ts +44 -0
  84. package/src/terminal/text.ts +2 -2
  85. package/src/terminal/width.ts +9 -0
  86. package/src/vocabulary/index.ts +6 -5
  87. package/src/watch/command.ts +28 -2
  88. package/src/watch/lane-report.ts +2 -0
  89. package/src/watch/lane-rest.ts +7 -1
  90. package/src/watch/reference-paths.ts +1 -0
  91. package/src/watch/unlinked-artifacts-read.ts +43 -0
  92. package/src/watch/unlinked-artifacts.ts +76 -0
  93. package/src/watch/watch-events.ts +12 -0
  94. 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
- const commandLineOf: CommandLineOf = (pid) => {
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
- /** Whether the lock names a live process that is a watcher, so its saved state is being kept up to date. */
113
- export const isWatcherRunning = (lockFile: string, lookUp: CommandLineOf = commandLineOf): boolean => {
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
  }