jackherd 1.17.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.
@@ -0,0 +1,23 @@
1
+ # § 4: an approval request, the question to the user
2
+
3
+ Read on `approval <lane> <id> <tool> · <call>`, before you ask (`/orchestrate` § 4, **Approvals**).
4
+
5
+ In a lane, a call that would open a permission dialog (an ask rule) is held by the approval gate hook (a global
6
+ install has none, and the dialog stays in the lane: § 5). The hook waits up to `guard.approvals.waitMinutes`
7
+ (default 8) for the user's answer. A subagent of your own session is held the same way, as
8
+ `subagent-<type>-<id>`. A stage tool (`guard.approvals.standing.tools`): read
9
+ [`stage-approval.md`](stage-approval.md) first.
10
+
11
+ Ask at once, one question per request, through `AskUserQuestion`:
12
+
13
+ - First read the request, `.jackherd/approvals/<id>.json` in the main checkout, and show its `tool`,
14
+ `cwd` and whole `input` in the chat above the dialog, in a code block whose fence is longer than any
15
+ run of backticks in it. The reported line is shortened; the user approves the call.
16
+ - The question names the lane and the call, and contains the mark `` `approval <id> <hash>` `` exactly
17
+ as the duty line prints it. If the `hash` of the file you showed differs, the request changed: do not
18
+ ask, tell the user.
19
+ - The options are exactly `{{kit.approval.allow}}` and `{{kit.approval.deny}}`, no multi-select.
20
+
21
+ An approval holds once, for exactly that call, for 30 minutes. When the wait ran out first, the lane was told
22
+ "pending"; tell it once the user allowed the call. Leave the client's own dialog in the lane's pane alone; the
23
+ hook's answer closes it.
@@ -5,7 +5,8 @@ for the plan commit, and again when its documentation PR has merged (§ 6).
5
5
 
6
6
  ## The question
7
7
 
8
- After an adoption, the diff round of [`adopt-plan.md`](adopt-plan.md) is this question; ask nothing more. The
8
+ Ask through `AskUserQuestion` whether to commit the missing files on a documentation branch and open a
9
+ documentation PR the lanes start from. After an adoption, the diff round of [`adopt-plan.md`](adopt-plan.md) is this question; ask nothing more. The
9
10
  answer is the approval of that commit, its push and the PR (§ 5). `<topic>` is the plan's file name without date
10
11
  and extension, or the board's directory name when several files are missing. Answered no: no lane of these plans
11
12
  starts; say why, and do not ask again in this run unless the user raises it.
@@ -16,16 +16,13 @@ You create a structured concept/plan document in `{{kit.planDirectory}}/`.
16
16
 
17
17
  ### 0. Are the decisions actually made?
18
18
 
19
- A plan records decisions; it is not where they get taken. If the `## {{kit.heading.decisions}}` table would end up holding your guesses rather than the user's answers, **stop and recommend `/grill`** — name the open questions you would otherwise have to invent answers for, and let the user decide whether to run it. Entering clarification mode is the user's call, so you recommend it and leave the invocation to them. It works the questions in rounds and hands back rows in this table's format.
20
-
21
- Skip this when the request already carries its decisions (a plan for a bug fix with one obvious approach, a change the user has already specified).
19
+ A plan records decisions; it is not where they get taken. If the `## {{kit.heading.decisions}}` table would hold your guesses rather than the user's answers, **stop and recommend `/grill`**: name the open questions and leave the invocation to the user. It hands back rows in this table's format. Skip this when the request already carries its decisions.
22
20
 
23
21
  ### 1. Pre-Scan (before a single word of the plan is written)
24
22
 
25
23
  - [ ] Read the relevant files in their **current** state, from disk
26
24
  - [ ] Search the project's shared components and utilities for what covers 80%+ of the need. 80%+ match → the plan specifies reuse or an override; 60–80% → the plan extends the shared piece; below 60% → new code, with the rationale under the rejected alternatives
