@joshuafolkken/kit 1.1094.0 → 1.1136.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 (177) hide show
  1. package/.claude/agents/investigator.md +17 -0
  2. package/.claude/settings.json +1 -0
  3. package/.claude/skills/workflow-commands/SKILL.md +1 -1
  4. package/.claude/skills/workflow-commands/background-commands.md +36 -85
  5. package/.claude/skills/workflow-commands/backlogrun-progress.md +31 -34
  6. package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -2
  7. package/.claude/skills/workflow-commands/delegation.md +5 -1
  8. package/.claude/skills/workflow-commands/fullrun.md +5 -4
  9. package/.claude/skills/workflow-commands/halfrun.md +5 -4
  10. package/.claude/skills/workflow-commands/latest-gate.md +7 -15
  11. package/.claude/skills/workflow-commands/pre-gate-cut.md +4 -8
  12. package/.claude/skills/workflow-commands/split-assessment.md +4 -18
  13. package/.github/actions/setup-node/action.yml +33 -0
  14. package/.github/actions/setup-pnpm/action.yml +4 -4
  15. package/.github/workflows/auto-tag.yml +2 -3
  16. package/.github/workflows/ci.yml +7 -8
  17. package/.github/workflows/dependabot-auto-merge.yml +1 -0
  18. package/.github/workflows/pr-classification.yml +1 -3
  19. package/.github/workflows/production.yml +1 -0
  20. package/.github/workflows/publish.yml +10 -9
  21. package/.github/workflows/sonar-qube.yml +17 -13
  22. package/.pnpmfile.mjs +29 -0
  23. package/AGENTS.md +4 -11
  24. package/CLAUDE.md +24 -17
  25. package/GEMINI.md +4 -13
  26. package/README.md +8 -3
  27. package/SECURITY.md +2 -0
  28. package/cspell/index.yaml +4 -0
  29. package/dist/CLAUDE.basic.md +6 -6
  30. package/dist/CLAUDE.md +24 -17
  31. package/dist/CLAUDE.static.md +6 -6
  32. package/dist/config-merge/index.js +33 -9
  33. package/dist/hooks/batch-guard.js +4 -4
  34. package/dist/hooks/{chunk-OIZUHPJI.js → chunk-FO2I7UT7.js} +1 -1
  35. package/dist/hooks/{chunk-NKOG3XAV.js → chunk-GDU26K3J.js} +11 -3
  36. package/dist/hooks/{chunk-O4LL4HTI.js → chunk-HANSKHES.js} +2 -2
  37. package/dist/hooks/{chunk-3C4YCCEF.js → chunk-HBKZLITF.js} +4 -4
  38. package/dist/hooks/{chunk-UD24EAIL.js → chunk-JKKTVDAG.js} +1984 -146
  39. package/dist/hooks/{chunk-M2XDCUKD.js → chunk-KBXZLMRH.js} +89 -1803
  40. package/dist/hooks/{chunk-IL27YTPH.js → chunk-KOKOAV6D.js} +2 -2
  41. package/dist/hooks/{chunk-7ZPFAVR7.js → chunk-LJBLGHEH.js} +59 -19
  42. package/dist/hooks/{chunk-E54KGFEQ.js → chunk-NFZGFGOR.js} +1 -1
  43. package/dist/hooks/{chunk-KHWHKJOM.js → chunk-RFWWUI5F.js} +4 -4
  44. package/dist/hooks/codex-hook-adapter.js +12 -47
  45. package/dist/hooks/duplicate-read-guard.js +5 -5
  46. package/dist/hooks/format-edited.js +2 -2
  47. package/dist/hooks/inputs.json +175 -173
  48. package/dist/hooks/investigation-guard.js +5 -5
  49. package/dist/hooks/pretool-guard.js +9 -9
  50. package/dist/hooks/stop-guard.js +17 -12
  51. package/dist/josh.js +87 -31
  52. package/dist/version/index.js +36 -20
  53. package/eslint/rules/unicorn.js +3 -2
  54. package/package.json +23 -5
  55. package/pnpm-workspace.yaml +5 -7
  56. package/prompts/collaboration-workflow/glossary.md +17 -0
  57. package/prompts/collaboration-workflow/issue-template.md +1 -1
  58. package/prompts/collaboration-workflow/operating-rules.md +1 -1
  59. package/prompts/collaboration-workflow/principles.md +9 -0
  60. package/prompts/collaboration-workflow.md +1 -0
  61. package/scripts/backlog/backlog-arrival.ts +30 -10
  62. package/scripts/backlog/backlog-budget-cli.ts +15 -16
  63. package/scripts/backlog/backlog-offer-cli.ts +6 -14
  64. package/scripts/backlog/backlog-plan.ts +29 -18
  65. package/scripts/backlog/backlog-ready.ts +20 -7
  66. package/scripts/config-merge/parse-jsonc.ts +33 -3
  67. package/scripts/config-merge/patch-json-key.ts +13 -6
  68. package/scripts/cost-runtime/cost-cli.ts +3 -7
  69. package/scripts/delegation/delegation-policy.ts +9 -1
  70. package/scripts/delegation/investigation-guard.ts +1 -1
  71. package/scripts/delegation/investigation-reads.ts +105 -19
  72. package/scripts/doctor/doctor-consumer.ts +3 -5
  73. package/scripts/doctor/doctor-io.ts +9 -13
  74. package/scripts/document/backlogrun-parent-read-set.ts +12 -1
  75. package/scripts/document/capped-print.ts +74 -0
  76. package/scripts/document/document-byte-budget.ts +1 -0
  77. package/scripts/document/document-section-cli.ts +4 -3
  78. package/scripts/document/entry-read-budget.ts +13 -7
  79. package/scripts/document/entry-read-set.ts +32 -8
  80. package/scripts/document/lane-child-read-set.ts +7 -0
  81. package/scripts/document/read-set-point-of-use.ts +8 -3
  82. package/scripts/document/read-set-trim.ts +3 -1
  83. package/scripts/epic/epic-audit.ts +3 -18
  84. package/scripts/gate/scoped-green.ts +25 -14
  85. package/scripts/gh/git-gh-issue-list.ts +12 -11
  86. package/scripts/gh/git-gh-issue-write.ts +4 -1
  87. package/scripts/gh/git-gh-pr-auto-merge.ts +4 -1
  88. package/scripts/git/git-command.ts +6 -1
  89. package/scripts/git/git-spawn-sync.ts +65 -0
  90. package/scripts/git/git-spawn.ts +5 -4
  91. package/scripts/git/stash/stash-pop-args.ts +1 -6
  92. package/scripts/init/claude-kit-only-settings.ts +29 -0
  93. package/scripts/init/init-copy-content.ts +8 -2
  94. package/scripts/init/init-file-action.ts +54 -0
  95. package/scripts/init/init-logic-migrate.ts +35 -17
  96. package/scripts/init/init-logic-secretlint.ts +1 -1
  97. package/scripts/init/init-logic.ts +6 -22
  98. package/scripts/init/init.ts +11 -68
  99. package/scripts/init/initial-commit.ts +24 -0
  100. package/scripts/init/kit-written-paths.ts +2 -0
  101. package/scripts/init/project-profile.ts +3 -6
  102. package/scripts/init/start-exec.ts +16 -3
  103. package/scripts/init/start-steps.ts +10 -13
  104. package/scripts/issue/defect-rate-cli.ts +61 -20
  105. package/scripts/issue/issue-backlinks.ts +7 -14
  106. package/scripts/issue/issue-cite.ts +12 -6
  107. package/scripts/issue/issue-file.ts +5 -12
  108. package/scripts/issue/issue-read-cli.ts +1 -0
  109. package/scripts/issue/issue-report-failures.ts +6 -2
  110. package/scripts/issue/issue-state-cli.ts +1 -1
  111. package/scripts/issue/markdown-section.ts +21 -6
  112. package/scripts/issue/measure-rerun-cli.ts +73 -14
  113. package/scripts/josh/josh-commands-lane.ts +1 -1
  114. package/scripts/josh/josh-commands-project.ts +8 -1
  115. package/scripts/josh/josh-commands-workflow.ts +1 -1
  116. package/scripts/josh/run-github-prerequisite.ts +3 -6
  117. package/scripts/lane/lane-await.ts +89 -15
  118. package/scripts/lane/lane-cli.ts +11 -13
  119. package/scripts/lane/lane-close.ts +4 -1
  120. package/scripts/lane/lane-launch-cli.ts +10 -16
  121. package/scripts/lane/openai-review-broker.ts +4 -1
  122. package/scripts/lib/cli-flags.ts +31 -2
  123. package/scripts/lib/error-message.ts +12 -1
  124. package/scripts/lines/line-targets.ts +0 -11
  125. package/scripts/lines/lines-command.ts +5 -3
  126. package/scripts/propagate/propagate-git.ts +3 -8
  127. package/scripts/refactor/refactor-scan.ts +2 -1
  128. package/scripts/registry-migration/migrate-logic.ts +28 -19
  129. package/scripts/registry-migration/migrate.ts +6 -5
  130. package/scripts/release/github-release-cli.ts +14 -3
  131. package/scripts/release/github-release-environment.ts +1 -0
  132. package/scripts/release/github-release.ts +103 -17
  133. package/scripts/release/release-publish.ts +36 -2
  134. package/scripts/release/release-worktree.ts +97 -2
  135. package/scripts/review/review-checkout.ts +3 -2
  136. package/scripts/review/review-record-cli.ts +9 -16
  137. package/scripts/rules/early-heartbeat.ts +4 -3
  138. package/scripts/rules/lane-park.ts +9 -0
  139. package/scripts/rules/stop-rules.ts +7 -4
  140. package/scripts/run/run-carry-args.ts +8 -12
  141. package/scripts/run/run-carry-cli.ts +3 -1
  142. package/scripts/run/run-carry-conversation.ts +94 -0
  143. package/scripts/run/run-carry-reclaim.ts +37 -0
  144. package/scripts/run/run-carry.ts +21 -5
  145. package/scripts/run/run-cut-args.ts +1 -6
  146. package/scripts/run/run-ending-cli.ts +1 -5
  147. package/scripts/run/run-entry-cli.ts +40 -9
  148. package/scripts/run/run-entry-stop.ts +104 -0
  149. package/scripts/run/run-event-stream-emit.ts +7 -2
  150. package/scripts/run/run-hold.ts +4 -1
  151. package/scripts/run/run-liveness-cli.ts +1 -5
  152. package/scripts/run/run-merge-cli.ts +2 -10
  153. package/scripts/run/run-merge-steps.ts +4 -1
  154. package/scripts/run/run-parent-cut-hook.ts +4 -1
  155. package/scripts/run/run-prep-cli.ts +4 -1
  156. package/scripts/run/run-progress-args.ts +20 -4
  157. package/scripts/run/run-progress-cli.ts +31 -29
  158. package/scripts/run/run-progress-clock.ts +4 -1
  159. package/scripts/run/run-ship-cli.ts +8 -16
  160. package/scripts/run/run-ship-probe.ts +4 -1
  161. package/scripts/run/run-status-cli.ts +2 -15
  162. package/scripts/run/run-tidy-lanes.ts +4 -1
  163. package/scripts/run/run-wake-cli.ts +2 -10
  164. package/scripts/run/run-watcher-guard.ts +9 -5
  165. package/scripts/safe-chain/pack-hook.ts +92 -0
  166. package/scripts/sync/sync-ai-files.ts +4 -0
  167. package/scripts/test/test-declared-changed.ts +13 -16
  168. package/scripts/test/test-declared.ts +3 -7
  169. package/scripts/test/test-related.ts +1 -6
  170. package/scripts/test/test-unit-guard.ts +21 -7
  171. package/scripts/time-runtime/time-spans.ts +4 -0
  172. package/scripts/version/version-commands.ts +11 -1
  173. package/templates/CLAUDE.basic.md +6 -6
  174. package/templates/workflows/ci.yml +16 -82
  175. package/templates/workflows/dependabot-auto-merge.yml +1 -0
  176. package/templates/workflows/github-release.yml +14 -28
  177. package/tsconfig.sonar.json +4 -22
