jackherd 1.14.0 → 1.15.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 +15 -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/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
|
@@ -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'),
|
package/src/config/schema.ts
CHANGED
|
@@ -46,6 +46,15 @@ export const isModelRole = (value: string): value is ModelRole =>
|
|
|
46
46
|
/** Lower-case letters and digits ending in a dash, so a prefixed name stays one plain path segment. */
|
|
47
47
|
export const SKILL_PREFIX = /^[a-z0-9]+-$/
|
|
48
48
|
|
|
49
|
+
/** What a global install, and a project that already has skills of kit names, puts before every kit name. */
|
|
50
|
+
export const DEFAULT_SKILL_PREFIX = 'jh-'
|
|
51
|
+
|
|
52
|
+
/** The skills every lane and the coordinator run on; `kit.exclude` cannot take them away. */
|
|
53
|
+
export const KIT_CORE: readonly string[] = ['orchestrate', 'start']
|
|
54
|
+
|
|
55
|
+
/** A kit skill or agent as the kit names it, before `skillPrefix`. */
|
|
56
|
+
const KIT_ENTRY_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
57
|
+
|
|
49
58
|
const FULL_MODEL_ID = /^claude-[a-z0-9][a-z0-9.-]*$/
|
|
50
59
|
|
|
51
60
|
const language = z.enum(Object.values(PLAN_LANGUAGE))
|
|
@@ -56,6 +65,13 @@ const modelId = z
|
|
|
56
65
|
|
|
57
66
|
const command = z.string().min(1)
|
|
58
67
|
|
|
68
|
+
const excludedKitEntry = z
|
|
69
|
+
.string()
|
|
70
|
+
.regex(KIT_ENTRY_NAME, 'a kit skill or agent name as the kit names it, e.g. devops')
|
|
71
|
+
.refine((name) => !KIT_CORE.includes(name), {
|
|
72
|
+
error: (issue) => `${issue.input} is the kit's core and cannot be excluded`,
|
|
73
|
+
})
|
|
74
|
+
|
|
59
75
|
const tokenCount = z.number().int().positive()
|
|
60
76
|
|
|
61
77
|
/**
|
|
@@ -360,6 +376,12 @@ const mergeSchema = z.strictObject({
|
|
|
360
376
|
* `--match-head-commit`. Empty means every merge waits for checks.
|
|
361
377
|
*/
|
|
362
378
|
withoutChecks: z.array(checkFreePattern).default([]),
|
|
379
|
+
/**
|
|
380
|
+
* Whether a lane opens every pull request as a draft (`gh pr create --draft`), so pushes while it is worked on start
|
|
381
|
+
* no workflow that skips drafts. `/orchestrate` runs `gh pr ready` before the merge and waits for the checks that
|
|
382
|
+
* start then; the merge guard never lets a draft through. Off opens pull requests ready, as before.
|
|
383
|
+
*/
|
|
384
|
+
openAsDraft: z.boolean().default(false),
|
|
363
385
|
})
|
|
364
386
|
|
|
365
387
|
const guardFields = z.strictObject({
|
|
@@ -480,6 +502,11 @@ export const jackherdConfigSchema = z.strictObject({
|
|
|
480
502
|
.union([z.literal(''), z.string().regex(SKILL_PREFIX, 'lower-case letters and digits ending in a dash, e.g. kit-')])
|
|
481
503
|
.default(''),
|
|
482
504
|
commands: commandsSchema.prefault({}),
|
|
505
|
+
/**
|
|
506
|
+
* Kit skills and agents `jh sync` leaves out, by the kit's name without `skillPrefix`; a name takes its skill and
|
|
507
|
+
* its agent alike. An unchanged copy already installed is removed, a changed one stays untracked.
|
|
508
|
+
*/
|
|
509
|
+
kit: z.strictObject({ exclude: z.array(excludedKitEntry).default([]) }).prefault({}),
|
|
483
510
|
/** How `/orchestrate` sets up a lane's worktree. */
|
|
484
511
|
lanes: z
|
|
485
512
|
.strictObject({
|
package/src/context/fill.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LOG_ENTRY_TYPE } from './log-vocabulary'
|
|
1
2
|
import { keptLogEntry } from './session-log'
|
|
2
3
|
|
|
3
4
|
/** How the context window of a model is assumed: by model ID, with a default for the models not listed. */
|
|
@@ -26,7 +27,7 @@ const count = (value: unknown): number => (typeof value === 'number' && Number.i
|
|
|
26
27
|
/** The answer of a main-chain `assistant` line; null for every other line, a sub-agent's answer and a synthetic one. */
|
|
27
28
|
const answerOf = (line: string): Answer | null => {
|
|
28
29
|
const entry = keptLogEntry(line)
|
|
29
|
-
if (!entry || entry.type !==
|
|
30
|
+
if (!entry || entry.type !== LOG_ENTRY_TYPE.assistant || entry.isSidechain === true) return null
|
|
30
31
|
const { message } = entry
|
|
31
32
|
if (!isRecord(message) || typeof message.model !== 'string' || message.model === SYNTHETIC_MODEL) return null
|
|
32
33
|
const { usage } = message
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The entry types of a Claude Code session log line that a reader tells apart; the log delivers them as strings. */
|
|
2
|
+
export const LOG_ENTRY_TYPE = { assistant: 'assistant' } as const
|
|
3
|
+
|
|
4
|
+
/** The block types inside an entry's `message.content`. */
|
|
5
|
+
export const CONTENT_BLOCK_TYPE = { text: 'text', toolUse: 'tool_use', toolResult: 'tool_result' } as const
|
|
6
|
+
|
|
7
|
+
/** The names of Claude Code's tools a reader looks for in a `tool_use` block. */
|
|
8
|
+
export const CLAUDE_TOOL = {
|
|
9
|
+
taskCreate: 'TaskCreate',
|
|
10
|
+
taskUpdate: 'TaskUpdate',
|
|
11
|
+
todoWrite: 'TodoWrite',
|
|
12
|
+
edit: 'Edit',
|
|
13
|
+
write: 'Write',
|
|
14
|
+
multiEdit: 'MultiEdit',
|
|
15
|
+
notebookEdit: 'NotebookEdit',
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What every assistant entry's raw line holds, as the log writes it without spaces: a text search for it finds the
|
|
20
|
+
* answers of a log without parsing every line.
|
|
21
|
+
*/
|
|
22
|
+
export const ASSISTANT_ENTRY_MARK = `"type":"${LOG_ENTRY_TYPE.assistant}"`
|
|
@@ -4,6 +4,7 @@ import { join } from 'node:path'
|
|
|
4
4
|
import { CLAUDE_DIRECTORY, SESSION_LOGS_DIRECTORY } from '../claude/paths'
|
|
5
5
|
import { boundedMemo } from '../shared/bounded-memo'
|
|
6
6
|
import { isRecord } from '../shared/is-record'
|
|
7
|
+
import { ASSISTANT_ENTRY_MARK } from './log-vocabulary'
|
|
7
8
|
|
|
8
9
|
/** A tail read before: the file as it was (inode, size, modification time), its lines and the byte offset each starts at. */
|
|
9
10
|
type FollowedTail = { inode: number; size: number; modifiedAt: number; lines: string[]; starts: number[] }
|
|
@@ -159,7 +160,7 @@ export const sessionLog = (
|
|
|
159
160
|
|
|
160
161
|
export type AgentSession = { session: string | null; cwd: string | null }
|
|
161
162
|
|
|
162
|
-
const hasAnswer = (lines: readonly string[]): boolean => lines.some((line) => line.includes(
|
|
163
|
+
const hasAnswer = (lines: readonly string[]): boolean => lines.some((line) => line.includes(ASSISTANT_ENTRY_MARK))
|
|
163
164
|
|
|
164
165
|
/** Claude Code names a project directory after the working directory, `/` and `.` turned into `-`. */
|
|
165
166
|
export const projectDirectoryOf = (cwd: string): string => cwd.replace(/[/.]/g, '-')
|
package/src/lane-prompt/build.ts
CHANGED
|
@@ -19,6 +19,8 @@ export type LanePromptInput = {
|
|
|
19
19
|
plans: LanePlan[]
|
|
20
20
|
port: number | null
|
|
21
21
|
handover: string | null
|
|
22
|
+
/** The project's `skillPrefix` (`''` for none), put before the name of the kit's skill the start names. */
|
|
23
|
+
skillPrefix: string
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const writesText = (language: PlanLanguage, texts: LanePromptTexts, writes: string[] | null): string => {
|
|
@@ -35,11 +37,12 @@ const planLines = (language: PlanLanguage, texts: LanePromptTexts, plan: LanePla
|
|
|
35
37
|
...plan.phases.map((phase) => ` - ${phaseText(language, texts, phase)}`),
|
|
36
38
|
]
|
|
37
39
|
|
|
38
|
-
const ruleLines = (input: LanePromptInput, texts: LanePromptTexts): string[] => [
|
|
39
|
-
input.port === null ? texts.noPort : texts.port(input.port),
|
|
40
|
+
const ruleLines = (input: LanePromptInput, texts: LanePromptTexts, implementSkill: string): string[] => [
|
|
41
|
+
input.port === null ? texts.noPort : texts.port(input.port, implementSkill),
|
|
40
42
|
texts.blocking,
|
|
41
43
|
texts.mustNot,
|
|
42
44
|
texts.commit,
|
|
45
|
+
texts.taskSignals,
|
|
43
46
|
texts.report(laneReportFile(input.lane)),
|
|
44
47
|
texts.textBlock,
|
|
45
48
|
texts.secret,
|
|
@@ -51,11 +54,13 @@ const ruleLines = (input: LanePromptInput, texts: LanePromptTexts): string[] =>
|
|
|
51
54
|
/**
|
|
52
55
|
* The prompt that starts a lane, in the plan language: a first line that names the lane (the words the ask hint hook
|
|
53
56
|
* and the project's lane exception recognise), the plans with their phases as `Phase P<n>` and each phase's write
|
|
54
|
-
* set, the rules of `/orchestrate` § 1
|
|
57
|
+
* set, the rules of `/orchestrate` § 1 and the lane contract (report each task, the report file, questions as text blocks,
|
|
58
|
+
* git through the coordinator), and the start: the phases built with `/implement` or another skill, or the
|
|
55
59
|
* hand-over file after a restart.
|
|
56
60
|
*/
|
|
57
61
|
export const buildLanePrompt = (input: LanePromptInput): string => {
|
|
58
62
|
const texts = LANE_PROMPT_TEXTS[input.language]
|
|
63
|
+
const implementSkill = `/${input.skillPrefix}implement`
|
|
59
64
|
return [
|
|
60
65
|
`${LANE_PROMPT_OPENING[input.language]} ${input.lane}. ${texts.answersTo}`,
|
|
61
66
|
'',
|
|
@@ -66,9 +71,9 @@ export const buildLanePrompt = (input: LanePromptInput): string => {
|
|
|
66
71
|
texts.editInside,
|
|
67
72
|
texts.writeTools,
|
|
68
73
|
'',
|
|
69
|
-
...ruleLines(input, texts).map((line) => `- ${line}`),
|
|
74
|
+
...ruleLines(input, texts, implementSkill).map((line) => `- ${line}`),
|
|
70
75
|
'',
|
|
71
|
-
input.handover === null ? texts.start : texts.restart(input.handover),
|
|
76
|
+
input.handover === null ? texts.start(implementSkill) : texts.restart(input.handover),
|
|
72
77
|
'',
|
|
73
78
|
].join('\n')
|
|
74
79
|
}
|