jackherd 1.12.1 → 1.12.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +16 -16
  3. package/dist/plan-format.d.ts +23 -11
  4. package/dist/plan-format.js +6 -3
  5. package/herdr-plugin/herdr-plugin.toml +1 -1
  6. package/install.sh +1 -1
  7. package/kit/hooks/hooks/merge-guard/decision.ts +35 -2
  8. package/kit/workflow/skills/orchestrate/SKILL.md +7 -1
  9. package/kit/workflow/skills/orchestrate/references/events.md +4 -2
  10. package/package.json +1 -1
  11. package/src/check/skill-budget-defaults.ts +6 -3
  12. package/src/context/session-log.ts +4 -6
  13. package/src/plan-format/phases.ts +22 -6
  14. package/src/plan-format/progress.ts +5 -8
  15. package/src/plans/command.ts +10 -3
  16. package/src/plans/current-task.ts +3 -7
  17. package/src/plans/focus-overlay.ts +80 -0
  18. package/src/plans/git.ts +6 -11
  19. package/src/plans/herd-state.ts +201 -0
  20. package/src/plans/lane-reason.ts +1 -1
  21. package/src/plans/lane-summary.ts +6 -2
  22. package/src/plans/layout-phase.ts +1 -3
  23. package/src/plans/layout-summary.ts +1 -1
  24. package/src/plans/layout.ts +32 -31
  25. package/src/plans/link-chips.ts +7 -16
  26. package/src/plans/live.ts +215 -267
  27. package/src/plans/markdown-text.ts +4 -0
  28. package/src/plans/page-sync.ts +39 -0
  29. package/src/plans/phase-source.ts +5 -2
  30. package/src/plans/reader.ts +217 -0
  31. package/src/plans/refresh-schedule.ts +53 -0
  32. package/src/plans/snapshot.ts +32 -31
  33. package/src/plans/status-pace.ts +7 -4
  34. package/src/plans/subagents-read.ts +28 -12
  35. package/src/plans/supervisor.ts +1 -1
  36. package/src/plans/view.ts +9 -101
  37. package/src/plans/waiting-click.ts +27 -17
  38. package/src/plans/worked-phases.ts +1 -1
  39. package/src/plans/worktree-stamp.ts +12 -9
  40. package/src/plans/your-turn-rows.ts +33 -32
  41. package/src/plans/your-turn.ts +6 -4
  42. package/src/shared/bounded-memo.ts +19 -0
  43. package/src/shared/stat-cache.ts +2 -1
  44. package/src/task-signals/command.ts +12 -3
  45. package/src/terminal/lifetime.ts +52 -0
  46. package/src/watch/command.ts +6 -0
  47. package/src/watch/herd-publisher.ts +84 -0
  48. package/src/watch/source-entries.ts +7 -0
  49. package/src/watch/source-statuses.ts +2 -2
  50. package/src/watch/sources.ts +5 -2
  51. package/src/plans/next-rows.ts +0 -12
