@lovek4d/kickflip 0.5.3 → 0.6.1
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/CONTEXT.md +82 -46
- package/SKILL.md +21 -15
- package/bin/doctor.mjs +5 -4
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +12 -12
- package/bin/wait-events.mjs +2 -2
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +179 -92
- package/board/build-script.mjs +2 -1
- package/board/domain.mjs +9 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +9 -5
- package/board/onboarding.mjs +11 -0
- package/board/prototype-base.mjs +1 -0
- package/board/prototype-page.mjs +1 -0
- package/board/public/app-arch.js +1 -1
- package/board/public/app-cards.js +40 -47
- package/board/public/app-header.js +6 -7
- package/board/public/app-lanes.js +19 -45
- package/board/public/app-needs.js +36 -31
- package/board/public/app-snapshot.js +43 -70
- package/board/public/app-threads.js +42 -46
- package/board/public/app.html +9 -4
- package/board/public/app.js +78 -32
- package/board/public/hub-kit.js +62 -2
- package/board/public/hub.css +2 -0
- package/board/public/new-task.js +8 -6
- package/board/public/progress-review.html +46 -0
- package/board/public/progress-review.js +158 -0
- package/board/public/prototype-kit.js +109 -4
- package/board/public/prototype-switcher.js +130 -31
- package/board/public/prototype.html +57 -3
- package/board/public/router.js +7 -6
- package/board/public/shell.js +65 -24
- package/board/public/sidebar.css +6 -0
- package/board/public/sidebar.js +19 -69
- package/board/public/style.css +116 -73
- package/board/public/ui-cards.js +78 -57
- package/board/public/ui-enums.js +2 -2
- package/board/public/ui-header.js +23 -16
- package/board/public/ui-hub.js +19 -5
- package/board/public/ui-links.js +7 -7
- package/board/public/ui-order.js +1 -1
- package/board/public/ui-page-header.js +19 -7
- package/board/public/ui-patch.js +7 -33
- package/board/public/ui-progress-bar.js +28 -0
- package/board/public/ui-questions.js +25 -17
- package/board/public/ui-sidebar.js +31 -23
- package/board/public/ui-snapshot.js +44 -53
- package/board/public/ui-threads.js +57 -28
- package/board/public/ui.js +94 -18
- package/board/public/view-hub-settings.js +12 -58
- package/board/public/view-hub.js +5 -2
- package/board/public/view-notes.js +27 -7
- package/board/public/view-questions.js +90 -68
- package/board/public/view-settings.js +133 -15
- package/board/render-check.mjs +10 -0
- package/board/review.mjs +1 -1
- package/board/server.mjs +20 -19
- package/board/stop.mjs +1 -0
- package/board/store.mjs +29 -18
- package/board/sync.mjs +2 -0
- package/board/tickets.mjs +3 -3
- package/board/transcript.mjs +2 -2
- package/board/worktree.mjs +22 -21
- package/hooks-plugin/hooks/board-data-gate.mjs +9 -6
- package/hooks-plugin/hooks/usage-gate.mjs +20 -3
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/front-door.mjs +2 -2
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/pr-report.mjs +2 -2
- package/hub/review-send.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/share.mjs +1 -1
- package/hub/slack.mjs +2 -2
- package/hub/try.mjs +6 -6
- package/hub/upgrade.mjs +1 -1
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/card-prs.mjs +1 -1
- package/shared/machine-gate.mjs +2 -0
- package/shared/prototype-frame.js +1 -0
- package/shared/tailscale.mjs +1 -1
- package/shared/usage-gate.mjs +1 -1
- package/board/public/ui-phase-bars.js +0 -17
package/board/bin/board.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var L=Object.defineProperty;var r=(e,a)=>L(e,"name",{value:a,configurable:!0});import{readiness as H}from"../../hub/readiness.mjs";import{readSlack as D,setSlack as M}from"../../hub/slack.mjs";import{routeRecord as y}from"../../hub/routes.mjs";import f from"node:fs";import x from"node:path";import{pathToFileURL as W}from"node:url";import*as s from"../store.mjs";import{boardWorktree as z,builderRepo as U,joiningLand as F,landInTurn as J,mergeHelper as G,worktreesDir as K}from"../worktree.mjs";import{pidAlive as Q}from"../headless.mjs";import{fileFindings as V}from"../arch-review.mjs";import{finishCard as $,landHeld as X}from"../finish.mjs";import{bump as O,queueLines as Y,stallCard as Z,stopBuilder as ee,stopForBlock as te,stopLand as ae}from"../stop.mjs";import{boardAt as se}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";import{checkTakesRender as ne}from"../render-check.mjs";import{prototypeBase as re}from"../prototype-base.mjs";const ie=`usage: node board.mjs <command> [--site <route>] ...
|
|
3
3
|
|
|
4
4
|
Writes a kickflip board's data files. --site <route> picks ~/.claude/kickflip/sites/<route>, and its
|
|
5
5
|
board.json says where the board's waves live. Without --site, waves come from ~/.claude/waves.
|
|
@@ -9,8 +9,8 @@ item add creates a ticket in the board\u2019s source unless --ticket <id> links
|
|
|
9
9
|
Internal defaults to INT; GitHub and Linear create an issue from the title and notes, Linear in the board's team and
|
|
10
10
|
project through Hub's Linear key. A linked ticket must exist in the source; an Internal one no other card holds.
|
|
11
11
|
Asks have no ticket; item set --from-ask creates one when the card has none.
|
|
12
|
-
A card moves its ticket one way:
|
|
13
|
-
|
|
12
|
+
A card moves its ticket one way: a build's start \u2192 In Progress, --pr, --pushed or a verified merge-ready build \u2192 In
|
|
13
|
+
Review, --done \u2192 Done (a GitHub issue closes), only for a ticket assigned to the owner or created by kickflip. A ticket that
|
|
14
14
|
can't move prints one "ticket <id>: \u2026" line; the card's change still stands.
|
|
15
15
|
Linear needs Hub's Linear key: settings set tickets refuses it without one, and once the key is removed the hub
|
|
16
16
|
moves every Linear board to internal tickets. A card is never left without a ticket: if Linear can't make one, the
|
|
@@ -25,12 +25,15 @@ command fails and changes nothing.
|
|
|
25
25
|
A Claude thread line, or a reply to an ask or item-message, rewrites the body of the thread's
|
|
26
26
|
queued push (tag ask-<eventId> or item-<wave>/<id>) to the message's first line, keeping
|
|
27
27
|
"\xB7 and N replies", and prints the new body. It never pushes: the thread's unread mark carries it
|
|
28
|
+
visual add <wave>/<id> --file <page.html> --title <t> a visual agent's page: copied to public/visuals/<id>/<slug>.html
|
|
29
|
+
(a new name each time), linked by a visual line in the
|
|
30
|
+
item's thread; prints the page's link
|
|
28
31
|
topic add <id> --title <t> [--summary <s>] a question topic in data/owner.json
|
|
29
32
|
question add --file <json> one question or an array of them:
|
|
30
33
|
{id, topic, question, options: [{id, label, recommended?, detail?, diagram?, practice?}],
|
|
31
34
|
source?, context?, url?, urgent?, round_of?, reask_of?, ask?, suggested?, prototype_gate?}; topic is declared, item:<wave>/<id>, item:ask:<eventId>
|
|
32
35
|
for a New card not sorted yet, lineup:<slug> for a wave's triage, or wave:<slug> for a wave file's triage on
|
|
33
|
-
a board over ~/.claude/waves. A question on a done
|
|
36
|
+
a board over ~/.claude/waves. A question on a done card reopens it.
|
|
34
37
|
A question on an item: topic whose card has a grill or prototype planner running is staged, not published,
|
|
35
38
|
whoever adds it: it shows nowhere until the planner finishes, fails or is stopped, when every staged question
|
|
36
39
|
on the card publishes in the order added, with one owner push. The command says "staged" and why.
|
|
@@ -44,14 +47,16 @@ command fails and changes nothing.
|
|
|
44
47
|
danger|success, to the next row's box indexes (default all); practice: ["<line>", \u2026]
|
|
45
48
|
A prototype url's topic must be recorded (prototype add), its option ids a non-empty subset of the
|
|
46
49
|
take keys in take order, without repeats, and every take file written; a hand-written prototype-*.html
|
|
47
|
-
is refused.
|
|
50
|
+
is refused. Each take is then loaded headlessly at 390px and 1280px, and the question is refused,
|
|
51
|
+
naming the take and width, if one throws, logs a console error or draws an empty stage.
|
|
52
|
+
On an item: topic, each other question answered and still waiting on Claude is
|
|
48
53
|
resolved, noting "Followed by <new id>", and printed as "resolved: <old id>". Any other unresolved
|
|
49
54
|
question on the same topic that links the same prototype is resolved, noting "Replaced by <new id>",
|
|
50
55
|
and printed as "replaced: <old id>". round_of names the question on the same topic whose
|
|
51
56
|
prototype-request this answers; the question needs a prototype url, takes the next round
|
|
52
57
|
number, and resolves that one, noting "Followed by prototype round N".
|
|
53
58
|
prototype_gate: true marks a grill's closing "Prototype round first?" question (options yes, no, on a card's
|
|
54
|
-
topic): the owner's yes moves the card to prototyping and starts its prototype planner, a no
|
|
59
|
+
topic): the owner's yes moves the card to prototyping and starts its prototype planner, a no stages it
|
|
55
60
|
once no question on it is left open, the board doing both with no orchestrator step.
|
|
56
61
|
reask_of names a resolved question on the same topic that the owner asked to ask again
|
|
57
62
|
(reask-request): it resolves that one again, noting "Asked again as <new id>", printed as "resolved: <old id>".
|
|
@@ -77,6 +82,17 @@ command fails and changes nothing.
|
|
|
77
82
|
public/prototypes/<topic>/<K>.js, which exports
|
|
78
83
|
render(stage, kit) (board/public/prototype-kit.js lists kit).
|
|
79
84
|
--card starts that card: it leaves Todo
|
|
85
|
+
[--served <path> [--state <key>:<label> \u2026]] a served round: no take files; the template frames <path>
|
|
86
|
+
from the board's base prototype worktree (prototype base),
|
|
87
|
+
passing ?variant=<K>&state=<key>, then each switch by
|
|
88
|
+
message (shared/prototype-frame.js). The viewer bar lists
|
|
89
|
+
the states; key: a-z, 0-9 and "-"
|
|
90
|
+
prototype base makes the board's base prototype worktree, a detached
|
|
91
|
+
checkout of its repo's main, or moves it to main's tip,
|
|
92
|
+
keeping the rounds' pages; prints its path, whether its
|
|
93
|
+
build script is there and the listener a page loads.
|
|
94
|
+
The hub serves it while an open question links a served
|
|
95
|
+
round (docs/adr/2026-11-06-1500-a-prototype-round-is-a-page-in-the-boards-base-worktree.md)
|
|
80
96
|
prototype list one line per recorded prototype
|
|
81
97
|
prototype rm <topic> drops the record, keeping its take files; refuses while an
|
|
82
98
|
open question links it
|
|
@@ -90,9 +106,12 @@ command fails and changes nothing.
|
|
|
90
106
|
blank line and <!-- claude-bridge --> after, the marker pr
|
|
91
107
|
reads as Claude's own. Refuses words that already carry
|
|
92
108
|
either. Prints the post's url
|
|
93
|
-
item set <wave>/<id> [--
|
|
94
|
-
|
|
95
|
-
|
|
109
|
+
item set <wave>/<id> [--done --text <t> [--force]] edits the item in its wave file; --done closes the card
|
|
110
|
+
(below). A card has no status: it stores its lane, which
|
|
111
|
+
a build's start, a handover, a block and a close move,
|
|
112
|
+
and the board server moves the rest, each with a thread
|
|
113
|
+
line; --status is refused
|
|
114
|
+
[--next <t> | --clear-next] an empty --next "" clears it, as --clear-next does
|
|
96
115
|
naming another unfinished card requires it in --deps
|
|
97
116
|
[--pr <n> | --pr <owner/repo#n>]... adds a PR to the card, a bare number in the board's repo;
|
|
98
117
|
repeatable, and never replaces one. Each PR gets its own
|
|
@@ -102,7 +121,7 @@ command fails and changes nothing.
|
|
|
102
121
|
on the owner's merge. Push it first (git push origin
|
|
103
122
|
<branch>). The hub's repo fetch moves it to Done once the
|
|
104
123
|
branch is merged into the board's branch or gone from the
|
|
105
|
-
remote.
|
|
124
|
+
remote. Moves it to In Review as --pr does
|
|
106
125
|
[--gh-since <ISO time>] records the last handled GitHub event, an ISO-8601 UTC
|
|
107
126
|
time such as 2026-10-23T11:00:00Z, stored as given
|
|
108
127
|
[--from-ask <eventId> --text <t>] the ask it came from: its New card gives way to this item,
|
|
@@ -111,19 +130,22 @@ command fails and changes nothing.
|
|
|
111
130
|
from another ask (use item relate-ask), and one with no
|
|
112
131
|
state when --state is missing
|
|
113
132
|
[--state ready|grilling|prototyping] what a card from an ask does next; it reads New until set.
|
|
114
|
-
|
|
133
|
+
Staging is the board's: it stages a card on the owner's
|
|
134
|
+
Send, Implement or Reply on a ready card, and refuses
|
|
135
|
+
--state staging, bar --from-ask on a Quick build ask,
|
|
136
|
+
which files it staging for the orchestrator to judge.
|
|
137
|
+
ready refuses a card not staging or ready, a Quick
|
|
138
|
+
build ask's included, a card with an open question, and
|
|
115
139
|
a card without --size, a notes file, or the id of each
|
|
116
140
|
answered question in that file, since a builder starts
|
|
117
|
-
from it; it resolves those answered questions.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"released: <key> starts building now"; one a dep still
|
|
121
|
-
blocks is bumped to the top of Todo instead ("bumped: \u2026")
|
|
141
|
+
from it; it resolves those answered questions.
|
|
142
|
+
grilling or prototyping on a staging or ready card sends
|
|
143
|
+
it back, and the board starts that round's planner
|
|
122
144
|
[--held | --unheld | --pick-up-at <time>] holds or resumes the card on the owner's words; the board
|
|
123
145
|
server records the owner's own Hold and Resume taps. A later
|
|
124
|
-
--held/--unheld overrides a tap
|
|
146
|
+
--held/--unheld overrides a tap.
|
|
125
147
|
--pick-up-at holds it until an ISO time such as
|
|
126
|
-
2026-11-04T09:00 (the
|
|
148
|
+
2026-11-04T09:00 (the host's local time without an
|
|
127
149
|
offset; a past time is refused), stored as UTC
|
|
128
150
|
pick_up_at. Within a minute of it the board server
|
|
129
151
|
resumes the card, adds a thread line, and appends
|
|
@@ -137,14 +159,16 @@ command fails and changes nothing.
|
|
|
137
159
|
[--size small|large] [--model opus|sonnet] what builder pick reads while the hub's Claude model is
|
|
138
160
|
Auto: a small card gets Claude Sonnet, any other Opus, and
|
|
139
161
|
--model wins over --size; "" clears either
|
|
140
|
-
[--notes-file <file>] the item's notes: a .md file inside its wave's folder,
|
|
141
|
-
named relative to it, holding the session's understanding
|
|
142
|
-
of the task so far. The open card's Notes link opens it,
|
|
143
|
-
or without one the item's short notes text. --plan is an
|
|
144
|
-
alias. Resolves answered questions still waiting on
|
|
145
|
-
Claude, noting "Recorded in the card's notes", and
|
|
146
|
-
prints "resolved: <id>" for each
|
|
147
162
|
[--deps <id>,<wave>/<id>] replaces its blocked-by list; --deps "" clears it
|
|
163
|
+
[--blocked-until "<condition>"] blocks it until a condition an agent can check, which
|
|
164
|
+
the orchestrator checks on its passes over the cards;
|
|
165
|
+
--blocked-until "" clears it, which threads and emits
|
|
166
|
+
card-unblocked {key} once nothing else blocks it
|
|
167
|
+
[--block-reason "<reason>"] why it is blocked, by its deps or its condition, which
|
|
168
|
+
goes when the block does; --block-reason "" clears it
|
|
169
|
+
A block on a card In Progress sends it back to Todo,
|
|
170
|
+
stopping its board-launched builder (refused for a
|
|
171
|
+
session's own builder); one In Review stays, unlanded
|
|
148
172
|
[--parent <wave>/<id>] sets its Parent, refusing itself or a card below it;
|
|
149
173
|
--parent "" clears it
|
|
150
174
|
[--related <id>,<wave>/<id>] replaces the cards it is Related to, which both cards
|
|
@@ -158,56 +182,67 @@ command fails and changes nothing.
|
|
|
158
182
|
an https:// .apk URL for Android. --install-sha is the
|
|
159
183
|
head it was built from: the badge reads "Install (stale)"
|
|
160
184
|
once the branch moves past it. Hidden once the card is Done
|
|
161
|
-
[--text <t>] with --
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
builder start for a build
|
|
165
|
-
questions, noting "Acted on: build started".
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
building and fixing count as working only while a builder record on the card is open.
|
|
173
|
-
--pr clears building/fixing and closes ordinary builders; install builders keep running.
|
|
174
|
-
checking is a builder's hand-back of work it couldn't land: In Progress, reading "Orchestrator
|
|
175
|
-
review", until the owning session takes it on; it unblocks nothing.
|
|
185
|
+
[--text <t>] with --done, required on a site: the card's closing line,
|
|
186
|
+
written last in its thread, saying why it closed (merged,
|
|
187
|
+
dropped, superseded)
|
|
188
|
+
builder start for a build moves the card to In Progress with "Moved to In Progress: a build
|
|
189
|
+
started", resolving its answered questions, noting "Acted on: build started". It refuses while
|
|
190
|
+
the item is held or a dep isn't done, and while the newest owner answer to its questions that
|
|
191
|
+
link a prototype-*.html page picks no take, which leaves the card prototyping with Next
|
|
192
|
+
"Prototype again" until the next round's pick; a pick's note is the build's guidance.
|
|
193
|
+
--pr closes ordinary builders and moves the card to In Review; install builders keep running.
|
|
194
|
+
A builder's hand-back of work it couldn't land (builder done with judgment calls) keeps the card
|
|
195
|
+
In Progress, reading "Orchestrator review", until the owning session lands it; it unblocks nothing.
|
|
176
196
|
The board server records the owner's card-done in the wave file itself.
|
|
177
|
-
|
|
178
|
-
--status done, merged or dropped closes the card: it resolves each of its questions (topic
|
|
197
|
+
--done moves the card to Done and its ticket to Done: it resolves each of its questions (topic
|
|
179
198
|
item:<wave>/<id>) that has an answer, noting "Closed with the card", and prints "resolved: <id>"
|
|
180
199
|
for each. It refuses while one has no answer yet; [--force] closes it and resolves that one too.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
It prints "unblocked: <wave>/<id>" and appends card-unblocked {key} once per unblocking,
|
|
201
|
+
including held cards: start each unheld card next. It refuses a card the owner reopened: it
|
|
202
|
+
closes again only on the owner's Done
|
|
203
|
+
item add <wave>[/<id>] --title <t> a new card, unstarted; never
|
|
184
204
|
[--ticket <id>] [--deps <id>,<wave>/<id>] hand-edit wave.json to add one. Refuses an id the wave has.
|
|
185
|
-
[--notes-
|
|
205
|
+
[--notes <t> | --notes-from <draft.md>] the board makes the card's own notes, <id>-notes.md beside
|
|
206
|
+
its wave file, read-only: the draft verbatim, or "# <title>"
|
|
207
|
+
then the --notes text. Refuses a draft that is any card's
|
|
208
|
+
notes file, live, done or archived, naming that card
|
|
186
209
|
[--pick-up-at <time>] holds it until then, as item set's --pick-up-at
|
|
187
210
|
[--size small|large] [--model opus|sonnet] as item set's
|
|
188
|
-
[--state ready] a Quick build: the pull takes it as it is, so it needs
|
|
189
|
-
--size and --notes-file. Without it the board grills the
|
|
190
|
-
card as soon as it lands in Todo
|
|
191
211
|
[--parent <wave>/<id>] the card it was spun out of, such as one filed from
|
|
192
212
|
another card's thread, grill or answer: it reads as a
|
|
193
213
|
follow-up, and its Parent lists it in Children. A card
|
|
194
214
|
from the + or New task has none
|
|
195
|
-
[--state
|
|
215
|
+
[--state grilling|prototyping] files it at once, as item set's --state; the session's
|
|
196
216
|
[--relate <id>,<wave>/<id>] own form, linking it with --relate in the same call.
|
|
217
|
+
Never ready: a card reaches ready through staging.
|
|
197
218
|
Without --state it reads New and a card-added event
|
|
198
|
-
|
|
219
|
+
brings in intake triage, which links and files it
|
|
199
220
|
Without /<id>, the card takes its ticket's id (GH-<n> for issue #n); pass an id only to import a
|
|
200
221
|
card. Prints "<label> added as <key>": use the key in later commands
|
|
222
|
+
item notes <wave>/<id> --heading <h> appends "## <h>" and the draft to the card's notes, making
|
|
223
|
+
(--file <draft.md> | --from <wave>/<id>) its <id>-notes.md when it has none; the only way to write
|
|
224
|
+
notes (--notes-file and --plan are gone). Write the draft
|
|
225
|
+
outside the board's data; one that is any card's notes
|
|
226
|
+
file, live, done or archived, is refused, naming that
|
|
227
|
+
card. --from folds that other card's notes in instead,
|
|
228
|
+
headed "Folded from <ref> "<title>"" without --heading.
|
|
229
|
+
Resolves answered questions still waiting on Claude,
|
|
230
|
+
noting "Recorded in the card's notes", and prints
|
|
231
|
+
"resolved: <id>" for each
|
|
201
232
|
item relate-ask <eventId> <wave>/<id> --summary <t> folds an ask about an existing item into it: the ask's
|
|
202
233
|
card goes, its message and thread show in the item's
|
|
203
234
|
thread marked Moved, then the summary line. Again with
|
|
204
235
|
another item moves it there
|
|
205
236
|
ask progress <eventId> [--next <text>] keeps an answered ask in In Review, --next as its next
|
|
206
|
-
line, until the owner's ask-done (
|
|
207
|
-
|
|
237
|
+
line, until the owner's ask-done (the Done beside Reply
|
|
238
|
+
in its thread) or ask done is newer. Use it when the answer
|
|
208
239
|
waits on the owner, such as a pick between prototypes
|
|
209
240
|
ask done <eventId> moves the ask to Done, as the owner's Done button does
|
|
210
241
|
ask unfold <eventId> moves a folded ask back out to a card of its own
|
|
242
|
+
intake handoff <eventId> --text <why> an intake triage run unsure how to sort a new task, a
|
|
243
|
+
message in an ask's thread or an agent-added card hands it
|
|
244
|
+
to the orchestrator: an intake-handoff event, whose wait
|
|
245
|
+
wakes the session, and a status line in its thread
|
|
211
246
|
ask move <eventId> --to <route> sends a New ask to another board in the hub's manifest:
|
|
212
247
|
a New card there with the owner's text, attachments and
|
|
213
248
|
"Moved from <this board>", which wakes its session; here the
|
|
@@ -215,11 +250,6 @@ command fails and changes nothing.
|
|
|
215
250
|
A card the ask became is dropped, unless it has started.
|
|
216
251
|
Refuses a route that isn't another board, and an ask
|
|
217
252
|
that isn't New
|
|
218
|
-
checked [--every <minutes> | --idle] data/checked.json: GitHub was just checked, next in
|
|
219
|
-
<minutes> (default 5, the away cadence); run it at the
|
|
220
|
-
end of each GitHub tick.
|
|
221
|
-
--idle: the board shows no open PR, so never late; with
|
|
222
|
-
neither flag it is idle while no card has an open PR.
|
|
223
253
|
Then sends the queued pushes, each on its own, unless
|
|
224
254
|
pushes are held (overnight, snoozed, the weekend or off)
|
|
225
255
|
lineup create <slug> --title <t> --goal <t> a wave, in the owner's words: a named set of cards in
|
|
@@ -261,21 +291,19 @@ command fails and changes nothing.
|
|
|
261
291
|
slack Slack identity as JSON; imports slack-bridge config once
|
|
262
292
|
slack set [--self-dm D..] [--user U..] sets hub Slack identity in ~/.claude/kickflip/slack.json
|
|
263
293
|
[--domain x] [--timezone Area/City]
|
|
264
|
-
review sent <wave>/<id> <reviewer> --ts <ts> records the PR link's message in the item
|
|
265
|
-
--channel <dm>
|
|
266
|
-
review react <wave>/<id> <reviewer> <emoji> records a reaction once on that reviewer's link
|
|
267
294
|
integrations readiness rows as one JSON line
|
|
268
|
-
review updated <wave>/<id> --text "<what changed>"
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
review digest's answer while
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
While Slack isn't ready: {waiting}
|
|
295
|
+
review updated <wave>/<id> --text "<what changed>" queues \u201CUpdated: <what changed>\u201D in the thread of every
|
|
296
|
+
posted link on the card's PRs; the hub's next poll sends it
|
|
297
|
+
review due one JSON line of the Slack sends the hub owes, from local state
|
|
298
|
+
and data/pr-slack.json: {links: [{pr, key, reviewer,
|
|
299
|
+
slack_user, head, text}], replies: [{pr, id, reviewer,
|
|
300
|
+
channel, thread_ts, text}], reactions: [{pr, reviewer,
|
|
301
|
+
channel, ts, emoji}], digest: review digest's answer while
|
|
302
|
+
due, else null}. links: each open, non-draft PR's link to
|
|
303
|
+
each reviewer without one; reactions: git_approved once
|
|
304
|
+
anyone approved, git_merged on merge, x on close. The hub
|
|
305
|
+
sends and records each. While Slack isn't ready: {waiting}
|
|
306
|
+
and nothing owed
|
|
279
307
|
review post <pr> --file <review.md> posts the session's words as one Comment review (never an
|
|
280
308
|
--read-at <time> approval or a request for changes) on an incoming PR at
|
|
281
309
|
its listed head, as the board's github-login (else gh's
|
|
@@ -290,9 +318,9 @@ command fails and changes nothing.
|
|
|
290
318
|
it chose to say nothing on, needs while it waits on the
|
|
291
319
|
owner, which puts it in Needs you and pushes. Same PRs as
|
|
292
320
|
review post
|
|
293
|
-
review digest pending reviews as JSON
|
|
294
|
-
|
|
295
|
-
|
|
321
|
+
review digest pending reviews as JSON, one item per PR; a reviewer is
|
|
322
|
+
owed the day's digest weekdays 09:00-17:00 in the Slack
|
|
323
|
+
identity timezone until data/review-digests.json has it all
|
|
296
324
|
rules list the hub rules, then with --site the board's, one
|
|
297
325
|
"<hub|board> <id> <owner|claude> <text>" line each
|
|
298
326
|
rules add --text <t> [--hub] [--by claude] a rule in board.json, or with --hub in the hub's
|
|
@@ -319,7 +347,7 @@ command fails and changes nothing.
|
|
|
319
347
|
pushed, which needs Push main after a land granted.
|
|
320
348
|
A local board never opens PRs. Without auto-merge, a
|
|
321
349
|
card with no PR waits in In Review for the owner's Merge
|
|
322
|
-
build <wave>/<id> --site <route> asks the hub to run this card's
|
|
350
|
+
build <wave>/<id> --site <route> asks the hub to run this card's build script; prints
|
|
323
351
|
--asked "<owner's words>" started and exits. install is an alias. Only the owner's
|
|
324
352
|
Serve / Build and Auto-build start a card run, so --asked
|
|
325
353
|
must quote the owner's ask, card message or answer note on
|
|
@@ -334,13 +362,16 @@ command fails and changes nothing.
|
|
|
334
362
|
the hub asks gh api user. Pass "" to clear it
|
|
335
363
|
settings set auto-build on|off on: each push builds its card. Off by default.
|
|
336
364
|
Main builds after every merge either way
|
|
365
|
+
settings set build-target <target>|default the script the hub runs for each card and main:
|
|
366
|
+
.kickflip/build-<target>, or .kickflip/build with
|
|
367
|
+
default (unset). Board Settings' Build target picks it
|
|
337
368
|
settings set auto-pull on|off Auto implement Todo cards. on: on every board change the
|
|
338
369
|
board server starts and launches the build of the top
|
|
339
370
|
ready Todo cards nothing blocks, holds or asks about, one
|
|
340
371
|
per free implementer slot (see max-implementers), each
|
|
341
372
|
once, and launches a grill on each card that arrives in
|
|
342
|
-
Todo, up to max-planners at a time (an arrival: not ready
|
|
343
|
-
or held, with no planner record and no questions
|
|
373
|
+
Todo, up to max-planners at a time (an arrival: not ready,
|
|
374
|
+
staging or held, with no planner record and no questions
|
|
344
375
|
yet). Off by default.
|
|
345
376
|
Only on the owner's word
|
|
346
377
|
settings set max-implementers <1..99> Maximum parallel implementers, 10 by default: the cards
|
|
@@ -396,8 +427,9 @@ command fails and changes nothing.
|
|
|
396
427
|
--agent <other name> refuses while the tap's main record
|
|
397
428
|
is open and not taken up.
|
|
398
429
|
A build's start clears the card's Next.
|
|
399
|
-
A build (--kind build, claude or codex) on
|
|
400
|
-
|
|
430
|
+
A build (--kind build, claude or codex) on a card not done
|
|
431
|
+
and not In Progress moves it to In Progress first,
|
|
432
|
+
refusing what item set's build start refuses;
|
|
401
433
|
Prints "ref: <ref>", the card's label, which commit subjects start with.
|
|
402
434
|
Resolves the card's answered questions, noting "Acted on:
|
|
403
435
|
builder started"
|
|
@@ -422,10 +454,9 @@ command fails and changes nothing.
|
|
|
422
454
|
the card's status shows the newest one from an agent still working. builder progress refuses
|
|
423
455
|
without an open record for that agent, as builder call does
|
|
424
456
|
builder done <wave>/<id> [--agent <name>] marks that agent's record done; the item's thread
|
|
425
|
-
[--next <t>] [--text <what you checked>] shows when it started and finished
|
|
426
|
-
when the card has no open builder records left. On a
|
|
457
|
+
[--next <t>] [--text <what you checked>] shows when it started and finished. On a
|
|
427
458
|
record with unverified judgment calls it hands the card
|
|
428
|
-
back:
|
|
459
|
+
back: its records close marked handed back, with Next "Answer the builder's
|
|
429
460
|
questions" when a call starts "Question:", else "Verify
|
|
430
461
|
the builder's judgment calls"; --next is refused there.
|
|
431
462
|
Else, on a board, the main build's record that keeps a
|
|
@@ -438,13 +469,15 @@ command fails and changes nothing.
|
|
|
438
469
|
owner's merge (item set --pushed). A finish that fails
|
|
439
470
|
refuses with the reason and leaves the record open; one
|
|
440
471
|
refused while another agent's record is open names it.
|
|
472
|
+
A helper's done takes --text, its hand-back, which agent
|
|
473
|
+
wait prints.
|
|
441
474
|
--next stops a build without finishing it. Closing the
|
|
442
475
|
last record of a card left In Progress with no Next is
|
|
443
476
|
refused without --next, which sets it
|
|
444
477
|
[--owner-step <t>] a step only the owner can do once the code lands: the card
|
|
445
478
|
then waits in In Review with it as its Next, its ticket
|
|
446
479
|
open, until the owner's Done; refused on a card already
|
|
447
|
-
done
|
|
480
|
+
done. land takes it too
|
|
448
481
|
[--check "<name> <args>"] with --owner-step: a read-only check on the step's final
|
|
449
482
|
evidence, which the hub's poll runs on each check once the
|
|
450
483
|
code lands, closing the card when it passes: tag
|
|
@@ -455,10 +488,46 @@ command fails and changes nothing.
|
|
|
455
488
|
arguments are refused. Three runs in a row that can't run
|
|
456
489
|
mark it broken and append owner-check-broken {key, check,
|
|
457
490
|
error}; fix it with item set --check. land takes it too
|
|
491
|
+
[--updated "<what changed>" | --updated none] on a card whose PR link a reviewer already has, done
|
|
492
|
+
refuses without it: the text queues \u201CUpdated: <what
|
|
493
|
+
changed>\u201D in each link's thread, as review updated does;
|
|
494
|
+
none sends nothing, for a change only to comments, names,
|
|
495
|
+
docs or tests
|
|
458
496
|
builder takes --dir <dir> for a wave file in <dir>; its records go in <dir>/agents
|
|
497
|
+
builder stop <wave>/<id> the orchestrator's stop of the card's run the board launched,
|
|
498
|
+
its headless builder, lander or rebase run, with every process
|
|
499
|
+
under it: the record stays open, marked stopped, so the server
|
|
500
|
+
relaunches nothing and the card stays In Progress reading
|
|
501
|
+
"build stopped by orchestrator" ("land \u2026" while it lands),
|
|
502
|
+
waiting on the orchestrator; the same words go in its thread.
|
|
503
|
+
Prints what it stopped, one process a line. Refuses a planner,
|
|
504
|
+
a builder a session started itself, or none running
|
|
505
|
+
item stall <wave>/<id> --reason "<why>" the orchestrator's flag on a stale card it can't restart: the
|
|
506
|
+
card stays in its lane reading red "stalled", an Action needed
|
|
507
|
+
"stalled: <why>" with one push, waiting on nobody else; its
|
|
508
|
+
run stops as builder stop stops it (nothing to stop when it
|
|
509
|
+
is gone), its record stays open and stopped and its worktree
|
|
510
|
+
stays, and it frees its implementer slot. Its thread reads
|
|
511
|
+
"stalled by orchestrator: <why>". A message on the card
|
|
512
|
+
clears it
|
|
513
|
+
land stop <wave>/<id> the orchestrator's stop of the card's land and its suite: one a
|
|
514
|
+
headless builder or lander runs stops that agent as builder
|
|
515
|
+
stop does; the land a held card's Merge tap started stops
|
|
516
|
+
alone, leaving the land queue, and the card's Merge shows
|
|
517
|
+
again, reading "land stopped by orchestrator". Refuses while
|
|
518
|
+
the land fast-forwards main, or a land in a session the board
|
|
519
|
+
didn't launch
|
|
520
|
+
land queue the host's land queue in order: the land running, then each
|
|
521
|
+
waiting, "(bumped)" on one a bump moved
|
|
522
|
+
item bump <wave>/<id> the card's bump, as the owner's bump icon sends it (card-bump;
|
|
523
|
+
item unbump <wave>/<id> card-unbump). A bumped card's land goes ahead in the land queue
|
|
524
|
+
of every land not bumped since, the latest bump first, without
|
|
525
|
+
stopping the one running; a bump of a queued land says so in
|
|
526
|
+
its thread ("land moved first by orchestrator")
|
|
459
527
|
lander <wave>/<id> relaunch the headless lander of a card it stopped on (a usage
|
|
460
|
-
pause ends one):
|
|
461
|
-
server launches a fresh lander; refused unless
|
|
528
|
+
pause ends one): its open record closes as handed back and
|
|
529
|
+
the server launches a fresh lander; refused unless a
|
|
530
|
+
builder record on the card is open
|
|
462
531
|
builder pick <wave>/<id> who builds the card, from the hub's Codex share and the pause
|
|
463
532
|
line, and for claude which model, from the hub's Claude model
|
|
464
533
|
and the card's --size and --model: "codex: <reason>" or
|
|
@@ -466,11 +535,29 @@ command fails and changes nothing.
|
|
|
466
535
|
--kind, and launch that model
|
|
467
536
|
audit [<wave>/<id>] the board's audit log of the last 30 days, data/audit.jsonl:
|
|
468
537
|
each call the code made on its own (builder-pick, pull,
|
|
469
|
-
|
|
538
|
+
round-planner, launch-held, arrival-planner, relaunch,
|
|
470
539
|
lander, triage, pr-review, push, pick-up, auto-merge,
|
|
471
540
|
ready-draft, session-resume, compact) as "<at> <call>
|
|
472
541
|
[<key>]: <result>. <reason>" and the inputs it read; with
|
|
473
542
|
a card, only that card's. The hub's own: kickflip audit
|
|
543
|
+
agent launch <wave>/<id> --name <n> --task <t> any session's helper on an open card, an agent the board server
|
|
544
|
+
[--writes] [--model <opus|sonnet>] launches headless with the task as its brief, under its own
|
|
545
|
+
name (lowercase, "-", not main or visual-\u2026), its record
|
|
546
|
+
<id>@<name> requested until then. A read-only helper launches
|
|
547
|
+
at once and takes no implementer slot; one with --writes
|
|
548
|
+
takes one, waits while Maximum parallel implementers is
|
|
549
|
+
reached, ahead of the pull and the topmost card's first,
|
|
550
|
+
and works in its own worktree, branch <id>-<name> from the
|
|
551
|
+
card's, so it needs the card's worktree. --model else
|
|
552
|
+
builder pick's Claude model. Its lines carry its name
|
|
553
|
+
agent wait <wave>/<id> --name <n> [--agent <me>] blocks until the helper's builder done, then prints its
|
|
554
|
+
[--timeout <minutes>] --text and, for one that writes, its branch. Meanwhile
|
|
555
|
+
--agent's record (main by default) takes no slot. Refuses
|
|
556
|
+
after --timeout (9) minutes, saying whether it runs or waits
|
|
557
|
+
for a slot, and once its process is gone without done
|
|
558
|
+
agent merge <wave>/<id> --name <n> merges a done helper's branch into the card's worktree and
|
|
559
|
+
removes the helper's worktree and branch; on a conflict it
|
|
560
|
+
names the files: resolve, commit and run it again
|
|
474
561
|
builder call <wave>/<id> <judgment call> appends one line to the agent's open builder record: a
|
|
475
562
|
[--agent <name>] judgment call as the builder makes it, or a "Question: \u2026"
|
|
476
563
|
for the session; refused with no open record. A builder start
|
|
@@ -494,12 +581,12 @@ command fails and changes nothing.
|
|
|
494
581
|
the orchestrator"
|
|
495
582
|
builder verified <wave>/<id> [--agent <name>] the session's mark that it checked the record's calls; land
|
|
496
583
|
[--note <t>] refuses while any record on the card has a call after it.
|
|
497
|
-
On a handed-back build (
|
|
584
|
+
On a handed-back build (not done, no builder open, every
|
|
498
585
|
call verified) it finishes the card in the same step, as
|
|
499
586
|
builder done does, --note its closing line (else "Judgment
|
|
500
587
|
calls verified."). While a question on the card is open it
|
|
501
588
|
says so and the card waits; a finish that fails keeps the
|
|
502
|
-
mark, the card
|
|
589
|
+
mark, the card handed back, and says why
|
|
503
590
|
land <wave>/<id> --text <t> [--repo <dir>] brings the worktree its builder record keeps (else
|
|
504
591
|
[--dir <dir>]
|
|
505
592
|
~/.claude/kickflip/worktrees/<id> of --repo) into the board's
|
|
@@ -517,7 +604,7 @@ command fails and changes nothing.
|
|
|
517
604
|
branch. Without --site it never pushes. One land runs at a time on the host: a
|
|
518
605
|
second refuses, naming the card landing. With --site, --text <what was checked>
|
|
519
606
|
is required, and once it lands the card closes as item set
|
|
520
|
-
--
|
|
607
|
+
--done --text does; it refuses first what that close
|
|
521
608
|
would. With --site it is builder done's finish: the push
|
|
522
609
|
after a land, and on a GitHub or plain-remote board the
|
|
523
610
|
branch push (and draft PR) instead of the fast-forward.
|
|
@@ -557,9 +644,9 @@ command fails and changes nothing.
|
|
|
557
644
|
hub a session's prompt, so any other --kind is refused. It records the
|
|
558
645
|
push in data/push-state.json only when kickflip push
|
|
559
646
|
succeeds, and exits with kickflip push's code. Without --now
|
|
560
|
-
it queues until the
|
|
647
|
+
it queues until the hub's
|
|
561
648
|
next check; --now sends at once unless pushes are held
|
|
562
649
|
(overnight, snoozed or the weekend). --urgent: a
|
|
563
650
|
production fault only; it beats those holds. While
|
|
564
651
|
notifications are off every push is dropped, urgent too
|
|
565
|
-
KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class l extends Error{static{r(this,"UsageError")}}const X=new Set(["clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","hub","clear-install","force","worktree","dry-run","dispatch"]);function Y(e){const a=Object.create(null),t=[];for(let s=0;s<e.length;s++){const i=e[s];if(!i.startsWith("--")){t.push(i);continue}const o=i.slice(2);if(X.has(o)){if(Object.hasOwn(a,o))throw new l(`--${o} is given twice`);a[o]=!0}else if(s+1<e.length)(a[o]||=[]).push(e[++s]);else throw new l(`--${o} needs a value`)}return{flags:a,rest:t}}r(Y,"parse");function Z(e,a,t){const s=new Set(a.filter(d=>d.startsWith("...")).map(d=>d.slice(3))),i=new Set(a.map(d=>d.replace(/^\.\.\./,""))),o=Object.keys(t).filter(d=>!i.has(d));if(o.length)throw new l(`${e} does not take ${o.map(d=>"--"+d).join(", ")}`);return Object.fromEntries(Object.entries(t).map(([d,c])=>{if(c===!0||s.has(d))return[d,c];if(c.length>1)throw new l(`--${d} is given twice`);return[d,c[0]]}))}r(Z,"flagsOf");const ee=["html","text","note","notes","summary","title","body","next","progress","install-label","confirmed","asked"],te=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function ae(e){const a=e.trim();if(!a||/\s/.test(a))return!1;if(te.test(a))return!0;try{return g.statSync(a).isFile()}catch{return!1}}r(ae,"looksLikePath");function se(e,a){for(const t of ee){if(typeof a[t]!="string"||!ae(a[t]))continue;const s=e.includes("file")?`; for a file use --file ${a[t].trim()}`:"";throw new l(`--${t} takes text, not a file path (${a[t].trim()})${s}`)}if(typeof a.file=="string"&&a.file!=="-"&&!g.existsSync(a.file))throw new l(`--file ${a.file}: no such file`)}r(se,"checkInputs");const w=r(e=>g.readFileSync(e==="-"?0:e,"utf8"),"readInput");function $(e){if(!e)throw new l("--file <path> is required");try{return JSON.parse(w(e))}catch(a){throw new n.BoardError(`${e}: ${a.message}`)}}r($,"readJSONInput");function R(e){if(e.plan!==void 0&&e["notes-file"]!==void 0)throw new l("pass --notes-file or --plan, not both");return e["notes-file"]??e.plan}r(R,"notesFileOf");function C(e){if(e.note!==void 0&&e.file!==void 0)throw new l("pass --note or --file, not both");return e.file===void 0?e.note:w(e.file)}r(C,"noteOf");const f={pr:[["wave/id"],[],async(e,[a])=>({lines:(await n.prReport(e,a)).map(t=>JSON.stringify(t))})],"pr comment":[["pr"],["body"],(e,[a],t)=>{if(t.body===void 0)throw new l('pr comment needs --body "<what Claude says>"');return n.postPR(e,a,{body:t.body})}],"pr reply":[["pr","comment-id"],["body"],(e,[a,t],s)=>{if(s.body===void 0)throw new l('pr reply needs --body "<what Claude says>"');return n.postPR(e,a,{replyTo:t,body:s.body})}],reply:[["eventId"],["html","file"],(e,[a],t)=>{if(t.html===void 0==(t.file===void 0))throw new l("reply takes --html or --file");return n.writeReply(e,a,t.html??w(t.file))}],thread:[["wave/id"],["text","link","who","via"],(e,[a],t)=>n.appendThread(e,a,t)],"topic add":[["id"],["title","summary"],(e,[a],t)=>n.addTopic(e,{id:a,...t})],"question add":[[],["file"],async(e,a,t)=>{const s=$(t.file);return{...await n.addQuestions(e,s),asked:[s].flat()}}],"question resolve":[["id"],["note"],(e,[a],t)=>n.resolveQuestion(e,a,t.note)],"question move":[["ids"],["to","block"],(e,[a],t)=>n.moveQuestions(e,a.split(",").filter(Boolean),t.to,(t.block||"").split(",").filter(Boolean))],"prototype add":[["topic"],["title","...take","card"],(e,[a],t)=>n.addPrototype(e,{topic:a,title:t.title,card:t.card,takes:t.take})],"prototype list":[[],[],e=>n.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[a])=>n.removePrototype(e,a)],"item set":[["wave/id"],["status","clear-status","next","clear-next","...pr","pushed","gh-since","from-ask","state","held","unheld","pick-up-at","notes-file","plan","deps","parent","related","relate","unrelate","install","install-sha","install-label","clear-install","title","size","model","force","ticket","text","check"],async(e,[a],t,s)=>(t.status==="done"&&e&&await n.checkNotReopened(e,a),t.status==="building"&&e&&S(e)&&await n.ensureWorktree(e,a,i=>O(e,a.split("/")[1],i)),n.setItem(e,a,{status:t.status,clearStatus:t["clear-status"],next:t.next,clearNext:t["clear-next"],pr:t.pr,pushed:t.pushed,ghSince:t["gh-since"],fromAsk:t["from-ask"],state:t.state,held:t.held,unheld:t.unheld,pickUpAt:t["pick-up-at"],notesFile:R(t),deps:t.deps,parent:t.parent,related:t.related,relate:t.relate,unrelate:t.unrelate,install:t.install,installSha:t["install-sha"],installLabel:t["install-label"],clearInstall:t["clear-install"],title:t.title,size:t.size,model:t.model,force:t.force,ticket:t.ticket,text:t.text,check:t.check},s))],"item add":[["wave[/id]"],["title","notes","deps","notes-file","plan","ticket","pick-up-at","parent","size","model","state","relate"],(e,[a],t,s)=>n.addItem(e,a,{title:t.title,notes:t.notes,deps:t.deps,notesFile:R(t),ticket:t.ticket,pickUpAt:t["pick-up-at"],parent:t.parent,size:t.size,model:t.model,state:t.state,relate:t.relate,intake:!0},s)],"item relate-ask":[["eventId","wave/id"],["summary"],(e,[a,t],s)=>n.relateAsk(e,a,t,s.summary)],"ask progress":[["eventId"],["next"],(e,[a],t)=>n.markAsk(e,a,"progress",t.next)],"ask done":[["eventId"],[],(e,[a])=>n.markAsk(e,a,"done")],"ask unfold":[["eventId"],[],(e,[a])=>n.unfoldAsk(e,a)],"ask move":[["eventId"],["to"],(e,[a],t)=>n.moveAsk(e,a,t.to)],"tickets backfill":[[],["dry-run"],(e,a,t,s)=>n.backfillTickets(e,{dryRun:t["dry-run"]},s)],"lineup create":[["slug"],["title","goal","...phase","note","file"],(e,[a],t)=>n.createLineup(e,a,{title:t.title,goal:t.goal,phases:t.phase,note:C(t)})],"lineup add":[["slug","wave/id"],["after"],(e,[a,t],s)=>n.changeLineup(e,a,{add:t,after:s.after})],"lineup remove":[["slug","wave/id"],[],(e,[a,t])=>n.changeLineup(e,a,{remove:t})],"lineup set":[["slug"],["title","goal","note","file","...phase"],(e,[a],t)=>n.changeLineup(e,a,{title:t.title,goal:t.goal,note:C(t),phases:t.phase})],"lineup finish":[["slug"],[],(e,[a])=>n.finishLineup(e,a)],slack:[[],[],()=>({lines:[JSON.stringify(D())]})],"slack set":[[],["self-dm","user","domain","timezone"],async(e,a,t)=>{const s={"self-dm":"self_dm",user:"owner_slack_user",domain:"slack_domain",timezone:"timezone"};try{return{lines:[JSON.stringify(await W(Object.fromEntries(Object.entries(t).map(([i,o])=>[s[i],o]))))]}}catch(i){throw new n.BoardError(i.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await L.rows())]})],"review updated":[["wave/id"],["text"],(e,[a],t)=>n.reviewUpdated(e,a,t.text)],"review sent":[["wave/id","reviewer"],["ts","channel"],(e,[a,t],s)=>n.reviewSent(e,a,t,s)],"review react":[["wave/id","reviewer","emoji"],[],(e,[a,t,s])=>n.reviewReact(e,a,t,s)],"review record":[["pr"],["state","text","file"],(e,[a],t)=>{if(t.text===void 0==(t.file===void 0))throw new l("review record takes --text or --file");return n.recordReview(e,a,{state:t.state,text:t.text??w(t.file)})}],"review post":[["pr"],["file","read-at"],(e,[a],t)=>{if(t.file===void 0)throw new l("review post needs --file <review.md>");if(t["read-at"]===void 0)throw new l("review post needs --read-at <time>, when you read the PR, so a review that landed since is caught");return n.postIncomingReview(e,a,{words:w(t.file),readAt:t["read-at"]})}],"review digest":[[],[],async(e,a,t,s)=>({lines:[JSON.stringify(await n.reviewDigest(e,s))]})],"review due":[[],[],async(e,a,t,s)=>({lines:[JSON.stringify(await n.reviewDue(e,s))]})],"review digest sent":[["reviewer"],["ts","channel","items"],(e,[a],t,s)=>n.reviewDigestSent(e,a,t,s)],"reviewers set":[[],["file"],(e,a,t)=>n.setReviewers(e,$(t.file))],"reviewers add":[[],["file"],(e,a,t)=>n.addReviewers(e,$(t.file))],"reviewers remove":[["...name"],[],(e,a)=>n.removeReviewers(e,a)],"rules list":[[],[],e=>n.listRules(e)],"rules add":[[],["text","hub","by"],(e,a,t)=>n.addRuleTo(e,t)],"rules edit":[["id"],["text","hub"],(e,[a],t)=>n.editRuleIn(e,a,t)],"rules remove":[["id"],["hub"],(e,[a],t)=>n.removeRuleFrom(e,a,t)],settings:[[],[],async e=>({lines:[JSON.stringify(await n.readBoardSettings(e))]})],build:[["wave/id"],["asked"],async(e,[a],t)=>{if(!e)throw new n.BoardError("build needs --site <route>");n.parseKey(a);const s=await n.askedGate(e,a,t.asked);try{const i=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:y.basename(e),key:a}),signal:AbortSignal.timeout(1e4)}),o=await i.json();if(!i.ok)throw new n.BoardError(o.error||"The hub refused the build.");return{lines:["started",...(await n.appendThread(e,a,{text:s})).lines]}}catch(i){throw i instanceof n.BoardError?i:new n.BoardError(`Could not start the build: ${i.message}`)}}],"main set":[[],["install","install-sha"],(e,a,t)=>n.setMainInstall(e,{install:t.install,installSha:t["install-sha"]})],"settings set":[["name","value"],[],(e,[a,t])=>n.setBoardSetting(e,a,t)],"builder start":[["wave/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[a],{worktree:t,repo:s,dispatch:i,...o})=>{if(s!==void 0&&!t)throw new l("--repo goes with --worktree");const d=r(p=>O(e,a.split("/")[1],{repo:s,...p}),"make"),c=e&&o.dir===void 0&&S(e);return n.startBuilder(e,a,{...o,dispatch:i?"session":void 0,worktree:t?d:void 0,ownWorktree:c?d:void 0})}],"builder progress":[["wave/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[a,...t],s)=>{if(t.length&&s.text!==void 0)throw new l("builder progress takes its text once: as words or as --text");if(!t.length&&s.text===void 0)throw new l("builder progress needs its text, as words or as --text");return n.builderProgress(e,a,s.text??t.join(" "),{...s,clearEta:s["clear-eta"]})}],"builder done":[["wave/id"],["dir","agent","next","owner-step","check","text"],async(e,[a],{"owner-step":t,check:s,text:i,...o})=>{if(t!==void 0&&(!e||o.dir))throw new l("--owner-step goes on a board card: pass --site, not --dir");if(s!==void 0&&t===void 0)throw new l("--check goes with --owner-step: it is the code check that closes the card once the step is done");t!==void 0&&await n.setItem(e,a,{ownerStep:t,check:s});const d=e&&!o.dir,c=d&&await n.handBackCalls(e,a,o);if(c)return c;if(d&&o.next===void 0&&await n.finishesCard(e,a,o)){if(i===void 0)throw new n.BoardError(`builder done finishes ${a}: pass --text "<what you checked>", or --next "<what happens next>" to stop without finishing`);return x(e,a,{text:i,ownerStep:t})}if(i!==void 0)throw new l("--text goes on a builder done that finishes the card: the main build, with a worktree and no --next");return n.stopBuilder(e,a,{...o,requireNext:!0})}],"builder pick":[["wave/id"],[],(e,[a])=>n.pickBuilder(e,a)],audit:[["...wave/id?"],[],(e,[a,...t])=>{if(t.length)throw new l("audit takes one <wave/id> at most");return n.auditLog(e,a)}],"builder call":[["wave/id","...text"],["dir","agent"],(e,[a,...t],s)=>n.recordCall(e,a,t.join(" "),s)],"builder calls":[["wave/id"],["dir","agent"],(e,[a],t)=>n.listCalls(e,a,t)],"builder ask":[["wave/id","...text"],["agent"],(e,[a,...t],s)=>n.askOrchestrator(e,a,t.join(" "),s)],"builder answer":[["wave/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text)],"builder defer":[["wave/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text,{deferred:!0})],"builder asks":[["wave/id"],[],(e,[a])=>n.listAsks(e,a)],"builder verified":[["wave/id"],["dir","agent","note"],async(e,[a],t)=>{await n.verifyCalls(e,a,t);const s=e&&!t.dir?await n.verifiedFinish(e,a):null;if(!s)return{};if(s.waits.length)return{lines:[`${await n.cardName(e,a)} finishes once the owner answers ${s.waits.join(", ")}`]};try{return await x(e,a,{text:t.note??"Judgment calls verified."})}catch(i){throw i instanceof n.BoardError?Object.assign(new n.BoardError(`judgment calls verified, but ${await n.cardName(e,a)} didn't finish: ${i.message}`),{exitCode:i.exitCode}):i}}],land:[["wave/id"],["repo","dir","text","owner-step","check"],async(e,[a],t)=>{const s=a.split("/")[1];if(e&&t.text===void 0)throw new l(`land needs --text "<what was checked>", which closes ${a} as done once it lands`);if(!e&&t.text!==void 0)throw new l("--text closes the card, which needs --site");if(!e&&t["owner-step"]!==void 0)throw new l("--owner-step keeps a board card open, which needs --site");if(t.check!==void 0&&t["owner-step"]===void 0)throw new l("--check goes with --owner-step: it is the code check that closes the card once the step is done");if(e&&t.dir===void 0)return x(e,a,{text:t.text,ownerStep:t["owner-step"],check:t.check,repo:t.repo});const i=e?await n.cardName(e,a):void 0,o=await G(i??a,async()=>{e&&await n.checkClose(e,a,t.text,t["owner-step"],t.check),await n.checkCallsVerified(e,a,{dir:t.dir});const d=await n.recordedWorktree(e,a,{dir:t.dir}),{repo:c,base:p}=z(e,{repo:t.repo??d?.repo});return await J({path:d?.path??y.join(U(),s),branch:d?.branch??s,repo:y.resolve(c),base:p,ref:i}),e?n.setItem(e,a,{status:"done",text:t.text,ownerStep:t["owner-step"],check:t.check}):{lines:[]}});return o.joined?{lines:[`joined the land already running for ${i??a}`]}:o}],lander:[["wave/id"],[],async(e,[a])=>{if(!e)throw new l("lander needs --site <route>");return await n.relaunchLander(e,a),{lines:[`${await n.cardName(e,a)} is back at checking; the server launches a lander for it within a minute`]}}],merge:[["wave/id"],["after-approval","confirmed"],async(e,[a],t)=>e&&await n.heldMerge(e,a)?K(e,a):n.mergeCard(e,a,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["wave/id"],[],(e,[a])=>e?F(e,a):Promise.reject(new n.BoardError("arch file needs --site <route>"))],checked:[[],["every","idle"],(e,a,t)=>n.writeChecked(e,t.every,{idle:t.idle})],push:[[],["kind","title","body","url","tag","urgent","now"],(e,a,t)=>{if(t.kind!=="fault"&&t.kind!=="reminder")throw new l(`a session pushes only --kind fault or reminder${t.kind===void 0||t.kind===!0?"":`, not "${t.kind}"`}: the board server pushes Action needed, and news goes on the board`);if(t.title===void 0)throw new l("push needs --title");return n.sendPush(e,t)}]};f.install=f.build;const h=r((e,a)=>n.cardName(e,a),"card"),T=r((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),_=r(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function ne(e){const a=e["pick-up-at"];return[e.status,e["clear-status"]&&"status cleared",e.next!==void 0&&(e.next.trim()?"next line set":"next line cleared"),e["clear-next"]&&"next line cleared",e.pr!==void 0&&`PR #${e.pr}`,e.pushed!==void 0&&`pushed ${e.pushed}`,e["gh-since"]&&`GitHub read to ${e["gh-since"]}`,e["from-ask"]&&`from task ${e["from-ask"]}`,e.state,e.held&&"held",e.unheld&&"resumed",a!==void 0&&(a?`held until ${a}`:"resumed"),R(e)!==void 0&&"notes recorded",e.deps!==void 0&&(e.deps?`blocked by ${e.deps}`:"deps cleared"),e.parent!==void 0&&(e.parent?`parent ${e.parent}`:"parent cleared"),e.related!==void 0&&(e.related?`related to ${e.related.split(",").join(", ")}`:"related cleared"),e.relate!==void 0&&`related to ${e.relate.split(",").join(", ")} added`,e.unrelate!==void 0&&`unrelated from ${e.unrelate.split(",").join(", ")}`,e.install!==void 0&&"install published",e["clear-install"]&&"install cleared",e.title!==void 0&&"renamed",e.ticket!==void 0&&`ticket ${e.ticket}`,e.check!==void 0&&(e.check.trim()?`check ${e.check.trim().split(/\s+/).join(" ")}`:"check removed")].filter(Boolean).join(", ")}r(ne,"itemChanges");const re={reply:r(async(e,[a])=>`reply written to ${await n.eventCard(e,a)}`,"reply"),thread:r(async(e,[a])=>`thread line on ${await h(e,a)}`,"thread"),"topic add":r((e,[a])=>`topic ${a} added`,"topic add"),"question add":r(async(e,a,t,{asked:s,staged:i})=>{const o=[...new Set(s.map(c=>c.topic))],d=o.length!==1?"":o[0].startsWith("item:")?` on ${await h(e,o[0].slice(5))}`:` on topic ${o[0]}`;return`${T(s.map(c=>c.id),"question")} ${i===s.length?"staged":"added"}${d}`},"question add"),"question resolve":r((e,[a])=>`question ${a} resolved`,"question resolve"),"question move":r((e,[a],t)=>`questions ${a} merged onto ${t.to}`,"question move"),"prototype add":r(async(e,[a],t)=>`prototype ${a} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":r((e,[a])=>`prototype ${a} removed`,"prototype rm"),"item set":r(async(e,[a],t)=>`${await h(e,a)}: ${ne(t)}`,"item set"),"item add":r(async(e,a,t,{key:s})=>`${await h(e,s)} added as ${s}`,"item add"),"item relate-ask":r(async(e,[a,t])=>`task ${a} folded into ${await h(e,t)}`,"item relate-ask"),"ask progress":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: in progress`,"ask progress"),"ask done":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: done`,"ask done"),"ask unfold":r((e,[a])=>`task ${a} back on a card of its own`,"ask unfold"),"ask move":r((e,[a],t)=>`task ${a} moved to /${t.to}`,"ask move"),checked:r((e,a,t,{every:s})=>s===null?"GitHub checked; no open PR":`GitHub checked; next in ${s} min`,"checked"),"lineup create":r((e,[a])=>`lineup ${a} created`,"lineup create"),"lineup add":r(async(e,[a,t])=>`${await h(e,t)} added to lineup ${a}`,"lineup add"),"lineup remove":r(async(e,[a,t])=>`${await h(e,t)} taken out of lineup ${a}`,"lineup remove"),"lineup set":r((e,[a])=>`lineup ${a} updated`,"lineup set"),"lineup finish":r((e,[a])=>`lineup ${a} finished`,"lineup finish"),"review sent":r(async(e,[a,t])=>`review link to ${t} recorded on ${await h(e,a)}`,"review sent"),"review react":r(async(e,[a,t,s])=>`${s} on ${t}'s review link recorded on ${await h(e,a)}`,"review react"),"review digest sent":r((e,[a])=>`digest to ${a} recorded`,"review digest sent"),"review record":r((e,[a],t)=>`review of PR #${a} recorded: ${t.state==="needs"?"needs you":"posted"}`,"review record"),"reviewers set":r(()=>"reviewers replaced","reviewers set"),"reviewers add":r(()=>"reviewers added","reviewers add"),"reviewers remove":r((e,a)=>`${T(a,"reviewer")} removed`,"reviewers remove"),"rules add":r((e,a,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} reworded`,"rules edit"),"rules remove":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} removed`,"rules remove"),"builder start":r(async(e,[a],t,s)=>`${await h(e,a)}: ${s.alreadyOpen?"already open: the Implement tap opened its record; launch the agent, whose own builder start takes it up":`${_(t)} started`}`,"builder start"),"builder progress":r(async(e,[a])=>`progress line on ${await h(e,a)}`,"builder progress"),"builder done":r(async(e,[a],t,s)=>`${await h(e,a)}: ${s?.said??`${_(t)} done${t["owner-step"]===void 0?"":`; owner step "${t["owner-step"].trim()}"`}`}`,"builder done"),"builder call":r(async(e,[a])=>`judgment call recorded on ${await h(e,a)}`,"builder call"),"builder ask":r(async(e,[a],t,{id:s})=>`${await h(e,a)}: asked the orchestrator, ${s}`,"builder ask"),"builder answer":r(async(e,[a,t])=>`${await h(e,a)}: ${t} answered`,"builder answer"),"builder defer":r(async(e,[a,t])=>`${await h(e,a)}: ${t} deferred to the owner`,"builder defer"),"builder verified":r(async(e,[a],t,s)=>`${await h(e,a)}: judgment calls verified${s?.said?`; ${s.said}`:""}`,"builder verified"),land:r(async(e,[a],t,s)=>e?s?.said?`${await h(e,a)}: ${s.said}`:t["owner-step"]===void 0?`${await h(e,a)}: landed and done`:`${await h(e,a)}: landed; waits on the owner step "${t["owner-step"].trim()}"`:null,"land"),merge:r(async(e,[a],t,s)=>`${await h(e,a)}: ${s?.said??"merged"}`,"merge"),"arch file":r(async(e,[a])=>`findings of ${await h(e,a)} filed`,"arch file")};async function ie(e,a={}){try{const{flags:t,rest:s}=Y(e),i=[3,2,1].map(u=>s.slice(0,u).join(" ")).find(u=>v(f,u)),o=v(f,i);if(!o)throw new l(s.length?`unknown command "${s.slice(0,2).join(" ")}"`:"no command given");const[d,c,p]=o,m=s.slice(i.split(" ").length),N=d.at(-1)?.startsWith("..."),B=d.at(-1)?.endsWith("?")?d.length-1:d.length;if(N?m.length<B:m.length!==d.length)throw new l(`${i} takes ${d.map(u=>`<${u.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:P,...E}=t;if(P?.length>1)throw new l("--site is given twice");const A=P?.[0],k=Z(i,c,E);se(c,k);const j=A===void 0?null:await n.siteDir(A),b=await p(j,m,k,a),q=v(re,i),I=q&&await q(j,m,k,b);I&&console.log(I);for(const u of typeof b=="object"&&b?.lines||[])console.log(u);return 0}catch(t){if(t instanceof l)return console.error(`${t.message} (node board.mjs --help lists the commands)`),2;if(t instanceof n.BoardError)return console.error(t.message),t.exitCode??1;throw t}}if(r(ie,"main"),process.argv[1]&&import.meta.url===M(g.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(V):process.exitCode=await ie(e)}export{V as USAGE,ie as main};
|
|
652
|
+
KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class d extends Error{static{r(this,"UsageError")}}const oe=new Set(["done","clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","hub","clear-install","force","worktree","dry-run","dispatch","writes"]);function de(e){const a=Object.create(null),t=[];for(let n=0;n<e.length;n++){const i=e[n];if(!i.startsWith("--")){t.push(i);continue}const o=i.slice(2);if(oe.has(o)){if(Object.hasOwn(a,o))throw new d(`--${o} is given twice`);a[o]=!0}else if(n+1<e.length)(a[o]||=[]).push(e[++n]);else throw new d(`--${o} needs a value`)}return{flags:a,rest:t}}r(de,"parse");function le(e,a,t){const n=new Set(a.filter(l=>l.startsWith("...")).map(l=>l.slice(3))),i=new Set(a.map(l=>l.replace(/^\.\.\./,""))),o=Object.keys(t).filter(l=>!i.has(l));if(o.length)throw new d(`${e} does not take ${o.map(l=>"--"+l).join(", ")}`);return Object.fromEntries(Object.entries(t).map(([l,c])=>{if(c===!0||n.has(l))return[l,c];if(c.length>1)throw new d(`--${l} is given twice`);return[l,c[0]]}))}r(le,"flagsOf");const he=["html","text","note","notes","heading","summary","title","body","next","progress","install-label","confirmed","asked","task","reason"],ce=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function ue(e){const a=e.trim();if(!a||/\s/.test(a))return!1;if(ce.test(a))return!0;try{return f.statSync(a).isFile()}catch{return!1}}r(ue,"looksLikePath");function pe(e,a){for(const t of he){if(typeof a[t]!="string"||!ue(a[t]))continue;const n=e.includes("file")?`; for a file use --file ${a[t].trim()}`:"";throw new d(`--${t} takes text, not a file path (${a[t].trim()})${n}`)}if(typeof a.file=="string"&&a.file!=="-"&&!f.existsSync(a.file))throw new d(`--file ${a.file}: no such file`)}r(pe,"checkInputs");const w=r(e=>f.readFileSync(e==="-"?0:e,"utf8"),"readInput");function R(e){if(!e)throw new d("--file <path> is required");try{return JSON.parse(w(e))}catch(a){throw new s.BoardError(`${e}: ${a.message}`)}}r(R,"readJSONInput");function S(e,a){const t=["notes-file","plan"].find(n=>e[n]!==void 0);if(t)throw new d(`--${t} is gone: the board names a card's notes file, <id>-notes.md, and writes it itself; ${a}`)}r(S,"refuseNotesFile");function C(e){if(e.note!==void 0&&e.file!==void 0)throw new d("pass --note or --file, not both");return e.file===void 0?e.note:w(e.file)}r(C,"noteOf");async function we(e,a,{"owner-step":t,check:n,text:i,...o}){if(t!==void 0&&(!e||o.dir))throw new d("--owner-step goes on a board card: pass --site, not --dir");if(n!==void 0&&t===void 0)throw new d("--check goes with --owner-step: it is the code check that closes the card once the step is done");t!==void 0&&await s.setItem(e,a,{ownerStep:t,check:n});const l=e&&!o.dir,c=l&&await s.handBackCalls(e,a,o);if(c)return c;if(l&&o.next===void 0&&await s.finishesCard(e,a,o)){if(i===void 0)throw new s.BoardError(`builder done finishes ${a}: pass --text "<what you checked>", or --next "<what happens next>" to stop without finishing`);return $(e,a,{text:i,ownerStep:t})}if(l&&await s.isHelper(e,a,o.agent)){if(i===void 0)throw new d('a helper hands back its result: pass --text "<what you did or found>"');return s.stopBuilder(e,a,{...o,note:i})}if(i!==void 0)throw new d("--text goes on a builder done that finishes the card, the main build with a worktree and no --next, or on a helper's");return s.stopBuilder(e,a,{...o,requireNext:!0})}r(we,"builderDone");const b={pr:[["wave/id"],[],async(e,[a])=>({lines:(await s.prReport(e,a)).map(t=>JSON.stringify(t))})],"pr comment":[["pr"],["body"],(e,[a],t)=>{if(t.body===void 0)throw new d('pr comment needs --body "<what Claude says>"');return s.postPR(e,a,{body:t.body})}],"pr reply":[["pr","comment-id"],["body"],(e,[a,t],n)=>{if(n.body===void 0)throw new d('pr reply needs --body "<what Claude says>"');return s.postPR(e,a,{replyTo:t,body:n.body})}],reply:[["eventId"],["html","file"],(e,[a],t)=>{if(t.html===void 0==(t.file===void 0))throw new d("reply takes --html or --file");return s.writeReply(e,a,t.html??w(t.file))}],thread:[["wave/id"],["text","link","who","via"],(e,[a],t)=>s.appendThread(e,a,t)],"visual add":[["wave/id"],["file","title"],(e,[a],t)=>s.addVisual(e,a,t)],"topic add":[["id"],["title","summary"],(e,[a],t)=>s.addTopic(e,{id:a,...t})],"question add":[[],["file"],async(e,a,t)=>{const n=R(t.file);return{...await s.addQuestions(e,n,process.env.KICKFLIP_RENDER_CHECK==="off"?void 0:ne),asked:[n].flat()}}],"question resolve":[["id"],["note"],(e,[a],t)=>s.resolveQuestion(e,a,t.note)],"question move":[["ids"],["to","block"],(e,[a],t)=>s.moveQuestions(e,a.split(",").filter(Boolean),t.to,(t.block||"").split(",").filter(Boolean))],"prototype add":[["topic"],["title","...take","card","served","...state"],(e,[a],t)=>s.addPrototype(e,{topic:a,title:t.title,card:t.card,takes:t.take,served:t.served,states:t.state})],"prototype base":[[],[],e=>re(e)],"prototype list":[[],[],e=>s.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[a])=>s.removePrototype(e,a)],"item set":[["wave/id"],["done","status","clear-status","next","clear-next","...pr","pushed","gh-since","from-ask","state","held","unheld","pick-up-at","notes-file","plan","deps","blocked-until","block-reason","parent","related","relate","unrelate","install","install-sha","install-label","clear-install","title","size","model","force","ticket","text","check"],async(e,[a],t,n)=>(S(t,`add to its notes with item notes ${a} --heading "<heading>" --file <draft.md>`),t.done&&e&&await s.checkNotReopened(e,a),s.setItem(e,a,{done:t.done,status:t.status,clearStatus:t["clear-status"],next:t.next,clearNext:t["clear-next"],pr:t.pr,pushed:t.pushed,ghSince:t["gh-since"],fromAsk:t["from-ask"],state:t.state,held:t.held,unheld:t.unheld,pickUpAt:t["pick-up-at"],deps:t.deps,blockedUntil:t["blocked-until"],blockReason:t["block-reason"],parent:t.parent,related:t.related,relate:t.relate,unrelate:t.unrelate,install:t.install,installSha:t["install-sha"],installLabel:t["install-label"],clearInstall:t["clear-install"],title:t.title,size:t.size,model:t.model,force:t.force,ticket:t.ticket,text:t.text,check:t.check,stopRun:r(()=>te(e,a),"stopRun")},n))],"item add":[["wave[/id]"],["title","notes","notes-from","deps","notes-file","plan","ticket","pick-up-at","parent","size","model","state","relate"],(e,[a],t,n)=>{if(S(t,"seed a new card's notes with item add --notes-from <draft.md>"),t.notes!==void 0&&t["notes-from"]!==void 0)throw new d("pass --notes or --notes-from, not both");return s.addItem(e,a,{title:t.title,notes:t.notes,notesFrom:t["notes-from"],deps:t.deps,ticket:t.ticket,pickUpAt:t["pick-up-at"],parent:t.parent,size:t.size,model:t.model,state:t.state,relate:t.relate,intake:!0},n)}],"item notes":[["wave/id"],["heading","file","from"],(e,[a],t)=>{if(t.heading===void 0&&t.from===void 0)throw new d(`item notes needs --heading "<the section's title>"`);if(t.file===void 0==(t.from===void 0))throw new d("item notes takes --file <draft.md> or --from <wave>/<id>");return s.addNotes(e,a,t)}],"item relate-ask":[["eventId","wave/id"],["summary"],(e,[a,t],n)=>s.relateAsk(e,a,t,n.summary)],"ask progress":[["eventId"],["next"],(e,[a],t)=>s.markAsk(e,a,"progress",t.next)],"ask done":[["eventId"],[],(e,[a])=>s.markAsk(e,a,"done")],"intake handoff":[["eventId"],["text"],(e,[a],t)=>s.intakeHandoff(e,a,t.text)],"ask unfold":[["eventId"],[],(e,[a])=>s.unfoldAsk(e,a)],"ask move":[["eventId"],["to"],(e,[a],t)=>s.moveAsk(e,a,t.to)],"tickets backfill":[[],["dry-run"],(e,a,t,n)=>s.backfillTickets(e,{dryRun:t["dry-run"]},n)],"lineup create":[["slug"],["title","goal","...phase","note","file"],(e,[a],t)=>s.createLineup(e,a,{title:t.title,goal:t.goal,phases:t.phase,note:C(t)})],"lineup add":[["slug","wave/id"],["after"],(e,[a,t],n)=>s.changeLineup(e,a,{add:t,after:n.after})],"lineup remove":[["slug","wave/id"],[],(e,[a,t])=>s.changeLineup(e,a,{remove:t})],"lineup set":[["slug"],["title","goal","note","file","...phase"],(e,[a],t)=>s.changeLineup(e,a,{title:t.title,goal:t.goal,note:C(t),phases:t.phase})],"lineup finish":[["slug"],[],(e,[a])=>s.finishLineup(e,a)],slack:[[],[],()=>({lines:[JSON.stringify(D())]})],"slack set":[[],["self-dm","user","domain","timezone"],async(e,a,t)=>{const n={"self-dm":"self_dm",user:"owner_slack_user",domain:"slack_domain",timezone:"timezone"};try{return{lines:[JSON.stringify(await M(Object.fromEntries(Object.entries(t).map(([i,o])=>[n[i],o]))))]}}catch(i){throw new s.BoardError(i.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await H.rows())]})],"review updated":[["wave/id"],["text"],(e,[a],t)=>s.reviewUpdated(e,a,t.text)],"review record":[["pr"],["state","text","file"],(e,[a],t)=>{if(t.text===void 0==(t.file===void 0))throw new d("review record takes --text or --file");return s.recordReview(e,a,{state:t.state,text:t.text??w(t.file)})}],"review post":[["pr"],["file","read-at"],(e,[a],t)=>{if(t.file===void 0)throw new d("review post needs --file <review.md>");if(t["read-at"]===void 0)throw new d("review post needs --read-at <time>, when you read the PR, so a review that landed since is caught");return s.postIncomingReview(e,a,{words:w(t.file),readAt:t["read-at"]})}],"review digest":[[],[],async(e,a,t,n)=>({lines:[JSON.stringify(await s.reviewDigest(e,n))]})],"review due":[[],[],async(e,a,t,n)=>({lines:[JSON.stringify(await s.reviewDue(e,n))]})],"reviewers set":[[],["file"],(e,a,t)=>s.setReviewers(e,R(t.file))],"reviewers add":[[],["file"],(e,a,t)=>s.addReviewers(e,R(t.file))],"reviewers remove":[["...name"],[],(e,a)=>s.removeReviewers(e,a)],"rules list":[[],[],e=>s.listRules(e)],"rules add":[[],["text","hub","by"],(e,a,t)=>s.addRuleTo(e,t)],"rules edit":[["id"],["text","hub"],(e,[a],t)=>s.editRuleIn(e,a,t)],"rules remove":[["id"],["hub"],(e,[a],t)=>s.removeRuleFrom(e,a,t)],settings:[[],[],async e=>({lines:[JSON.stringify(await s.readBoardSettings(e))]})],build:[["wave/id"],["asked"],async(e,[a],t)=>{if(!e)throw new s.BoardError("build needs --site <route>");s.parseKey(a);const n=await s.askedGate(e,a,t.asked);try{const i=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:x.basename(e),key:a}),signal:AbortSignal.timeout(1e4)}),o=await i.json();if(!i.ok)throw new s.BoardError(o.error||"The hub refused the build.");return{lines:["started",...(await s.appendThread(e,a,{text:n})).lines]}}catch(i){throw i instanceof s.BoardError?i:new s.BoardError(`Could not start the build: ${i.message}`)}}],"main set":[[],["install","install-sha"],(e,a,t)=>s.setMainInstall(e,{install:t.install,installSha:t["install-sha"]})],"settings set":[["name","value"],[],(e,[a,t])=>s.setBoardSetting(e,a,t)],"builder start":[["wave/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[a],{worktree:t,repo:n,dispatch:i,...o})=>{if(n!==void 0&&!t)throw new d("--repo goes with --worktree");const l=r(p=>z(e,a.split("/")[1],{repo:n,...p}),"make"),c=e&&o.dir===void 0&&se(e);return s.startBuilder(e,a,{...o,dispatch:i?"session":void 0,worktree:t?l:void 0,ownWorktree:c?l:void 0})}],"builder progress":[["wave/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[a,...t],n)=>{if(t.length&&n.text!==void 0)throw new d("builder progress takes its text once: as words or as --text");if(!t.length&&n.text===void 0)throw new d("builder progress needs its text, as words or as --text");return s.builderProgress(e,a,n.text??t.join(" "),{...n,clearEta:n["clear-eta"]})}],"builder done":[["wave/id"],["dir","agent","next","owner-step","check","text","updated"],async(e,[a],{updated:t,...n})=>{const i=e&&!n.dir;if(i&&t===void 0&&await s.cardLinked(e,a))throw new s.BoardError(`a reviewer has ${a}'s PR link: pass --updated "<what changed>" for its thread, or --updated none for a change only to comments, names, docs or tests`);const o=await we(e,a,n);return i&&t!==void 0&&t.trim().toLowerCase()!=="none"&&await s.reviewUpdated(e,a,t),o}],"builder pick":[["wave/id"],[],(e,[a])=>s.pickBuilder(e,a)],"agent launch":[["wave/id"],["name","task","writes","model"],(e,[a],t)=>s.requestHelper(e,a,t)],"agent wait":[["wave/id"],["name","agent","timeout"],(e,[a],t)=>s.waitHelper(e,a,{...t,alive:Q})],"agent merge":[["wave/id"],["name"],async(e,[a],t)=>{const{record:n,card:i}=await s.helperOf(e,a,t.name);if(!n.done)throw new s.BoardError(`helper ${t.name} is still open; run agent wait ${a} --name ${t.name} first`);if(!n.writes||!n.worktree)throw new s.BoardError(`helper ${t.name} writes no code, so there is nothing to merge`);if(!i)throw new s.BoardError(`${a} has no worktree to merge helper ${t.name} into`);return G(i,n.worktree)}],audit:[["...wave/id?"],[],(e,[a,...t])=>{if(t.length)throw new d("audit takes one <wave/id> at most");return s.auditLog(e,a)}],"builder call":[["wave/id","...text"],["dir","agent"],(e,[a,...t],n)=>s.recordCall(e,a,t.join(" "),n)],"builder calls":[["wave/id"],["dir","agent"],(e,[a],t)=>s.listCalls(e,a,t)],"builder ask":[["wave/id","...text"],["agent"],(e,[a,...t],n)=>s.askOrchestrator(e,a,t.join(" "),n)],"builder answer":[["wave/id","ask"],["text"],(e,[a,t],n)=>s.answerBuilder(e,a,t,n.text)],"builder defer":[["wave/id","ask"],["text"],(e,[a,t],n)=>s.answerBuilder(e,a,t,n.text,{deferred:!0})],"builder asks":[["wave/id"],[],(e,[a])=>s.listAsks(e,a)],"builder verified":[["wave/id"],["dir","agent","note"],async(e,[a],t)=>{await s.verifyCalls(e,a,t);const n=e&&!t.dir?await s.verifiedFinish(e,a):null;if(!n)return{};if(n.waits.length)return{lines:[`${await s.cardName(e,a)} finishes once the owner answers ${n.waits.join(", ")}`]};try{return await $(e,a,{text:t.note??"Judgment calls verified."})}catch(i){throw i instanceof s.BoardError?Object.assign(new s.BoardError(`judgment calls verified, but ${await s.cardName(e,a)} didn't finish: ${i.message}`),{exitCode:i.exitCode}):i}}],land:[["wave/id"],["repo","dir","text","owner-step","check"],async(e,[a],t)=>{const n=a.split("/")[1];if(e&&t.text===void 0)throw new d(`land needs --text "<what was checked>", which closes ${a} as done once it lands`);if(!e&&t.text!==void 0)throw new d("--text closes the card, which needs --site");if(!e&&t["owner-step"]!==void 0)throw new d("--owner-step keeps a board card open, which needs --site");if(t.check!==void 0&&t["owner-step"]===void 0)throw new d("--check goes with --owner-step: it is the code check that closes the card once the step is done");if(e&&t.dir===void 0)return $(e,a,{text:t.text,ownerStep:t["owner-step"],check:t.check,repo:t.repo});const i=e?await s.cardName(e,a):void 0,o=await F(i??a,async()=>{e&&await s.checkClose(e,a,t.text,t["owner-step"],t.check),await s.checkCallsVerified(e,a,{dir:t.dir});const l=await s.recordedWorktree(e,a,{dir:t.dir}),{repo:c,base:p}=U(e,{repo:t.repo??l?.repo});return await J({path:l?.path??x.join(K(),n),branch:l?.branch??n,repo:x.resolve(c),base:p,ref:i}),e?s.setItem(e,a,{done:!0,text:t.text,ownerStep:t["owner-step"],check:t.check}):{lines:[]}});return o.joined?{lines:[`joined the land already running for ${i??a}`]}:o}],"builder stop":[["wave/id"],[],(e,[a])=>ee(m(e,"builder stop"),a)],"item stall":[["wave/id"],["reason"],(e,[a],t)=>Z(m(e,"item stall"),a,t.reason)],"land stop":[["wave/id"],[],(e,[a])=>ae(m(e,"land stop"),a)],"land queue":[[],[],()=>Y()],"item bump":[["wave/id"],[],(e,[a])=>O(m(e,"item bump"),a,!0)],"item unbump":[["wave/id"],[],(e,[a])=>O(m(e,"item unbump"),a,!1)],lander:[["wave/id"],[],async(e,[a])=>{if(!e)throw new d("lander needs --site <route>");return await s.relaunchLander(e,a),{lines:[`${await s.cardName(e,a)} is handed back again; the server launches a lander for it within a minute`]}}],merge:[["wave/id"],["after-approval","confirmed"],async(e,[a],t)=>e&&await s.heldMerge(e,a)?X(e,a):s.mergeCard(e,a,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["wave/id"],[],(e,[a])=>e?V(e,a):Promise.reject(new s.BoardError("arch file needs --site <route>"))],push:[[],["kind","title","body","url","tag","urgent","now"],(e,a,t)=>{if(t.kind!=="fault"&&t.kind!=="reminder")throw new d(`a session pushes only --kind fault or reminder${t.kind===void 0||t.kind===!0?"":`, not "${t.kind}"`}: the board server pushes Action needed, and news goes on the board`);if(t.title===void 0)throw new d("push needs --title");return s.sendPush(e,t)}]};b.install=b.build;function m(e,a){if(!e)throw new d(`${a} needs --site <route>`);return e}r(m,"needSite");const h=r((e,a)=>s.cardName(e,a),"card"),_=r((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),T=r(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function me(e){const a=e["pick-up-at"];return[e.done&&"done",e.next!==void 0&&(e.next.trim()?"next line set":"next line cleared"),e["clear-next"]&&"next line cleared",e.pr!==void 0&&`PR #${e.pr}`,e.pushed!==void 0&&`pushed ${e.pushed}`,e["gh-since"]&&`GitHub read to ${e["gh-since"]}`,e["from-ask"]&&`from task ${e["from-ask"]}`,e.state,e.held&&"held",e.unheld&&"resumed",a!==void 0&&(a?`held until ${a}`:"resumed"),e.deps!==void 0&&(e.deps?`blocked by ${e.deps}`:"deps cleared"),e.parent!==void 0&&(e.parent?`parent ${e.parent}`:"parent cleared"),e.related!==void 0&&(e.related?`related to ${e.related.split(",").join(", ")}`:"related cleared"),e.relate!==void 0&&`related to ${e.relate.split(",").join(", ")} added`,e.unrelate!==void 0&&`unrelated from ${e.unrelate.split(",").join(", ")}`,e.install!==void 0&&"install published",e["clear-install"]&&"install cleared",e.title!==void 0&&"renamed",e.ticket!==void 0&&`ticket ${e.ticket}`,e.check!==void 0&&(e.check.trim()?`check ${e.check.trim().split(/\s+/).join(" ")}`:"check removed")].filter(Boolean).join(", ")}r(me,"itemChanges");const ge={reply:r(async(e,[a])=>`reply written to ${await s.eventCard(e,a)}`,"reply"),thread:r(async(e,[a])=>`thread line on ${await h(e,a)}`,"thread"),"topic add":r((e,[a])=>`topic ${a} added`,"topic add"),"question add":r(async(e,a,t,{asked:n,staged:i})=>{const o=[...new Set(n.map(c=>c.topic))],l=o.length!==1?"":o[0].startsWith("item:")?` on ${await h(e,o[0].slice(5))}`:` on topic ${o[0]}`;return`${_(n.map(c=>c.id),"question")} ${i===n.length?"staged":"added"}${l}`},"question add"),"question resolve":r((e,[a])=>`question ${a} resolved`,"question resolve"),"question move":r((e,[a],t)=>`questions ${a} merged onto ${t.to}`,"question move"),"prototype add":r(async(e,[a],t)=>`prototype ${a} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":r((e,[a])=>`prototype ${a} removed`,"prototype rm"),"item set":r(async(e,[a],t)=>`${await h(e,a)}: ${me(t)}`,"item set"),"item add":r(async(e,a,t,{key:n})=>`${await h(e,n)} added as ${n}`,"item add"),"item notes":r(async(e,[a],t,{file:n,heading:i})=>`${await h(e,a)}: notes section "${i}" added to ${n}`,"item notes"),"item relate-ask":r(async(e,[a,t])=>`task ${a} folded into ${await h(e,t)}`,"item relate-ask"),"ask progress":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: in progress`,"ask progress"),"ask done":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: done`,"ask done"),"intake handoff":r(async(e,[a])=>`${await s.eventCard(e,a)} handed to the orchestrator`,"intake handoff"),"ask unfold":r((e,[a])=>`task ${a} back on a card of its own`,"ask unfold"),"ask move":r((e,[a],t)=>`task ${a} moved to /${t.to}`,"ask move"),"lineup create":r((e,[a])=>`lineup ${a} created`,"lineup create"),"lineup add":r(async(e,[a,t])=>`${await h(e,t)} added to lineup ${a}`,"lineup add"),"lineup remove":r(async(e,[a,t])=>`${await h(e,t)} taken out of lineup ${a}`,"lineup remove"),"lineup set":r((e,[a])=>`lineup ${a} updated`,"lineup set"),"lineup finish":r((e,[a])=>`lineup ${a} finished`,"lineup finish"),"review record":r((e,[a],t)=>`review of PR #${a} recorded: ${t.state==="needs"?"needs you":"posted"}`,"review record"),"reviewers set":r(()=>"reviewers replaced","reviewers set"),"reviewers add":r(()=>"reviewers added","reviewers add"),"reviewers remove":r((e,a)=>`${_(a,"reviewer")} removed`,"reviewers remove"),"rules add":r((e,a,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} reworded`,"rules edit"),"rules remove":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} removed`,"rules remove"),"builder start":r(async(e,[a],t,n)=>`${await h(e,a)}: ${n.alreadyOpen?"already open: the Implement tap opened its record; launch the agent, whose own builder start takes it up":`${T(t)} started`}`,"builder start"),"builder progress":r(async(e,[a])=>`progress line on ${await h(e,a)}`,"builder progress"),"builder done":r(async(e,[a],t,n)=>`${await h(e,a)}: ${n?.said??`${T(t)} done${t["owner-step"]===void 0?"":`; owner step "${t["owner-step"].trim()}"`}`}`,"builder done"),"builder call":r(async(e,[a])=>`judgment call recorded on ${await h(e,a)}`,"builder call"),"builder ask":r(async(e,[a],t,{id:n})=>`${await h(e,a)}: asked the orchestrator, ${n}`,"builder ask"),"builder answer":r(async(e,[a,t])=>`${await h(e,a)}: ${t} answered`,"builder answer"),"builder defer":r(async(e,[a,t])=>`${await h(e,a)}: ${t} deferred to the owner`,"builder defer"),"builder verified":r(async(e,[a],t,n)=>`${await h(e,a)}: judgment calls verified${n?.said?`; ${n.said}`:""}`,"builder verified"),land:r(async(e,[a],t,n)=>e?n?.said?`${await h(e,a)}: ${n.said}`:t["owner-step"]===void 0?`${await h(e,a)}: landed and done`:`${await h(e,a)}: landed; waits on the owner step "${t["owner-step"].trim()}"`:null,"land"),merge:r(async(e,[a],t,n)=>`${await h(e,a)}: ${n?.said??"merged"}`,"merge"),"arch file":r(async(e,[a])=>`findings of ${await h(e,a)} filed`,"arch file")};async function fe(e,a={}){try{const{flags:t,rest:n}=de(e),i=[3,2,1].map(u=>n.slice(0,u).join(" ")).find(u=>y(b,u)),o=y(b,i);if(!o)throw new d(n.length?`unknown command "${n.slice(0,2).join(" ")}"`:"no command given");const[l,c,p]=o,g=n.slice(i.split(" ").length),N=l.at(-1)?.startsWith("..."),B=l.at(-1)?.endsWith("?")?l.length-1:l.length;if(N?g.length<B:g.length!==l.length)throw new d(`${i} takes ${l.map(u=>`<${u.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:I,...E}=t;if(I?.length>1)throw new d("--site is given twice");const q=I?.[0],k=le(i,c,E);pe(c,k);const A=q===void 0?null:await s.siteDir(q),v=await p(A,g,k,a),P=y(ge,i),j=P&&await P(A,g,k,v);j&&console.log(j);for(const u of typeof v=="object"&&v?.lines||[])console.log(u);return 0}catch(t){if(t instanceof d)return console.error(`${t.message} (node board.mjs --help lists the commands)`),2;if(t instanceof s.BoardError)return console.error(t.message),t.exitCode??1;throw t}}if(r(fe,"main"),process.argv[1]&&import.meta.url===W(f.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(ie):process.exitCode=await fe(e)}export{ie as USAGE,fe as main};
|