@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
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { agent_argv, type AgentArgv, type AgentArgvResult } from '#scripts/agent/agent-argv'
|
|
2
2
|
import { agent_role_profile, type AgentProfile } from '#scripts/agent/agent-role-profile'
|
|
3
3
|
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
4
|
+
import type { LabelWrite } from '#scripts/gh/git-gh-issue-write'
|
|
4
5
|
import { IN_PROGRESS_LABEL } from '#scripts/issue/issue-labels'
|
|
5
6
|
import { telegram_notify } from '#scripts/notify/telegram-notify'
|
|
6
7
|
import { detached_launch } from '#scripts/run/detached-launch'
|
|
8
|
+
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
9
|
+
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
7
10
|
import { run_ending } from '#scripts/run/run-ending'
|
|
8
|
-
import { run_event_stream } from '#scripts/run/run-event-stream'
|
|
9
|
-
import { run_event_stream_emit } from '#scripts/run/run-event-stream-emit'
|
|
10
11
|
import { run_issue_number } from '#scripts/run/run-issue-number'
|
|
11
12
|
import { lane_child_invocation } from './lane-child-invocation'
|
|
12
13
|
import { lane_child_marker } from './lane-child-marker'
|
|
@@ -79,7 +80,7 @@ type DispatchOutcome =
|
|
|
79
80
|
// that marks a child as holding its lane — the lane count, the offer filter, `run:progress` and the
|
|
80
81
|
// stale check all read it — so a child that ran without it would be counted as an empty lane and
|
|
81
82
|
// handed to a second session. Refusing the launch keeps "dispatched" and "counted as busy" one state.
|
|
82
|
-
| { kind: typeof LABEL_UNSET_KIND }
|
|
83
|
+
| { kind: typeof LABEL_UNSET_KIND; reason: string }
|
|
83
84
|
|
|
84
85
|
type LogOutcome = { kind: 'ready'; path: string } | { kind: 'unrecordable'; reason: string }
|
|
85
86
|
|
|
@@ -305,9 +306,10 @@ function prepared(lane: LaneInfo): Prepared {
|
|
|
305
306
|
// to its (idempotent) apply. `issue_add_label` creates the label if the repository lacks it — a
|
|
306
307
|
// `POST /issues/{N}/labels` provisions a missing label with a generated color (measured on
|
|
307
308
|
// joshuafolkken/kit#1026, `git-gh-issue-write.ts`) — so no separate `label_ensure` is needed here, and
|
|
308
|
-
// its
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
// its unapplied answer is a real refusal to launch rather than a note — confirmed by a read-back, so
|
|
310
|
+
// a write that landed but answered with an error still launches (joshuafolkken/kit#3312).
|
|
311
|
+
async function mark_in_progress(issue: string): Promise<LabelWrite> {
|
|
312
|
+
return await git_gh_command.issue_apply_label(issue, IN_PROGRESS_LABEL)
|
|
311
313
|
}
|
|
312
314
|
|
|
313
315
|
// The dispatch owns both halves of the marker: it applied it, so a launch that never started takes it
|
|
@@ -324,8 +326,12 @@ async function unmark_in_progress(issue: string): Promise<void> {
|
|
|
324
326
|
|
|
325
327
|
// A started child is recorded on the run's stream, which is where the stall detector ages the last
|
|
326
328
|
// dispatch from (joshuafolkken/kit#2464) — unrecorded, every stall read "hours since the last dispatch"
|
|
327
|
-
// minutes after a launch. A failed start records nothing: no child is running.
|
|
329
|
+
// minutes after a launch. A failed start records nothing: no child is running. The marker is claimed
|
|
330
|
+
// first, so a refused one starts nothing.
|
|
328
331
|
async function finish_dispatch(issue: string, request: StartRequest): Promise<DispatchOutcome> {
|
|
332
|
+
const mark = await mark_in_progress(issue)
|
|
333
|
+
|
|
334
|
+
if (!mark.is_applied) return { kind: LABEL_UNSET_KIND, reason: mark.reason }
|
|
329
335
|
const outcome = await started(request)
|
|
330
336
|
|
|
331
337
|
const is_failed = outcome.kind === 'failed'
|
|
@@ -353,8 +359,6 @@ async function dispatch_child(issue: string): Promise<DispatchOutcome> {
|
|
|
353
359
|
|
|
354
360
|
if (log.kind !== 'ready') return log
|
|
355
361
|
|
|
356
|
-
if (!(await mark_in_progress(issue))) return { kind: LABEL_UNSET_KIND }
|
|
357
|
-
|
|
358
362
|
return await finish_dispatch(issue, { lane, log_path: log.path, ...built })
|
|
359
363
|
}
|
|
360
364
|
|
|
@@ -410,7 +414,7 @@ function describe(outcome: DispatchOutcome, issue: string): string {
|
|
|
410
414
|
if (outcome.kind === 'unrecordable') return outcome.reason
|
|
411
415
|
|
|
412
416
|
if (outcome.kind === LABEL_UNSET_KIND) {
|
|
413
|
-
return `The child for #${issue} was not started: the \`${IN_PROGRESS_LABEL}\` label could not be applied, so the lane would be counted as empty. Nothing was launched.`
|
|
417
|
+
return `The child for #${issue} was not started: the \`${IN_PROGRESS_LABEL}\` label could not be applied (${outcome.reason}), so the lane would be counted as empty. Nothing was launched.`
|
|
414
418
|
}
|
|
415
419
|
|
|
416
420
|
if (outcome.kind === 'failed') {
|
|
@@ -185,9 +185,13 @@ function replace_assignment(
|
|
|
185
185
|
key: string,
|
|
186
186
|
): Array<string> {
|
|
187
187
|
return lines.flatMap((entry, index) => {
|
|
188
|
+
// One return type across the branches (sonarjs/function-return-type) keeps the array wrappers.
|
|
189
|
+
// eslint-disable-next-line unicorn/no-unnecessary-array-flat-map -- see above
|
|
188
190
|
if (index === keep) return [assignment]
|
|
191
|
+
if (is_key_line(entry, key)) return []
|
|
189
192
|
|
|
190
|
-
|
|
193
|
+
// eslint-disable-next-line unicorn/no-unnecessary-array-flat-map -- see above
|
|
194
|
+
return [entry]
|
|
191
195
|
})
|
|
192
196
|
}
|
|
193
197
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
2
|
-
import { run_ship_detach } from '#scripts/run/run-ship-detach'
|
|
2
|
+
import { run_ship_detach } from '#scripts/run/ship/run-ship-detach'
|
|
3
3
|
import { lane_child_marker, type MarkerSource } from './lane-child-marker'
|
|
4
4
|
|
|
5
5
|
// Whether a lane has handed its post-implementation region to the detached ship (joshuafolkken/kit#2962).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { buffered_process } from '#scripts/lib/buffered-process'
|
|
4
|
+
import { INSTALL_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
4
5
|
|
|
5
6
|
// Filling the lane, which is the other half of opening one (joshuafolkken/kit#1554).
|
|
6
7
|
//
|
|
@@ -34,12 +35,6 @@ const MANIFEST_FILE_NAME = 'package.json'
|
|
|
34
35
|
// The lane opens with no dependencies installed, and that is the reported state rather than a
|
|
35
36
|
// failure — there was nothing to install, so nothing failed.
|
|
36
37
|
const NO_MANIFEST_OUTPUT = `No ${MANIFEST_FILE_NAME} in the lane, so the dependency install was skipped.`
|
|
37
|
-
// The 3.5 s above is a warm store; a first run, or one after a dependency change, goes to the
|
|
38
|
-
// network, so that figure is not the bound to write here. Ten minutes covers that fetch and still
|
|
39
|
-
// ends `lane:open` when a registry never answers, rather than holding a parallel run open with
|
|
40
|
-
// nothing printed. It is well under the fan-out's own half hour because this one blocks a command a
|
|
41
|
-
// person is waiting on, not a check suite.
|
|
42
|
-
const INSTALL_TIMEOUT_MS = 600_000
|
|
43
38
|
|
|
44
39
|
interface InstallResult {
|
|
45
40
|
is_installed: boolean
|
|
@@ -85,10 +80,4 @@ async function install_dependencies(directory: string): Promise<InstallResult> {
|
|
|
85
80
|
const lane_install = { install_dependencies }
|
|
86
81
|
|
|
87
82
|
export type { InstallResult }
|
|
88
|
-
export {
|
|
89
|
-
lane_install,
|
|
90
|
-
INSTALL_ARGUMENTS,
|
|
91
|
-
INSTALL_TIMEOUT_MS,
|
|
92
|
-
MANIFEST_FILE_NAME,
|
|
93
|
-
NO_MANIFEST_OUTPUT,
|
|
94
|
-
}
|
|
83
|
+
export { lane_install, INSTALL_ARGUMENTS, MANIFEST_FILE_NAME, NO_MANIFEST_OUTPUT }
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
4
|
-
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
4
|
+
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
5
|
+
import { lane_vacant } from '#scripts/lane/lane-vacant'
|
|
5
6
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
6
7
|
import { INSTALL_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
7
8
|
import { execa } from 'execa'
|
|
@@ -88,17 +89,30 @@ type LaunchOutcome = { kind: 'launched'; pid: string } | { kind: 'unopened' } |
|
|
|
88
89
|
const UNOPENED: LaunchOutcome = { kind: 'unopened' }
|
|
89
90
|
const FAILED: LaunchOutcome = { kind: 'failed' }
|
|
90
91
|
|
|
92
|
+
async function close_if_vacant(lane: LaneInfo | undefined): Promise<void> {
|
|
93
|
+
if (lane === undefined || !(await lane_vacant.is_vacant(lane))) return
|
|
94
|
+
|
|
95
|
+
await josh_command.josh_run(['lane:close', lane.issue], should_forward_stderr)
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
// **A lane a park kept open is resumed, not reopened** (joshuafolkken/kit#3228). A child parked before
|
|
92
99
|
// its commit leaves its uncommitted work in its lane, and `lane:open` refuses that lane as
|
|
93
100
|
// `already-open` — so the released child is dispatched into the kept tree instead. **Only a lane a
|
|
94
101
|
// child was already dispatched into counts as kept**: `lane:dispatch` records the child's output in the
|
|
95
102
|
// lane's `.env`, and a lane whose install failed never got that far, so it still goes through
|
|
96
103
|
// `lane:open` and its refusal. So does a stranded lane, whose `.env` is gone with its tree.
|
|
104
|
+
//
|
|
105
|
+
// **A lane no child was dispatched into and that holds nothing is closed and opened afresh**
|
|
106
|
+
// (joshuafolkken/kit#3289) — a run cut between the open and the dispatch leaves one. It is reopened
|
|
107
|
+
// rather than reused because nothing recorded whether its install finished. A lane with work or a live
|
|
108
|
+
// process in it still meets `lane:open`'s refusal.
|
|
97
109
|
async function lane_directory(issue: string): Promise<string | undefined> {
|
|
98
110
|
const kept = await lane_registry.find_open_lane(issue)
|
|
99
111
|
|
|
100
112
|
if (kept?.output !== undefined) return kept.directory
|
|
101
113
|
|
|
114
|
+
await close_if_vacant(kept)
|
|
115
|
+
|
|
102
116
|
const opened = await josh_command.josh_run(['lane:open', issue], should_forward_stderr)
|
|
103
117
|
|
|
104
118
|
return opened.code === SUCCESS_EXIT_CODE ? opened.out : undefined
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { closeSync, constants, openSync, writeSync } from 'node:fs'
|
|
2
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
3
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
4
|
+
import { run_carry } from '#scripts/run/carry/run-carry'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
// The measurement ledger the lane limit is tuned against (joshuafolkken/kit#3355): one JSON line per
|
|
8
|
+
// lane merge, per finished `josh gate`, and per machine-load sample, read back by `josh lane:stats`.
|
|
9
|
+
//
|
|
10
|
+
// **Its own file rather than the run's event stream.** The stream is capped at 500 events and is
|
|
11
|
+
// rewritten whole on every append, which is right for "where is the run" and wrong for a measurement
|
|
12
|
+
// that spans days of one-a-minute load samples. This file is only ever appended to, so concurrent lanes
|
|
13
|
+
// never lose each other's lines to a read-modify-write race.
|
|
14
|
+
//
|
|
15
|
+
// **Keyed on the common git directory, the event stream's key**, so a lane child's gate, the parent's
|
|
16
|
+
// merge and the sampler all land in one ledger per repository. It sits in the temp root beside every
|
|
17
|
+
// other josh record; a reboot clears it, which is why `docs/maintainers/lane-limit-measurement.md`
|
|
18
|
+
// closes a period with `josh lane:stats` before one.
|
|
19
|
+
|
|
20
|
+
const LEDGER_PREFIX = 'josh-lane-ledger-'
|
|
21
|
+
const LEDGER_SUFFIX = '.jsonl'
|
|
22
|
+
const LINE_SEPARATOR = '\n'
|
|
23
|
+
const LEDGER_FILE_MODE = 0o600
|
|
24
|
+
// The flags `detached-launch.ts` appends its log with, for the same deterministic shared-temp path.
|
|
25
|
+
const { O_APPEND, O_CREAT, O_NOFOLLOW, O_WRONLY } = constants
|
|
26
|
+
// eslint-disable-next-line no-bitwise -- POSIX open flags are a bit field; `||` here would be a bug
|
|
27
|
+
const APPEND_FLAGS = O_WRONLY | O_APPEND | O_CREAT | O_NOFOLLOW
|
|
28
|
+
|
|
29
|
+
const KIND = { MERGE: 'merge', GATE: 'gate', LOAD: 'load' } as const
|
|
30
|
+
|
|
31
|
+
const merge_schema = z.object({ kind: z.literal(KIND.MERGE), at: z.string(), issue: z.number() })
|
|
32
|
+
const gate_schema = z.object({
|
|
33
|
+
kind: z.literal(KIND.GATE),
|
|
34
|
+
at: z.string(),
|
|
35
|
+
elapsed_ms: z.number(),
|
|
36
|
+
is_passed: z.boolean(),
|
|
37
|
+
})
|
|
38
|
+
// `swap_mb` and `lanes` are optional because either read can fail on its own — a platform with no
|
|
39
|
+
// swap reading, a `git worktree` call that did not answer — and a sample is still worth its load.
|
|
40
|
+
const load_schema = z.object({
|
|
41
|
+
kind: z.literal(KIND.LOAD),
|
|
42
|
+
at: z.string(),
|
|
43
|
+
load: z.number(),
|
|
44
|
+
free_mb: z.number(),
|
|
45
|
+
swap_mb: z.number().optional(),
|
|
46
|
+
lanes: z.number().optional(),
|
|
47
|
+
})
|
|
48
|
+
const entry_schema = z.discriminatedUnion('kind', [merge_schema, gate_schema, load_schema])
|
|
49
|
+
|
|
50
|
+
type LedgerEntry = z.infer<typeof entry_schema>
|
|
51
|
+
type MergeEntry = z.infer<typeof merge_schema>
|
|
52
|
+
type GateEntry = z.infer<typeof gate_schema>
|
|
53
|
+
type LoadEntry = z.infer<typeof load_schema>
|
|
54
|
+
|
|
55
|
+
function target_of(repository: string): string {
|
|
56
|
+
return stamp_file.stamp_path(LEDGER_PREFIX, repository, LEDGER_SUFFIX)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The ledger of the repository this checkout belongs to, or `undefined` outside one. `git rev-parse`
|
|
60
|
+
// rejects outside a repository, and `josh gate` resolves this before it runs a check, so the rejection
|
|
61
|
+
// is read as "no ledger" rather than allowed to fail the gate it would only have measured.
|
|
62
|
+
async function target(): Promise<string | undefined> {
|
|
63
|
+
try {
|
|
64
|
+
const repository = await run_carry.repository_directory()
|
|
65
|
+
|
|
66
|
+
return repository === undefined ? undefined : target_of(repository)
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// **Append, never rewrite** — `O_APPEND` keeps each short line whole across concurrent writers, and
|
|
73
|
+
// `O_NOFOLLOW` plus `stamp_file`'s ownership test keep a planted link or another account's file from
|
|
74
|
+
// receiving it.
|
|
75
|
+
function append(path: string, entry: LedgerEntry): void {
|
|
76
|
+
const descriptor = openSync(path, APPEND_FLAGS, LEDGER_FILE_MODE)
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
if (stamp_file.is_own_regular_file(path)) {
|
|
80
|
+
writeSync(descriptor, `${JSON.stringify(entry)}${LINE_SEPARATOR}`)
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
closeSync(descriptor)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Best-effort: a measurement must never fail the gate or the merge it measures, so every error — no
|
|
88
|
+
// repository, an unwritable temp root — is dropped.
|
|
89
|
+
async function record(entry: LedgerEntry, path?: string): Promise<void> {
|
|
90
|
+
try {
|
|
91
|
+
const destination = path ?? (await target())
|
|
92
|
+
|
|
93
|
+
if (destination !== undefined) append(destination, entry)
|
|
94
|
+
} catch {
|
|
95
|
+
// Reporting is best-effort: a failed append is dropped rather than raised into the caller's work.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function now_iso(): string {
|
|
100
|
+
return new Date().toISOString()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// One finished `josh gate`, whether it passed or not — a failing gate cost the lane its time as well.
|
|
104
|
+
async function record_gate(path: string, elapsed_ms: number, is_passed: boolean): Promise<void> {
|
|
105
|
+
await record({ kind: KIND.GATE, at: now_iso(), elapsed_ms, is_passed }, path)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function record_merge(issue: number): Promise<void> {
|
|
109
|
+
await record({ kind: KIND.MERGE, at: now_iso(), issue })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parse_line(line: string): LedgerEntry | undefined {
|
|
113
|
+
return entry_schema.safeParse(json_value.parse_or_undefined(line)).data
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function is_present(entry: LedgerEntry | undefined): entry is LedgerEntry {
|
|
117
|
+
return entry !== undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Every entry, oldest first. A malformed line — a write cut short — is skipped rather than making the
|
|
121
|
+
// whole ledger unreadable, and an absent or unowned file is an empty ledger.
|
|
122
|
+
function read_entries(path: string): ReadonlyArray<LedgerEntry> {
|
|
123
|
+
const raw = stamp_file.read_stamp_text(path)
|
|
124
|
+
|
|
125
|
+
if (raw === undefined) return []
|
|
126
|
+
|
|
127
|
+
return raw
|
|
128
|
+
.split(LINE_SEPARATOR)
|
|
129
|
+
.map((line) => parse_line(line.trim()))
|
|
130
|
+
.filter(is_present)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const lane_ledger = {
|
|
134
|
+
KIND,
|
|
135
|
+
append,
|
|
136
|
+
read_entries,
|
|
137
|
+
record,
|
|
138
|
+
record_gate,
|
|
139
|
+
record_merge,
|
|
140
|
+
target,
|
|
141
|
+
target_of,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type { GateEntry, LedgerEntry, LoadEntry, MergeEntry }
|
|
145
|
+
export { lane_ledger }
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { existsSync, lstatSync, readdirSync, readFileSync, readlinkSync, rmSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import { ENV_FILE_NAME } from '#ports'
|
|
4
5
|
import { git_spawn } from '#scripts/git/git-spawn'
|
|
6
|
+
import { IGNORED_CACHE_FILES } from '#scripts/josh/josh-command-types'
|
|
5
7
|
|
|
6
8
|
// A lane directory git no longer registers as a work tree (joshuafolkken/kit#2857).
|
|
7
9
|
//
|
|
@@ -11,31 +13,38 @@ import { git_spawn } from '#scripts/git/git-spawn'
|
|
|
11
13
|
// here before it adds the tree.
|
|
12
14
|
//
|
|
13
15
|
// **The test for removing it is that removing it loses nothing**, not a list of names. Every file in
|
|
14
|
-
// it has to
|
|
15
|
-
// write again; the one exception is the `.git` file, a pointer to the registration that is
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
16
|
+
// it has to hold content the repository's history already stores — a blob some ref reaches — so git
|
|
17
|
+
// can write it again; the one exception is the `.git` file, a pointer to the registration that is
|
|
18
|
+
// gone. **Any commit's content, not only `HEAD`'s at the same path** (joshuafolkken/kit#3370): a
|
|
19
|
+
// leftover is a checkout of whatever commit its lane was on, and measured against `HEAD` the
|
|
20
|
+
// leftovers that issue found differed in 167 to 1,431 files each while every one of them was in the
|
|
21
|
+
// history. A list of generated names would break the next time a file is generated, and would delete
|
|
22
|
+
// whatever shares one of those names.
|
|
23
|
+
//
|
|
24
|
+
// **The one list of names is what `lane:open` and the gate regenerate**, and only at the top level:
|
|
25
|
+
// the install's `node_modules`, the build's `dist`, the `.env` `lane:open` writes from the root's, and
|
|
26
|
+
// the gate's caches. A lane carries them on top of its checkout, none is in the history, and walking
|
|
27
|
+
// `node_modules` alone would hash tens of thousands of files.
|
|
28
|
+
const REGENERATED_PATHS: ReadonlySet<string> = new Set([
|
|
29
|
+
'node_modules',
|
|
30
|
+
'dist',
|
|
31
|
+
ENV_FILE_NAME,
|
|
32
|
+
...IGNORED_CACHE_FILES,
|
|
33
|
+
])
|
|
22
34
|
const GIT_POINTER = '.git'
|
|
23
35
|
const GITDIR_PREFIX = 'gitdir: '
|
|
24
36
|
// The foreign paths a refusal names before it summarizes the rest as a count.
|
|
25
37
|
const NAMED_PATH_LIMIT = 10
|
|
26
38
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (entry_path === undefined || object === undefined) return []
|
|
33
|
-
|
|
34
|
-
return [[entry_path, object]]
|
|
39
|
+
// `git rev-list --objects --all`: one `<object>[ <path>]` line per object any ref reaches — commits,
|
|
40
|
+
// trees and blobs alike, which costs nothing here, because only a blob id can equal a file's.
|
|
41
|
+
function parse_objects(listing: string): Set<string> {
|
|
42
|
+
return new Set(listing.split('\n').map((line) => line.split(' ', 1)[0] ?? ''))
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
/** Every object id the repository's refs reach — the content a leftover can be restored from. */
|
|
46
|
+
async function recoverable_objects(): Promise<Set<string>> {
|
|
47
|
+
return parse_objects(await git_spawn.read(['rev-list', '--objects', '--all']))
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
// The object id git gives this file — a symbolic link's is that of its target string, as git stores it.
|
|
@@ -59,13 +68,18 @@ function walk_files(directory: string, relative: string): Array<string> {
|
|
|
59
68
|
return readdirSync(path.join(directory, relative), { withFileTypes: true }).flatMap((entry) => {
|
|
60
69
|
const child = relative === '' ? entry.name : `${relative}/${entry.name}`
|
|
61
70
|
|
|
62
|
-
return entry.isDirectory() ? walk_files(directory, child) :
|
|
71
|
+
return entry.isDirectory() ? walk_files(directory, child) : child
|
|
63
72
|
})
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
// Every file under `directory` as a repository path, symbolic links included and never followed
|
|
75
|
+
// Every file under `directory` as a repository path, symbolic links included and never followed —
|
|
76
|
+
// less the regenerated top-level entries, which are neither walked nor listed.
|
|
67
77
|
function list_files(directory: string): Array<string> {
|
|
68
|
-
return
|
|
78
|
+
return readdirSync(directory)
|
|
79
|
+
.filter((name) => !REGENERATED_PATHS.has(name))
|
|
80
|
+
.flatMap((name) =>
|
|
81
|
+
lstatSync(path.join(directory, name)).isDirectory() ? walk_files(directory, name) : name,
|
|
82
|
+
)
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
// The `.git` file is disposable only while the registration it points to is gone: one that still
|
|
@@ -85,25 +99,25 @@ function is_dangling_pointer(directory: string): boolean {
|
|
|
85
99
|
function is_reclaimable_file(
|
|
86
100
|
directory: string,
|
|
87
101
|
relative: string,
|
|
88
|
-
|
|
102
|
+
recoverable: ReadonlySet<string>,
|
|
89
103
|
): boolean {
|
|
90
104
|
if (relative === GIT_POINTER) return is_dangling_pointer(directory)
|
|
91
105
|
|
|
92
|
-
return
|
|
106
|
+
return recoverable.has(blob_id(path.join(directory, relative)))
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
/**
|
|
96
110
|
* The paths under `directory` whose removal would lose something — empty when it holds nothing but
|
|
97
|
-
* the
|
|
98
|
-
* so it is foreign as a whole rather than walked.
|
|
111
|
+
* content the history stores, the regenerated entries and the `.git` pointer. A `.git` directory is a
|
|
112
|
+
* repository of its own, so it is foreign as a whole rather than walked.
|
|
99
113
|
*/
|
|
100
|
-
function foreign_paths(directory: string,
|
|
114
|
+
function foreign_paths(directory: string, recoverable: ReadonlySet<string>): Array<string> {
|
|
101
115
|
const pointer = lstatSync(path.join(directory, GIT_POINTER), { throwIfNoEntry: false })
|
|
102
116
|
|
|
103
117
|
if (pointer?.isDirectory() === true) return [GIT_POINTER]
|
|
104
118
|
|
|
105
119
|
return list_files(directory).filter(
|
|
106
|
-
(relative) => !is_reclaimable_file(directory, relative,
|
|
120
|
+
(relative) => !is_reclaimable_file(directory, relative, recoverable),
|
|
107
121
|
)
|
|
108
122
|
}
|
|
109
123
|
|
|
@@ -125,8 +139,7 @@ function refusal(directory: string, foreign: ReadonlyArray<string>): Error {
|
|
|
125
139
|
async function reclaim(directory: string): Promise<void> {
|
|
126
140
|
if (!existsSync(directory)) return
|
|
127
141
|
|
|
128
|
-
const
|
|
129
|
-
const foreign = foreign_paths(directory, tracked)
|
|
142
|
+
const foreign = foreign_paths(directory, await recoverable_objects())
|
|
130
143
|
|
|
131
144
|
if (foreign.length > 0) throw refusal(directory, foreign)
|
|
132
145
|
|
|
@@ -136,8 +149,9 @@ async function reclaim(directory: string): Promise<void> {
|
|
|
136
149
|
const lane_leftover = {
|
|
137
150
|
blob_id,
|
|
138
151
|
foreign_paths,
|
|
139
|
-
|
|
152
|
+
parse_objects,
|
|
140
153
|
reclaim,
|
|
154
|
+
recoverable_objects,
|
|
141
155
|
}
|
|
142
156
|
|
|
143
157
|
export { lane_leftover }
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { freemem, loadavg } from 'node:os'
|
|
3
|
+
import { execa } from 'execa'
|
|
4
|
+
import { lane_await } from './lane-await'
|
|
5
|
+
import type { LoadEntry } from './lane-ledger'
|
|
6
|
+
import { lane_registry, type LaneInfo } from './lane-registry'
|
|
7
|
+
|
|
8
|
+
// One machine-load sample for the lane-limit measurement (joshuafolkken/kit#3355): the one-minute load
|
|
9
|
+
// average, free memory, swap in use and how many lanes have a child working, read at one instant so
|
|
10
|
+
// `josh lane:stats` can set the load beside the lane count that produced it.
|
|
11
|
+
//
|
|
12
|
+
// **Swap is read per platform and is optional.** Node exposes no swap figure, so macOS asks `sysctl` and
|
|
13
|
+
// Linux reads `/proc/meminfo`; any other platform, or an output neither parser recognizes, records the
|
|
14
|
+
// sample without it rather than as a zero nobody measured.
|
|
15
|
+
|
|
16
|
+
const BYTES_PER_MB = 1_048_576
|
|
17
|
+
const MB_PER_GB = 1024
|
|
18
|
+
const KB_PER_MB = 1024
|
|
19
|
+
const DARWIN_SWAP = /used = (?<amount>[\d.]+)(?<unit>[MG])/u
|
|
20
|
+
const LINUX_SWAP_TOTAL = /^SwapTotal:\s+(?<kb>\d+) kB/mu
|
|
21
|
+
const LINUX_SWAP_FREE = /^SwapFree:\s+(?<kb>\d+) kB/mu
|
|
22
|
+
const MEMINFO_PATH = '/proc/meminfo'
|
|
23
|
+
const SYSCTL_ARGUMENTS = ['-n', 'vm.swapusage']
|
|
24
|
+
const GB_UNIT = 'G'
|
|
25
|
+
|
|
26
|
+
// `sysctl -n vm.swapusage` → `total = 5120.00M used = 3993.25M free = 1126.75M (encrypted)`.
|
|
27
|
+
function parse_darwin_swap(output: string): number | undefined {
|
|
28
|
+
const groups = DARWIN_SWAP.exec(output)?.groups
|
|
29
|
+
|
|
30
|
+
if (groups?.['amount'] === undefined) return undefined
|
|
31
|
+
|
|
32
|
+
const scale = groups['unit'] === GB_UNIT ? MB_PER_GB : 1
|
|
33
|
+
|
|
34
|
+
return Number(groups['amount']) * scale
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function kilobytes_of(meminfo: string, pattern: RegExp): number | undefined {
|
|
38
|
+
const kb = pattern.exec(meminfo)?.groups?.['kb']
|
|
39
|
+
|
|
40
|
+
return kb === undefined ? undefined : Number(kb)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// `/proc/meminfo` carries the total and the free swap; what is in use is their difference.
|
|
44
|
+
function parse_linux_swap(meminfo: string): number | undefined {
|
|
45
|
+
const total = kilobytes_of(meminfo, LINUX_SWAP_TOTAL)
|
|
46
|
+
const free = kilobytes_of(meminfo, LINUX_SWAP_FREE)
|
|
47
|
+
|
|
48
|
+
if (total === undefined || free === undefined) return undefined
|
|
49
|
+
|
|
50
|
+
return (total - free) / KB_PER_MB
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function read_swap_mb(
|
|
54
|
+
platform: NodeJS.Platform = process.platform,
|
|
55
|
+
): Promise<number | undefined> {
|
|
56
|
+
try {
|
|
57
|
+
if (platform === 'darwin') {
|
|
58
|
+
const { stdout } = await execa('sysctl', SYSCTL_ARGUMENTS)
|
|
59
|
+
|
|
60
|
+
return parse_darwin_swap(stdout)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return platform === 'linux' ? parse_linux_swap(await readFile(MEMINFO_PATH, 'utf8')) : undefined
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The lanes with a child at work. An open lane is not enough: a stranded lane, or one a park kept, holds
|
|
70
|
+
// a tree but no running child, so it adds no load — the child's process is what `lane:await` polls.
|
|
71
|
+
function count_working(
|
|
72
|
+
lanes: ReadonlyArray<LaneInfo>,
|
|
73
|
+
is_running: (issue: string) => boolean = lane_await.is_process_running_default,
|
|
74
|
+
): number {
|
|
75
|
+
return lanes.filter((lane) => !lane.is_stranded && is_running(lane.issue)).length
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function read_lanes(): Promise<number | undefined> {
|
|
79
|
+
try {
|
|
80
|
+
return count_working(await lane_registry.list_lanes())
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function sample(at: string): Promise<LoadEntry> {
|
|
87
|
+
const [swap_mb, lanes] = await Promise.all([read_swap_mb(), read_lanes()])
|
|
88
|
+
const [load = 0] = loadavg()
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
kind: 'load',
|
|
92
|
+
at,
|
|
93
|
+
load,
|
|
94
|
+
free_mb: Math.round(freemem() / BYTES_PER_MB),
|
|
95
|
+
swap_mb,
|
|
96
|
+
lanes,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const lane_load = {
|
|
101
|
+
count_working,
|
|
102
|
+
parse_darwin_swap,
|
|
103
|
+
parse_linux_swap,
|
|
104
|
+
read_swap_mb,
|
|
105
|
+
sample,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { lane_load }
|