@skillcdn/console 0.1.10 → 0.1.12
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.
- package/README.md +6 -5
- package/dist/api.d.ts +6 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +49 -9
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +2 -0
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +7 -6
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +10 -9
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +14 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +13 -1
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +1 -4
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +6 -13
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +21 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +69 -9
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +11 -9
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +11 -7
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/paged.d.ts +14 -0
- package/dist/components/paged.d.ts.map +1 -0
- package/dist/components/paged.js +28 -0
- package/dist/components/paged.js.map +1 -0
- package/dist/components/people.js +2 -2
- package/dist/components/people.js.map +1 -1
- package/dist/components/projects.d.ts +22 -2
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +37 -9
- package/dist/components/projects.js.map +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +3 -3
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.d.ts +2 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +3 -3
- package/dist/components/shell.js.map +1 -1
- package/dist/components/task-form.js +2 -2
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.d.ts +1 -1
- package/dist/components/task-list.d.ts.map +1 -1
- package/dist/components/task-list.js +6 -4
- package/dist/components/task-list.js.map +1 -1
- package/dist/components/task-view.js +1 -1
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +7 -2
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +9 -4
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +7 -3
- package/dist/components/work.js.map +1 -1
- package/dist/console.css +149 -2
- package/dist/console.d.ts +38 -4
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +84 -23
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +5 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +15 -2
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +17 -11
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +18 -14
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +18 -14
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +6 -0
- package/dist/limits.js.map +1 -1
- package/dist/marks.d.ts +12 -0
- package/dist/marks.d.ts.map +1 -0
- package/dist/marks.js +31 -0
- package/dist/marks.js.map +1 -0
- package/dist/router.d.ts +19 -3
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +28 -5
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +7 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +8 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +123 -3
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +42 -10
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +10 -4
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +8 -3
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +7 -5
- package/docs/specs/rest.md +18 -12
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +2 -2
- package/skills/working-the-board/SKILL.md +1 -1
- package/src/api.ts +12 -0
- package/src/cli/cli.ts +51 -8
- package/src/cli/format.ts +8 -6
- package/src/cli/help.ts +10 -10
- package/src/client.ts +29 -6
- package/src/components/board.tsx +6 -30
- package/src/components/calendar.tsx +168 -56
- package/src/components/decision-list.tsx +38 -33
- package/src/components/event-feed.tsx +34 -27
- package/src/components/paged.tsx +39 -0
- package/src/components/people.tsx +5 -5
- package/src/components/projects.tsx +132 -12
- package/src/components/runs.tsx +9 -3
- package/src/components/shell.tsx +8 -5
- package/src/components/task-form.tsx +2 -2
- package/src/components/task-list.tsx +35 -32
- package/src/components/task-view.tsx +1 -1
- package/src/components/ui.tsx +14 -5
- package/src/components/work.tsx +12 -2
- package/src/console.tsx +158 -25
- package/src/data.ts +20 -4
- package/src/i18n/en.ts +19 -14
- package/src/i18n/ko.ts +19 -14
- package/src/index.ts +9 -0
- package/src/limits.ts +6 -0
- package/src/marks.ts +40 -0
- package/src/router.ts +45 -7
- package/src/routes.ts +9 -0
- package/src/schemas.ts +48 -8
- package/src/styles/console.css +149 -2
- package/src/vocabulary.ts +14 -4
package/docs/specs/cli.md
CHANGED
|
@@ -23,7 +23,7 @@ The board is a project's ([ADR-0008](../adr/0008-a-workspace-holds-projects-and-
|
|
|
23
23
|
| `projects` | `GET /api/v1/projects` | one line per project: key, name, what the person is in it, what waits in it |
|
|
24
24
|
| `use <key>` | `GET /api/v1/projects/<key>` | that the directory works in the project from now on; writes `.skillcdn-console.json` |
|
|
25
25
|
| `work` | `GET /api/v1/work` | what is on the person's plate across their projects: the open tasks assigned to them, the sooner due first, each with its project; and the decisions waiting. Needs no project |
|
|
26
|
-
| `tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day\|today\|week\|overdue>]` | `GET .../tasks` with the filters | one line per task: number, state, priority, title, owner, assignee, category, due day, what waits on it |
|
|
26
|
+
| `tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day\|today\|week\|overdue>] [--limit <n>] [--offset <n>]` | `GET .../tasks` with the filters and the page | one line per task: number, state, priority, title, owner, assignee, category, due day, what waits on it; a page at a time |
|
|
27
27
|
| `task <number\|id>` | `GET .../tasks/<ref>`, the decisions about it, the runs on it | the task in full, with its plan and its moments |
|
|
28
28
|
| `task new <title> [--body <markdown>\|--file <path>] [--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--assignee <login\|me>] [--parent <number\|id>]` | `POST .../tasks` | the number and the id |
|
|
29
29
|
| `task edit <number\|id> [--title <title>] [--body ...] [the same flags; `none` clears a field]` | `GET .../tasks/<ref>`, then `PATCH .../tasks/<id>` | the task in full, as it is now |
|
|
@@ -33,15 +33,15 @@ The board is a project's ([ADR-0008](../adr/0008-a-workspace-holds-projects-and-
|
|
|
33
33
|
| `goal edit <id> [--title <title>] [--body ...] [--state <state>] [--due <day\|none>]` | `PATCH .../goals/<id>` | the goal in full, as it is now |
|
|
34
34
|
| `categories` | `GET /api/v1/categories` | one line per category: key, name, colour |
|
|
35
35
|
| `skills` | `GET .../skills` | the project's skills, or the organization's: each with its description and the URI an agent loads it by through its own SkillCDN connection; or why there are none to show |
|
|
36
|
-
| `take <number\|id> [--agent <name>]` | `GET .../tasks/<ref>`, then `POST .../runs` | the run that began, the task in full, and what to do next |
|
|
36
|
+
| `take <number\|id> [--agent <name>]` | `GET .../tasks/<ref>`, then `POST .../runs` | the run that began, the task in full, and what to do next; a run open on the task is taken over ([ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)) |
|
|
37
37
|
| `report <markdown>`, `report --file <path>`, `report -` | `POST .../runs/<id>/reports` | how many reports the run carries |
|
|
38
38
|
| `hand-in <https url \| file path> [--label <words>]` | `POST .../runs/<id>/artifacts` for a link; `POST .../runs/<id>/files` for a file, sent as a form with its name and its media type by extension | how many artifacts |
|
|
39
39
|
| `ask "<question>" --option "<label>" ... [--body <markdown>\|--file <path>] [--wait <seconds>]` | `POST .../decisions` with `runId`, then `GET .../decisions/<id>?wait=` | the decision raised, its options; then the answer, or that it still waits |
|
|
40
40
|
| `decision <id> [--wait <seconds>]` | `GET .../decisions/<id>`, with `?wait=` while waiting | the decision; with a wait, the answer or that it still waits |
|
|
41
|
-
| `decisions [--open]` | `GET .../decisions` | one line per decision |
|
|
41
|
+
| `decisions [--open] [--limit <n>] [--offset <n>]` | `GET .../decisions` | one line per decision; a page at a time |
|
|
42
42
|
| `finish [--summary <markdown>\|--file <path>]`, `fail [...]` | `POST .../runs/<id>/end` | how the run ended |
|
|
43
43
|
| `abandon [<run id>] [--summary <markdown>]` | `POST .../runs/<id>/end`, as `abandoned` | the same |
|
|
44
|
-
| `runs [--task <number\|id>] [--open] [--mine]` | `GET .../runs` | one line per run |
|
|
44
|
+
| `runs [--task <number\|id>] [--open] [--mine] [--limit <n>] [--offset <n>]` | `GET .../runs` | one line per run, with when an open one was last heard from; a page at a time |
|
|
45
45
|
| `run <id>` | `GET .../runs/<id>` | the run in full, with its reports, what it handed in and where each file is read |
|
|
46
46
|
| `decision <id> --outcome <markdown>\|--file <path>` | `PATCH .../decisions/<id>` | the decision as a record, with what followed written down |
|
|
47
47
|
| `docs [<folder>] [--search <words>] [--archived]` | `GET .../docs?folder=` or `?q=` | the folders and the pages of a folder, or the pages found by words: one line per page |
|
|
@@ -63,6 +63,8 @@ A project's pages of Markdown ([ADR-0009](../adr/0009-documents-are-pages-of-mar
|
|
|
63
63
|
|
|
64
64
|
`report`, `hand-in`, `ask`, `finish` and `fail` act on a run. Named with `--run <id>`, that one; else the one open run begun with this token in the project (`GET .../runs?open=true&mine=true`). None open, or several: the command exits with `1` and says so, listing the open ones, so that an agent working two tasks at once says which.
|
|
65
65
|
|
|
66
|
+
`tasks`, `decisions` and `runs` show a page at a time: `DEFAULT_PAGE_LIMIT` lines unless `--limit <n>` says otherwise (up to `LIST_LIMIT`), from `--offset <n>` on, and a last line, `... more: --offset <n>`, says how to ask for the next page when there is one; with `--json`, the console's answer as it is, `more` included. An agent reads the first page and asks for the next only when what it looks for is not on it.
|
|
67
|
+
|
|
66
68
|
## Waiting for a decision
|
|
67
69
|
|
|
68
70
|
`ask` waits 60 seconds for the answer unless `--wait` says otherwise, within what an agent's shell gives one command; `decision <id> --wait <seconds>` waits as long as it is told, a day at most. The command asks the console to hold each request for up to 50 seconds (`?wait=`), the server's own bound, and repeats while the time lasts, with a breath between requests; the answer arrives as soon as a person gives it, since the console is woken through the database's own channel. A decision that still waits when the time is up is exit code `3`: the run waits with it, so the agent keeps waiting with `console decision <id> --wait 100`, within what its shell gives one command, and goes on meanwhile only with work that does not depend on the answer. `ask` prints the decision's id as soon as it is raised, before any waiting, so that a wait cut short loses nothing; with `--json` it prints the decision then, and again with its answer when that comes.
|
|
@@ -73,7 +75,7 @@ A project's pages of Markdown ([ADR-0009](../adr/0009-documents-are-pages-of-mar
|
|
|
73
75
|
|
|
74
76
|
## Output and exit codes
|
|
75
77
|
|
|
76
|
-
Plain text on standard output: one line per thing in a list, a few lines for one thing in full, with ids given, since the commands take them, and the vocabulary's own words (`in_progress`, `waiting`), since the options take them. What went wrong goes to standard error as the console's stable code and its words (`run.
|
|
78
|
+
Plain text on standard output: one line per thing in a list, a few lines for one thing in full, with ids given, since the commands take them, and the vocabulary's own words (`in_progress`, `waiting`), since the options take them. What went wrong goes to standard error as the console's stable code and its words (`run.task_closed: the task is done or dropped`), with a hint when there is something to do about it.
|
|
77
79
|
|
|
78
80
|
| Exit code | Meaning |
|
|
79
81
|
|---|---|
|
package/docs/specs/rest.md
CHANGED
|
@@ -20,6 +20,8 @@ The API is the board as its members see it: the people, the projects, and in eac
|
|
|
20
20
|
|---|---|---|
|
|
21
21
|
| 400 | `request.invalid` | A query parameter or the body does not parse; the message names the first field at fault. |
|
|
22
22
|
| 400 | `project.invalid_address` | The skills address is not one, as the standard spells them. |
|
|
23
|
+
| 400 | `project.invalid_icon` | The image for the mark is not a PNG, a JPEG, a GIF or a WebP, by its first bytes. |
|
|
24
|
+
| 404 | `project.no_icon` | The project has no image for its mark. |
|
|
23
25
|
| 400 | `member.invalid_person` | The person to list is not one of the workspace. |
|
|
24
26
|
| 400 | `task.invalid_assignee` | The assignee is not a person who may work in the project. |
|
|
25
27
|
| 400 | `task.invalid_parent` | The parent is not a task of the project, is the task itself, or would make a loop. |
|
|
@@ -55,7 +57,7 @@ The API is the board as its members see it: the people, the projects, and in eac
|
|
|
55
57
|
| 409 | `connect.approved` | The connection was approved already; the command claims its token. |
|
|
56
58
|
| 429 | `connect.too_many` | As many agents ask to connect as the console holds at once; try again in a few minutes. |
|
|
57
59
|
| 409 | `person.last_admin` | The change would leave the board without an administrator. |
|
|
58
|
-
| 409 | `run.over`, `run.
|
|
60
|
+
| 409 | `run.over`, `run.task_closed` | The run has ended already; the task is done or dropped. A run open on a task is no refusal: a new run takes it over ([ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)). |
|
|
59
61
|
| 413 | `request.too_large` | The body is over the limit; for a file handed in or attached, the file is over `MAX_FILE_BYTES`. |
|
|
60
62
|
| 503 | `file.store_unavailable` | The bytes of files are kept in a bucket, and it did not answer or refused the console; `retry-after` says when to try again. Nothing was written on the run or the page. |
|
|
61
63
|
|
|
@@ -75,11 +77,11 @@ Takes `role` and answers the person. For an administrator signed in on the conso
|
|
|
75
77
|
|
|
76
78
|
### `GET /api/v1/projects`
|
|
77
79
|
|
|
78
|
-
`{ "items": [project, ...] }`: the projects the asker may see, by name. A project carries its `key` (what paths and the command say; lowercase letters, digits and hyphens; immutable), `name`, `description`, `visibility` (`workspace`: everyone of the workspace is a member; `private`: only those listed), `skillsAddress` (canonical, or `null` for the organization's), `role` (what the asker is in it: `owner`, who configures it, or `member`, who works in it), `openDecisions` and `openRuns` (
|
|
80
|
+
`{ "items": [project, ...] }`: the projects the asker may see, by name. A project carries its `key` (what paths and the command say; lowercase letters, digits and hyphens; immutable), `name`, `description`, `visibility` (`workspace`: everyone of the workspace is a member; `private`: only those listed), `skillsAddress` (canonical, or `null` for the organization's), its mark, `color` (one of `COLOR_NAMES`, or `null`) and `symbol` (up to `MAX_PROJECT_SYMBOL_LENGTH` characters with no whitespace, or `null`; where either is `null` the pages choose one from the key and the name, `projectColorOf` and `projectSymbolOf`, so that projects are told apart either way), `icon` (the project's own image for its mark, shown in place of the symbol: its `sha256` and its `contentType`, one of `PROJECT_ICON_TYPES`; or `null`), `role` (what the asker is in it: `owner`, who configures it, or `member`, who works in it), `openDecisions` (the decisions waiting in it) and `openRuns` (the runs open in it: begun and not ended, which says nothing of whether an agent is still at them, [ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)), `createdAt` and `updatedAt`. A workspace administrator sees every project, as its owner; a member sees those open to the workspace, as a member, and those they are listed in, as what they are listed.
|
|
79
81
|
|
|
80
82
|
### `POST /api/v1/projects`
|
|
81
83
|
|
|
82
|
-
Takes `key` and `name`, and optionally `description`, `visibility` (default `private`)
|
|
84
|
+
Takes `key` and `name`, and optionally `description`, `visibility` (default `private`), `skillsAddress` (checked as the standard spells one, kept canonical; `project.invalid_address`), `color` and `symbol`. For whoever is signed in on the console's own pages, never with a token; they become the project's owner. Answers `201` with the project and writes a `project.created` event in it, with `key` and `name`. A key taken is `project.key_taken`.
|
|
83
85
|
|
|
84
86
|
### `GET /api/v1/events?after=&limit=`
|
|
85
87
|
|
|
@@ -99,7 +101,7 @@ Kept by an administrator signed in on the console's own pages, never with a toke
|
|
|
99
101
|
|
|
100
102
|
### `GET /api/v1/work`
|
|
101
103
|
|
|
102
|
-
`{ "tasks": [{ "project": { "key", "name" }, "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.
|
|
104
|
+
`{ "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.
|
|
103
105
|
|
|
104
106
|
### `GET /api/v1/tokens` and `POST /api/v1/tokens`
|
|
105
107
|
|
|
@@ -129,7 +131,11 @@ Everything below is under `/api/v1/projects/<key>`, for whoever may see the proj
|
|
|
129
131
|
|
|
130
132
|
### `GET /api/v1/projects/<key>` and `PATCH /api/v1/projects/<key>`
|
|
131
133
|
|
|
132
|
-
The project, as the asker sees it. A patch, for an owner signed in, takes any of `name`, `description`, `visibility
|
|
134
|
+
The project, as the asker sees it. A patch, for an owner signed in, takes any of `name`, `description`, `visibility`, `skillsAddress` (`null` for the organization's), `color` and `symbol` (`null` for none), and writes a `project.updated` event naming the `fields`; what is already so writes nothing. The key does not change.
|
|
135
|
+
|
|
136
|
+
### `POST /api/v1/projects/<key>/icon`, `DELETE .../icon` and `GET .../icon`
|
|
137
|
+
|
|
138
|
+
The project's own image for its mark, in place of the symbol on the colour. `POST`, for an owner signed in on the console's own pages, takes a form with a part called `file`: a PNG, a JPEG, a GIF or a WebP of at most `MAX_PROJECT_ICON_BYTES`, known by its first bytes and never by what the form says of it (`project.invalid_icon`; `request.too_large`). The bytes are kept by their hash, the project is answered with its `icon`, and the board hears `project.updated` with `fields: ["icon"]`; the same image again writes nothing. `DELETE` takes the image away and answers the project. `GET`, for whoever may see the project, answers the bytes with their type, the hash as `ETag` and `Cache-Control: private, max-age=86400`, or `304` to a matching `If-None-Match`; a project without an image is `project.no_icon`. The pages read it at `projectIconPath(key, icon)`, which carries the hash as a version.
|
|
133
139
|
|
|
134
140
|
### `GET /api/v1/projects/<key>/members` and `POST /api/v1/projects/<key>/members`
|
|
135
141
|
|
|
@@ -139,9 +145,9 @@ The project, as the asker sees it. A patch, for an owner signed in, takes any of
|
|
|
139
145
|
|
|
140
146
|
For an owner signed in. The patch takes `role` and writes `project.member_changed`; the removal answers `204` and writes `project.member_removed`. A person not listed is `member.not_found`. A project may be left without an owner listed; the administrators own it then.
|
|
141
147
|
|
|
142
|
-
### `GET /api/v1/projects/<key>/tasks?state=&open=&category=&goal=&assignee=&dueBy=`
|
|
148
|
+
### `GET /api/v1/projects/<key>/tasks?state=&open=&category=&goal=&assignee=&dueBy=&limit=&offset=`
|
|
143
149
|
|
|
144
|
-
`{ "items": [task, ...] }`: the project's board, newest first, at
|
|
150
|
+
`{ "items": [task, ...], "more": boolean }`: the project's board, newest first, a page at a time: `limit` (1 to `LIST_LIMIT`, the default) and `offset` (from 0) say which page, and `more` whether there is one after it; a `limit` or an `offset` that is not a number in range is `request.invalid`. `state` keeps one state of it; `open=true` the tasks not done or dropped, `open=false` the rest; `category=<key>` those sorted into a category; `goal=<id>` those that work towards a goal; `assignee=<person id>` those assigned to one person; `dueBy=<day>` (`2026-10-24`) those due on that day or before it. A task carries its `number` (the one people say out loud, numbered per project), `title`, `body` (Markdown, to be shown as text or rendered to elements, never as HTML), `state`, `priority`, `owner`, `assignee` (a person, or `null`), `parentId`, `category` (its `key`, `name` and `color` as the workspace keeps it, or `null`), `goalId` (or `null`), its plan ([ADR-0019](../adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md)): `startsOn` and `dueOn`, each a day (`2026-10-24`) or `null`; its moments, which the board keeps itself: `startedAt`, when it was first moved past `ready`, and `doneAt`, when it was moved to `done`, each an instant or `null`; `links` (`url` and `label`), `openDecisions` (how many decisions about it wait for a person), `openRuns` (how many runs on it are open: begun and not ended, which says nothing of whether an agent is still at it, [ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)), `createdAt` and `updatedAt`.
|
|
145
151
|
|
|
146
152
|
### `POST /api/v1/projects/<key>/tasks`
|
|
147
153
|
|
|
@@ -151,9 +157,9 @@ Takes `title`, and optionally `body`, `state` (default `idea`), `priority` (defa
|
|
|
151
157
|
|
|
152
158
|
One task, by its id or by its number in the project (`/tasks/7`): the pages hold ids, people and their agents say numbers. A patch takes the id and names only what changes, with the same fields as a creation; a field set to `null` clears it. A change of `state` is a `task.moved` event with `from` and `to`; any other change is one `task.updated` event naming the `fields` (`category`, `goalId`, `startsOn` and `dueOn` among them, as the input names them). A patch that changes nothing writes nothing and answers the task as it is. The moments follow the moves: the first move past `ready` sets `startedAt`, a move to `done` sets `doneAt`, and a move back out of `done` clears it.
|
|
153
159
|
|
|
154
|
-
### `GET /api/v1/projects/<key>/decisions?open=&task=`
|
|
160
|
+
### `GET /api/v1/projects/<key>/decisions?open=&task=&limit=&offset=`
|
|
155
161
|
|
|
156
|
-
`{ "items": [decision, ...] }`: the project's, the ones that wait first, newest first within each; `open=true` keeps only those that wait, `task=<id>` only those about one task. A decision carries its `question`, `body` (Markdown: the context it rests on), `options` (each an `id` and a `label`), `taskId` and `taskNumber` (or `null`), `raisedBy`, `run` (the `id` and `agent` of the run that raised it, when an agent asked, or `null`), `answer`: `null` while it waits, else the chosen `option`, a `note` or `null` (the rationale given with it), `by` and `at`; and `outcome`, what followed, in Markdown, or `null` until someone writes it. Context, rationale and outcome make the decision a record ([ADR-0009](../adr/0009-documents-are-pages-of-markdown-in-a-project-addressed-by-path-versioned-and-linked-both-ways.md)); a link in any of them to a document's path is kept both ways, so that the document says the decision refers to it.
|
|
162
|
+
`{ "items": [decision, ...], "more": boolean }`: the project's, the ones that wait first, newest first within each, a page at a time as the tasks are (`limit`, `offset`, `more`); `open=true` keeps only those that wait, `task=<id>` only those about one task. A decision carries its `question`, `body` (Markdown: the context it rests on), `options` (each an `id` and a `label`), `taskId` and `taskNumber` (or `null`), `raisedBy`, `run` (the `id` and `agent` of the run that raised it, when an agent asked, or `null`), `answer`: `null` while it waits, else the chosen `option`, a `note` or `null` (the rationale given with it), `by` and `at`; and `outcome`, what followed, in Markdown, or `null` until someone writes it. Context, rationale and outcome make the decision a record ([ADR-0009](../adr/0009-documents-are-pages-of-markdown-in-a-project-addressed-by-path-versioned-and-linked-both-ways.md)); a link in any of them to a document's path is kept both ways, so that the document says the decision refers to it.
|
|
157
163
|
|
|
158
164
|
### `POST /api/v1/projects/<key>/decisions`
|
|
159
165
|
|
|
@@ -177,13 +183,13 @@ The roadmap ([ADR-0019](../adr/0019-a-task-is-planned-in-days-the-board-keeps-it
|
|
|
177
183
|
|
|
178
184
|
One goal, for whoever may see the project. The patch, for an owner, takes any of `title`, `body`, `state` and `dueOn` (`null` for none), answers the goal and writes `goal.updated` naming the `fields`; a patch that changes nothing writes nothing. A goal is never deleted: one let go is `dropped`. A task works towards a goal through its `goalId`; the goal's events are read from the project's feed with `goal=<id>`.
|
|
179
185
|
|
|
180
|
-
### `GET /api/v1/projects/<key>/runs?task=&open=&mine=` and `GET .../runs/<id>`
|
|
186
|
+
### `GET /api/v1/projects/<key>/runs?task=&open=&mine=&limit=&offset=` and `GET .../runs/<id>`
|
|
181
187
|
|
|
182
|
-
`{ "items": [run, ...] }`: the project's runs, newest first, at
|
|
188
|
+
`{ "items": [run, ...], "more": boolean }`: the project's runs, newest first, a page at a time as the tasks are (`limit`, `offset`, `more`); `task=<id>` keeps those on one task, `open=true` those not over (`running` or `waiting`) and `open=false` those over, `mine=true` the asker's own: with a token, the runs begun with that token, which is one agent's; on a session, the runs for the person. A run is one agent's work on one task for one person: `taskId` and `taskNumber` (the one people say), `person` (whom the agent acts for), `agent` (what it calls itself), `status` (`running`, that is open, `waiting` for a decision, `finished`, `failed`, `abandoned`), `startedAt`, `endedAt` or `null`, `lastHeardAt` (when the agent was last heard from through the run: its start, then its latest report, hand-in or question; the console knows nothing of an agent but what it sends, and claims nothing more, [ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)), `summary` (Markdown, what the agent said at the end, or `null`), `reports` (each `id`, `body` in Markdown, `createdAt`; oldest first), `artifacts` (each `id`, `kind`, `url` for a link or `null`, `label` or `null`, `file` for a file or `null`, and `createdAt`; a file is its `name`, `size` in bytes, `contentType` and `sha256`), and `waitingFor`, the id of the decision the run waits for, or `null`.
|
|
183
189
|
|
|
184
190
|
### `POST /api/v1/projects/<key>/runs`
|
|
185
191
|
|
|
186
|
-
Takes `taskId` (a task of the project; `run.task_not_found` otherwise), and optionally `agent` (what people see
|
|
192
|
+
Takes `taskId` (a task of the project; `run.task_not_found` otherwise), and optionally `agent` (what people see on the run, `MAX_AGENT_LENGTH`; left out, the name of the token presented, or the person's login on a session). The run begins: the task becomes the asker's and `in_progress`, and the board is told (`run.started`). A run open on the task is taken over ([ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)): it ends as `abandoned`, with a summary that says who took over, and the board is told (`run.taken_over`, by the one who took over, with the `agent` that was at it), since the console cannot tell a run still at work from one whose agent is gone. A task that is `done` or `dropped` cannot be taken (`run.task_closed`). Answers `201` with the run, whose `id` the rest take.
|
|
187
193
|
|
|
188
194
|
### `POST .../runs/<id>/reports` and `POST .../runs/<id>/artifacts`
|
|
189
195
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillcdn/console",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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",
|
|
@@ -66,11 +66,11 @@ Produces the **plan** and the first build. Install first, in the new folder, so
|
|
|
66
66
|
|
|
67
67
|
### Phase 2: Configure before replacing
|
|
68
68
|
|
|
69
|
-
Produces the **configuration**. `createConsole(config)` takes `title`; `brand`, the addresses of a symbol and a wordmark; `languages`, the packs the pages speak, where a language of one's own is `src/i18n/en.ts` copied whole and translated; `components`, the pieces to replace, with the props `ConsoleComponents` names; `baseUrl`, the API's origin, the page's own when left out; and `client`, for tests. A console with marks of its own imports their files in `main.tsx`, so that the build takes them as assets, and passes their addresses through `brand`; `build: { assetsInlineLimit: 0 }` in `vite.config.ts` keeps every picture and font a file, since the policy allows a small picture inlined as a `data:` address but no `data:` font, and the default console only happens to have none small enough; sets `index.html`'s `<title>`, its `lang` and its icons, the mark as `favicon.svg` linked directly, with an `.ico` and a touch icon when the person has them (the default console links three from its brand package through a small build plugin); and loads the faces its languages need. Most wants are configuration or the styles: re-skinning starts and mostly ends with the tokens of `console.css` (the colours, the glass, the field), overridden by a stylesheet of the console's own loaded after the package's, with the mapping of the plan. Replace a component only when configuration cannot do it: start from its source under `src/components/`, keep its props, and keep or add `sc-` classes. With the look in place, show the pages and stop for the look checkpoint.
|
|
69
|
+
Produces the **configuration**. `createConsole(config)` takes `title`; `brand`, the addresses of a symbol and a wordmark; `languages`, the packs the pages speak, where a language of one's own is `src/i18n/en.ts` copied whole and translated; `components`, the pieces to replace, with the props `ConsoleComponents` names; `pages`, tabs of the console's own, each a `CustomPage` with a key, a label by language, a level (the workspace's pages or every project's) and `render`, given the data, the client, the navigation and the words; `baseUrl`, the API's origin, the page's own when left out; and `client`, for tests. The look is the stylesheet's tokens, `--sc-*` on `:root`, set in a stylesheet of the console's own loaded after `console.css`, and the blocks are styled by their `sc-` classes; the package's README lists the tokens by group. A console with marks of its own imports their files in `main.tsx`, so that the build takes them as assets, and passes their addresses through `brand`; `build: { assetsInlineLimit: 0 }` in `vite.config.ts` keeps every picture and font a file, since the policy allows a small picture inlined as a `data:` address but no `data:` font, and the default console only happens to have none small enough; sets `index.html`'s `<title>`, its `lang` and its icons, the mark as `favicon.svg` linked directly, with an `.ico` and a touch icon when the person has them (the default console links three from its brand package through a small build plugin); and loads the faces its languages need. Most wants are configuration or the styles: re-skinning starts and mostly ends with the tokens of `console.css` (the colours, the glass, the field), overridden by a stylesheet of the console's own loaded after the package's, with the mapping of the plan. Replace a component only when configuration cannot do it: start from its source under `src/components/`, keep its props, and keep or add `sc-` classes. With the look in place, show the pages and stop for the look checkpoint.
|
|
70
70
|
|
|
71
71
|
### Phase 3: Compose pages of one's own
|
|
72
72
|
|
|
73
|
-
Produces the **pages**. For a page the default console lacks: `useConsoleData(client, projectKey)` answers the workspace and the project the page is on, kept current by the project's stream, with the actions; the components take their data as props and nothing from the network; `matchRoute`, `PATHS`, `projectHref`, `taskHref`, `decisionHref` and `docHref` name the addresses; `Shell` is the header with the tabs and the person's menu. A composition of one's own is `src/console.tsx` with pages added or taken away. A request without a page of its own skips this phase.
|
|
73
|
+
Produces the **pages**. For a page the default console lacks: `useConsoleData(client, projectKey)` answers the workspace and the project the page is on, kept current by the project's stream, with the actions; the components take their data as props and nothing from the network; `matchRoute`, `PATHS`, `projectHref`, `taskHref`, `decisionHref` and `docHref` name the addresses; `Shell` is the header with the tabs and the person's menu. A page as a tab is `pages` in the configuration, its `render` composing the components over what it is given; a composition of one's own is `src/console.tsx` with pages added or taken away. A request without a page of its own skips this phase.
|
|
74
74
|
|
|
75
75
|
### Phase 4: Run it against the board
|
|
76
76
|
|
|
@@ -47,7 +47,7 @@ Each phase ends with something on the board. Only phase 4 waits for a person.
|
|
|
47
47
|
|
|
48
48
|
### Phase 1: Read
|
|
49
49
|
|
|
50
|
-
`console task <number>` shows the task in full, with its plan, the decisions about it and the runs on it. A run open on it
|
|
50
|
+
`console task <number>` shows the task in full, with its plan, the decisions about it and the runs on it. A run open on it means another agent was at it, and may still be: the console cannot tell ([ADR-0020](/docs/adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)), so read its reports and when it was last heard from, and take the task only when the person said to, since taking it ends that run as taken over; otherwise say so and stop. A task that is past its due day is still the task; the first report says so, and what the day was for is kept in mind. Read what the body links to, with `console doc <path>` for the project's pages and `console skills` for the organization's skills, before anything is made.
|
|
51
51
|
|
|
52
52
|
### Phase 2: Take
|
|
53
53
|
|
package/src/api.ts
CHANGED
|
@@ -12,6 +12,7 @@ export {
|
|
|
12
12
|
type EventFilter,
|
|
13
13
|
type FetchLike,
|
|
14
14
|
type FileUpload,
|
|
15
|
+
type Page,
|
|
15
16
|
type ProjectClient,
|
|
16
17
|
type TaskFilter,
|
|
17
18
|
} from "./client.js";
|
|
@@ -31,6 +32,7 @@ export {
|
|
|
31
32
|
nameOf,
|
|
32
33
|
} from "./documents.js";
|
|
33
34
|
export {
|
|
35
|
+
DEFAULT_PAGE_LIMIT,
|
|
34
36
|
DEFAULT_TOKEN_DAYS,
|
|
35
37
|
EVENTS_PAGE_LIMIT,
|
|
36
38
|
LIST_LIMIT,
|
|
@@ -53,8 +55,10 @@ export {
|
|
|
53
55
|
MAX_OPTION_LABEL_LENGTH,
|
|
54
56
|
MAX_OPTIONS,
|
|
55
57
|
MAX_PROJECT_DESCRIPTION_LENGTH,
|
|
58
|
+
MAX_PROJECT_ICON_BYTES,
|
|
56
59
|
MAX_PROJECT_KEY_LENGTH,
|
|
57
60
|
MAX_PROJECT_NAME_LENGTH,
|
|
61
|
+
MAX_PROJECT_SYMBOL_LENGTH,
|
|
58
62
|
MAX_QUESTION_LENGTH,
|
|
59
63
|
MAX_REPORTS_PER_RUN,
|
|
60
64
|
MAX_SEARCH_LENGTH,
|
|
@@ -69,6 +73,7 @@ export {
|
|
|
69
73
|
MAX_VERSIONS_PER_DOCUMENT,
|
|
70
74
|
MIN_OPTIONS,
|
|
71
75
|
} from "./limits.js";
|
|
76
|
+
export { projectColorOf, projectSymbolOf } from "./marks.js";
|
|
72
77
|
export {
|
|
73
78
|
AUTH_ROUTES,
|
|
74
79
|
CLAIM_PATH,
|
|
@@ -79,6 +84,7 @@ export {
|
|
|
79
84
|
PROJECT_COLLECTIONS,
|
|
80
85
|
type ProjectCollection,
|
|
81
86
|
personTokensPath,
|
|
87
|
+
projectIconPath,
|
|
82
88
|
projectPath,
|
|
83
89
|
REST_ROUTES,
|
|
84
90
|
RETURN_TO_PARAM,
|
|
@@ -133,6 +139,7 @@ export {
|
|
|
133
139
|
type RestPerson,
|
|
134
140
|
type RestPersonPatch,
|
|
135
141
|
type RestProject,
|
|
142
|
+
type RestProjectIcon,
|
|
136
143
|
type RestProjectInput,
|
|
137
144
|
type RestProjectPatch,
|
|
138
145
|
type RestProjects,
|
|
@@ -202,6 +209,7 @@ export {
|
|
|
202
209
|
restPeopleSchema,
|
|
203
210
|
restPersonPatchSchema,
|
|
204
211
|
restPersonSchema,
|
|
212
|
+
restProjectIconSchema,
|
|
205
213
|
restProjectInputSchema,
|
|
206
214
|
restProjectPatchSchema,
|
|
207
215
|
restProjectSchema,
|
|
@@ -236,6 +244,8 @@ export {
|
|
|
236
244
|
type ArtifactKind,
|
|
237
245
|
CATEGORY_COLORS,
|
|
238
246
|
type CategoryColor,
|
|
247
|
+
COLOR_NAMES,
|
|
248
|
+
type ColorName,
|
|
239
249
|
EVENT_KINDS,
|
|
240
250
|
type EventKind,
|
|
241
251
|
GOAL_STATES,
|
|
@@ -246,9 +256,11 @@ export {
|
|
|
246
256
|
OPEN_TASK_STATES,
|
|
247
257
|
PERSON_ROLES,
|
|
248
258
|
type PersonRole,
|
|
259
|
+
PROJECT_ICON_TYPES,
|
|
249
260
|
PROJECT_ROLES,
|
|
250
261
|
PROJECT_VISIBILITIES,
|
|
251
262
|
PROVIDER_KEYS,
|
|
263
|
+
type ProjectIconType,
|
|
252
264
|
type ProjectRole,
|
|
253
265
|
type ProjectVisibility,
|
|
254
266
|
type ProviderKey,
|
package/src/cli/cli.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type ProjectClient,
|
|
9
9
|
} from "../client.js";
|
|
10
10
|
import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
|
|
11
|
-
import { MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
11
|
+
import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
12
12
|
import type { RestClaim, RestDecision, RestTaskPatch } from "../schemas.js";
|
|
13
13
|
import {
|
|
14
14
|
GOAL_STATES,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
formatDocuments,
|
|
31
31
|
formatGoal,
|
|
32
32
|
formatGoalLine,
|
|
33
|
+
formatMore,
|
|
33
34
|
formatOptions,
|
|
34
35
|
formatProjectLine,
|
|
35
36
|
formatRun,
|
|
@@ -351,6 +352,26 @@ function secondsOf(value: string | undefined, fallback: number): number {
|
|
|
351
352
|
return seconds;
|
|
352
353
|
}
|
|
353
354
|
|
|
355
|
+
/** The flags of a page of a listing: how many, from which one on. */
|
|
356
|
+
const PAGE_OPTIONS: OptionSpec = { limit: { type: "string" }, offset: { type: "string" } };
|
|
357
|
+
|
|
358
|
+
/** The page the flags ask for: `DEFAULT_PAGE_LIMIT` items from the first when nothing is said. */
|
|
359
|
+
function pageOf(values: Values): { readonly limit: number; readonly offset: number } {
|
|
360
|
+
const limit = text(values, "limit");
|
|
361
|
+
const offset = text(values, "offset");
|
|
362
|
+
const page = {
|
|
363
|
+
limit: limit === undefined ? DEFAULT_PAGE_LIMIT : Number(limit),
|
|
364
|
+
offset: offset === undefined ? 0 : Number(offset),
|
|
365
|
+
};
|
|
366
|
+
if (!Number.isInteger(page.limit) || page.limit < 1 || page.limit > LIST_LIMIT) {
|
|
367
|
+
throw misuse(`Not a number of items, 1 to ${LIST_LIMIT}: ${limit}`);
|
|
368
|
+
}
|
|
369
|
+
if (!Number.isInteger(page.offset) || page.offset < 0) {
|
|
370
|
+
throw misuse(`Not a number of items to skip: ${offset}`);
|
|
371
|
+
}
|
|
372
|
+
return page;
|
|
373
|
+
}
|
|
374
|
+
|
|
354
375
|
/** One positional, and nothing else. */
|
|
355
376
|
function theOne(positionals: readonly string[], ask: string): string {
|
|
356
377
|
const [first] = positionals;
|
|
@@ -546,10 +567,12 @@ const tasks: Command = async (args, io) => {
|
|
|
546
567
|
category: { type: "string" },
|
|
547
568
|
goal: { type: "string" },
|
|
548
569
|
due: { type: "string" },
|
|
570
|
+
...PAGE_OPTIONS,
|
|
549
571
|
},
|
|
550
572
|
false,
|
|
551
573
|
);
|
|
552
574
|
const state = stateOf(text(values, "state"));
|
|
575
|
+
const page = pageOf(values);
|
|
553
576
|
const session = await open(io, values);
|
|
554
577
|
const project = await session.project();
|
|
555
578
|
// What is due: a day, or the words for the usual windows; overdue keeps only the open ones.
|
|
@@ -573,9 +596,13 @@ const tasks: Command = async (args, io) => {
|
|
|
573
596
|
...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
|
|
574
597
|
...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
|
|
575
598
|
...(dueBy === undefined ? {} : { dueBy }),
|
|
599
|
+
...page,
|
|
576
600
|
});
|
|
577
601
|
answer(io, session, found, () =>
|
|
578
|
-
found.items.length === 0
|
|
602
|
+
found.items.length === 0
|
|
603
|
+
? "No tasks."
|
|
604
|
+
: found.items.map(formatTaskLine).join("\n") +
|
|
605
|
+
formatMore(found.more, page.limit, page.offset),
|
|
579
606
|
);
|
|
580
607
|
return EXIT.ok;
|
|
581
608
|
};
|
|
@@ -734,7 +761,7 @@ const take: Command = async (args, io) => {
|
|
|
734
761
|
});
|
|
735
762
|
answer(io, session, { run, task: found }, () =>
|
|
736
763
|
[
|
|
737
|
-
`Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.`,
|
|
764
|
+
`Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs. A run that was open on it is over now: the board cannot know whether its agent still ran, so taking it was deciding.`,
|
|
738
765
|
"",
|
|
739
766
|
formatTask(found),
|
|
740
767
|
"",
|
|
@@ -1154,12 +1181,19 @@ const goal: Command = async (args, io) => {
|
|
|
1154
1181
|
};
|
|
1155
1182
|
|
|
1156
1183
|
const decisions: Command = async (args, io) => {
|
|
1157
|
-
const { values } = parse(args, { open: { type: "boolean" } }, false);
|
|
1184
|
+
const { values } = parse(args, { open: { type: "boolean" }, ...PAGE_OPTIONS }, false);
|
|
1185
|
+
const page = pageOf(values);
|
|
1158
1186
|
const session = await open(io, values);
|
|
1159
1187
|
const project = await session.project();
|
|
1160
|
-
const found = await project.decisions(
|
|
1188
|
+
const found = await project.decisions({
|
|
1189
|
+
...(on(values, "open") ? { open: true } : {}),
|
|
1190
|
+
...page,
|
|
1191
|
+
});
|
|
1161
1192
|
answer(io, session, found, () =>
|
|
1162
|
-
found.items.length === 0
|
|
1193
|
+
found.items.length === 0
|
|
1194
|
+
? "No decisions."
|
|
1195
|
+
: found.items.map(formatDecisionLine).join("\n") +
|
|
1196
|
+
formatMore(found.more, page.limit, page.offset),
|
|
1163
1197
|
);
|
|
1164
1198
|
return EXIT.ok;
|
|
1165
1199
|
};
|
|
@@ -1212,9 +1246,15 @@ const abandon: Command = async (args, io) => {
|
|
|
1212
1246
|
const runs: Command = async (args, io) => {
|
|
1213
1247
|
const { values } = parse(
|
|
1214
1248
|
args,
|
|
1215
|
-
{
|
|
1249
|
+
{
|
|
1250
|
+
task: { type: "string" },
|
|
1251
|
+
open: { type: "boolean" },
|
|
1252
|
+
mine: { type: "boolean" },
|
|
1253
|
+
...PAGE_OPTIONS,
|
|
1254
|
+
},
|
|
1216
1255
|
false,
|
|
1217
1256
|
);
|
|
1257
|
+
const page = pageOf(values);
|
|
1218
1258
|
const session = await open(io, values);
|
|
1219
1259
|
const project = await session.project();
|
|
1220
1260
|
const ref = text(values, "task");
|
|
@@ -1223,9 +1263,12 @@ const runs: Command = async (args, io) => {
|
|
|
1223
1263
|
...(taskId === undefined ? {} : { task: taskId }),
|
|
1224
1264
|
...(on(values, "open") ? { open: true } : {}),
|
|
1225
1265
|
...(on(values, "mine") ? { mine: true } : {}),
|
|
1266
|
+
...page,
|
|
1226
1267
|
});
|
|
1227
1268
|
answer(io, session, found, () =>
|
|
1228
|
-
found.items.length === 0
|
|
1269
|
+
found.items.length === 0
|
|
1270
|
+
? "No runs."
|
|
1271
|
+
: found.items.map(formatRunLine).join("\n") + formatMore(found.more, page.limit, page.offset),
|
|
1229
1272
|
);
|
|
1230
1273
|
return EXIT.ok;
|
|
1231
1274
|
};
|
package/src/cli/format.ts
CHANGED
|
@@ -39,12 +39,14 @@ export function formatProjectLine(project: RestProject): string {
|
|
|
39
39
|
if (project.openDecisions > 0) {
|
|
40
40
|
notes.push(`${plural(project.openDecisions, "decision")} waiting`);
|
|
41
41
|
}
|
|
42
|
-
if (project.openRuns > 0) {
|
|
43
|
-
notes.push(`${plural(project.openRuns, "agent")} at work`);
|
|
44
|
-
}
|
|
45
42
|
return `${project.key} ${project.name} (${notes.join("; ")})`;
|
|
46
43
|
}
|
|
47
44
|
|
|
45
|
+
/** The line under a page of a listing that has more after it: how to ask for the next page. */
|
|
46
|
+
export function formatMore(more: boolean, limit: number, offset: number): string {
|
|
47
|
+
return more ? `\n... more: --offset ${offset + limit}` : "";
|
|
48
|
+
}
|
|
49
|
+
|
|
48
50
|
export function formatTaskLine(task: RestTask): string {
|
|
49
51
|
const notes = [`owner ${task.owner.login}`];
|
|
50
52
|
if (task.assignee !== null) {
|
|
@@ -59,9 +61,6 @@ export function formatTaskLine(task: RestTask): string {
|
|
|
59
61
|
if (task.openDecisions > 0) {
|
|
60
62
|
notes.push(`${plural(task.openDecisions, "decision")} waiting`);
|
|
61
63
|
}
|
|
62
|
-
if (task.openRuns > 0) {
|
|
63
|
-
notes.push("an agent at work");
|
|
64
|
-
}
|
|
65
64
|
return `#${task.number} ${task.state} ${task.priority} ${task.title} (${notes.join("; ")})`;
|
|
66
65
|
}
|
|
67
66
|
|
|
@@ -128,6 +127,9 @@ export function formatRunLine(run: RestRun): string {
|
|
|
128
127
|
if (run.waitingFor !== null) {
|
|
129
128
|
notes.push(`waiting for decision ${run.waitingFor}`);
|
|
130
129
|
}
|
|
130
|
+
if (run.endedAt === null) {
|
|
131
|
+
notes.push(`last heard ${run.lastHeardAt}`);
|
|
132
|
+
}
|
|
131
133
|
return `${run.id} ${run.status} ${run.agent} for ${run.person.login} task #${run.taskNumber} (${notes.join(", ")})`;
|
|
132
134
|
}
|
|
133
135
|
|
package/src/cli/help.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_PAGE_LIMIT,
|
|
2
3
|
MAX_AGENT_LENGTH,
|
|
3
4
|
MAX_ARTIFACTS_PER_RUN,
|
|
4
5
|
MAX_BODY_LENGTH,
|
|
@@ -105,9 +106,8 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
105
106
|
},
|
|
106
107
|
tasks: {
|
|
107
108
|
usage:
|
|
108
|
-
"tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>]",
|
|
109
|
-
about:
|
|
110
|
-
"The tasks on the board, newest first: number, state, priority, title, who owns and works on each, its category and its due day, and what waits on it. --open keeps the ones not done or dropped, --mine those assigned to you, --category and --goal those sorted into one, --due those due by a day: today, within the week, or overdue (the open ones past their day). States: idea, ready, in_progress, in_review, done, dropped.",
|
|
109
|
+
"tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>] [--limit <n>] [--offset <n>]",
|
|
110
|
+
about: `The tasks on the board, newest first: number, state, priority, title, who owns and works on each, its category and its due day, and what waits on it. --open keeps the ones not done or dropped, --mine those assigned to you, --category and --goal those sorted into one, --due those due by a day: today, within the week, or overdue (the open ones past their day). A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on; a last line says when there is more. States: idea, ready, in_progress, in_review, done, dropped.`,
|
|
111
111
|
},
|
|
112
112
|
task: {
|
|
113
113
|
usage: `task <number|id> | task new <title> [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS} | task edit <number|id> [--title <title>] [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS}`,
|
|
@@ -153,11 +153,10 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
153
153
|
take: {
|
|
154
154
|
usage: "take <number|id> [--agent <name>]",
|
|
155
155
|
about:
|
|
156
|
-
"Takes a task: a run begins, and the task is in progress and the person's.
|
|
156
|
+
"Takes a task: a run begins, and the task is in progress and the person's. 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. Look before taking (console task <number> lists the runs, with when each was last heard from), and take a task another agent was heard from on lately only when told to. --agent is what people see at work; left out, it is what the person called the token.",
|
|
157
157
|
limits: [`agent: one line, up to ${MAX_AGENT_LENGTH} characters`],
|
|
158
158
|
refusals: [
|
|
159
159
|
"task.not_found: no task has that number or id",
|
|
160
|
-
"run.task_taken: a run is open on the task already; console runs --open --mine shows whether it is yours",
|
|
161
160
|
"run.task_closed: the task is done or dropped",
|
|
162
161
|
],
|
|
163
162
|
},
|
|
@@ -207,8 +206,8 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
207
206
|
refusals: ["decision.not_found: no decision has that id"],
|
|
208
207
|
},
|
|
209
208
|
decisions: {
|
|
210
|
-
usage: "decisions [--open]",
|
|
211
|
-
about:
|
|
209
|
+
usage: "decisions [--open] [--limit <n>] [--offset <n>]",
|
|
210
|
+
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.`,
|
|
212
211
|
},
|
|
213
212
|
finish: {
|
|
214
213
|
usage: 'finish [--summary "<markdown>"|--file <path>]',
|
|
@@ -231,9 +230,8 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
231
230
|
refusals: RUN_REFUSALS,
|
|
232
231
|
},
|
|
233
232
|
runs: {
|
|
234
|
-
usage: "runs [--task <number|id>] [--open] [--mine]",
|
|
235
|
-
about:
|
|
236
|
-
"The runs, newest first: agents at work and what they did. --mine keeps those begun with this token.",
|
|
233
|
+
usage: "runs [--task <number|id>] [--open] [--mine] [--limit <n>] [--offset <n>]",
|
|
234
|
+
about: `The runs, newest first, each with when its agent was last heard from: --open keeps the ones not over, --mine those begun with this token. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on.`,
|
|
237
235
|
},
|
|
238
236
|
run: {
|
|
239
237
|
usage: "run <id>",
|
|
@@ -312,6 +310,8 @@ Usage: console <command> [arguments] [--json] [--url <origin>] [--project <key>]
|
|
|
312
310
|
|
|
313
311
|
const CLOSING = `Options everywhere: --json answers with the console's own JSON; --url names another console;
|
|
314
312
|
--project names the project to work in, over CONSOLE_PROJECT and over what console use kept here.
|
|
313
|
+
A listing (tasks, decisions, runs) is a page at a time: --limit <n> and --offset <n>, and a last line
|
|
314
|
+
says when there is more, so that nothing is read that is not needed.
|
|
315
315
|
Commands on a run (report, hand-in, ask, finish, fail) act on your one open run in the project;
|
|
316
316
|
with several open, say which with --run <id>. A Markdown body given as - is read from standard input.
|
|
317
317
|
console help <command> says what each command takes, the limits the console holds it to, and
|
package/src/client.ts
CHANGED
|
@@ -150,8 +150,14 @@ export interface EventFilter {
|
|
|
150
150
|
readonly document?: string | undefined;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/** One page of a listing: how many items, from which one on; the server's bounds apply. */
|
|
154
|
+
export interface Page {
|
|
155
|
+
readonly limit?: number | undefined;
|
|
156
|
+
readonly offset?: number | undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
153
159
|
/** What the board is narrowed to: one state, the open ones, a category, a goal, an assignee, or what is due by a day. */
|
|
154
|
-
export interface TaskFilter {
|
|
160
|
+
export interface TaskFilter extends Page {
|
|
155
161
|
readonly state?: TaskState | undefined;
|
|
156
162
|
/** True for the tasks not done or dropped; false for the rest. */
|
|
157
163
|
readonly open?: boolean | undefined;
|
|
@@ -183,6 +189,10 @@ export interface ProjectClient {
|
|
|
183
189
|
get(signal?: AbortSignal): Promise<RestProject>;
|
|
184
190
|
/** Changes the project's settings; for an owner. */
|
|
185
191
|
update(patch: RestProjectPatch): Promise<RestProject>;
|
|
192
|
+
/** Sets the project's own image for its mark, sent as a form; for an owner on the console's own pages. */
|
|
193
|
+
setIcon(input: FileUpload): Promise<RestProject>;
|
|
194
|
+
/** Takes the image away: the mark is the symbol on the colour again. */
|
|
195
|
+
removeIcon(): Promise<RestProject>;
|
|
186
196
|
/** Those listed in the project, by login. */
|
|
187
197
|
members(signal?: AbortSignal): Promise<RestMembers>;
|
|
188
198
|
/** Lists a person of the workspace in the project; for an owner. */
|
|
@@ -194,9 +204,9 @@ export interface ProjectClient {
|
|
|
194
204
|
task(ref: string, signal?: AbortSignal): Promise<RestTask>;
|
|
195
205
|
createTask(input: RestTaskInput): Promise<RestTask>;
|
|
196
206
|
updateTask(id: string, patch: RestTaskPatch): Promise<RestTask>;
|
|
197
|
-
/** The decisions, or only those that wait, or only those about one task. */
|
|
207
|
+
/** The decisions, or only those that wait, or only those about one task; a page of them. */
|
|
198
208
|
decisions(
|
|
199
|
-
filter?: { readonly open?: boolean; readonly task?: string },
|
|
209
|
+
filter?: { readonly open?: boolean; readonly task?: string } & Page,
|
|
200
210
|
signal?: AbortSignal,
|
|
201
211
|
): Promise<RestDecisions>;
|
|
202
212
|
decision(id: string, signal?: AbortSignal): Promise<RestDecision>;
|
|
@@ -209,9 +219,9 @@ export interface ProjectClient {
|
|
|
209
219
|
answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
|
|
210
220
|
/** Grows a decision's record: its context, or what followed. */
|
|
211
221
|
updateDecision(id: string, patch: RestDecisionPatch): Promise<RestDecision>;
|
|
212
|
-
/** The runs, newest first; or only those on one task, only the open ones, only the asker's own. */
|
|
222
|
+
/** The runs, newest first; or only those on one task, only the open ones, only the asker's own; a page of them. */
|
|
213
223
|
runs(
|
|
214
|
-
filter?: { readonly task?: string; readonly open?: boolean; readonly mine?: boolean },
|
|
224
|
+
filter?: { readonly task?: string; readonly open?: boolean; readonly mine?: boolean } & Page,
|
|
215
225
|
signal?: AbortSignal,
|
|
216
226
|
): Promise<RestRuns>;
|
|
217
227
|
run(id: string, signal?: AbortSignal): Promise<RestRun>;
|
|
@@ -381,6 +391,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
381
391
|
|
|
382
392
|
const flag = (value: boolean | undefined): string | undefined =>
|
|
383
393
|
value === undefined ? undefined : String(value);
|
|
394
|
+
const number = (value: number | undefined): string | undefined =>
|
|
395
|
+
value === undefined ? undefined : String(value);
|
|
384
396
|
|
|
385
397
|
/** A file as the parts of a form: the bytes with their name and type, and the label. */
|
|
386
398
|
const formOf = (input: FileUpload): FormData => {
|
|
@@ -405,6 +417,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
405
417
|
key,
|
|
406
418
|
get: (signal) => request("GET", `${base}${at()}`, undefined, restProjectSchema, signal),
|
|
407
419
|
update: (patch) => request("PATCH", `${base}${at()}`, patch, restProjectSchema),
|
|
420
|
+
setIcon: (input) => request("POST", `${base}${at()}/icon`, formOf(input), restProjectSchema),
|
|
421
|
+
removeIcon: () => request("DELETE", `${base}${at()}/icon`, undefined, restProjectSchema),
|
|
408
422
|
members: (signal) =>
|
|
409
423
|
request("GET", `${base}${at("members")}`, undefined, restMembersSchema, signal),
|
|
410
424
|
addMember: (input) => request("POST", `${base}${at("members")}`, input, restMemberSchema),
|
|
@@ -422,6 +436,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
422
436
|
goal: filter.goal,
|
|
423
437
|
assignee: filter.assignee,
|
|
424
438
|
dueBy: filter.dueBy,
|
|
439
|
+
limit: number(filter.limit),
|
|
440
|
+
offset: number(filter.offset),
|
|
425
441
|
}),
|
|
426
442
|
undefined,
|
|
427
443
|
restTasksSchema,
|
|
@@ -435,7 +451,12 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
435
451
|
decisions: (filter = {}, signal) =>
|
|
436
452
|
request(
|
|
437
453
|
"GET",
|
|
438
|
-
withQuery(at("decisions"), {
|
|
454
|
+
withQuery(at("decisions"), {
|
|
455
|
+
open: flag(filter.open),
|
|
456
|
+
task: filter.task,
|
|
457
|
+
limit: number(filter.limit),
|
|
458
|
+
offset: number(filter.offset),
|
|
459
|
+
}),
|
|
439
460
|
undefined,
|
|
440
461
|
restDecisionsSchema,
|
|
441
462
|
signal,
|
|
@@ -463,6 +484,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
463
484
|
task: filter.task,
|
|
464
485
|
open: flag(filter.open),
|
|
465
486
|
mine: flag(filter.mine),
|
|
487
|
+
limit: number(filter.limit),
|
|
488
|
+
offset: number(filter.offset),
|
|
466
489
|
}),
|
|
467
490
|
undefined,
|
|
468
491
|
restRunsSchema,
|