27
- - [ ] Read related plans in `{{kit.planDirectory}}/` — check dependencies and open TODOs
28
- - [ ] Read the architecture documentation the change touches — understand what is already documented before you plan
25
+ - [ ] Read related plans in `{{kit.planDirectory}}/` (dependencies, open TODOs) and the architecture documentation the change touches
29
26
 
30
27
  ### 2. Create the Document
31
28
 
@@ -37,53 +34,43 @@ The closing documentation section reads `## N — …` with rows `N.1` to `N.5`:
37
34
 
38
35
  A checkbox moved on purpose to a later phase stays where it is, unticked, and ends in the target: `- [ ] Update the documentation → P9`. The herd counts it neither as open nor as done in its own phase and lists it as open in phase 9. The arrow stands only at the end of the line. Once a phase's tasks are ticked on the base branch, every box still open in it, gate boxes included, is ticked or moved on this way; `jh check` checks both.
39
36
 
40
- A task starts with its short name: `- [ ] **Name**: …` (a review-gate item keeps its own form). The name has 40 characters at most and names the subject and the result, what is different afterwards (`Heartbeat shows progress without ticks`, `` `ask-hint` finds questions anywhere ``); the how, the conditions, the origin and the evidence follow the colon. A phase's title is its name, its context goes into its origin line, a quote under the heading (`> Origin: …`, in the plan's language). A task the user adds later: `- [ ] **Name**: … (User <DD.MM.> <HH:MM>)`.
37
+ A task starts with its short name: `- [ ] **Name**: …` (a review-gate item keeps its own form). The name has 40 characters at most and names the subject and the result (`Heartbeat shows progress without ticks`); the how, the conditions, the origin and the evidence follow the colon. A phase's title is its name; its context goes into its origin line, a quote under the heading (`> Origin: …`, in the plan's language). A task the user adds later: `- [ ] **Name**: … (User <DD.MM.> <HH:MM>)`.
41
38
 
42
- An Artifact that belongs to the plan (a mockup, a draft for review, a findings page) stands as a Markdown link with its title in the phase it concerns, in the same move as its first publish: `**Mockup:** [Title](url)` or in the origin line. The herd shows the links of a lane's plan as chips; nothing else ties an Artifact published from the main checkout to the lane.
39
+ An Artifact that belongs to the plan (a mockup, a draft for review, a findings page) stands as a Markdown link with its title in the phase it concerns, in the same move as its first publish: `**Mockup:** [Title](url)` or in the origin line. The herd shows them as chips; nothing else ties an Artifact published from the main checkout to the lane.
43
40
 
44
41
  What the template's tables mean:
45
42
 
46
- - **Model recommendations per phase** — one row per phase, judged by **error cost × complexity**. The project's default model for implementation (`{{kit.model.implement}}`) is the default of every row. A phase moves up to the planning model (`{{kit.model.plan}}`) when its rationale names a concrete error cost: a breakable invariant, possible data loss, security or tenant isolation, a migration without a way back, distributed state across systems. "Complex" or "many files" alone keeps the default. Review gates with `/security-audit` always get the strongest model.
47
- - **Dependencies and parallelization** (from three phases) — `/implement` reads it and starts every phase whose dependencies are done and that shares no written path with another, at the same time.
48
- - **{{kit.heading.dependsOn}}:** phases of this plan (numbers) or external gates (`User: mockups`). A phase is ready when everything here is done.
49
- - **{{kit.heading.writes}}:** the directories or files the phase creates or changes, as narrow as possible. Shared files many phases touch (barrels, translation files, `package.json`, generated types, architecture docs) are listed explicitly, so the overlap is visible. Write each path in full from the repository root: paths are compared as written, so `src/watch/` and `packages/app/src/watch/` do not overlap.
43
+ - **Model recommendations per phase** — one row per phase, judged by **error cost × complexity**. `{{kit.model.implement}}` is the default of every row. A phase moves up to `{{kit.model.plan}}` when its rationale names a concrete error cost: a breakable invariant, possible data loss, security or tenant isolation, a migration without a way back, distributed state across systems. "Complex" or "many files" alone keeps the default. Review gates with `/security-audit` always get the strongest model.
44
+ - **Dependencies and parallelization** (from three phases) — `/implement` starts every phase whose dependencies are done and that shares no written path with another, at the same time.
45
+ - **{{kit.heading.dependsOn}}:** phases of this plan (numbers) or external gates (`User: mockups`).
46
+ - **{{kit.heading.writes}}:** the directories or files the phase creates or changes, as narrow as possible, shared files (barrels, translation files, `package.json`, generated types, architecture docs) listed explicitly. Write each path in full from the repository root: paths are compared as written.
50
47
  - **{{kit.heading.serialMarker}}:** `Migration`, `<collection> schema bump`, `Event <type>`, a generator run. Two phases with a migration never run at the same time; neither do two that bump the same schema.
