rvlib-shipkit 0.38.1 → 1.0.1

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 (255) hide show
  1. package/CHANGELOG.md +379 -16
  2. package/README.md +26 -14
  3. package/package.json +2 -2
  4. package/src/assets/gen/comfyts-gen.ts +27 -10
  5. package/src/assets/gen/derived.ts +1 -1
  6. package/src/assets/gen/generate-roll.ts +1 -1
  7. package/src/assets/kit-config.ts +6 -6
  8. package/src/assets/run-assets.ts +14 -10
  9. package/src/assets/server/studio-server.ts +6 -5
  10. package/src/check.ts +343 -0
  11. package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
  12. package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
  13. package/src/checks/SK003-claude-md-shape.check.ts +66 -0
  14. package/src/checks/SK004-gitignore-policy.check.ts +309 -0
  15. package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +49 -32
  16. package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
  17. package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
  18. package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
  19. package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
  20. package/src/checks/SK010-undeclared-unit.check.ts +111 -0
  21. package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
  22. package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
  23. package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
  24. package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
  25. package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
  26. package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
  27. package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
  28. package/src/checks/SK018-test-files-run.check.ts +28 -0
  29. package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
  30. package/src/checks/SK020-self-spawning-check.check.ts +147 -0
  31. package/src/checks/contracts.check.ts +20 -0
  32. package/src/cli/cmd_Asks.ts +3 -2
  33. package/src/cli/cmd_Assets.ts +2 -2
  34. package/src/cli/cmd_Bump.ts +1 -1
  35. package/src/cli/cmd_Check.ts +6 -6
  36. package/src/cli/cmd_Checks.ts +95 -0
  37. package/src/cli/cmd_Ci.ts +48 -13
  38. package/src/cli/cmd_Commands.ts +2 -6
  39. package/src/cli/cmd_ConfigKeys.ts +69 -0
  40. package/src/cli/cmd_Contracts.ts +1 -1
  41. package/src/cli/cmd_Fleet.ts +16 -3
  42. package/src/cli/cmd_Hooks.ts +1 -1
  43. package/src/cli/cmd_Keys.ts +43 -18
  44. package/src/cli/cmd_Mv.ts +2 -2
  45. package/src/cli/cmd_Prose.ts +10 -1
  46. package/src/cli/cmd_Releases.ts +2 -2
  47. package/src/cli/cmd_Serve.ts +1 -1
  48. package/src/cli/cmd_Start.ts +6 -6
  49. package/src/cli/cmd_Status.ts +25 -0
  50. package/src/cli/cmd_Tui.ts +2 -2
  51. package/src/cli.ts +7 -4
  52. package/src/collections/actions.ts +4 -4
  53. package/src/collections/checks.ts +257 -0
  54. package/src/collections/collection-types.ts +254 -1
  55. package/src/collections/comfy.ts +23 -0
  56. package/src/collections/md-body.ts +14 -0
  57. package/src/collections/pings.ts +38 -12
  58. package/src/collections/posts.ts +19 -3
  59. package/src/collections/providers.ts +198 -91
  60. package/src/collections/reflections.ts +41 -0
  61. package/src/collections/state-rules.ts +73 -0
  62. package/src/collections/test-time.ts +288 -0
  63. package/src/collections/tests.ts +181 -0
  64. package/src/collections/wikis.ts +66 -0
  65. package/src/comfy/comfy-panel.ts +292 -0
  66. package/src/contracts/apply-contracts.ts +17 -12
  67. package/src/contracts/contract-types.ts +16 -4
  68. package/src/contracts/contracts-engine.ts +78 -42
  69. package/src/contracts/manifests/discord.contract.ts +6 -6
  70. package/src/contracts/manifests/identity.contract.ts +3 -3
  71. package/src/contracts/manifests/itch.contract.ts +12 -15
  72. package/src/contracts/manifests/social.contract.ts +5 -5
  73. package/src/contracts/manifests/steam.contract.ts +16 -16
  74. package/src/contracts/run-contracts.ts +4 -3
  75. package/src/core/attach.ts +96 -0
  76. package/src/core/canonical-scripts.ts +25 -0
  77. package/src/core/check-ctx.ts +204 -14
  78. package/src/core/check-file-names.ts +112 -0
  79. package/src/core/check-pass.ts +84 -0
  80. package/src/core/check-runner.ts +796 -0
  81. package/src/core/config-keys.ts +256 -0
  82. package/src/core/config-rewrite.ts +77 -0
  83. package/src/core/discover-checks.ts +166 -90
  84. package/src/core/discover-legacy-checks.ts +26 -0
  85. package/src/core/doc-block.ts +13 -0
  86. package/src/core/floor-resolve.ts +264 -0
  87. package/src/core/floor.ts +20 -137
  88. package/src/core/git-bin.ts +24 -0
  89. package/src/core/git-root.ts +108 -0
  90. package/src/core/instance.ts +195 -0
  91. package/src/core/jobs.ts +118 -0
  92. package/src/core/layout.ts +52 -0
  93. package/src/core/legacy-shapes.ts +207 -0
  94. package/src/core/lib-root.ts +5 -0
  95. package/src/core/locked-write.ts +110 -39
  96. package/src/core/machine-commit.ts +5 -4
  97. package/src/core/migrate-layout.ts +616 -0
  98. package/src/core/motherhome.ts +2 -8
  99. package/src/core/nesting.ts +4 -12
  100. package/src/core/one-way-in-policy.ts +73 -17
  101. package/src/core/outcome-words.ts +45 -0
  102. package/src/core/package-manager.ts +88 -0
  103. package/src/core/parallel.ts +21 -0
  104. package/src/core/process-tree.ts +44 -0
  105. package/src/core/raw-access.ts +93 -0
  106. package/src/core/read-set.ts +152 -0
  107. package/src/core/run-check.ts +155 -213
  108. package/src/core/run-ci.ts +152 -189
  109. package/src/core/run-fix.ts +99 -0
  110. package/src/core/run-fixers.ts +64 -33
  111. package/src/core/self-update.ts +11 -3
  112. package/src/core/slots.ts +83 -0
  113. package/src/core/spawn-timeout.ts +34 -3
  114. package/src/core/when-block.ts +401 -0
  115. package/src/core/when.ts +135 -0
  116. package/src/core/write-status.ts +5 -2
  117. package/src/fleet/scan-fleet.ts +16 -0
  118. package/src/hooks/leak-guard.ts +20 -9
  119. package/src/hooks/one-way-in-guard.ts +13 -5
  120. package/src/hooks/root-identity.ts +2 -1
  121. package/src/hooks/run-hooks.ts +38 -26
  122. package/src/init-repo.ts +39 -23
  123. package/src/ledger/check-stats.ts +155 -0
  124. package/src/ledger/check-table.ts +179 -0
  125. package/src/ledger/db.ts +255 -0
  126. package/src/ledger/gate-checks.ts +433 -0
  127. package/src/ledger/git-key.ts +116 -77
  128. package/src/ledger/git-scan.ts +96 -35
  129. package/src/ledger/ledger.ts +116 -42
  130. package/src/ledger/live-runs.ts +111 -0
  131. package/src/ledger/live-timeline.ts +97 -0
  132. package/src/ledger/repo-view.ts +73 -0
  133. package/src/ledger/run-log.ts +448 -0
  134. package/src/ledger/run-timeline.ts +173 -0
  135. package/src/ledger/store.ts +23 -12
  136. package/src/ledger/suite-log.ts +161 -0
  137. package/src/ledger/toolchain-identity.ts +7 -1
  138. package/src/ledger/world-cache.ts +111 -0
  139. package/src/mv/mv-md.ts +16 -6
  140. package/src/problems/extract.ts +182 -0
  141. package/src/problems/problem-board.ts +37 -0
  142. package/src/problems/problem-line.ts +77 -0
  143. package/src/prose/prose-view.ts +90 -0
  144. package/src/prose/run-prose.ts +60 -19
  145. package/src/prose/ste.ts +140 -24
  146. package/src/reflections/reflections-queue.ts +81 -0
  147. package/src/release/git-facts.ts +15 -9
  148. package/src/release/run-bump.ts +6 -5
  149. package/src/release/run-release.ts +25 -11
  150. package/src/release/run-releases.ts +2 -2
  151. package/src/release/unit-gate.ts +7 -54
  152. package/src/release/unit-resolve.ts +282 -0
  153. package/src/release/units.ts +33 -217
  154. package/src/repo-config.ts +157 -88
  155. package/src/rules/rule.ts +36 -96
  156. package/src/rules/untrack-generated.ts +5 -4
  157. package/src/scaffold/files/vscode-tasks.json +10 -31
  158. package/src/scaffold/jsonc-spans.ts +113 -0
  159. package/src/scaffold/project-scaffold.ts +16 -43
  160. package/src/scan.ts +98 -0
  161. package/src/serve/gate-trigger.ts +148 -0
  162. package/src/serve/health-schedule.ts +102 -0
  163. package/src/serve/hub-app/chart-view.ts +179 -0
  164. package/src/serve/hub-app/check-view.tsx +345 -0
  165. package/src/serve/hub-app/check-words.ts +164 -0
  166. package/src/serve/hub-app/checks-svc.ts +280 -0
  167. package/src/serve/hub-app/clock-svc.ts +33 -0
  168. package/src/serve/hub-app/col-card.tsx +23 -39
  169. package/src/serve/hub-app/cols-svc.ts +44 -20
  170. package/src/serve/hub-app/comfy-svc.ts +171 -0
  171. package/src/serve/hub-app/comfy-view.tsx +81 -0
  172. package/src/serve/hub-app/dur.ts +41 -0
  173. package/src/serve/hub-app/hash-route.ts +121 -0
  174. package/src/serve/hub-app/header-bar.tsx +117 -0
  175. package/src/serve/hub-app/health-svc.ts +23 -28
  176. package/src/serve/hub-app/help.tsx +29 -0
  177. package/src/serve/hub-app/hub-app.tsx +122 -119
  178. package/src/serve/hub-app/hub-st.ts +115 -42
  179. package/src/serve/hub-app/hub-types.ts +117 -2
  180. package/src/serve/hub-app/hub.css +534 -192
  181. package/src/serve/hub-app/index.html +2 -0
  182. package/src/serve/hub-app/live-svc.ts +174 -0
  183. package/src/serve/hub-app/md.ts +15 -0
  184. package/src/serve/hub-app/media-svc.ts +4 -1
  185. package/src/serve/hub-app/media-view.tsx +15 -0
  186. package/src/serve/hub-app/next-run.ts +68 -0
  187. package/src/serve/hub-app/project-gutter.css +65 -0
  188. package/src/serve/hub-app/project-gutter.tsx +128 -0
  189. package/src/serve/hub-app/projects-svc.ts +226 -0
  190. package/src/serve/hub-app/prose-hits-st.ts +124 -0
  191. package/src/serve/hub-app/prose-hits-view.tsx +176 -0
  192. package/src/serve/hub-app/prose-hits.css +21 -0
  193. package/src/serve/hub-app/run-strip.ts +47 -0
  194. package/src/serve/hub-app/runs-tabs.ts +24 -0
  195. package/src/serve/hub-app/test-view.tsx +107 -0
  196. package/src/serve/hub-app/tests-svc.ts +62 -0
  197. package/src/serve/hub-app/tests-view.tsx +238 -0
  198. package/src/serve/hub-app/time-ago.tsx +34 -0
  199. package/src/serve/hub-app/timeline-chart.tsx +274 -0
  200. package/src/serve/hub-app/timeline-geometry.ts +314 -0
  201. package/src/serve/hub-app/timeline-svc.ts +62 -0
  202. package/src/serve/hub-app/timeline-view.tsx +245 -0
  203. package/src/serve/hub-app/tree-build.ts +410 -0
  204. package/src/serve/hub-app/tree-row.tsx +42 -0
  205. package/src/serve/hub-app/tree-svc.ts +174 -0
  206. package/src/serve/hub-app/wikis-view.tsx +53 -0
  207. package/src/serve/other-repos.ts +79 -0
  208. package/src/serve/project-gutter-route.ts +350 -0
  209. package/src/serve/project-gutter.ts +449 -0
  210. package/src/serve/run-serve.ts +245 -16
  211. package/src/serve/same-origin.ts +14 -0
  212. package/src/serve/serve-hub.ts +213 -18
  213. package/src/shipkit.ts +19 -12
  214. package/src/smoke/run-smoke.ts +9 -4
  215. package/src/start-repo.ts +75 -36
  216. package/src/suite/judge-suites.ts +209 -15
  217. package/src/suite/kept-timings.ts +58 -0
  218. package/src/suite/runner-shapes.ts +88 -0
  219. package/src/suite/suite-cli.ts +147 -24
  220. package/src/suite/suite-cpu-preload.ts +92 -0
  221. package/src/suite/test-units.ts +243 -0
  222. package/src/ts/config.ts +4 -69
  223. package/src/ts/jsonc.ts +69 -0
  224. package/src/ts/lex.ts +143 -0
  225. package/src/ts/rename.ts +73 -10
  226. package/src/ts/source-graph.ts +28 -8
  227. package/src/ts/tsconfig-projects.ts +6 -97
  228. package/src/ts/tsconfig-resolve.ts +108 -0
  229. package/src/tui/build-steps-panel.ts +61 -0
  230. package/src/tui/checks-panel.ts +24 -16
  231. package/src/tui/run-tui.ts +2 -2
  232. package/src/tui/tui-panel.ts +5 -4
  233. package/src/wiki/graph-colors.ts +68 -0
  234. package/src/wiki/graph-mount.ts +7 -9
  235. package/src/wiki/graph-page.ts +42 -25
  236. package/src/wiki/graph-positions.ts +105 -0
  237. package/src/wiki/run-wiki.ts +1 -1
  238. package/src/wiki/scan-md.ts +1 -1
  239. package/src/wiki/serve-graph.ts +33 -3
  240. package/src/cli/cmd_Rules.ts +0 -51
  241. package/src/cli/cmd_Units.ts +0 -82
  242. package/src/collections/graphs.ts +0 -32
  243. package/src/contracts/contracts-once.ts +0 -43
  244. package/src/core/check-suite.ts +0 -302
  245. package/src/core/repair-contract-dirs.ts +0 -136
  246. package/src/core/task-runner.ts +0 -339
  247. package/src/ledger/gate-units.ts +0 -138
  248. package/src/ledger/unit-table.ts +0 -105
  249. package/src/rules/SK002-contracts-fulfillness.ts +0 -24
  250. package/src/rules/SK003-claude-md-shape.ts +0 -70
  251. package/src/rules/SK004-gitignore-policy.ts +0 -238
  252. package/src/rules/SK010-undeclared-release-unit.ts +0 -127
  253. package/src/rules/rule-registry.ts +0 -64
  254. package/src/rules/run-fix.ts +0 -72
  255. package/src/tui/tasks-panel.ts +0 -60
