@lovek4d/kickflip 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CONTEXT.md +105 -68
  2. package/README.md +1 -1
  3. package/SKILL.md +27 -21
  4. package/bin/kickflip.mjs +27 -18
  5. package/bin/wait-events.mjs +2 -2
  6. package/board/agents.mjs +2 -0
  7. package/board/arch-review.mjs +4 -4
  8. package/board/bin/board.mjs +199 -79
  9. package/board/build-script.mjs +2 -1
  10. package/board/domain.mjs +9 -7
  11. package/board/finish.mjs +4 -4
  12. package/board/headless.mjs +9 -4
  13. package/board/on-host.mjs +1 -1
  14. package/board/onboarding.mjs +11 -0
  15. package/board/prototype-base.mjs +1 -0
  16. package/board/prototype-page.mjs +1 -0
  17. package/board/public/app-arch.js +1 -1
  18. package/board/public/app-asks.js +6 -6
  19. package/board/public/app-banner.js +7 -13
  20. package/board/public/app-cards.js +75 -37
  21. package/board/public/app-core.js +26 -8
  22. package/board/public/app-header.js +8 -8
  23. package/board/public/app-lanes.js +67 -58
  24. package/board/public/app-needs.js +140 -10
  25. package/board/public/app-reader.js +1 -1
  26. package/board/public/app-snapshot.js +46 -72
  27. package/board/public/app-threads.js +78 -75
  28. package/board/public/app.html +9 -4
  29. package/board/public/app.js +114 -38
  30. package/board/public/attach.js +2 -2
  31. package/board/public/dialogs.js +9 -39
  32. package/board/public/hub-kit.js +66 -4
  33. package/board/public/hub.css +2 -0
  34. package/board/public/linear-key.js +46 -35
  35. package/board/public/new-task.js +20 -39
  36. package/board/public/progress-review.html +46 -0
  37. package/board/public/progress-review.js +158 -0
  38. package/board/public/prototype-kit.js +137 -7
  39. package/board/public/prototype-switcher.js +152 -123
  40. package/board/public/prototype.html +57 -3
  41. package/board/public/router.js +7 -6
  42. package/board/public/shell.js +68 -26
  43. package/board/public/sidebar.css +1 -0
  44. package/board/public/sidebar.js +16 -30
  45. package/board/public/style.css +205 -83
  46. package/board/public/ui-cards.js +91 -65
  47. package/board/public/ui-enums.js +1 -1
  48. package/board/public/ui-header.js +27 -18
  49. package/board/public/ui-hub.js +12 -1
  50. package/board/public/ui-links.js +7 -7
  51. package/board/public/ui-order.js +12 -10
  52. package/board/public/ui-page-header.js +19 -7
  53. package/board/public/ui-patch.js +7 -33
  54. package/board/public/ui-progress-bar.js +28 -0
  55. package/board/public/ui-questions.js +47 -52
  56. package/board/public/ui-sidebar.js +34 -26
  57. package/board/public/ui-snapshot.js +43 -52
  58. package/board/public/ui-threads.js +59 -30
  59. package/board/public/ui.js +125 -18
  60. package/board/public/view-hub-settings.js +22 -67
  61. package/board/public/view-hub.js +19 -4
  62. package/board/public/view-notes.js +27 -7
  63. package/board/public/view-questions.js +205 -240
  64. package/board/public/view-settings.js +313 -76
  65. package/board/render-check.mjs +10 -0
  66. package/board/review.mjs +1 -1
  67. package/board/server.mjs +19 -20
  68. package/board/stop.mjs +1 -0
  69. package/board/store.mjs +29 -15
  70. package/board/sync.mjs +2 -0
  71. package/board/tickets.mjs +3 -3
  72. package/board/transcript.mjs +2 -0
  73. package/board/worktree.mjs +25 -20
  74. package/hooks-plugin/hooks/board-data-gate.mjs +50 -0
  75. package/hooks-plugin/hooks/hooks.json +9 -0
  76. package/hooks-plugin/hooks/usage-gate.mjs +20 -3
  77. package/hub/board-repo.mjs +1 -1
  78. package/hub/boards.mjs +4 -4
  79. package/hub/front-door.mjs +5 -5
  80. package/hub/github.mjs +3 -3
  81. package/hub/hub.mjs +11 -11
  82. package/hub/incoming.mjs +2 -2
  83. package/hub/linear.mjs +1 -2
  84. package/hub/live.mjs +2 -2
  85. package/hub/locks.mjs +1 -1
  86. package/hub/owner-checks.mjs +1 -1
  87. package/hub/pr-report.mjs +2 -2
  88. package/hub/presence.mjs +1 -1
  89. package/hub/push.mjs +2 -2
  90. package/hub/readiness.mjs +1 -2
  91. package/hub/review-send.mjs +1 -0
  92. package/hub/secrets.mjs +2 -0
  93. package/hub/self-restart.mjs +2 -0
  94. package/hub/sessions.mjs +2 -2
  95. package/hub/share.mjs +1 -0
  96. package/hub/slack.mjs +2 -2
  97. package/hub/stream.mjs +10 -13
  98. package/hub/try.mjs +7 -7
  99. package/hub/upgrade.mjs +1 -1
  100. package/hub/usage.mjs +3 -3
  101. package/hub/views.mjs +1 -1
  102. package/package.json +2 -1
  103. package/shared/audit.mjs +4 -0
  104. package/shared/card-prs.mjs +1 -1
  105. package/shared/fake-gh.mjs +17 -4
  106. package/shared/machine-gate.mjs +2 -0
  107. package/shared/prototype-frame.js +1 -0
  108. package/shared/service.mjs +4 -4
  109. package/shared/usage-gate.mjs +1 -1
  110. package/board/public/ui-phase-bars.js +0 -17
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var E=Object.defineProperty;var r=(e,s)=>E(e,"name",{value:s,configurable:!0});import{readiness as B,reportSlack as H}from"../../hub/readiness.mjs";import{readSlack as L,setSlack as D}from"../../hub/slack.mjs";import{routeRecord as y}from"../../hub/routes.mjs";import m from"node:fs";import g from"node:path";import{pathToFileURL as M}from"node:url";import*as n from"../store.mjs";import{boardWorktree as W,builderRepo as G,joiningLand as z,landInTurn as J,worktreesDir as F}from"../worktree.mjs";import{fileFindings as U}from"../arch-review.mjs";import{finishCard as x,landHeld as K}from"../finish.mjs";import{ITEM_STATUSES as Q}from"../domain.mjs";import{boardAt as V,readManifest as X}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";const Y=`usage: node board.mjs <command> [--site <route>] ...
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 g 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 S,queueLines as Y,stopBuilder as Z,stopForBlock 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
5
  board.json says where the board's waves live. Without --site, waves come from ~/.claude/waves.
@@ -9,8 +9,8 @@ item add creates a ticket in the board\u2019s source unless --ticket <id> links
9
9
  Internal defaults to INT; GitHub and Linear create an issue from the title and notes, Linear in the board's team and
10
10
  project through Hub's Linear key. A linked ticket must exist in the source; an Internal one no other card holds.
11
11
  Asks have no ticket; item set --from-ask creates one when the card has none.
12
- A card moves its ticket one way: --status building or fixing \u2192 In Progress, --pr \u2192 In Review, --status done or
13
- merged \u2192 Done (a GitHub issue closes), only for a ticket assigned to the owner or created by kickflip. A ticket that
12
+ A card moves its ticket one way: a build's start \u2192 In Progress, --pr, --pushed or a verified merge-ready build \u2192 In
13
+ Review, --done \u2192 Done (a GitHub issue closes), only for a ticket assigned to the owner or created by kickflip. A ticket that
14
14
  can't move prints one "ticket <id>: \u2026" line; the card's change still stands.
15
15
  Linear needs Hub's Linear key: settings set tickets refuses it without one, and once the key is removed the hub
16
16
  moves every Linear board to internal tickets. A card is never left without a ticket: if Linear can't make one, the
@@ -25,12 +25,18 @@ command fails and changes nothing.
25
25
  A Claude thread line, or a reply to an ask or item-message, rewrites the body of the thread's
26
26
  queued push (tag ask-<eventId> or item-<wave>/<id>) to the message's first line, keeping
27
27
  "\xB7 and N replies", and prints the new body. It never pushes: the thread's unread mark carries it
28
+ visual add <wave>/<id> --file <page.html> --title <t> a visual agent's page: copied to public/visuals/<id>/<slug>.html
29
+ (a new name each time), linked by a visual line in the
30
+ item's thread; prints the page's link
28
31
  topic add <id> --title <t> [--summary <s>] a question topic in data/owner.json
