@joshuafolkken/kit 1.953.0 → 1.972.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/workflow-commands/background-commands.md +3 -0
- package/.claude/skills/workflow-commands/backlogrun-child.md +75 -120
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +78 -115
- package/.claude/skills/workflow-commands/backlogrun-progress.md +82 -146
- package/.claude/skills/workflow-commands/backlogrun-steps.md +173 -229
- package/.claude/skills/workflow-commands/backlogrun.md +3 -1
- package/.claude/skills/workflow-commands/followup.md +3 -0
- package/.claude/skills/workflow-commands/fullrun.md +3 -4
- package/.claude/skills/workflow-commands/observation-filing.md +31 -25
- package/.claude/skills/workflow-commands/pre-gate-cut.md +96 -161
- package/.github/workflows/pr-classification.yml +12 -1
- package/CLAUDE.md +3 -3
- package/README.md +5 -3
- package/dist/CLAUDE.md +3 -3
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-OM5VDM6F.js → chunk-2K52BRWV.js} +2 -2
- package/dist/hooks/{chunk-Z5TVZ4AP.js → chunk-BNWIOJFU.js} +29 -16
- package/dist/hooks/{chunk-NKVLBB65.js → chunk-BVMSPG3M.js} +3 -3
- package/dist/hooks/{chunk-IA7WC4NS.js → chunk-CRGJTD6E.js} +1 -1
- package/dist/hooks/{chunk-M4U4YNRE.js → chunk-F6OM6SIG.js} +3 -3
- package/dist/hooks/{chunk-O3PEDF3M.js → chunk-KQG4776P.js} +1 -1
- package/dist/hooks/{chunk-V7RH56HU.js → chunk-NXL3SBXY.js} +2 -2
- package/dist/hooks/{chunk-452OKRQX.js → chunk-WWTMHHZU.js} +120 -18
- package/dist/hooks/{chunk-EYFJ74MJ.js → chunk-ZPLXLJ7O.js} +21 -7
- package/dist/hooks/{chunk-IBFC25IX.js → chunk-ZYYSBNR6.js} +4 -4
- 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 +206 -176
- package/dist/josh.js +14 -6
- package/package.json +3 -3
- package/prompts/collaboration-workflow/wip-cap.md +1 -1
- package/scripts/backlog/backlog-arrival.ts +11 -1
- package/scripts/backlog/backlog-budget.ts +25 -8
- 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/backlog/backlog-next.ts +45 -2
- package/scripts/backlog/backlog-offer.ts +1 -0
- package/scripts/backlog/backlog-plan-cli.ts +66 -27
- package/scripts/backlog/backlog-plan.ts +30 -2
- package/scripts/backlog/backlog-stalled.ts +9 -0
- package/scripts/backlog/backlog-waves.ts +169 -0
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/document/entry-read-budget.ts +7 -5
- package/scripts/epic/epic-lane-offer.ts +69 -4
- package/scripts/epic/epic-next.ts +2 -2
- package/scripts/epic/epic-solo.ts +147 -0
- package/scripts/epic/epic-triage.ts +47 -0
- package/scripts/gate/hook-gate-reuse.ts +7 -8
- package/scripts/git/git-closes-keyword.ts +15 -0
- package/scripts/git/git-followup-flush.ts +5 -6
- package/scripts/git/git-followup-issue-close.ts +75 -0
- package/scripts/git/git-pr-followup-wrapup.ts +36 -6
- package/scripts/git/git-pr-followup.ts +9 -16
- package/scripts/git/git-staging.ts +33 -20
- package/scripts/git/issue-labels.ts +42 -0
- package/scripts/hooks/stop-guard.ts +1 -0
- package/scripts/init/init-install.ts +64 -0
- package/scripts/init/init.ts +38 -11
- 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/issue/issue-closing-pr.ts +93 -0
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +4 -2
- package/scripts/josh/josh-commands-project.ts +11 -3
- package/scripts/josh/josh-commands-workflow.ts +3 -3
- package/scripts/observations/observation-ledger-prepare.ts +31 -0
- package/scripts/observations/observation-ledger.ts +5 -5
- package/scripts/observations/observations-flush.ts +11 -30
- package/scripts/rules/implementation-cut.ts +49 -16
- package/scripts/rules/lane-background.ts +25 -5
- package/scripts/rules/stop-rules.ts +16 -4
- package/scripts/run/run-carry-cli.ts +21 -0
- package/scripts/run/run-cut-cli.ts +92 -110
- package/scripts/run/run-cut-report.ts +167 -0
- package/scripts/run/run-cut.ts +23 -7
- package/scripts/run/run-entry-cli.ts +36 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-hold-cli.ts +36 -15
- package/scripts/run/run-hold.ts +15 -4
- package/scripts/run/run-merge-cli.ts +28 -14
- package/scripts/run/run-merge-steps.ts +38 -2
- package/scripts/run/run-merge.ts +41 -3
- package/scripts/run/run-prep-cli.ts +8 -2
- package/scripts/run/run-prep-locate.ts +165 -0
- package/scripts/run/run-prep.ts +12 -1
- package/scripts/run/run-retrospective.ts +34 -0
- package/scripts/run/run-step-cli.ts +19 -5
- package/scripts/run/run-step.ts +31 -19
- package/scripts/run/run-tail-cli.ts +4 -2
- package/scripts/run/run-wake-work.ts +4 -0
- package/scripts/safe-chain/ci-installer-pin.ts +8 -4
- package/scripts/time-runtime/time-background.ts +11 -0
- package/scripts/time-runtime/time-transcript-line.ts +4 -0
- package/templates/pnpm-workspace.static.yaml +2 -0
package/scripts/run/run-merge.ts
CHANGED
|
@@ -32,7 +32,9 @@ const ONE = 1
|
|
|
32
32
|
|
|
33
33
|
// What a returned child turned out to be, decided from its GitHub state and — for the failed case
|
|
34
34
|
// alone — the exit record.
|
|
35
|
-
// - `merged` — CLOSED; the child finished and its pull request merged.
|
|
35
|
+
// - `merged` — CLOSED; the child finished and its pull request merged. Also an OPEN, unparked
|
|
36
|
+
// child a merged pull request's `closes #N` names — GitHub merged it without closing
|
|
37
|
+
// the issue (joshuafolkken/kit#2769).
|
|
36
38
|
// - `human-review` — OPEN and carrying `needs-human-review`; the run's own ending (SKILL.md → §2z).
|
|
37
39
|
// - `parked` — OPEN and carrying `needs-decision` or `already-done`; a person still owns it.
|
|
38
40
|
// - `split` — OPEN and carrying `epic`; its work was divided into new children.
|
|
@@ -60,6 +62,9 @@ type ChildOutcome =
|
|
|
60
62
|
interface EndingSignals {
|
|
61
63
|
is_outage: boolean
|
|
62
64
|
is_cut: boolean
|
|
65
|
+
// The merged pull request whose `closes #N` names the child, when GitHub left the child OPEN after
|
|
66
|
+
// merging it (joshuafolkken/kit#2769).
|
|
67
|
+
merged_pr?: string | undefined
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
const NO_SIGNALS: EndingSignals = { is_outage: false, is_cut: false }
|
|
@@ -82,8 +87,12 @@ function is_parked(state: IssueState): boolean {
|
|
|
82
87
|
// An OPEN, unparked child that is not waiting on a person: a declared cut is resumed
|
|
83
88
|
// (joshuafolkken/kit#2484), and it is read before the outage so a cut whose process then lost the API is
|
|
84
89
|
// still resumed from its record; an OPEN, unparked child that could not reach the API is an `outage`,
|
|
85
|
-
// not a `failed` (joshuafolkken/kit#2240).
|
|
90
|
+
// not a `failed` (joshuafolkken/kit#2240). **A merged pull request that closes it is read first**: the
|
|
91
|
+
// work landed and only GitHub's close did not, so it is `merged` — never a failure counted against the
|
|
92
|
+
// guard (joshuafolkken/kit#2769).
|
|
86
93
|
function unfinished_outcome(signals: EndingSignals): ChildOutcome {
|
|
94
|
+
if (signals.merged_pr !== undefined) return 'merged'
|
|
95
|
+
|
|
87
96
|
if (signals.is_cut) return 'cut'
|
|
88
97
|
|
|
89
98
|
return signals.is_outage ? 'outage' : 'failed'
|
|
@@ -152,15 +161,44 @@ function counters_comment(carry: RunCarry): string {
|
|
|
152
161
|
].join('\n')
|
|
153
162
|
}
|
|
154
163
|
|
|
164
|
+
// Why the run parked a child it judged failed (joshuafolkken/kit#2769). A child that stops itself
|
|
165
|
+
// leaves its own comment; one the driver stops left nothing, so a person opening the issue could not
|
|
166
|
+
// tell why it carried `needs-decision`. Script-emitted, so English.
|
|
167
|
+
interface ParkReason {
|
|
168
|
+
child: string
|
|
169
|
+
cause: string
|
|
170
|
+
carry: RunCarry | undefined
|
|
171
|
+
output: string | undefined
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function streak_line(carry: RunCarry | undefined): string {
|
|
175
|
+
if (carry === undefined) return '- Consecutive failures: not recorded (no carry record).'
|
|
176
|
+
|
|
177
|
+
return `- Consecutive failures: ${String(carry.failures)} of ${String(CONSECUTIVE_FAILURE_LIMIT)} (the run stops at ${String(CONSECUTIVE_FAILURE_LIMIT)}).`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function park_comment(reason: ParkReason): string {
|
|
181
|
+
const transcript = reason.output === undefined ? '' : ` (\`${reason.output}\`)`
|
|
182
|
+
|
|
183
|
+
return [
|
|
184
|
+
'Parked with `needs-decision` by `pnpm josh run:merge`, the `backlogrun` driver — not by the child itself.',
|
|
185
|
+
`- Why: ${reason.cause}`,
|
|
186
|
+
'- Read: the issue is OPEN, carries none of `needs-decision`, `already-done`, `needs-human-review` or `epic`, and no merged pull request’s `closes #N` names it.',
|
|
187
|
+
streak_line(reason.carry),
|
|
188
|
+
`- Next: read the child’s transcript${transcript}, then either resume it with \`fullrun #${reason.child}\`, or record the decision here and remove \`needs-decision\`.`,
|
|
189
|
+
].join('\n')
|
|
190
|
+
}
|
|
191
|
+
|
|
155
192
|
const run_merge = {
|
|
156
193
|
CONSECUTIVE_FAILURE_LIMIT,
|
|
157
194
|
CONSECUTIVE_OUTAGE_LIMIT,
|
|
158
195
|
change_of,
|
|
159
196
|
classify_child,
|
|
160
197
|
counters_comment,
|
|
198
|
+
park_comment,
|
|
161
199
|
is_guard_tripped,
|
|
162
200
|
is_outage_guard_tripped,
|
|
163
201
|
}
|
|
164
202
|
|
|
165
|
-
export type { ChildOutcome, EndingSignals }
|
|
203
|
+
export type { ChildOutcome, EndingSignals, ParkReason }
|
|
166
204
|
export { run_merge }
|
|
@@ -6,10 +6,12 @@ import { issue_state_cli, type StateRead } from '#scripts/issue/issue-state-cli'
|
|
|
6
6
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
7
7
|
import { latest_scope_cli } from '#scripts/version/latest-scope-cli'
|
|
8
8
|
import { run_prep, type PrepParts } from './run-prep'
|
|
9
|
+
import { run_prep_locate } from './run-prep-locate'
|
|
9
10
|
|
|
10
11
|
// `josh run:prep <N>` — one call for the reads a `fullrun` makes before its first edit
|
|
11
12
|
// (joshuafolkken/kit#1978): the issue body and comments (`issue:read`), the state, labels and
|
|
12
|
-
// `human_review` line (`issue:state`),
|
|
13
|
+
// `human_review` line (`issue:state`), the dependency-update scope (`latest:scope`), and where the
|
|
14
|
+
// paths and identifiers the body names occur in code (`run-prep-locate.ts`). Each is
|
|
13
15
|
// reused rather than reproduced, and the two body reads run concurrently, so three round trips become
|
|
14
16
|
// one.
|
|
15
17
|
//
|
|
@@ -37,6 +39,7 @@ interface PrepReads {
|
|
|
37
39
|
state: StateRead
|
|
38
40
|
latest: LatestDecision
|
|
39
41
|
has_changes: boolean
|
|
42
|
+
locations: string
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
// Exactly one issue number, or the call is refused: `run:prep` prepares one run, and a second number
|
|
@@ -90,8 +93,10 @@ async function gather(issue_number: string): Promise<PrepReads> {
|
|
|
90
93
|
issue_state_cli.read_issue(issue_number),
|
|
91
94
|
lane_has_changes(),
|
|
92
95
|
])
|
|
96
|
+
// Located from the body just read, so it waits for that read rather than joining the batch above.
|
|
97
|
+
const locations = await run_prep_locate.locate(content.kind === 'ok' ? content.block : '')
|
|
93
98
|
|
|
94
|
-
return { content, state, latest: latest_decision(), has_changes }
|
|
99
|
+
return { content, state, latest: latest_decision(), has_changes, locations }
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
function content_body(issue_number: string, content: BlockRead): string {
|
|
@@ -110,6 +115,7 @@ function to_parts(issue_number: string, reads: PrepReads): PrepParts {
|
|
|
110
115
|
latest_scope: reads.latest.scope,
|
|
111
116
|
latest_reason: reads.latest.reason,
|
|
112
117
|
has_changes: reads.has_changes,
|
|
118
|
+
locations: reads.locations,
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { get_exit_code } from '#scripts/git/git-execa-error'
|
|
2
|
+
import { git_spawn } from '#scripts/git/git-spawn'
|
|
3
|
+
|
|
4
|
+
// The code locations an issue names, bundled into `run:prep` (joshuafolkken/kit#2761). Across the 90
|
|
5
|
+
// runs `.time-history.jsonl` split by contributor, 53% of round trips were investigation, and most of
|
|
6
|
+
// it opened the files an issue names one read at a time. This finds each path, identifier and command
|
|
7
|
+
// the body puts in backticks and hands back where it occurs in code — `file:line` and a short excerpt,
|
|
8
|
+
// never a whole file — so the first read lands on the right place.
|
|
9
|
+
//
|
|
10
|
+
// **Only a backticked span is a target.** A prose word is not a name, and a span holding a space is a
|
|
11
|
+
// command line or a sentence rather than one thing to look up; the one exception is a `josh` command,
|
|
12
|
+
// whose subcommand is the name.
|
|
13
|
+
|
|
14
|
+
const BACKTICKED = /`([^`\n]+)`/gu
|
|
15
|
+
const JOSH_PREFIX = /^(?:pnpm\s+)?josh\s+(\S+)/u
|
|
16
|
+
// A `file:line` citation names the file; the suffix would make both the file lookup and the grep miss.
|
|
17
|
+
const LINE_SUFFIX = /(?::\d+)+$/u
|
|
18
|
+
// A name carries a separator a prose word does not — `run:prep`, `run_prep`, `run-prep.ts`, `a/b`.
|
|
19
|
+
const NAME_CHARACTERS = /^[\w@./:-]+$/u
|
|
20
|
+
const NAME_SEPARATOR = /[./:_-]/u
|
|
21
|
+
const GREP_LINE = /^(?<path>[^:]+):(?<line>\d+):(?<text>.*)$/u
|
|
22
|
+
const MIN_TARGET_LENGTH = 3
|
|
23
|
+
const MAX_TARGETS = 10
|
|
24
|
+
const MAX_FILES_PER_TARGET = 3
|
|
25
|
+
const MAX_EXCERPT_LENGTH = 80
|
|
26
|
+
const ELLIPSIS = '…'
|
|
27
|
+
const INDENT = ' '
|
|
28
|
+
const HIT_SEPARATOR = ' '
|
|
29
|
+
const NO_MATCH = ' — no match in code'
|
|
30
|
+
const SEARCH_FAILED = ' — git grep failed; not searched'
|
|
31
|
+
// `git grep` exits 1 for no match; any other exit is a search that never ran.
|
|
32
|
+
const GREP_NO_MATCH_EXIT = 1
|
|
33
|
+
const FILE_PATHSPEC = ':(glob)**/'
|
|
34
|
+
const FILE_HIT = { line: '1', excerpt: '(file named in the issue)' }
|
|
35
|
+
const NO_TARGETS = '(no backticked path or identifier in the issue)'
|
|
36
|
+
// Code only, tests and snapshots excluded: a document mentions a name far more often than code
|
|
37
|
+
// defines it, and the three-file cap would fill with prose before reaching the implementation.
|
|
38
|
+
const CODE_PATHS = [
|
|
39
|
+
'*.ts',
|
|
40
|
+
'*.js',
|
|
41
|
+
'*.svelte',
|
|
42
|
+
':(exclude)*.test.ts',
|
|
43
|
+
':(exclude)*.e2e.ts',
|
|
44
|
+
':(exclude)*.snap',
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
interface Hit {
|
|
48
|
+
path: string
|
|
49
|
+
line: string
|
|
50
|
+
excerpt: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface Located {
|
|
54
|
+
target: string
|
|
55
|
+
hits: ReadonlyArray<Hit>
|
|
56
|
+
has_failed: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function to_target(span: string): string {
|
|
60
|
+
const josh = JOSH_PREFIX.exec(span)
|
|
61
|
+
|
|
62
|
+
return josh?.[1] ?? span.trim().replace(LINE_SUFFIX, '')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function is_name(target: string): boolean {
|
|
66
|
+
if (target.length < MIN_TARGET_LENGTH || target.includes('://')) return false
|
|
67
|
+
|
|
68
|
+
return NAME_CHARACTERS.test(target) && NAME_SEPARATOR.test(target)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extract_targets(text: string): Array<string> {
|
|
72
|
+
const targets = [...text.matchAll(BACKTICKED)].map((match) => to_target(match[1] ?? ''))
|
|
73
|
+
|
|
74
|
+
return [...new Set(targets.filter((target) => is_name(target)))].slice(0, MAX_TARGETS)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function shorten(text: string): string {
|
|
78
|
+
const trimmed = text.trim()
|
|
79
|
+
|
|
80
|
+
if (trimmed.length <= MAX_EXCERPT_LENGTH) return trimmed
|
|
81
|
+
|
|
82
|
+
return `${trimmed.slice(0, MAX_EXCERPT_LENGTH)}${ELLIPSIS}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// `git grep -n` prints `path:line:text`; the text may itself hold colons, so only the first two split.
|
|
86
|
+
function to_hit(output_line: string): Hit | undefined {
|
|
87
|
+
const groups = GREP_LINE.exec(output_line)?.groups
|
|
88
|
+
|
|
89
|
+
if (groups === undefined) return undefined
|
|
90
|
+
|
|
91
|
+
// All three groups are non-optional in `GREP_LINE`, so a match always carries each of them.
|
|
92
|
+
const { path, line, text } = groups as Record<'line' | 'path' | 'text', string>
|
|
93
|
+
|
|
94
|
+
return { path, line, excerpt: shorten(text) }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parse_hits(stdout: string): Array<Hit> {
|
|
98
|
+
const hits = stdout.split('\n').map((line) => to_hit(line))
|
|
99
|
+
|
|
100
|
+
return hits.filter((hit): hit is Hit => hit !== undefined).slice(0, MAX_FILES_PER_TARGET)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function format_target(located: Located): string {
|
|
104
|
+
if (located.has_failed) return `${located.target}${SEARCH_FAILED}`
|
|
105
|
+
if (located.hits.length === 0) return `${located.target}${NO_MATCH}`
|
|
106
|
+
|
|
107
|
+
const lines = located.hits.map(
|
|
108
|
+
(hit) => `${INDENT}${hit.path}:${hit.line}${HIT_SEPARATOR}${hit.excerpt}`,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return [located.target, ...lines].join('\n')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function format_locations(located: ReadonlyArray<Located>): string {
|
|
115
|
+
if (located.length === 0) return NO_TARGETS
|
|
116
|
+
|
|
117
|
+
return located.map((entry) => format_target(entry)).join('\n')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// `git grep` exits 1 when nothing matches, which `git_spawn.read` raises. Neither that nor a search
|
|
121
|
+
// that could not run fails the bundle — the issue body is still worth printing without it — but only
|
|
122
|
+
// the first is reported as "no match": a git without `grep -m` (older than 2.38) exits 129, and naming
|
|
123
|
+
// every target absent then would send the reader away from code that is there.
|
|
124
|
+
async function grep(target: string): Promise<Located> {
|
|
125
|
+
const grep_arguments = ['grep', '-n', '-I', '-F', '-m', '1', '-e', target, '--', ...CODE_PATHS]
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
return { target, hits: parse_hits(await git_spawn.read(grep_arguments)), has_failed: false }
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return { target, hits: [], has_failed: get_exit_code(error) !== GREP_NO_MATCH_EXIT }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// A path is rarely spelled out inside code — imports drop the extension and resolve relatively — so
|
|
135
|
+
// grepping `scripts/run/run-prep.ts` misses the very file an issue most often names. A tracked file
|
|
136
|
+
// whose path ends in the target is the location itself; `**/` lets a bare file name match too.
|
|
137
|
+
async function find_files(target: string): Promise<Array<Hit>> {
|
|
138
|
+
try {
|
|
139
|
+
const stdout = await git_spawn.read(['ls-files', '--', `${FILE_PATHSPEC}${target}`])
|
|
140
|
+
const paths = stdout.split('\n').filter((path) => path !== '')
|
|
141
|
+
|
|
142
|
+
return paths.slice(0, MAX_FILES_PER_TARGET).map((path) => ({ path, ...FILE_HIT }))
|
|
143
|
+
} catch {
|
|
144
|
+
return []
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function find(target: string): Promise<Located> {
|
|
149
|
+
const files = await find_files(target)
|
|
150
|
+
|
|
151
|
+
if (files.length > 0) return { target, hits: files, has_failed: false }
|
|
152
|
+
|
|
153
|
+
return await grep(target)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function locate(text: string): Promise<string> {
|
|
157
|
+
const located = await Promise.all(extract_targets(text).map(async (target) => await find(target)))
|
|
158
|
+
|
|
159
|
+
return format_locations(located)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const run_prep_locate = { extract_targets, format_locations, locate, parse_hits }
|
|
163
|
+
|
|
164
|
+
export type { Hit, Located }
|
|
165
|
+
export { run_prep_locate }
|
package/scripts/run/run-prep.ts
CHANGED
|
@@ -23,6 +23,7 @@ const NO = 'no'
|
|
|
23
23
|
const CONTENT_HEADER = '=== issue ==='
|
|
24
24
|
const STATE_HEADER = '=== state ==='
|
|
25
25
|
const LATEST_HEADER = '=== dependency update ==='
|
|
26
|
+
const LOCATIONS_HEADER = '=== code locations ==='
|
|
26
27
|
const SECTION_SEPARATOR = '\n\n'
|
|
27
28
|
const LATEST_JOINER = ' — '
|
|
28
29
|
|
|
@@ -35,6 +36,8 @@ interface PrepParts {
|
|
|
35
36
|
state_failure: string
|
|
36
37
|
latest_scope: string
|
|
37
38
|
latest_reason: string
|
|
39
|
+
// Where the paths and identifiers the issue names occur in code (joshuafolkken/kit#2761).
|
|
40
|
+
locations: string
|
|
38
41
|
// Whether a lane child's tree holds uncommitted work — read for the verdict, not printed in the
|
|
39
42
|
// report (joshuafolkken/kit#2476).
|
|
40
43
|
has_changes: boolean
|
|
@@ -75,10 +78,18 @@ function format_report(parts: PrepParts): string {
|
|
|
75
78
|
section(CONTENT_HEADER, parts.content_body),
|
|
76
79
|
section(STATE_HEADER, state_body(parts)),
|
|
77
80
|
section(LATEST_HEADER, `${parts.latest_scope}${LATEST_JOINER}${parts.latest_reason}`),
|
|
81
|
+
section(LOCATIONS_HEADER, parts.locations),
|
|
78
82
|
].join(SECTION_SEPARATOR)
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
const run_prep = {
|
|
85
|
+
const run_prep = {
|
|
86
|
+
CONTENT_HEADER,
|
|
87
|
+
LATEST_HEADER,
|
|
88
|
+
LOCATIONS_HEADER,
|
|
89
|
+
STATE_HEADER,
|
|
90
|
+
SUMMARY_PREFIX,
|
|
91
|
+
format_report,
|
|
92
|
+
}
|
|
82
93
|
|
|
83
94
|
export type { PrepParts }
|
|
84
95
|
export { run_prep }
|
|
@@ -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 }
|
|
@@ -5,10 +5,13 @@ import { doctor_consumer } from '#scripts/doctor/doctor-consumer'
|
|
|
5
5
|
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
6
6
|
import { find_package_directory } from '#scripts/josh/josh-logic'
|
|
7
7
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
8
|
+
import { lane_reap } from '#scripts/lane/lane-reap'
|
|
8
9
|
import { run_carry, type CarryRead } from './run-carry'
|
|
9
10
|
import { run_event_scope } from './run-event-scope'
|
|
10
11
|
import { run_event_stream } from './run-event-stream'
|
|
12
|
+
import { run_headless } from './run-headless'
|
|
11
13
|
import { run_prep_cli } from './run-prep-cli'
|
|
14
|
+
import { run_retrospective } from './run-retrospective'
|
|
12
15
|
import { run_step, type StepInput } from './run-step'
|
|
13
16
|
|
|
14
17
|
// `josh run:step <N>` — print the run's next single action, computed from the event stream, the carry
|
|
@@ -22,18 +25,25 @@ const SUCCESS_EXIT_CODE = 0
|
|
|
22
25
|
const FAILURE_EXIT_CODE = 1
|
|
23
26
|
const ARGV_OFFSET = 2
|
|
24
27
|
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
28
|
|
|
30
29
|
interface RunReads {
|
|
31
30
|
carry_kind: CarryRead['kind']
|
|
32
31
|
is_retrospective_done: boolean
|
|
33
32
|
is_at_cut_cap: boolean
|
|
33
|
+
is_handed_off: boolean
|
|
34
34
|
last_event: string | undefined
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// **A hand-off stops only the session that declared the record** (joshuafolkken/kit#2760). The record is
|
|
38
|
+
// one per repository, so a `fullrun #M` a person starts in a fresh conversation beside a `backlogrun`
|
|
39
|
+
// waiting for its successor reads the same handed-off mark — and would be told to stop on every step.
|
|
40
|
+
// The owner is `--owner "$PPID"`, an ancestor of this process only in the session that took the cut.
|
|
41
|
+
function is_handed_off_here(carry: CarryRead): boolean {
|
|
42
|
+
if (carry.kind !== 'carried' || carry.carry.is_handed_off !== true) return false
|
|
43
|
+
|
|
44
|
+
return run_headless.is_owned_here(carry.carry, lane_reap.own_ancestry)
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
// The run-level reads, keyed on the common git directory both the carry and the event stream share. A
|
|
38
48
|
// directory that cannot be read leaves the position unknowable, which `next_action` answers `unknown`.
|
|
39
49
|
function read_run(directory: string | undefined, issue_number: string): RunReads {
|
|
@@ -42,6 +52,7 @@ function read_run(directory: string | undefined, issue_number: string): RunReads
|
|
|
42
52
|
carry_kind: 'unreadable',
|
|
43
53
|
is_retrospective_done: false,
|
|
44
54
|
is_at_cut_cap: false,
|
|
55
|
+
is_handed_off: false,
|
|
45
56
|
last_event: undefined,
|
|
46
57
|
}
|
|
47
58
|
}
|
|
@@ -63,6 +74,7 @@ function read_run(directory: string | undefined, issue_number: string): RunReads
|
|
|
63
74
|
carry_kind: carry.kind,
|
|
64
75
|
is_retrospective_done: run_carry.retrospective_done_of(carry),
|
|
65
76
|
is_at_cut_cap: carry.kind === 'carried' && run_carry.is_at_cut_cap(carry.carry),
|
|
77
|
+
is_handed_off: is_handed_off_here(carry),
|
|
66
78
|
last_event: last?.kind,
|
|
67
79
|
}
|
|
68
80
|
}
|
|
@@ -81,9 +93,11 @@ async function gather(issue_number: string): Promise<StepInput> {
|
|
|
81
93
|
carry_kind: run_reads.carry_kind,
|
|
82
94
|
is_retrospective_done: run_reads.is_retrospective_done,
|
|
83
95
|
is_at_cut_cap: run_reads.is_at_cut_cap,
|
|
96
|
+
is_handed_off: run_reads.is_handed_off,
|
|
84
97
|
is_lane_child: lane_child_marker.is_child_of(process.cwd()),
|
|
85
98
|
is_consumer: doctor_consumer.is_kit_consumer(find_package_directory(process.cwd())),
|
|
86
|
-
|
|
99
|
+
// Read here rather than in `run-step.ts` so the position logic stays a pure function of its input.
|
|
100
|
+
is_retrospective_enabled: run_retrospective.is_enabled(),
|
|
87
101
|
has_changes: parts.has_changes,
|
|
88
102
|
has_completion_callback: agent_role_profile.has_completion_callback(),
|
|
89
103
|
}
|
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
|
|
@@ -125,6 +126,11 @@ interface StepInput extends PreInput {
|
|
|
125
126
|
// Whether the carried invocation has already taken its maximum cuts, read off the carry record by the
|
|
126
127
|
// CLI (joshuafolkken/kit#2346). At the cap `run:carry --cut` refuses, so the hand-off is not offered.
|
|
127
128
|
is_at_cut_cap: boolean
|
|
129
|
+
// Whether the carry record has been handed off by a cut and no successor has adopted it yet, read off
|
|
130
|
+
// the record by the CLI (joshuafolkken/kit#2760) and set only in the session that declared the record.
|
|
131
|
+
// The session that took the cut is the only one that can read this — a successor spends the mark when
|
|
132
|
+
// it begins — so it is what tells that session to stop.
|
|
133
|
+
is_handed_off: boolean
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
interface StepAction {
|
|
@@ -174,23 +180,10 @@ const EVENT_ACTIONS: Record<string, (issue_number: string) => StepAction> = {
|
|
|
174
180
|
// (joshuafolkken/kit#2297).
|
|
175
181
|
const PARENT_ONLY_EVENTS: ReadonlySet<string> = new Set([KIND.MERGE, KIND.OUTAGE])
|
|
176
182
|
|
|
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
183
|
// A stopped run's last owed step is the end-of-run retrospective. When it is not owed the position stops
|
|
191
184
|
// with no command to run, which is what the position meant before the retrospective existed.
|
|
192
185
|
function stop_action(input: StepInput): StepAction {
|
|
193
|
-
return
|
|
186
|
+
return run_retrospective.is_owed(input) ? command(RETROSPECTIVE_COMMAND) : verdict(STOP)
|
|
194
187
|
}
|
|
195
188
|
|
|
196
189
|
// The drain fires the retrospective *before* the idle watch, not after it (joshuafolkken/kit#2335): the
|
|
@@ -199,7 +192,7 @@ function stop_action(input: StepInput): StepAction {
|
|
|
199
192
|
// run proceeds to that watch, which is a `WAIT` rather than a `STOP`: the backlog is empty but the run is
|
|
200
193
|
// not ending, and the real `STOP` follows when the watch expires with the retrospective already done.
|
|
201
194
|
function drain_action(input: StepInput): StepAction {
|
|
202
|
-
return
|
|
195
|
+
return run_retrospective.is_owed(input) ? command(RETROSPECTIVE_COMMAND) : verdict(WAIT)
|
|
203
196
|
}
|
|
204
197
|
|
|
205
198
|
// The hand-off a `backlogrun` parent without a completion callback takes right after dispatching
|
|
@@ -270,12 +263,31 @@ function closed_action(input: StepInput): StepAction {
|
|
|
270
263
|
}
|
|
271
264
|
}
|
|
272
265
|
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
266
|
+
// **A cut hands the run on, so the session that took it stops** (joshuafolkken/kit#2760). The driver had
|
|
267
|
+
// no handed-off state, so after `run:carry --cut` it went on answering `wait` or the next offer — a
|
|
268
|
+
// `backlogrun` parent measured at 233k kept launching children in the same conversation up to 294k. A
|
|
269
|
+
// lane child never holds the carry record, so its own position is not decided here.
|
|
270
|
+
function is_handed_off_parent(input: StepInput): boolean {
|
|
271
|
+
return input.is_handed_off && input.carry_kind === 'carried' && !input.is_lane_child
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// The carry record's terminal answers: an unreadable record leaves the position unknowable, a handed-off
|
|
275
|
+
// run is its successor's, and a spent budget is the person's call.
|
|
276
|
+
function carry_terminal_action(input: StepInput): StepAction | undefined {
|
|
277
277
|
if (input.carry_kind === 'unreadable') return verdict(UNKNOWN)
|
|
278
|
+
if (is_handed_off_parent(input)) return verdict(STOP)
|
|
278
279
|
if (input.carry_kind === 'expired') return { kind: 'decide', line: EXPIRED_DECISION }
|
|
280
|
+
|
|
281
|
+
return undefined
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// The terminal answers read before the run's position matters: the carry record's first, then the
|
|
285
|
+
// issue's own state, which can end the run whatever the events say. `undefined` when none applies and
|
|
286
|
+
// the position decides.
|
|
287
|
+
function terminal_action(input: StepInput): StepAction | undefined {
|
|
288
|
+
const carry_action = carry_terminal_action(input)
|
|
289
|
+
|
|
290
|
+
if (carry_action !== undefined) return carry_action
|
|
279
291
|
if (input.state === undefined) return verdict(UNKNOWN)
|
|
280
292
|
if (input.state === CLOSED) return closed_action(input)
|
|
281
293
|
|
|
@@ -33,8 +33,10 @@ interface Step {
|
|
|
33
33
|
argv: (issues: ReadonlyArray<string>) => ReadonlyArray<string>
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// The steps in the order a run closes on:
|
|
37
|
-
//
|
|
36
|
+
// The steps in the order a run closes on: any ledger line the run's own commit did not carry
|
|
37
|
+
// (joshuafolkken/kit#2763) is committed first so the recurrence count is on main — on most runs the
|
|
38
|
+
// flush answers `clean` — the citations are read for the completion report, and the release scope is
|
|
39
|
+
// decided last.
|
|
38
40
|
const FLUSH_STEP: Step = {
|
|
39
41
|
header: run_tail.OBSERVATIONS_HEADER,
|
|
40
42
|
argv: () => ['observations:flush'],
|
|
@@ -29,6 +29,7 @@ import { run_invocation } from './run-invocation'
|
|
|
29
29
|
const NO_FREE = 0
|
|
30
30
|
const NO_READY = 0
|
|
31
31
|
const SUCCESS_EXIT_CODE = 0
|
|
32
|
+
const TRIAGE_WORK = 1
|
|
32
33
|
|
|
33
34
|
interface WorkPorts {
|
|
34
35
|
// The free-lane count, or `undefined` where the lane limit could not be read.
|
|
@@ -41,6 +42,9 @@ async function real_ready_count(): Promise<number | undefined> {
|
|
|
41
42
|
const result = await josh_command.josh_run(['backlog:next'], true)
|
|
42
43
|
|
|
43
44
|
if (result.code !== SUCCESS_EXIT_CODE) return undefined
|
|
45
|
+
// A `triage` answer offers no number, but the untriaged issues are a session's to judge
|
|
46
|
+
// (joshuafolkken/kit#2779) — counted as one piece of work, or the whole pool would idle with nobody judging it.
|
|
47
|
+
if (backlog_stalled.needs_triage(result.out)) return TRIAGE_WORK
|
|
44
48
|
|
|
45
49
|
return backlog_stalled.count_ready_tokens(result.out)
|
|
46
50
|
}
|
|
@@ -2,11 +2,15 @@ import { createHash } from 'node:crypto'
|
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { execaSync } from 'execa'
|
|
4
4
|
|
|
5
|
-
// The workflows that install safe-chain on the runner.
|
|
5
|
+
// The workflows that install safe-chain on the runner. Each carries the release and the installer's
|
|
6
6
|
// SHA-256 as workflow-level env, so one pin per file moves every "Setup safe-chain" step at once
|
|
7
|
-
// (joshuafolkken/kit#2711). A consumer has
|
|
8
|
-
//
|
|
9
|
-
const WORKFLOW_PATHS = [
|
|
7
|
+
// (joshuafolkken/kit#2711, joshuafolkken/kit#2765). A consumer has the first and the last, and
|
|
8
|
+
// `josh sync` rewrites both from kit, so moving them anywhere but kit is harmless and short-lived.
|
|
9
|
+
const WORKFLOW_PATHS = [
|
|
10
|
+
'.github/workflows/ci.yml',
|
|
11
|
+
'templates/workflows/ci.yml',
|
|
12
|
+
'.github/workflows/pr-classification.yml',
|
|
13
|
+
]
|
|
10
14
|
// `[ \t]` rather than `\s`: under the `m` flag `\s*` would run across line ends and swallow the
|
|
11
15
|
// blank line that follows the env block.
|
|
12
16
|
const VERSION_RE = /^(?<key>[ \t]*SAFE_CHAIN_INSTALLER_VERSION:[ \t]*)(?<value>\S+)[ \t]*$/mu
|
|
@@ -50,6 +50,8 @@ const NO_FINISH = 0
|
|
|
50
50
|
// The launch's own result body, which is the only place the harness writes the id it assigns. Read
|
|
51
51
|
// as a derived fact and the body discarded, exactly as `has_failure_line` and `followup_stages` are.
|
|
52
52
|
const LAUNCH_PATTERN = /running in background with ID:\s*([\w-]+)/iu
|
|
53
|
+
// A backgrounded subagent's launch result, which names its id on an `agentId:` line of its own.
|
|
54
|
+
const AGENT_LAUNCH_PATTERN = /^Async agent launched[\s\S]*?\bagentId:\s*([\w-]+)/u
|
|
53
55
|
// The notice the harness writes when a task it took into the background ends
|
|
54
56
|
// (joshuafolkken/kit#1696). It is anchored at the start because the whole content of that line *is*
|
|
55
57
|
// the notice — a body merely quoting one is text, the same hazard `read_id` unquotes for — and it
|
|
@@ -96,6 +98,14 @@ function launch_id(text: string): string {
|
|
|
96
98
|
return LAUNCH_PATTERN.exec(text)?.[1] ?? NO_BACKGROUND
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
// The id of a subagent the harness took into the background (joshuafolkken/kit#2774). Its end is the
|
|
102
|
+
// same `<task-id>` notice a command's is, so `finished_id` pairs it unchanged. It is kept apart from
|
|
103
|
+
// `launch_id` on purpose: a subagent is not a command, and folding it in would open a timeline window
|
|
104
|
+
// that re-stamps the spans a backgrounded gate running beside the review is owed.
|
|
105
|
+
function agent_launch_id(text: string): string {
|
|
106
|
+
return AGENT_LAUNCH_PATTERN.exec(text)?.[1] ?? NO_BACKGROUND
|
|
107
|
+
}
|
|
108
|
+
|
|
99
109
|
// The background run a task-notification line reports the end of, or nothing for every other line —
|
|
100
110
|
// which is nearly all of them, since only one line kind in a transcript carries this notice.
|
|
101
111
|
function finished_id(text: string): string {
|
|
@@ -288,6 +298,7 @@ const time_background = {
|
|
|
288
298
|
NO_BACKGROUND,
|
|
289
299
|
NO_FINISH,
|
|
290
300
|
launch_id,
|
|
301
|
+
agent_launch_id,
|
|
291
302
|
finished_id,
|
|
292
303
|
finished_at,
|
|
293
304
|
read_id,
|
|
@@ -117,6 +117,9 @@ interface Block {
|
|
|
117
117
|
// it is what pairs a launch with the call that later reads its output, so the minutes the command
|
|
118
118
|
// actually ran can be placed on the timeline instead of only the seconds its launch call took.
|
|
119
119
|
background_id: string
|
|
120
|
+
// The id of a subagent this block launched into the background, and `''` for every other block
|
|
121
|
+
// (joshuafolkken/kit#2774) — apart from `background_id` so the timeline windows stay commands only.
|
|
122
|
+
agent_id: string
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
interface TranscriptLine {
|
|
@@ -183,6 +186,7 @@ function to_block(raw: z.infer<typeof BLOCK_SCHEMA>): Block {
|
|
|
183
186
|
error_text: is_error === true ? text.slice(0, ERROR_TEXT_LIMIT) : '',
|
|
184
187
|
refusal_guard: is_error === true ? guard_from_refusal(text) : '',
|
|
185
188
|
background_id: time_background.launch_id(text),
|
|
189
|
+
agent_id: time_background.agent_launch_id(text),
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
192
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# Written by `josh init` for a static project (joshuafolkken/kit#2693). kit's CLI runs on tsx, which
|
|
2
2
|
# depends on esbuild, and pnpm fails an install whose dependencies carry an unapproved build script.
|
|
3
|
+
# unrs-resolver arrives through kit's optional ESLint import peers (joshuafolkken/kit#2785).
|
|
3
4
|
allowBuilds:
|
|
4
5
|
esbuild: true
|
|
6
|
+
unrs-resolver: true
|