@joshuafolkken/kit 1.1387.0 → 1.1435.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +288 -229
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +62 -9
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +15 -0
- package/scripts/init/start-steps.ts +46 -0
- package/scripts/init/start.ts +1 -0
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +24 -0
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -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 }
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { backlog_drive_restore } from '#scripts/backlog/backlog-drive-restore'
|
|
2
|
+
import { git_gh_issue_read } from '#scripts/gh/git-gh-issue-read'
|
|
3
|
+
import { git_command } from '#scripts/git/git-command'
|
|
4
|
+
import { issue_state } from '#scripts/issue/issue-state'
|
|
5
|
+
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
6
|
+
import { run_hold } from '#scripts/run/hold/run-hold'
|
|
7
|
+
import {
|
|
8
|
+
PROCESS_ALIVE,
|
|
9
|
+
PROCESS_NONE,
|
|
10
|
+
PROCESS_UNKNOWN,
|
|
11
|
+
type ProcessTrace,
|
|
12
|
+
} from '#scripts/run/run-liveness'
|
|
13
|
+
import { lane_await } from './lane-await'
|
|
14
|
+
import { lane_registry, type LaneInfo } from './lane-registry'
|
|
15
|
+
|
|
16
|
+
// Whether a lane is in use, and whether it is spent — the one reading `lane:prune` discards a lane
|
|
17
|
+
// on and `josh backlogrun` keeps a watching run on.
|
|
18
|
+
//
|
|
19
|
+
// **In use means a child is working in it, or nobody could tell.** A lane a past run left behind has
|
|
20
|
+
// no process, and counting it as open kept a watching run from being replaced for as long as the lane
|
|
21
|
+
// sat there. An unanswered probe is in use: both callers act on "nobody is here", and the lane a
|
|
22
|
+
// wrong answer costs is one with a child mid-change.
|
|
23
|
+
//
|
|
24
|
+
// **Spent is all five: no child, the issue closed, not in flight, a clean tree, no commit off every
|
|
25
|
+
// remote.** `lane:close` forces the removal, so each condition names something a close cannot give
|
|
26
|
+
// back — and a condition that could not be read keeps the lane, with the reason said.
|
|
27
|
+
//
|
|
28
|
+
// **A lane the running `backlogrun` still has in flight is its `run:merge`'s.** A child that merged
|
|
29
|
+
// while no driver watched leaves a lane that reads spent on every other count; closed here, the run
|
|
30
|
+
// would have no `merge` event, no ledger line and no merged count for it (`run-tidy-lanes.ts`).
|
|
31
|
+
|
|
32
|
+
const STATE_FIELD = 'state'
|
|
33
|
+
const CLOSED_STATE = 'CLOSED'
|
|
34
|
+
|
|
35
|
+
interface SpentProbes {
|
|
36
|
+
process_trace: (issue: string) => ProcessTrace
|
|
37
|
+
issue_state: (issue: string) => Promise<string | undefined>
|
|
38
|
+
// The issues the running `backlogrun` launched and has not settled.
|
|
39
|
+
in_flight: () => Promise<ReadonlySet<string>>
|
|
40
|
+
is_tree_dirty: (directory: string) => Promise<boolean>
|
|
41
|
+
unpushed_commits: (branch: string) => Promise<number | undefined>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface KeptLane {
|
|
45
|
+
issue: string
|
|
46
|
+
reason: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The lanes that are in use, apart: a probe that did not answer is not a child seen working.
|
|
50
|
+
interface LaneUse {
|
|
51
|
+
working: Array<string>
|
|
52
|
+
unread: Array<string>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// One probe answer per lane: a second sample can disagree with the first, and a lane bucketed on
|
|
56
|
+
// two answers can land in neither list.
|
|
57
|
+
type TracedLane = readonly [issue: string, trace: ProcessTrace]
|
|
58
|
+
|
|
59
|
+
interface SpentSort {
|
|
60
|
+
spent: Array<LaneInfo>
|
|
61
|
+
kept: Array<KeptLane>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type Check = (lane: LaneInfo, probes: SpentProbes) => Promise<string | undefined>
|
|
65
|
+
|
|
66
|
+
async function read_issue_state(issue: string): Promise<string | undefined> {
|
|
67
|
+
const json = await git_gh_issue_read.issue_view_json(issue, STATE_FIELD)
|
|
68
|
+
|
|
69
|
+
return json === undefined ? undefined : issue_state.parse_issue_state(json)?.state
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function read_unpushed_commits(branch: string): Promise<number | undefined> {
|
|
73
|
+
try {
|
|
74
|
+
return await git_command.commit_count_unpushed(branch)
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function read_in_flight(): Promise<ReadonlySet<string>> {
|
|
81
|
+
return backlog_drive_restore.active_issues(await run_event_stream_emit.current_events())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const LIVE_PROBES: SpentProbes = {
|
|
85
|
+
process_trace: lane_await.process_trace_default,
|
|
86
|
+
issue_state: read_issue_state,
|
|
87
|
+
in_flight: read_in_flight,
|
|
88
|
+
is_tree_dirty: run_hold.is_tree_dirty,
|
|
89
|
+
unpushed_commits: read_unpushed_commits,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A stranded lane has no work tree for a child to be in, so it is never in use.
|
|
93
|
+
function trace_of(lane: LaneInfo, probes: SpentProbes): ProcessTrace {
|
|
94
|
+
return lane.is_stranded ? PROCESS_NONE : probes.process_trace(lane.issue)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function child_reason(lane: LaneInfo, probes: SpentProbes): Promise<string | undefined> {
|
|
98
|
+
const trace = probes.process_trace(lane.issue)
|
|
99
|
+
|
|
100
|
+
if (trace === PROCESS_NONE) return undefined
|
|
101
|
+
|
|
102
|
+
return trace === PROCESS_ALIVE
|
|
103
|
+
? 'a child is still working in it'
|
|
104
|
+
: 'whether a child is working in it could not be read'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function issue_reason(lane: LaneInfo, probes: SpentProbes): Promise<string | undefined> {
|
|
108
|
+
const state = await probes.issue_state(lane.issue)
|
|
109
|
+
|
|
110
|
+
if (state === CLOSED_STATE) return undefined
|
|
111
|
+
|
|
112
|
+
return state === undefined ? 'its issue could not be read' : 'its issue is still open'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function run_reason(lane: LaneInfo, probes: SpentProbes): Promise<string | undefined> {
|
|
116
|
+
const in_flight = await probes.in_flight()
|
|
117
|
+
|
|
118
|
+
return in_flight.has(lane.issue)
|
|
119
|
+
? 'the running backlogrun has not collected it yet (`pnpm josh run:merge` does)'
|
|
120
|
+
: undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// `is_tree_dirty` answers dirty for a status it could not read, which is the side that keeps the lane.
|
|
124
|
+
async function tree_reason(lane: LaneInfo, probes: SpentProbes): Promise<string | undefined> {
|
|
125
|
+
return (await probes.is_tree_dirty(lane.directory)) ? 'it has uncommitted changes' : undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function commit_reason(lane: LaneInfo, probes: SpentProbes): Promise<string | undefined> {
|
|
129
|
+
const count = await probes.unpushed_commits(lane.branch)
|
|
130
|
+
|
|
131
|
+
if (count === 0) return undefined
|
|
132
|
+
|
|
133
|
+
return count === undefined
|
|
134
|
+
? 'its commits could not be counted'
|
|
135
|
+
: `it has ${String(count)} commit(s) that are on no remote`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// In the order a person acts on them: a running child outranks everything, and an open issue is why
|
|
139
|
+
// the lane exists at all.
|
|
140
|
+
const CHECKS: ReadonlyArray<Check> = [
|
|
141
|
+
child_reason,
|
|
142
|
+
issue_reason,
|
|
143
|
+
run_reason,
|
|
144
|
+
tree_reason,
|
|
145
|
+
commit_reason,
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
// Why the lane is kept, or `undefined` for a spent one.
|
|
149
|
+
async function kept_reason(
|
|
150
|
+
lane: LaneInfo,
|
|
151
|
+
probes: SpentProbes = LIVE_PROBES,
|
|
152
|
+
): Promise<string | undefined> {
|
|
153
|
+
const reasons = await Promise.all(CHECKS.map(async (check) => await check(lane, probes)))
|
|
154
|
+
|
|
155
|
+
return reasons.find((reason) => reason !== undefined)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Every live lane as spent or kept. A stranded lane is neither: `lane:prune` closes it already.
|
|
159
|
+
async function sort_lanes(
|
|
160
|
+
lanes: ReadonlyArray<LaneInfo>,
|
|
161
|
+
probes: SpentProbes = LIVE_PROBES,
|
|
162
|
+
): Promise<SpentSort> {
|
|
163
|
+
const live = lanes.filter((lane) => !lane.is_stranded)
|
|
164
|
+
const reasons = await Promise.all(live.map(async (lane) => await kept_reason(lane, probes)))
|
|
165
|
+
const sorted: SpentSort = { spent: [], kept: [] }
|
|
166
|
+
|
|
167
|
+
for (const [index, lane] of live.entries()) {
|
|
168
|
+
const reason = reasons[index]
|
|
169
|
+
|
|
170
|
+
if (reason === undefined) sorted.spent.push(lane)
|
|
171
|
+
else sorted.kept.push({ issue: lane.issue, reason })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return sorted
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function issues_traced(traced: ReadonlyArray<TracedLane>, trace: ProcessTrace): Array<string> {
|
|
178
|
+
return traced.filter(([, answer]) => answer === trace).map(([issue]) => issue)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The lanes in use — what a run being replaced must not be stopped over. */
|
|
182
|
+
async function lanes_in_use(probes: SpentProbes = LIVE_PROBES): Promise<LaneUse> {
|
|
183
|
+
const lanes = await lane_registry.list_lanes()
|
|
184
|
+
const traced = lanes.map((lane): TracedLane => [lane.issue, trace_of(lane, probes)])
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
working: issues_traced(traced, PROCESS_ALIVE),
|
|
188
|
+
unread: issues_traced(traced, PROCESS_UNKNOWN),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const lane_spent = { kept_reason, lanes_in_use, sort_lanes }
|
|
193
|
+
|
|
194
|
+
export type { KeptLane, LaneUse, SpentProbes, SpentSort }
|
|
195
|
+
export { lane_spent }
|
|
@@ -112,11 +112,15 @@ function stage_row(stage: string, measured: Measured): string {
|
|
|
112
112
|
])
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function measured_of(entries: ReadonlyArray<LedgerEntry>): Measured {
|
|
116
|
+
const stages = entries.filter(is_stage)
|
|
117
|
+
|
|
118
|
+
return { implement: implement_durations(entries.filter(is_dispatch), stages), stages }
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
// The period's table: `entries` is the whole ledger, filtered here to `window`.
|
|
116
122
|
function table(entries: ReadonlyArray<LedgerEntry>, window: StatsWindow): string {
|
|
117
|
-
const
|
|
118
|
-
const stages = period.filter(is_stage)
|
|
119
|
-
const measured = { implement: implement_durations(period.filter(is_dispatch), stages), stages }
|
|
123
|
+
const measured = measured_of(lane_stats.in_window(entries, window))
|
|
120
124
|
|
|
121
125
|
return [
|
|
122
126
|
lane_stats.to_row(COLUMNS),
|
|
@@ -125,10 +129,30 @@ function table(entries: ReadonlyArray<LedgerEntry>, window: StatsWindow): string
|
|
|
125
129
|
].join('\n')
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
// Each stage's median over every entry given, a stage nothing recorded left out rather than read as
|
|
133
|
+
// `0` — how long a stage usually takes, which `run:board` measures a running child's phase against.
|
|
134
|
+
function medians(entries: ReadonlyArray<LedgerEntry>): ReadonlyMap<string, number> {
|
|
135
|
+
const measured = measured_of(entries)
|
|
136
|
+
|
|
137
|
+
return new Map(
|
|
138
|
+
STAGES.flatMap((stage) => {
|
|
139
|
+
const median = lane_stats.median(durations_of(stage, measured))
|
|
140
|
+
|
|
141
|
+
return median === undefined ? [] : [[stage, median] as const]
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
128
146
|
const lane_stage_stats = {
|
|
129
147
|
IMPLEMENT,
|
|
130
148
|
NOT_MEASURED,
|
|
131
149
|
STAGES,
|
|
150
|
+
cell,
|
|
151
|
+
is_dispatch,
|
|
152
|
+
is_number,
|
|
153
|
+
is_stage,
|
|
154
|
+
medians,
|
|
155
|
+
started_ms,
|
|
132
156
|
table,
|
|
133
157
|
}
|
|
134
158
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { run_ship_stage } from '#scripts/run/ship/run-ship-stage'
|
|
2
|
+
import { lane_ledger, type LedgerEntry, type StopEntry } from './lane-ledger'
|
|
3
|
+
import { lane_stage_stats } from './lane-stage-stats'
|
|
4
|
+
import { lane_stats, type StatsWindow } from './lane-stats'
|
|
5
|
+
|
|
6
|
+
// How long a lane stood stopped: the ledger's `stop` entries of one period reduced to one row per stop
|
|
7
|
+
// reason — median, maximum, how many stops were resumed and how many never were — printed by
|
|
8
|
+
// `josh lane:stats` under the per-stage table, so the stop reason worth removing next is a number.
|
|
9
|
+
//
|
|
10
|
+
// **The wait is derived, not recorded**: from a stop to the start of the same issue's next ship stage,
|
|
11
|
+
// the way `implement` is. A stop that no ship stage followed — the lane parked, or left stopped — is
|
|
12
|
+
// counted under `not resumed` and kept out of the median and the maximum, whose time has no end.
|
|
13
|
+
//
|
|
14
|
+
// **A re-dispatch of the issue ends the search**: the new child's ship is a fresh lane's work, not the
|
|
15
|
+
// resumption of a stop that may be days old.
|
|
16
|
+
|
|
17
|
+
const COLUMNS = ['stop', 'median (min)', 'max (min)', 'runs', 'not resumed'] as const
|
|
18
|
+
const { STAGE } = run_ship_stage
|
|
19
|
+
const { is_dispatch, is_number, is_stage } = lane_stage_stats
|
|
20
|
+
const REASONS = [
|
|
21
|
+
STAGE.PREFLIGHT,
|
|
22
|
+
STAGE.REVIEW,
|
|
23
|
+
STAGE.SYNC,
|
|
24
|
+
run_ship_stage.CONFLICT,
|
|
25
|
+
STAGE.GATE,
|
|
26
|
+
STAGE.COMMIT,
|
|
27
|
+
STAGE.ROUND_TWO,
|
|
28
|
+
STAGE.FOLLOWUP,
|
|
29
|
+
STAGE.REPORT,
|
|
30
|
+
] as const
|
|
31
|
+
|
|
32
|
+
interface StopWait {
|
|
33
|
+
reason: string
|
|
34
|
+
wait_ms: number | undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function is_stop(entry: LedgerEntry): entry is StopEntry {
|
|
38
|
+
return entry.kind === lane_ledger.KIND.STOP
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function redispatch_ms(stop: StopEntry, entries: ReadonlyArray<LedgerEntry>): number {
|
|
42
|
+
const from_ms = Date.parse(stop.at)
|
|
43
|
+
const later = entries
|
|
44
|
+
.filter((entry) => is_dispatch(entry))
|
|
45
|
+
.filter((entry) => entry.issue === stop.issue)
|
|
46
|
+
.map((entry) => Date.parse(entry.at))
|
|
47
|
+
.filter((at) => at > from_ms)
|
|
48
|
+
|
|
49
|
+
return later.length === 0 ? Infinity : Math.min(...later)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function wait_ms(stop: StopEntry, entries: ReadonlyArray<LedgerEntry>): number | undefined {
|
|
53
|
+
const from_ms = Date.parse(stop.at)
|
|
54
|
+
const until_ms = redispatch_ms(stop, entries)
|
|
55
|
+
const starts = entries
|
|
56
|
+
.filter((entry) => is_stage(entry))
|
|
57
|
+
.filter((stage) => stage.issue === stop.issue)
|
|
58
|
+
.map((stage) => lane_stage_stats.started_ms(stage))
|
|
59
|
+
.filter((start) => start >= from_ms && start < until_ms)
|
|
60
|
+
|
|
61
|
+
return starts.length === 0 ? undefined : Math.min(...starts) - from_ms
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function waits_of(entries: ReadonlyArray<LedgerEntry>): Array<StopWait> {
|
|
65
|
+
return entries
|
|
66
|
+
.filter(is_stop)
|
|
67
|
+
.map((stop) => ({ reason: stop.reason, wait_ms: wait_ms(stop, entries) }))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function reason_row(reason: string, waits: ReadonlyArray<StopWait>): string {
|
|
71
|
+
const of_reason = waits.filter((wait) => wait.reason === reason)
|
|
72
|
+
const resumed = of_reason.map((wait) => wait.wait_ms).filter((value) => is_number(value))
|
|
73
|
+
|
|
74
|
+
return lane_stats.to_row([
|
|
75
|
+
reason,
|
|
76
|
+
lane_stage_stats.cell(lane_stats.median(resumed)),
|
|
77
|
+
lane_stage_stats.cell(lane_stats.peak(resumed)),
|
|
78
|
+
String(resumed.length),
|
|
79
|
+
String(of_reason.length - resumed.length),
|
|
80
|
+
])
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The period's table: `entries` is the whole ledger, filtered here to `window`.
|
|
84
|
+
function table(entries: ReadonlyArray<LedgerEntry>, window: StatsWindow): string {
|
|
85
|
+
const waits = waits_of(lane_stats.in_window(entries, window))
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
lane_stats.to_row(COLUMNS),
|
|
89
|
+
lane_stats.to_row(COLUMNS.map(() => '---')),
|
|
90
|
+
...REASONS.map((reason) => reason_row(reason, waits)),
|
|
91
|
+
].join('\n')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const lane_stop_stats = { REASONS, table }
|
|
95
|
+
|
|
96
|
+
export { lane_stop_stats }
|
|
@@ -9,10 +9,10 @@ import { lane_child_invocation } from './lane-child-invocation'
|
|
|
9
9
|
import { lane_child_marker } from './lane-child-marker'
|
|
10
10
|
import { lane_dispatch_log } from './lane-dispatch-log'
|
|
11
11
|
import type { LaneInfo } from './lane-registry'
|
|
12
|
+
import { lane_session_broker } from './lane-session-broker'
|
|
12
13
|
import { openai_lane_ship } from './openai-lane-ship'
|
|
13
14
|
import { openai_lane_supervisor_decision } from './openai-lane-supervisor-decision'
|
|
14
15
|
import { openai_lane_supervisor_owner, type SupervisorOwner } from './openai-lane-supervisor-owner'
|
|
15
|
-
import { openai_review_broker } from './openai-review-broker'
|
|
16
16
|
|
|
17
17
|
const { claim, read_state, release, update } = openai_lane_supervisor_owner
|
|
18
18
|
const CHILD_POLL_MS = 250
|
|
@@ -178,7 +178,7 @@ async function supervise(lane: LaneInfo, profile: AgentProfile, nonce: string):
|
|
|
178
178
|
try {
|
|
179
179
|
if (!(await openai_lane_supervisor_decision.is_approved(lane.directory, nonce))) return 0
|
|
180
180
|
|
|
181
|
-
return await
|
|
181
|
+
return await lane_session_broker.with_server(
|
|
182
182
|
lane,
|
|
183
183
|
async () => await run_claimed(lane, profile, owner),
|
|
184
184
|
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
// One definition of "the process tools a lane reads are not installed".
|
|
4
|
+
//
|
|
5
|
+
// **`ps` and `pgrep` are a prerequisite, not an optional probe.** The owner check walks the caller's
|
|
6
|
+
// ancestry with `ps`, and lane liveness and reaping match children with `pgrep`. A machine without
|
|
7
|
+
// them (a slim Debian image ships no `procps`) used to read every probe as "nothing there": the
|
|
8
|
+
// ancestry stopped at the caller, `run:step` answered `unknown`, and the parent stopped before its
|
|
9
|
+
// first lane with no word of why. A missing tool is refused by name instead.
|
|
10
|
+
|
|
11
|
+
// What Node reports when the executable to spawn does not exist.
|
|
12
|
+
const NOT_FOUND_CODE = 'ENOENT'
|
|
13
|
+
const PACKAGE_HINT =
|
|
14
|
+
'install the package that provides it (procps on Debian/Ubuntu: `apt-get install procps`)'
|
|
15
|
+
|
|
16
|
+
function is_not_found(error: unknown): boolean {
|
|
17
|
+
return error instanceof Error && 'code' in error && error.code === NOT_FOUND_CODE
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function missing_notice(command: string): string {
|
|
21
|
+
return `\`${path.basename(command)}\` is not installed — kit's lane commands read processes with ps and pgrep; ${PACKAGE_HINT}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Throw a named refusal when `error` says `command` could not be spawned because it does not exist. */
|
|
25
|
+
function throw_if_missing(command: string, error: unknown): void {
|
|
26
|
+
if (is_not_found(error)) throw new Error(missing_notice(command), { cause: error })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const process_tools = { missing_notice, throw_if_missing }
|
|
30
|
+
|
|
31
|
+
export { process_tools }
|