jackherd 1.16.0 → 1.18.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 (158) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/INSTALL.md +2 -1
  3. package/README.md +29 -33
  4. package/dist/{chunk-4LUFR7RC.js → chunk-N5FAOQZL.js} +7 -0
  5. package/dist/config.d.ts +33 -1
  6. package/dist/config.js +57 -5
  7. package/dist/plan-format.d.ts +18 -6
  8. package/dist/plan-format.js +8 -1
  9. package/docs/plan-contract.md +1 -0
  10. package/herdr-plugin/herdr-plugin.toml +4 -1
  11. package/herdr-plugin/restore-plans.ts +60 -26
  12. package/herdr-plugin/resume.ts +18 -0
  13. package/install.sh +1 -1
  14. package/kit/hooks/ask-rules.json +11 -0
  15. package/kit/hooks/hooks/approval-gate/checked-marks.ts +59 -0
  16. package/kit/hooks/hooks/approval-gate/index.ts +36 -7
  17. package/kit/hooks/hooks/approval-gate/run.ts +72 -16
  18. package/kit/hooks/hooks/approval-gate/settings.ts +27 -1
  19. package/kit/hooks/hooks/approval-gate/standing-history.ts +90 -0
  20. package/kit/hooks/hooks/approval-gate/standing-sql.ts +58 -0
  21. package/kit/hooks/hooks/approval-gate/transcripts.ts +11 -5
  22. package/kit/hooks/hooks/lane-start-guard/decision.ts +74 -0
  23. package/kit/hooks/hooks/lane-start-guard/index.ts +36 -0
  24. package/kit/hooks/hooks/merge-guard/always-ask.ts +2 -2
  25. package/kit/hooks/hooks/merge-guard/approval.ts +25 -13
  26. package/kit/hooks/hooks/merge-guard/change-id.ts +34 -0
  27. package/kit/hooks/hooks/merge-guard/decision.ts +423 -35
  28. package/kit/hooks/hooks/merge-guard/pane-input.ts +61 -0
  29. package/kit/hooks/hooks/merge-guard/pane-process-info.ts +38 -0
  30. package/kit/hooks/hooks/merge-guard/run.ts +110 -28
  31. package/kit/hooks/hooks/merge-guard/standing-approval.ts +15 -78
  32. package/kit/hooks/hooks/plain-question/form.ts +89 -0
  33. package/kit/hooks/hooks/plain-question/index.ts +44 -4
  34. package/kit/hooks/hooks/plain-question/recommendation.ts +73 -0
  35. package/kit/hooks/hooks/plain-question/run.ts +14 -7
  36. package/kit/hooks/hooks/shared/standing-mark.ts +104 -0
  37. package/kit/hooks/hooks.json +11 -2
  38. package/kit/review/agents/skill-review.md +3 -2
  39. package/kit/review/skills/pre-commit/SKILL.md +16 -22
  40. package/kit/rules/rules/kit/code-style.md +26 -54
  41. package/kit/rules/rules/kit/pull-requests.md +17 -40
  42. package/kit/rules/rules/kit/testing.md +6 -12
  43. package/kit/rules/rules/kit/writing-for-agents.md +3 -0
  44. package/kit/workflow/skills/follow/SKILL.md +2 -3
  45. package/kit/workflow/skills/implement/SKILL.md +28 -43
  46. package/kit/workflow/skills/orchestrate/SKILL.md +201 -458
  47. package/kit/workflow/skills/orchestrate/references/appointments.md +21 -0
  48. package/kit/workflow/skills/orchestrate/references/approvals.md +23 -0
  49. package/kit/workflow/skills/orchestrate/references/close-lane.md +8 -7
  50. package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +8 -8
  51. package/kit/workflow/skills/orchestrate/references/events.md +46 -1
  52. package/kit/workflow/skills/orchestrate/references/herd-ask.md +10 -0
  53. package/kit/workflow/skills/orchestrate/references/lane-handover.md +9 -4
  54. package/kit/workflow/skills/orchestrate/references/lane-session.md +5 -4
  55. package/kit/workflow/skills/orchestrate/references/lane-setup.md +2 -1
  56. package/kit/workflow/skills/orchestrate/references/plan-commit.md +10 -2
  57. package/kit/workflow/skills/orchestrate/references/self-merge.md +26 -0
  58. package/kit/workflow/skills/orchestrate/references/stage-approval.md +50 -0
  59. package/kit/workflow/skills/orchestrate/references/standing-approval.md +38 -0
  60. package/kit/workflow/skills/orchestrate/references/start-lane.md +67 -0
  61. package/kit/workflow/skills/orchestrate/references/waiting.md +16 -0
  62. package/kit/workflow/skills/orchestrate/references/watch-without-monitor.md +13 -0
  63. package/kit/workflow/skills/plan/SKILL.md +29 -48
  64. package/package.json +2 -1
  65. package/recipes/README.md +1 -0
  66. package/recipes/jira-cli.md +1 -1
  67. package/recipes/sources/google-calendar-today.ts +8 -2
  68. package/recipes/sources/jira.ts +10 -2
  69. package/recipes/sources/linear.ts +9 -3
  70. package/recipes/sources/sentry.ts +8 -2
  71. package/recipes/sources.md +4 -2
  72. package/schemas/jackherd.schema.json +67 -0
  73. package/schemas/sources.schema.json +9 -0
  74. package/src/approval/checked.ts +60 -0
  75. package/src/approval/command.ts +16 -0
  76. package/src/check/command.ts +51 -6
  77. package/src/check/lint-pointers.ts +79 -0
  78. package/src/check/lint-shaped.ts +196 -0
  79. package/src/check/plan-structure-check.ts +116 -4
  80. package/src/check/report.ts +9 -3
  81. package/src/check/schema.ts +37 -0
  82. package/src/check/settings.ts +4 -0
  83. package/src/check/skill-budget-defaults.ts +90 -32
  84. package/src/check/skill-budgets.ts +96 -18
  85. package/src/cli.ts +2 -0
  86. package/src/config/personal-sources.ts +46 -10
  87. package/src/config/repository-identity.ts +40 -0
  88. package/src/config/schema.ts +53 -1
  89. package/src/context/command.ts +33 -2
  90. package/src/context/options.ts +33 -8
  91. package/src/context/usage-logs.ts +53 -0
  92. package/src/context/usage.ts +110 -0
  93. package/src/lane/base-command.ts +35 -0
  94. package/src/lane/base.ts +43 -0
  95. package/src/lane/close-check.ts +49 -15
  96. package/src/lane/close-command.ts +71 -11
  97. package/src/lane/close-options.ts +29 -8
  98. package/src/lane/close-survey.ts +13 -1
  99. package/src/lane/close.ts +51 -12
  100. package/src/lane/command.ts +21 -3
  101. package/src/lane/send-command.ts +32 -0
  102. package/src/lane/send-herdr.ts +77 -0
  103. package/src/lane/send-options.ts +23 -0
  104. package/src/lane/send.ts +90 -0
  105. package/src/lane/skill-hint.ts +27 -0
  106. package/src/lane/space-command.ts +30 -0
  107. package/src/lane/space.ts +49 -0
  108. package/src/lane/start-lane-hint.ts +18 -0
  109. package/src/plan-contract/base-check-git.ts +33 -0
  110. package/src/plan-contract/base-check.ts +17 -0
  111. package/src/plan-contract/command.ts +25 -4
  112. package/src/plan-format/plan-structure.ts +17 -5
  113. package/src/plans/ask-coordinator.ts +23 -13
  114. package/src/plans/board-rows.ts +9 -4
  115. package/src/plans/click-hint.ts +4 -0
  116. package/src/plans/command.ts +6 -5
  117. package/src/plans/ensure-panes.ts +14 -3
  118. package/src/plans/git.ts +14 -0
  119. package/src/plans/github.ts +19 -10
  120. package/src/plans/layout.ts +82 -33
  121. package/src/plans/live.ts +8 -12
  122. package/src/plans/plan-panes.ts +21 -4
  123. package/src/plans/selection.ts +29 -0
  124. package/src/plans/settings.ts +4 -0
  125. package/src/resume/command.ts +213 -0
  126. package/src/resume/decision.ts +102 -0
  127. package/src/resume/keeper.ts +65 -0
  128. package/src/resume/location.ts +22 -0
  129. package/src/resume/log.ts +27 -0
  130. package/src/resume/ports.ts +102 -0
  131. package/src/resume/record.ts +116 -0
  132. package/src/resume/targets.ts +84 -0
  133. package/src/resume/texts.ts +27 -0
  134. package/src/secrets/command.ts +15 -5
  135. package/src/sync/herdr-plugin.ts +2 -11
  136. package/src/sync/plan-ask-rules.ts +11 -0
  137. package/src/sync/render.ts +3 -1
  138. package/src/vocabulary/index.ts +13 -0
  139. package/src/waiting/command.ts +13 -4
  140. package/src/watch/command.ts +177 -78
  141. package/src/watch/config-reload.ts +119 -0
  142. package/src/watch/lane-model.ts +11 -1
  143. package/src/watch/lane-progress.ts +70 -8
  144. package/src/watch/lane-report.ts +66 -11
  145. package/src/watch/main-checkout.ts +5 -1
  146. package/src/watch/merge-duties.ts +33 -0
  147. package/src/watch/merge-overlap-read.ts +69 -0
  148. package/src/watch/plan-views.ts +16 -4
  149. package/src/watch/reference-paths.ts +46 -5
  150. package/src/watch/report-questions.ts +5 -2
  151. package/src/watch/single-report.ts +3 -2
  152. package/src/watch/source-dry-run-command.ts +22 -4
  153. package/src/watch/source-reason.ts +58 -0
  154. package/src/watch/source-reload.ts +21 -17
  155. package/src/watch/source-run.ts +24 -7
  156. package/src/watch/sources.ts +9 -2
  157. package/src/watch/texts.ts +14 -3
  158. package/src/watch/waiting-line.ts +4 -3