51
- - **Cutting phases:** many small phases that ship in one release are bundled into a few phases now, not appended one by one later: every phase costs a hand-over. Shared text files (skills, README, changelog, architecture docs) go into a small phase of their own or into the closing tasks of a phase, so a phase's code never waits behind another phase only for them. You cut them here; `jh check --parallel` only orders the phases it is given.
48
+ - **Cutting phases:** many small phases that ship in one release are bundled into a few phases now, not appended one by one later: every phase costs a hand-over. Shared text files (skills, README, changelog, architecture docs) go into a small phase of their own or into the closing tasks of a phase, so no phase's code waits behind them.
52
49
 
53
50
  ### 3. Architecture Review
54
51
 
55
- Apply `/uncle-bob` to the created plan. Additionally check:
56
-
57
- - **Interfaces:** Are the boundaries between modules/services clear? Who consumes what?
58
- - **Blast radius:** Which existing consumers are affected? Are tests needed in all consuming packages?
52
+ Apply `/uncle-bob` to the created plan, and check the **interfaces** (who consumes what) and the **blast radius** (which consumers are affected, which consuming packages need tests).
59
53
 
60
54
  ### 4. Anchor the Review Gate in the Plan
61
55
 
62
- Every phase gets the review gate block from the template at its end. Add the reviewers the project supplement names for its areas (a UI review for UI phases, for instance). A phase with tasks that genuinely needs no gate (documentation only, a draft) says so and why in a line of its own, in the plan's language: `**No review gate:** <reason>` (`en`), `**Kein Review-Gate:** <Grund>` (`de`). `jh check` refuses a phase the change adds with tasks and neither line.
56
+ Every phase gets the review gate block from the template at its end, plus the reviewers the project supplement names for its areas. A phase with tasks that genuinely needs no gate (documentation only, a draft) says so in a line of its own, in the plan's language: `**No review gate:** <reason>` (`en`), `**Kein Review-Gate:** <Grund>` (`de`). `jh check` refuses a phase the change adds with tasks and neither line.
63
57
 
64
58
  ### 5. Quality Checks
65
59
 
66
- - **Focus:** Does the plan solve a concrete problem? No "would be nice to have".
67
- - **Scope:** Is the scope clearly bounded? No feature-creep risk?
68
- - **Feasibility:** Can the phases be split into individual PRs/commits?
69
- - **References:** Are related plans in `{{kit.planDirectory}}/` linked?
70
- - **Current state is fact-based:** Read code, count files, verify architecture; every claim has a source.
71
- - **Shared code checked:** For every planned component it is documented why it is built new instead of reused or extended.
72
- - **Tests defined:** The tests section is filled in or carries an explicit rationale why none are needed (decision tree in `.claude/rules/kit/testing.md`).
60
+ - **Focus and scope:** a concrete problem, clearly bounded, phases that split into individual PRs, related plans in `{{kit.planDirectory}}/` linked.
61
+ - **Current state is fact-based:** every claim has a source (code read, files counted).
62
+ - **Shared code checked:** for every planned component it is documented why it is built new instead of reused or extended.
63
+ - **Tests defined:** the tests section is filled in or says why none are needed (decision tree in `.claude/rules/kit/testing.md`).
73
64
  - **Lint before prose:** a rule the plan writes or changes in an agent text that the project's linter can check becomes a lint task (a case where it fires, one where it stays silent); the prose shrinks to the sentence naming it with `lint:<rule id>`.
