jackherd 1.24.0 → 1.25.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 (68) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +21 -21
  3. package/dist/{chunk-JDO6O3UO.js → chunk-O365KHHP.js} +139 -3
  4. package/dist/config.d.ts +4 -0
  5. package/dist/config.js +4 -1
  6. package/dist/plan-format.d.ts +121 -3
  7. package/dist/plan-format.js +176 -35
  8. package/docs/plan-contract.md +52 -5
  9. package/herdr-plugin/herdr-plugin.toml +1 -1
  10. package/install.sh +1 -1
  11. package/kit/hooks/hooks/approval-gate/index.ts +2 -2
  12. package/kit/hooks/hooks/merge-guard/lane-plan.ts +1 -1
  13. package/kit/hooks/hooks/shared/board-lanes.ts +33 -12
  14. package/kit/hooks/hooks/shared/boards.ts +22 -6
  15. package/kit/workflow/skills/orchestrate/references/herd.md +3 -3
  16. package/kit/workflow/skills/plan/SKILL.md +3 -1
  17. package/kit/workflow/skills/plan/references/template-English.md +2 -0
  18. package/kit/workflow/skills/plan/references/template-German.md +2 -0
  19. package/package.json +1 -1
  20. package/schemas/jackherd.schema.json +7 -0
  21. package/src/brief/command.ts +1 -1
  22. package/src/brief/lane-facts.ts +2 -2
  23. package/src/check/command.ts +2 -1
  24. package/src/check/plan-structure-check.ts +68 -14
  25. package/src/check/settings.ts +6 -1
  26. package/src/check/skill-budget-defaults.ts +6 -3
  27. package/src/config/schema.ts +3 -0
  28. package/src/lane/close-board.ts +4 -4
  29. package/src/lane/close-command.ts +2 -2
  30. package/src/lane/dev-command.ts +1 -1
  31. package/src/lane/port-command.ts +1 -1
  32. package/src/plan-contract/command.ts +2 -1
  33. package/src/plan-contract/contract.ts +30 -9
  34. package/src/plan-format/bare-codes.ts +4 -4
  35. package/src/plan-format/dependencies.ts +5 -5
  36. package/src/plan-format/lanes.ts +3 -4
  37. package/src/plan-format/markdown.ts +125 -10
  38. package/src/plan-format/open-status.ts +15 -0
  39. package/src/plan-format/phases.ts +45 -4
  40. package/src/plan-format/plan-head.ts +31 -9
  41. package/src/plan-format/plan-structure.ts +43 -7
  42. package/src/plan-format/progress.ts +3 -0
  43. package/src/plan-format/sprint-document.ts +4 -4
  44. package/src/plan-format/unrecognized-sections.ts +21 -0
  45. package/src/plan-format/waiting.ts +13 -13
  46. package/src/plans/board-close.ts +25 -16
  47. package/src/plans/board-pick.ts +1 -1
  48. package/src/plans/ensure-panes.ts +2 -2
  49. package/src/plans/layout-summary.ts +8 -1
  50. package/src/plans/layout.ts +14 -5
  51. package/src/plans/live.ts +1 -1
  52. package/src/plans/settings.ts +3 -0
  53. package/src/plans/snapshot.ts +9 -5
  54. package/src/plans/sprint-export.ts +1 -1
  55. package/src/plans/texts.ts +3 -0
  56. package/src/plans/view.ts +1 -0
  57. package/src/resume/coordinator-read.ts +2 -2
  58. package/src/settled/command.ts +1 -1
  59. package/src/settled/settle.ts +5 -5
  60. package/src/sync/render.ts +20 -5
  61. package/src/sync/sync-plan.ts +6 -2
  62. package/src/sync/sync.ts +3 -2
  63. package/src/vocabulary/index.ts +192 -4
  64. package/src/watch/appointments.ts +13 -20
  65. package/src/watch/command.ts +5 -5
  66. package/src/watch/lane-progress.ts +1 -1
  67. package/src/watch/merge-overlap-read.ts +2 -2
  68. package/src/watch/plan-missing-read.ts +3 -1
