@skillcdn/console 0.4.0 → 0.4.2

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 (308) hide show
  1. package/README.md +20 -2
  2. package/SKILLCDN.md +7 -5
  3. package/dist/api.d.ts +5 -4
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +5 -4
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +579 -45
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts +34 -3
  11. package/dist/cli/format.d.ts.map +1 -1
  12. package/dist/cli/format.js +277 -6
  13. package/dist/cli/format.js.map +1 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +60 -7
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/plugin-check.d.ts +21 -0
  18. package/dist/cli/plugin-check.d.ts.map +1 -0
  19. package/dist/cli/plugin-check.js +374 -0
  20. package/dist/cli/plugin-check.js.map +1 -0
  21. package/dist/cli/plugin-cli.d.ts +20 -0
  22. package/dist/cli/plugin-cli.d.ts.map +1 -0
  23. package/dist/cli/plugin-cli.js +123 -0
  24. package/dist/cli/plugin-cli.js.map +1 -0
  25. package/dist/cli/plugin-init.d.ts +20 -0
  26. package/dist/cli/plugin-init.d.ts.map +1 -0
  27. package/dist/cli/plugin-init.js +74 -0
  28. package/dist/cli/plugin-init.js.map +1 -0
  29. package/dist/cli/plugin-main.d.ts +3 -0
  30. package/dist/cli/plugin-main.d.ts.map +1 -0
  31. package/dist/cli/plugin-main.js +28 -0
  32. package/dist/cli/plugin-main.js.map +1 -0
  33. package/dist/client.d.ts +109 -2
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +67 -2
  36. package/dist/client.js.map +1 -1
  37. package/dist/compat.d.ts +1 -1
  38. package/dist/compat.js +2 -2
  39. package/dist/components/board.d.ts +8 -0
  40. package/dist/components/board.d.ts.map +1 -1
  41. package/dist/components/board.js +59 -15
  42. package/dist/components/board.js.map +1 -1
  43. package/dist/components/event-feed.d.ts +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +33 -1
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/markdown.d.ts.map +1 -1
  48. package/dist/components/markdown.js +6 -3
  49. package/dist/components/markdown.js.map +1 -1
  50. package/dist/components/pack-connect.d.ts +3 -1
  51. package/dist/components/pack-connect.d.ts.map +1 -1
  52. package/dist/components/pack-connect.js +4 -4
  53. package/dist/components/pack-connect.js.map +1 -1
  54. package/dist/components/pack-token.d.ts +3 -1
  55. package/dist/components/pack-token.d.ts.map +1 -1
  56. package/dist/components/pack-token.js +1 -1
  57. package/dist/components/pack-token.js.map +1 -1
  58. package/dist/components/people.d.ts.map +1 -1
  59. package/dist/components/people.js +14 -3
  60. package/dist/components/people.js.map +1 -1
  61. package/dist/components/plugin-frame.d.ts +52 -0
  62. package/dist/components/plugin-frame.d.ts.map +1 -0
  63. package/dist/components/plugin-frame.js +314 -0
  64. package/dist/components/plugin-frame.js.map +1 -0
  65. package/dist/components/plugin-settings.d.ts +38 -0
  66. package/dist/components/plugin-settings.d.ts.map +1 -0
  67. package/dist/components/plugin-settings.js +348 -0
  68. package/dist/components/plugin-settings.js.map +1 -0
  69. package/dist/components/remote-pictures.d.ts +6 -0
  70. package/dist/components/remote-pictures.d.ts.map +1 -0
  71. package/dist/components/remote-pictures.js +8 -0
  72. package/dist/components/remote-pictures.js.map +1 -0
  73. package/dist/components/shell.d.ts +2 -0
  74. package/dist/components/shell.d.ts.map +1 -1
  75. package/dist/components/shell.js +26 -1
  76. package/dist/components/shell.js.map +1 -1
  77. package/dist/components/sign-in.d.ts.map +1 -1
  78. package/dist/components/sign-in.js +49 -2
  79. package/dist/components/sign-in.js.map +1 -1
  80. package/dist/components/ui.d.ts +3 -2
  81. package/dist/components/ui.d.ts.map +1 -1
  82. package/dist/components/ui.js +6 -3
  83. package/dist/components/ui.js.map +1 -1
  84. package/dist/components/workspace.d.ts +7 -1
  85. package/dist/components/workspace.d.ts.map +1 -1
  86. package/dist/components/workspace.js +1 -1
  87. package/dist/components/workspace.js.map +1 -1
  88. package/dist/console.css +384 -5
  89. package/dist/console.d.ts.map +1 -1
  90. package/dist/console.js +146 -29
  91. package/dist/console.js.map +1 -1
  92. package/dist/data.d.ts +11 -2
  93. package/dist/data.d.ts.map +1 -1
  94. package/dist/data.js +35 -21
  95. package/dist/data.js.map +1 -1
  96. package/dist/i18n/en.d.ts +163 -1
  97. package/dist/i18n/en.d.ts.map +1 -1
  98. package/dist/i18n/en.js +174 -1
  99. package/dist/i18n/en.js.map +1 -1
  100. package/dist/i18n/ko.d.ts.map +1 -1
  101. package/dist/i18n/ko.js +154 -1
  102. package/dist/i18n/ko.js.map +1 -1
  103. package/dist/index.d.ts +3 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +3 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/limits.d.ts +14 -0
  108. package/dist/limits.d.ts.map +1 -1
  109. package/dist/limits.js +14 -0
  110. package/dist/limits.js.map +1 -1
  111. package/dist/plugin/args.d.ts +148 -0
  112. package/dist/plugin/args.d.ts.map +1 -0
  113. package/dist/plugin/args.js +369 -0
  114. package/dist/plugin/args.js.map +1 -0
  115. package/dist/plugin/bridge.d.ts +146 -0
  116. package/dist/plugin/bridge.d.ts.map +1 -0
  117. package/dist/plugin/bridge.js +125 -0
  118. package/dist/plugin/bridge.js.map +1 -0
  119. package/dist/plugin/capabilities.d.ts +20 -0
  120. package/dist/plugin/capabilities.d.ts.map +1 -0
  121. package/dist/plugin/capabilities.js +133 -0
  122. package/dist/plugin/capabilities.js.map +1 -0
  123. package/dist/plugin/context.d.ts +374 -0
  124. package/dist/plugin/context.d.ts.map +1 -0
  125. package/dist/plugin/context.js +2 -0
  126. package/dist/plugin/context.js.map +1 -0
  127. package/dist/plugin/define.d.ts +208 -0
  128. package/dist/plugin/define.d.ts.map +1 -0
  129. package/dist/plugin/define.js +482 -0
  130. package/dist/plugin/define.js.map +1 -0
  131. package/dist/plugin/definition-error.d.ts +11 -0
  132. package/dist/plugin/definition-error.d.ts.map +1 -0
  133. package/dist/plugin/definition-error.js +17 -0
  134. package/dist/plugin/definition-error.js.map +1 -0
  135. package/dist/plugin/limits.d.ts +67 -0
  136. package/dist/plugin/limits.d.ts.map +1 -0
  137. package/dist/plugin/limits.js +70 -0
  138. package/dist/plugin/limits.js.map +1 -0
  139. package/dist/plugin/load-worker.d.ts +7 -0
  140. package/dist/plugin/load-worker.d.ts.map +1 -0
  141. package/dist/plugin/load-worker.js +41 -0
  142. package/dist/plugin/load-worker.js.map +1 -0
  143. package/dist/plugin/load.d.ts +18 -0
  144. package/dist/plugin/load.d.ts.map +1 -0
  145. package/dist/plugin/load.js +62 -0
  146. package/dist/plugin/load.js.map +1 -0
  147. package/dist/plugin/manifest.d.ts +237 -0
  148. package/dist/plugin/manifest.d.ts.map +1 -0
  149. package/dist/plugin/manifest.js +411 -0
  150. package/dist/plugin/manifest.js.map +1 -0
  151. package/dist/plugin/refusals.d.ts +49 -0
  152. package/dist/plugin/refusals.d.ts.map +1 -0
  153. package/dist/plugin/refusals.js +79 -0
  154. package/dist/plugin/refusals.js.map +1 -0
  155. package/dist/plugin/rest-schemas.d.ts +556 -0
  156. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  157. package/dist/plugin/rest-schemas.js +262 -0
  158. package/dist/plugin/rest-schemas.js.map +1 -0
  159. package/dist/plugin/sandbox.d.ts +23 -0
  160. package/dist/plugin/sandbox.d.ts.map +1 -0
  161. package/dist/plugin/sandbox.js +99 -0
  162. package/dist/plugin/sandbox.js.map +1 -0
  163. package/dist/plugin/testing.d.ts +45 -0
  164. package/dist/plugin/testing.d.ts.map +1 -0
  165. package/dist/plugin/testing.js +420 -0
  166. package/dist/plugin/testing.js.map +1 -0
  167. package/dist/plugin/web-react.d.ts +11 -0
  168. package/dist/plugin/web-react.d.ts.map +1 -0
  169. package/dist/plugin/web-react.js +34 -0
  170. package/dist/plugin/web-react.js.map +1 -0
  171. package/dist/plugin/web.d.ts +95 -0
  172. package/dist/plugin/web.d.ts.map +1 -0
  173. package/dist/plugin/web.js +245 -0
  174. package/dist/plugin/web.js.map +1 -0
  175. package/dist/plugin-nav.d.ts +29 -0
  176. package/dist/plugin-nav.d.ts.map +1 -0
  177. package/dist/plugin-nav.js +92 -0
  178. package/dist/plugin-nav.js.map +1 -0
  179. package/dist/plugin-testing.d.ts +2 -0
  180. package/dist/plugin-testing.d.ts.map +1 -0
  181. package/dist/plugin-testing.js +4 -0
  182. package/dist/plugin-testing.js.map +1 -0
  183. package/dist/plugin-web-react.d.ts +3 -0
  184. package/dist/plugin-web-react.d.ts.map +1 -0
  185. package/dist/plugin-web-react.js +3 -0
  186. package/dist/plugin-web-react.js.map +1 -0
  187. package/dist/plugin-web.d.ts +4 -0
  188. package/dist/plugin-web.d.ts.map +1 -0
  189. package/dist/plugin-web.js +5 -0
  190. package/dist/plugin-web.js.map +1 -0
  191. package/dist/plugin.d.ts +11 -0
  192. package/dist/plugin.d.ts.map +1 -0
  193. package/dist/plugin.js +15 -0
  194. package/dist/plugin.js.map +1 -0
  195. package/dist/router.d.ts +28 -0
  196. package/dist/router.d.ts.map +1 -1
  197. package/dist/router.js +47 -0
  198. package/dist/router.js.map +1 -1
  199. package/dist/routes.d.ts +6 -0
  200. package/dist/routes.d.ts.map +1 -1
  201. package/dist/routes.js +8 -0
  202. package/dist/routes.js.map +1 -1
  203. package/dist/schemas.d.ts +147 -1
  204. package/dist/schemas.d.ts.map +1 -1
  205. package/dist/schemas.js +55 -5
  206. package/dist/schemas.js.map +1 -1
  207. package/dist/tokens.d.ts +2 -0
  208. package/dist/tokens.d.ts.map +1 -0
  209. package/dist/tokens.js +101 -0
  210. package/dist/tokens.js.map +1 -0
  211. package/dist/vocabulary.d.ts +11 -1
  212. package/dist/vocabulary.d.ts.map +1 -1
  213. package/dist/vocabulary.js +24 -0
  214. package/dist/vocabulary.js.map +1 -1
  215. package/dist/web.d.ts.map +1 -1
  216. package/dist/web.js +5 -2
  217. package/dist/web.js.map +1 -1
  218. package/docs/specs/cli.md +16 -9
  219. package/docs/specs/plugins.md +487 -0
  220. package/docs/specs/rest.md +25 -13
  221. package/package.json +21 -3
  222. package/skills/building-a-console/SKILL.md +25 -26
  223. package/skills/building-a-console/references/configuring.md +40 -0
  224. package/skills/building-a-console/references/the-default-console.md +1 -1
  225. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  226. package/skills/building-a-plugin/SKILL.md +137 -0
  227. package/skills/deploying-a-console/SKILL.md +24 -26
  228. package/skills/deploying-a-console/references/access.md +13 -9
  229. package/skills/deploying-a-console/references/resources.md +25 -10
  230. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  231. package/skills/deploying-a-console/references/sizing.md +2 -0
  232. package/skills/planning-the-board/SKILL.md +96 -0
  233. package/skills/planning-the-board/references/a-task.md +36 -0
  234. package/skills/planning-the-board/references/setting-up.md +44 -0
  235. package/skills/working-the-board/SKILL.md +43 -34
  236. package/skills/working-the-board/references/the-pack.md +19 -0
  237. package/src/api.ts +16 -0
  238. package/src/cli/cli.ts +736 -45
  239. package/src/cli/format.ts +356 -9
  240. package/src/cli/help.ts +71 -6
  241. package/src/cli/plugin-check.ts +470 -0
  242. package/src/cli/plugin-cli.ts +148 -0
  243. package/src/cli/plugin-init.ts +90 -0
  244. package/src/cli/plugin-main.ts +30 -0
  245. package/src/client.ts +324 -1
  246. package/src/compat.ts +2 -2
  247. package/src/components/board.tsx +62 -14
  248. package/src/components/event-feed.tsx +42 -1
  249. package/src/components/markdown.tsx +6 -3
  250. package/src/components/pack-connect.tsx +10 -7
  251. package/src/components/pack-token.tsx +4 -1
  252. package/src/components/people.tsx +17 -7
  253. package/src/components/plugin-frame.tsx +437 -0
  254. package/src/components/plugin-settings.tsx +1111 -0
  255. package/src/components/remote-pictures.ts +19 -0
  256. package/src/components/shell.tsx +82 -14
  257. package/src/components/sign-in.tsx +114 -13
  258. package/src/components/ui.tsx +6 -3
  259. package/src/components/workspace.tsx +18 -0
  260. package/src/console.tsx +309 -23
  261. package/src/data.ts +47 -22
  262. package/src/i18n/en.ts +183 -1
  263. package/src/i18n/ko.ts +159 -1
  264. package/src/index.ts +12 -0
  265. package/src/limits.ts +14 -0
  266. package/src/plugin/args.ts +526 -0
  267. package/src/plugin/bridge.ts +314 -0
  268. package/src/plugin/capabilities.ts +150 -0
  269. package/src/plugin/context.ts +416 -0
  270. package/src/plugin/define.ts +792 -0
  271. package/src/plugin/definition-error.ts +19 -0
  272. package/src/plugin/limits.ts +70 -0
  273. package/src/plugin/load-worker.ts +50 -0
  274. package/src/plugin/load.ts +82 -0
  275. package/src/plugin/manifest.ts +561 -0
  276. package/src/plugin/refusals.ts +94 -0
  277. package/src/plugin/rest-schemas.ts +354 -0
  278. package/src/plugin/sandbox.ts +123 -0
  279. package/src/plugin/testing.ts +612 -0
  280. package/src/plugin/web-react.ts +45 -0
  281. package/src/plugin/web.ts +403 -0
  282. package/src/plugin-nav.ts +132 -0
  283. package/src/plugin-testing.ts +10 -0
  284. package/src/plugin-web-react.ts +4 -0
  285. package/src/plugin-web.ts +35 -0
  286. package/src/plugin.ts +126 -0
  287. package/src/router.ts +73 -0
  288. package/src/routes.ts +9 -0
  289. package/src/schemas.ts +60 -4
  290. package/src/styles/console.css +384 -5
  291. package/src/tokens.ts +101 -0
  292. package/src/vocabulary.ts +30 -0
  293. package/src/web.ts +5 -2
  294. package/template/plugin/.gitignore +2 -0
  295. package/template/plugin/README.md +26 -0
  296. package/template/plugin/SKILLCDN.md +13 -0
  297. package/template/plugin/build.mjs +40 -0
  298. package/template/plugin/console-plugin.json +50 -0
  299. package/template/plugin/package.json +27 -0
  300. package/template/plugin/skills/__key__/SKILL.md +23 -0
  301. package/template/plugin/src/check.test.ts +34 -0
  302. package/template/plugin/src/server/index.test.ts +41 -0
  303. package/template/plugin/src/server/index.ts +72 -0
  304. package/template/plugin/src/web/index.css +51 -0
  305. package/template/plugin/src/web/index.ts +115 -0
  306. package/template/plugin/tsconfig.json +17 -0
  307. package/skills/working-the-board/references/setting-up.md +0 -3
  308. /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
