jackherd 1.9.0 → 1.11.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 +49 -0
- package/INSTALL.md +1 -1
- package/README.md +65 -32
- package/dist/{chunk-MCP4XJQH.js → chunk-RMMXD4G5.js} +18 -6
- package/dist/config.d.ts +99 -4
- package/dist/config.js +105 -29
- package/dist/plan-format.d.ts +51 -6
- package/dist/plan-format.js +49 -7
- 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 +57 -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/rules/rules/kit/pull-requests.md +2 -0
- package/kit/workflow/skills/follow/SKILL.md +3 -10
- package/kit/workflow/skills/implement/SKILL.md +8 -6
- package/kit/workflow/skills/orchestrate/SKILL.md +218 -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 +5 -1
- package/schemas/jackherd.schema.json +139 -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 +2 -0
- package/src/config/auto-window.ts +8 -0
- package/src/config/link-kinds.ts +7 -1
- package/src/config/schema.ts +82 -25
- package/src/config/tones.ts +27 -0
- 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/additions.ts +14 -0
- package/src/lane-prompt/build.ts +74 -0
- package/src/lane-prompt/command.ts +93 -0
- package/src/lane-prompt/input.ts +157 -0
- package/src/lane-prompt/options.ts +70 -0
- package/src/lane-prompt/texts.ts +101 -0
- package/src/mermaid/command.ts +36 -0
- package/src/mermaid/problems.ts +48 -0
- package/src/plan-format/clock.ts +8 -0
- package/src/plan-format/dependencies.ts +9 -6
- package/src/plan-format/index.ts +1 -0
- package/src/plan-format/short-names.ts +71 -0
- package/src/plan-format/waiting.ts +22 -4
- package/src/plans/appointment-rows.ts +83 -0
- package/src/plans/ask-coordinator.ts +111 -0
- package/src/plans/ask-target.ts +36 -0
- package/src/plans/board-close.ts +85 -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/frame.ts +5 -12
- package/src/plans/git.ts +54 -5
- package/src/plans/groomed.ts +27 -0
- 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 +36 -11
- package/src/plans/layout-summary.ts +89 -18
- package/src/plans/layout.ts +502 -98
- package/src/plans/link-chips.ts +23 -4
- package/src/plans/live.ts +178 -23
- package/src/plans/next-rows.ts +44 -0
- 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/planned.ts +22 -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/row-actions.ts +52 -0
- package/src/plans/row-focus.ts +148 -0
- package/src/plans/row-targets.ts +7 -0
- package/src/plans/selection.ts +111 -18
- package/src/plans/sent-files-read.ts +6 -15
- package/src/plans/sent-files.ts +7 -14
- package/src/plans/settings.ts +12 -1
- package/src/plans/signalled-phase.ts +42 -0
- package/src/plans/signals-read.ts +12 -4
- package/src/plans/snapshot.ts +162 -81
- package/src/plans/source-asked.ts +19 -0
- package/src/plans/source-prefix.ts +17 -0
- package/src/plans/source-rows.ts +188 -0
- 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 +48 -6
- package/src/plans/tones.ts +30 -0
- package/src/plans/view.ts +25 -11
- package/src/plans/waiting-click.ts +295 -35
- 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 +16 -3
- package/src/terminal/types.ts +4 -0
- package/src/version/skill-diff.ts +46 -0
- package/src/vocabulary/index.ts +26 -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 +22 -9
- package/src/watch/plan-badges.ts +10 -2
- package/src/watch/pull-requests.ts +6 -2
- package/src/watch/reference-paths.ts +87 -0
- package/src/watch/report-questions.ts +4 -3
- 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/source-entries.ts +55 -0
- package/src/watch/source-fields.ts +1 -0
- package/src/watch/source-statuses.ts +60 -0
- package/src/watch/source-tickets.ts +21 -1
- package/src/watch/sources.ts +47 -4
- 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/plans/supervisor.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname } from 'node:path'
|
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { pendingKitUpdate, VERSION_POLL_MS } from '../version/installed-version'
|
|
6
6
|
import type { StartedKit } from '../version/started-kit'
|
|
7
|
+
import { STOP_REQUEST, type Stopper, stopperOf } from './child-stop'
|
|
7
8
|
import { HERD_COMMAND } from './herd-command'
|
|
8
9
|
import { CHILD_MARKER, exitCodeOf, isRestartTrigger, nextStep, RESTART_DEBOUNCE_MS } from './restart-decision'
|
|
9
10
|
import { TERMINAL_OFF } from './view'
|
|
@@ -30,7 +31,8 @@ const spawnView = (scriptPath: string, args: string[]): ChildProcess =>
|
|
|
30
31
|
/**
|
|
31
32
|
* Runs the live view as a child process and restarts it when the package's own source changes,
|
|
32
33
|
* so a long-lived `plans` never shows the behaviour of code that has since been edited. The child
|
|
33
|
-
* stops on SIGTERM through the handler that restores the terminal
|
|
34
|
+
* stops on SIGTERM through the handler that restores the terminal, and is killed when it does not end within
|
|
35
|
+
* `GRACE_MS` (a restart included); a second signal while stopping kills it at once. Resolves with the exit code
|
|
34
36
|
* of a wanted end only: the child's clean exit (`q`) or a signal forwarded to it. A child that
|
|
35
37
|
* dies otherwise leaves the supervisor waiting, with the terminal restored, for the next change
|
|
36
38
|
* to `src/`, which spawns a fresh child. `kit` is the package this process loaded (null when it cannot be
|
|
@@ -45,6 +47,7 @@ export const superviseView = (
|
|
|
45
47
|
): Promise<number> =>
|
|
46
48
|
new Promise((resolveExit) => {
|
|
47
49
|
let child: ChildProcess | undefined
|
|
50
|
+
let stopper: Stopper | undefined
|
|
48
51
|
let restarting = false
|
|
49
52
|
let stopping = false
|
|
50
53
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
@@ -66,6 +69,12 @@ export const superviseView = (
|
|
|
66
69
|
const start = (isFirst: boolean): void => {
|
|
67
70
|
const current = spawnView(scriptPath, args)
|
|
68
71
|
child = current
|
|
72
|
+
const currentStopper = stopperOf({
|
|
73
|
+
kill: (signal) => current.kill(signal),
|
|
74
|
+
setTimer: (run, ms) => setTimeout(run, ms),
|
|
75
|
+
clearTimer: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
76
|
+
})
|
|
77
|
+
stopper = currentStopper
|
|
69
78
|
|
|
70
79
|
current.on('error', (error) => {
|
|
71
80
|
if (child !== current) return
|
|
@@ -79,6 +88,7 @@ export const superviseView = (
|
|
|
79
88
|
})
|
|
80
89
|
|
|
81
90
|
current.on('exit', (code, signal) => {
|
|
91
|
+
currentStopper.ended()
|
|
82
92
|
if (child !== current) return
|
|
83
93
|
const context = { restarting, stopping, code, signal }
|
|
84
94
|
restarting = false
|
|
@@ -94,7 +104,7 @@ export const superviseView = (
|
|
|
94
104
|
const restartView = (): void => {
|
|
95
105
|
if (child) {
|
|
96
106
|
restarting = true
|
|
97
|
-
|
|
107
|
+
stopper?.request('SIGTERM')
|
|
98
108
|
} else start(false)
|
|
99
109
|
}
|
|
100
110
|
|
|
@@ -118,8 +128,8 @@ export const superviseView = (
|
|
|
118
128
|
stopping = true
|
|
119
129
|
clearTimeout(timer)
|
|
120
130
|
clearInterval(versionTimer)
|
|
121
|
-
if (child)
|
|
122
|
-
else finish(exitCodeOf(null, signal))
|
|
131
|
+
if (!child || !stopper) finish(exitCodeOf(null, signal))
|
|
132
|
+
else if (stopper.request(signal) === STOP_REQUEST.killed) finish(exitCodeOf(null, signal))
|
|
123
133
|
})
|
|
124
134
|
}
|
|
125
135
|
})
|
package/src/plans/symbols.ts
CHANGED
|
@@ -21,6 +21,9 @@ export const statusWord = (agent: string | null): string => agent ?? NO_AGENT_WO
|
|
|
21
21
|
/** Before a row that opens and closes: lanes, the planned phases, the ticked items of a phase. */
|
|
22
22
|
export const CHEVRON = { closed: '▸', open: '▾' } as const
|
|
23
23
|
|
|
24
|
+
/** Before the count under a block that shows all its rows; a click folds it back to five. */
|
|
25
|
+
export const LESS_MARK = '▴'
|
|
26
|
+
|
|
24
27
|
export const chevronOf = (open: boolean): string => (open ? CHEVRON.open : CHEVRON.closed)
|
|
25
28
|
|
|
26
29
|
export const PHASE_STATE = { missing: 'missing', empty: 'empty', all: 'all', open: 'open' } as const
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const TASK_NAME = /^\*\*([^*]+)\*\*:/
|
|
2
|
+
|
|
3
|
+
const TITLE_END = ': '
|
|
4
|
+
|
|
5
|
+
const titleEndOf = (text: string): number => {
|
|
6
|
+
let inCode = false
|
|
7
|
+
for (let index = 0; index < text.length; index++) {
|
|
8
|
+
if (text[index] === '`') inCode = !inCode
|
|
9
|
+
else if (!inCode && text.startsWith(TITLE_END, index)) return index
|
|
10
|
+
}
|
|
11
|
+
return -1
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The text up to its first colon followed by a space outside backticks, as a folded row shows it; the text as it is
|
|
16
|
+
* without such a colon or with nothing before it. A time like `18:55` has no space after its colon and does not split.
|
|
17
|
+
*/
|
|
18
|
+
export const shortTitle = (text: string): string => {
|
|
19
|
+
const end = titleEndOf(text)
|
|
20
|
+
return end > 0 ? text.slice(0, end) : text
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The short name a task carries in bold at its start, `**Name**: content`, as written; null for a line without one.
|
|
25
|
+
* Nothing is guessed from the text up to its first colon: a path at the start of a line would read as a name.
|
|
26
|
+
*/
|
|
27
|
+
export const taskNameOf = (text: string): string | null => {
|
|
28
|
+
const name = text.match(TASK_NAME)?.[1]?.trim()
|
|
29
|
+
return name ? name : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The text after a task's `**Name**:`, as an opened task shows it under its name; the text as it is without a name. */
|
|
33
|
+
export const taskBodyOf = (text: string): string =>
|
|
34
|
+
taskNameOf(text) === null ? text : text.replace(TASK_NAME, '').trimStart()
|
package/src/plans/texts.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { STALE_ACTIVITY_MINUTES } from './lane-summary'
|
|
2
|
+
import { STALE_PLAN_DAYS } from './planned'
|
|
2
3
|
import { CHECK_NAME_LENGTH, CHECKS, type ChecksState, type PullRequest } from './pull-requests'
|
|
3
4
|
import { shownWithMore } from './shown-with-more'
|
|
4
5
|
import { MODEL_MARK, NO_AGENT_WORD, PHASE_COUNT } from './symbols'
|
|
5
6
|
|
|
6
|
-
/** The words of an open lane: the age of a running phase, the
|
|
7
|
+
/** The words of an open lane: the age of a running phase, the words before the open phases that wait and the finished ones, and the pointer of a phase with only its review gate open. */
|
|
7
8
|
export type HerdTexts = {
|
|
8
9
|
minutesAgo: (minutes: number) => string
|
|
9
10
|
next: string
|
|
10
|
-
|
|
11
|
+
done: string
|
|
11
12
|
reviewGate: string
|
|
12
13
|
gate: string
|
|
13
14
|
}
|
|
@@ -23,20 +24,45 @@ export type PlanTexts = {
|
|
|
23
24
|
noPlan: string
|
|
24
25
|
noWorktree: string
|
|
25
26
|
noRow: string
|
|
27
|
+
/** The heads of the blocks on top of the herd. */
|
|
28
|
+
waitingHeading: string
|
|
29
|
+
appointmentHeading: string
|
|
30
|
+
sourceHeading: string
|
|
31
|
+
nextHeading: string
|
|
32
|
+
/** On a resting lane's line: the label before its starts-after cell, and the line for a lane without any reason. */
|
|
33
|
+
restAfter: string
|
|
34
|
+
restNoReason: string
|
|
35
|
+
/** After the head of the next block when the board's section holds no line. */
|
|
36
|
+
nextEmpty: string
|
|
26
37
|
notInPlan: string
|
|
27
38
|
/** Before a task an agent reports that no task of the plan names. */
|
|
28
39
|
now: string
|
|
40
|
+
/** Before the description of a running subagent that names no phase and sits on none. */
|
|
41
|
+
subagent: string
|
|
29
42
|
/** In place of the count of a phase whose only open box is the documentation box of its review gate. */
|
|
30
43
|
allButDocs: string
|
|
31
44
|
tickedSummary: (count: number) => string
|
|
45
|
+
/** The line above the listed ticked tasks of a phase; a click or `a` folds them again. */
|
|
46
|
+
tickedFold: (count: number) => string
|
|
32
47
|
draft: string
|
|
33
48
|
privateLink: string
|
|
34
49
|
planned: (phases: number, plans: number) => string
|
|
50
|
+
/** The head of the planned section in the herd: `planned · 3 plans, next:`. */
|
|
51
|
+
plannedHead: (plans: number) => string
|
|
35
52
|
phaseTally: (done: number, total: number) => string
|
|
36
53
|
herd: HerdTexts
|
|
37
54
|
checks: Record<ChecksState, string>
|
|
38
55
|
hint: string
|
|
56
|
+
/** The notice when `a` is pressed with no phase open, where the key changes nothing on screen. */
|
|
57
|
+
showDoneNeedsPhase: string
|
|
39
58
|
legend: string[]
|
|
59
|
+
/** The footer line when the coordinator could not be asked to put a question, with the reason. */
|
|
60
|
+
notAsked: (reason: string) => string
|
|
61
|
+
/** The footer line when the coordinator `name` was asked to put `subject`, `question <id>` or `board row <key>`. */
|
|
62
|
+
asked: (name: string, subject: string) => string
|
|
63
|
+
/** The footer line after `[done]` closed `subject`, and the one with the reason when it could not. */
|
|
64
|
+
closed: (subject: string) => string
|
|
65
|
+
notClosed: (reason: string) => string
|
|
40
66
|
}
|
|
41
67
|
|
|
42
68
|
/** The words of the watcher's badges and of the header notice when they are missing. */
|
|
@@ -78,38 +104,54 @@ export const TEXTS: PlanTexts = {
|
|
|
78
104
|
noPlan: 'no plan',
|
|
79
105
|
noWorktree: 'no worktree',
|
|
80
106
|
noRow: 'no row in lanes',
|
|
107
|
+
waitingHeading: 'Waiting for you',
|
|
108
|
+
appointmentHeading: 'Appointments',
|
|
109
|
+
sourceHeading: 'Sources',
|
|
110
|
+
nextHeading: 'Next',
|
|
111
|
+
restAfter: 'after:',
|
|
112
|
+
restNoReason: 'idle · no reason',
|
|
113
|
+
nextEmpty: 'nothing queued, groom the board',
|
|
81
114
|
notInPlan: 'not in the plan',
|
|
82
115
|
now: 'now:',
|
|
116
|
+
subagent: 'Subagent:',
|
|
83
117
|
allButDocs: ALL_BUT_DOCS,
|
|
84
118
|
tickedSummary: (count) => `${count} ticked · click or a lists them`,
|
|
119
|
+
tickedFold: (count) => `${count} ticked · click or a folds them`,
|
|
85
120
|
draft: 'draft · ',
|
|
86
121
|
privateLink: 'private',
|
|
87
122
|
phaseTally: (done, total) => (total > 0 ? `${done}/${total} ph` : ''),
|
|
88
123
|
herd: {
|
|
89
124
|
minutesAgo: (minutes) => (minutes === 0 ? '· just now' : `· ${minutes} min ago`),
|
|
90
125
|
next: 'next',
|
|
91
|
-
|
|
126
|
+
done: 'done',
|
|
92
127
|
reviewGate: 'Review gate',
|
|
93
128
|
gate: '· gate',
|
|
94
129
|
},
|
|
95
130
|
planned: (phases, plans) =>
|
|
96
131
|
`planned (${phases} ${phases === 1 ? 'phase' : 'phases'} in ${plans} ${plans === 1 ? 'plan' : 'plans'})`,
|
|
132
|
+
plannedHead: (plans) => `planned · ${plans} ${plans === 1 ? 'plan' : 'plans'}, next:`,
|
|
97
133
|
checks: {
|
|
98
134
|
[CHECKS.passed]: '✓ checks',
|
|
99
135
|
[CHECKS.failed]: '✗ checks',
|
|
100
136
|
[CHECKS.pending]: '… checks',
|
|
101
137
|
[CHECKS.none]: 'no checks',
|
|
102
138
|
},
|
|
103
|
-
hint: '↑↓ select →← open a
|
|
139
|
+
hint: '↑↓ select →← open a/click ticked tasks s html q quit ? help',
|
|
140
|
+
showDoneNeedsPhase: 'a lists the ticked tasks inside an open phase: open one with → or a click',
|
|
104
141
|
legend: [
|
|
105
142
|
'enter/space or click toggles a lane or a phase, click or enter on a task opens its whole text, and opens a ↗ pull request · wheel PgUp PgDn scroll · j k h l like the arrows · click or ctrl+click on a link chip opens its link · ✦ artifact · ⑂ pull request or issue · ◈ Figma · private = the plan says so',
|
|
106
143
|
'● working · ◆ blocked, waits for you · ◉ done = the agent finished its answer, not the lane · ○ idle · (·) no agent = a worktree nobody works in · • state unknown · only blocked wears its word in the lane line, the symbol says the rest · waiting = not started: no worktree yet, something left to do, and what it starts after; finished lanes (cleaned up, or every named phase ticked) are not shown · a working or blocked lane opens by itself only when it first appears, after that → ← and clicks are yours alone',
|
|
107
|
-
`▸ ▾ open and close · an open lane's branches, top down: the agent's title and ⎇ its branch, ↗ its pull requests, its link chips, the phases it works on, next,
|
|
144
|
+
`▸ ▾ open and close · an open lane's branches, top down: the agent's title and ⎇ its branch, ↗ its pull requests, its link chips, the phases it works on, next, done · a lane shows only the phases its board rows name · a phase row = some of its tasks are ticked, one of its boxes was ticked in the last ${STALE_ACTIVITY_MINUTES} min, or an agent reports working on it, by number · n min ago since the last tick or report, · gate = only the review gate is open · ▶ teal = a task an agent reported (its task list or jh task; its name dim at the right edge when several agents work in the lane), ▷ dim = only guessed from the words of its log · now: <text> = a reported task no plan task names · a running review sits on the gate box that names it, in the phase it names or else the lane's current one · Subagent: <text> = a running subagent that names no phase and sits on none · next = the other open phases, as ranges · done = the finished ones · ${ALL_BUT_DOCS} = only the documentation box is open · a folded lane line names the phase moved last, +n more active ones, and its count · ${PHASE_COUNT.empty} no checkboxes · ${PHASE_COUNT.missing} not in the plan · a or a click on its ▸ n ticked line lists the ticked checkboxes of an open phase one by one, a or a click on its ▾ line folds them · s opens every lane and plan as one HTML page`,
|
|
108
145
|
`the bar = progress over the lane's plans · n/m = m checkboxes, n ticked · n/m ph = n of m phases finished · n/n in green = all ticked · 🥣 share of the context window used, on a folded lane only once it warns · Sonnet ${MODEL_MARK.differs} Opus in amber = the phase's subagent ran on another model than the plan's`,
|
|
109
146
|
'↗ pull request · 💬 new feedback · ✋ changes requested · 🌐 preview · 📡 ticket: chip 📡 T-1 · status among the link chips, 📡 <n> in amber on a folded lane when tickets are new',
|
|
110
|
-
|
|
147
|
+
`▌ on top = waits for your answer: 📎 a file or an Artifact that was sent (click: a cut-off row expands, then it opens), ? a question of the board or one a session left in its dialog (a click on its text only expands and folds it; [ask] at the right edge of a question asks the coordinator to put it; [done] closes the row: a board row gets its status, a sent file its record) · it goes when the board row is closed or \`jh waiting close <id>\` · \`Waiting for you\` heads the block (click: fold; click on +n: show all, on ▴ n: five) · ◷ the appointments of the board in a grey bar below it (click: fold; [done] closes the row, [run] asks the coordinator to run it now, [move] to ask you for a new time) · 1 2 3 = Next, the board lines to do next · ● Sources = the watcher’s sources that are no tickets: when each ran and its newest report, the mark amber while that report is under a day old and red while the source fails (click on a row: opens the entry) · planned = the open phases no lane works on, the next one per plan; stale? = its plan file was not changed on the base branch for ${STALE_PLAN_DAYS} days`,
|
|
111
148
|
'⎇ branch · ↑ commits ahead · ± uncommitted · no worktree: plan read from main',
|
|
149
|
+
'❯ and a light background = the row under the mouse or the arrows, on every clickable row · enter on a block row asks, opens or expands it and never presses [done] · [xx] in square brackets = a click does something, bold under the mouse',
|
|
112
150
|
],
|
|
151
|
+
notAsked: (reason) => `no coordinator to ask: ${reason}`,
|
|
152
|
+
asked: (name, subject) => `asked ${name} for ${subject}`,
|
|
153
|
+
closed: (subject) => `closed ${subject}`,
|
|
154
|
+
notClosed: (reason) => `not closed: ${reason}`,
|
|
113
155
|
}
|
|
114
156
|
|
|
115
157
|
/** The mark of a running lane the board has no row for; null for a lane it lists. */
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DEFAULT_TONES, TONE, type ToneName, type ToneRole } from '../config/tones'
|
|
2
|
+
import { paint } from '../terminal/text'
|
|
3
|
+
|
|
4
|
+
const TONE_PAINT: Record<ToneName, (text: string) => string> = {
|
|
5
|
+
[TONE.amber]: paint.yellow,
|
|
6
|
+
[TONE.teal]: paint.teal,
|
|
7
|
+
[TONE.grey]: paint.dim,
|
|
8
|
+
[TONE.red]: paint.red,
|
|
9
|
+
[TONE.green]: paint.green,
|
|
10
|
+
[TONE.blue]: paint.blue,
|
|
11
|
+
[TONE.magenta]: paint.magenta,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let palette: Record<ToneRole, ToneName> = DEFAULT_TONES
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sets the tone of each role for the screens drawn from now on. The herd calls it once per frame with the config's
|
|
18
|
+
* tones, so the layout reads one palette and no function of it takes the colours as a parameter.
|
|
19
|
+
*/
|
|
20
|
+
export const setTones = (tones: Record<ToneRole, ToneName>): void => {
|
|
21
|
+
palette = tones
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const paintOfTone = (name: ToneName): ((text: string) => string) => TONE_PAINT[name]
|
|
25
|
+
|
|
26
|
+
/** The paint of a role in the palette set last. */
|
|
27
|
+
export const paintOfRole = (role: ToneRole): ((text: string) => string) => TONE_PAINT[palette[role]]
|
|
28
|
+
|
|
29
|
+
/** The tone name of a role in the palette set last. */
|
|
30
|
+
export const toneOfRole = (role: ToneRole): ToneName => palette[role]
|
package/src/plans/view.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { ARTIFACT_LINK_KIND } from '../config/link-kinds'
|
|
2
|
+
import { DEFAULT_TONES } from '../config/tones'
|
|
1
3
|
import { fit, plainText } from '../terminal/text'
|
|
2
4
|
import { renderSelected, type Screen } from './layout'
|
|
3
5
|
import type { Options } from './options'
|
|
4
6
|
import { laneSelection, overviewSelection, type Selection } from './selection'
|
|
5
7
|
import type { Snapshot } from './snapshot'
|
|
8
|
+
import { setTones } from './tones'
|
|
6
9
|
|
|
7
10
|
const DEFAULT_WIDTH = 100
|
|
8
11
|
const CURSOR_HOME = '\x1b[H'
|
|
9
12
|
const CLEAR_LINE_END = '\x1b[K'
|
|
10
13
|
const CLEAR_BELOW = '\x1b[J'
|
|
11
14
|
|
|
12
|
-
export const TERMINAL_ON = '\x1b[?1049h\x1b[?25l\x1b[?1000h\x1b[?1006h'
|
|
13
|
-
export const TERMINAL_OFF = '\x1b[?1006l\x1b[?1000l\x1b[?25h\x1b[?1049l'
|
|
15
|
+
export const TERMINAL_ON = '\x1b[?1049h\x1b[?25l\x1b[?1000h\x1b[?1003h\x1b[?1006h'
|
|
16
|
+
export const TERMINAL_OFF = '\x1b[?1006l\x1b[?1003l\x1b[?1000l\x1b[?25h\x1b[?1049l'
|
|
14
17
|
|
|
15
18
|
/** One column short of the pane: a line filling the last column makes some terminals wrap it. */
|
|
16
19
|
export const paneWidth = (): number => (process.stdout.columns ?? (Number(process.env.COLUMNS) || DEFAULT_WIDTH)) - 1
|
|
@@ -20,7 +23,9 @@ const paneRows = (): number => process.stdout.rows ?? Number.MAX_SAFE_INTEGER
|
|
|
20
23
|
export const bodyRows = (screen: Screen): number =>
|
|
21
24
|
Math.max(paneRows() - screen.header.length - screen.footer.length, 1)
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
/** The time as `HH:MM`, in the header and in the refresh error alike: a frame differs only once a minute. */
|
|
27
|
+
export const clockOf = (locale: string, now: Date = new Date()): string =>
|
|
28
|
+
now.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })
|
|
24
29
|
|
|
25
30
|
export const renderSnapshot = (
|
|
26
31
|
snapshot: Snapshot,
|
|
@@ -29,6 +34,7 @@ export const renderSnapshot = (
|
|
|
29
34
|
width: number,
|
|
30
35
|
locale: string,
|
|
31
36
|
): Screen => {
|
|
37
|
+
setTones(snapshot.tones ?? DEFAULT_TONES)
|
|
32
38
|
const screen = renderSelected(
|
|
33
39
|
{
|
|
34
40
|
title: snapshot.title,
|
|
@@ -38,6 +44,10 @@ export const renderSnapshot = (
|
|
|
38
44
|
watch: snapshot.watch,
|
|
39
45
|
watchesTickets: snapshot.watchesTickets,
|
|
40
46
|
awaiting: snapshot.waiting,
|
|
47
|
+
groomedDays: snapshot.groomedDays,
|
|
48
|
+
next: snapshot.next,
|
|
49
|
+
appointments: snapshot.appointments,
|
|
50
|
+
sourceBlock: snapshot.sourceBlock,
|
|
41
51
|
unplaced: snapshot.unplaced,
|
|
42
52
|
},
|
|
43
53
|
selection,
|
|
@@ -50,19 +60,23 @@ export const startSelection = (snapshot: Snapshot, options: Options): Selection
|
|
|
50
60
|
options.lane ? laneSelection(snapshot.views[0]?.progress) : overviewSelection()
|
|
51
61
|
|
|
52
62
|
/** Header and footer stay put; the body shows `bodyRows` lines from `top`, padded so the footer sits at the bottom. */
|
|
53
|
-
export const
|
|
54
|
-
const width = paneWidth()
|
|
55
|
-
const rows = bodyRows(screen)
|
|
63
|
+
export const frameOutput = (screen: Screen, top: number, width: number, rows: number): string => {
|
|
56
64
|
const body = screen.body.lines.slice(top, top + rows)
|
|
57
65
|
const padding = Array.from({ length: Math.max(rows - body.length, 0) }, () => '')
|
|
58
66
|
const lines = [...screen.header, ...body, ...padding, ...screen.footer]
|
|
59
|
-
|
|
60
|
-
`${CURSOR_HOME}${lines.map((line) => `${fit(line, width)}${CLEAR_LINE_END}`).join('\r\n')}${CLEAR_BELOW}`,
|
|
61
|
-
)
|
|
67
|
+
return `${CURSOR_HOME}${lines.map((line) => `${fit(line, width)}${CLEAR_LINE_END}`).join('\r\n')}${CLEAR_BELOW}`
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
/**
|
|
70
|
+
/** The frame to write, or null when the terminal already shows it; a null `previous` means the terminal content is unknown. */
|
|
71
|
+
export const frameToWrite = (previous: string | null, next: string): string | null => (previous === next ? null : next)
|
|
72
|
+
|
|
73
|
+
const ARTIFACT_ADDRESS = new RegExp(ARTIFACT_LINK_KIND.pattern)
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* One frame as text; ANSI styling is dropped when stdout is not a terminal, and links are written out as their
|
|
77
|
+
* address, except an Artifact's, which is long and would push the rest of its line past the pane.
|
|
78
|
+
*/
|
|
65
79
|
export const frameText = (screen: Screen, isTerminal: boolean): string => {
|
|
66
80
|
const lines = [...screen.header, ...screen.body.lines, ...screen.footer]
|
|
67
|
-
return (isTerminal ? lines : lines.map(plainText)).join('\n')
|
|
81
|
+
return (isTerminal ? lines : lines.map((line) => plainText(line, (url) => !ARTIFACT_ADDRESS.test(url)))).join('\n')
|
|
68
82
|
}
|