@joshuafolkken/kit 1.978.0 → 1.998.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 (138) hide show
  1. package/.claude/skills/epic-commands/SKILL.md +6 -5
  2. package/.claude/skills/workflow-commands/SKILL.md +9 -9
  3. package/.claude/skills/workflow-commands/background-commands.md +1 -1
  4. package/.claude/skills/workflow-commands/backlogrun-child.md +1 -1
  5. package/.claude/skills/workflow-commands/backlogrun-steps.md +1 -1
  6. package/.claude/skills/workflow-commands/fullrun-steps.md +8 -8
  7. package/.claude/skills/workflow-commands/halfrun.md +4 -5
  8. package/.claude/skills/workflow-commands/issue-comments.md +1 -1
  9. package/.claude/skills/workflow-commands/issue-scout.md +21 -22
  10. package/.claude/skills/workflow-commands/kickoff.md +8 -9
  11. package/.claude/skills/workflow-commands/observation-filing.md +9 -10
  12. package/.claude/skills/workflow-commands/prerequisite.md +7 -6
  13. package/.claude/skills/workflow-commands/retrospective.md +5 -5
  14. package/.claude/skills/workflow-commands/rule-residency.md +2 -2
  15. package/.claude/skills/workflow-commands/split-assessment.md +4 -3
  16. package/.claude/skills/workflow-commands/target-repository.md +7 -6
  17. package/.github/workflows/sonar-qube.yml +12 -2
  18. package/CLAUDE.md +2 -2
  19. package/README.md +30 -21
  20. package/dist/CLAUDE.md +2 -2
  21. package/dist/hooks/batch-guard.js +4 -4
  22. package/dist/hooks/{chunk-ZYYSBNR6.js → chunk-4GDR2YTV.js} +4 -4
  23. package/dist/hooks/{chunk-2K52BRWV.js → chunk-56LGMJJV.js} +2 -2
  24. package/dist/hooks/{chunk-BNGGEY3Z.js → chunk-762BDXDU.js} +266 -327
  25. package/dist/hooks/{chunk-Q7LE77CO.js → chunk-7WAOP42A.js} +34 -30
  26. package/dist/hooks/{chunk-NXL3SBXY.js → chunk-A2H5I2VD.js} +2 -2
  27. package/dist/hooks/{chunk-CRGJTD6E.js → chunk-AWTFKKFW.js} +1 -1
  28. package/dist/hooks/{chunk-KQG4776P.js → chunk-GBJUERYK.js} +1 -1
  29. package/dist/hooks/{chunk-BVMSPG3M.js → chunk-KPZN42RT.js} +3 -3
  30. package/dist/hooks/{chunk-ZPLXLJ7O.js → chunk-PAXMLGSS.js} +42 -8
  31. package/dist/hooks/{chunk-F6OM6SIG.js → chunk-WMIR5SNX.js} +12 -12
  32. package/dist/hooks/codex-hook-adapter.js +10 -10
  33. package/dist/hooks/duplicate-read-guard.js +5 -5
  34. package/dist/hooks/format-edited.js +2 -2
  35. package/dist/hooks/investigation-guard.js +5 -5
  36. package/dist/hooks/pretool-guard.js +9 -9
  37. package/dist/hooks/stop-guard.js +99 -27
  38. package/dist/josh.js +21 -6
  39. package/package.json +5 -1
  40. package/prettier/basic.d.ts +3 -0
  41. package/prettier/basic.js +10 -0
  42. package/prettier/index.js +2 -2
  43. package/prettier/static.js +3 -10
  44. package/prompts/collaboration-workflow/epic-bundle.md +8 -8
  45. package/prompts/collaboration-workflow/issue-template.md +5 -5
  46. package/prompts/collaboration-workflow/rule-delivery.md +43 -30
  47. package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
  48. package/prompts/collaboration-workflow/wip-cap.md +3 -3
  49. package/prompts/review.md +11 -9
  50. package/prompts/sonar-hotspot-handling.md +6 -0
  51. package/prompts/testing-guide.md +1 -1
  52. package/scripts/agent/agent-diagnostics.ts +1 -0
  53. package/scripts/agent/agent-role-profile.ts +1 -1
  54. package/scripts/behavior/behavior-rules.ts +8 -1
  55. package/scripts/build/build-claude-md.ts +13 -4
  56. package/scripts/delegation/delegation-policy.ts +9 -9
  57. package/scripts/document/document-byte-budget.ts +1 -1
  58. package/scripts/gate/gate-steps.ts +1 -1
  59. package/scripts/gate/project-checks.ts +12 -11
  60. package/scripts/gate/type-check-command.ts +1 -1
  61. package/scripts/git/git-branch.ts +14 -3
  62. package/scripts/git/git-issue.ts +13 -5
  63. package/scripts/git/git-pr.ts +16 -10
  64. package/scripts/git/git-preflight.ts +138 -0
  65. package/scripts/git/telegram-notify.ts +21 -0
  66. package/scripts/hooks/stop-guard.ts +2 -0
  67. package/scripts/init/basic-path-migration.ts +24 -0
  68. package/scripts/init/distributed-paths.ts +22 -0
  69. package/scripts/init/init-actions.ts +27 -19
  70. package/scripts/init/init-ai-copy.ts +86 -28
  71. package/scripts/init/{init-static.ts → init-basic.ts} +6 -6
  72. package/scripts/init/init-bootstrap.ts +4 -3
  73. package/scripts/init/init-install.ts +1 -1
  74. package/scripts/init/init-logic-templates.ts +1 -1
  75. package/scripts/init/init-logic.ts +1 -1
  76. package/scripts/init/init.ts +41 -19
  77. package/scripts/init/kit-setup-state.ts +30 -0
  78. package/scripts/init/kit-written-paths.ts +33 -0
  79. package/scripts/init/project-profile-cli.ts +15 -1
  80. package/scripts/init/project-profile.ts +41 -12
  81. package/scripts/init/session-language-environment.ts +83 -0
  82. package/scripts/init/start-exec.ts +28 -0
  83. package/scripts/init/start-plan.ts +35 -13
  84. package/scripts/init/start-prompt.ts +22 -8
  85. package/scripts/init/start-setup-pr.ts +138 -0
  86. package/scripts/init/start-steps.ts +30 -30
  87. package/scripts/init/start.ts +7 -3
  88. package/scripts/issue/issue-file-cli.ts +188 -0
  89. package/scripts/issue/issue-file.ts +192 -0
  90. package/scripts/issue/issue-lint-cli.ts +1 -1
  91. package/scripts/issue/issue-scout-cli.ts +41 -97
  92. package/scripts/issue/issue-scout-closed.ts +102 -0
  93. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +12 -4
  94. package/scripts/josh/josh-command-map.ts +1 -0
  95. package/scripts/josh/josh-commands-development.ts +3 -3
  96. package/scripts/josh/josh-commands-issue.ts +11 -0
  97. package/scripts/josh/josh-commands-project.ts +7 -3
  98. package/scripts/lint/cspell-cached.ts +3 -3
  99. package/scripts/lint/format.ts +1 -1
  100. package/scripts/lint/lint-parallel.ts +2 -2
  101. package/scripts/propagate/propagate-run.ts +1 -0
  102. package/scripts/propagate/propagate-select.ts +72 -0
  103. package/scripts/propagate/propagate-targets.ts +5 -3
  104. package/scripts/propagate/propagate.ts +86 -24
  105. package/scripts/rules/bash-triggers.ts +19 -6
  106. package/scripts/rules/delivered-rules.ts +28 -32
  107. package/scripts/rules/direct-filing.ts +38 -0
  108. package/scripts/rules/filing-cap.ts +12 -8
  109. package/scripts/rules/issue-citation.ts +30 -1
  110. package/scripts/rules/issue-fold.ts +4 -4
  111. package/scripts/rules/last-prompt.ts +79 -0
  112. package/scripts/rules/oracle-consulted.ts +4 -4
  113. package/scripts/rules/oracle-firing.ts +13 -16
  114. package/scripts/rules/rule-body-guard.ts +6 -9
  115. package/scripts/rules/stop-rules.ts +8 -5
  116. package/scripts/safe-chain/ci-installer-pin.ts +19 -12
  117. package/scripts/safe-chain/project-config.ts +1 -1
  118. package/scripts/sync/sync-ai-files.ts +204 -0
  119. package/scripts/sync/sync.ts +48 -184
  120. package/scripts/test/josh-harness-environment.ts +1 -1
  121. package/scripts/test/pilot-files.ts +0 -2
  122. package/scripts/test/test-declared-changed.ts +1 -1
  123. package/scripts/test/test-declared-logic.ts +55 -14
  124. package/scripts/test/test-declared.ts +23 -11
  125. package/scripts/test/test-unit-guard.ts +6 -6
  126. package/scripts/time-runtime/time-shell.ts +36 -2
  127. package/scripts/version/fresh-metadata-cache.ts +36 -0
  128. package/scripts/version/latest-update.ts +2 -1
  129. package/scripts-ai/git-workflow.ts +10 -0
  130. package/templates/CLAUDE.basic.md +8 -0
  131. package/templates/workflows/ci.yml +27 -0
  132. package/scripts/rules/issue-bug-label.ts +0 -85
  133. package/scripts/rules/issue-filing-args.ts +0 -50
  134. package/scripts/rules/issue-scout.ts +0 -64
  135. /package/{templates/CLAUDE.static.md → dist/CLAUDE.basic.md} +0 -0
  136. /package/templates/{cursorrules.static → cursorrules.basic} +0 -0
  137. /package/templates/{pnpm-workspace.static.yaml → pnpm-workspace.basic.yaml} +0 -0
  138. /package/templates/{prettierignore.static → prettierignore.basic} +0 -0
