@joshuafolkken/kit 1.1383.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-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
- package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
- package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
- package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
- package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
- 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 +262 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +124 -15
- package/dist/hooks/stop-guard.js +98 -55
- package/dist/josh.js +63 -10
- 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-cli.ts +2 -4
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +5 -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-owner.ts +72 -5
- 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 +80 -5
- 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 +30 -4
- package/scripts/init/start-steps.ts +77 -3
- package/scripts/init/start.ts +21 -13
- 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 +25 -1
- 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 +139 -37
- 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 +58 -5
- 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 +121 -0
- package/scripts/run/board/run-board-span.ts +49 -7
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +76 -13
- 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 +53 -16
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-added.ts +1 -1
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-change.ts +1 -1
- package/scripts/run/carry/run-carry-cli.ts +39 -52
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry-ended.ts +9 -1
- package/scripts/run/carry/run-carry.ts +24 -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 +3 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +91 -0
- package/scripts/run/event/run-event-stream.ts +46 -9
- 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/run-stranded-detect.ts +58 -12
- 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 +108 -74
- package/scripts/run/wake/run-wake-describe.ts +36 -2
- package/scripts/run/wake/run-wake-driver.ts +21 -5
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
- 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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { process_identity } from '#scripts/josh/process-identity'
|
|
2
|
+
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
3
|
+
import { run_carry, type CarryRead, type RunCarry } from '#scripts/run/carry/run-carry'
|
|
4
|
+
import { run_carry_end } from '#scripts/run/carry/run-carry-end'
|
|
5
|
+
import { run_wake } from '#scripts/run/wake/run-wake'
|
|
6
|
+
import { run_wake_stop } from '#scripts/run/wake/run-wake-stop'
|
|
7
|
+
import { backlogrun_launch } from './backlogrun-launch'
|
|
8
|
+
|
|
9
|
+
// `josh backlogrun --stop` — the order a `backlogrun` is stopped in, and what is said of it. Each stop
|
|
10
|
+
// is the one its own command already makes (`run:wake --stop`, `run:carry --end --stopped`); only the
|
|
11
|
+
// order is here.
|
|
12
|
+
//
|
|
13
|
+
// **Supervisor, then parent, then record.** A record ended first is one a supervisor or a parent still
|
|
14
|
+
// alive can write back, and a supervisor still watching would wake a session over the run just ended.
|
|
15
|
+
//
|
|
16
|
+
// **Only what `josh backlogrun` started is stopped.** A run a person typed into a session of their own
|
|
17
|
+
// is owned by that session; its record is kept and the owner is named, because ending a record under a
|
|
18
|
+
// live owner leaves a session running lanes nothing counts.
|
|
19
|
+
//
|
|
20
|
+
// **A headless session is the run's own.** Nobody can end the run from it, so when it holds the record
|
|
21
|
+
// it is stopped too. The record says so itself (`is_owner_headless`): the supervisor's record is gone
|
|
22
|
+
// after a `run:wake --stop` or a supervisor that gave up, and the session it woke outlives both.
|
|
23
|
+
//
|
|
24
|
+
// **Lanes are counted, never stopped**: a child killed mid-change leaves a tree nobody can resume.
|
|
25
|
+
|
|
26
|
+
const STOPPED_REASON = 'stopped by a person with `josh backlogrun --stop`'
|
|
27
|
+
const PREFIX = 'backlogrun:'
|
|
28
|
+
const ISSUE_PREFIX = '#'
|
|
29
|
+
|
|
30
|
+
type RecordOutcome = { kind: 'none' } | { kind: 'ended' } | { kind: 'held'; owner: number }
|
|
31
|
+
|
|
32
|
+
interface StopReport {
|
|
33
|
+
// The pids stopped, `undefined` where there was nothing to stop.
|
|
34
|
+
supervisor: number | undefined
|
|
35
|
+
parent: number | undefined
|
|
36
|
+
// The headless session that held the record.
|
|
37
|
+
session: number | undefined
|
|
38
|
+
record: RecordOutcome
|
|
39
|
+
// The issues whose lanes are still open, left running.
|
|
40
|
+
lanes: ReadonlyArray<string>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface StopPorts {
|
|
44
|
+
stop_supervisor: (git_directory: string) => number | undefined
|
|
45
|
+
stop_parent: (git_directory: string) => number | undefined
|
|
46
|
+
stop_session: (pid: number) => void
|
|
47
|
+
read_carry: (git_directory: string) => CarryRead
|
|
48
|
+
is_live: (pid: number, start: string | undefined) => boolean
|
|
49
|
+
end_record: (git_directory: string) => Promise<CarryRead>
|
|
50
|
+
lanes: () => Promise<Array<string>>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The issues whose lanes are open — every one a stop leaves on disk, a child in it or not.
|
|
54
|
+
async function open_lanes(): Promise<Array<string>> {
|
|
55
|
+
const lanes = await lane_registry.list_lanes()
|
|
56
|
+
|
|
57
|
+
return lanes.filter((lane) => !lane.is_stranded).map((lane) => lane.issue)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A person who typed the stop is at the terminal, so the ⏸️ confirmation a stop pushes is not sent.
|
|
61
|
+
const LIVE_PORTS: StopPorts = {
|
|
62
|
+
stop_supervisor: (git_directory) => run_wake_stop.stop(run_wake.wake_path(git_directory))?.pid,
|
|
63
|
+
stop_parent: backlogrun_launch.stop_parent,
|
|
64
|
+
stop_session: run_wake_stop.signal,
|
|
65
|
+
read_carry: (git_directory) => run_carry.read_carry(run_carry.carry_path(git_directory)),
|
|
66
|
+
is_live: (pid, start) => process_identity.is_same_process(pid, start) !== false,
|
|
67
|
+
end_record: async (git_directory) =>
|
|
68
|
+
await run_carry_end.end_run(git_directory, { stopped: STOPPED_REASON, is_announced: false }),
|
|
69
|
+
lanes: open_lanes,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function live_owner(carry: RunCarry, ports: StopPorts): number | undefined {
|
|
73
|
+
const { owner_pid, owner_start } = carry
|
|
74
|
+
|
|
75
|
+
if (owner_pid === undefined) return undefined
|
|
76
|
+
|
|
77
|
+
return ports.is_live(owner_pid, owner_start) ? owner_pid : undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The live owner this command did not just stop.
|
|
81
|
+
function standing_owner(
|
|
82
|
+
read: CarryRead,
|
|
83
|
+
stopped: ReadonlyArray<number | undefined>,
|
|
84
|
+
ports: StopPorts,
|
|
85
|
+
): number | undefined {
|
|
86
|
+
const owner = read.kind === 'carried' ? live_owner(read.carry, ports) : undefined
|
|
87
|
+
|
|
88
|
+
return stopped.includes(owner) ? undefined : owner
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function is_headless_owner(read: CarryRead): boolean {
|
|
92
|
+
return read.kind === 'carried' && read.carry.is_owner_headless === true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Stops the owner where the record marks it headless — the pid stopped, `undefined` where the owner
|
|
96
|
+
// is any other process, which this command has no business signalling.
|
|
97
|
+
function stop_headless(
|
|
98
|
+
owner: number | undefined,
|
|
99
|
+
read: CarryRead,
|
|
100
|
+
ports: StopPorts,
|
|
101
|
+
): number | undefined {
|
|
102
|
+
if (owner === undefined || !is_headless_owner(read)) return undefined
|
|
103
|
+
|
|
104
|
+
ports.stop_session(owner)
|
|
105
|
+
|
|
106
|
+
return owner
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function close_record(
|
|
110
|
+
git_directory: string,
|
|
111
|
+
read: CarryRead,
|
|
112
|
+
holder: number | undefined,
|
|
113
|
+
ports: StopPorts,
|
|
114
|
+
): Promise<RecordOutcome> {
|
|
115
|
+
if (read.kind === 'none') return { kind: 'none' }
|
|
116
|
+
if (holder !== undefined) return { kind: 'held', owner: holder }
|
|
117
|
+
|
|
118
|
+
await ports.end_record(git_directory)
|
|
119
|
+
|
|
120
|
+
return { kind: 'ended' }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function stop(git_directory: string, ports: StopPorts = LIVE_PORTS): Promise<StopReport> {
|
|
124
|
+
const supervisor = ports.stop_supervisor(git_directory)
|
|
125
|
+
const parent = ports.stop_parent(git_directory)
|
|
126
|
+
const read = ports.read_carry(git_directory)
|
|
127
|
+
const owner = standing_owner(read, [supervisor, parent], ports)
|
|
128
|
+
const session = stop_headless(owner, read, ports)
|
|
129
|
+
const holder = session === undefined ? owner : undefined
|
|
130
|
+
const record = await close_record(git_directory, read, holder, ports)
|
|
131
|
+
const lanes = await ports.lanes()
|
|
132
|
+
|
|
133
|
+
return { supervisor, parent, session, record, lanes }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function process_line(what: string, pid: number | undefined): Array<string> {
|
|
137
|
+
return pid === undefined ? [] : [`${PREFIX} stopped the ${what} (process ${String(pid)})`]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function record_lines(outcome: RecordOutcome): Array<string> {
|
|
141
|
+
if (outcome.kind === 'none') return []
|
|
142
|
+
if (outcome.kind === 'ended') return [`${PREFIX} ended the run's record`]
|
|
143
|
+
|
|
144
|
+
return [
|
|
145
|
+
`${PREFIX} process ${String(outcome.owner)} owns the run and was not started by \`josh backlogrun\`; its record is kept — end the run from that session`,
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function lane_lines(lanes: ReadonlyArray<string>): Array<string> {
|
|
150
|
+
if (lanes.length === 0) return []
|
|
151
|
+
|
|
152
|
+
const issues = lanes.map((issue) => `${ISSUE_PREFIX}${issue}`).join(' ')
|
|
153
|
+
|
|
154
|
+
return [`${PREFIX} ${String(lanes.length)} lane(s) still running, not stopped: ${issues}`]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// What a person reads back: one line per thing done, or the one line that says nothing was running.
|
|
158
|
+
function lines_of(report: StopReport): Array<string> {
|
|
159
|
+
const done = [
|
|
160
|
+
...process_line('supervisor', report.supervisor),
|
|
161
|
+
...process_line('parent session', report.parent),
|
|
162
|
+
...process_line('headless session', report.session),
|
|
163
|
+
...record_lines(report.record),
|
|
164
|
+
]
|
|
165
|
+
const stopped = done.length === 0 ? [`${PREFIX} nothing is running`] : done
|
|
166
|
+
|
|
167
|
+
return [...stopped, ...lane_lines(report.lanes)]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// A record left with its owner is a run this command did not stop.
|
|
171
|
+
function is_stopped(report: StopReport): boolean {
|
|
172
|
+
return report.record.kind !== 'held'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const backlogrun_stop = { is_stopped, lines_of, stop }
|
|
176
|
+
|
|
177
|
+
export type { StopPorts, StopReport }
|
|
178
|
+
export { backlogrun_stop }
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { cost_transcript, type SessionFile } from '#scripts/cost-runtime/cost-transcript'
|
|
4
4
|
import { status_icons } from '#scripts/lib/status-icons'
|
|
5
|
+
import { NOTHING_TO_CHECK_MARKER } from '#scripts/test/skip-marker'
|
|
5
6
|
import { behavior_assertion, type Violation } from './behavior-assertion'
|
|
6
7
|
import { behavior_rules } from './behavior-rules'
|
|
7
8
|
|
|
@@ -32,8 +33,12 @@ function current_session(cwd: string = process.cwd()): SessionFile | undefined {
|
|
|
32
33
|
// **A run with no transcript passes, and passes without the skip marker.** The gate reads `—
|
|
33
34
|
// skipping` to withhold its green record; a behavior check that found no transcript — a CI runner has
|
|
34
35
|
// none — has nothing to say about the tree and must not withhold the record the other checks earned.
|
|
36
|
+
// It carries the nothing-to-check marker instead, so the gate's verdict does not count it as a check
|
|
37
|
+
// that ran.
|
|
35
38
|
function report_no_transcript(): number {
|
|
36
|
-
console.info(
|
|
39
|
+
console.info(
|
|
40
|
+
`${LABEL}: no recorded transcript — ${NOTHING_TO_CHECK_MARKER} ${status_icons.PASS_ICON}`,
|
|
41
|
+
)
|
|
37
42
|
|
|
38
43
|
return PASS_EXIT_CODE
|
|
39
44
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
4
|
+
import { pnpm_reach } from '#scripts/lib/pnpm-reach'
|
|
4
5
|
import { read_spawn_stdout } from '#scripts/lib/spawn-exit'
|
|
5
|
-
import { PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
6
|
+
import { COMMAND_TIMEOUT_MS, PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
6
7
|
import { execaSync } from 'execa'
|
|
7
8
|
import { doctor_logic } from './doctor-logic'
|
|
8
9
|
|
|
@@ -19,22 +20,45 @@ const AUTO_MERGE_WORKFLOW_PATH = '.github/workflows/dependabot-auto-merge.yml'
|
|
|
19
20
|
// consumer's own auto-merge workflow which does creates the same one kit's does.
|
|
20
21
|
const AUTO_MERGE_COMMAND = 'gh pr merge --auto'
|
|
21
22
|
const YAML_COMMENT_PREFIX = '#'
|
|
23
|
+
const UNKNOWN_PNPM: PnpmProbe = { state: 'unknown' }
|
|
22
24
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// What the platform's lookup command said about a name: whether the command itself could be
|
|
26
|
+
// started, and the first match it printed.
|
|
27
|
+
interface PathLookup {
|
|
28
|
+
did_run: boolean
|
|
29
|
+
first_path: string | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Ask the platform's lookup command (`where` on Windows, `which` elsewhere) for `bin` on PATH.
|
|
33
|
+
function look_up_on_path(bin: string): PathLookup {
|
|
27
34
|
const lookup = doctor_logic.path_lookup_command(process.platform)
|
|
28
|
-
const result = execaSync(lookup, [
|
|
35
|
+
const result = execaSync(lookup, [bin], { reject: false })
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
did_run: !pnpm_reach.is_not_found(result),
|
|
39
|
+
first_path: doctor_logic.first_path_line(read_spawn_stdout(result)),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Resolve the `josh` that the shell would run — the first match on PATH. Undefined when the lookup
|
|
44
|
+
// fails or prints nothing (no `josh` on PATH, e.g. inside the kit repo where only `pnpm josh` runs).
|
|
45
|
+
function resolve_path_josh(): string | undefined {
|
|
46
|
+
return look_up_on_path(JOSH_BIN).first_path
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Whether the lookup command ran and found no `bin`. A lookup command that is itself absent proves
|
|
50
|
+
// nothing about `bin`, so it is not an answer.
|
|
51
|
+
function is_off_path(bin: string): boolean {
|
|
52
|
+
const { did_run, first_path } = look_up_on_path(bin)
|
|
29
53
|
|
|
30
|
-
return
|
|
54
|
+
return did_run && first_path === undefined
|
|
31
55
|
}
|
|
32
56
|
|
|
33
57
|
// Resolve the pnpm-global `josh` bin path. Undefined when pnpm is missing or the global bin does
|
|
34
58
|
// not contain a `josh` (kit not installed globally). `pnpm bin -g` prepends a `[WARN]` line to
|
|
35
59
|
// stdout inside a project, so the bin directory is extracted via first_path_line.
|
|
36
60
|
function resolve_pnpm_global_josh(): string | undefined {
|
|
37
|
-
const result = execaSync(
|
|
61
|
+
const result = execaSync(pnpm_reach.PNPM, ['bin', '-g'], { reject: false })
|
|
38
62
|
const bin_directory = doctor_logic.first_path_line(read_spawn_stdout(result))
|
|
39
63
|
if (bin_directory === undefined) return undefined
|
|
40
64
|
const candidate = path.join(bin_directory, JOSH_BIN)
|
|
@@ -42,6 +66,29 @@ function resolve_pnpm_global_josh(): string | undefined {
|
|
|
42
66
|
return existsSync(candidate) ? candidate : undefined
|
|
43
67
|
}
|
|
44
68
|
|
|
69
|
+
// A spawn that found no executable proves pnpm is off PATH, but Windows never reports one: an
|
|
70
|
+
// unresolved `pnpm` is handed to `cmd.exe`, which starts, prints "'pnpm' is not recognized" and exits
|
|
71
|
+
// 1. So a pnpm that did not answer is also looked up by name, which says the same thing everywhere.
|
|
72
|
+
function is_pnpm_missing(result: { code?: string | undefined }): boolean {
|
|
73
|
+
return pnpm_reach.is_not_found(result) || is_off_path(pnpm_reach.PNPM)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Whether this process can start `pnpm`, and which one answers. `josh` runs its checks as
|
|
77
|
+
// `pnpm josh <check>` children, so a `pnpm` this process cannot see fails every one of them before
|
|
78
|
+
// it starts; asking here is what lets a user learn it before a gate does. `missing` is
|
|
79
|
+
// kept apart from `unknown`: a pnpm that is on PATH and failed is a different fault with a
|
|
80
|
+
// different fix.
|
|
81
|
+
function probe_pnpm(): PnpmProbe {
|
|
82
|
+
const result = execaSync(pnpm_reach.PNPM, ['--version'], {
|
|
83
|
+
reject: false,
|
|
84
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
85
|
+
})
|
|
86
|
+
const version = read_spawn_stdout(result).trim()
|
|
87
|
+
if (version !== '' && result.exitCode === 0) return { state: 'found', version }
|
|
88
|
+
|
|
89
|
+
return is_pnpm_missing(result) ? { state: 'missing' } : UNKNOWN_PNPM
|
|
90
|
+
}
|
|
91
|
+
|
|
45
92
|
// Whether the working directory is provably *outside* a git work tree. Purely local — no network,
|
|
46
93
|
// and it does not depend on `gh` being installed or authenticated, which is what makes it usable to
|
|
47
94
|
// decide whether a repository-scoped check applies at all.
|
|
@@ -192,7 +239,11 @@ function has_auto_merge_workflow(start: string, boundary?: string): boolean {
|
|
|
192
239
|
type GitTopLevel =
|
|
193
240
|
{ state: 'inside'; top_level: string } | { state: 'outside' } | { state: 'undetermined' }
|
|
194
241
|
|
|
242
|
+
// What asking `pnpm --version` from this process established.
|
|
243
|
+
type PnpmProbe = { state: 'found'; version: string } | { state: 'missing' } | { state: 'unknown' }
|
|
244
|
+
|
|
195
245
|
const doctor_io = {
|
|
246
|
+
probe_pnpm,
|
|
196
247
|
resolve_path_josh,
|
|
197
248
|
resolve_pnpm_global_josh,
|
|
198
249
|
has_distributed_dependabot_config,
|
|
@@ -200,5 +251,5 @@ const doctor_io = {
|
|
|
200
251
|
resolve_git_top_level,
|
|
201
252
|
}
|
|
202
253
|
|
|
203
|
-
export type { GitTopLevel }
|
|
254
|
+
export type { GitTopLevel, PnpmProbe }
|
|
204
255
|
export { doctor_io }
|
package/scripts/doctor/doctor.ts
CHANGED
|
@@ -5,12 +5,13 @@ import { fileURLToPath } from 'node:url'
|
|
|
5
5
|
import { repo_discovery } from '#scripts/discovery/repo-discovery'
|
|
6
6
|
import { gh_spawn } from '#scripts/gh/gh-spawn'
|
|
7
7
|
import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
8
|
+
import { pnpm_reach } from '#scripts/lib/pnpm-reach'
|
|
8
9
|
import { auto_merge_setting } from '#scripts/repo/auto-merge-setting'
|
|
9
10
|
import { ruleset_check } from '#scripts/repo/ruleset-check'
|
|
10
11
|
import { security_updates } from '#scripts/security/security-updates'
|
|
11
12
|
import { running_binary } from '#scripts/version/running-binary'
|
|
12
13
|
import { doctor_consumer } from './doctor-consumer'
|
|
13
|
-
import { doctor_io, type GitTopLevel } from './doctor-io'
|
|
14
|
+
import { doctor_io, type GitTopLevel, type PnpmProbe } from './doctor-io'
|
|
14
15
|
import { doctor_logic } from './doctor-logic'
|
|
15
16
|
import { doctor_ports } from './doctor-ports'
|
|
16
17
|
|
|
@@ -43,10 +44,12 @@ interface DoctorContext {
|
|
|
43
44
|
running_path: string
|
|
44
45
|
path_josh: string | undefined
|
|
45
46
|
global_josh: string | undefined
|
|
47
|
+
pnpm: PnpmProbe
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
function gather_context(): DoctorContext {
|
|
49
51
|
return {
|
|
52
|
+
pnpm: doctor_io.probe_pnpm(),
|
|
50
53
|
running_version: running_binary.read_running_version(SELF_DIR),
|
|
51
54
|
running_path: running_binary.running_package_directory(SELF_DIR),
|
|
52
55
|
path_josh: doctor_io.resolve_path_josh(),
|
|
@@ -54,13 +57,28 @@ function gather_context(): DoctorContext {
|
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
function describe_pnpm(probe: PnpmProbe): string {
|
|
61
|
+
if (probe.state === 'found') return probe.version
|
|
62
|
+
|
|
63
|
+
return probe.state === 'missing' ? NOT_ON_PATH : UNKNOWN
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
function print_report(ctx: DoctorContext): void {
|
|
58
67
|
console.info('josh doctor')
|
|
59
68
|
console.info(` Running: ${ctx.running_version ?? UNKNOWN} (${ctx.running_path})`)
|
|
60
69
|
console.info(` On PATH: ${ctx.path_josh ?? NOT_ON_PATH}`)
|
|
70
|
+
console.info(` pnpm: ${describe_pnpm(ctx.pnpm)}`)
|
|
61
71
|
console.info(` pnpm global: ${ctx.global_josh ?? UNKNOWN}`)
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
// Said here rather than left to the gate: every check `josh` runs is a `pnpm` child, so without
|
|
75
|
+
// pnpm on this process's PATH each of them fails before it starts.
|
|
76
|
+
function report_pnpm_reach(ctx: DoctorContext): void {
|
|
77
|
+
if (ctx.pnpm.state !== 'missing') return
|
|
78
|
+
console.info('')
|
|
79
|
+
console.info(`⚠ ${pnpm_reach.NOT_ON_PATH_NOTICE}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
// Remove a confirmed stale kit shim so the pnpm-global josh reclaims PATH precedence. Any other
|
|
65
83
|
// shadowing binary is left in place and reported for manual review.
|
|
66
84
|
function reclaim_shim(path_josh: string, global_josh: string): void {
|
|
@@ -207,6 +225,7 @@ function consumer_root(git: GitTopLevel): string {
|
|
|
207
225
|
function run_reports(ctx: DoctorContext, git: GitTopLevel, options: DoctorOptions): void {
|
|
208
226
|
print_report(ctx)
|
|
209
227
|
report_path_diagnosis(ctx, options.is_fix)
|
|
228
|
+
report_pnpm_reach(ctx)
|
|
210
229
|
doctor_consumer.report_consumer_setup(consumer_root(git))
|
|
211
230
|
report_repository_map(git)
|
|
212
231
|
if (options.show_ports) report_port_seeds(git)
|
|
@@ -74,9 +74,9 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
|
|
|
74
74
|
{ path: '.claude/skills/workflow-commands/target-repository.md', bytes: 4096 },
|
|
75
75
|
{ path: '.claude/skills/workflow-commands/working-tree-hold.md', bytes: 8192 },
|
|
76
76
|
{ path: 'docs/josh-commands-automation.md', bytes: 53_248 },
|
|
77
|
-
{ path: 'docs/josh-commands-run.md', bytes:
|
|
78
|
-
{ path: 'docs/josh-commands-backlog.md', bytes:
|
|
79
|
-
{ path: 'docs/josh-commands.md', bytes:
|
|
77
|
+
{ path: 'docs/josh-commands-run.md', bytes: 77_824 },
|
|
78
|
+
{ path: 'docs/josh-commands-backlog.md', bytes: 61_440 },
|
|
79
|
+
{ path: 'docs/josh-commands.md', bytes: 53_248 },
|
|
80
80
|
{ path: 'docs/labels-and-run-states.md', bytes: 8192 },
|
|
81
81
|
{ path: 'prompts/coding-standards.md', bytes: 8192 },
|
|
82
82
|
{ path: 'prompts/collaboration-workflow.md', bytes: 8192 },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
4
|
+
import { issue_label_schema } from '#scripts/git/git-schemas'
|
|
4
5
|
import { cutoff_of, type ScanCutoff } from '#scripts/git/listing-cutoff'
|
|
5
6
|
import { parse_json_array_or_undefined } from '#scripts/git/parse-json-array'
|
|
6
7
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
type BacklogIssue,
|
|
15
16
|
type BundleAction,
|
|
16
17
|
type BundleDecision,
|
|
18
|
+
type MembershipContext,
|
|
17
19
|
} from './epic-bundle'
|
|
18
20
|
import { epic_bundle_evidence } from './epic-bundle-evidence'
|
|
19
21
|
import { epic_bundle_gaps } from './epic-bundle-gaps'
|
|
@@ -53,6 +55,7 @@ const UNKNOWN_REPO_MESSAGE =
|
|
|
53
55
|
// describing one it does.
|
|
54
56
|
const backlog_schema = epic_schema.extend({
|
|
55
57
|
title: z.string().nullable(),
|
|
58
|
+
labels: z.array(issue_label_schema).default([]),
|
|
56
59
|
blocked_by_count: z.number().nullish(),
|
|
57
60
|
})
|
|
58
61
|
|
|
@@ -62,10 +65,6 @@ type BacklogRow = z.infer<typeof backlog_schema>
|
|
|
62
65
|
// task-list read would answer differently the first time that shape moved.
|
|
63
66
|
const { build_epic_index } = epic_index
|
|
64
67
|
|
|
65
|
-
function to_epic_field(epic: number | undefined): { epic?: number } {
|
|
66
|
-
return epic === undefined ? {} : { epic }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
68
|
// Undefined when the read failed, which the caller reports — as opposed to an issue that genuinely
|
|
70
69
|
// declares no relations, which is an empty array.
|
|
71
70
|
async function epic_issue_relations(
|
|
@@ -139,18 +138,19 @@ interface FetchedBacklog {
|
|
|
139
138
|
// One listing row as the bundler reads it. Split from the fetch so that function stays a list of
|
|
140
139
|
// requests rather than a request list plus a row mapping.
|
|
141
140
|
function to_backlog_issue(
|
|
142
|
-
row:
|
|
143
|
-
context: { repo: string
|
|
141
|
+
row: Pick<BacklogRow, 'number' | 'title' | 'body' | 'labels'>,
|
|
142
|
+
context: MembershipContext & { repo: string },
|
|
144
143
|
blocked_by: ReadonlyArray<IssueReference>,
|
|
145
144
|
): BacklogIssue {
|
|
145
|
+
const labels = row.labels.map((label) => label.name)
|
|
146
|
+
|
|
146
147
|
return {
|
|
147
148
|
number: row.number,
|
|
148
149
|
repo: context.repo,
|
|
149
150
|
title: row.title ?? '',
|
|
150
151
|
body: row.body ?? '',
|
|
151
152
|
blocked_by,
|
|
152
|
-
|
|
153
|
-
...to_epic_field(context.epics.get(row.number)),
|
|
153
|
+
...epic_bundle.membership_of(row.number, labels, context),
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
2
2
|
import { bounded_pool } from '#scripts/lib/bounded-pool'
|
|
3
3
|
import { epic_audit_logic } from './epic-audit'
|
|
4
|
-
import type
|
|
4
|
+
import { epic_bundle, type BacklogIssue } from './epic-bundle'
|
|
5
5
|
import { epic_issue, type EpicIssue } from './epic-issue'
|
|
6
6
|
|
|
7
7
|
// Candidates the open backlog cannot show, read from the subject's own prose.
|
|
@@ -64,15 +64,12 @@ interface ReferencedContext {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function to_backlog_issue(issue: EpicIssue, context: ReferencedContext): BacklogIssue {
|
|
67
|
-
const epic = context.epics.get(issue.number)
|
|
68
|
-
|
|
69
67
|
return {
|
|
70
68
|
number: issue.number,
|
|
71
69
|
repo: context.repo,
|
|
72
70
|
body: issue.body,
|
|
73
71
|
blocked_by: epic_issue.blocker_references_of(issue, context.repo),
|
|
74
|
-
|
|
75
|
-
...(epic !== undefined && { epic }),
|
|
72
|
+
...epic_bundle.membership_of(issue.number, epic_issue.label_names(issue), context),
|
|
76
73
|
}
|
|
77
74
|
}
|
|
78
75
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
|
+
import { has_label_name, RELEASE_LABEL } from '#scripts/issue/issue-labels'
|
|
2
3
|
import { epic_audit_logic } from './epic-audit'
|
|
3
4
|
import { epic_graph } from './epic-graph'
|
|
4
5
|
import type { DependencyLink } from './epic-parse'
|
|
@@ -31,6 +32,11 @@ interface BacklogIssue {
|
|
|
31
32
|
// its parent, so without this every child would find its own epic as a candidate and be told to
|
|
32
33
|
// bundle with it.
|
|
33
34
|
is_epic?: boolean
|
|
35
|
+
// Whether this issue is the release issue. `issue:file --release` records every change awaiting
|
|
36
|
+
// publication as its blocker, so the relation is mechanical and says nothing about the work
|
|
37
|
+
// belonging together — read as a signal, it proposed one epic for the release and every
|
|
38
|
+
// unrelated change waiting on it.
|
|
39
|
+
is_release?: boolean
|
|
34
40
|
// Repository-qualified, not bare numbers. Issue numbers are unique per
|
|
35
41
|
// repository, so a blocker read as `40` alone matched any candidate numbered 40 — including one in
|
|
36
42
|
// a different repository entirely. That produced a bundle nobody's data supported, and, through
|
|
@@ -79,6 +85,36 @@ function has_recorded_dependency(subject: BacklogIssue, other: BacklogIssue): bo
|
|
|
79
85
|
return names_as_blocker(subject, other) || names_as_blocker(other, subject)
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
// The epic view an issue is placed against: which epic tracks each issue, and the epics themselves.
|
|
89
|
+
interface MembershipContext {
|
|
90
|
+
epics: ReadonlyMap<number, number>
|
|
91
|
+
epic_numbers: ReadonlySet<number>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type Membership = Pick<BacklogIssue, 'epic' | 'is_epic' | 'is_release'>
|
|
95
|
+
|
|
96
|
+
// The fields that decide whether and where an issue takes part in a bundle. Single-sourced so the
|
|
97
|
+
// listing read and the per-reference read cannot disagree about which issue is an epic or the
|
|
98
|
+
// release one.
|
|
99
|
+
function membership_of(
|
|
100
|
+
number: number,
|
|
101
|
+
labels: ReadonlyArray<string>,
|
|
102
|
+
context: MembershipContext,
|
|
103
|
+
): Membership {
|
|
104
|
+
const epic = context.epics.get(number)
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
is_epic: context.epic_numbers.has(number),
|
|
108
|
+
is_release: has_label_name(labels, RELEASE_LABEL),
|
|
109
|
+
...(epic !== undefined && { epic }),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Neither an epic nor the release issue takes part in a bundle, on either side.
|
|
114
|
+
function is_excluded(issue: BacklogIssue): boolean {
|
|
115
|
+
return issue.is_epic === true || issue.is_release === true
|
|
116
|
+
}
|
|
117
|
+
|
|
82
118
|
// A strong signal, and only a strong signal.
|
|
83
119
|
//
|
|
84
120
|
// Similar titles are deliberately not one. "Related" expands without limit, and a threshold is what
|
|
@@ -87,9 +123,7 @@ function has_recorded_dependency(subject: BacklogIssue, other: BacklogIssue): bo
|
|
|
87
123
|
function is_strong_signal(subject: BacklogIssue, other: BacklogIssue): boolean {
|
|
88
124
|
// Neither side. Asked about an epic, the one-sided check found every one of its children through
|
|
89
125
|
// their own `parent: #N` line and proposed bundling a container with its contents.
|
|
90
|
-
if (subject.number === other.number || subject
|
|
91
|
-
return false
|
|
92
|
-
}
|
|
126
|
+
if (subject.number === other.number || is_excluded(subject) || is_excluded(other)) return false
|
|
93
127
|
|
|
94
128
|
return has_mutual_reference(subject, other) || has_recorded_dependency(subject, other)
|
|
95
129
|
}
|
|
@@ -295,10 +329,11 @@ const epic_bundle = {
|
|
|
295
329
|
already_tracked_reason,
|
|
296
330
|
has_recorded_dependency,
|
|
297
331
|
is_strong_signal,
|
|
332
|
+
membership_of,
|
|
298
333
|
decide_bundle,
|
|
299
334
|
bundle_dependency_links,
|
|
300
335
|
bundle_children,
|
|
301
336
|
}
|
|
302
337
|
|
|
303
|
-
export type { BacklogIssue, BundleAction, BundleDecision }
|
|
338
|
+
export type { BacklogIssue, BundleAction, BundleDecision, MembershipContext }
|
|
304
339
|
export { epic_bundle }
|
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { doctor_consumer } from '#scripts/doctor/doctor-consumer'
|
|
5
5
|
import { project_checks } from '#scripts/gate/project-checks'
|
|
6
|
+
import { NOTHING_TO_CHECK_MARKER } from '#scripts/test/skip-marker'
|
|
6
7
|
import type { NamespaceMember } from './namespace-members'
|
|
7
8
|
|
|
8
9
|
const CHECK = 'exports'
|
|
@@ -13,7 +14,7 @@ const FAIL_EXIT_CODE = 1
|
|
|
13
14
|
// **Not a skip notice**. `SKIP_MARKER` means "a check passed without running",
|
|
14
15
|
// and the gate withholds its green record on it — so a consumer, where this notice prints on every
|
|
15
16
|
// run, never got a green record. Out of scope by design is not unverified, so the marker stays out.
|
|
16
|
-
const CONSUMER_NOTICE = `josh ${CHECK}: checks only the kit repository itself —
|
|
17
|
+
const CONSUMER_NOTICE = `josh ${CHECK}: checks only the kit repository itself — ${NOTHING_TO_CHECK_MARKER} in a consumer project.`
|
|
17
18
|
|
|
18
19
|
function format_member(root: string, member: NamespaceMember): string {
|
|
19
20
|
return `${path.relative(root, member.file)}:${String(member.line)} ${member.namespace}.${member.member}`
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { SnapshotObserver } from '#scripts/gh/git-pr-checks'
|
|
2
|
+
import { is_coderabbit_check, is_coderabbit_rate_limited } from '#scripts/gh/git-pr-checks-eval'
|
|
3
|
+
import {
|
|
4
|
+
CHECK_STATUS_FAIL,
|
|
5
|
+
CHECK_STATUS_PASS,
|
|
6
|
+
CHECK_STATUS_PENDING,
|
|
7
|
+
type PrStateSnapshot,
|
|
8
|
+
type RollupCheck,
|
|
9
|
+
} from '#scripts/gh/git-pr-checks-parse'
|
|
10
|
+
import {
|
|
11
|
+
run_event_checks,
|
|
12
|
+
type CodeRabbitState,
|
|
13
|
+
type PrChecks,
|
|
14
|
+
} from '#scripts/run/event/run-event-checks'
|
|
15
|
+
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
16
|
+
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
17
|
+
|
|
18
|
+
// What `followup`'s check wait tells the run's event stream (joshuafolkken/kit#3770): the CI counts and
|
|
19
|
+
// CodeRabbit's state, one `pr-checks` line each time they change, so `run:board` draws a child waiting
|
|
20
|
+
// in followup from the stream alone. CodeRabbit is counted apart — it has a state of its own and does
|
|
21
|
+
// not gate the merge.
|
|
22
|
+
|
|
23
|
+
const PR_NUMBER_PATTERN = /\/pull\/(?<number>\d+)/u
|
|
24
|
+
const { PR_CHECKS } = run_event_stream.EVENT_KIND
|
|
25
|
+
|
|
26
|
+
function count_of(checks: ReadonlyArray<RollupCheck>, status: string): number {
|
|
27
|
+
return checks.filter((check) => check.status === status).length
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function coderabbit_state_of(check: RollupCheck | undefined): CodeRabbitState {
|
|
31
|
+
if (check === undefined) return 'none'
|
|
32
|
+
if (check.status === CHECK_STATUS_PENDING) return 'running'
|
|
33
|
+
if (check.status !== CHECK_STATUS_PASS) return 'failed'
|
|
34
|
+
|
|
35
|
+
return is_coderabbit_rate_limited(check) ? 'rate-limited' : 'done'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function summary_of(snapshot: PrStateSnapshot, pr: number): PrChecks {
|
|
39
|
+
const ci = snapshot.rollup.filter((check) => !is_coderabbit_check(check.name))
|
|
40
|
+
const coderabbit = snapshot.rollup.find((check) => is_coderabbit_check(check.name))
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
pr,
|
|
44
|
+
pass: count_of(ci, CHECK_STATUS_PASS),
|
|
45
|
+
pending: count_of(ci, CHECK_STATUS_PENDING),
|
|
46
|
+
fail: count_of(ci, CHECK_STATUS_FAIL),
|
|
47
|
+
coderabbit: coderabbit_state_of(coderabbit),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pr_number_of(pr_url = ''): number | undefined {
|
|
52
|
+
const number = PR_NUMBER_PATTERN.exec(pr_url)?.groups?.['number']
|
|
53
|
+
|
|
54
|
+
return number === undefined ? undefined : Number(number)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function skip_snapshot(): Promise<void> {
|
|
58
|
+
// Nothing names the child or its pull request, so there is no row to report to.
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// An observer that writes a line only when it differs from the one this wait last wrote; one that
|
|
62
|
+
// does nothing where the issue or the pull request is unknown.
|
|
63
|
+
function observer_of(issue: string | undefined, pr_url: string | undefined): SnapshotObserver {
|
|
64
|
+
const pr = pr_number_of(pr_url)
|
|
65
|
+
|
|
66
|
+
if (issue === undefined || pr === undefined) return skip_snapshot
|
|
67
|
+
|
|
68
|
+
let last_text = ''
|
|
69
|
+
|
|
70
|
+
return async function report_snapshot(snapshot: PrStateSnapshot): Promise<void> {
|
|
71
|
+
const text = run_event_checks.text_of(issue, summary_of(snapshot, pr))
|
|
72
|
+
|
|
73
|
+
if (text === last_text) return
|
|
74
|
+
|
|
75
|
+
last_text = text
|
|
76
|
+
await run_event_stream_emit.emit(PR_CHECKS, text)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const followup_checks_event = { observer_of, summary_of }
|
|
81
|
+
|
|
82
|
+
export { followup_checks_event }
|