@skillcdn/console 0.1.22 → 0.1.24

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.
@@ -121,7 +121,7 @@ The rules of the board ([ADR-0023](../adr/0023-the-rules-of-a-board-are-pages-th
121
121
 
122
122
  ### `GET /api/v1/work`
123
123
 
124
- `{ "tasks": [{ "project": { "key", "name", "color", "symbol", "icon" }, "task" }, ...], "decisions": [{ "project", "decision" }, ...] }`: what is on the asker's plate across the projects they may see ([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)): the open tasks assigned to them, the sooner due first and those with no day last, and the decisions that wait for a person, the longest waiting first; each with the project it belongs to, so that a page says and links it. At most `LIST_LIMIT` of each.
124
+ `{ "tasks": [{ "project": { "key", "name", "color", "symbol", "icon" }, "task" }, ...], "closed": [{ "project", "task" }, ...], "decisions": [{ "project", "decision" }, ...] }`: what is on the asker's plate across the projects they may see ([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)): the open tasks assigned to them, the sooner due first and those with no day last; `closed`, the tasks assigned to them that are done or dropped and not yet put away, the last closed first, so that the week on the plate's page shows them on their days as a project's calendar does; and the decisions that wait for a person, the longest waiting first; each with the project it belongs to, so that a page says and links it. A task put away is in neither list ([ADR-0030](../adr/0030-a-task-is-put-away-from-any-state.md)). At most `LIST_LIMIT` of each.
125
125
 
126
126
  ### `GET /api/v1/tokens` and `POST /api/v1/tokens`
127
127
 
@@ -141,7 +141,7 @@ An administrator's sight of everyone's agents ([ADR-0011](../adr/0011-an-agent-c
141
141
 
142
142
  ### `GET /api/v1/people/<id>/work`
143
143
 
144
- `{ "tasks": [{ "project", "task" }, ...] }`: what is on another person's plate, for whoever oversees them: an administrator, or an owner of a project the person is in (everyone of the workspace is in a project open to it; a private one holds those listed), signed in or through their agent. The person's open tasks, the sooner due first and those with no day last, within the projects the asker may see and no other, each with its project; the decisions are left out, since they wait for anyone. `auth.forbidden` for anyone else; `person.not_found` for an id that is nobody's.
144
+ `{ "tasks": [{ "project", "task" }, ...], "closed": [{ "project", "task" }, ...] }`: what is on another person's plate, for whoever oversees them: an administrator, or an owner of a project the person is in (everyone of the workspace is in a project open to it; a private one holds those listed), signed in or through their agent. The person's open tasks, the sooner due first and those with no day last, and as `closed` their tasks done or dropped and not yet put away, both within the projects the asker may see and no other, each with its project; the decisions are left out, since they wait for anyone. `auth.forbidden` for anyone else; `person.not_found` for an id that is nobody's.
145
145
 
146
146
  ### Connecting an agent
147
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillcdn/console",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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: 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.16"
8
+ version: "1.17"
9
9
  skillcdn:
10
10
  translations:
11
11
  ko:
@@ -54,7 +54,7 @@ Told which task, go to phase 2: `console take` prints the rules of the board wit
54
54
 
55
55
  ### Phase 2: Take
56
56
 
57
- `console take <number>`. A run begins: the task is in progress and the person's, and every report, hand-in, question and ending from here acts on this run. Taken by mistake, `console abandon` leaves it and the task stays as it was.
57
+ `console take <number>`. A run begins, and the command says its id: the task is in progress and the person's, and every report, hand-in, question and ending from here acts on this run. With another run of the person's open in the project, which `console take` says, each of those commands names the run: `--run <id>`. Taken by mistake, `console abandon` leaves it and the task stays as it was.
58
58
 
59
59
  ### Phase 3: Work, and report at the milestones
60
60
 
@@ -78,13 +78,7 @@ After the last report, `console finish --summary "<what was done, and what is le
78
78
 
79
79
  ## Setting up, when told to
80
80
 
81
- A task or a person may ask for the project itself rather than work in it: a project made or changed (`console project new <key> "<name>"`, `console project edit <key>`), its mark (`console project icon <key> <file>`), its members (`console member add <login>`, `console member role`, `console member remove`), the workspace's categories (`console category new <key> "<name>"`, `console category edit`, `console category remove`), what a person is (`console person role <login> <admin|member>`), its goals (`console goal new`), and its rules (`console rules write --file <path>`, from [the example](references/project-rules.md); the workspace's with `--workspace`, for an administrator). The agent does these as its person may, an owner of the project or an administrator of the workspace ([ADR-0021](/docs/adr/0021-a-token-configures-as-its-person-does.md)); the feed names the agent beside the person on each. What the person may not, the console refuses (`auth.forbidden`): that step goes back to the person in the report, never worked around. `console people` and `console members` say who is who, by login.
82
-
83
- ## The sentence a repository carries
84
-
85
- A repository whose checkouts are worked through a console says so in its agreement (AGENTS.md, or CLAUDE.md where that is the file agents read), in one sentence copied as it is and placed at the head of its workflow; a pack says it in the rules of its `SKILLCDN.md`, with `the checkout` for `this checkout`. The sentence binds the checkout to the console and says nothing of how: the command's help and this skill say how, and the repository's own agreement keeps saying how a change is committed and pushed.
86
-
87
- > **Bound to a console?** When `.skillcdn-console.json` is in this checkout, or `console whoami` names a project, work the task through that SkillCDN console (`console help` says how).
81
+ A task or a person may ask for the project itself rather than work in it: a project made or changed, its mark, its members, the workspace's categories, what a person is, its goals, its rules. [references/setting-up.md](references/setting-up.md) says which commands do that, as the person may: an owner of the project, or an administrator of the workspace; a project's rules start from [references/project-rules.md](references/project-rules.md).
88
82
 
89
83
  ## Hard rules
90
84
 
@@ -93,9 +87,10 @@ A repository whose checkouts are worked through a console says so in its agreeme
93
87
  - The board's words are the vocabulary's own: states `idea`, `ready`, `in_progress`, `in_review`, `done` and `dropped`; priorities `low`, `normal`, `high` and `urgent`; a day is `2026-10-24`. Taking a task moves it, finishing moves it; it is not moved by hand around a run. When a task is started and when it is done, the board writes down itself: never typed.
94
88
  - The plan is the person's: a task's days, its category and its goal are changed (`console task edit`) only when the task says so or a person asks, and a goal is set or moved (`console goal new`, `console goal edit`) only when the person is an owner and said to.
95
89
  - The rules are the people's: `console rules write` only when a person asked for exactly that, and never to loosen what they ask of agents.
96
- - Exit codes: `0` done; `1` refused or unreachable, the reason on standard error as a code and words, which `console help <command>` names; `2` not understood; `3` a decision still waits.
90
+ - Exit codes: `0` done; `1` refused or unreachable, the reason on standard error as a code and words, which `console help <command>` names; `2` not understood; `3` a decision still waits; `4` the command and the console do not go together, and standard error says which to update.
97
91
  - Everything sent is held to limits the console states (`console help <command>`); a refusal `request.invalid` names the field.
98
92
 
99
- ## Terms
93
+ ## Beside this skill
100
94
 
101
- - **Task**: what is to be done, numbered per project, with its plan (a category, a goal, the days it starts and is due) and its moments (when it was started and done, which the board keeps). **Goal**: what the project works towards, on its roadmap, with the day it is meant to be reached by; its progress is its tasks. **Category**: a kind of work, the workspace's word for it, by key. **Run**: one agent at work on one task for one person, with its reports, what it handed in, and what it waits for. **Decision**: a question with options, which a person answers on the board. **Page**: a document of the project, Markdown at a path that does not change. **Project**: a board of its own, with its members and its skills address.
95
+ - The sentence a repository's agreement carries to bind its checkouts to a console, and how a pack says it: [references/the-sentence.md](references/the-sentence.md).
96
+ - The board's words, task, goal, category, run, decision, page and project, said once: [references/terms.md](references/terms.md).
@@ -0,0 +1,3 @@
1
+ # Setting up, when told to
2
+
3
+ A task or a person may ask for the project itself rather than work in it: a project made or changed (`console project new <key> "<name>"`, `console project edit <key>`), its mark (`console project icon <key> <file>`), its members (`console member add <login>`, `console member role`, `console member remove`), the workspace's categories (`console category new <key> "<name>"`, `console category edit`, `console category remove`), what a person is (`console person role <login> <admin|member>`), its goals (`console goal new`), and its rules (`console rules write --file <path>`, from [the example](project-rules.md); the workspace's with `--workspace`, for an administrator). The agent does these as its person may, an owner of the project or an administrator of the workspace ([ADR-0021](/docs/adr/0021-a-token-configures-as-its-person-does.md)); the feed names the agent beside the person on each. What the person may not, the console refuses (`auth.forbidden`): that step goes back to the person in the report, never worked around. `console people` and `console members` say who is who, by login.
@@ -0,0 +1,9 @@
1
+ # Terms
2
+
3
+ - **Task**: what is to be done, numbered per project, with its plan (a category, a goal, the days it starts and is due) and its moments (when it was started and done, which the board keeps).
4
+ - **Goal**: what the project works towards, on its roadmap, with the day it is meant to be reached by; its progress is its tasks.
5
+ - **Category**: a kind of work, the workspace's word for it, by key.
6
+ - **Run**: one agent at work on one task for one person, with its reports, what it handed in, and what it waits for.
7
+ - **Decision**: a question with options, which a person answers on the board.
8
+ - **Page**: a document of the project, Markdown at a path that does not change.
9
+ - **Project**: a board of its own, with its members and its skills address.
@@ -0,0 +1,5 @@
1
+ # The sentence a repository carries
2
+
3
+ A repository whose checkouts are worked through a console says so in its agreement (AGENTS.md, or CLAUDE.md where that is the file agents read), in one sentence copied as it is and placed at the head of its workflow; a pack says it in the rules of its `SKILLCDN.md`, with `the checkout` for `this checkout`. The sentence binds the checkout to the console and says nothing of how: the command's help and the skill working-the-board say how, and the repository's own agreement keeps saying how a change is committed and pushed.
4
+
5
+ > **Bound to a console?** When `.skillcdn-console.json` is in this checkout, or `console whoami` names a project, work the task through that SkillCDN console (`console help` says how).
package/src/cli/cli.ts CHANGED
@@ -86,7 +86,7 @@ import {
86
86
  formatVersionLine,
87
87
  formatWork,
88
88
  } from "./format.js";
89
- import { COMMAND_HELP, helpFor, usage } from "./help.js";
89
+ import { COMMAND_HELP, helpFor, ownSkillPath, usage } from "./help.js";
90
90
  import { describeInstall, type Install, updateCommandFor } from "./install.js";
91
91
  import type { NoticeStore } from "./notices.js";
92
92
  import { createServeHandler, DEFAULT_SERVE_PORT, type Listen } from "./serve.js";
@@ -339,11 +339,11 @@ async function currentRun(project: ProjectClient, values: Values): Promise<strin
339
339
  }
340
340
  if (items.length === 0) {
341
341
  throw failed(
342
- "No run of yours is open in this project. Take a task first: console take <number>",
342
+ "run.none: no run of yours is open in this project. Take a task first: console take <number>; or name a run: --run <id>",
343
343
  );
344
344
  }
345
345
  throw failed(
346
- `Several runs of yours are open; say which with --run <id>:\n${items.map((run) => ` ${formatRunLine(run)}`).join("\n")}`,
346
+ `run.ambiguous: several runs of yours are open in this project; say which with --run <id>:\n${items.map((run) => ` ${formatRunLine(run)}`).join("\n")}`,
347
347
  );
348
348
  }
349
349
 
@@ -851,14 +851,14 @@ const whoami: Command = async (args, io) => {
851
851
  ...(key === undefined
852
852
  ? []
853
853
  : [
854
- "What this organization asks of agents: console rules. How the board is worked: the skill working-the-board, shipped with this command (console skills says where it is).",
854
+ `What this organization asks of agents: console rules. How the board is worked: the skill working-the-board, shipped with this command${io.skillsDir === undefined ? " (console skills says where it is)." : `: ${ownSkillPath(io.skillsDir)}`}`,
855
855
  ]),
856
856
  `This command is ${io.version ?? "of a version it does not know"}${io.install === undefined ? "" : `, ${describeInstall(io.install)}`}; ${consoleWords}.`,
857
857
  ...(verdict === "behind"
858
858
  ? [`A newer command exists. Update it: ${updateWords(io, me.console?.version)}`]
859
859
  : verdict === "ahead"
860
860
  ? [
861
- `This command is newer than the console: what the console has not got yet is refused as not found. To match the console: ${updateWords(io, me.console?.version)}`,
861
+ `This command is newer than the console: it works, and what the console has not got yet is refused as not found; nothing to do until then. To match the console then: ${updateWords(io, me.console?.version)}`,
862
862
  ]
863
863
  : []),
864
864
  ].join("\n"),
@@ -1098,9 +1098,11 @@ const skill: Command = async (args, io) => {
1098
1098
  answer(io, session, { ...read, content }, () => formatSkillFile(read, content));
1099
1099
  return EXIT.ok;
1100
1100
  }
1101
- const path = theOne(
1102
- positionals,
1103
- "Say which skill: console skill <path of its SKILL.md>, or console skill --file <path> for a file of the pack",
1101
+ const { path, pack } = skillPathOf(
1102
+ theOne(
1103
+ positionals,
1104
+ "Say which skill: console skill <path of its SKILL.md>, or console skill --file <path> for a file of the pack",
1105
+ ),
1104
1106
  );
1105
1107
  // The skill whole: its pages until complete, the rules first, as load_skill lays them out.
1106
1108
  const pages: RestSkillPage[] = [];
@@ -1120,11 +1122,36 @@ const skill: Command = async (args, io) => {
1120
1122
  if (last === undefined) {
1121
1123
  throw failed("The skill could not be read.");
1122
1124
  }
1125
+ // A URI of another pack would read this pack's skill at the same path, or nothing: said instead.
1126
+ if (pack !== undefined && last.address !== null && last.address !== pack) {
1127
+ throw misuse(
1128
+ `That URI is of another pack, ${pack}; this project's pack is at ${last.address}. Say the path of the SKILL.md, or a URI console skills lists.`,
1129
+ );
1130
+ }
1123
1131
  const loaded = last;
1124
1132
  answer(io, session, { ...loaded, pages }, () => formatSkillLoad(loaded, pages));
1125
1133
  return EXIT.ok;
1126
1134
  };
1127
1135
 
1136
+ /**
1137
+ * A skill's path as given, or taken out of the URI `console skills` lists beside it
1138
+ * (`skill://gh/<owner>/<repo>/<path>`), with the pack the URI names, for the read to check.
1139
+ */
1140
+ function skillPathOf(given: string): { readonly path: string; readonly pack?: string } {
1141
+ const uri = /^skill:\/\/(gh\/[^/]+\/[^/]+)\/(.+)$/.exec(given);
1142
+ const named = uri?.[1];
1143
+ const within = uri?.[2];
1144
+ if (named !== undefined && within !== undefined) {
1145
+ return { path: within, pack: `/${named}` };
1146
+ }
1147
+ if (given.startsWith("skill://")) {
1148
+ throw misuse(
1149
+ `Not a skill's URI the console knows: ${given}. Say the path of its SKILL.md from the repository's root, or the URI console skills lists.`,
1150
+ );
1151
+ }
1152
+ return { path: given };
1153
+ }
1154
+
1128
1155
  /** A version number given as a flag, or nothing; anything else is not understood. */
1129
1156
  function versionOf(given: string | undefined): number | undefined {
1130
1157
  if (given === undefined) {
@@ -1487,24 +1514,32 @@ const take: Command = async (args, io) => {
1487
1514
  });
1488
1515
  // The rules come with the run, as they read for the person: everything from here acts
1489
1516
  // within them (ADR-0023); the task is read again, as it stands now that it is taken.
1490
- const [ruling, me, taken, comments] = await Promise.all([
1517
+ const [ruling, me, taken, comments, openRuns] = await Promise.all([
1491
1518
  project.rules(),
1492
1519
  session.client.me(),
1493
1520
  project.task(ref),
1494
1521
  project.comments(found.id),
1522
+ project.runs({ open: true, mine: true }),
1495
1523
  ]);
1496
1524
  // Taking a task with a run open on it ends that run (ADR-0020): said when it happened.
1497
1525
  const tookOver =
1498
1526
  found.openRuns > 0
1499
1527
  ? " A run that was open on it is over now: the board cannot know whether its agent still ran, so taking it was deciding."
1500
1528
  : "";
1529
+ // Another run of this token open in the project makes every command on a run ask which
1530
+ // (currentRun): said here, with the id to say, before the first report runs into it.
1531
+ const others = openRuns.items.filter((item) => item.id !== run.id);
1532
+ const elsewhere =
1533
+ others.length === 0
1534
+ ? ""
1535
+ : `\nOther runs of yours are open in this project (${others.map((item) => `#${item.taskNumber}`).join(", ")}): say which run on report, hand-in, ask, finish and fail, --run ${run.id} for this one.`;
1501
1536
  const rules = {
1502
1537
  workspace: forPerson(ruling.workspace, me.language),
1503
1538
  project: forPerson(ruling.project, me.language),
1504
1539
  };
1505
1540
  answer(io, session, { run, task: taken, comments: comments.items, rules }, () =>
1506
1541
  [
1507
- `Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.${tookOver}`,
1542
+ `Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.${tookOver}${elsewhere}`,
1508
1543
  "",
1509
1544
  formatTask(taken, undefined, undefined, comments.items),
1510
1545
  "",
@@ -2540,7 +2575,7 @@ const COMMANDS: Readonly<Record<string, Command>> = {
2540
2575
 
2541
2576
  function help(io: CliIo, name: string | undefined): number {
2542
2577
  if (name === undefined) {
2543
- io.stdout(usage(io.version));
2578
+ io.stdout(usage(io.version, io.skillsDir));
2544
2579
  return EXIT.ok;
2545
2580
  }
2546
2581
  const found = helpFor(name);
@@ -2614,7 +2649,7 @@ async function noticeVersions(io: CliIo, seen: ConsoleSeen): Promise<void> {
2614
2649
  io.stderr(
2615
2650
  verdict === "behind"
2616
2651
  ? `A newer command exists: the console runs ${seen.version}, this command is ${io.version}. Update it: ${updateWords(io, seen.version)}\n`
2617
- : `This command (${io.version}) is newer than the console (${seen.version}, which takes ${seen.range ?? "an older line"}): what the console has not got yet is refused as not found. The console's operator updates it; to match the console meanwhile: ${updateWords(io, seen.version)}\n`,
2652
+ : `This command (${io.version}) is newer than the console (${seen.version}, which takes ${seen.range ?? "an older line"}): it works, and what the console has not got yet is refused as not found; nothing to do until then. The console's operator updates it; to match the console meanwhile: ${updateWords(io, seen.version)}\n`,
2618
2653
  );
2619
2654
  await io.notices?.save({
2620
2655
  command: io.version,
package/src/cli/format.ts CHANGED
@@ -290,14 +290,18 @@ export function formatSkills(skills: RestSkills, ownSkills?: string): string {
290
290
  }
291
291
  if (skills.counts !== undefined) {
292
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.`,
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
294
  );
295
295
  }
296
296
  if (skills.items.length === 0) {
297
297
  lines.push("No skills at this address.");
298
298
  }
299
+ // The path, which console skill takes, and the URI beside it, which a host's own connection takes.
299
300
  for (const skill of skills.items) {
300
- lines.push(`${skill.name} ${skill.description}`, ` ${skill.uri ?? skill.path}`);
301
+ lines.push(
302
+ `${skill.name} ${skill.description}`,
303
+ ` ${skill.path}${skill.uri === null ? "" : ` ${skill.uri}`}`,
304
+ );
301
305
  }
302
306
  lines.push(
303
307
  "",
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`;
@@ -279,7 +289,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
279
289
  synopsis:
280
290
  "The project's pack: its skills; what a folder of it holds; or what words find in it.",
281
291
  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.",
292
+ "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.",
283
293
  limits: ["search: 1 to 500 characters; limit: 1 to 25"],
284
294
  refusals: [
285
295
  "status not_found: the pack is not served to the console; private, with no pack token on the project",
@@ -291,7 +301,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
291
301
  synopsis:
292
302
  "One skill of the pack whole, its rules first; or with --file a file of the pack, or what a folder holds.",
293
303
  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.",
304
+ "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, or the URI it lists beside the path (skill://gh/<owner>/<repo>/<path>). --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.",
295
305
  refusals: [
296
306
  "status not_found: the pack is not served to the console; private, with no pack token on the project",
297
307
  "status token_invalid: the pack token the project holds no longer works; an owner sets a new one",
@@ -325,10 +335,11 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
325
335
  ],
326
336
  },
327
337
  report: {
328
- usage: "report <markdown> | report --file <path> | report -",
338
+ usage:
339
+ "report <markdown> [--run <id>] | report --file <path> [--run <id>] | report - [--run <id>]",
340
+ overview: "report <markdown> | report --file <path> | report -",
329
341
  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.",
342
+ 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
343
  limits: [
333
344
  `the report: ${MARKDOWN}, not blank`,
334
345
  `at most ${MAX_REPORTS_PER_RUN} reports on one run`,
@@ -336,10 +347,10 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
336
347
  refusals: [...RUN_REFUSALS, "run.too_many_reports: the run carries as many as one may"],
337
348
  },
338
349
  "hand-in": {
339
- usage: "hand-in <https url | file path> [--label <words>]",
350
+ usage: "hand-in <https url | file path> [--label <words>] [--run <id>]",
351
+ overview: "hand-in <https url | file path> [--label <words>]",
340
352
  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.",
353
+ 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
354
  limits: [
344
355
  "a link: https only",
345
356
  `a file: up to ${MAX_FILE_BYTES} bytes, not empty; it is called by its name, without the path`,
@@ -354,17 +365,18 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
354
365
  },
355
366
  ask: {
356
367
  usage:
368
+ 'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--wait <seconds>] [--run <id>]',
369
+ overview:
357
370
  'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--wait <seconds>]',
358
371
  synopsis:
359
372
  "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.",
373
+ 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. ${ON_THE_RUN}`,
362
374
  limits: [
363
375
  `question: one line, up to ${MAX_QUESTION_LENGTH} characters`,
364
376
  `options: ${MIN_OPTIONS} to ${MAX_OPTIONS}, each one line up to ${MAX_OPTION_LABEL_LENGTH} characters`,
365
377
  `body: ${MARKDOWN}`,
366
378
  ],
367
- refusals: ["decision.invalid_run: the run is not yours, or is over"],
379
+ refusals: [...RUN_CHOICE, "decision.invalid_run: the run is not yours, or is over"],
368
380
  },
369
381
  decision: {
370
382
  usage: "decision <id> [--wait <seconds>] | decision <id> --outcome <markdown>|--file <path>",
@@ -381,17 +393,18 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
381
393
  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
394
  },
383
395
  finish: {
384
- usage: 'finish [--summary "<markdown>"|--file <path>]',
396
+ usage: 'finish [--summary "<markdown>"|--file <path>] [--run <id>]',
397
+ overview: 'finish [--summary "<markdown>"|--file <path>]',
385
398
  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.",
399
+ 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
400
  limits: [`summary: ${`Markdown, up to ${MAX_SUMMARY_LENGTH} characters`}`],
389
401
  refusals: RUN_REFUSALS,
390
402
  },
391
403
  fail: {
392
- usage: 'fail [--summary "<markdown>"|--file <path>]',
404
+ usage: 'fail [--summary "<markdown>"|--file <path>] [--run <id>]',
405
+ overview: 'fail [--summary "<markdown>"|--file <path>]',
393
406
  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.",
407
+ about: `Ends the run: the work could not be done. The summary says why, and what is left. ${ON_THE_RUN}`,
395
408
  limits: [`summary: Markdown, up to ${MAX_SUMMARY_LENGTH} characters`],
396
409
  refusals: RUN_REFUSALS,
397
410
  },
@@ -457,7 +470,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
457
470
  usage: 'write <path> [--title "<title>"] --body <markdown>|--file <path> [--base <version>]',
458
471
  synopsis: "Writes a page: a new one at a path, or a new version of the page there.",
459
472
  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.",
473
+ "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
474
  limits: [
462
475
  `path: ${DOCUMENT_PATH}`,
463
476
  `title: one line, up to ${MAX_TITLE_LENGTH} characters`,
@@ -510,13 +523,24 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
510
523
  },
511
524
  };
512
525
 
513
- /** The first lines of the usage, with the version of the command when it is known. */
514
- const preamble = (version: string | undefined): string =>
526
+ /**
527
+ * Where the skill working-the-board is, among the console's own skills in `skillsDir`, with the
528
+ * separators the directory came with, so that the path reads as the machine writes paths.
529
+ */
530
+ export function ownSkillPath(skillsDir: string): string {
531
+ const separator = skillsDir.includes("\\") ? "\\" : "/";
532
+ const base =
533
+ skillsDir.endsWith("/") || skillsDir.endsWith("\\") ? skillsDir : `${skillsDir}${separator}`;
534
+ return `${base}working-the-board${separator}SKILL.md`;
535
+ }
536
+
537
+ /** The first lines of the usage, with the version of the command and where its skill is, when known. */
538
+ const preamble = (version: string | undefined, skillsDir: string | undefined): string =>
515
539
  `console${version === undefined ? "" : ` ${version}`}: the board of your organization's work with AI agents, from the command line.
516
540
  You act as the person whose token you hold; everything you send is shown to people as text.
517
541
  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.
542
+ How the board is worked is the skill working-the-board, installed with this command; read it before the
543
+ first command of a run${skillsDir === undefined ? " (console whoami says where it is)" : `:\n${ownSkillPath(skillsDir)}`}
520
544
 
521
545
  Usage: console <command> [arguments] [--json] [--url <origin>] [--project <key>]
522
546
  A command in full, every form and flag ([flags] below), the limits the console holds it to and what
@@ -532,7 +556,8 @@ with several open, say which with --run <id>. A Markdown body given as - is read
532
556
  console --version says which version of the command this is.
533
557
 
534
558
  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.
559
+ 2 the command was not understood; 3 the decision still waits for a person; 4 the command and the
560
+ console do not go together, and stderr says which to update and how.
536
561
  What the console refuses is printed as its code and its words. Everywhere:
537
562
  ${COMMON_REFUSALS.map((refusal) => ` ${refusal}`).join("\n")}
538
563
 
@@ -583,8 +608,8 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
583
608
  ];
584
609
 
585
610
  /** 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)];
611
+ export function usage(version?: string, skillsDir?: string): string {
612
+ const parts = [preamble(version, skillsDir)];
588
613
  for (const section of SECTIONS) {
589
614
  parts.push(`${section.title}`);
590
615
  for (const name of section.commands) {
@@ -7,14 +7,17 @@ import { TaskList } from "./task-list.js";
7
7
  import { PersonChip, Time } from "./ui.js";
8
8
 
9
9
  // What is on a person's plate (ADR-0019), across the projects they may see: the week at the
10
- // top, with the open tasks assigned to them on its days; below, the same tasks by when they
11
- // are due, and the decisions that wait for a person. Takes its data as props and nothing from
12
- // the network; the week shown is the address's (ADR-0010).
10
+ // top, with the open tasks assigned to them on its days, and what of theirs was done or
11
+ // dropped on them, as a project's calendar shows it; below, the open tasks by when they are
12
+ // due, and the decisions that wait for a person. Takes its data as props and nothing from the
13
+ // network; the week shown is the address's (ADR-0010).
13
14
 
14
15
  export interface WorkListProps {
15
16
  /** The tasks on the plate; and the decisions waiting on the person's own plate, left out on another's. */
16
17
  readonly work: {
17
18
  readonly tasks: RestWork["tasks"];
19
+ /** The person's tasks done or dropped and not yet put away: on the week, not in the lists. */
20
+ readonly closed?: RestWork["closed"] | undefined;
18
21
  readonly decisions?: RestWork["decisions"] | undefined;
19
22
  };
20
23
  /** The day to count from; the browser's today when left out. */
@@ -33,7 +36,8 @@ export function WorkList(props: WorkListProps) {
33
36
  const now = props.today ?? todayOf();
34
37
  const weekEnd = addDays(now, 6);
35
38
  const { tasks, decisions } = props.work;
36
- const projectOf = new Map(tasks.map((item) => [item.task.id, item.project]));
39
+ const closed = props.work.closed ?? [];
40
+ const projectOf = new Map([...tasks, ...closed].map((item) => [item.task.id, item.project]));
37
41
  const hrefOf = (task: RestTask) => props.taskHref(projectOf.get(task.id)?.key ?? "", task);
38
42
  const markOf = (task: RestTask) => {
39
43
  const project = projectOf.get(task.id);
@@ -60,7 +64,7 @@ export function WorkList(props: WorkListProps) {
60
64
  return (
61
65
  <div className="sc-work">
62
66
  <Week
63
- tasks={tasks.map((item) => item.task)}
67
+ tasks={[...tasks, ...closed].map((item) => item.task)}
64
68
  day={props.week}
65
69
  today={now}
66
70
  taskHref={hrefOf}
package/src/console.tsx CHANGED
@@ -1748,9 +1748,10 @@ function PersonAgentsPage(props: {
1748
1748
  );
1749
1749
  }
1750
1750
 
1751
- /** What a plate's page shows: the tasks, and the decisions waiting on the person's own. */
1751
+ /** What a plate's page shows: the tasks, open and lately closed, and the decisions waiting on the person's own. */
1752
1752
  interface Plate {
1753
1753
  readonly tasks: RestWork["tasks"];
1754
+ readonly closed?: RestWork["closed"] | undefined;
1754
1755
  readonly decisions?: RestWork["decisions"] | undefined;
1755
1756
  }
1756
1757
 
@@ -1825,7 +1826,9 @@ function PlatePage(props: {
1825
1826
  <Spinner label={props.loading} />
1826
1827
  </div>
1827
1828
  )
1828
- ) : plate.tasks.length === 0 && (plate.decisions?.length ?? 0) === 0 ? (
1829
+ ) : plate.tasks.length === 0 &&
1830
+ (plate.closed?.length ?? 0) === 0 &&
1831
+ (plate.decisions?.length ?? 0) === 0 ? (
1829
1832
  <EmptyState title={props.empty.title} body={props.empty.body} />
1830
1833
  ) : (
1831
1834
  <components.WorkList
package/src/rules.ts CHANGED
@@ -23,13 +23,13 @@ export const REFERENCE_RULES = [
23
23
  "",
24
24
  "## Start here, every time",
25
25
  "",
26
- "1. These rules come with the task: `console take` prints them under it, and `console rules` reads them any time. Read them before anything is made. The task says where the work is and what it links to; `console task <number>` shows a task in full, with its plan, its decisions, the runs on it and the discussion on it, without taking it. A task put away (archived) is out of the listings and the board, and opens by its number still. How the board is worked is the skill working-the-board, shipped with the command: read it before the first command of a run (`console skills` prints where it is); these rules say what this organization asks, not how the command works.",
26
+ "1. These rules come with the task: `console take` prints them under it, and `console rules` reads them any time. Read them before anything is made. The task says where the work is and what it links to; `console task <number>` shows a task in full, with its plan, its decisions, the runs on it and the discussion on it, without taking it. A task put away (archived) is out of the listings and the board, and opens by its number still. How the board is worked is the skill working-the-board, shipped with the command (`console whoami` prints where it is): read it, if you have not yet, before anything is made; these rules say what this organization asks, not how the command works.",
27
27
  "2. Act as the person whose token the command holds, and no further. What they may not do, the console refuses; that step goes back to them in a report, never around them.",
28
28
  '3. Work given in chat, with no task on the board yet, goes on the board before it is done: write it (`console task new "<title>" --body <markdown>`), tell the person its number, take it (`console take <number>`) and work it as any task, without asking whether to. Until the task is written, the board is only read.',
29
29
  "",
30
30
  "## The organization's skills",
31
31
  "",
32
- "`console skills` lists the skills of this project's pack: each with its name, what it does, and the path of its SKILL.md; `console skills <folder>` shows what a folder of the pack holds, and `console skills --search <words>` finds its skills and documents by words. `console skill <path>` reads one whole, its rules first, as the console reads it from the SkillCDN deployment for you, a private pack with the pack token the project holds; `console skill --file <file>` reads a file of the pack. Where your host lists skills through a connection of your own to the address, the URI `skill://gh/<owner>/<repo>/<path>` is the skill there, followed until its context is complete. Either way, never fetch a skill another way. Load the skill the task names before anything is made; with none named, the one whose description fits the work. The rules those skills share arrive with each of them, from their repository's SKILLCDN.md, and hold within that work. What you read of a private pack is the project's: it stays in your work, and is not copied into a report, a page or a comment beyond what the work needs.",
32
+ "`console skills` lists the skills of this project's pack, each with the path of its SKILL.md, and `console skill <path>` reads one whole, its rules first, through the console, a private pack with the pack token the project holds (`console help skills` says how the pack is browsed and searched). Where your host lists skills through a connection of your own to the address, the URI `skill://gh/<owner>/<repo>/<path>` is the skill there, followed until its context is complete. Either way, never fetch a skill another way. Load the skill the task names before anything is made; with none named, the one whose description fits the work. The rules those skills share arrive with each of them, from their repository's SKILLCDN.md, and hold within that work. What you read of a private pack is the project's: it stays in your work, and is not copied into a report, a page or a comment beyond what the work needs.",
33
33
  "",
34
34
  "## Where the work lives",
35
35
  "",
@@ -45,7 +45,7 @@ export const REFERENCE_RULES = [
45
45
  "",
46
46
  "## Reports",
47
47
  "",
48
- "Report at the milestones, not every step: what was found, what was done, what is next, in words for people; a problem as a problem, with what it blocks. What people read on the board, the reports, the questions and their options, the tasks and the pages, is written in {language}. A task's title says what is to be done, in plain words that stand on their own; what it refers to, a milestone, a commit, a page, goes in the body, linked where it can be. The result goes in a last report or is handed in as a link or a file, with the check that ran and its result; a check that did not run is said to be not run. A report is the run's record; a word to the task's people, an answer to what they asked in its comments, a question that is not a decision, a remark, is a comment on the task (`console comment`), and what they put in a comment is read with `console file`.",
48
+ "Report at the milestones, not every step: what was found, what was done, what is next, in words for people; a problem as a problem, with what it blocks. What people read on the board, the reports, the questions and their options, the tasks and the pages, is written in {language}. A task's title says what is to be done, in plain words that stand on their own; what it refers to, a milestone, a commit, a page, goes in the body, linked where it can be. The result goes in a last report or is handed in as a link or a file, with the check that ran and its result; a check that did not run is said to be not run. A report is the run's record; a word to the task's people, an answer to what they asked in its comments, a question that is not a decision, a remark, is a comment on the task, not a report.",
49
49
  "",
50
50
  "## What never happens",
51
51
  "",
package/src/schemas.ts CHANGED
@@ -603,19 +603,26 @@ const projectRef = z.object({
603
603
 
604
604
  /**
605
605
  * `GET /api/v1/work`: what is on the asker's plate across the projects they may see (ADR-0019):
606
- * the open tasks assigned to them, and the decisions that wait for a person, each with its project.
606
+ * the open tasks assigned to them, and the decisions that wait for a person, each with its
607
+ * project; and, for the week on the plate, `closed`, the tasks assigned to them that are done
608
+ * or dropped and not yet put away. Left out by a console older than it.
607
609
  */
608
610
  export const restWorkSchema = z.object({
609
611
  tasks: z.array(z.object({ project: projectRef, task: restTaskSchema })),
612
+ closed: z.optional(z.array(z.object({ project: projectRef, task: restTaskSchema }))),
610
613
  decisions: z.array(z.object({ project: projectRef, decision: restDecisionSchema })),
611
614
  });
612
615
  export type RestWork = z.infer<typeof restWorkSchema>;
613
616
 
614
617
  /**
615
- * What `GET /api/v1/people/<id>/work` answers: another person's open tasks, within the projects
616
- * the asker may see, for whoever oversees them. No decisions, which wait for anyone.
618
+ * What `GET /api/v1/people/<id>/work` answers: another person's open tasks, and their closed
619
+ * ones for the week, within the projects the asker may see, for whoever oversees them. No
620
+ * decisions, which wait for anyone.
617
621
  */
618
- export const restPersonWorkSchema = z.object({ tasks: restWorkSchema.shape.tasks });
622
+ export const restPersonWorkSchema = z.object({
623
+ tasks: restWorkSchema.shape.tasks,
624
+ closed: restWorkSchema.shape.closed,
625
+ });
619
626
  export type RestPersonWork = z.infer<typeof restPersonWorkSchema>;
620
627
 
621
628
  /** What `POST /api/v1/projects/<key>/decisions` is sent: the question, its context, and the options. */