29
32
  question add --file <json> one question or an array of them:
30
33
  {id, topic, question, options: [{id, label, recommended?, detail?, diagram?, practice?}],
31
- source?, context?, url?, urgent?, round_of?, reask_of?, ask?, suggested?}; topic is declared, item:<wave>/<id>, item:ask:<eventId>
34
+ source?, context?, url?, urgent?, round_of?, reask_of?, ask?, suggested?, prototype_gate?}; topic is declared, item:<wave>/<id>, item:ask:<eventId>
32
35
  for a New card not sorted yet, lineup:<slug> for a wave's triage, or wave:<slug> for a wave file's triage on
33
- a board over ~/.claude/waves. A question on a done or merged card reopens it.
36
+ a board over ~/.claude/waves. A question on a done card reopens it.
37
+ A question on an item: topic whose card has a grill or prototype planner running is staged, not published,
38
+ whoever adds it: it shows nowhere until the planner finishes, fails or is stopped, when every staged question
39
+ on the card publishes in the order added, with one owner push. The command says "staged" and why.
34
40
  url is http(s), or a site page such as prototype-<topic>.html?q=<id>, stored as given.
35
41
  Prototype options keep take order, ids matching take keys; recommended: true tags the recommendation.
36
42
  suggested: {option, card, answer} pre-checks the option the owner already chose on an earlier card, with a
@@ -41,12 +47,17 @@ command fails and changes nothing.
41
47
  danger|success, to the next row's box indexes (default all); practice: ["<line>", \u2026]
42
48
  A prototype url's topic must be recorded (prototype add), its option ids a non-empty subset of the
43
49
  take keys in take order, without repeats, and every take file written; a hand-written prototype-*.html
44
- is refused. On an item: topic, each other question answered and still waiting on Claude is
50
+ is refused. Each take is then loaded headlessly at 390px and 1280px, and the question is refused,
51
+ naming the take and width, if one throws, logs a console error or draws an empty stage.
52
+ On an item: topic, each other question answered and still waiting on Claude is
45
53
  resolved, noting "Followed by <new id>", and printed as "resolved: <old id>". Any other unresolved
46
54
  question on the same topic that links the same prototype is resolved, noting "Replaced by <new id>",
47
55
  and printed as "replaced: <old id>". round_of names the question on the same topic whose
48
56
  prototype-request this answers; the question needs a prototype url, takes the next round
49
57
  number, and resolves that one, noting "Followed by prototype round N".
58
+ prototype_gate: true marks a grill's closing "Prototype round first?" question (options yes, no, on a card's
59
+ topic): the owner's yes moves the card to prototyping and starts its prototype planner, a no stages it
60
+ once no question on it is left open, the board doing both with no orchestrator step.
50
61
  reask_of names a resolved question on the same topic that the owner asked to ask again
51
62
  (reask-request): it resolves that one again, noting "Asked again as <new id>", printed as "resolved: <old id>".
52
63
  ask names a builder ask the session deferred on the item (builder defer): the question puts it to the
@@ -57,6 +68,10 @@ command fails and changes nothing.
57
68
  prototype page until an answer settles its round: a pick, with a note or none, or an answer to
58
69
  an "Is this it?" confirm already on the board. A question already resolved, such as one an
59
70
  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
72
+ one card: the first id moves to that card (topic item:<card>), the others resolve "Duplicate of
73
+ <card>'s question <first id>". Each --block card, in order, is blocked by the one before it (the
74
+ first by --to) and related to --to
60
75
  prototype add <topic> --title <t> --take <K>:<label> records a prototype in data/prototypes.json: the board
61
76
  [--take <K>:<label> \u2026] [--card <wave>/<id>] serves prototype-<topic>.html from its one template, with
62
77
  the title, a back arrow to --card (else the board), the
@@ -67,6 +82,17 @@ command fails and changes nothing.
67
82
  public/prototypes/<topic>/<K>.js, which exports
68
83
  render(stage, kit) (board/public/prototype-kit.js lists kit).
69
84
  --card starts that card: it leaves Todo
85
+ [--served <path> [--state <key>:<label> \u2026]] a served round: no take files; the template frames <path>
86
+ from the board's base prototype worktree (prototype base),
87
+ passing ?variant=<K>&state=<key>, then each switch by
88
+ message (shared/prototype-frame.js). The viewer bar lists
89
+ the states; key: a-z, 0-9 and "-"
90
+ prototype base makes the board's base prototype worktree, a detached
91
+ checkout of its repo's main, or moves it to main's tip,
92
+ keeping the rounds' pages; prints its path, whether its
93
+ build script is there and the listener a page loads.
94
+ The hub serves it while an open question links a served
95
+ round (docs/adr/2026-11-06-1500-a-prototype-round-is-a-page-in-the-boards-base-worktree.md)
70
96
  prototype list one line per recorded prototype
71
97
  prototype rm <topic> drops the record, keeping its take files; refuses while an
72
98
  open question links it
@@ -80,9 +106,12 @@ command fails and changes nothing.
80
106
  blank line and <!-- claude-bridge --> after, the marker pr
81
107
  reads as Claude's own. Refuses words that already carry
82
108
  either. Prints the post's url
83
- item set <wave>/<id> [--status <s> | --clear-status] edits the item in its wave file
84
- [--next <t> | --clear-next] status: ${Q.join(", ")}
85
- an empty --next "" clears it, as --clear-next does
109
+ item set <wave>/<id> [--done --text <t> [--force]] edits the item in its wave file; --done closes the card
110
+ (below). A card has no status: it stores its lane, which
111
+ a build's start, a handover, a block and a close move,
112
+ and the board server moves the rest, each with a thread
113
+ line; --status is refused
114
+ [--next <t> | --clear-next] an empty --next "" clears it, as --clear-next does
86
115
  naming another unfinished card requires it in --deps
87
116
  [--pr <n> | --pr <owner/repo#n>]... adds a PR to the card, a bare number in the board's repo;
88
117
  repeatable, and never replaces one. Each PR gets its own
@@ -92,7 +121,7 @@ command fails and changes nothing.
92
121
  on the owner's merge. Push it first (git push origin
93
122
  <branch>). The hub's repo fetch moves it to Done once the
94
123
  branch is merged into the board's branch or gone from the
95
- remote. Clears building/fixing as --pr does
124
+ remote. Moves it to In Review as --pr does
96
125
  [--gh-since <ISO time>] records the last handled GitHub event, an ISO-8601 UTC
97
126
  time such as 2026-10-23T11:00:00Z, stored as given
98
127
  [--from-ask <eventId> --text <t>] the ask it came from: its New card gives way to this item,
@@ -101,13 +130,20 @@ command fails and changes nothing.
101
130
  from another ask (use item relate-ask), and one with no
102
131
  state when --state is missing
103
132
  [--state ready|grilling|prototyping] what a card from an ask does next; it reads New until set.
104
- ready refuses while the card has an open question, and
133
+ Staging is the board's: it stages a card on the owner's
134
+ Send, Implement or Reply on a ready card, and refuses
135
+ --state staging, bar --from-ask on a Quick build ask,
136
+ which files it staging for the orchestrator to judge.
137
+ ready refuses a card not staging or ready, a Quick
138
+ build ask's included, a card with an open question, and
105
139
  a card without --size, a notes file, or the id of each
106
140
  answered question in that file, since a builder starts
107
- from it; it resolves those answered questions
141
+ from it; it resolves those answered questions.
142
+ grilling or prototyping on a staging or ready card sends
143
+ it back, and the board starts that round's planner
108
144
  [--held | --unheld | --pick-up-at <time>] holds or resumes the card on the owner's words; the board
109
145
  server records the owner's own Hold and Resume taps. A later
110
- --held/--unheld overrides a tap; --unheld also clears a held status.
146
+ --held/--unheld overrides a tap.
111
147
  --pick-up-at holds it until an ISO time such as
112
148
  2026-11-04T09:00 (the Mac's local time without an
113
149
  offset; a past time is refused), stored as UTC
@@ -131,39 +167,47 @@ command fails and changes nothing.
131
167
  Claude, noting "Recorded in the card's notes", and
132
168
  prints "resolved: <id>" for each
133
169
  [--deps <id>,<wave>/<id>] replaces its blocked-by list; --deps "" clears it
