jackherd 1.9.0 → 1.11.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 (228) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/INSTALL.md +1 -1
  3. package/README.md +65 -32
  4. package/dist/{chunk-MCP4XJQH.js → chunk-RMMXD4G5.js} +18 -6
  5. package/dist/config.d.ts +99 -4
  6. package/dist/config.js +105 -29
  7. package/dist/plan-format.d.ts +51 -6
  8. package/dist/plan-format.js +49 -7
  9. package/docs/workflow-dark.svg +67 -63
  10. package/docs/workflow.svg +67 -63
  11. package/herdr-plugin/herdr-plugin.toml +1 -1
  12. package/install.sh +1 -1
  13. package/kit/hooks/ask-rules.json +15 -0
  14. package/kit/hooks/hooks/ask-hint/decision.ts +82 -14
  15. package/kit/hooks/hooks/ask-hint/index.ts +15 -19
  16. package/kit/hooks/hooks/ask-hint/settings.ts +3 -18
  17. package/kit/hooks/hooks/git-guard/decision.ts +50 -102
  18. package/kit/hooks/hooks/merge-guard/always-ask.ts +63 -0
  19. package/kit/hooks/hooks/merge-guard/{plan-approval-settings.ts → board-settings.ts} +21 -67
  20. package/kit/hooks/hooks/merge-guard/check-free.ts +20 -0
  21. package/kit/hooks/hooks/merge-guard/decision.ts +127 -187
  22. package/kit/hooks/hooks/merge-guard/index.ts +6 -3
  23. package/kit/hooks/hooks/merge-guard/lane-plan.ts +79 -0
  24. package/kit/hooks/hooks/merge-guard/run.ts +59 -54
  25. package/kit/hooks/hooks/merge-guard/settings.ts +13 -1
  26. package/kit/hooks/hooks/merge-guard/standing-approval.ts +142 -0
  27. package/kit/hooks/hooks/open-question/index.ts +26 -0
  28. package/kit/hooks/hooks/open-question/record.ts +135 -0
  29. package/kit/hooks/hooks/open-question/run.ts +57 -0
  30. package/kit/hooks/hooks/open-question/store.ts +39 -0
  31. package/kit/hooks/hooks/sent-file/store.ts +3 -12
  32. package/kit/hooks/hooks/shared/config.ts +7 -0
  33. package/kit/hooks/hooks/shared/dialog-answers.ts +9 -9
  34. package/kit/hooks/hooks/shared/files.ts +7 -0
  35. package/kit/hooks/hooks/shared/lane.ts +1 -1
  36. package/kit/hooks/hooks/shared/newest-verdict.ts +10 -0
  37. package/kit/hooks/hooks/shared/private-directory.ts +56 -9
  38. package/kit/hooks/hooks/shared/program-position.ts +2 -75
  39. package/kit/hooks/hooks/shared/shell-aliases.ts +1 -71
  40. package/kit/hooks/hooks/{ask-hint → shared}/transcript.ts +37 -6
  41. package/kit/hooks/hooks/{merge-guard → shared}/xargs-program.ts +10 -8
  42. package/kit/hooks/hooks/task-hint/decision.ts +49 -0
  43. package/kit/hooks/hooks/task-hint/index.ts +79 -0
  44. package/kit/hooks/hooks/task-hint/run.ts +62 -0
  45. package/kit/hooks/hooks/task-hint/store.ts +51 -0
  46. package/kit/hooks/hooks.json +13 -5
  47. package/kit/review/agents/skill-review.md +34 -0
  48. package/kit/review/skills/pre-commit/SKILL.md +1 -1
  49. package/kit/rules/rules/kit/pull-requests.md +2 -0
  50. package/kit/workflow/skills/follow/SKILL.md +3 -10
  51. package/kit/workflow/skills/implement/SKILL.md +8 -6
  52. package/kit/workflow/skills/orchestrate/SKILL.md +218 -433
  53. package/kit/workflow/skills/orchestrate/handover/decision.ts +43 -8
  54. package/kit/workflow/skills/orchestrate/handover/index.ts +32 -4
  55. package/kit/workflow/skills/orchestrate/handover/standing-approval.ts +84 -0
  56. package/kit/workflow/skills/orchestrate/references/acceptance.md +15 -0
  57. package/kit/workflow/skills/orchestrate/references/close-lane.md +17 -0
  58. package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +43 -17
  59. package/kit/workflow/skills/orchestrate/references/draft-review.md +19 -0
  60. package/kit/workflow/skills/orchestrate/references/events.md +86 -0
  61. package/kit/workflow/skills/orchestrate/references/herd.md +6 -6
  62. package/kit/workflow/skills/orchestrate/references/lane-handover.md +18 -0
  63. package/kit/workflow/skills/orchestrate/references/lane-session.md +25 -0
  64. package/kit/workflow/skills/orchestrate/references/lane-setup.md +37 -0
  65. package/kit/workflow/skills/orchestrate/references/plan-commit.md +22 -10
  66. package/kit/workflow/skills/orchestrate/references/sensitive-input.md +5 -7
  67. package/kit/workflow/skills/plan/SKILL.md +6 -5
  68. package/kit/workflow/skills/plan/references/template-English.md +4 -4
  69. package/kit/workflow/skills/plan/references/template-German.md +4 -4
  70. package/package.json +3 -1
  71. package/recipes/README.md +2 -1
  72. package/recipes/sources.md +5 -1
  73. package/schemas/jackherd.schema.json +139 -5
  74. package/src/check/agent-text-reviewers.ts +23 -0
  75. package/src/check/command.ts +2 -0
  76. package/src/check/plan-structure-check.ts +49 -2
  77. package/src/check/plan.ts +8 -1
  78. package/src/check/schema.ts +9 -0
  79. package/src/check/skill-budget-defaults.ts +55 -0
  80. package/src/check/skill-budgets.ts +185 -0
  81. package/src/cli.ts +2 -0
  82. package/src/config/auto-window.ts +8 -0
  83. package/src/config/link-kinds.ts +7 -1
  84. package/src/config/schema.ts +82 -25
  85. package/src/config/tones.ts +27 -0
  86. package/src/lane/close-board.ts +106 -0
  87. package/src/lane/close-check.ts +183 -0
  88. package/src/lane/close-command-line.ts +11 -0
  89. package/src/lane/close-command.ts +217 -0
  90. package/src/lane/close-options.ts +43 -0
  91. package/src/lane/close-survey.ts +83 -0
  92. package/src/lane/close.ts +141 -0
  93. package/src/lane/command.ts +13 -0
  94. package/src/lane-prompt/additions.ts +14 -0
  95. package/src/lane-prompt/build.ts +74 -0
  96. package/src/lane-prompt/command.ts +93 -0
  97. package/src/lane-prompt/input.ts +157 -0
  98. package/src/lane-prompt/options.ts +70 -0
  99. package/src/lane-prompt/texts.ts +101 -0
  100. package/src/mermaid/command.ts +36 -0
  101. package/src/mermaid/problems.ts +48 -0
  102. package/src/plan-format/clock.ts +8 -0
  103. package/src/plan-format/dependencies.ts +9 -6
  104. package/src/plan-format/index.ts +1 -0
  105. package/src/plan-format/short-names.ts +71 -0
  106. package/src/plan-format/waiting.ts +22 -4
  107. package/src/plans/appointment-rows.ts +83 -0
  108. package/src/plans/ask-coordinator.ts +111 -0
  109. package/src/plans/ask-target.ts +36 -0
  110. package/src/plans/board-close.ts +85 -0
  111. package/src/plans/child-stop.ts +51 -0
  112. package/src/plans/closable-records.ts +34 -0
  113. package/src/plans/command.ts +3 -2
  114. package/src/plans/fetch.ts +26 -3
  115. package/src/plans/frame.ts +5 -12
  116. package/src/plans/git.ts +54 -5
  117. package/src/plans/groomed.ts +27 -0
  118. package/src/plans/herd-claim.ts +173 -0
  119. package/src/plans/herd-memory.ts +36 -25
  120. package/src/plans/lane-plans.ts +44 -13
  121. package/src/plans/lane-summary.ts +253 -45
  122. package/src/plans/layout-phase.ts +36 -11
  123. package/src/plans/layout-summary.ts +89 -18
  124. package/src/plans/layout.ts +502 -98
  125. package/src/plans/link-chips.ts +23 -4
  126. package/src/plans/live.ts +178 -23
  127. package/src/plans/next-rows.ts +44 -0
  128. package/src/plans/phase-ids.ts +8 -0
  129. package/src/plans/phase-source.ts +15 -0
  130. package/src/plans/plan-cache.ts +28 -0
  131. package/src/plans/plan-heads.ts +54 -0
  132. package/src/plans/planned.ts +22 -0
  133. package/src/plans/pull-requests.ts +17 -2
  134. package/src/plans/question-records.ts +48 -0
  135. package/src/plans/questions-read.ts +13 -0
  136. package/src/plans/read-cache.ts +8 -0
  137. package/src/plans/record-id.ts +4 -0
  138. package/src/plans/restart-decision.ts +13 -4
  139. package/src/plans/row-actions.ts +52 -0
  140. package/src/plans/row-focus.ts +148 -0
  141. package/src/plans/row-targets.ts +7 -0
  142. package/src/plans/selection.ts +111 -18
  143. package/src/plans/sent-files-read.ts +6 -15
  144. package/src/plans/sent-files.ts +7 -14
  145. package/src/plans/settings.ts +12 -1
  146. package/src/plans/signalled-phase.ts +42 -0
  147. package/src/plans/signals-read.ts +12 -4
  148. package/src/plans/snapshot.ts +162 -81
  149. package/src/plans/source-asked.ts +19 -0
  150. package/src/plans/source-prefix.ts +17 -0
  151. package/src/plans/source-rows.ts +188 -0
  152. package/src/plans/sprint-export.ts +11 -11
  153. package/src/plans/sprint-page.ts +5 -5
  154. package/src/plans/subagent-phases.ts +95 -0
  155. package/src/plans/subagent-tasks.ts +51 -0
  156. package/src/plans/subagents-read.ts +66 -17
  157. package/src/plans/subagents.ts +64 -6
  158. package/src/plans/supervisor.ts +14 -4
  159. package/src/plans/symbols.ts +3 -0
  160. package/src/plans/task-name.ts +34 -0
  161. package/src/plans/texts.ts +48 -6
  162. package/src/plans/tones.ts +30 -0
  163. package/src/plans/view.ts +25 -11
  164. package/src/plans/waiting-click.ts +295 -35
  165. package/src/plans/waiting-records.ts +39 -0
  166. package/src/plans/waiting-window.ts +8 -0
  167. package/src/preview/command.ts +25 -3
  168. package/src/shared/private-location.ts +89 -0
  169. package/src/shared/stat-cache.ts +48 -0
  170. package/src/sync/kit-source.ts +29 -10
  171. package/src/sync/kit.ts +2 -0
  172. package/src/sync/manifest.ts +2 -0
  173. package/src/sync/plan-ask-rules.ts +94 -0
  174. package/src/sync/plan-hooks.ts +2 -0
  175. package/src/sync/render.ts +3 -3
  176. package/src/sync/settings-hooks.ts +4 -1
  177. package/src/sync/sync-plan.ts +1 -0
  178. package/src/sync/sync.ts +24 -3
  179. package/src/task-signals/claude-read.ts +27 -10
  180. package/src/terminal/text.ts +16 -3
  181. package/src/terminal/types.ts +4 -0
  182. package/src/version/skill-diff.ts +46 -0
  183. package/src/vocabulary/index.ts +26 -6
  184. package/src/waiting/command.ts +22 -7
  185. package/src/watch/appointments.ts +1 -2
  186. package/src/watch/asking-lanes.ts +40 -0
  187. package/src/watch/base-tip.ts +11 -0
  188. package/src/watch/command.ts +330 -105
  189. package/src/watch/heartbeat-progress.ts +6 -18
  190. package/src/watch/kit-update.ts +28 -9
  191. package/src/watch/lane-name.ts +6 -0
  192. package/src/watch/lane-progress.ts +40 -8
  193. package/src/watch/lane-report-file.ts +20 -5
  194. package/src/watch/lane-report.ts +32 -4
  195. package/src/watch/lane-sources.ts +31 -3
  196. package/src/watch/main-checkout.ts +34 -0
  197. package/src/watch/plan-badges-read.ts +22 -9
  198. package/src/watch/plan-badges.ts +10 -2
  199. package/src/watch/pull-requests.ts +6 -2
  200. package/src/watch/reference-paths.ts +87 -0
  201. package/src/watch/report-questions.ts +4 -3
  202. package/src/watch/running-subagents.ts +43 -0
  203. package/src/watch/skill-change.ts +124 -32
  204. package/src/watch/source-change.ts +48 -0
  205. package/src/watch/source-entries.ts +55 -0
  206. package/src/watch/source-fields.ts +1 -0
  207. package/src/watch/source-statuses.ts +60 -0
  208. package/src/watch/source-tickets.ts +21 -1
  209. package/src/watch/sources.ts +47 -4
  210. package/src/watch/stream-output.ts +4 -3
  211. package/src/watch/task-parallel-auto.ts +208 -0
  212. package/src/watch/task-parallel-data-paths.ts +46 -0
  213. package/src/watch/task-parallel-input.ts +5 -15
  214. package/src/watch/task-parallel-read.ts +3 -1
  215. package/src/watch/task-parallel.ts +36 -7
  216. package/src/watch/task-signal-missing.ts +69 -0
  217. package/src/watch/task-signal-read.ts +29 -0
  218. package/src/watch/texts.ts +10 -0
  219. package/src/watch/update-available.ts +6 -3
  220. package/src/watch/watch-events.ts +23 -0
  221. package/src/watch/watcher-lock.ts +25 -29
  222. package/kit/hooks/hooks/merge-guard/plan-approval-record.ts +0 -23
  223. package/kit/hooks/hooks/merge-guard/plan-approval-sources.ts +0 -196
  224. package/kit/hooks/hooks/merge-guard/plan-approval.ts +0 -168
  225. package/kit/hooks/hooks/merge-guard/plan-growth.ts +0 -40
  226. package/kit/hooks/hooks/merge-guard/plan-pointers.ts +0 -194
  227. package/kit/hooks/hooks/shared/command-text-runners.ts +0 -71
  228. package/kit/hooks/hooks/shared/plan-open-phases.ts +0 -114
