@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { auto_ok_judge_read, type JudgedCache } from '#scripts/auto-ok/auto-ok-judge-read'
|
|
4
5
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
5
6
|
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
6
7
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
@@ -166,6 +167,18 @@ function end_line(end: DriveEnd): string {
|
|
|
166
167
|
return end.detail === undefined ? base : `${base} ${end.detail}`
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
// The count of open issues owing an `auto-ok` judgement, over a cache kept for the process so a watch
|
|
171
|
+
// minute reads the comments only of the issues that moved (`auto-ok-judge-read.ts`).
|
|
172
|
+
function judgement_reader(): () => Promise<number> {
|
|
173
|
+
const cache: JudgedCache = new Map()
|
|
174
|
+
|
|
175
|
+
return async () => {
|
|
176
|
+
const owed = await auto_ok_judge_read.owed(cache)
|
|
177
|
+
|
|
178
|
+
return owed?.length ?? 0
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
169
182
|
function ports_of(
|
|
170
183
|
context: DriveContext,
|
|
171
184
|
seeded: ReadonlyArray<string>,
|
|
@@ -177,6 +190,7 @@ function ports_of(
|
|
|
177
190
|
offer: async (state) => await read_offer(state, context),
|
|
178
191
|
launch: async (issue) => await backlog_drive_launch.launch(issue, context.owner),
|
|
179
192
|
free_lanes: backlog_ready.drive_free_lane_count,
|
|
193
|
+
judgement_owed: judgement_reader(),
|
|
180
194
|
now: () => new Date(),
|
|
181
195
|
sleep: async (milliseconds) => {
|
|
182
196
|
await sleep(milliseconds)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { epic_triage } from '#scripts/epic/epic-triage'
|
|
1
2
|
import { git_gh_repo } from '#scripts/gh/git-gh-repo'
|
|
2
3
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
3
4
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
@@ -12,7 +13,12 @@ import { backlog_offer } from './backlog-offer'
|
|
|
12
13
|
// is runnable, without `run:add` and without waking a parent session.
|
|
13
14
|
//
|
|
14
15
|
// **Only what needs a person is handed back.** `stop` means every child left waits on a person, which a
|
|
15
|
-
// judgment session reads.
|
|
16
|
+
// judgment session reads. `triage` means a child carries neither `run:solo` nor `run:lane`, so
|
|
17
|
+
// `epic:next --lanes` offers nothing until a session judges it — it exits 0 and repeats on every pass,
|
|
18
|
+
// so waiting on it would never end. It is handed back under its own word, as the pool path's is, never
|
|
19
|
+
// as `epic #N`: that verdict sends the session to run the epic's children, and the single-child
|
|
20
|
+
// `epic:next` it asks there is not triage-gated, so it would launch the untriaged child instead of
|
|
21
|
+
// labelling it. A failed `epic:next` is either a network blip or a broken graph (a cycle, an
|
|
16
22
|
// undeclared order, an unreadable child), and its exit code does not tell them apart — so a failure is
|
|
17
23
|
// a `wait` that retries on the next pass, and `backlog:offer`'s consecutive-retry limit of them hands the
|
|
18
24
|
// epic back too, rather than waiting out the idle budget on an anomaly nobody is shown.
|
|
@@ -21,6 +27,7 @@ const NO_RETRIES = 0
|
|
|
21
27
|
const SUCCESS_EXIT_CODE = 0
|
|
22
28
|
const FIRST_LINE = 0
|
|
23
29
|
const WAIT: OfferRead = { verdict: 'wait', issues: [], retries: NO_RETRIES }
|
|
30
|
+
const TRIAGE: OfferRead = { ...WAIT, verdict: epic_triage.TRIAGE_VERDICT }
|
|
24
31
|
|
|
25
32
|
// `--lanes` lists every child a free lane could take, so one the loop already knows about is passed
|
|
26
33
|
// over rather than standing in front of the rest. `undefined` is an answer that could not be read.
|
|
@@ -53,22 +60,31 @@ function child_offer(children: ReadonlyArray<string>, state: DriveState): OfferR
|
|
|
53
60
|
return child === undefined ? WAIT : { verdict: 'run', issues: [child], retries: NO_RETRIES }
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
// The answers only a judgment session can move.
|
|
64
|
+
function judgment_offer(epic: string, token: string): OfferRead | undefined {
|
|
65
|
+
if (token === 'stop') return handed_back(epic)
|
|
66
|
+
|
|
67
|
+
return token === epic_triage.TRIAGE_VERDICT ? TRIAGE : undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
async function answered_offer(
|
|
57
71
|
epic: string,
|
|
58
72
|
answers: ReadonlyArray<string>,
|
|
59
73
|
state: DriveState,
|
|
60
74
|
owner: string,
|
|
61
75
|
): Promise<OfferRead> {
|
|
62
|
-
const token = answers[FIRST_LINE]
|
|
76
|
+
const token = answers[FIRST_LINE] ?? ''
|
|
63
77
|
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (token === 'stop') return handed_back(epic)
|
|
78
|
+
if (issue_number_shape.ISSUE_NUMBER_PATTERN.test(token)) return child_offer(answers, state)
|
|
79
|
+
const judgment = judgment_offer(epic, token)
|
|
80
|
+
if (judgment !== undefined) return judgment
|
|
69
81
|
|
|
70
82
|
if (token === 'complete') {
|
|
71
|
-
await backlog_drive_named.mark_done(
|
|
83
|
+
await backlog_drive_named.mark_done(
|
|
84
|
+
epic,
|
|
85
|
+
{ outcome: 'merged', code: 0, token: 'none', is_offer: true },
|
|
86
|
+
owner,
|
|
87
|
+
)
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
return WAIT
|
|
@@ -33,6 +33,9 @@ async function launch(issue: string, owner: string): Promise<LaunchOutcome['kind
|
|
|
33
33
|
|
|
34
34
|
const outcome = await lane_launch_cli.launch_lane({ issue, stash: await latest_stash() })
|
|
35
35
|
|
|
36
|
+
// A refusal the driver waits out writes nothing else, so the lane's stage is ended here.
|
|
37
|
+
if (outcome.kind !== 'launched') await run_event_setup.emit(run_event_setup.STAGE.NO_CHILD)
|
|
38
|
+
|
|
36
39
|
return outcome.kind
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -10,6 +10,7 @@ import { backlog_budget } from './backlog-budget'
|
|
|
10
10
|
import type { DriveState, OfferRead } from './backlog-drive'
|
|
11
11
|
import { backlog_drive_epic } from './backlog-drive-epic'
|
|
12
12
|
import { backlog_drive_named } from './backlog-drive-named'
|
|
13
|
+
import { backlog_drive_unread } from './backlog-drive-unread'
|
|
13
14
|
|
|
14
15
|
interface NamedContext {
|
|
15
16
|
is_only: boolean
|
|
@@ -67,12 +68,16 @@ async function classify(
|
|
|
67
68
|
): Promise<OfferRead> {
|
|
68
69
|
const result = await issue_state_cli.read_issue(issue)
|
|
69
70
|
|
|
70
|
-
if (result.kind !== 'state') return
|
|
71
|
+
if (result.kind !== 'state') return backlog_drive_unread.offer(issue, result, state.retries)
|
|
71
72
|
|
|
72
73
|
const outcome = settled_outcome(result.state)
|
|
73
74
|
|
|
74
75
|
if (outcome !== undefined) {
|
|
75
|
-
await backlog_drive_named.mark_done(
|
|
76
|
+
await backlog_drive_named.mark_done(
|
|
77
|
+
issue,
|
|
78
|
+
{ outcome, code: 0, token: 'none', is_offer: true },
|
|
79
|
+
owner,
|
|
80
|
+
)
|
|
76
81
|
|
|
77
82
|
return { verdict: 'wait', issues: [], retries: NO_RETRIES }
|
|
78
83
|
}
|
|
@@ -55,7 +55,9 @@ function is_done(result: MergeResult): boolean {
|
|
|
55
55
|
return result.code === 0 && result.token !== MERGE_TOKEN.BUSY
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
return
|
|
58
|
+
return (
|
|
59
|
+
result.outcome === 'failed' && result.code === 0 && !run_merge_token.is_control_stop(result)
|
|
60
|
+
)
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
function can_mark(carry: RunCarry, issue: string, owner: string): boolean {
|
|
@@ -125,7 +127,7 @@ async function reconcile(
|
|
|
125
127
|
const outcome = parked.has(issue) ? 'parked' : 'merged'
|
|
126
128
|
|
|
127
129
|
// eslint-disable-next-line no-await-in-loop -- each mark rewrites the same carry file
|
|
128
|
-
await mark_done(String(issue), { outcome, code: 0, token: 'none' }, owner)
|
|
130
|
+
await mark_done(String(issue), { outcome, code: 0, token: 'none', is_offer: true }, owner)
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
|
+
import type { StateRead } from '#scripts/issue/issue-state-cli'
|
|
3
|
+
import type { OfferRead } from './backlog-drive'
|
|
4
|
+
import { backlog_offer } from './backlog-offer'
|
|
5
|
+
|
|
6
|
+
// **A named issue whose state did not read is asked again before a session hears of it.** The read is
|
|
7
|
+
// one `gh` request, and a dropped connection fails it for a moment and no longer: handing that to a
|
|
8
|
+
// session costs a wake that can only cut and hand the loop back. So the failure is a `wait` that reads
|
|
9
|
+
// again on the next pass — the driver keeps collecting children meanwhile — and `backlog:offer`'s
|
|
10
|
+
// consecutive-retry limit bounds it, the count `backlog-drive-epic.ts` advances for a failed `epic:next`.
|
|
11
|
+
//
|
|
12
|
+
// **What is handed back is `offer`, with the issue and the reason on stderr.** That is the row a session
|
|
13
|
+
// already reads for an offer that produced nothing to start: report what stderr printed, pick no issue
|
|
14
|
+
// by hand. A number that resolves to nothing is an answer rather than a gap, so it is not asked again.
|
|
15
|
+
|
|
16
|
+
type UnreadState = Exclude<StateRead, { kind: 'state' }>
|
|
17
|
+
|
|
18
|
+
const NO_RETRIES = 0
|
|
19
|
+
// The loop's own word for an offer it could not use (`backlog-drive.ts` → `ask_offer`).
|
|
20
|
+
const HANDED_BACK = 'offer'
|
|
21
|
+
const UNKNOWN_REASON = 'unknown reason'
|
|
22
|
+
|
|
23
|
+
function cause_of(read: UnreadState): string {
|
|
24
|
+
if (read.kind === 'missing') return 'the number does not resolve'
|
|
25
|
+
const status = read.status === undefined ? [] : [`status ${String(read.status)}`]
|
|
26
|
+
|
|
27
|
+
return [read.reason ?? UNKNOWN_REASON, ...status].join(', ')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function note_of(issue: string, read: UnreadState): string {
|
|
31
|
+
return `The state of named issue ${issue_cite.plain(issue)} could not be read (${cause_of(read)})`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handed_back(note: string): OfferRead {
|
|
35
|
+
console.error(note)
|
|
36
|
+
|
|
37
|
+
return { verdict: HANDED_BACK, issues: [], retries: NO_RETRIES }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// `retries` is the count of consecutive failed offers before this one. A named epic's failed
|
|
41
|
+
// `epic:next` advances it too, so the note counts offers, never state reads.
|
|
42
|
+
function offer(issue: string, read: UnreadState, retries: number): OfferRead {
|
|
43
|
+
const note = note_of(issue, read)
|
|
44
|
+
if (read.kind === 'missing') return handed_back(`${note}; check the number and the repository.`)
|
|
45
|
+
const failures = retries + 1
|
|
46
|
+
|
|
47
|
+
if (failures >= backlog_offer.RETRY_LIMIT) {
|
|
48
|
+
return handed_back(`${note}; the offer failed ${String(failures)} times in a row.`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.error(`${note}; reading it again.`)
|
|
52
|
+
|
|
53
|
+
return { verdict: 'wait', issues: [], retries: failures }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const backlog_drive_unread = { HANDED_BACK, offer }
|
|
@@ -26,10 +26,14 @@ const RUN_VERDICT = 'run'
|
|
|
26
26
|
const STOP_VERDICT = 'stop'
|
|
27
27
|
// How the loop reads each `run:merge` token — keyed by the producer's own type, so a token added there
|
|
28
28
|
// is a type error here until it is classified. `hand-back` ends the loop at once: a hand-off, a person's
|
|
29
|
-
// stop, the environment down, a foreign owner, an unreadable state. `stop` —
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
29
|
+
// stop, the environment down, a foreign owner, an unreadable state. `stop` — the failure guard, or a
|
|
30
|
+
// failed child left unparked — collects the child, starts nothing more, and collects the children still
|
|
31
|
+
// in flight before the loop ends. `await` is a cut resumed in place: the same lane is awaited again.
|
|
32
|
+
//
|
|
33
|
+
// **The offer `run:merge` passes through is never read here** (joshuafolkken/kit#3781): its `stop` —
|
|
34
|
+
// only person-waiting issues left — is spelled like the control one, and acting on it ended the run
|
|
35
|
+
// without the idle watch. The loop asks `backlog:offer` on the next pass instead, so whether a parked
|
|
36
|
+
// backlog waits or ends is `backlog:budget`'s alone, after a merge as after any other pass.
|
|
33
37
|
type TokenHandling = 'hand-back' | 'stop' | 'await'
|
|
34
38
|
const TOKEN_HANDLING: Readonly<Record<MergeToken, TokenHandling>> = {
|
|
35
39
|
[MERGE_TOKEN.OVER]: 'hand-back',
|
|
@@ -61,6 +65,8 @@ const KNOWN_VERDICTS: ReadonlySet<string> = new Set([
|
|
|
61
65
|
])
|
|
62
66
|
const NO_RETRIES = 0
|
|
63
67
|
const NO_FREE_LANES = 0
|
|
68
|
+
const EXHAUSTED_ANSWER = 'exhausted'
|
|
69
|
+
const JUDGE_REASON = 'judge'
|
|
64
70
|
const LAUNCHED = 'launched'
|
|
65
71
|
const UNOPENED = 'unopened'
|
|
66
72
|
|
|
@@ -94,7 +100,8 @@ interface DriveState {
|
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
interface DriveEnd {
|
|
97
|
-
// Why the loop handed control back: `merge`, `launch`, `offer`, `watch`, `stop`, `window`
|
|
103
|
+
// Why the loop handed control back: `merge`, `launch`, `offer`, `judge`, `watch`, `stop`, `window`
|
|
104
|
+
// or a verdict.
|
|
98
105
|
reason: string
|
|
99
106
|
// The token the parent reads, where one command printed it.
|
|
100
107
|
token: string | undefined
|
|
@@ -103,7 +110,7 @@ interface DriveEnd {
|
|
|
103
110
|
is_finish?: boolean | undefined
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
type Collected = Pick<MergeResult, 'token' | 'outcome'>
|
|
113
|
+
type Collected = Pick<MergeResult, 'token' | 'outcome' | 'is_offer'>
|
|
107
114
|
|
|
108
115
|
interface DrivePorts {
|
|
109
116
|
// Whether a child's process has confirmed-ended — `lane:await`'s re-confirmed check in production.
|
|
@@ -117,6 +124,9 @@ interface DrivePorts {
|
|
|
117
124
|
// The lanes free right now — `backlog_ready.drive_free_lane_count` in production, the limit less the
|
|
118
125
|
// open lanes.
|
|
119
126
|
free_lanes: () => Promise<number>
|
|
127
|
+
// How many open issues lacking `auto-ok` still owe a judgement — `auto_ok_judge_read.owed` in
|
|
128
|
+
// production, `0` when the read failed.
|
|
129
|
+
judgement_owed: () => Promise<number>
|
|
120
130
|
now: () => Date
|
|
121
131
|
}
|
|
122
132
|
|
|
@@ -145,7 +155,7 @@ function ended(reason: string, state: DriveState, token?: string, issue?: string
|
|
|
145
155
|
function is_drain(read: OfferRead, state: DriveState): boolean {
|
|
146
156
|
return (
|
|
147
157
|
read.verdict === WATCH_VERDICT &&
|
|
148
|
-
read.answer ===
|
|
158
|
+
read.answer === EXHAUSTED_ANSWER &&
|
|
149
159
|
state.in_flight.length === 0 &&
|
|
150
160
|
read.is_retrospective_owed === true
|
|
151
161
|
)
|
|
@@ -154,7 +164,7 @@ function is_drain(read: OfferRead, state: DriveState): boolean {
|
|
|
154
164
|
function needs_retrospective(read: OfferRead, state: DriveState): boolean {
|
|
155
165
|
return (
|
|
156
166
|
read.verdict === STOP_VERDICT &&
|
|
157
|
-
read.answer ===
|
|
167
|
+
read.answer === EXHAUSTED_ANSWER &&
|
|
158
168
|
state.in_flight.length === 0 &&
|
|
159
169
|
read.is_retrospective_owed === true
|
|
160
170
|
)
|
|
@@ -209,15 +219,19 @@ function collected_state(
|
|
|
209
219
|
// One finished child: handed back on an immediate token, otherwise collected.
|
|
210
220
|
async function collect(issue: string, state: DriveState, ports: DrivePorts): Promise<PassResult> {
|
|
211
221
|
const collected = await ports.merge(issue)
|
|
212
|
-
const { token } = collected
|
|
222
|
+
const { token, is_offer } = collected
|
|
213
223
|
|
|
214
224
|
// An empty first line is a `run:merge` that failed before printing one: never read as a collection.
|
|
225
|
+
// An offer's `retry` is `backlog:next`'s transport answer, left to the next `backlog:offer`.
|
|
215
226
|
if (token === '') return ended('merge', state, undefined, issue)
|
|
216
|
-
if (IMMEDIATE_TOKENS.has(token)) return ended('merge', state, token, issue)
|
|
227
|
+
if (!is_offer && IMMEDIATE_TOKENS.has(token)) return ended('merge', state, token, issue)
|
|
217
228
|
|
|
218
229
|
const next = collected_state(issue, collected, state, ports.now().toISOString())
|
|
219
230
|
|
|
220
|
-
return {
|
|
231
|
+
return {
|
|
232
|
+
kind: 'continue',
|
|
233
|
+
state: run_merge_token.is_control_stop(collected) ? merge_stopped(issue, next) : next,
|
|
234
|
+
}
|
|
221
235
|
}
|
|
222
236
|
|
|
223
237
|
async function collect_finished(state: DriveState, ports: DrivePorts): Promise<PassResult> {
|
|
@@ -310,6 +324,18 @@ function on_verdict(read: OfferRead, state: DriveState): PassResult {
|
|
|
310
324
|
return { kind: 'continue', state }
|
|
311
325
|
}
|
|
312
326
|
|
|
327
|
+
// **A free lane with nothing opted in is the open backlog's turn to be judged**
|
|
328
|
+
// (joshuafolkken/kit#3854): a watch over an exhausted pool hands back `judge` while an open issue
|
|
329
|
+
// lacking `auto-ok` still owes a judgement, ahead of the drain it would otherwise be. Once every one
|
|
330
|
+
// carries its judgement comment the count is zero and the watch is the driver's again.
|
|
331
|
+
async function is_judgement_owed(read: OfferRead, ports: DrivePorts): Promise<boolean> {
|
|
332
|
+
const is_exhausted = read.verdict === WATCH_VERDICT && read.answer === EXHAUSTED_ANSWER
|
|
333
|
+
|
|
334
|
+
if (!is_exhausted || (await ports.free_lanes()) <= NO_FREE_LANES) return false
|
|
335
|
+
|
|
336
|
+
return (await ports.judgement_owed()) > 0
|
|
337
|
+
}
|
|
338
|
+
|
|
313
339
|
async function ask_offer(state: DriveState, ports: DrivePorts): Promise<PassResult> {
|
|
314
340
|
const read = await ports.offer(state)
|
|
315
341
|
|
|
@@ -318,6 +344,7 @@ async function ask_offer(state: DriveState, ports: DrivePorts): Promise<PassResu
|
|
|
318
344
|
const next = { ...state, retries: read.retries }
|
|
319
345
|
|
|
320
346
|
if (read.verdict === RUN_VERDICT) return await launch_all(read.issues, next, ports)
|
|
347
|
+
if (await is_judgement_owed(read, ports)) return ended(JUDGE_REASON, next, JUDGE_REASON)
|
|
321
348
|
|
|
322
349
|
return on_verdict(read, next)
|
|
323
350
|
}
|
|
@@ -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
|
}
|