@@ -0,0 +1,84 @@
1
+ import { rmSync } from 'node:fs'
2
+ import { TERMINAL_ADAPTER } from '../config/schema'
3
+ import { errorText } from '../plans/error-text'
4
+ import {
5
+ type HerdRead,
6
+ herdFilesOf,
7
+ publishDecision,
8
+ publishedKey,
9
+ storedRead,
10
+ type Written,
11
+ writeHerdFile,
12
+ } from '../plans/herd-state'
13
+ import { initialPageState, syncSprintPage } from '../plans/page-sync'
14
+ import { startReader } from '../plans/reader'
15
+ import type { PlansSettings } from '../plans/settings'
16
+ import type { Snapshot, Source } from '../plans/snapshot'
17
+ import { laneStatusSourceFor } from '../terminal'
18
+ import { listHerdrAgents } from '../terminal/herdr'
19
+ import type { StartedKit } from '../version/started-kit'
20
+
21
+ /** What the publisher reads for: the watcher's checkout, board and settings. */
22
+ export type PublishedBoard = { root: string; sprintFile: string; settings: PlansSettings }
23
+
24
+ /** The board read as a herd without a lane reads it, with the terminal's agents read without holding the event loop. */
25
+ const sourceOf = ({ root, sprintFile, settings }: PublishedBoard): Source => ({
26
+ root,
27
+ sprintFile,
28
+ options: { boardPath: null, lane: null, once: false, ensure: false },
29
+ settings,
30
+ terminal: laneStatusSourceFor(settings.adapter),
31
+ ...(settings.adapter === TERMINAL_ADAPTER.herdr ? { listAgents: () => listHerdrAgents() } : {}),
32
+ })
33
+
34
+ /**
35
+ * Reads the board once for every herd of it and publishes the read (`herd-state.ts`): the read file only when its
36
+ * content changed, the plan texts only when they did, and the beat after every read that worked, so a herd reads for
37
+ * itself again once the watcher stops or fails for longer than `PUBLISHED_MAX_AGE_MS`. It writes the sprint page as
38
+ * well, so no herd builds it. Returns the stop.
39
+ */
40
+ export const startHerdPublisher = (board: PublishedBoard, kit: StartedKit): (() => void) => {
41
+ const source = sourceOf(board)
42
+ const key = publishedKey(board, kit)
43
+ const files = herdFilesOf(board.root)
44
+ const pageState = initialPageState()
45
+ const written: Written = { content: null, plansKey: null }
46
+ let last: { snapshot: Snapshot; fetchLine: string | null } | null = null
47
+ let stopped = false
48
+
49
+ /** Writes what changed and the beat; a failed write writes no beat, so the herds read for themselves soon. */
50
+ const publish = (): boolean => {
51
+ if (stopped || !last) return false
52
+ const { snapshot, fetchLine } = last
53
+ const stored: HerdRead = storedRead({ snapshot, page: pageState.page, fetchLine }, key, Date.now())
54
+ const { writePlans, writeRead, content } = publishDecision(written, stored)
55
+ try {
56
+ if (writePlans) writeHerdFile(files.plans, { plansKey: stored.plansKey, plans: snapshot.documents.plans })
57
+ written.plansKey = stored.plansKey
58
+ if (writeRead) writeHerdFile(files.read, stored)
59
+ written.content = content
60
+ writeHerdFile(files.beat, { key, at: stored.at })
61
+ } catch (error) {
62
+ console.log(`herd read not published: ${errorText(error)}`)
63
+ return false
64
+ }
65
+ return writeRead
66
+ }
67
+
68
+ // A beat of an earlier watcher must not vouch for its read until this one has read.
69
+ rmSync(files.beat, { force: true })
70
+ const reader = startReader(source, (outcome) => {
71
+ if ('error' in outcome) return false
72
+ last = { snapshot: outcome.read, fetchLine: reader.fetchLine() }
73
+ const changed = publish()
74
+ const { snapshot } = last
75
+ setTimeout(async () => {
76
+ if (await syncSprintPage(pageState, source, snapshot, async () => snapshot)) publish()
77
+ }, 0)
78
+ return changed
79
+ })
80
+ return () => {
81
+ stopped = true
82
+ reader.stop()
83
+ }
84
+ }
@@ -16,6 +16,12 @@ export const ENTRY_LIMIT = 20
16
16
 
17
17
  const shownAt = ({ changedAt, seenAt }: SourceEntry): number => changedAt ?? seenAt
18
18
 
19
+ /** The source's own time of an entry in epoch milliseconds; null for an unmapped `updatedAt` (`''`) or no date. */
20
+ const sourceTimeOf = (updatedAt: string): number | null => {
21
+ const time = updatedAt === '' ? Number.NaN : Date.parse(updatedAt)
22
+ return Number.isNaN(time) ? null : time
23
+ }
24
+
19
25
  const entryOf = (
20
26
  item: SourceItem,
21
27
  known: SourceEntry | undefined,
@@ -29,6 +35,7 @@ const entryOf = (
29
35
  count: item.count,
30
36
  seenAt: known?.seenAt ?? now,
31
37
  changedAt: seen && reportsItem(seen, item, settings) ? now : (known?.changedAt ?? null),
38
+ updatedAt: sourceTimeOf(item.updatedAt),
32
39
  })
