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/watch/command.ts
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* `watch.deploy` in the config), new feedback on a lane's open pull request (unless `watch.feedback` is false), a deploy preview for a
|
|
5
5
|
* lane's new head commit (only with `watch.previews`), a new or changed entry of a configured source or its failure (only
|
|
6
6
|
* with `watch.sources`), a gh outage, a lane's or the coordinator's own context fill rising to warn or alert, or the
|
|
7
|
-
* heartbeat. A blocked lane adds one duty line, a merge one per lane, a lane at alert one,
|
|
7
|
+
* heartbeat. A blocked lane adds one duty line, a merge one per lane, a lane at alert one, the main checkout behind
|
|
8
|
+
* `origin/<base>` one in every report and heartbeat until it caught up, and every report ends with one status line
|
|
8
9
|
* per lane and one for the coordinator, in English. When the installed kit changes version
|
|
9
|
-
* it prints `=== kit updated <old> → <new>` and ends
|
|
10
|
+
* it prints `=== kit updated <old> → <new>` and ends, and so does a watcher that runs from a checkout's source when
|
|
11
|
+
* that source changes (`… (source changed)`, the version kept); once a day it asks npm and prints `=== update available <old> → <new>`
|
|
10
12
|
* for a newer release, once per version. The coordinator is the agent in the
|
|
11
13
|
* watcher's own herdr pane unless `--coordinator` names it, else `jack-<repository>` (or the old `orchestrator-<repository>`). A new start replaces a running watcher of the same
|
|
12
14
|
* repository; one in another repository keeps running.
|
|
@@ -26,10 +28,11 @@
|
|
|
26
28
|
* jh watch [--stream] [--heartbeat <minutes>] [--board <board.md>] [--coordinator <name>] [<lane> ...]
|
|
27
29
|
* jh watch source <name|draft.json|-> --dry-run
|
|
28
30
|
*/
|
|
29
|
-
import { readFileSync } from 'node:fs'
|
|
31
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
30
32
|
import { homedir } from 'node:os'
|
|
31
33
|
import { basename, dirname, join, relative } from 'node:path'
|
|
32
|
-
import {
|
|
34
|
+
import { writePrivateFile } from '../../kit/hooks/hooks/shared/private-directory'
|
|
35
|
+
import { readConfig, readResolvedConfig } from '../config/read-config'
|
|
33
36
|
import type { JackherdConfig } from '../config/schema'
|
|
34
37
|
import { resolveCoordinatorName } from '../context/coordinator-name'
|
|
35
38
|
import { coordinatorSettings } from '../context/options'
|
|
@@ -37,17 +40,18 @@ import { contextReport } from '../context/report'
|
|
|
37
40
|
import { agentLog } from '../context/session-log'
|
|
38
41
|
import { type LaneProgress, parseSprintLanes, type SprintLane } from '../plan-format'
|
|
39
42
|
import { terminalSafe } from '../plan-format/terminal-text'
|
|
40
|
-
import { openWaitingItems } from '../plan-format/waiting'
|
|
41
43
|
import { boardFor, foundLanesOf } from '../plans/board-pick'
|
|
42
44
|
import { ensurePlanPanes } from '../plans/ensure-panes'
|
|
43
45
|
import { errorText } from '../plans/error-text'
|
|
44
|
-
import { mainCheckout, worktreeNames, worktreeRoot } from '../plans/git'
|
|
46
|
+
import { baseReaderOf, mainCheckout, worktreeNames, worktreeRoot } from '../plans/git'
|
|
45
47
|
import { discoverLanes, isUnchanged, laneSetChange, laneSetLines } from '../plans/lane-discovery'
|
|
46
|
-
import {
|
|
47
|
-
import { readSentRecords } from '../plans/sent-files-read'
|
|
48
|
+
import { readerOncePerRef } from '../plans/plan-heads'
|
|
48
49
|
import { CLOCK_LOCALE, type PlansSettings, plansSettingsOf, worktreeOf } from '../plans/settings'
|
|
50
|
+
import { readRunningSubagents, subagentSourceOf } from '../plans/subagents-read'
|
|
49
51
|
import { boardTexts } from '../plans/texts'
|
|
52
|
+
import { readWaitingRecords, waitingItemsOf } from '../plans/waiting-records'
|
|
50
53
|
import { defaultUpdateCheck } from '../sync/if-changed'
|
|
54
|
+
import { DEFAULT_SKILL_PREFIX } from '../sync/init'
|
|
51
55
|
import { herdrLaneStatusSource } from '../terminal/herdr'
|
|
52
56
|
import { AGENT_STATUS } from '../terminal/types'
|
|
53
57
|
import { startedKit } from '../version/started-kit'
|
|
@@ -59,6 +63,7 @@ import {
|
|
|
59
63
|
recordAppointments,
|
|
60
64
|
} from './appointments'
|
|
61
65
|
import { type ApprovalRequestFile, approvalEvents, readApprovalRequests, recordApprovals } from './approvals'
|
|
66
|
+
import { askingLanes, questionsFingerprint, recordReportQuestions } from './asking-lanes'
|
|
62
67
|
import {
|
|
63
68
|
type CoordinatorRead,
|
|
64
69
|
contextReadingOf,
|
|
@@ -82,14 +87,14 @@ import {
|
|
|
82
87
|
import { type FeedbackItem } from './feedback-item'
|
|
83
88
|
import { type FeedbackItemsRead, readCommentItems, readFeedbackItems, readOwnLogin } from './feedback-read'
|
|
84
89
|
import { answered, deployTargetOf, type GithubRead, readGithub, targetText } from './github-read'
|
|
85
|
-
import { currentPhaseCountsOf
|
|
90
|
+
import { currentPhaseCountsOf } from './heartbeat-progress'
|
|
86
91
|
import { agentScreen, listAgents } from './herdr'
|
|
87
92
|
import { endedByKitUpdate } from './kit-update'
|
|
88
93
|
import { type LaneReadings, laneFillEvents, recordLaneLevels } from './lane-fill'
|
|
89
94
|
import { laneMessage, rememberedStatusLines, statusBlock } from './lane-message'
|
|
90
95
|
import { type LaneModelReading, laneModelEvents, recordLaneModels } from './lane-model'
|
|
91
96
|
import { laneParallelEvents, recordLaneParallels } from './lane-parallel'
|
|
92
|
-
import { boardCheckoutRows, watchedLaneParallel, watchedLaneProgress } from './lane-progress'
|
|
97
|
+
import { boardCheckoutRows, watchedLaneParallel, watchedLaneProgress, watchedPlanPullRequests } from './lane-progress'
|
|
93
98
|
import {
|
|
94
99
|
appointmentEventLine,
|
|
95
100
|
approvalDutyLines,
|
|
@@ -102,16 +107,18 @@ import {
|
|
|
102
107
|
laneFillDutyLines,
|
|
103
108
|
laneModelDutyLines,
|
|
104
109
|
laneReportLine,
|
|
110
|
+
laneStopHeader,
|
|
105
111
|
mergeDutyLines,
|
|
112
|
+
noTaskSignalDutyLines,
|
|
106
113
|
otherPullRequestsLine,
|
|
107
114
|
parallelDutyLines,
|
|
108
115
|
sourceEventLines,
|
|
109
116
|
sourceFailingStatusLine,
|
|
110
117
|
watchEventLine,
|
|
111
118
|
} from './lane-report'
|
|
112
|
-
import { laneReportPath, readLaneReport, reportMtime } from './lane-report-file'
|
|
119
|
+
import { hasLaneHandover, laneReportPath, readLaneReport, reportMtime } from './lane-report-file'
|
|
113
120
|
import { restNote } from './lane-rest'
|
|
114
|
-
import { branchOf, fetchBaseWhenMoved } from './lane-sources'
|
|
121
|
+
import { branchOf, fetchBaseWhenMoved, fetchPlanHeadsWhenMoved, readMainCheckout } from './lane-sources'
|
|
115
122
|
import {
|
|
116
123
|
advanceBaseline,
|
|
117
124
|
armBaseline,
|
|
@@ -124,6 +131,7 @@ import {
|
|
|
124
131
|
NO_AGENT,
|
|
125
132
|
toSnapshot,
|
|
126
133
|
} from './lane-watch'
|
|
134
|
+
import { mainCheckoutDutyLine } from './main-checkout'
|
|
127
135
|
import { monitorToolAvailable } from './monitor-tool'
|
|
128
136
|
import { parseWatchOptions, type WatchOptions } from './options'
|
|
129
137
|
import {
|
|
@@ -145,8 +153,20 @@ import {
|
|
|
145
153
|
} from './previews'
|
|
146
154
|
import { latestPullRequestOf, type WatchedPullRequest } from './pull-requests'
|
|
147
155
|
import { recordedReadsOf } from './recorded-reads'
|
|
156
|
+
import {
|
|
157
|
+
annotateFirstLine,
|
|
158
|
+
eventReference,
|
|
159
|
+
REFERENCE,
|
|
160
|
+
type Reference,
|
|
161
|
+
referenceHint,
|
|
162
|
+
type SkillCopy,
|
|
163
|
+
skillCopiesFor,
|
|
164
|
+
skillDirectoryFor,
|
|
165
|
+
} from './reference-paths'
|
|
166
|
+
import { heartbeatNotesOf, runningSubagentCountsOf } from './running-subagents'
|
|
148
167
|
import { withSingleReportNotice } from './single-report'
|
|
149
|
-
import {
|
|
168
|
+
import { readOrchestrateSnapshot, skillLineFor } from './skill-change'
|
|
169
|
+
import { runsFromSource, sourceChangeSettled, watchSourceChanges } from './source-change'
|
|
150
170
|
import { sourceDryRun } from './source-dry-run-command'
|
|
151
171
|
import { runSource, SOURCE_LIMITS } from './source-run'
|
|
152
172
|
import { recordWatchedSources } from './source-tickets'
|
|
@@ -159,7 +179,11 @@ import {
|
|
|
159
179
|
type TaskParallelFingerprints,
|
|
160
180
|
taskParallelReport,
|
|
161
181
|
} from './task-parallel'
|
|
182
|
+
import { AUTO_ASK_PATHS, type AutoSettings, autoEndedReport, autoVerdict, localDay } from './task-parallel-auto'
|
|
183
|
+
import { holdsDataPathIn } from './task-parallel-data-paths'
|
|
162
184
|
import { readTaskParallelInputs } from './task-parallel-read'
|
|
185
|
+
import { type NoSignalState, type NoTaskSignalEvent, noTaskSignalEvents } from './task-signal-missing'
|
|
186
|
+
import { signalledLanes } from './task-signal-read'
|
|
163
187
|
import { type WatchTexts, watchTexts } from './texts'
|
|
164
188
|
import { updateAnnouncer } from './update-available'
|
|
165
189
|
import { waitingLine } from './waiting-line'
|
|
@@ -176,20 +200,14 @@ import {
|
|
|
176
200
|
pullRequestEvents,
|
|
177
201
|
recordCoordinatorLevel,
|
|
178
202
|
recordWatchState,
|
|
203
|
+
UPDATE_AVAILABLE_EVENT,
|
|
179
204
|
WATCH_EVENT,
|
|
180
205
|
type WatchEvent,
|
|
181
206
|
type WatchState,
|
|
182
207
|
withReportBaselines,
|
|
183
208
|
} from './watch-events'
|
|
184
|
-
import {
|
|
185
|
-
|
|
186
|
-
releaseLock,
|
|
187
|
-
stopNoticeOf,
|
|
188
|
-
takeOverLock,
|
|
189
|
-
WATCHER_FILES,
|
|
190
|
-
writePrivate,
|
|
191
|
-
} from './watcher-lock'
|
|
192
|
-
import type { DeployConfig } from './workflow-runs'
|
|
209
|
+
import { ensureWatcherDirectory, releaseLock, stopNoticeOf, takeOverLock, WATCHER_FILES } from './watcher-lock'
|
|
210
|
+
import type { DeployConfig, WorkflowRun } from './workflow-runs'
|
|
193
211
|
|
|
194
212
|
type Files = { state: string; lock: string }
|
|
195
213
|
|
|
@@ -218,9 +236,15 @@ type Watch = {
|
|
|
218
236
|
feedback: FeedbackConfig
|
|
219
237
|
checks: ChecksConfig
|
|
220
238
|
parallelTasks: ParallelTasksConfig
|
|
239
|
+
/** `watch.taskSignalMinutes`: how long a lane works without a task signal before `no task signal`. */
|
|
240
|
+
taskSignalMinutes: number
|
|
241
|
+
/** The switch `watch.parallelTasks.auto` and the paths that always ask, both from the main checkout's config. */
|
|
242
|
+
parallelAuto: AutoSettings
|
|
221
243
|
previews: PreviewsConfig
|
|
222
244
|
sources: SourceSettings[]
|
|
223
245
|
texts: WatchTexts
|
|
246
|
+
/** Where the orchestrate skill is installed, the project's copy first; the events name a reference inside the one found. */
|
|
247
|
+
skillCopies: SkillCopy[]
|
|
224
248
|
/** Runs inside herdr with the herdr adapter, so missing herd panes can be brought back. */
|
|
225
249
|
herdr: boolean
|
|
226
250
|
}
|
|
@@ -245,8 +269,6 @@ type Watching = {
|
|
|
245
269
|
state: WatchState
|
|
246
270
|
baseline: LaneSnapshot
|
|
247
271
|
current: LaneSnapshot
|
|
248
|
-
/** The phase count of each lane at the last heartbeat, so a heartbeat names only a count that moved. */
|
|
249
|
-
heartbeatCounts: Record<string, PhaseCount>
|
|
250
272
|
/** The last `herdr agent list` answer, kept for finding the coordinator by its pane. */
|
|
251
273
|
agents: HerdrAgent[]
|
|
252
274
|
github: GithubRead | null
|
|
@@ -255,8 +277,10 @@ type Watching = {
|
|
|
255
277
|
coordinator: Coordinator
|
|
256
278
|
herdrDown: boolean
|
|
257
279
|
reporting: boolean
|
|
258
|
-
/**
|
|
259
|
-
|
|
280
|
+
/** The fetch of the base branch under way, null when none runs; the next one waits for it, a report after a merge too. */
|
|
281
|
+
baseFetch: Promise<void> | null
|
|
282
|
+
/** A fetch of the plan pull request heads is under way; the next one waits for it. */
|
|
283
|
+
fetchingHeads: boolean
|
|
260
284
|
/** The account `gh` runs as, read once it answers; feedback is read only once it is known. */
|
|
261
285
|
ownLogin: string | null
|
|
262
286
|
/** The last feedback read that answered; its targets and items are recorded with every state. */
|
|
@@ -267,8 +291,6 @@ type Watching = {
|
|
|
267
291
|
previewRead: PreviewRead | null
|
|
268
292
|
/** The runs of the sources due at the last GitHub poll; recorded with every state like the feedback read. */
|
|
269
293
|
sourceRuns: SourceRun[]
|
|
270
|
-
/** Source name → when it was last started in this process; a source without an entry runs at the next GitHub poll. */
|
|
271
|
-
sourceRanAt: Record<string, number>
|
|
272
294
|
/** The lanes' fills of the last read; a lane without an entry or with null has no value. */
|
|
273
295
|
laneReadings: LaneReadings
|
|
274
296
|
/** The lanes' models against their board rows at the last read; recorded with every state like the fills. */
|
|
@@ -277,10 +299,16 @@ type Watching = {
|
|
|
277
299
|
laneParallelReadings: Record<string, string[]>
|
|
278
300
|
/** The parallel-task fingerprint of each lane at the last heartbeat; persisted like the lane parallels. */
|
|
279
301
|
taskParallels: TaskParallelFingerprints
|
|
302
|
+
/** The last day of `watch.parallelTasks.auto` whose end was reported; persisted like the fingerprints. */
|
|
303
|
+
parallelAutoEnded: string | null
|
|
304
|
+
/** Since when each lane works without a task signal and the phase last reported; persisted like the fingerprints. */
|
|
305
|
+
noTaskSignals: NoSignalState
|
|
280
306
|
/** The approval requests of the last poll; null before the first read, so the stored ids stay. */
|
|
281
307
|
approvalRequests: ApprovalRequestFile[] | null
|
|
282
308
|
/** The board's appointment table at the last poll and the time it was read at; null before the first read and while the board cannot be read. */
|
|
283
309
|
appointments: { table: AppointmentTable; readAt: number } | null
|
|
310
|
+
/** The questions of each watched lane's report at the last poll (`questionsFingerprint`); null before the first read, so the stored ones stay. */
|
|
311
|
+
reportQuestions: Record<string, string> | null
|
|
284
312
|
planViews: PlanViews
|
|
285
313
|
}
|
|
286
314
|
|
|
@@ -299,6 +327,14 @@ const observationOf = (watching: Watching): Observation => ({
|
|
|
299
327
|
github: answered(watching.github),
|
|
300
328
|
})
|
|
301
329
|
|
|
330
|
+
/** The report questions to remember with the lanes whose report was `shown`; before the first read, the stored ones. */
|
|
331
|
+
const reportQuestionsAfter = (
|
|
332
|
+
watching: Watching,
|
|
333
|
+
remembered: Record<string, string>,
|
|
334
|
+
shown: readonly string[],
|
|
335
|
+
): Record<string, string> =>
|
|
336
|
+
watching.reportQuestions === null ? remembered : recordReportQuestions(remembered, watching.reportQuestions, shown)
|
|
337
|
+
|
|
302
338
|
/** Stamped with the time of the last GitHub read, not of the poll: a run created after it is still unseen. */
|
|
303
339
|
const recordedOf = (watch: Watch, watching: Watching): WatchState => {
|
|
304
340
|
const recorded = recordCoordinatorLevel(
|
|
@@ -317,11 +353,14 @@ const recordedOf = (watch: Watch, watching: Watching): WatchState => {
|
|
|
317
353
|
laneModels: recordLaneModels(withReads.laneModels, watching.laneModelReadings, watching.lanes),
|
|
318
354
|
laneParallels: recordLaneParallels(withReads.laneParallels, watching.laneParallelReadings, watching.lanes),
|
|
319
355
|
taskParallels: watching.taskParallels,
|
|
356
|
+
parallelAutoEnded: watching.parallelAutoEnded,
|
|
357
|
+
noTaskSignals: watching.noTaskSignals,
|
|
320
358
|
approvals: watching.approvalRequests ? recordApprovals(watching.approvalRequests) : withReads.approvals,
|
|
321
359
|
sources: recordWatchedSources(withReads.sources, watching.sourceRuns, watch.sources, watching.githubReadAt),
|
|
322
360
|
appointments: watching.appointments
|
|
323
361
|
? recordAppointments(withReads.appointments, watching.appointments.table, watching.appointments.readAt)
|
|
324
362
|
: withReads.appointments,
|
|
363
|
+
reportQuestions: reportQuestionsAfter(watching, withReads.reportQuestions, []),
|
|
325
364
|
}
|
|
326
365
|
return withReportBaselines(withLevels, watching.lanes, (lane) => reportMtime(reportPathOf(watch, lane)))
|
|
327
366
|
}
|
|
@@ -364,6 +403,12 @@ const restNoteOf = (watch: Watch, lane: string, status: string | undefined): str
|
|
|
364
403
|
? restNote(readLaneReport(reportPathOf(watch, lane))?.text ?? null)
|
|
365
404
|
: null
|
|
366
405
|
|
|
406
|
+
/** The questions of each watched lane's report (`questionsFingerprint`), read at every poll. */
|
|
407
|
+
const readReportQuestions = (watch: Watch, lanes: readonly string[]): Record<string, string> =>
|
|
408
|
+
Object.fromEntries(
|
|
409
|
+
lanes.map((lane) => [lane, questionsFingerprint(readLaneReport(reportPathOf(watch, lane))?.text ?? null)]),
|
|
410
|
+
)
|
|
411
|
+
|
|
367
412
|
/** The lines under a stopped lane's header and its report time to remember (`laneMessage`). */
|
|
368
413
|
const stopMessageOf = (watch: Watch, lane: string, status: string, state: WatchState) =>
|
|
369
414
|
laneMessage({
|
|
@@ -425,6 +470,24 @@ const laneBranchesOf = (watch: Watch, lanes: readonly string[]): Record<string,
|
|
|
425
470
|
}),
|
|
426
471
|
)
|
|
427
472
|
|
|
473
|
+
/** ` · read <path>` of `reference` in the skill copy found now; read per line, so a skill installed meanwhile is found. */
|
|
474
|
+
const hintFor = (watch: Watch, reference: Reference | null): string =>
|
|
475
|
+
reference === null ? '' : referenceHint(reference, skillDirectoryFor(watch.skillCopies, existsSync))
|
|
476
|
+
|
|
477
|
+
/** The lines of one event with its reference hint; a deploy names its target when `watch.deploy` can read it. */
|
|
478
|
+
const linesOfEvent = async (watch: Watch, event: WatchEvent, runs: readonly WorkflowRun[]): Promise<string[]> => {
|
|
479
|
+
const hint = hintFor(watch, eventReference(event))
|
|
480
|
+
if (event.kind === WATCH_EVENT.feedback) return annotateFirstLine(feedbackEventLines(event), hint)
|
|
481
|
+
if (event.kind === WATCH_EVENT.source) return annotateFirstLine(sourceEventLines(event), hint)
|
|
482
|
+
if (isAppointmentEvent(event)) return [`${appointmentEventLine(event)}${hint}`]
|
|
483
|
+
const headSha = event.kind === WATCH_EVENT.deploy ? runs.find((run) => run.id === event.runId)?.headSha : undefined
|
|
484
|
+
const target =
|
|
485
|
+
event.kind === WATCH_EVENT.deploy && headSha && watch.deploy
|
|
486
|
+
? await deployTargetOf(watch.root, watch.deploy, event.runId, headSha)
|
|
487
|
+
: null
|
|
488
|
+
return [`${watchEventLine(event, target ? targetText(target) : null)}${hint}`]
|
|
489
|
+
}
|
|
490
|
+
|
|
428
491
|
const eventLinesOf = async (
|
|
429
492
|
watch: Watch,
|
|
430
493
|
events: readonly WatchEvent[],
|
|
@@ -432,26 +495,7 @@ const eventLinesOf = async (
|
|
|
432
495
|
): Promise<string[]> => {
|
|
433
496
|
const runs = answered(github)?.runs ?? []
|
|
434
497
|
const lines: string[] = []
|
|
435
|
-
for (const event of events)
|
|
436
|
-
if (event.kind === WATCH_EVENT.feedback) {
|
|
437
|
-
lines.push(...feedbackEventLines(event))
|
|
438
|
-
continue
|
|
439
|
-
}
|
|
440
|
-
if (event.kind === WATCH_EVENT.source) {
|
|
441
|
-
lines.push(...sourceEventLines(event))
|
|
442
|
-
continue
|
|
443
|
-
}
|
|
444
|
-
if (isAppointmentEvent(event)) {
|
|
445
|
-
lines.push(appointmentEventLine(event))
|
|
446
|
-
continue
|
|
447
|
-
}
|
|
448
|
-
const headSha = event.kind === WATCH_EVENT.deploy ? runs.find((run) => run.id === event.runId)?.headSha : undefined
|
|
449
|
-
const target =
|
|
450
|
-
event.kind === WATCH_EVENT.deploy && headSha && watch.deploy
|
|
451
|
-
? await deployTargetOf(watch.root, watch.deploy, event.runId, headSha)
|
|
452
|
-
: null
|
|
453
|
-
lines.push(watchEventLine(event, target ? targetText(target) : null))
|
|
454
|
-
}
|
|
498
|
+
for (const event of events) lines.push(...(await linesOfEvent(watch, event, runs)))
|
|
455
499
|
return lines
|
|
456
500
|
}
|
|
457
501
|
|
|
@@ -459,16 +503,16 @@ const eventLinesOf = async (
|
|
|
459
503
|
const boardReadOf = async (
|
|
460
504
|
watch: Watch,
|
|
461
505
|
lanes: readonly string[],
|
|
506
|
+
pullRequests: readonly WatchedPullRequest[],
|
|
462
507
|
): Promise<{ progress: Record<string, LaneProgress>; checkoutRows: number }> => {
|
|
463
508
|
const text = await Bun.file(watch.sprintFile).text()
|
|
464
509
|
const sprintLanes = parseSprintLanes(text, watch.settings.planLanguage)
|
|
465
510
|
const watched = sprintLanes.filter((lane) => lanes.includes(lane.name))
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
)
|
|
511
|
+
const planPulls = watchedPlanPullRequests(pullRequests, watch.settings)
|
|
512
|
+
const headReaderOf = readerOncePerRef((ref) => baseReaderOf(watch.root, ref))
|
|
513
|
+
const progressOf = (lane: SprintLane) =>
|
|
514
|
+
watchedLaneProgress(watch.root, watch.settings, watch.sprintFile, lane, planPulls, headReaderOf)
|
|
515
|
+
const entries = await Promise.all(watched.map(async (lane) => [lane.name, await progressOf(lane)] as const))
|
|
472
516
|
const checkoutRows = boardCheckoutRows(watch.root, watch.settings, watch.sprintFile, text)
|
|
473
517
|
return { progress: Object.fromEntries(entries), checkoutRows }
|
|
474
518
|
}
|
|
@@ -500,9 +544,10 @@ const laneReportOf = (
|
|
|
500
544
|
const boardReadOrNote = async (
|
|
501
545
|
watch: Watch,
|
|
502
546
|
lanes: readonly string[],
|
|
547
|
+
pullRequests: readonly WatchedPullRequest[],
|
|
503
548
|
): Promise<{ progress: Record<string, LaneProgress>; checkoutRows: number; notes: string[] }> => {
|
|
504
549
|
try {
|
|
505
|
-
return { ...(await boardReadOf(watch, lanes)), notes: [] }
|
|
550
|
+
return { ...(await boardReadOf(watch, lanes, pullRequests)), notes: [] }
|
|
506
551
|
} catch (error) {
|
|
507
552
|
return {
|
|
508
553
|
progress: {},
|
|
@@ -524,9 +569,14 @@ const statusLinesOf = async (
|
|
|
524
569
|
const github = answered(watching.github)
|
|
525
570
|
const pullRequests = github?.pullRequests ?? []
|
|
526
571
|
const others = otherPullRequestsLine(pullRequests, github?.own ?? [], watch.texts)
|
|
527
|
-
const { progress, checkoutRows, notes } = await boardReadOrNote(watch, watching.lanes)
|
|
572
|
+
const { progress, checkoutRows, notes } = await boardReadOrNote(watch, watching.lanes, pullRequests)
|
|
573
|
+
const closeLaneHint = hintFor(watch, REFERENCE.closeLane)
|
|
528
574
|
const laneLines = watching.lanes.map((lane) =>
|
|
529
|
-
laneReportLine(
|
|
575
|
+
laneReportLine(
|
|
576
|
+
laneReportOf(watch, lane, watching, state, pullRequests, progress[lane] ?? null),
|
|
577
|
+
watch.texts,
|
|
578
|
+
closeLaneHint,
|
|
579
|
+
),
|
|
530
580
|
)
|
|
531
581
|
const outage = state.ghDownSince === null ? [] : [ghUnavailableStatusLine(clock(state.ghDownSince))]
|
|
532
582
|
const waiting = waitingLineOf(watch)
|
|
@@ -556,7 +606,10 @@ const stoppedLaneSections = (
|
|
|
556
606
|
const status = watching.current[lane]?.status
|
|
557
607
|
if (!status) return { lane, lines: [`=== ${lane}: ${terminalSafe(LANE_GONE)}`], mtime: undefined }
|
|
558
608
|
const message = stopMessageOf(watch, lane, status, next)
|
|
559
|
-
|
|
609
|
+
const handoverHint = hasLaneHandover(worktreeOf(watch.root, watch.settings, lane), lane)
|
|
610
|
+
? hintFor(watch, REFERENCE.laneHandover)
|
|
611
|
+
: null
|
|
612
|
+
return { lane, lines: [laneStopHeader(lane, status, handoverHint), ...message.lines], mtime: message.reportedMtime }
|
|
560
613
|
})
|
|
561
614
|
const shown = sections.flatMap(({ lane, mtime }) => (mtime === undefined ? [] : [[lane, mtime] as const]))
|
|
562
615
|
return {
|
|
@@ -565,6 +618,20 @@ const stoppedLaneSections = (
|
|
|
565
618
|
}
|
|
566
619
|
}
|
|
567
620
|
|
|
621
|
+
/** The duty line while the main checkout lags behind `origin/<base>`, read from the local refs; null when caught up. */
|
|
622
|
+
const mainCheckoutLineOf = (watch: Watch): string | null =>
|
|
623
|
+
mainCheckoutDutyLine(readMainCheckout(watch.root, watch.settings.baseBranch), watch.settings.baseBranch, watch.texts)
|
|
624
|
+
|
|
625
|
+
/** `mainCheckoutLineOf` once a fetch of the base under way has ended when `events` hold a merge, so the merge already counts. */
|
|
626
|
+
const mainCheckoutLineAfter = async (
|
|
627
|
+
watch: Watch,
|
|
628
|
+
watching: Watching,
|
|
629
|
+
events: readonly WatchEvent[],
|
|
630
|
+
): Promise<string | null> => {
|
|
631
|
+
if (events.some((event) => event.kind === WATCH_EVENT.merged)) await watching.baseFetch
|
|
632
|
+
return mainCheckoutLineOf(watch)
|
|
633
|
+
}
|
|
634
|
+
|
|
568
635
|
/**
|
|
569
636
|
* Every line of one return, gathered before any is printed, so the output is never cut halfway by a slow lookup, and
|
|
570
637
|
* the state with what it showed: the report times of the stopped lanes and the status lines. A state without status
|
|
@@ -579,21 +646,26 @@ const reportOf = async (
|
|
|
579
646
|
): Promise<{ lines: string[]; next: WatchState }> => {
|
|
580
647
|
const stops = stoppedLaneSections(watch, watching, stopped, next)
|
|
581
648
|
const eventLines = await eventLinesOf(watch, events, watching.github)
|
|
649
|
+
const mainCheckout = await mainCheckoutLineAfter(watch, watching, events)
|
|
582
650
|
const dutyLines = [
|
|
583
651
|
...blockedDutyLines(stopped, watching.current, watch.texts),
|
|
584
652
|
...mergeDutyLines(events, watching.lanes, watch.texts),
|
|
653
|
+
...(mainCheckout === null ? [] : [mainCheckout]),
|
|
585
654
|
...laneFillDutyLines(events, watch.texts),
|
|
586
655
|
...laneModelDutyLines(events, watch.texts),
|
|
587
656
|
...parallelDutyLines(events, watch.texts),
|
|
657
|
+
...noTaskSignalDutyLines(events, watch.texts),
|
|
588
658
|
...approvalDutyLines(events, watch.texts),
|
|
589
659
|
]
|
|
590
660
|
const status = await statusLinesOf(watch, watching, next)
|
|
591
661
|
const block = statusBlock(status.laneLines, status.coordinator, next.statusLines, status.others)
|
|
662
|
+
const shownReports = stopped.filter((lane) => stops.reportMtimes[lane] !== next.reportMtimes[lane])
|
|
592
663
|
return {
|
|
593
664
|
lines: [...stops.lines, ...eventLines, ...dutyLines, ...block, ...status.rest],
|
|
594
665
|
next: {
|
|
595
666
|
...next,
|
|
596
667
|
reportMtimes: stops.reportMtimes,
|
|
668
|
+
reportQuestions: reportQuestionsAfter(watching, next.reportQuestions, shownReports),
|
|
597
669
|
statusLines: rememberedStatusLines(status.laneLines, status.others),
|
|
598
670
|
},
|
|
599
671
|
}
|
|
@@ -766,12 +838,14 @@ const recordPreviewRead = (watching: Watching, read: PreviewRead | null): WatchE
|
|
|
766
838
|
}
|
|
767
839
|
|
|
768
840
|
/**
|
|
769
|
-
* Runs the sources whose interval has passed
|
|
770
|
-
* run is a result, never a throw, so no
|
|
841
|
+
* Runs the sources whose interval has passed since the start the saved state holds (`ranAt`, so a restart runs none
|
|
842
|
+
* early), all at once, each without a shell and within `SOURCE_LIMITS`; a failed run is a result, never a throw, so no
|
|
843
|
+
* source stops the watcher. The run's start is recorded with the GitHub read's time, which is `now`.
|
|
771
844
|
*/
|
|
772
845
|
const runDueSources = (watch: Watch, watching: Watching, now: number): Promise<SourceRun[]> => {
|
|
773
|
-
const due = watch.sources.filter((source) =>
|
|
774
|
-
|
|
846
|
+
const due = watch.sources.filter((source) =>
|
|
847
|
+
isSourceDue(watching.state.sources[source.name]?.ranAt, source.every, now),
|
|
848
|
+
)
|
|
775
849
|
return Promise.all(due.map((source) => runSource(source, watch.root, SOURCE_LIMITS)))
|
|
776
850
|
}
|
|
777
851
|
|
|
@@ -782,10 +856,18 @@ const recordSourceRuns = (watch: Watch, watching: Watching, runs: SourceRun[]):
|
|
|
782
856
|
|
|
783
857
|
/** Starts a fetch of the base branch in the background when none runs; the loop does not wait for it. */
|
|
784
858
|
const syncBase = (watch: Watch, watching: Watching) => {
|
|
785
|
-
if (watching.
|
|
786
|
-
watching.
|
|
787
|
-
|
|
788
|
-
|
|
859
|
+
if (watching.baseFetch) return
|
|
860
|
+
watching.baseFetch = fetchBaseWhenMoved(watch.root, watch.settings.baseBranch).finally(() => {
|
|
861
|
+
watching.baseFetch = null
|
|
862
|
+
})
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** Starts a fetch of the moved plan pull request heads in the background when none runs, like `syncBase`. */
|
|
866
|
+
const syncPlanHeads = (watch: Watch, watching: Watching, pullRequests: readonly WatchedPullRequest[]) => {
|
|
867
|
+
if (watching.fetchingHeads) return
|
|
868
|
+
watching.fetchingHeads = true
|
|
869
|
+
void fetchPlanHeadsWhenMoved(watch.root, watchedPlanPullRequests(pullRequests, watch.settings)).finally(() => {
|
|
870
|
+
watching.fetchingHeads = false
|
|
789
871
|
})
|
|
790
872
|
}
|
|
791
873
|
|
|
@@ -804,6 +886,7 @@ const pollGithubAndCoordinator = async (watch: Watch, watching: Watching, now: n
|
|
|
804
886
|
runDueSources(watch, watching, now),
|
|
805
887
|
])
|
|
806
888
|
const pullRequests = answered(github)?.pullRequests ?? null
|
|
889
|
+
syncPlanHeads(watch, watching, pullRequests ?? [])
|
|
807
890
|
const branches = { ...watching.state.laneBranches, ...laneBranches }
|
|
808
891
|
const failureReadings = await readFailures(watch, watching, github, branches)
|
|
809
892
|
const targets = feedbackTargets(pullRequests, branches, watching.state.feedback)
|
|
@@ -842,9 +925,9 @@ const readAppointments = (watch: Watch, now: number): Watching['appointments'] =
|
|
|
842
925
|
|
|
843
926
|
/** The line of what waits on the user's answer, from the board's own section; null when nothing is open or the board cannot be read. */
|
|
844
927
|
const waitingLineOf = (watch: Watch): string | null => {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
928
|
+
return waitingLine(
|
|
929
|
+
waitingItemsOf(readBoardText(watch), watch.settings.planLanguage, readWaitingRecords(watch.root), Date.now()),
|
|
930
|
+
)
|
|
848
931
|
}
|
|
849
932
|
|
|
850
933
|
/** The board's lanes, null when the board cannot be read. */
|
|
@@ -925,17 +1008,18 @@ const report = async (
|
|
|
925
1008
|
console.log(
|
|
926
1009
|
(watch.options.stream ? lines : withSingleReportNotice(lines, monitorToolAvailable(process.env))).join('\n'),
|
|
927
1010
|
)
|
|
928
|
-
|
|
1011
|
+
writePrivateFile(watch.files.state, JSON.stringify(shown))
|
|
929
1012
|
watching.reporting = false
|
|
930
1013
|
return shown
|
|
931
1014
|
}
|
|
932
1015
|
|
|
933
1016
|
/**
|
|
934
1017
|
* The parallel-task proposals of this heartbeat, one event per open lane whose situation changed since the last
|
|
935
|
-
* heartbeat and that has a proposal,
|
|
936
|
-
* herdr or the board cannot be read: a proposal is a hint, and the next
|
|
1018
|
+
* heartbeat and that has a proposal, marked by `autoVerdict` against the switch, and the fingerprints of every lane
|
|
1019
|
+
* read, kept for the next one. Nothing when herdr or the board cannot be read: a proposal is a hint, and the next
|
|
1020
|
+
* heartbeat reads again.
|
|
937
1021
|
*/
|
|
938
|
-
const
|
|
1022
|
+
const taskParallelProposalsOf = (
|
|
939
1023
|
watch: Watch,
|
|
940
1024
|
watching: Watching,
|
|
941
1025
|
now: number,
|
|
@@ -950,13 +1034,22 @@ const taskParallelEventsOf = (
|
|
|
950
1034
|
now,
|
|
951
1035
|
})
|
|
952
1036
|
const reports = Object.entries(inputs).map(
|
|
953
|
-
([lane, input]) => [lane, taskParallelReport(watching.taskParallels[lane], input)] as const,
|
|
1037
|
+
([lane, input]) => [lane, input, taskParallelReport(watching.taskParallels[lane], input)] as const,
|
|
954
1038
|
)
|
|
1039
|
+
const today = localDay(now)
|
|
955
1040
|
return {
|
|
956
|
-
events: reports.flatMap(([lane, { proposal }]) =>
|
|
957
|
-
proposal
|
|
1041
|
+
events: reports.flatMap(([lane, input, { proposal }]) =>
|
|
1042
|
+
proposal
|
|
1043
|
+
? [
|
|
1044
|
+
{
|
|
1045
|
+
kind: TASK_PARALLEL_EVENT,
|
|
1046
|
+
lane,
|
|
1047
|
+
line: formatTaskParallelLine(lane, proposal, autoVerdict(input, proposal, watch.parallelAuto, today)),
|
|
1048
|
+
},
|
|
1049
|
+
]
|
|
1050
|
+
: [],
|
|
958
1051
|
),
|
|
959
|
-
fingerprints: Object.fromEntries(reports.map(([lane, { fingerprint }]) => [lane, fingerprint])),
|
|
1052
|
+
fingerprints: Object.fromEntries(reports.map(([lane, , { fingerprint }]) => [lane, fingerprint])),
|
|
960
1053
|
}
|
|
961
1054
|
} catch {
|
|
962
1055
|
return { events: [], fingerprints: {} }
|
|
@@ -964,26 +1057,120 @@ const taskParallelEventsOf = (
|
|
|
964
1057
|
}
|
|
965
1058
|
|
|
966
1059
|
/**
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
|
|
1060
|
+
* This heartbeat's parallel-task events: the line that `watch.parallelTasks.auto` ended, once per last day, then the
|
|
1061
|
+
* proposals. Records the fingerprints and the reported end in `watching`.
|
|
1062
|
+
*/
|
|
1063
|
+
const taskParallelEventsOf = (watch: Watch, watching: Watching, now: number): TaskParallelEvent[] => {
|
|
1064
|
+
const { events, fingerprints } = taskParallelProposalsOf(watch, watching, now)
|
|
1065
|
+
const { ended, line } = autoEndedReport(watching.parallelAutoEnded, watch.parallelAuto.auto, localDay(now))
|
|
1066
|
+
watching.taskParallels = { ...watching.taskParallels, ...fingerprints }
|
|
1067
|
+
watching.parallelAutoEnded = ended
|
|
1068
|
+
return line === null ? events : [{ kind: TASK_PARALLEL_EVENT, lane: null, line }, ...events]
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const workingLanes = (watching: Watching): string[] =>
|
|
1072
|
+
watching.lanes.filter((lane) => watching.current[lane]?.status === AGENT_STATUS.working)
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* This heartbeat's `no task signal` events (D112): each watched lane read as working or not, with an open task signal
|
|
1076
|
+
* or not, and its current phase from `progress`; records the clocks in `watching`. Nothing when herdr or the logs
|
|
1077
|
+
* cannot be read: the event is a reminder, and the next heartbeat reads again.
|
|
1078
|
+
*/
|
|
1079
|
+
const noTaskSignalEventsOf = (
|
|
1080
|
+
watch: Watch,
|
|
1081
|
+
watching: Watching,
|
|
1082
|
+
progress: Readonly<Record<string, LaneProgress>>,
|
|
1083
|
+
now: number,
|
|
1084
|
+
): NoTaskSignalEvent[] => {
|
|
1085
|
+
try {
|
|
1086
|
+
const working = new Set(workingLanes(watching))
|
|
1087
|
+
const signalled = signalledLanes(watching.lanes, {
|
|
1088
|
+
root: watch.root,
|
|
1089
|
+
worktreeDirectory: watch.settings.worktreeDirectory,
|
|
1090
|
+
now,
|
|
1091
|
+
})
|
|
1092
|
+
const counts = currentPhaseCountsOf(progress)
|
|
1093
|
+
const readings = Object.fromEntries(
|
|
1094
|
+
watching.lanes.map((lane) => [
|
|
1095
|
+
lane,
|
|
1096
|
+
{ working: working.has(lane), signalled: signalled.has(lane), phaseId: counts[lane]?.id ?? null },
|
|
1097
|
+
]),
|
|
1098
|
+
)
|
|
1099
|
+
const { events, state } = noTaskSignalEvents(watching.noTaskSignals, readings, now, watch.taskSignalMinutes)
|
|
1100
|
+
watching.noTaskSignals = state
|
|
1101
|
+
return events
|
|
1102
|
+
} catch {
|
|
1103
|
+
return []
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/** The progress of the working lanes, with the pull requests of the last GitHub read; nothing while no lane works. */
|
|
1108
|
+
const workingProgressOf = async (watch: Watch, watching: Watching): Promise<Record<string, LaneProgress>> => {
|
|
1109
|
+
const working = workingLanes(watching)
|
|
1110
|
+
if (working.length === 0) return {}
|
|
1111
|
+
const { progress } = await boardReadOrNote(watch, working, answered(watching.github)?.pullRequests ?? [])
|
|
1112
|
+
return progress
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/** The events a heartbeat of a report brings: the parallel-task proposals, then the lanes without a task signal. */
|
|
1116
|
+
const heartbeatEventsOf = async (watch: Watch, watching: Watching, now: number): Promise<WatchEvent[]> => {
|
|
1117
|
+
const progress = await workingProgressOf(watch, watching)
|
|
1118
|
+
return [...taskParallelEventsOf(watch, watching, now), ...noTaskSignalEventsOf(watch, watching, progress, now)]
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* The subagents running in each worktree, from the chain the herd reads for its own view (`readRunningSubagents`). Nothing
|
|
1123
|
+
* when the logs cannot be read: a note is a hint, and the next heartbeat reads again.
|
|
1124
|
+
*/
|
|
1125
|
+
const subagentCountsOf = (watch: Watch, now: number): Record<string, number> => {
|
|
1126
|
+
try {
|
|
1127
|
+
const worktreeDirectory = join(watch.root, watch.settings.worktreeDirectory)
|
|
1128
|
+
const running = readRunningSubagents(
|
|
1129
|
+
subagentSourceOf(watch.root, worktreeDirectory, watch.settings.skillPrefix),
|
|
1130
|
+
now,
|
|
1131
|
+
)
|
|
1132
|
+
return runningSubagentCountsOf(running, worktreeDirectory)
|
|
1133
|
+
} catch {
|
|
1134
|
+
return {}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* The heartbeat of a stream: one line, a working lane with its phase count at every heartbeat and the subagents running
|
|
1140
|
+
* in its worktree. The plans are read only when a lane works; a board that cannot be read leaves the lines without a
|
|
1141
|
+
* count, and the next report names the failure.
|
|
970
1142
|
*/
|
|
971
1143
|
const printStreamHeartbeat = async (watch: Watch, watching: Watching, now: number): Promise<void> => {
|
|
972
|
-
const working =
|
|
973
|
-
const
|
|
1144
|
+
const working = workingLanes(watching)
|
|
1145
|
+
const progress = await workingProgressOf(watch, watching)
|
|
974
1146
|
const counts = currentPhaseCountsOf(progress)
|
|
975
|
-
const notes =
|
|
976
|
-
watching.heartbeatCounts = { ...watching.heartbeatCounts, ...counts }
|
|
1147
|
+
const notes = heartbeatNotesOf(working, counts, working.length > 0 ? subagentCountsOf(watch, now) : {})
|
|
977
1148
|
const rests = Object.fromEntries(
|
|
978
1149
|
watching.lanes.flatMap((lane) => {
|
|
979
1150
|
const rest = restNoteOf(watch, lane, watching.current[lane]?.status)
|
|
980
1151
|
return rest === null ? [] : [[lane, rest]]
|
|
981
1152
|
}),
|
|
982
1153
|
)
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1154
|
+
const standing = [waitingLineOf(watch), mainCheckoutLineOf(watch)].filter((line) => line !== null)
|
|
1155
|
+
console.log(streamHeartbeatLine(clock(now), watching.lanes, watching.current, notes, rests, standing))
|
|
1156
|
+
const events = [
|
|
1157
|
+
...taskParallelEventsOf(watch, watching, now),
|
|
1158
|
+
...noTaskSignalEventsOf(watch, watching, progress, now),
|
|
1159
|
+
]
|
|
1160
|
+
for (const event of events) console.log(`${watchEventLine(event, null)}${hintFor(watch, eventReference(event))}`)
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* The lanes to report as stopped: those that stopped or left since the last poll, and the working lanes whose report
|
|
1165
|
+
* asks something new (`askingLanes`): a lane waiting on its background subagents ends no turn. Takes the departed
|
|
1166
|
+
* lanes and records the questions read.
|
|
1167
|
+
*/
|
|
1168
|
+
const takeStoppedLanes = (watch: Watch, watching: Watching): string[] => {
|
|
1169
|
+
const fired = [...firedLanes(watching.baseline, watching.current, watching.lanes), ...watching.departed]
|
|
1170
|
+
watching.departed = []
|
|
1171
|
+
watching.reportQuestions = readReportQuestions(watch, watching.lanes)
|
|
1172
|
+
const working = workingLanes(watching).filter((lane) => !fired.includes(lane))
|
|
1173
|
+
return [...fired, ...askingLanes(working, watching.reportQuestions, watching.state.reportQuestions)]
|
|
987
1174
|
}
|
|
988
1175
|
|
|
989
1176
|
/**
|
|
@@ -1008,8 +1195,7 @@ const pollOnce = async (watch: Watch, watching: Watching): Promise<boolean> => {
|
|
|
1008
1195
|
...approvalEvents(watching.state.approvals, watching.approvalRequests, now),
|
|
1009
1196
|
...appointmentEvents(watching.state.appointments, watching.appointments?.table ?? null, now),
|
|
1010
1197
|
]
|
|
1011
|
-
const stopped =
|
|
1012
|
-
watching.departed = []
|
|
1198
|
+
const stopped = takeStoppedLanes(watch, watching)
|
|
1013
1199
|
const heartbeat = isHeartbeatDue(watching.quietSince, now, heartbeatMinutes * MINUTE_MS)
|
|
1014
1200
|
let next = recordedOf(watch, watching)
|
|
1015
1201
|
const reported = stopped.length > 0 || polledEvents.length > 0
|
|
@@ -1017,8 +1203,7 @@ const pollOnce = async (watch: Watch, watching: Watching): Promise<boolean> => {
|
|
|
1017
1203
|
next = await report(watch, watching, stopped, polledEvents, next)
|
|
1018
1204
|
watching.baseline = { ...watching.current }
|
|
1019
1205
|
} else if (heartbeat && !stream) {
|
|
1020
|
-
const
|
|
1021
|
-
watching.taskParallels = { ...watching.taskParallels, ...fingerprints }
|
|
1206
|
+
const proposals = await heartbeatEventsOf(watch, watching, now)
|
|
1022
1207
|
next = await report(watch, watching, [], [{ kind: WATCH_EVENT.heartbeat }, ...proposals], {
|
|
1023
1208
|
...recordedOf(watch, watching),
|
|
1024
1209
|
statusLines: null,
|
|
@@ -1028,7 +1213,7 @@ const pollOnce = async (watch: Watch, watching: Watching): Promise<boolean> => {
|
|
|
1028
1213
|
await printStreamHeartbeat(watch, watching, now)
|
|
1029
1214
|
next = recordedOf(watch, watching)
|
|
1030
1215
|
}
|
|
1031
|
-
if (due)
|
|
1216
|
+
if (due) writePrivateFile(watch.files.state, JSON.stringify(next))
|
|
1032
1217
|
watching.baseline = advanceBaseline(watching.baseline, watching.current)
|
|
1033
1218
|
}
|
|
1034
1219
|
watching.state = next
|
|
@@ -1052,31 +1237,33 @@ const initialWatching = (
|
|
|
1052
1237
|
agents: start.agents,
|
|
1053
1238
|
github: null,
|
|
1054
1239
|
laneBranches: {},
|
|
1055
|
-
heartbeatCounts: {},
|
|
1056
1240
|
githubReadAt: 0,
|
|
1057
1241
|
coordinator: { name: watch.options.coordinator, read: null },
|
|
1058
1242
|
herdrDown: false,
|
|
1059
1243
|
reporting: false,
|
|
1060
|
-
|
|
1244
|
+
baseFetch: null,
|
|
1245
|
+
fetchingHeads: false,
|
|
1061
1246
|
ownLogin: null,
|
|
1062
1247
|
feedbackRead: null,
|
|
1063
1248
|
failureReadings: [],
|
|
1064
1249
|
previewRead: null,
|
|
1065
1250
|
sourceRuns: [],
|
|
1066
|
-
sourceRanAt: {},
|
|
1067
1251
|
laneReadings: {},
|
|
1068
1252
|
laneModelReadings: {},
|
|
1069
1253
|
laneParallelReadings: {},
|
|
1070
1254
|
taskParallels: start.state.taskParallels,
|
|
1255
|
+
parallelAutoEnded: start.state.parallelAutoEnded,
|
|
1256
|
+
noTaskSignals: start.state.noTaskSignals,
|
|
1071
1257
|
approvalRequests: null,
|
|
1072
1258
|
appointments: null,
|
|
1259
|
+
reportQuestions: null,
|
|
1073
1260
|
planViews: { failureReported: false },
|
|
1074
1261
|
})
|
|
1075
1262
|
|
|
1076
1263
|
/** Saves the state a stop leaves behind: the one from before an unfinished report or an unread GitHub answer, else the latest. */
|
|
1077
1264
|
const saveState = (watch: Watch, watching: Watching) => {
|
|
1078
1265
|
const unread = watching.reporting || watching.github === null
|
|
1079
|
-
|
|
1266
|
+
writePrivateFile(watch.files.state, JSON.stringify(unread ? watching.state : recordedOf(watch, watching)))
|
|
1080
1267
|
}
|
|
1081
1268
|
|
|
1082
1269
|
const watchLanes = async (watch: Watch) => {
|
|
@@ -1098,14 +1285,25 @@ const watchLanes = async (watch: Watch) => {
|
|
|
1098
1285
|
})
|
|
1099
1286
|
|
|
1100
1287
|
const kit = await startedKit()
|
|
1101
|
-
const skillSnapshot = kit ?
|
|
1288
|
+
const skillSnapshot = kit ? readOrchestrateSnapshot(kit.packageRoot) : null
|
|
1102
1289
|
const skillLine = () => skillLineFor(kit, skillSnapshot, dirname(watch.files.state))
|
|
1103
|
-
const announceUpdate = kit
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1290
|
+
const announceUpdate = kit
|
|
1291
|
+
? updateAnnouncer(homedir(), kit.version, defaultUpdateCheck(), console.log, () =>
|
|
1292
|
+
hintFor(watch, eventReference({ kind: UPDATE_AVAILABLE_EVENT })),
|
|
1293
|
+
)
|
|
1294
|
+
: null
|
|
1295
|
+
const sourceChanges = kit && runsFromSource(kit.packageRoot) ? watchSourceChanges(kit.packageRoot) : null
|
|
1296
|
+
const sourceChanged = () => sourceChangeSettled(sourceChanges?.lastChangeAt() ?? null, Date.now())
|
|
1297
|
+
const effects = { saveState: () => saveState(watch, watching), log: console.log, skillLine, sourceChanged }
|
|
1298
|
+
try {
|
|
1299
|
+
while (await pollOnce(watch, watching)) {
|
|
1300
|
+
await Bun.sleep(POLL_MS)
|
|
1301
|
+
pollLanes(watch, watching)
|
|
1302
|
+
await announceUpdate?.()
|
|
1303
|
+
if (endedByKitUpdate(kit, effects)) return
|
|
1304
|
+
}
|
|
1305
|
+
} finally {
|
|
1306
|
+
sourceChanges?.close()
|
|
1109
1307
|
}
|
|
1110
1308
|
}
|
|
1111
1309
|
|
|
@@ -1114,10 +1312,34 @@ const filesOf = (root: string): Files => {
|
|
|
1114
1312
|
return { state: join(directory, WATCHER_FILES.state), lock: join(directory, WATCHER_FILES.lock) }
|
|
1115
1313
|
}
|
|
1116
1314
|
|
|
1315
|
+
/**
|
|
1316
|
+
* The switch and the paths that always ask from the main checkout's config only, so a lane's worktree can neither switch
|
|
1317
|
+
* `auto` on, shorten the list nor raise the cost limit; off and the kit's paths when that config cannot be read.
|
|
1318
|
+
*/
|
|
1319
|
+
const autoSettingsOf = (root: string, checkoutRoot: string, config: JackherdConfig) => {
|
|
1320
|
+
try {
|
|
1321
|
+
const main = checkoutRoot === root ? config : readConfig([root])
|
|
1322
|
+
return {
|
|
1323
|
+
auto: main.watch.parallelTasks.auto,
|
|
1324
|
+
askPaths: [...AUTO_ASK_PATHS, ...main.guard.alwaysAsk],
|
|
1325
|
+
maxTokens: main.watch.parallelTasks.maxTokens,
|
|
1326
|
+
holdsDataPath: holdsDataPathIn(root),
|
|
1327
|
+
}
|
|
1328
|
+
} catch {
|
|
1329
|
+
return {
|
|
1330
|
+
auto: undefined,
|
|
1331
|
+
askPaths: AUTO_ASK_PATHS,
|
|
1332
|
+
maxTokens: config.watch.parallelTasks.maxTokens,
|
|
1333
|
+
holdsDataPath: holdsDataPathIn(root),
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1117
1338
|
/** The watch as the command line and the config set it up; throws when the config or the sprint is missing. */
|
|
1118
1339
|
const watchOf = (options: WatchOptions): Watch => {
|
|
1119
1340
|
const root = mainCheckout(process.cwd())
|
|
1120
|
-
const
|
|
1341
|
+
const checkoutRoot = worktreeRoot(process.cwd())
|
|
1342
|
+
const { config, notes } = readResolvedConfig([checkoutRoot, root])
|
|
1121
1343
|
for (const note of notes) console.log(note)
|
|
1122
1344
|
const settings = plansSettingsOf(config)
|
|
1123
1345
|
const picked = boardFor(options.boardPath, [root, worktreeRoot(process.cwd())], settings, () =>
|
|
@@ -1134,9 +1356,12 @@ const watchOf = (options: WatchOptions): Watch => {
|
|
|
1134
1356
|
feedback: config.watch.feedback,
|
|
1135
1357
|
checks: config.watch.checks,
|
|
1136
1358
|
parallelTasks: config.watch.parallelTasks,
|
|
1359
|
+
taskSignalMinutes: config.watch.taskSignalMinutes,
|
|
1360
|
+
parallelAuto: autoSettingsOf(root, checkoutRoot, config),
|
|
1137
1361
|
previews: config.watch.previews,
|
|
1138
1362
|
sources: config.watch.sources,
|
|
1139
1363
|
texts: watchTexts(settings.responseLanguage),
|
|
1364
|
+
skillCopies: skillCopiesFor(root, homedir(), config.skillPrefix, DEFAULT_SKILL_PREFIX),
|
|
1140
1365
|
herdr: insideHerdr(settings.adapter, process.env),
|
|
1141
1366
|
}
|
|
1142
1367
|
}
|