jackherd 1.15.2 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/INSTALL.md +2 -1
  3. package/README.md +26 -22
  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 +438 -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 +1 -1
  40. package/kit/rules/rules/kit/code-style.md +6 -6
  41. package/kit/rules/rules/kit/writing-for-agents.md +3 -0
  42. package/kit/workflow/skills/follow/SKILL.md +18 -11
  43. package/kit/workflow/skills/implement/SKILL.md +1 -1
  44. package/kit/workflow/skills/orchestrate/SKILL.md +124 -300
  45. package/kit/workflow/skills/orchestrate/references/appointments.md +21 -0
  46. package/kit/workflow/skills/orchestrate/references/close-lane.md +8 -7
  47. package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +8 -8
  48. package/kit/workflow/skills/orchestrate/references/events.md +46 -1
  49. package/kit/workflow/skills/orchestrate/references/herd-ask.md +10 -0
  50. package/kit/workflow/skills/orchestrate/references/lane-handover.md +9 -4
  51. package/kit/workflow/skills/orchestrate/references/lane-session.md +5 -4
  52. package/kit/workflow/skills/orchestrate/references/lane-setup.md +2 -1
  53. package/kit/workflow/skills/orchestrate/references/plan-commit.md +9 -2
  54. package/kit/workflow/skills/orchestrate/references/self-merge.md +26 -0
  55. package/kit/workflow/skills/orchestrate/references/stage-approval.md +50 -0
  56. package/kit/workflow/skills/orchestrate/references/standing-approval.md +38 -0
  57. package/kit/workflow/skills/orchestrate/references/start-lane.md +67 -0
  58. package/kit/workflow/skills/orchestrate/references/waiting.md +16 -0
  59. package/kit/workflow/skills/orchestrate/references/watch-without-monitor.md +13 -0
  60. package/kit/workflow/skills/plan/SKILL.md +3 -2
  61. package/package.json +3 -1
  62. package/recipes/README.md +4 -2
  63. package/recipes/google-calendar.md +30 -0
  64. package/recipes/jira-cli.md +1 -1
  65. package/recipes/sources/google-calendar-today.ts +38 -0
  66. package/recipes/sources/google-calendar.ts +368 -0
  67. package/recipes/sources/jira.ts +10 -2
  68. package/recipes/sources/linear.ts +9 -3
  69. package/recipes/sources/sentry.ts +8 -2
  70. package/recipes/sources.md +17 -3
  71. package/schemas/jackherd.schema.json +67 -0
  72. package/schemas/sources.schema.json +9 -0
  73. package/src/approval/checked.ts +60 -0
  74. package/src/approval/command.ts +16 -0
  75. package/src/check/command.ts +51 -6
  76. package/src/check/lint-pointers.ts +79 -0
  77. package/src/check/lint-shaped.ts +196 -0
  78. package/src/check/plan-structure-check.ts +116 -4
  79. package/src/check/report.ts +9 -3
  80. package/src/check/schema.ts +37 -0
  81. package/src/check/settings.ts +4 -0
  82. package/src/check/skill-budget-defaults.ts +81 -23
  83. package/src/check/skill-budgets.ts +58 -10
  84. package/src/cli.ts +2 -0
  85. package/src/config/personal-sources.ts +46 -10
  86. package/src/config/repository-identity.ts +40 -0
  87. package/src/config/schema.ts +53 -1
  88. package/src/context/command.ts +33 -2
  89. package/src/context/options.ts +33 -8
  90. package/src/context/usage-logs.ts +53 -0
  91. package/src/context/usage.ts +110 -0
  92. package/src/lane/base-command.ts +35 -0
  93. package/src/lane/base.ts +43 -0
  94. package/src/lane/close-check.ts +49 -15
  95. package/src/lane/close-command.ts +71 -11
  96. package/src/lane/close-options.ts +29 -8
  97. package/src/lane/close-survey.ts +13 -1
  98. package/src/lane/close.ts +51 -12
  99. package/src/lane/command.ts +21 -3
  100. package/src/lane/send-command.ts +32 -0
  101. package/src/lane/send-herdr.ts +77 -0
  102. package/src/lane/send-options.ts +23 -0
  103. package/src/lane/send.ts +90 -0
  104. package/src/lane/skill-hint.ts +27 -0
  105. package/src/lane/space-command.ts +30 -0
  106. package/src/lane/space.ts +49 -0
  107. package/src/lane/start-lane-hint.ts +18 -0
  108. package/src/plan-contract/base-check-git.ts +33 -0
  109. package/src/plan-contract/base-check.ts +17 -0
  110. package/src/plan-contract/command.ts +25 -4
  111. package/src/plan-format/plan-structure.ts +17 -5
  112. package/src/plans/ask-coordinator.ts +23 -13
  113. package/src/plans/board-rows.ts +9 -4
  114. package/src/plans/click-hint.ts +4 -0
  115. package/src/plans/command.ts +6 -5
  116. package/src/plans/ensure-panes.ts +14 -3
  117. package/src/plans/git.ts +14 -0
  118. package/src/plans/github.ts +19 -10
  119. package/src/plans/layout.ts +191 -25
  120. package/src/plans/live.ts +57 -14
  121. package/src/plans/plan-panes.ts +21 -4
  122. package/src/plans/row-actions.ts +17 -10
  123. package/src/plans/selection.ts +29 -0
  124. package/src/plans/settings.ts +4 -0
  125. package/src/plans/snapshot.ts +8 -3
  126. package/src/plans/source-rows.ts +35 -2
  127. package/src/resume/command.ts +213 -0
  128. package/src/resume/decision.ts +102 -0
  129. package/src/resume/keeper.ts +65 -0
  130. package/src/resume/location.ts +22 -0
  131. package/src/resume/log.ts +27 -0
  132. package/src/resume/ports.ts +102 -0
  133. package/src/resume/record.ts +116 -0
  134. package/src/resume/targets.ts +84 -0
  135. package/src/resume/texts.ts +27 -0
  136. package/src/secrets/command.ts +15 -5
  137. package/src/sync/herdr-plugin.ts +2 -11
  138. package/src/sync/plan-ask-rules.ts +11 -0
  139. package/src/sync/render.ts +3 -1
  140. package/src/vocabulary/index.ts +13 -0
  141. package/src/waiting/command.ts +13 -4
  142. package/src/watch/command.ts +177 -78
  143. package/src/watch/config-reload.ts +119 -0
  144. package/src/watch/lane-model.ts +11 -1
  145. package/src/watch/lane-progress.ts +70 -8
  146. package/src/watch/lane-report.ts +68 -12
  147. package/src/watch/main-checkout.ts +5 -1
  148. package/src/watch/merge-duties.ts +33 -0
  149. package/src/watch/merge-overlap-read.ts +69 -0
  150. package/src/watch/plan-views.ts +16 -4
  151. package/src/watch/reference-paths.ts +46 -5
  152. package/src/watch/report-questions.ts +5 -2
  153. package/src/watch/single-report.ts +3 -2
  154. package/src/watch/source-dry-run-command.ts +22 -4
  155. package/src/watch/source-dry-run.ts +1 -1
  156. package/src/watch/source-reason.ts +58 -0
  157. package/src/watch/source-reload.ts +21 -17
  158. package/src/watch/source-run.ts +24 -7
  159. package/src/watch/sources.ts +9 -2
  160. package/src/watch/texts.ts +15 -4
  161. package/src/watch/waiting-line.ts +4 -3
