@joshuafolkken/kit 1.929.0 → 1.947.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 (78) hide show
  1. package/.claude/skills/workflow-commands/background-commands.md +6 -0
  2. package/.claude/skills/workflow-commands/backlogrun-child.md +1 -1
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +4 -3
  4. package/.claude/skills/workflow-commands/observation-filing.md +3 -3
  5. package/.claude/skills/workflow-commands/prerequisite.md +1 -1
  6. package/.github/workflows/ci.yml +26 -8
  7. package/.github/workflows/pr-classification.yml +76 -0
  8. package/README.md +47 -22
  9. package/dist/hooks/batch-guard.js +4 -4
  10. package/dist/hooks/{chunk-GQ3EVWHY.js → chunk-3FWV6CFC.js} +113 -19
  11. package/dist/hooks/{chunk-2PGRQ62K.js → chunk-EYFJ74MJ.js} +11 -6
  12. package/dist/hooks/{chunk-YO43HEPY.js → chunk-GIX5SAAB.js} +237 -235
  13. package/dist/hooks/{chunk-LCJZE42I.js → chunk-IA7WC4NS.js} +1 -1
  14. package/dist/hooks/{chunk-BNFG2IFK.js → chunk-IBFC25IX.js} +4 -4
  15. package/dist/hooks/{chunk-I3JP3D4A.js → chunk-M4U4YNRE.js} +3 -3
  16. package/dist/hooks/{chunk-NJZSO7BK.js → chunk-NKVLBB65.js} +3 -3
  17. package/dist/hooks/{chunk-SXPTRFPG.js → chunk-O3PEDF3M.js} +1 -1
  18. package/dist/hooks/{chunk-7XQ6TK3R.js → chunk-OM5VDM6F.js} +2 -2
  19. package/dist/hooks/{chunk-ROULPSQC.js → chunk-V7RH56HU.js} +2 -2
  20. package/dist/hooks/codex-hook-adapter.js +10 -10
  21. package/dist/hooks/duplicate-read-guard.js +5 -5
  22. package/dist/hooks/format-edited.js +2 -2
  23. package/dist/hooks/investigation-guard.js +5 -5
  24. package/dist/hooks/pretool-guard.js +9 -9
  25. package/dist/hooks/stop-guard.js +9 -3
  26. package/dist/josh.js +11 -6
  27. package/package.json +13 -1
  28. package/prompts/collaboration-workflow/issue-template.md +1 -1
  29. package/scripts/ci/pr-classification.ts +27 -6
  30. package/scripts/document/document-byte-budget.ts +1 -1
  31. package/scripts/gate/hook-gate-reuse.ts +1 -1
  32. package/scripts/gate/project-checks.ts +45 -15
  33. package/scripts/gate/type-check-command.ts +43 -0
  34. package/scripts/git/constants.ts +4 -0
  35. package/scripts/git/git-command.ts +11 -5
  36. package/scripts/git/git-gh-pr-rollup.ts +56 -16
  37. package/scripts/git/git-staging.ts +6 -4
  38. package/scripts/git/git-stash.ts +73 -1
  39. package/scripts/git/stash-orphans.ts +24 -2
  40. package/scripts/git/stash-sweep-lock.ts +141 -0
  41. package/scripts/hooks/stop-guard.ts +2 -0
  42. package/scripts/init/init-actions.ts +3 -2
  43. package/scripts/init/init-logic-migrate.ts +10 -1
  44. package/scripts/init/init-logic-templates.ts +27 -7
  45. package/scripts/init/init-logic-workspace.ts +52 -1
  46. package/scripts/init/init-logic.ts +6 -1
  47. package/scripts/init/init.ts +60 -19
  48. package/scripts/issue/issue-merged.ts +75 -0
  49. package/scripts/issue/measure-rerun-cli.ts +2 -2
  50. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +10 -0
  51. package/scripts/josh/josh-command-map.ts +1 -0
  52. package/scripts/josh/josh-command-types.ts +0 -3
  53. package/scripts/josh/josh-commands-ai.ts +6 -0
  54. package/scripts/josh/josh-commands-development.ts +5 -5
  55. package/scripts/lint/lint-parallel.ts +19 -18
  56. package/scripts/observations/observation-ledger-home.ts +22 -2
  57. package/scripts/observations/observation-ledger-line.ts +11 -0
  58. package/scripts/observations/observation-ledger-migrate.ts +146 -0
  59. package/scripts/observations/observation-ledger.ts +51 -3
  60. package/scripts/observations/observations-flush.ts +38 -7
  61. package/scripts/review/review-finding-ledger.ts +1 -1
  62. package/scripts/review/review-record-cli.ts +1 -7
  63. package/scripts/rules/delivered-rules.ts +10 -0
  64. package/scripts/rules/lane-background.ts +111 -0
  65. package/scripts/rules/stop-rules.ts +15 -2
  66. package/scripts/run/run-carry-flush.ts +2 -7
  67. package/scripts/run/run-hold-cli.ts +10 -2
  68. package/scripts/run/run-tidy-cli.ts +50 -0
  69. package/scripts/run/run-tidy-lanes.ts +110 -0
  70. package/scripts/run/run-tidy-stashes.ts +160 -0
  71. package/scripts/run/run-tidy.ts +101 -0
  72. package/scripts/safe-chain/ci-installer-pin.ts +90 -0
  73. package/scripts/safe-chain/preinstall-command.ts +39 -0
  74. package/scripts/sync/sync.ts +5 -2
  75. package/scripts/test/pilot-files.ts +0 -1
  76. package/scripts/version/preinstall-version-update.ts +2 -0
  77. package/templates/workflows/ci.yml +21 -7
  78. package/tsconfig/base.json +4 -1
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  ESLINT_EDIT_CACHE_FLAGS,
7
7
  time_density_hook
