jackherd 1.16.0 → 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.
- package/CHANGELOG.md +22 -0
- package/INSTALL.md +2 -1
- package/README.md +22 -21
- 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 +1 -1
- package/kit/rules/rules/kit/code-style.md +6 -6
- 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 +1 -1
- package/kit/workflow/skills/orchestrate/SKILL.md +120 -299
- package/kit/workflow/skills/orchestrate/references/appointments.md +21 -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 +9 -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 +3 -2
- 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 +77 -26
- package/src/check/skill-budgets.ts +58 -10
- 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
|
|
|
@@ -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
|
|
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`, `//
|
|
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
|
-
|
|
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
|
-
// ✅
|
|
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
|
|
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
|
|
|
@@ -6,7 +6,7 @@ argument-hint: 'What to follow, e.g. a ticket key, an issue or a query'
|
|
|
6
6
|
|
|
7
7
|
If `.claude/project/follow.md` exists, read it first. It extends this skill for this project and wins where the two disagree.
|
|
8
8
|
|
|
9
|
-
You add one entry to 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.
|
|
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
|
|
|
@@ -35,8 +35,7 @@ Anything missing goes to the user as the command to run themselves (`! <command>
|
|
|
35
35
|
|
|
36
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.
|
|
37
37
|
|
|
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"]`).
|
|
39
|
-
|
|
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"]`).
|
|
40
39
|
Write the entry as one JSON object to a draft file in the scratchpad and run, from the main checkout:
|
|
41
40
|
|
|
42
41
|
```bash
|
|
@@ -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
|
|
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
|