rvlib-shipkit 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/assets/gen/comfyts-gen.ts +12 -8
  5. package/src/assets/server/studio-server.ts +14 -2
  6. package/src/check.ts +73 -103
  7. package/src/checks/SK001-package-json-shape.check.ts +11 -6
  8. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  9. package/src/checks/SK003-claude-md-shape.check.ts +7 -3
  10. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  11. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  12. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  13. package/src/checks/SK007-github-visibility.check.ts +4 -2
  14. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  15. package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
  16. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  17. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  18. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  19. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  20. package/src/checks/SK014-prose-ste.check.ts +4 -0
  21. package/src/checks/SK015-nested-repo.check.ts +4 -2
  22. package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
  23. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  24. package/src/checks/SK018-test-files-run.check.ts +6 -2
  25. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  26. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  27. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  28. package/src/checks/SK024-identity-colors.check.ts +5 -3
  29. package/src/checks/SK025-release-drift.check.ts +4 -5
  30. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  31. package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
  32. package/src/checks/SK028-readme.check.ts +5 -1
  33. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  34. package/src/checks/SK030-cache-audit.check.ts +34 -0
  35. package/src/checks/SK031-trace-audit.check.ts +39 -0
  36. package/src/checks/SK032-ignored-inputs.check.ts +79 -0
  37. package/src/cli/cmd_Asks.ts +5 -5
  38. package/src/cli/cmd_Assets.ts +17 -13
  39. package/src/cli/cmd_Bump.ts +12 -11
  40. package/src/cli/cmd_CacheAudit.ts +43 -0
  41. package/src/cli/cmd_Check.ts +8 -7
  42. package/src/cli/cmd_Checks.ts +7 -6
  43. package/src/cli/cmd_Ci.ts +12 -11
  44. package/src/cli/cmd_Commands.ts +16 -13
  45. package/src/cli/cmd_ConfigKeys.ts +7 -6
  46. package/src/cli/cmd_Contracts.ts +3 -3
  47. package/src/cli/cmd_CrossRepoActivity.ts +8 -6
  48. package/src/cli/cmd_Fleet.ts +9 -7
  49. package/src/cli/cmd_GlobalProcesses.ts +120 -7
  50. package/src/cli/cmd_Home.ts +26 -19
  51. package/src/cli/cmd_Hooks.ts +6 -5
  52. package/src/cli/cmd_Init.ts +5 -4
  53. package/src/cli/cmd_Keys.ts +20 -8
  54. package/src/cli/cmd_Mv.ts +7 -6
  55. package/src/cli/cmd_Needs.ts +4 -3
  56. package/src/cli/cmd_Overview.ts +5 -4
  57. package/src/cli/cmd_Pb.ts +2 -2
  58. package/src/cli/cmd_Planned.ts +6 -5
  59. package/src/cli/cmd_Prose.ts +5 -5
  60. package/src/cli/cmd_Reflect.ts +7 -7
  61. package/src/cli/cmd_Release.ts +6 -5
  62. package/src/cli/cmd_Releases.ts +5 -4
  63. package/src/cli/cmd_Serve.ts +6 -4
  64. package/src/cli/cmd_Smoke.ts +5 -4
  65. package/src/cli/cmd_Start.ts +7 -6
  66. package/src/cli/cmd_Status.ts +4 -3
  67. package/src/cli/cmd_TraceAudit.ts +62 -0
  68. package/src/cli/cmd_Tui.ts +3 -2
  69. package/src/cli/cmd_When.ts +71 -0
  70. package/src/cli/cmd_Wiki.ts +4 -4
  71. package/src/cli.ts +11 -2
  72. package/src/collections/checks.ts +2 -0
  73. package/src/collections/collection-types.ts +12 -4
  74. package/src/collections/providers.ts +5 -3
  75. package/src/core/app-icon.ts +154 -0
  76. package/src/core/audit-report.ts +21 -0
  77. package/src/core/cache-audit.ts +72 -0
  78. package/src/core/canonical-scripts.ts +2 -2
  79. package/src/core/check-ctx.ts +14 -5
  80. package/src/core/check-runner.ts +43 -23
  81. package/src/core/config-rewrite.ts +37 -24
  82. package/src/core/discover-checks.ts +23 -2
  83. package/src/core/floor-resolve.ts +5 -7
  84. package/src/core/git-root.ts +13 -0
  85. package/src/core/ignored-block.ts +94 -0
  86. package/src/core/ignored-inputs.ts +47 -0
  87. package/src/core/jobs.ts +9 -2
  88. package/src/core/legacy-shapes.ts +39 -10
  89. package/src/core/one-lib.ts +59 -4
  90. package/src/core/one-way-in-policy.ts +2 -2
  91. package/src/core/outcome-words.ts +18 -2
  92. package/src/core/port-check-cli.ts +52 -0
  93. package/src/core/port-check-file.ts +295 -0
  94. package/src/core/port-check-files.ts +31 -0
  95. package/src/core/read-set.ts +97 -9
  96. package/src/core/retention.ts +0 -14
  97. package/src/core/run-check.ts +12 -9
  98. package/src/core/run-ci.ts +14 -5
  99. package/src/core/run-fix.ts +30 -23
  100. package/src/core/run-fixers.ts +48 -5
  101. package/src/core/runs-matrix.ts +179 -0
  102. package/src/core/runs.ts +242 -0
  103. package/src/core/trace-audit.ts +120 -0
  104. package/src/core/update-policy.ts +20 -0
  105. package/src/core/when-block.ts +29 -30
  106. package/src/core/when.ts +41 -103
  107. package/src/core/wire.ts +10 -14
  108. package/src/fleet/scan-fleet.ts +20 -4
  109. package/src/fleet/update-fleet.ts +6 -6
  110. package/src/home/gui-app.ts +24 -27
  111. package/src/home/home-launchd.ts +20 -4
  112. package/src/home/home-routes.ts +12 -1
  113. package/src/home/home-start.ts +11 -5
  114. package/src/home/hubs.ts +5 -5
  115. package/src/home/open-app.ts +2 -2
  116. package/src/home/pm.ts +8 -0
  117. package/src/home/run-pm.ts +1 -0
  118. package/src/home/serve-web.ts +2 -2
  119. package/src/home/supervise.ts +24 -1
  120. package/src/home/via-hub.ts +14 -0
  121. package/src/hooks/run-hooks.ts +48 -4
  122. package/src/init-repo.ts +6 -6
  123. package/src/ledger/db.ts +2 -13
  124. package/src/ledger/gate-checks.ts +133 -61
  125. package/src/ledger/git-key.ts +153 -31
  126. package/src/ledger/git-scan.ts +5 -2
  127. package/src/ledger/ledger.ts +136 -21
  128. package/src/ledger/store.ts +14 -0
  129. package/src/ledger/toolchain-identity.ts +248 -45
  130. package/src/plan/repo-source.ts +18 -39
  131. package/src/problems/extract.ts +3 -0
  132. package/src/release/run-release.ts +5 -0
  133. package/src/repo-config.ts +46 -54
  134. package/src/rules/rule.ts +2 -2
  135. package/src/scaffold/files/agent-coding.md +1 -1
  136. package/src/scaffold/files/agent-index.md +1 -1
  137. package/src/scaffold/files/verify-skill.md +2 -2
  138. package/src/serve/hub-app/activity-st.ts +5 -4
  139. package/src/serve/hub-app/activity-view.tsx +1 -1
  140. package/src/serve/hub-app/admin-svc.ts +36 -3
  141. package/src/serve/hub-app/admin-view.tsx +46 -12
  142. package/src/serve/hub-app/check-view.tsx +66 -46
  143. package/src/serve/hub-app/check-words.ts +1 -11
  144. package/src/serve/hub-app/checks-svc.ts +14 -35
  145. package/src/serve/hub-app/col-card.tsx +1 -44
  146. package/src/serve/hub-app/col-tables-view.tsx +84 -11
  147. package/src/serve/hub-app/col-tables.ts +27 -0
  148. package/src/serve/hub-app/cols-svc.ts +5 -4
  149. package/src/serve/hub-app/dock-st.ts +3 -2
  150. package/src/serve/hub-app/dock-view.tsx +36 -0
  151. package/src/serve/hub-app/dock.css +6 -0
  152. package/src/serve/hub-app/float-cards.tsx +30 -0
  153. package/src/serve/hub-app/floating.tsx +41 -4
  154. package/src/serve/hub-app/git-st.ts +42 -0
  155. package/src/serve/hub-app/git-view.tsx +61 -0
  156. package/src/serve/hub-app/hash-route.ts +45 -1
  157. package/src/serve/hub-app/header-bar.tsx +62 -72
  158. package/src/serve/hub-app/health-svc.ts +3 -2
  159. package/src/serve/hub-app/home-st.ts +14 -2
  160. package/src/serve/hub-app/home-strip.css +43 -12
  161. package/src/serve/hub-app/home-strip.tsx +115 -64
  162. package/src/serve/hub-app/home.html +1 -0
  163. package/src/serve/hub-app/hub-app.tsx +32 -8
  164. package/src/serve/hub-app/hub-types.ts +5 -2
  165. package/src/serve/hub-app/hub.css +89 -63
  166. package/src/serve/hub-app/icons.tsx +11 -0
  167. package/src/serve/hub-app/index.html +1 -0
  168. package/src/serve/hub-app/keys.ts +15 -4
  169. package/src/serve/hub-app/live-svc.ts +3 -2
  170. package/src/serve/hub-app/machine-st.ts +18 -22
  171. package/src/serve/hub-app/plan-view.tsx +51 -0
  172. package/src/serve/hub-app/planned-st.ts +9 -4
  173. package/src/serve/hub-app/processes-view.tsx +45 -0
  174. package/src/serve/hub-app/project-list.css +4 -1
  175. package/src/serve/hub-app/project-list.tsx +6 -3
  176. package/src/serve/hub-app/projects-svc.ts +25 -10
  177. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  178. package/src/serve/hub-app/reach.ts +36 -0
  179. package/src/serve/hub-app/readme-svc.ts +3 -2
  180. package/src/serve/hub-app/repo-sections.ts +131 -0
  181. package/src/serve/hub-app/repo-st.ts +70 -14
  182. package/src/serve/hub-app/sidebar.tsx +17 -42
  183. package/src/serve/hub-app/timeline-svc.ts +3 -2
  184. package/src/serve/hub-app/tree-build.ts +151 -155
  185. package/src/serve/hub-app/tree-row.tsx +55 -12
  186. package/src/serve/hub-app/tree-svc.ts +97 -56
  187. package/src/serve/hub-app/when-view.tsx +89 -0
  188. package/src/serve/hub-app/when.css +17 -0
  189. package/src/serve/hub-runtime.ts +7 -18
  190. package/src/serve/project-gutter-route.ts +22 -1
  191. package/src/serve/run-serve.ts +1 -1
  192. package/src/serve/serve-hub.ts +4 -14
  193. package/src/suite/judge-suites.ts +6 -0
  194. package/src/trace/interpose.c +655 -0
  195. package/src/trace/read-trace.ts +485 -0
  196. package/src/trace/tracer.ts +282 -0
  197. package/src/serve/hub-app/processes-svc.ts +0 -63
  198. package/src/serve/hub-app/repo-sections.tsx +0 -61
  199. package/src/serve/repo-processes.ts +0 -89
