@skillcdn/console 0.4.3 → 0.4.5

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 (268) hide show
  1. package/README.md +6 -6
  2. package/dist/api.d.ts +4 -4
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +255 -33
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +20 -5
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +98 -26
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +39 -15
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/plugin-check.d.ts +5 -1
  17. package/dist/cli/plugin-check.d.ts.map +1 -1
  18. package/dist/cli/plugin-check.js +23 -4
  19. package/dist/cli/plugin-check.js.map +1 -1
  20. package/dist/cli/plugin-cli.d.ts.map +1 -1
  21. package/dist/cli/plugin-cli.js +10 -1
  22. package/dist/cli/plugin-cli.js.map +1 -1
  23. package/dist/cli/plugin-init.d.ts +2 -0
  24. package/dist/cli/plugin-init.d.ts.map +1 -1
  25. package/dist/cli/plugin-init.js +6 -0
  26. package/dist/cli/plugin-init.js.map +1 -1
  27. package/dist/client.d.ts +32 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +14 -2
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.js +1 -1
  32. package/dist/components/board.d.ts +8 -0
  33. package/dist/components/board.d.ts.map +1 -1
  34. package/dist/components/board.js +49 -18
  35. package/dist/components/board.js.map +1 -1
  36. package/dist/components/decision-form.d.ts +6 -3
  37. package/dist/components/decision-form.d.ts.map +1 -1
  38. package/dist/components/decision-form.js +24 -18
  39. package/dist/components/decision-form.js.map +1 -1
  40. package/dist/components/decision-list.d.ts +5 -1
  41. package/dist/components/decision-list.d.ts.map +1 -1
  42. package/dist/components/decision-list.js +25 -5
  43. package/dist/components/decision-list.js.map +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +34 -8
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/goals.d.ts.map +1 -1
  48. package/dist/components/goals.js +2 -2
  49. package/dist/components/goals.js.map +1 -1
  50. package/dist/components/home.d.ts +35 -0
  51. package/dist/components/home.d.ts.map +1 -0
  52. package/dist/components/home.js +326 -0
  53. package/dist/components/home.js.map +1 -0
  54. package/dist/components/my-decisions.d.ts +20 -3
  55. package/dist/components/my-decisions.d.ts.map +1 -1
  56. package/dist/components/my-decisions.js +14 -1
  57. package/dist/components/my-decisions.js.map +1 -1
  58. package/dist/components/pack-connect.d.ts.map +1 -1
  59. package/dist/components/pack-connect.js +5 -4
  60. package/dist/components/pack-connect.js.map +1 -1
  61. package/dist/components/people.d.ts +45 -1
  62. package/dist/components/people.d.ts.map +1 -1
  63. package/dist/components/people.js +125 -4
  64. package/dist/components/people.js.map +1 -1
  65. package/dist/components/picker.d.ts +8 -0
  66. package/dist/components/picker.d.ts.map +1 -1
  67. package/dist/components/picker.js +2 -2
  68. package/dist/components/picker.js.map +1 -1
  69. package/dist/components/plugin-frame.d.ts +3 -13
  70. package/dist/components/plugin-frame.d.ts.map +1 -1
  71. package/dist/components/plugin-frame.js +75 -21
  72. package/dist/components/plugin-frame.js.map +1 -1
  73. package/dist/components/plugin-settings.d.ts +5 -1
  74. package/dist/components/plugin-settings.d.ts.map +1 -1
  75. package/dist/components/plugin-settings.js +283 -45
  76. package/dist/components/plugin-settings.js.map +1 -1
  77. package/dist/components/projects.d.ts +0 -8
  78. package/dist/components/projects.d.ts.map +1 -1
  79. package/dist/components/projects.js +4 -14
  80. package/dist/components/projects.js.map +1 -1
  81. package/dist/components/searches.d.ts +11 -0
  82. package/dist/components/searches.d.ts.map +1 -0
  83. package/dist/components/searches.js +30 -0
  84. package/dist/components/searches.js.map +1 -0
  85. package/dist/components/task-form.d.ts.map +1 -1
  86. package/dist/components/task-form.js +6 -5
  87. package/dist/components/task-form.js.map +1 -1
  88. package/dist/components/task-view.d.ts.map +1 -1
  89. package/dist/components/task-view.js +2 -1
  90. package/dist/components/task-view.js.map +1 -1
  91. package/dist/components/ui.d.ts +16 -0
  92. package/dist/components/ui.d.ts.map +1 -1
  93. package/dist/components/ui.js +15 -2
  94. package/dist/components/ui.js.map +1 -1
  95. package/dist/connect-page.d.ts.map +1 -1
  96. package/dist/connect-page.js +5 -6
  97. package/dist/connect-page.js.map +1 -1
  98. package/dist/console.css +1123 -258
  99. package/dist/console.d.ts +12 -4
  100. package/dist/console.d.ts.map +1 -1
  101. package/dist/console.js +123 -27
  102. package/dist/console.js.map +1 -1
  103. package/dist/data.d.ts +18 -3
  104. package/dist/data.d.ts.map +1 -1
  105. package/dist/data.js +41 -1
  106. package/dist/data.js.map +1 -1
  107. package/dist/docs-page.d.ts.map +1 -1
  108. package/dist/docs-page.js +6 -5
  109. package/dist/docs-page.js.map +1 -1
  110. package/dist/home-tiles.d.ts +92 -0
  111. package/dist/home-tiles.d.ts.map +1 -0
  112. package/dist/home-tiles.js +294 -0
  113. package/dist/home-tiles.js.map +1 -0
  114. package/dist/i18n/en.d.ts +264 -1
  115. package/dist/i18n/en.d.ts.map +1 -1
  116. package/dist/i18n/en.js +260 -1
  117. package/dist/i18n/en.js.map +1 -1
  118. package/dist/i18n/index.d.ts +4 -2
  119. package/dist/i18n/index.d.ts.map +1 -1
  120. package/dist/i18n/index.js +10 -11
  121. package/dist/i18n/index.js.map +1 -1
  122. package/dist/i18n/ko.d.ts.map +1 -1
  123. package/dist/i18n/ko.js +255 -4
  124. package/dist/i18n/ko.js.map +1 -1
  125. package/dist/index.d.ts +7 -5
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +7 -5
  128. package/dist/index.js.map +1 -1
  129. package/dist/limits.d.ts +12 -0
  130. package/dist/limits.d.ts.map +1 -1
  131. package/dist/limits.js +12 -0
  132. package/dist/limits.js.map +1 -1
  133. package/dist/plugin/args.d.ts +6 -1
  134. package/dist/plugin/args.d.ts.map +1 -1
  135. package/dist/plugin/args.js +16 -11
  136. package/dist/plugin/args.js.map +1 -1
  137. package/dist/plugin/bridge.d.ts +63 -3
  138. package/dist/plugin/bridge.d.ts.map +1 -1
  139. package/dist/plugin/bridge.js +54 -0
  140. package/dist/plugin/bridge.js.map +1 -1
  141. package/dist/plugin/capabilities.d.ts +7 -1
  142. package/dist/plugin/capabilities.d.ts.map +1 -1
  143. package/dist/plugin/capabilities.js +24 -1
  144. package/dist/plugin/capabilities.js.map +1 -1
  145. package/dist/plugin/context.d.ts +174 -12
  146. package/dist/plugin/context.d.ts.map +1 -1
  147. package/dist/plugin/define.d.ts +14 -1
  148. package/dist/plugin/define.d.ts.map +1 -1
  149. package/dist/plugin/define.js +18 -2
  150. package/dist/plugin/define.js.map +1 -1
  151. package/dist/plugin/limits.d.ts +9 -2
  152. package/dist/plugin/limits.d.ts.map +1 -1
  153. package/dist/plugin/limits.js +9 -2
  154. package/dist/plugin/limits.js.map +1 -1
  155. package/dist/plugin/manifest.d.ts +98 -6
  156. package/dist/plugin/manifest.d.ts.map +1 -1
  157. package/dist/plugin/manifest.js +171 -10
  158. package/dist/plugin/manifest.js.map +1 -1
  159. package/dist/plugin/rest-schemas.d.ts +120 -0
  160. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  161. package/dist/plugin/rest-schemas.js +78 -5
  162. package/dist/plugin/rest-schemas.js.map +1 -1
  163. package/dist/plugin/sandbox.d.ts +4 -3
  164. package/dist/plugin/sandbox.d.ts.map +1 -1
  165. package/dist/plugin/sandbox.js +33 -6
  166. package/dist/plugin/sandbox.js.map +1 -1
  167. package/dist/plugin/semver.d.ts +26 -0
  168. package/dist/plugin/semver.d.ts.map +1 -0
  169. package/dist/plugin/semver.js +76 -0
  170. package/dist/plugin/semver.js.map +1 -0
  171. package/dist/plugin/testing.d.ts +15 -1
  172. package/dist/plugin/testing.d.ts.map +1 -1
  173. package/dist/plugin/testing.js +42 -5
  174. package/dist/plugin/testing.js.map +1 -1
  175. package/dist/plugin/web.d.ts +16 -1
  176. package/dist/plugin/web.d.ts.map +1 -1
  177. package/dist/plugin/web.js +44 -1
  178. package/dist/plugin/web.js.map +1 -1
  179. package/dist/plugin.d.ts +3 -2
  180. package/dist/plugin.d.ts.map +1 -1
  181. package/dist/plugin.js +3 -2
  182. package/dist/plugin.js.map +1 -1
  183. package/dist/router.d.ts +19 -0
  184. package/dist/router.d.ts.map +1 -1
  185. package/dist/router.js +18 -0
  186. package/dist/router.js.map +1 -1
  187. package/dist/routes.d.ts +11 -2
  188. package/dist/routes.d.ts.map +1 -1
  189. package/dist/routes.js +14 -1
  190. package/dist/routes.js.map +1 -1
  191. package/dist/rules.d.ts +2 -2
  192. package/dist/rules.js +3 -3
  193. package/dist/schemas.d.ts +864 -2
  194. package/dist/schemas.d.ts.map +1 -1
  195. package/dist/schemas.js +156 -9
  196. package/dist/schemas.js.map +1 -1
  197. package/dist/use-action.d.ts +8 -2
  198. package/dist/use-action.d.ts.map +1 -1
  199. package/dist/use-action.js +13 -6
  200. package/dist/use-action.js.map +1 -1
  201. package/dist/vocabulary.d.ts +3 -1
  202. package/dist/vocabulary.d.ts.map +1 -1
  203. package/dist/vocabulary.js +9 -0
  204. package/dist/vocabulary.js.map +1 -1
  205. package/docs/specs/cli.md +10 -6
  206. package/docs/specs/plugins.md +83 -62
  207. package/docs/specs/rest.md +34 -16
  208. package/package.json +1 -1
  209. package/skills/building-a-plugin/SKILL.md +15 -12
  210. package/skills/planning-the-board/references/setting-up.md +2 -1
  211. package/skills/working-the-board/SKILL.md +3 -3
  212. package/src/api.ts +23 -0
  213. package/src/cli/cli.ts +323 -30
  214. package/src/cli/format.ts +122 -26
  215. package/src/cli/help.ts +43 -14
  216. package/src/cli/plugin-check.ts +41 -2
  217. package/src/cli/plugin-cli.ts +10 -1
  218. package/src/cli/plugin-init.ts +8 -0
  219. package/src/client.ts +78 -0
  220. package/src/compat.ts +1 -1
  221. package/src/components/board.tsx +86 -45
  222. package/src/components/decision-form.tsx +73 -36
  223. package/src/components/decision-list.tsx +45 -13
  224. package/src/components/event-feed.tsx +40 -7
  225. package/src/components/goals.tsx +4 -1
  226. package/src/components/home.tsx +615 -0
  227. package/src/components/my-decisions.tsx +50 -5
  228. package/src/components/pack-connect.tsx +5 -4
  229. package/src/components/people.tsx +500 -61
  230. package/src/components/picker.tsx +11 -2
  231. package/src/components/plugin-frame.tsx +85 -63
  232. package/src/components/plugin-settings.tsx +735 -185
  233. package/src/components/projects.tsx +14 -69
  234. package/src/components/searches.ts +47 -0
  235. package/src/components/task-form.tsx +23 -28
  236. package/src/components/task-view.tsx +7 -0
  237. package/src/components/ui.tsx +42 -2
  238. package/src/connect-page.tsx +5 -6
  239. package/src/console.tsx +303 -64
  240. package/src/data.ts +62 -3
  241. package/src/docs-page.tsx +6 -5
  242. package/src/home-tiles.ts +438 -0
  243. package/src/i18n/en.ts +274 -0
  244. package/src/i18n/index.ts +17 -14
  245. package/src/i18n/ko.ts +267 -4
  246. package/src/index.ts +18 -5
  247. package/src/limits.ts +12 -0
  248. package/src/plugin/args.ts +16 -11
  249. package/src/plugin/bridge.ts +122 -1
  250. package/src/plugin/capabilities.ts +28 -1
  251. package/src/plugin/context.ts +186 -9
  252. package/src/plugin/define.ts +38 -1
  253. package/src/plugin/limits.ts +9 -2
  254. package/src/plugin/manifest.ts +228 -13
  255. package/src/plugin/rest-schemas.ts +89 -6
  256. package/src/plugin/sandbox.ts +36 -8
  257. package/src/plugin/semver.ts +89 -0
  258. package/src/plugin/testing.ts +63 -6
  259. package/src/plugin/web.ts +66 -0
  260. package/src/plugin.ts +19 -0
  261. package/src/router.ts +25 -0
  262. package/src/routes.ts +17 -2
  263. package/src/rules.ts +3 -3
  264. package/src/schemas.ts +197 -10
  265. package/src/styles/console.css +1123 -258
  266. package/src/use-action.ts +16 -7
  267. package/src/vocabulary.ts +10 -0
  268. package/template/plugin/console-plugin.json +3 -1
