@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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { core_budget } from '#scripts/gate/core-budget'
|
|
4
|
+
import {
|
|
5
|
+
CSPELL_COMMIT_CACHE_FLAGS,
|
|
6
|
+
ESLINT_COMMIT_CACHE_FLAGS,
|
|
7
|
+
PRETTIER_CACHE_FLAGS,
|
|
8
|
+
} from '#scripts/josh/josh-command-types'
|
|
9
|
+
import { resolve_spawn_exit } from '#scripts/lib/spawn-exit'
|
|
10
|
+
import { execa } from 'execa'
|
|
11
|
+
|
|
12
|
+
// The pre-commit hook's cspell, prettier and eslint runs over the staged files. They go through here
|
|
13
|
+
// rather than a bare `pnpm exec` in `lefthook/base.yml` so each cache location comes from the
|
|
14
|
+
// constant its ignore rules are asserted against (joshuafolkken/kit#3948) — a YAML copy would keep
|
|
15
|
+
// writing the old file the day that constant moves.
|
|
16
|
+
//
|
|
17
|
+
// cspell and eslint write the hook's own files: a staged-files cspell run prunes every entry it did
|
|
18
|
+
// not visit, and `--quiet` stores eslint reports without their warnings. Prettier does neither, so
|
|
19
|
+
// it shares the gate's file and a commit right after a green gate checks nothing twice.
|
|
20
|
+
|
|
21
|
+
const ARGV_OFFSET = 2
|
|
22
|
+
const USAGE_ERROR_EXIT_CODE = 2
|
|
23
|
+
const PNPM = 'pnpm'
|
|
24
|
+
|
|
25
|
+
const TOOL_ARGUMENTS: Readonly<Record<string, ReadonlyArray<string>>> = {
|
|
26
|
+
cspell: [
|
|
27
|
+
'exec',
|
|
28
|
+
'cspell',
|
|
29
|
+
'lint',
|
|
30
|
+
'--no-must-find-files',
|
|
31
|
+
'--no-progress',
|
|
32
|
+
...CSPELL_COMMIT_CACHE_FLAGS,
|
|
33
|
+
],
|
|
34
|
+
prettier: ['exec', 'prettier', '--check', '--ignore-unknown', ...PRETTIER_CACHE_FLAGS],
|
|
35
|
+
eslint: ['exec', 'eslint', '--quiet', ...ESLINT_COMMIT_CACHE_FLAGS],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// **Each tool claims its place in the core budget** (joshuafolkken/kit#3944): lefthook starts the three
|
|
39
|
+
// in parallel, and off the ledger every other lane's admission saw them only as foreign load. Each is
|
|
40
|
+
// one process over the staged files — eslint over 40 of them took 16.9s of CPU over 14.0s of wall at
|
|
41
|
+
// 1,225 MB, the one-process shape the whole-tree scan's weight already names.
|
|
42
|
+
// The key is the one `CORE_WEIGHTS` and `MEMORY_MB` both declare the tool under.
|
|
43
|
+
const TOOL_BUDGETS: Readonly<Record<string, 'spell_check' | 'format' | 'eslint_scan'>> = {
|
|
44
|
+
cspell: 'spell_check',
|
|
45
|
+
prettier: 'format',
|
|
46
|
+
eslint: 'eslint_scan',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const USAGE = `Usage: josh pre-commit-lint <${Object.keys(TOOL_ARGUMENTS).join('|')}> [files...]`
|
|
50
|
+
|
|
51
|
+
function build_arguments(
|
|
52
|
+
tool: string,
|
|
53
|
+
staged_files: ReadonlyArray<string>,
|
|
54
|
+
): ReadonlyArray<string> | undefined {
|
|
55
|
+
const tool_arguments = TOOL_ARGUMENTS[tool]
|
|
56
|
+
if (tool_arguments === undefined) return undefined
|
|
57
|
+
|
|
58
|
+
return [...tool_arguments, ...staged_files]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function run_tool(command_arguments: ReadonlyArray<string>): Promise<number> {
|
|
62
|
+
const result = await execa(PNPM, command_arguments, { stdio: 'inherit', reject: false })
|
|
63
|
+
|
|
64
|
+
return resolve_spawn_exit(PNPM, result)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
68
|
+
const [tool = '', ...staged_files] = argv
|
|
69
|
+
const command_arguments = build_arguments(tool, staged_files)
|
|
70
|
+
const budget = TOOL_BUDGETS[tool]
|
|
71
|
+
|
|
72
|
+
if (command_arguments === undefined || budget === undefined) {
|
|
73
|
+
console.error(USAGE)
|
|
74
|
+
|
|
75
|
+
return USAGE_ERROR_EXIT_CODE
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return await core_budget.with_command_reservation(
|
|
79
|
+
core_budget.CORE_WEIGHTS[budget],
|
|
80
|
+
async () => await run_tool(command_arguments),
|
|
81
|
+
{ memory_mb: core_budget.MEMORY_MB[budget], command: `pre-commit ${tool}` },
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
86
|
+
process.exitCode = await run(process.argv.slice(ARGV_OFFSET))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const pre_commit_lint = { build_arguments, run, USAGE_ERROR_EXIT_CODE }
|
|
90
|
+
|
|
91
|
+
export { pre_commit_lint }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { core_budget } from '#scripts/gate/core-budget'
|
|
4
|
+
import { gate_plan } from '#scripts/gate/gate-plan'
|
|
3
5
|
import { gate_skip, type GateReuse } from '#scripts/gate/gate-skip'
|
|
4
6
|
import { gate_tree, type GateTree } from '#scripts/gate/gate-tree'
|
|
5
7
|
import { hook_gate_reuse } from '#scripts/gate/hook-gate-reuse'
|
|
@@ -123,6 +125,18 @@ function format_miss(reason: string): string {
|
|
|
123
125
|
return `↻ running the whole unit suite — the green gate record does not cover this push: ${reason}.`
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
// **The whole suite claims its place in the core budget, the one `josh test:unit` claims.** Without it
|
|
129
|
+
// a push's suite ran beside other lanes' gates off the ledger, so their admission saw its workers only
|
|
130
|
+
// as foreign load (joshuafolkken/kit#3874). Only the miss reserves: a reused record runs nothing, and a
|
|
131
|
+
// claim there could make a push wait out a full ledger for a suite it then skips.
|
|
132
|
+
async function run_reserved_suite(vitest_arguments: ReadonlyArray<string>): Promise<number> {
|
|
133
|
+
return await core_budget.with_command_reservation(
|
|
134
|
+
gate_plan.direct_unit_weight(),
|
|
135
|
+
async () => await test_unit_guard.run_guarded_unit(process.cwd(), vitest_arguments),
|
|
136
|
+
{ memory_mb: gate_plan.direct_unit_memory(), command: 'pre-push test:unit' },
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
126
140
|
async function run_pre_push_unit(
|
|
127
141
|
extra_arguments: ReadonlyArray<string> = [],
|
|
128
142
|
source?: string,
|
|
@@ -132,10 +146,7 @@ async function run_pre_push_unit(
|
|
|
132
146
|
if ('miss' in reuse) {
|
|
133
147
|
process.stdout.write(`${format_miss(reuse.miss)}\n`)
|
|
134
148
|
|
|
135
|
-
return await
|
|
136
|
-
process.cwd(),
|
|
137
|
-
forwarded_arguments(extra_arguments),
|
|
138
|
-
)
|
|
149
|
+
return await run_reserved_suite(forwarded_arguments(extra_arguments))
|
|
139
150
|
}
|
|
140
151
|
|
|
141
152
|
process.stdout.write(`${format_skip(reuse.stamp.taken_at)}\n`)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { duplicate_read_outcome } from '#scripts/delegation/duplicate-read-guard'
|
|
2
|
-
import {
|
|
2
|
+
import { investigation_outcome } from '#scripts/delegation/investigation-guard'
|
|
3
3
|
import { issue_watch_hook } from '#scripts/issue/issue-watch-hook'
|
|
4
4
|
import { hook_decision, type GuardOutcome } from '#scripts/josh/hook-decision'
|
|
5
5
|
import { lane_phase } from '#scripts/lane/lane-phase'
|
|
6
|
-
import { json_value } from '#scripts/lib/json-value'
|
|
7
6
|
import { delivered_rules } from '#scripts/rules/delivered-rules'
|
|
8
7
|
import { lane_background } from '#scripts/rules/lane-background'
|
|
9
|
-
import {
|
|
8
|
+
import { rule_rewrite, type RuleRewrite } from '#scripts/rules/rule-rewrite'
|
|
10
9
|
import { run_parent_cut_hook } from '#scripts/run/run-parent-cut-hook'
|
|
10
|
+
import { run_poll_hook } from '#scripts/run/run-poll-hook'
|
|
11
11
|
import { run_watcher_hook } from '#scripts/run/run-watcher-hook'
|
|
12
12
|
import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
|
|
13
13
|
import { batch_outcome } from './batch-guard'
|
|
@@ -22,38 +22,34 @@ import { step_zero_notice } from './step-zero-notice'
|
|
|
22
22
|
//
|
|
23
23
|
// **The verdict is identical to running the three in turn**, and that is the whole point. Each guard
|
|
24
24
|
// self-gates on the tool name inside its own `is_candidate` / trigger, so running all three on the
|
|
25
|
-
// union matcher (`Bash|Edit|Read|Write|AskUserQuestion`) refuses exactly what the three
|
|
26
|
-
// entries did — `AskUserQuestion` is matched for the rule guard's lane-child interactive-ask
|
|
27
|
-
// and the batching and investigation guards self-gate
|
|
28
|
-
// refusal from any guard wins first; only when none refuses is
|
|
29
|
-
// (the whole-file-write notice
|
|
25
|
+
// union matcher (`Bash|Edit|Read|Write|AskUserQuestion|Agent|Task`) refuses exactly what the three
|
|
26
|
+
// separate entries did — `AskUserQuestion` is matched for the rule guard's lane-child interactive-ask
|
|
27
|
+
// row and `Agent|Task` for its `agent-model` row, and the batching and investigation guards self-gate
|
|
28
|
+
// away from both. A refusal from any guard wins first; only when none refuses is a guard's non-blocking
|
|
29
|
+
// notice or fault (the whole-file-write notice, a lane child's duplicate-read notice) emitted. Each
|
|
30
|
+
// guard still honours its own
|
|
30
31
|
// switch (`JOSH_BATCH_GUARD` / `JOSH_INVESTIGATION_GUARD` / `JOSH_RULE_GUARD`) internally, so this
|
|
31
32
|
// process needs no switch of its own.
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
// reason is shown first to match the entry order the separate hooks had. When nothing refuses,
|
|
35
|
-
// whatever the batch guard reported (a notice, a fault, or nothing) passes through untouched.
|
|
36
|
-
function combine_outcomes(batch: GuardOutcome, extra_reason: string | undefined): GuardOutcome {
|
|
37
|
-
if (batch.reason !== undefined) return batch
|
|
38
|
-
|
|
39
|
-
if (extra_reason !== undefined) {
|
|
40
|
-
return { reason: extra_reason, notice: undefined, fault: undefined }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return batch
|
|
44
|
-
}
|
|
34
|
+
const CLEAR: GuardOutcome = { reason: undefined, notice: undefined, fault: undefined }
|
|
45
35
|
|
|
46
36
|
function is_clear(outcome: GuardOutcome): boolean {
|
|
47
37
|
return outcome.reason === undefined && outcome.notice === undefined && outcome.fault === undefined
|
|
48
38
|
}
|
|
49
39
|
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
// Fold the guards' verdicts into one. A refusal wins over everything else, in the entry order the
|
|
41
|
+
// separate hooks had: the outcomes in the order given, then the rule guard's reason. When nothing
|
|
42
|
+
// refuses, the first guard that spoke passes its notice or fault through untouched — so a lane child's
|
|
43
|
+
// `notice`-mode guidance reaches the model rather than only spending its stamp (joshuafolkken/kit#3877).
|
|
44
|
+
function combine_outcomes(
|
|
45
|
+
outcomes: ReadonlyArray<GuardOutcome>,
|
|
46
|
+
rule_reason: string | undefined,
|
|
47
|
+
): GuardOutcome {
|
|
48
|
+
const reason = outcomes.find((outcome) => outcome.reason !== undefined)?.reason ?? rule_reason
|
|
49
|
+
|
|
50
|
+
if (reason !== undefined) return { reason, notice: undefined, fault: undefined }
|
|
55
51
|
|
|
56
|
-
return
|
|
52
|
+
return outcomes.find((outcome) => !is_clear(outcome)) ?? CLEAR
|
|
57
53
|
}
|
|
58
54
|
|
|
59
55
|
// The Step 0 reminder is the last word: asked only where every guard stayed
|
|
@@ -70,14 +66,13 @@ function with_step_zero_notice(outcome: GuardOutcome, raw_payload: string): Guar
|
|
|
70
66
|
// once-per-run stamp, so short-circuiting on the first refusal would change which guard is allowed to
|
|
71
67
|
// fire on a later call.
|
|
72
68
|
function guard_outcome(raw_payload: string): GuardOutcome {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return with_duplicate_notice(combined, duplicate.notice)
|
|
69
|
+
const outcomes = [
|
|
70
|
+
batch_outcome(raw_payload),
|
|
71
|
+
investigation_outcome(raw_payload),
|
|
72
|
+
duplicate_read_outcome(raw_payload),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
return combine_outcomes(outcomes, delivered_rules.delivery(raw_payload))
|
|
81
76
|
}
|
|
82
77
|
|
|
83
78
|
// The synchronous verdict the Codex adapter asks for, with the Step 0 reminder as its last layer.
|
|
@@ -102,12 +97,14 @@ async function mark_phase(outcome: GuardOutcome): Promise<void> {
|
|
|
102
97
|
// **The parent hand-off guard is the second asynchronous rule, asked after the watcher's**.
|
|
103
98
|
// It reads the carry record to tell the `backlogrun` parent apart, so it sits
|
|
104
99
|
// beside the watcher rather than among the synchronous rows; a stale watcher is the cheaper fix and is
|
|
105
|
-
// surfaced first.
|
|
100
|
+
// surfaced first. **The poll guard is third**: it reads the same carry record, and only for a call that
|
|
101
|
+
// repeats a state read, so the hand-off still wins over it.
|
|
106
102
|
async function async_reason(raw_payload: string): Promise<string | undefined> {
|
|
107
103
|
const watcher = await run_watcher_hook.watcher_hook_reason(raw_payload)
|
|
108
104
|
const cut = watcher ?? (await run_parent_cut_hook.parent_cut_reason(raw_payload))
|
|
105
|
+
const poll = cut ?? (await run_poll_hook.safe_poll_reason(raw_payload))
|
|
109
106
|
|
|
110
|
-
return
|
|
107
|
+
return poll ?? (await issue_watch_hook.watch_reason())
|
|
111
108
|
}
|
|
112
109
|
|
|
113
110
|
async function pretool_outcome_async(raw_payload: string): Promise<GuardOutcome> {
|
|
@@ -165,59 +162,32 @@ function foreground_rewrite(
|
|
|
165
162
|
)
|
|
166
163
|
}
|
|
167
164
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
input: Record<string, unknown>
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// A piped josh check the hook runs under `set -o pipefail` instead of refusing it:
|
|
174
|
-
// the rewritten input, and the payload that carries it. The rewrite is the
|
|
175
|
-
// rule guard's delivery, so the rule guard's switch turns it off too.
|
|
176
|
-
function pipefail_rewrite(raw_payload: string): PipefailRewrite | undefined {
|
|
177
|
-
if (!delivered_rules.is_enabled()) return undefined
|
|
178
|
-
|
|
179
|
-
const call = guarded_call(raw_payload)
|
|
180
|
-
const input = call === undefined ? undefined : piped_verification.pipefail_input(call)
|
|
181
|
-
const payload = json_value.parse_or_undefined(raw_payload)
|
|
182
|
-
|
|
183
|
-
if (input === undefined || !json_value.is_record(payload)) return undefined
|
|
184
|
-
|
|
185
|
-
return { payload: JSON.stringify({ ...payload, tool_input: input }), input }
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function pipefail_envelope(
|
|
165
|
+
// The rows' rewrite carries no permission decision: whatever else the line runs meets its own prompt.
|
|
166
|
+
function rule_envelope(
|
|
189
167
|
outcome: GuardOutcome,
|
|
190
|
-
|
|
168
|
+
rewrite: RuleRewrite | undefined,
|
|
191
169
|
): string | undefined {
|
|
192
|
-
if (
|
|
170
|
+
if (rewrite === undefined || outcome.reason !== undefined) return undefined
|
|
193
171
|
|
|
194
|
-
return hook_decision.
|
|
195
|
-
input,
|
|
196
|
-
rewrite_context(piped_verification.PIPEFAIL_NOTE, outcome),
|
|
197
|
-
)
|
|
172
|
+
return hook_decision.flow_rewrite_envelope(rewrite.input, rewrite_context(rewrite.note, outcome))
|
|
198
173
|
}
|
|
199
174
|
|
|
200
175
|
// The hook's one write: a rewrite where one applies, the outcome's own envelope otherwise.
|
|
201
|
-
function emit(
|
|
202
|
-
|
|
203
|
-
outcome: GuardOutcome,
|
|
204
|
-
pipefail_input?: Record<string, unknown>,
|
|
205
|
-
): void {
|
|
206
|
-
const rewrite =
|
|
207
|
-
pipefail_envelope(outcome, pipefail_input) ?? foreground_rewrite(outcome, raw_payload)
|
|
176
|
+
function emit(raw_payload: string, outcome: GuardOutcome, rewrite?: RuleRewrite): void {
|
|
177
|
+
const envelope = rule_envelope(outcome, rewrite) ?? foreground_rewrite(outcome, raw_payload)
|
|
208
178
|
|
|
209
|
-
if (
|
|
210
|
-
else process.stdout.write(`${
|
|
179
|
+
if (envelope === undefined) hook_decision.emit_outcome(outcome)
|
|
180
|
+
else process.stdout.write(`${envelope}\n`)
|
|
211
181
|
}
|
|
212
182
|
|
|
213
|
-
// **The guards judge the call that will run.** A
|
|
214
|
-
//
|
|
215
|
-
//
|
|
183
|
+
// **The guards judge the call that will run.** A rewritten call reaches every guard already made
|
|
184
|
+
// right, so the rewriting row stays quiet on it while any other guard's refusal still wins over the
|
|
185
|
+
// rewrite. The Codex adapter cannot rewrite a call, so it goes on judging the call as typed.
|
|
216
186
|
async function respond(raw_payload: string): Promise<void> {
|
|
217
|
-
const
|
|
218
|
-
const judged =
|
|
187
|
+
const rewrite = rule_rewrite.rewrite(raw_payload)
|
|
188
|
+
const judged = rewrite?.payload ?? raw_payload
|
|
219
189
|
|
|
220
|
-
emit(judged, await pretool_outcome_async(judged),
|
|
190
|
+
emit(judged, await pretool_outcome_async(judged), rewrite)
|
|
221
191
|
}
|
|
222
192
|
|
|
223
193
|
const pretool_guard = {
|
|
@@ -225,7 +195,6 @@ const pretool_guard = {
|
|
|
225
195
|
emit,
|
|
226
196
|
foreground_rewrite,
|
|
227
197
|
mark_phase,
|
|
228
|
-
pipefail_rewrite,
|
|
229
198
|
pretool_outcome,
|
|
230
199
|
respond,
|
|
231
200
|
}
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
# A hook command in `.claude/settings.json` / `.codex/hooks.json` only moves to the project root
|
|
6
6
|
# (`scripts/init/hook-launch.ts`) and calls this script, so everything every hook shares lives here once:
|
|
7
7
|
#
|
|
8
|
-
# - The git location variables a git hook exports
|
|
9
|
-
#
|
|
10
|
-
# them — git metadata outside the work tree.
|
|
8
|
+
# - The root prefix has already cleared the git location variables a git hook exports, or kept them
|
|
9
|
+
# when the checkout needs them; this script starts no `git` of its own (joshuafolkken/kit#3946).
|
|
11
10
|
# - The pre-built bundle `dist/hooks/<name>.js` runs once its gate passes; otherwise the live source
|
|
12
11
|
# runs through josh, whose command is the name with its first `-` turned into `:`
|
|
13
12
|
# (`pretool-guard` → `pretool:guard`).
|
|
@@ -16,27 +15,26 @@
|
|
|
16
15
|
# starts node once (joshuafolkken/kit#3327). In an installed package it is a presence check: the
|
|
17
16
|
# published bundles cannot be stale against a source the consumer never edits, and Node strips no
|
|
18
17
|
# types under `node_modules`.
|
|
18
|
+
# - Nothing is spawned that the shell can do itself: the directory is a parameter expansion rather than
|
|
19
|
+
# `dirname`, and `node` replaces this shell through `exec` rather than running beside it.
|
|
19
20
|
# - An `if`/`else`, never `&&`/`||`: a guard's refusal is a non-zero exit, and a chain would read it as
|
|
20
21
|
# a missing bundle and run the hook twice.
|
|
21
22
|
|
|
22
|
-
location_variables='GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_COMMON_DIR GIT_DIR GIT_INDEX_FILE GIT_NAMESPACE GIT_OBJECT_DIRECTORY GIT_WORK_TREE'
|
|
23
|
-
unset_options=''
|
|
24
|
-
for variable in $location_variables; do unset_options="$unset_options -u $variable"; done
|
|
25
|
-
# shellcheck disable=SC2086
|
|
26
|
-
if env $unset_options git rev-parse --show-toplevel >/dev/null 2>&1; then unset $location_variables; fi
|
|
27
|
-
|
|
28
23
|
hook_name=$1
|
|
29
24
|
shift
|
|
30
|
-
|
|
25
|
+
case $0 in
|
|
26
|
+
*/*) hooks_dir=${0%/*} ;;
|
|
27
|
+
*) hooks_dir=. ;;
|
|
28
|
+
esac
|
|
31
29
|
package_dir="$hooks_dir/../.."
|
|
32
30
|
bundle="$package_dir/dist/hooks/$hook_name.js"
|
|
33
31
|
josh_command="${hook_name%%-*}:${hook_name#*-}"
|
|
34
32
|
|
|
35
33
|
case $0 in
|
|
36
34
|
*node_modules/*)
|
|
37
|
-
if [ -d "$package_dir/dist/hooks" ]; then node "$bundle" "$@"; else node "$package_dir/dist/josh.js" "$josh_command" "$@"; fi
|
|
35
|
+
if [ -d "$package_dir/dist/hooks" ]; then exec node "$bundle" "$@"; else exec node "$package_dir/dist/josh.js" "$josh_command" "$@"; fi
|
|
38
36
|
;;
|
|
39
37
|
*)
|
|
40
|
-
node --disable-warning=ExperimentalWarning "$hooks_dir/hook-bundle-ready.ts" "$hook_name" "$josh_command" "$@"
|
|
38
|
+
exec node --disable-warning=ExperimentalWarning "$hooks_dir/hook-bundle-ready.ts" "$hook_name" "$josh_command" "$@"
|
|
41
39
|
;;
|
|
42
40
|
esac
|
|
@@ -65,8 +65,9 @@ function notice(raw_payload: string, root: string = process.cwd()): string | und
|
|
|
65
65
|
|
|
66
66
|
const target = STAMP.path(transcript)
|
|
67
67
|
|
|
68
|
-
if (STAMP.last_ms(target) !== hook_decision.NEVER_MS)
|
|
69
|
-
|
|
68
|
+
if (STAMP.last_ms(target) !== hook_decision.NEVER_MS || !STAMP.record(target, Date.now())) {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
70
71
|
|
|
71
72
|
return NOTICE
|
|
72
73
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { git_gh_repo } from '#scripts/gh/git-gh-repo'
|
|
2
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
3
|
+
import { issue_cite_cli } from '#scripts/issue/issue-cite-cli'
|
|
4
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
5
|
+
import { bounded_pool } from '#scripts/lib/bounded-pool'
|
|
6
|
+
import { telegram_test } from '#scripts/notify/telegram-test'
|
|
7
|
+
import type { CliValues } from '#scripts/notify/telegram-test-logic'
|
|
8
|
+
import { issue_citation } from '#scripts/rules/issue-citation'
|
|
9
|
+
import { stop_rules, type StopAction, type StopOutcome } from '#scripts/rules/stop-rules'
|
|
10
|
+
import { run_hold } from '#scripts/run/hold/run-hold'
|
|
11
|
+
import { run_hold_cli } from '#scripts/run/hold/run-hold-cli'
|
|
12
|
+
import { run_hold_session } from '#scripts/run/hold/run-hold-session'
|
|
13
|
+
|
|
14
|
+
type NotifyAction = Extract<StopAction, { kind: 'notify' }>
|
|
15
|
+
|
|
16
|
+
// The `Stop` hook running the operations whose answer is fixed (joshuafolkken/kit#3919): the
|
|
17
|
+
// `confirmation` notify, the hold release and the citation lines, through the very calls
|
|
18
|
+
// `josh notify`, `josh run:release` and `josh issue:cite` make — in process, since three `pnpm`
|
|
19
|
+
// launches would spend most of the hook's 20-second budget. What each did reaches the person as a
|
|
20
|
+
// `systemMessage`; one that did not take falls back to the refusal it replaced (`stop_rules.fallback_reason`).
|
|
21
|
+
|
|
22
|
+
const SUCCESS_EXIT_CODE = 0
|
|
23
|
+
const NOTICE_SEPARATOR = '\n'
|
|
24
|
+
// The reply is the stop's reason, but a Telegram is read at a glance — its opening is enough.
|
|
25
|
+
const SUMMARY_CHARS = 600
|
|
26
|
+
// A `#N`, `owner/repo#N` or URL is a run of printable ASCII; a cut inside one would link the wrong Issue.
|
|
27
|
+
const TOKEN_CHAR = /^[!-~]$/u
|
|
28
|
+
const EMPTY_SUMMARY = 'The run stopped and is waiting for you.'
|
|
29
|
+
|
|
30
|
+
interface Settled {
|
|
31
|
+
notices: ReadonlyArray<string>
|
|
32
|
+
failure: string | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// One action at a time — the order is the rule order.
|
|
36
|
+
const SEQUENTIAL = 1
|
|
37
|
+
|
|
38
|
+
// Where the run of token characters `text` ends with starts.
|
|
39
|
+
function token_start(text: string): number {
|
|
40
|
+
let index = text.length
|
|
41
|
+
|
|
42
|
+
while (index > 0 && TOKEN_CHAR.test(text.charAt(index - 1))) index -= 1
|
|
43
|
+
|
|
44
|
+
return index
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Cut before linking, on a boundary that keeps every reference whole: the character past the cut shows
|
|
48
|
+
// whether the last token ran on, and a token that did is dropped rather than halved.
|
|
49
|
+
function summary_of(message: string): string {
|
|
50
|
+
const trimmed = message.trim()
|
|
51
|
+
if (trimmed === '') return EMPTY_SUMMARY
|
|
52
|
+
if (trimmed.length <= SUMMARY_CHARS) return trimmed
|
|
53
|
+
|
|
54
|
+
const overshoot = trimmed.slice(0, SUMMARY_CHARS + 1)
|
|
55
|
+
const cut = overshoot.slice(0, Math.min(token_start(overshoot), SUMMARY_CHARS))
|
|
56
|
+
|
|
57
|
+
return cut === '' ? trimmed.slice(0, SUMMARY_CHARS) : cut
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The held Issue's URL, or `undefined` for a `new` entry or a repository that cannot be read.
|
|
61
|
+
async function issue_url_of(issue: string): Promise<string | undefined> {
|
|
62
|
+
if (issue === run_hold.UNNUMBERED_ISSUE) return undefined
|
|
63
|
+
|
|
64
|
+
const slug = await git_gh_repo.repo_get_name_with_owner()
|
|
65
|
+
|
|
66
|
+
return slug === undefined ? undefined : issue_cite.issue_url(slug, issue)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The body is the reply's opening copied into a Telegram, so its bare `#N` are linked (`issue-citation.md`).
|
|
70
|
+
// The choices are the reply's own decision lines (joshuafolkken/kit#3961).
|
|
71
|
+
function notify_values(issue_url: string | undefined, action: NotifyAction): CliValues {
|
|
72
|
+
const { recommended, alternative } = action.decision
|
|
73
|
+
const values: CliValues = {
|
|
74
|
+
'task-type': 'confirmation',
|
|
75
|
+
body: session_cite.text(summary_of(action.message)),
|
|
76
|
+
recommended,
|
|
77
|
+
...(alternative !== undefined && { alternative: [alternative] }),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return issue_url === undefined ? values : { ...values, 'issue-url': issue_url }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The send is stamped on the hold, so a later stop of the same pause does not announce it again. A
|
|
84
|
+
// stamp that fails does not fail the send: the Telegram is out, and a refused stop would send it twice.
|
|
85
|
+
async function mark_notified(): Promise<void> {
|
|
86
|
+
try {
|
|
87
|
+
const directory = await run_hold.worktree_directory()
|
|
88
|
+
|
|
89
|
+
if (directory !== undefined) run_hold_session.mark_notified(run_hold.hold_path(directory))
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.warn(`stop:guard could not stamp the notify on the hold: ${String(error)}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function notify(action: NotifyAction): Promise<string> {
|
|
96
|
+
const issue_url = await issue_url_of(action.issue)
|
|
97
|
+
|
|
98
|
+
await telegram_test.send_values(notify_values(issue_url, action))
|
|
99
|
+
await mark_notified()
|
|
100
|
+
|
|
101
|
+
return `📱 stop:guard sent the confirmation notify (${issue_url ?? action.issue}).`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function release(issue: string): Promise<string | undefined> {
|
|
105
|
+
const argv = issue === run_hold.UNNUMBERED_ISSUE ? ['--release'] : ['--release', issue]
|
|
106
|
+
const code = await run_hold_cli.run(argv)
|
|
107
|
+
|
|
108
|
+
return code === SUCCESS_EXIT_CODE ? '🔓 stop:guard released the run:hold record.' : undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Every line or none: a citation with a number missing is the half-fix the refusal exists to avoid.
|
|
112
|
+
async function cite(references: ReadonlyArray<string>): Promise<string | undefined> {
|
|
113
|
+
const cited = await issue_cite_cli.cite(issue_citation.cite_arguments(references))
|
|
114
|
+
if (cited === undefined || cited.failures.length > 0) return undefined
|
|
115
|
+
|
|
116
|
+
return ['Issue citations:', ...cited.lines].join(NOTICE_SEPARATOR)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// An action's line for the person, or `undefined` when it did not take.
|
|
120
|
+
async function run_action(action: StopAction): Promise<string | undefined> {
|
|
121
|
+
if (action.kind === 'notify') return await notify(action)
|
|
122
|
+
if (action.kind === 'release') return await release(action.issue)
|
|
123
|
+
|
|
124
|
+
return await cite(action.references)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// **The hook's stdout is the decision envelope, so nothing else may reach it**: the reused calls report
|
|
128
|
+
// on stdout (`josh run:release`'s verdict token, the notify's sent line) and are sent to stderr instead.
|
|
129
|
+
type StreamWrite = typeof process.stdout.write
|
|
130
|
+
|
|
131
|
+
function set_stdout_write(stream_write: StreamWrite): void {
|
|
132
|
+
process.stdout.write = stream_write
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function on_stderr<T>(work: () => Promise<T>): Promise<T> {
|
|
136
|
+
const saved = { write: process.stdout.write.bind(process.stdout) }
|
|
137
|
+
|
|
138
|
+
set_stdout_write(process.stderr.write.bind(process.stderr))
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
return await work()
|
|
142
|
+
} finally {
|
|
143
|
+
set_stdout_write(saved.write)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// A throw is an action that did not take, the same as one that answered it had failed.
|
|
148
|
+
async function attempt(action: StopAction): Promise<string | undefined> {
|
|
149
|
+
try {
|
|
150
|
+
return await run_action(action)
|
|
151
|
+
} catch {
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function is_notice(notice: string | undefined): notice is string {
|
|
157
|
+
return notice !== undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// In order, one at a time: the notify reports the hold the release then ends. The first failure is the
|
|
161
|
+
// one refused with, as the rule order put it first.
|
|
162
|
+
async function settle(actions: ReadonlyArray<StopAction>): Promise<Settled> {
|
|
163
|
+
const notices = await on_stderr(
|
|
164
|
+
async () => await bounded_pool.bounded_map(actions, SEQUENTIAL, attempt),
|
|
165
|
+
)
|
|
166
|
+
const failed = actions.find((_action, index) => notices[index] === undefined)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
notices: notices.filter((notice) => is_notice(notice)),
|
|
170
|
+
failure: failed === undefined ? undefined : stop_rules.fallback_reason(failed),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function joined(notices: ReadonlyArray<string>): string | undefined {
|
|
175
|
+
return notices.length === 0 ? undefined : notices.join(NOTICE_SEPARATOR)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A failure refuses only where the outcome allows it — never past the loop-breaker.
|
|
179
|
+
function settled_envelope(
|
|
180
|
+
settled: Settled,
|
|
181
|
+
is_blocking: boolean,
|
|
182
|
+
lang: string,
|
|
183
|
+
): string | undefined {
|
|
184
|
+
const notice = joined(settled.notices)
|
|
185
|
+
|
|
186
|
+
if (is_blocking && settled.failure !== undefined) {
|
|
187
|
+
return stop_rules.block_envelope(settled.failure, lang, notice)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return notice === undefined ? undefined : stop_rules.notice_envelope(notice)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// What the hook writes for one stop's outcome, or `undefined` for a stop that writes nothing.
|
|
194
|
+
async function envelope_of(outcome: StopOutcome, lang: string): Promise<string | undefined> {
|
|
195
|
+
if (outcome.reason !== undefined) return stop_rules.block_envelope(outcome.reason, lang)
|
|
196
|
+
|
|
197
|
+
const settled = await settle(outcome.actions)
|
|
198
|
+
|
|
199
|
+
return settled_envelope(settled, outcome.blocks_on_failure, lang)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const stop_actions = { envelope_of, summary_of }
|
|
203
|
+
|
|
204
|
+
export { stop_actions }
|