@skillcdn/console 0.1.23 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +6 -5
  2. package/dist/api.d.ts +1 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +1 -1
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +80 -54
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +4 -28
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +7 -260
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts +6 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +47 -24
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/pack.d.ts +46 -0
  18. package/dist/cli/pack.d.ts.map +1 -0
  19. package/dist/cli/pack.js +250 -0
  20. package/dist/cli/pack.js.map +1 -0
  21. package/dist/compat.d.ts +1 -1
  22. package/dist/compat.js +1 -1
  23. package/dist/components/decision-list.d.ts +13 -1
  24. package/dist/components/decision-list.d.ts.map +1 -1
  25. package/dist/components/decision-list.js +74 -29
  26. package/dist/components/decision-list.js.map +1 -1
  27. package/dist/components/paged.d.ts +24 -0
  28. package/dist/components/paged.d.ts.map +1 -1
  29. package/dist/components/paged.js +40 -4
  30. package/dist/components/paged.js.map +1 -1
  31. package/dist/components/skills.d.ts.map +1 -1
  32. package/dist/components/skills.js +8 -4
  33. package/dist/components/skills.js.map +1 -1
  34. package/dist/components/ui.d.ts +2 -0
  35. package/dist/components/ui.d.ts.map +1 -1
  36. package/dist/components/ui.js +7 -2
  37. package/dist/components/ui.js.map +1 -1
  38. package/dist/components/work.d.ts +2 -0
  39. package/dist/components/work.d.ts.map +1 -1
  40. package/dist/components/work.js +3 -2
  41. package/dist/components/work.js.map +1 -1
  42. package/dist/console.css +112 -5
  43. package/dist/console.d.ts.map +1 -1
  44. package/dist/console.js +5 -3
  45. package/dist/console.js.map +1 -1
  46. package/dist/data.d.ts.map +1 -1
  47. package/dist/data.js +27 -2
  48. package/dist/data.js.map +1 -1
  49. package/dist/i18n/en.d.ts +13 -0
  50. package/dist/i18n/en.d.ts.map +1 -1
  51. package/dist/i18n/en.js +15 -0
  52. package/dist/i18n/en.js.map +1 -1
  53. package/dist/i18n/ko.d.ts.map +1 -1
  54. package/dist/i18n/ko.js +15 -0
  55. package/dist/i18n/ko.js.map +1 -1
  56. package/dist/index.d.ts +2 -2
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +2 -2
  59. package/dist/index.js.map +1 -1
  60. package/dist/router.d.ts +6 -1
  61. package/dist/router.d.ts.map +1 -1
  62. package/dist/router.js +13 -7
  63. package/dist/router.js.map +1 -1
  64. package/dist/rules.js +3 -3
  65. package/dist/rules.js.map +1 -1
  66. package/dist/schemas.d.ts +557 -162
  67. package/dist/schemas.d.ts.map +1 -1
  68. package/dist/schemas.js +72 -158
  69. package/dist/schemas.js.map +1 -1
  70. package/docs/specs/cli.md +12 -12
  71. package/docs/specs/rest.md +7 -15
  72. package/package.json +2 -1
  73. package/skills/deploying-a-console/SKILL.md +2 -2
  74. package/skills/working-the-board/SKILL.md +9 -14
  75. package/skills/working-the-board/references/setting-up.md +3 -0
  76. package/skills/working-the-board/references/terms.md +9 -0
  77. package/skills/working-the-board/references/the-sentence.md +5 -0
  78. package/src/api.ts +2 -7
  79. package/src/cli/cli.ts +113 -62
  80. package/src/cli/format.ts +10 -324
  81. package/src/cli/help.ts +53 -27
  82. package/src/cli/pack.ts +356 -0
  83. package/src/compat.ts +1 -1
  84. package/src/components/decision-list.tsx +227 -94
  85. package/src/components/paged.tsx +72 -3
  86. package/src/components/skills.tsx +51 -33
  87. package/src/components/ui.tsx +16 -3
  88. package/src/components/work.tsx +9 -5
  89. package/src/console.tsx +8 -2
  90. package/src/data.ts +32 -2
  91. package/src/i18n/en.ts +18 -0
  92. package/src/i18n/ko.ts +17 -0
  93. package/src/index.ts +9 -1
  94. package/src/router.ts +20 -9
  95. package/src/rules.ts +3 -3
  96. package/src/schemas.ts +87 -184
  97. package/src/styles/console.css +112 -5
