jackherd 1.14.0 → 1.15.1
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 +20 -0
- package/README.md +27 -19
- package/assets/icons/skills.svg +1 -0
- package/dist/config.d.ts +9 -1
- package/dist/config.js +24 -1
- package/dist/plan-format.d.ts +24 -1
- package/dist/plan-format.js +25 -5
- package/docs/plan-contract.md +84 -0
- package/docs/skills-dark.svg +15 -2
- package/docs/skills.svg +15 -2
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/herdr-plugin/restore-plans.ts +56 -6
- package/install.sh +1 -1
- package/kit/hooks/hooks/merge-guard/board-settings.ts +5 -2
- package/kit/hooks/hooks/merge-guard/decision.ts +3 -1
- package/kit/hooks/hooks/merge-guard/pull-request.ts +28 -15
- package/kit/hooks/hooks/merge-guard/run.ts +6 -3
- package/kit/hooks/hooks/merge-guard/settings.ts +12 -1
- package/kit/hooks/hooks/model-log/transcript.ts +4 -7
- package/kit/hooks/hooks/open-question/record.ts +4 -3
- package/kit/hooks/hooks/shared/dialog-answers.ts +7 -3
- package/kit/hooks/hooks/shared/log-vocabulary.ts +13 -0
- package/kit/hooks/hooks/shared/transcript.ts +7 -4
- package/kit/hooks/hooks/task-hint/decision.ts +3 -2
- package/kit/hooks/hooks/task-hint/run.ts +2 -4
- package/kit/rules/rules/kit/pull-requests.md +1 -0
- package/kit/workflow/skills/orchestrate/SKILL.md +22 -9
- package/kit/workflow/skills/orchestrate/handover/index.ts +8 -2
- package/kit/workflow/skills/orchestrate/references/adopt-plan.md +51 -0
- package/kit/workflow/skills/orchestrate/references/plan-commit.md +5 -5
- package/package.json +1 -1
- package/schemas/jackherd.schema.json +19 -0
- package/src/check/command.ts +1 -1
- package/src/check/file-kinds.ts +10 -8
- package/src/check/parallel-report.ts +10 -3
- package/src/check/settings.ts +4 -0
- package/src/check/skill-budget-defaults.ts +17 -5
- package/src/check/skill-budgets.ts +49 -19
- package/src/claude/paths.ts +12 -0
- package/src/cli.ts +1 -0
- package/src/config/schema.ts +27 -0
- package/src/context/fill.ts +2 -1
- package/src/context/log-vocabulary.ts +22 -0
- package/src/context/session-log.ts +2 -1
- package/src/lane-prompt/build.ts +10 -5
- package/src/lane-prompt/command.ts +6 -1
- package/src/lane-prompt/input.ts +4 -4
- package/src/lane-prompt/texts.ts +18 -10
- package/src/plan-contract/command.ts +69 -0
- package/src/plan-contract/contract.ts +144 -0
- package/src/plan-format/dependencies.ts +1 -4
- package/src/plan-format/index.ts +4 -0
- package/src/plan-format/lanes.ts +11 -0
- package/src/plan-format/markdown.ts +6 -5
- package/src/plan-format/model-table.ts +27 -0
- package/src/plans/activity-read.ts +98 -0
- package/src/plans/ask-coordinator.ts +20 -11
- package/src/plans/layout.ts +49 -4
- package/src/plans/live.ts +17 -4
- package/src/plans/plan-panes.ts +25 -5
- package/src/plans/snapshot.ts +18 -8
- package/src/plans/subagents-read.ts +4 -3
- package/src/plans/subagents.ts +2 -1
- package/src/plans/texts.ts +27 -1
- package/src/settled/send.ts +80 -4
- package/src/shared/parse-json.ts +8 -0
- package/src/sync/command.ts +17 -10
- package/src/sync/foreign-skills.ts +40 -0
- package/src/sync/global-kit.ts +31 -21
- package/src/sync/init.ts +2 -2
- package/src/sync/kit-exclude.ts +39 -0
- package/src/sync/plan-file.ts +1 -1
- package/src/sync/render.ts +2 -1
- package/src/sync/report.ts +37 -2
- package/src/sync/sync-result.ts +5 -2
- package/src/sync/sync.ts +13 -5
- package/src/task-signals/claude-signals.ts +6 -5
- package/src/watch/command.ts +56 -17
- package/src/watch/lane-message.ts +1 -1
- package/src/watch/lane-progress.ts +51 -2
- package/src/watch/lane-report.ts +57 -2
- package/src/watch/lane-watch.ts +2 -8
- package/src/watch/task-parallel-touched.ts +9 -2
- package/src/watch/texts.ts +4 -0
|
@@ -60,6 +60,9 @@ Each file under `references/` is read when its trigger fires: the step or the ev
|
|
|
60
60
|
`origin/{{kit.baseBranch}}` would not have it: § 2 step 0. New rows of a board that is on the base
|
|
61
61
|
branch need nothing; the herd shows them. Check a plan that joins the board later the same way; one
|
|
62
62
|
only a working lane's worktree has, the watcher reports as `plan missing`.
|
|
63
|
+
- [ ] **Every plan meets the contract.** `jh plan check <plan>` for each plan a starting lane links. Exit 1,
|
|
64
|
+
or a row that links an issue or a `.scratch/` folder instead of a plan file: [`references/adopt-plan.md`](references/adopt-plan.md),
|
|
65
|
+
before § 2 step 0. Exit 2 goes to the user. Done when each check exits 0.
|
|
63
66
|
- [ ] Tell the user which lanes start now and which wait, before starting any, and whether § 2 step 0
|
|
64
67
|
is needed.
|
|
65
68
|
|
|
@@ -84,7 +87,8 @@ stay approval-bound (§ 5, § 6).
|
|
|
84
87
|
When the user wants a plan's lanes merged without a question for a while: § 5, **Standing approval**.
|
|
85
88
|
|
|
86
89
|
0. **Plan commit as the base**, when § 0 found the board or a plan missing on
|
|
87
|
-
`origin/{{kit.baseBranch}}
|
|
90
|
+
`origin/{{kit.baseBranch}}`, or adopted a plan (then the diff round of
|
|
91
|
+
[`references/adopt-plan.md`](references/adopt-plan.md) is this question; ask nothing more). Ask through `AskUserQuestion`: commit the missing files on a
|
|
88
92
|
documentation branch and open a documentation PR, so the lanes start from that commit? The answer
|
|
89
93
|
is the approval of that commit, its push and the PR (§ 5). `<topic>` is the plan's file name
|
|
90
94
|
without date and extension, or the board's directory name when several files are missing. On yes,
|
|
@@ -99,7 +103,7 @@ When the user wants a plan's lanes merged without a question for a while: § 5,
|
|
|
99
103
|
your judgement.
|
|
100
104
|
2. **Worktree workspace** from `<base>`: `origin/{{kit.baseBranch}}` when, after
|
|
101
105
|
`git fetch origin {{kit.baseBranch}}`, `git cat-file -e origin/{{kit.baseBranch}}:<plan>` succeeds
|
|
102
|
-
for the lane's plan, else `origin/docs/plan-<topic>` of step 0. The file decides, not the state of
|
|
106
|
+
for the lane's plan and `git diff --quiet origin/{{kit.baseBranch}} -- <plan>` exits 0, else `origin/docs/plan-<topic>` of step 0. The file decides, not the state of
|
|
103
107
|
the PR, so a later lane gets the right base:
|
|
104
108
|
```bash
|
|
105
109
|
herdr worktree create --cwd <main-checkout> --branch <branch> --base <base> \
|
|
@@ -133,12 +137,16 @@ When the user wants a plan's lanes merged without a question for a while: § 5,
|
|
|
133
137
|
within a poll or two: `bunx jackherd herd --ensure`.
|
|
134
138
|
8. **Prompt** from the file of § 1:
|
|
135
139
|
```bash
|
|
136
|
-
herdr agent prompt <lane> "$(cat <prompt-file>)"
|
|
140
|
+
herdr agent prompt <lane> "$(cat <prompt-file>)" --wait --until working --timeout 30000
|
|
137
141
|
```
|
|
138
142
|
The prompt quotes commands in backticks (`git reset`, `/pre-commit`); typed into the command
|
|
139
143
|
line inside double quotes, your own shell would run them before the lane sees the text. Text
|
|
140
144
|
read through `$(cat …)` is not expanded again. Every `herdr agent prompt` in this skill goes
|
|
141
|
-
this way, from a file you wrote with the `Write` tool or `jh lane prompt` wrote
|
|
145
|
+
this way, from a file you wrote with the `Write` tool or `jh lane prompt` wrote, with the same
|
|
146
|
+
`--wait` options, so the lane confirms it started. `agent_prompt_stalled` or `timeout` does not prove the
|
|
147
|
+
text is lost: read the screen (`herdr agent read <lane> --source visible --lines 12`). Text still in the input
|
|
148
|
+
line: `herdr agent send-keys <lane> enter`; a lane already at work: done; neither: send the file once more,
|
|
149
|
+
same command. A second stall goes to the user.
|
|
142
150
|
|
|
143
151
|
## 3. Dev server
|
|
144
152
|
|
|
@@ -242,7 +250,7 @@ On wake-up:
|
|
|
242
250
|
| `blocked` | Read the screen (`--source detection`, or zoom the pane when cut off). A question is vetted (below, **Settled questions stay in the lane**): one the plan or a rule settles you answer in the lane, an open decision goes to the user via `AskUserQuestion`. You never hand the lane's pane to the user. A question dialog in a lane (not a permission dialog) is a gap in the lane prompt (§ 1): send it the text-block rule and the answer by prompt. A permission dialog for a step on the list in § 5 you confirm, after checking the cursor sits on the one-time option; anything else goes to the user. Dim text in the input line is a suggestion, not input (`--format ansi` shows `\e[2m`); in a multi-select dialog `space` toggles an option, `enter` moves on. The watcher prints a duty line for each blocked lane; it is done when the lane has its answer or the user has the question |
|
|
243
251
|
| `working` with `questions: <n>` | The lane changed the questions of its report while its subagents run. Read `report <path>` and close each new question as the `done` / `idle` row does (`jh settled`, or `AskUserQuestion` with the answer sent on by `herdr agent prompt`). No diff check and no next phase: the lane keeps working |
|
|
244
252
|
| `main checkout on <branch>, not …` | § 0, **The main checkout stays on `{{kit.baseBranch}}`** |
|
|
245
|
-
| `done` / `idle` | Read the report the watcher shows (`report <path>` and its first lines, or the screen); read further only where that is not enough. Check the diff against the `{{kit.heading.writes}}` set (`git -C <worktree> diff --stat <base>...HEAD`, with the ref the lane branched from in § 2 step 2 until it has rebased after the documentation PR's merge) and that checks are quoted with numbers, not claimed. Every blocking question in the report is closed in the same wake-up, also one under open points, one without its mark and also when the `questions: <n>` line is missing: settled by you through `jh settled` (below), or put to the user through `AskUserQuestion` (vetted, below) and the answer sent on to the lane by `herdr agent prompt`. A not blocking one is settled the same way or collected (**Not blocking questions**, below). An answer that stays in your chat with the user is none. Then start the lane's next ready phase (§ 2) or name the reason to wait |
|
|
253
|
+
| `done` / `idle` | Read the report the watcher shows (`report <path>` and its first lines, or the screen); read further only where that is not enough. Check the diff against the `{{kit.heading.writes}}` set (`git -C <worktree> diff --stat <base>...HEAD`, with the ref the lane branched from in § 2 step 2 until it has rebased after the documentation PR's merge) and that checks are quoted with numbers, not claimed. Every blocking question in the report is closed in the same wake-up, also one under open points, one without its mark and also when the `questions: <n>` line is missing: settled by you through `jh settled` (below), or put to the user through `AskUserQuestion` (vetted, below) and the answer sent on to the lane by `herdr agent prompt`. A not blocking one is settled the same way or collected (**Not blocking questions**, below). An answer that stays in your chat with the user is none. Then start the lane's next ready phase (§ 2), at once after a `<lane> ready: …` line, or name the reason to wait; a PR awaiting approval holds only its commit, which stages only its own files |
|
|
246
254
|
| `merged #<n>` | § 6, for every lane |
|
|
247
255
|
| `checks failed #<n> (<lane>): <checks> (<k> tests: …)` | Send the finding to the lane with the named checks and tests (data, above). Without names, the lane reads the run itself |
|
|
248
256
|
| `checks passed #<n>` | One line to the user: ready to merge, with the board's merge order |
|
|
@@ -475,6 +483,9 @@ always goes to the user, whatever the plan settles.
|
|
|
475
483
|
permission mode refuses the push, push its branch yourself under the same approval: run
|
|
476
484
|
`cd <worktree>` as a command of its own, then `git push --force-with-lease origin HEAD:<branch>`,
|
|
477
485
|
then `cd` back (the kit's git guard refuses `git -C`).
|
|
486
|
+
5. **Drafts** (**{{kit.merge.openAsDraft}}**, `pull-requests.md` → Drafts): when you report a draft PR to the
|
|
487
|
+
user to merge, run `gh pr ready <n>` first; for your own merge, **Self-merge** step 1. The merge guard
|
|
488
|
+
never passes a draft.
|
|
478
489
|
|
|
479
490
|
### Self-merge
|
|
480
491
|
|
|
@@ -482,15 +493,17 @@ When the user picked `"{{kit.guard.selfMergeLabel}}"` for a PR:
|
|
|
482
493
|
|
|
483
494
|
1. Commit, push and open the PR as in step 4. Enter in the board's `{{kit.heading.history}}` that the
|
|
484
495
|
user approved this PR with self-merge (time, lane, PR number; the time as § 4 reads it), so the
|
|
485
|
-
approval has a record outside the chat.
|
|
496
|
+
approval has a record outside the chat. A draft turns ready here: `gh pr ready <n>`.
|
|
486
497
|
2. **Merging without checks.** The paths a PR may carry to be merged without waiting for checks are
|
|
487
498
|
`{{kit.merge.withoutChecks}}` (`merge.withoutChecks` in the config; `none` means every merge waits).
|
|
488
499
|
Unless `none`, list the PR's files with `gh pr diff <n> --name-only` and compare each one with the
|
|
489
500
|
patterns yourself. When **every** file matches, go straight to step 4. One file outside, an empty
|
|
490
501
|
list, or a failed `gh` call: go on with step 3. The merge guard reads the file list from GitHub
|
|
491
502
|
itself; if it asks, the dialog is the user's (step 5).
|
|
492
|
-
3. Wait for the checks in the background, never in the foreground
|
|
493
|
-
|
|
503
|
+
3. Wait for the checks in the background, never in the foreground. After a ready (step 1), start only
|
|
504
|
+
once every check that counts (each of `guard.requiredChecks`, else each check the draft skipped) has
|
|
505
|
+
a run that started after the ready, as a watch would end on the draft's skipped runs at once. Then a Bash call
|
|
506
|
+
`gh pr checks <n> --watch` with `run_in_background: true`, and work on until it reports.
|
|
494
507
|
4. Green (or a PR that qualified in step 2): `gh pr merge <n> --squash --delete-branch --match-head-commit <sha>`,
|
|
495
508
|
with the plain PR number, the full `headRefOid` from `gh pr view <n> --json headRefOid` read right before, and
|
|
496
509
|
without `-R`, `--repo` or `--admin`. The merge guard lets exactly this command through for the approved branch
|
|
@@ -531,7 +544,7 @@ For a PR the user approved (step 2) whose lane works on a plan under a standing
|
|
|
531
544
|
|
|
532
545
|
1. A PR behind the base branch is rebased first (§ 6, under the PR's approval), so its checks run
|
|
533
546
|
against the current base.
|
|
534
|
-
2.
|
|
547
|
+
2. Turn a draft ready as in **Self-merge** step 1, then wait for the checks as in its step 3.
|
|
535
548
|
3. Green: the same command as **Self-merge** step 4, with the `<sha>` read right before. The merge
|
|
536
549
|
guard lets it through only before the approval's end, with every check green, GitHub able to merge
|
|
537
550
|
it and no workflow, migration, env file or file of the guards in it; otherwise it asks, and that dialog is the user's
|
|
@@ -25,6 +25,9 @@ const EXIT = { done: 0, aborted: 1, usage: 2 } as const
|
|
|
25
25
|
|
|
26
26
|
const ABORT_TITLE = 'Hand-over aborted'
|
|
27
27
|
|
|
28
|
+
/** As SKILL.md § 2 step 8: herdr reports a line left as a paste (`agent_prompt_stalled`) instead of taking it. */
|
|
29
|
+
const CONFIRM_WORKING = ['--wait', '--until', 'working', '--timeout', '30000']
|
|
30
|
+
|
|
28
31
|
const herdr = async (args: string[]): Promise<HerdrResult> => {
|
|
29
32
|
try {
|
|
30
33
|
const child = Bun.spawn(['herdr', ...args], { stdout: 'pipe', stderr: 'pipe' })
|
|
@@ -58,7 +61,10 @@ const sleep = (milliseconds: number): Promise<void> => new Promise((resolve) =>
|
|
|
58
61
|
|
|
59
62
|
type Log = (line: string) => Promise<void>
|
|
60
63
|
|
|
61
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Returns the reason of a failed herdr call, or null once `/clear` went out. No `--wait`: `/clear` never turns
|
|
66
|
+
* the agent `working`, and the wait for a new session confirms it.
|
|
67
|
+
*/
|
|
62
68
|
const sendClear = async (options: HandoverOptions, log: Log): Promise<string | null> => {
|
|
63
69
|
const cleared = await herdr(['agent', 'prompt', options.pane, '/clear'])
|
|
64
70
|
if (!cleared.ok) return `herdr agent prompt /clear failed: ${cleared.error}`
|
|
@@ -98,7 +104,7 @@ const sendCommand = async (rename: string | null, options: HandoverOptions, log:
|
|
|
98
104
|
const renamed = await herdr(['agent', 'rename', options.pane, rename])
|
|
99
105
|
await log(renamed.ok ? `renamed the agent to ${rename}` : `rename to ${rename} failed: ${renamed.error}`)
|
|
100
106
|
}
|
|
101
|
-
const sent = await herdr(['agent', 'prompt', options.pane, line.line])
|
|
107
|
+
const sent = await herdr(['agent', 'prompt', options.pane, line.line, ...CONFIRM_WORKING])
|
|
102
108
|
if (!sent.ok) return `herdr agent prompt failed: ${sent.error}`
|
|
103
109
|
await log(`sent: ${line.line}`)
|
|
104
110
|
return null
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# § 0: adopt a plan that misses the contract
|
|
2
|
+
|
|
3
|
+
Read when `jh plan check <plan>` exits 1 for a plan a starting lane links, or when the lane's row links an issue
|
|
4
|
+
or a `.scratch/<feature>/` folder instead of a plan file. The contract is `docs/plan-contract.md` in the jackherd
|
|
5
|
+
package; the check's lines name each gap with `file:line`.
|
|
6
|
+
|
|
7
|
+
**You add, the author's text stays.** Every word the plan holds stays, in its place: you insert lines, and into
|
|
8
|
+
an open task line only its short name. Done when `jh plan check <plan>` exits 0, `jh check` names no line of
|
|
9
|
+
`<plan>`, and `git diff --no-index --word-diff=porcelain <copy> <plan>` shows no line opening with `-` apart from
|
|
10
|
+
its `--- a/…` header (its exit 1 only means a difference).
|
|
11
|
+
|
|
12
|
+
## A plan in an issue tracker
|
|
13
|
+
|
|
14
|
+
A plan that lives only in issues (a spec with its tickets in GitHub or GitLab, or Markdown under
|
|
15
|
+
`.scratch/<feature>/`) is mirrored first, to `{{kit.planDirectory}}/<YYYY-MM-DD>-<topic>.md` with the date from
|
|
16
|
+
`date`. Read it with `gh issue view <n> --comments`, `glab issue view <n> --comments` or `Read`, and copy the
|
|
17
|
+
spec and each ticket's text as it stands, under a heading per ticket. The origin line under the title links the
|
|
18
|
+
issue or the folder; done when each ticket's text reads in the file as in the issue. From then on the file is the one source: the board row links it, and nothing is written back
|
|
19
|
+
to the issue. Then adopt the file like any other plan.
|
|
20
|
+
|
|
21
|
+
## The additions
|
|
22
|
+
|
|
23
|
+
Before the first addition (a mirrored plan: right after the mirror, so the diff shows only additions), run `mktemp`, keep the path it prints as `<copy>`, then `cp <plan> <copy>`. Then add only
|
|
24
|
+
what the check's lines name as missing or malformed, in the plan language's headings, and the short names and the
|
|
25
|
+
no-gate line, which `jh check` asks of a plan the commit adds:
|
|
26
|
+
|
|
27
|
+
- **Phases**: a heading `### {{kit.heading.phase}} <id>: <title>` above each section or ticket that is one
|
|
28
|
+
piece of work, ids counting from 1 in the plan's order. A heading in a form the check does not read keeps its
|
|
29
|
+
line; the readable heading goes on the line below it.
|
|
30
|
+
- **Tasks**: in a phase without checkboxes, a list at its end built from the lists and steps it already has, one
|
|
31
|
+
`- [ ] **Name**: …` per item, the name 40 characters at most and naming the result.
|
|
32
|
+
- **Model**: one row per phase without one, appended to `### {{kit.heading.modelRecommendations}}`, or that
|
|
33
|
+
table placed before the first phase, each row judged as `/plan` judges it (its model recommendations); the
|
|
34
|
+
rationale cell names the error cost.
|
|
35
|
+
- **Dependencies**, from two phases on: `### {{kit.heading.dependencies}}` with `{{kit.heading.dependsOn}}`
|
|
36
|
+
(the previous phase unless the plan says otherwise), `{{kit.heading.writes}}` estimated from the code the phase
|
|
37
|
+
names (search for its modules and files) and `{{kit.heading.serialMarker}}`.
|
|
38
|
+
- **Short names**: an open task `- [ ] <text>` of a phase, before its review gate, without one gets `**<name>**: `
|
|
39
|
+
after its box, the name taken from its text, 40 characters at most.
|
|
40
|
+
- **No review gate**: a phase with checkboxes and no line opening `**Review gate` (`de`: `**Review-Gate`) gets a
|
|
41
|
+
line after its tasks, in the plan's language:
|
|
42
|
+
`**No review gate:** adopted plan, the source planned no review` (`en`), `**Kein Review-Gate:** übernommener
|
|
43
|
+
Plan, die Quelle plante kein Review` (`de`).
|
|
44
|
+
|
|
45
|
+
## The user sees the diff
|
|
46
|
+
|
|
47
|
+
Write the output of `git diff --no-index <copy> <plan>` into `.claude/handover/adopt-<topic>.md` as a `diff` code block, its fence longer than any run of backticks in it, and show it as
|
|
48
|
+
[`draft-review.md`](draft-review.md) says. The round's question is the one of § 2 step 0: its first option
|
|
49
|
+
approves the additions together with the commit, the push and the documentation PR; each remark is an option of
|
|
50
|
+
its own and leads to another round; one option rejects the additions, and it stops this plan's lanes as in
|
|
51
|
+
step 0. Rejecting chosen together with another option counts as a remark.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# § 2 step 0 and § 6: the plan commit as the base
|
|
2
2
|
|
|
3
|
-
Read when § 0 finds the board or a plan missing on `origin/{{kit.baseBranch}}
|
|
4
|
-
plan commit, and again when its documentation PR has merged (§ 6).
|
|
3
|
+
Read when § 0 finds the board or a plan missing on `origin/{{kit.baseBranch}}`, or adopted one, and the user
|
|
4
|
+
said yes to the plan commit, and again when its documentation PR has merged (§ 6).
|
|
5
5
|
|
|
6
6
|
## The commit
|
|
7
7
|
|
|
@@ -13,7 +13,7 @@ Bash call of its own:
|
|
|
13
13
|
git worktree add {{kit.worktreeDirectory}}/docs-plan-<topic> -b docs/plan-<topic> origin/{{kit.baseBranch}}
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
1. From the main checkout, per missing file: `mkdir -p {{kit.worktreeDirectory}}/docs-plan-<topic>/<directory>`,
|
|
16
|
+
1. From the main checkout, per missing or adopted file: `mkdir -p {{kit.worktreeDirectory}}/docs-plan-<topic>/<directory>`,
|
|
17
17
|
then `cp <path> {{kit.worktreeDirectory}}/docs-plan-<topic>/<path>`. For `plan missing` while a lane
|
|
18
18
|
works, copy from the lane's worktree instead (`<lane worktree>/<path>`): the main checkout lacks it.
|
|
19
19
|
2. `cd` into the temporary worktree, as a command of its own (the kit's git guard refuses `git -C`).
|
|
@@ -28,8 +28,8 @@ The copies in the main checkout stay until the PR is merged.
|
|
|
28
28
|
|
|
29
29
|
The lanes rebase on `origin/{{kit.baseBranch}}` as after every merge. Then, after `git fetch origin {{kit.baseBranch}}`,
|
|
30
30
|
compare each copy in the main checkout with the merged file
|
|
31
|
-
(`git show origin/{{kit.baseBranch}}:<path> | cmp -s - <path>`, one per file; read-only)
|
|
32
|
-
|
|
31
|
+
(`git show origin/{{kit.baseBranch}}:<path> | cmp -s - <path>`, one per file; read-only). Of the copies that are
|
|
32
|
+
equal, a tracked file is restored with `git checkout -- <path>`, an untracked one removed, so the next update of the main checkout does not stop on them. A board that only gained lane
|
|
33
33
|
rows since stays and is no question; any other difference goes to the user. Once every lane of the plan has
|
|
34
34
|
rebased, delete the local branch (`git branch -D docs/plan-<topic>`; a squash merge leaves it unmerged), so a
|
|
35
35
|
later step 0 can use the name again.
|
package/package.json
CHANGED
|
@@ -113,6 +113,21 @@
|
|
|
113
113
|
},
|
|
114
114
|
"additionalProperties": false
|
|
115
115
|
},
|
|
116
|
+
"kit": {
|
|
117
|
+
"default": {},
|
|
118
|
+
"type": "object",
|
|
119
|
+
"properties": {
|
|
120
|
+
"exclude": {
|
|
121
|
+
"default": [],
|
|
122
|
+
"type": "array",
|
|
123
|
+
"items": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"additionalProperties": false
|
|
130
|
+
},
|
|
116
131
|
"lanes": {
|
|
117
132
|
"default": {},
|
|
118
133
|
"type": "object",
|
|
@@ -726,6 +741,10 @@
|
|
|
726
741
|
"type": "string",
|
|
727
742
|
"pattern": "^[A-Za-z0-9_.*/-]+$"
|
|
728
743
|
}
|
|
744
|
+
},
|
|
745
|
+
"openAsDraft": {
|
|
746
|
+
"default": false,
|
|
747
|
+
"type": "boolean"
|
|
729
748
|
}
|
|
730
749
|
},
|
|
731
750
|
"additionalProperties": false
|
package/src/check/command.ts
CHANGED
|
@@ -203,7 +203,7 @@ const planRootsOf = (root: string, worktreeDirectory: string): string[] => {
|
|
|
203
203
|
const ownChecksOf = (root: string, paths: readonly string[], settings: CheckSettings, home: string) => [
|
|
204
204
|
publicContentCheck(root, paths, settings.check.publicContent, home),
|
|
205
205
|
deferralArrowCheck(root, paths, settings.planDirectory),
|
|
206
|
-
skillBudgetCheck(root, settings.check.skillBudgets),
|
|
206
|
+
skillBudgetCheck(root, settings.check.skillBudgets, settings.skillPrefix),
|
|
207
207
|
...planStructureChecks({
|
|
208
208
|
root,
|
|
209
209
|
paths,
|
package/src/check/file-kinds.ts
CHANGED
|
@@ -116,17 +116,18 @@ export const isSourceFile = (path: string): boolean => {
|
|
|
116
116
|
)
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
/** Separators at either end: `_result` (a private Python module) and `test_result` both name `result`. */
|
|
120
|
+
const withoutOuterSeparators = (name: string): string => name.replace(/^[._-]+/, '').replace(/[._-]+$/, '')
|
|
120
121
|
|
|
121
122
|
/** The name a test file covers: its stem without the test word (`test_parser.py` → `parser`); null when nothing is left. */
|
|
122
123
|
export const testStemOf = (path: string): string | null => {
|
|
123
124
|
const stem = stemOf(path)
|
|
124
|
-
const stripped =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
const stripped = withoutOuterSeparators(
|
|
126
|
+
stem
|
|
127
|
+
.replace(TEST_WORD, (_match, separator: string) => separator)
|
|
128
|
+
.replace(CAMEL_TEST_SUFFIX, '')
|
|
129
|
+
.replace(CAMEL_TEST_PREFIX, ''),
|
|
130
|
+
)
|
|
130
131
|
return stripped === '' ? null : stripped
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -139,4 +140,5 @@ export const testStemsOf = (files: readonly string[], isTest: (path: string) =>
|
|
|
139
140
|
* `parser_test.go`, `test_parser.py`, `ParserTest.java`. Only the last extension of the source is dropped, so
|
|
140
141
|
* `foo.module.ts` needs `foo.module.test.ts`.
|
|
141
142
|
*/
|
|
142
|
-
export const hasNamedTest = (source: string, testStems: ReadonlySet<string>): boolean =>
|
|
143
|
+
export const hasNamedTest = (source: string, testStems: ReadonlySet<string>): boolean =>
|
|
144
|
+
testStems.has(withoutOuterSeparators(stemOf(source)))
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
NO_DEPENDENCY_TABLE,
|
|
3
|
+
type PhaseSchedule,
|
|
4
|
+
phaseCount,
|
|
5
|
+
phaseLabels,
|
|
6
|
+
phasesText,
|
|
7
|
+
wavesOfPlan,
|
|
8
|
+
} from '../plan-format'
|
|
2
9
|
import type { PlanLanguage } from '../vocabulary'
|
|
3
10
|
|
|
4
11
|
/** The flag by which `jh check` prints the waves of one plan instead of running the checks. */
|
|
@@ -12,7 +19,7 @@ export const parallelArgument = (args: readonly string[]): string | null => {
|
|
|
12
19
|
|
|
13
20
|
/** `critical path: P1 → P3 → P6 (3 phases)` */
|
|
14
21
|
const criticalPathLine = (path: readonly string[]): string =>
|
|
15
|
-
`critical path: ${phaseLabels(path, ' → ')} (${path.length}
|
|
22
|
+
`critical path: ${phaseLabels(path, ' → ')} (${phaseCount(path.length)})`
|
|
16
23
|
|
|
17
24
|
const scheduleLines = ({ waves, criticalPath, unplaced }: PhaseSchedule): string[] => [
|
|
18
25
|
...(waves.length === 0
|
|
@@ -29,5 +36,5 @@ const scheduleLines = ({ waves, criticalPath, unplaced }: PhaseSchedule): string
|
|
|
29
36
|
*/
|
|
30
37
|
export const parallelReportLines = (markdown: string, language: PlanLanguage): string[] => {
|
|
31
38
|
const schedule = wavesOfPlan(markdown, language)
|
|
32
|
-
return schedule === null ? [
|
|
39
|
+
return schedule === null ? [NO_DEPENDENCY_TABLE] : scheduleLines(schedule)
|
|
33
40
|
}
|
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
|
+
/** Put before the kit's skill and agent names in this project; empty without one. */
|
|
29
|
+
skillPrefix: string
|
|
28
30
|
/** What the lock files (without one, the manifests) decide: one stack, none, or several (ambiguous); `stackOf` reads the stack from it. */
|
|
29
31
|
stack: StackDetection
|
|
30
32
|
notes: string[]
|
|
@@ -42,6 +44,7 @@ const planDirectorySchema = jackherdConfigSchema.shape.planDirectory
|
|
|
42
44
|
const planLanguageSchema = jackherdConfigSchema.shape.planLanguage
|
|
43
45
|
const baseBranchSchema = jackherdConfigSchema.shape.baseBranch
|
|
44
46
|
const worktreeDirectorySchema = jackherdConfigSchema.shape.worktreeDirectory
|
|
47
|
+
const skillPrefixSchema = jackherdConfigSchema.shape.skillPrefix
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
50
|
* The check runner's view of `.jackherd/config.json`: the shared `commands` block, `planDirectory`, `planLanguage` and `baseBranch` plus its own
|
|
@@ -59,6 +62,7 @@ export const parseCheckSettings = (raw: unknown, facts: RepositoryFacts): CheckS
|
|
|
59
62
|
planLanguage: planLanguageSchema.parse(object.planLanguage),
|
|
60
63
|
baseBranch: baseBranchSchema.parse(object.baseBranch),
|
|
61
64
|
worktreeDirectory: worktreeDirectorySchema.parse(object.worktreeDirectory),
|
|
65
|
+
skillPrefix: skillPrefixSchema.parse(object.skillPrefix),
|
|
62
66
|
stack: detectStack(facts.lockFiles, facts.manifests),
|
|
63
67
|
notes: detected.notes,
|
|
64
68
|
}
|
|
@@ -22,7 +22,17 @@ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
|
|
|
22
22
|
// P33: the paw in the header chip, panes by tab and title, the lane in the context line of a passed-on question.
|
|
23
23
|
// Plain words: no plan code without its words in a short question.
|
|
24
24
|
// Any language: a pointer in § 0 and a section in events.md for `plan missing`.
|
|
25
|
-
|
|
25
|
+
// Open up: a pointer in § 0 to adopt-plan.md, and § 2 step 8 waits until the lane works on its prompt (+113);
|
|
26
|
+
// step 0 lets the adoption diff stand for its question, step 2 bases on origin only for an unchanged plan.
|
|
27
|
+
// Cross-check: measured in a repository whose stack nothing detects, every unset command renders as a note (+7).
|
|
28
|
+
// Ready phase: the `done` / `idle` row names the watcher's mandatory `ready:` line (+20).
|
|
29
|
+
// Drafts until the merge: § 5 step 5 opens a draft and readies it, Self-merge step 3 waits for the runs after it (+100).
|
|
30
|
+
'.claude/skills/orchestrate/SKILL.md': 8230,
|
|
31
|
+
// skill-review: the copy is taken after a mirror, and rejecting together with a remark is a remark (+36).
|
|
32
|
+
// Cross-check: an adopted plan also gets short names and a no-review-gate line, which `jh check` asks of a new
|
|
33
|
+
// plan; the proof became a word diff, since a short name goes into an existing line (+74); skill-review: the
|
|
34
|
+
// done check names `jh check`, the gate and short-name rules as the code reads them (+24).
|
|
35
|
+
'.claude/skills/orchestrate/references/adopt-plan.md': 680,
|
|
26
36
|
'.claude/skills/orchestrate/references/acceptance.md': 200,
|
|
27
37
|
'.claude/skills/orchestrate/references/close-lane.md': 250,
|
|
28
38
|
'.claude/skills/orchestrate/references/coordinator-handover.md': 551,
|
|
@@ -33,7 +43,8 @@ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
|
|
|
33
43
|
'.claude/skills/orchestrate/references/lane-handover.md': 280,
|
|
34
44
|
'.claude/skills/orchestrate/references/lane-session.md': 280,
|
|
35
45
|
'.claude/skills/orchestrate/references/lane-setup.md': 380,
|
|
36
|
-
|
|
46
|
+
// Open up: an adopted plan is committed like a missing one, and a tracked copy is restored after the merge.
|
|
47
|
+
'.claude/skills/orchestrate/references/plan-commit.md': 350,
|
|
37
48
|
'.claude/skills/orchestrate/references/sensitive-input.md': 269,
|
|
38
49
|
'.claude/skills/plan/SKILL.md': 1920,
|
|
39
50
|
'.claude/skills/plan/references/template-English.md': 536,
|
|
@@ -44,8 +55,8 @@ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
|
|
|
44
55
|
// Any language: step 3 points to proposing the missing commands from the CI; the proposal itself is a reference.
|
|
45
56
|
'.claude/skills/start/SKILL.md': 630,
|
|
46
57
|
'.claude/skills/uncle-bob/SKILL.md': 466,
|
|
47
|
-
// P33: grows with the skill above.
|
|
48
|
-
'.claude/skills/orchestrate/SKILL.md + .claude/project/orchestrate.md':
|
|
58
|
+
// P33 and open up: grows with the skill above.
|
|
59
|
+
'.claude/skills/orchestrate/SKILL.md + .claude/project/orchestrate.md': 8880,
|
|
49
60
|
'.claude/agents/devops.md': 104,
|
|
50
61
|
'.claude/agents/explore.md': 132,
|
|
51
62
|
'.claude/agents/implement.md': 303,
|
|
@@ -56,6 +67,7 @@ export const KIT_SKILL_BUDGETS: Readonly<Record<string, number>> = {
|
|
|
56
67
|
'.claude/agents/spec-review.md': 1137,
|
|
57
68
|
'.claude/agents/uncle-bob.md': 111,
|
|
58
69
|
'.claude/rules/kit/code-style.md': 1664,
|
|
59
|
-
|
|
70
|
+
// Drafts until the merge: a precondition opens a draft when `merge.openAsDraft` says so (+39).
|
|
71
|
+
'.claude/rules/kit/pull-requests.md': 640,
|
|
60
72
|
'.claude/rules/kit/testing.md': 741,
|
|
61
73
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { CLAUDE_DIRECTORY, claudePath, SKILLS_DIRECTORY } from '../claude/paths'
|
|
3
|
+
import { CLAUDE_DIRECTORY, claudePath, prefixedKitPath, SKILLS_DIRECTORY } from '../claude/paths'
|
|
4
4
|
import { projectLockCandidates } from '../config/legacy-names'
|
|
5
|
+
import { parseJsonOrNull } from '../shared/parse-json'
|
|
6
|
+
import { readForeignSkills, SKILLS_LOCK_FILE, skillEntryPath, skillNameOfEntry } from '../sync/foreign-skills'
|
|
5
7
|
import { parseManifest } from '../sync/manifest'
|
|
6
8
|
import { matchesGlob } from './glob'
|
|
7
9
|
import { CHECK_STATUS, type CheckResult, type CheckStatus } from './result'
|
|
@@ -17,9 +19,10 @@ import {
|
|
|
17
19
|
export type BudgetTable = Readonly<SkillBudgets>
|
|
18
20
|
|
|
19
21
|
/** A file with the budget it falls back on when neither the project nor the kit names one. */
|
|
20
|
-
export type FileWords = { path: string; words: number; fallback: number }
|
|
22
|
+
export type FileWords = { path: string; words: number; fallback: number; foreign?: boolean }
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
/** A `foreign` measurement belongs to a skill `skills-lock.json` lists: it warns, it never fails. */
|
|
25
|
+
export type Measurement = { label: string; words: number; budget: number; foreign?: boolean }
|
|
23
26
|
|
|
24
27
|
type Overrun = typeof CHECK_STATUS.warn | typeof CHECK_STATUS.fail
|
|
25
28
|
|
|
@@ -31,6 +34,7 @@ const KIT_RULE = /^rules\//
|
|
|
31
34
|
const PATHS_IN_FRONTMATTER = /^---\r?\n(?:(?!---)[^\n]*\n)*?paths:/
|
|
32
35
|
const SUM_SEPARATOR = ' + '
|
|
33
36
|
const FAIL_FACTOR = 1.1
|
|
37
|
+
const FOREIGN_LABEL = `(foreign, ${SKILLS_LOCK_FILE})`
|
|
34
38
|
|
|
35
39
|
const OVERRUN_LABEL: Record<Overrun, string> = { warn: 'warning', fail: 'error' }
|
|
36
40
|
|
|
@@ -65,6 +69,23 @@ export const budgetOf = (path: string, project: BudgetTable, defaults: BudgetTab
|
|
|
65
69
|
return project[path] ?? (glob === undefined ? undefined : project[glob]) ?? defaults[path] ?? fallback
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
/** Every budget path starts with `.claude/` (the kit-content test holds them to shipped files). */
|
|
73
|
+
const prefixedPath = (path: string, prefix: string): string =>
|
|
74
|
+
claudePath(prefixedKitPath(path.slice(CLAUDE_DIRECTORY.length + 1), prefix))
|
|
75
|
+
|
|
76
|
+
/** The kit's budgets under the paths a project with `skillPrefix` installs its skills and agents at; supplements keep theirs. */
|
|
77
|
+
export const prefixedBudgets = (budgets: BudgetTable, prefix: string): BudgetTable =>
|
|
78
|
+
prefix === ''
|
|
79
|
+
? budgets
|
|
80
|
+
: Object.fromEntries(
|
|
81
|
+
Object.entries(budgets).map(([key, words]) => [
|
|
82
|
+
budgetKeyPaths(key)
|
|
83
|
+
.map((path) => prefixedPath(path, prefix))
|
|
84
|
+
.join(SUM_SEPARATOR),
|
|
85
|
+
words,
|
|
86
|
+
]),
|
|
87
|
+
)
|
|
88
|
+
|
|
68
89
|
const sumEntry = (key: string, files: readonly FileWords[]): number | null => {
|
|
69
90
|
const parts = budgetKeyPaths(key).map((part) => files.find((file) => file.path === part))
|
|
70
91
|
return parts.every((part) => part !== undefined) ? parts.reduce((total, part) => total + part.words, 0) : null
|
|
@@ -76,10 +97,11 @@ export const measureFiles = (
|
|
|
76
97
|
project: BudgetTable,
|
|
77
98
|
defaults: BudgetTable,
|
|
78
99
|
): Measurement[] => {
|
|
79
|
-
const single = files.map(({ path, words, fallback }) => ({
|
|
80
|
-
label: path,
|
|
100
|
+
const single = files.map(({ path, words, fallback, foreign }) => ({
|
|
101
|
+
label: foreign ? `${path} ${FOREIGN_LABEL}` : path,
|
|
81
102
|
words,
|
|
82
103
|
budget: budgetOf(path, project, defaults, fallback),
|
|
104
|
+
foreign,
|
|
83
105
|
}))
|
|
84
106
|
const sums = [...new Set([...Object.keys(project), ...Object.keys(defaults)])]
|
|
85
107
|
.filter((key) => key.includes(SUM_SEPARATOR))
|
|
@@ -90,6 +112,11 @@ export const measureFiles = (
|
|
|
90
112
|
return [...single, ...sums]
|
|
91
113
|
}
|
|
92
114
|
|
|
115
|
+
const overrunOfMeasurement = ({ words, budget, foreign }: Measurement): Overrun | null => {
|
|
116
|
+
const overrun = overrunOf(words, budget)
|
|
117
|
+
return foreign && overrun === CHECK_STATUS.fail ? CHECK_STATUS.warn : overrun
|
|
118
|
+
}
|
|
119
|
+
|
|
93
120
|
const overrunLine = ({ entry: { label, words, budget }, overrun }: MeasuredOverrun): string =>
|
|
94
121
|
`${OVERRUN_LABEL[overrun]}: ${label} has ${words} words, budget ${budget}`
|
|
95
122
|
|
|
@@ -104,7 +131,7 @@ const statusOf = (overruns: readonly MeasuredOverrun[]): CheckStatus => {
|
|
|
104
131
|
/** One line per overrun; one line in total when none is over. Warns above a budget, fails above the budget plus 10 %. */
|
|
105
132
|
export const budgetResult = (measured: readonly Measurement[], durationMs: number): CheckResult => {
|
|
106
133
|
const overruns = measured.flatMap((entry) => {
|
|
107
|
-
const overrun =
|
|
134
|
+
const overrun = overrunOfMeasurement(entry)
|
|
108
135
|
return overrun === null ? [] : [{ entry, overrun }]
|
|
109
136
|
})
|
|
110
137
|
return {
|
|
@@ -124,19 +151,11 @@ const readText = (path: string): string | null => {
|
|
|
124
151
|
}
|
|
125
152
|
}
|
|
126
153
|
|
|
127
|
-
const parseJson = (text: string): unknown => {
|
|
128
|
-
try {
|
|
129
|
-
return JSON.parse(text)
|
|
130
|
-
} catch {
|
|
131
|
-
return null
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
154
|
/** The kit documents the first valid lock file lists, the lock under its old name counting while sync has not moved it. */
|
|
136
155
|
const kitPathsOf = (root: string): string[] => {
|
|
137
156
|
for (const candidate of projectLockCandidates(root)) {
|
|
138
157
|
const text = readText(candidate)
|
|
139
|
-
const parsed = text === null ? null : parseManifest(
|
|
158
|
+
const parsed = text === null ? null : parseManifest(parseJsonOrNull(text))
|
|
140
159
|
if (parsed?.ok) return Object.keys(parsed.manifest.files).filter((key) => KIT_DOCUMENT.test(key))
|
|
141
160
|
}
|
|
142
161
|
return []
|
|
@@ -159,9 +178,17 @@ const kitFileWordsOf = (root: string, kitPath: string): FileWords[] => {
|
|
|
159
178
|
|
|
160
179
|
const ownSkillPaths = (root: string, kit: ReadonlySet<string>): string[] =>
|
|
161
180
|
listDirectory(join(root, CLAUDE_DIRECTORY, SKILLS_DIRECTORY))
|
|
162
|
-
.map((name) => claudePath(
|
|
181
|
+
.map((name) => claudePath(skillEntryPath(name)))
|
|
163
182
|
.filter((path) => !kit.has(path))
|
|
164
183
|
|
|
184
|
+
/** Marks the entry file of a skill `skills-lock.json` lists; any other file is left as it is. */
|
|
185
|
+
const markForeign =
|
|
186
|
+
(foreign: ReadonlySet<string>) =>
|
|
187
|
+
(file: FileWords): FileWords => {
|
|
188
|
+
const name = skillNameOfEntry(file.path)
|
|
189
|
+
return name !== undefined && foreign.has(name) ? { ...file, foreign: true } : file
|
|
190
|
+
}
|
|
191
|
+
|
|
165
192
|
const supplementPaths = (root: string): string[] =>
|
|
166
193
|
listDirectory(join(root, CLAUDE_DIRECTORY, SUPPLEMENTS_DIRECTORY))
|
|
167
194
|
.filter((name) => name.endsWith('.md'))
|
|
@@ -170,16 +197,19 @@ const supplementPaths = (root: string): string[] =>
|
|
|
170
197
|
/**
|
|
171
198
|
* The installed kit skills with their references, kit agents and kit rules that load every session (the lock file
|
|
172
199
|
* says which files the kit installed), the project's supplements and its own skills, measured against the budgets.
|
|
200
|
+
* `skillPrefix` is the project's: the kit's budgets follow its skills and agents to their prefixed paths.
|
|
173
201
|
* Null when there is nothing to measure.
|
|
174
202
|
*/
|
|
175
|
-
export const skillBudgetCheck = (root: string, project: BudgetTable = {}): CheckResult | null => {
|
|
203
|
+
export const skillBudgetCheck = (root: string, project: BudgetTable = {}, skillPrefix = ''): CheckResult | null => {
|
|
176
204
|
const started = Date.now()
|
|
177
205
|
const kitPaths = kitPathsOf(root)
|
|
178
206
|
const kit = new Set(kitPaths.map((key) => claudePath(key)))
|
|
207
|
+
const foreign = readForeignSkills(root)
|
|
179
208
|
const files = [
|
|
180
209
|
...kitPaths.flatMap((key) => kitFileWordsOf(root, key)),
|
|
181
210
|
...supplementPaths(root).flatMap((path) => fileWordsOf(root, path, SUPPLEMENT_BUDGET)),
|
|
182
211
|
...ownSkillPaths(root, kit).flatMap((path) => fileWordsOf(root, path, OWN_SKILL_BUDGET)),
|
|
183
|
-
]
|
|
184
|
-
|
|
212
|
+
].map(markForeign(foreign))
|
|
213
|
+
const defaults = prefixedBudgets(KIT_SKILL_BUDGETS, skillPrefix)
|
|
214
|
+
return files.length === 0 ? null : budgetResult(measureFiles(files, project, defaults), Date.now() - started)
|
|
185
215
|
}
|
package/src/claude/paths.ts
CHANGED
|
@@ -10,5 +10,17 @@ export const HANDOVER_DIRECTORY = 'handover'
|
|
|
10
10
|
export const WORKTREES_DIRECTORY = 'worktrees'
|
|
11
11
|
export const SESSION_LOGS_DIRECTORY = 'projects'
|
|
12
12
|
|
|
13
|
+
/** A skill's file and an agent below `.claude/`, by name: `skills/<name>/<file>`, `agents/<name>.md`. */
|
|
14
|
+
export const SKILL_PATH = new RegExp(`^${SKILLS_DIRECTORY}/([^/]+)/(.+)$`)
|
|
15
|
+
export const AGENT_PATH = new RegExp(`^${AGENTS_DIRECTORY}/([^/]+)\\.md$`)
|
|
16
|
+
|
|
13
17
|
/** `.claude/<directory>` as a path in a project and in a text that names it. */
|
|
14
18
|
export const claudePath = (...segments: string[]): string => [CLAUDE_DIRECTORY, ...segments].join('/')
|
|
19
|
+
|
|
20
|
+
/** A kit path below `.claude/` as a project with `skillPrefix` installs it: skills and agents prefixed, the rest as it is. */
|
|
21
|
+
export const prefixedKitPath = (path: string, prefix: string): string => {
|
|
22
|
+
const skill = SKILL_PATH.exec(path)
|
|
23
|
+
if (skill) return `${SKILLS_DIRECTORY}/${prefix}${skill[1]}/${skill[2]}`
|
|
24
|
+
const agent = AGENT_PATH.exec(path)
|
|
25
|
+
return agent ? `${AGENTS_DIRECTORY}/${prefix}${agent[1]}.md` : path
|
|
26
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -26,6 +26,7 @@ export const COMMANDS: CommandTable = {
|
|
|
26
26
|
[HERD_COMMAND.alias]: () => import('./plans/command'),
|
|
27
27
|
lane: () => import('./lane/command'),
|
|
28
28
|
mermaid: () => import('./mermaid/command'),
|
|
29
|
+
plan: () => import('./plan-contract/command'),
|
|
29
30
|
preview: () => import('./preview/command'),
|
|
30
31
|
settled: () => import('./settled/command'),
|
|
31
32
|
sync: () => import('./sync/command'),
|