@@ -16,6 +16,7 @@ The API is the board as its members see it: the people, the projects, and in eac
16
16
  - The command names its version on every request, as the product token of its `user-agent` (`skillcdn-console/0.1.18 (cli; node 24.11.0; linux)`), and every answer under `/api` says the console's version (`console-version`) and the range of command versions it takes (`console-client-range`, as npm spells one: `>=0.1.18 <0.2.0`) ([ADR-0025](../adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md)). Before 1.0 the range is the console's line, every patch of its major.minor, from the floor the console names where a release raised one: the oldest command whose requests and answers still fit it, raised only by a release that changed what passes between them ([ADR-0038](../adr/0038-the-command-and-the-console-go-together-within-a-line-and-a-release-that-changes-what-passes-between-them-raises-the-floors.md)); from 1.0, the console's major and a minor within one, either way. A command outside the range is refused with `426` before anything else is asked of its request, `client.outdated` or `client.too_new`, the message naming both versions and the range. The command holds the console to a floor of its own the same way, and refuses a console below it itself, as `console.outdated`, in this same form. A request that names no command, a browser's, a script's, is held to nothing.
17
17
  - Request bodies are JSON, at most 256 KiB, and parsed once with the package's input schema. Text is trimmed where it is a line, bounded everywhere (`MAX_TITLE_LENGTH` and the rest of `@skillcdn/console/api`), and refused when it carries control or invisible characters. Links are `https` URLs only.
18
18
  - Errors have `{ "error": { "code": "...", "message": "..." } }`. The code is stable; the message is for a person, in English.
19
+ - The plugins' routes, under `/api/v1/plugins`, are in [plugins.md](plugins.md) with their refusals; every event carries `plugin`, the key of the plugin that caused it, or `null` for the console's own doing.
19
20
 
20
21
  | Status | `code` | When |
21
22
  |---|---|---|
@@ -47,6 +48,8 @@ The API is the board as its members see it: the people, the projects, and in eac
47
48
  | 403 | `auth.forbidden_origin` | A request that changes something on a session did not come from the console's own pages. |
48
49
  | 403 | `auth.forbidden` | A member asked for what only an administrator, or only an owner of the project, may do. |
49
50
  | 403 | `auth.session_required` | A token asked for what only a person signed in may do: the tokens themselves, and the approval of an agent's connection, which makes one. |
51
+ | 403 | `auth.deactivated` | A token the console knows, for a person deactivated: the token is kept, refused on every route while they are out, `GET /api/v1/me` included, and theirs again when they are taken back in ([ADR-0044](../adr/0044-a-token-of-a-person-who-is-out-is-kept-and-refused-with-the-reason-until-they-are-back-in.md)). |
52
+ | 403 | `auth.not_admitted` | A token the console knows, for a person no longer let in: what let them in, the domain or the organization, is no longer named. Kept and refused the same way, and theirs again when they are back in. |
50
53
  | 403 | `run.not_yours` | The run is another person's. |
51
54
  | 404 | `project.not_found` | No project has that key, or it is not the asker's to see: the two are one answer. The key is checked before anything is asked. |
52
55
  | 404 | `task.not_found`, `decision.not_found`, `goal.not_found`, `token.not_found`, `person.not_found`, `run.not_found`, `run.task_not_found`, `member.not_found` | No such id in the project (or in the workspace, for a token, a person); for a token, none of the asker's. An id that is not a UUID is not found either, except a task's number. |
@@ -60,6 +63,11 @@ The API is the board as its members see it: the people, the projects, and in eac
60
63
  | 409 | `member.exists` | The person is listed in the project already. |
61
64
  | 409 | `member.open_project` | The project is open to the workspace, so everyone of it is a member already; only an owner is listed there ([ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md)). |
62
65
  | 409 | `category.key_taken`, `category.in_use`, `category.too_many` | A category of the workspace has that key already; tasks or goals are sorted into the category, so it stays until they are moved; the workspace has as many categories as one may (`MAX_CATEGORIES`). |
