@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
@@ -0,0 +1,204 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { init_ai_copy } from '#scripts/init/init-ai-copy'
4
+ import { transform_copied_content } from '#scripts/init/init-copy-content'
5
+ import { init_logic } from '#scripts/init/init-logic'
6
+ import { PACKAGE_DIR, PROJECT_ROOT } from '#scripts/init/init-paths'
7
+ import type { ProjectShape } from '#scripts/init/project-profile'
8
+ import { copy_directory_failure, directory_copy_blocker } from './directory-copy-guard'
9
+ import { file_content } from './file-content'
10
+ import { REMOVED_SKILL_MANIFEST } from './removed-skill-manifest'
11
+ import { skill_migration, type MigrationResult } from './skill-migration'
12
+ import { sync_hook_safety } from './sync-hook-safety'
13
+
14
+ const WORKSPACE_YAML = 'pnpm-workspace.yaml'
15
+ const PACKAGE_JSON = 'package.json'
16
+ const CLAUDE_MD_FILENAME = 'CLAUDE.md'
17
+ const CLAUDE_SETTINGS_FILE = '.claude/settings.json'
18
+ const CODEX_HOOKS_FILE = '.codex/hooks.json'
19
+
20
+ function sync_ai_file(source_path: string, destination_path: string): boolean {
21
+ const content = readFileSync(source_path, 'utf8')
22
+
23
+ return file_content.write_text_if_changed(
24
+ destination_path,
25
+ transform_copied_content(destination_path, content),
26
+ )
27
+ }
28
+
29
+ // Both copied hook files must run against the consumer's installed bundle. When sync is run from a
30
+ // newer source than that install, writing either file would leave hooks
31
+ // that fail every prompt (joshuafolkken/kit#1930); skip it and print how to update instead.
32
+ function should_skip_hook_file(filename: string): boolean {
33
+ if (filename !== CLAUDE_SETTINGS_FILE && filename !== CODEX_HOOKS_FILE) return false
34
+ const warning = sync_hook_safety.hook_write_warning(
35
+ PROJECT_ROOT,
36
+ sync_hook_safety.read_version_at(path.join(PACKAGE_DIR, PACKAGE_JSON)),
37
+ filename,
38
+ )
39
+ if (warning === undefined) return false
40
+ console.warn(warning)
41
+
42
+ return true
43
+ }
44
+
45
+ function sync_file(source: string, filename: string): void {
46
+ if (should_skip_hook_file(filename)) return
47
+ const did_change = sync_ai_file(path.join(PACKAGE_DIR, source), path.join(PROJECT_ROOT, filename))
48
+
49
+ console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${filename}`)
50
+ }
51
+
52
+ function sync_file_mapping(source_path: string, destination_path: string): void {
53
+ if (!existsSync(source_path)) {
54
+ console.warn(` ⚠ skipped ${path.basename(destination_path)} (not found in package)`)
55
+
56
+ return
57
+ }
58
+
59
+ // Routed through sync_ai_file rather than cpSync: templates/workflows/ci.yml is a mapped
60
+ // file, so a byte copy would hand the consumer the template's own action pins. The pins
61
+ // have to be resolved from .github/workflows at write time (joshuafolkken/kit#747).
62
+ const did_change = sync_ai_file(source_path, destination_path)
63
+
64
+ console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${path.basename(destination_path)}`)
65
+ }
66
+
67
+ function sync_workspace_yaml(
68
+ template_path: string,
69
+ destination_path: string,
70
+ is_force = false,
71
+ ): boolean {
72
+ const template = readFileSync(template_path, 'utf8')
73
+ const existing =
74
+ !is_force && existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : ''
75
+ const merged = init_logic.merge_workspace_yaml(existing, template)
76
+
77
+ return file_content.write_text_if_changed(destination_path, merged)
78
+ }
79
+
80
+ // The source is the profile's: a basic project syncs from its own templates, as `josh init` wrote it.
81
+ function sync_ai_copy_file(filename: string, is_force: boolean, shape?: ProjectShape): void {
82
+ const source = init_ai_copy.ai_file_source(filename, shape)
83
+
84
+ if (filename === WORKSPACE_YAML) {
85
+ const did_change = sync_workspace_yaml(
86
+ path.join(PACKAGE_DIR, source),
87
+ path.join(PROJECT_ROOT, WORKSPACE_YAML),
88
+ is_force,
89
+ )
90
+
91
+ console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${WORKSPACE_YAML}`)
92
+
93
+ return
94
+ }
95
+
96
+ sync_file(source, filename)
97
+ }
98
+
99
+ // The reason this directory was not synced, or nothing when it was. Both halves answer the same
100
+ // question — what stops the copy — so the caller has one line to print either way.
101
+ function directory_sync_failure(
102
+ directory_name: string,
103
+ on_copy: (did_change: boolean) => void,
104
+ ): string | undefined {
105
+ const source_path = path.join(PACKAGE_DIR, directory_name)
106
+ const destination_path = path.join(PROJECT_ROOT, directory_name)
107
+
108
+ return (
109
+ directory_copy_blocker(source_path, destination_path) ??
110
+ copy_directory_failure(source_path, destination_path, on_copy)
111
+ )
112
+ }
113
+
114
+ function sync_directory(directory_name: string): void {
115
+ const failure = directory_sync_failure(directory_name, (did_change) => {
116
+ console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${directory_name}/`)
117
+ })
118
+
119
+ if (failure !== undefined) console.warn(` ⚠ skipped ${directory_name}/ (${failure})`)
120
+ }
121
+
122
+ // A consumer's stale copy of a distributed skill is removed once it still matches the shipment and
123
+ // kept with a warning once it does not (joshuafolkken/kit#1879). Plugin skills compare against the
124
+ // package source; a retired skill compares against the frozen manifest (joshuafolkken/kit#1990). The
125
+ // note distinguishes the two, and `absent` — the consumer never had the copy — prints nothing.
126
+ function report_migration_result(result: MigrationResult): void {
127
+ if (result.action === 'absent') return
128
+
129
+ if (result.action === 'removed') {
130
+ console.info(
131
+ ` ✔ removed ${result.directory}/ (${skill_migration.removed_note(result.source)})`,
132
+ )
133
+
134
+ return
135
+ }
136
+
137
+ console.warn(` ⚠ kept ${result.directory}/ (${skill_migration.kept_note(result.source)})`)
138
+ }
139
+
140
+ function migrate_removed_skills(): void {
141
+ const plugin = skill_migration.migrate_removed_skill_directories(PACKAGE_DIR, PROJECT_ROOT)
142
+ const retired = skill_migration.migrate_manifest_skills(PROJECT_ROOT, REMOVED_SKILL_MANIFEST)
143
+
144
+ for (const result of [...plugin, ...retired]) report_migration_result(result)
145
+ }
146
+
147
+ function ensured_claude_md(existing: string | undefined, shape?: ProjectShape): string {
148
+ return shape?.profile === 'basic'
149
+ ? init_ai_copy.ensure_basic_claude_md(existing)
150
+ : init_logic.ensure_claude_md_import(existing)
151
+ }
152
+
153
+ // CLAUDE.md is not byte-copied (joshuafolkken/kit#1878): a consumer's file is one @import of kit's
154
+ // published rules plus the project's own additions below it. Ensure the import line is present
155
+ // without ever disturbing those additions — so this ignores --force, which would otherwise mean
156
+ // discarding a consumer's content.
157
+ function sync_claude_md(destination_path: string, shape?: ProjectShape): void {
158
+ const existing = existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : undefined
159
+ const ensured = ensured_claude_md(existing, shape)
160
+
161
+ if (ensured === existing) {
162
+ console.info(` ✔ unchanged ${CLAUDE_MD_FILENAME}`)
163
+
164
+ return
165
+ }
166
+
167
+ mkdirSync(path.dirname(destination_path), { recursive: true })
168
+ writeFileSync(destination_path, ensured)
169
+ console.info(` ✔ synced ${CLAUDE_MD_FILENAME}`)
170
+ }
171
+
172
+ // A basic shape syncs the file set `josh init` writes for that profile; no shape is the full set,
173
+ // unfiltered, as sync has always written it (joshuafolkken/kit#2827).
174
+ function sync_ai_copy_all(is_force: boolean, shape?: ProjectShape): void {
175
+ console.info('AI files:')
176
+
177
+ sync_claude_md(path.join(PROJECT_ROOT, CLAUDE_MD_FILENAME), shape)
178
+
179
+ for (const filename of init_ai_copy.ai_files(shape)) {
180
+ sync_ai_copy_file(filename, is_force, shape)
181
+ }
182
+
183
+ for (const { src, dest } of init_ai_copy.ai_file_mappings(shape)) {
184
+ sync_file_mapping(path.join(PACKAGE_DIR, src), path.join(PROJECT_ROOT, dest))
185
+ }
186
+
187
+ for (const directory_name of init_logic.get_ai_copy_directories()) {
188
+ sync_directory(directory_name)
189
+ }
190
+
191
+ migrate_removed_skills()
192
+ }
193
+
194
+ const sync_ai_files = {
195
+ should_skip_hook_file,
196
+ sync_file_mapping,
197
+ sync_ai_file,
198
+ sync_workspace_yaml,
199
+ sync_claude_md,
200
+ sync_directory,
201
+ sync_ai_copy_all,
202
+ }
203
+
204
+ export { sync_ai_files }
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env tsx
2
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
+ import { project_checks } from '#scripts/gate/project-checks'
5
6
  import { gh_spawn } from '#scripts/gh/gh-spawn'