@@ -2,25 +2,49 @@
2
2
  // comes from convention + repo.config's `serve` block: the wiki graph is
3
3
  // always on (custom provider via serve.wikiGraph, else the generic md
4
4
  // scan), the base-context wiki when a CLAUDE.md include names one, agent/
5
- // graph when the dir exists, the studio when .rv-assets/
5
+ // graph when the dir exists, the studio when .shipkit/assets/
6
6
  // kit.ts loads. Port: --port > serve.port > a deterministic house-range
7
7
  // default (4800-4979, hashed from the repo name — stable across launches;
8
8
  // pin serve.port when two repos collide).
9
- import { existsSync, readFileSync } from 'fs'
9
+ import { existsSync, readFileSync, statSync } from 'fs'
10
10
  import { homedir } from 'os'
11
11
  import { basename, dirname, join, resolve, sep } from 'path'
12
12
  import { fileURLToPath } from 'url'
13
13
  import { loadKit, type ResolvedKit } from '../assets/kit-config.ts'
14
14
  import { studioRoutes, type StudioRoutes } from '../assets/server/studio-server.ts'
15
15
  import { projectNeeds } from '../collections/collection-types.ts'
16
- import { graphsCollection } from '../collections/graphs.ts'
16
+ import { comfyCollection } from '../collections/comfy.ts'
17
+ import { ComfyPanel, comfyBox, defaultComfyPanelPort, scanComfyWorkflows } from '../comfy/comfy-panel.ts'
18
+ import { wikisCollection } from '../collections/wikis.ts'
17
19
  import { pingTick } from '../collections/pings.ts'
