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
package/src/plans/snapshot.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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'
|
|
5
|
-
import { agentLog } from '../context/session-log'
|
|
5
|
+
import { agentLog, defaultProjectsDirectory } from '../context/session-log'
|
|
6
6
|
import {
|
|
7
7
|
type LaneProgress,
|
|
8
8
|
laneProgress,
|
|
@@ -13,9 +13,14 @@ import {
|
|
|
13
13
|
shortTitle,
|
|
14
14
|
sprintTitleOf,
|
|
15
15
|
} from '../plan-format'
|
|
16
|
+
import type { WaitingItem } from '../plan-format/waiting'
|
|
17
|
+
import { dropUnused } from '../shared/stat-cache'
|
|
18
|
+
import type { TaskSignal } from '../task-signals/types'
|
|
16
19
|
import type { LaneAgent, LaneStatusSource } from '../terminal'
|
|
17
20
|
import type { SourceTicket, WatchBadges, WatchHealth } from '../watch/plan-badges'
|
|
18
21
|
import { readWatchReading } from '../watch/plan-badges-read'
|
|
22
|
+
import { assignSignals, laneOfDirectory, type UnplannedTask } from './active-tasks'
|
|
23
|
+
import { type AppointmentBlock, appointmentBlock } from './appointment-rows'
|
|
19
24
|
import { boardRowsOf } from './board-rows'
|
|
20
25
|
import {
|
|
21
26
|
type ActivityTimes,
|
|
@@ -26,21 +31,39 @@ import {
|
|
|
26
31
|
type PlanModified,
|
|
27
32
|
withMovedPhase,
|
|
28
33
|
} from './current-phase'
|
|
34
|
+
import { activityTextOf, taskSignalsOf } from './current-task'
|
|
29
35
|
import { errorText } from './error-text'
|
|
30
36
|
import { type BaseReader, baseReaderOf, gitStatusOf, refExists } from './git'
|
|
31
37
|
import type { PullRequestState } from './github'
|
|
32
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
type Fallbacks,
|
|
40
|
+
laneWorkOf,
|
|
41
|
+
type PlanSources,
|
|
42
|
+
readAtBase,
|
|
43
|
+
readFallback,
|
|
44
|
+
readPlans,
|
|
45
|
+
textAtBase,
|
|
46
|
+
} from './lane-plans'
|
|
33
47
|
import { laneSummary } from './lane-summary'
|
|
34
48
|
import { boardRowTexts, laneTicketsOf, withoutTicketLinks } from './lane-tickets'
|
|
35
|
-
import {
|
|
49
|
+
import type { LaneView } from './layout'
|
|
36
50
|
import { extractLinkChips } from './link-chips'
|
|
37
51
|
import type { Options } from './options'
|
|
38
52
|
import { laneModelsOf, type PhaseModelRecord } from './phase-models'
|
|
39
53
|
import { readPhaseModels } from './phase-models-read'
|
|
54
|
+
import { laneHeads, planPullRequests, readerOncePerRef } from './plan-heads'
|
|
40
55
|
import { type PlannedPlan, plannedPlans } from './planned'
|
|
56
|
+
import type { PullRequest } from './pull-requests'
|
|
57
|
+
import type { ReadCache } from './read-cache'
|
|
58
|
+
import { type SentChip, sentChipsByLane } from './sent-files'
|
|
41
59
|
import { baseRefOf, boardRepoPathOf, type PlansSettings, worktreeOf } from './settings'
|
|
60
|
+
import { signalledTitle } from './signalled-phase'
|
|
61
|
+
import { readAllSignals } from './signals-read'
|
|
62
|
+
import type { SubagentActivity } from './subagents'
|
|
63
|
+
import { readSubagentActivity, subagentSourceOf } from './subagents-read'
|
|
42
64
|
import { boardTexts } from './texts'
|
|
43
65
|
import { type UnlistedLane, unlistedLanes, unlistedSprintLane } from './unlisted-lanes'
|
|
66
|
+
import { readWaitingRecords, waitingItemsOf } from './waiting-records'
|
|
44
67
|
import { withWorkedPhases } from './worked-phases'
|
|
45
68
|
|
|
46
69
|
/** Where the view reads from: the main checkout, the sprint document, the command line and the project settings. */
|
|
@@ -52,6 +75,10 @@ export type Source = {
|
|
|
52
75
|
terminal: LaneStatusSource
|
|
53
76
|
/** The log lines of an agent's chat, null without one; defaults to reading them from `~/.claude/projects`. */
|
|
54
77
|
readAgentLog?: AgentLogReader
|
|
78
|
+
/** The worktrees subagents of the coordinator work in; defaults to reading them from `~/.claude/projects`. */
|
|
79
|
+
readSubagents?: (root: string, worktreeDirectory: string) => Map<string, SubagentActivity>
|
|
80
|
+
/** What agents report doing (Claude's task lists, `jh task`); defaults to reading `~/.claude/projects` and `.jackherd/tasks/`. */
|
|
81
|
+
readSignals?: (root: string, worktreeDirectory: string, now: number) => TaskSignal[]
|
|
55
82
|
}
|
|
56
83
|
|
|
57
84
|
/**
|
|
@@ -86,6 +113,12 @@ export type Snapshot = {
|
|
|
86
113
|
watch?: WatchHealth
|
|
87
114
|
/** True when the config marks a ticket source, so a missing watcher is said without a pull request too. */
|
|
88
115
|
watchesTickets?: boolean
|
|
116
|
+
/** What waits on the user's answer: the open rows of the board's section for it, from the checkout's board file. */
|
|
117
|
+
waiting?: WaitingItem[]
|
|
118
|
+
/** The board's open appointments, read from the same board text as `waiting`, for the block under it. */
|
|
119
|
+
appointments?: AppointmentBlock
|
|
120
|
+
/** What agents report doing that no lane takes: the coordinator's own, or a lane the board does not list. */
|
|
121
|
+
unplaced?: UnplannedTask[]
|
|
89
122
|
}
|
|
90
123
|
|
|
91
124
|
type LaneSources = {
|
|
@@ -93,32 +126,77 @@ type LaneSources = {
|
|
|
93
126
|
base: BaseReader
|
|
94
127
|
sprintDirectory: string
|
|
95
128
|
agents: LaneAgent[]
|
|
129
|
+
/** What subagents of the coordinator do in each worktree; a herdr agent in the worktree comes first. */
|
|
130
|
+
subagents: Map<string, SubagentActivity>
|
|
131
|
+
/** The agents' log reader, asking once per agent however often the read is used in one snapshot. */
|
|
132
|
+
readLog: AgentLogReader
|
|
96
133
|
pullRequests: PullRequestState
|
|
134
|
+
/** The open pull requests that change a plan, whose heads raise the ticks the base ref has. */
|
|
135
|
+
planPullRequests: PullRequest[]
|
|
136
|
+
/** The reader of a pull request head, resolved once per read whatever the number of lanes. */
|
|
137
|
+
headReaderOf: (ref: string) => BaseReader
|
|
138
|
+
/** What agents report doing, in every lane and outside them. */
|
|
139
|
+
signals: TaskSignal[]
|
|
140
|
+
/** The directory the lanes' worktrees lie in, which tells a signal's lane. */
|
|
141
|
+
worktrees: string
|
|
142
|
+
parsed?: PlanSources['parsed']
|
|
97
143
|
}
|
|
98
144
|
|
|
99
|
-
|
|
145
|
+
/** `liveMarkdown` is the checkout's board file (the coordinator edits it before any commit), else `markdown`. */
|
|
146
|
+
type BoardLanes = { markdown: string; liveMarkdown: string; lanes: SprintLane[] }
|
|
100
147
|
|
|
101
148
|
const SHOWN_NAMES = 2
|
|
102
149
|
|
|
103
150
|
type LaneResult = { view: LaneView; files: string[]; texts: Record<string, string>; pane: string | null }
|
|
104
151
|
|
|
152
|
+
/** The board's lanes the view shows, the running lanes it does not list, and what the lanes read from. */
|
|
153
|
+
type LaneSet = { lanes: SprintLane[]; unlisted: UnlistedLane[]; sources: LaneSources; errors: string[] }
|
|
154
|
+
|
|
155
|
+
/** What a lane's view is decorated with after it was read. */
|
|
156
|
+
type Decorations = {
|
|
157
|
+
reading: ReturnType<typeof readWatchReading> | null
|
|
158
|
+
phaseModels: readonly PhaseModelRecord[]
|
|
159
|
+
sprintDirectory: string
|
|
160
|
+
sentChips: ReadonlyMap<string | null, SentChip[]>
|
|
161
|
+
}
|
|
162
|
+
|
|
105
163
|
/** How full the agent's context is, null when the terminal gave no session or the session has no log. */
|
|
106
|
-
const contextOf = (agent: LaneAgent | undefined, source: Source): ContextReading | null => {
|
|
107
|
-
const report = contextOfAgent(agent,
|
|
164
|
+
const contextOf = (agent: LaneAgent | undefined, source: Source, readLog: AgentLogReader): ContextReading | null => {
|
|
165
|
+
const report = contextOfAgent(agent, readLog, source.settings.context)
|
|
108
166
|
return report && { share: report.share, level: report.level }
|
|
109
167
|
}
|
|
110
168
|
|
|
169
|
+
/** A reader that asks once per agent (session and directory), so context and task signal share one read of a large log. */
|
|
170
|
+
const onceReader = (read: AgentLogReader): AgentLogReader => {
|
|
171
|
+
const answers = new Map<string, string[] | null>()
|
|
172
|
+
return (agent) => {
|
|
173
|
+
const key = `${agent.session}\0${agent.cwd}`
|
|
174
|
+
if (!answers.has(key)) answers.set(key, read(agent))
|
|
175
|
+
return answers.get(key) ?? null
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const WORK_ENTRIES = 40
|
|
180
|
+
|
|
181
|
+
/** What the agents of a lane did lately: the end of its own agent's log and the log ends of subagents working in its worktree. */
|
|
182
|
+
const workText = ({ readLog, subagents }: LaneSources, lane: string, agent: LaneAgent | undefined): string => {
|
|
183
|
+
const lines = agent ? readLog(agent) : null
|
|
184
|
+
return [lines ? activityTextOf(lines, WORK_ENTRIES) : '', subagents.get(lane)?.activity ?? ''].join(' ')
|
|
185
|
+
}
|
|
186
|
+
|
|
111
187
|
/** What a lane's worktree and agent say, listed in the sprint or not: git state, pull requests, context. */
|
|
112
188
|
const runningState = async (sources: LaneSources, name: string, agent: LaneAgent | undefined) => {
|
|
113
189
|
const { source, pullRequests } = sources
|
|
114
190
|
const gitStatus = await gitStatusOf(worktreeOf(source.root, source.settings, name), baseRefOf(source.settings))
|
|
191
|
+
const subagent = agent ? undefined : sources.subagents.get(name)
|
|
192
|
+
const title = agent?.title ?? subagent?.description
|
|
115
193
|
return {
|
|
116
194
|
gitStatus,
|
|
117
195
|
view: {
|
|
118
|
-
status: { agent: agent?.status ?? null, ...gitStatus },
|
|
119
|
-
...(
|
|
196
|
+
status: { agent: agent?.status ?? subagent?.status ?? null, ...gitStatus },
|
|
197
|
+
...(title ? { sessionTitle: title } : {}),
|
|
120
198
|
pullRequests: gitStatus.branch ? (pullRequests.byBranch.get(gitStatus.branch) ?? []) : [],
|
|
121
|
-
context: contextOf(agent, source),
|
|
199
|
+
context: contextOf(agent, source, sources.readLog),
|
|
122
200
|
},
|
|
123
201
|
}
|
|
124
202
|
}
|
|
@@ -163,19 +241,41 @@ const laneView = async (
|
|
|
163
241
|
modified,
|
|
164
242
|
} = await readPlans(
|
|
165
243
|
lane,
|
|
166
|
-
{
|
|
244
|
+
{
|
|
245
|
+
root,
|
|
246
|
+
settings,
|
|
247
|
+
base: sources.base,
|
|
248
|
+
sprintDirectory: sources.sprintDirectory,
|
|
249
|
+
heads: laneHeads(sources.planPullRequests, gitStatus.branch, sources.headReaderOf),
|
|
250
|
+
parsed: sources.parsed,
|
|
251
|
+
},
|
|
167
252
|
laneWorkOf(worktree, gitStatus),
|
|
168
253
|
fallbacks,
|
|
169
254
|
)
|
|
170
255
|
const shown = withWorkedPhases(lane, phasesByHref)
|
|
171
256
|
const { progress, activity } = progressWithMemory(shown, phasesByHref, memories, { now, modified })
|
|
257
|
+
const laneSubagents = sources.subagents.get(lane.name)
|
|
172
258
|
const view: LaneView = {
|
|
173
259
|
progress,
|
|
174
260
|
...running,
|
|
175
261
|
chips: Object.fromEntries(
|
|
176
262
|
Object.entries(texts).map(([href, text]) => [href, extractLinkChips(text, settings.linkKinds)]),
|
|
177
263
|
),
|
|
178
|
-
summary: laneSummary(
|
|
264
|
+
summary: laneSummary(
|
|
265
|
+
{
|
|
266
|
+
progress,
|
|
267
|
+
phases: phasesByHref,
|
|
268
|
+
activity,
|
|
269
|
+
guesses: taskSignalsOf(Object.values(phasesByHref).flat(), workText(sources, lane.name, agent), now),
|
|
270
|
+
reported: sources.signals.filter(({ cwd }) => laneOfDirectory(cwd, sources.worktrees) === lane.name),
|
|
271
|
+
subagents: laneSubagents?.phaseRuns ?? [],
|
|
272
|
+
subagentTasks: laneSubagents?.taskDescriptions ?? [],
|
|
273
|
+
phaselessReviews: laneSubagents?.phaselessReviews ?? [],
|
|
274
|
+
describedSubagents: laneSubagents?.describedSubagents ?? [],
|
|
275
|
+
branch: gitStatus.branch,
|
|
276
|
+
},
|
|
277
|
+
now,
|
|
278
|
+
),
|
|
179
279
|
}
|
|
180
280
|
return { view, files, texts, pane: agent?.pane ?? null }
|
|
181
281
|
}
|
|
@@ -185,14 +285,14 @@ const laneView = async (
|
|
|
185
285
|
* and without phases, marked as unlisted.
|
|
186
286
|
*/
|
|
187
287
|
const unlistedView = async (sources: LaneSources, { name, agent }: UnlistedLane): Promise<LaneResult> => {
|
|
188
|
-
const { view: running } = await runningState(sources, name, agent)
|
|
288
|
+
const { view: running } = await runningState(sources, name, agent ?? undefined)
|
|
189
289
|
const view: LaneView = {
|
|
190
290
|
progress: laneProgress(unlistedSprintLane(name), {}),
|
|
191
291
|
...running,
|
|
192
292
|
chips: {},
|
|
193
293
|
unlisted: true,
|
|
194
294
|
}
|
|
195
|
-
return { view, files: [], texts: {}, pane: agent
|
|
295
|
+
return { view, files: [], texts: {}, pane: agent?.pane ?? null }
|
|
196
296
|
}
|
|
197
297
|
|
|
198
298
|
/** The view with the watcher's badges of its own pull requests, only when there are any, so it otherwise stays as it was. */
|
|
@@ -223,6 +323,35 @@ const withTickets = (result: LaneResult, tickets: readonly SourceTicket[]): Lane
|
|
|
223
323
|
return found ? { ...view, chips: withoutTicketLinks(view.chips, found), tickets: found } : view
|
|
224
324
|
}
|
|
225
325
|
|
|
326
|
+
/** What agents report doing; nothing when it cannot be read. */
|
|
327
|
+
const readSignals = (
|
|
328
|
+
{ root, readSignals: read }: Source,
|
|
329
|
+
worktrees: string,
|
|
330
|
+
now: number,
|
|
331
|
+
cache?: ReadCache,
|
|
332
|
+
): TaskSignal[] => {
|
|
333
|
+
try {
|
|
334
|
+
return (
|
|
335
|
+
read ??
|
|
336
|
+
((main, directory, at) =>
|
|
337
|
+
readAllSignals({ projects: defaultProjectsDirectory(), root: main, worktrees: directory }, at, cache?.signals))
|
|
338
|
+
)(root, worktrees, now)
|
|
339
|
+
} catch {
|
|
340
|
+
return []
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** The reports no shown lane takes, as lines for the overview: the coordinator's own and those of lanes without a plan. */
|
|
345
|
+
const unplacedOf = (sources: LaneSources, lanes: readonly SprintLane[], onlyLane: boolean): UnplannedTask[] => {
|
|
346
|
+
if (onlyLane) return []
|
|
347
|
+
const listed = new Set(lanes.map((lane) => lane.name))
|
|
348
|
+
const rest = sources.signals.filter(({ cwd }) => {
|
|
349
|
+
const lane = laneOfDirectory(cwd, sources.worktrees)
|
|
350
|
+
return lane === null || !listed.has(lane)
|
|
351
|
+
})
|
|
352
|
+
return assignSignals([], rest).unplanned
|
|
353
|
+
}
|
|
354
|
+
|
|
226
355
|
/** The terminal's agents, or none with the reason when it cannot be asked, so the screen says why lanes show "no agent". */
|
|
227
356
|
const readAgents = (terminal: LaneStatusSource): { agents: LaneAgent[]; errors: string[] } => {
|
|
228
357
|
try {
|
|
@@ -232,6 +361,26 @@ const readAgents = (terminal: LaneStatusSource): { agents: LaneAgent[]; errors:
|
|
|
232
361
|
}
|
|
233
362
|
}
|
|
234
363
|
|
|
364
|
+
/** What subagents of the coordinator do in the worktrees; nothing when their logs cannot be read. */
|
|
365
|
+
const readSubagents = (
|
|
366
|
+
{ root, settings, readSubagents: read }: Source,
|
|
367
|
+
worktrees: string,
|
|
368
|
+
): Map<string, SubagentActivity> => {
|
|
369
|
+
try {
|
|
370
|
+
return (
|
|
371
|
+
read ?? ((main, directory) => readSubagentActivity(subagentSourceOf(main, directory, settings.skillPrefix)))
|
|
372
|
+
)(root, worktrees)
|
|
373
|
+
} catch {
|
|
374
|
+
return new Map()
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** The view with the files its session sent to the user, only when there are any, so it otherwise stays as it was. */
|
|
379
|
+
const withSentFiles = (view: LaneView, chips: ReadonlyMap<string | null, SentChip[]>): LaneView => {
|
|
380
|
+
const found = chips.get(view.progress.lane.name)
|
|
381
|
+
return found ? { ...view, sentFiles: found } : view
|
|
382
|
+
}
|
|
383
|
+
|
|
235
384
|
/** Up to two names, then how many more: `a, b and 3 more`. */
|
|
236
385
|
const namedWithMore = (names: readonly string[]): string => {
|
|
237
386
|
const more = names.length - SHOWN_NAMES
|
|
@@ -259,8 +408,8 @@ const readBoardLanes = async (
|
|
|
259
408
|
const { sprintFile, settings } = source
|
|
260
409
|
const atBase = textAtBase(base, boardPath)
|
|
261
410
|
const markdown = atBase ?? (await readFallback(boardPath, sprintFile, fallbacks)) ?? ''
|
|
262
|
-
const
|
|
263
|
-
return { markdown, lanes: boardRowsOf(markdown,
|
|
411
|
+
const liveMarkdown = atBase !== null && existsSync(sprintFile) ? await Bun.file(sprintFile).text() : markdown
|
|
412
|
+
return { markdown, liveMarkdown, lanes: boardRowsOf(markdown, liveMarkdown, settings.planLanguage).lanes }
|
|
264
413
|
}
|
|
265
414
|
|
|
266
415
|
/** The plans the sprint's plans tables name, read at the base ref, by link; lanes read theirs themselves. */
|
|
@@ -291,83 +440,153 @@ const forgetLanes = (memories: Map<string, LaneMemory> | undefined, lanes: reado
|
|
|
291
440
|
for (const name of [...(memories?.keys() ?? [])]) if (!names.has(name)) memories?.delete(name)
|
|
292
441
|
}
|
|
293
442
|
|
|
443
|
+
const laneSetOf = (
|
|
444
|
+
source: Source,
|
|
445
|
+
context: {
|
|
446
|
+
base: BaseReader
|
|
447
|
+
board: BoardLanes
|
|
448
|
+
sprintDirectory: string
|
|
449
|
+
worktrees: string
|
|
450
|
+
cache?: ReadCache
|
|
451
|
+
},
|
|
452
|
+
pullRequests: PullRequestState,
|
|
453
|
+
now: number,
|
|
454
|
+
): LaneSet => {
|
|
455
|
+
const { base, board, sprintDirectory, worktrees, cache } = context
|
|
456
|
+
const { agents, errors } = readAgents(source.terminal)
|
|
457
|
+
const isWanted = ({ name }: { name: string }) => !source.options.lane || name === source.options.lane
|
|
458
|
+
const subagents = readSubagents(source, worktrees)
|
|
459
|
+
const listed = new Set(board.lanes.map((lane) => lane.name))
|
|
460
|
+
return {
|
|
461
|
+
lanes: board.lanes.filter(isWanted),
|
|
462
|
+
unlisted: unlistedLanes(agents, listed, worktrees, new Set(subagents.keys())).filter(isWanted),
|
|
463
|
+
sources: {
|
|
464
|
+
source,
|
|
465
|
+
base,
|
|
466
|
+
sprintDirectory,
|
|
467
|
+
agents,
|
|
468
|
+
subagents,
|
|
469
|
+
readLog: onceReader(source.readAgentLog ?? agentLog),
|
|
470
|
+
pullRequests,
|
|
471
|
+
planPullRequests: planPullRequests([...pullRequests.byBranch.values()].flat(), source.settings.planDirectory),
|
|
472
|
+
headReaderOf: readerOncePerRef((ref) => baseReaderOf(source.root, ref)),
|
|
473
|
+
signals: readSignals(source, worktrees, now, cache),
|
|
474
|
+
worktrees,
|
|
475
|
+
parsed: cache && { entries: cache.plans, used: new Set<string>() },
|
|
476
|
+
},
|
|
477
|
+
errors,
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** The views of the listed lanes, then of the unlisted ones; the lanes' memories are cleaned of lanes the board dropped. */
|
|
482
|
+
const laneResultsOf = async (
|
|
483
|
+
{ lanes, unlisted, sources }: LaneSet,
|
|
484
|
+
fallbacks: Fallbacks,
|
|
485
|
+
now: number,
|
|
486
|
+
memories: Map<string, LaneMemory> | undefined,
|
|
487
|
+
): Promise<LaneResult[]> => {
|
|
488
|
+
const results = [
|
|
489
|
+
...(await Promise.all(lanes.map((lane) => laneView(sources, lane, fallbacks, now, memories)))),
|
|
490
|
+
...(await Promise.all(unlisted.map((lane) => unlistedView(sources, lane)))),
|
|
491
|
+
]
|
|
492
|
+
forgetLanes(memories, lanes)
|
|
493
|
+
return results
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Each lane's view with its tickets, the watcher's badges, its phases' models and the files sent from it. */
|
|
497
|
+
const decoratedViews = (
|
|
498
|
+
results: readonly LaneResult[],
|
|
499
|
+
{ reading, phaseModels, sprintDirectory, sentChips }: Decorations,
|
|
500
|
+
): LaneView[] =>
|
|
501
|
+
results.map((result) =>
|
|
502
|
+
withSentFiles(
|
|
503
|
+
withModels(
|
|
504
|
+
withBadges(withTickets(result, reading?.tickets ?? []), reading?.badges ?? {}),
|
|
505
|
+
phaseModels,
|
|
506
|
+
sprintDirectory,
|
|
507
|
+
),
|
|
508
|
+
sentChips,
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
/** The open phases no lane with an agent leads; none while the view shows one lane. */
|
|
513
|
+
const plannedOf = (
|
|
514
|
+
source: Source,
|
|
515
|
+
plans: Record<string, string>,
|
|
516
|
+
results: readonly LaneResult[],
|
|
517
|
+
sprintDirectory: string,
|
|
518
|
+
): PlannedPlan[] =>
|
|
519
|
+
source.options.lane
|
|
520
|
+
? []
|
|
521
|
+
: plannedPlans({
|
|
522
|
+
plans,
|
|
523
|
+
lanes: results.map(({ view }) => ({ lane: view.progress.lane, agent: view.status.agent })),
|
|
524
|
+
sprintDirectory,
|
|
525
|
+
language: source.settings.planLanguage,
|
|
526
|
+
})
|
|
527
|
+
|
|
294
528
|
/**
|
|
295
529
|
* Reads the board, its plans and the lanes' state. `memories` holds, per lane, what the herd saw at the last read; it
|
|
296
530
|
* is how the current phase becomes the one whose boxes moved since, and it is cleaned of lanes the board dropped.
|
|
531
|
+
* `cache`, kept by the caller between reads, spares parsing the session logs and plans that did not change.
|
|
297
532
|
*/
|
|
298
533
|
export const loadSnapshot = async (
|
|
299
534
|
source: Source,
|
|
300
535
|
pullRequests: PullRequestState,
|
|
301
536
|
memories?: Map<string, LaneMemory>,
|
|
537
|
+
cache?: ReadCache,
|
|
302
538
|
): Promise<Snapshot> => {
|
|
303
|
-
const { root, sprintFile, options, settings
|
|
539
|
+
const { root, sprintFile, options, settings } = source
|
|
304
540
|
const fallbacks: Fallbacks = []
|
|
305
|
-
const
|
|
541
|
+
const baseRef = baseRefOf(settings)
|
|
542
|
+
const base = baseReaderOf(root, baseRef)
|
|
306
543
|
const boardPath = boardRepoPathOf(root, settings, sprintFile)
|
|
307
544
|
const board = await readBoardLanes(source, base, boardPath, fallbacks)
|
|
308
545
|
const { markdown } = board
|
|
309
546
|
const sprintDirectory = dirname(boardPath)
|
|
310
|
-
const
|
|
311
|
-
const isWanted = ({ name }: { name: string }) => !options.lane || name === options.lane
|
|
312
|
-
const lanes = board.lanes.filter(isWanted)
|
|
313
|
-
const unlisted = unlistedLanes(
|
|
314
|
-
agents,
|
|
315
|
-
new Set(board.lanes.map((lane) => lane.name)),
|
|
316
|
-
join(root, settings.worktreeDirectory),
|
|
317
|
-
).filter(isWanted)
|
|
318
|
-
const sources = {
|
|
319
|
-
source,
|
|
320
|
-
base,
|
|
321
|
-
sprintDirectory,
|
|
322
|
-
agents,
|
|
323
|
-
pullRequests,
|
|
324
|
-
}
|
|
547
|
+
const worktrees = join(root, settings.worktreeDirectory)
|
|
325
548
|
const now = Date.now()
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
549
|
+
const liveBoard = board.liveMarkdown
|
|
550
|
+
const laneSet = laneSetOf(source, { base, board, sprintDirectory, worktrees, cache }, pullRequests, now)
|
|
551
|
+
const results = await laneResultsOf(laneSet, fallbacks, now, memories)
|
|
552
|
+
const { parsed } = laneSet.sources
|
|
553
|
+
if (parsed) dropUnused(parsed.entries, parsed.used)
|
|
331
554
|
const listedPlans = await readListedPlans(source, base, sprintDirectory, markdown, fallbacks)
|
|
332
555
|
const documentsPlans = Object.assign({}, listedPlans, ...results.map((result) => result.texts))
|
|
333
|
-
const planned = options.lane
|
|
334
|
-
? []
|
|
335
|
-
: plannedPlans({
|
|
336
|
-
plans: documentsPlans,
|
|
337
|
-
lanes: results.map(({ view }) => ({ lane: view.progress.lane, agent: view.status.agent })),
|
|
338
|
-
sprintDirectory,
|
|
339
|
-
language: settings.planLanguage,
|
|
340
|
-
})
|
|
341
556
|
const { ticketSources } = settings
|
|
342
557
|
const watched = ticketSources.length > 0 || results.some(({ view }) => view.pullRequests.length > 0)
|
|
343
558
|
const reading = watched ? readWatchReading(root, now, ticketSources) : null
|
|
344
|
-
const
|
|
345
|
-
const
|
|
559
|
+
const waitingRecords = readWaitingRecords(root)
|
|
560
|
+
const sentChips = sentChipsByLane(waitingRecords.sent, worktrees, now)
|
|
346
561
|
const notice = fallbacks.length > 0 ? fallbackNotice(fallbacks, baseRef, refExists(root, baseRef)) : null
|
|
347
562
|
const unknownLaneTitle =
|
|
348
563
|
options.lane && results.length === 0 ? boardTexts(settings.board.term).laneNotListed(options.lane) : null
|
|
349
564
|
return {
|
|
350
565
|
title: unknownLaneTitle ?? sprintTitleOf(markdown, sprintDirectory),
|
|
351
|
-
views:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
),
|
|
358
|
-
),
|
|
359
|
-
),
|
|
566
|
+
views: decoratedViews(results, {
|
|
567
|
+
reading,
|
|
568
|
+
phaseModels: readPhaseModels(root),
|
|
569
|
+
sprintDirectory,
|
|
570
|
+
sentChips,
|
|
571
|
+
}),
|
|
360
572
|
documents: {
|
|
361
573
|
sprint: markdown,
|
|
362
574
|
plans: documentsPlans,
|
|
363
|
-
laneOrder: [...lanes, ...unlisted].map((lane) => lane.name),
|
|
575
|
+
laneOrder: [...laneSet.lanes, ...laneSet.unlisted].map((lane) => lane.name),
|
|
364
576
|
lanes: board.lanes,
|
|
365
577
|
},
|
|
366
|
-
planned,
|
|
578
|
+
planned: plannedOf(source, documentsPlans, results, sprintDirectory),
|
|
367
579
|
files: [sprintFile, ...results.flatMap((result) => result.files)],
|
|
368
|
-
titles: new Map(
|
|
369
|
-
|
|
580
|
+
titles: new Map(
|
|
581
|
+
results.flatMap(({ view, pane }) =>
|
|
582
|
+
pane ? [[pane, signalledTitle(view.summary?.signalled ?? null, shortTitle(view.progress))]] : [],
|
|
583
|
+
),
|
|
584
|
+
),
|
|
585
|
+
errors: [...laneSet.errors, ...pullRequests.errors, ...(notice ? [notice] : [])],
|
|
370
586
|
watch: reading?.health,
|
|
587
|
+
waiting: waitingItemsOf(liveBoard, settings.planLanguage, waitingRecords, now),
|
|
588
|
+
appointments: appointmentBlock(liveBoard, settings.planLanguage, new Date(now)),
|
|
589
|
+
unplaced: unplacedOf(laneSet.sources, laneSet.lanes, Boolean(options.lane)),
|
|
371
590
|
...(ticketSources.length > 0 ? { watchesTickets: true } : {}),
|
|
372
591
|
}
|
|
373
592
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { tmpdir } from 'node:os'
|
|
3
1
|
import { dirname } from 'node:path'
|
|
4
2
|
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import { ensurePrivateDirectory, writePrivateFile } from '../../kit/hooks/hooks/shared/private-directory'
|
|
4
|
+
import { pageDirectoryOf, systemLocation } from '../shared/private-location'
|
|
5
5
|
import { openInBrowser } from '../terminal/open'
|
|
6
6
|
import { errorText } from './error-text'
|
|
7
7
|
import { baseRefInfo, remoteBlobBase } from './git'
|
|
@@ -11,19 +11,19 @@ import { renderPlanBody, renderSprintHtml } from './sprint-html'
|
|
|
11
11
|
import { buildSprintPage, type SprintPage, sprintPagePath } from './sprint-page'
|
|
12
12
|
import { boardTexts } from './texts'
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
/**
|
|
15
|
+
* What writing and opening the page needs from outside; the defaults are the real ones. `pageDirectory` is the
|
|
16
|
+
* user's own directory the page goes into, verified private before anything is written.
|
|
17
|
+
*/
|
|
18
18
|
export type ExportEnvironment = {
|
|
19
19
|
now: () => Date
|
|
20
|
-
|
|
20
|
+
pageDirectory: string
|
|
21
21
|
open: (target: string) => string | null
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const systemEnvironment = (): ExportEnvironment => ({
|
|
25
25
|
now: () => new Date(),
|
|
26
|
-
|
|
26
|
+
pageDirectory: pageDirectoryOf(systemLocation()),
|
|
27
27
|
open: (target) => openInBrowser(target),
|
|
28
28
|
})
|
|
29
29
|
|
|
@@ -46,10 +46,10 @@ const writePage = (
|
|
|
46
46
|
{ sprintFile, root, settings }: Source,
|
|
47
47
|
environment: ExportEnvironment,
|
|
48
48
|
): WrittenPage | { error: string } => {
|
|
49
|
-
const path = sprintPagePath(environment.
|
|
49
|
+
const path = sprintPagePath(environment.pageDirectory, dirname(boardRepoPathOf(root, settings, sprintFile)))
|
|
50
50
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
ensurePrivateDirectory(environment.pageDirectory)
|
|
52
|
+
writePrivateFile(path, renderSprintHtml(page))
|
|
53
53
|
} catch (error) {
|
|
54
54
|
return { error: errorText(error) }
|
|
55
55
|
}
|
package/src/plans/sprint-page.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { PlanLanguage } from '../vocabulary'
|
|
|
16
16
|
import type { LaneView } from './layout'
|
|
17
17
|
import { extractLinkChips, type LinkChip } from './link-chips'
|
|
18
18
|
import { planAnchorOf } from './markdown-html'
|
|
19
|
+
import { mainPhaseIdOf } from './phase-ids'
|
|
19
20
|
import type { PullRequest } from './pull-requests'
|
|
20
21
|
import type { Snapshot } from './snapshot'
|
|
21
22
|
|
|
@@ -150,14 +151,13 @@ const uniqueId = (base: string, taken: Set<string>): string => {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
const phaseOf = (phase: PhaseProgress, dependencies: Record<string, string>): PagePhase => {
|
|
153
|
-
const numbered = phase.id.replace(/[a-z]$/, '')
|
|
154
154
|
return {
|
|
155
155
|
id: phase.id,
|
|
156
156
|
title: phase.title,
|
|
157
157
|
done: phase.done,
|
|
158
158
|
total: phase.total,
|
|
159
159
|
items: phase.items,
|
|
160
|
-
dependsOn: dependencies[phase.id] ?? dependencies[
|
|
160
|
+
dependsOn: dependencies[phase.id] ?? dependencies[mainPhaseIdOf(phase.id)] ?? null,
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -282,6 +282,6 @@ export const buildSprintPage = (snapshot: Snapshot, context: PageContext): Sprin
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
/** The file the page is written to: `<
|
|
286
|
-
export const sprintPagePath = (
|
|
287
|
-
join(
|
|
285
|
+
/** The file the page is written to: `<page directory>/<board directory>.html`. */
|
|
286
|
+
export const sprintPagePath = (pageDirectory: string, sprintDirectory: string): string =>
|
|
287
|
+
join(pageDirectory, `${posix.basename(sprintDirectory) || 'board'}.html`)
|