@joshuafolkken/kit 1.1136.0 → 1.1178.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.
Files changed (200) hide show
  1. package/.claude/settings.json +4 -4
  2. package/.claude/skills/epic-commands/SKILL.md +27 -239
  3. package/.claude/skills/epic-commands/epic-bundle.md +115 -0
  4. package/.claude/skills/epic-commands/execution-waves.md +126 -0
  5. package/.claude/skills/workflow-commands/SKILL.md +48 -194
  6. package/.claude/skills/workflow-commands/background-commands.md +1 -1
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +12 -123
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +14 -63
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +16 -15
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -173
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +65 -144
  13. package/.claude/skills/workflow-commands/backlogrun.md +5 -4
  14. package/.claude/skills/workflow-commands/chain-rule.md +3 -2
  15. package/.claude/skills/workflow-commands/delegation.md +2 -2
  16. package/.claude/skills/workflow-commands/entry-sequence.md +62 -0
  17. package/.claude/skills/workflow-commands/followup.md +104 -181
  18. package/.claude/skills/workflow-commands/fullrun-steps.md +8 -9
  19. package/.claude/skills/workflow-commands/fullrun.md +16 -73
  20. package/.claude/skills/workflow-commands/halfrun.md +10 -36
  21. package/.claude/skills/workflow-commands/into-target.md +4 -4
  22. package/.claude/skills/workflow-commands/issue-comments.md +7 -8
  23. package/.claude/skills/workflow-commands/issue-scout.md +6 -6
  24. package/.claude/skills/workflow-commands/kickoff.md +6 -6
  25. package/.claude/skills/workflow-commands/needs-human-review.md +6 -6
  26. package/.claude/skills/workflow-commands/observation-filing.md +21 -172
  27. package/.claude/skills/workflow-commands/observation-ledger.md +150 -0
  28. package/.claude/skills/workflow-commands/pre-gate-cut.md +35 -95
  29. package/.claude/skills/workflow-commands/prerequisite.md +6 -6
  30. package/.claude/skills/workflow-commands/progress-watcher.md +90 -0
  31. package/.claude/skills/workflow-commands/prrun.md +5 -5
  32. package/.claude/skills/workflow-commands/retrospective.md +9 -9
  33. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  34. package/.claude/skills/workflow-commands/target-repository.md +2 -2
  35. package/.claude/skills/workflow-commands/working-tree-hold.md +7 -5
  36. package/.codex/hooks.json +4 -4
  37. package/.github/workflows/ci.yml +15 -2
  38. package/AGENTS.md +1 -6
  39. package/CLAUDE.md +58 -120
  40. package/GEMINI.md +1 -6
  41. package/dist/CLAUDE.md +58 -120
  42. package/dist/hooks/batch-guard.js +4 -4
  43. package/dist/hooks/{chunk-NFZGFGOR.js → chunk-32GQTL4F.js} +14 -1
  44. package/dist/hooks/{chunk-HBKZLITF.js → chunk-DO6NWWHU.js} +4 -4
  45. package/dist/hooks/{chunk-KBXZLMRH.js → chunk-G6KRCKEC.js} +216 -76
  46. package/dist/hooks/{chunk-JKKTVDAG.js → chunk-GDS5N2KB.js} +186 -309
  47. package/dist/hooks/chunk-I4XSL4TT.js +56 -0
  48. package/dist/hooks/{chunk-GDU26K3J.js → chunk-S6X6WYKR.js} +886 -174
  49. package/dist/hooks/{chunk-FO2I7UT7.js → chunk-UAGLLIGZ.js} +1 -1
  50. package/dist/hooks/{chunk-RFWWUI5F.js → chunk-VESKRQOF.js} +3 -3
  51. package/dist/hooks/{chunk-KOKOAV6D.js → chunk-VQLLNFNT.js} +6 -4
  52. package/dist/hooks/{chunk-LJBLGHEH.js → chunk-ZAL3NRGL.js} +4 -6
  53. package/dist/hooks/codex-hook-adapter.js +10 -10
  54. package/dist/hooks/duplicate-read-guard.js +5 -5
  55. package/dist/hooks/format-edited.js +2 -2
  56. package/dist/hooks/inputs.json +178 -177
  57. package/dist/hooks/investigation-guard.js +5 -5
  58. package/dist/hooks/pretool-guard.js +11 -10
  59. package/dist/hooks/stop-guard.js +20 -6
  60. package/dist/josh.js +29 -3
  61. package/dist/version/index.js +5 -1
  62. package/package.json +2 -1
  63. package/prompts/collaboration-workflow/file-edits.md +11 -10
  64. package/prompts/collaboration-workflow/gh-rest.md +2 -4
  65. package/prompts/collaboration-workflow/glossary.md +1 -1
  66. package/prompts/collaboration-workflow/issue-citation.md +5 -9
  67. package/prompts/collaboration-workflow/issue-template.md +40 -115
  68. package/prompts/collaboration-workflow/operating-rules.md +4 -5
  69. package/prompts/collaboration-workflow/output-bounds.md +12 -78
  70. package/prompts/collaboration-workflow/overview.md +7 -13
  71. package/prompts/collaboration-workflow/plan-comment.md +11 -39
  72. package/prompts/collaboration-workflow/principles.md +3 -4
  73. package/prompts/collaboration-workflow/report-format.md +28 -74
  74. package/prompts/collaboration-workflow/residency.md +12 -109
  75. package/prompts/collaboration-workflow/rule-delivery.md +98 -97
  76. package/prompts/collaboration-workflow/shell-body.md +3 -13
  77. package/prompts/collaboration-workflow/turn-batching.md +14 -88
  78. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -14
  79. package/prompts/collaboration-workflow/wip-cap.md +6 -45
  80. package/prompts/collaboration-workflow.md +1 -13
  81. package/prompts/review-rubric.md +8 -71
  82. package/prompts/review.md +25 -55
  83. package/prompts/testing-guide.md +18 -67
  84. package/scripts/agent/agent-role-profile.ts +1 -1
  85. package/scripts/agent/codex-hooks.ts +3 -12
  86. package/scripts/backlog/backlog-budget.ts +1 -1
  87. package/scripts/backlog/backlog-drive-cli.ts +4 -3
  88. package/scripts/backlog/backlog-drive-offer-argv.ts +10 -1
  89. package/scripts/backlog/backlog-next.ts +15 -4
  90. package/scripts/backlog/backlog-offer.ts +6 -6
  91. package/scripts/backlog/backlog-plan-cli.ts +6 -5
  92. package/scripts/backlog/backlog-rank.ts +18 -9
  93. package/scripts/backlog/backlog-restructure.ts +152 -0
  94. package/scripts/backlog/backlog-waves.ts +15 -17
  95. package/scripts/behavior/behavior-rules.ts +11 -4
  96. package/scripts/cost-runtime/context-cut-threshold.ts +1 -9
  97. package/scripts/cost-runtime/cost-verdict.ts +15 -28
  98. package/scripts/delegation/investigation-reads.ts +5 -5
  99. package/scripts/discovery/repo-discovery.ts +21 -7
  100. package/scripts/discovery/repo-origin.ts +26 -0
  101. package/scripts/discovery/repo-party.ts +42 -1
  102. package/scripts/document/backlogrun-parent-read-set.ts +14 -15
  103. package/scripts/document/document-byte-budget.ts +34 -20
  104. package/scripts/document/entry-read-budget.ts +21 -9
  105. package/scripts/document/lane-child-read-set.ts +12 -14
  106. package/scripts/document/read-set-cli.ts +4 -4
  107. package/scripts/document/read-set-point-of-use.ts +10 -2
  108. package/scripts/document/resident-budget.ts +47 -0
  109. package/scripts/epic/epic-shape.ts +10 -13
  110. package/scripts/epic/epic-solo.ts +1 -1
  111. package/scripts/exports/unused-members-cli.ts +7 -3
  112. package/scripts/followup/git-followup-stages.ts +9 -0
  113. package/scripts/followup/git-pr-followup.ts +1 -1
  114. package/scripts/gh/git-gh-api-path.ts +7 -0
  115. package/scripts/gh/git-gh-issue.ts +5 -3
  116. package/scripts/gh/git-gh-pr-read.ts +4 -0
  117. package/scripts/gh/git-pr-checks-eval.ts +8 -0
  118. package/scripts/gh/git-pr-checks.ts +11 -1
  119. package/scripts/git/constants.ts +10 -0
  120. package/scripts/git/main-merge.ts +57 -21
  121. package/scripts/git/schemas.ts +2 -0
  122. package/scripts/hooks/pretool-guard-cli.ts +2 -2
  123. package/scripts/hooks/pretool-guard.ts +45 -2
  124. package/scripts/hooks/run-hook.sh +41 -0
  125. package/scripts/init/hook-command-rewrite.ts +18 -35
  126. package/scripts/init/hook-launch.ts +21 -32
  127. package/scripts/issue/issue-auto-ok.ts +95 -0
  128. package/scripts/issue/issue-file-cli.ts +53 -7
  129. package/scripts/issue/issue-file.ts +28 -5
  130. package/scripts/issue/issue-labels.ts +12 -12
  131. package/scripts/issue/issue-rank.ts +1 -1
  132. package/scripts/issue/issue-read-cli.ts +3 -3
  133. package/scripts/issue/issue-read.ts +2 -2
  134. package/scripts/issue/issue-scout-cli.ts +2 -2
  135. package/scripts/issue/issue-state-cli.ts +1 -1
  136. package/scripts/issue/issue-state.ts +2 -2
  137. package/scripts/issue/issue-wip.ts +70 -0
  138. package/scripts/josh/hook-decision.ts +17 -0
  139. package/scripts/josh/josh-commands-development.ts +11 -1
  140. package/scripts/josh/josh-commands-issue.ts +2 -2
  141. package/scripts/lane/lane-launch-cli.ts +21 -3
  142. package/scripts/lines/bytes-command.ts +29 -6
  143. package/scripts/notify/telegram-notify.ts +26 -1
  144. package/scripts/observations/observation-ledger-line.ts +1 -1
  145. package/scripts/release/release-progress.ts +167 -0
  146. package/scripts/release/release-publish.ts +19 -8
  147. package/scripts/release/release-tag.ts +22 -8
  148. package/scripts/rules/decision-oracle-stage.ts +1 -1
  149. package/scripts/rules/decision-oracle.ts +6 -6
  150. package/scripts/rules/delivered-rules.ts +11 -11
  151. package/scripts/rules/direct-pr-create.ts +110 -0
  152. package/scripts/rules/file-body.ts +98 -42
  153. package/scripts/rules/filing-cap.ts +2 -2
  154. package/scripts/rules/filing-offer.ts +1 -1
  155. package/scripts/rules/gh-api.ts +18 -1
  156. package/scripts/rules/implementation-cut.ts +3 -1
  157. package/scripts/rules/lane-background.ts +67 -10
  158. package/scripts/rules/permission-guards.ts +10 -2
  159. package/scripts/rules/pre-gate-cut.ts +2 -2
  160. package/scripts/rules/stop-rules.ts +4 -4
  161. package/scripts/rules/test-declared-commit.ts +1 -1
  162. package/scripts/rules/third-party-write.ts +56 -12
  163. package/scripts/run/run-carry.ts +11 -5
  164. package/scripts/run/run-cut-cli.ts +1 -1
  165. package/scripts/run/run-cut-handoff.ts +21 -1
  166. package/scripts/run/run-cut-report.ts +2 -2
  167. package/scripts/run/run-entry-cli.ts +11 -3
  168. package/scripts/run/run-hold-cli.ts +16 -2
  169. package/scripts/run/run-label.ts +50 -0
  170. package/scripts/run/run-merge-steps.ts +8 -3
  171. package/scripts/run/run-merge.ts +1 -1
  172. package/scripts/run/run-prep-cli.ts +6 -2
  173. package/scripts/run/run-prep.ts +17 -0
  174. package/scripts/run/run-ship-cli.ts +27 -6
  175. package/scripts/run/run-ship-next.ts +13 -0
  176. package/scripts/run/run-ship-pre-detach.ts +66 -0
  177. package/scripts/run/run-ship-preflight.ts +34 -3
  178. package/scripts/run/run-ship-return.ts +7 -4
  179. package/scripts/run/run-ship-review-steps.ts +1 -1
  180. package/scripts/run/run-ship-review.ts +26 -5
  181. package/scripts/run/run-ship-stage.ts +13 -0
  182. package/scripts/run/run-ship-steps.ts +10 -2
  183. package/scripts/run/run-ship-sync.ts +114 -0
  184. package/scripts/run/run-ship.ts +6 -0
  185. package/scripts/run/run-step-cli.ts +20 -7
  186. package/scripts/run/run-wake-cli.ts +8 -1
  187. package/scripts/run/run-wake-driver.ts +62 -8
  188. package/scripts/run/run-wake-session.ts +2 -1
  189. package/scripts/test/test-declared-logic.ts +1 -1
  190. package/scripts/test/test-red-logic.ts +17 -2
  191. package/scripts/test/test-red.ts +8 -7
  192. package/scripts/time-runtime/time-batch-guard.ts +52 -16
  193. package/scripts/time-runtime/time-density-hook.ts +20 -2
  194. package/scripts/time-runtime/time-density.ts +4 -3
  195. package/scripts/time-runtime/time-markers.ts +15 -4
  196. package/scripts/time-runtime/time-transcript-line.ts +27 -1
  197. package/scripts/version/npm-registry.ts +11 -1
  198. package/templates/workflows/ci.yml +9 -1
  199. package/.claude/skills/workflow-commands/followup-reference.md +0 -118
  200. package/dist/hooks/chunk-HANSKHES.js +0 -464