package/src/cli/format.ts CHANGED
@@ -15,13 +15,6 @@ import type {
15
15
  RestProjectRules,
16
16
  RestRules,
17
17
  RestRun,
18
- RestSkillBrowse,
19
- RestSkillBrowseEntry,
20
- RestSkillFile,
21
- RestSkillFind,
22
- RestSkillLoad,
23
- RestSkillPage,
24
- RestSkills,
25
18
  RestTask,
26
19
  RestVersion,
27
20
  RestVersionSummary,
@@ -247,66 +240,6 @@ export function formatArtifact(
247
240
  return artifact.label === null ? what : `${artifact.label}: ${what}`;
248
241
  }
249
242
 
250
- const SKILLS_WORDS: Readonly<Record<Exclude<RestSkills["status"], "none" | "ready">, string>> = {
251
- indexing: "SkillCDN is still indexing the repository; ask again in a moment.",
252
- failed: "SkillCDN could not index the repository.",
253
- not_found:
254
- "SkillCDN does not serve this address to the console: nothing is there, or the pack is private and the project holds no pack token for it (console project pack-token <key> --stdin, for an owner).",
255
- token_invalid:
256
- "The pack token the project holds no longer works at SkillCDN: an owner sets a new one (console project pack-token <key> --stdin).",
257
- unavailable: "SkillCDN could not be reached; ask again later.",
258
- };
259
-
260
- /** A count with its noun: `1 skill`, `2 skills`. */
261
- const counted = (n: number, noun: string): string => `${n} ${noun}${n === 1 ? "" : "s"}`;
262
-
263
- /**
264
- * The organization's skills: where they are, the pack by name and by count, each skill with
265
- * what an agent loads it by, where a person connects an agent that has no connection to the
266
- * address, and where the console's own skills are on this machine, when the command knows.
267
- */
268
- export function formatSkills(skills: RestSkills, ownSkills?: string): string {
269
- const own =
270
- ownSkills === undefined
271
- ? []
272
- : ["", `The console's own skills, for working the board: ${ownSkills}`];
273
- if (skills.status === "none" || skills.address === null || skills.page === null) {
274
- return ["No skills address: neither this project nor the console names one.", ...own].join(
275
- "\n",
276
- );
277
- }
278
- const head = `skills at ${skills.address}, served by ${skills.source}: ${skills.status}`;
279
- if (skills.status !== "ready") {
280
- return [head, SKILLS_WORDS[skills.status], ...own].join("\n");
281
- }
282
- const lines = [head];
283
- if (skills.pack !== undefined) {
284
- const named = [skills.pack.name, skills.pack.description]
285
- .filter((text): text is string => text !== null)
286
- .join(" ");
287
- if (named !== "") {
288
- lines.push(named);
289
- }
290
- }
291
- if (skills.counts !== undefined) {
292
- lines.push(
293
- `${counted(skills.counts.skills, "skill")}, ${counted(skills.counts.documents, "document")}: console skills <folder> shows what a folder holds (. for the root), console skills --search <words> finds skills and documents by words.`,
294
- );
295
- }
296
- if (skills.items.length === 0) {
297
- lines.push("No skills at this address.");
298
- }
299
- for (const skill of skills.items) {
300
- lines.push(`${skill.name} ${skill.description}`, ` ${skill.uri ?? skill.path}`);
301
- }
302
- lines.push(
303
- "",
304
- `Read a skill whole with console skill <path>, its rules first, and a file of the pack with console skill --file <path>; or load it by its URI through your own SkillCDN connection to this address, which a person makes at ${skills.page}. --json adds the page of each, for a person.`,
305
- ...own,
306
- );
307
- return lines.join("\n");
308
- }
309
-
310
243
  /** One half of the rules: where it comes from, and the text as it stands. */
311
244
  function formatRulesHalf(where: "the workspace" | "this project", rules: RestRules): string {
312
245
  const page = rules.page;
@@ -413,19 +346,25 @@ export function formatDecisionLine(decision: RestDecision): string {
413
346
  return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)})`;
414
347
  }
415
348
 
349
+ /** The options, one a line; the person's own answer, when they wrote one, is the last and says so. */
416
350
  export function formatOptions(decision: RestDecision): string {
417
- return decision.options.map((option) => ` ${option.id}) ${option.label}`).join("\n");
351
+ return decision.options
352
+ .map(
353
+ (option) =>
354
+ ` ${option.id}) ${option.label}${option.own === true ? " (the person's own answer, not one given)" : ""}`,
355
+ )
356
+ .join("\n");
418
357
  }
419
358
 
420
- /** The answer, with who gave it; or that there is none yet. */
359
+ /** The answer, with who gave it, and that it is in their own words when it is; or that there is none yet. */
421
360
  export function formatAnswer(decision: RestDecision): string {
422
361
  const { answer } = decision;
423
362
  if (answer === null) {
424
363
  return "waits for a person.";
425
364
  }
426
- const label = decision.options.find((option) => option.id === answer.option)?.label;
365
+ const chosen = decision.options.find((option) => option.id === answer.option);
427
366
  const lines = [
428
- `Answered: ${answer.option}) ${label ?? answer.option}, by ${answer.by.login} at ${answer.at}`,
367
+ `Answered: ${answer.option}) ${chosen?.label ?? answer.option}${chosen?.own === true ? ", in their own words" : ""}, by ${answer.by.login} at ${answer.at}`,
429
368
  ];
430
369
  if (answer.note !== null && answer.note.length > 0) {
431
370
  lines.push(`Note: ${answer.note}`);
@@ -527,256 +466,3 @@ export function formatVersion(path: string, version: RestVersion): string {
527
466
  }
528
467
 
529
468
  /** The text of one path across the pages a skill came in: the fragments joined in order. */
530
- function joinTexts(
531
- pages: readonly RestSkillPage[],
532
- pick: (page: RestSkillPage) => readonly { readonly path: string; readonly body: string }[],
533
- ): { readonly path: string; readonly body: string }[] {
534
- const joined = new Map<string, string>();
535
- for (const page of pages) {
536
- for (const text of pick(page)) {
537
- joined.set(text.path, (joined.get(text.path) ?? "") + text.body);
538
- }
539
- }
540
- return [...joined.entries()].map(([path, body]) => ({ path, body }));
541
- }
542
-
543
- const NO_ADDRESS = "No skills address: neither this project nor the console names one.";
544
-
545
- /**
546
- * One skill, as load_skill lays it out for a model (ADR-0028): the rules from the repository's
547
- * root to the nearest manifest, the body, the included files, each under a line that names its
548
- * path; then the files the skill has and what it refers to. A skill described only is named and
549
- * pointed at, and nothing of its body is shown.
550
- */
551
- export function formatSkillLoad(loaded: RestSkillLoad, pages: readonly RestSkillPage[]): string {
552
- if (loaded.status === "none" || loaded.address === null) {
553
- return NO_ADDRESS;
554
- }
555
- const head = `skill at ${loaded.address}, served by ${loaded.source}: ${loaded.status}`;
556
- if (loaded.status !== "ready") {
557
- return [head, SKILLS_WORDS[loaded.status]].join("\n");
558
- }
559
- const [first] = pages;
560
- if (first === undefined) {
561
- return [head, "Nothing was read."].join("\n");
562
- }
563
- const last = pages[pages.length - 1] ?? first;
564
- const lines = [
565
- `${first.name} ${first.description}`,
566
- `skill ${first.path} at ${loaded.address}, served by ${loaded.source}`,
567
- ];
568
- if (first.serving.license !== null) {
569
- lines.push(`license: ${first.serving.license}`);
570
- }
571
- if (!first.serving.full) {
572
- lines.push(
573
- "",
574
- `Described only: the license keeps the skill at its source. Read it there: ${first.serving.sourceUrl ?? loaded.page ?? loaded.source}`,
575
- );
576
- return lines.join("\n");
577
- }
578
- for (const rule of joinTexts(pages, (page) => page.rules)) {
579
- lines.push("", `--- applicable rules: ${rule.path} ---`, rule.body.trimEnd());
580
- }
581
- lines.push(
582
- "",
583
- `--- skill: ${first.path} ---`,
584
- pages
585
- .map((page) => page.body)
586
- .join("")
587
- .trimEnd(),
588
- );
589
- for (const file of joinTexts(pages, (page) => page.included)) {
590
- lines.push("", `--- included file: ${file.path} ---`, file.body.trimEnd());
591
- }
592
- if (!last.complete) {
593
- lines.push("", "The skill did not come whole: the deployment stopped before the end.");
594
- }
595
- if (last.files.length > 0) {
596
- lines.push(
597
- "",
598
- `Files of the skill, read with console skill --file <path>: ${last.files.join(", ")}`,
599
- );
600
- }
601
- if (last.references.length > 0) {
602
- lines.push(
603
- `References: ${last.references
604
- .map((reference) =>
605
- reference.status === "available"
606
- ? reference.path
607
- : `${reference.path} (${reference.status})`,
608
- )
609
- .join(", ")}`,
610
- );
611
- }
612
- if (last.warnings.length > 0) {
613
- lines.push(`Warnings: ${last.warnings.join("; ")}`);
614
- }
615
- lines.push(
616
- "",
617
- "Paths from the repository's root are the only identifiers. The content is the pack's, under its license; what a private pack says stays in your work.",
618
- );
619
- return lines.join("\n");
620
- }
621
-
622
- /** A file of the pack as read: its text whole, or what the folder holds. */
623
- export function formatSkillFile(read: RestSkillFile, content: string): string {
624
- if (read.status === "none" || read.address === null) {
625
- return NO_ADDRESS;
626
- }
627
- const head = `file at ${read.address}, served by ${read.source}: ${read.status}`;
628
- if (read.status !== "ready") {
629
- return [
630
- head,
631
- read.status === "failed"
632
- ? "SkillCDN will not carry this file: it is too large, or not text. Read it at the repository."
633
- : SKILLS_WORDS[read.status],
634
- ].join("\n");
635
- }
636
- if (read.folder !== null) {
637
- return [
638
- `folder ${read.folder.path} at ${read.address}${read.folder.truncated ? " (cut)" : ""}`,
639
- ...read.folder.entries.map(
640
- (entry) =>
641
- `${entry.kind === "directory" ? "d" : "f"} ${entry.path}${entry.size === null ? "" : ` ${entry.size}`}`,
642
- ),
643
- "",
644
- "console skills <folder> shows a folder with its skills marked, what each folder holds and the README that introduces it.",
645
- ].join("\n");
646
- }
647
- if (read.file === null) {
648
- return `${head}: nothing was read.`;
649
- }
650
- return [
651
- `--- file: ${read.file.path} (${read.file.totalLength} characters) ---`,
652
- content.trimEnd(),
653
- ].join("\n");
654
- }
655
-
656
- /** One entry of a browsed folder: what it is, its path, its words, and the facts of its kind. */
657
- function formatBrowseEntry(entry: RestSkillBrowseEntry): string {
658
- const facts: string[] = [];
659
- if (entry.kind === "directory") {
660
- if (entry.skillCount > 0) {
661
- facts.push(counted(entry.skillCount, "skill"));
662
- }
663
- if (entry.documentCount > 0) {
664
- facts.push(counted(entry.documentCount, "document"));
665
- }
666
- if (entry.overviewPath !== null) {
667
- facts.push(`README ${entry.overviewPath}`);
668
- }
669
- }
670
- if (entry.kind === "skill") {
671
- if (entry.license !== null) {
672
- facts.push(entry.license);
673
- }
674
- if (entry.describedOnly) {
675
- facts.push("described only");
676
- }
677
- }
678
- if (entry.kind === "file" && entry.size !== null) {
679
- facts.push(`${entry.size} bytes`);
680
- }
681
- const words = [entry.name, entry.description]
682
- .filter((text): text is string => text !== null && text !== "")
683
- .join(" ");
684
- const mark = entry.kind === "skill" ? "skill" : entry.kind === "directory" ? "d" : "f";
685
- return `${mark} ${entry.path}${words === "" ? "" : ` ${words}`}${facts.length === 0 ? "" : ` (${facts.join("; ")})`}`;
686
- }
687
-
688
- /**
689
- * What a folder of the pack holds, as browse_repo lays it out: the README that introduces the
690
- * folder, then its skills, its folders with what each holds, and its files, across the pages the
691
- * listing came in.
692
- */
693
- export function formatSkillBrowse(
694
- browsed: RestSkillBrowse,
695
- folders: readonly NonNullable<RestSkillBrowse["folder"]>[],
696
- ): string {
697
- if (browsed.status === "none" || browsed.address === null) {
698
- return NO_ADDRESS;
699
- }
700
- const head = `folder at ${browsed.address}, served by ${browsed.source}: ${browsed.status}`;
701
- if (browsed.status !== "ready") {
702
- return [head, SKILLS_WORDS[browsed.status]].join("\n");
703
- }
704
- const [first] = folders;
705
- if (first === undefined) {
706
- return [head, "Nothing was read."].join("\n");
707
- }
708
- const last = folders[folders.length - 1] ?? first;
709
- const lines = [`folder ${first.path === "" ? "/" : first.path} at ${browsed.address}`];
710
- if (first.overview !== null) {
711
- const words = [first.overview.title, first.overview.description]
712
- .filter((text): text is string => text !== null && text !== "")
713
- .join(" ");
714
- lines.push(` ${words === "" ? "" : `${words} `}(README: ${first.overview.path})`);
715
- }
716
- const entries = folders.flatMap((folder) => folder.entries);
717
- if (entries.length === 0) {
718
- lines.push("Nothing in this folder.");
719
- }
720
- for (const entry of entries) {
721
- lines.push(formatBrowseEntry(entry));
722
- }
723
- if (last.nextCursor !== null) {
724
- lines.push("More entries: the listing stopped before the end of the folder.");
725
- }
726
- lines.push(
727
- "",
728
- "Read a skill with console skill <path>; a file, or a README named above, with console skill --file <path>; a folder with console skills <folder>.",
729
- );
730
- return lines.join("\n");
731
- }
732
-
733
- /**
734
- * What words found in the pack, as search_repo lays it out: skills with the files of theirs that
735
- * matched, documents on their own, in relevance order; words, not meaning.
736
- */
737
- export function formatSkillFind(found: RestSkillFind): string {
738
- if (found.status === "none" || found.address === null) {
739
- return NO_ADDRESS;
740
- }
741
- const head = `search at ${found.address}, served by ${found.source}: ${found.status}`;
742
- if (found.status !== "ready") {
743
- return [head, SKILLS_WORDS[found.status]].join("\n");
744
- }
745
- if (found.found === null) {
746
- return [head, "Nothing was read."].join("\n");
747
- }
748
- const { query, path, items, nextCursor } = found.found;
749
- const where = path === null || path === "" ? "" : ` within ${path}`;
750
- const lines = [
751
- `found for "${query}"${where} at ${found.address}: ${counted(items.length, "result")}`,
752
- ];
753
- for (const item of items) {
754
- if (item.kind === "skill") {
755
- lines.push(
756
- `skill ${item.path} ${item.name} ${item.description}${item.describedOnly ? " (described only)" : ""}`,
757
- );
758
- for (const file of item.files) {
759
- lines.push(` file ${file.path}${file.title === null ? "" : ` ${file.title}`}`);
760
- }
761
- if (item.moreFiles > 0) {
762
- lines.push(` and ${counted(item.moreFiles, "more file")}`);
763
- }
764
- } else {
765
- lines.push(
766
- `document ${item.path}${item.title === null ? "" : ` ${item.title}`}${item.summary === null ? "" : ` ${item.summary}`}`,
767
- );
768
- }
769
- }
770
- if (nextCursor !== null) {
771
- lines.push(
772
- "More matched: narrow the words, or search within a folder (console skills <folder> --search <words>).",
773
- );
774
- }
775
- if (items.length > 0) {
776
- lines.push(
777
- "",
778
- "Read a skill with console skill <path>, a file or a document with console skill --file <path>.",
779
- );
780
- }
781
- return lines.join("\n");
782
- }
package/src/cli/help.ts CHANGED
@@ -60,11 +60,21 @@ const COMMON_REFUSALS = [
60
60
  "network: the console could not be reached at its address",
61
61
  ];
62
62
 
63
+ /** How a command on a run finds its run, and what it says when it cannot: the command's own refusals. */
64
+ const RUN_CHOICE = [
65
+ "run.none: no run of yours is open in the project; take a task first, or name a run with --run <id>",
66
+ "run.ambiguous: several runs of yours are open in the project; say which with --run <id>",
67
+ ];
68
+
63
69
  const RUN_REFUSALS = [
70
+ ...RUN_CHOICE,
64
71
  "run.not_yours: the run is another person's",
65
72
  "run.over: the run has ended already",
66
73
  ];
67
74
 
75
+ /** What every command on a run says of the run it acts on. */
76
+ const ON_THE_RUN = "On your one open run in the project, or the one --run <id> names.";
77
+
68
78
  const MARKDOWN = `Markdown, up to ${MAX_BODY_LENGTH} characters`;
69
79
 
70
80
  const DOCUMENT_PATH = `a path of lowercase letters, digits and hyphens, with slashes between its folders (guides/onboarding), up to ${MAX_DOCUMENT_PATH_LENGTH} characters; it does not change once written`;
@@ -275,11 +285,12 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
275
285
  ],
276
286
  },
277
287
  skills: {
278
- usage: "skills | skills <folder> | skills --search <words> [<folder>] [--limit <n>]",
288
+ usage:
289
+ "skills | skills <folder> | skills --search <words> [<folder>] [--limit <n>] [--cursor <cursor>]",
279
290
  synopsis:
280
291
  "The project's pack: its skills; what a folder of it holds; or what words find in it.",
281
292
  about:
282
- "The project's pack, as SkillCDN serves it at the project's address, read by the console with the pack token the project holds where the pack is private (ADR-0028). Alone: the pack by name with how many skills and documents it has, and its skills, each with its description, the path of its SKILL.md, which console skill reads it by, and the URI you load it by through your own SkillCDN connection where your host has one; and where the console's own skills are on this machine. With a folder (. for the root): what it holds, as browse_repo lays it out: its skills, its folders with how many skills and documents each holds and the README that introduces it, its files; whole, page after page. With --search: the skills and the documents the words find, in relevance order, a skill with the files of it that matched; 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 last line says when more matched. --json answers with the console's own JSON, the page of each skill included.",
293
+ "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.",
283
294
  limits: ["search: 1 to 500 characters; limit: 1 to 25"],
284
295
  refusals: [
285
296
  "status not_found: the pack is not served to the console; private, with no pack token on the project",
@@ -291,11 +302,11 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
291
302
  synopsis:
292
303
  "One skill of the pack whole, its rules first; or with --file a file of the pack, or what a folder holds.",
293
304
  about:
294
- "One skill of the project's pack, whole, as load_skill lays it out for you: the rules of the repository from its root to the nearest manifest, the skill's body, the files it includes, each under a line that names its path; then the files it has and what it refers to. Read by the console from the SkillCDN deployment, 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. --file <path> reads a file of the pack instead, whole, or what a folder holds, and takes no skill's path beside it; console skills <folder> shows a folder with more.",
305
+ "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.",
295
306
  refusals: [
296
307
  "status not_found: the pack is not served to the console; private, with no pack token on the project",
297
308
  "status token_invalid: the pack token the project holds no longer works; an owner sets a new one",
298
- "status failed, for a file: SkillCDN will not carry it, too large or not text",
309
+ "status failed, for a file: the pack will not carry it, too large or not text",
299
310
  ],
300
311
  },
301
312
  rules: {
@@ -325,10 +336,11 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
325
336
  ],
326
337
  },
327
338
  report: {
328
- usage: "report <markdown> | report --file <path> | report -",
339
+ usage:
340
+ "report <markdown> [--run <id>] | report --file <path> [--run <id>] | report - [--run <id>]",
341
+ overview: "report <markdown> | report --file <path> | report -",
329
342
  synopsis: "Says how the work goes, in Markdown, on the open run.",
330
- about:
331
- "Says how the work goes, in Markdown: what was found, what was done, what is next. With - the report is read from standard input.",
343
+ about: `Says how the work goes, in Markdown: what was found, what was done, what is next. With - the report is read from standard input. ${ON_THE_RUN}`,
332
344
  limits: [
333
345
  `the report: ${MARKDOWN}, not blank`,
334
346
  `at most ${MAX_REPORTS_PER_RUN} reports on one run`,
@@ -336,10 +348,10 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
336
348
  refusals: [...RUN_REFUSALS, "run.too_many_reports: the run carries as many as one may"],
337
349
  },
338
350
  "hand-in": {
339
- usage: "hand-in <https url | file path> [--label <words>]",
351
+ usage: "hand-in <https url | file path> [--label <words>] [--run <id>]",
352
+ overview: "hand-in <https url | file path> [--label <words>]",
340
353
  synopsis: "Hands in what was made: an https link, or a file from this machine.",
341
- about:
342
- "Hands in what was made: a link (a branch, a pull request, a document, a page), or a file from this machine, which the console keeps and shows on the task; a picture is shown there small and large, and a report that names it as ![words](its name) shows it where the words are.",
354
+ about: `Hands in what was made: a link (a branch, a pull request, a document, a page), or a file from this machine, which the console keeps and shows on the task; a picture is shown there small and large, and a report that names it as ![words](its name) shows it where the words are. ${ON_THE_RUN}`,
343
355
  limits: [
344
356
  "a link: https only",
345
357
  `a file: up to ${MAX_FILE_BYTES} bytes, not empty; it is called by its name, without the path`,
@@ -354,24 +366,25 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
354
366
  },
355
367
  ask: {
356
368
  usage:
369
+ 'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--wait <seconds>] [--run <id>]',
370
+ overview:
357
371
  'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--wait <seconds>]',
358
372
  synopsis:
359
373
  "Raises a decision a person must make, with options, and waits a while for the answer.",
360
- about:
361
- "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.",
374
+ 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}`,
362
375
  limits: [
363
376
  `question: one line, up to ${MAX_QUESTION_LENGTH} characters`,
364
377
  `options: ${MIN_OPTIONS} to ${MAX_OPTIONS}, each one line up to ${MAX_OPTION_LABEL_LENGTH} characters`,
365
378
  `body: ${MARKDOWN}`,
366
379
  ],