@@ -0,0 +1,73 @@
1
+ import { questionsOf, recordsOf } from './run'
2
+
3
+ /**
4
+ * The words the check needs, per response language: the mark a recommended option's label ends with, and the option
5
+ * labels a guard reads word for word (approvals of a lane call, the self-merge and the standing approval). An empty
6
+ * mark, from an install that does not pass one, checks nothing.
7
+ */
8
+ export type RecommendationWords = { mark: string; guardedLabels: readonly string[] }
9
+
10
+ const QUESTION_LENGTH = 80
11
+
12
+ const shorten = (text: string): string =>
13
+ text.length > QUESTION_LENGTH ? `${text.slice(0, QUESTION_LENGTH - 1)}…` : text
14
+
15
+ const labelsOf = (options: unknown): string[] =>
16
+ recordsOf(options).flatMap((option) => (typeof option.label === 'string' ? [option.label] : []))
17
+
18
+ const isMarked = (label: string, mark: string): boolean => label.trimEnd().toLowerCase().endsWith(mark.toLowerCase())
19
+
20
+ const withoutMark = (label: string, mark: string): string =>
21
+ isMarked(label, mark) ? label.trimEnd().slice(0, -mark.length).trimEnd() : label
22
+
23
+ /** A guarded label that carries the mark: the guard would not know the answer, so the dialog goes back. */
24
+ const markedGuardReason = (labels: readonly string[], { mark, guardedLabels }: RecommendationWords): string | null => {
25
+ const label = labels.find(
26
+ (candidate) => isMarked(candidate, mark) && guardedLabels.includes(withoutMark(candidate, mark)),
27
+ )
28
+ return label === undefined
29
+ ? null
30
+ : `Option "${label}": a guard reads "${withoutMark(label, mark)}" word for word; drop "${mark}" from it.`
31
+ }
32
+
33
+ /** The first option carries the mark; else the reason names a later marked option to move up, or asks for one. */
34
+ const firstMarkReason = (question: string, labels: readonly string[], mark: string): string | null => {
35
+ const [first, ...rest] = labels
36
+ if (first !== undefined && isMarked(first, mark)) return null
37
+ const later = rest.find((label) => isMarked(label, mark))
38
+ return later === undefined
39
+ ? `Question "${shorten(question)}": put your recommendation first and end its label with "${mark}".`
40
+ : `Question "${shorten(question)}": the recommendation goes first; move "${later}" to the top.`
41
+ }
42
+
43
+ /** A single choice has one recommendation: the first option carries the mark and no other does. */
44
+ const singleChoiceReason = (question: string, labels: readonly string[], mark: string): string | null => {
45
+ const firstReason = firstMarkReason(question, labels, mark)
46
+ const second = labels.slice(1).find((label) => isMarked(label, mark))
47
+ return firstReason ?? (second === undefined
48
+ ? null
49
+ : `Question "${shorten(question)}": one recommendation only; drop "${mark}" from "${second}".`)
50
+ }
51
+
52
+ const questionReason = (question: Record<string, unknown>, words: RecommendationWords): string | null => {
53
+ const labels = labelsOf(question.options)
54
+ if (labels.length === 0) return null
55
+ const guarded = markedGuardReason(labels, words)
56
+ if (guarded !== null) return guarded
57
+ if (labels.some((label) => words.guardedLabels.includes(label))) return null
58
+ const text = typeof question.question === 'string' ? question.question : ''
59
+ // A multiSelect question may recommend several options, the first among them.
60
+ const reasonOf = question.multiSelect === true ? firstMarkReason : singleChoiceReason
61
+ return reasonOf(text, labels, words.mark)
62
+ }
63
+
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.
68
+ */
69
+ export const recommendationReasonOf = (toolInput: unknown, words: RecommendationWords): string | null => {
70
+ if (words.mark === '') return null
71
+ const [reason] = questionsOf(toolInput).flatMap((question) => questionReason(question, words) ?? [])
72
+ return reason ?? null
73
+ }
@@ -9,16 +9,23 @@ const LANE_DIRECTORY = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/
9
9
 