170
+ [--blocked-until "<condition>"] blocks it until a condition an agent can check, which
171
+ the orchestrator checks on its passes over the cards;
172
+ --blocked-until "" clears it, which threads and emits
173
+ card-unblocked {key} once nothing else blocks it
174
+ [--block-reason "<reason>"] why it is blocked, by its deps or its condition, which
175
+ goes when the block does; --block-reason "" clears it
176
+ A block on a card In Progress sends it back to Todo,
177
+ stopping its board-launched builder (refused for a
178
+ session's own builder); one In Review stays, unlanded
134
179
  [--parent <wave>/<id>] sets its Parent, refusing itself or a card below it;
135
180
  --parent "" clears it
136
181
  [--related <id>,<wave>/<id>] replaces the cards it is Related to, which both cards
137
182
  show; --related "" clears it, on both
183
+ [--relate <id>,<wave>/<id>] adds cards to its Related to, keeping the rest
184
+ [--unrelate <id>,<wave>/<id>] takes cards off its Related to, on both cards; neither
185
+ goes with --related
138
186
  [--install <url> [--install-sha <sha>] a device build the owner installs from the card's Install
139
187
  [--install-label <text>] | --clear-install] button, stamped built_at now: itms-services://?action=
140
188
  download-manifest&url=https://\u2026/manifest.plist for iOS,
141
189
  an https:// .apk URL for Android. --install-sha is the
142
190
  head it was built from: the badge reads "Install (stale)"
143
191
  once the branch moves past it. Hidden once the card is Done
144
- [--text <t>] with --status done or dropped, required on a site: the
145
- card's closing line, written last in its thread. merged
146
- writes "Merged PR #n." unless --text says otherwise
147
- builder start for a build sets building itself. --status building resolves the card's answered
148
- questions, noting "Acted on: build started".
149
- --status building refuses while the item is held or a dep isn't done, merged or dropped,
150
- and while the newest owner answer to its questions that link a prototype-*.html page picks
151
- no take, which leaves the card prototyping with Next "Prototype again" until the next
152
- round's pick; a pick's note is the build's guidance;
153
- done, merged and dropped print "unblocked: <wave>/<id>" and append card-unblocked {key}
154
- once per unblocking, including held cards: start each unheld card next.
155
- building and fixing count as working only while a builder record on the card is open.
156
- --pr clears building/fixing and closes ordinary builders; install builders keep running.
157
- checking is a builder's hand-back of work it couldn't land: In Progress, reading "Orchestrator
158
- review", until the owning session takes it on; it unblocks nothing.
192
+ [--text <t>] with --done, required on a site: the card's closing line,
193
+ written last in its thread, saying why it closed (merged,
194
+ dropped, superseded)
195
+ builder start for a build moves the card to In Progress with "Moved to In Progress: a build
196
+ started", resolving its answered questions, noting "Acted on: build started". It refuses while
197
+ the item is held or a dep isn't done, and while the newest owner answer to its questions that
198
+ link a prototype-*.html page picks no take, which leaves the card prototyping with Next
199
+ "Prototype again" until the next round's pick; a pick's note is the build's guidance.
200
+ --pr closes ordinary builders and moves the card to In Review; install builders keep running.
201
+ A builder's hand-back of work it couldn't land (builder done with judgment calls) keeps the card
202
+ In Progress, reading "Orchestrator review", until the owning session lands it; it unblocks nothing.
159
203
  The board server records the owner's card-done in the wave file itself.
160
- Every --status or --clear-status records status_at; a later status change overrides the tap.
161
- --status done, merged or dropped closes the card: it resolves each of its questions (topic
204
+ --done moves the card to Done and its ticket to Done: it resolves each of its questions (topic
162
205
  item:<wave>/<id>) that has an answer, noting "Closed with the card", and prints "resolved: <id>"
163
206
  for each. It refuses while one has no answer yet; [--force] closes it and resolves that one too.
164
- --status done refuses a done card the owner messaged since it closed: it reads reopened, and closes
165
- again only on the owner's Done
166
- item add <wave>[/<id>] --title <t> [--notes <t>] a new card, planned (waiting when it has --deps); never
207
+ It prints "unblocked: <wave>/<id>" and appends card-unblocked {key} once per unblocking,
208
+ including held cards: start each unheld card next. It refuses a card the owner reopened: it
209
+ closes again only on the owner's Done
210
+ item add <wave>[/<id>] --title <t> [--notes <t>] a new card, unstarted; never
167
211
  [--ticket <id>] [--deps <id>,<wave>/<id>] hand-edit wave.json to add one. Refuses an id the wave has.
168
212
  [--notes-file <file>] --plan is an alias of --notes-file
169
213
  [--pick-up-at <time>] holds it until then, as item set's --pick-up-at
@@ -172,6 +216,11 @@ command fails and changes nothing.
172
216
  another card's thread, grill or answer: it reads as a
173
217
  follow-up, and its Parent lists it in Children. A card
174
218
  from the + or New task has none
219
+ [--state grilling|prototyping] files it at once, as item set's --state; the session's
220
+ [--relate <id>,<wave>/<id>] own form, linking it with --relate in the same call.
221
+ Never ready: a card reaches ready through staging.
222
+ Without --state it reads New and a card-added event
223
+ brings in intake triage, which links and files it
175
224
  Without /<id>, the card takes its ticket's id (GH-<n> for issue #n); pass an id only to import a
176
225
  card. Prints "<label> added as <key>": use the key in later commands
177
226
  item relate-ask <eventId> <wave>/<id> --summary <t> folds an ask about an existing item into it: the ask's
@@ -179,11 +228,15 @@ command fails and changes nothing.
179
228
  thread marked Moved, then the summary line. Again with
180
229
  another item moves it there
181
230
  ask progress <eventId> [--next <text>] keeps an answered ask in In Review, --next as its next
182
- line, until the owner's ask-done (Mark as done, or the Done
183
- beside Send in its thread) or ask done is newer. Use it when the answer
231
+ line, until the owner's ask-done (the Done beside Reply
232
+ in its thread) or ask done is newer. Use it when the answer
184
233
  waits on the owner, such as a pick between prototypes
185
234
  ask done <eventId> moves the ask to Done, as the owner's Done button does
186
235
  ask unfold <eventId> moves a folded ask back out to a card of its own
236
+ intake handoff <eventId> --text <why> an intake triage run unsure how to sort a new task, a
237
+ message in an ask's thread or an agent-added card hands it
238
+ to the orchestrator: an intake-handoff event, whose wait
239
+ wakes the session, and a status line in its thread
187
240
  ask move <eventId> --to <route> sends a New ask to another board in the hub's manifest:
188
241
  a New card there with the owner's text, attachments and
189
242
  "Moved from <this board>", which wakes its session; here the
@@ -191,11 +244,6 @@ command fails and changes nothing.
191
244
  A card the ask became is dropped, unless it has started.
192
245
  Refuses a route that isn't another board, and an ask
193
246
  that isn't New
194
- checked [--every <minutes> | --idle] data/checked.json: GitHub was just checked, next in
195
- <minutes> (default 5, the away cadence); run it at the
196
- end of each GitHub tick.
197
- --idle: the board shows no open PR, so never late; with
198
- neither flag it is idle while no card has an open PR.
199
247
  Then sends the queued pushes, each on its own, unless
200
248
  pushes are held (overnight, snoozed, the weekend or off)
201
249
  lineup create <slug> --title <t> --goal <t> a wave, in the owner's words: a named set of cards in
@@ -237,28 +285,36 @@ command fails and changes nothing.
237
285
  slack Slack identity as JSON; imports slack-bridge config once
238
286
  slack set [--self-dm D..] [--user U..] sets hub Slack identity in ~/.claude/kickflip/slack.json
239
287
  [--domain x] [--timezone Area/City]
240
- review sent <wave>/<id> <reviewer> --ts <ts> records the PR link's message in the item
241
- --channel <dm>
242
- review react <wave>/<id> <reviewer> <emoji> records a reaction once on that reviewer's link
243
288
  integrations readiness rows as one JSON line
