@lovek4d/kickflip 0.3.1 → 0.4.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 (104) hide show
  1. package/CONTEXT.md +151 -115
  2. package/README.md +23 -14
  3. package/SKILL.md +37 -37
  4. package/bin/doctor.mjs +4 -4
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +34 -26
  7. package/bin/wait-events.mjs +2 -2
  8. package/board/arch-review.mjs +1 -1
  9. package/board/auto-review.mjs +2 -0
  10. package/board/bin/board.mjs +186 -69
  11. package/board/domain.mjs +8 -5
  12. package/board/finish.mjs +3 -0
  13. package/board/headless.mjs +3 -0
  14. package/board/on-host.mjs +1 -1
  15. package/board/permissions.mjs +2 -0
  16. package/board/public/app-arch.js +2 -2
  17. package/board/public/app-asks.js +57 -38
  18. package/board/public/app-banner.js +2 -2
  19. package/board/public/app-cards.js +156 -72
  20. package/board/public/app-core.js +2 -4
  21. package/board/public/app-header.js +12 -5
  22. package/board/public/app-lanes.js +201 -47
  23. package/board/public/app-needs.js +2 -1
  24. package/board/public/app-snapshot.js +2 -2
  25. package/board/public/app-threads.js +11 -14
  26. package/board/public/app.html +2 -1
  27. package/board/public/app.js +53 -83
  28. package/board/public/attach.js +39 -3
  29. package/board/public/dialogs.js +59 -14
  30. package/board/public/hub-kit.js +9 -6
  31. package/board/public/hub.css +5 -18
  32. package/board/public/linear-key.js +6 -4
  33. package/board/public/log.html +48 -0
  34. package/board/public/prompts.js +9 -8
  35. package/board/public/prototype-kit.js +6 -6
  36. package/board/public/prototype-switcher.js +13 -5
  37. package/board/public/prototype.html +1 -0
  38. package/board/public/router.js +8 -7
  39. package/board/public/rules-editor.js +3 -4
  40. package/board/public/shell.js +20 -6
  41. package/board/public/sidebar.css +21 -1
  42. package/board/public/sidebar.js +64 -31
  43. package/board/public/style.css +155 -62
  44. package/board/public/toasts.js +2 -2
  45. package/board/public/ui-cards.js +176 -71
  46. package/board/public/ui-enums.js +5 -4
  47. package/board/public/ui-header.js +10 -10
  48. package/board/public/ui-hub.js +4 -4
  49. package/board/public/ui-markdown.js +1 -1
  50. package/board/public/ui-order.js +9 -6
  51. package/board/public/ui-page-header.js +10 -7
  52. package/board/public/ui-patch.js +53 -0
  53. package/board/public/ui-phase-bars.js +17 -0
  54. package/board/public/ui-prompts.js +8 -8
  55. package/board/public/ui-questions.js +26 -23
  56. package/board/public/ui-sidebar.js +46 -22
  57. package/board/public/ui-snapshot.js +20 -9
  58. package/board/public/ui-threads.js +17 -11
  59. package/board/public/ui.js +80 -12
  60. package/board/public/view-frame.js +1 -1
  61. package/board/public/view-hub-settings.js +25 -18
  62. package/board/public/view-hub.js +53 -12
  63. package/board/public/view-notes.js +1 -1
  64. package/board/public/view-questions.js +101 -50
  65. package/board/public/view-settings.js +167 -49
  66. package/board/review.mjs +1 -1
  67. package/board/server.mjs +18 -12
  68. package/board/store.mjs +13 -12
  69. package/board/tickets.mjs +3 -3
  70. package/board/worktree.mjs +20 -14
  71. package/docs/hub-session.md +4 -4
  72. package/hooks-plugin/hooks/codex-share-auto.mjs +1 -1
  73. package/hooks-plugin/hooks/watcher-stop.mjs +2 -2
  74. package/hub/board-repo.mjs +1 -1
  75. package/hub/boards.mjs +4 -4
  76. package/hub/close-board.mjs +1 -1
  77. package/hub/github.mjs +4 -4
  78. package/hub/home.mjs +1 -1
  79. package/hub/hub.mjs +12 -12
  80. package/hub/incoming.mjs +2 -0
  81. package/hub/linear.mjs +2 -2
  82. package/hub/live.mjs +2 -2
  83. package/hub/new-board.mjs +2 -2
  84. package/hub/permission-denied-hook.mjs +2 -0
  85. package/hub/pr-report.mjs +3 -1
  86. package/hub/presence.mjs +1 -1
  87. package/hub/prompts.mjs +1 -1
  88. package/hub/push.mjs +2 -2
  89. package/hub/readiness.mjs +2 -2
  90. package/hub/repos.mjs +3 -3
  91. package/hub/restart-board.mjs +1 -1
  92. package/hub/schedule.mjs +1 -1
  93. package/hub/sessions.mjs +2 -2
  94. package/hub/sw.js +1 -1
  95. package/hub/try.mjs +8 -4
  96. package/hub/usage.mjs +3 -3
  97. package/hub/views.mjs +1 -1
  98. package/package.json +5 -1
  99. package/shared/card-prs.mjs +1 -0
  100. package/shared/colors.css +5 -4
  101. package/shared/fake-gh.mjs +59 -8
  102. package/shared/repos.mjs +1 -1
  103. package/shared/usage-gate.mjs +1 -1
  104. package/board/public/view-polling.js +0 -87
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var P=Object.defineProperty;var n=(e,t)=>P(e,"name",{value:t,configurable:!0});import{readiness as N,reportSlack as E}from"../../hub/readiness.mjs";import{readSlack as L,setSlack as H}from"../../hub/slack.mjs";import{routeRecord as k}from"../../hub/routes.mjs";import w from"node:fs";import m from"node:path";import{pathToFileURL as B}from"node:url";import*as i from"../store.mjs";import{boardWorktree as D,builderRepo as M,holdingLandLock as W,land as z,worktreesDir as G}from"../worktree.mjs";import{fileFindings as J}from"../arch-review.mjs";import{ITEM_STATUSES as F}from"../domain.mjs";import{readManifest as U}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";import{pushKindError as K}from"../../hub/push.mjs";const Q=`usage: node board.mjs <command> [--site <route>] ...
2
+ var E=Object.defineProperty;var i=(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 w from"node:fs";import m from"node:path";import{pathToFileURL as W}from"node:url";import*as n from"../store.mjs";import{boardWorktree as z,builderRepo as G,holdingLandLock as M,land as J,worktreesDir as F}from"../worktree.mjs";import{fileFindings as U}from"../arch-review.mjs";import{finishCard as x}from"../finish.mjs";import{ITEM_STATUSES as K}from"../domain.mjs";import{boardAt as Q,readManifest as V}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";const X=`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.
@@ -12,14 +12,14 @@ Asks have no ticket; item set --from-ask creates one when the card has none.
12
12
  A card moves its ticket one way: --status building or fixing \u2192 In Progress, --pr \u2192 In Review, --status done or
13
13
  merged \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
- A Linear board without Hub's key hands Linear to the session: a linked id is checked on its pattern alone, and a card
16
- needing an issue, or a ticket to move, prints a "ticket" line saying what to do through your Linear MCP, such as
17
- creating the issue and running item set <wave>/<id> --ticket <id>.
15
+ Linear needs Hub's Linear key: settings set tickets refuses it without one, and once the key is removed the hub
16
+ moves every Linear board to internal tickets. A card is never left without a ticket: if Linear can't make one, the
17
+ command fails and changes nothing.
18
18
 
19
19
  reply <eventId> (--html <text> | --file <path>) data/replies/<eventId>.html. A reply to an ask resolves its
20
20
  answered questions (topic item:ask:<eventId>), and one to
21
21
  wave-triage its wave's, noting "Acted on: replied". A
22
- reply to todo-triage ends that triage
22
+ reply to overnight-triage ends that triage
23
23
  thread <wave>/<id> --text <t> [--link <url>] one line in the item's thread; ask:<eventId> in place
24
24
  [--who claude|love|<name>] [--via github|slack] of <wave>/<id> writes in that ask's thread
25
25
  A Claude thread line, or a reply to an ask or item-message, rewrites the body of the thread's
@@ -28,9 +28,9 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
28
28
  topic add <id> --title <t> [--summary <s>] a question topic in data/owner.json
29
29
  question add --file <json> one question or an array of them:
30
30
  {id, topic, question, options: [{id, label, recommended?, detail?, diagram?, practice?}],
31
- source?, context?, url?, urgent?, round_of?}; topic is declared, item:<wave>/<id>, item:ask:<eventId>
31
+ source?, context?, url?, urgent?, round_of?, ask?}; topic is declared, item:<wave>/<id>, item:ask:<eventId>
32
32
  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 merged card's topic is refused; a question on a done card reopens it.