@@ -15,7 +15,7 @@ The API is the board as its members see it: the people, the projects, and in eac
15
15
  - An agent connects without anyone copying a token ([ADR-0011](../adr/0011-an-agent-connects-with-a-short-code-a-person-approves-and-the-token-is-never-shown.md)): [`POST /api/v1/connect`](#connecting-an-agent) is nobody's, the approval is a person's own doing on the console's own pages, and the claim is the command's, with the secret it was given.
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
- - Errors have `{ "error": { "code": "...", "message": "..." } }`. The code is stable; the message is for a person, in English.
18
+ - Errors have `{ "error": { "code": "...", "message": "..." } }`. The code is stable; the message is in English, for whoever reads the API by hand or through the command. The pages never show it: they read the code in the language pack, `errors`, and a test holds every code the server can answer to having words in every pack, so that a person meets a failure in their own language ([ADR-0012](../adr/0012-the-pages-speak-the-persons-language-from-packs-that-ship-with-the-package.md)).
19
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.
20
20
 
21
21
  | Status | `code` | When |
@@ -40,7 +40,8 @@ The API is the board as its members see it: the people, the projects, and in eac
40
40
  | 400 | `decision.no_such_option` | The answer names an option the decision does not have. |
41
41
  | 400 | `decision.invalid_run` | A decision raised from a run that is not the asker's, or is over. |
42
42
  | 400 | `decision.too_many_files` | The decision carries more files than one may (`MAX_FILES_PER_DECISION`, [ADR-0055](../adr/0055-a-decision-carries-the-files-put-on-it-when-it-is-raised.md)). |
43
- | 400 | `decision.invalid_decider` | The person named to decide may not work in the project ([ADR-0042](../adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md)). |
43
+ | 400 | `decision.too_many_deciders` | More people were named to decide than one decision takes, `MAX_DECIDERS` ([ADR-0057](../adr/0057-a-decision-may-be-addressed-to-several-people-and-the-first-answer-ends-it.md)) |
44
+ | 400 | `decision.invalid_decider` | A person named to decide may not work in the project ([ADR-0042](../adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md)). |
44
45
  | 400 | `token.expiry_at_most` | A token that would not expire, or would outlast `TOKEN_DAYS_AT_MOST`, where the organization requires an expiry. |
45
46
  | 400 | `document.invalid_path` | The path a page is written at is not one: segments of lowercase letters, digits and hyphens, separated by slashes. |
46
47
  | 400 | `run.too_many_reports`, `run.too_many_artifacts` | The run carries as many as one may (`MAX_REPORTS_PER_RUN`, `MAX_ARTIFACTS_PER_RUN`). |
@@ -94,20 +95,29 @@ The API is the board as its members see it: the people, the projects, and in eac
94
95
 
95
96
  ### `GET /api/v1/me`
96
97
 
97
- `{ "workspace": { "name", "tokenDaysAtMost", "skills": { "source", "address", "packToken" }, "peopleVisibility" }, "person": ... | null, "agent": "..." | null, "language": "..." | null, "signIn": [{ "key", "label", "admits": { "organization", "domains" } }, ...], "console": { "version", "clientRange" }, "decisions": { "waiting" } | null }`: 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; and how many decisions wait for the person's answer across the projects they may see (`decisions.waiting`, [ADR-0042](../adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md); `null` for nobody; answered from 0.4.3 and read as optional), which the pages say beside My decisions and read again when a decision is raised or answered. 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)).
98
+ `{ "workspace": { "name", "tokenDaysAtMost", "skills": { "source", "address", "packToken" }, "peopleVisibility" }, "person": ... | null, "agent": "..." | null, "language": "..." | null, "signIn": [{ "key", "label", "admits": { "organization", "domains" } }, ...], "console": { "version", "clientRange" }, "decisions": { "waiting" } | null }`: 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 reads in (`language`, a tag: the one they chose, else the one the pages first saw them in, [ADR-0065](../adr/0065-a-plugin-writes-one-text-and-a-persons-language-is-kept-from-the-first-time-the-pages-see-them.md); `null` until then), their own arrangement of the front page's widgets (`home`, [ADR-0059](../adr/0059-a-person-arranges-the-front-pages-widgets-from-the-sizes-each-comes-in.md): `[{ plugin, widget, size, place? }]` in order, or `null` for the manifests'; answered from 0.4.4 and read as optional), 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; and how many decisions wait for the person's answer across the projects they may see (`decisions.waiting`, [ADR-0042](../adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md); `null` for nobody; answered from 0.4.3 and read as optional), which the pages say beside My decisions and read again when a decision is raised or answered. 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)).
98
99
 
99
100
  ### `GET /api/v1/people?login=`
100
101
 
101
- `{ "items": [person, ...], "listed": [listing, ...] }`: the people the asker may see, those in first and by login, and the logins an administrator let in that nobody has signed in with yet ([ADR-0022](../adr/0022-who-is-in-is-the-consoles-own-record-and-an-administrator-keeps-it.md)). Who sees whom is the workspace's setting, decided by the one policy of the server on every request ([ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md)): with `everyone`, everyone who has signed in; with `shared_projects`, a member sees the active people who are in a project they are in, themselves, and the administrators, while an administrator sees everyone whatever the setting; a project open to the workspace has everyone in it, so a member of one sees everyone. The logins let in (`listed`) and the organization's reading (`organization`) go only to whom the policy shows everyone, `[]` and `null` otherwise. With `login`, one person by their exact login, compared without regard to case, or none: whom the asker sees, and, for an owner of a project, anyone active of the workspace, so that an owner lists someone the list does not show, learning that the login is one of the workspace and nothing more; `listed` is `[]` and `organization` `null` then; an empty or overlong login is `request.invalid`. A person carries their `role` in the workspace (`admin`, who configures it, or `member`, who works in the projects they are in), their `status` (`active`, or `deactivated`: kept with everything they did, refused on every request, and offered to no picker), their `source` (what let them in at their last sign-in: `organization`, the git host's organization the deployment names; `domain`, the Workspace domain it names; or `listed`, an administrator's listing) and `deactivatedAt` (or `null`). A listing carries its `id`, the `host` (`gh`, `google`), the `login` as the administrator spelled it, and `createdAt`. `organization` is the organization at the git host whose members are in, where the deployment names one (`host`, `name`, and `syncedAt`, when the worker last read it whole, or `null` until it has), or `null`. What a picker of workers and of members is made of, the active people. A token reads it as its person does: what the command's `--worker`, `member` and `person` take a login from, through `login`.
102
+ `{ "items": [person, ...], "listed": [listing, ...] }`: the people the asker may see, those in first and by login, and the logins an administrator let in that nobody has signed in with yet ([ADR-0022](../adr/0022-who-is-in-is-the-consoles-own-record-and-an-administrator-keeps-it.md)). Who sees whom is the workspace's setting, decided by the one policy of the server on every request ([ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md)): with `everyone`, everyone who has signed in; with `shared_projects`, a member sees the active people who are in a project they are in, themselves, and the administrators, while an administrator sees everyone whatever the setting; a project open to the workspace has everyone in it, so a member of one sees everyone. The logins let in (`listed`) and the organization's reading (`organization`) go only to whom the policy shows everyone, `[]` and `null` otherwise. With `login`, one person by their exact login, compared without regard to case, or none: whom the asker sees, and, for an owner of a project, anyone active of the workspace, so that an owner lists someone the list does not show, learning that the login is one of the workspace and nothing more; `listed` is `[]` and `organization` `null` then; an empty or overlong login is `request.invalid`. A person carries their `role` in the workspace (`admin`, who configures it, or `member`, who works in the projects they are in), their `status` (`active`, or `deactivated`: kept with everything they did, refused on every request, and offered to no picker), their `source` (what let them in at their last sign-in: `organization`, the git host's organization the deployment names; `domain`, the Workspace domain it names; or `listed`, an administrator's listing), `deactivatedAt` (or `null`) and `managerId`, the id of the person they report to, the reporting line of the directory an administrator keeps, or `null` for nobody (answered from 0.4.4 and read as optional): what a plugin that reads people takes as the default where it needs someone's manager, the approver of their leave, say, in place of a line of its own. A person also carries `language`, the language they read in: the one they chose, else the one the pages first saw them in, or `null` until then (answered from 0.4.4 and read as optional): what a plugin or an agent writes for them in ([ADR-0065](../adr/0065-a-plugin-writes-one-text-and-a-persons-language-is-kept-from-the-first-time-the-pages-see-them.md)). To an administrator, and to each person for themselves, a person carries `email`, their work email, lowercase, or `null` until known ([ADR-0069](../adr/0069-the-directory-keeps-a-work-email-read-by-an-administrator-the-person-and-a-plugin-allowed-to.md)): the address they sign in with at Google, kept at every sign-in, else what an administrator wrote; everyone else reads a person without it, here and wherever a person is named (answered from 0.4.5 and read as optional). A listing carries its `id`, the `host` (`gh`, `google`), the `login` as the administrator spelled it, and `createdAt`. `organization` is the organization at the git host whose members are in, where the deployment names one (`host`, `name`, and `syncedAt`, when the worker last read it whole, or `null` until it has), or `null`. What a picker of workers and of members is made of, the active people. A token reads it as its person does: what the command's `--worker`, `member` and `person` take a login from, through `login`.
102
103
 
103
104
  ### `PATCH /api/v1/people/<id>`
104
105
 
