@joshuafolkken/kit 1.929.0 → 1.953.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.
- package/.claude/skills/workflow-commands/background-commands.md +6 -0
- package/.claude/skills/workflow-commands/backlogrun-child.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +4 -3
- package/.claude/skills/workflow-commands/observation-filing.md +3 -3
- package/.claude/skills/workflow-commands/prerequisite.md +1 -1
- package/.github/workflows/ci.yml +26 -8
- package/.github/workflows/pr-classification.yml +76 -0
- package/README.md +44 -22
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-GQ3EVWHY.js → chunk-452OKRQX.js} +421 -195
- package/dist/hooks/{chunk-2PGRQ62K.js → chunk-EYFJ74MJ.js} +11 -6
- package/dist/hooks/{chunk-LCJZE42I.js → chunk-IA7WC4NS.js} +1 -1
- package/dist/hooks/{chunk-BNFG2IFK.js → chunk-IBFC25IX.js} +4 -4
- package/dist/hooks/{chunk-I3JP3D4A.js → chunk-M4U4YNRE.js} +3 -3
- package/dist/hooks/{chunk-NJZSO7BK.js → chunk-NKVLBB65.js} +3 -3
- package/dist/hooks/{chunk-SXPTRFPG.js → chunk-O3PEDF3M.js} +1 -1
- package/dist/hooks/{chunk-7XQ6TK3R.js → chunk-OM5VDM6F.js} +2 -2
- package/dist/hooks/{chunk-ROULPSQC.js → chunk-V7RH56HU.js} +2 -2
- package/dist/hooks/{chunk-YO43HEPY.js → chunk-Z5TVZ4AP.js} +237 -235
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +9 -3
- package/dist/josh.js +11 -6
- package/dist/version/index.js +7 -4
- package/package.json +13 -1
- package/prompts/collaboration-workflow/issue-template.md +1 -1
- package/scripts/ci/pr-classification.ts +27 -6
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/gate/hook-gate-reuse.ts +1 -1
- package/scripts/gate/project-checks.ts +45 -15
- package/scripts/gate/type-check-command.ts +43 -0
- package/scripts/git/constants.ts +4 -0
- package/scripts/git/git-command.ts +11 -5
- package/scripts/git/git-gh-pr-rollup.ts +56 -16
- package/scripts/git/git-staging.ts +6 -4
- package/scripts/git/git-stash.ts +73 -1
- package/scripts/git/git-worktree.ts +35 -4
- package/scripts/git/repository-lock.ts +138 -0
- package/scripts/git/stash-orphans.ts +24 -2
- package/scripts/git/stash-sweep-lock.ts +30 -0
- package/scripts/hooks/stop-guard.ts +2 -0
- package/scripts/init/init-actions.ts +3 -2
- package/scripts/init/init-logic-migrate.ts +10 -1
- package/scripts/init/init-logic-templates.ts +27 -7
- package/scripts/init/init-logic-workspace.ts +52 -1
- package/scripts/init/init-logic.ts +6 -1
- package/scripts/init/init.ts +60 -19
- package/scripts/issue/issue-merged.ts +75 -0
- package/scripts/issue/measure-rerun-cli.ts +2 -2
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +10 -0
- package/scripts/josh/josh-command-map.ts +1 -0
- package/scripts/josh/josh-command-types.ts +0 -3
- package/scripts/josh/josh-commands-ai.ts +6 -0
- package/scripts/josh/josh-commands-development.ts +5 -5
- package/scripts/lint/lint-parallel.ts +19 -18
- package/scripts/observations/observation-ledger-home.ts +22 -2
- package/scripts/observations/observation-ledger-line.ts +11 -0
- package/scripts/observations/observation-ledger-migrate.ts +146 -0
- package/scripts/observations/observation-ledger.ts +51 -3
- package/scripts/observations/observations-flush.ts +38 -7
- package/scripts/review/review-finding-ledger.ts +1 -1
- package/scripts/review/review-record-cli.ts +1 -7
- package/scripts/rules/delivered-rules.ts +10 -0
- package/scripts/rules/lane-background.ts +111 -0
- package/scripts/rules/stop-rules.ts +15 -2
- package/scripts/run/run-carry-flush.ts +2 -7
- package/scripts/run/run-hold-cli.ts +10 -2
- package/scripts/run/run-tidy-cli.ts +50 -0
- package/scripts/run/run-tidy-lanes.ts +110 -0
- package/scripts/run/run-tidy-stashes.ts +160 -0
- package/scripts/run/run-tidy.ts +101 -0
- package/scripts/safe-chain/ci-installer-pin.ts +90 -0
- package/scripts/safe-chain/preinstall-command.ts +39 -0
- package/scripts/safe-chain/project-config.ts +55 -10
- package/scripts/sync/sync.ts +8 -3
- package/scripts/test/pilot-files.ts +0 -1
- package/scripts/version/preinstall-version-update.ts +2 -0
- package/scripts/version/release-age.ts +11 -6
- package/templates/workflows/ci.yml +21 -7
- package/tsconfig/base.json +4 -1
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import {
|
|
3
3
|
pretool_guard
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-Z5TVZ4AP.js";
|
|
5
|
+
import "./chunk-452OKRQX.js";
|
|
6
6
|
import {
|
|
7
7
|
build_envelope,
|
|
8
8
|
compose_context,
|
|
9
9
|
format_edited_payload
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-IA7WC4NS.js";
|
|
11
11
|
import "./chunk-3V37IID6.js";
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
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-
|
|
19
|
+
} from "./chunk-O3PEDF3M.js";
|
|
20
20
|
import {
|
|
21
21
|
time_density_hook
|
|
22
|
-
} from "./chunk-
|
|
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-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
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-
|
|
18
|
+
} from "./chunk-IA7WC4NS.js";
|
|
19
19
|
import "./chunk-3V37IID6.js";
|
|
20
|
-
import "./chunk-
|
|
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-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
17
|
-
import "./chunk-
|
|
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-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-Z5TVZ4AP.js";
|
|
7
|
+
import "./chunk-452OKRQX.js";
|
|
8
8
|
import "./chunk-3V37IID6.js";
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
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 {
|
package/dist/hooks/stop-guard.js
CHANGED
|
@@ -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-
|
|
19
|
+
} from "./chunk-452OKRQX.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-
|
|
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-
|
|
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
|
-
|
|
1007
|
-
description: "Type-check TypeScript
|
|
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/dist/version/index.js
CHANGED
|
@@ -163,9 +163,12 @@ var NO_QUARANTINE_MINUTES = 0;
|
|
|
163
163
|
var NPMRC_PATH = ".npmrc";
|
|
164
164
|
var release_times_schema = z2.record(z2.string(), z2.string());
|
|
165
165
|
var MS_PER_MINUTE = 6e4;
|
|
166
|
-
function
|
|
166
|
+
function parse_declared_minimum_release_age(npmrc_content) {
|
|
167
167
|
const raw = MINIMUM_RELEASE_AGE_RE.exec(npmrc_content)?.[1];
|
|
168
|
-
return raw === void 0 ?
|
|
168
|
+
return raw === void 0 ? void 0 : Number(raw);
|
|
169
|
+
}
|
|
170
|
+
function parse_minimum_release_age(npmrc_content) {
|
|
171
|
+
return parse_declared_minimum_release_age(npmrc_content) ?? NO_QUARANTINE_MINUTES;
|
|
169
172
|
}
|
|
170
173
|
function is_on_major(version, major) {
|
|
171
174
|
return major === void 0 || String(semver.major(version)) === major;
|
|
@@ -184,8 +187,7 @@ function select_aged_version(times, major, minimum_age_minutes, now_ms) {
|
|
|
184
187
|
}
|
|
185
188
|
function read_declared_minimum_release_age(npmrc_path) {
|
|
186
189
|
try {
|
|
187
|
-
|
|
188
|
-
return raw === void 0 ? void 0 : Number(raw);
|
|
190
|
+
return parse_declared_minimum_release_age(readFileSync2(npmrc_path, "utf8"));
|
|
189
191
|
} catch {
|
|
190
192
|
return void 0;
|
|
191
193
|
}
|
|
@@ -206,6 +208,7 @@ function read_nearest_minimum_release_age(start, boundary) {
|
|
|
206
208
|
var release_age = {
|
|
207
209
|
release_times_schema,
|
|
208
210
|
parse_minimum_release_age,
|
|
211
|
+
parse_declared_minimum_release_age,
|
|
209
212
|
read_minimum_release_age,
|
|
210
213
|
read_nearest_minimum_release_age,
|
|
211
214
|
select_aged_version
|
package/package.json
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.953.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
|
|
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({
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
88
|
+
return JSON.parse(readFileSync(manifest_path, 'utf8'))
|
|
87
89
|
} catch {
|
|
88
|
-
return
|
|
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
|
|
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
|
|
106
|
-
// project, which `josh init` gives no Prettier — is skipped for the same
|
|
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
|
|
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
|
|
117
|
-
// same reason by
|
|
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
|
|
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 }
|
package/scripts/git/constants.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PORCELAIN_FLAG } from './constants'
|
|
1
|
+
import { PORCELAIN_FLAG, UNTRACKED_FILES_FLAG } from './constants'
|
|
2
2
|
import { git_push_transport } from './git-push-transport'
|
|
3
3
|
import { git_spawn } from './git-spawn'
|
|
4
4
|
|
|
@@ -6,15 +6,21 @@ async function branch(): Promise<string> {
|
|
|
6
6
|
return await git_spawn.read(['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// **The untracked-files mode is passed rather than inherited** (joshuafolkken/kit#1381). Every
|
|
10
10
|
// reader of this output depends on the `??` lines being there: `git-staging.ts` stages exactly those,
|
|
11
11
|
// and `hook-gate-reuse.ts` reads an empty output as "this push carries the recorded tree". A person
|
|
12
12
|
// with `status.showUntrackedFiles=no` in their git config — a common setting on large repositories —
|
|
13
13
|
// gets porcelain output with those lines silently absent, so untracked files go unstaged and the
|
|
14
|
-
// pre-push hook reuses a record for a commit that does not contain them. Naming
|
|
15
|
-
//
|
|
14
|
+
// pre-push hook reuses a record for a commit that does not contain them. Naming the mode makes the
|
|
15
|
+
// reading answer to this codebase rather than to whoever ran it.
|
|
16
|
+
//
|
|
17
|
+
// **`all` rather than `normal`, so a new file is named even inside an untracked directory**
|
|
18
|
+
// (joshuafolkken/kit#2724). `normal` collapses it to `?? docs/maintainers/`, and every reader that
|
|
19
|
+
// recognizes the observation ledger by its path then misses it: `git-staging.ts` stages the directory
|
|
20
|
+
// — ledger included — into an unrelated pull request, and the flush refuses the directory as "other
|
|
21
|
+
// changes".
|
|
16
22
|
async function status(): Promise<string> {
|
|
17
|
-
return await git_spawn.read(['status', PORCELAIN_FLAG,
|
|
23
|
+
return await git_spawn.read(['status', PORCELAIN_FLAG, UNTRACKED_FILES_FLAG])
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
// The absolute path every other git command's output is relative to. Asking git rather than reading
|