@@ -16,16 +16,13 @@ You create a structured concept/plan document in `{{kit.planDirectory}}/`.
16
16
 
17
17
  ### 0. Are the decisions actually made?
18
18
 
19
- A plan records decisions; it is not where they get taken. If the `## {{kit.heading.decisions}}` table would end up holding your guesses rather than the user's answers, **stop and recommend `/grill`** — name the open questions you would otherwise have to invent answers for, and let the user decide whether to run it. Entering clarification mode is the user's call, so you recommend it and leave the invocation to them. It works the questions in rounds and hands back rows in this table's format.
20
-
21
- Skip this when the request already carries its decisions (a plan for a bug fix with one obvious approach, a change the user has already specified).
19
+ A plan records decisions; it is not where they get taken. If the `## {{kit.heading.decisions}}` table would hold your guesses rather than the user's answers, **stop and recommend `/grill`**: name the open questions and leave the invocation to the user. It hands back rows in this table's format. Skip this when the request already carries its decisions.
22
20
 
23
21
  ### 1. Pre-Scan (before a single word of the plan is written)
24
22
 
25
23
  - [ ] Read the relevant files in their **current** state, from disk
26
24
  - [ ] Search the project's shared components and utilities for what covers 80%+ of the need. 80%+ match → the plan specifies reuse or an override; 60–80% → the plan extends the shared piece; below 60% → new code, with the rationale under the rejected alternatives
27
- - [ ] Read related plans in `{{kit.planDirectory}}/` — check dependencies and open TODOs
28
- - [ ] Read the architecture documentation the change touches — understand what is already documented before you plan
25
+ - [ ] Read related plans in `{{kit.planDirectory}}/` (dependencies, open TODOs) and the architecture documentation the change touches
29
26
 
30
27
  ### 2. Create the Document
31
28
 
@@ -37,52 +34,43 @@ The closing documentation section reads `## N — …` with rows `N.1` to `N.5`:
37
34
 
38
35
  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.
39
36
 
40
- A task starts with its short name: `- [ ] **Name**: …` (a review-gate item keeps its own form). The name has 40 characters at most and names the subject and the result, what is different afterwards (`Heartbeat shows progress without ticks`, `` `ask-hint` finds questions anywhere ``); the how, the conditions, the origin and the evidence follow the colon. A phase's title is its name, its context goes into the origin line. A task the user adds later: `- [ ] **Name**: … (User <DD.MM.> <HH:MM>)`.
37
+ A task starts with its short name: `- [ ] **Name**: …` (a review-gate item keeps its own form). The name has 40 characters at most and names the subject and the result (`Heartbeat shows progress without ticks`); the how, the conditions, the origin and the evidence follow the colon. A phase's title is its name; its context goes into its origin line, a quote under the heading (`> Origin: …`, in the plan's language). A task the user adds later: `- [ ] **Name**: … (User <DD.MM.> <HH:MM>)`.
41
38
 
42
- An Artifact that belongs to the plan (a mockup, a draft for review, a findings page) stands as a Markdown link with its title in the phase it concerns, in the same move as its first publish: `**Mockup:** [Title](url)` or in the origin line. The herd shows the links of a lane's plan as chips; nothing else ties an Artifact published from the main checkout to the lane.
39
+ An Artifact that belongs to the plan (a mockup, a draft for review, a findings page) stands as a Markdown link with its title in the phase it concerns, in the same move as its first publish: `**Mockup:** [Title](url)` or in the origin line. The herd shows them as chips; nothing else ties an Artifact published from the main checkout to the lane.
43
40
 
