jackherd 1.18.1 → 1.19.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/INSTALL.md +1 -1
  3. package/README.md +31 -26
  4. package/assets/icons/model.svg +1 -0
  5. package/dist/{chunk-N5FAOQZL.js → chunk-JDO6O3UO.js} +52 -2
  6. package/dist/config.d.ts +15 -2
  7. package/dist/config.js +22 -3
  8. package/dist/plan-format.d.ts +84 -1
  9. package/dist/plan-format.js +85 -1
  10. package/docs/plan-contract.md +8 -2
  11. package/herdr-plugin/herdr-plugin.toml +1 -1
  12. package/install.sh +1 -1
  13. package/kit/hooks/ask-rules.json +1 -7
  14. package/kit/hooks/hooks/approval-gate/decision.ts +10 -11
  15. package/kit/hooks/hooks/approval-gate/run.ts +23 -3
  16. package/kit/hooks/hooks/browser-guard/decision.ts +7 -0
  17. package/kit/hooks/hooks/browser-guard/index.ts +50 -0
  18. package/kit/hooks/hooks/plain-question/arguments.ts +14 -1
  19. package/kit/hooks/hooks/plain-question/card.ts +102 -0
  20. package/kit/hooks/hooks/plain-question/form.ts +25 -9
  21. package/kit/hooks/hooks/plain-question/index.ts +20 -25
  22. package/kit/hooks/hooks/plain-question/recommendation.ts +6 -6
  23. package/kit/hooks/hooks/return-brief/decision.ts +29 -0
  24. package/kit/hooks/hooks/return-brief/index.ts +37 -0
  25. package/kit/hooks/hooks/return-brief/settings.ts +11 -0
  26. package/kit/hooks/hooks/shared/coordinator-call.ts +25 -0
  27. package/kit/hooks/hooks/shared/transcript.ts +9 -0
  28. package/kit/hooks/hooks.json +18 -1
  29. package/kit/review/agents/security-audit.md +1 -1
  30. package/kit/workflow/agents/browser-check.md +29 -0
  31. package/kit/workflow/skills/implement/SKILL.md +5 -3
  32. package/kit/workflow/skills/orchestrate/SKILL.md +36 -14
  33. package/kit/workflow/skills/orchestrate/references/acceptance.md +10 -9
  34. package/kit/workflow/skills/orchestrate/references/close-lane.md +1 -1
  35. package/kit/workflow/skills/orchestrate/references/coordinator-clear.md +45 -0
  36. package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +15 -12
  37. package/kit/workflow/skills/plan/SKILL.md +2 -0
  38. package/kit/workflow/skills/plan/references/template-English.md +8 -0
  39. package/kit/workflow/skills/plan/references/template-German.md +8 -0
  40. package/package.json +1 -1
  41. package/schemas/jackherd.schema.json +16 -0
  42. package/src/brief/command.ts +45 -0
  43. package/src/brief/lane-brief.ts +51 -0
  44. package/src/brief/lane-facts.ts +64 -0
  45. package/src/check/skill-budget-defaults.ts +15 -4
  46. package/src/cli.ts +1 -0
  47. package/src/config/schema.ts +22 -0
  48. package/src/lane/command.ts +1 -2
  49. package/src/lane-prompt/texts.ts +2 -2
  50. package/src/plan-contract/command.ts +1 -0
  51. package/src/plan-contract/contract.ts +28 -5
  52. package/src/plan-format/index.ts +1 -0
  53. package/src/plan-format/plan-head.ts +131 -0
  54. package/src/plans/active-tasks.ts +8 -1
  55. package/src/plans/click-hint.ts +2 -1
  56. package/src/plans/focus-overlay.ts +5 -2
  57. package/src/plans/folded-detail.ts +47 -0
  58. package/src/plans/lane-line.ts +86 -0
  59. package/src/plans/lane-reason.ts +24 -4
  60. package/src/plans/layout-phase.ts +4 -20
  61. package/src/plans/layout-summary.ts +56 -45
  62. package/src/plans/layout.ts +178 -44
  63. package/src/plans/port-listening.ts +25 -0
  64. package/src/plans/selection.ts +11 -0
  65. package/src/plans/snapshot.ts +27 -9
  66. package/src/plans/task-checklist.ts +57 -0
  67. package/src/plans/texts.ts +34 -9
  68. package/src/resume/command.ts +40 -6
  69. package/src/resume/coordinator-read.ts +136 -0
  70. package/src/resume/state.ts +163 -0
  71. package/src/resume/texts.ts +6 -0
  72. package/src/sync/render.ts +5 -1
  73. package/src/vocabulary/index.ts +74 -0
  74. package/src/watch/clear-moment.ts +228 -0
  75. package/src/watch/command.ts +92 -8
  76. package/src/watch/lane-report.ts +5 -1
  77. package/src/watch/reference-paths.ts +8 -4
  78. package/src/watch/watch-events.ts +13 -0
@@ -7,19 +7,22 @@
7
7
  // question that breaks the form of `form.ts` (🐾 header, short question and labels); argv[3…] are the labels a
8
8
  // guard reads word for word, which carry no mark. A PR question that offers the self-merge label (argv[5]) for a branch
9
9
  // whose staged change carries a guard file is denied too, since the merge guard never lets that approval cover it
10
- // (`self-merge-files.ts`; the positions are named in `arguments.ts`). Anything else, and any failure, answers nothing, so the dialog opens.
11
- import { existsSync, statSync } from 'node:fs'
10
+ // (`self-merge-files.ts`; the positions are named in `arguments.ts`). Any dialog but an approval is denied when it
11
+ // lacks the decision card (`card.ts`, field names argv[7…10]) in its own question and option previews: at `PreToolUse`
12
+ // the text before the dialog is not in the transcript yet. Anything else, and any failure, answers nothing, so the
13
+ // dialog opens.
14
+ import { statSync } from 'node:fs'
12
15
  import { join } from 'node:path'