@@ -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
@@ -1,3 +1,4 @@
1
+ import { bounded_pool } from '#scripts/lib/bounded-pool'
1
2
  import { git_gh_api_path } from './git-gh-api-path'
2
3
  import { git_gh_exec } from './git-gh-exec'
3
4
  import { read_blocked_by } from './git-gh-issue-read'
@@ -198,21 +199,21 @@ async function fetch_selected(request: IssueListRequest): Promise<SelectedRows>
198
199
  // issue GitHub itself reports as unblocked costs no request. A backlog with no declared blockers
199
200
  // therefore costs exactly what `gh` cost — one listing — rather than one request per row.
200
201
  //
201
- // Sequential rather than a burst: the only caller that asks for the field is the `auto-ok` pickup,
202
- // whose listing is already narrowed by a label, and a rate limit reached in parallel would fail rows
203
- // that a paced pass reads.
202
+ // Bounded rather than sequential (joshuafolkken/kit#3103): the pass was paced one request at a time,
203
+ // which made every `backlog:plan` wait about 0.7 s per opted-in issue declaring a blocker. The width
204
+ // is the one `epic:bundle` already reads the same endpoint at, so the burst stays bounded while a
205
+ // rate-limited read still fails the whole listing rather than a row (`bounded-pool.ts`).
206
+ const RELATION_CONCURRENCY = 8
207
+
204
208
  async function read_relations(
205
209
  rows: ReadonlyArray<RestIssue>,
206
210
  repo?: string,
207
211
  ): Promise<Array<BlockedBy>> {
208
- const relations: Array<BlockedBy> = []
209
-
210
- for (const row of rows) {
211
- // eslint-disable-next-line no-await-in-loop -- paced reads, so a rate limit does not fail rows
212
- relations.push(await read_blocked_by(String(row.number), row, repo))
213
- }
214
-
215
- return relations
212
+ return await bounded_pool.bounded_map(
213
+ rows,
214
+ RELATION_CONCURRENCY,
215
+ async (row) => await read_blocked_by(String(row.number), row, repo),
216
+ )
216
217
  }
