@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
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { git_gh_command } from '#scripts/git/git-gh-command'
|
|
2
|
+
import { EPIC_LABEL, has_any_label } from '#scripts/git/issue-labels'
|
|
3
|
+
import { PAGE_CEILING_CAUSE } from '#scripts/git/listing-cutoff'
|
|
4
|
+
import { parse_json_array_or_undefined } from '#scripts/git/parse-json-array'
|
|
5
|
+
import { issue_label_schema } from '#scripts/git/schemas'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import type { ScoutIssue } from './issue-scout'
|
|
8
|
+
|
|
9
|
+
// The closed half of `issue:scout`'s duplicate scan (joshuafolkken/kit#1679), held apart from the CLI
|
|
10
|
+
// so the command that reports the scan and the one that files after it (`issue:file`,
|
|
11
|
+
// joshuafolkken/kit#2808) read one listing rather than two.
|
|
12
|
+
|
|
13
|
+
// How far back the closed half of the scan reaches (joshuafolkken/kit#1679). One page: the duplicate
|
|
14
|
+
// this half exists to catch closed hours ago, not months, and the listing is ordered by update so
|
|
15
|
+
// the newest hundred already covers a run's own day several times over.
|
|
16
|
+
//
|
|
17
|
+
// **It is a window, not a cut, which is why reaching it is not warned about.** Every other listing in
|
|
18
|
+
// this command reports its `limit` as a gap, because there the caller wanted the whole backlog and
|
|
19
|
+
// got part of it; here the hundredth-most-recently-updated closed issue is where the question itself
|
|
20
|
+
// stops. A warning printed on every invocation in any repository with a hundred closed issues would
|
|
21
|
+
// carry no signal and would train a reader past the gap lines beside it that do. **The page ceiling
|
|
22
|
+
// is still reported**, and `ClosedScan` below says why the two are not the same event.
|
|
23
|
+
const CLOSED_LIMIT = 100
|
|
24
|
+
|
|
25
|
+
// Read through `has_any_label` rather than compared directly, for the casing reason
|
|
26
|
+
// `issue-labels.ts` records: GitHub keeps the spelling a label was created with.
|
|
27
|
+
const EPIC_LABELS: ReadonlySet<string> = new Set([EPIC_LABEL])
|
|
28
|
+
|
|
29
|
+
// The listing's own label shape, not a second spelling of it: a copy here would be the drift the
|
|
30
|
+
// shared schema module exists to prevent.
|
|
31
|
+
const closed_labels_schema = z.array(issue_label_schema).optional()
|
|
32
|
+
|
|
33
|
+
const closed_row_schema = z.object({
|
|
34
|
+
number: z.number(),
|
|
35
|
+
title: z.string().nullable(),
|
|
36
|
+
labels: closed_labels_schema,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
type ClosedRow = z.infer<typeof closed_row_schema>
|
|
40
|
+
|
|
41
|
+
// `rows` is `undefined` — never `[]` — when the listing failed: "nothing closed recently" is the
|
|
42
|
+
// answer that sends a run straight on to file the duplicate, which is the failure this half was
|
|
43
|
+
// added to prevent.
|
|
44
|
+
function to_closed_row(row: ClosedRow): ScoutIssue {
|
|
45
|
+
return {
|
|
46
|
+
number: row.number,
|
|
47
|
+
title: row.title ?? '',
|
|
48
|
+
is_closed: true,
|
|
49
|
+
// A closed epic is still a container, and `epic_bundle_cli`'s marking cannot reach it: that one
|
|
50
|
+
// is built from the *open* epic listing. Reported as a duplicate it reads as "this work is
|
|
51
|
+
// already done" against an epic that never had an implementation of its own.
|
|
52
|
+
is_epic: has_any_label(row.labels, EPIC_LABELS),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function to_closed_rows(json: string): Array<ScoutIssue> | undefined {
|
|
57
|
+
const rows = parse_json_array_or_undefined(json, closed_row_schema)
|
|
58
|
+
|
|
59
|
+
return rows?.map((row) => to_closed_row(row))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The rows, and whether the **page ceiling** stopped the paging short of the window. That is a
|
|
63
|
+
// different thing from the `limit` above, which is the window itself: pull requests are filtered out
|
|
64
|
+
// client-side, so a repository whose recent closures are mostly merged pull requests can select
|
|
65
|
+
// fewer than `CLOSED_LIMIT` issue rows while the listing still has more to give. The scan then covers
|
|
66
|
+
// less than it was asked for, which `git-gh-issue.ts`'s disposition table records as a warning.
|
|
67
|
+
interface ClosedScan {
|
|
68
|
+
rows: Array<ScoutIssue> | undefined
|
|
69
|
+
is_capped: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function read_recently_closed(): Promise<ClosedScan> {
|
|
73
|
+
const { json, is_capped } = await git_gh_command.issue_list_recently_closed(CLOSED_LIMIT)
|
|
74
|
+
|
|
75
|
+
return { rows: json === undefined ? undefined : to_closed_rows(json), is_capped }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The open half already ran, so both of these are gaps in the answer rather than failures of it —
|
|
79
|
+
// the same disposition every other truncated listing in this command takes.
|
|
80
|
+
const CLOSED_UNREADABLE_LINE =
|
|
81
|
+
'⚠ The recently closed issues could not be read, so the scan below covers open issues only — work that finished hours ago will not appear in it.'
|
|
82
|
+
const CLOSED_CEILING_LINE = `⚠ The recently closed listing ${PAGE_CEILING_CAUSE}, so a duplicate that closed inside the window may not be below.`
|
|
83
|
+
|
|
84
|
+
function warn_about_closed(scan: ClosedScan): void {
|
|
85
|
+
if (scan.rows === undefined) {
|
|
86
|
+
console.error(CLOSED_UNREADABLE_LINE)
|
|
87
|
+
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (scan.is_capped) console.error(CLOSED_CEILING_LINE)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const issue_scout_closed = {
|
|
95
|
+
CLOSED_CEILING_LINE,
|
|
96
|
+
CLOSED_UNREADABLE_LINE,
|
|
97
|
+
read_recently_closed,
|
|
98
|
+
warn_about_closed,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type { ClosedScan }
|
|
102
|
+
export { issue_scout_closed }
|
|
@@ -48,6 +48,7 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
48
48
|
"ibl": "issue:backlinks",
|
|
49
49
|
"ici": "issue:cite",
|
|
50
50
|
"icm": "issue:comment",
|
|
51
|
+
"ifl": "issue:file",
|
|
51
52
|
"ig": "investigation:guard",
|
|
52
53
|
"iln": "issue:lint",
|
|
53
54
|
"ird": "issue:read",
|
|
@@ -449,7 +450,7 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
449
450
|
],
|
|
450
451
|
],
|
|
451
452
|
"init": [
|
|
452
|
-
"[--profile
|
|
453
|
+
"[--profile basic|full] [--no-install]",
|
|
453
454
|
"developer",
|
|
454
455
|
[
|
|
455
456
|
"files",
|
|
@@ -485,6 +486,13 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
485
486
|
"network",
|
|
486
487
|
],
|
|
487
488
|
],
|
|
489
|
+
"issue:file": [
|
|
490
|
+
"<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,…>]",
|
|
491
|
+
"automation",
|
|
492
|
+
[
|
|
493
|
+
"network",
|
|
494
|
+
],
|
|
495
|
+
],
|
|
488
496
|
"issue:fold": [
|
|
489
497
|
"<title>... [--not-separable] [--json]",
|
|
490
498
|
"automation",
|
|
@@ -758,14 +766,14 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
758
766
|
],
|
|
759
767
|
],
|
|
760
768
|
"profile": [
|
|
761
|
-
"[--profile
|
|
769
|
+
"[--profile basic|full]",
|
|
762
770
|
"developer",
|
|
763
771
|
[
|
|
764
772
|
"files",
|
|
765
773
|
],
|
|
766
774
|
],
|
|
767
775
|
"propagate": [
|
|
768
|
-
"[--dry-run] [--skip-publish-wait]",
|
|
776
|
+
"[--dry-run] [--skip-publish-wait] [--target <repo>]",
|
|
769
777
|
"maintainer",
|
|
770
778
|
[
|
|
771
779
|
"files",
|
|
@@ -1104,7 +1112,7 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
|
|
|
1104
1112
|
],
|
|
1105
1113
|
],
|
|
1106
1114
|
"start": [
|
|
1107
|
-
"[--profile
|
|
1115
|
+
"[--profile basic|full] [--yes] [--github] [--public]",
|
|
1108
1116
|
"developer",
|
|
1109
1117
|
[
|
|
1110
1118
|
"files",
|
|
@@ -22,7 +22,7 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
|
|
|
22
22
|
},
|
|
23
23
|
lint: {
|
|
24
24
|
script: 'scripts/lint/lint-parallel.ts',
|
|
25
|
-
description: 'Check code with prettier and eslint (skips a tool a
|
|
25
|
+
description: 'Check code with prettier and eslint (skips a tool a basic project lacks)',
|
|
26
26
|
category: 'Development',
|
|
27
27
|
reference: ['', 'developer', ['processes']],
|
|
28
28
|
},
|
|
@@ -61,7 +61,7 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
|
|
|
61
61
|
},
|
|
62
62
|
format: {
|
|
63
63
|
script: 'scripts/lint/format.ts',
|
|
64
|
-
description: 'Format code with prettier and eslint (skips a tool a
|
|
64
|
+
description: 'Format code with prettier and eslint (skips a tool a basic project lacks)',
|
|
65
65
|
category: 'Development',
|
|
66
66
|
reference: ['', 'developer', ['files', 'processes']],
|
|
67
67
|
},
|
|
@@ -183,7 +183,7 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
|
|
|
183
183
|
},
|
|
184
184
|
check: {
|
|
185
185
|
script: 'scripts/gate/type-check-command.ts',
|
|
186
|
-
description: 'Type-check with tsc (skips a
|
|
186
|
+
description: 'Type-check with tsc (skips a basic project with no TypeScript to check)',
|
|
187
187
|
category: 'Development',
|
|
188
188
|
reference: ['[arguments...]', 'developer', ['processes']],
|
|
189
189
|
},
|
|
@@ -63,6 +63,17 @@ const ISSUE_COMMANDS: Record<string, CommandEntry> = {
|
|
|
63
63
|
category: 'AI tools',
|
|
64
64
|
reference: ['<issue> --body <text> | --body-file <path>', 'automation', ['network']],
|
|
65
65
|
},
|
|
66
|
+
'issue:file': {
|
|
67
|
+
script: 'scripts/issue/issue-file-cli.ts',
|
|
68
|
+
description:
|
|
69
|
+
'File an Issue with every filing step: lint, Origin, duplicate scout, labels, then epic:bundle',
|
|
70
|
+
category: 'AI tools',
|
|
71
|
+
reference: [
|
|
72
|
+
'<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,…>]',
|
|
73
|
+
'automation',
|
|
74
|
+
['network'],
|
|
75
|
+
],
|
|
76
|
+
},
|
|
66
77
|
}
|
|
67
78
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
68
79
|
|
|
@@ -4,7 +4,7 @@ import type { CommandEntry } from './josh-command-types'
|
|
|
4
4
|
// no identifier format allows. The same disable sits at the top of `josh-commands-maintenance.ts`
|
|
5
5
|
// for the same reason.
|
|
6
6
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
7
|
-
const PROFILE_ARGUMENTS = '[--profile
|
|
7
|
+
const PROFILE_ARGUMENTS = '[--profile basic|full]'
|
|
8
8
|
const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
9
9
|
profile: {
|
|
10
10
|
script: 'scripts/init/project-profile-cli.ts',
|
|
@@ -14,7 +14,7 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
14
14
|
},
|
|
15
15
|
start: {
|
|
16
16
|
script: 'scripts/init/start.ts',
|
|
17
|
-
description: '
|
|
17
|
+
description: 'Set a project up for the GitHub Issue workflow, from git init to the setup PR',
|
|
18
18
|
category: 'Project',
|
|
19
19
|
reference: [
|
|
20
20
|
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public]`,
|
|
@@ -67,7 +67,11 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
67
67
|
script: 'scripts/propagate/propagate.ts',
|
|
68
68
|
description: 'Carry the published release into every consumer repository next to this one',
|
|
69
69
|
category: 'Project',
|
|
70
|
-
reference: [
|
|
70
|
+
reference: [
|
|
71
|
+
'[--dry-run] [--skip-publish-wait] [--target <repo>]',
|
|
72
|
+
'maintainer',
|
|
73
|
+
['files', 'network'],
|
|
74
|
+
],
|
|
71
75
|
},
|
|
72
76
|
adopt: {
|
|
73
77
|
script: 'scripts/adopt/adopt.ts',
|
|
@@ -23,8 +23,8 @@ const CSPELL_ARGS = [
|
|
|
23
23
|
|
|
24
24
|
// The wrapper puts cache transfer directly around cspell rather than around the whole gate (#2060),
|
|
25
25
|
// so a later failing check cannot prevent a completed cache from reaching the primary checkout.
|
|
26
|
-
function
|
|
27
|
-
if (!project_checks.
|
|
26
|
+
function basic_skip_reason(directory: string): string | undefined {
|
|
27
|
+
if (!project_checks.is_basic(directory)) return undefined
|
|
28
28
|
|
|
29
29
|
if (!project_checks.has_config(directory, project_checks.CSPELL_CONFIGS)) {
|
|
30
30
|
return 'no cspell configuration was found'
|
|
@@ -36,7 +36,7 @@ function static_skip_reason(directory: string): string | undefined {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
async function run(extra_arguments: ReadonlyArray<string> = []): Promise<number> {
|
|
39
|
-
const reason =
|
|
39
|
+
const reason = basic_skip_reason(process.cwd())
|
|
40
40
|
|
|
41
41
|
if (reason !== undefined) {
|
|
42
42
|
console.info(project_checks.skip_notice('cspell', reason))
|
package/scripts/lint/format.ts
CHANGED
|
@@ -19,7 +19,7 @@ async function run_step(args: ReadonlyArray<string>, directory: string): Promise
|
|
|
19
19
|
|
|
20
20
|
// prettier first, deliberately: `eslint --fix` exits 1 whenever a non-autofixable error remains, so
|
|
21
21
|
// putting it first would mean one unused variable anywhere in the tree stops prettier from running.
|
|
22
|
-
// A
|
|
22
|
+
// A basic project without Prettier or ESLint is skipped with the same reason `josh lint` prints
|
|
23
23
|
// (joshuafolkken/kit#2693, joshuafolkken/kit#2606).
|
|
24
24
|
async function run_unless_skipped(
|
|
25
25
|
check: string,
|
|
@@ -64,8 +64,8 @@ function lint_exit_code(prettier: BufferedProcessResult, eslint: BufferedProcess
|
|
|
64
64
|
// The two checks are run from here whether they were pointed at the whole tree or at one change's
|
|
65
65
|
// files, so `josh lint:related` reaches prettier and eslint through the same buffering, the same
|
|
66
66
|
// "one failure does not abort the other" reading, and the same exit code (joshuafolkken/kit#1298) —
|
|
67
|
-
// and through the same
|
|
68
|
-
// run would have skipped (joshuafolkken/kit#2709). The skip reasons answer nothing for a
|
|
67
|
+
// and through the same basic-project skip, so a narrowed run never reaches a tool the whole-tree
|
|
68
|
+
// run would have skipped (joshuafolkken/kit#2709). The skip reasons answer nothing for a full
|
|
69
69
|
// project, so the one path serves both profiles.
|
|
70
70
|
async function run_lint_checks(
|
|
71
71
|
prettier_args: ReadonlyArray<string>,
|
|
@@ -64,6 +64,7 @@ const SKIP_REASONS: Readonly<Record<string, string>> = {
|
|
|
64
64
|
not_downstream: 'does not depend on this package',
|
|
65
65
|
missing_checkout: 'no local checkout at the mapped path — reported, not cloned',
|
|
66
66
|
unreadable: 'package.json could not be read',
|
|
67
|
+
not_selected: 'not the repository --target named',
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
// The reason text for a candidate that is not going to be processed. A missing local checkout is
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { propagate_run } from './propagate-run'
|
|
2
|
+
import type { PropagateTarget, TargetState } from './propagate-targets'
|
|
3
|
+
|
|
4
|
+
// Narrowing a run to the one consumer `--target` names (joshuafolkken/kit#2755).
|
|
5
|
+
//
|
|
6
|
+
// Every other consumer stays in the list as `not_selected`, so the report still accounts for each
|
|
7
|
+
// repository considered and says why it was left alone. A name that matches nothing — or matches a
|
|
8
|
+
// candidate that cannot take the release — refuses the whole run before anything is written, rather
|
|
9
|
+
// than quietly propagating to nobody.
|
|
10
|
+
|
|
11
|
+
const SELECTABLE_STATES: ReadonlySet<TargetState> = new Set(['ready', 'up_to_date'])
|
|
12
|
+
const REFUSAL_PREFIX = 'Refusing to propagate:'
|
|
13
|
+
|
|
14
|
+
interface TargetSelection {
|
|
15
|
+
targets: Array<PropagateTarget>
|
|
16
|
+
refusal?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A discovery key is `owner/repo`; the bare repository name is accepted too, because that is what a
|
|
20
|
+
// person types.
|
|
21
|
+
function matches_name(repo: string, name: string): boolean {
|
|
22
|
+
return repo === name || repo.slice(repo.indexOf('/') + 1) === name
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function refuse_match(name: string, matches: ReadonlyArray<PropagateTarget>): string | undefined {
|
|
26
|
+
const [match] = matches
|
|
27
|
+
|
|
28
|
+
if (match === undefined) {
|
|
29
|
+
return `${REFUSAL_PREFIX} no repository named "${name}" is checked out next to this one.`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (matches.length > 1) {
|
|
33
|
+
return `${REFUSAL_PREFIX} "${name}" names more than one repository; use the owner/repo form.`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (SELECTABLE_STATES.has(match.state)) return undefined
|
|
37
|
+
|
|
38
|
+
return `${REFUSAL_PREFIX} ${match.repo}: ${propagate_run.skip_reason(match)}.`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Only a candidate the run would otherwise have processed becomes `not_selected`. One that was never
|
|
42
|
+
// going to be — not downstream, no checkout — keeps its own reason, which says more than "not named".
|
|
43
|
+
function mark_unselected(
|
|
44
|
+
target: PropagateTarget,
|
|
45
|
+
matches: ReadonlyArray<PropagateTarget>,
|
|
46
|
+
): PropagateTarget {
|
|
47
|
+
if (matches.includes(target) || !SELECTABLE_STATES.has(target.state)) return target
|
|
48
|
+
|
|
49
|
+
return { ...target, state: 'not_selected' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The run's targets with every other consumer marked `not_selected`, or the reason `name` cannot be
|
|
53
|
+
// targeted. No name leaves the list untouched — the default run still covers every consumer.
|
|
54
|
+
function select_target(
|
|
55
|
+
targets: ReadonlyArray<PropagateTarget>,
|
|
56
|
+
name: string | undefined,
|
|
57
|
+
): TargetSelection {
|
|
58
|
+
if (name === undefined) return { targets: [...targets] }
|
|
59
|
+
const matches = targets.filter((target) => matches_name(target.repo, name))
|
|
60
|
+
const refusal = refuse_match(name, matches)
|
|
61
|
+
if (refusal !== undefined) return { targets: [], refusal }
|
|
62
|
+
|
|
63
|
+
return { targets: targets.map((target) => mark_unselected(target, matches)) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const propagate_select = {
|
|
67
|
+
matches_name,
|
|
68
|
+
select_target,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type { TargetSelection }
|
|
72
|
+
export { propagate_select }
|
|
@@ -19,8 +19,10 @@ import { z } from 'zod'
|
|
|
19
19
|
const MANIFEST_NAME = 'package.json'
|
|
20
20
|
const NODE_MODULES = 'node_modules'
|
|
21
21
|
|
|
22
|
-
// Why a candidate is not going to be processed, or that it is.
|
|
23
|
-
|
|
22
|
+
// Why a candidate is not going to be processed, or that it is. `not_selected` is the one state set
|
|
23
|
+
// after classification: a candidate `--target` left out (joshuafolkken/kit#2755).
|
|
24
|
+
type TargetState =
|
|
25
|
+
'ready' | 'up_to_date' | 'not_downstream' | 'missing_checkout' | 'unreadable' | 'not_selected'
|
|
24
26
|
|
|
25
27
|
interface PropagateTarget {
|
|
26
28
|
repo: string
|
|
@@ -156,5 +158,5 @@ const propagate_targets = {
|
|
|
156
158
|
resolve_targets,
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
export type { Manifest, PropagateTarget }
|
|
161
|
+
export type { Manifest, PropagateTarget, TargetState }
|
|
160
162
|
export { propagate_targets }
|
|
@@ -11,8 +11,9 @@ import { version_targets } from '#scripts/version/version-targets'
|
|
|
11
11
|
import { propagate_git } from './propagate-git'
|
|
12
12
|
import { propagate_publish } from './propagate-publish'
|
|
13
13
|
import { propagate_run } from './propagate-run'
|
|
14
|
+
import { propagate_select, type TargetSelection } from './propagate-select'
|
|
14
15
|
import { propagate_steps } from './propagate-steps'
|
|
15
|
-
import { propagate_targets } from './propagate-targets'
|
|
16
|
+
import { propagate_targets, type PropagateTarget } from './propagate-targets'
|
|
16
17
|
|
|
17
18
|
// `josh propagate` — carry the release this repository just published into every consumer checked
|
|
18
19
|
// out next to it (joshuafolkken/kit#863).
|
|
@@ -22,25 +23,56 @@ const FAILURE_EXIT_CODE = 1
|
|
|
22
23
|
const ARGV_OFFSET = 2
|
|
23
24
|
const SKIP_PUBLISH_FLAG = '--skip-publish-wait'
|
|
24
25
|
const DRY_RUN_FLAG = '--dry-run'
|
|
26
|
+
const TARGET_FLAG = '--target'
|
|
25
27
|
const KNOWN_FLAGS: ReadonlyArray<string> = [SKIP_PUBLISH_FLAG, DRY_RUN_FLAG]
|
|
28
|
+
// Shown in the usage line only. `--target` carries a value, so it is taken out of argv before the
|
|
29
|
+
// flag-only refusal runs, and this entry never has to match an argument.
|
|
30
|
+
const TARGET_USAGE = `${TARGET_FLAG} <repo>`
|
|
26
31
|
const DRY_RUN_REASON = 'would be propagated'
|
|
32
|
+
const UNTOUCHED_NOTE = 'No consumer was touched.'
|
|
27
33
|
|
|
28
34
|
interface RunOptions {
|
|
29
35
|
is_dry_run: boolean
|
|
30
36
|
is_publish_wait_skipped: boolean
|
|
37
|
+
// The one consumer to propagate to, when `--target` narrowed the run (joshuafolkken/kit#2755).
|
|
38
|
+
target?: string
|
|
31
39
|
// The usage message, when the arguments were not accepted. Carried alongside rather than returned
|
|
32
40
|
// instead, so the caller has one shape to branch on.
|
|
33
41
|
usage?: string
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
interface TargetArgument {
|
|
45
|
+
rest: Array<string>
|
|
46
|
+
target?: string
|
|
47
|
+
usage?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Take `--target <repo>` out of argv. A second `--target` stays in `rest`, where the flag-only
|
|
51
|
+
// refusal rejects it rather than letting one of the two names win silently.
|
|
52
|
+
function split_target(argv: ReadonlyArray<string>): TargetArgument {
|
|
53
|
+
const index = argv.indexOf(TARGET_FLAG)
|
|
54
|
+
if (index === -1) return { rest: [...argv] }
|
|
55
|
+
const target = argv[index + 1]
|
|
56
|
+
const rest = argv.filter((_, position) => position !== index && position !== index + 1)
|
|
57
|
+
|
|
58
|
+
if (target === undefined || target.startsWith('-')) {
|
|
59
|
+
return { rest, usage: `${TARGET_FLAG} needs a repository name, e.g. ${TARGET_FLAG} app-kit` }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { rest, target }
|
|
63
|
+
}
|
|
64
|
+
|
|
36
65
|
// Reject anything not on the list rather than ignoring it. `--dryrun` silently falling through to
|
|
37
66
|
// the real write path is the mistake this refusal exists to prevent.
|
|
38
67
|
function parse_options(argv: ReadonlyArray<string>): RunOptions {
|
|
68
|
+
const { rest, target, usage: target_usage } = split_target(argv)
|
|
39
69
|
const options: RunOptions = {
|
|
40
|
-
is_dry_run:
|
|
41
|
-
is_publish_wait_skipped:
|
|
70
|
+
is_dry_run: rest.includes(DRY_RUN_FLAG),
|
|
71
|
+
is_publish_wait_skipped: rest.includes(SKIP_PUBLISH_FLAG),
|
|
72
|
+
...(target !== undefined && { target }),
|
|
42
73
|
}
|
|
43
|
-
const usage =
|
|
74
|
+
const usage =
|
|
75
|
+
target_usage ?? refuse_unknown_flags(rest, [...KNOWN_FLAGS, TARGET_USAGE], 'propagate')
|
|
44
76
|
|
|
45
77
|
return usage === undefined ? options : { ...options, usage }
|
|
46
78
|
}
|
|
@@ -126,15 +158,57 @@ async function await_publish(target_version: string, is_skipped: boolean): Promi
|
|
|
126
158
|
console.error(
|
|
127
159
|
`${status_icons.FAIL_ICON} ${target_version} did not become available: ${result.state}.`,
|
|
128
160
|
)
|
|
129
|
-
console.error(
|
|
161
|
+
console.error(UNTOUCHED_NOTE)
|
|
130
162
|
|
|
131
163
|
return false
|
|
132
164
|
}
|
|
133
165
|
|
|
134
|
-
//
|
|
135
|
-
function
|
|
166
|
+
// Print whatever the version resolution had to say, and hand back the version when there is one.
|
|
167
|
+
function announce_run_version(resolved: RunVersion): string | undefined {
|
|
168
|
+
if (resolved.version === undefined) {
|
|
169
|
+
console.error(resolved.refusal ?? 'Nothing to propagate.')
|
|
170
|
+
|
|
171
|
+
return undefined
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (resolved.warning !== undefined) console.info(resolved.warning)
|
|
175
|
+
|
|
176
|
+
return resolved.version
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface RunPlan {
|
|
180
|
+
version: string
|
|
181
|
+
targets: ReadonlyArray<PropagateTarget>
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The consumers this run will consider, narrowed by `--target`. Resolved before the publish wait so
|
|
185
|
+
// a mistyped name fails at once instead of after minutes of polling the registry.
|
|
186
|
+
function resolve_run_targets(version: string, target: string | undefined): TargetSelection {
|
|
136
187
|
const map = repo_discovery.discover_repositories(PROJECT_ROOT)
|
|
137
188
|
const targets = propagate_targets.resolve_targets(map, KIT_PACKAGE_NAME, version)
|
|
189
|
+
|
|
190
|
+
return propagate_select.select_target(targets, target)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// The version and the consumers, or nothing once whatever stopped the run has been printed.
|
|
194
|
+
function plan_run(options: RunOptions): RunPlan | undefined {
|
|
195
|
+
const version = announce_run_version(resolve_run_version(PROJECT_ROOT, options.is_dry_run))
|
|
196
|
+
if (version === undefined) return undefined
|
|
197
|
+
const selection = resolve_run_targets(version, options.target)
|
|
198
|
+
|
|
199
|
+
if (selection.refusal !== undefined) {
|
|
200
|
+
console.error(selection.refusal)
|
|
201
|
+
console.error(UNTOUCHED_NOTE)
|
|
202
|
+
|
|
203
|
+
return undefined
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { version, targets: selection.targets }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Everything after the publish wait: run each consumer, print one report.
|
|
210
|
+
function propagate_to_consumers(plan: RunPlan, is_dry_run: boolean): number {
|
|
211
|
+
const { version, targets } = plan
|
|
138
212
|
const step = is_dry_run
|
|
139
213
|
? propagate_steps.describe_step
|
|
140
214
|
: propagate_steps.create_step_runner({
|
|
@@ -165,19 +239,6 @@ async function resolve_publish(version: string, options: RunOptions): Promise<bo
|
|
|
165
239
|
return await await_publish(version, options.is_dry_run || options.is_publish_wait_skipped)
|
|
166
240
|
}
|
|
167
241
|
|
|
168
|
-
// Print whatever the version resolution had to say, and hand back the version when there is one.
|
|
169
|
-
function announce_run_version(resolved: RunVersion): string | undefined {
|
|
170
|
-
if (resolved.version === undefined) {
|
|
171
|
-
console.error(resolved.refusal ?? 'Nothing to propagate.')
|
|
172
|
-
|
|
173
|
-
return undefined
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (resolved.warning !== undefined) console.info(resolved.warning)
|
|
177
|
-
|
|
178
|
-
return resolved.version
|
|
179
|
-
}
|
|
180
|
-
|
|
181
242
|
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
182
243
|
const options = parse_options(argv)
|
|
183
244
|
|
|
@@ -187,11 +248,11 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
|
187
248
|
return FAILURE_EXIT_CODE
|
|
188
249
|
}
|
|
189
250
|
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
if (!(await resolve_publish(version, options))) return FAILURE_EXIT_CODE
|
|
251
|
+
const plan = plan_run(options)
|
|
252
|
+
if (plan === undefined) return FAILURE_EXIT_CODE
|
|
253
|
+
if (!(await resolve_publish(plan.version, options))) return FAILURE_EXIT_CODE
|
|
193
254
|
|
|
194
|
-
return propagate_to_consumers(
|
|
255
|
+
return propagate_to_consumers(plan, options.is_dry_run)
|
|
195
256
|
}
|
|
196
257
|
|
|
197
258
|
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
@@ -201,6 +262,7 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
|
201
262
|
const propagate = {
|
|
202
263
|
DRY_RUN_REASON,
|
|
203
264
|
KNOWN_FLAGS,
|
|
265
|
+
TARGET_FLAG,
|
|
204
266
|
parse_options,
|
|
205
267
|
refuse_outside_source_repository,
|
|
206
268
|
resolve_target_version,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { cost_blocks } from '#scripts/cost-runtime/cost-blocks'
|
|
2
2
|
import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
|
|
3
3
|
import { time_shell } from '#scripts/time-runtime/time-shell'
|
|
4
|
+
import { shell_segments } from './shell-segments'
|
|
4
5
|
|
|
5
|
-
// The
|
|
6
|
+
// The trigger helpers the enumeration in `delivered-rules.ts` is built from, held here so a rule
|
|
6
7
|
// that lives in its own module can build its own row without importing the enumeration back
|
|
7
|
-
// (joshuafolkken/kit#2119). `is_issue_filing` is the one three rows
|
|
8
|
-
//
|
|
9
|
-
// their rows.
|
|
8
|
+
// (joshuafolkken/kit#2119). `is_issue_filing` is the one three rows share — the WIP cap, the per-run
|
|
9
|
+
// filing cap and the fold — so it could not stay a private function of the file that spreads their rows.
|
|
10
10
|
|
|
11
11
|
// **Only `Bash`, and the omission is deliberate** (joshuafolkken/kit#1390): Claude Code denies one
|
|
12
12
|
// call of a turn and runs the rest, so a refused `Edit` would leave its siblings applied and itself
|
|
@@ -33,12 +33,25 @@ const ISSUES_ENDPOINT = /repos\/[^\s'"]*\/issues(?=$|["'\s])/u
|
|
|
33
33
|
// that limit is recorded beside the non-`gh` one in `prompts/collaboration-workflow/rule-delivery.md`.
|
|
34
34
|
const TITLE_FIELD = /(?:-f|-F|--field|--raw-field)\s*'?title=/u
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// A filing made by hand rather than through `josh issue:file` — the call the `direct-filing` row
|
|
37
|
+
// refuses on every occurrence (joshuafolkken/kit#2808).
|
|
38
|
+
function is_direct_filing(command: string): boolean {
|
|
37
39
|
if (ISSUE_CREATE_COMMAND.test(command)) return true
|
|
38
40
|
|
|
39
41
|
return ISSUES_ENDPOINT.test(command) && TITLE_FIELD.test(command)
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
// `pnpm josh issue:file`, in either spelling — **the** filing act once a direct one is refused, so the
|
|
45
|
+
// run-level filing rows (the WIP cap, the per-run cap, the fold) trigger on it. Segment-wise, so a
|
|
46
|
+
// spelling quoted inside another command's body is not read as a filing.
|
|
47
|
+
const FILING_NAMES: ReadonlySet<string> = new Set(['issue:file'])
|
|
48
|
+
|
|
49
|
+
function is_issue_filing(command: string): boolean {
|
|
50
|
+
return shell_segments
|
|
51
|
+
.segments_of(command)
|
|
52
|
+
.some((segment) => shell_segments.is_josh_command(segment, FILING_NAMES))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const bash_triggers = { is_direct_filing, is_issue_filing, on_bash_command }
|
|
43
56
|
|
|
44
57
|
export { bash_triggers }
|