13
16
  import { boardLocationOf, boardsIn } from '../shared/boards'
14
17
  import { mainCheckoutOf } from '../shared/checkout'
15
18
  import { readConfig } from '../shared/config'
19
+ import { isCoordinatorCall } from '../shared/coordinator-call'
16
20
  import { readTextOrNull } from '../shared/files'
17
21
  import { isRecord } from '../shared/json'
18
- import { isCoordinatorAgent } from '../shared/lane'
19
- import { ownPaneAgent } from '../shared/own-pane'
20
22
  import { planCodesOf } from '../shared/plan-codes'
21
23
  import { ensurePrivateDirectory, writePrivateFile } from '../shared/private-directory'
22
- import { ARGUMENT, FIRST_GUARDED_LABEL } from './arguments'
24
+ import { AFTER_GUARDED_LABELS, ARGUMENT, FIRST_GUARDED_LABEL } from './arguments'
25
+ import { type CardFields, cardReasonOf } from './card'
23
26
  import { type CodeCache, lookUpCodes, type PlanFileStat, parseCodeCache, serializeCodeCache } from './code-cache'
24
27
  import { formReasonOf } from './form'
25
28
  import { type RecommendationWords, recommendationReasonOf } from './recommendation'
@@ -41,10 +44,18 @@ const statOf = (path: string): PlanFileStat | null => {
41
44
  return stat?.isFile() ? { path, mtimeMs: stat.mtimeMs, size: stat.size } : null
42
45
  }
43
46
 
44
- /** argv[2] is the recommended mark, argv[3…] the labels a guard reads word for word; an older install passes none. */
47
+ /** argv[2] is the recommended mark, argv[3…6] the labels a guard reads word for word; an older install passes none. */
45
48
  const recommendationWordsOf = (argv: readonly string[]): RecommendationWords => ({
46
49
  mark: argv[ARGUMENT.recommended] ?? '',
47
- guardedLabels: argv.slice(FIRST_GUARDED_LABEL),
50
+ guardedLabels: argv.slice(FIRST_GUARDED_LABEL, AFTER_GUARDED_LABELS),
51
+ })
52
+
53
+ /** argv[7…10] are the card's field names; an older install passes none, and an empty name checks no card. */
54
+ const cardFieldsOf = (argv: readonly string[]): CardFields => ({
55
+ topic: argv[ARGUMENT.cardTopic] ?? '',
56
+ consequences: argv[ARGUMENT.cardConsequences] ?? '',
57
+ ifUndecided: argv[ARGUMENT.cardIfUndecided] ?? '',
58
+ readMore: argv[ARGUMENT.cardReadMore] ?? '',
48
59
  })
49
60
 
50
61
  /** Keeps the cache for the next dialog; a cache that cannot be written only costs that dialog a full read. */
@@ -79,24 +90,7 @@ const planCodesFor = (cwd: string): Set<string> | null => {
79
90
  }
80
91
  }
81
92
 
82
- /** Whether the dialog is the coordinator's: the form (paw, short question and labels) binds nobody else. */
83
- const isCoordinatorCall = (cwd: string, session: unknown): boolean => {
84
- try {
85
- const main = mainCheckoutOf(cwd)
86
- const location = boardLocationOf(readConfig([main]))
87
- if (location === null) return false
88
- const { worktreeDirectory } = location
89
- return isCoordinatorAgent({
90
- agent: ownPaneAgent(),
91
- session,
92
- main,
93
- isLane: (name) => existsSync(join(main, worktreeDirectory, name)),
94
- })
95
- } catch {
96
- return false
97
- }
98
- }
99
-
93
+ /** The form (paw, short question and labels) binds the coordinator's dialog only. */
100
94
  const formReasonFor = (cwd: string, session: unknown, toolInput: unknown, guardedLabels: readonly string[]): string | null =>
101
95
  isCoordinatorCall(cwd, session) ? formReasonOf(toolInput, guardedLabels) : null
102
96
 
