@lovek4d/kickflip 0.7.0 → 0.7.2
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 +53 -49
- package/SKILL.md +17 -13
- package/bin/event-lines.mjs +1 -1
- package/bin/kickflip.mjs +3 -3
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +113 -105
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -0
- package/board/onboarding.mjs +3 -3
- package/board/public/app-arch.js +1 -1
- package/board/public/app-cards.js +6 -7
- package/board/public/app-header.js +1 -1
- package/board/public/app-lanes.js +39 -62
- package/board/public/app-needs.js +16 -11
- package/board/public/app-reader.js +9 -5
- package/board/public/app.js +3 -3
- package/board/public/attach.js +18 -2
- package/board/public/dialogs.js +11 -13
- package/board/public/hub-kit.js +1 -1
- package/board/public/hub.css +0 -1
- package/board/public/linear-key.js +2 -2
- package/board/public/new-task.js +1 -1
- package/board/public/prototype-kit.js +2 -5
- package/board/public/router.js +1 -1
- package/board/public/sidebar.css +3 -3
- package/board/public/sidebar.js +2 -2
- package/board/public/style.css +43 -43
- package/board/public/ui-cards.js +25 -20
- package/board/public/ui-hub.js +1 -5
- package/board/public/ui-links.js +5 -5
- package/board/public/ui-order.js +6 -7
- package/board/public/ui-questions.js +17 -13
- package/board/public/ui-sidebar.js +1 -1
- package/board/public/ui-threads.js +29 -21
- package/board/public/ui.js +23 -6
- package/board/public/view-hub-settings.js +11 -19
- package/board/public/view-hub.js +25 -11
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +18 -5
- package/board/public/view-settings.js +7 -7
- package/board/render-check.mjs +10 -10
- package/board/review.mjs +1 -1
- package/board/server.mjs +18 -19
- package/board/stop.mjs +1 -1
- package/board/store.mjs +26 -26
- package/board/tickets.mjs +3 -3
- package/hooks-plugin/hooks/board-data-gate.mjs +6 -6
- package/hub/awake.mjs +1 -1
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/linear.mjs +1 -1
- package/hub/locks.mjs +1 -1
- package/hub/new-board.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/readiness.mjs +1 -1
- package/hub/review-send.mjs +1 -1
- package/hub/slack.mjs +1 -1
- package/hub/try.mjs +6 -7
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/usage-gate.mjs +1 -1
- package/starter/server.mjs +3 -3
package/board/bin/board.mjs
CHANGED
|
@@ -1,49 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var D=Object.defineProperty;var i=(e,a)=>D(e,"name",{value:a,configurable:!0});import{readiness as W}from"../../hub/readiness.mjs";import{readSlack as H,setSlack as M}from"../../hub/slack.mjs";import{routeRecord as y}from"../../hub/routes.mjs";import g from"node:fs";import x from"node:path";import{pathToFileURL as L}from"node:url";import*as n from"../store.mjs";import{boardWorktree as z,builderRepo as U,joiningLand as J,landInTurn as F,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 S,queueLines as Z,stallCard as Y,stopBuilder as ee,stopLand as te}from"../stop.mjs";import{boardAt as ae}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";import{checkTakesRender as se}from"../render-check.mjs";import{prototypeBase as ne}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
|
-
board.json says where the board's
|
|
5
|
+
board.json says where the board's item files live. Without --site, they come from ~/.claude/waves.
|
|
6
6
|
Paths resolve under $HOME, so HOME=<dir> points every command at a copy.
|
|
7
7
|
A --file argument of - reads stdin.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
Every new card is a task, keyed T-<n>, with no ticket, until its grill's ticket question (answered yes, the board makes
|
|
9
|
+
it), item add or item set --new-ticket (the orchestrator, for a Quick task), or --ticket <id> gives it one. A ticket comes
|
|
10
|
+
from the board's source, GitHub or Linear, made from the card's title and notes, Linear in the board's team and project
|
|
11
|
+
through Hub's Linear key; a linked one must exist there. A board whose source is none has no tickets.
|
|
12
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
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
|
-
moves every Linear board to
|
|
17
|
-
command fails and changes nothing.
|
|
16
|
+
moves every Linear board to no ticket source. If the source can't make a ticket, the command fails and changes nothing.
|
|
18
17
|
|
|
19
|
-
reply <eventId> (--
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
reply <eventId> (--text <md> | --file <path>) data/replies/<eventId>.md, markdown the board draws. A
|
|
19
|
+
reply to an ask resolves its answered questions
|
|
20
|
+
(topic item:ask:<eventId>), and one to wave-triage its
|
|
21
|
+
wave's, noting "Acted on: replied". A
|
|
22
22
|
reply to overnight-triage ends that triage
|
|
23
23
|
[--title <t>] retitles the ask it answers, or whose thread the message
|
|
24
24
|
is in, which then wears an Ask badge in place of its first
|
|
25
25
|
line as title (data/ask-titles.json)
|
|
26
|
-
thread <
|
|
27
|
-
[--who claude|love|<name>] [--via github|slack] of <
|
|
26
|
+
thread <file>/<id> --text <t> [--link <url>] one line in the item's thread; ask:<eventId> in place
|
|
27
|
+
[--who claude|love|<name>] [--via github|slack] of <file>/<id> writes in that ask's thread
|
|
28
28
|
A Claude thread line, or a reply to an ask or item-message, rewrites the body of the thread's
|
|
29
|
-
queued push (tag ask-<eventId> or item-<
|
|
29
|
+
queued push (tag ask-<eventId> or item-<file>/<id>) to the message's first line, keeping
|
|
30
30
|
"\xB7 and N replies", and prints the new body. It never pushes: the thread's unread mark carries it
|
|
31
|
-
visual add <
|
|
31
|
+
visual add <file>/<id> --file <page.html> --title <t> a visual agent's page: copied to public/visuals/<id>/<slug>.html
|
|
32
32
|
(a new name each time), linked by a visual line in the
|
|
33
33
|
item's thread; prints the page's link
|
|
34
34
|
topic add <id> --title <t> [--summary <s>] a question topic in data/owner.json
|
|
35
35
|
question add --file <json> one question or an array of them:
|
|
36
36
|
{id, topic, question, options: [{id, label, recommended?, detail?, diagram?, practice?}],
|
|
37
|
-
source?, context?, url?, urgent?, round_of?, reask_of?, ask?, suggested?, prototype_gate?}; topic is declared, item:<
|
|
38
|
-
for a New card not sorted yet,
|
|
37
|
+
source?, context?, url?, urgent?, round_of?, reask_of?, ask?, suggested?, prototype_gate?}; topic is declared, item:<file>/<id>, item:ask:<eventId>
|
|
38
|
+
for a New card not sorted yet, wave:<slug> for a wave's triage, or file:<slug> for an item file's triage on
|
|
39
39
|
a board over ~/.claude/waves. A question on a done card reopens it.
|
|
40
40
|
A question on an item: topic whose card has a grill or prototype planner running is staged, not published,
|
|
41
41
|
whoever adds it: it shows nowhere until the planner finishes, fails or is stopped, when every staged question
|
|
42
42
|
on the card publishes in the order added, with one owner push. The command says "staged" and why.
|
|
43
43
|
url is http(s), or a site page such as prototype-<topic>.html?q=<id>, stored as given.
|
|
44
44
|
Prototype options keep take order, ids matching take keys; recommended: true tags the recommendation.
|
|
45
|
-
suggested: {option, card, answer} pre-checks the option the owner already chose on an earlier card, with a
|
|
46
|
-
|
|
45
|
+
suggested: {option, card, answer, reason?} pre-checks the option the owner already chose on an earlier card, with a blue
|
|
46
|
+
line above its description, Orchestrator choice: <reason>; give a reason, your own words for why. Without one the line
|
|
47
|
+
reads Orchestrator choice: you answered this on <card>: "<answer>"; card is that card's ref, answer their words.
|
|
47
48
|
Waiting on you shows fitting takes in a grid, or a chooser above one full-width take for a single take or if any
|
|
48
49
|
scrolls or fills the screen.
|
|
49
50
|
diagram, with practice: rows of 1-4 boxes [[{text, tone?, to?}]], tone quiet|info|notice|
|
|
@@ -65,7 +66,7 @@ command fails and changes nothing.
|
|
|
65
66
|
(reask-request): it resolves that one again, noting "Asked again as <new id>", printed as "resolved: <old id>".
|
|
66
67
|
ask names a builder ask the session deferred on the item (builder defer): the question puts it to the
|
|
67
68
|
owner, and while a card's only open questions are such, it waits on them with no Action needed.
|
|
68
|
-
The board server pushes the new open questions itself, under the card's tag (item-<
|
|
69
|
+
The board server pushes the new open questions itself, under the card's tag (item-<file>/<id>, or
|
|
69
70
|
ask-<eventId>) or topic-<topic> off the cards
|
|
70
71
|
question resolve <id> --note <text> sets resolved and resolved_at; refuses a question on a
|
|
71
72
|
prototype page until an answer settles its round: a pick, with a note or none, or an answer to
|
|
@@ -75,14 +76,14 @@ command fails and changes nothing.
|
|
|
75
76
|
(a screen wait), which the board already answers: it is published resolved "Settled by the
|
|
76
77
|
orchestrator: <text>", and the answer goes into the card's notes. Refuses the prototype question,
|
|
77
78
|
which is the owner's
|
|
78
|
-
question publish <
|
|
79
|
+
question publish <file>/<id> passes the rest of the card's screened questions on to the owner.
|
|
79
80
|
The board server publishes any left unscreened 15 minutes on, with a thread line
|
|
80
|
-
question move <id>,<id>,\u2026 --to <
|
|
81
|
+
question move <id>,<id>,\u2026 --to <file>/<id> [--block <file>/<id>,\u2026] merges open questions that ask one thing onto
|
|
81
82
|
one card: the first id moves to that card (topic item:<card>), the others resolve "Duplicate of
|
|
82
83
|
<card>'s question <first id>". Each --block card, in order, is blocked by the one before it (the
|
|
83
84
|
first by --to) and related to --to
|
|
84
85
|
prototype add <topic> --title <t> --take <K>:<label> records a prototype in data/prototypes.json: the board
|
|
85
|
-
[--take <K>:<label> \u2026] [--card <
|
|
86
|
+
[--take <K>:<label> \u2026] [--card <file>/<id>] serves prototype-<topic>.html from its one template, with
|
|
86
87
|
the title, a back arrow to --card (else the board), the
|
|
87
88
|
take pill and a stage. topic: a-z, 0-9 and "-"; K: one
|
|
88
89
|
capital letter, in take order; a label never starts with
|
|
@@ -105,7 +106,7 @@ command fails and changes nothing.
|
|
|
105
106
|
prototype list one line per recorded prototype
|
|
106
107
|
prototype rm <topic> drops the record, keeping its take files; refuses while an
|
|
107
108
|
open question links it
|
|
108
|
-
pr <
|
|
109
|
+
pr <file>/<id> one JSON line per PR on the card, in its order: the PR's
|
|
109
110
|
repo, state, checks and new events, using the board's
|
|
110
111
|
github-login and the card's gh_since
|
|
111
112
|
pr comment <pr> --body <text> posts Claude's words on PR <pr> through gh, a number in the
|
|
@@ -115,7 +116,7 @@ command fails and changes nothing.
|
|
|
115
116
|
blank line and <!-- claude-bridge --> after, the marker pr
|
|
116
117
|
reads as Claude's own. Refuses words that already carry
|
|
117
118
|
either. Prints the post's url
|
|
118
|
-
item set <
|
|
119
|
+
item set <file>/<id> [--done --text <t> [--force]] edits the item in its item file; --done closes the card
|
|
119
120
|
(below). A card has no status: it stores its lane, which
|
|
120
121
|
a build's start, a handover, a block and a close move,
|
|
121
122
|
and the board server moves the rest, each with a thread
|
|
@@ -161,7 +162,8 @@ command fails and changes nothing.
|
|
|
161
162
|
resumes the card, adds a thread line, and appends
|
|
162
163
|
card-unblocked {key, until} unless a dep is unfinished.
|
|
163
164
|
--held holds it with no time; --pick-up-at "" resumes it
|
|
164
|
-
[--ticket <id>]
|
|
165
|
+
[--ticket <id> | --new-ticket] links a ticket in the board\u2019s source, or makes one there
|
|
166
|
+
from the title and notes; the key stays
|
|
165
167
|
[--title <t>] renames the card
|
|
166
168
|
[--check "<name> <args>"] adds or changes the code check on the card's owner step,
|
|
167
169
|
as builder done --check takes it, clearing a broken mark;
|
|
@@ -169,7 +171,10 @@ command fails and changes nothing.
|
|
|
169
171
|
[--size small|large] [--model opus|sonnet] what builder pick reads while the hub's Claude model is
|
|
170
172
|
Auto: a small card gets Claude Sonnet, any other Opus, and
|
|
171
173
|
--model wins over --size; "" clears either
|
|
172
|
-
[--deps <id>,<
|
|
174
|
+
[--deps <id>,<file>/<id>,wave:<slug>] replaces its blocked-by list; --deps "" clears it.
|
|
175
|
+
wave:<slug> waits on every card in the wave at the
|
|
176
|
+
time, as it grows, until each is Done; refused for a
|
|
177
|
+
card in that wave
|
|
173
178
|
[--blocked-until "<condition>"] blocks it until a condition an agent can check, which
|
|
174
179
|
the orchestrator checks on its passes over the cards;
|
|
175
180
|
--blocked-until "" clears it, which threads and emits
|
|
@@ -179,12 +184,12 @@ command fails and changes nothing.
|
|
|
179
184
|
A block on a card In Progress sends it back to Todo,
|
|
180
185
|
stopping its board-launched builder (refused for a
|
|
181
186
|
session's own builder); one In Review stays, unlanded
|
|
182
|
-
[--parent <
|
|
187
|
+
[--parent <file>/<id>] sets its Parent, refusing itself or a card below it;
|
|
183
188
|
--parent "" clears it
|
|
184
|
-
[--related <id>,<
|
|
189
|
+
[--related <id>,<file>/<id>] replaces the cards it is Related to, which both cards
|
|
185
190
|
show; --related "" clears it, on both
|
|
186
|
-
[--relate <id>,<
|
|
187
|
-
[--unrelate <id>,<
|
|
191
|
+
[--relate <id>,<file>/<id>] adds cards to its Related to, keeping the rest
|
|
192
|
+
[--unrelate <id>,<file>/<id>] takes cards off its Related to, on both cards; neither
|
|
188
193
|
goes with --related
|
|
189
194
|
[--install <url> [--install-sha <sha>] a device build the owner installs from the card's Install
|
|
190
195
|
[--install-label <text>] | --clear-install] button, stamped built_at now: itms-services://?action=
|
|
@@ -195,6 +200,11 @@ command fails and changes nothing.
|
|
|
195
200
|
[--text <t>] with --done, required on a site: the card's closing line,
|
|
196
201
|
written last in its thread, saying why it closed (merged,
|
|
197
202
|
dropped, superseded)
|
|
203
|
+
[--split-into wave:<slug> | <id>,<file>/<id>] with --done, in place of --text: closes a split card. Each
|
|
204
|
+
open card waiting on it waits on the wave or cards it
|
|
205
|
+
split into instead, with a line in its thread, and the
|
|
206
|
+
closing line reads "Split into \u2026". Refused when a
|
|
207
|
+
waiting card is one of them, or the card is already Done
|
|
198
208
|
builder start for a build moves the card to In Progress with "Moved to In Progress: a build
|
|
199
209
|
started", resolving its answered questions, noting "Acted on: build started". It refuses while
|
|
200
210
|
the item is held or a dep isn't done, and while the newest owner answer to its questions that
|
|
@@ -203,27 +213,29 @@ command fails and changes nothing.
|
|
|
203
213
|
--pr closes ordinary builders and moves the card to In Review; install builders keep running.
|
|
204
214
|
A builder's hand-back of work it couldn't land (builder done with judgment calls) keeps the card
|
|
205
215
|
In Progress, reading "Orchestrator review", until the owning session lands it; it unblocks nothing.
|
|
206
|
-
The board server records the owner's card-done in the
|
|
216
|
+
The board server records the owner's card-done in the item file itself.
|
|
207
217
|
--done moves the card to Done and its ticket to Done: it resolves each of its questions (topic
|
|
208
|
-
item:<
|
|
218
|
+
item:<file>/<id>) that has an answer, noting "Closed with the card", and prints "resolved: <id>"
|
|
209
219
|
for each. It refuses while one has no answer yet; [--force] closes it and resolves that one too.
|
|
210
|
-
It prints "unblocked: <
|
|
220
|
+
It prints "unblocked: <file>/<id>" and appends card-unblocked {key} once per unblocking,
|
|
211
221
|
including held cards: start each unheld card next. It refuses a card the owner reopened: it
|
|
212
222
|
closes again only on the owner's Done
|
|
213
|
-
item add <
|
|
214
|
-
[--ticket <id>
|
|
223
|
+
item add <file>[/<id>] --title <t> a new card, unstarted; never
|
|
224
|
+
[--ticket <id> | --new-ticket] hand-edit the item file to add one. Refuses an id the file has.
|
|
225
|
+
[--deps <id>,<file>/<id>,wave:<slug>] A task with no ticket unless --ticket links one or
|
|
226
|
+
--new-ticket makes one from the title and notes
|
|
215
227
|
[--notes <t> | --notes-from <draft.md>] the board makes the card's own notes, <id>-notes.md beside
|
|
216
|
-
its
|
|
228
|
+
its item file, read-only: the draft verbatim, or "# <title>"
|
|
217
229
|
then the --notes text. Refuses a draft that is any card's
|
|
218
230
|
notes file, live, done or archived, naming that card
|
|
219
231
|
[--pick-up-at <time> | --held] holds it until then, or until resumed, as item set's
|
|
220
232
|
[--size small|large] [--model opus|sonnet] as item set's
|
|
221
|
-
[--parent <
|
|
233
|
+
[--parent <file>/<id>] the card it was spun out of, such as one filed from
|
|
222
234
|
another card's thread, grill or answer: it reads as a
|
|
223
235
|
follow-up, and its Parent lists it in Children. A card
|
|
224
236
|
from the + or New task has none
|
|
225
237
|
[--phase grilling|prototyping|ready] files it at once, as item set's --phase; the session's
|
|
226
|
-
[--relate <id>,<
|
|
238
|
+
[--relate <id>,<file>/<id>] own form, linking it with --relate in the same call.
|
|
227
239
|
ready is for a Quick build ask (--from-ask), with --size
|
|
228
240
|
and --notes or --notes-from, since a builder starts from it
|
|
229
241
|
[--from-ask <eventId> --text <t>] files the card from that ask in this one command, as
|
|
@@ -231,10 +243,10 @@ command fails and changes nothing.
|
|
|
231
243
|
the card, which takes the ask's place in Todo. Needs --phase
|
|
232
244
|
On a board --phase is required, so no card an agent
|
|
233
245
|
adds reads New
|
|
234
|
-
Without /<id>,
|
|
235
|
-
card. Prints "<label> added as <key>": use the key in later commands
|
|
236
|
-
item notes <
|
|
237
|
-
(--file <draft.md> | --from <
|
|
246
|
+
Without /<id>, a task is keyed T-<n>, the next on the board, and a ticketed card takes its ticket's
|
|
247
|
+
id (GH-<n> for issue #n); pass an id only to import a card. Prints "<label> added as <key>": use the key in later commands
|
|
248
|
+
item notes <file>/<id> --heading <h> appends "## <h>" and the draft to the card's notes, making
|
|
249
|
+
(--file <draft.md> | --from <file>/<id>) its <id>-notes.md when it has none; the only way to write
|
|
238
250
|
notes (--notes-file and --plan are gone). Write the draft
|
|
239
251
|
outside the board's data; one that is any card's notes
|
|
240
252
|
file, live, done or archived, is refused, naming that
|
|
@@ -243,7 +255,7 @@ command fails and changes nothing.
|
|
|
243
255
|
Resolves answered questions still waiting on Claude,
|
|
244
256
|
noting "Recorded in the card's notes", and prints
|
|
245
257
|
"resolved: <id>" for each
|
|
246
|
-
item relate-ask <eventId> <
|
|
258
|
+
item relate-ask <eventId> <file>/<id> --summary <t> folds an ask about an existing item into it: the ask's
|
|
247
259
|
card goes, its message and thread show in the item's
|
|
248
260
|
thread marked Moved, then the summary line. Again with
|
|
249
261
|
another item moves it there
|
|
@@ -263,47 +275,43 @@ command fails and changes nothing.
|
|
|
263
275
|
that isn't New
|
|
264
276
|
Then sends the queued pushes, each on its own, unless
|
|
265
277
|
pushes are held (overnight, snoozed, the weekend or off)
|
|
266
|
-
|
|
267
|
-
--
|
|
268
|
-
(--note <t> | --file <f>)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
links.
|
|
289
|
-
|
|
278
|
+
wave create <slug> --title <t> --goal <t> a wave, in the owner's words: a named set of cards from
|
|
279
|
+
--card <file>/<id>[:<file>/<id>,\u2026] [--card \u2026] one item file, one --card per card in build order, each
|
|
280
|
+
(--note <t> | --file <f>) naming after a colon the earlier cards it would conflict
|
|
281
|
+
with (as item set --deps takes them). Each card is then
|
|
282
|
+
blocked by those cards alone (its links outside the wave
|
|
283
|
+
stay), so cards that don't conflict build side by side,
|
|
284
|
+
and the note goes to wave-<slug>.md beside the item file.
|
|
285
|
+
Todo shows it as a band of its cards in order, each with
|
|
286
|
+
the board's blocked-by line, its goal on its name, and
|
|
287
|
+
Notes opens the goal, the order and the note. Refuses a
|
|
288
|
+
card already in another open wave, and a conflict that
|
|
289
|
+
isn't an earlier card of the wave. Prints the order by
|
|
290
|
+
card label, each card with "(after \u2026)" its conflicts
|
|
291
|
+
wave add <slug> <file>/<id> [--after <file>/<id>,\u2026] puts a card in the wave, last, blocked by the earlier
|
|
292
|
+
cards --after names; refused for a card whose --deps
|
|
293
|
+
names the wave
|
|
294
|
+
wave remove <slug> <file>/<id> takes a card out: it and the wave's cards waiting on it
|
|
295
|
+
lose their links to each other
|
|
296
|
+
wave set <slug> [<file>/<id> --after <file>/<id>,\u2026] rewrites only that card's links to the wave's cards to
|
|
297
|
+
[--title <t>] [--goal <t>] --after's earlier cards ("" clears them), renames the
|
|
298
|
+
[--note <t> | --file <f>] wave, sets its goal or rewrites its note
|
|
299
|
+
wave finish <slug> the wave's band leaves Todo; its cards keep their
|
|
300
|
+
links. Refused, naming them, while a card in it is
|
|
301
|
+
open. A wave whose cards are all Done leaves it too
|
|
302
|
+
A band's Triage sends wave-triage {wave}, or {file} for an item file's band; it reads
|
|
290
303
|
Triaging\u2026 until you reply to that event, then "Triage: N open questions" while questions on topic
|
|
291
|
-
|
|
292
|
-
tickets backfill [--dry-run] gives every open card without a ticket one: the first
|
|
293
|
-
id in its title (or legacy linear list) from the board's
|
|
294
|
-
source is linked, else a new ticket is made. Done cards are
|
|
295
|
-
left alone. Prints each card's result; --dry-run changes and
|
|
296
|
-
calls nothing
|
|
304
|
+
wave:<slug> or file:<slug> are open or answered
|
|
297
305
|
reviewers set --file <json> the board's reviewers in board.json, replacing them:
|
|
298
|
-
[{name, slack_user, dm, github}], as in
|
|
299
|
-
The settings page lists them; new
|
|
306
|
+
[{name, slack_user, dm, github}], as in an item file.
|
|
307
|
+
The settings page lists them; new item files copy them
|
|
300
308
|
reviewers add --file <json> one reviewer or an array; replaces the same name
|
|
301
309
|
reviewers remove <name>... refuses a name the board doesn't have
|
|
302
310
|
slack Slack identity as JSON; imports slack-bridge config once
|
|
303
311
|
slack set [--self-dm D..] [--user U..] sets hub Slack identity in ~/.claude/kickflip/slack.json
|
|
304
312
|
[--domain x] [--timezone Area/City]
|
|
305
313
|
integrations readiness rows as one JSON line
|
|
306
|
-
review updated <
|
|
314
|
+
review updated <file>/<id> --text "<what changed>" queues \u201CUpdated: <what changed>\u201D in the thread of every
|
|
307
315
|
posted link on the card's PRs; the hub's next poll sends it
|
|
308
316
|
review due one JSON line of the Slack sends the hub owes, from local state
|
|
309
317
|
and data/pr-slack.json: {links: [{pr, key, reviewer,
|
|
@@ -358,7 +366,7 @@ command fails and changes nothing.
|
|
|
358
366
|
pushed, which needs Push main after a land granted.
|
|
359
367
|
A local board never opens PRs. Without auto-merge, a
|
|
360
368
|
card with no PR waits in In Review for the owner's Merge
|
|
361
|
-
build <
|
|
369
|
+
build <file>/<id> --site <route> asks the hub to run this card's build script; prints
|
|
362
370
|
--asked "<owner's words>" started and exits. install is an alias. Only the owner's
|
|
363
371
|
Serve / Build and Auto-build start a card run, so --asked
|
|
364
372
|
must quote the owner's ask, card message or answer note on
|
|
@@ -413,7 +421,7 @@ command fails and changes nothing.
|
|
|
413
421
|
{"source":"linear","workspace","team","project"}. A
|
|
414
422
|
GitHub account runs the board's issue calls as that gh
|
|
415
423
|
login; without one they run as gh's active account
|
|
416
|
-
builder start <
|
|
424
|
+
builder start <file>/<id> [--agent <name>] [--kind <claude|codex|prototype|grill|install|\u2026>] [--label <who>]
|
|
417
425
|
[--model <opus|sonnet>] the Claude model running the builder, recorded and printed as
|
|
418
426
|
"model: Claude Sonnet" (kind claude takes builder pick's model;
|
|
419
427
|
codex prints "model: Codex"). The thread's agent line reads it: "Claude
|
|
@@ -453,7 +461,7 @@ command fails and changes nothing.
|
|
|
453
461
|
on a board makes it without --worktree when it keeps none
|
|
454
462
|
(a taken-up or resumed record's, or the worktree the
|
|
455
463
|
card's last record left while it is still there)
|
|
456
|
-
builder progress <
|
|
464
|
+
builder progress <file>/<id> --phase <phase> one status line: a phase, reading, building, testing,
|
|
457
465
|
(<detail> | --text <d>) [--agent <name>] checking or landing, and a present-tense detail of at most
|
|
458
466
|
80 characters in plain words, which the owner reads at a
|
|
459
467
|
glance; a longer detail is refused with its length. Hashes,
|
|
@@ -464,7 +472,7 @@ command fails and changes nothing.
|
|
|
464
472
|
A progress line, --progress included, needs --site: it is a status line in the item's thread, and
|
|
465
473
|
the card's status shows the newest one from an agent still working. builder progress refuses
|
|
466
474
|
without an open record for that agent, as builder call does
|
|
467
|
-
builder done <
|
|
475
|
+
builder done <file>/<id> [--agent <name>] marks that agent's record done; the item's thread
|
|
468
476
|
[--next <t>] [--text <what you checked>] shows when it started and finished. On a
|
|
469
477
|
record with unverified judgment calls it hands the card
|
|
470
478
|
back: its records close marked handed back, with Next "Answer the builder's
|
|
@@ -505,8 +513,8 @@ command fails and changes nothing.
|
|
|
505
513
|
changed>\u201D in each link's thread, as review updated does;
|
|
506
514
|
none sends nothing, for a change only to comments, names,
|
|
507
515
|
docs or tests
|
|
508
|
-
builder takes --dir <dir> for
|
|
509
|
-
builder stop <
|
|
516
|
+
builder takes --dir <dir> for an item file in <dir>; its records go in <dir>/agents
|
|
517
|
+
builder stop <file>/<id> the orchestrator's stop of the card's run the board launched,
|
|
510
518
|
its headless builder, lander or rebase run, with every process
|
|
511
519
|
under it: the record stays open, marked stopped, so the server
|
|
512
520
|
relaunches nothing and the card stays In Progress reading
|
|
@@ -514,7 +522,7 @@ command fails and changes nothing.
|
|
|
514
522
|
waiting on the orchestrator; the same words go in its thread.
|
|
515
523
|
Prints what it stopped, one process a line. Refuses a planner,
|
|
516
524
|
a builder a session started itself, or none running
|
|
517
|
-
item stall <
|
|
525
|
+
item stall <file>/<id> --reason "<why>" the orchestrator's flag on a stale card it can't restart: the
|
|
518
526
|
card stays in its lane reading red "stalled", an Action needed
|
|
519
527
|
"stalled: <why>" with one push, waiting on nobody else; its
|
|
520
528
|
run stops as builder stop stops it (nothing to stop when it
|
|
@@ -522,7 +530,7 @@ command fails and changes nothing.
|
|
|
522
530
|
stays, and it frees its implementer slot. Its thread reads
|
|
523
531
|
"stalled by orchestrator: <why>". A message on the card
|
|
524
532
|
clears it
|
|
525
|
-
land stop <
|
|
533
|
+
land stop <file>/<id> the orchestrator's stop of the card's land and its suite: one a
|
|
526
534
|
headless builder or lander runs stops that agent as builder
|
|
527
535
|
stop does; the land a held card's Merge tap started stops
|
|
528
536
|
alone, leaving the land queue, and the card's Merge shows
|
|
@@ -531,28 +539,28 @@ command fails and changes nothing.
|
|
|
531
539
|
didn't launch
|
|
532
540
|
land queue the host's land queue in order: the land running, then each
|
|
533
541
|
waiting, "(bumped)" on one a bump moved
|
|
534
|
-
item bump <
|
|
535
|
-
item unbump <
|
|
542
|
+
item bump <file>/<id> the card's bump, as the owner's bump icon sends it (card-bump;
|
|
543
|
+
item unbump <file>/<id> card-unbump). A bumped card's land goes ahead in the land queue
|
|
536
544
|
of every land not bumped since, the latest bump first, without
|
|
537
545
|
stopping the one running; a bump of a queued land says so in
|
|
538
546
|
its thread ("land moved first by orchestrator")
|
|
539
|
-
lander <
|
|
547
|
+
lander <file>/<id> relaunch the headless lander of a card it stopped on (a usage
|
|
540
548
|
pause ends one): its open record closes as handed back and
|
|
541
549
|
the server launches a fresh lander; refused unless a
|
|
542
550
|
builder record on the card is open
|
|
543
|
-
builder pick <
|
|
551
|
+
builder pick <file>/<id> who builds the card, from the hub's Codex share and the pause
|
|
544
552
|
line, and for claude which model, from the hub's Claude model
|
|
545
553
|
and the card's --size and --model: "codex: <reason>" or
|
|
546
554
|
"claude sonnet: <reason>". Pass that kind to builder start
|
|
547
555
|
--kind, and launch that model
|
|
548
|
-
audit [<
|
|
556
|
+
audit [<file>/<id>] the board's audit log of the last 30 days, data/audit.jsonl:
|
|
549
557
|
each call the code made on its own (builder-pick, pull,
|
|
550
558
|
round-planner, launch-held, arrival-planner, relaunch,
|
|
551
559
|
lander, triage, pr-review, push, pick-up, auto-merge,
|
|
552
560
|
ready-draft, session-resume, compact) as "<at> <call>
|
|
553
561
|
[<key>]: <result>. <reason>" and the inputs it read; with
|
|
554
562
|
a card, only that card's. The hub's own: kickflip audit
|
|
555
|
-
agent launch <
|
|
563
|
+
agent launch <file>/<id> --name <n> --task <t> any session's helper on an open card, an agent the board server
|
|
556
564
|
[--writes] [--model <opus|sonnet>] launches headless with the task as its brief, under its own
|
|
557
565
|
name (lowercase, "-", not main or visual-\u2026), its record
|
|
558
566
|
<id>@<name> requested until then. A read-only helper launches
|
|
@@ -562,38 +570,38 @@ command fails and changes nothing.
|
|
|
562
570
|
and works in its own worktree, branch <id>-<name> from the
|
|
563
571
|
card's, so it needs the card's worktree. --model else
|
|
564
572
|
builder pick's Claude model. Its lines carry its name
|
|
565
|
-
agent wait <
|
|
573
|
+
agent wait <file>/<id> --name <n> [--agent <me>] blocks until the helper's builder done, then prints its
|
|
566
574
|
[--timeout <minutes>] --text and, for one that writes, its branch. Meanwhile
|
|
567
575
|
--agent's record (main by default) takes no slot. Refuses
|
|
568
576
|
after --timeout (9) minutes, saying whether it runs or waits
|
|
569
577
|
for a slot, and once its process is gone without done
|
|
570
|
-
agent merge <
|
|
578
|
+
agent merge <file>/<id> --name <n> merges a done helper's branch into the card's worktree and
|
|
571
579
|
removes the helper's worktree and branch; on a conflict it
|
|
572
580
|
names the files: resolve, commit and run it again
|
|
573
|
-
builder call <
|
|
581
|
+
builder call <file>/<id> <judgment call> appends one line to the agent's open builder record: a
|
|
574
582
|
[--agent <name>] judgment call as the builder makes it, or a "Question: \u2026"
|
|
575
583
|
for the session; refused with no open record. A builder start
|
|
576
584
|
on a record with unverified calls resumes that build, keeping
|
|
577
585
|
its calls and worktree. The thread shows them, collapsed
|
|
578
|
-
builder calls <
|
|
586
|
+
builder calls <file>/<id> [--agent <name>] prints the record's calls, numbered, then "verified <time>"
|
|
579
587
|
or "not verified yet"
|
|
580
|
-
builder ask <
|
|
588
|
+
builder ask <file>/<id> <question> [--agent <name>] a running builder's one-line question for the board's session,
|
|
581
589
|
which it asks only when blocked (every way forward would likely
|
|
582
590
|
be wasted work), then waits with builder asks --wait: a thread
|
|
583
591
|
line named by its model ("Claude Opus builder"), never unread,
|
|
584
592
|
and a builder-ask {key, ask, agent, text} event. Prints its
|
|
585
593
|
id, ask<n>. Needs --site and an open builder record
|
|
586
|
-
builder answer <
|
|
594
|
+
builder answer <file>/<id> <ask> --text <t> the session's answer to that ask, a thread line named
|
|
587
595
|
Orchestrator that leaves the card quiet
|
|
588
|
-
builder defer <
|
|
596
|
+
builder defer <file>/<id> <ask> --text <t> the session's deferral of an ask it can't answer confidently:
|
|
589
597
|
the same thread line, and a "Question: <ask> (deferred to the
|
|
590
598
|
owner)" call on the asking agent's record. After its review the
|
|
591
599
|
session puts it to the owner as a card question with
|
|
592
600
|
"ask": "<ask>"; builder verified refuses until then
|
|
593
|
-
builder asks <
|
|
601
|
+
builder asks <file>/<id> [--wait] [--timeout <min>] prints each ask, then its answer, its deferral or "waiting on
|
|
594
602
|
the orchestrator"; --wait blocks until none is waiting, and
|
|
595
603
|
after --timeout (9) minutes says so and fails: run it again
|
|
596
|
-
builder verified <
|
|
604
|
+
builder verified <file>/<id> [--agent <name>] the session's mark that it checked the record's calls; land
|
|
597
605
|
[--note <t>] refuses while any record on the card has a call after it.
|
|
598
606
|
On a handed-back build (not done, no builder open, every
|
|
599
607
|
call verified) it finishes the card in the same step, as
|
|
@@ -601,7 +609,7 @@ command fails and changes nothing.
|
|
|
601
609
|
calls verified."). While a question on the card is open it
|
|
602
610
|
says so and the card waits; a finish that fails keeps the
|
|
603
611
|
mark, the card handed back, and says why
|
|
604
|
-
land <
|
|
612
|
+
land <file>/<id> --text <t> [--repo <dir>] brings the worktree its builder record keeps (else
|
|
605
613
|
[--dir <dir>]
|
|
606
614
|
~/.claude/kickflip/worktrees/<id> of --repo) into the board's
|
|
607
615
|
branch (the one its builders branch from, else main), one
|
|
@@ -626,7 +634,7 @@ command fails and changes nothing.
|
|
|
626
634
|
landed card in In Review on that step until the owner's
|
|
627
635
|
Done, or until its --check passes. KICKFLIP_RESTART_CMD='["<cmd>"]' replaces
|
|
628
636
|
the restarts, called with hub or board <route>
|
|
629
|
-
merge <
|
|
637
|
+
merge <file>/<id> [--after-approval "<why trivial>"] merges the card's PRs (gh pr merge --squash) in its order and
|
|
630
638
|
[--confirmed "<owner's words>"] records it merged, once GitHub reads every open one approved,
|
|
631
639
|
its checks green with none pending and its merge state CLEAN,
|
|
632
640
|
the test the Merge button uses; a merged or closed PR is left
|
|
@@ -640,10 +648,10 @@ command fails and changes nothing.
|
|
|
640
648
|
it is now, reruns the tests, fast-forwards, closes the card
|
|
641
649
|
and pushes when Push main is on; a conflict or failing
|
|
642
650
|
tests launch a headless lander that lands it
|
|
643
|
-
arch file <
|
|
651
|
+
arch file <file>/<id> turns the architecture review audit card's answered findings
|
|
644
652
|
into cards in the review's wave: titled with the picked
|
|
645
653
|
option's label, its detail, practice and the owner's note as
|
|
646
|
-
notes, each after the
|
|
654
|
+
notes, each after the audit card, its question resolved
|
|
647
655
|
"Filed as <ref>."; a Skip pick resolves its question alone.
|
|
648
656
|
Prints "filed: <ref> from <id>", "skipped: <id>", and
|
|
649
657
|
"waiting: <ids>" while any are unanswered; once none is, it
|
|
@@ -663,4 +671,4 @@ command fails and changes nothing.
|
|
|
663
671
|
(overnight, snoozed or the weekend). --urgent: a
|
|
664
672
|
production fault only; it beats those holds. While
|
|
665
673
|
notifications are off every push is dropped, urgent too
|
|
666
|
-
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","wait","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 q(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(q,"readJSONInput");function C(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(C,"refuseNotesFile");function T(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(T,"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(l&&i!==void 0&&await s.isPlanner(e,a,o.agent))return s.stopBuilder(e,a,{...o,note:i,requireNext:!0});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 or a planner'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","title"],(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),t.title)}],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=q(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 settle":[["id"],["note"],(e,[a],t)=>s.settleQuestion(e,a,t.note)],"question publish":[["key"],[],async(e,[a])=>({ids:await s.publishScreened(e,a)})],"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","phase","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)=>(C(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"],phase:t.phase,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","held","parent","size","model","phase","state","relate","from-ask","text"],(e,[a],t,n)=>{if(C(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"],held:t.held,parent:t.parent,size:t.size,model:t.model,phase:t.phase,state:t.state,relate:t.relate,fromAsk:t["from-ask"],text:t.text,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","title"],(e,[a,t],n)=>s.relateAsk(e,a,t,n.summary,n.title)],"ask progress":[["eventId"],["next"],(e,[a],t)=>s.markAsk(e,a,"progress",t.next)],"ask done":[["eventId"],[],(e,[a])=>s.markAsk(e,a,"done")],"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","...step","...phase","note","file"],(e,[a],t)=>s.createLineup(e,a,{title:t.title,goal:t.goal,steps:t.step,phase:t.phase,note:T(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","...step","...phase"],(e,[a],t)=>s.changeLineup(e,a,{title:t.title,goal:t.goal,note:T(t),steps:t.step,phase:t.phase})],"lineup finish":[["slug"],[],(e,[a])=>s.finishLineup(e,a)],slack:[[],[],()=>({lines:[JSON.stringify(H())]})],"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 D.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,q(t.file))],"reviewers add":[[],["file"],(e,a,t)=>s.addReviewers(e,q(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"],["wait","timeout"],(e,[a],t)=>s.listAsks(e,a,t)],"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])=>S(m(e,"item bump"),a,!0)],"item unbump":[["wave/id"],[],(e,[a])=>S(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=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 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.phase,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`${R(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 settle":r((e,[a])=>`question ${a} settled`,"question settle"),"question publish":r(async(e,[a],t,{ids:n})=>`${R(n,"question")} on ${await h(e,a)} passed to the owner`,"question publish"),"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"),"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)=>`${R(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)} 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)} 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 A=I?.[0],k=le(i,c,E);pe(c,k);const P=A===void 0?null:await s.siteDir(A),v=await p(P,g,k,a),j=y(ge,i),O=j&&await j(P,g,k,v);O&&console.log(O);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};
|
|
674
|
+
KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class d extends Error{static{i(this,"UsageError")}}const re=new Set(["new-ticket","done","clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","wait","hub","clear-install","force","worktree","dispatch","writes"]);function oe(e){const a=Object.create(null),t=[];for(let s=0;s<e.length;s++){const r=e[s];if(!r.startsWith("--")){t.push(r);continue}const o=r.slice(2);if(re.has(o)){if(Object.hasOwn(a,o))throw new d(`--${o} is given twice`);a[o]=!0}else if(s+1<e.length)(a[o]||=[]).push(e[++s]);else throw new d(`--${o} needs a value`)}return{flags:a,rest:t}}i(oe,"parse");function de(e,a,t){const s=new Set(a.filter(l=>l.startsWith("...")).map(l=>l.slice(3))),r=new Set(a.map(l=>l.replace(/^\.\.\./,""))),o=Object.keys(t).filter(l=>!r.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||s.has(l))return[l,c];if(c.length>1)throw new d(`--${l} is given twice`);return[l,c[0]]}))}i(de,"flagsOf");const le=["text","note","notes","heading","summary","title","body","next","progress","install-label","confirmed","asked","task","reason"],he=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function ce(e){const a=e.trim();if(!a||/\s/.test(a))return!1;if(he.test(a))return!0;try{return g.statSync(a).isFile()}catch{return!1}}i(ce,"looksLikePath");function ue(e,a){for(const t of le){if(typeof a[t]!="string"||!ce(a[t]))continue;const s=e.includes("file")?`; for a file use --file ${a[t].trim()}`:"";throw new d(`--${t} takes text, not a file path (${a[t].trim()})${s}`)}if(typeof a.file=="string"&&a.file!=="-"&&!g.existsSync(a.file))throw new d(`--file ${a.file}: no such file`)}i(ue,"checkInputs");const w=i(e=>g.readFileSync(e==="-"?0:e,"utf8"),"readInput");function q(e){if(!e)throw new d("--file <path> is required");try{return JSON.parse(w(e))}catch(a){throw new n.BoardError(`${e}: ${a.message}`)}}i(q,"readJSONInput");function C(e,a){const t=["notes-file","plan"].find(s=>e[s]!==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}`)}i(C,"refuseNotesFile");function T(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)}i(T,"noteOf");async function pe(e,a,{"owner-step":t,check:s,text:r,...o}){if(t!==void 0&&(!e||o.dir))throw new d("--owner-step goes on a board card: pass --site, not --dir");if(s!==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 n.setItem(e,a,{ownerStep:t,check:s});const l=e&&!o.dir,c=l&&await n.handBackCalls(e,a,o);if(c)return c;if(l&&o.next===void 0&&await n.finishesCard(e,a,o)){if(r===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 $(e,a,{text:r,ownerStep:t})}if(l&&await n.isHelper(e,a,o.agent)){if(r===void 0)throw new d('a helper hands back its result: pass --text "<what you did or found>"');return n.stopBuilder(e,a,{...o,note:r})}if(l&&r!==void 0&&await n.isPlanner(e,a,o.agent))return n.stopBuilder(e,a,{...o,note:r,requireNext:!0});if(r!==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 or a planner's");return n.stopBuilder(e,a,{...o,requireNext:!0})}i(pe,"builderDone");const b={pr:[["file/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 d('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 d('pr reply needs --body "<what Claude says>"');return n.postPR(e,a,{replyTo:t,body:s.body})}],reply:[["eventId"],["text","file","title"],(e,[a],t)=>{if(t.text===void 0==(t.file===void 0))throw new d("reply takes --text or --file");return n.writeReply(e,a,t.text??w(t.file),t.title)}],thread:[["file/id"],["text","link","who","via"],(e,[a],t)=>n.appendMessage(e,a,t)],"visual add":[["file/id"],["file","title"],(e,[a],t)=>n.addVisual(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=q(t.file);return{...await n.addQuestions(e,s,process.env.KICKFLIP_RENDER_CHECK==="off"?void 0:se),asked:[s].flat()}}],"question resolve":[["id"],["note"],(e,[a],t)=>n.resolveQuestion(e,a,t.note)],"question settle":[["id"],["note"],(e,[a],t)=>n.settleQuestion(e,a,t.note)],"question publish":[["key"],[],async(e,[a])=>({ids:await n.publishScreened(e,a)})],"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","served","...state"],(e,[a],t)=>n.addPrototype(e,{topic:a,title:t.title,card:t.card,takes:t.take,served:t.served,states:t.state})],"prototype base":[[],[],e=>ne(e)],"prototype list":[[],[],e=>n.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[a])=>n.removePrototype(e,a)],"item set":[["file/id"],["done","split-into","status","clear-status","next","clear-next","...pr","pushed","gh-since","from-ask","phase","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","new-ticket","text","check"],async(e,[a],t,s)=>(C(t,`add to its notes with item notes ${a} --heading "<heading>" --file <draft.md>`),t.done&&e&&await n.checkNotReopened(e,a),n.setItem(e,a,{done:t.done,splitInto:t["split-into"],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"],phase:t.phase,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,newTicket:t["new-ticket"],text:t.text,check:t.check},s))],"item add":[["file[/id]"],["title","notes","notes-from","deps","notes-file","plan","ticket","new-ticket","pick-up-at","held","parent","size","model","phase","state","relate","from-ask","text"],(e,[a],t,s)=>{if(C(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 n.addItem(e,a,{title:t.title,notes:t.notes,notesFrom:t["notes-from"],deps:t.deps,ticket:t.ticket,newTicket:t["new-ticket"],pickUpAt:t["pick-up-at"],held:t.held,parent:t.parent,size:t.size,model:t.model,phase:t.phase,state:t.state,relate:t.relate,fromAsk:t["from-ask"],text:t.text,intake:!0},s)}],"item notes":[["file/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 <file>/<id>");return n.addNotes(e,a,t)}],"item relate-ask":[["eventId","file/id"],["summary","title"],(e,[a,t],s)=>n.relateAsk(e,a,t,s.summary,s.title)],"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)],"wave create":[["slug"],["title","goal","...card","note","file"],(e,[a],t)=>n.createWave(e,a,{title:t.title,goal:t.goal,cards:t.card,note:T(t)})],"wave add":[["slug","file/id"],["after"],(e,[a,t],s)=>n.changeWave(e,a,{add:t,after:s.after})],"wave remove":[["slug","file/id"],[],(e,[a,t])=>n.changeWave(e,a,{remove:t})],"wave set":[["slug","...file/id?"],["after","title","goal","note","file"],(e,[a,...t],s)=>{if(t.length>1)throw new d("wave set takes <slug> [<file/id>]");return n.changeWave(e,a,{card:t[0],after:s.after,title:s.title,goal:s.goal,note:T(s)})}],"wave finish":[["slug"],[],(e,[a])=>n.finishWave(e,a)],slack:[[],[],()=>({lines:[JSON.stringify(H())]})],"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 M(Object.fromEntries(Object.entries(t).map(([r,o])=>[s[r],o]))))]}}catch(r){throw new n.BoardError(r.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await W.rows())]})],"review updated":[["file/id"],["text"],(e,[a],t)=>n.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 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 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 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))]})],"reviewers set":[[],["file"],(e,a,t)=>n.setReviewers(e,q(t.file))],"reviewers add":[[],["file"],(e,a,t)=>n.addReviewers(e,q(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:[["file/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 r=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 r.json();if(!r.ok)throw new n.BoardError(o.error||"The hub refused the build.");return{lines:["started",...(await n.appendMessage(e,a,{text:s})).lines]}}catch(r){throw r instanceof n.BoardError?r:new n.BoardError(`Could not start the build: ${r.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":[["file/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[a],{worktree:t,repo:s,dispatch:r,...o})=>{if(s!==void 0&&!t)throw new d("--repo goes with --worktree");const l=i(p=>z(e,a.split("/")[1],{repo:s,...p}),"make"),c=e&&o.dir===void 0&&ae(e);return n.startBuilder(e,a,{...o,dispatch:r?"session":void 0,worktree:t?l:void 0,ownWorktree:c?l:void 0})}],"builder progress":[["file/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[a,...t],s)=>{if(t.length&&s.text!==void 0)throw new d("builder progress takes its text once: as words or as --text");if(!t.length&&s.text===void 0)throw new d("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":[["file/id"],["dir","agent","next","owner-step","check","text","updated"],async(e,[a],{updated:t,...s})=>{const r=e&&!s.dir;if(r&&t===void 0&&await n.cardLinked(e,a))throw new n.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 pe(e,a,s);return r&&t!==void 0&&t.trim().toLowerCase()!=="none"&&await n.reviewUpdated(e,a,t),o}],"builder pick":[["file/id"],[],(e,[a])=>n.pickBuilder(e,a)],"agent launch":[["file/id"],["name","task","writes","model"],(e,[a],t)=>n.requestHelper(e,a,t)],"agent wait":[["file/id"],["name","agent","timeout"],(e,[a],t)=>n.waitHelper(e,a,{...t,alive:Q})],"agent merge":[["file/id"],["name"],async(e,[a],t)=>{const{record:s,card:r}=await n.helperOf(e,a,t.name);if(!s.done)throw new n.BoardError(`helper ${t.name} is still open; run agent wait ${a} --name ${t.name} first`);if(!s.writes||!s.worktree)throw new n.BoardError(`helper ${t.name} writes no code, so there is nothing to merge`);if(!r)throw new n.BoardError(`${a} has no worktree to merge helper ${t.name} into`);return G(r,s.worktree)}],audit:[["...file/id?"],[],(e,[a,...t])=>{if(t.length)throw new d("audit takes one <file/id> at most");return n.auditLog(e,a)}],"builder call":[["file/id","...text"],["dir","agent"],(e,[a,...t],s)=>n.recordCall(e,a,t.join(" "),s)],"builder calls":[["file/id"],["dir","agent"],(e,[a],t)=>n.listCalls(e,a,t)],"builder ask":[["file/id","...text"],["agent"],(e,[a,...t],s)=>n.askOrchestrator(e,a,t.join(" "),s)],"builder answer":[["file/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text)],"builder defer":[["file/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text,{deferred:!0})],"builder asks":[["file/id"],["wait","timeout"],(e,[a],t)=>n.listAsks(e,a,t)],"builder verified":[["file/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 $(e,a,{text:t.note??"Judgment calls verified."})}catch(r){throw r instanceof n.BoardError?Object.assign(new n.BoardError(`judgment calls verified, but ${await n.cardName(e,a)} didn't finish: ${r.message}`),{exitCode:r.exitCode}):r}}],land:[["file/id"],["repo","dir","text","owner-step","check"],async(e,[a],t)=>{const s=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 r=e?await n.cardName(e,a):void 0,o=await J(r??a,async()=>{e&&await n.checkClose(e,a,t.text,t["owner-step"],t.check),await n.checkCallsVerified(e,a,{dir:t.dir});const l=await n.recordedWorktree(e,a,{dir:t.dir}),{repo:c,base:p}=U(e,{repo:t.repo??l?.repo});return await F({path:l?.path??x.join(K(),s),branch:l?.branch??s,repo:x.resolve(c),base:p,ref:r}),e?n.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 ${r??a}`]}:o}],"builder stop":[["file/id"],[],(e,[a])=>ee(m(e,"builder stop"),a)],"item stall":[["file/id"],["reason"],(e,[a],t)=>Y(m(e,"item stall"),a,t.reason)],"land stop":[["file/id"],[],(e,[a])=>te(m(e,"land stop"),a)],"land queue":[[],[],()=>Z()],"item bump":[["file/id"],[],(e,[a])=>S(m(e,"item bump"),a,!0)],"item unbump":[["file/id"],[],(e,[a])=>S(m(e,"item unbump"),a,!1)],lander:[["file/id"],[],async(e,[a])=>{if(!e)throw new d("lander needs --site <route>");return await n.relaunchLander(e,a),{lines:[`${await n.cardName(e,a)} is handed back again; the server launches a lander for it within a minute`]}}],merge:[["file/id"],["after-approval","confirmed"],async(e,[a],t)=>e&&await n.heldMerge(e,a)?X(e,a):n.mergeCard(e,a,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["file/id"],[],(e,[a])=>e?V(e,a):Promise.reject(new n.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 n.sendPush(e,t)}]};b.install=b.build;function m(e,a){if(!e)throw new d(`${a} needs --site <route>`);return e}i(m,"needSite");const h=i((e,a)=>n.cardName(e,a),"card"),R=i((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),_=i(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function we(e){const a=e["pick-up-at"];return[e.done&&(e["split-into"]?`done, split into ${e["split-into"].split(",").join(", ")}`:"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.phase,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["new-ticket"]&&"ticket made",e.check!==void 0&&(e.check.trim()?`check ${e.check.trim().split(/\s+/).join(" ")}`:"check removed")].filter(Boolean).join(", ")}i(we,"itemChanges");const me={reply:i(async(e,[a])=>`reply written to ${await n.eventCard(e,a)}`,"reply"),thread:i(async(e,[a])=>`thread line on ${await h(e,a)}`,"thread"),"topic add":i((e,[a])=>`topic ${a} added`,"topic add"),"question add":i(async(e,a,t,{asked:s,staged:r})=>{const o=[...new Set(s.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`${R(s.map(c=>c.id),"question")} ${r===s.length?"staged":"added"}${l}`},"question add"),"question resolve":i((e,[a])=>`question ${a} resolved`,"question resolve"),"question settle":i((e,[a])=>`question ${a} settled`,"question settle"),"question publish":i(async(e,[a],t,{ids:s})=>`${R(s,"question")} on ${await h(e,a)} passed to the owner`,"question publish"),"question move":i((e,[a],t)=>`questions ${a} merged onto ${t.to}`,"question move"),"prototype add":i(async(e,[a],t)=>`prototype ${a} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":i((e,[a])=>`prototype ${a} removed`,"prototype rm"),"item set":i(async(e,[a],t)=>`${await h(e,a)}: ${we(t)}`,"item set"),"item add":i(async(e,a,t,{key:s})=>`${await h(e,s)} added as ${s}`,"item add"),"item notes":i(async(e,[a],t,{file:s,heading:r})=>`${await h(e,a)}: notes section "${r}" added to ${s}`,"item notes"),"item relate-ask":i(async(e,[a,t])=>`task ${a} folded into ${await h(e,t)}`,"item relate-ask"),"ask progress":i(async(e,[a])=>`${await h(e,`ask:${a}`)}: in progress`,"ask progress"),"ask done":i(async(e,[a])=>`${await h(e,`ask:${a}`)}: done`,"ask done"),"ask unfold":i((e,[a])=>`task ${a} back on a card of its own`,"ask unfold"),"ask move":i((e,[a],t)=>`task ${a} moved to /${t.to}`,"ask move"),"wave create":i((e,[a])=>`wave ${a} created`,"wave create"),"wave add":i(async(e,[a,t])=>`${await h(e,t)} added to wave ${a}`,"wave add"),"wave remove":i(async(e,[a,t])=>`${await h(e,t)} taken out of wave ${a}`,"wave remove"),"wave set":i((e,[a])=>`wave ${a} updated`,"wave set"),"wave finish":i((e,[a])=>`wave ${a} finished`,"wave finish"),"review record":i((e,[a],t)=>`review of PR #${a} recorded: ${t.state==="needs"?"needs you":"posted"}`,"review record"),"reviewers set":i(()=>"reviewers replaced","reviewers set"),"reviewers add":i(()=>"reviewers added","reviewers add"),"reviewers remove":i((e,a)=>`${R(a,"reviewer")} removed`,"reviewers remove"),"rules add":i((e,a,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":i((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} reworded`,"rules edit"),"rules remove":i((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} removed`,"rules remove"),"builder start":i(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":i(async(e,[a])=>`progress line on ${await h(e,a)}`,"builder progress"),"builder done":i(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":i(async(e,[a])=>`judgment call recorded on ${await h(e,a)}`,"builder call"),"builder ask":i(async(e,[a],t,{id:s})=>`${await h(e,a)}: asked the orchestrator, ${s}`,"builder ask"),"builder answer":i(async(e,[a,t])=>`${await h(e,a)}: ${t} answered`,"builder answer"),"builder defer":i(async(e,[a,t])=>`${await h(e,a)}: ${t} deferred to the owner`,"builder defer"),"builder verified":i(async(e,[a],t,s)=>`${await h(e,a)}: judgment calls verified${s?.said?`; ${s.said}`:""}`,"builder verified"),land:i(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:i(async(e,[a],t,s)=>`${await h(e,a)}: ${s?.said??"merged"}`,"merge"),"arch file":i(async(e,[a])=>`findings of ${await h(e,a)} filed`,"arch file")};async function fe(e,a={}){try{const{flags:t,rest:s}=oe(e),r=[3,2,1].map(u=>s.slice(0,u).join(" ")).find(u=>y(b,u)),o=y(b,r);if(!o)throw new d(s.length?`unknown command "${s.slice(0,2).join(" ")}"`:"no command given");const[l,c,p]=o,f=s.slice(r.split(" ").length),N=l.at(-1)?.startsWith("..."),B=l.at(-1)?.endsWith("?")?l.length-1:l.length;if(N?f.length<B:f.length!==l.length)throw new d(`${r} 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 A=I?.[0],k=de(r,c,E);ue(c,k);const P=A===void 0?null:await n.siteDir(A),v=await p(P,f,k,a),O=y(me,r),j=O&&await O(P,f,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 n.BoardError)return console.error(t.message),t.exitCode??1;throw t}}if(i(fe,"main"),process.argv[1]&&import.meta.url===L(g.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};
|