@joshuafolkken/kit 1.1128.0 → 1.1166.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 (192) hide show
  1. package/.claude/agents/investigator.md +17 -0
  2. package/.claude/settings.json +5 -4
  3. package/.claude/skills/epic-commands/SKILL.md +27 -239
  4. package/.claude/skills/epic-commands/epic-bundle.md +115 -0
  5. package/.claude/skills/epic-commands/execution-waves.md +126 -0
  6. package/.claude/skills/workflow-commands/SKILL.md +48 -194
  7. package/.claude/skills/workflow-commands/background-commands.md +1 -1
  8. package/.claude/skills/workflow-commands/backlogrun-child.md +12 -123
  9. package/.claude/skills/workflow-commands/backlogrun-lanes.md +11 -60
  10. package/.claude/skills/workflow-commands/backlogrun-park.md +10 -8
  11. package/.claude/skills/workflow-commands/backlogrun-progress.md +19 -175
  12. package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
  13. package/.claude/skills/workflow-commands/backlogrun-steps.md +64 -144
  14. package/.claude/skills/workflow-commands/backlogrun.md +5 -4
  15. package/.claude/skills/workflow-commands/chain-rule.md +3 -2
  16. package/.claude/skills/workflow-commands/delegation.md +7 -3
  17. package/.claude/skills/workflow-commands/entry-sequence.md +62 -0
  18. package/.claude/skills/workflow-commands/followup.md +104 -181
  19. package/.claude/skills/workflow-commands/fullrun-steps.md +8 -9
  20. package/.claude/skills/workflow-commands/fullrun.md +16 -73
  21. package/.claude/skills/workflow-commands/halfrun.md +10 -36
  22. package/.claude/skills/workflow-commands/into-target.md +4 -4
  23. package/.claude/skills/workflow-commands/issue-comments.md +7 -8
  24. package/.claude/skills/workflow-commands/issue-scout.md +6 -6
  25. package/.claude/skills/workflow-commands/kickoff.md +6 -6
  26. package/.claude/skills/workflow-commands/needs-human-review.md +6 -6
  27. package/.claude/skills/workflow-commands/observation-filing.md +21 -172
  28. package/.claude/skills/workflow-commands/observation-ledger.md +150 -0
  29. package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -93
  30. package/.claude/skills/workflow-commands/prerequisite.md +6 -6
  31. package/.claude/skills/workflow-commands/progress-watcher.md +90 -0
  32. package/.claude/skills/workflow-commands/prrun.md +5 -5
  33. package/.claude/skills/workflow-commands/retrospective.md +9 -9
  34. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  35. package/.claude/skills/workflow-commands/target-repository.md +2 -2
  36. package/.claude/skills/workflow-commands/working-tree-hold.md +7 -5
  37. package/.codex/hooks.json +4 -4
  38. package/AGENTS.md +1 -6
  39. package/CLAUDE.md +58 -120
  40. package/GEMINI.md +1 -6
  41. package/SECURITY.md +1 -0
  42. package/dist/CLAUDE.md +58 -120
  43. package/dist/hooks/batch-guard.js +4 -4
  44. package/dist/hooks/{chunk-MKVGBQPA.js → chunk-5KTCMCOT.js} +14 -1
  45. package/dist/hooks/{chunk-NXYMNPUQ.js → chunk-A6P7PN7P.js} +59 -21
  46. package/dist/hooks/{chunk-77J7IEQM.js → chunk-ANJ2WGFD.js} +1 -1
  47. package/dist/hooks/{chunk-4JZDP5X5.js → chunk-DYAVAM6S.js} +572 -127
  48. package/dist/hooks/{chunk-7HVB7SZG.js → chunk-ENA7P5FB.js} +4 -4
  49. package/dist/hooks/{chunk-M7QKEKWO.js → chunk-GU5MATVE.js} +214 -85
  50. package/dist/hooks/chunk-GYNGP7HI.js +56 -0
  51. package/dist/hooks/{chunk-5SD2YM7Z.js → chunk-URJA6XY4.js} +4 -4
  52. package/dist/hooks/{chunk-XYUIQOW3.js → chunk-V2BTOZHT.js} +6 -4
  53. package/dist/hooks/{chunk-AIOIOMPX.js → chunk-ZDZ555YH.js} +225 -78
  54. package/dist/hooks/codex-hook-adapter.js +10 -10
  55. package/dist/hooks/duplicate-read-guard.js +5 -5
  56. package/dist/hooks/format-edited.js +2 -2
  57. package/dist/hooks/inputs.json +186 -184
  58. package/dist/hooks/investigation-guard.js +5 -5
  59. package/dist/hooks/pretool-guard.js +11 -10
  60. package/dist/hooks/stop-guard.js +5 -5
  61. package/dist/josh.js +33 -3
  62. package/dist/version/index.js +5 -1
  63. package/package.json +7 -4
  64. package/prompts/collaboration-workflow/file-edits.md +11 -10
  65. package/prompts/collaboration-workflow/gh-rest.md +2 -4
  66. package/prompts/collaboration-workflow/glossary.md +1 -1
  67. package/prompts/collaboration-workflow/issue-citation.md +5 -9
  68. package/prompts/collaboration-workflow/issue-template.md +40 -115
  69. package/prompts/collaboration-workflow/operating-rules.md +4 -5
  70. package/prompts/collaboration-workflow/output-bounds.md +12 -78
  71. package/prompts/collaboration-workflow/overview.md +7 -13
  72. package/prompts/collaboration-workflow/plan-comment.md +11 -39
  73. package/prompts/collaboration-workflow/principles.md +3 -4
  74. package/prompts/collaboration-workflow/report-format.md +28 -74
  75. package/prompts/collaboration-workflow/residency.md +12 -109
  76. package/prompts/collaboration-workflow/rule-delivery.md +98 -97
  77. package/prompts/collaboration-workflow/shell-body.md +3 -13
  78. package/prompts/collaboration-workflow/turn-batching.md +14 -88
  79. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -14
  80. package/prompts/collaboration-workflow/wip-cap.md +6 -45
  81. package/prompts/collaboration-workflow.md +1 -13
  82. package/prompts/review-rubric.md +8 -71
  83. package/prompts/review.md +25 -55
  84. package/prompts/testing-guide.md +18 -67
  85. package/scripts/agent/agent-role-profile.ts +1 -1
  86. package/scripts/agent/codex-hooks.ts +3 -12
  87. package/scripts/backlog/backlog-budget.ts +1 -1
  88. package/scripts/backlog/backlog-drive-cli.ts +4 -3
  89. package/scripts/backlog/backlog-drive-offer-argv.ts +10 -1
  90. package/scripts/backlog/backlog-offer.ts +6 -6
  91. package/scripts/behavior/behavior-rules.ts +11 -4
  92. package/scripts/delegation/delegation-policy.ts +9 -1
  93. package/scripts/delegation/investigation-guard.ts +1 -1
  94. package/scripts/delegation/investigation-reads.ts +109 -23
  95. package/scripts/discovery/repo-discovery.ts +21 -7
  96. package/scripts/discovery/repo-origin.ts +26 -0
  97. package/scripts/discovery/repo-party.ts +42 -1
  98. package/scripts/document/backlogrun-parent-read-set.ts +14 -15
  99. package/scripts/document/capped-print.ts +74 -0
  100. package/scripts/document/document-byte-budget.ts +34 -20
  101. package/scripts/document/document-section-cli.ts +4 -3
  102. package/scripts/document/entry-read-budget.ts +21 -9
  103. package/scripts/document/lane-child-read-set.ts +12 -14
  104. package/scripts/document/read-set-cli.ts +4 -4
  105. package/scripts/document/read-set-point-of-use.ts +10 -2
  106. package/scripts/document/resident-budget.ts +47 -0
  107. package/scripts/epic/epic-shape.ts +10 -13
  108. package/scripts/epic/epic-solo.ts +1 -1
  109. package/scripts/exports/unused-members-cli.ts +7 -3
  110. package/scripts/gate/scoped-green.ts +25 -14
  111. package/scripts/gh/git-gh-api-path.ts +7 -0
  112. package/scripts/hooks/pretool-guard-cli.ts +2 -2
  113. package/scripts/hooks/pretool-guard.ts +45 -2
  114. package/scripts/hooks/run-hook.sh +41 -0
  115. package/scripts/init/claude-kit-only-settings.ts +29 -0
  116. package/scripts/init/hook-command-rewrite.ts +18 -35
  117. package/scripts/init/hook-launch.ts +21 -32
  118. package/scripts/init/init-copy-content.ts +8 -2
  119. package/scripts/init/initial-commit.ts +24 -0
  120. package/scripts/init/start-exec.ts +16 -3
  121. package/scripts/init/start-steps.ts +8 -12
  122. package/scripts/issue/issue-auto-ok.ts +95 -0
  123. package/scripts/issue/issue-file-cli.ts +53 -7
  124. package/scripts/issue/issue-file.ts +28 -5
  125. package/scripts/issue/issue-labels.ts +12 -12
  126. package/scripts/issue/issue-rank.ts +1 -1
  127. package/scripts/issue/issue-read-cli.ts +4 -3
  128. package/scripts/issue/issue-read.ts +2 -2
  129. package/scripts/issue/issue-report-failures.ts +6 -2
  130. package/scripts/issue/issue-scout-cli.ts +2 -2
  131. package/scripts/issue/issue-state-cli.ts +2 -2
  132. package/scripts/issue/issue-state.ts +2 -2
  133. package/scripts/issue/issue-wip.ts +70 -0
  134. package/scripts/josh/hook-decision.ts +17 -0
  135. package/scripts/josh/josh-commands-development.ts +10 -0
  136. package/scripts/josh/josh-commands-issue.ts +2 -2
  137. package/scripts/josh/josh-commands-lane.ts +1 -1
  138. package/scripts/josh/josh-commands-project.ts +7 -0
  139. package/scripts/lane/lane-await.ts +46 -2
  140. package/scripts/lane/lane-cli.ts +11 -13
  141. package/scripts/lines/bytes-command.ts +29 -6
  142. package/scripts/observations/observation-ledger-line.ts +1 -1
  143. package/scripts/release/github-release-environment.ts +1 -0
  144. package/scripts/release/github-release.ts +103 -17
  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/stop-rules.ts +4 -4
  160. package/scripts/rules/test-declared-commit.ts +1 -1
  161. package/scripts/rules/third-party-write.ts +56 -12
  162. package/scripts/run/run-carry-cli.ts +3 -1
  163. package/scripts/run/run-carry-conversation.ts +94 -0
  164. package/scripts/run/run-carry-reclaim.ts +37 -0
  165. package/scripts/run/run-carry.ts +21 -5
  166. package/scripts/run/run-cut-handoff.ts +21 -1
  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 +12 -4
  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-preflight.ts +34 -3
  175. package/scripts/run/run-ship-review-steps.ts +1 -1
  176. package/scripts/run/run-ship-review.ts +26 -5
  177. package/scripts/run/run-step-cli.ts +20 -7
  178. package/scripts/run/run-wake-driver.ts +24 -1
  179. package/scripts/run/run-wake-session.ts +2 -1
  180. package/scripts/test/test-declared-logic.ts +1 -1
  181. package/scripts/test/test-related.ts +1 -6
  182. package/scripts/test/test-unit-guard.ts +21 -7
  183. package/scripts/time-runtime/time-batch-guard.ts +52 -16
  184. package/scripts/time-runtime/time-density-hook.ts +20 -2
  185. package/scripts/time-runtime/time-density.ts +4 -3
  186. package/scripts/time-runtime/time-markers.ts +15 -4
  187. package/scripts/time-runtime/time-spans.ts +4 -0
  188. package/scripts/version/npm-registry.ts +11 -1
  189. package/templates/workflows/ci.yml +9 -1
  190. package/templates/workflows/github-release.yml +13 -25
  191. package/.claude/skills/workflow-commands/followup-reference.md +0 -118
  192. package/dist/hooks/chunk-QVLT52OV.js +0 -464
