@joshuafolkken/kit 1.1387.0 → 1.1435.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-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
- 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 +288 -229
- 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 +62 -9
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- 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/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 +16 -8
- 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 +130 -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 +13 -2
- 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-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -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 +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +15 -0
- package/scripts/init/start-steps.ts +46 -0
- package/scripts/init/start.ts +1 -0
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- 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-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +24 -0
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- 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/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- 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/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- 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/run/board/run-board-track.ts +0 -22
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
1
2
|
import { execa } from 'execa'
|
|
3
|
+
import { pnpm_reach } from './pnpm-reach'
|
|
2
4
|
import { SUITE_TIMEOUT_MS } from './timeouts'
|
|
3
5
|
|
|
4
6
|
// One definition of "run a check, keep its output, never throw".
|
|
@@ -9,7 +11,7 @@ import { SUITE_TIMEOUT_MS } from './timeouts'
|
|
|
9
11
|
// (so one failing check does not abort its siblings), and a bound on how long a child may hang.
|
|
10
12
|
// They had the same code twice, which is how a fix to one of them missed the other.
|
|
11
13
|
|
|
12
|
-
const PNPM =
|
|
14
|
+
const { PNPM } = pnpm_reach
|
|
13
15
|
const FORCE_COLOR = '1'
|
|
14
16
|
const FAIL_EXIT_CODE = 1
|
|
15
17
|
|
|
@@ -35,6 +37,38 @@ interface BufferedProcessOptions {
|
|
|
35
37
|
timeout_ms?: number
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
// What execa reports about a child that produced no exit code. Declared here because execa's own
|
|
41
|
+
// result type does not carry `code` or `shortMessage` on the non-rejecting path, though both are
|
|
42
|
+
// there at runtime whenever the child did not exit by itself.
|
|
43
|
+
interface UnfinishedChild {
|
|
44
|
+
exitCode?: number | undefined
|
|
45
|
+
code?: string | undefined
|
|
46
|
+
shortMessage?: string | undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A missing working directory is reported with the same code as a missing executable, so the code
|
|
50
|
+
// alone would blame pnpm for a lane directory that was removed.
|
|
51
|
+
function is_pnpm_missing(result: UnfinishedChild, cwd: string | undefined): boolean {
|
|
52
|
+
return pnpm_reach.is_not_found(result) && existsSync(cwd ?? process.cwd())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Why a child has no exit code: it could not be started, it ran past its bound, or a signal ended
|
|
56
|
+
// it. The child cannot say so itself — in the first case there is no child — and without this a
|
|
57
|
+
// check that never ran reads as a check that failed with nothing to report.
|
|
58
|
+
function unfinished_reason(result: UnfinishedChild, cwd: string | undefined): Array<string> {
|
|
59
|
+
if (result.exitCode !== undefined) return []
|
|
60
|
+
const reason = [result.shortMessage ?? '']
|
|
61
|
+
if (is_pnpm_missing(result, cwd)) reason.push(pnpm_reach.NOT_ON_PATH_NOTICE)
|
|
62
|
+
|
|
63
|
+
return reason
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The reason follows whatever the child wrote before it stopped: a timed-out suite's last lines
|
|
67
|
+
// are the evidence, and the reason is what explains them.
|
|
68
|
+
function collect_output(all: string, reason: ReadonlyArray<string>): string {
|
|
69
|
+
return [all, ...reason].filter((part) => part !== '').join('\n')
|
|
70
|
+
}
|
|
71
|
+
|
|
38
72
|
async function run_buffered_process(
|
|
39
73
|
command_args: ReadonlyArray<string>,
|
|
40
74
|
options: BufferedProcessOptions = {},
|
|
@@ -58,7 +92,7 @@ async function run_buffered_process(
|
|
|
58
92
|
})
|
|
59
93
|
|
|
60
94
|
return {
|
|
61
|
-
output: result.all,
|
|
95
|
+
output: collect_output(result.all, unfinished_reason(result, options.cwd)),
|
|
62
96
|
exit_code: result.exitCode,
|
|
63
97
|
elapsed_ms: performance.now() - started_at,
|
|
64
98
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// One definition of "pnpm is not reachable from this process", for every caller that starts pnpm.
|
|
2
|
+
//
|
|
3
|
+
// `josh` runs its checks as `pnpm josh <check>` children, so a `pnpm` missing from the PATH this
|
|
4
|
+
// process sees fails every one of them before it starts. Nothing in the child's output can say so —
|
|
5
|
+
// there is no child — and the two places that can tell the user (the spawn itself and `josh doctor`)
|
|
6
|
+
// have to say the same thing.
|
|
7
|
+
|
|
8
|
+
const PNPM = 'pnpm'
|
|
9
|
+
// What Node reports when the executable to spawn is not on PATH.
|
|
10
|
+
const NOT_FOUND_CODE = 'ENOENT'
|
|
11
|
+
const PREREQUISITES_URL =
|
|
12
|
+
'https://github.com/joshuafolkken/kit/blob/main/docs/setup/prerequisites.md#2-install-pnpm-and-nodejs'
|
|
13
|
+
const NOT_ON_PATH_NOTICE = [
|
|
14
|
+
'pnpm was not found on the PATH this process sees, so the command could not start.',
|
|
15
|
+
'A shell opened by an editor, an agent or a service can have a different PATH from your terminal.',
|
|
16
|
+
`Install pnpm or add its directory to that PATH: ${PREREQUISITES_URL}`,
|
|
17
|
+
].join('\n')
|
|
18
|
+
|
|
19
|
+
// Narrowed to the one field it reads: execa's declared result type does not carry `code` on the
|
|
20
|
+
// non-rejecting path, though the value is there at runtime whenever the spawn itself failed.
|
|
21
|
+
function is_not_found(result: { code?: string | undefined }): boolean {
|
|
22
|
+
return result.code === NOT_FOUND_CODE
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pnpm_reach = { PNPM, NOT_ON_PATH_NOTICE, is_not_found }
|
|
26
|
+
|
|
27
|
+
export { pnpm_reach }
|
|
@@ -15,10 +15,14 @@
|
|
|
15
15
|
// `✔ verification gate passed` line to tell a green gate from the third-party warning bodies it
|
|
16
16
|
// forwards, so the pass icon is under the same rule as the failure icon: declared
|
|
17
17
|
// once, imported by the printer and the reader alike.
|
|
18
|
+
//
|
|
19
|
+
// **The warning icon is read as well.** It opens the gate's `not verified` verdict — a gate that ran no
|
|
20
|
+
// check — which `josh-verdict.ts` reads back, so it is declared here for the reason the other two are.
|
|
18
21
|
|
|
19
22
|
const FAIL_ICON = '✗'
|
|
20
23
|
const PASS_ICON = '✔'
|
|
24
|
+
const WARN_ICON = '⚠'
|
|
21
25
|
|
|
22
|
-
const status_icons = { FAIL_ICON, PASS_ICON }
|
|
26
|
+
const status_icons = { FAIL_ICON, PASS_ICON, WARN_ICON }
|
|
23
27
|
|
|
24
28
|
export { status_icons }
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
FAIL_EXIT_CODE,
|
|
9
9
|
type BufferedProcessResult,
|
|
10
10
|
} from '#scripts/lib/buffered-process'
|
|
11
|
+
import { SKIP_MARKER } from '#scripts/test/skip-marker'
|
|
11
12
|
import { cross_file_recheck, type CrossFileRecheckOptions } from './cross-file-recheck'
|
|
12
13
|
|
|
13
14
|
const PRETTIER_ARGS = ['exec', 'prettier', '--check', '.'] as const
|
|
@@ -67,6 +68,22 @@ async function run_checked_eslint(
|
|
|
67
68
|
return await run_eslint(eslint)
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function is_silent_pass(result: BufferedProcessResult): boolean {
|
|
72
|
+
return result.output === '' && !buffered_process.is_process_failed(result)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Only beside a skipped sibling (`project_checks.ran_notice`): a `lint` that skipped nothing keeps the
|
|
76
|
+
// output it always had.
|
|
77
|
+
function with_ran_notice(
|
|
78
|
+
check: string,
|
|
79
|
+
result: BufferedProcessResult,
|
|
80
|
+
sibling: BufferedProcessResult,
|
|
81
|
+
): BufferedProcessResult {
|
|
82
|
+
if (!sibling.output.includes(SKIP_MARKER) || !is_silent_pass(result)) return result
|
|
83
|
+
|
|
84
|
+
return { ...result, output: `${project_checks.ran_notice(check)}\n` }
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
function lint_exit_code(prettier: BufferedProcessResult, eslint: BufferedProcessResult): number {
|
|
71
88
|
return buffered_process.is_process_failed(prettier) || buffered_process.is_process_failed(eslint)
|
|
72
89
|
? FAIL_EXIT_CODE
|
|
@@ -94,8 +111,8 @@ async function run_lint_checks(
|
|
|
94
111
|
run_checked_eslint(directory, { args: eslint_args, recheck }),
|
|
95
112
|
])
|
|
96
113
|
|
|
97
|
-
write_output(prettier)
|
|
98
|
-
write_output(eslint)
|
|
114
|
+
write_output(with_ran_notice('prettier', prettier, eslint))
|
|
115
|
+
write_output(with_ran_notice('eslint', eslint, prettier))
|
|
99
116
|
|
|
100
117
|
return lint_exit_code(prettier, eslint)
|
|
101
118
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { issue_watch_gate } from '#scripts/issue/issue-watch-gate'
|
|
3
4
|
import { review_attest, type AttestVerdict } from './review-attest'
|
|
4
5
|
import { review_checkout } from './review-checkout'
|
|
5
6
|
|
|
@@ -51,6 +52,22 @@ async function run_attest(nonce: string): Promise<number> {
|
|
|
51
52
|
return FAILURE_EXIT_CODE
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
// An attested review still describes the issue as it read it: a person's update it has not taken in,
|
|
56
|
+
// or one taken in after the brief, makes its verdict stale (`issue-watch-gate.ts`).
|
|
57
|
+
async function report_issue_watch(verdict: AttestVerdict): Promise<number> {
|
|
58
|
+
const refusal = await issue_watch_gate.review_refusal_here()
|
|
59
|
+
|
|
60
|
+
if (refusal !== undefined) {
|
|
61
|
+
console.error(refusal)
|
|
62
|
+
|
|
63
|
+
return FAILURE_EXIT_CODE
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.info(attested_line(verdict))
|
|
67
|
+
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
// **`check_here`, not `check`** — the pointer is keyed on the repository root, and `check`'s default
|
|
55
72
|
// is `PROJECT_ROOT`, which is `process.cwd()`. Run from a subdirectory, or from the session checkout
|
|
56
73
|
// rather than the lane's, the two hash different keys: no pointer is found, the command prints "no
|
|
@@ -65,15 +82,13 @@ async function run_check(): Promise<number> {
|
|
|
65
82
|
return 0
|
|
66
83
|
}
|
|
67
84
|
|
|
68
|
-
if (verdict.status
|
|
69
|
-
console.
|
|
85
|
+
if (verdict.status !== 'ok') {
|
|
86
|
+
console.error(review_attest.refusal_message(verdict))
|
|
70
87
|
|
|
71
|
-
return
|
|
88
|
+
return FAILURE_EXIT_CODE
|
|
72
89
|
}
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return FAILURE_EXIT_CODE
|
|
91
|
+
return await report_issue_watch(verdict)
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
// Anything unrecognized is a usage error rather than a default. A misspelled flag that fell through
|
|
@@ -12,7 +12,7 @@ import type { FileMapStamp } from '#scripts/josh/file-map-stamp'
|
|
|
12
12
|
import { find_package_directory } from '#scripts/josh/josh-logic'
|
|
13
13
|
import { path_decision } from '#scripts/josh/path-decision'
|
|
14
14
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
15
|
-
import {
|
|
15
|
+
import { lane_session_broker } from '#scripts/lane/lane-session-broker'
|
|
16
16
|
import { run_ship_review } from '#scripts/run/ship/run-ship-review'
|
|
17
17
|
import { review_attest } from './review-attest'
|
|
18
18
|
import { review_brief } from './review-brief'
|
|
@@ -150,7 +150,7 @@ function record_round_one(
|
|
|
150
150
|
// was written by the brief the pointer names: one left by an earlier brief is not the tree the
|
|
151
151
|
// finished review read, so the resume reads the whole change instead.
|
|
152
152
|
function attested_tree(checkout: ReviewCheckout): FileMapStamp | undefined {
|
|
153
|
-
const { findings } =
|
|
153
|
+
const { findings } = lane_session_broker.review_paths(PROJECT_ROOT)
|
|
154
154
|
const briefed = review_stamps.briefed_stamp.read()
|
|
155
155
|
|
|
156
156
|
if (briefed === undefined || !review_attest.is_briefed_since(briefed.taken_at, checkout.root)) {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { gh_author_association } from '#scripts/gh/gh-author-association'
|
|
3
|
+
import { git_gh_exec } from '#scripts/gh/git-gh-exec'
|
|
4
|
+
import { git_gh_issue_read } from '#scripts/gh/git-gh-issue-read'
|
|
5
|
+
import { parse_json, parse_json_array_safe } from '#scripts/git/parse-json-array'
|
|
6
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
7
|
+
import { observation_ledger_home } from '#scripts/observations/observation-ledger-home'
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { review_finding_ledger } from './review-finding-ledger'
|
|
10
|
+
|
|
11
|
+
// A review round recorded after its pull request opened (`josh review:record --comment`), and how its
|
|
12
|
+
// lines reach the ledger's readers (joshuafolkken/kit#3811). The round is posted on the issue because
|
|
13
|
+
// a ledger line written then would be pushed onto the open pull request and restart its CI — and so
|
|
14
|
+
// nothing that reads the ledger (`review:findings`, the retrospective) ever saw it.
|
|
15
|
+
//
|
|
16
|
+
// **The next run that records a round carries it.** Once an issue is closed no round is added to it,
|
|
17
|
+
// so `review:record` copies each closed issue's comment rounds not yet carried into
|
|
18
|
+
// `<issue>-comment-rounds.md` in the ledger, and the lines ride that run's own commit: the ledger stays
|
|
19
|
+
// the one place a reader looks, and no push restarts any CI. **The file is the carried mark** — an
|
|
20
|
+
// issue whose file exists is never read again, and two lanes carrying one issue write the same file,
|
|
21
|
+
// so their pull requests merge to the same lines.
|
|
22
|
+
|
|
23
|
+
const COMMENT_HEADING_MARK = '### '
|
|
24
|
+
const COMMENT_TITLE = 'Review round recorded after the pull request opened'
|
|
25
|
+
const COMMENT_HEADING = `${COMMENT_HEADING_MARK}${COMMENT_TITLE}`
|
|
26
|
+
const COMMENT_NOTE =
|
|
27
|
+
'Kept here rather than in the observation ledger: a line appended after the commit would be pushed onto the open pull request and restart its CI.'
|
|
28
|
+
const CODE_FENCE = '```'
|
|
29
|
+
const CARRIED_SUFFIX = '-comment-rounds'
|
|
30
|
+
// One page of the newest closed issues: an issue leaves the search's interest once carried, so the
|
|
31
|
+
// ones still to carry are the recently closed.
|
|
32
|
+
const PER_PAGE = '100'
|
|
33
|
+
const UNNAMED_REPOSITORY = 'Could not name this repository from `git remote`.'
|
|
34
|
+
const UNREAD_SEARCH = 'Could not read the search for closed issues with a comment round.'
|
|
35
|
+
const UNREAD_COMMENTS = 'Could not read the comments of'
|
|
36
|
+
|
|
37
|
+
const search_item_schema = z.object({ number: z.number() })
|
|
38
|
+
const search_schema = z.object({ items: z.array(search_item_schema) })
|
|
39
|
+
const comment_schema = z.object({
|
|
40
|
+
body: z.string().nullish(),
|
|
41
|
+
author_association: z.string().nullish(),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
type IssueComment = z.infer<typeof comment_schema>
|
|
45
|
+
|
|
46
|
+
function body_of(lines: ReadonlyArray<string>): string {
|
|
47
|
+
return [COMMENT_HEADING, '', COMMENT_NOTE, '', CODE_FENCE, ...lines, CODE_FENCE].join('\n')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The ledger lines of every comment round among one issue's comments, oldest first; any other
|
|
51
|
+
// comment, one that only quotes the heading among them, carries none — nor does one whose author
|
|
52
|
+
// cannot write to the repository: anyone can comment on a public issue, and the carried lines ride
|
|
53
|
+
// the run's commit unreviewed.
|
|
54
|
+
function lines_of(comments: ReadonlyArray<IssueComment>): Array<string> {
|
|
55
|
+
return comments
|
|
56
|
+
.filter((comment) => gh_author_association.is_trusted(comment.author_association))
|
|
57
|
+
.map((comment) => comment.body ?? '')
|
|
58
|
+
.filter((body) => body.startsWith(COMMENT_HEADING))
|
|
59
|
+
.flatMap((body) => body.split('\n'))
|
|
60
|
+
.filter((line) => review_finding_ledger.is_finding_line(line))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function search_path(repo: string): string {
|
|
64
|
+
const query = `repo:${repo} is:issue is:closed in:comments "${COMMENT_TITLE}"`
|
|
65
|
+
const parameters = new URLSearchParams({ q: query, sort: 'updated', per_page: PER_PAGE })
|
|
66
|
+
|
|
67
|
+
return `search/issues?${parameters.toString()}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The closed issues whose comments name a comment round; a repository that cannot be named or a
|
|
71
|
+
// search that cannot be read throws, so the run says so rather than carrying nothing in silence.
|
|
72
|
+
async function closed_issues(): Promise<ReadonlyArray<number>> {
|
|
73
|
+
const repo = session_cite.session_repo()
|
|
74
|
+
|
|
75
|
+
if (repo === undefined) throw new Error(UNNAMED_REPOSITORY)
|
|
76
|
+
|
|
77
|
+
const raw = await git_gh_exec.exec_gh_api({ path: search_path(repo) })
|
|
78
|
+
const page = parse_json.parse_json_object_or_undefined(raw, search_schema)
|
|
79
|
+
|
|
80
|
+
if (page === undefined) throw new Error(UNREAD_SEARCH)
|
|
81
|
+
|
|
82
|
+
return page.items.map((item) => item.number)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function carried_path(issue: number, root: string): string {
|
|
86
|
+
return observation_ledger_home.file_path(`${String(issue)}${CARRIED_SUFFIX}`, root)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// An issue whose comments could not be read writes no file, so the next run reads it again, and
|
|
90
|
+
// throws so this one says so.
|
|
91
|
+
async function carry_issue(issue: number, root: string): Promise<number> {
|
|
92
|
+
const raw = await git_gh_issue_read.issue_list_comments(String(issue))
|
|
93
|
+
|
|
94
|
+
if (raw === undefined) throw new Error(`${UNREAD_COMMENTS} ${session_cite.issue(issue)}`)
|
|
95
|
+
|
|
96
|
+
const lines = lines_of(parse_json_array_safe(raw, comment_schema))
|
|
97
|
+
|
|
98
|
+
await observation_ledger_home.append(carried_path(issue, root), lines)
|
|
99
|
+
|
|
100
|
+
return lines.length
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Copies every closed issue's comment rounds not yet in the ledger, and answers how many lines.
|
|
104
|
+
async function carry(root: string): Promise<number> {
|
|
105
|
+
const issues = await closed_issues()
|
|
106
|
+
const pending = issues.filter((issue) => !existsSync(carried_path(issue, root)))
|
|
107
|
+
const counts = await Promise.all(pending.map(async (issue) => await carry_issue(issue, root)))
|
|
108
|
+
|
|
109
|
+
return counts.reduce((total, count) => total + count, 0)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const review_comment_round = { body_of, carried_path, carry, lines_of }
|
|
113
|
+
|
|
114
|
+
export { review_comment_round }
|
|
@@ -4,6 +4,7 @@ import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
|
|
|
4
4
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
5
5
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
6
6
|
import { observation_ledger_home } from '#scripts/observations/observation-ledger-home'
|
|
7
|
+
import { review_comment_round } from './review-comment-round'
|
|
7
8
|
import { review_finding_ledger, type Finding } from './review-finding-ledger'
|
|
8
9
|
import { review_record, type RecordVerdict } from './review-record'
|
|
9
10
|
|
|
@@ -16,7 +17,8 @@ import { review_record, type RecordVerdict } from './review-record'
|
|
|
16
17
|
// **`--comment` records a round that ran after the pull request opened**: the same lines go to the
|
|
17
18
|
// issue as a comment and nothing is appended. A line written to the tree then has no commit of the
|
|
18
19
|
// run's left to ride, so `pnpm josh followup` would push it onto the open pull request and its CI
|
|
19
|
-
// would start over.
|
|
20
|
+
// would start over. The next round recorded to the ledger carries it there once the issue is closed
|
|
21
|
+
// (`review-comment-round.ts`).
|
|
20
22
|
//
|
|
21
23
|
// `josh review:record --check --issue <N>` is the read half `pnpm josh followup` runs before it
|
|
22
24
|
// merges: `ok` / `not-required` exit zero, `missing` exits non-zero so the
|
|
@@ -31,10 +33,10 @@ const CHECK_USAGE = 'Usage: josh review:record --check --issue <N>'
|
|
|
31
33
|
const RECORDED_LINE = 'Recorded: the review round for this issue is in the ledger.'
|
|
32
34
|
const NOT_REQUIRED_LINE =
|
|
33
35
|
'No review-finding ledger is kept in this checkout, so there is nothing to check.'
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const CARRIED_LINE =
|
|
37
|
+
'review-finding line(s) recorded on closed issues after their pull requests opened into the ledger.'
|
|
38
|
+
const CARRY_FAILED_LINE =
|
|
39
|
+
'Could not carry the closed issues’ comment rounds into the ledger; the next round retries:'
|
|
38
40
|
const OPTIONS = {
|
|
39
41
|
issue: { type: 'string' },
|
|
40
42
|
check: { type: 'boolean' },
|
|
@@ -161,15 +163,14 @@ async function run_check(issue: string | undefined, root: string): Promise<numbe
|
|
|
161
163
|
return FAILURE_EXIT_CODE
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
function comment_body(lines: ReadonlyArray<string>): string {
|
|
165
|
-
return [COMMENT_HEADING, '', COMMENT_NOTE, '', CODE_FENCE, ...lines, CODE_FENCE].join('\n')
|
|
166
|
-
}
|
|
167
|
-
|
|
168
166
|
// **A comment that could not be posted fails the record**: it is the round's only copy, so a
|
|
169
167
|
// swallowed refusal would read as a recorded round that exists nowhere.
|
|
170
168
|
async function post_comment(issue: number, lines: ReadonlyArray<string>): Promise<number> {
|
|
171
169
|
try {
|
|
172
|
-
const url = await git_gh_issue_write.issue_comment(
|
|
170
|
+
const url = await git_gh_issue_write.issue_comment(
|
|
171
|
+
String(issue),
|
|
172
|
+
review_comment_round.body_of(lines),
|
|
173
|
+
)
|
|
173
174
|
|
|
174
175
|
console.info(`Recorded ${String(lines.length)} review-finding line(s) in ${url.trim()}.`)
|
|
175
176
|
|
|
@@ -181,8 +182,35 @@ async function post_comment(issue: number, lines: ReadonlyArray<string>): Promis
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
// The closed issues' comment rounds ride this round's commit (`review-comment-round.ts`). **A carry
|
|
186
|
+
// that fails never fails the record**: this round is recorded, and the comment rounds wait for the
|
|
187
|
+
// next run's.
|
|
188
|
+
async function carry_comment_rounds(root: string): Promise<void> {
|
|
189
|
+
try {
|
|
190
|
+
const count = await review_comment_round.carry(root)
|
|
191
|
+
|
|
192
|
+
if (count > 0) console.info(`Carried ${String(count)} ${CARRIED_LINE}`)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.warn(`${CARRY_FAILED_LINE} ${error instanceof Error ? error.message : String(error)}`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
184
198
|
// The round lands in its own issue's file, so two lanes recording at once
|
|
185
199
|
// write two files, and their pull requests never conflict on the ledger.
|
|
200
|
+
async function append_round(
|
|
201
|
+
issue: number,
|
|
202
|
+
lines: ReadonlyArray<string>,
|
|
203
|
+
root: string,
|
|
204
|
+
): Promise<number> {
|
|
205
|
+
const ledger_path = observation_ledger_home.issue_path(issue, root)
|
|
206
|
+
|
|
207
|
+
await observation_ledger_home.append(ledger_path, lines)
|
|
208
|
+
console.info(confirmation(lines.length, ledger_path))
|
|
209
|
+
await carry_comment_rounds(root)
|
|
210
|
+
|
|
211
|
+
return 0
|
|
212
|
+
}
|
|
213
|
+
|
|
186
214
|
async function run_record(parsed: Parsed, now: Date, root: string): Promise<number> {
|
|
187
215
|
const request = to_request(parsed)
|
|
188
216
|
|
|
@@ -196,12 +224,7 @@ async function run_record(parsed: Parsed, now: Date, root: string): Promise<numb
|
|
|
196
224
|
|
|
197
225
|
if (parsed.comment) return await post_comment(request.issue, lines)
|
|
198
226
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
await observation_ledger_home.append(ledger_path, lines)
|
|
202
|
-
console.info(confirmation(lines.length, ledger_path))
|
|
203
|
-
|
|
204
|
-
return 0
|
|
227
|
+
return await append_round(request.issue, lines, root)
|
|
205
228
|
}
|
|
206
229
|
|
|
207
230
|
// **The default is the work tree the command runs in, a lane's inside a lane**.
|
|
@@ -232,7 +255,6 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
|
232
255
|
const review_record_cli = {
|
|
233
256
|
accepted_values,
|
|
234
257
|
build_lines,
|
|
235
|
-
comment_body,
|
|
236
258
|
parse_finding,
|
|
237
259
|
run,
|
|
238
260
|
usage_text,
|
|
@@ -176,9 +176,9 @@ const IMPLEMENTATION_CUT_REASON =
|
|
|
176
176
|
'because the verdict was only ever read at session entry, where the context has not yet grown. The ' +
|
|
177
177
|
"verdict is `pnpm josh cost --cut`'s exactly — the same per-request billed-input statistic against the " +
|
|
178
178
|
'same threshold, never a second measurement. Issue `pnpm josh run:cut --impl <N> --handoff <path>` now and read the ' +
|
|
179
|
-
'verdict: on `cut`,
|
|
180
|
-
'continues implementing, so it must not be waited for
|
|
181
|
-
'
|
|
179
|
+
'verdict: on `cut`, read what the command printed. Where it relaunched a fresh process, **end the turn ' +
|
|
180
|
+
'immediately** — that process owns the run and continues implementing, so it must not be waited for, ' +
|
|
181
|
+
'and **no Telegram is owed**: a cut that resumes itself leaves a person nothing to act on. Where it printed that nothing was relaunched, do what it prints, then end the turn; on `not-a-lane`, `unready`, `busy`, `failed` or ' +
|
|
182
182
|
'`unknown`, this process carries the run on and the edit is simply the next call. Never relaunch a ' +
|
|
183
183
|
'second process after `busy`. This fired because the dispatch mark names this lane or the run hold ' +
|
|
184
184
|
'names this issue; a person working here carries no mark and holds no run, so sees no refusal, so there is no human-or-child judgement left to make. The ' +
|
|
@@ -172,7 +172,7 @@ const PRE_GATE_CUT_REASON =
|
|
|
172
172
|
'176K of 204K output riding across that boundary, and joshuafolkken/kit#1864 measured the step ' +
|
|
173
173
|
'itself firing 0 times in 6 lane children while it was carried as prose. Issue ' +
|
|
174
174
|
'`pnpm josh run:cut <N>` now, and read the verdict: on `cut`, **end the turn immediately** — the ' +
|
|
175
|
-
'fresh process owns the run from there and must not be waited for; on `not-a-lane`, `unready`, ' +
|
|
175
|
+
'fresh process owns the run from there and must not be waited for, and **no Telegram is owed** for a cut that resumes itself; on `not-a-lane`, `unready`, ' +
|
|
176
176
|
'`busy`, `failed` or `unknown`, this process carries the run on and the gate is simply the next ' +
|
|
177
177
|
'call. Never relaunch a second process after `busy`. This fired because the dispatch mark names ' +
|
|
178
178
|
'this lane; a person working here carries no mark and sees no refusal, so there is no ' +
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { run_event_checks, type PrChecks } from '#scripts/run/event/run-event-checks'
|
|
2
|
+
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
3
|
+
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
4
|
+
|
|
5
|
+
// Each child's newest `pr-checks` line — its pull request's CI counts and CodeRabbit's state as
|
|
6
|
+
// `josh followup` last polled them (joshuafolkken/kit#3770). Read from the stream alone, so a redraw
|
|
7
|
+
// asks GitHub nothing.
|
|
8
|
+
|
|
9
|
+
interface HeldChecks {
|
|
10
|
+
checks: PrChecks
|
|
11
|
+
at_ms: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { PR_CHECKS } = run_event_stream.EVENT_KIND
|
|
15
|
+
|
|
16
|
+
function held_of(event: RunEvent): HeldChecks | undefined {
|
|
17
|
+
const checks = event.kind === PR_CHECKS ? run_event_checks.checks_of(event.text) : undefined
|
|
18
|
+
|
|
19
|
+
return checks === undefined ? undefined : { checks, at_ms: Date.parse(event.at) }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The stream's bound reorders its lines, so the newest is the latest `at`, not the last line.
|
|
23
|
+
function is_newer(held: HeldChecks, current: HeldChecks | undefined): boolean {
|
|
24
|
+
return current === undefined || held.at_ms >= current.at_ms
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function newest_of(events: ReadonlyArray<RunEvent>): ReadonlyMap<number, PrChecks> {
|
|
28
|
+
const newest = new Map<number, HeldChecks>()
|
|
29
|
+
|
|
30
|
+
for (const event of events) {
|
|
31
|
+
const held = held_of(event)
|
|
32
|
+
const issue = Number(run_event_scope.issue_named(event))
|
|
33
|
+
|
|
34
|
+
if (held !== undefined && is_newer(held, newest.get(issue))) newest.set(issue, held)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return new Map([...newest].map(([issue, held]) => [issue, held.checks]))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const run_board_checks = { newest_of }
|
|
41
|
+
|
|
42
|
+
export { run_board_checks }
|
|
@@ -15,6 +15,7 @@ import { run_board_read } from './run-board-read'
|
|
|
15
15
|
import { run_board_screen, type Screen } from './run-board-screen'
|
|
16
16
|
import { run_board_spin } from './run-board-spin'
|
|
17
17
|
import type { BoardPorts } from './run-board-state'
|
|
18
|
+
import { run_board_thanks_read } from './run-board-thanks-read'
|
|
18
19
|
import { run_board_tick } from './run-board-tick'
|
|
19
20
|
import { run_board_usage_read } from './run-board-usage-read'
|
|
20
21
|
|
|
@@ -52,6 +53,7 @@ const LIVE_PORTS: BoardPorts = {
|
|
|
52
53
|
read_machine: machine_capacity.read_sample,
|
|
53
54
|
read_usage: run_board_usage_read.usage_reader(),
|
|
54
55
|
read_closed: async (issues) => await run_board_closed.read_all(issues),
|
|
56
|
+
read_thanks: async () => await run_board_thanks_read.read_thanks(),
|
|
55
57
|
now: () => Date.now(),
|
|
56
58
|
write: (frame) => process.stdout.write(frame),
|
|
57
59
|
// No fallback: a terminal that opens no link, or a pipe, gets the bare number rather than a URL.
|
|
@@ -15,7 +15,8 @@ interface ChatFrame {
|
|
|
15
15
|
local: LocalRead | undefined
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// One frame in the chat's form, with every escape stripped because a chat shows them as text.
|
|
18
|
+
// One frame in the chat's form, with every escape stripped because a chat shows them as text. The
|
|
19
|
+
// reviewers are thanked on the terminal's board alone, so the chat reads no roster.
|
|
19
20
|
async function chat_frame(ports: BoardPorts): Promise<ChatFrame> {
|
|
20
21
|
let text = ''
|
|
21
22
|
const chat_ports: BoardPorts = {
|
|
@@ -23,6 +24,7 @@ async function chat_frame(ports: BoardPorts): Promise<ChatFrame> {
|
|
|
23
24
|
is_tty: false,
|
|
24
25
|
size: undefined,
|
|
25
26
|
form: 'chat',
|
|
27
|
+
read_thanks: undefined,
|
|
26
28
|
write: (frame) => {
|
|
27
29
|
text += stripVTControlCharacters(frame)
|
|
28
30
|
},
|
|
@@ -5,14 +5,17 @@ import { run_board_labels } from './run-board-labels'
|
|
|
5
5
|
// A live `run:board` frame kept within the terminal. A frame taller than the pane scrolls the
|
|
6
6
|
// alternate screen as it is written, and the header — the lines that say whether the run moves and
|
|
7
7
|
// how far it is — is what scrolls off. So the parts below it give way instead, least wanted first:
|
|
8
|
-
// the footer, the legend, the
|
|
9
|
-
//
|
|
8
|
+
// the footer, the legend, the oldest notes until three are left, the finished rows, the rest of the
|
|
9
|
+
// notes, the reviewers thanked above the legend, the rows not yet started, the parked rows and, last,
|
|
10
|
+
// the running rows — what a person keeps the board open to see. Each stage stops once the
|
|
10
11
|
// frame fits, a cut row is counted on its section's `more N` line, and the order a row is drawn in
|
|
11
12
|
// never changes. A line wider than the pane wraps, so every line is counted by the rows it takes on
|
|
12
13
|
// screen, not as one.
|
|
13
14
|
|
|
14
15
|
const { WORDS } = run_board_labels
|
|
15
16
|
const INDENT = ' '
|
|
17
|
+
// The newest notes kept until the finished rows are gone; the older ones give way before them.
|
|
18
|
+
const NOTES_KEPT = 3
|
|
16
19
|
|
|
17
20
|
interface TerminalSize {
|
|
18
21
|
rows: number
|
|
@@ -52,21 +55,24 @@ interface FrameParts {
|
|
|
52
55
|
above: ReadonlyArray<string>
|
|
53
56
|
plan: ReadonlyArray<PlanSection>
|
|
54
57
|
notes: NotesPart
|
|
58
|
+
// The reviewers thanked, drawn right above the legend; none where nobody is.
|
|
59
|
+
thanks?: ReadonlyArray<string> | undefined
|
|
55
60
|
legend: ReadonlyArray<string>
|
|
56
61
|
footer: ReadonlyArray<string>
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
// One thing that gives way: a part as a whole, one note (the oldest shown), or one issue row.
|
|
60
|
-
type Yielder = 'footer' | 'legend' | 'note' | PlanLine
|
|
65
|
+
type Yielder = 'thanks' | 'footer' | 'legend' | 'note' | PlanLine
|
|
61
66
|
|
|
62
67
|
interface Cut {
|
|
68
|
+
thanks: boolean
|
|
63
69
|
footer: boolean
|
|
64
70
|
legend: boolean
|
|
65
71
|
notes: number
|
|
66
72
|
rows: ReadonlySet<PlanLine>
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
const NO_CUT: Cut = { footer: false, legend: false, notes: 0, rows: new Set() }
|
|
75
|
+
const NO_CUT: Cut = { thanks: false, footer: false, legend: false, notes: 0, rows: new Set() }
|
|
70
76
|
|
|
71
77
|
// The columns a line takes on screen: its color escapes take none, an emoji two.
|
|
72
78
|
function width_of(line: string): number {
|
|
@@ -117,6 +123,7 @@ function compose(parts: FrameParts, cut: Cut): Array<string> {
|
|
|
117
123
|
...parts.above,
|
|
118
124
|
...plan_lines(parts.plan, cut.rows),
|
|
119
125
|
...notes_lines(parts.notes, cut.notes),
|
|
126
|
+
...(cut.thanks ? [] : (parts.thanks ?? [])),
|
|
120
127
|
...(cut.legend ? [] : parts.legend),
|
|
121
128
|
...(cut.footer ? [] : parts.footer),
|
|
122
129
|
]
|
|
@@ -154,12 +161,15 @@ function order_of(parts: FrameParts): Array<Yielder> {
|
|
|
154
161
|
.map((line, index) => ({ line, index }))
|
|
155
162
|
.filter((entry) => entry.line.row !== undefined)
|
|
156
163
|
const notes = parts.notes.lines.map((): Yielder => 'note')
|
|
164
|
+
const surplus = Math.max(0, notes.length - NOTES_KEPT)
|
|
157
165
|
|
|
158
166
|
return [
|
|
159
167
|
'footer',
|
|
160
168
|
'legend',
|
|
169
|
+
...notes.slice(0, surplus),
|
|
161
170
|
...yielding(rows, 'finished'),
|
|
162
|
-
...notes,
|
|
171
|
+
...notes.slice(surplus),
|
|
172
|
+
'thanks',
|
|
163
173
|
...yielding(rows, 'pending'),
|
|
164
174
|
...yielding(rows, 'parked'),
|
|
165
175
|
...yielding(rows, 'running'),
|
|
@@ -175,6 +185,7 @@ function cut_of(order: ReadonlyArray<Yielder>, count: number): Cut {
|
|
|
175
185
|
const gone = order.slice(0, count)
|
|
176
186
|
|
|
177
187
|
return {
|
|
188
|
+
thanks: gone.includes('thanks'),
|
|
178
189
|
footer: gone.includes('footer'),
|
|
179
190
|
legend: gone.includes('legend'),
|
|
180
191
|
notes: gone.filter((item) => item === 'note').length,
|