10
10
  const strings = (values: unknown[]): string[] => values.filter((value): value is string => typeof value === 'string')
11
11
 
12
+ /** The objects of a list, or none when it is no list: the questions of a dialog, the options of a question. */
13
+ export const recordsOf = (values: unknown): Record<string, unknown>[] =>
14
+ Array.isArray(values) ? values.filter(isRecord) : []
15
+
16
+ /** The questions of an `AskUserQuestion` input; none when it is no object or has no list of them. */
17
+ export const questionsOf = (toolInput: unknown): Record<string, unknown>[] =>
18
+ isRecord(toolInput) ? recordsOf(toolInput.questions) : []
19
+
12
20
  const optionTexts = (options: unknown): string[] =>
13
- Array.isArray(options) ? options.filter(isRecord).flatMap((option) => strings([option.label, option.description])) : []
21
+ recordsOf(options).flatMap((option) => strings([option.label, option.description]))
14
22
 
15
23
  /** The texts a dialog shows the user: each question, its header chip, and every option's label and description. */
16
- export const dialogTextsOf = (toolInput: unknown): string[] => {
17
- if (!isRecord(toolInput) || !Array.isArray(toolInput.questions)) return []
18
- return toolInput.questions
19
- .filter(isRecord)
20
- .flatMap((question) => [...strings([question.question, question.header]), ...optionTexts(question.options)])
21
- }
24
+ export const dialogTextsOf = (toolInput: unknown): string[] =>
25
+ questionsOf(toolInput).flatMap((question) => [
26
+ ...strings([question.question, question.header]),
27
+ ...optionTexts(question.options),
28
+ ])
22
29
 
23
30
  /** Every plan the boards link, relative to the repository, once; a path that leaves the repository is dropped. */
