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.
- package/CHANGELOG.md +28 -0
- package/INSTALL.md +2 -1
- package/README.md +29 -33
- package/dist/{chunk-4LUFR7RC.js → chunk-N5FAOQZL.js} +7 -0
- package/dist/config.d.ts +33 -1
- package/dist/config.js +57 -5
- package/dist/plan-format.d.ts +18 -6
- package/dist/plan-format.js +8 -1
- package/docs/plan-contract.md +1 -0
- package/herdr-plugin/herdr-plugin.toml +4 -1
- package/herdr-plugin/restore-plans.ts +60 -26
- package/herdr-plugin/resume.ts +18 -0
- package/install.sh +1 -1
- package/kit/hooks/ask-rules.json +11 -0
- package/kit/hooks/hooks/approval-gate/checked-marks.ts +59 -0
- package/kit/hooks/hooks/approval-gate/index.ts +36 -7
- package/kit/hooks/hooks/approval-gate/run.ts +72 -16
- package/kit/hooks/hooks/approval-gate/settings.ts +27 -1
- package/kit/hooks/hooks/approval-gate/standing-history.ts +90 -0
- package/kit/hooks/hooks/approval-gate/standing-sql.ts +58 -0
- package/kit/hooks/hooks/approval-gate/transcripts.ts +11 -5
- package/kit/hooks/hooks/lane-start-guard/decision.ts +74 -0
- package/kit/hooks/hooks/lane-start-guard/index.ts +36 -0
- package/kit/hooks/hooks/merge-guard/always-ask.ts +2 -2
- package/kit/hooks/hooks/merge-guard/approval.ts +25 -13
- package/kit/hooks/hooks/merge-guard/change-id.ts +34 -0
- package/kit/hooks/hooks/merge-guard/decision.ts +423 -35
- package/kit/hooks/hooks/merge-guard/pane-input.ts +61 -0
- package/kit/hooks/hooks/merge-guard/pane-process-info.ts +38 -0
- package/kit/hooks/hooks/merge-guard/run.ts +110 -28
- package/kit/hooks/hooks/merge-guard/standing-approval.ts +15 -78
- package/kit/hooks/hooks/plain-question/form.ts +89 -0
- package/kit/hooks/hooks/plain-question/index.ts +44 -4
- package/kit/hooks/hooks/plain-question/recommendation.ts +73 -0
- package/kit/hooks/hooks/plain-question/run.ts +14 -7
- package/kit/hooks/hooks/shared/standing-mark.ts +104 -0
- package/kit/hooks/hooks.json +11 -2
- package/kit/review/agents/skill-review.md +3 -2
- package/kit/review/skills/pre-commit/SKILL.md +16 -22
- package/kit/rules/rules/kit/code-style.md +26 -54
- package/kit/rules/rules/kit/pull-requests.md +17 -40
- package/kit/rules/rules/kit/testing.md +6 -12
- package/kit/rules/rules/kit/writing-for-agents.md +3 -0
- package/kit/workflow/skills/follow/SKILL.md +2 -3
- package/kit/workflow/skills/implement/SKILL.md +28 -43
- package/kit/workflow/skills/orchestrate/SKILL.md +201 -458
- package/kit/workflow/skills/orchestrate/references/appointments.md +21 -0
- package/kit/workflow/skills/orchestrate/references/approvals.md +23 -0
- package/kit/workflow/skills/orchestrate/references/close-lane.md +8 -7
- package/kit/workflow/skills/orchestrate/references/coordinator-handover.md +8 -8
- package/kit/workflow/skills/orchestrate/references/events.md +46 -1
- package/kit/workflow/skills/orchestrate/references/herd-ask.md +10 -0
- package/kit/workflow/skills/orchestrate/references/lane-handover.md +9 -4
- package/kit/workflow/skills/orchestrate/references/lane-session.md +5 -4
- package/kit/workflow/skills/orchestrate/references/lane-setup.md +2 -1
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +10 -2
- package/kit/workflow/skills/orchestrate/references/self-merge.md +26 -0
- package/kit/workflow/skills/orchestrate/references/stage-approval.md +50 -0
- package/kit/workflow/skills/orchestrate/references/standing-approval.md +38 -0
- package/kit/workflow/skills/orchestrate/references/start-lane.md +67 -0
- package/kit/workflow/skills/orchestrate/references/waiting.md +16 -0
- package/kit/workflow/skills/orchestrate/references/watch-without-monitor.md +13 -0
- package/kit/workflow/skills/plan/SKILL.md +29 -48
- package/package.json +2 -1
- package/recipes/README.md +1 -0
- package/recipes/jira-cli.md +1 -1
- package/recipes/sources/google-calendar-today.ts +8 -2
- package/recipes/sources/jira.ts +10 -2
- package/recipes/sources/linear.ts +9 -3
- package/recipes/sources/sentry.ts +8 -2
- package/recipes/sources.md +4 -2
- package/schemas/jackherd.schema.json +67 -0
- package/schemas/sources.schema.json +9 -0
- package/src/approval/checked.ts +60 -0
- package/src/approval/command.ts +16 -0
- package/src/check/command.ts +51 -6
- package/src/check/lint-pointers.ts +79 -0
- package/src/check/lint-shaped.ts +196 -0
- package/src/check/plan-structure-check.ts +116 -4
- package/src/check/report.ts +9 -3
- package/src/check/schema.ts +37 -0
- package/src/check/settings.ts +4 -0
- package/src/check/skill-budget-defaults.ts +90 -32
- package/src/check/skill-budgets.ts +96 -18
- package/src/cli.ts +2 -0
- package/src/config/personal-sources.ts +46 -10
- package/src/config/repository-identity.ts +40 -0
- package/src/config/schema.ts +53 -1
- package/src/context/command.ts +33 -2
- package/src/context/options.ts +33 -8
- package/src/context/usage-logs.ts +53 -0
- package/src/context/usage.ts +110 -0
- package/src/lane/base-command.ts +35 -0
- package/src/lane/base.ts +43 -0
- package/src/lane/close-check.ts +49 -15
- package/src/lane/close-command.ts +71 -11
- package/src/lane/close-options.ts +29 -8
- package/src/lane/close-survey.ts +13 -1
- package/src/lane/close.ts +51 -12
- package/src/lane/command.ts +21 -3
- package/src/lane/send-command.ts +32 -0
- package/src/lane/send-herdr.ts +77 -0
- package/src/lane/send-options.ts +23 -0
- package/src/lane/send.ts +90 -0
- package/src/lane/skill-hint.ts +27 -0
- package/src/lane/space-command.ts +30 -0
- package/src/lane/space.ts +49 -0
- package/src/lane/start-lane-hint.ts +18 -0
- package/src/plan-contract/base-check-git.ts +33 -0
- package/src/plan-contract/base-check.ts +17 -0
- package/src/plan-contract/command.ts +25 -4
- package/src/plan-format/plan-structure.ts +17 -5
- package/src/plans/ask-coordinator.ts +23 -13
- package/src/plans/board-rows.ts +9 -4
- package/src/plans/click-hint.ts +4 -0
- package/src/plans/command.ts +6 -5
- package/src/plans/ensure-panes.ts +14 -3
- package/src/plans/git.ts +14 -0
- package/src/plans/github.ts +19 -10
- package/src/plans/layout.ts +82 -33
- package/src/plans/live.ts +8 -12
- package/src/plans/plan-panes.ts +21 -4
- package/src/plans/selection.ts +29 -0
- package/src/plans/settings.ts +4 -0
- package/src/resume/command.ts +213 -0
- package/src/resume/decision.ts +102 -0
- package/src/resume/keeper.ts +65 -0
- package/src/resume/location.ts +22 -0
- package/src/resume/log.ts +27 -0
- package/src/resume/ports.ts +102 -0
- package/src/resume/record.ts +116 -0
- package/src/resume/targets.ts +84 -0
- package/src/resume/texts.ts +27 -0
- package/src/secrets/command.ts +15 -5
- package/src/sync/herdr-plugin.ts +2 -11
- package/src/sync/plan-ask-rules.ts +11 -0
- package/src/sync/render.ts +3 -1
- package/src/vocabulary/index.ts +13 -0
- package/src/waiting/command.ts +13 -4
- package/src/watch/command.ts +177 -78
- package/src/watch/config-reload.ts +119 -0
- package/src/watch/lane-model.ts +11 -1
- package/src/watch/lane-progress.ts +70 -8
- package/src/watch/lane-report.ts +66 -11
- package/src/watch/main-checkout.ts +5 -1
- package/src/watch/merge-duties.ts +33 -0
- package/src/watch/merge-overlap-read.ts +69 -0
- package/src/watch/plan-views.ts +16 -4
- package/src/watch/reference-paths.ts +46 -5
- package/src/watch/report-questions.ts +5 -2
- package/src/watch/single-report.ts +3 -2
- package/src/watch/source-dry-run-command.ts +22 -4
- package/src/watch/source-reason.ts +58 -0
- package/src/watch/source-reload.ts +21 -17
- package/src/watch/source-run.ts +24 -7
- package/src/watch/sources.ts +9 -2
- package/src/watch/texts.ts +14 -3
- package/src/watch/waiting-line.ts +4 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
|
|
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
|
+
}
|
package/kit/hooks/hooks.json
CHANGED
|
@@ -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
|
|
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
|
|
|
@@ -13,42 +13,38 @@ You are the **Pre-Commit Orchestrator**. You measure what a command can measure,
|
|
|
13
13
|
bunx jackherd check
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Its output is **the evidence for steps 1, 3 and 4**: the changed files (untracked included), the affected package roots, the reviewers the changed paths call for, the files a generator owns, staged build artifacts, lint, types and tests per package root, and the project's own checks (the `check` block of `.jackherd/config.json`).
|
|
17
17
|
|
|
18
|
-
**Quote its results; restate nothing from your own reading of the diff.**
|
|
18
|
+
**Quote its results; restate nothing from your own reading of the diff.** An unmeasured verdict is a guess, and its failure mode is a confident false PASS. Where the command says `❌`, the summary says FIX FIRST.
|
|
19
19
|
|
|
20
20
|
`--skip-slow` drops types and tests. Use it only when the user asks for a quick pass, and say so in the summary.
|
|
21
21
|
|
|
22
22
|
## Step 2: Judgement Review
|
|
23
23
|
|
|
24
|
-
Run `/review` for what the command cannot decide: dead exports, naming, function size, the design review.
|
|
24
|
+
Run `/review` for what the command cannot decide: dead exports, naming, function size, the design review.
|
|
25
25
|
|
|
26
26
|
## Step 3: Specialized Reviewers
|
|
27
27
|
|
|
28
|
-
**Step 1 printed the reviewer list** (`Reviewers:
|
|
28
|
+
**Step 1 printed the reviewer list** (`Reviewers: …`) — dispatch exactly those, each as a subagent via the `Agent` tool with its `subagent_type`:
|
|
29
29
|
|
|
30
|
-
- Pass
|
|
30
|
+
- Pass the `git diff` of the relevant files and their 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
|
-
`security-audit`
|
|
33
|
+
Add `security-audit` 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.
|
|
34
34
|
|
|
35
|
-
**`spec-review` starts in the same message.** It
|
|
35
|
+
**`spec-review` starts in the same message.** It applies whichever paths changed. Pass it the full `git diff`, the commit list (`git log <base>..HEAD --oneline`, base `{{kit.baseBranch}}`), the base ref and the branch name.
|
|
36
36
|
|
|
37
37
|
## Step 4: Generated Files
|
|
38
38
|
|
|
39
|
-
Step 1 lists the changed files a generator owns
|
|
39
|
+
Step 1 lists the changed files a generator owns. Ask when one looks hand-edited rather than regenerated.
|
|
40
40
|
|
|
41
41
|
## Step 5: Spec Alignment
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Steps 2 and 3 judge whether the code is written well (**Standards**), `spec-review` whether it does what was asked (**Spec**). Report the two side by side, each under its own heading.
|
|
44
44
|
|
|
45
|
-
## Step 6: arc42
|
|
45
|
+
## Step 6: arc42 and Architecture
|
|
46
46
|
|
|
47
|
-
Apply the filter in `.claude/rules/kit/arc42.md` to the changed files. On a hit, **ask the user concretely**. No hit for bug fixes, styling
|
|
48
|
-
|
|
49
|
-
## Step 7: Architecture Check
|
|
50
|
-
|
|
51
|
-
For changes in a service, a domain package or a data layer, apply the filter in `.claude/rules/kit/architecture-files.md`. On a hit, **ask the user concretely** whether the unit's `ARCHITECTURE.md` needs an update.
|
|
47
|
+
Apply the filter in `.claude/rules/kit/arc42.md` to the changed files; for a service, a domain package or a data layer, also the one in `.claude/rules/kit/architecture-files.md`. On a hit, **ask the user concretely**. No hit for bug fixes, styling or refactorings without decision character.
|
|
52
48
|
|
|
53
49
|
## Output Format
|
|
54
50
|
|
|
@@ -75,19 +71,17 @@ Active reviewers: Review + Spec + [routed reviewers]
|
|
|
75
71
|
### Verdict: COMMIT / FIX FIRST
|
|
76
72
|
```
|
|
77
73
|
|
|
78
|
-
On FIX FIRST, apply what a tool fixes on its own
|
|
79
|
-
|
|
80
|
-
A second review round after fixes dispatches only the reviewers whose findings were applied, plus the reviewers of any path the fixes newly touched (the reviewer list of the new `check` run); `jh check` itself always runs in full.
|
|
74
|
+
On FIX FIRST, apply what a tool fixes on its own (`{{kit.command.format}}`, lint auto-fixes), then re-run `bunx jackherd check` and quote the new result. A second review round dispatches only the reviewers whose findings were applied, plus those of any path the fixes newly touched; `jh check` always runs in full.
|
|
81
75
|
|
|
82
|
-
Architecture problems, missing tests, domain logic errors and **every Spec finding** are reported for the user to decide:
|
|
76
|
+
Architecture problems, missing tests, domain logic errors and **every Spec finding** are reported for the user to decide: a dropped requirement or a grown scope is the user's call.
|
|
83
77
|
|
|
84
78
|
## Commit
|
|
85
79
|
|
|
86
80
|
On COMMIT, propose a Conventional Commit message (`type(scope): description`, imperative, subject at most 72 characters; the body explains cause and consequence in prose).
|
|
87
81
|
|
|
88
|
-
Ask the commit question with the `AskUserQuestion` tool,
|
|
82
|
+
Ask the commit question with the `AskUserQuestion` tool, in {{kit.responseLanguage}}: commit with this message / adjust the message / cancel.
|
|
89
83
|
|
|
90
|
-
On confirmation, write the message to a file in your scratchpad with the `Write` tool, stage **the files step 1 listed** by name, and commit from the file:
|
|
84
|
+
On confirmation, write the message to a file in your scratchpad with the `Write` tool, stage **the files step 1 listed** by name, and commit from the file, each as one Bash call:
|
|
91
85
|
|
|
92
86
|
```bash
|
|
93
87
|
git add -- "<path from step 1>" "<path from step 1>"
|
|
@@ -97,4 +91,4 @@ git add -- "<path from step 1>" "<path from step 1>"
|
|
|
97
91
|
git commit -F "<message file>"
|
|
98
92
|
```
|
|
99
93
|
|
|
100
|
-
|
|
94
|
+
A chained command cannot be approved as one; a message passed through the shell (`-m "…"`, a heredoc) can run the backticks and `$(…)` it quotes. Let the repository's own commit hooks run.
|
|
@@ -1,32 +1,29 @@
|
|
|
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
|
|
10
|
+
- Blank lines separate sections; no decorative separator comments.
|
|
11
11
|
|
|
12
12
|
## Language
|
|
13
13
|
|
|
14
14
|
- **All code in English**: file names, identifiers, type and component names, comments. User-facing strings go through the project's i18n system.
|
|
15
15
|
- **Commit messages in English**, subject and body. The subject is a Conventional Commit (`fix(scope): …`); the body explains cause and consequence in prose.
|
|
16
16
|
- **Plans and ADRs** are written in {{kit.planLanguage}}.
|
|
17
|
-
- **Replies to the user** are in {{kit.responseLanguage}}, addressing the user with {{kit.responseAddress}}
|
|
17
|
+
- **Replies to the user** are in {{kit.responseLanguage}}, addressing the user with {{kit.responseAddress}}: every answer, question and report. Code, commits and plans keep their own language.
|
|
18
18
|
|
|
19
19
|
## Naming
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
- Keep naming aligned with the existing conventions and folder structure.
|
|
21
|
+
**No abbreviations in variable names**: `scrollElement`, not `scrollEl`. Established exceptions: `i`/`j` in tight loops, `el` in single-line callbacks, `err` in catch blocks.
|
|
23
22
|
|
|
24
23
|
## Comments & Documentation
|
|
25
24
|
|
|
26
25
|
- Self-documenting code over comments: clear naming, pure functions, obvious control flow.
|
|
27
|
-
- **No section-label comments** (`// Types`, `//
|
|
28
|
-
|
|
29
|
-
### When to add a doc comment
|
|
26
|
+
- **No section-label comments** (`// Types`, `// Imports`): the code order speaks for itself.
|
|
30
27
|
|
|
31
28
|
Add a doc comment (JSDoc, PHPDoc …) to an exported item when **any** of these is true:
|
|
32
29
|
|
|
@@ -34,7 +31,7 @@ Add a doc comment (JSDoc, PHPDoc …) to an exported item when **any** of these
|
|
|
34
31
|
- The function has non-obvious constraints or behaviour (serialization limits, a required context, a side-effect contract)
|
|
35
32
|
- It has edge cases or a subtle contract a caller could easily violate
|
|
36
33
|
|
|
37
|
-
A
|
|
34
|
+
A one-liner that states the obvious (`/** Returns the sum */`) is noise.
|
|
38
35
|
|
|
39
36
|
## Code Organization
|
|
40
37
|
|
|
@@ -44,51 +41,34 @@ A well-named function with typed parameters documents itself; one-liners that st
|
|
|
44
41
|
2. **Pure functions**: no side effects, no I/O, predictable outputs
|
|
45
42
|
3. **Higher-order functions**: functions that create side effects, orchestrate I/O, or compose other functions
|
|
46
43
|
|
|
47
|
-
```typescript
|
|
48
|
-
import { foo } from './bar'
|
|
49
|
-
|
|
50
|
-
export type Config = { ... }
|
|
51
|
-
|
|
52
|
-
const parse = (input: string): Config => { ... }
|
|
53
|
-
const validate = (config: Config): boolean => { ... }
|
|
54
|
-
|
|
55
|
-
export const processFile = async (path: string): Promise<void> => {
|
|
56
|
-
const config = parse(await readFile(path))
|
|
57
|
-
if (validate(config)) await saveResult(config)
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
44
|
## Framework as an Adapter, not Domain
|
|
62
45
|
|
|
63
|
-
The UI framework is a **replaceable adapter layer**,
|
|
46
|
+
The UI framework is a **replaceable adapter layer**, so the logic stays unit-testable without a component harness.
|
|
64
47
|
|
|
65
48
|
- **Domain logic lives in framework-free modules** — calculations, validation, state machines, mapping, formatting. These modules import nothing from the UI framework.
|
|
66
|
-
- **Components consume logic, they don't contain it.** A
|
|
49
|
+
- **Components consume logic, they don't contain it.** A derived value that grows past trivial derivation becomes a pure function the component calls.
|
|
67
50
|
- **Backend logic never touches the frontend framework.**
|
|
68
51
|
|
|
69
|
-
**Litmus test:** if swapping the framework would force you to rewrite a source file that has no UI in it,
|
|
52
|
+
**Litmus test:** if swapping the framework would force you to rewrite a source file that has no UI in it, extract the logic.
|
|
70
53
|
|
|
71
54
|
## Code Reuse
|
|
72
55
|
|
|
73
|
-
**
|
|
74
|
-
|
|
75
|
-
1. **Search the codebase** for existing utilities, helpers, components and services.
|
|
56
|
+
1. **Search the codebase** for existing utilities, helpers, components and services before implementing.
|
|
76
57
|
2. **Reuse > reinvent**: if something does 80% of what you need, use it and extend it.
|
|
77
|
-
3. **One copy of each piece of logic
|
|
78
|
-
4. **Shared placement**: generic helpers (formatters, validators) belong in the nearest shared module, not in a feature file. If more than one module could use it, it is shared.
|
|
58
|
+
3. **One copy of each piece of logic**; a helper more than one module could use belongs in the nearest shared module.
|
|
79
59
|
|
|
80
|
-
For UI: check the shared component library
|
|
60
|
+
For UI: check the shared component library first. Build a custom one only when the shared one is fundamentally incompatible, and write down why.
|
|
81
61
|
|
|
82
62
|
## Design Review (Clean Code)
|
|
83
63
|
|
|
84
64
|
After implementing, review every changed file through these lenses:
|
|
85
65
|
|
|
86
|
-
1. **Single Responsibility**:
|
|
66
|
+
1. **Single Responsibility**: if a description needs "and", split it.
|
|
87
67
|
2. **DRY**: more than 3 identical lines in two places → shared helper.
|
|
88
68
|
3. **Dead code**: remove unused exports, unreachable branches, commented-out code.
|
|
89
|
-
4. **Naming**:
|
|
90
|
-
5. **Small functions**:
|
|
91
|
-
6. **Dependency direction**: pure utilities never import side-effectful modules.
|
|
69
|
+
4. **Naming**: if you need a comment to explain what a variable holds, rename it.
|
|
70
|
+
5. **Small functions**: over ~20 lines is a smell; extract named sub-steps. Where the linter measures cognitive complexity, every `&&`, `||`, `??`, ternary and optional chain counts; a named helper gets its own budget, a variable does not.
|
|
71
|
+
6. **Dependency direction**: pure utilities never import side-effectful modules.
|
|
92
72
|
7. **No flag arguments**: a boolean that switches behaviour → two functions.
|
|
93
73
|
8. **Error surface**: handle only errors you can meaningfully recover from; let the rest propagate.
|
|
94
74
|
|
|
@@ -96,26 +76,20 @@ Apply these checks to **your own output** before presenting it, and fix violatio
|
|
|
96
76
|
|
|
97
77
|
## Generators
|
|
98
78
|
|
|
99
|
-
Before editing a file that might be generated
|
|
100
|
-
|
|
101
|
-
1. **Check the project's scripts** (`package.json`, `composer.json`, `Makefile`) for `gen`, `generate`, `codegen`.
|
|
102
|
-
2. **Run the generator** instead of editing by hand; a generated file is overwritten on the next run.
|
|
79
|
+
Before editing a file that might be generated, check the project's scripts (`package.json`, `composer.json`, `Makefile`) for `gen`, `generate`, `codegen`. **Run the generator** instead of editing by hand; the next run overwrites a hand edit.
|
|
103
80
|
|
|
104
81
|
## Validation & Types
|
|
105
82
|
|
|
106
83
|
- The project's schema library for schemas, with types derived from the schema.
|
|
107
|
-
- Validation lives once, at the boundary,
|
|
108
|
-
- Strict typing; an unknown value is parsed at the boundary, never typed as "anything".
|
|
84
|
+
- Validation lives once, at the boundary; an unknown value is parsed there, never typed as "anything".
|
|
109
85
|
- Read the installed version of the schema library before writing against it.
|
|
110
86
|
|
|
111
87
|
### A fixed vocabulary is a typed constant
|
|
112
88
|
|
|
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
|
-
|
|
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.
|
|
89
|
+
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. It earns its place at the **seams where a `string` arrives** (a query parameter, a form option id, a database row), where a literal comparison is unchecked:
|
|
116
90
|
|
|
117
91
|
```ts
|
|
118
|
-
// ✅
|
|
92
|
+
// ✅ the constant is the vocabulary; a guard narrows the string, an unknown value is refused
|
|
119
93
|
export const FEE_MODE = { tiers: 'tiers', perKm: 'per_km' } as const
|
|
120
94
|
export type FeeMode = (typeof FEE_MODE)[keyof typeof FEE_MODE]
|
|
121
95
|
export const isFeeMode = (value: string): value is FeeMode =>
|
|
@@ -127,31 +101,29 @@ if (isFeeMode(id)) handleChange(switchMode(form, id))
|
|
|
127
101
|
mode: id === 'per_km' ? 'per_km' : 'tiers'
|
|
128
102
|
```
|
|
129
103
|
|
|
130
|
-
Derive the schema from the constant
|
|
104
|
+
Derive the schema from the constant, never the other way round. Test fixtures may spell the literal.
|
|
131
105
|
|
|
132
106
|
## Database Writes
|
|
133
107
|
|
|
134
108
|
### Never `upsert` on a generated natural key
|
|
135
109
|
|
|
136
|
-
`upsert(row, { onConflict: '<natural key>' })`
|
|
137
|
-
|
|
138
|
-
The lesson behind this rule: order numbers drawn from a truncated clock collided for orders created within the same second, and an upsert on that number merged two customers' orders into one row — one name, both carts, the other order gone. The unique index was in place the whole time; the upsert asked it not to complain.
|
|
110
|
+
`upsert(row, { onConflict: '<natural key>' })` writes into whichever row already holds that key; when the key was generated, that row belongs to someone else. Order numbers drawn from a truncated clock collide within one second, and an upsert on them merges two customers' orders into one row.
|
|
139
111
|
|
|
140
112
|
- **Create and update are separate statements.** `insert` for new rows, `update … where key = ?` for known ones.
|
|
141
113
|
- **Scope an update by tenant as well as by key**, so a key replayed from a URL cannot reach another tenant's row.
|
|
142
|
-
- **An update that matches nothing is an error, not an insert.**
|
|
114
|
+
- **An update that matches nothing is an error, not an insert.**
|
|
143
115
|
- An upsert stays legitimate where the conflict target is a key the **caller owns** and the merge is the intent (a client-generated id, a per-user settings row).
|
|
144
116
|
|
|
145
117
|
### A pre-flight existence check is not uniqueness
|
|
146
118
|
|
|
147
|
-
`SELECT
|
|
119
|
+
A `SELECT` before an insert leaves a gap concurrent writers sit in. **The unique index is the guarantee: insert against it and retry on the unique-violation error.** When retrying, **name the column**, or a duplicate in another unique column burns fresh values until the attempts run out.
|
|
148
120
|
|
|
149
121
|
### Generated identifiers carry entropy, never a clock
|
|
150
122
|
|
|
151
|
-
|
|
123
|
+
Draw from the platform's CSPRNG (`crypto.getRandomValues`, `random_bytes`), keep the drawing in a **pure function** that takes the random values as an argument, and unit-test it.
|
|
152
124
|
|
|
153
125
|
## External Measurements
|
|
154
126
|
|
|
155
127
|
### Normalised to whole units at the adapter, like money to cents
|
|
156
128
|
|
|
157
|
-
|
|
129
|
+
The adapter that receives a fractional measurement (metres, seconds, currency) rounds it **once**, to the unit the domain counts in, and nothing past the adapter rounds again: two callers rounding for themselves round differently, and a billing boundary sits between them. The domain core may keep a defensive rounding as a guarantee against callers that are not the adapter; the adapter's contract says where the rounding is decided.
|
|
@@ -1,62 +1,39 @@
|
|
|
1
1
|
# Pull Requests
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Applies whenever you run `gh pr create` or edit a PR body.
|
|
4
4
|
|
|
5
5
|
## Preconditions
|
|
6
6
|
|
|
7
7
|
- **Open a PR only when the user asks.** Same rule as commit and push.
|
|
8
|
-
- **Base = `{{kit.baseBranch}}`** unless the user names another.
|
|
9
|
-
- **
|
|
10
|
-
- **Push before creating, with the user's go-ahead for the push.** The head branch must be on the remote so `gh pr create` has a target.
|
|
8
|
+
- **Base = `{{kit.baseBranch}}`** unless the user names another; on `{{kit.baseBranch}}` itself, branch first.
|
|
9
|
+
- **Push before creating, with the user's go-ahead for the push.**
|
|
11
10
|
- **Drafts** (`merge.openAsDraft`: **{{kit.merge.openAsDraft}}**). When `on`, open every PR with `gh pr create --draft`; run `gh pr ready <n>` before handing it to the user to merge, or before waiting on its checks for your own `gh pr merge`.
|
|
12
|
-
- **Check for an existing PR first** (`gh pr view <branch>`). If a prior PR for the same branch is already **merged**, the new one is a continuation
|
|
11
|
+
- **Check for an existing PR first** (`gh pr view <branch>`). If a prior PR for the same branch is already **merged**, the new one is a continuation.
|
|
13
12
|
|
|
14
13
|
## Title
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
`type(scope): short imperative summary`, lower case, no trailing period.
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
type(scope): short imperative summary
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
- `type` ∈ `feat | fix | refactor | perf | docs | test | chore | build | ci`. Pick the type of the PR's **dominant** change, not the first commit.
|
|
17
|
+
- `type` ∈ `feat | fix | refactor | perf | docs | test | chore | build | ci`, by the PR's **dominant** change, not the first commit.
|
|
23
18
|
- `scope` = the primary package, app or service.
|
|
24
19
|
- **Continuation of a merged PR on the same branch:** append `(PART N)`.
|
|
25
20
|
|
|
26
21
|
## Body structure
|
|
27
22
|
|
|
28
|
-
Fixed sections, in this order
|
|
29
|
-
|
|
30
|
-
1. **Context** — one or two lines: link the plan (`{{kit.planDirectory}}/…`), the predecessor PR, or the issue. State which phase or slice this is.
|
|
31
|
-
2. **Changes** — grouped **by theme**, not commit by commit. Use `###` subheadings or a table when the PR spans several areas. A reviewer must grasp the shape without reading the diff.
|
|
32
|
-
3. **Verification** — the checks you actually ran, with their numbers, and the review gates with their verdicts. Only checks you ran.
|
|
33
|
-
4. **Deploy checklist** — required whenever the PR touches **database migrations, secrets, message or mail templates, or a new or changed service**. Order the steps **causally**: database → secrets → templates → services (consumer before publisher) → apps. Use `- [ ]` so it doubles as a runbook. Flag any migration already applied to an environment and which ones still need it.
|
|
34
|
-
5. **Out of scope / follow-ups** — what was deliberately deferred, so reviewers don't ask.
|
|
35
|
-
|
|
36
|
-
## Diagrams (Mermaid)
|
|
37
|
-
|
|
38
|
-
Add a fenced ` ```mermaid ` block **when it removes reader effort**:
|
|
39
|
-
|
|
40
|
-
- a **new service** or a multi-hop **data flow** (who reads what, who publishes to whom),
|
|
41
|
-
- a **state machine** or transition set,
|
|
42
|
-
- a **security or replication mechanism** where the failure mode is the point.
|
|
43
|
-
|
|
44
|
-
Keep each diagram to one idea; two small diagrams beat one dense one. Small or single-file PRs need none.
|
|
45
|
-
|
|
46
|
-
Write edge labels as `-->|"label"|` (`-- text -->` breaks on a `--` in it); `jh mermaid <body file>` must exit 0 before `gh pr create` or `gh pr edit`.
|
|
23
|
+
Fixed sections, in this order; omit one only when it has no content, never leave a placeholder.
|
|
47
24
|
|
|
48
|
-
|
|
25
|
+
1. **Context** — link the plan (`{{kit.planDirectory}}/…`), the predecessor PR, or the issue; name the phase or slice.
|
|
26
|
+
2. **Changes** — grouped **by theme**, not commit by commit, so a reviewer grasps the shape without the diff.
|
|
27
|
+
3. **Verification** — the checks with their numbers, and the review gates with their verdicts. Only checks you ran.
|
|
28
|
+
4. **Deploy checklist** — whenever the PR touches **database migrations, secrets, message or mail templates, or a new or changed service**. Order the steps **causally**: database → secrets → templates → services (consumer before publisher) → apps. Use `- [ ]`, and flag any migration already applied to an environment and which ones still need it.
|
|
29
|
+
5. **Out of scope / follow-ups** — what was deliberately deferred.
|
|
49
30
|
|
|
50
|
-
|
|
31
|
+
## Diagrams and review order
|
|
51
32
|
|
|
52
|
-
|
|
33
|
+
Add a ` ```mermaid ` block **when it removes reader effort**: a new service or a multi-hop data flow, a state machine, a security or replication mechanism whose failure mode is the point. One idea per diagram. Write edge labels as `-->|"label"|`; `jh mermaid <body file>` must exit 0 before `gh pr create` or `gh pr edit`.
|
|
53
34
|
|
|
54
|
-
PR
|
|
35
|
+
A PR across many files adds a **suggested review order**: pure/domain logic first, adapters next, UI and wiring last.
|
|
55
36
|
|
|
56
|
-
##
|
|
37
|
+
## Language and content
|
|
57
38
|
|
|
58
|
-
|
|
59
|
-
- A deploy section out of causal order (a deploy before its migration).
|
|
60
|
-
- Claiming tests or reviews that did not run.
|
|
61
|
-
- Secrets, tokens or full connection strings anywhere in the body.
|
|
62
|
-
- A diagram that restates the title without adding structure.
|
|
39
|
+
PR **title and body are English**, whatever language the conversation runs in. Never: Secrets, tokens or full connection strings anywhere in the body.
|
|
@@ -12,9 +12,7 @@ All new or changed code **must** have tests.
|
|
|
12
12
|
| File naming | the name and place the runner finds; follow where the project's existing tests live |
|
|
13
13
|
| E2E runner | Playwright, or the project's own |
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
A runner started from the wrong directory can fail before it runs a test: a watcher exhausting file descriptors, a config file or compiler setting resolved from the working directory and not found. **Read the failure before calling it a broken test** — an environment error reads like a missing dependency in code nobody touched, and re-running the same wrong invocation with the changes stashed proves nothing. Run from the package or module directory the config lives in, and leave the user's dev servers running. Where the project has its own testing rule, it names the failures its runner shows.
|
|
15
|
+
A runner started from the wrong directory can fail before it runs a test (file descriptors exhausted, a config not found). **Read the failure before calling it a broken test**: an environment error reads like a missing dependency in code nobody touched, and re-running the same wrong invocation with the changes stashed proves nothing. Run from the package or module directory the config lives in, and leave the user's dev servers running. The project's own testing rule names the failures its runner shows.
|
|
18
16
|
|
|
19
17
|
## What to Test (decision tree)
|
|
20
18
|
|
|
@@ -45,23 +43,19 @@ Is it a new user-facing feature?
|
|
|
45
43
|
|
|
46
44
|
### Test-first: money, identifiers, state
|
|
47
45
|
|
|
48
|
-
The
|
|
46
|
+
The **order** buys the seam: a test that must pass before the code exists forces the random values or the clock to arrive as an argument, so the logic is a pure function a unit test reaches. See `code-style.md` → Generated identifiers carry entropy, never a clock.
|
|
49
47
|
|
|
50
48
|
## Test Priority Order
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
1. **Unit tests** — pure functions, validators, mappers
|
|
55
|
-
2. **Integration tests** — service interactions, database operations
|
|
56
|
-
3. **E2E tests** — user flows via the E2E runner (only when requested)
|
|
50
|
+
Unit tests (pure functions, validators, mappers), then integration tests (service interactions, database operations), then E2E tests (only when requested); stop when coverage expectations are met.
|
|
57
51
|
|
|
58
52
|
## Mock only the lowest-level dependency
|
|
59
53
|
|
|
60
|
-
**Mock only the lowest-level dependency** (adapters, repositories)
|
|
54
|
+
**Mock only the lowest-level dependency** (adapters, repositories): the use-case logic still runs. Where the runner's mock replaces a whole module for the entire file, mocking an intermediate module (a use-case in a handler test) breaks every other test there.
|
|
61
55
|
|
|
62
56
|
## Before every commit
|
|
63
57
|
|
|
64
|
-
`/pre-commit` runs `bunx jackherd check
|
|
58
|
+
`/pre-commit` runs `bunx jackherd check` (types, lint, tests, staged build artifacts, the project's own checks). Beyond that:
|
|
65
59
|
|
|
66
60
|
1. Version bumped if the change affects a published package (minor = feature, patch = fix)
|
|
67
|
-
2. `code-style.md` holds in every changed file: its code order
|
|
61
|
+
2. `code-style.md` holds in every changed file: its code order and its design review, dead code included
|
|
@@ -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
|
|