@joshuafolkken/kit 1.1387.0 → 1.1435.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +288 -229
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +62 -9
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +15 -0
- package/scripts/init/start-steps.ts +46 -0
- package/scripts/init/start.ts +1 -0
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +24 -0
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -1,27 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import type { AgentProvider } from '#scripts/agent/agent-role-profile'
|
|
4
5
|
import { josh_environment_file } from '#scripts/josh/josh-environment-file'
|
|
5
6
|
import { run_board_cli } from '#scripts/run/board/run-board-cli'
|
|
6
7
|
import { run_carry } from '#scripts/run/carry/run-carry'
|
|
8
|
+
import type { LaunchResult } from '#scripts/run/detached-launch'
|
|
9
|
+
import { run_wake_cli } from '#scripts/run/wake/run-wake-cli'
|
|
7
10
|
import { backlogrun_args, type BacklogrunArguments } from './backlogrun-args'
|
|
8
11
|
import { backlogrun_launch, type LaunchTarget, type StartResult } from './backlogrun-launch'
|
|
12
|
+
import { backlogrun_replace, type ReplaceVerdict } from './backlogrun-replace'
|
|
13
|
+
import { backlogrun_stop, type StopReport } from './backlogrun-stop'
|
|
9
14
|
|
|
10
|
-
// `josh backlogrun` — start a `backlogrun` from the terminal and watch it
|
|
15
|
+
// `josh backlogrun` — start a `backlogrun` from the terminal and watch it, or stop it with `--stop`. A
|
|
11
16
|
// run already going is never joined by a second one: the board is shown, and any issue named is pointed
|
|
12
17
|
// at `run:add` rather than added behind the person's back. Otherwise the agent is started in the
|
|
13
18
|
// background, its output to a log file, and the board takes over the terminal at once. Closing the
|
|
14
19
|
// board closes only the board.
|
|
20
|
+
//
|
|
21
|
+
// **An idle run gives way to a different invocation.** A run watching with no child at work in a lane
|
|
22
|
+
// is stopped as `--stop` stops it and the typed one is started in its place (`backlogrun-replace.ts`);
|
|
23
|
+
// a run that is kept says why in one line above the board. The same invocation only shows the board,
|
|
24
|
+
// so looking at a run never rebuilds it.
|
|
15
25
|
|
|
16
26
|
const ARGV_OFFSET = 2
|
|
27
|
+
const SUCCESS_EXIT_CODE = 0
|
|
17
28
|
const FAILURE_EXIT_CODE = 1
|
|
18
29
|
const USAGE =
|
|
19
|
-
'Usage: josh backlogrun [
|
|
30
|
+
'Usage: josh backlogrun [<n>...] [--only] [--max <n>] [--idle <minutes>] [--agent claude|codex] | --stop'
|
|
20
31
|
|
|
21
32
|
interface BacklogrunPorts {
|
|
22
33
|
repository_directory: () => Promise<string | undefined>
|
|
23
34
|
is_running: (git_directory: string) => boolean
|
|
35
|
+
replacement: (invocation: string, git_directory: string) => Promise<ReplaceVerdict>
|
|
24
36
|
start: (args: BacklogrunArguments, target: LaunchTarget) => StartResult
|
|
37
|
+
start_supervisor: (git_directory: string, provider: AgentProvider) => LaunchResult
|
|
38
|
+
stop: (git_directory: string) => Promise<StopReport>
|
|
25
39
|
show_board: () => Promise<number>
|
|
26
40
|
write: (line: string) => void
|
|
27
41
|
}
|
|
@@ -29,7 +43,11 @@ interface BacklogrunPorts {
|
|
|
29
43
|
const LIVE_PORTS: BacklogrunPorts = {
|
|
30
44
|
repository_directory: run_carry.repository_directory,
|
|
31
45
|
is_running: backlogrun_launch.is_running,
|
|
46
|
+
replacement: async (invocation, git_directory) =>
|
|
47
|
+
await backlogrun_replace.decide(invocation, git_directory),
|
|
32
48
|
start: (args, target) => backlogrun_launch.start(args.invocation, args.provider, target),
|
|
49
|
+
start_supervisor: run_wake_cli.start_for_launch,
|
|
50
|
+
stop: async (git_directory) => await backlogrun_stop.stop(git_directory),
|
|
33
51
|
show_board: async () => await run_board_cli.run([]),
|
|
34
52
|
write: (line) => process.stdout.write(`${line}\n`),
|
|
35
53
|
}
|
|
@@ -41,6 +59,12 @@ function running_notes(issues: ReadonlyArray<number>): Array<string> {
|
|
|
41
59
|
return ['backlogrun: a run is already going; showing its board.', ...guidance]
|
|
42
60
|
}
|
|
43
61
|
|
|
62
|
+
// Why a different invocation did not replace the run, ahead of the board. The same invocation is the
|
|
63
|
+
// board reopened, and adds nothing.
|
|
64
|
+
function kept_notes(verdict: ReplaceVerdict): Array<string> {
|
|
65
|
+
return verdict.kind === 'keep' ? [`backlogrun: not replacing the run — ${verdict.reason}`] : []
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
function launched_note(result: Extract<StartResult, { kind: 'launched' }>): string {
|
|
45
69
|
const { pid, session, log_path } = result.launched
|
|
46
70
|
const session_part = session === undefined ? '' : `, session ${session}`
|
|
@@ -48,22 +72,26 @@ function launched_note(result: Extract<StartResult, { kind: 'launched' }>): stri
|
|
|
48
72
|
return `backlogrun: started (pid ${String(pid)}${session_part}); log ${log_path}`
|
|
49
73
|
}
|
|
50
74
|
|
|
75
|
+
// **The supervisor is started here, beside the parent, never from the parent's own shell**: Claude
|
|
76
|
+
// Code removes its token from what its Bash tool starts, so only this process can hand a token-only
|
|
77
|
+
// credential on to the sessions the supervisor wakes (joshuafolkken/kit#3804). A supervisor that does
|
|
78
|
+
// not start leaves the parent's own `run:wake --start` to try, so the run is still shown.
|
|
79
|
+
function supervisor_note(result: LaunchResult): string {
|
|
80
|
+
if (result.kind === 'failed') return `backlogrun: could not start the supervisor: ${result.note}`
|
|
81
|
+
|
|
82
|
+
return `backlogrun: supervisor started (pid ${String(result.pid)})`
|
|
83
|
+
}
|
|
84
|
+
|
|
51
85
|
// The agent starts in the main checkout, the parent of the common git directory every record is keyed on.
|
|
52
86
|
function target_of(git_directory: string): LaunchTarget {
|
|
53
87
|
return { worktree: path.dirname(git_directory), git_directory }
|
|
54
88
|
}
|
|
55
89
|
|
|
56
|
-
async function
|
|
90
|
+
async function launch(
|
|
57
91
|
args: BacklogrunArguments,
|
|
58
92
|
git_directory: string,
|
|
59
93
|
ports: BacklogrunPorts,
|
|
60
94
|
): Promise<number> {
|
|
61
|
-
if (ports.is_running(git_directory)) {
|
|
62
|
-
for (const note of running_notes(args.issues)) ports.write(note)
|
|
63
|
-
|
|
64
|
-
return await ports.show_board()
|
|
65
|
-
}
|
|
66
|
-
|
|
67
95
|
const result = ports.start(args, target_of(git_directory))
|
|
68
96
|
|
|
69
97
|
if (result.kind === 'failed') {
|
|
@@ -73,29 +101,73 @@ async function begin(
|
|
|
73
101
|
}
|
|
74
102
|
|
|
75
103
|
ports.write(launched_note(result))
|
|
104
|
+
ports.write(supervisor_note(ports.start_supervisor(git_directory, args.provider)))
|
|
105
|
+
|
|
106
|
+
return await ports.show_board()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The idle run is stopped as `--stop` stops it, and a run left with an owner this command may not
|
|
110
|
+
// signal is not replaced: nothing is started beside it.
|
|
111
|
+
async function replace(
|
|
112
|
+
args: BacklogrunArguments,
|
|
113
|
+
git_directory: string,
|
|
114
|
+
ports: BacklogrunPorts,
|
|
115
|
+
): Promise<number> {
|
|
116
|
+
ports.write(`backlogrun: replacing the idle run with \`${args.invocation}\``)
|
|
117
|
+
|
|
118
|
+
const report = await ports.stop(git_directory)
|
|
119
|
+
|
|
120
|
+
for (const line of backlogrun_stop.lines_of(report)) ports.write(line)
|
|
121
|
+
|
|
122
|
+
if (!backlogrun_stop.is_stopped(report)) return FAILURE_EXIT_CODE
|
|
123
|
+
|
|
124
|
+
return await launch(args, git_directory, ports)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function begin(
|
|
128
|
+
args: BacklogrunArguments,
|
|
129
|
+
git_directory: string,
|
|
130
|
+
ports: BacklogrunPorts,
|
|
131
|
+
): Promise<number> {
|
|
132
|
+
if (!ports.is_running(git_directory)) return await launch(args, git_directory, ports)
|
|
133
|
+
|
|
134
|
+
const verdict = await ports.replacement(args.invocation, git_directory)
|
|
135
|
+
|
|
136
|
+
if (verdict.kind === 'replace') return await replace(args, git_directory, ports)
|
|
137
|
+
|
|
138
|
+
for (const note of [...kept_notes(verdict), ...running_notes(args.issues)]) ports.write(note)
|
|
76
139
|
|
|
77
140
|
return await ports.show_board()
|
|
78
141
|
}
|
|
79
142
|
|
|
143
|
+
// Stopping starts nothing and draws no board: the lines say what was stopped, and a run left with an
|
|
144
|
+
// owner this command may not signal is a failure the exit code carries.
|
|
145
|
+
async function stop(git_directory: string, ports: BacklogrunPorts): Promise<number> {
|
|
146
|
+
const report = await ports.stop(git_directory)
|
|
147
|
+
|
|
148
|
+
for (const line of backlogrun_stop.lines_of(report)) ports.write(line)
|
|
149
|
+
|
|
150
|
+
return backlogrun_stop.is_stopped(report) ? SUCCESS_EXIT_CODE : FAILURE_EXIT_CODE
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function refuse(line: string, ports: BacklogrunPorts): number {
|
|
154
|
+
ports.write(line)
|
|
155
|
+
|
|
156
|
+
return FAILURE_EXIT_CODE
|
|
157
|
+
}
|
|
158
|
+
|
|
80
159
|
async function run(
|
|
81
160
|
argv: ReadonlyArray<string>,
|
|
82
161
|
ports: BacklogrunPorts = LIVE_PORTS,
|
|
83
162
|
): Promise<number> {
|
|
84
163
|
const parsed = backlogrun_args.parse(argv)
|
|
85
164
|
|
|
86
|
-
if (parsed.kind === 'refused')
|
|
87
|
-
ports.write(USAGE)
|
|
88
|
-
|
|
89
|
-
return FAILURE_EXIT_CODE
|
|
90
|
-
}
|
|
165
|
+
if (parsed.kind === 'refused') return refuse(USAGE, ports)
|
|
91
166
|
|
|
92
167
|
const git_directory = await ports.repository_directory()
|
|
93
168
|
|
|
94
|
-
if (git_directory === undefined)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return FAILURE_EXIT_CODE
|
|
98
|
-
}
|
|
169
|
+
if (git_directory === undefined) return refuse('backlogrun: not inside a git repository', ports)
|
|
170
|
+
if (parsed.kind === 'stop') return await stop(git_directory, ports)
|
|
99
171
|
|
|
100
172
|
return await begin(parsed.args, git_directory, ports)
|
|
101
173
|
}
|
|
@@ -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
|
}
|