@@ -4,6 +4,7 @@ import { dirname } from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { pendingKitUpdate, VERSION_POLL_MS } from '../version/installed-version'
6
6
  import type { StartedKit } from '../version/started-kit'
7
+ import { STOP_REQUEST, type Stopper, stopperOf } from './child-stop'
7
8
  import { HERD_COMMAND } from './herd-command'
8
9
  import { CHILD_MARKER, exitCodeOf, isRestartTrigger, nextStep, RESTART_DEBOUNCE_MS } from './restart-decision'
9
10
  import { TERMINAL_OFF } from './view'
@@ -30,7 +31,8 @@ const spawnView = (scriptPath: string, args: string[]): ChildProcess =>
30
31
  /**
31
32
  * Runs the live view as a child process and restarts it when the package's own source changes,
32
33
  * so a long-lived `plans` never shows the behaviour of code that has since been edited. The child
33
- * stops on SIGTERM through the handler that restores the terminal. Resolves with the exit code
34
+ * stops on SIGTERM through the handler that restores the terminal, and is killed when it does not end within
35
+ * `GRACE_MS` (a restart included); a second signal while stopping kills it at once. Resolves with the exit code
34
36
  * of a wanted end only: the child's clean exit (`q`) or a signal forwarded to it. A child that
35
37
  * dies otherwise leaves the supervisor waiting, with the terminal restored, for the next change
36
38
  * to `src/`, which spawns a fresh child. `kit` is the package this process loaded (null when it cannot be
@@ -45,6 +47,7 @@ export const superviseView = (
45
47
  ): Promise<number> =>
46
48
  new Promise((resolveExit) => {
47
49
  let child: ChildProcess | undefined
50
+ let stopper: Stopper | undefined
48
51
  let restarting = false
49
52
  let stopping = false
50
53
  let timer: ReturnType<typeof setTimeout> | undefined
@@ -66,6 +69,12 @@ export const superviseView = (
66
69
  const start = (isFirst: boolean): void => {
67
70
  const current = spawnView(scriptPath, args)
68
71
  child = current
72
+ const currentStopper = stopperOf({
73
+ kill: (signal) => current.kill(signal),
74
+ setTimer: (run, ms) => setTimeout(run, ms),
75
+ clearTimer: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
76
+ })
77
+ stopper = currentStopper
69
78
 
70
79
  current.on('error', (error) => {
71
80
  if (child !== current) return
@@ -79,6 +88,7 @@ export const superviseView = (
79
88
  })
80
89
 
81
90
  current.on('exit', (code, signal) => {
91
+ currentStopper.ended()
82
92
  if (child !== current) return
83
93
  const context = { restarting, stopping, code, signal }
84
94
  restarting = false
@@ -94,7 +104,7 @@ export const superviseView = (
94
104
  const restartView = (): void => {
95
105
  if (child) {
96
106
  restarting = true
97
- child.kill('SIGTERM')
107
+ stopper?.request('SIGTERM')
98
108
  } else start(false)
99
109
  }
100
110
 
@@ -118,8 +128,8 @@ export const superviseView = (
118
128
  stopping = true
119
129
  clearTimeout(timer)
120
130
  clearInterval(versionTimer)
121
- if (child) child.kill(signal)
122
- else finish(exitCodeOf(null, signal))
131
+ if (!child || !stopper) finish(exitCodeOf(null, signal))
132
+ else if (stopper.request(signal) === STOP_REQUEST.killed) finish(exitCodeOf(null, signal))
123
133
  })
124
134
  }
125
135
  })
@@ -21,6 +21,9 @@ export const statusWord = (agent: string | null): string => agent ?? NO_AGENT_WO
21
21
  /** Before a row that opens and closes: lanes, the planned phases, the ticked items of a phase. */