8
- } from "./chunk-2PGRQ62K.js";
8
+ } from "./chunk-EYFJ74MJ.js";
9
9
 
10
10
  // scripts/hooks/format-edited-file.ts
11
11
  import { existsSync as existsSync2 } from "node:fs";
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  investigation_reads
3
- } from "./chunk-I3JP3D4A.js";
3
+ } from "./chunk-M4U4YNRE.js";
4
4
  import {
5
5
  lane_guard_policy
6
- } from "./chunk-7XQ6TK3R.js";
6
+ } from "./chunk-OM5VDM6F.js";
7
7
  import {
8
8
  hook_decision
9
- } from "./chunk-SXPTRFPG.js";
9
+ } from "./chunk-O3PEDF3M.js";
10
10
  import {
11
11
  json_value,
12
12
  time_spans
13
- } from "./chunk-2PGRQ62K.js";
13
+ } from "./chunk-EYFJ74MJ.js";
14
14
 
15
15
  // scripts/delegation/duplicate-read-guard.ts
16
16
  import { statSync } from "node:fs";
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  time_batch_guard
3
- } from "./chunk-7XQ6TK3R.js";
3
+ } from "./chunk-OM5VDM6F.js";
4
4
  import {
5
5
  hook_decision
6
- } from "./chunk-SXPTRFPG.js";
6
+ } from "./chunk-O3PEDF3M.js";
7
7
  import {
8
8
  cost_blocks,
9
9
  cost_transcript,
10
10
  time_bundle_call,
11
11
  time_shell,
12
12
  time_spans
13
- } from "./chunk-2PGRQ62K.js";
13
+ } from "./chunk-EYFJ74MJ.js";
14
14
 
15
15
  // scripts/delegation/investigation-reads.ts
16
16
  import path from "node:path";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  investigation_reads
3
- } from "./chunk-I3JP3D4A.js";
3
+ } from "./chunk-M4U4YNRE.js";
4
4
  import {
5
5
  lane_guard_policy
6
- } from "./chunk-7XQ6TK3R.js";
6
+ } from "./chunk-OM5VDM6F.js";
7
7
  import {
8
8
  hook_decision
9
- } from "./chunk-SXPTRFPG.js";
9
+ } from "./chunk-O3PEDF3M.js";
10
10
 
11
11
  // scripts/delegation/investigation-guard.ts
12
12
  import { text } from "node:stream/consumers";
@@ -3,7 +3,7 @@ import {
3
3
  stamp_file,
4
4
  time_density_hook,
5
5
  time_hook_transcript
6
- } from "./chunk-2PGRQ62K.js";
6
+ } from "./chunk-EYFJ74MJ.js";
7
7
  import {
8
8
  josh_environment_file
9
9
  } from "./chunk-WPALEIRX.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  lane_child_marker
3
- } from "./chunk-SXPTRFPG.js";
3
+ } from "./chunk-O3PEDF3M.js";
4
4
  import {
5
5
  time_bundle_call,
6
6
  time_format,
7
7
  time_round_trips,
8
8
  time_spans,
9
9
  time_transcript_line
10
- } from "./chunk-2PGRQ62K.js";
10
+ } from "./chunk-EYFJ74MJ.js";
11
11
 
12
12
  // scripts/lane/lane-guard-policy.ts
13
13
  var DEFAULT_MODE = "refuse";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  lane_guard_policy,
3
3
  time_batch_guard
4
- } from "./chunk-7XQ6TK3R.js";
4
+ } from "./chunk-OM5VDM6F.js";
5
5
  import {
6
6
  hook_decision
7
- } from "./chunk-SXPTRFPG.js";
7
+ } from "./chunk-O3PEDF3M.js";
8
8
 
9
9
  // scripts/hooks/batch-guard.ts
10
10
  import { text } from "node:stream/consumers";
@@ -1,25 +1,25 @@
1
1
  #!/usr/bin/env tsx
