@joshuafolkken/kit 1.1387.0 → 1.1453.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/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
- package/.claude/skills/workflow-commands/backlogrun.md +3 -2
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +1 -1
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/kickoff.md +12 -13
- package/.claude/skills/workflow-commands/observation-filing.md +1 -1
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/.claude/skills/workflow-commands/prerequisite.md +6 -0
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/README.md +27 -4
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +261 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +95 -19
- package/package.json +4 -3
- package/prompts/collaboration-workflow/issue-template.md +10 -8
- package/prompts/review.md +30 -4
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
- package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
- package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
- package/scripts/auto-ok/auto-ok-judge.ts +67 -0
- package/scripts/backlog/backlog-drive-cli.ts +14 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +38 -11
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +175 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +18 -4
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +13 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +7 -4
- package/scripts/init/init-ai-copy.ts +34 -11
- package/scripts/init/init-basic.ts +9 -4
- package/scripts/init/init-bootstrap.ts +6 -2
- package/scripts/init/init-dry-run-report.ts +150 -0
- package/scripts/init/init-dry-run.ts +189 -0
- package/scripts/init/init-install.ts +8 -1
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +62 -42
- package/scripts/init/kit-setup-state.ts +11 -17
- package/scripts/init/own-formatter.ts +21 -0
- package/scripts/init/project-profile.ts +60 -12
- package/scripts/init/start-dry-run.ts +70 -0
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +23 -3
- package/scripts/init/start-setup-pr.ts +13 -24
- package/scripts/init/start-steps.ts +62 -1
- package/scripts/init/start.ts +38 -17
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/init/working-tree.ts +116 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-auto-ok.ts +68 -99
- package/scripts/issue/issue-file-cli.ts +17 -17
- package/scripts/issue/issue-file.ts +1 -6
- package/scripts/issue/issue-labels.ts +46 -10
- package/scripts/issue/issue-release-cli.ts +69 -23
- package/scripts/issue/issue-release.ts +3 -1
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-ai.ts +11 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +10 -3
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +26 -2
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/josh/process-identity.ts +32 -6
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-broker-server.ts +69 -0
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/lane-session-broker.ts +363 -0
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/observations/observation-ledger-migrate.ts +3 -15
- package/scripts/release/release-issue-close.ts +133 -0
- package/scripts/release/release-publish.ts +12 -1
- package/scripts/repo/repository-labels.ts +11 -1
- package/scripts/review/comment-ranges.ts +199 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +24 -7
- package/scripts/review/review-code-tree.ts +119 -0
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/review/review-round2-cli.ts +16 -5
- package/scripts/review/review-round2.ts +30 -10
- package/scripts/review/review-stamps.ts +22 -1
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/lane/openai-review-broker.ts +0 -262
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kit-test-backlogrun",
|
|
3
|
+
"requires_github": true,
|
|
4
|
+
"package_source": "tarball",
|
|
5
|
+
"requires_claude": true,
|
|
6
|
+
"files": {
|
|
7
|
+
"index.html": "<!doctype html><html><body><h1>Hello</h1></body></html>\n"
|
|
8
|
+
},
|
|
9
|
+
"commands": [
|
|
10
|
+
"gh auth setup-git",
|
|
11
|
+
"git config --global user.name kit-test && git config --global user.email kit-test@example.com",
|
|
12
|
+
"printf '{ \"private\": true }\\n' > package.json",
|
|
13
|
+
"cp \"$PACKAGE_TARBALL\" . && pnpm add -D --allow-build=esbuild --allow-build=unrs-resolver \"./$(basename \"$PACKAGE_TARBALL\")\"",
|
|
14
|
+
"pnpm exec josh start --yes --github",
|
|
15
|
+
"gh label list --limit 100 --json name --jq '.[].name' > \"$HOME/labels.txt\"; grep -xE 'auto-ok|needs-decision|needs-human-review|already-done|review-round2-skipped|bug' \"$HOME/labels.txt\"; test \"$(grep -cxE 'auto-ok|needs-decision|needs-human-review|already-done|review-round2-skipped|bug' \"$HOME/labels.txt\")\" -eq 6",
|
|
16
|
+
"curl -fsSL https://claude.ai/install.sh | bash && export PATH=\"$HOME/.local/bin:$PATH\" && claude --version",
|
|
17
|
+
"gh issue create --title 'Add a heading to index.html' --body 'Add an h2 under the h1.' --label auto-ok --label run:lane",
|
|
18
|
+
"timeout 60 pnpm josh backlogrun > \"$HOME/backlogrun.out\" 2>&1; cat \"$HOME/backlogrun.out\"; grep -q '^backlogrun: started' \"$HOME/backlogrun.out\"",
|
|
19
|
+
"pnpm josh run:event --from 0 | grep 'starting session'",
|
|
20
|
+
"for i in $(seq 1 90); do pnpm josh run:event --from 0 | grep -q 'dispatching' && break; sleep 10; done; pnpm josh run:event --from 0 | grep 'dispatching'",
|
|
21
|
+
"for i in $(seq 1 120); do test \"$(gh pr list --state all --json number --jq length)\" -gt 0 && break; sleep 10; done; pnpm josh run:board --chat",
|
|
22
|
+
"pnpm josh backlogrun --stop"
|
|
23
|
+
],
|
|
24
|
+
"probes": [
|
|
25
|
+
"tail -n 40 \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\" | cut -c1-400",
|
|
26
|
+
"grep -c 'requires approval' \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\"",
|
|
27
|
+
"tail -n 40 /tmp/josh-lane-dispatch-*.log | cut -c1-400",
|
|
28
|
+
"cat /tmp/josh-lane-dispatch-*.log | grep -c 'requires approval'",
|
|
29
|
+
"gh pr list --state all"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -2,6 +2,7 @@ import { GATE_COMMAND } from '#scripts/josh/josh-command-types'
|
|
|
2
2
|
import { josh_verdict } from '#scripts/josh/josh-verdict'
|
|
3
3
|
import { buffered_process, type BufferedProcessResult } from '#scripts/lib/buffered-process'
|
|
4
4
|
import { status_icons } from '#scripts/lib/status-icons'
|
|
5
|
+
import { NOT_USED_MARKER, NOTHING_TO_CHECK_MARKER } from '#scripts/test/skip-marker'
|
|
5
6
|
import { test_unit_guard } from '#scripts/test/test-unit-guard'
|
|
6
7
|
import { gate_log } from './gate-log'
|
|
7
8
|
import { gate_plan } from './gate-plan'
|
|
@@ -11,7 +12,7 @@ import { gate_plan } from './gate-plan'
|
|
|
11
12
|
// about what is printed changes in the move: the four blocks, the log notice and the verdict come out
|
|
12
13
|
// exactly as before, with one addition — the per-failure action lines below.
|
|
13
14
|
|
|
14
|
-
const { FAIL_ICON, PASS_ICON } = status_icons
|
|
15
|
+
const { FAIL_ICON, PASS_ICON, WARN_ICON } = status_icons
|
|
15
16
|
|
|
16
17
|
interface GateStep {
|
|
17
18
|
label: string
|
|
@@ -76,16 +77,56 @@ function has_warnings(result: GateStepResult): boolean {
|
|
|
76
77
|
// other line still withholds.
|
|
77
78
|
const SAFE_CHAIN_BANNER = 'safe-chain is not available'
|
|
78
79
|
|
|
80
|
+
function checker_lines(result: GateStepResult): ReadonlyArray<string> {
|
|
81
|
+
return result.output.split('\n').filter((line) => !line.includes(SAFE_CHAIN_BANNER))
|
|
82
|
+
}
|
|
83
|
+
|
|
79
84
|
function has_checker_warning(result: GateStepResult): boolean {
|
|
80
85
|
if (!gate_plan.WARNING_CHECKER_LABELS.includes(result.label)) return false
|
|
81
86
|
|
|
82
|
-
return result.
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
return checker_lines(result).some((line) =>
|
|
88
|
+
WARNING_MARKERS.some((marker) => line.includes(marker)),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The record's counterpart to `is_skip_notice`, narrower for the reason `has_checker_warning` is
|
|
93
|
+
// narrower than `has_warnings`: a skip of a tool the project does not use (`NOT_USED_MARKER`) left
|
|
94
|
+
// nothing unverified, so only another skip line withholds the green record.
|
|
95
|
+
function is_unverified_skip(result: GateStepResult): boolean {
|
|
96
|
+
return checker_lines(result).some(
|
|
97
|
+
(line) => line.includes(test_unit_guard.SKIP_MARKER) && !line.includes(NOT_USED_MARKER),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// **A check the verdict counts as skipped said nothing but that it checked nothing.** `is_skip_notice`
|
|
102
|
+
// asks whether a check carries a skip notice, which is the right question for printing its body; it is
|
|
103
|
+
// the wrong one for a count, twice over. A check can skip half of
|
|
104
|
+
// itself — a `basic` project's `lint` runs Prettier and skips eslint — and calling it skipped would
|
|
105
|
+
// have the verdict say nothing ran on a gate that checked the formatting. And a check can run with
|
|
106
|
+
// nothing of the project's to look at — `exports` in a consumer, `behavior` with no transcript — which
|
|
107
|
+
// is no skip, and still left an empty `basic` project reading `2 ran`. A line that is neither notice is
|
|
108
|
+
// something a tool printed, so the check ran. A tool that ran clean and silent beside a skipped one is
|
|
109
|
+
// given a line of its own by the check (`project_checks.ran_notice`), so the reading holds for it too.
|
|
110
|
+
//
|
|
111
|
+
// **pnpm's script header is not something the check printed.** Every check is spawned as
|
|
112
|
+
// `pnpm josh <target>`, and where the project declares a `josh` script — `josh init` writes one — pnpm
|
|
113
|
+
// opens the captured output with `$ josh <target>`. Counted as a line, it made every spawned check one
|
|
114
|
+
// that ran, and `not verified` unreachable in the very project it was written for.
|
|
115
|
+
const IDLE_MARKERS: ReadonlyArray<string> = [test_unit_guard.SKIP_MARKER, NOTHING_TO_CHECK_MARKER]
|
|
116
|
+
const PNPM_SCRIPT_HEADER = '$ '
|
|
117
|
+
|
|
118
|
+
function is_idle_line(line: string): boolean {
|
|
119
|
+
return IDLE_MARKERS.some((marker) => line.includes(marker))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function is_tool_line(line: string): boolean {
|
|
123
|
+
return line.trim() !== '' && !line.startsWith(PNPM_SCRIPT_HEADER)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function is_idle(result: GateStepResult): boolean {
|
|
127
|
+
const lines = checker_lines(result).filter((line) => is_tool_line(line))
|
|
128
|
+
|
|
129
|
+
return lines.length > 0 && lines.every((line) => is_idle_line(line))
|
|
89
130
|
}
|
|
90
131
|
|
|
91
132
|
function should_print_body(result: GateStepResult, is_verbose: boolean): boolean {
|
|
@@ -144,10 +185,7 @@ function format_failure_actions(failed: ReadonlyArray<GateStepResult>): string {
|
|
|
144
185
|
|
|
145
186
|
// **A check that started past the core budget is said in the summary, not only in the log**.
|
|
146
187
|
// The wait cap admits a check that waited two minutes whatever the budget says,
|
|
147
|
-
// so a gate that ran over-subscribed would otherwise look exactly like one that fitted.
|
|
148
|
-
// local rather than in `status-icons.ts` because nothing reads this line back.
|
|
149
|
-
const WARN_ICON = '⚠'
|
|
150
|
-
|
|
188
|
+
// so a gate that ran over-subscribed would otherwise look exactly like one that fitted.
|
|
151
189
|
function format_budget_notes(results: ReadonlyArray<GateStepResult>): string {
|
|
152
190
|
const lines = results.flatMap((result) =>
|
|
153
191
|
result.budget_note === undefined
|
|
@@ -158,6 +196,24 @@ function format_budget_notes(results: ReadonlyArray<GateStepResult>): string {
|
|
|
158
196
|
return lines.length === 0 ? '' : `${lines.join('\n')}\n`
|
|
159
197
|
}
|
|
160
198
|
|
|
199
|
+
// **The verdict counts what ran apart from what was skipped, and a gate that ran nothing does not say
|
|
200
|
+
// `passed`.** It printed the planned number of checks whatever happened to them, so a `basic` project
|
|
201
|
+
// whose every check skipped read `passed (6 checks)` — the same last line as a gate that ran them all,
|
|
202
|
+
// and the skip this module keeps visible in the bodies was invisible in the one line a `tail` keeps.
|
|
203
|
+
//
|
|
204
|
+
// **A gate with no skip keeps the line it always printed.** A check with nothing to check is counted
|
|
205
|
+
// apart only beside a skip: `exports` says so in every consumer, and counting it alone would reword the
|
|
206
|
+
// verdict of every gate that skipped nothing.
|
|
207
|
+
function format_green_verdict(results: ReadonlyArray<GateStepResult>, total: string): string {
|
|
208
|
+
const has_skip = results.some((result) => is_skip_notice(result))
|
|
209
|
+
const skipped = has_skip ? results.filter((result) => is_idle(result)).length : 0
|
|
210
|
+
const ran = results.length - skipped
|
|
211
|
+
|
|
212
|
+
if (ran === 0) return josh_verdict.format_gate_unverified(skipped, total)
|
|
213
|
+
|
|
214
|
+
return josh_verdict.format_gate_passed({ ran, skipped }, total)
|
|
215
|
+
}
|
|
216
|
+
|
|
161
217
|
// The total is wall-clock for the whole command, not the sum of the four — they run concurrently,
|
|
162
218
|
// so a sum would report about three times what the caller waited.
|
|
163
219
|
//
|
|
@@ -172,7 +228,7 @@ function print_gate_summary(
|
|
|
172
228
|
const total = format_seconds(report.elapsed_ms)
|
|
173
229
|
const verdict =
|
|
174
230
|
failed.length === 0
|
|
175
|
-
?
|
|
231
|
+
? format_green_verdict(results, total)
|
|
176
232
|
: josh_verdict.format_gate_failed(failed.map((result) => result.label).join(', '), total)
|
|
177
233
|
const notes = format_budget_notes(results)
|
|
178
234
|
|
|
@@ -185,7 +241,6 @@ interface GateReport {
|
|
|
185
241
|
is_verbose: boolean
|
|
186
242
|
log_path: string | undefined
|
|
187
243
|
elapsed_ms: number
|
|
188
|
-
step_count: string
|
|
189
244
|
}
|
|
190
245
|
|
|
191
246
|
// Everything a finished run says, in the order a reader meets it: the four blocks, then the log the
|
|
@@ -213,7 +268,7 @@ const gate_report = {
|
|
|
213
268
|
format_failure_actions,
|
|
214
269
|
has_checker_warning,
|
|
215
270
|
is_gate_step_failed,
|
|
216
|
-
|
|
271
|
+
is_unverified_skip,
|
|
217
272
|
report_gate_steps,
|
|
218
273
|
}
|
|
219
274
|
|
|
@@ -2,10 +2,11 @@ import { existsSync, globSync, readFileSync } from 'node:fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { find_local_bin_upwards } from '#scripts/build/local-bin'
|
|
4
4
|
import { doctor_consumer } from '#scripts/doctor/doctor-consumer'
|
|
5
|
+
import { own_formatter } from '#scripts/init/own-formatter'
|
|
5
6
|
import { project_profile } from '#scripts/init/project-profile'
|
|
6
7
|
import { ancestor_directories } from '#scripts/lib/ancestor-directories'
|
|
7
8
|
import { json_value } from '#scripts/lib/json-value'
|
|
8
|
-
import { SKIP_MARKER } from '#scripts/test/skip-marker'
|
|
9
|
+
import { NOT_USED_MARKER, SKIP_MARKER } from '#scripts/test/skip-marker'
|
|
9
10
|
import { z } from 'zod'
|
|
10
11
|
|
|
11
12
|
const PACKAGE_JSON = 'package.json'
|
|
@@ -126,8 +127,17 @@ function missing_tool_reason(
|
|
|
126
127
|
return `${package_name} is not installed`
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
// Every skip reason here names a tool the project does not use — undeclared, or with no files or no
|
|
131
|
+
// configuration to read — so the notice says so (`NOT_USED_MARKER`).
|
|
129
132
|
function skip_notice(check: string, reason: string): string {
|
|
130
|
-
return `josh ${check}: ${reason} ${SKIP_MARKER} ${check}.`
|
|
133
|
+
return `josh ${check}: ${reason} ${SKIP_MARKER} ${check}, ${NOT_USED_MARKER}.`
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// **A tool that ran clean beside a skipped one says that it ran.** A clean eslint prints nothing, so a
|
|
137
|
+
// `lint` whose Prettier was skipped read as nothing but a skip notice — the same output as a `lint`
|
|
138
|
+
// that ran neither — and the gate's verdict counted a check that linted the whole tree as skipped.
|
|
139
|
+
function ran_notice(check: string): string {
|
|
140
|
+
return `josh ${check}: ran and reported no problems.`
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
// Shared by `josh check` and the gate's type-check step, so neither reaches `tsc` on a basic
|
|
@@ -141,10 +151,12 @@ function type_check_skip_reason(directory: string): string | undefined {
|
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
// Shared by `josh lint`, `josh lint:related` and `josh format`, so a basic project without Web
|
|
144
|
-
// files — a Python or Rust project, which `josh init` gives no Prettier —
|
|
145
|
-
// reason by all three.
|
|
154
|
+
// files — a Python or Rust project, which `josh init` gives no Prettier — or with a formatter of its
|
|
155
|
+
// own is skipped for the same reason by all three.
|
|
146
156
|
function prettier_skip_reason(directory: string): string | undefined {
|
|
147
157
|
if (!is_basic(directory)) return undefined
|
|
158
|
+
const formatter = own_formatter.find_config(project_root(directory))
|
|
159
|
+
if (formatter !== undefined) return `${formatter} configures the project's own formatter`
|
|
148
160
|
if (!has_files(directory, WEB_FILES)) return 'no HTML, CSS or JavaScript files were found'
|
|
149
161
|
|
|
150
162
|
return missing_tool_reason(directory, 'prettier', 'prettier')
|
|
@@ -173,6 +185,8 @@ const project_checks = {
|
|
|
173
185
|
is_kit_repository,
|
|
174
186
|
prettier_skip_reason,
|
|
175
187
|
project_root,
|
|
188
|
+
ran_notice,
|
|
189
|
+
read_manifest,
|
|
176
190
|
skip_notice,
|
|
177
191
|
type_check_skip_reason,
|
|
178
192
|
}
|
|
@@ -64,7 +64,8 @@ interface ScopedSources {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// **`test:related` is required only where it can ever answer**. In a project
|
|
67
|
-
// whose unit guard skips — vitest absent
|
|
67
|
+
// whose unit guard skips — vitest absent with no project test script to run in its place, or a basic
|
|
68
|
+
// project with no test file — `test:related` runs
|
|
68
69
|
// nothing and so writes no record, and requiring one refused the gate forever with no way through.
|
|
69
70
|
// The guard's own decision is asked rather than re-derived, so the writer and this reader agree; a
|
|
70
71
|
// project with a unit suite still needs the record, and a run that executed nothing still writes none.
|
|
@@ -63,7 +63,9 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
|
|
|
63
63
|
// passed *without running* — `test-unit-guard` exits 0 with a notice when vitest is absent or the
|
|
64
64
|
// project has no tests — must not become "the unit tests all passed": the gate keeps that skip
|
|
65
65
|
// visible on the console, and a record erasing it would have the brief tell a review agent not to
|
|
66
|
-
// re-run tests that never ran. A
|
|
66
|
+
// re-run tests that never ran. A skip of a tool the project does not use (`NOT_USED_MARKER` — eslint
|
|
67
|
+
// with no ESLint configuration) is not one: nothing went unverified, so it withholds nothing
|
|
68
|
+
// (`gate_report.is_unverified_skip`). A step that passed **with a checker's warnings** is withheld for the
|
|
67
69
|
// same reason one place further on: since the record is reused instead of the checks being re-run, a
|
|
68
70
|
// warning printed once would never be printed again on that tree, and hiding an eslint or
|
|
69
71
|
// svelte-check finding is the same failure as hiding a skip. It is `has_checker_warning`, not
|
|
@@ -80,7 +82,7 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
|
|
|
80
82
|
// The destination is a parameter so a test can exercise the record without overwriting the one a
|
|
81
83
|
// real run may be relying on — `josh gate` and `josh review:brief` share one path by design.
|
|
82
84
|
function has_nothing_to_say(result: GateStepResult): boolean {
|
|
83
|
-
return !gate_report.
|
|
85
|
+
return !gate_report.is_unverified_skip(result) && !gate_report.has_checker_warning(result)
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
async function record_green_gate(
|
|
@@ -362,7 +364,6 @@ async function settle_gate(
|
|
|
362
364
|
is_verbose: options.is_verbose ?? false,
|
|
363
365
|
log_path: options.log_path,
|
|
364
366
|
elapsed_ms,
|
|
365
|
-
step_count: String(plan.checks.length),
|
|
366
367
|
})
|
|
367
368
|
const is_passed = failed_labels.length === 0
|
|
368
369
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Whose words kit acts on: the `author_association` REST reports with an issue or a comment, and the
|
|
2
|
+
// ones that can write to the repository. On a public repository anyone can file an issue or comment,
|
|
3
|
+
// so a body kit runs (`josh measure:rerun`) or copies into the ledger (`review:record`) is taken only
|
|
4
|
+
// from an author with write access — and an unknown author is not a trusted one.
|
|
5
|
+
const TRUSTED_ASSOCIATIONS: ReadonlySet<string> = new Set(['OWNER', 'MEMBER', 'COLLABORATOR'])
|
|
6
|
+
const TRUSTED_LIST = [...TRUSTED_ASSOCIATIONS].join(' / ')
|
|
7
|
+
|
|
8
|
+
function is_trusted(association: string | null | undefined): boolean {
|
|
9
|
+
return typeof association === 'string' && TRUSTED_ASSOCIATIONS.has(association)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const gh_author_association = { TRUSTED_LIST, is_trusted }
|
|
13
|
+
|
|
14
|
+
export { gh_author_association }
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { git_gh_exec, has_stdout_field } from './git-gh-exec'
|
|
2
|
+
|
|
3
|
+
// A `gh api` GET that sends the ETag of the previous answer, so a resource polled every few minutes
|
|
4
|
+
// costs nothing when it has not changed: GitHub answers `304 Not Modified` to a matching
|
|
5
|
+
// `If-None-Match`, and a conditional request answered 304 does not count against the rate limit.
|
|
6
|
+
//
|
|
7
|
+
// **`gh` exits non-zero on a 304** (it prints `gh: HTTP 304`), so the "unchanged" answer arrives as
|
|
8
|
+
// a thrown failure. The status is read off the `--include` status line the failure still carries in
|
|
9
|
+
// its stdout — the same reading `exec_gh_api_status` makes — never off the stderr wording.
|
|
10
|
+
|
|
11
|
+
const NOT_MODIFIED_STATUS = 304
|
|
12
|
+
const OK_STATUS = 200
|
|
13
|
+
const INCLUDE_FLAG = '--include'
|
|
14
|
+
const HEADER_FLAG = '-H'
|
|
15
|
+
const IF_NONE_MATCH = 'If-None-Match: '
|
|
16
|
+
const HEADER_BODY_SEPARATOR = /\r?\n\r?\n/u
|
|
17
|
+
const ETAG_HEADER = /^etag: *(?<etag>[^\r\n]+)/imu
|
|
18
|
+
|
|
19
|
+
type ConditionalRead =
|
|
20
|
+
| { kind: 'unchanged' }
|
|
21
|
+
| { kind: 'changed'; etag: string | undefined; body: string }
|
|
22
|
+
| { kind: 'unreadable' }
|
|
23
|
+
|
|
24
|
+
const UNCHANGED: ConditionalRead = { kind: 'unchanged' }
|
|
25
|
+
const UNREADABLE: ConditionalRead = { kind: 'unreadable' }
|
|
26
|
+
|
|
27
|
+
function to_args(path: string, etag: string | undefined): Array<string> {
|
|
28
|
+
const header = etag === undefined ? [] : [HEADER_FLAG, `${IF_NONE_MATCH}${etag}`]
|
|
29
|
+
|
|
30
|
+
return ['api', INCLUDE_FLAG, ...header, path]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The headers and the body of an `--include` answer, split at the first blank line.
|
|
34
|
+
function to_changed(output: string): ConditionalRead {
|
|
35
|
+
const [headers = '', ...rest] = output.split(HEADER_BODY_SEPARATOR)
|
|
36
|
+
const etag = ETAG_HEADER.exec(headers)?.groups?.['etag']
|
|
37
|
+
|
|
38
|
+
return { kind: 'changed', etag, body: rest.join('\n\n') }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function to_read(output: string): ConditionalRead {
|
|
42
|
+
return git_gh_exec.parse_status_line(output) === OK_STATUS ? to_changed(output) : UNREADABLE
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function failure_status(error: unknown): number | undefined {
|
|
46
|
+
const cause = error instanceof Error ? error.cause : undefined
|
|
47
|
+
|
|
48
|
+
return has_stdout_field(cause) ? git_gh_exec.parse_status_line(cause.stdout) : undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function read_conditional(
|
|
52
|
+
path: string,
|
|
53
|
+
etag: string | undefined,
|
|
54
|
+
timeout_ms?: number,
|
|
55
|
+
): Promise<ConditionalRead> {
|
|
56
|
+
try {
|
|
57
|
+
return to_read(await git_gh_exec.exec_gh_command(to_args(path, etag), timeout_ms))
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return failure_status(error) === NOT_MODIFIED_STATUS ? UNCHANGED : UNREADABLE
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const git_gh_conditional = { read_conditional, to_args }
|
|
64
|
+
|
|
65
|
+
export type { ConditionalRead }
|
|
66
|
+
export { git_gh_conditional }
|
|
@@ -288,7 +288,7 @@ interface GhApiRequest {
|
|
|
288
288
|
// second argument for the reason recorded beside that constant. `0` is
|
|
289
289
|
// execa's own "no budget", kept for the one reader whose caller's writes depend on the answer
|
|
290
290
|
// (`gh-spawn.ts`).
|
|
291
|
-
timeout_ms?: number
|
|
291
|
+
timeout_ms?: number | undefined
|
|
292
292
|
// The checkout gh runs in, which is what `{owner}/{repo}` in `path` resolves against. Absent means
|
|
293
293
|
// the process's own directory.
|
|
294
294
|
cwd?: string
|
|
@@ -350,11 +350,14 @@ async function issue_get_labels_and_body(issue_number: string): Promise<string |
|
|
|
350
350
|
// No `repo` parameter, unlike the reads above: the auto-close lists its candidate epics from the
|
|
351
351
|
// repository it runs in, so the epic whose comments are read is always a local one. The path builder
|
|
352
352
|
// takes one when a caller that needs it appears.
|
|
353
|
-
async function issue_list_comments(
|
|
353
|
+
async function issue_list_comments(
|
|
354
|
+
issue_number: string,
|
|
355
|
+
timeout_ms?: number,
|
|
356
|
+
): Promise<string | undefined> {
|
|
354
357
|
const path = `${git_gh_api_path.issue_comments_api_path(issue_number)}${git_gh_api_path.FULL_PAGE_QUERY}`
|
|
355
358
|
|
|
356
359
|
try {
|
|
357
|
-
return await git_gh_exec.exec_gh_api({ path, should_paginate: true })
|
|
360
|
+
return await git_gh_exec.exec_gh_api({ path, should_paginate: true, timeout_ms })
|
|
358
361
|
} catch {
|
|
359
362
|
return undefined
|
|
360
363
|
}
|
|
@@ -436,5 +439,5 @@ const git_gh_issue_read = {
|
|
|
436
439
|
issue_list_comments,
|
|
437
440
|
}
|
|
438
441
|
|
|
439
|
-
export type { IssueRead, IssueReadFailure }
|
|
442
|
+
export type { IssueRead, IssueReadFailure, IssueReadReason }
|
|
440
443
|
export { git_gh_issue_read, read_blocked_by, NOT_FOUND_STATUS }
|
|
@@ -71,6 +71,7 @@ const REST_FIELD_NAMES = new Map<string, string>([
|
|
|
71
71
|
['url', 'html_url'],
|
|
72
72
|
['createdAt', 'created_at'],
|
|
73
73
|
['closedAt', 'closed_at'],
|
|
74
|
+
['updatedAt', 'updated_at'],
|
|
74
75
|
])
|
|
75
76
|
|
|
76
77
|
const NOT_AN_ISSUE_MESSAGE = 'gh api answered something other than an issue object'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { issue_watch_diff } from '#scripts/issue/issue-watch-diff'
|
|
1
2
|
import { session_cite } from '#scripts/issue/session-cite'
|
|
2
3
|
import { error_text } from '#scripts/lib/error-message'
|
|
3
4
|
import { git_gh_api_path } from './git-gh-api-path'
|
|
@@ -64,10 +65,13 @@ async function issue_edit_body(issue_number: string, body: string): Promise<stri
|
|
|
64
65
|
})
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// **Every comment josh posts carries `RUN_MARKER`**, an HTML comment GitHub does not render. A run
|
|
69
|
+
// posts under the person's own `gh` login, so the marker is the only thing that tells the issue watch
|
|
70
|
+
// (`issue-watch-diff.ts`) a run's comment from a person's update.
|
|
67
71
|
async function issue_comment(issue_number: string, body: string): Promise<string> {
|
|
68
72
|
return await git_gh_exec.exec_gh_api({
|
|
69
73
|
path: `${git_gh_api_path.issue_api_path(issue_number)}${COMMENTS_SEGMENT}`,
|
|
70
|
-
body: JSON.stringify({ body }),
|
|
74
|
+
body: JSON.stringify({ body: `${body}\n\n${issue_watch_diff.RUN_MARKER}` }),
|
|
71
75
|
jq_filter: HTML_URL_FILTER,
|
|
72
76
|
})
|
|
73
77
|
}
|
|
@@ -14,7 +14,10 @@ const NUMBER_AND_BODY_FIELDS = 'number,body'
|
|
|
14
14
|
// tell a row with no blockers from one with some **before** deciding whether that row is worth a
|
|
15
15
|
// per-issue read, which would otherwise be made for every open issue in the repository. Absent on a
|
|
16
16
|
// row whose summary is missing, which must not be read as zero; `git-gh-issue-rest.ts` states why.
|
|
17
|
-
|
|
17
|
+
//
|
|
18
|
+
// `labels` rides along on the same response too: `epic:bundle` reads the release label off it,
|
|
19
|
+
// because the release issue's blockers are recorded mechanically and signal no shared work.
|
|
20
|
+
const BACKLOG_SCAN_FIELDS = `number,title,body,labels,${BLOCKED_BY_COUNT_FIELD}`
|
|
18
21
|
// The recently-closed scan's fields. It compares titles like the scan above
|
|
19
22
|
// and never reads a body, and it needs the labels to tell an epic from a deliverable — the open half
|
|
20
23
|
// gets that from the epic listing, which by construction holds no closed epic.
|
|
@@ -147,6 +150,14 @@ async function issue_list_by_label_in_repo(
|
|
|
147
150
|
return await issue_list({ json_fields: SUMMARY_FIELDS, limit, label, repo })
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
// Every open issue with what `josh auto-ok:judge` judges it by — its labels and body — and `updatedAt`,
|
|
154
|
+
// so the backlog driver's resident check reads an issue's comments again only once it has moved.
|
|
155
|
+
const JUDGEMENT_FIELDS = 'number,title,body,labels,updatedAt'
|
|
156
|
+
|
|
157
|
+
async function issue_list_open_for_judgement(limit: number): Promise<IssueListOutcome> {
|
|
158
|
+
return await issue_list({ json_fields: JUDGEMENT_FIELDS, limit })
|
|
159
|
+
}
|
|
160
|
+
|
|
150
161
|
const git_gh_issue = {
|
|
151
162
|
...git_gh_issue_read,
|
|
152
163
|
...git_gh_issue_write,
|
|
@@ -156,6 +167,7 @@ const git_gh_issue = {
|
|
|
156
167
|
issue_list_by_label_summary,
|
|
157
168
|
issue_search_body,
|
|
158
169
|
issue_list_open_bodies,
|
|
170
|
+
issue_list_open_for_judgement,
|
|
159
171
|
issue_list_recently_closed,
|
|
160
172
|
}
|
|
161
173
|
|
|
@@ -119,6 +119,18 @@ function is_coderabbit_check(check_name: string): boolean {
|
|
|
119
119
|
return is_required_match(check_name, CODERABBIT_CHECK_NAME)
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// CodeRabbit reports a rate-limited review as a *passing* status whose description says so
|
|
123
|
+
// (`Review rate limited`), so only the description tells it from a finished review
|
|
124
|
+
// (joshuafolkken/kit#3770).
|
|
125
|
+
const CODERABBIT_RATE_LIMIT_DESCRIPTION = /rate limit/iu
|
|
126
|
+
|
|
127
|
+
function is_coderabbit_rate_limited(check: RollupCheck): boolean {
|
|
128
|
+
return (
|
|
129
|
+
is_coderabbit_check(check.name) &&
|
|
130
|
+
CODERABBIT_RATE_LIMIT_DESCRIPTION.test(check.description ?? '')
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
122
134
|
// Temporary: a pending or failing CodeRabbit check makes GitHub report UNSTABLE even
|
|
123
135
|
// though CodeRabbit is not required. Accept that state only when CodeRabbit checks are the
|
|
124
136
|
// sole non-passing ones — any other non-passing check keeps the gate strict.
|
|
@@ -249,6 +261,7 @@ export {
|
|
|
249
261
|
is_review_decision_decisive,
|
|
250
262
|
read_required_statuses,
|
|
251
263
|
is_coderabbit_check,
|
|
264
|
+
is_coderabbit_rate_limited,
|
|
252
265
|
collect_blocking_failures,
|
|
253
266
|
describe_pr_failure,
|
|
254
267
|
REQUIRED_CHECKS,
|
|
@@ -31,10 +31,14 @@ const KEY_STATUS = 'status'
|
|
|
31
31
|
const KEY_CONCLUSION = 'conclusion'
|
|
32
32
|
const KEY_NAME = 'name'
|
|
33
33
|
const KEY_CONTEXT = 'context'
|
|
34
|
+
const KEY_DESCRIPTION = 'description'
|
|
34
35
|
|
|
35
36
|
interface RollupCheck {
|
|
36
37
|
name: string
|
|
37
38
|
status: string
|
|
39
|
+
// A status context's own words — CodeRabbit says `Review rate limited` under a `success` state
|
|
40
|
+
// (joshuafolkken/kit#3770). Absent where the check carries none.
|
|
41
|
+
description?: string
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
interface PrStateSnapshot {
|
|
@@ -95,7 +99,10 @@ function parse_rollup_item(item: RollupItemData): RollupCheck | undefined {
|
|
|
95
99
|
const name = read_string(item[KEY_NAME]) ?? read_string(item[KEY_CONTEXT])
|
|
96
100
|
if (name === undefined) return undefined
|
|
97
101
|
|
|
98
|
-
|
|
102
|
+
const check = { name, status: parse_rollup_status(item) }
|
|
103
|
+
const description = read_string(item[KEY_DESCRIPTION])
|
|
104
|
+
|
|
105
|
+
return description === undefined ? check : { ...check, description }
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
function parse_rollup_checks(raw_json: string): Array<RollupCheck> {
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
default_fetch_pr_state,
|
|
5
5
|
DEFAULT_STABLE_READS,
|
|
6
6
|
is_pr_checks_timeout,
|
|
7
|
+
observed_fetcher,
|
|
7
8
|
SECONDS_TO_MS,
|
|
8
9
|
wait_for_pr_success,
|
|
9
10
|
type PrStateEvaluator,
|
|
11
|
+
type SnapshotObserver,
|
|
10
12
|
} from './git-pr-checks'
|
|
11
13
|
import { is_coderabbit_check, is_required_check, type PrEvaluation } from './git-pr-checks-eval'
|
|
12
14
|
import {
|
|
@@ -125,10 +127,12 @@ const CHECKS_SETTLED_EVALUATOR: PrStateEvaluator = {
|
|
|
125
127
|
should_read_review_decision: should_never_read_review_decision,
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
// `observe` sees the watch's polls too, so a red wait's checks line is replaced while the new CI is
|
|
131
|
+
// still pending rather than only after the watch (joshuafolkken/kit#3770).
|
|
132
|
+
async function watch_until_settled(branch_name: string, observe?: SnapshotObserver): Promise<void> {
|
|
129
133
|
await wait_for_pr_success({
|
|
130
134
|
branch_name,
|
|
131
|
-
fetcher:
|
|
135
|
+
fetcher: observed_fetcher(observe),
|
|
132
136
|
interval_ms: CHECK_WAIT_INTERVAL_MS,
|
|
133
137
|
max_attempts: compute_max_attempts(
|
|
134
138
|
PR_CHECKS_WATCH_TIMEOUT_MS / SECONDS_TO_MS,
|
|
@@ -154,9 +158,12 @@ async function fail_when_no_checks(branch_name: string): Promise<void> {
|
|
|
154
158
|
// Everything else — a failing check, an unreadable read, a branch with no checks — is rethrown, which
|
|
155
159
|
// is what the caller already handles: `git-pr-followup.ts` swallows it and falls through to its own
|
|
156
160
|
// poll.
|
|
157
|
-
async function pr_checks_watch(
|
|
161
|
+
async function pr_checks_watch(
|
|
162
|
+
branch_name: string,
|
|
163
|
+
observe?: SnapshotObserver,
|
|
164
|
+
): Promise<WatchResult> {
|
|
158
165
|
try {
|
|
159
|
-
await watch_until_settled(branch_name)
|
|
166
|
+
await watch_until_settled(branch_name, observe)
|
|
160
167
|
|
|
161
168
|
return { timed_out: false }
|
|
162
169
|
} catch (error) {
|
|
@@ -128,6 +128,9 @@ interface WaitForPrSuccessOptions {
|
|
|
128
128
|
max_attempts: number
|
|
129
129
|
required_stable_reads: number
|
|
130
130
|
evaluator?: PrStateEvaluator
|
|
131
|
+
// Asked before every poll; a throw ends the wait with that error. `josh followup` stops here on a
|
|
132
|
+
// person's unread issue update (`issue-watch-gate.ts`).
|
|
133
|
+
before_poll?: (() => Promise<void>) | undefined
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
function parse_repo_name_from_package(package_json_content: string): string {
|
|
@@ -272,6 +275,8 @@ async function wait_for_pr_success(options: WaitForPrSuccessOptions): Promise<Pr
|
|
|
272
275
|
|
|
273
276
|
for (let attempt = 0; attempt < options.max_attempts; attempt += 1) {
|
|
274
277
|
console.info(`Checking PR status… (${String(attempt + 1)}/${String(options.max_attempts)})`)
|
|
278
|
+
// eslint-disable-next-line no-await-in-loop -- polling: the check belongs to this poll, not to all of them at once
|
|
279
|
+
await options.before_poll?.()
|
|
275
280
|
// eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
|
|
276
281
|
carried = await attempt_pr_success_poll({
|
|
277
282
|
options,
|
|
@@ -311,16 +316,38 @@ async function default_fetch_pr_state(
|
|
|
311
316
|
// **`required_stable_reads` is a parameter, not a constant, so the caller can lower it** — `run_checks`
|
|
312
317
|
// passes `WATCH_CONFIRMED_STABLE_READS` when the watch already confirmed completion. It defaults to
|
|
313
318
|
// `DEFAULT_STABLE_READS`, so every caller that names only the branch is exactly where it was.
|
|
319
|
+
// What a caller is shown of each poll it waits on — `followup` reports the checks to the run's event
|
|
320
|
+
// stream from it (joshuafolkken/kit#3770). It decides nothing: the wait reads the same snapshot.
|
|
321
|
+
type SnapshotObserver = (snapshot: PrStateSnapshot) => Promise<void>
|
|
322
|
+
|
|
323
|
+
function observed_fetcher(observe: SnapshotObserver | undefined): PrStateFetcher {
|
|
324
|
+
if (observe === undefined) return default_fetch_pr_state
|
|
325
|
+
|
|
326
|
+
return async function fetch_observed(
|
|
327
|
+
branch_name: string,
|
|
328
|
+
should_read_review_decision: ReviewDecisionPredicate,
|
|
329
|
+
): Promise<PrStateSnapshot> {
|
|
330
|
+
const snapshot = await default_fetch_pr_state(branch_name, should_read_review_decision)
|
|
331
|
+
|
|
332
|
+
await observe(snapshot)
|
|
333
|
+
|
|
334
|
+
return snapshot
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
314
338
|
async function wait_for_pr_success_default(
|
|
315
339
|
branch_name: string,
|
|
316
340
|
required_stable_reads: number = DEFAULT_STABLE_READS,
|
|
341
|
+
before_poll?: () => Promise<void>,
|
|
342
|
+
observe?: SnapshotObserver,
|
|
317
343
|
): Promise<PrStateSnapshot> {
|
|
318
344
|
return await wait_for_pr_success({
|
|
319
345
|
branch_name,
|
|
320
|
-
fetcher:
|
|
346
|
+
fetcher: observed_fetcher(observe),
|
|
321
347
|
interval_ms: CHECK_WAIT_INTERVAL_MS,
|
|
322
348
|
max_attempts: CHECK_MAX_ATTEMPTS,
|
|
323
349
|
required_stable_reads,
|
|
350
|
+
before_poll,
|
|
324
351
|
})
|
|
325
352
|
}
|
|
326
353
|
|
|
@@ -383,10 +410,17 @@ export {
|
|
|
383
410
|
SECONDS_TO_MS,
|
|
384
411
|
is_pr_checks_timeout,
|
|
385
412
|
default_fetch_pr_state,
|
|
413
|
+
observed_fetcher,
|
|
386
414
|
MERGE_GATE_EVALUATOR,
|
|
387
415
|
should_always_read_review_decision,
|
|
388
416
|
}
|
|
389
|
-
export type {
|
|
417
|
+
export type {
|
|
418
|
+
MergeProgress,
|
|
419
|
+
PrStateFetcher,
|
|
420
|
+
PrStateEvaluator,
|
|
421
|
+
ReviewDecisionPredicate,
|
|
422
|
+
SnapshotObserver,
|
|
423
|
+
}
|
|
390
424
|
export {
|
|
391
425
|
collect_blocking_failures,
|
|
392
426
|
describe_pr_failure,
|