22
22
  export const CHEVRON = { closed: '▸', open: '▾' } as const
23
23
 
24
+ /** Before the count under a block that shows all its rows; a click folds it back to five. */
25
+ export const LESS_MARK = '▴'
26
+
24
27
  export const chevronOf = (open: boolean): string => (open ? CHEVRON.open : CHEVRON.closed)
25
28
 
26
29
  export const PHASE_STATE = { missing: 'missing', empty: 'empty', all: 'all', open: 'open' } as const
@@ -0,0 +1,34 @@
1
+ const TASK_NAME = /^\*\*([^*]+)\*\*:/
2
+
3
+ const TITLE_END = ': '
4
+
5
+ const titleEndOf = (text: string): number => {
6
+ let inCode = false
7
+ for (let index = 0; index < text.length; index++) {
8
+ if (text[index] === '`') inCode = !inCode
9
+ else if (!inCode && text.startsWith(TITLE_END, index)) return index
10
+ }
11
+ return -1
12
+ }
13
+
14
+ /**
15
+ * The text up to its first colon followed by a space outside backticks, as a folded row shows it; the text as it is
16
+ * without such a colon or with nothing before it. A time like `18:55` has no space after its colon and does not split.
17
+ */
18
+ export const shortTitle = (text: string): string => {
19
+ const end = titleEndOf(text)
20
+ return end > 0 ? text.slice(0, end) : text
21
+ }
22
+
23
+ /**
24
+ * The short name a task carries in bold at its start, `**Name**: content`, as written; null for a line without one.
25
+ * Nothing is guessed from the text up to its first colon: a path at the start of a line would read as a name.
26
+ */
27
+ export const taskNameOf = (text: string): string | null => {
28
+ const name = text.match(TASK_NAME)?.[1]?.trim()
29
+ return name ? name : null
30
+ }
31
+
32
+ /** The text after a task's `**Name**:`, as an opened task shows it under its name; the text as it is without a name. */
33
+ export const taskBodyOf = (text: string): string =>
34
+ taskNameOf(text) === null ? text : text.replace(TASK_NAME, '').trimStart()
@@ -1,13 +1,14 @@
1
1
  import { STALE_ACTIVITY_MINUTES } from './lane-summary'
