@joshuafolkken/kit 1.947.0 → 1.957.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/README.md +4 -5
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-V7RH56HU.js → chunk-244WYOEO.js} +2 -2
- package/dist/hooks/{chunk-IA7WC4NS.js → chunk-3ON7HOYB.js} +1 -1
- package/dist/hooks/{chunk-EYFJ74MJ.js → chunk-BELI5XSS.js} +6 -2
- package/dist/hooks/{chunk-IBFC25IX.js → chunk-GNOITLRR.js} +4 -4
- package/dist/hooks/{chunk-M4U4YNRE.js → chunk-HJXQZ7V4.js} +3 -3
- package/dist/hooks/{chunk-3FWV6CFC.js → chunk-IJPBTDTB.js} +371 -178
- package/dist/hooks/{chunk-NKVLBB65.js → chunk-KOCK5FXQ.js} +3 -3
- package/dist/hooks/{chunk-O3PEDF3M.js → chunk-LDQDZYHX.js} +1 -1
- package/dist/hooks/{chunk-GIX5SAAB.js → chunk-M6K7EQDO.js} +8 -8
- package/dist/hooks/{chunk-OM5VDM6F.js → chunk-XSL4SRMA.js} +2 -2
- 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 +3 -3
- package/dist/josh.js +6 -2
- package/dist/version/index.js +7 -4
- package/package.json +1 -1
- package/scripts/backlog/backlog-drive-cli.ts +5 -1
- package/scripts/backlog/backlog-drive-retrospective.ts +20 -0
- package/scripts/backlog/backlog-drive.ts +7 -3
- package/scripts/git/git-worktree.ts +35 -4
- package/scripts/git/issue-labels.ts +19 -0
- package/scripts/git/repository-lock.ts +138 -0
- package/scripts/git/stash-sweep-lock.ts +6 -117
- package/scripts/init/project-profile.ts +9 -5
- package/scripts/init/start-plan.ts +145 -0
- package/scripts/init/start-prompt.ts +55 -0
- package/scripts/init/start-steps.ts +167 -0
- package/scripts/init/start.ts +92 -16
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +2 -1
- package/scripts/josh/josh-commands-project.ts +6 -2
- package/scripts/run/run-retrospective.ts +34 -0
- package/scripts/run/run-step-cli.ts +3 -5
- package/scripts/run/run-step.ts +3 -15
- package/scripts/safe-chain/project-config.ts +55 -10
- package/scripts/sync/sync.ts +3 -1
- package/scripts/version/release-age.ts +11 -6
|
@@ -14,9 +14,13 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
14
14
|
},
|
|
15
15
|
start: {
|
|
16
16
|
script: 'scripts/init/start.ts',
|
|
17
|
-
description: '
|
|
17
|
+
description: 'Create a new project for the GitHub Issue workflow, from git init to labels',
|
|
18
18
|
category: 'Project',
|
|
19
|
-
reference: [
|
|
19
|
+
reference: [
|
|
20
|
+
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public]`,
|
|
21
|
+
'developer',
|
|
22
|
+
['files', 'git', 'network', 'processes'],
|
|
23
|
+
],
|
|
20
24
|
},
|
|
21
25
|
init: {
|
|
22
26
|
script: 'scripts/init/init.ts',
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
2
|
+
|
|
3
|
+
// Whether a run owes its end-of-run retrospective, single-sourced so `run:step` and `backlog:drive`
|
|
4
|
+
// never disagree on it (joshuafolkken/kit#2750): a drive that read "owed" where `run:step` read "not
|
|
5
|
+
// owed" handed back at every drain to a session that had nothing to run.
|
|
6
|
+
|
|
7
|
+
// The opt-in switch that gates the retrospective (joshuafolkken/kit#2370). It defaults off, so an unset
|
|
8
|
+
// variable owes no retrospective.
|
|
9
|
+
const RETROSPECTIVE_ENV_KEY = 'JOSH_RETROSPECTIVE'
|
|
10
|
+
|
|
11
|
+
interface RetrospectiveFacts {
|
|
12
|
+
is_retrospective_enabled: boolean
|
|
13
|
+
is_lane_child: boolean
|
|
14
|
+
is_retrospective_done: boolean
|
|
15
|
+
is_consumer: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function is_enabled(): boolean {
|
|
19
|
+
return hook_decision.is_switch_opt_in(RETROSPECTIVE_ENV_KEY)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// False when the switch is off, for a dispatched lane child (the batch runs one at its own end, never a
|
|
23
|
+
// child's), once it has run this invocation, and in a consumer checkout, where the kit-only command is
|
|
24
|
+
// refused. The switch is read first, so a run that has not opted in never consults the other three.
|
|
25
|
+
function is_owed(facts: RetrospectiveFacts): boolean {
|
|
26
|
+
if (!facts.is_retrospective_enabled) return false
|
|
27
|
+
|
|
28
|
+
return !facts.is_lane_child && !facts.is_retrospective_done && !facts.is_consumer
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const run_retrospective = { RETROSPECTIVE_ENV_KEY, is_enabled, is_owed }
|
|
32
|
+
|
|
33
|
+
export type { RetrospectiveFacts }
|
|
34
|
+
export { run_retrospective }
|
|
@@ -9,6 +9,7 @@ import { run_carry, type CarryRead } from './run-carry'
|
|
|
9
9
|
import { run_event_scope } from './run-event-scope'
|
|
10
10
|
import { run_event_stream } from './run-event-stream'
|
|
11
11
|
import { run_prep_cli } from './run-prep-cli'
|
|
12
|
+
import { run_retrospective } from './run-retrospective'
|
|
12
13
|
import { run_step, type StepInput } from './run-step'
|
|
13
14
|
|
|
14
15
|
// `josh run:step <N>` — print the run's next single action, computed from the event stream, the carry
|
|
@@ -22,10 +23,6 @@ const SUCCESS_EXIT_CODE = 0
|
|
|
22
23
|
const FAILURE_EXIT_CODE = 1
|
|
23
24
|
const ARGV_OFFSET = 2
|
|
24
25
|
const USAGE = 'Usage: josh run:step <issue-number>'
|
|
25
|
-
// The opt-in switch that gates the end-of-run retrospective (joshuafolkken/kit#2370). Read here rather
|
|
26
|
-
// than in `run-step.ts` so the position logic stays a pure function of its input; it defaults off, so an
|
|
27
|
-
// unset variable prints no retrospective step.
|
|
28
|
-
const RETROSPECTIVE_ENV_KEY = 'JOSH_RETROSPECTIVE'
|
|
29
26
|
|
|
30
27
|
interface RunReads {
|
|
31
28
|
carry_kind: CarryRead['kind']
|
|
@@ -83,7 +80,8 @@ async function gather(issue_number: string): Promise<StepInput> {
|
|
|
83
80
|
is_at_cut_cap: run_reads.is_at_cut_cap,
|
|
84
81
|
is_lane_child: lane_child_marker.is_child_of(process.cwd()),
|
|
85
82
|
is_consumer: doctor_consumer.is_kit_consumer(find_package_directory(process.cwd())),
|
|
86
|
-
|
|
83
|
+
// Read here rather than in `run-step.ts` so the position logic stays a pure function of its input.
|
|
84
|
+
is_retrospective_enabled: run_retrospective.is_enabled(),
|
|
87
85
|
has_changes: parts.has_changes,
|
|
88
86
|
has_completion_callback: agent_role_profile.has_completion_callback(),
|
|
89
87
|
}
|
package/scripts/run/run-step.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { git_stash } from '#scripts/git/git-stash'
|
|
|
2
2
|
import { latest_scope_cli } from '#scripts/version/latest-scope-cli'
|
|
3
3
|
import type { CarryRead } from './run-carry'
|
|
4
4
|
import { run_event_stream } from './run-event-stream'
|
|
5
|
+
import { run_retrospective } from './run-retrospective'
|
|
5
6
|
|
|
6
7
|
// `josh run:step <N>` — the run's next single action, computed from three mechanical inputs and nothing
|
|
7
8
|
// else (joshuafolkken/kit#2248). It is the child of epic #2166 that lifts the fold from an *event* to a
|
|
@@ -174,23 +175,10 @@ const EVENT_ACTIONS: Record<string, (issue_number: string) => StepAction> = {
|
|
|
174
175
|
// (joshuafolkken/kit#2297).
|
|
175
176
|
const PARENT_ONLY_EVENTS: ReadonlySet<string> = new Set([KIND.MERGE, KIND.OUTAGE])
|
|
176
177
|
|
|
177
|
-
// Whether a run at a winding-down position still owes its end-of-run retrospective
|
|
178
|
-
// (joshuafolkken/kit#2328) — false when the switch is off (the opt-in default, joshuafolkken/kit#2370),
|
|
179
|
-
// for a dispatched lane child, where the batch runs one at its own end and never a child's (the
|
|
180
|
-
// `release:scope` precedent), for a run whose retrospective has already run this invocation, and in a
|
|
181
|
-
// consumer checkout, where the kit-only command is refused. The switch is read first, so a run that has
|
|
182
|
-
// not opted in never consults the other three. The two positions that consult it are the drain and the
|
|
183
|
-
// stop; both read exactly this, so the exclusion set is single-sourced.
|
|
184
|
-
function is_retrospective_owed(input: StepInput): boolean {
|
|
185
|
-
if (!input.is_retrospective_enabled) return false
|
|
186
|
-
|
|
187
|
-
return !input.is_lane_child && !input.is_retrospective_done && !input.is_consumer
|
|
188
|
-
}
|
|
189
|
-
|
|
190
178
|
// A stopped run's last owed step is the end-of-run retrospective. When it is not owed the position stops
|
|
191
179
|
// with no command to run, which is what the position meant before the retrospective existed.
|
|
192
180
|
function stop_action(input: StepInput): StepAction {
|
|
193
|
-
return
|
|
181
|
+
return run_retrospective.is_owed(input) ? command(RETROSPECTIVE_COMMAND) : verdict(STOP)
|
|
194
182
|
}
|
|
195
183
|
|
|
196
184
|
// The drain fires the retrospective *before* the idle watch, not after it (joshuafolkken/kit#2335): the
|
|
@@ -199,7 +187,7 @@ function stop_action(input: StepInput): StepAction {
|
|
|
199
187
|
// run proceeds to that watch, which is a `WAIT` rather than a `STOP`: the backlog is empty but the run is
|
|
200
188
|
// not ending, and the real `STOP` follows when the watch expires with the retrospective already done.
|
|
201
189
|
function drain_action(input: StepInput): StepAction {
|
|
202
|
-
return
|
|
190
|
+
return run_retrospective.is_owed(input) ? command(RETROSPECTIVE_COMMAND) : verdict(WAIT)
|
|
203
191
|
}
|
|
204
192
|
|
|
205
193
|
// The hand-off a `backlogrun` parent without a completion callback takes right after dispatching
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { release_age } from '#scripts/version/release-age'
|
|
3
4
|
import { yaml_document } from '#scripts/yaml/yaml-document'
|
|
4
5
|
import { dump } from 'js-yaml'
|
|
5
6
|
import { z } from 'zod'
|
|
6
7
|
|
|
7
8
|
const WORKSPACE_FILE = 'pnpm-workspace.yaml'
|
|
8
9
|
const PROJECT_CONFIG_FILE = '.aikido'
|
|
10
|
+
const NPMRC_FILE = '.npmrc'
|
|
9
11
|
const EXCLUSIONS_FIELD = 'minimumPackageAgeExclusions'
|
|
12
|
+
const AGE_FIELD = 'minimumPackageAgeHours'
|
|
13
|
+
const MINUTES_PER_HOUR = 60
|
|
10
14
|
const workspace_schema = z.object({ minimumReleaseAgeExclude: z.array(z.string()).optional() })
|
|
11
15
|
|
|
12
16
|
function read_mapping(value: unknown, label: string): Record<string, unknown> {
|
|
@@ -72,27 +76,68 @@ function replace_safe_chain_block(existing: string, safe_chain: Record<string, u
|
|
|
72
76
|
return `${prefix}${prefix ? '\n' : ''}${replacement}${suffix}`
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// Safe Chain enforces its own minimum package age (48 hours by default), so CI refused lockfiles pnpm
|
|
80
|
+
// had resolved under `.npmrc`'s shorter window (kit #2743). `.npmrc` is the source: its minutes are
|
|
81
|
+
// floored to whole hours so Safe Chain is never stricter than pnpm, and an undeclared window writes
|
|
82
|
+
// nothing; an explicit `0` opt-out is written as 0 so an earlier synchronized age cannot outlive it.
|
|
83
|
+
function package_age_hours(npmrc: string): number | undefined {
|
|
84
|
+
const minutes = release_age.parse_declared_minimum_release_age(npmrc)
|
|
85
|
+
if (minutes === undefined) return undefined
|
|
86
|
+
|
|
87
|
+
return Math.floor(minutes / MINUTES_PER_HOUR)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function merge_age(safe_chain: Record<string, unknown>, npmrc: string): Record<string, unknown> {
|
|
91
|
+
const hours = package_age_hours(npmrc)
|
|
92
|
+
if (hours === undefined) return safe_chain
|
|
93
|
+
|
|
94
|
+
return { ...safe_chain, [AGE_FIELD]: hours }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function merge_exclusions(
|
|
98
|
+
safe_chain: Record<string, unknown>,
|
|
99
|
+
exclusions: ReadonlyArray<string>,
|
|
100
|
+
): Record<string, unknown> {
|
|
80
101
|
const npm = read_mapping(safe_chain['npm'], 'safe-chain.npm')
|
|
81
|
-
if (JSON.stringify(npm[EXCLUSIONS_FIELD]) === JSON.stringify(exclusions)) return existing
|
|
82
|
-
if (exclusions.length === 0 && existing.length === 0) return existing
|
|
83
102
|
|
|
84
|
-
|
|
103
|
+
return { ...safe_chain, npm: { ...npm, [EXCLUSIONS_FIELD]: exclusions } }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function has_nothing_to_write(
|
|
107
|
+
existing: string,
|
|
108
|
+
exclusions: ReadonlyArray<string>,
|
|
109
|
+
npmrc: string,
|
|
110
|
+
): boolean {
|
|
111
|
+
return existing.length === 0 && exclusions.length === 0 && package_age_hours(npmrc) === undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function read_exclusions(workspace: string): ReadonlyArray<string> {
|
|
115
|
+
return workspace_schema.parse(yaml_document.parse_yaml(workspace)).minimumReleaseAgeExclude ?? []
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function merge_project_config(existing: string, workspace: string, npmrc = ''): string {
|
|
119
|
+
const exclusions = read_exclusions(workspace)
|
|
120
|
+
const aikido = yaml_document.parse_yaml(existing)
|
|
121
|
+
const safe_chain = read_mapping(aikido['safe-chain'], 'safe-chain')
|
|
122
|
+
const merged = merge_age(merge_exclusions(safe_chain, exclusions), npmrc)
|
|
123
|
+
if (JSON.stringify(merged) === JSON.stringify(safe_chain)) return existing
|
|
124
|
+
if (has_nothing_to_write(existing, exclusions, npmrc)) return existing
|
|
85
125
|
|
|
86
126
|
return replace_safe_chain_block(existing, merged)
|
|
87
127
|
}
|
|
88
128
|
|
|
129
|
+
function read_optional(file_path: string): string {
|
|
130
|
+
return existsSync(file_path) ? readFileSync(file_path, 'utf8') : ''
|
|
131
|
+
}
|
|
132
|
+
|
|
89
133
|
function sync_project_config(root: string): boolean {
|
|
90
134
|
const workspace_path = path.join(root, WORKSPACE_FILE)
|
|
91
135
|
if (!existsSync(workspace_path)) return false
|
|
92
136
|
const config_path = path.join(root, PROJECT_CONFIG_FILE)
|
|
93
|
-
const existing =
|
|
137
|
+
const existing = read_optional(config_path)
|
|
94
138
|
const workspace = readFileSync(workspace_path, 'utf8')
|
|
95
|
-
const
|
|
139
|
+
const npmrc = read_optional(path.join(root, NPMRC_FILE))
|
|
140
|
+
const merged = merge_project_config(existing, workspace, npmrc)
|
|
96
141
|
if (merged === existing) return false
|
|
97
142
|
writeFileSync(config_path, merged)
|
|
98
143
|
|
package/scripts/sync/sync.ts
CHANGED
|
@@ -371,7 +371,6 @@ function report_repository_settings(name_with_owner: string | undefined): void {
|
|
|
371
371
|
|
|
372
372
|
function sync_project_artifacts(is_force: boolean): void {
|
|
373
373
|
sync_ai_copy_all(is_force)
|
|
374
|
-
project_config.sync_project_config(PROJECT_ROOT)
|
|
375
374
|
sync_prettier_config(path.join(PROJECT_ROOT, 'prettier.config.js'))
|
|
376
375
|
sync_playwright_config(path.join(PROJECT_ROOT, 'playwright.config.ts'))
|
|
377
376
|
sync_deploy_vps(path.join(PROJECT_ROOT, '.github/workflows/deploy-vps.yml'))
|
|
@@ -382,6 +381,9 @@ function sync_project_artifacts(is_force: boolean): void {
|
|
|
382
381
|
|
|
383
382
|
sync_sonar_with_template(name_with_owner, is_force)
|
|
384
383
|
sync_config_files()
|
|
384
|
+
// After `sync_config_files`, which may add the `.npmrc` window `.aikido`'s age is derived from
|
|
385
|
+
// (joshuafolkken/kit#2743).
|
|
386
|
+
project_config.sync_project_config(PROJECT_ROOT)
|
|
385
387
|
sync_package_json_migrations(path.join(PROJECT_ROOT, PACKAGE_JSON))
|
|
386
388
|
report_repository_settings(name_with_owner)
|
|
387
389
|
}
|
|
@@ -19,12 +19,18 @@ const NPMRC_PATH = '.npmrc'
|
|
|
19
19
|
const release_times_schema = z.record(z.string(), z.string())
|
|
20
20
|
const MS_PER_MINUTE = 60_000
|
|
21
21
|
|
|
22
|
+
// The window `.npmrc` content declares, or nothing when it does not declare one — an explicit
|
|
23
|
+
// `minimum-release-age=0` stays 0 rather than collapsing into "not declared".
|
|
24
|
+
function parse_declared_minimum_release_age(npmrc_content: string): number | undefined {
|
|
25
|
+
const raw = MINIMUM_RELEASE_AGE_RE.exec(npmrc_content)?.[1]
|
|
26
|
+
|
|
27
|
+
return raw === undefined ? undefined : Number(raw)
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
// The quarantine window in minutes from `.npmrc`; a missing or malformed entry means no
|
|
23
31
|
// quarantine, matching pnpm's own default for the setting.
|
|
24
32
|
function parse_minimum_release_age(npmrc_content: string): number {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return raw === undefined ? NO_QUARANTINE_MINUTES : Number(raw)
|
|
33
|
+
return parse_declared_minimum_release_age(npmrc_content) ?? NO_QUARANTINE_MINUTES
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
// Stable releases only, on the requested major, published at or before the cutoff. The
|
|
@@ -82,9 +88,7 @@ function select_aged_version(
|
|
|
82
88
|
// `minimum-release-age=0` opt-out stop the walk instead of falling through to an ancestor's policy.
|
|
83
89
|
function read_declared_minimum_release_age(npmrc_path: string): number | undefined {
|
|
84
90
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return raw === undefined ? undefined : Number(raw)
|
|
91
|
+
return parse_declared_minimum_release_age(readFileSync(npmrc_path, 'utf8'))
|
|
88
92
|
} catch {
|
|
89
93
|
return undefined
|
|
90
94
|
}
|
|
@@ -138,6 +142,7 @@ function read_nearest_minimum_release_age(start: string, boundary?: string): num
|
|
|
138
142
|
const release_age = {
|
|
139
143
|
release_times_schema,
|
|
140
144
|
parse_minimum_release_age,
|
|
145
|
+
parse_declared_minimum_release_age,
|
|
141
146
|
read_minimum_release_age,
|
|
142
147
|
read_nearest_minimum_release_age,
|
|
143
148
|
select_aged_version,
|