@joshuafolkken/kit 1.887.0 → 1.929.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 (127) hide show
  1. package/.claude/skills/workflow-commands/backlogrun-progress.md +13 -12
  2. package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -1
  3. package/.claude/skills/workflow-commands/fullrun-steps.md +4 -2
  4. package/.claude/skills/workflow-commands/halfrun.md +3 -1
  5. package/.claude/skills/workflow-commands/kickoff.md +5 -3
  6. package/.claude/skills/workflow-commands/observation-filing.md +3 -1
  7. package/.claude/skills/workflow-commands/prerequisite.md +3 -1
  8. package/.claude/skills/workflow-commands/target-repository.md +1 -1
  9. package/.github/pull_request_template.md +12 -0
  10. package/.github/release.yml +3 -0
  11. package/.github/workflows/ci.yml +8 -0
  12. package/.github/workflows/production.yml +10 -1
  13. package/.github/workflows/publish.yml +72 -2
  14. package/CLAUDE.md +2 -0
  15. package/README.md +2 -1
  16. package/dist/CLAUDE.md +2 -0
  17. package/dist/CLAUDE.static.md +8 -0
  18. package/dist/hooks/batch-guard.js +6 -6
  19. package/dist/hooks/{chunk-264QY6KS.js → chunk-2PGRQ62K.js} +25 -10
  20. package/dist/hooks/chunk-3V37IID6.js +82 -0
  21. package/dist/hooks/{chunk-36UE53LS.js → chunk-7XQ6TK3R.js} +2 -2
  22. package/dist/hooks/{chunk-PRDCNGA6.js → chunk-BNFG2IFK.js} +4 -4
  23. package/dist/hooks/{chunk-VGYSA2NT.js → chunk-GQ3EVWHY.js} +22 -3
  24. package/dist/hooks/{chunk-Y5OYCU2F.js → chunk-I3JP3D4A.js} +3 -3
  25. package/dist/hooks/{chunk-6MQCXEEA.js → chunk-LCJZE42I.js} +25 -88
  26. package/dist/hooks/{chunk-ZBK4XRAS.js → chunk-NJZSO7BK.js} +3 -3
  27. package/dist/hooks/{chunk-SSX23SJ2.js → chunk-RKUAD3I6.js} +1 -0
  28. package/dist/hooks/{chunk-FRJKVSK3.js → chunk-ROULPSQC.js} +2 -2
  29. package/dist/hooks/{chunk-FNFOLFFV.js → chunk-SXPTRFPG.js} +2 -2
  30. package/dist/hooks/{chunk-FXAKJKTB.js → chunk-WPALEIRX.js} +1 -1
  31. package/dist/hooks/{chunk-4OXFU3MK.js → chunk-YO43HEPY.js} +634 -156
  32. package/dist/hooks/codex-hook-adapter.js +13 -12
  33. package/dist/hooks/duplicate-read-guard.js +7 -7
  34. package/dist/hooks/format-edited.js +4 -3
  35. package/dist/hooks/investigation-guard.js +7 -7
  36. package/dist/hooks/pretool-guard.js +12 -11
  37. package/dist/hooks/session-lang.js +2 -2
  38. package/dist/hooks/stop-guard.js +5 -5
  39. package/dist/josh.js +156 -20
  40. package/eslint/base.js +4 -1
  41. package/package.json +72 -18
  42. package/playwright.config.ts +9 -17
  43. package/ports/index.d.ts +1 -0
  44. package/ports/index.js +1 -0
  45. package/ports/web-server.d.ts +5 -0
  46. package/ports/web-server.js +73 -0
  47. package/prettier/index.js +3 -8
  48. package/prettier/static.d.ts +3 -0
  49. package/prettier/static.js +10 -0
  50. package/prompts/collaboration-workflow/issue-template.md +11 -4
  51. package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
  52. package/prompts/collaboration-workflow/wip-cap.md +2 -2
  53. package/prompts/review.md +2 -2
  54. package/prompts/testing-guide.md +1 -0
  55. package/scripts/agent/agent-role-profile.ts +11 -0
  56. package/scripts/build/build-claude-md.ts +6 -1
  57. package/scripts/ci/pr-classification.ts +114 -0
  58. package/scripts/document/document-byte-budget.ts +3 -2
  59. package/scripts/gate/gate-report.ts +2 -0
  60. package/scripts/gate/gate-steps.ts +50 -0
  61. package/scripts/gate/project-checks.ts +145 -0
  62. package/scripts/gate/verification-gate.ts +14 -43
  63. package/scripts/git/git-common-directory.ts +9 -1
  64. package/scripts/git/git-gh-pr-rollup.ts +29 -1
  65. package/scripts/git/git-gh-pr.ts +61 -1
  66. package/scripts/git/git-pr.ts +62 -13
  67. package/scripts/git/issue-labels.ts +9 -0
  68. package/scripts/init/distributed-paths.ts +9 -5
  69. package/scripts/init/hook-command-bootstrap.ts +40 -8
  70. package/scripts/init/hook-command-rewrite.ts +7 -3
  71. package/scripts/init/init-actions.ts +94 -7
  72. package/scripts/init/init-ai-copy.ts +107 -19
  73. package/scripts/init/init-logic-workspace.ts +38 -5
  74. package/scripts/init/init-logic.ts +1 -0
  75. package/scripts/init/init-static.ts +55 -0
  76. package/scripts/init/init.ts +97 -30
  77. package/scripts/init/project-profile-cli.ts +14 -0
  78. package/scripts/init/project-profile.ts +158 -0
  79. package/scripts/init/start.ts +30 -0
  80. package/scripts/issue/issue-bug-label.ts +13 -0
  81. package/scripts/issue/issue-classification.ts +74 -0
  82. package/scripts/issue/issue-lint-cli.ts +10 -3
  83. package/scripts/issue/markdown-section.ts +38 -4
  84. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +27 -1
  85. package/scripts/josh/josh-command-map.ts +3 -0
  86. package/scripts/josh/josh-commands-development.ts +8 -15
  87. package/scripts/josh/josh-commands-project.ts +14 -1
  88. package/scripts/josh/josh-logic.ts +13 -0
  89. package/scripts/josh/run-github-prerequisite.ts +32 -0
  90. package/scripts/lane/lane-await.ts +8 -1
  91. package/scripts/lane/openai-lane-ship.ts +30 -0
  92. package/scripts/lane/openai-lane-supervisor.ts +35 -8
  93. package/scripts/lane/openai-review-broker.ts +260 -0
  94. package/scripts/lint/cspell-cached.ts +21 -0
  95. package/scripts/lint/format.ts +68 -0
  96. package/scripts/lint/lint-parallel.ts +30 -1
  97. package/scripts/observations/observations-flush.ts +1 -1
  98. package/scripts/package/production-dependency-graph.ts +68 -0
  99. package/scripts/package/verify-optional-eslint-install.ts +62 -0
  100. package/scripts/release/github-release-cli.ts +11 -0
  101. package/scripts/release/github-release.ts +220 -0
  102. package/scripts/release/publish-tag-cli.ts +8 -0
  103. package/scripts/release/publish-tag.ts +11 -0
  104. package/scripts/release/release-publish.ts +7 -2
  105. package/scripts/rules/delivered-rules.ts +2 -0
  106. package/scripts/rules/issue-bug-label.ts +85 -0
  107. package/scripts/rules/issue-filing-args.ts +50 -0
  108. package/scripts/run/run-merge-cli.ts +19 -0
  109. package/scripts/run/run-merge.ts +9 -1
  110. package/scripts/run/run-ship-cli.ts +20 -0
  111. package/scripts/run/run-ship-detach.ts +180 -14
  112. package/scripts/run/run-ship-review-steps.ts +45 -25
  113. package/scripts/run/run-step-cli.ts +11 -1
  114. package/scripts/run/run-step.ts +36 -8
  115. package/scripts/run/run-wake-session.ts +3 -1
  116. package/scripts/test/pilot-files.ts +0 -2
  117. package/scripts/test/skip-marker.ts +3 -0
  118. package/scripts/test/test-declared-changed.ts +2 -1
  119. package/scripts/test/test-declared-logic.ts +15 -10
  120. package/scripts/test/test-declared.ts +20 -8
  121. package/scripts/test/test-red-logic.ts +3 -3
  122. package/scripts/test/test-unit-guard.ts +24 -12
  123. package/templates/CLAUDE.static.md +8 -0
  124. package/templates/cursorrules.static +1 -0
  125. package/templates/pnpm-workspace.static.yaml +4 -0
  126. package/templates/prettierignore.static +12 -0
  127. package/templates/workflows/ci.yml +5 -0