244
- integrations report --slack yes|no with --site: reports whether this session has Claude's
245
- Slack connector, in ~/.claude/kickflip/integrations.json
246
- review due one JSON line of the Slack sends owed, from local state:
247
- {dms: [{key, reviewer, channel, text, record}], reactions:
248
- [{key, reviewer, channel, ts, emoji, record}], digest:
249
- review digest's answer while due (with record), else null}.
250
- dms: a PR link to each reviewer of an open, non-draft PR with
251
- no link recorded; reactions: git_merged on each recorded link
252
- of a merged card. record is the command to run once sent.
253
- While Slack isn't ready: {waiting} and nothing owed
254
- review record <pr> --state posted|needs the session's review of an incoming PR, after a pr-review
255
- --text <markdown> | --file <path> event, in data/reviews.json: posted once it is on GitHub
256
- [--url <review link>] (--url its link), needs while it waits on the owner, which
257
- puts it in Needs you and pushes. The PR must be one the hub
289
+ review updated <wave>/<id> --text "<what changed>" queues \u201CUpdated: <what changed>\u201D in the thread of every
290
+ posted link on the card's PRs; the hub's next poll sends it
291
+ review due one JSON line of the Slack sends the hub owes, from local state
292
+ and data/pr-slack.json: {links: [{pr, key, reviewer,
293
+ slack_user, head, text}], replies: [{pr, id, reviewer,
294
+ channel, thread_ts, text}], reactions: [{pr, reviewer,
295
+ channel, ts, emoji}], digest: review digest's answer while
296
+ due, else null}. links: each open, non-draft PR's link to
297
+ each reviewer without one; reactions: git_approved once
298
+ anyone approved, git_merged on merge, x on close. The hub
299
+ sends and records each. While Slack isn't ready: {waiting}
300
+ and nothing owed
301
+ review post <pr> --file <review.md> posts the session's words as one Comment review (never an
302
+ --read-at <time> approval or a request for changes) on an incoming PR at
303
+ its listed head, as the board's github-login (else gh's
304
+ account), under "\u{1F916} Claude review from the <board title>
305
+ board" and the <!-- claude-bridge --> marker, and records
306
+ it posted with the review's link. Refuses words that carry
307
+ the header or marker, and a PR that got
308
+ another review after that time. The PR must be one the hub
258
309
  listed in data/incoming.json or reviewed before
259
- review digest pending reviews as JSON; due weekday mornings from 09:00
260
- review digest sent <reviewer> --ts <ts> records a digest in data/review-digests.json;
261
- --channel <dm> --items <key,key> marks today sent in the Slack identity timezone
310
+ review record <pr> --state posted|needs the session's review of an incoming PR that was not posted
311
+ --text <markdown> | --file <path> through review post, in data/reviews.json: posted for a PR
312
+ it chose to say nothing on, needs while it waits on the
313
+ owner, which puts it in Needs you and pushes. Same PRs as
314
+ review post
315
+ review digest pending reviews as JSON, one item per PR; a reviewer is
316
+ owed the day's digest weekdays 09:00-17:00 in the Slack
317
+ identity timezone until data/review-digests.json has it all
262
318
  rules list the hub rules, then with --site the board's, one
263
319
  "<hub|board> <id> <owner|claude> <text>" line each
264
320
  rules add --text <t> [--hub] [--by claude] a rule in board.json, or with --hub in the hub's
@@ -269,7 +325,7 @@ command fails and changes nothing.
269
325
  settings the board's settings as JSON; flags default off, optional
270
326
  settings appear when set: {"auto_merge": false,
271
327
  "auto_build": false, "auto_pull": false,
272
- "max_implementers": 10,
328
+ "max_implementers": 10, "max_planners": 3,
273
329
  "auto_triage": false, "auto_review": false,
274
330
  "idle_notification": false, "skip_draft": false,
275
331
  "prs": true, "github_login": "owner"}
@@ -285,7 +341,7 @@ command fails and changes nothing.
285
341
  pushed, which needs Push main after a land granted.
286
342
  A local board never opens PRs. Without auto-merge, a
287
343
  card with no PR waits in In Review for the owner's Merge
288
- build <wave>/<id> --site <route> asks the hub to run this card's .kickflip/build; prints
344
+ build <wave>/<id> --site <route> asks the hub to run this card's build script; prints
289
345
  --asked "<owner's words>" started and exits. install is an alias. Only the owner's
290
346
  Serve / Build and Auto-build start a card run, so --asked
291
347
  must quote the owner's ask, card message or answer note on
@@ -300,18 +356,27 @@ command fails and changes nothing.
300
356
  the hub asks gh api user. Pass "" to clear it
301
357
  settings set auto-build on|off on: each push builds its card. Off by default.
302
358
  Main builds after every merge either way
359
+ settings set build-target <target>|default the script the hub runs for each card and main:
360
+ .kickflip/build-<target>, or .kickflip/build with
361
+ default (unset). Board Settings' Build target picks it
303
362
  settings set auto-pull on|off Auto implement Todo cards. on: on every board change the
304
363
  board server starts and launches the build of the top
305
364
  ready Todo cards nothing blocks, holds or asks about, one
306
365
  per free implementer slot (see max-implementers), each
307
- once; with none ready and nothing building, it launches a
308
- planner on the top unready card: a prototype when it is
309
- prototyping, else a grill. Off by default.
366
+ once, and launches a grill on each card that arrives in
367
+ Todo, up to max-planners at a time (an arrival: not ready,
368
+ staging or held, with no planner record and no questions
369
+ yet). Off by default.
310
370
  Only on the owner's word
311
371
  settings set max-implementers <1..99> Maximum parallel implementers, 10 by default: the cards
312
372
  building or waiting in Orchestrator review the pull fills
313
373
  up to. Planners and cards waiting on the owner don't count.
374
+ It limits the pull only: an Implement the owner asks for
375
+ starts past it.
314
376
  Only on the owner's word
377
+ settings set max-planners <1..99> Maximum parallel planners, 3 by default: the grills and
378
+ prototypes running at once; arrivals past it wait in board
379
+ order. Only on the owner's word
315
380
  settings set auto-review on|off on: while the board is idle and nothing is pulled, the board
316
381
  server sends pr-review {pr, head, repo, title, author, url,
317
382
  touches, asked} for one incoming PR at a time that touches
@@ -349,15 +414,16 @@ command fails and changes nothing.
349
414
  opened, it keeps that record's kind and worktree.
350
415
  [--dispatch] --dispatch: the session runs this right before it launches
351
416
  an agent of any kind (build, prototype, grill) so the record
352
- is open at launch and the card reads preheating; the agent's
417
+ is open at launch and the card reads starting; the agent's
353
418
  own builder start takes it up. On a record the Build tap
354
419
  opened it changes nothing and prints "already open". The
355
420
  record keeps dispatched: tap or session, whoever opened it.
356
421
  --agent <other name> refuses while the tap's main record
357
422
  is open and not taken up.
358
423
  A build's start clears the card's Next.
359
- A build (--kind build, claude or codex) on an open card sets
360
- --status building first, refusing what that refuses;
424
+ A build (--kind build, claude or codex) on a card not done
425
+ and not In Progress moves it to In Progress first,
426
+ refusing what item set's build start refuses;
361
427
  Prints "ref: <ref>", the card's label, which commit subjects start with.
362
428
  Resolves the card's answered questions, noting "Acted on:
363
429
  builder started"
@@ -382,10 +448,9 @@ command fails and changes nothing.
382
448
  the card's status shows the newest one from an agent still working. builder progress refuses
383
449
  without an open record for that agent, as builder call does
384
450
  builder done <wave>/<id> [--agent <name>] marks that agent's record done; the item's thread
385
- [--next <t>] [--text <what you checked>] shows when it started and finished; clears building/fixing
386
- when the card has no open builder records left. On a
451
+ [--next <t>] [--text <what you checked>] shows when it started and finished. On a
387
452
  record with unverified judgment calls it hands the card
388
- back: --status checking with Next "Answer the builder's
453
+ back: its records close marked handed back, with Next "Answer the builder's
389
454
  questions" when a call starts "Question:", else "Verify
390
455
  the builder's judgment calls"; --next is refused there.
391
456
  Else, on a board, the main build's record that keeps a
@@ -398,13 +463,15 @@ command fails and changes nothing.
398
463
  owner's merge (item set --pushed). A finish that fails
399
464
  refuses with the reason and leaves the record open; one
400
465
  refused while another agent's record is open names it.
466
+ A helper's done takes --text, its hand-back, which agent
467
+ wait prints.
401
468
  --next stops a build without finishing it. Closing the
402
469
  last record of a card left In Progress with no Next is
403
470
  refused without --next, which sets it
404
471
  [--owner-step <t>] a step only the owner can do once the code lands: the card
405
472
  then waits in In Review with it as its Next, its ticket
406
473
  open, until the owner's Done; refused on a card already
407
- done, merged or dropped. land takes it too
474
+ done. land takes it too
408
475
  [--check "<name> <args>"] with --owner-step: a read-only check on the step's final