@@ -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(' · ')
@@ -154,9 +157,10 @@ const ProcessRepo = observer(({ app, name, icon }: { app: HomeSt; name: string;
154
157
  /** the processes as one table in the activity page's style: shipkit's own
155
158
  * first, then a row group per declaring repo in the project list's order,
156
159
  * its name in the first column, at the top of its group, then one line per
157
- * process. The url's header carries the new window button */
158
- const ProcessTable = observer(({ app, m }: { app: HomeSt; m: MachineSt }) => {
159
- const groups = m.processGroups
160
+ * process. The url's header carries the new window button. `repo` keeps
161
+ * that one repo's group, the table of its processes page */
162
+ export const ProcessTable = observer(({ app, m, repo }: { app: HomeSt; m: MachineSt; repo?: string }) => {
163
+ const groups = repo == null ? m.processGroups : m.processGroups.filter((g) => g.core !== true && g.repo === repo)
160
164
  const rows = groups.flatMap((g) => g.rows)
161
165
  // wide enough for the longest set of declared actions, in the button font
162
166
  const act = Math.max(0, ...rows.map((r) => r.actions.reduce((n, a) => n + a.label.length + 3, 0)))
@@ -222,19 +226,6 @@ const Monitor = observer(({ m }: { m: MachineSt }) => {
222
226
  )
223
227
  })
224
228
 
225
- /** the two colours of a repo, a picker each: the page draws the dark one */
226
- export const IdentitySwatches = observer(({ app, name }: { app: HomeSt; name: string }) => {
227
- const id = app.projects.identityOf(name)
228
- return (
229
- <span className="hswatch">
230
- <input type="color" aria-label="colour on a dark background" title={`${name} on a dark background (this page): ${id.colorDark}, written to repo.config.ts identity.colorDark`}
231
- value={id.colorDark} onChange={(e) => app.projects.setIdentity(name, { colorDark: e.currentTarget.value })} />
232
- <input type="color" aria-label="colour on a light background" title={`${name} on a light background: ${id.colorLight}, written to repo.config.ts identity.colorLight`}
233
- value={id.colorLight} onChange={(e) => app.projects.setIdentity(name, { colorLight: e.currentTarget.value })} />
234
- </span>
235
- )
236
- })
237
-
238
229
  /** the shown repo in the strip: its icon, its name in its colour */
239
230
  const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
240
231
  const p = app.projects.list.find((x) => x.name === name)
@@ -244,23 +235,96 @@ const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
244
235
  {/* the icon's box is there before the project list is, so the name never moves */}
245
236
  {p?.icon != null ? <img src={p.icon} alt="" width={18} height={18} /> : <i className="hrico" />}
246
237
  <b className="hrname" style={{ color: id.colorDark }}>{name}</b>
247
- <IdentitySwatches app={app} name={name} />
248
238
  </span>
249
239
  )
250
240
  })
251
241
 
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
- ))
242
+ /** the machine not answering, as ONE card: the web (pm behind it is then
243
+ * unknown, not down), the daemon, pm, or the daemon and pm together */
244
+ const downCard = (m: MachineSt): Card[] => {
245
+ const web = m.down !== '' && m.downWho === 'web'
246
+ const pm = m.pmDown !== '' && !web
247
+ if (m.down === '' && !pm) return []
248
+ const head = m.down === '' ? 'pm down' : pm ? 'daemon and pm down' : `${m.downWho} down`
249
+ return [{ key: 'down', tone: 'red', head, body: m.down === '' ? m.pmDown : pm ? <>{m.down}<br />{m.pmDown}</> : m.down }]
250
+ }
251
+
252
+ /** what is wrong with the machine itself, one card each: the machine not
253
+ * answering, a crashed child of pm, a crashed global process (its switch
254
+ * starts it again), a failed poll. While pm is silent its last answer says
255
+ * nothing here */
256
+ const machineCards = (m: MachineSt): Card[] => {
257
+ const pm = m.pmDown !== '' ? null : m.pm
258
+ return [
259
+ ...downCard(m),
260
+ ...(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) ?? '' })),
261
+ ...(pm?.resources ?? []).filter((r) => r.status === 'crashed').map((r): Card => ({ key: `res:${r.id}`, tone: 'red', head: 'crashed', body: <ResourceChip m={m} r={r} /> })),
262
+ ...(m.error !== '' ? [{ key: 'poll', tone: 'red' as const, body: m.error }] : []),
263
+ ]
264
+ }
265
+
266
+ /** a global process's menu: start or stop, restart, its url opened three
267
+ * ways (each starts it first when it is down), then its declared actions and
268
+ * take over. Every entry is a click on purpose, never the chip itself */
269
+ /** `lastLog: false` where the whole log is drawn under it, so its last line is not said twice */
270
+ export const ProcessMenu = observer(({ m, r, close, lastLog = true }: { m: MachineSt; r: ProcessRow; close: () => void; lastLog?: boolean }) => {
271
+ const on = r.status === 'up' || r.status === 'starting'
272
+ const first = r.status === 'up' ? '' : ', starts it first'
273
+ const act = (a: Parameters<MachineSt['processAct']>[1], action?: string) => () => { close(); void m.processAct(r.id, a, action) }
274
+ const open = (how: OpenHow) => () => { close(); void m.openProcess(r.id, how) }
275
+ const log = lastLog ? r.lastLog : null
276
+ const about = r.description != null && r.description !== '' || r.note != null || log != null
277
+ return (
278
+ <div className="hmenu2">
279
+ <div className="hmenu hmacts">
280
+ <div className="hmhead"><i className={`pdot ${r.status}`} /><b>{r.label}</b><span>{r.status}</span></div>
281
+ {on ? (
282
+ <button className="hmitem" disabled={r.status === 'stopping' || !r.canStop} onClick={act('stop')}>
283
+ <Icon d={ICON.stop} /><span><b>stop</b>{r.canStop ? null : <small>started outside pm, so pm cannot stop it</small>}</span>
284
+ </button>
285
+ ) : (
286
+ <button className="hmitem" disabled={r.status === 'unknown'} onClick={act('start')}><Icon d={ICON.play} /><span><b>start</b></span></button>
287
+ )}
288
+ {r.canRestart ? <button className="hmitem" onClick={act('restart')}><Icon d={ICON.restart} /><span><b>restart</b></span></button> : null}
289
+ {r.url == null ? null : (
290
+ <>
291
+ <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>
292
+ <button className="hmitem" onClick={open('chrome')}><Icon d={ICON.browser} /><span><b>open in Chrome</b><small>{r.host}{first}</small></span></button>
293
+ <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>
294
+ </>
295
+ )}
296
+ {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>)}
297
+ {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}
298
+ </div>
299
+ {about ? (
300
+ <div className="hmabout">
301
+ {r.description != null && r.description !== '' ? <div className="md" dangerouslySetInnerHTML={{ __html: renderMd(r.description) }} /> : null}
302
+ {r.note != null ? <small className="hmnote">{r.note}</small> : null}
303
+ {log != null ? <code className="hmlog" title="its last log line">{log}</code> : null}
304
+ </div>
305
+ ) : null}
306
+ </div>
307
+ )
308
+ })
309
+
310
+ /** the shown repo's global processes, one chip each, right after its name: its
311
+ * dot and label, and a click opens its menu (ProcessMenu). pm runs them, so
312
+ * they show whether the repo's hub answers or not */
313
+ const RepoProcesses = observer(({ m, name }: { m: MachineSt; name: string }) => {
314
+ const rows = m.processGroups.find((g) => g.core !== true && g.repo === name)?.rows ?? []
315
+ if (rows.length === 0) return null
316
+ return (
317
+ <span className="hprocs hgprocs">
318
+ {rows.map((r) => (
319
+ <span key={r.id} className="hproc">
320
+ <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}</>}>
321
+ {(close) => <ProcessMenu m={m} r={r} close={close} />}
322
+ </Pop>
323
+ </span>
324
+ ))}
325
+ </span>
326
+ )
327
+ })
264
328
 