24
31
  export const planPathsOfBoards = (boards: readonly Board[], language: BoardLanguage): string[] => [
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A standing approval: the user's answer to a dialog whose question carries ``<Key> `<subject>` <until> <YYYY-MM-DD HH:MM>``
3
+ * lifts a question about that subject until that moment, at most 24 hours after the answer. The key names what is
4
+ * approved (`Plan` for the merges of a plan's lanes, `Environment` for the calls against an environment), so two kinds
5
+ * never read each other's questions. The newest answer to a question naming the subject decides; any other label ends
6
+ * the approval, also on a question without an end: in doubt it ends.
7
+ */
8
+ import type { DialogAnswer } from './dialog-answer'
9
+ import { MS_PER_MINUTE } from './fresh'
10
+ import { newestVerdict } from './newest-verdict'
11
+
12
+ /** The exact option label that approves, and the word before the end in the question, both per response language. */
13
+ export type StandingLabels = { approve: string; until: string }
14
+
15
+ /** What a kind of standing approval is about: the word before the subject, and which subjects it accepts. */
16
+ export type StandingKind = { key: string; isSubject: (subject: string) => boolean }
17
+
18
+ /** A subject approved until `until`, in milliseconds. */
19
+ export type StandingMark = { subject: string; until: number }
20
+
21
+ export const MAX_STANDING_MINUTES = 24 * 60
22
+
23
+ const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
24
+
25
+ /** A local moment from its parts; undefined for one the calendar or the clock does not have (Feb 30, 24:00, a DST gap). */
26
+ const localMoment = ([year, month, day, hour, minute]: number[]): number | undefined => {
27
+ const date = new Date(year, month - 1, day, hour, minute)
28
+ const exact =
29
+ date.getFullYear() === year &&
30
+ date.getMonth() === month - 1 &&
31
+ date.getDate() === day &&
32
+ date.getHours() === hour &&
33
+ date.getMinutes() === minute
34
+ return exact ? date.getTime() : undefined
35
+ }
36
+
37
+ /** The subject and the end a question names as ``<Key> `<subject>` <until> YYYY-MM-DD HH:MM``; none for none or several. */
38
+ export const standingMarkOf = (question: string, kind: StandingKind, until: string): StandingMark | undefined => {
39
+ if (until === '') return undefined
40
+ const pattern = `\\b${escapeRegExp(kind.key)} \`([^\`]*)\` ${escapeRegExp(until)} (\\d{4})-(\\d{2})-(\\d{2}) (\\d{2}):(\\d{2})(?!\\d)`
41
+ const marks = [...question.matchAll(new RegExp(pattern, 'g'))]
42
+ if (marks.length !== 1) return undefined
43
+ const [, subject, ...parts] = marks[0]
44
+ const end = localMoment(parts.map(Number))
45
+ return end !== undefined && kind.isSubject(subject) ? { subject, until: end } : undefined
46
+ }
47
+
48
+ /** The one subject a question names as ``<Key> `<subject>` ``, with or without an end; none for none or several. */
49
+ const namedSubjectOf = (question: string, kind: StandingKind): string | undefined => {
50
+ const mention = new RegExp(`\\b${escapeRegExp(kind.key)} \`([^\`]*)\``, 'g')
51
+ const subjects = new Set([...question.matchAll(mention)].map(([, subject]) => subject))
52
+ const [only] = subjects
53
+ return subjects.size === 1 && kind.isSubject(only) ? only : undefined
54
+ }
55
+
56
+ /**
57
+ * The subject an answer speaks about, and the end its question names; `NaN` for a question that names the subject
58
+ * without an end, which never approves but, answered with anything else, ends the approval: in doubt it ends.
59
+ */
60
+ const markOf = (question: string, kind: StandingKind, until: string): StandingMark | undefined => {
61
+ const mark = standingMarkOf(question, kind, until)
62
+ if (mark !== undefined) return mark
63
+ const subject = namedSubjectOf(question, kind)
64
+ return subject === undefined ? undefined : { subject, until: Number.NaN }
65
+ }
66
+
67
+ type MarkedAnswer = DialogAnswer & { mark: StandingMark }
68
+
69
+ const approves = ({ answer, offered, answeredAt, mark }: MarkedAnswer, label: string): boolean =>
70
+ label !== '' &&
71
+ answer === label &&
72
+ offered.includes(label) &&
73
+ mark.until > answeredAt &&
74
+ mark.until - answeredAt <= MAX_STANDING_MINUTES * MS_PER_MINUTE
75
+
76
+ /** The approval the newest answers about one subject give: all of them approve, and the earliest end is still ahead. */
77
+ const grantOf = (subject: string, answers: readonly MarkedAnswer[], label: string, now: number): StandingMark[] => {
78
+ const verdict = newestVerdict(answers, now)
79
+ if (verdict.length === 0 || !verdict.every((answer) => approves(answer, label))) return []
80
+ const until = Math.min(...verdict.map(({ mark }) => mark.until))
81
+ return until > now ? [{ subject, until }] : []
82
+ }
83
+
84
+ /** The subjects of this kind the user approved standing in these dialog answers, and until when; only those holding at `now`. */
85
+ export const standingGrants = (
86
+ answers: readonly DialogAnswer[],
87
+ kind: StandingKind,
88
+ labels: StandingLabels,
89
+ now: number,
90
+ ): StandingMark[] => {
91
+ const marked = answers.flatMap((answer): MarkedAnswer[] => {
92
+ const mark = markOf(answer.question, kind, labels.until)
93
+ return mark === undefined ? [] : [{ ...answer, mark }]
94
+ })
95
+ const subjects = [...new Set(marked.map(({ mark }) => mark.subject))]
96
+ return subjects.flatMap((subject) =>
97
+ grantOf(
98
+ subject,
99
+ marked.filter(({ mark }) => mark.subject === subject),
100
+ labels.approve,
101
+ now,
102
+ ),
103
+ )
104
+ }
@@ -3,7 +3,7 @@
3
3
  "event": "PermissionRequest",
4
4
  "handler": {
5
5
  "type": "command",
6
- "command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/approval-gate/index.ts '{{kit.approval.allow}}' '{{kit.approval.deny}}' '{{kit.approval.pending}}' '{{kit.approval.denied}}'",
6
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/approval-gate/index.ts '{{kit.approval.allow}}' '{{kit.approval.deny}}' '{{kit.approval.pending}}' '{{kit.approval.denied}}' '{{kit.guard.standingApproval.approve}}' '{{kit.guard.standingApproval.until}}'",
7
7
  "timeout": 600
8
8
  }
9
9
  },
@@ -34,6 +34,15 @@
34
34
  "onFailure": "block"
35
35
  }
36
36
  },
37
+ "lane-start-guard": {
38
+ "event": "PreToolUse",
39
+ "matcher": "Bash",
40
+ "handler": {
41
+ "type": "command",
42
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" && { input=$(cat); case \"$input\" in *herdr*) printf '%s' \"$input\" | bun .claude/hooks/lane-start-guard/index.ts;; esac; }",
43
+ "timeout": 10
44
+ }
45
+ },
37
46
  "merge-guard": {
38
47
  "event": "PreToolUse",
39
48
  "matcher": "Bash",
@@ -74,7 +83,7 @@
74
83
  "matcher": "AskUserQuestion",
75
84
  "handler": {
76
85
  "type": "command",
77
- "command": "cd \"$CLAUDE_PROJECT_DIR\" && bun .claude/hooks/plain-question/index.ts",
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}}'",
78
87
  "timeout": 10
79
88
  }
80
89
  },
@@ -8,11 +8,11 @@ user-invocable: false
8
8
 
9
9
  Read `.claude/rules/kit/writing-for-agents.md` first, then the project's `.claude/rules/writing-for-agents.md` if it exists, then `.claude/project/skill-review.md` if it exists. They carry the levers this review applies; the project files win where they disagree with the kit.
