@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
@@ -76,36 +76,48 @@ function typed_runtime_file(path: string): string {
76
76
  return `${test_type_logic.test_type_for(path)} — ${path}`
77
77
  }
78
78
 
79
+ const BROWSER_CHECK = 'confirm the rendered page in a browser'
80
+ const RUN_CHECK = 'run the changed code by hand and report what you observed'
81
+
82
+ // The manual confirmation a basic-profile exemption owes, one instruction per kind of file it covers:
83
+ // HTML/CSS is looked at in a browser, a source kit cannot test is run by hand (joshuafolkken/kit#2820).
84
+ function manual_instruction(paths: ReadonlyArray<string>, is_basic: boolean): string {
85
+ const manual = test_declared_logic.manual_check_files(paths, is_basic)
86
+ const has_visual = manual.some((path) => test_declared_logic.is_basic_visual(path))
87
+ const has_source = manual.some((path) => !test_declared_logic.is_basic_visual(path))
88
+ const steps = [has_visual ? BROWSER_CHECK : '', has_source ? RUN_CHECK : ''].filter(Boolean)
89
+
90
+ return steps.length === 0 ? '' : ` — ${steps.join('; ')}`
91
+ }
92
+
79
93
  // The word is stdout so a caller can read it alone; the reason — which files, and why — is stderr.