2
2
  import {
3
3
  pretool_guard
4
- } from "./chunk-YO43HEPY.js";
5
- import "./chunk-GQ3EVWHY.js";
4
+ } from "./chunk-GIX5SAAB.js";
5
+ import "./chunk-3FWV6CFC.js";
6
6
  import {
7
7
  build_envelope,
8
8
  compose_context,
9
9
  format_edited_payload
10
- } from "./chunk-LCJZE42I.js";
10
+ } from "./chunk-IA7WC4NS.js";
11
11
  import "./chunk-3V37IID6.js";
12
- import "./chunk-ROULPSQC.js";
13
- import "./chunk-NJZSO7BK.js";
14
- import "./chunk-BNFG2IFK.js";
15
- import "./chunk-I3JP3D4A.js";
16
- import "./chunk-7XQ6TK3R.js";
12
+ import "./chunk-V7RH56HU.js";
13
+ import "./chunk-NKVLBB65.js";
14
+ import "./chunk-IBFC25IX.js";
15
+ import "./chunk-M4U4YNRE.js";
16
+ import "./chunk-OM5VDM6F.js";
17
17
  import {
18
18
  hook_decision
19
- } from "./chunk-SXPTRFPG.js";
19
+ } from "./chunk-O3PEDF3M.js";
20
20
  import {
21
21
  time_density_hook
22
- } from "./chunk-2PGRQ62K.js";
22
+ } from "./chunk-EYFJ74MJ.js";
23
23
  import "./chunk-WPALEIRX.js";
24
24
  import "./chunk-RKUAD3I6.js";
25
25
 
@@ -9,11 +9,11 @@ import {
9
9
  is_enabled,
10
10
  notice_refusal_path,
11
11
  refusal_path
12
- } from "./chunk-BNFG2IFK.js";
13
- import "./chunk-I3JP3D4A.js";
14
- import "./chunk-7XQ6TK3R.js";
15
- import "./chunk-SXPTRFPG.js";
16
- import "./chunk-2PGRQ62K.js";
12
+ } from "./chunk-IBFC25IX.js";
13
+ import "./chunk-M4U4YNRE.js";
14
+ import "./chunk-OM5VDM6F.js";
15
+ import "./chunk-O3PEDF3M.js";
16
+ import "./chunk-EYFJ74MJ.js";
17
17
  import "./chunk-WPALEIRX.js";
18
18
  import "./chunk-RKUAD3I6.js";
19
19
  export {
@@ -15,9 +15,9 @@ import {
15
15
  relative_to_root,
16
16
  resolve_invocations,
17
17
  select_invocation
18
- } from "./chunk-LCJZE42I.js";
18
+ } from "./chunk-IA7WC4NS.js";
19
19
  import "./chunk-3V37IID6.js";
20
- import "./chunk-2PGRQ62K.js";
20
+ import "./chunk-EYFJ74MJ.js";
21
21
  import "./chunk-RKUAD3I6.js";
22
22
  export {
23
23
  ESLINT_DAEMON,
@@ -10,11 +10,11 @@ import {
10
10
  is_enabled,
11
11
  load_environment_file,
12
12
  refusal_path
13
- } from "./chunk-NJZSO7BK.js";
14
- import "./chunk-I3JP3D4A.js";
15
- import "./chunk-7XQ6TK3R.js";
16
- import "./chunk-SXPTRFPG.js";
17
- import "./chunk-2PGRQ62K.js";
13
+ } from "./chunk-NKVLBB65.js";
14
+ import "./chunk-M4U4YNRE.js";
15
+ import "./chunk-OM5VDM6F.js";
16
+ import "./chunk-O3PEDF3M.js";
17
+ import "./chunk-EYFJ74MJ.js";
18
18
  import "./chunk-WPALEIRX.js";
19
19
  import "./chunk-RKUAD3I6.js";
20
20
  export {
@@ -3,16 +3,16 @@ import {
3
3
  pretool_guard,
4
4
  pretool_outcome,
5
5
  pretool_outcome_async
6
- } from "./chunk-YO43HEPY.js";
7
- import "./chunk-GQ3EVWHY.js";
6
+ } from "./chunk-GIX5SAAB.js";
7
+ import "./chunk-3FWV6CFC.js";
8
8
  import "./chunk-3V37IID6.js";
9
- import "./chunk-ROULPSQC.js";
10
- import "./chunk-NJZSO7BK.js";
11
- import "./chunk-BNFG2IFK.js";
12
- import "./chunk-I3JP3D4A.js";
13
- import "./chunk-7XQ6TK3R.js";
14
- import "./chunk-SXPTRFPG.js";
15
- import "./chunk-2PGRQ62K.js";
9
+ import "./chunk-V7RH56HU.js";
10
+ import "./chunk-NKVLBB65.js";
11
+ import "./chunk-IBFC25IX.js";
12
+ import "./chunk-M4U4YNRE.js";
13
+ import "./chunk-OM5VDM6F.js";
14
+ import "./chunk-O3PEDF3M.js";
15
+ import "./chunk-EYFJ74MJ.js";
16
16
  import "./chunk-WPALEIRX.js";
