@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
|
@@ -25,7 +25,7 @@ import { run_merge_token, type MergeToken } from './run-merge-token'
|
|
|
25
25
|
// The contract is `run:carry`'s: **standard output carries one token** (or the newline-separated
|
|
26
26
|
// offer `epic:next --lanes` prints), and everything else is a captured subprocess or a quiet
|
|
27
27
|
// in-process call. The verdicts this command adds to the offer are `over` (hand off), `human-review`
|
|
28
|
-
// (stop — the child's own ending), `stop` (the consecutive-failure guard tripped) and `retry` (the
|
|
28
|
+
// (stop — the child's own ending), `stop` (the consecutive-failure guard tripped, or a failed child could not be parked) and `retry` (the
|
|
29
29
|
// child's state could not be read; re-read before deciding).
|
|
30
30
|
|
|
31
31
|
const ARGV_OFFSET = 2
|
|
@@ -163,13 +163,16 @@ function parse(argv: ReadonlyArray<string>): MergeContext | undefined {
|
|
|
163
163
|
interface ControlVerdict {
|
|
164
164
|
token: MergeToken
|
|
165
165
|
code: number
|
|
166
|
+
is_offer: false
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
// The next child offered in the collected one's place: the text `backlog:next` or `epic:next --lanes`
|
|
169
|
-
// printed, passed through as it was read, so no narrower type holds it.
|
|
170
|
+
// printed, passed through as it was read, so no narrower type holds it. `is_offer` keeps its `stop`
|
|
171
|
+
// apart from the control one (`run_merge_token.is_control_stop`).
|
|
170
172
|
interface OfferVerdict {
|
|
171
173
|
token: string
|
|
172
174
|
code: number
|
|
175
|
+
is_offer: true
|
|
173
176
|
}
|
|
174
177
|
|
|
175
178
|
type MergeVerdict = ControlVerdict | OfferVerdict
|
|
@@ -179,11 +182,11 @@ type MergeVerdict = ControlVerdict | OfferVerdict
|
|
|
179
182
|
type MergeResult = MergeVerdict & { outcome: ChildOutcome }
|
|
180
183
|
|
|
181
184
|
function emit(token: MergeToken, code: number): ControlVerdict {
|
|
182
|
-
return { token, code }
|
|
185
|
+
return { token, code, is_offer: false }
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
async function offered(ctx: MergeContext): Promise<OfferVerdict> {
|
|
186
|
-
return { token: await run_merge_steps.ask_next(ctx), code: SUCCESS_EXIT_CODE }
|
|
189
|
+
return { token: await run_merge_steps.ask_next(ctx), code: SUCCESS_EXIT_CODE, is_offer: true }
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
// Whether the child's exit record shows it could not reach the API — read only when `--output` named
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
2
|
+
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
3
|
+
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
4
|
+
import { run_ship_detach } from '#scripts/run/ship/run-ship-detach'
|
|
5
|
+
|
|
6
|
+
// Why `run:merge` parks a child whose detached ship has already ended (joshuafolkken/kit#3808).
|
|
7
|
+
//
|
|
8
|
+
// **A child that handed its region to `josh ship --detach` is parked for what its ship did.** A running
|
|
9
|
+
// ship is never parked (`merge_child` answers `shipping` first), so a park after a hand-off means the
|
|
10
|
+
// ship ended unmerged — and the session the ship relaunched to fix it ended too. The generic "session
|
|
11
|
+
// ended with its issue open" then hides the one fact a person needs: the stage the ship stopped at. The
|
|
12
|
+
// cause is that stage's `ship-stop` line, written by the supervisor since its own launch; a ship that
|
|
13
|
+
// ended without one (killed, or failed before any stage) is named by its result and its log instead.
|
|
14
|
+
|
|
15
|
+
type ShipEnding = ReturnType<typeof run_ship_detach.read_result>
|
|
16
|
+
|
|
17
|
+
const ENDED_RESULTS: ReadonlySet<string> = new Set(['failed', 'abnormal'])
|
|
18
|
+
|
|
19
|
+
type Events = ReadonlyArray<RunEvent>
|
|
20
|
+
|
|
21
|
+
// `child`'s events after its newest `ship-launch`, so an earlier launch's stop the relaunched session
|
|
22
|
+
// already fixed is never blamed for this ending. The record and the stream outlive a dispatch, so only a
|
|
23
|
+
// ship launched since the child's own newest dispatch is this attempt's: an earlier attempt's ended ship
|
|
24
|
+
// answers `undefined`, never this park.
|
|
25
|
+
function since_ship_launch(events: Events, child: string): Events | undefined {
|
|
26
|
+
const attempt = run_event_scope.since_own_launch(events, child)
|
|
27
|
+
const own = attempt.filter((event) => run_event_scope.issue_named(event) === child)
|
|
28
|
+
const launched_at = own.findLastIndex(
|
|
29
|
+
(event) => event.kind === run_event_stream.EVENT_KIND.SHIP_LAUNCH,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return launched_at === -1 ? undefined : own.slice(launched_at + 1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cause_of(ending: ShipEnding, events: Events, child: string): string | undefined {
|
|
36
|
+
if (ending === undefined || !ENDED_RESULTS.has(ending.result)) return undefined
|
|
37
|
+
|
|
38
|
+
const attempt = since_ship_launch(events, child)
|
|
39
|
+
if (attempt === undefined) return undefined
|
|
40
|
+
|
|
41
|
+
const stop = attempt.findLast((event) => event.kind === run_event_stream.EVENT_KIND.SHIP_STOP)
|
|
42
|
+
|
|
43
|
+
if (stop !== undefined) return `its detached ship stopped and was not fixed: ${stop.text}.`
|
|
44
|
+
|
|
45
|
+
return `its detached ship ended ${ending.result} before recording a stopped stage — pnpm josh ship --log ${child}.`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The ship's cause for `child`'s park, read from the record and the stream this checkout's repository
|
|
49
|
+
// keys on, or `undefined` when the child handed nothing to a ship or its ship succeeded.
|
|
50
|
+
function cause(child: string, directory: string = process.cwd()): string | undefined {
|
|
51
|
+
const repository = git_common_directory.repository(directory)
|
|
52
|
+
if (repository === undefined) return undefined
|
|
53
|
+
|
|
54
|
+
const events = run_event_stream.read_events(run_event_stream.target_of(repository))
|
|
55
|
+
|
|
56
|
+
return cause_of(run_ship_detach.read_result(repository, child), events, child)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const run_merge_ship = { cause, cause_of }
|
|
60
|
+
|
|
61
|
+
export { run_merge_ship }
|
|
@@ -9,6 +9,7 @@ import { NEEDS_DECISION_LABEL } from '#scripts/issue/issue-labels'
|
|
|
9
9
|
import { session_cite } from '#scripts/issue/session-cite'
|
|
10
10
|
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
11
11
|
import { lane_close } from '#scripts/lane/lane-close'
|
|
12
|
+
import { lane_preserve } from '#scripts/lane/lane-preserve'
|
|
12
13
|
import { lane_reap } from '#scripts/lane/lane-reap'
|
|
13
14
|
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
14
15
|
import { lane_relaunch } from '#scripts/lane/lane-relaunch'
|
|
@@ -24,6 +25,7 @@ import { run_carry_conversation } from '#scripts/run/carry/run-carry-conversatio
|
|
|
24
25
|
import { run_cut, type RunCut } from '#scripts/run/cut/run-cut'
|
|
25
26
|
import { run_label } from '#scripts/run/run-label'
|
|
26
27
|
import { run_merge } from './run-merge'
|
|
28
|
+
import { run_merge_ship } from './run-merge-ship'
|
|
27
29
|
|
|
28
30
|
// The result of attempting to apply a carry change. Distinguishing `refused` from `applied` lets
|
|
29
31
|
// callers surface the refusal as a hard failure rather than silently continuing on a carry that was
|
|
@@ -167,18 +169,8 @@ async function close_lane(child: string): Promise<void> {
|
|
|
167
169
|
await josh(['lane:close', child])
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
function preserved_comment(message: string): string {
|
|
171
|
-
return [
|
|
172
|
-
'The lane held uncommitted work when `run:merge` closed it, so it was stashed before the close:',
|
|
173
|
-
`- recover with \`pnpm josh stash:pop "${message}"\``,
|
|
174
|
-
].join('\n')
|
|
175
|
-
}
|
|
176
|
-
|
|
177
172
|
async function stash_work(child: string, directory: string): Promise<void> {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
await git_stash.push(message, directory)
|
|
181
|
-
await git_gh_issue_write.issue_try_comment(child, preserved_comment(message))
|
|
173
|
+
await lane_preserve.stash_work(child, directory, LANE_CLOSE_OCCASION, '`run:merge` closed it')
|
|
182
174
|
}
|
|
183
175
|
|
|
184
176
|
// Stash whatever the lane still holds uncommitted before its tree is removed: `lane:close` deletes by
|
|
@@ -344,13 +336,14 @@ async function park_failed(ctx: MergeContext, cause: string): Promise<FailedResu
|
|
|
344
336
|
}
|
|
345
337
|
|
|
346
338
|
// A child that ended unfinished: waiting when it records an open blocker (`do_waiting`), parked
|
|
347
|
-
// otherwise (`park_failed`) — an order already recorded is never a decision for a person.
|
|
348
|
-
|
|
339
|
+
// otherwise (`park_failed`) — an order already recorded is never a decision for a person. Without a
|
|
340
|
+
// cause of the caller's, a child whose detached ship ended is parked for the ship's stop.
|
|
341
|
+
async function do_failed(ctx: MergeContext, cause?: string): Promise<FailedResult> {
|
|
349
342
|
const blockers = await open_blockers(ctx.child)
|
|
350
343
|
|
|
351
344
|
if (blockers.length > 0) return await do_waiting(ctx, blockers)
|
|
352
345
|
|
|
353
|
-
return await park_failed(ctx, cause)
|
|
346
|
+
return await park_failed(ctx, cause ?? run_merge_ship.cause(ctx.child) ?? FAILED_CAUSE)
|
|
354
347
|
}
|
|
355
348
|
|
|
356
349
|
// The outcome of counting an outage: the record to read the streak against, and whether the count was
|
|
@@ -24,7 +24,21 @@ const MERGE_TOKEN = {
|
|
|
24
24
|
|
|
25
25
|
type MergeToken = (typeof MERGE_TOKEN)[keyof typeof MERGE_TOKEN]
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// Where the token came from: `run:merge`'s own control verdict, or the next offer (`backlog:next` /
|
|
28
|
+
// `epic:next`) passed through as it was read. The offer has a `stop` of its own — nothing ready to start
|
|
29
|
+
// is left — spelled like the control `stop`, so the source travels beside the token.
|
|
30
|
+
interface MergeTokenRead {
|
|
31
|
+
token: string
|
|
32
|
+
is_offer: boolean
|
|
33
|
+
}
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
// The single reading of "this merge stopped the run": the control `stop` alone. An offer's `stop` is a
|
|
36
|
+
// parked backlog, which `backlog:budget` decides — a watch while the idle budget lasts — never the merge.
|
|
37
|
+
function is_control_stop(read: MergeTokenRead): boolean {
|
|
38
|
+
return !read.is_offer && read.token === MERGE_TOKEN.STOP
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const run_merge_token = { MERGE_TOKEN, is_control_stop }
|
|
42
|
+
|
|
43
|
+
export type { MergeToken, MergeTokenRead }
|
|
30
44
|
export { run_merge_token }
|
package/scripts/run/run-step.ts
CHANGED
|
@@ -222,6 +222,8 @@ const FULL_INPUT_ACTIONS: Record<string, (input: StepInput) => StepAction> = {
|
|
|
222
222
|
[KIND.STOP]: stop_action,
|
|
223
223
|
[KIND.DRAIN]: drain_action,
|
|
224
224
|
[KIND.CHILD_LAUNCH]: child_launch_action,
|
|
225
|
+
// A person's restart re-dispatched the child, so the parent's position is a launch's.
|
|
226
|
+
[KIND.RESTART]: child_launch_action,
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
// An event the table does not name leaves the position unknown rather than guessing a next step. A
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { telegram_notify } from '#scripts/notify/telegram-notify'
|
|
2
2
|
import { run_carry, type CarryRead, type RunCarry } from '#scripts/run/carry/run-carry'
|
|
3
|
+
import { run_carry_ended } from '#scripts/run/carry/run-carry-ended'
|
|
3
4
|
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
4
5
|
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
5
6
|
import { run_wake, type RunWake } from '#scripts/run/wake/run-wake'
|
|
@@ -27,13 +28,14 @@ import {
|
|
|
27
28
|
// so recovery is another actor's.
|
|
28
29
|
const STRAND_RECOVERY = `Restart supervision with \`${run_stranded.RECOVERY_COMMAND}\` — it wakes a fresh successor to claim the budget; the cutting session must not resume it itself.`
|
|
29
30
|
|
|
30
|
-
// The seams a test replaces: the two record targets, the two record reads, the
|
|
31
|
-
// liveness probes, and the two side effects. The defaults are the real readers; a test hands fakes so
|
|
31
|
+
// The seams a test replaces: the two record targets, the two record reads, the ended-run read, the owner
|
|
32
|
+
// and supervisor liveness probes, and the two side effects. The defaults are the real readers; a test hands fakes so
|
|
32
33
|
// the gather and report logic run without a git tree, a process table or a network.
|
|
33
34
|
interface DetectPorts {
|
|
34
35
|
resolve_targets: () => Promise<RecordTargets | undefined>
|
|
35
36
|
read_carry: (target: string) => CarryRead
|
|
36
37
|
is_owner_live: (carry: RunCarry) => boolean
|
|
38
|
+
has_ended: (target: string, started_at: string) => boolean
|
|
37
39
|
read_wake: (target: string) => RunWake | undefined
|
|
38
40
|
supervisor_liveness: (wake: RunWake | undefined) => SupervisorLiveness
|
|
39
41
|
emit_stranded: (text: string) => Promise<boolean>
|
|
@@ -45,15 +47,25 @@ interface RecordTargets {
|
|
|
45
47
|
wake: string
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
// The run a supervisor was watching: its carry record, where `run:carry --end` records the run it
|
|
51
|
+
// removes, and the `started_at` the supervisor read when it took the run on.
|
|
52
|
+
interface WatchedRun {
|
|
53
|
+
carry_target: string
|
|
54
|
+
ended_target: string
|
|
55
|
+
started_at: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function targets_of(directory: string): RecordTargets {
|
|
59
|
+
return { carry: run_carry.carry_path(directory), wake: run_wake.wake_path(directory) }
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
// Both records key on the common git directory, the same one `run-carry.ts` and `run-wake.ts` resolve,
|
|
49
63
|
// so a strand read from any lane reads the one run's records. `undefined` outside a repository: no git
|
|
50
64
|
// directory to key on, so no run to be stranded.
|
|
51
65
|
async function real_resolve_targets(): Promise<RecordTargets | undefined> {
|
|
52
66
|
const directory = await run_carry.repository_directory()
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return { carry: run_carry.carry_path(directory), wake: run_wake.wake_path(directory) }
|
|
68
|
+
return directory === undefined ? undefined : targets_of(directory)
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
async function real_emit_stranded(text: string): Promise<boolean> {
|
|
@@ -68,6 +80,7 @@ const DEFAULT_PORTS: DetectPorts = {
|
|
|
68
80
|
resolve_targets: real_resolve_targets,
|
|
69
81
|
read_carry: (target) => run_carry.read_carry(target),
|
|
70
82
|
is_owner_live: run_carry.is_owner_live,
|
|
83
|
+
has_ended: run_carry_ended.has_ended,
|
|
71
84
|
read_wake: (target) => run_wake.read_wake(target),
|
|
72
85
|
supervisor_liveness: (wake) => run_stranded.supervisor_liveness(wake),
|
|
73
86
|
emit_stranded: real_emit_stranded,
|
|
@@ -77,11 +90,7 @@ const DEFAULT_PORTS: DetectPorts = {
|
|
|
77
90
|
// The three facts, assembled behind the carry-record gate. A non-`carried` read — nothing carried, or
|
|
78
91
|
// an unreadable record — is `undefined`: there is no live budget to be stranded, so the
|
|
79
92
|
// wake record is never read and the judge is never asked.
|
|
80
|
-
|
|
81
|
-
const targets = await ports.resolve_targets()
|
|
82
|
-
|
|
83
|
-
if (targets === undefined) return undefined
|
|
84
|
-
|
|
93
|
+
function input_at(ports: DetectPorts, targets: RecordTargets): StrandedInput | undefined {
|
|
85
94
|
const read = ports.read_carry(targets.carry)
|
|
86
95
|
|
|
87
96
|
if (read.kind !== 'carried') return undefined
|
|
@@ -93,6 +102,12 @@ async function gather(ports: DetectPorts): Promise<StrandedInput | undefined> {
|
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
async function gather(ports: DetectPorts): Promise<StrandedInput | undefined> {
|
|
106
|
+
const targets = await ports.resolve_targets()
|
|
107
|
+
|
|
108
|
+
return targets === undefined ? undefined : input_at(ports, targets)
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
// The marker and the notification, both once per strand episode. `emit_stranded` refuses a second while
|
|
97
112
|
// the strand event is still the newest, and its `false` is what holds the notification back too — so a
|
|
98
113
|
// strand polled every stop reaches the person once, not once a turn.
|
|
@@ -128,7 +143,38 @@ async function run_stranded_check(): Promise<void> {
|
|
|
128
143
|
}
|
|
129
144
|
}
|
|
130
145
|
|
|
131
|
-
|
|
146
|
+
// The same judgement for a reader that draws the run rather than reports on it: `run:board` asks it
|
|
147
|
+
// every local read, so a run nobody is driving is not drawn as one still waiting for work.
|
|
148
|
+
function is_stranded_at(directory: string, ports: DetectPorts = DEFAULT_PORTS): boolean {
|
|
149
|
+
const input = input_at(ports, targets_of(directory))
|
|
150
|
+
|
|
151
|
+
return input !== undefined && run_stranded.is_stranded(input)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The judgement a supervisor asks of the run it is about to stop watching. The asker is that
|
|
155
|
+
// supervisor on its way out, so it is taken as gone, and so is an owner that is the asker itself
|
|
156
|
+
// (`run_carry.is_held_by_another`) — that owner is about to be gone. A
|
|
157
|
+
// record another session holds is not left stranded. **A record that does not read is left stranded
|
|
158
|
+
// unless `run:carry --end` ended this very run**: the read that failed the driver fails here too, and
|
|
159
|
+
// that run still has its budget and nobody watching it.
|
|
160
|
+
function is_left_stranded(run: WatchedRun, ports: DetectPorts = DEFAULT_PORTS): boolean {
|
|
161
|
+
const read = ports.read_carry(run.carry_target)
|
|
162
|
+
|
|
163
|
+
if (read.kind !== 'carried') return !ports.has_ended(run.ended_target, run.started_at)
|
|
164
|
+
|
|
165
|
+
return run_stranded.is_stranded({
|
|
166
|
+
read,
|
|
167
|
+
is_owner_live: run_carry.is_held_by_another(read.carry, ports.is_owner_live),
|
|
168
|
+
supervisor: run_stranded.GONE,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const run_stranded_detect = {
|
|
173
|
+
detect_and_report,
|
|
174
|
+
is_left_stranded,
|
|
175
|
+
is_stranded_at,
|
|
176
|
+
run_stranded_check,
|
|
177
|
+
}
|
|
132
178
|
|
|
133
179
|
export { run_stranded_detect }
|
|
134
|
-
export type { DetectPorts, RecordTargets }
|
|
180
|
+
export type { DetectPorts, RecordTargets, WatchedRun }
|
|
@@ -274,6 +274,18 @@ async function open_context(args: ShipArguments): Promise<ShipContext> {
|
|
|
274
274
|
return { target, done, state, started }
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
// Every stop goes to the lane ledger — supervised, in an agent's own turn, or a lane child's preflight
|
|
278
|
+
// before the hand-off — so `lane:stats` can say how long a ship stayed stopped for each reason.
|
|
279
|
+
async function record_stop(
|
|
280
|
+
args: ShipArguments,
|
|
281
|
+
stage: Stage,
|
|
282
|
+
conflicts?: ReadonlyArray<string>,
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
const reason = run_ship_stage.stop_reason(stage, conflicts)
|
|
285
|
+
|
|
286
|
+
await lane_ledger.record_stop({ issue: Number(args.number), reason })
|
|
287
|
+
}
|
|
288
|
+
|
|
277
289
|
// A detached supervisor hands the stopped stage back (`run-ship-return.ts`); a
|
|
278
290
|
// ship in an agent's own turn has its report in front of that agent already. The `ship-stop` ends the
|
|
279
291
|
// attempt, so the record's started stages go with it.
|
|
@@ -283,8 +295,11 @@ async function stopped(
|
|
|
283
295
|
step: Step,
|
|
284
296
|
context: ShipContext,
|
|
285
297
|
): Promise<ReadonlyArray<ShipSection>> {
|
|
298
|
+
const conflicts = sections.at(-1)?.conflicts
|
|
299
|
+
|
|
300
|
+
await record_stop(args, step.stage, conflicts)
|
|
301
|
+
|
|
286
302
|
if (run_ship_detach.is_supervised()) {
|
|
287
|
-
const conflicts = sections.at(-1)?.conflicts
|
|
288
303
|
const resume = run_ship_next.resume_of(args)
|
|
289
304
|
|
|
290
305
|
record(context.target, run_ship_stage.end_attempt)
|
|
@@ -371,7 +386,12 @@ function should_detach(args: ShipArguments): boolean {
|
|
|
371
386
|
// supervisor's gate runs them again rather than passing over them.
|
|
372
387
|
async function pre_detach_sections(args: ShipArguments): Promise<ReadonlyArray<ShipSection>> {
|
|
373
388
|
const preflight = await run_stage(run_ship_steps.PREFLIGHT_STEP, args, await open_context(args))
|
|
374
|
-
|
|
389
|
+
|
|
390
|
+
if (preflight.code !== SUCCESS_EXIT_CODE) {
|
|
391
|
+
await record_stop(args, run_ship_steps.PREFLIGHT_STEP.stage)
|
|
392
|
+
|
|
393
|
+
return [preflight]
|
|
394
|
+
}
|
|
375
395
|
|
|
376
396
|
const result = await run_ship_pre_detach.checks()
|
|
377
397
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { agent_role_profile } from '#scripts/agent/agent-role-profile'
|
|
2
|
-
import { lane_child_invocation } from '#scripts/lane/lane-child-invocation'
|
|
2
|
+
import { lane_child_invocation, type ShipStop } from '#scripts/lane/lane-child-invocation'
|
|
3
3
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
4
4
|
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
5
5
|
import { lane_relaunch } from '#scripts/lane/lane-relaunch'
|
|
6
|
+
import { lane_session_broker } from '#scripts/lane/lane-session-broker'
|
|
6
7
|
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
7
8
|
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
8
9
|
import type { ShipResume } from './run-ship-next'
|
|
@@ -49,6 +50,31 @@ function note(text: string): void {
|
|
|
49
50
|
process.stderr.write(`${text}\n`)
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
function relaunched(lane: LaneInfo, stop: ShipStop): ReturnOutcome {
|
|
54
|
+
const invocation = lane_child_invocation.ship_stop_invocation(lane.issue, stop)
|
|
55
|
+
const phase = agent_role_profile.IMPLEMENTATION_PHASE
|
|
56
|
+
const result = lane_relaunch.relaunch(lane, invocation, phase, note)
|
|
57
|
+
|
|
58
|
+
if (result.kind === 'launched') return 'relaunched'
|
|
59
|
+
|
|
60
|
+
note(`the lane child could not be relaunched: ${result.note}`)
|
|
61
|
+
|
|
62
|
+
return 'recorded'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// **A served lane's child is relaunched by its broker, not from here**: this supervisor descends from the
|
|
66
|
+
// child's Bash tool, which Claude Code starts without the token, so a child started here could not sign
|
|
67
|
+
// in where the token is the only credential (joshuafolkken/kit#3817).
|
|
68
|
+
async function brokered(lane: LaneInfo, stop: ShipStop): Promise<ReturnOutcome> {
|
|
69
|
+
if (await lane_session_broker.request(lane.directory, lane.issue, { kind: 'relaunch', stop })) {
|
|
70
|
+
return 'relaunched'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
note('the lane child could not be relaunched: the lane broker did not start it')
|
|
74
|
+
|
|
75
|
+
return 'recorded'
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
async function return_control(
|
|
53
79
|
issue: string,
|
|
54
80
|
stage: Stage,
|
|
@@ -63,19 +89,11 @@ async function return_control(
|
|
|
63
89
|
|
|
64
90
|
if (lane === undefined) return 'recorded'
|
|
65
91
|
|
|
66
|
-
const
|
|
67
|
-
const result = lane_relaunch.relaunch(
|
|
68
|
-
lane,
|
|
69
|
-
invocation,
|
|
70
|
-
agent_role_profile.IMPLEMENTATION_PHASE,
|
|
71
|
-
note,
|
|
72
|
-
)
|
|
92
|
+
const stop = { stage, ...resume }
|
|
73
93
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return 'recorded'
|
|
94
|
+
return lane_session_broker.is_serving(lane.directory, issue)
|
|
95
|
+
? await brokered(lane, stop)
|
|
96
|
+
: relaunched(lane, stop)
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
const run_ship_return = { return_control }
|
|
@@ -4,7 +4,7 @@ import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
|
4
4
|
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
5
5
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
6
6
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
7
|
-
import {
|
|
7
|
+
import { lane_session_broker } from '#scripts/lane/lane-session-broker'
|
|
8
8
|
import { review_record } from '#scripts/review/review-record'
|
|
9
9
|
import { detached_launch } from '#scripts/run/detached-launch'
|
|
10
10
|
import { run_ship_review, type RoundOutcome, type ScoredVerdict } from './run-ship-review'
|
|
@@ -66,7 +66,7 @@ function failure(out: string): JoshResult {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function paths(): { brief: string; findings: string; log: string } {
|
|
69
|
-
return
|
|
69
|
+
return lane_session_broker.review_paths(PROJECT_ROOT)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
async function josh(argv: ReadonlyArray<string>): Promise<JoshResult> {
|
|
@@ -94,6 +94,16 @@ async function run_direct_session(
|
|
|
94
94
|
return { code: SUCCESS_EXIT_CODE, out: '' }
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// A lane child's reviewer is started by the lane's broker: an OpenAI lane's always, since a nested Codex
|
|
98
|
+
// cannot write outside its sandbox, and a Claude lane's whenever the `run:wake` supervisor serves it, since this
|
|
99
|
+
// ship was started from the child's Bash tool and so never received a token-only credential
|
|
100
|
+
// (joshuafolkken/kit#3817). A Claude lane nobody serves starts the reviewer itself, as before.
|
|
101
|
+
function is_brokered(profile: AgentProfile, issue: string): boolean {
|
|
102
|
+
if (!lane_child_marker.is_child_of(PROJECT_ROOT)) return false
|
|
103
|
+
|
|
104
|
+
return profile.provider === 'openai' || lane_session_broker.is_serving(PROJECT_ROOT, issue)
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
async function run_session(
|
|
98
108
|
prompt: string,
|
|
99
109
|
log_path: string,
|
|
@@ -103,11 +113,14 @@ async function run_session(
|
|
|
103
113
|
const built = agent_argv.resolve_in(prompt, agent_role_profile.REVIEWER, PROJECT_ROOT)
|
|
104
114
|
if (built.kind === 'rejected') return failure(`reviewer profile rejected: ${built.note}`)
|
|
105
115
|
|
|
106
|
-
if (built.profile
|
|
116
|
+
if (!is_brokered(built.profile, issue)) {
|
|
107
117
|
return await run_direct_session(built.argv, built.profile, log_path)
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
const is_success = await
|
|
120
|
+
const is_success = await lane_session_broker.request(PROJECT_ROOT, issue, {
|
|
121
|
+
kind: 'review',
|
|
122
|
+
round,
|
|
123
|
+
})
|
|
111
124
|
|
|
112
125
|
return is_success ? { code: SUCCESS_EXIT_CODE, out: '' } : failure('isolated reviewer failed')
|
|
113
126
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { AgentProfile } from '#scripts/agent/agent-role-profile'
|
|
2
|
+
import { backlogrun_launch } from '#scripts/backlog/backlogrun-launch'
|
|
3
|
+
import { poll } from '#scripts/lib/poll'
|
|
4
|
+
import { run_carry, type CarryRead, type RunCarry } from '#scripts/run/carry/run-carry'
|
|
5
|
+
import { run_wake, type RunWake } from './run-wake'
|
|
6
|
+
import type { WakeContext } from './run-wake-describe'
|
|
7
|
+
|
|
8
|
+
// How a supervisor claims the run it watches — including the one `josh backlogrun` starts beside the
|
|
9
|
+
// parent it launched, before that parent has claimed the carry record (joshuafolkken/kit#3804).
|
|
10
|
+
//
|
|
11
|
+
// **That supervisor is started by the command, not by the parent's own shell.** Claude Code removes its
|
|
12
|
+
// token from every process its Bash tool starts, so a supervisor the parent started there could not
|
|
13
|
+
// authenticate the sessions it wakes wherever the token is the only credential. Started by the command,
|
|
14
|
+
// it inherits the token — but before the parent has run `run:carry --begin`, so it waits for that record
|
|
15
|
+
// while the launch that will write it is alive, and gives up once that launch is gone.
|
|
16
|
+
//
|
|
17
|
+
// **While a launch is pending, only a live owner's record is its parent's**: a dead owner's record is one
|
|
18
|
+
// an earlier run left, and supervising it would wake that run beside the parent just launched.
|
|
19
|
+
|
|
20
|
+
interface AwaitPorts {
|
|
21
|
+
read_carry: () => CarryRead
|
|
22
|
+
// The invocation of the launched parent that has not claimed the carry record yet, `undefined` once
|
|
23
|
+
// no such launch is alive.
|
|
24
|
+
pending_invocation: () => string | undefined
|
|
25
|
+
is_owner_live: (carry: RunCarry) => boolean
|
|
26
|
+
sleep: (milliseconds: number) => Promise<void>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ClaimResult =
|
|
30
|
+
| { kind: 'claimed'; wake: RunWake; carry: RunCarry }
|
|
31
|
+
| { kind: 'running' }
|
|
32
|
+
| { kind: 'refused'; carry_kind: CarryRead['kind'] }
|
|
33
|
+
|
|
34
|
+
function is_live_carry(read: CarryRead, ports: AwaitPorts): boolean {
|
|
35
|
+
return read.kind === 'carried' && ports.is_owner_live(read.carry)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// What the supervisor claims its record for: the live carried run, else the launch about to carry one,
|
|
39
|
+
// else the carried run a supervisor started with no launch pending wakes.
|
|
40
|
+
function supervised_invocation(ports: AwaitPorts): string | undefined {
|
|
41
|
+
const read = ports.read_carry()
|
|
42
|
+
|
|
43
|
+
if (read.kind !== 'carried') return ports.pending_invocation()
|
|
44
|
+
if (ports.is_owner_live(read.carry)) return read.carry.invocation
|
|
45
|
+
|
|
46
|
+
return ports.pending_invocation() ?? read.carry.invocation
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// **The carry record is read once more after the launch is gone**, because a parent that wrote the
|
|
50
|
+
// record and ended between the two reads still left a run to supervise.
|
|
51
|
+
async function await_carry(ports: AwaitPorts, interval_ms: number): Promise<CarryRead> {
|
|
52
|
+
const read = ports.read_carry()
|
|
53
|
+
|
|
54
|
+
if (is_live_carry(read, ports)) return read
|
|
55
|
+
if (ports.pending_invocation() === undefined) return ports.read_carry()
|
|
56
|
+
|
|
57
|
+
await ports.sleep(interval_ms)
|
|
58
|
+
|
|
59
|
+
return await await_carry(ports, interval_ms)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ports_of(context: WakeContext): AwaitPorts {
|
|
63
|
+
return {
|
|
64
|
+
read_carry: () => run_carry.read_carry(context.carry_target),
|
|
65
|
+
pending_invocation: () => backlogrun_launch.pending_launch(context.git_directory)?.invocation,
|
|
66
|
+
is_owner_live: (carry) => run_carry.is_owner_live(carry),
|
|
67
|
+
sleep: poll.sleep,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The run the claim is held for: the one claimed for, or **a live run carried under another
|
|
72
|
+
// invocation** — a person's own `run:carry --begin` landing before the launched parent's. That run's
|
|
73
|
+
// `run:wake --start` found this supervisor live and answered `running`, so letting the claim go here
|
|
74
|
+
// would leave it with nobody watching it.
|
|
75
|
+
function supervised_carry(
|
|
76
|
+
read: CarryRead,
|
|
77
|
+
invocation: string,
|
|
78
|
+
ports: AwaitPorts,
|
|
79
|
+
): RunCarry | undefined {
|
|
80
|
+
if (read.kind !== 'carried') return undefined
|
|
81
|
+
if (read.carry.invocation === invocation || ports.is_owner_live(read.carry)) return read.carry
|
|
82
|
+
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// **The record is re-pointed at the run it now supervises**, because the loop wakes sessions for the
|
|
87
|
+
// record's invocation. **It carries the state a claim for that run would**, not the state the claim
|
|
88
|
+
// inherited for the pending launch's invocation — that run's wakes are not this one's. A refused write
|
|
89
|
+
// means the record is no longer this process's, which the loop's first pass reads as the stop it is.
|
|
90
|
+
function held_for(
|
|
91
|
+
target: string,
|
|
92
|
+
wake: RunWake,
|
|
93
|
+
carry: RunCarry,
|
|
94
|
+
profile: AgentProfile,
|
|
95
|
+
): ClaimResult {
|
|
96
|
+
if (carry.invocation === wake.invocation) return { kind: 'claimed', wake, carry }
|
|
97
|
+
|
|
98
|
+
const supervised = run_wake.fresh_wake(carry.invocation, new Date(), profile)
|
|
99
|
+
|
|
100
|
+
run_wake.update_wake(target, supervised)
|
|
101
|
+
|
|
102
|
+
return { kind: 'claimed', wake: supervised, carry }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// **The record is claimed before the carry record is awaited**, so the parent's own `run:wake --start`
|
|
106
|
+
// finds this supervisor live and answers `running` rather than starting a second one beside it.
|
|
107
|
+
// A parent that ends without carrying a run leaves nothing to supervise, and the claim is let go.
|
|
108
|
+
async function claim_run(
|
|
109
|
+
context: WakeContext,
|
|
110
|
+
profile: AgentProfile,
|
|
111
|
+
interval_ms: number,
|
|
112
|
+
ports: AwaitPorts = ports_of(context),
|
|
113
|
+
): Promise<ClaimResult> {
|
|
114
|
+
const invocation = supervised_invocation(ports)
|
|
115
|
+
if (invocation === undefined) return { kind: 'refused', carry_kind: ports.read_carry().kind }
|
|
116
|
+
const wake = run_wake.claim(context.wake_target, invocation, new Date(), profile)
|
|
117
|
+
if (wake === undefined) return { kind: 'running' }
|
|
118
|
+
const read = await await_carry(ports, interval_ms)
|
|
119
|
+
const carry = supervised_carry(read, invocation, ports)
|
|
120
|
+
if (carry !== undefined) return held_for(context.wake_target, wake, carry, profile)
|
|
121
|
+
|
|
122
|
+
run_wake.tidy_own_wake(context.wake_target)
|
|
123
|
+
|
|
124
|
+
return { kind: 'refused', carry_kind: read.kind }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const run_wake_await = { await_carry, claim_run, supervised_invocation }
|
|
128
|
+
|
|
129
|
+
export type { AwaitPorts, ClaimResult }
|
|
130
|
+
export { run_wake_await }
|