@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
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
-
import { availableParallelism } from 'node:os'
|
|
3
2
|
import { fileURLToPath } from 'node:url'
|
|
4
3
|
import type { FileMapStamp } from '#scripts/josh/file-map-stamp'
|
|
5
4
|
import { GATE_COMMAND } from '#scripts/josh/josh-command-types'
|
|
6
5
|
import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-composite-arguments'
|
|
7
6
|
import { lane_ledger } from '#scripts/lane/lane-ledger'
|
|
8
|
-
import {
|
|
9
|
-
import { buffered_process, FAIL_EXIT_CODE } from '#scripts/lib/buffered-process'
|
|
7
|
+
import { FAIL_EXIT_CODE } from '#scripts/lib/buffered-process'
|
|
10
8
|
import { review_stamps } from '#scripts/review/review-stamps'
|
|
11
9
|
import { review_tree } from '#scripts/review/review-tree'
|
|
12
10
|
import { unit_worker_share } from '#scripts/test/unit-worker-share'
|
|
13
|
-
import {
|
|
11
|
+
import { check_reuse, type CheckRecords } from './check-reuse'
|
|
12
|
+
import { free_cores } from './free-cores'
|
|
14
13
|
import { gate_ledger, type GateLedgerStart } from './gate-ledger'
|
|
15
14
|
import { gate_plan, type GatePlan } from './gate-plan'
|
|
16
|
-
import { gate_report, type
|
|
15
|
+
import { gate_report, type GateStepResult } from './gate-report'
|
|
16
|
+
import { gate_run_steps, type ReusedChecks } from './gate-run-steps'
|
|
17
17
|
import { gate_skip } from './gate-skip'
|
|
18
18
|
import { build_gate_step, build_gate_steps, UNIT_WORKER_FLAG } from './gate-steps'
|
|
19
19
|
import { gate_tree, type GateTree } from './gate-tree'
|
|
@@ -35,25 +35,6 @@ const { GATE_CHECKS } = gate_plan
|
|
|
35
35
|
|
|
36
36
|
const GATE_TARGETS: ReadonlyArray<string> = GATE_CHECKS.map((check) => check.target)
|
|
37
37
|
|
|
38
|
-
async function run_gate_step(step: GateStep): Promise<GateStepResult> {
|
|
39
|
-
if (step.skip_reason !== undefined) {
|
|
40
|
-
return {
|
|
41
|
-
label: step.label,
|
|
42
|
-
command: step.command_args.join(' '),
|
|
43
|
-
output: project_checks.skip_notice('check', step.skip_reason),
|
|
44
|
-
exit_code: 0,
|
|
45
|
-
elapsed_ms: 0,
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const result = await buffered_process.run_buffered_process(
|
|
50
|
-
step.command_args,
|
|
51
|
-
step.cwd === undefined ? {} : { cwd: step.cwd },
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
return { label: step.label, command: step.command_args.join(' '), ...result }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
38
|
// The record `josh review:brief` reads, written only on a fully green run and only with the tree it
|
|
58
39
|
// was green on. `/code-review` runs in a forked process that reads none of
|
|
59
40
|
// this repository's documents, so "the unit tests already passed" reaches it only if the invocation
|
|
@@ -65,7 +46,7 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
|
|
|
65
46
|
// visible on the console, and a record erasing it would have the brief tell a review agent not to
|
|
66
47
|
// re-run tests that never ran. A skip of a tool the project does not use (`NOT_USED_MARKER` — eslint
|
|
67
48
|
// with no ESLint configuration) is not one: nothing went unverified, so it withholds nothing
|
|
68
|
-
// (`gate_report.
|
|
49
|
+
// (`gate_report.has_nothing_to_say`). A step that passed **with a checker's warnings** is withheld for the
|
|
69
50
|
// same reason one place further on: since the record is reused instead of the checks being re-run, a
|
|
70
51
|
// warning printed once would never be printed again on that tree, and hiding an eslint or
|
|
71
52
|
// svelte-check finding is the same failure as hiding a skip. It is `has_checker_warning`, not
|
|
@@ -81,17 +62,13 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
|
|
|
81
62
|
//
|
|
82
63
|
// The destination is a parameter so a test can exercise the record without overwriting the one a
|
|
83
64
|
// real run may be relying on — `josh gate` and `josh review:brief` share one path by design.
|
|
84
|
-
function has_nothing_to_say(result: GateStepResult): boolean {
|
|
85
|
-
return !gate_report.is_unverified_skip(result) && !gate_report.has_checker_warning(result)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
65
|
async function record_green_gate(
|
|
89
66
|
results: ReadonlyArray<GateStepResult>,
|
|
90
67
|
before: Record<string, string>,
|
|
91
68
|
target?: string,
|
|
92
69
|
base?: string,
|
|
93
70
|
): Promise<void> {
|
|
94
|
-
if (results.some((result) => !has_nothing_to_say(result))) return
|
|
71
|
+
if (results.some((result) => !gate_report.has_nothing_to_say(result))) return
|
|
95
72
|
|
|
96
73
|
try {
|
|
97
74
|
const after = await review_tree.read_changed_tree()
|
|
@@ -153,94 +130,6 @@ async function with_gate_marker<T>(
|
|
|
153
130
|
}
|
|
154
131
|
}
|
|
155
132
|
|
|
156
|
-
// This gate's share of the machine, resolved once at the gate's entry and carried down as one value.
|
|
157
|
-
// `available_cores` is the budget every check reserves against; `is_reserved`
|
|
158
|
-
// is false for a gate nested inside another gate's unit suite — this repository's own gate tests — so
|
|
159
|
-
// the outer gate reserves and every gate beneath it does not. Captured before this gate sets
|
|
160
|
-
// `JOSH_UNIT_RUN_MARKED`, which is why it is read at entry rather than re-derived here.
|
|
161
|
-
interface BudgetContext {
|
|
162
|
-
available_cores: number
|
|
163
|
-
is_reserved: boolean
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function default_budget(): BudgetContext {
|
|
167
|
-
return {
|
|
168
|
-
available_cores: availableParallelism(),
|
|
169
|
-
is_reserved: !unit_worker_share.is_nested_run() && !core_budget.is_held(),
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// A gate step paired with the cores it reserves from the machine-wide budget while it runs.
|
|
174
|
-
// The weight travels with the step so `run_gate_steps` never has to re-pair a
|
|
175
|
-
// step with its check by index.
|
|
176
|
-
interface ReservedStep {
|
|
177
|
-
step: GateStep
|
|
178
|
-
weight: number
|
|
179
|
-
memory_mb: number
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function build_reserved_steps(
|
|
183
|
-
start_directory: string,
|
|
184
|
-
plan: GatePlan,
|
|
185
|
-
available_cores: number,
|
|
186
|
-
): Promise<ReadonlyArray<ReservedStep>> {
|
|
187
|
-
return await Promise.all(
|
|
188
|
-
plan.checks.map(async (check) => ({
|
|
189
|
-
step: await build_gate_step(check, start_directory, plan),
|
|
190
|
-
weight: gate_plan.check_weight(check, plan, available_cores),
|
|
191
|
-
memory_mb: gate_plan.check_memory(check, plan, available_cores),
|
|
192
|
-
})),
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// **A nested gate takes no reservation, and the budget is what makes that necessary rather than
|
|
197
|
-
// merely tidy**. The unit step spawns the suite, which runs this repository's
|
|
198
|
-
// own gate tests; each of those is a gate inside the outer gate's unit reservation, and a place it
|
|
199
|
-
// claimed would wait for cores the outer gate cannot free until the suite — this test included —
|
|
200
|
-
// finishes.
|
|
201
|
-
async function run_reserved_step(
|
|
202
|
-
reserved: ReservedStep,
|
|
203
|
-
budget: BudgetContext,
|
|
204
|
-
): Promise<GateStepResult> {
|
|
205
|
-
if (!budget.is_reserved) return await run_gate_step(reserved.step)
|
|
206
|
-
|
|
207
|
-
return await core_budget.with_core_reservation(
|
|
208
|
-
reserved.weight,
|
|
209
|
-
async (admission) => ({
|
|
210
|
-
...(await run_gate_step(reserved.step)),
|
|
211
|
-
budget_note: core_budget.describe_overflow(admission),
|
|
212
|
-
}),
|
|
213
|
-
{ budget: budget.available_cores, memory_mb: reserved.memory_mb },
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// `bounded_pool` rather than a bare `Promise.all`, so the plan's `concurrency` is what decides how
|
|
218
|
-
// many run at once. **The all-failures-in-one-pass property survives the
|
|
219
|
-
// change** because no check ever rejects: `buffered_process` reports a non-zero exit as a value, so
|
|
220
|
-
// the pool's first-failure abort — written for callers that spawn real Claude sessions — never
|
|
221
|
-
// fires here and every queued check still runs. Results come back in input order however they
|
|
222
|
-
// finished, which is what keeps the printed sections in declaration order. Each step first claims its
|
|
223
|
-
// place in the machine-wide budget and waits while the budget is full.
|
|
224
|
-
async function run_gate_steps(
|
|
225
|
-
plan: GatePlan,
|
|
226
|
-
budget: BudgetContext = default_budget(),
|
|
227
|
-
): Promise<ReadonlyArray<GateStepResult>> {
|
|
228
|
-
const reserved = await build_reserved_steps(process.cwd(), plan, budget.available_cores)
|
|
229
|
-
|
|
230
|
-
// **The checks run under the held mark, so a check's own dispatch claims nothing**.
|
|
231
|
-
// Each check is a `pnpm josh <target>` child, and a target that declares
|
|
232
|
-
// a weight would otherwise claim a second place for the cores reserved for it here. A nested gate
|
|
233
|
-
// sets the mark too, since its checks are covered by the outer gate's reservation.
|
|
234
|
-
return await core_budget.with_held_mark(
|
|
235
|
-
async () =>
|
|
236
|
-
await bounded_pool.bounded_map(
|
|
237
|
-
reserved,
|
|
238
|
-
plan.concurrency,
|
|
239
|
-
async (entry) => await run_reserved_step(entry, budget),
|
|
240
|
-
),
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
133
|
// **Both markers are claims about the unit suite, so a gate that is not running it makes neither**.
|
|
245
134
|
// The in-flight marker tells the next `josh review:brief` that a gate is
|
|
246
135
|
// covering this tree right now, which is what stops a review agent re-running the suite; the
|
|
@@ -276,15 +165,21 @@ async function run_marked_gate<T>(
|
|
|
276
165
|
// The concurrent-run count is read here rather than inside `gate-plan.ts` for the reason the core
|
|
277
166
|
// count already is: that module stays a pure function of its inputs, and the machine is asked once and
|
|
278
167
|
// handed to both calls. Counted *before* the unit step writes its own marker, so `+ 1` is this gate.
|
|
279
|
-
|
|
168
|
+
// `free` bounds the unit cap the run count produced (`gate_plan.bound_by_free`).
|
|
169
|
+
function announce_gate_plan(
|
|
170
|
+
is_unit_included: boolean,
|
|
171
|
+
available_cores: number,
|
|
172
|
+
free: number | undefined,
|
|
173
|
+
): GatePlan {
|
|
280
174
|
const concurrent_runs = unit_worker_share.live_run_count() + unit_worker_share.SOLO_RUNS
|
|
281
175
|
const is_kit_repository = project_checks.is_kit_repository(process.cwd())
|
|
282
|
-
const
|
|
176
|
+
const resolved = gate_plan.resolve_gate_plan(
|
|
283
177
|
available_cores,
|
|
284
178
|
concurrent_runs,
|
|
285
179
|
is_unit_included,
|
|
286
180
|
is_kit_repository,
|
|
287
181
|
)
|
|
182
|
+
const plan = gate_plan.bound_by_free(resolved, available_cores, free)
|
|
288
183
|
|
|
289
184
|
process.stdout.write(`${gate_plan.format_gate_plan(plan, available_cores, concurrent_runs)}\n`)
|
|
290
185
|
|
|
@@ -319,6 +214,35 @@ interface GateOptions {
|
|
|
319
214
|
// The lane ledger a finished gate's duration is appended to. Only the CLI
|
|
320
215
|
// entry resolves it, so a suite driving the gate never writes a measurement of checks that never ran.
|
|
321
216
|
ledger_path?: string | undefined
|
|
217
|
+
// Where each check's own green record lives (`check-reuse.ts`). Only the CLI entry resolves it, for
|
|
218
|
+
// the ledger's reason: a suite whose checks never ran must not record them green on the live tree.
|
|
219
|
+
check_records?: CheckRecords | undefined
|
|
220
|
+
// The cores free of load from outside the core budget's ledger (`free-cores.ts`). Only the CLI entry
|
|
221
|
+
// resolves it, for the ledger's reason: a suite driving the gate never reads the machine, so its plan
|
|
222
|
+
// does not depend on what else happened to be running.
|
|
223
|
+
read_free_cores?: ((available_cores: number) => Promise<number | undefined>) | undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// **Per-check records belong to the full local gate alone**: CI's `--no-unit` gate is the safety net
|
|
227
|
+
// under the local reuse and stays as it was, so it neither reads nor writes one.
|
|
228
|
+
function check_records_of(plan: GatePlan, options: GateOptions): CheckRecords | undefined {
|
|
229
|
+
return gate_plan.has_unit_check(plan.checks) ? options.check_records : undefined
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function checks_to_reuse(
|
|
233
|
+
plan: GatePlan,
|
|
234
|
+
tree: GateTree,
|
|
235
|
+
options: GateOptions,
|
|
236
|
+
): Promise<ReusedChecks> {
|
|
237
|
+
const records = check_records_of(plan, options)
|
|
238
|
+
|
|
239
|
+
if (records === undefined || options.is_forced === true) return new Map()
|
|
240
|
+
|
|
241
|
+
return await check_reuse.reusable_checks(
|
|
242
|
+
plan.checks.map((check) => check.label),
|
|
243
|
+
tree,
|
|
244
|
+
records,
|
|
245
|
+
)
|
|
322
246
|
}
|
|
323
247
|
|
|
324
248
|
// **The gate refuses to start when the scoped pair has not been green on this tree**
|
|
@@ -374,6 +298,10 @@ async function settle_gate(
|
|
|
374
298
|
|
|
375
299
|
await gate_ledger.record(options.ledger, results, { elapsed_ms, is_passed })
|
|
376
300
|
|
|
301
|
+
const records = check_records_of(plan, options)
|
|
302
|
+
|
|
303
|
+
if (records !== undefined) await check_reuse.record_green_checks(results, tree, records)
|
|
304
|
+
|
|
377
305
|
if (!is_passed) return FAIL_EXIT_CODE
|
|
378
306
|
|
|
379
307
|
await record_whole_gate(plan, results, tree, options)
|
|
@@ -396,8 +324,11 @@ async function run_checked_gate(
|
|
|
396
324
|
const ledger = await gate_ledger.start(options.ledger_path)
|
|
397
325
|
// Resolved at the gate's entry, before this gate sets `JOSH_UNIT_RUN_MARKED`, so a gate nested in the
|
|
398
326
|
// unit suite reads the flag as set and reserves nothing.
|
|
399
|
-
const budget = default_budget()
|
|
400
|
-
|
|
327
|
+
const budget = gate_run_steps.default_budget()
|
|
328
|
+
// Read before the count for the ledger's reason: the reading waits out a sample window.
|
|
329
|
+
const free = await options.read_free_cores?.(budget.available_cores)
|
|
330
|
+
const plan = announce_gate_plan(is_unit_included, budget.available_cores, free)
|
|
331
|
+
const reused = await checks_to_reuse(plan, tree, options)
|
|
401
332
|
|
|
402
333
|
// **The gate holds the unit-run marker for its whole run, not just its unit step.** The step that
|
|
403
334
|
// would write it is a subprocess started a second or so after the count above, so two lanes launched
|
|
@@ -406,7 +337,7 @@ async function run_checked_gate(
|
|
|
406
337
|
// the next lane asks; the guard inside the spawned `josh test:unit` sees the handoff and adds no
|
|
407
338
|
// second marker for the same run.
|
|
408
339
|
async function run_and_settle(): Promise<number> {
|
|
409
|
-
const results = await run_gate_steps(plan, budget)
|
|
340
|
+
const results = await gate_run_steps.run_gate_steps(plan, budget, reused)
|
|
410
341
|
|
|
411
342
|
return await settle_gate(plan, results, tree, { ...options, started_at, ledger })
|
|
412
343
|
}
|
|
@@ -506,6 +437,8 @@ async function run_gate_command(
|
|
|
506
437
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
507
438
|
process.exitCode = await run_gate_command(process.argv.slice(FIRST_ARGUMENT_INDEX), {
|
|
508
439
|
ledger_path: await lane_ledger.target(),
|
|
440
|
+
check_records: check_reuse.DEFAULT_RECORDS,
|
|
441
|
+
read_free_cores: async (available_cores) => await free_cores.read({ available_cores }),
|
|
509
442
|
})
|
|
510
443
|
}
|
|
511
444
|
|
|
@@ -519,11 +452,11 @@ const verification_gate = {
|
|
|
519
452
|
clear_gate_running,
|
|
520
453
|
mark_gate_running,
|
|
521
454
|
record_green_gate,
|
|
522
|
-
run_gate_steps,
|
|
455
|
+
run_gate_steps: gate_run_steps.run_gate_steps,
|
|
523
456
|
scoped_precheck_refusal,
|
|
524
457
|
with_gate_marker,
|
|
525
458
|
run_gate_command,
|
|
526
|
-
run_gate_step,
|
|
459
|
+
run_gate_step: gate_run_steps.run_gate_step,
|
|
527
460
|
run_verification_gate,
|
|
528
461
|
}
|
|
529
462
|
|
|
@@ -36,6 +36,7 @@ const LABEL_NAMES_FILTER = '.[].name'
|
|
|
36
36
|
const LABELS_SEGMENT = '/labels'
|
|
37
37
|
const COMMENTS_SEGMENT = '/comments'
|
|
38
38
|
const CLOSED_STATE = 'closed'
|
|
39
|
+
const NOT_PLANNED_REASON = 'not_planned'
|
|
39
40
|
const COLOR_HASH = '#'
|
|
40
41
|
const UNPROCESSABLE_STATUS = 422
|
|
41
42
|
const ALREADY_EXISTS_CODE = 'already_exists'
|
|
@@ -99,17 +100,37 @@ async function issue_try_comment(issue_number: string, body: string): Promise<bo
|
|
|
99
100
|
// It is a required parameter that may be `undefined` rather than an optional one: skipping the
|
|
100
101
|
// announcement is a decision the caller makes from what it read, never something a call site can
|
|
101
102
|
// fall into by leaving an argument off.
|
|
102
|
-
async function
|
|
103
|
+
async function close_with(
|
|
104
|
+
issue_number: string,
|
|
105
|
+
comment: string | undefined,
|
|
106
|
+
state: Readonly<Record<string, string>>,
|
|
107
|
+
): Promise<boolean> {
|
|
103
108
|
return await did_write_succeed(async () => {
|
|
104
109
|
if (comment !== undefined) await issue_comment(issue_number, comment)
|
|
105
110
|
await git_gh_exec.exec_gh_api({
|
|
106
111
|
path: git_gh_api_path.issue_api_path(issue_number),
|
|
107
112
|
method: PATCH_METHOD,
|
|
108
|
-
body: JSON.stringify(
|
|
113
|
+
body: JSON.stringify(state),
|
|
109
114
|
})
|
|
110
115
|
})
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
async function issue_close(issue_number: string, comment: string | undefined): Promise<boolean> {
|
|
119
|
+
return await close_with(issue_number, comment, { state: CLOSED_STATE })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The same close as "not planned" — the reason `issue-merged.ts` reads as no merge, so a closed issue
|
|
123
|
+
// that was withdrawn is never taken for one whose work landed.
|
|
124
|
+
async function issue_close_not_planned(
|
|
125
|
+
issue_number: string,
|
|
126
|
+
comment: string | undefined,
|
|
127
|
+
): Promise<boolean> {
|
|
128
|
+
return await close_with(issue_number, comment, {
|
|
129
|
+
state: CLOSED_STATE,
|
|
130
|
+
state_reason: NOT_PLANNED_REASON,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
113
134
|
// REST answers 422 `{"resource":"Label","code":"invalid","field":"color"}` for a color carrying a
|
|
114
135
|
// leading `#`. The one caller passes the hash form (`EPIC_LABEL_COLOR`), so dropping it here is what
|
|
115
136
|
// keeps that call site unchanged.
|
|
@@ -336,6 +357,7 @@ const git_gh_issue_write = {
|
|
|
336
357
|
issue_comment,
|
|
337
358
|
issue_try_comment,
|
|
338
359
|
issue_close,
|
|
360
|
+
issue_close_not_planned,
|
|
339
361
|
label_ensure,
|
|
340
362
|
issue_create_request,
|
|
341
363
|
issue_create_with_label,
|
package/scripts/gh/git-pr.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { issue_cite } from '#scripts/issue/issue-cite'
|
|
|
5
5
|
import { session_cite } from '#scripts/issue/session-cite'
|
|
6
6
|
import { animation_helpers, type AnimationOptions } from '#scripts/lib/animation-helpers'
|
|
7
7
|
import { json_value } from '#scripts/lib/json-value'
|
|
8
|
+
import { acceptance_map } from '#scripts/review/acceptance-map'
|
|
8
9
|
import { git_gh_command } from './git-gh-command'
|
|
9
10
|
import { git_pr_error } from './git-pr-error'
|
|
10
11
|
import { git_pr_messages } from './git-pr-messages'
|
|
@@ -172,6 +173,17 @@ function build_body(issue_info: IssueInfo, extra_body?: string): string {
|
|
|
172
173
|
return `${closes}\n\n${extra_body}`
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
// A body without the acceptance-criteria table gets one pending row per criterion, so the author fills
|
|
177
|
+
// in rows that already carry their identifiers — `josh followup` refuses the merge until they do.
|
|
178
|
+
async function body_with_map(issue_info: IssueInfo, extra_body?: string): Promise<string> {
|
|
179
|
+
const body = build_body(issue_info, extra_body)
|
|
180
|
+
|
|
181
|
+
if (acceptance_map.has_map(body)) return body
|
|
182
|
+
const map = acceptance_map.skeleton(await git_gh_command.issue_get_body(issue_info.number))
|
|
183
|
+
|
|
184
|
+
return map === undefined ? body : `${body}\n\n${map}`
|
|
185
|
+
}
|
|
186
|
+
|
|
175
187
|
async function existing_pr_label(branch_name: string): Promise<ReleaseClassification | undefined> {
|
|
176
188
|
if (
|
|
177
189
|
!(await git_gh_command.pr_exists(branch_name)) ||
|
|
@@ -190,7 +202,7 @@ async function create_for_issue(
|
|
|
190
202
|
): Promise<void> {
|
|
191
203
|
await create(
|
|
192
204
|
build_title(issue_info),
|
|
193
|
-
|
|
205
|
+
await body_with_map(issue_info, extra_body),
|
|
194
206
|
issue_info.branch_name,
|
|
195
207
|
{
|
|
196
208
|
label,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { SUITE_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
import { git_command } from './git-command'
|
|
4
|
+
import { git_spawn } from './git-spawn'
|
|
5
|
+
|
|
6
|
+
// A merge of the default branch that stops only on generated files is finished here rather than by a
|
|
7
|
+
// resumed session: each such file is the output of a command, so running that command on the merged
|
|
8
|
+
// tree writes the one correct resolution, with no conflict marker for anyone to read.
|
|
9
|
+
//
|
|
10
|
+
// **This table is the single list of what is resolved that way** — a file a person writes never
|
|
11
|
+
// belongs in it, because regenerating would discard that person's side of the merge. A conflict on any
|
|
12
|
+
// path not listed here still stops as `conflict`. A file belongs here only once its committed content
|
|
13
|
+
// is exactly what its command writes: `scripts/test/pure-files.ts` is not, so regenerating it would
|
|
14
|
+
// rewrite far more than the conflict.
|
|
15
|
+
|
|
16
|
+
interface RegeneratedFile {
|
|
17
|
+
path: string
|
|
18
|
+
command: ReadonlyArray<string>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const REGENERATED_FILES: ReadonlyArray<RegeneratedFile> = [
|
|
22
|
+
{
|
|
23
|
+
path: 'docs/josh-command-catalog.md',
|
|
24
|
+
command: ['pnpm', 'exec', 'tsx', 'scripts/document/generate-catalog.ts'],
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
function regenerated_for(
|
|
29
|
+
files: ReadonlyArray<string>,
|
|
30
|
+
table: ReadonlyArray<RegeneratedFile>,
|
|
31
|
+
): ReadonlyArray<RegeneratedFile> {
|
|
32
|
+
const entries = files.map((file) => table.find((entry) => entry.path === file))
|
|
33
|
+
|
|
34
|
+
return entries.every((entry) => entry !== undefined) ? entries : []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Each generator writes only its own file, so they run side by side.
|
|
38
|
+
async function regenerate(entries: ReadonlyArray<RegeneratedFile>, cwd: string): Promise<void> {
|
|
39
|
+
await Promise.all(
|
|
40
|
+
entries.map(async (entry) => {
|
|
41
|
+
const [binary = '', ...command_arguments] = entry.command
|
|
42
|
+
|
|
43
|
+
await execa(binary, command_arguments, { cwd })
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// `--no-edit` keeps the message the merge was started with — the one carrying the issue number the
|
|
49
|
+
// `commit-msg` hook asks for.
|
|
50
|
+
async function conclude_merge(entries: ReadonlyArray<RegeneratedFile>): Promise<void> {
|
|
51
|
+
await git_spawn.with_output('add', ['--', ...entries.map((entry) => entry.path)])
|
|
52
|
+
await git_spawn.with_output('commit', ['--no-edit'], { timeout_ms: SUITE_TIMEOUT_MS })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Whether the merge that stopped on `files` was finished. A command that fails leaves the merge in
|
|
56
|
+
// progress and answers `false`, so the stop is the `conflict` it would have been without this table.
|
|
57
|
+
async function resolve(
|
|
58
|
+
files: ReadonlyArray<string>,
|
|
59
|
+
table: ReadonlyArray<RegeneratedFile> = REGENERATED_FILES,
|
|
60
|
+
): Promise<boolean> {
|
|
61
|
+
const entries = regenerated_for(files, table)
|
|
62
|
+
|
|
63
|
+
if (entries.length === 0) return false
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await regenerate(entries, await git_command.repository_root())
|
|
67
|
+
await conclude_merge(entries)
|
|
68
|
+
} catch {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const main_merge_regenerate = { REGENERATED_FILES, resolve }
|
|
76
|
+
|
|
77
|
+
export type { RegeneratedFile }
|
|
78
|
+
export { main_merge_regenerate }
|
|
@@ -6,6 +6,7 @@ import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-c
|
|
|
6
6
|
import { error_text } from '#scripts/lib/error-message'
|
|
7
7
|
import { git_command } from './git-command'
|
|
8
8
|
import { main_merge_guard } from './main-merge-guard'
|
|
9
|
+
import { main_merge_regenerate } from './main-merge-regenerate'
|
|
9
10
|
|
|
10
11
|
// `josh main:merge` (`josh mm`) — bring the default branch into the branch this checkout is on.
|
|
11
12
|
//
|
|
@@ -62,18 +63,26 @@ interface MergeTarget {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
// A failure that left no unmerged path — a remote that cannot be reached, a hook that refused — is not
|
|
65
|
-
// a conflict, so it is rethrown rather than reported as one with nothing to resolve.
|
|
66
|
+
// a conflict, so it is rethrown rather than reported as one with nothing to resolve. A conflict only on
|
|
67
|
+
// generated files is regenerated and committed (`main-merge-regenerate.ts`), so it answers `undefined`
|
|
68
|
+
// and the merge reads as finished.
|
|
69
|
+
async function unresolved_conflict(error: unknown): Promise<MergeOutcome | undefined> {
|
|
70
|
+
const files = main_merge_guard.unmerged_paths(await git_command.status())
|
|
71
|
+
|
|
72
|
+
if (files.length === 0) throw error
|
|
73
|
+
|
|
74
|
+
return (await main_merge_regenerate.resolve(files)) ? undefined : { kind: 'conflict', files }
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
async function merge_into(target: MergeTarget): Promise<MergeOutcome> {
|
|
67
78
|
const { default_branch, current_branch } = target
|
|
68
79
|
|
|
69
80
|
try {
|
|
70
81
|
await git_command.merge_branch(default_branch, merge_message(default_branch, current_branch))
|
|
71
82
|
} catch (error) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
if (files.length === 0) throw error
|
|
83
|
+
const conflict = await unresolved_conflict(error)
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
if (conflict !== undefined) return conflict
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
return { kind: target.is_current ? 'current' : 'merged', branch: default_branch }
|
|
@@ -54,16 +54,14 @@ function batching_mode(): LaneGuardMode {
|
|
|
54
54
|
return lane_guard_policy.mode_here('batching')
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// A guard the enumeration turns fully `off` says nothing at all.
|
|
58
|
-
//
|
|
59
|
-
// `off` is skipped. On the main line the mode is `refuse`.
|
|
57
|
+
// A guard the enumeration turns fully `off` says nothing at all. The lane-child mode and its reason
|
|
58
|
+
// live in `lane-guard-policy.ts`; on the main line the mode is `refuse`.
|
|
60
59
|
function is_candidate(call: GuardedCall): boolean {
|
|
61
60
|
return is_batch_candidate(call) && batching_mode() !== 'off'
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
// A refusable call is refused only where the mode is `refuse`.
|
|
65
|
-
//
|
|
66
|
-
// because a refusal ends a headless child's turn (kit#2138).
|
|
63
|
+
// A refusable call is refused only where the mode is `refuse`. Under any other mode it is withheld the
|
|
64
|
+
// refusal; why a lane child takes that mode is its `because` in `lane-guard-policy.ts`.
|
|
67
65
|
function should_block_here(tail: string, call: GuardedCall, refused_at_ms: number): boolean {
|
|
68
66
|
if (batching_mode() !== 'refuse') return false
|
|
69
67
|
|
|
@@ -71,8 +69,7 @@ function should_block_here(tail: string, call: GuardedCall, refused_at_ms: numbe
|
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
// The whole-file write earns a notice on every run. The second branch turns a
|
|
74
|
-
// refusable call into a notice under a `notice`-mode guard
|
|
75
|
-
// child again since kit#2276. Both read the notice's own record, so a notice never spends the refusal's
|
|
72
|
+
// refusable call into a notice under a `notice`-mode guard. Both read the notice's own record, so a notice never spends the refusal's
|
|
76
73
|
// stamp and re-fires on the notice's own `NOTICE_REFIRE_EVERY` cadence.
|
|
77
74
|
function should_notify_here(tail: string, call: GuardedCall, notified_at_ms: number): boolean {
|
|
78
75
|
if (time_batch_guard.should_notify(tail, call, notified_at_ms)) return true
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { core_budget } from '#scripts/gate/core-budget'
|
|
4
|
+
import {
|
|
5
|
+
CSPELL_COMMIT_CACHE_FLAGS,
|
|
6
|
+
ESLINT_COMMIT_CACHE_FLAGS,
|
|
7
|
+
PRETTIER_CACHE_FLAGS,
|
|
8
|
+
} from '#scripts/josh/josh-command-types'
|
|
9
|
+
import { resolve_spawn_exit } from '#scripts/lib/spawn-exit'
|
|
10
|
+
import { execa } from 'execa'
|
|
11
|
+
|
|
12
|
+
// The pre-commit hook's cspell, prettier and eslint runs over the staged files. They go through here
|
|
13
|
+
// rather than a bare `pnpm exec` in `lefthook/base.yml` so each cache location comes from the
|
|
14
|
+
// constant its ignore rules are asserted against (joshuafolkken/kit#3948) — a YAML copy would keep
|
|
15
|
+
// writing the old file the day that constant moves.
|
|
16
|
+
//
|
|
17
|
+
// cspell and eslint write the hook's own files: a staged-files cspell run prunes every entry it did
|
|
18
|
+
// not visit, and `--quiet` stores eslint reports without their warnings. Prettier does neither, so
|
|
19
|
+
// it shares the gate's file and a commit right after a green gate checks nothing twice.
|
|
20
|
+
|
|
21
|
+
const ARGV_OFFSET = 2
|
|
22
|
+
const USAGE_ERROR_EXIT_CODE = 2
|
|
23
|
+
const PNPM = 'pnpm'
|
|
24
|
+
|
|
25
|
+
const TOOL_ARGUMENTS: Readonly<Record<string, ReadonlyArray<string>>> = {
|
|
26
|
+
cspell: [
|
|
27
|
+
'exec',
|
|
28
|
+
'cspell',
|
|
29
|
+
'lint',
|
|
30
|
+
'--no-must-find-files',
|
|
31
|
+
'--no-progress',
|
|
32
|
+
...CSPELL_COMMIT_CACHE_FLAGS,
|
|
33
|
+
],
|
|
34
|
+
prettier: ['exec', 'prettier', '--check', '--ignore-unknown', ...PRETTIER_CACHE_FLAGS],
|
|
35
|
+
eslint: ['exec', 'eslint', '--quiet', ...ESLINT_COMMIT_CACHE_FLAGS],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// **Each tool claims its place in the core budget** (joshuafolkken/kit#3944): lefthook starts the three
|
|
39
|
+
// in parallel, and off the ledger every other lane's admission saw them only as foreign load. Each is
|
|
40
|
+
// one process over the staged files — eslint over 40 of them took 16.9s of CPU over 14.0s of wall at
|
|
41
|
+
// 1,225 MB, the one-process shape the whole-tree scan's weight already names.
|
|
42
|
+
// The key is the one `CORE_WEIGHTS` and `MEMORY_MB` both declare the tool under.
|
|
43
|
+
const TOOL_BUDGETS: Readonly<Record<string, 'spell_check' | 'format' | 'eslint_scan'>> = {
|
|
44
|
+
cspell: 'spell_check',
|
|
45
|
+
prettier: 'format',
|
|
46
|
+
eslint: 'eslint_scan',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const USAGE = `Usage: josh pre-commit-lint <${Object.keys(TOOL_ARGUMENTS).join('|')}> [files...]`
|
|
50
|
+
|
|
51
|
+
function build_arguments(
|
|
52
|
+
tool: string,
|
|
53
|
+
staged_files: ReadonlyArray<string>,
|
|
54
|
+
): ReadonlyArray<string> | undefined {
|
|
55
|
+
const tool_arguments = TOOL_ARGUMENTS[tool]
|
|
56
|
+
if (tool_arguments === undefined) return undefined
|
|
57
|
+
|
|
58
|
+
return [...tool_arguments, ...staged_files]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function run_tool(command_arguments: ReadonlyArray<string>): Promise<number> {
|
|
62
|
+
const result = await execa(PNPM, command_arguments, { stdio: 'inherit', reject: false })
|
|
63
|
+
|
|
64
|
+
return resolve_spawn_exit(PNPM, result)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
68
|
+
const [tool = '', ...staged_files] = argv
|
|
69
|
+
const command_arguments = build_arguments(tool, staged_files)
|
|
70
|
+
const budget = TOOL_BUDGETS[tool]
|
|
71
|
+
|
|
72
|
+
if (command_arguments === undefined || budget === undefined) {
|
|
73
|
+
console.error(USAGE)
|
|
74
|
+
|
|
75
|
+
return USAGE_ERROR_EXIT_CODE
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return await core_budget.with_command_reservation(
|
|
79
|
+
core_budget.CORE_WEIGHTS[budget],
|
|
80
|
+
async () => await run_tool(command_arguments),
|
|
81
|
+
{ memory_mb: core_budget.MEMORY_MB[budget], command: `pre-commit ${tool}` },
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
86
|
+
process.exitCode = await run(process.argv.slice(ARGV_OFFSET))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const pre_commit_lint = { build_arguments, run, USAGE_ERROR_EXIT_CODE }
|
|
90
|
+
|
|
91
|
+
export { pre_commit_lint }
|
|
@@ -133,7 +133,7 @@ async function run_reserved_suite(vitest_arguments: ReadonlyArray<string>): Prom
|
|
|
133
133
|
return await core_budget.with_command_reservation(
|
|
134
134
|
gate_plan.direct_unit_weight(),
|
|
135
135
|
async () => await test_unit_guard.run_guarded_unit(process.cwd(), vitest_arguments),
|
|
136
|
-
{ memory_mb: gate_plan.direct_unit_memory() },
|
|
136
|
+
{ memory_mb: gate_plan.direct_unit_memory(), command: 'pre-push test:unit' },
|
|
137
137
|
)
|
|
138
138
|
}
|
|
139
139
|
|