@@ -1,5 +1,6 @@
1
1
  import { environment_flags } from '@joshuafolkken/kit/env'
2
2
  import { ports } from '@joshuafolkken/kit/ports'
3
+ import { web_server } from '@joshuafolkken/kit/web-server'
3
4
  import { defineConfig, devices, type ReporterDescription } from '@playwright/test'
4
5
 
5
6
  // Both ports come from kit's single definition (`@joshuafolkken/kit/ports`), offset by the personal
@@ -17,7 +18,7 @@ import { defineConfig, devices, type ReporterDescription } from '@playwright/tes
17
18
  // the file, and a project with no `.env` is untouched.
18
19
  //
19
20
  // The working directory is the anchor, and the loader ascends from it to the project root — the
20
- // same root `pnpm run` hands the `webServer` command, so both sides name one file from anywhere
21
+ // same root `node --run` hands the `webServer` command, so both sides name one file from anywhere
21
22
  // inside the project. The one layout that escapes that is a run whose working directory sits in a
22
23
  // *different* package from this config, as `playwright test --config ../../playwright.config.ts`
23
24
  // does in a workspace: Playwright would default `webServer.cwd` to this file's directory while the
@@ -31,13 +32,6 @@ const PREVIEW_PORT = ports.resolve_preview_port()
31
32
 
32
33
  const CI_TIMEOUT = 120_000
33
34
  const LOCAL_TIMEOUT = 30_000
34
- // pnpm 11.27.1+ places a script it runs without a controlling terminal in its own process group, so
35
- // Playwright's default teardown — SIGKILL to the webServer's group — no longer reaches the `vite dev`
36
- // / `preview` child: it survives holding Playwright's stdio pipes open, and Playwright blocks forever
37
- // on the `close` event (#2386). SIGTERM, unlike SIGKILL, is relayed by that pnpm to the child, so a
38
- // graceful shutdown lets the server exit and the pipes close. The timeout must stay well above the
39
- // observed ~40ms shutdown, or Playwright falls back to killProcess() (SIGKILL) and the hang returns.
40
- const SHUTDOWN_TIMEOUT = 10_000
41
35
  const CI_TEST_TIMEOUT = 30_000
42
36
  const ACTION_TIMEOUT = 10_000
43
37
  const NAV_TIMEOUT = 30_000
@@ -74,7 +68,7 @@ const IS_CI = environment_flags.is_ci_enabled(process.env['CI'])
74
68
  // run whose report is unreachable unless the developer already knows `playwright show-report`.
75
69
  // (`CI/1` in the Playwright user agent and the MCP headless force share the same root cause.)
76
70
  // Deleting the variable once this config has judged the run local is what makes every downstream
77
- // reader agree with that verdict, the `pnpm run dev` child process included: leaving `CI=0` set for
71
+ // reader agree with that verdict, the `dev` script's child process included: leaving `CI=0` set for
78
72
  // the child would only re-introduce the same bare-truthiness bug one process down. The guard is
79
73
  // what keeps this safe — a real provider value such as `CI=woodpecker` is never touched, and the
80
74
  // deletion cannot fire on the CI branch.
@@ -91,24 +85,22 @@ if (!IS_CI) delete process.env['CI']
91
85
  // application; a seed reduces how often a foreign server is on the port at all.
92
86
  const IS_REUSE_ENABLED = environment_flags.is_flag_enabled(process.env['PLAYWRIGHT_REUSE_SERVER'])
93
87
 