74
- - **Model recommendations defined:** `## {{kit.heading.implementation}}` starts with the model table, one row per phase, justified by error cost × complexity.
75
- - **Parallelism defined (3+ phases):** the dependencies table follows it, a row for every phase, one added later too. Every migration, schema bump and new event of a phase is a serial marker, every shared file stands under {{kit.heading.writes}}; otherwise `/implement` starts two phases that collide.
76
- - **arc42 check (after the plan exists):** apply the filter in `.claude/rules/kit/arc42.md` to the finished plan and **ask the user concretely** which sections should be extended. On "yes", add a concrete task to `## {{kit.heading.documentation}}`.
77
- - **Architecture check (after the plan exists):** apply the filter in `.claude/rules/kit/architecture-files.md` — new ownership, a new invariant or an accepted risk each trigger a concrete question about the unit's `ARCHITECTURE.md`.
78
- - **arc42 closing phase (3+ phases or a new service/package):** end the plan with the architecture documentation phase from the template. Add arc42 sections beyond it only when they deliver real value.
79
- - **Documentation planned:** `## {{kit.heading.documentation}}` names concretely what will be created or updated after completion.
65
+ - **Model recommendations and parallelism:** `## {{kit.heading.implementation}}` starts with the model table; from three phases the dependencies table follows, a row for every phase, every migration, schema bump and new event a serial marker, every shared file under {{kit.heading.writes}}.
66
+ - **arc42 and architecture (after the plan exists):** apply the filters in `.claude/rules/kit/arc42.md` and `.claude/rules/kit/architecture-files.md` to the finished plan and **ask the user concretely**; on "yes", add a concrete task to `## {{kit.heading.documentation}}`. From three phases or a new service/package, the plan ends with the template's architecture documentation phase.
67
+ - **Documentation planned:** `## {{kit.heading.documentation}}` names concretely what will be created or updated.
80
68
 
81
69
  ### 6. Propose the board rows
82
70
 
83
71
  A **board** gathers the lanes one coordinator runs with `/orchestrate`: `{{kit.board.file}}` in a directory
84
- matching `{{kit.planDirectory}}/{{kit.board.pattern}}` (this project calls it a "{{kit.board.term}}"). The plan
85
- already holds what a lane row needs, so end by proposing the rows; `/orchestrate` then starts the lanes from them.
86
- Skip this step for a plan of one phase that nobody runs as a lane, and say so.
72
+ matching `{{kit.planDirectory}}/{{kit.board.pattern}}` (this project calls it a "{{kit.board.term}}"). End by
73
+ proposing its rows; skip this for a plan of one phase that nobody runs as a lane, and say so.
87
74
 
88
75
  Derive the rows from the dependencies table and the model table, nothing else:
89
76
 
@@ -93,33 +80,26 @@ Derive the rows from the dependencies table and the model table, nothing else:
93
80
  Otherwise it is a `↳` row under a fitting lane, or a subagent there (`/implement` Step 0b).
94
81
  - **Continuation:** a phase that shares a written path or a serial marker with an earlier phase, or depends on it,
95
82
  is a `↳` row under that phase's lane, in the same worktree.
96
- - **Starts after:** from the phase's {{kit.heading.dependsOn}} cell, as the board reads it (`P2 merged`, an external
97
- gate such as `User: mockups`); a phase without one starts at once.
98
- - **Model:** the full model ID from the model table (`{{kit.model.implement}}`, `{{kit.model.plan}}`), never an alias.
99
- - **Plan:** a Markdown link to the plan, its path relative to the board's directory (`../<file>.md` for a plan
100
- directly under `{{kit.planDirectory}}`).
101
- Plans stay where they are; the row only links them.
83
+ - **Starts after:** from the phase's {{kit.heading.dependsOn}} cell (`P2 merged`, `User: mockups`); without one, at once.
84
+ - **Model:** the full model ID from the model table, never an alias.
85
+ - **Plan:** a Markdown link relative to the board's directory (`../<file>.md`). Plans stay where they are; the row only links them.
102
86
  - **Lane name:** short kebab case, unique on the board; the worktree directory and the herdr agent carry the same name
