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
@@ -1,21 +1,22 @@
1
1
  // the home's strip over every page it serves: one line that never wraps. On a
2
2
  // repo page the shown repo (its icon, its name in its own colour, the two
3
- // swatches that set those colours), what it runs now and its two runs, then
4
- // on the right its processes, its hub's trouble and its editor icons
5
- // (header-bar.tsx); on the overview and the plan, the page's name. At the
6
- // far right, only machine wide trouble, and only while it lasts (pm or the
7
- // daemon down, a child or a global process crashed). The places themselves
3
+ // swatches that set those colours), its check… button and a spinner while
4
+ // it runs, then on the right its processes and its editor icons
5
+ // (header-bar.tsx); on the overview and the plan, the page's name. The strip
6
+ // carries no trouble text: the shown hub's trouble and the machine's (the
7
+ // daemon, pm or the web not answering, a child or a global process crashed, a
8
+ // failed poll) are cards floating under it at the top right, only while they
9
+ // last (float-cards.tsx). The machine not answering is said there and nowhere
10
+ // else (reach.ts). The places themselves
8
11
  // are the place buttons at the top of the project list (the picker's ⌘J,
9
12
  // then ⌘K, ⌘L, ⌘') and the first three entries of the
10
13
  // picker. What administers one repo (reload its hub, keep it active, archive
11
14
  // it) is its admin page (admin-view.tsx). NotUp stands in for the hub page
12
15
  // while its hub cannot answer, with the fix. Every link of the home is a
13
- // page of the same app: a plain click moves it in place. The daemon or pm
14
- // down is said in MachineToasts, floating over the page column's bottom
15
- // right corner, taking no room.
16
+ // page of the same app: a plain click moves it in place.
16
17
  import { observer } from 'mobx-react-lite'
17
18
  import type { ReactNode } from 'react'
18
- import type { ProcessIcon, ResourceView } from '../../home/resources.ts'
19
+ import type { OpenHow, ProcessIcon, ResourceView } from '../../home/resources.ts'
19
20
  import type { WorkerView } from '../../home/hubs.ts'
20
21
  import { NOT_INSTALLED } from '../../home/home-words.ts'
21
22
  import { splitWidgetHead } from '../../home/widget-text.ts'
@@ -25,8 +26,10 @@ import { processIcon, stateWords, type ProcessRow } from '../../home/process-row
25
26
  import { plainClick, repoPage, type HomeSt } from './home-st.ts'
26
27
  import { stampWords } from './time-ago.tsx'
27
28
  import { Icon, ICON } from './icons.tsx'
28
- import { Tip } from './floating.tsx'
29
- import { notUp, RepoOpeners, RepoRun, RepoTools } from './header-bar.tsx'
29
+ import { Pop, Tip } from './floating.tsx'
30
+ import { renderMd } from './md.ts'
31
+ import { FloatCards, type Card } from './float-cards.tsx'
32
+ import { hubCards, notUp, RepoOpeners, RepoRun, RepoTools } from './header-bar.tsx'
30
33
 
31
34
  const STATUS_WORD: Record<WorkerView['status'], string> = { up: 'up', starting: 'starting', crashed: 'crashed', down: 'down', stopping: 'stopping' }
32
35
 
@@ -42,9 +45,9 @@ export const HomeLink = ({ app, to, className, title, children }: { app: HomeSt;
42
45
  }}>{children}</a>
43
46
  )
44
47
 
45
- const Log = ({ lines }: { lines: string[] }) => (lines.length === 0 ? null : <pre className="hlog">{lines.join('\n')}</pre>)
48
+ export const Log = ({ lines }: { lines: string[] }) => (lines.length === 0 ? null : <pre className="hlog">{lines.join('\n')}</pre>)
46
49
 
47
- /** one switch per global process; its log floats under it on hover */
50
+ /** a crashed global process's switch (start it again); its log floats under it on hover */
48
51
  export const ResourceChip = observer(({ m, r }: { m: MachineSt; r: ResourceView }) => {
49
52
  const on = r.status === 'up' || r.status === 'starting'
50
53
  const title = [r.description, r.where != null ? `on ${r.where}` : null, r.note].filter((x) => x != null).join(' · ')
@@ -249,18 +252,92 @@ const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
249
252
  )
250
253
  })
251
254
 