94
- // Both branches share one shutdown policy — see SHUTDOWN_TIMEOUT for why SIGTERM rather than the
95
- // SIGKILL default. `as const` keeps the signal a literal so it satisfies Playwright's signal union.
96
- const GRACEFUL_SHUTDOWN = { signal: 'SIGTERM', timeout: SHUTDOWN_TIMEOUT } as const
97
-
88
+ // The scripts run through `node --run` rather than `pnpm run`, so the server stays in the process
89
+ // group Playwright signals on teardown and its default SIGKILL reaches it directly — no package
90
+ // manager relays the signal, and no graceful-shutdown timeout has to be tuned around one (#2392).
91
+ // Each script's `pre` hook is chained in explicitly, since `node --run` does not run it.
98
92
  const web_server_config = IS_CI
99
93
  ? {
100
- command: 'pnpm run build && pnpm run preview',
94
+ command: web_server.script_command(['build', 'preview']),
101
95
  port: PREVIEW_PORT,
102
96
  timeout: CI_TIMEOUT,
103
97
  reuseExistingServer: IS_REUSE_ENABLED,
104
- gracefulShutdown: GRACEFUL_SHUTDOWN,
105
98
  }
106
99
  : {
107
- command: 'pnpm run dev',
100
+ command: web_server.script_command(['dev']),
108
101
  port: DEV_PORT,
109
102
  timeout: LOCAL_TIMEOUT,
110
103
  reuseExistingServer: IS_REUSE_ENABLED,
111
- gracefulShutdown: GRACEFUL_SHUTDOWN,
112
104
  }
113
105
 
114
106
  const env_config: EnvConfig = IS_CI
package/ports/index.d.ts CHANGED
@@ -7,6 +7,7 @@ declare const PROJECT_ENVIRONMENT_KEYS: ReadonlySet<string>
7
7
 
