@gavin-hu/my-pi-agent 0.21.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 -31
- package/README.zh-CN.md +40 -26
- 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 -24
- package/docs/extensions/board.md +361 -147
- package/docs/extensions/bytefly.md +152 -111
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +30 -31
- 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 +166 -126
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +34 -18
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +72 -33
- 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 +56 -15
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +5 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +4 -40
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +13 -11
- package/extensions/board/result.ts +30 -16
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +42 -10
- package/extensions/board/snapshot.ts +2 -1
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +131 -44
- package/extensions/board/types.ts +36 -32
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +33 -15
- 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 +89 -21
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +23 -9
- package/extensions/board/view/render.ts +208 -271
- package/extensions/board/view/styles.ts +123 -31
- 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 +4 -2
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/lines.ts +7 -4
- package/extensions/bytefly/footer/render/segments.ts +37 -10
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- 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/{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/index.ts +67 -36
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -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 +3 -3
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
- 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/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +53 -52
- package/extensions/plan/plan-index.ts +4 -2
- 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 +49 -39
- 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-client.ts +3 -0
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +9 -67
- package/extensions/plan/web-payload.ts +3 -3
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web.ts +1 -12
- 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 +10 -8
- package/extensions/serve/index.ts +17 -3
- package/extensions/serve/registry.ts +26 -17
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +2 -1
- 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/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- 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 -7
- 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/view/todo-screen.ts +122 -0
- 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 +27 -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 +1 -1
- 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 +136 -54
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +87 -30
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +70 -58
- 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 +14 -12
- package/lib/http/port.ts +15 -10
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +39 -10
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +8 -8
- package/lib/session-label.ts +111 -5
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +5 -4
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +23 -8
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +9 -7
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +3 -3
- package/lib/web/README.md +21 -0
- 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 +2 -1
- package/lib/web/runtime.ts +9 -10
- package/package.json +6 -1
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/seat.ts +0 -28
- package/extensions/todo/tui.ts +0 -212
- 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
|
@@ -14,12 +14,16 @@
|
|
|
14
14
|
|
|
15
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import type { Static } from "typebox";
|
|
17
|
-
import { type
|
|
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";
|
|
@@ -27,17 +31,17 @@ import { failedResult, textResult } from "../../lib/tool-result.ts";
|
|
|
27
31
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
28
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 { joinOnClaim } from "./join.ts";
|
|
41
45
|
import { collapsedLine, resultInput } from "./result.ts";
|
|
42
46
|
import { createInput } from "./input.ts";
|
|
43
47
|
import type { BoardSession } from "../../lib/board/session.ts";
|
|
@@ -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. */
|
|
@@ -127,14 +131,16 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
|
|
|
127
131
|
label: "Board",
|
|
128
132
|
description:
|
|
129
133
|
"The shared feature list for this repository: one card per feature, which any session reads and writes. " +
|
|
130
|
-
"A card
|
|
131
|
-
|
|
132
|
-
|
|
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`.",
|
|
133
138
|
promptSnippet: `The shared feature list for this repository: ${BOARD_ACTIONS.join(", ")}.`,
|
|
134
139
|
promptGuidelines: [
|
|
135
|
-
"Claim a card
|
|
136
|
-
"
|
|
137
|
-
"
|
|
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.",
|
|
138
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.",
|
|
139
145
|
"A feature's steps belong in `todo`, not on the board: one card is one feature, whatever it takes.",
|
|
140
146
|
"A `handoff` in your inbox is work you now own: read it with `session inbox`, record the plan, and start it.",
|
|
@@ -154,27 +160,35 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
|
|
|
154
160
|
`unknown action "${String(args.action)}"; use ${BOARD_ACTIONS.join(", ")}.`,
|
|
155
161
|
);
|
|
156
162
|
}
|
|
157
|
-
const me = runtime.who(ctx, ctx.cwd);
|
|
163
|
+
const me = await runtime.who(ctx, ctx.cwd);
|
|
158
164
|
try {
|
|
159
|
-
// Before the join and
|
|
160
|
-
// 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.
|
|
161
167
|
const stray = strayField(action, args, ACTION_FIELDS);
|
|
162
168
|
if (stray !== undefined) return refusal(action, me, stray);
|
|
163
169
|
const store = await runtime.store(ctx.cwd);
|
|
164
170
|
const now = runtime.now();
|
|
165
171
|
switch (action) {
|
|
166
172
|
case "list":
|
|
167
|
-
|
|
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)));
|
|
168
177
|
case "add":
|
|
169
178
|
return await add(store, me, args, now);
|
|
170
179
|
case "claim": {
|
|
171
|
-
const claimed = await mutate(store, action, me, now, args, { kind: "claim" });
|
|
180
|
+
const claimed = await mutate(store, action, me, now, args, { kind: "claim" }, CLAIM_NEXT);
|
|
172
181
|
// Taking a card is what makes this session a teammate. It is the one action here
|
|
173
182
|
// worth being addressed about, and the board is the user's surface for the team, so
|
|
174
183
|
// nobody has to know the bus exists. A refused claim wrote nothing and enlists
|
|
175
184
|
// nobody; a refused join is not a failed claim, so the card stands and the next
|
|
176
185
|
// claim asks again.
|
|
177
|
-
if (!refused(claimed))
|
|
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
|
+
}
|
|
178
192
|
return claimed;
|
|
179
193
|
}
|
|
180
194
|
case "release":
|
|
@@ -183,29 +197,47 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
|
|
|
183
197
|
return await mutate(store, action, me, now, args, { kind: "reclaim" });
|
|
184
198
|
case "move": {
|
|
185
199
|
if (args.card === undefined) return refusal(action, me, 'card is required: pass the id, e.g. "4".');
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
return refusal(
|
|
189
|
-
action,
|
|
190
|
-
me,
|
|
191
|
-
'move needs a target lane: "doing", "blocked", or "done". Use release to return a card to todo.',
|
|
192
|
-
);
|
|
200
|
+
const lane = args.lane;
|
|
201
|
+
if (lane === undefined) {
|
|
202
|
+
return refusal(action, me, `move needs a target lane: ${listOf(movableLanes())}.`);
|
|
193
203
|
}
|
|
194
|
-
// Only the
|
|
195
|
-
// records the commit that closed the card, and
|
|
196
|
-
//
|
|
197
|
-
|
|
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;
|
|
198
213
|
return await mutate(store, action, me, now, args, {
|
|
199
214
|
kind: "move",
|
|
200
|
-
|
|
215
|
+
lane,
|
|
201
216
|
...(args.blockedBy === undefined ? {} : { blockedBy: args.blockedBy }),
|
|
217
|
+
...(args.blocker === undefined ? {} : { blocker: args.blocker }),
|
|
202
218
|
...(landed === undefined ? {} : { landed }),
|
|
219
|
+
...(proved === undefined ? {} : { artifact: proved }),
|
|
203
220
|
});
|
|
204
221
|
}
|
|
205
222
|
case "note":
|
|
206
223
|
return await mutate(store, action, me, now, args, { kind: "note", text: args.text });
|
|
207
|
-
case "
|
|
208
|
-
|
|
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 });
|
|
209
241
|
case "edit": {
|
|
210
242
|
if (
|
|
211
243
|
args.title === undefined &&
|
|
@@ -259,6 +291,42 @@ export function registerTools(pi: ExtensionAPI, runtime: BoardSession, deps: { t
|
|
|
259
291
|
});
|
|
260
292
|
}
|
|
261
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
|
+
|
|
262
330
|
/** `list`: the board, one card, the lead's rollup, or the archive. */
|
|
263
331
|
async function read(
|
|
264
332
|
store: BoardStore,
|
|
@@ -266,6 +334,7 @@ async function read(
|
|
|
266
334
|
me: BoardDetails["me"],
|
|
267
335
|
args: BoardArgs,
|
|
268
336
|
now: number,
|
|
337
|
+
roster: () => Promise<readonly RosterSession[] | undefined>,
|
|
269
338
|
): Promise<ReturnType<typeof textResult<BoardDetails>>> {
|
|
270
339
|
if (args.archived === true) return readArchive(store, action, me, args, now);
|
|
271
340
|
const cards = await store.list();
|
|
@@ -275,6 +344,8 @@ async function read(
|
|
|
275
344
|
// A directory listing, not a read: a listing says what it left aside without opening one.
|
|
276
345
|
const archived = await store.archivedCount();
|
|
277
346
|
if (args.card !== undefined) {
|
|
347
|
+
const ignored = cardFiltersRefusal(args);
|
|
348
|
+
if (ignored !== undefined) return refusal(action, me, ignored);
|
|
278
349
|
const id = normalizeCardId(args.card);
|
|
279
350
|
if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
|
|
280
351
|
const card = cards.find((entry) => entry.id === id);
|
|
@@ -296,7 +367,7 @@ async function read(
|
|
|
296
367
|
}
|
|
297
368
|
const listed = cards.filter(
|
|
298
369
|
(card) =>
|
|
299
|
-
(args.
|
|
370
|
+
(args.lane === undefined || card.lane === args.lane) &&
|
|
300
371
|
(args.query === undefined || matchesQuery(card, args.query)) &&
|
|
301
372
|
// The same rule a claim is checked against, so "mine" cannot list a card this
|
|
302
373
|
// session could not release.
|
|
@@ -306,18 +377,23 @@ async function read(
|
|
|
306
377
|
// repository's own board is mostly finished cards — so a listing that names no lane
|
|
307
378
|
// leaves them out and says how many. A named lane, `card`, and `round` are unchanged;
|
|
308
379
|
// the rollup counts the whole board, which is what makes it a rollup.
|
|
309
|
-
const hiddenDone = args.
|
|
310
|
-
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");
|
|
311
382
|
if (args.round !== true) {
|
|
312
|
-
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
|
+
});
|
|
313
389
|
}
|
|
314
390
|
const round = buildRound(cards, now);
|
|
315
391
|
// One line under the rollup, because a reader who cannot see a card file should know why,
|
|
316
392
|
// and one for the archive, because a rollup that silently left it out would be a board
|
|
317
393
|
// smaller than the one on disk.
|
|
318
|
-
const notes = [
|
|
319
|
-
const text = [formatRound(cards, round), ...notes].join("\n");
|
|
320
|
-
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 } : {}) });
|
|
321
397
|
}
|
|
322
398
|
|
|
323
399
|
/**
|
|
@@ -334,7 +410,7 @@ async function readArchive(
|
|
|
334
410
|
args: BoardArgs,
|
|
335
411
|
now: number,
|
|
336
412
|
): Promise<ReturnType<typeof textResult<BoardDetails>>> {
|
|
337
|
-
const wrong = (["
|
|
413
|
+
const wrong = (["lane", "round"] as const).filter((field) => args[field] !== undefined);
|
|
338
414
|
if (wrong.length > 0) {
|
|
339
415
|
return refusal(
|
|
340
416
|
action,
|
|
@@ -344,6 +420,9 @@ async function readArchive(
|
|
|
344
420
|
}
|
|
345
421
|
const pile = await store.archived();
|
|
346
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);
|
|
347
426
|
const id = normalizeCardId(args.card);
|
|
348
427
|
if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
|
|
349
428
|
const card = pile.cards.find((entry) => entry.id === id);
|
|
@@ -360,7 +439,12 @@ async function readArchive(
|
|
|
360
439
|
const shown = pile.cards.filter(
|
|
361
440
|
(card) => (args.query === undefined || matchesQuery(card, args.query)) && (args.mine !== true || holds(card, me)),
|
|
362
441
|
);
|
|
363
|
-
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
|
+
});
|
|
364
448
|
}
|
|
365
449
|
|
|
366
450
|
/** `add`: the only action that creates a card. */
|
|
@@ -384,13 +468,16 @@ async function mutate(
|
|
|
384
468
|
now: number,
|
|
385
469
|
args: BoardArgs,
|
|
386
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,
|
|
387
473
|
): Promise<ReturnType<typeof textResult<BoardDetails>>> {
|
|
388
474
|
if (args.card === undefined) return refusal(action, me, 'card is required: pass the id, e.g. "4".');
|
|
389
475
|
const id = normalizeCardId(args.card);
|
|
390
476
|
if (id === undefined) return refusal(action, me, `"${args.card}" is not a card id.`);
|
|
391
477
|
const result = await store.mutate(id, op, me, now);
|
|
392
478
|
if (!result.ok) return refusal(action, me, result.error, result.card);
|
|
393
|
-
|
|
479
|
+
const lines = [formatCard(result.card), ...(next === undefined ? [] : [next])];
|
|
480
|
+
return textResult(lines.join("\n"), { action, me, card: result.card });
|
|
394
481
|
}
|
|
395
482
|
|
|
396
483
|
/**
|
|
@@ -415,7 +502,7 @@ async function archiveCards(
|
|
|
415
502
|
if (!result.changed) return textResult(`${cardRef(id)} is already archived.`, { action: "archive", me });
|
|
416
503
|
return textResult(formatArchived([id]), { action: "archive", me, card: result.card, archived: [id] });
|
|
417
504
|
}
|
|
418
|
-
const finished = (await store.list()).filter((card) => card.
|
|
505
|
+
const finished = (await store.list()).filter((card) => card.lane === "done");
|
|
419
506
|
const moved: string[] = [];
|
|
420
507
|
const failed: Array<{ card: string; error: string }> = [];
|
|
421
508
|
for (const card of finished) {
|
|
@@ -10,53 +10,47 @@
|
|
|
10
10
|
import type { Card } from "../../lib/board/card.ts";
|
|
11
11
|
import type { Actor } from "../../lib/board/rules.ts";
|
|
12
12
|
import type { RoundSummary } from "../../lib/board/round.ts";
|
|
13
|
+
import { BOARD_ACTIONS, type BoardAction } from "../../lib/board/actions.ts";
|
|
13
14
|
|
|
14
15
|
/** The tool's name. One extension registers it, so it stays here. */
|
|
15
16
|
export const BOARD_TOOL = "board";
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"release",
|
|
23
|
-
"reclaim",
|
|
24
|
-
"move",
|
|
25
|
-
"note",
|
|
26
|
-
"plan",
|
|
27
|
-
"edit",
|
|
28
|
-
"archive",
|
|
29
|
-
"drop",
|
|
30
|
-
] as const;
|
|
31
|
-
|
|
32
|
-
/** One action. */
|
|
33
|
-
export type BoardAction = (typeof BOARD_ACTIONS)[number];
|
|
18
|
+
// The actions are the board's, not the tool's: plan mode's read-only policy and the todo
|
|
19
|
+
// nudge classify a call by the same table, and nothing under `lib/` may import this
|
|
20
|
+
// file. Re-exported here so the schema, the description, and the suites keep one import
|
|
21
|
+
// for the tool's own vocabulary.
|
|
22
|
+
export { BOARD_ACTIONS, type BoardAction };
|
|
34
23
|
|
|
35
24
|
/**
|
|
36
|
-
* What each
|
|
25
|
+
* What each action does, in one sentence.
|
|
37
26
|
*
|
|
38
27
|
* The tool description is built from this table and the parameter's `action`
|
|
39
|
-
* description points at that sentence, so the
|
|
28
|
+
* description points at that sentence, so the actions are stated once. A new action is
|
|
40
29
|
* a row here and an entry in {@link BOARD_ACTIONS}; `test` holds the two together.
|
|
41
30
|
*/
|
|
42
31
|
export const BOARD_ACTION_DOCS: Record<BoardAction, string> = {
|
|
43
32
|
list:
|
|
44
|
-
"shows the board — `
|
|
33
|
+
"shows the board — `lane` filters one lane, `mine` only what this session holds, " +
|
|
45
34
|
"`query` matches a title, a holder, a path hint, the plan, or a note's text, `card` reads that one card " +
|
|
46
|
-
"instead of the board, and `round` adds the lead's rollup of owners, unassigned, unplanned,
|
|
47
|
-
"idle, which always covers the whole board — and a listing that names no lane leaves out the `done` cards, " +
|
|
35
|
+
"instead of the board, which refuses the fields that narrow a listing beside it, and `round` adds the lead's rollup of owners, unassigned, unplanned, awaiting approval, " +
|
|
36
|
+
"blocked, idle, and done, which always covers the whole board — and a listing that names no lane leaves out the `done` cards, " +
|
|
48
37
|
"saying how many it left out",
|
|
49
|
-
add: "creates a `
|
|
50
|
-
claim: "
|
|
51
|
-
release: "gives it back",
|
|
52
|
-
reclaim: "takes it from whoever holds it",
|
|
53
|
-
move: "changes lane",
|
|
38
|
+
add: "creates a `backlog` card",
|
|
39
|
+
claim: "pulls one into `plan` for this session, when `plan` has room",
|
|
40
|
+
release: "gives it back to `backlog`",
|
|
41
|
+
reclaim: "takes it from whoever holds it, back to `backlog`",
|
|
42
|
+
move: "changes lane, through the one transition the board allows from where the card is; a card enters `execute` or `done` only while the plan it recorded is still in the store",
|
|
54
43
|
note: "appends to its history",
|
|
55
|
-
|
|
44
|
+
contract:
|
|
45
|
+
"records the plan file and the three lines a card promises — `goal`, `acceptance`, `scopeOut` — and drops any approval, because the wording changed; `path` is required and must be the plan `plan` action `write` saved in this repository's plan store",
|
|
46
|
+
approve:
|
|
47
|
+
"approves the contract: the lead (`board.lead`, `lead` by default, the name the page serves as), who does not hold the card, says the wording is right, which is what `plan` to `execute` requires, and `text` records what the signer saw",
|
|
48
|
+
review:
|
|
49
|
+
"records a spot-check of the finished work: the lead, who does not hold the card, in `execute`, which is what `done` requires, and `text` records what the signer saw",
|
|
56
50
|
edit: "fixes what the card says — title, path hints, priority, and what it waits for",
|
|
57
51
|
archive:
|
|
58
52
|
"sets a finished card aside, out of the board and still readable, or every finished card when no `card` is named",
|
|
59
|
-
drop: "deletes a `
|
|
53
|
+
drop: "deletes a `backlog` or a `done` card, or one you hold",
|
|
60
54
|
};
|
|
61
55
|
|
|
62
56
|
/**
|
|
@@ -71,14 +65,16 @@ export const BOARD_ACTION_DOCS: Record<BoardAction, string> = {
|
|
|
71
65
|
* a new field cannot be published without an action that acts on it.
|
|
72
66
|
*/
|
|
73
67
|
export const ACTION_FIELDS: Record<BoardAction, readonly string[]> = {
|
|
74
|
-
list: ["card", "
|
|
68
|
+
list: ["card", "lane", "mine", "query", "round", "archived"],
|
|
75
69
|
add: ["title", "paths", "priority", "note"],
|
|
76
70
|
claim: ["card"],
|
|
77
71
|
release: ["card"],
|
|
78
72
|
reclaim: ["card"],
|
|
79
|
-
move: ["card", "
|
|
73
|
+
move: ["card", "lane", "blockedBy", "blocker"],
|
|
80
74
|
note: ["card", "text"],
|
|
81
|
-
|
|
75
|
+
contract: ["card", "path", "goal", "acceptance", "scopeOut"],
|
|
76
|
+
approve: ["card", "text"],
|
|
77
|
+
review: ["card", "text"],
|
|
82
78
|
edit: ["card", "title", "paths", "priority", "dependsOn"],
|
|
83
79
|
archive: ["card"],
|
|
84
80
|
drop: ["card"],
|
|
@@ -89,6 +85,14 @@ export interface BoardDetails {
|
|
|
89
85
|
action: BoardAction;
|
|
90
86
|
/** The serving session: the id Pi knows, and the label a card records. */
|
|
91
87
|
me: Actor;
|
|
88
|
+
/**
|
|
89
|
+
* Whether the read was narrowed to this session's own cards.
|
|
90
|
+
*
|
|
91
|
+
* Kept because the transcript says so: a listing that answers "what am I holding" draws
|
|
92
|
+
* those rows in the accent, and a listing that answers "what is on the board" does not
|
|
93
|
+
* pretend every row is the reader's.
|
|
94
|
+
*/
|
|
95
|
+
mine?: boolean;
|
|
92
96
|
/** The board a `list` read, in lane order. */
|
|
93
97
|
cards?: Card[];
|
|
94
98
|
/** The card an action produced, or the one a refusal left untouched. */
|