@@ -0,0 +1,152 @@
1
+ import type { BusyRead } from '#scripts/epic/epic-busy'
2
+ import type { EpicChild } from '#scripts/epic/epic-graph'
3
+ import type { SoloSelection } from '#scripts/epic/epic-solo'
4
+
5
+ // Which candidates may not run side by side because both restructure the same file
6
+ // (joshuafolkken/kit#3221). The condition is deliberately narrow: plain overlap is not serialized,
7
+ // because every resume record of the 2026-10-05 backlog run was resolved mechanically by merging
8
+ // main. What git cannot resolve on its own is a rename or modify/delete conflict — two lanes that
9
+ // each move, split, rename or delete the same file — so only a path both bodies name on a line with
10
+ // such a verb holds the second one back. `remove` is left out: issue bodies use it for lines and
11
+ // options far more often than for files.
12
+ const RESTRUCTURE_WORDS: ReadonlySet<string> = new Set([
13
+ 'move',
14
+ 'moved',
15
+ 'moves',
16
+ 'moving',
17
+ 'rename',
18
+ 'renamed',
19
+ 'renames',
20
+ 'renaming',
21
+ 'split',
22
+ 'splits',
23
+ 'splitting',
24
+ 'delete',
25
+ 'deleted',
26
+ 'deletes',
27
+ 'deleting',
28
+ ])
29
+ const RESTRUCTURE_PHRASES: ReadonlyArray<string> = ['移動', '分割', '削除', 'リネーム', '改名']
30
+ const WORD = /[a-z]+/giu
31
+ const CODE_SPAN = /`([^`\s]+)`/gu
32
+ const LINE_SUFFIX = /:\d+(?::\d+)?$/u
33
+ const FILE_EXTENSION = /\.[a-z]\w*$/iu
34
+
35
+ // The restructured paths each issue declares, by issue number.
36
+ type Declared = ReadonlyMap<number, ReadonlySet<string>>
37
+
38
+ interface DeclaringIssue {
39
+ number: number
40
+ body?: string | undefined
41
+ }
42
+
43
+ function is_path(token: string): boolean {
44
+ return token.includes('/') || FILE_EXTENSION.test(token)
45
+ }
46
+
47
+ function paths_on(line: string): Array<string> {
48
+ return [...line.matchAll(CODE_SPAN)]
49
+ .map((match) => (match[1] ?? '').replace(LINE_SUFFIX, ''))
50
+ .filter((token) => is_path(token))
51
+ }
52
+
53
+ function is_restructuring(line: string): boolean {
54
+ const words = line.match(WORD) ?? []
55
+
56
+ if (words.some((word) => RESTRUCTURE_WORDS.has(word.toLowerCase()))) return true
57
+
58
+ return RESTRUCTURE_PHRASES.some((phrase) => line.includes(phrase))
59
+ }
60
+
61
+ // The backticked paths on the lines of `body` that carry a restructuring verb.
62
+ function restructured_paths(body: string): ReadonlySet<string> {
63
+ const lines = body.split('\n').filter((line) => is_restructuring(line))
64
+
65
+ return new Set(lines.flatMap((line) => paths_on(line)))
66
+ }
67
+
68
+ function declared_of(issues: ReadonlyArray<DeclaringIssue>): Declared {
69
+ return new Map(issues.map((issue) => [issue.number, restructured_paths(issue.body ?? '')]))
70
+ }
71
+
72
+ interface SeparateContext {
73
+ read: BusyRead
74
+ repo: string
75
+ }
76
+
77
+ // The path each running lane restructures, mapped to the issue that holds it.
78
+ function running_claims(declared: Declared, read: BusyRead): Map<string, number> {
79
+ const holders = read.kind === 'busy' ? read.issues.map((issue) => issue.number) : []
80
+ const pairs = holders.flatMap((number) =>
81
+ [...(declared.get(number) ?? [])].map((path): [string, number] => [path, number]),
82
+ )
83
+
84
+ return new Map(pairs)
85
+ }
86
+
87
+ function paths_of(child: EpicChild, declared: Declared, repo: string): ReadonlySet<string> {
88
+ if (child.repo !== repo) return new Set()
89
+
90
+ return declared.get(child.number) ?? new Set()
91
+ }
92
+
93
+ function clash_note(child: EpicChild, path: string, holder: number): string {
94
+ return `#${String(child.number)} waits: it and #${String(holder)} both restructure \`${path}\`.`
95
+ }
96
+
97
+ interface Walk {
98
+ claims: Map<string, number>
99
+ offered: Array<EpicChild>
100
+ withheld: Array<EpicChild>
101
+ notes: Array<string>
102
+ }
103
+
104
+ function visit(walk: Walk, child: EpicChild, paths: ReadonlySet<string>): void {
105
+ const clash = [...paths].find((path) => walk.claims.has(path))
106
+
107
+ if (clash === undefined) {
108
+ walk.offered.push(child)
109
+ for (const path of paths) walk.claims.set(path, child.number)
110
+
111
+ return
112
+ }
113
+
114
+ walk.withheld.push(child)
115
+ walk.notes.push(clash_note(child, clash, walk.claims.get(clash) ?? child.number))
116
+ }
117
+
118
+ function merged_notice(
119
+ notice: string | undefined,
120
+ notes: ReadonlyArray<string>,
121
+ ): string | undefined {
122
+ const parts = notice === undefined ? notes : [notice, ...notes]
123
+
124
+ return parts.length === 0 ? undefined : parts.join('\n')
125
+ }
126
+
127
+ // `selection` with every offered candidate that restructures a path a running lane or an earlier
128
+ // candidate already restructures moved to `withheld`, in offer order.
129
+ function separate(
130
+ selection: SoloSelection,
131
+ declared: Declared,
132
+ context: SeparateContext,
133
+ ): SoloSelection {
134
+ const walk: Walk = {
135
+ claims: running_claims(declared, context.read),
136
+ offered: [],
137
+ withheld: [],
138
+ notes: [],
139
+ }
140
+
141
+ for (const child of selection.offered) visit(walk, child, paths_of(child, declared, context.repo))
142
+
143
+ const notice = merged_notice(selection.notice, walk.notes)
144
+ const withheld = [...walk.withheld, ...selection.withheld]
145
+
146
+ return { offered: walk.offered, withheld, ...(notice !== undefined && { notice }) }
147
+ }
148
+
149
+ const backlog_restructure = { declared_of, restructured_paths, separate }
150
+
151
+ export { backlog_restructure }
152
+ export type { Declared }
@@ -10,7 +10,7 @@ import {
10
10
  NEEDS_DECISION_LABEL,
11
11
  } from '#scripts/issue/issue-labels'