6
- import { transform_copied_content } from '#scripts/init/init-copy-content'
7
+ import { basic_path_migration } from '#scripts/init/basic-path-migration'
7
8
  import { init_logic } from '#scripts/init/init-logic'
8
9
  import { PACKAGE_DIR, PROJECT_ROOT } from '#scripts/init/init-paths'
9
10
  import { plugin_install_hint_module } from '#scripts/init/plugin-install-hint'
11
+ import { project_profile } from '#scripts/init/project-profile'
10
12
  import { auto_merge_setting } from '#scripts/repo/auto-merge-setting'
11
13
  import { repository_labels } from '#scripts/repo/repository-labels'
12
14
  import { project_config } from '#scripts/safe-chain/project-config'
@@ -14,121 +16,13 @@ import { security_updates } from '#scripts/security/security-updates'
14
16
  import { sonar_file } from '#scripts/security/sonar-file'
15
17
  import { did_refuse_self_run } from '#scripts/self-sync-guard/self-sync-refusal'
16
18
  import { package_manager_version } from '#scripts/version/package-manager-version'
17
- import { copy_directory_failure, directory_copy_blocker } from './directory-copy-guard'
18
19
  import { file_content } from './file-content'
19
- import { REMOVED_SKILL_MANIFEST } from './removed-skill-manifest'
20
- import { skill_migration, type MigrationResult } from './skill-migration'
20
+ import { sync_ai_files } from './sync-ai-files'
21
21
  import { sync_configs } from './sync-configs'