103
87
  (`/orchestrate` § 2), else the herd lists the lane as a worktree without an agent.
104
- - **Port:** `–`; the coordinator sets it when the lane starts, unless the project supplement assigns ports here.
88
+ - **Port:** `–`; the coordinator sets it, unless the project supplement assigns ports here.
105
89
 
106
90
  Show the rows as the table they become: `## {{kit.heading.lanes}}` with the columns lane, port, plan, phases, starts
107
91
  after, model, in this order and headed in {{kit.planLanguage}} (the herd reads them by position). Then ask
108
92
  where they go:
109
93
 
110
- - **The newest board**, by name: the newest `{{kit.planDirectory}}/{{kit.board.pattern}}/{{kit.board.file}}`. The
111
- rows are appended to its `## {{kit.heading.lanes}}` table.
112
- - **A new board.** Ask for its name and propose one: the year and month or the topic, in the pattern's shape. Check
113
- that the directory name matches `{{kit.board.pattern}}` before writing; if it does not, say why and ask again.
114
- Create `{{kit.planDirectory}}/<name>/{{kit.board.file}}` with a `#` title and the `## {{kit.heading.lanes}}`
115
- table holding the proposed rows.
94
+ - **The newest board**, by name: the newest `{{kit.planDirectory}}/{{kit.board.pattern}}/{{kit.board.file}}`, appended to its `## {{kit.heading.lanes}}` table.
95
+ - **A new board:** ask for its name, proposing one (the year and month or the topic). Check that the directory name matches `{{kit.board.pattern}}` before writing; if it does not, say why and ask again. Create `{{kit.planDirectory}}/<name>/{{kit.board.file}}` with a `#` title and the table.
116
96
  - **Not now:** the rows stay in your answer, for `/orchestrate` to write when it starts the lanes.
117
97
 
118
98
  Nothing is written without that answer. The board is a file like the plan: it goes into the same commit, under the
119
99
  same approval.
120
100
 
121
101
  After writing the rows, name the next step in one line: `/orchestrate <board path>`, from a Claude Code session
122
- inside herdr. A plan or board that is not on `origin/{{kit.baseBranch}}` yet needs no merge first: `/orchestrate`
102
+ inside herdr. A plan or board not yet on `origin/{{kit.baseBranch}}` needs no merge first: `/orchestrate`
123
103
  asks to commit it on a documentation branch and starts the lanes from there.
124
104
 
125
105
  Ask every question with a fixed set of answers through the `AskUserQuestion` tool, in {{kit.responseLanguage}}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jackherd",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "keywords": [
5
5
  "claude-code",
6
6
  "coding-agents",
