jackherd 1.16.0 → 1.18.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 (158) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/INSTALL.md +2 -1
  3. package/README.md +29 -33
  4. package/dist/{chunk-4LUFR7RC.js → chunk-N5FAOQZL.js} +7 -0
  5. package/dist/config.d.ts +33 -1
  6. package/dist/config.js +57 -5
  7. package/dist/plan-format.d.ts +18 -6
  8. package/dist/plan-format.js +8 -1
  9. package/docs/plan-contract.md +1 -0
  10. package/herdr-plugin/herdr-plugin.toml +4 -1
  11. package/herdr-plugin/restore-plans.ts +60 -26
  12. package/herdr-plugin/resume.ts +18 -0
  13. package/install.sh +1 -1
  14. package/kit/hooks/ask-rules.json +11 -0
  15. package/kit/hooks/hooks/approval-gate/checked-marks.ts +59 -0
  16. package/kit/hooks/hooks/approval-gate/index.ts +36 -7
  17. package/kit/hooks/hooks/approval-gate/run.ts +72 -16
  18. package/kit/hooks/hooks/approval-gate/settings.ts +27 -1
  19. package/kit/hooks/hooks/approval-gate/standing-history.ts +90 -0
  20. package/kit/hooks/hooks/approval-gate/standing-sql.ts +58 -0
  21. package/kit/hooks/hooks/approval-gate/transcripts.ts +11 -5
  22. package/kit/hooks/hooks/lane-start-guard/decision.ts +74 -0
  23. package/kit/hooks/hooks/lane-start-guard/index.ts +36 -0
  24. package/kit/hooks/hooks/merge-guard/always-ask.ts +2 -2
  25. package/kit/hooks/hooks/merge-guard/approval.ts +25 -13
  26. package/kit/hooks/hooks/merge-guard/change-id.ts +34 -0
  27. package/kit/hooks/hooks/merge-guard/decision.ts +423 -35
  28. package/kit/hooks/hooks/merge-guard/pane-input.ts +61 -0
  29. package/kit/hooks/hooks/merge-guard/pane-process-info.ts +38 -0
  30. package/kit/hooks/hooks/merge-guard/run.ts +110 -28
  31. package/kit/hooks/hooks/merge-guard/standing-approval.ts +15 -78
  32. package/kit/hooks/hooks/plain-question/form.ts +89 -0
  33. package/kit/hooks/hooks/plain-question/index.ts +44 -4
  34. package/kit/hooks/hooks/plain-question/recommendation.ts +73 -0
  35. package/kit/hooks/hooks/plain-question/run.ts +14 -7
  36. package/kit/hooks/hooks/shared/standing-mark.ts +104 -0
  37. package/kit/hooks/hooks.json +11 -2
  38. package/kit/review/agents/skill-review.md +3 -2
  39. package/kit/review/skills/pre-commit/SKILL.md +16 -22
  40. package/kit/rules/rules/kit/code-style.md +26 -54
  41. package/kit/rules/rules/kit/pull-requests.md +17 -40
  42. package/kit/rules/rules/kit/testing.md +6 -12
  43. package/kit/rules/rules/kit/writing-for-agents.md +3 -0
  44. package/kit/workflow/skills/follow/SKILL.md +2 -3
  45. package/kit/workflow/skills/implement/SKILL.md +28 -43
  46. package/kit/workflow/skills/orchestrate/SKILL.md +201 -458
  47. package/kit/workflow/skills/orchestrate/references/appointments.md +21 -0
  48. package/kit/workflow/skills/orchestrate/references/approvals.md +23 -0
  49. package/kit/workflow/skills/orchestrate/references/close-lane.md +8 -7
  50. package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +8 -8
  51. package/kit/workflow/skills/orchestrate/references/events.md +46 -1
  52. package/kit/workflow/skills/orchestrate/references/herd-ask.md +10 -0
  53. package/kit/workflow/skills/orchestrate/references/lane-handover.md +9 -4
  54. package/kit/workflow/skills/orchestrate/references/lane-session.md +5 -4
  55. package/kit/workflow/skills/orchestrate/references/lane-setup.md +2 -1
  56. package/kit/workflow/skills/orchestrate/references/plan-commit.md +10 -2
  57. package/kit/workflow/skills/orchestrate/references/self-merge.md +26 -0
  58. package/kit/workflow/skills/orchestrate/references/stage-approval.md +50 -0
  59. package/kit/workflow/skills/orchestrate/references/standing-approval.md +38 -0
  60. package/kit/workflow/skills/orchestrate/references/start-lane.md +67 -0
  61. package/kit/workflow/skills/orchestrate/references/waiting.md +16 -0
  62. package/kit/workflow/skills/orchestrate/references/watch-without-monitor.md +13 -0
  63. package/kit/workflow/skills/plan/SKILL.md +29 -48
  64. package/package.json +2 -1
  65. package/recipes/README.md +1 -0
  66. package/recipes/jira-cli.md +1 -1
  67. package/recipes/sources/google-calendar-today.ts +8 -2
  68. package/recipes/sources/jira.ts +10 -2
  69. package/recipes/sources/linear.ts +9 -3
  70. package/recipes/sources/sentry.ts +8 -2
  71. package/recipes/sources.md +4 -2
  72. package/schemas/jackherd.schema.json +67 -0
  73. package/schemas/sources.schema.json +9 -0
  74. package/src/approval/checked.ts +60 -0
  75. package/src/approval/command.ts +16 -0
  76. package/src/check/command.ts +51 -6
  77. package/src/check/lint-pointers.ts +79 -0
  78. package/src/check/lint-shaped.ts +196 -0
  79. package/src/check/plan-structure-check.ts +116 -4
  80. package/src/check/report.ts +9 -3
  81. package/src/check/schema.ts +37 -0
  82. package/src/check/settings.ts +4 -0
  83. package/src/check/skill-budget-defaults.ts +90 -32
  84. package/src/check/skill-budgets.ts +96 -18
  85. package/src/cli.ts +2 -0
  86. package/src/config/personal-sources.ts +46 -10
  87. package/src/config/repository-identity.ts +40 -0
  88. package/src/config/schema.ts +53 -1
  89. package/src/context/command.ts +33 -2
  90. package/src/context/options.ts +33 -8
  91. package/src/context/usage-logs.ts +53 -0
  92. package/src/context/usage.ts +110 -0
  93. package/src/lane/base-command.ts +35 -0
  94. package/src/lane/base.ts +43 -0
  95. package/src/lane/close-check.ts +49 -15
  96. package/src/lane/close-command.ts +71 -11
  97. package/src/lane/close-options.ts +29 -8
  98. package/src/lane/close-survey.ts +13 -1
  99. package/src/lane/close.ts +51 -12
  100. package/src/lane/command.ts +21 -3
  101. package/src/lane/send-command.ts +32 -0
  102. package/src/lane/send-herdr.ts +77 -0
  103. package/src/lane/send-options.ts +23 -0
  104. package/src/lane/send.ts +90 -0
  105. package/src/lane/skill-hint.ts +27 -0
  106. package/src/lane/space-command.ts +30 -0
  107. package/src/lane/space.ts +49 -0
  108. package/src/lane/start-lane-hint.ts +18 -0
  109. package/src/plan-contract/base-check-git.ts +33 -0
  110. package/src/plan-contract/base-check.ts +17 -0
  111. package/src/plan-contract/command.ts +25 -4
  112. package/src/plan-format/plan-structure.ts +17 -5
  113. package/src/plans/ask-coordinator.ts +23 -13
  114. package/src/plans/board-rows.ts +9 -4
  115. package/src/plans/click-hint.ts +4 -0
  116. package/src/plans/command.ts +6 -5
  117. package/src/plans/ensure-panes.ts +14 -3
  118. package/src/plans/git.ts +14 -0
  119. package/src/plans/github.ts +19 -10
  120. package/src/plans/layout.ts +82 -33
  121. package/src/plans/live.ts +8 -12
  122. package/src/plans/plan-panes.ts +21 -4
  123. package/src/plans/selection.ts +29 -0
  124. package/src/plans/settings.ts +4 -0
  125. package/src/resume/command.ts +213 -0
  126. package/src/resume/decision.ts +102 -0
  127. package/src/resume/keeper.ts +65 -0
  128. package/src/resume/location.ts +22 -0
  129. package/src/resume/log.ts +27 -0
  130. package/src/resume/ports.ts +102 -0
  131. package/src/resume/record.ts +116 -0
  132. package/src/resume/targets.ts +84 -0
  133. package/src/resume/texts.ts +27 -0
  134. package/src/secrets/command.ts +15 -5
  135. package/src/sync/herdr-plugin.ts +2 -11
  136. package/src/sync/plan-ask-rules.ts +11 -0
  137. package/src/sync/render.ts +3 -1
  138. package/src/vocabulary/index.ts +13 -0
  139. package/src/waiting/command.ts +13 -4
  140. package/src/watch/command.ts +177 -78
  141. package/src/watch/config-reload.ts +119 -0
  142. package/src/watch/lane-model.ts +11 -1
  143. package/src/watch/lane-progress.ts +70 -8
  144. package/src/watch/lane-report.ts +66 -11
  145. package/src/watch/main-checkout.ts +5 -1
  146. package/src/watch/merge-duties.ts +33 -0
  147. package/src/watch/merge-overlap-read.ts +69 -0
  148. package/src/watch/plan-views.ts +16 -4
  149. package/src/watch/reference-paths.ts +46 -5
  150. package/src/watch/report-questions.ts +5 -2
  151. package/src/watch/single-report.ts +3 -2
  152. package/src/watch/source-dry-run-command.ts +22 -4
  153. package/src/watch/source-reason.ts +58 -0
  154. package/src/watch/source-reload.ts +21 -17
  155. package/src/watch/source-run.ts +24 -7
  156. package/src/watch/sources.ts +9 -2
  157. package/src/watch/texts.ts +14 -3
  158. package/src/watch/waiting-line.ts +4 -3
