rvlib-shipkit 1.4.0 → 1.5.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 (161) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/check.ts +68 -95
  5. package/src/checks/SK001-package-json-shape.check.ts +6 -2
  6. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  7. package/src/checks/SK003-claude-md-shape.check.ts +5 -1
  8. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  9. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  10. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  11. package/src/checks/SK007-github-visibility.check.ts +4 -2
  12. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  13. package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
  14. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  15. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  16. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  17. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  18. package/src/checks/SK014-prose-ste.check.ts +4 -0
  19. package/src/checks/SK015-nested-repo.check.ts +4 -2
  20. package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
  21. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  22. package/src/checks/SK018-test-files-run.check.ts +6 -2
  23. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  24. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  25. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  26. package/src/checks/SK024-identity-colors.check.ts +5 -3
  27. package/src/checks/SK025-release-drift.check.ts +4 -5
  28. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  29. package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
  30. package/src/checks/SK028-readme.check.ts +5 -1
  31. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  32. package/src/cli/cmd_Asks.ts +5 -5
  33. package/src/cli/cmd_Assets.ts +7 -7
  34. package/src/cli/cmd_Bump.ts +12 -11
  35. package/src/cli/cmd_Check.ts +8 -7
  36. package/src/cli/cmd_Checks.ts +7 -6
  37. package/src/cli/cmd_Ci.ts +12 -11
  38. package/src/cli/cmd_Commands.ts +16 -13
  39. package/src/cli/cmd_ConfigKeys.ts +7 -6
  40. package/src/cli/cmd_Contracts.ts +3 -3
  41. package/src/cli/cmd_CrossRepoActivity.ts +6 -5
  42. package/src/cli/cmd_Fleet.ts +9 -7
  43. package/src/cli/cmd_GlobalProcesses.ts +116 -4
  44. package/src/cli/cmd_Home.ts +23 -17
  45. package/src/cli/cmd_Hooks.ts +6 -5
  46. package/src/cli/cmd_Init.ts +5 -4
  47. package/src/cli/cmd_Keys.ts +8 -7
  48. package/src/cli/cmd_Mv.ts +7 -6
  49. package/src/cli/cmd_Needs.ts +4 -3
  50. package/src/cli/cmd_Overview.ts +4 -3
  51. package/src/cli/cmd_Pb.ts +2 -2
  52. package/src/cli/cmd_Planned.ts +6 -5
  53. package/src/cli/cmd_Prose.ts +5 -5
  54. package/src/cli/cmd_Reflect.ts +7 -7
  55. package/src/cli/cmd_Release.ts +6 -5
  56. package/src/cli/cmd_Releases.ts +5 -4
  57. package/src/cli/cmd_Serve.ts +4 -3
  58. package/src/cli/cmd_Smoke.ts +5 -4
  59. package/src/cli/cmd_Start.ts +7 -6
  60. package/src/cli/cmd_Status.ts +4 -3
  61. package/src/cli/cmd_Tui.ts +3 -2
  62. package/src/cli/cmd_When.ts +71 -0
  63. package/src/cli/cmd_Wiki.ts +4 -4
  64. package/src/cli.ts +7 -2
  65. package/src/collections/checks.ts +2 -0
  66. package/src/collections/collection-types.ts +9 -4
  67. package/src/core/canonical-scripts.ts +1 -1
  68. package/src/core/check-runner.ts +37 -20
  69. package/src/core/config-rewrite.ts +37 -24
  70. package/src/core/discover-checks.ts +23 -2
  71. package/src/core/legacy-shapes.ts +39 -10
  72. package/src/core/one-way-in-policy.ts +2 -2
  73. package/src/core/outcome-words.ts +18 -2
  74. package/src/core/port-check-cli.ts +52 -0
  75. package/src/core/port-check-file.ts +295 -0
  76. package/src/core/port-check-files.ts +31 -0
  77. package/src/core/retention.ts +0 -14
  78. package/src/core/run-check.ts +12 -9
  79. package/src/core/run-ci.ts +8 -4
  80. package/src/core/run-fix.ts +30 -23
  81. package/src/core/run-fixers.ts +42 -5
  82. package/src/core/runs-matrix.ts +179 -0
  83. package/src/core/runs.ts +242 -0
  84. package/src/core/update-policy.ts +20 -0
  85. package/src/core/when-block.ts +29 -30
  86. package/src/core/when.ts +41 -103
  87. package/src/core/wire.ts +10 -14
  88. package/src/fleet/scan-fleet.ts +20 -4
  89. package/src/fleet/update-fleet.ts +6 -6
  90. package/src/home/home-launchd.ts +20 -4
  91. package/src/home/home-routes.ts +12 -1
  92. package/src/home/hubs.ts +5 -5
  93. package/src/home/pm.ts +8 -0
  94. package/src/home/run-pm.ts +1 -0
  95. package/src/home/serve-web.ts +2 -2
  96. package/src/home/supervise.ts +24 -1
  97. package/src/hooks/run-hooks.ts +48 -4
  98. package/src/init-repo.ts +6 -6
  99. package/src/ledger/gate-checks.ts +113 -59
  100. package/src/ledger/git-key.ts +5 -1
  101. package/src/ledger/ledger.ts +5 -5
  102. package/src/ledger/toolchain-identity.ts +45 -27
  103. package/src/plan/repo-source.ts +18 -39
  104. package/src/release/run-release.ts +5 -0
  105. package/src/repo-config.ts +30 -55
  106. package/src/rules/rule.ts +2 -2
  107. package/src/scaffold/files/agent-coding.md +1 -1
  108. package/src/scaffold/files/agent-index.md +1 -1
  109. package/src/scaffold/files/verify-skill.md +2 -2
  110. package/src/serve/hub-app/activity-st.ts +5 -4
  111. package/src/serve/hub-app/activity-view.tsx +1 -1
  112. package/src/serve/hub-app/admin-view.tsx +13 -11
  113. package/src/serve/hub-app/check-view.tsx +66 -46
  114. package/src/serve/hub-app/check-words.ts +1 -11
  115. package/src/serve/hub-app/checks-svc.ts +14 -35
  116. package/src/serve/hub-app/col-tables-view.tsx +13 -6
  117. package/src/serve/hub-app/cols-svc.ts +5 -4
  118. package/src/serve/hub-app/dock-st.ts +3 -2
  119. package/src/serve/hub-app/dock-view.tsx +36 -0
  120. package/src/serve/hub-app/dock.css +6 -0
  121. package/src/serve/hub-app/float-cards.tsx +30 -0
  122. package/src/serve/hub-app/floating.tsx +41 -4
  123. package/src/serve/hub-app/git-st.ts +42 -0
  124. package/src/serve/hub-app/git-view.tsx +61 -0
  125. package/src/serve/hub-app/hash-route.ts +45 -1
  126. package/src/serve/hub-app/header-bar.tsx +62 -72
  127. package/src/serve/hub-app/health-svc.ts +3 -2
  128. package/src/serve/hub-app/home-st.ts +14 -2
  129. package/src/serve/hub-app/home-strip.css +43 -8
  130. package/src/serve/hub-app/home-strip.tsx +111 -47
  131. package/src/serve/hub-app/home.html +1 -0
  132. package/src/serve/hub-app/hub-app.tsx +14 -4
  133. package/src/serve/hub-app/hub-types.ts +5 -2
  134. package/src/serve/hub-app/hub.css +65 -43
  135. package/src/serve/hub-app/icons.tsx +11 -0
  136. package/src/serve/hub-app/index.html +1 -0
  137. package/src/serve/hub-app/keys.ts +5 -2
  138. package/src/serve/hub-app/live-svc.ts +3 -2
  139. package/src/serve/hub-app/machine-st.ts +18 -22
  140. package/src/serve/hub-app/plan-view.tsx +51 -0
  141. package/src/serve/hub-app/planned-st.ts +9 -4
  142. package/src/serve/hub-app/processes-view.tsx +38 -0
  143. package/src/serve/hub-app/projects-svc.ts +15 -10
  144. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  145. package/src/serve/hub-app/reach.ts +36 -0
  146. package/src/serve/hub-app/readme-svc.ts +3 -2
  147. package/src/serve/hub-app/repo-sections.ts +117 -0
  148. package/src/serve/hub-app/repo-st.ts +46 -9
  149. package/src/serve/hub-app/sidebar.tsx +12 -37
  150. package/src/serve/hub-app/timeline-svc.ts +3 -2
  151. package/src/serve/hub-app/tree-build.ts +149 -155
  152. package/src/serve/hub-app/tree-row.tsx +39 -9
  153. package/src/serve/hub-app/tree-svc.ts +70 -49
  154. package/src/serve/hub-app/when-view.tsx +89 -0
  155. package/src/serve/hub-app/when.css +17 -0
  156. package/src/serve/hub-runtime.ts +3 -16
  157. package/src/serve/run-serve.ts +1 -1
  158. package/src/serve/serve-hub.ts +4 -14
  159. package/src/serve/hub-app/processes-svc.ts +0 -63
  160. package/src/serve/hub-app/repo-sections.tsx +0 -61
  161. package/src/serve/repo-processes.ts +0 -89