@@ -21,8 +21,11 @@ export const KIT_SKILL_TARGETS: Readonly<Record<string, number>> = {
21
21
  // dry-run by name after the entry is written, since a draft runs without the keychain (+103).
22
22
  '.claude/skills/follow/SKILL.md': 1498,
23
23
  '.claude/skills/grill/SKILL.md': 685,
24
- '.claude/skills/implement/SKILL.md': 2190,
24
+ // Prose cut, round 2: cut to its mandatory rules, measured size plus 5 %.
25
+ '.claude/skills/implement/SKILL.md': 1882,
25
26
  '.claude/skills/orchestrate/SKILL.md': 4500,
27
+ // Prose cut, round 2: § 4 Approvals moved here from the core.
28
+ '.claude/skills/orchestrate/references/approvals.md': 266,
26
29
  // skill-review: the copy is taken after a mirror, and rejecting together with a remark is a remark (+36).
27
30
  // Cross-check: an adopted plan also gets short names and a no-review-gate line, which `jh check` asks of a new
28
31
  // plan; the proof became a word diff, since a short name goes into an existing line (+74); skill-review: the
@@ -52,7 +55,8 @@ export const KIT_SKILL_TARGETS: Readonly<Record<string, number>> = {
52
55
  '.claude/skills/orchestrate/references/lane-setup.md': 380,
53
56
  // Open up: an adopted plan is committed like a missing one, and a tracked copy is restored after the merge.
54
57
  // Core cut: the question of § 2 step 0 (the adoption's diff round, the approval it is, `<topic>`, a no) (+60).
55
- '.claude/skills/orchestrate/references/plan-commit.md': 412,
58
+ // Prose cut, round 2: the question itself moved here from § 2 step 0; measured size plus 5 %.
59
+ '.claude/skills/orchestrate/references/plan-commit.md': 475,
56
60
  // Core cut: § 5 self-merge steps 1 to 6, moved out of the core.
57
61
  '.claude/skills/orchestrate/references/self-merge.md': 379,
58
62
  '.claude/skills/orchestrate/references/sensitive-input.md': 269,
@@ -63,10 +67,12 @@ export const KIT_SKILL_TARGETS: Readonly<Record<string, number>> = {
63
67
  // Core cut: § 4 waiting rows beyond the basic one, moved out of the core.
64
68
  '.claude/skills/orchestrate/references/waiting.md': 216,
65
69
  '.claude/skills/orchestrate/references/watch-without-monitor.md': 154,
66
- '.claude/skills/plan/SKILL.md': 1920,
70
+ // Prose cut, round 2: cut to its mandatory rules, measured size plus 5 %.
71
+ '.claude/skills/plan/SKILL.md': 1717,
67
72
  '.claude/skills/plan/references/template-English.md': 536,
68
73
  '.claude/skills/plan/references/template-German.md': 501,
69
- '.claude/skills/pre-commit/SKILL.md': 1003,
74
+ // Prose cut, round 2: cut to its mandatory rules, measured size plus 5 %.
75
+ '.claude/skills/pre-commit/SKILL.md': 825,
70
76
  '.claude/skills/review/SKILL.md': 319,
71
77
  '.claude/skills/security-audit/SKILL.md': 519,
72
78
  // Any language: step 3 points to proposing the missing commands from the CI; the proposal itself is a reference.
@@ -82,11 +88,10 @@ export const KIT_SKILL_TARGETS: Readonly<Record<string, number>> = {
82
88
  '.claude/agents/skill-review.md': 468,
83
89
  '.claude/agents/spec-review.md': 1137,
84
90
  '.claude/agents/uncle-bob.md': 111,
85
- // Lint before prose: the vocabulary section and two one-liners cut, a sentence on `lint:` pointers added (-79).
86
- '.claude/rules/kit/code-style.md': 1585,
87
- // Drafts until the merge: a precondition opens a draft when `merge.openAsDraft` says so (+39).
88
- '.claude/rules/kit/pull-requests.md': 640,
89
- '.claude/rules/kit/testing.md': 741,
91
+ // Prose cut, round 2: the three rules cut to their mandatory rules, measured size plus 5 %.
92
+ '.claude/rules/kit/code-style.md': 1290,
93
+ '.claude/rules/kit/pull-requests.md': 459,
94
+ '.claude/rules/kit/testing.md': 506,
90
95
  }
91
96
 
92
97
  /**
@@ -97,8 +102,6 @@ export const KIT_SKILL_CEILINGS: Readonly<Record<string, number>> = {
97
102
  // Orchestrate core plan P1: the reviewer also judges the whole length of a file against its target (+12).
98
103
  // Lint before prose: lens 7 for lint-shaped prose and the sentence on the `Lint pointers` result (+33).
99
104
  '.claude/agents/skill-review.md': 513,
100
- // Lint before prose: the quality check that asks of every written rule whether the linter can check it (+44).
101
- '.claude/skills/plan/SKILL.md': 1964,
102
105
  // P33: the paw in the header chip, panes by tab and title, the lane in the context line of a passed-on question.
103
106
  // Plain words: no plan code without its words in a short question.
104
107
  // Any language: a pointer in § 0 and a section in events.md for `plan missing`.
@@ -118,13 +121,17 @@ export const KIT_SKILL_CEILINGS: Readonly<Record<string, number>> = {
118
121
  // approval and the herd's ask line moved to references. Cut to the measured size plus 5 %.
119
122
  // Orchestrate core plan P1: the target is 4500. P5 cut the core to its longest rendering; the rest of the cut is
120
123
  // a follow-up, decided by the user after the P6 runs.
121
- '.claude/skills/orchestrate/SKILL.md': 5938,
124
+ // Prose cut, round 2: cut to the phrases the kit-content test holds word for word, § 4 Approvals moved to a
125
+ // reference; measured size plus 5 %. Then checked sentence by sentence (skill-review, uncle-bob): what a
126
+ // reference, a hook message or a watcher line already carries left the core; measured size plus 5 %.
127
+ '.claude/skills/orchestrate/SKILL.md': 5163,
122
128
  // P33 and open up: grows with the skill above (+132 for the ready round and the lone merge call); measured with
123
129
  // this repository's supplement after the skill-review fixes (+153); click merge grows with the skill (+30); change
124
130
  // id grows with the skill, staging first and the worktree named after skill-review (+55). Audit P6: cut with the
125
131
  // skill, the supplement down to its deviations; measured size plus 5 %.
126
132
  // Orchestrate core plan P1: the target is 5100; P5 lowered the ceiling with the skill's to the rendered sum.
127
- '.claude/skills/orchestrate/SKILL.md + .claude/project/orchestrate.md': 6635,
133
+ // Prose cut, round 2: lowered with the skill's, this repository's sum plus 5 %; again after the sentence check.
134
+ '.claude/skills/orchestrate/SKILL.md + .claude/project/orchestrate.md': 5611,
128
135
  }
129
136
 
130
137
  /** What `jh check` measures against: a file's ceiling where it has one, else its target. */
@@ -154,9 +154,11 @@ const overrunLine = ({ entry: { label, words, budget }, overrun }: MeasuredOverr
154
154
  const withinLine = (count: number): string =>
155
155
  count === 1 ? '1 file within its word budget' : `${count} files within their word budget`
156
156
 
157
- const statusOf = (overruns: readonly MeasuredOverrun[]): CheckStatus => {
157
+ const missingLine = (path: string): string => `warning: ${path} is named in check.skillBudgets but missing`
158
+
159
+ const statusOf = (overruns: readonly MeasuredOverrun[], missing: readonly string[]): CheckStatus => {
158
160
  if (overruns.some(({ overrun }) => overrun === CHECK_STATUS.fail)) return CHECK_STATUS.fail
159
- return overruns.length > 0 ? CHECK_STATUS.warn : CHECK_STATUS.pass
161
+ return overruns.length > 0 || missing.length > 0 ? CHECK_STATUS.warn : CHECK_STATUS.pass
160
162
  }
161
163
 
162
164
  /** A file within the kit's ceiling but above the kit's target for it: a hint, never a failure. A project budget has its note instead. */
@@ -172,19 +174,25 @@ const overrideNote = ({ label, target, projectBudget }: Measurement): string[] =
172
174
  : []
173
175
 
174
176
  /**
175
- * One line per overrun; one line in total when none is over. Warns above a budget, fails above the budget plus 10 %.
176
- * Lines for a file over its target and for a project budget above the kit target follow, and change no status.
177
+ * One line per overrun and per `missing` path the project budget names; one line in total when none is over. Warns
178
+ * above a budget and on a missing path, fails above the budget plus 10 %. Lines for a file over its target and for a
179
+ * project budget above the kit target follow, and change no status.
177
180
  */
178
- export const budgetResult = (measured: readonly Measurement[], durationMs: number): CheckResult => {
181
+ export const budgetResult = (
182
+ measured: readonly Measurement[],
183
+ durationMs: number,
184
+ missing: readonly string[] = [],
185
+ ): CheckResult => {
179
186
  const overruns = measured.flatMap((entry) => {
180
187
  const overrun = overrunOfMeasurement(entry)
181
188
  return overrun === null ? [] : [{ entry, overrun }]
182
189
  })
183
- const lines = overruns.length > 0 ? overruns.map(overrunLine) : [withinLine(measured.length)]
190
+ const problems = [...overruns.map(overrunLine), ...missing.map(missingLine)]
191
+ const lines = problems.length > 0 ? problems : [withinLine(measured.length)]
184
192
  return {
185
193
  name: 'Skill budgets',
186
194
  command: `${measured.length} measured, words per budget`,
187
- status: statusOf(overruns),
195
+ status: statusOf(overruns, missing),
188
196
  detail: [...lines, ...measured.flatMap(targetHint), ...measured.flatMap(overrideNote)].join('\n'),
189
197
  durationMs,
190
198
  }
@@ -241,10 +249,28 @@ const supplementPaths = (root: string): string[] =>
241
249
  .filter((name) => name.endsWith('.md'))
242
250
  .map((name) => claudePath(SUPPLEMENTS_DIRECTORY, name))
243
251
 
252
+ /** The entries of the project budget that name one file by its exact path: no glob, no sum. */
253
+ const namedProjectFiles = (project: BudgetTable): [path: string, words: number][] =>
254
+ Object.entries(project).filter(([key]) => !isGlob(key) && !key.includes(SUM_SEPARATOR))
255
+
256
+ /** The files the project budget names that `found` does not hold yet, and the named paths that cannot be read. */
257
+ const namedFilesOf = (
258
+ root: string,
259
+ project: BudgetTable,
260
+ found: readonly FileWords[],
261
+ ): { files: FileWords[]; missing: string[] } => {
262
+ const measured = new Set(found.map(({ path }) => path))
263
+ const named = namedProjectFiles(project).filter(([path]) => !measured.has(path))
264
+ const files = named.flatMap(([path, words]) => fileWordsOf(root, path, words))
265
+ const missing = named.map(([path]) => path).filter((path) => !files.some((file) => file.path === path))
266
+ return { files, missing }
267
+ }
268
+
244
269
  /**
245
270
  * The installed kit skills with their references, kit agents and kit rules that load every session (the lock file
246
- * says which files the kit installed), the project's supplements and its own skills, measured against the budgets.
247
- * `skillPrefix` is the project's: the kit's budgets follow its skills and agents to their prefixed paths.
271
+ * says which files the kit installed), the project's supplements and its own skills, and every other file the project
272
+ * budget names by its exact path, measured against the budgets; a named file that is missing warns. `skillPrefix` is
273
+ * the project's: the kit's budgets follow its skills and agents to their prefixed paths.
248
274
  * Null when there is nothing to measure.
249
275
  */
250
276
  export const skillBudgetCheck = (root: string, project: BudgetTable = {}, skillPrefix = ''): CheckResult | null => {
@@ -252,12 +278,16 @@ export const skillBudgetCheck = (root: string, project: BudgetTable = {}, skillP
252
278
  const kitPaths = kitPathsOf(root)
253
279
  const kit = new Set(kitPaths.map((key) => claudePath(key)))
254
280
  const foreign = readForeignSkills(root)
255
- const files = [
281
+ const found = [
256
282
  ...kitPaths.flatMap((key) => kitFileWordsOf(root, key)),
257
283
  ...supplementPaths(root).flatMap((path) => fileWordsOf(root, path, SUPPLEMENT_BUDGET)),
258
284
  ...ownSkillPaths(root, kit).flatMap((path) => fileWordsOf(root, path, OWN_SKILL_BUDGET)),
259
- ].map(markForeign(foreign))
285
+ ]
286
+ const named = namedFilesOf(root, project, found)
287
+ const files = [...found, ...named.files].map(markForeign(foreign))
260
288
  const defaults = prefixedBudgets(KIT_SKILL_BUDGETS, skillPrefix)
261
289
  const targets = prefixedBudgets(KIT_SKILL_TARGETS, skillPrefix)
262
- return files.length === 0 ? null : budgetResult(measureFiles(files, project, defaults, targets), Date.now() - started)
290
+ return files.length === 0 && named.missing.length === 0
291
+ ? null
292
+ : budgetResult(measureFiles(files, project, defaults, targets), Date.now() - started, named.missing)
263
293
  }