jackherd 1.8.1 → 1.9.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 +2 -0
- package/README.md +37 -44
- package/dist/{chunk-OU2KALXP.js → chunk-MCP4XJQH.js} +15 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +19 -3
- package/dist/plan-format.d.ts +19 -2
- package/dist/plan-format.js +21 -7
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/approval-gate/index.ts +22 -4
- package/kit/hooks/hooks/approval-gate/run.ts +29 -6
- package/kit/hooks/hooks/approval-gate/subagent.ts +18 -0
- package/kit/hooks/hooks/merge-guard/decision.ts +67 -20
- package/kit/hooks/hooks/sent-file/index.ts +19 -0
- package/kit/hooks/hooks/sent-file/record.ts +128 -0
- package/kit/hooks/hooks/sent-file/run.ts +48 -0
- package/kit/hooks/hooks/sent-file/store.ts +18 -0
- package/kit/hooks/hooks/shared/board-lanes.ts +15 -2
- package/kit/hooks/hooks.json +9 -0
- package/kit/hooks/statusline.sh +1 -1
- package/kit/workflow/skills/implement/SKILL.md +2 -0
- package/kit/workflow/skills/orchestrate/SKILL.md +30 -6
- package/package.json +2 -1
- package/schemas/jackherd.schema.json +26 -2
- package/src/cli.ts +3 -0
- package/src/config/schema.ts +22 -2
- package/src/context/fill.ts +12 -0
- package/src/context/session-log.ts +43 -1
- package/src/plan-format/clock.ts +4 -0
- package/src/plan-format/dependencies.ts +2 -5
- package/src/plan-format/lanes.ts +30 -5
- package/src/plan-format/terminal-text.ts +4 -0
- package/src/plan-format/waiting.ts +48 -0
- package/src/plans/active-tasks.ts +60 -0
- package/src/plans/current-phase.ts +17 -7
- package/src/plans/current-task.ts +63 -5
- package/src/plans/lane-standing.ts +9 -4
- package/src/plans/lane-summary.ts +141 -74
- package/src/plans/lane-tickets.ts +15 -0
- package/src/plans/layout-phase.ts +57 -18
- package/src/plans/layout-summary.ts +85 -67
- package/src/plans/layout.ts +253 -89
- package/src/plans/live.ts +22 -0
- package/src/plans/phase-models.ts +1 -1
- package/src/plans/phase-ranges.ts +45 -0
- package/src/plans/pull-requests.ts +6 -0
- package/src/plans/selection.ts +19 -16
- package/src/plans/sent-files-read.ts +22 -0
- package/src/plans/sent-files.ts +121 -0
- package/src/plans/signals-read.ts +54 -0
- package/src/plans/snapshot.ts +251 -55
- package/src/plans/subagents-read.ts +82 -0
- package/src/plans/subagents.ts +132 -0
- package/src/plans/texts.ts +16 -11
- package/src/plans/unlisted-lanes.ts +10 -5
- package/src/plans/view.ts +3 -3
- package/src/plans/waiting-click.ts +76 -0
- package/src/preview/command.ts +83 -0
- package/src/preview/embed.ts +209 -0
- package/src/preview/images.ts +89 -0
- package/src/preview/open.ts +12 -0
- package/src/preview/options.ts +32 -0
- package/src/preview/page.ts +89 -0
- package/src/preview/render.ts +41 -0
- package/src/settled/board-row.ts +1 -2
- package/src/shared/is-record.ts +2 -0
- package/src/shared/private-records.ts +48 -0
- package/src/task-signals/agent.ts +15 -0
- package/src/task-signals/claude-read.ts +72 -0
- package/src/task-signals/claude-signals.ts +112 -0
- package/src/task-signals/command.ts +71 -0
- package/src/task-signals/fixtures/session.jsonl +7 -0
- package/src/task-signals/matcher.ts +96 -0
- package/src/task-signals/record-store.ts +40 -0
- package/src/task-signals/record.ts +68 -0
- package/src/task-signals/text.ts +13 -0
- package/src/task-signals/types.ts +14 -0
- package/src/terminal/open.ts +13 -6
- package/src/terminal/text.ts +8 -1
- package/src/terminal/width.ts +2 -1
- package/src/vocabulary/index.ts +28 -0
- package/src/waiting/command.ts +35 -0
- package/src/watch/appointments.ts +2 -3
- package/src/watch/approvals.ts +2 -2
- package/src/watch/command.ts +91 -5
- package/src/watch/failed-tests.ts +56 -10
- package/src/watch/failure-read.ts +18 -6
- package/src/watch/failures.ts +29 -5
- package/src/watch/feedback.ts +1 -1
- package/src/watch/gh-process.ts +9 -3
- package/src/watch/json-guards.ts +0 -3
- package/src/watch/lane-fill.ts +1 -1
- package/src/watch/lane-model.ts +1 -1
- package/src/watch/lane-parallel.ts +1 -1
- package/src/watch/lane-progress.ts +19 -7
- package/src/watch/lane-report.ts +5 -1
- package/src/watch/lane-rest.ts +28 -0
- package/src/watch/plan-badges.ts +9 -2
- package/src/watch/previews.ts +1 -1
- package/src/watch/pull-requests.ts +2 -1
- package/src/watch/source-path.ts +1 -1
- package/src/watch/sources.ts +1 -1
- package/src/watch/stream-output.ts +12 -7
- package/src/watch/task-parallel-input.ts +118 -0
- package/src/watch/task-parallel-read.ts +149 -0
- package/src/watch/task-parallel-touched.ts +39 -0
- package/src/watch/task-parallel.ts +206 -0
- package/src/watch/waiting-line.ts +19 -0
- package/src/watch/watch-events.ts +13 -1
- package/src/watch/workflow-runs.ts +2 -1
|
@@ -1,49 +1,59 @@
|
|
|
1
|
-
import { phaseLabel
|
|
2
|
-
import { fit,
|
|
3
|
-
import {
|
|
4
|
-
import { deeper, type Frame, joinFrames, nest, prefixed, rowFrame, textFrame } from './frame'
|
|
1
|
+
import { phaseLabel } from '../plan-format'
|
|
2
|
+
import { fit, paint, spread, visibleLength } from '../terminal/text'
|
|
3
|
+
import { deeper, type Frame, joinFrames, nest, rowFrame, textFrame } from './frame'
|
|
5
4
|
import type { ActivePhase, LaneSummary } from './lane-summary'
|
|
6
5
|
import {
|
|
7
|
-
|
|
6
|
+
activeArrow,
|
|
7
|
+
activeTitle,
|
|
8
|
+
agentCell,
|
|
8
9
|
COLUMN_SEPARATOR,
|
|
9
|
-
DEFERRED_MARK,
|
|
10
10
|
type LaneContext,
|
|
11
11
|
MIN_TITLE_WIDTH,
|
|
12
12
|
modelMark,
|
|
13
|
+
nowLine,
|
|
13
14
|
phaseChildren,
|
|
14
15
|
phaseCountCell,
|
|
15
16
|
plain,
|
|
17
|
+
TASK_INDENT,
|
|
16
18
|
taskFrame,
|
|
17
19
|
} from './layout-phase'
|
|
18
20
|
import { type PhaseModel, phaseModelKey } from './phase-models'
|
|
21
|
+
import { phaseRanges } from './phase-ranges'
|
|
19
22
|
import { itemKey, phaseKey } from './selection'
|
|
20
23
|
import { PHASE_COUNT } from './symbols'
|
|
21
24
|
import { type HerdTexts, TEXTS } from './texts'
|
|
22
25
|
|
|
23
|
-
/** The context of a lane's
|
|
26
|
+
/** The context of a lane's branches: the lane's, with the herd's words. */
|
|
24
27
|
type SummaryContext = LaneContext & { labels: HerdTexts }
|
|
25
28
|
|
|
26
|
-
/**
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const labelled = (labelWidth: number, label: string, frame: Frame): Frame =>
|
|
32
|
-
prefixed(frame, { first: padVisible(label, labelWidth), rest: ' '.repeat(labelWidth), style: paint.dim })
|
|
29
|
+
/** Whether only the boxes of the review gate are open. */
|
|
30
|
+
const onlyGateOpen = ({ items }: ActivePhase['phase']): boolean => {
|
|
31
|
+
const open = items.filter(({ done }) => !done)
|
|
32
|
+
return open.length > 0 && open.every(({ gate }) => gate)
|
|
33
|
+
}
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* `· 6 min ago`, dim: when the running phase was last ticked or reported, nothing when the herd saw neither; `· gate`
|
|
37
|
+
* after it while only the review gate is open. Empty when there is neither.
|
|
38
|
+
*/
|
|
39
|
+
const ageCell = ({ minutesAgo, phase }: ActivePhase, labels: HerdTexts): string => {
|
|
40
|
+
const words = [
|
|
41
|
+
...(minutesAgo === null ? [] : [labels.minutesAgo(minutesAgo)]),
|
|
42
|
+
...(onlyGateOpen(phase) ? [labels.gate] : []),
|
|
43
|
+
]
|
|
44
|
+
return words.length > 0 ? `${paint.dim(words.join(' '))}${COLUMN_SEPARATOR}` : ''
|
|
45
|
+
}
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
|
-
*
|
|
40
|
-
* too narrow to leave the title `MIN_TITLE_WIDTH` cells, the model gives
|
|
48
|
+
* `P12 <title> [model] · 6 min ago 16/26`: title at the left, model, age and count at the right edge, no arrow
|
|
49
|
+
* (arrows stand before tasks only). In a pane too narrow to leave the title `MIN_TITLE_WIDTH` cells, the model gives
|
|
50
|
+
* way first, then the age.
|
|
41
51
|
*/
|
|
42
52
|
const activeRow = (active: ActivePhase, current: boolean, context: SummaryContext, model?: PhaseModel): string => {
|
|
43
53
|
const { width, labels } = context
|
|
44
54
|
const { phase } = active
|
|
45
55
|
const title = phase.title === null ? paint.dim(TEXTS.notInPlan) : plain(phase.title)
|
|
46
|
-
const head = `${
|
|
56
|
+
const head = `${phaseLabel(phase.id)} ${title}`
|
|
47
57
|
const count = phaseCountCell(phase, current)
|
|
48
58
|
const withAge = `${ageCell(active, labels)}${count}`
|
|
49
59
|
const mark = modelMark(model)
|
|
@@ -53,16 +63,24 @@ const activeRow = (active: ActivePhase, current: boolean, context: SummaryContex
|
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
66
|
+
* One row per active task under a closed running phase, each with its own arrow: the title bold and the agent dim
|
|
67
|
+
* after it where agents reported it, `▷` and a plain title where the herd guesses. Always on one line whatever the
|
|
68
|
+
* task's fold, and a row a click opens like the task itself; `Review gate` in place of a box of the review gate.
|
|
58
69
|
*/
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
const taskLinesFrame = (context: SummaryContext, phaseRowKey: string, { phase, tasks }: ActivePhase): Frame => {
|
|
71
|
+
return joinFrames(
|
|
72
|
+
tasks.flatMap((task) => {
|
|
73
|
+
const item = phase.items[task.index]
|
|
74
|
+
if (!item) return []
|
|
75
|
+
const text = item.gate ? context.labels.reviewGate : plain(item.text)
|
|
76
|
+
const line = spread(
|
|
77
|
+
`${TASK_INDENT}${activeArrow(task.guessed)} ${activeTitle(text, task.guessed)}`,
|
|
78
|
+
agentCell(task.agent),
|
|
79
|
+
context.width,
|
|
80
|
+
)
|
|
81
|
+
return [taskFrame(itemKey(phaseRowKey, task.index), [line], context)]
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
66
84
|
}
|
|
67
85
|
|
|
68
86
|
/** A running phase as a row that opens to its tasks; closed, the task it is on below it. */
|
|
@@ -71,70 +89,70 @@ const activeFrame = (context: SummaryContext, active: ActivePhase): Frame => {
|
|
|
71
89
|
const stepIndex = active.stepIndex ?? -1
|
|
72
90
|
const key = phaseKey(progress.lane.name, stepIndex, active.phase.id)
|
|
73
91
|
const current = progress.current?.stepIndex === stepIndex && progress.current.phaseId === active.phase.id
|
|
74
|
-
const item = active.taskIndex === null ? null : (active.phase.items[active.taskIndex] ?? null)
|
|
75
92
|
const model = context.models[phaseModelKey(stepIndex, active.phase.id)]
|
|
76
93
|
const row = rowFrame(key, [activeRow(active, current, context, model)], [], context)
|
|
77
|
-
if (!state.expanded.has(key)) return joinFrames([row,
|
|
78
|
-
return joinFrames([
|
|
94
|
+
if (!state.expanded.has(key)) return joinFrames([row, taskLinesFrame(context, key, active)])
|
|
95
|
+
return joinFrames([
|
|
96
|
+
row,
|
|
97
|
+
nest(phaseChildren(deeper(context), key, active.phase, new Map(active.tasks.map((task) => [task.index, task])))),
|
|
98
|
+
])
|
|
79
99
|
}
|
|
80
100
|
|
|
81
|
-
/** `wave 2: P5 · P10` or, without waves, `P7`. */
|
|
82
|
-
const nextText = ({ next }: LaneSummary, labels: HerdTexts): string =>
|
|
83
|
-
next ? `${next.wave === null ? '' : `${labels.wave(next.wave)} `}${phaseLabels(next.phaseIds, ' · ')}` : ''
|
|
84
|
-
|
|
85
101
|
/** `P8 ?` for a named phase the plan lacks, `P9 –` for one without a checkbox. */
|
|
86
102
|
const unwrittenText = ({ id, missing }: LaneSummary['unwritten'][number]): string =>
|
|
87
103
|
`${phaseLabel(id)} ${paint.dim(missing ? PHASE_COUNT.missing : PHASE_COUNT.empty)}`
|
|
88
104
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
105
|
+
/**
|
|
106
|
+
* `next P5–P7, P10 P8 ? · Second plan: P1` on one line: every open phase that does not run, the current plan's as
|
|
107
|
+
* ranges, then the other plans' by their name; empty when none waits.
|
|
108
|
+
*/
|
|
109
|
+
const nextLineText = (summary: LaneSummary, labels: HerdTexts): string => {
|
|
110
|
+
const ownIds = summary.waiting.filter(({ planTitle }) => planTitle === null).flatMap(({ phaseIds }) => phaseIds)
|
|
111
|
+
const ownRanges = phaseRanges(ownIds)
|
|
112
|
+
const own = [...(ownRanges ? [ownRanges] : []), ...summary.unwritten.map(unwrittenText)]
|
|
113
|
+
const others = summary.waiting
|
|
96
114
|
.filter(({ planTitle }) => planTitle !== null)
|
|
97
|
-
.map(({ planTitle, phaseIds }) => `${plain((planTitle ?? '').split(':')[0]).trim()}: ${
|
|
98
|
-
|
|
115
|
+
.map(({ planTitle, phaseIds }) => `${plain((planTitle ?? '').split(':')[0]).trim()}: ${phaseRanges(phaseIds)}`)
|
|
116
|
+
const text = [...(own.length > 0 ? [own.join(' ')] : []), ...others].join(' · ')
|
|
117
|
+
return text ? `${paint.dim(labels.next)} ${text}` : ''
|
|
99
118
|
}
|
|
100
119
|
|
|
101
|
-
/** ` · not merged` once for the line when every finished phase waits for its merge, ` · P1
|
|
120
|
+
/** ` · not merged` once for the line when every finished phase waits for its merge, ` · P1–P2 not merged` when some do. */
|
|
102
121
|
const unmergedNote = ({ finished }: LaneSummary, labels: HerdTexts): string => {
|
|
103
122
|
const unmerged = finished.filter(({ unmerged }) => unmerged).map(({ id }) => id)
|
|
104
123
|
if (unmerged.length === 0) return ''
|
|
105
|
-
const which = unmerged.length === finished.length ? '' : `${
|
|
124
|
+
const which = unmerged.length === finished.length ? '' : `${phaseRanges(unmerged)} `
|
|
106
125
|
return ` · ${paint.yellow(`${which}${labels.notMerged}`)}`
|
|
107
126
|
}
|
|
108
127
|
|
|
109
|
-
/** `✓ P1
|
|
128
|
+
/** `✓ P1–P7, P10 · not merged` on one line: one ✓, the finished phases as ranges. */
|
|
110
129
|
const finishedText = (summary: LaneSummary, labels: HerdTexts): string => {
|
|
111
|
-
const phases = summary.finished.map(({ id }) =>
|
|
112
|
-
return phases ? `${phases}${unmergedNote(summary, labels)}` : ''
|
|
130
|
+
const phases = phaseRanges(summary.finished.map(({ id }) => id))
|
|
131
|
+
return phases ? `${paint.green(PHASE_COUNT.allTicked)} ${phases}${unmergedNote(summary, labels)}` : ''
|
|
113
132
|
}
|
|
114
133
|
|
|
115
|
-
/**
|
|
116
|
-
const
|
|
117
|
-
text ? labelled(labelWidth, label, textFrame([fit(text, Math.max(width - labelWidth, 1))])) : textFrame([])
|
|
134
|
+
/** One line of text cut at the pane's edge; no line when the text is empty. */
|
|
135
|
+
const lineFrame = (text: string, width: number): Frame => textFrame(text ? [fit(text, width)] : [])
|
|
118
136
|
|
|
119
|
-
/** What an open lane shows above its
|
|
120
|
-
export type LaneHead = { session:
|
|
137
|
+
/** What an open lane shows above its phases, drawn at the width of its branches: the session, its pull requests and its link chips. */
|
|
138
|
+
export type LaneHead = { session: Frame; pullRequests: Frame; chips: Frame }
|
|
121
139
|
|
|
122
140
|
/**
|
|
123
|
-
* The children of an open lane,
|
|
124
|
-
*
|
|
141
|
+
* The children of an open lane, one branch each, told apart by their marks rather than by a label: the session
|
|
142
|
+
* (`⎇`), the pull requests (`↗`), the link chips, then one branch per running phase (its tasks `▶`/`▷` under it), the
|
|
143
|
+
* `now:` lines, `next` with the open phases that wait, and `✓` with the finished ones; a branch without lines is left out.
|
|
125
144
|
*/
|
|
126
145
|
export const summaryChildren = (context: LaneContext, summary: LaneSummary, head: LaneHead): Frame[] => {
|
|
127
146
|
const labels = TEXTS.herd
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const { sections } = labels
|
|
131
|
-
const now = summary.active.map((active) => activeFrame(inner, active))
|
|
147
|
+
const inner = { ...context, labels }
|
|
148
|
+
const { width } = context
|
|
132
149
|
return [
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
150
|
+
head.session,
|
|
151
|
+
head.pullRequests,
|
|
152
|
+
head.chips,
|
|
153
|
+
...summary.active.map((active) => activeFrame(inner, active)),
|
|
154
|
+
...summary.unplanned.map((task) => textFrame([nowLine(task, width)])),
|
|
155
|
+
lineFrame(nextLineText(summary, labels), width),
|
|
156
|
+
lineFrame(finishedText(summary, labels), width),
|
|
139
157
|
]
|
|
140
158
|
}
|