@joshuafolkken/kit 1.1387.0 → 1.1453.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 (278) hide show
  1. package/.claude/skills/epic-commands/SKILL.md +1 -1
  2. package/.claude/skills/epic-commands/epic-bundle.md +5 -0
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
  4. package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
  5. package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
  6. package/.claude/skills/workflow-commands/backlogrun.md +3 -2
  7. package/.claude/skills/workflow-commands/chain-rule.md +4 -2
  8. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  9. package/.claude/skills/workflow-commands/halfrun.md +1 -1
  10. package/.claude/skills/workflow-commands/issue-comments.md +6 -0
  11. package/.claude/skills/workflow-commands/kickoff.md +12 -13
  12. package/.claude/skills/workflow-commands/observation-filing.md +1 -1
  13. package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
  14. package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
  15. package/.claude/skills/workflow-commands/prerequisite.md +6 -0
  16. package/.claude/skills/workflow-commands/retrospective.md +5 -5
  17. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  18. package/README.md +27 -4
  19. package/dist/hooks/audit-provision.js +2 -2
  20. package/dist/hooks/batch-guard.js +7 -7
  21. package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
  22. package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
  23. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  24. package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
  25. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  26. package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
  27. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  28. package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
  29. package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
  30. package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
  31. package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
  32. package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
  33. package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
  34. package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
  35. package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
  36. package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
  37. package/dist/hooks/codex-hook-adapter.js +15 -15
  38. package/dist/hooks/duplicate-read-guard.js +8 -8
  39. package/dist/hooks/format-edited.js +5 -5
  40. package/dist/hooks/inputs.json +261 -201
  41. package/dist/hooks/investigation-guard.js +8 -8
  42. package/dist/hooks/pretool-guard.js +14 -14
  43. package/dist/hooks/session-lang.js +69 -16
  44. package/dist/hooks/stop-guard.js +13 -38
  45. package/dist/josh.js +95 -19
  46. package/package.json +4 -3
  47. package/prompts/collaboration-workflow/issue-template.md +10 -8
  48. package/prompts/review.md +30 -4
  49. package/scripts/agent/claude-agent-argv.ts +13 -1
  50. package/scripts/auth/auth-setup-cli.ts +101 -0
  51. package/scripts/auth/claude-token-output.ts +78 -0
  52. package/scripts/auth/claude-token.ts +68 -0
  53. package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
  54. package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
  55. package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
  56. package/scripts/auto-ok/auto-ok-judge.ts +67 -0
  57. package/scripts/backlog/backlog-drive-cli.ts +14 -0
  58. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  59. package/scripts/backlog/backlog-drive-launch.ts +3 -0
  60. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  61. package/scripts/backlog/backlog-drive-named.ts +4 -2
  62. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  63. package/scripts/backlog/backlog-drive.ts +38 -11
  64. package/scripts/backlog/backlog-overlap.ts +22 -5
  65. package/scripts/backlog/backlog-ready.ts +1 -3
  66. package/scripts/backlog/backlogrun-args.ts +13 -2
  67. package/scripts/backlog/backlogrun-cli.ts +91 -19
  68. package/scripts/backlog/backlogrun-launch.ts +24 -0
  69. package/scripts/backlog/backlogrun-replace.ts +166 -0
  70. package/scripts/backlog/backlogrun-stop.ts +178 -0
  71. package/scripts/behavior/behavior-cli.ts +6 -1
  72. package/scripts/doctor/doctor-io.ts +60 -9
  73. package/scripts/doctor/doctor.ts +20 -1
  74. package/scripts/document/document-byte-budget.ts +3 -3
  75. package/scripts/epic/epic-bundle-cli.ts +8 -8
  76. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  77. package/scripts/epic/epic-bundle.ts +39 -4
  78. package/scripts/exports/unused-members-cli.ts +2 -1
  79. package/scripts/followup/followup-checks-event.ts +82 -0
  80. package/scripts/followup/git-followup-finish.ts +16 -2
  81. package/scripts/followup/git-followup-workflow.ts +5 -2
  82. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  83. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  84. package/scripts/followup/git-pr-followup.ts +4 -1
  85. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  86. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  87. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  88. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  89. package/scripts/fresh-machine/fresh-machine-patterns.ts +175 -0
  90. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  91. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  92. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  93. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  94. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  95. package/scripts/gate/gate-report.ts +70 -15
  96. package/scripts/gate/project-checks.ts +18 -4
  97. package/scripts/gate/scoped-green.ts +2 -1
  98. package/scripts/gate/verification-gate.ts +4 -3
  99. package/scripts/gh/gh-author-association.ts +14 -0
  100. package/scripts/gh/git-gh-conditional.ts +66 -0
  101. package/scripts/gh/git-gh-exec.ts +1 -1
  102. package/scripts/gh/git-gh-issue-read.ts +6 -3
  103. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  104. package/scripts/gh/git-gh-issue-write.ts +5 -1
  105. package/scripts/gh/git-gh-issue.ts +13 -1
  106. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  107. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  108. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  109. package/scripts/gh/git-pr-checks.ts +36 -2
  110. package/scripts/git/git-command.ts +16 -4
  111. package/scripts/git/git-schemas.ts +1 -0
  112. package/scripts/git/git-spawn-sync.ts +1 -1
  113. package/scripts/hooks/pretool-guard.ts +3 -1
  114. package/scripts/init/hook-command-rewrite.ts +31 -0
  115. package/scripts/init/init-actions.ts +7 -4
  116. package/scripts/init/init-ai-copy.ts +34 -11
  117. package/scripts/init/init-basic.ts +9 -4
  118. package/scripts/init/init-bootstrap.ts +6 -2
  119. package/scripts/init/init-dry-run-report.ts +150 -0
  120. package/scripts/init/init-dry-run.ts +189 -0
  121. package/scripts/init/init-install.ts +8 -1
  122. package/scripts/init/init-logic-toolchain.ts +76 -0
  123. package/scripts/init/init-logic.ts +2 -0
  124. package/scripts/init/init.ts +62 -42
  125. package/scripts/init/kit-setup-state.ts +11 -17
  126. package/scripts/init/own-formatter.ts +21 -0
  127. package/scripts/init/project-profile.ts +60 -12
  128. package/scripts/init/start-dry-run.ts +70 -0
  129. package/scripts/init/start-exec.ts +27 -1
  130. package/scripts/init/start-plan.ts +23 -3
  131. package/scripts/init/start-setup-pr.ts +13 -24
  132. package/scripts/init/start-steps.ts +62 -1
  133. package/scripts/init/start.ts +38 -17
  134. package/scripts/init/workflow-mark.ts +35 -0
  135. package/scripts/init/working-tree.ts +116 -0
  136. package/scripts/issue/issue-ack-cli.ts +84 -0
  137. package/scripts/issue/issue-auto-ok.ts +68 -99
  138. package/scripts/issue/issue-file-cli.ts +17 -17
  139. package/scripts/issue/issue-file.ts +1 -6
  140. package/scripts/issue/issue-labels.ts +46 -10
  141. package/scripts/issue/issue-release-cli.ts +69 -23
  142. package/scripts/issue/issue-release.ts +3 -1
  143. package/scripts/issue/issue-state-cli.ts +14 -4
  144. package/scripts/issue/issue-watch-diff.ts +152 -0
  145. package/scripts/issue/issue-watch-gate.ts +81 -0
  146. package/scripts/issue/issue-watch-hook.ts +54 -0
  147. package/scripts/issue/issue-watch.ts +240 -0
  148. package/scripts/issue/measure-rerun-cli.ts +3 -4
  149. package/scripts/josh/josh-command-map.ts +4 -0
  150. package/scripts/josh/josh-commands-ai.ts +11 -0
  151. package/scripts/josh/josh-commands-backlog.ts +2 -2
  152. package/scripts/josh/josh-commands-development.ts +1 -1
  153. package/scripts/josh/josh-commands-issue.ts +10 -3
  154. package/scripts/josh/josh-commands-lane.ts +11 -2
  155. package/scripts/josh/josh-commands-project.ts +26 -2
  156. package/scripts/josh/josh-verdict.ts +33 -4
  157. package/scripts/josh/process-identity.ts +32 -6
  158. package/scripts/lane/claude-lane-broker.ts +142 -0
  159. package/scripts/lane/lane-await.ts +6 -2
  160. package/scripts/lane/lane-broker-server.ts +69 -0
  161. package/scripts/lane/lane-cache.ts +37 -7
  162. package/scripts/lane/lane-capacity.ts +8 -6
  163. package/scripts/lane/lane-child-invocation.ts +1 -0
  164. package/scripts/lane/lane-cli.ts +15 -1
  165. package/scripts/lane/lane-close.ts +15 -6
  166. package/scripts/lane/lane-dispatch.ts +36 -10
  167. package/scripts/lane/lane-environment.ts +1 -1
  168. package/scripts/lane/lane-launch-cli.ts +13 -4
  169. package/scripts/lane/lane-ledger.ts +21 -3
  170. package/scripts/lane/lane-measure-cli.ts +10 -2
  171. package/scripts/lane/lane-open.ts +9 -1
  172. package/scripts/lane/lane-preserve.ts +49 -0
  173. package/scripts/lane/lane-reap.ts +20 -5
  174. package/scripts/lane/lane-registry.ts +6 -0
  175. package/scripts/lane/lane-restart-cli.ts +51 -0
  176. package/scripts/lane/lane-restart.ts +175 -0
  177. package/scripts/lane/lane-session-broker.ts +363 -0
  178. package/scripts/lane/lane-spent.ts +195 -0
  179. package/scripts/lane/lane-stage-stats.ts +27 -3
  180. package/scripts/lane/lane-stop-stats.ts +96 -0
  181. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  182. package/scripts/lane/process-tools.ts +31 -0
  183. package/scripts/lib/buffered-process.ts +36 -2
  184. package/scripts/lib/pnpm-reach.ts +27 -0
  185. package/scripts/lib/status-icons.ts +5 -1
  186. package/scripts/lint/lint-parallel.ts +19 -2
  187. package/scripts/observations/observation-ledger-home.ts +1 -0
  188. package/scripts/observations/observation-ledger-migrate.ts +3 -15
  189. package/scripts/release/release-issue-close.ts +133 -0
  190. package/scripts/release/release-publish.ts +12 -1
  191. package/scripts/repo/repository-labels.ts +11 -1
  192. package/scripts/review/comment-ranges.ts +199 -0
  193. package/scripts/review/review-attest-cli.ts +21 -6
  194. package/scripts/review/review-brief-cli.ts +24 -7
  195. package/scripts/review/review-code-tree.ts +119 -0
  196. package/scripts/review/review-comment-round.ts +114 -0
  197. package/scripts/review/review-record-cli.ts +39 -17
  198. package/scripts/review/review-round2-cli.ts +16 -5
  199. package/scripts/review/review-round2.ts +30 -10
  200. package/scripts/review/review-stamps.ts +22 -1
  201. package/scripts/rules/implementation-cut.ts +3 -3
  202. package/scripts/rules/pre-gate-cut.ts +1 -1
  203. package/scripts/run/board/run-board-checks.ts +42 -0
  204. package/scripts/run/board/run-board-cli.ts +2 -0
  205. package/scripts/run/board/run-board-every.ts +3 -1
  206. package/scripts/run/board/run-board-fit.ts +16 -5
  207. package/scripts/run/board/run-board-fresh.ts +64 -0
  208. package/scripts/run/board/run-board-header.ts +129 -53
  209. package/scripts/run/board/run-board-labels.ts +47 -4
  210. package/scripts/run/board/run-board-lane-event.ts +66 -0
  211. package/scripts/run/board/run-board-layout.ts +57 -9
  212. package/scripts/run/board/run-board-now.ts +234 -0
  213. package/scripts/run/board/run-board-operation-read.ts +75 -0
  214. package/scripts/run/board/run-board-operation.ts +150 -0
  215. package/scripts/run/board/run-board-phase.ts +26 -1
  216. package/scripts/run/board/run-board-plan.ts +4 -6
  217. package/scripts/run/board/run-board-read.ts +49 -3
  218. package/scripts/run/board/run-board-reason.ts +124 -0
  219. package/scripts/run/board/run-board-render-checks.ts +55 -0
  220. package/scripts/run/board/run-board-render-legend.ts +12 -3
  221. package/scripts/run/board/run-board-render-slot.ts +2 -2
  222. package/scripts/run/board/run-board-render-tail.ts +128 -0
  223. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  224. package/scripts/run/board/run-board-render-title.ts +58 -0
  225. package/scripts/run/board/run-board-render.ts +96 -80
  226. package/scripts/run/board/run-board-row-yield.ts +32 -0
  227. package/scripts/run/board/run-board-setup.ts +61 -13
  228. package/scripts/run/board/run-board-state.ts +10 -0
  229. package/scripts/run/board/run-board-status.ts +66 -7
  230. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  231. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  232. package/scripts/run/board/run-board-thanks.ts +53 -0
  233. package/scripts/run/board/run-board-tick.ts +50 -21
  234. package/scripts/run/board/run-board-usual.ts +44 -0
  235. package/scripts/run/carry/run-carry-args.ts +9 -1
  236. package/scripts/run/carry/run-carry-cli.ts +2 -45
  237. package/scripts/run/carry/run-carry-end.ts +73 -0
  238. package/scripts/run/carry/run-carry.ts +8 -1
  239. package/scripts/run/entry/run-entry-cli.ts +5 -0
  240. package/scripts/run/event/run-event-checks.ts +54 -0
  241. package/scripts/run/event/run-event-render.ts +2 -0
  242. package/scripts/run/event/run-event-scope.ts +1 -0
  243. package/scripts/run/event/run-event-setup.ts +55 -11
  244. package/scripts/run/event/run-event-stream.ts +27 -11
  245. package/scripts/run/merge/run-merge-cli.ts +7 -4
  246. package/scripts/run/merge/run-merge-ship.ts +61 -0
  247. package/scripts/run/merge/run-merge-steps.ts +7 -14
  248. package/scripts/run/merge/run-merge-token.ts +16 -2
  249. package/scripts/run/run-headless.ts +1 -0
  250. package/scripts/run/run-step.ts +2 -0
  251. package/scripts/run/ship/run-ship-cli.ts +22 -2
  252. package/scripts/run/ship/run-ship-return.ts +31 -13
  253. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  254. package/scripts/run/ship/run-ship-stage.ts +1 -0
  255. package/scripts/run/wake/run-wake-await.ts +130 -0
  256. package/scripts/run/wake/run-wake-cli.ts +60 -71
  257. package/scripts/run/wake/run-wake-describe.ts +34 -2
  258. package/scripts/run/wake/run-wake-driver.ts +11 -4
  259. package/scripts/run/wake/run-wake-session.ts +20 -3
  260. package/scripts/run/wake/run-wake-stop.ts +37 -0
  261. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  262. package/scripts/sync/claude-settings-merge.ts +144 -0
  263. package/scripts/sync/sync-ai-files.ts +28 -3
  264. package/scripts/sync/sync.ts +15 -0
  265. package/scripts/test/project-test-script.ts +73 -0
  266. package/scripts/test/skip-marker.ts +14 -1
  267. package/scripts/test/test-related.ts +3 -2
  268. package/scripts/test/test-unit-guard.ts +78 -14
  269. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  270. package/scripts/uninstall/distributed-files.ts +92 -0
  271. package/scripts/uninstall/uninstall-plan.ts +119 -0
  272. package/scripts/uninstall/uninstall-report.ts +61 -0
  273. package/scripts/uninstall/uninstall.ts +206 -0
  274. package/templates/AGENTS.basic.md +4 -0
  275. package/templates/GEMINI.basic.md +6 -0
  276. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  277. package/scripts/lane/openai-review-broker.ts +0 -262
  278. package/scripts/run/board/run-board-track.ts +0 -22