@@ -34,6 +34,7 @@ import { basename, dirname, join, relative } from 'node:path'
34
34
  import { writePrivateFile } from '../../kit/hooks/hooks/shared/private-directory'
35
35
  import { sourcesWithPersonal } from '../config/personal-sources'
36
36
  import { readConfig, readResolvedConfig } from '../config/read-config'
37
+ import { readRepositoryIdentity } from '../config/repository-identity'
37
38
  import type { JackherdConfig } from '../config/schema'
38
39
  import { resolveCoordinatorName } from '../context/coordinator-name'
39
40
  import { coordinatorSettings } from '../context/options'
@@ -45,12 +46,14 @@ import { boardFor, foundLanesOf } from '../plans/board-pick'
45
46
  import { ensurePlanPanes } from '../plans/ensure-panes'
46
47
  import { errorText } from '../plans/error-text'
47
48
  import { baseReaderOf, mainCheckout, worktreeNames, worktreeRoot } from '../plans/git'
48
- import { discoverLanes, isUnchanged, laneSetChange, laneSetLines } from '../plans/lane-discovery'
49
+ import { discoverLanes, isUnchanged, laneSetChange } from '../plans/lane-discovery'
49
50
  import { readerOncePerRef } from '../plans/plan-heads'
50
51
  import { CLOCK_LOCALE, type PlansSettings, plansSettingsOf, worktreeOf } from '../plans/settings'
51
52
  import { readRunningSubagents, subagentSourceOf } from '../plans/subagents-read'
52
53
  import { boardTexts } from '../plans/texts'
53
54
  import { readWaitingRecords, waitingItemsOf } from '../plans/waiting-records'
55
+ import { keepRecord } from '../resume/keeper'
56
+ import { systemRecordStore } from '../resume/ports'
54
57
  import { defaultUpdateCheck } from '../sync/if-changed'
55
58
  import { DEFAULT_SKILL_PREFIX } from '../sync/init'
56
59
  import { herdrLaneStatusSource } from '../terminal/herdr'
@@ -65,6 +68,14 @@ import {
65
68
  } from './appointments'
66
69
  import { type ApprovalRequestFile, approvalEvents, readApprovalRequests, recordApprovals } from './approvals'
67
70
  import { askingLanes, questionsFingerprint, recordReportQuestions } from './asking-lanes'