409
476
  evidence, which the hub's poll runs on each check once the
410
477
  code lands, closing the card when it passes: tag
@@ -415,15 +482,68 @@ command fails and changes nothing.
415
482
  arguments are refused. Three runs in a row that can't run
416
483
  mark it broken and append owner-check-broken {key, check,
417
484
  error}; fix it with item set --check. land takes it too
485
+ [--updated "<what changed>" | --updated none] on a card whose PR link a reviewer already has, done
486
+ refuses without it: the text queues \u201CUpdated: <what
487
+ changed>\u201D in each link's thread, as review updated does;
488
+ none sends nothing, for a change only to comments, names,
489
+ docs or tests
418
490
  builder takes --dir <dir> for a wave file in <dir>; its records go in <dir>/agents
491
+ builder stop <wave>/<id> the orchestrator's stop of the card's run the board launched,
492
+ its headless builder, lander or rebase run, with every process
493
+ under it: the record stays open, marked stopped, so the server
494
+ relaunches nothing and the card stays In Progress reading
495
+ "build stopped by orchestrator" ("land \u2026" while it lands),
496
+ waiting on the orchestrator; the same words go in its thread.
497
+ Prints what it stopped, one process a line. Refuses a planner,
498
+ a builder a session started itself, or none running
499
+ land stop <wave>/<id> the orchestrator's stop of the card's land and its suite: one a
500
+ headless builder or lander runs stops that agent as builder
501
+ stop does; the land a held card's Merge tap started stops
502
+ alone, leaving the land queue, and the card's Merge shows
503
+ again, reading "land stopped by orchestrator". Refuses while
504
+ the land fast-forwards main, or a land in a session the board
505
+ didn't launch
506
+ land queue the host's land queue in order: the land running, then each
507
+ waiting, "(bumped)" on one a bump moved
508
+ item bump <wave>/<id> the card's bump, as the owner's bump icon sends it (card-bump;
509
+ item unbump <wave>/<id> card-unbump). A bumped card's land goes ahead in the land queue
510
+ of every land not bumped since, the latest bump first, without
511
+ stopping the one running; a bump of a queued land says so in
512
+ its thread ("land moved first by orchestrator")
419
513
  lander <wave>/<id> relaunch the headless lander of a card it stopped on (a usage
420
- pause ends one): the card goes back to checking and the
421
- server launches a fresh lander; refused unless it is building
514
+ pause ends one): its open record closes as handed back and
515
+ the server launches a fresh lander; refused unless a
516
+ builder record on the card is open
422
517
  builder pick <wave>/<id> who builds the card, from the hub's Codex share and the pause
423
518
  line, and for claude which model, from the hub's Claude model
424
519
  and the card's --size and --model: "codex: <reason>" or
425
520
  "claude sonnet: <reason>". Pass that kind to builder start
426
521
  --kind, and launch that model
522
+ audit [<wave>/<id>] the board's audit log of the last 30 days, data/audit.jsonl:
523
+ each call the code made on its own (builder-pick, pull,
524
+ round-planner, launch-held, arrival-planner, relaunch,
525
+ lander, triage, pr-review, push, pick-up, auto-merge,
526
+ ready-draft, session-resume, compact) as "<at> <call>
527
+ [<key>]: <result>. <reason>" and the inputs it read; with
528
+ a card, only that card's. The hub's own: kickflip audit
529
+ agent launch <wave>/<id> --name <n> --task <t> any session's helper on an open card, an agent the board server
530
+ [--writes] [--model <opus|sonnet>] launches headless with the task as its brief, under its own
531
+ name (lowercase, "-", not main or visual-\u2026), its record
532
+ <id>@<name> requested until then. A read-only helper launches
533
+ at once and takes no implementer slot; one with --writes
534
+ takes one, waits while Maximum parallel implementers is
535
+ reached, ahead of the pull and the topmost card's first,
536
+ and works in its own worktree, branch <id>-<name> from the
537
+ card's, so it needs the card's worktree. --model else
538
+ builder pick's Claude model. Its lines carry its name
539
+ agent wait <wave>/<id> --name <n> [--agent <me>] blocks until the helper's builder done, then prints its
540
+ [--timeout <minutes>] --text and, for one that writes, its branch. Meanwhile
541
+ --agent's record (main by default) takes no slot. Refuses
542
+ after --timeout (9) minutes, saying whether it runs or waits
543
+ for a slot, and once its process is gone without done
544
+ agent merge <wave>/<id> --name <n> merges a done helper's branch into the card's worktree and
545
+ removes the helper's worktree and branch; on a conflict it
546
+ names the files: resolve, commit and run it again
427
547
  builder call <wave>/<id> <judgment call> appends one line to the agent's open builder record: a
428
548
  [--agent <name>] judgment call as the builder makes it, or a "Question: \u2026"
429
549
  for the session; refused with no open record. A builder start
@@ -447,12 +567,12 @@ command fails and changes nothing.
447
567
  the orchestrator"
448
568
  builder verified <wave>/<id> [--agent <name>] the session's mark that it checked the record's calls; land
449
569
  [--note <t>] refuses while any record on the card has a call after it.
