@skillcdn/console 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +4 -3
  2. package/dist/api.d.ts +2 -2
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +2 -2
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +132 -18
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts.map +1 -1
  10. package/dist/cli/format.js +6 -4
  11. package/dist/cli/format.js.map +1 -1
  12. package/dist/cli/help.d.ts.map +1 -1
  13. package/dist/cli/help.js +72 -21
  14. package/dist/cli/help.js.map +1 -1
  15. package/dist/cli/pack.d.ts +7 -2
  16. package/dist/cli/pack.d.ts.map +1 -1
  17. package/dist/cli/pack.js +16 -2
  18. package/dist/cli/pack.js.map +1 -1
  19. package/dist/client.d.ts +19 -3
  20. package/dist/client.d.ts.map +1 -1
  21. package/dist/client.js +11 -1
  22. package/dist/client.js.map +1 -1
  23. package/dist/compat.d.ts +1 -1
  24. package/dist/compat.js +2 -2
  25. package/dist/components/board.d.ts +12 -1
  26. package/dist/components/board.d.ts.map +1 -1
  27. package/dist/components/board.js +52 -4
  28. package/dist/components/board.js.map +1 -1
  29. package/dist/components/calendar.d.ts +4 -0
  30. package/dist/components/calendar.d.ts.map +1 -1
  31. package/dist/components/calendar.js +61 -22
  32. package/dist/components/calendar.js.map +1 -1
  33. package/dist/components/decision-form.d.ts +12 -4
  34. package/dist/components/decision-form.d.ts.map +1 -1
  35. package/dist/components/decision-form.js +24 -6
  36. package/dist/components/decision-form.js.map +1 -1
  37. package/dist/components/decision-list.d.ts +4 -0
  38. package/dist/components/decision-list.d.ts.map +1 -1
  39. package/dist/components/decision-list.js +9 -6
  40. package/dist/components/decision-list.js.map +1 -1
  41. package/dist/components/documents.d.ts.map +1 -1
  42. package/dist/components/documents.js +3 -3
  43. package/dist/components/documents.js.map +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +63 -8
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/markdown.d.ts.map +1 -1
  48. package/dist/components/markdown.js +6 -3
  49. package/dist/components/markdown.js.map +1 -1
  50. package/dist/components/my-decisions.d.ts +20 -0
  51. package/dist/components/my-decisions.d.ts.map +1 -0
  52. package/dist/components/my-decisions.js +36 -0
  53. package/dist/components/my-decisions.js.map +1 -0
  54. package/dist/components/pack-connect.d.ts +3 -1
  55. package/dist/components/pack-connect.d.ts.map +1 -1
  56. package/dist/components/pack-connect.js +4 -4
  57. package/dist/components/pack-connect.js.map +1 -1
  58. package/dist/components/pack-token.d.ts +3 -1
  59. package/dist/components/pack-token.d.ts.map +1 -1
  60. package/dist/components/pack-token.js +1 -1
  61. package/dist/components/pack-token.js.map +1 -1
  62. package/dist/components/people.d.ts.map +1 -1
  63. package/dist/components/people.js +14 -3
  64. package/dist/components/people.js.map +1 -1
  65. package/dist/components/projects.d.ts.map +1 -1
  66. package/dist/components/projects.js +3 -3
  67. package/dist/components/projects.js.map +1 -1
  68. package/dist/components/remote-pictures.d.ts +6 -0
  69. package/dist/components/remote-pictures.d.ts.map +1 -0
  70. package/dist/components/remote-pictures.js +8 -0
  71. package/dist/components/remote-pictures.js.map +1 -0
  72. package/dist/components/roadmap.js +1 -1
  73. package/dist/components/roadmap.js.map +1 -1
  74. package/dist/components/runs.d.ts.map +1 -1
  75. package/dist/components/runs.js +1 -1
  76. package/dist/components/runs.js.map +1 -1
  77. package/dist/components/shell.d.ts.map +1 -1
  78. package/dist/components/shell.js +36 -1
  79. package/dist/components/shell.js.map +1 -1
  80. package/dist/components/sign-in.d.ts.map +1 -1
  81. package/dist/components/sign-in.js +49 -2
  82. package/dist/components/sign-in.js.map +1 -1
  83. package/dist/components/task-list.js +1 -1
  84. package/dist/components/task-list.js.map +1 -1
  85. package/dist/components/task-view.d.ts.map +1 -1
  86. package/dist/components/task-view.js +2 -2
  87. package/dist/components/task-view.js.map +1 -1
  88. package/dist/components/ui.d.ts +13 -2
  89. package/dist/components/ui.d.ts.map +1 -1
  90. package/dist/components/ui.js +21 -14
  91. package/dist/components/ui.js.map +1 -1
  92. package/dist/console.css +1686 -425
  93. package/dist/console.d.ts.map +1 -1
  94. package/dist/console.js +83 -13
  95. package/dist/console.js.map +1 -1
  96. package/dist/data.d.ts +15 -2
  97. package/dist/data.d.ts.map +1 -1
  98. package/dist/data.js +18 -19
  99. package/dist/data.js.map +1 -1
  100. package/dist/i18n/en.d.ts +48 -1
  101. package/dist/i18n/en.d.ts.map +1 -1
  102. package/dist/i18n/en.js +50 -3
  103. package/dist/i18n/en.js.map +1 -1
  104. package/dist/i18n/ko.d.ts.map +1 -1
  105. package/dist/i18n/ko.js +47 -4
  106. package/dist/i18n/ko.js.map +1 -1
  107. package/dist/index.d.ts +3 -2
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +3 -2
  110. package/dist/index.js.map +1 -1
  111. package/dist/limits.d.ts +14 -0
  112. package/dist/limits.d.ts.map +1 -1
  113. package/dist/limits.js +14 -0
  114. package/dist/limits.js.map +1 -1
  115. package/dist/router.d.ts +9 -1
  116. package/dist/router.d.ts.map +1 -1
  117. package/dist/router.js +12 -0
  118. package/dist/router.js.map +1 -1
  119. package/dist/routes.d.ts +5 -1
  120. package/dist/routes.d.ts.map +1 -1
  121. package/dist/routes.js +5 -1
  122. package/dist/routes.js.map +1 -1
  123. package/dist/rules.js +1 -1
  124. package/dist/rules.js.map +1 -1
  125. package/dist/schemas.d.ts +272 -4
  126. package/dist/schemas.d.ts.map +1 -1
  127. package/dist/schemas.js +33 -8
  128. package/dist/schemas.js.map +1 -1
  129. package/dist/web.d.ts.map +1 -1
  130. package/dist/web.js +3 -2
  131. package/dist/web.js.map +1 -1
  132. package/docs/specs/cli.md +11 -10
  133. package/docs/specs/rest.md +33 -17
  134. package/package.json +2 -2
  135. package/skills/building-a-console/SKILL.md +5 -5
  136. package/skills/building-a-console/references/the-default-console.md +4 -4
  137. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  138. package/skills/working-the-board/SKILL.md +6 -4
  139. package/skills/working-the-board/references/setting-up.md +1 -1
  140. package/skills/working-the-board/references/terms.md +1 -1
  141. package/src/api.ts +10 -0
  142. package/src/cli/cli.ts +162 -19
  143. package/src/cli/format.ts +11 -4
  144. package/src/cli/help.ts +79 -20
  145. package/src/cli/pack.ts +17 -1
  146. package/src/client.ts +51 -2
  147. package/src/compat.ts +2 -2
  148. package/src/components/board.tsx +114 -21
  149. package/src/components/calendar.tsx +207 -102
  150. package/src/components/decision-form.tsx +71 -23
  151. package/src/components/decision-list.tsx +30 -3
  152. package/src/components/documents.tsx +27 -11
  153. package/src/components/event-feed.tsx +116 -33
  154. package/src/components/markdown.tsx +6 -3
  155. package/src/components/my-decisions.tsx +138 -0
  156. package/src/components/pack-connect.tsx +10 -7
  157. package/src/components/pack-token.tsx +4 -1
  158. package/src/components/people.tsx +20 -8
  159. package/src/components/projects.tsx +7 -5
  160. package/src/components/remote-pictures.ts +19 -0
  161. package/src/components/roadmap.tsx +1 -1
  162. package/src/components/runs.tsx +5 -1
  163. package/src/components/shell.tsx +36 -1
  164. package/src/components/sign-in.tsx +114 -13
  165. package/src/components/task-list.tsx +1 -1
  166. package/src/components/task-view.tsx +48 -47
  167. package/src/components/ui.tsx +70 -14
  168. package/src/console.tsx +153 -32
  169. package/src/data.ts +37 -20
  170. package/src/i18n/en.ts +53 -3
  171. package/src/i18n/ko.ts +50 -4
  172. package/src/index.ts +3 -0
  173. package/src/limits.ts +14 -0
  174. package/src/router.ts +18 -1
  175. package/src/routes.ts +5 -1
  176. package/src/rules.ts +1 -1
  177. package/src/schemas.ts +37 -8
  178. package/src/styles/console.css +1686 -425
  179. package/src/web.ts +3 -2
