@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
package/lib/board/links.ts
CHANGED
|
@@ -36,20 +36,28 @@ export function indexOf(cards: readonly Card[]): BoardIndex {
|
|
|
36
36
|
export function blockersOf(card: Card, board: BoardIndex): Card[] {
|
|
37
37
|
return dependsOf(card)
|
|
38
38
|
.map((id) => board.get(id))
|
|
39
|
-
.filter((other): other is Card => other !== undefined && other.
|
|
39
|
+
.filter((other): other is Card => other !== undefined && other.lane !== "done");
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* A list of cards as a reader meets it: `#2 (
|
|
43
|
+
* A list of cards as a reader meets it: `#2 (execute), #3 (blocked)`.
|
|
44
44
|
*
|
|
45
45
|
* The lane is what turns ids into a decision — a finished card is about to start, a
|
|
46
|
-
* `
|
|
46
|
+
* `execute` one is to leave alone — so it is never left out. One formatter serves the
|
|
47
47
|
* refusal sentence, the page's reason line, and the mirror question of what a card is
|
|
48
48
|
* holding up: every one of them is a list of cards read to decide something, so a second
|
|
49
49
|
* formatter would be a second way for the same list to read.
|
|
50
|
+
*
|
|
51
|
+
* `titles` adds the card's own title after its lane, for the one audience with the room
|
|
52
|
+
* and the reason for it: a card's face on the page is where a reader decides what to do
|
|
53
|
+
* next, and an id alone would send them into the dialog to find out which card it is. A
|
|
54
|
+
* refusal stays ids and lanes — it is read in a transcript row, and a title there would
|
|
55
|
+
* push the sentence it belongs to off the line.
|
|
50
56
|
*/
|
|
51
|
-
export function lanesText(cards: readonly Card[]): string {
|
|
52
|
-
return cards
|
|
57
|
+
export function lanesText(cards: readonly Card[], options: { titles?: boolean } = {}): string {
|
|
58
|
+
return cards
|
|
59
|
+
.map((card) => `${cardRef(card.id)} (${card.lane})${options.titles === true ? ` ${card.title}` : ""}`)
|
|
60
|
+
.join(", ");
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
/** The cards that wait on `id`, oldest id first, so a report reads in board order. */
|
|
@@ -66,7 +74,7 @@ export function dependentsOf(id: string, board: BoardIndex): Card[] {
|
|
|
66
74
|
* refusal is about a link that would be stranded rather than about work that is held up.
|
|
67
75
|
*/
|
|
68
76
|
export function openDependentsOf(id: string, board: BoardIndex): Card[] {
|
|
69
|
-
return dependentsOf(id, board).filter((card) => card.
|
|
77
|
+
return dependentsOf(id, board).filter((card) => card.lane !== "done");
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
/** The sentence a claim, a move, or an assignment is refused with while `card` waits, or undefined. */
|
|
@@ -106,21 +114,34 @@ export function wouldCycle(id: string, dependsOn: readonly string[], board: Boar
|
|
|
106
114
|
*
|
|
107
115
|
* `[]` is a legal answer and means "waits on nothing", so an edit can remove a link
|
|
108
116
|
* — the caller decides whether an absent list means "leave it alone" or "none".
|
|
117
|
+
*
|
|
118
|
+
* A link the card already carries is kept even when the card it names has left the board:
|
|
119
|
+
* `archive` moves a finished card's file rather than deleting its record, the link is inert
|
|
120
|
+
* (a finished dependency stops blocking), and refusing it would make `edit` unable to
|
|
121
|
+
* reproduce what the card says. A link the card does not already carry still has to name a
|
|
122
|
+
* card on the board, so an id nobody can see cannot be added.
|
|
109
123
|
*/
|
|
110
|
-
export function normalizeDepends(
|
|
124
|
+
export function normalizeDepends(
|
|
125
|
+
raw: unknown,
|
|
126
|
+
card: Pick<Card, "id" | "dependsOn">,
|
|
127
|
+
board: BoardIndex,
|
|
128
|
+
): { dependsOn: string[] } | FieldError {
|
|
111
129
|
if (!Array.isArray(raw)) return { error: "dependsOn must be a list of card ids." };
|
|
130
|
+
const held = dependsOf(card);
|
|
112
131
|
const dependsOn: string[] = [];
|
|
113
132
|
for (const entry of raw) {
|
|
114
133
|
const parsed = normalizeCardId(entry);
|
|
115
134
|
// The file name is the canonical spelling of an id, so `#04` and `4` are one card.
|
|
116
135
|
const dependency = parsed === undefined ? undefined : String(Number(parsed));
|
|
117
136
|
if (dependency === undefined) return { error: `${JSON.stringify(entry)} is not a card id.` };
|
|
118
|
-
if (dependency === id) return { error: `${cardRef(id)} cannot wait on itself.` };
|
|
119
|
-
if (!board.has(dependency)
|
|
137
|
+
if (dependency === card.id) return { error: `${cardRef(card.id)} cannot wait on itself.` };
|
|
138
|
+
if (!board.has(dependency) && !held.includes(dependency)) {
|
|
139
|
+
return { error: `${cardRef(dependency)} does not exist.` };
|
|
140
|
+
}
|
|
120
141
|
if (!dependsOn.includes(dependency)) dependsOn.push(dependency);
|
|
121
142
|
}
|
|
122
143
|
if (dependsOn.length > MAX_DEPENDS) return { error: `at most ${MAX_DEPENDS} dependencies are allowed.` };
|
|
123
|
-
const loop = wouldCycle(id, dependsOn, board);
|
|
144
|
+
const loop = wouldCycle(card.id, dependsOn, board);
|
|
124
145
|
if (loop !== undefined) return { error: loop };
|
|
125
146
|
return { dependsOn };
|
|
126
147
|
}
|
|
@@ -27,6 +27,31 @@ import { abortableSleep } from "../timer.ts";
|
|
|
27
27
|
/** The file, beside the board's `cards/` directory. */
|
|
28
28
|
export const PAGE_FILE = "page.json";
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The prefix the page answers under on the session's one server.
|
|
32
|
+
*
|
|
33
|
+
* Named here, beside the routes below, because three callers have to agree on it: the
|
|
34
|
+
* page's router (which matches the stripped path), the note a second session reads, and
|
|
35
|
+
* the probe that asks whether the page is alive. A session serves several pages on one
|
|
36
|
+
* origin, so the board is never at the root.
|
|
37
|
+
*/
|
|
38
|
+
export const BOARD_BASE = "/board";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The two routes a probe asks for, named here because the probe and the page's router have
|
|
42
|
+
* to agree on them.
|
|
43
|
+
*
|
|
44
|
+
* `PING_PATH` is the cheap liveness answer this build serves; `SNAPSHOT_PATH` is what a
|
|
45
|
+
* page from an older build answers instead, which is why the probe falls back to it. A
|
|
46
|
+
* literal repeated in the router is a path that can move out from under the probe with
|
|
47
|
+
* every test still green — the probe would simply stop finding a live page.
|
|
48
|
+
*
|
|
49
|
+
* Both are the paths *inside* the page, which is what its router sees: the server strips
|
|
50
|
+
* `BOARD_BASE` before the route runs, and the probe adds it back.
|
|
51
|
+
*/
|
|
52
|
+
export const PING_PATH = "/api/ping";
|
|
53
|
+
export const SNAPSHOT_PATH = "/api/board";
|
|
54
|
+
|
|
30
55
|
/**
|
|
31
56
|
* How long a probe waits before it calls the page gone.
|
|
32
57
|
*
|
|
@@ -41,7 +66,7 @@ export interface PagePointer {
|
|
|
41
66
|
port: number;
|
|
42
67
|
/** The page's one token: every read, and nothing else to authorise. */
|
|
43
68
|
token: string;
|
|
44
|
-
/** The seat the page
|
|
69
|
+
/** The seat the page is read as. */
|
|
45
70
|
seat: { id?: string; label: string };
|
|
46
71
|
/** When the page started, so a reader can tell a fresh note from a stale one. */
|
|
47
72
|
startedAt: number;
|
|
@@ -52,7 +77,7 @@ export type ProbeFetch = (url: string, init: { signal: AbortSignal }) => Promise
|
|
|
52
77
|
|
|
53
78
|
/** The URL for this page, wherever it is opened from. */
|
|
54
79
|
export function urlOf(pointer: PagePointer): string {
|
|
55
|
-
return `http://127.0.0.1:${pointer.port}/?token=${pointer.token}`;
|
|
80
|
+
return `http://127.0.0.1:${pointer.port}${BOARD_BASE}/?token=${pointer.token}`;
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
/**
|
|
@@ -272,16 +297,16 @@ export async function probePage(
|
|
|
272
297
|
timeoutMs: number = PROBE_MS,
|
|
273
298
|
): Promise<boolean> {
|
|
274
299
|
const ask = (path: string): Promise<{ ok: boolean; status?: number }> =>
|
|
275
|
-
fetchLike(`http://127.0.0.1:${pointer.port}${path}?token=${pointer.token}`, {
|
|
300
|
+
fetchLike(`http://127.0.0.1:${pointer.port}${BOARD_BASE}${path}?token=${pointer.token}`, {
|
|
276
301
|
signal: AbortSignal.timeout(timeoutMs),
|
|
277
302
|
});
|
|
278
303
|
try {
|
|
279
|
-
const ping = await ask(
|
|
304
|
+
const ping = await ask(PING_PATH);
|
|
280
305
|
if (ping.ok) return true;
|
|
281
306
|
// A page this build started always answers the ping; a 404 is one started by an
|
|
282
307
|
// older build, whose only liveness answer is its snapshot.
|
|
283
308
|
if (ping.status !== 404) return false;
|
|
284
|
-
return (await ask(
|
|
309
|
+
return (await ask(SNAPSHOT_PATH)).ok;
|
|
285
310
|
} catch {
|
|
286
311
|
return false;
|
|
287
312
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a card file: the one gate a read passes, and the map from an older shape.
|
|
3
|
+
*
|
|
4
|
+
* The record itself is `card.ts`; this is what a file on disk becomes. Every version this
|
|
5
|
+
* build can read passes through {@link readCard}, which maps the names a past build wrote
|
|
6
|
+
* and stamps the version this one writes — a v1 file says `status`, `plan`, and a
|
|
7
|
+
* `blockedBy`/`blocker` pair where the record says `lane`, `planPath`, and `blocked`. The
|
|
8
|
+
* map is read-time only: no file is rewritten, so a board full of older cards reads as a
|
|
9
|
+
* board rather than as a pile of refusals, and each card takes the new shape on its next
|
|
10
|
+
* write.
|
|
11
|
+
*
|
|
12
|
+
* Pure: no I/O, no clock, no state.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { BLOCKERS, type BlockerKind, type Card, type CardBlock, CARD_VERSION, isCard } from "./card.ts";
|
|
16
|
+
import type { Lane } from "./lanes.ts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The lane words the four-lane board used, as this build's names.
|
|
20
|
+
*
|
|
21
|
+
* `todo` was the pool nobody held and `doing` was the work in hand; `blocked` and `done`
|
|
22
|
+
* kept their names, so they need no row here. A word with no row is not one this build can
|
|
23
|
+
* place, so the entry is left as it is and {@link isCard} refuses the record — a lane
|
|
24
|
+
* guessed from an unknown word would put a card somewhere its own history disagrees with.
|
|
25
|
+
*/
|
|
26
|
+
const LEGACY_LANES: Readonly<Record<string, Lane>> = { todo: "backlog", doing: "execute" };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The card as a file on disk holds it, or `undefined` when this build cannot trust it.
|
|
30
|
+
*
|
|
31
|
+
* A v1 file is the record before the lane and the block were named: `status` for `lane`,
|
|
32
|
+
* `plan` for `planPath`, a `blockedBy`/`blocker` pair for `blocked`, and — for a file older
|
|
33
|
+
* still — log entries whose lane is spelled `status` too. This maps them on read and stamps
|
|
34
|
+
* `v: 2`; the map is read-time only, so no file is rewritten and a card takes the new shape
|
|
35
|
+
* on its next write. Everything else is {@link isCard}'s decision; a version above this
|
|
36
|
+
* build's is the store's refusal to name, not this function's.
|
|
37
|
+
*/
|
|
38
|
+
export function readCard(value: unknown): Card | undefined {
|
|
39
|
+
const record = migrated(value);
|
|
40
|
+
return isCard(record) ? record : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A v1 record as this build's shape: the renames, and the version stamp. */
|
|
44
|
+
function migrated(value: unknown): unknown {
|
|
45
|
+
if (typeof value !== "object" || value === null) return value;
|
|
46
|
+
const record = value as Record<string, unknown>;
|
|
47
|
+
if (record.v !== 1) return value;
|
|
48
|
+
const { status, plan, blockedBy, blocker, log, ...rest } = record;
|
|
49
|
+
const blocked = blockOf(blockedBy, blocker);
|
|
50
|
+
return {
|
|
51
|
+
...rest,
|
|
52
|
+
v: CARD_VERSION,
|
|
53
|
+
...(status === undefined || record.lane !== undefined ? {} : { lane: status }),
|
|
54
|
+
...(plan === undefined ? {} : { planPath: record.planPath ?? plan }),
|
|
55
|
+
...(blocked === undefined || record.blocked !== undefined ? {} : { blocked }),
|
|
56
|
+
...(log === undefined ? {} : { log: logEntries(log) }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The block a v1 pair spells, keeping whichever half the file actually has. */
|
|
61
|
+
function blockOf(rawNote: unknown, rawKind: unknown): CardBlock | undefined {
|
|
62
|
+
const kind =
|
|
63
|
+
typeof rawKind === "string" && BLOCKERS.includes(rawKind as BlockerKind) ? (rawKind as BlockerKind) : undefined;
|
|
64
|
+
const note = typeof rawNote === "string" && rawNote !== "" ? rawNote : undefined;
|
|
65
|
+
if (kind === undefined && note === undefined) return undefined;
|
|
66
|
+
return { ...(kind === undefined ? {} : { kind }), ...(note === undefined ? {} : { note }) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A v1 log whose entries spelled the lane `status`, read as this build's entries. */
|
|
70
|
+
function logEntries(raw: unknown): unknown {
|
|
71
|
+
if (!Array.isArray(raw)) return raw;
|
|
72
|
+
return raw.map((entry) => {
|
|
73
|
+
if (typeof entry !== "object" || entry === null) return entry;
|
|
74
|
+
const record = entry as Record<string, unknown>;
|
|
75
|
+
if (record.status === undefined || record.lane !== undefined) return entry;
|
|
76
|
+
const { status, ...rest } = record;
|
|
77
|
+
// The four-lane board's `todo` and `doing` are this build's `backlog` and `execute`;
|
|
78
|
+
// a status that is already a lane name passes through, and anything else is left for
|
|
79
|
+
// `isCard` to refuse rather than mapped onto a lane nobody wrote.
|
|
80
|
+
const lane = typeof status === "string" ? (LEGACY_LANES[status] ?? status) : status;
|
|
81
|
+
return { ...rest, lane };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a card sits where it does: one rule, for the card face, the lead's queue, and the
|
|
3
|
+
* transcript.
|
|
4
|
+
*
|
|
5
|
+
* A card's lane says what kind of state it is in; the reason line says the one fact that
|
|
6
|
+
* decides what happens next — an order it waits on, a block somebody recorded, a contract
|
|
7
|
+
* nobody approved yet, a spot-check nobody has done. That sentence used to be written
|
|
8
|
+
* three times (the page's card face, its Team queue, and `board`'s card block), and the
|
|
9
|
+
* flow this board now runs multiplies the states it has to name, so it is one body.
|
|
10
|
+
*
|
|
11
|
+
* It is spliced into the browser bundle as {@link REASON_FOR_JS}, which is why it is
|
|
12
|
+
* self-contained: it reads the card and a formatter for relative times, and nothing else.
|
|
13
|
+
* The two callers differ only in whose clock an age is read against — the page's `ago`
|
|
14
|
+
* against the reader's now, the transcript's against the snapshot's — so the clock is a
|
|
15
|
+
* parameter rather than an import.
|
|
16
|
+
*
|
|
17
|
+
* Pure: no I/O, no clock of its own, no state.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Card } from "./card.ts";
|
|
21
|
+
|
|
22
|
+
/** What a reason is about, which is what a row's edge and a dot are coloured by. */
|
|
23
|
+
export const REASON_KINDS = [
|
|
24
|
+
"waiting",
|
|
25
|
+
"blocked",
|
|
26
|
+
"offered",
|
|
27
|
+
"contract",
|
|
28
|
+
"approval",
|
|
29
|
+
"executing",
|
|
30
|
+
"review",
|
|
31
|
+
"done",
|
|
32
|
+
"note",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
/** One kind of reason. */
|
|
36
|
+
export type ReasonKind = (typeof REASON_KINDS)[number];
|
|
37
|
+
|
|
38
|
+
/** One reason line: what it is about, and the sentence a reader gets. */
|
|
39
|
+
export interface Reason {
|
|
40
|
+
kind: ReasonKind;
|
|
41
|
+
text: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The one line under a card, or `undefined` when the card has nothing to say.
|
|
46
|
+
*
|
|
47
|
+
* The order inside a lane is the weight of the fact: an unfinished dependency outranks
|
|
48
|
+
* everything (a claim would be refused), then the lane's own decision, then the last note.
|
|
49
|
+
*/
|
|
50
|
+
export function reasonFor(card: Card, ago: (at: number) => string): Reason | undefined {
|
|
51
|
+
const last = card.notes.length > 0 ? card.notes[card.notes.length - 1] : undefined;
|
|
52
|
+
if (card.lane === "blocked") {
|
|
53
|
+
// The recorded block is the reason: its sentence if one was written, else the kind,
|
|
54
|
+
// which is what says where the card goes back to. Only a card with no block at all —
|
|
55
|
+
// one hand-edited, or released and parked again before this record existed — falls
|
|
56
|
+
// back to the last note, and then to saying it has nothing.
|
|
57
|
+
if (card.blocked !== undefined) {
|
|
58
|
+
const kind = card.blocked.kind;
|
|
59
|
+
return {
|
|
60
|
+
kind: "blocked",
|
|
61
|
+
text: card.blocked.note ?? "blocked on a " + (kind === undefined ? "decision" : kind),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { kind: "blocked", text: last === undefined ? "blocked, with nothing recorded" : last.text };
|
|
65
|
+
}
|
|
66
|
+
if (card.lane === "backlog") {
|
|
67
|
+
if (card.offeredAt !== undefined) {
|
|
68
|
+
return { kind: "offered", text: "offered " + ago(card.offeredAt) + " ago — nobody took it" };
|
|
69
|
+
}
|
|
70
|
+
return last === undefined ? undefined : { kind: "note", text: last.text };
|
|
71
|
+
}
|
|
72
|
+
if (card.lane === "plan") {
|
|
73
|
+
// The contract's three lines, read here so the page and the transcript name the same
|
|
74
|
+
// missing field: this body is spliced, so it cannot call the module that owns them.
|
|
75
|
+
const missing = [];
|
|
76
|
+
const fields = ["goal", "acceptance", "scopeOut"];
|
|
77
|
+
for (let index = 0; index < fields.length; index++) {
|
|
78
|
+
const value = card.contract === undefined ? undefined : card.contract[fields[index] as "goal"];
|
|
79
|
+
if (typeof value !== "string" || value === "") missing.push(fields[index]);
|
|
80
|
+
}
|
|
81
|
+
if (missing.length > 0) {
|
|
82
|
+
return { kind: "contract", text: "no contract yet — missing " + missing.join(", ") };
|
|
83
|
+
}
|
|
84
|
+
const approved = card.contract === undefined ? undefined : card.contract.approved;
|
|
85
|
+
if (approved === undefined) return { kind: "approval", text: "contract ready — needs approval" };
|
|
86
|
+
// A card planned before the plan was required: its contract names no artifact, which
|
|
87
|
+
// `execute` now refuses, so the line says what is missing rather than who signed.
|
|
88
|
+
if (card.planPath === undefined) return { kind: "contract", text: "contract approved — no plan recorded" };
|
|
89
|
+
return { kind: "approval", text: "contract approved by " + approved.by };
|
|
90
|
+
}
|
|
91
|
+
if (card.lane === "execute") {
|
|
92
|
+
if (card.review !== undefined) return { kind: "review", text: "spot-checked by " + card.review.by };
|
|
93
|
+
if (card.planPath === undefined) return { kind: "contract", text: "no plan recorded" };
|
|
94
|
+
return { kind: "executing", text: "executing — no spot-check yet" };
|
|
95
|
+
}
|
|
96
|
+
const landed = card.landed === undefined ? "" : " · landed " + card.landed.commit;
|
|
97
|
+
const who = card.owner === undefined ? "" : " by " + card.owner.label;
|
|
98
|
+
return { kind: "done", text: "✓ done" + who + landed };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The same body, as the browser receives it: one rule for the page and the transcript. */
|
|
102
|
+
export const REASON_FOR_JS = String(reasonFor);
|
package/lib/board/round-text.ts
CHANGED
|
@@ -6,32 +6,54 @@
|
|
|
6
6
|
* because a lead in a terminal should not have to open a browser to see what needs a
|
|
7
7
|
* decision. One text, so the two cannot describe one board differently.
|
|
8
8
|
*
|
|
9
|
-
* It reads the cards
|
|
10
|
-
*
|
|
9
|
+
* It reads the cards, the round, and — when a caller has one — the roster, and never a clock
|
|
10
|
+
* or a store: the group lines are pure and a caller supplies what it already read. The roster
|
|
11
|
+
* is what lets an owner who is only history be stated once instead of once per label, and it
|
|
12
|
+
* is optional because a board works with `session-bus` unloaded.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { type Card, cardRef } from "./card.ts";
|
|
14
16
|
import type { RoundSummary } from "./round.ts";
|
|
15
17
|
|
|
18
|
+
/** What the fold needs of a roster row: the page's `ViewTeammate` and the bus's record both answer it. */
|
|
19
|
+
export interface RosterLabel {
|
|
20
|
+
label: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
/**
|
|
17
24
|
* The rollup: a headline of what a lead decides on, then one line per group.
|
|
18
25
|
*
|
|
19
26
|
* Groups name their cards by id and state, because the ids are what the next action
|
|
20
27
|
* takes, and an empty group is left out rather than drawn empty.
|
|
28
|
+
*
|
|
29
|
+
* `roster` decides which owners are only history: the ones a bus has no record of and that
|
|
30
|
+
* hold nothing are stated in one line, because a label that ever claimed a card stays an
|
|
31
|
+
* owner for good. Without a roster nothing is called absent, which is the honest answer for
|
|
32
|
+
* a machine where the bus has recorded nobody.
|
|
21
33
|
*/
|
|
22
|
-
export function formatRound(cards: readonly Card[], round: RoundSummary): string {
|
|
34
|
+
export function formatRound(cards: readonly Card[], round: RoundSummary, roster?: readonly RosterLabel[]): string {
|
|
23
35
|
const owners = round.owners.length;
|
|
24
36
|
const headline = [
|
|
25
37
|
plural(owners, "owner"),
|
|
26
38
|
plural(cards.length, "feature"),
|
|
27
39
|
`${round.unassigned.length} unassigned`,
|
|
28
40
|
`${round.unplanned.length} unplanned`,
|
|
41
|
+
`${round.unapproved.length} awaiting approval`,
|
|
29
42
|
`${round.blocked.length} blocked`,
|
|
30
43
|
`${round.idle.length} idle`,
|
|
31
44
|
`${round.done.length} done`,
|
|
32
45
|
].join(" · ");
|
|
33
46
|
const lines = [headline];
|
|
47
|
+
// The history is counted as it goes, so its one line can follow the owner lines and the
|
|
48
|
+
// queue can start where a lead actually decides something.
|
|
49
|
+
let history = 0;
|
|
50
|
+
let finished = 0;
|
|
34
51
|
for (const group of round.owners) {
|
|
52
|
+
if (isHistoryOwner(group.label, group.cards.length, roster)) {
|
|
53
|
+
history += 1;
|
|
54
|
+
finished += group.done.length;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
35
57
|
// An owner who finished everything is still listed, with what it finished: the
|
|
36
58
|
// count says which kind of work is left, and an empty list is a legitimate answer.
|
|
37
59
|
const counts =
|
|
@@ -39,8 +61,10 @@ export function formatRound(cards: readonly Card[], round: RoundSummary): string
|
|
|
39
61
|
const listed = group.cards.map((card) => brief(card)).join(", ");
|
|
40
62
|
lines.push(listed === "" ? `${group.label} (${counts})` : `${group.label} (${counts}): ${listed}`);
|
|
41
63
|
}
|
|
64
|
+
if (history > 0) lines.push(historyLine(history, finished));
|
|
42
65
|
lines.push(...group("unassigned", round.unassigned));
|
|
43
66
|
lines.push(...group("unplanned", round.unplanned));
|
|
67
|
+
lines.push(...group("unapproved", round.unapproved));
|
|
44
68
|
lines.push(...group("blocked", round.blocked, true));
|
|
45
69
|
lines.push(...group("idle", round.idle));
|
|
46
70
|
lines.push(...group("done", round.done));
|
|
@@ -61,10 +85,48 @@ export function plural(count: number, noun: string): string {
|
|
|
61
85
|
/** The same body, as the browser receives it: one wording for `1 card`, not two. */
|
|
62
86
|
export const PLURAL_JS = String(plural);
|
|
63
87
|
|
|
64
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Whether an owner is only history: the bus has no record of it, and it holds nothing.
|
|
90
|
+
*
|
|
91
|
+
* The Team tab and this text answer one question — which owners a board still has to show —
|
|
92
|
+
* so they ask it here. A label that ever claimed a card stays an owner for good, which is why
|
|
93
|
+
* the set only grows and why it is stated once.
|
|
94
|
+
*
|
|
95
|
+
* `roster` absent is "no bus here", not "no record": a machine that has recorded nobody
|
|
96
|
+
* cannot call an owner absent, and the page says `—` for the same reason.
|
|
97
|
+
*/
|
|
98
|
+
export function isHistoryOwner(label: string, held: number, roster: readonly RosterLabel[] | undefined): boolean {
|
|
99
|
+
if (roster === undefined || held > 0) return false;
|
|
100
|
+
const key = label.toLowerCase();
|
|
101
|
+
for (const session of roster) {
|
|
102
|
+
if (session.label.toLowerCase() === key) return false;
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The same body, as the browser receives it. */
|
|
108
|
+
export const HISTORY_OWNER_JS = String(isHistoryOwner);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The one line the fold prints: `18 owners with no record · 71 done`.
|
|
112
|
+
*
|
|
113
|
+
* The page's history row and this line are one sentence, so the count of owners and the work
|
|
114
|
+
* they finished read the same on both surfaces.
|
|
115
|
+
*/
|
|
116
|
+
export function historyLine(owners: number, done: number): string {
|
|
117
|
+
return plural(owners, "owner") + " with no record · " + done + " done";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The same body, as the browser receives it: it calls `plural`, which the fragment holds. */
|
|
121
|
+
export const HISTORY_LINE_JS = String(historyLine);
|
|
122
|
+
|
|
123
|
+
/** `#4 execute planned`, the brief a rollup group needs. */
|
|
65
124
|
function brief(card: Card): string {
|
|
66
|
-
|
|
67
|
-
|
|
125
|
+
// Only the two lanes a card is worked in say whether it was planned first: the pool has
|
|
126
|
+
// no plan yet, and a finished card is history.
|
|
127
|
+
const planned =
|
|
128
|
+
card.lane === "plan" || card.lane === "execute" ? (card.planPath === undefined ? " unplanned" : " planned") : "";
|
|
129
|
+
return `${cardRef(card.id)} ${card.lane}${planned}`;
|
|
68
130
|
}
|
|
69
131
|
|
|
70
132
|
/** One rollup group, or nothing when it is empty. */
|
package/lib/board/round.ts
CHANGED
|
@@ -8,16 +8,17 @@
|
|
|
8
8
|
* know — nobody has touched it in half an hour — not that its owner's process is
|
|
9
9
|
* gone.
|
|
10
10
|
*
|
|
11
|
-
* The groups overlap on purpose. A `
|
|
12
|
-
* an hour is both `unplanned` and `idle
|
|
13
|
-
* half the reason a lead should look at it.
|
|
11
|
+
* The groups overlap on purpose. A `plan` card with no contract and no touch for half
|
|
12
|
+
* an hour is both `unplanned` and, once it is executing, `idle`; hiding it in one of the
|
|
13
|
+
* two would lose half the reason a lead should look at it.
|
|
14
14
|
*
|
|
15
15
|
* Pure: no I/O and no clock of its own.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { Card } from "./card.ts";
|
|
19
|
+
import { contractApproved, missingContractFields } from "./contract.ts";
|
|
19
20
|
|
|
20
|
-
/** How long
|
|
21
|
+
/** How long an `execute` card may go untouched before the rollup calls it idle. */
|
|
21
22
|
export const IDLE_MS = 30 * 60 * 1_000;
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -35,17 +36,22 @@ export interface RoundGroup<T extends Card = Card> {
|
|
|
35
36
|
|
|
36
37
|
/** The lead's rollup of one board. */
|
|
37
38
|
export interface RoundSummary<T extends Card = Card> {
|
|
38
|
-
/** Every owner
|
|
39
|
+
/** Every owner the board names, ordered by label, with its live cards and the ones it finished. */
|
|
39
40
|
owners: RoundGroup<T>[];
|
|
40
|
-
/** `
|
|
41
|
+
/** `backlog` cards: nobody has taken them yet. */
|
|
41
42
|
unassigned: T[];
|
|
42
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* `plan` cards whose contract is still missing a line, or whose plan was never recorded:
|
|
45
|
+
* work started before it was agreed, and either one refuses `execute`.
|
|
46
|
+
*/
|
|
43
47
|
unplanned: T[];
|
|
48
|
+
/** `plan` cards whose contract is complete and nobody has approved: the lead's queue. */
|
|
49
|
+
unapproved: T[];
|
|
44
50
|
/** `blocked` cards, and the note that says why. */
|
|
45
51
|
blocked: T[];
|
|
46
|
-
/** `
|
|
52
|
+
/** `execute` cards untouched for {@link IDLE_MS}: possibly a dead owner. */
|
|
47
53
|
idle: T[];
|
|
48
|
-
/** Finished cards,
|
|
54
|
+
/** Finished cards, in the board's own order — priority, then age — kept as history. */
|
|
49
55
|
done: T[];
|
|
50
56
|
}
|
|
51
57
|
|
|
@@ -64,21 +70,30 @@ export function buildRound<T extends Card = Card>(cards: readonly T[], now: numb
|
|
|
64
70
|
const groups = new Map<string, RoundGroup<T>>();
|
|
65
71
|
const unassigned: T[] = [];
|
|
66
72
|
const unplanned: T[] = [];
|
|
73
|
+
const unapproved: T[] = [];
|
|
67
74
|
const blocked: T[] = [];
|
|
68
75
|
const idle: T[] = [];
|
|
69
76
|
const done: T[] = [];
|
|
70
77
|
|
|
71
78
|
for (const card of cards) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (card.
|
|
76
|
-
if (card.
|
|
79
|
+
// The pool is what nobody holds, and the two lanes a lead watches are the two a card
|
|
80
|
+
// is worked in: a plan with a contract still missing is work started before it was
|
|
81
|
+
// agreed, and an execute nothing has touched is work that may have stopped.
|
|
82
|
+
if (card.lane === "backlog") unassigned.push(card);
|
|
83
|
+
if (card.lane === "done") done.push(card);
|
|
84
|
+
if (card.lane === "blocked") blocked.push(card);
|
|
85
|
+
if (card.lane === "plan") {
|
|
86
|
+
// A complete contract still needs a file to execute against: a card planned before
|
|
87
|
+
// the artifact rule has neither, and it belongs in the queue that leads somewhere.
|
|
88
|
+
if (missingContractFields(card.contract).length > 0 || card.planPath === undefined) unplanned.push(card);
|
|
89
|
+
else if (!contractApproved(card.contract)) unapproved.push(card);
|
|
90
|
+
}
|
|
91
|
+
if (card.lane === "execute" && now - card.updatedAt >= IDLE_MS) idle.push(card);
|
|
77
92
|
|
|
78
93
|
if (card.owner === undefined) continue;
|
|
79
94
|
const key = card.owner.label.toLowerCase();
|
|
80
95
|
const group = groups.get(key) ?? { label: card.owner.label, cards: [], done: [] };
|
|
81
|
-
if (card.
|
|
96
|
+
if (card.lane === "done") group.done.push(card);
|
|
82
97
|
else group.cards.push(card);
|
|
83
98
|
groups.set(key, group);
|
|
84
99
|
}
|
|
@@ -87,6 +102,7 @@ export function buildRound<T extends Card = Card>(cards: readonly T[], now: numb
|
|
|
87
102
|
owners: [...groups.values()].sort((a, b) => a.label.localeCompare(b.label)),
|
|
88
103
|
unassigned,
|
|
89
104
|
unplanned,
|
|
105
|
+
unapproved,
|
|
90
106
|
blocked,
|
|
91
107
|
idle,
|
|
92
108
|
done,
|