@@ -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')
@@ -1,5 +1,5 @@
1
- // SK019 — tests run through the generated test checks only (build/shipkit.md,
2
- // the test checks). The floor `test` slot and one `test:<unit>` per declared
1
+ // SK019 — tests run through the generated test checks only (build/shipkit/test-runner.md).
2
+ // The floor `test` slot and one `test:<unit>` per declared
3
3
  // unit are the ONE way tests run: in the unit's dir with its bunfig and
4
4
  // preload, parallel, timed against the budget, and planned file by file by
5
5
  // SK018. A hand written check that runs `bun test` itself is a second runner
@@ -9,7 +9,7 @@
9
9
  // Its own id rather than a line of SK018: SK018 answers "does every test file
10
10
  // run", this one "does anything else run them", and each is read from
11
11
  // different files (test files and bunfigs there, check sources and the config
12
- // here), so each keeps its own key, row and `when` switch.
12
+ // here), so each keeps its own key, row and `off` switch.
13
13
  import { dirname, posix, relative } from 'path'
14
14
  import { check } from '../check.ts'
15
15
  import { repoCheckSources } from '../core/legacy-shapes.ts'
@@ -36,7 +36,11 @@ export const testsRunInUnits = check({
36
36
  blocking: true,
37
37
  title: 'tests run in unit checks',
38
38
  description: 'no check runs the test runner itself: tests run only in the generated test checks',
39
- owner: 'build/shipkit.md',
39
+ owner: 'build/shipkit/test-runner.md',
40
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
41
+ cache: { by: 'reads' },
42
+ fix: 'none',
43
+ applies: 'always',
40
44
  run: (c) => {
41
45
  // the toolkit's own checks (this file's dir) live inside the repo that
42
46
  // hosts it, and a built-in that WRITES a test script (SK001) is not a
@@ -132,6 +132,10 @@ export const selfSpawningCheck = check({
132
132
  description: 'no check file runs its own body by spawning itself',
133
133
  owner: OWNER,
134
134
  blocking: true,
135
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
136
+ cache: { by: 'reads' },
137
+ fix: 'none',
138
+ applies: 'always',
135
139
  run: (c) => {
136
140
  const found = repoCheckSources(c).flatMap((f) => {
137
141
  if (f.src == null || isLegacyCheckSource(f.src)) return []
@@ -1,4 +1,4 @@
1
- // SK022 machine runs the pin (build/shipkit.md): on a machine with a house
1
+ // SK022 machine runs the pin (build/shipkit/home.md): on a machine with a house
2
2
  // every shipkit runs the house copy, whatever a repo installed, so a repo's
3
3
  // rvlib-shipkit pin is a minimum. It warns when the repo asks for a version
4
4
  // newer than the one this process runs: a rule or an api that pin relies on
@@ -24,11 +24,13 @@ export const machineRunsThePin = check({
24
24
  blocking: false,
25
25
  title: 'machine runs the pin',
26
26
  description: 'the shipkit this machine runs is at least the version this repo pins',
27
- owner: 'build/shipkit.md',
27
+ owner: 'build/shipkit/home.md',
28
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
28
29
  // the pin, the install (gitignored) and the version this machine runs,
29
30
  // which is no file of the repo
30
- inputs: ['package.json'],
31
- cacheKey: (env) => `${LIB_VERSION}|installed=${fileStamp(join(env.root, 'node_modules', NAME, 'package.json'))}`,
31
+ cache: { by: 'files', inputs: ['package.json'], key: (env) => `${LIB_VERSION}|installed=${fileStamp(join(env.root, 'node_modules', NAME, 'package.json'))}` },
32
+ fix: 'none',
33
+ applies: 'always',
32
34
  run: (c) => {
33
35
  const pkg = c.json<Pkg>('package.json')
34
36
  const spec = pkg?.devDependencies?.[NAME] ?? pkg?.dependencies?.[NAME]
@@ -18,9 +18,11 @@ export const identityColors = check({
18
18
  title: 'identity colors',
19
19
  description: 'repo.config.ts declares identity colours: colorLight and colorDark, two hex',
20
20
  owner: 'build/shipkit/built-in-checks.md',
21
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: 'moved', onClock: false },
21
22
  // the config, and the installed lib that says whether it knows the key
22
- inputs: [CONFIG_FILE],
23
- cacheKey: (env) => `installed=${fileStamp(join(env.root, INSTALLED_CONFIG_SRC))}`,
23
+ cache: { by: 'files', inputs: [CONFIG_FILE], key: (env) => `installed=${fileStamp(join(env.root, INSTALLED_CONFIG_SRC))}` },
24
+ fix: 'manual',
25
+ applies: 'always',
24
26
  run: (c) => {
25
27
  const text = c.read(CONFIG_FILE)
26
28
  if (text == null) return c.note(`no ${CONFIG_FILE}`)
@@ -32,7 +34,7 @@ export const identityColors = check({
32
34
  if (why != null) return c.warn(`${CONFIG_FILE}: ${why}`, FIX)
33
35
  c.ok(`identity ${id!.colorLight} on light, ${id!.colorDark} on dark`)
34
36
  },
35
- fix: async (c) => {
37
+ repair: async (c) => {
36
38
  const text = c.read(CONFIG_FILE)
37
39
  if (text == null) return { changed: false, summary: `no ${CONFIG_FILE}` }
38
40
  const have = identityOfText(text)
@@ -41,11 +41,10 @@ export const releaseDrift = check({
41
41
  title: 'release drift',
42
42
  description: 'every unit this repo releases matches npm and its push remote',
43
43
  owner: 'build/shipkit/built-in-checks.md',
44
- when: 'health',
45
- ttl: '20h',
46
- every: (last) => (last?.status === 'pass' ? ['20h', '28h'] : '4h'),
47
- planOff: (env) => (releasedUnits(env.root, env.config).length === 0 ? 'this repo releases nothing' : null),
48
- cacheKey: (env) => releasedUnits(env.root, env.config).map((u) => `${u.id}@${u.version}`).join(','),
44
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: false, onClock: { every: (last) => (last?.status === 'pass' ? ['20h', '28h'] : '4h'), inactive: 'wait' } },
45
+ cache: { by: 'ttl', span: '20h', key: (env) => releasedUnits(env.root, env.config).map((u) => `${u.id}@${u.version}`).join(',') },
46
+ fix: 'none',
47
+ applies: (env) => (releasedUnits(env.root, env.config).length === 0 ? 'this repo releases nothing' : null),
49
48
  run: async (c) => {
50
49
  const ids = new Set(releasedUnits(c.root, c.config).map((u) => u.id))
51
50
  if (ids.size === 0) return void c.lines.push({ kind: 'skip', text: 'this repo releases nothing: no npm root, no release config, no units' })