@joshuafolkken/kit 1.1010.0 → 1.1050.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 +1 -5
- package/.claude/skills/dependency-update/SKILL.md +6 -3
- package/.claude/skills/epic-commands/SKILL.md +46 -17
- package/.claude/skills/verify-ui/SKILL.md +1 -1
- package/.claude/skills/workflow-commands/SKILL.md +10 -25
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-child.md +6 -4
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-park.md +6 -0
- package/.claude/skills/workflow-commands/backlogrun-progress.md +30 -36
- package/.claude/skills/workflow-commands/backlogrun-steps.md +16 -16
- package/.claude/skills/workflow-commands/backlogrun.md +3 -12
- package/.claude/skills/workflow-commands/chain-rule.md +2 -2
- package/.claude/skills/workflow-commands/followup-reference.md +2 -2
- package/.claude/skills/workflow-commands/fullrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/fullrun.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +2 -1
- package/.claude/skills/workflow-commands/into-target.md +1 -1
- package/.claude/skills/workflow-commands/issue-comments.md +1 -1
- package/.claude/skills/workflow-commands/observation-filing.md +80 -126
- package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -36
- package/.claude/skills/workflow-commands/prerequisite.md +1 -1
- package/.claude/skills/workflow-commands/working-tree-hold.md +2 -2
- package/.codex/hooks.json +1 -5
- package/.github/workflows/auto-tag.yml +21 -0
- package/.github/workflows/publish.yml +9 -10
- package/.github/workflows/sonar-qube.yml +1 -1
- package/AGENTS.md +4 -25
- package/CLAUDE.md +24 -38
- package/GEMINI.md +4 -24
- package/README.md +22 -56
- package/dist/CLAUDE.md +24 -38
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-6PSTVOLH.js → chunk-5BK3K7V4.js} +114 -90
- package/dist/hooks/{chunk-PAXMLGSS.js → chunk-7VPDUBRN.js} +93 -175
- package/dist/hooks/{chunk-KPZN42RT.js → chunk-DMQN45FN.js} +3 -3
- package/dist/hooks/{chunk-WMIR5SNX.js → chunk-EJDX2TOB.js} +13 -3
- package/dist/hooks/{chunk-A2H5I2VD.js → chunk-GQ7KP45M.js} +2 -2
- package/dist/hooks/{chunk-4GDR2YTV.js → chunk-HJIISEAC.js} +4 -4
- package/dist/hooks/{chunk-AWTFKKFW.js → chunk-NAS4C7GP.js} +6 -33
- package/dist/hooks/{chunk-56LGMJJV.js → chunk-NJQOBE6L.js} +2 -2
- package/dist/hooks/{chunk-EZLHXKJ6.js → chunk-RLDX5SOC.js} +1222 -941
- package/dist/hooks/{chunk-GBJUERYK.js → chunk-WQ4OSBSS.js} +10 -3
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +15 -34
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +23 -16
- package/dist/hooks/stop-guard.js +21 -200
- package/dist/josh.js +42 -125
- package/dist/version/index.js +466 -42
- package/eslint/base.js +2 -11
- package/package.json +36 -10
- package/prompts/coding-standards.md +15 -290
- package/prompts/collaboration-workflow/cross-repo-epic.md +2 -51
- package/prompts/collaboration-workflow/epic-audit.md +2 -53
- package/prompts/collaboration-workflow/epic-bundle.md +2 -106
- package/prompts/collaboration-workflow/file-edits.md +6 -53
- package/prompts/collaboration-workflow/issue-template.md +4 -4
- package/prompts/collaboration-workflow/operating-rules.md +18 -75
- package/prompts/collaboration-workflow/principles.md +74 -0
- package/prompts/collaboration-workflow/residency.md +55 -15
- package/prompts/collaboration-workflow/rule-delivery.md +5 -46
- package/prompts/collaboration-workflow/shell-body.md +7 -9
- package/prompts/collaboration-workflow/turn-batching.md +21 -37
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/prompts/collaboration-workflow/wip-cap.md +15 -18
- package/prompts/collaboration-workflow.md +22 -28
- package/prompts/sonar-hotspot-handling.md +1 -1
- package/scripts/adopt/adopt-logic.ts +2 -2
- package/scripts/adopt/adopt-toolkits.ts +3 -1
- package/scripts/adopt/adopt.ts +2 -2
- package/scripts/agent/agent-event.ts +2 -9
- package/scripts/agent/agent-exit-record.ts +2 -9
- package/scripts/auto-ok/auto-ok-cli.ts +9 -9
- package/scripts/backlog/backlog-budget-cli.ts +2 -6
- package/scripts/backlog/backlog-drive-cli.ts +20 -10
- package/scripts/backlog/backlog-drive.ts +50 -16
- package/scripts/backlog/backlog-next.ts +24 -8
- package/scripts/backlog/backlog-offer-cli.ts +6 -5
- package/scripts/backlog/backlog-plan-cli.ts +5 -1
- package/scripts/backlog/backlog-plan.ts +1 -1
- package/scripts/backlog/backlog-pool.ts +31 -11
- package/scripts/backlog/backlog-rank.ts +106 -0
- package/scripts/backlog/backlog-waves.ts +42 -29
- package/scripts/build/build-bin.ts +2 -1
- package/scripts/build/build-config-merge.ts +2 -1
- package/scripts/build/build-hooks.ts +31 -6
- package/scripts/build/build-managed-marker.ts +2 -1
- package/scripts/build/build-self-sync-guard.ts +2 -1
- package/scripts/build/build-version.ts +2 -1
- package/scripts/cases/cases-logic.ts +2 -6
- package/scripts/ci/pr-classification.ts +13 -11
- package/scripts/cost-runtime/cost-transcript.ts +4 -11
- package/scripts/delegation/delegation-policy.ts +11 -0
- package/scripts/doctor/doctor-consumer.ts +5 -12
- package/scripts/doctor/doctor-ports.ts +2 -2
- package/scripts/document/document-byte-budget.ts +13 -21
- package/scripts/document/document-section.ts +2 -6
- package/scripts/document/entry-read-budget.ts +6 -5
- package/scripts/{git/git-epic-add-body.ts → epic/epic-add-body.ts} +23 -23
- package/scripts/{git/git-epic-add-plan.ts → epic/epic-add-plan.ts} +23 -26
- package/scripts/{git/git-epic-add.ts → epic/epic-add.ts} +17 -17
- package/scripts/epic/epic-audit-cli.ts +4 -4
- package/scripts/epic/epic-audit-orphans.ts +2 -2
- package/scripts/epic/epic-audit-rationale.ts +1 -1
- package/scripts/epic/epic-audit.ts +1 -1
- package/scripts/{git/git-epic-body.ts → epic/epic-body.ts} +6 -6
- package/scripts/epic/epic-bundle-cli.ts +1 -1
- package/scripts/epic/epic-bundle.ts +2 -2
- package/scripts/epic/epic-candidate-confirm.ts +2 -1
- package/scripts/{git/git-epic-chains.ts → epic/epic-chains.ts} +4 -4
- package/{scripts-ai → scripts/epic}/epic-check.ts +5 -4
- package/{scripts-ai → scripts/epic}/epic-cli.ts +4 -4
- package/scripts/{git/git-epic-close-comment.ts → epic/epic-close-comment.ts} +9 -9
- package/scripts/{git/git-epic-close.ts → epic/epic-close.ts} +23 -19
- package/scripts/epic/epic-cross-repo.ts +21 -38
- package/scripts/{git/git-epic-decision.ts → epic/epic-decision.ts} +12 -14
- package/scripts/epic/epic-fetch.ts +3 -3
- package/scripts/epic/epic-graph.ts +3 -3
- package/scripts/epic/epic-index.ts +2 -2
- package/scripts/epic/epic-issue.ts +1 -1
- package/scripts/epic/epic-lane-offer.ts +11 -22
- package/scripts/epic/epic-next.ts +3 -3
- package/scripts/{git/git-epic-parse.ts → epic/epic-parse.ts} +3 -3
- package/scripts/{git/git-epic-promote.ts → epic/epic-promote.ts} +10 -10
- package/scripts/epic/epic-rank.ts +37 -0
- package/scripts/{git/git-epic-read.ts → epic/epic-read.ts} +9 -9
- package/scripts/{git/git-epic-reconcile.ts → epic/epic-reconcile.ts} +18 -20
- package/scripts/{git/git-epic-reference.ts → epic/epic-reference.ts} +4 -23
- package/scripts/epic/epic-relation-recheck.ts +4 -3
- package/scripts/{git/git-epic-relations.ts → epic/epic-relations.ts} +5 -5
- package/scripts/{git/git-epic-remove-plan.ts → epic/epic-remove-plan.ts} +17 -19
- package/scripts/{git/git-epic-remove.ts → epic/epic-remove.ts} +13 -13
- package/scripts/{git/git-epic-run.ts → epic/epic-run.ts} +27 -27
- package/scripts/{git/git-epic-sections.ts → epic/epic-sections.ts} +6 -6
- package/scripts/{git/git-epic-shape.ts → epic/epic-shape.ts} +7 -7
- package/scripts/epic/epic-solo.ts +29 -28
- package/scripts/{git/git-epic-validate.ts → epic/epic-validate.ts} +13 -13
- package/{scripts-ai → scripts/epic}/epic.ts +17 -16
- package/scripts/gh/fix-gh-packages.ts +2 -1
- package/scripts/gh/gh-spawn.ts +12 -10
- package/scripts/git/constants.ts +1 -1
- package/scripts/git/git-branch.ts +10 -1
- package/scripts/git/git-command.ts +2 -2
- package/scripts/git/git-error.ts +2 -5
- package/scripts/git/git-execa-error.ts +6 -1
- package/{scripts-ai → scripts/git}/git-followup-finish.ts +7 -7
- package/scripts/git/git-followup-flush.ts +74 -48
- package/scripts/git/git-followup-issue-close.ts +2 -2
- package/scripts/git/git-followup-label.ts +1 -1
- package/scripts/git/git-followup-pending.ts +1 -1
- package/{scripts-ai → scripts/git}/git-followup-workflow.ts +23 -11
- package/scripts/git/git-gh-exec.ts +85 -26
- package/scripts/git/git-gh-issue-read.ts +2 -2
- package/scripts/git/git-gh-issue-rest.ts +2 -2
- package/scripts/git/git-gh-issue-write.ts +6 -6
- package/scripts/git/git-gh-pr-read.ts +0 -21
- package/scripts/git/git-gh-pr.ts +2 -23
- package/scripts/git/git-next-issues.ts +34 -5
- package/scripts/git/git-pr-followup-wrapup.ts +2 -6
- package/scripts/git/git-preflight.ts +25 -10
- package/scripts/git/git-push-transport.ts +12 -69
- package/scripts/git/git-spawn.ts +79 -3
- package/scripts/git/git-ssh-keepalive.ts +74 -0
- package/scripts/git/git-staging.ts +21 -9
- package/{scripts-ai → scripts/git}/git-workflow.ts +11 -11
- package/scripts/git/github-issue-url.ts +1 -1
- package/scripts/git/gone-branch.ts +3 -2
- package/scripts/git/issue-labels.ts +13 -1
- package/scripts/git/issue-rank.ts +89 -0
- package/scripts/git/issue-reference.ts +19 -0
- package/scripts/git/main-merge.ts +2 -1
- package/scripts/git/main-sync.ts +3 -2
- package/scripts/git/schemas.ts +1 -1
- package/scripts/git/stash-pop-args.ts +7 -13
- package/scripts/git/stash-pop-cli.ts +2 -1
- package/{scripts-ai → scripts/git}/telegram-test-logic.ts +2 -2
- package/{scripts-ai → scripts/git}/telegram-test.ts +7 -7
- package/scripts/hooks/format-edited-cli.ts +15 -0
- package/scripts/hooks/format-edited-file.ts +2 -9
- package/scripts/hooks/format-rewrite-notice.ts +2 -8
- package/scripts/hooks/pretool-guard-cli.ts +21 -0
- package/scripts/hooks/pretool-guard.ts +14 -15
- package/scripts/init/init-ai-copy.ts +2 -5
- package/scripts/init/init.ts +8 -4
- package/scripts/init/start-exec.ts +7 -1
- package/scripts/init/start-plan.ts +18 -5
- package/scripts/init/start-setup-pr.ts +46 -19
- package/scripts/init/start-steps.ts +43 -10
- package/scripts/init/start.ts +42 -11
- package/scripts/issue/issue-cite.ts +19 -1
- package/scripts/issue/issue-comment-cli.ts +2 -1
- package/scripts/issue/issue-file-cli.ts +2 -1
- package/scripts/issue/issue-fold-cli.ts +9 -16
- package/scripts/issue/issue-scout-cli.ts +8 -13
- package/scripts/issue/measure-rerun-cli.ts +7 -6
- package/scripts/josh/josh-command-map.ts +0 -102
- package/scripts/josh/josh-commands-ai.ts +2 -2
- package/scripts/josh/josh-commands-backlog.ts +1 -1
- package/scripts/josh/josh-commands-development.ts +3 -3
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/josh/josh-commands-workflow.ts +4 -4
- package/scripts/lane/lane-child-marker.ts +16 -2
- package/scripts/lane/lane-cli.ts +34 -15
- package/scripts/lane/lane-open.ts +3 -2
- package/scripts/lane/lane-registry.ts +3 -6
- package/scripts/lane/lane-report.ts +10 -4
- package/scripts/lib/cli-flags.ts +20 -2
- package/scripts/lib/error-message.ts +9 -0
- package/scripts/lib/read-file.ts +26 -2
- package/scripts/observations/observation-ledger-home.ts +98 -26
- package/scripts/observations/observation-ledger-migrate.ts +26 -21
- package/scripts/observations/observation-ledger-prepare.ts +10 -20
- package/scripts/observations/observation-ledger.ts +58 -25
- package/scripts/observations/observations-flush-cli.ts +6 -23
- package/scripts/observations/observations-flush-landing.ts +2 -1
- package/scripts/observations/observations-flush.ts +29 -20
- package/scripts/package/package-scout-cli.ts +8 -16
- package/scripts/ports/port-command.ts +2 -1
- package/scripts/propagate/propagate-plan.ts +185 -0
- package/scripts/propagate/propagate-publish.ts +6 -7
- package/scripts/propagate/propagate-run.ts +5 -1
- package/scripts/propagate/propagate-steps.ts +18 -2
- package/scripts/propagate/propagate-targets.ts +13 -2
- package/scripts/propagate/propagate.ts +63 -44
- package/scripts/refactor/import-graph.ts +2 -6
- package/scripts/registry-migration/migrate-logic.ts +1 -0
- package/scripts/registry-migration/migrate.ts +3 -2
- package/scripts/registry-migration/user-npmrc.ts +2 -2
- package/scripts/release/github-release.ts +1 -1
- package/scripts/release/release-cli.ts +2 -1
- package/scripts/repo/repo-setting.ts +10 -8
- package/scripts/review/review-attest.ts +44 -0
- package/scripts/review/review-brief-cli.ts +49 -2
- package/scripts/review/review-brief.ts +46 -1
- package/scripts/review/review-finding-ledger.ts +3 -3
- package/scripts/review/review-findings-cli.ts +4 -11
- package/scripts/review/review-record-cli.ts +14 -11
- package/scripts/review/review-record.ts +6 -16
- package/scripts/review/review-stamps.ts +7 -0
- package/scripts/rules/decision-oracle.ts +2 -2
- package/scripts/rules/implementation-cut.ts +1 -1
- package/scripts/rules/issue-fold.ts +1 -2
- package/scripts/rules/lane-carry-conflict.ts +1 -2
- package/scripts/rules/lane-park.ts +1 -2
- package/scripts/rules/pre-gate-cut.ts +2 -2
- package/scripts/rules/rule-body-guard.ts +2 -2
- package/scripts/rules/rule-value-cache.ts +99 -0
- package/scripts/rules/rule-value-cli.ts +10 -9
- package/scripts/rules/rule-value.ts +1 -1
- package/scripts/run/run-carry-args.ts +2 -6
- package/scripts/run/run-carry-cli.ts +5 -7
- package/scripts/run/run-cut-args.ts +7 -13
- package/scripts/run/run-ending-cli.ts +4 -7
- package/scripts/run/run-event-stream.ts +2 -5
- package/scripts/run/run-headless.ts +22 -6
- package/scripts/run/run-liveness-cli.ts +4 -7
- package/scripts/run/run-parent-cut-hook.ts +144 -0
- package/scripts/run/run-progress-args.ts +2 -6
- package/scripts/run/run-progress-cli.ts +2 -1
- package/scripts/run/run-progress-read.ts +2 -1
- package/scripts/run/run-progress.ts +21 -8
- package/scripts/run/run-ship-cli.ts +6 -74
- package/scripts/run/run-ship-preflight.ts +87 -0
- package/scripts/run/run-ship-review-steps.ts +5 -26
- package/scripts/run/run-ship-review.ts +9 -0
- package/scripts/run/run-ship-scoped.ts +44 -0
- package/scripts/run/run-ship-stage.ts +5 -1
- package/scripts/run/run-ship-steps.ts +105 -0
- package/scripts/run/run-ship.ts +2 -0
- package/scripts/run/run-tail-cli.ts +3 -3
- package/scripts/run/run-tidy-stashes.ts +7 -14
- package/scripts/run/run-wake-cli.ts +4 -8
- package/scripts/run/run-wake-describe.ts +19 -1
- package/scripts/run/run-wake-driver.ts +15 -2
- package/scripts/run/run-wake-session.ts +14 -4
- package/scripts/run/run-watcher-hook.ts +1 -1
- package/scripts/safe-chain/project-config.ts +3 -6
- package/scripts/security/security-audit-provision.ts +6 -7
- package/scripts/sonar/sonar-hotspots-cli.ts +2 -1
- package/scripts/sync/directory-copy-guard.ts +2 -1
- package/scripts/sync/sync-ai-files.ts +3 -3
- package/scripts/sync/template-source-logic.ts +3 -4
- package/scripts/test/e2e-retry-check.ts +3 -6
- package/scripts/time-runtime/time-bundle-call.ts +2 -2
- package/scripts/ui/ui-routes-cli.ts +3 -6
- package/scripts/version/bump-version.ts +2 -1
- package/scripts/version/npm-registry.ts +97 -0
- package/scripts/version/version-commands.ts +5 -4
- package/scripts/version/version-remote.ts +38 -33
- package/scripts/version/version-targets.ts +3 -3
- package/.claude/skills/workflow-commands/rule-residency.md +0 -267
- package/evals/scenarios/consult-not-execute.json +0 -20
- package/evals/scenarios/no-implicit-workflow.json +0 -26
- package/evals/scenarios/overrides-both-locations.json +0 -29
- package/evals/scenarios/upstream-interrupt.json +0 -23
- package/evals/scenarios/workflow-skill-loaded.json +0 -16
- package/prompts/agent-lint-protocol.json +0 -79
- package/prompts/agent-rules.md +0 -63
- package/prompts/collaboration-workflow/consultation-vs-execution.md +0 -8
- package/prompts/collaboration-workflow/distributed-docs.md +0 -7
- package/prompts/collaboration-workflow/durable-rules.md +0 -24
- package/prompts/collaboration-workflow/latest-first.md +0 -14
- package/prompts/collaboration-workflow/no-clones.md +0 -11
- package/prompts/collaboration-workflow/simplicity-first.md +0 -9
- package/prompts/collaboration-workflow/single-source-rules.md +0 -13
- package/scripts/build/shim-shell.ts +0 -25
- package/scripts/claude/skill-meta.ts +0 -37
- package/scripts/document/document-scan.ts +0 -108
- package/scripts/document/generate-catalog.ts +0 -69
- package/scripts/document/retired-phrases.ts +0 -143
- package/scripts/document/review-agent-type.ts +0 -46
- package/scripts/document/single-source.ts +0 -73
- package/scripts/git/git-fixture-workspace.ts +0 -88
- package/scripts/hooks/format-edited-file-fixtures.ts +0 -109
- package/scripts/hooks/prompt-hooks.ts +0 -38
- package/scripts/init/hook-command-bootstrap.ts +0 -83
- package/scripts/issue/issue-logic.ts +0 -50
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +0 -1227
- package/scripts/josh/josh-script-reader.ts +0 -11
- package/scripts/package/production-dependency-graph.ts +0 -68
- package/scripts/retrospective/retrospective-cli.ts +0 -97
- package/scripts/retrospective/retrospective.ts +0 -127
- package/scripts/rules/delivered-rules-harness.ts +0 -72
- package/scripts/rules/top-level-scripts-guard.ts +0 -47
- package/scripts/run/run-carry-flush.ts +0 -58
- package/scripts/run/run-transcript.golden.txt +0 -57
- package/scripts/run/run-wake-work.ts +0 -108
- package/scripts/sync/dependabot-pin-logic.ts +0 -135
- package/scripts/test/classify-isolation.ts +0 -149
- package/scripts/test/http-fault-injection.ts +0 -79
- package/scripts/test/josh-harness-environment.ts +0 -260
- package/scripts/test/josh-harness.ts +0 -96
- package/scripts/test/pilot-files.ts +0 -452
- package/scripts/test/pilot-measure.ts +0 -78
- package/scripts/test/test-network-guard.ts +0 -453
- package/scripts/test/test-repository-guard.ts +0 -317
- package/scripts/test/test-state-guard.ts +0 -85
- package/scripts/test/test-stdout-guard.ts +0 -54
- package/scripts/test/test-telegram-guard.ts +0 -90
- package/scripts/test/unit-guard-environment.ts +0 -12
- package/scripts/test/unit-projects.ts +0 -139
- package/scripts/test/vitest-include-globs.ts +0 -12
- package/scripts/time-runtime/time-batch-guard-fixtures.ts +0 -66
- package/scripts-ai/completion-gate.ts +0 -13
- package/scripts-ai/environment-loader.ts +0 -9
- /package/{scripts-ai → scripts/epic}/epic-cli-argv.ts +0 -0
- /package/{scripts-ai → scripts/epic}/epic-cli-remove.ts +0 -0
- /package/{scripts-ai → scripts/git}/followup-issue-number.ts +0 -0
package/scripts/git/git-gh-pr.ts
CHANGED
|
@@ -21,7 +21,8 @@ import { git_gh_pr_snapshot } from './git-gh-pr-snapshot'
|
|
|
21
21
|
// **Every path here was measured against the live API before it was written**, on a throwaway pull
|
|
22
22
|
// request opened between two throwaway branches so that neither `main` nor any real pull request was
|
|
23
23
|
// touched. Two of the four did not behave as reading the documentation would suggest: see
|
|
24
|
-
// `pr_create` for the duplicate-head response
|
|
24
|
+
// `pr_create` for the duplicate-head response. The other, `pr_checkout`, is gone with its one caller,
|
|
25
|
+
// `sync-dependabot-pins` (joshuafolkken/kit#2887).
|
|
25
26
|
//
|
|
26
27
|
// `pr_checks` — a `gh pr checks <branch>` wrapper — is gone rather than converted. Nothing called it:
|
|
27
28
|
// the merge gate reads the rollup out of the snapshot and `git-pr-checks-watch.ts` runs the watching
|
|
@@ -139,27 +140,6 @@ async function pr_create(
|
|
|
139
140
|
return url
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
// **`gh pr checkout` does use GraphQL** — one `POST /graphql` to resolve the pull request, measured
|
|
143
|
-
// with `GH_DEBUG=api` on the throwaway pull request (joshuafolkken/kit#1029). Everything after that
|
|
144
|
-
// is git, so only the resolution had to move: `pr_head_reference` is the REST read that already
|
|
145
|
-
// replaced it for this file's one caller, `sync-dependabot-pins.ts`.
|
|
146
|
-
//
|
|
147
|
-
// `fetch_branch` updates `refs/remotes/origin/<branch>`, which is what lets the plain `checkout`
|
|
148
|
-
// resolve a branch that exists only on the remote — the tracking branch `gh` left behind, by the
|
|
149
|
-
// operations `git-command.ts` already owns rather than a new wrapper. The fast-forward is the third
|
|
150
|
-
// thing the CLI did and the one a fetch-and-checkout pair silently drops: a branch that is already
|
|
151
|
-
// local is checked out at whatever commit the last run left it on.
|
|
152
|
-
//
|
|
153
|
-
// A fork's head is refused by `pr_head_reference` rather than fetched from `origin` under a name
|
|
154
|
-
// that repository may use for something else.
|
|
155
|
-
async function pr_checkout(pr_number: number): Promise<void> {
|
|
156
|
-
const branch_name = await git_gh_pr_read.pr_head_reference(pr_number)
|
|
157
|
-
|
|
158
|
-
await git_command.fetch_branch(branch_name)
|
|
159
|
-
await git_command.checkout(branch_name)
|
|
160
|
-
await git_command.merge_fast_forward(branch_name)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
143
|
// A pull request's conversation comment is an **issue** comment: `POST issues/{N}/comments` is the
|
|
164
144
|
// endpoint, and it answers the same `html_url` for a pull request as for an issue (measured on the
|
|
165
145
|
// throwaway pull request). `git-gh-issue-write.ts` already posts to it, so this resolves the number
|
|
@@ -270,7 +250,6 @@ const git_gh_pr = {
|
|
|
270
250
|
pr_create,
|
|
271
251
|
pr_get_classification,
|
|
272
252
|
pr_ensure_classification,
|
|
273
|
-
pr_checkout,
|
|
274
253
|
pr_comment,
|
|
275
254
|
pr_merge,
|
|
276
255
|
pr_update_body,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { git_gh_command } from './git-gh-command'
|
|
2
2
|
import { has_any_label, NOT_DIRECTLY_RUNNABLE_LABELS } from './issue-labels'
|
|
3
|
+
import { issue_rank, type DependencyRow } from './issue-rank'
|
|
3
4
|
import { parse_json_array_safe } from './parse-json-array'
|
|
4
5
|
import { open_issue_schema, type OpenIssueData } from './schemas'
|
|
5
6
|
|
|
6
7
|
// The next-issues display printed when a workflow completes (#821): up to five open issues in
|
|
7
8
|
// priority order, so the user picks the next run from the completion output instead of opening the
|
|
8
9
|
// issue list. Priority is recency — a newer issue usually encodes the most current understanding
|
|
9
|
-
// of the backlog — with the label-based exclusions below.
|
|
10
|
+
// of the backlog — with the label-based exclusions below, under the ranking keys `issue-rank.ts`
|
|
11
|
+
// defines (joshuafolkken/kit#2928).
|
|
10
12
|
//
|
|
11
13
|
// `prioritize` is also the order `epicrun` picks up `auto-ok` issues in (joshuafolkken/kit#906).
|
|
12
14
|
// The two ask one question — which open issue outside an epic is run next — so they share one
|
|
@@ -37,6 +39,34 @@ function compare_newest_first(first: OpenIssueData, second: OpenIssueData): numb
|
|
|
37
39
|
return first.createdAt < second.createdAt ? 1 : -1
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
// A row's blockers by number. **Lossy across repositories**: a blocker elsewhere sharing a number
|
|
43
|
+
// with a row here counts as one of its dependents. The listings this ranks are one repository's own,
|
|
44
|
+
// and `backlog:next` re-ranks its offer from the qualified graph (`backlog-rank.ts`), so the cost is a
|
|
45
|
+
// display row placed one slot off rather than a run started early.
|
|
46
|
+
function dependency_row(issue: OpenIssueData): DependencyRow {
|
|
47
|
+
return {
|
|
48
|
+
key: String(issue.number),
|
|
49
|
+
blockers: (issue.blockedBy?.nodes ?? []).map((blocker) => String(blocker.number)),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Every candidate in rank order, uncapped. `backlog:next` takes this rather than `prioritize`, so the
|
|
54
|
+
// `run:solo` gate sees the whole ranking before anything is cut (joshuafolkken/kit#2928).
|
|
55
|
+
function order(
|
|
56
|
+
issues: ReadonlyArray<OpenIssueData>,
|
|
57
|
+
completed_issue_number?: number,
|
|
58
|
+
): Array<OpenIssueData> {
|
|
59
|
+
const candidates = issues
|
|
60
|
+
.filter((issue) => is_candidate(issue, completed_issue_number))
|
|
61
|
+
.toSorted(compare_newest_first)
|
|
62
|
+
const dependents = issue_rank.count_dependents(candidates.map((issue) => dependency_row(issue)))
|
|
63
|
+
|
|
64
|
+
return issue_rank.rank(candidates, (issue) => ({
|
|
65
|
+
labels: (issue.labels ?? []).map((label) => label.name),
|
|
66
|
+
dependents: dependents.get(String(issue.number)) ?? 0,
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
// **This display deliberately does not drop a blocked issue, and the `auto-ok` pickup deliberately
|
|
41
71
|
// does.** They share this ordering and differ on the set, which reads as an inconsistency and is not
|
|
42
72
|
// one (joshuafolkken/kit#1005).
|
|
@@ -50,10 +80,7 @@ function prioritize(
|
|
|
50
80
|
issues: ReadonlyArray<OpenIssueData>,
|
|
51
81
|
completed_issue_number?: number,
|
|
52
82
|
): Array<OpenIssueData> {
|
|
53
|
-
return issues
|
|
54
|
-
.filter((issue) => is_candidate(issue, completed_issue_number))
|
|
55
|
-
.toSorted(compare_newest_first)
|
|
56
|
-
.slice(0, DISPLAY_LIMIT)
|
|
83
|
+
return order(issues, completed_issue_number).slice(0, DISPLAY_LIMIT)
|
|
57
84
|
}
|
|
58
85
|
|
|
59
86
|
// Empty input yields no lines at all — a bare header with nothing under it reads as an error.
|
|
@@ -90,6 +117,8 @@ async function fetch_next_issue_lines(completed_issue_number?: number): Promise<
|
|
|
90
117
|
}
|
|
91
118
|
|
|
92
119
|
const git_next_issues = {
|
|
120
|
+
DISPLAY_LIMIT,
|
|
121
|
+
order,
|
|
93
122
|
prioritize,
|
|
94
123
|
format_lines,
|
|
95
124
|
fetch_next_issue_lines,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { epic_close } from '#scripts/epic/epic-close'
|
|
2
2
|
import { git_followup_cleanup } from './git-followup-cleanup'
|
|
3
|
-
import { git_followup_flush } from './git-followup-flush'
|
|
4
3
|
import { git_followup_issue_close } from './git-followup-issue-close'
|
|
5
4
|
import { git_followup_label } from './git-followup-label'
|
|
6
5
|
import { git_followup_stages, type StageLog } from './git-followup-stages'
|
|
@@ -137,7 +136,7 @@ async function epic_close_step(input: WrapupInput): Promise<void> {
|
|
|
137
136
|
label: 'The epic auto-close',
|
|
138
137
|
recovery: undefined,
|
|
139
138
|
run: async () => {
|
|
140
|
-
await
|
|
139
|
+
await epic_close.close_completed_epics({
|
|
141
140
|
issue_number: input.issue_number,
|
|
142
141
|
is_merged: input.should_merge,
|
|
143
142
|
})
|
|
@@ -234,9 +233,6 @@ async function run_wrapup(input: WrapupInput, log: StageLog): Promise<void> {
|
|
|
234
233
|
}
|
|
235
234
|
|
|
236
235
|
await run_tail_steps(input)
|
|
237
|
-
// After the merge tail and before the hold is released (`git-followup-finish.ts`): the ledger
|
|
238
|
-
// flush switches branches, so it must land while this run still holds the tree (kit#1810).
|
|
239
|
-
await git_followup_flush.flush_ledger_step(input.should_merge)
|
|
240
236
|
|
|
241
237
|
lap(log, STAGE.completion_and_epic_close)
|
|
242
238
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
1
2
|
import { live_evidence } from '#scripts/review/live-evidence'
|
|
2
3
|
import { changed_paths } from './changed-paths'
|
|
3
4
|
import { git_branch } from './git-branch'
|
|
@@ -28,10 +29,6 @@ interface Resolution {
|
|
|
28
29
|
problems: Array<string>
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function error_message(error: unknown): string {
|
|
32
|
-
return error instanceof Error ? error.message : String(error)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
32
|
function resolve_from_branch(current_branch: string): Resolution {
|
|
36
33
|
const number = git_issue.branch_number(current_branch)
|
|
37
34
|
|
|
@@ -71,7 +68,7 @@ function resolve_from_title(
|
|
|
71
68
|
try {
|
|
72
69
|
return resolve_parsed(git_issue.parse(cli_input), current_branch, default_branch)
|
|
73
70
|
} catch (error) {
|
|
74
|
-
return { target: undefined, problems: [
|
|
71
|
+
return { target: undefined, problems: [error_text.message_of(error)] }
|
|
75
72
|
}
|
|
76
73
|
}
|
|
77
74
|
|
|
@@ -89,7 +86,7 @@ async function classification_problems(target: Target): Promise<Array<string>> {
|
|
|
89
86
|
|
|
90
87
|
return []
|
|
91
88
|
} catch (error) {
|
|
92
|
-
return [
|
|
89
|
+
return [error_text.message_of(error)]
|
|
93
90
|
}
|
|
94
91
|
}
|
|
95
92
|
|
|
@@ -120,19 +117,37 @@ function refusal(problems: ReadonlyArray<string>): Error {
|
|
|
120
117
|
)
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
|
|
120
|
+
interface PreflightAnswer {
|
|
121
|
+
pr_target: Target | undefined
|
|
122
|
+
problems: Array<string>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function answer(input: PreflightInput): Promise<PreflightAnswer> {
|
|
124
126
|
const { target, problems } = await resolve_target(input.cli_input)
|
|
125
127
|
// The classification and the evidence belong to the pull request, so a `--skip-pr` run asks neither.
|
|
126
128
|
const pr_target = input.will_open_pr ? target : undefined
|
|
127
129
|
const pr_problems = pr_target === undefined ? [] : await classification_problems(pr_target)
|
|
128
|
-
const all_problems = [...problems, ...pr_problems]
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
return { pr_target, problems: [...problems, ...pr_problems] }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// joshuafolkken/kit#2946: the unmet preconditions as a list, so `josh ship` can ask them before its
|
|
135
|
+
// review and gate start instead of meeting the refusal at the commit stage.
|
|
136
|
+
async function problems_of(input: PreflightInput): Promise<Array<string>> {
|
|
137
|
+
const { problems } = await answer(input)
|
|
138
|
+
|
|
139
|
+
return problems
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function check(input: PreflightInput): Promise<void> {
|
|
143
|
+
const { pr_target, problems } = await answer(input)
|
|
144
|
+
|
|
145
|
+
if (problems.length > 0) throw refusal(problems)
|
|
131
146
|
|
|
132
147
|
if (pr_target !== undefined) await announce_evidence(pr_target.branch_name)
|
|
133
148
|
}
|
|
134
149
|
|
|
135
|
-
const git_preflight = { check }
|
|
150
|
+
const git_preflight = { check, problems_of }
|
|
136
151
|
|
|
137
152
|
export type { PreflightInput }
|
|
138
153
|
export { git_preflight }
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { execa } from 'execa'
|
|
2
2
|
import { git_utilities } from './constants'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
create_spawn_error,
|
|
5
|
+
get_exit_code,
|
|
6
|
+
has_timed_out,
|
|
7
|
+
TIMEOUT_EXIT_CODE,
|
|
8
|
+
} from './git-execa-error'
|
|
9
|
+
import { git_ssh_keepalive } from './git-ssh-keepalive'
|
|
4
10
|
|
|
5
11
|
const MS_PER_SECOND = 1000
|
|
6
12
|
|
|
@@ -33,65 +39,10 @@ const PUSH_TIMEOUT_LABEL = `${String(PUSH_TIMEOUT_SECONDS)}s`
|
|
|
33
39
|
// said no" are different diagnoses, and only the first one is worth retrying.
|
|
34
40
|
const PUSH_TIMEOUT_MESSAGE = 'git push timed out'
|
|
35
41
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// against '128', which this can never equal, so a timed-out bare push is never mistaken for a
|
|
39
|
-
// missing upstream and retried as `--set-upstream`.
|
|
40
|
-
const PUSH_TIMEOUT_EXIT_CODE = 'timeout'
|
|
41
|
-
|
|
42
|
-
// SSH's own liveness probe: three unanswered checks 15 seconds apart, so a connection that dies
|
|
43
|
-
// mid-transfer is noticed in about 45 seconds instead of waiting on the TCP default. The budget
|
|
44
|
-
// above bounds the damage either way; this is what makes the common case fail in seconds rather
|
|
45
|
-
// than sit out the whole budget.
|
|
46
|
-
const SSH_KEEPALIVE_OPTIONS = '-o ServerAliveInterval=15 -o ServerAliveCountMax=3'
|
|
47
|
-
const SSH_COMMAND_VARIABLE = 'GIT_SSH_COMMAND'
|
|
48
|
-
const SSH_LEGACY_VARIABLE = 'GIT_SSH'
|
|
49
|
-
const KEEPALIVE_SSH_COMMAND = `ssh ${SSH_KEEPALIVE_OPTIONS}`
|
|
50
|
-
const SSH_COMMAND_CONFIG_KEY = 'core.sshCommand'
|
|
42
|
+
// `TIMEOUT_EXIT_CODE` is never '128', which `is_upstream_not_set_error` compares against, so a
|
|
43
|
+
// timed-out bare push is never mistaken for a missing upstream and retried as `--set-upstream`.
|
|
51
44
|
const PUSH_SUBCOMMAND = 'push'
|
|
52
45
|
|
|
53
|
-
// Whatever already decides how ssh is invoked, or an empty string when nothing does. git reads three
|
|
54
|
-
// sources in this order — `GIT_SSH_COMMAND`, `core.sshCommand`, then the legacy `GIT_SSH` — and the
|
|
55
|
-
// last one has to be asked as well precisely because it loses: setting `GIT_SSH_COMMAND` on top of a
|
|
56
|
-
// `GIT_SSH=plink.exe` outranks it, so the push would run an ssh binary the user did not choose.
|
|
57
|
-
function to_environment_value(name: string): string {
|
|
58
|
-
return process.env[name]?.trim() ?? ''
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function to_configured_ssh_command(): string {
|
|
62
|
-
const from_command = to_environment_value(SSH_COMMAND_VARIABLE)
|
|
63
|
-
|
|
64
|
-
return from_command === '' ? to_environment_value(SSH_LEGACY_VARIABLE) : from_command
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Whether any of those three sources answered. **A hit means hands off entirely.**
|
|
68
|
-
//
|
|
69
|
-
// Appending the options to a command someone else chose is not safe, because they are OpenSSH's:
|
|
70
|
-
// `plink` and `TortoiseGitPlink` are supported ssh commands on the platform `get_git_command_for_spawn`
|
|
71
|
-
// goes out of its way to handle, and a wrapper script with a fixed argument list is common on any
|
|
72
|
-
// platform — each of them exits on a usage error rather than pushing. Setting the variable while
|
|
73
|
-
// `core.sshCommand` is configured is the same mistake from the other side: the environment wins, so
|
|
74
|
-
// a per-repository key would be silently replaced by a plain `ssh`.
|
|
75
|
-
//
|
|
76
|
-
// What those users lose is the keepalive, not the fix: the budget above still bounds their push, and
|
|
77
|
-
// `ServerAliveInterval` belongs in their own ssh config where it applies to every tool they run.
|
|
78
|
-
async function has_configured_ssh_command(): Promise<boolean> {
|
|
79
|
-
if (to_configured_ssh_command() !== '') return true
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const git_command_bin = git_utilities.get_git_command_for_spawn()
|
|
83
|
-
// execa runs the binary directly with an argument array and no `shell` option, so CLI
|
|
84
|
-
// args cannot break out of a shell sandbox; the git command and args are internally
|
|
85
|
-
// controlled, never untrusted input. tssecurity:S8705 is a false positive here.
|
|
86
|
-
const { stdout } = await execa(git_command_bin, ['config', '--get', SSH_COMMAND_CONFIG_KEY]) // NOSONAR
|
|
87
|
-
|
|
88
|
-
return stdout.trim() !== ''
|
|
89
|
-
} catch {
|
|
90
|
-
// `git config --get` exits 1 when the key is unset, which is the answer rather than a failure.
|
|
91
|
-
return false
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
46
|
interface PushOptions {
|
|
96
47
|
stdio: 'inherit'
|
|
97
48
|
timeout: number
|
|
@@ -99,12 +50,10 @@ interface PushOptions {
|
|
|
99
50
|
}
|
|
100
51
|
|
|
101
52
|
async function to_push_options(): Promise<PushOptions> {
|
|
102
|
-
const should_keep_alive = !(await has_configured_ssh_command())
|
|
103
|
-
|
|
104
53
|
return {
|
|
105
54
|
stdio: 'inherit',
|
|
106
55
|
timeout: PUSH_TIMEOUT_MS,
|
|
107
|
-
env:
|
|
56
|
+
env: await git_ssh_keepalive.to_environment(),
|
|
108
57
|
}
|
|
109
58
|
}
|
|
110
59
|
|
|
@@ -142,7 +91,7 @@ function to_retry_notice(arguments_list: Array<string>): string {
|
|
|
142
91
|
function create_timeout_error(arguments_list: Array<string>): Error {
|
|
143
92
|
const message = `${PUSH_TIMEOUT_MESSAGE} twice (${PUSH_TIMEOUT_LABEL} each). Re-run it by hand: ${to_manual_command(arguments_list)}`
|
|
144
93
|
|
|
145
|
-
return new Error(message, { cause: { exit_code:
|
|
94
|
+
return new Error(message, { cause: { exit_code: TIMEOUT_EXIT_CODE } })
|
|
146
95
|
}
|
|
147
96
|
|
|
148
97
|
// One push, bounded and retried once — and **only** on a timeout. A push the remote rejected (a
|
|
@@ -164,10 +113,4 @@ const git_push_transport = {
|
|
|
164
113
|
}
|
|
165
114
|
|
|
166
115
|
export { git_push_transport }
|
|
167
|
-
export {
|
|
168
|
-
KEEPALIVE_SSH_COMMAND,
|
|
169
|
-
PUSH_TIMEOUT_MESSAGE,
|
|
170
|
-
PUSH_TIMEOUT_MS,
|
|
171
|
-
SSH_COMMAND_VARIABLE,
|
|
172
|
-
SSH_LEGACY_VARIABLE,
|
|
173
|
-
}
|
|
116
|
+
export { PUSH_TIMEOUT_MESSAGE, PUSH_TIMEOUT_MS }
|
package/scripts/git/git-spawn.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { execa } from 'execa'
|
|
2
2
|
import { git_utilities } from './constants'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
create_spawn_error,
|
|
5
|
+
get_exit_code,
|
|
6
|
+
has_timed_out,
|
|
7
|
+
TIMEOUT_EXIT_CODE,
|
|
8
|
+
} from './git-execa-error'
|
|
9
|
+
import { PUSH_TIMEOUT_MS } from './git-push-transport'
|
|
10
|
+
import { git_ssh_keepalive } from './git-ssh-keepalive'
|
|
4
11
|
|
|
5
12
|
// The one place the ordinary git commands are spawned from (joshuafolkken/kit#1640). It sits in a
|
|
6
13
|
// module of its own rather than inside `git-command.ts` so that a second command module —
|
|
@@ -9,8 +16,8 @@ import { create_spawn_error, get_exit_code } from './git-execa-error'
|
|
|
9
16
|
// `CLAUDE.md` prohibits, so a new caller imports this one instead of growing its own.
|
|
10
17
|
//
|
|
11
18
|
// **`git-push-transport.ts` is the one deliberate exception among the command modules** and is not a
|
|
12
|
-
// second helper of this kind: a push needs a
|
|
13
|
-
//
|
|
19
|
+
// second helper of this kind: a push needs a transport-fault retry that no other command has, so it
|
|
20
|
+
// spawns git itself. The timeout it carries is shared here by `read_remote` / `with_output_remote`. Those two files are the spawn sites of the ordinary command
|
|
14
21
|
// modules — the claim stops there. Other parts of this package spawn git for their own purposes
|
|
15
22
|
// (`scripts/run/run-progress-clock.ts`, `scripts/propagate/propagate-git.ts`,
|
|
16
23
|
// `scripts/doctor/doctor-io.ts`, `scripts/git/git-fixture-workspace.ts`), and an audit of how the git
|
|
@@ -26,6 +33,73 @@ async function read(arguments_: Array<string>): Promise<string> {
|
|
|
26
33
|
return stdout.trimEnd()
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
// The budget for a git call that talks to the remote — `fetch` and `pull` (joshuafolkken/kit#2942).
|
|
37
|
+
// `read` and `with_output` wait without end, and a `git fetch --prune` under `josh main:sync` sat on
|
|
38
|
+
// a dead ssh connection for 37 minutes, stalling the unattended backlog driver whose merge step runs
|
|
39
|
+
// it. The push's budget is reused rather than a second number chosen: both transfer objects over the
|
|
40
|
+
// same transport, and that one is already read off what a healthy transfer costs here.
|
|
41
|
+
const REMOTE_TIMEOUT_MS = PUSH_TIMEOUT_MS
|
|
42
|
+
const MS_PER_SECOND = 1000
|
|
43
|
+
|
|
44
|
+
// A budget kill is reported as a timeout, never as whatever the killed process printed first, and
|
|
45
|
+
// `cause` carries the same `TIMEOUT_EXIT_CODE` a timed-out push does. Unlike the push it is not
|
|
46
|
+
// retried here: what the issue needed is that the wait ends, and a failure then surfaces to the
|
|
47
|
+
// caller — `run:merge` raises a failed `main:sync` — instead of a second budget doubling the wait.
|
|
48
|
+
function to_remote_error(command: string, error: unknown, timeout_ms: number): Error {
|
|
49
|
+
if (!has_timed_out(error)) return create_spawn_error(command, get_exit_code(error))
|
|
50
|
+
|
|
51
|
+
const seconds = String(Math.round(timeout_ms / MS_PER_SECOND))
|
|
52
|
+
|
|
53
|
+
return new Error(`git ${command} timed out after ${seconds}s`, {
|
|
54
|
+
cause: { exit_code: TIMEOUT_EXIT_CODE },
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface RemoteOptions {
|
|
59
|
+
stdio?: 'inherit'
|
|
60
|
+
timeout: number
|
|
61
|
+
env: Record<string, string>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// One remote call, bounded and kept alive. execa kills git on the budget; the ssh keepalive is what
|
|
65
|
+
// makes ssh — which git does not kill with itself — exit on a dead connection as well.
|
|
66
|
+
async function spawn_remote(arguments_: Array<string>, options: RemoteOptions): Promise<string> {
|
|
67
|
+
const git_cmd = git_utilities.get_git_command_for_spawn()
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// execa runs the binary directly with an argument array and no `shell` option, so CLI
|
|
71
|
+
// args cannot break out of a shell sandbox; the git command and args are internally
|
|
72
|
+
// controlled, never untrusted input. tssecurity:S8705 is a false positive here.
|
|
73
|
+
const { stdout } = await execa(git_cmd, arguments_, options) // NOSONAR
|
|
74
|
+
|
|
75
|
+
return typeof stdout === 'string' ? stdout.trimEnd() : ''
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw to_remote_error(arguments_[0] ?? '', error, options.timeout)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// `read` for a call that reaches the remote. The budget is a parameter only so a test can hold it to
|
|
82
|
+
// seconds; every production caller takes the default.
|
|
83
|
+
async function read_remote(
|
|
84
|
+
arguments_: Array<string>,
|
|
85
|
+
timeout_ms: number = REMOTE_TIMEOUT_MS,
|
|
86
|
+
): Promise<string> {
|
|
87
|
+
return await spawn_remote(arguments_, {
|
|
88
|
+
timeout: timeout_ms,
|
|
89
|
+
env: await git_ssh_keepalive.to_environment(),
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// `with_output` for a call that reaches the remote — the output stays on the terminal, as a pull's
|
|
94
|
+
// progress and a credential prompt need.
|
|
95
|
+
async function with_output_remote(command: string, arguments_list: Array<string>): Promise<void> {
|
|
96
|
+
await spawn_remote([command, ...arguments_list], {
|
|
97
|
+
stdio: 'inherit',
|
|
98
|
+
timeout: REMOTE_TIMEOUT_MS,
|
|
99
|
+
env: await git_ssh_keepalive.to_environment(),
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
29
103
|
async function with_output(command: string, arguments_list: Array<string>): Promise<void> {
|
|
30
104
|
const git_command_bin = git_utilities.get_git_command_for_spawn()
|
|
31
105
|
|
|
@@ -41,7 +115,9 @@ async function with_output(command: string, arguments_list: Array<string>): Prom
|
|
|
41
115
|
|
|
42
116
|
const git_spawn = {
|
|
43
117
|
read,
|
|
118
|
+
read_remote,
|
|
44
119
|
with_output,
|
|
120
|
+
with_output_remote,
|
|
45
121
|
}
|
|
46
122
|
|
|
47
123
|
export { git_spawn }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import { git_utilities } from './constants'
|
|
3
|
+
|
|
4
|
+
// The ssh environment every git network call runs under (joshuafolkken/kit#2942). It began inside
|
|
5
|
+
// `git-push-transport.ts` as the push's own (joshuafolkken/kit#1251), and moved here when the fetch
|
|
6
|
+
// and pull of `josh main:sync` turned out to need the same thing: a dead connection under a `git
|
|
7
|
+
// fetch` was waited on for 37 minutes, stalling the unattended backlog driver behind it.
|
|
8
|
+
//
|
|
9
|
+
// SSH's own liveness probe: three unanswered checks 15 seconds apart, so a connection that dies
|
|
10
|
+
// mid-transfer is noticed in about 45 seconds instead of waiting on the TCP default. The caller's
|
|
11
|
+
// timeout bounds the damage either way; this is what makes the common case fail in seconds rather
|
|
12
|
+
// than sit out the whole budget — and, because ssh exits on its own, it is also what leaves no ssh
|
|
13
|
+
// behind: git does not kill its transport child when it is itself killed on a timeout.
|
|
14
|
+
const SSH_KEEPALIVE_OPTIONS = '-o ServerAliveInterval=15 -o ServerAliveCountMax=3'
|
|
15
|
+
const SSH_COMMAND_VARIABLE = 'GIT_SSH_COMMAND'
|
|
16
|
+
const SSH_LEGACY_VARIABLE = 'GIT_SSH'
|
|
17
|
+
const KEEPALIVE_SSH_COMMAND = `ssh ${SSH_KEEPALIVE_OPTIONS}`
|
|
18
|
+
const SSH_COMMAND_CONFIG_KEY = 'core.sshCommand'
|
|
19
|
+
|
|
20
|
+
// Whatever already decides how ssh is invoked, or an empty string when nothing does. git reads three
|
|
21
|
+
// sources in this order — `GIT_SSH_COMMAND`, `core.sshCommand`, then the legacy `GIT_SSH` — and the
|
|
22
|
+
// last one has to be asked as well precisely because it loses: setting `GIT_SSH_COMMAND` on top of a
|
|
23
|
+
// `GIT_SSH=plink.exe` outranks it, so git would run an ssh binary the user did not choose.
|
|
24
|
+
function to_environment_value(name: string): string {
|
|
25
|
+
return process.env[name]?.trim() ?? ''
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function to_configured_ssh_command(): string {
|
|
29
|
+
const from_command = to_environment_value(SSH_COMMAND_VARIABLE)
|
|
30
|
+
|
|
31
|
+
return from_command === '' ? to_environment_value(SSH_LEGACY_VARIABLE) : from_command
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Whether any of those three sources answered. **A hit means hands off entirely.**
|
|
35
|
+
//
|
|
36
|
+
// Appending the options to a command someone else chose is not safe, because they are OpenSSH's:
|
|
37
|
+
// `plink` and `TortoiseGitPlink` are supported ssh commands on the platform `get_git_command_for_spawn`
|
|
38
|
+
// goes out of its way to handle, and a wrapper script with a fixed argument list is common on any
|
|
39
|
+
// platform — each of them exits on a usage error rather than connecting. Setting the variable while
|
|
40
|
+
// `core.sshCommand` is configured is the same mistake from the other side: the environment wins, so
|
|
41
|
+
// a per-repository key would be silently replaced by a plain `ssh`.
|
|
42
|
+
//
|
|
43
|
+
// What those users lose is the keepalive, not the fix: the caller's timeout still bounds the call,
|
|
44
|
+
// and `ServerAliveInterval` belongs in their own ssh config where it applies to every tool they run.
|
|
45
|
+
async function has_configured_ssh_command(): Promise<boolean> {
|
|
46
|
+
if (to_configured_ssh_command() !== '') return true
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const git_command_bin = git_utilities.get_git_command_for_spawn()
|
|
50
|
+
// execa runs the binary directly with an argument array and no `shell` option, so CLI
|
|
51
|
+
// args cannot break out of a shell sandbox; the git command and args are internally
|
|
52
|
+
// controlled, never untrusted input. tssecurity:S8705 is a false positive here.
|
|
53
|
+
const { stdout } = await execa(git_command_bin, ['config', '--get', SSH_COMMAND_CONFIG_KEY]) // NOSONAR
|
|
54
|
+
|
|
55
|
+
return stdout.trim() !== ''
|
|
56
|
+
} catch {
|
|
57
|
+
// `git config --get` exits 1 when the key is unset, which is the answer rather than a failure.
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The `env` a git network spawn passes to execa: the keepalive command, or nothing at all.
|
|
63
|
+
async function to_environment(): Promise<Record<string, string>> {
|
|
64
|
+
const should_keep_alive = !(await has_configured_ssh_command())
|
|
65
|
+
|
|
66
|
+
return should_keep_alive ? { [SSH_COMMAND_VARIABLE]: KEEPALIVE_SSH_COMMAND } : {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const git_ssh_keepalive = {
|
|
70
|
+
to_environment,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { git_ssh_keepalive }
|
|
74
|
+
export { KEEPALIVE_SSH_COMMAND, SSH_COMMAND_VARIABLE, SSH_LEGACY_VARIABLE }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
LEGACY_OBSERVATION_LEDGER_PATHS,
|
|
2
3
|
observation_ledger,
|
|
3
|
-
|
|
4
|
+
OBSERVATION_LEDGER_DIRECTORY,
|
|
4
5
|
} from '#scripts/observations/observation-ledger'
|
|
5
6
|
import { observation_ledger_prepare } from '#scripts/observations/observation-ledger-prepare'
|
|
6
7
|
import { git_command } from './git-command'
|
|
@@ -77,17 +78,28 @@ async function stage_untracked_files(files: ReadonlyArray<string>): Promise<void
|
|
|
77
78
|
// **A line that breaks the grammar keeps the old exclusion** (joshuafolkken/kit#2123): the commit
|
|
78
79
|
// goes ahead without the ledger, and `pnpm josh observations:flush` — which refuses the same line and
|
|
79
80
|
// names it — is where it is repaired. The migration is run first, as the flush runs it, so a line
|
|
80
|
-
// still on
|
|
81
|
-
//
|
|
81
|
+
// still on an old path (joshuafolkken/kit#2724) is committed in the directory. **A lane carries its
|
|
82
|
+
// own lines** (joshuafolkken/kit#2919): its writers append to its own tree's issue file, so this is
|
|
83
|
+
// the step that takes them to the default branch.
|
|
84
|
+
const LEDGER_PATHSPECS: ReadonlyArray<string> = [
|
|
85
|
+
OBSERVATION_LEDGER_DIRECTORY,
|
|
86
|
+
...LEGACY_OBSERVATION_LEDGER_PATHS,
|
|
87
|
+
]
|
|
88
|
+
|
|
82
89
|
async function ledger_exclusions(): Promise<ReadonlyArray<string>> {
|
|
83
90
|
const broken = await observation_ledger_prepare.prepare(await git_command.repository_root())
|
|
84
91
|
|
|
85
|
-
return broken.length > 0 ?
|
|
92
|
+
return broken.length > 0 ? LEDGER_PATHSPECS : []
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// An excluded pathspec covers what sits under it, as git reads it — the directory's files included.
|
|
96
|
+
function is_excluded(file_path: string, excluded: ReadonlyArray<string>): boolean {
|
|
97
|
+
return excluded.some((spec) => file_path === spec || file_path.startsWith(`${spec}/`))
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
// A migration claim is a copy mid-move, never the ledger itself, so it is never staged.
|
|
89
101
|
function is_stageable(file_path: string, excluded: ReadonlyArray<string>): boolean {
|
|
90
|
-
return !observation_ledger.is_migration_claim(file_path) && !
|
|
102
|
+
return !observation_ledger.is_migration_claim(file_path) && !is_excluded(file_path, excluded)
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
// **Said out loud, because the alternative is an unexplained failure.** With the ledger the only
|
|
@@ -95,15 +107,15 @@ function is_stageable(file_path: string, excluded: ReadonlyArray<string>): boole
|
|
|
95
107
|
// step dies on git's empty index with `Failed to commit changes` — a message naming nothing that
|
|
96
108
|
// caused it. This line is what turns that into a diagnosis.
|
|
97
109
|
//
|
|
98
|
-
// **The paths are parsed rather than matched as substrings**: `docs/maintainers/observations.md
|
|
99
|
-
// ledger's path, and a hint naming a file the exclusion never touched is a hint that
|
|
100
|
-
// reader to ignore it.
|
|
110
|
+
// **The paths are parsed rather than matched as substrings**: `docs/maintainers/observations-old/x.md`
|
|
111
|
+
// contains the ledger's path, and a hint naming a file the exclusion never touched is a hint that
|
|
112
|
+
// teaches the reader to ignore it.
|
|
101
113
|
function report_excluded_paths(status_output: string, excluded: ReadonlyArray<string>): void {
|
|
102
114
|
const left_out = status_output
|
|
103
115
|
.split('\n')
|
|
104
116
|
.filter((line) => line.trim().length > 0)
|
|
105
117
|
.map((line) => observation_ledger.status_path(line))
|
|
106
|
-
.filter((file_path) =>
|
|
118
|
+
.filter((file_path) => is_excluded(file_path, excluded))
|
|
107
119
|
|
|
108
120
|
for (const file_path of left_out) {
|
|
109
121
|
console.info(
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { parseArgs } from 'node:util'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
3
|
+
import { cli_body } from '#scripts/josh/cli-body'
|
|
4
|
+
import { test_red_commit } from '#scripts/test/test-red-commit'
|
|
5
|
+
import { git_branch } from './git-branch'
|
|
6
|
+
import { git_commit } from './git-commit'
|
|
7
|
+
import { git_error } from './git-error'
|
|
8
|
+
import { git_issue, type IssueInfo } from './git-issue'
|
|
9
|
+
import { git_pr } from './git-pr'
|
|
10
|
+
import { git_preflight } from './git-preflight'
|
|
11
|
+
import { git_prompt, type WorkflowConfirmations } from './git-prompt'
|
|
12
|
+
import { git_push } from './git-push'
|
|
13
|
+
import { git_staging } from './git-staging'
|
|
14
14
|
|
|
15
15
|
const SKIP_MESSAGES = {
|
|
16
16
|
commit: '💡 Commit skipped.',
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// notification carrying only a pull-request URL until joshuafolkken/kit#994 added the second form
|
|
7
7
|
// here rather than beside it.
|
|
8
8
|
//
|
|
9
|
-
// Two other modules read a URL for something narrower and keep their own patterns. `
|
|
9
|
+
// Two other modules read a URL for something narrower and keep their own patterns. `epic-parse`
|
|
10
10
|
// scans an epic body for task-list rows, so its regex is anchored to the row marker and runs
|
|
11
11
|
// globally over a document; `propagate-steps` takes the trailing number of whatever URL
|
|
12
12
|
// `gh issue create` just printed, where the repository is already known. This one is for a caller
|
|
@@ -100,9 +100,10 @@ async function delete_each(names: ReadonlyArray<string>): Promise<PruneResult> {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// `fetch --prune` first, because `[gone]` is read from the remote-tracking refs and those outlive the
|
|
103
|
-
// remote branch until something prunes them.
|
|
103
|
+
// remote branch until something prunes them. The fetch is the one call here that reaches the remote,
|
|
104
|
+
// so it is the one bounded (joshuafolkken/kit#2942).
|
|
104
105
|
async function prune(default_branch: string): Promise<PruneResult> {
|
|
105
|
-
await git_spawn.
|
|
106
|
+
await git_spawn.read_remote(['fetch', '--prune'])
|
|
106
107
|
|
|
107
108
|
return await delete_each(await read_candidates(default_branch))
|
|
108
109
|
}
|