package/src/cli/cli.ts CHANGED
@@ -801,13 +801,19 @@ const logout: Command = async (args, io) => {
801
801
  }
802
802
  // The token is given up at the console first, so that it is good for nothing from now on;
803
803
  // one the console no longer knows is gone already. A console out of reach leaves it to the
804
- // Agents page, and the person is told so.
805
- let gone = true;
804
+ // Agents page, and the person is told so. A console that keeps it because the person is out
805
+ // (ADR-0044) is told so too: the token is theirs again when they are back in.
806
+ let outcome: "gone" | "kept" | "left" = "gone";
806
807
  try {
807
808
  await clientOf(io, url, login.token).giveUpToken();
808
809
  } catch (error) {
809
- if (!(error instanceof ApiError && error.code === "auth.required")) {
810
- gone = false;
810
+ if (
811
+ error instanceof ApiError &&
812
+ (error.code === "auth.deactivated" || error.code === "auth.not_admitted")
813
+ ) {
814
+ outcome = "kept";
815
+ } else if (!(error instanceof ApiError && error.code === "auth.required")) {
816
+ outcome = "left";
811
817
  }
812
818
  }
813
819
  const next = withoutLogin(credentials, url, login.account);
@@ -818,9 +824,11 @@ const logout: Command = async (args, io) => {
818
824
  }
819
825
  const whom = login.account === undefined ? "" : ` as ${login.account}`;
820
826
  io.stdout(
821
- gone
827
+ outcome === "gone"
822
828
  ? `Signed out of ${url}${whom}: the token is given up at the console and forgotten here.\n`
823
- : `Signed out of ${url}${whom} here, but the console could not be reached to give the token up: a person removes it on the Agents page.\n`,
829
+ : outcome === "kept"
830
+ ? `Signed out of ${url}${whom} here: the console keeps the token while the person is out, for when they are back in.\n`
831
+ : `Signed out of ${url}${whom} here, but the console could not be reached to give the token up: a person removes it on the Agents page.\n`,
824
832
  );
825
833
  envNote();
826
834
  return EXIT.ok;
@@ -1172,8 +1180,37 @@ const skills: Command = async (args, io) => {
1172
1180
  return EXIT.ok;
1173
1181
  };
1174
1182
 
1183
+ /** The most characters one read of a file asks for: what the console's API allows on one page. */
1184
+ const MAX_FILE_LIMIT = 100_000;
1185
+
1186
+ /** A count given as a flag, within its bounds, or nothing; anything else is not understood. */
1187
+ function countOf(
1188
+ given: string | undefined,
1189
+ name: string,
1190
+ bounds: { readonly least: number; readonly most?: number },
1191
+ ): number | undefined {
1192
+ if (given === undefined) {
1193
+ return undefined;
1194
+ }
1195
+ const count = Number(given);
1196
+ if (
1197
+ !Number.isInteger(count) ||
1198
+ count < bounds.least ||
1199
+ (bounds.most !== undefined && count > bounds.most)
1200
+ ) {
1201
+ const range =
1202
+ bounds.most === undefined ? `${bounds.least} or more` : `${bounds.least} to ${bounds.most}`;
1203
+ throw misuse(`Not a number for --${name}, ${range}: ${given}`);
1204
+ }
1205
+ return count;
1206
+ }
1207
+
1175
1208
  const skill: Command = async (args, io) => {
1176
- const { values, positionals } = parse(args, { file: { type: "string" } });
1209
+ const { values, positionals } = parse(args, {
1210
+ file: { type: "string" },
1211
+ offset: { type: "string" },
1212
+ limit: { type: "string" },
1213
+ });
1177
1214
  const file = text(values, "file");
1178
1215
  // A file is read by its own path from the repository's root; a skill's path beside it would be
1179
1216
  // ignored, which reads as if it mattered, so the two together are not understood.
@@ -1182,6 +1219,17 @@ const skill: Command = async (args, io) => {
1182
1219
  "Say one: console skill <path of its SKILL.md> for a skill, or console skill --file <path> for a file of the pack.",
1183
1220
  );
1184
1221
  }
1222
+ // A section is a page of a file read as asked (SkillCDN 0.4): the flags mean nothing without one.
1223
+ if (
1224
+ file === undefined &&
1225
+ (text(values, "offset") !== undefined || text(values, "limit") !== undefined)
1226
+ ) {
1227
+ throw misuse(
1228
+ "--offset and --limit go with --file: console skill --file <path> [--offset <n>] [--limit <n>]",
1229
+ );
1230
+ }
1231
+ const offset = countOf(text(values, "offset"), "offset", { least: 0 });
1232
+ const limit = countOf(text(values, "limit"), "limit", { least: 1, most: MAX_FILE_LIMIT });
1185
1233
  const session = await open(io, values);
1186
1234
  const project = await session.project();
1187
1235
  const { listing, summary } = await packOf(project);
@@ -1189,6 +1237,16 @@ const skill: Command = async (args, io) => {
1189
1237
  answer(io, session, listing, () => formatSkills(listing, io.skillsDir));
1190
1238
  return EXIT.ok;
1191
1239
  }
1240
+ if (file !== undefined && (offset !== undefined || limit !== undefined)) {
1241
+ // One page as asked, the section a search or an outline named; the result says where the
1242
+ // file continues, and asks for nothing.
1243
+ const page = await project.skillFile(file, { offset, limit });
1244
+ const section = page.read === null ? undefined : joinFile([page], summary, limit);
1245
+ answer(io, session, { ...page, read: section === undefined ? null : bodyOfFile(section) }, () =>
1246
+ formatSkillFile(page, section),
1247
+ );
1248
+ return EXIT.ok;
1249
+ }
1192
1250
  if (file !== undefined) {
1193
1251
  // The file whole, page after page, as the console reads it from the pack.
1194
1252
  const pages: RestSkillFile[] = [];
@@ -1800,11 +1858,16 @@ const ask: Command = async (args, io) => {
1800
1858
  file: { type: "string" },
1801
1859
  run: { type: "string" },
1802
1860
  wait: { type: "string" },
1861
+ to: { type: "string" },
1803
1862
  });
1804
1863
  const question = theOne(
1805
1864
  positionals,
1806
1865
  'Say the question: console ask "<question>" --option "<label>" --option "<label>"',
1807
1866
  );
1867
+ const to = text(values, "to");
1868
+ if (to === "none") {
1869
+ throw misuse("Say whom the decision is for: --to <login|me>.");
1870
+ }
1808
1871
  const options = texts(values, "option");
1809
1872
  if (options.length < MIN_OPTIONS || options.length > MAX_OPTIONS) {
1810
1873
  throw misuse(`Give ${MIN_OPTIONS} to ${MAX_OPTIONS} options, one --option each.`);
@@ -1814,21 +1877,58 @@ const ask: Command = async (args, io) => {
1814
1877
  const session = await open(io, values);
1815
1878
  const project = await session.project();
1816
1879
  const runId = await currentRun(project, values);
1880
+ // Whom the decision is for (ADR-0042): the person the agent works for, unless --to names another.
1881
+ const deciderId = await personIdOf(session, to);
1817
1882
  const raised = await project.raiseDecision({
1818
1883
  question,
1819
1884
  options: options.map((label) => ({ label })),
1820
1885
  runId,
1821
1886
  ...(body === undefined ? {} : { body }),
1887
+ ...(typeof deciderId === "string" ? { deciderId } : {}),
1822
1888
  });
1823
1889
  // The id is printed before any waiting, so that a wait cut short loses nothing.
1824
1890
  io.stdout(
1825
1891
  session.json
1826
1892
  ? `${JSON.stringify(raised, null, 2)}\n`
1827
- : `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`,
1893
+ : `Decision ${raised.id} raised on run ${runId}; the run waits for ${raised.decider.login}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`,
1828
1894
  );
1829
1895
  return settle(io, session, project, raised, seconds, true);
1830
1896
  };
1831
1897
 
1898
+ /**
1899
+ * Writes a person's answer on a decision, in their name through the agent (ADR-0021): the
1900
+ * option they chose, or their own words, and a note about it, so that a choice made in chat
1901
+ * or in passing reaches the record the run waits on.
1902
+ */
1903
+ const answering: Command = async (args, io) => {
1904
+ const { values, positionals } = parse(args, {
1905
+ option: { type: "string" },
1906
+ own: { type: "string" },
1907
+ note: { type: "string" },
1908
+ file: { type: "string" },
1909
+ });
1910
+ const id = theOne(
1911
+ positionals,
1912
+ 'Say which decision: console answer <id> --option <n>|--own "<answer>" [--note <markdown>|--file <path>]',
1913
+ );
1914
+ const option = text(values, "option");
1915
+ const own = text(values, "own");
1916
+ if ((option === undefined) === (own === undefined)) {
1917
+ throw misuse(
1918
+ 'Say the option chosen, --option <n>, or the person\'s own answer, --own "<words>": one of the two.',
1919
+ );
1920
+ }
1921
+ const note = await bodyOf(io, values, text(values, "note"));
1922
+ const session = await open(io, values);
1923
+ const project = await session.project();
1924
+ const answered = await project.answerDecision(id, {
1925
+ ...(own === undefined ? { option } : { own }),
1926
+ ...(note === undefined ? {} : { note }),
1927
+ });
1928
+ answer(io, session, answered, () => formatDecision(answered));
1929
+ return EXIT.ok;
1930
+ };
1931
+
1832
1932
  const decision: Command = async (args, io) => {
1833
1933
  const { values, positionals } = parse(args, {
1834
1934
  wait: { type: "string" },
@@ -2139,14 +2239,37 @@ const goal: Command = async (args, io) => {
2139
2239
  };
2140
2240
 
2141
2241
  const decisions: Command = async (args, io) => {
2142
- const { values } = parse(args, { open: { type: "boolean" }, ...PAGE_OPTIONS }, false);
2242
+ const { values } = parse(
2243
+ args,
2244
+ {
2245
+ open: { type: "boolean" },
2246
+ mine: { type: "boolean" },
2247
+ raised: { type: "boolean" },
2248
+ ...PAGE_OPTIONS,
2249
+ },
2250
+ false,
2251
+ );
2143
2252
  const page = pageOf(values);
2144
2253
  const session = await open(io, values);
2254
+ const open_ = on(values, "open") ? { open: true } : {};
2255
+ if (on(values, "mine") || on(values, "raised")) {
2256
+ // One's own across the projects (ADR-0042), addressed to the person or raised by them for
2257
+ // someone else: the waiting first, each with its project's key.
2258
+ const raised = on(values, "raised");
2259
+ const found = await session.client.myDecisions({ ...open_, raised, ...page });
2260
+ answer(io, session, found, () =>
2261
+ found.items.length === 0
2262
+ ? raised
2263
+ ? "No decisions you asked of others."
2264
+ : "No decisions of yours."
2265
+ : found.items
2266
+ .map(({ project, decision }) => `${project.key} ${formatDecisionLine(decision)}`)
2267
+ .join("\n") + formatMore(found.more, page.limit, page.offset),
2268
+ );
2269
+ return EXIT.ok;
2270
+ }
2145
2271
  const project = await session.project();
2146
- const found = await project.decisions({
2147
- ...(on(values, "open") ? { open: true } : {}),
2148
- ...page,
2149
- });
2272
+ const found = await project.decisions({ ...open_, ...page });
2150
2273
  answer(io, session, found, () =>
2151
2274
  found.items.length === 0
2152
2275
  ? "No decisions."
@@ -2384,8 +2507,8 @@ const workspace: Command = async (args, io) => {
2384
2507
  set: (token) => client.setWorkspacePackToken({ token }),
2385
2508
  remove: () => client.removeWorkspacePackToken(),
2386
2509
  connect: () => client.connectWorkspacePackToken(),
2387
- async standing() {
2388
- const state = await client.workspacePackTokenConnection();
2510
+ async claim() {
2511
+ const state = await client.claimWorkspacePackTokenConnection();
2389
2512
  return state.status === "connected"
2390
2513
  ? { status: "connected", held: state.workspace }
2391
2514
  : state;
@@ -2545,7 +2668,8 @@ interface TokenHolder<T> {
2545
2668
  set(token: string): Promise<T>;
2546
2669
  remove(): Promise<T>;
2547
2670
  connect(): Promise<RestPackConnectStarted>;
2548
- standing(): Promise<
2671
+ /** Asks the console to claim the token at the deployment (ADR-0046), and answers how it stands. */
2672
+ claim(): Promise<
2549
2673
  | { readonly status: "none" }
2550
2674
  | { readonly status: "pending" }
2551
2675
  | { readonly status: "ended"; readonly reason: keyof typeof PACK_CONNECT_ENDED }
@@ -2577,7 +2701,7 @@ async function packToken<T>(
2577
2701
  );
2578
2702
  for (;;) {
2579
2703
  await io.sleep(started.intervalSeconds * 1000);
2580
- const state = await holder.standing();
2704
+ const state = await holder.claim();
2581
2705
  if (state.status === "pending") {
2582
2706
  continue;
2583
2707
  }
@@ -2625,8 +2749,8 @@ const projectPackToken: Command = async (args, io) => {
2625
2749
  set: (token) => project.setPackToken({ token }),
2626
2750
  remove: () => project.removePackToken(),
2627
2751
  connect: () => project.connectPackToken(),
2628
- async standing() {
2629
- const state = await project.packTokenConnection();
2752
+ async claim() {
2753
+ const state = await project.claimPackTokenConnection();
2630
2754
  return state.status === "connected" ? { status: "connected", held: state.project } : state;
2631
2755
  },
2632
2756
  },
@@ -2869,6 +2993,7 @@ const COMMANDS: Readonly<Record<string, Command>> = {
2869
2993
  comment,
2870
2994
  file,
2871
2995
  ask,
2996
+ answer: answering,
2872
2997
  decision,
2873
2998
  decisions,
2874
2999
  finish: ending("finished"),
@@ -3006,6 +3131,10 @@ function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
3006
3131
  return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
3007
3132
  case "auth.required":
3008
3133
  return "The token is not one the console knows, any more or at all. A person signs the command in again: console login\n";
3134
+ case "auth.deactivated":
3135
+ return "Tell the person: they are taken back in on the People page, by an administrator or by their organization, and this token works again then. console logout forgets it here meanwhile.\n";
3136
+ case "auth.not_admitted":
3137
+ return "Tell the person: they are back in when what let them in is named again, or when an administrator lists their login and they sign in again, and this token works again then. console logout forgets it here meanwhile.\n";
3009
3138
  case "project.not_found":
3010
3139
  return "No project has this key, or it is not yours to see. console projects lists the projects you may work in.\n";
3011
3140
  case "document.not_found":
@@ -3030,6 +3159,20 @@ function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
3030
3159
  return "What was asked is an administrator's, or an owner's of the project, and the person this token acts as is not one here. Hand the step to them.\n";
3031
3160
  case "project.key_taken":
3032
3161
  return "A project has that key already. console projects lists them.\n";
3162
+ case "project.too_many":
3163
+ return "The workspace holds as many projects as one may. Work goes on in a project there is.\n";
3164
+ case "project.too_many_bytes":
3165
+ return "The project's files hold as many bytes together as this console allows. Hand in a link to where the file is instead, and say so in the report.\n";
3166
+ case "task.too_many":
3167
+ return "The project holds as many tasks as one may, those put away counted. The work goes on in another project; ask the person which.\n";
3168
+ case "goal.too_many":
3169
+ return "The project holds as many goals as one may. console goals lists them; one done or dropped is still a goal.\n";
3170
+ case "decision.too_many":
3171
+ return "The project holds as many decisions as one may. Ask the person in the report instead.\n";
3172
+ case "document.too_many":
3173
+ return "The project holds as many pages as one may. Write on a page there is: console docs lists them.\n";
3174
+ case "run.too_many":
3175
+ return "The task has been taken as many times as one may. Write a new task for what is left, and take that.\n";
3033
3176
  case "project.invalid_address":
3034
3177
  return "The skills address is not one as SkillCDN spells it; none makes the organization's skills the project's.\n";
3035
3178
  case "project.invalid_icon":
package/src/cli/format.ts CHANGED
@@ -386,7 +386,10 @@ export function formatWork(work: RestWork): string {
386
386
  lines.push(` ${project.key} ${formatTaskLine(task)}`);
387
387
  }
388
388
  }
389
- lines.push("", work.decisions.length === 0 ? "decisions waiting: none" : "decisions waiting:");
389
+ lines.push(
390
+ "",
391
+ work.decisions.length === 0 ? "decisions waiting for you: none" : "decisions waiting for you:",
392
+ );
390
393
  for (const { project, decision } of work.decisions) {
391
394
  lines.push(` ${project.key} ${formatDecisionLine(decision)}`);
392
395
  }
@@ -404,9 +407,13 @@ export function formatPersonWork(person: RestPerson, work: RestPersonWork): stri
404
407
  ].join("\n");
405
408
  }
406
409
 
410
+ /** Whom a decision is for, where that is not the one who asked (ADR-0042). */
411
+ const addressed = (decision: RestDecision): string =>
412
+ decision.decider.id === decision.raisedBy.id ? "" : `, for ${decision.decider.login}`;
413
+
407
414
  export function formatDecisionLine(decision: RestDecision): string {
408
415
  const state = decision.answer === null ? "waiting" : `answered ${decision.answer.option}`;
409
- return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)})`;
416
+ return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)}${addressed(decision)})`;
410
417
  }
411
418
 
412
419
  /** The options, one a line; the person's own answer, when they wrote one, is the last and says so. */
@@ -423,7 +430,7 @@ export function formatOptions(decision: RestDecision): string {
423
430
  export function formatAnswer(decision: RestDecision): string {
424
431
  const { answer } = decision;
425
432
  if (answer === null) {
426
- return "waits for a person.";
433
+ return `waits for ${decision.decider.login}.`;
427
434
  }
428
435
  const chosen = decision.options.find((option) => option.id === answer.option);
429
436
  const lines = [
@@ -438,7 +445,7 @@ export function formatAnswer(decision: RestDecision): string {
438
445
  export function formatDecision(decision: RestDecision): string {
439
446
  const lines = [
440
447
  `decision ${decision.id}: ${decision.question}`,
441
- `asked by ${asker(decision)}${decision.taskNumber === null ? "" : `, about task #${decision.taskNumber}`}`,
448
+ `asked by ${asker(decision)}${addressed(decision)}${decision.taskNumber === null ? "" : `, about task #${decision.taskNumber}`}`,
442
449
  ];
443
450
  if (decision.body.trim().length > 0) {
444
451
  lines.push("", decision.body.trimEnd());
package/src/cli/help.ts CHANGED
@@ -7,12 +7,16 @@ import {
7
7
  MAX_CATEGORY_KEY_LENGTH,
8
8
  MAX_CATEGORY_NAME_LENGTH,
9
9
  MAX_COMMENTS_PER_TASK,
10
+ MAX_DECISIONS_PER_PROJECT,
10
11
  MAX_DOCUMENT_LENGTH,
11
12
  MAX_DOCUMENT_PATH_LENGTH,
13
+ MAX_DOCUMENTS_PER_PROJECT,
12
14
  MAX_FILE_BYTES,
13
15
  MAX_FILES_PER_COMMENT,
14
16
  MAX_FILES_PER_DOCUMENT,
17
+ MAX_GOALS_PER_PROJECT,
15
18
  MAX_LINK_LABEL_LENGTH,
19
+ MAX_NOTE_LENGTH,
16
20
  MAX_OPTION_LABEL_LENGTH,
17
21
  MAX_OPTIONS,
18
22
  MAX_PROJECT_DESCRIPTION_LENGTH,
@@ -20,11 +24,14 @@ import {
20
24
  MAX_PROJECT_KEY_LENGTH,
21
25
  MAX_PROJECT_NAME_LENGTH,
22
26
  MAX_PROJECT_SYMBOL_LENGTH,
27
+ MAX_PROJECTS_PER_WORKSPACE,
23
28
  MAX_QUESTION_LENGTH,
24
29
  MAX_REPORTS_PER_RUN,
25
30
  MAX_RULES_LENGTH,
31
+ MAX_RUNS_PER_TASK,
26
32
  MAX_SEARCH_LENGTH,
27
33
  MAX_SUMMARY_LENGTH,
34
+ MAX_TASKS_PER_PROJECT,
28
35
  MAX_TITLE_LENGTH,
29
36
  MAX_VERSIONS_PER_DOCUMENT,
30
37
  MAX_WORKERS,
@@ -58,6 +65,7 @@ export interface CommandHelp {
58
65
  /** The refusals any command may meet, said once in the overview. */
59
66
  const COMMON_REFUSALS = [
60
67
  "auth.required: the token is not one the console knows, any more or at all; a person signs the command in again",
68
+ "auth.deactivated, auth.not_admitted: the person this token acts as is out, deactivated or no longer let in; the console keeps the token, and it works again when they are back in",
61
69
  "request.invalid: what was sent is over a limit or malformed; the message names the field",
62
70
  "network: the console could not be reached at its address",
63
71
  ];
@@ -147,9 +155,9 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
147
155
  work: {
148
156
  usage: "work [--person <login>]",
149
157
  synopsis:
150
- "What is on your plate across your projects: your open tasks, and the decisions waiting.",
158
+ "What is on your plate across your projects: your open tasks, and the decisions waiting for you.",
151
159
  about:
152
- "What is on your plate across the projects you may see: the open tasks you work on, the sooner due first, each with its project; and the decisions waiting for a person. Needs no project. --person <login> shows another person's plate, for an administrator or for an owner of a project they are in: the open tasks they work on within the projects you may see, without the decisions, which wait for anyone.",
160
+ "What is on your plate across the projects you may see: the open tasks you work on, the sooner due first, each with its project; and the decisions waiting for your answer (ADR-0042). Needs no project. --person <login> shows another person's plate, for an administrator or for an owner of a project they are in: the open tasks they work on within the projects you may see, without the decisions.",
153
161
  },
154
162
  tasks: {
155
163
  usage:
@@ -170,8 +178,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
170
178
  `title: one line, up to ${MAX_TITLE_LENGTH} characters, saying what is to be done on its own; what it refers to goes in the body`,
171
179
  `body: ${MARKDOWN}`,
172
180
  ...TASK_FIELD_LIMITS,
181
+ `at most ${MAX_TASKS_PER_PROJECT} tasks in one project, those put away counted`,
182
+ ],
183
+ refusals: [
184
+ "task.not_found: no task has that number or id",
185
+ ...TASK_FIELD_REFUSALS,
186
+ "task.too_many: the project holds as many tasks as one may; the work goes on in another project",
173
187
  ],
174
- refusals: ["task.not_found: no task has that number or id", ...TASK_FIELD_REFUSALS],
175
188
  },
176
189
  goals: {
177
190
  usage:
@@ -192,11 +205,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
192
205
  `title: one line, up to ${MAX_TITLE_LENGTH} characters`,
193
206
  `body: ${MARKDOWN}`,
194
207
  `starts, due: ${DAY}`,
208
+ `at most ${MAX_GOALS_PER_PROJECT} goals in one project`,
195
209
  ],
196
210
  refusals: [
197
211
  "goal.not_found: no goal of this project has that id",
198
212
  "goal.invalid_category: no category of the workspace has that key",
199
213
  "goal.invalid_dates: the target day is before the start",
214
+ "goal.too_many: the project holds as many goals as one may",
200
215
  "auth.forbidden: only an owner of the project sets its roadmap",
201
216
  ],
202
217
  },
@@ -258,11 +273,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
258
273
  `name: one line, up to ${MAX_PROJECT_NAME_LENGTH} characters; description: up to ${MAX_PROJECT_DESCRIPTION_LENGTH}`,
259
274
  `color: ${COLORS}; symbol: up to ${MAX_PROJECT_SYMBOL_LENGTH} characters without spaces, shown on the colour; left out, the pages choose both from the key and the name`,
260
275
  `image: a PNG, a JPEG, a GIF or a WebP of at most ${MAX_PROJECT_ICON_BYTES} bytes`,
276
+ `at most ${MAX_PROJECTS_PER_WORKSPACE} projects in the workspace`,
261
277
  ],
262
278
  refusals: [
263
279
  "project.not_found: no project has that key, or it is not yours to see",
264
- "auth.forbidden: only an owner of the project changes it",
280
+ "auth.forbidden: only an administrator makes a project, and only an owner changes it",
265
281
  "project.key_taken: a project has that key already",
282
+ "project.too_many: the workspace holds as many projects as one may",
266
283
  "project.invalid_address: the skills address is not one as SkillCDN spells it",
267
284
  "project.invalid_icon: the file is not an image of those kinds, by its bytes",
268
285
  "request.too_large: the image is over the limit",
@@ -313,11 +330,11 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
313
330
  },
314
331
  skills: {
315
332
  usage:
316
- "skills | skills <folder> | skills --search <words> [<folder>] [--limit <n>] [--cursor <cursor>]",
333
+ "skills | skills <folder|file> | skills --search <words> [<folder>] [--limit <n>] [--cursor <cursor>]",
317
334
  synopsis:
318
- "The project's pack: its skills; what a folder of it holds; or what words find in it.",
335
+ "The project's pack: its skills; what a folder of it holds, or a document's outline; or what words find in it.",
319
336
  about:
320
- "The project's pack, read by the console at the project's address, or the organization's: at a SkillCDN deployment, with the pack token the project holds where the pack is private (ADR-0028), or from a directory on the console's machine for a local address (ADR-0031). Alone: where the pack is and what a client is told of it on connect, then every skill with the path of its SKILL.md and every document, as a connection of your own would list them, and where the console's own skills are on this machine. With a folder (. for the root): what it holds, whole, as browse_repo lays it out for a model: its skills, its folders with what each holds and the README that introduces it, its files. With --search: what the words find, as search_repo does: skills first, with the files of theirs that matched, then documents, in relevance order; words, not meaning, in the language the pack is written in; within the folder when one is named; 10 unless --limit says otherwise, up to 25, and a nextCursor line says when more matched, which --cursor continues. The text is the one an MCP client's model is given, with the reads named as these commands. --json answers with the console's own JSON: what the pack answered, in the shapes of the main repository's REST API, the pages of a folder joined.",
337
+ "The project's pack, read by the console at the project's address, or the organization's: at a SkillCDN deployment, with the pack token the project holds where the pack is private (ADR-0028), or from a directory on the console's machine for a local address (ADR-0031). Alone: where the pack is and what a client is told of it on connect, then every skill with the path of its SKILL.md and every document, as a connection of your own would list them, and where the console's own skills are on this machine. With a folder (. for the root): what it holds, whole, as browse_repo lays it out for a model: its skills, its folders with what each holds and the README that introduces it, its files. With a file's path: the file's outline, every heading with its level and the offset and limit that read its section with console skill --file, whole; a file that is not Markdown has none. With --search: what the words find, as search_repo does: skills first, with the files of theirs that matched, then documents, each with the sections of it that matched and what reads each, in relevance order; words in the language the pack is written in, and meaning too where the deployment searches by it, which the result then says; within the folder when one is named; 10 unless --limit says otherwise, up to 25, and a nextCursor line says when more matched, which --cursor continues. The text is the one an MCP client's model is given, with the reads named as these commands. --json answers with the console's own JSON: what the pack answered, in the shapes of the main repository's REST API, the pages of a folder or an outline joined.",
321
338
  limits: ["search: 1 to 500 characters; limit: 1 to 25"],
322
339
  refusals: [
323
340
  "status not_found: the pack is not served to the console; private, with no pack token on the project",
@@ -325,11 +342,12 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
325
342
  ],
326
343
  },
327
344
  skill: {
328
- usage: "skill <path> | skill --file <path>",
345
+ usage: "skill <path> | skill --file <path> [--offset <n>] [--limit <n>]",
329
346
  synopsis:
330
- "One skill of the pack whole, its rules first; or with --file a file of the pack, or what a folder holds.",
347
+ "One skill of the pack whole, its rules first; or with --file a file of the pack, one section of it, or what a folder holds.",
331
348
  about:
332
- "One skill of the project's pack, whole, as load_skill lays it out for a model: the rules of the repository from its root to the nearest manifest, the skill's instructions, the files it includes, then its supporting files and what it refers to; the pages it came in are joined. Read by the console from the pack, with the pack token the project holds where the pack is private, so you need no connection of your own. <path> is the path of the SKILL.md from the repository's root, as console skills lists it, or the URI the pack names it by (skill://<host>/<owner>/<name>/<path>). --file <path> reads a file of the pack instead, whole, as read_repo_file does, or what a folder holds, and takes no skill's path beside it; console skills <folder> shows a folder with more. --json answers with what the pack answered, in the shapes of the main repository's REST API, the pages joined.",
349
+ "One skill of the project's pack, whole, as load_skill lays it out for a model: the rules of the repository from its root to the nearest manifest, the skill's instructions, the files it includes, then its supporting files and what it refers to; the pages it came in are joined. Read by the console from the pack, with the pack token the project holds where the pack is private, so you need no connection of your own. <path> is the path of the SKILL.md from the repository's root, as console skills lists it, or the URI the pack names it by (skill://<host>/<owner>/<name>/<path>). --file <path> reads a file of the pack instead, whole, as read_repo_file does, or what a folder holds, and takes no skill's path beside it; console skills <folder> shows a folder with more. --offset <n> and --limit <n>, with --file, read one page of the file as asked, from that character on and at most that many characters, as a search or an outline (console skills <file>) names a section, instead of the file whole; the result says where the file continues. --json answers with what the pack answered, in the shapes of the main repository's REST API, the pages joined.",
350
+ limits: ["offset: 0 or more; limit: 1 to 100000 characters; both with --file"],
333
351
  refusals: [
334
352
  "status not_found: the pack is not served to the console; private, with no pack token on the project",
335
353
  "status token_invalid: the pack token the project holds no longer works; an owner sets a new one",
@@ -356,10 +374,14 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
356
374
  synopsis: "Takes a task: a run begins, and the task is in progress and the person's.",
357
375
  about:
358
376
  "Takes a task: a run begins, and the task is in progress and the person's; the run's id and the task's id are printed on a line of their own, then the task, then the rules of the board. A run still open on the task is taken over, since the board cannot know whether its agent still runs: it ends as abandoned, the feed says who took over, and that agent is refused on its next command; console task <number> shows the runs on a task, with when each was last heard from, before it is taken. --agent is what people see at work; left out, it is what the person called the token.",
359
- limits: [`agent: one line, up to ${MAX_AGENT_LENGTH} characters`],
377
+ limits: [
378
+ `agent: one line, up to ${MAX_AGENT_LENGTH} characters`,
379
+ `at most ${MAX_RUNS_PER_TASK} runs on one task`,
380
+ ],
360
381
  refusals: [
361
382
  "task.not_found: no task has that number or id",
362
383
  "run.task_closed: the task is done or dropped",
384
+ "run.too_many: the task has been taken as many times as one may; write a new task for what is left",
363
385
  ],
364
386
  },
365
387
  report: {
@@ -389,22 +411,45 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
389
411
  ...RUN_REFUSALS,
390
412
  "run.too_many_artifacts: the run carries as many as one may",
391
413
  "request.too_large: the file is over the limit",
414
+ "project.too_many_bytes: the project's files hold as many bytes together as this console allows; hand in a link instead",
392
415
  ],
393
416
  },
394
417
  ask: {
395
418
  usage:
396
- 'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--wait <seconds>] [--run <id>]',
419
+ 'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--to <login|me>] [--wait <seconds>] [--run <id>]',
397
420
  overview:
398
- 'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--wait <seconds>]',
421
+ 'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--to <login|me>] [--wait <seconds>]',
399
422
  synopsis:
400
423
  "Raises a decision a person must make, with options, and waits a while for the answer.",
401
- about: `Raises a decision a person must make: the question, the options to choose from, and what they need to know. The run waits for the answer. The command waits 60 seconds for it unless --wait says otherwise, and exits with 3 if it is still to come: keep waiting with \`console decision <id> --wait 100\`, within what your shell gives one command. The decision's id is printed as soon as it is raised. The person chooses one of the options, or answers in their own words: the answer then names one more option, their words, marked own; act on it as given. ${ON_THE_RUN}`,
424
+ about: `Raises a decision a person must make: the question, the options to choose from, and what they need to know. It is for the person you work for unless --to names another of the project, who then finds it among their decisions (ADR-0042); anyone of the project may still answer. The run waits for the answer. The command waits 60 seconds for it unless --wait says otherwise, and exits with 3 if it is still to come: keep waiting with \`console decision <id> --wait 100\`, within what your shell gives one command. The decision's id is printed as soon as it is raised. The person chooses one of the options, or answers in their own words: the answer then names one more option, their words, marked own; act on it as given. ${ON_THE_RUN}`,
402
425
  limits: [
403
426
  `question: one line, up to ${MAX_QUESTION_LENGTH} characters`,
404
427
  `options: ${MIN_OPTIONS} to ${MAX_OPTIONS}, each one line up to ${MAX_OPTION_LABEL_LENGTH} characters`,
405
428
  `body: ${MARKDOWN}`,
429
+ "to: a login of the project, or me",
430
+ `at most ${MAX_DECISIONS_PER_PROJECT} decisions in one project`,
431
+ ],
432
+ refusals: [
433
+ ...RUN_CHOICE,
434
+ "decision.invalid_run: the run is not yours, or is over",
435
+ "decision.invalid_decider: the person named may not work in the project",
436
+ "decision.too_many: the project holds as many decisions as one may",
437
+ ],
438
+ },
439
+ answer: {
440
+ usage: 'answer <id> --option <n>|--own "<answer>" [--note <markdown>|--file <path>]',
441
+ synopsis: "Writes a person's answer on a decision: the option they chose, or their own words.",
442
+ about:
443
+ "Writes on the board the answer a person gave: one of the decision's options by its number, or their own words, which become one more option of the decision marked as theirs; and a note about it. The answer is the person's, written in their name through you (ADR-0021): a choice they made in chat, in a meeting or by changing the task is written here, with where and when they made it in the note, so that the record is whole and a run that waited goes on. A decision is answered once; what followed is written with console decision <id> --outcome.",
444
+ limits: [
445
+ `own: one line, up to ${MAX_OPTION_LABEL_LENGTH} characters`,
446
+ `note: Markdown, up to ${MAX_NOTE_LENGTH} characters`,
447
+ ],
448
+ refusals: [
449
+ "decision.not_found: no decision has that id",
450
+ "decision.answered: the decision has an answer already",
451
+ "decision.no_such_option: the option is not one of the decision's",
406
452
  ],
407
- refusals: [...RUN_CHOICE, "decision.invalid_run: the run is not yours, or is over"],
408
453
  },
409
454
  decision: {
410
455
  usage: "decision <id> [--wait <seconds>] | decision <id> --outcome <markdown>|--file <path>",
@@ -416,9 +461,9 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
416
461
  refusals: ["decision.not_found: no decision has that id"],
417
462
  },
418
463
  decisions: {
419
- usage: "decisions [--open] [--limit <n>] [--offset <n>]",
420
- synopsis: "The decisions on the board, the waiting first.",
421
- about: `The decisions on the board, the waiting first; --open keeps only those. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on.`,
464
+ usage: "decisions [--open] [--mine|--raised] [--limit <n>] [--offset <n>]",
465
+ synopsis: "The decisions on the board, the waiting first; or your own across the projects.",
466
+ about: `The decisions on the board, the waiting first; --open keeps only those. --mine lists the decisions addressed to you instead (ADR-0042), and --raised those you asked of someone else, through your agent or yourself, each across the projects you may see with its project's key; neither needs a project. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on.`,
422
467
  },
423
468
  finish: {
424
469
  usage: 'finish [--summary "<markdown>"|--file <path>] [--run <id>]',
@@ -472,6 +517,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
472
517
  "comment.too_many: the task carries as many comments as one may",
473
518
  "comment.too_many_files: more files than one comment may carry",
474
519
  "request.too_large: the files are over the limit together",
520
+ "project.too_many_bytes: the project's files hold as many bytes together as this console allows; say where the file is instead",
475
521
  ],
476
522
  },
477
523
  file: {
@@ -503,13 +549,14 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
503
549
  `path: ${DOCUMENT_PATH}`,
504
550
  `title: one line, up to ${MAX_TITLE_LENGTH} characters`,
505
551
  `body: Markdown, up to ${MAX_DOCUMENT_LENGTH} characters`,
506
- `at most ${MAX_VERSIONS_PER_DOCUMENT} versions of one page`,
552
+ `at most ${MAX_VERSIONS_PER_DOCUMENT} versions of one page, and ${MAX_DOCUMENTS_PER_PROJECT} pages in one project`,
507
553
  ],
508
554
  refusals: [
509
555
  "document.invalid_path: the path is not one",
510
556
  "document.conflict: the page has moved on since --base; read it again",
511
557
  "document.archived: the page is archived; restore it first",
512
558
  "document.too_many_versions: the page carries as many versions as one may; write a new page",
559
+ "document.too_many: the project holds as many pages as one may; write on a page there is",
513
560
  ],
514
561
  },
515
562
  attach: {
@@ -526,6 +573,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
526
573
  "document.archived: the page is archived; restore it first",
527
574
  "document.too_many_files: the page carries as many files as one may",
528
575
  "request.too_large: the file is over the limit",
576
+ "project.too_many_bytes: the project's files hold as many bytes together as this console allows; link to where the file is instead",
529
577
  ],
530
578
  },
531
579
  archive: {
@@ -622,7 +670,18 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
622
670
  },
623
671
  {
624
672
  title: "Working",
625
- commands: ["take", "report", "hand-in", "comment", "file", "ask", "finish", "fail", "abandon"],
673
+ commands: [
674
+ "take",
675
+ "report",
676
+ "hand-in",
677
+ "comment",
678
+ "file",
679
+ "ask",
680
+ "answer",
681
+ "finish",
682
+ "fail",
683
+ "abandon",
684
+ ],
626
685
  },
627
686
  {
628
687
  title: "The documents: the project's pages of Markdown, in folders, by path",