jackherd 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/INSTALL.md +1 -1
- package/README.md +65 -32
- package/dist/{chunk-MCP4XJQH.js → chunk-RMMXD4G5.js} +18 -6
- package/dist/config.d.ts +99 -4
- package/dist/config.js +105 -29
- package/dist/plan-format.d.ts +51 -6
- package/dist/plan-format.js +49 -7
- package/docs/workflow-dark.svg +67 -63
- package/docs/workflow.svg +67 -63
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/ask-rules.json +15 -0
- package/kit/hooks/hooks/ask-hint/decision.ts +82 -14
- package/kit/hooks/hooks/ask-hint/index.ts +15 -19
- package/kit/hooks/hooks/ask-hint/settings.ts +3 -18
- package/kit/hooks/hooks/git-guard/decision.ts +50 -102
- package/kit/hooks/hooks/merge-guard/always-ask.ts +63 -0
- package/kit/hooks/hooks/merge-guard/{plan-approval-settings.ts → board-settings.ts} +21 -67
- package/kit/hooks/hooks/merge-guard/check-free.ts +20 -0
- package/kit/hooks/hooks/merge-guard/decision.ts +127 -187
- package/kit/hooks/hooks/merge-guard/index.ts +6 -3
- package/kit/hooks/hooks/merge-guard/lane-plan.ts +79 -0
- package/kit/hooks/hooks/merge-guard/run.ts +59 -54
- package/kit/hooks/hooks/merge-guard/settings.ts +13 -1
- package/kit/hooks/hooks/merge-guard/standing-approval.ts +142 -0
- package/kit/hooks/hooks/open-question/index.ts +26 -0
- package/kit/hooks/hooks/open-question/record.ts +135 -0
- package/kit/hooks/hooks/open-question/run.ts +57 -0
- package/kit/hooks/hooks/open-question/store.ts +39 -0
- package/kit/hooks/hooks/sent-file/store.ts +3 -12
- package/kit/hooks/hooks/shared/config.ts +7 -0
- package/kit/hooks/hooks/shared/dialog-answers.ts +9 -9
- package/kit/hooks/hooks/shared/files.ts +7 -0
- package/kit/hooks/hooks/shared/lane.ts +1 -1
- package/kit/hooks/hooks/shared/newest-verdict.ts +10 -0
- package/kit/hooks/hooks/shared/private-directory.ts +56 -9
- package/kit/hooks/hooks/shared/program-position.ts +2 -75
- package/kit/hooks/hooks/shared/shell-aliases.ts +1 -71
- package/kit/hooks/hooks/{ask-hint → shared}/transcript.ts +37 -6
- package/kit/hooks/hooks/{merge-guard → shared}/xargs-program.ts +10 -8
- package/kit/hooks/hooks/task-hint/decision.ts +49 -0
- package/kit/hooks/hooks/task-hint/index.ts +79 -0
- package/kit/hooks/hooks/task-hint/run.ts +62 -0
- package/kit/hooks/hooks/task-hint/store.ts +51 -0
- package/kit/hooks/hooks.json +13 -5
- package/kit/review/agents/skill-review.md +34 -0
- package/kit/review/skills/pre-commit/SKILL.md +1 -1
- package/kit/rules/rules/kit/pull-requests.md +2 -0
- package/kit/workflow/skills/follow/SKILL.md +3 -10
- package/kit/workflow/skills/implement/SKILL.md +8 -6
- package/kit/workflow/skills/orchestrate/SKILL.md +218 -433
- package/kit/workflow/skills/orchestrate/handover/decision.ts +43 -8
- package/kit/workflow/skills/orchestrate/handover/index.ts +32 -4
- package/kit/workflow/skills/orchestrate/handover/standing-approval.ts +84 -0
- package/kit/workflow/skills/orchestrate/references/acceptance.md +15 -0
- package/kit/workflow/skills/orchestrate/references/close-lane.md +17 -0
- package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +43 -17
- package/kit/workflow/skills/orchestrate/references/draft-review.md +19 -0
- package/kit/workflow/skills/orchestrate/references/events.md +86 -0
- package/kit/workflow/skills/orchestrate/references/herd.md +6 -6
- package/kit/workflow/skills/orchestrate/references/lane-handover.md +18 -0
- package/kit/workflow/skills/orchestrate/references/lane-session.md +25 -0
- package/kit/workflow/skills/orchestrate/references/lane-setup.md +37 -0
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +22 -10
- package/kit/workflow/skills/orchestrate/references/sensitive-input.md +5 -7
- package/kit/workflow/skills/plan/SKILL.md +6 -5
- package/kit/workflow/skills/plan/references/template-English.md +4 -4
- package/kit/workflow/skills/plan/references/template-German.md +4 -4
- package/package.json +3 -1
- package/recipes/README.md +2 -1
- package/recipes/sources.md +5 -1
- package/schemas/jackherd.schema.json +139 -5
- package/src/check/agent-text-reviewers.ts +23 -0
- package/src/check/command.ts +2 -0
- package/src/check/plan-structure-check.ts +49 -2
- package/src/check/plan.ts +8 -1
- package/src/check/schema.ts +9 -0
- package/src/check/skill-budget-defaults.ts +55 -0
- package/src/check/skill-budgets.ts +185 -0
- package/src/cli.ts +2 -0
- package/src/config/auto-window.ts +8 -0
- package/src/config/link-kinds.ts +7 -1
- package/src/config/schema.ts +82 -25
- package/src/config/tones.ts +27 -0
- package/src/lane/close-board.ts +106 -0
- package/src/lane/close-check.ts +183 -0
- package/src/lane/close-command-line.ts +11 -0
- package/src/lane/close-command.ts +217 -0
- package/src/lane/close-options.ts +43 -0
- package/src/lane/close-survey.ts +83 -0
- package/src/lane/close.ts +141 -0
- package/src/lane/command.ts +13 -0
- package/src/lane-prompt/additions.ts +14 -0
- package/src/lane-prompt/build.ts +74 -0
- package/src/lane-prompt/command.ts +93 -0
- package/src/lane-prompt/input.ts +157 -0
- package/src/lane-prompt/options.ts +70 -0
- package/src/lane-prompt/texts.ts +101 -0
- package/src/mermaid/command.ts +36 -0
- package/src/mermaid/problems.ts +48 -0
- package/src/plan-format/clock.ts +8 -0
- package/src/plan-format/dependencies.ts +9 -6
- package/src/plan-format/index.ts +1 -0
- package/src/plan-format/short-names.ts +71 -0
- package/src/plan-format/waiting.ts +22 -4
- package/src/plans/appointment-rows.ts +83 -0
- package/src/plans/ask-coordinator.ts +111 -0
- package/src/plans/ask-target.ts +36 -0
- package/src/plans/board-close.ts +85 -0
- package/src/plans/child-stop.ts +51 -0
- package/src/plans/closable-records.ts +34 -0
- package/src/plans/command.ts +3 -2
- package/src/plans/fetch.ts +26 -3
- package/src/plans/frame.ts +5 -12
- package/src/plans/git.ts +54 -5
- package/src/plans/groomed.ts +27 -0
- package/src/plans/herd-claim.ts +173 -0
- package/src/plans/herd-memory.ts +36 -25
- package/src/plans/lane-plans.ts +44 -13
- package/src/plans/lane-summary.ts +253 -45
- package/src/plans/layout-phase.ts +36 -11
- package/src/plans/layout-summary.ts +89 -18
- package/src/plans/layout.ts +502 -98
- package/src/plans/link-chips.ts +23 -4
- package/src/plans/live.ts +178 -23
- package/src/plans/next-rows.ts +44 -0
- package/src/plans/phase-ids.ts +8 -0
- package/src/plans/phase-source.ts +15 -0
- package/src/plans/plan-cache.ts +28 -0
- package/src/plans/plan-heads.ts +54 -0
- package/src/plans/planned.ts +22 -0
- package/src/plans/pull-requests.ts +17 -2
- package/src/plans/question-records.ts +48 -0
- package/src/plans/questions-read.ts +13 -0
- package/src/plans/read-cache.ts +8 -0
- package/src/plans/record-id.ts +4 -0
- package/src/plans/restart-decision.ts +13 -4
- package/src/plans/row-actions.ts +52 -0
- package/src/plans/row-focus.ts +148 -0
- package/src/plans/row-targets.ts +7 -0
- package/src/plans/selection.ts +111 -18
- package/src/plans/sent-files-read.ts +6 -15
- package/src/plans/sent-files.ts +7 -14
- package/src/plans/settings.ts +12 -1
- package/src/plans/signalled-phase.ts +42 -0
- package/src/plans/signals-read.ts +12 -4
- package/src/plans/snapshot.ts +162 -81
- package/src/plans/source-asked.ts +19 -0
- package/src/plans/source-prefix.ts +17 -0
- package/src/plans/source-rows.ts +188 -0
- package/src/plans/sprint-export.ts +11 -11
- package/src/plans/sprint-page.ts +5 -5
- package/src/plans/subagent-phases.ts +95 -0
- package/src/plans/subagent-tasks.ts +51 -0
- package/src/plans/subagents-read.ts +66 -17
- package/src/plans/subagents.ts +64 -6
- package/src/plans/supervisor.ts +14 -4
- package/src/plans/symbols.ts +3 -0
- package/src/plans/task-name.ts +34 -0
- package/src/plans/texts.ts +48 -6
- package/src/plans/tones.ts +30 -0
- package/src/plans/view.ts +25 -11
- package/src/plans/waiting-click.ts +295 -35
- package/src/plans/waiting-records.ts +39 -0
- package/src/plans/waiting-window.ts +8 -0
- package/src/preview/command.ts +25 -3
- package/src/shared/private-location.ts +89 -0
- package/src/shared/stat-cache.ts +48 -0
- package/src/sync/kit-source.ts +29 -10
- package/src/sync/kit.ts +2 -0
- package/src/sync/manifest.ts +2 -0
- package/src/sync/plan-ask-rules.ts +94 -0
- package/src/sync/plan-hooks.ts +2 -0
- package/src/sync/render.ts +3 -3
- package/src/sync/settings-hooks.ts +4 -1
- package/src/sync/sync-plan.ts +1 -0
- package/src/sync/sync.ts +24 -3
- package/src/task-signals/claude-read.ts +27 -10
- package/src/terminal/text.ts +16 -3
- package/src/terminal/types.ts +4 -0
- package/src/version/skill-diff.ts +46 -0
- package/src/vocabulary/index.ts +26 -6
- package/src/waiting/command.ts +22 -7
- package/src/watch/appointments.ts +1 -2
- package/src/watch/asking-lanes.ts +40 -0
- package/src/watch/base-tip.ts +11 -0
- package/src/watch/command.ts +330 -105
- package/src/watch/heartbeat-progress.ts +6 -18
- package/src/watch/kit-update.ts +28 -9
- package/src/watch/lane-name.ts +6 -0
- package/src/watch/lane-progress.ts +40 -8
- package/src/watch/lane-report-file.ts +20 -5
- package/src/watch/lane-report.ts +32 -4
- package/src/watch/lane-sources.ts +31 -3
- package/src/watch/main-checkout.ts +34 -0
- package/src/watch/plan-badges-read.ts +22 -9
- package/src/watch/plan-badges.ts +10 -2
- package/src/watch/pull-requests.ts +6 -2
- package/src/watch/reference-paths.ts +87 -0
- package/src/watch/report-questions.ts +4 -3
- package/src/watch/running-subagents.ts +43 -0
- package/src/watch/skill-change.ts +124 -32
- package/src/watch/source-change.ts +48 -0
- package/src/watch/source-entries.ts +55 -0
- package/src/watch/source-fields.ts +1 -0
- package/src/watch/source-statuses.ts +60 -0
- package/src/watch/source-tickets.ts +21 -1
- package/src/watch/sources.ts +47 -4
- package/src/watch/stream-output.ts +4 -3
- package/src/watch/task-parallel-auto.ts +208 -0
- package/src/watch/task-parallel-data-paths.ts +46 -0
- package/src/watch/task-parallel-input.ts +5 -15
- package/src/watch/task-parallel-read.ts +3 -1
- package/src/watch/task-parallel.ts +36 -7
- package/src/watch/task-signal-missing.ts +69 -0
- package/src/watch/task-signal-read.ts +29 -0
- package/src/watch/texts.ts +10 -0
- package/src/watch/update-available.ts +6 -3
- package/src/watch/watch-events.ts +23 -0
- package/src/watch/watcher-lock.ts +25 -29
- package/kit/hooks/hooks/merge-guard/plan-approval-record.ts +0 -23
- package/kit/hooks/hooks/merge-guard/plan-approval-sources.ts +0 -196
- package/kit/hooks/hooks/merge-guard/plan-approval.ts +0 -168
- package/kit/hooks/hooks/merge-guard/plan-growth.ts +0 -40
- package/kit/hooks/hooks/merge-guard/plan-pointers.ts +0 -194
- package/kit/hooks/hooks/shared/command-text-runners.ts +0 -71
- package/kit/hooks/hooks/shared/plan-open-phases.ts +0 -114
package/src/plans/settings.ts
CHANGED
|
@@ -3,13 +3,14 @@ import { join, relative } from 'node:path'
|
|
|
3
3
|
import { type JackherdConfig, parseJackherdConfig } from '../config'
|
|
4
4
|
import { PROJECT_CONFIG_PATH } from '../config/location'
|
|
5
5
|
import { readConfig } from '../config/read-config'
|
|
6
|
+
import type { ToneName } from '../config/tones'
|
|
6
7
|
import type { TicketSource } from '../watch/plan-badges'
|
|
7
8
|
|
|
8
9
|
export const CONFIG_PATH = PROJECT_CONFIG_PATH
|
|
9
10
|
|
|
10
11
|
export type PlansSettings = Pick<
|
|
11
12
|
JackherdConfig,
|
|
12
|
-
'planDirectory' | 'board' | 'worktreeDirectory' | 'baseBranch' | 'planLanguage' | 'responseLanguage'
|
|
13
|
+
'planDirectory' | 'board' | 'worktreeDirectory' | 'baseBranch' | 'planLanguage' | 'responseLanguage' | 'skillPrefix'
|
|
13
14
|
> & {
|
|
14
15
|
adapter: JackherdConfig['terminal']['adapter']
|
|
15
16
|
fetchIntervalMinutes: number
|
|
@@ -17,6 +18,12 @@ export type PlansSettings = Pick<
|
|
|
17
18
|
context: JackherdConfig['plans']['context']
|
|
18
19
|
/** The `watch.sources` marked `ticket`, whose entries the view shows as badges. */
|
|
19
20
|
ticketSources: TicketSource[]
|
|
21
|
+
/** The other `watch.sources` by name, with the tone each names for a new report; the herd lists them in a block of their own. */
|
|
22
|
+
herdSources: { name: string; tone: ToneName | null }[]
|
|
23
|
+
/** By source name: what the herd puts before a row of "Waiting for you" that came from the source, its `label` or else its name. */
|
|
24
|
+
sourceLabels: Record<string, string>
|
|
25
|
+
/** The tone of each role in the herd. */
|
|
26
|
+
tones: JackherdConfig['plans']['tones']
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
/** The locale of the herd's clock: the herd is English, with a 24-hour clock. */
|
|
@@ -30,6 +37,7 @@ export const plansSettingsOf = (config: JackherdConfig): PlansSettings => ({
|
|
|
30
37
|
baseBranch: config.baseBranch,
|
|
31
38
|
planLanguage: config.planLanguage,
|
|
32
39
|
responseLanguage: config.responseLanguage,
|
|
40
|
+
skillPrefix: config.skillPrefix,
|
|
33
41
|
adapter: config.terminal.adapter,
|
|
34
42
|
fetchIntervalMinutes: config.plans.fetchIntervalMinutes,
|
|
35
43
|
linkKinds: config.plans.linkKinds,
|
|
@@ -37,6 +45,9 @@ export const plansSettingsOf = (config: JackherdConfig): PlansSettings => ({
|
|
|
37
45
|
ticketSources: config.watch.sources.flatMap(({ name, lane, ticket }) =>
|
|
38
46
|
ticket ? [{ name, lane: lane ?? null }] : [],
|
|
39
47
|
),
|
|
48
|
+
herdSources: config.watch.sources.flatMap(({ name, ticket, tone }) => (ticket ? [] : [{ name, tone: tone ?? null }])),
|
|
49
|
+
sourceLabels: Object.fromEntries(config.watch.sources.map(({ name, label }) => [name, label ?? name])),
|
|
50
|
+
tones: config.plans.tones,
|
|
40
51
|
})
|
|
41
52
|
|
|
42
53
|
/** The herd's slice of `.jackherd/config.json`, validated by the shared schema. */
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isPhaseOpen, type PhaseProgress, phaseLabel } from '../plan-format'
|
|
2
|
+
import type { ActiveTask } from './active-tasks'
|
|
3
|
+
|
|
4
|
+
/** `p24e` in a branch name, standing alone between separators: `feat/jackherd-p24e-task-signal`. */
|
|
5
|
+
const BRANCH_PHASE = /(?:^|[^a-z0-9])p(\d+[a-z]?)(?![a-z0-9])/i
|
|
6
|
+
|
|
7
|
+
/** The phase id a lane branch names (`feat/jackherd-p24e-task-signal` → `24e`); null for none. */
|
|
8
|
+
export const branchPhaseId = (branch: string | null): string | null =>
|
|
9
|
+
branch?.match(BRANCH_PHASE)?.[1]?.toLowerCase() ?? null
|
|
10
|
+
|
|
11
|
+
const newestStart = (tasks: readonly ActiveTask[]): number => Math.max(...tasks.map(({ startedAt }) => startedAt ?? 0))
|
|
12
|
+
|
|
13
|
+
/** The open phase with the newest task an agent reported; null when no open phase has one. */
|
|
14
|
+
const reportedPhase = (
|
|
15
|
+
phases: readonly PhaseProgress[],
|
|
16
|
+
reported: ReadonlyMap<string, readonly ActiveTask[]>,
|
|
17
|
+
): PhaseProgress | null =>
|
|
18
|
+
phases
|
|
19
|
+
.filter((phase) => isPhaseOpen(phase) && (reported.get(phase.id)?.length ?? 0) > 0)
|
|
20
|
+
.reduce<PhaseProgress | null>(
|
|
21
|
+
(best, phase) =>
|
|
22
|
+
best && newestStart(reported.get(best.id) ?? []) >= newestStart(reported.get(phase.id) ?? []) ? best : phase,
|
|
23
|
+
null,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The phase the lane says it works on (D112): the open phase with the newest reported task, else the open phase its
|
|
28
|
+
* branch names (`branchPhaseId`); null otherwise, and the ticks decide as before. A phase ticked to the end is never
|
|
29
|
+
* signalled, so a stale report or an old branch name cannot make finished work current.
|
|
30
|
+
*/
|
|
31
|
+
export const signalledPhase = (
|
|
32
|
+
phases: readonly PhaseProgress[],
|
|
33
|
+
reported: ReadonlyMap<string, readonly ActiveTask[]>,
|
|
34
|
+
branch: string | null,
|
|
35
|
+
): PhaseProgress | null => {
|
|
36
|
+
const id = branchPhaseId(branch)
|
|
37
|
+
return reportedPhase(phases, reported) ?? phases.find((phase) => phase.id === id && isPhaseOpen(phase)) ?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The sidebar title of a lane: the signalled phase as `P24d · 1/5`, else `fromProgress` (`shortTitle`). */
|
|
41
|
+
export const signalledTitle = (signalled: PhaseProgress | null, fromProgress: string | null): string | null =>
|
|
42
|
+
signalled ? `${phaseLabel(signalled.id)} · ${signalled.done}/${signalled.total}` : fromProgress
|
|
@@ -2,7 +2,8 @@ import { readdirSync, statSync } from 'node:fs'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { projectDirectoryOf } from '../context/session-log'
|
|
4
4
|
import { MS_PER_MINUTE } from '../plan-format/clock'
|
|
5
|
-
import {
|
|
5
|
+
import { dropUnused, type StatCache } from '../shared/stat-cache'
|
|
6
|
+
import { readClaudeSignals, type SignalCache } from '../task-signals/claude-read'
|
|
6
7
|
import { recordSignals } from '../task-signals/record'
|
|
7
8
|
import { readTaskRecords } from '../task-signals/record-store'
|
|
8
9
|
import type { TaskSignal } from '../task-signals/types'
|
|
@@ -41,14 +42,21 @@ const recentSessionLogs = (project: string, notOlderThan: number): string[] => {
|
|
|
41
42
|
/**
|
|
42
43
|
* What agents report doing, as `TaskSignal`s: the tasks in progress in the recent Claude Code sessions of the main
|
|
43
44
|
* checkout (the coordinator and its subagents) and of every worktree, and the open `jh task` records. Which lane a
|
|
44
|
-
* signal belongs to is decided by its `cwd`, not by where it was read.
|
|
45
|
+
* signal belongs to is decided by its `cwd`, not by where it was read. A `cache` kept between calls spares parsing a
|
|
46
|
+
* log whose modification time and size did not change; logs this call did not look at are dropped from it.
|
|
45
47
|
*/
|
|
46
|
-
export const readAllSignals = (
|
|
48
|
+
export const readAllSignals = (
|
|
49
|
+
{ projects, root, worktrees }: SignalSource,
|
|
50
|
+
now: number,
|
|
51
|
+
cache: StatCache<TaskSignal[]> = new Map(),
|
|
52
|
+
): TaskSignal[] => {
|
|
47
53
|
const notOlderThan = now - STALE_ACTIVITY_MINUTES * MS_PER_MINUTE
|
|
54
|
+
const round: SignalCache = { entries: cache, used: new Set<string>() }
|
|
48
55
|
const claude = [root, ...subdirectories(worktrees)].flatMap((directory) =>
|
|
49
56
|
recentSessionLogs(join(projects, projectDirectoryOf(directory)), notOlderThan).flatMap((path) =>
|
|
50
|
-
readClaudeSignals(path, { notOlderThan }),
|
|
57
|
+
readClaudeSignals(path, { notOlderThan, cache: round }),
|
|
51
58
|
),
|
|
52
59
|
)
|
|
60
|
+
dropUnused(round.entries, round.used)
|
|
53
61
|
return [...claude, ...recordSignals(readTaskRecords(root))]
|
|
54
62
|
}
|
package/src/plans/snapshot.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
import type { ContextReading } from '../context'
|
|
4
4
|
import { type AgentLogReader, contextOfAgent } from '../context/report'
|
|
@@ -13,12 +13,18 @@ import {
|
|
|
13
13
|
shortTitle,
|
|
14
14
|
sprintTitleOf,
|
|
15
15
|
} from '../plan-format'
|
|
16
|
-
import {
|
|
16
|
+
import type { WaitingItem } from '../plan-format/waiting'
|
|
17
|
+
import { dropUnused } from '../shared/stat-cache'
|
|
17
18
|
import type { TaskSignal } from '../task-signals/types'
|
|
18
19
|
import type { LaneAgent, LaneStatusSource } from '../terminal'
|
|
20
|
+
import { isRestingAgent } from '../terminal/types'
|
|
21
|
+
import { laneReportPath, readLaneReport } from '../watch/lane-report-file'
|
|
22
|
+
import { restNote } from '../watch/lane-rest'
|
|
19
23
|
import type { SourceTicket, WatchBadges, WatchHealth } from '../watch/plan-badges'
|
|
20
24
|
import { readWatchReading } from '../watch/plan-badges-read'
|
|
25
|
+
import type { SourceStatus } from '../watch/source-statuses'
|
|
21
26
|
import { assignSignals, laneOfDirectory, type UnplannedTask } from './active-tasks'
|
|
27
|
+
import { type AppointmentBlock, appointmentBlock } from './appointment-rows'
|
|
22
28
|
import { boardRowsOf } from './board-rows'
|
|
23
29
|
import {
|
|
24
30
|
type ActivityTimes,
|
|
@@ -31,25 +37,41 @@ import {
|
|
|
31
37
|
} from './current-phase'
|
|
32
38
|
import { activityTextOf, taskSignalsOf } from './current-task'
|
|
33
39
|
import { errorText } from './error-text'
|
|
34
|
-
import { type BaseReader, baseReaderOf, gitStatusOf, refExists } from './git'
|
|
40
|
+
import { type BaseReader, baseReaderOf, gitStatusOf, lastChangeReaderOf, refExists } from './git'
|
|
35
41
|
import type { PullRequestState } from './github'
|
|
36
|
-
import {
|
|
42
|
+
import { groomedDaysAgo } from './groomed'
|
|
43
|
+
import {
|
|
44
|
+
type Fallbacks,
|
|
45
|
+
laneWorkOf,
|
|
46
|
+
type PlanSources,
|
|
47
|
+
readAtBase,
|
|
48
|
+
readFallback,
|
|
49
|
+
readPlans,
|
|
50
|
+
textAtBase,
|
|
51
|
+
} from './lane-plans'
|
|
37
52
|
import { laneSummary } from './lane-summary'
|
|
38
53
|
import { boardRowTexts, laneTicketsOf, withoutTicketLinks } from './lane-tickets'
|
|
39
54
|
import type { LaneView } from './layout'
|
|
40
55
|
import { extractLinkChips } from './link-chips'
|
|
56
|
+
import { type NextBlock, nextBlock } from './next-rows'
|
|
41
57
|
import type { Options } from './options'
|
|
42
58
|
import { laneModelsOf, type PhaseModelRecord } from './phase-models'
|
|
43
59
|
import { readPhaseModels } from './phase-models-read'
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
60
|
+
import { laneHeads, planPullRequests, readerOncePerRef } from './plan-heads'
|
|
61
|
+
import { markStale, type PlannedPlan, plannedPlans } from './planned'
|
|
62
|
+
import type { PullRequest } from './pull-requests'
|
|
63
|
+
import type { ReadCache } from './read-cache'
|
|
64
|
+
import { type SentChip, sentChipsByLane } from './sent-files'
|
|
47
65
|
import { baseRefOf, boardRepoPathOf, type PlansSettings, worktreeOf } from './settings'
|
|
66
|
+
import { signalledTitle } from './signalled-phase'
|
|
48
67
|
import { readAllSignals } from './signals-read'
|
|
68
|
+
import { withSourcePrefix } from './source-prefix'
|
|
69
|
+
import type { HerdSource, SourceBlock } from './source-rows'
|
|
49
70
|
import type { SubagentActivity } from './subagents'
|
|
50
|
-
import { readSubagentActivity } from './subagents-read'
|
|
71
|
+
import { readSubagentActivity, subagentSourceOf } from './subagents-read'
|
|
51
72
|
import { boardTexts } from './texts'
|
|
52
73
|
import { type UnlistedLane, unlistedLanes, unlistedSprintLane } from './unlisted-lanes'
|
|
74
|
+
import { readWaitingRecords, waitingItemsOf } from './waiting-records'
|
|
53
75
|
import { withWorkedPhases } from './worked-phases'
|
|
54
76
|
|
|
55
77
|
/** Where the view reads from: the main checkout, the sprint document, the command line and the project settings. */
|
|
@@ -101,6 +123,16 @@ export type Snapshot = {
|
|
|
101
123
|
watchesTickets?: boolean
|
|
102
124
|
/** What waits on the user's answer: the open rows of the board's section for it, from the checkout's board file. */
|
|
103
125
|
waiting?: WaitingItem[]
|
|
126
|
+
/** Whole days since the board's grooming (`groomedDaysAgo`); null when its appointment is missing. */
|
|
127
|
+
groomedDays?: number | null
|
|
128
|
+
/** The board's next section, read from the same board text as `waiting`, for the block under it. */
|
|
129
|
+
next?: NextBlock
|
|
130
|
+
/** The board's open appointments, read from the same board text as `waiting`, for the block under the next one. */
|
|
131
|
+
appointments?: AppointmentBlock
|
|
132
|
+
/** The sources of `watch.sources` not marked `ticket`, from the watcher's state, for the block under the appointments. */
|
|
133
|
+
sourceBlock?: SourceBlock
|
|
134
|
+
/** The tone of each role, from the config, which `renderSnapshot` sets before it draws. */
|
|
135
|
+
tones?: PlansSettings['tones']
|
|
104
136
|
/** What agents report doing that no lane takes: the coordinator's own, or a lane the board does not list. */
|
|
105
137
|
unplaced?: UnplannedTask[]
|
|
106
138
|
}
|
|
@@ -115,13 +147,19 @@ type LaneSources = {
|
|
|
115
147
|
/** The agents' log reader, asking once per agent however often the read is used in one snapshot. */
|
|
116
148
|
readLog: AgentLogReader
|
|
117
149
|
pullRequests: PullRequestState
|
|
150
|
+
/** The open pull requests that change a plan, whose heads raise the ticks the base ref has. */
|
|
151
|
+
planPullRequests: PullRequest[]
|
|
152
|
+
/** The reader of a pull request head, resolved once per read whatever the number of lanes. */
|
|
153
|
+
headReaderOf: (ref: string) => BaseReader
|
|
118
154
|
/** What agents report doing, in every lane and outside them. */
|
|
119
155
|
signals: TaskSignal[]
|
|
120
156
|
/** The directory the lanes' worktrees lie in, which tells a signal's lane. */
|
|
121
157
|
worktrees: string
|
|
158
|
+
parsed?: PlanSources['parsed']
|
|
122
159
|
}
|
|
123
160
|
|
|
124
|
-
|
|
161
|
+
/** `liveMarkdown` is the checkout's board file (the coordinator edits it before any commit), else `markdown`. */
|
|
162
|
+
type BoardLanes = { markdown: string; liveMarkdown: string; lanes: SprintLane[] }
|
|
125
163
|
|
|
126
164
|
const SHOWN_NAMES = 2
|
|
127
165
|
|
|
@@ -136,6 +174,7 @@ type Decorations = {
|
|
|
136
174
|
phaseModels: readonly PhaseModelRecord[]
|
|
137
175
|
sprintDirectory: string
|
|
138
176
|
sentChips: ReadonlyMap<string | null, SentChip[]>
|
|
177
|
+
restNoteOf: (lane: string) => string | null
|
|
139
178
|
}
|
|
140
179
|
|
|
141
180
|
/** How full the agent's context is, null when the terminal gave no session or the session has no log. */
|
|
@@ -219,12 +258,20 @@ const laneView = async (
|
|
|
219
258
|
modified,
|
|
220
259
|
} = await readPlans(
|
|
221
260
|
lane,
|
|
222
|
-
{
|
|
261
|
+
{
|
|
262
|
+
root,
|
|
263
|
+
settings,
|
|
264
|
+
base: sources.base,
|
|
265
|
+
sprintDirectory: sources.sprintDirectory,
|
|
266
|
+
heads: laneHeads(sources.planPullRequests, gitStatus.branch, sources.headReaderOf),
|
|
267
|
+
parsed: sources.parsed,
|
|
268
|
+
},
|
|
223
269
|
laneWorkOf(worktree, gitStatus),
|
|
224
270
|
fallbacks,
|
|
225
271
|
)
|
|
226
272
|
const shown = withWorkedPhases(lane, phasesByHref)
|
|
227
273
|
const { progress, activity } = progressWithMemory(shown, phasesByHref, memories, { now, modified })
|
|
274
|
+
const laneSubagents = sources.subagents.get(lane.name)
|
|
228
275
|
const view: LaneView = {
|
|
229
276
|
progress,
|
|
230
277
|
...running,
|
|
@@ -238,6 +285,11 @@ const laneView = async (
|
|
|
238
285
|
activity,
|
|
239
286
|
guesses: taskSignalsOf(Object.values(phasesByHref).flat(), workText(sources, lane.name, agent), now),
|
|
240
287
|
reported: sources.signals.filter(({ cwd }) => laneOfDirectory(cwd, sources.worktrees) === lane.name),
|
|
288
|
+
subagents: laneSubagents?.phaseRuns ?? [],
|
|
289
|
+
subagentTasks: laneSubagents?.taskDescriptions ?? [],
|
|
290
|
+
phaselessReviews: laneSubagents?.phaselessReviews ?? [],
|
|
291
|
+
describedSubagents: laneSubagents?.describedSubagents ?? [],
|
|
292
|
+
branch: gitStatus.branch,
|
|
241
293
|
},
|
|
242
294
|
now,
|
|
243
295
|
),
|
|
@@ -289,12 +341,17 @@ const withTickets = (result: LaneResult, tickets: readonly SourceTicket[]): Lane
|
|
|
289
341
|
}
|
|
290
342
|
|
|
291
343
|
/** What agents report doing; nothing when it cannot be read. */
|
|
292
|
-
const readSignals = (
|
|
344
|
+
const readSignals = (
|
|
345
|
+
{ root, readSignals: read }: Source,
|
|
346
|
+
worktrees: string,
|
|
347
|
+
now: number,
|
|
348
|
+
cache?: ReadCache,
|
|
349
|
+
): TaskSignal[] => {
|
|
293
350
|
try {
|
|
294
351
|
return (
|
|
295
352
|
read ??
|
|
296
353
|
((main, directory, at) =>
|
|
297
|
-
readAllSignals({ projects: defaultProjectsDirectory(), root: main, worktrees: directory }, at))
|
|
354
|
+
readAllSignals({ projects: defaultProjectsDirectory(), root: main, worktrees: directory }, at, cache?.signals))
|
|
298
355
|
)(root, worktrees, now)
|
|
299
356
|
} catch {
|
|
300
357
|
return []
|
|
@@ -322,52 +379,31 @@ const readAgents = (terminal: LaneStatusSource): { agents: LaneAgent[]; errors:
|
|
|
322
379
|
}
|
|
323
380
|
|
|
324
381
|
/** What subagents of the coordinator do in the worktrees; nothing when their logs cannot be read. */
|
|
325
|
-
const readSubagents = (
|
|
382
|
+
const readSubagents = (
|
|
383
|
+
{ root, settings, readSubagents: read }: Source,
|
|
384
|
+
worktrees: string,
|
|
385
|
+
): Map<string, SubagentActivity> => {
|
|
326
386
|
try {
|
|
327
387
|
return (
|
|
328
|
-
read ??
|
|
329
|
-
((main, directory) =>
|
|
330
|
-
readSubagentActivity({
|
|
331
|
-
projects: defaultProjectsDirectory(),
|
|
332
|
-
root: main,
|
|
333
|
-
approvalsRoot: main,
|
|
334
|
-
worktreeDirectory: directory,
|
|
335
|
-
}))
|
|
388
|
+
read ?? ((main, directory) => readSubagentActivity(subagentSourceOf(main, directory, settings.skillPrefix)))
|
|
336
389
|
)(root, worktrees)
|
|
337
390
|
} catch {
|
|
338
391
|
return new Map()
|
|
339
392
|
}
|
|
340
393
|
}
|
|
341
394
|
|
|
342
|
-
/**
|
|
343
|
-
* The open items that wait on the user, from the checkout's board file (the coordinator edits it before any commit),
|
|
344
|
-
* else from the text read at the base ref.
|
|
345
|
-
*/
|
|
346
|
-
const waitingOf = (
|
|
347
|
-
{ sprintFile, settings }: Source,
|
|
348
|
-
markdown: string,
|
|
349
|
-
sends: readonly SentFileRecord[],
|
|
350
|
-
now: number,
|
|
351
|
-
): WaitingItem[] => {
|
|
352
|
-
const board = (() => {
|
|
353
|
-
try {
|
|
354
|
-
return openWaitingItems(
|
|
355
|
-
existsSync(sprintFile) ? readFileSync(sprintFile, 'utf8') : markdown,
|
|
356
|
-
settings.planLanguage,
|
|
357
|
-
)
|
|
358
|
-
} catch {
|
|
359
|
-
return openWaitingItems(markdown, settings.planLanguage)
|
|
360
|
-
}
|
|
361
|
-
})()
|
|
362
|
-
return [...board, ...sentWaitingItems(sends, now)]
|
|
363
|
-
}
|
|
364
|
-
|
|
365
395
|
/** The view with the files its session sent to the user, only when there are any, so it otherwise stays as it was. */
|
|
366
396
|
const withSentFiles = (view: LaneView, chips: ReadonlyMap<string | null, SentChip[]>): LaneView => {
|
|
367
397
|
const found = chips.get(view.progress.lane.name)
|
|
368
398
|
return found ? { ...view, sentFiles: found } : view
|
|
369
399
|
}
|
|
370
400
|
|
|
401
|
+
/** The view with what its resting lane waits on, read from the lane's report as the watcher reads it; as it was while the agent works. */
|
|
402
|
+
const withRestNote = (view: LaneView, restNoteOf: Decorations['restNoteOf']): LaneView => {
|
|
403
|
+
const note = isRestingAgent(view.status.agent) ? restNoteOf(view.progress.lane.name) : null
|
|
404
|
+
return note === null ? view : { ...view, restNote: note }
|
|
405
|
+
}
|
|
406
|
+
|
|
371
407
|
/** Up to two names, then how many more: `a, b and 3 more`. */
|
|
372
408
|
const namedWithMore = (names: readonly string[]): string => {
|
|
373
409
|
const more = names.length - SHOWN_NAMES
|
|
@@ -395,8 +431,8 @@ const readBoardLanes = async (
|
|
|
395
431
|
const { sprintFile, settings } = source
|
|
396
432
|
const atBase = textAtBase(base, boardPath)
|
|
397
433
|
const markdown = atBase ?? (await readFallback(boardPath, sprintFile, fallbacks)) ?? ''
|
|
398
|
-
const
|
|
399
|
-
return { markdown, lanes: boardRowsOf(markdown,
|
|
434
|
+
const liveMarkdown = atBase !== null && existsSync(sprintFile) ? await Bun.file(sprintFile).text() : markdown
|
|
435
|
+
return { markdown, liveMarkdown, lanes: boardRowsOf(markdown, liveMarkdown, settings.planLanguage).lanes }
|
|
400
436
|
}
|
|
401
437
|
|
|
402
438
|
/** The plans the sprint's plans tables name, read at the base ref, by link; lanes read theirs themselves. */
|
|
@@ -429,11 +465,17 @@ const forgetLanes = (memories: Map<string, LaneMemory> | undefined, lanes: reado
|
|
|
429
465
|
|
|
430
466
|
const laneSetOf = (
|
|
431
467
|
source: Source,
|
|
432
|
-
context: {
|
|
468
|
+
context: {
|
|
469
|
+
base: BaseReader
|
|
470
|
+
board: BoardLanes
|
|
471
|
+
sprintDirectory: string
|
|
472
|
+
worktrees: string
|
|
473
|
+
cache?: ReadCache
|
|
474
|
+
},
|
|
433
475
|
pullRequests: PullRequestState,
|
|
434
476
|
now: number,
|
|
435
477
|
): LaneSet => {
|
|
436
|
-
const { base, board, sprintDirectory, worktrees } = context
|
|
478
|
+
const { base, board, sprintDirectory, worktrees, cache } = context
|
|
437
479
|
const { agents, errors } = readAgents(source.terminal)
|
|
438
480
|
const isWanted = ({ name }: { name: string }) => !source.options.lane || name === source.options.lane
|
|
439
481
|
const subagents = readSubagents(source, worktrees)
|
|
@@ -449,8 +491,11 @@ const laneSetOf = (
|
|
|
449
491
|
subagents,
|
|
450
492
|
readLog: onceReader(source.readAgentLog ?? agentLog),
|
|
451
493
|
pullRequests,
|
|
452
|
-
|
|
494
|
+
planPullRequests: planPullRequests([...pullRequests.byBranch.values()].flat(), source.settings.planDirectory),
|
|
495
|
+
headReaderOf: readerOncePerRef((ref) => baseReaderOf(source.root, ref)),
|
|
496
|
+
signals: readSignals(source, worktrees, now, cache),
|
|
453
497
|
worktrees,
|
|
498
|
+
parsed: cache && { entries: cache.plans, used: new Set<string>() },
|
|
454
499
|
},
|
|
455
500
|
errors,
|
|
456
501
|
}
|
|
@@ -474,53 +519,56 @@ const laneResultsOf = async (
|
|
|
474
519
|
/** Each lane's view with its tickets, the watcher's badges, its phases' models and the files sent from it. */
|
|
475
520
|
const decoratedViews = (
|
|
476
521
|
results: readonly LaneResult[],
|
|
477
|
-
{ reading, phaseModels, sprintDirectory, sentChips }: Decorations,
|
|
522
|
+
{ reading, phaseModels, sprintDirectory, sentChips, restNoteOf }: Decorations,
|
|
478
523
|
): LaneView[] =>
|
|
479
524
|
results.map((result) =>
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
525
|
+
withRestNote(
|
|
526
|
+
withSentFiles(
|
|
527
|
+
withModels(
|
|
528
|
+
withBadges(withTickets(result, reading?.tickets ?? []), reading?.badges ?? {}),
|
|
529
|
+
phaseModels,
|
|
530
|
+
sprintDirectory,
|
|
531
|
+
),
|
|
532
|
+
sentChips,
|
|
485
533
|
),
|
|
486
|
-
|
|
534
|
+
restNoteOf,
|
|
487
535
|
),
|
|
488
536
|
)
|
|
489
537
|
|
|
490
|
-
/** The files sent to the user, read once: the records, and their badges by lane. */
|
|
491
|
-
const sendsOf = (
|
|
492
|
-
root: string,
|
|
493
|
-
worktrees: string,
|
|
494
|
-
now: number,
|
|
495
|
-
): { records: SentFileRecord[]; chips: Map<string | null, SentChip[]> } => {
|
|
496
|
-
const records = readSentRecords(root)
|
|
497
|
-
return { records, chips: sentChipsByLane(records, worktrees, now) }
|
|
498
|
-
}
|
|
499
|
-
|
|
500
538
|
/** The open phases no lane with an agent leads; none while the view shows one lane. */
|
|
539
|
+
const withSourceTones = (statuses: readonly SourceStatus[], configured: PlansSettings['herdSources']): HerdSource[] =>
|
|
540
|
+
statuses.map((status) => ({ ...status, tone: configured.find(({ name }) => name === status.name)?.tone ?? null }))
|
|
541
|
+
|
|
501
542
|
const plannedOf = (
|
|
502
543
|
source: Source,
|
|
503
544
|
plans: Record<string, string>,
|
|
504
545
|
results: readonly LaneResult[],
|
|
505
546
|
sprintDirectory: string,
|
|
547
|
+
now: number,
|
|
506
548
|
): PlannedPlan[] =>
|
|
507
549
|
source.options.lane
|
|
508
550
|
? []
|
|
509
|
-
:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
551
|
+
: markStale(
|
|
552
|
+
plannedPlans({
|
|
553
|
+
plans,
|
|
554
|
+
lanes: results.map(({ view }) => ({ lane: view.progress.lane, agent: view.status.agent })),
|
|
555
|
+
sprintDirectory,
|
|
556
|
+
language: source.settings.planLanguage,
|
|
557
|
+
}),
|
|
558
|
+
lastChangeReaderOf(source.root, baseRefOf(source.settings)),
|
|
559
|
+
now,
|
|
560
|
+
)
|
|
515
561
|
|
|
516
562
|
/**
|
|
517
563
|
* Reads the board, its plans and the lanes' state. `memories` holds, per lane, what the herd saw at the last read; it
|
|
518
564
|
* is how the current phase becomes the one whose boxes moved since, and it is cleaned of lanes the board dropped.
|
|
565
|
+
* `cache`, kept by the caller between reads, spares parsing the session logs and plans that did not change.
|
|
519
566
|
*/
|
|
520
567
|
export const loadSnapshot = async (
|
|
521
568
|
source: Source,
|
|
522
569
|
pullRequests: PullRequestState,
|
|
523
570
|
memories?: Map<string, LaneMemory>,
|
|
571
|
+
cache?: ReadCache,
|
|
524
572
|
): Promise<Snapshot> => {
|
|
525
573
|
const { root, sprintFile, options, settings } = source
|
|
526
574
|
const fallbacks: Fallbacks = []
|
|
@@ -532,14 +580,32 @@ export const loadSnapshot = async (
|
|
|
532
580
|
const sprintDirectory = dirname(boardPath)
|
|
533
581
|
const worktrees = join(root, settings.worktreeDirectory)
|
|
534
582
|
const now = Date.now()
|
|
535
|
-
const
|
|
583
|
+
const liveBoard = board.liveMarkdown
|
|
584
|
+
const laneSet = laneSetOf(source, { base, board, sprintDirectory, worktrees, cache }, pullRequests, now)
|
|
536
585
|
const results = await laneResultsOf(laneSet, fallbacks, now, memories)
|
|
586
|
+
const { parsed } = laneSet.sources
|
|
587
|
+
if (parsed) dropUnused(parsed.entries, parsed.used)
|
|
537
588
|
const listedPlans = await readListedPlans(source, base, sprintDirectory, markdown, fallbacks)
|
|
538
589
|
const documentsPlans = Object.assign({}, listedPlans, ...results.map((result) => result.texts))
|
|
539
|
-
const { ticketSources } = settings
|
|
540
|
-
const watched =
|
|
541
|
-
|
|
542
|
-
const
|
|
590
|
+
const { ticketSources, herdSources } = settings
|
|
591
|
+
const watched =
|
|
592
|
+
ticketSources.length > 0 || herdSources.length > 0 || results.some(({ view }) => view.pullRequests.length > 0)
|
|
593
|
+
const reading = watched
|
|
594
|
+
? readWatchReading(
|
|
595
|
+
root,
|
|
596
|
+
now,
|
|
597
|
+
ticketSources,
|
|
598
|
+
undefined,
|
|
599
|
+
undefined,
|
|
600
|
+
herdSources.map(({ name }) => name),
|
|
601
|
+
)
|
|
602
|
+
: null
|
|
603
|
+
const waitingRecords = readWaitingRecords(root)
|
|
604
|
+
const waiting = withSourcePrefix(
|
|
605
|
+
waitingItemsOf(liveBoard, settings.planLanguage, waitingRecords, now),
|
|
606
|
+
settings.sourceLabels,
|
|
607
|
+
)
|
|
608
|
+
const sentChips = sentChipsByLane(waitingRecords.sent, worktrees, now)
|
|
543
609
|
const notice = fallbacks.length > 0 ? fallbackNotice(fallbacks, baseRef, refExists(root, baseRef)) : null
|
|
544
610
|
const unknownLaneTitle =
|
|
545
611
|
options.lane && results.length === 0 ? boardTexts(settings.board.term).laneNotListed(options.lane) : null
|
|
@@ -549,7 +615,9 @@ export const loadSnapshot = async (
|
|
|
549
615
|
reading,
|
|
550
616
|
phaseModels: readPhaseModels(root),
|
|
551
617
|
sprintDirectory,
|
|
552
|
-
sentChips
|
|
618
|
+
sentChips,
|
|
619
|
+
restNoteOf: (lane) =>
|
|
620
|
+
restNote(readLaneReport(laneReportPath(worktreeOf(root, settings, lane), lane))?.text ?? null),
|
|
553
621
|
}),
|
|
554
622
|
documents: {
|
|
555
623
|
sprint: markdown,
|
|
@@ -557,12 +625,25 @@ export const loadSnapshot = async (
|
|
|
557
625
|
laneOrder: [...laneSet.lanes, ...laneSet.unlisted].map((lane) => lane.name),
|
|
558
626
|
lanes: board.lanes,
|
|
559
627
|
},
|
|
560
|
-
planned: plannedOf(source, documentsPlans, results, sprintDirectory),
|
|
628
|
+
planned: plannedOf(source, documentsPlans, results, sprintDirectory, now),
|
|
561
629
|
files: [sprintFile, ...results.flatMap((result) => result.files)],
|
|
562
|
-
titles: new Map(
|
|
630
|
+
titles: new Map(
|
|
631
|
+
results.flatMap(({ view, pane }) =>
|
|
632
|
+
pane ? [[pane, signalledTitle(view.summary?.signalled ?? null, shortTitle(view.progress))]] : [],
|
|
633
|
+
),
|
|
634
|
+
),
|
|
563
635
|
errors: [...laneSet.errors, ...pullRequests.errors, ...(notice ? [notice] : [])],
|
|
564
636
|
watch: reading?.health,
|
|
565
|
-
waiting
|
|
637
|
+
waiting,
|
|
638
|
+
groomedDays: groomedDaysAgo(liveBoard, settings.planLanguage, new Date(now)),
|
|
639
|
+
next: nextBlock(
|
|
640
|
+
liveBoard,
|
|
641
|
+
settings.planLanguage,
|
|
642
|
+
waiting.map(({ item }) => item),
|
|
643
|
+
),
|
|
644
|
+
appointments: appointmentBlock(liveBoard, settings.planLanguage, new Date(now)),
|
|
645
|
+
...(reading?.sources ? { sourceBlock: { sources: withSourceTones(reading.sources, herdSources), now } } : {}),
|
|
646
|
+
tones: settings.tones,
|
|
566
647
|
unplaced: unplacedOf(laneSet.sources, laneSet.lanes, Boolean(options.lane)),
|
|
567
648
|
...(ticketSources.length > 0 ? { watchesTickets: true } : {}),
|
|
568
649
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WaitingItem } from '../plan-format/waiting'
|
|
2
|
+
|
|
3
|
+
/** Whether the source `source` has an entry titled `title` that an open row of "Waiting for you" asks about. */
|
|
4
|
+
export type AskedCheck = (source: string, title: string) => boolean
|
|
5
|
+
|
|
6
|
+
const same = (left: string, right: string): boolean => left.trim().toLowerCase() === right.trim().toLowerCase()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The check against the open rows: a row asks about an entry when its origin cell names the entry's source and its text
|
|
10
|
+
* contains the entry's title, in any case. The rows come from the board, whose text names the entry in words; no id links them.
|
|
11
|
+
*/
|
|
12
|
+
export const askedCheckOf =
|
|
13
|
+
(waiting: readonly WaitingItem[]): AskedCheck =>
|
|
14
|
+
(source, title) =>
|
|
15
|
+
title.trim() !== '' &&
|
|
16
|
+
waiting.some(
|
|
17
|
+
({ origin, item }) =>
|
|
18
|
+
origin !== undefined && same(origin, source) && item.toLowerCase().includes(title.trim().toLowerCase()),
|
|
19
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WaitingItem } from '../plan-format/waiting'
|
|
2
|
+
|
|
3
|
+
/** By source name in lower case: what the herd puts before a row that came from the source, its `label` or else its name. */
|
|
4
|
+
export type SourceLabels = Readonly<Record<string, string>>
|
|
5
|
+
|
|
6
|
+
const lookup = (labels: SourceLabels, origin: string): string | undefined =>
|
|
7
|
+
Object.entries(labels).find(([name]) => name.toLowerCase() === origin.trim().toLowerCase())?.[1]
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The rows with `prefix` set on those whose origin cell names a source: the board row of a source's report then
|
|
11
|
+
* reads `Error log · Checkout: Resource Not Found` without a look at the plan. Any other row stays as it is.
|
|
12
|
+
*/
|
|
13
|
+
export const withSourcePrefix = (items: readonly WaitingItem[], labels: SourceLabels): WaitingItem[] =>
|
|
14
|
+
items.map((item) => {
|
|
15
|
+
const prefix = item.origin === undefined ? undefined : lookup(labels, item.origin)
|
|
16
|
+
return prefix === undefined ? item : { ...item, prefix }
|
|
17
|
+
})
|