2
+ import { STALE_PLAN_DAYS } from './planned'
2
3
  import { CHECK_NAME_LENGTH, CHECKS, type ChecksState, type PullRequest } from './pull-requests'
3
4
  import { shownWithMore } from './shown-with-more'
4
5
  import { MODEL_MARK, NO_AGENT_WORD, PHASE_COUNT } from './symbols'
5
6
 
6
- /** The words of an open lane: the age of a running phase, the word before the open phases that wait, the finished phases not merged yet, and the pointer of a phase with only its review gate open. */
7
+ /** The words of an open lane: the age of a running phase, the words before the open phases that wait and the finished ones, and the pointer of a phase with only its review gate open. */
7
8
  export type HerdTexts = {
8
9
  minutesAgo: (minutes: number) => string
9
10
  next: string
10
- notMerged: string
11
+ done: string
11
12
  reviewGate: string
12
13
  gate: string
13
14
  }
@@ -23,20 +24,45 @@ export type PlanTexts = {
23
24
  noPlan: string
24
25
  noWorktree: string
25
26
  noRow: string
27
+ /** The heads of the blocks on top of the herd. */
28
+ waitingHeading: string
29
+ appointmentHeading: string
30
+ sourceHeading: string
31
+ nextHeading: string
32
+ /** On a resting lane's line: the label before its starts-after cell, and the line for a lane without any reason. */
33
+ restAfter: string
34
+ restNoReason: string
35
+ /** After the head of the next block when the board's section holds no line. */
36
+ nextEmpty: string
26
37
  notInPlan: string
27
38
  /** Before a task an agent reports that no task of the plan names. */
28
39
  now: string
40
+ /** Before the description of a running subagent that names no phase and sits on none. */
41
+ subagent: string
29
42
  /** In place of the count of a phase whose only open box is the documentation box of its review gate. */
30
43
  allButDocs: string
31
44
  tickedSummary: (count: number) => string
45
+ /** The line above the listed ticked tasks of a phase; a click or `a` folds them again. */
46
+ tickedFold: (count: number) => string
32
47
  draft: string
33
48
  privateLink: string
34
49
  planned: (phases: number, plans: number) => string
50
+ /** The head of the planned section in the herd: `planned · 3 plans, next:`. */
51
+ plannedHead: (plans: number) => string
35
52
  phaseTally: (done: number, total: number) => string
36
53
  herd: HerdTexts
37
54
  checks: Record<ChecksState, string>
38
55
  hint: string
56
+ /** The notice when `a` is pressed with no phase open, where the key changes nothing on screen. */
57
+ showDoneNeedsPhase: string
39
58
  legend: string[]
59
+ /** The footer line when the coordinator could not be asked to put a question, with the reason. */
60
+ notAsked: (reason: string) => string
61
+ /** The footer line when the coordinator `name` was asked to put `subject`, `question <id>` or `board row <key>`. */
62
+ asked: (name: string, subject: string) => string
63
+ /** The footer line after `[done]` closed `subject`, and the one with the reason when it could not. */
64
+ closed: (subject: string) => string
65
+ notClosed: (reason: string) => string
40
66
  }
