rvlib-shipkit 1.4.0 → 1.6.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 (199) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/assets/gen/comfyts-gen.ts +12 -8
  5. package/src/assets/server/studio-server.ts +14 -2
  6. package/src/check.ts +73 -103
  7. package/src/checks/SK001-package-json-shape.check.ts +11 -6
  8. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  9. package/src/checks/SK003-claude-md-shape.check.ts +7 -3
  10. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  11. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  12. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  13. package/src/checks/SK007-github-visibility.check.ts +4 -2
  14. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  15. package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
  16. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  17. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  18. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  19. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  20. package/src/checks/SK014-prose-ste.check.ts +4 -0
  21. package/src/checks/SK015-nested-repo.check.ts +4 -2
  22. package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
  23. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  24. package/src/checks/SK018-test-files-run.check.ts +6 -2
  25. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  26. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  27. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  28. package/src/checks/SK024-identity-colors.check.ts +5 -3
  29. package/src/checks/SK025-release-drift.check.ts +4 -5
  30. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  31. package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
  32. package/src/checks/SK028-readme.check.ts +5 -1
  33. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  34. package/src/checks/SK030-cache-audit.check.ts +34 -0
  35. package/src/checks/SK031-trace-audit.check.ts +39 -0
  36. package/src/checks/SK032-ignored-inputs.check.ts +79 -0
  37. package/src/cli/cmd_Asks.ts +5 -5
  38. package/src/cli/cmd_Assets.ts +17 -13
  39. package/src/cli/cmd_Bump.ts +12 -11
  40. package/src/cli/cmd_CacheAudit.ts +43 -0
  41. package/src/cli/cmd_Check.ts +8 -7
  42. package/src/cli/cmd_Checks.ts +7 -6
  43. package/src/cli/cmd_Ci.ts +12 -11
  44. package/src/cli/cmd_Commands.ts +16 -13
  45. package/src/cli/cmd_ConfigKeys.ts +7 -6
  46. package/src/cli/cmd_Contracts.ts +3 -3
  47. package/src/cli/cmd_CrossRepoActivity.ts +8 -6
  48. package/src/cli/cmd_Fleet.ts +9 -7
  49. package/src/cli/cmd_GlobalProcesses.ts +120 -7
  50. package/src/cli/cmd_Home.ts +26 -19
  51. package/src/cli/cmd_Hooks.ts +6 -5
  52. package/src/cli/cmd_Init.ts +5 -4
  53. package/src/cli/cmd_Keys.ts +20 -8
  54. package/src/cli/cmd_Mv.ts +7 -6
  55. package/src/cli/cmd_Needs.ts +4 -3
  56. package/src/cli/cmd_Overview.ts +5 -4
  57. package/src/cli/cmd_Pb.ts +2 -2
  58. package/src/cli/cmd_Planned.ts +6 -5
  59. package/src/cli/cmd_Prose.ts +5 -5
  60. package/src/cli/cmd_Reflect.ts +7 -7
  61. package/src/cli/cmd_Release.ts +6 -5
  62. package/src/cli/cmd_Releases.ts +5 -4
  63. package/src/cli/cmd_Serve.ts +6 -4
  64. package/src/cli/cmd_Smoke.ts +5 -4
  65. package/src/cli/cmd_Start.ts +7 -6
  66. package/src/cli/cmd_Status.ts +4 -3
  67. package/src/cli/cmd_TraceAudit.ts +62 -0
  68. package/src/cli/cmd_Tui.ts +3 -2
  69. package/src/cli/cmd_When.ts +71 -0
  70. package/src/cli/cmd_Wiki.ts +4 -4
  71. package/src/cli.ts +11 -2
  72. package/src/collections/checks.ts +2 -0
  73. package/src/collections/collection-types.ts +12 -4
  74. package/src/collections/providers.ts +5 -3
  75. package/src/core/app-icon.ts +154 -0
  76. package/src/core/audit-report.ts +21 -0
  77. package/src/core/cache-audit.ts +72 -0
  78. package/src/core/canonical-scripts.ts +2 -2
  79. package/src/core/check-ctx.ts +14 -5
  80. package/src/core/check-runner.ts +43 -23
  81. package/src/core/config-rewrite.ts +37 -24
  82. package/src/core/discover-checks.ts +23 -2
  83. package/src/core/floor-resolve.ts +5 -7
  84. package/src/core/git-root.ts +13 -0
  85. package/src/core/ignored-block.ts +94 -0
  86. package/src/core/ignored-inputs.ts +47 -0
  87. package/src/core/jobs.ts +9 -2
  88. package/src/core/legacy-shapes.ts +39 -10
  89. package/src/core/one-lib.ts +59 -4
  90. package/src/core/one-way-in-policy.ts +2 -2
  91. package/src/core/outcome-words.ts +18 -2
  92. package/src/core/port-check-cli.ts +52 -0
  93. package/src/core/port-check-file.ts +295 -0
  94. package/src/core/port-check-files.ts +31 -0
  95. package/src/core/read-set.ts +97 -9
  96. package/src/core/retention.ts +0 -14
  97. package/src/core/run-check.ts +12 -9
  98. package/src/core/run-ci.ts +14 -5
  99. package/src/core/run-fix.ts +30 -23
  100. package/src/core/run-fixers.ts +48 -5
  101. package/src/core/runs-matrix.ts +179 -0
  102. package/src/core/runs.ts +242 -0
  103. package/src/core/trace-audit.ts +120 -0
  104. package/src/core/update-policy.ts +20 -0
  105. package/src/core/when-block.ts +29 -30
  106. package/src/core/when.ts +41 -103
  107. package/src/core/wire.ts +10 -14
  108. package/src/fleet/scan-fleet.ts +20 -4
  109. package/src/fleet/update-fleet.ts +6 -6
  110. package/src/home/gui-app.ts +24 -27
  111. package/src/home/home-launchd.ts +20 -4
  112. package/src/home/home-routes.ts +12 -1
  113. package/src/home/home-start.ts +11 -5
  114. package/src/home/hubs.ts +5 -5
  115. package/src/home/open-app.ts +2 -2
  116. package/src/home/pm.ts +8 -0
  117. package/src/home/run-pm.ts +1 -0
  118. package/src/home/serve-web.ts +2 -2
  119. package/src/home/supervise.ts +24 -1
  120. package/src/home/via-hub.ts +14 -0
  121. package/src/hooks/run-hooks.ts +48 -4
  122. package/src/init-repo.ts +6 -6
  123. package/src/ledger/db.ts +2 -13
  124. package/src/ledger/gate-checks.ts +133 -61
  125. package/src/ledger/git-key.ts +153 -31
  126. package/src/ledger/git-scan.ts +5 -2
  127. package/src/ledger/ledger.ts +136 -21
  128. package/src/ledger/store.ts +14 -0
  129. package/src/ledger/toolchain-identity.ts +248 -45
  130. package/src/plan/repo-source.ts +18 -39
  131. package/src/problems/extract.ts +3 -0
  132. package/src/release/run-release.ts +5 -0
  133. package/src/repo-config.ts +46 -54
  134. package/src/rules/rule.ts +2 -2
  135. package/src/scaffold/files/agent-coding.md +1 -1
  136. package/src/scaffold/files/agent-index.md +1 -1
  137. package/src/scaffold/files/verify-skill.md +2 -2
  138. package/src/serve/hub-app/activity-st.ts +5 -4
  139. package/src/serve/hub-app/activity-view.tsx +1 -1
  140. package/src/serve/hub-app/admin-svc.ts +36 -3
  141. package/src/serve/hub-app/admin-view.tsx +46 -12
  142. package/src/serve/hub-app/check-view.tsx +66 -46
  143. package/src/serve/hub-app/check-words.ts +1 -11
  144. package/src/serve/hub-app/checks-svc.ts +14 -35
  145. package/src/serve/hub-app/col-card.tsx +1 -44
  146. package/src/serve/hub-app/col-tables-view.tsx +84 -11
  147. package/src/serve/hub-app/col-tables.ts +27 -0
  148. package/src/serve/hub-app/cols-svc.ts +5 -4
  149. package/src/serve/hub-app/dock-st.ts +3 -2
  150. package/src/serve/hub-app/dock-view.tsx +36 -0
  151. package/src/serve/hub-app/dock.css +6 -0
  152. package/src/serve/hub-app/float-cards.tsx +30 -0
  153. package/src/serve/hub-app/floating.tsx +41 -4
  154. package/src/serve/hub-app/git-st.ts +42 -0
  155. package/src/serve/hub-app/git-view.tsx +61 -0
  156. package/src/serve/hub-app/hash-route.ts +45 -1
  157. package/src/serve/hub-app/header-bar.tsx +62 -72
  158. package/src/serve/hub-app/health-svc.ts +3 -2
  159. package/src/serve/hub-app/home-st.ts +14 -2
  160. package/src/serve/hub-app/home-strip.css +43 -12
  161. package/src/serve/hub-app/home-strip.tsx +115 -64
  162. package/src/serve/hub-app/home.html +1 -0
  163. package/src/serve/hub-app/hub-app.tsx +32 -8
  164. package/src/serve/hub-app/hub-types.ts +5 -2
  165. package/src/serve/hub-app/hub.css +89 -63
  166. package/src/serve/hub-app/icons.tsx +11 -0
  167. package/src/serve/hub-app/index.html +1 -0
  168. package/src/serve/hub-app/keys.ts +15 -4
  169. package/src/serve/hub-app/live-svc.ts +3 -2
  170. package/src/serve/hub-app/machine-st.ts +18 -22
  171. package/src/serve/hub-app/plan-view.tsx +51 -0
  172. package/src/serve/hub-app/planned-st.ts +9 -4
  173. package/src/serve/hub-app/processes-view.tsx +45 -0
  174. package/src/serve/hub-app/project-list.css +4 -1
  175. package/src/serve/hub-app/project-list.tsx +6 -3
  176. package/src/serve/hub-app/projects-svc.ts +25 -10
  177. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  178. package/src/serve/hub-app/reach.ts +36 -0
  179. package/src/serve/hub-app/readme-svc.ts +3 -2
  180. package/src/serve/hub-app/repo-sections.ts +131 -0
  181. package/src/serve/hub-app/repo-st.ts +70 -14
  182. package/src/serve/hub-app/sidebar.tsx +17 -42
  183. package/src/serve/hub-app/timeline-svc.ts +3 -2
  184. package/src/serve/hub-app/tree-build.ts +151 -155
  185. package/src/serve/hub-app/tree-row.tsx +55 -12
  186. package/src/serve/hub-app/tree-svc.ts +97 -56
  187. package/src/serve/hub-app/when-view.tsx +89 -0
  188. package/src/serve/hub-app/when.css +17 -0
  189. package/src/serve/hub-runtime.ts +7 -18
  190. package/src/serve/project-gutter-route.ts +22 -1
  191. package/src/serve/run-serve.ts +1 -1
  192. package/src/serve/serve-hub.ts +4 -14
  193. package/src/suite/judge-suites.ts +6 -0
  194. package/src/trace/interpose.c +655 -0
  195. package/src/trace/read-trace.ts +485 -0
  196. package/src/trace/tracer.ts +282 -0
  197. package/src/serve/hub-app/processes-svc.ts +0 -63
  198. package/src/serve/hub-app/repo-sections.tsx +0 -61
  199. package/src/serve/repo-processes.ts +0 -89