252
- /** what is wrong with the machine itself, one chip each. Nothing when all is
253
- * well: the strip states trouble, never health */
254
- const MachineAlerts = observer(({ m }: { m: MachineSt }) => (
255
- <>
256
- {/* pm silent, its last answer is kept for the table and says nothing here */}
257
- {(m.pmDown !== '' ? [] : m.pm?.children ?? []).filter((c) => c.status === 'crashed' || c.status === 'down').map((c) => (
258
- <span key={c.id} className="halert" title={c.note ?? c.tail.at(-1) ?? ''}><Dot s={c.status} />{c.id} {STATUS_WORD[c.status]}</span>
259
- ))}
260
- {(m.pmDown !== '' ? [] : m.pm?.resources ?? []).filter((r) => r.status === 'crashed').map((r) => <ResourceChip key={r.id} m={m} r={r} />)}
261
- {m.error !== '' ? <span className="herr" title={m.error}>{m.error}</span> : null}
262
- </>
263
- ))
255
+ /** the machine not answering, as ONE card: the web (pm behind it is then
256
+ * unknown, not down), the daemon, pm, or the daemon and pm together */
257
+ const downCard = (m: MachineSt): Card[] => {
258
+ const web = m.down !== '' && m.downWho === 'web'
259
+ const pm = m.pmDown !== '' && !web
260
+ if (m.down === '' && !pm) return []
261
+ const head = m.down === '' ? 'pm down' : pm ? 'daemon and pm down' : `${m.downWho} down`
262
+ return [{ key: 'down', tone: 'red', head, body: m.down === '' ? m.pmDown : pm ? <>{m.down}<br />{m.pmDown}</> : m.down }]
263
+ }
264
+
265
+ /** what is wrong with the machine itself, one card each: the machine not
266
+ * answering, a crashed child of pm, a crashed global process (its switch
267
+ * starts it again), a failed poll. While pm is silent its last answer says
268
+ * nothing here */
269
+ const machineCards = (m: MachineSt): Card[] => {
270
+ const pm = m.pmDown !== '' ? null : m.pm
271
+ return [
272
+ ...downCard(m),
273
+ ...(pm?.children ?? []).filter((c) => c.status === 'crashed' || c.status === 'down').map((c): Card => ({ key: `child:${c.id}`, tone: 'red', head: `${c.id} ${STATUS_WORD[c.status]}`, body: c.note ?? c.tail.at(-1) ?? '' })),
274
+ ...(pm?.resources ?? []).filter((r) => r.status === 'crashed').map((r): Card => ({ key: `res:${r.id}`, tone: 'red', head: 'crashed', body: <ResourceChip m={m} r={r} /> })),
275
+ ...(m.error !== '' ? [{ key: 'poll', tone: 'red' as const, body: m.error }] : []),
276
+ ]
277
+ }
278
+
279
+ /** a global process's menu: start or stop, restart, its url opened three
280
+ * ways (each starts it first when it is down), then its declared actions and
281
+ * take over. Every entry is a click on purpose, never the chip itself */
282
+ /** `lastLog: false` where the whole log is drawn under it, so its last line is not said twice */
283
+ export const ProcessMenu = observer(({ m, r, close, lastLog = true }: { m: MachineSt; r: ProcessRow; close: () => void; lastLog?: boolean }) => {
284
+ const on = r.status === 'up' || r.status === 'starting'
285
+ const first = r.status === 'up' ? '' : ', starts it first'
286
+ const act = (a: Parameters<MachineSt['processAct']>[1], action?: string) => () => { close(); void m.processAct(r.id, a, action) }
287
+ const open = (how: OpenHow) => () => { close(); void m.openProcess(r.id, how) }
288
+ const log = lastLog ? r.lastLog : null
289
+ const about = r.description != null && r.description !== '' || r.note != null || log != null
290
+ return (
291
+ <div className="hmenu2">
292
+ <div className="hmenu hmacts">
293
+ <div className="hmhead"><i className={`pdot ${r.status}`} /><b>{r.label}</b><span>{r.status}</span></div>
294
+ {on ? (
295
+ <button className="hmitem" disabled={r.status === 'stopping' || !r.canStop} onClick={act('stop')}>
296
+ <Icon d={ICON.stop} /><span><b>stop</b>{r.canStop ? null : <small>started outside pm, so pm cannot stop it</small>}</span>
297
+ </button>
298
+ ) : (
299
+ <button className="hmitem" disabled={r.status === 'unknown'} onClick={act('start')}><Icon d={ICON.play} /><span><b>start</b></span></button>
300
+ )}
301
+ {r.canRestart ? <button className="hmitem" onClick={act('restart')}><Icon d={ICON.restart} /><span><b>restart</b></span></button> : null}
302
+ {r.url == null ? null : (
303
+ <>
304
+ <button className="hmitem" onClick={open('browser')}><Icon d={ICON.browser} /><span><b>open in a browser tab</b><small>{r.host}{first}</small></span></button>
305
+ <button className="hmitem" onClick={open('chrome')}><Icon d={ICON.browser} /><span><b>open in Chrome</b><small>{r.host}{first}</small></span></button>
306
+ <button className="hmitem" disabled={r.appBusy} onClick={open('app')}><Icon d={ICON.window} /><span><b>open as an app</b><small>{r.appBusy ? `building the ${r.label} app` : `${r.label} in the dock${first}`}</small></span></button>
307
+ </>
308
+ )}
309
+ {r.actions.map((a) => <button key={a.id} className="hmitem" disabled={r.running != null} onClick={act('action', a.id)}><Icon d={ICON.process} /><span><b>{a.label}</b></span></button>)}
310
+ {r.takeOver?.ok === true ? <button className="hmitem" onClick={act('takeover')}><Icon d={ICON.restart} /><span><b>take over</b><small>stop the copy started elsewhere and start one pm supervises</small></span></button> : null}
311
+ </div>
312
+ {about ? (
313
+ <div className="hmabout">
314
+ {r.description != null && r.description !== '' ? <div className="md" dangerouslySetInnerHTML={{ __html: renderMd(r.description) }} /> : null}
315
+ {r.note != null ? <small className="hmnote">{r.note}</small> : null}
316
+ {log != null ? <code className="hmlog" title="its last log line">{log}</code> : null}
317
+ </div>
318
+ ) : null}
319
+ </div>
320
+ )
321
+ })
322
+
323
+ /** the shown repo's global processes, one chip each, right after its name: its
324
+ * dot and label, and a click opens its menu (ProcessMenu). pm runs them, so
325
+ * they show whether the repo's hub answers or not */
326
+ const RepoProcesses = observer(({ m, name }: { m: MachineSt; name: string }) => {
327
+ const rows = m.processGroups.find((g) => g.core !== true && g.repo === name)?.rows ?? []
328
+ if (rows.length === 0) return null
329
+ return (
330
+ <span className="hprocs hgprocs">
331
+ {rows.map((r) => (
332
+ <span key={r.id} className="hproc">
333
+ <Pop className={`ghost ${r.status}`} popClass="fpop fpopw" aria-label={`${r.label}, ${r.status}: open its menu`} label={<><i className={`pdot ${r.status}`} />{r.label}</>}>
334
+ {(close) => <ProcessMenu m={m} r={r} close={close} />}
335
+ </Pop>
336
+ </span>
337
+ ))}
338
+ </span>
339
+ )
340
+ })
264
341
 
