@lovek4d/kickflip 0.6.1 → 0.7.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.
Files changed (76) hide show
  1. package/CONTEXT.md +60 -48
  2. package/SKILL.md +20 -14
  3. package/bin/event-lines.mjs +1 -0
  4. package/bin/kickflip.mjs +8 -8
  5. package/bin/wait-events.mjs +2 -2
  6. package/board/agents.mjs +2 -2
  7. package/board/arch-review.mjs +4 -4
  8. package/board/bin/board.mjs +118 -104
  9. package/board/domain.mjs +7 -7
  10. package/board/finish.mjs +4 -4
  11. package/board/headless.mjs +8 -9
  12. package/board/hub-lines.mjs +1 -0
  13. package/board/onboarding.mjs +2 -2
  14. package/board/prototype-page.mjs +1 -1
  15. package/board/public/app-arch.js +3 -3
  16. package/board/public/app-banner.js +1 -1
  17. package/board/public/app-cards.js +17 -9
  18. package/board/public/app-header.js +1 -1
  19. package/board/public/app-lanes.js +39 -61
  20. package/board/public/app-needs.js +116 -30
  21. package/board/public/app-reader.js +54 -44
  22. package/board/public/app-threads.js +2 -2
  23. package/board/public/app.html +1 -1
  24. package/board/public/app.js +23 -13
  25. package/board/public/dialogs.js +1 -1
  26. package/board/public/hub-kit.js +1 -1
  27. package/board/public/hub.css +2 -0
  28. package/board/public/linear-key.js +1 -1
  29. package/board/public/new-task.js +1 -1
  30. package/board/public/progress-review.js +4 -4
  31. package/board/public/prototype-kit.js +27 -16
  32. package/board/public/prototype-switcher.js +47 -32
  33. package/board/public/prototype.html +7 -4
  34. package/board/public/router.js +35 -7
  35. package/board/public/shell.js +19 -13
  36. package/board/public/sidebar.css +4 -4
  37. package/board/public/sidebar.js +2 -2
  38. package/board/public/style.css +74 -36
  39. package/board/public/ui-cards.js +56 -37
  40. package/board/public/ui-enums.js +10 -1
  41. package/board/public/ui-hub.js +1 -5
  42. package/board/public/ui-links.js +6 -6
  43. package/board/public/ui-order.js +8 -6
  44. package/board/public/ui-progress-bar.js +6 -7
  45. package/board/public/ui-questions.js +30 -13
  46. package/board/public/ui-sidebar.js +3 -2
  47. package/board/public/ui-threads.js +20 -19
  48. package/board/public/ui.js +15 -4
  49. package/board/public/view-hub-settings.js +28 -31
  50. package/board/public/view-hub.js +7 -9
  51. package/board/public/view-notes.js +2 -2
  52. package/board/public/view-questions.js +35 -6
  53. package/board/public/view-settings.js +63 -31
  54. package/board/render-check.mjs +10 -10
  55. package/board/review.mjs +1 -1
  56. package/board/server.mjs +18 -20
  57. package/board/stop.mjs +1 -1
  58. package/board/store.mjs +26 -25
  59. package/board/tickets.mjs +1 -1
  60. package/board/transcript.mjs +4 -2
  61. package/hooks-plugin/hooks/board-data-gate.mjs +6 -6
  62. package/hub/awake.mjs +1 -1
  63. package/hub/boards.mjs +4 -4
  64. package/hub/github.mjs +3 -3
  65. package/hub/home.mjs +1 -1
  66. package/hub/hub.mjs +11 -11
  67. package/hub/incoming.mjs +2 -2
  68. package/hub/locks.mjs +1 -1
  69. package/hub/new-board.mjs +2 -2
  70. package/hub/owner-checks.mjs +1 -1
  71. package/hub/try.mjs +6 -7
  72. package/hub/upgrade.mjs +1 -1
  73. package/hub/views.mjs +1 -1
  74. package/package.json +1 -1
  75. package/shared/tailscale.mjs +1 -1
  76. package/shared/usage-gate.mjs +1 -1
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
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>] ...
2
+ var W=Object.defineProperty;var r=(e,a)=>W(e,"name",{value:a,configurable:!0});import{readiness as D}from"../../hub/readiness.mjs";import{readSlack as H,setSlack as L}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 M}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 re=`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 waves live. Without --site, waves come from ~/.claude/waves.
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
8
  item add creates a ticket in the board\u2019s source unless --ticket <id> links one.
@@ -20,27 +20,31 @@ command fails and changes nothing.
20
20
  answered questions (topic item:ask:<eventId>), and one to
21
21
  wave-triage its wave's, noting "Acted on: replied". A
22
22
  reply to overnight-triage ends that triage
23
- thread <wave>/<id> --text <t> [--link <url>] one line in the item's thread; ask:<eventId> in place
24
- [--who claude|love|<name>] [--via github|slack] of <wave>/<id> writes in that ask's thread
23
+ [--title <t>] retitles the ask it answers, or whose thread the message
24
+ is in, which then wears an Ask badge in place of its first
25
+ line as title (data/ask-titles.json)
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
25
28
  A Claude thread line, or a reply to an ask or item-message, rewrites the body of the thread's
26
- queued push (tag ask-<eventId> or item-<wave>/<id>) to the message's first line, keeping
29
+ queued push (tag ask-<eventId> or item-<file>/<id>) to the message's first line, keeping
27
30
  "\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
31
+ visual add <file>/<id> --file <page.html> --title <t> a visual agent's page: copied to public/visuals/<id>/<slug>.html
29
32
  (a new name each time), linked by a visual line in the
30
33
  item's thread; prints the page's link
31
34
  topic add <id> --title <t> [--summary <s>] a question topic in data/owner.json
32
35
  question add --file <json> one question or an array of them:
33
36
  {id, topic, question, options: [{id, label, recommended?, detail?, diagram?, practice?}],
34
- source?, context?, url?, urgent?, round_of?, reask_of?, ask?, suggested?, prototype_gate?}; topic is declared, item:<wave>/<id>, item:ask:<eventId>
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
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
36
39
  a board over ~/.claude/waves. A question on a done card reopens it.
37
40
  A question on an item: topic whose card has a grill or prototype planner running is staged, not published,
38
41
  whoever adds it: it shows nowhere until the planner finishes, fails or is stopped, when every staged question
39
42
  on the card publishes in the order added, with one owner push. The command says "staged" and why.
40
43
  url is http(s), or a site page such as prototype-<topic>.html?q=<id>, stored as given.
41
44
  Prototype options keep take order, ids matching take keys; recommended: true tags the recommendation.
42
- suggested: {option, card, answer} pre-checks the option the owner already chose on an earlier card, with a
43
- "suggested" chip and the line You answered this on <card>: "<answer>"; card is that card's ref, answer their words.
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.
44
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
45
49
  scrolls or fills the screen.
46
50
  diagram, with practice: rows of 1-4 boxes [[{text, tone?, to?}]], tone quiet|info|notice|
@@ -62,18 +66,24 @@ command fails and changes nothing.
62
66
  (reask-request): it resolves that one again, noting "Asked again as <new id>", printed as "resolved: <old id>".
63
67
  ask names a builder ask the session deferred on the item (builder defer): the question puts it to the
64
68
  owner, and while a card's only open questions are such, it waits on them with no Action needed.
65
- The board server pushes the new open questions itself, under the card's tag (item-<wave>/<id>, or
69
+ The board server pushes the new open questions itself, under the card's tag (item-<file>/<id>, or
66
70
  ask-<eventId>) or topic-<topic> off the cards
67
71
  question resolve <id> --note <text> sets resolved and resolved_at; refuses a question on a
68
72
  prototype page until an answer settles its round: a pick, with a note or none, or an answer to
69
73
  an "Is this it?" confirm already on the board. A question already resolved, such as one an
70
74
  answer with a note and no option resolved, is left as it is
71
- question move <id>,<id>,\u2026 --to <wave>/<id> [--block <wave>/<id>,\u2026] merges open questions that ask one thing onto
75
+ question settle <id> --note <text> a question a finished grill left for the orchestrator to screen
76
+ (a screen wait), which the board already answers: it is published resolved "Settled by the
77
+ orchestrator: <text>", and the answer goes into the card's notes. Refuses the prototype question,
78
+ which is the owner's
79
+ question publish <file>/<id> passes the rest of the card's screened questions on to the owner.
80
+ The board server publishes any left unscreened 15 minutes on, with a thread line
81
+ question move <id>,<id>,\u2026 --to <file>/<id> [--block <file>/<id>,\u2026] merges open questions that ask one thing onto
72
82
  one card: the first id moves to that card (topic item:<card>), the others resolve "Duplicate of
73
83
  <card>'s question <first id>". Each --block card, in order, is blocked by the one before it (the
74
84
  first by --to) and related to --to
75
85
  prototype add <topic> --title <t> --take <K>:<label> records a prototype in data/prototypes.json: the board
76
- [--take <K>:<label> \u2026] [--card <wave>/<id>] serves prototype-<topic>.html from its one template, with
86
+ [--take <K>:<label> \u2026] [--card <file>/<id>] serves prototype-<topic>.html from its one template, with
77
87
  the title, a back arrow to --card (else the board), the
78
88
  take pill and a stage. topic: a-z, 0-9 and "-"; K: one
79
89
  capital letter, in take order; a label never starts with
@@ -96,7 +106,7 @@ command fails and changes nothing.
96
106
  prototype list one line per recorded prototype
97
107
  prototype rm <topic> drops the record, keeping its take files; refuses while an
98
108
  open question links it
99
- pr <wave>/<id> one JSON line per PR on the card, in its order: the PR's
109
+ pr <file>/<id> one JSON line per PR on the card, in its order: the PR's
100
110
  repo, state, checks and new events, using the board's
101
111
  github-login and the card's gh_since
102
112
  pr comment <pr> --body <text> posts Claude's words on PR <pr> through gh, a number in the
@@ -106,7 +116,7 @@ command fails and changes nothing.
106
116
  blank line and <!-- claude-bridge --> after, the marker pr
107
117
  reads as Claude's own. Refuses words that already carry
108
118
  either. Prints the post's url
109
- item set <wave>/<id> [--done --text <t> [--force]] edits the item in its wave file; --done closes the card
119
+ item set <file>/<id> [--done --text <t> [--force]] edits the item in its item file; --done closes the card
110
120
  (below). A card has no status: it stores its lane, which
111
121
  a build's start, a handover, a block and a close move,
112
122
  and the board server moves the rest, each with a thread
@@ -128,14 +138,15 @@ command fails and changes nothing.
128
138
  whose thread opens with the ask and its thread, then --text
129
139
  as the item's first line; it leaves a card it was folded into; refuses a card
130
140
  from another ask (use item relate-ask), and one with no
131
- state when --state is missing
132
- [--state ready|grilling|prototyping] what a card from an ask does next; it reads New until set.
141
+ phase when --phase is missing
142
+ [--phase ready|grilling|prototyping] a Todo card's phase: what it does next; it reads New
143
+ until set. Refused on a card in another lane, whose phase
144
+ the board sets, and as --state, its old name.
133
145
  Staging is the board's: it stages a card on the owner's
134
146
  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
147
+ --phase staging. ready refuses a card not staging or
148
+ ready, bar --from-ask on a Quick build ask, whose filing
149
+ is your judgment; and a card with an open question, and
139
150
  a card without --size, a notes file, or the id of each
140
151
  answered question in that file, since a builder starts
141
152
  from it; it resolves those answered questions.
@@ -159,7 +170,7 @@ command fails and changes nothing.
159
170
  [--size small|large] [--model opus|sonnet] what builder pick reads while the hub's Claude model is
160
171
  Auto: a small card gets Claude Sonnet, any other Opus, and
161
172
  --model wins over --size; "" clears either
162
- [--deps <id>,<wave>/<id>] replaces its blocked-by list; --deps "" clears it
173
+ [--deps <id>,<file>/<id>] replaces its blocked-by list; --deps "" clears it
163
174
  [--blocked-until "<condition>"] blocks it until a condition an agent can check, which
164
175
  the orchestrator checks on its passes over the cards;
165
176
  --blocked-until "" clears it, which threads and emits
@@ -169,12 +180,12 @@ command fails and changes nothing.
169
180
  A block on a card In Progress sends it back to Todo,
170
181
  stopping its board-launched builder (refused for a
171
182
  session's own builder); one In Review stays, unlanded
172
- [--parent <wave>/<id>] sets its Parent, refusing itself or a card below it;
183
+ [--parent <file>/<id>] sets its Parent, refusing itself or a card below it;
173
184
  --parent "" clears it
174
- [--related <id>,<wave>/<id>] replaces the cards it is Related to, which both cards
185
+ [--related <id>,<file>/<id>] replaces the cards it is Related to, which both cards
175
186
  show; --related "" clears it, on both
176
- [--relate <id>,<wave>/<id>] adds cards to its Related to, keeping the rest
177
- [--unrelate <id>,<wave>/<id>] takes cards off its Related to, on both cards; neither
187
+ [--relate <id>,<file>/<id>] adds cards to its Related to, keeping the rest
188
+ [--unrelate <id>,<file>/<id>] takes cards off its Related to, on both cards; neither
178
189
  goes with --related
179
190
  [--install <url> [--install-sha <sha>] a device build the owner installs from the card's Install
180
191
  [--install-label <text>] | --clear-install] button, stamped built_at now: itms-services://?action=
@@ -193,34 +204,38 @@ command fails and changes nothing.
193
204
  --pr closes ordinary builders and moves the card to In Review; install builders keep running.
194
205
  A builder's hand-back of work it couldn't land (builder done with judgment calls) keeps the card
195
206
  In Progress, reading "Orchestrator review", until the owning session lands it; it unblocks nothing.
196
- The board server records the owner's card-done in the wave file itself.
207
+ The board server records the owner's card-done in the item file itself.
197
208
  --done moves the card to Done and its ticket to Done: it resolves each of its questions (topic
198
- item:<wave>/<id>) that has an answer, noting "Closed with the card", and prints "resolved: <id>"
209
+ item:<file>/<id>) that has an answer, noting "Closed with the card", and prints "resolved: <id>"
199
210
  for each. It refuses while one has no answer yet; [--force] closes it and resolves that one too.
200
- It prints "unblocked: <wave>/<id>" and appends card-unblocked {key} once per unblocking,
211
+ It prints "unblocked: <file>/<id>" and appends card-unblocked {key} once per unblocking,
201
212
  including held cards: start each unheld card next. It refuses a card the owner reopened: it
202
213
  closes again only on the owner's Done
203
- item add <wave>[/<id>] --title <t> a new card, unstarted; never
204
- [--ticket <id>] [--deps <id>,<wave>/<id>] hand-edit wave.json to add one. Refuses an id the wave has.
214
+ item add <file>[/<id>] --title <t> a new card, unstarted; never
215
+ [--ticket <id>] [--deps <id>,<file>/<id>] hand-edit the item file to add one. Refuses an id the file has.
205
216
  [--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>"
217
+ its item file, read-only: the draft verbatim, or "# <title>"
207
218
  then the --notes text. Refuses a draft that is any card's
208
219
  notes file, live, done or archived, naming that card
209
- [--pick-up-at <time>] holds it until then, as item set's --pick-up-at
220
+ [--pick-up-at <time> | --held] holds it until then, or until resumed, as item set's
210
221
  [--size small|large] [--model opus|sonnet] as item set's
211
- [--parent <wave>/<id>] the card it was spun out of, such as one filed from
222
+ [--parent <file>/<id>] the card it was spun out of, such as one filed from
212
223
  another card's thread, grill or answer: it reads as a
213
224
  follow-up, and its Parent lists it in Children. A card
214
225
  from the + or New task has none
215
- [--state grilling|prototyping] files it at once, as item set's --state; the session's
216
- [--relate <id>,<wave>/<id>] own form, linking it with --relate in the same call.
217
- Never ready: a card reaches ready through staging.
218
- Without --state it reads New and a card-added event
219
- brings in intake triage, which links and files it
226
+ [--phase grilling|prototyping|ready] files it at once, as item set's --phase; the session's
227
+ [--relate <id>,<file>/<id>] own form, linking it with --relate in the same call.
228
+ ready is for a Quick build ask (--from-ask), with --size
229
+ and --notes or --notes-from, since a builder starts from it
230
+ [--from-ask <eventId> --text <t>] files the card from that ask in this one command, as
231
+ item set's --from-ask: no grill starts before the ask is on
232
+ the card, which takes the ask's place in Todo. Needs --phase
233
+ On a board --phase is required, so no card an agent
234
+ adds reads New
220
235
  Without /<id>, the card takes its ticket's id (GH-<n> for issue #n); pass an id only to import a
221
236
  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
237
+ item notes <file>/<id> --heading <h> appends "## <h>" and the draft to the card's notes, making
238
+ (--file <draft.md> | --from <file>/<id>) its <id>-notes.md when it has none; the only way to write
224
239
  notes (--notes-file and --plan are gone). Write the draft
225
240
  outside the board's data; one that is any card's notes
226
241
  file, live, done or archived, is refused, naming that
@@ -229,20 +244,17 @@ command fails and changes nothing.
229
244
  Resolves answered questions still waiting on Claude,
230
245
  noting "Recorded in the card's notes", and prints
231
246
  "resolved: <id>" for each
232
- item relate-ask <eventId> <wave>/<id> --summary <t> folds an ask about an existing item into it: the ask's
247
+ item relate-ask <eventId> <file>/<id> --summary <t> folds an ask about an existing item into it: the ask's
233
248
  card goes, its message and thread show in the item's
234
249
  thread marked Moved, then the summary line. Again with
235
250
  another item moves it there
251
+ [--title <t>] retitles the ask, as reply's --title
236
252
  ask progress <eventId> [--next <text>] keeps an answered ask in In Review, --next as its next
237
253
  line, until the owner's ask-done (the Done beside Reply
238
254
  in its thread) or ask done is newer. Use it when the answer
239
255
  waits on the owner, such as a pick between prototypes
240
256
  ask done <eventId> moves the ask to Done, as the owner's Done button does
241
257
  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
246
258
  ask move <eventId> --to <route> sends a New ask to another board in the hub's manifest:
247
259
  a New card there with the owner's text, attachments and
248
260
  "Moved from <this board>", which wakes its session; here the
@@ -252,47 +264,46 @@ command fails and changes nothing.
252
264
  that isn't New
253
265
  Then sends the queued pushes, each on its own, unless
254
266
  pushes are held (overnight, snoozed, the weekend or off)
255
- lineup create <slug> --title <t> --goal <t> a wave, in the owner's words: a named set of cards in
256
- --phase <wave>/<id>,\u2026 [--phase \u2026] phases, all from one wave file, one --phase per phase
257
- (--note <t> | --file <f>) in build order. Each card is then blocked by every card
258
- of the phase before (its other deps stay), so one
259
- phase's cards build side by side, and the note goes to
260
- lineup-<slug>.md beside the wave file.
261
- Todo shows it as a band of its cards in order, numbered
262
- by phase, its goal on its name, and Notes opens the
263
- goal, the order and the note. Refuses a card already in
264
- another open wave. Prints the order by card label, one
265
- phase's cards by commas and the phases by arrows
266
- lineup add <slug> <wave>/<id> [--after <wave>/<id>] puts a card in the wave as a phase of its own, last
267
- unless --after's phase, and links the phases again
268
- lineup remove <slug> <wave>/<id> takes a card out: it loses its links to the wave's cards,
269
- its phase goes if it held only that card, and the phases
270
- are linked again
271
- lineup set <slug> [--title <t>] [--goal <t>] renames the wave, sets its goal, rewrites its note or
272
- [--note <t> | --file <f>] puts its cards in new phases (--phase per phase, every
273
- [--phase <wave>/<id>,\u2026 \u2026] card once) and links them again. A card's \u22EF in its
274
- band swaps it with the Todo card before or after it,
275
- each taking the other's phase
276
- lineup finish <slug> the wave's band leaves Todo; its cards keep their
267
+ wave create <slug> --title <t> --goal <t> a wave, in the owner's words: a named set of cards from
268
+ --card <file>/<id>[:<file>/<id>,\u2026] [--card \u2026] one item file, one --card per card in build order, each
269
+ (--note <t> | --file <f>) naming after a colon the earlier cards it would conflict
270
+ with (as item set --deps takes them). Each card is then
271
+ blocked by those cards alone (its links outside the wave
272
+ stay), so cards that don't conflict build side by side,
273
+ and the note goes to wave-<slug>.md beside the item file.
274
+ Todo shows it as a band of its cards in order, each with
275
+ the board's blocked-by line, its goal on its name, and
276
+ Notes opens the goal, the order and the note. Refuses a
277
+ card already in another open wave, and a conflict that
278
+ isn't an earlier card of the wave. Prints the order by
279
+ card label, each card with "(after \u2026)" its conflicts
280
+ wave add <slug> <file>/<id> [--after <file>/<id>,\u2026] puts a card in the wave, last, blocked by the earlier
281
+ cards --after names
282
+ wave remove <slug> <file>/<id> takes a card out: it and the wave's cards waiting on it
283
+ lose their links to each other
284
+ wave set <slug> [<file>/<id> --after <file>/<id>,\u2026] rewrites only that card's links to the wave's cards to
285
+ [--title <t>] [--goal <t>] --after's earlier cards ("" clears them), renames the
286
+ [--note <t> | --file <f>] wave, sets its goal or rewrites its note
287
+ wave finish <slug> the wave's band leaves Todo; its cards keep their
277
288
  links. A wave whose cards are all Done leaves it too
278
- A band's Triage sends wave-triage {lineup}, or {wave} for a wave file's band; it reads
289
+ A band's Triage sends wave-triage {wave}, or {file} for an item file's band; it reads
279
290
  Triaging\u2026 until you reply to that event, then "Triage: N open questions" while questions on topic
280
- lineup:<slug> or wave:<slug> are open or answered
291
+ wave:<slug> or file:<slug> are open or answered
281
292
  tickets backfill [--dry-run] gives every open card without a ticket one: the first
282
293
  id in its title (or legacy linear list) from the board's
283
294
  source is linked, else a new ticket is made. Done cards are
284
295
  left alone. Prints each card's result; --dry-run changes and
285
296
  calls nothing
286
297
  reviewers set --file <json> the board's reviewers in board.json, replacing them:
287
- [{name, slack_user, dm, github}], as in wave.json.
288
- The settings page lists them; new waves copy them
298
+ [{name, slack_user, dm, github}], as in an item file.
299
+ The settings page lists them; new item files copy them
289
300
  reviewers add --file <json> one reviewer or an array; replaces the same name
290
301
  reviewers remove <name>... refuses a name the board doesn't have
291
302
  slack Slack identity as JSON; imports slack-bridge config once
292
303
  slack set [--self-dm D..] [--user U..] sets hub Slack identity in ~/.claude/kickflip/slack.json
293
304
  [--domain x] [--timezone Area/City]
294
305
  integrations readiness rows as one JSON line
295
- review updated <wave>/<id> --text "<what changed>" queues \u201CUpdated: <what changed>\u201D in the thread of every
306
+ review updated <file>/<id> --text "<what changed>" queues \u201CUpdated: <what changed>\u201D in the thread of every
296
307
  posted link on the card's PRs; the hub's next poll sends it
297
308
  review due one JSON line of the Slack sends the hub owes, from local state
298
309
  and data/pr-slack.json: {links: [{pr, key, reviewer,
@@ -347,7 +358,7 @@ command fails and changes nothing.
347
358
  pushed, which needs Push main after a land granted.
348
359
  A local board never opens PRs. Without auto-merge, a
349
360
  card with no PR waits in In Review for the owner's Merge
350
- build <wave>/<id> --site <route> asks the hub to run this card's build script; prints
361
+ build <file>/<id> --site <route> asks the hub to run this card's build script; prints
351
362
  --asked "<owner's words>" started and exits. install is an alias. Only the owner's
352
363
  Serve / Build and Auto-build start a card run, so --asked
353
364
  must quote the owner's ask, card message or answer note on
@@ -402,7 +413,7 @@ command fails and changes nothing.
402
413
  {"source":"linear","workspace","team","project"}. A
403
414
  GitHub account runs the board's issue calls as that gh
404
415
  login; without one they run as gh's active account
405
- builder start <wave>/<id> [--agent <name>] [--kind <claude|codex|prototype|grill|install|\u2026>] [--label <who>]
416
+ builder start <file>/<id> [--agent <name>] [--kind <claude|codex|prototype|grill|install|\u2026>] [--label <who>]
406
417
  [--model <opus|sonnet>] the Claude model running the builder, recorded and printed as
407
418
  "model: Claude Sonnet" (kind claude takes builder pick's model;
408
419
  codex prints "model: Codex"). The thread's agent line reads it: "Claude
@@ -442,7 +453,7 @@ command fails and changes nothing.
442
453
  on a board makes it without --worktree when it keeps none
443
454
  (a taken-up or resumed record's, or the worktree the
444
455
  card's last record left while it is still there)
445
- builder progress <wave>/<id> --phase <phase> one status line: a phase, reading, building, testing,
456
+ builder progress <file>/<id> --phase <phase> one status line: a phase, reading, building, testing,
446
457
  (<detail> | --text <d>) [--agent <name>] checking or landing, and a present-tense detail of at most
447
458
  80 characters in plain words, which the owner reads at a
448
459
  glance; a longer detail is refused with its length. Hashes,
@@ -453,7 +464,7 @@ command fails and changes nothing.
453
464
  A progress line, --progress included, needs --site: it is a status line in the item's thread, and
454
465
  the card's status shows the newest one from an agent still working. builder progress refuses
455
466
  without an open record for that agent, as builder call does
456
- builder done <wave>/<id> [--agent <name>] marks that agent's record done; the item's thread
467
+ builder done <file>/<id> [--agent <name>] marks that agent's record done; the item's thread
457
468
  [--next <t>] [--text <what you checked>] shows when it started and finished. On a
458
469
  record with unverified judgment calls it hands the card
459
470
  back: its records close marked handed back, with Next "Answer the builder's
@@ -470,7 +481,8 @@ command fails and changes nothing.
470
481
  refuses with the reason and leaves the record open; one
471
482
  refused while another agent's record is open names it.
472
483
  A helper's done takes --text, its hand-back, which agent
473
- wait prints.
484
+ wait prints. A grill's or prototype's done takes
485
+ --text, what it asked, as the run's closing thread line.
474
486
  --next stops a build without finishing it. Closing the
475
487
  last record of a card left In Progress with no Next is
476
488
  refused without --next, which sets it
@@ -493,8 +505,8 @@ command fails and changes nothing.
493
505
  changed>\u201D in each link's thread, as review updated does;
494
506
  none sends nothing, for a change only to comments, names,
495
507
  docs or tests
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,
508
+ builder takes --dir <dir> for an item file in <dir>; its records go in <dir>/agents
509
+ builder stop <file>/<id> the orchestrator's stop of the card's run the board launched,
498
510
  its headless builder, lander or rebase run, with every process
499
511
  under it: the record stays open, marked stopped, so the server
500
512
  relaunches nothing and the card stays In Progress reading
@@ -502,7 +514,7 @@ command fails and changes nothing.
502
514
  waiting on the orchestrator; the same words go in its thread.
503
515
  Prints what it stopped, one process a line. Refuses a planner,
504
516
  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
517
+ item stall <file>/<id> --reason "<why>" the orchestrator's flag on a stale card it can't restart: the
506
518
  card stays in its lane reading red "stalled", an Action needed
507
519
  "stalled: <why>" with one push, waiting on nobody else; its
508
520
  run stops as builder stop stops it (nothing to stop when it
@@ -510,7 +522,7 @@ command fails and changes nothing.
510
522
  stays, and it frees its implementer slot. Its thread reads
511
523
  "stalled by orchestrator: <why>". A message on the card
512
524
  clears it
513
- land stop <wave>/<id> the orchestrator's stop of the card's land and its suite: one a
525
+ land stop <file>/<id> the orchestrator's stop of the card's land and its suite: one a
514
526
  headless builder or lander runs stops that agent as builder
515
527
  stop does; the land a held card's Merge tap started stops
516
528
  alone, leaving the land queue, and the card's Merge shows
@@ -519,28 +531,28 @@ command fails and changes nothing.
519
531
  didn't launch
520
532
  land queue the host's land queue in order: the land running, then each
521
533
  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
534
+ item bump <file>/<id> the card's bump, as the owner's bump icon sends it (card-bump;
535
+ item unbump <file>/<id> card-unbump). A bumped card's land goes ahead in the land queue
524
536
  of every land not bumped since, the latest bump first, without
525
537
  stopping the one running; a bump of a queued land says so in
526
538
  its thread ("land moved first by orchestrator")
527
- lander <wave>/<id> relaunch the headless lander of a card it stopped on (a usage
539
+ lander <file>/<id> relaunch the headless lander of a card it stopped on (a usage
528
540
  pause ends one): its open record closes as handed back and
529
541
  the server launches a fresh lander; refused unless a
530
542
  builder record on the card is open
531
- builder pick <wave>/<id> who builds the card, from the hub's Codex share and the pause
543
+ builder pick <file>/<id> who builds the card, from the hub's Codex share and the pause
532
544
  line, and for claude which model, from the hub's Claude model
533
545
  and the card's --size and --model: "codex: <reason>" or
534
546
  "claude sonnet: <reason>". Pass that kind to builder start
535
547
  --kind, and launch that model
536
- audit [<wave>/<id>] the board's audit log of the last 30 days, data/audit.jsonl:
548
+ audit [<file>/<id>] the board's audit log of the last 30 days, data/audit.jsonl:
537
549
  each call the code made on its own (builder-pick, pull,
538
550
  round-planner, launch-held, arrival-planner, relaunch,
539
551
  lander, triage, pr-review, push, pick-up, auto-merge,
540
552
  ready-draft, session-resume, compact) as "<at> <call>
541
553
  [<key>]: <result>. <reason>" and the inputs it read; with
542
554
  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
555
+ agent launch <file>/<id> --name <n> --task <t> any session's helper on an open card, an agent the board server
544
556
  [--writes] [--model <opus|sonnet>] launches headless with the task as its brief, under its own
545
557
  name (lowercase, "-", not main or visual-\u2026), its record
546
558
  <id>@<name> requested until then. A read-only helper launches
@@ -550,36 +562,38 @@ command fails and changes nothing.
550
562
  and works in its own worktree, branch <id>-<name> from the
551
563
  card's, so it needs the card's worktree. --model else
552
564
  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
565
+ agent wait <file>/<id> --name <n> [--agent <me>] blocks until the helper's builder done, then prints its
554
566
  [--timeout <minutes>] --text and, for one that writes, its branch. Meanwhile
555
567
  --agent's record (main by default) takes no slot. Refuses
556
568
  after --timeout (9) minutes, saying whether it runs or waits
557
569
  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
570
+ agent merge <file>/<id> --name <n> merges a done helper's branch into the card's worktree and
559
571
  removes the helper's worktree and branch; on a conflict it
560
572
  names the files: resolve, commit and run it again
561
- builder call <wave>/<id> <judgment call> appends one line to the agent's open builder record: a
573
+ builder call <file>/<id> <judgment call> appends one line to the agent's open builder record: a
562
574
  [--agent <name>] judgment call as the builder makes it, or a "Question: \u2026"
563
575
  for the session; refused with no open record. A builder start
564
576
  on a record with unverified calls resumes that build, keeping
565
577
  its calls and worktree. The thread shows them, collapsed
566
- builder calls <wave>/<id> [--agent <name>] prints the record's calls, numbered, then "verified <time>"
578
+ builder calls <file>/<id> [--agent <name>] prints the record's calls, numbered, then "verified <time>"
567
579
  or "not verified yet"
568
- builder ask <wave>/<id> <question> [--agent <name>] a running builder's one-line question for the board's session,
569
- which it asks at any point and carries on meanwhile: a thread
580
+ builder ask <file>/<id> <question> [--agent <name>] a running builder's one-line question for the board's session,
581
+ which it asks only when blocked (every way forward would likely
582
+ be wasted work), then waits with builder asks --wait: a thread
570
583
  line named by its model ("Claude Opus builder"), never unread,
571
584
  and a builder-ask {key, ask, agent, text} event. Prints its
572
585
  id, ask<n>. Needs --site and an open builder record
573
- builder answer <wave>/<id> <ask> --text <t> the session's answer to that ask, a thread line named
586
+ builder answer <file>/<id> <ask> --text <t> the session's answer to that ask, a thread line named
574
587
  Orchestrator that leaves the card quiet
575
- builder defer <wave>/<id> <ask> --text <t> the session's deferral of an ask it can't answer confidently:
588
+ builder defer <file>/<id> <ask> --text <t> the session's deferral of an ask it can't answer confidently:
576
589
  the same thread line, and a "Question: <ask> (deferred to the
577
590
  owner)" call on the asking agent's record. After its review the
578
591
  session puts it to the owner as a card question with
579
592
  "ask": "<ask>"; builder verified refuses until then
580
- builder asks <wave>/<id> prints each ask, then its answer, its deferral or "waiting on
581
- the orchestrator"
582
- builder verified <wave>/<id> [--agent <name>] the session's mark that it checked the record's calls; land
593
+ builder asks <file>/<id> [--wait] [--timeout <min>] prints each ask, then its answer, its deferral or "waiting on
594
+ the orchestrator"; --wait blocks until none is waiting, and
595
+ after --timeout (9) minutes says so and fails: run it again
596
+ builder verified <file>/<id> [--agent <name>] the session's mark that it checked the record's calls; land
583
597
  [--note <t>] refuses while any record on the card has a call after it.
584
598
  On a handed-back build (not done, no builder open, every
585
599
  call verified) it finishes the card in the same step, as
@@ -587,7 +601,7 @@ command fails and changes nothing.
587
601
  calls verified."). While a question on the card is open it
588
602
  says so and the card waits; a finish that fails keeps the
589
603
  mark, the card handed back, and says why
590
- land <wave>/<id> --text <t> [--repo <dir>] brings the worktree its builder record keeps (else
604
+ land <file>/<id> --text <t> [--repo <dir>] brings the worktree its builder record keeps (else
591
605
  [--dir <dir>]
592
606
  ~/.claude/kickflip/worktrees/<id> of --repo) into the board's
593
607
  branch (the one its builders branch from, else main), one
@@ -612,7 +626,7 @@ command fails and changes nothing.
612
626
  landed card in In Review on that step until the owner's
613
627
  Done, or until its --check passes. KICKFLIP_RESTART_CMD='["<cmd>"]' replaces
614
628
  the restarts, called with hub or board <route>
615
- merge <wave>/<id> [--after-approval "<why trivial>"] merges the card's PRs (gh pr merge --squash) in its order and
629
+ merge <file>/<id> [--after-approval "<why trivial>"] merges the card's PRs (gh pr merge --squash) in its order and
616
630
  [--confirmed "<owner's words>"] records it merged, once GitHub reads every open one approved,
617
631
  its checks green with none pending and its merge state CLEAN,
618
632
  the test the Merge button uses; a merged or closed PR is left
@@ -626,10 +640,10 @@ command fails and changes nothing.
626
640
  it is now, reruns the tests, fast-forwards, closes the card
627
641
  and pushes when Push main is on; a conflict or failing
628
642
  tests launch a headless lander that lands it
629
- arch file <wave>/<id> turns the architecture review audit card's answered findings
643
+ arch file <file>/<id> turns the architecture review audit card's answered findings
630
644
  into cards in the review's wave: titled with the picked
631
645
  option's label, its detail, practice and the owner's note as
632
- notes, each after the wave's last card, its question resolved
646
+ notes, each after the audit card, its question resolved
633
647
  "Filed as <ref>."; a Skip pick resolves its question alone.
634
648
  Prints "filed: <ref> from <id>", "skipped: <id>", and
635
649
  "waiting: <ids>" while any are unanswered; once none is, it
@@ -649,4 +663,4 @@ command fails and changes nothing.
649
663
  (overnight, snoozed or the weekend). --urgent: a
650
664
  production fault only; it beats those holds. While
651
665
  notifications are off every push is dropped, urgent too
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};
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 ie=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 oe(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(ie.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}}r(oe,"parse");function de(e,a,t){const s=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||s.has(l))return[l,c];if(c.length>1)throw new d(`--${l} is given twice`);return[l,c[0]]}))}r(de,"flagsOf");const le=["html","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}}r(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`)}r(ue,"checkInputs");const w=r(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}`)}}r(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}`)}r(C,"refuseNotesFile");function _(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(_,"noteOf");async function pe(e,a,{"owner-step":t,check:s,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(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(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 $(e,a,{text:i,ownerStep:t})}if(l&&await n.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 n.stopBuilder(e,a,{...o,note:i})}if(l&&i!==void 0&&await n.isPlanner(e,a,o.agent))return n.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 n.stopBuilder(e,a,{...o,requireNext:!0})}r(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"],["html","file","title"],(e,[a],t)=>{if(t.html===void 0==(t.file===void 0))throw new d("reply takes --html or --file");return n.writeReply(e,a,t.html??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","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,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,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},s))],"item add":[["file[/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,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,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)],"tickets backfill":[[],["dry-run"],(e,a,t,s)=>n.backfillTickets(e,{dryRun:t["dry-run"]},s)],"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)})],"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:_(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 L(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 D.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 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 n.BoardError(o.error||"The hub refused the build.");return{lines:["started",...(await n.appendMessage(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":[["file/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 d("--repo goes with --worktree");const l=r(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:i?"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 i=e&&!s.dir;if(i&&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 i&&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:i}=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(!i)throw new n.BoardError(`${a} has no worktree to merge helper ${t.name} into`);return G(i,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(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:[["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 i=e?await n.cardName(e,a):void 0,o=await J(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 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:i}),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 ${i??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}r(m,"needSite");const h=r((e,a)=>n.cardName(e,a),"card"),R=r((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),N=r(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&&"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(we,"itemChanges");const me={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))],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")} ${i===s.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:s})=>`${R(s,"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)}: ${we(t)}`,"item set"),"item add":r(async(e,a,t,{key:s})=>`${await h(e,s)} added as ${s}`,"item add"),"item notes":r(async(e,[a],t,{file:s,heading:i})=>`${await h(e,a)}: notes section "${i}" added to ${s}`,"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"),"wave create":r((e,[a])=>`wave ${a} created`,"wave create"),"wave add":r(async(e,[a,t])=>`${await h(e,t)} added to wave ${a}`,"wave add"),"wave remove":r(async(e,[a,t])=>`${await h(e,t)} taken out of wave ${a}`,"wave remove"),"wave set":r((e,[a])=>`wave ${a} updated`,"wave set"),"wave finish":r((e,[a])=>`wave ${a} finished`,"wave 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,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":`${N(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??`${N(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 fe(e,a={}){try{const{flags:t,rest:s}=oe(e),i=[3,2,1].map(u=>s.slice(0,u).join(" ")).find(u=>y(b,u)),o=y(b,i);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(i.split(" ").length),T=l.at(-1)?.startsWith("..."),B=l.at(-1)?.endsWith("?")?l.length-1:l.length;if(T?f.length<B:f.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=de(i,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,i),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(r(fe,"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(re):process.exitCode=await fe(e)}export{re as USAGE,fe as main};