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,39 +1,51 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isPhaseFinished,
|
|
3
3
|
isPhaseOpen,
|
|
4
|
-
isTickedUnmerged,
|
|
5
4
|
type LaneProgress,
|
|
6
5
|
type PhaseProgress,
|
|
7
6
|
type PlanPhase,
|
|
8
7
|
type PlansByHref,
|
|
9
|
-
parseDependencyTable,
|
|
10
8
|
phaseProgress,
|
|
11
|
-
phaseWaves,
|
|
12
9
|
} from '../plan-format'
|
|
13
|
-
import
|
|
10
|
+
import { MS_PER_MINUTE } from '../plan-format/clock'
|
|
11
|
+
import type { TaskSignal } from '../task-signals/types'
|
|
12
|
+
import { type ActiveTask, assignSignals, freshSignals, type UnplannedTask } from './active-tasks'
|
|
14
13
|
import { type ActivityTimes, activityKey } from './current-phase'
|
|
15
|
-
import { currentTasks, type
|
|
14
|
+
import { currentTasks, type TaskActivity } from './current-task'
|
|
15
|
+
import { isNamedBy } from './phase-ids'
|
|
16
|
+
import { signalledPhase } from './signalled-phase'
|
|
17
|
+
import {
|
|
18
|
+
type PhaselessReview,
|
|
19
|
+
type ReviewType,
|
|
20
|
+
type SubagentPhase,
|
|
21
|
+
type SubagentPhaseRun,
|
|
22
|
+
subagentPhases,
|
|
23
|
+
} from './subagent-phases'
|
|
24
|
+
import { type DescribedSubagent, subagentTaskPlacements } from './subagent-tasks'
|
|
16
25
|
|
|
17
26
|
/**
|
|
18
|
-
* A phase the lane works on now: an open one
|
|
19
|
-
* `stepIndex` is the newest step of the lane that shows it, null for a phase no step shows;
|
|
20
|
-
* minutes since
|
|
27
|
+
* A phase the lane works on now: an open one ticked less than `STALE_ACTIVITY_MINUTES` ago, one an agent reports, or
|
|
28
|
+
* one with a ticked task. `stepIndex` is the newest step of the lane that shows it, null for a phase no step shows;
|
|
29
|
+
* `minutesAgo` the whole minutes since its last tick or report, null when the herd saw neither.
|
|
21
30
|
*/
|
|
22
31
|
export type ActivePhase = {
|
|
23
32
|
phase: PhaseProgress
|
|
24
33
|
stepIndex: number | null
|
|
25
|
-
minutesAgo: number
|
|
26
|
-
/**
|
|
27
|
-
|
|
34
|
+
minutesAgo: number | null
|
|
35
|
+
/**
|
|
36
|
+
* The tasks the phase is on: the ones agents reported, else the one `currentTasks` guesses (marked `guessed`); empty
|
|
37
|
+
* when no task is open.
|
|
38
|
+
*/
|
|
39
|
+
tasks: ActiveTask[]
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
/**
|
|
31
|
-
export type
|
|
42
|
+
/** A phase with the step that shows it (null for none), which keys its row (`phaseKey`). */
|
|
43
|
+
export type PlacedPhase = { phase: PhaseProgress; stepIndex: number | null }
|
|
32
44
|
|
|
33
|
-
/** Open phases
|
|
34
|
-
export type
|
|
45
|
+
/** Open phases that do not run, by number; `planTitle` is null for the lane's current plan, the board's title for another. */
|
|
46
|
+
export type WaitingPhases = { planTitle: string | null; phaseIds: string[] }
|
|
35
47
|
|
|
36
|
-
export type FinishedPhase = { id: string
|
|
48
|
+
export type FinishedPhase = { id: string }
|
|
37
49
|
|
|
38
50
|
/** A phase the lane's board rows name that has nothing to run: `missing` when the plan lacks it, else it has no checkbox. */
|
|
39
51
|
export type UnwrittenPhase = { id: string; missing: boolean }
|
|
@@ -41,39 +53,56 @@ export type UnwrittenPhase = { id: string; missing: boolean }
|
|
|
41
53
|
/** Checkboxes and phases over every plan of the lane, a phase counted when it has a checkbox. */
|
|
42
54
|
export type PlanTally = { done: number; total: number; phasesDone: number; phases: number }
|
|
43
55
|
|
|
44
|
-
/** A lane as the herd sums it up: what runs now, what
|
|
56
|
+
/** A lane as the herd sums it up: what runs now, what waits, what is finished, and how far it is. */
|
|
45
57
|
export type LaneSummary = {
|
|
46
|
-
/** The running phases,
|
|
58
|
+
/** The running phases by number, never by recency, so a new movement reorders nothing. */
|
|
47
59
|
active: ActivePhase[]
|
|
48
60
|
/** The open phase of the current plan moved last, at any age; null while none has a time. */
|
|
49
61
|
latest: PhaseProgress | null
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
/** The open phase the lane's newest report or its branch names (`signalledPhase`); it runs and the lane line names it. */
|
|
63
|
+
signalled: PhaseProgress | null
|
|
64
|
+
/** The open phases of the current plan that do not run, then the open phases of the lane's other plans. */
|
|
65
|
+
waiting: WaitingPhases[]
|
|
66
|
+
/** The phases behind `waiting`, in its order, for the rows `next` opens to. */
|
|
67
|
+
waitingPhases: PlacedPhase[]
|
|
52
68
|
finished: FinishedPhase[]
|
|
69
|
+
/** The phases behind `finished`, for the rows `✓` opens to. */
|
|
70
|
+
finishedPhases: PlacedPhase[]
|
|
53
71
|
unwritten: UnwrittenPhase[]
|
|
54
72
|
tally: PlanTally
|
|
73
|
+
/** What agents report doing that no open task of the plan names. */
|
|
74
|
+
unplanned: UnplannedTask[]
|
|
75
|
+
/** What the running subagents without a phase in their description work on, as they described it. */
|
|
76
|
+
subagentTasks: string[]
|
|
55
77
|
}
|
|
56
78
|
|
|
57
79
|
/**
|
|
58
|
-
* `phases` are the lane's plans as it reads them (the base merged with its worktree)
|
|
59
|
-
*
|
|
80
|
+
* `phases` are the lane's plans as it reads them (the base merged with its worktree); a plan missing from `phases`
|
|
81
|
+
* falls back to the phases the lane's steps show.
|
|
60
82
|
*/
|
|
61
83
|
export type SummaryInput = {
|
|
62
84
|
progress: LaneProgress
|
|
63
85
|
phases: Readonly<PlansByHref>
|
|
64
|
-
texts: Readonly<Record<string, string>>
|
|
65
86
|
activity: ActivityTimes
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
|
|
87
|
+
/** Hints at the task a phase is on, guessed from the words of the lane's agents; the fallback while nothing is reported. */
|
|
88
|
+
guesses?: readonly TaskActivity[]
|
|
89
|
+
/** What the lane's agents report doing (task lists, `jh task`), matched to open tasks of the current plan. */
|
|
90
|
+
reported?: readonly TaskSignal[]
|
|
91
|
+
/** The phases the descriptions of the lane's running subagents name; a signal like a report, never a tick. */
|
|
92
|
+
subagents?: readonly SubagentPhaseRun[]
|
|
93
|
+
/** The descriptions of the running subagents that name no phase; the folded lane shows them as tasks. */
|
|
94
|
+
subagentTasks?: readonly string[]
|
|
95
|
+
/** The running reviews that name no phase; they sit on the `signalled` phase, else show as tasks like `subagentTasks`. */
|
|
96
|
+
phaselessReviews?: readonly PhaselessReview[]
|
|
97
|
+
/** The running subagents that are no review; one whose description starts with a task's short name sits on it (`subagentTaskPlacements`). */
|
|
98
|
+
describedSubagents?: readonly DescribedSubagent[]
|
|
99
|
+
/** The branch checked out in the lane's worktree; a phase it names (`branchPhaseId`) is current while no report names one. */
|
|
100
|
+
branch?: string | null
|
|
70
101
|
}
|
|
71
102
|
|
|
72
|
-
/** Minutes without a tick
|
|
103
|
+
/** Minutes without a tick after which a phase no longer runs, unless an agent reports it. */
|
|
73
104
|
export const STALE_ACTIVITY_MINUTES = 20
|
|
74
105
|
|
|
75
|
-
const MINUTE = 60_000
|
|
76
|
-
|
|
77
106
|
const isWritten = ({ total }: PhaseProgress): boolean => total > 0
|
|
78
107
|
|
|
79
108
|
const planHrefs = ({ steps }: LaneProgress): string[] => [
|
|
@@ -92,9 +121,21 @@ const stepPhases = ({ steps }: LaneProgress, href: string): PhaseProgress[] => {
|
|
|
92
121
|
return shown.filter((phase, index) => shown.findIndex(({ id }) => id === phase.id) === index)
|
|
93
122
|
}
|
|
94
123
|
|
|
124
|
+
/** The ids the lane's steps on `href` name, a row's repeats and the phases it worked on without a row included. */
|
|
125
|
+
const namedIds = ({ steps }: LaneProgress, href: string): Set<string> =>
|
|
126
|
+
new Set(
|
|
127
|
+
steps.flatMap(({ step }) => (step.planHref === href ? [...step.phaseIds, ...(step.namedPhaseIds ?? [])] : [])),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The phases of `href` the lane names, in plan order: another lane's phases of the same plan are not its own, however
|
|
132
|
+
* lately they moved.
|
|
133
|
+
*/
|
|
95
134
|
const phasesOf = (input: SummaryInput, href: string): PhaseProgress[] => {
|
|
96
135
|
const plan: readonly PlanPhase[] | undefined = input.phases[href]
|
|
97
|
-
|
|
136
|
+
if (!plan) return stepPhases(input.progress, href)
|
|
137
|
+
const named = namedIds(input.progress, href)
|
|
138
|
+
return plan.filter(({ id }) => isNamedBy(id, named)).map(({ id }) => phaseProgress(plan, id))
|
|
98
139
|
}
|
|
99
140
|
|
|
100
141
|
const newestStepShowing = ({ steps }: LaneProgress, href: string, id: string): number | null => {
|
|
@@ -106,53 +147,209 @@ const newestStepShowing = ({ steps }: LaneProgress, href: string, id: string): n
|
|
|
106
147
|
|
|
107
148
|
type TimedPhase = { phase: PhaseProgress; minutesAgo: number }
|
|
108
149
|
|
|
150
|
+
type RunningPhase = { phase: PhaseProgress; minutesAgo: number | null }
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A phase whose tasks outside the review gate are partly ticked: work on it has begun and goes on. One with only its
|
|
154
|
+
* review gate open waits for its review, as in `jh check --parallel`.
|
|
155
|
+
*/
|
|
156
|
+
const isStarted = ({ items }: PhaseProgress): boolean => {
|
|
157
|
+
const tasks = items.filter(({ gate }) => !gate)
|
|
158
|
+
return tasks.some(({ done }) => done) && tasks.some(({ done }) => !done)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const minutesSince = (at: number, now: number): number => Math.max(Math.floor((now - at) / MS_PER_MINUTE), 0)
|
|
162
|
+
|
|
109
163
|
/** The open phases with a time of their last movement, the one moved last first; a phase without a time is absent. */
|
|
110
164
|
const timedPhases = (input: SummaryInput, href: string, phases: PhaseProgress[], now: number): TimedPhase[] =>
|
|
111
165
|
phases
|
|
112
166
|
.filter(isPhaseOpen)
|
|
113
167
|
.flatMap((phase) => {
|
|
114
168
|
const at = input.activity[activityKey(href, phase.id)]
|
|
115
|
-
return at === undefined ? [] : [{ phase, minutesAgo:
|
|
169
|
+
return at === undefined ? [] : [{ phase, minutesAgo: minutesSince(at, now) }]
|
|
116
170
|
})
|
|
117
171
|
.sort((first, second) => first.minutesAgo - second.minutesAgo)
|
|
118
172
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return running.map(({ phase, minutesAgo }) => ({
|
|
129
|
-
phase,
|
|
130
|
-
stepIndex: newestStepShowing(input.progress, href, phase.id),
|
|
131
|
-
minutesAgo,
|
|
132
|
-
taskIndex: tasks.get(phase.id) ?? null,
|
|
133
|
-
}))
|
|
173
|
+
const newest = (times: readonly number[]): number | null => (times.length > 0 ? Math.max(...times) : null)
|
|
174
|
+
|
|
175
|
+
const isListed = (listed: readonly RunningPhase[], phase: PhaseProgress): boolean =>
|
|
176
|
+
listed.some((each) => each.phase.id === phase.id)
|
|
177
|
+
|
|
178
|
+
/** What names a phase as running besides its ticks, by phase id: the tasks agents reported and the subagents at work. */
|
|
179
|
+
type PhaseSignals = {
|
|
180
|
+
reported: ReadonlyMap<string, ActiveTask[]>
|
|
181
|
+
subagents: ReadonlyMap<string, SubagentPhase>
|
|
134
182
|
}
|
|
135
183
|
|
|
184
|
+
/** When the newest report or running subagent of a phase began, in milliseconds; null for none. */
|
|
185
|
+
const reportedAt = (id: string, { reported, subagents }: PhaseSignals): number | null => {
|
|
186
|
+
const subagent = subagents.get(id)
|
|
187
|
+
return newest([
|
|
188
|
+
...(reported.get(id) ?? []).flatMap((task) => task.startedAt ?? []),
|
|
189
|
+
...(subagent === undefined ? [] : [subagent.at]),
|
|
190
|
+
])
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The open phases a reported task or a running subagent names that are not `listed` yet, aged from the newest of them. */
|
|
194
|
+
const reportedPhases = (
|
|
195
|
+
phases: PhaseProgress[],
|
|
196
|
+
signals: PhaseSignals,
|
|
197
|
+
listed: readonly RunningPhase[],
|
|
198
|
+
now: number,
|
|
199
|
+
): RunningPhase[] =>
|
|
200
|
+
phases
|
|
201
|
+
.filter(
|
|
202
|
+
(phase) =>
|
|
203
|
+
isPhaseOpen(phase) &&
|
|
204
|
+
(signals.reported.has(phase.id) || signals.subagents.has(phase.id)) &&
|
|
205
|
+
!isListed(listed, phase),
|
|
206
|
+
)
|
|
207
|
+
.map((phase) => {
|
|
208
|
+
const startedAt = reportedAt(phase.id, signals)
|
|
209
|
+
return { phase, minutesAgo: startedAt === null ? 0 : minutesSince(startedAt, now) }
|
|
210
|
+
})
|
|
211
|
+
|
|
136
212
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
213
|
+
* Whether a text names the review type as a word (`/security-audit`, `` `uncle-bob` ``): `review` is not named by
|
|
214
|
+
* `skill-review` or `Review-Gate`. The types hold no character a pattern reads specially.
|
|
139
215
|
*/
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
216
|
+
const namesReview = (text: string, review: ReviewType): boolean =>
|
|
217
|
+
new RegExp(`(?<![\\w-])${review}(?![\\w-])`).test(text)
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The review gate box a running review sits on, which gets its `▶` like a reported task: the open one that names the
|
|
221
|
+
* review, else the first open one. A phase whose gate is ticked or that has none shows no arrow.
|
|
222
|
+
*/
|
|
223
|
+
const reviewTask = ({ items }: PhaseProgress, review: ReviewType, at: number): ActiveTask[] => {
|
|
224
|
+
const isOpenGate = (item: PhaseProgress['items'][number]): boolean => item.gate === true && !item.done
|
|
225
|
+
const named = items.findIndex((item) => isOpenGate(item) && namesReview(item.text, review))
|
|
226
|
+
const index = named === -1 ? items.findIndex(isOpenGate) : named
|
|
227
|
+
return index === -1 ? [] : [{ index, agent: null, guessed: false, startedAt: at }]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Adds `tasks` to the phase's tasks in `merged`, each task once. */
|
|
231
|
+
const addTasks = (merged: Map<string, ActiveTask[]>, phaseId: string, tasks: readonly ActiveTask[]): void => {
|
|
232
|
+
const known = merged.get(phaseId) ?? []
|
|
233
|
+
merged.set(phaseId, [...known, ...tasks.filter(({ index }) => !known.some((task) => task.index === index))])
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The reported tasks with the gate box each running review of a phase sits on added, unless a report names it already. */
|
|
237
|
+
const withReviewTasks = (
|
|
238
|
+
reported: ReadonlyMap<string, ActiveTask[]>,
|
|
143
239
|
phases: PhaseProgress[],
|
|
144
|
-
|
|
145
|
-
):
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
240
|
+
subagents: ReadonlyMap<string, SubagentPhase>,
|
|
241
|
+
): Map<string, ActiveTask[]> => {
|
|
242
|
+
const merged = new Map(reported)
|
|
243
|
+
for (const phase of phases) {
|
|
244
|
+
const found = subagents.get(phase.id)
|
|
245
|
+
if (!found) continue
|
|
246
|
+
for (const review of found.reviews) addTasks(merged, phase.id, reviewTask(phase, review, found.at))
|
|
247
|
+
}
|
|
248
|
+
return merged
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The reported tasks with the tasks the subagents sit on added (`subagentTaskPlacements`), unless a report names them already. */
|
|
252
|
+
const withTasks = (
|
|
253
|
+
reported: ReadonlyMap<string, ActiveTask[]>,
|
|
254
|
+
added: ReadonlyMap<string, ActiveTask[]>,
|
|
255
|
+
): Map<string, ActiveTask[]> => {
|
|
256
|
+
const merged = new Map(reported)
|
|
257
|
+
for (const [phaseId, tasks] of added) addTasks(merged, phaseId, tasks)
|
|
258
|
+
return merged
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The signalled phase when a review can sit on it: one with an open gate box, else none. */
|
|
262
|
+
const reviewPhaseOf = (signalled: PhaseProgress | null): PhaseProgress | null =>
|
|
263
|
+
signalled?.items.some((item) => item.gate && !item.done) ? signalled : null
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The runs of the lane's subagents, its reviews that name no phase placed on the `signalled` phase while its gate has
|
|
267
|
+
* an open box; otherwise they stay unplaced and show as tasks (`unplacedTasks`), so a running review never vanishes.
|
|
268
|
+
*/
|
|
269
|
+
const placedRuns = (input: SummaryInput, signalled: PhaseProgress | null): SubagentPhaseRun[] => {
|
|
270
|
+
const phase = reviewPhaseOf(signalled)
|
|
271
|
+
return [
|
|
272
|
+
...(input.subagents ?? []),
|
|
273
|
+
...(phase === null
|
|
274
|
+
? []
|
|
275
|
+
: (input.phaselessReviews ?? []).map(({ review, at }) => ({ ids: [phase.id], review, at }))),
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The descriptions of the subagents that name no phase and sit on no task (`placed`), then those of the reviews no
|
|
281
|
+
* signalled phase takes.
|
|
282
|
+
*/
|
|
283
|
+
const unplacedTasks = (input: SummaryInput, signalled: PhaseProgress | null, placed: readonly string[]): string[] => [
|
|
284
|
+
...(input.subagentTasks ?? []).filter((description) => !placed.includes(description)),
|
|
285
|
+
...(reviewPhaseOf(signalled) === null
|
|
286
|
+
? (input.phaselessReviews ?? []).flatMap(({ description }) => (description === null ? [] : [description]))
|
|
287
|
+
: []),
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
/** The open phases with a ticked task (`isStarted`) that are not `listed` yet, aged from their last tick when one is known. */
|
|
291
|
+
const startedPhases = (phases: PhaseProgress[], timed: TimedPhase[], listed: readonly RunningPhase[]): RunningPhase[] =>
|
|
292
|
+
phases
|
|
293
|
+
.filter((phase) => isPhaseOpen(phase) && isStarted(phase) && !isListed(listed, phase))
|
|
294
|
+
.map((phase) => ({ phase, minutesAgo: timed.find((each) => each.phase.id === phase.id)?.minutesAgo ?? null }))
|
|
295
|
+
|
|
296
|
+
/** The signalled phase when no other source lists it yet, aged from its last tick when one is known. */
|
|
297
|
+
const signalledRunning = (
|
|
298
|
+
signalled: PhaseProgress | null,
|
|
299
|
+
timed: TimedPhase[],
|
|
300
|
+
listed: readonly RunningPhase[],
|
|
301
|
+
): RunningPhase[] =>
|
|
302
|
+
signalled && !isListed(listed, signalled)
|
|
303
|
+
? [{ phase: signalled, minutesAgo: timed.find((each) => each.phase.id === signalled.id)?.minutesAgo ?? null }]
|
|
304
|
+
: []
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The phases that run: the timed ones ticked less than `STALE_ACTIVITY_MINUTES` ago, the phases a reported task or a
|
|
308
|
+
* running subagent names, the signalled phase (`signalledPhase`), then every other open phase with a ticked task
|
|
309
|
+
* (`isStarted`), with the age of its last tick when one is known, else none. A phase only written down, without a
|
|
310
|
+
* tick, a report or the lane's branch naming it, waits.
|
|
311
|
+
*/
|
|
312
|
+
const runningPhases = (
|
|
313
|
+
timed: TimedPhase[],
|
|
314
|
+
phases: PhaseProgress[],
|
|
315
|
+
signals: PhaseSignals & { signalled: PhaseProgress | null },
|
|
316
|
+
now: number,
|
|
317
|
+
): RunningPhase[] => {
|
|
318
|
+
const moved = timed.filter(({ minutesAgo }) => minutesAgo < STALE_ACTIVITY_MINUTES)
|
|
319
|
+
const reported = [...moved, ...reportedPhases(phases, signals, moved, now)]
|
|
320
|
+
const listed = [...reported, ...signalledRunning(signals.signalled, timed, reported)]
|
|
321
|
+
return [...listed, ...startedPhases(phases, timed, listed)]
|
|
154
322
|
}
|
|
155
323
|
|
|
324
|
+
/**
|
|
325
|
+
* The task the herd guesses for each of the `phases` from the words of the lane's agents (`currentTasks`); the caller
|
|
326
|
+
* passes no phase while an agent reports, so a lane with a report guesses nothing.
|
|
327
|
+
*/
|
|
328
|
+
const guessedTasks = (input: SummaryInput, phases: PhaseProgress[], now: number): ReadonlyMap<string, number> =>
|
|
329
|
+
currentTasks(phases, input.guesses ?? [], now, STALE_ACTIVITY_MINUTES * MS_PER_MINUTE)
|
|
330
|
+
|
|
331
|
+
/** The running phases, each with the tasks agents reported on it, else its task in `guessed`, marked as guessed. */
|
|
332
|
+
const activeOf = (
|
|
333
|
+
input: SummaryInput,
|
|
334
|
+
href: string,
|
|
335
|
+
running: RunningPhase[],
|
|
336
|
+
reported: ReadonlyMap<string, ActiveTask[]>,
|
|
337
|
+
guessed: ReadonlyMap<string, number>,
|
|
338
|
+
): ActivePhase[] =>
|
|
339
|
+
running
|
|
340
|
+
.map(({ phase, minutesAgo }) => {
|
|
341
|
+
const guess = guessed.get(phase.id)
|
|
342
|
+
return {
|
|
343
|
+
phase,
|
|
344
|
+
stepIndex: newestStepShowing(input.progress, href, phase.id),
|
|
345
|
+
minutesAgo,
|
|
346
|
+
tasks:
|
|
347
|
+
reported.get(phase.id) ??
|
|
348
|
+
(guess === undefined ? [] : [{ index: guess, agent: null, guessed: true, startedAt: null }]),
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
.sort((first, second) => first.phase.id.localeCompare(second.phase.id, undefined, { numeric: true }))
|
|
352
|
+
|
|
156
353
|
const tallyOf = (phases: PhaseProgress[]): PlanTally => ({
|
|
157
354
|
done: phases.reduce((sum, phase) => sum + phase.done, 0),
|
|
158
355
|
total: phases.reduce((sum, phase) => sum + phase.total, 0),
|
|
@@ -164,59 +361,137 @@ const titleOf = ({ steps }: LaneProgress, href: string): string =>
|
|
|
164
361
|
steps.find(({ step }) => step.planHref === href)?.step.planTitle ?? href
|
|
165
362
|
|
|
166
363
|
/** The open phases of the lane's other plans, each plan by its title. */
|
|
167
|
-
const otherPlans = (input: SummaryInput, hrefs: string[]):
|
|
364
|
+
const otherPlans = (input: SummaryInput, hrefs: string[]): WaitingPhases[] =>
|
|
168
365
|
hrefs.flatMap((href) => {
|
|
169
366
|
const open = phasesOf(input, href).filter(isPhaseOpen)
|
|
170
367
|
return open.length > 0 ? [{ planTitle: titleOf(input.progress, href), phaseIds: open.map(({ id }) => id) }] : []
|
|
171
368
|
})
|
|
172
369
|
|
|
173
|
-
|
|
174
|
-
|
|
370
|
+
const byPhaseNumber = (first: PhaseProgress, second: PhaseProgress): number =>
|
|
371
|
+
first.id.localeCompare(second.id, undefined, { numeric: true })
|
|
372
|
+
|
|
373
|
+
/** The open phases of the current plan that do not run, by number. */
|
|
374
|
+
const waitingIn = (phases: PhaseProgress[], running: ReadonlySet<string>): PhaseProgress[] =>
|
|
375
|
+
phases.filter((phase) => isPhaseOpen(phase) && !running.has(phase.id)).sort(byPhaseNumber)
|
|
376
|
+
|
|
377
|
+
const otherHrefs = (input: SummaryInput, href: string): string[] =>
|
|
378
|
+
planHrefs(input.progress).filter((each) => each !== href)
|
|
379
|
+
|
|
380
|
+
/** The open phases of the current plan that do not run, by number, then the open phases of the lane's other plans. */
|
|
381
|
+
const waitingOf = (
|
|
175
382
|
input: SummaryInput,
|
|
176
383
|
href: string,
|
|
177
384
|
phases: PhaseProgress[],
|
|
178
|
-
|
|
179
|
-
):
|
|
180
|
-
const waiting = phases
|
|
181
|
-
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
input,
|
|
186
|
-
planHrefs(input.progress).filter((each) => each !== href),
|
|
187
|
-
)
|
|
188
|
-
return [...(waiting.length > 0 ? [{ planTitle: null, phaseIds: waiting }] : []), ...others]
|
|
385
|
+
running: ReadonlySet<string>,
|
|
386
|
+
): WaitingPhases[] => {
|
|
387
|
+
const waiting = waitingIn(phases, running).map(({ id }) => id)
|
|
388
|
+
return [
|
|
389
|
+
...(waiting.length > 0 ? [{ planTitle: null, phaseIds: waiting }] : []),
|
|
390
|
+
...otherPlans(input, otherHrefs(input, href)),
|
|
391
|
+
]
|
|
189
392
|
}
|
|
190
393
|
|
|
191
|
-
|
|
394
|
+
const placedIn =
|
|
395
|
+
(progress: LaneProgress, href: string) =>
|
|
396
|
+
(phase: PhaseProgress): PlacedPhase => ({ phase, stepIndex: newestStepShowing(progress, href, phase.id) })
|
|
397
|
+
|
|
398
|
+
/** The phases behind `waitingOf`, in its order, each with the step that shows it. */
|
|
399
|
+
const waitingPlaced = (
|
|
400
|
+
input: SummaryInput,
|
|
401
|
+
href: string,
|
|
402
|
+
phases: PhaseProgress[],
|
|
403
|
+
running: ReadonlySet<string>,
|
|
404
|
+
): PlacedPhase[] => [
|
|
405
|
+
...waitingIn(phases, running).map(placedIn(input.progress, href)),
|
|
406
|
+
...otherHrefs(input, href).flatMap((other) =>
|
|
407
|
+
phasesOf(input, other).filter(isPhaseOpen).map(placedIn(input.progress, other)),
|
|
408
|
+
),
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
/** The finished phases of the current plan. */
|
|
192
412
|
const finishedOf = (phases: PhaseProgress[]): FinishedPhase[] =>
|
|
193
|
-
phases.filter(isPhaseFinished).map((phase) => ({ id: phase.id
|
|
413
|
+
phases.filter(isPhaseFinished).map((phase) => ({ id: phase.id }))
|
|
414
|
+
|
|
415
|
+
type Resting = Pick<LaneSummary, 'waiting' | 'waitingPhases' | 'finished' | 'finishedPhases' | 'unwritten'>
|
|
416
|
+
|
|
417
|
+
/** What does not run: the phases that wait or are finished, as ids and as phases to open, and those without a checkbox. */
|
|
418
|
+
const restingOf = (input: SummaryInput, href: string, phases: PhaseProgress[], active: ActivePhase[]): Resting => {
|
|
419
|
+
const running = new Set(active.map(({ phase }) => phase.id))
|
|
420
|
+
return {
|
|
421
|
+
waiting: waitingOf(input, href, phases, running),
|
|
422
|
+
waitingPhases: waitingPlaced(input, href, phases, running),
|
|
423
|
+
finished: finishedOf(phases),
|
|
424
|
+
finishedPhases: phases.filter(isPhaseFinished).map(placedIn(input.progress, href)),
|
|
425
|
+
unwritten: stepPhases(input.progress, href)
|
|
426
|
+
.filter((phase) => !isWritten(phase))
|
|
427
|
+
.map(({ id, title }) => ({ id, missing: title === null })),
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
type ActiveSources = {
|
|
432
|
+
phases: PhaseProgress[]
|
|
433
|
+
timed: TimedPhase[]
|
|
434
|
+
/** The tasks agents reported, by phase id. */
|
|
435
|
+
reported: ReadonlyMap<string, ActiveTask[]>
|
|
436
|
+
/** Whether any report is fresh; the herd guesses no task then. */
|
|
437
|
+
anyFresh: boolean
|
|
438
|
+
signalled: PhaseProgress | null
|
|
439
|
+
/** The runs of the lane's subagents, the reviews that name no phase placed (`placedRuns`). */
|
|
440
|
+
subagents: readonly SubagentPhaseRun[]
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** The running phases of the plan on `href`, each with the tasks reported on it, the review gate or the guessed one. */
|
|
444
|
+
const activePhases = (input: SummaryInput, href: string, sources: ActiveSources, now: number): ActivePhase[] => {
|
|
445
|
+
const { phases, timed, reported, anyFresh, signalled } = sources
|
|
446
|
+
const named = subagentPhases(phases, sources.subagents)
|
|
447
|
+
const running = runningPhases(timed, phases, { reported, subagents: named, signalled }, now)
|
|
448
|
+
const tasks = withReviewTasks(reported, phases, named)
|
|
449
|
+
const guessable = anyFresh ? [] : running.map(({ phase }) => phase).filter(({ id }) => !tasks.has(id))
|
|
450
|
+
return activeOf(input, href, running, tasks, guessedTasks(input, guessable, now))
|
|
451
|
+
}
|
|
194
452
|
|
|
195
453
|
/**
|
|
196
|
-
* The lane summed up over its plans at `now`: the open phases running in its current plan (
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* rows name with nothing to run, and the tally over every plan of the lane. A phase ticked to the end, review gate
|
|
454
|
+
* The lane summed up over its plans at `now`: the open phases running in its current plan (`runningPhases`), the open
|
|
455
|
+
* phase moved last, the open phases that wait, the finished ones, the phases its rows
|
|
456
|
+
* name with nothing to run, and the tally over every plan of the lane. A phase ticked to the end, review gate
|
|
200
457
|
* included, is finished, never running.
|
|
201
458
|
*/
|
|
202
459
|
export const laneSummary = (input: SummaryInput, now: number): LaneSummary => {
|
|
203
460
|
const href = focusHref(input.progress)
|
|
204
461
|
const tally = tallyOf(planHrefs(input.progress).flatMap((each) => phasesOf(input, each)))
|
|
205
|
-
if (href === null)
|
|
462
|
+
if (href === null) {
|
|
463
|
+
const subagentTasks = unplacedTasks(input, null, [])
|
|
464
|
+
return {
|
|
465
|
+
active: [],
|
|
466
|
+
latest: null,
|
|
467
|
+
signalled: null,
|
|
468
|
+
waiting: [],
|
|
469
|
+
waitingPhases: [],
|
|
470
|
+
finished: [],
|
|
471
|
+
finishedPhases: [],
|
|
472
|
+
unwritten: [],
|
|
473
|
+
tally,
|
|
474
|
+
unplanned: [],
|
|
475
|
+
subagentTasks,
|
|
476
|
+
}
|
|
477
|
+
}
|
|
206
478
|
const phases = phasesOf(input, href)
|
|
207
479
|
const timed = timedPhases(input, href, phases, now)
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
const
|
|
480
|
+
const fresh = freshSignals(input.reported ?? [], newest(Object.values(input.activity)))
|
|
481
|
+
const { byPhase, unplanned } = assignSignals(phases, fresh)
|
|
482
|
+
const signalled = signalledPhase(phases, byPhase, input.branch ?? null)
|
|
483
|
+
const subagents = placedRuns(input, signalled)
|
|
484
|
+
const onTasks = subagentTaskPlacements(phases, input.describedSubagents ?? [])
|
|
485
|
+
const reported = withTasks(byPhase, onTasks.byPhase)
|
|
486
|
+
const sources = { phases, timed, reported, anyFresh: fresh.length > 0, signalled, subagents }
|
|
487
|
+
const active = activePhases(input, href, sources, now)
|
|
211
488
|
return {
|
|
212
489
|
active,
|
|
213
490
|
latest: timed[0]?.phase ?? null,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
finished: finishedOf(phases),
|
|
217
|
-
unwritten: stepPhases(input.progress, href)
|
|
218
|
-
.filter((phase) => !isWritten(phase))
|
|
219
|
-
.map(({ id, title }) => ({ id, missing: title === null })),
|
|
491
|
+
signalled,
|
|
492
|
+
...restingOf(input, href, phases, active),
|
|
220
493
|
tally,
|
|
494
|
+
unplanned,
|
|
495
|
+
subagentTasks: unplacedTasks(input, signalled, onTasks.placed),
|
|
221
496
|
}
|
|
222
497
|
}
|
|
@@ -34,6 +34,21 @@ const onceById = (tickets: readonly SourceTicket[]): SourceTicket[] =>
|
|
|
34
34
|
|
|
35
35
|
const badgeOf = ({ lane: _lane, ...badge }: SourceTicket): TicketBadge => badge
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* A pull request title without the lane ticket it starts with (`T-1 …`, `[T-1] …`, `T-1: …`, `T-1 - …`): the ticket
|
|
39
|
+
* stands under the lane's links already. A key inside the title, a longer key or a title that is the key alone stays.
|
|
40
|
+
*/
|
|
41
|
+
export const withoutTicketKey = (title: string, ids: readonly string[]): string => {
|
|
42
|
+
for (const id of ids) {
|
|
43
|
+
const lead = title.startsWith(`[${id}]`) ? `[${id}]` : title.startsWith(id) ? id : ''
|
|
44
|
+
const rest = title.slice(lead.length)
|
|
45
|
+
if (lead === '' || isWordCharacter(rest[0])) continue
|
|
46
|
+
const stripped = rest.replace(/^\s*[:-]?\s+|^\s*:\s*/, '')
|
|
47
|
+
if (stripped !== '' && stripped !== rest) return stripped
|
|
48
|
+
}
|
|
49
|
+
return title
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
/** What the lane's board rows say of it beside its name, for finding ticket ids: the text of each row. */
|
|
38
53
|
export const boardRowTexts = (lane: SprintLane): string[] => lane.steps.map(({ label }) => label)
|
|
39
54
|
|