44
41
  What the template's tables mean:
45
42
 
46
- - **Model recommendations per phase** — one row per phase, judged by **error cost × complexity**. The project's default model for implementation (`{{kit.model.implement}}`) is the default of every row. A phase moves up to the planning model (`{{kit.model.plan}}`) when its rationale names a concrete error cost: a breakable invariant, possible data loss, security or tenant isolation, a migration without a way back, distributed state across systems. "Complex" or "many files" alone keeps the default. Review gates with `/security-audit` always get the strongest model.
47
- - **Dependencies and parallelization** (from three phases) — `/implement` reads it and starts every phase whose dependencies are done and that shares no written path with another, at the same time.
48
- - **{{kit.heading.dependsOn}}:** phases of this plan (numbers) or external gates (`User: mockups`). A phase is ready when everything here is done.
49
- - **{{kit.heading.writes}}:** the directories or files the phase creates or changes, as narrow as possible. Shared files many phases touch (barrels, translation files, `package.json`, generated types, architecture docs) are listed explicitly, so the overlap is visible. Write each path in full from the repository root: paths are compared as written, so `src/watch/` and `packages/app/src/watch/` do not overlap.
43
+ - **Model recommendations per phase** — one row per phase, judged by **error cost × complexity**. `{{kit.model.implement}}` is the default of every row. A phase moves up to `{{kit.model.plan}}` when its rationale names a concrete error cost: a breakable invariant, possible data loss, security or tenant isolation, a migration without a way back, distributed state across systems. "Complex" or "many files" alone keeps the default. Review gates with `/security-audit` always get the strongest model.
44
+ - **Dependencies and parallelization** (from three phases) — `/implement` starts every phase whose dependencies are done and that shares no written path with another, at the same time.
45
+ - **{{kit.heading.dependsOn}}:** phases of this plan (numbers) or external gates (`User: mockups`).
46
+ - **{{kit.heading.writes}}:** the directories or files the phase creates or changes, as narrow as possible, shared files (barrels, translation files, `package.json`, generated types, architecture docs) listed explicitly. Write each path in full from the repository root: paths are compared as written.
50
47
  - **{{kit.heading.serialMarker}}:** `Migration`, `<collection> schema bump`, `Event <type>`, a generator run. Two phases with a migration never run at the same time; neither do two that bump the same schema.
51
- - **Cutting phases:** many small phases that ship in one release are bundled into a few phases now, not appended one by one later: every phase costs a hand-over. Shared text files (skills, README, changelog, architecture docs) go into a small phase of their own or into the closing tasks of a phase, so a phase's code never waits behind another phase only for them. You cut them here; `jh check --parallel` only orders the phases it is given.
48
+ - **Cutting phases:** many small phases that ship in one release are bundled into a few phases now, not appended one by one later: every phase costs a hand-over. Shared text files (skills, README, changelog, architecture docs) go into a small phase of their own or into the closing tasks of a phase, so no phase's code waits behind them.
52
49
 
53
50
  ### 3. Architecture Review
54
51
 
55
- Apply `/uncle-bob` to the created plan. Additionally check:
56
-
57
- - **Interfaces:** Are the boundaries between modules/services clear? Who consumes what?
58
- - **Blast radius:** Which existing consumers are affected? Are tests needed in all consuming packages?
52
+ Apply `/uncle-bob` to the created plan, and check the **interfaces** (who consumes what) and the **blast radius** (which consumers are affected, which consuming packages need tests).
59
53
 
60
54
  ### 4. Anchor the Review Gate in the Plan
61
55
 
62
- Every phase gets the review gate block from the template at its end. Add the reviewers the project supplement names for its areas (a UI review for UI phases, for instance). A phase with tasks that genuinely needs no gate (documentation only, a draft) says so and why in a line of its own, in the plan's language: `**No review gate:** <reason>` (`en`), `**Kein Review-Gate:** <Grund>` (`de`). `jh check` refuses a phase the change adds with tasks and neither line.
56
+ Every phase gets the review gate block from the template at its end, plus the reviewers the project supplement names for its areas. A phase with tasks that genuinely needs no gate (documentation only, a draft) says so in a line of its own, in the plan's language: `**No review gate:** <reason>` (`en`), `**Kein Review-Gate:** <Grund>` (`de`). `jh check` refuses a phase the change adds with tasks and neither line.
63
57
 
64
58
  ### 5. Quality Checks
65
59
 
66
- - **Focus:** Does the plan solve a concrete problem? No "would be nice to have".
67
- - **Scope:** Is the scope clearly bounded? No feature-creep risk?
68
- - **Feasibility:** Can the phases be split into individual PRs/commits?
69
- - **References:** Are related plans in `{{kit.planDirectory}}/` linked?
70
- - **Current state is fact-based:** Read code, count files, verify architecture; every claim has a source.
71
- - **Shared code checked:** For every planned component it is documented why it is built new instead of reused or extended.
72
- - **Tests defined:** The tests section is filled in or carries an explicit rationale why none are needed (decision tree in `.claude/rules/kit/testing.md`).
73
- - **Model recommendations defined:** `## {{kit.heading.implementation}}` starts with the model table, one row per phase, justified by error cost × complexity.
74
- - **Parallelism defined (3+ phases):** the dependencies table follows it, a row for every phase, one added later too. Every migration, schema bump and new event of a phase is a serial marker, every shared file stands under {{kit.heading.writes}}; otherwise `/implement` starts two phases that collide.
75
- - **arc42 check (after the plan exists):** apply the filter in `.claude/rules/kit/arc42.md` to the finished plan and **ask the user concretely** which sections should be extended. On "yes", add a concrete task to `## {{kit.heading.documentation}}`.
76
- - **Architecture check (after the plan exists):** apply the filter in `.claude/rules/kit/architecture-files.md` — new ownership, a new invariant or an accepted risk each trigger a concrete question about the unit's `ARCHITECTURE.md`.
77
- - **arc42 closing phase (3+ phases or a new service/package):** end the plan with the architecture documentation phase from the template. Add arc42 sections beyond it only when they deliver real value.
78
- - **Documentation planned:** `## {{kit.heading.documentation}}` names concretely what will be created or updated after completion.
60
+ - **Focus and scope:** a concrete problem, clearly bounded, phases that split into individual PRs, related plans in `{{kit.planDirectory}}/` linked.
61
+ - **Current state is fact-based:** every claim has a source (code read, files counted).
62
+ - **Shared code checked:** for every planned component it is documented why it is built new instead of reused or extended.
63
+ - **Tests defined:** the tests section is filled in or says why none are needed (decision tree in `.claude/rules/kit/testing.md`).
64
+ - **Lint before prose:** a rule the plan writes or changes in an agent text that the project's linter can check becomes a lint task (a case where it fires, one where it stays silent); the prose shrinks to the sentence naming it with `lint:<rule id>`.
65
+ - **Model recommendations and parallelism:** `## {{kit.heading.implementation}}` starts with the model table; from three phases the dependencies table follows, a row for every phase, every migration, schema bump and new event a serial marker, every shared file under {{kit.heading.writes}}.
66
+ - **arc42 and architecture (after the plan exists):** apply the filters in `.claude/rules/kit/arc42.md` and `.claude/rules/kit/architecture-files.md` to the finished plan and **ask the user concretely**; on "yes", add a concrete task to `## {{kit.heading.documentation}}`. From three phases or a new service/package, the plan ends with the template's architecture documentation phase.
67
+ - **Documentation planned:** `## {{kit.heading.documentation}}` names concretely what will be created or updated.
79
68
 