@@ -2,6 +2,12 @@ export const PLAN_LANGUAGE = { de: 'de', en: 'en' } as const
2
2
 
3
3
  export type PlanLanguage = (typeof PLAN_LANGUAGE)[keyof typeof PLAN_LANGUAGE]
4
4
 
5
+ /** The languages to try when reading: `language` first, then every other known one. */
6
+ export const languagesFrom = (language: PlanLanguage): PlanLanguage[] => [
7
+ language,
8
+ ...Object.values(PLAN_LANGUAGE).filter((other) => other !== language),
9
+ ]
10
+
5
11
  /** The language's name as a reply or a question says it. */
6
12
  export const LANGUAGE_NAME: Record<PlanLanguage, string> = {
7
13
  [PLAN_LANGUAGE.en]: 'English',
@@ -175,6 +181,111 @@ export const WAITING_VOCABULARY: Record<
175
181
  },
176
182
  }
177
183
 
184
+ /**
185
+ * The closed set of keys a section carries in a marker, `<!-- jh: <key> -->` on the line directly under its heading.
186
+ * The marker names the section whatever language the heading is written in.
187
+ */
188
+ export const SECTION_KEY = {
189
+ waiting: 'waiting',
190
+ appointments: 'appointments',
191
+ lanes: 'lanes',
192
+ goal: 'goal',
193
+ history: 'history',
194
+ plans: 'plans',
195
+ decisions: 'decisions',
196
+ implementation: 'implementation',
197
+ model: 'model',
198
+ dependencies: 'dependencies',
199
+ documentation: 'documentation',
200
+ forYou: 'for-you',
201
+ next: 'next',
202
+ } as const
203
+
204
+ export type SectionKey = (typeof SECTION_KEY)[keyof typeof SECTION_KEY]
205
+
206
+ export const isSectionKey = (value: string): value is SectionKey =>
207
+ (Object.values(SECTION_KEY) as string[]).includes(value)
208
+
209
+ /**
210
+ * The marker key of each line of the head section (`- **Stand:** <!-- jh: head-status --> …`). The marker names the
211
+ * line whatever language its label is written in.
212
+ */
213
+ export const HEAD_KEY: Record<HeadLine, string> = {
214
+ changes: 'head-changes',
215
+ status: 'head-status',
216
+ decisions: 'head-decisions',
217
+ tryNow: 'head-try-now',
218
+ next: 'head-next',
219
+ }
220
+
221
+ /**
222
+ * The closed set of keys a line that opens with bold words carries in a marker
223
+ * (`**Kein Review-Gate** <!-- jh: no-review-gate --> reason`). The marker names the line whatever language it is in.
224
+ */
225
+ export const LINE_KEY = { noReviewGate: 'no-review-gate', purpose: 'purpose' } as const
226
+
227
+ /**
228
+ * The closed set of keys a table column carries in a marker in its header cell (`Hängt ab von <!-- jh: depends-on -->`).
229
+ * The marker names the column whatever language the header is written in.
230
+ */
231
+ export const COLUMN_KEY = {
232
+ since: 'since',
233
+ item: 'item',
234
+ origin: 'origin',
235
+ unblocks: 'unblocks',
236
+ status: 'status',
237
+ when: 'when',
238
+ action: 'action',
239
+ dependsOn: 'depends-on',
240
+ writes: 'writes',
241
+ serialMarker: 'serial-marker',
242
+ } as const
243
+
244
+ export type ColumnKey = (typeof COLUMN_KEY)[keyof typeof COLUMN_KEY]
245
+
246
+ /** The header names the column `key` goes by in `language`: its name first, then the names that count as the same column. */
247
+ export const columnWords = (key: ColumnKey, language: PlanLanguage): readonly string[] => {
248
+ const waiting = WAITING_VOCABULARY[language]
249
+ const appointments = APPOINTMENT_VOCABULARY[language]
250
+ const { columns } = PLAN_VOCABULARY[language]
251
+ const words: Record<ColumnKey, readonly string[]> = {
252
+ [COLUMN_KEY.since]: [waiting.columns.since],
253
+ [COLUMN_KEY.item]: [waiting.columns.item],
254
+ [COLUMN_KEY.origin]: [waiting.columns.origin],
255
+ [COLUMN_KEY.unblocks]: [waiting.columns.unblocks],
256
+ [COLUMN_KEY.status]: [
257
+ waiting.columns.status,
258
+ ...waiting.statusAlso,
259
+ appointments.columns.status,
260
+ ...appointments.statusAlso,
261
+ ],
262
+ [COLUMN_KEY.when]: [appointments.columns.when],
263
+ [COLUMN_KEY.action]: [appointments.columns.action],
264
+ [COLUMN_KEY.dependsOn]: [columns.dependsOn],
265
+ [COLUMN_KEY.writes]: [columns.writes],
266
+ [COLUMN_KEY.serialMarker]: [columns.serialMarker],
267
+ }
268
+ return words[key]
269
+ }
270
+
271
+ /**
272
+ * The closed set of keys a status carries in a marker in its cell (`erledigt <!-- jh: status-done -->`): `open` for a
273
+ * board row that still waits, the others for the status of a plan. The marker names the status whatever language the
274
+ * cell is written in.
275
+ */
276
+ export const STATUS_KEY = {
277
+ open: 'status-open',
278
+ done: 'status-done',
279
+ abandoned: 'status-abandoned',
280
+ inProgress: 'status-in-progress',
281
+ draft: 'status-draft',
282
+ } as const
283
+
284
+ export type StatusKey = (typeof STATUS_KEY)[keyof typeof STATUS_KEY]
285
+
286
+ export const isStatusKey = (value: string): value is StatusKey =>
287
+ (Object.values(STATUS_KEY) as string[]).includes(value)
288
+
178
289
  export const isPlanLanguage = (value: string): value is PlanLanguage =>
