@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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
1
3
|
import { run_cut, type CutState, type RunCut } from './run-cut'
|
|
2
4
|
import { run_cut_handoff } from './run-cut-handoff'
|
|
3
5
|
|
|
@@ -136,6 +138,30 @@ function report_reviewer(): number {
|
|
|
136
138
|
return report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE)
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
// The cut outside a lane is taken, and nothing is relaunched: the session that took it is the one a
|
|
142
|
+
// person is watching, so the run is handed on the way `fullrun`'s entry `over` stop hands it on.
|
|
143
|
+
function report_held_cut(issue: string): number {
|
|
144
|
+
console.error(
|
|
145
|
+
`The cut is recorded for ${session_cite.issue(issue)} outside a lane, so nothing was relaunched. Keep the hold, send a \`confirmation\` Telegram whose body names the resume command \`fullrun ${issue_cite.plain(issue)}\`, and end the turn; the fresh session's \`pnpm josh run:cut --resume ${issue}\` answers \`resume-impl\` with the handoff.`,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// **A cut that relaunched its successor says so on this session's own transcript** (joshuafolkken/kit#3936).
|
|
152
|
+
// The successor's `run:cut --resume` can clear the record before this session's turn ends, so the
|
|
153
|
+
// `Stop` hook cannot tell a cut stop by the record alone; it reads this line off the turn instead, a
|
|
154
|
+
// fact no other process can remove. The held cut never prints it — a person resumes that one. **A bare
|
|
155
|
+
// token line**, so it is read off the `run:cut` call's own result (`cost-run-cut.ts`), and a turn that
|
|
156
|
+
// merely quotes it is not read as a relaunch.
|
|
157
|
+
const RELAUNCHED_CUT_MARKER = 'cut-relaunched'
|
|
158
|
+
|
|
159
|
+
function report_relaunched_cut(): number {
|
|
160
|
+
console.error(RELAUNCHED_CUT_MARKER)
|
|
161
|
+
|
|
162
|
+
return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
|
|
163
|
+
}
|
|
164
|
+
|
|
139
165
|
// **An implementation cut is never resumed by a session over the threshold**.
|
|
140
166
|
// The cut outside a lane leaves the session that took it open, so a person retyping `fullrun #N` there
|
|
141
167
|
// would be answered `resume-impl`, keep implementing over the threshold, and be cut again on the next
|
|
@@ -162,6 +188,7 @@ const run_cut_report = {
|
|
|
162
188
|
INCOMPLETE_VERDICT,
|
|
163
189
|
NOT_A_LANE_VERDICT,
|
|
164
190
|
OVER_VERDICT,
|
|
191
|
+
RELAUNCHED_CUT_MARKER,
|
|
165
192
|
RESUME_IMPL_VERDICT,
|
|
166
193
|
RESUME_VERDICT,
|
|
167
194
|
STALE_VERDICT,
|
|
@@ -176,8 +203,10 @@ const run_cut_report = {
|
|
|
176
203
|
report_busy,
|
|
177
204
|
report_fault,
|
|
178
205
|
report_handed_off,
|
|
206
|
+
report_held_cut,
|
|
179
207
|
report_incomplete,
|
|
180
208
|
report_over,
|
|
209
|
+
report_relaunched_cut,
|
|
181
210
|
report_reviewer,
|
|
182
211
|
report_stale,
|
|
183
212
|
report_under_threshold,
|
|
@@ -106,6 +106,8 @@ interface CutState {
|
|
|
106
106
|
is_held: boolean
|
|
107
107
|
// The issue a `fullrun` hold names, absent when the tree is not held by one.
|
|
108
108
|
held_issue?: string | undefined
|
|
109
|
+
// When the hold standing over the tree was taken — what keys a declined cut to the run it answered.
|
|
110
|
+
hold_taken_at?: string | undefined
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
// The fields a cut is written from, bundled so `begin_cut` stays within the parameter limit. `phase`
|
|
@@ -402,7 +404,13 @@ async function current_state(): Promise<CutState> {
|
|
|
402
404
|
const is_dirty = await run_hold.is_tree_dirty()
|
|
403
405
|
const hold = hold_of(await worktree_directory())
|
|
404
406
|
|
|
405
|
-
return {
|
|
407
|
+
return {
|
|
408
|
+
branch,
|
|
409
|
+
is_dirty,
|
|
410
|
+
is_held: hold !== undefined,
|
|
411
|
+
held_issue: fullrun_issue(hold),
|
|
412
|
+
hold_taken_at: hold?.taken_at,
|
|
413
|
+
}
|
|
406
414
|
}
|
|
407
415
|
|
|
408
416
|
function describe_cut(cut: RunCut): string {
|
|
@@ -466,6 +474,7 @@ const run_cut = {
|
|
|
466
474
|
handed_off_message,
|
|
467
475
|
has_required_handoff,
|
|
468
476
|
held_issue_sync,
|
|
477
|
+
hold_of,
|
|
469
478
|
incomplete_message,
|
|
470
479
|
invocation_for,
|
|
471
480
|
is_expired,
|
|
@@ -4,6 +4,7 @@ import { agent_launch_environment } from '#scripts/agent/agent-launch-environmen
|
|
|
4
4
|
import { agent_role_profile, type AgentProfile } from '#scripts/agent/agent-role-profile'
|
|
5
5
|
import { agent_session_environment } from '#scripts/josh/agent-session-environment'
|
|
6
6
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
7
|
+
import { launch_priority } from '#scripts/run/launch-priority'
|
|
7
8
|
|
|
8
9
|
// Starting a process that outlives this one, and keeping what it writes.
|
|
9
10
|
//
|
|
@@ -34,13 +35,18 @@ const UNSAFE_NOTE =
|
|
|
34
35
|
//
|
|
35
36
|
// A NUL byte is what `execve` treats as the end of a string, so a value carrying one executes as a
|
|
36
37
|
// prefix of itself; the other control characters have no business in a command line and their presence
|
|
37
|
-
// means the value did not come from where it was supposed to.
|
|
38
|
+
// means the value did not come from where it was supposed to. **An agent's prompt is the one element a
|
|
39
|
+
// line break belongs in** — a ship-stop prompt carries the failed output's lines — so a vector marked
|
|
40
|
+
// `ends_with_prompt` lets it into its last element alone (joshuafolkken/kit#3964).
|
|
38
41
|
//
|
|
39
42
|
// The shared value validation lives with the role policy so environment overrides and argv elements
|
|
40
43
|
// cannot disagree about which control characters or lengths are safe.
|
|
41
44
|
interface LaunchArgv {
|
|
42
45
|
command: string
|
|
43
46
|
args: ReadonlyArray<string>
|
|
47
|
+
// Set by an agent argv builder, whose prompt is the last element: that one element may carry line
|
|
48
|
+
// breaks (`is_safe_prompt`), and every other element is checked as before.
|
|
49
|
+
ends_with_prompt?: boolean
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
type LaunchResult = { kind: 'launched'; pid: number } | { kind: 'failed'; note: string }
|
|
@@ -65,12 +71,23 @@ interface LaunchRequest {
|
|
|
65
71
|
profile?: AgentProfile | undefined
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
const { is_safe_value } = agent_role_profile
|
|
74
|
+
const { is_safe_prompt, is_safe_value } = agent_role_profile
|
|
75
|
+
const LAST = -1
|
|
76
|
+
|
|
77
|
+
// An agent vector's prompt, the last element, may break lines; every other element is a value.
|
|
78
|
+
function is_safe_element(argv: LaunchArgv, value: string, index: number): boolean {
|
|
79
|
+
const is_prompt = argv.ends_with_prompt === true && index === argv.args.length + LAST
|
|
80
|
+
|
|
81
|
+
return is_prompt ? is_safe_prompt(value) : is_safe_value(value)
|
|
82
|
+
}
|
|
69
83
|
|
|
70
84
|
// The single gate every argument vector passes before it reaches the operating system, whoever
|
|
71
85
|
// composed it.
|
|
72
86
|
function is_safe_argv(argv: LaunchArgv): boolean {
|
|
73
|
-
return
|
|
87
|
+
return (
|
|
88
|
+
is_safe_value(argv.command) &&
|
|
89
|
+
argv.args.every((value, index) => is_safe_element(argv, value, index))
|
|
90
|
+
)
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
function note_of(error: unknown): string {
|
|
@@ -251,6 +268,7 @@ async function attached_spawn(
|
|
|
251
268
|
child.once('exit', (exit_code) => {
|
|
252
269
|
resolve({ kind: 'completed', pid: child.pid ?? 0, exit_code })
|
|
253
270
|
})
|
|
271
|
+
launch_priority.lower(child.pid)
|
|
254
272
|
notify_spawn(child.pid, on_spawn, on_error)
|
|
255
273
|
})
|
|
256
274
|
}
|
|
@@ -287,6 +305,7 @@ function spawned(
|
|
|
287
305
|
on_error(note_of(error))
|
|
288
306
|
})
|
|
289
307
|
child.unref()
|
|
308
|
+
launch_priority.lower(child.pid)
|
|
290
309
|
|
|
291
310
|
return child.pid === undefined ? { kind: 'failed', note: NO_PID_NOTE } : launched(child.pid)
|
|
292
311
|
}
|
|
@@ -341,5 +360,5 @@ const detached_launch = {
|
|
|
341
360
|
note_of,
|
|
342
361
|
}
|
|
343
362
|
|
|
344
|
-
export type { LaunchArgv, LaunchRequest, LaunchResult }
|
|
363
|
+
export type { AttachedLaunchResult, LaunchArgv, LaunchRequest, LaunchResult }
|
|
345
364
|
export { detached_launch }
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '#scripts/run/run-stage'
|
|
23
23
|
import { run_stage_read, type StageRead } from '#scripts/run/run-stage-read'
|
|
24
24
|
import { run_step } from '#scripts/run/run-step'
|
|
25
|
+
import { run_step_section } from '#scripts/run/run-step-section'
|
|
25
26
|
import { run_entry, type EntryParts } from './run-entry'
|
|
26
27
|
import { run_entry_stop } from './run-entry-stop'
|
|
27
28
|
|
|
@@ -259,7 +260,13 @@ async function proceed(issue_number: string, hold: string, cost: string): Promis
|
|
|
259
260
|
// update is measured against (`issue-watch.ts`).
|
|
260
261
|
await issue_watch.record_baseline_here(issue_number)
|
|
261
262
|
|
|
262
|
-
|
|
263
|
+
const code = emit({ issue_number, hold, cost, verdict: reads.verdict, report: reads.report })
|
|
264
|
+
|
|
265
|
+
// The entry's own read — the split assessment every run answers before its work — printed here so
|
|
266
|
+
// the reader does not spend a turn fetching it.
|
|
267
|
+
if (code !== SUCCESS_EXIT_CODE) return code
|
|
268
|
+
|
|
269
|
+
return run_step_section.print_section(run_step_section.ENTRY)
|
|
263
270
|
}
|
|
264
271
|
|
|
265
272
|
async function claim_run(
|
|
@@ -29,6 +29,7 @@ const KIND_LABELS: Readonly<Record<EventKind, KindLabel>> = {
|
|
|
29
29
|
cut: { ja: 'セッション交代', en: 'session cut' },
|
|
30
30
|
resume: { ja: '実装を再開', en: 'implementation resumed' },
|
|
31
31
|
restart: { ja: '止めて出し直し', en: 'restarted' },
|
|
32
|
+
withdraw: { ja: '取り下げ', en: 'withdrawn' },
|
|
32
33
|
drain: { ja: 'バックログが空', en: 'backlog drained' },
|
|
33
34
|
stop: { ja: '停止', en: 'stopped' },
|
|
34
35
|
'pr-opened': { ja: 'PR 作成', en: 'PR opened' },
|
|
@@ -94,6 +94,7 @@ const KIND = run_event_stream.EVENT_KIND
|
|
|
94
94
|
const BATCH_RECORD_KINDS: ReadonlySet<string> = new Set([
|
|
95
95
|
KIND.CHILD_LAUNCH,
|
|
96
96
|
KIND.RESTART,
|
|
97
|
+
KIND.WITHDRAW,
|
|
97
98
|
KIND.PARK,
|
|
98
99
|
KIND.SPLIT,
|
|
99
100
|
KIND.RETROSPECTIVE,
|
|
@@ -167,12 +168,14 @@ function last_issue_event(
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
// A child's launch and the endings `run:merge` settles it with — the ones `run:board` stops drawing it
|
|
170
|
-
// running on. A park or a split already ran `run:merge`, so a child closed after one owes nothing
|
|
171
|
+
// running on. A park or a split already ran `run:merge`, so a child closed after one owes nothing; a
|
|
172
|
+
// withdrawal settles the child itself, its issue closed by the person who withdrew it.
|
|
171
173
|
const LAUNCH_OR_SETTLE_KINDS: ReadonlySet<string> = new Set([
|
|
172
174
|
KIND.CHILD_LAUNCH,
|
|
173
175
|
KIND.MERGE,
|
|
174
176
|
KIND.PARK,
|
|
175
177
|
KIND.SPLIT,
|
|
178
|
+
KIND.WITHDRAW,
|
|
176
179
|
])
|
|
177
180
|
|
|
178
181
|
// Whether this invocation's newest launch of the issue has not yet been settled.
|
|
@@ -186,9 +189,24 @@ function is_merge_owed(events: ReadonlyArray<RunEvent>, scope: EventScope, issue
|
|
|
186
189
|
return newest?.kind === KIND.CHILD_LAUNCH
|
|
187
190
|
}
|
|
188
191
|
|
|
192
|
+
function is_launch_or_withdrawal(event: RunEvent, issue: string): boolean {
|
|
193
|
+
const is_kind = event.kind === KIND.CHILD_LAUNCH || event.kind === KIND.WITHDRAW
|
|
194
|
+
|
|
195
|
+
return is_kind && names_issue(event, issue)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Whether a person withdrew the issue's newest launch (`lane:withdraw`): its newest launch or withdrawal
|
|
199
|
+
// is a withdrawal. A launch after it is a fresh attempt, which the old withdrawal does not settle.
|
|
200
|
+
function is_withdrawn(events: ReadonlyArray<RunEvent>, issue: string): boolean {
|
|
201
|
+
const newest = events.findLast((event) => is_launch_or_withdrawal(event, issue))
|
|
202
|
+
|
|
203
|
+
return newest?.kind === KIND.WITHDRAW
|
|
204
|
+
}
|
|
205
|
+
|
|
189
206
|
const run_event_scope = {
|
|
190
207
|
UNKNOWN_EVENT_SCOPE,
|
|
191
208
|
is_merge_owed,
|
|
209
|
+
is_withdrawn,
|
|
192
210
|
issue_named,
|
|
193
211
|
last_issue_event,
|
|
194
212
|
last_scoped_event,
|
|
@@ -113,10 +113,9 @@ async function emit_heartbeat(line: string): Promise<void> {
|
|
|
113
113
|
await emit(run_event_stream.EVENT_KIND.HEARTBEAT, line)
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
// The events of the invocation now running,
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
async function current_events(): Promise<ReadonlyArray<RunEvent>> {
|
|
116
|
+
// The events of the invocation now running, `undefined` where the invocation's scope could not be told
|
|
117
|
+
// — for a reader that must not take "no events" for "nothing launched" (`backlog-occupied.ts`).
|
|
118
|
+
async function scoped_current_events(): Promise<ReadonlyArray<RunEvent> | undefined> {
|
|
120
119
|
try {
|
|
121
120
|
const repository = await run_carry.repository_directory()
|
|
122
121
|
|
|
@@ -126,12 +125,19 @@ async function current_events(): Promise<ReadonlyArray<RunEvent>> {
|
|
|
126
125
|
const scope = run_event_scope.scope_of(read)
|
|
127
126
|
const events = run_event_stream.read_events(run_event_stream.target_of(repository))
|
|
128
127
|
|
|
129
|
-
return run_event_scope.scoped_events(events, scope)
|
|
128
|
+
return run_event_scope.scoped_events(events, scope)
|
|
130
129
|
} catch {
|
|
131
|
-
return
|
|
130
|
+
return undefined
|
|
132
131
|
}
|
|
133
132
|
}
|
|
134
133
|
|
|
134
|
+
// The events of the invocation now running, for a reader restoring the run from its stream
|
|
135
|
+
// (`backlog:drive`). Fail-quiet like the writers: no repository, an unreadable stream or an
|
|
136
|
+
// undetermined scope is an empty invocation.
|
|
137
|
+
async function current_events(): Promise<ReadonlyArray<RunEvent>> {
|
|
138
|
+
return (await scoped_current_events()) ?? []
|
|
139
|
+
}
|
|
140
|
+
|
|
135
141
|
// The invocation's events for a writer deciding whether a marker is already there: unlike
|
|
136
142
|
// `current_events`, an undetermined scope keeps the whole stream, so a run with no carry record skips a
|
|
137
143
|
// marker it already wrote rather than writing it twice.
|
|
@@ -167,6 +173,7 @@ const run_event_stream_emit = {
|
|
|
167
173
|
emit_once,
|
|
168
174
|
emit_once_since,
|
|
169
175
|
invocation_or_all_events,
|
|
176
|
+
scoped_current_events,
|
|
170
177
|
stream_target,
|
|
171
178
|
}
|
|
172
179
|
|
|
@@ -64,6 +64,10 @@ const EVENT_KIND = {
|
|
|
64
64
|
// (`lane:restart`). It follows the fresh dispatch's `CHILD_LAUNCH`, so it is a launch to the parent and
|
|
65
65
|
// the board, and — like that launch — a batch record a lane child never reads as its own position.
|
|
66
66
|
RESTART: 'restart',
|
|
67
|
+
// A person withdrew a lane child — stopped it and closed its issue as not planned (`lane:withdraw`).
|
|
68
|
+
// It is written before the stop, so `run:merge` settles the child as withdrawn whatever GitHub says
|
|
69
|
+
// by the time the parent collects it, and counts it as neither a merge, a failure nor an outage.
|
|
70
|
+
WITHDRAW: 'withdraw',
|
|
67
71
|
// The backlog emptied while nothing of the run's own was in flight — the drain. It marks the position
|
|
68
72
|
// at which the end-of-run retrospective is owed, *before* the idle watch opens, so `run:step` fires
|
|
69
73
|
// the retrospective at the drain rather than after the watch runs its course. It is not itself a
|
|
@@ -176,14 +176,25 @@ function held_block(hold: RunHold, issue: string): string | undefined {
|
|
|
176
176
|
return run_hold.is_reentry(hold, issue) ? undefined : run_hold.held_message(hold)
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// An expired record of the claimant's own issue that no stop marked is the earlier run of the same work —
|
|
180
|
+
// a child killed and re-dispatched into its lane — so the claim adopts it with the work it left. A
|
|
181
|
+
// stopped `halfrun` or `prrun` keeps its own adopt path.
|
|
182
|
+
function is_own_unstopped(hold: RunHold, issue: string): boolean {
|
|
183
|
+
return run_hold.is_reentry(hold, issue) && !run_hold.is_stop_marked(hold)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function stale_block(hold: RunHold, issue: string): Promise<string | undefined> {
|
|
187
|
+
if (is_own_unstopped(hold, issue)) return undefined
|
|
188
|
+
|
|
189
|
+
return (await run_hold.is_tree_dirty()) ? run_hold.uncommitted_message(hold) : undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
179
192
|
async function blocking_message(read: HoldRead, issue: string): Promise<string | undefined> {
|
|
180
193
|
if (read.kind === 'held') return held_block(read.hold, issue)
|
|
181
194
|
|
|
182
195
|
if (read.kind === 'unreadable') return run_hold.unreadable_message()
|
|
183
196
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return is_expired_over_work ? run_hold.uncommitted_message(read.hold) : undefined
|
|
197
|
+
return read.kind === 'stale' ? await stale_block(read.hold, issue) : undefined
|
|
187
198
|
}
|
|
188
199
|
|
|
189
200
|
// A record already here is never overwritten by a claim: overwriting is what the guard exists to
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
2
|
+
import { run_halfrun_resume } from '#scripts/run/run-halfrun-resume'
|
|
3
|
+
import { run_hold, type RunHold, type SessionOwner } from './run-hold'
|
|
4
|
+
|
|
5
|
+
// What the `Stop` hook asks of a hold beyond "is one in place" (joshuafolkken/kit#3961): whether it is
|
|
6
|
+
// this session's own run's, and whether its pause was already announced.
|
|
7
|
+
//
|
|
8
|
+
// **Another run's hold is not this session's pause.** A `kickoff`, or a question answered while a
|
|
9
|
+
// `backlogrun` parent holds the same tree, stops in a session that holds nothing — announcing that stop
|
|
10
|
+
// as the held run's "confirmation required" reports a decision nobody is waiting on.
|
|
11
|
+
//
|
|
12
|
+
// **Unknown is this session's own.** A record written outside an agent session, or a hook that cannot
|
|
13
|
+
// read its own session, keeps the behavior the hold had before ownership was recorded: a missed notify
|
|
14
|
+
// costs a person a silent pause, a wrong one only a needless Telegram.
|
|
15
|
+
|
|
16
|
+
function is_same_start(recorded: string | undefined, own: string | undefined): boolean {
|
|
17
|
+
return recorded === undefined || own === undefined || recorded === own
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function is_session_hold(hold: RunHold, owner: SessionOwner = run_hold.session_owner()): boolean {
|
|
21
|
+
if (hold.owner_pid === undefined || owner.owner_pid === undefined) return true
|
|
22
|
+
|
|
23
|
+
return hold.owner_pid === owner.owner_pid && is_same_start(hold.owner_start, owner.owner_start)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// **A pause is announced once, and a person's prompt ends it.** The hook's own send is recorded on the
|
|
27
|
+
// hold, so a later stop of the same pause — a background notice that woke the session — finds it; a send before `pause_start` belongs to an earlier pause. An unknown start
|
|
28
|
+
// announces again: a needless Telegram costs less than a silent wait.
|
|
29
|
+
function is_notified_since(hold: RunHold, pause_start: string | undefined): boolean {
|
|
30
|
+
if (pause_start === undefined || hold.notified_at === undefined) return false
|
|
31
|
+
|
|
32
|
+
return Date.parse(hold.notified_at) >= Date.parse(pause_start)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Stamps the send on the record in place; a tree no longer held has nothing to stamp. A stale record is
|
|
36
|
+
// stamped too: the hook reads it as the hold (`hold_of`), so a run past the expiry age still finds its send.
|
|
37
|
+
function mark_notified(target: string, now: Date = new Date()): void {
|
|
38
|
+
const hold = run_halfrun_resume.hold_of(run_hold.read_hold(target, now))
|
|
39
|
+
if (hold === undefined) return
|
|
40
|
+
|
|
41
|
+
stamp_file.write_stamp(target, { ...hold, notified_at: now.toISOString() })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const run_hold_session = { is_notified_since, is_session_hold, mark_notified }
|
|
45
|
+
|
|
46
|
+
export { run_hold_session }
|
|
@@ -65,6 +65,10 @@ interface RunHold {
|
|
|
65
65
|
// fields, or by a claim made outside an agent session.
|
|
66
66
|
owner_pid?: number | undefined
|
|
67
67
|
owner_start?: string | undefined
|
|
68
|
+
// When the `Stop` hook last sent this run's `confirmation` Telegram — its own send leaves no command
|
|
69
|
+
// on the transcript, so the record is what keeps one pause from being announced twice
|
|
70
|
+
// (`run-hold-session.ts`).
|
|
71
|
+
notified_at?: string | undefined
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
type HoldRead =
|
|
@@ -107,6 +111,7 @@ const run_hold_schema = z.object({
|
|
|
107
111
|
prrun_stop_head: z.string().optional(),
|
|
108
112
|
owner_pid: z.number().optional(),
|
|
109
113
|
owner_start: z.string().optional(),
|
|
114
|
+
notified_at: z.string().optional(),
|
|
110
115
|
})
|
|
111
116
|
|
|
112
117
|
// `undefined` for anything that is not a well-formed record, so the caller decides what a broken one
|
|
@@ -129,10 +134,12 @@ function is_expired(hold: RunHold, now: Date): boolean {
|
|
|
129
134
|
// nothing. Only a recorded session that is provably gone counts — no owner, or a live pid whose start
|
|
130
135
|
// cannot be read, leaves the age as the whole test. **A stop-marked record is exempt**: a `halfrun` or
|
|
131
136
|
// `prrun` stop is held across a person's latency on purpose, long after its session ends.
|
|
132
|
-
function
|
|
133
|
-
|
|
137
|
+
function is_stop_marked(hold: RunHold): boolean {
|
|
138
|
+
return hold.is_halfrun_stop === true || hold.prrun_stop_head !== undefined
|
|
139
|
+
}
|
|
134
140
|
|
|
135
|
-
|
|
141
|
+
function is_owner_gone(hold: RunHold): boolean {
|
|
142
|
+
if (is_stop_marked(hold) || hold.owner_pid === undefined) return false
|
|
136
143
|
|
|
137
144
|
return process_identity.is_same_process(hold.owner_pid, hold.owner_start) === false
|
|
138
145
|
}
|
|
@@ -317,6 +324,7 @@ function unknown_message(): string {
|
|
|
317
324
|
}
|
|
318
325
|
|
|
319
326
|
const run_hold = {
|
|
327
|
+
is_stop_marked,
|
|
320
328
|
FORCE_RELEASE_COMMAND,
|
|
321
329
|
HOLD_MAX_AGE_HOURS,
|
|
322
330
|
HOLD_MAX_AGE_MS,
|
|
@@ -339,6 +347,7 @@ const run_hold = {
|
|
|
339
347
|
race_message,
|
|
340
348
|
read_hold,
|
|
341
349
|
release_hold,
|
|
350
|
+
session_owner,
|
|
342
351
|
stale_message,
|
|
343
352
|
uncommitted_message,
|
|
344
353
|
unknown_message,
|
|
@@ -347,5 +356,5 @@ const run_hold = {
|
|
|
347
356
|
write_hold,
|
|
348
357
|
}
|
|
349
358
|
|
|
350
|
-
export type { HoldRead, RunHold, StopMarkFields }
|
|
359
|
+
export type { HoldRead, RunHold, SessionOwner, StopMarkFields }
|
|
351
360
|
export { run_hold }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { setPriority } from 'node:os'
|
|
2
|
+
|
|
3
|
+
// **A launched session runs below the interactive one, so a saturated machine still answers the
|
|
4
|
+
// user.** Every headless session and everything it starts — gates, tests, builds — used to share the
|
|
5
|
+
// interactive session's priority, and at a load of ~30 the lanes took the terminal's responsiveness
|
|
6
|
+
// with them (#3947). The niceness is set on the child right after `spawn` returns: a process inherits
|
|
7
|
+
// its parent's niceness at fork, and an agent CLI boots for far longer than this takes before it forks
|
|
8
|
+
// anything, so every descendant inherits it. Efficiency-core confinement (`taskpolicy -b`) is not
|
|
9
|
+
// used — it throttles a lane far beyond yielding to the user.
|
|
10
|
+
const LANE_NICENESS = 10
|
|
11
|
+
|
|
12
|
+
type SetPriority = (pid: number, priority: number) => void
|
|
13
|
+
|
|
14
|
+
// **A priority that cannot be lowered starts the session as before.** A platform without the call, or
|
|
15
|
+
// a child that already runs at a lower priority than this, is not a reason to fail the launch.
|
|
16
|
+
function lower(pid: number | undefined, set_priority: SetPriority = setPriority): void {
|
|
17
|
+
if (pid === undefined) return
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
set_priority(pid, LANE_NICENESS)
|
|
21
|
+
} catch {
|
|
22
|
+
// the session keeps the priority it was started with
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const launch_priority = {
|
|
27
|
+
LANE_NICENESS,
|
|
28
|
+
lower,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type { SetPriority }
|
|
32
|
+
export { launch_priority }
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { api_outage } from '#scripts/agent/api-outage'
|
|
4
3
|
import { CONTEXT_CUT_THRESHOLD } from '#scripts/cost-runtime/context-cut-threshold'
|
|
5
4
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
6
|
-
import { issue_closing_pr } from '#scripts/issue/issue-closing-pr'
|
|
7
5
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
8
6
|
import { issue_state_cli } from '#scripts/issue/issue-state-cli'
|
|
9
7
|
import { lane_handoff } from '#scripts/lane/lane-handoff'
|
|
@@ -12,9 +10,9 @@ import { cli_flags } from '#scripts/lib/cli-flags'
|
|
|
12
10
|
import { run_carry, type CarryOwner, type RunCarry } from '#scripts/run/carry/run-carry'
|
|
13
11
|
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
14
12
|
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
15
|
-
import { run_ending } from '#scripts/run/run-ending'
|
|
16
13
|
import { run_label } from '#scripts/run/run-label'
|
|
17
|
-
import { run_merge, type ChildOutcome
|
|
14
|
+
import { run_merge, type ChildOutcome } from './run-merge'
|
|
15
|
+
import { run_merge_signals } from './run-merge-signals'
|
|
18
16
|
import { run_merge_steps, type FailedResult, type MergeContext } from './run-merge-steps'
|
|
19
17
|
import { run_merge_token, type MergeToken } from './run-merge-token'
|
|
20
18
|
|
|
@@ -189,40 +187,6 @@ async function offered(ctx: MergeContext): Promise<OfferVerdict> {
|
|
|
189
187
|
return { token: await run_merge_steps.ask_next(ctx), code: SUCCESS_EXIT_CODE, is_offer: true }
|
|
190
188
|
}
|
|
191
189
|
|
|
192
|
-
// Whether the child's exit record shows it could not reach the API — read only when `--output` named
|
|
193
|
-
// the transcript, off otherwise. The path is validated inside
|
|
194
|
-
// `run_ending.read_exit`, which returns `undefined` for an unreadable or missing record, so a missing
|
|
195
|
-
// output is simply "not an outage".
|
|
196
|
-
function read_is_outage(output: string | undefined): boolean {
|
|
197
|
-
if (output === undefined) return false
|
|
198
|
-
|
|
199
|
-
return api_outage.is_outage(run_ending.read_exit(output))
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
type IssueRead = Awaited<ReturnType<typeof issue_state_cli.read_issue>>
|
|
203
|
-
|
|
204
|
-
// The timeline is read only for an OPEN, unparked child — the one classification a merged closing pull
|
|
205
|
-
// request changes. A CLOSED child was closed by GitHub, so it must carry no `merged_pr` for
|
|
206
|
-
// `do_merged` to close again.
|
|
207
|
-
async function read_merged_pr(child: string, read: IssueRead): Promise<string | undefined> {
|
|
208
|
-
if (read.kind !== 'state' || run_merge.classify_child(read.state) !== 'failed') return undefined
|
|
209
|
-
|
|
210
|
-
return await issue_closing_pr.read_closing_pr(child)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function read_signals(ctx: MergeContext, read: IssueRead): Promise<EndingSignals> {
|
|
214
|
-
const [is_cut, merged_pr] = await Promise.all([
|
|
215
|
-
run_merge_steps.has_resumable_cut(ctx.child),
|
|
216
|
-
read_merged_pr(ctx.child, read),
|
|
217
|
-
])
|
|
218
|
-
|
|
219
|
-
return { is_outage: read_is_outage(ctx.output), is_cut, merged_pr }
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function outcome_of(read: IssueRead, signals: EndingSignals): ChildOutcome {
|
|
223
|
-
return read.kind === 'state' ? run_merge.classify_child(read.state, signals) : 'unresolved'
|
|
224
|
-
}
|
|
225
|
-
|
|
226
190
|
// A refused count means this session is not the carry record's owner, so the whole operation is
|
|
227
191
|
// rejected — the lane is not closed and no next child is offered.
|
|
228
192
|
function report_count_refused(carry: RunCarry | undefined): MergeVerdict {
|
|
@@ -374,6 +338,13 @@ async function on_skipped(ctx: MergeContext): Promise<MergeVerdict> {
|
|
|
374
338
|
return await offered(ctx)
|
|
375
339
|
}
|
|
376
340
|
|
|
341
|
+
// A child a person withdrew with `lane:withdraw`: the withdrawal already stopped it, closed its issue
|
|
342
|
+
// and lane and put itself on the stream, so nothing is counted or written here and its seat goes to the
|
|
343
|
+
// next child.
|
|
344
|
+
async function on_withdrawn(ctx: MergeContext): Promise<MergeVerdict> {
|
|
345
|
+
return await offered(ctx)
|
|
346
|
+
}
|
|
347
|
+
|
|
377
348
|
// The child's own ending: it stopped before its commit for a person to look at, so nothing is counted
|
|
378
349
|
// and no next child is offered.
|
|
379
350
|
async function on_human_review(): Promise<MergeVerdict> {
|
|
@@ -399,6 +370,7 @@ const HANDLERS: Readonly<Record<ChildOutcome, (ctx: MergeContext) => Promise<Mer
|
|
|
399
370
|
split: on_skipped,
|
|
400
371
|
unresolved: on_unresolved,
|
|
401
372
|
shipping: on_shipping,
|
|
373
|
+
withdrawn: on_withdrawn,
|
|
402
374
|
}
|
|
403
375
|
|
|
404
376
|
// The whole merge event for one returned child, in-process: read its state, classify it, and run the
|
|
@@ -409,10 +381,12 @@ async function merge_child(ctx: MergeContext): Promise<MergeResult> {
|
|
|
409
381
|
}
|
|
410
382
|
|
|
411
383
|
const read = await issue_state_cli.read_issue(ctx.child, ctx.repo)
|
|
412
|
-
const signals = await read_signals(ctx, read)
|
|
413
|
-
const outcome = outcome_of(read, signals)
|
|
384
|
+
const signals = await run_merge_signals.read_signals(ctx, read)
|
|
385
|
+
const outcome = run_merge_signals.outcome_of(read, signals)
|
|
386
|
+
|
|
387
|
+
const { merged_pr, is_unanswered } = signals
|
|
414
388
|
|
|
415
|
-
return { outcome, ...(await HANDLERS[outcome]({ ...ctx, merged_pr
|
|
389
|
+
return { outcome, ...(await HANDLERS[outcome]({ ...ctx, merged_pr, is_unanswered })) }
|
|
416
390
|
}
|
|
417
391
|
|
|
418
392
|
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
@@ -438,7 +412,7 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
|
438
412
|
const run_merge_cli = {
|
|
439
413
|
merge_child,
|
|
440
414
|
parse,
|
|
441
|
-
read_merged_pr,
|
|
415
|
+
read_merged_pr: run_merge_signals.read_merged_pr,
|
|
442
416
|
record_merged,
|
|
443
417
|
run,
|
|
444
418
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
2
|
+
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
3
|
+
import { lane_relaunch } from '#scripts/lane/lane-relaunch'
|
|
2
4
|
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
3
5
|
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
6
|
+
import { run_ending } from '#scripts/run/run-ending'
|
|
4
7
|
import { run_ship_detach } from '#scripts/run/ship/run-ship-detach'
|
|
5
8
|
|
|
6
9
|
// Why `run:merge` parks a child whose detached ship has already ended (joshuafolkken/kit#3808).
|
|
@@ -56,6 +59,61 @@ function cause(child: string, directory: string = process.cwd()): string | undef
|
|
|
56
59
|
return cause_of(run_ship_detach.read_result(repository, child), events, child)
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
// Whether the ended ship was never answered: a session relaunched to fix a stop writes to the
|
|
63
|
+
// transcript after it, so a transcript last written before the newest stop means no fix-up ran — its
|
|
64
|
+
// broker or relaunch could not start. A ship with no stop at all ended abnormally only when its supervisor
|
|
65
|
+
// died before reaching a stage.
|
|
66
|
+
function is_stop_unanswered(
|
|
67
|
+
ending: ShipEnding,
|
|
68
|
+
attempt: Events,
|
|
69
|
+
written_at: number | undefined,
|
|
70
|
+
): boolean {
|
|
71
|
+
const stop = attempt.findLast((event) => event.kind === run_event_stream.EVENT_KIND.SHIP_STOP)
|
|
72
|
+
|
|
73
|
+
if (stop === undefined) return ending?.result === 'abnormal'
|
|
74
|
+
|
|
75
|
+
return written_at !== undefined && written_at < Date.parse(stop.at)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Whether the child's ship ended through an interruption rather than its work, so the child is
|
|
79
|
+
// re-dispatched instead of parked.
|
|
80
|
+
function interrupted_of(
|
|
81
|
+
ending: ShipEnding,
|
|
82
|
+
events: Events,
|
|
83
|
+
child: string,
|
|
84
|
+
written_at: number | undefined,
|
|
85
|
+
): boolean {
|
|
86
|
+
if (ending === undefined || !ENDED_RESULTS.has(ending.result)) return false
|
|
87
|
+
|
|
88
|
+
const attempt = since_ship_launch(events, child)
|
|
89
|
+
|
|
90
|
+
return attempt !== undefined && is_stop_unanswered(ending, attempt, written_at)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function is_interrupted(
|
|
94
|
+
child: string,
|
|
95
|
+
written_at: number | undefined,
|
|
96
|
+
directory: string = process.cwd(),
|
|
97
|
+
): boolean {
|
|
98
|
+
const repository = git_common_directory.repository(directory)
|
|
99
|
+
if (repository === undefined) return false
|
|
100
|
+
|
|
101
|
+
const events = run_event_stream.read_events(run_event_stream.target_of(repository))
|
|
102
|
+
|
|
103
|
+
return interrupted_of(run_ship_detach.read_result(repository, child), events, child, written_at)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Whether the child's ship stopped and the relaunch meant to fix it never ran, read against its
|
|
107
|
+
// transcript at `output`. An OpenAI lane is never relaunched — its parent books every stop as an ended
|
|
108
|
+
// child — and nothing relaunches a child whose lane is gone, so only an open non-OpenAI lane reads as one.
|
|
109
|
+
async function is_unanswered(child: string, output: string | undefined): Promise<boolean> {
|
|
110
|
+
if (output === undefined || !is_interrupted(child, run_ending.written_at(output))) return false
|
|
111
|
+
|
|
112
|
+
const lane = await lane_registry.find_open_lane(child)
|
|
113
|
+
|
|
114
|
+
return lane !== undefined && !lane_relaunch.is_openai_lane(lane)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const run_merge_ship = { cause, cause_of, interrupted_of, is_unanswered }
|
|
60
118
|
|
|
61
119
|
export { run_merge_ship }
|