17
17
  import "./chunk-RKUAD3I6.js";
18
18
  export {
@@ -5,6 +5,7 @@ import {
5
5
  filing_cap,
6
6
  git_gh_api_path,
7
7
  git_gh_exec,
8
+ lane_background,
8
9
  lane_child_invocation,
9
10
  lane_park,
10
11
  lane_registry,
@@ -15,20 +16,20 @@ import {
15
16
  run_hold,
16
17
  run_invocation,
17
18
  run_watcher_guard
18
- } from "./chunk-GQ3EVWHY.js";
19
+ } from "./chunk-3FWV6CFC.js";
19
20
  import {
20
21
  session_language
21
22
  } from "./chunk-6V5OPIUU.js";
22
23
  import {
23
24
  hook_decision,
24
25
  lane_child_marker
25
- } from "./chunk-SXPTRFPG.js";
26
+ } from "./chunk-O3PEDF3M.js";
26
27
  import {
27
28
  PROJECT_ROOT,
28
29
  stamp_file,
29
30
  time_density_hook,
30
31
  time_hook_transcript
31
- } from "./chunk-2PGRQ62K.js";
32
+ } from "./chunk-EYFJ74MJ.js";
32
33
  import "./chunk-WPALEIRX.js";
33
34
  import "./chunk-RKUAD3I6.js";
34
35
 
@@ -1186,8 +1187,12 @@ function count_headless_refusals(tail) {
1186
1187
  const last_wait = Math.max(...HEADLESS_WAIT_CALLS.map((call) => tail.lastIndexOf(call)));
1187
1188
  return tail.slice(Math.max(last_wait, 0)).split(HEADLESS_WAIT_MARKER).length - 1;
1188
1189
  }
1190
+ function needs_background_wait(context) {
1191
+ return context.lane_child && context.background_pending;
1192
+ }
1189
1193
  function hold_reason(context) {
1190
1194
  if (needs_index_route(context)) return LANE_INDEX_REASON;
1195
+ if (needs_background_wait(context)) return lane_background.LANE_BACKGROUND_STOP_REASON;
1191
1196
  if (needs_notify(context)) return STOP_NOTIFY_REASON;
1192
1197
  if (needs_release(context)) return HOLD_RELEASE_REASON;
1193
1198
  return void 0;
@@ -1558,6 +1563,7 @@ async function build_context(transcript_path, payload_tail) {
1558
1563
  headless_waiting: await run_headless.must_keep_waiting(),
1559
1564
  headless_refusals: stop_rules.count_headless_refusals(tail),
1560
1565
  lane_child: lane_child_marker.is_child_of(process.cwd()),
1566
+ background_pending: lane_background.pending_background_ids(tail).length > 0,
1561
1567
  session_lang: session_lang()
1562
1568
  };
1563
1569
  }
package/dist/josh.js CHANGED
@@ -262,7 +262,6 @@ var ENV_FILE_NAME = ".env";
262
262
  // scripts/josh/josh-command-types.ts
263
263
  var OPTIONAL_ENV_FILE_FLAGS = [`--env-file-if-exists=${ENV_FILE_NAME}`];
264
264
  var GATE_COMMAND = "gate";
265
- var PE = ["pnpm", "exec"];
266
265
  var ESLINT_CACHE_FILE = ".eslintcache";
267
266
  var TS_BUILD_INFO_FILE = ".tsbuildinfo";
268
267
  var CSPELL_CACHE_FILE = ".cspellcache";
