@joshuafolkken/kit 1.1387.0 → 1.1453.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
- package/.claude/skills/workflow-commands/backlogrun.md +3 -2
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +1 -1
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/kickoff.md +12 -13
- package/.claude/skills/workflow-commands/observation-filing.md +1 -1
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/.claude/skills/workflow-commands/prerequisite.md +6 -0
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/README.md +27 -4
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +261 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +95 -19
- package/package.json +4 -3
- package/prompts/collaboration-workflow/issue-template.md +10 -8
- package/prompts/review.md +30 -4
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
- package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
- package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
- package/scripts/auto-ok/auto-ok-judge.ts +67 -0
- package/scripts/backlog/backlog-drive-cli.ts +14 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +38 -11
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +175 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +18 -4
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +13 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +7 -4
- package/scripts/init/init-ai-copy.ts +34 -11
- package/scripts/init/init-basic.ts +9 -4
- package/scripts/init/init-bootstrap.ts +6 -2
- package/scripts/init/init-dry-run-report.ts +150 -0
- package/scripts/init/init-dry-run.ts +189 -0
- package/scripts/init/init-install.ts +8 -1
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +62 -42
- package/scripts/init/kit-setup-state.ts +11 -17
- package/scripts/init/own-formatter.ts +21 -0
- package/scripts/init/project-profile.ts +60 -12
- package/scripts/init/start-dry-run.ts +70 -0
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +23 -3
- package/scripts/init/start-setup-pr.ts +13 -24
- package/scripts/init/start-steps.ts +62 -1
- package/scripts/init/start.ts +38 -17
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/init/working-tree.ts +116 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-auto-ok.ts +68 -99
- package/scripts/issue/issue-file-cli.ts +17 -17
- package/scripts/issue/issue-file.ts +1 -6
- package/scripts/issue/issue-labels.ts +46 -10
- package/scripts/issue/issue-release-cli.ts +69 -23
- package/scripts/issue/issue-release.ts +3 -1
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-ai.ts +11 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +10 -3
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +26 -2
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/josh/process-identity.ts +32 -6
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-broker-server.ts +69 -0
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/lane-session-broker.ts +363 -0
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/observations/observation-ledger-migrate.ts +3 -15
- package/scripts/release/release-issue-close.ts +133 -0
- package/scripts/release/release-publish.ts +12 -1
- package/scripts/repo/repository-labels.ts +11 -1
- package/scripts/review/comment-ranges.ts +199 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +24 -7
- package/scripts/review/review-code-tree.ts +119 -0
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/review/review-round2-cli.ts +16 -5
- package/scripts/review/review-round2.ts +30 -10
- package/scripts/review/review-stamps.ts +22 -1
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/lane/openai-review-broker.ts +0 -262
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -172,6 +172,29 @@ function started_for(git_directory: string, owner_pid: number | undefined): Date
|
|
|
172
172
|
return Number.isNaN(started_ms) ? undefined : new Date(started_ms)
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
function signal_parent(pid: number): void {
|
|
176
|
+
try {
|
|
177
|
+
process.kill(pid, 'SIGTERM')
|
|
178
|
+
} catch {
|
|
179
|
+
note_to_stderr(`process ${String(pid)} could not be signalled; its launch record is removed.`)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Stops the parent this launcher started and forgets its launch — the pid stopped, `undefined` where
|
|
184
|
+
// no launched parent was live. **Only the process the record names, and only while it is still that
|
|
185
|
+
// process**: a session a person typed `backlogrun` into has no launch record and is never signalled.
|
|
186
|
+
function stop_parent(git_directory: string): number | undefined {
|
|
187
|
+
const record = live_launch(git_directory)
|
|
188
|
+
|
|
189
|
+
stamp_file.remove_stamp(launch_path(git_directory))
|
|
190
|
+
|
|
191
|
+
if (record === undefined) return undefined
|
|
192
|
+
|
|
193
|
+
signal_parent(record.pid)
|
|
194
|
+
|
|
195
|
+
return record.pid
|
|
196
|
+
}
|
|
197
|
+
|
|
175
198
|
// The headless mark holds the parent's turn open while its lanes run, as `run:wake`'s does. The launch
|
|
176
199
|
// strips only Claude's session keys, so a Codex session this was typed in is cleared here too — inherited,
|
|
177
200
|
// it would make the parent's own `josh` commands resolve the wrong provider.
|
|
@@ -223,6 +246,7 @@ const backlogrun_launch = {
|
|
|
223
246
|
pending_launch,
|
|
224
247
|
start,
|
|
225
248
|
started_for,
|
|
249
|
+
stop_parent,
|
|
226
250
|
}
|
|
227
251
|
|
|
228
252
|
export type { Launched, LaunchTarget, PendingLaunch, StartResult }
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { lane_spent, type LaneUse } from '#scripts/lane/lane-spent'
|
|
2
|
+
import { run_carry, type CarryRead, type RunCarry } from '#scripts/run/carry/run-carry'
|
|
3
|
+
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
4
|
+
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
5
|
+
import { backlog_drive_retrospective } from './backlog-drive-retrospective'
|
|
6
|
+
import { backlogrun_launch } from './backlogrun-launch'
|
|
7
|
+
|
|
8
|
+
// Whether `josh backlogrun` may replace the run already going with the one just typed. A run that
|
|
9
|
+
// drained its backlog keeps watching for hours with nothing in flight, and starting another meant typing
|
|
10
|
+
// `--stop` first; this is the test that lets the start do both.
|
|
11
|
+
//
|
|
12
|
+
// **A different invocation, a watching run, no lane in use — all three.** The same invocation is how a
|
|
13
|
+
// person reopens the board, so it never rebuilds the run. A run still starting is opening a lane, and a
|
|
14
|
+
// child killed mid-change leaves a tree nobody can resume, so only a run past its drain is replaced.
|
|
15
|
+
//
|
|
16
|
+
// **Only a lane a child is working in keeps the run.** A lane a past run left behind has nobody in it;
|
|
17
|
+
// counted as open, it kept every later invocation from replacing the run (`lane-spent.ts`).
|
|
18
|
+
//
|
|
19
|
+
// **A drain that still owes the retrospective is not the watch.** The drain is marked before the
|
|
20
|
+
// end-of-run retrospective runs, so a run owing one has a session at work on it; stopping that session
|
|
21
|
+
// loses the retrospective with the record, and the run is kept until it has run.
|
|
22
|
+
//
|
|
23
|
+
// **An undetermined run is not replaced.** A record or a scope that cannot be read answers `keep`, the
|
|
24
|
+
// side that keeps the run.
|
|
25
|
+
//
|
|
26
|
+
// **A launch its parent has not claimed yet is still starting, not unreadable.** No carry record stands
|
|
27
|
+
// then, so the invocation is read from the launch record: the same one is the board reopened, and any
|
|
28
|
+
// other is kept as a run that has not written its record yet.
|
|
29
|
+
//
|
|
30
|
+
// **A kept run says why.** Every `keep` carries the one condition that held it, because a board shown
|
|
31
|
+
// with no word looked the same whichever of them it was.
|
|
32
|
+
|
|
33
|
+
const KIND = run_event_stream.EVENT_KIND
|
|
34
|
+
const NOT_FOUND = -1
|
|
35
|
+
const ISSUE_PREFIX = '#'
|
|
36
|
+
const UNREADABLE_REASON = "the run's record could not be read"
|
|
37
|
+
const UNCLAIMED_REASON = 'the run is still starting: it has not written its record yet'
|
|
38
|
+
const WORKING_LANE = 'a child is still working in the lane for'
|
|
39
|
+
const UNREAD_LANE = 'whether a child is working could not be read in the lane for'
|
|
40
|
+
const LANE_WAY_OUT = '(`pnpm josh lane:close <N>` discards a lane)'
|
|
41
|
+
const RETROSPECTIVE_REASON = 'the run still owes its retrospective'
|
|
42
|
+
const WORKING_REASON = 'the run is not watching: it is still starting or has a child at work'
|
|
43
|
+
|
|
44
|
+
// `same` is the board reopened, which is kept without a word; `keep` is a refusal, and says why.
|
|
45
|
+
type ReplaceVerdict = { kind: 'replace' } | { kind: 'same' } | { kind: 'keep'; reason: string }
|
|
46
|
+
|
|
47
|
+
const REPLACE: ReplaceVerdict = { kind: 'replace' }
|
|
48
|
+
const SAME: ReplaceVerdict = { kind: 'same' }
|
|
49
|
+
|
|
50
|
+
interface ReplacePorts {
|
|
51
|
+
read_carry: (git_directory: string) => CarryRead
|
|
52
|
+
events: (git_directory: string) => ReadonlyArray<RunEvent>
|
|
53
|
+
is_retrospective_owed: (carry: RunCarry) => boolean
|
|
54
|
+
// The invocation of a launch whose parent has not claimed the record yet.
|
|
55
|
+
pending_invocation: (git_directory: string) => string | undefined
|
|
56
|
+
// The lanes in use: a child working in one, or a probe that did not answer.
|
|
57
|
+
lanes: () => Promise<LaneUse>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const LIVE_PORTS: ReplacePorts = {
|
|
61
|
+
read_carry: (git_directory) => run_carry.read_carry(run_carry.carry_path(git_directory)),
|
|
62
|
+
events: (git_directory) =>
|
|
63
|
+
run_event_stream.read_events(run_event_stream.target_of(git_directory)),
|
|
64
|
+
is_retrospective_owed: backlog_drive_retrospective.is_owed,
|
|
65
|
+
pending_invocation: (git_directory) =>
|
|
66
|
+
backlogrun_launch.pending_launch(git_directory)?.invocation,
|
|
67
|
+
lanes: async () => await lane_spent.lanes_in_use(),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function keep(reason: string): ReplaceVerdict {
|
|
71
|
+
return { kind: 'keep', reason }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// What the record alone settles, or `undefined` for a different invocation that owes nothing.
|
|
75
|
+
function record_verdict(
|
|
76
|
+
invocation: string,
|
|
77
|
+
read: CarryRead,
|
|
78
|
+
ports: ReplacePorts,
|
|
79
|
+
): ReplaceVerdict | undefined {
|
|
80
|
+
if (read.kind !== 'carried') return keep(UNREADABLE_REASON)
|
|
81
|
+
if (read.carry.invocation === invocation) return SAME
|
|
82
|
+
|
|
83
|
+
return ports.is_retrospective_owed(read.carry) ? keep(RETROSPECTIVE_REASON) : undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A launch with no carry record yet, answered from the launch record.
|
|
87
|
+
function unclaimed_verdict(pending: string | undefined, invocation: string): ReplaceVerdict {
|
|
88
|
+
return pending === invocation ? SAME : keep(UNCLAIMED_REASON)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Watching is the run's newest drain with no launch after it: a child the watch woke for ends it.
|
|
92
|
+
function is_watching(scoped: ReadonlyArray<RunEvent>): boolean {
|
|
93
|
+
const drained_at = scoped.findLastIndex((event) => event.kind === KIND.DRAIN)
|
|
94
|
+
|
|
95
|
+
if (drained_at === NOT_FOUND) return false
|
|
96
|
+
|
|
97
|
+
return scoped.slice(drained_at).every((event) => event.kind !== KIND.CHILD_LAUNCH)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function watch_verdict(
|
|
101
|
+
git_directory: string,
|
|
102
|
+
read: CarryRead,
|
|
103
|
+
ports: ReplacePorts,
|
|
104
|
+
): ReplaceVerdict {
|
|
105
|
+
const events = ports.events(git_directory)
|
|
106
|
+
const scoped = run_event_scope.scoped_events(events, run_event_scope.scope_of(read))
|
|
107
|
+
|
|
108
|
+
if (scoped === undefined) return keep(UNREADABLE_REASON)
|
|
109
|
+
|
|
110
|
+
return is_watching(scoped) ? REPLACE : keep(WORKING_REASON)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function named(lead: string, issues: ReadonlyArray<string>): Array<string> {
|
|
114
|
+
if (issues.length === 0) return []
|
|
115
|
+
|
|
116
|
+
const cited = issues.map((issue) => `${ISSUE_PREFIX}${issue}`).join(' ')
|
|
117
|
+
|
|
118
|
+
return [`${lead} ${cited}`]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The lanes are named with the command that discards one, the only way past a child that will not end —
|
|
122
|
+
// and a probe that did not answer is said as that, because the close takes a lane's uncommitted work.
|
|
123
|
+
function lane_reason(use: LaneUse): string {
|
|
124
|
+
const parts = [...named(WORKING_LANE, use.working), ...named(UNREAD_LANE, use.unread)]
|
|
125
|
+
|
|
126
|
+
return `${parts.join('; ')} ${LANE_WAY_OUT}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function is_free(use: LaneUse): boolean {
|
|
130
|
+
return use.working.length === 0 && use.unread.length === 0
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function claimed_verdict(
|
|
134
|
+
invocation: string,
|
|
135
|
+
git_directory: string,
|
|
136
|
+
read: CarryRead,
|
|
137
|
+
ports: ReplacePorts,
|
|
138
|
+
): Promise<ReplaceVerdict> {
|
|
139
|
+
const settled =
|
|
140
|
+
record_verdict(invocation, read, ports) ?? watch_verdict(git_directory, read, ports)
|
|
141
|
+
|
|
142
|
+
if (settled.kind !== 'replace') return settled
|
|
143
|
+
|
|
144
|
+
const use = await ports.lanes()
|
|
145
|
+
|
|
146
|
+
return is_free(use) ? REPLACE : keep(lane_reason(use))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function decide(
|
|
150
|
+
invocation: string,
|
|
151
|
+
git_directory: string,
|
|
152
|
+
ports: ReplacePorts = LIVE_PORTS,
|
|
153
|
+
): Promise<ReplaceVerdict> {
|
|
154
|
+
const read = ports.read_carry(git_directory)
|
|
155
|
+
|
|
156
|
+
if (read.kind === 'none') {
|
|
157
|
+
return unclaimed_verdict(ports.pending_invocation(git_directory), invocation)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return await claimed_verdict(invocation, git_directory, read, ports)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const backlogrun_replace = { decide }
|
|
164
|
+
|
|
165
|
+
export type { ReplacePorts, ReplaceVerdict }
|
|
166
|
+
export { backlogrun_replace }
|
|
@@ -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: 57_344 },
|
|
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
|
|