33
40
 
34
41
  /**
@@ -6,7 +6,7 @@ import type { WatchState } from './watch-events'
6
6
  /** The newest entry a source reported, as the herd draws it; `isNew` while it is younger than `NEW_FOR_MS`. */
7
7
  export type LatestStatus = { title: string; url: string | null; isNew: boolean }
8
8
 
9
- /** An entry as the herd lists it under its source: `at` is when it last changed, else when the watcher first saw it; `isNew` only for a reported change younger than `NEW_FOR_MS`. */
9
+ /** An entry as the herd lists it under its source: `at` is the source's own time of it, else when it last changed, else when the watcher first saw it; `isNew` only for a reported change younger than `NEW_FOR_MS`. */
10
10
  export type EntryStatus = {
11
11
  id: string
12
12
  title: string
@@ -30,7 +30,7 @@ const entryStatusOf = (entry: SourceEntry, now: number): EntryStatus => ({
30
30
  title: lineText(entry.title),
31
31
  url: entry.url === null ? null : previewUrlOf(entry.url, undefined),
32
32
  count: entry.count,
33
- at: entry.changedAt ?? entry.seenAt,
33
+ at: entry.updatedAt ?? entry.changedAt ?? entry.seenAt,
34
34
  isNew: entry.changedAt !== null && now - entry.changedAt < NEW_FOR_MS,
35
35
  })
36
36
 
@@ -93,7 +93,8 @@ export type LatestReport = { title: string; url: string | null; at: number }
93
93
 
94
94
  /**
95
95
  * An entry of a source as the herd lists it: its title as one line, its checked URL, its count, when the watcher first
96
- * saw it (`seenAt`) and when it last reported a change of it (`changedAt`, null while only the baseline knows it).
96
+ * saw it (`seenAt`), when it last reported a change of it (`changedAt`, null while only the baseline knows it), and the
97
+ * source's own time of it (`map.updatedAt` read as a date; null when unmapped or no date, absent in older states).
97
98
  */
98
99
  export type SourceEntry = {
99
100
  id: string
@@ -102,6 +103,7 @@ export type SourceEntry = {
102
103
  count: string | null
103
104
  seenAt: number
104
105
  changedAt: number | null
106
+ updatedAt?: number | null
105
107
  }
106
108
 
107
109
  /**
@@ -419,7 +421,8 @@ const isSourceEntry = (value: unknown): boolean =>
419
421
  isTextOrNull(value.url) &&
420
422
  isTextOrNull(value.count) &&
421
423
  typeof value.seenAt === 'number' &&
422
- (value.changedAt === null || typeof value.changedAt === 'number')
424
+ (value.changedAt === null || typeof value.changedAt === 'number') &&
425
+ (value.updatedAt === undefined || value.updatedAt === null || typeof value.updatedAt === 'number')
423
426
 
424
427
  const isSourceRecord = (value: unknown): boolean =>
425
428
  isRecord(value) &&
@@ -1,12 +0,0 @@
1
- import { sectionLines } from '../plan-format/markdown'
2
- import { NEXT_VOCABULARY, type PlanLanguage } from '../vocabulary'
3
-
4
- const MAX_NEXT_LINES = 3
5
-
6
- const LIST_LINE = /^(?:\d+[.)]|[-*])\s+(.+)$/
7
-
8
- /** The board's next section, up to three list lines in the order written; the parser cuts after three, as the board says. */
9
- export const nextLines = (board: string, language: PlanLanguage): string[] =>
10
- sectionLines(board, NEXT_VOCABULARY[language].heading)
11
- .flatMap((line) => LIST_LINE.exec(line.trim())?.[1] ?? [])
12
- .slice(0, MAX_NEXT_LINES)