@joshuafolkken/kit 1.1383.0 → 1.1435.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
- package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
- package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
- package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
- package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +262 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +124 -15
- package/dist/hooks/stop-guard.js +98 -55
- package/dist/josh.js +63 -10
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-cli.ts +2 -4
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +5 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-owner.ts +72 -5
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +80 -5
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +30 -4
- package/scripts/init/start-steps.ts +77 -3
- package/scripts/init/start.ts +21 -13
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +25 -1
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +139 -37
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +58 -5
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +121 -0
- package/scripts/run/board/run-board-span.ts +49 -7
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +76 -13
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +53 -16
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-added.ts +1 -1
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-change.ts +1 -1
- package/scripts/run/carry/run-carry-cli.ts +39 -52
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry-ended.ts +9 -1
- package/scripts/run/carry/run-carry.ts +24 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +3 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +91 -0
- package/scripts/run/event/run-event-stream.ts +46 -9
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/run-stranded-detect.ts +58 -12
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +108 -74
- package/scripts/run/wake/run-wake-describe.ts +36 -2
- package/scripts/run/wake/run-wake-driver.ts +21 -5
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -78,9 +78,8 @@ interface SeparateContext {
|
|
|
78
78
|
repo: string
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// The path each
|
|
82
|
-
function
|
|
83
|
-
const holders = read.kind === 'busy' ? read.issues.map((issue) => issue.number) : []
|
|
81
|
+
// The path each of `holders` declares, mapped to the issue that holds it.
|
|
82
|
+
function claims_of(declared: Declared, holders: ReadonlyArray<number>): Map<string, number> {
|
|
84
83
|
const pairs = holders.flatMap((number) =>
|
|
85
84
|
[...(declared.get(number) ?? [])].map((path): [string, number] => [path, number]),
|
|
86
85
|
)
|
|
@@ -88,6 +87,10 @@ function running_claims(declared: Declared, read: BusyRead): Map<string, number>
|
|
|
88
87
|
return new Map(pairs)
|
|
89
88
|
}
|
|
90
89
|
|
|
90
|
+
function running_claims(declared: Declared, read: BusyRead): Map<string, number> {
|
|
91
|
+
return claims_of(declared, read.kind === 'busy' ? read.issues.map((issue) => issue.number) : [])
|
|
92
|
+
}
|
|
93
|
+
|
|
91
94
|
function paths_of(child: EpicChild, declared: Declared, repo: string): ReadonlySet<string> {
|
|
92
95
|
if (child.repo !== repo) return new Set()
|
|
93
96
|
|
|
@@ -113,6 +116,20 @@ function clash_of(
|
|
|
113
116
|
return clashes[0]
|
|
114
117
|
}
|
|
115
118
|
|
|
119
|
+
// The running issues a candidate is weighed against, and the repository they are in.
|
|
120
|
+
interface Holding {
|
|
121
|
+
holders: ReadonlyArray<number>
|
|
122
|
+
repo: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The path `child` declares that one of the running `holders` already declares — the clash `separate`
|
|
126
|
+
// withholds a candidate on, for a reader that names the running issues itself (`run:board`).
|
|
127
|
+
function running_clash(child: EpicChild, declared: Declared, holding: Holding): Clash | undefined {
|
|
128
|
+
const claims = claims_of(declared, holding.holders)
|
|
129
|
+
|
|
130
|
+
return clash_of(claims, paths_of(child, declared, holding.repo))
|
|
131
|
+
}
|
|
132
|
+
|
|
116
133
|
function clash_note(child: EpicChild, clash: Clash): string {
|
|
117
134
|
const waiting = session_cite.issue(child.number, undefined, child.repo)
|
|
118
135
|
const holder = session_cite.issue(clash.holder, undefined, child.repo)
|
|
@@ -172,7 +189,7 @@ function separate(
|
|
|
172
189
|
return { offered: walk.offered, withheld, ...(notice !== undefined && { notice }) }
|
|
173
190
|
}
|
|
174
191
|
|
|
175
|
-
const backlog_overlap = { declared_of, declared_paths, separate }
|
|
192
|
+
const backlog_overlap = { declared_of, declared_paths, running_clash, separate }
|
|
176
193
|
|
|
177
194
|
export { backlog_overlap }
|
|
178
|
-
export type { Declared }
|
|
195
|
+
export type { Clash, Declared }
|
|
@@ -25,9 +25,7 @@ interface ReadyPorts {
|
|
|
25
25
|
|
|
26
26
|
// The lanes in use: the open worktrees an interruption has not stranded. `josh lane:limit` prints it.
|
|
27
27
|
async function live_lane_count(): Promise<number> {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return lanes.filter((lane) => !lane.is_stranded).length
|
|
28
|
+
return lane_registry.live_count(await lane_registry.list_lanes())
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
async function free_under(limit: number): Promise<number> {
|
|
@@ -9,8 +9,12 @@ import { run_invocation } from '#scripts/run/run-invocation'
|
|
|
9
9
|
//
|
|
10
10
|
// **A bare number is an issue.** A shell reads an unquoted `#3437` as a comment, so `3437` in the named
|
|
11
11
|
// list is taken as `#3437`; a number after the first flag is that flag's value and is left alone.
|
|
12
|
+
//
|
|
13
|
+
// **`--stop` stands alone.** It stops the run rather than starting one, so beside an issue or any other
|
|
14
|
+
// flag it is refused before anything is stopped — a mistyped start must never end a run.
|
|
12
15
|
|
|
13
16
|
const AGENT_FLAG = '--agent'
|
|
17
|
+
const STOP_FLAG = '--stop'
|
|
14
18
|
const BACKLOG_COMMAND = 'backlogrun'
|
|
15
19
|
const ISSUE_PREFIX = '#'
|
|
16
20
|
const FLAG_PREFIX = '-'
|
|
@@ -26,7 +30,8 @@ interface BacklogrunArguments {
|
|
|
26
30
|
issues: ReadonlyArray<number>
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
type ArgumentsResult =
|
|
33
|
+
type ArgumentsResult =
|
|
34
|
+
{ kind: 'args'; args: BacklogrunArguments } | { kind: 'stop' } | { kind: 'refused' }
|
|
30
35
|
|
|
31
36
|
interface AgentSplit {
|
|
32
37
|
agent: string | undefined
|
|
@@ -56,7 +61,7 @@ function normalized(rest: ReadonlyArray<string>): Array<string> {
|
|
|
56
61
|
return [...rest.slice(0, split_at).map((token) => issue_token(token)), ...rest.slice(split_at)]
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
function
|
|
64
|
+
function parse_start(argv: ReadonlyArray<string>): ArgumentsResult {
|
|
60
65
|
const { agent, rest } = split_agent(argv)
|
|
61
66
|
const provider = agent === undefined ? undefined : AGENTS[agent]
|
|
62
67
|
const invocation = run_invocation.rebuild([BACKLOG_COMMAND, ...normalized(rest)].join(' '))
|
|
@@ -68,6 +73,12 @@ function parse(argv: ReadonlyArray<string>): ArgumentsResult {
|
|
|
68
73
|
return { kind: 'args', args: { provider, invocation, issues } }
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
function parse(argv: ReadonlyArray<string>): ArgumentsResult {
|
|
77
|
+
if (!argv.includes(STOP_FLAG)) return parse_start(argv)
|
|
78
|
+
|
|
79
|
+
return { kind: argv.length === 1 ? 'stop' : 'refused' }
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
const backlogrun_args = { parse }
|
|
72
83
|
|
|
73
84
|
export type { BacklogrunArguments }
|
|
@@ -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
|
}
|
|
@@ -11,6 +11,7 @@ import { stamp_file } from '#scripts/josh/stamp-file'
|
|
|
11
11
|
import { json_value } from '#scripts/lib/json-value'
|
|
12
12
|
import { run_carry, type CarryRead } from '#scripts/run/carry/run-carry'
|
|
13
13
|
import { detached_launch, type LaunchResult } from '#scripts/run/detached-launch'
|
|
14
|
+
import { run_event_setup } from '#scripts/run/event/run-event-setup'
|
|
14
15
|
import { run_headless } from '#scripts/run/run-headless'
|
|
15
16
|
import { z } from 'zod'
|
|
16
17
|
|
|
@@ -34,10 +35,20 @@ const launch_schema = z.object({
|
|
|
34
35
|
pid: z.number(),
|
|
35
36
|
start: z.string().optional(),
|
|
36
37
|
session: z.string().optional(),
|
|
38
|
+
// What the parent was started to run and when, so `run:board` has a run to draw and a clock to count
|
|
39
|
+
// before the parent claims the carry record. Absent on a record an earlier version wrote.
|
|
40
|
+
invocation: z.string().optional(),
|
|
41
|
+
started_at: z.string().optional(),
|
|
37
42
|
})
|
|
38
43
|
|
|
39
44
|
type LaunchRecord = z.infer<typeof launch_schema>
|
|
40
45
|
|
|
46
|
+
// A launched run the parent has not claimed yet, as far as its record says.
|
|
47
|
+
interface PendingLaunch {
|
|
48
|
+
invocation: string
|
|
49
|
+
started_at: string
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
interface LaunchTarget {
|
|
42
53
|
// The checkout the parent runs in, and its common git directory the records are keyed on.
|
|
43
54
|
worktree: string
|
|
@@ -117,10 +128,71 @@ function note_to_stderr(note: string): void {
|
|
|
117
128
|
console.error(`backlogrun: ${note}`)
|
|
118
129
|
}
|
|
119
130
|
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
// **The record and the run's first stage carry one moment**, so the `starting session` event is never
|
|
132
|
+
// dated before the start the board measures from.
|
|
133
|
+
function record_launch(
|
|
134
|
+
git_directory: string,
|
|
135
|
+
launch: Pick<LaunchRecord, 'pid' | 'session' | 'invocation'>,
|
|
136
|
+
): void {
|
|
137
|
+
const now = new Date()
|
|
138
|
+
const identity = process_identity.read_start(launch.pid)
|
|
139
|
+
const record: LaunchRecord = { ...launch, start: identity, started_at: now.toISOString() }
|
|
140
|
+
|
|
141
|
+
stamp_file.replace_stamp(launch_path(git_directory), record)
|
|
142
|
+
run_event_setup.emit_in(git_directory, run_event_setup.STAGE.STARTING, now)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// The live launch record here, `undefined` once the parent it names is gone.
|
|
146
|
+
function live_launch(git_directory: string): LaunchRecord | undefined {
|
|
147
|
+
const record = read_launch(launch_path(git_directory))
|
|
148
|
+
|
|
149
|
+
return is_launch_live(record) ? record : undefined
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The launch `run:board` draws while no carry record stands: one whose record says what it runs and
|
|
153
|
+
// since when.
|
|
154
|
+
function pending_launch(git_directory: string): PendingLaunch | undefined {
|
|
155
|
+
const { invocation, started_at } = live_launch(git_directory) ?? {}
|
|
156
|
+
|
|
157
|
+
if (invocation === undefined || started_at === undefined) return undefined
|
|
158
|
+
|
|
159
|
+
return { invocation, started_at }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// When the launch that started `owner_pid` began, for `run:carry --begin` to start the run's clock
|
|
163
|
+
// from: **only the parent this launch started inherits it**, so a run typed inside a session — which has
|
|
164
|
+
// no launch record, or another parent's — still starts at its own `--begin`.
|
|
165
|
+
function started_for(git_directory: string, owner_pid: number | undefined): Date | undefined {
|
|
166
|
+
const record = live_launch(git_directory)
|
|
167
|
+
|
|
168
|
+
if (record?.started_at === undefined || record.pid !== owner_pid) return undefined
|
|
169
|
+
|
|
170
|
+
const started_ms = Date.parse(record.started_at)
|
|
171
|
+
|
|
172
|
+
return Number.isNaN(started_ms) ? undefined : new Date(started_ms)
|
|
173
|
+
}
|
|
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)
|
|
122
194
|
|
|
123
|
-
|
|
195
|
+
return record.pid
|
|
124
196
|
}
|
|
125
197
|
|
|
126
198
|
// The headless mark holds the parent's turn open while its lanes run, as `run:wake`'s does. The launch
|
|
@@ -160,7 +232,7 @@ function start(invocation: string, provider: AgentProvider, target: LaunchTarget
|
|
|
160
232
|
|
|
161
233
|
if (result.kind === 'failed') return result
|
|
162
234
|
|
|
163
|
-
record_launch(target.git_directory, result.pid, session)
|
|
235
|
+
record_launch(target.git_directory, { pid: result.pid, session, invocation })
|
|
164
236
|
|
|
165
237
|
const launched = { pid: result.pid, session, log_path: log_path(target.git_directory) }
|
|
166
238
|
|
|
@@ -171,8 +243,11 @@ const backlogrun_launch = {
|
|
|
171
243
|
argv_of,
|
|
172
244
|
chosen_environment,
|
|
173
245
|
is_running,
|
|
246
|
+
pending_launch,
|
|
174
247
|
start,
|
|
248
|
+
started_for,
|
|
249
|
+
stop_parent,
|
|
175
250
|
}
|
|
176
251
|
|
|
177
|
-
export type { Launched, LaunchTarget, StartResult }
|
|
252
|
+
export type { Launched, LaunchTarget, PendingLaunch, StartResult }
|
|
178
253
|
export { backlogrun_launch }
|
|
@@ -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 }
|