@@ -112,6 +106,7 @@ const decide = async (): Promise<string | null> => {
112
106
  denyReasonOf(texts, planCodesFor(cwd)),
113
107
  recommendationReasonOf(input.tool_input, words),
114
108
  formReasonFor(cwd, input.session_id, input.tool_input, words.guardedLabels),
109
+ cardReasonOf(input.tool_input, { fields: cardFieldsOf(process.argv), guardedLabels: words.guardedLabels }),
115
110
  selfMergeGuardFileReason(input.tool_input, selfMergeLabelOf(process.argv), (branch) =>
116
111
  stagedFilesOfBranch(cwd, branch),
117
112
  ),
@@ -30,17 +30,17 @@ const markedGuardReason = (labels: readonly string[], { mark, guardedLabels }: R
30
30
  : `Option "${label}": a guard reads "${withoutMark(label, mark)}" word for word; drop "${mark}" from it.`
31
31
  }
32
32
 
33
- /** The first option carries the mark; else the reason names a later marked option to move up, or asks for one. */
33
+ /** A mark on a later option while the first carries none: the recommendation goes first. */
34
34
  const firstMarkReason = (question: string, labels: readonly string[], mark: string): string | null => {
35
35
  const [first, ...rest] = labels
36
36
  if (first !== undefined && isMarked(first, mark)) return null
37
37
  const later = rest.find((label) => isMarked(label, mark))
38
38
  return later === undefined
39
- ? `Question "${shorten(question)}": put your recommendation first and end its label with "${mark}".`
39
+ ? null
40
40
  : `Question "${shorten(question)}": the recommendation goes first; move "${later}" to the top.`
41
41
  }
42
42
 
43
- /** A single choice has one recommendation: the first option carries the mark and no other does. */
43
+ /** A single choice has at most one recommendation: marked, it is the first option and no other carries the mark. */
44
44
  const singleChoiceReason = (question: string, labels: readonly string[], mark: string): string | null => {
45
45
  const firstReason = firstMarkReason(question, labels, mark)
46
46
  const second = labels.slice(1).find((label) => isMarked(label, mark))
@@ -62,9 +62,9 @@ const questionReason = (question: Record<string, unknown>, words: Recommendation
62
62
  }
63
63
 
64
64
  /**
65
- * The reason to send a dialog back because a question does not carry its recommendation as the first option, marked
66
- * at the end of its label; null when every question does. A question that offers a label a guard reads word for word
67
- * needs no mark, and such a label must not carry one.
65
+ * The reason to send a dialog back because a question marks a recommendation badly: the mark stands at the end of the
66
+ * label of the first option, on no other, and at most once; null when every question does. A recommendation is
67
+ * voluntary, so a question without a mark passes. A label a guard reads word for word must not carry a mark.
68
68
  */
69
69
  export const recommendationReasonOf = (toolInput: unknown, words: RecommendationWords): string | null => {
70
70
  if (words.mark === '') return null
@@ -0,0 +1,29 @@
1
+ const MINUTE_MS = 60_000
2
+
3
+ /** A prompt written this close to `now` is the one being submitted, which the transcript may already hold. */
4
+ const CURRENT_PROMPT_MS = 10_000
5
+
6
+ export const DEFAULT_BRIEF_AFTER_MINUTES = 60
7
+
8
+ export type ReturnInput = {
9
+ /** When each prompt of the session was written, in milliseconds since the epoch. */
10
+ promptTimes: readonly number[]
11
+ now: number
12
+ /** Minutes of silence after which the user counts as returned; 0 turns the hook off. */
13
+ afterMinutes: number
14
+ }
15
+
16
+ /** The minutes since the user's last earlier prompt; null when there is none (a first prompt is no return). */
17
+ export const minutesAway = (promptTimes: readonly number[], now: number): number | null => {
18
+ const earlier = promptTimes.filter((time) => time < now - CURRENT_PROMPT_MS)
19
+ return earlier.length === 0 ? null : Math.floor((now - Math.max(...earlier)) / MINUTE_MS)
20
+ }
21
+
22
+ /** The context for the coordinator when the user comes back after `afterMinutes` or more; null otherwise. */
23
+ export const returnContext = ({ promptTimes, now, afterMinutes }: ReturnInput): string | null => {
24
+ if (afterMinutes <= 0) return null
25
+ const away = minutesAway(promptTimes, now)
26
+ return away === null || away < afterMinutes
27
+ ? null
28
+ : `The user was away for ${away} minutes. Before you answer, run \`jh brief\` and put its lines to the user in the language you reply in. Report only: the open questions it counts wait for their own dialog.`
29
+ }
@@ -0,0 +1,37 @@
1
+ // UserPromptSubmit hook of Claude Code, shipped by jackherd. When the user comes back to the coordinator after
2
+ // `plans.briefAfterMinutes` (default 60) without a prompt, it adds the context to show the situation with `jh brief`
3
+ // first. It only adds context: every failure, a lane, an absent transcript or a first prompt ends in exit 0 without
4
+ // output, and a prompt is never blocked.
5
+ import { readFileSync } from 'node:fs'
6
+ import { isCoordinatorCall } from '../shared/coordinator-call'
7
+ import { isRecord } from '../shared/json'
8
+ import { promptTimesOf } from '../shared/transcript'
9
+ import { returnContext } from './decision'
10
+ import { briefAfterMinutes } from './settings'
11
+
12
+ const HOOK_EVENT = 'UserPromptSubmit'
13
+
14
+ const contextFor = (input: unknown): string | null => {
15
+ if (!isRecord(input) || typeof input.cwd !== 'string' || typeof input.transcript_path !== 'string') return null
16
+ const afterMinutes = briefAfterMinutes(process.env.CLAUDE_PROJECT_DIR ?? input.cwd)
17
+ if (afterMinutes <= 0) return null
18
+ const context = returnContext({
19
+ promptTimes: promptTimesOf(readFileSync(input.transcript_path, 'utf8')),
20
+ now: Date.now(),
21
+ afterMinutes,
22
+ })
23
+ return context !== null && isCoordinatorCall(input.cwd, input.session_id) ? context : null
24
+ }
25
+
26
+ const decide = async (): Promise<string | null> => {
27
+ try {
28
+ return contextFor(JSON.parse(await Bun.stdin.text()))
29
+ } catch {
30
+ return null
31
+ }
32
+ }
33
+
34
+ const context = await decide()
35
+ if (context !== null)
36
+ console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: HOOK_EVENT, additionalContext: context } }))
37
+ process.exit(0)
@@ -0,0 +1,11 @@
1
+ import { homedir } from 'node:os'
2
+ import { readConfig, sectionOf } from '../shared/config'
3
+ import { DEFAULT_BRIEF_AFTER_MINUTES } from './decision'
4
+
5
+ const isMinutes = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value) && value >= 0
6
+
7
+ /** `plans.briefAfterMinutes` of the first config that sets it to a number (project before home), else the default. */
8
+ export const briefAfterMinutes = (projectDirectory: string, home: string = homedir()): number =>
9
+ [readConfig([projectDirectory]), readConfig([home])]
10
+ .map((config) => sectionOf(config, 'plans').briefAfterMinutes)
11
+ .find(isMinutes) ?? DEFAULT_BRIEF_AFTER_MINUTES
@@ -0,0 +1,25 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { boardLocationOf } from './boards'
4
+ import { mainCheckoutOf } from './checkout'
5
+ import { readConfig } from './config'
6
+ import { isCoordinatorAgent } from './lane'
7
+ import { ownPaneAgent } from './own-pane'
8
+
9
+ /** Whether the hook was called for the coordinator's session; any failure or a project without a board answers no. */
10
+ export const isCoordinatorCall = (cwd: string, session: unknown): boolean => {
11
+ try {
12
+ const main = mainCheckoutOf(cwd)
13
+ const location = boardLocationOf(readConfig([main]))
14
+ if (location === null) return false
15
+ const { worktreeDirectory } = location
16
+ return isCoordinatorAgent({
17
+ agent: ownPaneAgent(),
18
+ session,
19
+ main,
20
+ isLane: (name) => existsSync(join(main, worktreeDirectory, name)),
21
+ })
22
+ } catch {
23
+ return false
24
+ }
25
+ }
@@ -13,6 +13,7 @@ type Entry = {
13
13
  type?: unknown
14
14
  isMeta?: unknown
15
15
  promptId?: unknown
16
+ timestamp?: unknown
16
17
  message?: { content?: unknown }
17
18
  }
