@joshuafolkken/kit 1.1455.0 → 1.1514.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/settings.json +6 -6
- package/.claude/skills/dependency-update/SKILL.md +26 -55
- package/.claude/skills/epic-commands/SKILL.md +83 -156
- package/.claude/skills/epic-commands/epic-bundle.md +51 -79
- package/.claude/skills/epic-commands/execution-waves.md +43 -81
- package/.claude/skills/verify-ui/SKILL.md +24 -50
- package/.claude/skills/workflow-commands/SKILL.md +7 -9
- package/.claude/skills/workflow-commands/background-commands.md +31 -46
- package/.claude/skills/workflow-commands/backlogrun-child.md +65 -85
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +86 -125
- package/.claude/skills/workflow-commands/backlogrun-park.md +68 -118
- package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -28
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +17 -26
- package/.claude/skills/workflow-commands/backlogrun-steps.md +35 -52
- package/.claude/skills/workflow-commands/backlogrun.md +31 -50
- package/.claude/skills/workflow-commands/chain-rule.md +31 -40
- package/.claude/skills/workflow-commands/delegation.md +12 -8
- package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
- package/.claude/skills/workflow-commands/followup.md +55 -77
- package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
- package/.claude/skills/workflow-commands/fullrun.md +4 -6
- package/.claude/skills/workflow-commands/halfrun.md +3 -7
- package/.claude/skills/workflow-commands/into-target.md +1 -2
- package/.claude/skills/workflow-commands/issue-comments.md +43 -60
- package/.claude/skills/workflow-commands/issue-scout.md +3 -2
- package/.claude/skills/workflow-commands/kickoff.md +65 -74
- package/.claude/skills/workflow-commands/latest-gate.md +2 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
- package/.claude/skills/workflow-commands/observation-filing.md +59 -118
- package/.claude/skills/workflow-commands/observation-ledger.md +52 -112
- package/.claude/skills/workflow-commands/pre-gate-cut.md +88 -142
- package/.claude/skills/workflow-commands/prerequisite.md +11 -17
- package/.claude/skills/workflow-commands/progress-watcher.md +23 -33
- package/.claude/skills/workflow-commands/prrun.md +3 -5
- package/.claude/skills/workflow-commands/retrospective.md +35 -54
- package/.claude/skills/workflow-commands/split-assessment.md +32 -58
- package/.claude/skills/workflow-commands/target-repository.md +1 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +54 -73
- package/.codex/hooks.json +6 -6
- package/.github/actions/setup-node/action.yml +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CLAUDE.md +7 -6
- package/cspell/index.yaml +7 -0
- package/dist/CLAUDE.md +7 -6
- package/dist/commands/document-section-cli.js +67 -34
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/chunk-4KKD2UTK.js +66 -0
- package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-6TS2JD6D.js} +14 -3
- package/dist/hooks/chunk-7DSBJVFH.js +50 -0
- package/dist/hooks/{chunk-5H4SSDSD.js → chunk-A65Y4YOR.js} +3 -3
- package/dist/hooks/{chunk-3C4XRYG6.js → chunk-APUO6U4J.js} +102 -19
- package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
- package/dist/hooks/chunk-GP4IEC4O.js +20 -0
- package/dist/hooks/chunk-JP45SOCJ.js +56 -0
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-LAOV7NLM.js} +1 -1
- package/dist/hooks/chunk-M4HGZV7U.js +83 -0
- package/dist/hooks/{chunk-TBJFTSUY.js → chunk-O6GDRNJB.js} +9 -7
- package/dist/hooks/{chunk-HFGWYGUV.js → chunk-PF2NZL2A.js} +5 -5
- package/dist/hooks/{chunk-WNLVGT7E.js → chunk-PTICX4OL.js} +4 -5
- package/dist/hooks/{chunk-HHI6FO3R.js → chunk-RT27QRJW.js} +763 -1515
- package/dist/hooks/{chunk-7WQBPLUN.js → chunk-SYOTDYCI.js} +1149 -1095
- package/dist/hooks/{chunk-LIAFCXL7.js → chunk-TNUMY6NL.js} +1519 -1332
- package/dist/hooks/chunk-UPUZF3AN.js +279 -0
- package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-WK6SMBIC.js} +1932 -784
- package/dist/hooks/codex-hook-adapter.js +17 -14
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +5 -4
- package/dist/hooks/inputs.json +302 -247
- package/dist/hooks/investigation-guard.js +8 -11
- package/dist/hooks/pretool-guard.js +16 -13
- package/dist/hooks/session-lang.js +372 -26
- package/dist/hooks/stop-guard.js +2593 -189
- package/dist/josh.js +2128 -282
- package/dist/managed-marker/index.js +1 -2
- package/dist/version/index.js +15 -12
- package/eslint/base.js +2 -1
- package/eslint/rules/unicorn.js +16 -0
- package/lefthook/base.yml +5 -3
- package/package.json +14 -12
- package/prompts/coding-standards.md +12 -22
- package/prompts/collaboration-workflow/file-edits.md +25 -45
- package/prompts/collaboration-workflow/gh-rest.md +19 -36
- package/prompts/collaboration-workflow/glossary.md +5 -7
- package/prompts/collaboration-workflow/issue-citation.md +6 -14
- package/prompts/collaboration-workflow/issue-template.md +40 -32
- package/prompts/collaboration-workflow/lane-instructions.md +35 -0
- package/prompts/collaboration-workflow/operating-rules.md +33 -46
- package/prompts/collaboration-workflow/output-bounds.md +15 -15
- package/prompts/collaboration-workflow/overview.md +14 -19
- package/prompts/collaboration-workflow/plan-comment.md +13 -13
- package/prompts/collaboration-workflow/principles.md +57 -54
- package/prompts/collaboration-workflow/proposal-request.md +1 -9
- package/prompts/collaboration-workflow/report-format.md +41 -49
- package/prompts/collaboration-workflow/residency.md +15 -11
- package/prompts/collaboration-workflow/rule-delivery.md +7 -15
- package/prompts/collaboration-workflow/shell-body.md +10 -20
- package/prompts/collaboration-workflow/turn-batching.md +8 -14
- package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
- package/prompts/collaboration-workflow/wip-cap.md +26 -32
- package/prompts/collaboration-workflow.md +25 -25
- package/prompts/refactoring.md +33 -43
- package/prompts/review-rubric.md +84 -129
- package/prompts/review.md +110 -141
- package/prompts/sonar-hotspot-handling.md +22 -50
- package/prompts/testing-guide.md +48 -99
- package/scripts/adopt/adopt-toolkits.ts +2 -4
- package/scripts/agent/agent-argv.ts +20 -4
- package/scripts/agent/agent-exit-record.ts +23 -1
- package/scripts/agent/agent-role-profile.ts +101 -45
- package/scripts/agent/agent-safe-text.ts +60 -0
- package/scripts/agent/claude-agent-argv.ts +14 -0
- package/scripts/agent/codex-agent-argv.ts +9 -2
- package/scripts/agent/lane-instructions.ts +144 -0
- package/scripts/backlog/backlog-budget-cli.ts +1 -2
- package/scripts/backlog/backlog-budget.ts +4 -4
- package/scripts/backlog/backlog-drive-carried.ts +26 -0
- package/scripts/backlog/backlog-drive-cli.ts +10 -5
- package/scripts/backlog/backlog-drive-named.ts +3 -5
- package/scripts/backlog/backlog-drive-restore.ts +28 -2
- package/scripts/backlog/backlog-drive-seats.ts +27 -0
- package/scripts/backlog/backlog-drive.ts +11 -6
- package/scripts/backlog/backlog-occupied.ts +56 -0
- package/scripts/backlog/backlog-ready.ts +59 -15
- package/scripts/behavior/behavior-rules.ts +3 -2
- package/scripts/cost-runtime/codex-usage.ts +4 -2
- package/scripts/cost-runtime/cost-cli.ts +1 -2
- package/scripts/delegation/duplicate-read-guard.ts +2 -2
- package/scripts/delegation/investigation-guard.ts +15 -65
- package/scripts/delegation/investigation-reads.ts +1 -2
- package/scripts/discovery/repo-discovery.ts +4 -2
- package/scripts/discovery/repo-origin.ts +2 -4
- package/scripts/doctor/doctor-logic.ts +6 -3
- package/scripts/document/document-byte-budget.ts +28 -27
- package/scripts/document/document-byte-check.ts +1 -2
- package/scripts/document/document-section-cli.ts +17 -5
- package/scripts/document/entry-read-budget.ts +3 -3
- package/scripts/document/read-set-cli.ts +4 -1
- package/scripts/epic/epic-audit-orphans.ts +5 -8
- package/scripts/epic/epic-audit-rationale.ts +8 -2
- package/scripts/epic/epic-audit.ts +52 -1
- package/scripts/epic/epic-bundle-cli.ts +1 -2
- package/scripts/epic/epic-bundle-evidence.ts +49 -26
- package/scripts/epic/epic-bundle-referenced.ts +14 -3
- package/scripts/epic/epic-bundle.ts +17 -9
- package/scripts/epic/epic-chains.ts +6 -2
- package/scripts/epic/epic-close-comment.ts +1 -2
- package/scripts/followup/git-followup-workflow.ts +19 -1
- package/scripts/gate/check-inputs.ts +129 -0
- package/scripts/gate/check-reuse.ts +162 -0
- package/scripts/gate/core-budget.ts +93 -44
- package/scripts/gate/core-wait.ts +130 -0
- package/scripts/gate/free-cores.ts +49 -0
- package/scripts/gate/gate-ledger.ts +17 -3
- package/scripts/gate/gate-plan.ts +23 -1
- package/scripts/gate/gate-report.ts +15 -2
- package/scripts/gate/gate-run-steps.ts +150 -0
- package/scripts/gate/held-mark.ts +55 -0
- package/scripts/gate/machine-capacity.ts +70 -11
- package/scripts/gate/pre-commit-type-check.ts +14 -1
- package/scripts/gate/verification-gate.ts +65 -127
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +24 -2
- package/scripts/gh/git-pr.ts +19 -3
- package/scripts/git/git-common-directory.ts +3 -3
- package/scripts/git/git-diff-reads.ts +16 -9
- package/scripts/git/git-execa-error.ts +1 -2
- package/scripts/git/main-merge-regenerate.ts +78 -0
- package/scripts/git/main-merge.ts +14 -5
- package/scripts/hooks/batch-guard.ts +5 -8
- package/scripts/hooks/format-edited-file.ts +10 -4
- package/scripts/hooks/pre-commit-lint.ts +91 -0
- package/scripts/hooks/pre-push-unit.ts +15 -4
- package/scripts/hooks/pretool-guard.ts +48 -79
- package/scripts/hooks/reserved-run.ts +1 -0
- package/scripts/hooks/run-hook.sh +10 -12
- package/scripts/hooks/step-zero-notice.ts +3 -2
- package/scripts/hooks/stop-actions.ts +204 -0
- package/scripts/hooks/stop-guard.ts +62 -21
- package/scripts/init/hook-command-rewrite.ts +6 -2
- package/scripts/init/hook-launch.ts +12 -7
- package/scripts/init/init-logic-deploy-vps.ts +4 -2
- package/scripts/issue/behavior-change-lint.ts +12 -6
- package/scripts/issue/expected-effect-lint.ts +59 -0
- package/scripts/issue/issue-cite-cli.ts +35 -15
- package/scripts/issue/issue-classification.ts +1 -1
- package/scripts/issue/issue-lint-cli.ts +5 -3
- package/scripts/issue/issue-scout-cli.ts +22 -16
- package/scripts/issue/markdown-section.ts +19 -5
- package/scripts/josh/command-usage.ts +110 -0
- package/scripts/josh/file-map-stamp.ts +1 -3
- package/scripts/josh/hook-decision.ts +17 -2
- package/scripts/josh/josh-command-map.ts +2 -0
- package/scripts/josh/josh-command-types.ts +55 -9
- package/scripts/josh/josh-commands-ai.ts +13 -0
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-hooks.ts +6 -0
- package/scripts/josh/josh-commands-lane.ts +10 -1
- package/scripts/josh/josh-commands-workflow.ts +5 -0
- package/scripts/josh/josh-logic.ts +6 -15
- package/scripts/josh/josh-usage-automation.ts +121 -0
- package/scripts/josh/josh-usage-backlog.ts +180 -0
- package/scripts/josh/josh-usage-developer.ts +230 -0
- package/scripts/josh/josh-usage-hooks.ts +38 -0
- package/scripts/josh/josh-usage-issue.ts +205 -0
- package/scripts/josh/josh-usage-review.ts +101 -0
- package/scripts/josh/josh-usage-run.ts +252 -0
- package/scripts/josh/josh-usage-session.ts +238 -0
- package/scripts/josh/josh-usage-workflow.ts +212 -0
- package/scripts/josh/josh.ts +14 -5
- package/scripts/josh/package-directory.ts +20 -0
- package/scripts/lane/lane-await.ts +3 -2
- package/scripts/lane/lane-capacity.ts +8 -2
- package/scripts/lane/lane-check-stats.ts +56 -0
- package/scripts/lane/lane-child-invocation.ts +49 -5
- package/scripts/lane/lane-dispatch.ts +3 -3
- package/scripts/lane/lane-guard-policy.ts +5 -5
- package/scripts/lane/lane-handoff.ts +8 -1
- package/scripts/lane/lane-install.ts +12 -4
- package/scripts/lane/lane-ledger.ts +52 -1
- package/scripts/lane/lane-limit-cli.ts +8 -9
- package/scripts/lane/lane-load.ts +9 -2
- package/scripts/lane/lane-measure-cli.ts +4 -0
- package/scripts/lane/lane-parked.ts +18 -0
- package/scripts/lane/lane-preserve.ts +29 -1
- package/scripts/lane/lane-process-load.ts +125 -0
- package/scripts/lane/lane-process-stats.ts +93 -0
- package/scripts/lane/lane-registry.ts +17 -10
- package/scripts/lane/lane-restart.ts +5 -26
- package/scripts/lane/lane-session-action.ts +146 -0
- package/scripts/lane/lane-session-broker.ts +68 -125
- package/scripts/lane/lane-spent.ts +14 -3
- package/scripts/lane/lane-stats.ts +1 -0
- package/scripts/lane/lane-withdraw-cli.ts +71 -0
- package/scripts/lane/lane-withdraw.ts +182 -0
- package/scripts/lint/format.ts +4 -4
- package/scripts/lint/lint-parallel.ts +10 -3
- package/scripts/lint/lint-related-scope.ts +4 -1
- package/scripts/managed-marker/managed-marker-logic.ts +1 -2
- package/scripts/notify/telegram-test.ts +10 -5
- package/scripts/overrides/lockfile-overrides.ts +1 -3
- package/scripts/propagate/propagate.ts +1 -2
- package/scripts/release/release-progress.ts +3 -2
- package/scripts/report/report-format-reference.ts +2 -0
- package/scripts/report/report-lint.ts +7 -4
- package/scripts/review/acceptance-map-check.ts +61 -0
- package/scripts/review/acceptance-map.ts +205 -0
- package/scripts/review/review-brief-cli.ts +1 -1
- package/scripts/review/review-record-cli.ts +6 -2
- package/scripts/rules/agent-model.ts +59 -0
- package/scripts/rules/delivered-rules.ts +11 -2
- package/scripts/rules/destructive-command.ts +6 -9
- package/scripts/rules/direct-filing.ts +3 -7
- package/scripts/rules/early-heartbeat.ts +7 -18
- package/scripts/rules/file-body.ts +6 -13
- package/scripts/rules/filing-cap.ts +4 -6
- package/scripts/rules/firing-point.ts +6 -5
- package/scripts/rules/git-force.ts +5 -9
- package/scripts/rules/guard-remedy.ts +117 -0
- package/scripts/rules/implementation-cut.ts +37 -35
- package/scripts/rules/index-guard.ts +4 -7
- package/scripts/rules/issue-comments.ts +67 -16
- package/scripts/rules/josh-git-bare.ts +3 -6
- package/scripts/rules/lane-background.ts +10 -21
- package/scripts/rules/lane-carry-conflict.ts +7 -17
- package/scripts/rules/lane-interactive-ask.ts +6 -11
- package/scripts/rules/lane-park.ts +5 -13
- package/scripts/rules/lane-split-park.ts +2 -7
- package/scripts/rules/lane-switch-main.ts +9 -12
- package/scripts/rules/last-prompt.ts +50 -5
- package/scripts/rules/oracle-consulted.ts +4 -5
- package/scripts/rules/oracle-list-cli.ts +7 -0
- package/scripts/rules/piped-verification.ts +84 -32
- package/scripts/rules/poll-loop.ts +7 -17
- package/scripts/rules/pre-gate-cut.ts +7 -21
- package/scripts/rules/prior-comment-read.ts +7 -5
- package/scripts/rules/protected-files.ts +5 -9
- package/scripts/rules/raw-field-body.ts +3 -7
- package/scripts/rules/rule-body-guard.ts +4 -11
- package/scripts/rules/rule-list.ts +4 -1
- package/scripts/rules/rule-registry-rows.ts +10 -0
- package/scripts/rules/rule-registry.ts +7 -4
- package/scripts/rules/rule-rewrite.ts +114 -0
- package/scripts/rules/rule-value.ts +7 -1
- package/scripts/rules/run-tail-rule.ts +6 -22
- package/scripts/rules/shell-body-trigger.ts +4 -6
- package/scripts/rules/shell-segments.ts +22 -0
- package/scripts/rules/stop-decision.ts +77 -0
- package/scripts/rules/stop-rules.ts +164 -77
- package/scripts/rules/tail-commands.ts +15 -1
- package/scripts/rules/test-declared-commit.ts +4 -10
- package/scripts/rules/third-party-write.ts +5 -10
- package/scripts/rules/worktree-guard.ts +5 -12
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-closed.ts +8 -2
- package/scripts/run/board/run-board-github.ts +19 -3
- package/scripts/run/board/run-board-header.ts +4 -1
- package/scripts/run/board/run-board-idle.ts +31 -0
- package/scripts/run/board/run-board-labels.ts +2 -0
- package/scripts/run/board/run-board-layout.ts +1 -1
- package/scripts/run/board/run-board-read.ts +25 -12
- package/scripts/run/board/run-board-reason.ts +17 -4
- package/scripts/run/board/run-board-render-legend.ts +1 -0
- package/scripts/run/board/run-board-render-slot.ts +1 -1
- package/scripts/run/board/run-board-render-waits.ts +41 -0
- package/scripts/run/board/run-board-render.ts +6 -1
- package/scripts/run/board/run-board-row-yield.ts +1 -0
- package/scripts/run/board/run-board-state.ts +9 -0
- package/scripts/run/board/run-board-status.ts +89 -23
- package/scripts/run/board/run-board-thanks.ts +7 -2
- package/scripts/run/board/run-board-tick.ts +37 -8
- package/scripts/run/carry/run-carry-args.ts +1 -3
- package/scripts/run/carry/run-carry-conversation.ts +7 -2
- package/scripts/run/cut/run-cut-cli.ts +18 -22
- package/scripts/run/cut/run-cut-decline.ts +72 -0
- package/scripts/run/cut/run-cut-ready.ts +74 -0
- package/scripts/run/cut/run-cut-report.ts +29 -0
- package/scripts/run/cut/run-cut.ts +10 -1
- package/scripts/run/detached-launch.ts +23 -4
- package/scripts/run/entry/run-entry-cli.ts +8 -1
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-scope.ts +19 -1
- package/scripts/run/event/run-event-stream-emit.ts +13 -6
- package/scripts/run/event/run-event-stream.ts +4 -0
- package/scripts/run/hold/run-hold-cli.ts +14 -3
- package/scripts/run/hold/run-hold-session.ts +46 -0
- package/scripts/run/hold/run-hold.ts +13 -4
- package/scripts/run/launch-priority.ts +32 -0
- package/scripts/run/merge/run-merge-cli.ts +16 -42
- package/scripts/run/merge/run-merge-ship.ts +59 -1
- package/scripts/run/merge/run-merge-signals.ts +99 -0
- package/scripts/run/merge/run-merge-steps.ts +11 -1
- package/scripts/run/merge/run-merge.ts +22 -9
- package/scripts/run/run-ending.ts +31 -3
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-parent-cut-hook.ts +5 -10
- package/scripts/run/run-poll-hook.ts +157 -0
- package/scripts/run/run-preflight.ts +1 -2
- package/scripts/run/run-step-cli.ts +22 -15
- package/scripts/run/run-step-section.ts +85 -0
- package/scripts/run/run-stop-notify.ts +1 -2
- package/scripts/run/run-watcher-guard.ts +6 -3
- package/scripts/run/ship/run-ship-cli.ts +5 -12
- package/scripts/run/ship/run-ship-detach.ts +9 -0
- package/scripts/run/ship/run-ship-next.ts +10 -2
- package/scripts/run/ship/run-ship-overlap.ts +195 -0
- package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
- package/scripts/run/ship/run-ship-preflight.ts +28 -2
- package/scripts/run/ship/run-ship-return.ts +31 -7
- package/scripts/run/ship/run-ship-review-steps.ts +25 -14
- package/scripts/run/ship/run-ship-review.ts +15 -2
- package/scripts/run/ship/run-ship-steps.ts +1 -1
- package/scripts/run/ship/run-ship-stop-context.ts +206 -0
- package/scripts/run/ship/run-ship-sync.ts +14 -6
- package/scripts/run/tidy/run-tidy-cli.ts +3 -3
- package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
- package/scripts/safe-chain/project-config.ts +7 -2
- package/scripts/sync/sync-hook-safety.ts +8 -2
- package/scripts/test/test-e2e-guard.ts +30 -5
- package/scripts/test/test-unit-guard.ts +16 -1
- package/scripts/test/unit-worker-share.ts +26 -3
- package/scripts/time-runtime/time-batch-guard.ts +3 -3
- package/scripts/time-runtime/time-hook-transcript.ts +3 -2
- package/scripts/time-runtime/time-shell.ts +39 -4
- package/scripts/time-runtime/time-spans.ts +1 -0
- package/scripts/time-runtime/time-tool-call.ts +7 -0
- package/scripts/version/effective-upstream.ts +3 -4
- package/scripts/version/kit-version-config.ts +8 -2
- package/scripts/version/latest-corepack.ts +1 -2
- package/scripts/version/release-hold.ts +11 -4
- package/scripts/version/version-commands.ts +12 -4
- package/templates/gitignore +6 -2
- package/dist/hooks/chunk-D5QBTOCZ.js +0 -68
- package/dist/hooks/chunk-UT7YC3N2.js +0 -56
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { agent_argv } from '#scripts/agent/agent-argv'
|
|
2
|
+
import { agent_role_profile, type AgentProvider } from '#scripts/agent/agent-role-profile'
|
|
3
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
4
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
5
|
+
import { detached_launch, type AttachedLaunchResult } from '#scripts/run/detached-launch'
|
|
6
|
+
import { run_ship_review } from '#scripts/run/ship/run-ship-review'
|
|
7
|
+
import { run_ship_stage } from '#scripts/run/ship/run-ship-stage'
|
|
8
|
+
import { run_ship_stop_context } from '#scripts/run/ship/run-ship-stop-context'
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
import { lane_child_invocation, type ShipStop } from './lane-child-invocation'
|
|
11
|
+
import { lane_child_marker } from './lane-child-marker'
|
|
12
|
+
import { lane_parked } from './lane-parked'
|
|
13
|
+
import type { LaneInfo } from './lane-registry'
|
|
14
|
+
import { lane_relaunch } from './lane-relaunch'
|
|
15
|
+
|
|
16
|
+
// The actions a lane's session broker performs for the ship that asked (`lane-session-broker.ts`):
|
|
17
|
+
// starting the round-1 or round-2 reviewer, or relaunching the lane's child after a stopped ship.
|
|
18
|
+
// **A failure names its reason**: a bare `false` left every relaunch a stale supervisor refused
|
|
19
|
+
// unexplained in the ship log (joshuafolkken/kit#3974).
|
|
20
|
+
|
|
21
|
+
const BRIEF_PREFIX = 'josh-ship-review-brief-'
|
|
22
|
+
const FINDINGS_PREFIX = 'josh-ship-review-findings-'
|
|
23
|
+
const LOG_PREFIX = 'josh-ship-review-log-'
|
|
24
|
+
const ANTHROPIC_PROVIDER: AgentProvider = 'anthropic'
|
|
25
|
+
const stop_schema = z
|
|
26
|
+
.object({
|
|
27
|
+
stage: z.enum(run_ship_stage.STAGE),
|
|
28
|
+
title: z.string(),
|
|
29
|
+
flags: z.array(z.string()).readonly(),
|
|
30
|
+
is_review: z.boolean(),
|
|
31
|
+
conflicts: z.array(z.string()).readonly().default([]),
|
|
32
|
+
context: run_ship_stop_context.stop_context_schema.exactOptional(),
|
|
33
|
+
})
|
|
34
|
+
.strict()
|
|
35
|
+
const action_schema = z.discriminatedUnion('kind', [
|
|
36
|
+
z.object({ kind: z.literal('review'), round: z.enum(['1', '2']) }).strict(),
|
|
37
|
+
z.object({ kind: z.literal('relaunch'), stop: stop_schema }).strict(),
|
|
38
|
+
])
|
|
39
|
+
type ReviewRound = Extract<z.infer<typeof action_schema>, { kind: 'review' }>['round']
|
|
40
|
+
type BrokerAction = { kind: 'review'; round: ReviewRound } | { kind: 'relaunch'; stop: ShipStop }
|
|
41
|
+
type BrokerAnswer = { is_success: true } | { is_success: false; reason: string }
|
|
42
|
+
|
|
43
|
+
const SUCCESS: BrokerAnswer = { is_success: true }
|
|
44
|
+
|
|
45
|
+
function failure(reason: string): BrokerAnswer {
|
|
46
|
+
return { is_success: false, reason }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function review_paths(directory: string): { brief: string; findings: string; log: string } {
|
|
50
|
+
return {
|
|
51
|
+
brief: stamp_file.stamp_path(BRIEF_PREFIX, directory, '.md'),
|
|
52
|
+
findings: stamp_file.stamp_path(FINDINGS_PREFIX, directory, '.txt'),
|
|
53
|
+
log: stamp_file.stamp_path(LOG_PREFIX, directory, '.log'),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The reviewer runs on the lane's own provider, so a Claude lane's server never starts a Codex reviewer
|
|
58
|
+
// and an OpenAI lane's never starts a Claude one.
|
|
59
|
+
function provider_of(lane: LaneInfo): AgentProvider {
|
|
60
|
+
return lane.profile?.provider ?? ANTHROPIC_PROVIDER
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// **The reviewer carries the lane's mark explicitly**: the supervisor serving a Claude lane is no lane child,
|
|
64
|
+
// so a reviewer inheriting its environment would descend from the carry record's owner unmarked and be
|
|
65
|
+
// read as the `backlogrun` parent by every hook. **Its prompt ends with the review invocation**, so the
|
|
66
|
+
// lane's liveness read and the park's reap find it as they find the child (joshuafolkken/kit#3878).
|
|
67
|
+
|
|
68
|
+
function review_prompt(lane: LaneInfo, round: ReviewRound): string {
|
|
69
|
+
const target = review_paths(lane.directory)
|
|
70
|
+
const prompt_of =
|
|
71
|
+
round === '1' ? run_ship_review.reviewer_prompt : run_ship_review.verification_prompt
|
|
72
|
+
const tail = lane_child_invocation.review_invocation(lane.issue)
|
|
73
|
+
|
|
74
|
+
return `${prompt_of(target.brief, target.findings, lane.issue)} This is ${tail}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function reviewer_answer(result: AttachedLaunchResult): BrokerAnswer {
|
|
78
|
+
if (result.kind === 'failed') return failure(`the reviewer could not start: ${result.note}`)
|
|
79
|
+
|
|
80
|
+
return result.exit_code === 0
|
|
81
|
+
? SUCCESS
|
|
82
|
+
: failure(`the reviewer exited with code ${String(result.exit_code)}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<BrokerAnswer> {
|
|
86
|
+
const target = review_paths(lane.directory)
|
|
87
|
+
const built = agent_argv.review_in(review_prompt(lane, round), Number(round), lane.directory)
|
|
88
|
+
if (built.kind === 'rejected') return failure(`the reviewer profile was rejected: ${built.note}`)
|
|
89
|
+
|
|
90
|
+
if (built.profile.provider !== provider_of(lane)) {
|
|
91
|
+
return failure(`the reviewer resolved to ${built.profile.provider}, not the lane's provider`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.error(agent_role_profile.describe_review(Number(round), built.profile))
|
|
95
|
+
const result = await detached_launch.launch_attached(
|
|
96
|
+
{
|
|
97
|
+
argv: built.argv,
|
|
98
|
+
cwd: lane.directory,
|
|
99
|
+
log_path: target.log,
|
|
100
|
+
profile: built.profile,
|
|
101
|
+
env: lane_child_marker.env_for(lane.issue),
|
|
102
|
+
},
|
|
103
|
+
console.error,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return reviewer_answer(result)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// An OpenAI lane's successor is its supervisor's to start, never a relaunch's.
|
|
110
|
+
function relaunch(lane: LaneInfo, stop: ShipStop): BrokerAnswer {
|
|
111
|
+
if (lane_relaunch.is_openai_lane(lane)) {
|
|
112
|
+
return failure('an OpenAI lane is relaunched by its own supervisor')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const invocation = lane_child_invocation.ship_stop_invocation(lane.issue, stop)
|
|
116
|
+
const phase = agent_role_profile.IMPLEMENTATION_PHASE
|
|
117
|
+
const result = lane_relaunch.relaunch(lane, invocation, phase, console.error)
|
|
118
|
+
|
|
119
|
+
return result.kind === 'launched'
|
|
120
|
+
? SUCCESS
|
|
121
|
+
: failure(`the relaunch did not start: ${result.note}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// **A parked issue gets no session**: a request can be read long after it was written, so the park is
|
|
125
|
+
// read when the action runs, never when it was asked for (joshuafolkken/kit#3878).
|
|
126
|
+
async function perform(lane: LaneInfo, action: BrokerAction): Promise<BrokerAnswer> {
|
|
127
|
+
if (await lane_parked.is_parked(lane.issue)) return failure('the issue is parked')
|
|
128
|
+
|
|
129
|
+
if (action.kind === 'relaunch') return relaunch(lane, action.stop)
|
|
130
|
+
|
|
131
|
+
return await launch_review(lane, action.round)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// What the server answers: a thrown action is a failure naming its error, never a dead server.
|
|
135
|
+
async function answer(lane: LaneInfo, action: BrokerAction): Promise<BrokerAnswer> {
|
|
136
|
+
try {
|
|
137
|
+
return await perform(lane, action)
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return failure(`the broker failed: ${error_text.message_of(error)}`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const lane_session_action = { SUCCESS, action_schema, answer, failure, review_paths }
|
|
144
|
+
|
|
145
|
+
export type { BrokerAction, BrokerAnswer, ReviewRound }
|
|
146
|
+
export { lane_session_action }
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
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
5
|
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
8
6
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
9
|
-
import { error_text } from '#scripts/lib/error-message'
|
|
10
7
|
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
8
|
import { z } from 'zod'
|
|
15
9
|
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
10
|
import type { LaneInfo } from './lane-registry'
|
|
19
|
-
import {
|
|
11
|
+
import { lane_session_action, type BrokerAction, type BrokerAnswer } from './lane-session-action'
|
|
20
12
|
|
|
21
13
|
// The sessions a lane child's detached `josh ship` cannot start itself, started for it by a process
|
|
22
14
|
// that can: the round-1 and round-2 reviewer, and the fresh child a stopped ship hands back to. An
|
|
@@ -24,13 +16,11 @@ import { lane_relaunch } from './lane-relaunch'
|
|
|
24
16
|
// Claude lane is served by the `run:wake` supervisor (`claude-lane-broker.ts`), because Claude Code removes its
|
|
25
17
|
// token from what its Bash tool starts, so a token-only credential never reaches the ship the child
|
|
26
18
|
// 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
|
|
19
|
+
// rebuilds the argv from the registered lane, so a child can ask for nothing but these two sessions
|
|
20
|
+
// (`lane-session-action.ts`).
|
|
28
21
|
|
|
29
22
|
const REQUEST_PREFIX = '.josh-review-request-'
|
|
30
23
|
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
24
|
const CACHE_DIRECTORY = ['node_modules', '.cache']
|
|
35
25
|
const POLL_MS = 100
|
|
36
26
|
// The heartbeat is kept for a ship branched before the server record, and read only where a
|
|
@@ -40,22 +30,8 @@ const LEGACY_HEARTBEAT_LIMIT_MS = 3000
|
|
|
40
30
|
const MINUTE_MS = 60_000
|
|
41
31
|
const REQUEST_LIMIT_MINUTES = 30
|
|
42
32
|
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
33
|
const request_schema = z
|
|
58
|
-
.object({ session: z.uuid(), nonce: z.uuid(), action: action_schema })
|
|
34
|
+
.object({ session: z.uuid(), nonce: z.uuid(), action: lane_session_action.action_schema })
|
|
59
35
|
.strict()
|
|
60
36
|
const response_schema = z
|
|
61
37
|
.object({
|
|
@@ -65,18 +41,13 @@ const response_schema = z
|
|
|
65
41
|
heartbeat_at: z.number().int().positive(),
|
|
66
42
|
})
|
|
67
43
|
.strict()
|
|
44
|
+
// **A failure's reason sits beside the response, never in it**: a ship branched before the reason parses
|
|
45
|
+
// the response strictly, so a new field would read as no server (joshuafolkken/kit#3974).
|
|
46
|
+
const reason_schema = z.object({ nonce: z.string(), reason: z.string() }).strict()
|
|
68
47
|
type BrokerRequest = z.infer<typeof request_schema>
|
|
69
48
|
type BrokerResponse = z.infer<typeof response_schema>
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
}
|
|
49
|
+
|
|
50
|
+
const { failure } = lane_session_action
|
|
80
51
|
|
|
81
52
|
function request_path(directory: string, issue: string): string {
|
|
82
53
|
return path.join(directory, ...CACHE_DIRECTORY, `${REQUEST_PREFIX}${issue}.json`)
|
|
@@ -89,6 +60,10 @@ function response_path(directory: string, issue: string): string | undefined {
|
|
|
89
60
|
return path.join(path.dirname(common), ...CACHE_DIRECTORY, `${RESPONSE_PREFIX}${issue}.json`)
|
|
90
61
|
}
|
|
91
62
|
|
|
63
|
+
function reason_path(response: string): string {
|
|
64
|
+
return `${response}.reason`
|
|
65
|
+
}
|
|
66
|
+
|
|
92
67
|
function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
|
|
93
68
|
try {
|
|
94
69
|
return json_value.parse_with(readFileSync(target, 'utf8'), schema)
|
|
@@ -97,90 +72,25 @@ function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
|
|
|
97
72
|
}
|
|
98
73
|
}
|
|
99
74
|
|
|
100
|
-
|
|
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 {
|
|
75
|
+
function write_record(target: string, body: BrokerResponse | z.infer<typeof reason_schema>): void {
|
|
153
76
|
const temporary = `${target}.${String(process.pid)}.tmp`
|
|
154
77
|
|
|
155
78
|
writeFileSync(temporary, JSON.stringify(body))
|
|
156
79
|
renameSync(temporary, target)
|
|
157
80
|
}
|
|
158
81
|
|
|
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
82
|
async function process_request(
|
|
170
83
|
lane: LaneInfo,
|
|
171
84
|
response: string,
|
|
172
85
|
received: BrokerRequest,
|
|
173
86
|
state: { current: BrokerResponse },
|
|
174
87
|
): Promise<void> {
|
|
175
|
-
const
|
|
88
|
+
const answer = await lane_session_action.answer(lane, received.action)
|
|
89
|
+
const { session, nonce } = received
|
|
176
90
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
is_success,
|
|
181
|
-
heartbeat_at: Date.now(),
|
|
182
|
-
}
|
|
183
|
-
write_response(response, state.current)
|
|
91
|
+
if (!answer.is_success) write_record(reason_path(response), { nonce, reason: answer.reason })
|
|
92
|
+
state.current = { session, nonce, is_success: answer.is_success, heartbeat_at: Date.now() }
|
|
93
|
+
write_record(response, state.current)
|
|
184
94
|
}
|
|
185
95
|
|
|
186
96
|
async function serve(
|
|
@@ -223,7 +133,7 @@ function prepare(lane: LaneInfo): { response: string; current: BrokerResponse }
|
|
|
223
133
|
heartbeat_at: Date.now(),
|
|
224
134
|
}
|
|
225
135
|
|
|
226
|
-
|
|
136
|
+
write_record(response, current)
|
|
227
137
|
|
|
228
138
|
return { response, current }
|
|
229
139
|
}
|
|
@@ -231,7 +141,7 @@ function prepare(lane: LaneInfo): { response: string; current: BrokerResponse }
|
|
|
231
141
|
function start_heartbeat(state: { current: BrokerResponse }, response: string): NodeJS.Timeout {
|
|
232
142
|
const heartbeat = setInterval(() => {
|
|
233
143
|
state.current = { ...state.current, heartbeat_at: Date.now() }
|
|
234
|
-
|
|
144
|
+
write_record(response, state.current)
|
|
235
145
|
}, LEGACY_HEARTBEAT_MS)
|
|
236
146
|
|
|
237
147
|
heartbeat.unref()
|
|
@@ -251,6 +161,7 @@ async function stop_server(
|
|
|
251
161
|
await serving
|
|
252
162
|
clearInterval(heartbeat)
|
|
253
163
|
unlinkSync(response)
|
|
164
|
+
rmSync(reason_path(response), { force: true })
|
|
254
165
|
lane_broker_server.remove(response)
|
|
255
166
|
}
|
|
256
167
|
|
|
@@ -293,31 +204,47 @@ function session_response(target: string, session: string): BrokerResponse | und
|
|
|
293
204
|
return response?.session === session ? response : undefined
|
|
294
205
|
}
|
|
295
206
|
|
|
207
|
+
// A failure from a server that predates the reason leaves none.
|
|
208
|
+
function answer_of(target: string, response: BrokerResponse): BrokerAnswer {
|
|
209
|
+
if (response.is_success) return lane_session_action.SUCCESS
|
|
210
|
+
|
|
211
|
+
const record = read_record(reason_path(target), reason_schema)
|
|
212
|
+
const reason = record?.nonce === response.nonce ? record.reason : 'it gave no reason'
|
|
213
|
+
|
|
214
|
+
return failure(`the broker answered a failure: ${reason}`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// The server's answer to this request, or `undefined` while it is still being served.
|
|
296
218
|
function response_verdict(
|
|
297
219
|
target: string,
|
|
298
220
|
session: string,
|
|
299
221
|
nonce: string,
|
|
300
|
-
):
|
|
222
|
+
): BrokerAnswer | undefined {
|
|
301
223
|
const response = session_response(target, session)
|
|
302
224
|
|
|
303
|
-
if (!is_live_response(target, response, lane_broker_server.is_running))
|
|
304
|
-
|
|
225
|
+
if (!is_live_response(target, response, lane_broker_server.is_running)) {
|
|
226
|
+
return failure('the broker stopped serving before it answered')
|
|
227
|
+
}
|
|
305
228
|
|
|
306
|
-
return response.
|
|
229
|
+
return response.nonce === nonce ? answer_of(target, response) : undefined
|
|
307
230
|
}
|
|
308
231
|
|
|
309
|
-
async function wait_response(
|
|
232
|
+
async function wait_response(
|
|
233
|
+
target: string,
|
|
234
|
+
session: string,
|
|
235
|
+
nonce: string,
|
|
236
|
+
): Promise<BrokerAnswer> {
|
|
310
237
|
const deadline = Date.now() + REQUEST_LIMIT_MS
|
|
311
238
|
|
|
312
239
|
while (Date.now() < deadline) {
|
|
313
240
|
const verdict = response_verdict(target, session, nonce)
|
|
314
|
-
if (verdict !==
|
|
241
|
+
if (verdict !== undefined) return verdict
|
|
315
242
|
|
|
316
243
|
// eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
|
|
317
244
|
await sleep(POLL_MS)
|
|
318
245
|
}
|
|
319
246
|
|
|
320
|
-
return
|
|
247
|
+
return failure(`the broker gave no answer within ${String(REQUEST_LIMIT_MINUTES)} minutes`)
|
|
321
248
|
}
|
|
322
249
|
|
|
323
250
|
function live_response(target: string): BrokerResponse | undefined {
|
|
@@ -336,18 +263,33 @@ function is_serving(directory: string, issue: string): boolean {
|
|
|
336
263
|
return target !== undefined && live_response(target) !== undefined
|
|
337
264
|
}
|
|
338
265
|
|
|
339
|
-
|
|
266
|
+
// **A request nobody waits for any more is taken back**: a server busy with an earlier request reads the
|
|
267
|
+
// file only once it is free, and a relaunch the ship had already given up on then started a session in a
|
|
268
|
+
// lane the run had parked in the meantime (joshuafolkken/kit#3878). A newer request is left alone.
|
|
269
|
+
function withdraw(pending: string, nonce: string): void {
|
|
270
|
+
if (read_record(pending, request_schema)?.nonce === nonce) stamp_file.remove_stamp(pending)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function request(
|
|
274
|
+
directory: string,
|
|
275
|
+
issue: string,
|
|
276
|
+
action: BrokerAction,
|
|
277
|
+
): Promise<BrokerAnswer> {
|
|
340
278
|
const target = response_path(directory, issue)
|
|
341
279
|
const response = target === undefined ? undefined : live_response(target)
|
|
342
280
|
|
|
343
|
-
if (target === undefined || response === undefined) return
|
|
281
|
+
if (target === undefined || response === undefined) return failure('no broker serves this lane')
|
|
344
282
|
|
|
345
283
|
const nonce = randomUUID()
|
|
346
284
|
const body = { session: response.session, nonce, action }
|
|
285
|
+
const pending = request_path(directory, issue)
|
|
347
286
|
|
|
348
|
-
stamp_file.replace_stamp(
|
|
287
|
+
stamp_file.replace_stamp(pending, body)
|
|
288
|
+
const answer = await wait_response(target, response.session, nonce)
|
|
349
289
|
|
|
350
|
-
|
|
290
|
+
withdraw(pending, nonce)
|
|
291
|
+
|
|
292
|
+
return answer
|
|
351
293
|
}
|
|
352
294
|
|
|
353
295
|
const lane_session_broker = {
|
|
@@ -355,9 +297,10 @@ const lane_session_broker = {
|
|
|
355
297
|
request,
|
|
356
298
|
request_path,
|
|
357
299
|
response_path,
|
|
358
|
-
review_paths,
|
|
300
|
+
review_paths: lane_session_action.review_paths,
|
|
359
301
|
with_server,
|
|
360
302
|
}
|
|
361
303
|
|
|
362
|
-
export type { BrokerAction, ReviewRound }
|
|
363
304
|
export { lane_session_broker }
|
|
305
|
+
|
|
306
|
+
export { type ReviewRound, type BrokerAction, type BrokerAnswer } from './lane-session-action'
|
|
@@ -178,10 +178,13 @@ function issues_traced(traced: ReadonlyArray<TracedLane>, trace: ProcessTrace):
|
|
|
178
178
|
return traced.filter(([, answer]) => answer === trace).map(([issue]) => issue)
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
function traced_of(lanes: ReadonlyArray<LaneInfo>, probes: SpentProbes): Array<TracedLane> {
|
|
182
|
+
return lanes.map((lane): TracedLane => [lane.issue, trace_of(lane, probes)])
|
|
183
|
+
}
|
|
184
|
+
|
|
181
185
|
/** The lanes in use — what a run being replaced must not be stopped over. */
|
|
182
186
|
async function lanes_in_use(probes: SpentProbes = LIVE_PROBES): Promise<LaneUse> {
|
|
183
|
-
const
|
|
184
|
-
const traced = lanes.map((lane): TracedLane => [lane.issue, trace_of(lane, probes)])
|
|
187
|
+
const traced = traced_of(await lane_registry.list_lanes(), probes)
|
|
185
188
|
|
|
186
189
|
return {
|
|
187
190
|
working: issues_traced(traced, PROCESS_ALIVE),
|
|
@@ -189,7 +192,15 @@ async function lanes_in_use(probes: SpentProbes = LIVE_PROBES): Promise<LaneUse>
|
|
|
189
192
|
}
|
|
190
193
|
}
|
|
191
194
|
|
|
192
|
-
|
|
195
|
+
/** The lanes no child is working in, read as such — never one whose probe did not answer. */
|
|
196
|
+
function idle_lanes(
|
|
197
|
+
lanes: ReadonlyArray<LaneInfo>,
|
|
198
|
+
probes: SpentProbes = LIVE_PROBES,
|
|
199
|
+
): Array<string> {
|
|
200
|
+
return issues_traced(traced_of(lanes, probes), PROCESS_NONE)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const lane_spent = { idle_lanes, kept_reason, lanes_in_use, sort_lanes }
|
|
193
204
|
|
|
194
205
|
export type { KeptLane, LaneUse, SpentProbes, SpentSort }
|
|
195
206
|
export { lane_spent }
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
4
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
|
+
import { lane_withdraw, type WithdrawOutcome } from './lane-withdraw'
|
|
6
|
+
|
|
7
|
+
// `josh lane:withdraw <issue> --reason <text>` — every explanation on standard error, and a withdrawal
|
|
8
|
+
// that did not end with the Issue closed exits non-zero, so a caller never takes a half-done one as done.
|
|
9
|
+
|
|
10
|
+
const ARGV_OFFSET = 2
|
|
11
|
+
const SUCCESS_EXIT_CODE = 0
|
|
12
|
+
const FAILURE_EXIT_CODE = 1
|
|
13
|
+
const USAGE = 'Usage: josh lane:withdraw <issue-number> --reason <text>'
|
|
14
|
+
const OPTIONS = { reason: { type: 'string' } } as const
|
|
15
|
+
|
|
16
|
+
interface Request {
|
|
17
|
+
issue: string
|
|
18
|
+
reason: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function issue_of(positionals: ReadonlyArray<string>): string | undefined {
|
|
22
|
+
const [issue, ...rest] = positionals
|
|
23
|
+
|
|
24
|
+
return rest.length === 0 && issue_number_shape.is_issue_number(issue ?? '') ? issue : undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A reason of only whitespace is no reason: the Issue would close with an empty `Why:` line.
|
|
28
|
+
function reason_of(value: unknown): string | undefined {
|
|
29
|
+
const reason = cli_flags.string_of(value)?.trim()
|
|
30
|
+
|
|
31
|
+
return reason === '' ? undefined : reason
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parse_request(argv: ReadonlyArray<string>): Request | undefined {
|
|
35
|
+
const parsed = cli_flags.arguments_of(argv, OPTIONS)
|
|
36
|
+
if (parsed === undefined) return undefined
|
|
37
|
+
const issue = issue_of(parsed.positionals)
|
|
38
|
+
const reason = reason_of(parsed.values.reason)
|
|
39
|
+
|
|
40
|
+
return issue === undefined || reason === undefined ? undefined : { issue, reason }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function exit_code_of(outcome: WithdrawOutcome): number {
|
|
44
|
+
return outcome.kind === 'withdrawn' && outcome.is_closed ? SUCCESS_EXIT_CODE : FAILURE_EXIT_CODE
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
48
|
+
const request = parse_request(argv)
|
|
49
|
+
|
|
50
|
+
if (request === undefined) {
|
|
51
|
+
console.error(USAGE)
|
|
52
|
+
|
|
53
|
+
return FAILURE_EXIT_CODE
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const outcome = await lane_withdraw.withdraw(request.issue, request.reason)
|
|
57
|
+
|
|
58
|
+
console.error(lane_withdraw.describe(outcome, request.issue))
|
|
59
|
+
|
|
60
|
+
return exit_code_of(outcome)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
64
|
+
process.exitCode = await run(argv)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lane_withdraw_cli = { run }
|
|
68
|
+
|
|
69
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
|
|
70
|
+
|
|
71
|
+
export { lane_withdraw_cli }
|