@@ -55,10 +55,23 @@ const CONTENT_READ_LABELS: ReadonlySet<string> = new Set(
55
55
  CONTENT_READ_COMMANDS.map((command) => time_shell.bash_label(command)),
56
56
  )
57
57
 
58
+ // **The searches carry no file text, so they are counted in turns rather than files**
59
+ // (joshuafolkken/kit#3139). The file count above never saw a `grep`, yet searches were the commonest
60
+ // exploration turn a lane took — 457 of them in one day, each re-reading the main line's whole context.
61
+ // What a search costs is the turn, so a turn is the unit: parallel searches in one turn count once.
62
+ const SEARCH_COMMANDS: ReadonlyArray<string> = ['fd', 'find', 'grep', 'rg']
63
+ const SEARCH_LABELS: ReadonlySet<string> = new Set(
64
+ SEARCH_COMMANDS.map((command) => time_shell.bash_label(command)),
65
+ )
66
+
67
+ // The unit the refusal sends the reading to (joshuafolkken/kit#3139): `.claude/agents/investigator.md`,
68
+ // the parent's model at a lowered effort. A consumer receives it through the plugin as `kit:investigator`.
69
+ const INVESTIGATOR_AGENT = 'investigator'
70
+
58
71
  // What the refusal tells the model. It has to name the count, the command and the return shape,