450
- On a handed-back build (checking, no builder open, every
570
+ On a handed-back build (not done, no builder open, every
451
571
  call verified) it finishes the card in the same step, as
452
572
  builder done does, --note its closing line (else "Judgment
453
573
  calls verified."). While a question on the card is open it
454
574
  says so and the card waits; a finish that fails keeps the
455
- mark, the card checking, and says why
575
+ mark, the card handed back, and says why
456
576
  land <wave>/<id> --text <t> [--repo <dir>] brings the worktree its builder record keeps (else
457
577
  [--dir <dir>]
458
578
  ~/.claude/kickflip/worktrees/<id> of --repo) into the board's
@@ -470,7 +590,7 @@ command fails and changes nothing.
470
590
  branch. Without --site it never pushes. One land runs at a time on the host: a
471
591
  second refuses, naming the card landing. With --site, --text <what was checked>
472
592
  is required, and once it lands the card closes as item set
473
- --status done --text does; it refuses first what that close
593
+ --done --text does; it refuses first what that close
474
594
  would. With --site it is builder done's finish: the push
475
595
  after a land, and on a GitHub or plain-remote board the
476
596
  branch push (and draft PR) instead of the fast-forward.
@@ -510,9 +630,9 @@ command fails and changes nothing.
510
630
  hub a session's prompt, so any other --kind is refused. It records the
511
631
  push in data/push-state.json only when kickflip push
512
632
  succeeds, and exits with kickflip push's code. Without --now
513
- it queues until the tick ends (checked) or the hub's
633
+ it queues until the hub's
514
634
  next check; --now sends at once unless pushes are held
515
635
  (overnight, snoozed or the weekend). --urgent: a
516
636
  production fault only; it beats those holds. While
517
637
  notifications are off every push is dropped, urgent too
518
- KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class l extends Error{static{r(this,"UsageError")}}const Z=new Set(["clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","hub","clear-install","force","worktree","dry-run","dispatch"]);function ee(e){const s=Object.create(null),t=[];for(let a=0;a<e.length;a++){const i=e[a];if(!i.startsWith("--")){t.push(i);continue}const d=i.slice(2);if(Z.has(d)){if(Object.hasOwn(s,d))throw new l(`--${d} is given twice`);s[d]=!0}else if(a+1<e.length)(s[d]||=[]).push(e[++a]);else throw new l(`--${d} needs a value`)}return{flags:s,rest:t}}r(ee,"parse");function te(e,s,t){const a=new Set(s.filter(o=>o.startsWith("...")).map(o=>o.slice(3))),i=new Set(s.map(o=>o.replace(/^\.\.\./,""))),d=Object.keys(t).filter(o=>!i.has(o));if(d.length)throw new l(`${e} does not take ${d.map(o=>"--"+o).join(", ")}`);return Object.fromEntries(Object.entries(t).map(([o,c])=>{if(c===!0||a.has(o))return[o,c];if(c.length>1)throw new l(`--${o} is given twice`);return[o,c[0]]}))}r(te,"flagsOf");const se=["html","text","note","notes","summary","title","body","next","progress","install-label","confirmed","asked"],ae=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function ne(e){const s=e.trim();if(!s||/\s/.test(s))return!1;if(ae.test(s))return!0;try{return m.statSync(s).isFile()}catch{return!1}}r(ne,"looksLikePath");function re(e,s){for(const t of se){if(typeof s[t]!="string"||!ne(s[t]))continue;const a=e.includes("file")?`; for a file use --file ${s[t].trim()}`:"";throw new l(`--${t} takes text, not a file path (${s[t].trim()})${a}`)}if(typeof s.file=="string"&&s.file!=="-"&&!m.existsSync(s.file))throw new l(`--file ${s.file}: no such file`)}r(re,"checkInputs");const f=r(e=>m.readFileSync(e==="-"?0:e,"utf8"),"readInput");function $(e){if(!e)throw new l("--file <path> is required");try{return JSON.parse(f(e))}catch(s){throw new n.BoardError(`${e}: ${s.message}`)}}r($,"readJSONInput");function R(e){if(e.plan!==void 0&&e["notes-file"]!==void 0)throw new l("pass --notes-file or --plan, not both");return e["notes-file"]??e.plan}r(R,"notesFileOf");function O(e){if(e.note!==void 0&&e.file!==void 0)throw new l("pass --note or --file, not both");return e.file===void 0?e.note:f(e.file)}r(O,"noteOf");const k={pr:[["wave/id"],[],async(e,[s])=>({lines:(await n.prReport(e,s)).map(t=>JSON.stringify(t))})],"pr comment":[["pr"],["body"],(e,[s],t)=>{if(t.body===void 0)throw new l('pr comment needs --body "<what Claude says>"');return n.postPR(e,s,{body:t.body})}],"pr reply":[["pr","comment-id"],["body"],(e,[s,t],a)=>{if(a.body===void 0)throw new l('pr reply needs --body "<what Claude says>"');return n.postPR(e,s,{replyTo:t,body:a.body})}],reply:[["eventId"],["html","file"],(e,[s],t)=>{if(t.html===void 0==(t.file===void 0))throw new l("reply takes --html or --file");return n.writeReply(e,s,t.html??f(t.file))}],thread:[["wave/id"],["text","link","who","via"],(e,[s],t)=>n.appendThread(e,s,t)],"topic add":[["id"],["title","summary"],(e,[s],t)=>n.addTopic(e,{id:s,...t})],"question add":[[],["file"],async(e,s,t)=>{const a=$(t.file);return{...await n.addQuestions(e,a),asked:[a].flat()}}],"question resolve":[["id"],["note"],(e,[s],t)=>n.resolveQuestion(e,s,t.note)],"prototype add":[["topic"],["title","...take","card"],(e,[s],t)=>n.addPrototype(e,{topic:s,title:t.title,card:t.card,takes:t.take})],"prototype list":[[],[],e=>n.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[s])=>n.removePrototype(e,s)],"item set":[["wave/id"],["status","clear-status","next","clear-next","...pr","pushed","gh-since","from-ask","state","held","unheld","pick-up-at","notes-file","plan","deps","parent","related","install","install-sha","install-label","clear-install","title","size","model","force","ticket","text","check"],async(e,[s],t,a)=>(t.status==="done"&&e&&await n.checkNotReopened(e,s),n.setItem(e,s,{status:t.status,clearStatus:t["clear-status"],next:t.next,clearNext:t["clear-next"],pr:t.pr,pushed:t.pushed,ghSince:t["gh-since"],fromAsk:t["from-ask"],state:t.state,held:t.held,unheld:t.unheld,pickUpAt:t["pick-up-at"],notesFile:R(t),deps:t.deps,parent:t.parent,related:t.related,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},a))],"item add":[["wave[/id]"],["title","notes","deps","notes-file","plan","ticket","pick-up-at","parent","size","model"],(e,[s],t,a)=>n.addItem(e,s,{title:t.title,notes:t.notes,deps:t.deps,notesFile:R(t),ticket:t.ticket,pickUpAt:t["pick-up-at"],parent:t.parent,size:t.size,model:t.model},a)],"item relate-ask":[["eventId","wave/id"],["summary"],(e,[s,t],a)=>n.relateAsk(e,s,t,a.summary)],"ask progress":[["eventId"],["next"],(e,[s],t)=>n.markAsk(e,s,"progress",t.next)],"ask done":[["eventId"],[],(e,[s])=>n.markAsk(e,s,"done")],"ask unfold":[["eventId"],[],(e,[s])=>n.unfoldAsk(e,s)],"ask move":[["eventId"],["to"],(e,[s],t)=>n.moveAsk(e,s,t.to)],"tickets backfill":[[],["dry-run"],(e,s,t,a)=>n.backfillTickets(e,{dryRun:t["dry-run"]},a)],"lineup create":[["slug"],["title","goal","...phase","note","file"],(e,[s],t)=>n.createLineup(e,s,{title:t.title,goal:t.goal,phases:t.phase,note:O(t)})],"lineup add":[["slug","wave/id"],["after"],(e,[s,t],a)=>n.changeLineup(e,s,{add:t,after:a.after})],"lineup remove":[["slug","wave/id"],[],(e,[s,t])=>n.changeLineup(e,s,{remove:t})],"lineup set":[["slug"],["title","goal","note","file","...phase"],(e,[s],t)=>n.changeLineup(e,s,{title:t.title,goal:t.goal,note:O(t),phases:t.phase})],"lineup finish":[["slug"],[],(e,[s])=>n.finishLineup(e,s)],slack:[[],[],()=>({lines:[JSON.stringify(L())]})],"slack set":[[],["self-dm","user","domain","timezone"],async(e,s,t)=>{const a={"self-dm":"self_dm",user:"owner_slack_user",domain:"slack_domain",timezone:"timezone"};try{return{lines:[JSON.stringify(await D(Object.fromEntries(Object.entries(t).map(([i,d])=>[a[i],d]))))]}}catch(i){throw new n.BoardError(i.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await B.rows())]})],"integrations report":[[],["slack"],async(e,s,t)=>{if(!["yes","no"].includes(t.slack))throw new l("integrations report takes --slack yes|no");if(!e)throw new n.BoardError("integrations report needs --site <route>");const a=g.basename(e);return H(t.slack==="yes",{route:a,title:X().sites?.[a]?.title||(await n.boardConfig(e)).title}),{lines:[`Slack connector reported: ${t.slack}.`]}}],"review sent":[["wave/id","reviewer"],["ts","channel"],(e,[s,t],a)=>n.reviewSent(e,s,t,a)],"review react":[["wave/id","reviewer","emoji"],[],(e,[s,t,a])=>n.reviewReact(e,s,t,a)],"review record":[["pr"],["state","text","file","url"],(e,[s],t)=>{if(t.text===void 0==(t.file===void 0))throw new l("review record takes --text or --file");return n.recordReview(e,s,{state:t.state,text:t.text??f(t.file),url:t.url})}],"review digest":[[],[],async(e,s,t,a)=>({lines:[JSON.stringify(await n.reviewDigest(e,a))]})],"review due":[[],[],async(e,s,t,a)=>({lines:[JSON.stringify(await n.reviewDue(e,a))]})],"review digest sent":[["reviewer"],["ts","channel","items"],(e,[s],t,a)=>n.reviewDigestSent(e,s,t,a)],"reviewers set":[[],["file"],(e,s,t)=>n.setReviewers(e,$(t.file))],"reviewers add":[[],["file"],(e,s,t)=>n.addReviewers(e,$(t.file))],"reviewers remove":[["...name"],[],(e,s)=>n.removeReviewers(e,s)],"rules list":[[],[],e=>n.listRules(e)],"rules add":[[],["text","hub","by"],(e,s,t)=>n.addRuleTo(e,t)],"rules edit":[["id"],["text","hub"],(e,[s],t)=>n.editRuleIn(e,s,t)],"rules remove":[["id"],["hub"],(e,[s],t)=>n.removeRuleFrom(e,s,t)],settings:[[],[],async e=>({lines:[JSON.stringify(await n.readBoardSettings(e))]})],build:[["wave/id"],["asked"],async(e,[s],t)=>{if(!e)throw new n.BoardError("build needs --site <route>");n.parseKey(s);const a=await n.askedGate(e,s,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:g.basename(e),key:s}),signal:AbortSignal.timeout(1e4)}),d=await i.json();if(!i.ok)throw new n.BoardError(d.error||"The hub refused the build.");return{lines:["started",...(await n.appendThread(e,s,{text:a})).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,s,t)=>n.setMainInstall(e,{install:t.install,installSha:t["install-sha"]})],"settings set":[["name","value"],[],(e,[s,t])=>n.setBoardSetting(e,s,t)],"builder start":[["wave/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[s],{worktree:t,repo:a,dispatch:i,...d})=>{if(a!==void 0&&!t)throw new l("--repo goes with --worktree");const o=r(p=>W(e,s.split("/")[1],{repo:a,...p}),"make"),c=e&&d.dir===void 0&&V(e);return n.startBuilder(e,s,{...d,dispatch:i?"session":void 0,worktree:t?o:void 0,ownWorktree:c?o:void 0})}],"builder progress":[["wave/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[s,...t],a)=>{if(t.length&&a.text!==void 0)throw new l("builder progress takes its text once: as words or as --text");if(!t.length&&a.text===void 0)throw new l("builder progress needs its text, as words or as --text");return n.builderProgress(e,s,a.text??t.join(" "),{...a,clearEta:a["clear-eta"]})}],"builder done":[["wave/id"],["dir","agent","next","owner-step","check","text"],async(e,[s],{"owner-step":t,check:a,text:i,...d})=>{if(t!==void 0&&(!e||d.dir))throw new l("--owner-step goes on a board card: pass --site, not --dir");if(a!==void 0&&t===void 0)throw new l("--check goes with --owner-step: it is the code check that closes the card once the step is done");t!==void 0&&await n.setItem(e,s,{ownerStep:t,check:a});const o=e&&!d.dir,c=o&&await n.handBackCalls(e,s,d);if(c)return c;if(o&&d.next===void 0&&await n.finishesCard(e,s,d)){if(i===void 0)throw new n.BoardError(`builder done finishes ${s}: pass --text "<what you checked>", or --next "<what happens next>" to stop without finishing`);return x(e,s,{text:i,ownerStep:t})}if(i!==void 0)throw new l("--text goes on a builder done that finishes the card: the main build, with a worktree and no --next");return n.stopBuilder(e,s,{...d,requireNext:!0})}],"builder pick":[["wave/id"],[],(e,[s])=>n.pickBuilder(e,s)],"builder call":[["wave/id","...text"],["dir","agent"],(e,[s,...t],a)=>n.recordCall(e,s,t.join(" "),a)],"builder calls":[["wave/id"],["dir","agent"],(e,[s],t)=>n.listCalls(e,s,t)],"builder ask":[["wave/id","...text"],["agent"],(e,[s,...t],a)=>n.askOrchestrator(e,s,t.join(" "),a)],"builder answer":[["wave/id","ask"],["text"],(e,[s,t],a)=>n.answerBuilder(e,s,t,a.text)],"builder defer":[["wave/id","ask"],["text"],(e,[s,t],a)=>n.answerBuilder(e,s,t,a.text,{deferred:!0})],"builder asks":[["wave/id"],[],(e,[s])=>n.listAsks(e,s)],"builder verified":[["wave/id"],["dir","agent","note"],async(e,[s],t)=>{await n.verifyCalls(e,s,t);const a=e&&!t.dir?await n.verifiedFinish(e,s):null;if(!a)return{};if(a.waits.length)return{lines:[`${await n.cardName(e,s)} finishes once the owner answers ${a.waits.join(", ")}`]};try{return await x(e,s,{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,s)} didn't finish: ${i.message}`),{exitCode:i.exitCode}):i}}],land:[["wave/id"],["repo","dir","text","owner-step","check"],async(e,[s],t)=>{const a=s.split("/")[1];if(e&&t.text===void 0)throw new l(`land needs --text "<what was checked>", which closes ${s} as done once it lands`);if(!e&&t.text!==void 0)throw new l("--text closes the card, which needs --site");if(!e&&t["owner-step"]!==void 0)throw new l("--owner-step keeps a board card open, which needs --site");if(t.check!==void 0&&t["owner-step"]===void 0)throw new l("--check goes with --owner-step: it is the code check that closes the card once the step is done");if(e&&t.dir===void 0)return x(e,s,{text:t.text,ownerStep:t["owner-step"],check:t.check,repo:t.repo});const i=e?await n.cardName(e,s):void 0,d=await z(i??s,async()=>{e&&await n.checkClose(e,s,t.text,t["owner-step"],t.check),await n.checkCallsVerified(e,s,{dir:t.dir});const o=await n.recordedWorktree(e,s,{dir:t.dir}),{repo:c,base:p}=G(e,{repo:t.repo??o?.repo});return await J({path:o?.path??g.join(F(),a),branch:o?.branch??a,repo:g.resolve(c),base:p,ref:i}),e?n.setItem(e,s,{status:"done",text:t.text,ownerStep:t["owner-step"],check:t.check}):{lines:[]}});return d.joined?{lines:[`joined the land already running for ${i??s}`]}:d}],lander:[["wave/id"],[],async(e,[s])=>{if(!e)throw new l("lander needs --site <route>");return await n.relaunchLander(e,s),{lines:[`${await n.cardName(e,s)} is back at checking; the server launches a lander for it within a minute`]}}],merge:[["wave/id"],["after-approval","confirmed"],async(e,[s],t)=>e&&await n.heldMerge(e,s)?K(e,s):n.mergeCard(e,s,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["wave/id"],[],(e,[s])=>e?U(e,s):Promise.reject(new n.BoardError("arch file needs --site <route>"))],checked:[[],["every","idle"],(e,s,t)=>n.writeChecked(e,t.every,{idle:t.idle})],push:[[],["kind","title","body","url","tag","urgent","now"],(e,s,t)=>{if(t.kind!=="fault"&&t.kind!=="reminder")throw new l(`a session pushes only --kind fault or reminder${t.kind===void 0||t.kind===!0?"":`, not "${t.kind}"`}: the board server pushes Action needed, and news goes on the board`);if(t.title===void 0)throw new l("push needs --title");return n.sendPush(e,t)}]};k.install=k.build;const h=r((e,s)=>n.cardName(e,s),"card"),q=r((e,s)=>`${s}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),C=r(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function ie(e){const s=e["pick-up-at"];return[e.status,e["clear-status"]&&"status cleared",e.next!==void 0&&(e.next.trim()?"next line set":"next line cleared"),e["clear-next"]&&"next line cleared",e.pr!==void 0&&`PR #${e.pr}`,e.pushed!==void 0&&`pushed ${e.pushed}`,e["gh-since"]&&`GitHub read to ${e["gh-since"]}`,e["from-ask"]&&`from task ${e["from-ask"]}`,e.state,e.held&&"held",e.unheld&&"resumed",s!==void 0&&(s?`held until ${s}`:"resumed"),R(e)!==void 0&&"notes recorded",e.deps!==void 0&&(e.deps?`blocked by ${e.deps}`:"deps cleared"),e.parent!==void 0&&(e.parent?`parent ${e.parent}`:"parent cleared"),e.related!==void 0&&(e.related?`related to ${e.related.split(",").join(", ")}`:"related cleared"),e.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(ie,"itemChanges");const oe={reply:r(async(e,[s])=>`reply written to ${await n.eventCard(e,s)}`,"reply"),thread:r(async(e,[s])=>`thread line on ${await h(e,s)}`,"thread"),"topic add":r((e,[s])=>`topic ${s} added`,"topic add"),"question add":r(async(e,s,t,{asked:a})=>{const i=[...new Set(a.map(o=>o.topic))],d=i.length!==1?"":i[0].startsWith("item:")?` on ${await h(e,i[0].slice(5))}`:` on topic ${i[0]}`;return`${q(a.map(o=>o.id),"question")} added${d}`},"question add"),"question resolve":r((e,[s])=>`question ${s} resolved`,"question resolve"),"prototype add":r(async(e,[s],t)=>`prototype ${s} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":r((e,[s])=>`prototype ${s} removed`,"prototype rm"),"item set":r(async(e,[s],t)=>`${await h(e,s)}: ${ie(t)}`,"item set"),"item add":r(async(e,s,t,{key:a})=>`${await h(e,a)} added as ${a}`,"item add"),"item relate-ask":r(async(e,[s,t])=>`task ${s} folded into ${await h(e,t)}`,"item relate-ask"),"ask progress":r(async(e,[s])=>`${await h(e,`ask:${s}`)}: in progress`,"ask progress"),"ask done":r(async(e,[s])=>`${await h(e,`ask:${s}`)}: done`,"ask done"),"ask unfold":r((e,[s])=>`task ${s} back on a card of its own`,"ask unfold"),"ask move":r((e,[s],t)=>`task ${s} moved to /${t.to}`,"ask move"),checked:r((e,s,t,{every:a})=>a===null?"GitHub checked; no open PR":`GitHub checked; next in ${a} min`,"checked"),"lineup create":r((e,[s])=>`lineup ${s} created`,"lineup create"),"lineup add":r(async(e,[s,t])=>`${await h(e,t)} added to lineup ${s}`,"lineup add"),"lineup remove":r(async(e,[s,t])=>`${await h(e,t)} taken out of lineup ${s}`,"lineup remove"),"lineup set":r((e,[s])=>`lineup ${s} updated`,"lineup set"),"lineup finish":r((e,[s])=>`lineup ${s} finished`,"lineup finish"),"review sent":r(async(e,[s,t])=>`review link to ${t} recorded on ${await h(e,s)}`,"review sent"),"review react":r(async(e,[s,t,a])=>`${a} on ${t}'s review link recorded on ${await h(e,s)}`,"review react"),"review digest sent":r((e,[s])=>`digest to ${s} recorded`,"review digest sent"),"review record":r((e,[s],t)=>`review of PR #${s} 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,s)=>`${q(s,"reviewer")} removed`,"reviewers remove"),"rules add":r((e,s,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":r((e,[s],t)=>`${t.hub?"hub":"board"} rule ${s} reworded`,"rules edit"),"rules remove":r((e,[s],t)=>`${t.hub?"hub":"board"} rule ${s} removed`,"rules remove"),"builder start":r(async(e,[s],t,a)=>`${await h(e,s)}: ${a.alreadyOpen?"already open: the Implement tap opened its record; launch the agent, whose own builder start takes it up":`${C(t)} started`}`,"builder start"),"builder progress":r(async(e,[s])=>`progress line on ${await h(e,s)}`,"builder progress"),"builder done":r(async(e,[s],t,a)=>`${await h(e,s)}: ${a?.said??`${C(t)} done${t["owner-step"]===void 0?"":`; owner step "${t["owner-step"].trim()}"`}`}`,"builder done"),"builder call":r(async(e,[s])=>`judgment call recorded on ${await h(e,s)}`,"builder call"),"builder ask":r(async(e,[s],t,{id:a})=>`${await h(e,s)}: asked the orchestrator, ${a}`,"builder ask"),"builder answer":r(async(e,[s,t])=>`${await h(e,s)}: ${t} answered`,"builder answer"),"builder defer":r(async(e,[s,t])=>`${await h(e,s)}: ${t} deferred to the owner`,"builder defer"),"builder verified":r(async(e,[s],t,a)=>`${await h(e,s)}: judgment calls verified${a?.said?`; ${a.said}`:""}`,"builder verified"),land:r(async(e,[s],t,a)=>e?a?.said?`${await h(e,s)}: ${a.said}`:t["owner-step"]===void 0?`${await h(e,s)}: landed and done`:`${await h(e,s)}: landed; waits on the owner step "${t["owner-step"].trim()}"`:null,"land"),merge:r(async(e,[s],t,a)=>`${await h(e,s)}: ${a?.said??"merged"}`,"merge"),"arch file":r(async(e,[s])=>`findings of ${await h(e,s)} filed`,"arch file")};async function de(e,s={}){try{const{flags:t,rest:a}=ee(e),i=[3,2,1].map(u=>a.slice(0,u).join(" ")).find(u=>y(k,u)),d=y(k,i);if(!d)throw new l(a.length?`unknown command "${a.slice(0,2).join(" ")}"`:"no command given");const[o,c,p]=d,w=a.slice(i.split(" ").length),_=o.at(-1)?.startsWith("..."),T=o.at(-1)?.endsWith("?")?o.length-1:o.length;if(_?w.length<T:w.length!==o.length)throw new l(`${i} takes ${o.map(u=>`<${u.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:A,...N}=t;if(A?.length>1)throw new l("--site is given twice");const P=A?.[0],b=te(i,c,N);re(c,b);const j=P===void 0?null:await n.siteDir(P),v=await p(j,w,b,s),S=y(oe,i),I=S&&await S(j,w,b,v);I&&console.log(I);for(const u of typeof v=="object"&&v?.lines||[])console.log(u);return 0}catch(t){if(t instanceof l)return console.error(`${t.message} (node board.mjs --help lists the commands)`),2;if(t instanceof n.BoardError)return console.error(t.message),t.exitCode??1;throw t}}if(r(de,"main"),process.argv[1]&&import.meta.url===M(m.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(Y):process.exitCode=await de(e)}export{Y as USAGE,de as main};
638
+ 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","hub","clear-install","force","worktree","dry-run","dispatch","writes"]);function oe(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(ie.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(oe,"parse");function de(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(de,"flagsOf");const le=["html","text","note","notes","summary","title","body","next","progress","install-label","confirmed","asked","task"],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 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!=="-"&&!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 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 I(e){if(e.plan!==void 0&&e["notes-file"]!==void 0)throw new d("pass --notes-file or --plan, not both");return e["notes-file"]??e.plan}r(I,"notesFileOf");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 pe(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(pe,"builderDone");const f={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:se),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=>ne(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)=>(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"],notesFile:I(t),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(()=>ee(e,a),"stopRun")},n))],"item add":[["wave[/id]"],["title","notes","deps","notes-file","plan","ticket","pick-up-at","parent","size","model","state","relate"],(e,[a],t,n)=>s.addItem(e,a,{title:t.title,notes:t.notes,deps:t.deps,notesFile:I(t),ticket:t.ticket,pickUpAt:t["pick-up-at"],parent:t.parent,size:t.size,model:t.model,state:t.state,relate:t.relate,intake:!0},n)],"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&&ae(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 pe(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])=>Z(b(e,"builder stop"),a)],"land stop":[["wave/id"],[],(e,[a])=>te(b(e,"land stop"),a)],"land queue":[[],[],()=>Y()],"item bump":[["wave/id"],[],(e,[a])=>S(b(e,"item bump"),a,!0)],"item unbump":[["wave/id"],[],(e,[a])=>S(b(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)}]};f.install=f.build;function b(e,a){if(!e)throw new d(`${a} needs --site <route>`);return e}r(b,"needSite");const h=r((e,a)=>s.cardName(e,a),"card"),T=r((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),_=r(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function 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.state,e.held&&"held",e.unheld&&"resumed",a!==void 0&&(a?`held until ${a}`:"resumed"),I(e)!==void 0&&"notes recorded",e.deps!==void 0&&(e.deps?`blocked by ${e.deps}`:"deps cleared"),e.parent!==void 0&&(e.parent?`parent ${e.parent}`:"parent cleared"),e.related!==void 0&&(e.related?`related to ${e.related.split(",").join(", ")}`:"related cleared"),e.relate!==void 0&&`related to ${e.relate.split(",").join(", ")} added`,e.unrelate!==void 0&&`unrelated from ${e.unrelate.split(",").join(", ")}`,e.install!==void 0&&"install published",e["clear-install"]&&"install cleared",e.title!==void 0&&"renamed",e.ticket!==void 0&&`ticket ${e.ticket}`,e.check!==void 0&&(e.check.trim()?`check ${e.check.trim().split(/\s+/).join(" ")}`:"check removed")].filter(Boolean).join(", ")}r(we,"itemChanges");const me={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`${T(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)}: ${we(t)}`,"item set"),"item add":r(async(e,a,t,{key:n})=>`${await h(e,n)} added as ${n}`,"item add"),"item relate-ask":r(async(e,[a,t])=>`task ${a} folded into ${await h(e,t)}`,"item relate-ask"),"ask progress":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: in progress`,"ask progress"),"ask done":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: done`,"ask done"),"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)=>`${T(a,"reviewer")} removed`,"reviewers remove"),"rules add":r((e,a,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} reworded`,"rules edit"),"rules remove":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} removed`,"rules remove"),"builder start":r(async(e,[a],t,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 ge(e,a={}){try{const{flags:t,rest:n}=oe(e),i=[3,2,1].map(u=>n.slice(0,u).join(" ")).find(u=>y(f,u)),o=y(f,i);if(!o)throw new d(n.length?`unknown command "${n.slice(0,2).join(" ")}"`:"no command given");const[l,c,p]=o,m=n.slice(i.split(" ").length),N=l.at(-1)?.startsWith("..."),B=l.at(-1)?.endsWith("?")?l.length-1:l.length;if(N?m.length<B:m.length!==l.length)throw new d(`${i} takes ${l.map(u=>`<${u.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:q,...E}=t;if(q?.length>1)throw new d("--site is given twice");const P=q?.[0],k=de(i,c,E);ue(c,k);const A=P===void 0?null:await s.siteDir(P),v=await p(A,m,k,a),j=y(me,i),O=j&&await j(A,m,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(ge,"main"),process.argv[1]&&import.meta.url===W(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 ge(e)}export{re as USAGE,ge as main};