jackherd 1.8.1 → 1.10.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 +60 -0
- package/INSTALL.md +3 -1
- package/README.md +75 -64
- package/dist/{chunk-OU2KALXP.js → chunk-5MQBCUQ5.js} +24 -4
- package/dist/config.d.ts +45 -4
- package/dist/config.js +84 -27
- package/dist/plan-format.d.ts +63 -7
- package/dist/plan-format.js +66 -12
- 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/approval-gate/index.ts +22 -4
- package/kit/hooks/hooks/approval-gate/run.ts +29 -6
- package/kit/hooks/hooks/approval-gate/subagent.ts +18 -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 -140
- 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 +52 -0
- package/kit/hooks/hooks/open-question/store.ts +39 -0
- package/kit/hooks/hooks/sent-file/index.ts +19 -0
- package/kit/hooks/hooks/sent-file/record.ts +128 -0
- package/kit/hooks/hooks/sent-file/run.ts +48 -0
- package/kit/hooks/hooks/sent-file/store.ts +9 -0
- package/kit/hooks/hooks/shared/board-lanes.ts +15 -2
- 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 +21 -4
- package/kit/hooks/statusline.sh +1 -1
- package/kit/review/agents/skill-review.md +34 -0
- package/kit/review/skills/pre-commit/SKILL.md +1 -1
- package/kit/workflow/skills/follow/SKILL.md +1 -8
- package/kit/workflow/skills/implement/SKILL.md +10 -6
- package/kit/workflow/skills/orchestrate/SKILL.md +213 -412
- 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 +4 -1
- package/recipes/README.md +2 -1
- package/recipes/sources.md +1 -1
- package/schemas/jackherd.schema.json +85 -7
- 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 +4 -0
- package/src/config/auto-window.ts +8 -0
- package/src/config/link-kinds.ts +7 -1
- package/src/config/schema.ts +80 -26
- package/src/context/fill.ts +12 -0
- package/src/context/session-log.ts +43 -1
- 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/build.ts +74 -0
- package/src/lane-prompt/command.ts +87 -0
- package/src/lane-prompt/input.ts +116 -0
- package/src/lane-prompt/options.ts +68 -0
- package/src/lane-prompt/texts.ts +101 -0
- package/src/plan-format/clock.ts +12 -0
- package/src/plan-format/dependencies.ts +9 -9
- package/src/plan-format/index.ts +1 -0
- package/src/plan-format/lanes.ts +30 -5
- package/src/plan-format/short-names.ts +71 -0
- package/src/plan-format/terminal-text.ts +4 -0
- package/src/plan-format/waiting.ts +53 -0
- package/src/plans/active-tasks.ts +60 -0
- package/src/plans/appointment-rows.ts +59 -0
- package/src/plans/ask-coordinator.ts +77 -0
- package/src/plans/ask-target.ts +25 -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/current-phase.ts +17 -7
- package/src/plans/current-task.ts +63 -5
- package/src/plans/fetch.ts +26 -3
- package/src/plans/git.ts +19 -5
- 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-standing.ts +9 -4
- package/src/plans/lane-summary.ts +367 -92
- package/src/plans/lane-tickets.ts +15 -0
- package/src/plans/layout-phase.ts +80 -19
- package/src/plans/layout-summary.ts +163 -74
- package/src/plans/layout.ts +472 -114
- package/src/plans/link-chips.ts +1 -0
- package/src/plans/live.ts +119 -10
- package/src/plans/phase-ids.ts +8 -0
- package/src/plans/phase-models.ts +1 -1
- package/src/plans/phase-ranges.ts +45 -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/pull-requests.ts +23 -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/selection.ts +117 -34
- package/src/plans/sent-files-read.ts +13 -0
- package/src/plans/sent-files.ts +114 -0
- package/src/plans/settings.ts +2 -1
- package/src/plans/signalled-phase.ts +42 -0
- package/src/plans/signals-read.ts +62 -0
- package/src/plans/snapshot.ts +279 -60
- 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 +131 -0
- package/src/plans/subagents.ts +190 -0
- 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 +36 -14
- package/src/plans/unlisted-lanes.ts +10 -5
- package/src/plans/view.ts +20 -12
- package/src/plans/waiting-click.ts +231 -0
- package/src/plans/waiting-records.ts +39 -0
- package/src/plans/waiting-window.ts +8 -0
- package/src/preview/command.ts +105 -0
- package/src/preview/embed.ts +209 -0
- package/src/preview/images.ts +89 -0
- package/src/preview/open.ts +12 -0
- package/src/preview/options.ts +32 -0
- package/src/preview/page.ts +89 -0
- package/src/preview/render.ts +41 -0
- package/src/settled/board-row.ts +1 -2
- package/src/shared/is-record.ts +2 -0
- package/src/shared/private-location.ts +89 -0
- package/src/shared/private-records.ts +48 -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/agent.ts +15 -0
- package/src/task-signals/claude-read.ts +89 -0
- package/src/task-signals/claude-signals.ts +112 -0
- package/src/task-signals/command.ts +71 -0
- package/src/task-signals/fixtures/session.jsonl +7 -0
- package/src/task-signals/matcher.ts +96 -0
- package/src/task-signals/record-store.ts +40 -0
- package/src/task-signals/record.ts +68 -0
- package/src/task-signals/text.ts +13 -0
- package/src/task-signals/types.ts +14 -0
- package/src/terminal/open.ts +13 -6
- package/src/terminal/text.ts +16 -4
- package/src/terminal/width.ts +2 -1
- package/src/version/skill-diff.ts +46 -0
- package/src/vocabulary/index.ts +44 -6
- package/src/waiting/command.ts +50 -0
- package/src/watch/appointments.ts +2 -4
- package/src/watch/approvals.ts +2 -2
- package/src/watch/asking-lanes.ts +40 -0
- package/src/watch/base-tip.ts +11 -0
- package/src/watch/command.ts +401 -90
- package/src/watch/failed-tests.ts +56 -10
- package/src/watch/failure-read.ts +18 -6
- package/src/watch/failures.ts +29 -5
- package/src/watch/feedback.ts +1 -1
- package/src/watch/gh-process.ts +9 -3
- package/src/watch/heartbeat-progress.ts +6 -18
- package/src/watch/json-guards.ts +0 -3
- package/src/watch/kit-update.ts +28 -9
- package/src/watch/lane-fill.ts +1 -1
- package/src/watch/lane-model.ts +1 -1
- package/src/watch/lane-name.ts +6 -0
- package/src/watch/lane-parallel.ts +1 -1
- package/src/watch/lane-progress.ts +58 -14
- package/src/watch/lane-report-file.ts +20 -5
- package/src/watch/lane-report.ts +37 -5
- package/src/watch/lane-rest.ts +28 -0
- package/src/watch/lane-sources.ts +31 -3
- package/src/watch/main-checkout.ts +34 -0
- package/src/watch/plan-badges-read.ts +13 -7
- package/src/watch/plan-badges.ts +9 -2
- package/src/watch/previews.ts +1 -1
- package/src/watch/pull-requests.ts +8 -3
- package/src/watch/reference-paths.ts +87 -0
- 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-path.ts +1 -1
- package/src/watch/sources.ts +7 -4
- package/src/watch/stream-output.ts +14 -8
- 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 +108 -0
- package/src/watch/task-parallel-read.ts +151 -0
- package/src/watch/task-parallel-touched.ts +39 -0
- package/src/watch/task-parallel.ts +235 -0
- 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/waiting-line.ts +19 -0
- package/src/watch/watch-events.ts +36 -1
- package/src/watch/watcher-lock.ts +25 -29
- package/src/watch/workflow-runs.ts +2 -1
- 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
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { ChecklistItem, DeferredItem, LaneProgress, PhaseProgress } from '../plan-format'
|
|
2
2
|
import { onlyDocumentationOpen, phaseLabel } from '../plan-format'
|
|
3
|
-
import {
|
|
3
|
+
import { terminalSafe } from '../plan-format/terminal-text'
|
|
4
|
+
import { fit, padVisible, paint, spread, wrap } from '../terminal/text'
|
|
4
5
|
import { textWidth } from '../terminal/width'
|
|
5
6
|
import type { WatchBadges } from '../watch/plan-badges'
|
|
7
|
+
import type { ActiveTask, UnplannedTask } from './active-tasks'
|
|
6
8
|
import { type Frame, rowFrame, textFrame } from './frame'
|
|
7
9
|
import type { LaneTickets } from './lane-tickets'
|
|
8
10
|
import type { LinkChip } from './link-chips'
|
|
9
11
|
import type { PhaseModel } from './phase-models'
|
|
10
12
|
import { itemKey, type Selection, tickedKey } from './selection'
|
|
11
13
|
import { chevronOf, MODEL_MARK, PHASE_COUNT, PHASE_STATE, type PhaseState, phaseStateOf } from './symbols'
|
|
14
|
+
import { taskBodyOf, taskNameOf } from './task-name'
|
|
12
15
|
import { type BadgeTexts, TEXTS } from './texts'
|
|
13
16
|
|
|
14
17
|
/** What every block of one lane is drawn with. */
|
|
@@ -26,13 +29,14 @@ export type LaneContext = {
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export const ACTIVE_MARK = '▶'
|
|
32
|
+
export const GUESSED_MARK = '▷'
|
|
29
33
|
export const DEFERRED_MARK = '→'
|
|
30
34
|
export const COLUMN_SEPARATOR = ' '
|
|
31
35
|
export const MIN_TITLE_WIDTH = 8
|
|
32
36
|
|
|
33
37
|
/** Places on each side of the slash of a count: ` 9/9 `, ` 15/15 `, `123/456`. */
|
|
34
38
|
const COUNT_PLACES = 3
|
|
35
|
-
/** The cells before a count:
|
|
39
|
+
/** The cells before a count: `✓`, `?`, `–` or nothing, padded so every slash on screen is in one column. */
|
|
36
40
|
const COUNT_MARK_WIDTH = 2
|
|
37
41
|
const BLANK_COUNT = ' '.repeat(2 * COUNT_PLACES + 1)
|
|
38
42
|
const COUNT_CELL_WIDTH = COUNT_MARK_WIDTH + 1 + BLANK_COUNT.length
|
|
@@ -54,6 +58,9 @@ const PHASE_MARK: Record<PhaseState, string> = {
|
|
|
54
58
|
|
|
55
59
|
export const plain = (text: string): string => text.replace(MARKDOWN_MARKS, '')
|
|
56
60
|
|
|
61
|
+
/** What a task reads as on one row: its short name where it carries one (`taskNameOf`), else its whole text. */
|
|
62
|
+
export const taskTitle = (text: string): string => plain(taskNameOf(text) ?? text)
|
|
63
|
+
|
|
57
64
|
export const countText = (done: number, total: number): string =>
|
|
58
65
|
`${String(done).padStart(COUNT_PLACES)}/${String(total).padEnd(COUNT_PLACES)}`
|
|
59
66
|
|
|
@@ -87,25 +94,79 @@ export const modelMark = (model: PhaseModel | undefined): string =>
|
|
|
87
94
|
: ''
|
|
88
95
|
|
|
89
96
|
/** `text` wrapped beside a one-cell `mark`, each line in `style`; follow-up lines indent under the text. */
|
|
90
|
-
const markedLines = (text: string,
|
|
91
|
-
wrap(plain(text), Math.max(width - 2, 1))
|
|
97
|
+
const markedLines = (text: string, look: TaskLook, width: number): string[] => {
|
|
98
|
+
const lines = wrap(plain(text), Math.max(width - 2, 1))
|
|
99
|
+
return lines.map(
|
|
100
|
+
(line, index) =>
|
|
101
|
+
`${index > 0 ? ' ' : look.mark} ${look.style(line)}${index === lines.length - 1 ? look.suffix : ''}`,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The arrow of a phase or task being worked on: a teal `▶` where an agent reported it, a dim teal `▷` where the herd
|
|
107
|
+
* only guesses it from words. Yellow is the herd's colour for what waits on the user.
|
|
108
|
+
*/
|
|
109
|
+
export const activeArrow = (guessed: boolean): string =>
|
|
110
|
+
guessed ? paint.dim(paint.teal(GUESSED_MARK)) : paint.teal(ACTIVE_MARK)
|
|
111
|
+
|
|
112
|
+
/** The title of a phase or task being worked on: bold where reported, plain where guessed. */
|
|
113
|
+
export const activeTitle = (text: string, guessed: boolean): string => (guessed ? text : paint.bold(text))
|
|
114
|
+
|
|
115
|
+
/** The agent's name after a title, dim; nothing while only one agent is active. */
|
|
116
|
+
export const agentSuffix = (agent: string | null): string => (agent ? ` ${paint.dim(terminalSafe(agent))}` : '')
|
|
117
|
+
|
|
118
|
+
/** The agent's name for the right edge of a row, dim; empty while only one agent is active. */
|
|
119
|
+
export const agentCell = (agent: string | null): string => (agent ? paint.dim(terminalSafe(agent)) : '')
|
|
120
|
+
|
|
121
|
+
/** Two cells: a task row hangs under its phase's label, which an arrow would otherwise push right. */
|
|
122
|
+
export const TASK_INDENT = ' '
|
|
92
123
|
|
|
93
|
-
/**
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
124
|
+
/** `▶ <label> <text>`, the text bold, the agent dim at the right edge. */
|
|
125
|
+
const labelledLine = (label: string, { text, agent }: UnplannedTask, width: number): string =>
|
|
126
|
+
spread(`${activeArrow(false)} ${label} ${paint.bold(plain(terminalSafe(text)))}`, agentCell(agent), width)
|
|
127
|
+
|
|
128
|
+
/** `▶ now: <text>`: what an agent reports that no task of the plan names; the agent dim at the right edge. */
|
|
129
|
+
export const nowLine = (task: UnplannedTask, width: number): string => labelledLine(TEXTS.now, task, width)
|
|
130
|
+
|
|
131
|
+
/** `▶ Subagent: <description>`: a running subagent that names no phase and sits on none. */
|
|
132
|
+
export const subagentLine = (description: string, width: number): string =>
|
|
133
|
+
labelledLine(TEXTS.subagent, { text: description, agent: null }, width)
|
|
134
|
+
|
|
135
|
+
type TaskLook = { mark: string; style: (line: string) => string; suffix: string }
|
|
136
|
+
|
|
137
|
+
/** The mark and the text style of a task: a ticked one green and dim, the one being worked on with its arrow. */
|
|
138
|
+
const taskLook = ({ done }: ChecklistItem, active: ActiveTask | undefined): TaskLook => {
|
|
139
|
+
if (active) {
|
|
140
|
+
return {
|
|
141
|
+
mark: activeArrow(active.guessed),
|
|
142
|
+
style: (line) => activeTitle(line, active.guessed),
|
|
143
|
+
suffix: agentSuffix(active.agent),
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return done
|
|
147
|
+
? { mark: paint.green('☑'), style: paint.dim, suffix: '' }
|
|
148
|
+
: { mark: '☐', style: (line) => line, suffix: '' }
|
|
97
149
|
}
|
|
98
150
|
|
|
99
|
-
/** A closed task as one row, cut at the pane's edge with `…`. */
|
|
100
|
-
const closedTaskRow = (item: ChecklistItem, active:
|
|
151
|
+
/** A closed task as one row, its short name or its text cut at the pane's edge with `…`. */
|
|
152
|
+
const closedTaskRow = (item: ChecklistItem, active: ActiveTask | undefined, width: number): string[] => {
|
|
101
153
|
const { mark, style } = taskLook(item, active)
|
|
102
|
-
return [
|
|
154
|
+
return [spread(`${mark} ${style(taskTitle(item.text))}`, agentCell(active?.agent ?? null), width)]
|
|
103
155
|
}
|
|
104
156
|
|
|
105
|
-
/**
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
157
|
+
/** A named task opened: its name bold beside the mark, below it the line after the name (`taskBodyOf`) wrapped. */
|
|
158
|
+
const namedTaskRows = (name: string, item: ChecklistItem, look: TaskLook, width: number): string[] => {
|
|
159
|
+
const body = wrap(plain(taskBodyOf(item.text)), Math.max(width - 2, 1)).map(
|
|
160
|
+
(line) => ` ${item.done ? paint.dim(line) : line}`,
|
|
161
|
+
)
|
|
162
|
+
return [fit(`${look.mark} ${paint.bold(plain(name))}${look.suffix}`, width), ...body]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** An open task as rows: a named one by `namedTaskRows`, else its whole text wrapped. */
|
|
166
|
+
const openTaskRows = (item: ChecklistItem, active: ActiveTask | undefined, width: number): string[] => {
|
|
167
|
+
const name = taskNameOf(item.text)
|
|
168
|
+
const look = taskLook(item, active)
|
|
169
|
+
return name ? namedTaskRows(name, item, look, width) : markedLines(item.text, look, width)
|
|
109
170
|
}
|
|
110
171
|
|
|
111
172
|
/** A task as a selectable row under its `key`: clickable across every line it takes, the cursor on its first. */
|
|
@@ -123,20 +184,20 @@ const tickedSummaryFrame = (count: number, phaseRowKey: string, width: number):
|
|
|
123
184
|
})
|
|
124
185
|
|
|
125
186
|
/**
|
|
126
|
-
* The children of an open phase, one frame per task: open ones first, `active`
|
|
187
|
+
* The children of an open phase, one frame per task: open ones first, the `active` ones (by task index) marked, then the ones
|
|
127
188
|
* deferred to another phase, then the ticked ones listed or summed up.
|
|
128
189
|
*/
|
|
129
190
|
export const phaseChildren = (
|
|
130
191
|
context: LaneContext,
|
|
131
192
|
phaseRowKey: string,
|
|
132
193
|
phase: PhaseProgress,
|
|
133
|
-
active:
|
|
194
|
+
active: ReadonlyMap<number, ActiveTask>,
|
|
134
195
|
): Frame[] => {
|
|
135
196
|
const { state, width } = context
|
|
136
197
|
const frameOf = (item: ChecklistItem, index: number): Frame => {
|
|
137
198
|
const key = itemKey(phaseRowKey, index)
|
|
138
|
-
const
|
|
139
|
-
const rows = state.expanded.has(key) ? openTaskRows(item,
|
|
199
|
+
const mark = active.get(index)
|
|
200
|
+
const rows = state.expanded.has(key) ? openTaskRows(item, mark, width) : closedTaskRow(item, mark, width)
|
|
140
201
|
return taskFrame(key, rows, context)
|
|
141
202
|
}
|
|
142
203
|
const framesWhere = (done: boolean): Frame[] =>
|
|
@@ -1,49 +1,61 @@
|
|
|
1
|
-
import { phaseLabel
|
|
2
|
-
import { fit,
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import type { ActivePhase, LaneSummary } from './lane-summary'
|
|
1
|
+
import { phaseLabel } from '../plan-format'
|
|
2
|
+
import { fit, paint, spread, visibleLength } from '../terminal/text'
|
|
3
|
+
import { deeper, type Frame, joinFrames, nest, rowFrame, textFrame } from './frame'
|
|
4
|
+
import type { ActivePhase, LaneSummary, PlacedPhase } from './lane-summary'
|
|
6
5
|
import {
|
|
7
|
-
|
|
6
|
+
activeArrow,
|
|
7
|
+
activeTitle,
|
|
8
|
+
agentCell,
|
|
8
9
|
COLUMN_SEPARATOR,
|
|
9
|
-
DEFERRED_MARK,
|
|
10
10
|
type LaneContext,
|
|
11
11
|
MIN_TITLE_WIDTH,
|
|
12
12
|
modelMark,
|
|
13
|
+
nowLine,
|
|
13
14
|
phaseChildren,
|
|
14
15
|
phaseCountCell,
|
|
15
16
|
plain,
|
|
17
|
+
subagentLine,
|
|
18
|
+
TASK_INDENT,
|
|
16
19
|
taskFrame,
|
|
20
|
+
taskTitle,
|
|
17
21
|
} from './layout-phase'
|
|
18
22
|
import { type PhaseModel, phaseModelKey } from './phase-models'
|
|
19
|
-
import {
|
|
23
|
+
import { phaseRanges } from './phase-ranges'
|
|
24
|
+
import { finishedKey, itemKey, nextKey, phaseKey } from './selection'
|
|
20
25
|
import { PHASE_COUNT } from './symbols'
|
|
21
26
|
import { type HerdTexts, TEXTS } from './texts'
|
|
22
27
|
|
|
23
|
-
/** The context of a lane's
|
|
28
|
+
/** The context of a lane's branches: the lane's, with the herd's words. */
|
|
24
29
|
type SummaryContext = LaneContext & { labels: HerdTexts }
|
|
25
30
|
|
|
26
|
-
/**
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const labelled = (labelWidth: number, label: string, frame: Frame): Frame =>
|
|
32
|
-
prefixed(frame, { first: padVisible(label, labelWidth), rest: ' '.repeat(labelWidth), style: paint.dim })
|
|
31
|
+
/** Whether only the boxes of the review gate are open. */
|
|
32
|
+
const onlyGateOpen = ({ items }: ActivePhase['phase']): boolean => {
|
|
33
|
+
const open = items.filter(({ done }) => !done)
|
|
34
|
+
return open.length > 0 && open.every(({ gate }) => gate)
|
|
35
|
+
}
|
|
33
36
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* `· 6 min ago`, dim: when the running phase was last ticked or reported, nothing when the herd saw neither; `· gate`
|
|
39
|
+
* after it while only the review gate is open. Empty when there is neither.
|
|
40
|
+
*/
|
|
41
|
+
const ageCell = ({ minutesAgo, phase }: ActivePhase, labels: HerdTexts): string => {
|
|
42
|
+
const words = [
|
|
43
|
+
...(minutesAgo === null ? [] : [labels.minutesAgo(minutesAgo)]),
|
|
44
|
+
...(onlyGateOpen(phase) ? [labels.gate] : []),
|
|
45
|
+
]
|
|
46
|
+
return words.length > 0 ? `${paint.dim(words.join(' '))}${COLUMN_SEPARATOR}` : ''
|
|
47
|
+
}
|
|
37
48
|
|
|
38
49
|
/**
|
|
39
|
-
*
|
|
40
|
-
* too narrow to leave the title `MIN_TITLE_WIDTH` cells, the model gives
|
|
50
|
+
* `P12 <title> [model] · 6 min ago 16/26`: title at the left, model, age and count at the right edge, no arrow
|
|
51
|
+
* (arrows stand before tasks only). In a pane too narrow to leave the title `MIN_TITLE_WIDTH` cells, the model gives
|
|
52
|
+
* way first, then the age.
|
|
41
53
|
*/
|
|
42
54
|
const activeRow = (active: ActivePhase, current: boolean, context: SummaryContext, model?: PhaseModel): string => {
|
|
43
55
|
const { width, labels } = context
|
|
44
56
|
const { phase } = active
|
|
45
57
|
const title = phase.title === null ? paint.dim(TEXTS.notInPlan) : plain(phase.title)
|
|
46
|
-
const head = `${
|
|
58
|
+
const head = `${phaseLabel(phase.id)} ${title}`
|
|
47
59
|
const count = phaseCountCell(phase, current)
|
|
48
60
|
const withAge = `${ageCell(active, labels)}${count}`
|
|
49
61
|
const mark = modelMark(model)
|
|
@@ -52,17 +64,65 @@ const activeRow = (active: ActivePhase, current: boolean, context: SummaryContex
|
|
|
52
64
|
return spread(head, candidates.find((right) => titleRoom(right) >= MIN_TITLE_WIDTH) ?? count, width)
|
|
53
65
|
}
|
|
54
66
|
|
|
67
|
+
/** What an active task reads as on one row: `Review gate` for a box of the review gate, else its short name or text. */
|
|
68
|
+
const taskText = (item: ActivePhase['phase']['items'][number], labels: HerdTexts): string =>
|
|
69
|
+
item.gate ? labels.reviewGate : taskTitle(item.text)
|
|
70
|
+
|
|
71
|
+
/** The texts of the tasks agents report on the phase, then the descriptions of the subagents that name no phase as `Subagent: …`. */
|
|
72
|
+
const reportedTexts = (
|
|
73
|
+
{ phase, tasks }: ActivePhase,
|
|
74
|
+
subagentTasks: readonly string[],
|
|
75
|
+
labels: HerdTexts,
|
|
76
|
+
): string[] => [
|
|
77
|
+
...tasks
|
|
78
|
+
.filter(({ guessed }) => !guessed)
|
|
79
|
+
.flatMap(({ index }) => {
|
|
80
|
+
const item = phase.items[index]
|
|
81
|
+
return item ? [taskText(item, labels)] : []
|
|
82
|
+
}),
|
|
83
|
+
...subagentTasks.map((description) => `${TEXTS.subagent} ${taskTitle(description)}`),
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* `▶ Name +1` for a folded lane, behind `indent`: the first task the phase is on with the arrow of the open lane, `+n`
|
|
88
|
+
* for the others in parallel at the end; no ticks, the phase row shows them; the name cut to the pane's edge. A task
|
|
89
|
+
* an agent reports comes first, the descriptions of the `subagentTasks` after it. Null while neither exists: a task
|
|
90
|
+
* the herd only guesses from words is no sign that the lane works.
|
|
91
|
+
*/
|
|
92
|
+
export const foldedTaskLine = (
|
|
93
|
+
active: ActivePhase,
|
|
94
|
+
subagentTasks: readonly string[],
|
|
95
|
+
labels: HerdTexts,
|
|
96
|
+
indent: string,
|
|
97
|
+
width: number,
|
|
98
|
+
): string | null => {
|
|
99
|
+
const [first, ...others] = reportedTexts(active, subagentTasks, labels)
|
|
100
|
+
if (first === undefined) return null
|
|
101
|
+
const lead = `${activeArrow(false)} `
|
|
102
|
+
const tail = others.length > 0 ? ` +${others.length}` : ''
|
|
103
|
+
const room = Math.max(width - visibleLength(indent) - visibleLength(lead) - visibleLength(tail), 1)
|
|
104
|
+
return `${indent}${lead}${activeTitle(fit(first, room), false)}${tail}`
|
|
105
|
+
}
|
|
106
|
+
|
|
55
107
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
108
|
+
* One row per active task under a closed running phase, each with its own arrow: the title bold and the agent dim
|
|
109
|
+
* after it where agents reported it, `▷` and a plain title where the herd guesses. Always on one line whatever the
|
|
110
|
+
* task's fold, and a row a click opens like the task itself; `Review gate` in place of a box of the review gate.
|
|
58
111
|
*/
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
112
|
+
const taskLinesFrame = (context: SummaryContext, phaseRowKey: string, { phase, tasks }: ActivePhase): Frame => {
|
|
113
|
+
return joinFrames(
|
|
114
|
+
tasks.flatMap((task) => {
|
|
115
|
+
const item = phase.items[task.index]
|
|
116
|
+
if (!item) return []
|
|
117
|
+
const text = taskText(item, context.labels)
|
|
118
|
+
const line = spread(
|
|
119
|
+
`${TASK_INDENT}${activeArrow(task.guessed)} ${activeTitle(text, task.guessed)}`,
|
|
120
|
+
agentCell(task.agent),
|
|
121
|
+
context.width,
|
|
122
|
+
)
|
|
123
|
+
return [taskFrame(itemKey(phaseRowKey, task.index), [line], context)]
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
66
126
|
}
|
|
67
127
|
|
|
68
128
|
/** A running phase as a row that opens to its tasks; closed, the task it is on below it. */
|
|
@@ -71,70 +131,99 @@ const activeFrame = (context: SummaryContext, active: ActivePhase): Frame => {
|
|
|
71
131
|
const stepIndex = active.stepIndex ?? -1
|
|
72
132
|
const key = phaseKey(progress.lane.name, stepIndex, active.phase.id)
|
|
73
133
|
const current = progress.current?.stepIndex === stepIndex && progress.current.phaseId === active.phase.id
|
|
74
|
-
const item = active.taskIndex === null ? null : (active.phase.items[active.taskIndex] ?? null)
|
|
75
134
|
const model = context.models[phaseModelKey(stepIndex, active.phase.id)]
|
|
76
135
|
const row = rowFrame(key, [activeRow(active, current, context, model)], [], context)
|
|
77
|
-
if (!state.expanded.has(key)) return joinFrames([row,
|
|
78
|
-
return joinFrames([
|
|
136
|
+
if (!state.expanded.has(key)) return joinFrames([row, taskLinesFrame(context, key, active)])
|
|
137
|
+
return joinFrames([
|
|
138
|
+
row,
|
|
139
|
+
nest(phaseChildren(deeper(context), key, active.phase, new Map(active.tasks.map((task) => [task.index, task])))),
|
|
140
|
+
])
|
|
79
141
|
}
|
|
80
142
|
|
|
81
|
-
/** `wave 2: P5 · P10` or, without waves, `P7`. */
|
|
82
|
-
const nextText = ({ next }: LaneSummary, labels: HerdTexts): string =>
|
|
83
|
-
next ? `${next.wave === null ? '' : `${labels.wave(next.wave)} `}${phaseLabels(next.phaseIds, ' · ')}` : ''
|
|
84
|
-
|
|
85
143
|
/** `P8 ?` for a named phase the plan lacks, `P9 –` for one without a checkbox. */
|
|
86
144
|
const unwrittenText = ({ id, missing }: LaneSummary['unwritten'][number]): string =>
|
|
87
145
|
`${phaseLabel(id)} ${paint.dim(missing ? PHASE_COUNT.missing : PHASE_COUNT.empty)}`
|
|
88
146
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
147
|
+
/**
|
|
148
|
+
* `next P5–P7, P10 P8 ? · Second plan: P1` on one line: every open phase that does not run, the current plan's as
|
|
149
|
+
* ranges, then the other plans' by their name; empty when none waits.
|
|
150
|
+
*/
|
|
151
|
+
const nextLineText = (summary: LaneSummary, labels: HerdTexts): string => {
|
|
152
|
+
const ownIds = summary.waiting.filter(({ planTitle }) => planTitle === null).flatMap(({ phaseIds }) => phaseIds)
|
|
153
|
+
const ownRanges = phaseRanges(ownIds)
|
|
154
|
+
const own = [...(ownRanges ? [ownRanges] : []), ...summary.unwritten.map(unwrittenText)]
|
|
155
|
+
const others = summary.waiting
|
|
96
156
|
.filter(({ planTitle }) => planTitle !== null)
|
|
97
|
-
.map(({ planTitle, phaseIds }) => `${plain((planTitle ?? '').split(':')[0]).trim()}: ${
|
|
98
|
-
|
|
157
|
+
.map(({ planTitle, phaseIds }) => `${plain((planTitle ?? '').split(':')[0]).trim()}: ${phaseRanges(phaseIds)}`)
|
|
158
|
+
const text = [...(own.length > 0 ? [own.join(' ')] : []), ...others].join(' · ')
|
|
159
|
+
return text ? `${paint.dim(labels.next)} ${text}` : ''
|
|
99
160
|
}
|
|
100
161
|
|
|
101
|
-
/** `
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
const which = unmerged.length === finished.length ? '' : `${phaseLabels(unmerged, ' ')} `
|
|
106
|
-
return ` · ${paint.yellow(`${which}${labels.notMerged}`)}`
|
|
162
|
+
/** `done P1–P7, P10` on one line: the word `done` in green, the finished phases as ranges. */
|
|
163
|
+
const finishedText = (summary: LaneSummary, labels: HerdTexts): string => {
|
|
164
|
+
const phases = phaseRanges(summary.finished.map(({ id }) => id))
|
|
165
|
+
return phases ? `${paint.green(labels.done)} ${phases}` : ''
|
|
107
166
|
}
|
|
108
167
|
|
|
109
|
-
/**
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
168
|
+
/** A phase that does not run as a row, which opens to its tasks like a running one; closed, it has no task line below it. */
|
|
169
|
+
const restingFrame = (context: SummaryContext, { phase, stepIndex }: PlacedPhase): Frame =>
|
|
170
|
+
activeFrame(context, { phase, stepIndex, minutesAgo: null, tasks: [] })
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One line of text cut at the pane's edge, a row under `key`; no line when the text is empty. Open, it continues with
|
|
174
|
+
* a row per phase it names and the `unwritten` lines, each phase opening to its tasks.
|
|
175
|
+
*/
|
|
176
|
+
const foldFrame = (
|
|
177
|
+
context: SummaryContext,
|
|
178
|
+
key: string,
|
|
179
|
+
text: string,
|
|
180
|
+
{ phases, unwritten }: { phases: readonly PlacedPhase[]; unwritten: string[] },
|
|
181
|
+
): Frame => {
|
|
182
|
+
if (!text) return textFrame([])
|
|
183
|
+
const row = rowFrame(key, [fit(text, context.width)], [], context)
|
|
184
|
+
if (!context.state.expanded.has(key)) return row
|
|
185
|
+
const inner = deeper(context)
|
|
186
|
+
const children = [
|
|
187
|
+
...phases.map((placed) => restingFrame(inner, placed)),
|
|
188
|
+
...unwritten.map((line) => textFrame([fit(line, inner.width)])),
|
|
189
|
+
]
|
|
190
|
+
return joinFrames([row, nest(children)])
|
|
113
191
|
}
|
|
114
192
|
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
text ? labelled(labelWidth, label, textFrame([fit(text, Math.max(width - labelWidth, 1))])) : textFrame([])
|
|
193
|
+
/** What an open lane shows above its phases, drawn at the width of its branches: the session, its pull requests and its link chips. */
|
|
194
|
+
export type LaneHead = { session: Frame; pullRequests: Frame; chips: Frame }
|
|
118
195
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
196
|
+
/** The `next` and `done` rows of an open lane. */
|
|
197
|
+
const restingFrames = (context: SummaryContext, summary: LaneSummary): Frame[] => {
|
|
198
|
+
const laneName = context.progress.lane.name
|
|
199
|
+
return [
|
|
200
|
+
foldFrame(context, nextKey(laneName), nextLineText(summary, context.labels), {
|
|
201
|
+
phases: summary.waitingPhases,
|
|
202
|
+
unwritten: summary.unwritten.map(unwrittenText),
|
|
203
|
+
}),
|
|
204
|
+
foldFrame(context, finishedKey(laneName), finishedText(summary, context.labels), {
|
|
205
|
+
phases: summary.finishedPhases,
|
|
206
|
+
unwritten: [],
|
|
207
|
+
}),
|
|
208
|
+
]
|
|
209
|
+
}
|
|
121
210
|
|
|
122
211
|
/**
|
|
123
|
-
* The children of an open lane,
|
|
124
|
-
*
|
|
212
|
+
* The children of an open lane, one branch each, told apart by their marks rather than by a label: the session
|
|
213
|
+
* (`⎇`), the pull requests (`↗`), the link chips, then one branch per running phase (its tasks `▶`/`▷` under it), the
|
|
214
|
+
* `now:` and `Subagent:` lines, `next` with the open phases that wait, and `done` with the finished ones; a branch without lines is left out.
|
|
125
215
|
*/
|
|
126
216
|
export const summaryChildren = (context: LaneContext, summary: LaneSummary, head: LaneHead): Frame[] => {
|
|
127
217
|
const labels = TEXTS.herd
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const { sections } = labels
|
|
131
|
-
const now = summary.active.map((active) => activeFrame(inner, active))
|
|
218
|
+
const inner = { ...context, labels }
|
|
219
|
+
const { width } = context
|
|
132
220
|
return [
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
221
|
+
head.session,
|
|
222
|
+
head.pullRequests,
|
|
223
|
+
head.chips,
|
|
224
|
+
...summary.active.map((active) => activeFrame(inner, active)),
|
|
225
|
+
...summary.unplanned.map((task) => textFrame([nowLine(task, width)])),
|
|
226
|
+
...summary.subagentTasks.map((description) => textFrame([subagentLine(description, width)])),
|
|
227
|
+
...restingFrames(inner, summary),
|
|
139
228
|
]
|
|
140
229
|
}
|