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
|
@@ -1,36 +1,47 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, join, posix } from 'node:path'
|
|
3
3
|
import { boardPlansOf, isInsideRepository } from '../../kit/hooks/hooks/shared/board-lanes'
|
|
4
|
-
import { planCopiesIn } from '../../kit/hooks/hooks/shared/boards'
|
|
4
|
+
import { boardsIn, planCopiesIn } from '../../kit/hooks/hooks/shared/boards'
|
|
5
|
+
import { readTextOrNull } from '../../kit/hooks/hooks/shared/files'
|
|
5
6
|
import { codesOfPlans } from '../../kit/hooks/hooks/shared/plan-codes'
|
|
6
7
|
import {
|
|
7
8
|
boardRowsWithoutPhase,
|
|
8
9
|
boardRowsWithProse,
|
|
10
|
+
currentPhase,
|
|
9
11
|
hasLanesTable,
|
|
10
12
|
type LongPhaseTitle,
|
|
13
|
+
laneProgress,
|
|
11
14
|
longPhaseTitles,
|
|
12
|
-
type MergedPhaseWithOpenBoxes,
|
|
13
15
|
mergedPhasesWithOpenBoxes,
|
|
14
16
|
newPhasesWithoutReviewGate,
|
|
17
|
+
openBoxesOfPhase,
|
|
15
18
|
PHASE_CELL_LIMIT,
|
|
16
19
|
PHASE_TITLE_LIMIT,
|
|
20
|
+
type PhaseWithOpenBoxes,
|
|
17
21
|
type PhaseWithoutGate,
|
|
18
22
|
type PhaseWithoutRow,
|
|
19
23
|
PLAN_VOCABULARY,
|
|
20
24
|
type PlanLanguage,
|
|
25
|
+
type PlanPhase,
|
|
21
26
|
PROSE_REASON,
|
|
27
|
+
parsePlanPhases,
|
|
28
|
+
parseSprintLanes,
|
|
22
29
|
phaseIdsOfPlan,
|
|
23
30
|
phasesWithoutDependencyRow,
|
|
24
31
|
type RowWithoutPhase,
|
|
25
32
|
type RowWithProse,
|
|
33
|
+
resolvePlanPath,
|
|
26
34
|
SHORT_NAME_LIMIT,
|
|
27
35
|
SHORT_NAME_PROBLEM,
|
|
28
36
|
type ShortNameFinding,
|
|
37
|
+
type SprintLane,
|
|
29
38
|
shortNameFindings,
|
|
30
39
|
} from '../plan-format'
|
|
31
40
|
import { boardRowsWithBareCodes, type RowWithBareCode } from '../plan-format/bare-codes'
|
|
41
|
+
import { mergePlanPhases } from '../plans/phase-source'
|
|
32
42
|
import { changedPlansOf } from './deferral-arrows'
|
|
33
43
|
import { CHECK_STATUS, type CheckResult } from './result'
|
|
44
|
+
import { OPEN_BOXES_LEVEL, type OpenBoxesLevel } from './schema'
|
|
34
45
|
|
|
35
46
|
/** The content of a file at a ref; null where the ref lacks the file. */
|
|
36
47
|
export type ContentAt = (path: string) => string | null
|
|
@@ -72,6 +83,22 @@ type Report<T> = {
|
|
|
72
83
|
clean: string
|
|
73
84
|
}
|
|
74
85
|
|
|
86
|
+
/** The lane a checkout is for and where its boards live, as the check reads them from the checkout. */
|
|
87
|
+
export type LaneBoxesInput = {
|
|
88
|
+
root: string
|
|
89
|
+
lane: string
|
|
90
|
+
planDirectory: string
|
|
91
|
+
board: { pattern: string; file: string }
|
|
92
|
+
language: PlanLanguage
|
|
93
|
+
/** A plan as the base has it (the pull request's parent in the watcher); null where it lacks the file. */
|
|
94
|
+
baseOf: ContentAt
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type BuiltPhase = { planPath: string; phaseId: string; partial: boolean }
|
|
98
|
+
|
|
99
|
+
/** The phase a lane builds with the plan it is in and its open boxes; `open` is null for a phase without any. */
|
|
100
|
+
export type LaneBuild = { path: string; open: PhaseWithOpenBoxes | null }
|
|
101
|
+
|
|
75
102
|
const changedCount = (count: number, noun: string): string => `${count} changed ${count === 1 ? noun : `${noun}s`}`
|
|
76
103
|
|
|
77
104
|
const findingsResult = <T>(
|
|
@@ -132,7 +159,7 @@ const bareCodeReport: Report<RowWithBareCode> = {
|
|
|
132
159
|
clean: 'no changed waiting row names a plan code bare',
|
|
133
160
|
}
|
|
134
161
|
|
|
135
|
-
const mergedReport: Report<
|
|
162
|
+
const mergedReport: Report<PhaseWithOpenBoxes> = {
|
|
136
163
|
name: 'Merged phases with open boxes',
|
|
137
164
|
noun: 'plan',
|
|
138
165
|
rule: 'a merged phase ticks or moves on its open boxes',
|
|
@@ -205,7 +232,7 @@ const bareCodesOfBoard = (roots: readonly string[], file: Changed, language: Pla
|
|
|
205
232
|
codesOfPlans(planTextsOf(roots, file, language), language),
|
|
206
233
|
).map(withPath(file.path))
|
|
207
234
|
|
|
208
|
-
const mergedOf = ({ path, current, base }: Changed, language: PlanLanguage): Found<
|
|
235
|
+
const mergedOf = ({ path, current, base }: Changed, language: PlanLanguage): Found<PhaseWithOpenBoxes>[] =>
|
|
209
236
|
base ? mergedPhasesWithOpenBoxes(current, base, language).map(withPath(path)) : []
|
|
210
237
|
|
|
211
238
|
const unlistedOf = ({ path, current }: Changed, language: PlanLanguage): Found<PhaseWithoutRow>[] | null =>
|
|
@@ -227,6 +254,91 @@ const changedContents = ({ root, previousOf, baseOf, language }: PlanCheckInput,
|
|
|
227
254
|
return { path, current, previous, previousLines, base: baseOf(path), board: hasLanesTable(current, language) }
|
|
228
255
|
})
|
|
229
256
|
|
|
257
|
+
const openBoxesText = (lane: string, partialWord: string, path: string, found: PhaseWithOpenBoxes): string =>
|
|
258
|
+
`${path}:${found.line} Phase ${found.id} ${found.title} is built by lane ${lane} but has open boxes at ${found.openLines.join(', ')}; tick them, end them with → P<n> or mark the row ${partialWord}`
|
|
259
|
+
|
|
260
|
+
/** The open lane `input.lane` of the newest board that lists it, with the directory its plan links are relative to. */
|
|
261
|
+
const laneOnBoards = (input: LaneBoxesInput): { lane: SprintLane; directory: string } | null => {
|
|
262
|
+
const boards = boardsIn(input.root, input).sort((left, right) => right.directory.localeCompare(left.directory))
|
|
263
|
+
for (const { directory, text } of boards) {
|
|
264
|
+
const lane = parseSprintLanes(text, input.language).find(({ name, closed }) => name === input.lane && !closed)
|
|
265
|
+
if (lane) return { lane, directory }
|
|
266
|
+
}
|
|
267
|
+
return null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The lane has work of its own in the checkout, so a phase it ticked further than the base is marked touched, as the herd reads it. */
|
|
271
|
+
const CHECKOUT_WORK = { ahead: 1, dirty: 0 }
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The phases of the plan at `path` in the checkout; with the plan on the base too, those the checkout ticked further
|
|
275
|
+
* are marked touched, which is how the herd tells the phase a lane builds from the first open phase of its row.
|
|
276
|
+
*/
|
|
277
|
+
const phasesInCheckout = (input: LaneBoxesInput, path: string): PlanPhase[] => {
|
|
278
|
+
const text = readTextOrNull(join(input.root, path))
|
|
279
|
+
const base = input.baseOf(path)
|
|
280
|
+
const own = text === null ? [] : parsePlanPhases(text, input.language)
|
|
281
|
+
return base === null ? own : mergePlanPhases(parsePlanPhases(base, input.language), own, [], CHECKOUT_WORK)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The phase the lane's newest board row names, the plan it is in and whether the row marks it as built in parts. */
|
|
285
|
+
const phaseBuiltBy = (input: LaneBoxesInput): BuiltPhase | null => {
|
|
286
|
+
const found = laneOnBoards(input)
|
|
287
|
+
if (!found) return null
|
|
288
|
+
const pathOf = (href: string): string => resolvePlanPath(found.directory, href)
|
|
289
|
+
const phasesByHref = Object.fromEntries(
|
|
290
|
+
found.lane.steps.flatMap(({ planHref }) =>
|
|
291
|
+
planHref === null ? [] : [[planHref, phasesInCheckout(input, pathOf(planHref))] as const],
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
const progress = laneProgress(found.lane, phasesByHref)
|
|
295
|
+
const phase = currentPhase(progress)
|
|
296
|
+
const step = progress.current === null ? null : progress.steps[progress.current.stepIndex].step
|
|
297
|
+
if (!phase || !step?.planHref) return null
|
|
298
|
+
// with the base known, a phase this checkout did not tick further waits in the row; it is not what the lane builds
|
|
299
|
+
if (phase.work === undefined && input.baseOf(pathOf(step.planHref)) !== null) return null
|
|
300
|
+
const partial = step.label.includes(PLAN_VOCABULARY[input.language].partialPhase)
|
|
301
|
+
return { planPath: pathOf(step.planHref), phaseId: phase.id, partial }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The phase the lane's board row names and its open boxes, read from the checkout whether or not a commit changes the
|
|
306
|
+
* plan. Null for a lane the checkout's board does not list, for a row marked with the `partialPhase` word (`(Teil)`),
|
|
307
|
+
* which builds part of the phase, and for a phase ticked to the end, which is no longer the lane's current one.
|
|
308
|
+
*/
|
|
309
|
+
export const laneBuild = (input: LaneBoxesInput): LaneBuild | null => {
|
|
310
|
+
const built = phaseBuiltBy(input)
|
|
311
|
+
const plan = built && !built.partial ? readTextOrNull(join(input.root, built.planPath)) : null
|
|
312
|
+
return built && plan !== null
|
|
313
|
+
? { path: built.planPath, open: openBoxesOfPhase(plan, input.language, built.phaseId) }
|
|
314
|
+
: null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** What the level makes of a finding: `fail` fails, `warn` warns; `off` never reaches here. */
|
|
318
|
+
const statusOf = (level: OpenBoxesLevel): typeof CHECK_STATUS.fail | typeof CHECK_STATUS.warn =>
|
|
319
|
+
level === OPEN_BOXES_LEVEL.warn ? CHECK_STATUS.warn : CHECK_STATUS.fail
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The open boxes of the phase the lane builds (`laneBuild`) as a check: `level` decides whether they fail, warn or are
|
|
323
|
+
* not looked at (`off` returns null).
|
|
324
|
+
*/
|
|
325
|
+
export const laneOpenBoxesCheck = (input: LaneBoxesInput & { level: OpenBoxesLevel }): CheckResult | null => {
|
|
326
|
+
if (input.level === OPEN_BOXES_LEVEL.off) return null
|
|
327
|
+
const started = Date.now()
|
|
328
|
+
const build = laneBuild(input)
|
|
329
|
+
if (!build) return null
|
|
330
|
+
const { open } = build
|
|
331
|
+
return {
|
|
332
|
+
name: 'Open boxes of the lane phase',
|
|
333
|
+
command: `lane ${input.lane}, the phase its board row names has no open box`,
|
|
334
|
+
status: open === null ? CHECK_STATUS.pass : statusOf(input.level),
|
|
335
|
+
detail: open
|
|
336
|
+
? openBoxesText(input.lane, PLAN_VOCABULARY[input.language].partialPhase, build.path, open)
|
|
337
|
+
: 'the phase the lane builds has no open box left',
|
|
338
|
+
durationMs: Date.now() - started,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
230
342
|
/**
|
|
231
343
|
* The structure of the changed plans and boards:
|
|
232
344
|
* - a phase the change added (against `HEAD`) with tasks and without a review gate fails;
|
package/src/check/report.ts
CHANGED
|
@@ -143,12 +143,18 @@ export const formatPlanHeader = (
|
|
|
143
143
|
'',
|
|
144
144
|
]
|
|
145
145
|
|
|
146
|
+
const ADVISORY_LINE = /^(hint|note):/
|
|
147
|
+
|
|
148
|
+
/** A passing check shows only its advisory lines (`hint:`, `note:`); any other status shows its whole detail. */
|
|
149
|
+
const shownDetail = ({ status, detail }: CheckResult): string[] => {
|
|
150
|
+
const lines = detail ? detail.split('\n') : []
|
|
151
|
+
return status === CHECK_STATUS.pass ? lines.filter((line) => ADVISORY_LINE.test(line)) : lines
|
|
152
|
+
}
|
|
153
|
+
|
|
146
154
|
export const formatResults = (results: readonly CheckResult[]): string[] =>
|
|
147
155
|
results.flatMap((result) => [
|
|
148
156
|
`${STATUS_ICON[result.status]} ${result.name} (${result.command}, ${result.durationMs}ms)`,
|
|
149
|
-
...(result.
|
|
150
|
-
? result.detail.split('\n').map((line) => ` ${line}`)
|
|
151
|
-
: []),
|
|
157
|
+
...shownDetail(result).map((line) => ` ${line}`),
|
|
152
158
|
])
|
|
153
159
|
|
|
154
160
|
const warnings = (count: number): string => (count > 0 ? `${count} ${count === 1 ? 'warning' : 'warnings'}, ` : '')
|
package/src/check/schema.ts
CHANGED
|
@@ -68,6 +68,30 @@ const publicContent = z.strictObject({
|
|
|
68
68
|
/** A budget in words per path or glob from the project root; a key joining two paths with ` + ` budgets them together. */
|
|
69
69
|
const skillBudgets = z.record(glob, z.number().int().positive())
|
|
70
70
|
|
|
71
|
+
/** What `jh check` does with open boxes in the phase a lane builds: fail, warn or leave the check out. */
|
|
72
|
+
export const OPEN_BOXES_LEVEL = { fail: 'fail', warn: 'warn', off: 'off' } as const
|
|
73
|
+
|
|
74
|
+
export type OpenBoxesLevel = (typeof OPEN_BOXES_LEVEL)[keyof typeof OPEN_BOXES_LEVEL]
|
|
75
|
+
|
|
76
|
+
/** What `jh check` does with a lint-shaped rule block that names no lint rule: warn (the default), fail or leave the check out. */
|
|
77
|
+
export const LINT_POINTERS_LEVEL = { warn: 'warn', fail: 'fail', off: 'off' } as const
|
|
78
|
+
|
|
79
|
+
export type LintPointersLevel = (typeof LINT_POINTERS_LEVEL)[keyof typeof LINT_POINTERS_LEVEL]
|
|
80
|
+
|
|
81
|
+
const lintPointers = z.strictObject({
|
|
82
|
+
/** `warn` reports a block and lets the commit through; `fail` stops it; `off` skips the check. */
|
|
83
|
+
level: z
|
|
84
|
+
.enum([LINT_POINTERS_LEVEL.warn, LINT_POINTERS_LEVEL.fail, LINT_POINTERS_LEVEL.off])
|
|
85
|
+
.default(LINT_POINTERS_LEVEL.warn),
|
|
86
|
+
/** Rule files left unread, as globs from the project root; the kit copies in the lock file are always left out. */
|
|
87
|
+
exclude: z.array(glob).default([]),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
export type LintPointersConfig = z.infer<typeof lintPointers>
|
|
91
|
+
|
|
92
|
+
/** The check without a `lintPointers` block: warn, nothing excluded. */
|
|
93
|
+
export const DEFAULT_LINT_POINTERS: LintPointersConfig = { level: LINT_POINTERS_LEVEL.warn, exclude: [] }
|
|
94
|
+
|
|
71
95
|
/**
|
|
72
96
|
* The `check` block of `.jackherd/config.json`. Test and type commands come from the top-level
|
|
73
97
|
* `commands` block; this block holds what only the check runner reads.
|
|
@@ -89,6 +113,19 @@ export const checkConfigSchema = z.strictObject({
|
|
|
89
113
|
* path and sets one for the project's own skills; a file with none is measured against the kit's default.
|
|
90
114
|
*/
|
|
91
115
|
skillBudgets: skillBudgets.optional(),
|
|
116
|
+
/**
|
|
117
|
+
* What a lane's check does when the phase its board row names still has open boxes without `→ P<n>`: `fail` (the
|
|
118
|
+
* default) stops the commit, `warn` reports, `off` skips. A row carrying the `partialPhase` word (`(Teil)` in German) builds part of the phase and is
|
|
119
|
+
* left alone; a branch whose directory is no lane's is not checked.
|
|
120
|
+
*/
|
|
121
|
+
openBoxes: z
|
|
122
|
+
.enum([OPEN_BOXES_LEVEL.fail, OPEN_BOXES_LEVEL.warn, OPEN_BOXES_LEVEL.off])
|
|
123
|
+
.default(OPEN_BOXES_LEVEL.fail),
|
|
124
|
+
/**
|
|
125
|
+
* Whether a new or changed block of a rule file (or `CLAUDE.md`, `AGENTS.md`) that a linter could check, such
|
|
126
|
+
* as a forbidden import path or call, must name its lint rule with `` `lint:<rule id>` `` (or `` `lint:none` `` and a reason).
|
|
127
|
+
*/
|
|
128
|
+
lintPointers: lintPointers.optional(),
|
|
92
129
|
/** Also run tests in directories that hold test files but belong to no package. */
|
|
93
130
|
looseTests: z.boolean().default(false),
|
|
94
131
|
/**
|
package/src/check/settings.ts
CHANGED
|
@@ -25,6 +25,8 @@ export type CheckSettings = {
|
|
|
25
25
|
baseBranch: string
|
|
26
26
|
/** Where the lane worktrees live, below the main checkout: a board's plans are read there too. */
|
|
27
27
|
worktreeDirectory: string
|
|
28
|
+
/** Where the boards live: the directories under `planDirectory` and the file in each. */
|
|
29
|
+
board: { pattern: string; file: string }
|
|
28
30
|
/** Put before the kit's skill and agent names in this project; empty without one. */
|
|
29
31
|
skillPrefix: string
|
|
30
32
|
/** What the lock files (without one, the manifests) decide: one stack, none, or several (ambiguous); `stackOf` reads the stack from it. */
|
|
@@ -45,6 +47,7 @@ const planLanguageSchema = jackherdConfigSchema.shape.planLanguage
|
|
|
45
47
|
const baseBranchSchema = jackherdConfigSchema.shape.baseBranch
|
|
46
48
|
const worktreeDirectorySchema = jackherdConfigSchema.shape.worktreeDirectory
|
|
47
49
|
const skillPrefixSchema = jackherdConfigSchema.shape.skillPrefix
|
|
50
|
+
const boardSchema = jackherdConfigSchema.shape.board
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
53
|
* The check runner's view of `.jackherd/config.json`: the shared `commands` block, `planDirectory`, `planLanguage` and `baseBranch` plus its own
|
|
@@ -62,6 +65,7 @@ export const parseCheckSettings = (raw: unknown, facts: RepositoryFacts): CheckS
|
|
|
62
65
|
planLanguage: planLanguageSchema.parse(object.planLanguage),
|
|
63
66
|
baseBranch: baseBranchSchema.parse(object.baseBranch),
|
|
64
67
|
worktreeDirectory: worktreeDirectorySchema.parse(object.worktreeDirectory),
|
|
68
|
+
board: boardSchema.parse(object.board),
|
|
65
69
|
skillPrefix: skillPrefixSchema.parse(object.skillPrefix),
|
|
66
70
|
stack: detectStack(facts.lockFiles, facts.manifests),
|
|
67
71
|
notes: detected.notes,
|
|
@@ -8,11 +8,11 @@ export const KIT_FILE_FALLBACK_BUDGET = 2000
|
|
|
8
8
|
export const SUPPLEMENT_BUDGET = 800
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* The kit's word
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The kit's word targets, by path from the project root: the size a file should have, not the size it has. A key
|
|
12
|
+
* joining two paths with ` + ` targets what is loaded together. A file above its target carries a ceiling in
|
|
13
|
+
* `KIT_SKILL_CEILINGS`; growth never moves a target.
|
|
14
14
|
*/
|
|
15
|
-
export const
|
|
15
|
+
export const KIT_SKILL_TARGETS: Readonly<Record<string, number>> = {
|
|
16
16
|
'.claude/skills/devops/SKILL.md': 321,
|
|
17
17
|
'.claude/skills/diagnose/SKILL.md': 1521,
|
|
18
18
|
'.claude/skills/diagnose/references/feedback-loops.md': 636,
|
|
@@ -22,36 +22,47 @@ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
|
|
|
22
22
|
'.claude/skills/follow/SKILL.md': 1498,
|
|
23
23
|
'.claude/skills/grill/SKILL.md': 685,
|
|
24
24
|
'.claude/skills/implement/SKILL.md': 2190,
|
|
25
|
-
|
|
26
|
-
// Plain words: no plan code without its words in a short question.
|
|
27
|
-
// Any language: a pointer in § 0 and a section in events.md for `plan missing`.
|
|
28
|
-
// Open up: a pointer in § 0 to adopt-plan.md, and § 2 step 8 waits until the lane works on its prompt (+113);
|
|
29
|
-
// step 0 lets the adoption diff stand for its question, step 2 bases on origin only for an unchanged plan.
|
|
30
|
-
// Cross-check: measured in a repository whose stack nothing detects, every unset command renders as a note (+7).
|
|
31
|
-
// Ready phase: the `done` / `idle` row names the watcher's mandatory `ready:` line (+20).
|
|
32
|
-
// Drafts until the merge: § 5 step 5 opens a draft and readies it, Self-merge step 3 waits for the runs after it (+100).
|
|
33
|
-
// Ready round: a PR of the lane waiting for approval or merge holds only the commit; the self-merge command runs
|
|
34
|
-
// alone, so the guard can check it (+132, the longest rendering of the kit's renderings); skill-review: the commit
|
|
35
|
-
// of a PR awaiting approval stages only its files, a branch over a squash-merged PR moves with `--onto` (+51).
|
|
36
|
-
'.claude/skills/orchestrate/SKILL.md': 8413,
|
|
25
|
+
'.claude/skills/orchestrate/SKILL.md': 4500,
|
|
37
26
|
// skill-review: the copy is taken after a mirror, and rejecting together with a remark is a remark (+36).
|
|
38
27
|
// Cross-check: an adopted plan also gets short names and a no-review-gate line, which `jh check` asks of a new
|
|
39
28
|
// plan; the proof became a word diff, since a short name goes into an existing line (+74); skill-review: the
|
|
40
29
|
// done check names `jh check`, the gate and short-name rules as the code reads them (+24).
|
|
41
30
|
'.claude/skills/orchestrate/references/adopt-plan.md': 680,
|
|
42
31
|
'.claude/skills/orchestrate/references/acceptance.md': 200,
|
|
43
|
-
|
|
44
|
-
'.claude/skills/orchestrate/references/
|
|
32
|
+
// Core cut: the `due` row of the wake-up table moved here (+33).
|
|
33
|
+
'.claude/skills/orchestrate/references/appointments.md': 290,
|
|
34
|
+
// Resume: `--branch` finishes a lane whose worktree is gone, a detached worktree on the base closes, a closed lane
|
|
35
|
+
// is reported (+41). The ask rule is the one question: the coordinator asks no dialog before it (+15).
|
|
36
|
+
'.claude/skills/orchestrate/references/close-lane.md': 309,
|
|
37
|
+
'.claude/skills/orchestrate/references/coordinator-handover.md': 567,
|
|
45
38
|
'.claude/skills/orchestrate/references/draft-review.md': 300,
|
|
46
39
|
// P33: a source entry the user must read or decide becomes a row of what waits on them, and when it closes.
|
|
47
|
-
|
|
40
|
+
// Core cut: the `deploy`, `stalled` and lane `warn` rows of the wake-up table moved here (+101), and the checks
|
|
41
|
+
// after `Nothing changed.` from `jh check` (+70 as rendered), and the board patch when `main checkout behind`
|
|
42
|
+
// cannot fast-forward (+60). skill-review: `lane removed`, `no longer listed`, `did not stop` and
|
|
43
|
+
// `herd --ensure failed`, whose lines the watcher already points here (+86).
|
|
44
|
+
'.claude/skills/orchestrate/references/events.md': 1491,
|
|
48
45
|
'.claude/skills/orchestrate/references/herd.md': 299,
|
|
49
|
-
'.claude/skills/orchestrate/references/
|
|
50
|
-
|
|
46
|
+
'.claude/skills/orchestrate/references/herd-ask.md': 152,
|
|
47
|
+
// Core cut: the request on `lane alert` moved here (+56); ending a lane names `herdr agent prompt <lane> /exit`,
|
|
48
|
+
// the form the merge guard passes (+23).
|
|
49
|
+
'.claude/skills/orchestrate/references/lane-handover.md': 360,
|
|
50
|
+
// Ending the agent names `herdr agent prompt <lane> /exit`, the form the merge guard passes (+25).
|
|
51
|
+
'.claude/skills/orchestrate/references/lane-session.md': 285,
|
|
51
52
|
'.claude/skills/orchestrate/references/lane-setup.md': 380,
|
|
52
53
|
// Open up: an adopted plan is committed like a missing one, and a tracked copy is restored after the merge.
|
|
53
|
-
|
|
54
|
+
// 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,
|
|
56
|
+
// Core cut: § 5 self-merge steps 1 to 6, moved out of the core.
|
|
57
|
+
'.claude/skills/orchestrate/references/self-merge.md': 379,
|
|
54
58
|
'.claude/skills/orchestrate/references/sensitive-input.md': 269,
|
|
59
|
+
'.claude/skills/orchestrate/references/stage-approval.md': 634,
|
|
60
|
+
'.claude/skills/orchestrate/references/standing-approval.md': 458,
|
|
61
|
+
// Core cut: § 2 steps 0 to 8 and § 3 dev server, moved out of the core.
|
|
62
|
+
'.claude/skills/orchestrate/references/start-lane.md': 712,
|
|
63
|
+
// Core cut: § 4 waiting rows beyond the basic one, moved out of the core.
|
|
64
|
+
'.claude/skills/orchestrate/references/waiting.md': 216,
|
|
65
|
+
'.claude/skills/orchestrate/references/watch-without-monitor.md': 154,
|
|
55
66
|
'.claude/skills/plan/SKILL.md': 1920,
|
|
56
67
|
'.claude/skills/plan/references/template-English.md': 536,
|
|
57
68
|
'.claude/skills/plan/references/template-German.md': 501,
|
|
@@ -61,9 +72,7 @@ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
|
|
|
61
72
|
// Any language: step 3 points to proposing the missing commands from the CI; the proposal itself is a reference.
|
|
62
73
|
'.claude/skills/start/SKILL.md': 630,
|
|
63
74
|
'.claude/skills/uncle-bob/SKILL.md': 466,
|
|
64
|
-
|
|
65
|
-
// this repository's supplement after the skill-review fixes (+153).
|
|
66
|
-
'.claude/skills/orchestrate/SKILL.md + .claude/project/orchestrate.md': 9165,
|
|
75
|
+
'.claude/skills/orchestrate/SKILL.md + .claude/project/orchestrate.md': 5100,
|
|
67
76
|
'.claude/agents/devops.md': 104,
|
|
68
77
|
'.claude/agents/explore.md': 132,
|
|
69
78
|
'.claude/agents/implement.md': 303,
|
|
@@ -73,8 +82,50 @@ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
|
|
|
73
82
|
'.claude/agents/skill-review.md': 468,
|
|
74
83
|
'.claude/agents/spec-review.md': 1137,
|
|
75
84
|
'.claude/agents/uncle-bob.md': 111,
|
|
76
|
-
|
|
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,
|
|
77
87
|
// Drafts until the merge: a precondition opens a draft when `merge.openAsDraft` says so (+39).
|
|
78
88
|
'.claude/rules/kit/pull-requests.md': 640,
|
|
79
89
|
'.claude/rules/kit/testing.md': 741,
|
|
80
90
|
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The most a file above its target may have, today's size. A ceiling only falls: the plan that cuts the file lowers
|
|
94
|
+
* or deletes it, and a rise needs a reason in the comment above the entry (a test holds every entry to one).
|
|
95
|
+
*/
|
|
96
|
+
export const KIT_SKILL_CEILINGS: Readonly<Record<string, number>> = {
|
|
97
|
+
// Orchestrate core plan P1: the reviewer also judges the whole length of a file against its target (+12).
|
|
98
|
+
// Lint before prose: lens 7 for lint-shaped prose and the sentence on the `Lint pointers` result (+33).
|
|
99
|
+
'.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
|
+
// P33: the paw in the header chip, panes by tab and title, the lane in the context line of a passed-on question.
|
|
103
|
+
// Plain words: no plan code without its words in a short question.
|
|
104
|
+
// Any language: a pointer in § 0 and a section in events.md for `plan missing`.
|
|
105
|
+
// Open up: a pointer in § 0 to adopt-plan.md, and § 2 step 8 waits until the lane works on its prompt (+113);
|
|
106
|
+
// step 0 lets the adoption diff stand for its question, step 2 bases on origin only for an unchanged plan.
|
|
107
|
+
// Cross-check: measured in a repository whose stack nothing detects, every unset command renders as a note (+7).
|
|
108
|
+
// Ready phase: the `done` / `idle` row names the watcher's mandatory `ready:` line (+20).
|
|
109
|
+
// Drafts until the merge: § 5 step 5 opens a draft and readies it, Self-merge step 3 waits for the runs after it (+100).
|
|
110
|
+
// Ready round: a PR of the lane waiting for approval or merge holds only the commit; the self-merge command runs
|
|
111
|
+
// alone, so the guard can check it (+132, the longest rendering of the kit's renderings); skill-review: the commit
|
|
112
|
+
// of a PR awaiting approval stages only its files, a branch over a squash-merged PR moves with `--onto` (+51).
|
|
113
|
+
// Change id: the PR question names the staged change's patch id, which the merge guard compares; skill-review:
|
|
114
|
+
// staging first and the worktree named (+55, to cut again).
|
|
115
|
+
// Bundled board lines: written into the main checkout at once, carried to the base bundled, and the fast-forward
|
|
116
|
+
// that keeps them (+115).
|
|
117
|
+
// Audit P6: commands and hooks carry rules the prose repeated; monitorless watching, appointments, the standing
|
|
118
|
+
// approval and the herd's ask line moved to references. Cut to the measured size plus 5 %.
|
|
119
|
+
// Orchestrate core plan P1: the target is 4500. P5 cut the core to its longest rendering; the rest of the cut is
|
|
120
|
+
// a follow-up, decided by the user after the P6 runs.
|
|
121
|
+
'.claude/skills/orchestrate/SKILL.md': 5938,
|
|
122
|
+
// P33 and open up: grows with the skill above (+132 for the ready round and the lone merge call); measured with
|
|
123
|
+
// this repository's supplement after the skill-review fixes (+153); click merge grows with the skill (+30); change
|
|
124
|
+
// id grows with the skill, staging first and the worktree named after skill-review (+55). Audit P6: cut with the
|
|
125
|
+
// skill, the supplement down to its deviations; measured size plus 5 %.
|
|
126
|
+
// 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,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** What `jh check` measures against: a file's ceiling where it has one, else its target. */
|
|
131
|
+
export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = { ...KIT_SKILL_TARGETS, ...KIT_SKILL_CEILINGS }
|
|
@@ -11,6 +11,7 @@ import type { SkillBudgets } from './schema'
|
|
|
11
11
|
import {
|
|
12
12
|
KIT_FILE_FALLBACK_BUDGET,
|
|
13
13
|
KIT_SKILL_BUDGETS,
|
|
14
|
+
KIT_SKILL_TARGETS,
|
|
14
15
|
OWN_SKILL_BUDGET,
|
|
15
16
|
SUPPLEMENT_BUDGET,
|
|
16
17
|
} from './skill-budget-defaults'
|
|
@@ -21,8 +22,18 @@ export type BudgetTable = Readonly<SkillBudgets>
|
|
|
21
22
|
/** A file with the budget it falls back on when neither the project nor the kit names one. */
|
|
22
23
|
export type FileWords = { path: string; words: number; fallback: number; foreign?: boolean }
|
|
23
24
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A `foreign` measurement belongs to a skill `skills-lock.json` lists: it warns, it never fails. `target` is the
|
|
27
|
+
* kit's target for the path, `projectBudget` is the project's own entry for the path.
|
|
28
|
+
*/
|
|
29
|
+
export type Measurement = {
|
|
30
|
+
label: string
|
|
31
|
+
words: number
|
|
32
|
+
budget: number
|
|
33
|
+
foreign?: boolean
|
|
34
|
+
target?: number
|
|
35
|
+
projectBudget?: number
|
|
36
|
+
}
|
|
26
37
|
|
|
27
38
|
type Overrun = typeof CHECK_STATUS.warn | typeof CHECK_STATUS.fail
|
|
28
39
|
|
|
@@ -63,12 +74,16 @@ export const overrunOf = (words: number, budget: number): Overrun | null => {
|
|
|
63
74
|
return words > budget ? CHECK_STATUS.warn : null
|
|
64
75
|
}
|
|
65
76
|
|
|
66
|
-
/** The project's entry for the exact path, else its first glob that matches
|
|
67
|
-
|
|
77
|
+
/** The project's entry for the exact path, else its first glob that matches; undefined when it names neither. */
|
|
78
|
+
const projectBudgetOf = (path: string, project: BudgetTable): number | undefined => {
|
|
68
79
|
const glob = Object.keys(project).find((key) => isGlob(key) && matchesGlob(path, key))
|
|
69
|
-
return project[path] ?? (glob === undefined ? undefined : project[glob])
|
|
80
|
+
return project[path] ?? (glob === undefined ? undefined : project[glob])
|
|
70
81
|
}
|
|
71
82
|
|
|
83
|
+
/** The project's entry for the exact path, else its first glob that matches, else the kit's default, else `fallback`. */
|
|
84
|
+
export const budgetOf = (path: string, project: BudgetTable, defaults: BudgetTable, fallback: number): number =>
|
|
85
|
+
projectBudgetOf(path, project) ?? defaults[path] ?? fallback
|
|
86
|
+
|
|
72
87
|
/** Every budget path starts with `.claude/` (the kit-content test holds them to shipped files). */
|
|
73
88
|
const prefixedPath = (path: string, prefix: string): string =>
|
|
74
89
|
claudePath(prefixedKitPath(path.slice(CLAUDE_DIRECTORY.length + 1), prefix))
|
|
@@ -91,23 +106,39 @@ const sumEntry = (key: string, files: readonly FileWords[]): number | null => {
|
|
|
91
106
|
return parts.every((part) => part !== undefined) ? parts.reduce((total, part) => total + part.words, 0) : null
|
|
92
107
|
}
|
|
93
108
|
|
|
94
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Every file against its budget, then every sum entry of the project and the kit whose files all exist. `targets`
|
|
111
|
+
* are the kit's targets by path; a measurement carries its own when the kit names one.
|
|
112
|
+
*/
|
|
95
113
|
export const measureFiles = (
|
|
96
114
|
files: readonly FileWords[],
|
|
97
115
|
project: BudgetTable,
|
|
98
116
|
defaults: BudgetTable,
|
|
117
|
+
targets: BudgetTable = {},
|
|
99
118
|
): Measurement[] => {
|
|
100
119
|
const single = files.map(({ path, words, fallback, foreign }) => ({
|
|
101
120
|
label: foreign ? `${path} ${FOREIGN_LABEL}` : path,
|
|
102
121
|
words,
|
|
103
122
|
budget: budgetOf(path, project, defaults, fallback),
|
|
104
123
|
foreign,
|
|
124
|
+
target: targets[path],
|
|
125
|
+
projectBudget: projectBudgetOf(path, project),
|
|
105
126
|
}))
|
|
106
127
|
const sums = [...new Set([...Object.keys(project), ...Object.keys(defaults)])]
|
|
107
128
|
.filter((key) => key.includes(SUM_SEPARATOR))
|
|
108
129
|
.flatMap((key) => {
|
|
109
130
|
const words = sumEntry(key, files)
|
|
110
|
-
return words === null
|
|
131
|
+
return words === null
|
|
132
|
+
? []
|
|
133
|
+
: [
|
|
134
|
+
{
|
|
135
|
+
label: key,
|
|
136
|
+
words,
|
|
137
|
+
budget: budgetOf(key, project, defaults, 0),
|
|
138
|
+
target: targets[key],
|
|
139
|
+
projectBudget: projectBudgetOf(key, project),
|
|
140
|
+
},
|
|
141
|
+
]
|
|
111
142
|
})
|
|
112
143
|
return [...single, ...sums]
|
|
113
144
|
}
|
|
@@ -128,17 +159,33 @@ const statusOf = (overruns: readonly MeasuredOverrun[]): CheckStatus => {
|
|
|
128
159
|
return overruns.length > 0 ? CHECK_STATUS.warn : CHECK_STATUS.pass
|
|
129
160
|
}
|
|
130
161
|
|
|
131
|
-
/**
|
|
162
|
+
/** 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. */
|
|
163
|
+
const targetHint = ({ label, words, budget, target, projectBudget }: Measurement): string[] =>
|
|
164
|
+
target !== undefined && projectBudget === undefined && words > target && budget > target
|
|
165
|
+
? [`hint: ${label} is over target by ${words - target} words (target ${target}, ceiling ${budget})`]
|
|
166
|
+
: []
|
|
167
|
+
|
|
168
|
+
/** A budget the project set above the kit's target for the path: shown, so the deviation stays visible. */
|
|
169
|
+
const overrideNote = ({ label, target, projectBudget }: Measurement): string[] =>
|
|
170
|
+
target !== undefined && projectBudget !== undefined && projectBudget > target
|
|
171
|
+
? [`note: ${label} budget ${projectBudget} comes from the project, above the kit target ${target}`]
|
|
172
|
+
: []
|
|
173
|
+
|
|
174
|
+
/**
|
|
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
|
+
*/
|
|
132
178
|
export const budgetResult = (measured: readonly Measurement[], durationMs: number): CheckResult => {
|
|
133
179
|
const overruns = measured.flatMap((entry) => {
|
|
134
180
|
const overrun = overrunOfMeasurement(entry)
|
|
135
181
|
return overrun === null ? [] : [{ entry, overrun }]
|
|
136
182
|
})
|
|
183
|
+
const lines = overruns.length > 0 ? overruns.map(overrunLine) : [withinLine(measured.length)]
|
|
137
184
|
return {
|
|
138
185
|
name: 'Skill budgets',
|
|
139
186
|
command: `${measured.length} measured, words per budget`,
|
|
140
187
|
status: statusOf(overruns),
|
|
141
|
-
detail:
|
|
188
|
+
detail: [...lines, ...measured.flatMap(targetHint), ...measured.flatMap(overrideNote)].join('\n'),
|
|
142
189
|
durationMs,
|
|
143
190
|
}
|
|
144
191
|
}
|
|
@@ -211,5 +258,6 @@ export const skillBudgetCheck = (root: string, project: BudgetTable = {}, skillP
|
|
|
211
258
|
...ownSkillPaths(root, kit).flatMap((path) => fileWordsOf(root, path, OWN_SKILL_BUDGET)),
|
|
212
259
|
].map(markForeign(foreign))
|
|
213
260
|
const defaults = prefixedBudgets(KIT_SKILL_BUDGETS, skillPrefix)
|
|
214
|
-
|
|
261
|
+
const targets = prefixedBudgets(KIT_SKILL_TARGETS, skillPrefix)
|
|
262
|
+
return files.length === 0 ? null : budgetResult(measureFiles(files, project, defaults, targets), Date.now() - started)
|
|
215
263
|
}
|
package/src/cli.ts
CHANGED
|
@@ -18,6 +18,7 @@ const ALIASES = new Set<string>([HERD_COMMAND.alias])
|
|
|
18
18
|
|
|
19
19
|
/** One line per command; each entry lazily imports the module that exports its `run`. */
|
|
20
20
|
export const COMMANDS: CommandTable = {
|
|
21
|
+
approval: () => import('./approval/command'),
|
|
21
22
|
check: () => import('./check/command'),
|
|
22
23
|
config: () => import('./config/command'),
|
|
23
24
|
context: () => import('./context/command'),
|
|
@@ -28,6 +29,7 @@ export const COMMANDS: CommandTable = {
|
|
|
28
29
|
mermaid: () => import('./mermaid/command'),
|
|
29
30
|
plan: () => import('./plan-contract/command'),
|
|
30
31
|
preview: () => import('./preview/command'),
|
|
32
|
+
resume: () => import('./resume/command'),
|
|
31
33
|
secret: () => import('./secrets/command'),
|
|
32
34
|
settled: () => import('./settled/command'),
|
|
33
35
|
sync: () => import('./sync/command'),
|