@gavin-hu/my-pi-agent 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web-settle.ts +14 -7
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Type } from "typebox";
|
|
11
|
-
import {
|
|
11
|
+
import { MAX_NOTE, PRIORITIES } from "../../lib/board/card.ts";
|
|
12
|
+
import { listOf } from "../../lib/format.ts";
|
|
13
|
+
import { BLOCKERS } from "../../lib/board/card.ts";
|
|
14
|
+
import { LANES, movableLanes } from "../../lib/board/lanes.ts";
|
|
12
15
|
import { BOARD_ACTIONS } from "./types.ts";
|
|
13
16
|
|
|
14
17
|
export const BoardParams = Type.Object({
|
|
@@ -24,15 +27,18 @@ export const BoardParams = Type.Object({
|
|
|
24
27
|
card: Type.Optional(
|
|
25
28
|
Type.String({
|
|
26
29
|
description:
|
|
27
|
-
'The card id for one card, e.g. "4" or "#4". `list` reads that card; every other action but `add` acts on it.',
|
|
30
|
+
'The card id for one card, e.g. "4" or "#4". `list` reads that card, and refuses `lane`, `query`, `mine`, and `round` with it; every other action but `add` acts on it.',
|
|
28
31
|
}),
|
|
29
32
|
),
|
|
30
|
-
|
|
33
|
+
lane: Type.Optional(
|
|
31
34
|
Type.Union(
|
|
32
|
-
|
|
35
|
+
LANES.map((lane) => Type.Literal(lane)),
|
|
33
36
|
{
|
|
37
|
+
// The lanes a move may name come from the transition table, so a route added
|
|
38
|
+
// there is offered here without a second list to forget.
|
|
34
39
|
description:
|
|
35
|
-
"`list`: show only this lane, and not skip the `done` cards.
|
|
40
|
+
"`list`: show only this lane, and not skip the `done` cards. " +
|
|
41
|
+
`\`move\`: the lane to move to — ${listOf(movableLanes())}; \`backlog\` is \`release\`'s, not a move's, and a card may move only where a transition exists from its own lane.`,
|
|
36
42
|
},
|
|
37
43
|
),
|
|
38
44
|
),
|
|
@@ -42,6 +48,15 @@ export const BoardParams = Type.Object({
|
|
|
42
48
|
"`move` to `blocked`: what the card waits on — the decision a person has to make, one line, at most 200 characters.",
|
|
43
49
|
}),
|
|
44
50
|
),
|
|
51
|
+
blocker: Type.Optional(
|
|
52
|
+
Type.Union(
|
|
53
|
+
BLOCKERS.map((blocker) => Type.Literal(blocker)),
|
|
54
|
+
{
|
|
55
|
+
description:
|
|
56
|
+
"`move` to `blocked`, required: why it is blocked — the errand, not the route. A blocked card resumes in `plan`, or in `execute` once its contract is approved.",
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
),
|
|
45
60
|
title: Type.Optional(Type.String({ description: "`add` and `edit`: the feature's outcome, one line." })),
|
|
46
61
|
paths: Type.Optional(
|
|
47
62
|
Type.Array(Type.String(), {
|
|
@@ -49,9 +64,31 @@ export const BoardParams = Type.Object({
|
|
|
49
64
|
"`add` and `edit`: repository-relative hints (directories or files) where the work happens. Not claims.",
|
|
50
65
|
}),
|
|
51
66
|
),
|
|
52
|
-
note: Type.Optional(
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
note: Type.Optional(
|
|
68
|
+
Type.String({
|
|
69
|
+
description: `\`add\`: a first note, e.g. why the card exists; one line, at most ${MAX_NOTE} characters.`,
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
text: Type.Optional(
|
|
73
|
+
Type.String({
|
|
74
|
+
description: `\`note\`: the note to append; \`approve\` and \`review\`: what the signer saw, recorded with the sign; one line, at most ${MAX_NOTE} characters.`,
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
path: Type.Optional(
|
|
78
|
+
Type.String({
|
|
79
|
+
description:
|
|
80
|
+
"`contract`, required: the plan file this card is executed against — the path `plan` action `write` returned, inside this repository's plan store.",
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
goal: Type.Optional(
|
|
84
|
+
Type.String({ description: "`contract`: what the feature is for, one line, at most 400 characters." }),
|
|
85
|
+
),
|
|
86
|
+
acceptance: Type.Optional(
|
|
87
|
+
Type.String({ description: "`contract`: how a reader can tell it is done, one line, at most 400 characters." }),
|
|
88
|
+
),
|
|
89
|
+
scopeOut: Type.Optional(
|
|
90
|
+
Type.String({ description: "`contract`: what this deliberately does not do, one line, at most 400 characters." }),
|
|
91
|
+
),
|
|
55
92
|
priority: Type.Optional(
|
|
56
93
|
Type.Union(
|
|
57
94
|
PRIORITIES.map((priority) => Type.Literal(priority)),
|
|
@@ -64,7 +101,7 @@ export const BoardParams = Type.Object({
|
|
|
64
101
|
dependsOn: Type.Optional(
|
|
65
102
|
Type.Array(Type.String(), {
|
|
66
103
|
description:
|
|
67
|
-
"`edit`: the cards this one waits for, as ids. It cannot be claimed or moved to
|
|
104
|
+
"`edit`: the cards this one waits for, as ids. It cannot be claimed, or moved to execute or done, while one of them is unfinished; `[]` removes every link.",
|
|
68
105
|
}),
|
|
69
106
|
),
|
|
70
107
|
mine: Type.Optional(
|
|
@@ -78,13 +115,14 @@ export const BoardParams = Type.Object({
|
|
|
78
115
|
),
|
|
79
116
|
round: Type.Optional(
|
|
80
117
|
Type.Boolean({
|
|
81
|
-
description:
|
|
118
|
+
description:
|
|
119
|
+
"`list`: also return the lead's rollup — owners, unassigned, unplanned, awaiting approval, blocked, idle, and done.",
|
|
82
120
|
}),
|
|
83
121
|
),
|
|
84
122
|
archived: Type.Optional(
|
|
85
123
|
Type.Boolean({
|
|
86
124
|
description:
|
|
87
|
-
"`list`: read the archive instead of the board — the finished cards `archive` set aside, which no other read opens. `query` and `mine` narrow it; `
|
|
125
|
+
"`list`: read the archive instead of the board — the finished cards `archive` set aside, which no other read opens. `query` and `mine` narrow it; `lane` and `round` do not apply to a pile that is out of the board.",
|
|
88
126
|
}),
|
|
89
127
|
),
|
|
90
128
|
});
|
|
@@ -18,8 +18,6 @@ export interface SnapshotContext {
|
|
|
18
18
|
store: BoardStore;
|
|
19
19
|
/** The actor every request is made and reported as: the page's seat. */
|
|
20
20
|
actor: () => Actor;
|
|
21
|
-
/** The session serving the page, which is who is reading, not who it writes as. */
|
|
22
|
-
session?: () => Actor;
|
|
23
21
|
now: () => number;
|
|
24
22
|
/** Where this repository's bus files its records. */
|
|
25
23
|
busDir: () => Promise<string>;
|
|
@@ -28,15 +26,19 @@ export interface SnapshotContext {
|
|
|
28
26
|
/** The snapshot the page renders, and the body of `/api/board`. */
|
|
29
27
|
export async function viewOf(context: SnapshotContext): Promise<BoardView> {
|
|
30
28
|
const roster = await readRoster(await context.busDir());
|
|
31
|
-
|
|
29
|
+
// One read of the pile answers both: a frame that asked `list` and `skipped` separately
|
|
30
|
+
// would parse every card twice, once a second, for as long as a tab is open.
|
|
31
|
+
const { cards, skipped } = await context.store.scan();
|
|
32
32
|
return buildView({
|
|
33
33
|
me: context.actor(),
|
|
34
|
-
|
|
35
|
-
cards: await context.store.list(),
|
|
34
|
+
cards,
|
|
36
35
|
now: context.now(),
|
|
37
36
|
bytes: await context.store.bytes(),
|
|
38
|
-
skipped
|
|
37
|
+
skipped,
|
|
38
|
+
repo: context.store.name(),
|
|
39
|
+
root: context.store.root(),
|
|
39
40
|
where: context.store.where(),
|
|
41
|
+
limits: context.store.config().wip,
|
|
40
42
|
...(roster === undefined ? {} : { roster }),
|
|
41
43
|
});
|
|
42
44
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal's word for a board tone.
|
|
3
|
+
*
|
|
4
|
+
* The tone table (`lib/board/tone.ts`) says what a lane, a reason, and a decision row mean;
|
|
5
|
+
* the page wears those words as CSS properties directly. The terminal has its own vocabulary —
|
|
6
|
+
* `lib/tui/tokens.ts`'s roles are `ok`, `warn`, `bad`, `meta`, `faint`, and deliberately not
|
|
7
|
+
* `success`, `danger`, or `muted` — so this is the one place the two are bridged, in the
|
|
8
|
+
* extension, because only the transcript speaks the terminal's words.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { BoardTone } from "../../lib/board/tone.ts";
|
|
12
|
+
import type { TuiRole } from "../../lib/tui/tokens.ts";
|
|
13
|
+
|
|
14
|
+
/** The role each board tone is painted as, closest first. */
|
|
15
|
+
export const TONE_ROLE: Record<BoardTone, TuiRole> = {
|
|
16
|
+
muted: "meta",
|
|
17
|
+
dim: "faint",
|
|
18
|
+
warn: "warn",
|
|
19
|
+
accent: "accent",
|
|
20
|
+
danger: "bad",
|
|
21
|
+
success: "ok",
|
|
22
|
+
};
|
|
@@ -14,30 +14,35 @@
|
|
|
14
14
|
|
|
15
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import type { Static } from "typebox";
|
|
17
|
-
import {
|
|
17
|
+
import { type ArtifactCheck, planForContract, planForExecute } from "../../lib/board/artifact.ts";
|
|
18
|
+
import { type Card, cardRef, holds, matchesQuery, normalizeCardId } from "../../lib/board/card.ts";
|
|
19
|
+
import { movableLanes } from "../../lib/board/lanes.ts";
|
|
18
20
|
import { indexOf } from "../../lib/board/links.ts";
|
|
19
|
-
import {
|
|
21
|
+
import type { CardOp } from "../../lib/board/rules.ts";
|
|
20
22
|
import { buildRound } from "../../lib/board/round.ts";
|
|
23
|
+
import { readRoster, type RosterSession } from "../../lib/session-roster.ts";
|
|
21
24
|
import type { BoardStore } from "../../lib/board/store.ts";
|
|
22
25
|
import { messageOf } from "../../lib/errors.ts";
|
|
26
|
+
import { listOf } from "../../lib/format.ts";
|
|
23
27
|
import { revParse } from "../../lib/git/read.ts";
|
|
24
28
|
import { strayField } from "../../lib/stray-fields.ts";
|
|
25
29
|
import { createExecRunner } from "../../lib/git/runner.ts";
|
|
26
30
|
import { failedResult, textResult } from "../../lib/tool-result.ts";
|
|
27
31
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
28
|
-
import { callLine, detailsOrLine } from "../../lib/tui/tool.ts";
|
|
32
|
+
import { callLine, detailsOrLine, isTailResult, TailResult } from "../../lib/tui/tool.ts";
|
|
29
33
|
import { formatRound } from "../../lib/board/round-text.ts";
|
|
34
|
+
import { skippedText } from "../../lib/board/skipped.ts";
|
|
30
35
|
import {
|
|
31
36
|
archiveNote,
|
|
37
|
+
CLAIM_NEXT,
|
|
32
38
|
formatArchive,
|
|
33
39
|
formatArchived,
|
|
34
40
|
formatBoard,
|
|
35
41
|
formatCard,
|
|
36
42
|
formatDropped,
|
|
37
|
-
formatSkipped,
|
|
38
43
|
} from "./format.ts";
|
|
39
44
|
import { createTeamJoin, type TeamJoin } from "../../lib/session-tool.ts";
|
|
40
|
-
import {
|
|
45
|
+
import { collapsedLine, resultInput } from "./result.ts";
|
|
41
46
|
import { createInput } from "./input.ts";
|
|
42
47
|
import type { BoardSession } from "../../lib/board/session.ts";
|
|
43
48
|
import { BoardParams } from "./schema.ts";
|
|
@@ -45,7 +50,6 @@ import {
|
|
|
45
50
|
ACTION_FIELDS,
|
|
46
51
|
BOARD_ACTION_DOCS,
|
|
47
52
|
BOARD_ACTIONS,
|
|
48
|
-
BOARD_MUTATING_ACTIONS,
|
|
49
53
|
BOARD_TOOL,
|
|
50
54
|
type BoardAction,
|
|
51
55
|
type BoardDetails,
|
|
@@ -76,14 +80,14 @@ function callSubject(action: BoardAction, args: Record<string, unknown>): string
|
|
|
76
80
|
if (action === "list") {
|
|
77
81
|
const parts = ["list"];
|
|
78
82
|
if (args.round === true) parts.push("round");
|
|
79
|
-
if (typeof args.
|
|
83
|
+
if (typeof args.lane === "string") parts.push(args.lane);
|
|
80
84
|
if (typeof args.query === "string" && args.query.trim() !== "") parts.push(`"${args.query.trim()}"`);
|
|
81
85
|
if (card !== "") parts.push(card);
|
|
82
86
|
return parts.join(" ");
|
|
83
87
|
}
|
|
84
88
|
if (card === "") return action;
|
|
85
|
-
const
|
|
86
|
-
return `${action} ${card}${
|
|
89
|
+
const lane = typeof args.lane === "string" ? ` ${args.lane}` : "";
|
|
90
|
+
return `${action} ${card}${lane}`;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
/** One refusal: the reason for the model, the state for the renderer. */
|
|
@@ -96,6 +100,17 @@ function refusal(action: BoardAction, me: BoardDetails["me"], message: string, c
|
|
|
96
100
|
});
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Whether a result this tool built is a refusal.
|
|
105
|
+
*
|
|
106
|
+
* `refusal` marks one with `isError`, and the success shape (`ToolResult`) has no such
|
|
107
|
+
* field, so this is the one probe that tells the two apart — read here rather than
|
|
108
|
+
* widening the shared envelope with a flag that would then be optional everywhere.
|
|
109
|
+
*/
|
|
110
|
+
function refused(result: unknown): boolean {
|
|
111
|
+
return typeof result === "object" && result !== null && (result as { isError?: unknown }).isError === true;
|
|
112
|
+
}
|
|
113
|
+
|
|
99
114
|
/**
|
|
100
115
|
* The commit at `cwd`'s `HEAD`, shortened, or nothing when there is none.
|
|
101
116
|
*
|
|
@@ -116,14 +131,16 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
|
|
|
116
131
|
label: "Board",
|
|
117
132
|
description:
|
|
118
133
|
"The shared feature list for this repository: one card per feature, which any session reads and writes. " +
|
|
119
|
-
"A card
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
"A card moves through five lanes - backlog, plan, execute, blocked, done - and carries the feature's " +
|
|
135
|
+
"outcome, the session that holds it, the contract it promises (goal, acceptance, scopeOut), the plan it " +
|
|
136
|
+
`is executed against, its priority, what it waits for, and its notes. ${verbHelp()} A card's steps do not ` +
|
|
137
|
+
"belong on the board - they belong in the plan and in `todo`.",
|
|
122
138
|
promptSnippet: `The shared feature list for this repository: ${BOARD_ACTIONS.join(", ")}.`,
|
|
123
139
|
promptGuidelines: [
|
|
124
|
-
"Claim a card
|
|
125
|
-
"
|
|
126
|
-
"
|
|
140
|
+
"Claim a card to pull it from backlog into plan, design it in plan mode, then record the contract with `contract`: goal, acceptance, scopeOut, and the plan file `plan` action `write` returned — a path outside this repository's plan store is refused. Move it to execute only once the lead (`board.lead` in `bytefly.json`, `lead` by default, the name the page serves as) has approved that contract, or the person has signed with `/board approve` — a lead releases a card before signing it.",
|
|
141
|
+
"Before a card is done, the lead records a spot-check with `review`, or the person signs with `/board review`; the plan it recorded must still be in the store, and `contract` is how it is re-recorded.",
|
|
142
|
+
"Move a card to `blocked` with a `blocker` kind and one line about what is waited on: it resumes in `plan`, or in `execute` once its contract is approved.",
|
|
143
|
+
"Note each phase of the work, and release or reclaim a card you stop holding, so the lane count means what it says.",
|
|
127
144
|
"A card that waits on another is linked with `edit` and `dependsOn`, so the order is on the board instead of in a refusal.",
|
|
128
145
|
"A feature's steps belong in `todo`, not on the board: one card is one feature, whatever it takes.",
|
|
129
146
|
"A `handoff` in your inbox is work you now own: read it with `session inbox`, record the plan, and start it.",
|
|
@@ -143,57 +160,84 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
|
|
|
143
160
|
`unknown action "${String(args.action)}"; use ${BOARD_ACTIONS.join(", ")}.`,
|
|
144
161
|
);
|
|
145
162
|
}
|
|
146
|
-
const me = runtime.who(ctx, ctx.cwd);
|
|
163
|
+
const me = await runtime.who(ctx, ctx.cwd);
|
|
147
164
|
try {
|
|
148
|
-
// Before the join and
|
|
149
|
-
// have enlisted this session as a teammate on its way to the refusal.
|
|
165
|
+
// Before the join and before a card is read: a refused call has written nothing, so it
|
|
166
|
+
// cannot have enlisted this session as a teammate on its way to the refusal.
|
|
150
167
|
const stray = strayField(action, args, ACTION_FIELDS);
|
|
151
168
|
if (stray !== undefined) return refusal(action, me, stray);
|
|
152
|
-
// Working a card makes this session a teammate, once: the board is the user's
|
|
153
|
-
// surface for the team, so nobody has to know the bus exists. A failed join is
|
|
154
|
-
// not a failed card write — the board works with no bus at all — and it is not
|
|
155
|
-
// marked, so the next write asks again.
|
|
156
|
-
if (BOARD_MUTATING_ACTIONS.has(action) && !runtime.joined()) {
|
|
157
|
-
if ((await team.join(ctx)).ok) runtime.markJoined();
|
|
158
|
-
}
|
|
159
169
|
const store = await runtime.store(ctx.cwd);
|
|
160
170
|
const now = runtime.now();
|
|
161
171
|
switch (action) {
|
|
162
172
|
case "list":
|
|
163
|
-
|
|
173
|
+
// The round folds the owners a bus has no record of, and the rollup is the only read
|
|
174
|
+
// that needs the bus for its own sake: a plain `list` reads no bus file beyond the
|
|
175
|
+
// record that names whoever is calling.
|
|
176
|
+
return await read(store, action, me, args, now, async () => readRoster(await runtime.busDir(ctx.cwd)));
|
|
164
177
|
case "add":
|
|
165
178
|
return await add(store, me, args, now);
|
|
166
|
-
case "claim":
|
|
167
|
-
|
|
179
|
+
case "claim": {
|
|
180
|
+
const claimed = await mutate(store, action, me, now, args, { kind: "claim" }, CLAIM_NEXT);
|
|
181
|
+
// Taking a card is what makes this session a teammate. It is the one action here
|
|
182
|
+
// worth being addressed about, and the board is the user's surface for the team, so
|
|
183
|
+
// nobody has to know the bus exists. A refused claim wrote nothing and enlists
|
|
184
|
+
// nobody; a refused join is not a failed claim, so the card stands and the next
|
|
185
|
+
// claim asks again.
|
|
186
|
+
if (!refused(claimed)) {
|
|
187
|
+
// Loaded here, not at registration: the join is the claim's side effect, so a
|
|
188
|
+
// session that never claims pays nothing for it.
|
|
189
|
+
const { joinOnce } = await import("../../lib/board/join.ts");
|
|
190
|
+
await joinOnce(runtime.team, team, ctx, me.label);
|
|
191
|
+
}
|
|
192
|
+
return claimed;
|
|
193
|
+
}
|
|
168
194
|
case "release":
|
|
169
195
|
return await mutate(store, action, me, now, args, { kind: "release" });
|
|
170
196
|
case "reclaim":
|
|
171
197
|
return await mutate(store, action, me, now, args, { kind: "reclaim" });
|
|
172
198
|
case "move": {
|
|
173
199
|
if (args.card === undefined) return refusal(action, me, 'card is required: pass the id, e.g. "4".');
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
return refusal(
|
|
177
|
-
action,
|
|
178
|
-
me,
|
|
179
|
-
'move needs a target lane: "doing", "blocked", or "done". Use release to return a card to todo.',
|
|
180
|
-
);
|
|
200
|
+
const lane = args.lane;
|
|
201
|
+
if (lane === undefined) {
|
|
202
|
+
return refusal(action, me, `move needs a target lane: ${listOf(movableLanes())}.`);
|
|
181
203
|
}
|
|
182
|
-
// Only the
|
|
183
|
-
// records the commit that closed the card, and
|
|
184
|
-
//
|
|
185
|
-
|
|
204
|
+
// Only the lanes that have something to say read for it: a move to `done`
|
|
205
|
+
// records the commit that closed the card, and both `execute` and `done` carry
|
|
206
|
+
// the proof that the plan the card recorded is still there. A board outside a
|
|
207
|
+
// repository simply has no commit to record.
|
|
208
|
+
const landed = lane === "done" ? await headCommit(pi, ctx.cwd) : undefined;
|
|
209
|
+
const proved =
|
|
210
|
+
lane === "execute" || lane === "done"
|
|
211
|
+
? await recordedPlan(runtime, ctx.cwd, store, args.card)
|
|
212
|
+
: undefined;
|
|
186
213
|
return await mutate(store, action, me, now, args, {
|
|
187
214
|
kind: "move",
|
|
188
|
-
|
|
215
|
+
lane,
|
|
189
216
|
...(args.blockedBy === undefined ? {} : { blockedBy: args.blockedBy }),
|
|
217
|
+
...(args.blocker === undefined ? {} : { blocker: args.blocker }),
|
|
190
218
|
...(landed === undefined ? {} : { landed }),
|
|
219
|
+
...(proved === undefined ? {} : { artifact: proved }),
|
|
191
220
|
});
|
|
192
221
|
}
|
|
193
222
|
case "note":
|
|
194
223
|
return await mutate(store, action, me, now, args, { kind: "note", text: args.text });
|
|
195
|
-
case "
|
|
196
|
-
|
|
224
|
+
case "contract": {
|
|
225
|
+
// The plan is proved here, at the boundary: the state machine decides, but only
|
|
226
|
+
// a caller holding the working directory can answer whether the file is there.
|
|
227
|
+
const plan =
|
|
228
|
+
args.path === undefined ? undefined : planForContract(await runtime.planDir(ctx.cwd), args.path);
|
|
229
|
+
return await mutate(store, action, me, now, args, {
|
|
230
|
+
kind: "contract",
|
|
231
|
+
...(plan === undefined ? {} : { artifact: plan }),
|
|
232
|
+
goal: args.goal,
|
|
233
|
+
acceptance: args.acceptance,
|
|
234
|
+
scopeOut: args.scopeOut,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
case "approve":
|
|
238
|
+
return await mutate(store, action, me, now, args, { kind: "approve", text: args.text });
|
|
239
|
+
case "review":
|
|
240
|
+
return await mutate(store, action, me, now, args, { kind: "review", text: args.text });
|
|
197
241
|
case "edit": {
|
|
198
242
|
if (
|
|
199
243
|
args.title === undefined &&
|
|
@@ -239,14 +283,50 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
|
|
|
239
283
|
text.setText(collapsedLine(theme, outcome.details));
|
|
240
284
|
return text;
|
|
241
285
|
}
|
|
242
|
-
const
|
|
243
|
-
|
|
286
|
+
const previous = context?.lastComponent;
|
|
287
|
+
const view = isTailResult<Card>(previous) ? previous : new TailResult<Card>(input, theme);
|
|
244
288
|
view.setInput(input, theme);
|
|
245
289
|
return view;
|
|
246
290
|
},
|
|
247
291
|
});
|
|
248
292
|
}
|
|
249
293
|
|
|
294
|
+
/**
|
|
295
|
+
* The plan file a card entering `execute` recorded, proved to be there.
|
|
296
|
+
*
|
|
297
|
+
* A `move` names the card, not its plan, so the tool reads the card — one read, on the one
|
|
298
|
+
* lane whose gate needs it — and hands `steps.ts` the fact. A card with no plan recorded
|
|
299
|
+
* gets no fact and the state machine refuses it; a recorded plan that is gone, or outside
|
|
300
|
+
* this repository's store, is refused here with the sentence that names the card and the
|
|
301
|
+
* way out.
|
|
302
|
+
*/
|
|
303
|
+
async function recordedPlan(
|
|
304
|
+
runtime: BoardSession,
|
|
305
|
+
cwd: string,
|
|
306
|
+
store: BoardStore,
|
|
307
|
+
rawCard: string | undefined,
|
|
308
|
+
): Promise<ArtifactCheck | undefined> {
|
|
309
|
+
const id = rawCard === undefined ? undefined : normalizeCardId(rawCard);
|
|
310
|
+
const card = id === undefined ? undefined : await store.read(id);
|
|
311
|
+
if (card?.planPath === undefined) return undefined;
|
|
312
|
+
return planForExecute(await runtime.planDir(cwd), card);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The `list` fields that narrow a listing, in the order a refusal names them. */
|
|
316
|
+
const CARD_FILTERS = ["lane", "query", "mine", "round"] as const;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The refusal for the filters a `list` that names a card would otherwise drop.
|
|
320
|
+
*
|
|
321
|
+
* `card` answers for that one card, so a filter beside it changes nothing — and a call that
|
|
322
|
+
* sent both meant one of them. A dropped field is what `strayField` exists to prevent one
|
|
323
|
+
* level up, so it is refused here in the same voice the archive's own reader uses.
|
|
324
|
+
*/
|
|
325
|
+
function cardFiltersRefusal(args: BoardArgs, fields: readonly (keyof BoardArgs)[] = CARD_FILTERS): string | undefined {
|
|
326
|
+
const given = fields.filter((field) => args[field] !== undefined);
|
|
327
|
+
return given.length === 0 ? undefined : `card does not read ${given.join(", ")}: it answers for that one card.`;
|
|
328
|
+
}
|
|
329
|
+
|
|
250
330
|
/** `list`: the board, one card, the lead's rollup, or the archive. */
|
|
251
331
|
async function read(
|
|
252
332
|
store: BoardStore,
|
|
@@ -254,6 +334,7 @@ async function read(
|
|
|
254
334
|
me: BoardDetails["me"],
|
|
255
335
|
args: BoardArgs,
|
|
256
336
|
now: number,
|
|
337
|
+
roster: () => Promise<readonly RosterSession[] | undefined>,
|
|
257
338
|
): Promise<ReturnType<typeof textResult<BoardDetails>>> {
|
|
258
339
|
if (args.archived === true) return readArchive(store, action, me, args, now);
|
|
259
340
|
const cards = await store.list();
|
|
@@ -263,6 +344,8 @@ async function read(
|
|
|
263
344
|
// A directory listing, not a read: a listing says what it left aside without opening one.
|
|
264
345
|
const archived = await store.archivedCount();
|
|
265
346
|
if (args.card !== undefined) {
|
|
347
|
+
const ignored = cardFiltersRefusal(args);
|
|
348
|
+
if (ignored !== undefined) return refusal(action, me, ignored);
|
|
266
349
|
const id = normalizeCardId(args.card);
|
|
267
350
|
if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
|
|
268
351
|
const card = cards.find((entry) => entry.id === id);
|
|
@@ -284,7 +367,7 @@ async function read(
|
|
|
284
367
|
}
|
|
285
368
|
const listed = cards.filter(
|
|
286
369
|
(card) =>
|
|
287
|
-
(args.
|
|
370
|
+
(args.lane === undefined || card.lane === args.lane) &&
|
|
288
371
|
(args.query === undefined || matchesQuery(card, args.query)) &&
|
|
289
372
|
// The same rule a claim is checked against, so "mine" cannot list a card this
|
|
290
373
|
// session could not release.
|
|
@@ -294,18 +377,23 @@ async function read(
|
|
|
294
377
|
// repository's own board is mostly finished cards — so a listing that names no lane
|
|
295
378
|
// leaves them out and says how many. A named lane, `card`, and `round` are unchanged;
|
|
296
379
|
// the rollup counts the whole board, which is what makes it a rollup.
|
|
297
|
-
const hiddenDone = args.
|
|
298
|
-
const shown = hiddenDone === 0 ? listed : listed.filter((card) => card.
|
|
380
|
+
const hiddenDone = args.lane === undefined ? listed.filter((card) => card.lane === "done").length : 0;
|
|
381
|
+
const shown = hiddenDone === 0 ? listed : listed.filter((card) => card.lane !== "done");
|
|
299
382
|
if (args.round !== true) {
|
|
300
|
-
return textResult(formatBoard(shown, { hiddenDone, archived, skipped }), {
|
|
383
|
+
return textResult(formatBoard(shown, { hiddenDone, archived, skipped }), {
|
|
384
|
+
action,
|
|
385
|
+
me,
|
|
386
|
+
cards: shown,
|
|
387
|
+
...(args.mine === true ? { mine: true } : {}),
|
|
388
|
+
});
|
|
301
389
|
}
|
|
302
390
|
const round = buildRound(cards, now);
|
|
303
391
|
// One line under the rollup, because a reader who cannot see a card file should know why,
|
|
304
392
|
// and one for the archive, because a rollup that silently left it out would be a board
|
|
305
393
|
// smaller than the one on disk.
|
|
306
|
-
const notes = [
|
|
307
|
-
const text = [formatRound(cards, round), ...notes].join("\n");
|
|
308
|
-
return textResult(text, { action, me, cards: listed, round });
|
|
394
|
+
const notes = [skippedText(skipped), archiveNote(archived)].filter((line) => line !== "");
|
|
395
|
+
const text = [formatRound(cards, round, await roster()), ...notes].join("\n");
|
|
396
|
+
return textResult(text, { action, me, cards: listed, round, ...(args.mine === true ? { mine: true } : {}) });
|
|
309
397
|
}
|
|
310
398
|
|
|
311
399
|
/**
|
|
@@ -322,7 +410,7 @@ async function readArchive(
|
|
|
322
410
|
args: BoardArgs,
|
|
323
411
|
now: number,
|
|
324
412
|
): Promise<ReturnType<typeof textResult<BoardDetails>>> {
|
|
325
|
-
const wrong = (["
|
|
413
|
+
const wrong = (["lane", "round"] as const).filter((field) => args[field] !== undefined);
|
|
326
414
|
if (wrong.length > 0) {
|
|
327
415
|
return refusal(
|
|
328
416
|
action,
|
|
@@ -332,6 +420,9 @@ async function readArchive(
|
|
|
332
420
|
}
|
|
333
421
|
const pile = await store.archived();
|
|
334
422
|
if (args.card !== undefined) {
|
|
423
|
+
// `query` and `mine` narrow the pile, not the one card it would answer for.
|
|
424
|
+
const ignored = cardFiltersRefusal(args, ["query", "mine"]);
|
|
425
|
+
if (ignored !== undefined) return refusal(action, me, ignored);
|
|
335
426
|
const id = normalizeCardId(args.card);
|
|
336
427
|
if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
|
|
337
428
|
const card = pile.cards.find((entry) => entry.id === id);
|
|
@@ -348,7 +439,12 @@ async function readArchive(
|
|
|
348
439
|
const shown = pile.cards.filter(
|
|
349
440
|
(card) => (args.query === undefined || matchesQuery(card, args.query)) && (args.mine !== true || holds(card, me)),
|
|
350
441
|
);
|
|
351
|
-
return textResult(formatArchive(shown, pile.skipped), {
|
|
442
|
+
return textResult(formatArchive(shown, pile.skipped), {
|
|
443
|
+
action,
|
|
444
|
+
me,
|
|
445
|
+
cards: shown,
|
|
446
|
+
...(args.mine === true ? { mine: true } : {}),
|
|
447
|
+
});
|
|
352
448
|
}
|
|
353
449
|
|
|
354
450
|
/** `add`: the only action that creates a card. */
|
|
@@ -372,13 +468,16 @@ async function mutate(
|
|
|
372
468
|
now: number,
|
|
373
469
|
args: BoardArgs,
|
|
374
470
|
op: CardOp,
|
|
471
|
+
/** One line a card's own text does not carry, for the action that has a next step. */
|
|
472
|
+
next?: string,
|
|
375
473
|
): Promise<ReturnType<typeof textResult<BoardDetails>>> {
|
|
376
474
|
if (args.card === undefined) return refusal(action, me, 'card is required: pass the id, e.g. "4".');
|
|
377
475
|
const id = normalizeCardId(args.card);
|
|
378
476
|
if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
|
|
379
477
|
const result = await store.mutate(id, op, me, now);
|
|
380
478
|
if (!result.ok) return refusal(action, me, result.error, result.card);
|
|
381
|
-
|
|
479
|
+
const lines = [formatCard(result.card), ...(next === undefined ? [] : [next])];
|
|
480
|
+
return textResult(lines.join("\n"), { action, me, card: result.card });
|
|
382
481
|
}
|
|
383
482
|
|
|
384
483
|
/**
|
|
@@ -403,7 +502,7 @@ async function archiveCards(
|
|
|
403
502
|
if (!result.changed) return textResult(`${cardRef(id)} is already archived.`, { action: "archive", me });
|
|
404
503
|
return textResult(formatArchived([id]), { action: "archive", me, card: result.card, archived: [id] });
|
|
405
504
|
}
|
|
406
|
-
const finished = (await store.list()).filter((card) => card.
|
|
505
|
+
const finished = (await store.list()).filter((card) => card.lane === "done");
|
|
407
506
|
const moved: string[] = [];
|
|
408
507
|
const failed: Array<{ card: string; error: string }> = [];
|
|
409
508
|
for (const card of finished) {
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type ServeResponse,
|
|
24
24
|
} from "../../../lib/http/index.ts";
|
|
25
25
|
import type { BoardView } from "../../../lib/board/view.ts";
|
|
26
|
+
import { PING_PATH, SNAPSHOT_PATH } from "../../../lib/board/page-pointer.ts";
|
|
26
27
|
import { THEME_JS, THEME_JS_PATH } from "../../../lib/web/index.ts";
|
|
27
28
|
import type { PageView } from "../view/page.ts";
|
|
28
29
|
|
|
@@ -45,13 +46,10 @@ export interface BoardRouterContext {
|
|
|
45
46
|
page: (view: PageView) => Promise<string>;
|
|
46
47
|
/** The client script. */
|
|
47
48
|
script: string;
|
|
48
|
-
/** The snapshot the page renders, and the body of
|
|
49
|
+
/** The snapshot the page renders, and the body of the snapshot route. */
|
|
49
50
|
view: () => Promise<BoardView>;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
/** The cheap liveness route: a probe reads this, and never the snapshot. */
|
|
53
|
-
export const PING_PATH = "/api/ping";
|
|
54
|
-
|
|
55
53
|
/**
|
|
56
54
|
* Handle one request. `method`, `rawUrl`, and `headers` are the raw HTTP values.
|
|
57
55
|
*/
|
|
@@ -77,7 +75,7 @@ export async function handleBoardRequest(
|
|
|
77
75
|
// must not pay for the snapshot, which is a store read and a roster read on the same
|
|
78
76
|
// event loop the probe is racing.
|
|
79
77
|
if (pathname === PING_PATH) return jsonResponse(200, { ok: true });
|
|
80
|
-
if (pathname ===
|
|
78
|
+
if (pathname === SNAPSHOT_PATH) return jsonResponse(200, await ctx.view());
|
|
81
79
|
if (pathname === "/" || pathname === "/index.html") {
|
|
82
80
|
return { status: 200, headers: pageHeaders(), body: await ctx.page(viewOf(search.get("view"))) };
|
|
83
81
|
}
|