jackherd 1.12.7 → 1.14.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 +15 -0
- package/INSTALL.md +1 -1
- package/README.md +17 -17
- package/dist/config.d.ts +35 -11
- package/dist/config.js +49 -83
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/merge-guard/board-settings.ts +7 -32
- package/kit/hooks/hooks/merge-guard/lane-plan.ts +2 -29
- package/kit/hooks/hooks/plain-question/code-cache.ts +70 -0
- package/kit/hooks/hooks/plain-question/index.ts +80 -0
- package/kit/hooks/hooks/plain-question/run.ts +50 -0
- package/kit/hooks/hooks/shared/board-lanes.ts +19 -0
- package/kit/hooks/hooks/shared/boards.ts +96 -0
- package/kit/hooks/hooks/shared/plan-codes.ts +68 -0
- package/kit/hooks/hooks.json +9 -0
- package/kit/review/skills/diagnose/SKILL.md +2 -2
- package/kit/review/skills/diagnose/references/feedback-loops.md +4 -13
- package/kit/rules/rules/kit/code-style.md +15 -18
- package/kit/rules/rules/kit/testing.md +7 -28
- package/kit/workflow/skills/implement/SKILL.md +3 -3
- package/kit/workflow/skills/orchestrate/SKILL.md +4 -1
- package/kit/workflow/skills/orchestrate/references/events.md +6 -1
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +2 -1
- package/kit/workflow/skills/start/SKILL.md +9 -6
- package/kit/workflow/skills/start/references/missing-commands.md +17 -0
- package/package.json +1 -1
- package/schemas/jackherd.schema.json +38 -11
- package/src/check/command.ts +167 -35
- package/src/check/file-kinds.ts +142 -0
- package/src/check/plan-structure-check.ts +34 -1
- package/src/check/plan.ts +230 -45
- package/src/check/report.ts +111 -3
- package/src/check/result.ts +13 -3
- package/src/check/roots.ts +47 -10
- package/src/check/schema.ts +20 -3
- package/src/check/settings.ts +25 -4
- package/src/check/skill-budget-defaults.ts +7 -4
- package/src/config/base-branch.ts +2 -0
- package/src/config/command.ts +32 -13
- package/src/config/detect.ts +242 -74
- package/src/config/location.ts +6 -0
- package/src/config/repository-facts.ts +32 -7
- package/src/config/repository-values.ts +162 -5
- package/src/config/schema.ts +34 -16
- package/src/lane-prompt/texts.ts +2 -2
- package/src/plan-format/bare-codes.ts +28 -0
- package/src/plans/lane-plans.ts +2 -1
- package/src/sync/command-context.ts +4 -2
- package/src/sync/herdr-plugin.ts +30 -6
- package/src/sync/init.ts +17 -16
- package/src/sync/render.ts +4 -6
- package/src/watch/command.ts +32 -1
- package/src/watch/lane-progress.ts +6 -1
- package/src/watch/lane-report.ts +10 -0
- package/src/watch/plan-missing-read.ts +47 -0
- package/src/watch/plan-missing.ts +43 -0
- package/src/watch/reference-paths.ts +1 -0
- package/src/watch/texts.ts +4 -0
- package/src/watch/watch-events.ts +7 -0
package/src/watch/lane-report.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { LaneModelEvent } from './lane-model'
|
|
|
13
13
|
import type { LaneParallelEvent } from './lane-parallel'
|
|
14
14
|
import { laneStalledLine } from './lane-stalled'
|
|
15
15
|
import { AGENT_STATUS, type LaneSnapshot, NO_AGENT } from './lane-watch'
|
|
16
|
+
import { PLAN_MISSING_EVENT, type PlanMissingEvent, planMissingLine } from './plan-missing'
|
|
16
17
|
import { PULL_REQUEST_STATE, type WatchedPullRequest } from './pull-requests'
|
|
17
18
|
import { SOURCE_FIELD } from './source-fields'
|
|
18
19
|
import { type FieldChange, lineText, type SourceEvent } from './sources'
|
|
@@ -161,6 +162,7 @@ const laneEventLine = (event: WatchEvent): string | null => {
|
|
|
161
162
|
return `=== ${event.kind} ${terminalSafe(event.lane)} ${phasesText(event.phases)}`
|
|
162
163
|
if (event.kind === WATCH_EVENT.noTaskSignal) return `=== ${terminalSafe(noTaskSignalLine(event))}`
|
|
163
164
|
if (event.kind === WATCH_EVENT.laneStalled) return `=== ${terminalSafe(laneStalledLine(event))}`
|
|
165
|
+
if (event.kind === WATCH_EVENT.planMissing) return `=== ${planMissingLine(event)}`
|
|
164
166
|
return null
|
|
165
167
|
}
|
|
166
168
|
|
|
@@ -339,6 +341,14 @@ export const noTaskSignalDutyLines = (events: readonly WatchEvent[], texts: Watc
|
|
|
339
341
|
.filter(isNoTaskSignal)
|
|
340
342
|
.map(({ lane }) => `${terminalSafe(lane)} ${NO_TASK_SIGNAL_EVENT}: ${texts.noTaskSignalDuty}`)
|
|
341
343
|
|
|
344
|
+
const isPlanMissing = (event: WatchEvent): event is PlanMissingEvent => event.kind === WATCH_EVENT.planMissing
|
|
345
|
+
|
|
346
|
+
/** What a lane whose plan only its worktree has asks of the coordinator (`orchestrate` § 2 step 0): one line per such lane. */
|
|
347
|
+
export const planMissingDutyLines = (events: readonly WatchEvent[], texts: WatchTexts): string[] =>
|
|
348
|
+
events
|
|
349
|
+
.filter(isPlanMissing)
|
|
350
|
+
.map(({ lane }) => `${terminalSafe(lane)} ${PLAN_MISSING_EVENT}: ${texts.planMissingDuty}`)
|
|
351
|
+
|
|
342
352
|
const isApproval = (event: WatchEvent): event is ApprovalEvent => event.kind === WATCH_EVENT.approval
|
|
343
353
|
|
|
344
354
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { parseSprintLanes } from '../plan-format'
|
|
4
|
+
import { baseReaderAt, commitOf } from '../plans/git'
|
|
5
|
+
import { isOutsideRepository } from '../plans/lane-plans'
|
|
6
|
+
import { baseRefOf, boardRepoPathOf, type PlansSettings, worktreeOf } from '../plans/settings'
|
|
7
|
+
import { lanePlanPath } from './lane-progress'
|
|
8
|
+
import {
|
|
9
|
+
type PlanMissingEvent,
|
|
10
|
+
type PlanMissingReadings,
|
|
11
|
+
type PlanMissingState,
|
|
12
|
+
planMissingEvents,
|
|
13
|
+
} from './plan-missing'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The plan the lane's newest linking row names in the board as its worktree has it; null without a worktree board or
|
|
17
|
+
* row, or for a link that leaves the repository, which no base ref can hold.
|
|
18
|
+
*/
|
|
19
|
+
const worktreePlanOf = (root: string, settings: PlansSettings, boardFile: string, laneName: string): string | null => {
|
|
20
|
+
const file = join(worktreeOf(root, settings, laneName), boardRepoPathOf(root, settings, boardFile))
|
|
21
|
+
if (!existsSync(file)) return null
|
|
22
|
+
const lane = parseSprintLanes(readFileSync(file, 'utf8'), settings.planLanguage).find(({ name }) => name === laneName)
|
|
23
|
+
const path = lane === undefined ? null : lanePlanPath(root, settings, boardFile, lane)
|
|
24
|
+
return path === null || isOutsideRepository(path) ? null : path
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* This heartbeat's `plan missing on base` events and the state to keep, for the working lanes: the plan of each lane's
|
|
29
|
+
* row in its own worktree against the base ref, where a worktree made from the base would not find it. Nothing when
|
|
30
|
+
* the base ref does not resolve, which would make every plan look missing.
|
|
31
|
+
*/
|
|
32
|
+
export const readPlanMissingEvents = (
|
|
33
|
+
root: string,
|
|
34
|
+
settings: PlansSettings,
|
|
35
|
+
boardFile: string,
|
|
36
|
+
workingLanes: readonly string[],
|
|
37
|
+
previous: PlanMissingState,
|
|
38
|
+
): { events: PlanMissingEvent[]; state: PlanMissingState } => {
|
|
39
|
+
const baseRef = baseRefOf(settings)
|
|
40
|
+
const commit = commitOf(root, baseRef)
|
|
41
|
+
if (commit === null) return { events: [], state: previous }
|
|
42
|
+
const base = baseReaderAt(root, commit)
|
|
43
|
+
const readings: PlanMissingReadings = Object.fromEntries(
|
|
44
|
+
workingLanes.map((lane) => [lane, worktreePlanOf(root, settings, boardFile, lane)]),
|
|
45
|
+
)
|
|
46
|
+
return planMissingEvents(previous, readings, (path) => base(path) !== null, baseRef)
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { terminalSafe } from '../plan-format/terminal-text'
|
|
2
|
+
import { isRecord } from '../shared/is-record'
|
|
3
|
+
|
|
4
|
+
export const PLAN_MISSING_EVENT = 'plan missing on base'
|
|
5
|
+
|
|
6
|
+
/** Per working lane: the plan its newest board row links, as its worktree has it; null without one. */
|
|
7
|
+
export type PlanMissingReadings = Readonly<Record<string, string | null>>
|
|
8
|
+
|
|
9
|
+
/** Per lane: the plan path last reported as missing on the base ref; a lane whose plan is there has no entry. */
|
|
10
|
+
export type PlanMissingState = Record<string, string>
|
|
11
|
+
|
|
12
|
+
export type PlanMissingEvent = { kind: typeof PLAN_MISSING_EVENT; lane: string; path: string; baseRef: string }
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One event per working lane whose board row, as its worktree has it, links a plan the base ref lacks (`existsAtBase`
|
|
16
|
+
* answers for a repository path). A plan is reported once while it stays missing: the state clears when it reaches the
|
|
17
|
+
* base or the row links another, and a lane without a reading (not at work) keeps what it had.
|
|
18
|
+
*/
|
|
19
|
+
export const planMissingEvents = (
|
|
20
|
+
previous: PlanMissingState,
|
|
21
|
+
readings: PlanMissingReadings,
|
|
22
|
+
existsAtBase: (path: string) => boolean,
|
|
23
|
+
baseRef: string,
|
|
24
|
+
): { events: PlanMissingEvent[]; state: PlanMissingState } => {
|
|
25
|
+
const events: PlanMissingEvent[] = []
|
|
26
|
+
const state: PlanMissingState = { ...previous }
|
|
27
|
+
for (const [lane, planPath] of Object.entries(readings)) {
|
|
28
|
+
if (planPath === null || existsAtBase(planPath)) {
|
|
29
|
+
delete state[lane]
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
if (previous[lane] !== planPath) events.push({ kind: PLAN_MISSING_EVENT, lane, path: planPath, baseRef })
|
|
33
|
+
state[lane] = planPath
|
|
34
|
+
}
|
|
35
|
+
return { events, state }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `lane <name> plan missing on <base ref>: <path>`. */
|
|
39
|
+
export const planMissingLine = ({ lane, path, baseRef }: PlanMissingEvent): string =>
|
|
40
|
+
`lane ${terminalSafe(lane)} plan missing on ${terminalSafe(baseRef)}: ${terminalSafe(path)}`
|
|
41
|
+
|
|
42
|
+
export const isPlanMissingState = (value: unknown): value is PlanMissingState =>
|
|
43
|
+
isRecord(value) && Object.values(value).every((path) => typeof path === 'string')
|
|
@@ -37,6 +37,7 @@ const EVENT_REFERENCES: Readonly<Record<string, Reference>> = {
|
|
|
37
37
|
[WATCH_EVENT.taskParallel]: REFERENCE.events,
|
|
38
38
|
[WATCH_EVENT.noTaskSignal]: REFERENCE.events,
|
|
39
39
|
[WATCH_EVENT.unlinkedArtifact]: REFERENCE.events,
|
|
40
|
+
[WATCH_EVENT.planMissing]: REFERENCE.events,
|
|
40
41
|
[UPDATE_AVAILABLE_EVENT]: REFERENCE.events,
|
|
41
42
|
[WATCH_EVENT.laneModel]: REFERENCE.laneSession,
|
|
42
43
|
}
|
package/src/watch/texts.ts
CHANGED
|
@@ -28,6 +28,8 @@ export type WatchTexts = {
|
|
|
28
28
|
parallelDuty: (phases: string) => string
|
|
29
29
|
/** What a lane that works without a task signal asks of the coordinator. */
|
|
30
30
|
noTaskSignalDuty: string
|
|
31
|
+
/** What a lane whose plan the base ref lacks asks of the coordinator. */
|
|
32
|
+
planMissingDuty: string
|
|
31
33
|
/** What a lane's call waiting for approval asks of the coordinator, with the mark (request id and call hash) the question must carry. */
|
|
32
34
|
approvalDuty: (id: string, hash: string) => string
|
|
33
35
|
/** The standing duty while the main checkout is behind its base branch on the remote (`origin/main`), with the count of commits. */
|
|
@@ -70,6 +72,8 @@ export const watchTexts = (dialogLanguage: PlanLanguage): WatchTexts => ({
|
|
|
70
72
|
parallelDuty: (phases) =>
|
|
71
73
|
`${phases} can run in parallel: propose it to the user, check content and release files yourself (the table covers paths and markers only)`,
|
|
72
74
|
noTaskSignalDuty: 'remind the lane to report its task with jh task start; do not stop it',
|
|
75
|
+
planMissingDuty:
|
|
76
|
+
"the herd shows the base ref's plan, not this one: run § 2 step 0 for it, copying from the lane's worktree",
|
|
73
77
|
laneModelDuty:
|
|
74
78
|
'switch it while idle: /exit, then claude --resume <session> --model <id> in the same pane (§ 2 step 6)',
|
|
75
79
|
mainCheckoutBehind: (remoteBase, count) =>
|
|
@@ -16,6 +16,7 @@ import { isLaneModels, LANE_MODEL_EVENT, type LaneModelEvent, type LaneModels }
|
|
|
16
16
|
import { isLaneParallels, LANE_PARALLEL_EVENT, type LaneParallelEvent, type LaneParallels } from './lane-parallel'
|
|
17
17
|
import { isStallState, LANE_STALLED_EVENT, type LaneStalledEvent, type StallState } from './lane-stalled'
|
|
18
18
|
import { type LaneSnapshot, type SeenSeqs, seenSeqsOf } from './lane-watch'
|
|
19
|
+
import { isPlanMissingState, PLAN_MISSING_EVENT, type PlanMissingEvent, type PlanMissingState } from './plan-missing'
|
|
19
20
|
import { emptyPreviewState, isPreviewState, PREVIEW_EVENT, type PreviewEvent, type PreviewState } from './previews'
|
|
20
21
|
import { PULL_REQUEST_STATE, type WatchedPullRequest } from './pull-requests'
|
|
21
22
|
import { emptySourceState, isSourceState, SOURCE_EVENT, type SourceEvent, type SourceState } from './sources'
|
|
@@ -60,6 +61,7 @@ export const WATCH_EVENT = {
|
|
|
60
61
|
noTaskSignal: NO_TASK_SIGNAL_EVENT,
|
|
61
62
|
laneStalled: LANE_STALLED_EVENT,
|
|
62
63
|
unlinkedArtifact: UNLINKED_ARTIFACT_EVENT,
|
|
64
|
+
planMissing: PLAN_MISSING_EVENT,
|
|
63
65
|
approval: APPROVAL_EVENT,
|
|
64
66
|
source: SOURCE_EVENT,
|
|
65
67
|
due: APPOINTMENT_EVENT.due,
|
|
@@ -122,6 +124,8 @@ export type WatchState = {
|
|
|
122
124
|
laneStalls: StallState
|
|
123
125
|
/** Per open artifact send no plan or board links: in its grace or already reported as `unlinked artifact`. */
|
|
124
126
|
unlinkedArtifacts: UnlinkedState
|
|
127
|
+
/** Per working lane whose board row links a plan the base ref lacks: the plan last reported as `plan missing on base`. */
|
|
128
|
+
planMissing: PlanMissingState
|
|
125
129
|
/** The ids of the approval requests on disk when they were last read, each reported once while it is open. */
|
|
126
130
|
approvals: string[]
|
|
127
131
|
/** Per configured source of `watch.sources`: the entries seen (`[id, fingerprint]`) and the start of a failure streak. */
|
|
@@ -185,6 +189,7 @@ export type WatchEvent =
|
|
|
185
189
|
| NoTaskSignalEvent
|
|
186
190
|
| LaneStalledEvent
|
|
187
191
|
| UnlinkedArtifactEvent
|
|
192
|
+
| PlanMissingEvent
|
|
188
193
|
| ApprovalEvent
|
|
189
194
|
| SourceEvent
|
|
190
195
|
| AppointmentEvent
|
|
@@ -224,6 +229,7 @@ export const emptyWatchState = (): WatchState => ({
|
|
|
224
229
|
noTaskSignals: {},
|
|
225
230
|
laneStalls: {},
|
|
226
231
|
unlinkedArtifacts: {},
|
|
232
|
+
planMissing: {},
|
|
227
233
|
approvals: [],
|
|
228
234
|
sources: emptySourceState(),
|
|
229
235
|
reportMtimes: {},
|
|
@@ -277,6 +283,7 @@ const isWatchState = (value: Record<string, unknown>): value is WatchState =>
|
|
|
277
283
|
isNoSignalState(value.noTaskSignals) &&
|
|
278
284
|
isStallState(value.laneStalls) &&
|
|
279
285
|
isUnlinkedState(value.unlinkedArtifacts) &&
|
|
286
|
+
isPlanMissingState(value.planMissing) &&
|
|
280
287
|
Array.isArray(value.approvals) &&
|
|
281
288
|
value.approvals.every((id) => typeof id === 'string') &&
|
|
282
289
|
isSourceState(value.sources) &&
|