@joshuafolkken/kit 1.1178.0 → 1.1240.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/.claude-plugin/plugin.json +1 -1
- package/.claude/settings.json +2 -2
- package/.claude/skills/dependency-update/SKILL.md +4 -8
- package/.claude/skills/epic-commands/SKILL.md +2 -2
- package/.claude/skills/epic-commands/epic-bundle.md +1 -1
- package/.claude/skills/workflow-commands/SKILL.md +2 -4
- package/.claude/skills/workflow-commands/backlogrun-child.md +6 -4
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +7 -3
- package/.claude/skills/workflow-commands/backlogrun-park.md +23 -2
- package/.claude/skills/workflow-commands/backlogrun-progress.md +9 -4
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +21 -13
- package/.claude/skills/workflow-commands/chain-rule.md +1 -1
- package/.claude/skills/workflow-commands/delegation.md +2 -2
- package/.claude/skills/workflow-commands/followup.md +10 -0
- package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +1 -1
- package/.claude/skills/workflow-commands/issue-scout.md +1 -1
- package/.claude/skills/workflow-commands/kickoff.md +2 -1
- package/.claude/skills/workflow-commands/latest-gate.md +1 -1
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -3
- package/.claude/skills/workflow-commands/pre-gate-cut.md +6 -1
- package/.claude/skills/workflow-commands/prerequisite.md +2 -2
- package/.claude/skills/workflow-commands/progress-watcher.md +1 -1
- package/.claude/skills/workflow-commands/working-tree-hold.md +1 -1
- package/.codex/hooks.json +2 -2
- package/.cursorrules +6 -41
- package/.github/actions/setup-node/action.yml +1 -1
- package/.github/workflows/auto-tag.yml +3 -4
- package/.github/workflows/ci.yml +32 -31
- package/.github/workflows/dependabot-auto-merge.yml +13 -6
- package/.github/workflows/publish.yml +50 -4
- package/.github/workflows/sonar-qube.yml +3 -0
- package/.pnpmfile.mjs +4 -3
- package/.prettierignore +8 -2
- package/CLAUDE.md +18 -20
- package/README.md +44 -38
- package/SECURITY.md +15 -1
- package/dist/CLAUDE.md +18 -20
- package/dist/commands/document-section-cli.js +734 -0
- package/dist/hooks/audit-provision.js +346 -0
- package/dist/hooks/batch-guard.js +5 -4
- package/dist/hooks/{chunk-G6KRCKEC.js → chunk-4TZVB44A.js} +496 -378
- package/dist/hooks/chunk-5VNDSNG3.js +12 -0
- package/dist/hooks/chunk-CDY4NFQG.js +15 -0
- package/dist/hooks/chunk-ELCN5LCH.js +135 -0
- package/dist/hooks/{chunk-VQLLNFNT.js → chunk-F3ELQ2AF.js} +3 -3
- package/dist/hooks/{chunk-I4XSL4TT.js → chunk-GPTFMK5E.js} +1 -1
- package/dist/hooks/{chunk-3V37IID6.js → chunk-JBBMAPC4.js} +23 -14
- package/dist/hooks/{chunk-UAGLLIGZ.js → chunk-KHGB5S3Y.js} +7 -6
- package/dist/hooks/{chunk-GDS5N2KB.js → chunk-KHX4U3BG.js} +735 -683
- package/dist/hooks/{chunk-VESKRQOF.js → chunk-KRBERBBI.js} +3 -3
- package/dist/hooks/{chunk-DO6NWWHU.js → chunk-QVSWU24O.js} +4 -4
- package/dist/hooks/{chunk-S6X6WYKR.js → chunk-WK2XSHSI.js} +1643 -923
- package/dist/hooks/{chunk-ZAL3NRGL.js → chunk-XZYKEM6J.js} +25 -10
- package/dist/hooks/{chunk-32GQTL4F.js → chunk-ZQGD4GBY.js} +4 -2
- package/dist/hooks/codex-hook-adapter.js +14 -12
- package/dist/hooks/duplicate-read-guard.js +6 -5
- package/dist/hooks/format-edited.js +4 -3
- package/dist/hooks/inputs.json +203 -179
- package/dist/hooks/investigation-guard.js +6 -5
- package/dist/hooks/pretool-guard.js +12 -10
- package/dist/hooks/session-lang.js +2 -4
- package/dist/hooks/stop-guard.js +66 -72
- package/dist/josh.js +1088 -78
- package/dist/version/index.d.ts +7 -2
- package/dist/version/index.js +152 -75
- package/eslint/base.js +23 -1
- package/eslint/rules/code-quality.js +3 -0
- package/eslint/rules/early-return-one-liner.js +98 -0
- package/eslint/rules/unicorn.js +16 -1
- package/package.json +7 -6
- package/pnpm-workspace.yaml +9 -4
- package/prettier/basic.d.ts +3 -1
- package/prettier/index.d.ts +3 -1
- package/prettier/static.d.ts +3 -1
- package/prompts/coding-standards.md +1 -1
- package/prompts/collaboration-workflow/file-edits.md +4 -4
- package/prompts/collaboration-workflow/glossary.md +1 -1
- package/prompts/collaboration-workflow/issue-template.md +1 -0
- package/prompts/collaboration-workflow/operating-rules.md +17 -9
- package/prompts/collaboration-workflow/overview.md +2 -2
- package/prompts/collaboration-workflow/principles.md +14 -12
- package/prompts/collaboration-workflow/report-format.md +1 -1
- package/prompts/collaboration-workflow/residency.md +1 -1
- package/prompts/collaboration-workflow/rule-delivery.md +6 -6
- package/prompts/collaboration-workflow/shell-body.md +6 -6
- package/prompts/collaboration-workflow/turn-batching.md +1 -1
- package/prompts/collaboration-workflow/upstream-interrupt.md +12 -2
- package/prompts/collaboration-workflow/wip-cap.md +10 -10
- package/prompts/review-rubric.md +1 -2
- package/prompts/review.md +1 -1
- package/scripts/agent/agent-headless.ts +29 -0
- package/scripts/agent/agent-launch-environment.ts +6 -2
- package/scripts/agent/codex-agent-argv.ts +2 -1
- package/scripts/auto-ok/auto-ok-cli.ts +1 -1
- package/scripts/backlog/backlog-arrival.ts +3 -3
- package/scripts/backlog/backlog-budget.ts +1 -1
- package/scripts/backlog/backlog-drive-args.ts +135 -0
- package/scripts/backlog/backlog-drive-cli.ts +11 -137
- package/scripts/backlog/backlog-drive-launch.ts +39 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +3 -5
- package/scripts/backlog/backlog-drive-named.ts +3 -3
- package/scripts/backlog/backlog-drive-offer-argv.ts +1 -1
- package/scripts/backlog/backlog-drive-owner.ts +1 -1
- package/scripts/backlog/backlog-drive-restore.ts +1 -1
- package/scripts/backlog/backlog-drive-retrospective.ts +1 -1
- package/scripts/backlog/backlog-drive.ts +1 -1
- package/scripts/backlog/backlog-next.ts +16 -5
- package/scripts/backlog/backlog-offer-cli.ts +6 -6
- package/scripts/backlog/backlog-offer.ts +1 -1
- package/scripts/backlog/backlog-plan-cli.ts +1 -1
- package/scripts/backlog/backlog-pool.ts +57 -11
- package/scripts/backlog/backlog-ready.ts +1 -1
- package/scripts/backlog/backlog-scope.ts +1 -1
- package/scripts/backlog/backlog-stalled-detect.ts +2 -2
- package/scripts/backlog/backlog-stalled.ts +1 -1
- package/scripts/build/build-codex-hooks.ts +3 -1
- package/scripts/build/build-commands.ts +66 -0
- package/scripts/build/build-hooks.ts +2 -1
- package/scripts/build/build-library.ts +12 -3
- package/scripts/build/build.ts +70 -0
- package/scripts/build/local-bin.ts +3 -13
- package/scripts/delegation/investigation-reads.ts +3 -1
- package/scripts/discovery/repo-origin.ts +5 -5
- package/scripts/doctor/doctor-consumer.ts +2 -2
- package/scripts/doctor/doctor-io.ts +2 -3
- package/scripts/document/capped-print-part.ts +30 -0
- package/scripts/document/capped-print.ts +3 -4
- package/scripts/document/document-byte-budget.ts +11 -2
- package/scripts/document/document-scan.ts +117 -0
- package/scripts/document/document-section.ts +143 -16
- package/scripts/document/entry-read-budget.ts +5 -3
- package/scripts/document/entry-read-set.ts +10 -89
- package/scripts/document/read-set-cost.ts +98 -0
- package/scripts/epic/epic-add-body.ts +1 -0
- package/scripts/epic/epic-audit-checks.ts +1 -1
- package/scripts/epic/epic-audit-rationale.ts +4 -3
- package/scripts/epic/epic-bundle-cli.ts +14 -3
- package/scripts/epic/epic-bundle-poll.ts +41 -0
- package/scripts/epic/epic-busy.ts +1 -1
- package/scripts/epic/epic-candidate-confirm.ts +3 -2
- package/scripts/epic/epic-classify.ts +1 -1
- package/scripts/epic/epic-cli-argv.ts +78 -46
- package/scripts/epic/epic-cli-remove.ts +11 -19
- package/scripts/epic/epic-cli.ts +74 -73
- package/scripts/epic/epic-close-comment.ts +1 -1
- package/scripts/epic/epic-close.ts +1 -1
- package/scripts/epic/epic-cross-repo.ts +5 -5
- package/scripts/epic/epic-graph.ts +1 -1
- package/scripts/epic/epic-index.ts +1 -1
- package/scripts/epic/epic-issue.ts +2 -6
- package/scripts/epic/epic-lane-offer.ts +2 -2
- package/scripts/epic/epic-next.ts +29 -42
- package/scripts/epic/epic-solo-stale.ts +1 -1
- package/scripts/epic/epic-triage.ts +8 -4
- package/scripts/epic/epic-validate.ts +1 -1
- package/scripts/followup/git-followup-finish.ts +2 -2
- package/scripts/followup/git-followup-label.ts +2 -2
- package/scripts/followup/git-followup-merged.ts +2 -2
- package/scripts/followup/git-pr-followup-checks.ts +142 -0
- package/scripts/followup/git-pr-followup.ts +4 -129
- package/scripts/gate/core-admission.ts +162 -0
- package/scripts/gate/core-budget.ts +190 -68
- package/scripts/gate/gate-plan.ts +80 -7
- package/scripts/gate/gate-report.ts +28 -9
- package/scripts/gate/gate-skip.ts +38 -7
- package/scripts/gate/hook-gate-reuse.ts +12 -10
- package/scripts/gate/machine-capacity.ts +175 -0
- package/scripts/gate/pre-commit-type-check.ts +4 -1
- package/scripts/gate/project-checks.ts +6 -9
- package/scripts/gate/type-check-step.ts +2 -3
- package/scripts/gate/verification-gate.ts +34 -10
- package/scripts/gh/git-gh-api-path.ts +2 -2
- package/scripts/gh/git-gh-issue-rest.ts +3 -3
- package/scripts/gh/git-gh-issue-write.ts +53 -7
- package/scripts/gh/git-gh-pr-read.ts +64 -19
- package/scripts/gh/git-gh-pr-rest.ts +1 -1
- package/scripts/gh/git-gh-pr.ts +5 -1
- package/scripts/gh/git-pr-ai-review.ts +1 -1
- package/scripts/gh/git-pr-checks-eval.ts +1 -3
- package/scripts/gh/git-pr-checks-parse.ts +1 -1
- package/scripts/gh/git-pr-checks-watch.ts +6 -5
- package/scripts/gh/git-pr-checks.ts +7 -5
- package/scripts/gh/git-pr-coderabbit.ts +1 -1
- package/scripts/gh/git-pr-error.ts +1 -3
- package/scripts/gh/git-pr.ts +9 -13
- package/scripts/gh/github-issue-url.ts +2 -2
- package/scripts/git/constants.ts +2 -6
- package/scripts/git/git-command.ts +20 -218
- package/scripts/git/git-diff-reads.ts +225 -0
- package/scripts/git/git-error.ts +4 -12
- package/scripts/git/git-pre-push-hook.ts +139 -0
- package/scripts/git/git-prompt.ts +2 -6
- package/scripts/git/git-spawn-sync.ts +3 -1
- package/scripts/git/git-spawn.ts +1 -1
- package/scripts/git/git-staging.ts +4 -6
- package/scripts/git/git-status.ts +3 -9
- package/scripts/hooks/codex-hook-adapter.ts +1 -1
- package/scripts/hooks/edited-cspell.ts +2 -4
- package/scripts/hooks/format-edited-file.ts +3 -12
- package/scripts/hooks/pre-push-unit.ts +31 -9
- package/scripts/hooks/reserved-run.ts +2 -2
- package/scripts/hooks/run-hook.sh +5 -4
- package/scripts/hooks/stop-guard.ts +5 -3
- package/scripts/init/init-actions.ts +0 -5
- package/scripts/init/init-ai-copy.ts +13 -2
- package/scripts/init/init-logic-json-merge.ts +19 -0
- package/scripts/init/init-logic-secretlint.ts +7 -7
- package/scripts/init/init-logic-workspace.ts +41 -2
- package/scripts/init/init-logic.ts +16 -26
- package/scripts/init/init.ts +10 -16
- package/scripts/init/initial-commit.ts +4 -4
- package/scripts/init/kit-development-versions.ts +40 -0
- package/scripts/init/kit-setup-state.ts +2 -2
- package/scripts/init/package-manager-pin.ts +35 -0
- package/scripts/init/start-exec.ts +68 -19
- package/scripts/init/start-setup-pr.ts +9 -9
- package/scripts/init/start-steps.ts +6 -10
- package/scripts/issue/defect-rate-cli.ts +1 -1
- package/scripts/issue/git-next-issues.ts +1 -1
- package/scripts/issue/issue-auto-ok.ts +32 -7
- package/scripts/issue/issue-backlinks-cli.ts +2 -2
- package/scripts/issue/issue-backlinks.ts +9 -9
- package/scripts/issue/issue-comment-cli.ts +19 -13
- package/scripts/issue/issue-file-cli.ts +72 -22
- package/scripts/issue/issue-file.ts +29 -1
- package/scripts/issue/issue-labels.ts +13 -0
- package/scripts/issue/issue-release-cli.ts +155 -0
- package/scripts/issue/issue-release.ts +58 -0
- package/scripts/issue/issue-scout-closed.ts +1 -1
- package/scripts/issue/issue-state-cli.ts +30 -74
- package/scripts/josh/josh-command-map.ts +2 -0
- package/scripts/josh/josh-command-types.ts +14 -0
- package/scripts/josh/josh-commands-ai.ts +4 -4
- package/scripts/josh/josh-commands-backlog.ts +4 -1
- package/scripts/josh/josh-commands-development.ts +18 -0
- package/scripts/josh/josh-commands-document.ts +1 -0
- package/scripts/josh/josh-commands-issue.ts +8 -1
- package/scripts/josh/josh-commands-lane.ts +24 -1
- package/scripts/josh/josh-commands-maintenance.ts +6 -6
- package/scripts/josh/josh-commands-run.ts +12 -7
- package/scripts/josh/josh-in-process.ts +28 -9
- package/scripts/josh/josh-logic.ts +34 -15
- package/scripts/josh/process-identity.ts +1 -3
- package/scripts/josh/session-language-cli.ts +3 -4
- package/scripts/lane/lane-await.ts +5 -1
- package/scripts/lane/lane-child-invocation.ts +2 -2
- package/scripts/lane/lane-cli.ts +12 -4
- package/scripts/lane/lane-dispatch.ts +14 -10
- package/scripts/lane/lane-environment.ts +5 -1
- package/scripts/lane/lane-handoff.ts +1 -1
- package/scripts/lane/lane-install.ts +2 -13
- package/scripts/lane/lane-launch-cli.ts +15 -1
- package/scripts/lane/lane-ledger.ts +145 -0
- package/scripts/lane/lane-leftover.ts +44 -30
- package/scripts/lane/lane-load.ts +108 -0
- package/scripts/lane/lane-measure-cli.ts +149 -0
- package/scripts/lane/lane-occupancy.ts +3 -0
- package/scripts/lane/lane-open.ts +5 -5
- package/scripts/lane/lane-paths.ts +13 -0
- package/scripts/lane/lane-reap.ts +1 -3
- package/scripts/lane/lane-registry.ts +11 -0
- package/scripts/lane/lane-relaunch.ts +1 -1
- package/scripts/lane/lane-stats.ts +224 -0
- package/scripts/lane/lane-stray.ts +140 -0
- package/scripts/lane/lane-vacant.ts +36 -0
- package/scripts/lane/openai-lane-ship.ts +2 -2
- package/scripts/lane/openai-lane-supervisor-owner.ts +180 -0
- package/scripts/lane/openai-lane-supervisor.ts +7 -165
- package/scripts/lane/openai-review-broker.ts +2 -4
- package/scripts/lib/ancestor-directories.ts +35 -0
- package/scripts/lib/animation-helpers.ts +2 -2
- package/scripts/lib/buffered-process.ts +3 -7
- package/scripts/lib/cli-flags.ts +71 -1
- package/scripts/lib/spawn-exit.ts +2 -2
- package/scripts/lib/timeouts.ts +35 -2
- package/scripts/lines/line-budget.ts +4 -6
- package/scripts/notify/telegram-test.ts +6 -0
- package/scripts/observations/observation-ledger-line.ts +6 -6
- package/scripts/observations/observation-ledger-migrate.ts +55 -4
- package/scripts/observations/observation-ledger.ts +26 -7
- package/scripts/overrides/lockfile-overrides.ts +1 -1
- package/scripts/overrides/overrides-logic.ts +1 -1
- package/scripts/propagate/propagate-git.ts +1 -1
- package/scripts/propagate/propagate-publish.ts +2 -2
- package/scripts/propagate/propagate-steps.ts +20 -15
- package/scripts/propagate/propagate.ts +36 -27
- package/scripts/refactor/import-graph.ts +18 -1
- package/scripts/refactor/refactor-lint.ts +4 -4
- package/scripts/release/github-release-environment.ts +1 -0
- package/scripts/release/github-release-runs.ts +115 -0
- package/scripts/release/github-release.ts +13 -67
- package/scripts/repo/repository-labels.ts +2 -2
- package/scripts/repo/required-checks-logic.ts +1 -3
- package/scripts/review/live-evidence.ts +2 -4
- package/scripts/review/review-brief-cli.ts +1 -1
- package/scripts/review/review-finding-ledger.ts +1 -1
- package/scripts/review/review-findings-cli.ts +1 -3
- package/scripts/rules/decision-oracle-batch.ts +128 -0
- package/scripts/rules/decision-oracle-tokens.ts +21 -0
- package/scripts/rules/decision-oracle.ts +8 -120
- package/scripts/rules/delivered-rules.ts +26 -182
- package/scripts/rules/direct-filing.ts +1 -1
- package/scripts/rules/early-heartbeat.ts +3 -3
- package/scripts/rules/git-argv.ts +1 -1
- package/scripts/rules/git-force.ts +1 -1
- package/scripts/rules/implementation-cut.ts +2 -2
- package/scripts/rules/index-guard.ts +1 -1
- package/scripts/rules/issue-comments.ts +118 -0
- package/scripts/rules/{issue-fold.ts → issue-fold-rule.ts} +2 -2
- package/scripts/rules/lane-background.ts +2 -2
- package/scripts/rules/lane-park.ts +18 -0
- package/scripts/rules/lane-split-park.ts +128 -0
- package/scripts/rules/oracle-firing.ts +1 -1
- package/scripts/rules/poll-loop.ts +1 -1
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/rules/protected-files.ts +59 -4
- package/scripts/rules/{run-tail.ts → run-tail-rule.ts} +2 -2
- package/scripts/rules/shell-body-trigger.ts +1 -1
- package/scripts/rules/stop-rules.ts +18 -7
- package/scripts/rules/test-declared-commit.ts +4 -4
- package/scripts/rules/wip-cap.ts +63 -0
- package/scripts/run/{run-carry-args.ts → carry/run-carry-args.ts} +37 -13
- package/scripts/run/{run-carry-change.ts → carry/run-carry-change.ts} +29 -7
- package/scripts/run/{run-carry-cli.ts → carry/run-carry-cli.ts} +4 -6
- package/scripts/run/{run-carry.ts → carry/run-carry.ts} +1 -1
- package/scripts/run/{run-cut-args.ts → cut/run-cut-args.ts} +1 -1
- package/scripts/run/{run-cut-cli.ts → cut/run-cut-cli.ts} +18 -5
- package/scripts/run/{run-cut.ts → cut/run-cut.ts} +1 -1
- package/scripts/run/{run-entry-cli.ts → entry/run-entry-cli.ts} +11 -11
- package/scripts/run/{run-entry.ts → entry/run-entry.ts} +2 -2
- package/scripts/run/{run-event-render.ts → event/run-event-render.ts} +1 -0
- package/scripts/run/{run-event-scope.ts → event/run-event-scope.ts} +1 -1
- package/scripts/run/{run-event-stream-emit.ts → event/run-event-stream-emit.ts} +1 -1
- package/scripts/run/{run-event-stream.ts → event/run-event-stream.ts} +20 -2
- package/scripts/run/{run-hold-cli.ts → hold/run-hold-cli.ts} +5 -5
- package/scripts/run/{run-merge-cli.ts → merge/run-merge-cli.ts} +10 -5
- package/scripts/run/{run-merge-steps.ts → merge/run-merge-steps.ts} +53 -5
- package/scripts/run/{run-merge.ts → merge/run-merge.ts} +1 -1
- package/scripts/run/{run-progress-cli.ts → progress/run-progress-cli.ts} +1 -1
- package/scripts/run/{run-progress-config.ts → progress/run-progress-config.ts} +2 -17
- package/scripts/run/{run-progress-read.ts → progress/run-progress-read.ts} +5 -5
- package/scripts/run/{run-progress.ts → progress/run-progress.ts} +1 -1
- package/scripts/run/{run-review-steps.ts → review/run-review-steps.ts} +2 -3
- package/scripts/run/run-ending.ts +2 -2
- package/scripts/run/run-halfrun-resume.ts +6 -1
- package/scripts/run/run-headless.ts +1 -1
- package/scripts/run/run-invocation.ts +85 -84
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-parent-cut-hook.ts +1 -1
- package/scripts/run/run-preflight.ts +6 -6
- package/scripts/run/run-prep-cli.ts +1 -1
- package/scripts/run/run-prrun-resume.ts +1 -1
- package/scripts/run/run-report-cli.ts +4 -4
- package/scripts/run/run-report.ts +2 -2
- package/scripts/run/run-status-cli.ts +1 -1
- package/scripts/run/run-step-cli.ts +3 -3
- package/scripts/run/run-step.ts +7 -4
- package/scripts/run/run-stop-notify.ts +1 -1
- package/scripts/run/run-stranded-detect.ts +4 -4
- package/scripts/run/run-stranded.ts +2 -2
- package/scripts/run/run-watcher-guard-cli.ts +1 -1
- package/scripts/run/run-watcher-guard.ts +5 -3
- package/scripts/run/run-watcher-hook.ts +1 -1
- package/scripts/run/{run-ship-cli.ts → ship/run-ship-cli.ts} +2 -2
- package/scripts/run/{run-ship-detach.ts → ship/run-ship-detach.ts} +3 -3
- package/scripts/run/ship/run-ship-lock.ts +50 -0
- package/scripts/run/{run-ship-preflight.ts → ship/run-ship-preflight.ts} +6 -2
- package/scripts/run/{run-ship-probe.ts → ship/run-ship-probe.ts} +2 -2
- package/scripts/run/{run-ship-return.ts → ship/run-ship-return.ts} +4 -3
- package/scripts/run/{run-ship-review-steps.ts → ship/run-ship-review-steps.ts} +3 -3
- package/scripts/run/{run-ship-scoped.ts → ship/run-ship-scoped.ts} +10 -1
- package/scripts/run/{run-ship-steps.ts → ship/run-ship-steps.ts} +11 -12
- package/scripts/run/ship/run-ship-stop-text.ts +13 -0
- package/scripts/run/{run-ship-sync.ts → ship/run-ship-sync.ts} +25 -11
- package/scripts/run/{run-tidy-lanes.ts → tidy/run-tidy-lanes.ts} +1 -1
- package/scripts/run/{run-wake-cli.ts → wake/run-wake-cli.ts} +25 -11
- package/scripts/run/{run-wake-describe.ts → wake/run-wake-describe.ts} +58 -7
- package/scripts/run/{run-wake-driver.ts → wake/run-wake-driver.ts} +59 -8
- package/scripts/run/{run-wake-handoff.ts → wake/run-wake-handoff.ts} +1 -1
- package/scripts/run/{run-wake-loop.ts → wake/run-wake-loop.ts} +13 -15
- package/scripts/run/wake/run-wake-restart.ts +58 -0
- package/scripts/run/{run-wake-session.ts → wake/run-wake-session.ts} +3 -3
- package/scripts/run/{run-wake.ts → wake/run-wake.ts} +16 -65
- package/scripts/safe-chain/preinstall-command.ts +20 -21
- package/scripts/safe-chain/project-config.ts +19 -15
- package/scripts/sync/sync-ai-files.ts +1 -1
- package/scripts/sync/sync-configs.ts +2 -1
- package/scripts/sync/sync.ts +15 -2
- package/scripts/test/unit-worker-share.ts +1 -0
- package/scripts/time-runtime/time-background.ts +5 -1
- package/scripts/time-runtime/time-instant.ts +3 -1
- package/scripts/time-runtime/time-spans.ts +6 -139
- package/scripts/time-runtime/time-tool-call.ts +160 -0
- package/scripts/version/effective-upstream.ts +2 -20
- package/scripts/version/latest-corepack.ts +124 -93
- package/scripts/version/latest-regression.ts +1 -1
- package/scripts/version/latest-update.ts +4 -4
- package/scripts/version/npm-registry.ts +20 -5
- package/scripts/version/publishable-range-check.ts +2 -2
- package/scripts/version/publishable-range.ts +1 -1
- package/scripts/version/release-age.ts +48 -38
- package/scripts/version/release-hold.ts +1 -1
- package/scripts/version/safe-chain-version-update.ts +40 -0
- package/scripts/version/upgrade-shell-command.ts +48 -8
- package/scripts/version/version-check-logic.ts +23 -14
- package/scripts/version/version-commands.ts +23 -11
- package/scripts/version/version-remote.ts +28 -13
- package/templates/pnpm-workspace.basic.yaml +4 -0
- package/templates/workflows/dependabot-auto-merge.yml +5 -5
- package/templates/workflows/github-release.yml +6 -0
- package/scripts/build/build-config-merge.ts +0 -27
- package/scripts/build/build-managed-marker.ts +0 -27
- package/scripts/build/build-self-sync-guard.ts +0 -27
- package/scripts/build/build-version.ts +0 -26
- package/scripts/version/preinstall-version-update.ts +0 -86
- /package/scripts/git/{schemas.ts → git-schemas.ts} +0 -0
- /package/scripts/overrides/{schemas.ts → overrides-schemas.ts} +0 -0
- /package/scripts/run/{run-carry-conversation.ts → carry/run-carry-conversation.ts} +0 -0
- /package/scripts/run/{run-carry-reclaim.ts → carry/run-carry-reclaim.ts} +0 -0
- /package/scripts/run/{run-carry-stash.ts → carry/run-carry-stash.ts} +0 -0
- /package/scripts/run/{run-carry-streak.ts → carry/run-carry-streak.ts} +0 -0
- /package/scripts/run/{run-cut-handoff.ts → cut/run-cut-handoff.ts} +0 -0
- /package/scripts/run/{run-cut-report.ts → cut/run-cut-report.ts} +0 -0
- /package/scripts/run/{run-entry-stop.ts → entry/run-entry-stop.ts} +0 -0
- /package/scripts/run/{run-event-cli.ts → event/run-event-cli.ts} +0 -0
- /package/scripts/run/{run-event-follow.ts → event/run-event-follow.ts} +0 -0
- /package/scripts/run/{run-event-watch.ts → event/run-event-watch.ts} +0 -0
- /package/scripts/run/{run-hold.ts → hold/run-hold.ts} +0 -0
- /package/scripts/run/{run-progress-args.ts → progress/run-progress-args.ts} +0 -0
- /package/scripts/run/{run-progress-clock.ts → progress/run-progress-clock.ts} +0 -0
- /package/scripts/run/{run-review-cli.ts → review/run-review-cli.ts} +0 -0
- /package/scripts/run/{run-review.ts → review/run-review.ts} +0 -0
- /package/scripts/run/{run-ship-next.ts → ship/run-ship-next.ts} +0 -0
- /package/scripts/run/{run-ship-pre-detach.ts → ship/run-ship-pre-detach.ts} +0 -0
- /package/scripts/run/{run-ship-review.ts → ship/run-ship-review.ts} +0 -0
- /package/scripts/run/{run-ship-stage.ts → ship/run-ship-stage.ts} +0 -0
- /package/scripts/run/{run-ship.ts → ship/run-ship.ts} +0 -0
- /package/scripts/run/{run-tidy-cli.ts → tidy/run-tidy-cli.ts} +0 -0
- /package/scripts/run/{run-tidy-stashes.ts → tidy/run-tidy-stashes.ts} +0 -0
- /package/scripts/run/{run-tidy.ts → tidy/run-tidy.ts} +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { setInterval as every } from 'node:timers/promises'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
|
+
import { lane_capacity } from './lane-capacity'
|
|
6
|
+
import { lane_ledger } from './lane-ledger'
|
|
7
|
+
import { lane_load } from './lane-load'
|
|
8
|
+
import { lane_stats } from './lane-stats'
|
|
9
|
+
|
|
10
|
+
// `josh lane:sample` / `josh lane:stats` — the lane-limit measurement (joshuafolkken/kit#3355).
|
|
11
|
+
//
|
|
12
|
+
// `lane:sample` appends one machine-load sample to the ledger, or one every `--every <seconds>` until it
|
|
13
|
+
// is stopped; `lane:stats` reduces a period of the ledger to the one table row #3347 records per lane
|
|
14
|
+
// limit. The merges and the gates write themselves (`run:merge`, `josh gate`), so a person runs only
|
|
15
|
+
// the sampler beside a `backlogrun` and closes the period with `lane:stats`. The procedure is
|
|
16
|
+
// `docs/maintainers/lane-limit-measurement.md`.
|
|
17
|
+
|
|
18
|
+
const ARGV_OFFSET = 2
|
|
19
|
+
const FAILURE_EXIT_CODE = 1
|
|
20
|
+
const MS_PER_SECOND = 1000
|
|
21
|
+
const SAMPLE_VERB = 'sample'
|
|
22
|
+
const STATS_VERB = 'stats'
|
|
23
|
+
const USAGE = [
|
|
24
|
+
'Usage: josh lane:sample [--every <seconds>]',
|
|
25
|
+
' josh lane:stats --period <days> [--limit <lane-limit>]',
|
|
26
|
+
].join('\n')
|
|
27
|
+
|
|
28
|
+
const SAMPLE_OPTIONS = { every: { type: 'string' } } as const
|
|
29
|
+
const STATS_OPTIONS = { period: { type: 'string' }, limit: { type: 'string' } } as const
|
|
30
|
+
|
|
31
|
+
// Where the ledger is and what "now" is, overridable so a test reads a fixed ledger at a fixed instant.
|
|
32
|
+
interface MeasureContext {
|
|
33
|
+
ledger_path?: string
|
|
34
|
+
now_ms?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function refuse(): number {
|
|
38
|
+
console.error(USAGE)
|
|
39
|
+
|
|
40
|
+
return FAILURE_EXIT_CODE
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A positive finite number, or `undefined` for anything else — a typo must not become a zero period.
|
|
44
|
+
function positive(text: string | undefined): number | undefined {
|
|
45
|
+
const value = Number(text)
|
|
46
|
+
|
|
47
|
+
return text !== undefined && Number.isFinite(value) && value > 0 ? value : undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function take_sample(context: MeasureContext): Promise<void> {
|
|
51
|
+
const at = new Date(context.now_ms ?? Date.now()).toISOString()
|
|
52
|
+
|
|
53
|
+
await lane_ledger.record(await lane_load.sample(at), context.ledger_path)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The `--every` interval: `{ seconds: undefined }` for a single sample, `undefined` for a refusal.
|
|
57
|
+
function sample_interval(rest: ReadonlyArray<string>): { seconds: number | undefined } | undefined {
|
|
58
|
+
const values = cli_flags.values_of(rest, SAMPLE_OPTIONS)
|
|
59
|
+
|
|
60
|
+
if (values === undefined) return undefined
|
|
61
|
+
|
|
62
|
+
if (values.every === undefined) return { seconds: undefined }
|
|
63
|
+
|
|
64
|
+
const seconds = positive(values.every)
|
|
65
|
+
|
|
66
|
+
return seconds === undefined ? undefined : { seconds }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A sample every `seconds` until a person stops the process, which is how a measurement period is held
|
|
70
|
+
// open; it never returns.
|
|
71
|
+
async function keep_sampling(context: MeasureContext, seconds: number): Promise<void> {
|
|
72
|
+
const ticks = every(seconds * MS_PER_SECOND, context)
|
|
73
|
+
|
|
74
|
+
for await (const tick_context of ticks) await take_sample(tick_context)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// One sample now, then — with `--every` — one per interval.
|
|
78
|
+
async function sample_command(
|
|
79
|
+
rest: ReadonlyArray<string>,
|
|
80
|
+
context: MeasureContext,
|
|
81
|
+
): Promise<number> {
|
|
82
|
+
const interval = sample_interval(rest)
|
|
83
|
+
|
|
84
|
+
if (interval === undefined) return refuse()
|
|
85
|
+
|
|
86
|
+
await take_sample(context)
|
|
87
|
+
|
|
88
|
+
if (interval.seconds !== undefined) await keep_sampling(context, interval.seconds)
|
|
89
|
+
|
|
90
|
+
return 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The label defaults to the limit this process reads, which is the period's limit only when nobody
|
|
94
|
+
// changed it since — `--limit` names it outright.
|
|
95
|
+
function limit_label(given: string | undefined): string {
|
|
96
|
+
if (given !== undefined) return given
|
|
97
|
+
|
|
98
|
+
const choice = lane_capacity.lane_limit()
|
|
99
|
+
|
|
100
|
+
return choice.kind === 'limit' ? String(choice.limit) : lane_stats.MISSING
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function read_ledger(
|
|
104
|
+
context: MeasureContext,
|
|
105
|
+
): Promise<ReturnType<typeof lane_ledger.read_entries>> {
|
|
106
|
+
const path = context.ledger_path ?? (await lane_ledger.target())
|
|
107
|
+
|
|
108
|
+
return path === undefined ? [] : lane_ledger.read_entries(path)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function stats_command(
|
|
112
|
+
rest: ReadonlyArray<string>,
|
|
113
|
+
context: MeasureContext,
|
|
114
|
+
): Promise<number> {
|
|
115
|
+
const values = cli_flags.values_of(rest, STATS_OPTIONS)
|
|
116
|
+
const period_days = positive(values?.period)
|
|
117
|
+
|
|
118
|
+
if (values === undefined || period_days === undefined) return refuse()
|
|
119
|
+
|
|
120
|
+
const window = lane_stats.window_of(period_days, context.now_ms ?? Date.now())
|
|
121
|
+
const label = { limit: limit_label(values.limit), period_days }
|
|
122
|
+
const row = lane_stats.row(await read_ledger(context), window, label)
|
|
123
|
+
|
|
124
|
+
console.info(`${lane_stats.header()}\n${row}`)
|
|
125
|
+
|
|
126
|
+
return 0
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function run(argv: ReadonlyArray<string>, context: MeasureContext = {}): Promise<number> {
|
|
130
|
+
const [verb, ...rest] = argv
|
|
131
|
+
|
|
132
|
+
if (verb === SAMPLE_VERB) return await sample_command(rest, context)
|
|
133
|
+
|
|
134
|
+
return verb === STATS_VERB ? await stats_command(rest, context) : refuse()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
138
|
+
process.exitCode = await run(argv)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const lane_measure_cli = {
|
|
142
|
+
USAGE,
|
|
143
|
+
run,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
|
|
147
|
+
|
|
148
|
+
export type { MeasureContext }
|
|
149
|
+
export { lane_measure_cli }
|
|
@@ -82,7 +82,10 @@ function without_label_line(issue: number): string {
|
|
|
82
82
|
|
|
83
83
|
function labelled_lines(labelled: ReadonlyArray<LabelledIssue>): Array<string> {
|
|
84
84
|
return labelled.flatMap((entry) => {
|
|
85
|
+
// One return type across the branches (sonarjs/function-return-type) keeps the array wrappers.
|
|
86
|
+
// eslint-disable-next-line unicorn/no-unnecessary-array-flat-map -- see above
|
|
85
87
|
if (entry.liveness === 'stopped') return [stopped_line(entry.issue)]
|
|
88
|
+
// eslint-disable-next-line unicorn/no-unnecessary-array-flat-map -- see above
|
|
86
89
|
if (entry.liveness === 'unknown') return [unknown_line(entry.issue)]
|
|
87
90
|
|
|
88
91
|
return []
|
|
@@ -37,14 +37,13 @@ type OpenOutcome =
|
|
|
37
37
|
|
|
38
38
|
const FULL_OUTCOME: OpenOutcome = { kind: 'full' }
|
|
39
39
|
|
|
40
|
-
// The seat a lane will take is claimed by exclusively creating its lock directory
|
|
41
|
-
// lanes root and clear of the lanes themselves (which are numbered, never dot-prefixed). The claim
|
|
40
|
+
// The seat a lane will take is claimed by exclusively creating its lock directory under
|
|
41
|
+
// `lane_paths.SEAT_LOCK_DIR`, in the lanes root and clear of the lanes themselves (which are numbered, never dot-prefixed). The claim
|
|
42
42
|
// is what closes the gap between reading the free seats and writing the lane's `.env`: two
|
|
43
43
|
// `lane:open` runs that both saw seat 1 free cannot both create the same lock, so the loser steps to
|
|
44
44
|
// the next free seat (joshuafolkken/kit#1494). The lock is released once the lane's `.env` is on
|
|
45
45
|
// disk and the seat is discoverable — or on any failure — so only a hard crash mid-open can strand
|
|
46
46
|
// one, which removing the lanes-root `.seat-locks` directory clears.
|
|
47
|
-
const SEAT_LOCK_DIR = '.seat-locks'
|
|
48
47
|
const SEAT_LOCK_PREFIX = 'seat-'
|
|
49
48
|
|
|
50
49
|
// A root with no `.env` is the normal state of a fresh clone and of CI, and it means seed 0 — the
|
|
@@ -79,7 +78,7 @@ function build_lane(
|
|
|
79
78
|
}
|
|
80
79
|
|
|
81
80
|
function seat_lock_path(root: string, seat: number): string {
|
|
82
|
-
return path.join(root, SEAT_LOCK_DIR, `${SEAT_LOCK_PREFIX}${String(seat)}`)
|
|
81
|
+
return path.join(root, lane_paths.SEAT_LOCK_DIR, `${SEAT_LOCK_PREFIX}${String(seat)}`)
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
// Release the seat lock — idempotent (`force`), so calling it after the `.env` write and again in the
|
|
@@ -93,7 +92,7 @@ function release_seat_lock(lock: string): void {
|
|
|
93
92
|
// being created. A missing directory is none held — the normal state when no open is running.
|
|
94
93
|
function open_in_flight_count(root: string): number {
|
|
95
94
|
try {
|
|
96
|
-
return readdirSync(path.join(root, SEAT_LOCK_DIR)).filter((entry) =>
|
|
95
|
+
return readdirSync(path.join(root, lane_paths.SEAT_LOCK_DIR)).filter((entry) =>
|
|
97
96
|
entry.startsWith(SEAT_LOCK_PREFIX),
|
|
98
97
|
).length
|
|
99
98
|
} catch {
|
|
@@ -279,6 +278,7 @@ async function open_lane(issue: string): Promise<OpenOutcome> {
|
|
|
279
278
|
}
|
|
280
279
|
|
|
281
280
|
const lane_open = {
|
|
281
|
+
open_in_flight_count,
|
|
282
282
|
open_lane,
|
|
283
283
|
}
|
|
284
284
|
|
|
@@ -82,10 +82,23 @@ function lane_branch(issue: string): string {
|
|
|
82
82
|
return `${issue}${LANE_BRANCH_SUFFIX}`
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// The lanes root's own bookkeeping, beside the lanes and never one of them: `lane:open` claims a seat
|
|
86
|
+
// by creating a lock directory under it (joshuafolkken/kit#1494).
|
|
87
|
+
const SEAT_LOCK_DIR = '.seat-locks'
|
|
88
|
+
|
|
89
|
+
// A name the lane machinery writes into a lanes root: a lane's issue number, or a lanes root of its
|
|
90
|
+
// own — the nested one a lane opened from inside a lane once put there. Anything else in the root
|
|
91
|
+
// was put there by someone else (joshuafolkken/kit#3370).
|
|
92
|
+
function is_lane_entry(name: string): boolean {
|
|
93
|
+
return LANE_ISSUE_PATTERN.test(name) || DEFAULT_LANE_ROOT_PATTERN.test(name)
|
|
94
|
+
}
|
|
95
|
+
|
|
85
96
|
const lane_paths = {
|
|
86
97
|
LANE_BRANCH_SUFFIX,
|
|
87
98
|
LANE_ROOT_KEY,
|
|
99
|
+
SEAT_LOCK_DIR,
|
|
88
100
|
default_lane_root,
|
|
101
|
+
is_lane_entry,
|
|
89
102
|
lane_branch,
|
|
90
103
|
lane_directory,
|
|
91
104
|
lane_issue_of,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
2
3
|
import { lane_child_invocation } from './lane-child-invocation'
|
|
3
4
|
|
|
4
5
|
// Ending a lane child that did not end itself (joshuafolkken/kit#2421).
|
|
@@ -24,9 +25,6 @@ const CHILDREN_OF_FLAG = '-P'
|
|
|
24
25
|
const PS_COMMAND = '/bin/ps'
|
|
25
26
|
const PARENT_FORMAT = ['-o', 'ppid=', '-p']
|
|
26
27
|
const TERMINATE_SIGNAL = 'SIGTERM'
|
|
27
|
-
// A probe that hung would hang the close it runs inside; failing answers "no process", which leaves
|
|
28
|
-
// the lane exactly as it was before this module existed.
|
|
29
|
-
const PROBE_TIMEOUT_MS = 2000
|
|
30
28
|
// `0` addresses the caller's own process group and `1` is init — neither is ever a lane child.
|
|
31
29
|
const LOWEST_CHILD_PID = 2
|
|
32
30
|
|
|
@@ -217,6 +217,16 @@ async function list_lanes(): Promise<Array<LaneInfo>> {
|
|
|
217
217
|
return lanes.toSorted((left, right) => Number(left.issue) - Number(right.issue))
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Every directory git registers as a work tree, lane or not — what a sweep of the lanes root must
|
|
222
|
+
* never touch, including a work tree at a lane's path whose branch does not read as a lane's.
|
|
223
|
+
*/
|
|
224
|
+
async function registered_directories(): Promise<Array<string>> {
|
|
225
|
+
const blocks = await worktree_blocks()
|
|
226
|
+
|
|
227
|
+
return blocks.flatMap((block) => line_value(block.split('\n'), WORKTREE_PREFIX) ?? [])
|
|
228
|
+
}
|
|
229
|
+
|
|
220
230
|
// The lanes a parent is still waiting on. A stranded lane has no child working in it, so it is not
|
|
221
231
|
// in-flight — the one definition the watcher guard and the headless stop rule both read.
|
|
222
232
|
async function has_lanes_in_flight(): Promise<boolean> {
|
|
@@ -259,6 +269,7 @@ const lane_registry = {
|
|
|
259
269
|
main_repository_root,
|
|
260
270
|
parse_block,
|
|
261
271
|
read_environment,
|
|
272
|
+
registered_directories,
|
|
262
273
|
unreadable_lanes,
|
|
263
274
|
used_seats,
|
|
264
275
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent_argv } from '#scripts/agent/agent-argv'
|
|
2
2
|
import { detached_launch, type LaunchResult } from '#scripts/run/detached-launch'
|
|
3
|
-
import { run_ship_detach } from '#scripts/run/run-ship-detach'
|
|
3
|
+
import { run_ship_detach } from '#scripts/run/ship/run-ship-detach'
|
|
4
4
|
import { lane_child_invocation } from './lane-child-invocation'
|
|
5
5
|
import { lane_child_marker } from './lane-child-marker'
|
|
6
6
|
import { lane_dispatch_log } from './lane-dispatch-log'
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { GateEntry, LedgerEntry, LoadEntry } from './lane-ledger'
|
|
2
|
+
|
|
3
|
+
// The lane-limit measurement's one table row (joshuafolkken/kit#3355): the ledger entries of one period
|
|
4
|
+
// reduced to throughput, gate duration and machine load, in the column order
|
|
5
|
+
// `docs/maintainers/lane-limit-measurement.md` documents, so the row pastes into #3347's table as is.
|
|
6
|
+
//
|
|
7
|
+
// **A column with no samples prints `—`, never `0`.** A period nobody ran the sampler through has no
|
|
8
|
+
// load, which is not a load of zero; a zero would be read as an idle machine.
|
|
9
|
+
//
|
|
10
|
+
// **Throughput is per active hour** — the time load samples saw a lane child working — rather than per
|
|
11
|
+
// calendar hour, so the nights between runs in a several-day period do not dilute the rate. The gaps
|
|
12
|
+
// between work entries cannot stand in for it: at a low limit the gap from one merge to the next child's
|
|
13
|
+
// first gate is a whole implementation, which a gap cap drops, inflating exactly the low limits' rate.
|
|
14
|
+
// Only a period no sample saw working falls back to the gaps of up to an hour between merges and gates.
|
|
15
|
+
//
|
|
16
|
+
// **The means take working samples only** for the same reason: the sampler runs through the nights,
|
|
17
|
+
// and their idle samples would read four busy lanes as about one. Peaks and minima take every sample.
|
|
18
|
+
|
|
19
|
+
const MS_PER_MINUTE = 60_000
|
|
20
|
+
const MS_PER_HOUR = 3_600_000
|
|
21
|
+
// A busy lane pool finishes a gate well within this; a longer silence is time nobody was running work.
|
|
22
|
+
const ACTIVE_GAP_MS = MS_PER_HOUR
|
|
23
|
+
const MS_PER_DAY = 86_400_000
|
|
24
|
+
const MB_PER_GB = 1024
|
|
25
|
+
const HALF = 2
|
|
26
|
+
const DECIMALS = 1
|
|
27
|
+
const MISSING = '—'
|
|
28
|
+
const CELL_SEPARATOR = ' | '
|
|
29
|
+
|
|
30
|
+
const COLUMNS = [
|
|
31
|
+
'JOSH_LANE_LIMIT',
|
|
32
|
+
'period (days)',
|
|
33
|
+
'merges',
|
|
34
|
+
'merges / hour',
|
|
35
|
+
'effective lanes',
|
|
36
|
+
'gate median (min)',
|
|
37
|
+
'gate max (min)',
|
|
38
|
+
'load peak',
|
|
39
|
+
'load mean',
|
|
40
|
+
'swap peak (GB)',
|
|
41
|
+
'swap mean (GB)',
|
|
42
|
+
'free memory min (GB)',
|
|
43
|
+
'free memory mean (GB)',
|
|
44
|
+
'load samples',
|
|
45
|
+
] as const
|
|
46
|
+
|
|
47
|
+
interface StatsWindow {
|
|
48
|
+
since_ms: number
|
|
49
|
+
until_ms: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface RowLabel {
|
|
53
|
+
limit: string
|
|
54
|
+
period_days: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function window_of(period_days: number, now_ms: number): StatsWindow {
|
|
58
|
+
return { since_ms: now_ms - period_days * MS_PER_DAY, until_ms: now_ms }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function in_window(entries: ReadonlyArray<LedgerEntry>, window: StatsWindow): Array<LedgerEntry> {
|
|
62
|
+
return entries.filter((entry) => {
|
|
63
|
+
const at = Date.parse(entry.at)
|
|
64
|
+
|
|
65
|
+
return at >= window.since_ms && at <= window.until_ms
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function is_gate(entry: LedgerEntry): entry is GateEntry {
|
|
70
|
+
return entry.kind === 'gate'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function is_load(entry: LedgerEntry): entry is LoadEntry {
|
|
74
|
+
return entry.kind === 'load'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function is_number(value: number | undefined): value is number {
|
|
78
|
+
return value !== undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function mean(values: ReadonlyArray<number>): number | undefined {
|
|
82
|
+
if (values.length === 0) return undefined
|
|
83
|
+
|
|
84
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function median(values: ReadonlyArray<number>): number | undefined {
|
|
88
|
+
if (values.length === 0) return undefined
|
|
89
|
+
|
|
90
|
+
const sorted = values.toSorted((left, right) => left - right)
|
|
91
|
+
const middle = Math.floor(sorted.length / HALF)
|
|
92
|
+
|
|
93
|
+
return sorted.length % HALF === 0 ? mean(sorted.slice(middle - 1, middle + 1)) : sorted[middle]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function peak(values: ReadonlyArray<number>): number | undefined {
|
|
97
|
+
return values.length === 0 ? undefined : Math.max(...values)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function lowest(values: ReadonlyArray<number>): number | undefined {
|
|
101
|
+
return values.length === 0 ? undefined : Math.min(...values)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type WorkingEntry = LoadEntry & { lanes: number }
|
|
105
|
+
|
|
106
|
+
function is_working(sample: LoadEntry): sample is WorkingEntry {
|
|
107
|
+
return (sample.lanes ?? 0) > 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sum_hours(gaps: ReadonlyArray<number>): number | undefined {
|
|
111
|
+
const active = gaps.filter((gap) => gap <= ACTIVE_GAP_MS).reduce((sum, gap) => sum + gap, 0)
|
|
112
|
+
|
|
113
|
+
return active > 0 ? active / MS_PER_HOUR : undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The fallback for a period no sampler saw working: the gaps between consecutive work entries (merges
|
|
117
|
+
// and gates) no longer than `ACTIVE_GAP_MS`.
|
|
118
|
+
function gap_hours(entries: ReadonlyArray<LedgerEntry>): number | undefined {
|
|
119
|
+
const instants = entries
|
|
120
|
+
.filter((entry) => !is_load(entry))
|
|
121
|
+
.map((entry) => Date.parse(entry.at))
|
|
122
|
+
.toSorted((left, right) => left - right)
|
|
123
|
+
|
|
124
|
+
return sum_hours(
|
|
125
|
+
instants.slice(1).map((instant, index) => instant - (instants[index] ?? instant)),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The time from each working sample to the next sample. The cap still applies, so a sampler stopped
|
|
130
|
+
// mid-run does not count its silence as work.
|
|
131
|
+
function sampled_hours(samples: ReadonlyArray<LoadEntry>): number | undefined {
|
|
132
|
+
const sorted = samples.toSorted((left, right) => Date.parse(left.at) - Date.parse(right.at))
|
|
133
|
+
const gaps = sorted
|
|
134
|
+
.slice(1)
|
|
135
|
+
.map((next, index) => ({ sample: sorted[index] ?? next, next }))
|
|
136
|
+
.filter(({ sample }) => is_working(sample))
|
|
137
|
+
.map(({ sample, next }) => Date.parse(next.at) - Date.parse(sample.at))
|
|
138
|
+
|
|
139
|
+
return sum_hours(gaps)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The hours work was under way; `undefined` when nothing leaves any active time.
|
|
143
|
+
function active_hours(entries: ReadonlyArray<LedgerEntry>): number | undefined {
|
|
144
|
+
const samples = entries.filter(is_load)
|
|
145
|
+
|
|
146
|
+
return samples.some((sample) => is_working(sample)) ? sampled_hours(samples) : gap_hours(entries)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function gigabytes(megabytes: number | undefined): number | undefined {
|
|
150
|
+
return megabytes === undefined ? undefined : megabytes / MB_PER_GB
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cell(value: number | undefined): string {
|
|
154
|
+
return value === undefined ? MISSING : value.toFixed(DECIMALS)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function throughput_cells(entries: ReadonlyArray<LedgerEntry>): Array<string> {
|
|
158
|
+
const merges = entries.filter((entry) => entry.kind === 'merge').length
|
|
159
|
+
const hours = active_hours(entries)
|
|
160
|
+
const lanes = entries
|
|
161
|
+
.filter(is_load)
|
|
162
|
+
.filter(is_working)
|
|
163
|
+
.map((entry) => entry.lanes)
|
|
164
|
+
const per_hour = hours === undefined ? undefined : merges / hours
|
|
165
|
+
|
|
166
|
+
return [String(merges), cell(per_hour), cell(mean(lanes))]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function gate_cells(entries: ReadonlyArray<LedgerEntry>): Array<string> {
|
|
170
|
+
const minutes = entries.filter(is_gate).map((entry) => entry.elapsed_ms / MS_PER_MINUTE)
|
|
171
|
+
|
|
172
|
+
return [cell(median(minutes)), cell(peak(minutes))]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function load_cells(samples: ReadonlyArray<LoadEntry>): Array<string> {
|
|
176
|
+
const working = samples.filter(is_working)
|
|
177
|
+
const swap = samples.map((sample) => sample.swap_mb).filter(is_number)
|
|
178
|
+
const working_swap = working.map((sample) => sample.swap_mb).filter(is_number)
|
|
179
|
+
const free = samples.map((sample) => sample.free_mb)
|
|
180
|
+
const working_free = working.map((sample) => sample.free_mb)
|
|
181
|
+
|
|
182
|
+
return [
|
|
183
|
+
cell(peak(samples.map((sample) => sample.load))),
|
|
184
|
+
cell(mean(working.map((sample) => sample.load))),
|
|
185
|
+
cell(gigabytes(peak(swap))),
|
|
186
|
+
cell(gigabytes(mean(working_swap))),
|
|
187
|
+
cell(gigabytes(lowest(free))),
|
|
188
|
+
cell(gigabytes(mean(working_free))),
|
|
189
|
+
String(samples.length),
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function to_row(cells: ReadonlyArray<string>): string {
|
|
194
|
+
return `| ${cells.join(CELL_SEPARATOR)} |`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function header(): string {
|
|
198
|
+
return [to_row(COLUMNS), to_row(COLUMNS.map(() => '---'))].join('\n')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// The period's row: `entries` is the whole ledger, filtered here to `window`.
|
|
202
|
+
function row(entries: ReadonlyArray<LedgerEntry>, window: StatsWindow, label: RowLabel): string {
|
|
203
|
+
const period = in_window(entries, window)
|
|
204
|
+
|
|
205
|
+
return to_row([
|
|
206
|
+
label.limit,
|
|
207
|
+
String(label.period_days),
|
|
208
|
+
...throughput_cells(period),
|
|
209
|
+
...gate_cells(period),
|
|
210
|
+
...load_cells(period.filter(is_load)),
|
|
211
|
+
])
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const lane_stats = {
|
|
215
|
+
COLUMNS,
|
|
216
|
+
MISSING,
|
|
217
|
+
header,
|
|
218
|
+
median,
|
|
219
|
+
row,
|
|
220
|
+
window_of,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type { RowLabel, StatsWindow }
|
|
224
|
+
export { lane_stats }
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, rmSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
4
|
+
import { lane_leftover } from './lane-leftover'
|
|
5
|
+
import { lane_open } from './lane-open'
|
|
6
|
+
import { lane_paths } from './lane-paths'
|
|
7
|
+
import { lane_registry } from './lane-registry'
|
|
8
|
+
|
|
9
|
+
// What the lanes root holds that no registered work tree accounts for (joshuafolkken/kit#3370).
|
|
10
|
+
//
|
|
11
|
+
// **Every other sweep starts from `git worktree list`**, so a directory whose registration is gone —
|
|
12
|
+
// a lane whose `rmSync` stopped half-way, one whose `.git` points at a registration git pruned, an
|
|
13
|
+
// empty lanes root a lane once nested inside itself — is invisible to all of them, and stays on disk
|
|
14
|
+
// for good. This reads the root itself.
|
|
15
|
+
//
|
|
16
|
+
// **A stray is removed only when removing it loses nothing** — `lane_leftover.foreign_paths`, the one
|
|
17
|
+
// test `lane:open`'s reclaim also applies. One that holds anything else is kept and reported with what
|
|
18
|
+
// it holds; so is every entry whose name the lane machinery never writes, because nothing here knows
|
|
19
|
+
// who put it there. The root's own `.seat-locks` and every registered work tree are not strays.
|
|
20
|
+
|
|
21
|
+
type StrayVerdict = 'foreign' | 'not-a-lane' | 'opening' | 'removed' | 'stuck' | 'unreadable'
|
|
22
|
+
|
|
23
|
+
interface Stray {
|
|
24
|
+
path: string
|
|
25
|
+
verdict: StrayVerdict
|
|
26
|
+
// The paths inside it whose removal would lose something — only on `foreign`.
|
|
27
|
+
foreign: Array<string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stray_names(root: string, registered: ReadonlySet<string>): Array<string> {
|
|
31
|
+
if (!existsSync(root)) return []
|
|
32
|
+
|
|
33
|
+
return readdirSync(root).filter(
|
|
34
|
+
(name) => name !== lane_paths.SEAT_LOCK_DIR && !registered.has(path.join(root, name)),
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function is_lane_directory(directory: string): boolean {
|
|
39
|
+
return lane_paths.is_lane_entry(path.basename(directory)) && lstatSync(directory).isDirectory()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A removal that stops part-way is judged by what is left, exactly as `lane:close` judges its own —
|
|
43
|
+
// and what is left is a stray the next sweep reads again.
|
|
44
|
+
function remove(directory: string): StrayVerdict {
|
|
45
|
+
try {
|
|
46
|
+
rmSync(directory, { force: true, recursive: true })
|
|
47
|
+
} catch (error) {
|
|
48
|
+
error_text.trace_swallowed('lane_stray.remove', error)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return existsSync(directory) ? 'stuck' : 'removed'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// **A `lane:open` in flight holds a seat lock from before its `worktree add` until after the
|
|
55
|
+
// registration**, so while any lock is held a numbered directory may be a lane being created, not a
|
|
56
|
+
// leftover — it is kept, and the next sweep reads it again.
|
|
57
|
+
interface SweepContext {
|
|
58
|
+
recoverable: ReadonlySet<string>
|
|
59
|
+
is_opening: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function judge(directory: string, context: SweepContext): Stray {
|
|
63
|
+
if (!is_lane_directory(directory)) return { path: directory, verdict: 'not-a-lane', foreign: [] }
|
|
64
|
+
if (context.is_opening) return { path: directory, verdict: 'opening', foreign: [] }
|
|
65
|
+
|
|
66
|
+
const foreign = lane_leftover.foreign_paths(directory, context.recoverable)
|
|
67
|
+
|
|
68
|
+
if (foreign.length > 0) return { path: directory, verdict: 'foreign', foreign }
|
|
69
|
+
|
|
70
|
+
return { path: directory, verdict: remove(directory), foreign: [] }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// One stray that cannot be read — a permission, a file vanishing mid-walk — is kept and reported
|
|
74
|
+
// rather than thrown, so it never stops the sweep of the rest.
|
|
75
|
+
function judge_safely(directory: string, context: SweepContext): Stray {
|
|
76
|
+
try {
|
|
77
|
+
return judge(directory, context)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
error_text.trace_swallowed('lane_stray.judge', error)
|
|
80
|
+
|
|
81
|
+
return { path: directory, verdict: 'unreadable', foreign: [] }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Remove the lanes root's unregistered leftovers that hold nothing worth keeping; report the rest. */
|
|
86
|
+
async function sweep_strays(): Promise<Array<Stray>> {
|
|
87
|
+
const root = lane_paths.lane_root(await lane_registry.main_repository_root())
|
|
88
|
+
const registered = new Set(await lane_registry.registered_directories())
|
|
89
|
+
const names = stray_names(root, registered)
|
|
90
|
+
|
|
91
|
+
if (names.length === 0) return []
|
|
92
|
+
|
|
93
|
+
const context: SweepContext = {
|
|
94
|
+
recoverable: await lane_leftover.recoverable_objects(),
|
|
95
|
+
is_opening: lane_open.open_in_flight_count(root) > 0,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return names.map((name) => judge_safely(path.join(root, name), context))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The foreign paths a report names before it summarizes the rest as a count.
|
|
102
|
+
const NAMED_PATH_LIMIT = 5
|
|
103
|
+
|
|
104
|
+
function named_paths(foreign: ReadonlyArray<string>): string {
|
|
105
|
+
const rest = foreign.length - NAMED_PATH_LIMIT
|
|
106
|
+
const more = rest > 0 ? ` and ${String(rest)} more` : ''
|
|
107
|
+
|
|
108
|
+
return `${foreign.slice(0, NAMED_PATH_LIMIT).join(', ')}${more}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const KEPT_REASONS: Record<Exclude<StrayVerdict, 'foreign' | 'removed'>, string> = {
|
|
112
|
+
'not-a-lane': 'not a name the lane machinery writes, so it was left alone',
|
|
113
|
+
opening:
|
|
114
|
+
'a `lane:open` holds a seat lock and this may be its lane; the next sweep reads it again — if no `lane:open` is running, the lock outlived a crash: remove the lanes root `.seat-locks` directory',
|
|
115
|
+
stuck: 'the removal did not finish; the next `pnpm josh lane:prune` tries again',
|
|
116
|
+
unreadable: 'it could not be read, so it was left in place',
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** One line per stray, for standard error. */
|
|
120
|
+
function describe(stray: Stray): string {
|
|
121
|
+
if (stray.verdict === 'removed') return `Removed unregistered lane leftover ${stray.path}.`
|
|
122
|
+
|
|
123
|
+
if (stray.verdict === 'foreign') {
|
|
124
|
+
return `Kept ${stray.path}: it holds files git cannot restore (${named_paths(stray.foreign)}). Move them out and delete the directory.`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return `Kept ${stray.path}: ${KEPT_REASONS[stray.verdict]}.`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Sweep, and say on standard error what was removed and what was kept. */
|
|
131
|
+
async function sweep_and_report(): Promise<void> {
|
|
132
|
+
const strays = await sweep_strays()
|
|
133
|
+
|
|
134
|
+
for (const stray of strays) console.error(describe(stray))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const lane_stray = { describe, sweep_and_report, sweep_strays }
|
|
138
|
+
|
|
139
|
+
export type { Stray, StrayVerdict }
|
|
140
|
+
export { lane_stray }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { git_command } from '#scripts/git/git-command'
|
|
2
|
+
import { run_hold } from '#scripts/run/hold/run-hold'
|
|
3
|
+
import { lane_await } from './lane-await'
|
|
4
|
+
import type { LaneInfo } from './lane-registry'
|
|
5
|
+
|
|
6
|
+
// Whether an open lane holds nothing worth keeping (joshuafolkken/kit#3289): no commit on its branch
|
|
7
|
+
// beyond the default branch, a clean work tree, and no child or ship supervisor alive for its issue. A
|
|
8
|
+
// `backlogrun` cut between `lane:open` and `lane:dispatch` leaves exactly such a lane, and `lane:open`
|
|
9
|
+
// refuses it as `already-open` — so the next launch of that issue failed on a lane with nothing in it.
|
|
10
|
+
//
|
|
11
|
+
// **Every unreadable answer is "not vacant"**: `is_tree_dirty` already reads an unreadable status as
|
|
12
|
+
// dirty, and a branch whose commits cannot be counted is kept rather than closed, because closing it
|
|
13
|
+
// deletes the branch and anything on it.
|
|
14
|
+
async function has_no_commits(lane: LaneInfo): Promise<boolean> {
|
|
15
|
+
try {
|
|
16
|
+
const base = await git_command.default_branch_reference()
|
|
17
|
+
|
|
18
|
+
return (await git_command.commit_count_beyond(base, lane.branch)) === 0
|
|
19
|
+
} catch {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function is_occupied(lane: LaneInfo): boolean {
|
|
25
|
+
return lane.is_stranded || lane_await.is_process_running_default(lane.issue)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function is_vacant(lane: LaneInfo): Promise<boolean> {
|
|
29
|
+
if (is_occupied(lane) || (await run_hold.is_tree_dirty(lane.directory))) return false
|
|
30
|
+
|
|
31
|
+
return await has_no_commits(lane)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const lane_vacant = { is_vacant }
|
|
35
|
+
|
|
36
|
+
export { lane_vacant }
|