jackherd 1.7.0 → 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 +43 -0
- package/INSTALL.md +4 -2
- 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/legacy-names.ts +8 -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/command.ts +16 -1
- 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/update/command.ts +4 -2
- package/src/update/update.ts +33 -9
- 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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { phaseLabel, phaseLabels } from '../plan-format'
|
|
2
|
+
import { fit, padVisible, paint, spread, visibleLength } from '../terminal/text'
|
|
3
|
+
import { textWidth } from '../terminal/width'
|
|
4
|
+
import { deeper, type Frame, joinFrames, nest, prefixed, rowFrame, textFrame } from './frame'
|
|
5
|
+
import type { ActivePhase, LaneSummary } from './lane-summary'
|
|
6
|
+
import {
|
|
7
|
+
ACTIVE_MARK,
|
|
8
|
+
COLUMN_SEPARATOR,
|
|
9
|
+
DEFERRED_MARK,
|
|
10
|
+
type LaneContext,
|
|
11
|
+
MIN_TITLE_WIDTH,
|
|
12
|
+
modelMark,
|
|
13
|
+
phaseChildren,
|
|
14
|
+
phaseCountCell,
|
|
15
|
+
plain,
|
|
16
|
+
taskFrame,
|
|
17
|
+
} from './layout-phase'
|
|
18
|
+
import { type PhaseModel, phaseModelKey } from './phase-models'
|
|
19
|
+
import { itemKey, phaseKey } from './selection'
|
|
20
|
+
import { PHASE_COUNT } from './symbols'
|
|
21
|
+
import { type HerdTexts, TEXTS } from './texts'
|
|
22
|
+
|
|
23
|
+
/** The context of a lane's sections: the lane's, with the herd's words. */
|
|
24
|
+
type SummaryContext = LaneContext & { labels: HerdTexts }
|
|
25
|
+
|
|
26
|
+
/** The label column of the summary's sections, as wide as the widest label and two cells. */
|
|
27
|
+
const sectionWidth = ({ sections }: HerdTexts): number =>
|
|
28
|
+
Math.max(...Object.values(sections).map(textWidth)) + COLUMN_SEPARATOR.length
|
|
29
|
+
|
|
30
|
+
/** A frame behind a section label on its first line, its other lines indented to where the content starts. */
|
|
31
|
+
const labelled = (labelWidth: number, label: string, frame: Frame): Frame =>
|
|
32
|
+
prefixed(frame, { first: padVisible(label, labelWidth), rest: ' '.repeat(labelWidth), style: paint.dim })
|
|
33
|
+
|
|
34
|
+
/** `· 6 min ago`, dim: when the running phase last moved. */
|
|
35
|
+
const ageCell = ({ minutesAgo }: ActivePhase, labels: HerdTexts): string =>
|
|
36
|
+
`${paint.dim(labels.minutesAgo(minutesAgo))}${COLUMN_SEPARATOR}`
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `▶ P12 <title> [model] · 6 min ago 16/26`: title at the left, model, age and count at the right edge. In a pane
|
|
40
|
+
* too narrow to leave the title `MIN_TITLE_WIDTH` cells, the model gives way first, then the age.
|
|
41
|
+
*/
|
|
42
|
+
const activeRow = (active: ActivePhase, current: boolean, context: SummaryContext, model?: PhaseModel): string => {
|
|
43
|
+
const { width, labels } = context
|
|
44
|
+
const { phase } = active
|
|
45
|
+
const title = phase.title === null ? paint.dim(TEXTS.notInPlan) : plain(phase.title)
|
|
46
|
+
const head = `${paint.yellow(ACTIVE_MARK)} ${phaseLabel(phase.id)} ${title}`
|
|
47
|
+
const count = phaseCountCell(phase, current)
|
|
48
|
+
const withAge = `${ageCell(active, labels)}${count}`
|
|
49
|
+
const mark = modelMark(model)
|
|
50
|
+
const candidates = [...(mark ? [`${mark}${COLUMN_SEPARATOR}${withAge}`] : []), withAge]
|
|
51
|
+
const titleRoom = (right: string) => width - visibleLength(head) + visibleLength(title) - visibleLength(right) - 1
|
|
52
|
+
return spread(head, candidates.find((right) => titleRoom(right) >= MIN_TITLE_WIDTH) ?? count, width)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* `→ <task>` under a closed running phase: the task it is on, dim, on one line whatever the task's fold, and a row a
|
|
57
|
+
* click opens like the task itself; `→ review gate` in place of a box of the review gate; nothing when no task is open.
|
|
58
|
+
*/
|
|
59
|
+
const taskLineFrame = (context: SummaryContext, phaseRowKey: string, { phase, taskIndex }: ActivePhase): Frame => {
|
|
60
|
+
const item = taskIndex === null ? undefined : phase.items[taskIndex]
|
|
61
|
+
if (!item || taskIndex === null) return textFrame([])
|
|
62
|
+
const text = item.gate ? context.labels.reviewGate : plain(item.text)
|
|
63
|
+
const underTitle = ' '.repeat(visibleLength(ACTIVE_MARK) + 1)
|
|
64
|
+
const line = fit(`${underTitle}${paint.dim(`${DEFERRED_MARK} ${text}`)}`, context.width)
|
|
65
|
+
return taskFrame(itemKey(phaseRowKey, taskIndex), [line], context)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A running phase as a row that opens to its tasks; closed, the task it is on below it. */
|
|
69
|
+
const activeFrame = (context: SummaryContext, active: ActivePhase): Frame => {
|
|
70
|
+
const { progress, state } = context
|
|
71
|
+
const stepIndex = active.stepIndex ?? -1
|
|
72
|
+
const key = phaseKey(progress.lane.name, stepIndex, active.phase.id)
|
|
73
|
+
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
|
+
const model = context.models[phaseModelKey(stepIndex, active.phase.id)]
|
|
76
|
+
const row = rowFrame(key, [activeRow(active, current, context, model)], [], context)
|
|
77
|
+
if (!state.expanded.has(key)) return joinFrames([row, taskLineFrame(context, key, active)])
|
|
78
|
+
return joinFrames([row, nest(phaseChildren(deeper(context), key, active.phase, item))])
|
|
79
|
+
}
|
|
80
|
+
|
|
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
|
+
/** `P8 ?` for a named phase the plan lacks, `P9 –` for one without a checkbox. */
|
|
86
|
+
const unwrittenText = ({ id, missing }: LaneSummary['unwritten'][number]): string =>
|
|
87
|
+
`${phaseLabel(id)} ${paint.dim(missing ? PHASE_COUNT.missing : PHASE_COUNT.empty)}`
|
|
88
|
+
|
|
89
|
+
/** `P6 P7 P8 ? · Second plan: P1`: the current plan's waiting phases, then the other plans' by their name. */
|
|
90
|
+
const laterText = ({ later, unwritten }: LaneSummary): string => {
|
|
91
|
+
const own = [
|
|
92
|
+
...(later[0]?.planTitle === null ? [phaseLabels(later[0].phaseIds, ' ')] : []),
|
|
93
|
+
...unwritten.map(unwrittenText),
|
|
94
|
+
]
|
|
95
|
+
const others = later
|
|
96
|
+
.filter(({ planTitle }) => planTitle !== null)
|
|
97
|
+
.map(({ planTitle, phaseIds }) => `${plain((planTitle ?? '').split(':')[0]).trim()}: ${phaseLabels(phaseIds, ' ')}`)
|
|
98
|
+
return [...(own.length > 0 ? [own.join(' ')] : []), ...others].join(' · ')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** ` · not merged` once for the line when every finished phase waits for its merge, ` · P1 P2 not merged` when some do. */
|
|
102
|
+
const unmergedNote = ({ finished }: LaneSummary, labels: HerdTexts): string => {
|
|
103
|
+
const unmerged = finished.filter(({ unmerged }) => unmerged).map(({ id }) => id)
|
|
104
|
+
if (unmerged.length === 0) return ''
|
|
105
|
+
const which = unmerged.length === finished.length ? '' : `${phaseLabels(unmerged, ' ')} `
|
|
106
|
+
return ` · ${paint.yellow(`${which}${labels.notMerged}`)}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `✓ P1 ✓ P4 · not merged` on one line. */
|
|
110
|
+
const finishedText = (summary: LaneSummary, labels: HerdTexts): string => {
|
|
111
|
+
const phases = summary.finished.map(({ id }) => `${paint.green(PHASE_COUNT.allTicked)} ${phaseLabel(id)}`).join(' ')
|
|
112
|
+
return phases ? `${phases}${unmergedNote(summary, labels)}` : ''
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A section with one line of text, cut at the pane's edge; no frame when the text is empty. */
|
|
116
|
+
const textSection = (labelWidth: number, label: string, text: string, width: number): Frame =>
|
|
117
|
+
text ? labelled(labelWidth, label, textFrame([fit(text, Math.max(width - labelWidth, 1))])) : textFrame([])
|
|
118
|
+
|
|
119
|
+
/** What an open lane shows above its sections, each drawn at the width its label column leaves. */
|
|
120
|
+
export type LaneHead = { session: (width: number) => string; chips: (width: number) => Frame }
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The children of an open lane, each on a branch with its label in one column: SESSION, the 📎 chips, then NOW, NEXT,
|
|
124
|
+
* LATER and DONE, each section only when it has phases.
|
|
125
|
+
*/
|
|
126
|
+
export const summaryChildren = (context: LaneContext, summary: LaneSummary, head: LaneHead): Frame[] => {
|
|
127
|
+
const labels = TEXTS.herd
|
|
128
|
+
const labelWidth = sectionWidth(labels)
|
|
129
|
+
const inner = { ...context, width: Math.max(context.width - labelWidth, 1), labels }
|
|
130
|
+
const { sections } = labels
|
|
131
|
+
const now = summary.active.map((active) => activeFrame(inner, active))
|
|
132
|
+
return [
|
|
133
|
+
labelled(labelWidth, sections.session, textFrame([head.session(inner.width)])),
|
|
134
|
+
labelled(labelWidth, sections.chips, head.chips(inner.width)),
|
|
135
|
+
...(now.length > 0 ? [labelled(labelWidth, sections.now, joinFrames(now))] : []),
|
|
136
|
+
textSection(labelWidth, sections.next, nextText(summary, labels), context.width),
|
|
137
|
+
textSection(labelWidth, sections.later, laterText(summary), context.width),
|
|
138
|
+
textSection(labelWidth, sections.done, finishedText(summary, labels), context.width),
|
|
139
|
+
]
|
|
140
|
+
}
|