179
290
  (Object.values(PLAN_LANGUAGE) as string[]).includes(value)
180
291
 
@@ -248,6 +359,8 @@ export type PlanVocabulary = {
248
359
  phase: string
249
360
  /** Opens the line by which a phase says it has no review gate on purpose: `**Kein Review-Gate:** <reason>`. */
250
361
  noReviewGate: string
362
+ /** Opens the line directly under a phase heading that says in one sentence what is different afterwards, for whom: `**Worum es geht:** <sentence>`. */
363
+ phasePurpose: string
251
364
  /** Marks a lane as closed when its name cell contains it. */
252
365
  laneClosed: string
253
366
  /** Stands in a board row's phases cell when that pull request builds only part of the phase. */
@@ -296,6 +409,7 @@ export const PLAN_VOCABULARY: Record<PlanLanguage, PlanVocabulary> = {
296
409
  columns: { dependsOn: 'Hängt ab von', writes: 'Schreibt', serialMarker: 'Seriell-Marker' },
297
410
  phase: 'Phase',
298
411
  noReviewGate: 'Kein Review-Gate',
412
+ phasePurpose: 'Worum es geht',
299
413
  laneClosed: 'abgeräumt',
300
414
  partialPhase: '(Teil)',
301
415
  openSubagents: 'offene Sub-Agenten',
@@ -339,6 +453,7 @@ export const PLAN_VOCABULARY: Record<PlanLanguage, PlanVocabulary> = {
339
453
  columns: { dependsOn: 'Depends on', writes: 'Writes', serialMarker: 'Serial marker' },
340
454
  phase: 'Phase',
341
455
  noReviewGate: 'No review gate',
456
+ phasePurpose: 'What it is about',
342
457
  laneClosed: 'cleaned up',
343
458
  partialPhase: '(part)',
344
459
  openSubagents: 'open subagents',
@@ -362,20 +477,93 @@ export const PLAN_VOCABULARY: Record<PlanLanguage, PlanVocabulary> = {
362
477
 
363
478
  const STATUS_PRECEDENCE = [PLAN_STATUS.done, PLAN_STATUS.abandoned, PLAN_STATUS.inProgress, PLAN_STATUS.draft] as const
364
479
 
365
- /** Classifies a free-text `Status:` line; the first matching status in precedence order wins. */
366
- export const classifyStatus = (language: PlanLanguage, statusText: string): PlanStatus => {
480
+ const STATUS_OF_KEY: Partial<Record<StatusKey, PlanStatus>> = {
481
+ [STATUS_KEY.done]: PLAN_STATUS.done,
482
+ [STATUS_KEY.abandoned]: PLAN_STATUS.abandoned,
483
+ [STATUS_KEY.inProgress]: PLAN_STATUS.inProgress,
484
+ [STATUS_KEY.draft]: PLAN_STATUS.draft,
485
+ }
486
+
487
+ /** A `<!-- jh: <key> -->` marker anywhere in a text; the key is group 1. */
488
+ export const MARKER_IN_TEXT = /<!--\s*jh:\s*([a-z-]+)\s*-->/
489
+
490
+ const statusOfMarker = (statusText: string): PlanStatus | undefined => {
491
+ const key = MARKER_IN_TEXT.exec(statusText)?.[1]
492
+ return key !== undefined && isStatusKey(key) ? STATUS_OF_KEY[key] : undefined
493
+ }
494
+
495
+ const statusOfWords = (language: PlanLanguage, statusText: string): PlanStatus | undefined => {
367
496
  const { statusWords } = PLAN_VOCABULARY[language]
368
497
  const lowerStatus = statusText.toLowerCase()
369
498
  const matches = ({ leading, anywhere }: StatusWord) =>
370
499
  leading.some((word) => lowerStatus.startsWith(word)) || anywhere.some((fragment) => lowerStatus.includes(fragment))
371
- const matched = STATUS_PRECEDENCE.find((status) => matches(statusWords[status]))
500
+ return STATUS_PRECEDENCE.find((status) => matches(statusWords[status]))
501
+ }
502
+
503
+ /**
504
+ * Classifies a free-text `Status:` line. A status marker (`<!-- jh: status-done -->`) decides, any language; else the
505
+ * first matching status in precedence order of the words of `language`, then of any other known language.
506
+ */
507
+ export const classifyStatus = (language: PlanLanguage, statusText: string): PlanStatus => {
508
+ const marked = statusOfMarker(statusText)
509
+ if (marked) return marked
510
+ const text = statusText.replace(MARKER_IN_TEXT, '')
511
+ const matched = languagesFrom(language)
512
+ .map((candidate) => statusOfWords(candidate, text))
513
+ .find((status) => status !== undefined)
372
514
  if (matched) return matched
373
- return statusText.trim() ? PLAN_STATUS.other : PLAN_STATUS.none
515
+ return text.trim() ? PLAN_STATUS.other : PLAN_STATUS.none
374
516
  }
375
517
 
376
518
  /** `### Phase 3: Title` as the plan spells it in `language`. */
377
519
  export const phaseHeading = (language: PlanLanguage, id: string, title: string): string =>
378
520
  `### ${PLAN_VOCABULARY[language].phase} ${id}: ${title}`
379
521
 
522
+ /** The heading text of the section `key` names, as `language` writes it. */
523
+ export const headingOfSection = (key: SectionKey, language: PlanLanguage): string => {
524
+ const { sections } = PLAN_VOCABULARY[language]
525
+ const headings: Record<SectionKey, string> = {
526
+ [SECTION_KEY.waiting]: WAITING_VOCABULARY[language].heading,
527
+ [SECTION_KEY.appointments]: APPOINTMENT_VOCABULARY[language].heading,
528
+ [SECTION_KEY.lanes]: sections.lanes,
529
+ [SECTION_KEY.goal]: sections.goal,
530
+ [SECTION_KEY.history]: sections.history,
531
+ [SECTION_KEY.plans]: sections.plans,
532
+ [SECTION_KEY.decisions]: sections.decisions,
533
+ [SECTION_KEY.implementation]: sections.implementation,
534
+ [SECTION_KEY.model]: sections.modelRecommendations,
535
+ [SECTION_KEY.dependencies]: sections.dependencies,
536
+ [SECTION_KEY.documentation]: sections.documentation,
537
+ [SECTION_KEY.forYou]: sections.forYou,
538
+ [SECTION_KEY.next]: NEXT_VOCABULARY[language].heading,
539
+ }
540
+ return headings[key]
541
+ }
542
+
543
+ /** The key and language of the section a heading text names, read the way a caller spells it; `null` when no language does. */
544
+ export const sectionOfHeading = (headingText: string): { key: SectionKey; language: PlanLanguage } | null => {
545
+ for (const language of Object.values(PLAN_LANGUAGE)) {
546
+ const key = Object.values(SECTION_KEY).find((candidate) => headingOfSection(candidate, language) === headingText)
547
+ if (key !== undefined) return { key, language }
548
+ }
549
+ return null
550
+ }
551
+
552
+ /**
553
+ * The closed set of keys a lanes-table cell carries in a marker: `lane-closed` in the name cell of a lane that was
554
+ * cleaned up, `partial-phase` in the phases cell of a row that builds only part of a phase.
555
+ */
556
+ export const CELL_KEY = { laneClosed: 'lane-closed', partialPhase: 'partial-phase' } as const
557
+
558
+ export type CellKey = (typeof CELL_KEY)[keyof typeof CELL_KEY]
559
+
560
+ /** The words of every known language that mark a lane as cleaned up. */
561
+ export const LANE_CLOSED_WORDS: readonly string[] = Object.values(PLAN_VOCABULARY).map(({ laneClosed }) => laneClosed)
562
+
563
+ /** The words of every known language that mark a row as building only part of a phase. */
564
+ export const PARTIAL_PHASE_WORDS: readonly string[] = Object.values(PLAN_VOCABULARY).map(
565
+ ({ partialPhase }) => partialPhase,
566
+ )
567
+
380
568
  export const sectionHeading = (language: PlanLanguage, section: keyof PlanVocabulary['sections']): string =>
381
569
  `## ${PLAN_VOCABULARY[language].sections[section]}`
@@ -1,7 +1,8 @@
1
1
  import { DAY_MS, twoDigits } from '../plan-format/clock'
2
- import { firstTableWithHeader, sectionLines } from '../plan-format/markdown'
2
+ import { columnIndex, firstTableWithHeader, sectionLines } from '../plan-format/markdown'
3
+ import { isOpenStatus } from '../plan-format/open-status'
3
4
  import { isRecord } from '../shared/is-record'
4
- import { APPOINTMENT_VOCABULARY, type PlanLanguage } from '../vocabulary'
5
+ import { APPOINTMENT_VOCABULARY, COLUMN_KEY, type PlanLanguage } from '../vocabulary'
5
6
 
6
7
  export const APPOINTMENT_EVENT = { due: 'due', unreadable: 'appointment unreadable' } as const
7
8
 
@@ -98,22 +99,15 @@ const unreadableKey = ({ whenText, action }: UnreadableAppointment): string => `
98
99
 
99
100
  const oneLine = (text: string): string => text.replace(/\s+/g, ' ').trim()
100
101
 
101
- /** An empty status counts as open: a row nobody closed is not dropped in silence. */
102
- const isOpen = (status: string, language: PlanLanguage): boolean => {
103
- const lower = status.toLowerCase()
104
- return lower === '' || APPOINTMENT_VOCABULARY[language].open.some((word) => lower.startsWith(word))
105
- }
102
+ const OPEN_WORDS = Object.values(APPOINTMENT_VOCABULARY).flatMap(({ open }) => open)
103
+
104
+ const isOpen = (status: string): boolean => isOpenStatus(status, OPEN_WORDS)
106
105
 
107
106
  /** The row as an appointment, as an unreadable row when it is open and its time cannot be read, else as nothing. */
108
- const tableOfRow = (
109
- cells: string[],
110
- [when, action, status]: number[],
111
- language: PlanLanguage,
112
- now: Date,
113
- ): AppointmentTable => {
107
+ const tableOfRow = (cells: string[], [when, action, status]: number[], now: Date): AppointmentTable => {
114
108
  const whenText = cells[when] ?? ''
115
109
  const actionText = oneLine(cells[action] ?? '')
116
- const open = isOpen(cells[status] ?? '', language)
110
+ const open = isOpen(cells[status] ?? '')
117
111
  const time = parseAppointmentTime(whenText, now)
118
112
  if (time)
119
113
  return {
@@ -130,19 +124,18 @@ const tableOfRow = (
130
124
  * listed as unreadable as a whole, with its header and the missing names; a section without a table has no rows.
131
125
  */
132
126
  export const readAppointmentTable = (board: string, language: PlanLanguage, now: Date): AppointmentTable => {
133
- const { heading, columns, statusAlso } = APPOINTMENT_VOCABULARY[language]
127
+ const { heading, columns } = APPOINTMENT_VOCABULARY[language]
134
128
  const { header, rows } = firstTableWithHeader(sectionLines(board, heading))
135
129
  if (header.length === 0) return { appointments: [], unreadable: [] }
136
- const statusName = [columns.status, ...statusAlso].find((name) => header.includes(name)) ?? columns.status
137
- const needed = [columns.when, columns.action, statusName]
138
- const missing = needed.filter((name) => !header.includes(name))
130
+ const needed = [COLUMN_KEY.when, COLUMN_KEY.action, COLUMN_KEY.status] as const
131
+ const positions = needed.map((key) => columnIndex(header, key, language))
132
+ const missing = needed.filter((_, index) => positions[index] === -1).map((key) => columns[key])
139
133
  if (missing.length > 0)
140
134
  return {
141
135
  appointments: [],
142
136
  unreadable: [{ whenText: header.join(' | '), action: `missing columns: ${missing.join(', ')}` }],
143
137
  }
144
- const positions = needed.map((name) => header.indexOf(name))
145
- const tables = rows.map((cells) => tableOfRow(cells, positions, language, now))
138
+ const tables = rows.map((cells) => tableOfRow(cells, positions, now))
146
139
  return {
147
140
  appointments: tables.flatMap(({ appointments }) => appointments),
148
141
  unreadable: tables.flatMap(({ unreadable }) => unreadable),
@@ -609,7 +609,7 @@ const boardReadOf = async (
609
609
  pullRequests: readonly WatchedPullRequest[],
610
610
  ): Promise<{ progress: Record<string, LaneProgress>; checkoutRows: number }> => {
611
611
  const text = await Bun.file(watch.sprintFile).text()
612
- const sprintLanes = parseSprintLanes(text, watch.settings.planLanguage)
612
+ const sprintLanes = parseSprintLanes(text, watch.settings.boardLanguage)
613
613
  const watched = sprintLanes.filter((lane) => lanes.includes(lane.name))
614
614
  const planPulls = watchedPlanPullRequests(pullRequests, watch.settings)
615
615
  const headReaderOf = readerOncePerRef((ref) => baseReaderOf(watch.root, ref))
@@ -1108,13 +1108,13 @@ const readBoardText = (watch: Watch): string | null => {
1108
1108
  const readAppointments = (watch: Watch, now: number): Watching['appointments'] => {
1109
1109
  const board = readBoardText(watch)
1110
1110
  if (board === null) return null
1111
- return { table: readAppointmentTable(board, watch.settings.planLanguage, new Date(now)), readAt: now }
1111
+ return { table: readAppointmentTable(board, watch.settings.boardLanguage, new Date(now)), readAt: now }
1112
1112
  }
1113
1113
 
1114
1114
  /** 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. */
1115
1115
  const waitingLineOf = (watch: Watch, hint = ''): string | null => {
1116
1116
  return waitingLine(
1117
- waitingItemsOf(readBoardText(watch), watch.settings.planLanguage, readWaitingRecords(watch.root), Date.now()),
1117
+ waitingItemsOf(readBoardText(watch), watch.settings.boardLanguage, readWaitingRecords(watch.root), Date.now()),
1118
1118
  hint,
1119
1119
  )
1120
1120
  }
@@ -1122,7 +1122,7 @@ const waitingLineOf = (watch: Watch, hint = ''): string | null => {
1122
1122
  /** The board's lanes, null when the board cannot be read. */
1123
1123
  const readSprintLanes = (watch: Watch): SprintLane[] | null => {
1124
1124
  const board = readBoardText(watch)
1125
- return board === null ? null : parseSprintLanes(board, watch.settings.planLanguage)
1125
+ return board === null ? null : parseSprintLanes(board, watch.settings.boardLanguage)
1126
1126
  }
1127
1127
 
1128
1128
  /** The sprint's lane names; the previous ones when the sprint cannot be read, so no lane drops out over it. */
@@ -1449,7 +1449,7 @@ const selfMergesOf = (watch: Watch, watching: Watching, now: number): number[] =
1449
1449
  const open = Object.entries(watching.state.pullRequests).flatMap(([number, { state }]) =>
1450
1450
  state === PULL_REQUEST_STATE.open ? [Number(number)] : [],
1451
1451
  )
1452
- return selfMergePullRequests(board, watch.settings.planLanguage, open, now, watch.selfMergeMinutes)
1452
+ return selfMergePullRequests(board, watch.settings.boardLanguage, open, now, watch.selfMergeMinutes)
1453
1453
  }
1454
1454
 
1455
1455
  const clearInputOf = (
@@ -224,5 +224,5 @@ export const boardCheckoutRows = (
224
224
  checkoutText: string,
225
225
  ): number => {
226
226
  const atBase = textAtBase(baseReaderOf(root, baseRefOf(settings)), boardRepoPathOf(root, settings, boardFile))
227
- return atBase === null ? 0 : boardRowsOf(atBase, checkoutText, settings.planLanguage).rowsFromCheckout
227
+ return atBase === null ? 0 : boardRowsOf(atBase, checkoutText, settings.boardLanguage).rowsFromCheckout
228
228
  }
@@ -1,7 +1,7 @@
1
1
  import { STACKS } from '../config/detect'
2
2
  import { isObjectId } from '../lane/close-survey'
3
3
  import { parseSprintLanes } from '../plan-format'
4
- import type { PlansSettings } from '../plans/settings'
4
+ import { type PlansSettings } from '../plans/settings'
5
5
  import { lanePlanPath, watchedLanePlanText } from './lane-progress'
6
6
  import type { OverlapOf } from './lane-report'
7
7
  import { type LaneScope, type MergeOverlap, overlapOf } from './merge-duties'
@@ -39,7 +39,7 @@ const scopeOf = (
39
39
  boardText: string,
40
40
  name: string,
41
41
  ): LaneScope | null => {
42
- const lane = parseSprintLanes(boardText, settings.planLanguage).find((candidate) => candidate.name === name)
42
+ const lane = parseSprintLanes(boardText, settings.boardLanguage).find((candidate) => candidate.name === name)
43
43
  if (lane === undefined) return null
44
44
  const text = watchedLanePlanText(root, settings, boardFile, lane)
45
45
  if (text === null) return null
@@ -19,7 +19,9 @@ import {
19
19
  const worktreePlanOf = (root: string, settings: PlansSettings, boardFile: string, laneName: string): string | null => {
20
20
  const file = join(worktreeOf(root, settings, laneName), boardRepoPathOf(root, settings, boardFile))
21
21
  if (!existsSync(file)) return null
22
- const lane = parseSprintLanes(readFileSync(file, 'utf8'), settings.planLanguage).find(({ name }) => name === laneName)
22
+ const lane = parseSprintLanes(readFileSync(file, 'utf8'), settings.boardLanguage).find(
23
+ ({ name }) => name === laneName,
24
+ )
23
25
  const path = lane === undefined ? null : lanePlanPath(root, settings, boardFile, lane)
24
26
  return path === null || isOutsideRepository(path) ? null : path
25
27
  }