217
218
 
218
219
  // The listing, plus what it could not cover. `json` is `undefined` — never `'[]'` — when the listing
@@ -1,3 +1,4 @@
1
+ import { error_text } from '#scripts/lib/error-message'
1
2
  import { git_gh_api_path } from './git-gh-api-path'
2
3
  import { git_gh_exec, type GhApiRequest } from './git-gh-exec'
3
4
 
@@ -43,7 +44,9 @@ async function did_write_succeed(write: () => Promise<unknown>): Promise<boolean
43
44
  await write()
44
45
 
45
46
  return true
46
- } catch {
47
+ } catch (error) {
48
+ error_text.trace_swallowed('git_gh_issue_write.did_write_succeed', error)
49
+
47
50
  return false
48
51
  }
49
52
  }
@@ -1,3 +1,4 @@
1
+ import { error_text } from '#scripts/lib/error-message'
1
2
  import { git_gh_api_path } from './git-gh-api-path'
2
3
  import { git_gh_exec } from './git-gh-exec'
3
4
  import { read_pull, require_pr_number } from './git-gh-pr-read'
@@ -40,7 +41,9 @@ async function pr_enable_auto_merge(branch_name: string): Promise<void> {
40
41
  async function pr_is_merged(branch_name: string): Promise<boolean> {
41
42
  try {
42
43
  return git_gh_pr_rest.is_merged(await read_pull(await require_pr_number(branch_name)))
43
- } catch {
44
+ } catch (error) {
45
+ error_text.trace_swallowed('git_gh_pr_auto_merge.pr_is_merged', error)
46
+
44
47
  return false
45
48
  }
46
49
  }
@@ -1,6 +1,8 @@
1
1
  import { PORCELAIN_FLAG, UNTRACKED_FILES_FLAG } from './constants'
2
+ import { create_spawn_error } from './git-execa-error'
2
3
  import { git_push_transport } from './git-push-transport'
3
4
  import { git_spawn } from './git-spawn'
5
+ import { git_spawn_sync } from './git-spawn-sync'
4
6
 
5
7
  async function branch(): Promise<string> {
6
8
  return await git_spawn.read(['rev-parse', '--abbrev-ref', 'HEAD'])
@@ -31,7 +33,10 @@ async function status(directory?: string): Promise<string> {
31
33
  // onto the working directory resolves to nothing, and a digest map built that way collapses to
32
34
  // "every file absent" — which compares *equal* to another such map (joshuafolkken/kit#1241).
33
35
  async function repository_root(): Promise<string> {
34
- return await git_spawn.read(['rev-parse', '--show-toplevel'])
36
+ const result = git_spawn_sync.toplevel()
37
+ if (result.exit_code !== 0) throw create_spawn_error('rev-parse', result.exit_code)
38
+
39
+ return result.stdout.trim()
35
40
  }
36
41
 
37
42
  // The commit this checkout is sitting on, as opposed to `change_base_commit`'s commit a change is
@@ -0,0 +1,65 @@
1
+ import { read_spawn_stderr, read_spawn_stdout } from '#scripts/lib/spawn-exit'
2
+ import { execaSync } from 'execa'
3
+ import { git_utilities } from './constants'
4
+
5
+ // The one place a synchronous git command is spawned from (joshuafolkken/kit#3065) — the sync
6
+ // counterpart of `git-spawn.ts`. Every caller here wants an answer rather than an exception, so a
7
+ // non-zero exit is returned as a result, and every call is bounded: a git that never answers is an
8
+ // unknown the caller already handles, never a hang.
9
+
10
+ const GIT_TIMEOUT_MS = 10_000
11
+ const SUCCESS_EXIT_CODE = 0
12
+ // git translates its messages, so a caller matching stderr would fail under any non-English locale.
13
+ const C_LOCALE: Record<string, string> = { LC_ALL: 'C', LANGUAGE: 'C' }
14
+
15
+ interface GitSyncOptions {
16
+ cwd?: string
17
+ timeout?: number
18
+ env?: Record<string, string | undefined>
19
+ }
20
+
21
+ interface GitSyncResult {
22
+ exit_code: number | undefined
23
+ stdout: string
24
+ stderr: string
25
+ }
26
+
27
+ function run(args: ReadonlyArray<string>, options: GitSyncOptions = {}): GitSyncResult {
28
+ // execa runs the binary directly with an argument array and no `shell` option; the git command and
29
+ // args are internally controlled, never untrusted input. tssecurity:S8705 is a false positive here.
30
+ const result = execaSync(git_utilities.get_git_command_for_spawn(), args, {
31
+ ...options,
32
+ extendEnv: true,
33
+ reject: false,
34
+ timeout: options.timeout ?? GIT_TIMEOUT_MS,
35
+ }) // NOSONAR
36
+
37
+ return {
38
+ exit_code: result.exitCode,
39
+ stdout: read_spawn_stdout(result),
40
+ stderr: read_spawn_stderr(result),
41
+ }
42
+ }
43
+
44
+ // The trimmed output of a command that succeeded, or undefined for any failure.
45
+ function read(args: ReadonlyArray<string>, options: GitSyncOptions = {}): string | undefined {
46
+ const result = run(args, options)
47
+
48
+ return result.exit_code === SUCCESS_EXIT_CODE ? result.stdout.trim() : undefined
49
+ }
50
+
51
+ // The configured `origin` URL, or undefined when the repository has none.
52
+ function origin_url(cwd: string): string | undefined {
53
+ return read(['config', '--get', 'remote.origin.url'], { cwd })
54
+ }
55
+
56
+ // The absolute repository (or worktree) root, asked under the C locale so a caller can tell "not a
57
+ // repository" apart from every other failure by its stderr.
58
+ function toplevel(timeout: number = GIT_TIMEOUT_MS): GitSyncResult {
59
+ return run(['rev-parse', '--show-toplevel'], { env: C_LOCALE, timeout })
60
+ }
61
+
62
+ const git_spawn_sync = { GIT_TIMEOUT_MS, C_LOCALE, run, read, origin_url, toplevel }
63
+
64
+ export { git_spawn_sync }
65
+ export type { GitSyncResult }
@@ -18,10 +18,11 @@ import { git_ssh_keepalive } from './git-ssh-keepalive'
18
18
  // **`git-push-transport.ts` is the one deliberate exception among the command modules** and is not a
19
19
  // second helper of this kind: a push needs a transport-fault retry that no other command has, so it
20
20
  // spawns git itself. The timeout it carries is shared here by `read_remote` / `with_output_remote`. Those two files are the spawn sites of the ordinary command
21
- // modules — the claim stops there. Other parts of this package spawn git for their own purposes
22
- // (`scripts/run/run-progress-clock.ts`, `scripts/propagate/propagate-git.ts`,
23
- // `scripts/doctor/doctor-io.ts`, `scripts/git/git-fixture-workspace.ts`), and an audit of how the git
24
- // binary is resolved has to read those too.
21
+ // modules — the claim stops there. A synchronous caller goes through `git-spawn-sync.ts`, the same
22
+ // binary resolution with a result instead of an exception (joshuafolkken/kit#3065). Other parts of
23
+ // this package spawn git for their own purposes (`scripts/run/run-progress-clock.ts`,
24
+ // `scripts/git/git-fixture-workspace.ts`), and an audit of how the git binary is resolved has to read
25
+ // those too.
25
26
 
26
27
  async function read(arguments_: Array<string>): Promise<string> {
27
28
  const git_cmd = git_utilities.get_git_command_for_spawn()
@@ -21,12 +21,7 @@ interface Request {
21
21
  }
22
22
 
23
23
  function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
24
- return cli_flags.parse_or_undefined({
25
- args: [...argv],
26
- options: OPTIONS,
27
- strict: true,
28
- allowPositionals: true,
29
- })
24
+ return cli_flags.arguments_of(argv, OPTIONS)
30
25
  }
31
26
 
32
27
  // The message is the sole positional and must be a non-empty string: an empty message matches every
@@ -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 }
@@ -1,6 +1,7 @@
1
1
  import { managed_marker_logic } from '#scripts/managed-marker/managed-marker-logic'
2
2
  import { workflow_pin_logic } from '#scripts/sync/workflow-pin-logic'
3
3
  import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
4
+ import { claude_kit_only_settings } from './claude-kit-only-settings'
4
5
  import { claude_plugin_config } from './claude-plugin-config'
5
6
  import { hook_command_rewrite } from './hook-command-rewrite'
6
7
  import { init_logic } from './init-logic'
@@ -19,7 +20,8 @@ import { init_logic } from './init-logic'
19
20
  * workflow-pin-logic.ts for why the template refs are not authoritative, and
20
21
  * managed-marker-logic.ts for why the stamp is on the file rather than in a list.
21
22
  *
22
- * The consumer's `.claude/settings.json` gets two further passes. The kit plugin's marketplace and
23
+ * The consumer's `.claude/settings.json` gets three further passes. The keys kit sets only for its
24
+ * own sessions are removed (joshuafolkken/kit#3140, see claude-kit-only-settings.ts). The kit plugin's marketplace and
23
25
  * `enabledPlugins` declaration is injected, because the skills that used to be copied now ship as the
24
26
  * `kit` plugin and load from that declaration (joshuafolkken/kit#1879). And its hook commands are
25
27
  * rewritten from `pnpm josh` to the published bundle invoked directly with node
@@ -36,10 +38,14 @@ function transform_copied_content(destination_path: string, content: string): st
36
38
  with_pins,
37
39
  KIT_PACKAGE_NAME,
38
40
  )
39
- const with_plugin = claude_plugin_config.apply_plugin_config_for_destination(
41
+ const with_kit_only_stripped = claude_kit_only_settings.apply_kit_only_strip_for_destination(
40
42
  destination_path,
41
43
  with_marker,
42
44
  )
45
+ const with_plugin = claude_plugin_config.apply_plugin_config_for_destination(
46
+ destination_path,
47
+ with_kit_only_stripped,
48
+ )
43
49
 
44
50
  return hook_command_rewrite.apply_hook_command_rewrite_for_destination(
45
51
  destination_path,
@@ -0,0 +1,54 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { FileAction } from './init-actions'
4
+
5
+ const SAMPLE_INDENT_WIDTH = 4
6
+ const SAMPLE_INDENT = ' '.repeat(SAMPLE_INDENT_WIDTH)
7
+
8
+ function write_new_file(action: FileAction, destination_path: string): void {
9
+ mkdirSync(path.dirname(destination_path), { recursive: true })
10
+ writeFileSync(destination_path, action.create())
11
+ console.info(` ✔ created ${action.dest}`)
12
+ }
13
+
14
+ function show_sample(action: FileAction): void {
15
+ console.info(` ⚠ exists ${action.dest} — add manually:`)
16
+ console.info('')
17
+ console.info(action.create().replaceAll(/^/gmu, () => SAMPLE_INDENT))
18
+ }
19
+
20
+ // A file that already holds the sample needs nothing added, so only one that differs gets it shown —
21
+ // for a create-only file (no merge) as for a merge that only migrates a kit-written line (#3069).
22
+ function report_unchanged(action: FileAction, existing: string): void {
23
+ const is_sample_missing = existing !== action.create()
24
+ const should_show_sample =
25
+ action.merge === undefined || action.should_show_sample_when_unchanged === true
26
+
27
+ if (is_sample_missing && should_show_sample) show_sample(action)
28
+ else console.info(` ✔ unchanged ${action.dest}`)
29
+ }
30
+
31
+ function merge_existing_file(action: FileAction, destination_path: string): void {
32
+ const existing = readFileSync(destination_path, 'utf8')
33
+ const merged = action.merge === undefined ? existing : action.merge(existing)
34
+
35
+ if (merged === existing) {
36
+ report_unchanged(action, existing)
37
+
38
+ return
39
+ }
40
+
41
+ writeFileSync(destination_path, merged)
42
+ console.info(` ✔ updated ${action.dest}`)
43
+ }
44
+
45
+ function execute_file_action(action: FileAction, project_root: string): void {
46
+ const destination_path = path.join(project_root, action.dest)
47
+
48
+ if (existsSync(destination_path)) merge_existing_file(action, destination_path)
49
+ else write_new_file(action, destination_path)
50
+ }
51
+
52
+ const init_file_action = { execute_file_action }
53
+
54
+ export { init_file_action }
@@ -4,6 +4,27 @@ const PREINSTALL_KEY = 'preinstall'
4
4
  const JF_PREFIX = 'jf-'
5
5
  const JOSH_PREFIX = 'josh '
6
6
 
7
+ // The plain values kit wrote before its scripts became josh subcommands (#80). `check` is left out:
8
+ // its value is the one `sv create` writes itself, so it cannot be told apart from the project's own.
9
+ const PRE_JOSH_SCRIPT_VALUES: ReadonlyMap<string, string> = new Map([
10
+ ['lint', 'pnpm lint:prettier && pnpm lint:eslint'],
11
+ ['lint:prettier', 'prettier --check .'],
12
+ ['lint:eslint', 'eslint . --cache --cache-strategy content'],
13
+ ['format', 'pnpm format:prettier && pnpm format:eslint'],
14
+ ['format:prettier', 'prettier --write .'],
15
+ ['format:eslint', 'eslint . --fix --cache --cache-strategy content'],
16
+ ['cspell', 'cspell lint --no-must-find-files --no-progress "**/*.{ts,js,md,yaml,yml,json}"'],
17
+ ['cspell:dot', 'pnpm cspell . --dot'],
18
+ ['test:unit', 'vitest run'],
19
+ ['lefthook:install', 'lefthook install'],
20
+ ['lefthook:uninstall', 'lefthook uninstall'],
21
+ ['lefthook:commit', 'lefthook run pre-commit'],
22
+ ['lefthook:push', 'lefthook run pre-push'],
23
+ ['main:sync', 'git checkout main && git pull'],
24
+ ['main:merge', 'git pull origin main'],
25
+ ['check:ci', 'svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --threshold error'],
26
+ ])
27
+
7
28
  const RETIRED_MANAGED_SCRIPTS = new Set<string>([
8
29
  'git',
9
30
  'git:followup',
@@ -18,30 +39,15 @@ const RETIRED_MANAGED_SCRIPTS = new Set<string>([
18
39
  'version:patch',
19
40
  'version:current',
20
41
  'overrides:check',
21
- 'lint',
22
- 'lint:prettier',
23
- 'lint:eslint',
24
- 'format',
25
- 'format:prettier',
26
- 'format:eslint',
27
- 'cspell',
28
- 'cspell:dot',
29
- 'test:unit',
30
42
  'test:e2e',
31
43
  'test',
32
- 'lefthook:install',
33
- 'lefthook:uninstall',
34
- 'lefthook:commit',
35
- 'lefthook:push',
36
- 'main:sync',
37
- 'main:merge',
38
44
  'latest',
39
45
  'latest:corepack',
40
46
  'latest:update',
41
47
  'check',
42
- 'check:ci',
43
48
  'check:svelte',
44
49
  'check:svelte:ci',
50
+ ...PRE_JOSH_SCRIPT_VALUES.keys(),
45
51
  ])
46
52
 
47
53
  function migrate_jf_value(value: string): string {
@@ -62,9 +68,21 @@ function apply_jf_migrations(scripts: Record<string, string>): Record<string, st
62
68
  )
63
69
  }
64
70
 
71
+ function is_kit_written(key: string, value: string): boolean {
72
+ if (value.startsWith(JOSH_PREFIX) || value.startsWith(JF_PREFIX)) return true
73
+
74
+ return PRE_JOSH_SCRIPT_VALUES.get(key) === value
75
+ }
76
+
77
+ // A retired name alone does not make a script kit's: `sv create` writes its own `check`, which must
78
+ // survive `josh init` (joshuafolkken/kit#3069). Only a value kit itself wrote is retired.
79
+ function is_retired_script(key: string, value: string): boolean {
80
+ return RETIRED_MANAGED_SCRIPTS.has(key) && is_kit_written(key, value)
81
+ }
82
+
65
83
  function remove_retired_scripts(scripts: Record<string, string>): Record<string, string> {
66
84
  return Object.fromEntries(
67
- Object.entries(scripts).filter(([key]) => !RETIRED_MANAGED_SCRIPTS.has(key)),
85
+ Object.entries(scripts).filter(([key, value]) => !is_retired_script(key, value)),
68
86
  )
69
87
  }
70
88
 
@@ -5,7 +5,7 @@ const SECRETLINT_RULE_PRESET = '@secretlint/secretlint-rule-preset-recommend'
5
5
 
6
6
  // secretlint resolves both its CLI and every rule package from the project it runs in, not
7
7
  // transitively through the kit — the same constraint that forces the prettier plugins into
8
- // consumer devDependencies (see PRETTIER_PLUGIN_DEV_DEPS in init-logic.ts). Omitting either
8
+ // consumer devDependencies (as kit peers, see init.ts). Omitting either
9
9
  // entry makes the pre-commit hook fail with "Cannot find module". Versions mirror the kit's
10
10
  // own devDependencies.
11
11
  const SECRETLINT_DEV_DEPS: Record<string, string> = {
@@ -92,10 +92,13 @@ const AI_COPY_FILES: ReadonlyArray<string> = [
92
92
  '.github/workflows/production.yml',
93
93
  '.github/workflows/sonar-qube.yml',
94
94
  '.github/workflows/pr-classification.yml',
95
- // Called by `pr-classification.yml` as `uses: ./.github/actions/setup-pnpm`; without it the
96
- // consumer's job cannot resolve the action and the Release classification check fails
97
- // (joshuafolkken/kit#3013).
95
+ // Called by `ci.yml` and `pr-classification.yml` as `uses: ./.github/actions/setup-pnpm`; without
96
+ // it the consumer's jobs cannot resolve the action and their checks fail (joshuafolkken/kit#3013,
97
+ // joshuafolkken/kit#3095).
98
98
  '.github/actions/setup-pnpm/action.yml',
99
+ // The one place the workflows' Node.js version is written, called by every distributed workflow
100
+ // that sets Node.js up (joshuafolkken/kit#3084).
101
+ '.github/actions/setup-node/action.yml',
99
102
  '.github/pull_request_template.md',
100
103
  '.github/release.yml',
101
104
  '.github/dependabot.yml',
@@ -220,20 +223,6 @@ const SUGGESTED_SCRIPTS_COMMON: Record<string, string> = {
220
223
  josh: 'josh',
221
224
  }
222
225
 
223
- // prettier resolves its `plugins[]` from the consumer project, not transitively through the kit,
224
- // so every package that uses the kit prettier preset must declare these as devDependencies. The
225
- // preset references all three unconditionally (prettier/index.js → plugins), hence all three are
226
- // added regardless of project — omitting any breaks `prettier`/`josh lint` with a
227
- // "Cannot find package" error. Versions mirror the kit's own devDependencies.
228
- const SORT_IMPORTS_PLUGIN_KEY = '@ianvs/prettier-plugin-sort-imports'
229
- const PRETTIER_SVELTE_PLUGIN_KEY = 'prettier-plugin-svelte'
230
- const PRETTIER_TAILWIND_PLUGIN_KEY = 'prettier-plugin-tailwindcss'
231
- const PRETTIER_PLUGIN_DEV_DEPS: Record<string, string> = {
232
- [SORT_IMPORTS_PLUGIN_KEY]: '^4.7.1',
233
- [PRETTIER_SVELTE_PLUGIN_KEY]: '^4.1.1',
234
- [PRETTIER_TAILWIND_PLUGIN_KEY]: '^0.8.0',
235
- }
236
-
237
226
  // format_json rather than JSON.stringify: it lays arrays out the way prettier does — inline while
238
227
  // they fit within printWidth, one element per line once they do not — so the file kit writes is
239
228
  // `prettier --check`-clean in the consumer whatever the entry count happens to be (#660).
@@ -433,10 +422,6 @@ function strip_managed_postinstall(content: string): string {
433
422
  )
434
423
  }
435
424
 
436
- function merge_prettier_plugin_development_deps(content: string): string {
437
- return init_logic_json_merge.merge_development_dependencies(content, PRETTIER_PLUGIN_DEV_DEPS)
438
- }
439
-
440
425
  const init_logic = {
441
426
  ...init_logic_templates,
442
427
  ...init_logic_workspace,
@@ -454,7 +439,6 @@ const init_logic = {
454
439
  generate_npmrc,
455
440
  merge_npmrc,
456
441
  merge_gitignore,
457
- merge_prettier_plugin_development_deps,
458
442
  get_tsconfig_extends_entry,
459
443
  get_tsconfig_preset_filename,
460
444
  get_lefthook_extends_value,
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env tsx
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { resolve_local_bin } from '#scripts/build/local-bin'
@@ -7,6 +7,7 @@ import { doctor_io } from '#scripts/doctor/doctor-io'
7
7
  import { error_text } from '#scripts/lib/error-message'
8
8
  import { package_version_schema, with_package_manager_schema } from '#scripts/lib/schemas'
9
9
  import { auto_merge_setting } from '#scripts/repo/auto-merge-setting'
10
+ import { pack_hook } from '#scripts/safe-chain/pack-hook'
10
11
  import { project_config } from '#scripts/safe-chain/project-config'
11
12
  import { security_updates } from '#scripts/security/security-updates'
12
13
  import { did_refuse_self_run } from '#scripts/self-sync-guard/self-sync-refusal'
@@ -14,10 +15,11 @@ import { sync } from '#scripts/sync/sync'
14
15
  import { package_manager_version } from '#scripts/version/package-manager-version'
15
16
  import { execaSync } from 'execa'
16
17
  import { z } from 'zod'
17
- import { init_actions, PRETTIER_CONFIG_JS, type FileAction } from './init-actions'
18
+ import { init_actions, PRETTIER_CONFIG_JS } from './init-actions'
18
19
  import { init_ai_copy } from './init-ai-copy'
19
20
  import { init_basic } from './init-basic'
20
21
  import { init_bootstrap } from './init-bootstrap'
22
+ import { init_file_action } from './init-file-action'
21
23
  import { init_install } from './init-install'
22
24
  import { init_logic } from './init-logic'
23
25
  import { PACKAGE_DIR, PROJECT_ROOT } from './init-paths'
@@ -27,69 +29,10 @@ import { project_profile, type ProjectProfile, type ProjectShape } from './proje
27
29
  const PACKAGE_JSON = 'package.json'
28
30
  const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
29
31
  const LEFTHOOK_BIN = 'lefthook'
30
- const SAMPLE_INDENT_WIDTH = 4
31
32
  const ARGUMENT_START_INDEX = 2
32
- const SAMPLE_INDENT = ' '.repeat(SAMPLE_INDENT_WIDTH)
33
33
  const INSTALL_HINT = '→ run `pnpm install` to install what package.json lists'
34
34
  const INIT_USAGE = 'josh init [--profile basic|full] [--no-install]'
35
35
 
36
- function write_new_file(action: FileAction, destination_path: string): void {
37
- mkdirSync(path.dirname(destination_path), { recursive: true })
38
- writeFileSync(destination_path, action.create())
39
- console.info(` ✔ created ${action.dest}`)
40
- }
41
-
42
- function show_sample(action: FileAction): void {
43
- console.info(` ⚠ exists ${action.dest} — add manually:`)
44
- console.info('')
45
- console.info(action.create().replaceAll(/^/gmu, () => SAMPLE_INDENT))
46
- }
47
-
48
- // A file that already holds the sample needs nothing added, so only one that differs gets it shown.
49
- function report_unchanged(action: FileAction, existing: string): void {
50
- const is_sample_missing = existing !== action.create()
51
-
52
- if (is_sample_missing && action.should_show_sample_when_unchanged === true) show_sample(action)
53
- else console.info(` ✔ unchanged ${action.dest}`)
54
- }
55
-
56
- function merge_existing_file(
57
- merge_function: (existing: string) => string,
58
- destination_path: string,
59
- action: FileAction,
60
- ): void {
61
- const existing = readFileSync(destination_path, 'utf8')
62
- const merged = merge_function(existing)
63
-
64
- if (merged === existing) {
65
- report_unchanged(action, existing)
66
-
67
- return
68
- }
69
-
70
- writeFileSync(destination_path, merged)
71
- console.info(` ✔ updated ${action.dest}`)
72
- }
73
-
74
- function execute_file_action(action: FileAction): void {
75
- const destination_path = path.join(PROJECT_ROOT, action.dest)
76
- const is_existing = existsSync(destination_path)
77
-
78
- if (!is_existing) {
79
- write_new_file(action, destination_path)
80
-
81
- return
82
- }
83
-
84
- if (action.merge === undefined) {
85
- show_sample(action)
86
-
87
- return
88
- }
89
-
90
- merge_existing_file(action.merge, destination_path, action)
91
- }
92
-
93
36
  const engine_pin_schema = z.looseObject({ name: z.string(), version: z.string() })
94
37
 
95
38
  const published_development_engines_schema = z.looseObject({
@@ -124,10 +67,10 @@ function get_kit_self_dependency(): Record<string, string> {
124
67
  }
125
68
 
126
69
  // Each config `init` generates runs a CLI or imports types the consumer has to resolve itself:
127
- // `prettier.config.js` → prettier, `cspell.config.yaml` → cspell, `playwright.config.ts` →
128
- // @types/node (with @playwright/test among the peers), `lefthook.yml` → lefthook. Without them the
129
- // first `josh gate` after `pnpm install` fails (joshuafolkken/kit#2710).
130
- const TOOL_DEVELOPMENT_DEPENDENCIES = ['prettier', 'cspell', '@types/node']
70
+ // `cspell.config.yaml` → cspell, `playwright.config.ts` → @types/node, `lefthook.yml` → lefthook.
71
+ // Prettier, its preset plugins and @playwright/test come in as peers. Without them the first
72
+ // `josh gate` after `pnpm install` fails (joshuafolkken/kit#2710).
73
+ const TOOL_DEVELOPMENT_DEPENDENCIES = ['cspell', '@types/node']
131
74
  const LEFTHOOK_DEVELOPMENT_DEPENDENCY = 'lefthook'
132
75
 
133
76
  function tool_dependency_names(has_git: boolean): ReadonlyArray<string> {
@@ -164,9 +107,8 @@ function apply_dependency_merges(content: string, has_git = true): string {
164
107
  ? suggested
165
108
  : Object.fromEntries(Object.entries(suggested).filter(([key]) => key !== 'prepare')),
166
109
  )
167
- const with_prettier = init_logic.merge_prettier_plugin_development_deps(merged)
168
110
  const with_toolchain = init_logic.merge_development_dependencies(
169
- with_prettier,
111
+ merged,
170
112
  get_toolchain_development_dependencies(has_git),
171
113
  )
172
114
  const with_secretlint = init_logic.merge_secretlint_development_deps(with_toolchain)
@@ -262,7 +204,7 @@ function run_config_file_actions(shape: ProjectShape): void {
262
204
  }
263
205
 
264
206
  for (const action of init_actions.build_file_actions(shape)) {
265
- execute_file_action(action)
207
+ init_file_action.execute_file_action(action, PROJECT_ROOT)
266
208
  }
267
209
  }
268
210
 
@@ -305,6 +247,7 @@ function initialize_project(shape: ProjectShape): void {
305
247
  merge_project_package_json(shape)
306
248
 
307
249
  run_ai_file_actions(shape)
250
+ pack_hook.sync_pack_hook(PROJECT_ROOT, PACKAGE_DIR)
308
251
  if (shape.profile === 'full') project_config.sync_project_config(PROJECT_ROOT)
309
252
  }
310
253