18
19
 
@@ -87,3 +88,11 @@ export const readTurn = (jsonl: string, promptId: string | undefined): Turn => {
87
88
  .map(({ name }) => name),
88
89
  }
89
90
  }
91
+
92
+ /** When each prompt the user typed was written, in milliseconds since the epoch, in order; a prompt without a readable time is skipped. */
93
+ export const promptTimesOf = (jsonl: string): number[] =>
94
+ parseEntries(jsonl).flatMap((entry) => {
95
+ if (promptText(entry) === undefined || typeof entry.timestamp !== 'string') return []
96
+ const time = Date.parse(entry.timestamp)
97
+ return Number.isNaN(time) ? [] : [time]
98
+ })
@@ -15,6 +15,15 @@
15
15
  "timeout": 10
16
16
  }
17
17
  },
18
+ "browser-guard": {
19
+ "event": "PreToolUse",
20
+ "matcher": "mcp__claude-in-chrome__.*",
21
+ "handler": {
22
+ "type": "command",
23
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/browser-guard/index.ts",
24
+ "timeout": 10
25
+ }
26
+ },
18
27
  "format-on-edit": {
19
28
  "event": "PostToolUse",
20
29
  "matcher": "Edit|Write",
@@ -83,7 +92,15 @@
83
92
  "matcher": "AskUserQuestion",
84
93
  "handler": {
85
94
  "type": "command",
86
- "command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/plain-question/index.ts '{{kit.question.recommended}}' '{{kit.approval.allow}}' '{{kit.approval.deny}}' '{{kit.guard.selfMergeLabel}}' '{{kit.guard.standingApproval.approve}}'",
95
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/plain-question/index.ts '{{kit.question.recommended}}' '{{kit.approval.allow}}' '{{kit.approval.deny}}' '{{kit.guard.selfMergeLabel}}' '{{kit.guard.standingApproval.approve}}' '{{kit.card.topic}}' '{{kit.card.consequences}}' '{{kit.card.ifUndecided}}' '{{kit.card.readMore}}'",
96
+ "timeout": 10
97
+ }
98
+ },
99
+ "return-brief": {
100
+ "event": "UserPromptSubmit",
101
+ "handler": {
102
+ "type": "command",
103
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/return-brief/index.ts",
87
104
  "timeout": 10
88
105
  }
89
106
  },
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: security-audit
3
3
  description: 'Use when a change touches auth, tenant isolation, access policies, API keys, secrets, user input, database queries, message payloads or infrastructure: OWASP Top 10, secrets exposure, authz, input validation, injection, path traversal.'
4
- model: {{kit.model.review}}
4
+ model: {{kit.model.plan}}
5
5
  tools: Read, Grep, Glob
6
6
  user-invocable: false
7
7
  ---
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: browser-check
3
+ description: 'Looks at a running page in the Claude in Chrome extension and reports findings with saved screenshots. Use when: a change needs a look in the real browser (acceptance of a lane, a visual or console check). Reads and looks only.'
4
+ model: {{kit.model.review}}
5
+ tools: Read, mcp__claude-in-chrome__tabs_context_mcp, mcp__claude-in-chrome__tabs_create_mcp, mcp__claude-in-chrome__tabs_close_mcp, mcp__claude-in-chrome__navigate, mcp__claude-in-chrome__computer, mcp__claude-in-chrome__read_page, mcp__claude-in-chrome__get_page_text, mcp__claude-in-chrome__find, mcp__claude-in-chrome__read_console_messages, mcp__claude-in-chrome__read_network_requests
6
+ user-invocable: false
7
+ ---
8
+
9
+ You look at a page in the user's Chrome and report what you saw. The page stays exactly as it is.
10
+
11
+ ## Task
12
+
13
+ The prompt names the port or address, the path to open and what to check. Open a new tab of your own, never an existing one, and close it when you are done.
14
+
15
+ ## Rules
16
+
17
+ - The extension acts as the user, with their real login: read and look only. Navigate, scroll, hover, read the page, the console and the network; a click that only opens or shows is yours. A click or an entry that would write (submit, save, delete, send, confirm) is not: name the step in your report and stop there. Never run against a shared environment unless the prompt says so.
18
+ - One Chrome serves all lanes. Work one check at a time and close your tab afterwards.
19
+ - A tab in a background window renders the DOM and takes screenshots, but anything waiting on animation frames stays blank there. Report that as the reason, not as a finding about the page.
20
+ - Take screenshots with `save_to_disk`, at least one per path and one per step the prompt asks to see, and keep the saved paths. A screenshot that times out gets one more try.
21
+ - Wait for data before you judge: poll for real content instead of reading an empty page right after navigation, for up to 60 seconds unless the prompt says otherwise, then report a timeout.
22
+
23
+ ## Report
24
+
25
+ Return plain text, no screenshot inline:
26
+
27
+ - Per step: what you opened, what you saw, and the finding (matches the ask, or what differs).
28
+ - The absolute path of every saved screenshot, one per line, with what it shows.
29
+ - Steps you did not run because they would write, and why you could not look (blank page, login wall, timeout).
@@ -54,7 +54,7 @@ Write code and tests with Write and Edit, never through a here-document or a scr
54
54
 
