@joshuafolkken/kit 1.1455.0 → 1.1471.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/dependency-update/SKILL.md +26 -55
- package/.claude/skills/epic-commands/SKILL.md +87 -148
- package/.claude/skills/epic-commands/epic-bundle.md +45 -77
- package/.claude/skills/epic-commands/execution-waves.md +44 -81
- package/.claude/skills/verify-ui/SKILL.md +25 -50
- package/.claude/skills/workflow-commands/background-commands.md +36 -47
- package/.claude/skills/workflow-commands/backlogrun-child.md +72 -85
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +91 -121
- package/.claude/skills/workflow-commands/backlogrun-park.md +73 -117
- package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -9
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +11 -20
- package/.claude/skills/workflow-commands/backlogrun-steps.md +43 -52
- package/.claude/skills/workflow-commands/chain-rule.md +24 -34
- package/.claude/skills/workflow-commands/followup.md +57 -76
- package/.claude/skills/workflow-commands/issue-comments.md +43 -60
- package/.claude/skills/workflow-commands/kickoff.md +65 -74
- package/.claude/skills/workflow-commands/observation-filing.md +62 -107
- package/.claude/skills/workflow-commands/observation-ledger.md +56 -111
- package/.claude/skills/workflow-commands/pre-gate-cut.md +89 -138
- package/.claude/skills/workflow-commands/prerequisite.md +10 -15
- package/.claude/skills/workflow-commands/progress-watcher.md +21 -26
- package/.claude/skills/workflow-commands/retrospective.md +31 -50
- package/.claude/skills/workflow-commands/split-assessment.md +34 -58
- package/.claude/skills/workflow-commands/working-tree-hold.md +54 -71
- package/CLAUDE.md +6 -5
- package/dist/CLAUDE.md +6 -5
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-7WQBPLUN.js → chunk-43ZHT22P.js} +31 -15
- package/dist/hooks/{chunk-LIAFCXL7.js → chunk-AS74OU3D.js} +23 -7
- package/dist/hooks/{chunk-3C4XRYG6.js → chunk-E7OIKX5Z.js} +17 -12
- package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-EUMACUK2.js} +18 -12
- package/dist/hooks/{chunk-WNLVGT7E.js → chunk-H5HVWYXK.js} +4 -4
- package/dist/hooks/{chunk-HHI6FO3R.js → chunk-IIGFYXKC.js} +345 -240
- package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-J47HU5H5.js} +14 -3
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-K4EOONYM.js} +1 -1
- package/dist/hooks/{chunk-D5QBTOCZ.js → chunk-K4LMBQC2.js} +4 -4
- package/dist/hooks/{chunk-TBJFTSUY.js → chunk-KC5AK2GF.js} +8 -6
- package/dist/hooks/chunk-SHGPANFB.js +56 -0
- package/dist/hooks/{chunk-5H4SSDSD.js → chunk-YHBZAJSV.js} +3 -3
- package/dist/hooks/{chunk-HFGWYGUV.js → chunk-YYPI7SJZ.js} +5 -5
- package/dist/hooks/codex-hook-adapter.js +13 -13
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +3 -3
- package/dist/hooks/inputs.json +216 -214
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +6 -5
- package/dist/hooks/stop-guard.js +18 -14
- package/dist/josh.js +5 -6
- 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/package.json +3 -3
- package/prompts/coding-standards.md +12 -22
- package/prompts/collaboration-workflow/file-edits.md +26 -44
- package/prompts/collaboration-workflow/gh-rest.md +20 -36
- package/prompts/collaboration-workflow/glossary.md +5 -7
- package/prompts/collaboration-workflow/issue-citation.md +7 -13
- package/prompts/collaboration-workflow/issue-template.md +29 -29
- package/prompts/collaboration-workflow/operating-rules.md +35 -44
- 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 +50 -53
- package/prompts/collaboration-workflow/proposal-request.md +1 -9
- package/prompts/collaboration-workflow/report-format.md +32 -49
- package/prompts/collaboration-workflow/residency.md +10 -10
- package/prompts/collaboration-workflow/rule-delivery.md +9 -9
- 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 +24 -30
- package/prompts/collaboration-workflow.md +24 -25
- package/prompts/refactoring.md +33 -43
- package/prompts/review-rubric.md +84 -129
- package/prompts/review.md +111 -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-exit-record.ts +23 -1
- package/scripts/agent/codex-agent-argv.ts +7 -2
- package/scripts/backlog/backlog-budget-cli.ts +1 -2
- 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 +2 -4
- package/scripts/backlog/backlog-drive-restore.ts +19 -1
- package/scripts/backlog/backlog-drive-seats.ts +27 -0
- package/scripts/backlog/backlog-drive.ts +11 -6
- package/scripts/backlog/backlog-ready.ts +13 -6
- 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/investigation-guard.ts +1 -1
- package/scripts/delegation/investigation-reads.ts +1 -1
- 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 +27 -27
- package/scripts/document/document-byte-check.ts +1 -2
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/epic/epic-audit-rationale.ts +8 -2
- package/scripts/epic/epic-bundle-cli.ts +1 -2
- package/scripts/epic/epic-chains.ts +6 -2
- package/scripts/epic/epic-close-comment.ts +1 -2
- package/scripts/gate/verification-gate.ts +7 -2
- package/scripts/gh/git-pr.ts +6 -2
- 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/hooks/format-edited-file.ts +10 -4
- package/scripts/hooks/pre-push-unit.ts +15 -4
- package/scripts/hooks/pretool-guard.ts +23 -29
- package/scripts/hooks/step-zero-notice.ts +3 -2
- package/scripts/init/hook-command-rewrite.ts +6 -2
- package/scripts/init/init-logic-deploy-vps.ts +4 -2
- package/scripts/issue/behavior-change-lint.ts +12 -6
- package/scripts/issue/issue-scout-cli.ts +3 -2
- package/scripts/josh/file-map-stamp.ts +1 -3
- package/scripts/josh/hook-decision.ts +17 -2
- package/scripts/lane/lane-await.ts +3 -2
- package/scripts/lane/lane-child-invocation.ts +14 -1
- package/scripts/lane/lane-guard-policy.ts +4 -4
- package/scripts/lane/lane-ledger.ts +9 -0
- package/scripts/lane/lane-load.ts +9 -2
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-parked.ts +18 -0
- package/scripts/lane/lane-process-load.ts +125 -0
- package/scripts/lane/lane-process-stats.ts +93 -0
- package/scripts/lane/lane-registry.ts +8 -3
- package/scripts/lane/lane-session-broker.ts +29 -5
- package/scripts/lane/lane-spent.ts +14 -3
- package/scripts/lane/lane-stats.ts +1 -0
- package/scripts/managed-marker/managed-marker-logic.ts +1 -2
- 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-lint.ts +7 -4
- package/scripts/review/review-record-cli.ts +6 -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 +3 -6
- package/scripts/rules/git-force.ts +5 -9
- package/scripts/rules/implementation-cut.ts +37 -35
- package/scripts/rules/index-guard.ts +4 -7
- package/scripts/rules/issue-comments.ts +5 -15
- 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/oracle-consulted.ts +4 -5
- package/scripts/rules/piped-verification.ts +81 -31
- 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/run-tail-rule.ts +6 -22
- package/scripts/rules/shell-body-trigger.ts +4 -6
- package/scripts/rules/shell-segments.ts +7 -0
- package/scripts/rules/stop-rules.ts +16 -29
- 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-github.ts +11 -3
- package/scripts/run/board/run-board-idle.ts +31 -0
- package/scripts/run/board/run-board-read.ts +9 -12
- package/scripts/run/board/run-board-reason.ts +7 -3
- package/scripts/run/board/run-board-state.ts +4 -0
- package/scripts/run/board/run-board-status.ts +18 -3
- package/scripts/run/board/run-board-thanks.ts +7 -2
- package/scripts/run/board/run-board-tick.ts +32 -7
- 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 +15 -20
- 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 +13 -0
- package/scripts/run/cut/run-cut.ts +10 -1
- package/scripts/run/hold/run-hold-cli.ts +14 -3
- package/scripts/run/hold/run-hold.ts +6 -3
- package/scripts/run/merge/run-merge-cli.ts +30 -7
- package/scripts/run/merge/run-merge-ship.ts +59 -1
- package/scripts/run/merge/run-merge.ts +3 -2
- package/scripts/run/run-ending.ts +31 -3
- package/scripts/run/run-parent-cut-hook.ts +5 -10
- package/scripts/run/run-preflight.ts +1 -2
- package/scripts/run/run-stop-notify.ts +1 -2
- package/scripts/run/run-watcher-guard.ts +6 -3
- package/scripts/run/ship/run-ship-review-steps.ts +13 -4
- package/scripts/run/ship/run-ship-review.ts +15 -2
- 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/unit-worker-share.ts +3 -2
- 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 +10 -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/dist/hooks/chunk-UT7YC3N2.js +0 -56
package/prompts/testing-guide.md
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## 0. Per-Requirement Test Coverage (MANDATORY)
|
|
4
4
|
|
|
5
|
-
Before
|
|
5
|
+
Before any implementation code, **list every requirement from the user's request and assign a test to each one**.
|
|
6
6
|
|
|
7
7
|
### Planning template
|
|
8
8
|
|
|
9
|
-
For a user request with N requirements, enumerate each one before touching any source files:
|
|
10
|
-
|
|
11
9
|
```text
|
|
12
10
|
Req 1: "Label chip shown immediately"
|
|
13
11
|
→ E2E: open editor, type new label, press Enter → chip visible without blurring
|
|
@@ -21,19 +19,17 @@ Req N: ...
|
|
|
21
19
|
|
|
22
20
|
### Rules
|
|
23
21
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
22
|
+
- User-facing behavior change (UI interaction, keyboard shortcut, visible state) → an E2E test.
|
|
23
|
+
- Logic/utility change (pure functions, filters, transforms) → a unit test. Bug fix → a regression test; refactor → tests pinning the existing behavior BEFORE the change (`prompts/refactoring.md`).
|
|
26
24
|
- **Non-runtime updates (pre-approved manual-only exception):** a change touching no executable runtime code path — docs, non-executable config, editor/IDE files, cosmetic asset swaps with no selector/path change — may be verified by hand; declare it in Step 0 with the reason. The mechanically exempt (`*.md`, `.editorconfig`, `.idea/**`, `.vscode/**`, `prompts/**`) need no declaration. A runtime change whose test is infeasible needs the reason and user approval.
|
|
27
|
-
- **basic で kit がテストできない言語だけの変更:** `package.json` に `josh.profile: basic`(改名前の `static`
|
|
28
|
-
-
|
|
29
|
-
-
|
|
25
|
+
- **basic で kit がテストできない言語だけの変更:** `package.json` に `josh.profile: basic`(改名前の `static` も同じ)があり、変更が HTML/CSS や Lua などのソースだけなら自動テストは不要。HTML/CSS はブラウザーで対象ページを開き、想定する画面幅でレイアウト・リンク・HTML 内の操作を確認する。そのほかのソースは変更した処理を実際に動かす。どちらも結果を記録する。JavaScript または TypeScript のファイルを含む変更は自動テストを行う。
|
|
26
|
+
- Test infeasible (e.g., a native OS date-picker Playwright cannot drive) → comment why in the test file and test the closest observable behavior (e.g., the editor stays open after `change`).
|
|
27
|
+
- A requirement without its test is not done.
|
|
30
28
|
|
|
31
29
|
### Verification checklist (add to the completion gate)
|
|
32
30
|
|
|
33
|
-
- [ ]
|
|
34
|
-
- [ ]
|
|
35
|
-
- [ ] The two counts match (or each gap is documented as infeasible with a comment).
|
|
36
|
-
- [ ] **UI changes only:** a screenshot of the affected screen was captured and visually confirmed before reporting completion (passing tests are not proof the UI looks correct — see the **UI verification (screenshot)** rule in the completion gate). If a screenshot is impossible in this environment, that is stated and the user is asked to verify visually.
|
|
31
|
+
- [ ] Requirement count equals the count of tests added/updated (each gap documented as infeasible).
|
|
32
|
+
- [ ] UI changes: a screenshot of the affected screen was checked (`CLAUDE.md` → UI verification); if impossible here, say so and ask the user to check.
|
|
37
33
|
|
|
38
34
|
---
|
|
39
35
|
|
|
@@ -44,21 +40,19 @@ Req N: ...
|
|
|
44
40
|
| `src/routes/` pages, UI components with user interaction | E2E (Playwright) |
|
|
45
41
|
| `.ts`/`.js` utilities, `src/lib/server/`, display-only components | Unit/Integration (Vitest) |
|
|
46
42
|
|
|
47
|
-
**The type is decided from the path, not by asking
|
|
43
|
+
**The type is decided from the path, not by asking**: under `src/routes/` → E2E; every other runtime path → Unit (an interactive component is reached through its route). `pnpm josh test:declared` prints the type per untested runtime file on its `required` line.
|
|
48
44
|
|
|
49
45
|
### Test file naming & placement (one unambiguous rule)
|
|
50
46
|
|
|
51
|
-
There is exactly **one** convention for every test file — never choose between two. The rules below are mandatory; some are config-enforced (a violating file runs under the wrong vitest project or not at all), others are enforced by convention/review — each rule states which.
|
|
52
|
-
|
|
53
47
|
| Test kind | Required filename | Routed to |
|
|
54
48
|
| ------------------- | ------------------------ | -------------------------------- |
|
|
55
49
|
| Unit / integration | `*.test.ts` | node/unit vitest project |
|
|
56
50
|
| Component / browser | `*.svelte.test.ts` | browser/component vitest project |
|
|
57
51
|
| E2E | `src/routes/**/*.e2e.ts` | Playwright |
|
|
58
52
|
|
|
59
|
-
- **
|
|
60
|
-
- **
|
|
61
|
-
- **Colocate
|
|
53
|
+
- **Never `*.spec.ts`** — lint-enforced by `eslint/rules/test-filename.js` (wired into `eslint/base.js`) for every JS/TS extension; rationale: `docs/maintainers/testing-guide-history.md` → "Why the `*.spec.*` ban is a lint rule".
|
|
54
|
+
- **Keep the `.svelte.` infix** on component/browser tests: `*.svelte.test.ts` matches the browser project (`include: src/**/*.svelte.{test,spec}.{js,ts}`), plain `*.test.ts` the node project (`include: src/**/*.{test,spec}.{js,ts}`); dropping it silently moves the file to the wrong project.
|
|
55
|
+
- **Colocate** each test beside its code (`foo.test.ts` next to `foo.ts`); no top-level `tests/` (lint-enforced by the same rule). E2E specs go under `src/routes/**` — enforced by review only, since `playwright.config.ts` `testMatch: '**/*.e2e.{ts,js}'` runs them anywhere.
|
|
62
56
|
|
|
63
57
|
---
|
|
64
58
|
|
|
@@ -72,156 +66,111 @@ There is exactly **one** convention for every test file — never choose between
|
|
|
72
66
|
### Test Functions
|
|
73
67
|
|
|
74
68
|
- Outside `describe`: `test`; inside `describe`: `it`
|
|
75
|
-
-
|
|
69
|
+
- `describe` only when grouping multiple tests
|
|
76
70
|
|
|
77
71
|
### Parameterized Tests
|
|
78
72
|
|
|
79
73
|
- Playwright: `for` loop; Vitest: `test.each` / `it.each`
|
|
80
|
-
-
|
|
74
|
+
- Cases: a `cases` or `*_cases` array at file top, outside test functions
|
|
81
75
|
|
|
82
76
|
### Constants
|
|
83
77
|
|
|
84
|
-
-
|
|
85
|
-
- Place after imports at file top
|
|
78
|
+
- Magic numbers/strings except `0`, `1`, `-1` → `UPPER_SNAKE_CASE` constants after the imports
|
|
86
79
|
|
|
87
80
|
### Imports
|
|
88
81
|
|
|
89
|
-
- Always
|
|
82
|
+
- Always the `.js` extension: `import { foo } from './bar.js'`
|
|
90
83
|
- Type imports: `import { type Foo } from '...'`
|
|
91
84
|
|
|
92
85
|
### Playwright
|
|
93
86
|
|
|
94
|
-
-
|
|
87
|
+
- Select with `page.getByTestId('id')`; add `data-testid` to the implementation
|
|
95
88
|
- Test functions always `async`
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
89
|
+
- `playwright.config.ts` projects, each depending on the previous: `e2e-guest` (no auth: `src/routes/**/dash-guest.e2e.ts`, `src/routes/**/demo.e2e.ts`) → `e2e-main` → `e2e-leak-check`. Authenticated ones restore `storageState` from `src/routes/.auth/user.json` when it exists — no UI login per test.
|
|
90
|
+
- `workers: 1` — authenticated E2E shares one storage state and DB user.
|
|
91
|
+
- Guest/main/leak projects set per-project test timeouts so tab switches and dash actions are not cut off.
|
|
99
92
|
|
|
100
93
|
### Regression fix workflow (E2E data / cleanup bugs)
|
|
101
94
|
|
|
102
|
-
When
|
|
95
|
+
When tests leave persistent state behind:
|
|
103
96
|
|
|
104
|
-
1.
|
|
105
|
-
2.
|
|
106
|
-
3.
|
|
107
|
-
4.
|
|
97
|
+
1. Add or extend a guard and confirm it fails (e.g. `dash_leak_guard.scrub_then_assert_clean` in `src/routes/dash/dash-leak-check.e2e.ts` scrubs `E2E_`-prefixed tasks, then asserts none remain).
|
|
98
|
+
2. Fix production code or teardown — prefer `data-testid` over locale-specific `aria-label`; do not swallow cleanup errors unless intended.
|
|
99
|
+
3. Confirm green with full `pnpm test:e2e` (or a targeted Playwright run).
|
|
100
|
+
4. Document any new invariant here or in the E2E helper module.
|
|
108
101
|
|
|
109
102
|
### Error Messages
|
|
110
103
|
|
|
111
|
-
-
|
|
104
|
+
- State expected and actual values
|
|
112
105
|
- Wrap `number` in template literals with `String()`
|
|
113
|
-
-
|
|
106
|
+
- No `?? ''` unless the value can be `undefined`
|
|
114
107
|
|
|
115
108
|
### Type Safety
|
|
116
109
|
|
|
117
|
-
- No `any`;
|
|
118
|
-
-
|
|
110
|
+
- No `any`; use every loop variable
|
|
111
|
+
- `@ts-expect-error`, not `@ts-ignore`
|
|
119
112
|
|
|
120
113
|
### Statement Limit
|
|
121
114
|
|
|
122
|
-
- Max 10 statements per test function; split
|
|
115
|
+
- Max 10 statements per test function; split beyond that
|
|
123
116
|
|
|
124
117
|
### No live network
|
|
125
118
|
|
|
126
|
-
- **A unit test must not reach the network
|
|
127
|
-
- **Mock the whole read
|
|
128
|
-
-
|
|
119
|
+
- **A unit test must not reach the network** — mock every read leaving the process (`gh` call, HTTP request, CLI subprocess that makes one).
|
|
120
|
+
- **Mock the whole read**: a stub of one call leaving a second on its default still passes, slowly and against the remote (joshuafolkken/kit#1353: a 10-second CI timeout).
|
|
121
|
+
- kit's `vitest.config.ts` fails such a run, listing the `gh` invocations (`docs/josh-commands.md` → `josh test:unit`); elsewhere the rule holds unguarded.
|
|
129
122
|
|
|
130
123
|
### A josh command that writes state runs through the integration harness (kit only)
|
|
131
124
|
|
|
132
|
-
-
|
|
125
|
+
- A new or changed state-writing josh command needs a test in all three environments of `scripts/test/josh-harness.ts` — kit, a consumer without `docs/`, a lane worktree (joshuafolkken/kit#2402). Packed suite: `vitest.harness.config.ts`
|
|
133
126
|
|
|
134
127
|
---
|
|
135
128
|
|
|
136
129
|
## 3. Checklist
|
|
137
130
|
|
|
138
|
-
- [ ]
|
|
139
|
-
- [ ]
|
|
140
|
-
- [ ] `.js`
|
|
141
|
-
- [ ] Playwright: `data-testid` selectors only
|
|
142
|
-
- [ ] `async`/`await` used correctly
|
|
143
|
-
- [ ] No `any`; all loop variables used
|
|
144
|
-
- [ ] Max 10 statements per test function
|
|
145
|
-
- [ ] `read_lints` run; 0 errors before reporting done
|
|
146
|
-
- [ ] No unnecessary `?? ''`; `String()` for numbers in template literals
|
|
131
|
+
- [ ] `pnpm josh gate`, then E2E per § 6: the CI E2E job where a pull request is open; where there is none, run `pnpm josh test:e2e` with `CI` unset (DEV: `pnpm run dev` on the port `playwright.config.ts` derives from `PORT_SEED`); no `pnpm build` or `CI=true` unless asked. Playwright cannot run here → report the E2E result missing and the gate not closed
|
|
132
|
+
- [ ] `read_lints`: 0 errors on touched files; Playwright: no failures and no flaky runs
|
|
133
|
+
- [ ] § 2 holds: constants, `.js` imports, `data-testid` selectors, correct `async`/`await`, no `any`, ≤10 statements, `String()` / no needless `?? ''`
|
|
147
134
|
|
|
148
135
|
---
|
|
149
136
|
|
|
150
137
|
## 4. Reference Files
|
|
151
138
|
|
|
152
|
-
|
|
153
|
-
**Unit:** `src/lib/data/phrases/phrases.test.ts`, `src/lib/data/praise-audio.test.ts`
|
|
154
|
-
**ESLint:** `eslint.config.js`
|
|
139
|
+
E2E: `src/routes/**/page.e2e.ts`, `src/routes/**/praise.e2e.ts` · Unit: `src/lib/data/phrases/phrases.test.ts`, `src/lib/data/praise-audio.test.ts` · ESLint: `eslint.config.js`
|
|
155
140
|
|
|
156
141
|
---
|
|
157
142
|
|
|
158
143
|
## 5. Unit Test Example — Stateful Functions
|
|
159
144
|
|
|
160
|
-
|
|
161
|
-
import { expect, test } from 'vitest'
|
|
162
|
-
import { get_praise_audio_file, reset_praise_audio_index } from './praise-audio.js'
|
|
163
|
-
|
|
164
|
-
test('returns expected values in sequence', () => {
|
|
165
|
-
reset_praise_audio_index() // reset state at the start of each test
|
|
166
|
-
|
|
167
|
-
const first_result = get_praise_audio_file()
|
|
168
|
-
expect(first_result).toBe('expected-value-1')
|
|
169
|
-
|
|
170
|
-
const second_result = get_praise_audio_file()
|
|
171
|
-
expect(second_result).toBe('expected-value-2')
|
|
172
|
-
})
|
|
173
|
-
```
|
|
145
|
+
Reset module state at the start of each test (`reset_praise_audio_index()` in `src/lib/data/praise-audio.test.ts`), then assert each call of the sequence in order.
|
|
174
146
|
|
|
175
147
|
---
|
|
176
148
|
|
|
177
149
|
## 6. Closing the E2E gate without a human run
|
|
178
150
|
|
|
179
|
-
No human E2E run closes the gate, and nothing a red local run stopped is let through
|
|
180
|
-
(`docs/maintainers/testing-guide-history.md`).
|
|
151
|
+
No human E2E run closes the gate, and nothing a red local run stopped is let through (`docs/maintainers/testing-guide-history.md`).
|
|
181
152
|
|
|
182
153
|
### Which result closes the gate
|
|
183
154
|
|
|
184
|
-
Decided by whether a pull request is open
|
|
155
|
+
Decided by whether a pull request is open:
|
|
185
156
|
|
|
186
157
|
| Situation | What closes the gate |
|
|
187
158
|
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
188
159
|
| A pull request is open — `fullrun`, `prrun`, `backlogrun` | The CI E2E job. `pnpm josh followup` waits for the checks and refuses to merge while any of them is non-passing. |
|
|
189
160
|
| No pull request — `halfrun`'s stop before commit, a completion reported outside any workflow | `pnpm josh test:e2e`, run by **you**, output read by you. |
|
|
190
161
|
|
|
191
|
-
**Never ask the user to run it
|
|
192
|
-
somebody being at the keyboard; that dependency is what made a `fullrun` chain stall at its last
|
|
193
|
-
step. And never report completion on an E2E result nobody read — an unread result is not a pass.
|
|
162
|
+
**Never ask the user to run it**, in either row, and never report completion on an unread E2E result — unread is not a pass.
|
|
194
163
|
|
|
195
|
-
|
|
196
|
-
for the second row: `@joshuafolkken/app-kit` ships `pnpm josh-app verify`, which builds once and
|
|
197
|
-
boots once to run the E2E suite and the DAST scan against that one server — and which delegates E2E
|
|
198
|
-
to `josh test:e2e`, so the skip below still applies. **Decide by the command list the installed
|
|
199
|
-
toolkit prints, not by which toolkit is installed and not by a version number written here**: the
|
|
200
|
-
same rule the UI gate uses, and the only one that survives a release.
|
|
164
|
+
Second row: prefer an integrated gate when the installed toolkit's printed command list has one (not by toolkit or version) — `@joshuafolkken/app-kit`'s `pnpm josh-app verify` builds and boots once for E2E and the DAST scan, delegating E2E to `josh test:e2e` (so the skip below applies).
|
|
201
165
|
|
|
202
166
|
### A project with no E2E suite
|
|
203
167
|
|
|
204
|
-
`pnpm josh test:e2e` runs through `scripts/test/test-e2e-guard.ts`, which
|
|
205
|
-
the reason, when `@playwright/test` is not installed or no `*.e2e.{ts,js}` file exists. That skip is
|
|
206
|
-
a **defined** outcome, not an accident: a project with no E2E suite has nothing for the gate to
|
|
207
|
-
read, and the run says so out loud. **A skip you did not see printed is not one** — it is an unread
|
|
208
|
-
result, and it falls under the previous paragraph.
|
|
168
|
+
`pnpm josh test:e2e` runs through `scripts/test/test-e2e-guard.ts`, which skips and exits 0, naming the reason, when `@playwright/test` is not installed or no `*.e2e.{ts,js}` file exists. That is a defined outcome; **a skip you did not see printed is an unread result**.
|
|
209
169
|
|
|
210
|
-
|
|
211
|
-
`templates/workflows/ci.yml` enables the `e2e` job when any `*.e2e.{ts,js}` exists outside
|
|
212
|
-
`node_modules` — the guard's own glob — whatever the config's filename;
|
|
213
|
-
`scripts/ci/ci-yml-e2e-detect.test.ts` pins the agreement. CI enables the job on the spec alone, even
|
|
214
|
-
where the guard skips for a missing `@playwright/test`, so a spec that cannot run ends red rather than
|
|
215
|
-
silently green.
|
|
170
|
+
CI uses the same rule: the `e2e-detect` job in `templates/workflows/ci.yml` enables the `e2e` job when any `*.e2e.{ts,js}` exists outside `node_modules` (the guard's glob), whatever the config's filename — on the spec alone, so a spec that cannot run ends red, not silently green. `scripts/ci/ci-yml-e2e-detect.test.ts` pins the agreement.
|
|
216
171
|
|
|
217
172
|
### The gate is not weakened
|
|
218
173
|
|
|
219
|
-
- **A non-passing check keeps the merge closed.** `evaluate_pr_state` returns `success` only when
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
ends the wait on the poll that sees it, by name (`PR checks failed (failed checks: E2E).`).
|
|
223
|
-
- **Filtering, narrowing or reinterpreting E2E output to get past it is a workaround** —
|
|
224
|
-
`CLAUDE.md` → "Cross-package problems"; reporting the filtered result honestly does not make it
|
|
225
|
-
compliant.
|
|
226
|
-
- **`E2E` stays off the default required-check list**; a project opts in through
|
|
227
|
-
`JOSH_REQUIRED_CHECKS`. `scripts/gh/git-pr-checks-e2e-gate.test.ts` pins both properties.
|
|
174
|
+
- **A non-passing check keeps the merge closed.** `evaluate_pr_state` returns `success` only when GitHub reports the pull request `CLEAN` and every required check passes; the only exception is a non-passing set made entirely of CodeRabbit checks (the temporary kit#753 policy). A failing check ends the wait on the poll that sees it, by name (`PR checks failed (failed checks: E2E).`).
|
|
175
|
+
- **Filtering, narrowing or reinterpreting E2E output to get past it is a workaround** (`CLAUDE.md` → "Cross-package problems"), however honestly reported.
|
|
176
|
+
- `E2E` stays off the default required-check list; a project opts in through `JOSH_REQUIRED_CHECKS`. `scripts/gh/git-pr-checks-e2e-gate.test.ts` pins both properties.
|
|
@@ -65,8 +65,7 @@ function declared_toolkits(project_root: string): ReadonlyArray<string> {
|
|
|
65
65
|
// declares one, so a toolkit shipping several CLIs still resolves to one deterministically.
|
|
66
66
|
function pick_bin_name(package_name: string, bin: string | Record<string, string>): string {
|
|
67
67
|
const unscoped = path.basename(package_name)
|
|
68
|
-
if (typeof bin === 'string') return unscoped
|
|
69
|
-
if (bin[unscoped] !== undefined) return unscoped
|
|
68
|
+
if (typeof bin === 'string' || bin[unscoped] !== undefined) return unscoped
|
|
70
69
|
|
|
71
70
|
return Object.keys(bin)[0] ?? unscoped
|
|
72
71
|
}
|
|
@@ -99,8 +98,7 @@ function has_runnable_cli(project_root: string, bin_name: string): boolean {
|
|
|
99
98
|
// spawning it would fail the sync step rather than say what is wrong.
|
|
100
99
|
function resolve_toolkit(project_root: string, package_name: string): Release | undefined {
|
|
101
100
|
const bin_name = read_bin_name(project_root, package_name)
|
|
102
|
-
if (bin_name === undefined) return undefined
|
|
103
|
-
if (!has_runnable_cli(project_root, bin_name)) return undefined
|
|
101
|
+
if (bin_name === undefined || !has_runnable_cli(project_root, bin_name)) return undefined
|
|
104
102
|
|
|
105
103
|
return { package_name, version: LATEST_VERSION, bin_name }
|
|
106
104
|
}
|
|
@@ -20,6 +20,28 @@ function read_exit_record(transcript: string): ClaudeResultEvent | undefined {
|
|
|
20
20
|
.findLast((record) => record !== undefined)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
function event_type(line: string): unknown {
|
|
24
|
+
const value = json_value.parse_or_undefined(line)
|
|
25
|
+
|
|
26
|
+
return typeof value === 'object' && value !== null && 'type' in value ? value.type : undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The Claude stream events a turn is made of — a Codex transcript's own (`turn.completed`, `error`)
|
|
30
|
+
// are none of these, so one never reads as killed.
|
|
31
|
+
const TURN_TYPES: ReadonlySet<unknown> = new Set(['system', 'assistant', 'user', 'stream_event'])
|
|
32
|
+
|
|
33
|
+
// Whether a Claude session was killed inside a turn: its last event is a turn's, not a `result`. A
|
|
34
|
+
// finished turn ends on a result; a background task can wake the session for another turn, and a
|
|
35
|
+
// process killed before that turn's result leaves other events last.
|
|
36
|
+
function is_mid_turn(transcript: string): boolean {
|
|
37
|
+
const last = transcript
|
|
38
|
+
.split('\n')
|
|
39
|
+
.map((line) => event_type(line))
|
|
40
|
+
.findLast((type) => type !== undefined)
|
|
41
|
+
|
|
42
|
+
return TURN_TYPES.has(last)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const agent_exit_record = { is_mid_turn, read_exit_record }
|
|
24
46
|
|
|
25
47
|
export { agent_exit_record }
|
|
@@ -47,8 +47,13 @@ function ledger_arguments(
|
|
|
47
47
|
common_directory: string | undefined,
|
|
48
48
|
profile: AgentProfile,
|
|
49
49
|
): ReadonlyArray<string> {
|
|
50
|
-
if (
|
|
51
|
-
|
|
50
|
+
if (
|
|
51
|
+
common_directory === undefined ||
|
|
52
|
+
profile.role !== agent_role_profile.WORKER ||
|
|
53
|
+
path.basename(common_directory) !== GIT_DIRECTORY
|
|
54
|
+
) {
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
52
57
|
|
|
53
58
|
const directory = path.join(path.dirname(common_directory), LEDGER_DIRECTORY)
|
|
54
59
|
|
|
@@ -157,8 +157,7 @@ function build_input(values: ParsedValues, now_ms: number): BudgetInput | undefi
|
|
|
157
157
|
|
|
158
158
|
const started_at_ms = to_time_ms(cli_flags.string_of(values.started))
|
|
159
159
|
|
|
160
|
-
if (started_at_ms === undefined) return undefined
|
|
161
|
-
if (!are_values_readable(values)) return undefined
|
|
160
|
+
if (started_at_ms === undefined || !are_values_readable(values)) return undefined
|
|
162
161
|
|
|
163
162
|
return input_of(values, { answer, now_ms, started_at_ms })
|
|
164
163
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { has_label_name, IN_PROGRESS_LABEL } from '#scripts/issue/issue-labels'
|
|
2
|
+
import { issue_state_cli } from '#scripts/issue/issue-state-cli'
|
|
3
|
+
import type { LaneInfo } from '#scripts/lane/lane-registry'
|
|
4
|
+
|
|
5
|
+
// The open lanes an earlier invocation dispatched that still claim their issue: `in-progress`, and not
|
|
6
|
+
// stopped for a person's review. Their unit may have died across the hand-off, so the drive collects
|
|
7
|
+
// them; a lane parked or held for review is a person's and is left alone.
|
|
8
|
+
|
|
9
|
+
async function is_claimed(issue: string): Promise<boolean> {
|
|
10
|
+
const read = await issue_state_cli.read_issue(issue)
|
|
11
|
+
|
|
12
|
+
if (read.kind !== 'state') return false
|
|
13
|
+
|
|
14
|
+
return has_label_name(read.state.labels, IN_PROGRESS_LABEL) && !read.state.is_human_review
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function carried(lanes: ReadonlyArray<LaneInfo>): Promise<ReadonlyArray<string>> {
|
|
18
|
+
const dispatched = lanes.filter((lane) => lane.output !== undefined).map((lane) => lane.issue)
|
|
19
|
+
const claims = await Promise.all(dispatched.map(async (issue) => await is_claimed(issue)))
|
|
20
|
+
|
|
21
|
+
return dispatched.filter((_issue, index) => claims[index] === true)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const backlog_drive_carried = { carried }
|
|
25
|
+
|
|
26
|
+
export { backlog_drive_carried }
|
|
@@ -6,7 +6,7 @@ import { issue_cite } from '#scripts/issue/issue-cite'
|
|
|
6
6
|
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
7
7
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
8
8
|
import { lane_await, type AwaitState } from '#scripts/lane/lane-await'
|
|
9
|
-
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
9
|
+
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
10
10
|
import { lane_sampler } from '#scripts/lane/lane-sampler'
|
|
11
11
|
import { error_text } from '#scripts/lib/error-message'
|
|
12
12
|
import { json_value } from '#scripts/lib/json-value'
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type OfferRead,
|
|
25
25
|
} from './backlog-drive'
|
|
26
26
|
import { backlog_drive_args, type DriveContext } from './backlog-drive-args'
|
|
27
|
+
import { backlog_drive_carried } from './backlog-drive-carried'
|
|
27
28
|
import { backlog_drive_finish } from './backlog-drive-finish'
|
|
28
29
|
import { backlog_drive_launch } from './backlog-drive-launch'
|
|
29
30
|
import { backlog_drive_named } from './backlog-drive-named'
|
|
@@ -32,6 +33,7 @@ import { backlog_drive_offer_argv } from './backlog-drive-offer-argv'
|
|
|
32
33
|
import { backlog_drive_owner } from './backlog-drive-owner'
|
|
33
34
|
import { backlog_drive_restore } from './backlog-drive-restore'
|
|
34
35
|
import { backlog_drive_retrospective } from './backlog-drive-retrospective'
|
|
36
|
+
import { backlog_drive_seats } from './backlog-drive-seats'
|
|
35
37
|
import { backlog_offer_cli } from './backlog-offer-cli'
|
|
36
38
|
import { backlog_ready } from './backlog-ready'
|
|
37
39
|
|
|
@@ -153,10 +155,10 @@ function finished_checker(seeded: ReadonlyArray<string>): (issue: string) => boo
|
|
|
153
155
|
}
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
async function open_lanes(): Promise<ReadonlyArray<
|
|
158
|
+
async function open_lanes(): Promise<ReadonlyArray<LaneInfo>> {
|
|
157
159
|
const lanes = await lane_registry.list_lanes()
|
|
158
160
|
|
|
159
|
-
return lanes.filter((lane) => !lane.is_stranded)
|
|
161
|
+
return lanes.filter((lane) => !lane.is_stranded)
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
function end_line(end: DriveEnd): string {
|
|
@@ -190,6 +192,7 @@ function ports_of(
|
|
|
190
192
|
offer: async (state) => await read_offer(state, context),
|
|
191
193
|
launch: async (issue) => await backlog_drive_launch.launch(issue, context.owner),
|
|
192
194
|
free_lanes: backlog_ready.drive_free_lane_count,
|
|
195
|
+
has_lane: backlog_drive_seats.has_kept_lane,
|
|
193
196
|
judgement_owed: judgement_reader(),
|
|
194
197
|
now: () => new Date(),
|
|
195
198
|
sleep: async (milliseconds) => {
|
|
@@ -211,9 +214,11 @@ async function seeded_lanes(context: DriveContext): Promise<ReadonlyArray<string
|
|
|
211
214
|
|
|
212
215
|
await backlog_drive_named.reconcile(carry, events, context.owner)
|
|
213
216
|
|
|
214
|
-
const
|
|
217
|
+
const open = await open_lanes()
|
|
218
|
+
const lanes = [...new Set([...open.map((lane) => lane.issue), ...context.awaited])]
|
|
219
|
+
const dispatched = await backlog_drive_carried.carried(open)
|
|
215
220
|
|
|
216
|
-
return backlog_drive_restore.restore(lanes, events, carry.merged_issues ?? [])
|
|
221
|
+
return backlog_drive_restore.restore(lanes, events, carry.merged_issues ?? [], dispatched)
|
|
217
222
|
}
|
|
218
223
|
|
|
219
224
|
// The interval between two asks is `backlog:budget`'s own, so the driver keeps the one its reason prints.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { issue_state_cli } from '#scripts/issue/issue-state-cli'
|
|
1
|
+
import { lane_parked } from '#scripts/lane/lane-parked'
|
|
3
2
|
import { run_carry, type RunCarry } from '#scripts/run/carry/run-carry'
|
|
4
3
|
import type { RunEvent } from '#scripts/run/event/run-event-stream'
|
|
5
4
|
import type { MergeResult } from '#scripts/run/merge/run-merge-cli'
|
|
@@ -98,9 +97,8 @@ function skip_after_failure(target: string, issue: string, owner: string): void
|
|
|
98
97
|
async function should_skip(issue: string, outcome: MergeResult['outcome']): Promise<boolean> {
|
|
99
98
|
if (outcome === 'failed') return true
|
|
100
99
|
if (outcome !== 'parked') return false
|
|
101
|
-
const read = await issue_state_cli.read_issue(issue)
|
|
102
100
|
|
|
103
|
-
return
|
|
101
|
+
return await lane_parked.is_parked(issue)
|
|
104
102
|
}
|
|
105
103
|
|
|
106
104
|
async function mark_done(issue: string, result: MergeResult, owner: string): Promise<void> {
|
|
@@ -54,14 +54,32 @@ function parked_issues(events: ReadonlyArray<RunEvent>): ReadonlySet<number> {
|
|
|
54
54
|
)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// The issues this invocation launched or settled — any other dispatched lane was carried in from an
|
|
58
|
+
// earlier invocation.
|
|
59
|
+
function mentioned_issues(events: ReadonlyArray<RunEvent>): ReadonlySet<string> {
|
|
60
|
+
const tracked = events.filter(
|
|
61
|
+
(event) =>
|
|
62
|
+
event.kind === run_event_stream.EVENT_KIND.CHILD_LAUNCH || SETTLED_KINDS.has(event.kind),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return new Set(tracked.map((event) => issue_of(event)).filter((issue) => issue !== undefined))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The lanes still in flight: launched by this invocation and not settled, or dispatched by an earlier
|
|
69
|
+
// one and never seen by this one — a lane nobody collects otherwise waits forever.
|
|
57
70
|
function restore(
|
|
58
71
|
lanes: ReadonlyArray<string>,
|
|
59
72
|
events: ReadonlyArray<RunEvent>,
|
|
60
73
|
merged_issues: ReadonlyArray<number>,
|
|
74
|
+
dispatched: ReadonlyArray<string> = [],
|
|
61
75
|
): ReadonlyArray<string> {
|
|
62
76
|
const active = active_issues(events)
|
|
77
|
+
const mentioned = mentioned_issues(events)
|
|
78
|
+
const carried = new Set(dispatched.filter((issue) => !mentioned.has(issue)))
|
|
63
79
|
|
|
64
|
-
return lanes.filter(
|
|
80
|
+
return lanes.filter(
|
|
81
|
+
(issue) => (active.has(issue) || carried.has(issue)) && !merged_issues.includes(Number(issue)),
|
|
82
|
+
)
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
export const backlog_drive_restore = { active_issues, restore, settled_issues, parked_issues }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
2
|
+
import type { DrivePorts } from './backlog-drive'
|
|
3
|
+
|
|
4
|
+
type SeatPorts = Pick<DrivePorts, 'free_lanes' | 'has_lane'>
|
|
5
|
+
|
|
6
|
+
// The issues a pass launches: every one that already holds a kept lane, which it reuses, then as many
|
|
7
|
+
// of the rest as there are free lanes.
|
|
8
|
+
async function seated(issues: ReadonlyArray<string>, ports: SeatPorts): Promise<Array<string>> {
|
|
9
|
+
const kept = await Promise.all(issues.map(async (issue) => await ports.has_lane(issue)))
|
|
10
|
+
const fresh = issues.filter((_issue, index) => kept[index] !== true)
|
|
11
|
+
|
|
12
|
+
return [
|
|
13
|
+
...issues.filter((_issue, index) => kept[index] === true),
|
|
14
|
+
...fresh.slice(0, await ports.free_lanes()),
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// A kept lane is one a child was dispatched into — `lane:launch` reuses it rather than opening one.
|
|
19
|
+
async function has_kept_lane(issue: string): Promise<boolean> {
|
|
20
|
+
const lane = await lane_registry.find_open_lane(issue)
|
|
21
|
+
|
|
22
|
+
return lane?.output !== undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const backlog_drive_seats = { has_kept_lane, seated }
|
|
26
|
+
|
|
27
|
+
export { backlog_drive_seats }
|
|
@@ -20,6 +20,7 @@ import { issue_cite } from '#scripts/issue/issue-cite'
|
|
|
20
20
|
import type { LaunchOutcome } from '#scripts/lane/lane-launch-cli'
|
|
21
21
|
import type { MergeResult } from '#scripts/run/merge/run-merge-cli'
|
|
22
22
|
import { run_merge_token, type MergeToken } from '#scripts/run/merge/run-merge-token'
|
|
23
|
+
import { backlog_drive_seats } from './backlog-drive-seats'
|
|
23
24
|
|
|
24
25
|
const { MERGE_TOKEN } = run_merge_token
|
|
25
26
|
const RUN_VERDICT = 'run'
|
|
@@ -124,6 +125,9 @@ interface DrivePorts {
|
|
|
124
125
|
// The lanes free right now — `backlog_ready.drive_free_lane_count` in production, the limit less the
|
|
125
126
|
// open lanes.
|
|
126
127
|
free_lanes: () => Promise<number>
|
|
128
|
+
// Whether the issue already holds a kept lane (one a child was dispatched into) — it is reused, so it
|
|
129
|
+
// takes no free seat.
|
|
130
|
+
has_lane: (issue: string) => Promise<boolean>
|
|
127
131
|
// How many open issues lacking `auto-ok` still owe a judgement — `auto_ok_judge_read.owed` in
|
|
128
132
|
// production, `0` when the read failed.
|
|
129
133
|
judgement_owed: () => Promise<number>
|
|
@@ -265,10 +269,10 @@ async function refused(
|
|
|
265
269
|
return ended('launch', state, undefined, issue)
|
|
266
270
|
}
|
|
267
271
|
|
|
268
|
-
// **No more
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
+
// **No more new lanes than free lanes**. `backlog:offer` lists every ready issue without reading the
|
|
273
|
+
// lanes, so launching all of them overran `JOSH_LANE_LIMIT` until `lane:open` ran out of seats. An issue
|
|
274
|
+
// that already holds a kept lane reuses it and takes no seat (`backlog-drive-seats.ts`); the rest launch
|
|
275
|
+
// up to the free count, read once before the first launch.
|
|
272
276
|
//
|
|
273
277
|
// **Only a launch is activity**: a pass that started nothing — every lane taken
|
|
274
278
|
// — keeps `active` where it was, or the once-a-minute offer would restart the idle budget for good.
|
|
@@ -284,9 +288,10 @@ async function launch_all(
|
|
|
284
288
|
ports: DrivePorts,
|
|
285
289
|
): Promise<PassResult> {
|
|
286
290
|
const in_flight = [...state.in_flight]
|
|
287
|
-
const free = await ports.free_lanes()
|
|
288
291
|
|
|
289
|
-
|
|
292
|
+
const launching = await backlog_drive_seats.seated(issues, ports)
|
|
293
|
+
|
|
294
|
+
for (const issue of launching) {
|
|
290
295
|
// eslint-disable-next-line no-await-in-loop -- lanes launch in order and the first refusal ends the pass
|
|
291
296
|
const outcome = await ports.launch(issue)
|
|
292
297
|
|
|
@@ -2,7 +2,7 @@ import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
|
2
2
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
3
3
|
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
4
4
|
import { lane_capacity } from '#scripts/lane/lane-capacity'
|
|
5
|
-
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
5
|
+
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
6
6
|
import type { RunCarry } from '#scripts/run/carry/run-carry'
|
|
7
7
|
import { run_headless } from '#scripts/run/run-headless'
|
|
8
8
|
import { run_invocation } from '#scripts/run/run-invocation'
|
|
@@ -28,6 +28,16 @@ async function live_lane_count(): Promise<number> {
|
|
|
28
28
|
return lane_registry.live_count(await lane_registry.list_lanes())
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Free lanes beside `lanes`, `undefined` where the limit could not be read: the count `lane:limit`
|
|
32
|
+
// prints, and the one `run:board` draws, so the two never answer apart.
|
|
33
|
+
async function free_lanes_beside(lanes: ReadonlyArray<LaneInfo>): Promise<number | undefined> {
|
|
34
|
+
const limit = await lane_capacity.lane_limit()
|
|
35
|
+
|
|
36
|
+
if (limit.kind !== 'limit') return undefined
|
|
37
|
+
|
|
38
|
+
return lane_capacity.free_lanes(limit.limit, lane_registry.live_count(lanes))
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
async function free_under(limit: number): Promise<number> {
|
|
32
42
|
return lane_capacity.free_lanes(limit, await live_lane_count())
|
|
33
43
|
}
|
|
@@ -42,11 +52,7 @@ async function free_under(limit: number): Promise<number> {
|
|
|
42
52
|
// (`backlog:next`) is gated behind a free lane, so a network read here would defeat that design. The
|
|
43
53
|
// two can diverge, but only toward over-counting live lanes → fewer free lanes, the safe direction.
|
|
44
54
|
async function free_lane_count(): Promise<number> {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (limit.kind !== 'limit') return NO_FREE
|
|
48
|
-
|
|
49
|
-
return await free_under(limit.limit)
|
|
55
|
+
return (await free_lanes_beside(await lane_registry.list_lanes())) ?? NO_FREE
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
// **The drive's count throws on an unreadable limit**. The drive launches no
|
|
@@ -165,6 +171,7 @@ const backlog_ready = {
|
|
|
165
171
|
read_backlog_next,
|
|
166
172
|
print_offer_hint,
|
|
167
173
|
free_lane_count,
|
|
174
|
+
free_lanes_beside,
|
|
168
175
|
live_lane_count,
|
|
169
176
|
drive_free_lane_count,
|
|
170
177
|
drains_pool,
|