@@ -5,7 +5,7 @@
5
5
  // what it read: package.json, the probes below, the test file listing, and
6
6
  // whatever the floor resolution read.
7
7
  //
8
- // It also enforces the package.json half of ONE WAY IN (build/shipkit.md):
8
+ // It also enforces the package.json half of ONE WAY IN (build/shipkit/one-way-in.md):
9
9
  // `test`, `lint` and `typecheck` ROUTE to `shipkit ci --only <id>` and the
10
10
  // tool body they used to hold sits at `<id>:raw`. That is what makes a cache
11
11
  // worth having: `bun run test` is the reflex, and the reflex has to land in
@@ -125,13 +125,14 @@ export function evaluatePackageJson(t: CheckCtx): void {
125
125
  //
126
126
  // a shipkit verb is never on this list. `shipkit ci`, `check`, `contracts`,
127
127
  // `tui`, `mv`, `assets`, `asks` are typed as themselves, and a package.json
128
- // row that only forwards to one is a second name for one capability
128
+ // row that only forwards to one is a second name for one capability. nor is
129
+ // a global process (ComfyUI, TTS): `shipkit gp start <id>` starts it from any
130
+ // repo, so a per repo script would only copy it
129
131
  function requiredScripts(c: CheckCtx): Array<{ name: string; why: string }> {
130
132
  const required: Array<{ name: string; why: string }> = [
131
133
  // watch-mode serve — the ONE shipkit surface the bare verb cannot give
132
134
  { name: 'hub', why: 'shipkit repo' },
133
135
  ]
134
- if (c.exists('.shipkit/comfy')) required.push({ name: 'comfy:up', why: '.shipkit/comfy/ exists' })
135
136
  if (c.exists('deploy.sh')) required.push({ name: 'up', why: 'deploy.sh exists' })
136
137
  return required
137
138
  }
@@ -197,12 +198,16 @@ export const packageJsonShape = check({
197
198
  title: 'package.json',
198
199
  description: 'package.json routes lint, typecheck and test through the gate',
199
200
  owner: OWNER,
201
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
202
+ cache: { by: 'reads' },
203
+ fix: 'ci',
204
+ applies: 'always',
200
205
  run: (c) => evaluatePackageJson(c),
201
206
  // mechanical repairs only: field flips + scripts whose canonical body the
202
- // vocabulary page states. Bodies it does NOT state (assets, comfy:up) are
203
- // named in the summary, never invented. rvlib-* roots untouched — their
207
+ // vocabulary page states. A body it does NOT state is named in the
208
+ // summary, never invented. rvlib-* roots untouched — their
204
209
  // license/private are publish decisions.
205
- fix: (c): FixResult => {
210
+ repair: (c): FixResult => {
206
211
  const raw = c.read('package.json')
207
212
  if (raw == null) return { changed: false, summary: 'no package.json — nothing to fix' }
208
213
  if (rootPublishesNpm(c.config)) return { changed: false, summary: 'published to npm: its package.json is its public contract, never rewritten' }
@@ -16,6 +16,10 @@ export const contractsFulfillness = check({
16
16
  owner: 'meta/contracts.md',
17
17
  // keyed on what the contracts read: each artifact, each detector, and each
18
18
  // folder a glob walked through `c.entries`, a gitignored one included
19
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
20
+ cache: { by: 'reads' },
21
+ fix: 'none',
22
+ applies: 'always',
19
23
  run: async (c) => {
20
24
  applyContractsResult(c, await c.use(contractsResult), contractsFulfillness)
21
25
  },
@@ -18,8 +18,12 @@ export const claudeMdShape = check({
18
18
  title: 'CLAUDE.md',
19
19
  description: 'CLAUDE.md loads the house wiki into every session',
20
20
  owner: 'meta/agent-folder.md',
21
- // keyed on what it reads: CLAUDE.md and whether agent/index.md exists. The
22
- // gitignore probe runs git, so an answer that took it is never stored
21
+ // keyed on what it reads: CLAUDE.md, whether agent/index.md exists, and
22
+ // the ignore files git read for the gitignore probe (c.exec runs traced)
23
+ runs: { onPreCommit: { budgetMs: 40 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
24
+ cache: { by: 'reads' },
25
+ fix: 'ci',
26
+ applies: 'always',
23
27
  run: async (c) => {
24
28
  const hasAgent = c.exists('agent/index.md')
25
29
  const claude = c.read('CLAUDE.md')
@@ -53,7 +57,7 @@ export const claudeMdShape = check({
53
57
  // CLAUDE.md would lint green with the base include forever missing,
54
58
  // trading a loud FAIL for a permanent silent gap. Creation and prose
55
59
  // relocation stay human/machine-local (`shipkit start --include`).
56
- fix: (c): FixResult => {
60
+ repair: (c): FixResult => {
57
61
  if (!c.exists('agent/index.md')) return { changed: false, summary: 'no agent/index.md — nothing mechanical to fix' }
58
62
  const cur = c.read('CLAUDE.md')
59
63
  if (cur == null) return { changed: false, summary: 'CLAUDE.md missing — creation needs the base include (shipkit start <name> writes it), not autofixable' }
@@ -205,11 +205,14 @@ export const gitignorePolicy = check({
205
205
  title: '.gitignore',
206
206
  description: 'the managed .gitignore block keeps private contract folders out of git',
207
207
  owner: 'meta/contracts.md',
208
- // .gitignore plus what git currently TRACKS and IGNORES, which the tree
209
- // carries and no read set records (the header says why)
210
- inputs: ['.'],
211
208
  // it untracks generated files (a commit) unless the run is frozen
212
209
  writes: true,
210
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
211
+ // .gitignore plus what git currently TRACKS and IGNORES, which the tree
212
+ // carries and no read set records (the header says why)
213
+ cache: { by: 'files', inputs: ['.'], key: null },
214
+ fix: 'ci',
215
+ applies: 'always',
213
216
  run: async (c) => {
214
217
  // a generated path still in the index is the half-applied state the
215
218
  // managed block cannot fix on its own
@@ -278,7 +281,7 @@ export const gitignorePolicy = check({
278
281
  }
279
282
  c.ok(`.gitignore policy block current, ${claimed.length + backProbes.size} patterns verified against git`)
280
283
  },
281
- fix: async (c): Promise<FixResult> => {
284
+ repair: async (c): Promise<FixResult> => {
282
285
  // the move first: it writes the new block itself, before any folder moves
283
286
  const moved = process.env.SHIPKIT_FROZEN === '1' ? null : migrateLayout(c.root, { config: c.config, renderGitignore: (t, legacy, cfg) => renderPolicy(t, cfg ?? c.config, legacy) })
284
287
  const layout = moved == null ? null
@@ -10,7 +10,12 @@
10
10
  // Per rvlib-* dep:
11
11
  // link: → the symlink must resolve (a broken link dies cryptically at
12
12
  // import time otherwise) — nothing to update, code is live
13
- // semver → installed vs npm latest; a BREAKING jump (a new major, or a
13
+ // semver → installed vs npm latest. A semver pin whose node_modules
14
+ // entry is a `bun link` symlink is judged by the PIN's floor
15
+ // instead: the link carries the source, always the newest, while
16
+ // a fresh clone installs the pin. Behind, the pin moves through
17
+ // the same guarded update, and `bun link <name>` restores the
18
+ // link after it (a link that does not come back FAILs). A BREAKING jump (a new major, or a
14
19
  // new 0.x minor) is never taken by itself
15
20
  // (src/core/update-policy.ts: WARN naming `fleet --update --major`,
16
21
  // package.json untouched); the rvlib-shipkit tree running this
@@ -46,7 +51,7 @@
46
51
  import { existsSync, readFileSync } from 'fs'
47
52
  import { basename, join } from 'path'
48
53
  import type { CheckCtx } from '../core/check-ctx.ts'
49
- import { allowMajor, breakingAdvice, isBreaking } from '../core/update-policy.ts'
54
+ import { allowMajor, breakingAdvice, isBreaking, isLinkedInstall, pinFloor } from '../core/update-policy.ts'
50
55
  import { isRunningCopy, rememberLatest } from '../core/self-update.ts'
51
56
  import { rootPublishesNpm } from '../core/repo-facts.ts'
52
57
  import { mutateLease, withMutateLease } from '../core/locked-write.ts'
@@ -61,6 +66,13 @@ const installedVersion = (root: string, name: string): string | null => {
61
66
  try { return (JSON.parse(readFileSync(p, 'utf8')) as Pkg).version ?? null } catch { return null }
62
67
  }
63
68
 
69
+ const readPin = (root: string, name: string): string | null => {
70
+ try {
71
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as Pkg
72
+ return pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? null
73
+ } catch { return null }
74
+ }
75
+
64
76
  async function latestVersion(t: CheckCtx, name: string): Promise<string> {
65
77
  const res = await t.fetch(`https://registry.npmjs.org/${name}/latest`)
66
78
  if (!res.ok) throw new Error(`registry ${res.status}`)
@@ -74,23 +86,24 @@ export const rvlibVersions = check({
74
86
  blocking: true,
75
87
  title: 'rvlib versions',
76
88
  description: 'the rvlib packages installed here are as new as the published ones',
77
- owner: 'build/shipkit.md',
78
- ttl: '24h',
89
+ owner: 'build/shipkit/consuming.md',
79
90
  // it updates the rvlib deps unless the run is frozen
80
91
  writes: true,
81
- planOff: (env) => {
82
- let pkg: Pkg
83
- try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'no readable package.json' }
84
- return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).some((n) => n.startsWith('rvlib-')) ? null : 'no rvlib-* dependency'
85
- },
86
- cacheKey: (env) => {
92
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: false, onClock: { every: '24h', inactive: 'wait' } },
93
+ cache: { by: 'ttl', span: '24h', key: (env) => {
87
94
  let pkg: Pkg
88
95
  try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'unreadable' }
89
96
  return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })
90
97
  .filter((n) => n.startsWith('rvlib-'))
91
98
  .sort()
92
- .map((n) => `${n}@${installedVersion(env.root, n) ?? 'absent'}`)
99
+ .map((n) => `${n}@${installedVersion(env.root, n) ?? 'absent'}${isLinkedInstall(env.root, n) ? ` pin ${readPin(env.root, n)}` : ''}`)
93
100
  .join(',') + (rootPublishesNpm(env.config) ? ' npm' : '')
101
+ } },
102
+ fix: 'none',
103
+ applies: (env) => {
104
+ let pkg: Pkg
105
+ try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'no readable package.json' }
106
+ return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).some((n) => n.startsWith('rvlib-')) ? null : 'no rvlib-* dependency'
94
107
  },
95
108
  run: async (c) => {
96
109
  let pkg: Pkg
@@ -100,39 +113,47 @@ export const rvlibVersions = check({
100
113
  const pm = detectPackageManager(c.root)
101
114
  const manifest = ['package.json', ...(isRefusal(pm) ? LOCK_FILES : pm.locks)]
102
115
 
103
- const stale: Array<{ name: string; installed: string; latest: string }> = []
116
+ const stale: Array<{ name: string; installed: string; latest: string; linked: boolean }> = []
104
117
  for (const [name, spec] of deps) {
105
118
  if (spec.startsWith('link:')) {
106
119
  if (installedVersion(c.root, name) == null) c.fail(`${name} is link: but the symlink does not resolve — re-run \`bun link ${name}\` (a broken link dies cryptically at import time)`)
107
120
  else c.ok(`${name} (linked, live)`)
108
121
  continue
109
122
  }
110
- const installed = installedVersion(c.root, name)
111
- if (installed == null) { c.fail(`${name} declared (${spec}) but not installed — ${isRefusal(pm) ? pm.refused : `run: ${pm.install.join(' ')}`}`); continue }
123
+ const onDisk = installedVersion(c.root, name)
124
+ if (onDisk == null) { c.fail(`${name} declared (${spec}) but not installed — ${isRefusal(pm) ? pm.refused : `run: ${pm.install.join(' ')}`}`); continue }
125
+ const linked = isLinkedInstall(c.root, name)
126
+ const floor = linked ? pinFloor(spec) : null
127
+ if (linked && floor == null) { c.ok(`${name} (linked to its source, pin ${spec} names no version to judge)`); continue }
128
+ const installed = floor ?? onDisk
129
+ // how the lines below name it: a linked dep is its pin, never the source it links
130
+ const label = linked ? `${name} pin ${installed}` : `${name} ${installed}`
112
131
  let latest: string
113
132
  try { latest = await latestVersion(c, name) } catch (e) {
114
- c.lines.push({ kind: 'skip', text: `${name} ${installed} — registry unreachable (${String(e).slice(0, 80)}), version drift unknown` })
133
+ c.lines.push({ kind: 'skip', text: `${label} — registry unreachable (${String(e).slice(0, 80)}), version drift unknown` })
115
134
  continue
116
135
  }
117
136
  // real semver order, not string difference — a dist-tag pointing
118
137
  // BEHIND the installed version (prerelease, unpublish window)
119
138
  // must never trigger a silent downgrade
120
139
  const cmp = Bun.semver.order(latest, installed)
121
- if (cmp === 0) c.ok(`${name} ${installed} (latest)`)
122
- else if (cmp < 0) c.ok(`${name} ${installed} ahead of npm latest ${latest} (prerelease or dist-tag lag) — not touching`)
140
+ if (cmp === 0) c.ok(`${label} (latest${linked ? ', linked to its source' : ''})`)
141
+ else if (cmp < 0) c.ok(`${label} ahead of npm latest ${latest} (prerelease or dist-tag lag) — not touching`)
123
142
  else if (isBreaking(installed, latest) && !allowMajor()) c.warn(breakingAdvice(name, latest, basename(c.root)))
124
- else if (name === 'rvlib-shipkit' && isRunningCopy(c.root)) {
143
+ // a linked toolkit moves only its pin: the source tree this process
144
+ // runs from is never written, so the swap below cannot happen
145
+ else if (name === 'rvlib-shipkit' && !linked && isRunningCopy(c.root)) {
125
146
  // swapping this tree under the running process is how a run
126
147
  // meets modules of two versions. The startup self-update owns
127
148
  // it: handing it the answer makes the next run update and
128
149
  // restart before any check loads
129
150
  rememberLatest(latest)
130
151
  c.warn(`${name} ${installed} → ${latest} is the toolkit running this check, so it is not swapped mid run: the next shipkit run updates it and restarts before any check`)
131
- } else stale.push({ name, installed, latest })
152
+ } else stale.push({ name, installed, latest, linked })
132
153
  }
133
154
  if (stale.length === 0) return
134
155
 
135
- const drift = stale.map((s) => `${s.name} ${s.installed} → ${s.latest}`).join(', ')
156
+ const drift = stale.map((s) => `${s.name}${s.linked ? ' pin' : ''} ${s.installed} → ${s.latest}`).join(', ')
136
157
  const names = stale.map((s) => s.name)
137
158
  if (isRefusal(pm)) {
138
159
  c.warn(`newer rvlib live: ${drift}, not updating: ${pm.refused}`)
@@ -164,7 +185,11 @@ export const rvlibVersions = check({
164
185
  const leased = await withMutateLease(mutateLease(c.root, 'fix'), { label: 'self-update', print: () => {}, escape: 'SHIPKIT_FROZEN=1' }, async () => {
165
186
  const again = await c.exec(['git', 'status', '--porcelain', '--', ...manifest], { cwd: c.root })
166
187
  if (again.code === 0 && again.out.trim() !== '') return null
167
- return await c.exec(cmd, { cwd: c.root, timeoutMs: 120_000 })
188
+ const up = await c.exec(cmd, { cwd: c.root, timeoutMs: 120_000 })
189
+ // the update may swap a link for the tarball: put every link back,
190
+ // whatever the update did, inside the same lease
191
+ for (const s of stale) if (s.linked) await c.exec(['bun', 'link', s.name], { cwd: c.root, timeoutMs: 60_000 })
192
+ return up
168
193
  })
169
194
  if (!leased.ok) {
170
195
  c.warn(`newer rvlib live: ${drift} — self-update deferred (${leased.warn}); re-run check once that run finishes`)
@@ -188,6 +213,13 @@ export const rvlibVersions = check({
188
213
  return
189
214
  }
190
215
  for (const s of stale) {
216
+ if (s.linked) {
217
+ const now = pinFloor(readPin(c.root, s.name) ?? '')
218
+ if (!isLinkedInstall(c.root, s.name)) c.fail(`${s.name} pin ${s.installed} → ${now ?? 'unreadable'} but node_modules/${s.name} is no longer linked to its source — run: bun link ${s.name}`)
219
+ else if (now === s.latest) c.warn(`${s.name} pin ${s.installed} → ${now} self-updated, link to the source restored — commit the bump`)
220
+ else c.warn(`${s.name} updated but its pin reads ${now ?? 'unreadable'} (wanted ${s.latest}) — check package.json`)
221
+ continue
222
+ }
191
223
  const now = installedVersion(c.root, s.name)
192
224
  // WARN on purpose: the run MUTATED the repo and a follow-up
193
225
  // (restart watchers, commit) is owed — an ok here would be a
@@ -40,6 +40,10 @@ export const npmPublishSafety = check({
40
40
  owner: 'meta/rv-conventions/package-json.md',
41
41
  // keyed on what it reads: the root package.json and every declared release
42
42
  // unit's, at the dirs the config picks
43
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
44
+ cache: { by: 'reads' },
45
+ fix: 'none',
46
+ applies: 'always',
43
47
  run: (c) => {
44
48
  const json = (rel: string): Pkg | null | 'unparseable' => {
45
49
  try { return c.json<Pkg>(rel) } catch { return 'unparseable' }
@@ -27,8 +27,10 @@ export const githubVisibility = check({
27
27
  title: 'github visibility',
28
28
  description: 'the github visibility of this repo matches the one it declares',
29
29
  owner: 'meta/contracts.md',
30
- ttl: 'forever',
31
- cacheKey: (env) => declaredVisibility(env.config),
30
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: 'moved', onClock: 'once' },
31
+ cache: { by: 'fact', key: (env) => declaredVisibility(env.config) },
32
+ fix: 'none',
33
+ applies: 'always',
32
34
  run: async (c) => {
33
35
  const declared = declaredVisibility(c.config)
34
36
  const skip = (why: string): void => void c.lines.push({ kind: 'skip', text: `github visibility unverified — ${why} (declared: ${declared})` })
@@ -86,15 +86,17 @@ export const reviewerAgent = check({
86
86
  title: 'reviewer definition',
87
87
  description: 'the declared reviewer agent has its brief, its definition and read only tools',
88
88
  owner: 'identity/how-we-work.md',
89
+ runs: { onPreCommit: { budgetMs: 20 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
89
90
  // the answer is the config, the definition file and the brief. The brief
90
91
  // is a CONFIG path that may sit outside the repo, so both files enter the
91
92
  // key by content, wherever they live
92
- inputs: [CONFIG_FILE],
93
- cacheKey: (env) => {
93
+ cache: { by: 'files', inputs: [CONFIG_FILE], key: (env) => {
94
94
  const r = env.config.reviewer
95
95
  if (r == null) return 'no reviewer'
96
96
  return `${r.name}|${(r.tools ?? REVIEWER_TOOLS).join(',')}|def=${fileStamp(join(env.root, reviewerAgentPath(r.name)))}|${r.brief}=${fileStamp(configPath(env.root, r.brief))}`
97
- },
97
+ } },
98
+ fix: 'ci',
99
+ applies: 'always',
98
100
  run: (c) => {
99
101
  const r = c.config.reviewer
100
102
  if (r == null) return c.ok('no reviewer agent declared (repo.config reviewer:) — skipped')
@@ -109,7 +111,7 @@ export const reviewerAgent = check({
109
111
  // create-if-missing only: an existing definition may carry a repo's own
110
112
  // tuned prompt, and silently rewriting that would cost more than the
111
113
  // finding it repairs. Violations in a present file stay a loud FAIL.
112
- fix: (c): FixResult => {
114
+ repair: (c): FixResult => {
113
115
  const r = c.config.reviewer
114
116
  if (r == null) return { changed: false, summary: 'no reviewer declared — nothing to write' }
115
117
  const rel = reviewerAgentPath(r.name)
@@ -81,9 +81,11 @@ export const commitAiTells = check({
81
81
  title: 'commit ai tells',
82
82
  description: 'the commit-msg hook that refuses AI attribution is armed, and no ref name tells',
83
83
  owner: 'identity/how-we-commit.md',
84
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
84
85
  // git config and ref names, which no tree id describes, so they are the key
85
- inputs: [CONFIG_FILE],
86
- cacheKey: historyKey,
86
+ cache: { by: 'files', inputs: [CONFIG_FILE], key: historyKey },
87
+ fix: 'none',
88
+ applies: 'always',
87
89
  run: async (c) => {
88
90
  const nested = nestedIn(c.root)
89
91
  if (nested != null) return void c.lines.push({ kind: 'skip', text: `commit gates not checked, nested in ${nested}: the parent repo owns them` })
@@ -40,6 +40,10 @@ export const undeclaredUnit = check({
40
40
  owner: 'build/shipkit/units.md',
41
41
  // keyed on what it reads: the listing of every package.json, each one it
42
42
  // opens, and the declared units' package.json
43
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
44
+ cache: { by: 'reads' },
45
+ fix: 'none',
46
+ applies: 'always',
43
47
  run: (c) => {
44
48
  const { units, errors, testOnly } = resolveUnits(c.root, c.config, checkUnitReader(c))
45
49
  // a DECLARED unit that does not resolve is a declaration bug and
@@ -61,6 +61,10 @@ export const siblingUnitRanges = check({
61
61
  owner: 'build/shipkit/built-in-checks.md',
62
62
  // keyed on what it reads: every declared unit's package.json, at the dirs
63
63
  // the config picks
64
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
65
+ cache: { by: 'reads' },
66
+ fix: 'none',
67
+ applies: 'always',
64
68
  run: (c) => {
65
69
  const { units } = resolveUnits(c.root, c.config, checkUnitReader(c))
66
70
  const version = new Map(units.map((u) => [u.name, u.version]))
@@ -30,6 +30,10 @@ export const configBarrelImport = check({
30
30
  title: 'config import path',
31
31
  description: 'repo.config.ts imports `defineRepo` from the narrow module, never the barrel',
32
32
  owner: 'build/shipkit/config-keys.md',
33
+ runs: { onPreCommit: { budgetMs: 20 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
34
+ cache: { by: 'reads' },
35
+ fix: 'ci',
36
+ applies: 'always',
33
37
  run: (c) => {
34
38
  const src = c.read(CONFIG)
35
39
  if (src == null) return void c.ok(`${CONFIG} absent — nothing to narrow`)
@@ -43,7 +47,7 @@ export const configBarrelImport = check({
43
47
  }
44
48
  c.fail(`${CONFIG} imports from the package barrel (~400ms of module load on every shipkit run, the pre-commit hook included)`)
45
49
  },
46
- fix: (c): FixResult => {
50
+ repair: (c): FixResult => {
47
51
  const src = c.read(CONFIG)
48
52
  if (src == null) return { changed: false, summary: `${CONFIG} absent` }
49
53
  if (!BARREL.test(src)) return { changed: false, summary: 'already narrow' }
@@ -1,4 +1,4 @@
1
- // SK013 — the harness half of ONE WAY IN (build/shipkit.md). Rule SK001 routes
1
+ // SK013 — the harness half of ONE WAY IN (build/shipkit/one-way-in.md). Rule SK001 routes
2
2
  // the package.json scripts, and that alone is not enough: `bun test` is a bun
3
3
  // BUILTIN, so no script can intercept it and the reflex stays open. This rule
4
4
  // makes the repo carry the two things that CAN close it, and repairs them:
@@ -19,7 +19,7 @@ import { rootPublishesNpm } from '../core/repo-facts.ts'
19
19
  import type { RepoConfig } from '../repo-config.ts'
20
20
  import { check, type CheckCtx, type FixResult } from '../check.ts'
21
21
 
22
- const OWNER = 'build/shipkit.md'
22
+ const OWNER = 'build/shipkit/one-way-in.md'
23
23
  export const SETTINGS_PATH = '.claude/settings.json'
24
24
 
25
25
  type Settings = {
@@ -124,6 +124,10 @@ export const oneWayInPolicy = check({
124
124
  // keyed on what it reads: .claude/settings.json, package.json, and whether
125
125
  // the declared guard script resolves. A guard at an absolute path lives
126
126
  // outside the repo, so an answer that probed one is never stored
127
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
128
+ cache: { by: 'reads' },
129
+ fix: 'ci',
130
+ applies: 'always',
127
131
  run: async (c) => {
128
132
  if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
129
133
  const d = c
@@ -171,7 +175,7 @@ export const oneWayInPolicy = check({
171
175
  // MERGES, never rewrites: this file also carries the attribution silencer,
172
176
  // a repo's own allow list and its own hooks, and clobbering those to fix a
173
177
  // deny list would cost more than the finding it repairs
174
- fix: async (c): Promise<FixResult> => {
178
+ repair: async (c): Promise<FixResult> => {
175
179
  if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
176
180
  const d = c
177
181
  const want = denyRules(exemptSlots(c.config))
@@ -30,6 +30,10 @@ export const proseSte = check({
30
30
  owner: OWNER,
31
31
  // keyed on what it reads: each include glob's listing and every file in
32
32
  // scope, so an edit outside the declared prose never reruns it
33
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
34
+ cache: { by: 'reads' },
35
+ fix: 'none',
36
+ applies: 'always',
33
37
  run: (c) => {
34
38
  const cfg = c.config.prose
35
39
  if (cfg == null || (cfg.include ?? []).length === 0) {
@@ -12,10 +12,12 @@ export const nestedRepo = check({
12
12
  title: 'nested repo',
13
13
  description: 'a config inside a larger work tree declares `nested: true`, and only there',
14
14
  owner: 'build/shipkit/config-keys.md',
15
+ runs: { onPreCommit: { budgetMs: 20 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
15
16
  // the declaration, and where the git work tree root sits, which no tree
16
17
  // id describes, so its path is the rest of the key
17
- inputs: [CONFIG_FILE],
18
- cacheKey: (env) => `nested=${env.config.nested === true}|top=${nestedIn(env.root) ?? 'root'}`,
18
+ cache: { by: 'files', inputs: [CONFIG_FILE], key: (env) => `nested=${env.config.nested === true}|top=${nestedIn(env.root) ?? 'root'}` },
19
+ fix: 'none',
20
+ applies: 'always',
19
21
  run: (c) => {
20
22
  const top = nestedIn(c.root)
21
23
  const declared = c.config.nested === true
@@ -49,6 +49,10 @@ export const legacyCheckShapes = check({
49
49
  blocking: true,
50
50
  // keyed on what it reads: the listing of check files, their sources, and
51
51
  // the config. A gitignored check file is no declaration of this repo
52
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
53
+ cache: { by: 'reads' },
54
+ fix: 'manual',
55
+ applies: 'always',
52
56
  run: (c) => {
53
57
  const s = legacyShapes(c, c.config)
54
58
  if (s.count === 0 && s.placeholders.length === 0) {
@@ -70,7 +74,7 @@ export const legacyCheckShapes = check({
70
74
  const head = s.checkFiles.slice(0, 3).map((f) => f.rel).join(', ')
71
75
  c.warn(`${s.checkFiles.length} legacy check script(s) read by the static reader: ${head}${s.checkFiles.length > 3 ? `, and ${s.checkFiles.length - 3} more` : ''}`)
72
76
  const off = s.checkFiles.filter((f) => !f.read.gate).length
73
- if (off > 0) c.warn(`${off} of them carry \`export const gate = false\`, which is \`when: 'health'\``)
77
+ if (off > 0) c.warn(`${off} of them carry \`export const gate = false\`, which is every gate axis false and onHealth true`)
74
78
  }
75
79
  if (s.checkDefaults) c.warn(`repo.config.ts checkDefaults lets a file forget \`blocking\` — the value belongs in each file`)
76
80
  if (s.tasks.length > 0) {
@@ -78,10 +82,10 @@ export const legacyCheckShapes = check({
78
82
  c.warn(`${s.tasks.length} \`tasks: []\` entr(ies), ${data} of them pure data — each one is a *.check.ts file with \`build: true\``)
79
83
  }
80
84
  if (s.rules.length > 0) c.warn(`${s.rules.length} \`rules: []\` entr(ies): ${s.rules.join(', ')} — each one is a *.check.ts file whose \`run\` is its \`lint\``)
81
- if (s.ruleExempt.length > 0) c.warn(`ruleExempt names ${s.ruleExempt.join(', ')} — the spelling is \`when: { id: 'off:<why>' }\``)
85
+ if (s.ruleExempt.length > 0) c.warn(`ruleExempt names ${s.ruleExempt.join(', ')} — the spelling is \`off: { id: '<why>' }\`, and \`shipkit ci\` ports it (SK029)`)
82
86
  // what the fixer rewrites without guessing: the exemptions, the tasks
83
87
  // once all of them are data, checkDefaults once no script leans on it
84
- const mechanical = s.ruleExempt.length + (s.tasks.every((x) => x.data) ? s.tasks.length : 0) + (s.checkDefaults && s.checkFiles.length === 0 ? 1 : 0)
88
+ const mechanical = (s.tasks.every((x) => x.data) ? s.tasks.length : 0) + (s.checkDefaults && s.checkFiles.length === 0 ? 1 : 0)
85
89
  c.warn(
86
90
  `${s.count} declaration(s) in a shape the check file replaced: still read, and refused by the first shipkit release after \`shipkit fleet\` shows no repo below 0.40 and none still carrying a check script`,
87
91
  mechanical > 0
@@ -89,7 +93,7 @@ export const legacyCheckShapes = check({
89
93
  : `nothing left is mechanical, so the port is by hand: each line above becomes a *.check.ts file (${OWNER})`,
90
94
  )
91
95
  },
92
- fix: async (c) => await fixLegacyShapes(c),
96
+ repair: async (c) => await fixLegacyShapes(c),
93
97
  })
94
98
 
95
99
  export default legacyCheckShapes
@@ -162,17 +166,14 @@ export async function fixLegacyShapes(c: CheckCtx, rewrite: ConfigRewriter = inO
162
166
  for (const r of config.rules ?? [])
163
167
  hand.push(`rule ${r.id}: move it into ${checkFileName(r.id)} beside repo.config.ts, \`lint\` becomes \`run\` and \`code\` goes (the file's own path is its closure)`)
164
168
 
165
- // 4. the exemptions that were their own key, as `when` entries (the
166
- // managed block is the one writer), then 5. the config keys, last: a
167
- // key is only dropped once every declaration that leaned on it has a
168
- // home. Both load the config, so they run in their own process
169
- const cfgPlan: ConfigPlan = { when: {}, drop: [], name: config.name }
170
- for (const [id, why] of Object.entries(config.ruleExempt ?? {})) cfgPlan.when[id] = `off:${why}`
169
+ // 4. the config keys, last: a key is only dropped once every declaration
170
+ // that leaned on it has a home. It loads the config, so it runs in its
171
+ // own process. `ruleExempt` is SK029's: it ports it into `off`
172
+ const cfgPlan: ConfigPlan = { off: {}, drop: [], name: config.name }
171
173
  if (s.checkDefaults && filesLeft === 0) cfgPlan.drop.push({ key: 'checkDefaults', why: 'every file now declares its own blocking' })
172
174
  else if (s.checkDefaults) hand.push(`repo.config.ts \`checkDefaults\` stays until the ${filesLeft} file(s) above declare their own \`blocking\``)
173
175
  if (tasksMove) cfgPlan.drop.push({ key: 'tasks', why: 'every task is a check file now' })
174
- if (s.ruleExempt.length > 0) cfgPlan.drop.push({ key: 'ruleExempt', why: 'the exemptions are `when` entries now', afterWhen: true })
175
- if (Object.keys(cfgPlan.when).length > 0 || cfgPlan.drop.length > 0) {
176
+ if (cfgPlan.drop.length > 0) {
176
177
  const out = await rewrite(cfgPlan)
177
178
  did.push(...out.did)
178
179
  hand.push(...out.hand)
@@ -12,7 +12,7 @@ import { check } from '../check.ts'
12
12
  import type { CheckCtx } from '../core/check-ctx.ts'
13
13
  import { checkFloorReader, resolveFloor } from '../core/floor-resolve.ts'
14
14
  import { rootPublishesNpm } from '../core/repo-facts.ts'
15
- import { whenOfTask } from '../core/when.ts'
15
+ import { repoOff } from '../core/when.ts'
16
16
  import { hubProblemMatcher } from '../problems/problem-line.ts'
17
17
  import { indentAt, parseSpans, plain, removalSpan, type Node } from '../scaffold/jsonc-spans.ts'
18
18
 
@@ -24,12 +24,15 @@ type Arr = Extract<Node, { kind: 'array' }>
24
24
 
25
25
  const prop = (o: Obj, key: string): Node | undefined => o.props.find((p) => p.key === key)?.value
26
26
 
27
+ /** the type checker binaries: tsc, and tsgo, its native port */
28
+ const TSC_BINS = new Set(['tsc', 'tsgo'])
29
+
27
30
  /** the house type checker task and nothing that only looks like it: the exact
28
- * label, a tsc binary, in watch mode */
31
+ * label, a tsc or tsgo binary, in watch mode */
29
32
  function isTscWatcher(t: Record<string, unknown>): boolean {
30
33
  const cmd = typeof t['command'] === 'string' ? t['command'] : ''
31
34
  const args = Array.isArray(t['args']) ? t['args'] : []
32
- return t['label'] === TSC_LABEL && basename(cmd) === 'tsc' && args.includes('--watch')
35
+ return t['label'] === TSC_LABEL && TSC_BINS.has(basename(cmd)) && args.includes('--watch')
33
36
  }
34
37
 
35
38
  /** the hub task: `bun run hub`, or the house label */
@@ -90,9 +93,17 @@ export function repairTasks(text: string, o: RepairOpts): { text: string; change
90
93
  if (/\/\/.*(master task|subtask)/i.test(text)) changes.push('a comment still describes a master task with subtasks: rewrite it by hand')
91
94
  }
92
95
 
96
+ // a wrapper that started only the TSC task starts nothing once it goes
97
+ const emptied = !dropTsc ? [] : tasks.items.flatMap((n, i) => (n !== wrapper && n.kind === 'object' && isWrapper(record(n)) && depsOf(n).length === 0 ? [i] : []))
98
+ for (const i of emptied) {
99
+ const [start, end] = removalSpan(text, tasks, i)
100
+ edits.push({ start, end, body: '' })
101
+ changes.push(`removed the '${labelOf(tasks.items[i]!)}' wrapper, which started nothing else`)
102
+ }
103
+
93
104
  if (dropTsc) {
94
105
  for (const t of items) {
95
- if (t === wrapper) continue
106
+ if (t === wrapper || emptied.some((i) => tasks.items[i] === t)) continue
96
107
  const d = prop(t, 'dependsOn')
97
108
  if (d?.kind === 'array') {
98
109
  const idx = d.items.findIndex((x) => x.kind === 'scalar' && x.value === TSC_LABEL)
@@ -148,13 +159,13 @@ export function repairTasks(text: string, o: RepairOpts): { text: string; change
148
159
  }
149
160
 
150
161
  /** whether this repo's gate runs a typecheck: a floor slot or a task that
151
- * resolves, and no `when` turning it off */
162
+ * resolves, and no `off` entry turning it off */
152
163
  export function typecheckOn(c: CheckCtx): boolean {
153
164
  const config = c.config
154
165
  let floor: ReturnType<typeof resolveFloor>
155
166
  try { floor = resolveFloor(config, c.root, checkFloorReader(c)) } catch { return false }
156
167
  const t = [...floor.tasks, ...(config.tasks ?? [])].find((x) => x.id === 'typecheck')
157
- return t != null && whenOfTask(config, t).at !== 'off'
168
+ return t != null && repoOff(config, t.id) == null
158
169
  }
159
170
 
160
171
  /** the tasks file this check may touch: none, or left alone because .vscode/
@@ -178,6 +189,10 @@ export const vscodeHubTask = check({
178
189
  title: 'vscode hub task',
179
190
  description: 'the vscode hub task carries the problem matcher and no extra tsc watcher runs',
180
191
  owner: 'build/vscode-tasks.md',
192
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: 'moved', onClock: false },
193
+ cache: { by: 'reads' },
194
+ fix: 'manual',
195
+ applies: 'always',
181
196
  run: (c) => {
182
197
  if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
183
198
  const own = ownTasks(c)
@@ -190,7 +205,7 @@ export const vscodeHubTask = check({
190
205
  if (r.changes.length === 0) return c.ok(`${TASKS}: the hub task feeds the problems pane`)
191
206
  for (const ch of r.changes) c.fail(`${TASKS}: ${ch.replace(/^removed/, 'should remove').replace(/^dropped/, 'should drop').replace(/^wrote/, 'should write').replace(/^folded/, 'should fold')}`)
192
207
  },
193
- fix: (c) => {
208
+ repair: (c) => {
194
209
  if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
195
210
  const own = ownTasks(c)
196
211
  if (typeof own === 'string') return { changed: false, summary: `no ${TASKS} to repair` }
@@ -1,4 +1,4 @@
1
- // SK018 — every test file runs in a check (build/shipkit.md, the test checks).
1
+ // SK018 — every test file runs in a check (build/shipkit/test-runner.md).
2
2
  // A `*.test.ts` that no check runs is a green nobody measured: the file says
3
3
  // something is proved, and nothing proves it. Each file belongs to the release
4
4
  // unit whose dir holds it (the root takes the rest), and the unit's own
@@ -12,7 +12,11 @@ export const testFilesRun = check({
12
12
  blocking: true,
13
13
  title: 'test files run',
14
14
  description: 'every test file in the repo runs in a test check or is ignored on purpose',
15
- owner: 'build/shipkit.md',
15
+ owner: 'build/shipkit/test-runner.md',
16
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
17
+ cache: { by: 'reads' },
18
+ fix: 'none',
19
+ applies: 'always',
16
20
  run: (c) => {
17
21
  const files = c.list(TEST_GLOB).filter(isTestFile)
18
22
  if (files.length === 0) return c.ok('no test file in this repo')