12
12
  import { backlog_plan, type PlanContext } from './backlog-plan'
13
- import { backlog_rank } from './backlog-rank'
13
+ import { backlog_rank, type GateScope } from './backlog-rank'
14
14
 
15
15
  // The order a `backlogrun` takes, wave by wave, before it starts (joshuafolkken/kit#2778).
16
16
  //
@@ -18,7 +18,8 @@ import { backlog_rank } from './backlog-rank'
18
18
  // made from, so the order past the first answer was left for a person to work out by hand. This plays
19
19
  // the asks forward under one assumption: **every issue of a wave merges before the next wave starts**.
20
20
  //
21
- // **No rule is written twice.** The first wave is `backlog_rank.select` — rank, `run:solo` gate, cap —
21
+ // **No rule is written twice.** The first wave is `backlog_rank.select` — rank, `run:solo` gate,
22
+ // restructure separation, cap —
22
23
  // over the very candidates `backlog:next` resolves, asked of an idle repository — so it is what
23
24
  // `backlog:next` prints when nothing is running (joshuafolkken/kit#2928). Each later wave marks the earlier waves closed and re-classifies the same pool
24
25
  // through `epic_classify.classify_children` with the resolver `epic:next` sorted it with the first
@@ -28,6 +29,7 @@ import { backlog_rank } from './backlog-rank'
28
29
 
