@joshuafolkken/kit 1.1094.0 → 1.1128.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/SKILL.md +1 -1
- package/.claude/skills/workflow-commands/background-commands.md +36 -85
- package/.claude/skills/workflow-commands/backlogrun-progress.md +29 -32
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -2
- package/.claude/skills/workflow-commands/fullrun.md +5 -4
- package/.claude/skills/workflow-commands/halfrun.md +5 -4
- package/.claude/skills/workflow-commands/latest-gate.md +7 -15
- package/.claude/skills/workflow-commands/pre-gate-cut.md +4 -8
- package/.claude/skills/workflow-commands/split-assessment.md +4 -18
- package/.github/actions/setup-node/action.yml +33 -0
- package/.github/actions/setup-pnpm/action.yml +4 -4
- package/.github/workflows/auto-tag.yml +2 -3
- package/.github/workflows/ci.yml +7 -8
- package/.github/workflows/dependabot-auto-merge.yml +1 -0
- package/.github/workflows/pr-classification.yml +1 -3
- package/.github/workflows/production.yml +1 -0
- package/.github/workflows/publish.yml +10 -9
- package/.github/workflows/sonar-qube.yml +17 -13
- package/.pnpmfile.mjs +29 -0
- package/AGENTS.md +4 -11
- package/CLAUDE.md +24 -17
- package/GEMINI.md +4 -13
- package/README.md +8 -3
- package/SECURITY.md +1 -0
- package/cspell/index.yaml +4 -0
- package/dist/CLAUDE.basic.md +6 -6
- package/dist/CLAUDE.md +24 -17
- package/dist/CLAUDE.static.md +6 -6
- package/dist/config-merge/index.js +33 -9
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-NKOG3XAV.js → chunk-4JZDP5X5.js} +2 -2
- package/dist/hooks/{chunk-3C4YCCEF.js → chunk-5SD2YM7Z.js} +4 -4
- package/dist/hooks/{chunk-OIZUHPJI.js → chunk-77J7IEQM.js} +1 -1
- package/dist/hooks/{chunk-KHWHKJOM.js → chunk-7HVB7SZG.js} +3 -3
- package/dist/hooks/{chunk-UD24EAIL.js → chunk-AIOIOMPX.js} +1875 -104
- package/dist/hooks/{chunk-M2XDCUKD.js → chunk-M7QKEKWO.js} +88 -1783
- package/dist/hooks/{chunk-E54KGFEQ.js → chunk-MKVGBQPA.js} +1 -1
- package/dist/hooks/{chunk-7ZPFAVR7.js → chunk-NXYMNPUQ.js} +3 -3
- package/dist/hooks/{chunk-O4LL4HTI.js → chunk-QVLT52OV.js} +2 -2
- package/dist/hooks/{chunk-IL27YTPH.js → chunk-XYUIQOW3.js} +2 -2
- package/dist/hooks/codex-hook-adapter.js +12 -47
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/inputs.json +180 -179
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +17 -12
- package/dist/josh.js +79 -30
- package/dist/version/index.js +36 -20
- package/eslint/rules/unicorn.js +3 -2
- package/package.json +18 -2
- package/pnpm-workspace.yaml +5 -7
- package/prompts/collaboration-workflow/glossary.md +17 -0
- package/prompts/collaboration-workflow/issue-template.md +1 -1
- package/prompts/collaboration-workflow/operating-rules.md +1 -1
- package/prompts/collaboration-workflow/principles.md +9 -0
- package/prompts/collaboration-workflow.md +1 -0
- package/scripts/backlog/backlog-arrival.ts +30 -10
- package/scripts/backlog/backlog-budget-cli.ts +15 -16
- package/scripts/backlog/backlog-offer-cli.ts +6 -14
- package/scripts/backlog/backlog-plan.ts +29 -18
- package/scripts/backlog/backlog-ready.ts +20 -7
- package/scripts/config-merge/parse-jsonc.ts +33 -3
- package/scripts/config-merge/patch-json-key.ts +13 -6
- package/scripts/cost-runtime/cost-cli.ts +3 -7
- package/scripts/doctor/doctor-consumer.ts +3 -5
- package/scripts/doctor/doctor-io.ts +9 -13
- package/scripts/document/backlogrun-parent-read-set.ts +12 -1
- package/scripts/document/document-byte-budget.ts +1 -0
- package/scripts/document/entry-read-budget.ts +13 -7
- package/scripts/document/entry-read-set.ts +32 -8
- package/scripts/document/lane-child-read-set.ts +7 -0
- package/scripts/document/read-set-point-of-use.ts +8 -3
- package/scripts/document/read-set-trim.ts +3 -1
- package/scripts/epic/epic-audit.ts +3 -18
- package/scripts/gh/git-gh-issue-list.ts +12 -11
- package/scripts/gh/git-gh-issue-write.ts +4 -1
- package/scripts/gh/git-gh-pr-auto-merge.ts +4 -1
- package/scripts/git/git-command.ts +6 -1
- package/scripts/git/git-spawn-sync.ts +65 -0
- package/scripts/git/git-spawn.ts +5 -4
- package/scripts/git/stash/stash-pop-args.ts +1 -6
- package/scripts/init/init-file-action.ts +54 -0
- package/scripts/init/init-logic-migrate.ts +35 -17
- package/scripts/init/init-logic-secretlint.ts +1 -1
- package/scripts/init/init-logic.ts +6 -22
- package/scripts/init/init.ts +11 -68
- package/scripts/init/kit-written-paths.ts +2 -0
- package/scripts/init/project-profile.ts +3 -6
- package/scripts/init/start-steps.ts +2 -1
- package/scripts/issue/defect-rate-cli.ts +61 -20
- package/scripts/issue/issue-backlinks.ts +7 -14
- package/scripts/issue/issue-cite.ts +12 -6
- package/scripts/issue/issue-file.ts +5 -12
- package/scripts/issue/markdown-section.ts +21 -6
- package/scripts/issue/measure-rerun-cli.ts +73 -14
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/josh/josh-commands-workflow.ts +1 -1
- package/scripts/josh/run-github-prerequisite.ts +3 -6
- package/scripts/lane/lane-await.ts +43 -13
- package/scripts/lane/lane-close.ts +4 -1
- package/scripts/lane/lane-launch-cli.ts +10 -16
- package/scripts/lane/openai-review-broker.ts +4 -1
- package/scripts/lib/cli-flags.ts +31 -2
- package/scripts/lib/error-message.ts +12 -1
- package/scripts/lines/line-targets.ts +0 -11
- package/scripts/lines/lines-command.ts +5 -3
- package/scripts/propagate/propagate-git.ts +3 -8
- package/scripts/refactor/refactor-scan.ts +2 -1
- package/scripts/registry-migration/migrate-logic.ts +28 -19
- package/scripts/registry-migration/migrate.ts +6 -5
- package/scripts/release/github-release-cli.ts +14 -3
- package/scripts/release/release-publish.ts +36 -2
- package/scripts/release/release-worktree.ts +97 -2
- package/scripts/review/review-checkout.ts +3 -2
- package/scripts/review/review-record-cli.ts +9 -16
- package/scripts/rules/early-heartbeat.ts +4 -3
- package/scripts/rules/lane-park.ts +9 -0
- package/scripts/rules/stop-rules.ts +7 -4
- package/scripts/run/run-carry-args.ts +8 -12
- package/scripts/run/run-cut-args.ts +1 -6
- package/scripts/run/run-ending-cli.ts +1 -5
- package/scripts/run/run-entry-cli.ts +40 -9
- package/scripts/run/run-entry-stop.ts +104 -0
- package/scripts/run/run-event-stream-emit.ts +7 -2
- package/scripts/run/run-hold.ts +4 -1
- package/scripts/run/run-liveness-cli.ts +1 -5
- package/scripts/run/run-merge-cli.ts +2 -10
- package/scripts/run/run-parent-cut-hook.ts +4 -1
- package/scripts/run/run-prep-cli.ts +4 -1
- package/scripts/run/run-progress-args.ts +20 -4
- package/scripts/run/run-progress-cli.ts +31 -29
- package/scripts/run/run-progress-clock.ts +4 -1
- package/scripts/run/run-ship-cli.ts +8 -16
- package/scripts/run/run-ship-probe.ts +4 -1
- package/scripts/run/run-status-cli.ts +2 -15
- package/scripts/run/run-tidy-lanes.ts +4 -1
- package/scripts/run/run-wake-cli.ts +2 -10
- package/scripts/run/run-watcher-guard.ts +9 -5
- package/scripts/safe-chain/pack-hook.ts +92 -0
- package/scripts/sync/sync-ai-files.ts +4 -0
- package/scripts/test/test-declared-changed.ts +13 -16
- package/scripts/test/test-declared.ts +3 -7
- package/scripts/version/version-commands.ts +11 -1
- package/templates/CLAUDE.basic.md +6 -6
- package/templates/workflows/ci.yml +16 -82
- package/templates/workflows/dependabot-auto-merge.yml +1 -0
- package/templates/workflows/github-release.yml +1 -3
- package/tsconfig.sonar.json +4 -22
|
@@ -18,10 +18,11 @@ import { backlog_stalled } from './backlog-stalled'
|
|
|
18
18
|
//
|
|
19
19
|
// **"New" is measured against the watcher's own start**, so one issue wakes the parent once: the next
|
|
20
20
|
// `--wait` the parent starts takes the pool as it stands then — the woken issue included, dispatched or
|
|
21
|
-
// not — as its baseline. A reading that fails is not an arrival; the probe says nothing and
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
21
|
+
// not — as its baseline. A reading that fails is not an arrival; the probe says nothing and reads again
|
|
22
|
+
// a minute later. A baseline that cannot be read is never read as empty — an empty one would wake the
|
|
23
|
+
// parent for the whole pool — but it is not given up on either: the scheduled report no longer ends the
|
|
24
|
+
// wait (joshuafolkken/kit#3102), so an inert probe would leave the parent without a wake until `--hours`. The
|
|
25
|
+
// probe reads the baseline again each minute until one answers.
|
|
25
26
|
//
|
|
26
27
|
// **Every read is bounded.** The probe is awaited inside the watcher's tick loop, so a `gh` call that
|
|
27
28
|
// hangs would otherwise freeze the loop — its `--hours` bound, its liveness ping and the exit that is
|
|
@@ -116,19 +117,40 @@ async function never_arrives(): Promise<boolean> {
|
|
|
116
117
|
|
|
117
118
|
const INERT: ArrivalProbe = { has_arrived: never_arrives }
|
|
118
119
|
|
|
119
|
-
//
|
|
120
|
+
// A probe spent on the baseline reports no arrival, whether or not the baseline answered.
|
|
121
|
+
async function retry_baseline(
|
|
122
|
+
answered: Array<ReadonlySet<string>>,
|
|
123
|
+
ports: ReadyPorts,
|
|
124
|
+
): Promise<boolean> {
|
|
125
|
+
const late = await read_baseline(ports)
|
|
126
|
+
|
|
127
|
+
if (late !== undefined) answered.push(late)
|
|
128
|
+
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The next-probe instant is set before any read is awaited. A baseline that did not answer is read again
|
|
133
|
+
// at the next probe instead of the reading, and the first one that answers is the baseline from then on
|
|
134
|
+
// — never an arrival itself, since it is the pool as it stands.
|
|
120
135
|
function probe_of(
|
|
121
|
-
|
|
136
|
+
initial: ReadonlySet<string> | undefined,
|
|
122
137
|
first_ms: number,
|
|
123
138
|
ports: ReadyPorts,
|
|
124
139
|
): ArrivalProbe {
|
|
125
140
|
let next_ms = first_ms
|
|
141
|
+
// At most one entry, held in a list so the baseline that answers after an await is added rather than
|
|
142
|
+
// reassigned over the value read before it.
|
|
143
|
+
const answered: Array<ReadonlySet<string>> = initial === undefined ? [] : [initial]
|
|
126
144
|
|
|
127
145
|
async function has_arrived(now_ms: number): Promise<boolean> {
|
|
128
146
|
if (now_ms < next_ms) return false
|
|
129
147
|
|
|
130
148
|
next_ms = now_ms + PROBE_INTERVAL_MS
|
|
131
149
|
|
|
150
|
+
const [baseline] = answered
|
|
151
|
+
|
|
152
|
+
if (baseline === undefined) return await retry_baseline(answered, ports)
|
|
153
|
+
|
|
132
154
|
const reading = await read_reading(ports)
|
|
133
155
|
|
|
134
156
|
return reading !== undefined && arrivals(baseline, reading).length > NONE
|
|
@@ -146,12 +168,10 @@ async function start(
|
|
|
146
168
|
): Promise<ArrivalProbe> {
|
|
147
169
|
if (!(await is_parent_safely(is_parent))) return INERT
|
|
148
170
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return baseline === undefined ? INERT : probe_of(baseline, now_ms + PROBE_INTERVAL_MS, ports)
|
|
171
|
+
return probe_of(await read_baseline(ports), now_ms + PROBE_INTERVAL_MS, ports)
|
|
152
172
|
}
|
|
153
173
|
|
|
154
|
-
const backlog_arrival = {
|
|
174
|
+
const backlog_arrival = { PROBE_INTERVAL_MS, answered_issues, arrivals, start }
|
|
155
175
|
|
|
156
176
|
export type { ArrivalProbe }
|
|
157
177
|
export { backlog_arrival }
|
|
@@ -32,11 +32,7 @@ type ParsedValues = Partial<Record<OptionName, string | boolean>>
|
|
|
32
32
|
// A misspelled flag is refused rather than defaulted, the contract `path_decision.has_unknown_flag`
|
|
33
33
|
// states for the path-decided commands: an invocation nobody can read is never answered.
|
|
34
34
|
function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
|
|
35
|
-
return cli_flags.
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function text_of(value: string | boolean | undefined): string | undefined {
|
|
39
|
-
return typeof value === 'string' ? value : undefined
|
|
35
|
+
return cli_flags.values_of(argv, OPTIONS)
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
// `find` rather than a cast: an unrecognized word has to be refused, and a type assertion would
|
|
@@ -79,15 +75,15 @@ type Counts = Record<(typeof COUNT_NAMES)[number], number | undefined>
|
|
|
79
75
|
|
|
80
76
|
function counts_of(values: ParsedValues): Counts {
|
|
81
77
|
return {
|
|
82
|
-
idle: to_count(
|
|
83
|
-
max: to_count(
|
|
84
|
-
merged: to_count(
|
|
85
|
-
running: to_count(
|
|
78
|
+
idle: to_count(cli_flags.string_of(values.idle)),
|
|
79
|
+
max: to_count(cli_flags.string_of(values.max)),
|
|
80
|
+
merged: to_count(cli_flags.string_of(values.merged)),
|
|
81
|
+
running: to_count(cli_flags.string_of(values.running)),
|
|
86
82
|
}
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
function are_counts_readable(values: ParsedValues, counts: Counts): boolean {
|
|
90
|
-
return COUNT_NAMES.every((name) => is_readable(
|
|
86
|
+
return COUNT_NAMES.every((name) => is_readable(cli_flags.string_of(values[name]), counts[name]))
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
// **`--idle 0` is how the watch is turned off, and omitting the flag takes the default**
|
|
@@ -116,14 +112,17 @@ function to_idle_ms(idle_minutes: number | undefined): number | undefined {
|
|
|
116
112
|
// lands on the very first ask, before the run has started anything, and the fix is one flag the loop
|
|
117
113
|
// already holds: it always knows when it last had work.
|
|
118
114
|
function is_watch_readable(values: ParsedValues): boolean {
|
|
119
|
-
if (to_idle_ms(to_count(
|
|
115
|
+
if (to_idle_ms(to_count(cli_flags.string_of(values.idle))) === undefined) return true
|
|
120
116
|
|
|
121
|
-
return
|
|
117
|
+
return cli_flags.string_of(values.active) !== undefined
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
function are_values_readable(values: ParsedValues): boolean {
|
|
125
121
|
return (
|
|
126
|
-
is_readable(
|
|
122
|
+
is_readable(
|
|
123
|
+
cli_flags.string_of(values.active),
|
|
124
|
+
to_time_ms(cli_flags.string_of(values.active)),
|
|
125
|
+
) &&
|
|
127
126
|
is_watch_readable(values) &&
|
|
128
127
|
are_counts_readable(values, counts_of(values))
|
|
129
128
|
)
|
|
@@ -138,7 +137,7 @@ function input_of(values: ParsedValues, base: InputBase): BudgetInput {
|
|
|
138
137
|
|
|
139
138
|
return {
|
|
140
139
|
...base,
|
|
141
|
-
active_at_ms: to_time_ms(
|
|
140
|
+
active_at_ms: to_time_ms(cli_flags.string_of(values.active)) ?? base.started_at_ms,
|
|
142
141
|
merged: counts.merged ?? 0,
|
|
143
142
|
running: counts.running ?? 0,
|
|
144
143
|
max_issues: counts.max,
|
|
@@ -147,11 +146,11 @@ function input_of(values: ParsedValues, base: InputBase): BudgetInput {
|
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
function build_input(values: ParsedValues, now_ms: number): BudgetInput | undefined {
|
|
150
|
-
const answer = to_answer(
|
|
149
|
+
const answer = to_answer(cli_flags.string_of(values.answer))
|
|
151
150
|
|
|
152
151
|
if (answer === undefined) return undefined
|
|
153
152
|
|
|
154
|
-
const started_at_ms = to_time_ms(
|
|
153
|
+
const started_at_ms = to_time_ms(cli_flags.string_of(values.started))
|
|
155
154
|
|
|
156
155
|
if (started_at_ms === undefined) return undefined
|
|
157
156
|
if (!are_values_readable(values)) return undefined
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
3
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
4
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
5
|
import { rule_value_cache } from '#scripts/rules/rule-value-cache'
|
|
6
6
|
import { run_event_stream } from '#scripts/run/run-event-stream'
|
|
7
7
|
import { run_event_stream_emit } from '#scripts/run/run-event-stream-emit'
|
|
@@ -61,15 +61,7 @@ type ParsedValues = Partial<Record<OptionName, string | boolean | ReadonlyArray<
|
|
|
61
61
|
const FORWARDED = ['started', 'active', 'merged', 'running', 'max', 'idle'] as const
|
|
62
62
|
|
|
63
63
|
function read_values(argv: ReadonlyArray<string>): ParsedValues | undefined {
|
|
64
|
-
|
|
65
|
-
return parseArgs({ args: [...argv], options: OPTIONS, strict: true }).values
|
|
66
|
-
} catch {
|
|
67
|
-
return undefined
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function text_of(value: string | boolean | ReadonlyArray<string> | undefined): string | undefined {
|
|
72
|
-
return typeof value === 'string' ? value : undefined
|
|
64
|
+
return cli_flags.values_of(argv, OPTIONS)
|
|
73
65
|
}
|
|
74
66
|
|
|
75
67
|
function to_count(raw: string | undefined): number | undefined {
|
|
@@ -94,8 +86,8 @@ interface OfferCounts {
|
|
|
94
86
|
// The two counts the mapping needs: `--running` decides `wait`, `--retries` decides `retry`. Either
|
|
95
87
|
// given but unreadable refuses the whole invocation.
|
|
96
88
|
function counts_of(values: ParsedValues): OfferCounts | undefined {
|
|
97
|
-
const running = count_of(
|
|
98
|
-
const retries = count_of(
|
|
89
|
+
const running = count_of(cli_flags.string_of(values.running), DEFAULT_COUNT)
|
|
90
|
+
const retries = count_of(cli_flags.string_of(values.retries), DEFAULT_COUNT)
|
|
99
91
|
|
|
100
92
|
if (running === undefined || retries === undefined) return undefined
|
|
101
93
|
|
|
@@ -109,7 +101,7 @@ function excludes_of(values: ParsedValues): ReadonlyArray<string> {
|
|
|
109
101
|
}
|
|
110
102
|
|
|
111
103
|
function next_argv(values: ParsedValues): ReadonlyArray<string> {
|
|
112
|
-
const repo =
|
|
104
|
+
const repo = cli_flags.string_of(values.repo)
|
|
113
105
|
|
|
114
106
|
return [
|
|
115
107
|
'backlog:next',
|
|
@@ -120,7 +112,7 @@ function next_argv(values: ParsedValues): ReadonlyArray<string> {
|
|
|
120
112
|
|
|
121
113
|
function budget_argv(values: ParsedValues, answer: string): ReadonlyArray<string> {
|
|
122
114
|
const forwarded = FORWARDED.flatMap((name) => {
|
|
123
|
-
const value =
|
|
115
|
+
const value = cli_flags.string_of(values[name])
|
|
124
116
|
|
|
125
117
|
return value === undefined ? [] : [`--${name}`, value]
|
|
126
118
|
})
|
|
@@ -3,6 +3,7 @@ import type { IssueReference } from '#scripts/epic/epic-reference'
|
|
|
3
3
|
import { epic_report, type EpicNextResult } from '#scripts/epic/epic-report'
|
|
4
4
|
import { epic_solo } from '#scripts/epic/epic-solo'
|
|
5
5
|
import { epic_triage } from '#scripts/epic/epic-triage'
|
|
6
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
6
7
|
import { has_label_name, IN_PROGRESS_LABEL, RUN_SOLO_LABEL } from '#scripts/issue/issue-labels'
|
|
7
8
|
import type { OutOfScopeRow } from './backlog-scope'
|
|
8
9
|
|
|
@@ -57,23 +58,29 @@ interface PlanContext {
|
|
|
57
58
|
|
|
58
59
|
// A child elsewhere is qualified, because a bare number would name *this* repository's issue of that
|
|
59
60
|
// number — a different issue (joshuafolkken/kit#1016). Takes the reference rather than the child, so
|
|
60
|
-
// a `blocked_by` edge is named by the same rule the child itself is.
|
|
61
|
-
|
|
61
|
+
// a `blocked_by` edge is named by the same rule the child itself is. This repository's issue is cited
|
|
62
|
+
// (joshuafolkken/kit#3099): the plan is copied into a report verbatim, so the line it prints is the
|
|
63
|
+
// citation the report carries.
|
|
64
|
+
function cite_of(reference: IssueReference, repo: string, title: string | undefined): string {
|
|
62
65
|
const number = String(reference.number)
|
|
63
66
|
|
|
64
|
-
return reference.repo === repo
|
|
67
|
+
return reference.repo === repo
|
|
68
|
+
? issue_cite.reference(repo, number, title)
|
|
69
|
+
: `${reference.repo}#${number}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function reference_of(reference: IssueReference, repo: string): string {
|
|
73
|
+
return cite_of(reference, repo, undefined)
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
// A title is only known for this repository's issues — the open listing the titles come from is this
|
|
68
77
|
// repository's. A child elsewhere is still named, just without one.
|
|
69
|
-
function title_of(child: EpicChild, context: PlanContext): string {
|
|
70
|
-
return child.repo === context.repo ?
|
|
78
|
+
function title_of(child: EpicChild, context: PlanContext): string | undefined {
|
|
79
|
+
return child.repo === context.repo ? context.titles.get(child.number) : undefined
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
function join_row(reference: string,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return `${ROW_INDENT}${parts.join(' ')}`
|
|
82
|
+
function join_row(reference: string, note: string): string {
|
|
83
|
+
return note === '' ? `${ROW_INDENT}${reference}` : `${ROW_INDENT}${reference} — ${note}`
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
// A `run:solo` child is marked beside its number, so a person reading the plan sees which issues
|
|
@@ -89,15 +96,15 @@ function mark_of(child: EpicChild): string | undefined {
|
|
|
89
96
|
return epic_triage.is_triaged(child) ? undefined : UNTRIAGED_MARK
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
function marked_reference(child: EpicChild, repo: string): string {
|
|
93
|
-
const reference =
|
|
99
|
+
function marked_reference(child: EpicChild, repo: string, title?: string): string {
|
|
100
|
+
const reference = cite_of(child, repo, title)
|
|
94
101
|
const mark = mark_of(child)
|
|
95
102
|
|
|
96
103
|
return mark === undefined ? reference : `${reference} ${mark}`
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
function row_of(child: EpicChild, context: PlanContext, note: string): string {
|
|
100
|
-
return join_row(marked_reference(child, context.repo
|
|
107
|
+
return join_row(marked_reference(child, context.repo, title_of(child, context)), note)
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
// Why this one is not offered yet. The blocker numbers are the answer whenever there are any — that
|
|
@@ -164,8 +171,10 @@ function waiting_lines(children: ReadonlyArray<EpicChild>, context: PlanContext)
|
|
|
164
171
|
})
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
function scope_lines(rows: ReadonlyArray<OutOfScopeRow
|
|
168
|
-
return rows.map((row) =>
|
|
174
|
+
function scope_lines(rows: ReadonlyArray<OutOfScopeRow>, repo: string): Array<string> {
|
|
175
|
+
return rows.map((row) =>
|
|
176
|
+
join_row(issue_cite.reference(repo, String(row.number), row.title), row.reason),
|
|
177
|
+
)
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
// The classification could not be made, so every section below it would be rendered from buckets
|
|
@@ -186,10 +195,12 @@ interface NamedPlan {
|
|
|
186
195
|
only: boolean
|
|
187
196
|
}
|
|
188
197
|
|
|
189
|
-
// The named issues are this repository's own, so
|
|
190
|
-
// same way the pool's rows are. The order is the invocation's, never sorted.
|
|
198
|
+
// The named issues are this repository's own, so they are cited against it and a title is looked up
|
|
199
|
+
// the same way the pool's rows are. The order is the invocation's, never sorted.
|
|
191
200
|
function named_lines(named: ReadonlyArray<number>, context: PlanContext): Array<string> {
|
|
192
|
-
return named.map((issue) =>
|
|
201
|
+
return named.map((issue) =>
|
|
202
|
+
join_row(issue_cite.reference(context.repo, String(issue), context.titles.get(issue)), ''),
|
|
203
|
+
)
|
|
193
204
|
}
|
|
194
205
|
|
|
195
206
|
// The section leads the plan when the invocation named issues, and is absent otherwise — a bare
|
|
@@ -230,7 +241,7 @@ function format_plan(
|
|
|
230
241
|
'',
|
|
231
242
|
section(HUMAN_HEADING, child_lines(result.blocked_on_people, context, no_note)),
|
|
232
243
|
'',
|
|
233
|
-
section(SCOPE_HEADING, scope_lines(out_of_scope)),
|
|
244
|
+
section(SCOPE_HEADING, scope_lines(out_of_scope, context.repo)),
|
|
234
245
|
].join('\n')
|
|
235
246
|
}
|
|
236
247
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
2
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
1
3
|
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
2
4
|
import { lane_capacity } from '#scripts/lane/lane-capacity'
|
|
3
5
|
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
@@ -92,35 +94,46 @@ async function read_ready(ports: ReadyPorts = DEFAULT_PORTS): Promise<ReadyReadi
|
|
|
92
94
|
return { issues: await ports.ready_issues(), free_lanes }
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
// `undefined` unless there is both runnable work and a lane for it — the only state worth a line.
|
|
96
|
-
|
|
97
|
+
// `undefined` unless there is both runnable work and a lane for it — the only state worth a line. Each
|
|
98
|
+
// issue is a number-link against `slug` (joshuafolkken/kit#3099): the parent copies this line into its
|
|
99
|
+
// report, so a bare `#N` here is the citation the Stop guard sends back.
|
|
100
|
+
function ready_line(reading: ReadyReading, slug: string | undefined): string | undefined {
|
|
97
101
|
if (reading.issues.length === NONE || reading.free_lanes <= NO_FREE) return undefined
|
|
98
102
|
|
|
99
|
-
const
|
|
103
|
+
const cite = issue_cite.citer(slug, new Map())
|
|
104
|
+
const issues = reading.issues.map((issue) => cite(issue)).join(' ')
|
|
100
105
|
|
|
101
106
|
return `ready ${issues} · free lanes ${String(reading.free_lanes)}`
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
type RepoRead = () => Promise<string | undefined>
|
|
110
|
+
|
|
104
111
|
// The watcher's line on exit, for the driving parent alone — a `fullrun` watcher has no pool to pick
|
|
105
112
|
// from. Best-effort: a reading that fails prints nothing rather than failing the watcher whose exit is
|
|
106
113
|
// the wake.
|
|
107
114
|
async function parent_ready_line(
|
|
108
115
|
ports: ReadyPorts,
|
|
109
116
|
is_parent: () => Promise<boolean>,
|
|
117
|
+
read_repo: RepoRead,
|
|
110
118
|
): Promise<string | undefined> {
|
|
111
119
|
if (!(await is_parent())) return undefined
|
|
112
120
|
|
|
113
|
-
|
|
121
|
+
const reading = await read_ready(ports)
|
|
122
|
+
|
|
123
|
+
return reading.issues.length === NONE ? undefined : ready_line(reading, await read_repo())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function print_line(line: string | undefined): void {
|
|
127
|
+
if (line !== undefined) console.info(line)
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
async function print_ready_line(
|
|
117
131
|
ports: ReadyPorts = DEFAULT_PORTS,
|
|
118
132
|
is_parent: () => Promise<boolean> = run_headless.is_backlog_parent,
|
|
133
|
+
read_repo: RepoRead = git_gh_command.repo_get_name_with_owner,
|
|
119
134
|
): Promise<void> {
|
|
120
135
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (line !== undefined) console.info(line)
|
|
136
|
+
print_line(await parent_ready_line(ports, is_parent, read_repo))
|
|
124
137
|
} catch {
|
|
125
138
|
// A pick-up line is a hint; the wake it rides must still be delivered.
|
|
126
139
|
}
|
|
@@ -1,14 +1,44 @@
|
|
|
1
1
|
import { json_object_schema } from '#scripts/lib/schemas'
|
|
2
|
-
import
|
|
2
|
+
import { parseTree, printParseErrorCode, type Node, type ParseError } from 'jsonc-parser'
|
|
3
3
|
|
|
4
4
|
// Every managed JSON config kit reads (tsconfig.json, package.json, .vscode/*.json) may legally
|
|
5
5
|
// carry comments and trailing commas — TypeScript and VS Code both parse them as JSONC — so a plain
|
|
6
6
|
// `JSON.parse` would throw on files consumers hand-authored. Single-sourced here because the
|
|
7
7
|
// config-merge library and the init/sync merge helpers both need the exact same tolerant read.
|
|
8
|
+
|
|
9
|
+
// Built from the tree rather than with jsonc-parser's own `parse` / `getNodeValue`, which assign each
|
|
10
|
+
// key: a `"__proto__"` key would become the object's prototype instead of the own property
|
|
11
|
+
// `JSON.parse` keeps, and the spread-and-stringify write-back would then drop it from the file.
|
|
12
|
+
// `Object.fromEntries` defines properties, so every key stays data.
|
|
13
|
+
function property_key(property: Node): string {
|
|
14
|
+
return String(property.children?.[0]?.value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function node_value(node: Node | undefined): unknown {
|
|
18
|
+
if (node === undefined) return undefined
|
|
19
|
+
const children = node.children ?? []
|
|
20
|
+
if (node.type === 'array') return children.map((child) => node_value(child))
|
|
21
|
+
if (node.type !== 'object') return node.value
|
|
22
|
+
|
|
23
|
+
return Object.fromEntries(
|
|
24
|
+
children.map((property) => [property_key(property), node_value(property.children?.[1])]),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// jsonc-parser recovers from errors and returns a best-effort tree, so any reported error is
|
|
29
|
+
// rethrown: a malformed document must fail the read the way `JSON.parse` would, not merge partially.
|
|
8
30
|
function parse_jsonc(content: string): Record<string, unknown> {
|
|
9
|
-
const
|
|
31
|
+
const errors: Array<ParseError> = []
|
|
32
|
+
const tree = parseTree(content, errors, { allowTrailingComma: true })
|
|
33
|
+
const [first_error] = errors
|
|
34
|
+
|
|
35
|
+
if (first_error !== undefined) {
|
|
36
|
+
throw new SyntaxError(
|
|
37
|
+
`Invalid JSONC: ${printParseErrorCode(first_error.error)} at offset ${String(first_error.offset)}`,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
10
40
|
|
|
11
|
-
return json_object_schema.parse(
|
|
41
|
+
return json_object_schema.parse(node_value(tree))
|
|
12
42
|
}
|
|
13
43
|
|
|
14
44
|
export { parse_jsonc }
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createScanner,
|
|
3
|
+
findNodeAtLocation,
|
|
4
|
+
getNodeValue,
|
|
5
|
+
parseTree,
|
|
6
|
+
stripComments,
|
|
7
|
+
type Node,
|
|
8
|
+
} from 'jsonc-parser'
|
|
3
9
|
import { json_format } from './json-format'
|
|
4
10
|
|
|
5
11
|
// Set or remove one top-level key while leaving every other byte of the document alone.
|
|
@@ -163,13 +169,14 @@ function target_value_end(property: Node | undefined): number | undefined {
|
|
|
163
169
|
return value_node === undefined ? undefined : value_node.offset + value_node.length
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
// Whether the span holds a comment. `
|
|
167
|
-
//
|
|
168
|
-
// need `SyntaxKind`,
|
|
172
|
+
// Whether the span holds a comment. `stripComments` drops every comment token the scanner finds, so
|
|
173
|
+
// it changes the text exactly when a comment is present, and a `//` inside a string value is not
|
|
174
|
+
// counted — comparing token kinds directly would need `SyntaxKind`, an ambient const enum this
|
|
175
|
+
// project cannot import.
|
|
169
176
|
function contains_comment(content: string, node: Node): boolean {
|
|
170
177
|
const text = content.slice(node.offset, node.offset + node.length)
|
|
171
178
|
|
|
172
|
-
return
|
|
179
|
+
return stripComments(text) !== text
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
// Re-lay-out a property whose trailing comma has just appeared or disappeared. That one column
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
3
|
import { agent_role_profile, type AgentProvider } from '#scripts/agent/agent-role-profile'
|
|
4
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
5
|
import { codex_usage } from './codex-usage'
|
|
6
6
|
import { CONTEXT_CUT_THRESHOLD } from './context-cut-threshold'
|
|
7
7
|
import { cost_corpus } from './cost-corpus'
|
|
@@ -103,13 +103,9 @@ const PARSE_ARGS_OPTIONS = {
|
|
|
103
103
|
} as const
|
|
104
104
|
|
|
105
105
|
function parse_options(argv: ReadonlyArray<string>): Options | undefined {
|
|
106
|
-
|
|
107
|
-
const { values } = parseArgs({ args: [...argv], options: PARSE_ARGS_OPTIONS, strict: true })
|
|
106
|
+
const values = cli_flags.values_of(argv, PARSE_ARGS_OPTIONS)
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
} catch {
|
|
111
|
-
return undefined
|
|
112
|
-
}
|
|
108
|
+
return values === undefined ? undefined : to_options(values)
|
|
113
109
|
}
|
|
114
110
|
|
|
115
111
|
// An empty corpus is reported, never priced at zero. "No transcript was found" and "this run was
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
3
4
|
import { json_value } from '#scripts/lib/json-value'
|
|
4
5
|
import { file_reader } from '#scripts/lib/read-file'
|
|
5
|
-
import { read_spawn_stdout } from '#scripts/lib/spawn-exit'
|
|
6
6
|
import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
7
|
-
import { execaSync } from 'execa'
|
|
8
7
|
import { z } from 'zod'
|
|
9
8
|
|
|
10
9
|
// The consumer-setup checks `josh doctor` runs in a repository that installs kit (joshuafolkken/kit#1930).
|
|
@@ -77,11 +76,9 @@ function is_plugin_declared(root: string): boolean {
|
|
|
77
76
|
// The value of `core.hooksPath`, or undefined when it is unset. A set value is what stops lefthook
|
|
78
77
|
// from installing kit's git hooks — the failure is silent but for one stderr line.
|
|
79
78
|
function configured_hooks_path(root: string): string | undefined {
|
|
80
|
-
const
|
|
81
|
-
reject: false,
|
|
79
|
+
const value = git_spawn_sync.read(['-C', root, 'config', '--get', 'core.hooksPath'], {
|
|
82
80
|
timeout: GIT_TIMEOUT_MS,
|
|
83
81
|
})
|
|
84
|
-
const value = read_spawn_stdout(result).trim()
|
|
85
82
|
|
|
86
83
|
return value === '' ? undefined : value
|
|
87
84
|
}
|
|
@@ -159,6 +156,7 @@ function report_consumer_setup(root: string): void {
|
|
|
159
156
|
const doctor_consumer = {
|
|
160
157
|
claude_md_line,
|
|
161
158
|
claude_md_state,
|
|
159
|
+
configured_hooks_path,
|
|
162
160
|
hooks_path_line,
|
|
163
161
|
is_kit_consumer,
|
|
164
162
|
is_plugin_declared,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
4
|
+
import { read_spawn_stdout } from '#scripts/lib/spawn-exit'
|
|
4
5
|
import { execaSync } from 'execa'
|
|
5
6
|
import { doctor_logic } from './doctor-logic'
|
|
6
7
|
|
|
@@ -69,18 +70,13 @@ function classify_git_failure(exit_code: number | undefined, stderr: string): Gi
|
|
|
69
70
|
// caller's check, because reporting an unknown as a clean result is the false all-clear
|
|
70
71
|
// joshuafolkken/kit#805 exists to remove. See the two classifiers above for what proves what.
|
|
71
72
|
function resolve_git_top_level(): GitTopLevel {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
if (result.exitCode === 0) return classify_top_level(read_spawn_stdout(result).trim())
|
|
82
|
-
|
|
83
|
-
return classify_git_failure(result.exitCode, read_spawn_stderr(result))
|
|
73
|
+
// The C locale `toplevel` asks under is what keeps the stderr match below from failing on a
|
|
74
|
+
// translated message and bringing the spurious warning back.
|
|
75
|
+
const result = git_spawn_sync.toplevel(GIT_TIMEOUT_MS)
|
|
76
|
+
|
|
77
|
+
if (result.exit_code === 0) return classify_top_level(result.stdout.trim())
|
|
78
|
+
|
|
79
|
+
return classify_git_failure(result.exit_code, result.stderr)
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
// Whether one line is the setting itself rather than prose about it. kit's own template explains the
|
|
@@ -34,15 +34,26 @@ const UNUSED_SKILL_SECTIONS: ReadonlyArray<string> = [
|
|
|
34
34
|
// the park — so it drops none. Named empty so the shared trim reads the same shape as the worker's.
|
|
35
35
|
const SKIPPED_POINT_OF_USE: ReadonlySet<string> = new Set<string>()
|
|
36
36
|
|
|
37
|
+
// **The parent runs the end-of-run retrospective** (`SKILL.md` §2j) when `run:step` prints it, but its
|
|
38
|
+
// manifest names `retrospective.md` only through `backlogrun-steps.md` — one hop further than the
|
|
39
|
+
// derivation follows — so the parent's reach names it (joshuafolkken/kit#3078).
|
|
40
|
+
const REACHED_POINT_OF_USE: ReadonlySet<string> = new Set(['retrospective.md'])
|
|
41
|
+
|
|
37
42
|
function costed(root: string): ReadSetCost {
|
|
38
43
|
return read_set_trim.costed(root, {
|
|
39
44
|
base_entry: BACKLOGRUN,
|
|
40
45
|
label: BACKLOGRUN,
|
|
41
46
|
unused_skill_sections: UNUSED_SKILL_SECTIONS,
|
|
42
47
|
skipped_point_of_use: SKIPPED_POINT_OF_USE,
|
|
48
|
+
reached_point_of_use: REACHED_POINT_OF_USE,
|
|
43
49
|
})
|
|
44
50
|
}
|
|
45
51
|
|
|
46
|
-
const backlogrun_parent_read_set = {
|
|
52
|
+
const backlogrun_parent_read_set = {
|
|
53
|
+
BACKLOGRUN,
|
|
54
|
+
REACHED_POINT_OF_USE,
|
|
55
|
+
UNUSED_SKILL_SECTIONS,
|
|
56
|
+
costed,
|
|
57
|
+
}
|
|
47
58
|
|
|
48
59
|
export { backlogrun_parent_read_set }
|
|
@@ -88,6 +88,7 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
|
|
|
88
88
|
{ path: 'prompts/collaboration-workflow.md', bytes: 8192 },
|
|
89
89
|
{ path: 'prompts/collaboration-workflow/file-edits.md', bytes: 12_288 },
|
|
90
90
|
{ path: 'prompts/collaboration-workflow/gh-rest.md', bytes: 8192 },
|
|
91
|
+
{ path: 'prompts/collaboration-workflow/glossary.md', bytes: 4096 },
|
|
91
92
|
{ path: 'prompts/collaboration-workflow/issue-citation.md', bytes: 8192 },
|
|
92
93
|
// #2662 adds the feature and compatibility declarations to the canonical filing procedure.
|
|
93
94
|
{ path: 'prompts/collaboration-workflow/issue-template.md', bytes: 28_672 },
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
// **The per-document ceiling was a proxy for a run's cost, not the cost.** A run does not pay "one
|
|
5
5
|
// document's size"; it pays the total its entry reads — the mandated skill files, the sections they
|
|
6
|
-
// point out to, the point-of-use documents
|
|
7
|
-
// carries. `read:set`
|
|
6
|
+
// point out to, the point-of-use documents that entry reaches later, and the resident `CLAUDE.md` every
|
|
7
|
+
// request carries. `read:set`'s `total read` is that sum less `CLAUDE.md`, which `entry_total_bytes`
|
|
8
|
+
// adds back (`RESIDENT_BASE`), so this budgets the whole directly: the entry total is
|
|
8
9
|
// the primary ceiling, and the per-document ceiling (`document-byte-budget.ts`) falls back to the
|
|
9
10
|
// documents no entry reads (`document-reachability.ts` draws the line). A document an entry reads is
|
|
10
11
|
// held by the entry total and carries no per-document ceiling, so neither budget holds it twice.
|
|
@@ -86,11 +87,16 @@ const ENTRY_READ_BUDGET: ReadonlyArray<EntryBudget> = [
|
|
|
86
87
|
// `wip-cap.md`, so its ceiling stays.
|
|
87
88
|
// joshuafolkken/kit#3023 added the `prrun` keyword to `SKILL.md` and `CLAUDE.md`, tipping backlogrun
|
|
88
89
|
// one block over; `prrun` reads `fullrun`'s set plus its own short manifest.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
// Lowered in joshuafolkken/kit#3078 — a correction of an over-count, not a trim: every entry was
|
|
91
|
+
// charged for every global point-of-use document, so `kickoff` paid for `followup.md` and the
|
|
92
|
+
// `backlogrun-*` phase documents it never opens. Each total now counts only what its path reaches.
|
|
93
|
+
// joshuafolkken/kit#3077 trimmed non-resident rule bodies from `CLAUDE.md`, lowering fullrun, prrun
|
|
94
|
+
// and backlogrun one block each.
|
|
95
|
+
{ entry: 'kickoff', bytes: 65_536 },
|
|
96
|
+
{ entry: 'fullrun', bytes: 126_976 },
|
|
97
|
+
{ entry: 'halfrun', bytes: 106_496 },
|
|
98
|
+
{ entry: 'prrun', bytes: 131_072 },
|
|
99
|
+
{ entry: 'backlogrun', bytes: 212_992 },
|
|
94
100
|
{ entry: LANE_CHILD, bytes: 77_824 },
|
|
95
101
|
]
|
|
96
102
|
|