367
- refusals: ["decision.invalid_run: the run is not yours, or is over"],
380
+ refusals: [...RUN_CHOICE, "decision.invalid_run: the run is not yours, or is over"],
368
381
  },
369
382
  decision: {
370
383
  usage: "decision <id> [--wait <seconds>] | decision <id> --outcome <markdown>|--file <path>",
371
384
  synopsis:
372
385
  "One decision in full; waits for its answer with --wait; writes what followed with --outcome.",
373
386
  about:
374
- "One decision, with its context, its options, its answer and what followed. With --wait, holds on for the answer that long, and exits with 3 if it is still to come. With --outcome, writes down what followed the decision, so that it reads as a record.",
387
+ "One decision, with its context, its options, its answer (one of the options, or the person's own words as one more, marked own) and what followed. With --wait, holds on for the answer that long, and exits with 3 if it is still to come. With --outcome, writes down what followed the decision, so that it reads as a record.",
375
388
  limits: [`outcome: ${MARKDOWN}`],
376
389
  refusals: ["decision.not_found: no decision has that id"],
377
390
  },
@@ -381,17 +394,18 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
381
394
  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.`,
382
395
  },
383
396
  finish: {
384
- usage: 'finish [--summary "<markdown>"|--file <path>]',
397
+ usage: 'finish [--summary "<markdown>"|--file <path>] [--run <id>]',
398
+ overview: 'finish [--summary "<markdown>"|--file <path>]',
385
399
  synopsis: "Ends the run: done and handed in; the task goes up for review.",
386
- about:
387
- "Ends the run: the work is done and handed in, and the task goes up for review. The summary says what was done and what is left; the result itself goes in a last report before, which may be much longer.",
400
+ about: `Ends the run: the work is done and handed in, and the task goes up for review. The summary says what was done and what is left; the result itself goes in a last report before, which may be much longer. ${ON_THE_RUN}`,
388
401
  limits: [`summary: ${`Markdown, up to ${MAX_SUMMARY_LENGTH} characters`}`],
389
402
  refusals: RUN_REFUSALS,
390
403
  },
391
404
  fail: {
392
- usage: 'fail [--summary "<markdown>"|--file <path>]',
405
+ usage: 'fail [--summary "<markdown>"|--file <path>] [--run <id>]',
406
+ overview: 'fail [--summary "<markdown>"|--file <path>]',
393
407
  synopsis: "Ends the run: the work could not be done.",
394
- about: "Ends the run: the work could not be done. The summary says why, and what is left.",
408
+ about: `Ends the run: the work could not be done. The summary says why, and what is left. ${ON_THE_RUN}`,
395
409
  limits: [`summary: Markdown, up to ${MAX_SUMMARY_LENGTH} characters`],
396
410
  refusals: RUN_REFUSALS,
397
411
  },
@@ -457,7 +471,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
457
471
  usage: 'write <path> [--title "<title>"] --body <markdown>|--file <path> [--base <version>]',
458
472
  synopsis: "Writes a page: a new one at a path, or a new version of the page there.",
459
473
  about:
460
- "Writes a page: the first version at a new path, or a new version of the page there. A link in the body whose destination is a page's path (guides/onboarding) refers to that page, and the page says what refers to it. Without --title the page keeps its title. With --base, the write is refused if the page has moved on since that version.",
474
+ "Writes a page: the first version at a new path, or a new version of the page there. The title is shown above the body, on the page and by console doc, so the body does not begin with it as a heading. A link in the body whose destination is a page's path (guides/onboarding) refers to that page, and the page says what refers to it. Without --title the page keeps its title. With --base, the write is refused if the page has moved on since that version.",
461
475
  limits: [
462
476
  `path: ${DOCUMENT_PATH}`,
463
477
  `title: one line, up to ${MAX_TITLE_LENGTH} characters`,
@@ -510,13 +524,24 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
510
524
  },
511
525
  };
512
526
 
513
- /** The first lines of the usage, with the version of the command when it is known. */
514
- const preamble = (version: string | undefined): string =>
527
+ /**
528
+ * Where the skill working-the-board is, among the console's own skills in `skillsDir`, with the
529
+ * separators the directory came with, so that the path reads as the machine writes paths.
530
+ */
531
+ export function ownSkillPath(skillsDir: string): string {
532
+ const separator = skillsDir.includes("\\") ? "\\" : "/";
533
+ const base =
534
+ skillsDir.endsWith("/") || skillsDir.endsWith("\\") ? skillsDir : `${skillsDir}${separator}`;
535
+ return `${base}working-the-board${separator}SKILL.md`;
536
+ }
537
+
538
+ /** The first lines of the usage, with the version of the command and where its skill is, when known. */
539
+ const preamble = (version: string | undefined, skillsDir: string | undefined): string =>
515
540
  `console${version === undefined ? "" : ` ${version}`}: the board of your organization's work with AI agents, from the command line.
