jackherd 1.7.1 → 1.8.1
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 +47 -0
- package/INSTALL.md +2 -0
- package/{NOTICE → NOTICE.txt} +1 -1
- package/README.md +86 -1037
- package/assets/branding/README.md +5 -7
- package/assets/branding/jackherd-logo-outline.svg +1 -0
- package/assets/branding/outline.test.ts +30 -0
- package/assets/icons/bark.svg +1 -1
- package/assets/icons/bowl.svg +1 -1
- package/assets/icons/decide.svg +1 -1
- package/assets/icons/ear.svg +1 -1
- package/assets/icons/lanes.svg +1 -1
- package/assets/icons/plan.svg +1 -1
- package/dist/{chunk-OBUWTQ6Z.js → chunk-OU2KALXP.js} +26 -10
- package/dist/config.d.ts +11 -5
- package/dist/config.js +53 -21
- package/dist/plan-format.d.ts +75 -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 +344 -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/merge-guard/xargs-program.ts +182 -0
- 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 +156 -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 +4 -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/read-config.ts +16 -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-standing.ts +5 -4
- 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/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 +2 -0
- package/src/update/config-shares.ts +30 -0
- package/src/update/update.ts +12 -1
- package/src/vocabulary/index.ts +51 -11
- package/src/watch/appointments.ts +206 -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/assets/icons/bark-dark.svg +0 -1
- package/assets/icons/bowl-dark.svg +0 -1
- package/assets/icons/decide-dark.svg +0 -1
- package/assets/icons/ear-dark.svg +0 -1
- package/assets/icons/lanes-dark.svg +0 -1
- package/assets/icons/plan-dark.svg +0 -1
- package/kit/hooks/hooks/approval-gate/lane.ts +0 -14
|
@@ -27,6 +27,22 @@ type Handler = (cursor: Cursor, state: ListState) => boolean
|
|
|
27
27
|
|
|
28
28
|
export class ShellParseError extends Error {}
|
|
29
29
|
|
|
30
|
+
/** What a guard tells the agent when it refuses or asks for a script `parseShell` cannot read. */
|
|
31
|
+
export const UNREADABLE_ADVICE =
|
|
32
|
+
'Avoid quotes, substitutions and operators inside `${…}` and `$((…))`, close every quote and here-document, or put the script in a file and run that.'
|
|
33
|
+
|
|
34
|
+
/** `<<EOF` or `<<-EOF` with an unquoted delimiter, not a here-string. */
|
|
35
|
+
const EXPANDING_HERE_DOCUMENT = /(^|[^<])<<-?[ \t]*[A-Za-z_]/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What a guard adds when the script holds a here-document with an unquoted delimiter: the shell runs `$(…)` and
|
|
39
|
+
* backticks in its body, which plain text such as Markdown rarely means. Empty for any other script.
|
|
40
|
+
*/
|
|
41
|
+
export const expandingHereDocumentAdvice = (script: string): string =>
|
|
42
|
+
EXPANDING_HERE_DOCUMENT.test(script)
|
|
43
|
+
? "The shell runs `$(…)` and backticks in a here-document with an unquoted delimiter; for plain text, quote it (`<<'EOF'`)."
|
|
44
|
+
: ''
|
|
45
|
+
|
|
30
46
|
const BLANKS = ' \t'
|
|
31
47
|
const OPERATORS = '&;|'
|
|
32
48
|
const REDIRECTION_CHARS = '<>&|'
|
|
@@ -35,6 +51,9 @@ const DELIMITER_END = ' \t\n;&|<>()'
|
|
|
35
51
|
/** Marks where an expansion stood in a word (`$z` keeps its name after it): the value is unknown before the shell runs. */
|
|
36
52
|
const EXPANSION = '$'
|
|
37
53
|
|
|
54
|
+
/** Whether an expansion stood in the word's value, so the shell decides the text only when it runs. */
|
|
55
|
+
export const isExpanded = (value: string): boolean => value.includes(EXPANSION)
|
|
56
|
+
|
|
38
57
|
const peek = (cursor: Cursor, offset = 0): string => cursor.source[cursor.index + offset] ?? ''
|
|
39
58
|
|
|
40
59
|
/** Whether the character is one of `chars`; the empty string past the end of input never is. */
|
|
@@ -68,10 +87,13 @@ const appendShaped = (state: ListState, text: string): void => {
|
|
|
68
87
|
/** `{a,b}` and `{a..b}` expand into other words before the program sees them. */
|
|
69
88
|
const BRACE_EXPANSION = /\{[^}]*(,|\.\.)[^}]*\}/
|
|
70
89
|
|
|
90
|
+
/** Whether the shell would expand braces in the word's value into several words. */
|
|
91
|
+
export const expandsBraces = (value: string): boolean => BRACE_EXPANSION.test(value)
|
|
92
|
+
|
|
71
93
|
const endWord = (state: ListState): void => {
|
|
72
94
|
const { word } = state
|
|
73
95
|
if (!word) return
|
|
74
|
-
if (
|
|
96
|
+
if (expandsBraces(word.value)) word.literal = false
|
|
75
97
|
state.word = undefined
|
|
76
98
|
if (state.dropNextWord) state.dropNextWord = false
|
|
77
99
|
else state.words.push(word)
|
|
@@ -90,11 +112,21 @@ const endCommand = (state: ListState): void => {
|
|
|
90
112
|
*/
|
|
91
113
|
const MAYBE_COMMAND = /\$\(|[<>]\(|[`'";&|\n\\]/
|
|
92
114
|
|
|
115
|
+
/**
|
|
116
|
+
* `${(e)x}` (zsh flags) and `${x@P}` (bash prompt expansion) evaluate the variable's value as shell text, so a
|
|
117
|
+
* substitution stored in it runs.
|
|
118
|
+
*/
|
|
119
|
+
const EVALUATING_EXPANSION = /^\$\{\(|@P/
|
|
120
|
+
|
|
93
121
|
/** Rather than parse a command inside an expansion, the whole text is refused as unparsable. */
|
|
94
122
|
const assertNoHiddenCommand = (text: string): void => {
|
|
95
123
|
if (MAYBE_COMMAND.test(text)) throw new ShellParseError('a command or quote inside an expansion')
|
|
96
124
|
}
|
|
97
125
|
|
|
126
|
+
const assertNotEvaluating = (text: string): void => {
|
|
127
|
+
if (EVALUATING_EXPANSION.test(text)) throw new ShellParseError('an expansion that evaluates its value')
|
|
128
|
+
}
|
|
129
|
+
|
|
98
130
|
const skipBalanced = (cursor: Cursor, open: string, close: string): void => {
|
|
99
131
|
const start = cursor.index
|
|
100
132
|
let depth = 0
|
|
@@ -107,19 +139,60 @@ const skipBalanced = (cursor: Cursor, open: string, close: string): void => {
|
|
|
107
139
|
throw new ShellParseError(`missing ${close}`)
|
|
108
140
|
}
|
|
109
141
|
|
|
110
|
-
|
|
142
|
+
/** Where the line at the cursor ends: at its newline, or at the end of input. */
|
|
143
|
+
const lineEnd = (cursor: Cursor): number => {
|
|
144
|
+
const newline = cursor.source.indexOf('\n', cursor.index)
|
|
145
|
+
return newline === -1 ? cursor.source.length : newline
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const isDelimiterLine = (cursor: Cursor, { delimiter, stripTabs }: Heredoc): boolean => {
|
|
149
|
+
const line = cursor.source.slice(cursor.index, lineEnd(cursor))
|
|
150
|
+
return (stripTabs ? line.replace(/^\t+/, '') : line) === delimiter
|
|
151
|
+
}
|
|
111
152
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Reads the commands of an expanding body. A backslash escapes the next character, so `\`` and `\$`
|
|
155
|
+
* stay text; `$(…)` and backticks run. The body ends at its delimiter line before the shell expands
|
|
156
|
+
* it, so a substitution that does not close inside the body is unparsable.
|
|
157
|
+
*/
|
|
158
|
+
const readExpandingBody = (body: string): SimpleCommand[] => {
|
|
159
|
+
const cursor: Cursor = { source: body, index: 0 }
|
|
160
|
+
const state = newListState()
|
|
114
161
|
while (!atEnd(cursor)) {
|
|
115
|
-
|
|
116
|
-
const lineEnd = newline === -1 ? cursor.source.length : newline
|
|
117
|
-
const line = cursor.source.slice(cursor.index, lineEnd)
|
|
118
|
-
cursor.index = lineEnd + 1
|
|
119
|
-
if ((stripTabs ? line.replace(/^\t+/, '') : line) === delimiter) return
|
|
120
|
-
if (expands && SUBSTITUTION.test(line)) throw new ShellParseError('a command inside a here-document')
|
|
162
|
+
if (!BODY_HANDLERS.some((handle) => handle(cursor, state))) cursor.index++
|
|
121
163
|
}
|
|
122
|
-
|
|
164
|
+
return state.commands
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const skipLine = (cursor: Cursor): void => {
|
|
168
|
+
cursor.index = lineEnd(cursor) + 1
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** An odd number of backslashes at the end of the line: the last one joins the next line to it. */
|
|
172
|
+
const CONTINUED_LINE = /(?:^|[^\\])(?:\\\\)*\\$/
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* In an expanding body `\⏎` joins two lines before the shell looks for the delimiter, so `EO\⏎F` ends the body
|
|
176
|
+
* where the parse would not: such a body is refused as unparsable.
|
|
177
|
+
*/
|
|
178
|
+
const assertNotContinued = (cursor: Cursor): void => {
|
|
179
|
+
if (CONTINUED_LINE.test(cursor.source.slice(cursor.index, lineEnd(cursor)))) {
|
|
180
|
+
throw new ShellParseError('a line continuation in a here-document body')
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Skips the body; in an expanding body the commands of its substitutions run, so they are kept. */
|
|
185
|
+
const readHeredocBody = (cursor: Cursor, heredoc: Heredoc, commands: SimpleCommand[]): void => {
|
|
186
|
+
const start = cursor.index
|
|
187
|
+
while (!atEnd(cursor)) {
|
|
188
|
+
if (isDelimiterLine(cursor, heredoc)) {
|
|
189
|
+
if (heredoc.expands) commands.push(...readExpandingBody(cursor.source.slice(start, cursor.index)))
|
|
190
|
+
return skipLine(cursor)
|
|
191
|
+
}
|
|
192
|
+
if (heredoc.expands) assertNotContinued(cursor)
|
|
193
|
+
skipLine(cursor)
|
|
194
|
+
}
|
|
195
|
+
throw new ShellParseError(`here-document without its ${heredoc.delimiter} line`)
|
|
123
196
|
}
|
|
124
197
|
|
|
125
198
|
const readHeredocHeader = (cursor: Cursor, stripTabs: boolean): Heredoc => {
|
|
@@ -161,7 +234,7 @@ const newline: Handler = (cursor, state) => {
|
|
|
161
234
|
if (peek(cursor) !== '\n') return false
|
|
162
235
|
endCommand(state)
|
|
163
236
|
cursor.index++
|
|
164
|
-
for (const heredoc of state.heredocs) readHeredocBody(cursor, heredoc)
|
|
237
|
+
for (const heredoc of state.heredocs) readHeredocBody(cursor, heredoc, state.commands)
|
|
165
238
|
state.heredocs = []
|
|
166
239
|
return true
|
|
167
240
|
}
|
|
@@ -181,7 +254,16 @@ const singleQuoted: Handler = (cursor, state) => {
|
|
|
181
254
|
return true
|
|
182
255
|
}
|
|
183
256
|
|
|
184
|
-
const ANSI_C_ESCAPES: Record<string, string> = {
|
|
257
|
+
const ANSI_C_ESCAPES: Record<string, string> = {
|
|
258
|
+
n: '\n',
|
|
259
|
+
t: '\t',
|
|
260
|
+
r: '\r',
|
|
261
|
+
e: '\u001b',
|
|
262
|
+
a: '\u0007',
|
|
263
|
+
b: '\b',
|
|
264
|
+
f: '\f',
|
|
265
|
+
v: '\v',
|
|
266
|
+
}
|
|
185
267
|
const MAX_CODE_POINT = 0x10ffff
|
|
186
268
|
const ANSI_C_NUMERIC = /^(x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{1,4}|U[0-9a-fA-F]{1,8}|[0-7]{1,3})/
|
|
187
269
|
|
|
@@ -217,15 +299,41 @@ const ansiCQuoted: Handler = (cursor, state) => {
|
|
|
217
299
|
return true
|
|
218
300
|
}
|
|
219
301
|
|
|
302
|
+
/**
|
|
303
|
+
* A backtick substitution ends at its first unescaped backtick, and inside it `` \` `` opens a nested one that the
|
|
304
|
+
* parse would read as text: such a substitution is refused as unparsable. The cursor stands after the opening one.
|
|
305
|
+
*/
|
|
306
|
+
const assertNoNestedBacktick = ({ source, index }: Cursor): void => {
|
|
307
|
+
for (let at = index; at < source.length && source[at] !== '`'; at++) {
|
|
308
|
+
if (source[at] !== '\\') continue
|
|
309
|
+
if (source[at + 1] === '`') throw new ShellParseError('an escaped backtick inside backticks')
|
|
310
|
+
at++
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Parses a substitution's list up to its terminator. A `case` in it is refused as unparsable: the
|
|
316
|
+
* `)` of its patterns would close the substitution early and leave the rest of its commands unread.
|
|
317
|
+
*/
|
|
318
|
+
const parseSubstitution = (cursor: Cursor, terminator: Terminator): SimpleCommand[] => {
|
|
319
|
+
if (terminator === '`') assertNoNestedBacktick(cursor)
|
|
320
|
+
const commands = parseList(cursor, terminator)
|
|
321
|
+
if (commands.some(([program]) => program.value === 'case')) throw new ShellParseError('a case inside a substitution')
|
|
322
|
+
return commands
|
|
323
|
+
}
|
|
324
|
+
|
|
220
325
|
const substitute = (cursor: Cursor, state: ListState, skip: number, terminator: Terminator): void => {
|
|
221
326
|
cursor.index += skip
|
|
222
|
-
state.commands.push(...
|
|
327
|
+
state.commands.push(...parseSubstitution(cursor, terminator))
|
|
223
328
|
appendShaped(state, EXPANSION)
|
|
224
329
|
}
|
|
225
330
|
|
|
226
331
|
const startsCommandSubstitution = (cursor: Cursor): boolean =>
|
|
227
332
|
peek(cursor) === '$' && peek(cursor, 1) === '(' && peek(cursor, 2) !== '('
|
|
228
333
|
|
|
334
|
+
const startsArithmetic = (cursor: Cursor): boolean =>
|
|
335
|
+
peek(cursor) === '$' && peek(cursor, 1) === '(' && peek(cursor, 2) === '('
|
|
336
|
+
|
|
229
337
|
const readDoubleQuotedEscape = (cursor: Cursor, state: ListState): void => {
|
|
230
338
|
const next = peek(cursor, 1)
|
|
231
339
|
if (next !== '\n') appendShaped(state, isOneOf('$`"\\', next) ? next : `\\${next}`)
|
|
@@ -238,10 +346,32 @@ const backtick: Handler = (cursor, state) => {
|
|
|
238
346
|
return true
|
|
239
347
|
}
|
|
240
348
|
|
|
241
|
-
|
|
242
|
-
|
|
349
|
+
/** Whether the inner parenthesis of `((…))` closes right before the outer one, as arithmetic does. */
|
|
350
|
+
const innerClosesLast = (text: string): boolean => {
|
|
351
|
+
let depth = 0
|
|
352
|
+
for (let at = 1; at < text.length; at++) {
|
|
353
|
+
if (text[at] === '(') depth++
|
|
354
|
+
if (text[at] === ')' && --depth === 0) return at === text.length - 2
|
|
355
|
+
}
|
|
356
|
+
return false
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Skips `$((…))` from the cursor on its `$`. Text that opens like it but closes differently, such
|
|
361
|
+
* as `$((cmd) )`, is a command substitution of a subshell and is refused as unparsable.
|
|
362
|
+
*/
|
|
363
|
+
const skipArithmetic = (cursor: Cursor): void => {
|
|
243
364
|
cursor.index++
|
|
365
|
+
const start = cursor.index
|
|
244
366
|
skipBalanced(cursor, '(', ')')
|
|
367
|
+
if (!innerClosesLast(cursor.source.slice(start, cursor.index))) {
|
|
368
|
+
throw new ShellParseError('a command substitution that opens with a subshell')
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const arithmetic: Handler = (cursor, state) => {
|
|
373
|
+
if (!startsArithmetic(cursor)) return false
|
|
374
|
+
skipArithmetic(cursor)
|
|
245
375
|
appendShaped(state, EXPANSION)
|
|
246
376
|
return true
|
|
247
377
|
}
|
|
@@ -252,8 +382,22 @@ const commandSubstitution: Handler = (cursor, state) => {
|
|
|
252
382
|
return true
|
|
253
383
|
}
|
|
254
384
|
|
|
255
|
-
/**
|
|
256
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Inside double quotes and here-document bodies a `${…}` stays text the parse does not follow, so this only refuses
|
|
387
|
+
* one that evaluates its value; it never consumes the text and leaves it to the next handler.
|
|
388
|
+
*/
|
|
389
|
+
const evaluatingExpansion: Handler = (cursor) => {
|
|
390
|
+
if (peek(cursor) !== '$' || peek(cursor, 1) !== '{') return false
|
|
391
|
+
const close = cursor.source.indexOf('}', cursor.index)
|
|
392
|
+
assertNotEvaluating(cursor.source.slice(cursor.index, close === -1 ? undefined : close + 1))
|
|
393
|
+
return false
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Inside double quotes only substitutions still open a command. */
|
|
397
|
+
const QUOTED_HANDLERS: Handler[] = [evaluatingExpansion, backtick, commandSubstitution]
|
|
398
|
+
|
|
399
|
+
/** In an expanding here-document body an escape keeps the next character text; substitutions run. */
|
|
400
|
+
const BODY_HANDLERS: Handler[] = [escapedChar, evaluatingExpansion, backtick, arithmetic, commandSubstitution]
|
|
257
401
|
|
|
258
402
|
const doubleQuoted: Handler = (cursor, state) => {
|
|
259
403
|
if (peek(cursor) !== '"') return false
|
|
@@ -284,8 +428,10 @@ const localeQuoted: Handler = (cursor, state) => {
|
|
|
284
428
|
const parameterExpansion: Handler = (cursor, state) => {
|
|
285
429
|
if (peek(cursor) !== '$') return false
|
|
286
430
|
if (peek(cursor, 1) === '{') {
|
|
431
|
+
const start = cursor.index
|
|
287
432
|
cursor.index++
|
|
288
433
|
skipBalanced(cursor, '{', '}')
|
|
434
|
+
assertNotEvaluating(cursor.source.slice(start, cursor.index))
|
|
289
435
|
} else cursor.index++
|
|
290
436
|
appendShaped(state, EXPANSION)
|
|
291
437
|
return true
|
|
@@ -317,7 +463,8 @@ const hereDocument: Handler = (cursor, state) => {
|
|
|
317
463
|
return true
|
|
318
464
|
}
|
|
319
465
|
|
|
320
|
-
const isFileDescriptor = (word: Word | undefined): boolean =>
|
|
466
|
+
const isFileDescriptor = (word: Word | undefined): boolean =>
|
|
467
|
+
word !== undefined && word.literal && /^\d+$/.test(word.value)
|
|
321
468
|
|
|
322
469
|
const redirection: Handler = (cursor, state) => {
|
|
323
470
|
const char = peek(cursor)
|
|
@@ -402,6 +549,6 @@ const parseList = (cursor: Cursor, terminator: Terminator): SimpleCommand[] => {
|
|
|
402
549
|
* Splits a shell script into its simple commands, the way a permission rule sees them: at `&&`,
|
|
403
550
|
* `||`, `;`, `|`, `&` and newlines, and into subshells, command substitutions and process
|
|
404
551
|
* substitutions. Here-document bodies, comments and redirection targets are not commands and are
|
|
405
|
-
* dropped. Throws `ShellParseError` on an unterminated quote or substitution.
|
|
552
|
+
* dropped, except the substitutions in the body of a here-document with an unquoted delimiter, which run. Throws `ShellParseError` on an unterminated quote or substitution.
|
|
406
553
|
*/
|
|
407
554
|
export const parseShell = (script: string): SimpleCommand[] => parseList({ source: script, index: 0 }, undefined)
|
package/kit/hooks/hooks.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"matcher": "Bash",
|
|
40
40
|
"handler": {
|
|
41
41
|
"type": "command",
|
|
42
|
-
"command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/merge-guard/index.ts '{{kit.guard.selfMergeLabel}}' || exit 2",
|
|
42
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/merge-guard/index.ts '{{kit.guard.selfMergeLabel}}' '{{kit.guard.planApproval.approve}}' || exit 2",
|
|
43
43
|
"timeout": 10,
|
|
44
44
|
"onFailure": "block"
|
|
45
45
|
}
|
|
@@ -61,12 +61,21 @@
|
|
|
61
61
|
"timeout": 10
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
|
+
"plan-approval": {
|
|
65
|
+
"event": "PostToolUse",
|
|
66
|
+
"matcher": "AskUserQuestion",
|
|
67
|
+
"handler": {
|
|
68
|
+
"type": "command",
|
|
69
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/merge-guard/plan-approval-record.ts '{{kit.guard.planApproval.approve}}' '{{kit.guard.planApproval.revoke}}'",
|
|
70
|
+
"timeout": 10
|
|
71
|
+
}
|
|
72
|
+
},
|
|
64
73
|
"watch-guard": {
|
|
65
74
|
"event": "PreToolUse",
|
|
66
75
|
"matcher": "Bash",
|
|
67
76
|
"handler": {
|
|
68
77
|
"type": "command",
|
|
69
|
-
"command": "input=$(cat); case \"$input\" in *watch*) printf '%s' \"$input\" | bun
|
|
78
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" && { input=$(cat); case \"$input\" in *watch*) printf '%s' \"$input\" | bun .claude/hooks/watch-guard/index.ts;; esac; }",
|
|
70
79
|
"timeout": 10
|
|
71
80
|
}
|
|
72
81
|
}
|
package/kit/hooks/statusline.sh
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Status line for Claude Code: model, context use and session cost. Shipped by jackherd.
|
|
3
3
|
# Enable it in .claude/settings.json: "statusLine": { "type": "command", "command": ".claude/statusline.sh" }
|
|
4
|
-
# Colours follow plans.context of the jackherd config: the lane thresholds inside a
|
|
5
|
-
# coordinator's elsewhere; without
|
|
4
|
+
# Colours follow plans.context of the jackherd config, in tokens in the context: the lane thresholds inside a
|
|
5
|
+
# worktree, the coordinator's elsewhere; without thresholds in the config, jackherd's default thresholds in tokens.
|
|
6
|
+
# Either stops at 60 % and 85 % of the window.
|
|
6
7
|
input=$(cat)
|
|
7
8
|
|
|
8
9
|
model=$(jq -r '.model.display_name // .model.id // "?"' <<<"$input")
|
|
@@ -11,6 +12,9 @@ cost=$(jq -r '.cost.total_cost_usd // 0' <<<"$input")
|
|
|
11
12
|
cwd=$(jq -r '.workspace.current_dir // .cwd // empty' <<<"$input")
|
|
12
13
|
pct_int=${pct%.*}
|
|
13
14
|
[ -z "$pct_int" ] && pct_int=0
|
|
15
|
+
# Tokens in the context: the count Claude Code reports, else share times window.
|
|
16
|
+
tokens=$(jq -r '(.context_window.total_input_tokens // ((.context_window.used_percentage // 0) * (.context_window.context_window_size // 0) / 100)) | floor' <<<"$input" 2>/dev/null)
|
|
17
|
+
[[ "$tokens" =~ ^[0-9]+$ ]] || tokens=0
|
|
14
18
|
|
|
15
19
|
case '{{kit.responseLanguage}}' in
|
|
16
20
|
German) label='Kontext'; compact_hint='/compact oder /clear' ;;
|
|
@@ -41,27 +45,36 @@ config_file() {
|
|
|
41
45
|
config_in "$HOME"
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
# The tokens decide, at the thresholds of the config or, where it leaves one out, at jackherd's defaults
|
|
49
|
+
# (the schema's, as `jh context` reads them).
|
|
46
50
|
config=$(config_file "${cwd:-$PWD}")
|
|
51
|
+
worktrees='.claude/worktrees'
|
|
52
|
+
[ -n "$config" ] && worktrees=$(jq -r '.worktreeDirectory // ".claude/worktrees"' "$config" 2>/dev/null)
|
|
53
|
+
case "${cwd:-$PWD}/" in
|
|
54
|
+
*/"$worktrees"/*) block='.plans.context'; warn_at=250000; alert_at=350000 ;;
|
|
55
|
+
*) block='.plans.context.coordinator'; warn_at=200000; alert_at=300000 ;;
|
|
56
|
+
esac
|
|
47
57
|
if [ -n "$config" ]; then
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
warn=$(jq -r "$block.warnTokens // empty | floor" "$config" 2>/dev/null)
|
|
59
|
+
alert=$(jq -r "$block.alertTokens // empty | floor" "$config" 2>/dev/null)
|
|
60
|
+
[[ "$warn" =~ ^[0-9]+$ ]] && warn_at=$warn
|
|
61
|
+
[[ "$alert" =~ ^[0-9]+$ ]] && alert_at=$alert
|
|
62
|
+
fi
|
|
63
|
+
# A threshold stops at a share of the window, so a small window still hands over before it is full.
|
|
64
|
+
warn_cap=60
|
|
65
|
+
alert_cap=85
|
|
66
|
+
window=$(jq -r '.context_window.context_window_size // 0 | floor' <<<"$input" 2>/dev/null)
|
|
67
|
+
if [[ "$window" =~ ^[0-9]+$ ]] && [ "$window" -gt 0 ]; then
|
|
68
|
+
[ "$warn_at" -gt $((window * warn_cap / 100)) ] && warn_at=$((window * warn_cap / 100))
|
|
69
|
+
[ "$alert_at" -gt $((window * alert_cap / 100)) ] && alert_at=$((window * alert_cap / 100))
|
|
58
70
|
fi
|
|
71
|
+
level=$tokens
|
|
59
72
|
|
|
60
73
|
reset='\033[0m'
|
|
61
|
-
if [ "$
|
|
74
|
+
if [ "$level" -ge "$alert_at" ]; then
|
|
62
75
|
color='\033[31m'
|
|
63
76
|
hint=" ⚠ ${compact_hint}"
|
|
64
|
-
elif [ "$
|
|
77
|
+
elif [ "$level" -ge "$warn_at" ]; then
|
|
65
78
|
color='\033[33m'
|
|
66
79
|
hint=''
|
|
67
80
|
else
|
|
@@ -5,7 +5,7 @@ description: 'Diagnosis loop for hard bugs, flaky failures and performance regre
|
|
|
5
5
|
|
|
6
6
|
<!--
|
|
7
7
|
Adapted from https://github.com/mattpocock/skills (skills/engineering/diagnosing-bugs),
|
|
8
|
-
commit 8b78b53, MIT License, Copyright (c) 2026 Matt Pocock. See NOTICE in jackherd.
|
|
8
|
+
commit 8b78b53, MIT License, Copyright (c) 2026 Matt Pocock. See NOTICE.txt in jackherd.
|
|
9
9
|
Phase structure and the feedback-loop discipline are his; the loop recipes, redaction rules
|
|
10
10
|
and hand-off targets are the kit's.
|
|
11
11
|
-->
|
|
@@ -22,6 +22,7 @@ Types, lint, unused imports, staged build artifacts and failing tests are decida
|
|
|
22
22
|
3. **Function size**: anything past ~20 lines, or a description that needs an "and"
|
|
23
23
|
4. **Duplication**: more than three lines of identical logic in two places
|
|
24
24
|
5. **Design review**: the checklist in `.claude/rules/kit/code-style.md` → Design Review
|
|
25
|
+
6. **Recurring context**: text and output that reach an agent's context again and again (skill and rule bodies, hook reasons, tool reports, event lines) justify their cost; count what the change adds there and weigh it against what it buys
|
|
25
26
|
|
|
26
27
|
## Output Format
|
|
27
28
|
|
|
@@ -38,6 +39,7 @@ Measured (jh check): PASS / FAIL
|
|
|
38
39
|
3. Function size: ✅ / ❌
|
|
39
40
|
4. Duplication: ✅ / ❌
|
|
40
41
|
5. Design review: ✅ / ❌
|
|
42
|
+
6. Recurring context: ✅ / ❌
|
|
41
43
|
|
|
42
44
|
Verdict: PASS / FIX FIRST
|
|
43
45
|
```
|
|
@@ -84,8 +84,5 @@ mock.module('../adapters', () => ({
|
|
|
84
84
|
|
|
85
85
|
`/pre-commit` runs `bunx jackherd check`, which measures types, lint, tests, staged build artifacts and the project's own checks. Beyond that, the judgement points:
|
|
86
86
|
|
|
87
|
-
1.
|
|
88
|
-
2.
|
|
89
|
-
3. Code follows the order types → pure functions → higher-order functions, without section-label comments
|
|
90
|
-
4. The design review in `code-style.md` passed
|
|
91
|
-
5. No dead code: unused exports, unreachable branches, commented-out code removed from every changed file
|
|
87
|
+
1. Version bumped if the change affects a published package (minor = feature, patch = fix)
|
|
88
|
+
2. `code-style.md` holds in every changed file: its code order, and its design review passed, dead code (unused exports, unreachable branches, commented-out code) included
|
|
@@ -9,7 +9,7 @@ paths:
|
|
|
9
9
|
|
|
10
10
|
<!--
|
|
11
11
|
Adapted from https://github.com/mattpocock/skills (skills/productivity/writing-for-agents),
|
|
12
|
-
commit 8b78b53, MIT License, Copyright (c) 2026 Matt Pocock. See NOTICE in jackherd.
|
|
12
|
+
commit 8b78b53, MIT License, Copyright (c) 2026 Matt Pocock. See NOTICE.txt in jackherd.
|
|
13
13
|
The levers are his; the kit mechanics and the worked examples are the kit's.
|
|
14
14
|
-->
|
|
15
15
|
|
|
@@ -67,6 +67,8 @@ Every document spends one of two budgets:
|
|
|
67
67
|
|
|
68
68
|
Material behind a pointer escapes context load at the price of the pointer's own line. Material with no pointer rides entirely on cognitive load.
|
|
69
69
|
|
|
70
|
+
Context load also arrives later than the start of a session. Text and output that reach an agent's context again and again (a skill body, a hook's reason, a tool's report, an event line) justify their cost: each is paid on every arrival and read again on every turn after it. Count what a change adds there and weigh it against what it buys.
|
|
71
|
+
|
|
70
72
|
## Information hierarchy
|
|
71
73
|
|
|
72
74
|
Documents are built from **steps** (ordered actions) and **reference** (facts consulted on demand), and they mix freely. The decision is where each piece sits:
|
|
@@ -9,7 +9,7 @@ user-invocable: false
|
|
|
9
9
|
<!--
|
|
10
10
|
The two-axis idea (Standards vs. Spec) is adapted from
|
|
11
11
|
https://github.com/mattpocock/skills (skills/engineering/code-review), commit 8b78b53,
|
|
12
|
-
MIT License, Copyright (c) 2026 Matt Pocock. See NOTICE in jackherd. Only the Spec
|
|
12
|
+
MIT License, Copyright (c) 2026 Matt Pocock. See NOTICE.txt in jackherd. Only the Spec
|
|
13
13
|
axis is used here — the Standards axis is covered by /review and /uncle-bob.
|
|
14
14
|
-->
|
|
15
15
|
|
|
@@ -6,13 +6,13 @@ argument-hint: 'What to follow, e.g. a ticket key, an issue or a query'
|
|
|
6
6
|
|
|
7
7
|
If `.claude/project/follow.md` exists, read it first. It extends this skill for this project and wins where the two disagree.
|
|
8
8
|
|
|
9
|
-
You add one entry to `watch.sources`, so that `jh watch` reports a new or changed entry of an external service as `=== source <name> …`. You run only reading commands; the one file you write is the project's `.jackherd/config.json` in the main checkout, and only after the user approved the entry.
|
|
9
|
+
You add one entry to `watch.sources`, so that `jh watch` reports a new or changed entry of an external service as `=== source <name> …`. You run only reading commands; the one file you write is the project's `.jackherd/config.json` in the main checkout, and only after the user approved the entry. Read the recipe index `recipes/README.md` of the installed package first (`"$(dirname "$(readlink -f "$(command -v jh)")")/../recipes/README.md"`), then only the recipe file it names for the chosen service; do not copy a recipe from memory. Where the recipe leaves a field open, the index's section **Rules every recipe follows** decides, and `recipes/sources.md` beside it describes every field and limit of a source.
|
|
10
10
|
|
|
11
11
|
## 1. Ask
|
|
12
12
|
|
|
13
13
|
`AskUserQuestion` in {{kit.responseLanguage}}, only for what the argument and the conversation leave open, each with the recommended option first; at most four questions per call, so more open points go into a second call:
|
|
14
14
|
|
|
15
|
-
- **Service:** the
|
|
15
|
+
- **Service:** the services the recipe index lists, and "another command that prints JSON".
|
|
16
16
|
- **What to follow:** the item the argument names (its comments, recommended) or a query over several items.
|
|
17
17
|
- **Lane:** the lane the reports name, from the board's lanes table, or none.
|
|
18
18
|
- **Ticket:** whether the entries are tickets (`ticket: true`, recommended for an issue tracker or a project board), which the herd shows as a badge beside the plan of their lane: the lane above, or the lane whose name, board row or plan names the ticket's id. A source of comments or messages is no ticket. The path of the status you find yourself in step 3.
|
|
@@ -30,7 +30,7 @@ Anything missing goes to the user as the command to run themselves (`! <command>
|
|
|
30
30
|
|
|
31
31
|
Before filling, read the help of the installed CLI version for every command the recipe calls (`acli jira workitem search --help` and `workitem view --help`, `gh issue list --help`) and confirm that the fields and options the entry needs exist there. Read the provider's web documentation only when the help leaves it open or no recipe fits. A recipe is a starting point; the installed version decides.
|
|
32
32
|
|
|
33
|
-
Fill the recipe with the item or query and the answers from step 1: a `name` of letters, digits, `.`, `_` and `-` that says what is followed, `lane`, `ignoreAuthors` and `ticket` where chosen. For a ticket source, find the path of the status in the output (from the CLI's help, or the dry run with `--raw`) and set it as `map.status`; the entry you show for approval in step 4 carries it, and without a status in the output the entry has none and the badge shows only the key. A recipe that needs a script (a token from the environment, or one call per item) gets TypeScript run by Bun: write it to the scratchpad first as `<name>.ts`, never into the repository before step 4, and let the entry's `command` be `["bun", "scripts/sources/<name>.ts"]`. The script:
|
|
33
|
+
Fill the recipe with the item or query and the answers from step 1: a `name` of letters, digits, `.`, `_` and `-` that says what is followed, `lane`, `ignoreAuthors` and `ticket` where chosen. For a ticket source, find the path of the status in the output (from the CLI's help, or the dry run with `--raw`) and set it as `map.status`; the entry you show for approval in step 4 carries it, and without a status in the output the entry has none and the badge shows only the key. A recipe that needs a script (a token from the environment, or one call per item) gets TypeScript run by Bun: write it, from the recipe's script under `recipes/sources/`, to the scratchpad first as `<name>.ts`, never into the repository before step 4, and let the entry's `command` be `["bun", "scripts/sources/<name>.ts"]`. The script:
|
|
34
34
|
|
|
35
35
|
- bounds every request and every call of a tool with `AbortSignal.timeout(20_000)`, where a shell recipe would use `--max-time`;
|
|
36
36
|
- reads a token only from `process.env`, and never prints it, passes it as an argument or writes it into an output;
|
|
@@ -7,7 +7,7 @@ argument-hint: 'Describe the idea, feature or change you want stress-tested'
|
|
|
7
7
|
|
|
8
8
|
<!--
|
|
9
9
|
Adapted from https://github.com/mattpocock/skills (skills/productivity/grilling),
|
|
10
|
-
commit 8b78b53, MIT License, Copyright (c) 2026 Matt Pocock. See NOTICE in jackherd.
|
|
10
|
+
commit 8b78b53, MIT License, Copyright (c) 2026 Matt Pocock. See NOTICE.txt in jackherd.
|
|
11
11
|
The design-tree/frontier model is his; the question mechanics and the handover format to /plan
|
|
12
12
|
are the kit's.
|
|
13
13
|
-->
|
|
@@ -26,11 +26,11 @@ You are the **Implementer**. Your job is to carry tasks through completely: expl
|
|
|
26
26
|
|
|
27
27
|
When the user asks for more than one phase:
|
|
28
28
|
|
|
29
|
-
1. **Read the computed
|
|
30
|
-
2. **Batch:** the requested phases in
|
|
31
|
-
3. **Dispatch the batch in one message**, one `Agent(subagent_type: 'implement', model: <from the model table>, prompt: …)` per phase, in the same checkout: the disjoint `{{kit.heading.writes}}` sets are the isolation. Each prompt names its phase (`Phase P<n>` and the plan's path, as in Step 0) and its `{{kit.heading.writes}}` set as the **only** paths it may edit, besides ticking its own phase's task lines in the plan, and states that it ticks the task checkboxes of its own phase as it finishes each one (Plan Hygiene), and leaves the review gate, commits and `/pre-commit` to you — it reports changed files, test results and the gate items it ran.
|
|
32
|
-
4. **When the batch returns:** check each report against its set (`git diff --stat`; an edit outside the set is a finding; a ticked line outside its own phase is one too), tick what it left open, walk each review gate. Gate items that write outside a phase's set (documentation, plan synchronisation) are yours. Then recompute the ready set and dispatch the next batch.
|
|
33
|
-
5. **Commits stay serial and with you:** one `/pre-commit` per finished phase, in phase order.
|
|
29
|
+
1. **Read the computed waves:** run `jh check --parallel <plan>`. It reads the plan's `{{kit.heading.dependencies}}` table and the plan's ticks and prints one line per wave of phases that can run side by side, in order (`wave 1: P19 ∥ P20`), then `critical path: …` and `in no wave: …`; wave 1 can start now, each later wave once the waves before it are done. A phase is ready when every `{{kit.heading.dependsOn}}` entry is done (ticked off with its review gate); a phase with an external gate (`User: …`) is never ready, since the plan cannot say the gate holds. Two phases share a wave only with no shared path under `{{kit.heading.writes}}`, no shared `{{kit.heading.serialMarker}}` (compared without case), and no release of the same package: a marker `Release <package> <version>`, or the package's `CHANGELOG.md` or `package.json` under `{{kit.heading.writes}}`. Only backticked paths count, written from the repository root; a directory overlaps every file below it, a glob counts as the directory before its first wildcard, and a wildcard at the root (`*.md`) overlaps everything. A `like N` entry (`wie N`, the plan's own word) in the cell, with several phases (`like 15 and 16`) as their union, adds the sets of those phases. `no dependency table` means the plan has none: the phases run one after the other. `no phase is ready` means every open phase waits, is done, or is in no wave.
|
|
30
|
+
2. **Batch:** the requested phases in wave 1. A requested phase in a later wave waits, naming the phase it collides with. One in no wave is not ready for one of these reasons: it has an external gate (ask whether the gate holds), its `like N` entry names an unknown phase or runs in a cycle, its `{{kit.heading.writes}}` cell has no backticked path, or it depends on such a phase; say which. The command checks paths, markers and releases only: two phases that collide in content stay your judgement, so serialise them yourself.
|
|
31
|
+
3. **Dispatch the batch in one message**, one `Agent(subagent_type: 'implement', model: <from the model table>, prompt: …)` per phase, in the same checkout: the disjoint `{{kit.heading.writes}}` sets are the isolation. They may run in the background; a hand-over (Step 7) waits until all are back. Each prompt names its phase (`Phase P<n>` and the plan's path, as in Step 0) and its `{{kit.heading.writes}}` set as the **only** paths it may edit, besides ticking its own phase's task lines in the plan, and states that it ticks the task checkboxes of its own phase as it finishes each one (Plan Hygiene), and leaves the review gate, commits and `/pre-commit` to you — it reports changed files, test results and the gate items it ran.
|
|
32
|
+
4. **When the batch returns:** check each report against its set (`git diff --stat`; an edit outside the set is a finding; a ticked line outside its own phase is one too), tick what it left open, walk each review gate. Gate items that write outside a phase's set (documentation, a changelog entry, plan synchronisation) are yours, at the wave's end. Then recompute the ready set and dispatch the next batch.
|
|
33
|
+
5. **Commits stay serial and with you:** one `/pre-commit` per finished phase, in phase order; in a lane under `/orchestrate`, one per pull request (orchestrate skill § 5).
|
|
34
34
|
|
|
35
35
|
## Plan Hygiene (when implementing from a plan in `{{kit.planDirectory}}/`)
|
|
36
36
|
|
|
@@ -83,7 +83,7 @@ Once all tests are green, types are clean, the change has been looked at (Step 4
|
|
|
83
83
|
|
|
84
84
|
Ask every question with a fixed set of answers through the `AskUserQuestion` tool, in {{kit.responseLanguage}}.
|
|
85
85
|
|
|
86
|
-
The report at the end is short: one line of diff stat, one line per check with its numbers, open points, questions; everything else is in the plan, by reference.
|
|
86
|
+
The report at the end is short: one line of diff stat, one line per check with its numbers, open points, questions; everything else is in the plan, by reference. Each question carries the situation, the problem and what each option leads to, in everyday words, not only the options and `file:line`. Mark each question `blocking` or `not blocking`. A blocking one stops the step it decides; a not blocking one leaves a `TODO` at its spot and the work goes on, with neither your recommendation nor another option built as decided.
|
|
87
87
|
|
|
88
88
|
### Git commands run one at a time
|
|
89
89
|
|
|
@@ -93,7 +93,7 @@ Why: Claude Code checks permissions per subcommand and splits a command at `&&`,
|
|
|
93
93
|
|
|
94
94
|
## Step 7: Phase Boundary
|
|
95
95
|
|
|
96
|
-
Runs once
|
|
96
|
+
Runs once the phase's review gate is ticked off, committed or not (a lane under `/orchestrate` commits only when its pull request is ready), and only then — never in the middle of a task.
|
|
97
97
|
|
|
98
98
|
1. Read the fill of your own context, from the log and not from memory:
|
|
99
99
|
|
|
@@ -101,7 +101,6 @@ Runs once a phase is committed or its pull request is merged, and only then —
|
|
|
101
101
|
bunx jackherd context
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
It prints one line of JSON with `share` and `level` (`ok`, `warn` or `alert`), or `null` when there is no value (no terminal adapter, no log). `null` or `ok` → continue with the next phase in this chat, and say nothing about it.
|
|
105
|
-
2. `warn` or `alert`, and a next phase exists →
|
|
106
|
-
3.
|
|
107
|
-
4. On no, continue.
|
|
104
|
+
It prints one line of JSON with `tokens`, `share` and `level` (`ok`, `warn` or `alert`, from the token thresholds `plans.context.warnTokens` and `alertTokens`), or `null` when there is no value (no terminal adapter, no log). `null` or `ok` → continue with the next phase in this chat, and say nothing about it. A line on stderr that the auto-compact window is not above `alertTokens` means Claude Code compacts before this hand-over: name it in one line of your report.
|
|
105
|
+
2. `warn` or `alert`, and a next phase exists → the next phase runs in a new chat, without a question: go on with step 3, once every subagent you started is back, no dialog of yours is open and no question of yours waits for an answer. The same holds when Jack, the coordinator, asked this lane for its hand-over.
|
|
106
|
+
3. Write the hand-over to `.claude/handover/<lane>.md` (`<lane>` is the name of the worktree directory; the directory is ignored by git and stays out of every commit) and end your turn with its path. The file holds, in this order: the line `{{kit.word.openSubagents}}: 0`, which the coordinator requires, the state (branch, commits or uncommitted changes, what is merged), the open points, the next step (after a wave, the state of each of its phases), the traps you ran into, and links to the plan and the files that matter. Jack ends this agent and starts a fresh one in the same worktree with the hand-over as its first task; without a coordinator, the last line names the fill and tells the user to start a new chat with that file.
|