29
30
  const IDLE: BusyRead = { kind: 'idle' }
30
31
  const CLOSED = 'CLOSED'
32
+ const EMPTY_SCOPE: GateScope = { standalone: new Set(), declared: new Map() }
31
33
 
32
34
  const HEADING_ASSUMPTION =
33
35
  'Assumes every issue in a wave merges before the next wave starts. Issues a run already has are left out, and only issues of this repository are planned.'
@@ -59,17 +61,17 @@ interface WavePool {
59
61
  children: ReadonlyArray<EpicChild>
60
62
  running: ReadonlySet<string>
61
63
  repo: string
62
- standalone: ReadonlySet<string>
64
+ scope: GateScope
63
65
  }
64
66
 
65
- function pool_of(result: EpicNextResult, repo: string, standalone: ReadonlySet<string>): WavePool {
67
+ function pool_of(result: EpicNextResult, repo: string, scope: GateScope): WavePool {
66
68
  const all = backlog_rank.everything_in(result)
67
69
 
68
70
  return {
69
71
  children: all.filter((child) => !is_running(child)),
70
72
  running: epic_outside_blocker.running_keys(all),
71
73
  repo,
72
- standalone,
74
+ scope,
73
75
  }
74
76
  }
75
77
 
@@ -78,9 +80,9 @@ function offer(
78
80
  children: ReadonlyArray<EpicChild>,
79
81
  pool: WavePool,
80
82
  ): ReadonlyArray<EpicChild> {
81
- const { repo, standalone } = pool
83
+ const { repo, scope } = pool
82
84
 
83
- return backlog_rank.select({ candidates, pool: children, read: IDLE, repo, standalone }).offered
85
+ return backlog_rank.select({ candidates, pool: children, read: IDLE, repo, ...scope }).offered
84
86
  }
85
87
 
86
88
  // Ranked against the whole result, exactly as `backlog:next` ranks it, so the two agree.
@@ -106,14 +108,10 @@ function next_wave(pool: WavePool, done: ReadonlySet<string>): ReadonlyArray<Epi
106
108
  }
107
109
 
108
110
  // Each wave is closed before the next is asked for; a wave that offers nothing ends the plan, which
