jackherd 1.24.0 → 1.26.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.
- package/CHANGELOG.md +9 -0
- package/INSTALL.md +1 -1
- package/README.md +22 -22
- package/dist/{chunk-JDO6O3UO.js → chunk-O365KHHP.js} +139 -3
- package/dist/config.d.ts +4 -0
- package/dist/config.js +4 -1
- package/dist/plan-format.d.ts +121 -3
- package/dist/plan-format.js +176 -35
- package/docs/plan-contract.md +52 -5
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/approval-gate/index.ts +2 -2
- package/kit/hooks/hooks/merge-guard/lane-plan.ts +1 -1
- package/kit/hooks/hooks/open-question/record.ts +81 -1
- package/kit/hooks/hooks/open-question/run.ts +17 -3
- package/kit/hooks/hooks/question-session-end/index.ts +18 -0
- package/kit/hooks/hooks/question-session-end/run.ts +37 -0
- package/kit/hooks/hooks/sent-file/record.ts +4 -1
- package/kit/hooks/hooks/shared/board-lanes.ts +33 -12
- package/kit/hooks/hooks/shared/boards.ts +22 -6
- package/kit/hooks/hooks.json +8 -0
- package/kit/workflow/skills/orchestrate/references/herd.md +3 -3
- package/kit/workflow/skills/orchestrate/references/waiting.md +15 -1
- package/kit/workflow/skills/plan/SKILL.md +3 -1
- package/kit/workflow/skills/plan/references/template-English.md +2 -0
- package/kit/workflow/skills/plan/references/template-German.md +2 -0
- package/package.json +1 -1
- package/schemas/jackherd.schema.json +7 -0
- package/src/brief/command.ts +1 -1
- package/src/brief/lane-facts.ts +2 -2
- package/src/check/command.ts +2 -1
- package/src/check/plan-structure-check.ts +68 -14
- package/src/check/settings.ts +6 -1
- package/src/check/skill-budget-defaults.ts +9 -4
- package/src/config/schema.ts +3 -0
- package/src/lane/close-board.ts +4 -4
- package/src/lane/close-command.ts +2 -2
- package/src/lane/dev-command.ts +1 -1
- package/src/lane/port-command.ts +1 -1
- package/src/plan-contract/command.ts +2 -1
- package/src/plan-contract/contract.ts +30 -9
- package/src/plan-format/bare-codes.ts +4 -4
- package/src/plan-format/dependencies.ts +5 -5
- package/src/plan-format/lanes.ts +3 -4
- package/src/plan-format/markdown.ts +125 -10
- package/src/plan-format/open-status.ts +15 -0
- package/src/plan-format/phases.ts +45 -4
- package/src/plan-format/plan-head.ts +31 -9
- package/src/plan-format/plan-structure.ts +43 -7
- package/src/plan-format/progress.ts +3 -0
- package/src/plan-format/sprint-document.ts +4 -4
- package/src/plan-format/unrecognized-sections.ts +21 -0
- package/src/plan-format/waiting.ts +13 -13
- package/src/plans/board-close.ts +25 -16
- package/src/plans/board-pick.ts +1 -1
- package/src/plans/closable-records.ts +11 -10
- package/src/plans/close-reason.ts +19 -0
- package/src/plans/ensure-panes.ts +2 -2
- package/src/plans/layout-summary.ts +8 -1
- package/src/plans/layout.ts +14 -5
- package/src/plans/live.ts +3 -2
- package/src/plans/question-records.ts +23 -2
- package/src/plans/questions-read.ts +2 -2
- package/src/plans/sent-files-read.ts +2 -2
- package/src/plans/sent-files.ts +12 -4
- package/src/plans/settings.ts +3 -0
- package/src/plans/snapshot.ts +9 -5
- package/src/plans/sprint-export.ts +1 -1
- package/src/plans/texts.ts +3 -0
- package/src/plans/view.ts +1 -0
- package/src/resume/coordinator-read.ts +2 -2
- package/src/settled/command.ts +1 -1
- package/src/settled/settle.ts +5 -5
- package/src/sync/render.ts +20 -5
- package/src/sync/sync-plan.ts +6 -2
- package/src/sync/sync.ts +3 -2
- package/src/vocabulary/index.ts +192 -4
- package/src/waiting/command.ts +82 -26
- package/src/watch/appointments.ts +13 -20
- package/src/watch/command.ts +100 -6
- package/src/watch/lane-progress.ts +1 -1
- package/src/watch/lane-report.ts +4 -0
- package/src/watch/merge-overlap-read.ts +2 -2
- package/src/watch/obsolete-proposals.ts +175 -0
- package/src/watch/obsolete-records.ts +77 -0
- package/src/watch/plan-missing-read.ts +3 -1
- package/src/watch/reference-paths.ts +2 -0
- package/src/watch/watch-events.ts +20 -0
package/src/vocabulary/index.ts
CHANGED
|
@@ -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
|
-
|
|
366
|
-
|
|
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
|
-
|
|
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
|
|
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]}`
|
package/src/waiting/command.ts
CHANGED
|
@@ -1,14 +1,64 @@
|
|
|
1
1
|
import { installedReferenceHint } from '../lane/skill-hint'
|
|
2
|
+
import { clockText, DAY_MS } from '../plan-format/clock'
|
|
2
3
|
import { terminalSafe } from '../plan-format/terminal-text'
|
|
3
4
|
import type { WaitingItem } from '../plan-format/waiting'
|
|
5
|
+
import { CLOSE_REASON, closeReasonOf } from '../plans/close-reason'
|
|
4
6
|
import { mainCheckout } from '../plans/git'
|
|
7
|
+
import { type QuestionRecord, questionTextOf } from '../plans/question-records'
|
|
5
8
|
import { closeAllQuestionRecords, closeQuestionRecords } from '../plans/questions-read'
|
|
9
|
+
import { type SentFileRecord, sentTextOf } from '../plans/sent-files'
|
|
6
10
|
import { closeAllSentRecords, closeSentRecords } from '../plans/sent-files-read'
|
|
7
|
-
import { recordWaitingItems } from '../plans/waiting-records'
|
|
11
|
+
import { readWaitingRecords, recordWaitingItems } from '../plans/waiting-records'
|
|
8
12
|
import { CLI_NAME } from '../version/release'
|
|
9
13
|
import { REFERENCE, referenceLine } from '../watch/reference-paths'
|
|
10
14
|
|
|
11
|
-
const USAGE = `usage: ${CLI_NAME} waiting [close <id> ... | close --all]`
|
|
15
|
+
const USAGE = `usage: ${CLI_NAME} waiting [--closed | close <id> ... [--reason "<text>"] | close --all [--reason "<text>"]]`
|
|
16
|
+
|
|
17
|
+
const CLOSE_ALL = '--all'
|
|
18
|
+
const CLOSED = '--closed'
|
|
19
|
+
const REASON = '--reason'
|
|
20
|
+
|
|
21
|
+
/** A closed record as `jh waiting --closed` lists it. */
|
|
22
|
+
type ClosedRecord = { id: string; closedAt: number; closedReason?: string | null; text: string }
|
|
23
|
+
|
|
24
|
+
const closedRecordsOf = (sent: readonly SentFileRecord[], questions: readonly QuestionRecord[]): ClosedRecord[] =>
|
|
25
|
+
[
|
|
26
|
+
...sent.map((record) => ({ ...record, text: sentTextOf(record) })),
|
|
27
|
+
...questions.map((record) => ({ ...record, text: questionTextOf(record) })),
|
|
28
|
+
].flatMap(({ id, closedAt, closedReason, text }) => (closedAt === null ? [] : [{ id, closedAt, closedReason, text }]))
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The lines `jh waiting --closed` prints: the records closed within the last 24 hours, newest first, each as
|
|
32
|
+
* `id · time of closing · reason · text` (a dash for a record closed without a reason: one that predates the field, or one the hook closed itself); a note when none.
|
|
33
|
+
*/
|
|
34
|
+
export const closedListing = (
|
|
35
|
+
sent: readonly SentFileRecord[],
|
|
36
|
+
questions: readonly QuestionRecord[],
|
|
37
|
+
now: number,
|
|
38
|
+
): string[] => {
|
|
39
|
+
const recent = closedRecordsOf(sent, questions)
|
|
40
|
+
.filter(({ closedAt }) => now - closedAt <= DAY_MS)
|
|
41
|
+
.sort((left, right) => right.closedAt - left.closedAt)
|
|
42
|
+
return recent.length === 0
|
|
43
|
+
? ['nothing was closed in the last 24 hours']
|
|
44
|
+
: recent.map(({ id, closedAt, closedReason, text }) =>
|
|
45
|
+
terminalSafe(`${id} · ${clockText(closedAt)} · ${closedReason ?? '-'} · ${text}`),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What `jh waiting close` was asked to close and with which reason. */
|
|
50
|
+
export type CloseArguments = { all: true; reason: string } | { all: false; ids: string[]; reason: string }
|
|
51
|
+
|
|
52
|
+
/** The arguments after `close`: the ids or `--all`, then optionally `--reason "<text>"`; the reason is `coordinator` plus the text. Null when they do not fit. */
|
|
53
|
+
export const closeArgumentsOf = (args: readonly string[]): CloseArguments | null => {
|
|
54
|
+
const at = args.indexOf(REASON)
|
|
55
|
+
if (at !== -1 && at !== args.length - 2) return null
|
|
56
|
+
const ids = at === -1 ? [...args] : args.slice(0, at)
|
|
57
|
+
const all = ids.includes(CLOSE_ALL)
|
|
58
|
+
if (ids.length === 0 || (all && ids.length > 1)) return null
|
|
59
|
+
const reason = closeReasonOf(CLOSE_REASON.coordinator, at === -1 ? '' : args[at + 1])
|
|
60
|
+
return all ? { all, reason } : { all, ids, reason }
|
|
61
|
+
}
|
|
12
62
|
|
|
13
63
|
/**
|
|
14
64
|
* The lines `jh waiting` prints: id, time and text of each item, made safe for the terminal, then the reference that
|
|
@@ -19,41 +69,47 @@ export const waitingListing = (items: readonly WaitingItem[], hint = ''): string
|
|
|
19
69
|
? ['nothing waits on you']
|
|
20
70
|
: [...items.map(({ since, item, id }) => terminalSafe(`${id} · ${since} · ${item}`)), ...referenceLine(hint)]
|
|
21
71
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export const closeWaiting = (root: string, ids: readonly string[], now: number): number =>
|
|
26
|
-
closeSentRecords(root, ids, now) + closeQuestionRecords(root, ids, now)
|
|
72
|
+
/** Closes the records the ids name with `reason`, files and questions alike; how many it closed. */
|
|
73
|
+
export const closeWaiting = (root: string, ids: readonly string[], now: number, reason: string): number =>
|
|
74
|
+
closeSentRecords(root, ids, now, reason) + closeQuestionRecords(root, ids, now, reason)
|
|
27
75
|
|
|
28
|
-
/** Closes every open record
|
|
29
|
-
export const closeAllWaiting = (root: string, now: number): number =>
|
|
30
|
-
closeAllSentRecords(root, now) + closeAllQuestionRecords(root, now)
|
|
76
|
+
/** Closes every open record with `reason`, files and questions alike; how many it closed. */
|
|
77
|
+
export const closeAllWaiting = (root: string, now: number, reason: string): number =>
|
|
78
|
+
closeAllSentRecords(root, now, reason) + closeAllQuestionRecords(root, now, reason)
|
|
31
79
|
|
|
32
80
|
/**
|
|
33
81
|
* `jh waiting` lists the files sent to the user and the questions left unanswered that no one closed (their id, time
|
|
34
|
-
* and text); `
|
|
35
|
-
* command line.
|
|
82
|
+
* and text); `--closed` lists what was closed in the last 24 hours with its reason; `jh waiting close <id> ...` or
|
|
83
|
+
* `close --all` closes them once the user has answered. Exit 0, 1 on a wrong command line.
|
|
36
84
|
*/
|
|
37
85
|
export const run = async (args: string[]): Promise<number> => {
|
|
38
86
|
const root = mainCheckout(process.cwd())
|
|
39
|
-
if (args.length === 0)
|
|
40
|
-
|
|
41
|
-
recordWaitingItems(root, Date.now()),
|
|
42
|
-
installedReferenceHint(root, REFERENCE.waiting),
|
|
43
|
-
))
|
|
44
|
-
console.log(line)
|
|
45
|
-
return 0
|
|
46
|
-
}
|
|
87
|
+
if (args.length === 0) return printWaiting(root)
|
|
88
|
+
if (args.length === 1 && args[0] === CLOSED) return printClosed(root)
|
|
47
89
|
const [command, ...rest] = args
|
|
48
|
-
|
|
90
|
+
const close = command === 'close' ? closeArgumentsOf(rest) : null
|
|
91
|
+
if (close === null) {
|
|
49
92
|
console.error(USAGE)
|
|
50
93
|
return 1
|
|
51
94
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
const closed = rest[0] === CLOSE_ALL ? closeAllWaiting(root, Date.now()) : closeWaiting(root, rest, Date.now())
|
|
95
|
+
const closed = close.all
|
|
96
|
+
? closeAllWaiting(root, Date.now(), close.reason)
|
|
97
|
+
: closeWaiting(root, close.ids, Date.now(), close.reason)
|
|
57
98
|
console.log(`closed ${closed}`)
|
|
58
99
|
return 0
|
|
59
100
|
}
|
|
101
|
+
|
|
102
|
+
const printWaiting = (root: string): number => {
|
|
103
|
+
for (const line of waitingListing(
|
|
104
|
+
recordWaitingItems(root, Date.now()),
|
|
105
|
+
installedReferenceHint(root, REFERENCE.waiting),
|
|
106
|
+
))
|
|
107
|
+
console.log(line)
|
|
108
|
+
return 0
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const printClosed = (root: string): number => {
|
|
112
|
+
const { sent, questions } = readWaitingRecords(root)
|
|
113
|
+
for (const line of closedListing(sent, questions, Date.now())) console.log(line)
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
@@ -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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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] ?? ''
|
|
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
|
|
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
|
|
137
|
-
const
|
|
138
|
-
const missing = needed.filter((
|
|
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
|
|
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),
|
package/src/watch/command.ts
CHANGED
|
@@ -50,12 +50,15 @@ import { contextReport } from '../context/report'
|
|
|
50
50
|
import { agentLog } from '../context/session-log'
|
|
51
51
|
import { type LaneProgress, parseSprintLanes, type SprintLane } from '../plan-format'
|
|
52
52
|
import { terminalSafe } from '../plan-format/terminal-text'
|
|
53
|
+
import { openWaitingItems } from '../plan-format/waiting'
|
|
53
54
|
import { boardFor, foundLanesOf } from '../plans/board-pick'
|
|
54
55
|
import { ensurePlanPanes } from '../plans/ensure-panes'
|
|
55
56
|
import { errorText } from '../plans/error-text'
|
|
56
57
|
import { baseReaderOf, mainCheckout, worktreeNames, worktreeRoot } from '../plans/git'
|
|
57
58
|
import { discoverLanes, isUnchanged, laneSetChange } from '../plans/lane-discovery'
|
|
58
59
|
import { readerOncePerRef } from '../plans/plan-heads'
|
|
60
|
+
import { readQuestionRecords } from '../plans/questions-read'
|
|
61
|
+
import { closeSentRecords, readSentRecords } from '../plans/sent-files-read'
|
|
59
62
|
import { CLOCK_LOCALE, type PlansSettings, plansSettingsOf, worktreeOf } from '../plans/settings'
|
|
60
63
|
import { readRunningSubagents, subagentSourceOf } from '../plans/subagents-read'
|
|
61
64
|
import { boardTexts } from '../plans/texts'
|
|
@@ -182,6 +185,21 @@ import {
|
|
|
182
185
|
import { isBehindOnBase, mainCheckoutDutyLine } from './main-checkout'
|
|
183
186
|
import { mergeOverlapReader } from './merge-overlap-read'
|
|
184
187
|
import { monitorToolAvailable } from './monitor-tool'
|
|
188
|
+
import {
|
|
189
|
+
obsoleteProposalEvents,
|
|
190
|
+
type ProposalInput,
|
|
191
|
+
type ProposalState,
|
|
192
|
+
type WaitingObsoleteEvent,
|
|
193
|
+
waitingLanesOf,
|
|
194
|
+
} from './obsolete-proposals'
|
|
195
|
+
import {
|
|
196
|
+
type Closing,
|
|
197
|
+
closingsByReason,
|
|
198
|
+
laneAbsenceOf,
|
|
199
|
+
obsoleteSendsOf,
|
|
200
|
+
WAITING_CLOSED_EVENT,
|
|
201
|
+
type WaitingClosedEvent,
|
|
202
|
+
} from './obsolete-records'
|
|
185
203
|
import { parseWatchOptions, type WatchOptions } from './options'
|
|
186
204
|
import type { PlanMissingEvent, PlanMissingState } from './plan-missing'
|
|
187
205
|
import { readPlanMissingEvents } from './plan-missing-read'
|
|
@@ -306,6 +324,8 @@ type Watch = {
|
|
|
306
324
|
herdr: boolean
|
|
307
325
|
/** `lanes.backend` and the worktree directory of the main checkout's config; undefined without one, then the backend is never touched. */
|
|
308
326
|
backend: MainCheckoutBackend | undefined
|
|
327
|
+
/** Runs in the main checkout, not in a lane's worktree: only then does it close records of "Your turn" or propose to. */
|
|
328
|
+
inMainCheckout: boolean
|
|
309
329
|
}
|
|
310
330
|
|
|
311
331
|
/** The coordinator's agent name (null when none is found), its last read (null before the first one) and the tokens of that read (null without a value). */
|
|
@@ -376,6 +396,10 @@ type Watching = {
|
|
|
376
396
|
unlinkedArtifacts: UnlinkedState
|
|
377
397
|
/** Per working lane, the plan last reported as `plan missing on base`; persisted like the fingerprints. */
|
|
378
398
|
planMissing: PlanMissingState
|
|
399
|
+
/** The lanes of open waiting points missing at the last heartbeat; persisted like the fingerprints. */
|
|
400
|
+
waitingMissingLanes: string[]
|
|
401
|
+
/** The reasons proposed per open waiting point as `waiting obsolete?`; persisted like the fingerprints. */
|
|
402
|
+
waitingProposals: ProposalState
|
|
379
403
|
/** The approval requests of the last poll; null before the first read, so the stored ids stay. */
|
|
380
404
|
approvalRequests: ApprovalRequestFile[] | null
|
|
381
405
|
/** The board's appointment table at the last poll and the time it was read at; null before the first read and while the board cannot be read. */
|
|
@@ -443,6 +467,8 @@ const recordedOf = (watch: Watch, watching: Watching): WatchState => {
|
|
|
443
467
|
laneStalls: watching.laneStalls,
|
|
444
468
|
unlinkedArtifacts: watching.unlinkedArtifacts,
|
|
445
469
|
planMissing: watching.planMissing,
|
|
470
|
+
waitingMissingLanes: watching.waitingMissingLanes,
|
|
471
|
+
waitingProposals: watching.waitingProposals,
|
|
446
472
|
approvals: watching.approvalRequests ? recordApprovals(watching.approvalRequests) : withReads.approvals,
|
|
447
473
|
...recordedSourceState(withReads.sources, watching.sourceRuns, watching.sources.sources, watching.githubReadAt),
|
|
448
474
|
appointments: watching.appointments
|
|
@@ -609,7 +635,7 @@ const boardReadOf = async (
|
|
|
609
635
|
pullRequests: readonly WatchedPullRequest[],
|
|
610
636
|
): Promise<{ progress: Record<string, LaneProgress>; checkoutRows: number }> => {
|
|
611
637
|
const text = await Bun.file(watch.sprintFile).text()
|
|
612
|
-
const sprintLanes = parseSprintLanes(text, watch.settings.
|
|
638
|
+
const sprintLanes = parseSprintLanes(text, watch.settings.boardLanguage)
|
|
613
639
|
const watched = sprintLanes.filter((lane) => lanes.includes(lane.name))
|
|
614
640
|
const planPulls = watchedPlanPullRequests(pullRequests, watch.settings)
|
|
615
641
|
const headReaderOf = readerOncePerRef((ref) => baseReaderOf(watch.root, ref))
|
|
@@ -1108,13 +1134,13 @@ const readBoardText = (watch: Watch): string | null => {
|
|
|
1108
1134
|
const readAppointments = (watch: Watch, now: number): Watching['appointments'] => {
|
|
1109
1135
|
const board = readBoardText(watch)
|
|
1110
1136
|
if (board === null) return null
|
|
1111
|
-
return { table: readAppointmentTable(board, watch.settings.
|
|
1137
|
+
return { table: readAppointmentTable(board, watch.settings.boardLanguage, new Date(now)), readAt: now }
|
|
1112
1138
|
}
|
|
1113
1139
|
|
|
1114
1140
|
/** 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
1141
|
const waitingLineOf = (watch: Watch, hint = ''): string | null => {
|
|
1116
1142
|
return waitingLine(
|
|
1117
|
-
waitingItemsOf(readBoardText(watch), watch.settings.
|
|
1143
|
+
waitingItemsOf(readBoardText(watch), watch.settings.boardLanguage, readWaitingRecords(watch.root), Date.now()),
|
|
1118
1144
|
hint,
|
|
1119
1145
|
)
|
|
1120
1146
|
}
|
|
@@ -1122,7 +1148,7 @@ const waitingLineOf = (watch: Watch, hint = ''): string | null => {
|
|
|
1122
1148
|
/** The board's lanes, null when the board cannot be read. */
|
|
1123
1149
|
const readSprintLanes = (watch: Watch): SprintLane[] | null => {
|
|
1124
1150
|
const board = readBoardText(watch)
|
|
1125
|
-
return board === null ? null : parseSprintLanes(board, watch.settings.
|
|
1151
|
+
return board === null ? null : parseSprintLanes(board, watch.settings.boardLanguage)
|
|
1126
1152
|
}
|
|
1127
1153
|
|
|
1128
1154
|
/** The sprint's lane names; the previous ones when the sprint cannot be read, so no lane drops out over it. */
|
|
@@ -1322,6 +1348,69 @@ const unlinkedArtifactEventsOf = (watch: Watch, watching: Watching): UnlinkedArt
|
|
|
1322
1348
|
}
|
|
1323
1349
|
}
|
|
1324
1350
|
|
|
1351
|
+
/** Closes the sends through the log, one call per reason; returns the closings that were written, so a failed write is neither reported nor counted. */
|
|
1352
|
+
const closeObsoleteSends = (root: string, closings: readonly Closing[], now: number): Closing[] =>
|
|
1353
|
+
[...closingsByReason(closings)].flatMap(([reason, ids]) => {
|
|
1354
|
+
try {
|
|
1355
|
+
closeSentRecords(root, ids, now, reason)
|
|
1356
|
+
return ids.map((id) => ({ id, reason }))
|
|
1357
|
+
} catch {
|
|
1358
|
+
return []
|
|
1359
|
+
}
|
|
1360
|
+
})
|
|
1361
|
+
|
|
1362
|
+
/** What the proposals read besides the records: the pull requests of the last GitHub read and the lanes' branches. */
|
|
1363
|
+
const proposalContextOf = (watching: Watching): Pick<ProposalInput, 'pullRequests' | 'laneBranches' | 'knownLanes'> => {
|
|
1364
|
+
const laneBranches = { ...watching.state.laneBranches, ...watching.laneBranches }
|
|
1365
|
+
return {
|
|
1366
|
+
pullRequests: answered(watching.github)?.pullRequests ?? null,
|
|
1367
|
+
laneBranches,
|
|
1368
|
+
knownLanes: new Set([...Object.keys(laneBranches), ...watching.lanes]),
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* This heartbeat's run over "Your turn", only from the main checkout: closes the sends a safe signal makes obsolete
|
|
1374
|
+
* (`waiting closed`, one event per run that closes something), then proposes what only looks obsolete (`waiting
|
|
1375
|
+
* obsolete?`, each point and reason once); records the missing lanes and the proposals in `watching`. Never writes
|
|
1376
|
+
* into the board. A read error closes nothing and reports nothing: the next heartbeat reads again.
|
|
1377
|
+
*/
|
|
1378
|
+
const waitingObsoleteEventsOf = (
|
|
1379
|
+
watch: Watch,
|
|
1380
|
+
watching: Watching,
|
|
1381
|
+
now: number,
|
|
1382
|
+
): (WaitingClosedEvent | WaitingObsoleteEvent)[] => {
|
|
1383
|
+
if (!watch.inMainCheckout) return []
|
|
1384
|
+
try {
|
|
1385
|
+
const worktreeDirectory = join(watch.root, watch.settings.worktreeDirectory)
|
|
1386
|
+
if (!existsSync(worktreeDirectory)) return []
|
|
1387
|
+
const sends = readSentRecords(watch.root)
|
|
1388
|
+
const questions = readQuestionRecords(watch.root)
|
|
1389
|
+
const boardText = readBoardText(watch)
|
|
1390
|
+
const board = boardText === null ? null : openWaitingItems(boardText, watch.settings.boardLanguage)
|
|
1391
|
+
const { missing, gone } = laneAbsenceOf(
|
|
1392
|
+
waitingLanesOf({ sends, questions, board, worktreeDirectory }),
|
|
1393
|
+
new Set(worktreeNames(worktreeDirectory)),
|
|
1394
|
+
watching.waitingMissingLanes,
|
|
1395
|
+
)
|
|
1396
|
+
const closings = closeObsoleteSends(watch.root, obsoleteSendsOf(sends, worktreeDirectory, gone), now)
|
|
1397
|
+
const closed = new Set(closings.map(({ id }) => id))
|
|
1398
|
+
const { events, state } = obsoleteProposalEvents(watching.waitingProposals, {
|
|
1399
|
+
sends: sends.filter(({ id }) => !closed.has(id)),
|
|
1400
|
+
questions,
|
|
1401
|
+
board,
|
|
1402
|
+
worktreeDirectory,
|
|
1403
|
+
gone,
|
|
1404
|
+
...proposalContextOf(watching),
|
|
1405
|
+
})
|
|
1406
|
+
watching.waitingMissingLanes = missing
|
|
1407
|
+
watching.waitingProposals = state
|
|
1408
|
+
return closings.length === 0 ? events : [{ kind: WAITING_CLOSED_EVENT, closings }, ...events]
|
|
1409
|
+
} catch {
|
|
1410
|
+
return []
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1325
1414
|
/**
|
|
1326
1415
|
* This heartbeat's `plan missing on base` events: the working lanes whose board row, as their worktree has it, links a
|
|
1327
1416
|
* plan the base ref lacks, each once; records the state in `watching`. Nothing when the boards or git cannot be read:
|
|
@@ -1351,13 +1440,14 @@ const workingProgressOf = async (watch: Watch, watching: Watching): Promise<Reco
|
|
|
1351
1440
|
return progress
|
|
1352
1441
|
}
|
|
1353
1442
|
|
|
1354
|
-
/** The events a heartbeat of a report brings: the parallel-task proposals, the lanes without a task signal, the unlinked artifacts, the plans missing on the base. */
|
|
1443
|
+
/** The events a heartbeat of a report brings: the parallel-task proposals, the lanes without a task signal, the unlinked artifacts, the obsolete waiting points, the plans missing on the base. */
|
|
1355
1444
|
const heartbeatEventsOf = async (watch: Watch, watching: Watching, now: number): Promise<WatchEvent[]> => {
|
|
1356
1445
|
const progress = await workingProgressOf(watch, watching)
|
|
1357
1446
|
return [
|
|
1358
1447
|
...taskParallelEventsOf(watch, watching, now),
|
|
1359
1448
|
...noTaskSignalEventsOf(watch, watching, progress, now),
|
|
1360
1449
|
...unlinkedArtifactEventsOf(watch, watching),
|
|
1450
|
+
...waitingObsoleteEventsOf(watch, watching, now),
|
|
1361
1451
|
...planMissingEventsOf(watch, watching),
|
|
1362
1452
|
]
|
|
1363
1453
|
}
|
|
@@ -1421,6 +1511,7 @@ const printStreamHeartbeat = async (watch: Watch, watching: Watching, now: numbe
|
|
|
1421
1511
|
...taskParallelEventsOf(watch, watching, now),
|
|
1422
1512
|
...noTaskSignalEventsOf(watch, watching, progress, now),
|
|
1423
1513
|
...unlinkedArtifactEventsOf(watch, watching),
|
|
1514
|
+
...waitingObsoleteEventsOf(watch, watching, now),
|
|
1424
1515
|
...planMissingEventsOf(watch, watching),
|
|
1425
1516
|
]
|
|
1426
1517
|
for (const event of events) console.log(`${watchEventLine(event, null)}${hintFor(watch, eventReference(event))}`)
|
|
@@ -1449,7 +1540,7 @@ const selfMergesOf = (watch: Watch, watching: Watching, now: number): number[] =
|
|
|
1449
1540
|
const open = Object.entries(watching.state.pullRequests).flatMap(([number, { state }]) =>
|
|
1450
1541
|
state === PULL_REQUEST_STATE.open ? [Number(number)] : [],
|
|
1451
1542
|
)
|
|
1452
|
-
return selfMergePullRequests(board, watch.settings.
|
|
1543
|
+
return selfMergePullRequests(board, watch.settings.boardLanguage, open, now, watch.selfMergeMinutes)
|
|
1453
1544
|
}
|
|
1454
1545
|
|
|
1455
1546
|
const clearInputOf = (
|
|
@@ -1589,6 +1680,8 @@ const initialWatching = (
|
|
|
1589
1680
|
laneStalls: start.state.laneStalls,
|
|
1590
1681
|
unlinkedArtifacts: start.state.unlinkedArtifacts,
|
|
1591
1682
|
planMissing: start.state.planMissing,
|
|
1683
|
+
waitingMissingLanes: start.state.waitingMissingLanes,
|
|
1684
|
+
waitingProposals: start.state.waitingProposals,
|
|
1592
1685
|
approvalRequests: null,
|
|
1593
1686
|
appointments: null,
|
|
1594
1687
|
reportQuestions: null,
|
|
@@ -1737,6 +1830,7 @@ const watchOf = (options: WatchOptions): Watch => {
|
|
|
1737
1830
|
selfMergeMinutes: config.guard.selfMerge.approvalMinutes,
|
|
1738
1831
|
herdr: insideHerdr(settings.adapter, process.env),
|
|
1739
1832
|
backend: mainCheckoutBackendOf(root, checkoutRoot, config),
|
|
1833
|
+
inMainCheckout: checkoutRoot === root,
|
|
1740
1834
|
}
|
|
1741
1835
|
}
|
|
1742
1836
|
|