516
541
  You act as the person whose token you hold; everything you send is shown to people as text.
517
542
  The rules of this organization come with the task: console take prints them; console rules reads them any time.
518
- How the board is worked is the skill working-the-board, installed with this command: console skills prints
519
- where it is; read it before the first command of a run.
543
+ How the board is worked is the skill working-the-board, installed with this command; read it before the
544
+ first command of a run${skillsDir === undefined ? " (console whoami says where it is)" : `:\n${ownSkillPath(skillsDir)}`}
520
545
 
521
546
  Usage: console <command> [arguments] [--json] [--url <origin>] [--project <key>]
522
547
  A command in full, every form and flag ([flags] below), the limits the console holds it to and what
@@ -532,7 +557,8 @@ with several open, say which with --run <id>. A Markdown body given as - is read
532
557
  console --version says which version of the command this is.
533
558
 
534
559
  Exit codes: 0 done; 1 the console refused, or could not be reached (the reason is on stderr);
535
- 2 the command was not understood; 3 the decision still waits for a person.
560
+ 2 the command was not understood; 3 the decision still waits for a person; 4 the command and the
561
+ console do not go together, and stderr says which to update and how.
536
562
  What the console refuses is printed as its code and its words. Everywhere:
537
563
  ${COMMON_REFUSALS.map((refusal) => ` ${refusal}`).join("\n")}
538
564
 
@@ -583,8 +609,8 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
583
609
  ];
584
610
 
585
611
  /** The overview, as `console help` prints it: every command in a line, naming the version when it is known. */
586
- export function usage(version?: string): string {
587
- const parts = [preamble(version)];
612
+ export function usage(version?: string, skillsDir?: string): string {
613
+ const parts = [preamble(version, skillsDir)];
588
614
  for (const section of SECTIONS) {
589
615
  parts.push(`${section.title}`);
590
616
  for (const name of section.commands) {