10
10
 
11
- You receive the `git diff` of agent texts: skills, agents, rules, project supplements, `CLAUDE.md`, `AGENTS.md`. Judge only the lines the diff adds, changes or removes, a moved passage where its trigger or rung changed; the rest of a file is context. A budget, a broken link, a required rule and a trigger in the first lines are measured by `jh check`, so leave them to it.
11
+ You receive the `git diff` of agent texts: skills, agents, rules, project supplements, `CLAUDE.md`, `AGENTS.md`. Judge only the lines the diff adds, changes or removes, a moved passage where its trigger or rung changed; the rest of a file is context. A budget, a broken link, a required rule and a trigger in the first lines are measured by `jh check`, so leave them to it. Length: a finding when the diff takes a file over its target or grows one already over, per the `hint:` lines of the `Skill budgets` result /pre-commit passes; that result missing is a finding. `jh check` measures `Lint pointers`; lens 7 judges what its patterns miss.
12
12
 
13
13
  ## What is a finding
14
14
 
15
- Check every changed passage against each lens; a passage that passes all six needs no row.
15
+ Check every changed passage against each lens; a passage that passes all seven needs no row.
16
16
 
17
17
  1. **No-op:** the sentence changes nothing against what the model does by default. Delete the sentence, never trim it.
18
18
  2. **Same meaning twice:** the passage restates, in other words, what the skill, its project supplement, a rule, `CLAUDE.md` or a memory file the caller names already carries. Grep for the key terms before you call it new; name the other place. A passage removed in favour of a pointer is lost when the target lacks part of it.
@@ -20,6 +20,7 @@ Check every changed passage against each lens; a passage that passes all six nee
20
20
  4. **Late trigger:** a pointer to a reference or another document names its condition after the content, or fires at a moment the agent has already passed.
21
21
  5. **Negation:** the passage steers by prohibition where a target could be stated; a hard guardrail stays, paired with its target.
22
22
  6. **Open step:** a step ends without a condition the agent can check, so done and not done look alike.
23
+ 7. **Lint-shaped prose:** a rule the project's linter can check, without a `lint:` pointer. Propose the lint rule plus the pointer.
23
24
 
24
25
  ## Output
25
26
 
@@ -27,7 +27,7 @@ Run `/review` for what the command cannot decide: dead exports, naming, function
27
27
 
28
28
  **Step 1 printed the reviewer list** (`Reviewers: …`, from `check.reviewerRoutes`, plus `check.publicContent` and `skill-review` for changed agent texts) — dispatch exactly those, each as a subagent via the `Agent` tool with its `subagent_type`:
29
29
 
30
- - Pass in the `prompt`: the `git diff` of the relevant files and the file paths.
30
+ - Pass in the `prompt`: the `git diff` of the relevant files and the file paths; to `skill-review`, also the `Skill budgets` and `Lint pointers` results of the latest `jh check` run.
31
31
  - Start all of them **in parallel**, in a single message, and wait for every result before step 4.
32
32
 
33
33
  `security-audit` has a judgement half: add it when the diff *does* something security-relevant in a path no route lists — auth middleware, API key handling, a schema at an HTTP endpoint, a raw database query, an access policy.
@@ -1,13 +1,13 @@
1
1
  # Code Style
2
2
 
3
- The kit's core. Project-specific conventions (UI framework, shared component library, class utilities) live in the project's own rules beside `.claude/rules/kit/`; where they are stricter, they win.
3
+ The kit's core. Project-specific conventions (UI framework, shared component library, class utilities) live in the project's own rules beside `.claude/rules/kit/`; where they are stricter, they win. Where the project's own rule names a `lint:<rule id>` for a rule below, the linter's message is the authority.
4
4
 
5
5
  ## Formatting
6
6
 
7
7
  The formatter and linter config is the authority; where it is silent, match the surrounding code.
8
8
 
9
9
  - No unused imports.
10
- - Blank lines separate logical sections; decorative separator comments (`// ─────────`) stay out.
10
+ - Blank lines separate sections; no decorative separator comments.
11
11
 
12
12
  ## Language
13
13
 
@@ -24,7 +24,7 @@ The formatter and linter config is the authority; where it is silent, match the
24
24
  ## Comments & Documentation
25
25
 
26
26
  - Self-documenting code over comments: clear naming, pure functions, obvious control flow.
27
- - **No section-label comments** (`// Types`, `// Pure functions`, `// Imports`). The code order speaks for itself.
27
+ - **No section-label comments** (`// Types`, `// Imports`): the code order speaks for itself.
28
28
 
29
29
  ### When to add a doc comment
30
30
 
@@ -112,10 +112,10 @@ Before editing a file that might be generated:
112
112
 
113
113
  Roles, modes, statuses, channels: every closed set of values has **one** constant next to its type (an `as const` object, an enum, a class constant), and every comparison, option id and schema literal reads from it.
114
114
 