265
329
  export const HomeStrip = observer(({ app }: { app: HomeSt }) => {
266
330
  const m = app.machine
@@ -273,33 +337,17 @@ export const HomeStrip = observer(({ app }: { app: HomeSt }) => {
273
337
  <>
274
338
  <div className="hstrip">
275
339
  {name == null ? <span className="hplace">{app.page === 'overview' ? 'overview' : app.page}</span> : <ShownRepo app={app} name={name} />}
276
- {live == null ? null : <RepoRun app={live} />}
340
+ {name == null ? null : <RepoProcesses m={m} name={name} />}
277
341
  <span className="sp" />
278
342
  {live == null ? null : <RepoTools app={live} />}
279
- <MachineAlerts m={m} />
343
+ {live == null ? null : <RepoRun app={live} />}
280
344
  {live == null ? null : <RepoOpeners app={live} />}
281
345
  </div>
282
- <MachineToasts app={app} />
346
+ <FloatCards cards={[...(live == null ? [] : hubCards(live)), ...machineCards(m)]} />
283
347
  </>
284
348
  )
285
349
  })
286
350
 
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
351
  /** archive the repo or bring it back: `archived` in its repo.config.ts, and
304
352
  * only that key. Archived, it stays listed and nothing runs there */
305
353
  export const ArchiveButton = observer(({ app, name }: { app: HomeSt; name: string }) => {
@@ -326,7 +374,7 @@ export const NotUp = observer(({ app }: { app: HomeSt }) => {
326
374
  <ArchiveButton app={app} name={name} />
327
375
  </div>
328
376
  )
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>
377
+ if (w == null) return <div className="hwait"><p className="twait"><span className="spin" />{`looking for ${name}…`}</p></div>
330
378
  return (
331
379
  <div className="hwait">
332
380
  {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 +414,12 @@ export const Overview = observer(({ app }: { app: HomeSt }) => {
366
414
  const m = app.machine
367
415
  if (m == null) return null
368
416
  // 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
417
+ // rows never arrive or reorder after it paints. The daemon silent, it
418
+ // draws whatever it last had: the down card says the rest
370
419
  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>
420
+ const nothing = m.data == null && m.pm == null && m.widgets == null
421
+ const first = m.data == null || (m.pm == null && m.pmDown === '') || (p.list.length === 0 && !p.fresh && p.error === '')
422
+ if (nothing || (m.down === '' && first)) return <div className="hwait"><p className="twait"><span className="spin" />loading…</p></div>
372
423
  return (
373
424
  <div className="hall">
374
425
  <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,16 +2,17 @@
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'
11
12
  import { StrictMode, useEffect, useLayoutEffect, useRef } from 'react'
12
13
  import { createRoot } from 'react-dom/client'
13
- import { ColCard, ColGrid, StatusCard } from './col-card.tsx'
14
- import { AskTable, CheckTable } from './col-tables-view.tsx'
14
+ import { ColCard, StatusCard } from './col-card.tsx'
15
+ import { AskTable, CheckTable, ItemTable, SecretTable } from './col-tables-view.tsx'
15
16
  import { WIKIS, WikisView } from './wikis-view.tsx'
16
17
  import { ComfyOverlay, ComfyView } from './comfy-view.tsx'
17
18
  import type { Filter, RepoSt } from './repo-st.ts'
@@ -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'
@@ -88,11 +93,16 @@ const Main = observer(({ app }: { app: RepoSt }) => {
88
93
  if (app.sel?.t === 'col') return (
89
94
  <main>
90
95
  {errs}
91
- {app.sel.id === 'checks' ? <CheckTable app={app} id={app.sel.id} /> : app.sel.id === 'asks' ? <AskTable app={app} id={app.sel.id} /> : <ColGrid app={app} id={app.sel.id} />}
96
+ {app.sel.id === 'checks' ? <CheckTable app={app} id={app.sel.id} /> : app.sel.id === 'asks' ? <AskTable app={app} id={app.sel.id} /> : app.sel.id === 'creds' ? <SecretTable app={app} id={app.sel.id} /> : <ItemTable app={app} id={app.sel.id} />}
92
97
  </main>
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 (
@@ -185,13 +195,18 @@ const RepoView = observer(({ app }: { app: RepoSt }) => {
185
195
  function onKey(e: KeyboardEvent): void {
186
196
  // the command keys beat the focus check: they work from anywhere, any page
187
197
  const pk = pageKey(e)
188
- if (pk != null) {
198
+ // in a text field ⌘↑ and ⌘↓ keep moving the caret to its ends
199
+ const typing = (e.target as HTMLElement).tagName === 'INPUT' || (e.target as HTMLElement).tagName === 'TEXTAREA'
200
+ if (pk != null && !(pk.t === 'step' && typing)) {
189
201
  e.preventDefault()
190
202
  if (pk.t === 'picker') home.palette.toggle()
191
203
  else if (pk.t === 'dock') home.dock.cycle()
192
204
  else if (pk.t === 'home' || pk.t === 'planned' || pk.t === 'activity') {
193
205
  home.palette.close()
194
206
  if (home.machine != null) home.navigate(PLACE_PATH[pk.t === 'home' ? 'overview' : pk.t])
207
+ } else if (pk.t === 'step') {
208
+ home.palette.close()
209
+ home.projects.step(pk.by)
195
210
  } else {
196
211
  const p = home.projects.list[pk.n - 1]
197
212
  home.palette.close()
@@ -225,6 +240,15 @@ function onKey(e: KeyboardEvent): void {
225
240
  return
226
241
  }
227
242
  if (e.metaKey || e.ctrlKey || e.altKey) return
243
+ // the tree's keys need no focus: the arrows move it from anywhere on the
244
+ // page, Enter and space only where no button or link claims them
245
+ // the atelier's ←/→ roll, so at its first or last roll they stop there
246
+ const rolls = app.sel?.t === 'asset' && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')
247
+ const own = !rolls && (e.key.startsWith('Arrow') || t.closest('button, a, select, [role="button"]') == null)
248
+ if (own && app.tree.key(e.key)) {
249
+ e.preventDefault()
250
+ return
251
+ }
228
252
  if (e.key === FOLLOW_KEY && (!app.live.chart.follow || !home.dock.chart.follow)) {
229
253
  e.preventDefault()
230
254
  app.live.chart.attach()
@@ -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