22
- import { sync_hook_safety } from './sync-hook-safety'
23
22
 
24
- const WORKSPACE_YAML = 'pnpm-workspace.yaml'
25
23
  const PACKAGE_JSON = 'package.json'
26
24
  const PACKAGE_JSON_UNCHANGED_MSG = ' ✔ unchanged package.json'
27
- const CLAUDE_MD_FILENAME = 'CLAUDE.md'
28
- const CLAUDE_SETTINGS_FILE = '.claude/settings.json'
29
- const CODEX_HOOKS_FILE = '.codex/hooks.json'
30
-
31
- function sync_ai_file(source_path: string, destination_path: string): boolean {
32
- const content = readFileSync(source_path, 'utf8')
33
-
34
- return file_content.write_text_if_changed(
35
- destination_path,
36
- transform_copied_content(destination_path, content),
37
- )
38
- }
39
-
40
- // Both copied hook files must run against the consumer's installed bundle. When sync is run from a
41
- // newer source than that install, writing either file would leave hooks
42
- // that fail every prompt (joshuafolkken/kit#1930); skip it and print how to update instead.
43
- function should_skip_hook_file(filename: string): boolean {
44
- if (filename !== CLAUDE_SETTINGS_FILE && filename !== CODEX_HOOKS_FILE) return false
45
- const warning = sync_hook_safety.hook_write_warning(
46
- PROJECT_ROOT,
47
- sync_hook_safety.read_version_at(path.join(PACKAGE_DIR, PACKAGE_JSON)),
48
- filename,
49
- )
50
- if (warning === undefined) return false
51
- console.warn(warning)
52
-
53
- return true
54
- }
55
-
56
- function sync_file(filename: string): void {
57
- if (should_skip_hook_file(filename)) return
58
- const did_change = sync_ai_file(
59
- path.join(PACKAGE_DIR, filename),
60
- path.join(PROJECT_ROOT, filename),
61
- )
62
-
63
- console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${filename}`)
64
- }
65
-
66
- function sync_file_mapping(source_path: string, destination_path: string): void {
67
- if (!existsSync(source_path)) {
68
- console.warn(` ⚠ skipped ${path.basename(destination_path)} (not found in package)`)
69
-
70
- return
71
- }
72
-
73
- // Routed through sync_ai_file rather than cpSync: templates/workflows/ci.yml is a mapped
74
- // file, so a byte copy would hand the consumer the template's own action pins. The pins
75
- // have to be resolved from .github/workflows at write time (joshuafolkken/kit#747).
76
- const did_change = sync_ai_file(source_path, destination_path)
77
-
78
- console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${path.basename(destination_path)}`)
79
- }
80
-
81
- function sync_workspace_yaml(
82
- template_path: string,
83
- destination_path: string,
84
- is_force = false,
85
- ): boolean {
86
- const template = readFileSync(template_path, 'utf8')
87
- const existing =
88
- !is_force && existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : ''
89
- const merged = init_logic.merge_workspace_yaml(existing, template)
90
-
91
- return file_content.write_text_if_changed(destination_path, merged)
92
- }
93
-
94
- function sync_ai_copy_file(filename: string, is_force: boolean): void {
95
- if (filename === WORKSPACE_YAML) {
96
- const did_change = sync_workspace_yaml(
97
- path.join(PACKAGE_DIR, WORKSPACE_YAML),
98
- path.join(PROJECT_ROOT, WORKSPACE_YAML),
99
- is_force,
100
- )
101
-
102
- console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${WORKSPACE_YAML}`)
103
-
104
- return
105
- }
106
-
107
- sync_file(filename)
108
- }
109
-
110
- // The reason this directory was not synced, or nothing when it was. Both halves answer the same
111
- // question — what stops the copy — so the caller has one line to print either way.
112
- function directory_sync_failure(
113
- directory_name: string,
114
- on_copy: (did_change: boolean) => void,
115
- ): string | undefined {
116
- const source_path = path.join(PACKAGE_DIR, directory_name)
117
- const destination_path = path.join(PROJECT_ROOT, directory_name)
118
-
119
- return (
120
- directory_copy_blocker(source_path, destination_path) ??
121
- copy_directory_failure(source_path, destination_path, on_copy)
122
- )
123
- }
124
-
125
- function sync_directory(directory_name: string): void {
126
- const failure = directory_sync_failure(directory_name, (did_change) => {
127
- console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${directory_name}/`)
128
- })
129
-
130
- if (failure !== undefined) console.warn(` ⚠ skipped ${directory_name}/ (${failure})`)
131
- }
25
+ const BASIC_PRETTIER_CONFIG = 'prettier.config.mjs'
132
26
 