80
- function exempt_detail(paths: ReadonlyArray<string>, is_static: boolean): string {
81
- const exempt = test_declared_logic.exempt_files(paths, is_static)
82
- const has_visual = is_static && exempt.some((path) => /\.(?:html|css)$/u.test(path))
83
- const instruction = has_visual ? ' — confirm the rendered page in a browser' : ''
94
+ function exempt_detail(paths: ReadonlyArray<string>, is_basic: boolean): string {
95
+ const exempt = test_declared_logic.exempt_files(paths, is_basic)
84
96
 
85
- return `exempt paths: ${exempt.join(', ')}${instruction}`
97
+ return `exempt paths: ${exempt.join(', ')}${manual_instruction(paths, is_basic)}`
86
98
  }
87
99
 
88
- function detail_for(verdict: Verdict, paths: ReadonlyArray<string>, is_static = false): string {
100
+ function detail_for(verdict: Verdict, paths: ReadonlyArray<string>, is_basic = false): string {
89
101
  if (verdict === 'required') {
90
102
  const typed = test_declared_logic
91
- .runtime_files(paths, is_static)
103
+ .runtime_files(paths, is_basic)
92
104
  .map((path) => typed_runtime_file(path))
93
105
 
94
106
  return `runtime files with no test: ${typed.join(', ')}`
95
107
  }
96
108
 
97
- if (verdict === 'exempt') return exempt_detail(paths, is_static)
109
+ if (verdict === 'exempt') return exempt_detail(paths, is_basic)
98
110
 
99
111
  return 'a test file changed'
100
112
  }
101
113
 
102
114
  function report(
103
115
  paths: ReadonlyArray<string>,
104
- is_static: boolean = project_checks.is_static(process.cwd()),
116
+ is_basic: boolean = project_checks.is_basic(process.cwd()),
105
117
  ): { detail: string; verdict: Verdict } {
106
- const verdict = test_declared_logic.verdict_for(paths, is_static)
118
+ const verdict = test_declared_logic.verdict_for(paths, is_basic)
107
119
 
108
- return { detail: detail_for(verdict, paths, is_static), verdict }
120
+ return { detail: detail_for(verdict, paths, is_basic), verdict }
109
121
  }
110
122
 
111
123
  function run(): void {
@@ -21,8 +21,8 @@ const NODE_MODULES = 'node_modules'
21
21
  // - **`vitest` absent is the young project.** `josh init` installs no vitest and writes no
22
22
  // `test:unit` script, so a freshly-bootstrapped project always lands here — which is the case the
23
23
  // skip was written for, and it keeps it.
24
- // - **`vitest` present is the node project's declaration that it runs unit tests.** Zero matching
25
- // files there is a broken state — a mis-scoped glob, or a deleted suite — and fails. A static
24
+ // - **`vitest` present is the full project's declaration that it runs unit tests.** Zero matching
25
+ // files there is a broken state — a mis-scoped glob, or a deleted suite — and fails. A basic
26
26
  // project may carry vitest as an unused dependency without declaring a unit suite; zero files
27
27
  // there are reported as skipped, never as a test pass.
28
28
  //
@@ -47,19 +47,19 @@ type GuardAction = 'run' | typeof SKIP_ACTION | typeof SKIP_NO_TESTS_ACTION | ty
47
47
  // body, and without this a gate that skipped the whole unit suite printed the same five lines as one
48
48
  // that ran it.
49
49
  const SKIP_REASON = 'vitest is not installed'
50
- const SKIP_NO_TESTS_REASON = 'no unit test files were found in this static project'
50
+ const SKIP_NO_TESTS_REASON = 'no unit test files were found in this basic project'
51
51
  const FAIL_REASON =
52
52
  'vitest is installed but no *.{test,spec}.{ts,js} test file was found — refusing to report a unit check that ran nothing'
53
53
 
54
54
  function resolve_guard_action(
55
55
  is_installed: boolean,
56
56
  has_tests: boolean,
57
- is_static = false,
57
+ is_basic = false,
58
58
  ): GuardAction {
59
59
  if (!is_installed) return SKIP_ACTION
60
60
  if (has_tests) return 'run'
61
61
 
62
- return is_static ? SKIP_NO_TESTS_ACTION : FAIL_ACTION
62
+ return is_basic ? SKIP_NO_TESTS_ACTION : FAIL_ACTION
63
63
  }
64
64
 
65
65
  function is_vitest_installed(project_directory: string): boolean {
@@ -160,7 +160,7 @@ async function run_guarded_vitest(
160
160
  const action = resolve_guard_action(
161
161
  is_installed,
162
162
  has_tests,
163
- project_checks.is_static(project_directory),
163
+ project_checks.is_basic(project_directory),
164
164
  )
165
165
 
166
166
  if (action !== 'run') return report_no_run(action, command_label)
@@ -45,7 +45,17 @@ const CHAIN_PATTERN = /&&|\|\||;/u
45
45
  // invisible here. Erring this way is the cheaper error for every caller: a missed reading costs
46
46
  // nothing beyond the reading, while a fragment read as a command answers about work the shell never
47
47
  // did, on the shape this repository writes constantly.
48
- const QUOTED_SPAN_PATTERN = /'[^']*'|"[^"]*"/gu
48
+ //
49
+ // **Escapes and heredoc bodies are text too** (joshuafolkken/kit#2841). A backslash outside single
50
+ // quotes makes the next character literal, so `it\'s` opens nothing and a `"` closes only at an
51
+ // unescaped `"`; inside `'…'` a backslash is an ordinary character, so `'C:\'` still closes. A heredoc
52
+ // body is not shell syntax at all — an `it's` there is prose — so it is removed before the quotes are
53
+ // paired, keeping the rest of its marker line (`cat <<EOF > f && git add f`) where the shell reads it.
54
+ // Without both, a stray quote paired with one much later and erased every command between them.
55
+ const QUOTED_SPAN_PATTERN = /\\[\s\S]|'[^']*'|"(?:\\[\s\S]|[^"\\])*"/gu
56
+ const HEREDOC_MARKER = /(?<!<)<<-?\s*(['"]?)(\w+)\1/u
57
+ const HEREDOC_DELIMITER_GROUP = 2
58
+ const LINE_BREAK = '\n'
49
59
  // `set -o pipefail` tells the shell to report the pipeline with the first failing command's status,
50
60
  // so nothing in it is discarded. The flag letters are loose because `set -eo pipefail` and
51
61
  // `set -euo pipefail` are the same instruction, and the `set` word is required so that a path or a
@@ -146,13 +156,37 @@ function leading_word(command: string): string {
146
156
  //
147
157
  // **A pipeline under `set -o pipefail` discards nothing**, so it is not this function's business —
148
158
  // answering otherwise would name the very form a caller is told to use instead.
159
+ // A line outside a heredoc body is kept with its marker blanked, and a marker opens a body whose
160
+ // delimiter is returned; inside one, every line is dropped through the line that is the delimiter alone.
161
+ function open_heredoc(line: string, kept: Array<string>): string | undefined {
162
+ kept.push(line.replace(HEREDOC_MARKER, ' '))
163
+
164
+ return HEREDOC_MARKER.exec(line)?.[HEREDOC_DELIMITER_GROUP]
165
+ }
166
+
167
+ function close_heredoc(line: string, delimiter: string): string | undefined {
168
+ return line.trim() === delimiter ? undefined : delimiter
169
+ }
170
+
171
+ function without_heredoc_bodies(command: string): string {
172
+ const kept: Array<string> = []
173
+ const body: { delimiter?: string | undefined } = {}
174
+
175
+ for (const line of command.split(LINE_BREAK)) {
176
+ body.delimiter =
177
+ body.delimiter === undefined ? open_heredoc(line, kept) : close_heredoc(line, body.delimiter)
178
+ }
179
+
180
+ return kept.join(LINE_BREAK)
181
+ }
182
+
149
183
  // Quoted text replaced by a space, which is what a reader walking into the middle of a chain has to
150
184
  // do before it can trust an operator it finds there. Exported because `time-writes.ts` needs the same
151
185
  // removal for a different reason — a quoted `sed` script holds `|` and slashes that read as a
152
186
  // pipeline and as paths — and two spellings of "remove the quotes" would disagree the first time one
153
187
  // of them learned about a quoting form the other did not.
154
188
  function unquoted(command: string): string {
155
- return command.replaceAll(QUOTED_SPAN_PATTERN, ' ')
189
+ return without_heredoc_bodies(command).replaceAll(QUOTED_SPAN_PATTERN, ' ')
156
190
  }
157
191
 
158
192
  function discarded_commands(command: string): Array<string> {
@@ -0,0 +1,36 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import path from 'node:path'
4
+
5
+ const CACHE_DIRECTORY_PREFIX = 'josh-latest-cache-'
6
+ const CACHE_DIRECTORY_FLAG = '--config.cache-dir='
7
+
8
+ // Points every stage at one metadata cache directory. pnpm keeps registry metadata there and the
9
+ // package store elsewhere, so an empty one costs a metadata refetch, never a tarball download.
10
+ function with_cache_directory(
11
+ stages: ReadonlyArray<ReadonlyArray<string>>,
12
+ cache_directory: string,
13
+ ): Array<Array<string>> {
14
+ return stages.map((stage) => [...stage, `${CACHE_DIRECTORY_FLAG}${cache_directory}`])
15
+ }
16
+
17
+ // A cached metadata file written through safe-chain lacks every version that was under 24 hours old
18
+ // at the time, and with `minimumReleaseAge` set pnpm reuses a cache file younger than that age
19
+ // instead of refetching it — so a version that has since aged in stays invisible for up to a day
20
+ // (joshuafolkken/kit#2805). A fresh directory per run means nothing filtered earlier is ever read.
21
+ function run_in_fresh_cache<T>(
22
+ stages: ReadonlyArray<ReadonlyArray<string>>,
23
+ run_stages: (stages: Array<Array<string>>) => T,
24
+ ): T {
25
+ const cache_directory = mkdtempSync(path.join(tmpdir(), CACHE_DIRECTORY_PREFIX))
26
+
27
+ try {
28
+ return run_stages(with_cache_directory(stages, cache_directory))
29
+ } finally {
30
+ rmSync(cache_directory, { recursive: true, force: true })
31
+ }
32
+ }
33
+
34
+ const fresh_metadata_cache = { with_cache_directory, run_in_fresh_cache }
35
+
36
+ export { fresh_metadata_cache }
@@ -11,6 +11,7 @@ import { lockfile_overrides } from '#scripts/overrides/lockfile-overrides'
11
11
  import { overrides_files } from '#scripts/overrides/overrides-files'
12
12
  import { overrides_check } from '#scripts/overrides/overrides-logic'
13
13
  import { execaSync } from 'execa'
14
+ import { fresh_metadata_cache } from './fresh-metadata-cache'
14
15
  import { latest_regression, type VersionRegression } from './latest-regression'
15
16
  import { preinstall_version_update } from './preinstall-version-update'
16
17
 
@@ -99,7 +100,7 @@ function find_regressions_after_update(snapshot: TreeSnapshot): Array<VersionReg
99
100
  // Exits zero: the tree is left exactly as it was found, nothing is broken, and every workflow that
100
101
  // runs `josh latest` in its preamble would otherwise stop for a situation that resolves itself.
101
102
  function update_without_downgrading(snapshot: TreeSnapshot): UpdateOutcome {
102
- const status = run_update_stages(build_update_commands())
103
+ const status = fresh_metadata_cache.run_in_fresh_cache(build_update_commands(), run_update_stages)
103
104
  if (status !== 0) return { status, is_rolled_back: false }
104
105
 
105
106
  const regressions = find_regressions_after_update(snapshot)
@@ -5,6 +5,7 @@ import { git_commit } from '../scripts/git/git-commit'
5
5
  import { git_error } from '../scripts/git/git-error'
6
6
  import { git_issue, type IssueInfo } from '../scripts/git/git-issue'
7
7
  import { git_pr } from '../scripts/git/git-pr'
8
+ import { git_preflight } from '../scripts/git/git-preflight'
8
9
  import { git_prompt, type WorkflowConfirmations } from '../scripts/git/git-prompt'
9
10
  import { git_push } from '../scripts/git/git-push'
10
11
  import { git_staging } from '../scripts/git/git-staging'
@@ -206,6 +207,14 @@ async function execute_workflow(
206
207
  const { cli_issue_input, is_auto_mode, commit_suffix } = parse_positionals(positionals)
207
208
  const is_non_interactive = is_non_interactive_mode(is_auto_mode, values)
208
209
 
210
+ // Unattended, nobody can fix a precondition between stages — so all are asked before staging.
211
+ if (is_non_interactive) {
212
+ await git_preflight.check({
213
+ cli_input: cli_issue_input,
214
+ will_open_pr: values['skip-pr'] !== true,
215
+ })
216
+ }
217
+
209
218
  await git_staging.check_and_confirm_staging(values.yes === true)
210
219
 
211
220
  const issue_info = await prepare_issue_info(cli_issue_input, is_non_interactive)
@@ -235,6 +244,7 @@ try {
235
244
  }
236
245
 
237
246
  const git_workflow = {
247
+ execute_workflow,
238
248
  parse_positionals,
239
249
  get_workflow_confirmations,
240
250
  prepare_issue_info,
@@ -0,0 +1,8 @@
1
+ # Project instructions
2
+
3
+ - 既存のファイルと利用者の設定を尊重し、変更する前に内容を確認する。
4
+ - 変更には検証を付け、実行した結果を報告する。
5
+ - HTML、CSS、JavaScript のファイルがある場合は Prettier で整形する。
6
+ - 画面に見える変更をした場合はブラウザーで表示と操作を確認する。
7
+ - JavaScript または TypeScript の動作を変更した場合は、その動作を確認する自動テストを追加する。
8
+ - Git や GitHub を使う場合だけ、それぞれのワークフローを利用する。
@@ -200,24 +200,45 @@ jobs:
200
200
  NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
201
201
  run: pnpm install --frozen-lockfile --ignore-scripts
202
202
 
203
+ # A `basic` project has no `prepare`, no build and no SvelteKit, so the ordered steps below
204
+ # would fail on its first pull request (joshuafolkken/kit#2814). The profile is read from
205
+ # `josh profile` rather than re-derived here, and a basic project hands the whole check set to
206
+ # `josh gate`, which already decides per profile which checks apply. A `case` instead of a pipe
207
+ # into `grep -q`, for the `pipefail` reason given at `Detect E2E specs` below.
208
+ - name: Resolve project profile
209
+ id: profile
210
+ run: |
211
+ profile=$(pnpm --silent josh profile)
212
+ echo "${profile}"
213
+ case "${profile}" in
214
+ *'profile: basic ('*) echo 'is_basic=true' >> "$GITHUB_OUTPUT" ;;
215
+ *) echo 'is_basic=false' >> "$GITHUB_OUTPUT" ;;
216
+ esac
217
+
203
218
  - name: Run spell check
219
+ if: steps.profile.outputs.is_basic != 'true'
204
220
  run: pnpm josh cspell:dot
205
221
 
206
222
  - name: Prepare
223
+ if: steps.profile.outputs.is_basic != 'true'
207
224
  run: pnpm prepare
208
225
 
209
226
  - name: Run type check
227
+ if: steps.profile.outputs.is_basic != 'true'
210
228
  run: pnpm exec svelte-kit sync && pnpm exec svelte-check --tsconfig ./tsconfig.json --threshold error
211
229
 
212
230
  - name: Run prettier check
231
+ if: steps.profile.outputs.is_basic != 'true'
213
232
  run: pnpm exec prettier --check .
214
233
 
215
234
  # Build before eslint so generated outputs exist for type-aware linting.
216
235
  # The same build output is reused by the size check below.
217
236
  - name: Build
237
+ if: steps.profile.outputs.is_basic != 'true'
218
238
  run: pnpm build
219
239
 
220
240
  - name: Run eslint check
241
+ if: steps.profile.outputs.is_basic != 'true'
221
242
  run: pnpm exec eslint . --cache --cache-strategy content
222
243
 
223
244
  # Vitest browser-mode projects (*.svelte.test.ts) drive a real Playwright browser,
@@ -233,9 +254,15 @@ jobs:
233
254
  run: ./node_modules/.bin/playwright install --with-deps ${PLAYWRIGHT_BROWSERS}
234
255
 
235
256
  - name: Run unit tests
257
+ if: steps.profile.outputs.is_basic != 'true'
236
258
  run: pnpm josh test:unit
237
259
 
260
+ - name: Run verification gate
261
+ if: steps.profile.outputs.is_basic == 'true'
262
+ run: pnpm josh gate --verbose
263
+
238
264
  - name: Check bundle size
265
+ if: steps.profile.outputs.is_basic != 'true'
239
266
  run: pnpm size-limit
240
267
 
241
268
  security:
@@ -1,85 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import path from 'node:path'
3
- import { issue_classification } from '#scripts/issue/issue-classification'
4
- import type { GuardRun } from '#scripts/josh/hook-decision'
5
- import { time_density_hook } from '#scripts/time-runtime/time-density-hook'
6
- import { time_shell } from '#scripts/time-runtime/time-shell'
7
- import { bash_triggers } from './bash-triggers'
8
- import { issue_filing_args } from './issue-filing-args'
9
- import { shell_segments } from './shell-segments'
10
- import { tail_commands } from './tail-commands'
11
-
12
- const LINT_NAMES = new Set(['issue:lint'])
13
- const LINT_PATH = /\b(?:issue:lint|iln)\s+(?:'([^']+)'|"([^"]+)"|(\S+))/u
14
-
15
- function lint_argument(segment: string): string | undefined {
16
- const [, single_quoted, double_quoted, bare] = LINT_PATH.exec(segment) ?? []
17
-
18
- return single_quoted ?? double_quoted ?? bare
19
- }
20
-
21
- function latest_lint_path(tail: string): string | undefined {
22
- const segments = tail_commands
23
- .prior_bash_commands(tail)
24
- .flatMap((command) => shell_segments.segments_of(command))
25
- const lint = segments.findLast((segment) => shell_segments.is_josh_command(segment, LINT_NAMES))
26
-
27
- return lint === undefined ? undefined : lint_argument(lint)
28
- }
29
-
30
- function linted_body(body_path: string): string | undefined {
31
- try {
32
- return readFileSync(body_path, 'utf8')
33
- } catch {
34
- return undefined
35
- }
36
- }
37
-
38
- function matches_linted_file(command: string, lint_path: string): boolean {
39
- const filing_path = issue_filing_args.body_file(command)
40
-
41
- return filing_path !== undefined && path.resolve(filing_path) === path.resolve(lint_path)
42
- }
43
-
44
- function needs_classification_labels(command: string, tail: string): boolean {
45
- const lint_path = latest_lint_path(tail)
46
-
47
- if (lint_path === undefined) return false
48
- const body = linted_body(lint_path)
49
-
50
- if (body === undefined) return false
51
-
52
- if (!matches_linted_file(command, lint_path) || issue_classification.problems(body).length > 0) {
53
- return true
54
- }
55
-
56
- const expected = issue_classification.required_labels(body)
57
-
58
- return issue_classification.CLASSIFICATION_LABELS.some(
59
- (label) => expected.includes(label) !== issue_filing_args.has_label(command, label),
60
- )
61
- }
62
-
63
- function decide(call: { input: unknown }, run: GuardRun): boolean {
64
- const command = time_shell.bash_command(call.input)
65
- const tail = time_density_hook.read_tail(run.transcript)
66
-
67
- return needs_classification_labels(command, tail)
68
- }
69
-
70
- const ISSUE_CLASSIFICATION_REASON =
71
- '⛔ issue classification mismatch: use the exact body file checked by `pnpm josh issue:lint` ' +
72
- 'in this creation call (`-F body=@<path>` or `gh issue create --body-file <path>`) and ' +
73
- 'apply exactly the `labels:` it reports (`bug`, `enhancement`, `breaking-change`). See ' +
74
- '`prompts/collaboration-workflow/issue-template.md`.'
75
-
76
- const ROW = {
77
- id: 'issue-bug-label',
78
- is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
79
- reason: ISSUE_CLASSIFICATION_REASON,
80
- decide,
81
- }
82
-
83
- const issue_bug_label_rule = { ROW, latest_lint_path, needs_classification_labels }
84
-
85
- export { issue_bug_label_rule }
@@ -1,50 +0,0 @@
1
- const SHELL_WORD = /(?:[^\s'"]|'[^']*'|"[^"]*")+/gu
2
- const QUOTE = /['"]/gu
3
- const RAW_FIELDS = new Set(['-f', '--raw-field'])
4
- const FILE_FIELDS = new Set(['-F', '--field'])
5
- const LABEL_OPTIONS = new Set(['-l', '--label'])
6
- const BODY_FILE_OPTIONS = new Set(['--body-file'])
7
- const INLINE_BODY_OPTIONS = new Set(['-b', '--body'])
8
- const BODY_FILE_PREFIX = 'body=@'
9
-
10
- function words_of(command: string): Array<string> {
11
- return [...command.matchAll(SHELL_WORD)].map((match) => match[0].replaceAll(QUOTE, ''))
12
- }
13
-
14
- function values_after(words: ReadonlyArray<string>, flags: ReadonlySet<string>): Array<string> {
15
- return words.flatMap((word, index) => {
16
- const value = words[index + 1]
17
-
18
- return value !== undefined && flags.has(word) ? [value] : []
19
- })
20
- }
21
-
22
- function has_inline_body(words: ReadonlyArray<string>): boolean {
23
- const has_raw = values_after(words, RAW_FIELDS).some((value) => value.startsWith('body='))
24
-
25
- return has_raw || values_after(words, INLINE_BODY_OPTIONS).length > 0
26
- }
27
-
28
- function body_file(command: string): string | undefined {
29
- const words = words_of(command)
30
- const fields = values_after(words, FILE_FIELDS).filter((value) => value.startsWith('body='))
31
- const options = values_after(words, BODY_FILE_OPTIONS)
32
-
33
- if (has_inline_body(words) || fields.length + options.length !== 1) return undefined
34
-
35
- return fields[0]?.startsWith(BODY_FILE_PREFIX)
36
- ? fields[0].slice(BODY_FILE_PREFIX.length)
37
- : options[0]
38
- }
39
-
40
- function has_label(command: string, label: string): boolean {
41
- const words = words_of(command)
42
- const fields = [...values_after(words, RAW_FIELDS), ...values_after(words, FILE_FIELDS)]
43
- const options = values_after(words, LABEL_OPTIONS).flatMap((value) => value.split(','))
44
-
45
- return fields.includes(`labels[]=${label}`) || options.includes(label)
46
- }
47
-
48
- const issue_filing_args = { body_file, has_label, words_of }
49
-
50
- export { issue_filing_args }
@@ -1,64 +0,0 @@
1
- import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
2
- import { bash_triggers } from './bash-triggers'
3
- import { shell_segments } from './shell-segments'
4
- import { tail_commands } from './tail-commands'
5
-
6
- // The trigger and the delivered text behind the `issue-scout` row of `delivered-rules.ts`
7
- // (joshuafolkken/kit#2119).
8
- //
9
- // **A filing without `pnpm josh issue:scout` in front of it, refused at the filing itself.** §2e
10
- // requires the scout before every `gh api … issues` call, and a duplicate is likeliest for the work
11
- // that just finished — the one case self-restraint is least able to catch. The input is mechanical —
12
- // did the run already run the scout — so it is a delivered rule rather than resident prose.
13
- //
14
- // **It is `issue-comments`'s shape, not the WIP cap's.** Like that row it is once per run with an
15
- // `already_satisfied` stand-down read off the tail, and the refusal hands over the command rather than
16
- // asking the run to "scout first" — a body-only read is refused so the reissue makes the comments
17
- // present, and here a scout-less filing is refused so the reissue makes the scout present. **No new
18
- // delivery path**: the trigger is `bash_triggers.is_issue_filing`, reused verbatim.
19
-
20
- // `pnpm josh issue:scout`, the one act the rule asks for. The alias `isc` is expanded where the
21
- // command is read, so the canonical name matches both spellings (joshuafolkken/kit#1789).
22
- const SCOUT_NAMES: ReadonlySet<string> = new Set(['issue:scout'])
23
-
24
- // Keeping the rule: a call that runs the scout. Segment-wise, so a spelling quoted inside a filing's
25
- // own body is not read as the scout that filing skipped.
26
- function runs_the_scout(command: string): boolean {
27
- return shell_segments
28
- .segments_of(command)
29
- .some((segment) => shell_segments.is_josh_command(segment, SCOUT_NAMES))
30
- }
31
-
32
- // The `already_satisfied` stand-down: the run has already scouted, so this filing asks for nothing new.
33
- // The tool-name gate the row's trigger applies has already passed, so `call` only marks the shape; the
34
- // answer is entirely in the tail.
35
- function already_scouted(tail: string, _call: GuardedCall): boolean {
36
- return tail_commands.prior_bash_commands(tail).some((command) => runs_the_scout(command))
37
- }
38
-
39
- // The instruction in the shape a refusal can carry: what the call skipped, why it matters most here,
40
- // and the command that fixes it. The duplicate risk is named because it is the half that makes the
41
- // rule believable — the filing most likely to duplicate is the one about work that just finished.
42
- const ISSUE_SCOUT_REASON =
43
- '⛔ issue scout: this files an Issue with no `pnpm josh issue:scout` earlier in the run. A ' +
44
- 'duplicate is likeliest for the work that just finished, which is exactly where self-restraint is ' +
45
- 'weakest. Reissue after scouting: `pnpm josh issue:scout "<title>"` (add `--body "<one-line ' +
46
- 'summary, citing #N where the work follows one>"` so the epic half can answer), then read its ' +
47
- '`Duplicates:` — an **open** candidate covering the same work stops the run rather than filing a ' +
48
- 'second Issue, and a `(closed)` one means the work is already done — before reissuing this filing. ' +
49
- 'The procedure is `.claude/skills/workflow-commands/SKILL.md` → §2e. It fires once per run and ' +
50
- 'cannot repeat on the call in hand.'
51
-
52
- // The enumeration row itself, so `delivered-rules.ts` spreads one entry rather than restating the
53
- // trigger and reason it already single-sources here. Once per run with the stand-down above.
54
- const ROW = {
55
- id: 'issue-scout',
56
- is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
57
- reason: ISSUE_SCOUT_REASON,
58
- already_satisfied: already_scouted,
59
- keeps: bash_triggers.on_bash_command(runs_the_scout),
60
- }
61
-
62
- const issue_scout = { ISSUE_SCOUT_REASON, ROW, already_scouted, runs_the_scout }
63
-
64
- export { issue_scout }