41
67
 
42
68
  /** The words of the watcher's badges and of the header notice when they are missing. */
@@ -78,38 +104,54 @@ export const TEXTS: PlanTexts = {
78
104
  noPlan: 'no plan',
79
105
  noWorktree: 'no worktree',
80
106
  noRow: 'no row in lanes',
107
+ waitingHeading: 'Waiting for you',
108
+ appointmentHeading: 'Appointments',
109
+ sourceHeading: 'Sources',
110
+ nextHeading: 'Next',
111
+ restAfter: 'after:',
112
+ restNoReason: 'idle · no reason',
113
+ nextEmpty: 'nothing queued, groom the board',
81
114
  notInPlan: 'not in the plan',
82
115
  now: 'now:',
116
+ subagent: 'Subagent:',
83
117
  allButDocs: ALL_BUT_DOCS,
84
118
  tickedSummary: (count) => `${count} ticked · click or a lists them`,
119
+ tickedFold: (count) => `${count} ticked · click or a folds them`,
85
120
  draft: 'draft · ',
86
121
  privateLink: 'private',
87
122
  phaseTally: (done, total) => (total > 0 ? `${done}/${total} ph` : ''),
88
123
  herd: {
89
124
  minutesAgo: (minutes) => (minutes === 0 ? '· just now' : `· ${minutes} min ago`),
90
125
  next: 'next',
91
- notMerged: 'not merged',
126
+ done: 'done',
92
127
  reviewGate: 'Review gate',
93
128
  gate: '· gate',
94
129
  },
95
130
  planned: (phases, plans) =>
96
131
  `planned (${phases} ${phases === 1 ? 'phase' : 'phases'} in ${plans} ${plans === 1 ? 'plan' : 'plans'})`,
132
+ plannedHead: (plans) => `planned · ${plans} ${plans === 1 ? 'plan' : 'plans'}, next:`,
97
133
  checks: {
98
134
  [CHECKS.passed]: '✓ checks',
99
135
  [CHECKS.failed]: '✗ checks',
100
136
  [CHECKS.pending]: '… checks',
101
137
  [CHECKS.none]: 'no checks',
102
138
  },
103
- hint: '↑↓ select →← open a show ticked s html q quit ? help',
139
+ hint: '↑↓ select →← open a/click ticked tasks s html q quit ? help',
140
+ showDoneNeedsPhase: 'a lists the ticked tasks inside an open phase: open one with → or a click',
104
141
  legend: [
105
142
  'enter/space or click toggles a lane or a phase, click or enter on a task opens its whole text, and opens a ↗ pull request · wheel PgUp PgDn scroll · j k h l like the arrows · click or ctrl+click on a link chip opens its link · ✦ artifact · ⑂ pull request or issue · ◈ Figma · private = the plan says so',
106
143
  '● working · ◆ blocked, waits for you · ◉ done = the agent finished its answer, not the lane · ○ idle · (·) no agent = a worktree nobody works in · • state unknown · only blocked wears its word in the lane line, the symbol says the rest · waiting = not started: no worktree yet, something left to do, and what it starts after; finished lanes (cleaned up, or every named phase ticked) are not shown · a working or blocked lane opens by itself only when it first appears, after that → ← and clicks are yours alone',
107
- `▸ ▾ open and close · an open lane's branches, top down: the agent's title and ⎇ its branch, ↗ its pull requests, its link chips, the phases it works on, next, ✓ · a lane shows only the phases its board rows name · a phase row = some of its tasks are ticked, one of its boxes was ticked in the last ${STALE_ACTIVITY_MINUTES} min, or an agent reports working on it, by number · n min ago since the last tick or report, · gate = only the review gate is open · ▶ teal = a task an agent reported (its task list or jh task; its name dim at the right edge when several agents work in the lane), ▷ dim = only guessed from the words of its log · now: <text> = a reported task no plan task names · next = the other open phases, as ranges · ✓ the finished ones, not merged = ticked in the worktree, not on the base branch yet · ${ALL_BUT_DOCS} = only the documentation box is open · a folded lane line names the phase moved last, +n more active ones, and its count · ${PHASE_COUNT.empty} no checkboxes · ${PHASE_COUNT.missing} not in the plan · a lists the ticked checkboxes one by one · s opens every lane and plan as one HTML page`,
144
+ `▸ ▾ open and close · an open lane's branches, top down: the agent's title and ⎇ its branch, ↗ its pull requests, its link chips, the phases it works on, next, done · a lane shows only the phases its board rows name · a phase row = some of its tasks are ticked, one of its boxes was ticked in the last ${STALE_ACTIVITY_MINUTES} min, or an agent reports working on it, by number · n min ago since the last tick or report, · gate = only the review gate is open · ▶ teal = a task an agent reported (its task list or jh task; its name dim at the right edge when several agents work in the lane), ▷ dim = only guessed from the words of its log · now: <text> = a reported task no plan task names · a running review sits on the gate box that names it, in the phase it names or else the lane's current one · Subagent: <text> = a running subagent that names no phase and sits on none · next = the other open phases, as ranges · done = the finished ones · ${ALL_BUT_DOCS} = only the documentation box is open · a folded lane line names the phase moved last, +n more active ones, and its count · ${PHASE_COUNT.empty} no checkboxes · ${PHASE_COUNT.missing} not in the plan · a or a click on its ▸ n ticked line lists the ticked checkboxes of an open phase one by one, a or a click on its ▾ line folds them · s opens every lane and plan as one HTML page`,
108
145
  `the bar = progress over the lane's plans · n/m = m checkboxes, n ticked · n/m ph = n of m phases finished · n/n in green = all ticked · 🥣 share of the context window used, on a folded lane only once it warns · Sonnet ${MODEL_MARK.differs} Opus in amber = the phase's subagent ran on another model than the plan's`,
109
146
  '↗ pull request · 💬 new feedback · ✋ changes requested · 🌐 preview · 📡 ticket: chip 📡 T-1 · status among the link chips, 📡 <n> in amber on a folded lane when tickets are new',
110
- '▌ on top = waits for your answer: 📎 a file or an Artifact that was sent (click: a cut-off row expands, then it opens), ? a question of the board · it goes when the board row is closed or `jh waiting close <id>` · woof closes the block',
147
+ `▌ on top = waits for your answer: 📎 a file or an Artifact that was sent (click: a cut-off row expands, then it opens), ? a question of the board or one a session left in its dialog (a click on its text only expands and folds it; [ask] at the right edge of a question asks the coordinator to put it; [done] closes the row: a board row gets its status, a sent file its record) · it goes when the board row is closed or \`jh waiting close <id>\` · \`Waiting for you\` heads the block (click: fold; click on +n: show all, on ▴ n: five) · ◷ the appointments of the board in a grey bar below it (click: fold; [done] closes the row, [run] asks the coordinator to run it now, [move] to ask you for a new time) · 1 2 3 = Next, the board lines to do next · ● Sources = the watcher’s sources that are no tickets: when each ran and its newest report, the mark amber while that report is under a day old and red while the source fails (click on a row: opens the entry) · planned = the open phases no lane works on, the next one per plan; stale? = its plan file was not changed on the base branch for ${STALE_PLAN_DAYS} days`,
111
148
  '⎇ branch · ↑ commits ahead · ± uncommitted · no worktree: plan read from main',
149
+ '❯ and a light background = the row under the mouse or the arrows, on every clickable row · enter on a block row asks, opens or expands it and never presses [done] · [xx] in square brackets = a click does something, bold under the mouse',
112
150
  ],
151
+ notAsked: (reason) => `no coordinator to ask: ${reason}`,
152
+ asked: (name, subject) => `asked ${name} for ${subject}`,
153
+ closed: (subject) => `closed ${subject}`,
154
+ notClosed: (reason) => `not closed: ${reason}`,
113
155
  }