105
- Takes `role`, `status`, or both, and answers the person. For an administrator, signed in or through their agent's token ([ADR-0021](../adr/0021-a-token-configures-as-its-person-does.md)). The board keeps at least one administrator in (`person.last_admin`). A change of role writes `person.role_changed` with `login` and `role`. `status: "deactivated"` takes the person out: refused on every request from the next one, their agents' tokens with them, listed nowhere and offered to no picker, kept with everything they did; the listing they came in on goes with it, so that signing in again admits nobody. `status: "active"` takes them back in, and lists them again where a listing let them in. Each writes `person.deactivated` or `person.reactivated` with `login`; what is already so writes nothing. A person the organization let in is neither (`person.of_organization`): the organization says, on the schedule the worker reads it on.
106
+ Takes `role`, `status`, `managerId`, `email`, one or more, and answers the person, with their `email`. For an administrator, signed in or through their agent's token ([ADR-0021](../adr/0021-a-token-configures-as-its-person-does.md)). The board keeps at least one administrator in (`person.last_admin`). A change of role writes `person.role_changed` with `login` and `role`. `managerId` names whom the person reports to: another active person of the workspace who does not report to them, directly or through others, so that the line never loops, or `null` for nobody (`person.invalid_manager` otherwise); it writes `person.manager_changed` with `login` and `manager`, the manager's login or `null`, which reaches the hooks of a plugin that reads the directory as every people event does ([specs/plugins.md](plugins.md#capabilities)). `email` writes the person's work email, lowercase and one per workspace (`person.email_taken`), or `null` for none ([ADR-0069](../adr/0069-the-directory-keeps-a-work-email-read-by-an-administrator-the-person-and-a-plugin-allowed-to.md)); it writes `person.email_changed` with `login` and `set`, whether an address is set now, and never the address, so that the feed tells nobody what only an administrator, the person and a plugin with `people:read:email` read. A person who signs in at Google has theirs from the sign-in, written again at each: the address Google vouches for is theirs over an administrator's word, and whoever else of the workspace held it gives it up, their `email` becoming `null`, whether an administrator wrote it on them or they are the person of an account Google has since made again under another id, so that the sign-in never fails on it. Each address a sign-in changes, the other person's and the signing-in person's own, writes `person.email_changed` the same way, by nobody; a person's first sign-in writes `person.joined` alone. `status: "deactivated"` takes the person out: refused on every request from the next one, their agents' tokens with them, listed nowhere and offered to no picker, kept with everything they did; the listing they came in on goes with it, so that signing in again admits nobody. `status: "active"` takes them back in, and lists them again where a listing let them in. Each writes `person.deactivated` or `person.reactivated` with `login`; what is already so writes nothing. A person the organization let in is neither (`person.of_organization`): the organization says, on the schedule the worker reads it on.
106
107
 
107
108
  ### `POST /api/v1/listings` and `DELETE /api/v1/listings/<id>`
108
109
 
109
110
  Who may sign in, by login ([ADR-0022](../adr/0022-who-is-in-is-the-consoles-own-record-and-an-administrator-keeps-it.md)): for an administrator, signed in or through their agent's token. `POST` takes `host` (one the deployment signs people in through; `listing.unknown_host` otherwise) and `login` (a login at the git host, or an address at Google, as the host spells one; trimmed, compared without regard to case), answers `201` with the listing, and writes `person.listed` with `login`. Whoever signs in with that account at that host is a person from then on, and the listing is theirs: bound to the account, so that a rename changes nothing and no other account can take the login. A login listed already, or one an active person signs in as, is `listing.exists`; a deactivated person's login may be listed, and they are back in, as listed, when they next sign in. `DELETE` takes a listing away that nobody has signed in with yet, answers `204` and writes `person.unlisted`; one somebody signed in with is `listing.not_found`, since it goes with the person's deactivation.
110
111
 
112
+ ### `GET /api/v1/teams`
113
+
114
+ `{ "items": [team, ...] }`: the teams of the workspace ([ADR-0073](../adr/0073-a-team-is-a-name-and-its-members-a-unit-to-call-people-by-and-to-grant-a-plugins-role-to.md)), by name, each `{ "id", "key", "name", "memberIds", "createdAt" }`, `memberIds` being the members the asker may see, under the same policy as the people. A team is a key, a name and its members, a person in any number of them: a unit to call people by, and to grant a plugin's role to; no head, no team above a team. The key ([ADR-0074](../adr/0074-a-team-is-called-by-a-key-that-never-changes-one-name-among-the-projects-keys.md)) is what the team is called by, in these paths, in a grant and to a plugin: a project key's shape (lowercase letters, digits and hyphens, at most `MAX_TEAM_KEY_LENGTH`, not `new`), one among the teams' keys and the projects' keys of the workspace, and it never changes. The people listing names each person's teams by key, as `teams`.
115
+
116
+ ### `POST /api/v1/teams`, `PATCH /api/v1/teams/<key>`, `DELETE /api/v1/teams/<key>`, `POST /api/v1/teams/<key>/members` and `DELETE /api/v1/teams/<key>/members/<person id>`
117
+
118
+ For an administrator, signed in or through their agent's token. `POST` takes `{ "key", "name" }`: the key one that no team and no project of the workspace has (`team.key_taken`; a project is likewise refused a team's key, `project.key_taken`), the name one per workspace without regard to case (`team.name_taken`), up to `MAX_TEAMS` of them (`team.too_many`), and answers the team with `201`; `PATCH` takes `{ "name" }` and renames it, the key staying; `DELETE` removes it with its memberships and the plugin roles granted to it, and answers `204`. `POST .../members` takes `{ "personId" }`, an active person of the workspace (`team.invalid_member`), and answers the team; `DELETE .../members/<person id>` takes them out (`team.not_a_member` where they were not in) and answers the team. Each writes an event of the workspace's own, with the team's `key` beside its name in `team`: `team.created`, `team.renamed` (`team` the name now, `name` the name before), `team.removed`, `team.member_added` and `team.member_removed` (with `login`). A path whose key no team has is `team.not_found`.
119
+
120
+
111
121
  ### `GET /api/v1/projects`
112
122
 
113
123
  `{ "items": [project, ...] }`: the projects the asker may see, by name. A project carries its `key` (what paths and the command say; lowercase letters, digits and hyphens; immutable), `name`, `description`, `visibility` (`workspace`: everyone of the workspace is a member; `private`: only those listed), `skillsAddress` (canonical, or `null` for the organization's), `packToken` (`{ setAt }` while the project holds a token for a private pack, never the token itself, or `null`, [ADR-0028](../adr/0028-the-console-reads-a-projects-pack-through-the-deployments-rest-api-with-a-pack-token-for-a-private-one.md)), its mark, `color` (one of `COLOR_NAMES`, or `null`) and `symbol` (up to `MAX_PROJECT_SYMBOL_LENGTH` characters with no whitespace, or `null`; where either is `null` the pages choose one from the key and the name, `projectColorOf` and `projectSymbolOf`, so that projects are told apart either way), `icon` (the project's own image for its mark, shown in place of the symbol: its `sha256` and its `contentType`, one of `PROJECT_ICON_TYPES`; or `null`), `role` (what the asker is in it: `owner`, who configures it, or `member`, who works in it), `openDecisions` (the decisions waiting in it) and `openRuns` (the runs open in it: begun and not ended, which says nothing of whether an agent is still at them, [ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)), `createdAt` and `updatedAt`. A workspace administrator sees every project, as its owner; a member sees those open to the workspace, as a member, and those they are listed in, as what they are listed.
@@ -118,11 +128,11 @@ Takes `key` and `name`, and optionally `description`, `visibility` (default `pri
118
128
 
119
129
  ### `GET /api/v1/events?after=&limit=`
120
130
 
121
- The workspace's own events, the ones about no project: who joined (`person.joined`), who was made what (`person.role_changed`), who was let in, taken off the list, taken out and taken back in (`person.listed`, `person.unlisted`, `person.deactivated`, `person.reactivated`, each with the `login`), what became of the categories (`category.created`, `category.updated`, `category.removed`, each with the `key` and the `name`), and which of the workspace's settings an administrator changed (`workspace.updated`, with `fields`). The same shape and parameters as a project's feed below, without the filters. They say who is in, so they go to whom the policy shows everyone ([ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md)): `auth.forbidden` for a member while the people are narrowed to those they share a project with.
131
+ The workspace's own events, the ones about no project: who joined (`person.joined`), who was made what (`person.role_changed`), who was let in, taken off the list, taken out and taken back in (`person.listed`, `person.unlisted`, `person.deactivated`, `person.reactivated`, each with the `login`), whom someone reports to now (`person.manager_changed`, with the `login` and the `manager`'s login, or `null`), what became of the categories (`category.created`, `category.updated`, `category.removed`, each with the `key` and the `name`), and which of the workspace's settings an administrator changed (`workspace.updated`, with `fields`). The same shape and parameters as a project's feed below, without the filters. They say who is in, so they go to whom the policy shows everyone ([ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md)): `auth.forbidden` for a member while the people are narrowed to those they share a project with.
122
132
 
123
133
  ### `PATCH /api/v1/me`
124
134
 
125
- Keeps the person's own choice of language for the pages ([ADR-0012](../adr/0012-the-pages-speak-the-persons-language-from-packs-that-ship-with-the-package.md)): `{ "language": "ko" }`, a language tag as a browser sends one, or `null` to choose none and let the browser's languages decide; answers what `GET` answers. From the console's own pages, or with a token: a console of a person's own keeps the choice on the person too.
135
+ Keeps the language the person reads in ([ADR-0012](../adr/0012-the-pages-speak-the-persons-language-from-packs-that-ship-with-the-package.md), [ADR-0065](../adr/0065-a-plugin-writes-one-text-and-a-persons-language-is-kept-from-the-first-time-the-pages-see-them.md)): `{ "language": "ko" }`, a language tag as a browser sends one, sent by the menu of the pages and by the pages themselves the first time they see a person who has none, where the browser asks for a language they speak; `null` leaves none, which the pages fill again; and their front page ([ADR-0061](../adr/0061-the-front-page-is-one-grid-of-tiles-a-person-arranges-by-hand.md)): `{ "home": { "hidden": [<tile>, ...], "layouts": { "8": [{ "tile", "x", "y", "size" }, ...], ... } } }`, the tiles taken off and what they arranged at each width of 12, 8, 6 or 4 columns, up to `MAX_HOME_TILES` in each, a tile being `project:<id>` or `widget:<plugin>:<widget>`, `x` from 0 to 11, `size` one of `icon`, `small`, `medium`, `large`, `wide`, `full`, or `null` for nothing kept; either or both, `request.invalid` with neither. Answers what `GET` answers. From the console's own pages, or with a token: a console of a person's own keeps the choice on the person too.
126
136
 
127
137
  ### `PATCH /api/v1/workspace`
128
138
 
@@ -146,7 +156,7 @@ Kept by an administrator, signed in or through their agent's token. `POST` takes
146
156
 
147
157
  ### `GET /api/v1/rules`, `PUT /api/v1/rules`, `POST /api/v1/rules/archive` and `GET /api/v1/rules/versions`
148
158
 
149
- 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.
159
+ 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 reads in (`GET /api/v1/me`, `language`) or with the task's while they have 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.
150
160
 
151
161
  ### `GET /api/v1/work`
152
162
 
@@ -154,7 +164,15 @@ The rules of the board ([ADR-0023](../adr/0023-the-rules-of-a-board-are-pages-th
154
164
 
155
165
  ### `GET /api/v1/decisions?open=&raised=&limit=&offset=`
156
166
 
157
- `{ "items": [{ "project": { "key", "name", "color", "symbol", "icon" }, "decision" }, ...], "more": boolean }`: the decisions addressed to the asker ([ADR-0042](../adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md)) across the projects they may see, a page at a time as the tasks are (`limit`, `offset`, `more`): the ones that wait first, the longest waiting first among them, then the answered, newest first, as the record; `open=true` keeps only those that wait, and `raised=true` answers instead the decisions the asker raised, through their agent or themselves, for someone else. Each decision comes with the project it belongs to, as the plate's things do, so that a page says and links it. What a person's own page of decisions reads, in both kinds, and `console decisions --mine` and `--raised`.
167
+ `{ "items": [{ "project": { "key", "name", "color", "symbol", "icon" }, "decision" }, ...], "more": boolean }`: the decisions addressed to the asker ([ADR-0042](../adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md)), alone or among others ([ADR-0057](../adr/0057-a-decision-may-be-addressed-to-several-people-and-the-first-answer-ends-it.md)), wherever they are, in a project the asker may see or not ([ADR-0068](../adr/0068-a-decision-reaches-whom-it-is-addressed-to-wherever-it-is-and-a-plugins-reaches-the-managers-above-them.md); the project's key, name and mark come with one from elsewhere, and nothing else of it), a page at a time as the tasks are (`limit`, `offset`, `more`): the ones that wait first, the longest waiting first among them, then the answered, newest first, as the record; `open=true` keeps only those that wait, and `raised=true` answers instead the decisions the asker raised, through their agent or themselves, for someone else. Each decision comes with the project it belongs to, as the plate's things do, so that a page says and links it. What a person's own page of decisions reads, in both kinds, and `console decisions --mine` and `--raised`.
168
+
169
+ ### `GET /api/v1/decisions/<id>?wait=` and `POST /api/v1/decisions/<id>/answer`
170
+
171
+ One decision by its id, wherever it is ([ADR-0068](../adr/0068-a-decision-reaches-whom-it-is-addressed-to-wherever-it-is-and-a-plugins-reaches-the-managers-above-them.md)): `{ "project": { "key", "name", "color", "symbol", "icon" }, "decision", "mayAnswer" }`, for whoever it reaches: anyone who may see its project; and, from outside the project, the people it is addressed to, the person who raised it, and, for a decision a plugin raised, whoever manages one of the people it is addressed to, directly or up the reporting line. `wait` holds the request as the project's route does. `mayAnswer` says whether the asker may answer it: anyone who may work in the project, those it is addressed to and the managers above them for a plugin's; the raiser reads alone. `POST .../answer` takes what the project's route takes and writes the answer for whoever `mayAnswer` is true for (`auth.forbidden` otherwise). A decision out of the asker's reach is `decision.not_found`, not told apart from one that is not there; nothing else of a project the asker may not see opens through it.
172
+
173
+ ### `GET /api/v1/decisions/<id>/files/<file id>`
174
+
175
+ The bytes of a file put on a decision ([ADR-0055](../adr/0055-a-decision-carries-the-files-put-on-it-when-it-is-raised.md)) at the decision's own address, for whoever the decision reaches as `GET /api/v1/decisions/<id>` has it, in a project they may see or not ([ADR-0068](../adr/0068-a-decision-reaches-whom-it-is-addressed-to-wherever-it-is-and-a-plugins-reaches-the-managers-above-them.md)), with a session or a token: what the pages read a decision's attachments and the pictures its context names at, on the person's own decisions and at `/decisions/<id>`, where the decision may be of a project the person may not see. Answered as the project's address for files answers: the same `content-length`, `content-disposition`, types and policy. Only a file of that decision: another file of its project, a decision out of the asker's reach, one that is not there and an id that is no id are each `file.not_found`, told apart by nothing.
158
176
 
159
177
  ### `GET /api/v1/tokens` and `POST /api/v1/tokens`
160
178
 
@@ -212,7 +230,7 @@ For an owner, signed in or with a token. The patch takes `role` and writes `proj
212
230
 
213
231
  ### `GET /api/v1/projects/<key>/tasks?state=&open=&archived=&category=&goal=&worker=&unassigned=&dueBy=&q=&limit=&offset=`
214
232
 
215
- `{ "items": [task, ...], "more": boolean }`: the project's board, newest first, a page at a time: `limit` (1 to `LIST_LIMIT`, the default) and `offset` (from 0) say which page, and `more` whether there is one after it; a `limit` or an `offset` that is not a number in range is `request.invalid`. `state` keeps one state of it; `open=true` the tasks not done or dropped, `open=false` the rest; `category=<key>` those sorted into a category; `goal=<id>` those that work towards a goal; `worker=<person id>` those one person is at work on, `unassigned=true` those nobody is at work on (with `worker`, nothing); `dueBy=<day>` (`2026-10-24`) those due on that day or before it; `q=<words>` (1 to `MAX_SEARCH_LENGTH` characters) those whose title holds every word, whatever the case and in any order, and the task of that number for a number (`12`, `#12`), which is how a picker or a command finds a task by its title ([ADR-0037](../adr/0037-a-task-has-one-owner-and-any-number-of-workers-and-a-parent-or-a-goal-is-found-by-search.md)). The tasks put away ([ADR-0029](../adr/0029-a-task-done-or-dropped-is-put-away-out-of-the-board-and-open-by-its-number.md)) are left out; `archived=true` answers them alone, newest first: the archive and the board are two listings, never one. A task carries its `number` (the one people say out loud, numbered per project), `title`, `body` (Markdown, to be shown as text or rendered to elements, never as HTML), `state`, `priority`, `owner` (one person, whoever wrote it), `workers` (the people at work on it, in the order they came to it, `[]` while nobody is; [ADR-0037](../adr/0037-a-task-has-one-owner-and-any-number-of-workers-and-a-parent-or-a-goal-is-found-by-search.md)), `parentId`, `category` (its `key`, `name` and `color` as the workspace keeps it, or `null`), `goalId` (or `null`), its plan ([ADR-0019](../adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md)): `startsOn` and `dueOn`, each a day (`2026-10-24`) or `null`; its moments, which the board keeps itself: `startedAt`, when it was first moved past `ready`, and `doneAt`, when it was moved to `done`, each an instant or `null`; `archivedAt`, when it was put away, an instant or `null` while it is current; `links` (`url` and `label`), `openDecisions` (how many decisions about it wait for a person), `openRuns` (how many runs on it are open: begun and not ended, which says nothing of whether an agent is still at it, [ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)), `createdAt` and `updatedAt`.
233
+ `{ "items": [task, ...], "more": boolean }`: the project's board, newest first, a page at a time: `limit` (1 to `LIST_LIMIT`, the default) and `offset` (from 0) say which page, and `more` whether there is one after it; a `limit` or an `offset` that is not a number in range is `request.invalid`. `state` keeps one state of it; `open=true` the tasks not done or dropped, `open=false` the rest; `category=<key>` those sorted into a category; `goal=<id>` those that work towards a goal; `worker=<person id>` those one person is at work on, `unassigned=true` those nobody is at work on (with `worker`, nothing); `dueBy=<day>` (`2026-10-24`) those due on that day or before it; `q=<words>` (1 to `MAX_SEARCH_LENGTH` characters) those whose title holds every word, whatever the case and in any order, and the task of that number for a number (`12`, `#12`), which is how a picker or a command finds a task by its title ([ADR-0037](../adr/0037-a-task-has-one-owner-and-any-number-of-workers-and-a-parent-or-a-goal-is-found-by-search.md)). The tasks put away ([ADR-0029](../adr/0029-a-task-done-or-dropped-is-put-away-out-of-the-board-and-open-by-its-number.md)) are left out; `archived=true` answers them alone, newest first: the archive and the board are two listings, never one. A task carries its `number` (the one people say out loud, numbered per project), `title`, `body` (Markdown, to be shown as text or rendered to elements, never as HTML), `state`, `priority`, `owner` (one person, whoever wrote it), `agent` (the agent the task was written through, or `null` when a person wrote it themselves, the owner may have changed since; [ADR-0056](../adr/0056-a-record-that-names-the-person-who-did-a-thing-names-the-agent-they-did-it-through.md); from 0.4.4, read as optional), `workers` (the people at work on it, in the order they came to it, `[]` while nobody is; [ADR-0037](../adr/0037-a-task-has-one-owner-and-any-number-of-workers-and-a-parent-or-a-goal-is-found-by-search.md)), `parentId`, `category` (its `key`, `name` and `color` as the workspace keeps it, or `null`), `goalId` (or `null`), its plan ([ADR-0019](../adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md)): `startsOn` and `dueOn`, each a day (`2026-10-24`) or `null`; its moments, which the board keeps itself: `startedAt`, when it was first moved past `ready`, and `doneAt`, when it was moved to `done`, each an instant or `null`; `archivedAt`, when it was put away, an instant or `null` while it is current; `links` (`url` and `label`), `openDecisions` (how many decisions about it wait for a person), `openRuns` (how many runs on it are open: begun and not ended, which says nothing of whether an agent is still at it, [ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)), `createdAt` and `updatedAt`.
216
234
 
217
235
  ### `POST /api/v1/projects/<key>/tasks`
218
236
 
@@ -234,25 +252,25 @@ Writing one, for whoever may work in the project, with a session or a token, is
234
252
 
235
253
  ### `GET /api/v1/projects/<key>/decisions?open=&task=&limit=&offset=`
236
254
 
237
- `{ "items": [decision, ...], "more": boolean }`: the project's, the ones that wait first, newest first within each, a page at a time as the tasks are (`limit`, `offset`, `more`); `open=true` keeps only those that wait, `task=<id>` only those about one task. A decision carries its `question`, `body` (Markdown: the context it rests on), `options` (each an `id` and a `label`, and the `key` the raiser gave it, if any, for a program to read the answer by; the last carries `own: true` when it is the person's own answer, written instead of choosing, and the options given carry no `own`), `taskId` and `taskNumber` (or `null`), `raisedBy`, `decider` (whom it is for, [ADR-0042](../adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md): the raiser, unless another person of the project was named), `run` (the `id` and `agent` of the run that raised it, when an agent asked, or `null`), `files` (the files put on it when it was raised, [ADR-0055](../adr/0055-a-decision-carries-the-files-put-on-it-when-it-is-raised.md): each an `id` and the `file` as a comment's is, `name`, `size`, `contentType`, `sha256`; answered from 0.4.3 and read as optional), `answer`: `null` while it waits, else the chosen `option`, a `note` or `null` (the rationale given with it), `by` and `at`; and `outcome`, what followed, in Markdown, or `null` until someone writes it. Context, rationale and outcome make the decision a record ([ADR-0009](../adr/0009-documents-are-pages-of-markdown-in-a-project-addressed-by-path-versioned-and-linked-both-ways.md)); a link in any of them to a document's path is kept both ways, so that the document says the decision refers to it.
255
+ `{ "items": [decision, ...], "more": boolean }`: the project's, the ones that wait first, newest first within each, a page at a time as the tasks are (`limit`, `offset`, `more`); `open=true` keeps only those that wait, `task=<id>` only those about one task. A decision carries its `question`, `body` (Markdown: the context it rests on), `options` (each an `id` and a `label`, and the `key` the raiser gave it, if any, for a program to read the answer by; the last carries `own: true` when it is the person's own answer, written instead of choosing, and the options given carry no `own`), `taskId` and `taskNumber` (or `null`), `raisedBy`, `decider` (whom it is for, [ADR-0042](../adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md): the raiser, unless another person of the project was named; the first of them where several were) and `deciders` (everyone it is addressed to, in the order named, [ADR-0057](../adr/0057-a-decision-may-be-addressed-to-several-people-and-the-first-answer-ends-it.md): each finds it among their decisions, any of them may answer, and the first answer ends it; `[decider]` where one was named; answered from 0.4.4 and read as optional), `run` (the `id` and `agent` of the run that raised it, when an agent asked, or `null`), `files` (the files put on it when it was raised, [ADR-0055](../adr/0055-a-decision-carries-the-files-put-on-it-when-it-is-raised.md): each an `id` and the `file` as a comment's is, `name`, `size`, `contentType`, `sha256`; answered from 0.4.3 and read as optional), `answer`: `null` while it waits, else the chosen `option`, a `note` or `null` (the rationale given with it), `by`, `at` and `agent` (the agent the answer came through, when the person's agent wrote it in their name, or `null` when they answered themselves; [ADR-0056](../adr/0056-a-record-that-names-the-person-who-did-a-thing-names-the-agent-they-did-it-through.md); answered from 0.4.4 and read as optional); and `outcome`, what followed, in Markdown, or `null` until someone writes it, with `outcomeBy`, `outcomeAgent` and `outcomeAt`, who wrote it, through which agent, and when (each `null` until then; from 0.4.4, read as optional). Context, rationale and outcome make the decision a record ([ADR-0009](../adr/0009-documents-are-pages-of-markdown-in-a-project-addressed-by-path-versioned-and-linked-both-ways.md)); a link in any of them to a document's path is kept both ways, so that the document says the decision refers to it.
238
256
 
239
257
  ### `POST /api/v1/projects/<key>/decisions`
240
258
 
241
- Takes `question`, `options` (two to `MAX_OPTIONS`, each a `label` and optionally a `key`, a lowercase word spelled as a plugin's key is and given once, which the `decision.answered` event carries back as `optionKey`, so that a program reads the answer without comparing labels), and optionally `body`, and either `taskId` or `runId`; as JSON, or, with files put on the decision ([ADR-0055](../adr/0055-a-decision-carries-the-files-put-on-it-when-it-is-raised.md)), as a form (`multipart/form-data`) whose part `decision` says what the JSON would, with a part `file` for each file, each checked as a file handed in is, at most `MAX_FILES_PER_DECISION` of them and together at most `MAX_FILE_BYTES` (`request.too_large`); the whole form is read and found good before any byte is kept, and a picture among the files is shown where the body names it as `![words](its name)`, the rest after the body. `deciderId` names whom the decision is for ([ADR-0042](../adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md)): a person who may work in the project, as a worker of a task must (`decision.invalid_decider` otherwise); left out, the asker, which from a run is the person the agent works for. Anyone of the project may still answer. Options are numbered from `1`. From a run, which must be the asker's and open (`decision.invalid_run`), the decision is about the run's task, `taskId` is not looked at, and the run waits (`status: waiting`) until the answer. Answers `201` with the decision and writes a `decision.raised` event, with the `agent` when a run asked and, when files came, how many as `files`.
259
+ Takes `question`, `options` (two to `MAX_OPTIONS`, each a `label` and optionally a `key`, a lowercase word spelled as a plugin's key is and given once, which the `decision.answered` event carries back as `optionKey`, so that a program reads the answer without comparing labels), and optionally `body`, and either `taskId` or `runId`; as JSON, or, with files put on the decision ([ADR-0055](../adr/0055-a-decision-carries-the-files-put-on-it-when-it-is-raised.md)), as a form (`multipart/form-data`) whose part `decision` says what the JSON would, with a part `file` for each file, each checked as a file handed in is, at most `MAX_FILES_PER_DECISION` of them and together at most `MAX_FILE_BYTES` (`request.too_large`); the whole form is read and found good before any byte is kept, and a picture among the files is shown where the body names it as `![words](its name)`, the rest after the body. `deciderId` names whom the decision is for ([ADR-0042](../adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md)): a person who may work in the project, as a worker of a task must (`decision.invalid_decider` otherwise), or, where a plugin raises it, anyone active of the workspace ([ADR-0068](../adr/0068-a-decision-reaches-whom-it-is-addressed-to-wherever-it-is-and-a-plugins-reaches-the-managers-above-them.md)); left out, the asker, which from a run is the person the agent works for. `deciderIds` names several in its place ([ADR-0057](../adr/0057-a-decision-may-be-addressed-to-several-people-and-the-first-answer-ends-it.md)), each once in the order given and each able to work in the project, up to `MAX_DECIDERS` (`decision.too_many_deciders` beyond it): every one of them finds the decision among their own, any of them may answer, and the first answer ends it. Anyone of the project may still answer. Options are numbered from `1`. From a run, which must be the asker's and open (`decision.invalid_run`), the decision is about the run's task, `taskId` is not looked at, and the run waits (`status: waiting`) until the answer. Answers `201` with the decision and writes a `decision.raised` event, with the `agent` when a run asked and, when files came, how many as `files`.
242
260
 
243
261
  ### `GET .../decisions/<id>?wait=` and `POST .../decisions/<id>/answer`
244
262
 
245
263
  One decision. With `wait=<seconds>` (`0` to `600`) the request is held while the decision waits, up to the server's own bound (50 seconds), and answers the decision as it stands then: how an agent learns the answer at once, woken through the database's own channel, without asking again and again. A decision answered already, or a wait of `0`, answers at once.
246
264
 
247
- The answer takes `option` (the id of one of its options) or `own` (the person's own answer, where none of the options is it: one line, up to `MAX_OPTION_LABEL_LENGTH`, as an option is worded; `request.invalid` with both or with neither), and optionally a `note`. An answer of the person's own becomes one more option of the decision, numbered after the ones given and marked `own: true`, and the one chosen, so that the answer names an option either way and every reader finds its label. A decision is answered once; the answer records who gave it and when, writes a `decision.answered` event with the label of the option (`option`) and, where the raiser gave one, its key (`optionKey`), and lets a run that waited go on (`status: running`), unless another decision of its still waits.
265
+ The answer takes `option` (the id of one of its options) or `own` (the person's own answer, where none of the options is it: one line, up to `MAX_OPTION_LABEL_LENGTH`, as an option is worded; `request.invalid` with both or with neither), and optionally a `note`. An answer of the person's own becomes one more option of the decision, numbered after the ones given and marked `own: true`, and the one chosen, so that the answer names an option either way and every reader finds its label. A decision is answered once; the answer records who gave it, the agent it came through (`agent`: the token's name when an agent wrote it in the person's name, `null` when they answered themselves; [ADR-0056](../adr/0056-a-record-that-names-the-person-who-did-a-thing-names-the-agent-they-did-it-through.md)) and when, writes a `decision.answered` event with the label of the option (`option`) and, where the raiser gave one, its key (`optionKey`), and lets a run that waited go on (`status: running`), unless another decision of its still waits.
248
266
 
249
267
  ### `PATCH /api/v1/projects/<key>/decisions/<id>`
250
268
 
251
- Grows the record: takes any of `body` (the context) and `outcome` (what followed; empty clears it), for anyone who may work in the project, with a session from the console's own pages or a token, answered or not. Writes a `decision.updated` event naming the `fields`; a patch that changes nothing writes nothing.
269
+ Grows the record: takes any of `body` (the context) and `outcome` (what followed; empty clears it), for anyone who may work in the project, with a session from the console's own pages or a token, answered or not. What followed records who wrote it, through which agent, and when (`outcomeBy`, `outcomeAgent`, `outcomeAt`; [ADR-0056](../adr/0056-a-record-that-names-the-person-who-did-a-thing-names-the-agent-they-did-it-through.md)), and clears with it. Writes a `decision.updated` event naming the `fields`; a patch that changes nothing writes nothing.
252
270
 
253
271
  ### `GET /api/v1/projects/<key>/goals?state=&open=&category=&q=&limit=&offset=` and `POST /api/v1/projects/<key>/goals`
254
272
 
255
- The roadmap ([ADR-0019](../adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md), [ADR-0026](../adr/0026-a-goal-is-planned-in-days-and-sorted-into-a-category-and-the-roadmap-is-read-as-months.md)). `GET`, for whoever may see the project, answers `{ "items": [goal, ...], "more": boolean }`: a page of the roadmap, the active goals first, then the planned, the reached and the dropped, the sooner due first within each and the older first among those with no day; `limit` (1 to `LIST_LIMIT`, the default) and `offset` say which page and `more` whether there is one after it; `?category=<key>` keeps those sorted into a category, `?state=<state>` those in one state, `?open=true` the planned and the active and `?open=false` the reached and the dropped, and `?q=<words>` those whose title holds every word, whatever the case, which is how a picker or a command finds a goal ([ADR-0037](../adr/0037-a-task-has-one-owner-and-any-number-of-workers-and-a-parent-or-a-goal-is-found-by-search.md)). A goal carries its `title`, `body` (Markdown: what reaching it means), `state` (`planned`, `active`, `done`, `dropped`), `category` (its `key`, `name` and `color` as the workspace keeps it, or `null`), its plan, `startsOn` (the day the work towards it is meant to begin, or `null`) and `dueOn` (the day it is meant to be reached by, or `null`), `createdBy`, `taskCounts` (the tasks that work towards it, counted by state: `idea`, `ready`, `in_progress`, `in_review`, `done`, `dropped`; read from the board as it stands, never written down), `createdAt` and `updatedAt`. `POST`, for an owner of the project, from the console's own pages or with a token, takes `title` and optionally `body`, `state` (default `planned`), `category` (the key of a category of the workspace, or `null`; `goal.invalid_category`), `startsOn` and `dueOn` (days; the target not before the start, `goal.invalid_dates`), answers `201` with the goal and writes a `goal.created` event with its `title`; a member is `auth.forbidden`.
273
+ The roadmap ([ADR-0019](../adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md), [ADR-0026](../adr/0026-a-goal-is-planned-in-days-and-sorted-into-a-category-and-the-roadmap-is-read-as-months.md)). `GET`, for whoever may see the project, answers `{ "items": [goal, ...], "more": boolean }`: a page of the roadmap, the active goals first, then the planned, the reached and the dropped, the sooner due first within each and the older first among those with no day; `limit` (1 to `LIST_LIMIT`, the default) and `offset` say which page and `more` whether there is one after it; `?category=<key>` keeps those sorted into a category, `?state=<state>` those in one state, `?open=true` the planned and the active and `?open=false` the reached and the dropped, and `?q=<words>` those whose title holds every word, whatever the case, which is how a picker or a command finds a goal ([ADR-0037](../adr/0037-a-task-has-one-owner-and-any-number-of-workers-and-a-parent-or-a-goal-is-found-by-search.md)). A goal carries its `title`, `body` (Markdown: what reaching it means), `state` (`planned`, `active`, `done`, `dropped`), `category` (its `key`, `name` and `color` as the workspace keeps it, or `null`), its plan, `startsOn` (the day the work towards it is meant to begin, or `null`) and `dueOn` (the day it is meant to be reached by, or `null`), `createdBy` and `agent` (the agent the goal was set through, or `null` when a person set it themselves; [ADR-0056](../adr/0056-a-record-that-names-the-person-who-did-a-thing-names-the-agent-they-did-it-through.md); from 0.4.4, read as optional), `taskCounts` (the tasks that work towards it, counted by state: `idea`, `ready`, `in_progress`, `in_review`, `done`, `dropped`; read from the board as it stands, never written down), `createdAt` and `updatedAt`. `POST`, for an owner of the project, from the console's own pages or with a token, takes `title` and optionally `body`, `state` (default `planned`), `category` (the key of a category of the workspace, or `null`; `goal.invalid_category`), `startsOn` and `dueOn` (days; the target not before the start, `goal.invalid_dates`), answers `201` with the goal and writes a `goal.created` event with its `title`; a member is `auth.forbidden`.
256
274
 
257
275
  ### `GET .../goals/<id>` and `PATCH .../goals/<id>`
258
276
 
@@ -274,7 +292,7 @@ How the work goes, and what was made. A report takes `body` (Markdown, `MAX_BODY
274
292
 
275
293
  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 on a decision, or attached to a page, is `project.too_many_bytes` (`409`) and nothing of it is kept; a comment's files, a decision'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.
276
294
 
277
- The bytes of any file of the project, handed in by a run, put in a comment or on a decision, 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`.
295
+ The bytes of any file of the project, handed in by a run, put in a comment or on a decision, 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`. A file put on a decision is also read at the decision's own address, `GET /api/v1/decisions/<id>/files/<file id>`, by whoever the decision reaches, in the project or not.
278
296
 
279
297
  ### `POST .../runs/<id>/end`
280
298
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillcdn/console",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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",
@@ -5,7 +5,7 @@ license: MIT
5
5
  compatibility: A shell with Node.js 24 or newer, npm and git, and an agent that reads and writes files. For running the plugin on a local console, Docker for the database and a checkout of the console's repository, where signing in needs a GitHub OAuth app of the person's own, as the console's README says. The console command is optional, for running the plugin's commands as an agent would. 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.0"
8
+ version: "1.7"
9
9
  skillcdn:
10
10
  translations:
11
11
  ko:
@@ -71,25 +71,26 @@ Produces the **design**: one table, a row per thing the feature does, saying who
71
71
  |---|---|
72
72
  | An agent does it for a person, and something changes | A **command**: `defineCommand({ about, args, roles, run })`. Its `text` answer is for people, its `data` for agents (`--json`); it does one thing and does it again safely where it can. |
73
73
  | An agent, or a page, looks something up | A **query**: `defineQuery(...)`, which writes nothing; a query may answer a file. |
74
- | A person must approve, choose or confirm | A **decision** the command raises with `ctx.console.decisions.ask`, with options, addressed to the person a setting or the record names (`deciderId`), `about` the record it is for; the command answers `waiting`, the agent waits as the console's own `ask` is waited for, and the plugin hears the answer in a hook on `console:decision.answered` and goes on. The plugin never answers one, and never computes what a person must choose. |
75
- | Someone must do something later | A **task** on the board project with `ctx.console.tasks.create`, marked as the plugin's, worked by whoever takes it. |
76
- | The console must tell the plugin something happened | A **hook**, `on["console:<kind>"]` (`events:console`): the plugin's board project, the projects it is on and the items it made are what it hears of. Delivery is at least once: a handler reads the record first and does nothing twice. |
74
+ | A person must approve, choose or confirm | A **decision** the command raises with `ctx.console.decisions.ask`, with options, addressed to the person a setting or the record names (`deciderId`), or to several of them (`deciderIds`, any of whom may answer), anyone active of the workspace, in the board project or not: they find it among their decisions and answer it there, and whoever manages them up the reporting line may answer it too ([ADR-0068](/docs/adr/0068-a-decision-reaches-whom-it-is-addressed-to-wherever-it-is-and-a-plugins-reaches-the-managers-above-them.md)); a page sends a person to it with `frame.navigate("/decisions/<id>")`, which opens for them though the project does not; `about` the record it is for, its question, body and labels written once, in the language of whoever is to answer: `ctx.console.people.get(id)` says it as `language`, and where that is `null`, a person who has not opened the console yet, the plugin writes in the first of `ctx.console.workspace().languages` ([ADR-0065](/docs/adr/0065-a-plugin-writes-one-text-and-a-persons-language-is-kept-from-the-first-time-the-pages-see-them.md)); the command answers `waiting`, the agent waits as the console's own `ask` is waited for, and the plugin hears the answer in a hook on `console:decision.answered` and goes on. The plugin never answers one, and never computes what a person must choose. |
75
+ | Someone must do something later | A **task** on the board project with `ctx.console.tasks.create`, marked as the plugin's, worked by whoever takes it; its title and body, and a comment on it, one text each, written as a decision's words are, in the language of whoever it is for. |
76
+ | The console must tell the plugin something happened | A **hook**, `on["console:<kind>"]` (`events:console`): the plugin's board project, the projects it is on and the items it made are what it hears of. Delivery is at least once: a handler reads the record first and does nothing twice, and the event's `id` (one across both streams) with its `attempt` is what it keeps to tell a delivery repeated; a service that says to come back later is told to the console as `retryAfterMs` on the refusal, and the next try waits that long. A collection that grows with every event, deliveries, log lines, gets `retention: { days }` and `history: false`, so that it never reaches the quota. |
77
77
  | Another plugin must tell it | A **hook** on `plugin:<key>:<kind>` (`events:plugins`), or a call on the other's `api` (`plugins:<key>`). |
78
78
  | Time passes | A **job** (`jobs`), `every` or `at` in the workspace's time zone, never two at once; what it did goes into records and, where people should know, a task or a decision. |
79
79
  | An outside service calls in | An **endpoint** (`endpoints`): a `person` endpoint for whoever holds a role, a `webhook` for nobody, verified with `ctx.secrets.verify` against a secret setting. |
80
- | The plugin must call out | `ctx.fetch` to a host the manifest names (`outbound:<host>`), the secret put in by the console from a `secret` setting, never read by the plugin. |
81
- | People look at it, or fill a form | A **page** (level `workspace` or `project`) or a **widget** on the front page: it shows records and calls commands and queries through the bridge; the logic stays in the server module, so that the command line and the pages do the same thing. |
80
+ | The plugin must call out | `ctx.fetch` to a host the manifest names (`outbound:<host>`), with `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` or `OPTIONS`, the secrets put in by the console from `secret` settings (`secret` or `secrets`, each `as` a header, `bearer`, a query parameter, `basic` or `body:<field>`), never read by the plugin, nor in an answer that sends them back; what an answer gives back that is secret, the tokens of an exchange, sealed by the console into `secret` settings with `seal` before the plugin reads the answer, with their expiry, the answer's `sealed` saying what became of each (a token endpoint is asked for JSON, since the body of an answer that is no JSON is withheld), and sealed again by a job that reads `ctx.secrets.status(name)`; a callback the service is to call is `ctx.endpoints.url(name)`, the full address of one of the plugin's endpoints, and a link back to the console starts from `ctx.console.workspace().url`. |
81
+ | It acts for a person who did something elsewhere, or reads another plugin as itself | `people:act`, then `ctx.plugins.get(key).for(personId)` for the other plugin's api as the person, or `ctx.console.decisions.answer(id, answer, { for })` for a decision the person answered elsewhere, one on their own plate (addressed to them, or a plugin's addressed to someone they manage): the person's standing as a member decides, never their administration, and the record is theirs through the plugin. A role of another plugin, asked in the manifest's `grants` with why, is held as itself from a job or a hook, with `ctx.plugin.grants` saying what was granted ([ADR-0072](/docs/adr/0072-a-plugin-is-a-principal-it-holds-roles-granted-to-it-calls-as-itself-and-acts-for-a-person-within-what-both-may-do.md)). |
82
+ | People look at it, or fill a form (a calendar starts its week on `frame.context.workspace.firstDay`, the day the workspace's setting says, and reads `timeZone` beside it; faces come from `frame.people.picture(id)`, since a frame loads no image from elsewhere) | A **page** (level `workspace` or `project`) or a **widget** on the front page, which declares the `sizes` it comes in, at least one of them no wider than four cells, which is all a phone has (`small` 2×2, `medium` 4×2, `large` 4×4, `wide` 8×2, `full` 8×4, square cells of the page's grid) and lays itself out for the one it has (`frame.context.size`, `frame.onSize`), since the person arranges the page ([ADR-0061](/docs/adr/0061-the-front-page-is-one-grid-of-tiles-a-person-arranges-by-hand.md)): it shows records and calls commands and queries through the bridge; the logic stays in the server module, so that the command line and the pages do the same thing. |
82
83
  | An agent must know how | A **skill** in the package, one per kind of work, naming the commands as `console plugin <key> <command>`, with the rules of that work; `SKILLCDN.md` at the root. The agent note of the manifest is the one line that sends an agent to them. |
83
84
 
84
85
  Then the data. Records, unless the feature needs what records do not give: joins across many collections, more rows than a plugin's records may hold, or figures beyond counting, summing, the least and the most; then `storage:sql`, a schema and a role of the plugin's own with migrations under `migrations/`, which needs the deployment's database user to hold `CREATEROLE` and is said in the hand-over. Records are the default: the console keeps them with versions, files, links, search and aggregates, exports and purges them, and shows them to people by role; a schema is the plugin's to keep.
85
86
 
86
- Then the capabilities: the list the split needs, each read as the sentence the administrator will consent to (the specification's catalogue says what each opens and what a malicious plugin could do with it). A capability no row needs is dropped; `people:read:all`, `projects:read:all`, `storage:sql` and every `outbound:<host>` are asked for only where the design shows the row that needs them. Write the design: the table, the collections with their fields, the roles, the settings, the board (`needs`, the defaults), the menu group, the pages and the widgets, and the capabilities with their sentences; stop for the checkpoint.
87
+ Then the capabilities: the list the split needs, each read as the sentence the administrator will consent to (the specification's catalogue says what each opens and what a malicious plugin could do with it). A capability no row needs is dropped; `people:read:all`, `people:read:email` (the work email, to match a person to an account elsewhere), `projects:read:all`, `storage:sql` and every `outbound:<host>` are asked for only where the design shows the row that needs them. Write the design: the table, the collections with their fields, the roles, the settings, the board (`needs`, the defaults), the menu group, the pages and the widgets, and the capabilities with their sentences; stop for the checkpoint.
87
88
 
88
89
  ### Phase 2: Start from the template, then fill it
89
90
 
90
- Produces the **plugin as source**. `console-plugin init <dir> --key <key> --name "<Name>"` writes the smallest complete plugin: the manifest, a server module with one collection, one command and one query, a web bundle with one page and one widget, one skill with its `SKILLCDN.md`, the tests, the build and a README that says where things are; `--console` names the version of `@skillcdn/console` it depends on, the command's own unless said, and for a plugin written for a deployed console, the version that console runs (`console whoami` says it); `--package <name>` the package's name where it is not `console-plugin-<key>`, a scoped one (`@<scope>/console-plugin-<key>`) for a repository of several plugins, published as public; `--repository <url>` with `--directory <path>` where the plugin lives in a repository, which fill `repository`, `homepage` and `bugs` and the manifest's `store`; and `--languages en,ko` the languages of its words, `en` first, which gives every language map and both manifests' translations the others as English words to translate, of which `console-plugin check` warns while they stay English. Then, in `<dir>`: `npm install`, `npm run build`, `npm test`, `npm run check`, all green before a line is changed, and `git init`.
91
+ Produces the **plugin as source**. `console-plugin init <dir> --key <key> --name "<Name>"` writes the smallest complete plugin: the manifest, a server module with one collection, one command and one query, a web bundle with one page and one widget, one skill with its `SKILLCDN.md`, the tests, the build and a README that says where things are; `--console` names the version of `@skillcdn/console` it depends on, the command's own unless said, and for a plugin written for a deployed console, the version that console runs (`console whoami` says it), and the manifest's `console` is written from it, the line of that version (`>=0.4.3 <0.5.0`): the consoles the plugin runs on, which the console checks when it reads the plugin and `console-plugin check` holds the dependency to; narrow it to the patch that brought what the plugin depends on, and widen it to the next line once the plugin is built against it; `--package <name>` the package's name where it is not `console-plugin-<key>`, a scoped one (`@<scope>/console-plugin-<key>`) for a repository of several plugins, published as public; `--repository <url>` with `--directory <path>` where the plugin lives in a repository, which fill `repository`, `homepage` and `bugs` and the manifest's `store`; and `--languages en,ko` the languages of its words, `en` first, which gives every language map and both manifests' translations the others as English words to translate, of which `console-plugin check` warns while they stay English. Then, in `<dir>`: `npm install`, `npm run build`, `npm test`, `npm run check`, all green before a line is changed, and `git init`.
91
92
 
92
- Then the design goes in, file by file: the manifest `console-plugin.json` from the design, its `key`, names and descriptions per language, `agentNote`, `capabilities`, `roles` and `defaultRole`, `board`, `settings`, `menu`, `pages`, `widgets`, `dataVersion`; the server module `src/server/index.ts`, one `defineCollection` per collection, one `defineCommand` or `defineQuery` per row, `endpoints`, `on`, `jobs`, `api`, `onEnable` and `onDisable` where the design has them, exported by name for the tests and gathered in `definePlugin(...)` as the default export; the web bundle `src/web/index.ts` and `index.css`, where `connect()` gives the page the context (who looks, at what, in which language), `command`, `query`, `records`, `files`, `navigate`, `download`, `setTitle` and the events, every class prefixed `pl-<key>-`, the console's `sc-` classes and `--sc-*` tokens for the look, and every word a person wrote put in as text; the skills under `skills/<name>/SKILL.md`, one per kind of work, in the SkillCDN Format, and the README of the package. The server module imports only what the host allows and bundles everything it imports; it keeps no state between calls but in records, since its thread is started afresh after a fault. Where the design has `storage:sql`, `migrations/0001-<words>.sql` makes the tables, and `manifest.migrations` names the directory.
93
+ Then the design goes in, file by file: the manifest `console-plugin.json` from the design, its `key`, names and descriptions per language, `agentNote`, `capabilities`, `roles` (a role for managers is `follows: "reports"`, held by whoever someone reports to and granted by nobody) and `defaultRole`, `board`, `settings`, `menu`, `pages`, `widgets`, `dataVersion`; the server module `src/server/index.ts`, one `defineCollection` per collection, one `defineCommand` or `defineQuery` per row, `endpoints`, `on`, `jobs`, `api`, `onEnable` and `onDisable` where the design has them, exported by name for the tests and gathered in `definePlugin(...)` as the default export; the web bundle `src/web/index.ts` and `index.css`, where `connect()` gives the page the context (who looks, at what, in which language), `command`, `query`, `records`, `files`, `navigate` (a path of the console's), `open` (a link to the web or to mail, from the press itself, which the console's page opens in a new tab), `download`, `setTitle` and the events, every class prefixed `pl-<key>-`, the console's `sc-` classes and `--sc-*` tokens for the look, and every word a person wrote put in as text; the skills under `skills/<name>/SKILL.md`, one per kind of work, in the SkillCDN Format, and the README of the package. The server module imports only what the host allows and bundles everything it imports; it keeps no state between calls but in records, since its thread is started afresh after a fault. Where the design has `storage:sql`, `migrations/0001-<words>.sql` makes the tables, and `manifest.migrations` names the directory.
93
94
 
94
95
  ### Phase 3: Test it as the console runs it
95
96
 
@@ -97,13 +98,13 @@ Produces the **tests**, green. `src/server/index.test.ts` runs each command, que
97
98
 
98
99
  ### Phase 4: Run it on a local console
99
100
 
100
- Produces the **plugin running**, enabled, with its commands tried and its pages shown. The console from its checkout, as the console's README's "Install" says, with one difference: before the api starts, `.env` sets `PLUGINS_DIR` to a directory made for the purpose, whose `package.json` is `{ "private": true, "dependencies": {} }` and in which `npm install --ignore-scripts <path to the plugin>` writes the plugin as a `file:` dependency and links `node_modules/<its name>` to the checkout, built; the console reads the link where it points, so the checkout is the plugin, and `npm run build` in it followed by a restart of the api is a new version. The person signs in on the local console as its administrator (`ADMINS` in `.env`, a GitHub OAuth app registered for the local address, as `apps/console/README.md` of the console says), opens the plugins' settings page, where the plugin is listed as installed with any reason it failed to load, and enables it: the consent to each capability, the board project made from the manifest's defaults or chosen, the projects it is on, the settings; or their agent does the same with `console plugin <key> enable` ([setting-up.md](/skills/planning-the-board/references/setting-up.md), "A plugin's board project"), signed in there with `console login`. The compose database's user owns the database, so `storage:sql` is available locally. Then the tabs appear where the manifest put them, and the plugin is worked as an agent works it ([working-the-board](/skills/working-the-board/SKILL.md)): `console plugin <key>` lists the commands, `console plugin <key> <command> ...` runs one, a decision raised is answered on the pages, a hook runs, a job runs at its time (the worker inside the api, locally), and the pages are looked at with a browser the agent can drive, else by the person. What is seen is shown at the checkpoint; what could not run is reported as not run. Without a provider to register: in the console's checkout, `pnpm dev:plugins -- --plugin <path to the built plugin>` runs the console on this machine with the fixture's people signing in from the browser at the addresses it prints, and the plugin installed beside the fixtures and not enabled; the administrator among them enables it on the settings page, and the pages and `console plugin <key>` are looked at as above. The directory is read as the image lays a plugin out, so a checkout is built first (`npm run build`), and a directory that is not a built plugin stops the console with the reason.
101
+ Produces the **plugin running**, enabled, with its commands tried and its pages shown. The console from its checkout, as the console's README's "Install" says, with one difference: before the api starts, `.env` sets `PLUGINS_DIR` to a directory made for the purpose, whose `package.json` is `{ "private": true, "dependencies": {} }` and in which `npm install --ignore-scripts <path to the plugin>` writes the plugin as a `file:` dependency and links `node_modules/<its name>` to the checkout, built; the console reads the link where it points, so the checkout is the plugin, and `npm run build` in it followed by a restart of the api is a new version. The person signs in on the local console as its administrator (`ADMINS` in `.env`, a GitHub OAuth app registered for the local address, as `apps/console/README.md` of the console says), opens the plugins' settings page, where the plugin is listed as installed with any reason it failed to load, and enables it: the consent to each capability, the board project made from the manifest's defaults or chosen, the projects it is on, the settings; or their agent does the same with `console plugin <key> enable` ([setting-up.md](/skills/planning-the-board/references/setting-up.md), "A plugin's board project"), signed in there with `console login`. The compose database's user owns the database, so `storage:sql` is available locally. Then the tabs appear where the manifest put them, and the plugin is worked as an agent works it ([working-the-board](/skills/working-the-board/SKILL.md)): `console plugin <key>` lists the commands, `console plugin <key> <command> ...` runs one, a decision raised is answered on the pages, a hook runs, a job runs at its time (the worker inside the api, locally), and the pages are looked at with a browser the agent can drive, else by the person. What is seen is shown at the checkpoint; what could not run is reported as not run. Without a provider to register: in the console's checkout, `pnpm dev:plugins --plugin <path to the built plugin>` runs the console on this machine with the fixture's people signing in from the browser at the addresses it prints, the plugin installed beside the fixtures and not enabled, and the plugin's hooks and jobs running in that process, so that a decision answered there reaches the hook within seconds and a job runs at its time; the administrator among them enables it on the settings page, and the pages and `console plugin <key>` are looked at as above. The directory is read as the image lays a plugin out, so a checkout is built first (`npm run build`), and a directory that is not a built plugin stops the console with the reason.
101
102
 
102
103
  ### Phase 5: Ship, and hand the install to the deployment
103
104
 
104
105
  Produces the **release** and the **hand-over**. The version in `package.json` moves as semver says, `dataVersion` moves with an entry in `upgrades` whenever the shape of a record changed; `npm run build`, `npm test` and `npm run check` pass; a commit, and a push of the repository. Then one of two, confirmed with its consequence: `npm publish` (`--access public` for a scoped name), which puts the package on the registry under that name for good, and is done by the person or with their word; or a tag `v<version>` whose tree carries the built package (the template's `.gitignore` leaves `dist/` out of git, so a release commit adds it with `git add -f dist` before the tag), since the deployment installs with no scripts and runs no build. The deployment then names the plugin as `"<name>": "<version>"` or, for a repository of one plugin alone, `"github:<owner>/<repo>#v<version>"`.
105
106
 
106
- The hand-over is a task on the deployment's board, written as [planning-the-board](/skills/planning-the-board/SKILL.md) writes one, or the same words in chat where there is no board: the package and its version, and how it is installed ([deploy/README.md, "Plugins in the image"](/deploy/README.md#plugins-in-the-image), a row of deploying-a-console's [running-it-again.md](/skills/deploying-a-console/references/running-it-again.md)); what the deployment must provide, where the plugin asks it (`CREATEROLE` for `storage:sql`, the hosts of `outbound`, a bound on the files); what the administrator does when they enable it: the capabilities they consent to, the board project, the projects, the settings to fill and the secrets to set, the roles and who gets them; and where the plugin's skills are once it runs (`console plugin <key> skills`). The deployment's agent installs it as a release of the deployment; the administrator enables it; the plugin is then proven on that console as phase 4 proved it locally, and that is the delivery.
107
+ The hand-over is a task on the deployment's board, written as [planning-the-board](/skills/planning-the-board/SKILL.md) writes one, or the same words in chat where there is no board: the package and its version, the consoles it runs on (the manifest's `console`, which must admit the deployment's console: `console whoami` says its version, and a console outside the range lists the plugin as failed with the reason), and how it is installed ([deploy/README.md, "Plugins in the image"](/deploy/README.md#plugins-in-the-image), a row of deploying-a-console's [running-it-again.md](/skills/deploying-a-console/references/running-it-again.md)); what the deployment must provide, where the plugin asks it (`CREATEROLE` for `storage:sql`, the hosts of `outbound`, a bound on the files); what the administrator does when they enable it: the capabilities they consent to, the board project, the projects, the settings to fill and the secrets to set, the roles and who gets them; and where the plugin's skills are once it runs (`console plugin <key> skills`). The deployment's agent installs it as a release of the deployment; the administrator enables it; the plugin is then proven on that console as phase 4 proved it locally, and that is the delivery.
107
108
 
108
109
  ## Verdicts
109
110
 
@@ -118,9 +119,11 @@ The hand-over is a task on the deployment's board, written as [planning-the-boar
118
119
 
119
120
  - People decide: a plugin raises decisions and waits; it never answers one, never acts as a person it is not, and never computes an approval. What it makes on a board carries its name and is reversible by people.
120
121
  - The fewest capabilities: each one in the manifest has a row of the design that needs it, and the sentence an administrator reads for it is true of the code. `check` warns of one no definition uses; it is dropped.
121
- - A secret is a `secret` setting the administrator sets, put into a request by the console or verified by it; the plugin never reads its value, logs it, keeps it in a record, or puts it in the repository, the tests or the chat.
122
+ - A secret is a `secret` setting the administrator sets or the console seals from an answer (`seal`), put into a request by the console within the `hosts` the setting names, or verified by it (`use: "verify"` for one that verifies alone); the plugin never reads its value, logs it, keeps it in a record, or puts it in the repository, the tests or the chat.
123
+ - A plugin acting for a person acts within what that person may do as a member and what the plugin was granted, both, and never answers a decision itself; it never calls its own api (`plugins:<its key>` is refused); a callee reads `ctx.caller.kind === "plugin"` and refuses where its rules need a person at the keyboard.
122
124
  - Everything a person wrote is shown as text, in the pages and in a command's answer; the frame renders no HTML from a record, and its classes are prefixed `pl-<key>-`.
123
125
  - The server module imports nothing the host refuses, reaches no network but `ctx.fetch`, no file but its own, and keeps no state between calls but in records; a hook does nothing twice.
126
+ - A page reaches the console through the bridge alone: a frame has no network and no WebRTC, and the console serves each script of the bundle with a line before it, so that a source map is a line out. A page that needs WebRTC asks for `outbound:webrtc`, optional where the plugin can go without it, whose reach an administrator reads before consenting: any address, from each person's browser, and no camera or microphone with it ([ADR-0066](/docs/adr/0066-webrtc-is-a-way-out-a-plugin-asks-for-and-how-a-frame-is-held-stays-in-one-place.md)).
124
127
  - Nothing is published, tagged for an install or installed without the person's word; the install is the deployment's release, planned and started by its people, never by this skill.
125
128
  - The specification wins: a field, a bound or a refusal is as [docs/specs/plugins.md](/docs/specs/plugins.md) says; the plugin is rebuilt against the console it runs on, since before 1.0 the plugin API moves with the console's line.
126
129
 
@@ -10,7 +10,7 @@ What an administrator or an owner does on the pages, their agent does here with
10
10
 
11
11
  ## A plugin's board project, for an administrator
12
12
 
13
- A plugin the deployment installed ([building-a-plugin](/skills/building-a-plugin/SKILL.md) writes one; the deployment installs it) does nothing until an administrator enables it, and one that puts decisions, tasks and pages on a board names its project then: `console plugin <key> enable --consent <capability>,... --board new|<key> [--projects all|<key>,...] [--set <setting>=<value>]...`, `new` making a private project from the plugin's own defaults (its key, name and description, which `console plugin <key>` shows beforehand, with what each capability opens), `<key>` binding a project that exists; several plugins may share one. `--consent` must name every capability the manifest asks, each read as the sentence it is: what the plugin may do and reach. The project's members are listed as any project's; the plugin's own roles are granted apart, `console plugin <key> grant <role> --person <login>|--project <key>|--everyone`, and taken back with `console plugin <key> revoke <grant>`; `console plugin <key> settings --set <setting>=<value>` fills its settings, and a secret is read from standard input by `console plugin <key> secret <name>`, never an argument. From then on the rules an agent reads in that project end with `## Plugins`, naming it ([working-the-board](/skills/working-the-board/SKILL.md)), and what the plugin writes there carries its name beside the person's. `console plugin <key> disable` stops it and keeps its data. An owner who is not an administrator enables no plugin: the step goes back to the administrator, with the command.
13
+ A plugin the deployment installed ([building-a-plugin](/skills/building-a-plugin/SKILL.md) writes one; the deployment installs it) does nothing until an administrator enables it, consenting to what it requires and turning on what they choose of what it can go without (`console plugin <key> capabilities` says which, and turns them on or off later), and one that puts decisions, tasks and pages on a board names its project then: `console plugin <key> enable --consent <capability>,... --board new|<key> [--projects all|<key>,...] [--set <setting>=<value>]...`, `new` making a private project from the plugin's own defaults (its key, name and description, which `console plugin <key>` shows beforehand, with what each capability opens), `<key>` binding a project that exists; several plugins may share one. `--consent` must name every capability the manifest asks, each read as the sentence it is: what the plugin may do and reach. The project's members are listed as any project's; the plugin's own roles are granted apart, `console plugin <key> grant <role> --person <login>|--project <key>|--everyone`, and taken back with `console plugin <key> revoke <grant>`; `console plugin <key> settings --set <setting>=<value>` fills its settings, and a secret is read from standard input by `console plugin <key> secret <name>`, never an argument. From then on the rules an agent reads in that project end with `## Plugins`, naming it ([working-the-board](/skills/working-the-board/SKILL.md)), and what the plugin writes there carries its name beside the person's. `console plugin <key> disable` stops it and keeps its data. An owner who is not an administrator enables no plugin: the step goes back to the administrator, with the command.
14
14
 
15
15
  ## The members, for an owner
16
16
 
@@ -23,6 +23,7 @@ A plugin the deployment installed ([building-a-plugin](/skills/building-a-plugin
23
23
  ## The people, for an administrator
24
24
 
25
25
  - `console person role <login> <admin|member>`: what a person is in the workspace; the board keeps at least one administrator in (`person.last_admin`).
26
+ - `console person manager <login> <manager login|none>`: whom a person reports to, the reporting line of the directory, which `console people` shows beside each person and the plugins read as a default (the approver of their leave, say) in place of a line of their own; another active person who does not report to them, directly or through others (`person.invalid_manager`), or `none`.
26
27
  - `console person add <login> [--host gh|google]` lets a login in: whoever signs in with that account at that host is a person from then on; the host is read off the login (an address is Google's, anything else GitHub's) unless `--host` says. `console person remove <login>` takes a login away that nobody has signed in with yet.
27
28
  - `console person deactivate <login>` takes a person out: refused on every request at once, their agents with them, kept with everything they did, and listed nowhere ([ADR-0044](/docs/adr/0044-a-token-of-a-person-who-is-out-is-kept-and-refused-with-the-reason-until-they-are-back-in.md)); `console person reactivate <login>` takes them back in. A person the organization let in, through its domain or its organization at the git host, comes and goes with it (`person.of_organization`).
28
29
  - `console people` lists the people the person sees, by login, with what each is, and the logins let in that nobody has signed in with yet.
@@ -5,7 +5,7 @@ license: MIT
5
5
  compatibility: Needs a shell with the console command on the PATH (npm install -g @skillcdn/console, Node.js 24 or newer), signed in by a person. Nothing else; the command needs no key of its own and the agent never sees a token.
6
6
  metadata:
7
7
  author: skillcdn
8
- version: "1.27"
8
+ version: "1.28"
9
9
  skillcdn:
10
10
  translations:
11
11
  ko:
@@ -22,7 +22,7 @@ A task of the board goes in, by its number; a run comes out: the task taken, the
22
22
  - **Checkpoints:** on the board, as decisions (phase 4), which a person answers from wherever they are. The reports say what was done at each milestone and need no answer.
23
23
  - **Go-ahead:** the task's body is the go-ahead. What it says the agent may decide alone is decided within that; anything else that changes scope, cost or a trade-off is a decision.
24
24
  - **Changes mid-run:** a person changes the task or answers a decision on the board; the run takes it from there and says so in its next report. A report is never rewritten.
25
- - **A choice made elsewhere:** a decision is the record of a choice that is a person's to make, not the place it has to be made. An answer given in chat, in a meeting or by changing the task is written on the decision in their name, `console answer <id> --option <n>|--own "<their words>" --note "<where and when>"`, before the work goes on, so that the board shows as waiting only what is still open. What was decided is not asked again.
25
+ - **A choice made elsewhere:** a decision is the record of a choice that is a person's to make, not the place it has to be made. An answer given in chat, in a meeting or by changing the task is written on the decision in their name, `console answer <id> --option <n>|--own "<their words>" --note "<where and when>"`, before the work goes on, so that the board shows as waiting only what is still open; the record names you beside them as the agent the answer came through ([ADR-0056](/docs/adr/0056-a-record-that-names-the-person-who-did-a-thing-names-the-agent-they-did-it-through.md)). What was decided is not asked again.
26
26
  - **A briefing:** asked what is on their plate or for a briefing, start from `console work`: the decisions waiting for the person's answer first, each with what waits on it and since when, since a waiting decision holds a run; then the tasks by their days (`--person <login>` is another person's plate, for an administrator or an owner of a project they are in). `console decisions --mine` and `--raised` read the rest of their decisions.
27
27
  - **The discussion:** what people say about the task is in its comments, printed with the task by `console task <number>` and `console take`; a picture they put in is saved with `console file <id>` and looked at with the agent's own tools. An answer to them, a question that is not a decision, a remark on what was handed in, is a comment, `console comment <number> "<markdown>" [--attach <file>]`, which needs no run. A report is the run's record; a comment is a word to people.
28
28
  - **Work given in chat:** in a directory that works in a project, work given in chat is written on the board first, `console task new "<title>" --body <markdown> --state ready [--category <key>] [--due <day>]`, with the plan's flags as [planning-the-board](/skills/planning-the-board/SKILL.md) lists them, and the person is told the number the command printed, never one assumed, since other agents write on the same board; then it is taken by that number and worked as below, so that the board holds the work and its record. Several tasks, a week's plan, a goal, or the project's own set-up is planning-the-board, the skill beside this one; a task whose body asks for the project itself (a member, a category, the rules, a goal) is worked as a run all the same, with the commands and the standing that skill gives.
@@ -66,7 +66,7 @@ The work happens where it happens: in the repository, the document, the tool. At
66
66
 
67
67
  ### Phase 4: Ask when a person must decide
68
68
 
69
- A choice the body does not settle, a trade-off, a cost, a change of scope: `console ask "<question>" --option "<one>" --option "<another>" --body "<what they need to know>"`, with `--attach <file>` for a picture of what is to be chosen between, a screenshot of what was made; a picture the body names as `![words](its name)` is shown there ([ADR-0055](/docs/adr/0055-a-decision-carries-the-files-put-on-it-when-it-is-raised.md)). The decision is for the person you work for, who finds it among their decisions; `--to <login>` addresses it to another person of the project when the task or the rules name who decides such things ([ADR-0042](/docs/adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md)). The run waits for the answer; the command waits a minute and exits with `3` while it is still to come. Keep waiting with `console decision <id> --wait 100` (seconds), as often as needed, and meanwhile do only what does not depend on the answer. Act on the answer as given, which may be the person's own words rather than one of the options, marked as theirs; what was answered is not asked again. An answer that reaches you anywhere but the board is written on the decision in the person's name first (`console answer`, above) and acted on from there; a question the work overtook is answered the same way, with the person's word on it, however short. Once acted on, `console decision <id> --outcome "<what followed>"` makes the decision read as a record.
69
+ A choice the body does not settle, a trade-off, a cost, a change of scope: `console ask "<question>" --option "<one>" --option "<another>" --body "<what they need to know>"`, with `--attach <file>` for a picture of what is to be chosen between, a screenshot of what was made; a picture the body names as `![words](its name)` is shown there ([ADR-0055](/docs/adr/0055-a-decision-carries-the-files-put-on-it-when-it-is-raised.md)). The decision is for the person you work for, who finds it among their decisions; `--to <login>` addresses it to another person of the project when the task or the rules name who decides such things ([ADR-0042](/docs/adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md)), and `--to` given again to several of them, any of whom may answer, the first answer ending it ([ADR-0057](/docs/adr/0057-a-decision-may-be-addressed-to-several-people-and-the-first-answer-ends-it.md)). The run waits for the answer; the command waits a minute and exits with `3` while it is still to come. Keep waiting with `console decision <id> --wait 100` (seconds), as often as needed, and meanwhile do only what does not depend on the answer; the command finds a decision wherever it is, in the project it works in or, for a plugin's decision that waits in the plugin's own project, there ([ADR-0068](/docs/adr/0068-a-decision-reaches-whom-it-is-addressed-to-wherever-it-is-and-a-plugins-reaches-the-managers-above-them.md)). Act on the answer as given, which may be the person's own words rather than one of the options, marked as theirs; what was answered is not asked again. An answer that reaches you anywhere but the board is written on the decision in the person's name first (`console answer`, above) and acted on from there; a question the work overtook is answered the same way, with the person's word on it, however short. Once acted on, `console decision <id> --outcome "<what followed>"` makes the decision read as a record.
70
70
 
71
71
  ### Phase 5: Hand in
72
72
 
package/src/api.ts CHANGED
@@ -72,6 +72,7 @@ export {
72
72
  MAX_CATEGORY_KEY_LENGTH,
73
73
  MAX_CATEGORY_NAME_LENGTH,
74
74
  MAX_COMMENTS_PER_TASK,
75
+ MAX_DECIDERS,
75
76
  MAX_DECISIONS_PER_PROJECT,
76
77
  MAX_DOCUMENT_LENGTH,
77
78
  MAX_DOCUMENT_PATH_LENGTH,
@@ -107,6 +108,9 @@ export {
107
108
  MAX_STREAMS_PER_PERSON,
108
109
  MAX_SUMMARY_LENGTH,
109
110
  MAX_TASKS_PER_PROJECT,
111
+ MAX_TEAM_KEY_LENGTH,
112
+ MAX_TEAM_NAME_LENGTH,
113
+ MAX_TEAMS,
110
114
  MAX_TITLE_LENGTH,
111
115
  MAX_TOKEN_DAYS,
112
116
  MAX_TOKEN_NAME_LENGTH,
@@ -123,6 +127,8 @@ export {
123
127
  CLAIM_PATH,
124
128
  categoryPath,
125
129
  connectPath,
130
+ decisionFilePath,
131
+ decisionPath,
126
132
  isSignInFailure,
127
133
  loginPath,
128
134
  PROJECT_COLLECTIONS,
@@ -196,6 +202,8 @@ export {
196
202
  type RestGoalInput,
197
203
  type RestGoalPatch,
198
204
  type RestGoals,
205
+ type RestHome,
206
+ type RestHomeTile,
199
207
  type RestListing,
200
208
  type RestListingInput,
201
209
  type RestMe,
@@ -221,6 +229,7 @@ export {
221
229
  type RestProjectRules,
222
230
  type RestProjects,
223
231
  type RestProvider,
232
+ type RestReachedDecision,
224
233
  type RestReport,
225
234
  type RestReportInput,
226
235
  type RestRules,
@@ -242,6 +251,11 @@ export {
242
251
  type RestTaskLink,
243
252
  type RestTaskPatch,
244
253
  type RestTasks,
254
+ type RestTeam,
255
+ type RestTeamInput,
256
+ type RestTeamMemberInput,
257
+ type RestTeamPatch,
258
+ type RestTeams,
245
259
  type RestToken,
246
260
  type RestTokenCreated,
247
261
  type RestTokenInput,
@@ -294,6 +308,8 @@ export {
294
308
  restGoalPatchSchema,
295
309
  restGoalSchema,
296
310
  restGoalsSchema,
311
+ restHomeSchema,
312
+ restHomeTileSchema,
297
313
  restListingInputSchema,
298
314
  restListingSchema,
299
315
  restMemberInputSchema,
@@ -319,6 +335,7 @@ export {
319
335
  restProjectSchema,
320
336
  restProjectsSchema,
321
337
  restProviderSchema,
338
+ restReachedDecisionSchema,
322
339
  restReportInputSchema,
323
340
  restReportSchema,
324
341
  restRulesInputSchema,
@@ -340,6 +357,11 @@ export {
340
357
  restTaskPatchSchema,
341
358
  restTaskSchema,
342
359
  restTasksSchema,
360
+ restTeamInputSchema,
361
+ restTeamMemberInputSchema,
362
+ restTeamPatchSchema,
363
+ restTeamSchema,
364
+ restTeamsSchema,
343
365
  restTokenCreatedSchema,
344
366
  restTokenInputSchema,
345
367
  restTokenSchema,
@@ -359,6 +381,7 @@ export {
359
381
  CATEGORY_COLORS,
360
382
  type CategoryColor,
361
383
  COLOR_NAMES,
384
+ CONSOLE_LANGUAGES,
362
385
  type ColorName,
363
386
  EVENT_KINDS,
364
387
  type EventKind,