59
72
  // because the deny reason is the only text that reaches the model — a run that is refused and told
60
73
  // nothing reads it as a broken tool.
61
- const REASON = `⛔ investigation: ${String(delegation_policy.INVESTIGATION_FILE_THRESHOLD)} files read and not edited since the last delegated unit, so the reading from here goes to a unit of its own. Ask \`pnpm josh delegate investigation\`, then brief a unit with 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 — are never counted, so they are not what took the count here. 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".`
62
75
 
63
76
  interface ReadTally {
64
77
  // The files read and not since edited, resolved so the two ways a target reaches here compare.
@@ -74,14 +87,18 @@ interface ReadTally {
74
87
  // The first instant the window covers. A refusal recorded before it cannot be checked against a
75
88
  // delegation any more, because the delegation that would re-arm it has scrolled out.
76
89
  window_start_ms: number
90
+ // The search turns since the last delegation or successful write, and how many of them came after
91
+ // the instant asked about — the search-side `pending` / `pending_since` (joshuafolkken/kit#3139).
92
+ searches: number
93
+ searches_since: number
77
94
  // The files this run has successfully edited within the window, resolved (joshuafolkken/kit#1840).
78
- // 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
79
96
  // it is excluded from the count and from a call's projected growth rather than counted as fresh
80
97
  // investigation.
81
98
  edited: ReadonlySet<string>
82
99
  }
83
100
 
84
- // **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.**
85
102
  // #1426's rule is about reading to find out how the subject works; `CLAUDE.md`, a `prompts/` topic and
86
103
  // a skill file are read to find out what to *do*, they are read by nearly every run, and a unit cannot
87
104
  // be sent to read them on the main line's behalf. #1441's own measurement excluded 5 of them by hand
@@ -247,7 +264,7 @@ function is_failed_outcome(span: Span): boolean {
247
264
 
248
265
  // **Every file the run successfully wrote within the window, resolved** (joshuafolkken/kit#1840). It
249
266
  // is the one source of "this run edited that file", shared by the live tally and the replay: a read of
250
- // 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
251
268
  // came before the edit or after it. A failed edit wrote nothing and so exempts nothing, and a file
252
269
  // edited outside the window is unknown rather than assumed: the set is only ever as wide as the tail
253
270
  // the hook read, which is the bound joshuafolkken/kit#1764 already holds the accumulation to.
@@ -279,18 +296,65 @@ function add_all(
279
296
  // the edit's own span, so a read issued *after* the edit was added and stayed. `sed -i`, `MultiEdit`
280
297
  // and `NotebookEdit` need no special case any more — each is a write, so its target is in `edited` and
281
298
  // never enters the read count.
282
- function apply_span(pending: Map<string, number>, span: Span, edited: ReadonlySet<string>): void {
299
+ interface Accumulation {
300
+ // Read files, keyed by resolved path, valued by the instant each entered.
301
+ pending: Map<string, number>
302
+ // Search turns, keyed by message id so a turn's parallel searches count once, valued the same way.
303
+ searches: Map<string, number>
304
+ }
305
+
306
+ function is_read_only_search(span: Span): boolean {
307
+ return SEARCH_LABELS.has(span.label) && span.is_bundleable && !span.may_write
308
+ }
309
+
310
+ // **A span with no message id belongs to no known turn, so it is a turn of its own** — the reading every
311
+ // other turn counter gives `NO_MESSAGE_ID` (`time-round-trips.ts`, `time-bundles.ts`). Keyed by the
312
+ // id, every untagged search would collapse into one entry and the count would never reach its threshold.
313
+ function search_turn_key(searches: ReadonlyMap<string, number>, span: Span): string {
314
+ if (span.message_id !== time_spans.NO_MESSAGE_ID) return span.message_id
315
+
316
+ return `${time_spans.NO_MESSAGE_ID}#${String(searches.size)}`
317
+ }
318
+
319
+ // **A successful write ends the streak.** The measurement behind the threshold counted searches since
320
+ // the last edit or delegation: a run that has started writing is implementing, and the searches that
321
+ // follow locate the next edit rather than continue an investigation.
322
+ //
323
+ // **Every read-only search counts, whatever it names** (joshuafolkken/kit#3139). The file count can
324
+ // exempt the run's own instructions because a read names each file it reads; a search does not — the
325
+ // targets drop a bare directory, so `grep -rn foo CLAUDE.md scripts` looked like a search of
326
+ // `CLAUDE.md` alone and an exemption let the whole sweep through. `is_bundleable` without `may_write`
327
+ // is the read-only test `is_search_call` asks at the call, so the transcript never counts a search the
328
+ // guard could not have refused.
329
+ function apply_search(searches: Map<string, number>, span: Span): void {
330
+ if (span.is_writing) {
331
+ searches.clear()
332
+
333
+ return
334
+ }
335
+
336
+ const turn = search_turn_key(searches, span)
337
+
338
+ if (is_read_only_search(span) && !searches.has(turn)) searches.set(turn, span.ended_ms)
339
+ }
340
+
341
+ function apply_span(accumulation: Accumulation, span: Span, edited: ReadonlySet<string>): void {
283
342
  if (DELEGATION_TOOLS.has(span.label)) {
284
- pending.clear()
343
+ accumulation.pending.clear()
344
+ accumulation.searches.clear()
285
345
 
286
346
  return
287
347
  }
288
348
 
289
- if (!is_content_read(span.label) || is_failed_outcome(span)) return
349
+ if (is_failed_outcome(span)) return
350
+
351
+ apply_search(accumulation.searches, span)
352
+
353
+ if (!is_content_read(span.label)) return
290
354
 
291
355
  const fresh = subject_targets(span.targets).filter((target) => !edited.has(target))
292
356
 
293
- add_all(pending, fresh, span.ended_ms)
357
+ add_all(accumulation.pending, fresh, span.ended_ms)
294
358
  }
295
359
 
296
360
  function last_delegation_ms(spans: ReadonlyArray<Span>): number {
@@ -307,15 +371,17 @@ function pending_after(pending: ReadonlyMap<string, number>, since_ms: number):
307
371
  function tally_of(text: string, since_ms: number = hook_decision.NEVER_MS): ReadTally {
308
372
  const { spans, started_ms } = time_spans.parse_timeline(text)
309
373
  const edited = edited_files(spans)
310
- const pending = new Map<string, number>()
374
+ const accumulation: Accumulation = { pending: new Map(), searches: new Map() }
311
375
 
312
- for (const span of spans) apply_span(pending, span, edited)
376
+ for (const span of spans) apply_span(accumulation, span, edited)
313
377
 
314
378
  return {
315
- pending: [...pending.keys()],
316
- pending_since: pending_after(pending, since_ms),
379
+ pending: [...accumulation.pending.keys()],
380
+ pending_since: pending_after(accumulation.pending, since_ms),
317
381
  reset_ms: last_delegation_ms(spans),
318
382
  window_start_ms: started_ms,
383
+ searches: accumulation.searches.size,
384
+ searches_since: pending_after(accumulation.searches, since_ms),
319
385
  edited,
320
386
  }
321
387
  }
@@ -347,6 +413,11 @@ function is_at_threshold(pending_count: number): boolean {
347
413
  return pending_count + 1 >= delegation_policy.INVESTIGATION_FILE_THRESHOLD
348
414
  }
349
415
 
416
+ // The search turn that takes the count up to its threshold is the boundary, as above.
417
+ function is_at_search_threshold(search_count: number): boolean {
418
+ return search_count + 1 >= delegation_policy.INVESTIGATION_SEARCH_TURN_THRESHOLD
419
+ }
420
+
350
421
  // **One refusal per accumulation, re-armed by a delegation and by nothing else.** A run that reads on
351
422
  // regardless is not refused again, so a false positive — three files the run was about to edit — costs
352
423
  // one round trip rather than wedging the run. And a run that *does* delegate is refused again as soon
@@ -373,7 +444,7 @@ function is_at_threshold(pending_count: number): boolean {
373
444
  //
374
445
  // **A delegated unit is not re-armed here because it is not refused at all** (joshuafolkken/kit#1840).
375
446
  // The hook counts a unit's reading against the unit (joshuafolkken/kit#1424), and the refusal asks for
376
- // 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`,
377
448
  // `Plan`) has no `Agent` tool to dispatch with. It was formerly degraded to a single first refusal,
378
449
  // which still asked for an action the unit could not take; now the unit short circuit in `should_block`
379
450
  // turns it off entirely, so this function is the parent's alone and needs no delegation condition.
@@ -386,7 +457,7 @@ function is_rearmed(tally: ReadTally, refused_at_ms: number): boolean {
386
457
  return true
387
458
  }
388
459
 
389
- return is_at_threshold(tally.pending_since)
460
+ return is_at_threshold(tally.pending_since) || is_at_search_threshold(tally.searches_since)
390
461
  }
391
462
 
392
463
  // A transcript under a session's `subagents/` directory is a delegated unit's. The segment is
@@ -403,10 +474,19 @@ function is_unit_run(run: GuardRun | undefined): boolean {
403
474
  return run !== undefined && is_unit_transcript(run.transcript)
404
475
  }
405
476
 
406
- function is_refusable_command(call: GuardedCall): boolean {
477
+ function is_read_only_command(call: GuardedCall, labels: ReadonlySet<string>): boolean {
478
+ if (call.name !== cost_blocks.BASH_TOOL) return false
479
+
407
480
  const label = time_shell.bash_label(time_shell.bash_command(call.input))
408
481
 
409
- return CONTENT_READ_LABELS.has(label) && time_batch_guard.is_read_only_call(call)
482
+ return labels.has(label) && time_batch_guard.is_read_only_call(call)
483
+ }
484
+
485
+ // **A search is refusable without a subject target**, unlike a read: `grep -rn foo scripts` names a
486
+ // bare directory, which `call_facts` deliberately does not take as a target, and such a sweep is exactly
487
+ // the search this count exists for.
488
+ function is_search_call(call: GuardedCall): boolean {
489
+ return is_read_only_command(call, SEARCH_LABELS)
410
490
  }
411
491
 
412
492
  // **A `Read` is always safe to refuse; a `Bash` line is refused only where it writes nothing.** This
@@ -427,10 +507,20 @@ function is_refusable_command(call: GuardedCall): boolean {
427
507
  // rule being enforced — and a call whose targets could not be read at all, which cannot move the count
428
508
  // and so has nothing to be refused for.
429
509
  function is_refusable_call(call: GuardedCall): boolean {
510
+ if (is_search_call(call)) return true
430
511
  if (call_targets(call).length === 0) return false
431
512
  if (READ_TOOLS.has(call.name)) return true
432
513
 
433
- return call.name === cost_blocks.BASH_TOOL && is_refusable_command(call)
514
+ return is_read_only_command(call, CONTENT_READ_LABELS)
515
+ }
516
+
517
+ // A search call grows the search count by its own turn; a read grows the file count only by a file the
518
+ // count does not already hold.
519
+ function is_growing_past_threshold(tally: ReadTally, call: GuardedCall): boolean {
520
+ if (is_search_call(call)) return is_at_search_threshold(tally.searches)
521
+ if (!is_at_threshold(tally.pending.length)) return false
522
+
523
+ return projected_count(tally.pending, call, tally.edited) > tally.pending.length
434
524
  }
435
525
 
436
526
  function should_block(
@@ -446,15 +536,11 @@ function should_block(
446
536
 
447
537
  const tally = tally_of(text, refused_at_ms)
448
538
 
449
- if (!is_at_threshold(tally.pending.length)) return false
450
-
451
- return (
452
- projected_count(tally.pending, call, tally.edited) > tally.pending.length &&
453
- is_rearmed(tally, refused_at_ms)
454
- )
539
+ return is_growing_past_threshold(tally, call) && is_rearmed(tally, refused_at_ms)
455
540
  }
456
541
 
457
542
  const investigation_reads = {
543
+ INVESTIGATOR_AGENT,
458
544
  READ_TOOLS,
459
545
  REASON,
460
546
  is_at_threshold,
@@ -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, {
@@ -0,0 +1,74 @@
1
+ import { mkdtempSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { PLATFORM_TEMP_ROOT } from '#scripts/josh/platform-temporary'
4
+ import { review_diff_parts } from '#scripts/review/review-diff-parts'
5
+ import { bash_output_cap_reader } from './bash-output-cap'
6
+
7
+ // Prints a command's output whole when it fits under the Bash output cap, and as part files when it
8
+ // does not (joshuafolkken/kit#3143). Past `BASH_MAX_OUTPUT_LENGTH` Claude Code saves the output and
9
+ // hands back a 2 KB preview, and the run spends a turn reading the saved file — 224 overflows and 132
10
+ // re-reads over one day's sessions, `doc:section` and `issue:read` the two most frequent `josh` sources.
11
+ //
12
+ // **The cut is #2963's, not a second one.** The parts come from `review_diff_parts.part_texts`, so each
13
+ // file — header included — fits under the cap and the bodies concatenate back to exactly the output.
14
+ // A reader opens them all with the Read tool in one turn, which is the turn the preview used to cost.
15
+ //
16
+ // **A fresh directory per call**, unlike the review's per-checkout one: a run fetches several sections
17
+ // in one turn, and a shared directory replaced on every call would delete one call's parts while
18
+ // another call's listing still names them.
19
+
20
+ const DIRECTORY_PREFIX = 'josh-capped-print-'
21
+ const FILE_MODE = 0o600
22
+ const PART_SUFFIX = '.md'
23
+ const LINE_BREAK = '\n'
24
+ // What else shares the Bash result with this output: the launcher line, a script banner, a few
25
+ // stderr lines. Measured at about 80 characters for `pnpm josh doc:section` in kit, so 512 holds the
26
+ // longest heading or issue list with room for a consumer's banner.
27
+ const HEADROOM_CHARS = 512
28
+
29
+ function write_parts(label: string, text: string, cap: number): Array<string> {
30
+ const directory = mkdtempSync(path.join(PLATFORM_TEMP_ROOT, DIRECTORY_PREFIX))
31
+
32
+ return review_diff_parts.part_texts(label, text, cap).map((part, index) => {
33
+ const file = path.join(directory, `part-${String(index + 1)}${PART_SUFFIX}`)
34
+
35
+ writeFileSync(file, part, { flag: 'wx', mode: FILE_MODE })
36
+
37
+ return file
38
+ })
39
+ }
40
+
41
+ function part_count(count: number): string {
42
+ return `written as ${String(count)} part file${count === 1 ? '' : 's'} under it`
43
+ }
44
+
45
+ function listing(text: string, cap: number, files: ReadonlyArray<string>): string {
46
+ const figures = `${text.length.toLocaleString('en-US')} > ${cap.toLocaleString('en-US')} characters`
47
+ const lead = `Over the Bash cap (${figures}) — ${part_count(files.length)}, together exactly the output. Read every part with the Read tool, in one turn, not cat:`
48
+
49
+ return [lead, ...files.map((file) => ` ${file}`)].join(LINE_BREAK)
50
+ }
51
+
52
+ // **The budget is the cap less a headroom, not the cap.** The Bash result carries more than this
53
+ // output — `pnpm josh`'s own launcher line, a consumer's script banner, the stderr lines naming a
54
+ // number that did not resolve — so a text measured against the bare cap would overflow by exactly
55
+ // those lines, and cost the re-read this module exists to remove.
56
+ function output_budget(root: string): number {
57
+ return Math.max(1, bash_output_cap_reader.bash_output_cap(root) - HEADROOM_CHARS)
58
+ }
59
+
60
+ function capped_print(text: string, label: string, root: string = process.cwd()): void {
61
+ const budget = output_budget(root)
62
+
63
+ if (text.length < budget) {
64
+ console.info(text)
65
+
66
+ return
67
+ }
68
+
69
+ console.info(listing(text, budget, write_parts(label, text, budget)))
70
+ }
71
+
72
+ const capped_output = { HEADROOM_CHARS, capped_print }
73
+
74
+ export { capped_output }
@@ -65,15 +65,19 @@ function block_ceiling(size: number): number {
65
65
  // unreached file, so the definition cannot rot as documents move on or off the execution path.
66
66
  const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
67
67
  { path: '.claude/skills/dependency-update/SKILL.md', bytes: 8192 },
68
- { path: '.claude/skills/epic-commands/SKILL.md', bytes: 36_864 },
68
+ // joshuafolkken/kit#3173 split the waves and the `epic:bundle` detail into lazily read references.
69
+ { path: '.claude/skills/epic-commands/SKILL.md', bytes: 20_480 },
70
+ { path: '.claude/skills/epic-commands/epic-bundle.md', bytes: 12_288 },
71
+ { path: '.claude/skills/epic-commands/execution-waves.md', bytes: 12_288 },
69
72
  { path: '.claude/skills/verify-ui/SKILL.md', bytes: 8192 },
70
- { path: '.claude/skills/workflow-commands/followup-reference.md', bytes: 16_384 },
71
73
  { path: '.claude/skills/workflow-commands/fullrun-steps.md', bytes: 8192 },
72
74
  { path: '.claude/skills/workflow-commands/into-target.md', bytes: 4096 },
73
75
  { path: '.claude/skills/workflow-commands/issue-comments.md', bytes: 8192 },
74
76
  { path: '.claude/skills/workflow-commands/issue-fold-existing.md', bytes: 4096 },
75
77
  { path: '.claude/skills/workflow-commands/needs-human-review.md', bytes: 8192 },
76
- { path: '.claude/skills/workflow-commands/observation-filing.md', bytes: 24_576 },
78
+ { path: '.claude/skills/workflow-commands/observation-filing.md', bytes: 12_288 },
79
+ // joshuafolkken/kit#3176 moved the ledger out of `observation-filing.md`, read only when it appends.
80
+ { path: '.claude/skills/workflow-commands/observation-ledger.md', bytes: 12_288 },
77
81
  // `pre-gate-cut.md` left this per-document budget in joshuafolkken/kit#2289: it became a point-of-use
78
82
  // document, so its bytes are now held by every entry's total read (`entry-read-budget.ts`) and the
79
83
  // two budgets must not hold it twice — the reachability line moved it from `unreached` to
@@ -82,34 +86,44 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
82
86
  { path: '.claude/skills/workflow-commands/target-repository.md', bytes: 8192 },
83
87
  { path: '.claude/skills/workflow-commands/working-tree-hold.md', bytes: 8192 },
84
88
  // joshuafolkken/kit#2998 split the reference: the developer commands stay, the rest moved out.
85
- { path: 'docs/josh-commands-automation.md', bytes: 155_648 },
89
+ // joshuafolkken/kit#3175 raised it: the supervisor, provider-table and answer-to-budget prose moved
90
+ // here out of `backlogrun-steps.md`, so a run reads it only when it asks about those commands.
91
+ { path: 'docs/josh-commands-automation.md', bytes: 159_744 },
86
92
  { path: 'docs/josh-commands.md', bytes: 36_864 },
87
93
  { path: 'prompts/coding-standards.md', bytes: 8192 },
88
94
  { path: 'prompts/collaboration-workflow.md', bytes: 8192 },
89
95
  { path: 'prompts/collaboration-workflow/file-edits.md', bytes: 12_288 },
90
96
  { path: 'prompts/collaboration-workflow/gh-rest.md', bytes: 8192 },
91
97
  { path: 'prompts/collaboration-workflow/glossary.md', bytes: 4096 },
92
- { path: 'prompts/collaboration-workflow/issue-citation.md', bytes: 8192 },
93
- // #2662 adds the feature and compatibility declarations to the canonical filing procedure.
94
- { path: 'prompts/collaboration-workflow/issue-template.md', bytes: 28_672 },
95
- { path: 'prompts/collaboration-workflow/operating-rules.md', bytes: 16_384 },
96
- { path: 'prompts/collaboration-workflow/output-bounds.md', bytes: 16_384 },
97
- { path: 'prompts/collaboration-workflow/overview.md', bytes: 8192 },
98
- { path: 'prompts/collaboration-workflow/plan-comment.md', bytes: 20_480 },
98
+ { path: 'prompts/collaboration-workflow/issue-citation.md', bytes: 4096 },
99
+ // #3178 left the template and the judgement tables; the epic commands moved to `epic-commands`, the by-hand fallback stayed.
100
+ { path: 'prompts/collaboration-workflow/issue-template.md', bytes: 8192 },
101
+ { path: 'prompts/collaboration-workflow/operating-rules.md', bytes: 12_288 },
102
+ // #3177 moved the rationale to `docs/maintainers/output-bounds-rationale.md`.
103
+ { path: 'prompts/collaboration-workflow/output-bounds.md', bytes: 4096 },
104
+ { path: 'prompts/collaboration-workflow/overview.md', bytes: 4096 },
105
+ // #3178 replaced the review-chain restatement with a pointer to `chain-rule.md`.
106
+ { path: 'prompts/collaboration-workflow/plan-comment.md', bytes: 8192 },
99
107
  { path: 'prompts/collaboration-workflow/principles.md', bytes: 12_288 },
100
108
  { path: 'prompts/collaboration-workflow/proposal-request.md', bytes: 4096 },
101
- { path: 'prompts/collaboration-workflow/report-format.md', bytes: 28_672 },
102
- { path: 'prompts/collaboration-workflow/residency.md', bytes: 28_672 },
103
- { path: 'prompts/collaboration-workflow/rule-delivery.md', bytes: 45_056 },
104
- { path: 'prompts/collaboration-workflow/shell-body.md', bytes: 12_288 },
105
- { path: 'prompts/collaboration-workflow/turn-batching.md', bytes: 20_480 },
109
+ // #3178 dropped the restatements `report:lint` and `chain-rule.md` already carry.
110
+ { path: 'prompts/collaboration-workflow/report-format.md', bytes: 16_384 },
111
+ // #3177 cut it to the four questions; the history and lists moved to `docs/maintainers/residency-rationale.md`.
112
+ { path: 'prompts/collaboration-workflow/residency.md', bytes: 8192 },
113
+ // #3186 moved the hook wiring and history to `docs/maintainers/rule-delivery-rationale.md`;
114
+ // #3179 adds the `index-mutation` / `destructive-command` / `protected-file` entries.
115
+ { path: 'prompts/collaboration-workflow/rule-delivery.md', bytes: 20_480 },
116
+ { path: 'prompts/collaboration-workflow/shell-body.md', bytes: 8192 },
117
+ // #3177 moved the measurements and rejected mechanisms to `docs/maintainers/turn-batching-rationale.md`.
118
+ { path: 'prompts/collaboration-workflow/turn-batching.md', bytes: 8192 },
106
119
  { path: 'prompts/collaboration-workflow/upstream-interrupt.md', bytes: 12_288 },
107
- { path: 'prompts/collaboration-workflow/wip-cap.md', bytes: 16_384 },
120
+ { path: 'prompts/collaboration-workflow/wip-cap.md', bytes: 12_288 },
108
121
  { path: 'prompts/refactoring.md', bytes: 8192 },
109
- { path: 'prompts/review-rubric.md', bytes: 20_480 },
110
- { path: 'prompts/review.md', bytes: 20_480 },
122
+ // #3180 moved the history and the lint-settled proofs to `docs/maintainers/`.
123
+ { path: 'prompts/review-rubric.md', bytes: 16_384 },
124
+ { path: 'prompts/review.md', bytes: 16_384 },
111
125
  { path: 'prompts/sonar-hotspot-handling.md', bytes: 8192 },
112
- { path: 'prompts/testing-guide.md', bytes: 24_576 },
126
+ { path: 'prompts/testing-guide.md', bytes: 20_480 },
113
127
  ]
114
128
 
115
129
  // The recorded ceiling for one path, or undefined when it carries no budget entry.
@@ -2,6 +2,7 @@
2
2
  import { existsSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
+ import { capped_output } from './capped-print'
5
6
  import { document_section } from './document-section'
6
7
  import { entry_read_set } from './entry-read-set'
7
8
 
@@ -56,12 +57,12 @@ function report_missing(markdown: string, heading: string): number {
56
57
  return FAILURE_EXIT_CODE
57
58
  }
58
59
 
59
- function print_section(markdown: string, heading: string): number {
60
+ function print_section(markdown: string, heading: string, root: string): number {
60
61
  const found = document_section.section(markdown, heading)
61
62
 
62
63
  if (found === undefined) return report_missing(markdown, heading)
63
64
 
64
- console.info(found.text)
65
+ capped_output.capped_print(found.text, heading, root)
65
66
 
66
67
  return 0
67
68
  }
@@ -87,7 +88,7 @@ function run(argv: ReadonlyArray<string>, root: string = process.cwd()): number
87
88
 
88
89
  const markdown = read_named(name, root)
89
90
 
90
- return markdown === undefined ? FAILURE_EXIT_CODE : print_section(markdown, heading)
91
+ return markdown === undefined ? FAILURE_EXIT_CODE : print_section(markdown, heading, root)
91
92
  }
92
93
 
93
94
  function main(argv: ReadonlyArray<string>): void {