@@ -166,14 +166,25 @@ async function delete_branch(branch_name: string): Promise<void> {
166
166
  // report "nothing is stranded" about a branch nobody measured, which is the one wrong answer that
167
167
  // loses work. `count_merges` guards the same way and falls back to `0` instead, because there a
168
168
  // missing count means an empty range rather than a claim about somebody's commits.
169
- async function commit_count_beyond(base: string, tip: string): Promise<number> {
170
- const range = `${base}..${tip}`
171
- const output = await git_spawn.read(['rev-list', '--count', range])
169
+ async function read_commit_count(revisions: ReadonlyArray<string>): Promise<number> {
170
+ const output = await git_spawn.read(['rev-list', '--count', ...revisions])
172
171
  const parsed = Number(output)
173
172
 
174
173
  if (output.length > 0 && Number.isFinite(parsed)) return parsed
175
174
 
176
- throw new Error(`\`git rev-list --count ${range}\` printed \`${output}\` rather than a count`)
175
+ throw new Error(
176
+ `\`git rev-list --count ${revisions.join(' ')}\` printed \`${output}\` rather than a count`,
177
+ )
178
+ }
179
+
180
+ async function commit_count_beyond(base: string, tip: string): Promise<number> {
181
+ return await read_commit_count([`${base}..${tip}`])
182
+ }
183
+
184
+ // How many commits `branch_name` holds that no remote-tracking ref reaches — what deleting the branch
185
+ // would lose. An unparseable count throws for the reason `commit_count_beyond`'s does.
186
+ async function commit_count_unpushed(branch_name: string): Promise<number> {
187
+ return await read_commit_count([branch_name, '--not', '--remotes'])
177
188
  }
178
189
 
179
190
  function is_exit_code_128(cause: unknown): boolean {
@@ -397,6 +408,7 @@ const git_command = {
397
408
  checkout,
398
409
  commit,
399
410
  commit_count_beyond,
411
+ commit_count_unpushed,
400
412
  delete_branch,
401
413
  push,
402
414
  pull_fast_forward,
@@ -10,6 +10,7 @@ const rollup_item_schema = z.looseObject({
10
10
  conclusion: z.string().optional(),
11
11
  name: z.string().optional(),
12
12
  context: z.string().optional(),
13
+ description: z.string().nullish(),
13
14
  })
14
15
 
15
16
  const pr_raw_schema = z.object({
@@ -72,4 +72,4 @@ function toplevel(timeout: number = GIT_TIMEOUT_MS): GitSyncResult {
72
72
  const git_spawn_sync = { GIT_TIMEOUT_MS, C_LOCALE, run, read, origin_url, toplevel }
73
73
 
74
74
  export { git_spawn_sync }
75
- export type { GitSyncResult }
75
+ export type { GitSyncOptions, GitSyncResult }
@@ -1,5 +1,6 @@
1
1
  import { duplicate_read_outcome } from '#scripts/delegation/duplicate-read-guard'
2
2
  import { investigation_refusal } from '#scripts/delegation/investigation-guard'
3
+ import { issue_watch_hook } from '#scripts/issue/issue-watch-hook'
3
4
  import { hook_decision, type GuardOutcome } from '#scripts/josh/hook-decision'
4
5
  import { lane_phase } from '#scripts/lane/lane-phase'
5
6
  import { json_value } from '#scripts/lib/json-value'
@@ -104,8 +105,9 @@ async function mark_phase(outcome: GuardOutcome): Promise<void> {
104
105
  // surfaced first.
105
106
  async function async_reason(raw_payload: string): Promise<string | undefined> {
106
107
  const watcher = await run_watcher_hook.watcher_hook_reason(raw_payload)
108
+ const cut = watcher ?? (await run_parent_cut_hook.parent_cut_reason(raw_payload))
107
109
 
108
- return watcher ?? (await run_parent_cut_hook.parent_cut_reason(raw_payload))
110
+ return cut ?? (await issue_watch_hook.watch_reason())
109
111
  }
110
112
 
111
113
  async function pretool_outcome_async(raw_payload: string): Promise<GuardOutcome> {
@@ -25,6 +25,30 @@ const CONSUMER_PACKAGE_MANIFEST = `${claude_plugin_config.MARKETPLACE_PATH}/pack
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
27
  const CODEX_HOOKS_DESTINATION = '.codex/hooks.json'
28
+ // Every shape a hook command kit wrote can carry: the launcher as kit's own file names it, the
29
+ // launcher rebased onto the installed package, and the installed dispatcher the releases from
30
+ // 2026-09-13 (joshuafolkken/kit#1930) to the launcher rebased `pnpm josh` onto. The settings merge
31
+ // tells kit's hooks from a project's own by these and the list below, so the shapes stay beside the
32
+ // rewrite that produces them.
33
+ const KIT_COMMAND_MARKERS: ReadonlyArray<string> = [
34
+ RUN_HOOK_INVOCATION,
35
+ CONSUMER_RUN_HOOK_INVOCATION,
36
+ BUNDLE_INVOCATION,
37
+ ]
38
+ // Before joshuafolkken/kit#1930 nothing was rebased: a consumer received these six as the whole hook
39
+ // command. A plain `pnpm josh <command>` is also how a project writes a hook of its own, so only the
40
+ // exact commands count — and kit still delivers each of them through the launcher, so one left in
41
+ // place would run its guard or formatter a second time. The list is history and never grows.
42
+ const PLAIN_KIT_COMMANDS: ReadonlySet<string> = new Set(
43
+ [
44
+ 'audit:provision',
45
+ 'batch:guard',
46
+ 'format:edited',
47
+ 'investigation:guard',
48
+ 'rule:guard',
49
+ 'session:lang',
50
+ ].map((command) => `${PNPM_JOSH_PREFIX}${command}`),
51
+ )
28
52
  // The values below are matched inside raw JSON text, so the root prefix is taken in its escaped form.
29
53
  const PROJECT_ROOT_PREFIX = JSON.stringify(hook_launch.PROJECT_ROOT_PREFIX).slice(1, -1)
30
54
  // Capture the value of every `"command"` field, escapes and all, so the rewrites below touch command
@@ -80,8 +104,15 @@ function apply_hook_command_rewrite_for_destination(
80
104
  return content
81
105
  }
82
106
 
107
+ function is_kit_hook_command(command: string): boolean {
108
+ if (PLAIN_KIT_COMMANDS.has(command)) return true
109
+
110
+ return KIT_COMMAND_MARKERS.some((marker) => command.includes(marker))
111
+ }
112
+
83
113
  const hook_command_rewrite = {
84
114
  apply_hook_command_rewrite_for_destination,
115
+ is_kit_hook_command,
85
116
  rewrite_hook_commands,
86
117
  }
87
118
 
@@ -106,7 +106,7 @@ function build_basic_settings_action(): FileAction {
106
106
  }
107
107
 
108
108
  function build_basic_vscode_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
109
- const recommendations = shape.has_web
109
+ const recommendations = shape.should_use_prettier
110
110
  ? [...BASIC_EXTENSIONS, PRETTIER_EXTENSION]
111
111
  : BASIC_EXTENSIONS
112
112
  const actions: Array<FileAction> = [
@@ -117,7 +117,7 @@ function build_basic_vscode_actions(shape: ProjectShape): ReadonlyArray<FileActi
117
117
  ),
118
118
  ]
119
119
 
120
- if (shape.has_web) actions.push(build_basic_settings_action())
120
+ if (shape.should_use_prettier) actions.push(build_basic_settings_action())
121
121
 
122
122
  return actions
123
123
  }
@@ -210,7 +210,7 @@ function build_basic_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
210
210
  if (shape.has_git) actions.push(build_gitignore_action())
211
211
  if (shape.has_typescript) actions.push(build_basic_tsconfig_action())
212
212
 
213
- if (shape.has_web) {
213
+ if (shape.should_use_prettier) {
214
214
  actions.push({
215
215
  dest: BASIC_PRETTIER_CONFIG,
216
216
  create: () =>
@@ -221,6 +221,9 @@ function build_basic_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
221
221
  }
222
222
 
223
223
  actions.push(...build_basic_vscode_actions(shape))
224
+ // The `.claude/settings.json` a `josh start` project receives wires the `session:lang` hook, whose
225
+ // unset default is `ja`, so the language is seeded from the locale as on `full`.
226
+ if (shape.has_workflow) actions.push(...session_language_environment.build_session_lang_actions())
224
227
 
225
228
  return actions
226
229
  }
@@ -248,4 +251,4 @@ function build_file_actions(shape?: ProjectShape): ReadonlyArray<FileAction> {
248
251
  const init_actions = { build_file_actions, read_package_json }
249
252
 
250
253
  export type { FileAction }
251
- export { init_actions, PRETTIER_CONFIG_JS, BASIC_PRETTIER_CONFIG }
254
+ export { init_actions, PRETTIER_CONFIG_JS, BASIC_PRETTIER_CONFIG, LEFTHOOK_PATH }
@@ -25,9 +25,11 @@ const PRETTIER_IGNORE = '.prettierignore'
25
25
  const GIT_ATTRIBUTES = '.gitattributes'
26
26
  const CODE_OF_CONDUCT = 'CODE_OF_CONDUCT.md'
27
27
  const { SECURITY_MD } = security_policy
28
+ const AGENTS_MD = 'AGENTS.md'
29
+ const GEMINI_MD = 'GEMINI.md'
28
30
  const BASIC_AI_FILES = [
29
- 'AGENTS.md',
30
- 'GEMINI.md',
31
+ AGENTS_MD,
32
+ GEMINI_MD,
31
33
  PRETTIER_IGNORE,
32
34
  GIT_ATTRIBUTES,
33
35
  CODE_OF_CONDUCT,
@@ -36,6 +38,8 @@ const BASIC_AI_FILES = [
36
38
  ]
37
39
  // A basic project gets its own template for these, not kit's development copy.
38
40
  const BASIC_SOURCES: Readonly<Record<string, string>> = {
41
+ [AGENTS_MD]: 'templates/AGENTS.basic.md',
42
+ [GEMINI_MD]: 'templates/GEMINI.basic.md',
39
43
  [PRETTIER_IGNORE]: 'templates/prettierignore.basic',
40
44
  [WORKSPACE_YAML]: 'templates/pnpm-workspace.basic.yaml',
41
45
  }
@@ -43,6 +47,9 @@ const BASIC_SOURCES: Readonly<Record<string, string>> = {
43
47
  const CONSUMER_SOURCES: Readonly<Record<string, string>> = {
44
48
  [SECURITY_MD]: security_policy.SECURITY_TEMPLATE,
45
49
  }
50
+ // A basic project `josh start` set up for the GitHub Issue workflow runs its agents unattended, on the
51
+ // permissions, hooks and plugin this file declares (#3796).
52
+ const BASIC_WORKFLOW_AI_FILES = [...BASIC_AI_FILES, '.claude/settings.json']
46
53
  const GITHUB_FILES = new Set([CODE_OF_CONDUCT, SECURITY_MD])
47
54
  const BASIC_POINTER_MAPPING = { src: 'templates/cursorrules.basic', dest: '.cursorrules' }
48
55
  const PULL_REQUEST_TEMPLATE = '.github/pull_request_template.md'
@@ -62,11 +69,15 @@ function did_read_unstamped(destination_path: string): boolean {
62
69
  }
63
70
  }
64
71
 
65
- function copy_ai_file(source_path: string, destination_path: string): void {
66
- const content = readFileSync(source_path, 'utf8')
72
+ // What kit writes at a destination it copies to — the content `josh uninstall` compares a project's
73
+ // file against, so it is read from the one place the copy itself reads it.
74
+ function copied_content(source_path: string, destination_path: string): string {
75
+ return transform_copied_content(destination_path, readFileSync(source_path, 'utf8'))
76
+ }
67
77
 
78
+ function copy_ai_file(source_path: string, destination_path: string): void {
68
79
  mkdirSync(path.dirname(destination_path), { recursive: true })
69
- writeFileSync(destination_path, transform_copied_content(destination_path, content))
80
+ writeFileSync(destination_path, copied_content(source_path, destination_path))
70
81
  }
71
82
 
72
83
  // `init` declines to modify a file that already exists, and that includes not stamping it: the
@@ -132,6 +143,14 @@ function merge_existing_workspace_yaml(source_path: string, destination_path: st
132
143
  console.info(` ✔ updated ${WORKSPACE_YAML}`)
133
144
  }
134
145
 
146
+ // The workspace file kit writes where the project has none.
147
+ function workspace_content(source_path: string, destination_path: string): string {
148
+ return transform_copied_content(
149
+ destination_path,
150
+ read_workspace_template(source_path, destination_path),
151
+ )
152
+ }
153
+
135
154
  function did_skip_workspace_yaml_copy(source_path: string, destination_path: string): boolean {
136
155
  if (existsSync(destination_path)) {
137
156
  merge_existing_workspace_yaml(source_path, destination_path)
@@ -139,9 +158,7 @@ function did_skip_workspace_yaml_copy(source_path: string, destination_path: str
139
158
  return false
140
159
  }
141
160
 
142
- const template = read_workspace_template(source_path, destination_path)
143
-
144
- writeFileSync(destination_path, transform_copied_content(destination_path, template))
161
+ writeFileSync(destination_path, workspace_content(source_path, destination_path))
145
162
  console.info(` ✔ created ${WORKSPACE_YAML}`)
146
163
 
147
164
  return false
@@ -331,7 +348,7 @@ function should_copy_github_file(filename: string, shape?: ProjectShape): boolea
331
348
  function should_copy_basic_file(filename: string, shape?: ProjectShape): boolean {
332
349
  if (filename !== PRETTIER_IGNORE || shape?.profile !== 'basic') return true
333
350
 
334
- return shape.has_web
351
+ return shape.should_use_prettier
335
352
  }
336
353
 
337
354
  function should_copy_ai_file(filename: string, shape?: ProjectShape): boolean {
@@ -342,8 +359,12 @@ function should_copy_ai_file(filename: string, shape?: ProjectShape): boolean {
342
359
  )
343
360
  }
344
361
 
362
+ function basic_ai_files(shape: ProjectShape): ReadonlyArray<string> {
363
+ return shape.has_workflow ? BASIC_WORKFLOW_AI_FILES : BASIC_AI_FILES
364
+ }
365
+
345
366
  function ai_files(shape?: ProjectShape): ReadonlyArray<string> {
346
- const files = shape?.profile === 'basic' ? BASIC_AI_FILES : init_logic.get_ai_copy_files()
367
+ const files = shape?.profile === 'basic' ? basic_ai_files(shape) : init_logic.get_ai_copy_files()
347
368
 
348
369
  return files.filter((filename) => should_copy_ai_file(filename, shape))
349
370
  }
@@ -397,6 +418,8 @@ function run_ai_copies(shape?: ProjectShape): string | undefined {
397
418
 
398
419
  const init_ai_copy = {
399
420
  copy_ai_file,
421
+ copied_content,
422
+ workspace_content,
400
423
  run_ai_copies,
401
424
  ai_files,
402
425
  ai_file_source,
@@ -405,4 +428,4 @@ const init_ai_copy = {
405
428
  read_workspace_template,
406
429
  }
407
430
 
408
- export { init_ai_copy, CLAUDE_MD_FILENAME }
431
+ export { init_ai_copy, CLAUDE_MD_FILENAME, WORKSPACE_YAML }
@@ -3,6 +3,7 @@ import { json_value } from '#scripts/lib/json-value'
3
3
  import { z } from 'zod'
4
4
  import { init_logic } from './init-logic'
5
5
  import type { ProjectShape } from './project-profile'
6
+ import { workflow_mark } from './workflow-mark'
6
7
 
7
8
  const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
8
9
  const record_schema = z.record(z.string(), z.unknown())
@@ -16,13 +17,17 @@ function initial_manifest(): string {
16
17
  return json_format.format_json({ private: true })
17
18
  }
18
19
 
19
- function with_recorded_profile(content: string, profile: ProjectShape['profile']): string {
20
+ function with_recorded_profile(
21
+ content: string,
22
+ profile: ProjectShape['profile'],
23
+ has_workflow = false,
24
+ ): string {
20
25
  const parsed: unknown = JSON.parse(content)
21
26
  const manifest = record_schema.parse(parsed)
22
27
  const existing = manifest['josh']
23
28
  const josh = json_value.is_record(existing) ? existing : {}
24
29
 
25
- manifest['josh'] = { ...josh, profile }
30
+ manifest['josh'] = { ...josh, profile, ...workflow_mark.recorded_fields(has_workflow) }
26
31
 
27
32
  return json_format.format_json(manifest)
28
33
  }
@@ -32,7 +37,7 @@ function merge_basic_manifest(
32
37
  shape: ProjectShape,
33
38
  versions: BasicVersions,
34
39
  ): string {
35
- const with_profile = with_recorded_profile(content, shape.profile)
40
+ const with_profile = with_recorded_profile(content, shape.profile, shape.has_workflow)
36
41
  const with_script = init_logic.merge_package_scripts(with_profile, {
37
42
  preinstall: init_logic.SAFE_CHAIN_CMD,
38
43
  josh: 'josh',
@@ -40,7 +45,7 @@ function merge_basic_manifest(
40
45
  const with_kit = init_logic.merge_development_dependencies(with_script, {
41
46
  [KIT_PACKAGE_NAME]: versions.kit,
42
47
  })
43
- const with_prettier = shape.has_web
48
+ const with_prettier = shape.should_use_prettier
44
49
  ? init_logic.merge_development_dependencies(with_kit, { prettier: versions.prettier })
45
50
  : with_kit
46
51
 
@@ -82,13 +82,17 @@ function ensure_project_manifest(project_root: string): void {
82
82
  if (!existsSync(manifest_path)) writeFileSync(manifest_path, init_basic.initial_manifest())
83
83
  }
84
84
 
85
+ function is_handed_off(): boolean {
86
+ return process.env[HANDOFF_ENV] !== undefined
87
+ }
88
+
85
89
  // Returns the failure to report, or undefined once the project-installed kit finished the setup.
86
90
  function hand_off(
87
91
  package_directory: string,
88
92
  project_root: string,
89
93
  args: ReadonlyArray<string>,
90
94
  ): string | undefined {
91
- if (process.env[HANDOFF_ENV] !== undefined) return REPEATED_HANDOFF
95
+ if (is_handed_off()) return REPEATED_HANDOFF
92
96
  console.info('\n🚀 Installing @joshuafolkken/kit into the project, then running its josh init')
93
97
  const template = readFileSync(path.join(package_directory, KIT_BUILDS_TEMPLATE), 'utf8')
94
98
  const install = kit_install_step(has_kit_dependency(project_root), template)
@@ -100,5 +104,5 @@ function hand_off(
100
104
  return undefined
101
105
  }
102
106
 
103
- const init_bootstrap = { is_project_kit, kit_build_flags, hand_off }
107
+ const init_bootstrap = { HANDOFF_ENV, is_project_kit, kit_build_flags, hand_off, is_handed_off }
104
108
  export { init_bootstrap }
@@ -0,0 +1,150 @@
1
+ import { json_value } from '#scripts/lib/json-value'
2
+
3
+ type ChangeKind = 'create' | 'merge' | 'overwrite' | 'delete'
4
+
5
+ interface FileChange {
6
+ path: string
7
+ before: string | undefined
8
+ after: string | undefined
9
+ }
10
+
11
+ type JsonRecord = Record<string, unknown>
12
+
13
+ const PACKAGE_JSON = 'package.json'
14
+ const JSON_EXTENSION = '.json'
15
+ const KIND_WIDTH = 'overwrite'.length
16
+ const NO_CHANGES = ' (no file changes)'
17
+
18
+ // Every path whose content differs between the two snapshots, sorted, so a run reads the same twice.
19
+ function changed_files(
20
+ before: ReadonlyMap<string, string>,
21
+ after: ReadonlyMap<string, string>,
22
+ ): Array<FileChange> {
23
+ const paths = [...new Set([...before.keys(), ...after.keys()])].toSorted((left, right) =>
24
+ left.localeCompare(right),
25
+ )
26
+
27
+ return paths
28
+ .filter((file) => before.get(file) !== after.get(file))
29
+ .map((file) => ({ path: file, before: before.get(file), after: after.get(file) }))
30
+ }
31
+
32
+ // A changed file is a merge when every line it had survives — the project's own content is kept —
33
+ // and an overwrite when any of its lines is replaced or removed.
34
+ function keeps_every_line(before: string, after: string): boolean {
35
+ const remaining = new Set(after.split('\n'))
36
+
37
+ return before.split('\n').every((line) => remaining.has(line))
38
+ }
39
+
40
+ function is_kept_array(before: ReadonlyArray<unknown>, after: unknown): boolean {
41
+ if (!Array.isArray(after)) return false
42
+ const remaining = new Set(after.map((value) => JSON.stringify(value)))
43
+
44
+ return before.every((value) => remaining.has(JSON.stringify(value)))
45
+ }
46
+
47
+ // A JSON file is rewritten whole when kit adds to it — `package.json` written out again, a list grown — so
48
+ // it is compared by value: a merge when every value it had is still there, wherever the lines moved.
49
+ function is_kept(before: unknown, after: unknown): boolean {
50
+ if (Array.isArray(before)) return is_kept_array(before, after)
51
+ if (!json_value.is_record(before) || !json_value.is_record(after)) return before === after
52
+
53
+ return Object.keys(before).every((key) => is_kept(before[key], after[key]))
54
+ }
55
+
56
+ // Undefined when either side is not plain JSON (a `tsconfig.json` with comments), which falls back
57
+ // to the line test.
58
+ function keeps_every_value(before: string, after: string): boolean | undefined {
59
+ const old_value = json_value.parse_or_undefined(before)
60
+ const new_value = json_value.parse_or_undefined(after)
61
+ if (old_value === undefined || new_value === undefined) return undefined
62
+
63
+ return is_kept(old_value, new_value)
64
+ }
65
+
66
+ function keeps_content(file: string, before: string, after: string): boolean {
67
+ const is_kept_by_value = file.endsWith(JSON_EXTENSION)
68
+ ? keeps_every_value(before, after)
69
+ : undefined
70
+
71
+ return is_kept_by_value ?? keeps_every_line(before, after)
72
+ }
73
+
74
+ function change_kind(change: FileChange): ChangeKind {
75
+ if (change.before === undefined) return 'create'
76
+ if (change.after === undefined) return 'delete'
77
+
78
+ return keeps_content(change.path, change.before, change.after) ? 'merge' : 'overwrite'
79
+ }
80
+
81
+ function parse_record(content: string | undefined): JsonRecord {
82
+ const parsed = content === undefined ? undefined : json_value.parse_or_undefined(content)
83
+
84
+ return json_value.is_record(parsed) ? parsed : {}
85
+ }
86
+
87
+ function changed_keys(before: JsonRecord, after: JsonRecord): Array<string> {
88
+ const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])]
89
+
90
+ return keys.filter((key) => JSON.stringify(before[key]) !== JSON.stringify(after[key]))
91
+ }
92
+
93
+ function entry_line(name: string, before: unknown, after: unknown): string {
94
+ if (before === undefined) return `+ ${name} ${JSON.stringify(after)}`
95
+ if (after === undefined) return `- ${name}`
96
+
97
+ return `~ ${name} ${JSON.stringify(before)} → ${JSON.stringify(after)}`
98
+ }
99
+
100
+ // A missing field reads as an empty object, so a block added or removed whole is listed too.
101
+ function as_entries(value: unknown): JsonRecord | undefined {
102
+ if (value === undefined) return {}
103
+
104
+ return json_value.is_record(value) ? value : undefined
105
+ }
106
+
107
+ // A field that is an object (`scripts`, `devDependencies`) is listed entry by entry, so the plan
108
+ // names each script and dependency rather than the whole block.
109
+ function field_lines(key: string, before: unknown, after: unknown): Array<string> {
110
+ const old_entries = as_entries(before)
111
+ const new_entries = as_entries(after)
112
+
113
+ if (old_entries === undefined || new_entries === undefined) {
114
+ return [entry_line(key, before, after)]
115
+ }
116
+
117
+ return changed_keys(old_entries, new_entries).map((name) =>
118
+ entry_line(`${key}.${name}`, old_entries[name], new_entries[name]),
119
+ )
120
+ }
121
+
122
+ function package_json_lines(change: FileChange): Array<string> {
123
+ const before = parse_record(change.before)
124
+ const after = parse_record(change.after)
125
+
126
+ return changed_keys(before, after).flatMap((key) => field_lines(key, before[key], after[key]))
127
+ }
128
+
129
+ function change_lines(change: FileChange): Array<string> {
130
+ const head = ` ${change_kind(change).padEnd(KIND_WIDTH)} ${change.path}`
131
+ if (change.path !== PACKAGE_JSON || change.after === undefined) return [head]
132
+
133
+ return [head, ...package_json_lines(change).map((line) => ` ${line}`)]
134
+ }
135
+
136
+ function format_report(
137
+ title: string,
138
+ changes: ReadonlyArray<FileChange>,
139
+ dependency_lines: ReadonlyArray<string>,
140
+ ): string {
141
+ const files =
142
+ changes.length === 0 ? [NO_CHANGES] : changes.flatMap((change) => change_lines(change))
143
+ const dependencies = dependency_lines.map((line) => ` ${line}`)
144
+
145
+ return [title, '', 'Files:', ...files, '', 'Dependencies:', ...dependencies].join('\n')
146
+ }
147
+
148
+ const init_dry_run_report = { changed_files, change_kind, package_json_lines, format_report }
149
+ export { init_dry_run_report }
150
+ export type { ChangeKind, FileChange }