114
156
 
115
157
  /** The mark of a running lane the board has no row for; null for a lane it lists. */
@@ -0,0 +1,30 @@
1
+ import { DEFAULT_TONES, TONE, type ToneName, type ToneRole } from '../config/tones'
2
+ import { paint } from '../terminal/text'
3
+
4
+ const TONE_PAINT: Record<ToneName, (text: string) => string> = {
5
+ [TONE.amber]: paint.yellow,
6
+ [TONE.teal]: paint.teal,
7
+ [TONE.grey]: paint.dim,
8
+ [TONE.red]: paint.red,
9
+ [TONE.green]: paint.green,
10
+ [TONE.blue]: paint.blue,
11
+ [TONE.magenta]: paint.magenta,
12
+ }
13
+
14
+ let palette: Record<ToneRole, ToneName> = DEFAULT_TONES
15
+
16
+ /**
17
+ * Sets the tone of each role for the screens drawn from now on. The herd calls it once per frame with the config's
18
+ * tones, so the layout reads one palette and no function of it takes the colours as a parameter.
19
+ */
20
+ export const setTones = (tones: Record<ToneRole, ToneName>): void => {
21
+ palette = tones
22
+ }
23
+
24
+ export const paintOfTone = (name: ToneName): ((text: string) => string) => TONE_PAINT[name]
25
+
26
+ /** The paint of a role in the palette set last. */
27
+ export const paintOfRole = (role: ToneRole): ((text: string) => string) => TONE_PAINT[palette[role]]
28
+
29
+ /** The tone name of a role in the palette set last. */
30
+ export const toneOfRole = (role: ToneRole): ToneName => palette[role]
package/src/plans/view.ts CHANGED
@@ -1,16 +1,19 @@
1
+ import { ARTIFACT_LINK_KIND } from '../config/link-kinds'
2
+ import { DEFAULT_TONES } from '../config/tones'
1
3
  import { fit, plainText } from '../terminal/text'