33
+ a board over ~/.claude/waves. A question on a done or merged card reopens it.
34
34
  url is http(s), or a site page such as prototype-<topic>.html?q=<id>, stored as given.
35
35
  Prototype options keep take order, ids matching take keys; recommended: true tags the recommendation.
36
36
  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,6 +45,8 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
45
45
  and printed as "replaced: <old id>". round_of names the question on the same topic whose
46
46
  prototype-request this answers; the question needs a prototype url, takes the next round
47
47
  number, and resolves that one, noting "Followed by prototype round N".
48
+ ask names a builder ask the session deferred on the item (builder defer): the question puts it to the
49
+ owner, and while a card's only open questions are such, it waits on them with no Action needed.
48
50
  The board server pushes the new open questions itself, under the card's tag (item-<wave>/<id>, or
49
51
  ask-<eventId>) or topic-<topic> off the cards
50
52
  question resolve <id> --note <text> sets resolved and resolved_at; refuses a question on a
@@ -64,12 +66,23 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
64
66
  prototype list one line per recorded prototype
65
67
  prototype rm <topic> drops the record, keeping its take files; refuses while an
66
68
  open question links it
67
- pr <wave>/<id> one JSON line: the card's PR state, checks and new events,
68
- using the board's repo, github-login and the card's gh_since
69
+ pr <wave>/<id> one JSON line per PR on the card, in its order: the PR's
70
+ repo, state, checks and new events, using the board's
71
+ github-login and the card's gh_since
72
+ pr comment <pr> --body <text> posts Claude's words on PR <pr> through gh, a number in the
73
+ board's repo or owner/repo#<n> for a card's PR in another,
74
+ pr reply <pr> <comment id> --body <text> or replies in that review comment's thread, as the board's
75
+ github-login (else gh's account), adding "\u{1F916} " before and a
76
+ blank line and <!-- claude-bridge --> after, the marker pr
77
+ reads as Claude's own. Refuses words that already carry
78
+ either. Prints the post's url
69
79
  item set <wave>/<id> [--status <s> | --clear-status] edits the item in its wave file
70
- [--next <t> | --clear-next] [--pr <n>] status: ${F.join(", ")}
80
+ [--next <t> | --clear-next] status: ${K.join(", ")}
71
81
  an empty --next "" clears it, as --clear-next does
72
82
  naming another unfinished card requires it in --deps
83
+ [--pr <n> | --pr <owner/repo#n>]... adds a PR to the card, a bare number in the board's repo;
84
+ repeatable, and never replaces one. Each PR gets its own
85
+ badge and polling; the least-ready decides the card's lane
73
86
  [--pushed <branch>] on a board whose repo is a plain git remote (no GitHub):
74
87
  the card's branch is pushed there, so it waits In Review
75
88
  on the owner's merge. Push it first (git push origin
@@ -81,7 +94,8 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
81
94
  [--from-ask <eventId> --text <t>] the ask it came from: its New card gives way to this item,
82
95
  whose thread opens with the ask and its thread, then --text
83
96
  as the item's first line; it leaves a card it was folded into; refuses a card
84
- from another ask (use item relate-ask)
97
+ from another ask (use item relate-ask), and one with no
98
+ state when --state is missing
85
99
  [--state ready|grilling|prototyping] what a card from an ask does next; it reads New until set.
86
100
  ready refuses while the card has an open question
87
101
  [--held | --unheld | --pick-up-at <time>] holds or resumes the card on the owner's words; the board
@@ -124,7 +138,8 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
124
138
  questions, noting "Acted on: build started".
125
139
  --status building refuses while the item is held or a dep isn't done, merged or dropped,
126
140
  and while the newest owner answer to its questions that link a prototype-*.html page picks
127
- no take; a pick's note is the build's guidance;
141
+ no take, which leaves the card prototyping with Next "Prototype again" until the next
142
+ round's pick; a pick's note is the build's guidance;
128
143
  done, merged and dropped print "unblocked: <wave>/<id>" and append card-unblocked {key}
129
144
  once per unblocking, including held cards: start each unheld card next.
130
145
  building and fixing count as working only while a builder record on the card is open.
@@ -135,7 +150,9 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
135
150
  Every --status or --clear-status records status_at; a later status change overrides the tap.
136
151
  --status done, merged or dropped closes the card: it resolves each of its questions (topic
137
152
  item:<wave>/<id>) that has an answer, noting "Closed with the card", and prints "resolved: <id>"
138
- for each. It refuses while one has no answer yet; [--force] closes it and resolves that one too
153
+ for each. It refuses while one has no answer yet; [--force] closes it and resolves that one too.
154
+ --status done refuses a done card the owner messaged since it closed: it reads reopened, and closes
155
+ again only on the owner's Done
139
156
  item add <wave>[/<id>] --title <t> [--notes <t>] a new card, planned (waiting when it has --deps); never
140
157
  [--ticket <id>] [--deps <id>,<wave>/<id>] hand-edit wave.json to add one. Refuses an id the wave has.
141
158
  [--notes-file <file>] --plan is an alias of --notes-file
@@ -165,29 +182,36 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
165
182
  Refuses a route that isn't another board, and an ask
166
183
  that isn't New
167
184
  checked [--every <minutes> | --idle] data/checked.json: GitHub was just checked, next in
168
- <minutes> (default the polling window's, as kickflip
169
- polling prints); run it at the end of each GitHub tick.
185
+ <minutes> (default 5, the away cadence); run it at the
186
+ end of each GitHub tick.
170
187
  --idle: the board shows no open PR, so never late; with
171
188
  neither flag it is idle while no card has an open PR.
172
189
  Then sends the queued pushes, each on its own, unless
173
190
  pushes are held (overnight, snoozed, the weekend or off)
174
191
  lineup create <slug> --title <t> --goal <t> a wave, in the owner's words: a named set of cards in
175
- --cards <wave>/<id>,\u2026 (--note <t> | --file <f>) build order, all from one wave file. Each card is then
176
- blocked by the one before it (its other deps stay), and
177
- the note goes to lineup-<slug>.md beside the wave file.
178
- The board's Waves section shows it with its goal and
179
- order, its cards carry its chip, and Notes opens the
192
+ --phase <wave>/<id>,\u2026 [--phase \u2026] phases, all from one wave file, one --phase per phase
193
+ (--note <t> | --file <f>) in build order. Each card is then blocked by every card
194
+ of the phase before (its other deps stay), so one
195
+ phase's cards build side by side, and the note goes to
196
+ lineup-<slug>.md beside the wave file.
197
+ Todo shows it as a band of its cards in order, numbered
198
+ by phase, its goal on its name, and Notes opens the
180
199
  goal, the order and the note. Refuses a card already in
181
- another open wave. Prints the order by card label
182
- lineup add <slug> <wave>/<id> [--after <wave>/<id>] puts a card in the wave, last unless --after, and chains
183
- the order again
200
+ another open wave. Prints the order by card label, one
201
+ phase's cards by commas and the phases by arrows
202
+ lineup add <slug> <wave>/<id> [--after <wave>/<id>] puts a card in the wave as a phase of its own, last
203
+ unless --after's phase, and links the phases again
184
204
  lineup remove <slug> <wave>/<id> takes a card out: it loses its links to the wave's cards,
185
- and the card after it is blocked by the one before
186
- lineup set <slug> [--title <t>] [--goal <t>] renames the wave, sets its goal or rewrites its note
187
- [--note <t> | --file <f>]
188
- lineup finish <slug> the wave leaves the Waves section; its cards keep their
205
+ its phase goes if it held only that card, and the phases
206
+ are linked again
207
+ lineup set <slug> [--title <t>] [--goal <t>] renames the wave, sets its goal, rewrites its note or
208
+ [--note <t> | --file <f>] puts its cards in new phases (--phase per phase, every
209
+ [--phase <wave>/<id>,\u2026 \u2026] card once) and links them again. A card's \u22EF in its
210
+ band swaps it with the Todo card before or after it,
211
+ each taking the other's phase
212
+ lineup finish <slug> the wave's band leaves Todo; its cards keep their
189
213
  links. A wave whose cards are all Done leaves it too
190
- The Waves section's Triage sends wave-triage {lineup}, or {wave} for a wave file's row; the row reads
214
+ A band's Triage sends wave-triage {lineup}, or {wave} for a wave file's band; it reads
191
215
  Triaging\u2026 until you reply to that event, then "Triage: N open questions" while questions on topic
192
216
  lineup:<slug> or wave:<slug> are open or answered
193
217
  tickets backfill [--dry-run] gives every open card without a ticket one: the first
@@ -217,6 +241,11 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
217
241
  no link recorded; reactions: git_merged on each recorded link
218
242
  of a merged card. record is the command to run once sent.
219
243
  While Slack isn't ready: {waiting} and nothing owed
244
+ review record <pr> --state posted|needs the session's review of an incoming PR, after a pr-review
245
+ --text <markdown> | --file <path> event, in data/reviews.json: posted once it is on GitHub
246
+ [--url <review link>] (--url its link), needs while it waits on the owner, which
247
+ puts it in Needs you and pushes. The PR must be one the hub
248
+ listed in data/incoming.json or reviewed before
220
249
  review digest pending reviews as JSON; due weekday mornings from 09:00
221
250
  review digest sent <reviewer> --ts <ts> records a digest in data/review-digests.json;
222
251
  --channel <dm> --items <key,key> marks today sent in the Slack identity timezone
@@ -230,11 +259,18 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
230
259
  settings the board's settings as JSON; flags default off, optional
231
260
  settings appear when set: {"auto_merge": false,
232
261
  "auto_build": false, "auto_pull": false,
233
- "auto_triage": false, "github_login": "owner"}
262
+ "max_implementers": 10,
263
+ "auto_triage": false, "auto_review": false,
264
+ "headless_builders": false,
265
+ "idle_notification": false, "skip_draft": false,
266
+ "github_login": "owner"}
234
267
  settings set auto-merge on|off on: the session merges an approved PR once CI is green and
235
268
  the merge state is CLEAN. off (the default): the card
236
269
  shows the owner a Merge button, and the session merges on its
237
270
  card-merge event. Board Settings' toggle writes it too
271
+ settings set skip-draft on|off on: the hub's poll marks a card's draft PR ready for
272
+ review once its CI passes. off (the default): the draft
273
+ waits in In Review on the owner's review and Ready
238
274
  build <wave>/<id> --site <route> asks the hub to run this card's .kickflip/build; prints
239
275
  --asked "<owner's words>" started and exits. install is an alias. Only the owner's
240
276
  Serve / Build and Auto-build start a card run, so --asked
@@ -250,16 +286,39 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
250
286
  the hub asks gh api user. Pass "" to clear it
251
287
  settings set auto-build on|off on: each push builds its card. Off by default.
252
288
  Main builds after every merge either way
253
- settings set auto-pull on|off on: while no builder runs and nothing waits in Orchestrator
254
- review, the board server sends card-pull {key} for the top
255
- Todo card nothing blocks, holds or asks about, ready cards
256
- first; with none, card-plan {key} for the top unready
257
- card to grill or prototype. Off by default.
289
+ settings set auto-pull on|off Auto implement Todo cards. on: on every board change the
290
+ board server sends card-pull {key} for the top ready Todo
291
+ cards nothing blocks, holds or asks about, one per free
292
+ implementer slot (see max-implementers), each once; with
293
+ none ready and nothing building, card-plan {key} for the
294
+ top unready card to grill or prototype. Off by default.
258
295
  Only on the owner's word
259
- settings set auto-triage on|off on: while the board is idle and Todo changed since the last
260
- triage, at most once a day and before any pull, the board
261
- server declares topic todo-triage-<date> ("Todo triage
262
- <date>") and sends todo-triage {date, topic}; the pull
296
+ settings set max-implementers <1..99> Maximum parallel implementers, 10 by default: the cards
297
+ building or waiting in Orchestrator review the pull fills
298
+ up to. Planners and cards waiting on the owner don't count.
299
+ Only on the owner's word
300
+ settings set auto-review on|off on: while the board is idle and nothing is pulled, the board
301
+ server sends pr-review {pr, head, repo, title, author, url,
302
+ touches, asked} for one incoming PR at a time that touches
303
+ this board's cards or asks the owner for review. Off by
304
+ default. Only on the owner's word
305
+ settings set headless-builders on|off on: the board server launches each Claude build itself, as
306
+ claude -p in the card's worktree with a brief built from the
307
+ card's notes and rules, for the Implement tap, a queued start
308
+ and Auto implement's pull, instead of waking you with
309
+ card-build or card-pull; a codex pick, or a launch that
310
+ fails, still sends card-build. The builder lands its own
311
+ work; once you verify a handed-back build's calls and its
312
+ card has no open question, the server launches a headless
313
+ lander. item set --state ready then refuses a card without
314
+ --size, a notes file, or the id of each answered question in
315
+ that file. Off by default. Only on the owner's word
316
+ settings set auto-triage on|off on: once a night, from the middle of the hub's quiet hours
317
+ to their end, when two or more cards wait on open
318
+ questions, nothing builds and something changed since the
319
+ last triage, the board server declares topic
320
+ overnight-triage-<date> ("Overnight triage <date>") and
321
+ sends overnight-triage {date, topic, cards}; the pull
263
322
  waits until you reply to it. Off by default. Only on the
264
323
  owner's word
265
324
  settings set tickets <json> the board's ticket source, as Board Settings' Tickets row
@@ -286,8 +345,13 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
286
345
  opened, it keeps that record's kind and worktree.
287
346
  [--dispatch] --dispatch: the session runs this right before it launches
288
347
  an agent of any kind (build, prototype, grill) so the record
289
- is open at launch and the card reads Starting; the agent's
290
- own builder start takes it up.
348
+ is open at launch and the card reads preheating; the agent's
349
+ own builder start takes it up. On a record the Build tap
350
+ opened it changes nothing and prints "already open". The
351
+ record keeps dispatched: tap or session, whoever opened it.
352
+ --agent <other name> refuses while the tap's main record
353
+ is open and not taken up.
354
+ A build's start clears the card's Next.
291
355
  A build (--kind build, claude or codex) on an open card sets
292
356
  --status building first, refusing what that refuses;
293
357
  Prints "ref: <ref>", the card's label, which commit subjects start with.
@@ -297,7 +361,11 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
297
361
  ~/.claude/kickflip/worktrees/<id> on a new branch <id> from
298
362
  the board's branch of its clone (--repo overrides the clone), kept on
299
363
  the record and printed as "worktree: <path>". Refuses when
300
- that path or branch exists
364
+ that path or branch exists, unless it is a leftover
365
+ holding no work, which it remakes. A build's main record
366
+ on a board makes it without --worktree when it keeps none
367
+ (a taken-up or resumed record's, or the worktree the
368
+ card's last record left while it is still there)
301
369
  builder progress <wave>/<id> --phase <phase> one status line: a phase, reading, building, testing,
302
370
  (<detail> | --text <d>) [--agent <name>] checking or landing, and a present-tense detail of at most
303
371
  80 characters in plain words, which the owner reads at a
@@ -307,13 +375,32 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
307
375
  for another agent, and the card "Testing \xB7 <detail>"
308
376
  [--eta <ISO time> | --clear-eta] an ETA shows as a line under the card's status
309
377
  A progress line, --progress included, needs --site: it is a status line in the item's thread, and
310
- the card's status shows the newest one from an agent still working
378
+ the card's status shows the newest one from an agent still working. builder progress refuses
379
+ without an open record for that agent, as builder call does
311
380
  builder done <wave>/<id> [--agent <name>] marks that agent's record done; the item's thread
312
- [--next <t>] shows when it started and finished; clears building/fixing
313
- when the card has no open builder records left. Closing the
314
- last one of a card left In Progress with no Next is refused
315
- without --next, which sets it; item set --status checking
316
- and land close the records themselves
381
+ [--next <t>] [--text <what you checked>] shows when it started and finished; clears building/fixing
382
+ when the card has no open builder records left. On a
383
+ record with unverified judgment calls it hands the card
384
+ back: --status checking with Next "Answer the builder's
385
+ questions" when a call starts "Question:", else "Verify
386
+ the builder's judgment calls"; --next is refused there.
387
+ Else, on a board, the main build's record that keeps a
388
+ worktree finishes the card, as land does, --text its
389
+ closing line: lands it, closes the card and its ticket
390
+ and, with the board's "Push main after a land" on, pushes
391
+ the branch to origin; on a GitHub board it pushes the
392
+ card's branch, opens a draft PR and moves the card to it,
393
+ and on a plain git remote pushes the branch for the
394
+ owner's merge (item set --pushed). A finish that fails
395
+ refuses with the reason and leaves the record open; one
396
+ refused while another agent's record is open names it.
397
+ --next stops a build without finishing it. Closing the
398
+ last record of a card left In Progress with no Next is
399
+ refused without --next, which sets it
400
+ [--owner-step <t>] a step only the owner can do once the code lands: the card
401
+ then waits in In Review with it as its Next, its ticket
402
+ open, until the owner's Done; refused on a card already
403
+ done, merged or dropped. land takes it too
317
404
  builder takes --dir <dir> for a wave file in <dir>; its records go in <dir>/agents
318
405
  builder pick <wave>/<id> who builds the card, from the hub's Codex share and the pause
319
406
  line, and for claude which model, from the hub's Claude model
@@ -327,29 +414,58 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
327
414
  its calls and worktree. The thread shows them, collapsed
328
415
  builder calls <wave>/<id> [--agent <name>] prints the record's calls, numbered, then "verified <time>"
329
416
  or "not verified yet"
417
+ builder ask <wave>/<id> <question> [--agent <name>] a running builder's one-line question for the board's session,
418
+ which it asks at any point and carries on meanwhile: a thread
419
+ line named by its model ("Claude Opus builder"), never unread,
420
+ and a builder-ask {key, ask, agent, text} event. Prints its
421
+ id, ask<n>. Needs --site and an open builder record
422
+ builder answer <wave>/<id> <ask> --text <t> the session's answer to that ask, a thread line named
423
+ Orchestrator that leaves the card quiet
424
+ builder defer <wave>/<id> <ask> --text <t> the session's deferral of an ask it can't answer confidently:
425
+ the same thread line, and a "Question: <ask> (deferred to the
426
+ owner)" call on the asking agent's record. After its review the
427
+ session puts it to the owner as a card question with
428
+ "ask": "<ask>"; builder verified refuses until then
429
+ builder asks <wave>/<id> prints each ask, then its answer, its deferral or "waiting on
430
+ the orchestrator"
330
431
  builder verified <wave>/<id> [--agent <name>] the session's mark that it checked the record's calls; land
331
- [--note <t>] refuses while any record on the card has a call after it
432
+ [--note <t>] refuses while any record on the card has a call after it.
433
+ On a handed-back build (checking, no builder open, every
434
+ call verified) it finishes the card in the same step, as
435
+ builder done does, --note its closing line (else "Judgment
436
+ calls verified."). While a question on the card is open it
437
+ says so and the card waits; a finish that fails keeps the
438
+ mark, the card checking, and says why
332
439
  land <wave>/<id> --text <t> [--repo <dir>] brings the worktree its builder record keeps (else
333
440
  [--dir <dir>]
334
- ~/.claude/kickflip/worktrees/<id> of --repo) into main, one line
335
- per step: refuses while it has uncommitted changes or adds no
336
- commit; rebases onto main when main moved, refusing on a
337
- conflict with the rebase undone; refuses unless its full
338
- node --test suite passes, naming each failing test and
339
- saving the TAP output under ~/.claude/kickflip/logs;
340
- fast-forwards main in the repo;
441
+ ~/.claude/kickflip/worktrees/<id> of --repo) into the board's
442
+ branch (the one its builders branch from, else main), one
443
+ line per step: refuses unless the repo has that branch
444
+ checked out, naming it, or while the worktree has uncommitted
445
+ changes or adds no commit; rebases onto the branch when it
446
+ moved, refusing on a conflict with the rebase undone; refuses
447
+ a new ADR named with a number instead of a timestamp; refuses
448
+ unless its full node --test suite passes, naming each failing
449
+ test and saving the TAP output under ~/.claude/kickflip/logs;
450
+ fast-forwards the branch in the repo;
341
451
  restarts the hub when its code changed, else each board
342
452
  server when board/ changed; removes the worktree and its
343
- branch. Never pushes. One land runs at a time on the host: a
453
+ branch. Without --site it never pushes. One land runs at a time on the host: a
344
454
  second refuses, naming the card landing. With --site, --text <what was checked>
345
455
  is required, and once it lands the card closes as item set
346
456
  --status done --text does; it refuses first what that close
347
- would. KICKFLIP_RESTART_CMD='["<cmd>"]' replaces
457
+ would. With --site it is builder done's finish: the push
458
+ after a land, and on a GitHub or plain-remote board the
459
+ branch push (and draft PR) instead of the fast-forward.
460
+ --owner-step <t>, as builder done takes it, keeps the
461
+ landed card in In Review on that step until the owner's
462
+ Done. KICKFLIP_RESTART_CMD='["<cmd>"]' replaces
348
463
  the restarts, called with hub or board <route>
349
- merge <wave>/<id> [--after-approval "<why trivial>"] merges the card's PR (gh pr merge --squash) and records it merged,
350
- [--confirmed "<owner's words>"] once GitHub reads it approved, its checks green with none
351
- pending and its merge state CLEAN, the test the Merge button
352
- uses. Refuses, naming each reason ("pending: 2 checks"),
464
+ merge <wave>/<id> [--after-approval "<why trivial>"] merges the card's PRs (gh pr merge --squash) in its order and
465
+ [--confirmed "<owner's words>"] records it merged, once GitHub reads every open one approved,
466
+ its checks green with none pending and its merge state CLEAN,
467
+ the test the Merge button uses; a merged or closed PR is left
468
+ as it is. Refuses, naming each reason ("pending: 2 checks"),
353
469
  commits after the newest approval unless --after-approval
354
470
  says why they're trivial (its closing line says so), and a
355
471
  card with manual steps unless --confirmed quotes the owner.
@@ -362,18 +478,19 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
362
478
  Prints "filed: <ref> from <id>", "skipped: <id>", and
363
479
  "waiting: <ids>" while any are unanswered; once none is, it
364
480
  closes the audit card as done
365
- push --kind action|prompt|fault --title <t> runs kickflip push with these flags, adding
481
+ push --kind fault|reminder --title <t> runs kickflip push with these flags, adding
366
482
  [--body <b>] [--url </route/path>] "\xB7 and N replies" to the body for the replies
367
483
  [--tag <tag>] [--now | --urgent] written since the site's last push through this
368
- command (none before its first). The board server pushes
369
- each card's Action needed itself, so this is for a fault,
370
- what only the owner can fix. It records the push in
371
- data/push-state.json only when kickflip push succeeds,
372
- and exits with kickflip push's code. Refused without an
373
- allowed --kind: news stays on the board. Without --now
484
+ command (none before its first). A session pushes only a
485
+ fault, what only the owner can fix, or a reminder the
486
+ owner asked for in words ("push me when it's all done"):
487
+ the board server pushes each card's Action needed and the
488
+ hub a session's prompt, so any other --kind is refused. It records the
489
+ push in data/push-state.json only when kickflip push
490
+ succeeds, and exits with kickflip push's code. Without --now
374
491
  it queues until the tick ends (checked) or the hub's
375
492
  next check; --now sends at once unless pushes are held
376
493
  (overnight, snoozed or the weekend). --urgent: a
377
494
  production fault only; it beats those holds. While
378
495
  notifications are off every push is dropped, urgent too
379
- KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class o extends Error{static{n(this,"UsageError")}}const V=new Set(["clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","hub","clear-install","force","worktree","dry-run","dispatch"]),X=new Set(["take"]);function Z(e){const t=Object.create(null),s=[];for(let a=0;a<e.length;a++){const r=e[a];if(!r.startsWith("--")){s.push(r);continue}const d=r.slice(2);if(X.has(d)){if(a+1>=e.length)throw new o(`--${d} needs a value`);(t[d]||=[]).push(e[++a]);continue}if(Object.hasOwn(t,d))throw new o(`--${d} is given twice`);if(V.has(d))t[d]=!0;else if(a+1<e.length)t[d]=e[++a];else throw new o(`--${d} needs a value`)}return{flags:t,rest:s}}n(Z,"parse");const Y=["html","text","note","notes","summary","title","body","next","progress","install-label","confirmed","asked"],ee=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function te(e){const t=e.trim();if(!t||/\s/.test(t))return!1;if(ee.test(t))return!0;try{return w.statSync(t).isFile()}catch{return!1}}n(te,"looksLikePath");function se(e,t){for(const s of Y){if(typeof t[s]!="string"||!te(t[s]))continue;const a=e.includes("file")?`; for a file use --file ${t[s].trim()}`:"";throw new o(`--${s} takes text, not a file path (${t[s].trim()})${a}`)}if(typeof t.file=="string"&&t.file!=="-"&&!w.existsSync(t.file))throw new o(`--file ${t.file}: no such file`)}n(se,"checkInputs");const b=n(e=>w.readFileSync(e==="-"?0:e,"utf8"),"readInput");function v(e){if(!e)throw new o("--file <path> is required");try{return JSON.parse(b(e))}catch(t){throw new i.BoardError(`${e}: ${t.message}`)}}n(v,"readJSONInput");function y(e){if(e.plan!==void 0&&e["notes-file"]!==void 0)throw new o("pass --notes-file or --plan, not both");return e["notes-file"]??e.plan}n(y,"notesFileOf");function O(e){if(e.note!==void 0&&e.file!==void 0)throw new o("pass --note or --file, not both");return e.file===void 0?e.note:b(e.file)}n(O,"noteOf");const g={pr:[["wave/id"],[],async(e,[t])=>({lines:[JSON.stringify(await i.prReport(e,t))]})],reply:[["eventId"],["html","file"],(e,[t],s)=>{if(s.html===void 0==(s.file===void 0))throw new o("reply takes --html or --file");return i.writeReply(e,t,s.html??b(s.file))}],thread:[["wave/id"],["text","link","who","via"],(e,[t],s)=>i.appendThread(e,t,s)],"topic add":[["id"],["title","summary"],(e,[t],s)=>i.addTopic(e,{id:t,...s})],"question add":[[],["file"],async(e,t,s)=>{const a=v(s.file);return{...await i.addQuestions(e,a),asked:[a].flat()}}],"question resolve":[["id"],["note"],(e,[t],s)=>i.resolveQuestion(e,t,s.note)],"prototype add":[["topic"],["title","take","card"],(e,[t],s)=>i.addPrototype(e,{topic:t,title:s.title,card:s.card,takes:s.take})],"prototype list":[[],[],e=>i.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[t])=>i.removePrototype(e,t)],"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"],(e,[t],s,a)=>i.setItem(e,t,{status:s.status,clearStatus:s["clear-status"],next:s.next,clearNext:s["clear-next"],pr:s.pr,pushed:s.pushed,ghSince:s["gh-since"],fromAsk:s["from-ask"],state:s.state,held:s.held,unheld:s.unheld,pickUpAt:s["pick-up-at"],notesFile:y(s),deps:s.deps,parent:s.parent,related:s.related,install:s.install,installSha:s["install-sha"],installLabel:s["install-label"],clearInstall:s["clear-install"],title:s.title,size:s.size,model:s.model,force:s.force,ticket:s.ticket,text:s.text},a)],"item add":[["wave[/id]"],["title","notes","deps","notes-file","plan","ticket","pick-up-at","parent","size","model"],(e,[t],s,a)=>i.addItem(e,t,{title:s.title,notes:s.notes,deps:s.deps,notesFile:y(s),ticket:s.ticket,pickUpAt:s["pick-up-at"],parent:s.parent,size:s.size,model:s.model},a)],"item relate-ask":[["eventId","wave/id"],["summary"],(e,[t,s],a)=>i.relateAsk(e,t,s,a.summary)],"ask progress":[["eventId"],["next"],(e,[t],s)=>i.markAsk(e,t,"progress",s.next)],"ask done":[["eventId"],[],(e,[t])=>i.markAsk(e,t,"done")],"ask unfold":[["eventId"],[],(e,[t])=>i.unfoldAsk(e,t)],"ask move":[["eventId"],["to"],(e,[t],s)=>i.moveAsk(e,t,s.to)],"tickets backfill":[[],["dry-run"],(e,t,s,a)=>i.backfillTickets(e,{dryRun:s["dry-run"]},a)],"lineup create":[["slug"],["title","goal","cards","note","file"],(e,[t],s)=>i.createLineup(e,t,{title:s.title,goal:s.goal,cards:s.cards,note:O(s)})],"lineup add":[["slug","wave/id"],["after"],(e,[t,s],a)=>i.changeLineup(e,t,{add:s,after:a.after})],"lineup remove":[["slug","wave/id"],[],(e,[t,s])=>i.changeLineup(e,t,{remove:s})],"lineup set":[["slug"],["title","goal","note","file"],(e,[t],s)=>i.changeLineup(e,t,{title:s.title,goal:s.goal,note:O(s)})],"lineup finish":[["slug"],[],(e,[t])=>i.finishLineup(e,t)],slack:[[],[],()=>({lines:[JSON.stringify(L())]})],"slack set":[[],["self-dm","user","domain","timezone"],async(e,t,s)=>{const a={"self-dm":"self_dm",user:"owner_slack_user",domain:"slack_domain",timezone:"timezone"};try{return{lines:[JSON.stringify(await H(Object.fromEntries(Object.entries(s).map(([r,d])=>[a[r],d]))))]}}catch(r){throw new i.BoardError(r.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await N.rows())]})],"integrations report":[[],["slack"],async(e,t,s)=>{if(!["yes","no"].includes(s.slack))throw new o("integrations report takes --slack yes|no");if(!e)throw new i.BoardError("integrations report needs --site <route>");const a=m.basename(e);return E(s.slack==="yes",{route:a,title:U().sites?.[a]?.title||(await i.boardConfig(e)).title}),{lines:[`Slack connector reported: ${s.slack}.`]}}],"review sent":[["wave/id","reviewer"],["ts","channel"],(e,[t,s],a)=>i.reviewSent(e,t,s,a)],"review react":[["wave/id","reviewer","emoji"],[],(e,[t,s,a])=>i.reviewReact(e,t,s,a)],"review digest":[[],[],async(e,t,s,a)=>({lines:[JSON.stringify(await i.reviewDigest(e,a))]})],"review due":[[],[],async(e,t,s,a)=>({lines:[JSON.stringify(await i.reviewDue(e,a))]})],"review digest sent":[["reviewer"],["ts","channel","items"],(e,[t],s,a)=>i.reviewDigestSent(e,t,s,a)],"reviewers set":[[],["file"],(e,t,s)=>i.setReviewers(e,v(s.file))],"reviewers add":[[],["file"],(e,t,s)=>i.addReviewers(e,v(s.file))],"reviewers remove":[["...name"],[],(e,t)=>i.removeReviewers(e,t)],"rules list":[[],[],e=>i.listRules(e)],"rules add":[[],["text","hub","by"],(e,t,s)=>i.addRuleTo(e,s)],"rules edit":[["id"],["text","hub"],(e,[t],s)=>i.editRuleIn(e,t,s)],"rules remove":[["id"],["hub"],(e,[t],s)=>i.removeRuleFrom(e,t,s)],settings:[[],[],async e=>({lines:[JSON.stringify(await i.readBoardSettings(e))]})],build:[["wave/id"],["asked"],async(e,[t],s)=>{if(!e)throw new i.BoardError("build needs --site <route>");i.parseKey(t);const a=await i.askedGate(e,t,s.asked);try{const r=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:m.basename(e),key:t}),signal:AbortSignal.timeout(1e4)}),d=await r.json();if(!r.ok)throw new i.BoardError(d.error||"The hub refused the build.");return{lines:["started",...(await i.appendThread(e,t,{text:a})).lines]}}catch(r){throw r instanceof i.BoardError?r:new i.BoardError(`Could not start the build: ${r.message}`)}}],"main set":[[],["install","install-sha"],(e,t,s)=>i.setMainInstall(e,{install:s.install,installSha:s["install-sha"]})],"settings set":[["name","value"],[],(e,[t,s])=>i.setBoardSetting(e,t,s)],"builder start":[["wave/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[t],{worktree:s,repo:a,...r})=>{if(a!==void 0&&!s)throw new o("--repo goes with --worktree");return i.startBuilder(e,t,{...r,worktree:s&&(()=>D(e,t.split("/")[1],{repo:a}))})}],"builder progress":[["wave/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[t,...s],a)=>{if(s.length&&a.text!==void 0)throw new o("builder progress takes its text once: as words or as --text");if(!s.length&&a.text===void 0)throw new o("builder progress needs its text, as words or as --text");return i.builderProgress(e,t,a.text??s.join(" "),{...a,clearEta:a["clear-eta"]})}],"builder done":[["wave/id"],["dir","agent","next"],(e,[t],s)=>i.stopBuilder(e,t,{...s,requireNext:!0})],"builder pick":[["wave/id"],[],(e,[t])=>i.pickBuilder(e,t)],"builder call":[["wave/id","...text"],["dir","agent"],(e,[t,...s],a)=>i.recordCall(e,t,s.join(" "),a)],"builder calls":[["wave/id"],["dir","agent"],(e,[t],s)=>i.listCalls(e,t,s)],"builder verified":[["wave/id"],["dir","agent","note"],(e,[t],s)=>i.verifyCalls(e,t,s)],land:[["wave/id"],["repo","dir","text"],async(e,[t],s)=>{const a=t.split("/")[1];if(e&&s.text===void 0)throw new o(`land needs --text "<what was checked>", which closes ${t} as done once it lands`);if(!e&&s.text!==void 0)throw new o("--text closes the card, which needs --site");return W(e?await i.cardName(e,t):t,async()=>{e&&await i.checkClose(e,t,s.text),await i.checkCallsVerified(e,t,{dir:s.dir});const r=await i.recordedWorktree(e,t,{dir:s.dir}),d=m.resolve(s.repo??r?.repo??M(e).repo);return z({path:r?.path??m.join(G(),a),branch:r?.branch??a,repo:d}),e?i.setItem(e,t,{status:"done",text:s.text}):{lines:[]}})}],merge:[["wave/id"],["after-approval","confirmed"],(e,[t],s)=>i.mergeCard(e,t,{afterApproval:s["after-approval"],confirmed:s.confirmed})],"arch file":[["wave/id"],[],(e,[t])=>e?J(e,t):Promise.reject(new i.BoardError("arch file needs --site <route>"))],checked:[[],["every","idle"],(e,t,s)=>i.writeChecked(e,s.every,{idle:s.idle})],push:[[],["kind","title","body","url","tag","urgent","now"],(e,t,s)=>{const a=K(s.kind);if(a)throw new o(a);if(s.title===void 0)throw new o("push needs --title");return i.sendPush(e,s)}]};g.install=g.build;const l=n((e,t)=>i.cardName(e,t),"card"),C=n((e,t)=>`${t}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),T=n(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function ae(e){const t=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",t!==void 0&&(t?`held until ${t}`:"resumed"),y(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}`].filter(Boolean).join(", ")}n(ae,"itemChanges");const ie={reply:n(async(e,[t])=>`reply written to ${await i.eventCard(e,t)}`,"reply"),thread:n(async(e,[t])=>`thread line on ${await l(e,t)}`,"thread"),"topic add":n((e,[t])=>`topic ${t} added`,"topic add"),"question add":n(async(e,t,s,{asked:a})=>{const r=[...new Set(a.map(c=>c.topic))],d=r.length!==1?"":r[0].startsWith("item:")?` on ${await l(e,r[0].slice(5))}`:` on topic ${r[0]}`;return`${C(a.map(c=>c.id),"question")} added${d}`},"question add"),"question resolve":n((e,[t])=>`question ${t} resolved`,"question resolve"),"prototype add":n(async(e,[t],s)=>`prototype ${t} recorded${s.card?` for ${await l(e,s.card)}`:""}`,"prototype add"),"prototype rm":n((e,[t])=>`prototype ${t} removed`,"prototype rm"),"item set":n(async(e,[t],s)=>`${await l(e,t)}: ${ae(s)}`,"item set"),"item add":n(async(e,t,s,{key:a})=>`${await l(e,a)} added as ${a}`,"item add"),"item relate-ask":n(async(e,[t,s])=>`task ${t} folded into ${await l(e,s)}`,"item relate-ask"),"ask progress":n(async(e,[t])=>`${await l(e,`ask:${t}`)}: in progress`,"ask progress"),"ask done":n(async(e,[t])=>`${await l(e,`ask:${t}`)}: done`,"ask done"),"ask unfold":n((e,[t])=>`task ${t} back on a card of its own`,"ask unfold"),"ask move":n((e,[t],s)=>`task ${t} moved to /${s.to}`,"ask move"),checked:n((e,t,s,{every:a})=>a===null?"GitHub checked; no open PR":`GitHub checked; next in ${a} min`,"checked"),"lineup create":n((e,[t])=>`lineup ${t} created`,"lineup create"),"lineup add":n(async(e,[t,s])=>`${await l(e,s)} added to lineup ${t}`,"lineup add"),"lineup remove":n(async(e,[t,s])=>`${await l(e,s)} taken out of lineup ${t}`,"lineup remove"),"lineup set":n((e,[t])=>`lineup ${t} updated`,"lineup set"),"lineup finish":n((e,[t])=>`lineup ${t} finished`,"lineup finish"),"review sent":n(async(e,[t,s])=>`review link to ${s} recorded on ${await l(e,t)}`,"review sent"),"review react":n(async(e,[t,s,a])=>`${a} on ${s}'s review link recorded on ${await l(e,t)}`,"review react"),"review digest sent":n((e,[t])=>`digest to ${t} recorded`,"review digest sent"),"reviewers set":n(()=>"reviewers replaced","reviewers set"),"reviewers add":n(()=>"reviewers added","reviewers add"),"reviewers remove":n((e,t)=>`${C(t,"reviewer")} removed`,"reviewers remove"),"rules add":n((e,t,s)=>`${s.hub?"hub":"board"} rule added`,"rules add"),"rules edit":n((e,[t],s)=>`${s.hub?"hub":"board"} rule ${t} reworded`,"rules edit"),"rules remove":n((e,[t],s)=>`${s.hub?"hub":"board"} rule ${t} removed`,"rules remove"),"builder start":n(async(e,[t],s)=>`${await l(e,t)}: ${T(s)} started`,"builder start"),"builder progress":n(async(e,[t])=>`progress line on ${await l(e,t)}`,"builder progress"),"builder done":n(async(e,[t],s)=>`${await l(e,t)}: ${T(s)} done`,"builder done"),"builder call":n(async(e,[t])=>`judgment call recorded on ${await l(e,t)}`,"builder call"),"builder verified":n(async(e,[t])=>`${await l(e,t)}: judgment calls verified`,"builder verified"),land:n(async(e,[t])=>e?`${await l(e,t)}: landed and done`:null,"land"),merge:n(async(e,[t])=>`${await l(e,t)}: merged`,"merge"),"arch file":n(async(e,[t])=>`findings of ${await l(e,t)} filed`,"arch file")};async function ne(e,t={}){try{const{flags:s,rest:a}=Z(e),r=[3,2,1].map(h=>a.slice(0,h).join(" ")).find(h=>k(g,h)),d=k(g,r);if(!d)throw new o(a.length?`unknown command "${a.slice(0,2).join(" ")}"`:"no command given");const[c,x,R]=d,u=a.slice(r.split(" ").length),_=c.at(-1)?.startsWith("..."),q=c.at(-1)?.endsWith("?")?c.length-1:c.length;if(_?u.length<q:u.length!==c.length)throw new o(`${r} takes ${c.map(h=>`<${h.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:$,...p}=s,S=Object.keys(p).filter(h=>!x.includes(h));if(S.length)throw new o(`${r} does not take ${S.map(h=>"--"+h).join(", ")}`);se(x,p);const I=$===void 0?null:await i.siteDir($),f=await R(I,u,p,t),A=k(ie,r),j=A&&await A(I,u,p,f);j&&console.log(j);for(const h of typeof f=="object"&&f?.lines||[])console.log(h);return 0}catch(s){if(s instanceof o)return console.error(`${s.message} (node board.mjs --help lists the commands)`),2;if(s instanceof i.BoardError)return console.error(s.message),s.exitCode??1;throw s}}if(n(ne,"main"),process.argv[1]&&import.meta.url===B(w.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(Q):process.exitCode=await ne(e)}export{Q as USAGE,ne as main};
496
+ KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class d extends Error{static{i(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 Y(e){const s=Object.create(null),t=[];for(let a=0;a<e.length;a++){const r=e[a];if(!r.startsWith("--")){t.push(r);continue}const l=r.slice(2);if(Z.has(l)){if(Object.hasOwn(s,l))throw new d(`--${l} is given twice`);s[l]=!0}else if(a+1<e.length)(s[l]||=[]).push(e[++a]);else throw new d(`--${l} needs a value`)}return{flags:s,rest:t}}i(Y,"parse");function ee(e,s,t){const a=new Set(s.filter(o=>o.startsWith("...")).map(o=>o.slice(3))),r=new Set(s.map(o=>o.replace(/^\.\.\./,""))),l=Object.keys(t).filter(o=>!r.has(o));if(l.length)throw new d(`${e} does not take ${l.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 d(`--${o} is given twice`);return[o,c[0]]}))}i(ee,"flagsOf");const te=["html","text","note","notes","summary","title","body","next","progress","install-label","confirmed","asked"],se=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function ae(e){const s=e.trim();if(!s||/\s/.test(s))return!1;if(se.test(s))return!0;try{return w.statSync(s).isFile()}catch{return!1}}i(ae,"looksLikePath");function ne(e,s){for(const t of te){if(typeof s[t]!="string"||!ae(s[t]))continue;const a=e.includes("file")?`; for a file use --file ${s[t].trim()}`:"";throw new d(`--${t} takes text, not a file path (${s[t].trim()})${a}`)}if(typeof s.file=="string"&&s.file!=="-"&&!w.existsSync(s.file))throw new d(`--file ${s.file}: no such file`)}i(ne,"checkInputs");const f=i(e=>w.readFileSync(e==="-"?0:e,"utf8"),"readInput");function $(e){if(!e)throw new d("--file <path> is required");try{return JSON.parse(f(e))}catch(s){throw new n.BoardError(`${e}: ${s.message}`)}}i($,"readJSONInput");function A(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}i(A,"notesFileOf");function P(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:f(e.file)}i(P,"noteOf");const g={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 d('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 d('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 d("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"],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:A(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},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:A(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:P(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:P(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(([r,l])=>[a[r],l]))))]}}catch(r){throw new n.BoardError(r.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await B.rows())]})],"integrations report":[[],["slack"],async(e,s,t)=>{if(!["yes","no"].includes(t.slack))throw new d("integrations report takes --slack yes|no");if(!e)throw new n.BoardError("integrations report needs --site <route>");const a=m.basename(e);return H(t.slack==="yes",{route:a,title:V().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 d("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 r=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:m.basename(e),key:s}),signal:AbortSignal.timeout(1e4)}),l=await r.json();if(!r.ok)throw new n.BoardError(l.error||"The hub refused the build.");return{lines:["started",...(await n.appendThread(e,s,{text:a})).lines]}}catch(r){throw r instanceof n.BoardError?r:new n.BoardError(`Could not start the build: ${r.message}`)}}],"main set":[[],["install","install-sha"],(e,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:r,...l})=>{if(a!==void 0&&!t)throw new d("--repo goes with --worktree");const o=i(b=>z(e,s.split("/")[1],{repo:a,...b}),"make"),c=e&&l.dir===void 0&&Q(e);return n.startBuilder(e,s,{...l,dispatch:r?"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 d("builder progress takes its text once: as words or as --text");if(!t.length&&a.text===void 0)throw new d("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","text"],async(e,[s],{"owner-step":t,text:a,...r})=>{if(t!==void 0&&(!e||r.dir))throw new d("--owner-step goes on a board card: pass --site, not --dir");t!==void 0&&await n.setItem(e,s,{ownerStep:t});const l=e&&!r.dir,o=l&&await n.handBackCalls(e,s,r);if(o)return o;if(l&&r.next===void 0&&await n.finishesCard(e,s,r)){if(a===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:a,ownerStep:t})}if(a!==void 0)throw new d("--text goes on a builder done that finishes the card: the main build, with a worktree and no --next");return n.stopBuilder(e,s,{...r,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(r){throw r instanceof n.BoardError?Object.assign(new n.BoardError(`judgment calls verified, but ${await n.cardName(e,s)} didn't finish: ${r.message}`),{exitCode:r.exitCode}):r}}],land:[["wave/id"],["repo","dir","text","owner-step"],async(e,[s],t)=>{const a=s.split("/")[1];if(e&&t.text===void 0)throw new d(`land needs --text "<what was checked>", which closes ${s} 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");return e&&t.dir===void 0?x(e,s,{text:t.text,ownerStep:t["owner-step"],repo:t.repo}):M(e?await n.cardName(e,s):s,async()=>{e&&await n.checkClose(e,s,t.text,t["owner-step"]),await n.checkCallsVerified(e,s,{dir:t.dir});const r=await n.recordedWorktree(e,s,{dir:t.dir}),{repo:l,base:o}=G(e,{repo:t.repo??r?.repo});return J({path:r?.path??m.join(F(),a),branch:r?.branch??a,repo:m.resolve(l),base:o,ref:e?await n.cardName(e,s):void 0}),e?n.setItem(e,s,{status:"done",text:t.text,ownerStep:t["owner-step"]}):{lines:[]}})}],merge:[["wave/id"],["after-approval","confirmed"],(e,[s],t)=>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 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)}]};g.install=g.build;const h=i((e,s)=>n.cardName(e,s),"card"),C=i((e,s)=>`${s}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),q=i(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"),A(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}`].filter(Boolean).join(", ")}i(ie,"itemChanges");const re={reply:i(async(e,[s])=>`reply written to ${await n.eventCard(e,s)}`,"reply"),thread:i(async(e,[s])=>`thread line on ${await h(e,s)}`,"thread"),"topic add":i((e,[s])=>`topic ${s} added`,"topic add"),"question add":i(async(e,s,t,{asked:a})=>{const r=[...new Set(a.map(o=>o.topic))],l=r.length!==1?"":r[0].startsWith("item:")?` on ${await h(e,r[0].slice(5))}`:` on topic ${r[0]}`;return`${C(a.map(o=>o.id),"question")} added${l}`},"question add"),"question resolve":i((e,[s])=>`question ${s} resolved`,"question resolve"),"prototype add":i(async(e,[s],t)=>`prototype ${s} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":i((e,[s])=>`prototype ${s} removed`,"prototype rm"),"item set":i(async(e,[s],t)=>`${await h(e,s)}: ${ie(t)}`,"item set"),"item add":i(async(e,s,t,{key:a})=>`${await h(e,a)} added as ${a}`,"item add"),"item relate-ask":i(async(e,[s,t])=>`task ${s} folded into ${await h(e,t)}`,"item relate-ask"),"ask progress":i(async(e,[s])=>`${await h(e,`ask:${s}`)}: in progress`,"ask progress"),"ask done":i(async(e,[s])=>`${await h(e,`ask:${s}`)}: done`,"ask done"),"ask unfold":i((e,[s])=>`task ${s} back on a card of its own`,"ask unfold"),"ask move":i((e,[s],t)=>`task ${s} moved to /${t.to}`,"ask move"),checked:i((e,s,t,{every:a})=>a===null?"GitHub checked; no open PR":`GitHub checked; next in ${a} min`,"checked"),"lineup create":i((e,[s])=>`lineup ${s} created`,"lineup create"),"lineup add":i(async(e,[s,t])=>`${await h(e,t)} added to lineup ${s}`,"lineup add"),"lineup remove":i(async(e,[s,t])=>`${await h(e,t)} taken out of lineup ${s}`,"lineup remove"),"lineup set":i((e,[s])=>`lineup ${s} updated`,"lineup set"),"lineup finish":i((e,[s])=>`lineup ${s} finished`,"lineup finish"),"review sent":i(async(e,[s,t])=>`review link to ${t} recorded on ${await h(e,s)}`,"review sent"),"review react":i(async(e,[s,t,a])=>`${a} on ${t}'s review link recorded on ${await h(e,s)}`,"review react"),"review digest sent":i((e,[s])=>`digest to ${s} recorded`,"review digest sent"),"review record":i((e,[s],t)=>`review of PR #${s} recorded: ${t.state==="needs"?"needs you":"posted"}`,"review record"),"reviewers set":i(()=>"reviewers replaced","reviewers set"),"reviewers add":i(()=>"reviewers added","reviewers add"),"reviewers remove":i((e,s)=>`${C(s,"reviewer")} removed`,"reviewers remove"),"rules add":i((e,s,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":i((e,[s],t)=>`${t.hub?"hub":"board"} rule ${s} reworded`,"rules edit"),"rules remove":i((e,[s],t)=>`${t.hub?"hub":"board"} rule ${s} removed`,"rules remove"),"builder start":i(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":`${q(t)} started`}`,"builder start"),"builder progress":i(async(e,[s])=>`progress line on ${await h(e,s)}`,"builder progress"),"builder done":i(async(e,[s],t,a)=>`${await h(e,s)}: ${a?.said??`${q(t)} done${t["owner-step"]===void 0?"":`; owner step "${t["owner-step"].trim()}"`}`}`,"builder done"),"builder call":i(async(e,[s])=>`judgment call recorded on ${await h(e,s)}`,"builder call"),"builder ask":i(async(e,[s],t,{id:a})=>`${await h(e,s)}: asked the orchestrator, ${a}`,"builder ask"),"builder answer":i(async(e,[s,t])=>`${await h(e,s)}: ${t} answered`,"builder answer"),"builder defer":i(async(e,[s,t])=>`${await h(e,s)}: ${t} deferred to the owner`,"builder defer"),"builder verified":i(async(e,[s],t,a)=>`${await h(e,s)}: judgment calls verified${a?.said?`; ${a.said}`:""}`,"builder verified"),land:i(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:i(async(e,[s])=>`${await h(e,s)}: merged`,"merge"),"arch file":i(async(e,[s])=>`findings of ${await h(e,s)} filed`,"arch file")};async function oe(e,s={}){try{const{flags:t,rest:a}=Y(e),r=[3,2,1].map(u=>a.slice(0,u).join(" ")).find(u=>y(g,u)),l=y(g,r);if(!l)throw new d(a.length?`unknown command "${a.slice(0,2).join(" ")}"`:"no command given");const[o,c,b]=l,p=a.slice(r.split(" ").length),_=o.at(-1)?.startsWith("..."),N=o.at(-1)?.endsWith("?")?o.length-1:o.length;if(_?p.length<N:p.length!==o.length)throw new d(`${r} takes ${o.map(u=>`<${u.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:S,...T}=t;if(S?.length>1)throw new d("--site is given twice");const O=S?.[0],k=ee(r,c,T);ne(c,k);const R=O===void 0?null:await n.siteDir(O),v=await b(R,p,k,s),I=y(re,r),j=I&&await I(R,p,k,v);j&&console.log(j);for(const u of typeof v=="object"&&v?.lines||[])console.log(u);return 0}catch(t){if(t instanceof d)return console.error(`${t.message} (node board.mjs --help lists the commands)`),2;if(t instanceof n.BoardError)return console.error(t.message),t.exitCode??1;throw t}}if(i(oe,"main"),process.argv[1]&&import.meta.url===W(w.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(X):process.exitCode=await oe(e)}export{X as USAGE,oe as main};