115
- A type checker already checks a literal against the closed set inside the core. The constant earns its place at the **seams where a `string` arrives**: a query parameter, a form option id, a row read from the database. There a literal comparison is unchecked, and its usual companion — `mode === 'per_km' ? 'per_km' : 'tiers'` — maps every unknown value to a default in silence.
115
+ The constant earns its place at the **seams where a `string` arrives** (a query parameter, a form option id, a database row): there a literal comparison is unchecked, and `mode === 'per_km' ? 'per_km' : 'tiers'` maps every unknown value to a default in silence.
116
116
 
117
117
  ```ts
118
- // ✅ (TypeScript shown) the constant is the vocabulary; a guard narrows the string, an unknown value is refused
118
+ // ✅ the constant is the vocabulary; a guard narrows the string, an unknown value is refused
119
119
  export const FEE_MODE = { tiers: 'tiers', perKm: 'per_km' } as const
120
120
  export type FeeMode = (typeof FEE_MODE)[keyof typeof FEE_MODE]
121
121
  export const isFeeMode = (value: string): value is FeeMode =>
@@ -127,7 +127,7 @@ if (isFeeMode(id)) handleChange(switchMode(form, id))
127
127
  mode: id === 'per_km' ? 'per_km' : 'tiers'
128
128
  ```
129
129
 
130
- Derive the schema from the constant (a literal built from `FEE_MODE.perKm`), never the other way round, so the constant, the type and the parser cannot disagree. Test fixtures may spell the literal.
130
+ Derive the schema from the constant, never the other way round, so constant, type and parser cannot disagree. Test fixtures may spell the literal.
131
131
 
132
132
  ## Database Writes
133
133
 
@@ -57,6 +57,9 @@ A **context pointer** names material that is out of context and encodes the cond
57
57
  - **One trigger per branch.** Synonyms renaming one branch are that branch written twice.
58
58
  - **Cut what the body already carries.** A pointer says when to come, not what is inside.
59
59
  - **Every trigger names something its loader can honour.** A skill with `disable-model-invocation: true` describes what it is for, since the user picks it; a `paths:` list names globs the repository actually has. Check both against reality — a trigger nothing can fire is a line paid for on every load and never earned back.
60
+ - **A mandatory rule behind a pointer needs a trigger that fires without the reader's judgement.** Code raises it (an event line that names the file, a hook's denial, a command's output) or it is a step every run reaches; "read when needed" is no pointer, since a rule reached only when the reader thinks of it is a weakened rule. Test the link: the rule's sentence stands in the target, the target is linked, and the trigger names it.
61
+ - **A rule a hook enforces is one sentence in the text: how a call gets through.** The full rule lives in the hook's denial, which the reader sees at the moment it matters; written out in both places, it is one meaning in two places that drift apart.
62
+ - **A rule the project's linter can check is a lint rule, and the text keeps one sentence naming it as `lint:<rule id>`**, plus the why where it is not obvious. The full rule lives in the linter's message, for the same reason as above.
60
63
 
61
64
  ## The two loads
62
65
 
@@ -1,49 +1,56 @@
1
1
  ---
2
2
  name: follow
3
- description: 'Have the watcher report changes of something outside GitHub (a ticket, an issue, an error, a status page): pick a recipe, check the tool and its login read only, dry-run the source, write it to `watch.sources` after approval. Use when the user asks to follow or be told about such an item, e.g. "/follow PROJ-123".'
3
+ description: 'Have the watcher report changes of something outside GitHub (a ticket, an issue, an error, a status page): pick a recipe, check the tool and its login read only, dry-run the source, write it to the watcher''s sources (personal or repository) after approval. Use when the user asks to follow or be told about such an item, e.g. "/follow PROJ-123".'
4
4
  argument-hint: 'What to follow, e.g. a ticket key, an issue or a query'
5
5
  ---
6
6
 
7
7
  If `.claude/project/follow.md` exists, read it first. It extends this skill for this project and wins where the two disagree.
8
8
 
9
- You add one entry to `watch.sources`, so that `jh watch` reports a new or changed entry of an external service as `=== source <name> …`. You run only reading commands; the one file you write is the project's `.jackherd/config.json` in the main checkout, and only after the user approved the entry. Read the recipe index `recipes/README.md` of the installed package first (`"$(dirname "$(readlink -f "$(command -v jh)")")/../recipes/README.md"`), then only the recipe file it names for the chosen service; do not copy a recipe from memory. Where the recipe leaves a field open, the index's section **Rules every recipe follows** decides, and `recipes/sources.md` beside it describes every field and limit of a source.
9
+ You add one entry to the watcher's sources, so that `jh watch` reports a new or changed entry of an external service as `=== source <name> …`. You run only reading commands; the one file you write is the entry's file (step 4: the user's `~/.jackherd/sources.json` or the project's `.jackherd/config.json` in the main checkout), and only after the user approved the entry. Read the recipe index `recipes/README.md` of the installed package first (`"$(dirname "$(readlink -f "$(command -v jh)")")/../recipes/README.md"`), then only the recipe file it names for the chosen service; do not copy a recipe from memory. The index's section **Rules every recipe follows** binds every script and direct command, and decides any field the recipe leaves open, and `recipes/sources.md` beside it describes every field and limit of a source.
10
10
 
11
11
  ## 1. Ask
12
12
 
13
13
  `AskUserQuestion` in {{kit.responseLanguage}}, only for what the argument and the conversation leave open, each with the recommended option first; at most four questions per call, so more open points go into a second call:
14
14
 
