jackherd 1.9.0 → 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 +30 -0
- package/INSTALL.md +1 -1
- package/README.md +50 -32
- package/dist/{chunk-MCP4XJQH.js → chunk-5MQBCUQ5.js} +9 -4
- package/dist/config.d.ts +39 -4
- package/dist/config.js +67 -26
- package/dist/plan-format.d.ts +45 -6
- package/dist/plan-format.js +46 -6
- 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 +52 -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/workflow/skills/follow/SKILL.md +1 -8
- package/kit/workflow/skills/implement/SKILL.md +8 -6
- package/kit/workflow/skills/orchestrate/SKILL.md +210 -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 +1 -1
- package/schemas/jackherd.schema.json +59 -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 +1 -0
- package/src/config/auto-window.ts +8 -0
- package/src/config/link-kinds.ts +7 -1
- package/src/config/schema.ts +58 -24
- 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 +8 -0
- package/src/plan-format/dependencies.ts +7 -4
- package/src/plan-format/index.ts +1 -0
- package/src/plan-format/short-names.ts +71 -0
- package/src/plan-format/waiting.ts +7 -2
- 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/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-summary.ts +253 -45
- package/src/plans/layout-phase.ts +29 -7
- package/src/plans/layout-summary.ts +89 -18
- package/src/plans/layout.ts +262 -68
- package/src/plans/link-chips.ts +1 -0
- package/src/plans/live.ts +107 -20
- 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/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/selection.ts +98 -18
- package/src/plans/sent-files-read.ts +6 -15
- package/src/plans/sent-files.ts +7 -14
- package/src/plans/settings.ts +2 -1
- package/src/plans/signalled-phase.ts +42 -0
- package/src/plans/signals-read.ts +12 -4
- package/src/plans/snapshot.ts +85 -62
- 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 +23 -6
- package/src/plans/view.ts +17 -9
- package/src/plans/waiting-click.ts +192 -37
- 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 +8 -3
- package/src/version/skill-diff.ts +46 -0
- package/src/vocabulary/index.ts +16 -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 +13 -7
- package/src/watch/pull-requests.ts +6 -2
- 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/sources.ts +6 -3
- 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/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,14 @@ 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'
|
|
19
20
|
import type { SourceTicket, WatchBadges, WatchHealth } from '../watch/plan-badges'
|
|
20
21
|
import { readWatchReading } from '../watch/plan-badges-read'
|
|
21
22
|
import { assignSignals, laneOfDirectory, type UnplannedTask } from './active-tasks'
|
|
23
|
+
import { type AppointmentBlock, appointmentBlock } from './appointment-rows'
|
|
22
24
|
import { boardRowsOf } from './board-rows'
|
|
23
25
|
import {
|
|
24
26
|
type ActivityTimes,
|
|
@@ -33,7 +35,15 @@ import { activityTextOf, taskSignalsOf } from './current-task'
|
|
|
33
35
|
import { errorText } from './error-text'
|
|
34
36
|
import { type BaseReader, baseReaderOf, gitStatusOf, refExists } from './git'
|
|
35
37
|
import type { PullRequestState } from './github'
|
|
36
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
type Fallbacks,
|
|
40
|
+
laneWorkOf,
|
|
41
|
+
type PlanSources,
|
|
42
|
+
readAtBase,
|
|
43
|
+
readFallback,
|
|
44
|
+
readPlans,
|
|
45
|
+
textAtBase,
|
|
46
|
+
} from './lane-plans'
|
|
37
47
|
import { laneSummary } from './lane-summary'
|
|
38
48
|
import { boardRowTexts, laneTicketsOf, withoutTicketLinks } from './lane-tickets'
|
|
39
49
|
import type { LaneView } from './layout'
|
|
@@ -41,15 +51,19 @@ import { extractLinkChips } from './link-chips'
|
|
|
41
51
|
import type { Options } from './options'
|
|
42
52
|
import { laneModelsOf, type PhaseModelRecord } from './phase-models'
|
|
43
53
|
import { readPhaseModels } from './phase-models-read'
|
|
54
|
+
import { laneHeads, planPullRequests, readerOncePerRef } from './plan-heads'
|
|
44
55
|
import { type PlannedPlan, plannedPlans } from './planned'
|
|
45
|
-
import
|
|
46
|
-
import {
|
|
56
|
+
import type { PullRequest } from './pull-requests'
|
|
57
|
+
import type { ReadCache } from './read-cache'
|
|
58
|
+
import { type SentChip, sentChipsByLane } from './sent-files'
|
|
47
59
|
import { baseRefOf, boardRepoPathOf, type PlansSettings, worktreeOf } from './settings'
|
|
60
|
+
import { signalledTitle } from './signalled-phase'
|
|
48
61
|
import { readAllSignals } from './signals-read'
|
|
49
62
|
import type { SubagentActivity } from './subagents'
|
|
50
|
-
import { readSubagentActivity } from './subagents-read'
|
|
63
|
+
import { readSubagentActivity, subagentSourceOf } from './subagents-read'
|
|
51
64
|
import { boardTexts } from './texts'
|
|
52
65
|
import { type UnlistedLane, unlistedLanes, unlistedSprintLane } from './unlisted-lanes'
|
|
66
|
+
import { readWaitingRecords, waitingItemsOf } from './waiting-records'
|
|
53
67
|
import { withWorkedPhases } from './worked-phases'
|
|
54
68
|
|
|
55
69
|
/** Where the view reads from: the main checkout, the sprint document, the command line and the project settings. */
|
|
@@ -101,6 +115,8 @@ export type Snapshot = {
|
|
|
101
115
|
watchesTickets?: boolean
|
|
102
116
|
/** What waits on the user's answer: the open rows of the board's section for it, from the checkout's board file. */
|
|
103
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
|
|
104
120
|
/** What agents report doing that no lane takes: the coordinator's own, or a lane the board does not list. */
|
|
105
121
|
unplaced?: UnplannedTask[]
|
|
106
122
|
}
|
|
@@ -115,13 +131,19 @@ type LaneSources = {
|
|
|
115
131
|
/** The agents' log reader, asking once per agent however often the read is used in one snapshot. */
|
|
116
132
|
readLog: AgentLogReader
|
|
117
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
|
|
118
138
|
/** What agents report doing, in every lane and outside them. */
|
|
119
139
|
signals: TaskSignal[]
|
|
120
140
|
/** The directory the lanes' worktrees lie in, which tells a signal's lane. */
|
|
121
141
|
worktrees: string
|
|
142
|
+
parsed?: PlanSources['parsed']
|
|
122
143
|
}
|
|
123
144
|
|
|
124
|
-
|
|
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[] }
|
|
125
147
|
|
|
126
148
|
const SHOWN_NAMES = 2
|
|
127
149
|
|
|
@@ -219,12 +241,20 @@ const laneView = async (
|
|
|
219
241
|
modified,
|
|
220
242
|
} = await readPlans(
|
|
221
243
|
lane,
|
|
222
|
-
{
|
|
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
|
+
},
|
|
223
252
|
laneWorkOf(worktree, gitStatus),
|
|
224
253
|
fallbacks,
|
|
225
254
|
)
|
|
226
255
|
const shown = withWorkedPhases(lane, phasesByHref)
|
|
227
256
|
const { progress, activity } = progressWithMemory(shown, phasesByHref, memories, { now, modified })
|
|
257
|
+
const laneSubagents = sources.subagents.get(lane.name)
|
|
228
258
|
const view: LaneView = {
|
|
229
259
|
progress,
|
|
230
260
|
...running,
|
|
@@ -238,6 +268,11 @@ const laneView = async (
|
|
|
238
268
|
activity,
|
|
239
269
|
guesses: taskSignalsOf(Object.values(phasesByHref).flat(), workText(sources, lane.name, agent), now),
|
|
240
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,
|
|
241
276
|
},
|
|
242
277
|
now,
|
|
243
278
|
),
|
|
@@ -289,12 +324,17 @@ const withTickets = (result: LaneResult, tickets: readonly SourceTicket[]): Lane
|
|
|
289
324
|
}
|
|
290
325
|
|
|
291
326
|
/** What agents report doing; nothing when it cannot be read. */
|
|
292
|
-
const readSignals = (
|
|
327
|
+
const readSignals = (
|
|
328
|
+
{ root, readSignals: read }: Source,
|
|
329
|
+
worktrees: string,
|
|
330
|
+
now: number,
|
|
331
|
+
cache?: ReadCache,
|
|
332
|
+
): TaskSignal[] => {
|
|
293
333
|
try {
|
|
294
334
|
return (
|
|
295
335
|
read ??
|
|
296
336
|
((main, directory, at) =>
|
|
297
|
-
readAllSignals({ projects: defaultProjectsDirectory(), root: main, worktrees: directory }, at))
|
|
337
|
+
readAllSignals({ projects: defaultProjectsDirectory(), root: main, worktrees: directory }, at, cache?.signals))
|
|
298
338
|
)(root, worktrees, now)
|
|
299
339
|
} catch {
|
|
300
340
|
return []
|
|
@@ -322,46 +362,19 @@ const readAgents = (terminal: LaneStatusSource): { agents: LaneAgent[]; errors:
|
|
|
322
362
|
}
|
|
323
363
|
|
|
324
364
|
/** What subagents of the coordinator do in the worktrees; nothing when their logs cannot be read. */
|
|
325
|
-
const readSubagents = (
|
|
365
|
+
const readSubagents = (
|
|
366
|
+
{ root, settings, readSubagents: read }: Source,
|
|
367
|
+
worktrees: string,
|
|
368
|
+
): Map<string, SubagentActivity> => {
|
|
326
369
|
try {
|
|
327
370
|
return (
|
|
328
|
-
read ??
|
|
329
|
-
((main, directory) =>
|
|
330
|
-
readSubagentActivity({
|
|
331
|
-
projects: defaultProjectsDirectory(),
|
|
332
|
-
root: main,
|
|
333
|
-
approvalsRoot: main,
|
|
334
|
-
worktreeDirectory: directory,
|
|
335
|
-
}))
|
|
371
|
+
read ?? ((main, directory) => readSubagentActivity(subagentSourceOf(main, directory, settings.skillPrefix)))
|
|
336
372
|
)(root, worktrees)
|
|
337
373
|
} catch {
|
|
338
374
|
return new Map()
|
|
339
375
|
}
|
|
340
376
|
}
|
|
341
377
|
|
|
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
378
|
/** The view with the files its session sent to the user, only when there are any, so it otherwise stays as it was. */
|
|
366
379
|
const withSentFiles = (view: LaneView, chips: ReadonlyMap<string | null, SentChip[]>): LaneView => {
|
|
367
380
|
const found = chips.get(view.progress.lane.name)
|
|
@@ -395,8 +408,8 @@ const readBoardLanes = async (
|
|
|
395
408
|
const { sprintFile, settings } = source
|
|
396
409
|
const atBase = textAtBase(base, boardPath)
|
|
397
410
|
const markdown = atBase ?? (await readFallback(boardPath, sprintFile, fallbacks)) ?? ''
|
|
398
|
-
const
|
|
399
|
-
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 }
|
|
400
413
|
}
|
|
401
414
|
|
|
402
415
|
/** The plans the sprint's plans tables name, read at the base ref, by link; lanes read theirs themselves. */
|
|
@@ -429,11 +442,17 @@ const forgetLanes = (memories: Map<string, LaneMemory> | undefined, lanes: reado
|
|
|
429
442
|
|
|
430
443
|
const laneSetOf = (
|
|
431
444
|
source: Source,
|
|
432
|
-
context: {
|
|
445
|
+
context: {
|
|
446
|
+
base: BaseReader
|
|
447
|
+
board: BoardLanes
|
|
448
|
+
sprintDirectory: string
|
|
449
|
+
worktrees: string
|
|
450
|
+
cache?: ReadCache
|
|
451
|
+
},
|
|
433
452
|
pullRequests: PullRequestState,
|
|
434
453
|
now: number,
|
|
435
454
|
): LaneSet => {
|
|
436
|
-
const { base, board, sprintDirectory, worktrees } = context
|
|
455
|
+
const { base, board, sprintDirectory, worktrees, cache } = context
|
|
437
456
|
const { agents, errors } = readAgents(source.terminal)
|
|
438
457
|
const isWanted = ({ name }: { name: string }) => !source.options.lane || name === source.options.lane
|
|
439
458
|
const subagents = readSubagents(source, worktrees)
|
|
@@ -449,8 +468,11 @@ const laneSetOf = (
|
|
|
449
468
|
subagents,
|
|
450
469
|
readLog: onceReader(source.readAgentLog ?? agentLog),
|
|
451
470
|
pullRequests,
|
|
452
|
-
|
|
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),
|
|
453
474
|
worktrees,
|
|
475
|
+
parsed: cache && { entries: cache.plans, used: new Set<string>() },
|
|
454
476
|
},
|
|
455
477
|
errors,
|
|
456
478
|
}
|
|
@@ -487,16 +509,6 @@ const decoratedViews = (
|
|
|
487
509
|
),
|
|
488
510
|
)
|
|
489
511
|
|
|
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
512
|
/** The open phases no lane with an agent leads; none while the view shows one lane. */
|
|
501
513
|
const plannedOf = (
|
|
502
514
|
source: Source,
|
|
@@ -516,11 +528,13 @@ const plannedOf = (
|
|
|
516
528
|
/**
|
|
517
529
|
* Reads the board, its plans and the lanes' state. `memories` holds, per lane, what the herd saw at the last read; it
|
|
518
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.
|
|
519
532
|
*/
|
|
520
533
|
export const loadSnapshot = async (
|
|
521
534
|
source: Source,
|
|
522
535
|
pullRequests: PullRequestState,
|
|
523
536
|
memories?: Map<string, LaneMemory>,
|
|
537
|
+
cache?: ReadCache,
|
|
524
538
|
): Promise<Snapshot> => {
|
|
525
539
|
const { root, sprintFile, options, settings } = source
|
|
526
540
|
const fallbacks: Fallbacks = []
|
|
@@ -532,14 +546,18 @@ export const loadSnapshot = async (
|
|
|
532
546
|
const sprintDirectory = dirname(boardPath)
|
|
533
547
|
const worktrees = join(root, settings.worktreeDirectory)
|
|
534
548
|
const now = Date.now()
|
|
535
|
-
const
|
|
549
|
+
const liveBoard = board.liveMarkdown
|
|
550
|
+
const laneSet = laneSetOf(source, { base, board, sprintDirectory, worktrees, cache }, pullRequests, now)
|
|
536
551
|
const results = await laneResultsOf(laneSet, fallbacks, now, memories)
|
|
552
|
+
const { parsed } = laneSet.sources
|
|
553
|
+
if (parsed) dropUnused(parsed.entries, parsed.used)
|
|
537
554
|
const listedPlans = await readListedPlans(source, base, sprintDirectory, markdown, fallbacks)
|
|
538
555
|
const documentsPlans = Object.assign({}, listedPlans, ...results.map((result) => result.texts))
|
|
539
556
|
const { ticketSources } = settings
|
|
540
557
|
const watched = ticketSources.length > 0 || results.some(({ view }) => view.pullRequests.length > 0)
|
|
541
558
|
const reading = watched ? readWatchReading(root, now, ticketSources) : null
|
|
542
|
-
const
|
|
559
|
+
const waitingRecords = readWaitingRecords(root)
|
|
560
|
+
const sentChips = sentChipsByLane(waitingRecords.sent, worktrees, now)
|
|
543
561
|
const notice = fallbacks.length > 0 ? fallbackNotice(fallbacks, baseRef, refExists(root, baseRef)) : null
|
|
544
562
|
const unknownLaneTitle =
|
|
545
563
|
options.lane && results.length === 0 ? boardTexts(settings.board.term).laneNotListed(options.lane) : null
|
|
@@ -549,7 +567,7 @@ export const loadSnapshot = async (
|
|
|
549
567
|
reading,
|
|
550
568
|
phaseModels: readPhaseModels(root),
|
|
551
569
|
sprintDirectory,
|
|
552
|
-
sentChips
|
|
570
|
+
sentChips,
|
|
553
571
|
}),
|
|
554
572
|
documents: {
|
|
555
573
|
sprint: markdown,
|
|
@@ -559,10 +577,15 @@ export const loadSnapshot = async (
|
|
|
559
577
|
},
|
|
560
578
|
planned: plannedOf(source, documentsPlans, results, sprintDirectory),
|
|
561
579
|
files: [sprintFile, ...results.flatMap((result) => result.files)],
|
|
562
|
-
titles: new Map(
|
|
580
|
+
titles: new Map(
|
|
581
|
+
results.flatMap(({ view, pane }) =>
|
|
582
|
+
pane ? [[pane, signalledTitle(view.summary?.signalled ?? null, shortTitle(view.progress))]] : [],
|
|
583
|
+
),
|
|
584
|
+
),
|
|
563
585
|
errors: [...laneSet.errors, ...pullRequests.errors, ...(notice ? [notice] : [])],
|
|
564
586
|
watch: reading?.health,
|
|
565
|
-
waiting:
|
|
587
|
+
waiting: waitingItemsOf(liveBoard, settings.planLanguage, waitingRecords, now),
|
|
588
|
+
appointments: appointmentBlock(liveBoard, settings.planLanguage, new Date(now)),
|
|
566
589
|
unplaced: unplacedOf(laneSet.sources, laneSet.lanes, Boolean(options.lane)),
|
|
567
590
|
...(ticketSources.length > 0 ? { watchesTickets: true } : {}),
|
|
568
591
|
}
|
|
@@ -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`)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { isPhaseOpen, type PlanPhase } from '../plan-format'
|
|
2
|
+
import { PHASE_ID_PREFIX, spannedPhaseIds } from '../plan-format/lanes'
|
|
3
|
+
import { isNamedBy } from './phase-ids'
|
|
4
|
+
|
|
5
|
+
/** The agent types that review a phase; they sit on its review gate. */
|
|
6
|
+
export const REVIEW_TYPE = {
|
|
7
|
+
uncleBob: 'uncle-bob',
|
|
8
|
+
specReview: 'spec-review',
|
|
9
|
+
securityAudit: 'security-audit',
|
|
10
|
+
review: 'review',
|
|
11
|
+
ive: 'ive',
|
|
12
|
+
jobs: 'jobs',
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
export type ReviewType = (typeof REVIEW_TYPE)[keyof typeof REVIEW_TYPE]
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The phases a running subagent names in its description, its review type (null for no review), and when its log was
|
|
19
|
+
* last written. `ids` is empty only for a review that names no phase: the lane places it (`PhaselessReview`).
|
|
20
|
+
*/
|
|
21
|
+
export type SubagentPhaseRun = { ids: string[]; review: ReviewType | null; at: number }
|
|
22
|
+
|
|
23
|
+
/** A running review whose description names no phase: it sits on the lane's signalled phase, else shows as its description. */
|
|
24
|
+
export type PhaselessReview = { review: ReviewType; at: number; description: string | null }
|
|
25
|
+
|
|
26
|
+
/** What the subagents say about one phase of the lane: the types of the reviews among them, and the newest log write. */
|
|
27
|
+
export type SubagentPhase = { reviews: ReviewType[]; at: number }
|
|
28
|
+
|
|
29
|
+
const isReviewTypeName = (type: string): type is ReviewType => (Object.values(REVIEW_TYPE) as string[]).includes(type)
|
|
30
|
+
|
|
31
|
+
/** `P21`, `P17-P20`, `P17 – 19`, `Phase 21`, `Phase 10c`: the first number, an optional sub-phase letter, an optional end. */
|
|
32
|
+
const PHASE_MENTION = new RegExp(
|
|
33
|
+
`(?:\\b${PHASE_ID_PREFIX}|\\bPhase\\s+)(\\d+)([a-z]?)(?:\\s*[-–—]\\s*(?:${PHASE_ID_PREFIX})?(\\d+)\\b)?`,
|
|
34
|
+
'gi',
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
/** The ids of a range `first`-`last`; a range that runs backwards or is absurdly wide names only its first end. */
|
|
38
|
+
const rangeIds = (first: number, last: number): string[] => {
|
|
39
|
+
const spanned = spannedPhaseIds(first, last)
|
|
40
|
+
return spanned.length > 0 ? spanned : [String(first)]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The ids of the phases a description names, in the order named; a range names every phase between its ends. */
|
|
44
|
+
export const phaseIdsOfDescription = (description: string): string[] =>
|
|
45
|
+
[...description.matchAll(PHASE_MENTION)].flatMap(([, first = '', letter = '', last]) =>
|
|
46
|
+
last === undefined ? [`${Number(first)}${letter.toLowerCase()}`] : rangeIds(Number(first), Number(last)),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const withoutPrefix = (type: string, prefixes: readonly string[]): string => {
|
|
50
|
+
const prefix = prefixes.find((each) => each !== '' && type.startsWith(each))
|
|
51
|
+
return prefix === undefined ? type : type.slice(prefix.length)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The `REVIEW_TYPE` an agent type is, plain or with one of the skill `prefixes` of a copy; null for any other type. */
|
|
55
|
+
export const reviewTypeOf = (type: string | null, prefixes: readonly string[]): ReviewType | null => {
|
|
56
|
+
const plain = type === null ? null : withoutPrefix(type, prefixes)
|
|
57
|
+
return plain !== null && isReviewTypeName(plain) ? plain : null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The run of a subagent from its `description` and `agentType`: null when the description names no phase and the
|
|
62
|
+
* subagent is no review. The description is read whole, not cut for the display.
|
|
63
|
+
*/
|
|
64
|
+
export const subagentPhaseRunOf = (
|
|
65
|
+
description: string | null,
|
|
66
|
+
agentType: string | null,
|
|
67
|
+
at: number,
|
|
68
|
+
reviewPrefixes: readonly string[],
|
|
69
|
+
): SubagentPhaseRun | null => {
|
|
70
|
+
const ids = description === null ? [] : phaseIdsOfDescription(description)
|
|
71
|
+
const review = reviewTypeOf(agentType, reviewPrefixes)
|
|
72
|
+
return ids.length === 0 && review === null ? null : { ids, review, at }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const names = (run: SubagentPhaseRun, id: string): boolean => isNamedBy(id, new Set(run.ids))
|
|
76
|
+
|
|
77
|
+
const reviewsOf = (runs: readonly SubagentPhaseRun[]): ReviewType[] => [
|
|
78
|
+
...new Set(runs.flatMap(({ review }) => (review === null ? [] : [review]))),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The open phases of the lane that running subagents name, by phase id: a sub-phase (`21b`) is named by its main phase.
|
|
83
|
+
* Phases the lane does not hold are left out, since another lane's phases are not its own.
|
|
84
|
+
*/
|
|
85
|
+
export const subagentPhases = (
|
|
86
|
+
phases: readonly Pick<PlanPhase, 'id' | 'items'>[],
|
|
87
|
+
runs: readonly SubagentPhaseRun[],
|
|
88
|
+
): Map<string, SubagentPhase> =>
|
|
89
|
+
new Map(
|
|
90
|
+
phases.filter(isPhaseOpen).flatMap(({ id }) => {
|
|
91
|
+
const naming = runs.filter((run) => names(run, id))
|
|
92
|
+
if (naming.length === 0) return []
|
|
93
|
+
return [[id, { reviews: reviewsOf(naming), at: Math.max(...naming.map(({ at }) => at)) }]]
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ActiveTask } from './active-tasks'
|
|
2
|
+
import type { TaskPhase } from './current-task'
|
|
3
|
+
import { taskNameOf } from './task-name'
|
|
4
|
+
|
|
5
|
+
/** A running subagent that is no review: its description as the herd shows it, and when its log was last written. */
|
|
6
|
+
export type DescribedSubagent = { description: string; at: number }
|
|
7
|
+
|
|
8
|
+
/** Where the subagents sit: their tasks by phase id, and the descriptions placed, which show no line of their own. */
|
|
9
|
+
export type SubagentTaskPlacements = { byPhase: Map<string, ActiveTask[]>; placed: string[] }
|
|
10
|
+
|
|
11
|
+
type NamedTask = { phaseId: string; index: number; name: string }
|
|
12
|
+
|
|
13
|
+
/** A letter or digit right after the name continues a word, so `Probe` does not name `Proben lesen`. */
|
|
14
|
+
const WORD_GOES_ON = /^[\p{L}\p{N}]/u
|
|
15
|
+
|
|
16
|
+
/** The open tasks that carry a short name, the longest names first, so a name that starts another loses to it. */
|
|
17
|
+
const namedOpenTasks = (phases: readonly TaskPhase[]): NamedTask[] =>
|
|
18
|
+
phases
|
|
19
|
+
.flatMap(({ id, items }) =>
|
|
20
|
+
items.flatMap((item, index) => {
|
|
21
|
+
const name = item.done || item.gate ? null : taskNameOf(item.text)
|
|
22
|
+
return name === null ? [] : [{ phaseId: id, index, name }]
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
.sort((first, second) => second.name.length - first.name.length)
|
|
26
|
+
|
|
27
|
+
const startsWithName = (description: string, name: string): boolean =>
|
|
28
|
+
description.toLowerCase().startsWith(name.toLowerCase()) && !WORD_GOES_ON.test(description.slice(name.length))
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The open task each subagent works on, from the short name its description starts with (`/implement` Step 0b, case
|
|
32
|
+
* ignored): it gets its `▶` like a reported task, one per task however many subagents share it. A ticked task and a
|
|
33
|
+
* review gate item take none; a subagent whose description starts with no name stays where it was.
|
|
34
|
+
*/
|
|
35
|
+
export const subagentTaskPlacements = (
|
|
36
|
+
phases: readonly TaskPhase[],
|
|
37
|
+
subagents: readonly DescribedSubagent[],
|
|
38
|
+
): SubagentTaskPlacements => {
|
|
39
|
+
const tasks = namedOpenTasks(phases)
|
|
40
|
+
const byPhase = new Map<string, ActiveTask[]>()
|
|
41
|
+
const placed: string[] = []
|
|
42
|
+
for (const { description, at } of subagents) {
|
|
43
|
+
const task = tasks.find(({ name }) => startsWithName(description, name))
|
|
44
|
+
if (task === undefined) continue
|
|
45
|
+
placed.push(description)
|
|
46
|
+
const known = byPhase.get(task.phaseId) ?? []
|
|
47
|
+
if (known.some(({ index }) => index === task.index)) continue
|
|
48
|
+
byPhase.set(task.phaseId, [...known, { index: task.index, agent: null, guessed: false, startedAt: at }])
|
|
49
|
+
}
|
|
50
|
+
return { byPhase, placed }
|
|
51
|
+
}
|