@joshuafolkken/kit 1.983.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/workflow-commands/backlogrun-child.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-steps.md +1 -1
- 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-ZT65LR3U.js → chunk-4GDR2YTV.js} +4 -4
- package/dist/hooks/{chunk-ASUZGZFT.js → chunk-56LGMJJV.js} +2 -2
- package/dist/hooks/{chunk-LBXQHXMC.js → chunk-762BDXDU.js} +220 -68
- package/dist/hooks/{chunk-IM3KM25Y.js → chunk-7WAOP42A.js} +5 -3
- package/dist/hooks/{chunk-P3UX7SFN.js → chunk-A2H5I2VD.js} +2 -2
- package/dist/hooks/{chunk-ERXMJJOV.js → chunk-AWTFKKFW.js} +1 -1
- package/dist/hooks/{chunk-APVOACVT.js → chunk-GBJUERYK.js} +1 -1
- package/dist/hooks/{chunk-5JX6HXDX.js → chunk-KPZN42RT.js} +3 -3
- package/dist/hooks/{chunk-47SPMGOD.js → chunk-PAXMLGSS.js} +26 -7
- package/dist/hooks/{chunk-RFPHKU7C.js → chunk-WMIR5SNX.js} +3 -3
- 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 +98 -26
- package/dist/josh.js +5 -5
- 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/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/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/josh/__snapshots__/josh-command-map.test.ts.snap +3 -3
- package/scripts/josh/josh-commands-development.ts +3 -3
- package/scripts/josh/josh-commands-project.ts +2 -2
- 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/rules/filing-cap.ts +1 -0
- package/scripts/rules/issue-citation.ts +30 -1
- package/scripts/rules/last-prompt.ts +79 -0
- package/scripts/rules/stop-rules.ts +5 -3
- 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 -1
- 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-ai/git-workflow.ts +10 -0
- package/templates/CLAUDE.basic.md +8 -0
- package/templates/workflows/ci.yml +27 -0
- /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
|
@@ -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: 180_224 },
|
|
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 },
|
|
@@ -19,7 +19,7 @@ async function build_gate_step(
|
|
|
19
19
|
start_directory: string,
|
|
20
20
|
plan: GatePlan = gate_plan.resolve_gate_plan(),
|
|
21
21
|
): Promise<GateStep> {
|
|
22
|
-
const cwd = project_checks.
|
|
22
|
+
const cwd = project_checks.is_basic(start_directory)
|
|
23
23
|
? project_checks.project_root(start_directory)
|
|
24
24
|
: undefined
|
|
25
25
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { project_profile } from '#scripts/init/project-profile'
|
|
4
5
|
import { SKIP_MARKER } from '#scripts/test/skip-marker'
|
|
5
6
|
import { z } from 'zod'
|
|
6
7
|
|
|
@@ -74,11 +75,11 @@ function josh_settings(manifest: unknown): unknown {
|
|
|
74
75
|
return manifest.josh
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
function
|
|
78
|
+
function is_basic_manifest(manifest: unknown): boolean {
|
|
78
79
|
const josh = josh_settings(manifest)
|
|
79
80
|
if (typeof josh !== 'object' || josh === null || !('profile' in josh)) return false
|
|
80
81
|
|
|
81
|
-
return josh.profile === '
|
|
82
|
+
return project_profile.parse_profile(josh.profile) === 'basic'
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
function read_manifest(directory: string): unknown {
|
|
@@ -91,8 +92,8 @@ function read_manifest(directory: string): unknown {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
function
|
|
95
|
-
return
|
|
95
|
+
function is_basic(directory: string): boolean {
|
|
96
|
+
return is_basic_manifest(read_manifest(directory))
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
function is_declared(directory: string, package_name: string): boolean {
|
|
@@ -124,30 +125,30 @@ function skip_notice(check: string, reason: string): string {
|
|
|
124
125
|
return `josh ${check}: ${reason} ${SKIP_MARKER} ${check}.`
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
// Shared by `josh check` and the gate's type-check step, so neither reaches `tsc` on a
|
|
128
|
+
// Shared by `josh check` and the gate's type-check step, so neither reaches `tsc` on a basic
|
|
128
129
|
// project that has nothing to type-check (joshuafolkken/kit#2709).
|
|
129
130
|
function type_check_skip_reason(directory: string): string | undefined {
|
|
130
|
-
if (!
|
|
131
|
+
if (!is_basic(directory)) return undefined
|
|
131
132
|
if (!has_files(directory, TYPE_FILES)) return 'no TypeScript files were found'
|
|
132
133
|
if (!has_config(directory, TYPE_CONFIGS)) return 'no TypeScript configuration was found'
|
|
133
134
|
|
|
134
135
|
return missing_tool_reason(directory, 'typescript', 'tsc')
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
// Shared by `josh lint`, `josh lint:related` and `josh format`, so a
|
|
138
|
+
// Shared by `josh lint`, `josh lint:related` and `josh format`, so a basic project without Web
|
|
138
139
|
// files — a Python or Rust project, which `josh init` gives no Prettier — is skipped for the same
|
|
139
140
|
// reason by all three (joshuafolkken/kit#2606, joshuafolkken/kit#2709).
|
|
140
141
|
function prettier_skip_reason(directory: string): string | undefined {
|
|
141
|
-
if (!
|
|
142
|
+
if (!is_basic(directory)) return undefined
|
|
142
143
|
if (!has_files(directory, WEB_FILES)) return 'no HTML, CSS or JavaScript files were found'
|
|
143
144
|
|
|
144
145
|
return missing_tool_reason(directory, 'prettier', 'prettier')
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
// Shared by `josh lint`, `josh lint:related` and `josh format`, so a
|
|
148
|
+
// Shared by `josh lint`, `josh lint:related` and `josh format`, so a basic project without ESLint
|
|
148
149
|
// is skipped for the same reason by all three (joshuafolkken/kit#2693, joshuafolkken/kit#2709).
|
|
149
150
|
function eslint_skip_reason(directory: string): string | undefined {
|
|
150
|
-
if (!
|
|
151
|
+
if (!is_basic(directory)) return undefined
|
|
151
152
|
if (!has_files(directory, SCRIPT_FILES)) return 'no JavaScript or TypeScript files were found'
|
|
152
153
|
if (!has_config(directory, ESLINT_CONFIGS)) return 'no ESLint configuration was found'
|
|
153
154
|
|
|
@@ -165,7 +166,7 @@ const project_checks = {
|
|
|
165
166
|
has_bin,
|
|
166
167
|
has_config,
|
|
167
168
|
has_files,
|
|
168
|
-
|
|
169
|
+
is_basic,
|
|
169
170
|
prettier_skip_reason,
|
|
170
171
|
project_root,
|
|
171
172
|
skip_notice,
|
|
@@ -5,7 +5,7 @@ import { execa } from 'execa'
|
|
|
5
5
|
import { project_checks } from './project-checks'
|
|
6
6
|
|
|
7
7
|
// `josh check` — the project-wide TypeScript check. It used to be a bare `tsc --noEmit` shell entry,
|
|
8
|
-
// so on a
|
|
8
|
+
// so on a basic project with nothing to type-check it failed with `Command "tsc" not found` while
|
|
9
9
|
// the gate, which asks `type_check_skip_reason` first, skipped the same step
|
|
10
10
|
// (joshuafolkken/kit#2709). Both now read that one decision.
|
|
11
11
|
|
|
@@ -84,6 +84,18 @@ function has_same_issue_prefix(branch_a: string, branch_b: string): boolean {
|
|
|
84
84
|
return prefix_a !== undefined && prefix_a === prefix_b
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// The one rule `check_and_create_branch` refuses on, kept pure so `git-preflight.ts` asks it before
|
|
88
|
+
// anything is pulled, switched or staged.
|
|
89
|
+
function is_mismatch(
|
|
90
|
+
current_branch: string,
|
|
91
|
+
target_branch_name: string,
|
|
92
|
+
default_branch: string,
|
|
93
|
+
): boolean {
|
|
94
|
+
if (current_branch === default_branch || current_branch === target_branch_name) return false
|
|
95
|
+
|
|
96
|
+
return !has_same_issue_prefix(current_branch, target_branch_name)
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
async function handle_default_branch(target_branch_name: string): Promise<void> {
|
|
88
100
|
await pull_latest()
|
|
89
101
|
const is_branch_exists: boolean = await exists(target_branch_name)
|
|
@@ -103,9 +115,7 @@ async function check_and_create_branch(
|
|
|
103
115
|
return target_branch_name
|
|
104
116
|
}
|
|
105
117
|
|
|
106
|
-
if (current_branch
|
|
107
|
-
if (has_same_issue_prefix(current_branch, target_branch_name)) return current_branch
|
|
108
|
-
|
|
118
|
+
if (is_mismatch(current_branch, target_branch_name, default_branch)) {
|
|
109
119
|
git_error.display_branch_mismatch_error(current_branch, target_branch_name)
|
|
110
120
|
}
|
|
111
121
|
|
|
@@ -118,6 +128,7 @@ const git_branch = {
|
|
|
118
128
|
switch_to,
|
|
119
129
|
exists,
|
|
120
130
|
check_and_create_branch,
|
|
131
|
+
is_mismatch,
|
|
121
132
|
// Exported so a lane's branch name can be checked against the real matcher rather than against a
|
|
122
133
|
// second copy of the pattern (joshuafolkken/kit#1497). A copy is what let `lane/<N>` ship: nothing
|
|
123
134
|
// asserted that the name `lane:open` writes is one this function accepts.
|
package/scripts/git/git-issue.ts
CHANGED
|
@@ -92,14 +92,18 @@ async function lane_issue_title(branch_name: string, issue_number: string): Prom
|
|
|
92
92
|
return title
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function branch_derivation_error(branch_name: string): string {
|
|
96
|
+
return `Cannot derive issue info from branch "${branch_name}" in non-interactive mode. ${argument_hint(EXAMPLE_ISSUE_NUMBER)}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function branch_number(branch_name: string): string | undefined {
|
|
100
|
+
return BRANCH_NUMBER_PATTERN.exec(branch_name)?.[1]
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
async function derive_issue_input_from_branch(branch_name: string): Promise<string> {
|
|
96
104
|
const match = BRANCH_NUMBER_PATTERN.exec(branch_name)
|
|
97
105
|
|
|
98
|
-
if (match === null)
|
|
99
|
-
throw new Error(
|
|
100
|
-
`Cannot derive issue info from branch "${branch_name}" in non-interactive mode. ${argument_hint(EXAMPLE_ISSUE_NUMBER)}`,
|
|
101
|
-
)
|
|
102
|
-
}
|
|
106
|
+
if (match === null) throw new Error(branch_derivation_error(branch_name))
|
|
103
107
|
|
|
104
108
|
const [, number = '', slug = ''] = match
|
|
105
109
|
const title = is_lane_branch(branch_name, number)
|
|
@@ -162,6 +166,10 @@ const git_issue = {
|
|
|
162
166
|
get_and_display,
|
|
163
167
|
resolve_and_display,
|
|
164
168
|
derive_from_branch,
|
|
169
|
+
// The pure halves of the two resolutions above, read by `git-preflight.ts` before anything changes.
|
|
170
|
+
parse: parse_issue_input,
|
|
171
|
+
branch_number,
|
|
172
|
+
branch_derivation_error,
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
export type { IssueInfo }
|
package/scripts/git/git-pr.ts
CHANGED
|
@@ -202,22 +202,27 @@ async function create_for_issue(
|
|
|
202
202
|
)
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
// The label the pull request is opened with — also asked by `git-preflight.ts` before the commit, so
|
|
206
|
+
// a missing classification is reported beside the other unmet preconditions (joshuafolkken/kit#2817).
|
|
207
|
+
async function release_classification(
|
|
208
|
+
issue_number: string,
|
|
209
|
+
branch_name: string,
|
|
210
|
+
): Promise<ReleaseClassification> {
|
|
211
|
+
const existing_label = await existing_pr_label(branch_name)
|
|
210
212
|
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
+
if (existing_label !== undefined) return existing_label
|
|
213
214
|
|
|
214
|
-
const issue_json = await git_gh_command.issue_view_json(
|
|
215
|
+
const issue_json = await git_gh_command.issue_view_json(issue_number, 'labels,body')
|
|
215
216
|
|
|
216
217
|
if (issue_json === undefined) {
|
|
217
|
-
throw new Error(`Could not read issue #${
|
|
218
|
+
throw new Error(`Could not read issue #${issue_number} for release classification`)
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
|
|
221
|
+
return pr_classification.select_issue_classification(issue_json)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function create_with_issue_info(issue_info: IssueInfo, extra_body?: string): Promise<void> {
|
|
225
|
+
const label = await release_classification(issue_info.number, issue_info.branch_name)
|
|
221
226
|
|
|
222
227
|
await create_for_issue(issue_info, extra_body, label)
|
|
223
228
|
}
|
|
@@ -225,6 +230,7 @@ async function create_with_issue_info(issue_info: IssueInfo, extra_body?: string
|
|
|
225
230
|
const git_pr = {
|
|
226
231
|
create,
|
|
227
232
|
create_with_issue_info,
|
|
233
|
+
release_classification,
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
export { git_pr }
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { live_evidence } from '#scripts/review/live-evidence'
|
|
2
|
+
import { changed_paths } from './changed-paths'
|
|
3
|
+
import { git_branch } from './git-branch'
|
|
4
|
+
import { git_command } from './git-command'
|
|
5
|
+
import { git_gh_command } from './git-gh-command'
|
|
6
|
+
import { git_issue, type IssueInfo } from './git-issue'
|
|
7
|
+
import { git_pr } from './git-pr'
|
|
8
|
+
|
|
9
|
+
// joshuafolkken/kit#2817. A first `josh git -y` was refused once per unmet precondition — the title's
|
|
10
|
+
// issue number, the branch name, the issue's release classification — each only after the stage
|
|
11
|
+
// before it had already staged, switched or pushed. **Every precondition decidable at the start is
|
|
12
|
+
// asked here, before anything changes, and all of them are reported at once.** No rule is new: each
|
|
13
|
+
// answer comes from the function the later stage refuses with, so the two cannot drift apart.
|
|
14
|
+
|
|
15
|
+
interface PreflightInput {
|
|
16
|
+
cli_input: string | undefined
|
|
17
|
+
will_open_pr: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The issue the run resolves to and the branch it will commit on, when both are known.
|
|
21
|
+
interface Target {
|
|
22
|
+
number: string
|
|
23
|
+
branch_name: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Resolution {
|
|
27
|
+
target: Target | undefined
|
|
28
|
+
problems: Array<string>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function error_message(error: unknown): string {
|
|
32
|
+
return error instanceof Error ? error.message : String(error)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolve_from_branch(current_branch: string): Resolution {
|
|
36
|
+
const number = git_issue.branch_number(current_branch)
|
|
37
|
+
|
|
38
|
+
if (number === undefined) {
|
|
39
|
+
return { target: undefined, problems: [git_issue.branch_derivation_error(current_branch)] }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { target: { number, branch_name: current_branch }, problems: [] }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A branch is created only from the default branch, so on any other the run commits where it stands.
|
|
46
|
+
function resolve_parsed(
|
|
47
|
+
parsed: IssueInfo,
|
|
48
|
+
current_branch: string,
|
|
49
|
+
default_branch: string,
|
|
50
|
+
): Resolution {
|
|
51
|
+
const branch_name = current_branch === default_branch ? parsed.branch_name : current_branch
|
|
52
|
+
const target = { number: parsed.number, branch_name }
|
|
53
|
+
|
|
54
|
+
if (!git_branch.is_mismatch(current_branch, parsed.branch_name, default_branch)) {
|
|
55
|
+
return { target, problems: [] }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
target,
|
|
60
|
+
problems: [
|
|
61
|
+
`Branch mismatch: on "${current_branch}", but the title resolves to "${parsed.branch_name}".`,
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolve_from_title(
|
|
67
|
+
cli_input: string,
|
|
68
|
+
current_branch: string,
|
|
69
|
+
default_branch: string,
|
|
70
|
+
): Resolution {
|
|
71
|
+
try {
|
|
72
|
+
return resolve_parsed(git_issue.parse(cli_input), current_branch, default_branch)
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return { target: undefined, problems: [error_message(error)] }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function resolve_target(cli_input: string | undefined): Promise<Resolution> {
|
|
79
|
+
const current_branch = await git_branch.current()
|
|
80
|
+
|
|
81
|
+
if (cli_input === undefined) return resolve_from_branch(current_branch)
|
|
82
|
+
|
|
83
|
+
return resolve_from_title(cli_input, current_branch, await git_command.get_default_branch())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function classification_problems(target: Target): Promise<Array<string>> {
|
|
87
|
+
try {
|
|
88
|
+
await git_pr.release_classification(target.number, target.branch_name)
|
|
89
|
+
|
|
90
|
+
return []
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return [error_message(error)]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A notice rather than a refusal: the evidence is the output of running the change, which does not
|
|
97
|
+
// exist before the commit. Saying so now is what spares the round trip at `josh followup`.
|
|
98
|
+
async function announce_evidence(branch_name: string): Promise<void> {
|
|
99
|
+
// Before staging, a new file is still untracked, so the tracked diff alone would miss it.
|
|
100
|
+
const [paths, body] = await Promise.all([
|
|
101
|
+
changed_paths.read_changed_paths(false),
|
|
102
|
+
git_gh_command.pr_get_body(branch_name),
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
if (live_evidence.verdict_for(paths, body) !== 'required') return
|
|
106
|
+
|
|
107
|
+
console.info(
|
|
108
|
+
`💡 This change touches runtime code: the PR body needs a ${live_evidence.EVIDENCE_HEADING} section before \`josh followup\` merges it.`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function refusal(problems: ReadonlyArray<string>): Error {
|
|
113
|
+
const lines = problems.map((problem) => ` - ${problem}`)
|
|
114
|
+
|
|
115
|
+
return new Error(
|
|
116
|
+
[
|
|
117
|
+
`Preflight found ${String(problems.length)} unmet precondition(s); nothing was changed:`,
|
|
118
|
+
...lines,
|
|
119
|
+
].join('\n'),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function check(input: PreflightInput): Promise<void> {
|
|
124
|
+
const { target, problems } = await resolve_target(input.cli_input)
|
|
125
|
+
// The classification and the evidence belong to the pull request, so a `--skip-pr` run asks neither.
|
|
126
|
+
const pr_target = input.will_open_pr ? target : undefined
|
|
127
|
+
const pr_problems = pr_target === undefined ? [] : await classification_problems(pr_target)
|
|
128
|
+
const all_problems = [...problems, ...pr_problems]
|
|
129
|
+
|
|
130
|
+
if (all_problems.length > 0) throw refusal(all_problems)
|
|
131
|
+
|
|
132
|
+
if (pr_target !== undefined) await announce_evidence(pr_target.branch_name)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const git_preflight = { check }
|
|
136
|
+
|
|
137
|
+
export type { PreflightInput }
|
|
138
|
+
export { git_preflight }
|
|
@@ -61,6 +61,8 @@ const TASK_DEFINITIONS: Record<TelegramTaskType, TaskDefinition> = {
|
|
|
61
61
|
stranded: { icon: '🚨', label: 'Run stranded — nobody is driving it' },
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
const NOTIFY_SWITCH_KEY = 'JOSH_NOTIFY'
|
|
65
|
+
const NOTIFY_OFF_VALUE = 'off'
|
|
64
66
|
const NOT_CONFIGURED_PREFIX = 'Telegram is not configured'
|
|
65
67
|
const SEND_FAILED_PREFIX = 'Telegram notification failed'
|
|
66
68
|
const REDACTED = '<redacted>'
|
|
@@ -136,6 +138,14 @@ function load_config(): TelegramConfig {
|
|
|
136
138
|
return { bot_token: result.data.telegram_bot_token, chat_id: result.data.telegram_chat_id }
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
// **An explicit opt-out is not a missing credential** (joshuafolkken/kit#2821). A consumer who never
|
|
142
|
+
// meant to use Telegram had no way to say so, so every stop printed the #1564 failure as noise. Only
|
|
143
|
+
// the stated value `off` disables the send: an unset or mistyped switch still falls through to
|
|
144
|
+
// `load_config`, so a forgotten setup stays the loud failure #1564 made it.
|
|
145
|
+
function is_notify_disabled(): boolean {
|
|
146
|
+
return parse_environment_string(process.env[NOTIFY_SWITCH_KEY]).toLowerCase() === NOTIFY_OFF_VALUE
|
|
147
|
+
}
|
|
148
|
+
|
|
139
149
|
function build_header(task_type: TelegramTaskType, repo_name: string | undefined): string {
|
|
140
150
|
const { icon, label } = TASK_DEFINITIONS[task_type]
|
|
141
151
|
if (repo_name === undefined || repo_name.length === 0) return `${icon} ${label}`
|
|
@@ -210,7 +220,18 @@ function build_send_failure(error: unknown, config: TelegramConfig): Error {
|
|
|
210
220
|
// warning and exiting 0, so a message that reached nobody looked exactly like one that arrived.
|
|
211
221
|
//
|
|
212
222
|
// The failure it throws carries a `cause`, and that cause is redacted too — see `build_send_failure`.
|
|
223
|
+
//
|
|
224
|
+
// The opt-out is checked here, before `load_config`, so every caller — the tolerant form included —
|
|
225
|
+
// skips the same way and none has to repeat the check.
|
|
213
226
|
async function send(input: TelegramSendInput): Promise<void> {
|
|
227
|
+
if (is_notify_disabled()) {
|
|
228
|
+
console.info(
|
|
229
|
+
`🔕 Telegram notifications are disabled (${NOTIFY_SWITCH_KEY}=${NOTIFY_OFF_VALUE}).`,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
214
235
|
const config = load_config()
|
|
215
236
|
const text = build_text(input)
|
|
216
237
|
|
|
@@ -10,6 +10,7 @@ import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
|
10
10
|
import { filing_cap } from '#scripts/rules/filing-cap'
|
|
11
11
|
import { lane_background } from '#scripts/rules/lane-background'
|
|
12
12
|
import { lane_park } from '#scripts/rules/lane-park'
|
|
13
|
+
import { last_prompt } from '#scripts/rules/last-prompt'
|
|
13
14
|
import { stop_rules, type StopContext, type StopOutcome } from '#scripts/rules/stop-rules'
|
|
14
15
|
import { run_cut } from '#scripts/run/run-cut'
|
|
15
16
|
import { run_headless } from '#scripts/run/run-headless'
|
|
@@ -70,6 +71,7 @@ async function build_context(
|
|
|
70
71
|
tree_clean: !(await run_hold.is_tree_dirty()),
|
|
71
72
|
notified: lane_park.mentions_confirmation_notify(tail),
|
|
72
73
|
message: payload_tail.message,
|
|
74
|
+
prompt: last_prompt.prompt_text(tail),
|
|
73
75
|
stop_hook_active: payload_tail.stop_hook_active,
|
|
74
76
|
cut_pending: run_cut.carried_cut_sync() !== undefined,
|
|
75
77
|
filed: was_filed(tail),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The two references `josh init` writes into a basic project, under the paths they had before
|
|
2
|
+
// joshuafolkken/kit#2829 renamed the static profile. kit still ships both old paths so an earlier
|
|
3
|
+
// project keeps working; re-running `josh init` moves the project onto the new ones, which is what
|
|
4
|
+
// lets the old paths be retired once projects have moved.
|
|
5
|
+
const RENAMED_REFERENCES: ReadonlyArray<readonly [RegExp, string]> = [
|
|
6
|
+
[
|
|
7
|
+
/@node_modules\/@joshuafolkken\/kit\/dist\/CLAUDE\.static\.md(?![\w.])/gu,
|
|
8
|
+
'@node_modules/@joshuafolkken/kit/dist/CLAUDE.basic.md',
|
|
9
|
+
],
|
|
10
|
+
[/@joshuafolkken\/kit\/prettier\/static(?=['"])/gu, '@joshuafolkken/kit/prettier/basic'],
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
function migrate_basic_paths(content: string): string {
|
|
14
|
+
let migrated = content
|
|
15
|
+
|
|
16
|
+
for (const [pattern, replacement] of RENAMED_REFERENCES) {
|
|
17
|
+
migrated = migrated.replaceAll(pattern, () => replacement)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return migrated
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const basic_path_migration = { migrate_basic_paths }
|
|
24
|
+
export { basic_path_migration }
|
|
@@ -97,10 +97,32 @@ function ensure_claude_md_import(existing: string | undefined): string {
|
|
|
97
97
|
return `${CLAUDE_MD_BOOTSTRAP}\n\n${CLAUDE_MD_IMPORT_LINE}\n\n${updated}`
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// The lines `ensure_claude_md_import` writes. A basic project imports other rules, so a sync from
|
|
101
|
+
// before joshuafolkken/kit#2827, or a switch from full, leaves these behind for it to remove.
|
|
102
|
+
const FULL_CLAUDE_MD_LINES = new Set([
|
|
103
|
+
CLAUDE_MD_BOOTSTRAP,
|
|
104
|
+
OLD_CLAUDE_MD_BOOTSTRAP,
|
|
105
|
+
CLAUDE_MD_IMPORT_LINE,
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
// Each written line goes with the blank line after it, so the rest of the file is left as it was.
|
|
109
|
+
function remove_claude_md_import(content: string): string {
|
|
110
|
+
const lines = content.split('\n')
|
|
111
|
+
|
|
112
|
+
return lines
|
|
113
|
+
.filter((line, index) => {
|
|
114
|
+
if (FULL_CLAUDE_MD_LINES.has(line)) return false
|
|
115
|
+
|
|
116
|
+
return line !== '' || !FULL_CLAUDE_MD_LINES.has(lines[index - 1] ?? '')
|
|
117
|
+
})
|
|
118
|
+
.join('\n')
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
const distributed_paths = {
|
|
101
122
|
transform_prompt_paths,
|
|
102
123
|
transform_distributed_paths,
|
|
103
124
|
ensure_claude_md_import,
|
|
125
|
+
remove_claude_md_import,
|
|
104
126
|
CLAUDE_MD_IMPORT_LINE,
|
|
105
127
|
}
|
|
106
128
|
|
|
@@ -2,12 +2,14 @@ import { readFileSync } from 'node:fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { json_format } from '#scripts/config-merge/json-format'
|
|
4
4
|
import { string_array_schema, vscode_settings_schema } from '#scripts/lib/schemas'
|
|
5
|
+
import { basic_path_migration } from './basic-path-migration'
|
|
5
6
|
import { init_logic } from './init-logic'
|
|
6
7
|
import { package_path, PROJECT_ROOT } from './init-paths'
|
|
7
8
|
import type { ProjectShape } from './project-profile'
|
|
9
|
+
import { session_language_environment } from './session-language-environment'
|
|
8
10
|
|
|
9
11
|
const PRETTIER_CONFIG_JS = 'prettier.config.js'
|
|
10
|
-
const
|
|
12
|
+
const BASIC_PRETTIER_CONFIG = 'prettier.config.mjs'
|
|
11
13
|
const VSCODE_EXTENSIONS_PATH = '.vscode/extensions.json'
|
|
12
14
|
const VSCODE_SETTINGS_PATH = '.vscode/settings.json'
|
|
13
15
|
const TSCONFIG_PATH = 'tsconfig.json'
|
|
@@ -17,6 +19,9 @@ interface FileAction {
|
|
|
17
19
|
dest: string
|
|
18
20
|
create: () => string
|
|
19
21
|
merge?: (existing: string) => string
|
|
22
|
+
// A merge that only migrates a kit-written line, for a file kit otherwise leaves to the project:
|
|
23
|
+
// when it changes nothing, the file is reported with the sample to add, as one with no merge is.
|
|
24
|
+
should_show_sample_when_unchanged?: boolean
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
function read_package_file(relative_path: string): string {
|
|
@@ -59,7 +64,7 @@ function build_vscode_actions(): ReadonlyArray<FileAction> {
|
|
|
59
64
|
]
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
const
|
|
67
|
+
const BASIC_EXTENSIONS = [
|
|
63
68
|
'streetsidesoftware.code-spell-checker',
|
|
64
69
|
'pkief.material-icon-theme',
|
|
65
70
|
'usernamehw.errorlens',
|
|
@@ -69,16 +74,16 @@ const STATIC_EXTENSIONS = [
|
|
|
69
74
|
const PRETTIER_EXTENSION = 'esbenp.prettier-vscode'
|
|
70
75
|
const DEFAULT_FORMATTER = 'editor.defaultFormatter'
|
|
71
76
|
const FORMAT_ON_SAVE = 'editor.formatOnSave'
|
|
72
|
-
const
|
|
77
|
+
const BASIC_LANGUAGES = ['[html]', '[css]', '[javascript]']
|
|
73
78
|
|
|
74
|
-
function
|
|
79
|
+
function basic_settings(): Record<string, Record<string, unknown>> {
|
|
75
80
|
const formatter = { [DEFAULT_FORMATTER]: PRETTIER_EXTENSION, [FORMAT_ON_SAVE]: true }
|
|
76
81
|
|
|
77
|
-
return Object.fromEntries(
|
|
82
|
+
return Object.fromEntries(BASIC_LANGUAGES.map((language) => [language, formatter]))
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
function
|
|
81
|
-
const settings =
|
|
85
|
+
function build_basic_settings_action(): FileAction {
|
|
86
|
+
const settings = basic_settings()
|
|
82
87
|
|
|
83
88
|
return build_action(
|
|
84
89
|
VSCODE_SETTINGS_PATH,
|
|
@@ -87,10 +92,10 @@ function build_static_settings_action(): FileAction {
|
|
|
87
92
|
)
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
function
|
|
95
|
+
function build_basic_vscode_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
|
|
91
96
|
const recommendations = shape.has_web
|
|
92
|
-
? [...
|
|
93
|
-
:
|
|
97
|
+
? [...BASIC_EXTENSIONS, PRETTIER_EXTENSION]
|
|
98
|
+
: BASIC_EXTENSIONS
|
|
94
99
|
const actions: Array<FileAction> = [
|
|
95
100
|
build_action(
|
|
96
101
|
VSCODE_EXTENSIONS_PATH,
|
|
@@ -99,12 +104,12 @@ function build_static_vscode_actions(shape: ProjectShape): ReadonlyArray<FileAct
|
|
|
99
104
|
),
|
|
100
105
|
]
|
|
101
106
|
|
|
102
|
-
if (shape.has_web) actions.push(
|
|
107
|
+
if (shape.has_web) actions.push(build_basic_settings_action())
|
|
103
108
|
|
|
104
109
|
return actions
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
function
|
|
112
|
+
function build_basic_tsconfig_action(): FileAction {
|
|
108
113
|
return build_action(
|
|
109
114
|
TSCONFIG_PATH,
|
|
110
115
|
() => json_format.format_json({ extends: init_logic.get_tsconfig_extends_entry() }),
|
|
@@ -187,26 +192,28 @@ function build_gitignore_action(): FileAction {
|
|
|
187
192
|
)
|
|
188
193
|
}
|
|
189
194
|
|
|
190
|
-
function
|
|
195
|
+
function build_basic_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
|
|
191
196
|
const actions: Array<FileAction> = []
|
|
192
197
|
if (shape.has_git) actions.push(build_gitignore_action())
|
|
193
|
-
if (shape.has_typescript) actions.push(
|
|
198
|
+
if (shape.has_typescript) actions.push(build_basic_tsconfig_action())
|
|
194
199
|
|
|
195
200
|
if (shape.has_web) {
|
|
196
201
|
actions.push({
|
|
197
|
-
dest:
|
|
202
|
+
dest: BASIC_PRETTIER_CONFIG,
|
|
198
203
|
create: () =>
|
|
199
|
-
"import { config } from '@joshuafolkken/kit/prettier/
|
|
204
|
+
"import { config } from '@joshuafolkken/kit/prettier/basic'\n\nexport default config\n",
|
|
205
|
+
merge: (existing) => basic_path_migration.migrate_basic_paths(existing),
|
|
206
|
+
should_show_sample_when_unchanged: true,
|
|
200
207
|
})
|
|
201
208
|
}
|
|
202
209
|
|
|
203
|
-
actions.push(...
|
|
210
|
+
actions.push(...build_basic_vscode_actions(shape))
|
|
204
211
|
|
|
205
212
|
return actions
|
|
206
213
|
}
|
|
207
214
|
|
|
208
215
|
function build_file_actions(shape?: ProjectShape): ReadonlyArray<FileAction> {
|
|
209
|
-
if (shape?.profile === '
|
|
216
|
+
if (shape?.profile === 'basic') return build_basic_actions(shape)
|
|
210
217
|
|
|
211
218
|
return [
|
|
212
219
|
...(shape?.has_git === false ? [] : [build_gitignore_action()]),
|
|
@@ -226,10 +233,11 @@ function build_file_actions(shape?: ProjectShape): ReadonlyArray<FileAction> {
|
|
|
226
233
|
...build_config_file_actions().filter(
|
|
227
234
|
(action) => shape?.has_git !== false || action.dest !== LEFTHOOK_PATH,
|
|
228
235
|
),
|
|
236
|
+
...session_language_environment.build_session_lang_actions(),
|
|
229
237
|
]
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
const init_actions = { build_file_actions, read_package_json }
|
|
233
241
|
|
|
234
242
|
export type { FileAction }
|
|
235
|
-
export { init_actions, PRETTIER_CONFIG_JS }
|
|
243
|
+
export { init_actions, PRETTIER_CONFIG_JS, BASIC_PRETTIER_CONFIG }
|