15
+ - **Where it lives:** a personal source (`~/.jackherd/sources.json`: runs in every repository of the user, the keychain can hold its secrets; recommended when a personal secret is attached, otherwise second) or a repository source (`watch.sources` of the project: runs for everyone who starts the watcher there, and its `secrets` read only the watcher's environment, never the keychain, so it needs a token from the environment or none).
15
16
  - **Service:** the services the recipe index lists, and "another command that prints JSON".
16
17
  - **What to follow:** the item the argument names (its comments, recommended) or a query over several items.
17
18
  - **Lane:** the lane the reports name, from the board's lanes table, or none.
18
19
  - **Ticket:** whether the entries are tickets (`ticket: true`, recommended for an issue tracker or a project board), which the herd shows as a badge beside the plan of their lane: the lane above, or the lane whose name, board row or plan names the ticket's id. A source of comments or messages is no ticket; it shows in the herd's `Sources` block instead (a click opens the entry at `map.url`). The path of the status you find yourself in step 3.
19
- - **Tone:** for a non-ticket source, the colour of its `Sources` mark while its report is new (`tone`: amber, teal, grey, red, green, blue, magenta); recommended: no `tone`.- **What reports:** a change of what the user reads (title, body, the latest comment; recommended), or a change of named fields such as a status (`reportOn` with those fields, mapped in `map`).
20
+ - **Tone:** for a non-ticket source, the colour of its `Sources` mark while its report is new (`tone`: amber, teal, grey, red, green, blue, magenta); recommended: no `tone`.
21
+ - **Show:** for a non-ticket source whose entries are the day's content (a calendar), `show: "entries"` opens them in a block of their own under "Your turn"; otherwise none.
22
+ - **What reports:** a change of what the user reads (title, body, the latest comment; recommended), or a change of named fields such as a status (`reportOn` with those fields, mapped in `map`).
20
23
  - **Own changes:** ignore the user's own account on that service (`ignoreAuthors`, needs an author in the map), or report everything.
21
24
 
22
25
  ## 2. Check, read only
23
26
 
24
27
  - The tool: `command -v <tool>` for the recipe's program (`curl`, the service's CLI, and `bun` for a recipe with a script).
25
- - The login: the tool's own status command where it has one. For a token in an environment variable only whether it is set: `test -n "$<VARIABLE>" && echo set || echo missing`. Never print, echo or pass on a token's value, and never read a file that holds one.
28
+ - The login: the tool's own status command where it has one. For a token in an environment variable only whether it is set: `test -n "$<VARIABLE>" && echo set || echo missing`. Never print, echo or pass on a token's value, never ask for it, and never read a file that holds one.
26
29
 
27
- Anything missing goes to the user as the command to run themselves (`! <command>`); wait until they did, then check again.
30
+ Anything missing goes to the user as the command to run themselves (`! <command>`; a keychain secret: below, not with `!`); wait until they did, then check again.
31
+
32
+ **A secret** (a token the script needs) is named in the entry's `secrets` (`["NAME"]`) and never passes through you (the rule above). A repository source does not read the keychain: check its environment variable as above. A personal source's secret goes into the keychain in step 4, after the entry is written: `jh secret list` knows only names an entry already lists.
28
33
 
29
34
  ## 3. Fill and dry-run
30
35
 
31
36
  Before filling, read the help of the installed CLI version for every command the recipe calls (`acli jira workitem search --help` and `workitem view --help`, `gh issue list --help`) and confirm that the fields and options the entry needs exist there. Read the provider's web documentation only when the help leaves it open or no recipe fits. A recipe is a starting point; the installed version decides.
32
37
 
33
- Fill the recipe with the item or query and the answers from step 1: a `name` of letters, digits, `.`, `_` and `-` that says what is followed, `lane`, `ignoreAuthors`, `ticket` and `tone` where chosen, and a short `label` (e.g. `Error log · Checkout`; it prefixes the source's `Waiting for you` rows). For a ticket source, find the path of the status in the output (from the CLI's help, or the dry run with `--raw`) and set it as `map.status`; the entry you show for approval in step 4 carries it, and without a status in the output the entry has none and the badge shows only the key. A recipe that needs a script (a token from the environment, or one call per item) gets TypeScript run by Bun: write it, from the recipe's script under `recipes/sources/`, to the scratchpad first as `<name>.ts`, never into the repository before step 4, and let the entry's `command` be `["bun", "scripts/sources/<name>.ts"]`. Script and direct command follow the index's **Rules every recipe follows**.
34
-
38
+ Fill the recipe with the item or query and the answers from step 1: a `name` of letters, digits, `.`, `_` and `-` that says what is followed, `lane`, `ignoreAuthors`, `ticket` and `tone` where chosen, and a short `label` (e.g. `Error log · Checkout`; it prefixes the source's `Waiting for you` rows). For a ticket source, find the path of the status in the output (from the CLI's help, or the dry run with `--raw`) and set it as `map.status`; the entry you show for approval in step 4 carries it, and without a status in the output the entry has none and the badge shows only the key. A recipe that needs a script (a token from the environment, or one call per item) gets TypeScript run by Bun: write it, from the recipe's script under `recipes/sources/`, to the scratchpad first as `<name>.ts`, never into the repository before step 4, and let the entry's `command` be `["bun", "scripts/sources/<name>.ts"]` (a personal source runs in `~/.jackherd/`: `["bun", "sources/<name>.ts"]`).
35
39
  Write the entry as one JSON object to a draft file in the scratchpad and run, from the main checkout:
