@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
|
@@ -36,9 +36,16 @@ const KIND = {
|
|
|
36
36
|
DISPATCH: 'dispatch',
|
|
37
37
|
STAGE: 'stage',
|
|
38
38
|
STOP: 'stop',
|
|
39
|
+
OVERLAP: 'overlap',
|
|
39
40
|
} as const
|
|
40
41
|
|
|
41
42
|
const merge_schema = z.object({ kind: z.literal(KIND.MERGE), at: z.string(), issue: z.number() })
|
|
43
|
+
const check_schema = z.object({
|
|
44
|
+
label: z.string(),
|
|
45
|
+
elapsed_ms: z.number(),
|
|
46
|
+
is_passed: z.boolean(),
|
|
47
|
+
is_reused: z.boolean().optional(),
|
|
48
|
+
})
|
|
42
49
|
const gate_schema = z.object({
|
|
43
50
|
kind: z.literal(KIND.GATE),
|
|
44
51
|
at: z.string(),
|
|
@@ -50,12 +57,21 @@ const gate_schema = z.object({
|
|
|
50
57
|
// The whole cores busy outside this gate, beyond the machine's baseline, at its start or its end —
|
|
51
58
|
// whichever was higher. Absent when the machine could not be read.
|
|
52
59
|
external_cores: z.number().optional(),
|
|
60
|
+
// Each check's own duration and verdict, so `josh lane:stats` can name the check a slow gate waited
|
|
61
|
+
// on. A reused check started no process: its row says so, and its zero is no timing.
|
|
62
|
+
checks: z.array(check_schema).optional(),
|
|
53
63
|
})
|
|
54
64
|
// `available_mb`, `swapped_mb` and `lanes` are optional because each read can fail on its own — a
|
|
55
65
|
// platform with no memory reading, a `git worktree` call that did not answer — and a sample is still
|
|
56
66
|
// worth its load. `swapped_mb` is `machine_capacity`'s counter, every page swapped since boot.
|
|
57
67
|
// Both memory fields were renamed with their meaning (joshuafolkken/kit#3593): an older row's
|
|
58
68
|
// `free_mb` / `swap_mb` are stripped on read, so the two measurements never share a column.
|
|
69
|
+
const process_kind_schema = z.object({
|
|
70
|
+
kind: z.string(),
|
|
71
|
+
cpu_percent: z.number(),
|
|
72
|
+
rss_mb: z.number(),
|
|
73
|
+
count: z.number(),
|
|
74
|
+
})
|
|
59
75
|
const load_schema = z.object({
|
|
60
76
|
kind: z.literal(KIND.LOAD),
|
|
61
77
|
at: z.string(),
|
|
@@ -63,6 +79,9 @@ const load_schema = z.object({
|
|
|
63
79
|
available_mb: z.number().optional(),
|
|
64
80
|
swapped_mb: z.number().optional(),
|
|
65
81
|
lanes: z.number().optional(),
|
|
82
|
+
// The heaviest process kinds at the instant of the sample (`lane-process-load.ts`), so the load the
|
|
83
|
+
// core budget's ledger does not explain can be read back by what made it (joshuafolkken/kit#3874).
|
|
84
|
+
processes: z.array(process_kind_schema).optional(),
|
|
66
85
|
})
|
|
67
86
|
// A lane child started on `issue` — the instant its implementation is timed from.
|
|
68
87
|
const dispatch_schema = z.object({
|
|
@@ -89,6 +108,18 @@ const stop_schema = z.object({
|
|
|
89
108
|
issue: z.number(),
|
|
90
109
|
reason: z.string(),
|
|
91
110
|
})
|
|
111
|
+
// A `josh ship` on `issue` whose actual change overlapped the open pull requests of `lanes` on `paths`
|
|
112
|
+
// before its sync: it waited `waited_ms` for their ships to end, and `is_cleared` is false where the
|
|
113
|
+
// wait ran out first (joshuafolkken/kit#3901).
|
|
114
|
+
const overlap_schema = z.object({
|
|
115
|
+
kind: z.literal(KIND.OVERLAP),
|
|
116
|
+
at: z.string(),
|
|
117
|
+
issue: z.number(),
|
|
118
|
+
lanes: z.array(z.number()),
|
|
119
|
+
paths: z.array(z.string()),
|
|
120
|
+
waited_ms: z.number(),
|
|
121
|
+
is_cleared: z.boolean(),
|
|
122
|
+
})
|
|
92
123
|
const entry_schema = z.discriminatedUnion('kind', [
|
|
93
124
|
merge_schema,
|
|
94
125
|
gate_schema,
|
|
@@ -96,6 +127,7 @@ const entry_schema = z.discriminatedUnion('kind', [
|
|
|
96
127
|
dispatch_schema,
|
|
97
128
|
stage_schema,
|
|
98
129
|
stop_schema,
|
|
130
|
+
overlap_schema,
|
|
99
131
|
])
|
|
100
132
|
|
|
101
133
|
type LedgerEntry = z.infer<typeof entry_schema>
|
|
@@ -105,10 +137,13 @@ type LoadEntry = z.infer<typeof load_schema>
|
|
|
105
137
|
type DispatchEntry = z.infer<typeof dispatch_schema>
|
|
106
138
|
type StageEntry = z.infer<typeof stage_schema>
|
|
107
139
|
type StopEntry = z.infer<typeof stop_schema>
|
|
140
|
+
type OverlapEntry = z.infer<typeof overlap_schema>
|
|
108
141
|
// What a finished gate reports; the ledger stamps the kind and the time.
|
|
109
142
|
type GateRecord = Omit<GateEntry, 'kind' | 'at'>
|
|
143
|
+
type GateCheckRecord = z.infer<typeof check_schema>
|
|
110
144
|
type StageRecord = Omit<StageEntry, 'kind' | 'at'>
|
|
111
145
|
type StopRecord = Omit<StopEntry, 'kind' | 'at'>
|
|
146
|
+
type OverlapRecord = Omit<OverlapEntry, 'kind' | 'at'>
|
|
112
147
|
|
|
113
148
|
function target_of(repository: string): string {
|
|
114
149
|
return stamp_file.stamp_path(LEDGER_PREFIX, repository, LEDGER_SUFFIX)
|
|
@@ -185,6 +220,10 @@ async function record_stop(stop: StopRecord, path?: string): Promise<void> {
|
|
|
185
220
|
await record({ kind: KIND.STOP, at: now_iso(), ...stop }, path)
|
|
186
221
|
}
|
|
187
222
|
|
|
223
|
+
async function record_overlap(overlap: OverlapRecord, path?: string): Promise<void> {
|
|
224
|
+
await record({ kind: KIND.OVERLAP, at: now_iso(), ...overlap }, path)
|
|
225
|
+
}
|
|
226
|
+
|
|
188
227
|
function parse_line(line: string): LedgerEntry | undefined {
|
|
189
228
|
return entry_schema.safeParse(json_value.parse_or_undefined(line)).data
|
|
190
229
|
}
|
|
@@ -215,11 +254,23 @@ const lane_ledger = {
|
|
|
215
254
|
record_dispatch,
|
|
216
255
|
record_gate,
|
|
217
256
|
record_merge,
|
|
257
|
+
record_overlap,
|
|
218
258
|
record_stage,
|
|
219
259
|
record_stop,
|
|
220
260
|
target,
|
|
221
261
|
target_of,
|
|
222
262
|
}
|
|
223
263
|
|
|
224
|
-
export type {
|
|
264
|
+
export type {
|
|
265
|
+
DispatchEntry,
|
|
266
|
+
GateCheckRecord,
|
|
267
|
+
GateEntry,
|
|
268
|
+
LedgerEntry,
|
|
269
|
+
LoadEntry,
|
|
270
|
+
MergeEntry,
|
|
271
|
+
OverlapEntry,
|
|
272
|
+
OverlapRecord,
|
|
273
|
+
StageEntry,
|
|
274
|
+
StopEntry,
|
|
275
|
+
}
|
|
225
276
|
export { lane_ledger }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { backlog_ready } from '#scripts/backlog/backlog-ready'
|
|
3
|
+
import { backlog_ready, type LaneUse } from '#scripts/backlog/backlog-ready'
|
|
4
4
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
5
|
import { lane_capacity, type LimitChoice } from './lane-capacity'
|
|
6
6
|
import { lane_limit_override } from './lane-limit-override'
|
|
@@ -37,13 +37,13 @@ interface LimitPorts {
|
|
|
37
37
|
read_override: () => Promise<number | undefined>
|
|
38
38
|
write_override: (limit: number | undefined) => Promise<boolean>
|
|
39
39
|
emit_raise: (text: string) => Promise<void>
|
|
40
|
-
|
|
40
|
+
lane_use: () => Promise<LaneUse>
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
interface LimitStatus {
|
|
44
44
|
limit: number
|
|
45
45
|
override: number | undefined
|
|
46
|
-
|
|
46
|
+
use: LaneUse
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const SHOW: LimitRequest = { kind: 'show' }
|
|
@@ -55,7 +55,7 @@ const DEFAULT_PORTS: LimitPorts = {
|
|
|
55
55
|
read_override: lane_limit_override.read_override,
|
|
56
56
|
write_override: lane_limit_override.write_override,
|
|
57
57
|
emit_raise: lane_limit_override.emit_raise,
|
|
58
|
-
|
|
58
|
+
lane_use: backlog_ready.lane_use,
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function refuse(message: string): number {
|
|
@@ -98,9 +98,10 @@ function limit_text(limit: number): string {
|
|
|
98
98
|
|
|
99
99
|
function status_line(status: LimitStatus): string {
|
|
100
100
|
const source = status.override === undefined ? lane_capacity.LANE_LIMIT_KEY : OVERRIDE_SOURCE
|
|
101
|
-
const
|
|
101
|
+
const { occupied, seated } = status.use
|
|
102
|
+
const free = lane_capacity.free_lanes(status.limit, occupied, seated)
|
|
102
103
|
|
|
103
|
-
return `lane limit ${limit_text(status.limit)} (${source}) · in use ${String(
|
|
104
|
+
return `lane limit ${limit_text(status.limit)} (${source}) · in use ${String(occupied)} · free ${String(free)}`
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
async function show(ports: LimitPorts): Promise<number> {
|
|
@@ -110,9 +111,7 @@ async function show(ports: LimitPorts): Promise<number> {
|
|
|
110
111
|
|
|
111
112
|
const override = await ports.read_override()
|
|
112
113
|
|
|
113
|
-
console.info(
|
|
114
|
-
status_line({ limit: choice.limit, override, in_use: await ports.live_lane_count() }),
|
|
115
|
-
)
|
|
114
|
+
console.info(status_line({ limit: choice.limit, override, use: await ports.lane_use() }))
|
|
116
115
|
|
|
117
116
|
return SUCCESS_EXIT_CODE
|
|
118
117
|
}
|
|
@@ -2,10 +2,12 @@ import { loadavg } from 'node:os'
|
|
|
2
2
|
import { machine_capacity } from '#scripts/gate/machine-capacity'
|
|
3
3
|
import { lane_await } from './lane-await'
|
|
4
4
|
import type { LoadEntry } from './lane-ledger'
|
|
5
|
+
import { lane_process_load } from './lane-process-load'
|
|
5
6
|
import { lane_registry, type LaneInfo } from './lane-registry'
|
|
6
7
|
|
|
7
8
|
// One machine-load sample for the lane-limit measurement: the one-minute load
|
|
8
|
-
// average, available memory, the swap counter
|
|
9
|
+
// average, available memory, the swap counter, how many lanes have a child working and the heaviest
|
|
10
|
+
// process kinds (`lane-process-load.ts`), read at one
|
|
9
11
|
// instant so `josh lane:stats` can set the load beside the lane count that produced it.
|
|
10
12
|
//
|
|
11
13
|
// **The memory is `machine_capacity`'s reading, the one the gate admits against.** `os.freemem()` reads
|
|
@@ -38,7 +40,11 @@ function rounded(megabytes: number | undefined): number | undefined {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
async function sample(at: string): Promise<LoadEntry> {
|
|
41
|
-
const [memory, lanes] = await Promise.all([
|
|
43
|
+
const [memory, lanes, processes] = await Promise.all([
|
|
44
|
+
machine_capacity.read_memory(),
|
|
45
|
+
read_lanes(),
|
|
46
|
+
lane_process_load.read(),
|
|
47
|
+
])
|
|
42
48
|
const [load = 0] = loadavg()
|
|
43
49
|
|
|
44
50
|
return {
|
|
@@ -48,6 +54,7 @@ async function sample(at: string): Promise<LoadEntry> {
|
|
|
48
54
|
available_mb: rounded(memory.available_mb),
|
|
49
55
|
swapped_mb: rounded(memory.swapped_mb),
|
|
50
56
|
lanes,
|
|
57
|
+
processes,
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -3,7 +3,9 @@ import { setInterval as every } from 'node:timers/promises'
|
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
5
|
import { lane_capacity } from './lane-capacity'
|
|
6
|
+
import { lane_check_stats } from './lane-check-stats'
|
|
6
7
|
import { lane_ledger } from './lane-ledger'
|
|
8
|
+
import { lane_process_stats } from './lane-process-stats'
|
|
7
9
|
import { lane_sampler, type MeasureContext } from './lane-sampler'
|
|
8
10
|
import { lane_stage_stats } from './lane-stage-stats'
|
|
9
11
|
import { lane_stats } from './lane-stats'
|
|
@@ -119,6 +121,8 @@ async function stats_command(
|
|
|
119
121
|
`${lane_stats.header()}\n${row}`,
|
|
120
122
|
lane_stage_stats.table(entries, window),
|
|
121
123
|
lane_stop_stats.table(entries, window),
|
|
124
|
+
lane_process_stats.table(entries, window),
|
|
125
|
+
lane_check_stats.table(entries, window),
|
|
122
126
|
].join('\n\n'),
|
|
123
127
|
)
|
|
124
128
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { has_label_name, NEEDS_DECISION_LABEL } from '#scripts/issue/issue-labels'
|
|
2
|
+
import { issue_state_cli } from '#scripts/issue/issue-state-cli'
|
|
3
|
+
|
|
4
|
+
// Whether an issue is parked for a person — the `needs-decision` label — read live, so a session a
|
|
5
|
+
// lane's broker would start for it is refused once the run has parked it (joshuafolkken/kit#3878).
|
|
6
|
+
//
|
|
7
|
+
// **An issue whose state cannot be read is not parked**: the read gates every brokered review, so
|
|
8
|
+
// reading an outage as a park would fail every review in flight for as long as GitHub is away.
|
|
9
|
+
|
|
10
|
+
async function is_parked(issue: string): Promise<boolean> {
|
|
11
|
+
const read = await issue_state_cli.read_issue(issue)
|
|
12
|
+
|
|
13
|
+
return read.kind === 'state' && has_label_name(read.state.labels, NEEDS_DECISION_LABEL)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const lane_parked = { is_parked }
|
|
17
|
+
|
|
18
|
+
export { lane_parked }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
|
|
2
2
|
import { git_stash } from '#scripts/git/stash/git-stash'
|
|
3
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
3
4
|
|
|
4
5
|
// Keeping a lane's uncommitted work before something discards or restarts it — `run:merge` closing a
|
|
5
6
|
// lane, `lane:restart` handing it to a fresh session. The stash stack outlives the tree, so the push is
|
|
@@ -30,6 +31,32 @@ async function announce(issue: string, message: string, cause: string): Promise<
|
|
|
30
31
|
await git_gh_issue_write.issue_try_comment(issue, preserved_comment(message, cause))
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
// The tree's work kept before a stopped child's lane is reused or closed: `saved` with the stash, or
|
|
35
|
+
// none for a clean tree, and `unsaved` with the reason when the tree could not be read or pushed — the
|
|
36
|
+
// caller then leaves the tree as it is rather than handing it on or deleting it.
|
|
37
|
+
type KeptWork =
|
|
38
|
+
| { kind: 'saved'; stash: string | undefined }
|
|
39
|
+
| { kind: 'unsaved'; directory: string; reason: string }
|
|
40
|
+
|
|
41
|
+
async function stash_of(
|
|
42
|
+
issue: string,
|
|
43
|
+
directory: string,
|
|
44
|
+
occasion: string,
|
|
45
|
+
): Promise<string | undefined> {
|
|
46
|
+
if (!(await git_stash.has_changes(directory))) return undefined
|
|
47
|
+
|
|
48
|
+
return await push_work(issue, directory, occasion)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Stash the tree's uncommitted work, if any, answering `unsaved` rather than throwing. */
|
|
52
|
+
async function keep_work(issue: string, directory: string, occasion: string): Promise<KeptWork> {
|
|
53
|
+
try {
|
|
54
|
+
return { kind: 'saved', stash: await stash_of(issue, directory, occasion) }
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return { kind: 'unsaved', directory, reason: error_text.message_of(error) }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
/** `push_work`, then `announce` — for a caller with no reason to separate the two. */
|
|
34
61
|
async function stash_work(
|
|
35
62
|
issue: string,
|
|
@@ -44,6 +71,7 @@ async function stash_work(
|
|
|
44
71
|
return message
|
|
45
72
|
}
|
|
46
73
|
|
|
47
|
-
const lane_preserve = { announce,
|
|
74
|
+
const lane_preserve = { announce, keep_work, stash_work }
|
|
48
75
|
|
|
76
|
+
export type { KeptWork }
|
|
49
77
|
export { lane_preserve }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
|
|
4
|
+
// What the machine's load is made of at one instant: every process grouped by the kind of tool it
|
|
5
|
+
// runs, the heaviest kinds kept with their CPU and resident size, for a `load` sample to carry.
|
|
6
|
+
//
|
|
7
|
+
// **Why a load sample needs it.** The gate's static checks and unit suite are on the core budget's
|
|
8
|
+
// ledger, so the load the ledger does not explain comes from somewhere else — review subagents, the
|
|
9
|
+
// sessions themselves, a hook's unit run, an IDE — and a load average alone cannot say which.
|
|
10
|
+
// joshuafolkken/kit#3874 measured load 93 with 1.6 lanes working and nothing on record to explain it.
|
|
11
|
+
//
|
|
12
|
+
// **The kind is read from the command line, not the executable name.** Every tool here is a `node`
|
|
13
|
+
// process, so the name says nothing; the first pattern a command line matches names its kind, in the
|
|
14
|
+
// order below, so `pnpm exec vitest` is `vitest` rather than `josh`. The IDE comes first: its language
|
|
15
|
+
// servers run out of a `typescript` or `eslint` directory, and read as `tsc` or `eslint` they would
|
|
16
|
+
// pass the IDE's load off as the gate's own checks — and a server the IDE starts on a plain `node`
|
|
17
|
+
// runtime is known only by its `.vscode/extensions/` path. `claude` is the executable or its package,
|
|
18
|
+
// never a `.claude/` path a session's Bash-tool shell or hook script runs through. `pnpm` is the
|
|
19
|
+
// command, never the `.pnpm/` store a resolved binary's path runs through.
|
|
20
|
+
//
|
|
21
|
+
// **`%cpu` is the share of one core**, so 300 is three cores busy; it is `ps`'s decaying average,
|
|
22
|
+
// which is what a one-minute sample beside the one-minute load average wants.
|
|
23
|
+
|
|
24
|
+
const PS_ARGUMENTS = ['-A', '-o', 'rss=,%cpu=,args=']
|
|
25
|
+
// `ps` prints `%cpu` with the locale's decimal separator, and a `0,7` would parse as NaN.
|
|
26
|
+
const PS_ENVIRONMENT = { LC_ALL: 'C' }
|
|
27
|
+
const FIELD_SEPARATOR = /\s+/u
|
|
28
|
+
const KB_PER_MB = 1024
|
|
29
|
+
// Enough to name what dominates and the runner-up behind it, short enough for one ledger line a minute.
|
|
30
|
+
const TOP_KINDS = 5
|
|
31
|
+
const OTHER_KIND = 'other'
|
|
32
|
+
|
|
33
|
+
const KIND_PATTERNS: ReadonlyArray<readonly [string, RegExp]> = [
|
|
34
|
+
['vscode', /Visual Studio Code|Code Helper|\.vscode\/extensions\//u],
|
|
35
|
+
['vitest', /vitest/u],
|
|
36
|
+
['eslint', /eslint/u],
|
|
37
|
+
['tsc', /\btsc\b|svelte-check|typescript/u],
|
|
38
|
+
['cspell', /cspell/u],
|
|
39
|
+
['prettier', /prettier/u],
|
|
40
|
+
['playwright', /playwright/u],
|
|
41
|
+
['chrome', /Google Chrome/u],
|
|
42
|
+
['chromium', /chromium|headless_shell/iu],
|
|
43
|
+
['claude', /(?:^|\/)claude(?:\s|$)|@anthropic-ai\/claude-code\//u],
|
|
44
|
+
['josh', /josh\.ts|\btsx\b|(?<!\.)\bpnpm\b/u],
|
|
45
|
+
['node', /\bnode\b/u],
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
interface ProcessKind {
|
|
49
|
+
kind: string
|
|
50
|
+
cpu_percent: number
|
|
51
|
+
rss_mb: number
|
|
52
|
+
count: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function kind_of(command_line: string): string {
|
|
56
|
+
return KIND_PATTERNS.find(([, pattern]) => pattern.test(command_line))?.[0] ?? OTHER_KIND
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// One line of `ps`'s listing as its kind and readings; a header or a cut-short line reads as nothing.
|
|
60
|
+
function process_of(line: string): Omit<ProcessKind, 'count'> | undefined {
|
|
61
|
+
const [rss, cpu, ...command] = line.trim().split(FIELD_SEPARATOR)
|
|
62
|
+
const reading = { cpu_percent: Number(cpu), rss_mb: Number(rss) / KB_PER_MB }
|
|
63
|
+
|
|
64
|
+
if (command.length === 0 || Number.isNaN(reading.cpu_percent + reading.rss_mb)) return undefined
|
|
65
|
+
|
|
66
|
+
return { kind: kind_of(command.join(' ')), ...reading }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function add_line(totals: Map<string, ProcessKind>, line: string): void {
|
|
70
|
+
const reading = process_of(line)
|
|
71
|
+
|
|
72
|
+
if (reading === undefined) return
|
|
73
|
+
|
|
74
|
+
const { kind } = reading
|
|
75
|
+
const total = totals.get(kind) ?? { kind, cpu_percent: 0, rss_mb: 0, count: 0 }
|
|
76
|
+
|
|
77
|
+
total.cpu_percent += reading.cpu_percent
|
|
78
|
+
total.rss_mb += reading.rss_mb
|
|
79
|
+
total.count += 1
|
|
80
|
+
totals.set(kind, total)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function rounded(total: ProcessKind): ProcessKind {
|
|
84
|
+
return { ...total, cpu_percent: Math.round(total.cpu_percent), rss_mb: Math.round(total.rss_mb) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The heaviest kinds first: by CPU, the load average's own measure, then by memory for idle kinds.
|
|
88
|
+
function by_weight(left: ProcessKind, right: ProcessKind): number {
|
|
89
|
+
return right.cpu_percent - left.cpu_percent || right.rss_mb - left.rss_mb
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function summarize(listing: string): Array<ProcessKind> {
|
|
93
|
+
const totals = new Map<string, ProcessKind>()
|
|
94
|
+
|
|
95
|
+
for (const line of listing.split('\n')) add_line(totals, line)
|
|
96
|
+
|
|
97
|
+
return [...totals.values()]
|
|
98
|
+
.map((total) => rounded(total))
|
|
99
|
+
.toSorted(by_weight)
|
|
100
|
+
.slice(0, TOP_KINDS)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// `undefined` where `ps` did not answer, so the sample records no breakdown rather than an empty one.
|
|
104
|
+
async function read(): Promise<Array<ProcessKind> | undefined> {
|
|
105
|
+
try {
|
|
106
|
+
const { stdout } = await execa('ps', PS_ARGUMENTS, {
|
|
107
|
+
env: PS_ENVIRONMENT,
|
|
108
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return summarize(stdout)
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const lane_process_load = {
|
|
118
|
+
TOP_KINDS,
|
|
119
|
+
kind_of,
|
|
120
|
+
read,
|
|
121
|
+
summarize,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type { ProcessKind }
|
|
125
|
+
export { lane_process_load }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { LedgerEntry, LoadEntry } from './lane-ledger'
|
|
2
|
+
import type { ProcessKind } from './lane-process-load'
|
|
3
|
+
import { lane_stats, type StatsWindow } from './lane-stats'
|
|
4
|
+
|
|
5
|
+
// What the period's machine load was made of: the process kinds the load samples carried
|
|
6
|
+
// (`lane-process-load.ts`), one row per kind, printed by `josh lane:stats` under the stop table so the
|
|
7
|
+
// load the core budget's ledger does not explain is read back by what made it (joshuafolkken/kit#3874).
|
|
8
|
+
//
|
|
9
|
+
// **A kind is averaged over the samples it appears in**, never over every sample. A sample keeps only
|
|
10
|
+
// its heaviest kinds, so a kind absent from one was below them rather than idle, and a zero there would
|
|
11
|
+
// understate it; `samples` says how often it was among the heaviest, which is the weight to read it by.
|
|
12
|
+
//
|
|
13
|
+
// **CPU is in cores** — `ps`'s share of one core over a hundred — so the column reads against the load
|
|
14
|
+
// average and the machine's core count without a conversion.
|
|
15
|
+
|
|
16
|
+
const COLUMNS = [
|
|
17
|
+
'process',
|
|
18
|
+
'cpu mean (cores)',
|
|
19
|
+
'cpu peak (cores)',
|
|
20
|
+
'rss peak (GB)',
|
|
21
|
+
'samples',
|
|
22
|
+
] as const
|
|
23
|
+
const PERCENT_PER_CORE = 100
|
|
24
|
+
const MB_PER_GB = 1024
|
|
25
|
+
const DECIMALS = 1
|
|
26
|
+
|
|
27
|
+
interface KindRow {
|
|
28
|
+
kind: string
|
|
29
|
+
readings: Array<ProcessKind>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function is_load(entry: LedgerEntry): entry is LoadEntry {
|
|
33
|
+
return entry.kind === 'load'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function group(samples: ReadonlyArray<LoadEntry>): Array<KindRow> {
|
|
37
|
+
const rows = new Map<string, KindRow>()
|
|
38
|
+
const readings = samples.flatMap((sample) => sample.processes ?? [])
|
|
39
|
+
|
|
40
|
+
for (const reading of readings) {
|
|
41
|
+
const row = rows.get(reading.kind) ?? { kind: reading.kind, readings: [] }
|
|
42
|
+
|
|
43
|
+
row.readings.push(reading)
|
|
44
|
+
rows.set(reading.kind, row)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return [...rows.values()]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function cores(readings: ReadonlyArray<ProcessKind>): Array<number> {
|
|
51
|
+
return readings.map((reading) => reading.cpu_percent / PERCENT_PER_CORE)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A row has one reading at least, so its mean always exists.
|
|
55
|
+
function mean_cores(row: KindRow): number {
|
|
56
|
+
return lane_stats.mean(cores(row.readings)) ?? 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cell(value: number | undefined): string {
|
|
60
|
+
return value === undefined ? lane_stats.MISSING : value.toFixed(DECIMALS)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function kind_cells(row: KindRow): Array<string> {
|
|
64
|
+
const rss_peak = lane_stats.peak(row.readings.map((reading) => reading.rss_mb))
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
row.kind,
|
|
68
|
+
cell(mean_cores(row)),
|
|
69
|
+
cell(lane_stats.peak(cores(row.readings))),
|
|
70
|
+
cell(rss_peak === undefined ? undefined : rss_peak / MB_PER_GB),
|
|
71
|
+
String(row.readings.length),
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function by_mean_cpu(left: KindRow, right: KindRow): number {
|
|
76
|
+
return mean_cores(right) - mean_cores(left)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The period's table: `entries` is the whole ledger, filtered here to `window`. A period whose samples
|
|
80
|
+
// carried no breakdown prints the header alone.
|
|
81
|
+
function table(entries: ReadonlyArray<LedgerEntry>, window: StatsWindow): string {
|
|
82
|
+
const rows = group(lane_stats.in_window(entries, window).filter(is_load)).toSorted(by_mean_cpu)
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
lane_stats.to_row(COLUMNS),
|
|
86
|
+
lane_stats.to_row(COLUMNS.map(() => '---')),
|
|
87
|
+
...rows.map((row) => lane_stats.to_row(kind_cells(row))),
|
|
88
|
+
].join('\n')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lane_process_stats = { table }
|
|
92
|
+
|
|
93
|
+
export { lane_process_stats }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
1
|
+
import { existsSync, statSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { ENV_FILE_NAME } from '#ports'
|
|
4
4
|
import type { AgentProfile } from '#scripts/agent/agent-role-profile'
|
|
@@ -38,6 +38,9 @@ interface LaneInfo {
|
|
|
38
38
|
// instead of waiting for the pool to drain before it can be cut.
|
|
39
39
|
output: string | undefined
|
|
40
40
|
profile?: AgentProfile | undefined
|
|
41
|
+
// When the lane last took a child: `lane:open` writes its `.env`, and every dispatch records its
|
|
42
|
+
// unit there (`lane-output.ts`). `undefined` where the `.env` cannot be read.
|
|
43
|
+
dispatched_ms?: number | undefined
|
|
41
44
|
is_stranded: boolean
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -106,6 +109,10 @@ function read_environment(directory: string): string | undefined {
|
|
|
106
109
|
return file_reader.read_if_readable(path.join(directory, ENV_FILE_NAME))
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
function dispatched_at(directory: string): number | undefined {
|
|
113
|
+
return statSync(path.join(directory, ENV_FILE_NAME), { throwIfNoEntry: false })?.mtimeMs
|
|
114
|
+
}
|
|
115
|
+
|
|
109
116
|
// A lane is identified by its branch rather than by where it sits, so nothing here compares two
|
|
110
117
|
// spellings of one path — git prints the resolved one, and a lane root reached through a symlink
|
|
111
118
|
// would otherwise read as no lane at all.
|
|
@@ -151,6 +158,7 @@ function build_lane(
|
|
|
151
158
|
preview_port: held.preview_port,
|
|
152
159
|
output,
|
|
153
160
|
profile,
|
|
161
|
+
dispatched_ms: is_stranded ? undefined : dispatched_at(directory),
|
|
154
162
|
is_stranded,
|
|
155
163
|
}
|
|
156
164
|
}
|
|
@@ -175,9 +183,14 @@ function parse_block(block: string, root: string): LaneInfo | undefined {
|
|
|
175
183
|
const branch = line_value(lines, BRANCH_PREFIX)
|
|
176
184
|
const issue = branch_issue(branch)
|
|
177
185
|
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
if (
|
|
187
|
+
directory === undefined ||
|
|
188
|
+
branch === undefined ||
|
|
189
|
+
issue === undefined ||
|
|
190
|
+
!is_lane_directory(root, issue, directory)
|
|
191
|
+
) {
|
|
192
|
+
return undefined
|
|
193
|
+
}
|
|
181
194
|
|
|
182
195
|
return build_lane(issue, branch, directory, is_gone(directory))
|
|
183
196
|
}
|
|
@@ -236,11 +249,6 @@ async function has_lanes_in_flight(): Promise<boolean> {
|
|
|
236
249
|
return lanes.some((lane) => !lane.is_stranded)
|
|
237
250
|
}
|
|
238
251
|
|
|
239
|
-
/** The lanes in use: the open work trees an interruption has not stranded. */
|
|
240
|
-
function live_count(lanes: ReadonlyArray<LaneInfo>): number {
|
|
241
|
-
return lanes.filter((lane) => !lane.is_stranded).length
|
|
242
|
-
}
|
|
243
|
-
|
|
244
252
|
/** The seats live lanes hold — what a new lane's seat has to avoid. */
|
|
245
253
|
function used_seats(lanes: ReadonlyArray<LaneInfo>): Array<number> {
|
|
246
254
|
return lanes.map((lane) => lane.seat).filter((seat): seat is number => seat !== undefined)
|
|
@@ -272,7 +280,6 @@ const lane_registry = {
|
|
|
272
280
|
find_open_lane,
|
|
273
281
|
has_lanes_in_flight,
|
|
274
282
|
list_lanes,
|
|
275
|
-
live_count,
|
|
276
283
|
main_repository_root,
|
|
277
284
|
parse_block,
|
|
278
285
|
read_environment,
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { git_stash } from '#scripts/git/stash/git-stash'
|
|
2
1
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
3
2
|
import { IN_PROGRESS_LABEL } from '#scripts/issue/issue-labels'
|
|
4
3
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
5
4
|
import { session_cite } from '#scripts/issue/session-cite'
|
|
6
|
-
import { error_text } from '#scripts/lib/error-message'
|
|
7
5
|
import { run_cut } from '#scripts/run/cut/run-cut'
|
|
8
6
|
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
9
7
|
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
10
8
|
import { lane_dispatch, type DispatchOutcome } from './lane-dispatch'
|
|
11
|
-
import { lane_preserve } from './lane-preserve'
|
|
9
|
+
import { lane_preserve, type KeptWork } from './lane-preserve'
|
|
12
10
|
import { lane_reap } from './lane-reap'
|
|
13
11
|
import { lane_registry, type LaneInfo } from './lane-registry'
|
|
14
12
|
|
|
@@ -38,11 +36,7 @@ const NO_LANE_KIND = 'no-lane'
|
|
|
38
36
|
const NOT_RUNNING_KIND = 'not-running'
|
|
39
37
|
const UNCLAIMED_KIND = 'unclaimed'
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
kind: 'unsaved'
|
|
43
|
-
directory: string
|
|
44
|
-
reason: string
|
|
45
|
-
}
|
|
39
|
+
type Unsaved = Extract<KeptWork, { kind: 'unsaved' }>
|
|
46
40
|
|
|
47
41
|
interface Restarted {
|
|
48
42
|
kind: 'restarted'
|
|
@@ -55,7 +49,6 @@ type Refused =
|
|
|
55
49
|
| { kind: typeof NOT_RUNNING_KIND }
|
|
56
50
|
| { kind: typeof UNCLAIMED_KIND; reason: string }
|
|
57
51
|
type RestartOutcome = Restarted | Unsaved | Refused
|
|
58
|
-
type Saved = Unsaved | { kind: 'saved'; stash: string | undefined }
|
|
59
52
|
|
|
60
53
|
// A cut the stopped child carried would hand its successor the old session's hand-off at
|
|
61
54
|
// `run:cut --resume`; the fresh session has to start from the Issue instead.
|
|
@@ -65,22 +58,6 @@ function end_carried_cut(lane: LaneInfo): void {
|
|
|
65
58
|
if (carried !== undefined) run_cut.end_cut(carried.target)
|
|
66
59
|
}
|
|
67
60
|
|
|
68
|
-
async function stash_of(lane: LaneInfo): Promise<string | undefined> {
|
|
69
|
-
if (!(await git_stash.has_changes(lane.directory))) return undefined
|
|
70
|
-
|
|
71
|
-
return await lane_preserve.push_work(lane.issue, lane.directory, RESTART_OCCASION)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// A tree that could not be read or pushed is left as it is: re-dispatching into it would hand the fresh
|
|
75
|
-
// session the stopped one's half-done work as though it were its own.
|
|
76
|
-
async function save_work(lane: LaneInfo): Promise<Saved> {
|
|
77
|
-
try {
|
|
78
|
-
return { kind: 'saved', stash: await stash_of(lane) }
|
|
79
|
-
} catch (error) {
|
|
80
|
-
return { kind: 'unsaved', directory: lane.directory, reason: error_text.message_of(error) }
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
61
|
async function record_restart(issue: string): Promise<void> {
|
|
85
62
|
await run_event_stream_emit.emit(
|
|
86
63
|
run_event_stream.EVENT_KIND.RESTART,
|
|
@@ -88,9 +65,11 @@ async function record_restart(issue: string): Promise<void> {
|
|
|
88
65
|
)
|
|
89
66
|
}
|
|
90
67
|
|
|
68
|
+
// A tree that could not be read or pushed is left as it is: re-dispatching into it would hand the fresh
|
|
69
|
+
// session the stopped one's half-done work as though it were its own.
|
|
91
70
|
async function relaunch(lane: LaneInfo): Promise<RestartOutcome> {
|
|
92
71
|
end_carried_cut(lane)
|
|
93
|
-
const saved = await
|
|
72
|
+
const saved = await lane_preserve.keep_work(lane.issue, lane.directory, RESTART_OCCASION)
|
|
94
73
|
|
|
95
74
|
if (saved.kind === 'unsaved') return saved
|
|
96
75
|
const dispatch = await lane_dispatch.dispatch_child(lane.issue, 'restart')
|