8
8
  declare const ports: {
9
9
  load_environment_file: (directory?: string) => boolean
10
+ resolve_project_directory: (directory: string) => string
10
11
  resolve_seed: (environment?: PortEnvironment) => number
11
12
  resolve_lane: (environment?: PortEnvironment) => number
12
13
  resolve_development_port: (environment?: PortEnvironment) => number
package/ports/index.js CHANGED
@@ -247,6 +247,7 @@ function load_environment_file(directory = process.cwd()) {
247
247
 
248
248
  const ports = {
249
249
  load_environment_file,
250
+ resolve_project_directory,
250
251
  resolve_seed,
251
252
  resolve_lane,
252
253
  resolve_development_port,
@@ -0,0 +1,5 @@
1
+ declare const web_server: {
2
+ script_command: (names: Array<string>, directory?: string) => string
3
+ }
4
+
5
+ export { web_server }
@@ -0,0 +1,73 @@
1
+ // The command Playwright's `webServer` runs, built so no package manager stands between Playwright
2
+ // and the server it starts.
3
+ //
4
+ // Playwright spawns the command detached and tears it down by signalling that process group. pnpm
5
+ // 11.27.1+ moves a script it runs without a controlling terminal into a group of its own, so the
6
+ // teardown reached pnpm alone and the dev / preview server survived holding Playwright's stdio pipes
7
+ // open — a hang #2386 patched by relying on pnpm to relay SIGTERM. `node --run` executes the same
8
+ // `package.json` script in the caller's process group, so the teardown signal reaches the server
9
+ // itself and there is no relay left to depend on (#2392).
10
+ //
11
+ // The script names stay the interface rather than a hard-coded `vite` invocation: a consumer's
12
+ // `preview` is not necessarily `vite preview` (wrangler serves the Cloudflare build), and each script
13
+ // resolves its own port through `josh port`. What `node --run` does not do is run `pre<name>` hooks,
14
+ // which a consumer can rely on — a `prepreview` applying local database migrations, say — so the hook
15
+ // is chained explicitly here instead of being dropped without a word. `post<name>` is not: a server
16
+ // script never ends on its own, so its post hook would never have run under pnpm either.
17
+ import { existsSync, readFileSync } from 'node:fs'
18
+ import path from 'node:path'
19
+ import { ports } from './index.js'
20
+
21
+ const PACKAGE_FILE_NAME = 'package.json'
22
+ const PRE_HOOK_PREFIX = 'pre'
23
+ const STEP_SEPARATOR = ' && '
24
+
25
+ /**
26
+ * The `scripts` of the project root's `package.json` — the same file `node --run` resolves from the
27
+ * working directory — or an empty record when there is none.
28
+ *
29
+ * @param {string} directory
30
+ * @returns {Record<string, unknown>}
31
+ */
32
+ function read_scripts(directory) {
33
+ const file = path.join(ports.resolve_project_directory(directory), PACKAGE_FILE_NAME)
34
+ if (!existsSync(file)) return {}
35
+
36
+ const { scripts } = JSON.parse(readFileSync(file, 'utf8'))
37
+
38
+ return typeof scripts === 'object' && scripts !== null ? scripts : {}
39
+ }
40
+
41
+ /**
42
+ * @param {string} name
43
+ * @param {Record<string, unknown>} scripts
44
+ * @returns {string[]}
45
+ */
46
+ function steps_for(name, scripts) {
47
+ const hook = `${PRE_HOOK_PREFIX}${name}`
48
+
49
+ return Object.hasOwn(scripts, hook) ? [hook, name] : [name]
50
+ }
51
+
52
+ /**
53
+ * Chain the named `package.json` scripts, each preceded by its `pre` hook when the project defines
54
+ * one, into a single shell command run through `node --run`.
55
+ *
56
+ * @param {string[]} names
57
+ * @param {string} [directory]
58
+ * @returns {string}
59
+ */
60
+ function script_command(names, directory = process.cwd()) {
61
+ const scripts = read_scripts(directory)
62
+
63
+ return names
64
+ .flatMap((name) => steps_for(name, scripts))
65
+ .map((step) => `node --run ${step}`)
66
+ .join(STEP_SEPARATOR)
67
+ }
68
+
69
+ const web_server = {
70
+ script_command,
71
+ }
72
+
73
+ export { web_server }
package/prettier/index.js CHANGED
@@ -1,12 +1,7 @@
1
+ import { config as static_config } from './static.js'
2
+
1
3
  export const config = {
2
- useTabs: true,
3
- singleQuote: true,
4
- trailingComma: 'all',
5
- printWidth: 100,
6
- semi: false,
7
- arrowParens: 'always',
8
- bracketSpacing: true,
9
- endOfLine: 'lf',
4
+ ...static_config,
10
5
  plugins: [
11
6
  '@ianvs/prettier-plugin-sort-imports',
12
7
  'prettier-plugin-svelte',
@@ -0,0 +1,3 @@
1
+ import type { Config } from 'prettier'
2
+
3
+ export const config: Config
@@ -0,0 +1,10 @@
1
+ export const config = {
2
+ useTabs: true,
3
+ singleQuote: true,
4
+ trailingComma: 'all',
5
+ printWidth: 100,
6
+ semi: false,
7
+ arrowParens: 'always',
8
+ bracketSpacing: true,
9
+ endOfLine: 'lf',
10
+ }
@@ -13,6 +13,8 @@ Issue には次の要素を必ず含める。
13
13
  ````md
14
14
  ## 背景
15
15
 
16
+ <不具合なら `- 種別: 不具合`、そうでなければ `- 種別: 非不具合` を宣言する>
17
+
16
18
  <なぜ必要か>
17
19
 
18
20
  ## 現象
@@ -57,7 +59,7 @@ Issue には次の要素を必ず含める。
57
59
 
58
60
  **AI の振る舞いを変えると主張する Issue は、`## 発火点` と `## ベースライン` と `## 再現` の 3 見出しを必ず含める。** 散文の規則は「配って終わり」になりやすく、効いたかどうかがマージ後に読み直されないためである(joshuafolkken/kit#2178)。読みだけを根拠にした起票(既に直った現象の起票・壊れたままの現象の誤読)を防ぐため、再現の実測も課す(joshuafolkken/kit#2353)。
59
61
 
60
- - **対象かどうかは本文の宣言から読み、判断させない。** 本文に `- 種別: 振る舞い変更` の行がある Issue だけが対象で、コードだけを変える Issue(`- 種別: コードのみ`、または宣言なし)には 3 見出しを課さない。宣言は `## 背景` の直下に置く
62
+ - **対象かどうかは本文の宣言から読み、判断させない。** 本文に `- 種別: 振る舞い変更` の行がある Issue だけが対象で、コードだけを変える Issue(`- 種別: コードのみ`、または振る舞い変更の宣言なし)には 3 見出しを課さない。宣言は `## 背景` の直下に置く
61
63
 
62
64
  ```md
63
65
  ## 背景
@@ -67,11 +69,16 @@ Issue には次の要素を必ず含める。
67
69
  <なぜ必要か>
68
70
  ```
69
71
 
70
- - **不具合修正の Issue は `- 種別: 不具合` を宣言する。** 同じ位置(`## 背景` の直下)に置く。この宣言がある Issue では、`pnpm josh git -y` が追加・変更したテストを修正前のツリー(merge-base)に当てて `pnpm josh test:red` で実行し、1 本も赤にならない(`green`)ときはコミットを拒否する。修正前でも緑の回帰テストは、報告された症状を再現していないためである(joshuafolkken/kit#2448)。回帰テストは、利用者が症状を見たのと同じ粒度で書く
72
+ - **起票前に不具合かどうかを必ず選ぶ。** `## 背景` の直下に、不具合なら `- 種別: 不具合`、非不具合なら `- 種別: 非不具合` を置く。両方の併記も省略も `pnpm josh issue:lint` が拒否する。故障を述べる本文でも単語から自動判定せず、起票者が判断する。振る舞い変更、機能改善、互換性の宣言はこの判断と併記できる
73
+ - **不具合修正の Issue は `- 種別: 不具合` を宣言する。** この宣言がある Issue では、`pnpm josh git -y` が追加・変更したテストを修正前のツリー(merge-base)に当てて `pnpm josh test:red` で実行し、1 本も赤にならない(`green`)ときはコミットを拒否する。修正前でも緑の回帰テストは、報告された症状を再現していないためである(joshuafolkken/kit#2448)。回帰テストは、利用者が症状を見たのと同じ粒度で書く
74
+ - **起票時の `bug` ラベルは本文の宣言から決める。** `pnpm josh issue:lint <本文ファイル>` の後、同じファイルを `gh api …/issues -F body=@<本文ファイル>` で送る。`labels:` に `bug` が含まれたら `-f 'labels[]=bug'` を同じ作成呼び出しに追加する。`depth`・`route` は併記する。`bugfix` は PR のリリース分類であり、Issue の `bug` の代わりにしない
75
+ - **機能追加・機能改善は目的を本文で宣言する。** `## 背景` の直下に `- 目的: 機能追加` または `- 目的: 機能改善` を書く。`pnpm josh issue:lint <本文ファイル>` が `labels: enhancement` を返したら、同じ作成呼び出しに `-f 'labels[]=enhancement'` を付ける。不具合修正、文書のみ、依存更新、保守作業はタイトルの単語から推測して宣言しない。目的が曖昧なら起票文案の期待結果と受け入れ条件を具体化してから分類する
76
+ - **互換性を壊す変更は影響を本文で宣言する。** 既存の利用者、API、設定の契約を変える場合は、`## 背景` の直下に `- 互換性: 破壊的変更` を書き、廃止対象と影響を説明する。移行措置や事前案内があっても破壊的変更なら宣言する。`pnpm josh issue:lint <本文ファイル>` が `labels: breaking-change` を返したら、同じ作成呼び出しに `-f 'labels[]=breaking-change'` を付ける。互換性を保つ変更には付けない。影響が曖昧なら起票文案で対象の利用者と契約を明確にしてから分類する
77
+ - **`issue:lint` が返す分類ラベルをすべて起票時に指定する。** `bug`、`enhancement`、`breaking-change` は組み合わせて表示されることがある。`depth`・`route` など既存のラベルを残し、`labels: none` のときは分類ラベルを付けない。Issue が `enhancement` と `breaking-change` の両方を持つとき、PR のリリース分類は `breaking-change` 一つにする。Issue の目的の分類と PR のリリース分類を区別する
71
78
  - **`## 発火点`** には、その規則が破れる瞬間に最初に呼ばれるツールコール名を 1 つ書く。名前は `prompts/collaboration-workflow/rule-delivery.md` の配送表に載る、フックで配送可能なツール(`Bash` / `Edit` / `Read` / `Write` / `AskUserQuestion`)と突き合わせる。**表に無い名前**(そもそもツールコール名でない)と**不一致**(既知のツールだが配送できない)は区別して印字され、joshuafolkken/kit#2201 の「規則は正しいが名指ししたコールが実際に破れるコールの 1 つ後ろ」を起票時に検出する
72
79
  - **`## ベースライン`** は **「コマンド + 値」** で書く。``- `<測定コマンド>` → <現在の値>`` の形に限り、自然文だけの測定説明は受け付けない(再実行できないため)。マージ後に `pnpm josh measure:rerun` が同じコマンドをやり直し、**before / after の対**を印字する。値が動かなかったとき、反証された前提として `docs/observations.md` に 1 行追記される(既存の append-only 台帳をそのまま使い、第 2 の台帳は作らない)
73
80
  - **`## 再現`** は **「コマンド + 実際の出力」** で書く。コマンドを backtick、出力をフェンス(``` または ~~~)で囲む。自然文の「確認した」は再実行できないため受け付けない(`## ベースライン` と同じ理由)。読みだけの起票を起票時に弾くためである
74
- - **検査は `pnpm josh issue:lint <path>` が行う。** 対象判定・3 見出しの有無・発火点の配送可否・ベースラインが「コマンド + 値」か・再現が「コマンド + 実際の出力」かを、起票前の本文ファイルにまとめて検査する(未リントの起票は `pnpm josh rule:guard` が拒否する)
81
+ - **検査は `pnpm josh issue:lint <path>` が行う。** 対象判定・3 見出しの有無・発火点の配送可否・ベースラインが「コマンド + 値」か・再現が「コマンド + 実際の出力」かと、起票時に必要な分類ラベルを、起票前の本文ファイルからまとめて答える(未リントの起票は `pnpm josh rule:guard` が拒否する)
75
82
 
76
83
  ### 起票元へのバックリンク(`## Origin` / `## Upstream issues`)
77
84
 
@@ -130,7 +137,7 @@ pnpm josh epic "<epic-title>" <N1> <N2> --origin <owner/repo#N>
130
137
 
131
138
  1. 子 Issue を全て作成し、番号 `<N1> <N2> ...` を控える
132
139
  2. `epic` ラベルを用意する(未作成なら `gh api repos/{owner}/{repo}/labels -f name=epic -f color=5319e7 -f description="Tracks a batch of child issues from one split" --silent 2>/dev/null || true`)
133
- 3. epic を作成し、番号 `<E>` を控える: `gh api repos/{owner}/{repo}/issues -f title="<epic-title>" -f 'labels[]=epic' -f body="<body>" --jq .number`(`--jq` を落とすと Issue の JSON 全体が出る。そこには `id` も含まれるが、**控えるのは `number` である** — `id` はデータベース id で、手順 4 が要求する値ではあっても epic の番号ではない)
140
+ 3. epic を作成し、番号 `<E>` を控える: `gh api repos/{owner}/{repo}/issues -f title="<epic-title>" -f 'labels[]=epic' -F body=@<body-file> --jq .number`(`--jq` を落とすと Issue の JSON 全体が出る。そこには `id` も含まれるが、**控えるのは `number` である** — `id` はデータベース id で、手順 4 が要求する値ではあっても epic の番号ではない)
134
141
  4. 実行順序が**ある場合のみ**、子 Issue に依存関係を付与する(後述の理由により**作成後の独立ステップ**として行う):
135
142
 
136
143
  ```bash
@@ -16,7 +16,7 @@
16
16
 
17
17
  1. **現在の作業を退避する**: `git stash push -u -m "upstream-interrupt: paused #<N>"`(WIP コミットでも可)。退避したことを忘れないよう、この時点で stash を作る。メッセージ付きで積むのは、復元をメッセージで特定するため(下記ステップ 5)
18
18
  2. **進行中の Issue に状況を明記する**: `pnpm josh issue:comment <N> --body-file <path>` で、(a) 作業を stash したこと、(b) 中断理由(どの別パッケージの・どんな問題で中断したか)、(c) 対象パッケージに作成した新 Issue へのリンクを `## Upstream issues` 見出しの下に `owner/repo#N` 形式で、を記載する。これにより「なぜこの Issue が一時停止しているか」と「何を待っているのか」が後から監査できる(→「起票元へのバックリンク」)
19
- 3. **対象パッケージのリポジトリに新しい Issue を作成する(確認なし)**: `gh api repos/<owner>/<repo>/issues -f title="<root-cause title>" -f 'labels[]=route:tier-a' -f 'labels[]=depth:<n>' -f body="<root cause and context>"`(`route:tier-a` は実装中の Tier A 起票を経路として集計するためのラベル — joshuafolkken/kit#1083。付与するのは first-party の場合だけで、third-party はそもそもこの手順に到達せず起票しない)。根本原因・再現・期待結果を Step 1 のテンプレに沿って記載し、**本文に `## Origin` 節を置いて起票元 Issue を `owner/repo#N` 形式で書く**(→「起票元へのバックリンク」)。上流 Issue は欠陥を、起票元 Issue は証拠を持つため、リンクがないと上流 Issue は後から解釈できなくなる。ここで「起票してよいか」を尋ねて停止してはならない。**ただし確認なしで起票してよいのは対象が first-party のときだけ**で、third-party(owner が自リポジトリと一致しないリポジトリ)なら起票せずに停止する(→「第三者リポジトリへの書き込みは Tier C(明示指示が必要)」)
19
+ 3. **対象パッケージのリポジトリに新しい Issue を作成する(確認なし)**: `gh api repos/<owner>/<repo>/issues -f title="<root-cause title>" -f 'labels[]=route:tier-a' -f 'labels[]=depth:<n>' -F body=@<body-file>`(`route:tier-a` は実装中の Tier A 起票を経路として集計するためのラベル — joshuafolkken/kit#1083。付与するのは first-party の場合だけで、third-party はそもそもこの手順に到達せず起票しない)。起票前に `pnpm josh issue:lint <body-file>` を実行し、`labels:` に列挙された分類ラベルをすべて同じ作成呼び出しに付ける。分類の単一ソースは `prompts/collaboration-workflow/issue-template.md`。根本原因・再現・期待結果を Step 1 のテンプレに沿って記載し、**本文に `## Origin` 節を置いて起票元 Issue を `owner/repo#N` 形式で書く**(→「起票元へのバックリンク」)。上流 Issue は欠陥を、起票元 Issue は証拠を持つため、リンクがないと上流 Issue は後から解釈できなくなる。ここで「起票してよいか」を尋ねて停止してはならない。**ただし確認なしで起票してよいのは対象が first-party のときだけ**で、third-party(owner が自リポジトリと一致しないリポジトリ)なら起票せずに停止する(→「第三者リポジトリへの書き込みは Tier C(明示指示が必要)」)
20
20
  4. **`confirmation` Telegram を送って停止する**: 上流 Issue の URL と、何が止まっているかを本文に書く(→「確認待ちで停止するときの Telegram 通知(`confirmation`)」)。無人実行でも画面外でユーザーが気付ける。停止は Issue が既に存在する状態で行うので、ユーザーは「待つ / 先送りする」を Issue を見ながら 1 語で答えられる
21
21
  5. **元の作業を再開する**: 上流の修正がマージされた、または**ユーザーが先送りを明示判断した**後に `pnpm josh stash:pop "upstream-interrupt: paused #<N>"`(位置指定や引数なしの `git stash pop` は使わない — stash は全 work tree が共有する 1 本のスタックで、別のレーンの stash を取り込む恐れがある)して、退避していた元タスクを続行する。上流 Issue を割り込みで実装するかどうかもユーザーの判断(上流パッケージの実装・PR・マージはそれぞれのワークフロー規則に従う)
22
22
 
@@ -69,10 +69,10 @@ gh api "repos/{owner}/{repo}/issues?state=open&per_page=100" --paginate \
69
69
 
70
70
  #### 該当したときの手順
71
71
 
72
- 1. **上限に関係なく起票する。** 1 件閉じることを条件にしない。
72
+ 1. **上限に関係なく起票する。** 1 件閉じることを条件にしない。起票前に `pnpm josh issue:lint <body-file>` を実行し、その `labels:` に列挙された分類ラベルをすべて同じ作成呼び出しに付ける。`prompts/collaboration-workflow/issue-template.md` の手順に従う。
73
73
 
74
74
  ```bash
75
- gh api repos/{owner}/{repo}/issues -f title="<title>" -f 'labels[]=route:interrupt' -f 'labels[]=depth:<n>' -f body="<body>"
75
+ gh api repos/{owner}/{repo}/issues -f title="<title>" -f 'labels[]=route:interrupt' -f 'labels[]=depth:<n>' -F body=@<body-file>
76
76
  ```
77
77
 
78
78
  2. **超過している事実と、当たった条件の番号を本文に 1 行書く**(例:「オープン 37 件で起票。3 条件のうち 2(文書化された手順が完了できない)に該当」)。**どちらか一方では足りない** — 件数だけでは判定が再現できず、条件だけでは上限の可視化が効かない。
package/prompts/review.md CHANGED
@@ -194,8 +194,8 @@ Only branch 2 files an Issue. What follows applies to that branch.
194
194
  two questions decide the filing-time fold"). `pnpm josh rule:guard` refuses the second `gh api …
195
195
  issues` call until it is folded; the first filing asks nothing.
196
196
 
197
- 1. File the follow-up Issue referencing the current one, tagged `route:review-cap`:
198
- `gh api repos/{owner}/{repo}/issues -f title="<title>" -f 'labels[]=route:review-cap' -f 'labels[]=depth:<n>' -f body="<body referencing the current Issue>"`.
197
+ 1. File the follow-up Issue referencing the current one, tagged `route:review-cap`. Run `pnpm josh issue:lint <body-file>` first and apply every label in its `labels:` answer at creation, following `prompts/collaboration-workflow/issue-template.md`:
198
+ `gh api repos/{owner}/{repo}/issues -f title="<title>" -f 'labels[]=route:review-cap' -f 'labels[]=depth:<n>' -F body=@<body-file>`.
199
199
  2. Run `pnpm josh epic:bundle <new>` — **before the current Issue closes.** The candidate search reads
200
200
  open issues only, so once the parent has closed the command answers `none` permanently.
201
201
  3. Act on its answer. **`epic:bundle` recommends and writes nothing**, so acting means running the
@@ -23,6 +23,7 @@ Req N: ...
23
23
 
24
24
  - **Every user-facing behavior change** (UI interaction, keyboard shortcut, visible state change) must have a corresponding E2E test.
25
25
  - **Every logic/utility change** (pure functions, filters, transforms) must have a unit test.
26
+ - **static の HTML/CSS だけの変更:** `package.json` に `josh.profile: static` が記録されていれば自動テストは不要。ブラウザーで対象ページを開き、想定する画面幅でレイアウト・リンク・HTML 内の操作を確認して結果を記録する。同じ変更に JavaScript または TypeScript のファイルが含まれれば自動テストを行う。
26
27
  - If a test is genuinely infeasible (e.g., native OS date-picker popup cannot be driven by Playwright), write a comment in the test file explaining why and test the closest observable behavior instead (e.g., that the editor remains open after the `change` event fires).
27
28
  - Do **not** report a requirement as done if its test is missing.
28
29
 
@@ -201,6 +201,16 @@ function resolve_provider(environment: AgentEnvironment = process.env): Provider
201
201
  return detected_provider(environment) ?? handed_provider(environment)
202
202
  }
203
203
 
204
+ // Whether this session is woken when a background command it started completes
205
+ // (joshuafolkken/kit#2653). Claude Code re-invokes the session at the completion; a Codex session is not
206
+ // re-invoked, so a parent there can only wait by polling, each poll a model call over its whole context.
207
+ // Only a resolved Codex session answers no — an unresolved one keeps the behavior it had before.
208
+ function has_completion_callback(environment: AgentEnvironment = process.env): boolean {
209
+ const selected = resolve_provider(environment)
210
+
211
+ return selected.kind !== 'provider' || selected.provider !== OPENAI_PROVIDER
212
+ }
213
+
204
214
  // The mark a detached launch sets on its child: the provider this session resolved, or nothing when it
205
215
  // resolved none — the child then fails the same way this session would have.
206
216
  function handoff_environment(
@@ -309,6 +319,7 @@ const agent_role_profile = {
309
319
  WORKER,
310
320
  describe,
311
321
  handoff_environment,
322
+ has_completion_callback,
312
323
  is_safe_value,
313
324
  parse,
314
325
  phase_effort,
@@ -6,8 +6,12 @@ import { init_logic } from '#scripts/init/init-logic'
6
6
 
7
7
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
8
8
  const REPO_ROOT = path.resolve(SCRIPT_DIR, '..', '..')
9
+ const DIST_ROOT = path.join(REPO_ROOT, 'dist')
10
+ const STATIC_CLAUDE_FILENAME = 'CLAUDE.static.md'
9
11
  const SOURCE_CLAUDE_MD = path.join(REPO_ROOT, 'CLAUDE.md')
10
- const DIST_CLAUDE_MD = path.join(REPO_ROOT, 'dist', 'CLAUDE.md')
12
+ const DIST_CLAUDE_MD = path.join(DIST_ROOT, 'CLAUDE.md')
13
+ const SOURCE_STATIC_CLAUDE_MD = path.join(REPO_ROOT, 'templates', STATIC_CLAUDE_FILENAME)
14
+ const DIST_STATIC_CLAUDE_MD = path.join(DIST_ROOT, STATIC_CLAUDE_FILENAME)
11
15
 
12
16
  // Read kit's own CLAUDE.md and apply the distribution path transform. kit's source keeps relative
13
17
  // paths so it resolves inside the kit repository; the published copy rewrites them so every backtick
@@ -23,6 +27,7 @@ function generate_distributed_claude_md(): string {
23
27
  function build_claude_md(): string {
24
28
  mkdirSync(path.dirname(DIST_CLAUDE_MD), { recursive: true })
25
29
  writeFileSync(DIST_CLAUDE_MD, generate_distributed_claude_md())
30
+ writeFileSync(DIST_STATIC_CLAUDE_MD, readFileSync(SOURCE_STATIC_CLAUDE_MD, 'utf8'))
26
31
 
27
32
  return DIST_CLAUDE_MD
28
33
  }
@@ -0,0 +1,114 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { z } from 'zod'
4
+
5
+ const BUGFIX = 'bugfix'
6
+ const BREAKING = 'breaking-change'
7
+ const CLASSIFICATION_LABELS = [
8
+ BREAKING,
9
+ 'enhancement',
10
+ BUGFIX,
11
+ 'other-change',
12
+ 'ignore-for-release',
13
+ ] as const
14
+ type ReleaseClassification = (typeof CLASSIFICATION_LABELS)[number]
15
+ const CLASSIFICATION_SET: ReadonlySet<string> = new Set(CLASSIFICATION_LABELS)
16
+ const ISSUE_BUG_LABEL = 'bug'
17
+ const DECLARATION = /^- リリース分類: (.+)$/gmu
18
+ const LEGACY_LABELS: ReadonlySet<string> = new Set(['semver-major', 'semver-minor'])
19
+
20
+ const LABEL = z.object({ name: z.string() })
21
+ const USER = z.object({ login: z.string() })
22
+ const PULL_REQUEST = z.object({ user: USER, labels: z.array(LABEL) })
23
+ const EVENT = z.object({
24
+ pull_request: PULL_REQUEST,
25
+ })
26
+
27
+ function classification_error(labels: ReadonlyArray<string>, author: string): string | undefined {
28
+ const legacy = labels.filter((label) => LEGACY_LABELS.has(label.toLowerCase()))
29
+ if (legacy.length > 0) return `Remove legacy release labels: ${legacy.join(', ')}`
30
+
31
+ const selected = labels.filter((label) => CLASSIFICATION_SET.has(label.toLowerCase()))
32
+
33
+ if (selected.length > 1) {
34
+ return `Choose exactly one release classification; found: ${selected.join(', ')}`
35
+ }
36
+
37
+ if (selected.length === 1 || author.endsWith('[bot]')) return undefined
38
+
39
+ return `Choose one release classification: ${CLASSIFICATION_LABELS.join(', ')}`
40
+ }
41
+
42
+ function is_classification(value: string): value is ReleaseClassification {
43
+ return CLASSIFICATION_SET.has(value)
44
+ }
45
+
46
+ function release_candidates(
47
+ candidates: ReadonlyArray<ReleaseClassification>,
48
+ ): ReadonlyArray<ReleaseClassification> {
49
+ if (!candidates.includes(BREAKING)) return candidates
50
+
51
+ return candidates.filter((candidate) => candidate !== 'enhancement')
52
+ }
53
+
54
+ function issue_candidates(issue_json: string): ReadonlyArray<ReleaseClassification> {
55
+ const issue = z
56
+ .object({ labels: z.array(LABEL), body: z.string().nullable() })
57
+ .parse(JSON.parse(issue_json))
58
+ const names = issue.labels.map((label) => label.name.toLowerCase())
59
+ const declared = [...(issue.body ?? '').matchAll(DECLARATION)].map((match) => match[1] ?? '')
60
+ const candidates = [...release_candidates([...names, ...declared].filter(is_classification))]
61
+ const unknown = declared.filter((value) => !is_classification(value))
62
+
63
+ if (unknown.length > 0) throw new Error(`Unknown release classification: ${unknown.join(', ')}`)
64
+
65
+ if (names.includes(ISSUE_BUG_LABEL) && !candidates.includes(BREAKING)) {
66
+ candidates.push(BUGFIX)
67
+ }
68
+
69
+ return candidates
70
+ }
71
+
72
+ function select_issue_classification(issue_json: string): ReleaseClassification {
73
+ const candidates = issue_candidates(issue_json)
74
+ const selected = [...new Set(candidates)]
75
+ const [label] = selected
76
+
77
+ if (label === undefined || selected.length > 1) {
78
+ throw new Error(
79
+ `Choose one release classification before opening the PR: ${CLASSIFICATION_LABELS.join(', ')}; found: ${selected.join(', ') || 'none'}. Add "- リリース分類: <label>" to the Issue body.`,
80
+ )
81
+ }
82
+
83
+ return label
84
+ }
85
+
86
+ function check_event(event_path: string): string | undefined {
87
+ const event = EVENT.parse(JSON.parse(readFileSync(event_path, 'utf8')))
88
+
89
+ return classification_error(
90
+ event.pull_request.labels.map((label) => label.name),
91
+ event.pull_request.user.login,
92
+ )
93
+ }
94
+
95
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
96
+ const event_path = process.env['GITHUB_EVENT_PATH']
97
+ if (!event_path) throw new Error('GITHUB_EVENT_PATH is required')
98
+
99
+ const error = check_event(event_path)
100
+
101
+ if (error) {
102
+ console.error(`::error::${error}`)
103
+ process.exitCode = 1
104
+ }
105
+ }
106
+
107
+ const pr_classification = {
108
+ classification_error,
109
+ check_event,
110
+ is_classification,
111
+ select_issue_classification,
112
+ }
113
+
114
+ export { pr_classification, CLASSIFICATION_LABELS, type ReleaseClassification }
@@ -95,7 +95,8 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
95
95
  { path: 'prompts/collaboration-workflow/file-edits.md', bytes: 12_288 },
96
96
  { path: 'prompts/collaboration-workflow/gh-rest.md', bytes: 8192 },
97
97
  { path: 'prompts/collaboration-workflow/issue-citation.md', bytes: 8192 },
98
- { path: 'prompts/collaboration-workflow/issue-template.md', bytes: 24_576 },
98
+ // #2662 adds the feature and compatibility declarations to the canonical filing procedure.
99
+ { path: 'prompts/collaboration-workflow/issue-template.md', bytes: 28_672 },
99
100
  { path: 'prompts/collaboration-workflow/latest-first.md', bytes: 4096 },
100
101
  { path: 'prompts/collaboration-workflow/no-clones.md', bytes: 4096 },
101
102
  { path: 'prompts/collaboration-workflow/operating-rules.md', bytes: 24_576 },
@@ -116,7 +117,7 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
116
117
  { path: 'prompts/review-rubric.md', bytes: 20_480 },
117
118
  { path: 'prompts/review.md', bytes: 20_480 },
118
119
  { path: 'prompts/sonar-hotspot-handling.md', bytes: 8192 },
119
- { path: 'prompts/testing-guide.md', bytes: 20_480 },
120
+ { path: 'prompts/testing-guide.md', bytes: 24_576 },
120
121
  ]
121
122
 
122
123
  // The recorded ceiling for one path, or undefined when it carries no budget entry.
@@ -16,6 +16,8 @@ const { FAIL_ICON, PASS_ICON } = status_icons
16
16
  interface GateStep {
17
17
  label: string
18
18
  command_args: ReadonlyArray<string>
19
+ cwd?: string | undefined
20
+ skip_reason?: string | undefined
19
21
  }
20
22
 
21
23
  interface GateStepResult extends BufferedProcessResult {
@@ -0,0 +1,50 @@
1
+ import { gate_plan, type GateCheck, type GatePlan } from './gate-plan'
2
+ import type { GateStep } from './gate-report'
3
+ import { project_checks } from './project-checks'
4
+ import { type_check_step } from './type-check-step'
5
+
6
+ const JOSH = 'josh'
7
+ const { TYPE_CHECK_LABEL, UNIT_LABEL } = gate_plan
8
+ const UNIT_WORKER_FLAG = '--maxWorkers'
9
+
10
+ function unit_worker_args(check: GateCheck, plan: GatePlan): ReadonlyArray<string> {
11
+ if (check.label !== UNIT_LABEL || plan.unit_worker_cap === undefined) return []
12
+
13
+ return [`${UNIT_WORKER_FLAG}=${String(plan.unit_worker_cap)}`]
14
+ }
15
+
16
+ // Resolve the type checker per project and run every static check from its package root.
17
+ async function build_gate_step(
18
+ check: GateCheck,
19
+ start_directory: string,
20
+ plan: GatePlan = gate_plan.resolve_gate_plan(),
21
+ ): Promise<GateStep> {
22
+ const cwd = project_checks.is_static(start_directory)
23
+ ? project_checks.project_root(start_directory)
24
+ : undefined
25
+
26
+ if (check.label !== TYPE_CHECK_LABEL) {
27
+ return {
28
+ label: check.label,
29
+ command_args: [JOSH, check.target, ...unit_worker_args(check, plan)],
30
+ cwd,
31
+ }
32
+ }
33
+
34
+ const command_args = await type_check_step.resolve_type_check_args(start_directory)
35
+ const skip_reason =
36
+ command_args[0] === JOSH ? project_checks.type_check_skip_reason(start_directory) : undefined
37
+
38
+ return { label: check.label, command_args, cwd, skip_reason }
39
+ }
40
+
41
+ async function build_gate_steps(
42
+ start_directory: string,
43
+ plan: GatePlan = gate_plan.resolve_gate_plan(),
44
+ ): Promise<ReadonlyArray<GateStep>> {
45
+ return await Promise.all(
46
+ plan.checks.map(async (check) => await build_gate_step(check, start_directory, plan)),
47
+ )
48
+ }
49
+
50
+ export { build_gate_step, build_gate_steps, UNIT_WORKER_FLAG }