55
55
  ## Step 3: Verify
56
56
 
57
- After each changed file: `{{kit.command.typecheck}}` in the package and `{{kit.command.lint}}` clean, no dead code.
57
+ After each changed file: `{{kit.command.typecheck}}` and `{{kit.command.lint}}` clean, no dead code.
58
58
 
59
59
  ## Step 4: Write Tests
60
60
 
@@ -62,7 +62,7 @@ Follow the decision tree in `.claude/rules/kit/testing.md`; run the tests with `
62
62
 
63
63
  ## Step 4b: Look at It
64
64
 
65
- A change a user sees (a page, a route, a redirect, a printed document) is looked at in the running app before `/pre-commit`: unit tests prove the pieces, only the running app proves they meet. Use a temporary end-to-end spec or the browser, a screenshot per state, and report what each shows; delete the temporary spec before the commit unless the plan asks for one. No visible change → skip, and say so in the report.
65
+ A change a user sees (a page, a route, a redirect, a printed document) is looked at in the running app before `/pre-commit`: unit tests prove the pieces, only the running app proves they meet. Use a temporary end-to-end spec or the browser, a screenshot per state, and report what each shows; delete the temporary spec before the commit unless the plan asks for one. No visible change → skip.
66
66
 
67
67
  ## Step 5: arc42 and Architecture
68
68
 
@@ -74,7 +74,7 @@ Once tests are green, types clean, the change looked at and Step 5 done: call `/
74
74
 
75
75
  Ask every question with a fixed set of answers through the `AskUserQuestion` tool, in {{kit.responseLanguage}}.
76
76
 
77
- 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.
77
+ The report at the end is five lines, each opening with its field: `{{kit.report.goal}}:`, `{{kit.report.phase}} x/y:`, `{{kit.report.result}}:` (diff stat, a figure per check), `{{kit.report.needsYou}}:` (else `nothing`), `{{kit.report.next}}:`; then the questions; the rest is in the plan. Each question carries the decision card, four lines opening with `{{kit.card.topic}}:`, `{{kit.card.consequences}}:`, `{{kit.card.ifUndecided}}:` and `{{kit.card.readMore}}:` (a link or path): the situation, the problem and what each option leads to, in everyday words. 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.
78
78
 
79
79
  ### Git commands run one at a time
80
80
 
@@ -84,6 +84,8 @@ The report at the end is short: one line of diff stat, one line per check with i
84
84
 
85
85
  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.
86
86
 
87
+ Before the next phase or the hand-over, update the plan's `## {{kit.heading.forYou}}` head: `Phase x/y` names the next phase with an open box (the last one once all are done), then open decisions, what the user can try, next step; done when `jh plan check <plan>` prints `head: present`. A plan without a head gets one at its last phase boundary, not before.
88
+
87
89
  1. Read the fill of your own context from the log, not from memory:
88
90
 
89
91
  ```bash
@@ -140,7 +140,7 @@ On wake-up:
140
140
  | `gh unavailable` | Tell the user once; herdr watching continues |
141
141
  | `lane <name> alert` | Send the lane once the request for its hand-over at the end of its current phase: [`references/lane-handover.md`](references/lane-handover.md) |
142
142
  | `approval <name> <id> <tool> · <call>` | A call of the lane waits for the user's approval (**Approvals**, below) |
143
- | `coordinator warn` / `coordinator alert` | § 8, for yourself |
143
+ | `coordinator clear` / `coordinator warn` / `coordinator alert` | § 8, for yourself |
144
144
  | `lane added: <name>` | Check that it has its row (§ 2 step 5); a name `<lane>-<purpose>` without a worktree is a helper agent of that lane and needs no row. One the lane did not announce goes to the user |
145
145
  | `kit updated <old> → <new>` | The watcher ended. After the `jh update` call has finished (with `(source changed)` there is no call to wait for), read the change of this skill and of each reference the next lines name, by the diff or file each names; read this skill file again (no session restart) only when the line says `diff unavailable`; `orchestrate skill unchanged` needs no reading. Then start the stream again |
146
146
  | an event line ending in `· read <path>` | A rare event (`feedback`, `preview`, `source`, `parallel`, `deploy`, `due`, `stalled`, a lane's `warn`, …): read the file it names and handle it as it says; the `parallel tasks` section of [`references/events.md`](references/events.md) also before you switch `watch.parallelTasks.auto` on |
@@ -148,6 +148,9 @@ On wake-up:
148
148
  | `superseded by <pid>` / `stopped by SIGTERM` | Nothing for `superseded`. After `stopped by SIGTERM` from outside, start it again |
149
149
  | `watcher stopped: <reason>` | Tell the user, fix the cause, then start it again |
150
150
 
151
+ **A look in the browser goes to the `browser-check` agent:** screenshots in your own context stay until the next
152
+ `/clear`. A look that needs page scripts goes to the lane.
153
+
151
154
  **A decision of the user goes into the file at once.** Whatever the user decides about a plan or the
152
155
  board (a phase dropped or postponed, the order changed, an approval with its scope, an answer) you write
153
156
  into the plan or the board in the same turn, with the date and "User". **A date or time in a record is read,
@@ -177,6 +180,9 @@ the turn the answer arrives (also a "no") or the thing is withdrawn; an empty st
177
180
  waiting on the answer, a source's report and what needs no row: [`references/waiting.md`](references/waiting.md).
178
181
  A line `Ask me the open … now (jh waiting).` from the herd: [`references/herd-ask.md`](references/herd-ask.md).
179
182
 
183
+ **The user comes back after a long absence** (the `return-brief` hook adds the context "The user was away for n
184
+ minutes"): show the `jh brief` lines, so they see each lane's goal, status and what it needs from them.
185
+
180
186
  **A new idea during a release** (while a plan's phases run toward one release) is a line in the plan's
181
187
  `## Follow-ups` section, with its origin, and one line to the user: no dialog, no new phase. It enters the