71
+ import {
72
+ baselinesOf,
73
+ type LiveValues,
74
+ liveValuesOf,
75
+ nextLoadedConfig,
76
+ readWatchedConfig,
77
+ withoutRunsBefore,
78
+ } from './config-reload'
68
79
  import {
69
80
  type CoordinatorRead,
70
81
  contextReadingOf,
@@ -94,7 +105,7 @@ import { agentScreen, listAgents } from './herdr'
94
105
  import { endedByKitUpdate } from './kit-update'
95
106
  import { type LaneReadings, laneFillEvents, recordLaneLevels } from './lane-fill'
96
107
  import { laneMessage, rememberedStatusLines, statusBlock } from './lane-message'
97
- import { type LaneModelReading, laneModelEvents, recordLaneModels } from './lane-model'
108
+ import { type LaneModelReading, laneModelEvents, modelColumnOf, recordLaneModels } from './lane-model'
98
109
  import { laneParallelEvents, recordLaneParallels } from './lane-parallel'
99
110
  import {
100
111
  boardCheckoutRows,
@@ -103,6 +114,7 @@ import {
103
114
  watchedPlanPullRequests,
104
115
  watchedReadyPhases,
105
116
  workedPhaseKeys,
117
+ worktreeOpenBoxes,
106
118
  } from './lane-progress'
107
119
  import {
108
120
  appointmentEventLine,
@@ -119,6 +131,8 @@ import {
119
131
  laneStopHeader,
120
132
  mergeDutyLines,
121
133
  noTaskSignalDutyLines,
134
+ type OverlapOf,
135
+ openBoxesDutyLines,
122
136
  otherPullRequestsLine,
123
137
  parallelDutyLines,
124
138
  planMissingDutyLines,
@@ -144,7 +158,8 @@ import {
144
158
  NO_AGENT,
145
159
  toSnapshot,
146
160
  } from './lane-watch'
147
- import { mainCheckoutDutyLine } from './main-checkout'
161
+ import { isBehindOnBase, mainCheckoutDutyLine } from './main-checkout'
162
+ import { mergeOverlapReader } from './merge-overlap-read'
148
163
  import { monitorToolAvailable } from './monitor-tool'
149
164
  import { parseWatchOptions, type WatchOptions } from './options'
150
165
  import type { PlanMissingEvent, PlanMissingState } from './plan-missing'
@@ -160,6 +175,7 @@ import {
160
175
  import { readDeploymentCandidates } from './preview-read'
161
176
  import {
162
177
  commentCandidates,
178
+ emptyPreviewState,
163
179
  feedbackReadCovers,
164
180
  type PreviewRead,
165
181
  previewEvents,
@@ -169,27 +185,24 @@ import {
169
185
  import { latestPullRequestOf, type WatchedPullRequest } from './pull-requests'
170
186
  import { recordedReadsOf } from './recorded-reads'
171
187
  import {
188
+ annotateEach,
172
189
  annotateFirstLine,
173
190
  eventReference,
191
+ laneSetEventLines,
174
192
  REFERENCE,
175
193
  type Reference,
176
194
  referenceHint,
177
195
  type SkillCopy,
178
196
  skillCopiesFor,
179
197
  skillDirectoryFor,
198
+ stageToolsOf,
180
199
  } from './reference-paths'
181
200
  import { heartbeatNotesOf, runningSubagentCountsOf } from './running-subagents'
182
201
  import { withSingleReportNotice } from './single-report'
183
202
  import { readOrchestrateSnapshot, skillLineFor } from './skill-change'
184
203
  import { runsFromSource, sourceChangeSettled, watchSourceChanges } from './source-change'
185
204
  import { sourceDryRun } from './source-dry-run-command'
186
- import {
187
- type LoadedSources,
188
- nextLoadedSources,
189
- readFileStamps,
190
- readWatchedSources,
191
- sourceFilePaths,
192
- } from './source-reload'
205
+ import { type LoadedSources, readFileStamps, sourceFilePaths } from './source-reload'
193
206
  import { runSource, SOURCE_LIMITS } from './source-run'
194
207
  import { recordedSourceState } from './source-tickets'
195
208
  import { isSourceDue, type SourceRun, type SourceState, sourceEvents } from './sources'
@@ -235,15 +248,8 @@ import type { DeployConfig, WorkflowRun } from './workflow-runs'
235
248
 
236
249
  type Files = { state: string; lock: string }
237
250
 
238
- /** The logins whose feedback does not count, or false when the project turned feedback off. */
239
- type FeedbackConfig = JackherdConfig['watch']['feedback']
240
-
241
- type ChecksConfig = JackherdConfig['watch']['checks']
242
251
  type ParallelTasksConfig = JackherdConfig['watch']['parallelTasks']
243
252
 
244
- /** Where deploy previews are read from; undefined when the project reads none. */
245
- type PreviewsConfig = JackherdConfig['watch']['previews']
246
-
247
253
  /** One feedback read: what counts as feedback, and every item it read, which the previews take their comments from. */
248
254
  type FeedbackPoll = { read: FeedbackRead; items: FeedbackItem[] }
249
255
 
@@ -256,15 +262,13 @@ type Watch = {
256
262
  options: WatchOptions
257
263
  files: Files
258
264
  settings: PlansSettings
259
- deploy: DeployConfig | undefined
260
- feedback: FeedbackConfig
261
- checks: ChecksConfig
265
+ /** `watch.deploy`, `feedback`, `previews` and `checks` as read at the start; the loop reads them from `Watching.live`. */
266
+ live: LiveValues
262
267
  parallelTasks: ParallelTasksConfig
263
268
  /** `watch.taskSignalMinutes`: how long a lane works without a task signal before `no task signal`. */
264
269
  taskSignalMinutes: number
265
270
  /** The switch `watch.parallelTasks.auto` and the paths that always ask, both from the main checkout's config. */
266
271
  parallelAuto: AutoSettings
267
- previews: PreviewsConfig
268
272
  /** The sources as read at the start; the loop takes up later changes of their files (`Watching.sources`). */
269
273
  sources: LoadedSources
270
274
  /** The checkouts whose config the sources are read from, as `readResolvedConfig` takes them. */
@@ -272,6 +276,8 @@ type Watch = {
272
276
  texts: WatchTexts
273
277
  /** Where the orchestrate skill is installed, the project's copy first; the events name a reference inside the one found. */
274
278
  skillCopies: SkillCopy[]
279
+ /** The tools of `guard.approvals.standing`: an approval of one of them points to the stage approval reference. */
280
+ stageTools: readonly string[]
275
281
  /** Runs inside herdr with the herdr adapter, so missing herd panes can be brought back. */
276
282
  herdr: boolean
277
283
  }
@@ -316,8 +322,12 @@ type Watching = {
316
322
  failureReadings: FailureReading[]
317
323
  /** The last preview read that answered; recorded with every state like the feedback read. */
318
324
  previewRead: PreviewRead | null
319
- /** The sources run now, read again whenever one of their files changes (`nextLoadedSources`). */
325
+ /** The sources run now, read again whenever one of their files changes (`nextLoadedConfig`). */
320
326
  sources: LoadedSources
327
+ /** The deploy, feedback, previews and checks settings in force now, taken up with the sources. */
328
+ live: LiveValues
329
+ /** When a new deploy workflow was taken up, in epoch milliseconds: its runs from before count as the baseline. */
330
+ deploySince: number | null
321
331
  /** The runs of the sources due at the last GitHub poll; recorded with every state like the feedback read. */
322
332
  sourceRuns: SourceRun[]
323
333
  /** The lanes' fills of the last read; a lane without an entry or with null has no value. */
@@ -347,6 +357,15 @@ type Watching = {
347
357
  planViews: PlanViews
348
358
  }
349
359
 
360
+ /** What the duty lines of one return are made of. */
361
+ type DutyFacts = {
362
+ stopped: readonly string[]
363
+ events: readonly WatchEvent[]
364
+ ready: Readonly<Record<string, ReadyPhase[]>>
365
+ mainCheckout: string | null
366
+ overlapOf: OverlapOf | undefined
367
+ }
368
+
350
369
  const POLL_MS = 5_000
351
370
  const GITHUB_POLL_MS = 60_000
352
371
  const MINUTE_MS = 60_000
@@ -512,28 +531,37 @@ const laneBranchesOf = (watch: Watch, lanes: readonly string[]): Record<string,
512
531
  const hintFor = (watch: Watch, reference: Reference | null): string =>
513
532
  reference === null ? '' : referenceHint(reference, skillDirectoryFor(watch.skillCopies, existsSync))
514
533
 
515
- /** The lines of one event with its reference hint; a deploy names its target when `watch.deploy` can read it. */
516
- const linesOfEvent = async (watch: Watch, event: WatchEvent, runs: readonly WorkflowRun[]): Promise<string[]> => {
517
- const hint = hintFor(watch, eventReference(event))
534
+ /** The lines of one event with its reference hint; a deploy names its target when `deploy` (`watch.deploy`) can read it. */
535
+ const linesOfEvent = async (
536
+ watch: Watch,
537
+ deploy: DeployConfig | undefined,
538
+ event: WatchEvent,
539
+ runs: readonly WorkflowRun[],
540
+ ): Promise<string[]> => {
541
+ const hint = hintFor(
542
+ watch,
543
+ eventReference(event, (tool) => watch.stageTools.includes(tool)),
544
+ )
518
545
  if (event.kind === WATCH_EVENT.feedback) return annotateFirstLine(feedbackEventLines(event), hint)
519
546
  if (event.kind === WATCH_EVENT.source) return annotateFirstLine(sourceEventLines(event), hint)
520
547
  if (isAppointmentEvent(event)) return [`${appointmentEventLine(event)}${hint}`]
521
548
  const headSha = event.kind === WATCH_EVENT.deploy ? runs.find((run) => run.id === event.runId)?.headSha : undefined
522
549
  const target =
523
- event.kind === WATCH_EVENT.deploy && headSha && watch.deploy
524
- ? await deployTargetOf(watch.root, watch.deploy, event.runId, headSha)
550
+ event.kind === WATCH_EVENT.deploy && headSha && deploy
551
+ ? await deployTargetOf(watch.root, deploy, event.runId, headSha)
525
552
  : null
526
553
  return [`${watchEventLine(event, target ? targetText(target) : null)}${hint}`]
527
554
  }
528
555
 
529
556
  const eventLinesOf = async (
530
557
  watch: Watch,
558
+ deploy: DeployConfig | undefined,
531
559
  events: readonly WatchEvent[],
532
560
  github: GithubRead | null,
533
561
  ): Promise<string[]> => {
534
562
  const runs = answered(github)?.runs ?? []
535
563
  const lines: string[] = []
536
- for (const event of events) lines.push(...(await linesOfEvent(watch, event, runs)))
564
+ for (const event of events) lines.push(...(await linesOfEvent(watch, deploy, event, runs)))
537
565
  return lines
538
566
  }
539
567
 
@@ -619,7 +647,7 @@ const statusLinesOf = (
619
647
  ),
620
648
  )
621
649
  const outage = state.ghDownSince === null ? [] : [ghUnavailableStatusLine(clock(state.ghDownSince))]
622
- const waiting = waitingLineOf(watch)
650
+ const waiting = waitingLineOf(watch, hintFor(watch, REFERENCE.waiting))
623
651
  const board = { term: watch.settings.board.term, checkoutRows }
624
652
  const coordinator = coordinatorStatusLine(watching.coordinator.name, watching.coordinator.read, watch.texts, board)
625
653
  return {
@@ -644,7 +672,12 @@ const stoppedLaneSections = (
644
672
  ): { lines: string[]; reportMtimes: Record<string, number | null> } => {
645
673
  const sections = stopped.map((lane) => {
646
674
  const status = watching.current[lane]?.status
647
- if (!status) return { lane, lines: [`=== ${lane}: ${terminalSafe(LANE_GONE)}`], mtime: undefined }
675
+ if (!status)
676
+ return {
677
+ lane,
678
+ lines: [`=== ${lane}: ${terminalSafe(LANE_GONE)}${hintFor(watch, REFERENCE.events)}`],
679
+ mtime: undefined,
680
+ }
648
681
  const message = stopMessageOf(watch, lane, status, next)
649
682
  const handoverHint = hasLaneHandover(worktreeOf(watch.root, watch.settings, lane), lane)
650
683
  ? hintFor(watch, REFERENCE.laneHandover)
@@ -662,14 +695,21 @@ const stoppedLaneSections = (
662
695
  const mainCheckoutLineOf = (watch: Watch): string | null =>
663
696
  mainCheckoutDutyLine(readMainCheckout(watch.root, watch.settings.baseBranch), watch.settings.baseBranch, watch.texts)
664
697
 
665
- /** `mainCheckoutLineOf` once a fetch of the base under way has ended when `events` hold a merge, so the merge already counts. */
698
+ /**
699
+ * The duty line of the main checkout once a fetch of the base under way has ended when `events` hold a merge, so the
700
+ * merge already counts. While it lags behind, the line names the reference of the board patch that follows a merge.
701
+ */
666
702
  const mainCheckoutLineAfter = async (
667
703
  watch: Watch,
668
704
  watching: Watching,
669
705
  events: readonly WatchEvent[],
670
706
  ): Promise<string | null> => {
671
707
  if (events.some((event) => event.kind === WATCH_EVENT.merged)) await watching.baseFetch
672
- return mainCheckoutLineOf(watch)
708
+ const read = readMainCheckout(watch.root, watch.settings.baseBranch)
709
+ const line = mainCheckoutDutyLine(read, watch.settings.baseBranch, watch.texts)
710
+ return line !== null && isBehindOnBase(read, watch.settings.baseBranch)
711
+ ? `${line}${hintFor(watch, REFERENCE.events)}`
712
+ : line
673
713
  }
674
714
 
675
715
  /** The ready phases of the stopped lanes that rest (`done`, `idle`); only their plans are read. */
@@ -685,6 +725,35 @@ const readyPhasesOfResting = (
685
725
  return watchedReadyPhases(watch.root, watch.settings, watch.sprintFile, lanes, workedPhaseKeys(progress))
686
726
  }
687
727
 
728
+ /** The reader of which checks a merge asks of a lane; only a return that holds a merge reads the board for it. */
729
+ const overlapReaderFor = async (watch: Watch, events: readonly WatchEvent[]): Promise<OverlapOf | undefined> => {
730
+ if (!events.some((event) => event.kind === WATCH_EVENT.merged)) return undefined
731
+ try {
732
+ return mergeOverlapReader(watch.root, watch.settings, watch.sprintFile, await Bun.file(watch.sprintFile).text())
733
+ } catch {
734
+ return undefined
735
+ }
736
+ }
737
+
738
+ /** The duty lines of one return; those that start a lane name `start-lane.md`, a missing plan `plan-commit.md`. */
739
+ const dutyLinesOf = (watch: Watch, watching: Watching, facts: DutyFacts): string[] => {
740
+ const { stopped, events, ready, mainCheckout, overlapOf } = facts
741
+ const startLaneHint = hintFor(watch, REFERENCE.startLane)
742
+ return [
743
+ ...blockedDutyLines(stopped, watching.current, watch.texts),
744
+ ...annotateEach(readyPhaseDutyLines(stopped, watching.current, ready, watch.texts), startLaneHint),
745
+ ...annotateEach(mergeDutyLines(events, watching.lanes, watch.texts, overlapOf), startLaneHint),
746
+ ...openBoxesDutyLines(events, worktreeOpenBoxes(watch.root, watch.settings), watch.texts),
747
+ ...(mainCheckout === null ? [] : [mainCheckout]),
748
+ ...laneFillDutyLines(events, watch.texts),
749
+ ...laneModelDutyLines(events, watch.texts),
750
+ ...parallelDutyLines(events, watch.texts),
751
+ ...noTaskSignalDutyLines(events, watch.texts),
752
+ ...annotateEach(planMissingDutyLines(events, watch.texts), hintFor(watch, REFERENCE.planCommit)),
753
+ ...approvalDutyLines(events, watch.texts),
754
+ ]
755
+ }
756
+
688
757
  /**
689
758
  * Every line of one return, gathered before any is printed, so the output is never cut halfway by a slow lookup, and
690
759
  * the state with what it showed: the report times of the stopped lanes and the status lines. A state without status
@@ -698,22 +767,12 @@ const reportOf = async (
698
767
  next: WatchState,
699
768
  ): Promise<{ lines: string[]; next: WatchState }> => {
700
769
  const stops = stoppedLaneSections(watch, watching, stopped, next)
701
- const eventLines = await eventLinesOf(watch, events, watching.github)
770
+ const eventLines = await eventLinesOf(watch, watching.live.deploy, events, watching.github)
702
771
  const mainCheckout = await mainCheckoutLineAfter(watch, watching, events)
703
772
  const board = await boardReadOrNote(watch, watching.lanes, answered(watching.github)?.pullRequests ?? [])
704
773
  const ready = readyPhasesOfResting(watch, watching, stopped, board.progress)
705
- const dutyLines = [
706
- ...blockedDutyLines(stopped, watching.current, watch.texts),
707
- ...readyPhaseDutyLines(stopped, watching.current, ready, watch.texts),
708
- ...mergeDutyLines(events, watching.lanes, watch.texts),
709
- ...(mainCheckout === null ? [] : [mainCheckout]),
710
- ...laneFillDutyLines(events, watch.texts),
711
- ...laneModelDutyLines(events, watch.texts),
712
- ...parallelDutyLines(events, watch.texts),
713
- ...noTaskSignalDutyLines(events, watch.texts),
714
- ...planMissingDutyLines(events, watch.texts),
715
- ...approvalDutyLines(events, watch.texts),
716
- ]
774
+ const overlapOf = await overlapReaderFor(watch, events)
775
+ const dutyLines = dutyLinesOf(watch, watching, { stopped, events, ready, mainCheckout, overlapOf })
717
776
  const status = statusLinesOf(watch, watching, next, board)
718
777
  const block = statusBlock(status.laneLines, status.coordinator, next.statusLines, status.others)
719
778
  const shownReports = stopped.filter((lane) => stops.reportMtimes[lane] !== next.reportMtimes[lane])
@@ -744,7 +803,7 @@ const recordGithub = (
744
803
 
745
804
  /**
746
805
  * The logs of the lanes' red pull requests, once per head commit (`failureTargets`): `gh run view --log-failed` of each
747
- * failed run, matched with `watch.checks.failedTestPattern`, which holds with feedback off too. A read
806
+ * failed run, matched with `watching.live.checks.failedTestPattern`, which holds with feedback off too. A read
748
807
  * that fails is recorded as not read, so the next GitHub poll asks again, a few times at most.
749
808
  */
750
809
  const readFailures = async (
@@ -755,7 +814,7 @@ const readFailures = async (
755
814
  ): Promise<FailureReading[]> => {
756
815
  const answer = answered(github)
757
816
  if (!answer) return []
758
- const pattern = watch.checks.failedTestPattern
817
+ const pattern = watching.live.checks.failedTestPattern
759
818
  const targets = failureTargets(watching.state.failures, answer.pullRequests, branches)
760
819
  return Promise.all(
761
820
  targets.map(async (target) => failureReadingOf(target, await readFailedTests(watch.root, target.runIds, pattern))),
@@ -773,10 +832,13 @@ const recordLaneFills = (watching: Watching, readings: LaneReadings): WatchEvent
773
832
  return laneFillEvents(watching.state.laneLevels, readings)
774
833
  }
775
834
 
776
- /** Lane → the model column of its newest board row; none when the board cannot be read, so no lane reports. */
777
- const laneModelColumns = (watch: Watch): Record<string, string> =>
835
+ /** Lane → the model column of the row of its current phase; none when the board cannot be read, so no lane reports. */
836
+ const laneModelColumns = (watch: Watch, progress: Readonly<Record<string, LaneProgress>>): Record<string, string> =>
778
837
  Object.fromEntries(
779
- (readSprintLanes(watch) ?? []).flatMap((lane) => lane.steps.slice(-1).map((step) => [lane.name, step.model])),
838
+ (readSprintLanes(watch) ?? []).flatMap((lane) => {
839
+ const column = modelColumnOf(lane, progress[lane.name])
840
+ return column === undefined ? [] : [[lane.name, column]]
841
+ }),
780
842
  )
781
843
 
782
844
  /** The agent states a lane can be switched in: it ended its turn and waits for no answer. */
@@ -787,8 +849,9 @@ const laneModelReadingsOf = (
787
849
  watch: Watch,
788
850
  watching: Watching,
789
851
  models: Record<string, string | null>,
852
+ progress: Readonly<Record<string, LaneProgress>>,
790
853
  ): Record<string, LaneModelReading> => {
791
- const columns = laneModelColumns(watch)
854
+ const columns = laneModelColumns(watch, progress)
792
855
  return Object.fromEntries(
793
856
  Object.entries(models).flatMap(([lane, model]) => {
794
857
  const column = columns[lane]
@@ -833,12 +896,13 @@ const readFeedback = async (
833
896
  targets: FeedbackTarget[],
834
897
  now: number,
835
898
  ): Promise<FeedbackPoll | null> => {
836
- if (watch.feedback === false || targets.length === 0) return null
899
+ const { feedback, previews } = watching.live
900
+ if (feedback === false || targets.length === 0) return null
837
901
  const own = await ensureOwnLogin(watch, watching)
838
902
  if (own === null) return null
839
903
  const items = await readFeedbackItems(watch.root, targets, feedbackSince(watching.state.feedback, now))
840
904
  if (!items.ok) return null
841
- const filter = feedbackFilterOf(own, watch.feedback, watch.previews?.authors ?? [])
905
+ const filter = feedbackFilterOf(own, feedback, previews?.authors ?? [])
842
906
  return { read: feedbackReadOf(now, targets, items.items, filter), items: items.items }
843
907
  }
844
908
 
@@ -854,11 +918,12 @@ const recordFeedbackRead = (watching: Watching, read: FeedbackRead | null): Watc
854
918
  */
855
919
  const previewComments = async (
856
920
  watch: Watch,
921
+ previews: LiveValues['previews'],
857
922
  state: WatchState,
858
923
  feedbackItems: FeedbackItem[] | null,
859
924
  now: number,
860
925
  ): Promise<FeedbackItemsRead> => {
861
- if (!watch.previews || watch.previews.authors.length === 0) return { ok: true, items: [] }
926
+ if (!previews || previews.authors.length === 0) return { ok: true, items: [] }
862
927
  if (feedbackItems && feedbackReadCovers(state.feedback.since, state.previews.since))
863
928
  return { ok: true, items: feedbackItems }
864
929
  return readCommentItems(watch.root, feedbackSince(state.previews, now))
@@ -876,12 +941,12 @@ const readPreviews = async (
876
941
  feedbackItems: FeedbackItem[] | null,
877
942
  now: number,
878
943
  ): Promise<PreviewRead | null> => {
879
- const previews = watch.previews
944
+ const { previews, deploy } = watching.live
880
945
  if (!previews || targets.length === 0) return null
881
946
  const unreported = previews.deployments ? unreportedTargets(watching.state.previews, targets) : []
882
947
  const [comments, deployments] = await Promise.all([
883
- previewComments(watch, watching.state, feedbackItems, now),
884
- readDeploymentCandidates(watch.root, unreported, watch.deploy?.environments ?? []),
948
+ previewComments(watch, previews, watching.state, feedbackItems, now),
949
+ readDeploymentCandidates(watch.root, unreported, deploy?.environments ?? []),
885
950
  ])
886
951
  if (!comments.ok || !deployments.ok) return null
887
952
  const candidates = [...commentCandidates(comments.items, previews), ...deployments.candidates]
@@ -907,16 +972,28 @@ const runDueSources = (watch: Watch, watching: Watching, now: number): Promise<S
907
972
  }
908
973
 
909
974
  /**
910
- * Takes up a change of the files the sources come from before the due ones run (`nextLoadedSources`): a source read
911
- * anew has no `ranAt` and runs at once, its first run a baseline; an invalid file keeps the list and is said once.
975
+ * Takes up a change of the files the config comes from before the GitHub reads and the due sources run
976
+ * (`nextLoadedConfig`): the sources and the deploy, feedback, previews and checks settings, read once. A source read
977
+ * anew has no `ranAt` and runs at once, its first run a baseline; an invalid file keeps everything and is said once.
978
+ * A new deploy workflow and previews switched on start counting from now (`baselinesOf`).
912
979
  */
913
- const reloadSources = (watch: Watch, watching: Watching) => {
980
+ const reloadConfig = (watch: Watch, watching: Watching) => {
914
981
  const stamps = readFileStamps(Object.keys(watching.sources.stamps))
915
- const next = nextLoadedSources(watching.sources, stamps, () => readWatchedSources(watch.configRoots, homedir()))
916
- watching.sources = next.loaded
982
+ const next = nextLoadedConfig({ sources: watching.sources, live: watching.live }, stamps, () =>
983
+ readWatchedConfig(watch.configRoots, homedir()),
984
+ )
985
+ const baselines = baselinesOf(watching.live, next.loaded.live)
986
+ watching.sources = next.loaded.sources
987
+ watching.live = next.loaded.live
988
+ if (baselines.deploy) watching.deploySince = Date.now()
989
+ if (baselines.previews) watching.state = { ...watching.state, previews: emptyPreviewState() }
917
990
  for (const line of next.lines) console.log(line)
918
991
  }
919
992
 
993
+ /** The GitHub read without the deploy runs from before a newly taken up deploy workflow. */
994
+ const withDeployBaseline = (github: GithubRead, since: number | null): GithubRead =>
995
+ github.ok ? { ...github, runs: withoutRunsBefore(github.runs, since) } : github
996
+
920
997
  const recordSourceRuns = (watching: Watching, runs: SourceRun[]): WatchEvent[] => {
921
998
  watching.sourceRuns = runs
922
999
  return sourceEvents(watching.state.sources, runs, watching.sources.sources)
@@ -941,8 +1018,8 @@ const syncPlanHeads = (watch: Watch, watching: Watching, pullRequests: readonly
941
1018
 
942
1019
  /**
943
1020
  * Reads the coordinator's fill, GitHub, the feedback, the previews and the due sources, and stores them only once every read is complete: a SIGTERM
944
- * while a read is pending saves the state as it was, so no event is taken as reported. Before the sources run, a
945
- * change of the files they come from is taken up (`reloadSources`), which prints its notes.
1021
+ * while a read is pending saves the state as it was, so no event is taken as reported. Before the reads, a
1022
+ * change of the files the config comes from is taken up (`reloadConfig`), which prints its notes.
946
1023
  */
947
1024
  const pollGithubAndCoordinator = async (watch: Watch, watching: Watching, now: number): Promise<WatchEvent[]> => {
948
1025
  syncBase(watch, watching)
@@ -950,11 +1027,12 @@ const pollGithubAndCoordinator = async (watch: Watch, watching: Watching, now: n
950
1027
  const fills = fillReadFor(watch, name, watching.agents, watching.lanes)
951
1028
  const laneBranches = laneBranchesOf(watch, watching.lanes)
952
1029
  const searched = Object.values({ ...watching.state.laneBranches, ...laneBranches })
953
- reloadSources(watch, watching)
954
- const [github, sourceRuns] = await Promise.all([
955
- readGithub(watch.root, watch.deploy, watch.settings.baseBranch, searched),
1030
+ reloadConfig(watch, watching)
1031
+ const [read, sourceRuns] = await Promise.all([
1032
+ readGithub(watch.root, watching.live.deploy, watch.settings.baseBranch, searched),
956
1033
  runDueSources(watch, watching, now),
957
1034
  ])
1035
+ const github = withDeployBaseline(read, watching.deploySince)
958
1036
  const pullRequests = answered(github)?.pullRequests ?? null
959
1037
  syncPlanHeads(watch, watching, pullRequests ?? [])
960
1038
  const branches = { ...watching.state.laneBranches, ...laneBranches }
@@ -962,6 +1040,7 @@ const pollGithubAndCoordinator = async (watch: Watch, watching: Watching, now: n
962
1040
  const targets = feedbackTargets(pullRequests, branches, watching.state.feedback)
963
1041
  const feedback = await readFeedback(watch, watching, targets, now)
964
1042
  const previews = pullRequests ? await readPreviews(watch, watching, targets, feedback?.items ?? null, now) : null
1043
+ const { progress: modelProgress } = await boardReadOrNote(watch, Object.keys(fills.models), pullRequests ?? [])
965
1044
  return [
966
1045
  ...recordGithub(watching, github, laneBranches, now, failureReadings),
967
1046
  ...recordFeedbackRead(watching, feedback?.read ?? null),
@@ -969,7 +1048,7 @@ const pollGithubAndCoordinator = async (watch: Watch, watching: Watching, now: n
969
1048
  ...recordSourceRuns(watching, sourceRuns),
970
1049
  ...recordCoordinator(watching, { name, read: fills.coordinator }),
971
1050
  ...recordLaneFills(watching, fills.lanes),
972
- ...recordLaneModelReads(watching, laneModelReadingsOf(watch, watching, fills.models)),
1051
+ ...recordLaneModelReads(watching, laneModelReadingsOf(watch, watching, fills.models, modelProgress)),
973
1052
  ...recordLaneParallelReads(watching, laneParallelReadingsOf(watch, watching)),
974
1053
  ]
975
1054
  }
@@ -994,9 +1073,10 @@ const readAppointments = (watch: Watch, now: number): Watching['appointments'] =
994
1073
  }
995
1074
 
996
1075
  /** The line of what waits on the user's answer, from the board's own section; null when nothing is open or the board cannot be read. */
997
- const waitingLineOf = (watch: Watch): string | null => {
1076
+ const waitingLineOf = (watch: Watch, hint = ''): string | null => {
998
1077
  return waitingLine(
999
1078
  waitingItemsOf(readBoardText(watch), watch.settings.planLanguage, readWaitingRecords(watch.root), Date.now()),
1079
+ hint,
1000
1080
  )
1001
1081
  }
1002
1082
 
@@ -1024,6 +1104,7 @@ const keepViews = (watch: Watch, watching: Watching, trigger: EnsureTrigger) =>
1024
1104
  watching.planViews = keepPlanViews(trigger, watching.planViews, {
1025
1105
  ensure: () => ensurePlanPanes(watch.root, watch.settings, watch.sprintFile),
1026
1106
  log: console.log,
1107
+ failureHint: () => hintFor(watch, REFERENCE.events),
1027
1108
  })
1028
1109
  }
1029
1110
 
@@ -1037,7 +1118,7 @@ const followLaneSet = (watch: Watch, watching: Watching): string[] => {
1037
1118
  watching.departed = [...watching.departed, ...departedLanes(change.removed, watching.baseline, watching.current)]
1038
1119
  watching.lanes = lanes
1039
1120
  watching.current = toSnapshot(watching.agents, lanes)
1040
- console.log(laneSetLines(change).join('\n'))
1121
+ console.log(laneSetEventLines(change, hintFor(watch, REFERENCE.events)).join('\n'))
1041
1122
  return change.added
1042
1123
  }
1043
1124
 
@@ -1076,7 +1157,10 @@ const report = async (
1076
1157
  watching.reporting = true
1077
1158
  const { lines, next: shown } = await reportOf(watch, watching, stopped, events, next)
1078
1159
  console.log(
1079
- (watch.options.stream ? lines : withSingleReportNotice(lines, monitorToolAvailable(process.env))).join('\n'),
1160
+ (watch.options.stream
1161
+ ? lines
1162
+ : withSingleReportNotice(lines, monitorToolAvailable(process.env), hintFor(watch, REFERENCE.watchWithoutMonitor))
1163
+ ).join('\n'),
1080
1164
  )
1081
1165
  writePrivateFile(watch.files.state, JSON.stringify(shown))
1082
1166
  watching.reporting = false
@@ -1382,6 +1466,8 @@ const initialWatching = (
1382
1466
  failureReadings: [],
1383
1467
  previewRead: null,
1384
1468
  sources: watch.sources,
1469
+ live: watch.live,
1470
+ deploySince: null,
1385
1471
  sourceRuns: [],
1386
1472
  laneReadings: {},
1387
1473
  laneModelReadings: {},
@@ -1413,12 +1499,20 @@ const watchLanes = async (watch: Watch): Promise<boolean> => {
1413
1499
  const lanes = lanesFor(watch, agents, sprintLanes)
1414
1500
  const armed = toSnapshot(agents, lanes)
1415
1501
  const arm = armNotice(state.armed, lanes, armed, takeOver, relative(watch.root, watch.sprintFile))
1416
- if (arm.line !== null) console.log(arm.line)
1502
+ if (arm.line !== null)
1503
+ console.log(`${arm.line}${takeOver.stillRunning === null ? '' : hintFor(watch, REFERENCE.events)}`)
1417
1504
  if (watch.boardNotice) console.log(watch.boardNotice)
1418
1505
 
1419
1506
  const watching = initialWatching(watch, { lanes, sprintLanes, state: { ...state, armed: arm.state }, armed, agents })
1420
1507
  keepViews(watch, watching, { kind: ENSURE_TRIGGER.start })
1508
+ const resumeRecord = keepRecord(
1509
+ { root: watch.root, board: watch.sprintFile, coordinator: coordinatorNameFor(watch, agents), pid: process.pid },
1510
+ Date.now(),
1511
+ { store: systemRecordStore(watch.root), log: console.log },
1512
+ )
1513
+ resumeRecord.follow(armed, Date.now())
1421
1514
  process.on('SIGTERM', () => {
1515
+ resumeRecord.stop(Date.now())
1422
1516
  saveState(watch, watching)
1423
1517
  console.log(stopNoticeOf(watch.files.lock))
1424
1518
  process.exit(0)
@@ -1438,16 +1532,23 @@ const watchLanes = async (watch: Watch): Promise<boolean> => {
1438
1532
  : null
1439
1533
  const sourceChanges = kit && runsFromSource(kit.packageRoot) ? watchSourceChanges(kit.packageRoot) : null
1440
1534
  const sourceChanged = () => sourceChangeSettled(sourceChanges?.lastChangeAt() ?? null, Date.now())
1441
- const effects = { saveState: () => saveState(watch, watching), log: console.log, skillLine, sourceChanged }
1535
+ const effects = {
1536
+ saveState: () => saveState(watch, watching),
1537
+ log: console.log,
1538
+ skillLine,
1539
+ sourceChanged,
1540
+ }
1442
1541
  try {
1443
1542
  while (await pollOnce(watch, watching)) {
1444
1543
  await Bun.sleep(POLL_MS)
1445
1544
  pollLanes(watch, watching)
1545
+ resumeRecord.follow(watching.current, Date.now())
1446
1546
  await announceUpdate?.()
1447
1547
  if (endedByKitUpdate(kit, effects)) return true
1448
1548
  }
1449
1549
  return false
1450
1550
  } finally {
1551
+ resumeRecord.stop(Date.now())
1451
1552
  sourceChanges?.close()
1452
1553
  stopPublisher?.()
1453
1554
  }
@@ -1489,7 +1590,7 @@ const watchOf = (options: WatchOptions): Watch => {
1489
1590
  // Stamped before the read: a change in between is read again at the first poll, never missed.
1490
1591
  const stamps = readFileStamps(sourceFilePaths(configRoots, homedir()))
1491
1592
  const { config, notes } = readResolvedConfig(configRoots)
1492
- const merged = sourcesWithPersonal(config.watch.sources)
1593
+ const merged = sourcesWithPersonal(config.watch.sources, readRepositoryIdentity(root))
1493
1594
  for (const note of [...notes, ...merged.notes]) console.log(note)
1494
1595
  const settings = plansSettingsOf(config)
1495
1596
  const picked = boardFor(options.boardPath, [root, worktreeRoot(process.cwd())], settings, () =>
@@ -1502,17 +1603,15 @@ const watchOf = (options: WatchOptions): Watch => {
1502
1603
  options,
1503
1604
  files: filesOf(root),
1504
1605
  settings,
1505
- deploy: config.watch.deploy,
1506
- feedback: config.watch.feedback,
1507
- checks: config.watch.checks,
1606
+ live: liveValuesOf(config),
1508
1607
  parallelTasks: config.watch.parallelTasks,
1509
1608
  taskSignalMinutes: config.watch.taskSignalMinutes,
1510
1609
  parallelAuto: autoSettingsOf(root, checkoutRoot, config),
1511
- previews: config.watch.previews,
1512
1610
  sources: { sources: merged.sources, stamps },
1513
1611
  configRoots,
1514
1612
  texts: watchTexts(settings.responseLanguage),
1515
1613
  skillCopies: skillCopiesFor(root, homedir(), config.skillPrefix, DEFAULT_SKILL_PREFIX),
1614
+ stageTools: stageToolsOf(config.guard.approvals.standing),
1516
1615
  herdr: insideHerdr(settings.adapter, process.env),
1517
1616
  }
1518
1617
  }