2
4
  import { renderSelected, type Screen } from './layout'
3
5
  import type { Options } from './options'
4
6
  import { laneSelection, overviewSelection, type Selection } from './selection'
5
7
  import type { Snapshot } from './snapshot'
8
+ import { setTones } from './tones'
6
9
 
7
10
  const DEFAULT_WIDTH = 100
8
11
  const CURSOR_HOME = '\x1b[H'
9
12
  const CLEAR_LINE_END = '\x1b[K'
10
13
  const CLEAR_BELOW = '\x1b[J'
11
14
 
12
- export const TERMINAL_ON = '\x1b[?1049h\x1b[?25l\x1b[?1000h\x1b[?1006h'
13
- export const TERMINAL_OFF = '\x1b[?1006l\x1b[?1000l\x1b[?25h\x1b[?1049l'
15
+ export const TERMINAL_ON = '\x1b[?1049h\x1b[?25l\x1b[?1000h\x1b[?1003h\x1b[?1006h'
16
+ export const TERMINAL_OFF = '\x1b[?1006l\x1b[?1003l\x1b[?1000l\x1b[?25h\x1b[?1049l'
14
17
 
15
18
  /** One column short of the pane: a line filling the last column makes some terminals wrap it. */
16
19
  export const paneWidth = (): number => (process.stdout.columns ?? (Number(process.env.COLUMNS) || DEFAULT_WIDTH)) - 1