18
20
  import { buildCollections } from '../collections/providers.ts'
19
21
  import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
22
+ import { CheckCtx } from '../core/check-ctx.ts'
23
+ import { proseView } from '../prose/prose-view.ts'
24
+ import { allTasks, gateInventory } from '../ledger/gate-checks.ts'
25
+ import { runCheck } from '../core/run-check.ts'
26
+ import { runCi, type CiOpts } from '../core/run-ci.ts'
20
27
  import type { GraphMount } from '../wiki/graph-mount.ts'
21
28
  import { scanMdGraph } from '../wiki/scan-md.ts'
29
+ import { lastHealthPassAt, readInvocations, readLive, readRuns, readRunsSince, runsPath, sweepLive } from '../ledger/run-log.ts'
30
+ import { LIVE_HISTORY_MS, shapeLive, type LiveView } from '../ledger/live-timeline.ts'
31
+ import { parallelCap } from '../core/parallel.ts'
32
+ import { reclaimPreviousImage } from '../core/instance.ts'
33
+ import { isLinkedWorktree } from '../core/git-root.ts'
34
+ import { OtherRepos } from './other-repos.ts'
35
+ import { recordStamp } from './project-gutter.ts'
36
+ import { shapeTimeline } from '../ledger/run-timeline.ts'
37
+ import { readSuiteRuns, readSuites } from '../ledger/suite-log.ts'
38
+ import { shapeTestCheck, shapeTestsRoot } from '../collections/test-time.ts'
39
+ import type { TestFacts } from '../collections/collection-types.ts'
40
+ import { declaredSlowFiles } from '../suite/test-units.ts'
41
+ import { BUILD_TIMEOUT_MS, CHECK_TIMEOUT_MS } from '../collections/checks.ts'
42
+ import { HealthSchedule } from './health-schedule.ts'
43
+ import { GateTrigger, watchTracked } from './gate-trigger.ts'
44
+ import { ProblemBoard } from '../problems/problem-board.ts'
22
45
  import { serveHub, type HubDeps } from './serve-hub.ts'
