@joshuafolkken/kit 1.1455.0 → 1.1514.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +6 -6
- package/.claude/skills/dependency-update/SKILL.md +26 -55
- package/.claude/skills/epic-commands/SKILL.md +83 -156
- package/.claude/skills/epic-commands/epic-bundle.md +51 -79
- package/.claude/skills/epic-commands/execution-waves.md +43 -81
- package/.claude/skills/verify-ui/SKILL.md +24 -50
- package/.claude/skills/workflow-commands/SKILL.md +7 -9
- package/.claude/skills/workflow-commands/background-commands.md +31 -46
- package/.claude/skills/workflow-commands/backlogrun-child.md +65 -85
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +86 -125
- package/.claude/skills/workflow-commands/backlogrun-park.md +68 -118
- package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -28
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +17 -26
- package/.claude/skills/workflow-commands/backlogrun-steps.md +35 -52
- package/.claude/skills/workflow-commands/backlogrun.md +31 -50
- package/.claude/skills/workflow-commands/chain-rule.md +31 -40
- package/.claude/skills/workflow-commands/delegation.md +12 -8
- package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
- package/.claude/skills/workflow-commands/followup.md +55 -77
- package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
- package/.claude/skills/workflow-commands/fullrun.md +4 -6
- package/.claude/skills/workflow-commands/halfrun.md +3 -7
- package/.claude/skills/workflow-commands/into-target.md +1 -2
- package/.claude/skills/workflow-commands/issue-comments.md +43 -60
- package/.claude/skills/workflow-commands/issue-scout.md +3 -2
- package/.claude/skills/workflow-commands/kickoff.md +65 -74
- package/.claude/skills/workflow-commands/latest-gate.md +2 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
- package/.claude/skills/workflow-commands/observation-filing.md +59 -118
- package/.claude/skills/workflow-commands/observation-ledger.md +52 -112
- package/.claude/skills/workflow-commands/pre-gate-cut.md +88 -142
- package/.claude/skills/workflow-commands/prerequisite.md +11 -17
- package/.claude/skills/workflow-commands/progress-watcher.md +23 -33
- package/.claude/skills/workflow-commands/prrun.md +3 -5
- package/.claude/skills/workflow-commands/retrospective.md +35 -54
- package/.claude/skills/workflow-commands/split-assessment.md +32 -58
- package/.claude/skills/workflow-commands/target-repository.md +1 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +54 -73
- package/.codex/hooks.json +6 -6
- package/.github/actions/setup-node/action.yml +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CLAUDE.md +7 -6
- package/cspell/index.yaml +7 -0
- package/dist/CLAUDE.md +7 -6
- package/dist/commands/document-section-cli.js +67 -34
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/chunk-4KKD2UTK.js +66 -0
- package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-6TS2JD6D.js} +14 -3
- package/dist/hooks/chunk-7DSBJVFH.js +50 -0
- package/dist/hooks/{chunk-5H4SSDSD.js → chunk-A65Y4YOR.js} +3 -3
- package/dist/hooks/{chunk-3C4XRYG6.js → chunk-APUO6U4J.js} +102 -19
- package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
- package/dist/hooks/chunk-GP4IEC4O.js +20 -0
- package/dist/hooks/chunk-JP45SOCJ.js +56 -0
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-LAOV7NLM.js} +1 -1
- package/dist/hooks/chunk-M4HGZV7U.js +83 -0
- package/dist/hooks/{chunk-TBJFTSUY.js → chunk-O6GDRNJB.js} +9 -7
- package/dist/hooks/{chunk-HFGWYGUV.js → chunk-PF2NZL2A.js} +5 -5
- package/dist/hooks/{chunk-WNLVGT7E.js → chunk-PTICX4OL.js} +4 -5
- package/dist/hooks/{chunk-HHI6FO3R.js → chunk-RT27QRJW.js} +763 -1515
- package/dist/hooks/{chunk-7WQBPLUN.js → chunk-SYOTDYCI.js} +1149 -1095
- package/dist/hooks/{chunk-LIAFCXL7.js → chunk-TNUMY6NL.js} +1519 -1332
- package/dist/hooks/chunk-UPUZF3AN.js +279 -0
- package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-WK6SMBIC.js} +1932 -784
- package/dist/hooks/codex-hook-adapter.js +17 -14
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +5 -4
- package/dist/hooks/inputs.json +302 -247
- package/dist/hooks/investigation-guard.js +8 -11
- package/dist/hooks/pretool-guard.js +16 -13
- package/dist/hooks/session-lang.js +372 -26
- package/dist/hooks/stop-guard.js +2593 -189
- package/dist/josh.js +2128 -282
- package/dist/managed-marker/index.js +1 -2
- package/dist/version/index.js +15 -12
- package/eslint/base.js +2 -1
- package/eslint/rules/unicorn.js +16 -0
- package/lefthook/base.yml +5 -3
- package/package.json +14 -12
- package/prompts/coding-standards.md +12 -22
- package/prompts/collaboration-workflow/file-edits.md +25 -45
- package/prompts/collaboration-workflow/gh-rest.md +19 -36
- package/prompts/collaboration-workflow/glossary.md +5 -7
- package/prompts/collaboration-workflow/issue-citation.md +6 -14
- package/prompts/collaboration-workflow/issue-template.md +40 -32
- package/prompts/collaboration-workflow/lane-instructions.md +35 -0
- package/prompts/collaboration-workflow/operating-rules.md +33 -46
- package/prompts/collaboration-workflow/output-bounds.md +15 -15
- package/prompts/collaboration-workflow/overview.md +14 -19
- package/prompts/collaboration-workflow/plan-comment.md +13 -13
- package/prompts/collaboration-workflow/principles.md +57 -54
- package/prompts/collaboration-workflow/proposal-request.md +1 -9
- package/prompts/collaboration-workflow/report-format.md +41 -49
- package/prompts/collaboration-workflow/residency.md +15 -11
- package/prompts/collaboration-workflow/rule-delivery.md +7 -15
- package/prompts/collaboration-workflow/shell-body.md +10 -20
- package/prompts/collaboration-workflow/turn-batching.md +8 -14
- package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
- package/prompts/collaboration-workflow/wip-cap.md +26 -32
- package/prompts/collaboration-workflow.md +25 -25
- package/prompts/refactoring.md +33 -43
- package/prompts/review-rubric.md +84 -129
- package/prompts/review.md +110 -141
- package/prompts/sonar-hotspot-handling.md +22 -50
- package/prompts/testing-guide.md +48 -99
- package/scripts/adopt/adopt-toolkits.ts +2 -4
- package/scripts/agent/agent-argv.ts +20 -4
- package/scripts/agent/agent-exit-record.ts +23 -1
- package/scripts/agent/agent-role-profile.ts +101 -45
- package/scripts/agent/agent-safe-text.ts +60 -0
- package/scripts/agent/claude-agent-argv.ts +14 -0
- package/scripts/agent/codex-agent-argv.ts +9 -2
- package/scripts/agent/lane-instructions.ts +144 -0
- package/scripts/backlog/backlog-budget-cli.ts +1 -2
- package/scripts/backlog/backlog-budget.ts +4 -4
- package/scripts/backlog/backlog-drive-carried.ts +26 -0
- package/scripts/backlog/backlog-drive-cli.ts +10 -5
- package/scripts/backlog/backlog-drive-named.ts +3 -5
- package/scripts/backlog/backlog-drive-restore.ts +28 -2
- package/scripts/backlog/backlog-drive-seats.ts +27 -0
- package/scripts/backlog/backlog-drive.ts +11 -6
- package/scripts/backlog/backlog-occupied.ts +56 -0
- package/scripts/backlog/backlog-ready.ts +59 -15
- package/scripts/behavior/behavior-rules.ts +3 -2
- package/scripts/cost-runtime/codex-usage.ts +4 -2
- package/scripts/cost-runtime/cost-cli.ts +1 -2
- package/scripts/delegation/duplicate-read-guard.ts +2 -2
- package/scripts/delegation/investigation-guard.ts +15 -65
- package/scripts/delegation/investigation-reads.ts +1 -2
- package/scripts/discovery/repo-discovery.ts +4 -2
- package/scripts/discovery/repo-origin.ts +2 -4
- package/scripts/doctor/doctor-logic.ts +6 -3
- package/scripts/document/document-byte-budget.ts +28 -27
- package/scripts/document/document-byte-check.ts +1 -2
- package/scripts/document/document-section-cli.ts +17 -5
- package/scripts/document/entry-read-budget.ts +3 -3
- package/scripts/document/read-set-cli.ts +4 -1
- package/scripts/epic/epic-audit-orphans.ts +5 -8
- package/scripts/epic/epic-audit-rationale.ts +8 -2
- package/scripts/epic/epic-audit.ts +52 -1
- package/scripts/epic/epic-bundle-cli.ts +1 -2
- package/scripts/epic/epic-bundle-evidence.ts +49 -26
- package/scripts/epic/epic-bundle-referenced.ts +14 -3
- package/scripts/epic/epic-bundle.ts +17 -9
- package/scripts/epic/epic-chains.ts +6 -2
- package/scripts/epic/epic-close-comment.ts +1 -2
- package/scripts/followup/git-followup-workflow.ts +19 -1
- package/scripts/gate/check-inputs.ts +129 -0
- package/scripts/gate/check-reuse.ts +162 -0
- package/scripts/gate/core-budget.ts +93 -44
- package/scripts/gate/core-wait.ts +130 -0
- package/scripts/gate/free-cores.ts +49 -0
- package/scripts/gate/gate-ledger.ts +17 -3
- package/scripts/gate/gate-plan.ts +23 -1
- package/scripts/gate/gate-report.ts +15 -2
- package/scripts/gate/gate-run-steps.ts +150 -0
- package/scripts/gate/held-mark.ts +55 -0
- package/scripts/gate/machine-capacity.ts +70 -11
- package/scripts/gate/pre-commit-type-check.ts +14 -1
- package/scripts/gate/verification-gate.ts +65 -127
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +24 -2
- package/scripts/gh/git-pr.ts +19 -3
- package/scripts/git/git-common-directory.ts +3 -3
- package/scripts/git/git-diff-reads.ts +16 -9
- package/scripts/git/git-execa-error.ts +1 -2
- package/scripts/git/main-merge-regenerate.ts +78 -0
- package/scripts/git/main-merge.ts +14 -5
- package/scripts/hooks/batch-guard.ts +5 -8
- package/scripts/hooks/format-edited-file.ts +10 -4
- package/scripts/hooks/pre-commit-lint.ts +91 -0
- package/scripts/hooks/pre-push-unit.ts +15 -4
- package/scripts/hooks/pretool-guard.ts +48 -79
- package/scripts/hooks/reserved-run.ts +1 -0
- package/scripts/hooks/run-hook.sh +10 -12
- package/scripts/hooks/step-zero-notice.ts +3 -2
- package/scripts/hooks/stop-actions.ts +204 -0
- package/scripts/hooks/stop-guard.ts +62 -21
- package/scripts/init/hook-command-rewrite.ts +6 -2
- package/scripts/init/hook-launch.ts +12 -7
- package/scripts/init/init-logic-deploy-vps.ts +4 -2
- package/scripts/issue/behavior-change-lint.ts +12 -6
- package/scripts/issue/expected-effect-lint.ts +59 -0
- package/scripts/issue/issue-cite-cli.ts +35 -15
- package/scripts/issue/issue-classification.ts +1 -1
- package/scripts/issue/issue-lint-cli.ts +5 -3
- package/scripts/issue/issue-scout-cli.ts +22 -16
- package/scripts/issue/markdown-section.ts +19 -5
- package/scripts/josh/command-usage.ts +110 -0
- package/scripts/josh/file-map-stamp.ts +1 -3
- package/scripts/josh/hook-decision.ts +17 -2
- package/scripts/josh/josh-command-map.ts +2 -0
- package/scripts/josh/josh-command-types.ts +55 -9
- package/scripts/josh/josh-commands-ai.ts +13 -0
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-hooks.ts +6 -0
- package/scripts/josh/josh-commands-lane.ts +10 -1
- package/scripts/josh/josh-commands-workflow.ts +5 -0
- package/scripts/josh/josh-logic.ts +6 -15
- package/scripts/josh/josh-usage-automation.ts +121 -0
- package/scripts/josh/josh-usage-backlog.ts +180 -0
- package/scripts/josh/josh-usage-developer.ts +230 -0
- package/scripts/josh/josh-usage-hooks.ts +38 -0
- package/scripts/josh/josh-usage-issue.ts +205 -0
- package/scripts/josh/josh-usage-review.ts +101 -0
- package/scripts/josh/josh-usage-run.ts +252 -0
- package/scripts/josh/josh-usage-session.ts +238 -0
- package/scripts/josh/josh-usage-workflow.ts +212 -0
- package/scripts/josh/josh.ts +14 -5
- package/scripts/josh/package-directory.ts +20 -0
- package/scripts/lane/lane-await.ts +3 -2
- package/scripts/lane/lane-capacity.ts +8 -2
- package/scripts/lane/lane-check-stats.ts +56 -0
- package/scripts/lane/lane-child-invocation.ts +49 -5
- package/scripts/lane/lane-dispatch.ts +3 -3
- package/scripts/lane/lane-guard-policy.ts +5 -5
- package/scripts/lane/lane-handoff.ts +8 -1
- package/scripts/lane/lane-install.ts +12 -4
- package/scripts/lane/lane-ledger.ts +52 -1
- package/scripts/lane/lane-limit-cli.ts +8 -9
- package/scripts/lane/lane-load.ts +9 -2
- package/scripts/lane/lane-measure-cli.ts +4 -0
- package/scripts/lane/lane-parked.ts +18 -0
- package/scripts/lane/lane-preserve.ts +29 -1
- package/scripts/lane/lane-process-load.ts +125 -0
- package/scripts/lane/lane-process-stats.ts +93 -0
- package/scripts/lane/lane-registry.ts +17 -10
- package/scripts/lane/lane-restart.ts +5 -26
- package/scripts/lane/lane-session-action.ts +146 -0
- package/scripts/lane/lane-session-broker.ts +68 -125
- package/scripts/lane/lane-spent.ts +14 -3
- package/scripts/lane/lane-stats.ts +1 -0
- package/scripts/lane/lane-withdraw-cli.ts +71 -0
- package/scripts/lane/lane-withdraw.ts +182 -0
- package/scripts/lint/format.ts +4 -4
- package/scripts/lint/lint-parallel.ts +10 -3
- package/scripts/lint/lint-related-scope.ts +4 -1
- package/scripts/managed-marker/managed-marker-logic.ts +1 -2
- package/scripts/notify/telegram-test.ts +10 -5
- package/scripts/overrides/lockfile-overrides.ts +1 -3
- package/scripts/propagate/propagate.ts +1 -2
- package/scripts/release/release-progress.ts +3 -2
- package/scripts/report/report-format-reference.ts +2 -0
- package/scripts/report/report-lint.ts +7 -4
- package/scripts/review/acceptance-map-check.ts +61 -0
- package/scripts/review/acceptance-map.ts +205 -0
- package/scripts/review/review-brief-cli.ts +1 -1
- package/scripts/review/review-record-cli.ts +6 -2
- package/scripts/rules/agent-model.ts +59 -0
- package/scripts/rules/delivered-rules.ts +11 -2
- package/scripts/rules/destructive-command.ts +6 -9
- package/scripts/rules/direct-filing.ts +3 -7
- package/scripts/rules/early-heartbeat.ts +7 -18
- package/scripts/rules/file-body.ts +6 -13
- package/scripts/rules/filing-cap.ts +4 -6
- package/scripts/rules/firing-point.ts +6 -5
- package/scripts/rules/git-force.ts +5 -9
- package/scripts/rules/guard-remedy.ts +117 -0
- package/scripts/rules/implementation-cut.ts +37 -35
- package/scripts/rules/index-guard.ts +4 -7
- package/scripts/rules/issue-comments.ts +67 -16
- package/scripts/rules/josh-git-bare.ts +3 -6
- package/scripts/rules/lane-background.ts +10 -21
- package/scripts/rules/lane-carry-conflict.ts +7 -17
- package/scripts/rules/lane-interactive-ask.ts +6 -11
- package/scripts/rules/lane-park.ts +5 -13
- package/scripts/rules/lane-split-park.ts +2 -7
- package/scripts/rules/lane-switch-main.ts +9 -12
- package/scripts/rules/last-prompt.ts +50 -5
- package/scripts/rules/oracle-consulted.ts +4 -5
- package/scripts/rules/oracle-list-cli.ts +7 -0
- package/scripts/rules/piped-verification.ts +84 -32
- package/scripts/rules/poll-loop.ts +7 -17
- package/scripts/rules/pre-gate-cut.ts +7 -21
- package/scripts/rules/prior-comment-read.ts +7 -5
- package/scripts/rules/protected-files.ts +5 -9
- package/scripts/rules/raw-field-body.ts +3 -7
- package/scripts/rules/rule-body-guard.ts +4 -11
- package/scripts/rules/rule-list.ts +4 -1
- package/scripts/rules/rule-registry-rows.ts +10 -0
- package/scripts/rules/rule-registry.ts +7 -4
- package/scripts/rules/rule-rewrite.ts +114 -0
- package/scripts/rules/rule-value.ts +7 -1
- package/scripts/rules/run-tail-rule.ts +6 -22
- package/scripts/rules/shell-body-trigger.ts +4 -6
- package/scripts/rules/shell-segments.ts +22 -0
- package/scripts/rules/stop-decision.ts +77 -0
- package/scripts/rules/stop-rules.ts +164 -77
- package/scripts/rules/tail-commands.ts +15 -1
- package/scripts/rules/test-declared-commit.ts +4 -10
- package/scripts/rules/third-party-write.ts +5 -10
- package/scripts/rules/worktree-guard.ts +5 -12
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-closed.ts +8 -2
- package/scripts/run/board/run-board-github.ts +19 -3
- package/scripts/run/board/run-board-header.ts +4 -1
- package/scripts/run/board/run-board-idle.ts +31 -0
- package/scripts/run/board/run-board-labels.ts +2 -0
- package/scripts/run/board/run-board-layout.ts +1 -1
- package/scripts/run/board/run-board-read.ts +25 -12
- package/scripts/run/board/run-board-reason.ts +17 -4
- package/scripts/run/board/run-board-render-legend.ts +1 -0
- package/scripts/run/board/run-board-render-slot.ts +1 -1
- package/scripts/run/board/run-board-render-waits.ts +41 -0
- package/scripts/run/board/run-board-render.ts +6 -1
- package/scripts/run/board/run-board-row-yield.ts +1 -0
- package/scripts/run/board/run-board-state.ts +9 -0
- package/scripts/run/board/run-board-status.ts +89 -23
- package/scripts/run/board/run-board-thanks.ts +7 -2
- package/scripts/run/board/run-board-tick.ts +37 -8
- package/scripts/run/carry/run-carry-args.ts +1 -3
- package/scripts/run/carry/run-carry-conversation.ts +7 -2
- package/scripts/run/cut/run-cut-cli.ts +18 -22
- package/scripts/run/cut/run-cut-decline.ts +72 -0
- package/scripts/run/cut/run-cut-ready.ts +74 -0
- package/scripts/run/cut/run-cut-report.ts +29 -0
- package/scripts/run/cut/run-cut.ts +10 -1
- package/scripts/run/detached-launch.ts +23 -4
- package/scripts/run/entry/run-entry-cli.ts +8 -1
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-scope.ts +19 -1
- package/scripts/run/event/run-event-stream-emit.ts +13 -6
- package/scripts/run/event/run-event-stream.ts +4 -0
- package/scripts/run/hold/run-hold-cli.ts +14 -3
- package/scripts/run/hold/run-hold-session.ts +46 -0
- package/scripts/run/hold/run-hold.ts +13 -4
- package/scripts/run/launch-priority.ts +32 -0
- package/scripts/run/merge/run-merge-cli.ts +16 -42
- package/scripts/run/merge/run-merge-ship.ts +59 -1
- package/scripts/run/merge/run-merge-signals.ts +99 -0
- package/scripts/run/merge/run-merge-steps.ts +11 -1
- package/scripts/run/merge/run-merge.ts +22 -9
- package/scripts/run/run-ending.ts +31 -3
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-parent-cut-hook.ts +5 -10
- package/scripts/run/run-poll-hook.ts +157 -0
- package/scripts/run/run-preflight.ts +1 -2
- package/scripts/run/run-step-cli.ts +22 -15
- package/scripts/run/run-step-section.ts +85 -0
- package/scripts/run/run-stop-notify.ts +1 -2
- package/scripts/run/run-watcher-guard.ts +6 -3
- package/scripts/run/ship/run-ship-cli.ts +5 -12
- package/scripts/run/ship/run-ship-detach.ts +9 -0
- package/scripts/run/ship/run-ship-next.ts +10 -2
- package/scripts/run/ship/run-ship-overlap.ts +195 -0
- package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
- package/scripts/run/ship/run-ship-preflight.ts +28 -2
- package/scripts/run/ship/run-ship-return.ts +31 -7
- package/scripts/run/ship/run-ship-review-steps.ts +25 -14
- package/scripts/run/ship/run-ship-review.ts +15 -2
- package/scripts/run/ship/run-ship-steps.ts +1 -1
- package/scripts/run/ship/run-ship-stop-context.ts +206 -0
- package/scripts/run/ship/run-ship-sync.ts +14 -6
- package/scripts/run/tidy/run-tidy-cli.ts +3 -3
- package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
- package/scripts/safe-chain/project-config.ts +7 -2
- package/scripts/sync/sync-hook-safety.ts +8 -2
- package/scripts/test/test-e2e-guard.ts +30 -5
- package/scripts/test/test-unit-guard.ts +16 -1
- package/scripts/test/unit-worker-share.ts +26 -3
- package/scripts/time-runtime/time-batch-guard.ts +3 -3
- package/scripts/time-runtime/time-hook-transcript.ts +3 -2
- package/scripts/time-runtime/time-shell.ts +39 -4
- package/scripts/time-runtime/time-spans.ts +1 -0
- package/scripts/time-runtime/time-tool-call.ts +7 -0
- package/scripts/version/effective-upstream.ts +3 -4
- package/scripts/version/kit-version-config.ts +8 -2
- package/scripts/version/latest-corepack.ts +1 -2
- package/scripts/version/release-hold.ts +11 -4
- package/scripts/version/version-commands.ts +12 -4
- package/templates/gitignore +6 -2
- package/dist/hooks/chunk-D5QBTOCZ.js +0 -68
- package/dist/hooks/chunk-UT7YC3N2.js +0 -56
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
-
import { availableParallelism } from 'node:os'
|
|
3
2
|
import { fileURLToPath } from 'node:url'
|
|
4
3
|
import type { FileMapStamp } from '#scripts/josh/file-map-stamp'
|
|
5
4
|
import { GATE_COMMAND } from '#scripts/josh/josh-command-types'
|
|
6
5
|
import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-composite-arguments'
|
|
7
6
|
import { lane_ledger } from '#scripts/lane/lane-ledger'
|
|
8
|
-
import {
|
|
9
|
-
import { buffered_process, FAIL_EXIT_CODE } from '#scripts/lib/buffered-process'
|
|
7
|
+
import { FAIL_EXIT_CODE } from '#scripts/lib/buffered-process'
|
|
10
8
|
import { review_stamps } from '#scripts/review/review-stamps'
|
|
11
9
|
import { review_tree } from '#scripts/review/review-tree'
|
|
12
10
|
import { unit_worker_share } from '#scripts/test/unit-worker-share'
|
|
13
|
-
import {
|
|
11
|
+
import { check_reuse, type CheckRecords } from './check-reuse'
|
|
12
|
+
import { free_cores } from './free-cores'
|
|
14
13
|
import { gate_ledger, type GateLedgerStart } from './gate-ledger'
|
|
15
14
|
import { gate_plan, type GatePlan } from './gate-plan'
|
|
16
|
-
import { gate_report, type
|
|
15
|
+
import { gate_report, type GateStepResult } from './gate-report'
|
|
16
|
+
import { gate_run_steps, type ReusedChecks } from './gate-run-steps'
|
|
17
17
|
import { gate_skip } from './gate-skip'
|
|
18
18
|
import { build_gate_step, build_gate_steps, UNIT_WORKER_FLAG } from './gate-steps'
|
|
19
19
|
import { gate_tree, type GateTree } from './gate-tree'
|
|
@@ -35,25 +35,6 @@ const { GATE_CHECKS } = gate_plan
|
|
|
35
35
|
|
|
36
36
|
const GATE_TARGETS: ReadonlyArray<string> = GATE_CHECKS.map((check) => check.target)
|
|
37
37
|
|
|
38
|
-
async function run_gate_step(step: GateStep): Promise<GateStepResult> {
|
|
39
|
-
if (step.skip_reason !== undefined) {
|
|
40
|
-
return {
|
|
41
|
-
label: step.label,
|
|
42
|
-
command: step.command_args.join(' '),
|
|
43
|
-
output: project_checks.skip_notice('check', step.skip_reason),
|
|
44
|
-
exit_code: 0,
|
|
45
|
-
elapsed_ms: 0,
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const result = await buffered_process.run_buffered_process(
|
|
50
|
-
step.command_args,
|
|
51
|
-
step.cwd === undefined ? {} : { cwd: step.cwd },
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
return { label: step.label, command: step.command_args.join(' '), ...result }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
38
|
// The record `josh review:brief` reads, written only on a fully green run and only with the tree it
|
|
58
39
|
// was green on. `/code-review` runs in a forked process that reads none of
|
|
59
40
|
// this repository's documents, so "the unit tests already passed" reaches it only if the invocation
|
|
@@ -65,7 +46,7 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
|
|
|
65
46
|
// visible on the console, and a record erasing it would have the brief tell a review agent not to
|
|
66
47
|
// re-run tests that never ran. A skip of a tool the project does not use (`NOT_USED_MARKER` — eslint
|
|
67
48
|
// with no ESLint configuration) is not one: nothing went unverified, so it withholds nothing
|
|
68
|
-
// (`gate_report.
|
|
49
|
+
// (`gate_report.has_nothing_to_say`). A step that passed **with a checker's warnings** is withheld for the
|
|
69
50
|
// same reason one place further on: since the record is reused instead of the checks being re-run, a
|
|
70
51
|
// warning printed once would never be printed again on that tree, and hiding an eslint or
|
|
71
52
|
// svelte-check finding is the same failure as hiding a skip. It is `has_checker_warning`, not
|
|
@@ -81,17 +62,13 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
|
|
|
81
62
|
//
|
|
82
63
|
// The destination is a parameter so a test can exercise the record without overwriting the one a
|
|
83
64
|
// real run may be relying on — `josh gate` and `josh review:brief` share one path by design.
|
|
84
|
-
function has_nothing_to_say(result: GateStepResult): boolean {
|
|
85
|
-
return !gate_report.is_unverified_skip(result) && !gate_report.has_checker_warning(result)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
65
|
async function record_green_gate(
|
|
89
66
|
results: ReadonlyArray<GateStepResult>,
|
|
90
67
|
before: Record<string, string>,
|
|
91
68
|
target?: string,
|
|
92
69
|
base?: string,
|
|
93
70
|
): Promise<void> {
|
|
94
|
-
if (results.some((result) => !has_nothing_to_say(result))) return
|
|
71
|
+
if (results.some((result) => !gate_report.has_nothing_to_say(result))) return
|
|
95
72
|
|
|
96
73
|
try {
|
|
97
74
|
const after = await review_tree.read_changed_tree()
|
|
@@ -153,94 +130,6 @@ async function with_gate_marker<T>(
|
|
|
153
130
|
}
|
|
154
131
|
}
|
|
155
132
|
|
|
156
|
-
// This gate's share of the machine, resolved once at the gate's entry and carried down as one value.
|
|
157
|
-
// `available_cores` is the budget every check reserves against; `is_reserved`
|
|
158
|
-
// is false for a gate nested inside another gate's unit suite — this repository's own gate tests — so
|
|
159
|
-
// the outer gate reserves and every gate beneath it does not. Captured before this gate sets
|
|
160
|
-
// `JOSH_UNIT_RUN_MARKED`, which is why it is read at entry rather than re-derived here.
|
|
161
|
-
interface BudgetContext {
|
|
162
|
-
available_cores: number
|
|
163
|
-
is_reserved: boolean
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function default_budget(): BudgetContext {
|
|
167
|
-
return {
|
|
168
|
-
available_cores: availableParallelism(),
|
|
169
|
-
is_reserved: !unit_worker_share.is_nested_run() && !core_budget.is_held(),
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// A gate step paired with the cores it reserves from the machine-wide budget while it runs.
|
|
174
|
-
// The weight travels with the step so `run_gate_steps` never has to re-pair a
|
|
175
|
-
// step with its check by index.
|
|
176
|
-
interface ReservedStep {
|
|
177
|
-
step: GateStep
|
|
178
|
-
weight: number
|
|
179
|
-
memory_mb: number
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function build_reserved_steps(
|
|
183
|
-
start_directory: string,
|
|
184
|
-
plan: GatePlan,
|
|
185
|
-
available_cores: number,
|
|
186
|
-
): Promise<ReadonlyArray<ReservedStep>> {
|
|
187
|
-
return await Promise.all(
|
|
188
|
-
plan.checks.map(async (check) => ({
|
|
189
|
-
step: await build_gate_step(check, start_directory, plan),
|
|
190
|
-
weight: gate_plan.check_weight(check, plan, available_cores),
|
|
191
|
-
memory_mb: gate_plan.check_memory(check, plan, available_cores),
|
|
192
|
-
})),
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// **A nested gate takes no reservation, and the budget is what makes that necessary rather than
|
|
197
|
-
// merely tidy**. The unit step spawns the suite, which runs this repository's
|
|
198
|
-
// own gate tests; each of those is a gate inside the outer gate's unit reservation, and a place it
|
|
199
|
-
// claimed would wait for cores the outer gate cannot free until the suite — this test included —
|
|
200
|
-
// finishes.
|
|
201
|
-
async function run_reserved_step(
|
|
202
|
-
reserved: ReservedStep,
|
|
203
|
-
budget: BudgetContext,
|
|
204
|
-
): Promise<GateStepResult> {
|
|
205
|
-
if (!budget.is_reserved) return await run_gate_step(reserved.step)
|
|
206
|
-
|
|
207
|
-
return await core_budget.with_core_reservation(
|
|
208
|
-
reserved.weight,
|
|
209
|
-
async (admission) => ({
|
|
210
|
-
...(await run_gate_step(reserved.step)),
|
|
211
|
-
budget_note: core_budget.describe_overflow(admission),
|
|
212
|
-
}),
|
|
213
|
-
{ budget: budget.available_cores, memory_mb: reserved.memory_mb },
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// `bounded_pool` rather than a bare `Promise.all`, so the plan's `concurrency` is what decides how
|
|
218
|
-
// many run at once. **The all-failures-in-one-pass property survives the
|
|
219
|
-
// change** because no check ever rejects: `buffered_process` reports a non-zero exit as a value, so
|
|
220
|
-
// the pool's first-failure abort — written for callers that spawn real Claude sessions — never
|
|
221
|
-
// fires here and every queued check still runs. Results come back in input order however they
|
|
222
|
-
// finished, which is what keeps the printed sections in declaration order. Each step first claims its
|
|
223
|
-
// place in the machine-wide budget and waits while the budget is full.
|
|
224
|
-
async function run_gate_steps(
|
|
225
|
-
plan: GatePlan,
|
|
226
|
-
budget: BudgetContext = default_budget(),
|
|
227
|
-
): Promise<ReadonlyArray<GateStepResult>> {
|
|
228
|
-
const reserved = await build_reserved_steps(process.cwd(), plan, budget.available_cores)
|
|
229
|
-
|
|
230
|
-
// **The checks run under the held mark, so a check's own dispatch claims nothing**.
|
|
231
|
-
// Each check is a `pnpm josh <target>` child, and a target that declares
|
|
232
|
-
// a weight would otherwise claim a second place for the cores reserved for it here. A nested gate
|
|
233
|
-
// sets the mark too, since its checks are covered by the outer gate's reservation.
|
|
234
|
-
return await core_budget.with_held_mark(
|
|
235
|
-
async () =>
|
|
236
|
-
await bounded_pool.bounded_map(
|
|
237
|
-
reserved,
|
|
238
|
-
plan.concurrency,
|
|
239
|
-
async (entry) => await run_reserved_step(entry, budget),
|
|
240
|
-
),
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
133
|
// **Both markers are claims about the unit suite, so a gate that is not running it makes neither**.
|
|
245
134
|
// The in-flight marker tells the next `josh review:brief` that a gate is
|
|
246
135
|
// covering this tree right now, which is what stops a review agent re-running the suite; the
|
|
@@ -276,15 +165,21 @@ async function run_marked_gate<T>(
|
|
|
276
165
|
// The concurrent-run count is read here rather than inside `gate-plan.ts` for the reason the core
|
|
277
166
|
// count already is: that module stays a pure function of its inputs, and the machine is asked once and
|
|
278
167
|
// handed to both calls. Counted *before* the unit step writes its own marker, so `+ 1` is this gate.
|
|
279
|
-
|
|
168
|
+
// `free` bounds the unit cap the run count produced (`gate_plan.bound_by_free`).
|
|
169
|
+
function announce_gate_plan(
|
|
170
|
+
is_unit_included: boolean,
|
|
171
|
+
available_cores: number,
|
|
172
|
+
free: number | undefined,
|
|
173
|
+
): GatePlan {
|
|
280
174
|
const concurrent_runs = unit_worker_share.live_run_count() + unit_worker_share.SOLO_RUNS
|
|
281
175
|
const is_kit_repository = project_checks.is_kit_repository(process.cwd())
|
|
282
|
-
const
|
|
176
|
+
const resolved = gate_plan.resolve_gate_plan(
|
|
283
177
|
available_cores,
|
|
284
178
|
concurrent_runs,
|
|
285
179
|
is_unit_included,
|
|
286
180
|
is_kit_repository,
|
|
287
181
|
)
|
|
182
|
+
const plan = gate_plan.bound_by_free(resolved, available_cores, free)
|
|
288
183
|
|
|
289
184
|
process.stdout.write(`${gate_plan.format_gate_plan(plan, available_cores, concurrent_runs)}\n`)
|
|
290
185
|
|
|
@@ -319,6 +214,35 @@ interface GateOptions {
|
|
|
319
214
|
// The lane ledger a finished gate's duration is appended to. Only the CLI
|
|
320
215
|
// entry resolves it, so a suite driving the gate never writes a measurement of checks that never ran.
|
|
321
216
|
ledger_path?: string | undefined
|
|
217
|
+
// Where each check's own green record lives (`check-reuse.ts`). Only the CLI entry resolves it, for
|
|
218
|
+
// the ledger's reason: a suite whose checks never ran must not record them green on the live tree.
|
|
219
|
+
check_records?: CheckRecords | undefined
|
|
220
|
+
// The cores free of load from outside the core budget's ledger (`free-cores.ts`). Only the CLI entry
|
|
221
|
+
// resolves it, for the ledger's reason: a suite driving the gate never reads the machine, so its plan
|
|
222
|
+
// does not depend on what else happened to be running.
|
|
223
|
+
read_free_cores?: ((available_cores: number) => Promise<number | undefined>) | undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// **Per-check records belong to the full local gate alone**: CI's `--no-unit` gate is the safety net
|
|
227
|
+
// under the local reuse and stays as it was, so it neither reads nor writes one.
|
|
228
|
+
function check_records_of(plan: GatePlan, options: GateOptions): CheckRecords | undefined {
|
|
229
|
+
return gate_plan.has_unit_check(plan.checks) ? options.check_records : undefined
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function checks_to_reuse(
|
|
233
|
+
plan: GatePlan,
|
|
234
|
+
tree: GateTree,
|
|
235
|
+
options: GateOptions,
|
|
236
|
+
): Promise<ReusedChecks> {
|
|
237
|
+
const records = check_records_of(plan, options)
|
|
238
|
+
|
|
239
|
+
if (records === undefined || options.is_forced === true) return new Map()
|
|
240
|
+
|
|
241
|
+
return await check_reuse.reusable_checks(
|
|
242
|
+
plan.checks.map((check) => check.label),
|
|
243
|
+
tree,
|
|
244
|
+
records,
|
|
245
|
+
)
|
|
322
246
|
}
|
|
323
247
|
|
|
324
248
|
// **The gate refuses to start when the scoped pair has not been green on this tree**
|
|
@@ -328,8 +252,13 @@ interface GateOptions {
|
|
|
328
252
|
// `--force` is the caller saying to run regardless. `scoped_green` owns the record and the `JOSH_SCOPED_GREEN`
|
|
329
253
|
// escape hatch, so a person can always get past a record that is wrong about their tree.
|
|
330
254
|
function scoped_precheck_refusal(tree: GateTree, options: GateOptions): string | undefined {
|
|
331
|
-
if (
|
|
332
|
-
|
|
255
|
+
if (
|
|
256
|
+
options.is_scoped_enforced !== true ||
|
|
257
|
+
options.is_forced === true ||
|
|
258
|
+
options.is_unit_included === false
|
|
259
|
+
) {
|
|
260
|
+
return undefined
|
|
261
|
+
}
|
|
333
262
|
|
|
334
263
|
return scoped_green.refusal_for(tree.files, tree.base, options.scoped_sources)
|
|
335
264
|
}
|
|
@@ -369,6 +298,10 @@ async function settle_gate(
|
|
|
369
298
|
|
|
370
299
|
await gate_ledger.record(options.ledger, results, { elapsed_ms, is_passed })
|
|
371
300
|
|
|
301
|
+
const records = check_records_of(plan, options)
|
|
302
|
+
|
|
303
|
+
if (records !== undefined) await check_reuse.record_green_checks(results, tree, records)
|
|
304
|
+
|
|
372
305
|
if (!is_passed) return FAIL_EXIT_CODE
|
|
373
306
|
|
|
374
307
|
await record_whole_gate(plan, results, tree, options)
|
|
@@ -391,8 +324,11 @@ async function run_checked_gate(
|
|
|
391
324
|
const ledger = await gate_ledger.start(options.ledger_path)
|
|
392
325
|
// Resolved at the gate's entry, before this gate sets `JOSH_UNIT_RUN_MARKED`, so a gate nested in the
|
|
393
326
|
// unit suite reads the flag as set and reserves nothing.
|
|
394
|
-
const budget = default_budget()
|
|
395
|
-
|
|
327
|
+
const budget = gate_run_steps.default_budget()
|
|
328
|
+
// Read before the count for the ledger's reason: the reading waits out a sample window.
|
|
329
|
+
const free = await options.read_free_cores?.(budget.available_cores)
|
|
330
|
+
const plan = announce_gate_plan(is_unit_included, budget.available_cores, free)
|
|
331
|
+
const reused = await checks_to_reuse(plan, tree, options)
|
|
396
332
|
|
|
397
333
|
// **The gate holds the unit-run marker for its whole run, not just its unit step.** The step that
|
|
398
334
|
// would write it is a subprocess started a second or so after the count above, so two lanes launched
|
|
@@ -401,7 +337,7 @@ async function run_checked_gate(
|
|
|
401
337
|
// the next lane asks; the guard inside the spawned `josh test:unit` sees the handoff and adds no
|
|
402
338
|
// second marker for the same run.
|
|
403
339
|
async function run_and_settle(): Promise<number> {
|
|
404
|
-
const results = await run_gate_steps(plan, budget)
|
|
340
|
+
const results = await gate_run_steps.run_gate_steps(plan, budget, reused)
|
|
405
341
|
|
|
406
342
|
return await settle_gate(plan, results, tree, { ...options, started_at, ledger })
|
|
407
343
|
}
|
|
@@ -501,6 +437,8 @@ async function run_gate_command(
|
|
|
501
437
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
502
438
|
process.exitCode = await run_gate_command(process.argv.slice(FIRST_ARGUMENT_INDEX), {
|
|
503
439
|
ledger_path: await lane_ledger.target(),
|
|
440
|
+
check_records: check_reuse.DEFAULT_RECORDS,
|
|
441
|
+
read_free_cores: async (available_cores) => await free_cores.read({ available_cores }),
|
|
504
442
|
})
|
|
505
443
|
}
|
|
506
444
|
|
|
@@ -514,11 +452,11 @@ const verification_gate = {
|
|
|
514
452
|
clear_gate_running,
|
|
515
453
|
mark_gate_running,
|
|
516
454
|
record_green_gate,
|
|
517
|
-
run_gate_steps,
|
|
455
|
+
run_gate_steps: gate_run_steps.run_gate_steps,
|
|
518
456
|
scoped_precheck_refusal,
|
|
519
457
|
with_gate_marker,
|
|
520
458
|
run_gate_command,
|
|
521
|
-
run_gate_step,
|
|
459
|
+
run_gate_step: gate_run_steps.run_gate_step,
|
|
522
460
|
run_verification_gate,
|
|
523
461
|
}
|
|
524
462
|
|
|
@@ -36,6 +36,7 @@ const LABEL_NAMES_FILTER = '.[].name'
|
|
|
36
36
|
const LABELS_SEGMENT = '/labels'
|
|
37
37
|
const COMMENTS_SEGMENT = '/comments'
|
|
38
38
|
const CLOSED_STATE = 'closed'
|
|
39
|
+
const NOT_PLANNED_REASON = 'not_planned'
|
|
39
40
|
const COLOR_HASH = '#'
|
|
40
41
|
const UNPROCESSABLE_STATUS = 422
|
|
41
42
|
const ALREADY_EXISTS_CODE = 'already_exists'
|
|
@@ -99,17 +100,37 @@ async function issue_try_comment(issue_number: string, body: string): Promise<bo
|
|
|
99
100
|
// It is a required parameter that may be `undefined` rather than an optional one: skipping the
|
|
100
101
|
// announcement is a decision the caller makes from what it read, never something a call site can
|
|
101
102
|
// fall into by leaving an argument off.
|
|
102
|
-
async function
|
|
103
|
+
async function close_with(
|
|
104
|
+
issue_number: string,
|
|
105
|
+
comment: string | undefined,
|
|
106
|
+
state: Readonly<Record<string, string>>,
|
|
107
|
+
): Promise<boolean> {
|
|
103
108
|
return await did_write_succeed(async () => {
|
|
104
109
|
if (comment !== undefined) await issue_comment(issue_number, comment)
|
|
105
110
|
await git_gh_exec.exec_gh_api({
|
|
106
111
|
path: git_gh_api_path.issue_api_path(issue_number),
|
|
107
112
|
method: PATCH_METHOD,
|
|
108
|
-
body: JSON.stringify(
|
|
113
|
+
body: JSON.stringify(state),
|
|
109
114
|
})
|
|
110
115
|
})
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
async function issue_close(issue_number: string, comment: string | undefined): Promise<boolean> {
|
|
119
|
+
return await close_with(issue_number, comment, { state: CLOSED_STATE })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The same close as "not planned" — the reason `issue-merged.ts` reads as no merge, so a closed issue
|
|
123
|
+
// that was withdrawn is never taken for one whose work landed.
|
|
124
|
+
async function issue_close_not_planned(
|
|
125
|
+
issue_number: string,
|
|
126
|
+
comment: string | undefined,
|
|
127
|
+
): Promise<boolean> {
|
|
128
|
+
return await close_with(issue_number, comment, {
|
|
129
|
+
state: CLOSED_STATE,
|
|
130
|
+
state_reason: NOT_PLANNED_REASON,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
113
134
|
// REST answers 422 `{"resource":"Label","code":"invalid","field":"color"}` for a color carrying a
|
|
114
135
|
// leading `#`. The one caller passes the hash form (`EPIC_LABEL_COLOR`), so dropping it here is what
|
|
115
136
|
// keeps that call site unchanged.
|
|
@@ -336,6 +357,7 @@ const git_gh_issue_write = {
|
|
|
336
357
|
issue_comment,
|
|
337
358
|
issue_try_comment,
|
|
338
359
|
issue_close,
|
|
360
|
+
issue_close_not_planned,
|
|
339
361
|
label_ensure,
|
|
340
362
|
issue_create_request,
|
|
341
363
|
issue_create_with_label,
|
package/scripts/gh/git-pr.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { issue_cite } from '#scripts/issue/issue-cite'
|
|
|
5
5
|
import { session_cite } from '#scripts/issue/session-cite'
|
|
6
6
|
import { animation_helpers, type AnimationOptions } from '#scripts/lib/animation-helpers'
|
|
7
7
|
import { json_value } from '#scripts/lib/json-value'
|
|
8
|
+
import { acceptance_map } from '#scripts/review/acceptance-map'
|
|
8
9
|
import { git_gh_command } from './git-gh-command'
|
|
9
10
|
import { git_pr_error } from './git-pr-error'
|
|
10
11
|
import { git_pr_messages } from './git-pr-messages'
|
|
@@ -172,9 +173,24 @@ function build_body(issue_info: IssueInfo, extra_body?: string): string {
|
|
|
172
173
|
return `${closes}\n\n${extra_body}`
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
// A body without the acceptance-criteria table gets one pending row per criterion, so the author fills
|
|
177
|
+
// in rows that already carry their identifiers — `josh followup` refuses the merge until they do.
|
|
178
|
+
async function body_with_map(issue_info: IssueInfo, extra_body?: string): Promise<string> {
|
|
179
|
+
const body = build_body(issue_info, extra_body)
|
|
180
|
+
|
|
181
|
+
if (acceptance_map.has_map(body)) return body
|
|
182
|
+
const map = acceptance_map.skeleton(await git_gh_command.issue_get_body(issue_info.number))
|
|
183
|
+
|
|
184
|
+
return map === undefined ? body : `${body}\n\n${map}`
|
|
185
|
+
}
|
|
186
|
+
|
|
175
187
|
async function existing_pr_label(branch_name: string): Promise<ReleaseClassification | undefined> {
|
|
176
|
-
if (
|
|
177
|
-
|
|
188
|
+
if (
|
|
189
|
+
!(await git_gh_command.pr_exists(branch_name)) ||
|
|
190
|
+
is_pr_state_merged(await get_pr_state(branch_name))
|
|
191
|
+
) {
|
|
192
|
+
return undefined
|
|
193
|
+
}
|
|
178
194
|
|
|
179
195
|
return await git_gh_command.pr_get_classification(branch_name)
|
|
180
196
|
}
|
|
@@ -186,7 +202,7 @@ async function create_for_issue(
|
|
|
186
202
|
): Promise<void> {
|
|
187
203
|
await create(
|
|
188
204
|
build_title(issue_info),
|
|
189
|
-
|
|
205
|
+
await body_with_map(issue_info, extra_body),
|
|
190
206
|
issue_info.branch_name,
|
|
191
207
|
{
|
|
192
208
|
label,
|
|
@@ -88,9 +88,9 @@ function select_linked(found: ReadonlyArray<string>): string | undefined {
|
|
|
88
88
|
const own_directory = found[OWN_GIT_DIRECTORY]
|
|
89
89
|
const common = found[COMMON_GIT_DIRECTORY]
|
|
90
90
|
|
|
91
|
-
return own_directory
|
|
92
|
-
?
|
|
93
|
-
:
|
|
91
|
+
return own_directory === undefined || common === undefined || own_directory === common
|
|
92
|
+
? undefined
|
|
93
|
+
: common
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function resolve(cwd: string): string | undefined {
|
|
@@ -9,19 +9,24 @@ const REFS_REMOTES_ORIGIN_PREFIX = 'refs/remotes/origin/'
|
|
|
9
9
|
const NAME_ONLY_FLAG = '--name-only'
|
|
10
10
|
const DEFAULT_BRANCH_FALLBACK = 'main'
|
|
11
11
|
|
|
12
|
+
const DEFAULT_BRANCH_ARGUMENTS = ['symbolic-ref', 'refs/remotes/origin/HEAD'] as const
|
|
13
|
+
|
|
14
|
+
// The default branch `DEFAULT_BRANCH_ARGUMENTS` names, or the fallback for a missing or unexpected
|
|
15
|
+
// answer — shared with the synchronous read the implementation-cut guard takes it with.
|
|
16
|
+
function default_branch_of(output: string | undefined): string {
|
|
17
|
+
const trimmed = output?.trim() ?? ''
|
|
18
|
+
|
|
19
|
+
if (!trimmed.startsWith(REFS_REMOTES_ORIGIN_PREFIX)) return DEFAULT_BRANCH_FALLBACK
|
|
20
|
+
|
|
21
|
+
return trimmed.slice(REFS_REMOTES_ORIGIN_PREFIX.length)
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
async function get_default_branch(): Promise<string> {
|
|
13
25
|
try {
|
|
14
|
-
|
|
15
|
-
const trimmed = output.trim()
|
|
16
|
-
|
|
17
|
-
if (trimmed.startsWith(REFS_REMOTES_ORIGIN_PREFIX)) {
|
|
18
|
-
return trimmed.slice(REFS_REMOTES_ORIGIN_PREFIX.length)
|
|
19
|
-
}
|
|
26
|
+
return default_branch_of(await git_spawn.read([...DEFAULT_BRANCH_ARGUMENTS]))
|
|
20
27
|
} catch {
|
|
21
|
-
|
|
28
|
+
return DEFAULT_BRANCH_FALLBACK
|
|
22
29
|
}
|
|
23
|
-
|
|
24
|
-
return DEFAULT_BRANCH_FALLBACK
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
const MERGE_BASE_COMMAND = 'merge-base'
|
|
@@ -210,8 +215,10 @@ async function untracked_names(): Promise<string> {
|
|
|
210
215
|
}
|
|
211
216
|
|
|
212
217
|
const git_diff_reads = {
|
|
218
|
+
DEFAULT_BRANCH_ARGUMENTS,
|
|
213
219
|
change_base,
|
|
214
220
|
change_base_commit,
|
|
221
|
+
default_branch_of,
|
|
215
222
|
default_branch_reference,
|
|
216
223
|
diff_cached_names,
|
|
217
224
|
diff_main,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// Extract the exit code from a thrown execa error. Returns the numeric exit code,
|
|
2
2
|
// or undefined when the process was terminated by a signal or failed to spawn.
|
|
3
3
|
function get_exit_code(error: unknown): number | undefined {
|
|
4
|
-
if (typeof error !== 'object' || error === null) return undefined
|
|
5
|
-
if (!('exitCode' in error)) return undefined
|
|
4
|
+
if (typeof error !== 'object' || error === null || !('exitCode' in error)) return undefined
|
|
6
5
|
|
|
7
6
|
const { exitCode: exit_code } = error
|
|
8
7
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { SUITE_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
import { git_command } from './git-command'
|
|
4
|
+
import { git_spawn } from './git-spawn'
|
|
5
|
+
|
|
6
|
+
// A merge of the default branch that stops only on generated files is finished here rather than by a
|
|
7
|
+
// resumed session: each such file is the output of a command, so running that command on the merged
|
|
8
|
+
// tree writes the one correct resolution, with no conflict marker for anyone to read.
|
|
9
|
+
//
|
|
10
|
+
// **This table is the single list of what is resolved that way** — a file a person writes never
|
|
11
|
+
// belongs in it, because regenerating would discard that person's side of the merge. A conflict on any
|
|
12
|
+
// path not listed here still stops as `conflict`. A file belongs here only once its committed content
|
|
13
|
+
// is exactly what its command writes: `scripts/test/pure-files.ts` is not, so regenerating it would
|
|
14
|
+
// rewrite far more than the conflict.
|
|
15
|
+
|
|
16
|
+
interface RegeneratedFile {
|
|
17
|
+
path: string
|
|
18
|
+
command: ReadonlyArray<string>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const REGENERATED_FILES: ReadonlyArray<RegeneratedFile> = [
|
|
22
|
+
{
|
|
23
|
+
path: 'docs/josh-command-catalog.md',
|
|
24
|
+
command: ['pnpm', 'exec', 'tsx', 'scripts/document/generate-catalog.ts'],
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
function regenerated_for(
|
|
29
|
+
files: ReadonlyArray<string>,
|
|
30
|
+
table: ReadonlyArray<RegeneratedFile>,
|
|
31
|
+
): ReadonlyArray<RegeneratedFile> {
|
|
32
|
+
const entries = files.map((file) => table.find((entry) => entry.path === file))
|
|
33
|
+
|
|
34
|
+
return entries.every((entry) => entry !== undefined) ? entries : []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Each generator writes only its own file, so they run side by side.
|
|
38
|
+
async function regenerate(entries: ReadonlyArray<RegeneratedFile>, cwd: string): Promise<void> {
|
|
39
|
+
await Promise.all(
|
|
40
|
+
entries.map(async (entry) => {
|
|
41
|
+
const [binary = '', ...command_arguments] = entry.command
|
|
42
|
+
|
|
43
|
+
await execa(binary, command_arguments, { cwd })
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// `--no-edit` keeps the message the merge was started with — the one carrying the issue number the
|
|
49
|
+
// `commit-msg` hook asks for.
|
|
50
|
+
async function conclude_merge(entries: ReadonlyArray<RegeneratedFile>): Promise<void> {
|
|
51
|
+
await git_spawn.with_output('add', ['--', ...entries.map((entry) => entry.path)])
|
|
52
|
+
await git_spawn.with_output('commit', ['--no-edit'], { timeout_ms: SUITE_TIMEOUT_MS })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Whether the merge that stopped on `files` was finished. A command that fails leaves the merge in
|
|
56
|
+
// progress and answers `false`, so the stop is the `conflict` it would have been without this table.
|
|
57
|
+
async function resolve(
|
|
58
|
+
files: ReadonlyArray<string>,
|
|
59
|
+
table: ReadonlyArray<RegeneratedFile> = REGENERATED_FILES,
|
|
60
|
+
): Promise<boolean> {
|
|
61
|
+
const entries = regenerated_for(files, table)
|
|
62
|
+
|
|
63
|
+
if (entries.length === 0) return false
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await regenerate(entries, await git_command.repository_root())
|
|
67
|
+
await conclude_merge(entries)
|
|
68
|
+
} catch {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const main_merge_regenerate = { REGENERATED_FILES, resolve }
|
|
76
|
+
|
|
77
|
+
export type { RegeneratedFile }
|
|
78
|
+
export { main_merge_regenerate }
|
|
@@ -6,6 +6,7 @@ import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-c
|
|
|
6
6
|
import { error_text } from '#scripts/lib/error-message'
|
|
7
7
|
import { git_command } from './git-command'
|
|
8
8
|
import { main_merge_guard } from './main-merge-guard'
|
|
9
|
+
import { main_merge_regenerate } from './main-merge-regenerate'
|
|
9
10
|
|
|
10
11
|
// `josh main:merge` (`josh mm`) — bring the default branch into the branch this checkout is on.
|
|
11
12
|
//
|
|
@@ -62,18 +63,26 @@ interface MergeTarget {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
// A failure that left no unmerged path — a remote that cannot be reached, a hook that refused — is not
|
|
65
|
-
// a conflict, so it is rethrown rather than reported as one with nothing to resolve.
|
|
66
|
+
// a conflict, so it is rethrown rather than reported as one with nothing to resolve. A conflict only on
|
|
67
|
+
// generated files is regenerated and committed (`main-merge-regenerate.ts`), so it answers `undefined`
|
|
68
|
+
// and the merge reads as finished.
|
|
69
|
+
async function unresolved_conflict(error: unknown): Promise<MergeOutcome | undefined> {
|
|
70
|
+
const files = main_merge_guard.unmerged_paths(await git_command.status())
|
|
71
|
+
|
|
72
|
+
if (files.length === 0) throw error
|
|
73
|
+
|
|
74
|
+
return (await main_merge_regenerate.resolve(files)) ? undefined : { kind: 'conflict', files }
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
async function merge_into(target: MergeTarget): Promise<MergeOutcome> {
|
|
67
78
|
const { default_branch, current_branch } = target
|
|
68
79
|
|
|
69
80
|
try {
|
|
70
81
|
await git_command.merge_branch(default_branch, merge_message(default_branch, current_branch))
|
|
71
82
|
} catch (error) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
if (files.length === 0) throw error
|
|
83
|
+
const conflict = await unresolved_conflict(error)
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
if (conflict !== undefined) return conflict
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
return { kind: target.is_current ? 'current' : 'merged', branch: default_branch }
|
|
@@ -54,16 +54,14 @@ function batching_mode(): LaneGuardMode {
|
|
|
54
54
|
return lane_guard_policy.mode_here('batching')
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// A guard the enumeration turns fully `off` says nothing at all.
|
|
58
|
-
//
|
|
59
|
-
// `off` is skipped. On the main line the mode is `refuse`.
|
|
57
|
+
// A guard the enumeration turns fully `off` says nothing at all. The lane-child mode and its reason
|
|
58
|
+
// live in `lane-guard-policy.ts`; on the main line the mode is `refuse`.
|
|
60
59
|
function is_candidate(call: GuardedCall): boolean {
|
|
61
60
|
return is_batch_candidate(call) && batching_mode() !== 'off'
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
// A refusable call is refused only where the mode is `refuse`.
|
|
65
|
-
//
|
|
66
|
-
// because a refusal ends a headless child's turn (kit#2138).
|
|
63
|
+
// A refusable call is refused only where the mode is `refuse`. Under any other mode it is withheld the
|
|
64
|
+
// refusal; why a lane child takes that mode is its `because` in `lane-guard-policy.ts`.
|
|
67
65
|
function should_block_here(tail: string, call: GuardedCall, refused_at_ms: number): boolean {
|
|
68
66
|
if (batching_mode() !== 'refuse') return false
|
|
69
67
|
|
|
@@ -71,8 +69,7 @@ function should_block_here(tail: string, call: GuardedCall, refused_at_ms: numbe
|
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
// The whole-file write earns a notice on every run. The second branch turns a
|
|
74
|
-
// refusable call into a notice under a `notice`-mode guard
|
|
75
|
-
// child again since kit#2276. Both read the notice's own record, so a notice never spends the refusal's
|
|
72
|
+
// refusable call into a notice under a `notice`-mode guard. Both read the notice's own record, so a notice never spends the refusal's
|
|
76
73
|
// stamp and re-fires on the notice's own `NOTICE_REFIRE_EVERY` cadence.
|
|
77
74
|
function should_notify_here(tail: string, call: GuardedCall, notified_at_ms: number): boolean {
|
|
78
75
|
if (time_batch_guard.should_notify(tail, call, notified_at_ms)) return true
|
|
@@ -251,8 +251,13 @@ function plan_daemon_restart(
|
|
|
251
251
|
): ReadonlyArray<FormatCommand> {
|
|
252
252
|
const relative = relative_to_root(project_root, file_path)
|
|
253
253
|
|
|
254
|
-
if (
|
|
255
|
-
|
|
254
|
+
if (
|
|
255
|
+
relative === undefined ||
|
|
256
|
+
!is_eslint_config_input(relative) ||
|
|
257
|
+
resolve_daemon_cli(ESLINT_DAEMON, project_root) === undefined
|
|
258
|
+
) {
|
|
259
|
+
return []
|
|
260
|
+
}
|
|
256
261
|
|
|
257
262
|
return [{ bin: ESLINT_DAEMON, command_arguments: [DAEMON_RESTART] }]
|
|
258
263
|
}
|
|
@@ -325,8 +330,9 @@ async function run_command(command: FormatCommand, project_root: string): Promis
|
|
|
325
330
|
// something to say. A start failure (non-zero, empty) has already been retried and carries no
|
|
326
331
|
// problems, and prettier's stdout is the list of files it wrote rather than anything to act on.
|
|
327
332
|
function eslint_unfixed(command: FormatCommand, outcome: CommandOutcome): string | undefined {
|
|
328
|
-
if (command.bin !== ESLINT_BIN)
|
|
329
|
-
|
|
333
|
+
if (command.bin !== ESLINT_BIN || outcome.exit_code === 0 || outcome.stdout.length === 0) {
|
|
334
|
+
return undefined
|
|
335
|
+
}
|
|
330
336
|
|
|
331
337
|
return outcome.stdout
|
|
332
338
|
}
|