@joshuafolkken/kit 1.1383.0 → 1.1435.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
- package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
- package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
- package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
- package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +262 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +124 -15
- package/dist/hooks/stop-guard.js +98 -55
- package/dist/josh.js +63 -10
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-cli.ts +2 -4
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +5 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-owner.ts +72 -5
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +80 -5
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +30 -4
- package/scripts/init/start-steps.ts +77 -3
- package/scripts/init/start.ts +21 -13
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +25 -1
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +139 -37
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +58 -5
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +121 -0
- package/scripts/run/board/run-board-span.ts +49 -7
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +76 -13
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +53 -16
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-added.ts +1 -1
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-change.ts +1 -1
- package/scripts/run/carry/run-carry-cli.ts +39 -52
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry-ended.ts +9 -1
- package/scripts/run/carry/run-carry.ts +24 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +3 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +91 -0
- package/scripts/run/event/run-event-stream.ts +46 -9
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/run-stranded-detect.ts +58 -12
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +108 -74
- package/scripts/run/wake/run-wake-describe.ts +36 -2
- package/scripts/run/wake/run-wake-driver.ts +21 -5
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs'
|
|
3
11
|
import path from 'node:path'
|
|
4
12
|
import { hook_launch } from '#scripts/init/hook-launch'
|
|
5
13
|
import {
|
|
@@ -30,6 +38,8 @@ import {
|
|
|
30
38
|
// checkout never produced is skipped, and a copy that fails is skipped rather than thrown — so
|
|
31
39
|
// seeding can never be the reason an open fails, unlike the `.env` write the lane cannot run without.
|
|
32
40
|
|
|
41
|
+
const STAGING_SEGMENTS = ['node_modules', '.cache', 'josh-lane-cache'] as const
|
|
42
|
+
|
|
33
43
|
function replace_root(value: unknown, source_prefix: string, destination_root: string): unknown {
|
|
34
44
|
if (typeof value !== 'string') return value
|
|
35
45
|
|
|
@@ -61,19 +71,39 @@ function portable_content(
|
|
|
61
71
|
return content
|
|
62
72
|
}
|
|
63
73
|
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
// The staging file lives under `node_modules/.cache`, never beside the cache it replaces: prettier
|
|
75
|
+
// runs in parallel with the tool whose cache is being copied, and a temporary name in the checkout
|
|
76
|
+
// root is listed by its tree walk and gone by the time it is read (#3763). `node_modules` is skipped
|
|
77
|
+
// by every tree scan and sits on the checkout's own filesystem, so the rename stays atomic.
|
|
78
|
+
function stage_path(destination_root: string, cache_file: string): string {
|
|
79
|
+
const staging_directory = path.join(destination_root, ...STAGING_SEGMENTS)
|
|
80
|
+
|
|
81
|
+
mkdirSync(staging_directory, { recursive: true })
|
|
82
|
+
|
|
83
|
+
return path.join(staging_directory, `${cache_file}.${String(process.pid)}.${randomUUID()}.tmp`)
|
|
84
|
+
}
|
|
66
85
|
|
|
86
|
+
function publish_staged(temporary_path: string, destination_path: string, content: string): void {
|
|
67
87
|
try {
|
|
68
88
|
writeFileSync(temporary_path, content, { flag: 'wx' })
|
|
69
89
|
renameSync(temporary_path, destination_path)
|
|
70
|
-
} catch {
|
|
71
|
-
// Best-effort: a cache that could not be published simply leaves the destination unchanged.
|
|
72
90
|
} finally {
|
|
73
91
|
rmSync(temporary_path, { force: true })
|
|
74
92
|
}
|
|
75
93
|
}
|
|
76
94
|
|
|
95
|
+
function write_atomically(destination_root: string, cache_file: string, content: string): void {
|
|
96
|
+
if (!existsSync(destination_root)) return
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const temporary_path = stage_path(destination_root, cache_file)
|
|
100
|
+
|
|
101
|
+
publish_staged(temporary_path, path.join(destination_root, cache_file), content)
|
|
102
|
+
} catch {
|
|
103
|
+
// Best-effort: a cache that could not be published simply leaves the destination unchanged.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
77
107
|
function copy_if_present(source_root: string, destination: string, spec: GateCacheSpec): void {
|
|
78
108
|
const source_path = path.join(source_root, spec.cache_file)
|
|
79
109
|
if (!existsSync(source_path)) return
|
|
@@ -86,7 +116,7 @@ function copy_if_present(source_root: string, destination: string, spec: GateCac
|
|
|
86
116
|
destination,
|
|
87
117
|
)
|
|
88
118
|
|
|
89
|
-
write_atomically(
|
|
119
|
+
write_atomically(destination, spec.cache_file, content)
|
|
90
120
|
} catch {
|
|
91
121
|
// A missing, changing, or malformed cache is a miss, never a failed lane or gate.
|
|
92
122
|
}
|
|
@@ -130,6 +160,6 @@ function sync_cache(source_root: string, destination: string, cache_file: string
|
|
|
130
160
|
copy_if_present(source_root, destination, spec)
|
|
131
161
|
}
|
|
132
162
|
|
|
133
|
-
const lane_cache = { seed_caches, seed_hook_bundles, sync_cache }
|
|
163
|
+
const lane_cache = { STAGING_SEGMENTS, seed_caches, seed_hook_bundles, sync_cache }
|
|
134
164
|
|
|
135
165
|
export { lane_cache }
|
|
@@ -10,14 +10,16 @@ import { lane_seed_policy } from './lane-seed'
|
|
|
10
10
|
// is what would have to be undone the first time two epics run together, which is why the scheduler
|
|
11
11
|
// is epic-free.
|
|
12
12
|
//
|
|
13
|
-
// **It is a ceiling, not a prediction.**
|
|
14
|
-
// says a
|
|
13
|
+
// **It is a ceiling, not a prediction.** Four lanes does not say four children will run at once; it
|
|
14
|
+
// says a fifth will not start. What actually runs is bounded by what is runnable, by the port
|
|
15
15
|
// seats `lane-seed.ts` can allocate, and by the machine.
|
|
16
16
|
|
|
17
|
-
//
|
|
17
|
+
// Four rather than the nine seats `lane_seed_policy` can allocate: the seats are the hard structural
|
|
18
18
|
// bound, this is the one a person tunes, and leaving headroom between them means raising the limit
|
|
19
|
-
// never lands on "the ports ran out" as its first symptom.
|
|
20
|
-
|
|
19
|
+
// never lands on "the ports ran out" as its first symptom. joshuafolkken/kit#3788 lowered it from
|
|
20
|
+
// six: a week of `lane:stats` showed 1.5 effective lanes, while the hours at five or six lanes held
|
|
21
|
+
// the swap peak, the slowest gates and most of the gate and commit failures.
|
|
22
|
+
const DEFAULT_LANE_LIMIT = 4
|
|
21
23
|
const LANE_LIMIT_KEY = 'JOSH_LANE_LIMIT'
|
|
22
24
|
const POSITIVE_INTEGER = /^[1-9]\d*$/u
|
|
23
25
|
const NO_LANES = 0
|
|
@@ -28,7 +30,7 @@ const NO_LANES = 0
|
|
|
28
30
|
type LimitChoice = { kind: 'limit'; limit: number } | { kind: 'problem'; problem: string }
|
|
29
31
|
|
|
30
32
|
// **An invalid value is a hard error, never a silent fall back to the default** — the rule
|
|
31
|
-
// `PORT_SEED` already holds to. A typo that quietly becomes
|
|
33
|
+
// `PORT_SEED` already holds to. A typo that quietly becomes 4 is a limit nobody set, and the whole
|
|
32
34
|
// point of the setting is that the number is the one a person chose.
|
|
33
35
|
//
|
|
34
36
|
// Blank is unset, not invalid: an `.env` line left as `JOSH_LANE_LIMIT=` is a variable nobody
|
package/scripts/lane/lane-cli.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { lane_output, type ReadOutcome, type RecordOutcome } from './lane-output
|
|
|
18
18
|
import { lane_registry, type LaneInfo } from './lane-registry'
|
|
19
19
|
import { lane_report } from './lane-report'
|
|
20
20
|
import { lane_seed_policy } from './lane-seed'
|
|
21
|
+
import type { KeptLane } from './lane-spent'
|
|
21
22
|
import { lane_stray } from './lane-stray'
|
|
22
23
|
|
|
23
24
|
// `josh lane:open` / `lane:close` / `lane:list` / `lane:prune` — the lane's whole lifecycle.
|
|
@@ -263,10 +264,23 @@ async function list_command(rest: ReadonlyArray<string>): Promise<number> {
|
|
|
263
264
|
return code
|
|
264
265
|
}
|
|
265
266
|
|
|
267
|
+
// A lane left open is named with its reason, so a person can tell a lane somebody is in from one that
|
|
268
|
+
// only waits on a push, a commit or an issue — and knows the command that discards it anyway.
|
|
269
|
+
function report_kept(kept: ReadonlyArray<KeptLane>): void {
|
|
270
|
+
for (const lane of kept) {
|
|
271
|
+
console.error(
|
|
272
|
+
`Kept the lane for ${session_cite.issue(lane.issue)}: ${lane.reason}. \`pnpm josh lane:close ${lane.issue}\` discards it.`,
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
266
277
|
async function prune_command(rest: ReadonlyArray<string>): Promise<number> {
|
|
267
278
|
if (rest.length > 0) return report_usage()
|
|
268
279
|
|
|
269
|
-
const
|
|
280
|
+
const outcome = await lane_close.prune_lanes()
|
|
281
|
+
const code = report_sweep(outcome)
|
|
282
|
+
|
|
283
|
+
report_kept(outcome.kept)
|
|
270
284
|
|
|
271
285
|
// After the closes, so a lane whose close left its directory behind is swept in the same run. A
|
|
272
286
|
// kept stray is reported, never a failure: it waits on a person, and a non-zero exit would stop
|
|
@@ -5,6 +5,7 @@ import { error_text } from '#scripts/lib/error-message'
|
|
|
5
5
|
import { lane_paths } from './lane-paths'
|
|
6
6
|
import { lane_reap } from './lane-reap'
|
|
7
7
|
import { lane_registry, type LaneInfo } from './lane-registry'
|
|
8
|
+
import { lane_spent, type KeptLane } from './lane-spent'
|
|
8
9
|
|
|
9
10
|
// Closing a lane: no work tree, no branch, no directory.
|
|
10
11
|
//
|
|
@@ -42,6 +43,11 @@ interface SweepOutcome {
|
|
|
42
43
|
failed: Array<string>
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
interface PruneOutcome extends SweepOutcome {
|
|
47
|
+
// The live lanes left open, each with the reason it was not discarded.
|
|
48
|
+
kept: Array<KeptLane>
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
async function ignore_failure(action: () => Promise<string>): Promise<void> {
|
|
46
52
|
try {
|
|
47
53
|
await action()
|
|
@@ -171,14 +177,17 @@ async function close_each(issues: ReadonlyArray<string>): Promise<SweepOutcome>
|
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
/**
|
|
174
|
-
* Close every lane whose work tree is no longer on disk — the route out of
|
|
175
|
-
* the directory with it and left git's registration behind
|
|
180
|
+
* Close every lane nothing needs any more: one whose work tree is no longer on disk — the route out of
|
|
181
|
+
* an interruption that took the directory with it and left git's registration behind — and one a past
|
|
182
|
+
* run left spent (`lane-spent.ts`). A live lane that is not spent is kept, and `kept` says why.
|
|
176
183
|
*/
|
|
177
|
-
async function prune_lanes(): Promise<
|
|
184
|
+
async function prune_lanes(): Promise<PruneOutcome> {
|
|
178
185
|
const lanes = await lane_registry.list_lanes()
|
|
179
|
-
const
|
|
186
|
+
const { spent, kept } = await lane_spent.sort_lanes(lanes)
|
|
187
|
+
const doomed = lanes.filter((lane) => lane.is_stranded || spent.includes(lane))
|
|
188
|
+
const sweep = await close_each(doomed.map((lane) => lane.issue))
|
|
180
189
|
|
|
181
|
-
return
|
|
190
|
+
return { ...sweep, kept }
|
|
182
191
|
}
|
|
183
192
|
|
|
184
193
|
async function close_all_lanes(): Promise<SweepOutcome> {
|
|
@@ -194,5 +203,5 @@ const lane_close = {
|
|
|
194
203
|
resolve_lane,
|
|
195
204
|
}
|
|
196
205
|
|
|
197
|
-
export type { CloseKind, CloseOutcome, ResolvedLane, SweepOutcome }
|
|
206
|
+
export type { CloseKind, CloseOutcome, PruneOutcome, ResolvedLane, SweepOutcome }
|
|
198
207
|
export { lane_close }
|
|
@@ -49,6 +49,7 @@ const WARNING_RECOVERY =
|
|
|
49
49
|
// The discriminant for a dispatch refused because the `in-progress` marker could not be applied, named
|
|
50
50
|
// once rather than inlined at each use.
|
|
51
51
|
const LABEL_UNSET_KIND = 'label-unset'
|
|
52
|
+
const RESTART_START = 'restart'
|
|
52
53
|
// The role a lane child runs under, resolved once so the fresh and resume builders read the same one.
|
|
53
54
|
const RESOLVED_ROLE = agent_role_profile.WORKER
|
|
54
55
|
|
|
@@ -281,13 +282,15 @@ function built_for(plan: ResumePlan, profile: AgentProfile, lane: LaneInfo): Age
|
|
|
281
282
|
// **The re-dispatch decides resume-or-fresh from the lane's own exit record**.
|
|
282
283
|
// The record is read from the log the previous dispatch of this lane wrote — a first dispatch finds
|
|
283
284
|
// none and starts fresh, an `outage` ending with a session id resumes it. The provider is resolved
|
|
284
|
-
// first because the resume mechanism is Claude Code's, and an OpenAI lane falls back to fresh.
|
|
285
|
-
|
|
285
|
+
// first because the resume mechanism is Claude Code's, and an OpenAI lane falls back to fresh. **A
|
|
286
|
+
// fresh dispatch never reads the record**: a person restarting a child (`lane:restart`) wants a new
|
|
287
|
+
// session that reads the Issue from the start, whatever the previous one ended on.
|
|
288
|
+
function prepared(lane: LaneInfo, is_fresh: boolean): Prepared {
|
|
286
289
|
const resolved = agent_role_profile.resolve(RESOLVED_ROLE)
|
|
287
290
|
|
|
288
291
|
if (resolved.kind === 'rejected') return resolved
|
|
289
292
|
|
|
290
|
-
const record = run_ending.read_exit(default_log_path(lane))
|
|
293
|
+
const record = is_fresh ? undefined : run_ending.read_exit(default_log_path(lane))
|
|
291
294
|
const plan = lane_resume.plan(lane.issue, record, resolved.profile.provider)
|
|
292
295
|
const built = built_for(plan, resolved.profile, lane)
|
|
293
296
|
|
|
@@ -333,8 +336,23 @@ async function record_started(issue: string): Promise<void> {
|
|
|
333
336
|
await lane_ledger.record_dispatch(Number(issue))
|
|
334
337
|
}
|
|
335
338
|
|
|
336
|
-
|
|
337
|
-
|
|
339
|
+
// How a dispatch begins. A `resumable` one claims the marker itself and resumes an `outage` session; a
|
|
340
|
+
// `restart` is a person's (`lane:restart`): its caller claimed the marker before stopping the old child,
|
|
341
|
+
// so no GitHub round-trip sits between that stop and this launch, and the session starts fresh.
|
|
342
|
+
type DispatchStart = 'resumable' | typeof RESTART_START
|
|
343
|
+
|
|
344
|
+
async function claimed(issue: string, start: DispatchStart): Promise<LabelWrite> {
|
|
345
|
+
if (start === RESTART_START) return { is_applied: true }
|
|
346
|
+
|
|
347
|
+
return await mark_in_progress(issue)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function finish_dispatch(
|
|
351
|
+
issue: string,
|
|
352
|
+
request: StartRequest,
|
|
353
|
+
start: DispatchStart,
|
|
354
|
+
): Promise<DispatchOutcome> {
|
|
355
|
+
const mark = await claimed(issue, start)
|
|
338
356
|
|
|
339
357
|
if (!mark.is_applied) return { kind: LABEL_UNSET_KIND, reason: mark.reason }
|
|
340
358
|
const outcome = await started(request)
|
|
@@ -346,14 +364,21 @@ async function finish_dispatch(issue: string, request: StartRequest): Promise<Di
|
|
|
346
364
|
return outcome
|
|
347
365
|
}
|
|
348
366
|
|
|
349
|
-
/**
|
|
350
|
-
|
|
367
|
+
/**
|
|
368
|
+
* Start `fullrun #<N>` detached in the lane for `#<N>`, recording where it writes as it does so.
|
|
369
|
+
* A `restart` start expects the marker already claimed and starts a new session even where the lane's
|
|
370
|
+
* last ending would resume one.
|
|
371
|
+
*/
|
|
372
|
+
async function dispatch_child(
|
|
373
|
+
issue: string,
|
|
374
|
+
start: DispatchStart = 'resumable',
|
|
375
|
+
): Promise<DispatchOutcome> {
|
|
351
376
|
issue_number_shape.require_issue_number(issue)
|
|
352
377
|
|
|
353
378
|
const lane = await lane_registry.find_open_lane(issue)
|
|
354
379
|
|
|
355
380
|
if (lane === undefined) return { kind: 'no-lane' }
|
|
356
|
-
const built = prepared(lane)
|
|
381
|
+
const built = prepared(lane, start === RESTART_START)
|
|
357
382
|
const log_path = default_log_path(lane)
|
|
358
383
|
|
|
359
384
|
if (built.kind === 'rejected') return { kind: 'failed', lane, log_path, note: built.note }
|
|
@@ -362,7 +387,7 @@ async function dispatch_child(issue: string): Promise<DispatchOutcome> {
|
|
|
362
387
|
|
|
363
388
|
if (log.kind !== 'ready') return log
|
|
364
389
|
|
|
365
|
-
return await finish_dispatch(issue, { lane, log_path: log.path, ...built })
|
|
390
|
+
return await finish_dispatch(issue, { lane, log_path: log.path, ...built }, start)
|
|
366
391
|
}
|
|
367
392
|
|
|
368
393
|
// **A launch whose log could not be opened is still a launch, and the reader has to hear both halves.**
|
|
@@ -438,6 +463,7 @@ async function warn_of_problem(outcome: DispatchOutcome, issue: string): Promise
|
|
|
438
463
|
|
|
439
464
|
const lane_dispatch = {
|
|
440
465
|
child_invocation,
|
|
466
|
+
claim_marker: mark_in_progress,
|
|
441
467
|
default_log_path,
|
|
442
468
|
describe,
|
|
443
469
|
dispatch_child,
|
|
@@ -446,5 +472,5 @@ const lane_dispatch = {
|
|
|
446
472
|
warn_of_problem,
|
|
447
473
|
}
|
|
448
474
|
|
|
449
|
-
export type { DispatchOutcome }
|
|
475
|
+
export type { DispatchOutcome, DispatchStart }
|
|
450
476
|
export { lane_dispatch }
|
|
@@ -7,7 +7,7 @@ import { agent_role_profile, type AgentProfile } from '#scripts/agent/agent-role
|
|
|
7
7
|
// the nearest ancestor holding a `package.json` — which, inside a linked work tree, is the lane's
|
|
8
8
|
// own directory — so a lane with no `.env` runs on seed 0 and a lane handed a verbatim copy of the
|
|
9
9
|
// root's runs on the root's exact offset; either way every lane lands on one pair of ports, and a
|
|
10
|
-
// busy port fails without retrying on another. At
|
|
10
|
+
// busy port fails without retrying on another. At six lanes that turns E2E from "fails
|
|
11
11
|
// sometimes" into "fails nearly always". So the lane's `.env` keeps the project's `PORT_SEED`
|
|
12
12
|
// unchanged and adds `JOSH_LANE_SEAT`, and `ports/index.js` combines them as `seed × 10 + seat`:
|
|
13
13
|
// the file plainly shows which number is the project's and which is the
|
|
@@ -6,6 +6,7 @@ import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
|
6
6
|
import { lane_vacant } from '#scripts/lane/lane-vacant'
|
|
7
7
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
8
8
|
import { INSTALL_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
9
|
+
import { run_event_setup } from '#scripts/run/event/run-event-setup'
|
|
9
10
|
import { execa } from 'execa'
|
|
10
11
|
|
|
11
12
|
// `josh lane:launch <issue> [--stash <message>]` — one composite command for a `backlogrun` lane-start
|
|
@@ -73,8 +74,11 @@ async function install(directory: string): Promise<boolean> {
|
|
|
73
74
|
|
|
74
75
|
// The first lane pops the shared stash and re-installs against the lock it brought in; a pop that
|
|
75
76
|
// refused (`no-match` / `ambiguous`) stops the lane rather than dispatching onto an unprepared tree.
|
|
76
|
-
// A caller without `--stash` skips both, which is every lane after the first.
|
|
77
|
-
|
|
77
|
+
// A caller without `--stash` skips both, which is every lane after the first. The re-install is a
|
|
78
|
+
// stage of its own on the run's stream, as the install `lane:open` ran was.
|
|
79
|
+
async function prepare(context: LaunchContext, directory: string): Promise<boolean> {
|
|
80
|
+
const { issue, stash } = context
|
|
81
|
+
|
|
78
82
|
if (stash === undefined) return true
|
|
79
83
|
|
|
80
84
|
const popped = await josh_command.josh_run(
|
|
@@ -84,6 +88,8 @@ async function prepare(stash: string | undefined, directory: string): Promise<bo
|
|
|
84
88
|
|
|
85
89
|
if (popped.code !== SUCCESS_EXIT_CODE) return false
|
|
86
90
|
|
|
91
|
+
await run_event_setup.emit(run_event_setup.installing(issue))
|
|
92
|
+
|
|
87
93
|
return await install(directory)
|
|
88
94
|
}
|
|
89
95
|
|
|
@@ -124,13 +130,16 @@ async function lane_directory(issue: string): Promise<string | undefined> {
|
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
// The launch chain in-process. The CLI prints the pid; `backlog:drive` launches through this same
|
|
127
|
-
// chain.
|
|
133
|
+
// chain. The dispatch is said on the run's stream before it starts, so the board draws a lane that is
|
|
134
|
+
// ready apart from one still being filled.
|
|
128
135
|
async function launch_lane(context: LaunchContext): Promise<LaunchOutcome> {
|
|
129
136
|
const directory = await lane_directory(context.issue)
|
|
130
137
|
|
|
131
138
|
if (directory === undefined) return UNOPENED
|
|
132
139
|
|
|
133
|
-
if (!(await prepare(context
|
|
140
|
+
if (!(await prepare(context, directory))) return FAILED
|
|
141
|
+
|
|
142
|
+
await run_event_setup.emit(run_event_setup.dispatching(context.issue))
|
|
134
143
|
|
|
135
144
|
const dispatched = await josh_command.josh_run(
|
|
136
145
|
['lane:dispatch', context.issue],
|
|
@@ -5,8 +5,8 @@ import { run_carry } from '#scripts/run/carry/run-carry'
|
|
|
5
5
|
import { z } from 'zod'
|
|
6
6
|
|
|
7
7
|
// The measurement ledger the lane limit is tuned against: one JSON line per
|
|
8
|
-
// lane merge, per finished `josh gate`, per machine-load sample, per lane dispatch
|
|
9
|
-
// `josh ship` stage
|
|
8
|
+
// lane merge, per finished `josh gate`, per machine-load sample, per lane dispatch, per finished
|
|
9
|
+
// `josh ship` stage and per stopped `josh ship`, read back by `josh lane:stats`.
|
|
10
10
|
//
|
|
11
11
|
// **Its own file rather than the run's event stream.** The stream is capped at 500 events and is
|
|
12
12
|
// rewritten whole on every append, which is right for "where is the run" and wrong for a measurement
|
|
@@ -35,6 +35,7 @@ const KIND = {
|
|
|
35
35
|
LOAD: 'load',
|
|
36
36
|
DISPATCH: 'dispatch',
|
|
37
37
|
STAGE: 'stage',
|
|
38
|
+
STOP: 'stop',
|
|
38
39
|
} as const
|
|
39
40
|
|
|
40
41
|
const merge_schema = z.object({ kind: z.literal(KIND.MERGE), at: z.string(), issue: z.number() })
|
|
@@ -79,12 +80,22 @@ const stage_schema = z.object({
|
|
|
79
80
|
elapsed_ms: z.number(),
|
|
80
81
|
issue: z.number().optional(),
|
|
81
82
|
})
|
|
83
|
+
// A `josh ship` that stopped on `issue`: `reason` is the stage it stopped at, or `conflict` — the
|
|
84
|
+
// retrospective's stop reason. How long it stayed stopped is derived from the next ship stage, not
|
|
85
|
+
// recorded, so a resumed ship never has to know the stop it ends.
|
|
86
|
+
const stop_schema = z.object({
|
|
87
|
+
kind: z.literal(KIND.STOP),
|
|
88
|
+
at: z.string(),
|
|
89
|
+
issue: z.number(),
|
|
90
|
+
reason: z.string(),
|
|
91
|
+
})
|
|
82
92
|
const entry_schema = z.discriminatedUnion('kind', [
|
|
83
93
|
merge_schema,
|
|
84
94
|
gate_schema,
|
|
85
95
|
load_schema,
|
|
86
96
|
dispatch_schema,
|
|
87
97
|
stage_schema,
|
|
98
|
+
stop_schema,
|
|
88
99
|
])
|
|
89
100
|
|
|
90
101
|
type LedgerEntry = z.infer<typeof entry_schema>
|
|
@@ -93,9 +104,11 @@ type GateEntry = z.infer<typeof gate_schema>
|
|
|
93
104
|
type LoadEntry = z.infer<typeof load_schema>
|
|
94
105
|
type DispatchEntry = z.infer<typeof dispatch_schema>
|
|
95
106
|
type StageEntry = z.infer<typeof stage_schema>
|
|
107
|
+
type StopEntry = z.infer<typeof stop_schema>
|
|
96
108
|
// What a finished gate reports; the ledger stamps the kind and the time.
|
|
97
109
|
type GateRecord = Omit<GateEntry, 'kind' | 'at'>
|
|
98
110
|
type StageRecord = Omit<StageEntry, 'kind' | 'at'>
|
|
111
|
+
type StopRecord = Omit<StopEntry, 'kind' | 'at'>
|
|
99
112
|
|
|
100
113
|
function target_of(repository: string): string {
|
|
101
114
|
return stamp_file.stamp_path(LEDGER_PREFIX, repository, LEDGER_SUFFIX)
|
|
@@ -168,6 +181,10 @@ async function record_stage(stage: StageRecord, path?: string): Promise<void> {
|
|
|
168
181
|
await record({ kind: KIND.STAGE, at: now_iso(), ...stage }, path)
|
|
169
182
|
}
|
|
170
183
|
|
|
184
|
+
async function record_stop(stop: StopRecord, path?: string): Promise<void> {
|
|
185
|
+
await record({ kind: KIND.STOP, at: now_iso(), ...stop }, path)
|
|
186
|
+
}
|
|
187
|
+
|
|
171
188
|
function parse_line(line: string): LedgerEntry | undefined {
|
|
172
189
|
return entry_schema.safeParse(json_value.parse_or_undefined(line)).data
|
|
173
190
|
}
|
|
@@ -199,9 +216,10 @@ const lane_ledger = {
|
|
|
199
216
|
record_gate,
|
|
200
217
|
record_merge,
|
|
201
218
|
record_stage,
|
|
219
|
+
record_stop,
|
|
202
220
|
target,
|
|
203
221
|
target_of,
|
|
204
222
|
}
|
|
205
223
|
|
|
206
|
-
export type { DispatchEntry, GateEntry, LedgerEntry, LoadEntry, MergeEntry, StageEntry }
|
|
224
|
+
export type { DispatchEntry, GateEntry, LedgerEntry, LoadEntry, MergeEntry, StageEntry, StopEntry }
|
|
207
225
|
export { lane_ledger }
|
|
@@ -7,12 +7,14 @@ import { lane_ledger } from './lane-ledger'
|
|
|
7
7
|
import { lane_sampler, type MeasureContext } from './lane-sampler'
|
|
8
8
|
import { lane_stage_stats } from './lane-stage-stats'
|
|
9
9
|
import { lane_stats } from './lane-stats'
|
|
10
|
+
import { lane_stop_stats } from './lane-stop-stats'
|
|
10
11
|
|
|
11
12
|
// `josh lane:sample` / `josh lane:stats` — the lane-limit measurement.
|
|
12
13
|
//
|
|
13
14
|
// `lane:sample` appends one machine-load sample to the ledger, or one every `--every <seconds>` until it
|
|
14
15
|
// is stopped; `lane:stats` reduces a period of the ledger to the one table row #3347 records per lane
|
|
15
|
-
// limit,
|
|
16
|
+
// limit, under it the per-stage durations of the period's lanes, and under those how long a stopped
|
|
17
|
+
// ship waited for each stop reason. Every entry writes itself during
|
|
16
18
|
// a `backlogrun` — the merges, the gates, the stages, and the load `backlog:drive` samples while it
|
|
17
19
|
// runs — so a person only closes the period with `lane:stats`; `lane:sample` is for a load reading
|
|
18
20
|
// outside one. The procedure is `docs/maintainers/lane-limit-measurement.md`.
|
|
@@ -112,7 +114,13 @@ async function stats_command(
|
|
|
112
114
|
const entries = await read_ledger(context)
|
|
113
115
|
const row = lane_stats.row(entries, window, label)
|
|
114
116
|
|
|
115
|
-
console.info(
|
|
117
|
+
console.info(
|
|
118
|
+
[
|
|
119
|
+
`${lane_stats.header()}\n${row}`,
|
|
120
|
+
lane_stage_stats.table(entries, window),
|
|
121
|
+
lane_stop_stats.table(entries, window),
|
|
122
|
+
].join('\n\n'),
|
|
123
|
+
)
|
|
116
124
|
|
|
117
125
|
return 0
|
|
118
126
|
}
|
|
@@ -4,6 +4,7 @@ import { ENV_FILE_NAME } from '#ports'
|
|
|
4
4
|
import { git_worktree } from '#scripts/git/git-worktree'
|
|
5
5
|
import { session_cite } from '#scripts/issue/session-cite'
|
|
6
6
|
import { file_reader } from '#scripts/lib/read-file'
|
|
7
|
+
import { run_event_setup } from '#scripts/run/event/run-event-setup'
|
|
7
8
|
import { lane_cache } from './lane-cache'
|
|
8
9
|
import { lane_environment } from './lane-environment'
|
|
9
10
|
import { lane_install, type InstallResult } from './lane-install'
|
|
@@ -172,6 +173,13 @@ function guard_install(lane: LaneInfo, result: InstallResult): void {
|
|
|
172
173
|
)
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
// **The install is said on the run's stream as it starts**: it is the long part of opening a lane, and
|
|
177
|
+
// `run:board` draws it apart from the work tree being made. Best-effort, as every stage is.
|
|
178
|
+
async function fill(lane: LaneInfo): Promise<void> {
|
|
179
|
+
await run_event_setup.emit(run_event_setup.installing(lane.issue))
|
|
180
|
+
guard_install(lane, await lane_install.install_dependencies(lane.directory))
|
|
181
|
+
}
|
|
182
|
+
|
|
175
183
|
function build_plan(
|
|
176
184
|
repository_root: string,
|
|
177
185
|
root: string,
|
|
@@ -231,7 +239,7 @@ async function materialize(plan: LanePlan, source_root: string): Promise<void> {
|
|
|
231
239
|
release_seat_lock(plan.seat_lock)
|
|
232
240
|
lane_cache.seed_caches(source_root, plan.lane.directory)
|
|
233
241
|
lane_cache.seed_hook_bundles(source_root, plan.lane.directory)
|
|
234
|
-
|
|
242
|
+
await fill(plan.lane)
|
|
235
243
|
} finally {
|
|
236
244
|
release_seat_lock(plan.seat_lock)
|
|
237
245
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
|
|
2
|
+
import { git_stash } from '#scripts/git/stash/git-stash'
|
|
3
|
+
|
|
4
|
+
// Keeping a lane's uncommitted work before something discards or restarts it — `run:merge` closing a
|
|
5
|
+
// lane, `lane:restart` handing it to a fresh session. The stash stack outlives the tree, so the push is
|
|
6
|
+
// the copy, and the comment on the Issue is where a person finds it again. One writer, so the message
|
|
7
|
+
// `stash:pop` matches on and the recovery line the comment names cannot drift between the callers.
|
|
8
|
+
|
|
9
|
+
function preserved_comment(message: string, cause: string): string {
|
|
10
|
+
return [
|
|
11
|
+
`The lane held uncommitted work when ${cause}, so it was stashed:`,
|
|
12
|
+
`- recover with \`pnpm josh stash:pop "${message}"\``,
|
|
13
|
+
].join('\n')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Push the lane's uncommitted work under `<issue>: uncommitted work at <occasion>`, and return the
|
|
18
|
+
* message. A push that fails throws, so the caller keeps the tree rather than losing it.
|
|
19
|
+
*/
|
|
20
|
+
async function push_work(issue: string, directory: string, occasion: string): Promise<string> {
|
|
21
|
+
const message = git_stash.work_message(issue, occasion)
|
|
22
|
+
|
|
23
|
+
await git_stash.push(message, directory)
|
|
24
|
+
|
|
25
|
+
return message
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Say on the Issue where the stash `message` went, `cause` completing "when …". Never throws. */
|
|
29
|
+
async function announce(issue: string, message: string, cause: string): Promise<void> {
|
|
30
|
+
await git_gh_issue_write.issue_try_comment(issue, preserved_comment(message, cause))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `push_work`, then `announce` — for a caller with no reason to separate the two. */
|
|
34
|
+
async function stash_work(
|
|
35
|
+
issue: string,
|
|
36
|
+
directory: string,
|
|
37
|
+
occasion: string,
|
|
38
|
+
cause: string,
|
|
39
|
+
): Promise<string> {
|
|
40
|
+
const message = await push_work(issue, directory, occasion)
|
|
41
|
+
|
|
42
|
+
await announce(issue, message, cause)
|
|
43
|
+
|
|
44
|
+
return message
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lane_preserve = { announce, push_work, stash_work }
|
|
48
|
+
|
|
49
|
+
export { lane_preserve }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process'
|
|
2
2
|
import { PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
3
3
|
import { lane_child_invocation } from './lane-child-invocation'
|
|
4
|
+
import { process_tools } from './process-tools'
|
|
4
5
|
|
|
5
6
|
// Ending a lane child that did not end itself.
|
|
6
7
|
//
|
|
@@ -42,7 +43,9 @@ function parse_pids(printed: string): Array<number> {
|
|
|
42
43
|
.filter((pid) => Number.isSafeInteger(pid) && pid >= LOWEST_CHILD_PID)
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
// `pgrep` exits 1 when nothing matches, which `execFileSync` throws — the empty list either way.
|
|
46
|
+
// `pgrep` exits 1 when nothing matches, which `execFileSync` throws — the empty list either way. A
|
|
47
|
+
// probe that is not installed is refused by name: reading it as "nothing there" is how a missing `ps`
|
|
48
|
+
// left the owner check answering `unknown`.
|
|
46
49
|
function run_probe(command: string, args: ReadonlyArray<string>): string {
|
|
47
50
|
try {
|
|
48
51
|
return execFileSync(command, [...args], {
|
|
@@ -50,7 +53,9 @@ function run_probe(command: string, args: ReadonlyArray<string>): string {
|
|
|
50
53
|
timeout: PROBE_TIMEOUT_MS,
|
|
51
54
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
52
55
|
})
|
|
53
|
-
} catch {
|
|
56
|
+
} catch (error) {
|
|
57
|
+
process_tools.throw_if_missing(command, error)
|
|
58
|
+
|
|
54
59
|
return ''
|
|
55
60
|
}
|
|
56
61
|
}
|
|
@@ -111,6 +116,12 @@ function tree_of(roots: ReadonlyArray<number>, probes: ReapProbes): Array<number
|
|
|
111
116
|
return tree
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
function roots_of(issue: string, own: Set<number>, probes: ReapProbes): Array<number> {
|
|
120
|
+
const pattern = lane_child_invocation.process_pattern(issue)
|
|
121
|
+
|
|
122
|
+
return probes.matching(pattern).filter((pid) => !own.has(pid))
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
/**
|
|
115
126
|
* Terminate every process of the lane child for `issue` and everything under it, sparing the caller's
|
|
116
127
|
* own ancestry. Returns the pids that were signalled — the empty list is the healthy case, a child
|
|
@@ -118,14 +129,18 @@ function tree_of(roots: ReadonlyArray<number>, probes: ReapProbes): Array<number
|
|
|
118
129
|
*/
|
|
119
130
|
function reap_child(issue: string, probes: ReapProbes = SYSTEM_PROBES): Array<number> {
|
|
120
131
|
const own = own_ancestry(probes)
|
|
121
|
-
const
|
|
122
|
-
const roots = probes.matching(pattern).filter((pid) => !own.has(pid))
|
|
123
|
-
const doomed = tree_of(roots, probes).filter((pid) => !own.has(pid))
|
|
132
|
+
const doomed = tree_of(roots_of(issue, own, probes), probes).filter((pid) => !own.has(pid))
|
|
124
133
|
|
|
125
134
|
return doomed.filter((pid) => probes.terminate(pid))
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
/** Whether a lane child for `issue` is running, by the same match `reap_child` would end. */
|
|
138
|
+
function is_child_running(issue: string, probes: ReapProbes = SYSTEM_PROBES): boolean {
|
|
139
|
+
return roots_of(issue, own_ancestry(probes), probes).length > 0
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
const lane_reap = {
|
|
143
|
+
is_child_running,
|
|
129
144
|
own_ancestry,
|
|
130
145
|
parse_pids,
|
|
131
146
|
reap_child,
|
|
@@ -236,6 +236,11 @@ async function has_lanes_in_flight(): Promise<boolean> {
|
|
|
236
236
|
return lanes.some((lane) => !lane.is_stranded)
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
/** The lanes in use: the open work trees an interruption has not stranded. */
|
|
240
|
+
function live_count(lanes: ReadonlyArray<LaneInfo>): number {
|
|
241
|
+
return lanes.filter((lane) => !lane.is_stranded).length
|
|
242
|
+
}
|
|
243
|
+
|
|
239
244
|
/** The seats live lanes hold — what a new lane's seat has to avoid. */
|
|
240
245
|
function used_seats(lanes: ReadonlyArray<LaneInfo>): Array<number> {
|
|
241
246
|
return lanes.map((lane) => lane.seat).filter((seat): seat is number => seat !== undefined)
|
|
@@ -267,6 +272,7 @@ const lane_registry = {
|
|
|
267
272
|
find_open_lane,
|
|
268
273
|
has_lanes_in_flight,
|
|
269
274
|
list_lanes,
|
|
275
|
+
live_count,
|
|
270
276
|
main_repository_root,
|
|
271
277
|
parse_block,
|
|
272
278
|
read_environment,
|