182
188
  release only when the release would otherwise be wrong (a bug, a false statement, a security hole) or the
@@ -194,8 +200,8 @@ Jack has seen. Before `AskUserQuestion`, every option of the lane's question blo
194
200
  --topic <topic>`, the only way to answer a lane's question yourself. A refusal sends the question to
195
201
  the user.
196
202
  4. **Own words are evidence.** A sentence Jack adds is either the lane's evidence or checked by Jack.
197
- 5. **Context the user can follow** (**Short questions**, below). Options with only `file:line` go back
198
- to the lane.
203
+ 5. **Context the user can follow**: the decision card (**Short questions**, below). A question without
204
+ its four fields, or with options that carry only `file:line`, goes back to the lane.
199
205
 
200
206
  Done when each relayed premise carries its source and only open decisions remain.
201
207
 
@@ -216,22 +222,37 @@ Silence is no answer: a collected question stays open until the user answered it
216
222
 
217
223
  **Short questions.** A dialog holds the decision and nothing else. The
218
224
  `plain-question` hook sends back a question of more than 15 words or with a path, flag or code in it, a label
219
- of more than five words, a description past one sentence of about twelve, and a plan code without its words
220
- (`the architecture docs (P7)`); a dialog that offers a guard's label is checked only for the paw. Beyond
221
- the hook:
225
+ of more than five words, a description past one sentence of about twelve, a plan code without its words
226
+ (`the architecture docs (P7)`), and a dialog that lacks the **decision card**; a dialog that offers a
227
+ guard's label is checked only for the paw. Beyond the hook:
222
228
 
223
229
  - No number in the question, unless it is the decision itself; two or three options, mostly.
224
- - The context comes before it, in the chat: three short lines at most, and only what the decision needs:
225
- the situation, the problem and what each option leads to, in everyday words, with any term the
226
- user may not know explained. Details come on request.
227
- - A lane's question you pass on names the lane in backticks in its first context line
228
- (``Lane `<lane>` asks: …``, in the language of the replies), not in the question; **Approvals** keeps its own form.
230
+ - A recommendation only where you would honestly give one, and then always: first option, ending its label with
231
+ `{{kit.question.recommended}}`, at most one in a single choice. With none, no option carries a mark.
232
+ - The context comes with it, in the dialog, as the decision card: four lines, each opening with its field
233
+ (`{{kit.card.topic}}`, `{{kit.card.consequences}}`, `{{kit.card.ifUndecided}}`, `{{kit.card.readMore}}`),
234
+ only what the decision needs: the situation, the problem and what each option leads to, in everyday words,
235
+ with any term the user may not know explained. The hook does not see the chat before the dialog, so the card
236
+ stands in each option's `preview`: its own `{{kit.card.consequences}}` line in every option, the other three
237
+ lines once, in the first option's. A multiple choice has no `preview`: its question carries the four lines,
238
+ up to 60 words. Consequences name what can be checked (diff lines, files, an amount, downtime), not a summary.
239
+ The link is one you read, never an invented one. Details come on request.
240
+ - A lane's question you pass on names the lane in backticks in its `{{kit.card.topic}}` line
241
+ (``{{kit.card.topic}}: lane `<lane>` asks …``, in the language of the replies), not in the question;
242
+ **Approvals** keeps its own form.
229
243
  - A plan item in the chat and in a waiting row is named by what it is, as in the dialog.
230
244
 
231
245
  ```text
232
246
  Question: Retry the failed check, or leave it?
233
247
  - Retry it (recommended): runs the check again
248
+ preview:
249
+ {{kit.card.topic}}: the type check failed once on an unchanged diff.
250
+ {{kit.card.consequences}}: a retry takes about 8 minutes and changes no file.
251
+ {{kit.card.ifUndecided}}: the pull request waits.
252
+ {{kit.card.readMore}}: <link to the failed run>
234
253
  - Leave it: the pull request waits
