@joshuafolkken/kit 1.1035.0 → 1.1050.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/verify-ui/SKILL.md +1 -1
- package/.claude/skills/workflow-commands/SKILL.md +2 -2
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-child.md +2 -2
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-park.md +6 -0
- package/.claude/skills/workflow-commands/backlogrun-progress.md +30 -36
- package/.claude/skills/workflow-commands/backlogrun-steps.md +16 -16
- package/.claude/skills/workflow-commands/chain-rule.md +2 -2
- package/.claude/skills/workflow-commands/followup-reference.md +2 -2
- package/.claude/skills/workflow-commands/issue-comments.md +1 -1
- package/.claude/skills/workflow-commands/observation-filing.md +41 -86
- package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -36
- package/.claude/skills/workflow-commands/working-tree-hold.md +2 -2
- package/.github/workflows/auto-tag.yml +21 -0
- package/.github/workflows/publish.yml +9 -10
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-HU66PWWL.js → chunk-5BK3K7V4.js} +89 -41
- package/dist/hooks/{chunk-GLBDBJOK.js → chunk-7VPDUBRN.js} +49 -151
- package/dist/hooks/{chunk-GJT4BNVE.js → chunk-DMQN45FN.js} +3 -3
- package/dist/hooks/{chunk-D67KVGGY.js → chunk-EJDX2TOB.js} +13 -3
- package/dist/hooks/{chunk-KG6PZD72.js → chunk-GQ7KP45M.js} +2 -2
- package/dist/hooks/{chunk-Y27M6A7X.js → chunk-HJIISEAC.js} +4 -4
- package/dist/hooks/{chunk-XPLP27WA.js → chunk-NAS4C7GP.js} +4 -26
- package/dist/hooks/{chunk-ZXNE6NY3.js → chunk-NJQOBE6L.js} +2 -2
- package/dist/hooks/{chunk-EIFCAJRB.js → chunk-RLDX5SOC.js} +1184 -946
- package/dist/hooks/{chunk-HOYJU5RM.js → chunk-WQ4OSBSS.js} +10 -3
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +15 -34
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +23 -16
- package/dist/hooks/stop-guard.js +13 -188
- package/dist/josh.js +4 -106
- package/dist/version/index.js +381 -9
- package/package.json +6 -6
- package/prompts/collaboration-workflow/file-edits.md +6 -53
- package/prompts/collaboration-workflow/residency.md +1 -1
- package/prompts/collaboration-workflow/rule-delivery.md +4 -35
- package/prompts/collaboration-workflow/shell-body.md +7 -9
- package/prompts/collaboration-workflow/turn-batching.md +21 -37
- package/prompts/collaboration-workflow/wip-cap.md +15 -18
- package/prompts/sonar-hotspot-handling.md +1 -1
- package/scripts/adopt/adopt-toolkits.ts +2 -0
- package/scripts/auto-ok/auto-ok-cli.ts +9 -9
- package/scripts/backlog/backlog-next.ts +24 -8
- package/scripts/backlog/backlog-plan-cli.ts +5 -1
- package/scripts/backlog/backlog-pool.ts +31 -11
- package/scripts/backlog/backlog-rank.ts +106 -0
- package/scripts/backlog/backlog-waves.ts +42 -29
- package/scripts/build/build-hooks.ts +7 -2
- package/scripts/delegation/delegation-policy.ts +11 -0
- package/scripts/document/document-byte-budget.ts +5 -5
- package/scripts/document/entry-read-budget.ts +5 -4
- package/scripts/epic/epic-lane-offer.ts +11 -22
- package/scripts/epic/epic-rank.ts +37 -0
- package/scripts/epic/epic-solo.ts +29 -28
- package/scripts/git/git-command.ts +1 -1
- package/scripts/git/git-execa-error.ts +6 -1
- package/scripts/git/git-next-issues.ts +34 -5
- package/scripts/git/git-preflight.ts +22 -4
- package/scripts/git/git-push-transport.ts +12 -69
- package/scripts/git/git-spawn.ts +79 -3
- package/scripts/git/git-ssh-keepalive.ts +74 -0
- package/scripts/git/gone-branch.ts +3 -2
- package/scripts/git/issue-labels.ts +12 -0
- package/scripts/git/issue-rank.ts +89 -0
- package/scripts/hooks/format-edited-cli.ts +15 -0
- package/scripts/hooks/format-edited-file.ts +2 -9
- package/scripts/hooks/pretool-guard-cli.ts +21 -0
- package/scripts/hooks/pretool-guard.ts +14 -15
- package/scripts/init/start-exec.ts +7 -1
- package/scripts/init/start-plan.ts +18 -5
- package/scripts/init/start-setup-pr.ts +46 -19
- package/scripts/init/start-steps.ts +41 -9
- package/scripts/init/start.ts +40 -10
- package/scripts/issue/issue-cite.ts +19 -1
- package/scripts/josh/josh-command-map.ts +0 -102
- package/scripts/josh/josh-commands-development.ts +2 -2
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/lane/lane-child-marker.ts +16 -2
- package/scripts/lane/lane-cli.ts +32 -14
- package/scripts/lane/lane-report.ts +10 -4
- package/scripts/propagate/propagate-plan.ts +185 -0
- package/scripts/propagate/propagate-run.ts +5 -1
- package/scripts/propagate/propagate-steps.ts +16 -1
- package/scripts/propagate/propagate-targets.ts +13 -2
- package/scripts/propagate/propagate.ts +60 -42
- package/scripts/registry-migration/migrate-logic.ts +1 -0
- package/scripts/release/github-release.ts +1 -1
- package/scripts/review/review-attest.ts +44 -0
- package/scripts/review/review-brief-cli.ts +49 -2
- package/scripts/review/review-brief.ts +46 -1
- package/scripts/review/review-stamps.ts +7 -0
- package/scripts/rules/implementation-cut.ts +1 -1
- package/scripts/rules/issue-fold.ts +1 -2
- package/scripts/rules/lane-carry-conflict.ts +1 -2
- package/scripts/rules/lane-park.ts +1 -2
- package/scripts/rules/pre-gate-cut.ts +2 -2
- package/scripts/run/run-headless.ts +22 -6
- package/scripts/run/run-parent-cut-hook.ts +144 -0
- package/scripts/run/run-progress-read.ts +2 -1
- package/scripts/run/run-progress.ts +21 -8
- package/scripts/run/run-ship-cli.ts +6 -74
- package/scripts/run/run-ship-preflight.ts +87 -0
- package/scripts/run/run-ship-review-steps.ts +5 -26
- package/scripts/run/run-ship-review.ts +9 -0
- package/scripts/run/run-ship-scoped.ts +44 -0
- package/scripts/run/run-ship-stage.ts +5 -1
- package/scripts/run/run-ship-steps.ts +105 -0
- package/scripts/run/run-ship.ts +2 -0
- package/scripts/run/run-wake-cli.ts +2 -2
- package/scripts/run/run-wake-describe.ts +19 -1
- package/scripts/run/run-watcher-hook.ts +1 -1
- package/scripts/time-runtime/time-bundle-call.ts +2 -2
- package/scripts/version/npm-registry.ts +97 -0
- package/scripts/version/version-commands.ts +5 -4
- package/scripts/version/version-remote.ts +26 -12
|
@@ -37,6 +37,9 @@ const DECISION_LABELS: ReadonlySet<string> = new Set([NEEDS_DECISION_LABEL, ALRE
|
|
|
37
37
|
// `tracked` is the narrowed set `epic_index.withheld_children` builds, not every child an epic
|
|
38
38
|
// tracks (joshuafolkken/kit#1668) — the narrowing itself is defined there, once, for this half and
|
|
39
39
|
// `auto-ok:next` alike.
|
|
40
|
+
// The part of the context that decides which rows the standalone half considers at all.
|
|
41
|
+
type ConsideredContext = Pick<StandaloneContext, 'tracked' | 'exclude' | 'repo'>
|
|
42
|
+
|
|
40
43
|
interface StandaloneContext {
|
|
41
44
|
tracked: ReadonlyMap<number, number>
|
|
42
45
|
exclude: ReadonlyArray<number>
|
|
@@ -135,26 +138,28 @@ function standalone_rows(issues: ReadonlyArray<OpenIssueData>): ReadonlyArray<Op
|
|
|
135
138
|
// are actually going to offer their children and applied here for the same reason. Which rows those
|
|
136
139
|
// are is `epic_index.withheld_children`'s answer, not a second test made here. A row the caller
|
|
137
140
|
// excluded is out of the pool entirely rather than merely unoffered: it has just merged.
|
|
138
|
-
function is_considered(issue: OpenIssueData, context:
|
|
141
|
+
function is_considered(issue: OpenIssueData, context: ConsideredContext): boolean {
|
|
139
142
|
return !context.tracked.has(issue.number) && !context.exclude.includes(issue.number)
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
// The standalone half, in the buckets the verdict is read from.
|
|
143
146
|
//
|
|
144
|
-
// The order is `git_next_issues.
|
|
145
|
-
// `🗒 Next issues` display and `auto-ok:next` already answer with,
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
147
|
+
// The order is `git_next_issues.order`'s and not a second one — the same ranking the
|
|
148
|
+
// `🗒 Next issues` display and `auto-ok:next` already answer with, so the three cannot disagree about
|
|
149
|
+
// what comes next. Anything it withholds is still open work: `needs-decision` is waiting on a person,
|
|
150
|
+
// and everything else — a standing blocker, a run already in progress — resolves on its own and is
|
|
151
|
+
// waiting on time.
|
|
152
|
+
//
|
|
153
|
+
// **The display's cap is not applied here** (joshuafolkken/kit#2928). Cut at this point, a sixth
|
|
154
|
+
// runnable `run:solo` issue never reached the gate that holds back what is ranked below it, so the
|
|
155
|
+
// cap is applied after that gate instead (`backlog-rank.ts`). A row past it is still reported as
|
|
156
|
+
// waiting and offered by a later ask, so the backlog drains without it ever being lost.
|
|
152
157
|
function classify_standalone(
|
|
153
158
|
issues: ReadonlyArray<OpenIssueData>,
|
|
154
159
|
context: StandaloneContext,
|
|
155
160
|
): Classification {
|
|
156
161
|
const considered = issues.filter((issue) => is_considered(issue, context))
|
|
157
|
-
const runnable = git_next_issues.
|
|
162
|
+
const runnable = git_next_issues.order(
|
|
158
163
|
considered.filter((issue) => auto_ok_cli.is_runnable(issue, context.tracked, context.exclude)),
|
|
159
164
|
)
|
|
160
165
|
const offered = new Set(runnable.map((issue) => issue.number))
|
|
@@ -301,11 +306,26 @@ function merge_classifications(left: Classification, right: Classification): Cla
|
|
|
301
306
|
}
|
|
302
307
|
}
|
|
303
308
|
|
|
309
|
+
// The keys of the rows the standalone half considers — the rows the offer's cap bounds. An issue an
|
|
310
|
+
// opted-in epic tracks is that epic's child here, and never capped, exactly as before.
|
|
311
|
+
function standalone_keys(
|
|
312
|
+
issues: ReadonlyArray<OpenIssueData>,
|
|
313
|
+
context: ConsideredContext,
|
|
314
|
+
): ReadonlySet<string> {
|
|
315
|
+
return keys_of(
|
|
316
|
+
to_children(
|
|
317
|
+
standalone_rows(issues).filter((issue) => is_considered(issue, context)),
|
|
318
|
+
context.repo,
|
|
319
|
+
),
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
304
323
|
const backlog_pool = {
|
|
305
324
|
classify_standalone,
|
|
306
325
|
cross_epic_cycles,
|
|
307
326
|
running_set,
|
|
308
327
|
settle_standalone,
|
|
328
|
+
standalone_keys,
|
|
309
329
|
drop_excluded,
|
|
310
330
|
epic_classification,
|
|
311
331
|
is_epic_row,
|
|
@@ -319,4 +339,4 @@ const backlog_pool = {
|
|
|
319
339
|
}
|
|
320
340
|
|
|
321
341
|
export { backlog_pool }
|
|
322
|
-
export type { StandaloneContext }
|
|
342
|
+
export type { ConsideredContext, StandaloneContext }
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { BusyRead } from '#scripts/epic/epic-busy'
|
|
2
|
+
import { epic_graph, type EpicChild } from '#scripts/epic/epic-graph'
|
|
3
|
+
import { epic_rank } from '#scripts/epic/epic-rank'
|
|
4
|
+
import type { EpicNextResult } from '#scripts/epic/epic-report'
|
|
5
|
+
import { epic_solo, type SoloGate, type SoloSelection } from '#scripts/epic/epic-solo'
|
|
6
|
+
import { git_next_issues } from '#scripts/git/git-next-issues'
|
|
7
|
+
|
|
8
|
+
// What the backlog offers, in the order it is decided (joshuafolkken/kit#2928): rank, then the
|
|
9
|
+
// `run:solo` gate, then the cap. `backlog:next` and `backlog:plan --waves` both call `select`, so the
|
|
10
|
+
// plan's first wave is what the run starts into an idle repository rather than an order the run never
|
|
11
|
+
// takes.
|
|
12
|
+
//
|
|
13
|
+
// **The cap comes last.** It used to be applied while the standalone half was classified, before the
|
|
14
|
+
// gate, so a `run:solo` issue ranked sixth or lower was cut before the gate could see it — and the
|
|
15
|
+
// issues below it were offered past it. Cut after the gate, the cap only ever removes what the gate
|
|
16
|
+
// would have offered, so it cannot hide a waiting `run:solo` issue from it. It still bounds only the
|
|
17
|
+
// standalone rows, as it always has; an epic's children were never capped.
|
|
18
|
+
|
|
19
|
+
// Every child the classification placed, in the order it placed them: the ranked candidates first,
|
|
20
|
+
// then the waiting ones, then the ones waiting on a person.
|
|
21
|
+
function everything_in(result: EpicNextResult): ReadonlyArray<EpicChild> {
|
|
22
|
+
const candidates = result.candidates.flatMap((bundle) => bundle.children)
|
|
23
|
+
|
|
24
|
+
return [...candidates, ...result.waiting, ...result.blocked_on_people]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The ranking is `epic_rank.rank`, shared with a named epic's lanes.
|
|
28
|
+
const { rank } = epic_rank
|
|
29
|
+
|
|
30
|
+
// This repository's candidates re-ranked inside the result, so `backlog:plan` prints the order the
|
|
31
|
+
// offer is decided in.
|
|
32
|
+
function rank_result(result: EpicNextResult, repo: string): EpicNextResult {
|
|
33
|
+
if (result.verdict !== 'run') return result
|
|
34
|
+
|
|
35
|
+
const pool = everything_in(result)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...result,
|
|
39
|
+
candidates: result.candidates.map((bundle) =>
|
|
40
|
+
bundle.repo === repo ? { ...bundle, children: rank(bundle.children, pool) } : bundle,
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The standalone rows past the cap, in offer order.
|
|
46
|
+
function overflow_of(
|
|
47
|
+
offered: ReadonlyArray<EpicChild>,
|
|
48
|
+
standalone: ReadonlySet<string>,
|
|
49
|
+
): ReadonlySet<string> {
|
|
50
|
+
const keys = offered.map((child) => epic_graph.key_of(child)).filter((key) => standalone.has(key))
|
|
51
|
+
|
|
52
|
+
return new Set(keys.slice(git_next_issues.DISPLAY_LIMIT))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function is_in(child: EpicChild, keys: ReadonlySet<string>): boolean {
|
|
56
|
+
return keys.has(epic_graph.key_of(child))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cap(selection: SoloSelection, standalone: ReadonlySet<string>): SoloSelection {
|
|
60
|
+
const overflow = overflow_of(selection.offered, standalone)
|
|
61
|
+
|
|
62
|
+
if (overflow.size === 0) return selection
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...selection,
|
|
66
|
+
offered: selection.offered.filter((child) => !is_in(child, overflow)),
|
|
67
|
+
withheld: [
|
|
68
|
+
...selection.offered.filter((child) => is_in(child, overflow)),
|
|
69
|
+
...selection.withheld,
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface OfferInput {
|
|
75
|
+
candidates: ReadonlyArray<EpicChild>
|
|
76
|
+
pool: ReadonlyArray<EpicChild>
|
|
77
|
+
read: BusyRead
|
|
78
|
+
repo: string
|
|
79
|
+
// The keys of the standalone rows — the ones the cap bounds.
|
|
80
|
+
standalone: ReadonlySet<string>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function select(input: OfferInput): SoloSelection {
|
|
84
|
+
const ranked = rank(input.candidates, input.pool)
|
|
85
|
+
|
|
86
|
+
return cap(epic_solo.select(ranked, input.read, input.repo), input.standalone)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// `backlog:next`'s gate: `epic_solo.gate` with this selection in place of its bare one.
|
|
90
|
+
function gate(
|
|
91
|
+
result: EpicNextResult,
|
|
92
|
+
read: BusyRead,
|
|
93
|
+
repo: string,
|
|
94
|
+
standalone: ReadonlySet<string>,
|
|
95
|
+
): SoloGate {
|
|
96
|
+
const pool = everything_in(result)
|
|
97
|
+
|
|
98
|
+
return epic_solo.gate(result, read, repo, (candidates) =>
|
|
99
|
+
select({ candidates, pool, read, repo, standalone }),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const backlog_rank = { everything_in, gate, rank, rank_result, select }
|
|
104
|
+
|
|
105
|
+
export { backlog_rank }
|
|
106
|
+
export type { OfferInput }
|
|
@@ -4,9 +4,9 @@ import { epic_cross_repo } from '#scripts/epic/epic-cross-repo'
|
|
|
4
4
|
import { epic_graph, type EpicChild } from '#scripts/epic/epic-graph'
|
|
5
5
|
import { epic_outside_blocker } from '#scripts/epic/epic-outside-blocker'
|
|
6
6
|
import { epic_report, type EpicNextResult } from '#scripts/epic/epic-report'
|
|
7
|
-
import { epic_solo } from '#scripts/epic/epic-solo'
|
|
8
7
|
import { has_label_name, IN_PROGRESS_LABEL, NEEDS_DECISION_LABEL } from '#scripts/git/issue-labels'
|
|
9
8
|
import { backlog_plan, type PlanContext } from './backlog-plan'
|
|
9
|
+
import { backlog_rank } from './backlog-rank'
|
|
10
10
|
|
|
11
11
|
// The order a `backlogrun` takes, wave by wave, before it starts (joshuafolkken/kit#2778).
|
|
12
12
|
//
|
|
@@ -14,13 +14,13 @@ import { backlog_plan, type PlanContext } from './backlog-plan'
|
|
|
14
14
|
// made from, so the order past the first answer was left for a person to work out by hand. This plays
|
|
15
15
|
// the asks forward under one assumption: **every issue of a wave merges before the next wave starts**.
|
|
16
16
|
//
|
|
17
|
-
// **No rule is written twice.** The first wave is `
|
|
18
|
-
// `backlog:next` resolves, asked of an idle repository — so it is what
|
|
19
|
-
// nothing is running. Each later wave marks the earlier waves closed and re-classifies the same pool
|
|
17
|
+
// **No rule is written twice.** The first wave is `backlog_rank.select` — rank, `run:solo` gate, cap —
|
|
18
|
+
// over the very candidates `backlog:next` resolves, asked of an idle repository — so it is what
|
|
19
|
+
// `backlog:next` prints when nothing is running (joshuafolkken/kit#2928). Each later wave marks the earlier waves closed and re-classifies the same pool
|
|
20
20
|
// through `epic_classify.classify_children` with the resolver `epic:next` sorted it with the first
|
|
21
21
|
// time — `epic_cross_repo.resolve_cross_repo`, so a blocker in another repository still waits for its
|
|
22
22
|
// release rather than counting as done on close — then puts the runnable set through
|
|
23
|
-
// `
|
|
23
|
+
// `backlog_rank.select` again.
|
|
24
24
|
|
|
25
25
|
const IDLE: BusyRead = { kind: 'idle' }
|
|
26
26
|
const CLOSED = 'CLOSED'
|
|
@@ -45,14 +45,6 @@ function is_running(child: EpicChild): boolean {
|
|
|
45
45
|
return has_label_name(child.labels, IN_PROGRESS_LABEL)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// Every child the classification placed, in the order it placed them: the ranked candidates first,
|
|
49
|
-
// then the waiting ones, then the ones waiting on a person.
|
|
50
|
-
function everything_in(result: EpicNextResult): ReadonlyArray<EpicChild> {
|
|
51
|
-
const candidates = result.candidates.flatMap((bundle) => bundle.children)
|
|
52
|
-
|
|
53
|
-
return [...candidates, ...result.waiting, ...result.blocked_on_people]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
48
|
function as_closed(child: EpicChild, done: ReadonlySet<string>): EpicChild {
|
|
57
49
|
return done.has(epic_graph.key_of(child)) ? { ...child, state: CLOSED } : child
|
|
58
50
|
}
|
|
@@ -63,24 +55,35 @@ interface WavePool {
|
|
|
63
55
|
children: ReadonlyArray<EpicChild>
|
|
64
56
|
running: ReadonlySet<string>
|
|
65
57
|
repo: string
|
|
58
|
+
standalone: ReadonlySet<string>
|
|
66
59
|
}
|
|
67
60
|
|
|
68
|
-
function pool_of(result: EpicNextResult, repo: string): WavePool {
|
|
69
|
-
const all = everything_in(result)
|
|
61
|
+
function pool_of(result: EpicNextResult, repo: string, standalone: ReadonlySet<string>): WavePool {
|
|
62
|
+
const all = backlog_rank.everything_in(result)
|
|
70
63
|
|
|
71
64
|
return {
|
|
72
65
|
children: all.filter((child) => !is_running(child)),
|
|
73
66
|
running: epic_outside_blocker.running_keys(all),
|
|
74
67
|
repo,
|
|
68
|
+
standalone,
|
|
75
69
|
}
|
|
76
70
|
}
|
|
77
71
|
|
|
78
|
-
function offer(
|
|
79
|
-
|
|
72
|
+
function offer(
|
|
73
|
+
candidates: ReadonlyArray<EpicChild>,
|
|
74
|
+
children: ReadonlyArray<EpicChild>,
|
|
75
|
+
pool: WavePool,
|
|
76
|
+
): ReadonlyArray<EpicChild> {
|
|
77
|
+
const { repo, standalone } = pool
|
|
78
|
+
|
|
79
|
+
return backlog_rank.select({ candidates, pool: children, read: IDLE, repo, standalone }).offered
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// Ranked against the whole result, exactly as `backlog:next` ranks it, so the two agree.
|
|
83
|
+
function first_wave(result: EpicNextResult, pool: WavePool): ReadonlyArray<EpicChild> {
|
|
84
|
+
const candidates = epic_report.candidates_for_repo(result, pool.repo)
|
|
85
|
+
|
|
86
|
+
return offer(candidates, backlog_rank.everything_in(result), pool)
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
function next_wave(pool: WavePool, done: ReadonlySet<string>): ReadonlyArray<EpicChild> {
|
|
@@ -93,25 +96,31 @@ function next_wave(pool: WavePool, done: ReadonlySet<string>): ReadonlyArray<Epi
|
|
|
93
96
|
|
|
94
97
|
return offer(
|
|
95
98
|
runnable.filter((child) => child.repo === pool.repo),
|
|
96
|
-
|
|
99
|
+
children,
|
|
100
|
+
pool,
|
|
97
101
|
)
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
// Each wave is closed before the next is asked for; a wave that offers nothing ends the plan, which
|
|
101
|
-
// the pool's size bounds because every wave closes at least one child.
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
// the pool's size bounds because every wave closes at least one child. `standalone` names the rows the
|
|
106
|
+
// offer's cap bounds; left empty, no wave is capped.
|
|
107
|
+
function build(
|
|
108
|
+
result: EpicNextResult,
|
|
109
|
+
repo: string,
|
|
110
|
+
standalone: ReadonlySet<string> = new Set(),
|
|
111
|
+
): WavePlan {
|
|
112
|
+
const pool = pool_of(result, repo, standalone)
|
|
104
113
|
const done = new Set<string>()
|
|
105
114
|
const waves: Array<ReadonlyArray<EpicChild>> = []
|
|
106
115
|
|
|
107
|
-
for (let wave = first_wave(result,
|
|
116
|
+
for (let wave = first_wave(result, pool); wave.length > 0; wave = next_wave(pool, done)) {
|
|
108
117
|
waves.push(wave)
|
|
109
118
|
for (const child of wave) done.add(epic_graph.key_of(child))
|
|
110
119
|
}
|
|
111
120
|
|
|
112
|
-
const unreached =
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
const unreached = backlog_rank
|
|
122
|
+
.everything_in(result)
|
|
123
|
+
.filter((child) => child.repo === repo && !done.has(epic_graph.key_of(child)))
|
|
115
124
|
|
|
116
125
|
return { waves, unreached }
|
|
117
126
|
}
|
|
@@ -139,10 +148,14 @@ function unreached_lines(plan: WavePlan, context: PlanContext): Array<string> {
|
|
|
139
148
|
)
|
|
140
149
|
}
|
|
141
150
|
|
|
142
|
-
function format_waves(
|
|
151
|
+
function format_waves(
|
|
152
|
+
result: EpicNextResult,
|
|
153
|
+
context: PlanContext,
|
|
154
|
+
standalone: ReadonlySet<string> = new Set(),
|
|
155
|
+
): string {
|
|
143
156
|
if (result.verdict === 'error') return backlog_plan.format_unusable(result)
|
|
144
157
|
|
|
145
|
-
const plan = build(result, context.repo)
|
|
158
|
+
const plan = build(result, context.repo, standalone)
|
|
146
159
|
const waves = plan.waves.map((wave, index) => wave_line(wave, index, context.repo))
|
|
147
160
|
|
|
148
161
|
return [
|
|
@@ -20,6 +20,11 @@ const OUT_DIR = path.join(PACKAGE_DIR, 'dist', 'hooks')
|
|
|
20
20
|
// splitting keeps every entry in its own file with its own `import.meta.url`, so an imported guard's
|
|
21
21
|
// `import.meta.url` never equals `argv[1]` and only the file actually launched runs its main.
|
|
22
22
|
//
|
|
23
|
+
// **The flip side: an entry another entry imports loses its main** (joshuafolkken/kit#2922). Splitting
|
|
24
|
+
// moves a module two entries share into a chunk, so a launched hook whose own module is imported
|
|
25
|
+
// elsewhere becomes a re-export stub that runs nothing. Each launched hook is therefore a `*-cli.ts`
|
|
26
|
+
// file holding only the self-invoke, which no other module imports.
|
|
27
|
+
//
|
|
23
28
|
// `packages: 'external'` keeps bare dependencies as runtime imports the consumer resolves from kit's
|
|
24
29
|
// node_modules, exactly as the library bundles do, while kit's internal `#scripts/*` graph is inlined.
|
|
25
30
|
// No shebang banner is added: unlike `dist/josh.js` (the `bin.josh` entry, executed directly), a hook
|
|
@@ -35,9 +40,9 @@ interface HookBundle {
|
|
|
35
40
|
// `delivered-rules` (the fourth guard) has no self-invoke, so it stays inlined.
|
|
36
41
|
const HOOK_BUNDLES: ReadonlyArray<HookBundle> = [
|
|
37
42
|
{ source: 'scripts/hooks/codex-hook-adapter.ts', out: 'codex-hook-adapter' },
|
|
38
|
-
{ source: 'scripts/hooks/pretool-guard.ts', out: 'pretool-guard' },
|
|
43
|
+
{ source: 'scripts/hooks/pretool-guard-cli.ts', out: 'pretool-guard' },
|
|
39
44
|
{ source: 'scripts/hooks/stop-guard.ts', out: 'stop-guard' },
|
|
40
|
-
{ source: 'scripts/hooks/format-edited-
|
|
45
|
+
{ source: 'scripts/hooks/format-edited-cli.ts', out: 'format-edited' },
|
|
41
46
|
{ source: 'scripts/josh/session-language-cli.ts', out: 'session-lang' },
|
|
42
47
|
{ source: 'scripts/hooks/batch-guard.ts', out: 'batch-guard' },
|
|
43
48
|
{ source: 'scripts/delegation/investigation-guard.ts', out: 'investigation-guard' },
|
|
@@ -92,6 +92,17 @@ const DELEGATABLE_STEPS: ReadonlyArray<DelegatableStep> = [
|
|
|
92
92
|
verifier:
|
|
93
93
|
'the parent opens the cited lines; a conclusion those lines do not support fails there, and reading a handful of cited regions costs far less than redoing the reading. The unit reports what the code does and never what to change — deciding that is `diagnosis`, which stays kept, so a unit returning a root cause would be delegating the rejected row under this name',
|
|
94
94
|
},
|
|
95
|
+
{
|
|
96
|
+
name: 'lane-failure-investigation',
|
|
97
|
+
// The `backlogrun` parent's counterpart of `investigation` (joshuafolkken/kit#2947). The parent
|
|
98
|
+
// orchestrates and never implements, yet it read a failed lane's logs, records and sources in its
|
|
99
|
+
// own context — about 150 grep / cat / sed calls on 2026-10-02 — and every later request of the
|
|
100
|
+
// batch re-read them. The verifier is `investigation`'s, and so is the boundary: the unit reports
|
|
101
|
+
// what happened, and what the parent does about it (park, re-dispatch, file) stays its own call.
|
|
102
|
+
does: "find out why a lane's child stopped, failed or conflicted — its output log, run records, PR checks and the files they name — and return the conclusion plus the `file:line` or log-line citations that support it, never the text read, so the batch parent's context stays the size its orchestration needs",
|
|
103
|
+
verifier:
|
|
104
|
+
'the parent opens the cited lines and re-reads the child with `pnpm josh issue:state`; a conclusion those do not support fails there. The unit reports what happened and never what to do — parking, re-dispatching or filing stays with the parent, so a wrong account is caught before it is acted on',
|
|
105
|
+
},
|
|
95
106
|
]
|
|
96
107
|
|
|
97
108
|
// Steps that were considered and are **not** delegatable, kept as a list rather than dropped: the
|
|
@@ -73,7 +73,7 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
|
|
|
73
73
|
{ path: '.claude/skills/workflow-commands/issue-comments.md', bytes: 8192 },
|
|
74
74
|
{ path: '.claude/skills/workflow-commands/issue-fold-existing.md', bytes: 4096 },
|
|
75
75
|
{ path: '.claude/skills/workflow-commands/needs-human-review.md', bytes: 8192 },
|
|
76
|
-
{ path: '.claude/skills/workflow-commands/observation-filing.md', bytes:
|
|
76
|
+
{ path: '.claude/skills/workflow-commands/observation-filing.md', bytes: 24_576 },
|
|
77
77
|
// `pre-gate-cut.md` left this per-document budget in joshuafolkken/kit#2289: it became a point-of-use
|
|
78
78
|
// document, so its bytes are now held by every entry's total read (`entry-read-budget.ts`) and the
|
|
79
79
|
// two budgets must not hold it twice — the reachability line moved it from `unreached` to
|
|
@@ -81,7 +81,7 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
|
|
|
81
81
|
{ path: '.claude/skills/workflow-commands/prerequisite.md', bytes: 8192 },
|
|
82
82
|
{ path: '.claude/skills/workflow-commands/target-repository.md', bytes: 8192 },
|
|
83
83
|
{ path: '.claude/skills/workflow-commands/working-tree-hold.md', bytes: 8192 },
|
|
84
|
-
{ path: 'docs/josh-commands.md', bytes:
|
|
84
|
+
{ path: 'docs/josh-commands.md', bytes: 184_320 },
|
|
85
85
|
{ path: 'prompts/coding-standards.md', bytes: 8192 },
|
|
86
86
|
{ path: 'prompts/collaboration-workflow.md', bytes: 8192 },
|
|
87
87
|
{ path: 'prompts/collaboration-workflow/cross-repo-epic.md', bytes: 4096 },
|
|
@@ -100,11 +100,11 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
|
|
|
100
100
|
{ path: 'prompts/collaboration-workflow/proposal-request.md', bytes: 4096 },
|
|
101
101
|
{ path: 'prompts/collaboration-workflow/report-format.md', bytes: 28_672 },
|
|
102
102
|
{ path: 'prompts/collaboration-workflow/residency.md', bytes: 28_672 },
|
|
103
|
-
{ path: 'prompts/collaboration-workflow/rule-delivery.md', bytes:
|
|
103
|
+
{ path: 'prompts/collaboration-workflow/rule-delivery.md', bytes: 45_056 },
|
|
104
104
|
{ path: 'prompts/collaboration-workflow/shell-body.md', bytes: 12_288 },
|
|
105
|
-
{ path: 'prompts/collaboration-workflow/turn-batching.md', bytes:
|
|
105
|
+
{ path: 'prompts/collaboration-workflow/turn-batching.md', bytes: 20_480 },
|
|
106
106
|
{ path: 'prompts/collaboration-workflow/upstream-interrupt.md', bytes: 12_288 },
|
|
107
|
-
{ path: 'prompts/collaboration-workflow/wip-cap.md', bytes:
|
|
107
|
+
{ path: 'prompts/collaboration-workflow/wip-cap.md', bytes: 16_384 },
|
|
108
108
|
{ path: 'prompts/refactoring.md', bytes: 8192 },
|
|
109
109
|
{ path: 'prompts/review-rubric.md', bytes: 20_480 },
|
|
110
110
|
{ path: 'prompts/review.md', bytes: 20_480 },
|
|
@@ -77,11 +77,12 @@ const ENTRY_READ_BUDGET: ReadonlyArray<EntryBudget> = [
|
|
|
77
77
|
// planning entries): kickoff has no headroom, so the addition was offset by tightening that file's
|
|
78
78
|
// heartbeat prose, keeping every entry within its existing block.
|
|
79
79
|
// joshuafolkken/kit#2762 moved the rationale of five point-of-use documents to
|
|
80
|
-
// `docs/maintainers/*-rationale.md`, and the ratchet holds the reduction.
|
|
80
|
+
// `docs/maintainers/*-rationale.md`, and the ratchet holds the reduction. joshuafolkken/kit#2895 did
|
|
81
|
+
// the same for eight more, lowering fullrun, halfrun and backlogrun one block each.
|
|
81
82
|
{ entry: 'kickoff', bytes: 229_376 },
|
|
82
|
-
{ entry: 'fullrun', bytes:
|
|
83
|
-
{ entry: 'halfrun', bytes:
|
|
84
|
-
{ entry: 'backlogrun', bytes:
|
|
83
|
+
{ entry: 'fullrun', bytes: 225_280 },
|
|
84
|
+
{ entry: 'halfrun', bytes: 225_280 },
|
|
85
|
+
{ entry: 'backlogrun', bytes: 229_376 },
|
|
85
86
|
{ entry: LANE_CHILD, bytes: 77_824 },
|
|
86
87
|
]
|
|
87
88
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type RepoAnswer,
|
|
7
7
|
} from './epic-candidate-confirm'
|
|
8
8
|
import { epic_graph, type EpicChild } from './epic-graph'
|
|
9
|
+
import { epic_rank } from './epic-rank'
|
|
9
10
|
import type { EpicVerdict } from './epic-report'
|
|
10
11
|
import { epic_solo } from './epic-solo'
|
|
11
12
|
import { epic_triage, type TriageVerdict } from './epic-triage'
|
|
@@ -107,6 +108,15 @@ function candidates_in(pool: RepoPool, repo: string): RepoPool {
|
|
|
107
108
|
return { ...pool, candidates: pool.candidates.filter((child) => child.repo === repo) }
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
// A pool's candidates for the repository in `epic_rank.rank` order — the ranking `backlog:next` offers
|
|
112
|
+
// in (joshuafolkken/kit#2928), counted against the pool's own graph. **Within a pool, not across
|
|
113
|
+
// pools**: the pool order is the order a person named the epics in, and that stays the outer key.
|
|
114
|
+
function ranked_in(pool: RepoPool, repo: string): RepoPool {
|
|
115
|
+
const scoped = candidates_in(pool, repo)
|
|
116
|
+
|
|
117
|
+
return { ...scoped, candidates: epic_rank.rank(scoped.candidates, pool.context.children) }
|
|
118
|
+
}
|
|
119
|
+
|
|
110
120
|
// One pool's candidates, minus every one an earlier pool already carries. `seen` is mutated rather
|
|
111
121
|
// than rebuilt because the earlier pools' claim is what the later one is filtered against.
|
|
112
122
|
function unseen_candidates(pool: RepoPool, seen: Set<string>): RepoPool {
|
|
@@ -184,24 +194,6 @@ function solo_offer(
|
|
|
184
194
|
return { ...answer, children: offered, notice: offered_notice(offered, read, request) }
|
|
185
195
|
}
|
|
186
196
|
|
|
187
|
-
// The confirmation walk stops at the free-lane count, so a `run:solo` candidate ranked past it — or
|
|
188
|
-
// in a pool past it — would never reach `solo_offer` to be preferred. Into an idle repository the
|
|
189
|
-
// first pool holding one is asked first, with it at its head through the same `epic_solo.prefer` the
|
|
190
|
-
// offer applies (joshuafolkken/kit#2778). The candidate stays in its own pool, because a pool carries
|
|
191
|
-
// the graph it is confirmed against.
|
|
192
|
-
function solo_first(pools: ReadonlyArray<RepoPool>, read: BusyRead): ReadonlyArray<RepoPool> {
|
|
193
|
-
if (read.kind !== 'idle') return pools
|
|
194
|
-
|
|
195
|
-
const index = pools.findIndex((pool) => pool.candidates.some((child) => epic_solo.is_solo(child)))
|
|
196
|
-
const pool = pools[index]
|
|
197
|
-
|
|
198
|
-
if (pool === undefined) return pools
|
|
199
|
-
|
|
200
|
-
const lifted = { ...pool, candidates: epic_solo.prefer(pool.candidates, read) }
|
|
201
|
-
|
|
202
|
-
return [lifted, ...pools.filter((other) => other !== pool)]
|
|
203
|
-
}
|
|
204
|
-
|
|
205
197
|
// The repository is asked how full it is **before** any candidate is confirmed, for the reason
|
|
206
198
|
// joshuafolkken/kit#1121 records: a repository with no free lane is handed nothing, so the relations
|
|
207
199
|
// request that would confirm a candidate there buys an answer nobody reads — and a polling `epicrun`
|
|
@@ -247,10 +239,7 @@ async function offer_for_repo(
|
|
|
247
239
|
}
|
|
248
240
|
}
|
|
249
241
|
|
|
250
|
-
const for_repo =
|
|
251
|
-
dedupe_pools(pools.map((pool) => candidates_in(pool, request.repo))),
|
|
252
|
-
read,
|
|
253
|
-
)
|
|
242
|
+
const for_repo = dedupe_pools(pools.map((pool) => ranked_in(pool, request.repo)))
|
|
254
243
|
|
|
255
244
|
return solo_offer(await collect(for_repo, wanted_of(request, free)), read, request)
|
|
256
245
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { issue_rank } from '#scripts/git/issue-rank'
|
|
2
|
+
import { epic_graph, type EpicChild } from './epic-graph'
|
|
3
|
+
|
|
4
|
+
// `issue-rank.ts`'s keys over graph children (joshuafolkken/kit#2928), for both offer paths that hand
|
|
5
|
+
// out children: `backlog:next` (`backlog-rank.ts`) and a named epic's `epic:next --lanes`
|
|
6
|
+
// (`epic-lane-offer.ts`). One ranking, so the two cannot disagree about which child goes first.
|
|
7
|
+
|
|
8
|
+
const CLOSED = 'CLOSED'
|
|
9
|
+
|
|
10
|
+
// How many open issues of the pool wait on each child. Only the pool is counted: it is every issue the
|
|
11
|
+
// caller may run, and an issue outside it is not one this ranking can start sooner.
|
|
12
|
+
function dependents_of(pool: ReadonlyArray<EpicChild>): ReadonlyMap<string, number> {
|
|
13
|
+
return issue_rank.count_dependents(
|
|
14
|
+
pool
|
|
15
|
+
.filter((child) => child.state !== CLOSED)
|
|
16
|
+
.map((child) => ({
|
|
17
|
+
key: epic_graph.key_of(child),
|
|
18
|
+
blockers: child.blocked_by.map((blocker) => epic_graph.blocker_key(blocker)),
|
|
19
|
+
})),
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function rank(
|
|
24
|
+
children: ReadonlyArray<EpicChild>,
|
|
25
|
+
pool: ReadonlyArray<EpicChild>,
|
|
26
|
+
): Array<EpicChild> {
|
|
27
|
+
const dependents = dependents_of(pool)
|
|
28
|
+
|
|
29
|
+
return issue_rank.rank(children, (child) => ({
|
|
30
|
+
labels: child.labels,
|
|
31
|
+
dependents: dependents.get(epic_graph.key_of(child)) ?? 0,
|
|
32
|
+
}))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const epic_rank = { dependents_of, rank }
|
|
36
|
+
|
|
37
|
+
export { epic_rank }
|
|
@@ -21,12 +21,11 @@ import { epic_report, type EpicNextResult } from './epic-report'
|
|
|
21
21
|
// it, so it is not starved by the lanes it is waiting on. A listing that could not be read is not an
|
|
22
22
|
// idle repository — the same fail-safe answer `epic_busy` gives.
|
|
23
23
|
//
|
|
24
|
-
// **
|
|
25
|
-
//
|
|
26
|
-
// path
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// the ones ahead of it keep their lanes.
|
|
24
|
+
// **The label does not move a candidate up the ranking** (joshuafolkken/kit#2928, reversing
|
|
25
|
+
// joshuafolkken/kit#2778). `run:solo` means "runs alone", and tidying that merely touches the
|
|
26
|
+
// verification path carries it as well as a defect that breaks it. Which of them goes first is the
|
|
27
|
+
// ranking's question — `issue-rank.ts` puts a verification-path defect at the head — so an idle
|
|
28
|
+
// repository takes the candidates in rank order like any other.
|
|
30
29
|
|
|
31
30
|
const SOLO_LABELS: ReadonlySet<string> = new Set([RUN_SOLO_LABEL])
|
|
32
31
|
|
|
@@ -75,32 +74,21 @@ function none_of(children: ReadonlyArray<EpicChild>, notice: string): SoloSelect
|
|
|
75
74
|
return { offered: [], withheld: [...children], notice }
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
// The ranking with the first `run:solo` candidate moved to the head — but only into an idle
|
|
79
|
-
// repository, the one place it may start. Anywhere else the ranking is returned as it was.
|
|
80
|
-
function prefer(children: ReadonlyArray<EpicChild>, read: BusyRead): ReadonlyArray<EpicChild> {
|
|
81
|
-
const solo = children.find((child) => is_solo(child))
|
|
82
|
-
|
|
83
|
-
if (solo === undefined || read.kind !== 'idle') return children
|
|
84
|
-
|
|
85
|
-
return [solo, ...children.filter((child) => child !== solo)]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
77
|
// Where the first `run:solo` candidate sits decides what starts: everything when there is none, the
|
|
89
|
-
// candidates ahead of it when
|
|
78
|
+
// candidates ahead of it when it is not first, and it alone once it heads an idle repository's ranking.
|
|
90
79
|
function select_candidates(
|
|
91
80
|
children: ReadonlyArray<EpicChild>,
|
|
92
81
|
read: BusyRead,
|
|
93
82
|
repo: string,
|
|
94
83
|
): SoloSelection {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const solo = ordered[index]
|
|
84
|
+
const index = children.findIndex((child) => is_solo(child))
|
|
85
|
+
const solo = children[index]
|
|
98
86
|
|
|
99
|
-
if (solo === undefined) return first_of(
|
|
100
|
-
if (index > 0) return first_of(
|
|
101
|
-
if (read.kind === 'idle') return first_of(
|
|
87
|
+
if (solo === undefined) return first_of(children, children.length)
|
|
88
|
+
if (index > 0) return first_of(children, index)
|
|
89
|
+
if (read.kind === 'idle') return first_of(children, 1)
|
|
102
90
|
|
|
103
|
-
return none_of(
|
|
91
|
+
return none_of(children, waiting_message(solo.number, repo))
|
|
104
92
|
}
|
|
105
93
|
|
|
106
94
|
// An unreadable or truncated listing keeps nothing: without it a running `run:solo` issue cannot be
|
|
@@ -129,11 +117,24 @@ function withhold(result: EpicNextResult, repo: string, selection: SoloSelection
|
|
|
129
117
|
}
|
|
130
118
|
}
|
|
131
119
|
|
|
120
|
+
// What decides the selection. `select` by default; `backlog:next` passes one that ranks before it and
|
|
121
|
+
// caps after it (`backlog-rank.ts`), so the gate itself is written once.
|
|
122
|
+
type SoloSelector = (
|
|
123
|
+
children: ReadonlyArray<EpicChild>,
|
|
124
|
+
read: BusyRead,
|
|
125
|
+
repo: string,
|
|
126
|
+
) => SoloSelection
|
|
127
|
+
|
|
132
128
|
// Only a `run` answer offers anything, so every other verdict passes through untouched.
|
|
133
|
-
function gate(
|
|
129
|
+
function gate(
|
|
130
|
+
result: EpicNextResult,
|
|
131
|
+
read: BusyRead,
|
|
132
|
+
repo: string,
|
|
133
|
+
selector: SoloSelector = select,
|
|
134
|
+
): SoloGate {
|
|
134
135
|
if (result.verdict !== 'run') return { result }
|
|
135
136
|
|
|
136
|
-
const selection =
|
|
137
|
+
const selection = selector(epic_report.candidates_for_repo(result, repo), read, repo)
|
|
137
138
|
const gated = withhold(result, repo, selection)
|
|
138
139
|
|
|
139
140
|
return selection.notice === undefined
|
|
@@ -141,7 +142,7 @@ function gate(result: EpicNextResult, read: BusyRead, repo: string): SoloGate {
|
|
|
141
142
|
: { result: gated, notice: selection.notice }
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
const epic_solo = { gate, is_solo,
|
|
145
|
+
const epic_solo = { gate, is_solo, select }
|
|
145
146
|
|
|
146
|
-
export type { SoloGate, SoloSelection }
|
|
147
|
+
export type { SoloGate, SoloSelection, SoloSelector }
|
|
147
148
|
export { epic_solo }
|
|
@@ -418,7 +418,7 @@ async function push(): Promise<void> {
|
|
|
418
418
|
// has diverged is a state to fail loudly on rather than to grow a merge commit over. The name says
|
|
419
419
|
// which of the two this is, as `merge_fast_forward` does beside `merge_branch`.
|
|
420
420
|
async function pull_fast_forward(): Promise<void> {
|
|
421
|
-
await git_spawn.
|
|
421
|
+
await git_spawn.with_output_remote('pull', ['--ff-only'])
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
// Every local branch matching a `git branch --list` pattern, one name per line. The boolean below is
|
|
@@ -22,6 +22,11 @@ function has_timed_out(error: unknown): boolean {
|
|
|
22
22
|
return error.timedOut === true
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// A timeout has no exit code — the process was killed rather than allowed to exit — so `cause` says
|
|
26
|
+
// so in the slot every other git failure puts a number in. Shared by every git spawn that carries a
|
|
27
|
+
// budget: the push (`git-push-transport.ts`) and the fetch and pull (`git-spawn.ts`).
|
|
28
|
+
const TIMEOUT_EXIT_CODE = 'timeout'
|
|
29
|
+
|
|
25
30
|
// The error a spawned git command fails with. The exit code is carried on `cause` as well as in the
|
|
26
31
|
// message, because callers branch on it rather than parse it: `git_command.push` reads 128 there to
|
|
27
32
|
// decide whether to retry with `--set-upstream`.
|
|
@@ -32,4 +37,4 @@ function create_spawn_error(command: string, exit_code: number | undefined): Err
|
|
|
32
37
|
return new Error(error_message, { cause: { exit_code: exit_code_string } })
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
export { create_spawn_error, get_exit_code, has_timed_out }
|
|
40
|
+
export { create_spawn_error, get_exit_code, has_timed_out, TIMEOUT_EXIT_CODE }
|