@joshuafolkken/kit 1.1383.0 → 1.1435.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.
Files changed (244) hide show
  1. package/.claude/skills/epic-commands/SKILL.md +1 -1
  2. package/.claude/skills/epic-commands/epic-bundle.md +5 -0
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
  4. package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
  5. package/.claude/skills/workflow-commands/backlogrun.md +1 -0
  6. package/.claude/skills/workflow-commands/chain-rule.md +4 -2
  7. package/.claude/skills/workflow-commands/issue-comments.md +6 -0
  8. package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
  9. package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
  10. package/README.md +25 -0
  11. package/dist/hooks/audit-provision.js +2 -2
  12. package/dist/hooks/batch-guard.js +7 -7
  13. package/dist/hooks/{chunk-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
  14. package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
  15. package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
  16. package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
  17. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  18. package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
  19. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  20. package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
  21. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  22. package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
  23. package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
  24. package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
  25. package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
  26. package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
  27. package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
  28. package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
  29. package/dist/hooks/codex-hook-adapter.js +15 -15
  30. package/dist/hooks/duplicate-read-guard.js +8 -8
  31. package/dist/hooks/format-edited.js +5 -5
  32. package/dist/hooks/inputs.json +262 -201
  33. package/dist/hooks/investigation-guard.js +8 -8
  34. package/dist/hooks/pretool-guard.js +14 -14
  35. package/dist/hooks/session-lang.js +124 -15
  36. package/dist/hooks/stop-guard.js +98 -55
  37. package/dist/josh.js +63 -10
  38. package/package.json +4 -3
  39. package/prompts/review.md +22 -0
  40. package/scripts/agent/claude-agent-argv.ts +13 -1
  41. package/scripts/auth/auth-setup-cli.ts +101 -0
  42. package/scripts/auth/claude-token-output.ts +78 -0
  43. package/scripts/auth/claude-token.ts +68 -0
  44. package/scripts/backlog/backlog-drive-cli.ts +2 -4
  45. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  46. package/scripts/backlog/backlog-drive-launch.ts +5 -0
  47. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  48. package/scripts/backlog/backlog-drive-named.ts +4 -2
  49. package/scripts/backlog/backlog-drive-owner.ts +72 -5
  50. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  51. package/scripts/backlog/backlog-drive.ts +16 -8
  52. package/scripts/backlog/backlog-overlap.ts +22 -5
  53. package/scripts/backlog/backlog-ready.ts +1 -3
  54. package/scripts/backlog/backlogrun-args.ts +13 -2
  55. package/scripts/backlog/backlogrun-cli.ts +91 -19
  56. package/scripts/backlog/backlogrun-launch.ts +80 -5
  57. package/scripts/backlog/backlogrun-replace.ts +166 -0
  58. package/scripts/backlog/backlogrun-stop.ts +178 -0
  59. package/scripts/behavior/behavior-cli.ts +6 -1
  60. package/scripts/doctor/doctor-io.ts +60 -9
  61. package/scripts/doctor/doctor.ts +20 -1
  62. package/scripts/document/document-byte-budget.ts +3 -3
  63. package/scripts/epic/epic-bundle-cli.ts +8 -8
  64. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  65. package/scripts/epic/epic-bundle.ts +39 -4
  66. package/scripts/exports/unused-members-cli.ts +2 -1
  67. package/scripts/followup/followup-checks-event.ts +82 -0
  68. package/scripts/followup/git-followup-finish.ts +16 -2
  69. package/scripts/followup/git-followup-workflow.ts +5 -2
  70. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  71. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  72. package/scripts/followup/git-pr-followup.ts +4 -1
  73. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  74. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  75. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  76. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  77. package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
  78. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  79. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  80. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  81. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  82. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  83. package/scripts/gate/gate-report.ts +70 -15
  84. package/scripts/gate/project-checks.ts +13 -2
  85. package/scripts/gate/scoped-green.ts +2 -1
  86. package/scripts/gate/verification-gate.ts +4 -3
  87. package/scripts/gh/gh-author-association.ts +14 -0
  88. package/scripts/gh/git-gh-conditional.ts +66 -0
  89. package/scripts/gh/git-gh-exec.ts +1 -1
  90. package/scripts/gh/git-gh-issue-read.ts +6 -3
  91. package/scripts/gh/git-gh-issue-write.ts +5 -1
  92. package/scripts/gh/git-gh-issue.ts +4 -1
  93. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  94. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  95. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  96. package/scripts/gh/git-pr-checks.ts +36 -2
  97. package/scripts/git/git-command.ts +16 -4
  98. package/scripts/git/git-schemas.ts +1 -0
  99. package/scripts/git/git-spawn-sync.ts +1 -1
  100. package/scripts/hooks/pretool-guard.ts +3 -1
  101. package/scripts/init/hook-command-rewrite.ts +31 -0
  102. package/scripts/init/init-actions.ts +4 -1
  103. package/scripts/init/init-ai-copy.ts +33 -10
  104. package/scripts/init/init-basic.ts +8 -3
  105. package/scripts/init/init-logic-toolchain.ts +76 -0
  106. package/scripts/init/init-logic.ts +2 -0
  107. package/scripts/init/init.ts +5 -23
  108. package/scripts/init/project-profile.ts +36 -7
  109. package/scripts/init/start-exec.ts +27 -1
  110. package/scripts/init/start-plan.ts +30 -4
  111. package/scripts/init/start-steps.ts +77 -3
  112. package/scripts/init/start.ts +21 -13
  113. package/scripts/init/workflow-mark.ts +35 -0
  114. package/scripts/issue/issue-ack-cli.ts +84 -0
  115. package/scripts/issue/issue-labels.ts +40 -8
  116. package/scripts/issue/issue-state-cli.ts +14 -4
  117. package/scripts/issue/issue-watch-diff.ts +152 -0
  118. package/scripts/issue/issue-watch-gate.ts +81 -0
  119. package/scripts/issue/issue-watch-hook.ts +54 -0
  120. package/scripts/issue/issue-watch.ts +240 -0
  121. package/scripts/issue/measure-rerun-cli.ts +3 -4
  122. package/scripts/josh/josh-command-map.ts +4 -0
  123. package/scripts/josh/josh-commands-backlog.ts +2 -2
  124. package/scripts/josh/josh-commands-development.ts +1 -1
  125. package/scripts/josh/josh-commands-issue.ts +7 -0
  126. package/scripts/josh/josh-commands-lane.ts +11 -2
  127. package/scripts/josh/josh-commands-project.ts +25 -1
  128. package/scripts/josh/josh-verdict.ts +33 -4
  129. package/scripts/lane/claude-lane-broker.ts +142 -0
  130. package/scripts/lane/lane-await.ts +6 -2
  131. package/scripts/lane/lane-cache.ts +37 -7
  132. package/scripts/lane/lane-capacity.ts +8 -6
  133. package/scripts/lane/lane-child-invocation.ts +1 -0
  134. package/scripts/lane/lane-cli.ts +15 -1
  135. package/scripts/lane/lane-close.ts +15 -6
  136. package/scripts/lane/lane-dispatch.ts +36 -10
  137. package/scripts/lane/lane-environment.ts +1 -1
  138. package/scripts/lane/lane-launch-cli.ts +13 -4
  139. package/scripts/lane/lane-ledger.ts +21 -3
  140. package/scripts/lane/lane-measure-cli.ts +10 -2
  141. package/scripts/lane/lane-open.ts +9 -1
  142. package/scripts/lane/lane-preserve.ts +49 -0
  143. package/scripts/lane/lane-reap.ts +20 -5
  144. package/scripts/lane/lane-registry.ts +6 -0
  145. package/scripts/lane/lane-restart-cli.ts +51 -0
  146. package/scripts/lane/lane-restart.ts +175 -0
  147. package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
  148. package/scripts/lane/lane-spent.ts +195 -0
  149. package/scripts/lane/lane-stage-stats.ts +27 -3
  150. package/scripts/lane/lane-stop-stats.ts +96 -0
  151. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  152. package/scripts/lane/process-tools.ts +31 -0
  153. package/scripts/lib/buffered-process.ts +36 -2
  154. package/scripts/lib/pnpm-reach.ts +27 -0
  155. package/scripts/lib/status-icons.ts +5 -1
  156. package/scripts/lint/lint-parallel.ts +19 -2
  157. package/scripts/observations/observation-ledger-home.ts +1 -0
  158. package/scripts/review/review-attest-cli.ts +21 -6
  159. package/scripts/review/review-brief-cli.ts +2 -2
  160. package/scripts/review/review-comment-round.ts +114 -0
  161. package/scripts/review/review-record-cli.ts +39 -17
  162. package/scripts/rules/implementation-cut.ts +3 -3
  163. package/scripts/rules/pre-gate-cut.ts +1 -1
  164. package/scripts/run/board/run-board-checks.ts +42 -0
  165. package/scripts/run/board/run-board-cli.ts +2 -0
  166. package/scripts/run/board/run-board-every.ts +3 -1
  167. package/scripts/run/board/run-board-fit.ts +16 -5
  168. package/scripts/run/board/run-board-fresh.ts +64 -0
  169. package/scripts/run/board/run-board-header.ts +139 -37
  170. package/scripts/run/board/run-board-labels.ts +47 -4
  171. package/scripts/run/board/run-board-lane-event.ts +66 -0
  172. package/scripts/run/board/run-board-layout.ts +57 -9
  173. package/scripts/run/board/run-board-now.ts +234 -0
  174. package/scripts/run/board/run-board-operation-read.ts +75 -0
  175. package/scripts/run/board/run-board-operation.ts +150 -0
  176. package/scripts/run/board/run-board-phase.ts +26 -1
  177. package/scripts/run/board/run-board-plan.ts +4 -6
  178. package/scripts/run/board/run-board-read.ts +58 -5
  179. package/scripts/run/board/run-board-reason.ts +124 -0
  180. package/scripts/run/board/run-board-render-checks.ts +55 -0
  181. package/scripts/run/board/run-board-render-legend.ts +12 -3
  182. package/scripts/run/board/run-board-render-slot.ts +2 -2
  183. package/scripts/run/board/run-board-render-tail.ts +128 -0
  184. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  185. package/scripts/run/board/run-board-render-title.ts +58 -0
  186. package/scripts/run/board/run-board-render.ts +96 -80
  187. package/scripts/run/board/run-board-row-yield.ts +32 -0
  188. package/scripts/run/board/run-board-setup.ts +121 -0
  189. package/scripts/run/board/run-board-span.ts +49 -7
  190. package/scripts/run/board/run-board-state.ts +10 -0
  191. package/scripts/run/board/run-board-status.ts +76 -13
  192. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  193. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  194. package/scripts/run/board/run-board-thanks.ts +53 -0
  195. package/scripts/run/board/run-board-tick.ts +53 -16
  196. package/scripts/run/board/run-board-usual.ts +44 -0
  197. package/scripts/run/carry/run-carry-added.ts +1 -1
  198. package/scripts/run/carry/run-carry-args.ts +9 -1
  199. package/scripts/run/carry/run-carry-change.ts +1 -1
  200. package/scripts/run/carry/run-carry-cli.ts +39 -52
  201. package/scripts/run/carry/run-carry-end.ts +73 -0
  202. package/scripts/run/carry/run-carry-ended.ts +9 -1
  203. package/scripts/run/carry/run-carry.ts +24 -1
  204. package/scripts/run/entry/run-entry-cli.ts +5 -0
  205. package/scripts/run/event/run-event-checks.ts +54 -0
  206. package/scripts/run/event/run-event-render.ts +3 -0
  207. package/scripts/run/event/run-event-scope.ts +1 -0
  208. package/scripts/run/event/run-event-setup.ts +91 -0
  209. package/scripts/run/event/run-event-stream.ts +46 -9
  210. package/scripts/run/merge/run-merge-cli.ts +7 -4
  211. package/scripts/run/merge/run-merge-ship.ts +61 -0
  212. package/scripts/run/merge/run-merge-steps.ts +7 -14
  213. package/scripts/run/merge/run-merge-token.ts +16 -2
  214. package/scripts/run/run-headless.ts +1 -0
  215. package/scripts/run/run-step.ts +2 -0
  216. package/scripts/run/run-stranded-detect.ts +58 -12
  217. package/scripts/run/ship/run-ship-cli.ts +22 -2
  218. package/scripts/run/ship/run-ship-return.ts +31 -13
  219. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  220. package/scripts/run/ship/run-ship-stage.ts +1 -0
  221. package/scripts/run/wake/run-wake-await.ts +130 -0
  222. package/scripts/run/wake/run-wake-cli.ts +108 -74
  223. package/scripts/run/wake/run-wake-describe.ts +36 -2
  224. package/scripts/run/wake/run-wake-driver.ts +21 -5
  225. package/scripts/run/wake/run-wake-handoff.ts +1 -1
  226. package/scripts/run/wake/run-wake-session.ts +20 -3
  227. package/scripts/run/wake/run-wake-stop.ts +37 -0
  228. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  229. package/scripts/sync/claude-settings-merge.ts +144 -0
  230. package/scripts/sync/sync-ai-files.ts +28 -3
  231. package/scripts/sync/sync.ts +15 -0
  232. package/scripts/test/project-test-script.ts +73 -0
  233. package/scripts/test/skip-marker.ts +14 -1
  234. package/scripts/test/test-related.ts +3 -2
  235. package/scripts/test/test-unit-guard.ts +78 -14
  236. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  237. package/scripts/uninstall/distributed-files.ts +92 -0
  238. package/scripts/uninstall/uninstall-plan.ts +119 -0
  239. package/scripts/uninstall/uninstall-report.ts +61 -0
  240. package/scripts/uninstall/uninstall.ts +206 -0
  241. package/templates/AGENTS.basic.md +4 -0
  242. package/templates/GEMINI.basic.md +6 -0
  243. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  244. package/scripts/run/board/run-board-track.ts +0 -22
@@ -0,0 +1,130 @@
1
+ import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
2
+ import { fresh_machine_spec, type Scenario } from './fresh-machine-spec'
3
+
4
+ // kit's own six patterns: three projects (one Lua file, one `index.html`, a minimal TypeScript
5
+ // SvelteKit app from `sv`) by two commands (`josh init`, `josh start`), each from the starting state
6
+ // joshuafolkken/kit#2848 gave it. `josh init` installs the packed checkout, so a fix is retested
7
+ // without a release; `josh start` installs a published version, because its setup pull request
8
+ // would carry a tarball path CI cannot resolve.
9
+
10
+ const KIT = KIT_PACKAGE_NAME
11
+ const ALLOW_BUILDS = '--allow-build=esbuild --allow-build=unrs-resolver'
12
+ // What `pnpm dlx @joshuafolkken/kit init` writes before it installs kit.
13
+ const BARE_MANIFEST = String.raw`printf '{ "private": true }\n' > package.json`
14
+ const CREATE_SVELTEKIT =
15
+ 'pnpm dlx sv create --template minimal --types ts --no-add-ons --install pnpm .'
16
+ // The `sv` that created the project is the one `pnpm dlx` resolved, so its version is recorded.
17
+ const SV_VERSION = 'pnpm dlx sv --version'
18
+ // The app builds and type-checks before kit is installed and still does after it.
19
+ const SVELTEKIT_CHECKS = 'pnpm build && pnpm check'
20
+ const INSTALL_TARBALL = `pnpm add -D ${ALLOW_BUILDS} "$PACKAGE_TARBALL"`
21
+ // A version published minutes ago is inside pnpm's release-age wait; kit alone is excluded from it
22
+ // (joshuafolkken/kit#2869), so every other dependency keeps the wait.
23
+ const EXCLUDE_RELEASE_AGE = String.raw`printf 'minimumReleaseAgeExclude:\n - "${KIT}"\n' >> pnpm-workspace.yaml`
24
+ const INSTALL_PUBLISHED = `pnpm add -D ${ALLOW_BUILDS} ${KIT}@"$PACKAGE_VERSION"`
25
+ const GATE = 'pnpm josh gate'
26
+ const INIT_COMMANDS = [INSTALL_TARBALL, 'pnpm exec josh init', GATE]
27
+ // `josh start` leaves the project on its setup branch, where the gate asks for the scoped checks
28
+ // first; this is the line its refusal hands back.
29
+ const SCOPED_CHECKS = 'pnpm josh lint:related && pnpm josh test:related'
30
+ const START_COMMANDS = [
31
+ EXCLUDE_RELEASE_AGE,
32
+ INSTALL_PUBLISHED,
33
+ 'pnpm exec josh start --yes --github',
34
+ SCOPED_CHECKS,
35
+ GATE,
36
+ ]
37
+
38
+ // `gh auth login` cannot run without a person, and it is what points git at gh for credentials; with
39
+ // the token in the environment this is the same step.
40
+ const SIGN_IN = 'gh auth setup-git'
41
+ // What the prerequisites guide has a person run once per machine. Only the pattern with no repository
42
+ // uses it: `josh start` makes the first commit there, and stops before writing without it (#3754).
43
+ const MACHINE_IDENTITY =
44
+ 'git config --global user.name kit-test && git config --global user.email kit-test@example.com'
45
+ // The identity is the repository's own, so the machine stays one nobody has configured git on.
46
+ const FIRST_COMMIT = [
47
+ 'git init --initial-branch main',
48
+ 'git config user.name kit-test && git config user.email kit-test@example.com',
49
+ 'git add --all && git commit --message "Initial commit"',
50
+ ]
51
+ const CREATE_REPOSITORY = 'gh repo create "$RUN_NAME" --private --source . --push'
52
+
53
+ // The two states #3712 was found in: a shell that reads no rc file, and a PATH with `node` on it and
54
+ // no `pnpm`.
55
+ const BARE_SHELL =
56
+ 'env -i HOME="$HOME" PATH=/usr/local/bin:/usr/bin:/bin bash --noprofile --norc -c'
57
+ const NODE_ONLY = '"$HOME/node-only"'
58
+ const PATH_PROBES = [
59
+ `${BARE_SHELL} '${GATE}'`,
60
+ `mkdir -p ${NODE_ONLY} && ln -sf "$(command -v node)" ${NODE_ONLY}/node && ${BARE_SHELL} 'PATH="$HOME/node-only:$PATH" ./node_modules/.bin/josh gate'`,
61
+ ]
62
+
63
+ // What a `josh init` run is checked against after the gate: the profile it chose, the files it left
64
+ // (a `./.git` line is a repository it should not have made), and a second run that changes nothing.
65
+ const PROJECT_FILES =
66
+ 'find . -name node_modules -prune -o -name .git -prune -o -type f -print | sort'
67
+ const CHECKSUMS = `${PROJECT_FILES} | xargs sha256sum`
68
+ const INIT_PROBES = [
69
+ 'pnpm josh profile',
70
+ 'find . -name node_modules -prune -o -print | sort',
71
+ `${CHECKSUMS} > "$HOME/before-second-init.sha" && pnpm exec josh init && ${CHECKSUMS} | diff "$HOME/before-second-init.sha" -`,
72
+ ]
73
+
74
+ const LUA_FILE = 'main.lua'
75
+ const HTML_FILE = 'index.html'
76
+ const LUA_FILES = { [LUA_FILE]: 'print("Hello")\n' }
77
+ const HTML_FILES = { [HTML_FILE]: '<!doctype html><html><body><h1>Hello</h1></body></html>\n' }
78
+
79
+ function init_pattern(
80
+ project: string,
81
+ files: Record<string, string>,
82
+ setup: ReadonlyArray<string>,
83
+ project_probes: ReadonlyArray<string> = [],
84
+ ): Scenario {
85
+ return fresh_machine_spec.define({
86
+ name: `kit-test-${project}-init`,
87
+ files,
88
+ commands: [...setup, ...INIT_COMMANDS],
89
+ probes: [...INIT_PROBES, ...project_probes, ...PATH_PROBES],
90
+ })
91
+ }
92
+
93
+ function start_pattern(
94
+ project: string,
95
+ files: Record<string, string>,
96
+ setup: ReadonlyArray<string>,
97
+ ): Scenario {
98
+ return fresh_machine_spec.define({
99
+ name: `kit-test-${project}-start`,
100
+ requires_github: true,
101
+ package_source: 'published',
102
+ files,
103
+ commands: [SIGN_IN, ...setup, ...START_COMMANDS],
104
+ probes: PATH_PROBES,
105
+ })
106
+ }
107
+
108
+ const PATTERNS: ReadonlyArray<Scenario> = [
109
+ init_pattern('lua', LUA_FILES, [BARE_MANIFEST]),
110
+ // No Git: `josh start` creates the repository, the first commit and the GitHub repository.
111
+ start_pattern('lua', LUA_FILES, [MACHINE_IDENTITY, BARE_MANIFEST]),
112
+ init_pattern('html', HTML_FILES, [BARE_MANIFEST]),
113
+ // Git with a first commit on `main`, no GitHub repository.
114
+ start_pattern('html', HTML_FILES, [...FIRST_COMMIT, BARE_MANIFEST]),
115
+ init_pattern('svelte', {}, [CREATE_SVELTEKIT, SV_VERSION, SVELTEKIT_CHECKS], [SVELTEKIT_CHECKS]),
116
+ // A GitHub repository with `main` already pushed.
117
+ start_pattern('svelte', {}, [CREATE_SVELTEKIT, ...FIRST_COMMIT, CREATE_REPOSITORY]),
118
+ ]
119
+
120
+ function names(): Array<string> {
121
+ return PATTERNS.map((pattern) => pattern.name)
122
+ }
123
+
124
+ function find(name: string): Scenario | undefined {
125
+ return PATTERNS.find((pattern) => pattern.name === name)
126
+ }
127
+
128
+ const fresh_machine_patterns = { names, find }
129
+
130
+ export { fresh_machine_patterns }
@@ -0,0 +1,215 @@
1
+ import {
2
+ appendFileSync,
3
+ chmodSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from 'node:fs'
11
+ import os from 'node:os'
12
+ import path from 'node:path'
13
+ import { PROJECT_ROOT } from '#scripts/init/init-paths'
14
+ import { execa, type Options } from 'execa'
15
+ import { z } from 'zod'
16
+ import { fresh_machine_docker, type ContainerRun } from './fresh-machine-docker'
17
+ import { fresh_machine_image } from './fresh-machine-image'
18
+ import { fresh_machine_log } from './fresh-machine-log'
19
+ import { fresh_machine_script } from './fresh-machine-script'
20
+ import type { Scenario } from './fresh-machine-spec'
21
+
22
+ // The I/O of `josh fresh:run`: build the machine, stage what the container may read, run the
23
+ // scenario, and put every line of all three into one log.
24
+
25
+ const DOCKER = 'docker'
26
+ const PNPM = 'pnpm'
27
+ const FAILURE_EXIT_CODE = 1
28
+ const STAGE_PREFIX = 'josh-fresh-machine-'
29
+ // The container's user is not the host's, so what it reads has to be readable by anyone — whatever
30
+ // the host's umask gave the seed files and the tarball `pnpm pack` wrote.
31
+ const SHARED_DIRECTORY_MODE = 0o755
32
+ const SHARED_FILE_MODE = 0o644
33
+ const TARBALL_EXTENSION = '.tgz'
34
+ const MANIFEST_SCHEMA = z.object({ name: z.string() })
35
+
36
+ interface RunRequest {
37
+ scenario: Scenario
38
+ run_name: string
39
+ log_path: string
40
+ package_version: string | undefined
41
+ // Variable name → value, each passed into the container by name and replaced in every line.
42
+ secrets: Readonly<Record<string, string>>
43
+ is_rebuild: boolean
44
+ }
45
+
46
+ interface Stage {
47
+ root: string
48
+ seed_directory: string
49
+ pack_directory: string
50
+ }
51
+
52
+ type Write = (line: string) => void
53
+
54
+ function logger(request: RunRequest): Write {
55
+ const secrets = Object.values(request.secrets)
56
+
57
+ return function write(line: string): void {
58
+ const safe = fresh_machine_log.redact(line, secrets)
59
+
60
+ console.info(safe)
61
+ appendFileSync(request.log_path, `${safe}\n`)
62
+ }
63
+ }
64
+
65
+ // One host command, its output streamed line by line into the log. A command that could not start
66
+ // (no docker on the machine) has no exit code, and its reason is the only line there is to log.
67
+ async function stream(
68
+ file: string,
69
+ arguments_: Array<string>,
70
+ options: Options,
71
+ write: Write,
72
+ ): Promise<number> {
73
+ const subprocess = execa(file, arguments_, { ...options, all: true, reject: false })
74
+ const lines = subprocess.iterable({ from: 'all' })
75
+
76
+ for await (const line of lines) write(String(line))
77
+
78
+ const result = await subprocess
79
+
80
+ if (result.exitCode === undefined) write(result.shortMessage ?? `${file} did not start`)
81
+
82
+ return result.exitCode ?? FAILURE_EXIT_CODE
83
+ }
84
+
85
+ function staged_directory(root: string, name: string): string {
86
+ const directory = path.join(root, name)
87
+
88
+ mkdirSync(directory)
89
+
90
+ return directory
91
+ }
92
+
93
+ function stage(scenario: Scenario): Stage {
94
+ const root = mkdtempSync(path.join(os.tmpdir(), STAGE_PREFIX))
95
+ const seed_directory = staged_directory(root, 'seed')
96
+
97
+ for (const [file, content] of Object.entries(scenario.files)) {
98
+ const target = path.join(seed_directory, file)
99
+
100
+ mkdirSync(path.dirname(target), { recursive: true })
101
+ writeFileSync(target, content)
102
+ }
103
+
104
+ return { root, seed_directory, pack_directory: staged_directory(root, 'pack') }
105
+ }
106
+
107
+ // Run once everything is staged, so the tarball is covered as well as the seed. The root itself stays
108
+ // as private as `mkdtemp` made it: a bind mount never enters a source through its parent, and on a
109
+ // shared host an open root would let every other user read the tarball for the length of the run.
110
+ function share(root: string): void {
111
+ const entries = readdirSync(root, { withFileTypes: true, recursive: true })
112
+
113
+ for (const entry of entries) {
114
+ const mode = entry.isDirectory() ? SHARED_DIRECTORY_MODE : SHARED_FILE_MODE
115
+
116
+ chmodSync(path.join(entry.parentPath, entry.name), mode)
117
+ }
118
+ }
119
+
120
+ // `pnpm pack` runs the checkout's own `prepack`, so the tarball holds what a release of this tree
121
+ // would.
122
+ async function pack(pack_directory: string, write: Write): Promise<string> {
123
+ const pack_arguments = ['pack', '--pack-destination', pack_directory]
124
+
125
+ write(`$ ${PNPM} ${pack_arguments.join(' ')}`)
126
+ await stream(PNPM, pack_arguments, { cwd: PROJECT_ROOT }, write)
127
+
128
+ const tarball = readdirSync(pack_directory).find((file) => file.endsWith(TARBALL_EXTENSION))
129
+
130
+ if (tarball === undefined) throw new Error('pnpm pack produced no tarball')
131
+
132
+ return tarball
133
+ }
134
+
135
+ function package_name(): string {
136
+ const manifest = readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8')
137
+
138
+ return MANIFEST_SCHEMA.parse(JSON.parse(manifest)).name
139
+ }
140
+
141
+ // The published version is always written out in the log and the install command, so a run can be
142
+ // repeated against the same release; without `--package-version` it is the registry's latest.
143
+ async function published_version(requested: string | undefined): Promise<string> {
144
+ if (requested !== undefined) return requested
145
+
146
+ const { stdout } = await execa(PNPM, ['view', package_name(), 'version'], { cwd: PROJECT_ROOT })
147
+
148
+ return stdout.trim()
149
+ }
150
+
151
+ async function container_run(
152
+ request: RunRequest,
153
+ staged: Stage,
154
+ write: Write,
155
+ ): Promise<ContainerRun> {
156
+ const { scenario } = request
157
+ const is_tarball = scenario.package_source === 'tarball'
158
+
159
+ return {
160
+ image: fresh_machine_image.tag(scenario.requires_github),
161
+ run_name: request.run_name,
162
+ script: fresh_machine_script.build(scenario),
163
+ seed_directory: staged.seed_directory,
164
+ pack_directory: staged.pack_directory,
165
+ tarball: is_tarball ? await pack(staged.pack_directory, write) : undefined,
166
+ package_version: is_tarball ? undefined : await published_version(request.package_version),
167
+ secret_names: Object.keys(request.secrets),
168
+ }
169
+ }
170
+
171
+ async function build_image(request: RunRequest, write: Write): Promise<number> {
172
+ const { scenario } = request
173
+ const image = fresh_machine_image.tag(scenario.requires_github)
174
+ const build_arguments = fresh_machine_docker.build_arguments(image, request.is_rebuild)
175
+ const input = fresh_machine_image.dockerfile(scenario.requires_github)
176
+
177
+ write(`$ ${DOCKER} ${build_arguments.join(' ')}`)
178
+
179
+ return await stream(DOCKER, build_arguments, { input }, write)
180
+ }
181
+
182
+ async function run_container(request: RunRequest, staged: Stage, write: Write): Promise<number> {
183
+ const container = await container_run(request, staged, write)
184
+
185
+ share(staged.root)
186
+ write(`$ ${DOCKER} run ${container.image} — ${request.run_name}`)
187
+ write(`# package: ${container.tarball ?? container.package_version ?? ''}`)
188
+
189
+ return await stream(
190
+ DOCKER,
191
+ fresh_machine_docker.run_arguments(container),
192
+ { env: request.secrets },
193
+ write,
194
+ )
195
+ }
196
+
197
+ async function run(request: RunRequest): Promise<number> {
198
+ const write = logger(request)
199
+ const build_code = await build_image(request, write)
200
+
201
+ if (build_code !== 0) return build_code
202
+
203
+ const staged = stage(request.scenario)
204
+
205
+ try {
206
+ return await run_container(request, staged, write)
207
+ } finally {
208
+ rmSync(staged.root, { recursive: true, force: true })
209
+ }
210
+ }
211
+
212
+ const fresh_machine_run = { run }
213
+
214
+ export { fresh_machine_run }
215
+ export type { RunRequest }
@@ -0,0 +1,45 @@
1
+ import type { Scenario } from './fresh-machine-spec'
2
+
3
+ // The script one container runs. Every command is printed before it runs and its exit status after,
4
+ // so the log reads as a terminal session; the first failing command ends the run, and the probes
5
+ // that follow a clean run are recorded whatever they exit with.
6
+
7
+ const SEED_MOUNT = '/seed'
8
+ const PACK_MOUNT = '/pack'
9
+ const COPY_SEED = `cp -R ${SEED_MOUNT}/. .`
10
+ const PROBES_HEADING = '# probes — recorded, never failing the run'
11
+ const ESCAPED_QUOTE = String.raw`'\''`
12
+ const PREAMBLE = [
13
+ String.raw`step() { printf '\n$ %s\n' "$1"; eval "$1"; local code=$?; printf '[exit %s]\n' "$code"; return "$code"; }`,
14
+ 'mkdir -p "$HOME/$RUN_NAME" && cd "$HOME/$RUN_NAME" || exit 1',
15
+ ]
16
+
17
+ function quote(command: string): string {
18
+ const escaped = command.split("'").join(ESCAPED_QUOTE)
19
+
20
+ return `'${escaped}'`
21
+ }
22
+
23
+ function required(command: string): string {
24
+ return `step ${quote(command)} || exit $?`
25
+ }
26
+
27
+ function probe_lines(probes: ReadonlyArray<string>): Array<string> {
28
+ if (probes.length === 0) return []
29
+
30
+ return [
31
+ String.raw`printf '\n%s\n' ${quote(PROBES_HEADING)}`,
32
+ ...probes.map((probe) => `step ${quote(probe)}`),
33
+ ]
34
+ }
35
+
36
+ function build(scenario: Scenario): string {
37
+ const seed = Object.keys(scenario.files).length > 0 ? [COPY_SEED] : []
38
+ const commands = [...seed, ...scenario.commands].map((command) => required(command))
39
+
40
+ return [...PREAMBLE, ...commands, ...probe_lines(scenario.probes), 'exit 0', ''].join('\n')
41
+ }
42
+
43
+ const fresh_machine_script = { build, SEED_MOUNT, PACK_MOUNT }
44
+
45
+ export { fresh_machine_script }
@@ -0,0 +1,57 @@
1
+ import path from 'node:path'
2
+ import { z } from 'zod'
3
+
4
+ // A scenario is everything one `josh fresh:run` needs to know: the files that are in the project
5
+ // before anything is installed, the commands a person would type, and the probes recorded after
6
+ // them. kit's own six patterns and a consumer toolkit's spec file are the same shape, so the machine
7
+ // and the log are shared and nothing about them is kit's alone (joshuafolkken/kit#3714).
8
+ //
9
+ // Inside the container a command reads `$RUN_NAME` (the numbered project and repository name),
10
+ // `$PACKAGE_TARBALL` (the packed checkout, for `package_source: tarball`) and `$PACKAGE_VERSION`
11
+ // (the published version, for `package_source: published`).
12
+
13
+ function is_project_relative(file: string): boolean {
14
+ const normalized = path.posix.normalize(file)
15
+
16
+ return !path.posix.isAbsolute(normalized) && !normalized.startsWith('..')
17
+ }
18
+
19
+ const COMMAND_SCHEMA = z.string().min(1)
20
+ const SCENARIO_SCHEMA = z.strictObject({
21
+ name: z.string().regex(/^[a-z0-9][a-z0-9-]*$/u),
22
+ // A GitHub scenario runs on the image that also has git and gh, and receives the host's token.
23
+ requires_github: z.boolean().default(false),
24
+ // A Claude Code scenario receives the host's long-lived token as `CLAUDE_CODE_OAUTH_TOKEN`.
25
+ requires_claude: z.boolean().default(false),
26
+ package_source: z.enum(['tarball', 'published']).default('tarball'),
27
+ files: z.record(z.string().refine(is_project_relative), z.string()).default({}),
28
+ // A failing command ends the run; a failing probe is only recorded.
29
+ commands: z.array(COMMAND_SCHEMA).min(1),
30
+ probes: z.array(COMMAND_SCHEMA).default([]),
31
+ })
32
+
33
+ type Scenario = z.infer<typeof SCENARIO_SCHEMA>
34
+ type ScenarioInput = z.input<typeof SCENARIO_SCHEMA>
35
+
36
+ function define(input: ScenarioInput): Scenario {
37
+ return SCENARIO_SCHEMA.parse(input)
38
+ }
39
+
40
+ function problems(error: z.ZodError): string {
41
+ return error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ')
42
+ }
43
+
44
+ // A spec file is written by hand in another repository, so a mistake is reported as the fields that
45
+ // are wrong rather than as a stack trace.
46
+ function parse(json: string): Scenario {
47
+ const result = SCENARIO_SCHEMA.safeParse(JSON.parse(json))
48
+
49
+ if (!result.success) throw new Error(`Invalid scenario spec — ${problems(result.error)}`)
50
+
51
+ return result.data
52
+ }
53
+
54
+ const fresh_machine_spec = { define, parse }
55
+
56
+ export { fresh_machine_spec }
57
+ export type { Scenario, ScenarioInput }
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "kit-test-backlogrun-python",
3
+ "requires_github": true,
4
+ "package_source": "tarball",
5
+ "requires_claude": true,
6
+ "files": {
7
+ "pyproject.toml": "[project]\nname = \"greeting\"\nversion = \"0.1.0\"\nrequires-python = \">=3.9\"\n",
8
+ "main.py": "def main() -> None:\n print(\"Hello\")\n\n\nif __name__ == \"__main__\":\n main()\n"
9
+ },
10
+ "commands": [
11
+ "gh auth setup-git",
12
+ "git config --global user.name kit-test && git config --global user.email kit-test@example.com",
13
+ "printf '{ \"private\": true }\\n' > package.json",
14
+ "cp \"$PACKAGE_TARBALL\" . && pnpm add -D --allow-build=esbuild --allow-build=unrs-resolver \"./$(basename \"$PACKAGE_TARBALL\")\"",
15
+ "pnpm exec josh start --yes --github",
16
+ "pnpm josh profile | grep -x 'profile: basic (package.json josh.profile)'",
17
+ "grep -q '\"workflow\": \"github\"' package.json && test -f .claude/settings.json",
18
+ "curl -fsSL https://claude.ai/install.sh | bash && export PATH=\"$HOME/.local/bin:$PATH\" && claude --version",
19
+ "gh issue create --title 'Print a greeting with a name in main.py' --body 'Make main.py print \"Hello, Josh\" instead of \"Hello\".' --label auto-ok --label run:lane",
20
+ "timeout 60 pnpm josh backlogrun > \"$HOME/backlogrun.out\" 2>&1; cat \"$HOME/backlogrun.out\"; grep -q '^backlogrun: started' \"$HOME/backlogrun.out\"",
21
+ "pnpm josh run:event --from 0 | grep 'starting session'",
22
+ "for i in $(seq 1 90); do pnpm josh run:event --from 0 | grep -q 'dispatching' && break; sleep 10; done; pnpm josh run:event --from 0 | grep 'dispatching'",
23
+ "for i in $(seq 1 120); do test \"$(gh pr list --state all --json number --jq length)\" -gt 0 && break; sleep 10; done; pnpm josh run:board --chat",
24
+ "pnpm josh backlogrun --stop"
25
+ ],
26
+ "probes": [
27
+ "tail -n 40 \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\" | cut -c1-400",
28
+ "grep -c 'requires approval' \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\"",
29
+ "grep -c 'run:carry --begin' \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\"",
30
+ "tail -n 40 /tmp/josh-lane-dispatch-*.log | cut -c1-400",
31
+ "cat /tmp/josh-lane-dispatch-*.log | grep -c 'requires approval'",
32
+ "gh pr list --state all"
33
+ ]
34
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "kit-test-backlogrun",
3
+ "requires_github": true,
4
+ "package_source": "tarball",
5
+ "requires_claude": true,
6
+ "files": {
7
+ "index.html": "<!doctype html><html><body><h1>Hello</h1></body></html>\n"
8
+ },
9
+ "commands": [
10
+ "gh auth setup-git",
11
+ "git config --global user.name kit-test && git config --global user.email kit-test@example.com",
12
+ "printf '{ \"private\": true }\\n' > package.json",
13
+ "cp \"$PACKAGE_TARBALL\" . && pnpm add -D --allow-build=esbuild --allow-build=unrs-resolver \"./$(basename \"$PACKAGE_TARBALL\")\"",
14
+ "pnpm exec josh start --yes --github",
15
+ "gh label list --limit 100 --json name --jq '.[].name' > \"$HOME/labels.txt\"; grep -xE 'auto-ok|needs-decision|needs-human-review|already-done|review-round2-skipped|bug' \"$HOME/labels.txt\"; test \"$(grep -cxE 'auto-ok|needs-decision|needs-human-review|already-done|review-round2-skipped|bug' \"$HOME/labels.txt\")\" -eq 6",
16
+ "curl -fsSL https://claude.ai/install.sh | bash && export PATH=\"$HOME/.local/bin:$PATH\" && claude --version",
17
+ "gh issue create --title 'Add a heading to index.html' --body 'Add an h2 under the h1.' --label auto-ok --label run:lane",
18
+ "timeout 60 pnpm josh backlogrun > \"$HOME/backlogrun.out\" 2>&1; cat \"$HOME/backlogrun.out\"; grep -q '^backlogrun: started' \"$HOME/backlogrun.out\"",
19
+ "pnpm josh run:event --from 0 | grep 'starting session'",
20
+ "for i in $(seq 1 90); do pnpm josh run:event --from 0 | grep -q 'dispatching' && break; sleep 10; done; pnpm josh run:event --from 0 | grep 'dispatching'",
21
+ "for i in $(seq 1 120); do test \"$(gh pr list --state all --json number --jq length)\" -gt 0 && break; sleep 10; done; pnpm josh run:board --chat",
22
+ "pnpm josh backlogrun --stop"
23
+ ],
24
+ "probes": [
25
+ "tail -n 40 \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\" | cut -c1-400",
26
+ "grep -c 'requires approval' \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\"",
27
+ "tail -n 40 /tmp/josh-lane-dispatch-*.log | cut -c1-400",
28
+ "cat /tmp/josh-lane-dispatch-*.log | grep -c 'requires approval'",
29
+ "gh pr list --state all"
30
+ ]
31
+ }
@@ -2,6 +2,7 @@ import { GATE_COMMAND } from '#scripts/josh/josh-command-types'
2
2
  import { josh_verdict } from '#scripts/josh/josh-verdict'
3
3
  import { buffered_process, type BufferedProcessResult } from '#scripts/lib/buffered-process'
4
4
  import { status_icons } from '#scripts/lib/status-icons'
5
+ import { NOT_USED_MARKER, NOTHING_TO_CHECK_MARKER } from '#scripts/test/skip-marker'
5
6
  import { test_unit_guard } from '#scripts/test/test-unit-guard'
6
7
  import { gate_log } from './gate-log'
7
8
  import { gate_plan } from './gate-plan'
@@ -11,7 +12,7 @@ import { gate_plan } from './gate-plan'
11
12
  // about what is printed changes in the move: the four blocks, the log notice and the verdict come out
12
13
  // exactly as before, with one addition — the per-failure action lines below.
13
14
 
14
- const { FAIL_ICON, PASS_ICON } = status_icons
15
+ const { FAIL_ICON, PASS_ICON, WARN_ICON } = status_icons
15
16
 
16
17
  interface GateStep {
17
18
  label: string
@@ -76,16 +77,56 @@ function has_warnings(result: GateStepResult): boolean {
76
77
  // other line still withholds.
77
78
  const SAFE_CHAIN_BANNER = 'safe-chain is not available'
78
79
 
80
+ function checker_lines(result: GateStepResult): ReadonlyArray<string> {
81
+ return result.output.split('\n').filter((line) => !line.includes(SAFE_CHAIN_BANNER))
82
+ }
83
+
79
84
  function has_checker_warning(result: GateStepResult): boolean {
80
85
  if (!gate_plan.WARNING_CHECKER_LABELS.includes(result.label)) return false
81
86
 
82
- return result.output
83
- .split('\n')
84
- .some(
85
- (line) =>
86
- !line.includes(SAFE_CHAIN_BANNER) &&
87
- WARNING_MARKERS.some((marker) => line.includes(marker)),
88
- )
87
+ return checker_lines(result).some((line) =>
88
+ WARNING_MARKERS.some((marker) => line.includes(marker)),
89
+ )
90
+ }
91
+
92
+ // The record's counterpart to `is_skip_notice`, narrower for the reason `has_checker_warning` is
93
+ // narrower than `has_warnings`: a skip of a tool the project does not use (`NOT_USED_MARKER`) left
94
+ // nothing unverified, so only another skip line withholds the green record.
95
+ function is_unverified_skip(result: GateStepResult): boolean {
96
+ return checker_lines(result).some(
97
+ (line) => line.includes(test_unit_guard.SKIP_MARKER) && !line.includes(NOT_USED_MARKER),
98
+ )
99
+ }
100
+
101
+ // **A check the verdict counts as skipped said nothing but that it checked nothing.** `is_skip_notice`
102
+ // asks whether a check carries a skip notice, which is the right question for printing its body; it is
103
+ // the wrong one for a count, twice over. A check can skip half of
104
+ // itself — a `basic` project's `lint` runs Prettier and skips eslint — and calling it skipped would
105
+ // have the verdict say nothing ran on a gate that checked the formatting. And a check can run with
106
+ // nothing of the project's to look at — `exports` in a consumer, `behavior` with no transcript — which
107
+ // is no skip, and still left an empty `basic` project reading `2 ran`. A line that is neither notice is
108
+ // something a tool printed, so the check ran. A tool that ran clean and silent beside a skipped one is
109
+ // given a line of its own by the check (`project_checks.ran_notice`), so the reading holds for it too.
110
+ //
111
+ // **pnpm's script header is not something the check printed.** Every check is spawned as
112
+ // `pnpm josh <target>`, and where the project declares a `josh` script — `josh init` writes one — pnpm
113
+ // opens the captured output with `$ josh <target>`. Counted as a line, it made every spawned check one
114
+ // that ran, and `not verified` unreachable in the very project it was written for.
115
+ const IDLE_MARKERS: ReadonlyArray<string> = [test_unit_guard.SKIP_MARKER, NOTHING_TO_CHECK_MARKER]
116
+ const PNPM_SCRIPT_HEADER = '$ '
117
+
118
+ function is_idle_line(line: string): boolean {
119
+ return IDLE_MARKERS.some((marker) => line.includes(marker))
120
+ }
121
+
122
+ function is_tool_line(line: string): boolean {
123
+ return line.trim() !== '' && !line.startsWith(PNPM_SCRIPT_HEADER)
124
+ }
125
+
126
+ function is_idle(result: GateStepResult): boolean {
127
+ const lines = checker_lines(result).filter((line) => is_tool_line(line))
128
+
129
+ return lines.length > 0 && lines.every((line) => is_idle_line(line))
89
130
  }
90
131
 
91
132
  function should_print_body(result: GateStepResult, is_verbose: boolean): boolean {
@@ -144,10 +185,7 @@ function format_failure_actions(failed: ReadonlyArray<GateStepResult>): string {
144
185
 
145
186
  // **A check that started past the core budget is said in the summary, not only in the log**.
146
187
  // The wait cap admits a check that waited two minutes whatever the budget says,
147
- // so a gate that ran over-subscribed would otherwise look exactly like one that fitted. The icon is
148
- // local rather than in `status-icons.ts` because nothing reads this line back.
149
- const WARN_ICON = '⚠'
150
-
188
+ // so a gate that ran over-subscribed would otherwise look exactly like one that fitted.
151
189
  function format_budget_notes(results: ReadonlyArray<GateStepResult>): string {
152
190
  const lines = results.flatMap((result) =>
153
191
  result.budget_note === undefined
@@ -158,6 +196,24 @@ function format_budget_notes(results: ReadonlyArray<GateStepResult>): string {
158
196
  return lines.length === 0 ? '' : `${lines.join('\n')}\n`
159
197
  }
160
198
 
199
+ // **The verdict counts what ran apart from what was skipped, and a gate that ran nothing does not say
200
+ // `passed`.** It printed the planned number of checks whatever happened to them, so a `basic` project
201
+ // whose every check skipped read `passed (6 checks)` — the same last line as a gate that ran them all,
202
+ // and the skip this module keeps visible in the bodies was invisible in the one line a `tail` keeps.
203
+ //
204
+ // **A gate with no skip keeps the line it always printed.** A check with nothing to check is counted
205
+ // apart only beside a skip: `exports` says so in every consumer, and counting it alone would reword the
206
+ // verdict of every gate that skipped nothing.
207
+ function format_green_verdict(results: ReadonlyArray<GateStepResult>, total: string): string {
208
+ const has_skip = results.some((result) => is_skip_notice(result))
209
+ const skipped = has_skip ? results.filter((result) => is_idle(result)).length : 0
210
+ const ran = results.length - skipped
211
+
212
+ if (ran === 0) return josh_verdict.format_gate_unverified(skipped, total)
213
+
214
+ return josh_verdict.format_gate_passed({ ran, skipped }, total)
215
+ }
216
+
161
217
  // The total is wall-clock for the whole command, not the sum of the four — they run concurrently,
162
218
  // so a sum would report about three times what the caller waited.
163
219
  //
@@ -172,7 +228,7 @@ function print_gate_summary(
172
228
  const total = format_seconds(report.elapsed_ms)
173
229
  const verdict =
174
230
  failed.length === 0
175
- ? josh_verdict.format_gate_passed(report.step_count, total)
231
+ ? format_green_verdict(results, total)
176
232
  : josh_verdict.format_gate_failed(failed.map((result) => result.label).join(', '), total)
177
233
  const notes = format_budget_notes(results)
178
234
 
@@ -185,7 +241,6 @@ interface GateReport {
185
241
  is_verbose: boolean
186
242
  log_path: string | undefined
187
243
  elapsed_ms: number
188
- step_count: string
189
244
  }
190
245
 
191
246
  // Everything a finished run says, in the order a reader meets it: the four blocks, then the log the
@@ -213,7 +268,7 @@ const gate_report = {
213
268
  format_failure_actions,
214
269
  has_checker_warning,
215
270
  is_gate_step_failed,
216
- is_skip_notice,
271
+ is_unverified_skip,
217
272
  report_gate_steps,
218
273
  }
219
274