@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,182 @@
|
|
|
1
|
+
import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
|
|
2
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
3
|
+
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
4
|
+
import { issue_state_cli } from '#scripts/issue/issue-state-cli'
|
|
5
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
6
|
+
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
7
|
+
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
8
|
+
import { run_label } from '#scripts/run/run-label'
|
|
9
|
+
import { lane_close, type CloseKind } from './lane-close'
|
|
10
|
+
import { lane_preserve, type KeptWork } from './lane-preserve'
|
|
11
|
+
import { lane_reap } from './lane-reap'
|
|
12
|
+
import { lane_registry, type LaneInfo } from './lane-registry'
|
|
13
|
+
|
|
14
|
+
// `josh lane:withdraw <N> --reason <text>` — a person takes a running lane child's issue out of the run
|
|
15
|
+
// for good: the child is stopped, its work stashed, the Issue closed as not planned and the lane closed.
|
|
16
|
+
//
|
|
17
|
+
// **The `withdraw` event is written first, before anything is stopped.** Killing the child alone would
|
|
18
|
+
// let `lane:await` book it stopped and `run:merge` classify it from GitHub — an open issue resumed as an
|
|
19
|
+
// `outage` or parked `needs-decision`. `run:merge` reads the event stream before GitHub, so a child whose
|
|
20
|
+
// newest launch-or-withdraw event is a withdraw is booked `withdrawn` whatever the Issue's state reads
|
|
21
|
+
// at that moment, and the race between the kill and the close cannot book it as anything else.
|
|
22
|
+
//
|
|
23
|
+
// **The issue is read again once the child is stopped.** It may merge between the first read and the
|
|
24
|
+
// reap; the reap stopped it, so the second read is final, and an issue it already closed keeps
|
|
25
|
+
// GitHub's close — `run:merge` books a merged close as merged whatever the event stream says.
|
|
26
|
+
//
|
|
27
|
+
// A tree that could not be stashed is left in place with the Issue open: closing the lane would delete
|
|
28
|
+
// the work, and the person decides what to keep.
|
|
29
|
+
|
|
30
|
+
const WITHDRAW_OCCASION = 'lane withdraw'
|
|
31
|
+
const WITHDRAW_CAUSE = 'a person withdrew it with `lane:withdraw`'
|
|
32
|
+
const CLOSED_STATE = 'CLOSED'
|
|
33
|
+
const CLOSED_KIND = 'closed'
|
|
34
|
+
const NO_LANE_KIND = 'no-lane'
|
|
35
|
+
const UNREADABLE_KIND = 'unreadable'
|
|
36
|
+
|
|
37
|
+
type Unsaved = Extract<KeptWork, { kind: 'unsaved' }>
|
|
38
|
+
|
|
39
|
+
interface Withdrawn {
|
|
40
|
+
kind: 'withdrawn'
|
|
41
|
+
// How many of the child's processes the withdrawal stopped — 0 for a child that had already ended.
|
|
42
|
+
reaped: number
|
|
43
|
+
stash: string | undefined
|
|
44
|
+
is_closed: boolean
|
|
45
|
+
// The issue had closed on its own — merged — before the withdrawal reached it, so it was left as is.
|
|
46
|
+
had_closed: boolean
|
|
47
|
+
lane: CloseKind
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type IssueSettled = Pick<Withdrawn, 'had_closed' | 'is_closed'>
|
|
51
|
+
|
|
52
|
+
type Refused =
|
|
53
|
+
{ kind: typeof CLOSED_KIND } | { kind: typeof NO_LANE_KIND } | { kind: typeof UNREADABLE_KIND }
|
|
54
|
+
type WithdrawOutcome = Withdrawn | Unsaved | Refused
|
|
55
|
+
|
|
56
|
+
function close_comment(reason: string): string {
|
|
57
|
+
return [
|
|
58
|
+
'Withdrawn by a person with `pnpm josh lane:withdraw`.',
|
|
59
|
+
'Its lane child was stopped and the lane closed.',
|
|
60
|
+
`- Why: ${reason}`,
|
|
61
|
+
].join('\n')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function record_withdraw(issue: string, reason: string): Promise<void> {
|
|
65
|
+
await run_event_stream_emit.emit(
|
|
66
|
+
run_event_stream.EVENT_KIND.WITHDRAW,
|
|
67
|
+
`${issue_cite.plain(issue)} withdrawn by a person: ${reason}`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A closed issue has nothing to withdraw, and an unread one might be: neither writes anything.
|
|
72
|
+
async function refusal_of(issue: string): Promise<Refused | undefined> {
|
|
73
|
+
const read = await issue_state_cli.read_issue(issue)
|
|
74
|
+
|
|
75
|
+
if (read.kind !== 'state') return { kind: UNREADABLE_KIND }
|
|
76
|
+
if (read.state.state.toUpperCase() === CLOSED_STATE) return { kind: CLOSED_KIND }
|
|
77
|
+
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function close_open_issue(issue: string, reason: string): Promise<IssueSettled> {
|
|
82
|
+
const refusal = await refusal_of(issue)
|
|
83
|
+
|
|
84
|
+
// An unread issue may have merged during the reap, so it is left open for a person to close.
|
|
85
|
+
if (refusal !== undefined) {
|
|
86
|
+
const is_closed = refusal.kind === CLOSED_KIND
|
|
87
|
+
|
|
88
|
+
return { is_closed, had_closed: is_closed }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const is_closed = await git_gh_issue_write.issue_close_not_planned(issue, close_comment(reason))
|
|
92
|
+
|
|
93
|
+
return { is_closed, had_closed: false }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function close_issue(
|
|
97
|
+
issue: string,
|
|
98
|
+
reason: string,
|
|
99
|
+
stash: string | undefined,
|
|
100
|
+
): Promise<IssueSettled> {
|
|
101
|
+
const settled = await close_open_issue(issue, reason)
|
|
102
|
+
|
|
103
|
+
if (stash !== undefined) await lane_preserve.announce(issue, stash, WITHDRAW_CAUSE)
|
|
104
|
+
await run_label.unmark(issue)
|
|
105
|
+
|
|
106
|
+
return settled
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function stop_and_close(lane: LaneInfo, reason: string): Promise<WithdrawOutcome> {
|
|
110
|
+
await record_withdraw(lane.issue, reason)
|
|
111
|
+
const reaped = lane_reap.reap_child(lane.issue).length
|
|
112
|
+
const saved = await lane_preserve.keep_work(lane.issue, lane.directory, WITHDRAW_OCCASION)
|
|
113
|
+
|
|
114
|
+
if (saved.kind === 'unsaved') return saved
|
|
115
|
+
const settled = await close_issue(lane.issue, reason, saved.stash)
|
|
116
|
+
const closed = await lane_close.close_lane(lane.issue)
|
|
117
|
+
|
|
118
|
+
return { kind: 'withdrawn', reaped, stash: saved.stash, ...settled, lane: closed.kind }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Stop `#<N>`'s lane child, stash its work, close the Issue as not planned and close the lane. */
|
|
122
|
+
async function withdraw(issue: string, reason: string): Promise<WithdrawOutcome> {
|
|
123
|
+
issue_number_shape.require_issue_number(issue)
|
|
124
|
+
const refusal = await refusal_of(issue)
|
|
125
|
+
|
|
126
|
+
if (refusal !== undefined) return refusal
|
|
127
|
+
const lane = await lane_registry.find_open_lane(issue)
|
|
128
|
+
|
|
129
|
+
if (lane === undefined) return { kind: NO_LANE_KIND }
|
|
130
|
+
|
|
131
|
+
return await stop_and_close(lane, reason)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stash_sentence(stash: string | undefined): string {
|
|
135
|
+
if (stash === undefined) return 'The lane held no uncommitted work.'
|
|
136
|
+
|
|
137
|
+
return `Its uncommitted work was stashed — recover with \`pnpm josh stash:pop "${stash}"\`.`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function withdrawn_text(outcome: Withdrawn, issue: string): string {
|
|
141
|
+
const cited = session_cite.issue(issue)
|
|
142
|
+
const stopped = outcome.reaped === 0 ? 'Its child had already ended.' : 'Stopped its child.'
|
|
143
|
+
const head = `Withdrew ${cited}. ${stopped} ${stash_sentence(outcome.stash)}`
|
|
144
|
+
const lane = outcome.lane === 'incomplete' ? ' The lane could not be fully removed.' : ''
|
|
145
|
+
|
|
146
|
+
if (outcome.had_closed) return `${head} The Issue had already closed and was left as is.${lane}`
|
|
147
|
+
|
|
148
|
+
if (outcome.is_closed) return `${head} The Issue is closed as not planned.${lane}`
|
|
149
|
+
|
|
150
|
+
return `${head} The Issue could not be closed — close ${cited} by hand; \`run:merge\` books it withdrawn either way.${lane}`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function unsaved_text(outcome: Unsaved, issue: string): string {
|
|
154
|
+
return `The child for ${session_cite.issue(issue)} was stopped, but its uncommitted work could not be stashed (${outcome.reason}), so the Issue is left open and the lane at ${outcome.directory}; \`run:merge\` books it withdrawn.`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function refused_text(outcome: Refused, issue: string): string {
|
|
158
|
+
const cited = session_cite.issue(issue)
|
|
159
|
+
|
|
160
|
+
if (outcome.kind === CLOSED_KIND) {
|
|
161
|
+
return `${cited} is already closed, so there is nothing to withdraw.`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (outcome.kind === NO_LANE_KIND) {
|
|
165
|
+
return `No lane is open for ${cited}, so there is no child to withdraw.`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return `The state of ${cited} could not be read, so nothing was stopped or closed — try again.`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function describe(outcome: WithdrawOutcome, issue: string): string {
|
|
172
|
+
if (outcome.kind === 'unsaved') return unsaved_text(outcome, issue)
|
|
173
|
+
|
|
174
|
+
return outcome.kind === 'withdrawn'
|
|
175
|
+
? withdrawn_text(outcome, issue)
|
|
176
|
+
: refused_text(outcome, issue)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const lane_withdraw = { describe, withdraw }
|
|
180
|
+
|
|
181
|
+
export type { WithdrawOutcome }
|
|
182
|
+
export { lane_withdraw }
|
package/scripts/lint/format.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { project_checks } from '#scripts/gate/project-checks'
|
|
4
|
-
import { ESLINT_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
|
|
4
|
+
import { ESLINT_CACHE_FLAGS, PRETTIER_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
|
|
5
5
|
import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-composite-arguments'
|
|
6
6
|
import { execa } from 'execa'
|
|
7
7
|
|
|
8
8
|
const COMMAND_NAME = 'format'
|
|
9
9
|
const ARGV_OFFSET = 2
|
|
10
10
|
const FAIL_EXIT_CODE = 1
|
|
11
|
-
const
|
|
11
|
+
const PRETTIER_WRITE_ARGS = ['exec', 'prettier', '--write', '.', ...PRETTIER_CACHE_FLAGS] as const
|
|
12
12
|
const ESLINT_FIX_ARGS = ['exec', 'eslint', '.', '--fix', ...ESLINT_CACHE_FLAGS] as const
|
|
13
13
|
|
|
14
14
|
async function run_step(args: ReadonlyArray<string>, directory: string): Promise<number> {
|
|
@@ -37,7 +37,7 @@ async function run_format(directory: string): Promise<number> {
|
|
|
37
37
|
const prettier_exit_code = await run_unless_skipped(
|
|
38
38
|
'prettier',
|
|
39
39
|
prettier_reason,
|
|
40
|
-
|
|
40
|
+
PRETTIER_WRITE_ARGS,
|
|
41
41
|
directory,
|
|
42
42
|
)
|
|
43
43
|
if (prettier_exit_code !== 0) return prettier_exit_code
|
|
@@ -64,4 +64,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
64
64
|
|
|
65
65
|
const format = { run, run_format }
|
|
66
66
|
|
|
67
|
-
export { ESLINT_FIX_ARGS, format }
|
|
67
|
+
export { ESLINT_FIX_ARGS, format, PRETTIER_WRITE_ARGS }
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { project_checks } from '#scripts/gate/project-checks'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ESLINT_CACHE_FILE,
|
|
6
|
+
ESLINT_CACHE_FLAGS,
|
|
7
|
+
PRETTIER_CACHE_FLAGS,
|
|
8
|
+
} from '#scripts/josh/josh-command-types'
|
|
5
9
|
import { lane_cache_run } from '#scripts/lane/lane-cache-run'
|
|
6
10
|
import {
|
|
7
11
|
buffered_process,
|
|
@@ -11,7 +15,6 @@ import {
|
|
|
11
15
|
import { SKIP_MARKER } from '#scripts/test/skip-marker'
|
|
12
16
|
import { cross_file_recheck, type CrossFileRecheckOptions } from './cross-file-recheck'
|
|
13
17
|
|
|
14
|
-
const PRETTIER_ARGS = ['exec', 'prettier', '--check', '.'] as const
|
|
15
18
|
// This — not the `lint:eslint` map entry — is the eslint invocation `josh gate` reaches, so the
|
|
16
19
|
// cache flags come from the same constant the ignore rules are asserted against.
|
|
17
20
|
// A copy here would keep writing the old location the day that constant
|
|
@@ -23,6 +26,10 @@ const WHOLE_TREE_RECHECK: CrossFileRecheckOptions = {
|
|
|
23
26
|
patterns: [WHOLE_TREE],
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
// Prettier's cache stays this checkout's own, never shared through `lane_cache_run` as eslint's is:
|
|
30
|
+
// its entries do not survive a move (`josh-command-types.ts` → `GATE_CACHE_SPECS`).
|
|
31
|
+
const PRETTIER_ARGS = ['exec', 'prettier', '--check', WHOLE_TREE, ...PRETTIER_CACHE_FLAGS] as const
|
|
32
|
+
|
|
26
33
|
// What one eslint run is made of: the child's arguments, and what re-reading a cached cross-file report
|
|
27
34
|
// (`cross-file-recheck.ts`) needs to ask the same question again — the cache file and the targets.
|
|
28
35
|
interface EslintRun {
|
|
@@ -132,4 +139,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
132
139
|
|
|
133
140
|
const lint_parallel = { run_lint_checks, run_lint_parallel_checks }
|
|
134
141
|
|
|
135
|
-
export { ESLINT_ARGS, lint_parallel }
|
|
142
|
+
export { ESLINT_ARGS, lint_parallel, PRETTIER_ARGS }
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
ESLINT_RELATED_CACHE_FILE,
|
|
10
10
|
ESLINT_RELATED_CACHE_FLAGS,
|
|
11
|
+
PRETTIER_RELATED_CACHE_FLAGS,
|
|
11
12
|
} from '#scripts/josh/josh-command-types'
|
|
12
13
|
import type { CrossFileRecheckOptions } from './cross-file-recheck'
|
|
13
14
|
|
|
@@ -94,8 +95,10 @@ function describe_scope(scope: ChangedFileScope, root: string): string {
|
|
|
94
95
|
return changed_file_scope.describe_scope(scope, root, VOCABULARY)
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
// The scoped run's own cache file, for the reason eslint's below gives: prettier too rewrites its
|
|
99
|
+
// cache file whole, so a narrowed run sharing the gate's would discard entries a concurrent gate wrote.
|
|
97
100
|
function prettier_arguments(files: ReadonlyArray<string>): ReadonlyArray<string> {
|
|
98
|
-
return [...PRETTIER_ARGS, ...files]
|
|
101
|
+
return [...PRETTIER_ARGS, ...files, ...PRETTIER_RELATED_CACHE_FLAGS]
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
// The cache file is this command's own rather than the gate's, because the two run at the same time.
|
|
@@ -46,8 +46,7 @@ function apply_marker_for_destination(
|
|
|
46
46
|
text: string,
|
|
47
47
|
package_name: string,
|
|
48
48
|
): string {
|
|
49
|
-
if (!is_workflow_destination(destination)) return text
|
|
50
|
-
if (is_marked(text)) return text
|
|
49
|
+
if (!is_workflow_destination(destination) || is_marked(text)) return text
|
|
51
50
|
|
|
52
51
|
return `${marker_block(package_name)}${text}`
|
|
53
52
|
}
|
|
@@ -112,6 +112,14 @@ async function resolve_context(values: CliValues): Promise<ResolvedContext> {
|
|
|
112
112
|
return { repo_name, issue_title }
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// The send behind the command line, for a caller that already holds the values — the `Stop` hook's
|
|
116
|
+
// own confirmation (`stop-actions.ts`). It throws as `telegram_notify.send` does.
|
|
117
|
+
async function send_values(values: CliValues): Promise<void> {
|
|
118
|
+
const context = await resolve_context(values)
|
|
119
|
+
|
|
120
|
+
await telegram_notify.send(telegram_test_logic.build_input({ values, context }))
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
async function main(): Promise<void> {
|
|
116
124
|
josh_environment_file.load_environment_file()
|
|
117
125
|
const values = parse_cli_arguments()
|
|
@@ -122,10 +130,7 @@ async function main(): Promise<void> {
|
|
|
122
130
|
return
|
|
123
131
|
}
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
const input = telegram_test_logic.build_input({ values, context })
|
|
127
|
-
|
|
128
|
-
await telegram_notify.send(input)
|
|
133
|
+
await send_values(values)
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
// **A notification nobody received must not read as success**. This command
|
|
@@ -148,6 +153,6 @@ async function run_cli(): Promise<void> {
|
|
|
148
153
|
|
|
149
154
|
if (process.argv[1] === fileURLToPath(import.meta.url)) await run_cli()
|
|
150
155
|
|
|
151
|
-
const telegram_test = { fetch_repo_name, fetch_issue_title, resolve_context, run_cli }
|
|
156
|
+
const telegram_test = { fetch_repo_name, fetch_issue_title, resolve_context, run_cli, send_values }
|
|
152
157
|
|
|
153
158
|
export { telegram_test }
|
|
@@ -42,9 +42,7 @@ function find_entry_mismatch(
|
|
|
42
42
|
specifier: string,
|
|
43
43
|
override: string | undefined,
|
|
44
44
|
): Array<SpecifierMismatch> {
|
|
45
|
-
if (override === undefined) return []
|
|
46
|
-
if (is_non_registry(specifier)) return []
|
|
47
|
-
if (specifier === override) return []
|
|
45
|
+
if (override === undefined || specifier === override || is_non_registry(specifier)) return []
|
|
48
46
|
|
|
49
47
|
return [{ importer, name, lockfile_specifier: specifier, override }]
|
|
50
48
|
}
|
|
@@ -277,8 +277,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
const plan = plan_run(options)
|
|
280
|
-
if (plan === undefined) return FAILURE_EXIT_CODE
|
|
281
|
-
if (!(await resolve_publish(plan, options))) return FAILURE_EXIT_CODE
|
|
280
|
+
if (plan === undefined || !(await resolve_publish(plan, options))) return FAILURE_EXIT_CODE
|
|
282
281
|
|
|
283
282
|
return propagate_to_consumers(plan, options.is_dry_run)
|
|
284
283
|
}
|
|
@@ -145,8 +145,9 @@ async function wait_for_github_release(version: string): Promise<boolean> {
|
|
|
145
145
|
async function wait_for_distribution(version: string): Promise<number> {
|
|
146
146
|
const manifest = read_manifest(process.cwd())
|
|
147
147
|
|
|
148
|
-
if (!(await wait_for_npm(manifest, version)))
|
|
149
|
-
|
|
148
|
+
if (!(await wait_for_npm(manifest, version)) || !(await wait_for_github_release(version))) {
|
|
149
|
+
return FAILURE_EXIT_CODE
|
|
150
|
+
}
|
|
150
151
|
|
|
151
152
|
console.info(complete_line(version))
|
|
152
153
|
|
|
@@ -8,6 +8,7 @@ const REPORT_FORMAT_PATH = 'prompts/collaboration-workflow/report-format.md'
|
|
|
8
8
|
const SUMMARY_RULES_HEADING = '作業前サマリはいつ・どう出すか'
|
|
9
9
|
const SUMMARY_TEMPLATE_HEADING = '作業前サマリ(セッション向け・`JOSH_SESSION_LANG` の言語)'
|
|
10
10
|
const COMPLETION_REPORT_HEADING = '完了報告(セッション向け)'
|
|
11
|
+
const ACCEPTANCE_MAP_HEADING = 'PR 本文の受け入れ条件の対応表'
|
|
11
12
|
|
|
12
13
|
// The heading goes in single quotes in the runnable form: the template heading carries backticks,
|
|
13
14
|
// which a double-quoted shell argument would run as a command substitution.
|
|
@@ -16,6 +17,7 @@ function pointer(heading: string): string {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
const report_format_reference = {
|
|
20
|
+
ACCEPTANCE_MAP_HEADING,
|
|
19
21
|
COMPLETION_REPORT_HEADING,
|
|
20
22
|
pointer,
|
|
21
23
|
REPORT_FORMAT_PATH,
|
|
@@ -102,8 +102,7 @@ function overview_lines(summary: string): ReadonlyArray<string> {
|
|
|
102
102
|
function item_sentence(line: string): string {
|
|
103
103
|
const open = line.indexOf(BOLD_MARKER)
|
|
104
104
|
const close = open === -1 ? -1 : line.indexOf(BOLD_MARKER, open + BOLD_MARKER.length)
|
|
105
|
-
const raw =
|
|
106
|
-
close === -1 ? line.slice(ITEM_LINE_PREFIX.length) : line.slice(close + BOLD_MARKER.length)
|
|
105
|
+
const raw = line.slice(close === -1 ? ITEM_LINE_PREFIX.length : close + BOLD_MARKER.length)
|
|
107
106
|
|
|
108
107
|
return raw.replace(LEADING_LABEL_PUNCTUATION, '').trim()
|
|
109
108
|
}
|
|
@@ -199,8 +198,12 @@ function break_condition_count(lines: ReadonlyArray<string>): number {
|
|
|
199
198
|
}
|
|
200
199
|
|
|
201
200
|
function case_line_violations(summary: string): ReadonlyArray<string> {
|
|
202
|
-
if (
|
|
203
|
-
|
|
201
|
+
if (
|
|
202
|
+
!has_changes_section(summary) ||
|
|
203
|
+
changes_region_lines(summary).some((line) => line.includes(CASE_LABEL))
|
|
204
|
+
) {
|
|
205
|
+
return []
|
|
206
|
+
}
|
|
204
207
|
|
|
205
208
|
return [`✖ no case tier (${CASE_LABEL}:) in ${CHANGES_LABEL}`]
|
|
206
209
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
2
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
3
|
+
import { acceptance_map, type Deferral } from './acceptance-map'
|
|
4
|
+
|
|
5
|
+
// The reads behind `acceptance_map`: the Issue as it reads at the merge, the pull request body, and
|
|
6
|
+
// whether each issue a deferral row names exists. Kept apart so the comparison stays pure.
|
|
7
|
+
|
|
8
|
+
const ISSUE_SEPARATOR = '#'
|
|
9
|
+
|
|
10
|
+
// `owner/repo#12` reads that repository's issue; a bare `#12` reads this one's.
|
|
11
|
+
async function is_found(deferral: Deferral): Promise<boolean> {
|
|
12
|
+
const [repo, number = ''] = deferral.issue.split(ISSUE_SEPARATOR)
|
|
13
|
+
const read = await git_gh_command.issue_get_body_classified(
|
|
14
|
+
number,
|
|
15
|
+
repo === '' ? undefined : repo,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
return read.kind === 'read'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function absent_deferrals(
|
|
22
|
+
deferrals: ReadonlyArray<Deferral>,
|
|
23
|
+
): Promise<ReadonlyArray<string>> {
|
|
24
|
+
const found = await Promise.all(deferrals.map(async (deferral) => await is_found(deferral)))
|
|
25
|
+
|
|
26
|
+
return deferrals
|
|
27
|
+
.filter((_deferral, index) => found[index] !== true)
|
|
28
|
+
.map((deferral) => `- \`${deferral.id}\` names ${deferral.issue}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function unreadable_message(issue: string): string {
|
|
32
|
+
return `Issue ${session_cite.issue(issue)} could not be re-read to check its acceptance criteria, so the merge is refused; re-run once it can be.`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// **An Issue that cannot be re-read is a refusal, not a pass** — the criteria it would have named are
|
|
36
|
+
// exactly what this gate exists to see. An Issue with no acceptance section merges as before.
|
|
37
|
+
// `josh ship`'s preflight asks the body it is about to deliver, so a missing table stops it before the
|
|
38
|
+
// review rather than at the merge.
|
|
39
|
+
async function body_refusal(
|
|
40
|
+
issue: string,
|
|
41
|
+
pr_body: string | undefined,
|
|
42
|
+
): Promise<string | undefined> {
|
|
43
|
+
const issue_read = await git_gh_command.issue_get_body_classified(issue)
|
|
44
|
+
|
|
45
|
+
if (issue_read.kind !== 'read') return unreadable_message(issue)
|
|
46
|
+
const criteria = acceptance_map.criteria_of(issue_read.text)
|
|
47
|
+
const comparison = acceptance_map.compare(criteria, acceptance_map.rows_of(pr_body))
|
|
48
|
+
const absent = await absent_deferrals(comparison.deferrals)
|
|
49
|
+
|
|
50
|
+
if (comparison.missing.length === 0 && absent.length === 0) return undefined
|
|
51
|
+
|
|
52
|
+
return acceptance_map.refusal_message(issue, comparison, absent)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function refusal(issue: string, branch_name: string): Promise<string | undefined> {
|
|
56
|
+
return await body_refusal(issue, await git_gh_command.pr_get_body(branch_name))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const acceptance_map_check = { body_refusal, refusal }
|
|
60
|
+
|
|
61
|
+
export { acceptance_map_check }
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { epic_audit_logic } from '#scripts/epic/epic-audit'
|
|
3
|
+
import { markdown_section } from '#scripts/issue/markdown-section'
|
|
4
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
5
|
+
import { report_format_reference } from '#scripts/report/report-format-reference'
|
|
6
|
+
|
|
7
|
+
// A green gate and a clean review say the diff is sound, never that it delivers what the Issue asked
|
|
8
|
+
// for. **This is the seam where each acceptance criterion is checked one by one**: the pull request
|
|
9
|
+
// body carries one row per criterion — met, with its evidence, or deferred to a split-off issue — and
|
|
10
|
+
// `josh followup` re-reads the Issue just before the merge and refuses a criterion with no row.
|
|
11
|
+
//
|
|
12
|
+
// **A criterion is matched by an identifier built from its own text**, so a criterion added or
|
|
13
|
+
// reworded after the body was written has an identifier no row carries, and is refused as unmapped
|
|
14
|
+
// rather than passing on a stale row. The checkbox is stripped first: ticking it changes nothing.
|
|
15
|
+
//
|
|
16
|
+
// The criteria are read through `epic_audit_logic.acceptance_section`, the one place the acceptance
|
|
17
|
+
// headings are defined.
|
|
18
|
+
|
|
19
|
+
const MAP_HEADING = '## 受け入れ条件の対応'
|
|
20
|
+
const MET = '満たした'
|
|
21
|
+
const DEFERRED = '見送り'
|
|
22
|
+
const PENDING = '未記入'
|
|
23
|
+
const PENDING_SUFFIX = `— ${PENDING}`
|
|
24
|
+
const ID_PREFIX = 'ac-'
|
|
25
|
+
const ID_HEX_LENGTH = 8
|
|
26
|
+
const BACKTICK = '`'
|
|
27
|
+
// A top-level list item only: a nested item is detail under a criterion, not one of its own.
|
|
28
|
+
const CRITERION_PATTERN = /^(?:[-*+]|\d+\.)\s+(?:\[[ xX]\]\s+)?(?<text>\S.*)$/u
|
|
29
|
+
// A list item, heading, table row or HTML comment opens a block of its own, never a wrapped line.
|
|
30
|
+
const BLOCK_START_PATTERN = /^\s*(?:(?:[-*+]|\d+\.)\s|#|\||<!--)/u
|
|
31
|
+
// The criterion's text may sit between the identifier and the status for the reader, so the status is
|
|
32
|
+
// the last status word on the line — a criterion quoting the row format stays text.
|
|
33
|
+
const ROW_PATTERN = new RegExp(
|
|
34
|
+
String.raw`^[-*]\s+${BACKTICK}(?<id>${ID_PREFIX}[0-9a-f]{${String(ID_HEX_LENGTH)}})${BACKTICK}(?:\s.*)?\s(?<kind>${MET}|${DEFERRED}):\s*(?<rest>\S.*)$`,
|
|
35
|
+
'u',
|
|
36
|
+
)
|
|
37
|
+
// A deferral names the split-off issue first, then why it was deferred.
|
|
38
|
+
const DEFERRAL_PATTERN = /^(?<issue>(?:[\w.-]+\/[\w.-]+)?#\d+)\s+\S/u
|
|
39
|
+
const ROW_FORMAT = `under ${MAP_HEADING}, one line per acceptance criterion: \`- \`<id>\` <criterion> — ${MET}: <test name, file:line or command output>\` or \`- \`<id>\` <criterion> — ${DEFERRED}: #<split-off issue> <reason>\``
|
|
40
|
+
|
|
41
|
+
interface Criterion {
|
|
42
|
+
id: string
|
|
43
|
+
text: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Deferral {
|
|
47
|
+
id: string
|
|
48
|
+
issue: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface Row {
|
|
52
|
+
id: string
|
|
53
|
+
kind: string
|
|
54
|
+
rest: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface MapRows {
|
|
58
|
+
met: ReadonlySet<string>
|
|
59
|
+
deferrals: ReadonlyArray<Deferral>
|
|
60
|
+
ids: ReadonlySet<string>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface MapComparison {
|
|
64
|
+
missing: ReadonlyArray<Criterion>
|
|
65
|
+
deferrals: ReadonlyArray<Deferral>
|
|
66
|
+
stale: ReadonlyArray<string>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function criterion_id(text: string): string {
|
|
70
|
+
const normalized = text.replaceAll(/\s+/gu, ' ').trim()
|
|
71
|
+
const digest = createHash('sha256').update(normalized).digest('hex')
|
|
72
|
+
|
|
73
|
+
return `${ID_PREFIX}${digest.slice(0, ID_HEX_LENGTH)}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function joins(last: string | undefined, line: string): boolean {
|
|
77
|
+
const is_continuation = line.trim() !== '' && !BLOCK_START_PATTERN.test(line)
|
|
78
|
+
|
|
79
|
+
return is_continuation && last !== undefined && CRITERION_PATTERN.test(last)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A criterion wrapped onto continuation lines is one criterion, hashed whole — rewording any of its
|
|
83
|
+
// lines changes its identifier. A blank line or a nested item ends it.
|
|
84
|
+
function unwrapped(lines: ReadonlyArray<string>): ReadonlyArray<string> {
|
|
85
|
+
const items: Array<string> = []
|
|
86
|
+
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
items.push(joins(items.at(-1), line) ? `${items.pop() ?? ''} ${line.trim()}` : line)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return items
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function criteria_of(issue_body: string | undefined): ReadonlyArray<Criterion> {
|
|
95
|
+
// A list item inside a code fence is an example the Issue quotes, not a criterion. An indented line
|
|
96
|
+
// outside one is a wrapped criterion's continuation, so it is kept.
|
|
97
|
+
const lines = markdown_section.fence_free_lines(epic_audit_logic.acceptance_section(issue_body))
|
|
98
|
+
const texts = unwrapped(lines).map((line) => CRITERION_PATTERN.exec(line)?.groups?.['text'])
|
|
99
|
+
|
|
100
|
+
return texts
|
|
101
|
+
.filter((text): text is string => text !== undefined)
|
|
102
|
+
.map((text) => ({ id: criterion_id(text), text: text.trim() }))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A deferral row with no issue or no reason is no row at all, so its criterion reads as unmapped.
|
|
106
|
+
function deferral_of(id: string, rest: string): Deferral | undefined {
|
|
107
|
+
const issue = DEFERRAL_PATTERN.exec(rest)?.groups?.['issue']
|
|
108
|
+
|
|
109
|
+
return issue === undefined ? undefined : { id, issue }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Every group in `ROW_PATTERN` is mandatory, so a match always carries the name.
|
|
113
|
+
function group(groups: Readonly<Record<string, string>>, name: string): string {
|
|
114
|
+
return groups[name] ?? ''
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A pending row is no row even when the criterion it quotes carries a status word — otherwise the
|
|
118
|
+
// skeleton's own unfilled row would read as met.
|
|
119
|
+
function row_of(line: string): Row | undefined {
|
|
120
|
+
const trimmed = line.trim()
|
|
121
|
+
|
|
122
|
+
if (trimmed.endsWith(PENDING_SUFFIX)) return undefined
|
|
123
|
+
const groups = ROW_PATTERN.exec(trimmed)?.groups
|
|
124
|
+
|
|
125
|
+
if (groups === undefined) return undefined
|
|
126
|
+
|
|
127
|
+
return { id: group(groups, 'id'), kind: group(groups, 'kind'), rest: group(groups, 'rest') }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function rows_of(pr_body = ''): MapRows {
|
|
131
|
+
const rows = markdown_section
|
|
132
|
+
.section_lines(pr_body, MAP_HEADING)
|
|
133
|
+
.map((line) => row_of(line))
|
|
134
|
+
.filter((row): row is Row => row !== undefined)
|
|
135
|
+
const met = rows.filter((row) => row.kind === MET).map((row) => row.id)
|
|
136
|
+
const deferrals = rows
|
|
137
|
+
.filter((row) => row.kind === DEFERRED)
|
|
138
|
+
.map((row) => deferral_of(row.id, row.rest))
|
|
139
|
+
.filter((deferral): deferral is Deferral => deferral !== undefined)
|
|
140
|
+
|
|
141
|
+
return { met: new Set(met), deferrals, ids: new Set([...met, ...deferrals.map((row) => row.id)]) }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function compare(criteria: ReadonlyArray<Criterion>, rows: MapRows): MapComparison {
|
|
145
|
+
const current = new Set(criteria.map((criterion) => criterion.id))
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
missing: criteria.filter((criterion) => !rows.ids.has(criterion.id)),
|
|
149
|
+
deferrals: rows.deferrals.filter((deferral) => current.has(deferral.id)),
|
|
150
|
+
stale: [...rows.ids].filter((id) => !current.has(id)),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// What `josh pr` writes when the caller's body carries no table: every criterion as a pending row,
|
|
155
|
+
// which the merge refuses until it is filled in.
|
|
156
|
+
function skeleton(issue_body: string | undefined): string | undefined {
|
|
157
|
+
const criteria = criteria_of(issue_body)
|
|
158
|
+
|
|
159
|
+
if (criteria.length === 0) return undefined
|
|
160
|
+
const rows = criteria.map(
|
|
161
|
+
(criterion) => `- \`${criterion.id}\` ${criterion.text} ${PENDING_SUFFIX}`,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return [MAP_HEADING, '', ...rows].join('\n')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function has_map(pr_body = ''): boolean {
|
|
168
|
+
return markdown_section.has_unfenced_line(pr_body, MAP_HEADING)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function listed(title: string, lines: ReadonlyArray<string>): ReadonlyArray<string> {
|
|
172
|
+
return lines.length === 0 ? [] : [title, ...lines.map((line) => ` ${line}`)]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function refusal_message(
|
|
176
|
+
issue: string,
|
|
177
|
+
comparison: MapComparison,
|
|
178
|
+
absent: ReadonlyArray<string>,
|
|
179
|
+
): string {
|
|
180
|
+
return [
|
|
181
|
+
`The pull request body does not map every acceptance criterion of ${session_cite.issue(issue)} as it reads now, so the merge is refused.`,
|
|
182
|
+
...listed(
|
|
183
|
+
'Criteria with no row (added or reworded since the body was written, or never mapped):',
|
|
184
|
+
comparison.missing.map((criterion) => `- \`${criterion.id}\` ${criterion.text}`),
|
|
185
|
+
),
|
|
186
|
+
...listed('Deferral rows naming an issue that could not be found:', absent),
|
|
187
|
+
...listed('Rows naming no current criterion:', comparison.stale),
|
|
188
|
+
`Write ${ROW_FORMAT} — format: ${report_format_reference.pointer(report_format_reference.ACCEPTANCE_MAP_HEADING)}`,
|
|
189
|
+
'Write it into the `--body-file` before `josh ship`, or update an open PR with `gh api -X PATCH repos/{owner}/{repo}/pulls/<N> -F body=@<path>`, then re-run.',
|
|
190
|
+
].join('\n')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const acceptance_map = {
|
|
194
|
+
MAP_HEADING,
|
|
195
|
+
compare,
|
|
196
|
+
criteria_of,
|
|
197
|
+
criterion_id,
|
|
198
|
+
has_map,
|
|
199
|
+
refusal_message,
|
|
200
|
+
rows_of,
|
|
201
|
+
skeleton,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type { Criterion, Deferral, MapComparison }
|
|
205
|
+
export { acceptance_map }
|
|
@@ -330,7 +330,7 @@ async function run_review(round: number, reading: ChangeReading): Promise<number
|
|
|
330
330
|
const refusal = scoped_green.refusal_for(tree, commit)
|
|
331
331
|
|
|
332
332
|
if (refusal !== undefined) return report_error(refusal)
|
|
333
|
-
const resolved = agent_role_profile.
|
|
333
|
+
const resolved = agent_role_profile.resolve_review(round)
|
|
334
334
|
|
|
335
335
|
if (resolved.kind === 'rejected') return report_error(resolved.note)
|
|
336
336
|
agent_role_profile.warn_of_default()
|