@@ -20,7 +23,9 @@ const paneRows = (): number => process.stdout.rows ?? Number.MAX_SAFE_INTEGER
20
23
  export const bodyRows = (screen: Screen): number =>
21
24
  Math.max(paneRows() - screen.header.length - screen.footer.length, 1)
22
25
 
23
- export const clockOf = (locale: string): string => new Date().toLocaleTimeString(locale)
26
+ /** The time as `HH:MM`, in the header and in the refresh error alike: a frame differs only once a minute. */
27
+ export const clockOf = (locale: string, now: Date = new Date()): string =>
28
+ now.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })
24
29
 
25
30
  export const renderSnapshot = (
26
31
  snapshot: Snapshot,
@@ -29,6 +34,7 @@ export const renderSnapshot = (
29
34
  width: number,
30
35
  locale: string,
31
36
  ): Screen => {
37
+ setTones(snapshot.tones ?? DEFAULT_TONES)
32
38
  const screen = renderSelected(
33
39
  {
34
40
  title: snapshot.title,
@@ -38,6 +44,10 @@ export const renderSnapshot = (
38
44
  watch: snapshot.watch,
39
45
  watchesTickets: snapshot.watchesTickets,
40
46
  awaiting: snapshot.waiting,
47
+ groomedDays: snapshot.groomedDays,
48
+ next: snapshot.next,
49
+ appointments: snapshot.appointments,
50
+ sourceBlock: snapshot.sourceBlock,
41
51
  unplaced: snapshot.unplaced,
42
52
  },
43
53
  selection,
@@ -50,19 +60,23 @@ export const startSelection = (snapshot: Snapshot, options: Options): Selection
50
60
  options.lane ? laneSelection(snapshot.views[0]?.progress) : overviewSelection()
51
61
 
52
62
  /** Header and footer stay put; the body shows `bodyRows` lines from `top`, padded so the footer sits at the bottom. */
53
- export const draw = (screen: Screen, top: number): void => {
54
- const width = paneWidth()
55
- const rows = bodyRows(screen)
63
+ export const frameOutput = (screen: Screen, top: number, width: number, rows: number): string => {
56
64
  const body = screen.body.lines.slice(top, top + rows)
57
65
  const padding = Array.from({ length: Math.max(rows - body.length, 0) }, () => '')
58
66
  const lines = [...screen.header, ...body, ...padding, ...screen.footer]
59
- process.stdout.write(
60
- `${CURSOR_HOME}${lines.map((line) => `${fit(line, width)}${CLEAR_LINE_END}`).join('\r\n')}${CLEAR_BELOW}`,
61
- )
67
+ return `${CURSOR_HOME}${lines.map((line) => `${fit(line, width)}${CLEAR_LINE_END}`).join('\r\n')}${CLEAR_BELOW}`
62
68
  }
63
69
 
64
- /** One frame as text; ANSI styling is dropped when stdout is not a terminal. */
70
+ /** The frame to write, or null when the terminal already shows it; a null `previous` means the terminal content is unknown. */
71
+ export const frameToWrite = (previous: string | null, next: string): string | null => (previous === next ? null : next)
72
+
73
+ const ARTIFACT_ADDRESS = new RegExp(ARTIFACT_LINK_KIND.pattern)
74
+
75
+ /**
76
+ * One frame as text; ANSI styling is dropped when stdout is not a terminal, and links are written out as their
77
+ * address, except an Artifact's, which is long and would push the rest of its line past the pane.
78
+ */
65
79
  export const frameText = (screen: Screen, isTerminal: boolean): string => {
66
80
  const lines = [...screen.header, ...screen.body.lines, ...screen.footer]
67
- return (isTerminal ? lines : lines.map(plainText)).join('\n')
81
+ return (isTerminal ? lines : lines.map((line) => plainText(line, (url) => !ARTIFACT_ADDRESS.test(url)))).join('\n')
68
82
  }