23
46
  import { defaultHubPort } from './hub-port.ts'
47
+ import { LAYOUT } from '../core/layout.ts'
24
48
  export { defaultHubPort }
25
49
 
26
50
  export type ServeIo = { print: (l: string) => void; printErr: (l: string) => void }
@@ -53,10 +77,42 @@ export function baseWikiRoot(root: string): string | null {
53
77
 
54
78
  // `~/dev/<repo>/wiki` reads as "<repo> wiki" — a dir literally named wiki says
55
79
  // nothing on its own, so the owning repo names the card
80
+ /** the tab of the repo's own wiki, beside the house wiki named for its repo */
81
+ const WIKI_LABEL = "this repo's wiki"
56
82
  const wikiLabel = (dir: string): string => (basename(dir) === 'wiki' ? `${basename(dirname(dir))} wiki` : basename(dir))
57
83
 
58
- export async function resolveHubDeps(root: string, c: RepoConfig, io: ServeIo, o?: { port?: number }): Promise<HubDeps> {
59
- // ONE list: the routes, the /hub.json tools and the graphs section all
84
+ /** the config file's bytes, cheaply: a re-read costs a new module instance,
85
+ * so it happens only when the file really moved */
86
+ function configStamp(root: string): string {
87
+ try {
88
+ const st = statSync(join(root, 'repo.config.ts'))
89
+ return `${st.mtimeMs}:${st.size}`
90
+ } catch {
91
+ return ''
92
+ }
93
+ }
94
+
95
+ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo, o?: { port?: number }): Promise<HubDeps> {
96
+ // THE SWITCH WRITES repo.config.ts, so the surfaces built on the config
97
+ // read the one on disk rather than the one this process booted with. A
98
+ // toggle that never reached the rows would read as a toggle that did
99
+ // nothing. What is resolved ONCE stays once: the kit, the graphs, the
100
+ // studio and the comfy panel are mounts, and remounting a server under a
101
+ // live page is a different question from re-reading a declaration.
102
+ let c = boot
103
+ let stamp = configStamp(root)
104
+ const refresh = async (): Promise<void> => {
105
+ const now = configStamp(root)
106
+ if (now === stamp || now === '') return
107
+ stamp = now
108
+ try {
109
+ c = await loadRepoConfig(root, { fresh: true })
110
+ } catch (e) {
111
+ io.print(`WARN repo.config.ts did not load, so the page is reading the one this process booted with — ${(e as Error).message}`)
112
+ }
113
+ }
114
+
115
+ // ONE list: the routes, the /hub.json tools and the wikis section all
60
116
  // read it, so nothing can advertise a graph the server does not serve.
61
117
  // The repo's OWN wiki mounts when it has one (a game repo's hub
62
118
  // advertising an empty wiki route was startup noise), a config-declared
@@ -65,54 +121,181 @@ export async function resolveHubDeps(root: string, c: RepoConfig, io: ServeIo, o
65
121
  // agent working here obeys that wiki, so a reader can open it.
66
122
  const graphs: GraphMount[] = []
67
123
  if (c.serve?.wikiGraph != null)
68
- graphs.push({ id: 'wiki', label: 'wiki graph', title: c.serve.wikiGraph.title ?? `${c.name} wiki`, load: c.serve.wikiGraph.load, lint: null, note: 'repo-owned lint' })
124
+ graphs.push({ id: 'wiki', label: WIKI_LABEL, title: c.serve.wikiGraph.title ?? `${c.name} wiki`, load: c.serve.wikiGraph.load, lint: c.serve.wikiGraph.lint != null ? { checks: c.serve.wikiGraph.lint } : { root } })
69
125
  else if (existsSync(join(root, 'wiki')))
70
- graphs.push({ id: 'wiki', label: 'wiki graph', title: `${c.name} wiki`, load: () => scanMdGraph({ root }), lint: { root } })
126
+ graphs.push({ id: 'wiki', label: WIKI_LABEL, title: `${c.name} wiki`, load: () => scanMdGraph({ root }), lint: { root } })
71
127
  const baseWiki = baseWikiRoot(root)
72
128
  if (baseWiki != null)
73
129
  graphs.push({ id: 'base', label: wikiLabel(baseWiki), title: wikiLabel(baseWiki), load: () => scanMdGraph({ root: baseWiki }), lint: null })
74
130
  const agentRoot = join(root, 'agent')
75
131
  if (existsSync(agentRoot))
76
- graphs.push({ id: 'agent', label: 'agent graph', title: `${c.name} agent/`, load: () => scanMdGraph({ root: agentRoot, index: c.serve?.agentIndex }), lint: { root: agentRoot, index: c.serve?.agentIndex } })
132
+ graphs.push({ id: 'agent', label: 'agent/', title: `${c.name} agent/`, load: () => scanMdGraph({ root: agentRoot, index: c.serve?.agentIndex }), lint: { root: agentRoot, index: c.serve?.agentIndex } })
77
133
  // ONE page for every repo: the studio ALWAYS
78
134
  // mounts — kit-less repos get an empty media section, never a second
79
135
  // page. A broken kit degrades to the same empty kit, loudly.
80
136
  let kit: ResolvedKit | null = null
81
- if (existsSync(join(root, '.rv-assets/kit.ts'))) {
137
+ if (existsSync(join(root, '.shipkit/assets/kit.ts'))) {
82
138
  try {
83
139
  kit = await loadKit(root)
84
140
  } catch (e) {
85
141
  io.print(`WARN kit not loaded (media section empty): ${(e as Error).message}`)
86
142
  }
87
143
  }
88
- const emptyKit: ResolvedKit = { kinds: [], workspace: '.rv-assets/out' }
144
+ const emptyKit: ResolvedKit = { kinds: [], workspace: '.shipkit/assets/out' }
89
145
  let studio: StudioRoutes | null = null
90
146
  try {
91
147
  studio = await studioRoutes({ kit: kit ?? emptyKit, root })
92
148
  } catch (e) {
93
149
  io.print(`WARN media API not mounted: ${(e as Error).message}`)
94
150
  }
95
- const cli = fileURLToPath(new URL('../cli.ts', import.meta.url))
151
+ // a repo with no workflow module gets NO comfy section and no panel
152
+ // process: an empty section header is a surface that teaches nothing
153
+ const hasComfy = scanComfyWorkflows(root).length > 0
154
+ const panel = hasComfy ? new ComfyPanel({ root, port: c.serve?.comfyPort ?? defaultComfyPanelPort(c.name) }) : null
96
155
  const allCollections = () => [
97
156
  ...buildCollections(root, c, kit, { bootVersion: BOOT_VERSION, libVersion }),
98
- graphsCollection(graphs),
157
+ ...(hasComfy ? [comfyCollection(root)] : []),
158
+ wikisCollection(graphs, root),
99
159
  ]
100
160
  const port = o?.port ?? c.serve?.port ?? defaultHubPort(c.name)
161
+ // every run this process has going, whoever started it: the daily timer
162
+ // never stacks a pass on top of one
163
+ let active = 0
164
+ // ONE run at a time, whoever asked: two gates over one tree would measure
165
+ // the same checks twice and race each other's repair pass
166
+ let chain: Promise<unknown> = Promise.resolve()
167
+ // the latest verdict of every check, printed as one problem frame after
168
+ // each run for the vscode problems pane (problems/problem-line.ts)
169
+ const board = new ProblemBoard()
170
+ const runChecks: HubDeps['runChecks'] = (tap, o) => {
171
+ const run = chain.then(async () => {
172
+ active++
173
+ try {
174
+ return await runIn({
175
+ line: tap.line,
176
+ check: (e) => {
177
+ // a frame per verdict that moved the board, so a tsc
178
+ // error reaches the pane when typecheck ends rather
179
+ // than when the whole gate does
180
+ if (board.take(e)) for (const l of board.frame()) io.print(l)
181
+ tap.check(e)
182
+ },
183
+ }, o)
184
+ } finally {
185
+ active--
186
+ // and one per run, so every run ends on a frame
187
+ for (const l of board.frame()) io.print(l)
188
+ }
189
+ })
190
+ chain = run.catch(() => {})
191
+ return run
192
+ }
193
+ const runIn: HubDeps['runChecks'] = async (tap, o) => {
194
+ const only = o?.only != null && o.only.length > 0 ? o.only : undefined
195
+ const by = o?.by ?? 'hub'
196
+ const buildIds = new Set((await allTasks(c, root)).tasks.map((t) => t.id))
197
+ const namedTasks = (only ?? []).filter((id) => buildIds.has(id))
198
+ // ONE runner, two questions: a build step is the gate's, so
199
+ // anything naming one (or asking for the gate itself) goes through
200
+ // runCi, and a set of everything else takes the health pass
201
+ if (o?.gate === true || namedTasks.length > 0)
202
+ return runCi(root, {
203
+ ...hubGateOpts({ only, auto: o?.auto === true }),
204
+ buildOnly: only != null && namedTasks.length === only.length,
205
+ print: tap.line,
206
+ config: c,
207
+ onCheck: tap.check,
208
+ invokedBy: by,
209
+ })
210
+ // a NARROWED check run must not stamp STATUS.md: that file carries
211
+ // the verdict of a whole run, and one check is not one
212
+ return runCheck(root, { only, status: only == null, print: tap.line, config: c, onCheck: tap.check, invokedBy: by })
213
+ }
214
+ // resolved once: it shells out to git, and the page asks on every poll
215
+ let record = ''
216
+ try { record = runsPath(root) } catch { record = '' }
217
+ // nothing runs in this process yet, so a live row holding its pid was
218
+ // left by an earlier image of it: `bun --watch` restarts in place
219
+ if (record !== '') sweepLive(record, Date.now(), { ownPid: process.pid })
220
+ // the live strip asks every second, from every open tab: the record is
221
+ // read only when its files moved, and one shaped answer serves every
222
+ // request inside LIVE_SHARE_MS
223
+ const others = new OtherRepos(root)
224
+ let local: { stamp: string; at: number; live: ReturnType<typeof readLive>; runs: ReturnType<typeof readRunsSince>; invocations: ReturnType<typeof readInvocations> } | null = null
225
+ let shaped: { at: number; view: LiveView } | null = null
226
+ const LIVE_SHARE_MS = 500
227
+ const live = (): LiveView => {
228
+ const now = Date.now()
229
+ if (shaped != null && now - shaped.at < LIVE_SHARE_MS) return shaped.view
230
+ const stamp = recordStamp(record)
231
+ // the window slides even when nothing is written, so the history is
232
+ // re-read at least every few seconds
233
+ if (local == null || local.stamp !== stamp || now - local.at > 5_000) {
234
+ const since = now - LIVE_HISTORY_MS
235
+ local = { stamp, at: now, live: readLive(record), runs: readRunsSince(record, since), invocations: readInvocations(record, since) }
236
+ }
237
+ const view = shapeLive({ now, cap: parallelCap(), live: local.live, runs: local.runs, invocations: local.invocations, others: others.read() })
238
+ shaped = { at: now, view }
239
+ return view
240
+ }
241
+ const schedule = new HealthSchedule({
242
+ now: Date.now,
243
+ lastPassAt: () => lastHealthPassAt(record),
244
+ busy: () => active > 0,
245
+ run: async () => {
246
+ const t0 = Date.now()
247
+ io.print(`health pass: the daily run starts`)
248
+ const code = await runChecks({ line: () => {}, check: () => {} }, { by: 'schedule' })
249
+ io.print(`health pass: the daily run ended, exit ${code}, ${Math.round((Date.now() - t0) / 1000)}s`)
250
+ },
251
+ })
101
252
  return {
102
253
  root,
103
254
  name: c.name,
104
255
  port,
105
256
  graphs,
106
257
  studio,
107
- checkCmd: [process.execPath, cli, 'check'],
258
+ refresh,
259
+ // the run happens IN THIS PROCESS, so every check result reaches the
260
+ // page as it finishes. A shelled run could only ever deliver one wall
261
+ // of text, which is what made seventy checks share one destination
262
+ runChecks,
263
+ schedule,
264
+ // every run in flight, whoever started it: they all write the same
265
+ // record, so a terminal gate shows here the same as a button's
266
+ live,
267
+ repoHub: (repo) => others.hubOf(repo),
268
+ timeline: async () => {
269
+ const ceilings: Record<string, number> = {}
270
+ const inventory = await gateInventory(root, c)
271
+ for (const row of inventory) ceilings[row.decl.id] = row.timeoutMs ?? (row.build ? BUILD_TIMEOUT_MS : CHECK_TIMEOUT_MS)
272
+ return shapeTimeline({
273
+ runs: readRuns(record),
274
+ invocations: readInvocations(record),
275
+ suites: readSuites(record),
276
+ ceilings,
277
+ inventory: inventory.map((row) => row.decl.id),
278
+ declaredSlow: declaredSlowFiles(c),
279
+ cap: parallelCap(),
280
+ })
281
+ },
282
+ tests: async (check) => {
283
+ // the SAME test files the tree lists, from the same provider
284
+ const col = allCollections().find((x) => x.id === 'tests')
285
+ const facts = col == null ? [] : (await col.list()).flatMap((i): TestFacts[] => (i.test == null ? [] : [i.test]))
286
+ const input = { facts, runs: readRuns(record), suiteRuns: readSuiteRuns(record) }
287
+ return check == null || check === '' ? shapeTestsRoot(input) : shapeTestCheck(check, input)
288
+ },
289
+ prose: async () => proseView(CheckCtx.open({ root, config: c }), c),
108
290
  needs: () => projectNeeds(allCollections()),
109
291
  collections: allCollections,
110
- asksDir: c.asks?.dir ?? 'asks',
292
+ asksDir: c.asks?.dir ?? LAYOUT.asks,
293
+ comfy: panel == null ? null : { ensure: () => panel.ensure(), restart: () => panel.restart(), box: () => comfyBox(root), stop: () => panel.stop() },
111
294
  // RED-only pings while serving; notify command is repo config data.
112
295
  // Delivery failure must be LOUD — a silently broken notify path means
113
- // he assumes no blockers exist
296
+ // a silent one reads as no blockers at all
114
297
  pingTick: (needs) =>
115
- pingTick(root, c.asks?.dir ?? 'asks', needs, {
298
+ pingTick(root, c.asks?.dir ?? LAYOUT.asks, needs, {
116
299
  link: c.serve?.publicHost != null ? `http://${c.serve.publicHost}:${port}/?f=needs` : undefined,
117
300
  run: (cmd) => {
118
301
  const p = Bun.spawn(cmd, { stdout: 'ignore', stderr: 'pipe' })
@@ -125,6 +308,17 @@ export async function resolveHubDeps(root: string, c: RepoConfig, io: ServeIo, o
125
308
  }
126
309
  }
127
310
 
311
+ /** how a gate the hub starts differs from `shipkit ci`. The gate repairs and
312
+ * stamps STATUS.md, a narrowed run does neither: one check is not the verdict
313
+ * of a gate. A run the hub starts ON ITS OWN never mutates the tree someone
314
+ * is editing: no repair pass, frozen (SK005 never updates a dependency), no
315
+ * status files, and the quick tier */
316
+ export function hubGateOpts(o: { only?: string[]; auto: boolean }): Pick<CiOpts, 'onlyIds' | 'fix' | 'status' | 'frozen' | 'tests'> {
317
+ const whole = o.only == null
318
+ if (o.auto) return { onlyIds: o.only, fix: false, status: false, frozen: true, tests: 'quick' }
319
+ return { onlyIds: o.only, fix: whole, status: whole }
320
+ }
321
+
128
322
  // the hub must KNOW when it is outdated — a stale morning
129
323
  // process serves broken-looking picks all day: version read at
130
324
  // boot vs on disk per attention load; mismatch = red line telling him to
@@ -138,6 +332,12 @@ const BOOT_VERSION = libVersion()
138
332
  // (src/collections/providers.ts) — never a parallel projection.
139
333
 
140
334
  export async function runServe(root: string, io: ServeIo, o?: { port?: number }): Promise<number> {
335
+ // `bun --watch` restarts the hub in the same pid: what its previous image
336
+ // held is gone with it, and the check children it spawned are still ours
337
+ // and still running, so they die here, before this image spawns anything
338
+ const reclaimed = reclaimPreviousImage()
339
+ if (reclaimed.killed.length > 0)
340
+ io.print(`ok: this hub restarted in place: killed ${reclaimed.killed.length} check process(es) its previous image left running (pid ${reclaimed.killed.join(', ')})`)
141
341
  const c = await loadRepoConfig(root)
142
342
  const deps = await resolveHubDeps(root, c, io, o)
143
343
  try {
@@ -148,8 +348,37 @@ export async function runServe(root: string, io: ServeIo, o?: { port?: number })
148
348
  io.print(`${c.name} hub → ${base}`)
149
349
  const routes = hub.tools.filter((t) => t.path !== '/').map((t) => `${t.label} ${t.path}`).join(' · ')
150
350
  if (routes !== '') io.print(` also: ${routes}`)
351
+ // a page that cannot refresh itself looks identical to a broken one,
352
+ // and the only tell is that every change needs a reload key
353
+ if (!hub.liveReload) io.print(`WARN the page will not refresh itself: NODE_ENV=production strips bun hot reload, unset it and restart the hub`)
151
354
  // the off-machine door (phone, other machines) — publicHost is config
152
355
  if (c.serve?.publicHost != null) io.print(` phone: http://${c.serve.publicHost}:${hub.server.port}/`)
356
+ // said once at boot, so a silent phone is never mistaken for no blockers
357
+ if (c.notifyCommand != null && isLinkedWorktree(root)) io.print(` pings: off, this is a linked worktree (only the main checkout's hub notifies)`)
358
+ // the daily health pass rides the serving process, and only it: a hub
359
+ // built for a test never starts a timer
360
+ deps.schedule?.start((e) => io.printErr(`WARN the daily health pass broke: ${e.message}`))
361
+ // the quick gate at start, then after every save burst, so the
362
+ // problems pane follows the code. It only asks the runner, which
363
+ // measures or replays like any run: the watch decides WHEN, never
364
+ // what is true
365
+ const trigger = new GateTrigger({
366
+ run: () => deps.runChecks({ line: () => {}, check: () => {} }, { gate: true, by: 'hub', auto: true }),
367
+ onError: (e) => io.printErr(`WARN the hub's own gate run broke: ${e.message}`),
368
+ })
369
+ const watcher = watchTracked(root, () => trigger.touched(), { onError: (e) => io.printErr(`WARN the file watch stopped, so the problems pane refreshes only on a hub run: ${e.message}`) })
370
+ if (watcher.error != null) io.printErr(`WARN no file watch (${watcher.error}), so the problems pane refreshes only on a hub run`)
371
+ trigger.request()
372
+ // a child process (the comfy panel) outlives a parent nobody stopped,
373
+ // and the next launch then adopts one holding the PREVIOUS run's
374
+ // modules. A hard kill still leaks one, which is why adoption exists
375
+ for (const sig of ['SIGINT', 'SIGTERM'] as const)
376
+ process.on(sig, () => {
377
+ trigger.stop()
378
+ watcher.close()
379
+ hub.stop()
380
+ process.exit(0)
381
+ })
153
382
  await new Promise(() => {}) // the hub owns the process — never resolve
154
383
  return 0
155
384
  } catch (e) {
@@ -0,0 +1,14 @@
1
+ // the one guard on a request that changes something, shared by the hub and
2
+ // the standalone graph server.
3
+ /** 127.0.0.1 keeps other machines out, not other sites: any page open in the
4
+ * browser can POST to localhost. A request that changes something passes
5
+ * only from the hub's own page, or from a client that is not a browser at
6
+ * all (no origin header: curl, an agent). */
7
+ export function sameOrigin(req: Request): boolean {
8
+ if (req.method === 'GET' || req.method === 'HEAD') return true
9
+ const site = req.headers.get('sec-fetch-site')
10
+ if (site != null && site !== 'same-origin' && site !== 'none') return false
11
+ const origin = req.headers.get('origin')
12
+ if (origin == null) return true
13
+ try { return new URL(origin).host === new URL(req.url).host } catch { return false }
14
+ }