80
69
  ### 6. Propose the board rows
81
70
 
82
71
  A **board** gathers the lanes one coordinator runs with `/orchestrate`: `{{kit.board.file}}` in a directory
83
- matching `{{kit.planDirectory}}/{{kit.board.pattern}}` (this project calls it a "{{kit.board.term}}"). The plan
84
- already holds what a lane row needs, so end by proposing the rows; `/orchestrate` then starts the lanes from them.
85
- Skip this step for a plan of one phase that nobody runs as a lane, and say so.
72
+ matching `{{kit.planDirectory}}/{{kit.board.pattern}}` (this project calls it a "{{kit.board.term}}"). End by
73
+ proposing its rows; skip this for a plan of one phase that nobody runs as a lane, and say so.
86
74
 
87
75
  Derive the rows from the dependencies table and the model table, nothing else:
88
76
 
@@ -92,33 +80,26 @@ Derive the rows from the dependencies table and the model table, nothing else:
92
80
  Otherwise it is a `↳` row under a fitting lane, or a subagent there (`/implement` Step 0b).
93
81
  - **Continuation:** a phase that shares a written path or a serial marker with an earlier phase, or depends on it,
94
82
  is a `↳` row under that phase's lane, in the same worktree.
95
- - **Starts after:** from the phase's {{kit.heading.dependsOn}} cell, as the board reads it (`P2 merged`, an external
96
- gate such as `User: mockups`); a phase without one starts at once.
97
- - **Model:** the full model ID from the model table (`{{kit.model.implement}}`, `{{kit.model.plan}}`), never an alias.
98
- - **Plan:** a Markdown link to the plan, its path relative to the board's directory (`../<file>.md` for a plan
99
- directly under `{{kit.planDirectory}}`).
100
- Plans stay where they are; the row only links them.
83
+ - **Starts after:** from the phase's {{kit.heading.dependsOn}} cell (`P2 merged`, `User: mockups`); without one, at once.
84
+ - **Model:** the full model ID from the model table, never an alias.
85
+ - **Plan:** a Markdown link relative to the board's directory (`../<file>.md`). Plans stay where they are; the row only links them.
101
86
  - **Lane name:** short kebab case, unique on the board; the worktree directory and the herdr agent carry the same name
102
- (`/orchestrate` § 2).
103
- - **Port:** `–`; the coordinator sets it when the lane starts, unless the project supplement assigns ports here.
87
+ (`/orchestrate` § 2), else the herd lists the lane as a worktree without an agent.
88
+ - **Port:** `–`; the coordinator sets it, unless the project supplement assigns ports here.
104
89
 
105
90
  Show the rows as the table they become: `## {{kit.heading.lanes}}` with the columns lane, port, plan, phases, starts
106
91
  after, model, in this order and headed in {{kit.planLanguage}} (the herd reads them by position). Then ask
107
92
  where they go:
108
93
 
109
- - **The newest board**, by name: the newest `{{kit.planDirectory}}/{{kit.board.pattern}}/{{kit.board.file}}`. The
110
- rows are appended to its `## {{kit.heading.lanes}}` table.
111
- - **A new board.** Ask for its name and propose one: the year and month or the topic, in the pattern's shape. Check
112
- that the directory name matches `{{kit.board.pattern}}` before writing; if it does not, say why and ask again.
113
- Create `{{kit.planDirectory}}/<name>/{{kit.board.file}}` with a `#` title and the `## {{kit.heading.lanes}}`
114
- table holding the proposed rows.
94
+ - **The newest board**, by name: the newest `{{kit.planDirectory}}/{{kit.board.pattern}}/{{kit.board.file}}`, appended to its `## {{kit.heading.lanes}}` table.
95
+ - **A new board:** ask for its name, proposing one (the year and month or the topic). Check that the directory name matches `{{kit.board.pattern}}` before writing; if it does not, say why and ask again. Create `{{kit.planDirectory}}/<name>/{{kit.board.file}}` with a `#` title and the table.
115
96
  - **Not now:** the rows stay in your answer, for `/orchestrate` to write when it starts the lanes.
116
97
 
117
98
  Nothing is written without that answer. The board is a file like the plan: it goes into the same commit, under the
118
99
  same approval.
119
100
 
120
101
  After writing the rows, name the next step in one line: `/orchestrate <board path>`, from a Claude Code session
121
- inside herdr. A plan or board that is not on `origin/{{kit.baseBranch}}` yet needs no merge first: `/orchestrate`
102
+ inside herdr. A plan or board not yet on `origin/{{kit.baseBranch}}` needs no merge first: `/orchestrate`
122
103
  asks to commit it on a documentation branch and starts the lanes from there.
123
104
 
124
105
  Ask every question with a fixed set of answers through the `AskUserQuestion` tool, in {{kit.responseLanguage}}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jackherd",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "keywords": [
5
5
  "claude-code",
6
6
  "coding-agents",
@@ -78,6 +78,7 @@
78
78
  "zod": "^4.0.0"
79
79
  },
