jackherd 1.16.0 → 1.17.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 +22 -0
- package/INSTALL.md +2 -1
- package/README.md +22 -21
- package/dist/{chunk-4LUFR7RC.js → chunk-N5FAOQZL.js} +7 -0
- package/dist/config.d.ts +33 -1
- package/dist/config.js +57 -5
- package/dist/plan-format.d.ts +18 -6
- package/dist/plan-format.js +8 -1
- package/docs/plan-contract.md +1 -0
- package/herdr-plugin/herdr-plugin.toml +4 -1
- package/herdr-plugin/restore-plans.ts +60 -26
- package/herdr-plugin/resume.ts +18 -0
- package/install.sh +1 -1
- package/kit/hooks/ask-rules.json +11 -0
- package/kit/hooks/hooks/approval-gate/checked-marks.ts +59 -0
- package/kit/hooks/hooks/approval-gate/index.ts +36 -7
- package/kit/hooks/hooks/approval-gate/run.ts +72 -16
- package/kit/hooks/hooks/approval-gate/settings.ts +27 -1
- package/kit/hooks/hooks/approval-gate/standing-history.ts +90 -0
- package/kit/hooks/hooks/approval-gate/standing-sql.ts +58 -0
- package/kit/hooks/hooks/approval-gate/transcripts.ts +11 -5
- package/kit/hooks/hooks/lane-start-guard/decision.ts +74 -0
- package/kit/hooks/hooks/lane-start-guard/index.ts +36 -0
- package/kit/hooks/hooks/merge-guard/always-ask.ts +2 -2
- package/kit/hooks/hooks/merge-guard/approval.ts +25 -13
- package/kit/hooks/hooks/merge-guard/change-id.ts +34 -0
- package/kit/hooks/hooks/merge-guard/decision.ts +423 -35
- package/kit/hooks/hooks/merge-guard/pane-input.ts +61 -0
- package/kit/hooks/hooks/merge-guard/pane-process-info.ts +38 -0
- package/kit/hooks/hooks/merge-guard/run.ts +110 -28
- package/kit/hooks/hooks/merge-guard/standing-approval.ts +15 -78
- package/kit/hooks/hooks/plain-question/form.ts +89 -0
- package/kit/hooks/hooks/plain-question/index.ts +44 -4
- package/kit/hooks/hooks/plain-question/recommendation.ts +73 -0
- package/kit/hooks/hooks/plain-question/run.ts +14 -7
- package/kit/hooks/hooks/shared/standing-mark.ts +104 -0
- package/kit/hooks/hooks.json +11 -2
- package/kit/review/agents/skill-review.md +3 -2
- package/kit/review/skills/pre-commit/SKILL.md +1 -1
- package/kit/rules/rules/kit/code-style.md +6 -6
- package/kit/rules/rules/kit/writing-for-agents.md +3 -0
- package/kit/workflow/skills/follow/SKILL.md +2 -3
- package/kit/workflow/skills/implement/SKILL.md +1 -1
- package/kit/workflow/skills/orchestrate/SKILL.md +120 -299
- package/kit/workflow/skills/orchestrate/references/appointments.md +21 -0
- package/kit/workflow/skills/orchestrate/references/close-lane.md +8 -7
- package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +8 -8
- package/kit/workflow/skills/orchestrate/references/events.md +46 -1
- package/kit/workflow/skills/orchestrate/references/herd-ask.md +10 -0
- package/kit/workflow/skills/orchestrate/references/lane-handover.md +9 -4
- package/kit/workflow/skills/orchestrate/references/lane-session.md +5 -4
- package/kit/workflow/skills/orchestrate/references/lane-setup.md +2 -1
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +9 -2
- package/kit/workflow/skills/orchestrate/references/self-merge.md +26 -0
- package/kit/workflow/skills/orchestrate/references/stage-approval.md +50 -0
- package/kit/workflow/skills/orchestrate/references/standing-approval.md +38 -0
- package/kit/workflow/skills/orchestrate/references/start-lane.md +67 -0
- package/kit/workflow/skills/orchestrate/references/waiting.md +16 -0
- package/kit/workflow/skills/orchestrate/references/watch-without-monitor.md +13 -0
- package/kit/workflow/skills/plan/SKILL.md +3 -2
- package/package.json +2 -1
- package/recipes/README.md +1 -0
- package/recipes/jira-cli.md +1 -1
- package/recipes/sources/google-calendar-today.ts +8 -2
- package/recipes/sources/jira.ts +10 -2
- package/recipes/sources/linear.ts +9 -3
- package/recipes/sources/sentry.ts +8 -2
- package/recipes/sources.md +4 -2
- package/schemas/jackherd.schema.json +67 -0
- package/schemas/sources.schema.json +9 -0
- package/src/approval/checked.ts +60 -0
- package/src/approval/command.ts +16 -0
- package/src/check/command.ts +51 -6
- package/src/check/lint-pointers.ts +79 -0
- package/src/check/lint-shaped.ts +196 -0
- package/src/check/plan-structure-check.ts +116 -4
- package/src/check/report.ts +9 -3
- package/src/check/schema.ts +37 -0
- package/src/check/settings.ts +4 -0
- package/src/check/skill-budget-defaults.ts +77 -26
- package/src/check/skill-budgets.ts +58 -10
- package/src/cli.ts +2 -0
- package/src/config/personal-sources.ts +46 -10
- package/src/config/repository-identity.ts +40 -0
- package/src/config/schema.ts +53 -1
- package/src/context/command.ts +33 -2
- package/src/context/options.ts +33 -8
- package/src/context/usage-logs.ts +53 -0
- package/src/context/usage.ts +110 -0
- package/src/lane/base-command.ts +35 -0
- package/src/lane/base.ts +43 -0
- package/src/lane/close-check.ts +49 -15
- package/src/lane/close-command.ts +71 -11
- package/src/lane/close-options.ts +29 -8
- package/src/lane/close-survey.ts +13 -1
- package/src/lane/close.ts +51 -12
- package/src/lane/command.ts +21 -3
- package/src/lane/send-command.ts +32 -0
- package/src/lane/send-herdr.ts +77 -0
- package/src/lane/send-options.ts +23 -0
- package/src/lane/send.ts +90 -0
- package/src/lane/skill-hint.ts +27 -0
- package/src/lane/space-command.ts +30 -0
- package/src/lane/space.ts +49 -0
- package/src/lane/start-lane-hint.ts +18 -0
- package/src/plan-contract/base-check-git.ts +33 -0
- package/src/plan-contract/base-check.ts +17 -0
- package/src/plan-contract/command.ts +25 -4
- package/src/plan-format/plan-structure.ts +17 -5
- package/src/plans/ask-coordinator.ts +23 -13
- package/src/plans/board-rows.ts +9 -4
- package/src/plans/click-hint.ts +4 -0
- package/src/plans/command.ts +6 -5
- package/src/plans/ensure-panes.ts +14 -3
- package/src/plans/git.ts +14 -0
- package/src/plans/github.ts +19 -10
- package/src/plans/layout.ts +82 -33
- package/src/plans/live.ts +8 -12
- package/src/plans/plan-panes.ts +21 -4
- package/src/plans/selection.ts +29 -0
- package/src/plans/settings.ts +4 -0
- package/src/resume/command.ts +213 -0
- package/src/resume/decision.ts +102 -0
- package/src/resume/keeper.ts +65 -0
- package/src/resume/location.ts +22 -0
- package/src/resume/log.ts +27 -0
- package/src/resume/ports.ts +102 -0
- package/src/resume/record.ts +116 -0
- package/src/resume/targets.ts +84 -0
- package/src/resume/texts.ts +27 -0
- package/src/secrets/command.ts +15 -5
- package/src/sync/herdr-plugin.ts +2 -11
- package/src/sync/plan-ask-rules.ts +11 -0
- package/src/sync/render.ts +3 -1
- package/src/vocabulary/index.ts +13 -0
- package/src/waiting/command.ts +13 -4
- package/src/watch/command.ts +177 -78
- package/src/watch/config-reload.ts +119 -0
- package/src/watch/lane-model.ts +11 -1
- package/src/watch/lane-progress.ts +70 -8
- package/src/watch/lane-report.ts +66 -11
- package/src/watch/main-checkout.ts +5 -1
- package/src/watch/merge-duties.ts +33 -0
- package/src/watch/merge-overlap-read.ts +69 -0
- package/src/watch/plan-views.ts +16 -4
- package/src/watch/reference-paths.ts +46 -5
- package/src/watch/report-questions.ts +5 -2
- package/src/watch/single-report.ts +3 -2
- package/src/watch/source-dry-run-command.ts +22 -4
- package/src/watch/source-reason.ts +58 -0
- package/src/watch/source-reload.ts +21 -17
- package/src/watch/source-run.ts +24 -7
- package/src/watch/sources.ts +9 -2
- package/src/watch/texts.ts +14 -3
- package/src/watch/waiting-line.ts +4 -3
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { isRecord } from '../shared/is-record'
|
|
2
|
+
import { parseJsonOrNull } from '../shared/parse-json'
|
|
3
|
+
|
|
4
|
+
/** One pane an agent runs in, from herdr's lists; `name` is null while herdr names none. */
|
|
5
|
+
export type AgentPane = { pane: string; name: string | null; cwd: string | null }
|
|
6
|
+
|
|
7
|
+
export const TARGET_MISS = { missing: 'missing', ambiguous: 'ambiguous', elsewhere: 'elsewhere' } as const
|
|
8
|
+
|
|
9
|
+
export type TargetMiss = (typeof TARGET_MISS)[keyof typeof TARGET_MISS]
|
|
10
|
+
|
|
11
|
+
/** No agent to send to, with the panes that made it ambiguous or carry the name in another directory. */
|
|
12
|
+
export type TargetMissed = { kind: 'none'; reason: TargetMiss; panes: string[] }
|
|
13
|
+
|
|
14
|
+
/** Where a message goes: the agent's name or its pane id, `byName` when herdr named it. */
|
|
15
|
+
export type Target = { kind: 'found'; target: string; byName: boolean } | TargetMissed
|
|
16
|
+
|
|
17
|
+
/** The one unnamed pane found in the directory so far, and in how many polls in a row. */
|
|
18
|
+
export type Streak = { pane: string; polls: number } | null
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* How many polls in a row the one unnamed agent pane of a directory must stay the only one before it gets the message:
|
|
22
|
+
* herdr brings the sessions back one after the other within seconds, and a pane that is alone at first may be the
|
|
23
|
+
* user's other session, with the named one still to come. Five polls of two seconds are past the ~8 s the
|
|
24
|
+
* coordinator's session took after a crash (`RESTORE_LIMITS`).
|
|
25
|
+
*/
|
|
26
|
+
export const STEADY_POLLS = 5
|
|
27
|
+
|
|
28
|
+
const entriesOf = (json: string, key: 'agents' | 'panes'): Record<string, unknown>[] => {
|
|
29
|
+
const answer = parseJsonOrNull(json)
|
|
30
|
+
const result = isRecord(answer) ? answer.result : undefined
|
|
31
|
+
const entries = isRecord(result) ? result[key] : undefined
|
|
32
|
+
return Array.isArray(entries) ? entries.filter(isRecord) : []
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const stringOrNull = (value: unknown): string | null => (typeof value === 'string' ? value : null)
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The agent panes of `herdr agent list` and `herdr pane list`, each once; none from an answer that cannot be read.
|
|
39
|
+
* Right after a restart the pane list names no agent for a few seconds while the agent list already does, and the
|
|
40
|
+
* other way round, so a pane counts when either list names its agent. Tolerant, unlike `agentPanesOf` of
|
|
41
|
+
* `src/plans/plan-panes.ts`: a hook never throws over herdr's answer.
|
|
42
|
+
*/
|
|
43
|
+
export const resumablePanesOf = (agentList: string, paneList: string): AgentPane[] => {
|
|
44
|
+
const found = new Map<string, AgentPane>()
|
|
45
|
+
for (const { pane_id, name, cwd } of entriesOf(agentList, 'agents'))
|
|
46
|
+
if (typeof pane_id === 'string')
|
|
47
|
+
found.set(pane_id, { pane: pane_id, name: stringOrNull(name), cwd: stringOrNull(cwd) })
|
|
48
|
+
for (const { pane_id, agent, cwd } of entriesOf(paneList, 'panes')) {
|
|
49
|
+
if (typeof pane_id !== 'string' || (typeof agent !== 'string' && !found.has(pane_id))) continue
|
|
50
|
+
const known = found.get(pane_id)
|
|
51
|
+
found.set(pane_id, { pane: pane_id, name: known?.name ?? null, cwd: known?.cwd ?? stringOrNull(cwd) })
|
|
52
|
+
}
|
|
53
|
+
return [...found.values()]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const missed = (reason: TargetMiss, panes: string[]): TargetMissed => ({ kind: 'none', reason, panes })
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The agent to send to: the one named `name` when it runs in `directory` (the main checkout for the coordinator, the
|
|
60
|
+
* worktree for a lane) or herdr names no directory; the name in another directory belongs to another repository's
|
|
61
|
+
* agent. Without the name, the one unnamed agent pane in `directory`: an agent with another name there is another
|
|
62
|
+
* agent, a helper of the lane for one, and two unnamed panes are never guessed between.
|
|
63
|
+
*/
|
|
64
|
+
export const targetFor = (panes: readonly AgentPane[], name: string, directory: string): Target => {
|
|
65
|
+
const named = panes.find((pane) => pane.name === name)
|
|
66
|
+
if (named)
|
|
67
|
+
return named.cwd === null || named.cwd === directory
|
|
68
|
+
? { kind: 'found', target: name, byName: true }
|
|
69
|
+
: missed(TARGET_MISS.elsewhere, [named.pane])
|
|
70
|
+
const candidates = panes.filter((pane) => pane.name === null && pane.cwd === directory).map(({ pane }) => pane)
|
|
71
|
+
if (candidates.length === 1) return { kind: 'found', target: candidates[0] as string, byName: false }
|
|
72
|
+
return missed(candidates.length === 0 ? TARGET_MISS.missing : TARGET_MISS.ambiguous, candidates)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The target of one poll once it can be trusted: a named agent at once, the one unnamed pane only after it stayed
|
|
77
|
+
* alone for `STEADY_POLLS` polls in a row; until then it counts as missing. Returns the streak for the next poll.
|
|
78
|
+
*/
|
|
79
|
+
export const steadyTarget = (target: Target, streak: Streak): { target: Target; streak: Streak } => {
|
|
80
|
+
if (target.kind === 'none' || target.byName) return { target, streak: null }
|
|
81
|
+
const polls = streak?.pane === target.target ? streak.polls + 1 : 1
|
|
82
|
+
const next = { pane: target.target, polls }
|
|
83
|
+
return { target: polls >= STEADY_POLLS ? target : missed(TARGET_MISS.missing, []), streak: next }
|
|
84
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PLAN_LANGUAGE, type PlanLanguage } from '../vocabulary'
|
|
2
|
+
|
|
3
|
+
/** The fixed words `jh resume` sends; nothing else from outside reaches a session but checked lane names and the board. */
|
|
4
|
+
export type ResumeTexts = {
|
|
5
|
+
/** One line, no break: herdr would send a break as an enter and split the command. */
|
|
6
|
+
coordinatorLine: (board: string, interrupted: readonly string[], blocked: readonly string[]) => string
|
|
7
|
+
laneText: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const listed = (lanes: readonly string[], none: string): string => (lanes.length > 0 ? lanes.join(', ') : none)
|
|
11
|
+
|
|
12
|
+
const TEXTS: Record<PlanLanguage, ResumeTexts> = {
|
|
13
|
+
[PLAN_LANGUAGE.de]: {
|
|
14
|
+
coordinatorLine: (board, interrupted, blocked) =>
|
|
15
|
+
`/orchestrate ${board} Neustart nach Absturz: Watcher starten (§ 4), Wecker mit den Terminen abgleichen; fortgesetzt: ${listed(interrupted, 'keine')}; vor dem Absturz blockiert: ${listed(blocked, 'keine')}.`,
|
|
16
|
+
laneText:
|
|
17
|
+
'Der Rechner ist abgestürzt und herdr wurde neu gestartet. Dein letzter Befehl wurde unterbrochen: prüfe, wie weit er kam, und setze ihn fort.',
|
|
18
|
+
},
|
|
19
|
+
[PLAN_LANGUAGE.en]: {
|
|
20
|
+
coordinatorLine: (board, interrupted, blocked) =>
|
|
21
|
+
`/orchestrate ${board} Restart after a crash: start the watcher (§ 4), match the timers against the appointments; resumed: ${listed(interrupted, 'none')}; blocked before the crash: ${listed(blocked, 'none')}.`,
|
|
22
|
+
laneText:
|
|
23
|
+
'The computer crashed and herdr was restarted. Your last command was interrupted: check how far it got, and carry on with it.',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const resumeTexts = (language: PlanLanguage): ResumeTexts => TEXTS[language]
|
package/src/secrets/command.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { configuredSources } from '../config/personal-sources'
|
|
1
|
+
import { configuredSources, type ExcludedSource, type MergedSources } from '../config/personal-sources'
|
|
2
|
+
import { readRepositoryIdentity } from '../config/repository-identity'
|
|
2
3
|
import { errorText } from '../plans/error-text'
|
|
3
4
|
import { mainCheckout, worktreeRoot } from '../plans/git'
|
|
4
5
|
import { CLI_NAME } from '../version/release'
|
|
@@ -17,7 +18,7 @@ export type SecretPorts = {
|
|
|
17
18
|
/** The value typed after `prompt`, hidden; null when cancelled. */
|
|
18
19
|
readHidden: (prompt: string) => Promise<string | null>
|
|
19
20
|
/** The sources of the repository and the personal file, with the notes of their merge. */
|
|
20
|
-
sources: () =>
|
|
21
|
+
sources: () => MergedSources
|
|
21
22
|
environment: Environment
|
|
22
23
|
out: (line: string) => void
|
|
23
24
|
err: (line: string) => void
|
|
@@ -33,6 +34,12 @@ const SIGNAL_EXIT = 130
|
|
|
33
34
|
|
|
34
35
|
const NO_TERMINAL = `${CLI_NAME} secret set reads the value from a terminal only, never from a pipe: run it in a terminal of your own`
|
|
35
36
|
|
|
37
|
+
/** A source left out for its `repos`, named in the listing with the reason, so its secrets show where they are needed. */
|
|
38
|
+
const excludedAsListed = ({ source, reason }: ExcludedSource): SourceSettings => ({
|
|
39
|
+
...source,
|
|
40
|
+
name: `${source.name} (${reason})`,
|
|
41
|
+
})
|
|
42
|
+
|
|
36
43
|
/**
|
|
37
44
|
* Reads one hidden value from the terminal: raw mode, nothing echoed, each chunk applied by `hiddenInputStep`. The
|
|
38
45
|
* terminal is given back on every end, the process's `exit` included.
|
|
@@ -82,7 +89,10 @@ const terminalPorts = (): SecretPorts => ({
|
|
|
82
89
|
store: secretStore(),
|
|
83
90
|
isTerminal: () => process.stdin.isTTY === true,
|
|
84
91
|
readHidden: readHiddenFromTerminal,
|
|
85
|
-
sources: () =>
|
|
92
|
+
sources: () => {
|
|
93
|
+
const root = mainCheckout(process.cwd())
|
|
94
|
+
return configuredSources([worktreeRoot(process.cwd()), root], readRepositoryIdentity(root))
|
|
95
|
+
},
|
|
86
96
|
environment: process.env,
|
|
87
97
|
out: (line) => console.log(line),
|
|
88
98
|
err: (line) => console.error(line),
|
|
@@ -107,9 +117,9 @@ const setSecret = async (name: string, ports: SecretPorts): Promise<number> => {
|
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
const listSecrets = async (ports: SecretPorts): Promise<number> => {
|
|
110
|
-
const { sources, notes } = ports.sources()
|
|
120
|
+
const { sources, notes, excluded } = ports.sources()
|
|
111
121
|
for (const note of notes) ports.err(note)
|
|
112
|
-
const required = requiredSecrets(sources)
|
|
122
|
+
const required = requiredSecrets([...sources, ...excluded.map(excludedAsListed)])
|
|
113
123
|
const lookups = await Promise.all(required.map(({ name, keychain }) => (keychain ? ports.store.get(name) : null)))
|
|
114
124
|
const rows = required.map((secret, index) => {
|
|
115
125
|
const lookup = lookups[index] ?? { kind: SECRET_LOOKUP.absent }
|
package/src/sync/herdr-plugin.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs'
|
|
2
2
|
import { join, resolve } from 'node:path'
|
|
3
|
-
import { isLinkedWorktree,
|
|
3
|
+
import { isLinkedWorktree, mainCheckoutOrNull } from '../plans/git'
|
|
4
4
|
import { herdrBinary, spawnTool, type ToolRun } from './tools'
|
|
5
5
|
|
|
6
6
|
export const HERDR_PLUGIN_ID = 'crossdelta.jackherd'
|
|
@@ -161,15 +161,6 @@ const realPathOf = (path: string): string => {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/** The repository a path belongs to (its main checkout, also from a linked worktree); null outside git. */
|
|
165
|
-
const repositoryOf = (path: string): string | null => {
|
|
166
|
-
try {
|
|
167
|
-
return mainCheckout(path)
|
|
168
|
-
} catch {
|
|
169
|
-
return null
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
164
|
/**
|
|
174
165
|
* A checkout takes over only at home: when the project lies in the checkout's own repository. For another repository
|
|
175
166
|
* (or a project outside git) it acts like an installed package, so `init` there does not move the link.
|
|
@@ -181,7 +172,7 @@ export const checkoutCopyFor = (projectRepository: string | null, packageReposit
|
|
|
181
172
|
const packageCopyOf = (packageRoot: string, projectRoot: string): PackageCopy => {
|
|
182
173
|
if (isLinkedWorktree(packageRoot)) return PACKAGE_COPY.laneWorktree
|
|
183
174
|
if (isInstalledCopy(realPathOf(packageRoot))) return PACKAGE_COPY.installed
|
|
184
|
-
return checkoutCopyFor(
|
|
175
|
+
return checkoutCopyFor(mainCheckoutOrNull(projectRoot), mainCheckoutOrNull(packageRoot))
|
|
185
176
|
}
|
|
186
177
|
|
|
187
178
|
export const linkHerdrPluginFrom = ({
|
|
@@ -32,6 +32,17 @@ export const SHIPPED_ASK_RULES: readonly string[] = [
|
|
|
32
32
|
'Bash(gh api *dispatches*)',
|
|
33
33
|
'Bash(gh api *rerun*)',
|
|
34
34
|
'Bash(gh api *git/refs*)',
|
|
35
|
+
'Bash(gh api *mergePullRequest*)',
|
|
36
|
+
'Bash(gh api *mergeBranch*)',
|
|
37
|
+
'Bash(gh api *enablePullRequestAutoMerge*)',
|
|
38
|
+
'Bash(gh api *enqueuePullRequest*)',
|
|
39
|
+
'Bash(gh api *createCommitOnBranch*)',
|
|
40
|
+
'Bash(gh api *createRef*)',
|
|
41
|
+
'Bash(gh api *updateRef*)',
|
|
42
|
+
'Bash(gh api *deleteRef*)',
|
|
43
|
+
'Bash(gh api *rerequestCheckSuite*)',
|
|
44
|
+
'Bash(gh api *graphql*query=@*)',
|
|
45
|
+
'Bash(gh api *graphql*--input*)',
|
|
35
46
|
'Bash(jh lane close*)',
|
|
36
47
|
'Bash(jackherd lane close*)',
|
|
37
48
|
'Bash(bunx jackherd* lane close*)',
|
package/src/sync/render.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
APPROVAL_TEXTS,
|
|
6
6
|
LANGUAGE_NAME,
|
|
7
7
|
PLAN_VOCABULARY,
|
|
8
|
+
RECOMMENDED_MARK,
|
|
8
9
|
SELF_MERGE_LABEL,
|
|
9
10
|
STANDING_APPROVAL_TEXTS,
|
|
10
11
|
} from '../vocabulary'
|
|
@@ -51,6 +52,7 @@ const placeholderValues = (config: JackherdConfig): Map<string, string> => {
|
|
|
51
52
|
['kit.guard.selfMerge', config.guard.selfMerge.offer ? 'offered' : 'not offered'],
|
|
52
53
|
...prefixed('guard.standingApproval', STANDING_APPROVAL_TEXTS[config.responseLanguage]),
|
|
53
54
|
...prefixed('approval', APPROVAL_TEXTS[config.responseLanguage]),
|
|
55
|
+
['kit.question.recommended', RECOMMENDED_MARK[config.responseLanguage]],
|
|
54
56
|
['kit.merge.withoutChecks', checkFreeList(config.merge.withoutChecks)],
|
|
55
57
|
['kit.merge.openAsDraft', config.merge.openAsDraft ? 'on' : 'off'],
|
|
56
58
|
...prefixed('heading', { ...header, ...sections, ...columns, phase }),
|
|
@@ -64,7 +66,7 @@ export const listPlaceholders = (template: string): string[] =>
|
|
|
64
66
|
/**
|
|
65
67
|
* Fills the kit placeholders from the project config: `{{kit.model.<role>}}`,
|
|
66
68
|
* `{{kit.command.<test|typecheck|lint|format|install|dev>}}`, `{{kit.lanes.<copyCommand|lockFile|verifyInstall>}}`, `{{kit.word.<laneClosed|openSubagents>}}`,
|
|
67
|
-
* `{{kit.board.<pattern|file|term>}}`, `{{kit.guard.<selfMerge|selfMergeLabel>}}`, `{{kit.guard.standingApproval.<approve|until>}}`, `{{kit.merge.<withoutChecks|openAsDraft>}}`, `{{kit.heading.<key>}}` from the plan
|
|
69
|
+
* `{{kit.board.<pattern|file|term>}}`, `{{kit.guard.<selfMerge|selfMergeLabel>}}`, `{{kit.guard.standingApproval.<approve|until>}}`, `{{kit.question.recommended}}`, `{{kit.merge.<withoutChecks|openAsDraft>}}`, `{{kit.heading.<key>}}` from the plan
|
|
68
70
|
* vocabulary, the two languages, `{{kit.responseAddress}}`, the plan and worktree directory and the base branch. A
|
|
69
71
|
* command or lock file the config leaves unset renders as `unsetValueText`, never as a guessed default. Other
|
|
70
72
|
* `{{…}}` text is left alone; an unknown `kit.` placeholder throws rather than reaching a project
|
package/src/vocabulary/index.ts
CHANGED
|
@@ -49,6 +49,15 @@ export const STANDING_APPROVAL_TEXTS: Record<PlanLanguage, { approve: string; un
|
|
|
49
49
|
[PLAN_LANGUAGE.de]: { approve: 'Stehend freigeben', until: 'bis' },
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The mark at the end of the first option's label that says it is the recommendation. The plain question hook sends
|
|
54
|
+
* a dialog back whose first option lacks it, so it is fixed per language and not a config value.
|
|
55
|
+
*/
|
|
56
|
+
export const RECOMMENDED_MARK: Record<PlanLanguage, string> = {
|
|
57
|
+
[PLAN_LANGUAGE.en]: '(recommended)',
|
|
58
|
+
[PLAN_LANGUAGE.de]: '(Empfohlen)',
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
/**
|
|
53
62
|
* The words a lane prompt opens with, followed by the lane's name. The ask hint hook and the project's lane exception
|
|
54
63
|
* recognise a lane by them, so they are fixed per language and not a config value.
|
|
@@ -183,6 +192,8 @@ export type PlanVocabulary = {
|
|
|
183
192
|
noReviewGate: string
|
|
184
193
|
/** Marks a lane as closed when its name cell contains it. */
|
|
185
194
|
laneClosed: string
|
|
195
|
+
/** Stands in a board row's phases cell when that pull request builds only part of the phase. */
|
|
196
|
+
partialPhase: string
|
|
186
197
|
/** Opens the line a lane's hand-over file must hold as `<word>: 0`, so the coordinator hands over only after a wave. */
|
|
187
198
|
openSubagents: string
|
|
188
199
|
/** Words a starts-after cell begins with when the lane may start at once. */
|
|
@@ -220,6 +231,7 @@ export const PLAN_VOCABULARY: Record<PlanLanguage, PlanVocabulary> = {
|
|
|
220
231
|
phase: 'Phase',
|
|
221
232
|
noReviewGate: 'Kein Review-Gate',
|
|
222
233
|
laneClosed: 'abgeräumt',
|
|
234
|
+
partialPhase: '(Teil)',
|
|
223
235
|
openSubagents: 'offene Sub-Agenten',
|
|
224
236
|
sideOrderMarker: 'dazu (User',
|
|
225
237
|
startsNow: ['sofort'],
|
|
@@ -254,6 +266,7 @@ export const PLAN_VOCABULARY: Record<PlanLanguage, PlanVocabulary> = {
|
|
|
254
266
|
phase: 'Phase',
|
|
255
267
|
noReviewGate: 'No review gate',
|
|
256
268
|
laneClosed: 'cleaned up',
|
|
269
|
+
partialPhase: '(part)',
|
|
257
270
|
openSubagents: 'open subagents',
|
|
258
271
|
sideOrderMarker: 'also (User',
|
|
259
272
|
startsNow: ['now', 'immediately'],
|
package/src/waiting/command.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { installedReferenceHint } from '../lane/skill-hint'
|
|
1
2
|
import { terminalSafe } from '../plan-format/terminal-text'
|
|
2
3
|
import type { WaitingItem } from '../plan-format/waiting'
|
|
3
4
|
import { mainCheckout } from '../plans/git'
|
|
@@ -5,14 +6,18 @@ import { closeAllQuestionRecords, closeQuestionRecords } from '../plans/question
|
|
|
5
6
|
import { closeAllSentRecords, closeSentRecords } from '../plans/sent-files-read'
|
|
6
7
|
import { recordWaitingItems } from '../plans/waiting-records'
|
|
7
8
|
import { CLI_NAME } from '../version/release'
|
|
9
|
+
import { REFERENCE, referenceLine } from '../watch/reference-paths'
|
|
8
10
|
|
|
9
11
|
const USAGE = `usage: ${CLI_NAME} waiting [close <id> ... | close --all]`
|
|
10
12
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The lines `jh waiting` prints: id, time and text of each item, made safe for the terminal, then the reference that
|
|
15
|
+
* holds how to put them to the user (`hint`, none without the skill); a note when none waits.
|
|
16
|
+
*/
|
|
17
|
+
export const waitingListing = (items: readonly WaitingItem[], hint = ''): string[] =>
|
|
13
18
|
items.length === 0
|
|
14
19
|
? ['nothing waits on you']
|
|
15
|
-
: items.map(({ since, item, id }) => terminalSafe(`${id} · ${since} · ${item}`))
|
|
20
|
+
: [...items.map(({ since, item, id }) => terminalSafe(`${id} · ${since} · ${item}`)), ...referenceLine(hint)]
|
|
16
21
|
|
|
17
22
|
const CLOSE_ALL = '--all'
|
|
18
23
|
|
|
@@ -32,7 +37,11 @@ export const closeAllWaiting = (root: string, now: number): number =>
|
|
|
32
37
|
export const run = async (args: string[]): Promise<number> => {
|
|
33
38
|
const root = mainCheckout(process.cwd())
|
|
34
39
|
if (args.length === 0) {
|
|
35
|
-
for (const line of waitingListing(
|
|
40
|
+
for (const line of waitingListing(
|
|
41
|
+
recordWaitingItems(root, Date.now()),
|
|
42
|
+
installedReferenceHint(root, REFERENCE.waiting),
|
|
43
|
+
))
|
|
44
|
+
console.log(line)
|
|
36
45
|
return 0
|
|
37
46
|
}
|
|
38
47
|
const [command, ...rest] = args
|