265
342
  export const HomeStrip = observer(({ app }: { app: HomeSt }) => {
266
343
  const m = app.machine
@@ -273,33 +350,17 @@ export const HomeStrip = observer(({ app }: { app: HomeSt }) => {
273
350
  <>
274
351
  <div className="hstrip">
275
352
  {name == null ? <span className="hplace">{app.page === 'overview' ? 'overview' : app.page}</span> : <ShownRepo app={app} name={name} />}
276
- {live == null ? null : <RepoRun app={live} />}
353
+ {name == null ? null : <RepoProcesses m={m} name={name} />}
277
354
  <span className="sp" />
278
355
  {live == null ? null : <RepoTools app={live} />}
279
- <MachineAlerts m={m} />
356
+ {live == null ? null : <RepoRun app={live} />}
280
357
  {live == null ? null : <RepoOpeners app={live} />}
281
358
  </div>
282
- <MachineToasts app={app} />
359
+ <FloatCards cards={[...(live == null ? [] : hubCards(live)), ...machineCards(m)]} />
283
360
  </>
284
361
  )
285
362
  })
286
363
 
287
- /** the daemon or pm not answering, as toasts floating over the bottom right
288
- * corner of the page column, just above the dock. They take no room, so
289
- * one coming or going moves nothing, and nothing is drawn while all is
290
- * well. Only a failed probe sets down or pmDown: before the first answer
291
- * there is none */
292
- export const MachineToasts = observer(({ app }: { app: HomeSt }) => {
293
- const m = app.machine
294
- if (m == null || (m.down === '' && m.pmDown === '')) return null
295
- return (
296
- <div className="htoasts">
297
- {m.down !== '' ? <div className="htoast" role="alert" title={m.down}><b>daemon down</b> {m.down}</div> : null}
298
- {m.pmDown !== '' ? <div className="htoast" role="alert" title={m.pmDown}><b>pm down</b> {m.pmDown}</div> : null}
299
- </div>
300
- )
301
- })
302
-
303
364
  /** archive the repo or bring it back: `archived` in its repo.config.ts, and
304
365
  * only that key. Archived, it stays listed and nothing runs there */
305
366
  export const ArchiveButton = observer(({ app, name }: { app: HomeSt; name: string }) => {
@@ -326,7 +387,7 @@ export const NotUp = observer(({ app }: { app: HomeSt }) => {
326
387
  <ArchiveButton app={app} name={name} />
327
388
  </div>
328
389
  )
329
- if (w == null) return <div className="hwait"><p className="twait"><span className="spin" />{m.down !== '' ? `waiting for the daemon to load ${name}…` : `looking for ${name}…`}</p></div>
390
+ if (w == null) return <div className="hwait"><p className="twait"><span className="spin" />{`looking for ${name}…`}</p></div>
330
391
  return (
331
392
  <div className="hwait">
332
393
  {w.blocked != null ? <p>{w.blocked}</p> : <p><Dot s={w.status} />{name}'s hub is {STATUS_WORD[w.status]}{w.note != null ? `: ${w.note}` : ''}</p>}
@@ -366,9 +427,12 @@ export const Overview = observer(({ app }: { app: HomeSt }) => {
366
427
  const m = app.machine
367
428
  if (m == null) return null
368
429
  // the table waits for pm's first word and for the list's order, so its
369
- // rows never arrive or reorder after it paints
430
+ // rows never arrive or reorder after it paints. The daemon silent, it
431
+ // draws whatever it last had: the down card says the rest
370
432
  const p = app.projects
371
- if (m.data == null || (m.pm == null && m.pmDown === '') || (p.list.length === 0 && !p.fresh && p.error === '')) return <div className="hwait"><p className="twait"><span className="spin" />{m.down !== '' ? 'the daemon does not answer yet' : 'loading…'}</p></div>
433
+ const nothing = m.data == null && m.pm == null && m.widgets == null
434
+ const first = m.data == null || (m.pm == null && m.pmDown === '') || (p.list.length === 0 && !p.fresh && p.error === '')
435
+ if (nothing || (m.down === '' && first)) return <div className="hwait"><p className="twait"><span className="spin" />loading…</p></div>
372
436
  return (
373
437
  <div className="hall">
374
438
  <div className="hstack">
@@ -9,6 +9,7 @@
9
9
  <link rel="stylesheet" href="./prose-hits.css" />
10
10
  <link rel="stylesheet" href="./home-strip.css" />
11
11
  <link rel="stylesheet" href="./planned.css" />
12
+ <link rel="stylesheet" href="./when.css" />
12
13
  <link rel="stylesheet" href="./activity.css" />
13
14
  <link rel="stylesheet" href="./readme.css" />
14
15
  <link rel="stylesheet" href="./dock.css" />
@@ -2,9 +2,10 @@
2
2
  // app for every page the home serves ('/', '/planned', '/activity', '/r/<repo>/') and for a
3
3
  // hub serving on its own at '/'. mobx+react per build/app-state-tree: HomeSt
4
4
  // is the single root (window.app), each repo a RepoSt under it, services own
5
- // state, components read and call. A repo's page is its sections over ONE
6
- // tree of every repo resource (sidebar.tsx), by path or by type, coloured by
7
- // state in the label itself, over one severity filter persisted at
5
+ // state, components read and call. A repo's page is ONE tree whose top rows
6
+ // are its sections and whose rows under them are every repo resource
7
+ // (sidebar.tsx), coloured by state in the label itself, under one filter
8
+ // input and one severity filter persisted at
8
9
  // ?f=needs · ?f=red. A row opens its item view, an asset row opens the atelier.
9
10
  import { runInAction } from 'mobx'
10
11
  import { observer } from 'mobx-react-lite'
@@ -24,6 +25,10 @@ import { TestCard } from './test-view.tsx'
24
25
  import { TestsPage } from './tests-view.tsx'
25
26
  import { AssetStage, CatGrid, InfoChips, MediaTopbar, ToolsBar, VariantsBar } from './media-view.tsx'
26
27
  import { Sidebar } from './sidebar.tsx'
28
+ import { ProcessesPage } from './processes-view.tsx'
29
+ import { PlanPage } from './plan-view.tsx'
30
+ import { WhenPage } from './when-view.tsx'
31
+ import { GitPage } from './git-view.tsx'
27
32
  import { AdminPage } from './admin-view.tsx'
28
33
  import { ReadmeView } from './readme-view.tsx'
29
34
  import { ProjectList } from './project-list.tsx'
@@ -35,7 +40,7 @@ import { ActivityView } from './activity-view.tsx'
35
40
  import { Palette } from './palette-view.tsx'
36
41
  import { SCREEN_WAIT_MS } from './screen-cache.ts'
37
42
  import { isSearchKey, pageKey, sectionKey } from './keys.ts'
38
- import { repoSections } from './repo-sections.tsx'
43
+ import { repoSections } from './repo-sections.ts'
39
44
  import { Dock } from './dock-view.tsx'
40
45
  import { DOCK } from './dock-st.ts'
41
46
  import { animateDock } from './dock-anim.ts'
@@ -93,6 +98,11 @@ const Main = observer(({ app }: { app: RepoSt }) => {
93
98
  )
94
99
  if (app.sel?.t === 'tests') return <main>{errs}<div className="stagewrap"><TestsPage app={app} check={app.sel.check} /></div></main>
95
100
  if (app.sel?.t === 'admin') return <main>{errs}<div className="stagewrap"><AdminPage app={app} /></div></main>
101
+ if (app.sel?.t === 'plan') return <main>{errs}<div className="stagewrap"><PlanPage app={app} /></div></main>
102
+ if (app.sel?.t === 'git') return <main>{errs}<div className="stagewrap"><GitPage app={app} /></div></main>
103
+ if (app.sel?.t === 'checks') return <main>{errs}<CheckTable app={app} id="checks" from={app.sel.from} /></main>
104
+ if (app.sel?.t === 'when') return <main>{errs}<div className="stagewrap"><WhenPage app={app} /></div></main>
105
+ if (app.sel?.t === 'processes') return <main>{errs}<div className="stagewrap"><ProcessesPage app={app} id={app.sel.id} /></div></main>
96
106
  if (app.sel?.t === 'status') return <main>{errs}<div className="stagewrap"><StatusCard app={app} /></div></main>
97
107
  if (app.sel?.t === 'cat') return <main>{errs}<CatGrid app={app} /></main>
98
108
  if (app.sel?.t === 'asset') return (
@@ -73,9 +73,10 @@ export type RecentRun = { at: number; ms: number; status: CheckStatus; replayed:
73
73
  // engine reaches the bundle: two declarations of one vocabulary is how a
74
74
  // switch ends up writing a word the other half does not know
75
75
  import type { WhenAt, WhenSource } from '../../core/when.ts'
76
+ import type { RunsCells } from '../../core/runs-matrix.ts'
76
77
  import type { Line } from '../../core/wire.ts'
77
78
  import type { CheckFrom } from '../../check.ts'
78
- export type { CheckFrom, WhenAt, WhenSource }
79
+ export type { CheckFrom, WhenAt, WhenSource, RunsCells }
79
80
 
80
81
  /** a gate check's declaration and its numbers (collections/collection-types.ts
81
82
  * CheckFacts is the source; this is the wire mirror the page reads) */
@@ -87,6 +88,8 @@ export type CheckFacts = {
87
88
  /** the wire lines of its last run, rendered as inline markdown */
88
89
  lastLines: string[]
89
90
  gate: boolean
91
+ /** every context it runs in and how it caches */
92
+ runs: RunsCells
90
93
  /** a build step: the gate runs it, the health pass does not */
91
94
  build: boolean
92
95
  blocking: boolean | null
@@ -138,7 +141,7 @@ export type GateBlock = {
138
141
  path: string
139
142
  hash: string
140
143
  state: 'missing-file' | 'missing-block' | 'stale' | 'current'
141
- when: Record<string, string>
144
+ off: Record<string, string>
142
145
  tests: { budgetMs?: number; slow?: SlowSuite[]; serial?: { file: string; why: string }[] }
143
146
  declaredOutside: boolean
144
147
  dirty: boolean
@@ -38,7 +38,6 @@ a,a:visited{color:var(--accent)}
38
38
  .pjpanel,.appcol,.pside,.pmain{height:100%;min-width:0}
39
39
  .apptop,.pdock{height:100%;min-height:0}
40
40
  .pjpanel{display:flex}
41
- /* the box the machine toasts float in (home-strip.css .htoasts) */
42
41
  .appcol{position:relative;display:flex;flex-direction:column}
43
42
  /* one region per repo visited, the shown one alone displayed */
44
43
  .repo{flex:1;min-height:0;display:flex;flex-direction:column}
@@ -50,10 +49,9 @@ a,a:visited{color:var(--accent)}
50
49
  .pmain > main{flex:1}
51
50
 
52
51
  /* ── a repo's bar: on the home the strip (home-strip.css .hstrip), on a hub
53
- of its own the header (.hdr). Its name, what runs now and the two runs,
54
- then its processes, its trouble and its editor icons. One line at any
55
- width: the container queries shrink the labels to their icons, then the
56
- run state to its spinner ── */
52
+ of its own the header (.hdr). Its name, its check… button and a spinner
53
+ while a run goes, then its processes and its editor icons, on one line.
54
+ Its trouble floats under it (home-strip.css .htoasts) ── */
57
55
  /* the repo's processes (serve.processes): a dot, its id, a link once up */
58
56
  .hprocs{display:inline-flex;gap:var(--s1);align-items:center}
59
57
  .hproc{display:inline-flex;align-items:center;gap:var(--s0)}
@@ -64,7 +62,7 @@ a,a:visited{color:var(--accent)}
64
62
  .hdr{display:flex;gap:var(--s2);align-items:center;height:36px;flex:none;padding:0 var(--s3);border-bottom:1px solid var(--line);background:var(--bg-top)}
65
63
  .hdr .sp{flex:1}
66
64
  .hname{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);font-weight:600;margin-right:var(--s2);white-space:nowrap}
67
- .hdr,.hstrip{container-type:inline-size;white-space:nowrap}
65
+ .hdr,.hstrip{white-space:nowrap}
68
66
  .hrun{display:inline-flex;align-items:center;gap:var(--s1);flex:none;margin-left:var(--s2)}
69
67
  .hbtn{display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s2);font-size:11.5px;white-space:nowrap;flex:none}
70
68
  .hbtn .hico{width:13px;height:13px}
@@ -72,40 +70,34 @@ a,a:visited{color:var(--accent)}
72
70
  .hicon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:24px;height:22px;padding:0;border:0;background:none;color:var(--dim)}
73
71
  .hicon:hover{color:var(--accent)}
74
72
  .hicon .hico{width:15px;height:15px}
75
- /* the run state holds its width whatever it says, so a run never moves the buttons beside it */
76
- .hstatus{max-width:420px;flex:0 1 auto;font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
77
- .hstatus.red{color:var(--red)}
78
- .hstatus.warn{color:var(--warn)}
79
- /* its words sit against the run buttons, cut with an ellipsis */
80
- .hstate{display:inline-flex;gap:var(--s1);align-items:center;width:200px;min-width:200px;flex:none;justify-content:flex-end;font-size:11.5px;color:var(--dim);overflow:hidden}
81
- .hstate.on{color:var(--ink)}
82
- .hstate .htext{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
83
- .hstate .spin.idle{visibility:hidden;animation:none}
84
- /* after .hstate, so a narrow bar wins over its width */
85
- @container (max-width:600px){.hbtn .lbl{display:none}.hbtn{padding:0 var(--s1)}}
86
- @container (max-width:520px){.hstate{width:120px;min-width:120px}}
87
- @container (max-width:420px){.hstate .htext{display:none}.hstate{width:16px;min-width:16px}}
88
73
 
89
74
  /* a row of a (?) legend: one glyph or word, then its meaning */
90
75
  .lrow{display:flex;gap:var(--s2);align-items:center;height:29px;flex:none;padding:0 var(--s3)}
91
76
 
92
- /* ── the left pane: the sections, the filter, then the tree, the one part
93
- that scrolls ── */
77
+ /* ── the left pane: the filter, then the tree, the one part that
78
+ scrolls ── */
94
79
  aside{border-right:1px solid var(--line);background:var(--panel);min-height:0;display:flex;flex-direction:column;overflow:hidden}
95
80
  aside > *{flex:none}
96
- aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
97
- /* a repo's sections: the project list's lines (project-list.css .pjpl), a
98
- little shorter, the current one lit and marked in the accent */
99
- .rsecs{display:flex;flex-direction:column;padding:var(--s1) 0;border-bottom:1px solid var(--line)}
100
- .rsec{display:flex;align-items:center;gap:var(--s2);width:100%;height:26px;padding:0 var(--s3);background:transparent;border:0;border-radius:0;text-align:left;color:var(--dim);font-size:13px;cursor:pointer}
101
- .rsec:hover{background:var(--panel2);color:var(--ink);border-color:transparent}
102
- .rsec.cur{background:var(--sel);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
103
- .rsec .hico{flex:none;width:16px;height:16px}
104
- .rsecl{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
105
- .rsecn{flex:none;min-width:16px;height:16px;padding:0 var(--s1);border-radius:8px;font-size:10px;font-weight:700;line-height:16px;text-align:center;color:var(--bg)}
81
+ /* the tree is long, so it keeps a scroll indicator, a thin one with no track:
82
+ WebKit (the macOS app) ignores scrollbar-width and draws a wide dark band */
83
+ aside > .tscroll{flex:1;min-height:0;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--line) transparent}
84
+ aside > .tscroll::-webkit-scrollbar{width:6px}
85
+ aside > .tscroll::-webkit-scrollbar-track{background:transparent}
86
+ aside > .tscroll::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px}
87
+ /* a section, the tree's top row: the project list's lines (project-list.css
88
+ .pjpl), a little shorter, the current one lit and marked in the accent.
89
+ The chevron column is the tree's own, so a section's rows line up under it */
90
+ .tsec{display:grid;grid-template-columns:14px 16px minmax(0,1fr) auto auto;align-items:center;gap:var(--s2);height:26px;padding:0 var(--s1) 0 var(--s2);color:var(--dim);font-size:13px;cursor:pointer;white-space:nowrap}
91
+ .tsec:hover{background:var(--panel2);color:var(--ink)}
92
+ .tsec.on{box-shadow:inset 2px 0 0 var(--accent)}
93
+ .tsec.cur{background:var(--sel);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
94
+ .tsec .hico{width:16px;height:16px}
95
+ .tsec:hover .tw{color:var(--ink)}
96
+ .rsecl{min-width:0;overflow:hidden;text-overflow:ellipsis}
97
+ .rsecn{min-width:16px;height:16px;padding:0 var(--s1);border-radius:8px;font-size:10px;font-weight:700;line-height:16px;text-align:center;color:var(--bg)}
106
98
  .rsecn.nred{background:var(--red)}
107
99
  .rsecn.nwarn{background:var(--warn)}
108
- .rsec:hover .pjkey,.rsec.cur .pjkey{opacity:1}
100
+ .tsec:hover .pjkey,.tsec.cur .pjkey{opacity:1}
109
101
  /* segmented severity filter — all · needs · red. A thin border and an
110
102
  underline on the live one: no fill, no pill */
111
103
  .seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden;flex:none}
@@ -115,9 +107,8 @@ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
115
107
  .seg button:hover{color:var(--ink)}
116
108
  .seg button.on{color:var(--accent);border-bottom-color:var(--accent)}
117
109
  .seg button .sk{margin-left:var(--s1);font-size:9px;opacity:.55}
118
- .swhen{padding:0 var(--s3) var(--s2);font-size:11px;color:var(--dim)}
119
110
  /* a narrow pane puts the severity filter under the input, never over it */
120
- .ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3)}
111
+ .ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3);border-bottom:1px solid var(--line)}
121
112
  .sfield{position:relative;flex:1 1 96px;min-width:0;display:flex}
122
113
  .ssearch input{width:100%;background:var(--bg);border:1px solid var(--line);border-radius:4px;color:var(--ink);
123
114
  padding:var(--s0) var(--s4) var(--s0) var(--s2);font-size:12px;outline:none}
@@ -133,8 +124,8 @@ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
133
124
  no prose, no dash separators ── */
134
125
  .tree{padding:var(--s1) 0 var(--s4);outline:none}
135
126
  .tree:focus-visible{box-shadow:inset 2px 0 0 var(--accent)}
136
- .trow{display:grid;grid-template-columns:calc(var(--d,0)*12px + 18px) 14px minmax(0,1fr) 58px 46px 14px;
137
- align-items:center;gap:var(--s1);padding:0 var(--s2);height:20px;line-height:20px;cursor:pointer;white-space:nowrap}
127
+ .trow{display:grid;grid-template-columns:calc(var(--d,0)*10px + 14px) 14px minmax(0,1fr) 12px auto 14px;
128
+ align-items:center;gap:var(--s1);padding:0 var(--s1) 0 var(--s2);height:20px;line-height:20px;cursor:pointer;white-space:nowrap}
138
129
  .trow:hover{background:var(--panel2)}
139
130
  .trow.on{background:var(--sel);box-shadow:inset 2px 0 0 var(--accent)}
140
131
  /* the fold marker: an 11px chevron with the whole first column as its hit
@@ -168,21 +159,20 @@ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
168
159
  .tg.ok{color:var(--ok)}
169
160
  .tg.adv{color:var(--warn);opacity:.75}
170
161
  .tg.run{color:var(--accent);animation:pulse 1.1s ease-in-out infinite}
171
- /* right aligned, tabular figures, ONE unit (ms) and one tone table, so a
172
- column of durations compares by eye (hub-app/dur.ts owns both) */
173
- .tdur{font:10.5px ui-monospace,Menlo,monospace;font-variant-numeric:tabular-nums;text-align:right;color:var(--dim);overflow:hidden}
162
+ /* the duration: a clock tinted by the tone table (hub-app/dur.ts), the
163
+ figure in its tooltip, so the label keeps the width */
164
+ .tdur{display:flex;align-items:center;justify-content:center;color:var(--dim)}
165
+ .tdur .hico{width:11px;height:11px;color:inherit}
174
166
  .tdur.fast,.dnum.fast{color:var(--ok)}
175
167
  .tdur.plain,.dnum.plain{color:var(--ink)}
176
168
  .tdur.slow,.dnum.slow{color:var(--warn)}
177
169
  .tdur.bad,.dnum.bad{color:var(--red)}
178
170
  .tnote{font-size:10px;text-align:right;color:var(--dim);overflow:hidden;text-overflow:ellipsis}
179
171
  .tnote b{font-weight:600;color:var(--red)}
180
- .stabs{display:flex;gap:var(--s2);align-items:center;padding:var(--s1) var(--s3);border-bottom:1px solid var(--line)}
181
- .stabs .sp{flex:1}
182
- .stabs .cnt{min-width:4ch;text-align:right;font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums}
183
172
  .catgen{display:inline-flex;gap:var(--s1)}
184
173
  .catgen button{padding:var(--s0) var(--s2);font-size:11px;font-weight:700}
185
174
  .tempty{padding:var(--s2) var(--s3);font-size:12px;color:var(--dim);white-space:pre-wrap}
175
+ .thidden{cursor:pointer}.thidden:hover{color:var(--ink)}
186
176
  .twait{display:flex;align-items:center;gap:var(--s2)}
187
177
  .warnline{color:var(--red)}
188
178
  .fold{cursor:pointer;color:var(--dim);font-size:15px;display:inline-block;width:22px;text-align:center;
@@ -369,7 +359,15 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
369
359
  .assetrem .rem{color:var(--ok);border-left:2px solid var(--ok);padding-left:var(--s2);font-size:12.5px;margin:0 0 var(--s2);white-space:pre-wrap}
370
360
 
371
361
  /* ── collection item card + status card ── */
372
- .colcard{max-width:760px;width:100%;background:var(--panel);border:1px solid var(--line);border-radius:3px;padding:var(--s4);text-align:left}
362
+ /* a repo page sits on the page background, never in a box of its own: its
363
+ header and its sentence are unboxed, and each group it holds (the facts,
364
+ a titled section, the command, the admin rows, the runs list) is one
365
+ lighter box, so a box always means "these belong together" */
366
+ .colcard{max-width:760px;width:100%;padding:var(--s2) 0;text-align:left}
367
+ .colcard>.cfacts,.colcard>.csec,.colcard>.ccmd,.colcard>.md:not(.usays),.colcard>.adbox,.tlcard>.rlist{background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:var(--s2) var(--s3)}
368
+ .colcard>.cfacts,.colcard>.ccmd,.colcard>.md:not(.usays),.colcard>.adbox{margin-top:var(--s3)}
369
+ .ccmd>.cf:first-child,.cfacts>.cf:nth-child(-n+2){border-top:0}
370
+ @media (max-width:900px){.cfacts>.cf:nth-child(2){border-top:1px solid var(--line)}}
373
371
  .colcard h3{margin:0 0 var(--s2);font-size:15px;display:flex;gap:var(--s2);align-items:baseline}
374
372
  .colcard h3 .sp{flex:1}
375
373
  .colcard .truth{margin-left:auto;font:11px ui-monospace,Menlo,monospace;color:var(--dim)}
@@ -560,6 +558,15 @@ a.tok{color:var(--accent)}
560
558
  .checkcard .dim{color:var(--dim)}
561
559
  /* what is not a fact: the output and the slow suites, titled, unboxed */
562
560
  .csec{margin-top:var(--s4)}
561
+ /* where it runs: one line per context, the value then its meaning. A context
562
+ that never runs the check stays dim, so the lit lines read down the list */
563
+ .cruns{display:grid;grid-template-columns:auto auto minmax(0,1fr);column-gap:var(--s3);row-gap:var(--s0);align-items:baseline}
564
+ .crun{display:contents;color:var(--dim)}
565
+ .crv{font:11px ui-monospace,Menlo,monospace;white-space:nowrap}
566
+ .crun.on .crv{color:var(--ok)}
567
+ .crun.cache .crv{color:var(--ink)}
568
+ .crm{font-size:11.5px;color:var(--dim);opacity:.8}
569
+ .crun:not(.on) .crm{opacity:.5}
563
570
  .cstitle{display:flex;gap:var(--s1);align-items:center;font-size:11px;text-transform:uppercase;letter-spacing:.08em;
564
571
  color:var(--dim);margin-bottom:var(--s2)}
565
572
  .wline{margin-bottom:var(--s1);word-break:break-word}
@@ -628,6 +635,7 @@ a.tok{color:var(--accent)}
628
635
  .colgrid{flex:1;min-height:0;overflow-y:auto;padding:var(--s3) var(--s4)}
629
636
  .cghead{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
630
637
  display:flex;gap:var(--s2);align-items:baseline;margin:0 0 var(--s3)}
638
+ .ctlead{font-size:11px;color:var(--dim);margin:calc(-1 * var(--s1)) 0 var(--s2)}
631
639
  .cghead .cnt{font-size:11px;color:var(--dim);letter-spacing:0;text-transform:none}
632
640
  /* the checks and asks pages: one dense table each (col-tables-view.tsx), on
633
641
  the activity table (.actable), a group header per state that folds */
@@ -745,9 +753,23 @@ pre{background:var(--panel);border:1px solid var(--line);border-radius:3px;paddi
745
753
 
746
754
  /* ── the admin page: one row per control, its sentence under its name ── */
747
755
  .adrow{display:flex;align-items:center;gap:var(--s4);padding:var(--s3) 0;border-top:1px solid var(--line)}
756
+ .adrow:first-child{border-top:0}
748
757
  .adtext{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--s0);font-size:13px}
749
758
  .adtext .dim{font-size:12px}
750
759
  .adctl{flex:none;display:flex;align-items:center}
751
760
  .adflag{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px;color:var(--dim)}
752
761
  .adflag input{width:auto;margin:0}
753
762
  .adconfirm{display:inline-flex;align-items:center;gap:var(--s2);font-size:12px}
763
+ /* a repo's plan page: one line per planned check */
764
+ .plntable{width:100%;border-collapse:collapse;font-size:12.5px}
765
+ .plntable td{padding:var(--s1) var(--s2);border-top:1px solid var(--line);vertical-align:baseline}
766
+ .plntable tr:first-child td{border-top:0}
767
+ .plnwhen{white-space:nowrap;font-variant-numeric:tabular-nums}
768
+ .plnrun{text-align:right;white-space:nowrap}
769
+ /* a repo's git page */
770
+ .gitfiles,.gitcommits{width:100%;border-collapse:collapse;font-size:12.5px}
771
+ .gitfiles td,.gitcommits td{padding:2px var(--s2);vertical-align:baseline}
772
+ .gitkind{width:1%;white-space:nowrap;color:var(--dim)}
773
+ .gitkind.staged{color:var(--ok)}
774
+ .gitkind.untracked{color:var(--warn)}
775
+ .gitpath{font:12px ui-monospace,Menlo,monospace;overflow-wrap:anywhere}
@@ -5,6 +5,11 @@
5
5
  export const ICON = {
6
6
  shipkit: 'M3 7.5 10 4l7 3.5v5L10 16l-7-3.5zM3 7.5 10 11l7-3.5M10 11v5',
7
7
  process: 'M2 10h3.5l2-5 5 10 2-5H18',
8
+ processes: 'M2 10h3.5l2-5 5 10 2-5H18',
9
+ when: 'M10 3a7 7 0 1 0 .01 0M10 6v4l2.5 2',
10
+ plan: 'M5.5 3h9M5.5 17h9M6.5 3c0 4 7 4 7 7s-7 3-7 7M13.5 3c0 4-7 4-7 7s7 3 7 7',
11
+ git: 'M6 5v10M6 3a2 2 0 1 0 0 .01M6 17a2 2 0 1 0 0 .01M14 5a2 2 0 1 0 0 .01M14 7c0 4.5-8 3.5-8 7',
12
+ reflections: 'M4 3.5h12v9H9l-4 3.5v-3.5H4zM7.5 7h5M7.5 9.5h3',
8
13
  monitor: 'M3 4h14v9H3zM7 16.5h6M10 13v3.5',
9
14
  archived: 'M2.5 4h15v3.5h-15zM4 7.5V16h12V7.5M8 10.5h4',
10
15
  home: 'M3 9.5 10 3.5l7 6M5 8v8.5h10V8M8.5 16.5v-4.5h3v4.5',
@@ -15,7 +20,11 @@ export const ICON = {
15
20
  fork: 'M6 5v10M6 3a2 2 0 1 0 0 .01M6 17a2 2 0 1 0 0 .01M14 5a2 2 0 1 0 0 .01M14 7c0 4.5-8 3.5-8 7',
16
21
  search: 'M8.5 3a5.5 5.5 0 1 0 0 11a5.5 5.5 0 1 0 0-11M12.5 12.5 17 17',
17
22
  // a repo page's sections, then its two runs and its editor
23
+ readme: 'M5 2.5h7l3 3v12H5zM12 2.5v3h3M7.5 9h5M7.5 12h5M7.5 15h3',
18
24
  checks: 'M4 10.5 7.5 14 16 5.5',
25
+ contracts: 'M5 2.5h10v15H5zM7.5 6h5M7.5 9h5M7.5 12h2.5',
26
+ repo: 'M2.5 5h5l1.5 1.5h8.5v9.5h-15z',
27
+ secrets: 'M7 7a3 3 0 1 0 0 6a3 3 0 1 0 0-6M10 10h7.5M15 10v2.5M17.5 10v2',
19
28
  runs: 'M3 5h8M3 10h12M3 15h6',
20
29
  asks: 'M3 4h14v9H8l-4 3.5V13H3z',
21
30
  tests: 'M8 3h4M8.5 3v5L4 16.5h12L11.5 8V3M6.2 12.5h7.6',
@@ -30,6 +39,8 @@ export const ICON = {
30
39
  // a process's own switches on the overview
31
40
  play: 'M6.5 4.5v11l9-5.5z',
32
41
  stop: 'M5.5 5.5h9v9h-9z',
42
+ link: 'M8.5 11.5a3.5 3.5 0 0 0 5 0l2.5-2.5a3.5 3.5 0 0 0-5-5l-1 1M11.5 8.5a3.5 3.5 0 0 0-5 0L4 11a3.5 3.5 0 0 0 5 5l1-1',
43
+ clock: 'M10 2.5a7.5 7.5 0 1 0 0 15a7.5 7.5 0 1 0 0-15M10 6v4l2.5 2',
33
44
  restart: 'M15.5 10a5.5 5.5 0 1 1-1.6-3.9M14.2 3v3.3h-3.3',
34
45
  } as const
35
46
 
@@ -9,6 +9,7 @@
9
9
  <link rel="stylesheet" href="./prose-hits.css" />
10
10
  <link rel="stylesheet" href="./home-strip.css" />
11
11
  <link rel="stylesheet" href="./planned.css" />
12
+ <link rel="stylesheet" href="./when.css" />
12
13
  <link rel="stylesheet" href="./activity.css" />
13
14
  <link rel="stylesheet" href="./readme.css" />
14
15
  <link rel="stylesheet" href="./dock.css" />
@@ -46,8 +46,11 @@ export function isSearchKey(e: Pick<KeyLike, 'key' | 'metaKey' | 'ctrlKey' | 'al
46
46
  * plus a letter only types a character inside a text field, where the page
47
47
  * leaves the key alone. Read from the physical key (`code`): on a Mac ⌥C
48
48
  * types ç, and AZERTY moves the letters */
49
- export type SectionId = 'checks' | 'runs' | 'asks' | 'tests' | 'media' | 'wikis' | 'admin'
50
- export const SECTION_LETTER: Record<SectionId, string> = { checks: 'C', runs: 'R', asks: 'A', tests: 'T', media: 'M', wikis: 'W', admin: 'D' }
49
+ export type SectionId = 'readme' | 'contracts' | 'shipkit' | 'repo' | 'secrets' | 'when' | 'runs' | 'plan' | 'processes' | 'git' | 'asks' | 'reflections' | 'tests' | 'media' | 'wikis' | 'admin'
50
+ export const SECTION_LETTER: Record<SectionId, string> = { readme: 'E', contracts: 'K', shipkit: 'S', repo: 'C', secrets: 'X', when: 'O', runs: 'R', plan: 'N', processes: 'P', git: 'G', asks: 'A', reflections: 'F', tests: 'T', media: 'M', wikis: 'W', admin: 'D' }
51
+ /** a section's words on its row, where the id alone would not say it */
52
+ export const SECTION_LABEL: Partial<Record<SectionId, string>> = { shipkit: 'shipkit checks', repo: 'repo checks' }
53
+ export const sectionLabel = (id: SectionId): string => SECTION_LABEL[id] ?? id
51
54
  /** the badge at the end of a section's line */
52
55
  export const sectionKeyLabel = (id: SectionId): string => `⌥${SECTION_LETTER[id]}`
53
56
 
@@ -11,6 +11,7 @@ import type { TimelineInvocation } from '../../ledger/run-timeline.ts'
11
11
  import { ChartView, type ChartSource } from './chart-view.ts'
12
12
  import type { RepoSt } from './repo-st.ts'
13
13
  import { chartHeight } from './timeline-geometry.ts'
14
+ import { ownTrouble, reach } from './reach.ts'
14
15
 
15
16
  /** how often the page asks. A strip a second behind the runner is still live */
16
17
  export const LIVE_POLL_MS = 1_000
@@ -107,7 +108,7 @@ export class LiveSvc {
107
108
  if (this.inFlight) return
108
109
  this.inFlight = true
109
110
  try {
110
- const r = await fetch(this.st.url('/api/live.json'))
111
+ const r = await reach(this.st.url('/api/live.json'))
111
112
  if (!r.ok) throw new Error(await r.text())
112
113
  const v = (await r.json()) as LiveView
113
114
  const ended = this.running && v.runs.length === 0
@@ -125,7 +126,7 @@ export class LiveSvc {
125
126
  if (this.st.timeline.data != null) void this.st.timeline.load(true)
126
127
  }
127
128
  } catch (e) {
128
- runInAction(() => { this.error = `the live runs could not be read: ${(e as Error).message}` })
129
+ runInAction(() => { this.error = ownTrouble(e, this.st.home.machine, (m) => `the live runs could not be read: ${m}`) })
129
130
  } finally {
130
131
  this.inFlight = false
131
132
  }