254
+ preview:
255
+ {{kit.card.consequences}}: the merge stays held until the check is green.
235
256
  ```
236
257
 
237
258
  **A draft for review** (a README, a page, a plan a lane wrote for the user to read, or a text you wrote for the
@@ -339,8 +360,9 @@ After its last PR is merged, follow
339
360
  - **A lane's hand-over:** its request on `lane <name> alert`, and a lane `done` or `idle` whose worktree holds
340
361
  `.claude/handover/<lane>.md` with the line `{{kit.word.openSubagents}}: 0`:
341
362
  [`references/lane-handover.md`](references/lane-handover.md).
342
- - **For yourself:** on `coordinator warn` or above, hand yourself over without asking, at a quiet moment,
343
- through the hand-over script: [`references/coordinator-handover.md`](references/coordinator-handover.md).
344
- After a restart or hand-over, compare `CronList` with the board's appointments as that file says. A status
363
+ - **For yourself:** on `coordinator clear`, clear without asking; on `coordinator warn`, work on until that line
364
+ comes: [`references/coordinator-clear.md`](references/coordinator-clear.md). On
365
+ `coordinator alert`, hand yourself over: [`references/coordinator-handover.md`](references/coordinator-handover.md).
366
+ After a restart, a hand-over or a clear, compare `CronList` with the board's appointments as those files say. A status
345
367
  line without a value (the reason in brackets, such as `no agent named <name>`, or the error it hit) is
346
368
  no value: tell the user in one line and never read it as `ok`.
@@ -3,13 +3,14 @@
3
3
  Read when a lane with a visible change reports ready, before you put its PR question to the user
4
4
  (`/orchestrate` § 5 step 2).
5
5
 
6
- Open the lane's port in the Claude in Chrome extension: it carries the user's real login, so it shows what the
7
- lane's Playwright spec cannot (the user's account, their role, their theme). Walk the path the phase changed,
8
- screenshot it for the user, and send a finding back to the lane instead of asking about the PR. Done when the
9
- user has the screenshots and every finding went to the lane.
6
+ Send the look to the `browser-check` agent. Its prompt names the lane's port, the path the phase changed, what to
7
+ check, above all what the lane's spec cannot show (the user's account, role, theme), and the surfaces the project supplement lists as blank in a background window. The agent returns findings as text and the paths of the
8
+ screenshots it saved: forward the files to the user with `SendUserFile` without looking at them yourself, and send
9
+ each finding back to the lane instead of asking about the PR. A session without that tool gives the user the files' absolute paths instead, unseen as well. Done when the user has the files or their paths and every finding
10
+ went to the lane.
10
11
 
11
- - One Chrome serves all lanes: accept them one after the other.
12
- - A tab in a background window renders the DOM and screenshots fine, but anything waiting on animation frames
13
- stays blank there; only for that, ask the user to bring the window up.
14
- - The extension acts as the user: read and look only. A step that writes goes to the user first, and never
15
- runs against a shared environment without their approval.
12
+ - One Chrome serves all lanes: run one `browser-check` at a time.
13
+ - A report that it could not look (blank in a background window, login wall, timeout) is no finding for the lane:
14
+ ask the user to bring the window up or log in, then send the look again.
15
+ - A step that writes comes back to you and goes to the user first: the user does it in their browser, or the lane
16
+ in its own spec. It never runs against a shared environment without their approval.
@@ -9,7 +9,7 @@ or when the user names a lane to close. Run both commands from the main checkout
9
9
  nothing: resolve each reason it prints (a named file goes to the user first, a working agent is waited for, a lock
10
10
  is lifted once its session ended, an unmerged branch is not closed) and repeat until it exits 0.
11
11
  A worktree on no branch closes when its head is on the base branch and only staged plan changes the base holds are left.
12
- 2. `jh lane close <lane>` asks the user through its ask rule, dry run included; that prompt is the user's agreement, so ask no dialog around it (under `bypassPermissions` no rule asks: ask first). It runs the listed steps, one line each. After a failure,
12
+ 2. `jh lane close <lane>` Run it without asking the user; its checks are the safeguard. It repeats the step 1 checks (resolve a refusal as in step 1) and runs the listed steps, one line each. After a failure,
13
13
  run it again: with the worktree gone and the branch left, `jh lane close <lane> --branch <branch>` finishes the
14
14
  branch and the board; a lane closed in full says so and changes nothing.
15
15
  3. The command leaves the rows marked `{{kit.word.laneClosed}} <date>` and the `{{kit.heading.history}}` row as a
@@ -0,0 +1,45 @@
1
+ # § 8: clearing your context at a quiet moment
2
+
3
+ Read on the watcher line `coordinator clear <n>k`, on `coordinator warn`, and when a chat starts as
4
+ `/orchestrate <board> Cleared: …` in your reply language.
5
+
6
+ **The watcher decides the moment, you do not.** It sends `coordinator clear` when nothing is open: once per rise of
7
+ your fill above `plans.context.coordinator.clearTokens`, and once after a merge with the board written afterwards.
8
+ `coordinator warn`: work on until that line comes. `coordinator alert`:
9
+ [`coordinator-handover.md`](coordinator-handover.md); with both lines standing, `coordinator clear` wins, it needs
10
+ no note.
11
+
12
+ ## On `coordinator clear`
13
+
14
+ 1. **Secure what only the chat holds.** A decision of the user not yet in a plan or the board goes there now (§ 4);
15
+ a trap, a suspicion or a next step only the chat holds goes as one dated line into the board's history, an
16
+ instruction that holds only for now with the condition it ends on.
17
+ Write no note: what the files hold, nobody copies. A clear is no bundling moment: board lines stay in the main
18
+ checkout's board. Wait for your own background tasks and subagents to finish, the watcher's stream aside. A dialog
19
+ or a new report since the line means this moment has passed: work on. The next line comes after the next merge;
20
+ without one, `coordinator alert` hands you over.
21
+ 2. **Read the list and get the line:** the `standing approvals` post of `jh resume --coordinator --board <board>`
22
+ names each line and end for step 3; `jh resume --clear-line --board <board>` prints the first input of the next
23
+ chat.
24
+ 3. **Start the hand-over script** with it, as a plain Bash call, without `&`, `run_in_background` or `setsid`:
25
+ `bun .claude/skills/orchestrate/handover/index.ts --send "<the line>"`. Per standing approval
26
+ still holding, add `--board <board> --approval "<line number> <its end>"`;
27
+ it goes in as a hint and the new chat asks again. The script returns `hand-over started: pid <n>, log <path>`.
28
+ **End your turn right after it**, with one line to the user that the chat is about to clear. Any other output
29
+ (`hand-over not started: <reason>`) means no clear runs.
30
+ [`coordinator-handover.md`](coordinator-handover.md) covers what the script does once started, aborts included.
31
+
32
+ ## In the new chat
33
+
34
+ 1. Run `jh resume --coordinator --board <board>`. Each post lists its count and one line per entry with its place;
35
+ zero means none, `unknown` means it could not be read, so read that one yourself. Only the board, herdr and `gh`
36
+ can be `unknown`: an unreadable approvals or records directory counts as empty. Then read the newest dated lines
37
+ of the board's history: what the old chat secured stands there.
38
+ 2. Start the watcher (§ 4). The old chat's stream outlives the clear, and starting yours ends it with `superseded by
39
+ <pid>`, nothing to do; background tasks of the old chat keep running too.
40
+ 3. Approvals the watcher reported before the clear are not reported again: take the open ones from
41
+ the list and handle them (**Approvals**, § 4). Work through the rest of the list: a lane's open questions from the report it names; an `order without a report` waits for
42
+ that lane, nothing to send again; PRs, what waits on the user and appointments as § 4 and § 5 say. Timers survive
43
+ `/clear`: compare `CronList` with the appointments listed, create a missing one, delete one without a row. Done
44
+ when every listed lane has a started next action or a named reason to wait (§ 4), and every other post is handled
45
+ or zero.
@@ -1,27 +1,30 @@
1
1
  # § 8: the coordinator hands itself over
2
2
 
3
- Read on the watcher event `coordinator warn` or `coordinator alert`, and when a chat starts as
4
- `/orchestrate <board> Hand-over: …` or after a restart of your session. `jh resume` starts a chat as
3
+ Read on the watcher event `coordinator alert`, and when a chat starts as
4
+ `/orchestrate <board> Hand-over: …` or after a restart of your session. Clearing is the normal way
5
+ ([`coordinator-clear.md`](coordinator-clear.md)); a hand-over with a note is for `coordinator alert`, when no quiet
6
+ moment came. `jh resume` starts a chat as
5
7
  `/orchestrate <board> Restart after a crash: …` in your reply language: start the watcher (§ 4), compare the
6
8
  alarms below, handle each lane as on wake-up; the resumed lanes it names have their message.
7
9
 
8
- ## The hand-over, at a quiet moment
10
+ ## The hand-over, at your next stop
9
11
 
10
- Hand yourself over to a fresh chat at a quiet moment (no merge or rebase half done, no dialog open, no answer of
11
- the user awaited):
12
+ Hand yourself over to a fresh chat as soon as no merge or rebase is half done, no dialog is open and no answer of
13
+ the user is awaited:
12
14
 
13
15
  1. **Prepare.** Record your hand-over where the project supplement says, by default in the board's
14
- `{{kit.heading.history}}` (dated as § 4 reads it): the state of every lane, what waits on the user, the next
15
- step, the traps. It holds the hand-over only: a decision of the user that is not yet in a plan or the board
16
- belongs there first (§ 4), not into the note. Collected not blocking questions are named by the path of the
16
+ `{{kit.heading.history}}` (dated as § 4 reads it): the next step and the traps and suspicions no file holds. The note holds the hand-over only: a decision of the user that is not yet in a plan or the board
17
+ belongs there first (§ 4), not into the note; the state of the lanes and what waits on the user come from
18
+ `jh resume --coordinator`. Collected not blocking questions are named by the path of the
17
19
  lane report that holds them, never copied. Board lines of § 4 not on `{{kit.baseBranch}}` yet go into a documentation PR now (the bundling moment). Write the prompt for the new chat to
18
- `.claude/handover/coordinator.md`: what to read first, then the steps in order. It is read, not typed, so it
19
- may be long.
20
+ `.claude/handover/coordinator.md`: what to read first, then the steps in order, the first of them
21
+ `jh resume --coordinator --board <board>`, whose list holds the approvals the watcher will not report again. The
22
+ file is read, not typed, so it may be long.
20
23
  2. **Start the hand-over script** as a plain Bash call, without `&`, `run_in_background` or `setsid`:
21
24
  `bun .claude/skills/orchestrate/handover/index.ts --send "/orchestrate <board> Hand-over: read .claude/handover/coordinator.md first and continue with the next step it names."`
22
25
  Per standing approval still holding, add `--board <board> --approval "<line number> <its end>"`: its row
23
- goes in as a hint; the new chat asks again ([`standing-approval.md`](standing-approval.md)); `claude --resume` keeps it. It detaches itself and returns `hand-over started: pid <n>, log <path>`. **End your turn right after it**,
24
- with one line to the user that the chat is about to clear.
26
+ goes in as a hint; the new chat asks again ([`standing-approval.md`](standing-approval.md)); `claude --resume` keeps the approval. The script detaches itself and returns `hand-over started: pid <n>, log <path>`. **End your turn right after it**,
27
+ with one line to the user that the chat is about to clear. Any other output (`hand-over not started: <reason>`, for example a refused `--approval`) means no hand-over runs.
25
28
 
26
29
  ## What the script does
27
30
 
@@ -30,6 +30,8 @@ A plan records decisions; it is not where they get taken. If the `## {{kit.headi
30
30
 