66
+ | 409 | `project.too_many`, `task.too_many`, `goal.too_many`, `decision.too_many`, `document.too_many`, `run.too_many` | The workspace holds as many projects as one may (`MAX_PROJECTS_PER_WORKSPACE`); the project as many tasks, goals, decisions or pages (`MAX_TASKS_PER_PROJECT`, `MAX_GOALS_PER_PROJECT`, `MAX_DECISIONS_PER_PROJECT`, `MAX_DOCUMENTS_PER_PROJECT`), those put away counted; the task has been taken as many times as one may (`MAX_RUNS_PER_TASK`). Bounds a runaway script meets and real work does not. |
67
+ | 409 | `project.too_many_bytes` | The files of the project, handed in by runs, put in comments and attached to pages, hold as many bytes together as the deployment allows one project (`PROJECT_FILE_BYTES_AT_MOST`, [deploy/README.md](../../deploy/README.md#a-shared-console)); no bound unless set. Asked before any byte is kept. |
68
+ | 429 | `feed.too_many` | The person holds as many streams of the feed as one may at once (`MAX_STREAMS_PER_PERSON`), or the process does for everyone (`MAX_STREAMS`); `retry-after` says when to try again. |
69
+ | 403 | `picture.refused` | The console fetches no picture from that address: one of its own network, the loopback, a private or a link-local range, or a redirect to one ([ADR-0048](../adr/0048-a-picture-from-elsewhere-is-read-through-the-console-from-a-public-address-and-kept-a-while.md)). |
70
+ | 502 | `picture.unavailable` | The picture could not be fetched from where it is named: the origin answered an error or something that is not a picture, sent more than `MAX_FILE_BYTES`, or could not be reached in time. |
63
71
  | 409 | `decision.answered` | The decision has an answer already. |
64
72
  | 409 | `document.conflict` | The page has moved on since the version the write started from (`baseVersion`), or was written at that path by someone else first. |
65
73
  | 409 | `document.archived` | The page is archived: restore it before writing to it or attaching to it. |
@@ -85,7 +93,7 @@ The API is the board as its members see it: the people, the projects, and in eac
85
93
 
86
94
  ### `GET /api/v1/me`
87
95
 
88
- `{ "workspace": { "name", "tokenDaysAtMost", "skills": { "source", "address", "packToken" }, "peopleVisibility" }, "person": ... | null, "agent": "..." | null, "language": "..." | null, "signIn": [{ "key", "label" }, ...], "console": { "version", "clientRange" } }`: what the board is called, the most days a token may be good for here (or `null` where a person chooses), where the organization's skills come from (`source`, the origin of the SkillCDN deployment they are read through, and `address`, the organization's, canonical, which a project without one of its own reads, or `null`; and `packToken`, when the SkillCDN token the organization's pack is read with was set, `{ setAt }`, or `null`), who sees whom on the People page (`peopleVisibility`: `everyone`, or `shared_projects`, where a member sees only the people they share a project with and an administrator everyone; [ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md); read as optional, for a console from before the setting), the day the calendars begin a week on and this week is counted by (`weekStart`: `auto`, as the language of the pages has it, or `monday`, `sunday`; [ADR-0039](../adr/0039-this-week-is-the-plans-unit-and-the-week-begins-as-the-workspace-says.md)) and the workspace's time zone (`timeZone`: an IANA name, in which the command prints moments, or `null` for none; both optional the same way), who the session cookie or the token says is asking (`id`, `login`, `name` or `null`, `avatar` or `null`, `role`), with what (`agent`: the name of the token when a token asks, as its person called it, so that a console of a person's own knows it holds one and offers nothing a token cannot do; `null` on a session), the language the person chose for the pages (`language`, a tag, or `null` for the browser's), and the identity providers people sign in through (`gh`, `google`, each with the label of its button), none where nobody can; and the console's own version with the range of command versions it takes (`console`, [ADR-0025](../adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md)), for a console built on the package to compare itself with. Nobody is an answer, not an error; the pages ask this once to decide what to show, and the command once, at `login`, to see that the console knows the token.
96
+ `{ "workspace": { "name", "tokenDaysAtMost", "skills": { "source", "address", "packToken" }, "peopleVisibility" }, "person": ... | null, "agent": "..." | null, "language": "..." | null, "signIn": [{ "key", "label", "admits": { "organization", "domains" } }, ...], "console": { "version", "clientRange" } }`: what the board is called, the most days a token may be good for here (or `null` where a person chooses), where the organization's skills come from (`source`, the origin of the SkillCDN deployment they are read through, and `address`, the organization's, canonical, which a project without one of its own reads, or `null`; and `packToken`, when the SkillCDN token the organization's pack is read with was set, `{ setAt }`, or `null`), who sees whom on the People page (`peopleVisibility`: `everyone`, or `shared_projects`, where a member sees only the people they share a project with and an administrator everyone; [ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md); read as optional, for a console from before the setting), the day the calendars begin a week on and this week is counted by (`weekStart`: `auto`, as the language of the pages has it, or `monday`, `sunday`; [ADR-0039](../adr/0039-this-week-is-the-plans-unit-and-the-week-begins-as-the-workspace-says.md)) and the workspace's time zone (`timeZone`: an IANA name, in which the command prints moments, or `null` for none; both optional the same way), who the session cookie or the token says is asking (`id`, `login`, `name` or `null`, `avatar` or `null`, `role`), with what (`agent`: the name of the token when a token asks, as its person called it, so that a console of a person's own knows it holds one and offers nothing a token cannot do; `null` on a session), the language the person chose for the pages (`language`, a tag, or `null` for the browser's), and the identity providers people sign in through (`gh`, `google`, each with the label of its button and, in `admits`, who gets in through it: the organization at the git host whose members are in, or `null`, and the Workspace domains whose accounts are, which a sign-in page says under the button; answered from 0.4.1 and read as optional), none where nobody can; and the console's own version with the range of command versions it takes (`console`, [ADR-0025](../adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md)), for a console built on the package to compare itself with. Nobody is an answer, not an error; the pages ask this once to decide what to show, and the command once, at `login`, to see that the console knows the token. A token the console knows whose person is out is refused here as on every route, `auth.deactivated` or `auth.not_admitted`, so that the agent learns why ([ADR-0044](../adr/0044-a-token-of-a-person-who-is-out-is-kept-and-refused-with-the-reason-until-they-are-back-in.md)).
89
97
 
90
98
  ### `GET /api/v1/people?login=`
91
99
 
@@ -105,7 +113,7 @@ Who may sign in, by login ([ADR-0022](../adr/0022-who-is-in-is-the-consoles-own-
105
113
 
106
114
  ### `POST /api/v1/projects`
107
115
 
108
- Takes `key` and `name`, and optionally `description`, `visibility` (default `private`), `skillsAddress` (checked as the standard spells one, kept canonical; `project.invalid_address`), `color` and `symbol`. For whoever is signed in, or their agent with their token; they become the project's owner. Answers `201` with the project and writes a `project.created` event in it, with `key` and `name`. A key taken is `project.key_taken`.
116
+ Takes `key` and `name`, and optionally `description`, `visibility` (default `private`), `skillsAddress` (checked as the standard spells one, kept canonical; `project.invalid_address`), `color` and `symbol`. For an administrator, signed in or through their agent with their token ([ADR-0045](../adr/0045-a-project-is-made-by-an-administrator.md)); they become the project's owner, and list another as owner from there. A member is refused with `auth.forbidden`. Answers `201` with the project and writes a `project.created` event in it, with `key` and `name`. A key taken is `project.key_taken`.
109
117
 
110
118
  ### `GET /api/v1/events?after=&limit=`
111
119
 
@@ -123,9 +131,9 @@ The workspace's settings, which an administrator keeps on the Settings page ([AD
123
131
 
124
132
  The SkillCDN token the organization's pack is read with ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an administrator, signed in from the console's own pages or through their agent's token: every project that names no address of its own and holds no token of its own reads the organization's pack (`SKILLS_ADDRESS`) with it. `PUT` takes `{ "token" }` as the project's route does, tries it against the organization's pack once (`workspace.pack_token_refused` when the pack is not served to it or the console names no address for the organization's skills; `skills.unavailable` when the deployment could not be reached), seals it with the console's secret (`workspace.pack_token_unsupported` where the console has none) and keeps it on the workspace, answering the workspace as `me` carries it, with `skills.packToken: { setAt }`, and writing a `workspace.updated` event naming the field `packToken`. The token is answered nowhere and logged nowhere. `DELETE` takes it away the same way; taking away what is not there writes nothing.
125
133
 
126
- ### `POST /api/v1/workspace/pack-token/connect`, `GET .../pack-token/connect` and `DELETE .../pack-token/connect`
134
+ ### `POST /api/v1/workspace/pack-token/connect`, `GET .../pack-token/connect`, `POST .../pack-token/connect/claim` and `DELETE .../pack-token/connect`
127
135
 
128
- The workspace's token connected with a code a person approves ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an administrator, as a project's is: `POST` asks the deployment to connect the organization's address and answers `201` with `{ "code", "url", "expiresAt", "intervalSeconds" }`, with `workspace.pack_connect_unsupported`, `workspace.pack_connect_refused` and `skills.unavailable` where a project's route has its own, the refusal naming a console with no address for the organization's skills; `GET` answers `{ "status": "none" }`, `pending` with what the person is shown, `connected` with `workspace` as `me` carries it once the token is kept, or `ended` with `reason`; `DELETE` gives the request up. One request for the workspace at a time.
136
+ The workspace's token connected with a code a person approves ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an administrator, as a project's is: `POST .../connect` asks the deployment to connect the organization's address and answers `201` with `{ "code", "url", "expiresAt", "intervalSeconds" }`, with `workspace.pack_connect_unsupported`, `workspace.pack_connect_refused` and `skills.unavailable` where a project's route has its own, the refusal naming a console with no address for the organization's skills; `GET` reads how it stands and changes nothing: `{ "status": "none" }`, `pending` with what the person is shown, or `ended` with `reason` `expired`; `POST .../connect/claim` asks the deployment whether the person approved ([ADR-0046](../adr/0046-the-claim-of-a-pack-token-is-a-request-of-its-own-and-reading-how-the-connection-stands-changes-nothing.md)) and answers `pending` still, `connected` with `workspace` as `me` carries it once the token is kept, or `ended` with `reason`; `DELETE` gives the request up. One request for the workspace at a time.
129
137
 
130
138
  ### `GET /api/v1/categories`
131
139
 
@@ -137,7 +145,7 @@ Kept by an administrator, signed in or through their agent's token. `POST` takes
137
145
 
138
146
  ### `GET /api/v1/rules`, `PUT /api/v1/rules`, `POST /api/v1/rules/archive` and `GET /api/v1/rules/versions`
139
147
 
140
- The rules of the board ([ADR-0023](../adr/0023-the-rules-of-a-board-are-pages-the-workspace-and-its-projects-keep-read-first-by-every-agent.md)): what the organization asks of agents, read by whoever is signed in or presents a token, kept by an administrator, signed in or through their agent's token. `GET` answers `{ "source", "body", "page" }`: `source` is `reference` while the organization has written none, and `body` is then the console's own rules as this version of the console carries them, with `page: null`; once written, `source` is `written`, `body` the page's and `page` the page without its body (as a folder lists one: `path` `rules`, `title` `Rules`, `version`, `updatedBy`, `agent`, `createdAt`, `updatedAt`). `PUT` takes `body` (Markdown, `MAX_RULES_LENGTH`) and optionally `baseVersion`, writes the next version of the page, bringing it back when it was put away, and answers what `GET` answers; the console's own rules sent as they are are never written, so that what holds keeps following the console's version: with none written they change nothing, and with the organization's written they put its page away, as `POST .../archive` does; a write from a version the rules have moved on from is `document.conflict`. The write is a `document.written` event in the workspace's own feed, with `path`, `title` and `version`. `POST .../archive` puts the written rules away, so that the console's own hold again (`document.archived`); what is put away already changes nothing. `.../versions` answers `{ "items": [version, ...] }` as a page's versions are answered, newest first, and `{ "items": [] }` while none is written. A body may say `{language}` for the language of the person an agent works for: the API answers the text as written, and the pages and the command fill it (`fillRules`), with the name of the language the person chose (`GET /api/v1/me`, `language`) or with the task's when they chose none.
148
+ The rules of the board ([ADR-0023](../adr/0023-the-rules-of-a-board-are-pages-the-workspace-and-its-projects-keep-read-first-by-every-agent.md)): what the organization asks of agents, read by whoever is signed in or presents a token, kept by an administrator, signed in or through their agent's token. `GET` answers `{ "source", "body", "page" }`: `source` is `reference` while the organization has written none, and `body` is then the console's own rules as this version of the console carries them, with `page: null`; once written, `source` is `written`, `body` the page's and `page` the page without its body (as a folder lists one: `path` `rules`, `title` `Rules`, `version`, `updatedBy`, `agent`, `createdAt`, `updatedAt`). `PUT` takes `body` (Markdown, `MAX_RULES_LENGTH`) and optionally `baseVersion`, writes the next version of the page, bringing it back when it was put away, and answers what `GET` answers; the console's own rules sent as they are are never written, so that what holds keeps following the console's version: with none written they change nothing, and with the organization's written they put its page away, as `POST .../archive` does; a write from a version the rules have moved on from is `document.conflict`. The write is a `document.written` event in the workspace's own feed, with `path`, `title` and `version`. `POST .../archive` puts the written rules away, so that the console's own hold again (`document.archived`); what is put away already changes nothing. `.../versions` answers `{ "items": [version, ...] }` as a page's versions are answered, newest first, and `{ "items": [] }` while none is written. A body may say `{language}` for the language of the person an agent works for: the API answers the text as written, and the pages and the command fill it (`fillRules`), with the name of the language the person chose (`GET /api/v1/me`, `language`) or with the task's when they chose none. The answer also carries `plugins` ([plugins.md](plugins.md), "The command"): every enabled plugin the asker may use, by key, each with its `name`, `agentNote` and `description` by language and the asker's `roles` in it; empty where the console has none.
141
149
 
142
150
  ### `GET /api/v1/work`
143
151
 
@@ -157,7 +165,7 @@ Takes one of the asker's tokens away, whoever holds it, and answers `204`. A tok
157
165
 
158
166
  ### `DELETE /api/v1/me/token`
159
167
 
160
- The token the request is made with gives itself up, so that it is good for nothing from now on, and the answer is `204`: what `console logout` does, so that a token forgotten on a machine does not outlive it. The one thing a token does about the tokens; with a session, which has none to give, it is `request.invalid`.
168
+ The token the request is made with gives itself up, so that it is good for nothing from now on, and the answer is `204`: what `console logout` does, so that a token forgotten on a machine does not outlive it. The one thing a token does about the tokens; with a session, which has none to give, it is `request.invalid`. A token whose person is out is refused here as everywhere ([ADR-0044](../adr/0044-a-token-of-a-person-who-is-out-is-kept-and-refused-with-the-reason-until-they-are-back-in.md)): the console keeps it for when they are back in, and the command forgets it on the machine.
161
169
 
162
170
  ### `GET /api/v1/people/<id>/tokens` and `DELETE /api/v1/people/<id>/tokens/<token id>`
163
171
 
@@ -167,11 +175,15 @@ An administrator's sight of everyone's agents ([ADR-0011](../adr/0011-an-agent-c
167
175
 
168
176
  `{ "tasks": [{ "project", "task" }, ...], "closed": [{ "project", "task" }, ...] }`: what is on another person's plate, for whoever oversees them: an administrator, or an owner of a project the person is in (everyone of the workspace is in a project open to it; a private one holds those listed), signed in or through their agent. The open tasks the person is at work on, the sooner due first and those with no day last, and as `closed` those of them done or dropped and not yet put away, both within the projects the asker may see and no other, each with its project; the decisions are left out, since they wait for anyone. `auth.forbidden` for anyone else; `person.not_found` for an id that is nobody's.
169
177
 
178
+ ### `GET /api/v1/pictures?src=`
179
+
180
+ A picture from elsewhere, for a page that shows one ([ADR-0048](../adr/0048-a-picture-from-elsewhere-is-read-through-the-console-from-a-public-address-and-kept-a-while.md)): the console fetches it and hands it over, so that the person's browser reaches nobody's server. `src` is the https URL that names it, up to `MAX_URL_LENGTH`, on its default port and with no credentials in it; anything else is `request.invalid`. For whoever is signed in, with a session or a token. Fetched from a public address only, the host resolved first: an address of the console's own network, the loopback, a private or a link-local range, is `picture.refused` (`403`), and so is a redirect to one. What comes back must say it is a picture (`image/*`) and be at most `MAX_FILE_BYTES`; the origin's error, its silence or anything else is `picture.unavailable` (`502`). Answered with the picture's bytes and type, an `etag` of the bytes, `cache-control: private, max-age=86400`, so that a browser keeps it a day and asks with `if-none-match` after (`304`), and the headers a file is served with, under a policy that runs nothing. The console keeps what it fetched in memory, within a bounded room, for an hour before it asks the origin again with what the origin said the picture was; keeps serving it for a day when the origin cannot be reached; and keeps a refusal five minutes, so that a dead link costs one try in a while. The pages read every picture from elsewhere through here, a person's picture from the git host included, and load nothing from elsewhere themselves.
181
+
170
182
  ### Connecting an agent
171
183
 
172
184
  `POST /api/v1/connect`, `GET /api/v1/connect/<code>`, `POST /api/v1/connect/<code>/approve`, `POST /api/v1/connect/<code>/deny` and `POST /api/v1/connect/claim`: how an agent gets its token without anyone copying one ([ADR-0011](../adr/0011-an-agent-connects-with-a-short-code-a-person-approves-and-the-token-is-never-shown.md)).
173
185
 
174
- `POST /api/v1/connect` is nobody's: the command sends `{ "agent" }` (what the agent calls itself and where it runs, a line up to `MAX_TOKEN_NAME_LENGTH`) and is answered `201` with `{ "code", "url", "secret", "expiresAt", "interval" }`: a code of eight characters in two groups (`ABCD-EFGH`, from an alphabet without look-alikes) for a person to approve, the console's own page for it, a secret (`cns_c_...`) the command claims with and shows nowhere, when the request expires (thirty minutes after it began), and how many seconds the command waits between claims. A deployment holds a bounded number of requests at once (`connect.too_many`).
186
+ `POST /api/v1/connect` is nobody's: the command sends `{ "agent" }` (what the agent calls itself and where it runs, a line up to `MAX_TOKEN_NAME_LENGTH`) and is answered `201` with `{ "code", "url", "secret", "expiresAt", "interval" }`: a code of eight characters in two groups (`ABCD-EFGH`, from an alphabet without look-alikes) for a person to approve, the console's own page for it, a secret (`cns_c_...`) the command claims with and shows nowhere, when the request expires (thirty minutes after it began), and how many seconds the command waits between claims. A deployment holds a bounded number of requests at once (`connect.too_many`), and a bounded number from one address, as the trusted proxies resolve it: at that bound the oldest request of the address not yet approved makes way for the new one, and its claim is `connect.not_found`; an address whose requests are all approved is `connect.too_many` ([ADR-0047](../adr/0047-a-connection-request-is-bounded-per-address-and-the-oldest-not-yet-approved-makes-way.md)).
175
187
 
176
188
  `GET /api/v1/connect/<code>`, for a person signed in, answers what asks: `{ "code", "agent", "createdAt", "expiresAt", "approved" }`. A code is read as a person types it: any case, with or without the hyphen. `POST .../approve`, from the console's own pages, takes what `POST /api/v1/tokens` takes (`name`, `expiresInDays`) and answers the request, approved; `POST .../deny` takes the request away and answers `204`.
177
189
 
@@ -259,7 +271,7 @@ How the work goes, and what was made. A report takes `body` (Markdown, `MAX_BODY
259
271
 
260
272
  ### `POST .../runs/<id>/files` and `GET /api/v1/projects/<key>/files/<id>`
261
273
 
262
- A file handed in, as the parts of a form (`multipart/form-data`): the part `file` carries the bytes, with the file's name and its media type; the part `label` is optional. The name is one line without a path (`MAX_FILE_NAME_LENGTH`), the file is not empty and at most `MAX_FILE_BYTES` (`request.too_large`), and a media type that is not one, or none, is `application/octet-stream`. The bytes are kept under their SHA-256, so the same file handed in twice is kept once; the artifact on the run is `kind: "file"` with `file` filled in and `url` null. Answers `201` with the run and tells the board (`run.handed_in`, with the label or the file's name). The same rules as for a link: the run's person only, while the run is open, and only so many. A picture among the files is shown on the task's page small, and large when pressed, and a report that names it as `![words](its name)` shows it where the words are.
274
+ A file handed in, as the parts of a form (`multipart/form-data`): the part `file` carries the bytes, with the file's name and its media type; the part `label` is optional. The name is one line without a path (`MAX_FILE_NAME_LENGTH`), the file is not empty and at most `MAX_FILE_BYTES` (`request.too_large`), and a media type that is not one, or none, is `application/octet-stream`. Where the deployment bounds what one project's files hold together (`PROJECT_FILE_BYTES_AT_MOST`), a file that would take the project over it, counted with every file handed in, put in a comment or attached to a page, is `project.too_many_bytes` (`409`) and nothing of it is kept; a comment's files and a page's file are held to the same bound. The bytes are kept under their SHA-256, so the same file handed in twice is kept once; the artifact on the run is `kind: "file"` with `file` filled in and `url` null. Answers `201` with the run and tells the board (`run.handed_in`, with the label or the file's name). The same rules as for a link: the run's person only, while the run is open, and only so many. A picture among the files is shown on the task's page small, and large when pressed, and a report that names it as `![words](its name)` shows it where the words are.
263
275
 
264
276
  The bytes of any file of the project, handed in by a run, put in a comment or attached to a page, are read back at `/api/v1/projects/<key>/files/<file id>`, the project's one address for files, by whoever may see the project, with a session or a token: `content-length` and `content-disposition` name the file, the few kinds a browser may show in place (pictures, plain text, Markdown, CSV, JSON) are answered with their type and `inline`, and every other kind as `application/octet-stream` and `attachment`, never sniffed (`x-content-type-options`) and under a policy that runs nothing (`sandbox`). An id that is a link's, or nothing, or another project's, is `file.not_found`.
265
277
 
@@ -279,13 +291,13 @@ One skill of the pack, a file of it, what a folder holds and what words find, re
279
291
 
280
292
  The pack token ([ADR-0028](../adr/0028-the-console-reads-a-projects-pack-through-the-deployments-rest-api-with-a-pack-token-for-a-private-one.md)), for an owner, signed in from the console's own pages or through their agent's token. `PUT` takes `{ "token" }`, a token a person made at the deployment for the pack's repository (8 to 1024 characters, no whitespace), tries it against the deployment once (`project.pack_token_refused` when the pack is not served to it or the project names no pack; `skills.unavailable` when the deployment could not be reached), seals it with the console's secret (`project.pack_token_unsupported` where the console has none) and keeps it, answering the project with `packToken: { setAt }` and writing a `project.updated` event naming the field `packToken`. The token is answered nowhere and logged nowhere. `DELETE` takes it away the same way; taking away what is not there writes nothing.
281
293
 
282
- ### `POST /api/v1/projects/<key>/pack-token/connect`, `GET .../pack-token/connect` and `DELETE .../pack-token/connect`
294
+ ### `POST /api/v1/projects/<key>/pack-token/connect`, `GET .../pack-token/connect`, `POST .../pack-token/connect/claim` and `DELETE .../pack-token/connect`
283
295
 
284
- A private pack connected with a code a person approves ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an owner. `POST` asks the deployment to connect the project's address (the project's own, or the organization's), as nobody, the console calling itself by the workspace's name with Console after it, once (`Acme Console`, whether the name ends with it or not), and answers `201` with what the person is shown: `{ "code", "url", "expiresAt", "intervalSeconds" }`, the code, the page at the deployment to approve it on, until when, and how often to ask again; the secret the console claims with is kept sealed on a row of the project's and answered never. One request per project at a time: a new one replaces the one under way. `project.pack_connect_unsupported` (`409`) where the console reaches no deployment or keeps no secret to seal with; `project.pack_connect_refused` (`400`) when the project names no pack, the pack is on the console's machine, or the deployment refused to begin, in its words (an address with a ref or a path, a public pack); `skills.unavailable` (`503`) when the deployment could not be reached. `GET` asks how it stands and answers one of `{ "status": "none" }`, nothing asked; `{ "status": "pending", "code", "url", "expiresAt", "intervalSeconds" }`, while the person has not approved; `{ "status": "connected", "project" }`, once, when the token was claimed, sealed and kept as `PUT .../pack-token` keeps one, writing `project.updated` with `packToken`, the project answered as it stands; or `{ "status": "ended", "reason": "expired" | "declined" | "refused" }`, when the code's time ran out, the person declined or the code was spent, or the deployment refused (a repository the person may not open, as many tokens as one may hold), the request gone each time. A deployment that could not be reached is `skills.unavailable`, and the request stands. `DELETE` gives the request up and answers `204`.
296
+ A private pack connected with a code a person approves ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an owner. `POST` asks the deployment to connect the project's address (the project's own, or the organization's), as nobody, the console calling itself by the workspace's name with Console after it, once (`Acme Console`, whether the name ends with it or not), and answers `201` with what the person is shown: `{ "code", "url", "expiresAt", "intervalSeconds" }`, the code, the page at the deployment to approve it on, until when, and how often to ask again; the secret the console claims with is kept sealed on a row of the project's and answered never. One request per project at a time: a new one replaces the one under way. `project.pack_connect_unsupported` (`409`) where the console reaches no deployment or keeps no secret to seal with; `project.pack_connect_refused` (`400`) when the project names no pack, the pack is on the console's machine, or the deployment refused to begin, in its words (an address with a ref or a path, a public pack); `skills.unavailable` (`503`) when the deployment could not be reached. `GET` reads how it stands and changes nothing ([ADR-0046](../adr/0046-the-claim-of-a-pack-token-is-a-request-of-its-own-and-reading-how-the-connection-stands-changes-nothing.md)): `{ "status": "none" }`, nothing asked; `{ "status": "pending", "code", "url", "expiresAt", "intervalSeconds" }`, while the request stands; or `{ "status": "ended", "reason": "expired" }`, once its time ran out. `POST .../pack-token/connect/claim`, from the console's own pages or with a token, asks the deployment whether the person approved, at the interval it named, and answers `pending` still, with the same; `{ "status": "connected", "project" }`, once, when the token was claimed, sealed and kept as `PUT .../pack-token` keeps one, writing `project.updated` with `packToken`, the project answered as it stands; or `{ "status": "ended", "reason": "expired" | "declined" | "refused" }`, when the code's time ran out, the person declined or the code was spent, or the deployment refused (a repository the person may not open, as many tokens as one may hold), the request gone each time. A deployment that could not be reached is `skills.unavailable`, and the request stands. `DELETE` gives the request up and answers `204`.
285
297
 
286
298
  ### `GET /api/v1/projects/<key>/rules`
287
299
 
288
- `{ "workspace": rules, "project": rules }`: the rules that hold in the project ([ADR-0023](../adr/0023-the-rules-of-a-board-are-pages-the-workspace-and-its-projects-keep-read-first-by-every-agent.md)), for whoever may see it, each half shaped as `GET /api/v1/rules` answers: the workspace's, and the project's own, whose `source` is `written`, with its `body` and its `page`, or `none`, with `body` and `page` `null`, when the project adds nothing to the workspace's. A project's rules are the page `rules` of its documents, written and put away through the routes below.
300
+ `{ "workspace": rules, "project": rules, "plugins": [...] }`: the rules that hold in the project ([ADR-0023](../adr/0023-the-rules-of-a-board-are-pages-the-workspace-and-its-projects-keep-read-first-by-every-agent.md)), for whoever may see it, each half shaped as `GET /api/v1/rules` answers: the workspace's, and the project's own, whose `source` is `written`, with its `body` and its `page`, or `none`, with `body` and `page` `null`, when the project adds nothing to the workspace's. A project's rules are the page `rules` of its documents, written and put away through the routes below. `plugins` names the enabled plugins bound to the project, as their board project or on it, that the asker holds a role in, shaped as `GET /api/v1/rules` names them ([plugins.md](plugins.md), "The command").
289
301
 
290
302
  ### The documents
291
303
 
@@ -317,11 +329,11 @@ A file attached to the page, sent and read back exactly as a run hands one in ([
317
329
 
318
330
  ### `GET /api/v1/projects/<key>/events?after=&limit=&task=&run=&decision=&document=&goal=`
319
331
 
320
- `{ "items": [event, ...], "more": boolean }`: what happened in the project after event number `after` (`0` for the beginning), oldest first, at most `limit` (default and maximum `EVENTS_PAGE_LIMIT`); `more` says whether there is more after the last item. `task=<id>`, `run=<id>`, `decision=<id>`, `document=<id>` or `goal=<id>` keeps the events about one of them: how a page shows everything that happened to a task, a run, a decision, a document or a goal. An event carries its `id` (the number), `kind` (one of `EVENT_KINDS`), the `actor` (a person, or, for what an agent did, the person it acts for), `agent` (what the person acted through: the run's agent for what a run did, else the name of the token presented; `null` when a person acted themselves), `projectId`, `taskId`, `decisionId`, `runId`, `documentId` and `goalId` (or `null`), `data` (what a feed shows without asking for the subject: `number`, `title`, `fields`, `from`, `to`, `question`, `option`, `login`, `role`, `key`, `name`, `agent`, `status`, `label`, `excerpt`, `path`, `version`, each only when the kind has it), and `createdAt`.
332
+ `{ "items": [event, ...], "more": boolean }`: what happened in the project after event number `after` (`0` for the beginning), oldest first, at most `limit` (default and maximum `EVENTS_PAGE_LIMIT`); `more` says whether there is more after the last item. With `latest=true`, the newest `limit` events instead, oldest first, and `more` says whether there are earlier ones: how a page shows the feed as it stands without reading it from the beginning; `after` is not looked at then. The workspace's own feed takes `latest` the same way. `task=<id>`, `run=<id>`, `decision=<id>`, `document=<id>` or `goal=<id>` keeps the events about one of them: how a page shows everything that happened to a task, a run, a decision, a document or a goal. An event carries its `id` (the number), `kind` (one of `EVENT_KINDS`), the `actor` (a person, or, for what an agent did, the person it acts for), `agent` (what the person acted through: the run's agent for what a run did, else the name of the token presented; `null` when a person acted themselves), `projectId`, `taskId`, `decisionId`, `runId`, `documentId` and `goalId` (or `null`), `data` (what a feed shows without asking for the subject: `number`, `title`, `fields`, `from`, `to`, `question`, `option`, `login`, `role`, `key`, `name`, `agent`, `status`, `label`, `excerpt`, `path`, `version`, each only when the kind has it), and `createdAt`.
321
333
 
322
334
  ### `GET /api/v1/projects/<key>/events/stream?after=`
323
335
 
324
- The same events as they happen, as server-sent events: one message per event, with `id` the event's number, `event` its kind and `data` the event as JSON; a comment (`: ping`) every 25 seconds of silence. A browser that reconnects sends `Last-Event-ID`, which wins over `after`, so that nothing is missed. The stream ends when the server shuts down; an `EventSource` reconnects on its own.
336
+ The same events as they happen, as server-sent events: one message per event, with `id` the event's number, `event` its kind and `data` the event as JSON; a comment (`: ping`) every 25 seconds of silence. A browser that reconnects sends `Last-Event-ID`, which wins over `after`, so that nothing is missed. The stream ends when the server shuts down, and when the person may no longer read the project, which is asked again before every delivery and every heartbeat: a member taken off a private project, a person deactivated or a token revoked stops reading within a heartbeat. An `EventSource` reconnects on its own, and is answered as any request is. A person holds at most `MAX_STREAMS_PER_PERSON` streams at once, across their tabs, and a process of the console `MAX_STREAMS` for everyone: one more is `feed.too_many` (`429`, `retry-after`), which a browser's `EventSource` does not retry on its own; the page shows the board as it stands and loads it again on the next visit.
325
337
 
326
338
  The feed is written in the same transaction as the change it records, so what a stream carries happened, and what happened is carried; every process of the deployment learns of a change through the database's own channel, so a board with several `api` replicas is one board.
327
339
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillcdn/console",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "What a custom SkillCDN console is built from: the API client and schemas, the components, the composition of the default console, and the console command agents work the board with.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/skillcdn/console#readme",
@@ -20,7 +20,8 @@
20
20
  "type": "module",
21
21
  "sideEffects": false,
22
22
  "bin": {
23
- "console": "./dist/cli/main.js"
23
+ "console": "./dist/cli/main.js",
24
+ "console-plugin": "./dist/cli/plugin-main.js"
24
25
  },
25
26
  "engines": {
26
27
  "node": ">=24"
@@ -42,6 +43,22 @@
42
43
  "types": "./dist/web.d.ts",
43
44
  "default": "./dist/web.js"
44
45
  },
46
+ "./plugin": {
47
+ "types": "./dist/plugin.d.ts",
48
+ "default": "./dist/plugin.js"
49
+ },
50
+ "./plugin/web": {
51
+ "types": "./dist/plugin-web.d.ts",
52
+ "default": "./dist/plugin-web.js"
53
+ },
54
+ "./plugin/web/react": {
55
+ "types": "./dist/plugin-web-react.d.ts",
56
+ "default": "./dist/plugin-web-react.js"
57
+ },
58
+ "./plugin/testing": {
59
+ "types": "./dist/plugin-testing.d.ts",
60
+ "default": "./dist/plugin-testing.js"
61
+ },
45
62
  "./console.css": "./dist/console.css",
46
63
  "./package.json": "./package.json"
47
64
  },
@@ -51,6 +68,7 @@
51
68
  "src",
52
69
  "!src/**/*.test.ts",
53
70
  "!src/**/*.test.tsx",
71
+ "template",
54
72
  "skills",
55
73
  "docs",
56
74
  "SKILLCDN.md"
@@ -68,7 +86,7 @@
68
86
  "vitest": "^5.0.3"
69
87
  },
70
88
  "dependencies": {
71
- "@skillcdn/core": "^0.4.0",
89
+ "@skillcdn/core": "^0.4.1",
72
90
  "react-markdown": "^10.1.0",
73
91
  "remark-gfm": "^4.0.1",
74
92
  "zod": "^4.6.5"
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: building-a-console
3
- description: "Builds a console from the @skillcdn/console package: a console of a person's own, served from their machine with their token by console serve; an organization's console, which takes the default UI's place in the image and, given the organization's website, takes its look, the marks, the colours and the type, by default; or a script against the console's REST API. Says where the package's layers are (the API client and schemas, the components, the composition), how the default console is put together, what to configure and what to replace (a component, the language packs, the brand, the styles' tokens), how a look is read from a website and mapped onto the pages, how a build is run against the board and checked, and the rules that keep untrusted content harmless. Use when an agent is asked to make, change, re-skin or extend a SkillCDN Console UI or a client of its API. To work tasks on the board, use working-the-board; to deploy a console for an organization, use deploying-a-console."
3
+ description: "Builds a console from the @skillcdn/console package: a person's own console, served from their machine by console serve; an organization's console, which takes the default pages' place in the image and, given the organization's website, its look, the marks, the colours and the type; or a script against the console's REST API. Use when a person says 'make me a console with a column for this', 're-skin the console in our brand', 'add a page that shows that', 'build our own console UI', or 'write a script that reads the board'. A feature with data of its own (leave, customers) is a plugin, not a page: use building-a-plugin. To work tasks on the board, use working-the-board; to deploy a console, deploying-a-console. The package's README, types and sources are the reference; this skill says what to configure before anything is replaced, how the build is run against the real board and checked, and what keeps untrusted content harmless. Nothing is spent, pushed or published here."
4
4
  license: MIT
5
5
  compatibility: A shell with Node.js 24 or newer and npm or pnpm, and an agent that reads and writes files. For looking at the pages against a board, the console command signed in by a person. A browser the agent can drive is optional. No key of the agent's own is needed, and it never sees a token.
6
6
  metadata:
7
7
  author: skillcdn
8
- version: "1.7"
8
+ version: "1.9"
9
9
  skillcdn:
10
10
  translations:
11
11
  ko:
12
12
  title: 패키지로 콘솔 만들기
13
- description: "@skillcdn/console 패키지로 콘솔을 만듭니다. console serve로 내 PC에서 내 토큰으로 돌리는 개인 콘솔, 이미지에서 기본 UI 자리를 대신하며 조직의 웹사이트가 있으면 그 디자인(마크, 색, 글꼴)을 기본으로 삼는 조직 콘솔, 또는 콘솔 REST API를 쓰는 스크립트. 패키지의 세 계층(API 클라이언트와 스키마, 컴포넌트, 구성)이 어디 있는지, 기본 콘솔이 어떻게 조립되는지, 무엇을 설정하고 무엇을 바꾸는지(컴포넌트, 언어 팩, 브랜드, 스타일 토큰), 웹사이트에서 디자인을 읽어 페이지에 입히는 법, 빌드를 보드에 붙여 돌리고 확인하는 법, 신뢰할 수 없는 내용을 안전하게 다루는 규칙을 말합니다. SkillCDN 콘솔 UI나 API 클라이언트를 만들거나 바꾸거나 디자인을 바꾸라고 할 때 쓰세요. 보드의 작업을 할 때는 working-the-board를, 조직의 콘솔을 배포할 때는 deploying-a-console을 쓰세요."
13
+ description: "@skillcdn/console 패키지로 콘솔을 만듭니다. console serve로 내 PC에서 내 토큰으로 돌리는 개인 콘솔, 이미지의 기본 페이지 자리를 대신하며 조직 웹사이트가 있으면 그 디자인(마크, 색, 글꼴)을 입는 조직 콘솔, 또는 콘솔 REST API를 쓰는 스크립트. '이 열이 있는 내 콘솔 만들어줘', '콘솔을 우리 브랜드로 바꿔줘', '그걸 보여주는 페이지 추가해줘', '우리 콘솔 UI 만들자', '보드를 읽는 스크립트 써줘'라고 할 때 쓰세요. 자기 데이터를 가진 조직 업무 기능(연차, 고객, 돈)은 페이지가 아니라 플러그인입니다: building-a-plugin을 쓰세요. 보드의 작업은 working-the-board, 콘솔 배포는 deploying-a-console(디자인은 이 스킬로 만듭니다)을 쓰세요. 기준은 패키지의 README·타입·소스이고, 이 스킬은 그것이 어디 있는지, 무엇을 바꾸기 전에 설정으로 할지, 빌드를 실제 보드에 붙여 돌리고 확인하는 법, 신뢰할 수 없는 내용을 안전하게 두는 규칙을 말합니다. 비용은 들지 않고, 여기서는 푸시도 발행도 하지 않습니다."
14
14
  ---
15
15
  # Building a console from the package
16
16
 
@@ -19,7 +19,7 @@ What goes in: what the console is for, which is one person's own, the organizati
19
19
  ## How the person is involved
20
20
 
21
21
  - **Questions:** one message at the start, the ones under "Inputs" that the request leaves open, each for the half it leaves open. Everything else is derived and shown with the plan.
22
- - **Checkpoints:** the plan (the kind, where the build goes, what is configured and what is replaced, the colours and the marks as they will be used), before a file of the console is written; the look, with the pages in front of them, before more pages are built on it. One short message each, with the recommendation; "OK", in the person's language, continues.
22
+ - **Checkpoints:** the plan (the kind, where the build goes, what is configured and what is replaced, the colours and the marks as they will be used, with the contrast checked), before a file of the console is written; the look, with the pages in front of them, before more pages are built on it. One short message each, with the recommendation; "OK", in the person's language, continues.
23
23
  - **Go-ahead:** when the person says to go ahead alone, the look is reported instead of approved; the plan holds in every mode. Nothing is pushed or published here.
24
24
  - **Changes mid-run:** applied from that point; a change after the build rewrites the parts it touches, not the whole console. A phase with nothing to do for the request is skipped, and the line says so.
25
25
 
@@ -28,8 +28,8 @@ What goes in: what the console is for, which is one person's own, the organizati
28
28
  | Need | How | When missing |
29
29
  |---|---|---|
30
30
  | Node.js 24 or newer, with npm or pnpm | `node --version` | Stop and say so. |
31
- | The package | `npm install @skillcdn/console react react-dom`, for pages `npm install --save-dev vite @vitejs/plugin-react typescript`, and for Korean `npm install pretendard`, the face the default console loads | The package is on npm; nothing else is needed. |
32
- | For looking at the pages against a board, one's own or the organization's: the `console` command, signed in | `console whoami` says as whom and at which console. | `npm install -g @skillcdn/console`, then `console login --url <the console's address>`: a person approves the code it shows. Never look for a token. |
31
+ | The package | `npm install @skillcdn/console react react-dom`, at the version the deployment pins for the organization's console (phase 1); for a console with pages `npm install --save-dev vite @vitejs/plugin-react typescript`, and for Korean `npm install pretendard`, the face the default console loads | The package is on npm; nothing else is needed. |
32
+ | For looking at the pages against a board, one's own or the organization's: the `console` command, signed in | `console whoami` says as whom and at which console; `console rules` what the board asks of agents, which holds over this skill's defaults where it speaks of consoles. | `npm install -g @skillcdn/console`, then `console login --url <the console's address>`: a person approves the code it shows. Never look for a token. |
33
33
  | Optional: a browser the agent can drive, or a screenshot tool | Looking at the pages and their console at the look checkpoint and for the verdicts. | The person opens the local address themselves, and the layout, console and policy verdicts are reported as not run. |
34
34
 
35
35
  ## Where to read, in this order
@@ -37,23 +37,22 @@ What goes in: what the console is for, which is one person's own, the organizati
37
37
  Nothing here restates them: they are the contract, current at the version installed.
38
38
 
39
39
  1. **The package's README**: `node_modules/@skillcdn/console/README.md` once installed, [`packages/console/README.md`](/packages/console/README.md) in the console's repository. Every export by layer and what each takes, the styles, the languages, and "A console of your own" with two of the four files to write.
40
- 2. **The types**: `node_modules/@skillcdn/console/dist/index.d.ts`, `api.d.ts`, `cli/cli.d.ts` and `web.d.ts`: the exact props of every component and the shape of every answer, with the comment that says what each is for. `brand` takes a `symbol`, a `wordmark`, or both.
41
- 3. **The sources**, shipped with the package under `src/`: `console.tsx` (how the default console composes the pages from the components, routes, and loads its data), `components/*.tsx` (each component and what it renders), `data.ts` (how the workspace and a project are loaded and kept current by the stream), `styles/console.css` (the tokens, `--sc-*`, and every class, `sc-`-prefixed), `i18n/en.ts` (every word the pages show, keyed as a pack is). The repository's tests (`packages/console/src/components/components.test.tsx`, `src/console.test.tsx`), not shipped in the package, show what each piece renders, for tests of one's own.
42
- 4. **The REST API**: [docs/specs/rest.md](/docs/specs/rest.md), in the package at `docs/specs/rest.md`: every route, what it takes and answers, who may ask, and the codes of its refusals. **The command line**: [docs/specs/cli.md](/docs/specs/cli.md).
40
+ 2. **The types**: `node_modules/@skillcdn/console/dist/index.d.ts`, `api.d.ts`, `cli/cli.d.ts` and `web.d.ts`: the exact props of every component and the shape of every answer, with the comment that says what each is for.
41
+ 3. **The sources**, shipped with the package under `src/`: `console.tsx` (how the default console composes the pages from the components, routes, and loads its data), `components/*.tsx` (each component and what it renders), `data.ts` (how the workspace and a project are loaded and kept current by the stream), `styles/console.css` (the tokens, `--sc-*`, and every class, `sc`-prefixed), `i18n/en.ts` (every word the pages show, keyed as a pack is). The repository's tests (`packages/console/src/components/components.test.tsx`, `src/console.test.tsx`), not shipped in the package, show what each piece renders, for tests of one's own.
42
+ 4. **The REST API**: [docs/specs/rest.md](/docs/specs/rest.md), in the package at `docs/specs/rest.md`: every route, what it takes and answers, who may ask, and the codes of its refusals; for a script. **The command line**: [docs/specs/cli.md](/docs/specs/cli.md), for `console serve`.
43
43
  5. **The default console itself**, `apps/console/web/` in the console's repository: the page, the script, the build and the compiler's settings, with the brand, the fonts and the icons wired in. Its four files are quoted in [the-default-console.md](references/the-default-console.md), kept identical to them by the repository's check, for an agent that has the skill and not the repository; a console of one's own copies their shape.
44
- 6. **The architecture**: [docs/architecture.md](/docs/architecture.md) for how the parts fit, and the [decisions](/docs/adr/README.md), served through the repository's connection and not in the package; ADR-0014 says how a person's own console reaches the organization's.
44
+ 6. **The architecture**: [docs/architecture.md](/docs/architecture.md) for how the parts fit, and the [decisions](/docs/adr/README.md), both documents of the console's repository, read through the connection that served this skill and not in the package; ADR-0014 says how a person's own console reaches the organization's. Not needed for a re-skin.
45
45
 
46
46
  ## Inputs
47
47
 
48
48
  | Input | Source |
49
49
  |---|---|
50
50
  | Which kind | Asked when the request does not say: "Is this console for you alone, served from your machine with your token; for the whole organization, in place of the default pages in the image; or a script against the API?" A member usually wants their own. |
51
- | Where the organization's build goes | Asked for the organization's: "How is the console run: with the deployment repository that deploying-a-console made, with an image you build yourselves, or from a directory mounted into it?" The answer places the result: the source in `console-ui/` of that repository, which its workflow builds on every push; a repository of its own whose build goes beside the Dockerfile of [deploy/README.md](/deploy/README.md) "A custom console in the image"; or the build in the directory. |
52
- | What it must show or do | From the request: a page, a component, a language, a brand, a column, a filter, a report. Asked for its missing half: "What should it show or do that the default console does not?" Everything else is the default console's and stays. |
53
- | The marks | From the organization's website when the request, or deploying-a-console, names one: its symbol and wordmark read from the site as [the-look-from-a-website.md](references/the-look-from-a-website.md) says, the dark version or, with the person's word, the one-colour mark recoloured, shown at the plan. Else asked for a console that shows marks of its own, for the half the request leaves open: "Which picture should the pages show beside the board's name: a small square mark, the name as lettering, or both? Where is the file? The pages are light, so it should be a dark one." `brand` takes either or both. The symbol's slot is a square of one line's height (`.sc-brand-symbol`, 1rem): a wide mark, a lettermark of three or four times its height, is a wordmark and goes in as one, alone or beside a square symbol. A mark drawn in a light colour vanishes on the light pages: ask for a dark version, or, with the person's word, recolour it when it is one colour. Default without a site: the board's name alone and no marks, which is the honest console. |
54
- | The colour | From the website's own colours when one is named, as the reference says, its primary colour read from the theme colour, the stylesheet's properties or the primary button. Else asked when the request names none and marks are wanted: "Which colour, or which mood?" A colour comes as words or a value; the value is the mark's own fill when it has one, else chosen and shown. A dark or saturated colour becomes the accent (`--sc-color-accent` and its family); a pale one tints the field (`--sc-field-gradient`), with a darker shade of it as the accent, since an accent must read on the light field. The mapping is shown at the plan, with the contrast checked. |
55
- | The languages | Asked when the organization's language is not among the packs: "Which languages should the pages speak?" Default: the packs the package ships, chosen by the browser; a Korean organization gets Pretendard loaded, as the default console does. |
56
- | The name | Not asked: the server names the board (`WORKSPACE_NAME`, conventionally the organization or the person it is for, then Console: `Acme Console`), and `title` is only what the page says until the server answers. `index.html` gets the organization's `<title>` and `lang`. |
51
+ | Where the organization's build goes | Asked for the organization's: "How is the console run: with the deployment repository that deploying-a-console made, with an image you build yourselves, or from a directory mounted into it? And where is that repository: a folder on this machine, or its address to clone?" The answer places the result: the source in `console-ui/` of that repository, which its workflow builds on every push, an existing `console-ui/` there being the console to change, not to start over; a repository of its own whose build goes beside the Dockerfile of [deploy/README.md](/deploy/README.md) "A custom console in the image"; or the build in the directory. |
52
+ | What it must show or do | From the request: a page, a component, a language, a brand, a column, a filter, a report. Asked for its missing half, when the request wants more than a look: "What should it show or do that the default console does not?" Everything else is the default console's and stays. |
53
+ | The look: the marks and the colour | From the organization's website when the request, or deploying-a-console, names one: read and mapped as [the-look-from-a-website.md](references/the-look-from-a-website.md) says, and shown at the plan; the pages stay light unless the person says otherwise, a mark is recoloured only with their word, and a site that cannot be read, or shows no mark or colour of its own, is said so, the person asked for the files or the honest default kept. Else asked, for a console that is to show marks of its own, each for the half the request leaves open: "Which picture should the pages show beside the board's name: a small square mark, the name as lettering, or both? Where is the file? The pages are light, so it should be a dark one." and "Which colour, or which mood?" A colour comes as words or a value; the value is the mark's own fill when it has one, else chosen and shown. Default without a site: the board's name alone and no marks, which is the honest console. How a mark, a colour and a face go into the pages is [configuring.md](references/configuring.md). |
54
+ | The languages | Asked when the organization's language, the request's and the site's `lang`, is not among the packs: "Which languages should the pages speak?" Default: the packs the package ships, chosen by the browser; a Korean organization gets Pretendard loaded, as the default console does. |
55
+ | The name | Not asked: the server names the board (`WORKSPACE_NAME`, conventionally the organization or the person it is for, then Console: `Acme Console`), and `title` is only what the page says until the server answers. `index.html` gets the board's name, as `console whoami` prints it, for its `<title>`, and the organization's language for `lang`. |
57
56
  | The console's address | For running one's own, what `console whoami` says. A script takes `CONSOLE_URL` and `CONSOLE_TOKEN` from the environment, never from a file in the repository. |
58
57
 
59
58
  ## Workflow
@@ -62,40 +61,40 @@ Each phase produces something the person can look at and ends with one line to t
62
61
 
63
62
  ### Phase 1: Start from the default console
64
63
 
65
- Produces the **plan** and the first build. Install first, in the new folder, so that the README, the types and the sources are at hand for the plan: the install writes only `package.json`, the lockfile and `node_modules`, and the plan is shown before any file of the console is written. A console is four files and a configuration: `index.html` with a `#root`, `src/main.tsx` that calls `createConsole({ ... }).mount(root)` and imports `@skillcdn/console/console.css`, `vite.config.ts` with the React plugin and, for building against the real board, `/api` sent to `console serve` with `changeOrigin`, and `tsconfig.json` with `jsx`, `moduleResolution: "Bundler"` and the `vite/client` types that make an imported picture a module. The README's "A console of your own" shows two of them, the-default-console.md all four with the marks, the icons and a face wired in. Write the plan (the kind, where the build goes, the files, what is configured, what is replaced, the colours and the marks) and stop for the checkpoint; then write the files and build: `npx vite build` makes `dist/` with its `index.html`. A script instead imports `createClient` from `@skillcdn/console/api`, which brings no React.
64
+ Produces the **plan** and the first build. Install first, in the new folder, or in `console-ui/` of the deployment repository, so that the README, the types and the sources are at hand for the plan: the install writes only `package.json`, the lockfile and `node_modules`, and the plan is shown before any file of the console is written. For the organization's console the package is installed at the version the deployment pins: `CONSOLE_REF` in `deploy/console.env` of the deployment repository is the commit of a release tag `@skillcdn/console@<version>`, and `npm install @skillcdn/console@<version>` takes that one, so that the README, the types and the sources are the deployment's own, and the pages and the server are one release. A console is four files and a configuration: `index.html` with a `#root`, `src/main.tsx` that calls `createConsole({ ... }).mount(root)` and imports `@skillcdn/console/console.css`, `vite.config.ts` with the React plugin and, for building against the real board, `/api` sent to `console serve` with `changeOrigin`, and `tsconfig.json` with `jsx`, `moduleResolution: "Bundler"` and the `vite/client` types that make an imported picture a module. The README's "A console of your own" shows two of them, the-default-console.md all four with the marks, the icons and a face wired in. Write the plan (the kind, where the build goes, the files, what is configured, what is replaced, the colours and the marks) and stop for the checkpoint; then write the files and build: `npx vite build` makes `dist/` with its `index.html`. A script instead imports `createClient` from `@skillcdn/console/api`, which brings no React.
66
65
 
67
66
  ### Phase 2: Configure before replacing
68
67
 
69
- Produces the **configuration**. `createConsole(config)` takes `title`; `brand`, the addresses of a symbol and a wordmark; `languages`, the packs the pages speak, where a language of one's own is `src/i18n/en.ts` copied whole and translated; `components`, the pieces to replace, with the props `ConsoleComponents` names; `pages`, tabs of the console's own, each a `CustomPage` with a key, a label by language, a level (the workspace's pages or every project's) and `render`, given the data, the client, the navigation and the words; `baseUrl`, the API's origin, the page's own when left out; and `client`, for tests. The look is the stylesheet's tokens, `--sc-*` on `:root`, set in a stylesheet of the console's own loaded after `console.css`, and the blocks are styled by their `sc-` classes; the package's README lists the tokens by group. A console with marks of its own imports their files in `main.tsx`, so that the build takes them as assets, and passes their addresses through `brand`; `build: { assetsInlineLimit: 0 }` in `vite.config.ts` keeps every picture and font a file, since the policy allows a small picture inlined as a `data:` address but no `data:` font, and the default console only happens to have none small enough; sets `index.html`'s `<title>`, its `lang` and its icons, the mark as `favicon.svg` linked directly, with an `.ico` and a touch icon when the person has them (the default console links three from its brand package through a small build plugin); and loads the faces its languages need. Most wants are configuration or the styles: re-skinning starts and mostly ends with the tokens of `console.css` (the colours, the glass, the field), overridden by a stylesheet of the console's own loaded after the package's, with the mapping of the plan. Replace a component only when configuration cannot do it: start from its source under `src/components/`, keep its props, and keep or add `sc-` classes. With the look in place, show the pages and stop for the look checkpoint.
68
+ Produces the **configuration**, as [configuring.md](references/configuring.md) lays it out: `createConsole(config)` with its `title`, `brand`, `languages`, `components`, `pages`, `baseUrl` and `client`; the look as the tokens of `console.css`, `--sc-*`, set in a stylesheet of the console's own imported in `main.tsx` after the package's, with the mapping of the plan; the marks imported in `main.tsx` so that the build takes them as assets, with `build: { assetsInlineLimit: 0 }` so that every picture and font stays a file; the page's `<title>`, `lang` and icons; the faces its languages need. Most wants are configuration or the styles: re-skinning starts and mostly ends with the tokens. Replace a component only when configuration cannot do it: start from its source under `src/components/`, keep its props, and keep or add `sc-` classes. With the look in place, serve it as phase 4 says, show the pages, and stop for the look checkpoint.
70
69
 
71
70
  ### Phase 3: Compose pages of one's own
72
71
 
73
- Produces the **pages**. For a page the default console lacks: `useConsoleData(client, projectKey)` answers the workspace and the project the page is on, kept current by the project's stream, with the actions; the components take their data as props and nothing from the network; `matchRoute`, `PATHS`, `projectHref`, `taskHref`, `decisionHref` and `docHref` name the addresses; `Shell` is the header with the tabs and the person's menu. A page as a tab is `pages` in the configuration, its `render` composing the components over what it is given; a composition of one's own is `src/console.tsx` with pages added or taken away. A request without a page of its own skips this phase.
72
+ Produces the **pages**. For a page the default console lacks: `useConsoleData(client, projectKey)` answers the workspace and the project the page is on, kept current by the project's stream, with the actions; the components take their data as props and nothing from the network; `matchRoute`, `PATHS`, `projectHref`, `taskHref`, `decisionHref` and `docHref` name the addresses; `Shell` is the header with the tabs and the person's menu. A page as a tab is `pages` in the configuration, its `render` composing the components over what it is given; a composition of one's own is `src/console.tsx` with pages added or taken away (configuring.md "A page of one's own"). A page composes the board's own data and holds none of its own: a page that would keep records, run logic or reach outside is a feature of the organization's work, which is a plugin, not a page ([building-a-plugin](/skills/building-a-plugin/SKILL.md)); a plugin's pages come in its package and run in frames the console serves, drawn into the tabs by the console itself, so that a console's pages need no build and no change for one. A request without a page of its own skips this phase.
74
73
 
75
74
  ### Phase 4: Run it against the board
76
75
 
77
- Produces the **running console**. `console serve` serves the API at `http://127.0.0.1:11197/api/` while `npx vite` serves the pages, and `console serve dist` serves the build whole at `http://127.0.0.1:11197/`. The pages hold no token; the command carries it, on the loopback only. The pages know they hold a token (`GET /api/v1/me` answers `agent`) and offer nothing a token cannot do: the tokens themselves stay on the organization's console, and everything else, configuring included, the pages do on the token as the person ([ADR-0021](/docs/adr/0021-a-token-configures-as-its-person-does.md)). The organization's console is looked at the same way, on the builder's token: it shows every page a token may see, and the pages a token may not (the agents) are the package's own and unchanged, so the look checkpoint covers everything the console changed. Where the organization's build then goes is the plan's answer: `console-ui/` of the deployment repository, whose workflow builds it into the image on every push, handed over to deploying-a-console, the skill beside this one at the same address (in the package under `node_modules/@skillcdn/console/skills/deploying-a-console/`), with the source and the repository, and with the package pinned at the version of the release the deployment pins as `CONSOLE_REF` (the tag `@skillcdn/console@<version>`), so that the pages and the server are one release and move together; the build beside the Dockerfile of [deploy/README.md](/deploy/README.md) "A custom console in the image", for an image the organization builds itself; or the mounted directory. A script: `createClient({ baseUrl, token })`, every answer parsed with the package's schemas; it configures as its person does too, with `createProject`, `updatePerson`, a project's `update`, `setIcon`, `addMember`, `updateMember` and `removeMember`, `createCategory`, `updateCategory` and `removeCategory`, and the rules of the board with `writeRules` and `archiveRules` and a project's as the page `rules` ([ADR-0023](/docs/adr/0023-the-rules-of-a-board-are-pages-the-workspace-and-its-projects-keep-read-first-by-every-agent.md)), refused with `auth.forbidden` where the person may not, and never the tokens.
76
+ Produces the **running console**. `console serve` serves the API at `http://127.0.0.1:11197/api/` while `npx vite` serves the pages, and `console serve dist` serves the build whole at `http://127.0.0.1:11197/`. The pages hold no token; the command carries it, on the loopback only. The pages know they hold a token (`GET /api/v1/me` answers `agent`) and offer nothing a token cannot do: the tokens themselves stay on the organization's console, and everything else, configuring included, the pages do on the token as the person ([ADR-0021](/docs/adr/0021-a-token-configures-as-its-person-does.md)). The organization's console is looked at the same way, on the builder's token: it shows every page a token may see, and the pages a token may not (the agents) are the package's own, reached by the tokens as every page is, so the look checkpoint covers everything but them, which the delivery names. Where the organization's build then goes is the plan's answer: `console-ui/` of the deployment repository, whose workflow builds it into the image on every push, with the package pinned at the deployment's `CONSOLE_REF` version, so that the pages and the server are one release and move together; the first deploy is deploying-a-console's, the skill beside this one at the same address (in the package under `node_modules/@skillcdn/console/skills/deploying-a-console/`), handed the source and the repository, and a console that is already deployed takes the commit in `console-ui/`, whose push the person makes, since a push there deploys; the build beside the Dockerfile of [deploy/README.md](/deploy/README.md) "A custom console in the image", for an image the organization builds itself; or the mounted directory. A script: `createClient({ baseUrl, token })`, every answer parsed with the package's schemas; it configures as its person does too, with the client's methods for projects, members, categories, people and the rules (configuring.md "A script"), refused with `auth.forbidden` where the person may not, and never the tokens.
78
77
 
79
78
  ### Phase 5: Check and show
80
79
 
81
- Produces the **checked build** and the delivery. `npx tsc --noEmit` and `npx vite build` pass; the pages are looked at, with a browser the agent can drive (the board, a task, a decision, the documents, a narrow window, and the browser's console), else by the person at the local address, with the layout, console and policy verdicts reported as not run; `dist/` holds no token and loads no script, style or font from elsewhere. `git init` and a commit of what was made; the CI job is the organization's own: with the reference deployment, the deployment repository's workflow builds `console-ui/`, else a job that runs the two checks and the build. What was made is shown to the person as a diff or a repository, with the checks that ran and what was not run, before anything is pushed or published.
80
+ Produces the **checked build** and the delivery. `npx tsc --noEmit` and `npx vite build` pass; the pages are looked at, with a browser the agent can drive (the board, a task, a decision, the documents, a narrow window, and the browser's console), else by the person at the local address, with the layout, console and policy verdicts reported as not run; `dist/` holds no token and loads no script, style or font from elsewhere, which a search of the build for addresses outside it and for `data:` fonts shows. A repository of its own gets `git init` and a commit of what was made; in the deployment repository, a commit as its agreement says, and no push, since a push there deploys. The CI job is the organization's own: with the reference deployment, the deployment repository's workflow builds `console-ui/`, else a job that runs the two checks and the build. What was made is shown to the person as a diff or a repository, with the checks that ran and what was not run, before anything is pushed or published.
82
81
 
83
- ### Verdicts
82
+ ## Verdicts
84
83
 
85
84
  | Result | Verdict |
86
85
  |---|---|
87
86
  | `tsc --noEmit` and `vite build` pass; the build, served by `console serve dist` or the image, shows the board, a task, a decision and the documents, with nothing in the browser's console where a browser could look; every word the pages show is in every pack; a narrow window scrolls no sideways | Accepted |
88
- | A layout, console or policy verdict could not run without a browser; a pack of one's own is still a copy of English, marked so; the `.ico` and the touch icon wait for files the person does not have yet | Accepted, with the gap named in the delivery |
87
+ | A layout, console or policy verdict could not run without a browser; a pack of one's own is still a copy of English, marked so; the `.ico` and the touch icon wait for files neither the site nor the person has yet | Accepted, with the gap named in the delivery |
89
88
  | A token in the bundle or in a file of the repository; a script, a style or a font loaded from elsewhere; what people or agents wrote rendered as HTML; the policy refusing something the pages need | Redone |
90
89
 
91
90
  ## Hard rules
92
91
 
93
- - What people and agents wrote is untrusted: shown with the package's `Markdown` (elements, never HTML), never with `dangerouslySetInnerHTML`, never as a script or a style; pictures over https only.
92
+ - What people and agents wrote is untrusted: shown with the package's `Markdown` (elements, never HTML), never with `dangerouslySetInnerHTML`, never as a script or a style; a picture from elsewhere is shown through the console, whose client gives its address (`pictureUrl`), never loaded by the page itself ([ADR-0048](/docs/adr/0048-a-picture-from-elsewhere-is-read-through-the-console-from-a-public-address-and-kept-a-while.md)).
94
93
  - A token never goes into a page, a bundle, a configuration file or a repository: `console serve` carries it, and a script reads it from the environment.
95
- - The policy stays: a build is served under a content security policy that runs nothing inline and loads nothing from elsewhere but pictures over https. Scripts, styles and fonts are bundled, never loaded from a CDN or written inline.
94
+ - The policy stays: a build is served under a content security policy that runs nothing inline and loads nothing from elsewhere, pictures included, which come through the console. Scripts, styles and fonts are bundled, never loaded from a CDN or written inline, a face only under a license that allows bundling it, and a picture of the console's own pages is in the build or reached through `GET /api/v1/pictures`.
96
95
  - The package is pinned by version: the API is `/api/v1`, the schemas in the package are the contract of the version it was built against, and a console is rebuilt to take a new one.
97
96
  - The marks of SkillCDN are trademarks, not part of the MIT license: a console of one's own shows its own marks through `brand`, or none.
98
- - Nothing of the server is rebuilt: a console shows the organization's board and holds nothing of its own. A second database is not a console.
97
+ - Nothing of the server is rebuilt: a console shows the organization's board and holds nothing of its own. A second database is not a console, and a feature with data of its own is a plugin, not a page.
99
98
 
100
99
  ## Terms
101
100
 
@@ -0,0 +1,40 @@
1
+ # Configuring a console
2
+
3
+ What `createConsole` takes, how the look is set, how marks, icons and faces go in, how a language is added, when a component is replaced and how a page of one's own is composed, and what a script against the API uses. The package's README lists every export and every token, and the types under `dist/` are exact; this page says what goes where.
4
+
5
+ ## The configuration
6
+
7
+ `createConsole(config)` returns `{ App, mount }`; `config` takes:
8
+
9
+ | Field | What it is |
10
+ |---|---|
11
+ | `title` | What the page says until the server answers with the board's name (`WORKSPACE_NAME`). |
12
+ | `brand` | The addresses of a `symbol`, a `wordmark`, or both, imported as files so that the build takes them as assets. |
13
+ | `languages` | The packs the pages speak; a language of one's own is `src/i18n/en.ts` copied whole and translated, every key kept. |
14
+ | `components` | The pieces to replace, with the props `ConsoleComponents` names. |
15
+ | `pages` | Tabs of the console's own: each a `CustomPage` with a key, a label by language, a level (the workspace's pages or every project's) and `render`, given the data, the client, the navigation and the words. |
16
+ | `baseUrl` | The API's origin; the page's own when left out. |
17
+ | `client` | A client of one's own, for tests. |
18
+
19
+ ## The look
20
+
21
+ The tokens, `--sc-*` on `:root` in the package's `console.css`, grouped as the README lists them (the colours, the glass, the field, the radii, the type), are set in a stylesheet of the console's own loaded after the package's; the blocks are styled by their `sc-` classes. A dark or saturated colour becomes the accent, `--sc-color-accent` and its family (`-solid`, `-contrast`, `-subtle`); a pale one tints the field, `--sc-field-gradient`, with a darker shade of it as the accent, since an accent must read on the light field. The mapping is shown at the plan, value by value, with the contrast checked at 4.5:1 for text ([the-look-from-a-website.md](the-look-from-a-website.md)). The pages are light by default ([ADR-0040](/docs/adr/0040-the-pages-are-light-by-default.md)); darkening them is the person's word, not the default.
22
+
23
+ ## Marks, icons and faces
24
+
25
+ - The marks go in through `brand`, imported in `main.tsx` as files, so that the build bundles them as assets and gives back their addresses. The symbol's slot is a square of one line's height (`.sc-brand-symbol`, 1rem); a wide mark, three or four times its height, is a wordmark and goes in as one, alone or beside a square symbol. A mark drawn in a light colour vanishes on the light pages: take a dark version, or, with the person's word, recolour a one-colour mark.
26
+ - `build: { assetsInlineLimit: 0 }` in `vite.config.ts` keeps every picture and font a file: Vite inlines an asset under 4 KB as a `data:` address, which the policy allows for a picture and refuses for a font (`font-src 'self'`); the default console gets by only because none of its assets is that small.
27
+ - The page's icons: the mark as `favicon.svg` linked directly in `index.html`, with an `.ico` and a touch icon from the site where it has them, else when the person has them; the default console links three from its brand package through a small build plugin ([the-default-console.md](the-default-console.md)).
28
+ - The faces: a face the organization owns, or one under a license that allows bundling, which the person is asked ("Does your organization own the site's typeface, or is it under a license that allows bundling it?"), in the repository and named in `--sc-font-sans`; otherwise the system faces stay. Pretendard for Korean, `npm install pretendard` and its stylesheet imported in `main.tsx`, as the default console does. No script, style or font from a CDN.
29
+
30
+ ## A component replaced
31
+
32
+ Only when configuration cannot do it. Start from the component's source under `src/components/` of the package, keep its props (the types say them), keep or add `sc-` classes so that the tokens still reach it, and pass it in `components`. A component takes its data as props and nothing from the network; what people and agents wrote goes through the package's `Markdown`, elements and never HTML.
33
+
34
+ ## A page of one's own
35
+
36
+ `useConsoleData(client, projectKey)` answers the workspace and the project the page is on, kept current by the project's stream, with the actions; `matchRoute`, `PATHS`, `projectHref`, `taskHref`, `decisionHref` and `docHref` name the addresses; `Shell` is the header with the tabs and the person's menu. A page as a tab is `pages` in the configuration, its `render` composing the components over what it is given; a composition of one's own is `src/console.tsx` of the package copied, with pages added or taken away. A picture from elsewhere in what people wrote is shown through the console: the composition asks the client for its address (`pictureUrl`), and the pages load nothing from elsewhere themselves ([ADR-0048](/docs/adr/0048-a-picture-from-elsewhere-is-read-through-the-console-from-a-public-address-and-kept-a-while.md)).
37
+
38
+ ## A script
39
+
40
+ `createClient` from `@skillcdn/console/api` brings no React: `createClient({ baseUrl, token })`, the token from the environment (`CONSOLE_URL`, `CONSOLE_TOKEN`) and never from a file in the repository; every answer is parsed with the package's schemas, and a refusal is an `ApiError` with the server's stable code. The client configures as its person does too ([ADR-0021](/docs/adr/0021-a-token-configures-as-its-person-does.md)): `createProject`, `updatePerson`, `createListing` and `removeListing`, `updateWorkspace`, `createCategory`, `updateCategory` and `removeCategory`, the rules with `writeRules` and `archiveRules`, and on a project (`client.project(key)`) `update`, `setIcon`, `addMember`, `updateMember`, `removeMember`, the SkillCDN token a private pack is read with (`pack-token`) and the rules as the page `rules` ([ADR-0023](/docs/adr/0023-the-rules-of-a-board-are-pages-the-workspace-and-its-projects-keep-read-first-by-every-agent.md)); refused with `auth.forbidden` where the person may not, and never the tokens themselves, which stay on the console's own pages.
@@ -137,4 +137,4 @@ export default defineConfig({
137
137
  }
138
138
  ```
139
139
 
140
- What a console of one's own changes. In the page: its `<title>` and `lang`, and its icons, linked directly in the page or through the plugin, for its own. In the script: the `title` it passes; the two `@skillcdn/brand` imports, for its own symbol and wordmark (a dark one, since the pages are light), or none; the face it loads, for its languages (`npm install pretendard` for Korean). In the build: the proxy's target, `http://127.0.0.1:11197` with `changeOrigin: true` for `console serve`; the default console's own proxy points at the `api` role's development port, which a console of one's own does not have, and its plugin reads the icons from the brand package, which a console of one's own replaces with files of its own. In the compiler's settings: `jsx`, `moduleResolution` and the `vite/client` types stay, which make an imported picture a module; `extends` and `references` are the repository's and go, with `"strict": true` of the console's own in their place. One thing to add in the build: `build: { assetsInlineLimit: 0 }`, so that every picture and font stays a file; Vite inlines an asset under 4 KB as a `data:` address, which the policy allows for a picture and refuses for a font (`font-src 'self'`), and the default console gets by only because none of its assets is that small.
140
+ What a console of one's own changes. In the page: its `<title>` and `lang`, and its icons, linked directly in the page or through the plugin, for its own. In the script: the `title` it passes; the two `@skillcdn/brand` imports, for its own symbol and wordmark (a dark one, since the pages are light), or none; the face it loads, for its languages (`npm install pretendard` for Korean). In the build: the proxy's target, `http://127.0.0.1:11197` with `changeOrigin: true` for `console serve`; the default console's own proxy points at the `api` role's development port, which a console of one's own does not have, and its plugin reads the icons from the brand package, which a console of one's own replaces with files of its own. In the compiler's settings: `jsx`, `moduleResolution` and the `vite/client` types stay, which make an imported picture a module; `node` leaves `types` unless `@types/node` is installed; `extends` and `references` are the repository's and go, with `"strict": true` and a `target` (`ES2022` or newer) of the console's own in their place. One thing to add in the build: `build: { assetsInlineLimit: 0 }`, so that every picture and font stays a file; Vite inlines an asset under 4 KB as a `data:` address, which the policy allows for a picture and refuses for a font (`font-src 'self'`), and the default console gets by only because none of its assets is that small.
@@ -4,24 +4,24 @@ An organization with a website has a look already: its marks, its colours, its t
4
4
 
5
5
  ## Reading the site
6
6
 
7
- Fetch the homepage as HTML, with the agent's own tools, and read, in this order:
7
+ Fetch the homepage as HTML, with the agent's own tools (a fetch tool, or `curl` from the shell), and its stylesheets where the head links them, and read, in this order:
8
8
 
9
9
  | What | Where it is | What to take |
10
10
  |---|---|---|
11
11
  | The symbol | `<link rel="icon">` and `apple-touch-icon` in the head; an inline `<svg>` in the header; an `<img>` whose name or `alt` says logo, mark or brand | The square mark, as SVG where there is one, else the largest PNG. It goes in `brand.symbol`, in a slot one line high (`.sc-brand-symbol`). |
12
12
  | The wordmark | The same header: the name as lettering, wide, often an SVG beside or instead of the symbol | `brand.wordmark`, alone or beside the symbol. A mark three or four times wider than high is a wordmark, not a symbol. |
13
- | The colours | `<meta name="theme-color">`; custom properties on `:root` in the site's stylesheet (`--color-primary`, `--brand`, `--accent` and their kin); the fill of the primary button and the colour of links | One primary colour, as a value; a second where the site plainly has two. |
13
+ | The colours | `<meta name="theme-color">`; custom properties on `:root` in the site's stylesheet (`--color-primary`, `--brand`, `--accent` and their kin); the rule that fills the primary button and the one that colours links, read from the stylesheet | One primary colour, as a value; a second where the site plainly has two. |
14
14
  | The type | `font-family` of the body and of the headings, in the stylesheet | The names, to tell the person; a face is loaded only as the rules below allow. |
15
15
  | The mood | The site itself: dark or light, flat or glassy, round or square corners | Whether the field stays light (the pages are light by default, ADR-0040; a dark site is said at the plan, and darkening the pages is the person's word, not the default), and whether the radii follow the site (`--sc-radius-*`). |
16
16
 
17
- A site that cannot be fetched (a login wall, a block, no site after all) ends the reading: say so in one line and ask the person for the files, or keep the honest default.
17
+ A site that cannot be fetched (a login wall, a block, no site after all), or one fetched that shows no mark or no colour of its own, ends the reading: say so in one line and ask the person for the files, or keep the honest default, the board's name alone.
18
18
 
19
19
  ## Mapping onto the pages
20
20
 
21
21
  The pages are light: a pale field, and panes of frosted glass over it. The mapping is shown at the plan as a table, value by value, before a file is written.
22
22
 
23
23
  - **A mark drawn in a light colour vanishes** on the light pages. Take the site's dark or black variant first (its header on a light page, a light-mode asset, a `prefers-color-scheme` rule). Found none, and the mark is one colour: recolour it to the pages' text colour, with the person's word, since the mark is theirs. A mark of several colours on a light fill: keep its fill as a rounded square behind it.
24
- - **A dark or saturated primary colour** becomes the accent: `--sc-color-accent` and its family (`-solid`, `-contrast`, `-subtle`, the point), as the package's README lists them.
24
+ - **A dark or saturated primary colour** becomes the accent: `--sc-color-accent` and its family (`-solid`, `-contrast`, `-subtle`), as the package's README lists them.
25
25
  - **A pale primary colour** tints the field: `--sc-field-gradient`, with a darker shade of it as the accent, since an accent must read on the light field.
26
26
  - **Contrast is checked**, not assumed: the accent on the field, and text on the panes, at the 4.5:1 of WCAG AA for text; a shade is moved until it passes, and the plan says which.
27
27
  - **The type follows the rules of the skill**: scripts, styles and fonts are bundled, never loaded from a CDN or written inline. A face the organization owns, or one under a license that allows bundling, is put in the repository and named in `--sc-font-sans`; otherwise the system faces stay, and the plan says that the site's face was not taken and why. Pretendard stays for Korean, as the default console has it.
@@ -36,4 +36,4 @@ The pages are light: a pale field, and panes of frosted glass over it. The mappi
36
36
 
37
37
  ## Where it goes
38
38
 
39
- For an organization's console deployed with deploying-a-console, the result is the source in `console-ui/` of the deployment repository, depending on `@skillcdn/console` at the console's pinned version, which the workflow builds into the image on every push; the first deploy already carries the look. For a console of one's own, the same configuration in its own repository. Either way the look checkpoint shows the pages, a screenshot where a browser can be driven, before more is built on it.
39
+ For an organization's console deployed with deploying-a-console, the result is the source in `console-ui/` of the deployment repository, depending on `@skillcdn/console` at the console's pinned version, which the workflow builds into the image on every push; the first deploy already carries the look. For a console of one's own, the same configuration in its own repository. Either way the pages are shown before more is built on them, at the plan of deploying-a-console or at the look checkpoint of building-a-console, a screenshot where a browser can be driven.