80
80
  "devDependencies": {
81
+ "@anthropic-ai/claude-agent-sdk": "0.3.310",
81
82
  "@types/bun": "^1.4.3",
82
83
  "tsup": "^8.5.1",
83
84
  "typescript": "^5.8.3"
package/recipes/README.md CHANGED
@@ -18,6 +18,7 @@ Each recipe is one `watch.sources` entry for the jackherd watcher: put in your k
18
18
  - **A token never goes into an argument:** an argument shows in the process list. Where a tool keeps its own login, it is used; where a token sits in an environment variable, a small TypeScript script run by Bun (which runs wherever `jh` does) puts it from `process.env` straight into the request header. The token reaches the script through the entry's `secrets` (the keychain, for a personal source only; see [sources.md](sources.md)) or through the watcher's environment; either way the script reads `process.env`.
19
19
  - **A script prints one JSON value on stdout and nothing else.**
20
20
  - **A script fails with an exit code**, never with an empty answer the watcher would take for a source with nothing new, and it prints neither the token nor the body of a failed answer. A 200 that carries an `errors` list, as GraphQL answers, is a failure too. Every request and every tool call is bounded with `AbortSignal.timeout(20_000)`, well under the 30 seconds a source may run.
21
+ - **The reason of a failure comes only through one stdout line.** Before it exits, a script may print `jackherd-reason: <reason>` on stdout (`HTTP 401`, `LINEAR_API_KEY not set`); the watcher shows the last such line as `exit 1: <reason>`, cut to 160 characters; a line over 1024 characters gives no reason. stderr stays unread. The line names a status or a missing variable, never a token and never a URL with access in it: the watcher masks the values of the entry's `secrets`, credentials and query values of URLs, but a token the script took from elsewhere it cannot recognise.
21
22
  - **The scripts are files of their own** under [`sources/`](sources/). Copy one to `scripts/sources/<name>.ts` in your repository (a personal source: `~/.jackherd/sources/<name>.ts`), the path the entry's `command` names. The closing `export {}` makes the file a module, so the type check accepts top-level `await` and the names do not clash between scripts.
22
23
  - **A recipe that needs no script** (`gh …`, `curl` on a public page, `curl --netrc`) stays a direct command.
23
24
  - **`bun` as the first word, never a path to it.** The watcher starts a source without a shell and reads `bun` to mean the Bun that runs the watcher itself, so the entry needs no absolute path, which would differ from machine to machine, even when Bun is not on the `PATH` (installed in `~/.bun/bin` only, say).
@@ -4,7 +4,7 @@
4
4
 
5
5
  `acli jira workitem search` cannot return `updated` or `comment`, so a script searches for the keys and reads each one with `view`, a few at a time to stay well under the 30 seconds a source may run. `plainText` joins the text and mention nodes of the comment's document tree into plain text; it is a pure function, so you can test it on its own.
6
6
 
7
- **Script:** copy [sources/jira.ts](sources/jira.ts) to `scripts/sources/jira.ts` in your repository, put your project into `jql`, and set `host` to your `<site>.atlassian.net`. A failed `acli` call ends the script with exit code 1, which the watcher reports as a failing source; nothing is printed that it could take for an empty list.
7
+ **Script:** copy [sources/jira.ts](sources/jira.ts) to `scripts/sources/jira.ts` in your repository, put your project into `jql`, and set `host` to your `<site>.atlassian.net`. A failed `acli` call ends the script with exit code 1, which the watcher reports as a failing source; nothing is printed that it could take for an empty list. Before it exits, the script prints one stdout line `jackherd-reason: <reason>`, so the report reads `exit 1: <reason>`.
8
8
 
9
9
  **Entry:**
10
10
 
@@ -7,8 +7,14 @@ import { addressesOf, civilOf, occurrencesOn, parseIcs, toItems } from './google
7
7
 
8
8
  const zone = Intl.DateTimeFormat().resolvedOptions().timeZone
9
9
 
10
+ // The watcher shows this line with the exit code; it never names an address, the address is the credential.
11
+ const fail: (reason: string) => never = (reason) => {
12
+ console.log(`jackherd-reason: ${reason}`)
13
+ return process.exit(1)
14
+ }
15
+
10
16
  const addresses = addressesOf(process.env)
11
- if (addresses.length === 0) process.exit(1)
17
+ if (addresses.length === 0) fail('GOOGLE_CALENDAR_ICS_URL not set')
12
18
 
13
19
  // The address is the credential: nothing from a failed request (error text, URL) may reach stderr.
14
20
  const readCalendar = async (address: string): Promise<string | undefined> => {
@@ -23,7 +29,7 @@ const readCalendar = async (address: string): Promise<string | undefined> => {
23
29
 
24
30
  const answers = await Promise.all(addresses.map(readCalendar))
25
31
  const texts = answers.filter((text) => text !== undefined)
26
- if (texts.length < answers.length) process.exit(1)
32
+ if (texts.length < answers.length) fail(`${answers.length - texts.length} of ${answers.length} calendars unreadable`)
27
33
 
28
34
  const today = civilOf(Date.now(), zone)
29
35
  const events = texts.flatMap((text) => parseIcs(text, zone))
@@ -12,14 +12,22 @@ const plainText = (node: DocumentNode): string =>
12
12
  ? (node.text ?? node.attrs?.text ?? '')
13
13
  : (node.content ?? []).map(plainText).join('')
14
14
 
15
+ // The watcher shows this line with the exit code; it never carries acli's output.
16
+ const fail: (reason: string) => never = (reason) => {
17
+ console.log(`jackherd-reason: ${reason}`)
18
+ return process.exit(1)
19
+ }
20
+
15
21
  const acli = async (args: string[]): Promise<unknown> => {
16
22
  const child = Bun.spawn(['acli', ...args], { stdout: 'pipe', stderr: 'ignore', signal: AbortSignal.timeout(20_000) })
17
23
  const output = await new Response(child.stdout).text()
18
- if ((await child.exited) !== 0) process.exit(1)
24
+ const code = await child.exited
25
+ const call = `acli ${args.slice(0, 2).join(' ')}`
26
+ if (code !== 0) fail(`${call} exited ${code}`)
19
27
  try {
20
28
  return JSON.parse(output)
21
29
  } catch {
22
- return process.exit(1)
30
+ return fail(`${call} answered no JSON`)
23
31
  }
24
32
  }
25
33
 
@@ -1,6 +1,12 @@
1
1
  // Copy to scripts/sources/linear.ts in your repository; the recipe recipes/linear.md says what to fill in.
2
+ // The watcher shows this line with the exit code; it never carries the token or a URL.
3
+ const fail: (reason: string) => never = (reason) => {
4
+ console.log(`jackherd-reason: ${reason}`)
5
+ return process.exit(1)
6
+ }
7
+
2
8
  const token = process.env.LINEAR_API_KEY
3
- if (!token) process.exit(1)
9
+ if (!token) fail('LINEAR_API_KEY not set')
4
10
 
5
11
  const query = `{ issues(filter: { team: { key: { eq: "ENG" } }, number: { eq: 123 } }) {
6
12
  nodes { identifier title url updatedAt comments { nodes { body user { name } } } } } }`
@@ -11,9 +17,9 @@ const response = await fetch('https://<host>/graphql', {
11
17
  body: JSON.stringify({ query }),
12
18
  signal: AbortSignal.timeout(20_000),
13
19
  })
14
- if (!response.ok) process.exit(1)
20
+ if (!response.ok) fail(`HTTP ${response.status}`)
15
21
  const answer = (await response.json()) as { errors?: unknown }
16
- if (answer.errors) process.exit(1)
22
+ if (answer.errors) fail('GraphQL errors in the answer')
17
23
  console.log(JSON.stringify(answer))
18
24
 
19
25
  export {}
@@ -1,12 +1,18 @@
1
1
  // Copy to scripts/sources/sentry.ts in your repository; the recipe recipes/sentry.md says what to fill in.
2
+ // The watcher shows this line with the exit code; it never carries the token or a URL.
3
+ const fail: (reason: string) => never = (reason) => {
4
+ console.log(`jackherd-reason: ${reason}`)
5
+ return process.exit(1)
6
+ }
7
+
2
8
  const token = process.env.SENTRY_AUTH_TOKEN
3
- if (!token) process.exit(1)
9
+ if (!token) fail('SENTRY_AUTH_TOKEN not set')
4
10
 
5
11
  const response = await fetch(
6
12
  'https://<host>/api/0/projects/<org>/<project>/issues/?query=is:unresolved&statsPeriod=24h',
7
13
  { headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(20_000) },
8
14
  )
9
- if (!response.ok) process.exit(1)
15
+ if (!response.ok) fail(`HTTP ${response.status}`)
10
16
  console.log(JSON.stringify(await response.json()))
11
17
 
12
18
  export {}
@@ -79,7 +79,7 @@ first run of this source: baseline, nothing reported; later a new or changed ent
79
79
  watcher state not read or changed; a running watcher takes a new source over at its next cycle
80
80
  ```
81
81
 
82
- **Keep a personal source.** A source can live in `~/.jackherd/sources.json` instead of the repository: `{ "$schema": "…/schemas/sources.schema.json", "sources": [ … ] }`, entries as above. It runs in every repository where the watcher starts, after the repository's `watch.sources`. On a name clash the repository's source wins and the personal one is skipped with a note. A personal source runs with `~/.jackherd/` as its working directory, so its script is `~/.jackherd/sources/<name>.ts` and its command `["bun", "sources/<name>.ts"]`. `jh watch source <name> --dry-run` finds it too and prints `origin: personal, <path>`; a draft file runs as a repository source, in the main checkout and without the keychain.
82
+ **Keep a personal source.** A source can live in `~/.jackherd/sources.json` instead of the repository: `{ "$schema": "…/schemas/sources.schema.json", "sources": [ … ] }`, entries as above. It runs in every repository where the watcher starts, after the repository's `watch.sources`, unless it carries `"repos": ["owner/name"]` (one to twenty, unique without regard to case): then it runs only where the main checkout's `origin` names one of them, and elsewhere the watcher notes `<name>: not for this repo` (`<name>: no origin` when the checkout has none). `repos` belongs in the personal file only; the repository's config refuses it. `jh watch source <name> --dry-run` of a left-out source prints the note and exits 2 without running anything. On a name clash the repository's source wins and the personal one is skipped with a note. A personal source runs with `~/.jackherd/` as its working directory, so its script is `~/.jackherd/sources/<name>.ts` and its command `["bun", "sources/<name>.ts"]`. `jh watch source <name> --dry-run` finds it too and prints `origin: personal, <path>`; a draft file runs as a repository source, in the main checkout and without the keychain.
83
83
 
84
84
  **Keep a secret for a source.** `"secrets": ["NAME"]` (up to 10 names, `A-Z`, digits and `_`, starting with a letter) puts those variables into the environment of the source's command, and nowhere else. For a personal source each value comes from the keychain (service `jackherd`), else from the watcher's own environment; a repository source gets them from the watcher's environment only, never from the keychain. A value that is found nowhere fails the run with `secret missing: NAME`, an unreachable keychain with `secret store unavailable`; other sources go on. The script reads the value from `process.env` and never prints it.
85
85
 
@@ -91,6 +91,8 @@ jh secret delete NAME
91
91
 
92
92
  **Show entries as content.** `"show": "entries"` opens the source's entries in a herd block of their own under "Your turn", headed by its `label`, instead of the folded row in `Sources`. For a source whose entries are the day's content, such as a calendar; not together with `ticket`.
93
93
 
94
- The watcher and the herd read the repository's config and the personal file again each cycle (about a minute): a new, changed or removed source counts without a restart, a new one runs at once. A changed file that is invalid keeps the previous sources and prints `sources not reloaded: <file>: <reason>`. The rest of the config is read when the watcher starts.
94
+ The watcher and the herd read the repository's config and the personal file again each cycle (about a minute): a new, changed or removed source counts without a restart, a new one runs at once. A changed file that is invalid keeps the previous sources and prints `sources not reloaded: <file>: <reason>`. `deploy`, `feedback`, `previews` and `checks` follow the same way; a change that applies prints `config reloaded: <parts>`, an invalid one keeps every value and prints `config not reloaded: <file>: <reason>`. The board and the language are read when the watcher starts.
95
+
96
+ **Say why a source failed.** A failing command may print one stdout line `jackherd-reason: <reason>` (a status, a missing variable); the watcher shows the last such line as `exit 1: <reason>`, with the values of the source's `secrets` and URL credentials masked, cut to 160 characters. stderr is never read. Name no token in it.
95
97
 
96
98
  Three limits stand: the watcher cannot make a command read only, so name only commands that read. A source in `.jackherd/config.json` runs for everyone who starts the watcher in the repository, at the same level of trust as `check.checks` and hooks. The watcher reads the config of the main checkout, never the one of a lane's worktree; a dry run started inside a worktree reads that worktree's config first. And `jh watch source … --dry-run` runs whatever command its draft names, so it never belongs in a permission allowlist.
@@ -143,6 +143,10 @@
143
143
  "lockFile": {
144
144
  "type": "string",
145
145
  "pattern": "^(?![/-])(?!(?:.*\\/)?\\.\\.(?:\\/|$))[A-Za-z0-9._/-]+$"
146
+ },
147
+ "minFreeGb": {
148
+ "type": "number",
149
+ "exclusiveMinimum": 0
146
150
  }
147
151
  },
148
152
  "additionalProperties": false
@@ -645,6 +649,15 @@
645
649
  "show": {
646
650
  "type": "string",
647
651
  "const": "entries"
652
+ },
653
+ "repos": {
654
+ "minItems": 1,
655
+ "maxItems": 20,
656
+ "type": "array",
657
+ "items": {
658
+ "type": "string",
659
+ "pattern": "^[\\w.-]+\\/[\\w.-]+$"
660
+ }
648
661
  }
649
662
  },
650
663
  "required": [
@@ -699,6 +712,28 @@
699
712
  "type": "integer",
700
713
  "minimum": 1,
701
714
  "maximum": 9
715
+ },
716
+ "standing": {
717
+ "type": "object",
718
+ "properties": {
719
+ "tools": {
720
+ "type": "object",
721
+ "propertyNames": {
722
+ "type": "string",
723
+ "pattern": "^mcp__[A-Za-z0-9_-]+__execute_sql$"
724
+ },
725
+ "additionalProperties": {
726
+ "type": "string",
727
+ "enum": [
728
+ "stage"
729
+ ]
730
+ }
731
+ }
732
+ },
733
+ "required": [
734
+ "tools"
735
+ ],
736
+ "additionalProperties": false
702
737
  }
703
738
  },
704
739
  "additionalProperties": false
@@ -929,6 +964,38 @@
929
964
  "maximum": 9007199254740991
930
965
  }
931
966
  },
967
+ "openBoxes": {
968
+ "default": "fail",
969
+ "type": "string",
970
+ "enum": [
971
+ "fail",
972
+ "warn",
973
+ "off"
974
+ ]
975
+ },
976
+ "lintPointers": {
977
+ "type": "object",
978
+ "properties": {
979
+ "level": {
980
+ "default": "warn",
981
+ "type": "string",
982
+ "enum": [
983
+ "warn",
984
+ "fail",
985
+ "off"
986
+ ]
987
+ },
988
+ "exclude": {
989
+ "default": [],
990
+ "type": "array",
991
+ "items": {
992
+ "type": "string",
993
+ "minLength": 1
994
+ }
995
+ }
996
+ },
997
+ "additionalProperties": false
998
+ },
932
999
  "looseTests": {
933
1000
  "default": false,
934
1001
  "type": "boolean"
@@ -126,6 +126,15 @@
126
126
  "show": {
127
127
  "type": "string",
128
128
  "const": "entries"
129
+ },
130
+ "repos": {
131
+ "minItems": 1,
132
+ "maxItems": 20,
133
+ "type": "array",
134
+ "items": {
135
+ "type": "string",
136
+ "pattern": "^[\\w.-]+\\/[\\w.-]+$"
137
+ }
129
138
  }
130
139
  },
131
140
  "required": [
@@ -0,0 +1,60 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { terminalSafe } from '../plan-format/terminal-text'
3
+ import { isRecord } from '../shared/is-record'
4
+ import type { ApprovalRequestFile } from '../watch/approvals'
5
+
6
+ /** What `jh approval show|checked` prints and how it ends: 0 shown, 1 wrong arguments, 2 no such open request. */
7
+ export type ApprovalReport = { exitCode: number; stdout: string[]; stderr: string[] }
8
+
9
+ type RequestCall = Pick<ApprovalRequestFile, 'lane' | 'cwd' | 'tool' | 'input'>
10
+
11
+ const VERBS = new Set(['show', 'checked'])
12
+ const USAGE = 'Usage: jh approval show|checked <id> <hash>'
13
+ const ID = /^[0-9a-f]{12}$/
14
+ const HASH = /^[0-9a-f]{16}$/
15
+
16
+ /** JSON with the keys of every object sorted, as the approval gate hook hashes a call. */
17
+ const canonicalJson = (value: unknown): string => {
18
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
19
+ if (isRecord(value))
20
+ return `{${Object.keys(value)
21
+ .sort()
22
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
23
+ .join(',')}}`
24
+ return JSON.stringify(value) ?? 'null'
25
+ }
26
+
27
+ /** The hash of a call as the approval gate hook writes it (`callHash`, which this package does not import; contract test). */
28
+ export const requestHash = ({ lane, cwd, tool, input }: RequestCall): string =>
29
+ createHash('sha256').update(canonicalJson({ lane, cwd, tool, input })).digest('hex').slice(0, 16)
30
+
31
+ const refused = (exitCode: number, line: string): ApprovalReport => ({ exitCode, stdout: [], stderr: [line] })
32
+
33
+ /** The request as the coordinator reads it before it marks it: every field the lane wrote, made safe to print. */
34
+ const shown = ({ id, hash, lane, tool, cwd, input }: ApprovalRequestFile): string[] => [
35
+ `approval ${id} ${hash}`,
36
+ `lane: ${terminalSafe(lane)}`,
37
+ `tool: ${terminalSafe(tool)}`,
38
+ `cwd: ${terminalSafe(cwd)}`,
39
+ 'input:',
40
+ ...(JSON.stringify(input, null, 2) ?? 'null').split('\n').map(terminalSafe),
41
+ ]
42
+
43
+ /**
44
+ * `jh approval show <id> <hash>` and `jh approval checked <id> <hash>`: print the open request whose id and hash these
45
+ * are, and whose content its hash covers, and end with 0; otherwise say why and end with 2. Neither writes anything.
46
+ * `show` is for reading; a successful `checked` in the coordinator's transcript is the mark the approval gate hook
47
+ * reads as "the coordinator checked exactly this call", so it comes after the check, never before.
48
+ */
49
+ export const approvalReport = (args: readonly string[], requests: readonly ApprovalRequestFile[]): ApprovalReport => {
50
+ const [verb, id, hash, ...rest] = args
51
+ if (!VERBS.has(verb ?? '') || !ID.test(id ?? '') || !HASH.test(hash ?? '') || rest.length > 0)
52
+ return refused(1, USAGE)
53
+ const request = requests.find((candidate) => candidate.id === id && candidate.usedAt === null)
54
+ if (request === undefined) return refused(2, `no open request ${id}`)
55
+ if (request.hash !== hash)
56
+ return refused(2, `request ${id} has hash ${request.hash}, not ${hash}: it changed, look at it again`)
57
+ if (requestHash(request) !== hash)
58
+ return refused(2, `request ${id} does not match its hash: it was changed by hand, ask the user`)
59
+ return { exitCode: 0, stdout: shown(request), stderr: [] }
60
+ }
@@ -0,0 +1,16 @@
1
+ import { mainCheckout } from '../plans/git'
2
+ import { readApprovalRequests } from '../watch/approvals'
3
+ import { approvalReport } from './checked'
4
+
5
+ /** `jh approval show|checked <id> <hash>`: read a held call, and the coordinator's mark that it checked it (`approvalReport`). */
6
+ export const run = (args: string[]): number => {
7
+ try {
8
+ const report = approvalReport(args, readApprovalRequests(mainCheckout(process.cwd())))
9
+ for (const line of report.stdout) console.log(line)
10
+ for (const line of report.stderr) console.error(line)
11
+ return report.exitCode
12
+ } catch (error) {
13
+ console.error(`jh approval: ${error instanceof Error ? error.message : String(error)}`)
14
+ return 1
15
+ }
16
+ }
@@ -3,17 +3,21 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
3
3
  import { homedir } from 'node:os'
4
4
  import { join, resolve } from 'node:path'
5
5
  import { planRootsIn } from '../../kit/hooks/hooks/shared/boards'
6
+ import { laneOf } from '../../kit/hooks/hooks/shared/lane'
6
7
  import { findProjectRoot, globalConfigPath, locateConfigFile, MANIFEST_FILE, TOOL_DIRECTORY } from '../config/location'
7
8
  import { readRepositoryFacts } from '../config/repository-facts'
9
+ import { installedReferenceHint } from '../lane/skill-hint'
8
10
  import { mainCheckout } from '../plans/git'
9
11
  import { baseRefOf } from '../plans/settings'
12
+ import { REFERENCE } from '../watch/reference-paths'
10
13
  import { type ChangedFile, parsePorcelain, STATUS_COMMAND } from './changes'
11
14
  import { deferralArrowCheck } from './deferral-arrows'
12
15
  import { runCheck, runProcess } from './execute'
13
16
  import { matchesAnyGlob } from './glob'
17
+ import { lintPointerCheck } from './lint-pointers'
14
18
  import { parallelArgument, parallelReportLines } from './parallel-report'
15
19
  import { type CheckPlan, type CheckStack, checkStackOf, type Filesystem, isSlow, planChecks } from './plan'
16
- import { planStructureChecks } from './plan-structure-check'
20
+ import { laneOpenBoxesCheck, planStructureChecks } from './plan-structure-check'
17
21
  import { publicContentCheck } from './public-content-check'
18
22
  import {
19
23
  artifactResult,
@@ -29,6 +33,7 @@ import {
29
33
  } from './report'
30
34
  import { type CheckResult, RUN_MODE, type RunMode, runModeOf } from './result'
31
35
  import { isWorkspaceManifest, kitCopyPaths, WORKSPACE_LIST_FILE, WORKSPACE_MANIFEST } from './roots'
36
+ import { DEFAULT_LINT_POINTERS } from './schema'
32
37
  import { type CheckSettings, CONFIG_PATH, noStackFallbackOf, parseCheckSettings, stackOf } from './settings'
33
38
  import { skillBudgetCheck } from './skill-budgets'
34
39
 
@@ -199,11 +204,50 @@ const planRootsOf = (root: string, worktreeDirectory: string): string[] => {
199
204
  }
200
205
  }
201
206
 
202
- /** The checks jackherd runs itself beside the configured ones: public content, deferral arrows, skill budgets, plan structure. */
203
- const ownChecksOf = (root: string, paths: readonly string[], settings: CheckSettings, home: string) => [
207
+ /** The lane a checkout is for: the first directory below the main checkout's worktree directory; null outside one. */
208
+ const laneOfCheckout = (root: string, worktreeDirectory: string): string | null => {
209
+ try {
210
+ return laneOf({ cwd: root, main: mainCheckout(root), worktreeDirectory })
211
+ } catch {
212
+ return null
213
+ }
214
+ }
215
+
216
+ /** The open boxes of the lane's phase, left out for a checkout that is no lane's. */
217
+ const openBoxesCheckOf = (root: string, settings: CheckSettings): CheckResult | null => {
218
+ const lane = laneOfCheckout(root, settings.worktreeDirectory)
219
+ return lane === null
220
+ ? null
221
+ : laneOpenBoxesCheck({
222
+ root,
223
+ lane,
224
+ planDirectory: settings.planDirectory,
225
+ board: settings.board,
226
+ language: settings.planLanguage,
227
+ level: settings.check.openBoxes,
228
+ baseOf: contentAt(root, baseRefOf(settings)),
229
+ })
230
+ }
231
+
232
+ /** The checks jackherd runs itself beside the configured ones: public content, deferral arrows, skill budgets, lint pointers, plan structure. */
233
+ const ownChecksOf = (
234
+ root: string,
235
+ paths: readonly string[],
236
+ settings: CheckSettings,
237
+ home: string,
238
+ kitCopies: readonly string[],
239
+ ) => [
240
+ openBoxesCheckOf(root, settings),
204
241
  publicContentCheck(root, paths, settings.check.publicContent, home),
205
242
  deferralArrowCheck(root, paths, settings.planDirectory),
206
243
  skillBudgetCheck(root, settings.check.skillBudgets, settings.skillPrefix),
244
+ lintPointerCheck(
245
+ root,
246
+ paths,
247
+ settings.check.lintPointers ?? DEFAULT_LINT_POINTERS,
248
+ kitCopies,
249
+ contentAt(root, 'HEAD'),
250
+ ),
207
251
  ...planStructureChecks({
208
252
  root,
209
253
  paths,
@@ -228,8 +272,9 @@ const checkChanges = async (
228
272
  ): Promise<CheckResult[] | string> => {
229
273
  const stack = checkStackOf(stackOf(settings.stack))
230
274
  let plan: CheckPlan
275
+ let kitCopies: string[]
231
276
  try {
232
- const kitCopies = kitCopyPaths(readJsonIfPresent(join(root, TOOL_DIRECTORY, MANIFEST_FILE)))
277
+ kitCopies = kitCopyPaths(readJsonIfPresent(join(root, TOOL_DIRECTORY, MANIFEST_FILE)))
233
278
  plan = planChecks(
234
279
  files,
235
280
  {
@@ -253,7 +298,7 @@ const checkChanges = async (
253
298
  testCommandResult(settings.commands.test, settings.stack),
254
299
  missingTestCommandResult(settings.commands.test, settings.stack),
255
300
  namedTestResult(plan.sourcesWithoutNamedTest),
256
- ...ownChecksOf(root, plan.paths, settings, home),
301
+ ...ownChecksOf(root, plan.paths, settings, home, kitCopies),
257
302
  ])
258
303
  }
259
304
 
@@ -282,7 +327,7 @@ export const run = async (args: string[], home: string = homedir()): Promise<num
282
327
  }
283
328
  print(settings.notes)
284
329
  if (files.length === 0) {
285
- console.log('Nothing changed.')
330
+ console.log(`Nothing changed.${installedReferenceHint(root, REFERENCE.events, home)}`)
286
331
  return 0
287
332
  }
288
333
  const results = await checkChanges(files, root, settings, runModeOf(args), home)