31
31
  **Structure:** copy the template in [`references/template-{{kit.planLanguage}}.md`](references/template-{{kit.planLanguage}}.md) and fill it in. Every section stays, in its order, with its heading verbatim.
32
32
 
33
+ The `## {{kit.heading.forYou}}` head is what a person reads first, in everyday words, with `Phase 1/<number of phases>` in the status line; done when `jh plan check <plan>` prints `head: present`.
34
+
33
35
  The closing documentation section reads `## N — …` with rows `N.1` to `N.5`: replace `N` with the number of that phase (`## 10 — …`, `10.1`). A literal `N` is no phase to the herd, which shows it as "not in the plan".
34
36
 
35
37
  A checkbox moved on purpose to a later phase stays where it is, unticked, and ends in the target: `- [ ] Update the documentation → P9`. The herd counts it neither as open nor as done in its own phase and lists it as open in phase 9. The arrow stands only at the end of the line. Once a phase's tasks are ticked on the base branch, every box still open in it, gate boxes included, is ticked or moved on this way; `jh check` checks both.
@@ -4,6 +4,14 @@
4
4
  **{{kit.heading.status}}:** Draft / In Progress / Done / Abandoned
5
5
  **{{kit.heading.scope}}:** `affected/paths`
6
6
 
7
+ ## {{kit.heading.forYou}}
8
+
9
+ - **What changes for you:** one sentence in everyday words
10
+ - **Status:** Phase 1/N (N = number of phases): name of the running one
11
+ - **Your open decisions:** which, or "none"
12
+ - **Try it now:** what, or "nothing"
13
+ - **Next step:** what, and who
14
+
7
15
  ## Problem / Context
8
16
 
9
17
  What is the trigger? Why now? One paragraph.