@joshuafolkken/kit 1.1471.0 → 1.1514.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/settings.json +6 -6
- package/.claude/skills/epic-commands/SKILL.md +2 -14
- package/.claude/skills/epic-commands/epic-bundle.md +9 -5
- package/.claude/skills/epic-commands/execution-waves.md +1 -2
- package/.claude/skills/verify-ui/SKILL.md +1 -2
- package/.claude/skills/workflow-commands/SKILL.md +7 -9
- package/.claude/skills/workflow-commands/background-commands.md +3 -7
- package/.claude/skills/workflow-commands/backlogrun-child.md +3 -10
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -15
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -8
- package/.claude/skills/workflow-commands/backlogrun-progress.md +10 -19
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +8 -8
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -10
- package/.claude/skills/workflow-commands/backlogrun.md +31 -50
- package/.claude/skills/workflow-commands/chain-rule.md +7 -6
- package/.claude/skills/workflow-commands/delegation.md +12 -8
- package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
- package/.claude/skills/workflow-commands/followup.md +3 -6
- package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
- package/.claude/skills/workflow-commands/fullrun.md +4 -6
- package/.claude/skills/workflow-commands/halfrun.md +3 -7
- package/.claude/skills/workflow-commands/into-target.md +1 -2
- package/.claude/skills/workflow-commands/issue-comments.md +3 -3
- package/.claude/skills/workflow-commands/issue-scout.md +3 -2
- package/.claude/skills/workflow-commands/latest-gate.md +2 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
- package/.claude/skills/workflow-commands/observation-filing.md +0 -14
- package/.claude/skills/workflow-commands/observation-ledger.md +4 -9
- package/.claude/skills/workflow-commands/pre-gate-cut.md +11 -16
- package/.claude/skills/workflow-commands/prerequisite.md +1 -2
- package/.claude/skills/workflow-commands/progress-watcher.md +2 -7
- package/.claude/skills/workflow-commands/prrun.md +3 -5
- package/.claude/skills/workflow-commands/retrospective.md +4 -4
- package/.claude/skills/workflow-commands/split-assessment.md +1 -3
- package/.claude/skills/workflow-commands/target-repository.md +1 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +2 -4
- package/.codex/hooks.json +6 -6
- package/.github/actions/setup-node/action.yml +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CLAUDE.md +1 -1
- package/cspell/index.yaml +7 -0
- package/dist/CLAUDE.md +1 -1
- package/dist/commands/document-section-cli.js +67 -34
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/chunk-4KKD2UTK.js +66 -0
- package/dist/hooks/{chunk-J47HU5H5.js → chunk-6TS2JD6D.js} +3 -3
- package/dist/hooks/chunk-7DSBJVFH.js +50 -0
- package/dist/hooks/{chunk-YHBZAJSV.js → chunk-A65Y4YOR.js} +3 -3
- package/dist/hooks/{chunk-E7OIKX5Z.js → chunk-APUO6U4J.js} +85 -7
- package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
- package/dist/hooks/chunk-GP4IEC4O.js +20 -0
- package/dist/hooks/{chunk-SHGPANFB.js → chunk-JP45SOCJ.js} +3 -3
- package/dist/hooks/{chunk-K4EOONYM.js → chunk-LAOV7NLM.js} +1 -1
- package/dist/hooks/chunk-M4HGZV7U.js +83 -0
- package/dist/hooks/{chunk-KC5AK2GF.js → chunk-O6GDRNJB.js} +3 -3
- package/dist/hooks/{chunk-YYPI7SJZ.js → chunk-PF2NZL2A.js} +5 -5
- package/dist/hooks/{chunk-H5HVWYXK.js → chunk-PTICX4OL.js} +3 -4
- package/dist/hooks/{chunk-IIGFYXKC.js → chunk-RT27QRJW.js} +581 -1438
- package/dist/hooks/{chunk-43ZHT22P.js → chunk-SYOTDYCI.js} +1127 -1089
- package/dist/hooks/{chunk-AS74OU3D.js → chunk-TNUMY6NL.js} +1507 -1336
- package/dist/hooks/chunk-UPUZF3AN.js +279 -0
- package/dist/hooks/{chunk-EUMACUK2.js → chunk-WK6SMBIC.js} +1948 -806
- package/dist/hooks/codex-hook-adapter.js +17 -14
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +5 -4
- package/dist/hooks/inputs.json +317 -264
- package/dist/hooks/investigation-guard.js +8 -11
- package/dist/hooks/pretool-guard.js +16 -13
- package/dist/hooks/session-lang.js +370 -25
- package/dist/hooks/stop-guard.js +2576 -176
- package/dist/josh.js +2124 -277
- package/lefthook/base.yml +5 -3
- package/package.json +12 -10
- package/prompts/collaboration-workflow/file-edits.md +1 -3
- package/prompts/collaboration-workflow/gh-rest.md +1 -2
- package/prompts/collaboration-workflow/issue-citation.md +2 -4
- package/prompts/collaboration-workflow/issue-template.md +10 -2
- package/prompts/collaboration-workflow/lane-instructions.md +35 -0
- package/prompts/collaboration-workflow/operating-rules.md +2 -6
- package/prompts/collaboration-workflow/output-bounds.md +3 -3
- package/prompts/collaboration-workflow/overview.md +1 -1
- package/prompts/collaboration-workflow/principles.md +14 -8
- package/prompts/collaboration-workflow/report-format.md +9 -0
- package/prompts/collaboration-workflow/residency.md +12 -8
- package/prompts/collaboration-workflow/rule-delivery.md +3 -11
- package/prompts/collaboration-workflow/shell-body.md +6 -6
- package/prompts/collaboration-workflow/turn-batching.md +5 -5
- package/prompts/collaboration-workflow/upstream-interrupt.md +2 -2
- package/prompts/collaboration-workflow/wip-cap.md +6 -6
- package/prompts/collaboration-workflow.md +1 -0
- package/prompts/review.md +4 -5
- package/prompts/testing-guide.md +2 -2
- package/scripts/agent/agent-argv.ts +20 -4
- package/scripts/agent/agent-role-profile.ts +101 -45
- package/scripts/agent/agent-safe-text.ts +60 -0
- package/scripts/agent/claude-agent-argv.ts +14 -0
- package/scripts/agent/codex-agent-argv.ts +2 -0
- package/scripts/agent/lane-instructions.ts +144 -0
- package/scripts/backlog/backlog-budget.ts +4 -4
- package/scripts/backlog/backlog-drive-named.ts +1 -1
- package/scripts/backlog/backlog-drive-restore.ts +9 -1
- package/scripts/backlog/backlog-occupied.ts +56 -0
- package/scripts/backlog/backlog-ready.ts +47 -10
- package/scripts/delegation/duplicate-read-guard.ts +2 -2
- package/scripts/delegation/investigation-guard.ts +15 -65
- package/scripts/delegation/investigation-reads.ts +0 -1
- package/scripts/document/document-byte-budget.ts +6 -5
- package/scripts/document/document-section-cli.ts +17 -5
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/document/read-set-cli.ts +4 -1
- package/scripts/epic/epic-audit-orphans.ts +5 -8
- package/scripts/epic/epic-audit.ts +52 -1
- package/scripts/epic/epic-bundle-evidence.ts +49 -26
- package/scripts/epic/epic-bundle-referenced.ts +14 -3
- package/scripts/epic/epic-bundle.ts +17 -9
- package/scripts/followup/git-followup-workflow.ts +19 -1
- package/scripts/gate/check-inputs.ts +129 -0
- package/scripts/gate/check-reuse.ts +162 -0
- package/scripts/gate/core-budget.ts +93 -44
- package/scripts/gate/core-wait.ts +130 -0
- package/scripts/gate/free-cores.ts +49 -0
- package/scripts/gate/gate-ledger.ts +17 -3
- package/scripts/gate/gate-plan.ts +23 -1
- package/scripts/gate/gate-report.ts +15 -2
- package/scripts/gate/gate-run-steps.ts +150 -0
- package/scripts/gate/held-mark.ts +55 -0
- package/scripts/gate/machine-capacity.ts +70 -11
- package/scripts/gate/pre-commit-type-check.ts +14 -1
- package/scripts/gate/verification-gate.ts +58 -125
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +24 -2
- package/scripts/gh/git-pr.ts +13 -1
- package/scripts/git/main-merge-regenerate.ts +78 -0
- package/scripts/git/main-merge.ts +14 -5
- package/scripts/hooks/batch-guard.ts +5 -8
- package/scripts/hooks/pre-commit-lint.ts +91 -0
- package/scripts/hooks/pre-push-unit.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +27 -52
- package/scripts/hooks/reserved-run.ts +1 -0
- package/scripts/hooks/run-hook.sh +10 -12
- package/scripts/hooks/stop-actions.ts +204 -0
- package/scripts/hooks/stop-guard.ts +62 -21
- package/scripts/init/hook-launch.ts +12 -7
- package/scripts/issue/expected-effect-lint.ts +59 -0
- package/scripts/issue/issue-cite-cli.ts +35 -15
- package/scripts/issue/issue-classification.ts +1 -1
- package/scripts/issue/issue-lint-cli.ts +5 -3
- package/scripts/issue/issue-scout-cli.ts +19 -14
- package/scripts/issue/markdown-section.ts +19 -5
- package/scripts/josh/command-usage.ts +110 -0
- package/scripts/josh/josh-command-map.ts +2 -0
- package/scripts/josh/josh-command-types.ts +55 -9
- package/scripts/josh/josh-commands-ai.ts +13 -0
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-hooks.ts +6 -0
- package/scripts/josh/josh-commands-lane.ts +10 -1
- package/scripts/josh/josh-commands-workflow.ts +5 -0
- package/scripts/josh/josh-logic.ts +6 -15
- package/scripts/josh/josh-usage-automation.ts +121 -0
- package/scripts/josh/josh-usage-backlog.ts +180 -0
- package/scripts/josh/josh-usage-developer.ts +230 -0
- package/scripts/josh/josh-usage-hooks.ts +38 -0
- package/scripts/josh/josh-usage-issue.ts +205 -0
- package/scripts/josh/josh-usage-review.ts +101 -0
- package/scripts/josh/josh-usage-run.ts +252 -0
- package/scripts/josh/josh-usage-session.ts +238 -0
- package/scripts/josh/josh-usage-workflow.ts +212 -0
- package/scripts/josh/josh.ts +14 -5
- package/scripts/josh/package-directory.ts +20 -0
- package/scripts/lane/lane-capacity.ts +8 -2
- package/scripts/lane/lane-check-stats.ts +56 -0
- package/scripts/lane/lane-child-invocation.ts +35 -4
- package/scripts/lane/lane-dispatch.ts +3 -3
- package/scripts/lane/lane-guard-policy.ts +2 -2
- package/scripts/lane/lane-handoff.ts +8 -1
- package/scripts/lane/lane-install.ts +12 -4
- package/scripts/lane/lane-ledger.ts +43 -1
- package/scripts/lane/lane-limit-cli.ts +8 -9
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-preserve.ts +29 -1
- package/scripts/lane/lane-registry.ts +9 -7
- package/scripts/lane/lane-restart.ts +5 -26
- package/scripts/lane/lane-session-action.ts +146 -0
- package/scripts/lane/lane-session-broker.ts +57 -138
- package/scripts/lane/lane-withdraw-cli.ts +71 -0
- package/scripts/lane/lane-withdraw.ts +182 -0
- package/scripts/lint/format.ts +4 -4
- package/scripts/lint/lint-parallel.ts +10 -3
- package/scripts/lint/lint-related-scope.ts +4 -1
- package/scripts/notify/telegram-test.ts +10 -5
- package/scripts/report/report-format-reference.ts +2 -0
- package/scripts/review/acceptance-map-check.ts +61 -0
- package/scripts/review/acceptance-map.ts +205 -0
- package/scripts/review/review-brief-cli.ts +1 -1
- package/scripts/rules/agent-model.ts +59 -0
- package/scripts/rules/delivered-rules.ts +11 -2
- package/scripts/rules/filing-cap.ts +1 -0
- package/scripts/rules/firing-point.ts +6 -5
- package/scripts/rules/guard-remedy.ts +117 -0
- package/scripts/rules/issue-comments.ts +67 -6
- package/scripts/rules/last-prompt.ts +50 -5
- package/scripts/rules/oracle-list-cli.ts +7 -0
- package/scripts/rules/piped-verification.ts +3 -1
- package/scripts/rules/rule-list.ts +4 -1
- package/scripts/rules/rule-registry-rows.ts +10 -0
- package/scripts/rules/rule-registry.ts +7 -4
- package/scripts/rules/rule-rewrite.ts +114 -0
- package/scripts/rules/rule-value.ts +7 -1
- package/scripts/rules/shell-segments.ts +15 -0
- package/scripts/rules/stop-decision.ts +77 -0
- package/scripts/rules/stop-rules.ts +147 -47
- package/scripts/rules/tail-commands.ts +15 -1
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-closed.ts +8 -2
- package/scripts/run/board/run-board-github.ts +9 -1
- package/scripts/run/board/run-board-header.ts +4 -1
- package/scripts/run/board/run-board-labels.ts +2 -0
- package/scripts/run/board/run-board-layout.ts +1 -1
- package/scripts/run/board/run-board-read.ts +16 -0
- package/scripts/run/board/run-board-reason.ts +10 -1
- package/scripts/run/board/run-board-render-legend.ts +1 -0
- package/scripts/run/board/run-board-render-slot.ts +1 -1
- package/scripts/run/board/run-board-render-waits.ts +41 -0
- package/scripts/run/board/run-board-render.ts +6 -1
- package/scripts/run/board/run-board-row-yield.ts +1 -0
- package/scripts/run/board/run-board-state.ts +5 -0
- package/scripts/run/board/run-board-status.ts +72 -21
- package/scripts/run/board/run-board-tick.ts +5 -1
- package/scripts/run/cut/run-cut-cli.ts +3 -2
- package/scripts/run/cut/run-cut-report.ts +16 -0
- package/scripts/run/detached-launch.ts +23 -4
- package/scripts/run/entry/run-entry-cli.ts +8 -1
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-scope.ts +19 -1
- package/scripts/run/event/run-event-stream-emit.ts +13 -6
- package/scripts/run/event/run-event-stream.ts +4 -0
- package/scripts/run/hold/run-hold-session.ts +46 -0
- package/scripts/run/hold/run-hold.ts +7 -1
- package/scripts/run/launch-priority.ts +32 -0
- package/scripts/run/merge/run-merge-cli.ts +16 -65
- package/scripts/run/merge/run-merge-signals.ts +99 -0
- package/scripts/run/merge/run-merge-steps.ts +11 -1
- package/scripts/run/merge/run-merge.ts +21 -9
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-poll-hook.ts +157 -0
- package/scripts/run/run-step-cli.ts +22 -15
- package/scripts/run/run-step-section.ts +85 -0
- package/scripts/run/ship/run-ship-cli.ts +5 -12
- package/scripts/run/ship/run-ship-detach.ts +9 -0
- package/scripts/run/ship/run-ship-next.ts +10 -2
- package/scripts/run/ship/run-ship-overlap.ts +195 -0
- package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
- package/scripts/run/ship/run-ship-preflight.ts +28 -2
- package/scripts/run/ship/run-ship-return.ts +31 -7
- package/scripts/run/ship/run-ship-review-steps.ts +12 -10
- package/scripts/run/ship/run-ship-steps.ts +1 -1
- package/scripts/run/ship/run-ship-stop-context.ts +206 -0
- package/scripts/run/ship/run-ship-sync.ts +14 -6
- package/scripts/test/test-e2e-guard.ts +30 -5
- package/scripts/test/test-unit-guard.ts +16 -1
- package/scripts/test/unit-worker-share.ts +26 -4
- package/scripts/time-runtime/time-shell.ts +29 -4
- package/scripts/time-runtime/time-spans.ts +1 -0
- package/scripts/time-runtime/time-tool-call.ts +7 -0
- package/templates/gitignore +6 -2
- package/dist/hooks/chunk-K4LMBQC2.js +0 -68
|
@@ -10,6 +10,8 @@ import { json_value } from '#scripts/lib/json-value'
|
|
|
10
10
|
import { unit_worker_share } from '#scripts/test/unit-worker-share'
|
|
11
11
|
import { z } from 'zod'
|
|
12
12
|
import { core_admission, type Admission, type LedgerEntry, type Overflow } from './core-admission'
|
|
13
|
+
import { core_wait, type BudgetWait, type WaitReason } from './core-wait'
|
|
14
|
+
import { held_mark } from './held-mark'
|
|
13
15
|
import { machine_capacity, type MachineBudget, type MachineReading } from './machine-capacity'
|
|
14
16
|
|
|
15
17
|
// A machine-wide weighted core budget with admission control.
|
|
@@ -55,11 +57,22 @@ const MIN_BUDGET = 1
|
|
|
55
57
|
// and spell check numbers are `gate-plan.ts`'s measured table, floored. The whole-tree eslint scan
|
|
56
58
|
// behind `josh lines` and `josh refactor:scan` runs in one process: measured cold on the 11-core
|
|
57
59
|
// machine on 2026-10-06, 113s of CPU over 98s of wall, so it gets one core.
|
|
60
|
+
//
|
|
61
|
+
// The rest ran outside the ledger until joshuafolkken/kit#3944, measured with `/usr/bin/time -l` on the
|
|
62
|
+
// same machine on 2026-10-17 under a load average of 16. Every one stays under one core, so each takes
|
|
63
|
+
// one: `prettier --check` over 40 staged files 2.1s of CPU over 1.7s of wall (`format`), `josh
|
|
64
|
+
// retrospective` 16.5s over 19.8s, and a fresh lane's `pnpm install --frozen-lockfile` 1.9s over 6.1s.
|
|
65
|
+
// `e2e` is the Playwright worker count `josh test:e2e` passes locally — the two-worker run CI already
|
|
66
|
+
// sizes for its four-core runner — since kit has no e2e suite of its own to time.
|
|
58
67
|
const CORE_WEIGHTS = {
|
|
59
68
|
lint: 2,
|
|
60
69
|
type_check: 1,
|
|
61
70
|
spell_check: 1,
|
|
62
71
|
eslint_scan: 1,
|
|
72
|
+
format: 1,
|
|
73
|
+
retrospective: 1,
|
|
74
|
+
install: 1,
|
|
75
|
+
e2e: 2,
|
|
63
76
|
} as const
|
|
64
77
|
|
|
65
78
|
// **The memory each heavy tool holds, declared beside its cores**: peak
|
|
@@ -67,33 +80,37 @@ const CORE_WEIGHTS = {
|
|
|
67
80
|
// 2026-10-07: the type check 1,244 MB, the spell check 330 MB, and one unit worker 319 MB over 54 test
|
|
68
81
|
// files. Typed eslint was 1,207 MB over one directory and was observed at 1.6–2.4 GB per whole-tree
|
|
69
82
|
// `eslint .` with three lanes running the same day, so lint and the whole-tree scan take that peak.
|
|
83
|
+
// On 2026-10-17: prettier 260 MB, the retrospective 862 MB and a lane install 65 MB.
|
|
70
84
|
const MEMORY_MB = {
|
|
71
85
|
lint: 2400,
|
|
72
86
|
type_check: 1300,
|
|
73
87
|
spell_check: 350,
|
|
74
88
|
eslint_scan: 2400,
|
|
75
89
|
unit_worker: 350,
|
|
90
|
+
format: 300,
|
|
91
|
+
retrospective: 900,
|
|
92
|
+
install: 100,
|
|
76
93
|
} as const
|
|
77
94
|
|
|
78
|
-
// **The environment mark that says a parent already holds this process's cores**
|
|
79
|
-
// `josh gate` reserves per check and then spawns `pnpm josh lint`. Without
|
|
80
|
-
// the mark, that child's own dispatch would claim a second place for the cores its parent already
|
|
81
|
-
// holds. Every holder sets it, and every child inherits it through the environment and claims nothing.
|
|
82
|
-
const HELD_KEY = 'JOSH_CORE_RESERVED'
|
|
83
|
-
const HELD_VALUE = '1'
|
|
84
|
-
|
|
85
95
|
// `memory_mb` and `admitted_at` are optional so a marker an older `josh` wrote still reads: it holds no
|
|
86
96
|
// declared memory, and it counts as load from outside the ledger until it is gone.
|
|
97
|
+
// `command`, `checkout`, `lane`, `lanes_root` and `wait_reason` name a waiting claim on `run:board`
|
|
98
|
+
// (`core-wait.ts`).
|
|
87
99
|
const reservation_schema = process_owner_schema.extend({
|
|
88
100
|
weight: z.number(),
|
|
89
101
|
claimed_at: z.number(),
|
|
90
102
|
memory_mb: z.number().optional(),
|
|
91
103
|
admitted_at: z.number().optional(),
|
|
104
|
+
command: z.string().optional(),
|
|
105
|
+
checkout: z.string().optional(),
|
|
106
|
+
lane: z.string().optional(),
|
|
107
|
+
lanes_root: z.string().optional(),
|
|
108
|
+
wait_reason: z.enum(core_wait.WAIT_REASONS).optional(),
|
|
92
109
|
})
|
|
93
110
|
|
|
94
111
|
type Reservation = z.infer<typeof reservation_schema>
|
|
95
112
|
|
|
96
|
-
type ReservationFields =
|
|
113
|
+
type ReservationFields = Omit<Reservation, 'pid' | 'process_start'>
|
|
97
114
|
|
|
98
115
|
interface LiveReservation extends LedgerEntry {
|
|
99
116
|
reservation: Reservation
|
|
@@ -102,6 +119,9 @@ interface LiveReservation extends LedgerEntry {
|
|
|
102
119
|
interface ReserveOptions {
|
|
103
120
|
budget?: number
|
|
104
121
|
memory_mb?: number | undefined
|
|
122
|
+
// What the claim is for, named on the waiting line and on `run:board`.
|
|
123
|
+
command?: string | undefined
|
|
124
|
+
report?: (line: string) => void
|
|
105
125
|
read_machine?: () => Promise<MachineReading>
|
|
106
126
|
wait_cap_ms?: number
|
|
107
127
|
poll_interval_ms?: number
|
|
@@ -195,6 +215,10 @@ function write_reservation(target: string, fields: ReservationFields): void {
|
|
|
195
215
|
stamp_file.replace_stamp(target, { ...process_identity.own_fields(), ...fields })
|
|
196
216
|
}
|
|
197
217
|
|
|
218
|
+
function report_wait(line: string): void {
|
|
219
|
+
process.stderr.write(`${line}\n`)
|
|
220
|
+
}
|
|
221
|
+
|
|
198
222
|
// The clock and cadence a wait runs on, resolved apart from the ledger fields so `reserve` stays under
|
|
199
223
|
// the complexity limit — each default is a branch.
|
|
200
224
|
interface TimingConfig {
|
|
@@ -206,6 +230,7 @@ interface TimingConfig {
|
|
|
206
230
|
|
|
207
231
|
interface WaitContext extends TimingConfig {
|
|
208
232
|
read_machine: () => Promise<MachineReading>
|
|
233
|
+
report: (line: string) => void
|
|
209
234
|
key: string
|
|
210
235
|
target: string
|
|
211
236
|
directory: string
|
|
@@ -259,6 +284,19 @@ function overflow_of(
|
|
|
259
284
|
}
|
|
260
285
|
}
|
|
261
286
|
|
|
287
|
+
// The marker records why the claim waits, rewritten only when the reason changes, and the command says
|
|
288
|
+
// so once, when its wait starts.
|
|
289
|
+
function mark_waiting(context: WaitContext, reason: WaitReason): void {
|
|
290
|
+
const previous = context.fields.wait_reason
|
|
291
|
+
|
|
292
|
+
if (previous === reason) return
|
|
293
|
+
|
|
294
|
+
context.fields = { ...context.fields, wait_reason: reason }
|
|
295
|
+
write_reservation(context.target, context.fields)
|
|
296
|
+
|
|
297
|
+
if (previous === undefined) context.report(core_wait.wait_line(context.fields.command, reason))
|
|
298
|
+
}
|
|
299
|
+
|
|
262
300
|
async function poll_admission(context: WaitContext): Promise<Admission | undefined> {
|
|
263
301
|
const reservations = live_reservations(context.directory)
|
|
264
302
|
const budget = await current_budget(context, reservations)
|
|
@@ -269,6 +307,8 @@ async function poll_admission(context: WaitContext): Promise<Admission | undefin
|
|
|
269
307
|
return admit(context, overflow_of(context, reservations, budget))
|
|
270
308
|
}
|
|
271
309
|
|
|
310
|
+
mark_waiting(context, core_wait.wait_reason(context.key, reservations, budget))
|
|
311
|
+
|
|
272
312
|
return undefined
|
|
273
313
|
}
|
|
274
314
|
|
|
@@ -288,13 +328,33 @@ async function await_admission(context: WaitContext): Promise<Admission> {
|
|
|
288
328
|
return admission
|
|
289
329
|
}
|
|
290
330
|
|
|
331
|
+
// What a wait reads the machine through and reports its start to.
|
|
332
|
+
function resolve_ports(options: ReserveOptions): Pick<WaitContext, 'read_machine' | 'report'> {
|
|
333
|
+
return {
|
|
334
|
+
read_machine: options.read_machine ?? machine_capacity.read_machine,
|
|
335
|
+
report: options.report ?? report_wait,
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// The checkout the claim runs in, so `run:board` can say whose claim waits. The ledger is the
|
|
340
|
+
// machine's, so the checkout may be another repository's (`core-wait.ts` → `where_of`).
|
|
341
|
+
function claim_fields(
|
|
342
|
+
weight: number,
|
|
343
|
+
claimed_at: number,
|
|
344
|
+
options: ReserveOptions,
|
|
345
|
+
): ReservationFields {
|
|
346
|
+
const { memory_mb, command } = options
|
|
347
|
+
|
|
348
|
+
return { weight, claimed_at, memory_mb, command, ...core_wait.where_of(process.cwd()) }
|
|
349
|
+
}
|
|
350
|
+
|
|
291
351
|
async function reserve(weight: number, options: ReserveOptions = {}): Promise<Handle> {
|
|
292
352
|
const timing = resolve_timing(options)
|
|
293
353
|
const directory = options.directory ?? PLATFORM_TEMP_ROOT
|
|
294
354
|
const claimed_at = timing.now()
|
|
295
355
|
const name = marker_name(randomUUID())
|
|
296
356
|
const target = path.join(directory, name)
|
|
297
|
-
const fields =
|
|
357
|
+
const fields = claim_fields(weight, claimed_at, options)
|
|
298
358
|
|
|
299
359
|
write_reservation(target, fields)
|
|
300
360
|
const admission = await await_admission({
|
|
@@ -305,58 +365,42 @@ async function reserve(weight: number, options: ReserveOptions = {}): Promise<Ha
|
|
|
305
365
|
budget: normalize_budget(options.budget ?? availableParallelism()),
|
|
306
366
|
deadline: claimed_at + timing.wait_cap_ms,
|
|
307
367
|
fields,
|
|
308
|
-
|
|
368
|
+
...resolve_ports(options),
|
|
309
369
|
})
|
|
310
370
|
|
|
311
371
|
return { target, admission }
|
|
312
372
|
}
|
|
313
373
|
|
|
374
|
+
// The claims waiting in the ledger now, for a `run:board` running in `home`.
|
|
375
|
+
function budget_waits(
|
|
376
|
+
directory: string = PLATFORM_TEMP_ROOT,
|
|
377
|
+
now: number = Date.now(),
|
|
378
|
+
home: string = process.cwd(),
|
|
379
|
+
): Array<BudgetWait> {
|
|
380
|
+
return core_wait.waits_of(live_reservations(directory), now, home)
|
|
381
|
+
}
|
|
382
|
+
|
|
314
383
|
function release(handle: Handle): void {
|
|
315
384
|
sweep_marker(handle.target)
|
|
316
385
|
}
|
|
317
386
|
|
|
318
387
|
// The whole reservation lifetime around a step: claim a place, wait for admission, run, and free the
|
|
319
388
|
// place on any exit. `finally`, so a step that threw still releases its cores. The step is told how it was
|
|
320
|
-
// admitted, so the gate can report one that started past the budget.
|
|
389
|
+
// admitted, so the gate can report one that started past the budget, and the claim's ledger key.
|
|
321
390
|
async function with_core_reservation<T>(
|
|
322
391
|
weight: number,
|
|
323
|
-
run: (admission: Admission) => Promise<T>,
|
|
392
|
+
run: (admission: Admission, key: string) => Promise<T>,
|
|
324
393
|
options: ReserveOptions = {},
|
|
325
394
|
): Promise<T> {
|
|
326
395
|
const handle = await reserve(weight, options)
|
|
327
396
|
|
|
328
397
|
try {
|
|
329
|
-
return await run(handle.admission)
|
|
398
|
+
return await run(handle.admission, path.basename(handle.target))
|
|
330
399
|
} finally {
|
|
331
400
|
release(handle)
|
|
332
401
|
}
|
|
333
402
|
}
|
|
334
403
|
|
|
335
|
-
function is_held(): boolean {
|
|
336
|
-
return process.env[HELD_KEY] === HELD_VALUE
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// A function of its own so the restoring write does not sit after an `await`, where
|
|
340
|
-
// `require-atomic-updates` reads any write to `process` as a possible race — the reason `josh.ts`
|
|
341
|
-
// gives `record_exit_code`.
|
|
342
|
-
function set_held_mark(value: string): void {
|
|
343
|
-
process.env[HELD_KEY] = value
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Marks this process, and every child it spawns, as running inside a held reservation. The previous
|
|
347
|
-
// value comes back afterwards, so a nested holder never clears the mark its outer holder set.
|
|
348
|
-
async function with_held_mark<T>(run: () => Promise<T>): Promise<T> {
|
|
349
|
-
const previous = process.env[HELD_KEY] ?? ''
|
|
350
|
-
|
|
351
|
-
set_held_mark(HELD_VALUE)
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
return await run()
|
|
355
|
-
} finally {
|
|
356
|
-
set_held_mark(previous)
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
404
|
// **A command's reservation: the gate's claim, unless a parent already holds the cores**
|
|
361
405
|
// `josh-logic.ts` calls this around every command that declares a weight.
|
|
362
406
|
// Each command then only declares its weight, and no command carries a copy of the claim, run and
|
|
@@ -368,27 +412,32 @@ async function with_command_reservation<T>(
|
|
|
368
412
|
run: () => Promise<T>,
|
|
369
413
|
options: ReserveOptions = {},
|
|
370
414
|
): Promise<T> {
|
|
371
|
-
if (is_held() || unit_worker_share.is_nested_run()) return await run()
|
|
415
|
+
if (held_mark.is_held() || unit_worker_share.is_nested_run()) return await run()
|
|
372
416
|
|
|
373
|
-
return await with_core_reservation(
|
|
417
|
+
return await with_core_reservation(
|
|
418
|
+
weight,
|
|
419
|
+
async (_admission, key) => await held_mark.with_held_mark(run, key),
|
|
420
|
+
options,
|
|
421
|
+
)
|
|
374
422
|
}
|
|
375
423
|
|
|
376
424
|
const core_budget = {
|
|
377
425
|
CORE_WEIGHTS,
|
|
378
|
-
HELD_KEY,
|
|
379
|
-
HELD_VALUE,
|
|
426
|
+
HELD_KEY: held_mark.HELD_KEY,
|
|
427
|
+
HELD_VALUE: held_mark.HELD_VALUE,
|
|
380
428
|
MEMORY_MB,
|
|
381
429
|
RESERVED_PREFIX,
|
|
382
430
|
admitted_keys: core_admission.admitted_keys,
|
|
383
431
|
admitted_load: core_admission.admitted_load,
|
|
432
|
+
budget_waits,
|
|
384
433
|
describe_overflow: core_admission.describe_overflow,
|
|
385
|
-
is_held,
|
|
434
|
+
is_held: held_mark.is_held,
|
|
386
435
|
live_reservations,
|
|
387
436
|
release,
|
|
388
437
|
reserve,
|
|
389
438
|
with_command_reservation,
|
|
390
439
|
with_core_reservation,
|
|
391
|
-
with_held_mark,
|
|
440
|
+
with_held_mark: held_mark.with_held_mark,
|
|
392
441
|
}
|
|
393
442
|
|
|
394
443
|
export type { LiveReservation, Reservation, ReserveOptions }
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { lane_paths } from '#scripts/lane/lane-paths'
|
|
3
|
+
import { core_admission, type LedgerEntry } from './core-admission'
|
|
4
|
+
import type { MachineBudget } from './machine-capacity'
|
|
5
|
+
|
|
6
|
+
// **Why a claim waits, and who is waiting** (joshuafolkken/kit#3943). A claim held back by the budget
|
|
7
|
+
// used to wait in silence: the command printed nothing and no board showed it, so a lane stalled on
|
|
8
|
+
// memory pressure read the same as one stuck. The waiting claim records its reason on its marker and
|
|
9
|
+
// says it once on its own output; `run:board` draws every marker still waiting. Everything here is a
|
|
10
|
+
// pure function of the ledger and the budget it is handed.
|
|
11
|
+
|
|
12
|
+
const WAIT_REASONS = ['cores', 'memory', 'pressure'] as const
|
|
13
|
+
|
|
14
|
+
type WaitReason = (typeof WAIT_REASONS)[number]
|
|
15
|
+
|
|
16
|
+
// The one wording of each reason, shared by the waiting command's line and the board's.
|
|
17
|
+
const REASON_TEXT: Record<WaitReason, string> = {
|
|
18
|
+
cores: 'not enough free cores',
|
|
19
|
+
memory: 'not enough free memory',
|
|
20
|
+
pressure: 'memory under pressure',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// What the board reads off a marker. Every field but the claim time is optional, so a marker an older
|
|
24
|
+
// `josh` wrote still reads, and one that never waited is not drawn.
|
|
25
|
+
interface WhereFields {
|
|
26
|
+
checkout?: string | undefined
|
|
27
|
+
lane?: string | undefined
|
|
28
|
+
lanes_root?: string | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface WaitMarker extends WhereFields {
|
|
32
|
+
claimed_at: number
|
|
33
|
+
admitted_at?: number | undefined
|
|
34
|
+
command?: string | undefined
|
|
35
|
+
wait_reason?: WaitReason | undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Where a waiting claim runs: `lane` is the board's own repository's lane number, which the board
|
|
39
|
+
// links; `elsewhere` names another repository's checkout, which it cannot link. Neither is the board's
|
|
40
|
+
// own main checkout.
|
|
41
|
+
interface BudgetPlace {
|
|
42
|
+
lane: string | undefined
|
|
43
|
+
elsewhere: string | undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface BudgetWait extends BudgetPlace {
|
|
47
|
+
command: string | undefined
|
|
48
|
+
waited_ms: number
|
|
49
|
+
reason: WaitReason
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// **The cores decide first**: a claim that would not fit even with unlimited memory waits for cores;
|
|
53
|
+
// one that fits on cores waits for memory, named as pressure when the reading saw compression or swap.
|
|
54
|
+
function wait_reason(
|
|
55
|
+
key: string,
|
|
56
|
+
reservations: ReadonlyArray<LedgerEntry>,
|
|
57
|
+
budget: MachineBudget,
|
|
58
|
+
): WaitReason {
|
|
59
|
+
const cores_only = { cores: budget.cores, memory_mb: Infinity }
|
|
60
|
+
|
|
61
|
+
if (!core_admission.is_admitted(key, reservations, cores_only)) return 'cores'
|
|
62
|
+
|
|
63
|
+
return budget.memory_pressured === true ? 'pressure' : 'memory'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The line a waiting command prints once, when its wait starts.
|
|
67
|
+
function wait_line(command: string | undefined, reason: WaitReason): string {
|
|
68
|
+
return `⏳ ${command ?? 'a step'} waits for the core budget — ${REASON_TEXT[reason]}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The lanes root a checkout belongs to: a lane's parent, or the root beside a main checkout. Two
|
|
72
|
+
// checkouts of one repository share it; the ledger is machine-wide, so another repository's do not.
|
|
73
|
+
function lanes_root_of(checkout: string): string {
|
|
74
|
+
if (lane_paths.lane_issue_of(checkout) === undefined) return lane_paths.lane_root(checkout)
|
|
75
|
+
|
|
76
|
+
return path.dirname(path.resolve(checkout))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Where a claim runs, read by the claimer under its own environment: `JOSH_LANE_ROOT` is a
|
|
80
|
+
// per-project setting, so a board cannot derive another repository's lanes root from its own.
|
|
81
|
+
function where_of(checkout: string): Required<WhereFields> {
|
|
82
|
+
return { checkout, lane: lane_paths.lane_issue_of(checkout), lanes_root: lanes_root_of(checkout) }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Another repository's checkout is named by its lanes root and lane, or its directory, so its lane
|
|
86
|
+
// number is never linked to the board's repository's issue of that number.
|
|
87
|
+
function place_of(marker: WhereFields, home_root: string): BudgetPlace {
|
|
88
|
+
const { checkout, lane, lanes_root } = marker
|
|
89
|
+
|
|
90
|
+
if (checkout === undefined || lanes_root === undefined || lanes_root === home_root) {
|
|
91
|
+
return { lane, elsewhere: undefined }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const name = lane === undefined ? path.basename(checkout) : `${path.basename(lanes_root)}/${lane}`
|
|
95
|
+
|
|
96
|
+
return { lane: undefined, elsewhere: name }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function wait_of(marker: WaitMarker, now: number, home_root: string): Array<BudgetWait> {
|
|
100
|
+
const { command, wait_reason: reason } = marker
|
|
101
|
+
|
|
102
|
+
if (reason === undefined || marker.admitted_at !== undefined) return []
|
|
103
|
+
|
|
104
|
+
return [{ command, ...place_of(marker, home_root), waited_ms: now - marker.claimed_at, reason }]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The claims still waiting, longest first, placed against the board's checkout `home`.
|
|
108
|
+
function waits_of(
|
|
109
|
+
entries: ReadonlyArray<{ reservation: WaitMarker }>,
|
|
110
|
+
now: number,
|
|
111
|
+
home: string,
|
|
112
|
+
): Array<BudgetWait> {
|
|
113
|
+
const home_root = lanes_root_of(home)
|
|
114
|
+
|
|
115
|
+
return entries
|
|
116
|
+
.flatMap((entry) => wait_of(entry.reservation, now, home_root))
|
|
117
|
+
.toSorted((left, right) => right.waited_ms - left.waited_ms)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const core_wait = {
|
|
121
|
+
REASON_TEXT,
|
|
122
|
+
WAIT_REASONS,
|
|
123
|
+
wait_line,
|
|
124
|
+
wait_reason,
|
|
125
|
+
waits_of,
|
|
126
|
+
where_of,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type { BudgetWait, WaitReason }
|
|
130
|
+
export { core_wait }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { availableParallelism } from 'node:os'
|
|
2
|
+
import { core_admission } from './core-admission'
|
|
3
|
+
import { core_budget, type LiveReservation } from './core-budget'
|
|
4
|
+
import { held_mark } from './held-mark'
|
|
5
|
+
import { machine_capacity, type MachineReading } from './machine-capacity'
|
|
6
|
+
|
|
7
|
+
// The cores a unit suite may open workers on: the core count less the load from outside the core
|
|
8
|
+
// budget's ledger, the figure the ledger admits against (`machine_capacity.machine_budget`). The run
|
|
9
|
+
// count in `unit-worker-share.ts` divides the machine between unit runs; this is what bounds that
|
|
10
|
+
// division when something else holds the cores.
|
|
11
|
+
//
|
|
12
|
+
// Its own module because the reading needs the ledger, and `core-budget.ts` already imports
|
|
13
|
+
// `unit-worker-share.ts` — reading it from there would close a cycle.
|
|
14
|
+
|
|
15
|
+
interface FreeCoresOptions {
|
|
16
|
+
available_cores?: number
|
|
17
|
+
read_machine?: () => Promise<MachineReading>
|
|
18
|
+
directory?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// **The claim a parent command holds for this run is left out of the ledger.** It is admitted before
|
|
22
|
+
// this run starts a worker, so none of its weight is in the reading yet; counting it would subtract
|
|
23
|
+
// it from load it never added and mask that much outside load (`held_mark.held_reservation`).
|
|
24
|
+
function running_claims(directory: string | undefined): Array<LiveReservation> {
|
|
25
|
+
const own = held_mark.held_reservation()
|
|
26
|
+
|
|
27
|
+
return core_budget.live_reservations(directory).filter((entry) => entry.key !== own)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// `undefined` where the machine could not be read — a CI runner, a CPU quota narrower than the host —
|
|
31
|
+
// which `unit_worker_share.cap_to_free` reads as the share as it was.
|
|
32
|
+
async function read(options: FreeCoresOptions = {}): Promise<number | undefined> {
|
|
33
|
+
const reading = await (options.read_machine ?? machine_capacity.read_machine)()
|
|
34
|
+
|
|
35
|
+
if (reading.busy_cores === undefined) return undefined
|
|
36
|
+
|
|
37
|
+
const ledger = core_admission.ledger_load(running_claims(options.directory), Date.now())
|
|
38
|
+
|
|
39
|
+
return machine_capacity.machine_budget(
|
|
40
|
+
options.available_cores ?? availableParallelism(),
|
|
41
|
+
reading,
|
|
42
|
+
ledger,
|
|
43
|
+
).cores
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const free_cores = { read }
|
|
47
|
+
|
|
48
|
+
export type { FreeCoresOptions }
|
|
49
|
+
export { free_cores }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { process_identity } from '#scripts/josh/process-identity'
|
|
2
|
-
import { lane_ledger } from '#scripts/lane/lane-ledger'
|
|
2
|
+
import { lane_ledger, type GateCheckRecord } from '#scripts/lane/lane-ledger'
|
|
3
3
|
import { core_admission } from './core-admission'
|
|
4
4
|
import { core_budget } from './core-budget'
|
|
5
5
|
import { gate_plan } from './gate-plan'
|
|
6
|
-
import type
|
|
6
|
+
import { gate_report, type GateStepResult } from './gate-report'
|
|
7
7
|
import { machine_capacity } from './machine-capacity'
|
|
8
8
|
|
|
9
9
|
// A finished gate's line in the lane ledger: its duration, its verdict, its
|
|
@@ -88,6 +88,15 @@ async function start(ledger_path: string | undefined): Promise<GateLedgerStart>
|
|
|
88
88
|
return { ledger_path, readings, sampler: start_sampler(readings) }
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function check_records(results: ReadonlyArray<GateStepResult>): Array<GateCheckRecord> {
|
|
92
|
+
return results.map((result) => ({
|
|
93
|
+
label: result.label,
|
|
94
|
+
elapsed_ms: result.elapsed_ms,
|
|
95
|
+
is_passed: !gate_report.is_gate_step_failed(result),
|
|
96
|
+
is_reused: result.reused_at !== undefined,
|
|
97
|
+
}))
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
// The caller measures the elapsed time before this, so the end's reading is never part of it.
|
|
92
101
|
async function record(
|
|
93
102
|
begun: GateLedgerStart,
|
|
@@ -101,7 +110,12 @@ async function record(
|
|
|
101
110
|
const unit_ms = results.find((result) => result.label === gate_plan.UNIT_LABEL)?.elapsed_ms
|
|
102
111
|
const external_cores = peak([...begun.readings, await read_external_cores()])
|
|
103
112
|
|
|
104
|
-
await lane_ledger.record_gate(begun.ledger_path, {
|
|
113
|
+
await lane_ledger.record_gate(begun.ledger_path, {
|
|
114
|
+
...outcome,
|
|
115
|
+
unit_ms,
|
|
116
|
+
external_cores,
|
|
117
|
+
checks: check_records(results),
|
|
118
|
+
})
|
|
105
119
|
}
|
|
106
120
|
|
|
107
121
|
const gate_ledger = {
|
|
@@ -51,6 +51,7 @@ const TYPE_CHECK_LABEL = 'check'
|
|
|
51
51
|
const UNIT_LABEL = 'test:unit'
|
|
52
52
|
const BEHAVIOR_LABEL = 'behavior'
|
|
53
53
|
const METRICS_LABEL = 'metrics'
|
|
54
|
+
const EXPORTS_LABEL = 'exports'
|
|
54
55
|
|
|
55
56
|
// The checks whose warnings must survive the green record: eslint (the `lint` step) and svelte-check
|
|
56
57
|
// (the `check` step) both exit 0 while reporting per-tree warnings a developer needs to see again, so
|
|
@@ -106,7 +107,7 @@ const STATIC_CHECKS: ReadonlyArray<GateCheck> = [
|
|
|
106
107
|
// program and walks it for about 5s, and it is over well before lint and the type check. A
|
|
107
108
|
// reservation would only take a place from the four-core CI runner's fan-out and a worker from the
|
|
108
109
|
// unit suite for the whole gate.
|
|
109
|
-
{ label:
|
|
110
|
+
{ label: EXPORTS_LABEL, target: 'exports:unused', reserved_cores: 0 },
|
|
110
111
|
// **The metrics ratchet is kit's own**: it holds kit's repository-wide
|
|
111
112
|
// totals to the merge-base's. It is one in-process eslint pass with only `max-lines` enabled, over
|
|
112
113
|
// in a few seconds, so it reserves no core for the reason the unused-member check above does not.
|
|
@@ -397,6 +398,25 @@ function resolve_gate_plan(
|
|
|
397
398
|
}
|
|
398
399
|
}
|
|
399
400
|
|
|
401
|
+
// The unit cap bounded by the cores free of load from outside the core budget's ledger
|
|
402
|
+
// (`free-cores.ts`), so the run count's share is never wider than what is free. A plan with no unit
|
|
403
|
+
// step carries no cap, and an unread machine leaves the cap as resolved (`unit_worker_share.cap_to_free`).
|
|
404
|
+
function bound_by_free(
|
|
405
|
+
plan: GatePlan,
|
|
406
|
+
available_cores: number,
|
|
407
|
+
free_cores: number | undefined,
|
|
408
|
+
): GatePlan {
|
|
409
|
+
if (!has_unit_check(plan.checks)) return plan
|
|
410
|
+
|
|
411
|
+
const unit_worker_cap = unit_worker_share.cap_to_free(
|
|
412
|
+
plan.unit_worker_cap,
|
|
413
|
+
free_cores,
|
|
414
|
+
available_cores,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
return { ...plan, unit_worker_cap }
|
|
418
|
+
}
|
|
419
|
+
|
|
400
420
|
// Printed once per run, so a gate that was slow on someone else's machine can be read without
|
|
401
421
|
// re-deriving the plan from their core count.
|
|
402
422
|
//
|
|
@@ -440,6 +460,7 @@ function format_gate_plan(
|
|
|
440
460
|
}
|
|
441
461
|
|
|
442
462
|
const gate_plan = {
|
|
463
|
+
EXPORTS_LABEL,
|
|
443
464
|
GATE_CHECKS,
|
|
444
465
|
LINT_LABEL,
|
|
445
466
|
MEASURED_CORES,
|
|
@@ -449,6 +470,7 @@ const gate_plan = {
|
|
|
449
470
|
TYPE_CHECK_LABEL,
|
|
450
471
|
UNIT_LABEL,
|
|
451
472
|
WARNING_CHECKER_LABELS,
|
|
473
|
+
bound_by_free,
|
|
452
474
|
check_memory,
|
|
453
475
|
check_weight,
|
|
454
476
|
direct_unit_memory,
|
|
@@ -28,6 +28,9 @@ interface GateStepResult extends BufferedProcessResult {
|
|
|
28
28
|
command: string
|
|
29
29
|
// Set when the check started past the machine-wide budget at the wait cap.
|
|
30
30
|
budget_note?: string | undefined
|
|
31
|
+
// Set when the check was not started because its inputs match the record taken at this time
|
|
32
|
+
// (`check-reuse.ts`).
|
|
33
|
+
reused_at?: string | undefined
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
function is_gate_step_failed(result: GateStepResult): boolean {
|
|
@@ -98,6 +101,12 @@ function is_unverified_skip(result: GateStepResult): boolean {
|
|
|
98
101
|
)
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
// Whether a passing check said nothing a green record would hide — the condition every record the
|
|
105
|
+
// gate writes is withheld on (`verification-gate.ts` → `record_green_gate`, `check-reuse.ts`).
|
|
106
|
+
function has_nothing_to_say(result: GateStepResult): boolean {
|
|
107
|
+
return !is_unverified_skip(result) && !has_checker_warning(result)
|
|
108
|
+
}
|
|
109
|
+
|
|
101
110
|
// **A check the verdict counts as skipped said nothing but that it checked nothing.** `is_skip_notice`
|
|
102
111
|
// asks whether a check carries a skip notice, which is the right question for printing its body; it is
|
|
103
112
|
// the wrong one for a count, twice over. A check can skip half of
|
|
@@ -154,8 +163,12 @@ function format_seconds(elapsed_ms: number): string {
|
|
|
154
163
|
// and the log exists precisely to be read when the console's copy has been elided.
|
|
155
164
|
function gate_step_header(result: GateStepResult): string {
|
|
156
165
|
const icon = is_gate_step_failed(result) ? FAIL_ICON : PASS_ICON
|
|
166
|
+
const timing =
|
|
167
|
+
result.reused_at === undefined
|
|
168
|
+
? format_seconds(result.elapsed_ms)
|
|
169
|
+
: `reused — inputs unchanged since it passed at ${result.reused_at}`
|
|
157
170
|
|
|
158
|
-
return `${icon} ${result.label} (pnpm ${result.command}) ${
|
|
171
|
+
return `${icon} ${result.label} (pnpm ${result.command}) ${timing}`
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
function print_gate_step(result: GateStepResult, is_verbose: boolean): void {
|
|
@@ -267,8 +280,8 @@ const gate_report = {
|
|
|
267
280
|
format_budget_notes,
|
|
268
281
|
format_failure_actions,
|
|
269
282
|
has_checker_warning,
|
|
283
|
+
has_nothing_to_say,
|
|
270
284
|
is_gate_step_failed,
|
|
271
|
-
is_unverified_skip,
|
|
272
285
|
report_gate_steps,
|
|
273
286
|
}
|
|
274
287
|
|