rvlib-shipkit 1.3.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 (203) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/asks/asks-queue.ts +4 -2
  5. package/src/assets/kits/identity-kit.ts +15 -13
  6. package/src/assets/run-assets.ts +12 -2
  7. package/src/check.ts +73 -98
  8. package/src/checks/SK001-package-json-shape.check.ts +26 -4
  9. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  10. package/src/checks/SK003-claude-md-shape.check.ts +5 -1
  11. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  12. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  13. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  14. package/src/checks/SK007-github-visibility.check.ts +4 -2
  15. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  16. package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
  17. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  18. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  19. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  20. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  21. package/src/checks/SK014-prose-ste.check.ts +20 -6
  22. package/src/checks/SK015-nested-repo.check.ts +4 -2
  23. package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
  24. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  25. package/src/checks/SK018-test-files-run.check.ts +6 -2
  26. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  27. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  28. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  29. package/src/checks/SK024-identity-colors.check.ts +5 -3
  30. package/src/checks/SK025-release-drift.check.ts +4 -5
  31. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  32. package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
  33. package/src/checks/SK028-readme.check.ts +10 -2
  34. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  35. package/src/cli/cmd_Asks.ts +5 -5
  36. package/src/cli/cmd_Assets.ts +7 -7
  37. package/src/cli/cmd_Bump.ts +12 -11
  38. package/src/cli/cmd_Check.ts +8 -7
  39. package/src/cli/cmd_Checks.ts +7 -6
  40. package/src/cli/cmd_Ci.ts +12 -11
  41. package/src/cli/cmd_Commands.ts +16 -13
  42. package/src/cli/cmd_ConfigKeys.ts +7 -6
  43. package/src/cli/cmd_Contracts.ts +3 -3
  44. package/src/cli/cmd_CrossRepoActivity.ts +10 -6
  45. package/src/cli/cmd_Fleet.ts +9 -7
  46. package/src/cli/cmd_GlobalProcesses.ts +116 -4
  47. package/src/cli/cmd_Home.ts +23 -17
  48. package/src/cli/cmd_Hooks.ts +6 -5
  49. package/src/cli/cmd_Init.ts +5 -4
  50. package/src/cli/cmd_Keys.ts +8 -7
  51. package/src/cli/cmd_Mv.ts +7 -6
  52. package/src/cli/cmd_Needs.ts +4 -3
  53. package/src/cli/cmd_Overview.ts +4 -3
  54. package/src/cli/cmd_Pb.ts +2 -2
  55. package/src/cli/cmd_Planned.ts +6 -5
  56. package/src/cli/cmd_Prose.ts +5 -5
  57. package/src/cli/cmd_Reflect.ts +7 -7
  58. package/src/cli/cmd_Release.ts +6 -5
  59. package/src/cli/cmd_Releases.ts +5 -4
  60. package/src/cli/cmd_Serve.ts +4 -3
  61. package/src/cli/cmd_Smoke.ts +5 -4
  62. package/src/cli/cmd_Start.ts +7 -6
  63. package/src/cli/cmd_Status.ts +4 -3
  64. package/src/cli/cmd_Tui.ts +3 -2
  65. package/src/cli/cmd_When.ts +71 -0
  66. package/src/cli/cmd_Wiki.ts +4 -4
  67. package/src/cli.ts +7 -2
  68. package/src/collections/checks.ts +2 -0
  69. package/src/collections/collection-types.ts +24 -4
  70. package/src/collections/providers.ts +6 -2
  71. package/src/contracts/apply-contracts.ts +9 -8
  72. package/src/contracts/contract-types.ts +8 -2
  73. package/src/contracts/contracts-engine.ts +34 -13
  74. package/src/contracts/manifests/identity.contract.ts +6 -5
  75. package/src/contracts/manifests/social.contract.ts +4 -2
  76. package/src/contracts/manifests/steam.contract.ts +7 -3
  77. package/src/contracts/web-surface.ts +59 -0
  78. package/src/core/budget.ts +19 -9
  79. package/src/core/canonical-scripts.ts +1 -1
  80. package/src/core/check-runner.ts +63 -25
  81. package/src/core/config-rewrite.ts +37 -24
  82. package/src/core/discover-checks.ts +31 -7
  83. package/src/core/layout.ts +2 -0
  84. package/src/core/legacy-shapes.ts +39 -10
  85. package/src/core/motherhome.ts +88 -66
  86. package/src/core/one-way-in-policy.ts +2 -2
  87. package/src/core/outcome-words.ts +18 -2
  88. package/src/core/port-check-cli.ts +52 -0
  89. package/src/core/port-check-file.ts +295 -0
  90. package/src/core/port-check-files.ts +31 -0
  91. package/src/core/retention.ts +0 -14
  92. package/src/core/run-check.ts +14 -14
  93. package/src/core/run-ci.ts +11 -7
  94. package/src/core/run-fix.ts +30 -23
  95. package/src/core/run-fixers.ts +42 -5
  96. package/src/core/runs-matrix.ts +179 -0
  97. package/src/core/runs.ts +242 -0
  98. package/src/core/update-policy.ts +20 -0
  99. package/src/core/when-block.ts +29 -30
  100. package/src/core/when.ts +41 -103
  101. package/src/core/wire.ts +34 -9
  102. package/src/fleet/scan-fleet.ts +20 -4
  103. package/src/fleet/update-fleet.ts +6 -6
  104. package/src/home/activity-shape.ts +4 -1
  105. package/src/home/activity.ts +22 -15
  106. package/src/home/gui-app/ShipkitGui.swift +144 -25
  107. package/src/home/gui-app.ts +93 -30
  108. package/src/home/home-launchd.ts +30 -6
  109. package/src/home/home-routes.ts +14 -2
  110. package/src/home/home-start.ts +33 -5
  111. package/src/home/hubs.ts +5 -5
  112. package/src/home/machine-stores.ts +4 -2
  113. package/src/home/open-app.ts +23 -10
  114. package/src/home/pm-config.ts +15 -11
  115. package/src/home/pm-inputs.ts +76 -0
  116. package/src/home/pm.ts +11 -3
  117. package/src/home/process-rows.ts +23 -6
  118. package/src/home/resources.ts +67 -15
  119. package/src/home/run-daemon.ts +6 -1
  120. package/src/home/run-pm.ts +30 -5
  121. package/src/home/serve-web.ts +2 -2
  122. package/src/home/supervise.ts +24 -1
  123. package/src/home/widgets.ts +32 -3
  124. package/src/hooks/commit-message.ts +2 -3
  125. package/src/hooks/run-hooks.ts +60 -6
  126. package/src/init-repo.ts +6 -6
  127. package/src/ledger/gate-checks.ts +115 -61
  128. package/src/ledger/git-key.ts +5 -1
  129. package/src/ledger/ledger.ts +5 -5
  130. package/src/ledger/store.ts +11 -0
  131. package/src/ledger/toolchain-identity.ts +45 -27
  132. package/src/ledger/world-cache.ts +17 -15
  133. package/src/plan/repo-source.ts +18 -39
  134. package/src/release/run-release.ts +5 -0
  135. package/src/repo-config.ts +36 -56
  136. package/src/rules/rule.ts +2 -2
  137. package/src/scaffold/files/agent-coding.md +1 -1
  138. package/src/scaffold/files/agent-index.md +1 -1
  139. package/src/scaffold/files/verify-skill.md +2 -2
  140. package/src/serve/hub-app/activity-st.ts +5 -4
  141. package/src/serve/hub-app/activity-view.tsx +4 -4
  142. package/src/serve/hub-app/admin-view.tsx +13 -11
  143. package/src/serve/hub-app/check-view.tsx +66 -46
  144. package/src/serve/hub-app/check-words.ts +1 -11
  145. package/src/serve/hub-app/checks-svc.ts +16 -36
  146. package/src/serve/hub-app/col-tables-view.tsx +131 -0
  147. package/src/serve/hub-app/col-tables.ts +115 -0
  148. package/src/serve/hub-app/cols-svc.ts +33 -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 +7 -1
  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 +15 -3
  160. package/src/serve/hub-app/home-strip.css +69 -19
  161. package/src/serve/hub-app/home-strip.tsx +192 -118
  162. package/src/serve/hub-app/home.html +1 -0
  163. package/src/serve/hub-app/hub-app.tsx +21 -5
  164. package/src/serve/hub-app/hub-types.ts +11 -5
  165. package/src/serve/hub-app/hub.css +88 -44
  166. package/src/serve/hub-app/icons.tsx +15 -0
  167. package/src/serve/hub-app/index.html +1 -0
  168. package/src/serve/hub-app/keys.ts +11 -8
  169. package/src/serve/hub-app/live-svc.ts +3 -2
  170. package/src/serve/hub-app/machine-st.ts +34 -49
  171. package/src/serve/hub-app/palette-st.ts +5 -5
  172. package/src/serve/hub-app/palette-view.tsx +1 -1
  173. package/src/serve/hub-app/plan-view.tsx +51 -0
  174. package/src/serve/hub-app/planned-st.ts +9 -4
  175. package/src/serve/hub-app/processes-view.tsx +38 -0
  176. package/src/serve/hub-app/project-list.tsx +3 -3
  177. package/src/serve/hub-app/projects-svc.ts +15 -10
  178. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  179. package/src/serve/hub-app/reach.ts +36 -0
  180. package/src/serve/hub-app/readme-svc.ts +3 -2
  181. package/src/serve/hub-app/repo-sections.ts +117 -0
  182. package/src/serve/hub-app/repo-st.ts +47 -10
  183. package/src/serve/hub-app/runs-tabs.ts +1 -1
  184. package/src/serve/hub-app/sidebar.tsx +12 -37
  185. package/src/serve/hub-app/timeline-svc.ts +3 -2
  186. package/src/serve/hub-app/timeline-view.tsx +14 -11
  187. package/src/serve/hub-app/tree-build.ts +149 -155
  188. package/src/serve/hub-app/tree-row.tsx +39 -9
  189. package/src/serve/hub-app/tree-svc.ts +70 -49
  190. package/src/serve/hub-app/when-view.tsx +89 -0
  191. package/src/serve/hub-app/when.css +17 -0
  192. package/src/serve/hub-runtime.ts +3 -16
  193. package/src/serve/project-gutter.ts +1 -2
  194. package/src/serve/run-serve.ts +1 -1
  195. package/src/serve/serve-hub.ts +4 -14
  196. package/src/serve/warm-cell.ts +49 -16
  197. package/src/suite/judge-suites.ts +42 -17
  198. package/src/suite/suite-cli.ts +5 -8
  199. package/src/wiki/lint-md.ts +2 -0
  200. package/src/wiki/scan-md.ts +37 -10
  201. package/src/serve/hub-app/processes-svc.ts +0 -63
  202. package/src/serve/hub-app/repo-sections.tsx +0 -61
  203. package/src/serve/repo-processes.ts +0 -89
