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