@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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
4
|
+
import { lane_dispatch } from './lane-dispatch'
|
|
5
|
+
import { lane_restart, type RestartOutcome } from './lane-restart'
|
|
6
|
+
|
|
7
|
+
// `josh lane:restart <issue>` — the contract `lane:dispatch` sets: the fresh child's pid is the one
|
|
8
|
+
// token on standard output, every explanation on standard error, and a restart that did not start a
|
|
9
|
+
// fresh child exits non-zero. A failed dispatch is warned off-screen exactly as `lane:dispatch` warns it,
|
|
10
|
+
// since the stopped child is then left for `run:merge` to collect.
|
|
11
|
+
|
|
12
|
+
const ARGV_OFFSET = 2
|
|
13
|
+
const SUCCESS_EXIT_CODE = 0
|
|
14
|
+
const FAILURE_EXIT_CODE = 1
|
|
15
|
+
const USAGE = 'Usage: josh lane:restart <issue-number>'
|
|
16
|
+
|
|
17
|
+
async function report(outcome: RestartOutcome, issue: string): Promise<number> {
|
|
18
|
+
console.error(lane_restart.describe(outcome, issue))
|
|
19
|
+
|
|
20
|
+
if (outcome.kind !== 'restarted') return FAILURE_EXIT_CODE
|
|
21
|
+
const { dispatch } = outcome
|
|
22
|
+
|
|
23
|
+
if (lane_dispatch.is_worth_warning(dispatch)) await lane_dispatch.warn_of_problem(dispatch, issue)
|
|
24
|
+
if (dispatch.kind !== 'dispatched') return FAILURE_EXIT_CODE
|
|
25
|
+
|
|
26
|
+
console.info(String(dispatch.pid))
|
|
27
|
+
|
|
28
|
+
return SUCCESS_EXIT_CODE
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
32
|
+
const issue = argv.length === 1 ? argv[0] : undefined
|
|
33
|
+
|
|
34
|
+
if (issue === undefined || !issue_number_shape.is_issue_number(issue)) {
|
|
35
|
+
console.error(USAGE)
|
|
36
|
+
|
|
37
|
+
return FAILURE_EXIT_CODE
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return await report(await lane_restart.restart(issue), issue)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
44
|
+
process.exitCode = await run(argv)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lane_restart_cli = { run }
|
|
48
|
+
|
|
49
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
|
|
50
|
+
|
|
51
|
+
export { lane_restart_cli }
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { git_stash } from '#scripts/git/stash/git-stash'
|
|
2
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
3
|
+
import { IN_PROGRESS_LABEL } from '#scripts/issue/issue-labels'
|
|
4
|
+
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
5
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
6
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
7
|
+
import { run_cut } from '#scripts/run/cut/run-cut'
|
|
8
|
+
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
9
|
+
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
10
|
+
import { lane_dispatch, type DispatchOutcome } from './lane-dispatch'
|
|
11
|
+
import { lane_preserve } from './lane-preserve'
|
|
12
|
+
import { lane_reap } from './lane-reap'
|
|
13
|
+
import { lane_registry, type LaneInfo } from './lane-registry'
|
|
14
|
+
|
|
15
|
+
// `josh lane:restart <N>` — a person stops a running lane child and starts its issue over in a fresh
|
|
16
|
+
// session, typically after correcting the Issue's specification mid-run.
|
|
17
|
+
//
|
|
18
|
+
// **The restart is a hand-off inside the parent's re-confirm window, not a recovery.** Killing the child
|
|
19
|
+
// alone would let `lane:await` book it stopped and `run:merge` classify it — an `outage` resumes the old
|
|
20
|
+
// session with the old specification in mind, anything else is parked `needs-decision` and counted as a
|
|
21
|
+
// failure. `run:cut` already hands a lane to a new process inside `lane:await`'s re-confirm window, so a
|
|
22
|
+
// child that disappears and reappears under the same `fullrun #<N>` pattern is never collected: the
|
|
23
|
+
// restart stops, stashes and re-dispatches within that window, and no failure or outage is counted.
|
|
24
|
+
//
|
|
25
|
+
// **No GitHub round-trip sits between the stop and the launch**, since one slow answer would outlast the
|
|
26
|
+
// window. The `in-progress` marker is claimed while the old child still runs — a refused claim stops
|
|
27
|
+
// nothing — and the Issue comment naming the stash is posted after the fresh child is up.
|
|
28
|
+
//
|
|
29
|
+
// The fresh dispatch reads no exit record (an outage record would otherwise resume the stopped session),
|
|
30
|
+
// a carried cut is ended (its successor would adopt the old hand-off), and the work in the tree is
|
|
31
|
+
// stashed with the recovery line on the Issue. The `restart` event follows the dispatch's own
|
|
32
|
+
// `child-launch`, so the stream and `run:board` say a person restarted the child.
|
|
33
|
+
|
|
34
|
+
const RESTART_OCCASION = 'lane restart'
|
|
35
|
+
const RESTART_CAUSE = 'a person restarted its child with `lane:restart`'
|
|
36
|
+
const DISPATCHED_KIND = 'dispatched'
|
|
37
|
+
const NO_LANE_KIND = 'no-lane'
|
|
38
|
+
const NOT_RUNNING_KIND = 'not-running'
|
|
39
|
+
const UNCLAIMED_KIND = 'unclaimed'
|
|
40
|
+
|
|
41
|
+
interface Unsaved {
|
|
42
|
+
kind: 'unsaved'
|
|
43
|
+
directory: string
|
|
44
|
+
reason: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Restarted {
|
|
48
|
+
kind: 'restarted'
|
|
49
|
+
dispatch: DispatchOutcome
|
|
50
|
+
stash: string | undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Refused =
|
|
54
|
+
| { kind: typeof NO_LANE_KIND }
|
|
55
|
+
| { kind: typeof NOT_RUNNING_KIND }
|
|
56
|
+
| { kind: typeof UNCLAIMED_KIND; reason: string }
|
|
57
|
+
type RestartOutcome = Restarted | Unsaved | Refused
|
|
58
|
+
type Saved = Unsaved | { kind: 'saved'; stash: string | undefined }
|
|
59
|
+
|
|
60
|
+
// A cut the stopped child carried would hand its successor the old session's hand-off at
|
|
61
|
+
// `run:cut --resume`; the fresh session has to start from the Issue instead.
|
|
62
|
+
function end_carried_cut(lane: LaneInfo): void {
|
|
63
|
+
const carried = run_cut.lane_cut_sync(lane.directory)
|
|
64
|
+
|
|
65
|
+
if (carried !== undefined) run_cut.end_cut(carried.target)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function stash_of(lane: LaneInfo): Promise<string | undefined> {
|
|
69
|
+
if (!(await git_stash.has_changes(lane.directory))) return undefined
|
|
70
|
+
|
|
71
|
+
return await lane_preserve.push_work(lane.issue, lane.directory, RESTART_OCCASION)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A tree that could not be read or pushed is left as it is: re-dispatching into it would hand the fresh
|
|
75
|
+
// session the stopped one's half-done work as though it were its own.
|
|
76
|
+
async function save_work(lane: LaneInfo): Promise<Saved> {
|
|
77
|
+
try {
|
|
78
|
+
return { kind: 'saved', stash: await stash_of(lane) }
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return { kind: 'unsaved', directory: lane.directory, reason: error_text.message_of(error) }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function record_restart(issue: string): Promise<void> {
|
|
85
|
+
await run_event_stream_emit.emit(
|
|
86
|
+
run_event_stream.EVENT_KIND.RESTART,
|
|
87
|
+
`${issue_cite.plain(issue)} restarted in a fresh session by a person`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function relaunch(lane: LaneInfo): Promise<RestartOutcome> {
|
|
92
|
+
end_carried_cut(lane)
|
|
93
|
+
const saved = await save_work(lane)
|
|
94
|
+
|
|
95
|
+
if (saved.kind === 'unsaved') return saved
|
|
96
|
+
const dispatch = await lane_dispatch.dispatch_child(lane.issue, 'restart')
|
|
97
|
+
|
|
98
|
+
if (dispatch.kind === DISPATCHED_KIND) await record_restart(lane.issue)
|
|
99
|
+
|
|
100
|
+
if (saved.stash !== undefined) {
|
|
101
|
+
await lane_preserve.announce(lane.issue, saved.stash, RESTART_CAUSE)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { kind: 'restarted', dispatch, stash: saved.stash }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A child that ended on its own during the claim (say, right after merging its PR) is a finished child
|
|
108
|
+
// for `run:merge` to collect — relaunching it would re-run a done issue inside the re-confirm window.
|
|
109
|
+
async function stop_and_relaunch(lane: LaneInfo): Promise<RestartOutcome> {
|
|
110
|
+
if (lane_reap.reap_child(lane.issue).length === 0) return { kind: NOT_RUNNING_KIND }
|
|
111
|
+
|
|
112
|
+
return await relaunch(lane)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Stop the running child of `#<N>`'s lane and re-dispatch the issue there in a fresh session. */
|
|
116
|
+
async function restart(issue: string): Promise<RestartOutcome> {
|
|
117
|
+
issue_number_shape.require_issue_number(issue)
|
|
118
|
+
const lane = await lane_registry.find_open_lane(issue)
|
|
119
|
+
|
|
120
|
+
if (lane === undefined) return { kind: NO_LANE_KIND }
|
|
121
|
+
if (!lane_reap.is_child_running(issue)) return { kind: NOT_RUNNING_KIND }
|
|
122
|
+
const claim = await lane_dispatch.claim_marker(issue)
|
|
123
|
+
|
|
124
|
+
if (!claim.is_applied) return { kind: UNCLAIMED_KIND, reason: claim.reason }
|
|
125
|
+
|
|
126
|
+
return await stop_and_relaunch(lane)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function stash_sentence(stash: string | undefined): string {
|
|
130
|
+
if (stash === undefined) return 'The lane held no uncommitted work.'
|
|
131
|
+
|
|
132
|
+
return `Its uncommitted work was stashed — recover with \`pnpm josh stash:pop "${stash}"\`.`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A dispatch that failed leaves the stopped child to `run:merge`: nothing re-appeared inside the
|
|
136
|
+
// re-confirm window, so the parent collects it as it would any stopped child.
|
|
137
|
+
function restarted_text(outcome: Restarted, issue: string): string {
|
|
138
|
+
const head = `Stopped the child for ${session_cite.issue(issue)}. ${stash_sentence(outcome.stash)}`
|
|
139
|
+
const dispatch = lane_dispatch.describe(outcome.dispatch, issue)
|
|
140
|
+
|
|
141
|
+
if (outcome.dispatch.kind === DISPATCHED_KIND) return `${head} ${dispatch}`
|
|
142
|
+
|
|
143
|
+
return `${head} The fresh dispatch failed, so \`run:merge\` will collect the stopped child: ${dispatch}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function unsaved_text(outcome: Unsaved, issue: string): string {
|
|
147
|
+
return `The child for ${session_cite.issue(issue)} was stopped, but its uncommitted work could not be stashed (${outcome.reason}), so nothing was re-dispatched and the lane is left at ${outcome.directory}; \`run:merge\` will collect the stopped child.`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function refused_text(outcome: Refused, issue: string): string {
|
|
151
|
+
const cited = session_cite.issue(issue)
|
|
152
|
+
|
|
153
|
+
if (outcome.kind === NO_LANE_KIND) {
|
|
154
|
+
return `No lane is open for ${cited}, so there is no child to restart.`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (outcome.kind === UNCLAIMED_KIND) {
|
|
158
|
+
return `The \`${IN_PROGRESS_LABEL}\` label could not be confirmed on ${cited} (${outcome.reason}), so the running child was left as it is — nothing was stopped or re-dispatched.`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return `No child is running in the lane for ${cited}, so nothing was stopped or re-dispatched — a child that has already ended is \`run:merge\`'s to collect.`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function describe(outcome: RestartOutcome, issue: string): string {
|
|
165
|
+
if (outcome.kind === 'unsaved') return unsaved_text(outcome, issue)
|
|
166
|
+
|
|
167
|
+
return outcome.kind === 'restarted'
|
|
168
|
+
? restarted_text(outcome, issue)
|
|
169
|
+
: refused_text(outcome, issue)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const lane_restart = { describe, restart }
|
|
173
|
+
|
|
174
|
+
export type { RestartOutcome }
|
|
175
|
+
export { lane_restart }
|
|
@@ -3,15 +3,27 @@ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from '
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
5
5
|
import { agent_argv } from '#scripts/agent/agent-argv'
|
|
6
|
-
import { agent_role_profile } from '#scripts/agent/agent-role-profile'
|
|
6
|
+
import { agent_role_profile, type AgentProvider } from '#scripts/agent/agent-role-profile'
|
|
7
7
|
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
8
8
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
9
9
|
import { error_text } from '#scripts/lib/error-message'
|
|
10
10
|
import { json_value } from '#scripts/lib/json-value'
|
|
11
11
|
import { detached_launch } from '#scripts/run/detached-launch'
|
|
12
12
|
import { run_ship_review } from '#scripts/run/ship/run-ship-review'
|
|
13
|
+
import { run_ship_stage } from '#scripts/run/ship/run-ship-stage'
|
|
13
14
|
import { z } from 'zod'
|
|
15
|
+
import { lane_child_invocation, type ShipStop } from './lane-child-invocation'
|
|
16
|
+
import { lane_child_marker } from './lane-child-marker'
|
|
14
17
|
import type { LaneInfo } from './lane-registry'
|
|
18
|
+
import { lane_relaunch } from './lane-relaunch'
|
|
19
|
+
|
|
20
|
+
// The sessions a lane child's detached `josh ship` cannot start itself, started for it by a process
|
|
21
|
+
// that can: the round-1 and round-2 reviewer, and the fresh child a stopped ship hands back to. An
|
|
22
|
+
// OpenAI lane's own supervisor serves it, because a nested Codex cannot write outside its sandbox; a
|
|
23
|
+
// Claude lane is served by the `run:wake` supervisor (`claude-lane-broker.ts`), because Claude Code removes its
|
|
24
|
+
// token from what its Bash tool starts, so a token-only credential never reaches the ship the child
|
|
25
|
+
// launched (joshuafolkken/kit#3817). **The request names an action, never a command**: the server
|
|
26
|
+
// rebuilds the argv from the registered lane, so a child can ask for nothing but these two sessions.
|
|
15
27
|
|
|
16
28
|
const REQUEST_PREFIX = '.josh-review-request-'
|
|
17
29
|
const RESPONSE_PREFIX = '.josh-review-response-'
|
|
@@ -25,8 +37,22 @@ const HEARTBEAT_LIMIT_MS = 3000
|
|
|
25
37
|
const MINUTE_MS = 60_000
|
|
26
38
|
const REQUEST_LIMIT_MINUTES = 30
|
|
27
39
|
const REQUEST_LIMIT_MS = REQUEST_LIMIT_MINUTES * MINUTE_MS
|
|
40
|
+
const ANTHROPIC_PROVIDER: AgentProvider = 'anthropic'
|
|
41
|
+
const stop_schema = z
|
|
42
|
+
.object({
|
|
43
|
+
stage: z.enum(run_ship_stage.STAGE),
|
|
44
|
+
title: z.string(),
|
|
45
|
+
flags: z.array(z.string()).readonly(),
|
|
46
|
+
is_review: z.boolean(),
|
|
47
|
+
conflicts: z.array(z.string()).readonly().default([]),
|
|
48
|
+
})
|
|
49
|
+
.strict()
|
|
50
|
+
const action_schema = z.discriminatedUnion('kind', [
|
|
51
|
+
z.object({ kind: z.literal('review'), round: z.enum(['1', '2']) }).strict(),
|
|
52
|
+
z.object({ kind: z.literal('relaunch'), stop: stop_schema }).strict(),
|
|
53
|
+
])
|
|
28
54
|
const request_schema = z
|
|
29
|
-
.object({ session: z.uuid(), nonce: z.uuid(),
|
|
55
|
+
.object({ session: z.uuid(), nonce: z.uuid(), action: action_schema })
|
|
30
56
|
.strict()
|
|
31
57
|
const response_schema = z
|
|
32
58
|
.object({
|
|
@@ -36,9 +62,10 @@ const response_schema = z
|
|
|
36
62
|
heartbeat_at: z.number().int().positive(),
|
|
37
63
|
})
|
|
38
64
|
.strict()
|
|
39
|
-
type
|
|
40
|
-
type
|
|
41
|
-
type ReviewRound =
|
|
65
|
+
type BrokerRequest = z.infer<typeof request_schema>
|
|
66
|
+
type BrokerResponse = z.infer<typeof response_schema>
|
|
67
|
+
type ReviewRound = Extract<z.infer<typeof action_schema>, { kind: 'review' }>['round']
|
|
68
|
+
type BrokerAction = { kind: 'review'; round: ReviewRound } | { kind: 'relaunch'; stop: ShipStop }
|
|
42
69
|
|
|
43
70
|
function review_paths(directory: string): { brief: string; findings: string; log: string } {
|
|
44
71
|
return {
|
|
@@ -67,6 +94,16 @@ function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
|
|
|
67
94
|
}
|
|
68
95
|
}
|
|
69
96
|
|
|
97
|
+
// The reviewer runs on the lane's own provider, so a Claude lane's server never starts a Codex reviewer
|
|
98
|
+
// and an OpenAI lane's never starts a Claude one.
|
|
99
|
+
function provider_of(lane: LaneInfo): AgentProvider {
|
|
100
|
+
return lane.profile?.provider ?? ANTHROPIC_PROVIDER
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// **The reviewer carries the lane's mark explicitly**: the supervisor serving a Claude lane is no lane child,
|
|
104
|
+
// so a reviewer inheriting its environment would descend from the carry record's owner unmarked and be
|
|
105
|
+
// read as the `backlogrun` parent by every hook.
|
|
106
|
+
|
|
70
107
|
async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<boolean> {
|
|
71
108
|
const target = review_paths(lane.directory)
|
|
72
109
|
const prompt_of =
|
|
@@ -76,7 +113,7 @@ async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<boolea
|
|
|
76
113
|
agent_role_profile.REVIEWER,
|
|
77
114
|
lane.directory,
|
|
78
115
|
)
|
|
79
|
-
if (built.kind === 'rejected' || built.profile.provider !==
|
|
116
|
+
if (built.kind === 'rejected' || built.profile.provider !== provider_of(lane)) return false
|
|
80
117
|
|
|
81
118
|
const result = await detached_launch.launch_attached(
|
|
82
119
|
{
|
|
@@ -84,6 +121,7 @@ async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<boolea
|
|
|
84
121
|
cwd: lane.directory,
|
|
85
122
|
log_path: target.log,
|
|
86
123
|
profile: built.profile,
|
|
124
|
+
env: lane_child_marker.env_for(lane.issue),
|
|
87
125
|
},
|
|
88
126
|
console.error,
|
|
89
127
|
)
|
|
@@ -91,18 +129,35 @@ async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<boolea
|
|
|
91
129
|
return result.kind === 'completed' && result.exit_code === 0
|
|
92
130
|
}
|
|
93
131
|
|
|
94
|
-
|
|
132
|
+
// An OpenAI lane's successor is its supervisor's to start, never a relaunch's.
|
|
133
|
+
function relaunch(lane: LaneInfo, stop: ShipStop): boolean {
|
|
134
|
+
if (lane_relaunch.is_openai_lane(lane)) return false
|
|
135
|
+
|
|
136
|
+
const invocation = lane_child_invocation.ship_stop_invocation(lane.issue, stop)
|
|
137
|
+
const phase = agent_role_profile.IMPLEMENTATION_PHASE
|
|
138
|
+
const result = lane_relaunch.relaunch(lane, invocation, phase, console.error)
|
|
139
|
+
|
|
140
|
+
return result.kind === 'launched'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function perform(lane: LaneInfo, action: BrokerAction): Promise<boolean> {
|
|
144
|
+
if (action.kind === 'relaunch') return relaunch(lane, action.stop)
|
|
145
|
+
|
|
146
|
+
return await launch_review(lane, action.round)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function write_response(target: string, body: BrokerResponse): void {
|
|
95
150
|
const temporary = `${target}.${String(process.pid)}.tmp`
|
|
96
151
|
|
|
97
152
|
writeFileSync(temporary, JSON.stringify(body))
|
|
98
153
|
renameSync(temporary, target)
|
|
99
154
|
}
|
|
100
155
|
|
|
101
|
-
async function launched(lane: LaneInfo,
|
|
156
|
+
async function launched(lane: LaneInfo, action: BrokerAction): Promise<boolean> {
|
|
102
157
|
try {
|
|
103
|
-
return await
|
|
158
|
+
return await perform(lane, action)
|
|
104
159
|
} catch (error) {
|
|
105
|
-
error_text.trace_swallowed('
|
|
160
|
+
error_text.trace_swallowed('lane_session_broker.launched', error)
|
|
106
161
|
|
|
107
162
|
return false
|
|
108
163
|
}
|
|
@@ -111,10 +166,10 @@ async function launched(lane: LaneInfo, round: ReviewRound): Promise<boolean> {
|
|
|
111
166
|
async function process_request(
|
|
112
167
|
lane: LaneInfo,
|
|
113
168
|
response: string,
|
|
114
|
-
received:
|
|
115
|
-
state: { current:
|
|
169
|
+
received: BrokerRequest,
|
|
170
|
+
state: { current: BrokerResponse },
|
|
116
171
|
): Promise<void> {
|
|
117
|
-
const is_success = await launched(lane, received.
|
|
172
|
+
const is_success = await launched(lane, received.action)
|
|
118
173
|
|
|
119
174
|
state.current = {
|
|
120
175
|
session: received.session,
|
|
@@ -129,7 +184,7 @@ async function serve(
|
|
|
129
184
|
lane: LaneInfo,
|
|
130
185
|
session: string,
|
|
131
186
|
response: string,
|
|
132
|
-
state: { is_running: boolean; current:
|
|
187
|
+
state: { is_running: boolean; current: BrokerResponse },
|
|
133
188
|
): Promise<void> {
|
|
134
189
|
let last_nonce = ''
|
|
135
190
|
const target = request_path(lane.directory, lane.issue)
|
|
@@ -148,7 +203,7 @@ async function serve(
|
|
|
148
203
|
}
|
|
149
204
|
}
|
|
150
205
|
|
|
151
|
-
function prepare(lane: LaneInfo): { response: string; current:
|
|
206
|
+
function prepare(lane: LaneInfo): { response: string; current: BrokerResponse } {
|
|
152
207
|
const response = response_path(lane.directory, lane.issue)
|
|
153
208
|
if (response === undefined) throw new Error('review broker has no trusted response location')
|
|
154
209
|
|
|
@@ -156,7 +211,7 @@ function prepare(lane: LaneInfo): { response: string; current: ReviewResponse }
|
|
|
156
211
|
|
|
157
212
|
mkdirSync(path.dirname(response), { recursive: true })
|
|
158
213
|
mkdirSync(path.dirname(request_path(lane.directory, lane.issue)), { recursive: true })
|
|
159
|
-
const current:
|
|
214
|
+
const current: BrokerResponse = {
|
|
160
215
|
session,
|
|
161
216
|
nonce: '',
|
|
162
217
|
is_success: false,
|
|
@@ -168,7 +223,7 @@ function prepare(lane: LaneInfo): { response: string; current: ReviewResponse }
|
|
|
168
223
|
return { response, current }
|
|
169
224
|
}
|
|
170
225
|
|
|
171
|
-
function start_heartbeat(state: { current:
|
|
226
|
+
function start_heartbeat(state: { current: BrokerResponse }, response: string): NodeJS.Timeout {
|
|
172
227
|
const heartbeat = setInterval(() => {
|
|
173
228
|
state.current = { ...state.current, heartbeat_at: Date.now() }
|
|
174
229
|
write_response(response, state.current)
|
|
@@ -179,15 +234,17 @@ function start_heartbeat(state: { current: ReviewResponse }, response: string):
|
|
|
179
234
|
return heartbeat
|
|
180
235
|
}
|
|
181
236
|
|
|
237
|
+
// **The heartbeat outlives the request being served.** A ship waiting on a reviewer reads a stale
|
|
238
|
+
// heartbeat as a dead server, so stopping it first would fail a review the server is still running.
|
|
182
239
|
async function stop_server(
|
|
183
240
|
state: { is_running: boolean },
|
|
184
241
|
heartbeat: NodeJS.Timeout,
|
|
185
242
|
serving: Promise<void>,
|
|
186
243
|
response: string,
|
|
187
244
|
): Promise<void> {
|
|
188
|
-
clearInterval(heartbeat)
|
|
189
245
|
state.is_running = false
|
|
190
246
|
await serving
|
|
247
|
+
clearInterval(heartbeat)
|
|
191
248
|
unlinkSync(response)
|
|
192
249
|
}
|
|
193
250
|
|
|
@@ -205,9 +262,9 @@ async function with_server<T>(lane: LaneInfo, run: () => Promise<T>): Promise<T>
|
|
|
205
262
|
}
|
|
206
263
|
|
|
207
264
|
function is_live_response(
|
|
208
|
-
response:
|
|
265
|
+
response: BrokerResponse | undefined,
|
|
209
266
|
session: string,
|
|
210
|
-
): response is
|
|
267
|
+
): response is BrokerResponse {
|
|
211
268
|
const now = Date.now()
|
|
212
269
|
|
|
213
270
|
return (
|
|
@@ -218,7 +275,7 @@ function is_live_response(
|
|
|
218
275
|
}
|
|
219
276
|
|
|
220
277
|
function response_verdict(
|
|
221
|
-
response:
|
|
278
|
+
response: BrokerResponse | undefined,
|
|
222
279
|
session: string,
|
|
223
280
|
nonce: string,
|
|
224
281
|
): 'wait' | 'success' | 'failure' {
|
|
@@ -242,21 +299,42 @@ async function wait_response(target: string, session: string, nonce: string): Pr
|
|
|
242
299
|
return false
|
|
243
300
|
}
|
|
244
301
|
|
|
245
|
-
|
|
246
|
-
const target = response_path(directory, issue)
|
|
247
|
-
if (target === undefined) return false
|
|
302
|
+
function live_response(target: string): BrokerResponse | undefined {
|
|
248
303
|
const response = read_record(target, response_schema)
|
|
249
304
|
|
|
250
|
-
|
|
305
|
+
return is_live_response(response, response?.session ?? '') ? response : undefined
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Whether a server answers for this lane now — what a Claude lane's ship asks before it hands a session
|
|
309
|
+
// over rather than starting the session itself.
|
|
310
|
+
function is_serving(directory: string, issue: string): boolean {
|
|
311
|
+
const target = response_path(directory, issue)
|
|
312
|
+
|
|
313
|
+
return target !== undefined && live_response(target) !== undefined
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function request(directory: string, issue: string, action: BrokerAction): Promise<boolean> {
|
|
317
|
+
const target = response_path(directory, issue)
|
|
318
|
+
const response = target === undefined ? undefined : live_response(target)
|
|
319
|
+
|
|
320
|
+
if (target === undefined || response === undefined) return false
|
|
251
321
|
|
|
252
322
|
const nonce = randomUUID()
|
|
253
|
-
const body
|
|
323
|
+
const body = { session: response.session, nonce, action }
|
|
254
324
|
|
|
255
325
|
stamp_file.replace_stamp(request_path(directory, issue), body)
|
|
256
326
|
|
|
257
327
|
return await wait_response(target, response.session, nonce)
|
|
258
328
|
}
|
|
259
329
|
|
|
260
|
-
const
|
|
330
|
+
const lane_session_broker = {
|
|
331
|
+
is_serving,
|
|
332
|
+
request,
|
|
333
|
+
request_path,
|
|
334
|
+
response_path,
|
|
335
|
+
review_paths,
|
|
336
|
+
with_server,
|
|
337
|
+
}
|
|
261
338
|
|
|
262
|
-
export {
|
|
339
|
+
export type { BrokerAction, ReviewRound }
|
|
340
|
+
export { lane_session_broker }
|