@joshuafolkken/kit 1.1455.0 → 1.1514.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +6 -6
- package/.claude/skills/dependency-update/SKILL.md +26 -55
- package/.claude/skills/epic-commands/SKILL.md +83 -156
- package/.claude/skills/epic-commands/epic-bundle.md +51 -79
- package/.claude/skills/epic-commands/execution-waves.md +43 -81
- package/.claude/skills/verify-ui/SKILL.md +24 -50
- package/.claude/skills/workflow-commands/SKILL.md +7 -9
- package/.claude/skills/workflow-commands/background-commands.md +31 -46
- package/.claude/skills/workflow-commands/backlogrun-child.md +65 -85
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +86 -125
- package/.claude/skills/workflow-commands/backlogrun-park.md +68 -118
- package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -28
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +17 -26
- package/.claude/skills/workflow-commands/backlogrun-steps.md +35 -52
- package/.claude/skills/workflow-commands/backlogrun.md +31 -50
- package/.claude/skills/workflow-commands/chain-rule.md +31 -40
- package/.claude/skills/workflow-commands/delegation.md +12 -8
- package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
- package/.claude/skills/workflow-commands/followup.md +55 -77
- package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
- package/.claude/skills/workflow-commands/fullrun.md +4 -6
- package/.claude/skills/workflow-commands/halfrun.md +3 -7
- package/.claude/skills/workflow-commands/into-target.md +1 -2
- package/.claude/skills/workflow-commands/issue-comments.md +43 -60
- package/.claude/skills/workflow-commands/issue-scout.md +3 -2
- package/.claude/skills/workflow-commands/kickoff.md +65 -74
- package/.claude/skills/workflow-commands/latest-gate.md +2 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
- package/.claude/skills/workflow-commands/observation-filing.md +59 -118
- package/.claude/skills/workflow-commands/observation-ledger.md +52 -112
- package/.claude/skills/workflow-commands/pre-gate-cut.md +88 -142
- package/.claude/skills/workflow-commands/prerequisite.md +11 -17
- package/.claude/skills/workflow-commands/progress-watcher.md +23 -33
- package/.claude/skills/workflow-commands/prrun.md +3 -5
- package/.claude/skills/workflow-commands/retrospective.md +35 -54
- package/.claude/skills/workflow-commands/split-assessment.md +32 -58
- package/.claude/skills/workflow-commands/target-repository.md +1 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +54 -73
- package/.codex/hooks.json +6 -6
- package/.github/actions/setup-node/action.yml +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CLAUDE.md +7 -6
- package/cspell/index.yaml +7 -0
- package/dist/CLAUDE.md +7 -6
- package/dist/commands/document-section-cli.js +67 -34
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/chunk-4KKD2UTK.js +66 -0
- package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-6TS2JD6D.js} +14 -3
- package/dist/hooks/chunk-7DSBJVFH.js +50 -0
- package/dist/hooks/{chunk-5H4SSDSD.js → chunk-A65Y4YOR.js} +3 -3
- package/dist/hooks/{chunk-3C4XRYG6.js → chunk-APUO6U4J.js} +102 -19
- package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
- package/dist/hooks/chunk-GP4IEC4O.js +20 -0
- package/dist/hooks/chunk-JP45SOCJ.js +56 -0
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-LAOV7NLM.js} +1 -1
- package/dist/hooks/chunk-M4HGZV7U.js +83 -0
- package/dist/hooks/{chunk-TBJFTSUY.js → chunk-O6GDRNJB.js} +9 -7
- package/dist/hooks/{chunk-HFGWYGUV.js → chunk-PF2NZL2A.js} +5 -5
- package/dist/hooks/{chunk-WNLVGT7E.js → chunk-PTICX4OL.js} +4 -5
- package/dist/hooks/{chunk-HHI6FO3R.js → chunk-RT27QRJW.js} +763 -1515
- package/dist/hooks/{chunk-7WQBPLUN.js → chunk-SYOTDYCI.js} +1149 -1095
- package/dist/hooks/{chunk-LIAFCXL7.js → chunk-TNUMY6NL.js} +1519 -1332
- package/dist/hooks/chunk-UPUZF3AN.js +279 -0
- package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-WK6SMBIC.js} +1932 -784
- package/dist/hooks/codex-hook-adapter.js +17 -14
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +5 -4
- package/dist/hooks/inputs.json +302 -247
- package/dist/hooks/investigation-guard.js +8 -11
- package/dist/hooks/pretool-guard.js +16 -13
- package/dist/hooks/session-lang.js +372 -26
- package/dist/hooks/stop-guard.js +2593 -189
- package/dist/josh.js +2128 -282
- package/dist/managed-marker/index.js +1 -2
- package/dist/version/index.js +15 -12
- package/eslint/base.js +2 -1
- package/eslint/rules/unicorn.js +16 -0
- package/lefthook/base.yml +5 -3
- package/package.json +14 -12
- package/prompts/coding-standards.md +12 -22
- package/prompts/collaboration-workflow/file-edits.md +25 -45
- package/prompts/collaboration-workflow/gh-rest.md +19 -36
- package/prompts/collaboration-workflow/glossary.md +5 -7
- package/prompts/collaboration-workflow/issue-citation.md +6 -14
- package/prompts/collaboration-workflow/issue-template.md +40 -32
- package/prompts/collaboration-workflow/lane-instructions.md +35 -0
- package/prompts/collaboration-workflow/operating-rules.md +33 -46
- package/prompts/collaboration-workflow/output-bounds.md +15 -15
- package/prompts/collaboration-workflow/overview.md +14 -19
- package/prompts/collaboration-workflow/plan-comment.md +13 -13
- package/prompts/collaboration-workflow/principles.md +57 -54
- package/prompts/collaboration-workflow/proposal-request.md +1 -9
- package/prompts/collaboration-workflow/report-format.md +41 -49
- package/prompts/collaboration-workflow/residency.md +15 -11
- package/prompts/collaboration-workflow/rule-delivery.md +7 -15
- package/prompts/collaboration-workflow/shell-body.md +10 -20
- package/prompts/collaboration-workflow/turn-batching.md +8 -14
- package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
- package/prompts/collaboration-workflow/wip-cap.md +26 -32
- package/prompts/collaboration-workflow.md +25 -25
- package/prompts/refactoring.md +33 -43
- package/prompts/review-rubric.md +84 -129
- package/prompts/review.md +110 -141
- package/prompts/sonar-hotspot-handling.md +22 -50
- package/prompts/testing-guide.md +48 -99
- package/scripts/adopt/adopt-toolkits.ts +2 -4
- package/scripts/agent/agent-argv.ts +20 -4
- package/scripts/agent/agent-exit-record.ts +23 -1
- package/scripts/agent/agent-role-profile.ts +101 -45
- package/scripts/agent/agent-safe-text.ts +60 -0
- package/scripts/agent/claude-agent-argv.ts +14 -0
- package/scripts/agent/codex-agent-argv.ts +9 -2
- package/scripts/agent/lane-instructions.ts +144 -0
- package/scripts/backlog/backlog-budget-cli.ts +1 -2
- package/scripts/backlog/backlog-budget.ts +4 -4
- package/scripts/backlog/backlog-drive-carried.ts +26 -0
- package/scripts/backlog/backlog-drive-cli.ts +10 -5
- package/scripts/backlog/backlog-drive-named.ts +3 -5
- package/scripts/backlog/backlog-drive-restore.ts +28 -2
- package/scripts/backlog/backlog-drive-seats.ts +27 -0
- package/scripts/backlog/backlog-drive.ts +11 -6
- package/scripts/backlog/backlog-occupied.ts +56 -0
- package/scripts/backlog/backlog-ready.ts +59 -15
- package/scripts/behavior/behavior-rules.ts +3 -2
- package/scripts/cost-runtime/codex-usage.ts +4 -2
- package/scripts/cost-runtime/cost-cli.ts +1 -2
- package/scripts/delegation/duplicate-read-guard.ts +2 -2
- package/scripts/delegation/investigation-guard.ts +15 -65
- package/scripts/delegation/investigation-reads.ts +1 -2
- package/scripts/discovery/repo-discovery.ts +4 -2
- package/scripts/discovery/repo-origin.ts +2 -4
- package/scripts/doctor/doctor-logic.ts +6 -3
- package/scripts/document/document-byte-budget.ts +28 -27
- package/scripts/document/document-byte-check.ts +1 -2
- package/scripts/document/document-section-cli.ts +17 -5
- package/scripts/document/entry-read-budget.ts +3 -3
- package/scripts/document/read-set-cli.ts +4 -1
- package/scripts/epic/epic-audit-orphans.ts +5 -8
- package/scripts/epic/epic-audit-rationale.ts +8 -2
- package/scripts/epic/epic-audit.ts +52 -1
- package/scripts/epic/epic-bundle-cli.ts +1 -2
- package/scripts/epic/epic-bundle-evidence.ts +49 -26
- package/scripts/epic/epic-bundle-referenced.ts +14 -3
- package/scripts/epic/epic-bundle.ts +17 -9
- package/scripts/epic/epic-chains.ts +6 -2
- package/scripts/epic/epic-close-comment.ts +1 -2
- package/scripts/followup/git-followup-workflow.ts +19 -1
- package/scripts/gate/check-inputs.ts +129 -0
- package/scripts/gate/check-reuse.ts +162 -0
- package/scripts/gate/core-budget.ts +93 -44
- package/scripts/gate/core-wait.ts +130 -0
- package/scripts/gate/free-cores.ts +49 -0
- package/scripts/gate/gate-ledger.ts +17 -3
- package/scripts/gate/gate-plan.ts +23 -1
- package/scripts/gate/gate-report.ts +15 -2
- package/scripts/gate/gate-run-steps.ts +150 -0
- package/scripts/gate/held-mark.ts +55 -0
- package/scripts/gate/machine-capacity.ts +70 -11
- package/scripts/gate/pre-commit-type-check.ts +14 -1
- package/scripts/gate/verification-gate.ts +65 -127
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +24 -2
- package/scripts/gh/git-pr.ts +19 -3
- package/scripts/git/git-common-directory.ts +3 -3
- package/scripts/git/git-diff-reads.ts +16 -9
- package/scripts/git/git-execa-error.ts +1 -2
- package/scripts/git/main-merge-regenerate.ts +78 -0
- package/scripts/git/main-merge.ts +14 -5
- package/scripts/hooks/batch-guard.ts +5 -8
- package/scripts/hooks/format-edited-file.ts +10 -4
- package/scripts/hooks/pre-commit-lint.ts +91 -0
- package/scripts/hooks/pre-push-unit.ts +15 -4
- package/scripts/hooks/pretool-guard.ts +48 -79
- package/scripts/hooks/reserved-run.ts +1 -0
- package/scripts/hooks/run-hook.sh +10 -12
- package/scripts/hooks/step-zero-notice.ts +3 -2
- package/scripts/hooks/stop-actions.ts +204 -0
- package/scripts/hooks/stop-guard.ts +62 -21
- package/scripts/init/hook-command-rewrite.ts +6 -2
- package/scripts/init/hook-launch.ts +12 -7
- package/scripts/init/init-logic-deploy-vps.ts +4 -2
- package/scripts/issue/behavior-change-lint.ts +12 -6
- package/scripts/issue/expected-effect-lint.ts +59 -0
- package/scripts/issue/issue-cite-cli.ts +35 -15
- package/scripts/issue/issue-classification.ts +1 -1
- package/scripts/issue/issue-lint-cli.ts +5 -3
- package/scripts/issue/issue-scout-cli.ts +22 -16
- package/scripts/issue/markdown-section.ts +19 -5
- package/scripts/josh/command-usage.ts +110 -0
- package/scripts/josh/file-map-stamp.ts +1 -3
- package/scripts/josh/hook-decision.ts +17 -2
- package/scripts/josh/josh-command-map.ts +2 -0
- package/scripts/josh/josh-command-types.ts +55 -9
- package/scripts/josh/josh-commands-ai.ts +13 -0
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-hooks.ts +6 -0
- package/scripts/josh/josh-commands-lane.ts +10 -1
- package/scripts/josh/josh-commands-workflow.ts +5 -0
- package/scripts/josh/josh-logic.ts +6 -15
- package/scripts/josh/josh-usage-automation.ts +121 -0
- package/scripts/josh/josh-usage-backlog.ts +180 -0
- package/scripts/josh/josh-usage-developer.ts +230 -0
- package/scripts/josh/josh-usage-hooks.ts +38 -0
- package/scripts/josh/josh-usage-issue.ts +205 -0
- package/scripts/josh/josh-usage-review.ts +101 -0
- package/scripts/josh/josh-usage-run.ts +252 -0
- package/scripts/josh/josh-usage-session.ts +238 -0
- package/scripts/josh/josh-usage-workflow.ts +212 -0
- package/scripts/josh/josh.ts +14 -5
- package/scripts/josh/package-directory.ts +20 -0
- package/scripts/lane/lane-await.ts +3 -2
- package/scripts/lane/lane-capacity.ts +8 -2
- package/scripts/lane/lane-check-stats.ts +56 -0
- package/scripts/lane/lane-child-invocation.ts +49 -5
- package/scripts/lane/lane-dispatch.ts +3 -3
- package/scripts/lane/lane-guard-policy.ts +5 -5
- package/scripts/lane/lane-handoff.ts +8 -1
- package/scripts/lane/lane-install.ts +12 -4
- package/scripts/lane/lane-ledger.ts +52 -1
- package/scripts/lane/lane-limit-cli.ts +8 -9
- package/scripts/lane/lane-load.ts +9 -2
- package/scripts/lane/lane-measure-cli.ts +4 -0
- package/scripts/lane/lane-parked.ts +18 -0
- package/scripts/lane/lane-preserve.ts +29 -1
- package/scripts/lane/lane-process-load.ts +125 -0
- package/scripts/lane/lane-process-stats.ts +93 -0
- package/scripts/lane/lane-registry.ts +17 -10
- package/scripts/lane/lane-restart.ts +5 -26
- package/scripts/lane/lane-session-action.ts +146 -0
- package/scripts/lane/lane-session-broker.ts +68 -125
- package/scripts/lane/lane-spent.ts +14 -3
- package/scripts/lane/lane-stats.ts +1 -0
- package/scripts/lane/lane-withdraw-cli.ts +71 -0
- package/scripts/lane/lane-withdraw.ts +182 -0
- package/scripts/lint/format.ts +4 -4
- package/scripts/lint/lint-parallel.ts +10 -3
- package/scripts/lint/lint-related-scope.ts +4 -1
- package/scripts/managed-marker/managed-marker-logic.ts +1 -2
- package/scripts/notify/telegram-test.ts +10 -5
- package/scripts/overrides/lockfile-overrides.ts +1 -3
- package/scripts/propagate/propagate.ts +1 -2
- package/scripts/release/release-progress.ts +3 -2
- package/scripts/report/report-format-reference.ts +2 -0
- package/scripts/report/report-lint.ts +7 -4
- package/scripts/review/acceptance-map-check.ts +61 -0
- package/scripts/review/acceptance-map.ts +205 -0
- package/scripts/review/review-brief-cli.ts +1 -1
- package/scripts/review/review-record-cli.ts +6 -2
- package/scripts/rules/agent-model.ts +59 -0
- package/scripts/rules/delivered-rules.ts +11 -2
- package/scripts/rules/destructive-command.ts +6 -9
- package/scripts/rules/direct-filing.ts +3 -7
- package/scripts/rules/early-heartbeat.ts +7 -18
- package/scripts/rules/file-body.ts +6 -13
- package/scripts/rules/filing-cap.ts +4 -6
- package/scripts/rules/firing-point.ts +6 -5
- package/scripts/rules/git-force.ts +5 -9
- package/scripts/rules/guard-remedy.ts +117 -0
- package/scripts/rules/implementation-cut.ts +37 -35
- package/scripts/rules/index-guard.ts +4 -7
- package/scripts/rules/issue-comments.ts +67 -16
- package/scripts/rules/josh-git-bare.ts +3 -6
- package/scripts/rules/lane-background.ts +10 -21
- package/scripts/rules/lane-carry-conflict.ts +7 -17
- package/scripts/rules/lane-interactive-ask.ts +6 -11
- package/scripts/rules/lane-park.ts +5 -13
- package/scripts/rules/lane-split-park.ts +2 -7
- package/scripts/rules/lane-switch-main.ts +9 -12
- package/scripts/rules/last-prompt.ts +50 -5
- package/scripts/rules/oracle-consulted.ts +4 -5
- package/scripts/rules/oracle-list-cli.ts +7 -0
- package/scripts/rules/piped-verification.ts +84 -32
- package/scripts/rules/poll-loop.ts +7 -17
- package/scripts/rules/pre-gate-cut.ts +7 -21
- package/scripts/rules/prior-comment-read.ts +7 -5
- package/scripts/rules/protected-files.ts +5 -9
- package/scripts/rules/raw-field-body.ts +3 -7
- package/scripts/rules/rule-body-guard.ts +4 -11
- package/scripts/rules/rule-list.ts +4 -1
- package/scripts/rules/rule-registry-rows.ts +10 -0
- package/scripts/rules/rule-registry.ts +7 -4
- package/scripts/rules/rule-rewrite.ts +114 -0
- package/scripts/rules/rule-value.ts +7 -1
- package/scripts/rules/run-tail-rule.ts +6 -22
- package/scripts/rules/shell-body-trigger.ts +4 -6
- package/scripts/rules/shell-segments.ts +22 -0
- package/scripts/rules/stop-decision.ts +77 -0
- package/scripts/rules/stop-rules.ts +164 -77
- package/scripts/rules/tail-commands.ts +15 -1
- package/scripts/rules/test-declared-commit.ts +4 -10
- package/scripts/rules/third-party-write.ts +5 -10
- package/scripts/rules/worktree-guard.ts +5 -12
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-closed.ts +8 -2
- package/scripts/run/board/run-board-github.ts +19 -3
- package/scripts/run/board/run-board-header.ts +4 -1
- package/scripts/run/board/run-board-idle.ts +31 -0
- package/scripts/run/board/run-board-labels.ts +2 -0
- package/scripts/run/board/run-board-layout.ts +1 -1
- package/scripts/run/board/run-board-read.ts +25 -12
- package/scripts/run/board/run-board-reason.ts +17 -4
- package/scripts/run/board/run-board-render-legend.ts +1 -0
- package/scripts/run/board/run-board-render-slot.ts +1 -1
- package/scripts/run/board/run-board-render-waits.ts +41 -0
- package/scripts/run/board/run-board-render.ts +6 -1
- package/scripts/run/board/run-board-row-yield.ts +1 -0
- package/scripts/run/board/run-board-state.ts +9 -0
- package/scripts/run/board/run-board-status.ts +89 -23
- package/scripts/run/board/run-board-thanks.ts +7 -2
- package/scripts/run/board/run-board-tick.ts +37 -8
- package/scripts/run/carry/run-carry-args.ts +1 -3
- package/scripts/run/carry/run-carry-conversation.ts +7 -2
- package/scripts/run/cut/run-cut-cli.ts +18 -22
- package/scripts/run/cut/run-cut-decline.ts +72 -0
- package/scripts/run/cut/run-cut-ready.ts +74 -0
- package/scripts/run/cut/run-cut-report.ts +29 -0
- package/scripts/run/cut/run-cut.ts +10 -1
- package/scripts/run/detached-launch.ts +23 -4
- package/scripts/run/entry/run-entry-cli.ts +8 -1
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-scope.ts +19 -1
- package/scripts/run/event/run-event-stream-emit.ts +13 -6
- package/scripts/run/event/run-event-stream.ts +4 -0
- package/scripts/run/hold/run-hold-cli.ts +14 -3
- package/scripts/run/hold/run-hold-session.ts +46 -0
- package/scripts/run/hold/run-hold.ts +13 -4
- package/scripts/run/launch-priority.ts +32 -0
- package/scripts/run/merge/run-merge-cli.ts +16 -42
- package/scripts/run/merge/run-merge-ship.ts +59 -1
- package/scripts/run/merge/run-merge-signals.ts +99 -0
- package/scripts/run/merge/run-merge-steps.ts +11 -1
- package/scripts/run/merge/run-merge.ts +22 -9
- package/scripts/run/run-ending.ts +31 -3
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-parent-cut-hook.ts +5 -10
- package/scripts/run/run-poll-hook.ts +157 -0
- package/scripts/run/run-preflight.ts +1 -2
- package/scripts/run/run-step-cli.ts +22 -15
- package/scripts/run/run-step-section.ts +85 -0
- package/scripts/run/run-stop-notify.ts +1 -2
- package/scripts/run/run-watcher-guard.ts +6 -3
- package/scripts/run/ship/run-ship-cli.ts +5 -12
- package/scripts/run/ship/run-ship-detach.ts +9 -0
- package/scripts/run/ship/run-ship-next.ts +10 -2
- package/scripts/run/ship/run-ship-overlap.ts +195 -0
- package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
- package/scripts/run/ship/run-ship-preflight.ts +28 -2
- package/scripts/run/ship/run-ship-return.ts +31 -7
- package/scripts/run/ship/run-ship-review-steps.ts +25 -14
- package/scripts/run/ship/run-ship-review.ts +15 -2
- package/scripts/run/ship/run-ship-steps.ts +1 -1
- package/scripts/run/ship/run-ship-stop-context.ts +206 -0
- package/scripts/run/ship/run-ship-sync.ts +14 -6
- package/scripts/run/tidy/run-tidy-cli.ts +3 -3
- package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
- package/scripts/safe-chain/project-config.ts +7 -2
- package/scripts/sync/sync-hook-safety.ts +8 -2
- package/scripts/test/test-e2e-guard.ts +30 -5
- package/scripts/test/test-unit-guard.ts +16 -1
- package/scripts/test/unit-worker-share.ts +26 -3
- package/scripts/time-runtime/time-batch-guard.ts +3 -3
- package/scripts/time-runtime/time-hook-transcript.ts +3 -2
- package/scripts/time-runtime/time-shell.ts +39 -4
- package/scripts/time-runtime/time-spans.ts +1 -0
- package/scripts/time-runtime/time-tool-call.ts +7 -0
- package/scripts/version/effective-upstream.ts +3 -4
- package/scripts/version/kit-version-config.ts +8 -2
- package/scripts/version/latest-corepack.ts +1 -2
- package/scripts/version/release-hold.ts +11 -4
- package/scripts/version/version-commands.ts +12 -4
- package/templates/gitignore +6 -2
- package/dist/hooks/chunk-D5QBTOCZ.js +0 -68
- package/dist/hooks/chunk-UT7YC3N2.js +0 -56
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util'
|
|
2
|
+
import { agent_role_profile } from '#scripts/agent/agent-role-profile'
|
|
3
|
+
import { changed_paths } from '#scripts/git/changed-paths'
|
|
4
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { run_ship_next } from './run-ship-next'
|
|
7
|
+
import { run_ship_probe } from './run-ship-probe'
|
|
8
|
+
|
|
9
|
+
// What a relaunched lane child needs in front of it to fix a stopped ship: the tail of the failed
|
|
10
|
+
// stage's output — a red check, or the High/Medium findings a review stage reports — and the branch's
|
|
11
|
+
// changed files. Without them the resumed session opened `josh ship --log` and `git diff` itself before
|
|
12
|
+
// doing anything (joshuafolkken/kit#3923).
|
|
13
|
+
//
|
|
14
|
+
// **Both are bounded**, so a long gate log never becomes the whole prompt. The tail is kept because a
|
|
15
|
+
// check reports its verdict last; the lines left out go to a file whose path and line range the prompt
|
|
16
|
+
// names, so nothing is lost and the session reads only the range it needs.
|
|
17
|
+
|
|
18
|
+
const EXCERPT_LINE_LIMIT = 40
|
|
19
|
+
const EXCERPT_LINE_WIDTH = 240
|
|
20
|
+
const CHANGED_PATH_LIMIT = 30
|
|
21
|
+
const OUTPUT_PREFIX = 'josh-ship-stop-'
|
|
22
|
+
const OUTPUT_SUFFIX = '.log'
|
|
23
|
+
const KEY_SEPARATOR = '-'
|
|
24
|
+
const LINE_BREAK = '\n'
|
|
25
|
+
const CARRIAGE_RETURN = '\r'
|
|
26
|
+
|
|
27
|
+
const omitted_schema = z
|
|
28
|
+
.object({ path: z.string().optional(), last_line: z.number().int() })
|
|
29
|
+
.strict()
|
|
30
|
+
const stop_context_schema = z
|
|
31
|
+
.object({
|
|
32
|
+
excerpt: z.array(z.string()).readonly(),
|
|
33
|
+
omitted: omitted_schema.optional(),
|
|
34
|
+
changed: z.array(z.string()).readonly().optional(),
|
|
35
|
+
more_changed: z.number().int(),
|
|
36
|
+
})
|
|
37
|
+
.strict()
|
|
38
|
+
|
|
39
|
+
type StopContext = z.infer<typeof stop_context_schema>
|
|
40
|
+
type Omitted = z.infer<typeof omitted_schema>
|
|
41
|
+
|
|
42
|
+
// A spinner redraws its line with a carriage return, so only the frame after the last one is what the
|
|
43
|
+
// terminal showed; a CRLF line ends in one and keeps its whole text.
|
|
44
|
+
function shown_frame(line: string): string {
|
|
45
|
+
return line.trimEnd().split(CARRIAGE_RETURN).at(-1) ?? ''
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function output_lines(output: string): Array<string> {
|
|
49
|
+
return stripVTControlCharacters(output)
|
|
50
|
+
.trimEnd()
|
|
51
|
+
.split(LINE_BREAK)
|
|
52
|
+
.map((line) => agent_role_profile.printable(shown_frame(line)))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The full output is kept beside the ship log, keyed the same way, so the prompt's line range points at
|
|
56
|
+
// text that reads exactly as the excerpt was cut from.
|
|
57
|
+
async function keep_full_output(issue: string, output: string): Promise<string | undefined> {
|
|
58
|
+
const repository = await run_ship_probe.repository_directory()
|
|
59
|
+
|
|
60
|
+
if (repository === undefined) return undefined
|
|
61
|
+
|
|
62
|
+
const target = stamp_file.stamp_path(
|
|
63
|
+
`${OUTPUT_PREFIX}${issue}${KEY_SEPARATOR}`,
|
|
64
|
+
repository,
|
|
65
|
+
OUTPUT_SUFFIX,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
return stamp_file.replace_text_stamp(target, output)
|
|
70
|
+
} catch {
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A full output that could not be kept still reports the cut, with no path: the prompt then points at
|
|
76
|
+
// the stopped report, which holds the same lines.
|
|
77
|
+
async function omitted_range(issue: string, lines: ReadonlyArray<string>): Promise<Omitted> {
|
|
78
|
+
const path = await keep_full_output(issue, lines.join(LINE_BREAK))
|
|
79
|
+
const last_line = lines.length - EXCERPT_LINE_LIMIT
|
|
80
|
+
|
|
81
|
+
return path === undefined ? { last_line } : { path, last_line }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// A line is cut to a width, so one minified or wrapped line cannot spend the prompt's whole budget.
|
|
85
|
+
function clipped(lines: ReadonlyArray<string>): Array<string> {
|
|
86
|
+
return lines.map((line) => line.slice(0, EXCERPT_LINE_WIDTH))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function excerpt_of(
|
|
90
|
+
issue: string,
|
|
91
|
+
output: string,
|
|
92
|
+
): Promise<Pick<StopContext, 'excerpt' | 'omitted'>> {
|
|
93
|
+
const lines = output.trim() === '' ? [] : output_lines(output)
|
|
94
|
+
|
|
95
|
+
if (lines.length <= EXCERPT_LINE_LIMIT) return { excerpt: clipped(lines) }
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
excerpt: clipped(lines.slice(-EXCERPT_LINE_LIMIT)),
|
|
99
|
+
omitted: await omitted_range(issue, lines),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A git that cannot answer leaves the list out rather than costing the relaunch — never empty, which
|
|
104
|
+
// would read as a branch with no changes.
|
|
105
|
+
async function read_paths(): Promise<Array<string> | undefined> {
|
|
106
|
+
try {
|
|
107
|
+
return await changed_paths.read_changed_paths(false)
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function changed_of(): Promise<Pick<StopContext, 'changed' | 'more_changed'>> {
|
|
114
|
+
const paths = await read_paths()
|
|
115
|
+
|
|
116
|
+
if (paths === undefined) return { more_changed: 0 }
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
changed: paths.slice(0, CHANGED_PATH_LIMIT),
|
|
120
|
+
more_changed: Math.max(0, paths.length - CHANGED_PATH_LIMIT),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function build(issue: string, output: string): Promise<StopContext> {
|
|
125
|
+
return { ...(await excerpt_of(issue, output)), ...(await changed_of()) }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function has_excerpt(context: StopContext | undefined): boolean {
|
|
129
|
+
return context !== undefined && context.excerpt.length > 0
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function excerpt_heading(issue: string, omitted: Omitted | undefined, shown: number): string {
|
|
133
|
+
if (omitted === undefined) return 'The failed stage output:'
|
|
134
|
+
|
|
135
|
+
const source = omitted.path ?? `the stopped report (\`${run_ship_next.COMMAND.LOG} ${issue}\`)`
|
|
136
|
+
const range = `lines 1-${String(omitted.last_line)} are in ${source}`
|
|
137
|
+
|
|
138
|
+
return `The failed stage output (its last ${String(shown)} lines; ${range}):`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The head lines a budget leaves out join the range the heading names; the file, when one was kept,
|
|
142
|
+
// still holds every line.
|
|
143
|
+
function with_dropped(omitted: Omitted | undefined, dropped: number): Omitted | undefined {
|
|
144
|
+
if (dropped === 0) return omitted
|
|
145
|
+
|
|
146
|
+
return { ...omitted, last_line: (omitted?.last_line ?? 0) + dropped }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function excerpt_block(
|
|
150
|
+
issue: string,
|
|
151
|
+
context: StopContext,
|
|
152
|
+
changed: string,
|
|
153
|
+
dropped: number,
|
|
154
|
+
): string {
|
|
155
|
+
const shown = context.excerpt.slice(dropped)
|
|
156
|
+
const heading = excerpt_heading(issue, with_dropped(context.omitted, dropped), shown.length)
|
|
157
|
+
|
|
158
|
+
return [heading, ...shown, changed].join(LINE_BREAK)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function changed_text(context: StopContext): string {
|
|
162
|
+
if (context.changed === undefined) return 'The changed files could not be read from git.'
|
|
163
|
+
|
|
164
|
+
if (context.changed.length === 0) return 'No files changed against the default branch.'
|
|
165
|
+
|
|
166
|
+
const more = context.more_changed > 0 ? ` (and ${String(context.more_changed)} more)` : ''
|
|
167
|
+
|
|
168
|
+
return `Changed files on the branch: ${context.changed.join(', ')}${more}.`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// The block the ship-stop prompt carries between the next command and the closing invocation.
|
|
172
|
+
//
|
|
173
|
+
// **It fits the `budget` the prompt leaves it** — the prompt is one argv element, refused past
|
|
174
|
+
// `MAX_VALUE_LENGTH` (joshuafolkken/kit#3964) — by dropping the excerpt's head first, since a check
|
|
175
|
+
// reports its verdict last; the tail line always stays.
|
|
176
|
+
function describe(
|
|
177
|
+
issue: string,
|
|
178
|
+
context: StopContext,
|
|
179
|
+
budget: number = agent_role_profile.MAX_VALUE_LENGTH,
|
|
180
|
+
): string {
|
|
181
|
+
const changed = changed_text(context)
|
|
182
|
+
|
|
183
|
+
if (!has_excerpt(context)) return changed
|
|
184
|
+
|
|
185
|
+
let dropped = 0
|
|
186
|
+
let block = excerpt_block(issue, context, changed, dropped)
|
|
187
|
+
|
|
188
|
+
while (block.length > budget && dropped < context.excerpt.length - 1) {
|
|
189
|
+
dropped += 1
|
|
190
|
+
block = excerpt_block(issue, context, changed, dropped)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return block
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const run_ship_stop_context = {
|
|
197
|
+
CHANGED_PATH_LIMIT,
|
|
198
|
+
EXCERPT_LINE_LIMIT,
|
|
199
|
+
build,
|
|
200
|
+
describe,
|
|
201
|
+
has_excerpt,
|
|
202
|
+
stop_context_schema,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export type { StopContext }
|
|
206
|
+
export { run_ship_stop_context }
|
|
@@ -4,6 +4,7 @@ import { git_command } from '#scripts/git/git-command'
|
|
|
4
4
|
import { main_merge, type MergeOutcome } from '#scripts/git/main-merge'
|
|
5
5
|
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
6
6
|
import { error_text } from '#scripts/lib/error-message'
|
|
7
|
+
import { run_ship_overlap } from './run-ship-overlap'
|
|
7
8
|
import { run_ship_scoped } from './run-ship-scoped'
|
|
8
9
|
|
|
9
10
|
// `josh ship` keeps the branch current with the default branch itself: a default branch that moves
|
|
@@ -11,9 +12,9 @@ import { run_ship_scoped } from './run-ship-scoped'
|
|
|
11
12
|
// merging the default branch again usually resolves it.
|
|
12
13
|
//
|
|
13
14
|
// So the merge happens mechanically: once before the commit (`sync_stage`), and once when
|
|
14
|
-
// the followup fails on a pull request GitHub reports `DIRTY` (`followup_stage`).
|
|
15
|
-
//
|
|
16
|
-
// resumed session goes straight to them.
|
|
15
|
+
// the followup fails on a pull request GitHub reports `DIRTY` (`followup_stage`). A conflict only on
|
|
16
|
+
// generated files is regenerated by the merge itself (`main-merge-regenerate.ts`); any other conflict
|
|
17
|
+
// stops the ship as `conflict`, naming the unmerged paths so the resumed session goes straight to them.
|
|
17
18
|
|
|
18
19
|
const should_forward_stderr = true
|
|
19
20
|
const SUCCESS_EXIT_CODE = 0
|
|
@@ -66,12 +67,19 @@ function sync_note(outcome: Exclude<MergeOutcome, { kind: 'conflict' }>): string
|
|
|
66
67
|
// touches, and once that work is committed the followup merges again on a clean tree. A merge that changed
|
|
67
68
|
// the tree is checked by the gate stage that follows, so the commit never carries
|
|
68
69
|
// an unchecked merge and the gate's record names the merge base the push carries.
|
|
69
|
-
|
|
70
|
+
//
|
|
71
|
+
// The merge waits first for any lane whose open pull request changes the same paths
|
|
72
|
+
// (`run-ship-overlap.ts`), so it brings that pull request in before the gate rather than meeting it
|
|
73
|
+
// as `DIRTY` after CI.
|
|
74
|
+
async function sync_stage(issue: string): Promise<StepResult> {
|
|
75
|
+
const waited = run_ship_overlap.note(await run_ship_overlap.await_overlap(issue))
|
|
70
76
|
const outcome = await merge_outcome()
|
|
71
77
|
|
|
72
|
-
if (outcome.kind === 'conflict') return conflict_result(outcome.files,
|
|
78
|
+
if (outcome.kind === 'conflict') return conflict_result(outcome.files, waited)
|
|
79
|
+
|
|
80
|
+
const out = [waited, sync_note(outcome)].filter((line) => line !== '').join('\n')
|
|
73
81
|
|
|
74
|
-
return { code: SUCCESS_EXIT_CODE, out
|
|
82
|
+
return { code: SUCCESS_EXIT_CODE, out }
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
// Any read that fails answers "not conflicting": the followup failure then stands as it was.
|
|
@@ -33,12 +33,12 @@ function memoized(read: IsMerged): IsMerged {
|
|
|
33
33
|
|
|
34
34
|
async function sweep(): Promise<void> {
|
|
35
35
|
try {
|
|
36
|
-
const
|
|
36
|
+
const is_merged = memoized(issue_merged.read_merged)
|
|
37
37
|
const in_flight = backlog_drive_restore.active_issues(
|
|
38
38
|
await run_event_stream_emit.current_events(),
|
|
39
39
|
)
|
|
40
|
-
const lanes = await run_tidy_lanes.tidy_lanes(
|
|
41
|
-
const stashes = await run_tidy_stashes.tidy_stashes(
|
|
40
|
+
const lanes = await run_tidy_lanes.tidy_lanes(is_merged, in_flight)
|
|
41
|
+
const stashes = await run_tidy_stashes.tidy_stashes(is_merged)
|
|
42
42
|
const report = run_tidy.format_report([...lanes, ...stashes])
|
|
43
43
|
|
|
44
44
|
if (report !== undefined) console.error(report)
|
|
@@ -95,8 +95,9 @@ async function tidy_lane(
|
|
|
95
95
|
is_merged: IsMerged,
|
|
96
96
|
in_flight: ReadonlySet<string>,
|
|
97
97
|
): Promise<Outcome | undefined> {
|
|
98
|
-
if (in_flight.has(lane.issue))
|
|
99
|
-
|
|
98
|
+
if (in_flight.has(lane.issue) || lane.is_stranded || !(await is_merged(lane.issue))) {
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
100
101
|
|
|
101
102
|
return { target: `lane ${session_cite.issue(lane.issue)}`, verdict: await settle(lane) }
|
|
102
103
|
}
|
|
@@ -126,8 +126,13 @@ function merge_project_config(existing: string, workspace: string): string {
|
|
|
126
126
|
const aikido = yaml_document.parse_yaml(existing)
|
|
127
127
|
const safe_chain = read_mapping(aikido['safe-chain'], 'safe-chain')
|
|
128
128
|
const merged = merge_age(merge_exclusions(safe_chain, exclusions), workspace)
|
|
129
|
-
|
|
130
|
-
if (
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
JSON.stringify(merged) === JSON.stringify(safe_chain) ||
|
|
132
|
+
has_nothing_to_write(existing, exclusions, workspace)
|
|
133
|
+
) {
|
|
134
|
+
return existing
|
|
135
|
+
}
|
|
131
136
|
|
|
132
137
|
return replace_safe_chain_block(existing, merged)
|
|
133
138
|
}
|
|
@@ -65,8 +65,14 @@ function hook_write_warning(
|
|
|
65
65
|
filename: string,
|
|
66
66
|
): string | undefined {
|
|
67
67
|
const installed_version = installed_consumer_version(project_root)
|
|
68
|
-
|
|
69
|
-
if (
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
writing_version === undefined ||
|
|
71
|
+
installed_version === undefined ||
|
|
72
|
+
is_safe_to_write_hooks(writing_version, installed_version)
|
|
73
|
+
) {
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
70
76
|
|
|
71
77
|
return outdated_install_warning(writing_version, installed_version, filename)
|
|
72
78
|
}
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
import { existsSync, globSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { environment_flags } from '@joshuafolkken/kit/env'
|
|
6
|
+
import { core_budget } from '#scripts/gate/core-budget'
|
|
5
7
|
import { execa } from 'execa'
|
|
6
8
|
|
|
7
9
|
const PNPM = 'pnpm'
|
|
10
|
+
// `--workers`, `--workers=<n>` and Playwright's `-j` short form.
|
|
11
|
+
const WORKER_FLAGS = /^(?:--workers(?:=|$)|-j)/u
|
|
8
12
|
const FAIL_EXIT_CODE = 1
|
|
9
13
|
const ARGV_START = 2
|
|
10
14
|
const PLAYWRIGHT_PACKAGE = path.join('node_modules', '@playwright', 'test')
|
|
@@ -45,15 +49,36 @@ function has_e2e_tests(project_directory: string): boolean {
|
|
|
45
49
|
return matches.length > 0
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
// **A local run's worker count is the weight it reserved** (joshuafolkken/kit#3944). Playwright's own
|
|
53
|
+
// default is half the cores, which no ledger entry stood for; CI keeps its config's count, and a
|
|
54
|
+
// count the caller names wins.
|
|
55
|
+
function playwright_arguments(extra_arguments: ReadonlyArray<string>): ReadonlyArray<string> {
|
|
56
|
+
const is_count_named = extra_arguments.some((argument) => WORKER_FLAGS.test(argument))
|
|
57
|
+
|
|
58
|
+
if (is_count_named || environment_flags.is_ci_enabled(process.env['CI'])) return extra_arguments
|
|
59
|
+
|
|
60
|
+
return [`--workers=${String(core_budget.CORE_WEIGHTS.e2e)}`, ...extra_arguments]
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
async function run_playwright(extra_arguments: ReadonlyArray<string>): Promise<number> {
|
|
49
|
-
const result = await execa(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
const result = await execa(
|
|
65
|
+
PNPM,
|
|
66
|
+
['exec', 'playwright', 'test', ...playwright_arguments(extra_arguments)],
|
|
67
|
+
{ stdio: 'inherit', reject: false },
|
|
68
|
+
)
|
|
53
69
|
|
|
54
70
|
return result.exitCode ?? FAIL_EXIT_CODE
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
// Only a run reserves: a skip starts nothing, and a claim there could wait out a full ledger for it.
|
|
74
|
+
async function run_reserved_playwright(extra_arguments: ReadonlyArray<string>): Promise<number> {
|
|
75
|
+
return await core_budget.with_command_reservation(
|
|
76
|
+
core_budget.CORE_WEIGHTS.e2e,
|
|
77
|
+
async () => await run_playwright(extra_arguments),
|
|
78
|
+
{ command: 'test:e2e' },
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
async function run_guarded_e2e(
|
|
58
83
|
project_directory: string,
|
|
59
84
|
extra_arguments: ReadonlyArray<string>,
|
|
@@ -61,7 +86,7 @@ async function run_guarded_e2e(
|
|
|
61
86
|
const is_installed = is_playwright_installed(project_directory)
|
|
62
87
|
const has_tests = has_e2e_tests(project_directory)
|
|
63
88
|
const action = resolve_guard_action(is_installed, has_tests)
|
|
64
|
-
if (action === 'run') return await
|
|
89
|
+
if (action === 'run') return await run_reserved_playwright(extra_arguments)
|
|
65
90
|
|
|
66
91
|
console.info(`josh test:e2e: ${SKIP_REASONS[action]} — skipping Playwright e2e.`)
|
|
67
92
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { existsSync, globSync } from 'node:fs'
|
|
3
|
+
import { availableParallelism } from 'node:os'
|
|
3
4
|
import path from 'node:path'
|
|
4
5
|
import { fileURLToPath } from 'node:url'
|
|
5
6
|
import { PROJECT_ENVIRONMENT_KEYS } from '#ports'
|
|
7
|
+
import { free_cores } from '#scripts/gate/free-cores'
|
|
6
8
|
import { project_checks } from '#scripts/gate/project-checks'
|
|
7
9
|
import { execa } from 'execa'
|
|
8
10
|
import { project_test_script, type ProjectScriptState } from './project-test-script'
|
|
@@ -175,11 +177,24 @@ function isolated_unit_environment(
|
|
|
175
177
|
// Reading the share first and claiming afterwards leaves a window in which two hooks firing together
|
|
176
178
|
// both count "alone" and both run uncapped. Inside the marker the number is identical — this run's own
|
|
177
179
|
// record is now on disk and `is_nested_run` therefore contributes nothing — and the window is gone.
|
|
180
|
+
//
|
|
181
|
+
// **The machine is read only when no count was typed.** The reading waits out a sample window, and a
|
|
182
|
+
// caller's `--maxWorkers` — the gate's cap among them, already bounded by the same reading — wins
|
|
183
|
+
// whatever it says.
|
|
184
|
+
async function vitest_share(vitest_arguments: ReadonlyArray<string>): Promise<number | undefined> {
|
|
185
|
+
if (unit_worker_share.has_worker_flag(vitest_arguments)) return undefined
|
|
186
|
+
|
|
187
|
+
const available_cores = availableParallelism()
|
|
188
|
+
const free = await free_cores.read({ available_cores })
|
|
189
|
+
|
|
190
|
+
return unit_worker_share.current_share(available_cores, unit_worker_share.live_run_count(), free)
|
|
191
|
+
}
|
|
192
|
+
|
|
178
193
|
async function run_vitest(vitest_arguments: ReadonlyArray<string>): Promise<number> {
|
|
179
194
|
return await unit_worker_share.with_run_marker(async () => {
|
|
180
195
|
const sized = unit_worker_share.worker_arguments(
|
|
181
196
|
vitest_arguments,
|
|
182
|
-
|
|
197
|
+
await vitest_share(vitest_arguments),
|
|
183
198
|
)
|
|
184
199
|
const result = await execa(PNPM, ['exec', 'vitest', ...vitest_arguments, ...sized], {
|
|
185
200
|
env: isolated_unit_environment(),
|
|
@@ -153,6 +153,22 @@ function resolve_unit_workers(available_cores: number, runs: number): number | u
|
|
|
153
153
|
return Math.max(MIN_WORKERS, Math.floor(available_cores / runs))
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// **The run count divides the machine; the free cores bound what that division hands out.** The
|
|
157
|
+
// share counts only unit runs, so load from outside the core budget's ledger — Claude sessions, other
|
|
158
|
+
// tools — went unseen: 22 of 38 ledger samples at a load average of 20 or more had vitest as their
|
|
159
|
+
// heaviest process, one at 381% over 30 processes. `free_cores` is `free-cores.ts`'s reading, the
|
|
160
|
+
// cores less that outside load. `undefined` — a CI runner, a CPU quota — and a reading at or above
|
|
161
|
+
// the ceiling both leave `workers` as it was, so an unread or quiet machine changes nothing.
|
|
162
|
+
function cap_to_free(
|
|
163
|
+
workers: number | undefined,
|
|
164
|
+
free_cores: number | undefined,
|
|
165
|
+
available_cores: number,
|
|
166
|
+
): number | undefined {
|
|
167
|
+
if (free_cores === undefined || free_cores >= (workers ?? available_cores)) return workers
|
|
168
|
+
|
|
169
|
+
return Math.max(MIN_WORKERS, Math.floor(free_cores))
|
|
170
|
+
}
|
|
171
|
+
|
|
156
172
|
// The share this run may take, read from the machine. Separate from `resolve_unit_workers` so that
|
|
157
173
|
// one stays a pure function of two numbers and every test of it can pass them.
|
|
158
174
|
//
|
|
@@ -163,10 +179,12 @@ function resolve_unit_workers(available_cores: number, runs: number): number | u
|
|
|
163
179
|
function current_share(
|
|
164
180
|
available_cores: number = availableParallelism(),
|
|
165
181
|
live_runs: number = live_run_count(),
|
|
182
|
+
free_cores?: number,
|
|
166
183
|
): number | undefined {
|
|
167
184
|
const own_run = is_nested_run() ? 0 : SOLO_RUNS
|
|
185
|
+
const share = resolve_unit_workers(available_cores, live_runs + own_run)
|
|
168
186
|
|
|
169
|
-
return
|
|
187
|
+
return cap_to_free(share, free_cores, available_cores)
|
|
170
188
|
}
|
|
171
189
|
|
|
172
190
|
function is_worker_flag(argument: string): boolean {
|
|
@@ -182,12 +200,15 @@ function is_worker_flag(argument: string): boolean {
|
|
|
182
200
|
// would make this function's own answer depend on what happened to be running while it was asked, and
|
|
183
201
|
// `f(args, undefined)` takes the default rather than the value — so the case that matters most, "this
|
|
184
202
|
// run is alone", would be the one case no test could express.
|
|
203
|
+
function has_worker_flag(vitest_arguments: ReadonlyArray<string>): boolean {
|
|
204
|
+
return vitest_arguments.some((argument) => is_worker_flag(argument))
|
|
205
|
+
}
|
|
206
|
+
|
|
185
207
|
function worker_arguments(
|
|
186
208
|
vitest_arguments: ReadonlyArray<string>,
|
|
187
209
|
share: number | undefined,
|
|
188
210
|
): Array<string> {
|
|
189
|
-
if (share === undefined) return []
|
|
190
|
-
if (vitest_arguments.some((argument) => is_worker_flag(argument))) return []
|
|
211
|
+
if (share === undefined || has_worker_flag(vitest_arguments)) return []
|
|
191
212
|
|
|
192
213
|
return [`${WORKER_FLAG}=${String(share)}`]
|
|
193
214
|
}
|
|
@@ -229,7 +250,9 @@ const unit_worker_share = {
|
|
|
229
250
|
NESTED_VALUE,
|
|
230
251
|
RUN_PREFIX,
|
|
231
252
|
SOLO_RUNS,
|
|
253
|
+
cap_to_free,
|
|
232
254
|
current_share,
|
|
255
|
+
has_worker_flag,
|
|
233
256
|
is_nested_run,
|
|
234
257
|
live_run_count,
|
|
235
258
|
marker_path,
|
|
@@ -51,12 +51,12 @@ type NamedCall = Pick<Span, 'is_bundleable' | 'is_writing' | 'label' | 'targets'
|
|
|
51
51
|
const RULE_POINTER = '`prompts/collaboration-workflow/turn-batching.md`'
|
|
52
52
|
const STREAK = `${String(CONSECUTIVE_LIMIT)} single-call turns in a row`
|
|
53
53
|
|
|
54
|
-
// Includes what to do when
|
|
54
|
+
// Includes what to do when there is nothing to pair — the guard cannot see the turn it interrupted.
|
|
55
55
|
const REASON =
|
|
56
56
|
`⛔ batching: ${STREAK}. Reissue this with the calls that do not need its result — the test is ` +
|
|
57
57
|
`whether a call's input needs another call's result, not what kind of call it is (edits are covered ` +
|
|
58
|
-
`exactly as reads)
|
|
59
|
-
`
|
|
58
|
+
`exactly as reads); it never authorizes weakening a verification gate or a review. Nothing to ` +
|
|
59
|
+
`pair? Reissue it unchanged. ${RULE_POINTER}`
|
|
60
60
|
|
|
61
61
|
// A whole-file write earns a notice, not a refusal: a `Write` cannot be refused safely.
|
|
62
62
|
const NOTICE =
|
|
@@ -68,8 +68,9 @@ function fork_path(transcript_path: string, agent_id: string): string {
|
|
|
68
68
|
// **A path that is not a transcript is answered with itself**, since nothing can be derived from it and
|
|
69
69
|
// guessing would name a file in some other layout entirely.
|
|
70
70
|
function transcript_of(transcript_path: string, agent_id: string | null | undefined): string {
|
|
71
|
-
if (!is_agent_id(agent_id)
|
|
72
|
-
|
|
71
|
+
if (!is_agent_id(agent_id) || !transcript_path.endsWith(TRANSCRIPT_EXTENSION)) {
|
|
72
|
+
return transcript_path
|
|
73
|
+
}
|
|
73
74
|
|
|
74
75
|
return fork_path(transcript_path, agent_id)
|
|
75
76
|
}
|
|
@@ -32,8 +32,9 @@ const SEGMENT_PATTERN = /&&|\|\||;|\|/u
|
|
|
32
32
|
// last, while `||` keeps the first command's and runs the second only when it failed. `|&` is a pipe
|
|
33
33
|
// and is cut here, its `&` left in the fragment that follows.
|
|
34
34
|
const PIPE_PATTERN = /(?<!\|)\|(?!\|)/u
|
|
35
|
-
// What `SEGMENT_PATTERN` cuts on minus the pipe, for reading *inside* one pipe segment
|
|
36
|
-
|
|
35
|
+
// What `SEGMENT_PATTERN` cuts on minus the pipe, for reading *inside* one pipe segment — plus the
|
|
36
|
+
// newline, which ends a command as `;` does, so a line typed before the piped check is not read as it.
|
|
37
|
+
const CHAIN_PATTERN = /&&|\|\||;|\n/u
|
|
37
38
|
// A quoted span is text, not shell syntax, and it is removed before the pipe reading below. This
|
|
38
39
|
// repository's issue bodies, comment bodies and commit messages quote command chains constantly, so
|
|
39
40
|
// `--body "cd x && pnpm josh gate | tail"` is a `gh` call and nothing else — a reader that split its
|
|
@@ -189,6 +190,15 @@ function unquoted(command: string): string {
|
|
|
189
190
|
return without_heredoc_bodies(command).replaceAll(QUOTED_SPAN_PATTERN, ' ')
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
// Quoted text replaced by spaces of its own length, so an offset found here is the same offset in the
|
|
194
|
+
// command — what a caller cutting the command where the unquoted reading says to needs. `undefined`
|
|
195
|
+
// for a line carrying a heredoc, whose body lines a cut would read as commands of their own.
|
|
196
|
+
function blanked(command: string): string | undefined {
|
|
197
|
+
if (HEREDOC_MARKER.test(command)) return undefined
|
|
198
|
+
|
|
199
|
+
return command.replaceAll(QUOTED_SPAN_PATTERN, (span) => ' '.repeat(span.length))
|
|
200
|
+
}
|
|
201
|
+
|
|
192
202
|
function discarded_commands(command: string): Array<string> {
|
|
193
203
|
const plain = unquoted(command)
|
|
194
204
|
|
|
@@ -290,8 +300,8 @@ function drop_redirections(words: ReadonlyArray<string>): Array<string> {
|
|
|
290
300
|
// **The match's own text is what is sliced off, not a fixed word count.** The prefix is two words in
|
|
291
301
|
// `josh lint`, three in `pnpm josh lint` and four in `pnpm exec josh lint`, so counting words would
|
|
292
302
|
// leave `josh` itself in the argument list for one of the three spellings.
|
|
293
|
-
function
|
|
294
|
-
const text = command_words(
|
|
303
|
+
function josh_arguments_of_segment(segment: string): Array<string> {
|
|
304
|
+
const text = command_words(segment).join(' ')
|
|
295
305
|
const match = JOSH_PATTERN.exec(text)
|
|
296
306
|
|
|
297
307
|
if (match === null) return []
|
|
@@ -304,15 +314,40 @@ function josh_arguments(command: string): Array<string> {
|
|
|
304
314
|
return drop_redirections(words)
|
|
305
315
|
}
|
|
306
316
|
|
|
317
|
+
function josh_arguments(command: string): Array<string> {
|
|
318
|
+
return josh_arguments_of_segment(command_segment(command))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The josh subcommand one segment runs plus every argument it passes, redirections dropped, or `''`
|
|
322
|
+
// for a segment that runs none. **Flags are kept**: which of them ask nothing new (`run:board --chat`)
|
|
323
|
+
// and which turn a read into a write (`run:carry --cut`) is the reader's rule, not this one's.
|
|
324
|
+
function josh_signature_of_segment(segment: string): string {
|
|
325
|
+
const name = josh_command_of_segment(segment)
|
|
326
|
+
|
|
327
|
+
if (name === '') return ''
|
|
328
|
+
|
|
329
|
+
return [name, ...josh_arguments_of_segment(segment)].join(' ')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Every segment's signature, read over the same unquoted chain `josh_commands_of` walks.
|
|
333
|
+
function josh_signatures_of(command: string): Array<string> {
|
|
334
|
+
return unquoted(command)
|
|
335
|
+
.split(SEGMENT_PATTERN)
|
|
336
|
+
.map((segment) => josh_signature_of_segment(segment))
|
|
337
|
+
.filter((signature) => signature !== '')
|
|
338
|
+
}
|
|
339
|
+
|
|
307
340
|
const time_shell = {
|
|
308
341
|
JOSH_PREFIX,
|
|
309
342
|
bash_command,
|
|
310
343
|
bash_label,
|
|
344
|
+
blanked,
|
|
311
345
|
command_segment,
|
|
312
346
|
discarded_commands,
|
|
313
347
|
josh_arguments,
|
|
314
348
|
josh_command_of,
|
|
315
349
|
josh_commands_of,
|
|
350
|
+
josh_signatures_of,
|
|
316
351
|
leading_word,
|
|
317
352
|
unquoted,
|
|
318
353
|
}
|
|
@@ -334,6 +334,7 @@ function to_spans(events: ReadonlyArray<TimelineEvent>, finished: FinishedAt): A
|
|
|
334
334
|
label: event.label,
|
|
335
335
|
josh_command: event.josh_command,
|
|
336
336
|
josh_commands: event.josh_commands,
|
|
337
|
+
josh_signatures: event.josh_signatures,
|
|
337
338
|
check_key: event.check_key,
|
|
338
339
|
marker: event.marker,
|
|
339
340
|
is_bundleable: event.is_bundleable,
|
|
@@ -55,6 +55,9 @@ interface ToolCall {
|
|
|
55
55
|
// A chained `pnpm josh lint:related && pnpm josh test:related` carries both; the count tables expand
|
|
56
56
|
// it so the ones `josh_command` drops are still counted.
|
|
57
57
|
josh_commands: ReadonlyArray<string>
|
|
58
|
+
// Each of those with the operands it named, flags dropped — what tells `lane:output 1` from
|
|
59
|
+
// `lane:output 2` once the span has kept no command string. `run-poll-hook.ts` reads it.
|
|
60
|
+
josh_signatures: ReadonlyArray<string>
|
|
58
61
|
check_key: string
|
|
59
62
|
marker: PhaseMarker
|
|
60
63
|
is_bundleable: boolean
|
|
@@ -83,6 +86,7 @@ const NO_CALL: ToolCall = {
|
|
|
83
86
|
label: '',
|
|
84
87
|
josh_command: '',
|
|
85
88
|
josh_commands: [],
|
|
89
|
+
josh_signatures: [],
|
|
86
90
|
check_key: time_single_check.NO_CHECK,
|
|
87
91
|
marker: time_markers.NO_MARKER,
|
|
88
92
|
message_id: NO_MESSAGE_ID,
|
|
@@ -95,6 +99,7 @@ const UNKNOWN_CALL: ToolCall = {
|
|
|
95
99
|
label: UNKNOWN_TOOL,
|
|
96
100
|
josh_command: '',
|
|
97
101
|
josh_commands: [],
|
|
102
|
+
josh_signatures: [],
|
|
98
103
|
check_key: time_single_check.NO_CHECK,
|
|
99
104
|
marker: time_markers.NO_MARKER,
|
|
100
105
|
message_id: NO_MESSAGE_ID,
|
|
@@ -111,6 +116,7 @@ function non_bash_call(name: string, input: unknown, message_id: string): ToolCa
|
|
|
111
116
|
label: name,
|
|
112
117
|
josh_command: '',
|
|
113
118
|
josh_commands: [],
|
|
119
|
+
josh_signatures: [],
|
|
114
120
|
check_key: time_single_check.NO_CHECK,
|
|
115
121
|
marker: time_markers.tool_marker(name, input),
|
|
116
122
|
message_id,
|
|
@@ -132,6 +138,7 @@ function bash_call(input: unknown, message_id: string): ToolCall {
|
|
|
132
138
|
label: time_shell.bash_label(command),
|
|
133
139
|
josh_command,
|
|
134
140
|
josh_commands: time_shell.josh_commands_of(command),
|
|
141
|
+
josh_signatures: time_shell.josh_signatures_of(command),
|
|
135
142
|
check_key: time_single_check.check_key(josh_command, command),
|
|
136
143
|
marker: time_markers.bash_marker(command),
|
|
137
144
|
message_id,
|
|
@@ -23,8 +23,7 @@ function read_matching_version(manifest_path: string, package_name: string): str
|
|
|
23
23
|
const parsed = package_named_version_schema.safeParse(
|
|
24
24
|
json_value.parse_or_undefined(readFileSync(manifest_path, 'utf8')),
|
|
25
25
|
)
|
|
26
|
-
if (!parsed.success) return undefined
|
|
27
|
-
if (parsed.data.name !== package_name) return undefined
|
|
26
|
+
if (!parsed.success || parsed.data.name !== package_name) return undefined
|
|
28
27
|
|
|
29
28
|
return parsed.data.version
|
|
30
29
|
}
|
|
@@ -45,10 +44,10 @@ function walk_up_for_version(start_directory: string, package_name: string): str
|
|
|
45
44
|
function resolve_from_base(
|
|
46
45
|
base_url: string,
|
|
47
46
|
package_name: string,
|
|
48
|
-
resolve_marker
|
|
47
|
+
resolve_marker = package_name,
|
|
49
48
|
): string | undefined {
|
|
50
49
|
try {
|
|
51
|
-
return createRequire(base_url).resolve(resolve_marker
|
|
50
|
+
return createRequire(base_url).resolve(resolve_marker)
|
|
52
51
|
} catch {
|
|
53
52
|
return undefined
|
|
54
53
|
}
|