109
- // the pool's size bounds because every wave closes at least one child. `standalone` names the rows the
110
- // offer's cap bounds; left empty, no wave is capped.
111
- function build(
112
- result: EpicNextResult,
113
- repo: string,
114
- standalone: ReadonlySet<string> = new Set(),
115
- ): WavePlan {
116
- const pool = pool_of(result, repo, standalone)
111
+ // the pool's size bounds because every wave closes at least one child. `scope` names the rows the
112
+ // offer's cap bounds and the paths each issue restructures; left empty, no wave is capped or separated.
113
+ function build(result: EpicNextResult, repo: string, scope: GateScope = EMPTY_SCOPE): WavePlan {
114
+ const pool = pool_of(result, repo, scope)
117
115
  const done = new Set<string>()
118
116
  const waves: Array<ReadonlyArray<EpicChild>> = []
119
117
 
@@ -155,11 +153,11 @@ function unreached_lines(plan: WavePlan, context: PlanContext): Array<string> {
155
153
  function format_waves(
156
154
  result: EpicNextResult,
157
155
  context: PlanContext,
158
- standalone: ReadonlySet<string> = new Set(),
156
+ scope: GateScope = EMPTY_SCOPE,
159
157
  ): string {
160
158
  if (result.verdict === 'error') return backlog_plan.format_unusable(result)
161
159
 
162
- const plan = build(result, context.repo, standalone)
160
+ const plan = build(result, context.repo, scope)
163
161
  const waves = plan.waves.map((wave, index) => wave_line(wave, index, context.repo))
164
162
 
165
163
  return [
@@ -13,7 +13,13 @@ const TOOL_USE = 'tool_use'
13
13
  const COMMAND_KEY = 'command'
14
14
  const FIRST_POSITION_OFFSET = 1
15
15
  const TOOL_RESULT = 'tool_result'
16
- const DENIED_BASH_PREFIX = 'Permission to use Bash with command '
16
+ // The openings of the two results that mean a Bash call never ran: the permission layer's refusal,
17
+ // and a PreToolUse hook's block (joshuafolkken/kit#3197). A PostToolUse hook error is deliberately
18
+ // absent — that hook fires after the command has already run.
19
+ const DENIED_BASH_PREFIXES = [
20
+ 'Permission to use Bash with command ',
21
+ 'PreToolUse:Bash hook error: ',
22
+ ]
17
23
 
18
24
  // The git subcommands that stage or rewrite the index — the ones `CLAUDE.md` reserves for `pnpm josh
19
25
  // git` under "Never stage or mutate the git index on your own". An agent that runs one directly has
@@ -71,14 +77,15 @@ function is_index_mutation(command: string): boolean {
71
77
  .some((segment) => INDEX_MUTATION_HEAD.test(segment) && !is_dry_run(segment))
72
78
  }
73
79
 
74
- // An errored result alone may mean a command ran and failed. Only the permission layer's refusal
75
- // identifies a Bash call that never ran; the parser keeps the opening of that result's text.
80
+ // An errored result alone may mean a command ran and failed. Only a refusal before execution — the
81
+ // permission layer's or a PreToolUse hook's — identifies a Bash call that never ran; the parser keeps
82
+ // the opening of that result's text.
76
83
  function is_denied_result(block: Block): boolean {
77
84
  return (
78
85
  block.type === TOOL_RESULT &&
79
86
  block.result_id !== '' &&
80
87
  block.is_error === true &&
81
- block.error_text.startsWith(DENIED_BASH_PREFIX)
88
+ DENIED_BASH_PREFIXES.some((prefix) => block.error_text.startsWith(prefix))
82
89
  )
83
90
  }
84
91
 
@@ -10,12 +10,4 @@ import { context_cut_payback } from './context-cut-payback'
10
10
  // from the cache-price multipliers and the horizon rather than restated, so it moves with the model.
11
11
  const CONTEXT_CUT_THRESHOLD = context_cut_payback.break_even_context()
12
12
 
13
- // The hand-off prices the average billed input of the most recent requests, not the whole session
14
- // (joshuafolkken/kit#2295). A long parent session's whole-session average lags its current context by
15
- // dozens of requests — measured at 85 requests (~4 hours) behind on the 2026-09-21 parent — so it
16
- // keeps supervising an already-oversized context long after a cut was due. Ten requests is enough to
17
- // smooth a single outlier without reaching back into the cheap early context; it sits beside the
18
- // threshold so the window and the limit are read from one place and cannot drift.
19
- const RECENT_REQUEST_WINDOW = 10
20
-
21
- export { CONTEXT_CUT_THRESHOLD, RECENT_REQUEST_WINDOW }
13
+ export { CONTEXT_CUT_THRESHOLD }
@@ -3,42 +3,31 @@
3
3
  // of a session will cost, and takes only the figures its verdict needs — an over measurement — never
4
4
  // a whole report. The `--cap` counterfactual it once sat beside was retired in #2016 as readerless.
5
5
 
6
- import { RECENT_REQUEST_WINDOW } from './context-cut-threshold'
7
-
8
6
  const FAILURE_EXIT_CODE = 1
9
7
  const OVER_VERDICT = 'over'
10
8
  const UNDER_VERDICT = 'under'
11
9
 
12
- // What `--over` reads of a session: the billed input each request paid, oldest first. The whole
13
- // per-request sequence is carried rather than a single sum because the hand-off prices the recent
14
- // tail alone (joshuafolkken/kit#2295) — a long session's whole-session average lags its current
15
- // context by dozens of requests, so a sum could not answer what the recent window actually cost.
10
+ // What `--over` reads of a session: the billed input each request paid, oldest first. The sequence
11
+ // is carried rather than a single sum because the hand-off prices the newest request alone
12
+ // (joshuafolkken/kit#3224) — a sum could not answer what the session's current context costs.
16
13
  interface OverMeasurement {
17
14
  billed_input_per_request: ReadonlyArray<number>
18
15
  }
19
16
 
20
- // The most recent requests the hand-off prices, oldest first — the whole sequence when it is shorter
21
- // than the window, so a session with fewer than `RECENT_REQUEST_WINDOW` requests is priced on what it
22
- // has rather than padded.
23
- function recent_requests(measurement: OverMeasurement): ReadonlyArray<number> {
24
- return measurement.billed_input_per_request.slice(-RECENT_REQUEST_WINDOW)
25
- }
26
-
27
17
  // What a turn costs is decided by the accumulated preamble, not by what the turn does, so the
28
- // marginal cost of a session is the billed input its recent requests paid. Measured across one
18
+ // marginal cost of a session is the billed input its newest request paid. Measured across one
29
19
  // `epicrun` that ran six children in one context: 222k per request during the first child, 645k
30
20
  // during the sixth — the same work at 2.9x the price (joshuafolkken/kit#968).
31
21
  //
32
- // The recent window rather than the whole session, because a long parent's whole-session average
33
- // lags its current context by dozens of requests (joshuafolkken/kit#2295); the recent average says
34
- // what the *next* turn will cost, which is the thing a hand-off decision turns on.
22
+ // **The newest request, not an average.** joshuafolkken/kit#2295 replaced the whole-session average,
23
+ // which lagged a long parent by dozens of requests, with the average of the last ten; that still
24
+ // lagged. A session's billed input only grows between compactions (a unit writes its own transcript),
25
+ // so an average smooths no outlier — it only trails the context it averages. On 2026-10-05 wake
26
+ // session 03367124 crossed the threshold at request 65 and its ten-request average at request 70
27
+ // (joshuafolkken/kit#3224). The threshold is a break-even on the *current* context
28
+ // (`context-cut-payback.ts`), so the current context is what it is compared against.
35
29
  function per_request_cost(measurement: OverMeasurement): number {
36
- const window = recent_requests(measurement)
37
- if (window.length === 0) return 0
38
-
39
- const total = window.reduce((sum, billed) => sum + billed, 0)
40
-
41
- return Math.round(total / window.length)
30
+ return measurement.billed_input_per_request.at(-1) ?? 0
42
31
  }
43
32
 
44
33
  // The bare over/under decision, without a report: `run:status` reads this same verdict read-only
@@ -51,14 +40,12 @@ function classify(
51
40
  return per_request_cost(measurement) > limit ? OVER_VERDICT : UNDER_VERDICT
52
41
  }
53
42
 
54
- // The stderr line names the window the average covers, so the reader can tell a recent-tail average
55
- // from a whole-session one (joshuafolkken/kit#2295): "the last W of T request(s)" reads W when the
56
- // session is longer than the window and the whole count when it is shorter.
43
+ // The stderr line names which request was priced, so the reader can tell the newest request's cost
44
+ // from an average over the session (joshuafolkken/kit#3224).
57
45
  function over_summary(measurement: OverMeasurement, limit: number): string {
58
46
  const total = measurement.billed_input_per_request.length
59
- const window = recent_requests(measurement).length
60
47
 
61
- return `${String(per_request_cost(measurement))} billed input tokens per request over the last ${String(window)} of ${String(total)} request(s); limit ${String(limit)}`
48
+ return `${String(per_request_cost(measurement))} billed input tokens per request, on the newest of ${String(total)} request(s); limit ${String(limit)}`
62
49
  }
63
50
 
64
51
  // A verdict, not a table: the point of the flag is that the hand-off is decided by a number rather
@@ -71,7 +71,7 @@ const INVESTIGATOR_AGENT = 'investigator'
71
71
  // What the refusal tells the model. It has to name the count, the command and the return shape,
72
72
  // because the deny reason is the only text that reaches the model — a run that is refused and told
73
73
  // nothing reads it as a broken tool.
74
- const REASON = `⛔ investigation: ${String(delegation_policy.INVESTIGATION_FILE_THRESHOLD)} files read and not edited, or ${String(delegation_policy.INVESTIGATION_SEARCH_TURN_THRESHOLD)} search turns, since the last delegated unit, so the reading from here goes to a unit of its own (a successful write also starts the search count over). Ask \`pnpm josh delegate investigation\`, then dispatch the \`${INVESTIGATOR_AGENT}\` agent (\`kit:${INVESTIGATOR_AGENT}\` in a consumer) with a brief of what the main line has already concluded and what is left to find out; it returns the conclusion plus its \`file:line\` citations, never the file text. Keep a read in the main line only where this run will edit that file — an \`Edit\` cannot be issued against text you do not hold. This run's own instructions and the harness's own session files — a backgrounded call's output, a persisted tool result, this session's scratchpad — never count toward the files, so they are not what took the file count here; every read-only search turn counts, whatever it names. The rule is \`.claude/skills/workflow-commands/SKILL.md\` → §2b, "The pre-implementation reading".`
74
+ const REASON = `⛔ investigation: ${String(delegation_policy.INVESTIGATION_FILE_THRESHOLD)} files read and not edited, or ${String(delegation_policy.INVESTIGATION_SEARCH_TURN_THRESHOLD)} search turns, since the last delegated unit, so the reading from here goes to a unit of its own (a successful write also starts the search count over). Ask \`pnpm josh delegate investigation\`, then dispatch the \`${INVESTIGATOR_AGENT}\` agent (\`kit:${INVESTIGATOR_AGENT}\` in a consumer) with a brief of what the main line has already concluded and what is left to find out; it returns the conclusion plus its \`file:line\` citations, never the file text. Keep a read in the main line only where this run will edit that file — an \`Edit\` cannot be issued against text you do not hold. This run's own instructions and the harness's own session files — a backgrounded call's output, a persisted tool result, this session's scratchpad — never count toward the files, so they are not what took the file count here; every read-only search turn counts, whatever it names. The rule is \`.claude/skills/workflow-commands/delegation.md\` → "The pre-implementation reading".`
75
75
 
76
76
  interface ReadTally {
77
77
  // The files read and not since edited, resolved so the two ways a target reaches here compare.
@@ -92,13 +92,13 @@ interface ReadTally {
92
92
  searches: number
93
93
  searches_since: number
94
94
  // The files this run has successfully edited within the window, resolved (joshuafolkken/kit#1840).
95
- // A re-read of one of them is a read of a file the run edited, which §2b keeps in the main line — so
95
+ // A re-read of one of them is a read of a file the run edited, which `delegation.md` keeps in the main line — so
96
96
  // it is excluded from the count and from a call's projected growth rather than counted as fresh
97
97
  // investigation.
98
98
  edited: ReadonlySet<string>
99
99
  }
100
100
 
101
- // **The run's own instructions are not the Issue's subject, so reading them is not what §2b delegates.**
101
+ // **The run's own instructions are not the Issue's subject, so reading them is not what `delegation.md` delegates.**
102
102
  // #1426's rule is about reading to find out how the subject works; `CLAUDE.md`, a `prompts/` topic and
103
103
  // a skill file are read to find out what to *do*, they are read by nearly every run, and a unit cannot
104
104
  // be sent to read them on the main line's behalf. #1441's own measurement excluded 5 of them by hand
@@ -264,7 +264,7 @@ function is_failed_outcome(span: Span): boolean {
264
264
 
265
265
  // **Every file the run successfully wrote within the window, resolved** (joshuafolkken/kit#1840). It
266
266
  // is the one source of "this run edited that file", shared by the live tally and the replay: a read of
267
- // a file the run edits is reading §2b keeps in the main line, so it is never counted — whether the read
267
+ // a file the run edits is reading `delegation.md` keeps in the main line, so it is never counted — whether the read
268
268
  // came before the edit or after it. A failed edit wrote nothing and so exempts nothing, and a file
269
269
  // edited outside the window is unknown rather than assumed: the set is only ever as wide as the tail
270
270
  // the hook read, which is the bound joshuafolkken/kit#1764 already holds the accumulation to.
@@ -444,7 +444,7 @@ function is_at_search_threshold(search_count: number): boolean {
444
444
  //
445
445
  // **A delegated unit is not re-armed here because it is not refused at all** (joshuafolkken/kit#1840).
446
446
  // The hook counts a unit's reading against the unit (joshuafolkken/kit#1424), and the refusal asks for
447
- // a dispatch — but a unit is already where §2b sends the reading, and a read-only one (`Explore`,
447
+ // a dispatch — but a unit is already where `delegation.md` sends the reading, and a read-only one (`Explore`,
448
448
  // `Plan`) has no `Agent` tool to dispatch with. It was formerly degraded to a single first refusal,
449
449
  // which still asked for an action the unit could not take; now the unit short circuit in `should_block`
450
450
  // turns it off entirely, so this function is the parent's alone and needs no delegation condition.
@@ -59,22 +59,35 @@ function resolve_git_directory(repository_path: string): string | undefined {
59
59
  }
60
60
  }
61
61
 
62
- // The `origin` URL a work tree declares, or nothing when it has no `origin`, no readable config, or
63
- // is not a work tree at all. Every failure reads as "no remote" so one unreadable sibling cannot
64
- // fail a command whose contract is to report what it could find.
65
- function read_origin_url(repository_path: string): string | undefined {
62
+ // The work tree's git config text, or nothing when it is unreadable or not a work tree.
63
+ function read_config(repository_path: string): string | undefined {
66
64
  const git_directory = resolve_git_directory(repository_path)
67
65
  if (git_directory === undefined) return undefined
68
66
 
69
67
  try {
70
- return repo_origin.parse_origin_from_config(
71
- readFileSync(path.join(git_directory, GIT_CONFIG), 'utf8'),
72
- )
68
+ return readFileSync(path.join(git_directory, GIT_CONFIG), 'utf8')
73
69
  } catch {
74
70
  return undefined
75
71
  }
76
72
  }
77
73
 
74
+ // The `origin` URL a work tree declares, or nothing when it has no `origin`, no readable config, or
75
+ // is not a work tree at all. Every failure reads as "no remote" so one unreadable sibling cannot
76
+ // fail a command whose contract is to report what it could find.
77
+ function read_origin_url(repository_path: string): string | undefined {
78
+ const config = read_config(repository_path)
79
+
80
+ return config === undefined ? undefined : repo_origin.parse_origin_from_config(config)
81
+ }
82
+
83
+ // The `url` of every remote the work tree declares, with the same "unreadable reads as none" contract
84
+ // as `read_origin_url`.
85
+ function read_remote_urls(repository_path: string): Array<string> {
86
+ const config = read_config(repository_path)
87
+
88
+ return config === undefined ? [] : repo_origin.parse_remote_urls_from_config(config)
89
+ }
90
+
78
91
  // Every immediate child directory of `parent`, in name order. An unreadable parent yields nothing
79
92
  // rather than throwing: `josh doctor` reports what it could find, and a permission error on the
80
93
  // parent is not a broken install.
@@ -158,6 +171,7 @@ const repo_discovery = {
158
171
  OVERRIDE_ENV_KEY,
159
172
  main_worktree,
160
173
  read_origin_url,
174
+ read_remote_urls,
161
175
  resolve_current_owner,
162
176
  scan_siblings,
163
177
  discover_repositories,
@@ -26,6 +26,7 @@ const REPO_PATH_SEGMENTS = 2
26
26
  const NOT_FOUND = -1
27
27
  const SECTION_PREFIX = '['
28
28
  const ORIGIN_SECTION = /^\[remote\s+"origin"\]$/u
29
+ const REMOTE_SECTION = /^\[remote\s+"[^"]+"\]$/u
29
30
  const URL_KEY = 'url'
30
31
 
31
32
  // Whether a URL's hostname is GitHub. Exact, with no alias spelling accepted: an alias is a name
@@ -132,6 +133,30 @@ function parse_origin_from_config(content: string): string | undefined {
132
133
  .find((url) => url !== undefined)
133
134
  }
134
135
 
136
+ // The config's lines grouped by section, each group led by its header. Lines before the first header
137
+ // belong to no section and are dropped.
138
+ function section_groups(lines: ReadonlyArray<string>): Array<Array<string>> {
139
+ const groups: Array<Array<string>> = []
140
+
141
+ for (const line of lines) {
142
+ if (is_section_header(line)) groups.push([line])
143
+ else groups.at(-1)?.push(line)
144
+ }
145
+
146
+ return groups
147
+ }
148
+
149
+ // The `url` of every `[remote "…"]` section, in config order — the candidates `gh` picks its base
150
+ // repository from when it expands the `{owner}` placeholder (joshuafolkken/kit#3188).
151
+ function parse_remote_urls_from_config(content: string): Array<string> {
152
+ const lines = content.split('\n').map((line) => line.trim())
153
+
154
+ return section_groups(lines)
155
+ .filter((group) => REMOTE_SECTION.test(group[0] ?? ''))
156
+ .flatMap((group) => group.map((line) => entry_value(line, URL_KEY)))
157
+ .filter((url) => url !== undefined)
158
+ }
159
+
135
160
  // The `owner/repo` key a discovery map is keyed by. Lowercased, because GitHub resolves owner and
136
161
  // repository names case-insensitively: two remotes spelling the same repository differently are the
137
162
  // same repository, and keying by the spelling would put both in the map — where an override, which
@@ -146,6 +171,7 @@ const repo_origin = {
146
171
  is_github_ssh_host,
147
172
  parse_origin_from_config,
148
173
  parse_origin_url,
174
+ parse_remote_urls_from_config,
149
175
  format_identity,
150
176
  }
151
177
 
@@ -16,6 +16,8 @@ type Party = 'first-party' | 'third-party' | 'unknown'
16
16
  const FIRST_PARTY: Party = 'first-party'
17
17
  const THIRD_PARTY: Party = 'third-party'
18
18
  const UNKNOWN: Party = 'unknown'
19
+ const GH_REPO_ENV_KEY = 'GH_REPO'
20
+ const GH_REPO_PATH_SEGMENTS = 2
19
21
 
20
22
  // `unknown` is not `third-party`. An owner that cannot be read — no `origin`, an unreadable config —
21
23
  // is a "cannot decide", and reading it as third-party would refuse first-party writes whenever the
@@ -40,7 +42,46 @@ function current_owner(repository_path: string = process.cwd()): string | undefi
40
42
  return repo_discovery.resolve_current_owner(repository_path)
41
43
  }
42
44
 
43
- const repo_party = { FIRST_PARTY, THIRD_PARTY, UNKNOWN, classify, current_owner, target_owner }
45
+ // The owner `GH_REPO` names, read from its last two segments so an optional host prefix is skipped.
46
+ function gh_repo_owners(gh_repo: string): Array<string | undefined> {
47
+ return [target_owner(gh_repo.split('/').slice(-GH_REPO_PATH_SEGMENTS).join('/'))]
48
+ }
49
+
50
+ // The owner of every remote the work tree declares. A remote whose owner cannot be read stays in the
51
+ // set as `undefined` rather than being dropped: an SSH host alias (`git@github-work:o/r.git`) is one
52
+ // `gh` translates to github.com and may pick, so dropping it would hide the owner `gh` writes to.
53
+ function remote_owners(repository_path: string): Array<string | undefined> {
54
+ return repo_discovery
55
+ .read_remote_urls(repository_path)
56
+ .map((url) => repo_origin.parse_origin_url(url)?.owner)
57
+ }
58
+
59
+ // Every owner `gh` could expand the `{owner}` placeholder to (joshuafolkken/kit#3188). `GH_REPO`
60
+ // (`[HOST/]OWNER/REPO`) overrides the remotes outright; otherwise `gh` picks its base repository among
61
+ // the GitHub remotes — `upstream` ahead of `origin`, or a `gh repo set-default` choice — so in a fork
62
+ // checkout it can expand to the upstream owner. Answering the whole candidate set rather than
63
+ // re-implementing that pick keeps the caller safe whichever remote `gh` chooses. An `undefined` entry
64
+ // is a candidate whose owner could not be read, which the caller must not assume is first-party.
65
+ function placeholder_owners(
66
+ repository_path: string = process.cwd(),
67
+ environment: NodeJS.ProcessEnv = process.env,
68
+ ): Array<string | undefined> {
69
+ const gh_repo = environment[GH_REPO_ENV_KEY]
70
+
71
+ return gh_repo === undefined || gh_repo === ''
72
+ ? remote_owners(repository_path)
73
+ : gh_repo_owners(gh_repo)
74
+ }
75
+
76
+ const repo_party = {
77
+ FIRST_PARTY,
78
+ THIRD_PARTY,
79
+ UNKNOWN,
80
+ classify,
81
+ current_owner,
82
+ placeholder_owners,
83
+ target_owner,
84
+ }
44
85
 
45
86
  export type { Party }
46
87
  export { repo_party }
@@ -8,25 +8,19 @@
8
8
  // (`lane-child-read-set.ts`) — through the shared `read-set-trim`.
9
9
  //
10
10
  // **This is a different set from the worker's.** The parent keeps §0 (its session-cut / resume
11
- // paragraph is the parent's own), §2b (the parent's whole job is the epic-child delegation §2b
12
- // defines), §2c (the parent is the entry that receives an `owner/repo#` prefix), §2e and §2i (the
13
- // parent files prerequisites and observations for the batch) — all of which the worker drops. What the
14
- // parent drops is the implementer-only set below. **The parent owns every point-of-use document**, so
15
- // unlike the worker it drops none of them: the trim is the `SKILL.md` sections alone.
11
+ // paragraph is the parent's own), which the worker drops. **The parent owns every point-of-use
12
+ // document**, so unlike the worker it drops none of them: the trim is the `SKILL.md` sections alone.
16
13
 
17
14
  import type { ReadSetCost } from './entry-read-set'
18
15
  import { read_set_trim } from './read-set-trim'
19
16
 
20
17
  const BACKLOGRUN = 'backlogrun'
21
18
 
22
- // **The `SKILL.md` sections the `backlogrun` parent never uses.** The parent has no `new` entry, so it
23
- // names no `into` target (§2a); it claims no working tree, a dispatched child does that in its own
24
- // unit (§2f, `backlogrun.md:47-49`); it implements nothing, so it reads no Issue's comments before
25
- // implementing (§2g); and it edits no rule mid-run (§3, read only on an editing turn).
19
+ // **The `SKILL.md` sections the `backlogrun` parent never uses.** It edits no rule mid-run (§3, read
20
+ // only on an editing turn). The implementer-only rules it used to skip — the `into` target, the
21
+ // working-tree hold, the Issue's comments — are single rows of §2's table since joshuafolkken/kit#3174,
22
+ // so there is no section of them left to trim.
26
23
  const UNUSED_SKILL_SECTIONS: ReadonlyArray<string> = [
27
- '2a. The `into <target>` suffix — where the new Issue lands',
28
- '2f. The working-tree hold — one run per tree',
29
- "2g. An Issue's comments are part of the Issue",
30
24
  '3. What stays resident, and what is read from here',
31
25
  ]
32
26
 
@@ -34,10 +28,15 @@ const UNUSED_SKILL_SECTIONS: ReadonlyArray<string> = [
34
28
  // the park — so it drops none. Named empty so the shared trim reads the same shape as the worker's.
35
29
  const SKIPPED_POINT_OF_USE: ReadonlySet<string> = new Set<string>()
36
30
 
37
- // **The parent runs the end-of-run retrospective** (`SKILL.md` §2j) when `run:step` prints it, but its
31
+ // **The parent runs the end-of-run retrospective** (`retrospective.md`) when `run:step` prints it, but its
38
32
  // manifest names `retrospective.md` only through `backlogrun-steps.md` — one hop further than the
39
- // derivation follows — so the parent's reach names it (joshuafolkken/kit#3078).
40
- const REACHED_POINT_OF_USE: ReadonlySet<string> = new Set(['retrospective.md'])
33
+ // derivation follows — so the parent's reach names it (joshuafolkken/kit#3078). **It polls a
34
+ // delegated unit that went silent** through `backlogrun-recovery.md`, named only from
35
+ // `backlogrun-child.md`, so the reach names that too (joshuafolkken/kit#3175).
36
+ const REACHED_POINT_OF_USE: ReadonlySet<string> = new Set([
37
+ 'retrospective.md',
38
+ 'backlogrun-recovery.md',
39
+ ])
41
40
 
42
41
  function costed(root: string): ReadSetCost {
43
42
  return read_set_trim.costed(root, {