36
40
 
37
41
  ```bash
38
42
  jh watch source <draft.json> --dry-run
39
43
  ```
40
44
 
41
- It runs the command once and shows the entries and the report the watcher would print, without touching the watcher's state. Exit code 1 means the run failed or a `map` field is empty in every entry (`map.<field>: in … entries empty (<path>)`): never write such an entry, correct the path or drop the field. When the run fails or the entries look wrong, run it again with `--raw` to see the command's output, correct `items` or a path of `map`, and try again; after two failed corrections, show the user the reason and the output's first lines and ask how to go on. Show the user the dry run's list and report, not the raw output.
45
+ It runs the command once and shows the entries and the report the watcher would print, without touching the watcher's state. A draft runs as a repository source: in the main checkout, without the keychain. A personal entry with `secrets` or a script under `sources/` cannot pass there; skip its draft run, show the entry and the recipe's sample output, and dry-run it by name in step 4. Exit code 1 means the run failed or a `map` field is empty in every entry (`map.<field>: in … entries empty (<path>)`): never write such an entry, correct the path or drop the field. When the run fails or the entries look wrong, run it again with `--raw` to see the command's output, correct `items` or a path of `map`, and try again; after two failed corrections, show the user the reason and the output's first lines and ask how to go on. Show the user the dry run's list and report, not the raw output.
42
46
 
43
47
  ## 4. Write after approval
44
48
 
45
- Ask through `AskUserQuestion`: write the entry (recommended), change it, or drop it. Say in the question that the source runs for everyone who starts the watcher in this repository, and that the watcher cannot keep a command from writing, so the command only reads.
49
+ Ask through `AskUserQuestion`: write the entry (recommended), change it, or drop it. Say in the question which file it goes to and that the watcher cannot keep a command from writing, so the command only reads; for a repository source add that it runs for everyone who starts the watcher in this repository.
50
+
51
+ On approval write exactly one of these files, and a script from step 3 under the path the entry names:
46
52
 
47
- On approval add the entry to `watch.sources` in `.jackherd/config.json` of the main checkout (create the array if missing), and a script from step 3 under the path the entry names. Then run `jh config`, which fails on an invalid config, and `jh watch source <name> --dry-run` once more against the written entry.
53
+ - **Personal:** add the entry to `sources` in `~/.jackherd/sources.json`; create the file as `{ "$schema": "<installed package>/schemas/sources.schema.json", "sources": [] }` if missing (the package directory is the one the recipe path above resolves to, minus `/recipes/README.md`). The script goes to `~/.jackherd/sources/<name>.ts`. A repository source of the same `name` wins over it, so pick a name the project does not use. For each name in `secrets`, ask the user to run `jh secret set <NAME>` in a terminal of their own (a separate pane; it asks hidden and refuses without a terminal), wait, then check only `jh secret list`: `set` is fine, `missing` means they still have to, `set (environment)` means the watcher's environment must carry it too, `secret store unavailable` means it can come only from that environment. Then run `jh watch source <name> --dry-run`; it prints `origin: personal`. A failure here is corrected in the written entry as in step 3.
54
+ - **Repository:** add the entry to `watch.sources` in `.jackherd/config.json` of the main checkout (create the array if missing). Then run `jh config`, which fails on an invalid config, and `jh watch source <name> --dry-run` once more.
48
55
 
49
- End with one line: the watcher takes the source over at its next start, so a running watcher (the coordinator's stream) has to be started again. The new file and the changed config are part of the repository and follow its commit rules.
56
+ End with one line: a running watcher and the herd take the source over at their next cycle (about a minute); a file that is invalid then keeps the previous sources and prints `sources not reloaded`. A repository entry and its script are part of the repository and follow its commit rules; the personal file is not.
@@ -35,7 +35,7 @@ When the user asks for more than one phase:
35
35
 
36
36
  - **Tick off tasks as you go.** As soon as a task is done, set `- [ ]` → `- [x]` in the plan document, right then, so the plan reflects real progress and the herd shows it while the phase runs.
37
37
  - **A subagent ticks only its own phase,** and only with `Edit` on the exact task line (enough of it to be unique in the file). Never `Write` the plan or rewrite it with a script: parallel subagents share the file, and `Edit` applies its replacement to the file as it is on disk, so ticks in other phases survive, while a whole-file write from an older read erases them. A line that is not unique, and every review-gate item, stays with the caller.
38
- - **A task you add starts with its short name:** `- [ ] **Name**: …`, the name 40 characters at most, naming the subject and the result; how, conditions, origin and evidence follow the colon (`/plan`, step 2). A task the user adds later ends with `(User <DD.MM.> <HH:MM>)`.
38
+ - **A task you add, or one the user adds later,** takes the form of `/plan` step 2: `- [ ] **Name**: …`.
39
39
  - **The review gate is binding.** A phase is finished once its review gate has been walked through completely and ticked off; only then comes the next phase or `/pre-commit`. Gate items that name a skill (`/uncle-bob`, `/security-audit`, …) are ticked after the skill has actually run.
40
40
 
41
41
  ## Step 1: Explore