@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
@@ -57,7 +57,7 @@ const ENTRY_READ_BUDGET: ReadonlyArray<EntryBudget> = [
57
57
  // block in joshuafolkken/kit#2317 when `backlogrun-child.md`'s outage section recorded the
58
58
  // session-resume re-dispatch and the outage-fold window.
59
59
  // Raised in joshuafolkken/kit#2328 when `retrospective.md` joined the point-of-use set and `SKILL.md`
60
- // grew by §2j: every entry that reads `SKILL.md` discovers the retrospective document, so its ~5KB
60
+ // grew by `retrospective.md`: every entry that reads `SKILL.md` discovers the retrospective document, so its ~5KB
61
61
  // and the new section crossed a block for each. The lane child is exempt — it never runs the
62
62
  // retrospective (`run:step` answers `stop` for a child at the stop position), so it skips both.
63
63
  // joshuafolkken/kit#2335 recorded the drain-time firing with the operational detail in
@@ -70,8 +70,8 @@ const ENTRY_READ_BUDGET: ReadonlyArray<EntryBudget> = [
70
70
  // list, so its ~48KB read crossed a block downward and the recorded ceiling drops to the block multiple
71
71
  // the stale-ratchet message named. The other entries still read it, so their rows hold.
72
72
  // joshuafolkken/kit#2345 added the `implementation-unit` delegation row (full fan-out procedure in
73
- // `docs/josh-commands-automation.md` → "`josh fanout`"); `SKILL.md` §2b gained only a one-clause mention and a
74
- // pointer, folded into the existing enumeration and offset by tightening §2b prose, so every entry
73
+ // `docs/josh-commands-automation.md` → "`josh fanout`"); `delegation.md` gained only a one-clause mention and a
74
+ // pointer, folded into the existing enumeration and offset by tightening `delegation.md` prose, so every entry
75
75
  // stayed within its block (kickoff has no block of headroom — its pre-#2294 ceiling is the next
76
76
  // multiple, which the downward ratchet holds shut).
77
77
  // joshuafolkken/kit#2353 wired the watcher-guard note into `backlogrun-progress.md` (read by the three
@@ -92,12 +92,24 @@ const ENTRY_READ_BUDGET: ReadonlyArray<EntryBudget> = [
92
92
  // `backlogrun-*` phase documents it never opens. Each total now counts only what its path reaches.
93
93
  // joshuafolkken/kit#3077 trimmed non-resident rule bodies from `CLAUDE.md`, lowering fullrun, prrun
94
94
  // and backlogrun one block each.
95
- { entry: 'kickoff', bytes: 65_536 },
96
- { entry: 'fullrun', bytes: 126_976 },
97
- { entry: 'halfrun', bytes: 106_496 },
98
- { entry: 'prrun', bytes: 131_072 },
99
- { entry: 'backlogrun', bytes: 212_992 },
100
- { entry: LANE_CHILD, bytes: 77_824 },
95
+ // Lowered in joshuafolkken/kit#3172: the heartbeat left `backlogrun-progress.md` for its own
96
+ // `progress-watcher.md`, and `pre-gate-cut.md` became the lane child's alone, so fullrun, halfrun
97
+ // and prrun stop paying for ~50KB of batch and lane procedure; backlogrun lost the moved prose.
98
+ // Lowered in joshuafolkken/kit#3174: `SKILL.md` was cut to a manifest (~21KB → ~11KB) and the entry
99
+ // sequence `fullrun`, `halfrun` and `prrun` shared moved into one `entry-sequence.md`.
100
+ // Lowered in joshuafolkken/kit#3175: the loop became the driver's hand-backs, the supervisor and
101
+ // provider prose moved to `docs/josh-commands-automation.md`, and the failure-only sections moved to
102
+ // `backlogrun-recovery.md`, which the parent's reach still charges whole.
103
+ // joshuafolkken/kit#3171 cut `CLAUDE.md` to triggers and pointers (~12 KB), lowering every entry.
104
+ { entry: 'kickoff', bytes: 45_056 },
105
+ // Raised in joshuafolkken/kit#3177: #3173, #3181 and #3183 merged in parallel and together tipped
106
+ // fullrun 12 bytes over its block; none of #3177's documents are in its read set.
107
+ // Lowered back in joshuafolkken/kit#3180: the history and lint-settled proofs left its read set.
108
+ { entry: 'fullrun', bytes: 57_344 },
109
+ { entry: 'halfrun', bytes: 57_344 },
110
+ { entry: 'prrun', bytes: 61_440 },
111
+ { entry: 'backlogrun', bytes: 176_128 },
112
+ { entry: LANE_CHILD, bytes: 57_344 },
101
113
  ]
102
114
 
103
115
  function byte_size(root: string, relative_path: string): number {
@@ -19,19 +19,12 @@ const LANE_CHILD = 'lane-child'
19
19
  const FULLRUN = 'fullrun'
20
20
 
21
21
  // **The `SKILL.md` sections a dispatched lane child never uses**, read at the section level rather
22
- // than in full. A child is handed one issue to run, so it never files a new one with an `into`
23
- // target (§2a), never delegates its own pre-implementation reading (§2b), never names a repository or
24
- // scouts for a duplicate at its entry (§2c, §2e), returns an observation to the parent rather than
25
- // filing it (§2i), and never edits these documents (§3). Its dispatch is already an explicit workflow
26
- // invocation, so §0 cannot decide anything for it either. Review delegation remains in chain-rule.md.
22
+ // than in full. A child never edits these documents (§3), and its dispatch is already an explicit
23
+ // workflow invocation, so §0 cannot decide anything for it either. The other rules it never uses — the
24
+ // `into` target, delegation, the repository prefix, the scout, observation filing, the retrospective —
25
+ // are single rows of §2's table since joshuafolkken/kit#3174, so no section of them is left to trim.
27
26
  const UNUSED_SKILL_SECTIONS: ReadonlyArray<string> = [
28
27
  '0. The rule that fires before any of them — explicit invocation',
29
- '2a. The `into <target>` suffix — where the new Issue lands',
30
- '2b. Delegating a step to a cheaper tier',
31
- '2c. The `owner/repo#` prefix — which repository the run acts on',
32
- '2e. Before filing a new Issue — `pnpm josh issue:scout`',
33
- '2i. An observation worth filing is filed without asking',
34
- '2j. The end-of-run retrospective — read when `run:step` prints it',
35
28
  '3. What stays resident, and what is read from here',
36
29
  ]
37
30
 
@@ -49,10 +42,12 @@ const UNUSED_SKILL_SECTIONS: ReadonlyArray<string> = [
49
42
  // list — what one invocation approves, the named-issue order, the session-cut record, the loop and the
50
43
  // once-per-session tail — none of which a leaf child performs. Every reference to it lives in a
51
44
  // document the child never reads (`backlogrun.md`, `backlogrun-progress.md`, `retrospective.md`) or in a
52
- // `SKILL.md` section the child trims (§0's session-cut note, §2b, §2i, §2j), so the child has no path
45
+ // `SKILL.md` section the child trims (§0's session-cut note), so the child has no path
53
46
  // that opens it. It was the child's single largest read — ~16,000 tokens read whole — charged for a
54
47
  // document it never reaches, so dropping it is a correction of an over-count, not a loss of any rule the
55
48
  // child needs. The parent still reads it in full.
49
+ // **`progress-watcher.md` is the parent's too** (joshuafolkken/kit#3172): a dispatched child is refused
50
+ // a watcher by its `JOSH_LANE_CHILD` mark, so it never opens the watcher's document.
56
51
  const SKIPPED_POINT_OF_USE: ReadonlySet<string> = new Set([
57
52
  'backlogrun-child.md',
58
53
  'backlogrun-lanes.md',
@@ -61,13 +56,16 @@ const SKIPPED_POINT_OF_USE: ReadonlySet<string> = new Set([
61
56
  'delegation.md',
62
57
  'issue-scout.md',
63
58
  'latest-gate.md',
59
+ 'progress-watcher.md',
64
60
  'retrospective.md',
65
61
  ])
66
62
 
67
63
  // **A child that parks reads `backlogrun-park.md`** (`SKILL.md` §1, "A lane child that parks"), a
68
64
  // document a standalone `fullrun`'s path never names — so the child's reach adds it to its base
69
- // entry's (joshuafolkken/kit#3078).
70
- const REACHED_POINT_OF_USE: ReadonlySet<string> = new Set(['backlogrun-park.md'])
65
+ // entry's (joshuafolkken/kit#3078). **`pre-gate-cut.md` is the child's alone** (joshuafolkken/kit#3172):
66
+ // only a lane child resumes or takes the pre-gate cut, so `fullrun.md` stopped naming it and the
67
+ // child's reach carries it instead.
68
+ const REACHED_POINT_OF_USE: ReadonlySet<string> = new Set(['backlogrun-park.md', 'pre-gate-cut.md'])
71
69
 
72
70
  function costed(root: string): ReadSetCost {
73
71
  return read_set_trim.costed(root, {
@@ -48,15 +48,15 @@ const POINT_OF_USE_LABEL = '-- read at the point of use, not at the entry --'
48
48
  const LANE_CHILD_NOTE: ReadonlyArray<string> = [
49
49
  ' a dispatched lane child (`JOSH_LANE_CHILD`) reads this trimmed set:',
50
50
  ' - skips the point-of-use documents the parent owns — child dispatch, lane opening, the progress watcher and the hand-off',
51
- ' - omits SKILL.md §0/§2a/§2b/§2c/§2e/§2i/§3 — dispatch already authorized the run and a leaf child never uses them',
51
+ ' - omits SKILL.md §0/§3 — dispatch already authorized the run and a leaf child edits no rule',
52
52
  ]
53
53
  // **The `backlogrun` parent reads a trimmed set of its own** — a scheduler that never implements, so
54
54
  // the implementer-only `SKILL.md` sections are read section by section rather than whole
55
- // (joshuafolkken/kit#2256). A *different* trim from the lane child's: the parent keeps §0/§2b/§2c/§2e/
56
- // §2i, which are the scheduler's own, and drops no point-of-use document.
55
+ // (joshuafolkken/kit#2256). A *different* trim from the lane child's: the parent keeps §0, which is the
56
+ // scheduler's own, and drops no point-of-use document.
57
57
  const BACKLOGRUN_PARENT_NOTE: ReadonlyArray<string> = [
58
58
  ' the backlogrun parent (scheduler) reads this trimmed set:',
59
- ' - omits SKILL.md §2a/§2f/§2g/§3 — the parent never implements, so it claims no tree and files no `into` target',
59
+ ' - omits SKILL.md §3 — the parent edits no rule mid-run',
60
60
  ' - keeps every point-of-use document — the parent is the one dispatching children and running lanes',
61
61
  ]
62
62
  const FETCH_RULE =
@@ -15,7 +15,7 @@
15
15
  // is joshuafolkken/kit#1856's addition**: it governs the `/code-review` → `followup` chain, which in
16
16
  // `fullrun` / `queue` / `epicrun` / `backlogrun` runs *after* the first edit, so its 7,396 tokens
17
17
  // were resident from the entry for no run that had yet reached a review.
18
- // **`background-commands.md` is joshuafolkken/kit#1873's**: it was §2h's resident body, governing the
18
+ // **`background-commands.md` is joshuafolkken/kit#1873's**: it was `background-commands.md`'s resident body, governing the
19
19
  // background execution of the gate, the push and the merge tail — all after the first edit — so it
20
20
  // left `SKILL.md` for the point-of-use list. `SKILL.md` → §1, "Four documents are read at the point
21
21
  // of use", is the single source.
@@ -32,18 +32,26 @@
32
32
  // cut, a step every implementing run — and every dispatched lane child — reaches after the entry, so
33
33
  // its read is a point-of-use read the count was silently omitting. Measured on the backlogrun of
34
34
  // 2026-09-21, the lane children read it 21 times across 13 runs — the largest single document read,
35
- // and the one the point-of-use list had no row for.
35
+ // and the one the point-of-use list had no row for. Only a lane child reaches it (joshuafolkken/kit#3172):
36
+ // `fullrun.md` no longer names it, so it is charged to the lane child's role alone.
37
+ // **`progress-watcher.md` joins them** (joshuafolkken/kit#3172): the heartbeat every implementing run
38
+ // starts once its hold is claimed, split out of `backlogrun-progress.md` so a single-issue run stops
39
+ // paying for the batch's hand-off and waiting procedure to read it.
36
40
  const POINT_OF_USE_FILES: ReadonlySet<string> = new Set([
37
41
  'latest-gate.md',
38
42
  'followup.md',
39
43
  'chain-rule.md',
40
44
  'background-commands.md',
41
45
  'pre-gate-cut.md',
46
+ 'progress-watcher.md',
42
47
  'backlogrun-child.md',
43
48
  'backlogrun-lanes.md',
44
49
  'backlogrun-progress.md',
45
50
  'backlogrun-park.md',
46
51
  'backlogrun-steps.md',
52
+ // Read only when a delegated unit goes silent or a lane's pull request conflicts with `main` — a
53
+ // failure path, so a run in which nothing fails never reads it (joshuafolkken/kit#3175).
54
+ 'backlogrun-recovery.md',
47
55
  // Read only when `run:step` prints `pnpm josh retrospective` at a run's stop position — the very
48
56
  // end of a run, never the entry (joshuafolkken/kit#2328). Kept out of every entry's read for the
49
57
  // same reason the phase documents above are: no run that never drains its backlog reaches it.
@@ -0,0 +1,47 @@
1
+ import { document_reachability } from './document-reachability'
2
+
3
+ // The byte budget of the always-resident document — `CLAUDE.md`, loaded on every request. It is held
4
+ // here rather than in `document-byte-budget.ts` because a resident document is covered by every entry
5
+ // total and must not carry a per-document block entry as well (joshuafolkken/kit#2257); it is held here
6
+ // rather than inside `workflow-skills.test.ts` so `josh bytes CLAUDE.md` counts against the same number
7
+ // the suite enforces (joshuafolkken/kit#3171). Loosening any of the three constants is Tier C —
8
+ // `prompts/collaboration-workflow/residency.md`.
9
+
10
+ // The documents sat at ~83 KB each before the joshuafolkken/kit#854 split, crept back to ~56 KB, and
11
+ // were cut below 30 KB by joshuafolkken/kit#1924. joshuafolkken/kit#3171 cut `CLAUDE.md` to ~14 KB —
12
+ // triggers and pointers only — and lowered this ceiling with it, so the document cannot creep back. It
13
+ // is a guard against re-inlining, not a budget to tune prose against.
14
+ const RESIDENT_CEILING_BYTES = 18_000
15
+
16
+ // joshuafolkken/kit#951: the ceiling alone stops the wrong thing. Reached, it does not block the
17
+ // next rule — it makes that rule pay for itself by deleting a neighboring sentence, and the
18
+ // sentence chosen is whichever one no marker pinned rather than whichever one matters least. A
19
+ // required margin turns "at the limit" into a failure while there is still room to write the fix,
20
+ // which is the only point at which moving a procedure into a skill is still a choice.
21
+ const RESIDENT_HEADROOM_BYTES = 2000
22
+
23
+ // joshuafolkken/kit#1275: a floor under the room left once the procedures are out, so a wholesale
24
+ // re-inlining fails by name instead of silently returning the document to a few bytes of headroom.
25
+ const RE_INLINE_GUARD_HEADROOM_BYTES = 1000
26
+
27
+ const EFFECTIVE_CEILING_BYTES = RESIDENT_CEILING_BYTES - RESIDENT_HEADROOM_BYTES
28
+
29
+ // The size a resident document must stay below — the effective ceiling minus the re-inline floor. The
30
+ // suite fails at it, and `josh bytes` counts the headroom against it.
31
+ const RESIDENT_LIMIT_BYTES = EFFECTIVE_CEILING_BYTES - RE_INLINE_GUARD_HEADROOM_BYTES
32
+
33
+ // The limit for a resident document, or undefined for any other path.
34
+ function limit_bytes_for(relative_path: string): number | undefined {
35
+ return document_reachability.RESIDENT_BASE.includes(relative_path)
36
+ ? RESIDENT_LIMIT_BYTES
37
+ : undefined
38
+ }
39
+
40
+ const resident_budget = {
41
+ EFFECTIVE_CEILING_BYTES,
42
+ RE_INLINE_GUARD_HEADROOM_BYTES,
43
+ RESIDENT_LIMIT_BYTES,
44
+ limit_bytes_for,
45
+ }
46
+
47
+ export { resident_budget }
@@ -5,7 +5,7 @@ import { to_issue_reference } from './epic-reference'
5
5
  // Whether an issue is an epic this repository's writers may edit, and why it is not when it is not.
6
6
  //
7
7
  // Both `josh epic --add` and `josh epic --remove` ask exactly this before they compute anything: the
8
- // label, the absence of a cross-repository child, a task list to place rows in, and a declaration
8
+ // label, a task list to place rows in, and a declaration
9
9
  // their edit can be relative to. It lives in one module rather than in each of them, because the two
10
10
  // commands must refuse the same inputs for the same reasons — a second copy is where one comes to
11
11
  // accept an epic the other refuses (joshuafolkken/kit#1712).
@@ -40,18 +40,15 @@ function promote_remedy(epic_number: number): string {
40
40
  // The label and the task list are checked separately from the declaration because they fail
41
41
  // differently: without rows there is nowhere to put a new one, and without a declaration there is
42
42
  // nothing for an edit to be relative to.
43
- function find_epic_shape_error(subject: EpicShapeSubject, body: string): string | undefined {
44
- const reference = to_issue_reference(subject.epic_number)
45
-
46
- if (!subject.labels.includes(EPIC_LABEL)) {
47
- return `${reference} does not carry the \`${EPIC_LABEL}\` label, so it is not an epic. ${promote_remedy(subject.epic_number)}`
48
- }
49
-
50
- if (epic_parse.has_external_task_list_entry(body)) {
51
- return `${reference} tracks a child in another repository; editing the declared order of a cross-repository epic is joshuafolkken/kit#864's scope, not this command's.`
52
- }
43
+ //
44
+ // **A child in another repository is not a refusal** (joshuafolkken/kit#3161). The declaration names
45
+ // bare numbers only, so neither command can write an order across repositories — such an order lives
46
+ // in the native relations alone (`epic_graph.child_links`), which these edits never read or drop — and
47
+ // the rewrite leaves a row it does not recognize exactly where it stood.
48
+ function find_epic_shape_error(subject: EpicShapeSubject): string | undefined {
49
+ if (subject.labels.includes(EPIC_LABEL)) return undefined
53
50
 
54
- return undefined
51
+ return `${to_issue_reference(subject.epic_number)} does not carry the \`${EPIC_LABEL}\` label, so it is not an epic. ${promote_remedy(subject.epic_number)}`
55
52
  }
56
53
 
57
54
  function find_subject_error(
@@ -60,7 +57,7 @@ function find_subject_error(
60
57
  ): string | undefined {
61
58
  const reference = to_issue_reference(subject.epic_number)
62
59
  if (subject.body === undefined) return `Could not read the body of ${reference}.`
63
- const shape_error = find_epic_shape_error(subject, subject.body)
60
+ const shape_error = find_epic_shape_error(subject)
64
61
  if (shape_error !== undefined) return shape_error
65
62
 
66
63
  if (tracked.length === 0) {
@@ -6,7 +6,7 @@ import { epic_report, type EpicNextResult } from './epic-report'
6
6
 
7
7
  // The `run:solo` gate on what a run is offered (joshuafolkken/kit#2776).
8
8
  //
9
- // `wip-cap.md`'s run-alone section says an issue whose defect reaches the verification path runs
9
+ // `backlogrun-lanes.md`'s run-alone rule says an issue whose defect reaches the verification path runs
10
10
  // alone, and until this gate that was a judgement made at dispatch — the offer handed such an issue
11
11
  // out beside others and `backlog:drive` launched every number it was given. The label records the
12
12
  // judgement once; this applies it to every ask, so it holds without an agent in the loop.
@@ -9,7 +9,11 @@ const CHECK = 'exports'
9
9
  const FAIL_EXIT_CODE = 1
10
10
  // A consumer's namespaces are read from `.svelte` files and routes a TypeScript program over its
11
11
  // `tsconfig.json` does not see, so every member they use would read as unused. The check is kit's own.
12
- const CONSUMER_SKIP_REASON = 'checks only the kit repository itself, not a consumer project'
12
+ //
13
+ // **Not a skip notice** (joshuafolkken/kit#3162). `SKIP_MARKER` means "a check passed without running",
14
+ // and the gate withholds its green record on it — so a consumer, where this notice prints on every
15
+ // run, never got a green record. Out of scope by design is not unverified, so the marker stays out.
16
+ const CONSUMER_NOTICE = `josh ${CHECK}: checks only the kit repository itself — nothing to check in a consumer project.`
13
17
 
14
18
  function format_member(root: string, member: NamespaceMember): string {
15
19
  return `${path.relative(root, member.file)}:${String(member.line)} ${member.namespace}.${member.member}`
@@ -26,7 +30,7 @@ function report(root: string, members: ReadonlyArray<NamespaceMember>): string {
26
30
 
27
31
  async function run_unused_members(root: string): Promise<number> {
28
32
  if (doctor_consumer.is_kit_consumer(root)) {
29
- console.info(project_checks.skip_notice(CHECK, CONSUMER_SKIP_REASON))
33
+ console.info(CONSUMER_NOTICE)
30
34
 
31
35
  return 0
32
36
  }
@@ -48,6 +52,6 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
48
52
  process.exitCode = await run_unused_members(project_checks.project_root(process.cwd()))
49
53
  }
50
54
 
51
- const unused_members_cli = { run_unused_members }
55
+ const unused_members_cli = { run_unused_members, CONSUMER_NOTICE }
52
56
 
53
57
  export { unused_members_cli }
@@ -2,6 +2,7 @@ import { file_map_stamp, type FileMapStampAccess } from '#scripts/josh/file-map-
2
2
  import { hook_decision } from '#scripts/josh/hook-decision'
3
3
  import { review_brief } from '#scripts/review/review-brief'
4
4
  import { review_stamps } from '#scripts/review/review-stamps'
5
+ import { test_unit_guard } from '#scripts/test/test-unit-guard'
5
6
  import { gate_tree, type GateTree } from './gate-tree'
6
7
 
7
8
  // Whether the two scoped checks have been green on *this exact tree* by the time the review is
@@ -60,26 +61,36 @@ interface ScopedCheck {
60
61
 
61
62
  // The record paths, overridable so a test can plant one without overwriting the record a real run in
62
63
  // this checkout relies on — the same reason `gate_skip.reusable_green_gate` takes a `source`.
64
+ // `is_unit_skipped` is overridable for the same reason, so a suite running in a checkout with vitest
65
+ // can stand in for a project without one.
63
66
  interface ScopedSources {
64
67
  lint?: string
65
68
  test?: string
69
+ is_unit_skipped?: boolean
66
70
  }
67
71
 
72
+ // **`test:related` is required only where it can ever answer** (joshuafolkken/kit#3136). In a project
73
+ // whose unit guard skips — vitest absent, or a basic project with no test file — `test:related` runs
74
+ // nothing and so writes no record, and requiring one refused the gate forever with no way through.
75
+ // The guard's own decision is asked rather than re-derived, so the writer and this reader agree; a
76
+ // project with a unit suite still needs the record, and a run that executed nothing still writes none.
68
77
  function checks_of(sources: ScopedSources): ReadonlyArray<ScopedCheck> {
69
- return [
70
- {
71
- script: LINT_SCRIPT,
72
- command: LINT_COMMAND,
73
- stamp: review_stamps.lint_related_stamp,
74
- source: sources.lint,
75
- },
76
- {
77
- script: TEST_SCRIPT,
78
- command: TEST_COMMAND,
79
- stamp: review_stamps.test_related_stamp,
80
- source: sources.test,
81
- },
82
- ]
78
+ const lint = {
79
+ script: LINT_SCRIPT,
80
+ command: LINT_COMMAND,
81
+ stamp: review_stamps.lint_related_stamp,
82
+ source: sources.lint,
83
+ }
84
+ const test = {
85
+ script: TEST_SCRIPT,
86
+ command: TEST_COMMAND,
87
+ stamp: review_stamps.test_related_stamp,
88
+ source: sources.test,
89
+ }
90
+
91
+ return (sources.is_unit_skipped ?? test_unit_guard.is_skipping(process.cwd()))
92
+ ? [lint]
93
+ : [lint, test]
83
94
  }
84
95
 
85
96
  // **An unreadable record reads as an absent one, and that is the safe direction.** `read` swallows a
@@ -97,10 +97,17 @@ function tag_reference_api_path(tag: string, repo?: string): string {
97
97
  return `${repo_api_path(repo)}/git/ref/tags/${tag}`
98
98
  }
99
99
 
100
+ // The GitHub Release published for one tag. REST answers 404 until `publish.yml`'s `create-release`
101
+ // job has run, which is what the release command's progress watch asks (joshuafolkken/kit#3193).
102
+ function release_by_tag_api_path(tag: string, repo?: string): string {
103
+ return `${repo_api_path(repo)}/releases/tags/${tag}`
104
+ }
105
+
100
106
  const git_gh_api_path = {
101
107
  FULL_PAGE_QUERY,
102
108
  repo_api_path,
103
109
  tag_ref_api_path: tag_reference_api_path,
110
+ release_by_tag_api_path,
104
111
  issues_api_path,
105
112
  issue_api_path,
106
113
  blocked_by_api_path,
@@ -2,7 +2,7 @@
2
2
  import { text } from 'node:stream/consumers'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { hook_decision } from '#scripts/josh/hook-decision'
5
- import { pretool_outcome_async } from './pretool-guard'
5
+ import { pretool_guard, pretool_outcome_async } from './pretool-guard'
6
6
 
7
7
  // The `PreToolUse` hook's entry, kept apart from the composition in `pretool-guard.ts`
8
8
  // (joshuafolkken/kit#2922). The hook bundles are built with `splitting: true`, which moves a module
@@ -16,6 +16,6 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
16
16
  const raw_payload = await text(process.stdin)
17
17
 
18
18
  hook_decision.load_environment_file()
19
- hook_decision.emit_outcome(await pretool_outcome_async(raw_payload))
19
+ pretool_guard.emit(raw_payload, await pretool_outcome_async(raw_payload))
20
20
  }
21
21
  }
@@ -1,9 +1,11 @@
1
1
  import { duplicate_read_outcome } from '#scripts/delegation/duplicate-read-guard'
2
2
  import { investigation_refusal } from '#scripts/delegation/investigation-guard'
3
- import type { GuardOutcome } from '#scripts/josh/hook-decision'
3
+ import { hook_decision, type GuardOutcome } from '#scripts/josh/hook-decision'
4
4
  import { delivered_rules } from '#scripts/rules/delivered-rules'
5
+ import { lane_background } from '#scripts/rules/lane-background'
5
6
  import { run_parent_cut_hook } from '#scripts/run/run-parent-cut-hook'
6
7
  import { run_watcher_hook } from '#scripts/run/run-watcher-hook'
8
+ import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
7
9
  import { batch_outcome } from './batch-guard'
8
10
  import { step_zero_notice } from './step-zero-notice'
9
11
 
@@ -108,6 +110,47 @@ async function pretool_outcome_async(raw_payload: string): Promise<GuardOutcome>
108
110
  return with_step_zero_notice(base, raw_payload)
109
111
  }
110
112
 
111
- const pretool_guard = { combine_outcomes, pretool_outcome }
113
+ function guarded_call(raw_payload: string): GuardedCall | undefined {
114
+ try {
115
+ const payload = hook_decision.parse_hook_payload(raw_payload)
116
+
117
+ return payload === undefined
118
+ ? undefined
119
+ : { name: payload.tool_name, input: payload.tool_input }
120
+ } catch {
121
+ return undefined
122
+ }
123
+ }
124
+
125
+ // The envelope that replaces the outcome's own when a lane child backgrounded `josh ship` alone
126
+ // (joshuafolkken/kit#3154): the call is run in the foreground instead of being refused. A refusal from
127
+ // any guard still wins, and a notice the outcome carried rides along in the same context.
128
+ function foreground_rewrite(
129
+ outcome: GuardOutcome,
130
+ raw_payload: string,
131
+ is_lane_child?: () => boolean,
132
+ ): string | undefined {
133
+ if (outcome.reason !== undefined) return undefined
134
+
135
+ const call = guarded_call(raw_payload)
136
+ const input =
137
+ call === undefined ? undefined : lane_background.foreground_input(call, is_lane_child)
138
+
139
+ if (input === undefined) return undefined
140
+
141
+ const context = [lane_background.FOREGROUND_NOTE, outcome.notice ?? outcome.fault]
142
+
143
+ return hook_decision.rewrite_envelope(input, context.filter(Boolean).join('\n\n'))
144
+ }
145
+
146
+ // The hook's one write: the foreground rewrite where it applies, the outcome's own envelope otherwise.
147
+ function emit(raw_payload: string, outcome: GuardOutcome): void {
148
+ const rewrite = foreground_rewrite(outcome, raw_payload)
149
+
150
+ if (rewrite === undefined) hook_decision.emit_outcome(outcome)
151
+ else process.stdout.write(`${rewrite}\n`)
152
+ }
153
+
154
+ const pretool_guard = { combine_outcomes, emit, foreground_rewrite, pretool_outcome }
112
155
 
113
156
  export { pretool_guard, pretool_outcome, pretool_outcome_async }
@@ -0,0 +1,41 @@
1
+ #!/bin/sh
2
+ # Launch one kit hook by name: `sh scripts/hooks/run-hook.sh <name> [arguments...]`
3
+ # (joshuafolkken/kit#3184).
4
+ #
5
+ # A hook command in `.claude/settings.json` / `.codex/hooks.json` only moves to the project root
6
+ # (`scripts/init/hook-launch.ts`) and calls this script, so everything every hook shares lives here once:
7
+ #
8
+ # - The git location variables a git hook exports are cleared when the root resolves without them, so
9
+ # the hook's own git calls act on the checkout it stands in. They are kept when the checkout needs
10
+ # them — git metadata outside the work tree.
11
+ # - The pre-built bundle `dist/hooks/<name>.js` runs once its gate passes; otherwise the live source
12
+ # runs through josh, whose command is the name with its first `-` turned into `:`
13
+ # (`pretool-guard` → `pretool:guard`).
14
+ # - In kit's own checkout the gate is `hook-bundle-ready.ts`, which rebuilds stale bundles
15
+ # (joshuafolkken/kit#2984). In an installed package it is a presence check: the published bundles
16
+ # cannot be stale against a source the consumer never edits, and Node strips no types under
17
+ # `node_modules`.
18
+ # - An `if`/`else`, never `&&`/`||`: a guard's refusal is a non-zero exit, and a chain would read it as
19
+ # a missing bundle and run the hook twice.
20
+
21
+ location_variables='GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_COMMON_DIR GIT_DIR GIT_INDEX_FILE GIT_NAMESPACE GIT_OBJECT_DIRECTORY GIT_WORK_TREE'
22
+ unset_options=''
23
+ for variable in $location_variables; do unset_options="$unset_options -u $variable"; done
24
+ # shellcheck disable=SC2086
25
+ if env $unset_options git rev-parse --show-toplevel >/dev/null 2>&1; then unset $location_variables; fi
26
+
27
+ hook_name=$1
28
+ shift
29
+ hooks_dir=$(dirname "$0")
30
+ package_dir="$hooks_dir/../.."
31
+ bundle="$package_dir/dist/hooks/$hook_name.js"
32
+ josh_command="${hook_name%%-*}:${hook_name#*-}"
33
+
34
+ case $0 in
35
+ *node_modules/*)
36
+ if [ -d "$package_dir/dist/hooks" ]; then node "$bundle" "$@"; else node "$package_dir/dist/josh.js" "$josh_command" "$@"; fi
37
+ ;;
38
+ *)
39
+ if node --disable-warning=ExperimentalWarning "$hooks_dir/hook-bundle-ready.ts"; then node "$bundle" "$@"; else pnpm josh "$josh_command" "$@"; fi
40
+ ;;
41
+ esac
@@ -0,0 +1,29 @@
1
+ import { patch_json_key } from '#scripts/config-merge/patch-json-key'
2
+ import { claude_plugin_config } from './claude-plugin-config'
3
+
4
+ // Keys kit's own `.claude/settings.json` sets for its sessions but never ships. `enableArtifact:
5
+ // false` keeps the Artifact tool's ~12k tokens out of every call of a kit run
6
+ // (joshuafolkken/kit#3140), but Claude Code lets any layer that sets it to `false` win, so a
7
+ // consumer given it could not turn the tool back on from `.claude/settings.local.json`.
8
+ const KIT_ONLY_SETTING_KEYS: ReadonlyArray<string> = ['enableArtifact']
9
+
10
+ function strip_kit_only_settings(content: string): string {
11
+ let stripped = content
12
+ for (const key of KIT_ONLY_SETTING_KEYS) stripped = patch_json_key.remove_json_key(stripped, key)
13
+
14
+ return stripped
15
+ }
16
+
17
+ function apply_kit_only_strip_for_destination(destination_path: string, content: string): string {
18
+ return destination_path.endsWith(claude_plugin_config.CLAUDE_SETTINGS_DESTINATION)
19
+ ? strip_kit_only_settings(content)
20
+ : content
21
+ }
22
+
23
+ const claude_kit_only_settings = {
24
+ KIT_ONLY_SETTING_KEYS,
25
+ strip_kit_only_settings,
26
+ apply_kit_only_strip_for_destination,
27
+ }
28
+
29
+ export { claude_kit_only_settings }
@@ -3,60 +3,43 @@ import { hook_launch } from './hook-launch'
3
3
 
4
4
  // The consumer's `.claude/settings.json` runs its hooks off the published package directly, not
5
5
  // through `pnpm josh` (joshuafolkken/kit#1930, joshuafolkken/kit#2023). Two paths inside a hook
6
- // command are rebased onto the installed package: the per-hook bundle `dist/hooks/<name>.js` a hook
7
- // prefers, and the `pnpm josh <command>` fallback it drops to when that bundle is missing — which for
8
- // a consumer becomes the `dist/josh.js` dispatcher, since the published package always ships `dist`
9
- // and a `pnpm` launch would only add the wrapper this exists to remove.
6
+ // command are rebased onto the installed package: the hook launcher `scripts/hooks/run-hook.sh`
7
+ // (joshuafolkken/kit#3184), which picks the bundle or the dispatcher from where it is installed, and a
8
+ // plain `pnpm josh <command>`, which becomes the `dist/josh.js` dispatcher — the published package
9
+ // always ships `dist`, and a `pnpm` launch would only add the wrapper this exists to remove.
10
10
  //
11
- // kit's own settings file keeps the `pnpm josh` fallback (`tsx scripts/josh/josh.ts` against live
12
- // source), so a fresh clone with no `dist/` still runs the current guards. The rewrite is gated on the
13
- // copy destination, exactly as the plugin injection is — kit's source tree is never transformed in
14
- // place.
11
+ // kit's own settings file keeps the launcher's live-source fallback (`pnpm josh` against
12
+ // `scripts/josh/josh.ts`), so a fresh clone with no `dist/` still runs the current guards. The rewrite
13
+ // is gated on the copy destination, exactly as the plugin injection is — kit's source tree is never
14
+ // transformed in place.
15
15
 
16
16
  const PNPM_JOSH_PREFIX = 'pnpm josh '
17
- const CODEX_ADAPTER_SOURCE = 'pnpm exec tsx scripts/hooks/codex-hook-adapter.ts'
18
17
  // Relative to the consumer's project root, which hook commands select before using this path.
19
18
  // The path is the plugin marketplace's `node_modules` location plus the built entry `bin.josh` names.
20
19
  const BUNDLE_INVOCATION = `node ${claude_plugin_config.MARKETPLACE_PATH}/dist/josh.js `
21
- // The per-hook bundle directory a fresh clone builds, and where it lives inside the installed package.
22
- const HOOK_BUNDLE_DIR = `${hook_launch.HOOK_DIST_DIR}/`
23
- const CONSUMER_HOOK_BUNDLE_DIR = `${claude_plugin_config.MARKETPLACE_PATH}/${HOOK_BUNDLE_DIR}`
20
+ // Anchored on the `sh ` that launches it: the installed path still ends in the source path, so only the
21
+ // anchor keeps a second pass from rebasing an already-rebased launcher onto itself.
22
+ const RUN_HOOK_INVOCATION = `sh ${hook_launch.RUN_HOOK_SCRIPT} `
23
+ const CONSUMER_RUN_HOOK_INVOCATION = `sh ${claude_plugin_config.MARKETPLACE_PATH}/${hook_launch.RUN_HOOK_SCRIPT} `
24
24
  const CONSUMER_PACKAGE_MANIFEST = `${claude_plugin_config.MARKETPLACE_PATH}/package.json`
25
25
  const MISSING_INSTALL_NOTICE = 'kit hooks inactive: run pnpm install, then reread CLAUDE.md'
26
26
  const MISSING_INSTALL_GUARD = `if [ ! -f ${CONSUMER_PACKAGE_MANIFEST} ]; then echo '${MISSING_INSTALL_NOTICE}' >&2; exit 0; fi; `
27
- const CODEX_ADAPTER_BUNDLE = `node ${CONSUMER_HOOK_BUNDLE_DIR}codex-hook-adapter.js`
28
27
  const CODEX_HOOKS_DESTINATION = '.codex/hooks.json'
29
28
  // The values below are matched inside raw JSON text, so the root prefix is taken in its escaped form.
30
29
  const PROJECT_ROOT_PREFIX = JSON.stringify(hook_launch.PROJECT_ROOT_PREFIX).slice(1, -1)
31
- // The installed package ships its bundles built, so they cannot be stale against a source the consumer
32
- // never has: the ready gate kit's own commands pass (joshuafolkken/kit#2984) becomes a presence check.
33
- const CONSUMER_READY_GATE = `[ -d ${CONSUMER_HOOK_BUNDLE_DIR} ]`
34
30
  // Capture the value of every `"command"` field, escapes and all, so the rewrites below touch command
35
31
  // values alone — never an echo reminder's prose or a `"description"` that merely mentions the string.
36
32
  const COMMAND_FIELD = /("command":\s*")((?:[^"\\]|\\.)*)(")/gu
37
33
 
38
- // Idempotent on its own: `CONSUMER_HOOK_BUNDLE_DIR` still contains `HOOK_BUNDLE_DIR`, so a blind
39
- // second pass would rebase an already-rebased path onto itself. A value already targeting the
40
- // installed package is left untouched, restoring the idempotency the earlier `pnpm josh`-anchored
41
- // rewrite had for free.
42
- function rebase_bundle_directory(value: string): string {
43
- return value.includes(CONSUMER_HOOK_BUNDLE_DIR)
44
- ? value
45
- : value.split(HOOK_BUNDLE_DIR).join(CONSUMER_HOOK_BUNDLE_DIR)
46
- }
47
-
48
- // Rebase both paths a hook command can carry: the bundle it prefers, then the `pnpm josh` fallback.
49
- // The bundle rewrite runs first — its replacement never contains `pnpm josh`, so the two are
50
- // independent whichever order they run. The fallback rewrite is idempotent too (`BUNDLE_INVOCATION`
51
- // holds no `pnpm josh `), so the whole rewrite is safe to run more than once.
34
+ // Rebase both paths a hook command can carry: the launcher, then a plain `pnpm josh`. Neither
35
+ // replacement contains the other's anchor, so the two are independent whichever order they run, and
36
+ // the whole rewrite is safe to run more than once.
52
37
  function rewrite_command_value(value: string): string {
53
- const rewritten = rebase_bundle_directory(value)
54
- .split(hook_launch.BUNDLE_READY_GATE)
55
- .join(CONSUMER_READY_GATE)
38
+ const rewritten = value
39
+ .split(RUN_HOOK_INVOCATION)
40
+ .join(CONSUMER_RUN_HOOK_INVOCATION)
56
41
  .split(PNPM_JOSH_PREFIX)
57
42
  .join(BUNDLE_INVOCATION)
58
- .split(CODEX_ADAPTER_SOURCE)
59
- .join(CODEX_ADAPTER_BUNDLE)
60
43
 
61
44
  if (!rewritten.includes(claude_plugin_config.MARKETPLACE_PATH)) return rewritten
62
45
  if (rewritten.includes(MISSING_INSTALL_GUARD)) return rewritten