@@ -84,6 +84,18 @@ function has_same_issue_prefix(branch_a: string, branch_b: string): boolean {
84
84
  return prefix_a !== undefined && prefix_a === prefix_b
85
85
  }
86
86
 
87
+ // The one rule `check_and_create_branch` refuses on, kept pure so `git-preflight.ts` asks it before
88
+ // anything is pulled, switched or staged.
89
+ function is_mismatch(
90
+ current_branch: string,
91
+ target_branch_name: string,
92
+ default_branch: string,
93
+ ): boolean {
94
+ if (current_branch === default_branch || current_branch === target_branch_name) return false
95
+
96
+ return !has_same_issue_prefix(current_branch, target_branch_name)
97
+ }
98
+
87
99
  async function handle_default_branch(target_branch_name: string): Promise<void> {
88
100
  await pull_latest()
89
101
  const is_branch_exists: boolean = await exists(target_branch_name)
@@ -103,9 +115,7 @@ async function check_and_create_branch(
103
115
  return target_branch_name
104
116
  }
105
117
 
106
- if (current_branch !== target_branch_name) {
107
- if (has_same_issue_prefix(current_branch, target_branch_name)) return current_branch
108
-
118
+ if (is_mismatch(current_branch, target_branch_name, default_branch)) {
109
119
  git_error.display_branch_mismatch_error(current_branch, target_branch_name)
110
120
  }
111
121
 
@@ -118,6 +128,7 @@ const git_branch = {
118
128
  switch_to,
119
129
  exists,
120
130
  check_and_create_branch,
131
+ is_mismatch,
121
132
  // Exported so a lane's branch name can be checked against the real matcher rather than against a
122
133
  // second copy of the pattern (joshuafolkken/kit#1497). A copy is what let `lane/<N>` ship: nothing
123
134
  // asserted that the name `lane:open` writes is one this function accepts.
@@ -92,14 +92,18 @@ async function lane_issue_title(branch_name: string, issue_number: string): Prom
92
92
  return title
93
93
  }
94
94
 
95
+ function branch_derivation_error(branch_name: string): string {
96
+ return `Cannot derive issue info from branch "${branch_name}" in non-interactive mode. ${argument_hint(EXAMPLE_ISSUE_NUMBER)}`
97
+ }
98
+
99
+ function branch_number(branch_name: string): string | undefined {
100
+ return BRANCH_NUMBER_PATTERN.exec(branch_name)?.[1]
101
+ }
102
+
95
103
  async function derive_issue_input_from_branch(branch_name: string): Promise<string> {
96
104
  const match = BRANCH_NUMBER_PATTERN.exec(branch_name)
97
105
 
98
- if (match === null) {
99
- throw new Error(
100
- `Cannot derive issue info from branch "${branch_name}" in non-interactive mode. ${argument_hint(EXAMPLE_ISSUE_NUMBER)}`,
101
- )
102
- }
106
+ if (match === null) throw new Error(branch_derivation_error(branch_name))
103
107
 
104
108
  const [, number = '', slug = ''] = match
105
109
  const title = is_lane_branch(branch_name, number)
@@ -162,6 +166,10 @@ const git_issue = {
162
166
  get_and_display,
163
167
  resolve_and_display,
164
168
  derive_from_branch,
169
+ // The pure halves of the two resolutions above, read by `git-preflight.ts` before anything changes.
170
+ parse: parse_issue_input,
171
+ branch_number,
172
+ branch_derivation_error,
165
173
  }
166
174
 
167
175
  export type { IssueInfo }
@@ -202,22 +202,27 @@ async function create_for_issue(
202
202
  )
203
203
  }
204
204
 
205
- async function create_with_issue_info(issue_info: IssueInfo, extra_body?: string): Promise<void> {
206
- const existing_label = await existing_pr_label(issue_info.branch_name)
207
-
208
- if (existing_label !== undefined) {
209
- await create_for_issue(issue_info, extra_body, existing_label)
205
+ // The label the pull request is opened with — also asked by `git-preflight.ts` before the commit, so
206
+ // a missing classification is reported beside the other unmet preconditions (joshuafolkken/kit#2817).
207
+ async function release_classification(
208
+ issue_number: string,
209
+ branch_name: string,
210
+ ): Promise<ReleaseClassification> {
211
+ const existing_label = await existing_pr_label(branch_name)
210
212
 
211
- return
212
- }
213
+ if (existing_label !== undefined) return existing_label
213
214
 
214
- const issue_json = await git_gh_command.issue_view_json(issue_info.number, 'labels,body')
215
+ const issue_json = await git_gh_command.issue_view_json(issue_number, 'labels,body')
215
216
 
216
217
  if (issue_json === undefined) {
217
- throw new Error(`Could not read issue #${issue_info.number} for release classification`)
218
+ throw new Error(`Could not read issue #${issue_number} for release classification`)
218
219
  }
219
220
 
220
- const label = pr_classification.select_issue_classification(issue_json)
221
+ return pr_classification.select_issue_classification(issue_json)
222
+ }
223
+
224
+ async function create_with_issue_info(issue_info: IssueInfo, extra_body?: string): Promise<void> {
225
+ const label = await release_classification(issue_info.number, issue_info.branch_name)
221
226
 
222
227
  await create_for_issue(issue_info, extra_body, label)
223
228
  }
@@ -225,6 +230,7 @@ async function create_with_issue_info(issue_info: IssueInfo, extra_body?: string
225
230
  const git_pr = {
226
231
  create,
227
232
  create_with_issue_info,
233
+ release_classification,
228
234
  }
229
235
 
230
236
  export { git_pr }
@@ -0,0 +1,138 @@
1
+ import { live_evidence } from '#scripts/review/live-evidence'
2
+ import { changed_paths } from './changed-paths'
3
+ import { git_branch } from './git-branch'
4
+ import { git_command } from './git-command'
5
+ import { git_gh_command } from './git-gh-command'
6
+ import { git_issue, type IssueInfo } from './git-issue'
7
+ import { git_pr } from './git-pr'
8
+
9
+ // joshuafolkken/kit#2817. A first `josh git -y` was refused once per unmet precondition — the title's
10
+ // issue number, the branch name, the issue's release classification — each only after the stage
11
+ // before it had already staged, switched or pushed. **Every precondition decidable at the start is
12
+ // asked here, before anything changes, and all of them are reported at once.** No rule is new: each
13
+ // answer comes from the function the later stage refuses with, so the two cannot drift apart.
14
+
15
+ interface PreflightInput {
16
+ cli_input: string | undefined
17
+ will_open_pr: boolean
18
+ }
19
+
20
+ // The issue the run resolves to and the branch it will commit on, when both are known.
21
+ interface Target {
22
+ number: string
23
+ branch_name: string
24
+ }
25
+
26
+ interface Resolution {
27
+ target: Target | undefined
28
+ problems: Array<string>
29
+ }
30
+
31
+ function error_message(error: unknown): string {
32
+ return error instanceof Error ? error.message : String(error)
33
+ }
34
+
35
+ function resolve_from_branch(current_branch: string): Resolution {
36
+ const number = git_issue.branch_number(current_branch)
37
+
38
+ if (number === undefined) {
39
+ return { target: undefined, problems: [git_issue.branch_derivation_error(current_branch)] }
40
+ }
41
+
42
+ return { target: { number, branch_name: current_branch }, problems: [] }
43
+ }
44
+
45
+ // A branch is created only from the default branch, so on any other the run commits where it stands.
46
+ function resolve_parsed(
47
+ parsed: IssueInfo,
48
+ current_branch: string,
49
+ default_branch: string,
50
+ ): Resolution {
51
+ const branch_name = current_branch === default_branch ? parsed.branch_name : current_branch
52
+ const target = { number: parsed.number, branch_name }
53
+
54
+ if (!git_branch.is_mismatch(current_branch, parsed.branch_name, default_branch)) {
55
+ return { target, problems: [] }
56
+ }
57
+
58
+ return {
59
+ target,
60
+ problems: [
61
+ `Branch mismatch: on "${current_branch}", but the title resolves to "${parsed.branch_name}".`,
62
+ ],
63
+ }
64
+ }
65
+
66
+ function resolve_from_title(
67
+ cli_input: string,
68
+ current_branch: string,
69
+ default_branch: string,
70
+ ): Resolution {
71
+ try {
72
+ return resolve_parsed(git_issue.parse(cli_input), current_branch, default_branch)
73
+ } catch (error) {
74
+ return { target: undefined, problems: [error_message(error)] }
75
+ }
76
+ }
77
+
78
+ async function resolve_target(cli_input: string | undefined): Promise<Resolution> {
79
+ const current_branch = await git_branch.current()
80
+
81
+ if (cli_input === undefined) return resolve_from_branch(current_branch)
82
+
83
+ return resolve_from_title(cli_input, current_branch, await git_command.get_default_branch())
84
+ }
85
+
86
+ async function classification_problems(target: Target): Promise<Array<string>> {
87
+ try {
88
+ await git_pr.release_classification(target.number, target.branch_name)
89
+
90
+ return []
91
+ } catch (error) {
92
+ return [error_message(error)]
93
+ }
94
+ }
95
+
96
+ // A notice rather than a refusal: the evidence is the output of running the change, which does not
97
+ // exist before the commit. Saying so now is what spares the round trip at `josh followup`.
98
+ async function announce_evidence(branch_name: string): Promise<void> {
99
+ // Before staging, a new file is still untracked, so the tracked diff alone would miss it.
100
+ const [paths, body] = await Promise.all([
101
+ changed_paths.read_changed_paths(false),
102
+ git_gh_command.pr_get_body(branch_name),
103
+ ])
104
+
105
+ if (live_evidence.verdict_for(paths, body) !== 'required') return
106
+
107
+ console.info(
108
+ `💡 This change touches runtime code: the PR body needs a ${live_evidence.EVIDENCE_HEADING} section before \`josh followup\` merges it.`,
109
+ )
110
+ }
111
+
112
+ function refusal(problems: ReadonlyArray<string>): Error {
113
+ const lines = problems.map((problem) => ` - ${problem}`)
114
+
115
+ return new Error(
116
+ [
117
+ `Preflight found ${String(problems.length)} unmet precondition(s); nothing was changed:`,
118
+ ...lines,
119
+ ].join('\n'),
120
+ )
121
+ }
122
+
123
+ async function check(input: PreflightInput): Promise<void> {
124
+ const { target, problems } = await resolve_target(input.cli_input)
125
+ // The classification and the evidence belong to the pull request, so a `--skip-pr` run asks neither.
126
+ const pr_target = input.will_open_pr ? target : undefined
127
+ const pr_problems = pr_target === undefined ? [] : await classification_problems(pr_target)
128
+ const all_problems = [...problems, ...pr_problems]
129
+
130
+ if (all_problems.length > 0) throw refusal(all_problems)
131
+
132
+ if (pr_target !== undefined) await announce_evidence(pr_target.branch_name)
133
+ }
134
+
135
+ const git_preflight = { check }
136
+
137
+ export type { PreflightInput }
138
+ export { git_preflight }
@@ -61,6 +61,8 @@ const TASK_DEFINITIONS: Record<TelegramTaskType, TaskDefinition> = {
61
61
  stranded: { icon: '🚨', label: 'Run stranded — nobody is driving it' },
62
62
  }
63
63
 
64
+ const NOTIFY_SWITCH_KEY = 'JOSH_NOTIFY'
65
+ const NOTIFY_OFF_VALUE = 'off'
64
66
  const NOT_CONFIGURED_PREFIX = 'Telegram is not configured'
65
67
  const SEND_FAILED_PREFIX = 'Telegram notification failed'
66
68
  const REDACTED = '<redacted>'
@@ -136,6 +138,14 @@ function load_config(): TelegramConfig {
136
138
  return { bot_token: result.data.telegram_bot_token, chat_id: result.data.telegram_chat_id }
137
139
  }
138
140
 
141
+ // **An explicit opt-out is not a missing credential** (joshuafolkken/kit#2821). A consumer who never
142
+ // meant to use Telegram had no way to say so, so every stop printed the #1564 failure as noise. Only
143
+ // the stated value `off` disables the send: an unset or mistyped switch still falls through to
144
+ // `load_config`, so a forgotten setup stays the loud failure #1564 made it.
145
+ function is_notify_disabled(): boolean {
146
+ return parse_environment_string(process.env[NOTIFY_SWITCH_KEY]).toLowerCase() === NOTIFY_OFF_VALUE
147
+ }
148
+
139
149
  function build_header(task_type: TelegramTaskType, repo_name: string | undefined): string {
140
150
  const { icon, label } = TASK_DEFINITIONS[task_type]
141
151
  if (repo_name === undefined || repo_name.length === 0) return `${icon} ${label}`
@@ -210,7 +220,18 @@ function build_send_failure(error: unknown, config: TelegramConfig): Error {
210
220
  // warning and exiting 0, so a message that reached nobody looked exactly like one that arrived.
211
221
  //
212
222
  // The failure it throws carries a `cause`, and that cause is redacted too — see `build_send_failure`.
223
+ //
224
+ // The opt-out is checked here, before `load_config`, so every caller — the tolerant form included —
225
+ // skips the same way and none has to repeat the check.
213
226
  async function send(input: TelegramSendInput): Promise<void> {
227
+ if (is_notify_disabled()) {
228
+ console.info(
229
+ `🔕 Telegram notifications are disabled (${NOTIFY_SWITCH_KEY}=${NOTIFY_OFF_VALUE}).`,
230
+ )
231
+
232
+ return
233
+ }
234
+
214
235
  const config = load_config()
215
236
  const text = build_text(input)
216
237
 
@@ -10,6 +10,7 @@ import { lane_child_marker } from '#scripts/lane/lane-child-marker'
10
10
  import { filing_cap } from '#scripts/rules/filing-cap'
11
11
  import { lane_background } from '#scripts/rules/lane-background'
12
12
  import { lane_park } from '#scripts/rules/lane-park'
13
+ import { last_prompt } from '#scripts/rules/last-prompt'
13
14
  import { stop_rules, type StopContext, type StopOutcome } from '#scripts/rules/stop-rules'
14
15
  import { run_cut } from '#scripts/run/run-cut'
15
16
  import { run_headless } from '#scripts/run/run-headless'
@@ -70,6 +71,7 @@ async function build_context(
70
71
  tree_clean: !(await run_hold.is_tree_dirty()),
71
72
  notified: lane_park.mentions_confirmation_notify(tail),
72
73
  message: payload_tail.message,
74
+ prompt: last_prompt.prompt_text(tail),
73
75
  stop_hook_active: payload_tail.stop_hook_active,
74
76
  cut_pending: run_cut.carried_cut_sync() !== undefined,
75
77
  filed: was_filed(tail),
@@ -0,0 +1,24 @@
1
+ // The two references `josh init` writes into a basic project, under the paths they had before
2
+ // joshuafolkken/kit#2829 renamed the static profile. kit still ships both old paths so an earlier
3
+ // project keeps working; re-running `josh init` moves the project onto the new ones, which is what
4
+ // lets the old paths be retired once projects have moved.
5
+ const RENAMED_REFERENCES: ReadonlyArray<readonly [RegExp, string]> = [
6
+ [
7
+ /@node_modules\/@joshuafolkken\/kit\/dist\/CLAUDE\.static\.md(?![\w.])/gu,
8
+ '@node_modules/@joshuafolkken/kit/dist/CLAUDE.basic.md',
9
+ ],
10
+ [/@joshuafolkken\/kit\/prettier\/static(?=['"])/gu, '@joshuafolkken/kit/prettier/basic'],
11
+ ]
12
+
13
+ function migrate_basic_paths(content: string): string {
14
+ let migrated = content
15
+
16
+ for (const [pattern, replacement] of RENAMED_REFERENCES) {
17
+ migrated = migrated.replaceAll(pattern, () => replacement)
18
+ }
19
+
20
+ return migrated
21
+ }
22
+
23
+ const basic_path_migration = { migrate_basic_paths }
24
+ export { basic_path_migration }
@@ -97,10 +97,32 @@ function ensure_claude_md_import(existing: string | undefined): string {
97
97
  return `${CLAUDE_MD_BOOTSTRAP}\n\n${CLAUDE_MD_IMPORT_LINE}\n\n${updated}`
98
98
  }
99
99
 
100
+ // The lines `ensure_claude_md_import` writes. A basic project imports other rules, so a sync from
101
+ // before joshuafolkken/kit#2827, or a switch from full, leaves these behind for it to remove.
102
+ const FULL_CLAUDE_MD_LINES = new Set([
103
+ CLAUDE_MD_BOOTSTRAP,
104
+ OLD_CLAUDE_MD_BOOTSTRAP,
105
+ CLAUDE_MD_IMPORT_LINE,
106
+ ])
107
+
108
+ // Each written line goes with the blank line after it, so the rest of the file is left as it was.
109
+ function remove_claude_md_import(content: string): string {
110
+ const lines = content.split('\n')
111
+
112
+ return lines
113
+ .filter((line, index) => {
114
+ if (FULL_CLAUDE_MD_LINES.has(line)) return false
115
+
116
+ return line !== '' || !FULL_CLAUDE_MD_LINES.has(lines[index - 1] ?? '')
117
+ })
118
+ .join('\n')
119
+ }
120
+
100
121
  const distributed_paths = {
101
122
  transform_prompt_paths,
102
123
  transform_distributed_paths,
103
124
  ensure_claude_md_import,
125
+ remove_claude_md_import,
104
126
  CLAUDE_MD_IMPORT_LINE,
105
127
  }
106
128
 
@@ -2,12 +2,14 @@ import { readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { json_format } from '#scripts/config-merge/json-format'
4
4
  import { string_array_schema, vscode_settings_schema } from '#scripts/lib/schemas'
5
+ import { basic_path_migration } from './basic-path-migration'
5
6
  import { init_logic } from './init-logic'
6
7
  import { package_path, PROJECT_ROOT } from './init-paths'
7
8
  import type { ProjectShape } from './project-profile'
9
+ import { session_language_environment } from './session-language-environment'
8
10
 
9
11
  const PRETTIER_CONFIG_JS = 'prettier.config.js'
10
- const STATIC_PRETTIER_CONFIG = 'prettier.config.mjs'
12
+ const BASIC_PRETTIER_CONFIG = 'prettier.config.mjs'
11
13
  const VSCODE_EXTENSIONS_PATH = '.vscode/extensions.json'
12
14
  const VSCODE_SETTINGS_PATH = '.vscode/settings.json'
13
15
  const TSCONFIG_PATH = 'tsconfig.json'
@@ -17,6 +19,9 @@ interface FileAction {
17
19
  dest: string
18
20
  create: () => string
19
21
  merge?: (existing: string) => string
22
+ // A merge that only migrates a kit-written line, for a file kit otherwise leaves to the project:
23
+ // when it changes nothing, the file is reported with the sample to add, as one with no merge is.
24
+ should_show_sample_when_unchanged?: boolean
20
25
  }
21
26
 
22
27
  function read_package_file(relative_path: string): string {
@@ -59,7 +64,7 @@ function build_vscode_actions(): ReadonlyArray<FileAction> {
59
64
  ]
60
65
  }
61
66
 
62
- const STATIC_EXTENSIONS = [
67
+ const BASIC_EXTENSIONS = [
63
68
  'streetsidesoftware.code-spell-checker',
64
69
  'pkief.material-icon-theme',
65
70
  'usernamehw.errorlens',
@@ -69,16 +74,16 @@ const STATIC_EXTENSIONS = [
69
74
  const PRETTIER_EXTENSION = 'esbenp.prettier-vscode'
70
75
  const DEFAULT_FORMATTER = 'editor.defaultFormatter'
71
76
  const FORMAT_ON_SAVE = 'editor.formatOnSave'
72
- const STATIC_LANGUAGES = ['[html]', '[css]', '[javascript]']
77
+ const BASIC_LANGUAGES = ['[html]', '[css]', '[javascript]']
73
78
 
74
- function static_settings(): Record<string, Record<string, unknown>> {
79
+ function basic_settings(): Record<string, Record<string, unknown>> {
75
80
  const formatter = { [DEFAULT_FORMATTER]: PRETTIER_EXTENSION, [FORMAT_ON_SAVE]: true }
76
81
 
77
- return Object.fromEntries(STATIC_LANGUAGES.map((language) => [language, formatter]))
82
+ return Object.fromEntries(BASIC_LANGUAGES.map((language) => [language, formatter]))
78
83
  }
79
84
 
80
- function build_static_settings_action(): FileAction {
81
- const settings = static_settings()
85
+ function build_basic_settings_action(): FileAction {
86
+ const settings = basic_settings()
82
87
 
83
88
  return build_action(
84
89
  VSCODE_SETTINGS_PATH,
@@ -87,10 +92,10 @@ function build_static_settings_action(): FileAction {
87
92
  )
88
93
  }
89
94
 
90
- function build_static_vscode_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
95
+ function build_basic_vscode_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
91
96
  const recommendations = shape.has_web
92
- ? [...STATIC_EXTENSIONS, PRETTIER_EXTENSION]
93
- : STATIC_EXTENSIONS
97
+ ? [...BASIC_EXTENSIONS, PRETTIER_EXTENSION]
98
+ : BASIC_EXTENSIONS
94
99
  const actions: Array<FileAction> = [
95
100
  build_action(
96
101
  VSCODE_EXTENSIONS_PATH,
@@ -99,12 +104,12 @@ function build_static_vscode_actions(shape: ProjectShape): ReadonlyArray<FileAct
99
104
  ),
100
105
  ]
101
106
 
102
- if (shape.has_web) actions.push(build_static_settings_action())
107
+ if (shape.has_web) actions.push(build_basic_settings_action())
103
108
 
104
109
  return actions
105
110
  }
106
111
 
107
- function build_static_tsconfig_action(): FileAction {
112
+ function build_basic_tsconfig_action(): FileAction {
108
113
  return build_action(
109
114
  TSCONFIG_PATH,
110
115
  () => json_format.format_json({ extends: init_logic.get_tsconfig_extends_entry() }),
@@ -187,26 +192,28 @@ function build_gitignore_action(): FileAction {
187
192
  )
188
193
  }
189
194
 
190
- function build_static_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
195
+ function build_basic_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
191
196
  const actions: Array<FileAction> = []
192
197
  if (shape.has_git) actions.push(build_gitignore_action())
193
- if (shape.has_typescript) actions.push(build_static_tsconfig_action())
198
+ if (shape.has_typescript) actions.push(build_basic_tsconfig_action())
194
199
 
195
200
  if (shape.has_web) {
196
201
  actions.push({
197
- dest: STATIC_PRETTIER_CONFIG,
202
+ dest: BASIC_PRETTIER_CONFIG,
198
203
  create: () =>
199
- "import { config } from '@joshuafolkken/kit/prettier/static'\n\nexport default config\n",
204
+ "import { config } from '@joshuafolkken/kit/prettier/basic'\n\nexport default config\n",
205
+ merge: (existing) => basic_path_migration.migrate_basic_paths(existing),
206
+ should_show_sample_when_unchanged: true,
200
207
  })
201
208
  }
202
209
 
203
- actions.push(...build_static_vscode_actions(shape))
210
+ actions.push(...build_basic_vscode_actions(shape))
204
211
 
205
212
  return actions
206
213
  }
207
214
 
208
215
  function build_file_actions(shape?: ProjectShape): ReadonlyArray<FileAction> {
209
- if (shape?.profile === 'static') return build_static_actions(shape)
216
+ if (shape?.profile === 'basic') return build_basic_actions(shape)
210
217
 
211
218
  return [
212
219
  ...(shape?.has_git === false ? [] : [build_gitignore_action()]),
@@ -226,10 +233,11 @@ function build_file_actions(shape?: ProjectShape): ReadonlyArray<FileAction> {
226
233
  ...build_config_file_actions().filter(
227
234
  (action) => shape?.has_git !== false || action.dest !== LEFTHOOK_PATH,
228
235
  ),
236
+ ...session_language_environment.build_session_lang_actions(),
229
237
  ]
230
238
  }
231
239
 
232
240
  const init_actions = { build_file_actions, read_package_json }
233
241
 
234
242
  export type { FileAction }
235
- export { init_actions, PRETTIER_CONFIG_JS }
243
+ export { init_actions, PRETTIER_CONFIG_JS, BASIC_PRETTIER_CONFIG }