@@ -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' })
@@ -14,11 +14,11 @@ export const asksAutoclose = check({
14
14
  title: 'asks auto close',
15
15
  description: 'no open ask whose own probe passes stays in the queue',
16
16
  owner: 'build/shipkit/built-in-checks.md',
17
- when: 'health',
18
- never: "runs each open ask's own probe, a shell command that reads the world",
19
- every: ['20h', '28h'],
17
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: false, onClock: { every: ['20h', '28h'], inactive: 'wait' } },
18
+ cache: { by: 'never', why: "runs each open ask's own probe, a shell command that reads the world" },
19
+ fix: 'none',
20
20
  // it only probes an ask that carries one, and archives one marked done
21
- planOff: (env) => {
21
+ applies: (env) => {
22
22
  const dir = join(env.root, env.config.asks?.dir ?? LAYOUT.asks)
23
23
  if (!existsSync(dir)) return 'no asks queue'
24
24
  return listAsks(dir).some((a) => a.check != null || a.status === 'done') ? null : 'no open ask carries a probe'
@@ -1,10 +1,10 @@
1
1
  // SK027 agent wiki links (build/shipkit/built-in-checks.md): a repo's agent/
2
2
  // IS an md wiki (entry index.md, the CLAUDE.md include), so an orphan page or
3
- // a dangling ref there fails. A cross wiki pointer (into a house wiki) is
4
- // backticked, never a bare → or @, which reads as an agent local link and
5
- // dangles. A repo without agent/index.md skips.
6
- import { existsSync } from 'fs'
7
- import { join } from 'path'
3
+ // a dangling ref there fails. A link that leaves agent/ but stays in the repo
4
+ // is checked on disk; a pointer out of the repo (a house wiki) is backticked,
5
+ // never a bare → or @. A repo without agent/index.md skips.
6
+ import { existsSync, readFileSync } from 'fs'
7
+ import { basename, join } from 'path'
8
8
  import { check } from '../check.ts'
9
9
  import { lintMd } from '../wiki/lint-md.ts'
10
10
 
@@ -14,15 +14,40 @@ export const agentWikiLinks = check({
14
14
  title: 'agent wiki links',
15
15
  description: 'every agent/ page is linked from somewhere, and every agent/ link resolves',
16
16
  owner: 'meta/agent-folder.md',
17
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
17
18
  // the md lint walks agent/ on disk rather than through `c`: agent/ is all it reads
18
- inputs: ['agent'],
19
+ cache: { by: 'files', inputs: ['agent'], key: null },
20
+ fix: 'none',
21
+ applies: 'always',
19
22
  run: (c) => {
20
23
  if (!existsSync(join(c.root, 'agent/index.md'))) return void c.lines.push({ kind: 'skip', text: 'no agent/ wiki in this repo' })
21
- const lint = lintMd({ root: join(c.root, 'agent'), index: 'index.md' })
22
- for (const f of lint.orphans) c.fail(`agent/${f}: orphan, unreachable from agent/index.md: link it from a page or delete it`)
23
- for (const d of lint.dangling) c.fail(`agent/${d.from}: dangling ref → ${d.token}, target not found (backtick a cross wiki ref; fix or delete the pointer)`)
24
+ const lint = lintMd({ root: join(c.root, 'agent'), index: 'index.md', boundary: c.root })
25
+ const linked = lint.pages.filter((p) => !lint.orphans.includes(p))
26
+ for (const f of lint.orphans) {
27
+ const by = namingPage(c.root, f, linked)
28
+ const where = by != null ? `${by}, which already names it (turn that mention into the link)` : 'agent/index.md or the page of its topic'
29
+ c.fail(`agent/${f}: orphan, unreachable from agent/index.md: add a markdown link to it from ${where}; delete it only if the page is dead`)
30
+ }
31
+ for (const d of lint.dangling) c.fail(`agent/${d.from}: dangling ref → ${d.token}, target not found on disk: fix the path or delete the pointer (a pointer out of this repo, into a house wiki or another repo, goes in backticks)`)
24
32
  if (!c.failed) c.ok('agent/ has no orphans and no dangling refs')
25
33
  },
26
34
  })
27
35
 
36
+ /** the linked page whose text already names an orphan (its path, its file
37
+ * name, or a glob its file name matches), the one to turn into a link */
38
+ function namingPage(root: string, orphan: string, pages: string[]): string | null {
39
+ const name = basename(orphan)
40
+ const stem = name.replace(/\.md$/, '')
41
+ for (const p of pages) {
42
+ const text = readFileSync(join(root, 'agent', p), 'utf8')
43
+ if (text.includes(orphan) || text.includes(name) || text.includes(`${stem}.`) || text.includes(`${stem} `)) return `agent/${p}`
44
+ for (const m of text.matchAll(/[\w.-]*\*[\w.*-]*/g)) {
45
+ const g = m[0]
46
+ if (g.replace(/[*.]/g, '').length < 3) continue
47
+ if (new Bun.Glob(g).match(name) || new Bun.Glob(`${g}.md`).match(name)) return `agent/${p}`
48
+ }
49
+ }
50
+ return null
51
+ }
52
+
28
53
  export default agentWikiLinks
@@ -52,21 +52,29 @@ const REQUIREMENTS: Requirement[] = [
52
52
  const why = found.map((f) => (f as { bad: string }).bad)
53
53
  return {
54
54
  warn: `${README}: screenshot: ${why.length === 0 ? 'no image at all' : `no image resolves (${why.join(', ')})`}`,
55
- fix: 'add a screenshot to README.md: ![what it looks like](docs/screenshot.png), a file in the repo, or an https url',
55
+ fix: `add a screenshot to README.md: ![what it looks like](docs/screenshot.png), a file in the repo, or an https url; ${OFF}`,
56
56
  }
57
57
  },
58
58
  },
59
59
  ]
60
60
 
61
+ /** the other way out, for a repo that cannot show one: the rule off, with the
62
+ * reason `defineRepo` requires (core/when.ts) */
63
+ const OFF = `or, when this repo cannot show one, turn the rule off with the reason: off: { SK028: '<why this repo shows no screenshot>' } in repo.config.ts, inside its shipkit gate block`
64
+
61
65
  export const readme = check({
62
66
  id: 'SK028',
63
67
  blocking: false,
64
68
  title: 'readme',
65
69
  description: 'README.md exists and shows at least one screenshot',
66
70
  owner: 'build/shipkit/built-in-checks.md',
71
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: 'moved', onClock: false },
72
+ cache: { by: 'reads' },
73
+ fix: 'none',
74
+ applies: 'always',
67
75
  run: (c) => {
68
76
  const md = c.read(README)
69
- if (md == null) return c.warn(`${README}: exists: no ${README} at the repo root`, 'write README.md: what the repo is, and a screenshot of it')
77
+ if (md == null) return c.warn(`${README}: exists: no ${README} at the repo root`, `write README.md: what the repo is, and a screenshot of it; ${OFF}`)
70
78
  c.ok(`${README} exists`)
71
79
  for (const r of REQUIREMENTS) {
72
80
  const f = r.test(c, md)
@@ -0,0 +1,95 @@
1
+ // SK029 explicit check files (build/shipkit/runs.md owns the declaration):
2
+ // every check file of the repo declares runs, cache, fix and applies, with
3
+ // no retired key. A file that does not does not load, and discovery shows
4
+ // it as one failing row. The repair ports it with the values the retired
5
+ // keys resolved to (core/port-check-file.ts), so the check keeps running
6
+ // where it ran, and the ci fix pass applies it before the gate reads the
7
+ // files: a repo carried across by its first `shipkit ci`. The same holds for
8
+ // the config: a retired `when` (only its `off:<why>` entries still load) and
9
+ // `ruleExempt` become `off` entries in the managed block.
10
+ import { join } from 'path'
11
+ import { check, type FixResult } from '../check.ts'
12
+ import { unportedCheckFiles } from '../core/port-check-files.ts'
13
+ import type { PortRun } from '../core/port-check-cli.ts'
14
+ import type { ConfigPlan, ConfigRewrite } from '../core/config-rewrite.ts'
15
+ import type { RepoConfig } from '../repo-config.ts'
16
+
17
+ /** the write half, run as its own process: it walks import closures on disk */
18
+ const PORT_CLI = join(import.meta.dir, '../core/port-check-cli.ts')
19
+ /** the config half, run as its own process: it loads repo.config.ts */
20
+ const CONFIG_REWRITE = join(import.meta.dir, '../core/config-rewrite.ts')
21
+
22
+ /** the retired config keys this repo still carries, and the `off` entries
23
+ * they become. defineRepo already refused every `when` word but `off:<why>` */
24
+ export function retiredOff(c: RepoConfig): { keys: string[]; off: Record<string, string> } {
25
+ const off: Record<string, string> = {}
26
+ for (const [id, raw] of Object.entries(c.when ?? {})) off[id] = raw.trim().replace(/^off:?/, '').trim()
27
+ for (const [id, why] of Object.entries(c.ruleExempt ?? {})) off[id] ??= why
28
+ const keys = [...(c.when != null ? ['when'] : []), ...(c.ruleExempt != null ? ['ruleExempt'] : [])]
29
+ return { keys, off }
30
+ }
31
+
32
+ export const explicitCheckFiles = check({
33
+ id: 'SK029',
34
+ title: 'explicit check files',
35
+ description: 'every check file declares runs, cache, fix and applies, and the config says only off',
36
+ owner: 'build/shipkit/runs.md',
37
+ blocking: true,
38
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
39
+ // the listing of check files and their sources, read through `c`
40
+ cache: { by: 'reads' },
41
+ // the port is mechanical: every value it writes is what the retired keys
42
+ // resolved to, so the fix pass applies it without asking
43
+ fix: 'ci',
44
+ applies: 'always',
45
+ run: (c) => {
46
+ const files = unportedCheckFiles(c)
47
+ const cfg = retiredOff(c.config)
48
+ if (files.length === 0 && cfg.keys.length === 0) return c.ok('every check file declares runs, cache, fix and applies, and the config says only off')
49
+ for (const f of files) c.fail(`${f.rel} still declares the retired shape (${f.keys.join(', ')}), so it does not load`)
50
+ if (cfg.keys.length > 0) c.fail(`repo.config.ts still carries ${cfg.keys.join(' and ')}: a repo only turns a check off, as off: { <id>: '<why>' }`)
51
+ c.fail(`${files.length + (cfg.keys.length > 0 ? 1 : 0)} file(s) to port`, 'shipkit check --fix SK029 ports each one with the values it ran on, and `shipkit ci` does it in its fix pass')
52
+ },
53
+ repair: async (c): Promise<FixResult> => {
54
+ const files = unportedCheckFiles(c)
55
+ const cfg = retiredOff(c.config)
56
+ if (files.length === 0 && cfg.keys.length === 0) return { changed: false, summary: 'every check file is explicit already, and the config says only off' }
57
+ const did: string[] = []
58
+ const hand: string[] = []
59
+ if (files.length > 0) {
60
+ const r = await c.exec([process.execPath, PORT_CLI, c.root, ...files.map((f) => f.rel)], { cwd: c.root, timeoutMs: 60_000 })
61
+ try {
62
+ const run = JSON.parse(r.out.trim().split('\n').at(-1) ?? '') as PortRun
63
+ if (run.did.length > 0) did.push(`ported ${run.did.join('; ')}`)
64
+ hand.push(...run.hand)
65
+ } catch {
66
+ hand.push(`the port did not answer (exit ${r.code}: ${r.out.trim().slice(0, 160)}): port ${files.map((f) => f.rel).join(', ')} by hand (build/shipkit/runs.md)`)
67
+ }
68
+ }
69
+ if (cfg.keys.length > 0) {
70
+ // `when` sits inside the managed block, so it goes first and the
71
+ // block describes the config again before the entries land
72
+ const plan: ConfigPlan = {
73
+ off: cfg.off,
74
+ drop: [
75
+ ...(c.config.when != null ? [{ key: 'when', why: 'its off entries are `off` now', phase: 'first' as const }] : []),
76
+ ...(c.config.ruleExempt != null ? [{ key: 'ruleExempt', why: 'its exemptions are `off` now', phase: 'afterOff' as const }] : []),
77
+ ],
78
+ name: c.config.name,
79
+ }
80
+ const r = await c.exec([process.execPath, CONFIG_REWRITE, c.root, JSON.stringify(plan)], { cwd: c.root, timeoutMs: 60_000 })
81
+ try {
82
+ const out = JSON.parse(r.out.trim().split('\n').at(-1) ?? '') as ConfigRewrite
83
+ did.push(...out.did)
84
+ hand.push(...out.hand)
85
+ if (out.restored != null) hand.push(`repo.config.ts was left as it was: ${out.restored}`)
86
+ } catch {
87
+ hand.push(`the repo.config.ts rewrite did not answer (exit ${r.code}): write off: ${JSON.stringify(cfg.off)} by hand and drop ${cfg.keys.join(', ')}`)
88
+ }
89
+ }
90
+ const summary = [did.length > 0 ? did.join('; ') : 'nothing ported', ...(hand.length > 0 ? [`${hand.length} by hand: ${hand.join(' | ')}`] : [])].join('. ')
91
+ return { changed: did.length > 0, summary }
92
+ },
93
+ })
94
+
95
+ export default explicitCheckFiles
@@ -15,7 +15,7 @@ export class AsksCommand extends Command {
15
15
  static override usage = Command.Usage({
16
16
  category: 'Asks',
17
17
  description: 'list open asks — the per-repo human action queue',
18
- examples: [['list open asks', 'shipkit asks']],
18
+ examples: [['list open asks', 'sk asks']],
19
19
  })
20
20
 
21
21
  async execute(): Promise<number> {
@@ -37,7 +37,7 @@ export class AsksCheckCommand extends Command {
37
37
  static override usage = Command.Usage({
38
38
  category: 'Asks',
39
39
  description: 'probe every open ask and archive the ones whose check now passes',
40
- examples: [['probe + auto-close', 'shipkit asks check']],
40
+ examples: [['probe + auto-close', 'sk asks check']],
41
41
  })
42
42
 
43
43
  async execute(): Promise<number> {
@@ -59,9 +59,9 @@ export class AskNewCommand extends Command {
59
59
  category: 'Asks',
60
60
  description: 'create a new ask file in the queue',
61
61
  examples: [
62
- ['minimal', 'shipkit ask "rotate the API token"'],
63
- ['with a reason', 'shipkit ask "buy the domain" --why "landing page is blocked on it"'],
64
- ['with metadata', 'shipkit ask "buy the domain" --kind action --blocks deploy --check "dig +short example.com"'],
62
+ ['minimal', 'sk ask "rotate the API token"'],
63
+ ['with a reason', 'sk ask "buy the domain" --why "landing page is blocked on it"'],
64
+ ['with metadata', 'sk ask "buy the domain" --kind action --blocks deploy --check "dig +short example.com"'],
65
65
  ],
66
66
  })
67
67
 
@@ -19,8 +19,8 @@ export class AssetsFillCommand extends Command {
19
19
  category: 'Assets',
20
20
  description: 'top rolls up to N per asset (canvas or comfy), sparsest-first',
21
21
  examples: [
22
- ['every asset up to 8 rolls', 'shipkit assets fill 8'],
23
- ['only two categories', 'shipkit assets fill 8 icons sprites'],
22
+ ['every asset up to 8 rolls', 'sk assets fill 8'],
23
+ ['only two categories', 'sk assets fill 8 icons sprites'],
24
24
  ],
25
25
  })
26
26
 
@@ -43,7 +43,7 @@ export class AssetsAddCommand extends Command {
43
43
  static override usage = Command.Usage({
44
44
  category: 'Assets',
45
45
  description: 'N more rolls per asset (canvas or comfy), regardless of what it has',
46
- examples: [['3 fresh rolls everywhere', 'shipkit assets add 3']],
46
+ examples: [['3 fresh rolls everywhere', 'sk assets add 3']],
47
47
  })
48
48
 
49
49
  n = Option.String({ required: true })
@@ -66,8 +66,8 @@ export class AssetsGenCommand extends Command {
66
66
  category: 'Assets',
67
67
  description: 'one-shot rolls for a single asset (canvas or comfy)',
68
68
  examples: [
69
- ['one roll', 'shipkit assets gen hero'],
70
- ['five rolls', 'shipkit assets gen hero --n 5'],
69
+ ['one roll', 'sk assets gen hero'],
70
+ ['five rolls', 'sk assets gen hero --n 5'],
71
71
  ],
72
72
  })
73
73
 
@@ -86,7 +86,7 @@ export class AssetsIdentityCommand extends Command {
86
86
  static override usage = Command.Usage({
87
87
  category: 'Assets',
88
88
  description: 'render favicon/logo/OG from .shipkit/identity/brand.ts into public/',
89
- examples: [['regenerate the identity outputs', 'shipkit assets identity']],
89
+ examples: [['regenerate the identity outputs', 'sk assets identity']],
90
90
  })
91
91
 
92
92
  async execute(): Promise<number> {
@@ -100,7 +100,7 @@ export class AssetsCheckCommand extends Command {
100
100
  static override usage = Command.Usage({
101
101
  category: 'Assets',
102
102
  description: 'kit loads, kinds valid, cflow workflows load, drafts present, identity outputs present',
103
- examples: [['health-check the asset pipeline', 'shipkit assets check']],
103
+ examples: [['health-check the asset pipeline', 'sk assets check']],
104
104
  })
105
105
 
106
106
  async execute(): Promise<number> {
@@ -9,18 +9,19 @@ export class BumpCommand extends Command {
9
9
 
10
10
  static override usage = Command.Usage({
11
11
  category: 'Gate',
12
- description: 'ship a version in one word: bumps a unit (default the repo root), run from a feature branch it merges into the release branch first, then promotes the CHANGELOG Unreleased section, bumps package.json, runs the gate and every prepublish script, commits, tags, pushes, and cuts the release. Any failure before the version commit rolls the merge back and returns you to your branch.',
12
+ description: 'ship a version: merge, changelog, bump, gate, tag, push, release',
13
+ details: 'ship a version in one word: bumps a unit (default the repo root), run from a feature branch it merges into the release branch first, then promotes the CHANGELOG Unreleased section, bumps package.json, runs the gate and every prepublish script, commits, tags, pushes, and cuts the release. Any failure before the version commit rolls the merge back and returns you to your branch.',
13
14
  examples: [
14
- ['a bugfix release of the repo itself', 'shipkit bump patch'],
15
- ['a feature release', 'shipkit bump minor'],
16
- ['a breaking release', 'shipkit bump major'],
17
- ['one sub-unit (ids: shipkit releases)', 'shipkit bump minor rvlib-shipkit'],
18
- ['a whole mixed batch in ONE gate, each unit at the kind its heading declares', 'shipkit bump --all'],
19
- ['every unit carrying Unreleased notes, all at the same kind', 'shipkit bump patch --all'],
20
- ['one unit, kind from its own heading', 'shipkit bump rvlib-shipkit'],
21
- ['rehearse the whole thing', 'shipkit bump minor --dry-run'],
22
- ['bump and commit, release later', 'shipkit bump patch --no-release'],
23
- ['from a feature branch, refuse rather than merge', 'shipkit bump patch --no-merge'],
15
+ ['a bugfix release of the repo itself', 'sk bump patch'],
16
+ ['a feature release', 'sk bump minor'],
17
+ ['a breaking release', 'sk bump major'],
18
+ ['one sub-unit (ids: shipkit releases)', 'sk bump minor rvlib-shipkit'],
19
+ ['a whole mixed batch in ONE gate, each unit at the kind its heading declares', 'sk bump --all'],
20
+ ['every unit carrying Unreleased notes, all at the same kind', 'sk bump patch --all'],
21
+ ['one unit, kind from its own heading', 'sk bump rvlib-shipkit'],
22
+ ['rehearse the whole thing', 'sk bump minor --dry-run'],
23
+ ['bump and commit, release later', 'sk bump patch --no-release'],
24
+ ['from a feature branch, refuse rather than merge', 'sk bump patch --no-merge'],
24
25
  ],
25
26
  })
26
27
 
@@ -14,14 +14,15 @@ export class CheckCommand extends Command {
14
14
 
15
15
  static override usage = Command.Usage({
16
16
  category: 'Health',
17
- description: 'every check except the build steps. `shipkit ci` runs these too and is what you normally want. --fix applies check autofixes',
17
+ description: 'every check except the build steps: the slower health surface',
18
+ details: 'every check except the build steps. `shipkit ci` runs these too and is what you normally want. --fix applies check autofixes',
18
19
  examples: [
19
- ['run (cached where valid)', 'shipkit check'],
20
- ['bust every cache layer', 'shipkit check --fresh'],
21
- ['apply every rule autofix, re-lint', 'shipkit check --fix'],
22
- ['fix one rule', 'shipkit check --fix SK004'],
23
- ['report-only: self-maintaining rules never write', 'shipkit check --frozen'],
24
- ['machine-readable', 'shipkit check --json'],
20
+ ['run (cached where valid)', 'sk check'],
21
+ ['bust every cache layer', 'sk check --fresh'],
22
+ ['apply every rule autofix, re-lint', 'sk check --fix'],
23
+ ['fix one rule', 'sk check --fix SK004'],
24
+ ['report-only: self-maintaining rules never write', 'sk check --frozen'],
25
+ ['machine-readable', 'sk check --json'],
25
26
  ],
26
27
  })
27
28
 
@@ -21,13 +21,14 @@ export class ChecksCommand extends Command {
21
21
  // widened so the hidden alias below can drop it: no usage, no help entry
22
22
  static override usage: Usage | undefined = Command.Usage({
23
23
  category: 'Health',
24
- description: 'every gate check on one table: where it comes from, whether it blocks or only warns, whether the gate runs it or only `shipkit check`, and how it caches. Reports, never gates',
24
+ description: 'every gate check on one table: source, blocking or warning, cache',
25
+ details: 'every gate check on one table: where it comes from, whether it blocks or only warns, whether the gate runs it or only `shipkit check`, and how it caches. Reports, never gates',
25
26
  examples: [
26
- ['the whole inventory', 'shipkit checks'],
27
- ['the big markdown table, written to a file', 'shipkit checks --out STATUS.full.md'],
28
- ['only the ones the gate never runs', 'shipkit checks --proactive'],
29
- ['everything that repairs itself, with its owner', 'shipkit checks --fixable'],
30
- ['machine-readable', 'shipkit checks --json'],
27
+ ['the whole inventory', 'sk checks'],
28
+ ['the big markdown table, written to a file', 'sk checks --out STATUS.full.md'],
29
+ ['only the ones the gate never runs', 'sk checks --proactive'],
30
+ ['everything that repairs itself, with its owner', 'sk checks --fixable'],
31
+ ['machine-readable', 'sk checks --json'],
31
32
  ],
32
33
  })
33
34
 
package/src/cli/cmd_Ci.ts CHANGED
@@ -13,18 +13,19 @@ export class CiCommand extends Command {
13
13
 
14
14
  static override usage = Command.Usage({
15
15
  category: 'Gate',
16
- description: 'THE command. Repairs what a machine can (formatter, imports, repo shape), then runs every check the gate asks for, through one DAG. A check that declares blocking:false warns instead of failing. Run this instead of anything else.',
16
+ description: 'THE gate: repairs what a machine can, then runs every check. Run this, not the parts',
17
+ details: 'THE command. Repairs what a machine can (formatter, imports, repo shape), then runs every check the gate asks for, through one DAG. A check that declares blocking:false warns instead of failing. Run this instead of anything else.',
17
18
  examples: [
18
- ['full gate, mechanical fixes applied', 'shipkit ci'],
19
- ['gate without the repair pass (checks can still write)', 'shipkit ci --no-fix'],
20
- ['leave repo config files alone', 'shipkit ci --no-fix-rules'],
21
- ['stop at the first failure', 'shipkit ci --bail'],
22
- ['one check + its dep closure', 'shipkit ci --only typecheck'],
23
- ['every check declaring one group', 'shipkit ci --only wiki'],
24
- ['run everything, take nothing from the cache', 'shipkit ci --fresh'],
25
- ['include an opt in check', 'shipkit ci --with up:itch'],
26
- ['in the background, read it back with shipkit status', 'shipkit ci --detach'],
27
- ['every line, the ones nobody acts on included', 'shipkit ci --verbose'],
19
+ ['full gate, mechanical fixes applied', 'sk ci'],
20
+ ['gate without the repair pass (checks can still write)', 'sk ci --no-fix'],
21
+ ['leave repo config files alone', 'sk ci --no-fix-rules'],
22
+ ['stop at the first failure', 'sk ci --bail'],
23
+ ['one check + its dep closure', 'sk ci --only typecheck'],
24
+ ['every check declaring one group', 'sk ci --only wiki'],
25
+ ['run everything, take nothing from the cache', 'sk ci --fresh'],
26
+ ['include an opt in check', 'sk ci --with up:itch'],
27
+ ['in the background, read it back with shipkit status', 'sk ci --detach'],
28
+ ['every line, the ones nobody acts on included', 'sk ci --verbose'],
28
29
  ],
29
30
  })
30
31
 
@@ -23,6 +23,10 @@ export type Definition = {
23
23
  options?: { definition?: string; description?: string | null }[] | null
24
24
  }
25
25
 
26
+ /** the longest description the index shows: one short line. The rest goes in
27
+ * the command's `details`, which --help prints */
28
+ export const DESCRIPTION_MAX = 100
29
+
26
30
  // clipanion renders a value placeholder as `#0`, which nobody can type
27
31
  const clean = (s: string | null | undefined): string => (s ?? '').trim().replace(/\s+/g, ' ').replace(/ #\d+\b/g, ' <value>')
28
32
 
@@ -34,9 +38,9 @@ const clean = (s: string | null | undefined): string => (s ?? '').trim().replace
34
38
  export function commandRows(plain: Definition[], detailed: Definition[] = []): CommandRow[] {
35
39
  const flagsByPath = new Map(detailed.map((d) => [d.path, d.options ?? []]))
36
40
  return plain
37
- .filter((d) => !/^shipkit (--help|-h|--version|-v)$/.test(d.path))
41
+ .filter((d) => !/^(shipkit|sk) (--help|-h|--version|-v)$/.test(d.path))
38
42
  .map((d) => ({
39
- path: d.path.replace(/^shipkit /, ''),
43
+ path: d.path.replace(/^(shipkit|sk) /, ''),
40
44
  category: clean(d.category) || 'Other',
41
45
  // verbatim from the CLI so the doc cannot say something it does not
42
46
  description: clean(d.description),
@@ -49,21 +53,19 @@ export function commandRows(plain: Definition[], detailed: Definition[] = []): C
49
53
  .sort((a, b) => a.category.localeCompare(b.category) || a.path.localeCompare(b.path))
50
54
  }
51
55
 
52
- /** the markdown block: per command, the real invocation with its arguments and
53
- * flags, then a worked example. A name and a sentence are not enough to act
54
- * on: an agent that cannot see the arguments either guesses them or runs
55
- * `--help` first, and both are the waste this page exists to remove. */
56
+ /** the markdown block, loaded into every agent's context at start: per
57
+ * command its invocation with every flag, one line of description, then its
58
+ * examples. What a flag does and the long text are `sk <command> --help`. */
56
59
  export function renderMarkdown(rows: CommandRow[]): string {
57
60
  const out: string[] = []
58
61
  let cat = ''
59
62
  for (const r of rows) {
60
63
  if (r.category !== cat) { cat = r.category; out.push(`\n### ${cat}\n`) }
61
- const sig = r.usage.replace(/^\$0/, 'shipkit').trim() || `shipkit ${r.path}`
64
+ const sig = r.usage.replace(/^\$0/, 'sk').trim() || `sk ${r.path}`
62
65
  const flags = r.options.map((o) => `[${o.definition}]`).join(' ')
63
66
  // one line per bullet: a description on its own indented line reads as
64
67
  // hard-wrapped prose to the house lint
65
68
  out.push(`- \`${sig}${flags === '' ? '' : ` ${flags}`}\` — ${r.description}`)
66
- for (const o of r.options) if (o.description !== '') out.push(` - \`${o.definition}\` ${o.description}`)
67
69
  for (const [label, cmd] of r.examples) out.push(` - ${label}: \`${cmd}\``)
68
70
  }
69
71
  return out.join('\n').trim()
@@ -82,12 +84,13 @@ export class CommandsCommand extends Command {
82
84
 
83
85
  static override usage = Command.Usage({
84
86
  category: 'Health',
85
- description: 'every command this binary registers, as markdown or json — the source a docs page is generated FROM, never hand-copied',
87
+ description: 'every command this binary registers, as markdown or json',
88
+ details: 'every command this binary registers, as markdown or json — the source a docs page is generated FROM, never hand-copied',
86
89
  examples: [
87
- ['markdown index', 'shipkit commands'],
88
- ['machine-readable', 'shipkit commands --json'],
89
- ['write into a docs page between the markers', 'shipkit commands --out wiki/build/shipkit-cli.md'],
90
- ['fail if that page is stale (lint mode)', 'shipkit commands --out wiki/build/shipkit-cli.md --check'],
90
+ ['markdown index', 'sk commands'],
91
+ ['machine-readable', 'sk commands --json'],
92
+ ['write into a docs page between the markers', 'sk commands --out wiki/build/shipkit-cli.md'],
93
+ ['fail if that page is stale (lint mode)', 'sk commands --out wiki/build/shipkit-cli.md --check'],
91
94
  ],
92
95
  })
93
96
 
@@ -19,13 +19,14 @@ export class ConfigKeysCommand extends Command {
19
19
 
20
20
  static override usage = Command.Usage({
21
21
  category: 'Health',
22
- description: 'every repo.config.ts key and every check field with its doc, read from the types — the source a config reference page is generated FROM',
22
+ description: 'every repo.config.ts key and check field with its doc, from the types',
23
+ details: 'every repo.config.ts key and every check field with its doc, read from the types — the source a config reference page is generated FROM',
23
24
  examples: [
24
- ['markdown reference', 'shipkit config-keys'],
25
- ['machine-readable', 'shipkit config-keys --json'],
26
- ['write into a docs page between the markers', 'shipkit config-keys --out wiki/build/shipkit/config-keys.md'],
27
- ['fail if that page is stale (lint mode)', 'shipkit config-keys --out wiki/build/shipkit/config-keys.md --check'],
28
- ['name the keys carrying no doc comment', 'shipkit config-keys --undocumented'],
25
+ ['markdown reference', 'sk config-keys'],
26
+ ['machine-readable', 'sk config-keys --json'],
27
+ ['write into a docs page between the markers', 'sk config-keys --out wiki/build/shipkit/config-keys.md'],
28
+ ['fail if that page is stale (lint mode)', 'sk config-keys --out wiki/build/shipkit/config-keys.md --check'],
29
+ ['name the keys carrying no doc comment', 'sk config-keys --undocumented'],
29
30
  ],
30
31
  })
31
32
 
@@ -10,9 +10,9 @@ export class ContractsCommand extends Command {
10
10
  category: 'Gate',
11
11
  description: 'release-contract fulfillness, fast and filesystem-only — deploy checks dep on it',
12
12
  examples: [
13
- ['all applicable contracts', 'shipkit contracts'],
14
- ['one venue', 'shipkit contracts --venue itch'],
15
- ['machine-readable', 'shipkit contracts --json'],
13
+ ['all applicable contracts', 'sk contracts'],
14
+ ['one venue', 'sk contracts --venue itch'],
15
+ ['machine-readable', 'sk contracts --json'],
16
16
  ],
17
17
  })
18
18
 
@@ -60,7 +60,10 @@ export function renderActivity(a: ActivityAnswer, cap: number): string[] {
60
60
  for (const r of [...busy, ...quiet]) {
61
61
  const branch = r.branch ?? (r.detached == null ? '' : `detached ${r.detached}`)
62
62
  out.push(`${r.name} ${dot(r)} ${branch} (${upstreamWords(r)})`)
63
- if (r.status != null && r.status.reds.length > 0) out.push(` failing: ${r.status.reds.map((id) => (r.status!.since[id] == null ? id : `${id} (${ageShort(r.status!.since[id]!, a.now)})`)).join(', ')}`)
63
+ for (const c of r.failing ?? []) {
64
+ out.push(` ${c.status === 'fail' ? '✗' : '!'} ${c.id}${c.at == null ? '' : ` (${ageShort(c.at, a.now)})`}`)
65
+ for (const l of c.lines) out.push(` ${l}`)
66
+ }
64
67
  if (r.error != null) out.push(` git failed: ${r.error}`)
65
68
  const dirt = r.error == null ? dirtWords(r) : ''
66
69
  if (dirt !== '') {
@@ -82,12 +85,13 @@ export class CrossRepoActivityCommand extends Command {
82
85
 
83
86
  static override usage = Command.Usage({
84
87
  category: 'Home',
85
- description: 'the activity page in the console: every house repo (archived ones left out), its checks, branch, upstream, uncommitted work and commits in a window. The same computation the page reads, local git only, no daemon needed',
88
+ description: 'the activity page as text: every house repo, its git state and recent commits',
89
+ details: 'the activity page in the console: every house repo (archived ones left out), its checks, branch, upstream, uncommitted work and commits in a window. The same computation the page reads, local git only, no daemon needed',
86
90
  examples: [
87
- ['the last 30 minutes, 3 commits per repo, as the page opens', 'shipkit cross-repo-activity'],
88
- ['the same, by the page name', 'shipkit activity'],
89
- ['the last 3 hours, up to 10 commits per repo', 'shipkit cross-repo-activity --max-age 3h --max-commit 10'],
90
- ['the route\'s exact payload', 'shipkit cross-repo-activity --max-age 30m --json'],
91
+ ['the last 30 minutes, 3 commits per repo, as the page opens', 'sk cross-repo-activity'],
92
+ ['the same, by the page name', 'sk activity'],
93
+ ['the last 3 hours, up to 10 commits per repo', 'sk cross-repo-activity --max-age 3h --max-commit 10'],
94
+ ['the route\'s exact payload', 'sk cross-repo-activity --max-age 30m --json'],
91
95
  ],
92
96
  })
93
97