133
27
  function did_migrate_prettierrc(destination_path: string): boolean {
134
28
  const legacy_path = path.join(path.dirname(destination_path), '.prettierrc')
@@ -171,6 +65,20 @@ function sync_prettier_config(destination_path: string): void {
171
65
  write_merged_prettier_config(destination_path)
172
66
  }
173
67
 
68
+ // A basic project's prettier config written before joshuafolkken/kit#2829 still imports the static
69
+ // preset path; it is moved onto the basic one with the rule `josh init` applies.
70
+ function sync_basic_prettier_config(destination_path: string): void {
71
+ if (!existsSync(destination_path)) return
72
+
73
+ const existing = readFileSync(destination_path, 'utf8')
74
+ const did_change = file_content.write_text_if_changed(
75
+ destination_path,
76
+ basic_path_migration.migrate_basic_paths(existing),
77
+ )
78
+
79
+ console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${BASIC_PRETTIER_CONFIG}`)
80
+ }
81
+
174
82
  function sync_playwright_config(destination_path: string): void {
175
83
  if (!existsSync(destination_path)) return
176
84
 
@@ -220,70 +128,6 @@ function sync_sonar_with_template(name_with_owner: string | undefined, is_force
220
128
  console.info(` ✔ synced ${destination}`)
221
129
  }
222
130
 
223
- // A consumer's stale copy of a distributed skill is removed once it still matches the shipment and
224
- // kept with a warning once it does not (joshuafolkken/kit#1879). Plugin skills compare against the
225
- // package source; a retired skill compares against the frozen manifest (joshuafolkken/kit#1990). The
226
- // note distinguishes the two, and `absent` — the consumer never had the copy — prints nothing.
227
- function report_migration_result(result: MigrationResult): void {
228
- if (result.action === 'absent') return
229
-
230
- if (result.action === 'removed') {
231
- console.info(
232
- ` ✔ removed ${result.directory}/ (${skill_migration.removed_note(result.source)})`,
233
- )
234
-
235
- return
236
- }
237
-
238
- console.warn(` ⚠ kept ${result.directory}/ (${skill_migration.kept_note(result.source)})`)
239
- }
240
-
241
- function migrate_removed_skills(): void {
242
- const plugin = skill_migration.migrate_removed_skill_directories(PACKAGE_DIR, PROJECT_ROOT)
243
- const retired = skill_migration.migrate_manifest_skills(PROJECT_ROOT, REMOVED_SKILL_MANIFEST)
244
-
245
- for (const result of [...plugin, ...retired]) report_migration_result(result)
246
- }
247
-
248
- // CLAUDE.md is not byte-copied (joshuafolkken/kit#1878): a consumer's file is one @import of kit's
249
- // published rules plus the project's own additions below it. Ensure the import line is present
250
- // without ever disturbing those additions — so this ignores --force, which would otherwise mean
251
- // discarding a consumer's content.
252
- function sync_claude_md(destination_path: string): void {
253
- const existing = existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : undefined
254
- const ensured = init_logic.ensure_claude_md_import(existing)
255
-
256
- if (ensured === existing) {
257
- console.info(` ✔ unchanged ${CLAUDE_MD_FILENAME}`)
258
-
259
- return
260
- }
261
-
262
- mkdirSync(path.dirname(destination_path), { recursive: true })
263
- writeFileSync(destination_path, ensured)
264
- console.info(` ✔ synced ${CLAUDE_MD_FILENAME}`)
265
- }
266
-
267
- function sync_ai_copy_all(is_force: boolean): void {
268
- console.info('AI files:')
269
-
270
- sync_claude_md(path.join(PROJECT_ROOT, CLAUDE_MD_FILENAME))
271
-
272
- for (const filename of init_logic.get_ai_copy_files()) {
273
- sync_ai_copy_file(filename, is_force)
274
- }
275
-
276
- for (const { src, dest } of init_logic.get_ai_copy_file_mappings()) {
277
- sync_file_mapping(path.join(PACKAGE_DIR, src), path.join(PROJECT_ROOT, dest))
278
- }
279
-
280
- for (const directory_name of init_logic.get_ai_copy_directories()) {
281
- sync_directory(directory_name)
282
- }
283
-
284
- migrate_removed_skills()
285
- }
286
-
287
131
  function sync_config_files(): void {
288
132
  sync_configs.sync_npmrc(path.join(PROJECT_ROOT, '.npmrc'))
289
133
  sync_configs.sync_gitignore(path.join(PROJECT_ROOT, '.gitignore'))
@@ -376,7 +220,7 @@ function report_repository_settings(name_with_owner: string | undefined): void {
376
220
  }
377
221
 
378
222
  function sync_project_artifacts(is_force: boolean): void {
379
- sync_ai_copy_all(is_force)
223
+ sync_ai_files.sync_ai_copy_all(is_force)
380
224
  sync_prettier_config(path.join(PROJECT_ROOT, 'prettier.config.js'))
381
225
  sync_playwright_config(path.join(PROJECT_ROOT, 'playwright.config.ts'))
382
226
  sync_deploy_vps(path.join(PROJECT_ROOT, '.github/workflows/deploy-vps.yml'))
@@ -394,29 +238,47 @@ function sync_project_artifacts(is_force: boolean): void {
394
238
  report_repository_settings(name_with_owner)
395
239
  }
396
240
 
241
+ // A basic project gets what `josh init` gives that profile and nothing more: its AI file set and the
242
+ // move of its prettier preset path. The full toolchain's configs, Sonar, package.json migrations and
243
+ // repository settings are never written into it (joshuafolkken/kit#2827).
244
+ function sync_basic_artifacts(is_force: boolean): void {
245
+ console.info('profile: basic (package.json josh.profile)\n')
246
+ sync_ai_files.sync_ai_copy_all(is_force, project_profile.inspect_project(PROJECT_ROOT, 'basic'))
247
+ sync_basic_prettier_config(path.join(PROJECT_ROOT, BASIC_PRETTIER_CONFIG))
248
+ }
249
+
250
+ function sync_full_artifacts(is_force: boolean): void {
251
+ sync_project_artifacts(is_force)
252
+ plugin_install_hint_module.report_plugin_install_hint()
253
+ }
254
+
397
255
  // Checked before anything is written, never per file: the damage is the whole run, and a partial
398
256
  // sync that stopped halfway would leave the source repository in a state neither `git checkout` nor
399
257
  // a re-run describes (joshuafolkken/kit#868).
258
+ //
259
+ // Only a recorded basic profile switches the file set; an unrecorded one stays full, as every
260
+ // project initialized before the profile was recorded was set up.
400
261
  function main(): void {
401
262
  if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return
402
263
 
403
264
  const is_force = process.argv.includes('--force')
404
265
 
405
266
  console.info('\n🔄 Syncing @joshuafolkken/kit AI files\n')
406
- sync_project_artifacts(is_force)
407
- plugin_install_hint_module.report_plugin_install_hint()
267
+ if (project_checks.is_basic(PROJECT_ROOT)) sync_basic_artifacts(is_force)
268
+ else sync_full_artifacts(is_force)
408
269
  console.info('\n✅ Done.\n')
409
270
  }
410
271
 
411
272
  if (process.argv[1] === fileURLToPath(import.meta.url)) main()
412
273
 
413
274
  const sync = {
414
- should_skip_hook_file,
415
- sync_file_mapping,
416
- sync_ai_file,
417
- sync_workspace_yaml,
418
- sync_claude_md,
275
+ should_skip_hook_file: sync_ai_files.should_skip_hook_file,
276
+ sync_file_mapping: sync_ai_files.sync_file_mapping,
277
+ sync_ai_file: sync_ai_files.sync_ai_file,
278
+ sync_workspace_yaml: sync_ai_files.sync_workspace_yaml,
279
+ sync_claude_md: sync_ai_files.sync_claude_md,
419
280
  sync_prettier_config,
281
+ sync_basic_prettier_config,
420
282
  sync_playwright_config,
421
283
  sync_deploy_vps,
422
284
  sync_package_manager_version,
@@ -427,4 +289,6 @@ const sync = {
427
289
 
428
290
  // `sync` is the entry point; `sync_directory` and `main` are exported for their own unit tests, the
429
291
  // way `git-pr-checks.ts` exports the helpers beside its namespace.
292
+ const { sync_directory } = sync_ai_files
293
+
430
294
  export { main, sync, sync_directory }
@@ -85,7 +85,7 @@ type FixtureFile = readonly [string, string]
85
85
 
86
86
  const TOOLCHAIN_FILES: ReadonlyArray<FixtureFile> = [
87
87
  ['.gitignore', 'node_modules\n.*cache\n.tsbuildinfo\npnpm-lock.yaml\n'],
88
- ['.prettierignore', '*.json\npnpm-lock.yaml\n'],
88
+ ['.prettierignore', '*.json\npnpm-lock.yaml\n_tmp_*\n'],
89
89
  ['.prettierrc', json_file({ semi: false, singleQuote: true, useTabs: true })],
90
90
  [
91
91
  'cspell.config.yaml',
@@ -199,7 +199,6 @@ const PILOT_FILES: ReadonlyArray<string> = [
199
199
  'scripts/hooks/prompt-hooks.test.ts',
200
200
  'scripts/hooks/reserved-run.test.ts',
201
201
  'scripts/init/claude-plugin-config.test.ts',
202
- 'scripts/init/init-actions.test.ts',
203
202
  'scripts/init/init-copy-content.test.ts',
204
203
  'scripts/init/init-logic-deploy-vps.test.ts',
205
204
  'scripts/init/init-logic-development-engines.test.ts',
@@ -320,7 +319,6 @@ const PILOT_FILES: ReadonlyArray<string> = [
320
319
  'scripts/rules/issue-comments-rule.test.ts',
321
320
  'scripts/rules/issue-depth-label.test.ts',
322
321
  'scripts/rules/issue-fold.test.ts',
323
- 'scripts/rules/issue-scout.test.ts',
324
322
  'scripts/rules/josh-git-bare.test.ts',
325
323
  'scripts/rules/lane-interactive-ask-rule.test.ts',
326
324
  'scripts/rules/lane-park-rule.test.ts',
@@ -62,7 +62,7 @@ function source_paths(): ReadonlyArray<string> {
62
62
  // The verdict over the current working tree. `paths` stays injectable for a direct caller; when it is
63
63
  // omitted the injected set is read if one is in force, else the real tree.
64
64
  function current_verdict(paths: ReadonlyArray<string> = source_paths()): Verdict {
65
- return test_declared_logic.verdict_for(paths, project_checks.is_static(process.cwd()))
65
+ return test_declared_logic.verdict_for(paths, project_checks.is_basic(process.cwd()))
66
66
  }
67
67
 
68
68
  // Put the injection back to what it was before a `with_paths` span — cleared to the live reader when
@@ -14,7 +14,8 @@
14
14
  // a clone avoided. Here only paths that are mechanically certain to touch no runtime code are exempt —
15
15
  // the non-executable config and cosmetic-asset arms of `CLAUDE.md`'s "Non-runtime updates" exception
16
16
  // stay `required`, and a person declares that exception in the Step 0 work summary. Under-exempting
17
- // costs one declared exception; over-exempting ships an untested runtime change.
17
+ // costs one declared exception; over-exempting ships an untested runtime change. The one widening is
18
+ // the basic profile's manual check below, for files no runner kit has could test.
18
19
 
19
20
  type Verdict = 'required' | 'exempt' | 'satisfied'
20
21
 
@@ -27,7 +28,30 @@ const TEST_SUFFIXES: ReadonlyArray<string> = ['.e2e.ts', '.test.ts']
27
28
  const EXEMPT_PATHS: ReadonlyArray<string> = ['.editorconfig']
28
29
  const EXEMPT_PREFIXES: ReadonlyArray<string> = ['.idea/', '.vscode/', 'prompts/']
29
30
  const EXEMPT_SUFFIXES: ReadonlyArray<string> = ['.md']
30
- const STATIC_VISUAL_SUFFIXES: ReadonlyArray<string> = ['.html', '.css']
31
+ const BASIC_VISUAL_SUFFIXES: ReadonlyArray<string> = ['.html', '.css']
32
+
33
+ // What stays `required` under the basic profile (joshuafolkken/kit#2820): the languages kit's own
34
+ // runner can test, and the data and config formats a JS/TS test can load. Any other file — HTML/CSS, or
35
+ // the source of a project kit does not run, such as Lua — has no test kit could execute, so `required`
36
+ // there would be unsatisfiable; it is confirmed by hand instead. The kept side is enumerated rather than
37
+ // the manual one, because that side is closed.
38
+ const BASIC_REQUIRED_SUFFIXES: ReadonlyArray<string> = [
39
+ '.ts',
40
+ '.tsx',
41
+ '.mts',
42
+ '.cts',
43
+ '.js',
44
+ '.jsx',
45
+ '.mjs',
46
+ '.cjs',
47
+ '.svelte',
48
+ '.json',
49
+ '.jsonc',
50
+ '.json5',
51
+ '.yaml',
52
+ '.yml',
53
+ '.toml',
54
+ ]
31
55
 
32
56
  function normalize(paths: ReadonlyArray<string>): Array<string> {
33
57
  return paths.map((path) => path.trim()).filter((path) => path !== '')
@@ -37,12 +61,21 @@ function is_test_file(path: string): boolean {
37
61
  return TEST_SUFFIXES.some((suffix) => path.endsWith(suffix))
38
62
  }
39
63
 
40
- function is_static_visual(path: string, is_static: boolean): boolean {
41
- return is_static && STATIC_VISUAL_SUFFIXES.some((suffix) => path.endsWith(suffix))
64
+ // Matched case-insensitively, so `Main.TS` is not mistaken for a language kit cannot test.
65
+ function is_basic_manual(path: string, is_basic: boolean): boolean {
66
+ const lower = path.toLowerCase()
67
+
68
+ return is_basic && BASIC_REQUIRED_SUFFIXES.every((suffix) => !lower.endsWith(suffix))
69
+ }
70
+
71
+ function is_basic_visual(path: string): boolean {
72
+ const lower = path.toLowerCase()
73
+
74
+ return BASIC_VISUAL_SUFFIXES.some((suffix) => lower.endsWith(suffix))
42
75
  }
43
76
 
44
- function is_exempt(path: string, is_static = false): boolean {
45
- if (EXEMPT_PATHS.includes(path) || is_static_visual(path, is_static)) return true
77
+ function is_exempt(path: string, is_basic = false): boolean {
78
+ if (EXEMPT_PATHS.includes(path) || is_basic_manual(path, is_basic)) return true
46
79
 
47
80
  return (
48
81
  EXEMPT_SUFFIXES.some((suffix) => path.endsWith(suffix)) ||
@@ -52,31 +85,37 @@ function is_exempt(path: string, is_static = false): boolean {
52
85
 
53
86
  // A runtime file is one that is neither a test nor exempt. Its presence with no test file beside it is
54
87
  // the whole of what `required` reports.
55
- function is_runtime(path: string, is_static = false): boolean {
56
- return !is_test_file(path) && !is_exempt(path, is_static)
88
+ function is_runtime(path: string, is_basic = false): boolean {
89
+ return !is_test_file(path) && !is_exempt(path, is_basic)
57
90
  }
58
91
 
59
92
  // **A test file changed → `satisfied`, whatever else did.** A runtime file with no test beside it →
60
93
  // `required`. Everything left — an all-exempt change, and the empty diff — is `exempt`, because there
61
94
  // is no runtime file the change failed to test.
62
- function verdict_for(paths: ReadonlyArray<string>, is_static = false): Verdict {
95
+ function verdict_for(paths: ReadonlyArray<string>, is_basic = false): Verdict {
63
96
  const changed = normalize(paths)
64
97
 
65
98
  if (changed.some((path) => is_test_file(path))) return 'satisfied'
66
- if (changed.some((path) => is_runtime(path, is_static))) return 'required'
99
+ if (changed.some((path) => is_runtime(path, is_basic))) return 'required'
67
100
 
68
101
  return 'exempt'
69
102
  }
70
103
 
71
104
  // The runtime files with no test beside them — the `required` detail line, so the answer says which
72
105
  // files it is about rather than only that it refused.
73
- function runtime_files(paths: ReadonlyArray<string>, is_static = false): Array<string> {
74
- return normalize(paths).filter((path) => is_runtime(path, is_static))
106
+ function runtime_files(paths: ReadonlyArray<string>, is_basic = false): Array<string> {
107
+ return normalize(paths).filter((path) => is_runtime(path, is_basic))
75
108
  }
76
109
 
77
110
  // The paths that made the change exempt — the `exempt` detail line.
78
- function exempt_files(paths: ReadonlyArray<string>, is_static = false): Array<string> {
79
- return normalize(paths).filter((path) => is_exempt(path, is_static))
111
+ function exempt_files(paths: ReadonlyArray<string>, is_basic = false): Array<string> {
112
+ return normalize(paths).filter((path) => is_exempt(path, is_basic))
113
+ }
114
+
115
+ // The paths exempt only because the basic profile has no runner for them — the ones a person still has
116
+ // to confirm by hand, as opposed to docs and editor settings, which nobody checks.
117
+ function manual_check_files(paths: ReadonlyArray<string>, is_basic: boolean): Array<string> {
118
+ return normalize(paths).filter((path) => is_exempt(path, is_basic) && !is_exempt(path))
80
119
  }
81
120
 
82
121
  const test_declared_logic = {
@@ -85,9 +124,11 @@ const test_declared_logic = {
85
124
  EXEMPT_SUFFIXES,
86
125
  TEST_SUFFIXES,
87
126
  exempt_files,
127
+ is_basic_visual,
88
128
  is_exempt,
89
129
  is_runtime,
90
130
  is_test_file,
131
+ manual_check_files,
91
132
  runtime_files,
92
133
  verdict_for,
93
134
  }