jackherd 1.7.1 → 1.8.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 +38 -0
- package/INSTALL.md +2 -0
- package/{NOTICE → NOTICE.txt} +1 -1
- package/README.md +94 -1033
- package/assets/icons/bark-dark.svg +1 -1
- package/assets/icons/bark.svg +1 -1
- package/assets/icons/bowl-dark.svg +1 -1
- package/assets/icons/bowl.svg +1 -1
- package/assets/icons/decide-dark.svg +1 -1
- package/assets/icons/decide.svg +1 -1
- package/assets/icons/ear-dark.svg +1 -1
- package/assets/icons/ear.svg +1 -1
- package/assets/icons/lanes-dark.svg +1 -1
- package/assets/icons/lanes.svg +1 -1
- package/assets/icons/plan-dark.svg +1 -1
- package/assets/icons/plan.svg +1 -1
- package/dist/{chunk-OBUWTQ6Z.js → chunk-M6DDZEW4.js} +24 -10
- package/dist/config.d.ts +11 -5
- package/dist/config.js +53 -21
- package/dist/plan-format.d.ts +73 -7
- package/dist/plan-format.js +180 -100
- package/docs/workflow-dark.svg +1 -1
- package/docs/workflow.svg +1 -1
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/approval-gate/herdr-panes.ts +2 -12
- package/kit/hooks/hooks/approval-gate/index.ts +10 -8
- package/kit/hooks/hooks/approval-gate/run.ts +21 -12
- package/kit/hooks/hooks/ask-hint/decision.ts +3 -3
- package/kit/hooks/hooks/git-guard/decision.ts +189 -15
- package/kit/hooks/hooks/git-guard/index.ts +13 -3
- package/kit/hooks/hooks/git-guard/push-target.ts +85 -0
- package/kit/hooks/hooks/git-guard/repository.ts +63 -0
- package/kit/hooks/hooks/merge-guard/check-free.ts +1 -13
- package/kit/hooks/hooks/merge-guard/decision.ts +342 -102
- package/kit/hooks/hooks/merge-guard/index.ts +6 -4
- package/kit/hooks/hooks/merge-guard/plain-path.ts +15 -0
- package/kit/hooks/hooks/merge-guard/plan-approval-record.ts +23 -0
- package/kit/hooks/hooks/merge-guard/plan-approval-settings.ts +142 -0
- package/kit/hooks/hooks/merge-guard/plan-approval-sources.ts +196 -0
- package/kit/hooks/hooks/merge-guard/plan-approval.ts +168 -0
- package/kit/hooks/hooks/merge-guard/plan-growth.ts +40 -0
- package/kit/hooks/hooks/merge-guard/plan-pointers.ts +194 -0
- package/kit/hooks/hooks/merge-guard/pull-request.ts +67 -21
- package/kit/hooks/hooks/merge-guard/run.ts +118 -29
- package/kit/hooks/hooks/merge-guard/settings.ts +8 -8
- package/kit/hooks/hooks/merge-guard/worker.ts +3 -3
- package/kit/hooks/hooks/shared/base-branch.ts +23 -0
- package/kit/hooks/hooks/shared/board-lanes.ts +93 -0
- package/kit/hooks/hooks/shared/command-text-runners.ts +71 -0
- package/kit/hooks/hooks/shared/command.ts +14 -2
- package/kit/hooks/hooks/shared/commit.ts +2 -0
- package/kit/hooks/hooks/shared/dialog-answers.ts +4 -1
- package/kit/hooks/hooks/shared/json.ts +10 -0
- package/kit/hooks/hooks/shared/lane.ts +57 -0
- package/kit/hooks/hooks/shared/once.ts +8 -0
- package/kit/hooks/hooks/shared/own-pane.ts +31 -0
- package/kit/hooks/hooks/shared/pane-agent.ts +24 -0
- package/kit/hooks/hooks/shared/plan-open-phases.ts +114 -0
- package/kit/hooks/hooks/shared/program-position.ts +148 -5
- package/kit/hooks/hooks/shared/shell-aliases.ts +258 -0
- package/kit/hooks/hooks/shared/shell-snapshots.ts +68 -0
- package/kit/hooks/hooks/shared/shell.ts +167 -20
- package/kit/hooks/hooks.json +11 -2
- package/kit/hooks/statusline.sh +29 -16
- package/kit/review/skills/diagnose/SKILL.md +1 -1
- package/kit/review/skills/review/SKILL.md +2 -0
- package/kit/rules/rules/kit/testing.md +2 -5
- package/kit/rules/rules/kit/writing-for-agents.md +3 -1
- package/kit/workflow/agents/spec-review.md +1 -1
- package/kit/workflow/skills/follow/SKILL.md +3 -3
- package/kit/workflow/skills/grill/SKILL.md +1 -1
- package/kit/workflow/skills/implement/SKILL.md +10 -11
- package/kit/workflow/skills/orchestrate/SKILL.md +155 -117
- package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +17 -0
- package/kit/workflow/skills/orchestrate/references/herd.md +27 -0
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +22 -0
- package/kit/workflow/skills/orchestrate/references/sensitive-input.md +23 -0
- package/kit/workflow/skills/plan/SKILL.md +5 -2
- package/kit/workflow/skills/plan/references/template-English.md +6 -0
- package/kit/workflow/skills/plan/references/template-German.md +6 -0
- package/package.json +3 -2
- package/recipes/README.md +21 -0
- package/recipes/github-issues.md +23 -0
- package/recipes/github-projects.md +22 -0
- package/recipes/jira-cli.md +26 -0
- package/recipes/jira-rest.md +24 -0
- package/recipes/linear.md +25 -0
- package/recipes/sentry.md +18 -0
- package/recipes/sources/jira.ts +51 -0
- package/recipes/sources/linear.ts +19 -0
- package/recipes/sources/sentry.ts +12 -0
- package/recipes/sources.md +80 -0
- package/recipes/status-page.md +19 -0
- package/schemas/jackherd.schema.json +42 -18
- package/src/check/command.ts +2 -2
- package/src/check/parallel-report.ts +19 -9
- package/src/cli.ts +1 -0
- package/src/config/context-shares.ts +130 -0
- package/src/config/read-config.ts +12 -4
- package/src/config/schema.ts +75 -20
- package/src/context/auto-compact.ts +68 -0
- package/src/context/command.ts +68 -17
- package/src/context/coordinator-name.ts +27 -9
- package/src/context/index.ts +0 -1
- package/src/context/level.ts +18 -11
- package/src/context/report.ts +6 -3
- package/src/plan-format/dependencies.ts +144 -32
- package/src/plan-format/index.ts +6 -0
- package/src/plan-format/lanes.ts +6 -0
- package/src/plan-format/progress.ts +19 -8
- package/src/plans/command.ts +14 -5
- package/src/plans/current-phase.ts +63 -7
- package/src/plans/current-task.ts +51 -0
- package/src/plans/ensure-panes.ts +25 -8
- package/src/plans/frame.ts +99 -0
- package/src/plans/git.ts +8 -0
- package/src/plans/herd-memory.ts +107 -0
- package/src/plans/lane-plans.ts +5 -3
- package/src/plans/lane-summary.ts +222 -0
- package/src/plans/layout-phase.ts +148 -0
- package/src/plans/layout-summary.ts +140 -0
- package/src/plans/layout.ts +164 -420
- package/src/plans/live.ts +11 -6
- package/src/plans/plan-panes.ts +15 -0
- package/src/plans/planned.ts +9 -42
- package/src/plans/selection.ts +2 -6
- package/src/plans/settings.ts +2 -7
- package/src/plans/snapshot.ts +38 -39
- package/src/plans/symbols.ts +2 -2
- package/src/plans/texts.ts +32 -31
- package/src/plans/view.ts +0 -1
- package/src/plans/worked-phases.ts +2 -2
- package/src/settled/board-row.ts +133 -0
- package/src/settled/check.ts +129 -0
- package/src/settled/command.ts +65 -0
- package/src/settled/evidence-version.ts +14 -0
- package/src/settled/options.ts +69 -0
- package/src/settled/send.ts +27 -0
- package/src/settled/settle.ts +87 -0
- package/src/settled/topics.ts +56 -0
- package/src/sync/render.ts +13 -4
- package/src/sync/sync.ts +25 -5
- package/src/terminal/herdr.ts +46 -10
- package/src/terminal/none.ts +1 -0
- package/src/terminal/types.ts +15 -2
- package/src/vocabulary/index.ts +47 -11
- package/src/watch/appointments.ts +205 -0
- package/src/watch/command.ts +62 -19
- package/src/watch/lane-report.ts +11 -3
- package/src/watch/texts.ts +35 -59
- package/src/watch/watch-events.ts +21 -28
- package/kit/hooks/hooks/approval-gate/lane.ts +0 -14
package/src/watch/command.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
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
7
|
* heartbeat. A blocked lane adds one duty line, a merge one per lane, a lane at alert one, and every report ends with one status line
|
|
8
|
-
* per lane and one for the coordinator, in
|
|
8
|
+
* per lane and one for the coordinator, in English. When the installed kit changes version
|
|
9
9
|
* it prints `=== kit updated <old> → <new>` and ends; once a day it asks npm and prints `=== update available <old> → <new>`
|
|
10
10
|
* for a newer release, once per version. The coordinator is the agent in the
|
|
11
|
-
* watcher's own herdr pane unless `--coordinator` names it, else `orchestrator-<repository
|
|
11
|
+
* 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
12
|
* repository; one in another repository keeps running.
|
|
13
13
|
*
|
|
14
14
|
* Without `--stream` it exits after one report, whose last line says so and points to the stream: that run is the
|
|
@@ -31,7 +31,7 @@ import { homedir } from 'node:os'
|
|
|
31
31
|
import { basename, dirname, join, relative } from 'node:path'
|
|
32
32
|
import { readResolvedConfig } from '../config/read-config'
|
|
33
33
|
import type { JackherdConfig } from '../config/schema'
|
|
34
|
-
import {
|
|
34
|
+
import { resolveCoordinatorName } from '../context/coordinator-name'
|
|
35
35
|
import { coordinatorSettings } from '../context/options'
|
|
36
36
|
import { contextReport } from '../context/report'
|
|
37
37
|
import { agentLog } from '../context/session-log'
|
|
@@ -42,12 +42,19 @@ import { ensurePlanPanes } from '../plans/ensure-panes'
|
|
|
42
42
|
import { errorText } from '../plans/error-text'
|
|
43
43
|
import { mainCheckout, worktreeNames, worktreeRoot } from '../plans/git'
|
|
44
44
|
import { discoverLanes, isUnchanged, laneSetChange, laneSetLines } from '../plans/lane-discovery'
|
|
45
|
-
import {
|
|
45
|
+
import { CLOCK_LOCALE, type PlansSettings, plansSettingsOf, worktreeOf } from '../plans/settings'
|
|
46
46
|
import { boardTexts } from '../plans/texts'
|
|
47
47
|
import { defaultUpdateCheck } from '../sync/if-changed'
|
|
48
48
|
import { herdrLaneStatusSource } from '../terminal/herdr'
|
|
49
49
|
import { AGENT_STATUS } from '../terminal/types'
|
|
50
50
|
import { startedKit } from '../version/started-kit'
|
|
51
|
+
import {
|
|
52
|
+
type AppointmentTable,
|
|
53
|
+
appointmentEvents,
|
|
54
|
+
isAppointmentEvent,
|
|
55
|
+
readAppointmentTable,
|
|
56
|
+
recordAppointments,
|
|
57
|
+
} from './appointments'
|
|
51
58
|
import { type ApprovalRequestFile, approvalEvents, readApprovalRequests, recordApprovals } from './approvals'
|
|
52
59
|
import {
|
|
53
60
|
type CoordinatorRead,
|
|
@@ -81,6 +88,7 @@ import { type LaneModelReading, laneModelEvents, recordLaneModels } from './lane
|
|
|
81
88
|
import { laneParallelEvents, recordLaneParallels } from './lane-parallel'
|
|
82
89
|
import { boardCheckoutRows, watchedLaneParallel, watchedLaneProgress } from './lane-progress'
|
|
83
90
|
import {
|
|
91
|
+
appointmentEventLine,
|
|
84
92
|
approvalDutyLines,
|
|
85
93
|
armLine,
|
|
86
94
|
blockedDutyLines,
|
|
@@ -140,7 +148,7 @@ import { runSource, SOURCE_LIMITS } from './source-run'
|
|
|
140
148
|
import { recordWatchedSources } from './source-tickets'
|
|
141
149
|
import { isSourceDue, type SourceRun, type SourceSettings, type SourceState, sourceEvents } from './sources'
|
|
142
150
|
import { streamHeartbeatLine } from './stream-output'
|
|
143
|
-
import {
|
|
151
|
+
import { type WatchTexts, watchTexts } from './texts'
|
|
144
152
|
import { updateAnnouncer } from './update-available'
|
|
145
153
|
import {
|
|
146
154
|
coordinatorWatchEvent,
|
|
@@ -254,6 +262,8 @@ type Watching = {
|
|
|
254
262
|
laneParallelReadings: Record<string, string[]>
|
|
255
263
|
/** The approval requests of the last poll; null before the first read, so the stored ids stay. */
|
|
256
264
|
approvalRequests: ApprovalRequestFile[] | null
|
|
265
|
+
/** 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. */
|
|
266
|
+
appointments: { table: AppointmentTable; readAt: number } | null
|
|
257
267
|
planViews: PlanViews
|
|
258
268
|
}
|
|
259
269
|
|
|
@@ -262,8 +272,7 @@ const GITHUB_POLL_MS = 60_000
|
|
|
262
272
|
const MINUTE_MS = 60_000
|
|
263
273
|
const USAGE_EXIT = 2
|
|
264
274
|
|
|
265
|
-
const clock = (
|
|
266
|
-
new Date(epochMs).toLocaleTimeString(clockLocaleOf(watch.settings))
|
|
275
|
+
const clock = (epochMs: number): string => new Date(epochMs).toLocaleTimeString(CLOCK_LOCALE)
|
|
267
276
|
|
|
268
277
|
const agentNamesOf = (agents: readonly HerdrAgent[]): string[] => agents.flatMap((agent) => agent.name ?? [])
|
|
269
278
|
|
|
@@ -292,6 +301,9 @@ const recordedOf = (watch: Watch, watching: Watching): WatchState => {
|
|
|
292
301
|
laneParallels: recordLaneParallels(withReads.laneParallels, watching.laneParallelReadings, watching.lanes),
|
|
293
302
|
approvals: watching.approvalRequests ? recordApprovals(watching.approvalRequests) : withReads.approvals,
|
|
294
303
|
sources: recordWatchedSources(withReads.sources, watching.sourceRuns, watch.sources, watching.githubReadAt),
|
|
304
|
+
appointments: watching.appointments
|
|
305
|
+
? recordAppointments(withReads.appointments, watching.appointments.table, watching.appointments.readAt)
|
|
306
|
+
: withReads.appointments,
|
|
295
307
|
}
|
|
296
308
|
return withReportBaselines(withLevels, watching.lanes, (lane) => reportMtime(reportPathOf(watch, lane)))
|
|
297
309
|
}
|
|
@@ -337,11 +349,14 @@ const stopMessageOf = (watch: Watch, lane: string, status: string, state: WatchS
|
|
|
337
349
|
reportedMtime: state.reportMtimes[lane] ?? null,
|
|
338
350
|
})
|
|
339
351
|
|
|
340
|
-
/**
|
|
352
|
+
/**
|
|
353
|
+
* `--coordinator` if given, else the agent in the watcher's own pane, else `jack-<repository>`, or the old
|
|
354
|
+
* `orchestrator-<repository>` while only that one runs.
|
|
355
|
+
*/
|
|
341
356
|
const coordinatorNameFor = (watch: Watch, agents: readonly HerdrAgent[]): string =>
|
|
342
357
|
watch.options.coordinator ??
|
|
343
358
|
coordinatorNameOf(agents, process.env.HERDR_PANE_ID) ??
|
|
344
|
-
|
|
359
|
+
resolveCoordinatorName(basename(watch.root), agentNamesOf(agents))
|
|
345
360
|
|
|
346
361
|
/**
|
|
347
362
|
* One read of the fills: the coordinator's, and each watched lane's (null without a value), with `models`, lane → the
|
|
@@ -402,6 +417,10 @@ const eventLinesOf = async (
|
|
|
402
417
|
lines.push(...sourceEventLines(event))
|
|
403
418
|
continue
|
|
404
419
|
}
|
|
420
|
+
if (isAppointmentEvent(event)) {
|
|
421
|
+
lines.push(appointmentEventLine(event))
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
405
424
|
const headSha = event.kind === WATCH_EVENT.deploy ? runs.find((run) => run.id === event.runId)?.headSha : undefined
|
|
406
425
|
const target =
|
|
407
426
|
event.kind === WATCH_EVENT.deploy && headSha && watch.deploy
|
|
@@ -483,20 +502,20 @@ const statusLinesOf = async (
|
|
|
483
502
|
const laneLines = watching.lanes.map((lane) =>
|
|
484
503
|
laneReportLine(laneReportOf(lane, watching, state, pullRequests, progress[lane] ?? null), watch.texts),
|
|
485
504
|
)
|
|
486
|
-
const outage = state.ghDownSince === null ? [] : [ghUnavailableStatusLine(clock(
|
|
505
|
+
const outage = state.ghDownSince === null ? [] : [ghUnavailableStatusLine(clock(state.ghDownSince))]
|
|
487
506
|
const board = { term: watch.settings.board.term, checkoutRows }
|
|
488
507
|
const coordinator = coordinatorStatusLine(watching.coordinator.name, watching.coordinator.read, watch.texts, board)
|
|
489
508
|
return {
|
|
490
509
|
laneLines,
|
|
491
510
|
coordinator,
|
|
492
511
|
others: others === null ? [] : [others],
|
|
493
|
-
rest: [...notes, ...outage, ...failingSourceLines(
|
|
512
|
+
rest: [...notes, ...outage, ...failingSourceLines(state.sources)],
|
|
494
513
|
}
|
|
495
514
|
}
|
|
496
515
|
|
|
497
|
-
const failingSourceLines = (
|
|
516
|
+
const failingSourceLines = (sources: SourceState): string[] =>
|
|
498
517
|
Object.entries(sources).flatMap(([name, { failingSince }]) =>
|
|
499
|
-
failingSince === null ? [] : [sourceFailingStatusLine(name, clock(
|
|
518
|
+
failingSince === null ? [] : [sourceFailingStatusLine(name, clock(failingSince))],
|
|
500
519
|
)
|
|
501
520
|
|
|
502
521
|
/** The section of each stopped lane (its header and message) and the report times it leaves behind. */
|
|
@@ -775,15 +794,31 @@ const pollGithubAndCoordinator = async (watch: Watch, watching: Watching, now: n
|
|
|
775
794
|
]
|
|
776
795
|
}
|
|
777
796
|
|
|
778
|
-
/** The board's
|
|
779
|
-
const
|
|
797
|
+
/** The board's text, null when it cannot be read; parsing it is the caller's, so a parser error is not taken for an unreadable board. */
|
|
798
|
+
const readBoardText = (watch: Watch): string | null => {
|
|
780
799
|
try {
|
|
781
|
-
return
|
|
800
|
+
return readFileSync(watch.sprintFile, 'utf8')
|
|
782
801
|
} catch {
|
|
783
802
|
return null
|
|
784
803
|
}
|
|
785
804
|
}
|
|
786
805
|
|
|
806
|
+
/**
|
|
807
|
+
* The board's appointment table at `now`, in the plan language, and when it was read; null when the board cannot be
|
|
808
|
+
* read, so the reported appointments stay as they were.
|
|
809
|
+
*/
|
|
810
|
+
const readAppointments = (watch: Watch, now: number): Watching['appointments'] => {
|
|
811
|
+
const board = readBoardText(watch)
|
|
812
|
+
if (board === null) return null
|
|
813
|
+
return { table: readAppointmentTable(board, watch.settings.planLanguage, new Date(now)), readAt: now }
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** The board's lanes, null when the board cannot be read. */
|
|
817
|
+
const readSprintLanes = (watch: Watch): SprintLane[] | null => {
|
|
818
|
+
const board = readBoardText(watch)
|
|
819
|
+
return board === null ? null : parseSprintLanes(board, watch.settings.planLanguage)
|
|
820
|
+
}
|
|
821
|
+
|
|
787
822
|
/** The sprint's lane names; the previous ones when the sprint cannot be read, so no lane drops out over it. */
|
|
788
823
|
const sprintLaneNames = (watch: Watch, previous: string[]): string[] =>
|
|
789
824
|
readSprintLanes(watch)?.map((lane) => lane.name) ?? previous
|
|
@@ -872,7 +907,7 @@ const printStreamHeartbeat = async (watch: Watch, watching: Watching, now: numbe
|
|
|
872
907
|
const counts = currentPhaseCountsOf(progress)
|
|
873
908
|
const notes = progressNotes(watching.lanes, watching.current, counts, watching.heartbeatCounts)
|
|
874
909
|
watching.heartbeatCounts = { ...watching.heartbeatCounts, ...counts }
|
|
875
|
-
console.log(streamHeartbeatLine(clock(
|
|
910
|
+
console.log(streamHeartbeatLine(clock(now), watching.lanes, watching.current, notes))
|
|
876
911
|
}
|
|
877
912
|
|
|
878
913
|
/**
|
|
@@ -889,7 +924,14 @@ const pollOnce = async (watch: Watch, watching: Watching): Promise<boolean> => {
|
|
|
889
924
|
// read at every poll, not with GitHub: a lane waits for the answer only minutes; after the await, so a state saved
|
|
890
925
|
// during the GitHub read records no request as seen before it was reported
|
|
891
926
|
watching.approvalRequests = readApprovalRequests(watch.root)
|
|
892
|
-
|
|
927
|
+
// read at every poll like the approvals; the events and the record use the same read and time, so an appointment
|
|
928
|
+
// that falls due between the two is neither recorded unreported nor reported twice
|
|
929
|
+
watching.appointments = readAppointments(watch, now)
|
|
930
|
+
const polledEvents = [
|
|
931
|
+
...githubEvents,
|
|
932
|
+
...approvalEvents(watching.state.approvals, watching.approvalRequests, now),
|
|
933
|
+
...appointmentEvents(watching.state.appointments, watching.appointments?.table ?? null, now),
|
|
934
|
+
]
|
|
893
935
|
const stopped = [...firedLanes(watching.baseline, watching.current, watching.lanes), ...watching.departed]
|
|
894
936
|
watching.departed = []
|
|
895
937
|
const heartbeat = isHeartbeatDue(watching.quietSince, now, heartbeatMinutes * MINUTE_MS)
|
|
@@ -942,6 +984,7 @@ const initialWatching = (
|
|
|
942
984
|
laneModelReadings: {},
|
|
943
985
|
laneParallelReadings: {},
|
|
944
986
|
approvalRequests: null,
|
|
987
|
+
appointments: null,
|
|
945
988
|
planViews: { failureReported: false },
|
|
946
989
|
})
|
|
947
990
|
|
|
@@ -1007,7 +1050,7 @@ const watchOf = (options: WatchOptions): Watch => {
|
|
|
1007
1050
|
checks: config.watch.checks,
|
|
1008
1051
|
previews: config.watch.previews,
|
|
1009
1052
|
sources: config.watch.sources,
|
|
1010
|
-
texts:
|
|
1053
|
+
texts: watchTexts(settings.responseLanguage),
|
|
1011
1054
|
herdr: insideHerdr(settings.adapter, process.env),
|
|
1012
1055
|
}
|
|
1013
1056
|
}
|
package/src/watch/lane-report.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { currentPhase, isTickedUnmerged, type LaneProgress, phasesText } from '.
|
|
|
3
3
|
import { terminalSafe } from '../plan-format/terminal-text'
|
|
4
4
|
import { CHECK_NAME_LENGTH, type ChecksState } from '../plans/pull-requests'
|
|
5
5
|
import { shownWithMore } from '../plans/shown-with-more'
|
|
6
|
+
import type { AppointmentEvent } from './appointments'
|
|
6
7
|
import type { ApprovalEvent } from './approvals'
|
|
7
8
|
import type { CoordinatorRead } from './coordinator-fill'
|
|
8
9
|
import { type FeedbackEvent, firstLine } from './feedback'
|
|
@@ -74,8 +75,8 @@ const contextSegment = (level: ContextLevel | null): string[] =>
|
|
|
74
75
|
level === null || level === CONTEXT_LEVEL.ok ? [] : [`context ${level}`]
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
|
-
* One line per lane, in the words of `texts`: `<lane> <status> [· context warn|alert] · <branch>|
|
|
78
|
-
* PR #<n> <checks>|
|
|
78
|
+
* One line per lane, in the words of `texts`: `<lane> <status> [· context warn|alert] · <branch>|no branch ·
|
|
79
|
+
* PR #<n> <checks>|no PR · open phase …|phase … ticked, not merged|plan finished|no plan`.
|
|
79
80
|
*/
|
|
80
81
|
export const laneReportLine = (report: LaneReport, texts: WatchTexts): string =>
|
|
81
82
|
[
|
|
@@ -158,6 +159,13 @@ export const watchEventLine = (event: WatchEvent, target: string | null): string
|
|
|
158
159
|
return `=== ${event.kind}`
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
/**
|
|
163
|
+
* The line of an appointment event: `=== due <when> · <action>` for one that fell due, `=== appointment unreadable
|
|
164
|
+
* <when> · <action>` for an open row whose time cell could not be read. Both texts come from the board as written.
|
|
165
|
+
*/
|
|
166
|
+
export const appointmentEventLine = (event: AppointmentEvent): string =>
|
|
167
|
+
`=== ${event.kind} ${terminalSafe(event.when)} · ${terminalSafe(event.action).slice(0, REPORT_TEXT_LENGTH)}`
|
|
168
|
+
|
|
161
169
|
/**
|
|
162
170
|
* The lines of a feedback event: `=== feedback #<number> (lane) <author> <label> · <k> new`, then one line per entry,
|
|
163
171
|
* `<path>:<line> <first line>` or `<first line>`, cut to a length and to `FEEDBACK_ENTRY_LIMIT` entries.
|
|
@@ -230,7 +238,7 @@ const coordinatorValue = (name: string | null, read: CoordinatorRead | null, tex
|
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
/**
|
|
233
|
-
* `coordinator <name> 62 % warn`, `coordinator <name>
|
|
241
|
+
* `coordinator <name> 62 % warn`, `coordinator <name> no value (<reason>)` or `coordinator no name`;
|
|
234
242
|
* while the board has rows only in the checkout, `· board: 2 rows only in the checkout` follows.
|
|
235
243
|
*/
|
|
236
244
|
export const coordinatorStatusLine = (
|
package/src/watch/texts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { APPROVAL_TEXTS,
|
|
1
|
+
import { APPROVAL_TEXTS, type PlanLanguage } from '../vocabulary'
|
|
2
2
|
|
|
3
|
-
/** The words of the watcher's status and duty lines
|
|
3
|
+
/** The words of the watcher's status and duty lines. */
|
|
4
4
|
export type WatchTexts = {
|
|
5
5
|
noBranch: string
|
|
6
6
|
noPullRequest: string
|
|
@@ -37,60 +37,36 @@ export type WatchTexts = {
|
|
|
37
37
|
/** The mark the approval gate hook reads from the question, as its `requestMark` writes it (contract test). */
|
|
38
38
|
const approvalMark = (id: string, hash: string): string => `\`approval ${id} ${hash}\``
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
phase: 'phase',
|
|
74
|
-
startsAfter: 'starts after',
|
|
75
|
-
planFinished: 'plan finished',
|
|
76
|
-
noPlan: 'no plan',
|
|
77
|
-
noName: 'no name',
|
|
78
|
-
noValue: 'no value',
|
|
79
|
-
unnamedPane: (name) => `pane without a name, herdr agent rename "$HERDR_PANE_ID" ${name}`,
|
|
80
|
-
noAgentNamed: (name) => `no agent named ${name}`,
|
|
81
|
-
afterMerge: (number) => `after merge #${number}`,
|
|
82
|
-
mergedLaneDuty: 'start the next phase or close the lane',
|
|
83
|
-
otherLaneDuty: 'rebase, then start the next phase or name the reason to wait',
|
|
84
|
-
blockedDuty:
|
|
85
|
-
'vet the question against the plan and the rules, answer it in the lane or ask the user in a dialog; never hand the pane over',
|
|
86
|
-
laneAlertDuty: 'request the hand-over at the next phase boundary (§ 8)',
|
|
87
|
-
approvalDuty: (id, hash) =>
|
|
88
|
-
`ask through AskUserQuestion, the question contains ${approvalMark(id, hash)} (hash as in the file shown), options "${APPROVAL_TEXTS.en.allow}" and "${APPROVAL_TEXTS.en.deny}" (§ 4)`,
|
|
89
|
-
parallelDuty: (phases) =>
|
|
90
|
-
`${phases} can run in parallel: propose it to the user, check content and release files yourself (the table covers paths and markers only)`,
|
|
91
|
-
laneModelDuty:
|
|
92
|
-
'switch it while idle: /exit, then claude --resume <session> --model <id> in the same pane (§ 2 step 6)',
|
|
93
|
-
checkoutRows: (term, count) => `${term}: ${count} ${count === 1 ? 'row' : 'rows'} only in the checkout`,
|
|
94
|
-
otherPullRequests: 'other PRs',
|
|
95
|
-
},
|
|
96
|
-
}
|
|
40
|
+
/**
|
|
41
|
+
* The watcher's words: English whatever the project's languages. Only the two option labels in the approval duty follow
|
|
42
|
+
* `dialogLanguage` (the config's `responseLanguage`), because the coordinator asks the user with them and the approval
|
|
43
|
+
* gate hook matches the answer against exactly those labels, as `jackherd sync` renders them into it.
|
|
44
|
+
*/
|
|
45
|
+
export const watchTexts = (dialogLanguage: PlanLanguage): WatchTexts => ({
|
|
46
|
+
noBranch: 'no branch',
|
|
47
|
+
noPullRequest: 'no PR',
|
|
48
|
+
openPhase: 'open phase',
|
|
49
|
+
tickedUnmerged: 'ticked, not merged',
|
|
50
|
+
phase: 'phase',
|
|
51
|
+
startsAfter: 'starts after',
|
|
52
|
+
planFinished: 'plan finished',
|
|
53
|
+
noPlan: 'no plan',
|
|
54
|
+
noName: 'no name',
|
|
55
|
+
noValue: 'no value',
|
|
56
|
+
unnamedPane: (name) => `pane without a name, herdr agent rename "$HERDR_PANE_ID" ${name}`,
|
|
57
|
+
noAgentNamed: (name) => `no agent named ${name}`,
|
|
58
|
+
afterMerge: (number) => `after merge #${number}`,
|
|
59
|
+
mergedLaneDuty: 'start the next phase or close the lane',
|
|
60
|
+
otherLaneDuty: 'rebase, then start the next phase or name the reason to wait',
|
|
61
|
+
blockedDuty:
|
|
62
|
+
'vet the question against the plan and the rules, answer it in the lane or ask the user in a dialog; never hand the pane over',
|
|
63
|
+
laneAlertDuty: 'request the hand-over at the next phase boundary (§ 8)',
|
|
64
|
+
parallelDuty: (phases) =>
|
|
65
|
+
`${phases} can run in parallel: propose it to the user, check content and release files yourself (the table covers paths and markers only)`,
|
|
66
|
+
laneModelDuty:
|
|
67
|
+
'switch it while idle: /exit, then claude --resume <session> --model <id> in the same pane (§ 2 step 6)',
|
|
68
|
+
checkoutRows: (term, count) => `${term}: ${count} ${count === 1 ? 'row' : 'rows'} only in the checkout`,
|
|
69
|
+
otherPullRequests: 'other PRs',
|
|
70
|
+
approvalDuty: (id, hash) =>
|
|
71
|
+
`ask through AskUserQuestion, the question contains ${approvalMark(id, hash)} (hash as in the file shown), options "${APPROVAL_TEXTS[dialogLanguage].allow}" and "${APPROVAL_TEXTS[dialogLanguage].deny}" (§ 4)`,
|
|
72
|
+
})
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { CHECKS, type ChecksState } from '../plans/pull-requests'
|
|
2
|
+
import {
|
|
3
|
+
APPOINTMENT_EVENT,
|
|
4
|
+
type AppointmentEvent,
|
|
5
|
+
type AppointmentState,
|
|
6
|
+
emptyAppointmentState,
|
|
7
|
+
isAppointmentState,
|
|
8
|
+
} from './appointments'
|
|
2
9
|
import { APPROVAL_EVENT, type ApprovalEvent } from './approvals'
|
|
3
10
|
import { type CoordinatorLevel, type CoordinatorReading, isContextLevel, levelStep } from './coordinator-fill'
|
|
4
11
|
import { emptyFailureState, type FailureState, failedTestsOf, isFailureState } from './failures'
|
|
@@ -29,6 +36,8 @@ export const WATCH_EVENT = {
|
|
|
29
36
|
laneParallel: LANE_PARALLEL_EVENT,
|
|
30
37
|
approval: APPROVAL_EVENT,
|
|
31
38
|
source: SOURCE_EVENT,
|
|
39
|
+
due: APPOINTMENT_EVENT.due,
|
|
40
|
+
appointmentUnreadable: APPOINTMENT_EVENT.unreadable,
|
|
32
41
|
} as const
|
|
33
42
|
|
|
34
43
|
export type PullRequestObservation = Omit<WatchedPullRequest, 'number' | 'branch' | 'failedRuns'>
|
|
@@ -85,6 +94,8 @@ export type WatchState = {
|
|
|
85
94
|
reportMtimes: Record<string, number | null>
|
|
86
95
|
/** The lane lines of the status block last printed; null before the first report. */
|
|
87
96
|
statusLines: string[] | null
|
|
97
|
+
/** The board's appointments reported as due and its unreadable appointment rows reported, each once (`appointments.ts`). */
|
|
98
|
+
appointments: AppointmentState
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
/** The kinds of the events a pull request fires. */
|
|
@@ -130,6 +141,7 @@ export type WatchEvent =
|
|
|
130
141
|
| LaneParallelEvent
|
|
131
142
|
| ApprovalEvent
|
|
132
143
|
| SourceEvent
|
|
144
|
+
| AppointmentEvent
|
|
133
145
|
|
|
134
146
|
export const isPullRequestEvent = (event: WatchEvent): event is PullRequestEvent =>
|
|
135
147
|
(PULL_REQUEST_KINDS as readonly string[]).includes(event.kind)
|
|
@@ -165,8 +177,12 @@ export const emptyWatchState = (): WatchState => ({
|
|
|
165
177
|
sources: emptySourceState(),
|
|
166
178
|
reportMtimes: {},
|
|
167
179
|
statusLines: null,
|
|
180
|
+
appointments: emptyAppointmentState(),
|
|
168
181
|
})
|
|
169
182
|
|
|
183
|
+
/** The fields every state file carries since the first one; each field added later is read as empty when missing. */
|
|
184
|
+
const FIRST_FORMAT_FIELDS = ['seenSeqs', 'pullRequests', 'runs', 'ghDownSince'] as const
|
|
185
|
+
|
|
170
186
|
const isOneOf = (values: Record<string, string>, value: unknown): boolean =>
|
|
171
187
|
typeof value === 'string' && Object.values(values).includes(value)
|
|
172
188
|
|
|
@@ -207,7 +223,8 @@ const isWatchState = (value: Record<string, unknown>): value is WatchState =>
|
|
|
207
223
|
isSourceState(value.sources) &&
|
|
208
224
|
everyValue(value.reportMtimes, isSeq) &&
|
|
209
225
|
(value.statusLines === null ||
|
|
210
|
-
(Array.isArray(value.statusLines) && value.statusLines.every((line) => typeof line === 'string')))
|
|
226
|
+
(Array.isArray(value.statusLines) && value.statusLines.every((line) => typeof line === 'string'))) &&
|
|
227
|
+
isAppointmentState(value.appointments)
|
|
211
228
|
|
|
212
229
|
/**
|
|
213
230
|
* Reads the state file; throws on any other shape, so a damaged file is seen, not read as empty. The
|
|
@@ -217,22 +234,8 @@ export const parseWatchState = (json: string): WatchState => {
|
|
|
217
234
|
const value: unknown = JSON.parse(json)
|
|
218
235
|
if (!isRecord(value)) throw new Error('watch state: unexpected shape')
|
|
219
236
|
if (isRecord(value.seenSeqs)) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
laneBranches: {},
|
|
223
|
-
coordinatorLevel: null,
|
|
224
|
-
feedback: emptyFeedbackState(),
|
|
225
|
-
failures: emptyFailureState(),
|
|
226
|
-
previews: emptyPreviewState(),
|
|
227
|
-
laneLevels: {},
|
|
228
|
-
laneModels: {},
|
|
229
|
-
laneParallels: {},
|
|
230
|
-
approvals: [],
|
|
231
|
-
sources: emptySourceState(),
|
|
232
|
-
reportMtimes: {},
|
|
233
|
-
statusLines: null,
|
|
234
|
-
...value,
|
|
235
|
-
}
|
|
237
|
+
if (!FIRST_FORMAT_FIELDS.every((field) => field in value)) throw new Error('watch state: unexpected shape')
|
|
238
|
+
const complete = { ...emptyWatchState(), ...value }
|
|
236
239
|
if (!isWatchState(complete)) throw new Error('watch state: unexpected shape')
|
|
237
240
|
return complete
|
|
238
241
|
}
|
|
@@ -289,23 +292,13 @@ export const recordWatchState = (previous: WatchState, observation: Observation,
|
|
|
289
292
|
return { ...previous, seenSeqs: seenSeqsOf(lanes), ghDownSince: previous.ghDownSince ?? now, laneBranches }
|
|
290
293
|
}
|
|
291
294
|
return {
|
|
295
|
+
...previous,
|
|
292
296
|
seenSeqs: seenSeqsOf(lanes),
|
|
293
297
|
pullRequests: recordsOf(previous, github.pullRequests, github.own ?? []),
|
|
294
298
|
runs: runsOf(github.runs),
|
|
295
299
|
ghDownSince: null,
|
|
296
300
|
savedAt: now,
|
|
297
301
|
laneBranches: keptLaneBranches(previous, observation, github.pullRequests),
|
|
298
|
-
coordinatorLevel: previous.coordinatorLevel,
|
|
299
|
-
feedback: previous.feedback,
|
|
300
|
-
failures: previous.failures,
|
|
301
|
-
previews: previous.previews,
|
|
302
|
-
laneLevels: previous.laneLevels,
|
|
303
|
-
laneModels: previous.laneModels,
|
|
304
|
-
laneParallels: previous.laneParallels,
|
|
305
|
-
approvals: previous.approvals,
|
|
306
|
-
sources: previous.sources,
|
|
307
|
-
reportMtimes: previous.reportMtimes,
|
|
308
|
-
statusLines: previous.statusLines,
|
|
309
302
|
}
|
|
310
303
|
}
|
|
311
304
|
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { isAbsolute, relative, resolve, sep } from 'node:path'
|
|
2
|
-
|
|
3
|
-
/** herdr's agent names; a worktree whose directory has another name cannot be a lane. */
|
|
4
|
-
const LANE_NAME = /^[a-z][a-z0-9_-]{0,31}$/
|
|
5
|
-
|
|
6
|
-
export type LaneLocation = { cwd: string; main: string; worktreeDirectory: string }
|
|
7
|
-
|
|
8
|
-
/** The lane whose worktree holds `cwd`: the first directory below the main checkout's worktree directory, else null. */
|
|
9
|
-
export const laneOf = ({ cwd, main, worktreeDirectory }: LaneLocation): string | null => {
|
|
10
|
-
const path = relative(resolve(main, worktreeDirectory), resolve(cwd))
|
|
11
|
-
if (path === '' || path.startsWith('..') || isAbsolute(path)) return null
|
|
12
|
-
const [lane] = path.split(sep)
|
|
13
|
-
return LANE_NAME.test(lane) ? lane : null
|
|
14
|
-
}
|