@joshuafolkken/kit 1.1387.0 → 1.1453.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
- package/.claude/skills/workflow-commands/backlogrun.md +3 -2
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +1 -1
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/kickoff.md +12 -13
- package/.claude/skills/workflow-commands/observation-filing.md +1 -1
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/.claude/skills/workflow-commands/prerequisite.md +6 -0
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/README.md +27 -4
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +261 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +95 -19
- package/package.json +4 -3
- package/prompts/collaboration-workflow/issue-template.md +10 -8
- package/prompts/review.md +30 -4
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
- package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
- package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
- package/scripts/auto-ok/auto-ok-judge.ts +67 -0
- package/scripts/backlog/backlog-drive-cli.ts +14 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +38 -11
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +175 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +18 -4
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +13 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +7 -4
- package/scripts/init/init-ai-copy.ts +34 -11
- package/scripts/init/init-basic.ts +9 -4
- package/scripts/init/init-bootstrap.ts +6 -2
- package/scripts/init/init-dry-run-report.ts +150 -0
- package/scripts/init/init-dry-run.ts +189 -0
- package/scripts/init/init-install.ts +8 -1
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +62 -42
- package/scripts/init/kit-setup-state.ts +11 -17
- package/scripts/init/own-formatter.ts +21 -0
- package/scripts/init/project-profile.ts +60 -12
- package/scripts/init/start-dry-run.ts +70 -0
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +23 -3
- package/scripts/init/start-setup-pr.ts +13 -24
- package/scripts/init/start-steps.ts +62 -1
- package/scripts/init/start.ts +38 -17
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/init/working-tree.ts +116 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-auto-ok.ts +68 -99
- package/scripts/issue/issue-file-cli.ts +17 -17
- package/scripts/issue/issue-file.ts +1 -6
- package/scripts/issue/issue-labels.ts +46 -10
- package/scripts/issue/issue-release-cli.ts +69 -23
- package/scripts/issue/issue-release.ts +3 -1
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-ai.ts +11 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +10 -3
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +26 -2
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/josh/process-identity.ts +32 -6
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-broker-server.ts +69 -0
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/lane-session-broker.ts +363 -0
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/observations/observation-ledger-migrate.ts +3 -15
- package/scripts/release/release-issue-close.ts +133 -0
- package/scripts/release/release-publish.ts +12 -1
- package/scripts/repo/repository-labels.ts +11 -1
- package/scripts/review/comment-ranges.ts +199 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +24 -7
- package/scripts/review/review-code-tree.ts +119 -0
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/review/review-round2-cli.ts +16 -5
- package/scripts/review/review-round2.ts +30 -10
- package/scripts/review/review-stamps.ts +22 -1
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/lane/openai-review-broker.ts +0 -262
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
5
|
+
import { agent_argv } from '#scripts/agent/agent-argv'
|
|
6
|
+
import { agent_role_profile, type AgentProvider } from '#scripts/agent/agent-role-profile'
|
|
7
|
+
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
8
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
9
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
10
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
11
|
+
import { detached_launch } from '#scripts/run/detached-launch'
|
|
12
|
+
import { run_ship_review } from '#scripts/run/ship/run-ship-review'
|
|
13
|
+
import { run_ship_stage } from '#scripts/run/ship/run-ship-stage'
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { lane_broker_server, type BrokerServer } from './lane-broker-server'
|
|
16
|
+
import { lane_child_invocation, type ShipStop } from './lane-child-invocation'
|
|
17
|
+
import { lane_child_marker } from './lane-child-marker'
|
|
18
|
+
import type { LaneInfo } from './lane-registry'
|
|
19
|
+
import { lane_relaunch } from './lane-relaunch'
|
|
20
|
+
|
|
21
|
+
// The sessions a lane child's detached `josh ship` cannot start itself, started for it by a process
|
|
22
|
+
// that can: the round-1 and round-2 reviewer, and the fresh child a stopped ship hands back to. An
|
|
23
|
+
// OpenAI lane's own supervisor serves it, because a nested Codex cannot write outside its sandbox; a
|
|
24
|
+
// Claude lane is served by the `run:wake` supervisor (`claude-lane-broker.ts`), because Claude Code removes its
|
|
25
|
+
// token from what its Bash tool starts, so a token-only credential never reaches the ship the child
|
|
26
|
+
// launched (joshuafolkken/kit#3817). **The request names an action, never a command**: the server
|
|
27
|
+
// rebuilds the argv from the registered lane, so a child can ask for nothing but these two sessions.
|
|
28
|
+
|
|
29
|
+
const REQUEST_PREFIX = '.josh-review-request-'
|
|
30
|
+
const RESPONSE_PREFIX = '.josh-review-response-'
|
|
31
|
+
const BRIEF_PREFIX = 'josh-ship-review-brief-'
|
|
32
|
+
const FINDINGS_PREFIX = 'josh-ship-review-findings-'
|
|
33
|
+
const LOG_PREFIX = 'josh-ship-review-log-'
|
|
34
|
+
const CACHE_DIRECTORY = ['node_modules', '.cache']
|
|
35
|
+
const POLL_MS = 100
|
|
36
|
+
// The heartbeat is kept for a ship branched before the server record, and read only where a
|
|
37
|
+
// supervisor started before it left no such record (`lane-broker-server.ts`).
|
|
38
|
+
const LEGACY_HEARTBEAT_MS = 500
|
|
39
|
+
const LEGACY_HEARTBEAT_LIMIT_MS = 3000
|
|
40
|
+
const MINUTE_MS = 60_000
|
|
41
|
+
const REQUEST_LIMIT_MINUTES = 30
|
|
42
|
+
const REQUEST_LIMIT_MS = REQUEST_LIMIT_MINUTES * MINUTE_MS
|
|
43
|
+
const ANTHROPIC_PROVIDER: AgentProvider = 'anthropic'
|
|
44
|
+
const stop_schema = z
|
|
45
|
+
.object({
|
|
46
|
+
stage: z.enum(run_ship_stage.STAGE),
|
|
47
|
+
title: z.string(),
|
|
48
|
+
flags: z.array(z.string()).readonly(),
|
|
49
|
+
is_review: z.boolean(),
|
|
50
|
+
conflicts: z.array(z.string()).readonly().default([]),
|
|
51
|
+
})
|
|
52
|
+
.strict()
|
|
53
|
+
const action_schema = z.discriminatedUnion('kind', [
|
|
54
|
+
z.object({ kind: z.literal('review'), round: z.enum(['1', '2']) }).strict(),
|
|
55
|
+
z.object({ kind: z.literal('relaunch'), stop: stop_schema }).strict(),
|
|
56
|
+
])
|
|
57
|
+
const request_schema = z
|
|
58
|
+
.object({ session: z.uuid(), nonce: z.uuid(), action: action_schema })
|
|
59
|
+
.strict()
|
|
60
|
+
const response_schema = z
|
|
61
|
+
.object({
|
|
62
|
+
session: z.uuid(),
|
|
63
|
+
nonce: z.string(),
|
|
64
|
+
is_success: z.boolean(),
|
|
65
|
+
heartbeat_at: z.number().int().positive(),
|
|
66
|
+
})
|
|
67
|
+
.strict()
|
|
68
|
+
type BrokerRequest = z.infer<typeof request_schema>
|
|
69
|
+
type BrokerResponse = z.infer<typeof response_schema>
|
|
70
|
+
type ReviewRound = Extract<z.infer<typeof action_schema>, { kind: 'review' }>['round']
|
|
71
|
+
type BrokerAction = { kind: 'review'; round: ReviewRound } | { kind: 'relaunch'; stop: ShipStop }
|
|
72
|
+
|
|
73
|
+
function review_paths(directory: string): { brief: string; findings: string; log: string } {
|
|
74
|
+
return {
|
|
75
|
+
brief: stamp_file.stamp_path(BRIEF_PREFIX, directory, '.md'),
|
|
76
|
+
findings: stamp_file.stamp_path(FINDINGS_PREFIX, directory, '.txt'),
|
|
77
|
+
log: stamp_file.stamp_path(LOG_PREFIX, directory, '.log'),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function request_path(directory: string, issue: string): string {
|
|
82
|
+
return path.join(directory, ...CACHE_DIRECTORY, `${REQUEST_PREFIX}${issue}.json`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function response_path(directory: string, issue: string): string | undefined {
|
|
86
|
+
const common = git_common_directory.resolve(directory)
|
|
87
|
+
if (common === undefined || path.basename(common) !== '.git') return undefined
|
|
88
|
+
|
|
89
|
+
return path.join(path.dirname(common), ...CACHE_DIRECTORY, `${RESPONSE_PREFIX}${issue}.json`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
|
|
93
|
+
try {
|
|
94
|
+
return json_value.parse_with(readFileSync(target, 'utf8'), schema)
|
|
95
|
+
} catch {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The reviewer runs on the lane's own provider, so a Claude lane's server never starts a Codex reviewer
|
|
101
|
+
// and an OpenAI lane's never starts a Claude one.
|
|
102
|
+
function provider_of(lane: LaneInfo): AgentProvider {
|
|
103
|
+
return lane.profile?.provider ?? ANTHROPIC_PROVIDER
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// **The reviewer carries the lane's mark explicitly**: the supervisor serving a Claude lane is no lane child,
|
|
107
|
+
// so a reviewer inheriting its environment would descend from the carry record's owner unmarked and be
|
|
108
|
+
// read as the `backlogrun` parent by every hook.
|
|
109
|
+
|
|
110
|
+
async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<boolean> {
|
|
111
|
+
const target = review_paths(lane.directory)
|
|
112
|
+
const prompt_of =
|
|
113
|
+
round === '1' ? run_ship_review.reviewer_prompt : run_ship_review.verification_prompt
|
|
114
|
+
const built = agent_argv.resolve_in(
|
|
115
|
+
prompt_of(target.brief, target.findings),
|
|
116
|
+
agent_role_profile.REVIEWER,
|
|
117
|
+
lane.directory,
|
|
118
|
+
)
|
|
119
|
+
if (built.kind === 'rejected' || built.profile.provider !== provider_of(lane)) return false
|
|
120
|
+
|
|
121
|
+
const result = await detached_launch.launch_attached(
|
|
122
|
+
{
|
|
123
|
+
argv: built.argv,
|
|
124
|
+
cwd: lane.directory,
|
|
125
|
+
log_path: target.log,
|
|
126
|
+
profile: built.profile,
|
|
127
|
+
env: lane_child_marker.env_for(lane.issue),
|
|
128
|
+
},
|
|
129
|
+
console.error,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return result.kind === 'completed' && result.exit_code === 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// An OpenAI lane's successor is its supervisor's to start, never a relaunch's.
|
|
136
|
+
function relaunch(lane: LaneInfo, stop: ShipStop): boolean {
|
|
137
|
+
if (lane_relaunch.is_openai_lane(lane)) return false
|
|
138
|
+
|
|
139
|
+
const invocation = lane_child_invocation.ship_stop_invocation(lane.issue, stop)
|
|
140
|
+
const phase = agent_role_profile.IMPLEMENTATION_PHASE
|
|
141
|
+
const result = lane_relaunch.relaunch(lane, invocation, phase, console.error)
|
|
142
|
+
|
|
143
|
+
return result.kind === 'launched'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function perform(lane: LaneInfo, action: BrokerAction): Promise<boolean> {
|
|
147
|
+
if (action.kind === 'relaunch') return relaunch(lane, action.stop)
|
|
148
|
+
|
|
149
|
+
return await launch_review(lane, action.round)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function write_response(target: string, body: BrokerResponse): void {
|
|
153
|
+
const temporary = `${target}.${String(process.pid)}.tmp`
|
|
154
|
+
|
|
155
|
+
writeFileSync(temporary, JSON.stringify(body))
|
|
156
|
+
renameSync(temporary, target)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function launched(lane: LaneInfo, action: BrokerAction): Promise<boolean> {
|
|
160
|
+
try {
|
|
161
|
+
return await perform(lane, action)
|
|
162
|
+
} catch (error) {
|
|
163
|
+
error_text.trace_swallowed('lane_session_broker.launched', error)
|
|
164
|
+
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function process_request(
|
|
170
|
+
lane: LaneInfo,
|
|
171
|
+
response: string,
|
|
172
|
+
received: BrokerRequest,
|
|
173
|
+
state: { current: BrokerResponse },
|
|
174
|
+
): Promise<void> {
|
|
175
|
+
const is_success = await launched(lane, received.action)
|
|
176
|
+
|
|
177
|
+
state.current = {
|
|
178
|
+
session: received.session,
|
|
179
|
+
nonce: received.nonce,
|
|
180
|
+
is_success,
|
|
181
|
+
heartbeat_at: Date.now(),
|
|
182
|
+
}
|
|
183
|
+
write_response(response, state.current)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function serve(
|
|
187
|
+
lane: LaneInfo,
|
|
188
|
+
session: string,
|
|
189
|
+
response: string,
|
|
190
|
+
state: { is_running: boolean; current: BrokerResponse },
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
let last_nonce = ''
|
|
193
|
+
const target = request_path(lane.directory, lane.issue)
|
|
194
|
+
|
|
195
|
+
while (state.is_running) {
|
|
196
|
+
const received = read_record(target, request_schema)
|
|
197
|
+
|
|
198
|
+
if (received?.session === session && received.nonce !== last_nonce) {
|
|
199
|
+
last_nonce = received.nonce
|
|
200
|
+
// eslint-disable-next-line no-await-in-loop -- requests are served one at a time, in arrival order
|
|
201
|
+
await process_request(lane, response, received, state)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
|
|
205
|
+
await sleep(POLL_MS)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The server record is written before the response, so a ship that finds the response finds it too.
|
|
210
|
+
function prepare(lane: LaneInfo): { response: string; current: BrokerResponse } {
|
|
211
|
+
const response = response_path(lane.directory, lane.issue)
|
|
212
|
+
if (response === undefined) throw new Error('review broker has no trusted response location')
|
|
213
|
+
|
|
214
|
+
const session = randomUUID()
|
|
215
|
+
|
|
216
|
+
mkdirSync(path.dirname(response), { recursive: true })
|
|
217
|
+
mkdirSync(path.dirname(request_path(lane.directory, lane.issue)), { recursive: true })
|
|
218
|
+
lane_broker_server.write(response, session)
|
|
219
|
+
const current: BrokerResponse = {
|
|
220
|
+
session,
|
|
221
|
+
nonce: '',
|
|
222
|
+
is_success: false,
|
|
223
|
+
heartbeat_at: Date.now(),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
write_response(response, current)
|
|
227
|
+
|
|
228
|
+
return { response, current }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function start_heartbeat(state: { current: BrokerResponse }, response: string): NodeJS.Timeout {
|
|
232
|
+
const heartbeat = setInterval(() => {
|
|
233
|
+
state.current = { ...state.current, heartbeat_at: Date.now() }
|
|
234
|
+
write_response(response, state.current)
|
|
235
|
+
}, LEGACY_HEARTBEAT_MS)
|
|
236
|
+
|
|
237
|
+
heartbeat.unref()
|
|
238
|
+
|
|
239
|
+
return heartbeat
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// **The heartbeat outlives the request being served.** A ship waiting on a reviewer reads a stale
|
|
243
|
+
// heartbeat as a dead server, so stopping it first would fail a review the server is still running.
|
|
244
|
+
async function stop_server(
|
|
245
|
+
state: { is_running: boolean },
|
|
246
|
+
heartbeat: NodeJS.Timeout,
|
|
247
|
+
serving: Promise<void>,
|
|
248
|
+
response: string,
|
|
249
|
+
): Promise<void> {
|
|
250
|
+
state.is_running = false
|
|
251
|
+
await serving
|
|
252
|
+
clearInterval(heartbeat)
|
|
253
|
+
unlinkSync(response)
|
|
254
|
+
lane_broker_server.remove(response)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function with_server<T>(lane: LaneInfo, run: () => Promise<T>): Promise<T> {
|
|
258
|
+
const { response, current } = prepare(lane)
|
|
259
|
+
const state = { is_running: true, current }
|
|
260
|
+
const heartbeat = start_heartbeat(state, response)
|
|
261
|
+
const serving = serve(lane, current.session, response, state)
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
return await run()
|
|
265
|
+
} finally {
|
|
266
|
+
await stop_server(state, heartbeat, serving, response)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function is_fresh_heartbeat(heartbeat_at: number): boolean {
|
|
271
|
+
const now = Date.now()
|
|
272
|
+
|
|
273
|
+
return heartbeat_at <= now && heartbeat_at >= now - LEGACY_HEARTBEAT_LIMIT_MS
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Live by the server record (`lane-broker-server.ts`); a response with none was written by a
|
|
277
|
+
// supervisor started before it, read by the heartbeat that supervisor still writes.
|
|
278
|
+
function is_live_response(
|
|
279
|
+
target: string,
|
|
280
|
+
response: BrokerResponse | undefined,
|
|
281
|
+
is_live: (server: BrokerServer) => boolean,
|
|
282
|
+
): response is BrokerResponse {
|
|
283
|
+
if (response === undefined) return false
|
|
284
|
+
|
|
285
|
+
const server = lane_broker_server.read(target, response.session)
|
|
286
|
+
|
|
287
|
+
return server === undefined ? is_fresh_heartbeat(response.heartbeat_at) : is_live(server)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function session_response(target: string, session: string): BrokerResponse | undefined {
|
|
291
|
+
const response = read_record(target, response_schema)
|
|
292
|
+
|
|
293
|
+
return response?.session === session ? response : undefined
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function response_verdict(
|
|
297
|
+
target: string,
|
|
298
|
+
session: string,
|
|
299
|
+
nonce: string,
|
|
300
|
+
): 'wait' | 'success' | 'failure' {
|
|
301
|
+
const response = session_response(target, session)
|
|
302
|
+
|
|
303
|
+
if (!is_live_response(target, response, lane_broker_server.is_running)) return 'failure'
|
|
304
|
+
if (response.nonce !== nonce) return 'wait'
|
|
305
|
+
|
|
306
|
+
return response.is_success ? 'success' : 'failure'
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function wait_response(target: string, session: string, nonce: string): Promise<boolean> {
|
|
310
|
+
const deadline = Date.now() + REQUEST_LIMIT_MS
|
|
311
|
+
|
|
312
|
+
while (Date.now() < deadline) {
|
|
313
|
+
const verdict = response_verdict(target, session, nonce)
|
|
314
|
+
if (verdict !== 'wait') return verdict === 'success'
|
|
315
|
+
|
|
316
|
+
// eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
|
|
317
|
+
await sleep(POLL_MS)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return false
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function live_response(target: string): BrokerResponse | undefined {
|
|
324
|
+
const response = read_record(target, response_schema)
|
|
325
|
+
|
|
326
|
+
return is_live_response(target, response, lane_broker_server.is_serving_process)
|
|
327
|
+
? response
|
|
328
|
+
: undefined
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Whether a server answers for this lane now — what a Claude lane's ship asks before it hands a session
|
|
332
|
+
// over rather than starting the session itself.
|
|
333
|
+
function is_serving(directory: string, issue: string): boolean {
|
|
334
|
+
const target = response_path(directory, issue)
|
|
335
|
+
|
|
336
|
+
return target !== undefined && live_response(target) !== undefined
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function request(directory: string, issue: string, action: BrokerAction): Promise<boolean> {
|
|
340
|
+
const target = response_path(directory, issue)
|
|
341
|
+
const response = target === undefined ? undefined : live_response(target)
|
|
342
|
+
|
|
343
|
+
if (target === undefined || response === undefined) return false
|
|
344
|
+
|
|
345
|
+
const nonce = randomUUID()
|
|
346
|
+
const body = { session: response.session, nonce, action }
|
|
347
|
+
|
|
348
|
+
stamp_file.replace_stamp(request_path(directory, issue), body)
|
|
349
|
+
|
|
350
|
+
return await wait_response(target, response.session, nonce)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const lane_session_broker = {
|
|
354
|
+
is_serving,
|
|
355
|
+
request,
|
|
356
|
+
request_path,
|
|
357
|
+
response_path,
|
|
358
|
+
review_paths,
|
|
359
|
+
with_server,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export type { BrokerAction, ReviewRound }
|
|
363
|
+
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
|
|