@@ -300,7 +299,6 @@ function content_cache_flags(cache_file) {
300
299
  var ESLINT_CACHE_FLAGS = content_cache_flags(ESLINT_CACHE_FILE);
301
300
  var ESLINT_EDIT_CACHE_FLAGS = content_cache_flags(ESLINT_EDIT_CACHE_FILE);
302
301
  var ESLINT_RELATED_CACHE_FLAGS = content_cache_flags(ESLINT_RELATED_CACHE_FILE);
303
- var TS_CACHE_FLAGS = ["--incremental", "--tsBuildInfoFile", TS_BUILD_INFO_FILE];
304
302
  var CSPELL_CACHE_FLAGS = content_cache_flags(CSPELL_CACHE_FILE);
305
303
 
306
304
  // scripts/josh/josh-commands-backlog.ts
@@ -659,6 +657,12 @@ var AI_COMMANDS = {
659
657
  reference: ["[issue|--force]", "automation", ["files"]],
660
658
  default_script_arguments: ["--release"]
661
659
  },
660
+ "run:tidy": {
661
+ script: "scripts/run/run-tidy-cli.ts",
662
+ description: "Close merged lanes and drop stashes whose issues are all merged",
663
+ category: "AI tools",
664
+ reference: ["", "automation", ["git", "network", "files"]]
665
+ },
662
666
  "run:carry": {
663
667
  script: "scripts/run/run-carry-cli.ts",
664
668
  description: "Carry one invocation\u2019s budget across its own session cuts",
@@ -855,7 +859,7 @@ var DEV_COMMANDS = {
855
859
  },
856
860
  lint: {
857
861
  script: "scripts/lint/lint-parallel.ts",
858
- description: "Check code with prettier and eslint",
862
+ description: "Check code with prettier and eslint (skips a tool a static project lacks)",
859
863
  category: "Development",
860
864
  reference: ["", "developer", ["processes"]]
861
865
  },
@@ -891,7 +895,7 @@ var DEV_COMMANDS = {
891
895
  },
892
896
  format: {
893
897
  script: "scripts/lint/format.ts",
894
- description: "Format code with prettier and eslint",
898
+ description: "Format code with prettier and eslint (skips a tool a static project lacks)",
895
899
  category: "Development",
896
900
  reference: ["", "developer", ["files", "processes"]]
897
901
  },
@@ -1003,8 +1007,8 @@ var DEV_COMMANDS = {
1003
1007
  argument_targets: ["test:unit", "test:e2e"]
1004
1008
  },
1005
1009
  check: {
1006
- shell: [...PE, "tsc", "--noEmit", ...TS_CACHE_FLAGS],
1007
- description: "Type-check TypeScript project",
1010
+ script: "scripts/gate/type-check-command.ts",
1011
+ description: "Type-check with tsc (skips a static project with no TypeScript to check)",
1008
1012
  category: "Development",
1009
1013
  reference: ["[arguments...]", "developer", ["processes"]]
1010
1014
  },
@@ -1502,6 +1506,7 @@ var ALIASES = {
1502
1506
  rh: "run:hold",
1503
1507
  rr: "run:release",
1504
1508
  rc: "run:carry",
1509
+ rtd: "run:tidy",
1505
1510
  rw: "run:wake",
1506
1511
  rct: "run:cut",
1507
1512
  rv: "run:liveness",
package/package.json CHANGED
@@ -1,6 +1,17 @@
1
1
  {
2
- "version": "1.929.0",
2
+ "version": "1.947.0",
3
3
  "name": "@joshuafolkken/kit",
4
+ "description": "Development toolkit for AI-assisted projects: formatting, AI assistant rules and a Git/GitHub Issue workflow for any repository, plus lint, type check, tests and hooks for Node projects",
5
+ "keywords": [
6
+ "ai",
7
+ "claude-code",
8
+ "cli",
9
+ "developer-tools",
10
+ "eslint",
11
+ "prettier",
12
+ "toolkit",
13
+ "workflow"
14
+ ],
4
15
  "type": "module",
5
16
  "license": "MIT",
6
17
  "repository": {
@@ -177,6 +188,7 @@
177
188
  "@eslint/compat": "^2.1.1",
178
189
  "@eslint/js": "^10.0.1",
179
190
  "@ianvs/prettier-plugin-sort-imports": "^4.7.1",
191
+ "@playwright/test": "^1.63.0",
180
192
  "@secretlint/secretlint-rule-preset-recommend": "^13.0.6",
181
193
  "@stylistic/eslint-plugin": "^5.10.0",
182
194
  "@types/node": "^26.6.3",
@@ -76,7 +76,7 @@ Issue には次の要素を必ず含める。
76
76
  - **互換性を壊す変更は影響を本文で宣言する。** 既存の利用者、API、設定の契約を変える場合は、`## 背景` の直下に `- 互換性: 破壊的変更` を書き、廃止対象と影響を説明する。移行措置や事前案内があっても破壊的変更なら宣言する。`pnpm josh issue:lint <本文ファイル>` が `labels: breaking-change` を返したら、同じ作成呼び出しに `-f 'labels[]=breaking-change'` を付ける。互換性を保つ変更には付けない。影響が曖昧なら起票文案で対象の利用者と契約を明確にしてから分類する
77
77
  - **`issue:lint` が返す分類ラベルをすべて起票時に指定する。** `bug`、`enhancement`、`breaking-change` は組み合わせて表示されることがある。`depth`・`route` など既存のラベルを残し、`labels: none` のときは分類ラベルを付けない。Issue が `enhancement` と `breaking-change` の両方を持つとき、PR のリリース分類は `breaking-change` 一つにする。Issue の目的の分類と PR のリリース分類を区別する
78
78
  - **`## 発火点`** には、その規則が破れる瞬間に最初に呼ばれるツールコール名を 1 つ書く。名前は `prompts/collaboration-workflow/rule-delivery.md` の配送表に載る、フックで配送可能なツール(`Bash` / `Edit` / `Read` / `Write` / `AskUserQuestion`)と突き合わせる。**表に無い名前**(そもそもツールコール名でない)と**不一致**(既知のツールだが配送できない)は区別して印字され、joshuafolkken/kit#2201 の「規則は正しいが名指ししたコールが実際に破れるコールの 1 つ後ろ」を起票時に検出する
79
- - **`## ベースライン`** は **「コマンド + 値」** で書く。``- `<測定コマンド>` → <現在の値>`` の形に限り、自然文だけの測定説明は受け付けない(再実行できないため)。マージ後に `pnpm josh measure:rerun` が同じコマンドをやり直し、**before / after の対**を印字する。値が動かなかったとき、反証された前提として `docs/observations.md` に 1 行追記される(既存の append-only 台帳をそのまま使い、第 2 の台帳は作らない)
79
+ - **`## ベースライン`** は **「コマンド + 値」** で書く。``- `<測定コマンド>` → <現在の値>`` の形に限り、自然文だけの測定説明は受け付けない(再実行できないため)。マージ後に `pnpm josh measure:rerun` が同じコマンドをやり直し、**before / after の対**を印字する。値が動かなかったとき、反証された前提として `docs/maintainers/observations.md` に 1 行追記される(既存の append-only 台帳を使い、別の台帳は作らない)
80
80
  - **`## 再現`** は **「コマンド + 実際の出力」** で書く。コマンドを backtick、出力をフェンス(``` または ~~~)で囲む。自然文の「確認した」は再実行できないため受け付けない(`## ベースライン` と同じ理由)。読みだけの起票を起票時に弾くためである
81
81
  - **検査は `pnpm josh issue:lint <path>` が行う。** 対象判定・3 見出しの有無・発火点の配送可否・ベースラインが「コマンド + 値」か・再現が「コマンド + 実際の出力」かと、起票時に必要な分類ラベルを、起票前の本文ファイルからまとめて答える(未リントの起票は `pnpm josh rule:guard` が拒否する)
82
82
 
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import { fileURLToPath } from 'node:url'
3
+ import { execaSync } from 'execa'
3
4
  import { z } from 'zod'
4
5
 
5
6
  const BUGFIX = 'bugfix'
@@ -19,9 +20,10 @@ const LEGACY_LABELS: ReadonlySet<string> = new Set(['semver-major', 'semver-mino
19
20
 
20
21
  const LABEL = z.object({ name: z.string() })
21
22
  const USER = z.object({ login: z.string() })
22
- const PULL_REQUEST = z.object({ user: USER, labels: z.array(LABEL) })
23
+ const PULL_REQUEST = z.object({ number: z.number(), user: USER })
23
24
  const EVENT = z.object({
24
25
  pull_request: PULL_REQUEST,
26
+ repository: z.object({ full_name: z.string() }),
25
27
  })
26
28
 
27
29
  function classification_error(labels: ReadonlyArray<string>, author: string): string | undefined {
@@ -83,13 +85,32 @@ function select_issue_classification(issue_json: string): ReleaseClassification
83
85
  return label
84
86
  }
85
87
 
86
- function check_event(event_path: string): string | undefined {
88
+ // The labels as they stand when the check runs, not as the event carried them. `josh pr` opens the
89
+ // pull request and labels it in a second call, so the `opened` event carries none; judged from the
90
+ // payload, that run fails, and when it lands in the newer check suite GitHub keeps its red result —
91
+ // a rerun replays the same payload and fails again (joshuafolkken/kit#2712).
92
+ function fetch_current_labels(repository: string, pull_number: number): ReadonlyArray<string> {
93
+ const result = execaSync(
94
+ 'gh',
95
+ ['api', `repos/${repository}/issues/${String(pull_number)}/labels`, '--jq', '.[].name'],
96
+ { reject: false },
97
+ )
98
+
99
+ if (result.exitCode !== 0) {
100
+ throw new Error(`Could not read the pull request labels: ${result.stderr}`)
101
+ }
102
+
103
+ return result.stdout.split('\n').filter((name) => name !== '')
104
+ }
105
+
106
+ function check_event(
107
+ event_path: string,
108
+ read_labels: typeof fetch_current_labels = fetch_current_labels,
109
+ ): string | undefined {
87
110
  const event = EVENT.parse(JSON.parse(readFileSync(event_path, 'utf8')))
111
+ const labels = read_labels(event.repository.full_name, event.pull_request.number)
88
112
 
89
- return classification_error(
90
- event.pull_request.labels.map((label) => label.name),
91
- event.pull_request.user.login,
92
- )
113
+ return classification_error(labels, event.pull_request.user.login)
93
114
  }
94
115
 
95
116
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
@@ -82,7 +82,7 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
82
82
  { path: '.claude/skills/workflow-commands/rule-residency.md', bytes: 24_576 },
83
83
  { path: '.claude/skills/workflow-commands/target-repository.md', bytes: 8192 },
84
84
  { path: '.claude/skills/workflow-commands/working-tree-hold.md', bytes: 8192 },
85
- { path: 'docs/josh-commands.md', bytes: 163_840 },
85
+ { path: 'docs/josh-commands.md', bytes: 167_936 },
86
86
  { path: 'prompts/agent-rules.md', bytes: 4096 },
87
87
  { path: 'prompts/coding-standards.md', bytes: 16_384 },
88
88
  { path: 'prompts/collaboration-workflow.md', bytes: 8192 },
@@ -53,7 +53,7 @@ function is_force_requested(force_environment: string): boolean {
53
53
  // nothing" for "git could not be asked". Every predicate below treats the two differently.
54
54
  //
55
55
  // **The observation ledger is dropped from the reading, and it is the one line that may be**
56
- // (joshuafolkken/kit#1756). Since that issue `pnpm josh git` excludes `docs/observations.md` from
56
+ // (joshuafolkken/kit#1756). Since that issue `pnpm josh git` excludes the ledger from
57
57
  // what it stages, so a parent's appended line sits modified-but-never-staged for the whole interval
58
58
  // between the append and the next `pnpm josh observations:flush` — and every condition below reads a
59
59
  // non-empty status as "this operation carries a tree no check has read". Left in, one ledger line
@@ -2,6 +2,7 @@ import { existsSync, globSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { find_local_bin_upwards } from '#scripts/build/local-bin'
4
4
  import { SKIP_MARKER } from '#scripts/test/skip-marker'
5
+ import { z } from 'zod'
5
6
 
6
7
  const PACKAGE_JSON = 'package.json'
7
8
  const IGNORED_DIRS = new Set([
@@ -36,6 +37,8 @@ const CSPELL_CONFIGS = [
36
37
  'cspell.yaml',
37
38
  ]
38
39
  const TYPE_CONFIGS = ['tsconfig.json', 'jsconfig.json']
40
+ const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies']
41
+ const object_schema = z.record(z.string(), z.unknown())
39
42
 
40
43
  function project_root(directory: string): string {
41
44
  let current = directory
@@ -78,50 +81,77 @@ function is_static_manifest(manifest: unknown): boolean {
78
81
  return josh.profile === 'static'
79
82
  }
80
83
 
81
- function is_static(directory: string): boolean {
84
+ function read_manifest(directory: string): unknown {
82
85
  try {
83
86
  const manifest_path = path.join(project_root(directory), PACKAGE_JSON)
84
- const manifest: unknown = JSON.parse(readFileSync(manifest_path, 'utf8'))
85
87
 
86
- return is_static_manifest(manifest)
88
+ return JSON.parse(readFileSync(manifest_path, 'utf8'))
87
89
  } catch {
88
- return false
90
+ return undefined
89
91
  }
90
92
  }
91
93
 
94
+ function is_static(directory: string): boolean {
95
+ return is_static_manifest(read_manifest(directory))
96
+ }
97
+
98
+ function is_declared(directory: string, package_name: string): boolean {
99
+ const manifest = object_schema.safeParse(read_manifest(directory))
100
+ if (!manifest.success) return false
101
+
102
+ return DEPENDENCY_FIELDS.some((field) => {
103
+ const dependencies = object_schema.safeParse(manifest.data[field])
104
+
105
+ return dependencies.success && Object.hasOwn(dependencies.data, package_name)
106
+ })
107
+ }
108
+
109
+ // A tool the manifest lists but `node_modules` lacks is a project that has not run `pnpm install`
110
+ // yet — typically right after `josh init` added it (joshuafolkken/kit#2709). Skipping it would pass a
111
+ // check the configuration asks for without running it, so only an undeclared tool is skipped; a
112
+ // declared one is run and fails until it is installed.
113
+ function missing_tool_reason(
114
+ directory: string,
115
+ package_name: string,
116
+ bin_name: string,
117
+ ): string | undefined {
118
+ if (has_bin(directory, bin_name) || is_declared(directory, package_name)) return undefined
119
+
120
+ return `${package_name} is not installed`
121
+ }
122
+
92
123
  function skip_notice(check: string, reason: string): string {
93
124
  return `josh ${check}: ${reason} ${SKIP_MARKER} ${check}.`
94
125
  }
95
126
 
127
+ // Shared by `josh check` and the gate's type-check step, so neither reaches `tsc` on a static
128
+ // project that has nothing to type-check (joshuafolkken/kit#2709).
96
129
  function type_check_skip_reason(directory: string): string | undefined {
97
130
  if (!is_static(directory)) return undefined
98
131
  if (!has_files(directory, TYPE_FILES)) return 'no TypeScript files were found'
99
132
  if (!has_config(directory, TYPE_CONFIGS)) return 'no TypeScript configuration was found'
100
- if (!has_bin(directory, 'tsc')) return 'typescript is not installed'
101
133
 
102
- return undefined
134
+ return missing_tool_reason(directory, 'typescript', 'tsc')
103
135
  }
104
136
 
105
- // Shared by `josh lint` and `josh format`, so a static project without Web files — a Python or Rust
106
- // project, which `josh init` gives no Prettier — is skipped for the same reason by both
107
- // (joshuafolkken/kit#2606).
137
+ // Shared by `josh lint`, `josh lint:related` and `josh format`, so a static project without Web
138
+ // files — a Python or Rust project, which `josh init` gives no Prettier — is skipped for the same
139
+ // reason by all three (joshuafolkken/kit#2606, joshuafolkken/kit#2709).
108
140
  function prettier_skip_reason(directory: string): string | undefined {
109
141
  if (!is_static(directory)) return undefined
110
142
  if (!has_files(directory, WEB_FILES)) return 'no HTML, CSS or JavaScript files were found'
111
- if (!has_bin(directory, 'prettier')) return 'prettier is not installed'
112
143
 
113
- return undefined
144
+ return missing_tool_reason(directory, 'prettier', 'prettier')
114
145
  }
115
146
 
116
- // Shared by `josh lint` and `josh format`, so a static project without ESLint is skipped for the
117
- // same reason by both (joshuafolkken/kit#2693).
147
+ // Shared by `josh lint`, `josh lint:related` and `josh format`, so a static project without ESLint
148
+ // is skipped for the same reason by all three (joshuafolkken/kit#2693, joshuafolkken/kit#2709).
118
149
  function eslint_skip_reason(directory: string): string | undefined {
119
150
  if (!is_static(directory)) return undefined
120
151
  if (!has_files(directory, SCRIPT_FILES)) return 'no JavaScript or TypeScript files were found'
121
152
  if (!has_config(directory, ESLINT_CONFIGS)) return 'no ESLint configuration was found'
122
- if (!has_bin(directory, 'eslint')) return 'eslint is not installed'
123
153
 
124
- return undefined
154
+ return missing_tool_reason(directory, 'eslint', 'eslint')
125
155
  }
126
156
 
127
157
  const project_checks = {
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env tsx
2
+ import { fileURLToPath } from 'node:url'
3
+ import { TS_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
4
+ import { execa } from 'execa'
5
+ import { project_checks } from './project-checks'
6
+
7
+ // `josh check` — the project-wide TypeScript check. It used to be a bare `tsc --noEmit` shell entry,
8
+ // so on a static project with nothing to type-check it failed with `Command "tsc" not found` while
9
+ // the gate, which asks `type_check_skip_reason` first, skipped the same step
10
+ // (joshuafolkken/kit#2709). Both now read that one decision.
11
+
12
+ const ARGV_OFFSET = 2
13
+ const FAIL_EXIT_CODE = 1
14
+ const TSC_ARGS = ['exec', 'tsc', '--noEmit', ...TS_CACHE_FLAGS] as const
15
+
16
+ async function run_type_check(
17
+ command_arguments: ReadonlyArray<string>,
18
+ directory: string,
19
+ ): Promise<number> {
20
+ const reason = project_checks.type_check_skip_reason(directory)
21
+
22
+ if (reason !== undefined) {
23
+ console.info(project_checks.skip_notice('check', reason))
24
+
25
+ return 0
26
+ }
27
+
28
+ const result = await execa('pnpm', [...TSC_ARGS, ...command_arguments], {
29
+ cwd: directory,
30
+ stdio: 'inherit',
31
+ reject: false,
32
+ })
33
+
34
+ return result.exitCode ?? FAIL_EXIT_CODE
35
+ }
36
+
37
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
38
+ process.exitCode = await run_type_check(process.argv.slice(ARGV_OFFSET), process.cwd())
39
+ }
40
+
41
+ const type_check_command = { run_type_check }
42
+
43
+ export { TSC_ARGS, type_check_command }
@@ -9,6 +9,9 @@ const GIT_COMMAND_UNIX = '/usr/bin/git'
9
9
  // here rather than beside either of them because `status` and `worktree_list` now sit in different
10
10
  // modules (joshuafolkken/kit#1640) and a copy in each is a second answer to one question.
11
11
  const PORCELAIN_FLAG = '--porcelain'
12
+ // The untracked-files mode every porcelain status reading passes (`git-command.ts` carries why it is
13
+ // passed and why it is `all`), shared so `run-carry-flush.ts`'s own reading cannot drift from it.
14
+ const UNTRACKED_FILES_FLAG = '--untracked-files=all'
12
15
 
13
16
  function get_git_command(): string {
14
17
  if (platform() === 'win32') {
@@ -37,5 +40,6 @@ export {
37
40
  UNTRACKED_FILE_PREFIX,
38
41
  SEPARATOR_LINE,
39
42
  PORCELAIN_FLAG,
43
+ UNTRACKED_FILES_FLAG,
40
44
  git_utilities,
41
45
  }