@lovek4d/kickflip 0.3.1 → 0.5.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 (111) hide show
  1. package/CONTEXT.md +170 -118
  2. package/README.md +23 -14
  3. package/SKILL.md +40 -38
  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 +213 -74
  11. package/board/domain.mjs +8 -5
  12. package/board/finish.mjs +4 -0
  13. package/board/headless.mjs +4 -0
  14. package/board/merge3.mjs +1 -0
  15. package/board/on-host.mjs +1 -1
  16. package/board/permissions.mjs +2 -0
  17. package/board/public/app-arch.js +2 -2
  18. package/board/public/app-asks.js +39 -103
  19. package/board/public/app-banner.js +2 -2
  20. package/board/public/app-cards.js +166 -73
  21. package/board/public/app-core.js +2 -23
  22. package/board/public/app-header.js +12 -5
  23. package/board/public/app-lanes.js +201 -47
  24. package/board/public/app-needs.js +2 -1
  25. package/board/public/app-reader.js +174 -0
  26. package/board/public/app-snapshot.js +2 -2
  27. package/board/public/app-threads.js +53 -42
  28. package/board/public/app.html +4 -1
  29. package/board/public/app.js +78 -96
  30. package/board/public/attach.js +39 -3
  31. package/board/public/dialogs.js +59 -14
  32. package/board/public/hub-kit.js +9 -6
  33. package/board/public/hub.css +6 -18
  34. package/board/public/linear-key.js +6 -4
  35. package/board/public/log.html +48 -0
  36. package/board/public/new-task.js +121 -0
  37. package/board/public/prompts.js +9 -8
  38. package/board/public/prototype-kit.js +6 -6
  39. package/board/public/prototype-switcher.js +13 -5
  40. package/board/public/prototype.html +1 -0
  41. package/board/public/router.js +8 -7
  42. package/board/public/rules-editor.js +3 -4
  43. package/board/public/shell.js +24 -6
  44. package/board/public/sidebar.css +22 -1
  45. package/board/public/sidebar.js +64 -31
  46. package/board/public/style.css +245 -64
  47. package/board/public/toasts.js +2 -2
  48. package/board/public/ui-cards.js +205 -75
  49. package/board/public/ui-enums.js +6 -5
  50. package/board/public/ui-header.js +10 -10
  51. package/board/public/ui-hub.js +4 -4
  52. package/board/public/ui-markdown.js +1 -1
  53. package/board/public/ui-order.js +9 -6
  54. package/board/public/ui-page-header.js +10 -7
  55. package/board/public/ui-patch.js +53 -0
  56. package/board/public/ui-phase-bars.js +17 -0
  57. package/board/public/ui-prompts.js +8 -8
  58. package/board/public/ui-questions.js +61 -29
  59. package/board/public/ui-sidebar.js +54 -24
  60. package/board/public/ui-snapshot.js +20 -9
  61. package/board/public/ui-threads.js +40 -21
  62. package/board/public/ui.js +116 -18
  63. package/board/public/view-frame.js +2 -1
  64. package/board/public/view-hub-settings.js +25 -18
  65. package/board/public/view-hub.js +53 -12
  66. package/board/public/view-notes.js +1 -1
  67. package/board/public/view-questions.js +142 -62
  68. package/board/public/view-settings.js +189 -52
  69. package/board/review.mjs +1 -1
  70. package/board/server.mjs +20 -12
  71. package/board/store.mjs +15 -12
  72. package/board/tickets.mjs +3 -3
  73. package/board/worktree.mjs +21 -15
  74. package/docs/hub-session.md +4 -4
  75. package/hooks-plugin/hooks/codex-share-auto.mjs +1 -1
  76. package/hooks-plugin/hooks/watcher-stop.mjs +2 -2
  77. package/hub/board-repo.mjs +1 -1
  78. package/hub/boards.mjs +4 -4
  79. package/hub/close-board.mjs +1 -1
  80. package/hub/front-door.mjs +20 -0
  81. package/hub/github.mjs +4 -4
  82. package/hub/home.mjs +1 -1
  83. package/hub/hub.mjs +11 -14
  84. package/hub/incoming.mjs +2 -0
  85. package/hub/linear.mjs +2 -2
  86. package/hub/live.mjs +2 -2
  87. package/hub/new-board.mjs +2 -2
  88. package/hub/owner-checks.mjs +1 -0
  89. package/hub/permission-denied-hook.mjs +2 -0
  90. package/hub/pr-report.mjs +3 -1
  91. package/hub/presence.mjs +1 -1
  92. package/hub/prompts.mjs +1 -1
  93. package/hub/push.mjs +2 -2
  94. package/hub/readiness.mjs +2 -2
  95. package/hub/repos.mjs +3 -3
  96. package/hub/restart-board.mjs +1 -1
  97. package/hub/schedule.mjs +1 -1
  98. package/hub/sessions.mjs +2 -2
  99. package/hub/sw.js +1 -1
  100. package/hub/try.mjs +8 -4
  101. package/hub/usage.mjs +3 -3
  102. package/hub/views.mjs +1 -1
  103. package/package.json +5 -1
  104. package/shared/card-prs.mjs +1 -0
  105. package/shared/colors.css +5 -4
  106. package/shared/fake-gh.mjs +59 -8
  107. package/shared/owner-check.mjs +3 -0
  108. package/shared/repos.mjs +1 -1
  109. package/shared/tailscale.mjs +1 -0
  110. package/shared/usage-gate.mjs +1 -1
  111. 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 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>] ...
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,11 +28,13 @@ 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?, reask_of?, ask?, suggested?}; 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
+ suggested: {option, card, answer} pre-checks the option the owner already chose on an earlier card, with a
37
+ "suggested" chip and the line You answered this on <card>: "<answer>"; card is that card's ref, answer their words.
36
38
  Waiting on you shows fitting takes in a grid, or a chooser above one full-width take for a single take or if any
37
39
  scrolls or fills the screen.
38
40
  diagram, with practice: rows of 1-4 boxes [[{text, tone?, to?}]], tone quiet|info|notice|
@@ -45,6 +47,10 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
45
47
  and printed as "replaced: <old id>". round_of names the question on the same topic whose
46
48
  prototype-request this answers; the question needs a prototype url, takes the next round
47
49
  number, and resolves that one, noting "Followed by prototype round N".
50
+ reask_of names a resolved question on the same topic that the owner asked to ask again
51
+ (reask-request): it resolves that one again, noting "Asked again as <new id>", printed as "resolved: <old id>".
52
+ ask names a builder ask the session deferred on the item (builder defer): the question puts it to the
53
+ owner, and while a card's only open questions are such, it waits on them with no Action needed.
48
54
  The board server pushes the new open questions itself, under the card's tag (item-<wave>/<id>, or
49
55
  ask-<eventId>) or topic-<topic> off the cards
50
56
  question resolve <id> --note <text> sets resolved and resolved_at; refuses a question on a
@@ -64,12 +70,23 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
64
70
  prototype list one line per recorded prototype
65
71
  prototype rm <topic> drops the record, keeping its take files; refuses while an
66
72
  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
73
+ pr <wave>/<id> one JSON line per PR on the card, in its order: the PR's
74
+ repo, state, checks and new events, using the board's
75
+ github-login and the card's gh_since
76
+ pr comment <pr> --body <text> posts Claude's words on PR <pr> through gh, a number in the
77
+ board's repo or owner/repo#<n> for a card's PR in another,
78
+ pr reply <pr> <comment id> --body <text> or replies in that review comment's thread, as the board's
79
+ github-login (else gh's account), adding "\u{1F916} " before and a
80
+ blank line and <!-- claude-bridge --> after, the marker pr
81
+ reads as Claude's own. Refuses words that already carry
82
+ either. Prints the post's url
69
83
  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(", ")}
84
+ [--next <t> | --clear-next] status: ${Q.join(", ")}
71
85
  an empty --next "" clears it, as --clear-next does
72
86
  naming another unfinished card requires it in --deps
87
+ [--pr <n> | --pr <owner/repo#n>]... adds a PR to the card, a bare number in the board's repo;
88
+ repeatable, and never replaces one. Each PR gets its own
89
+ badge and polling; the least-ready decides the card's lane
73
90
  [--pushed <branch>] on a board whose repo is a plain git remote (no GitHub):
74
91
  the card's branch is pushed there, so it waits In Review
75
92
  on the owner's merge. Push it first (git push origin
@@ -81,9 +98,13 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
81
98
  [--from-ask <eventId> --text <t>] the ask it came from: its New card gives way to this item,
82
99
  whose thread opens with the ask and its thread, then --text
83
100
  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)
101
+ from another ask (use item relate-ask), and one with no
102
+ state when --state is missing
85
103
  [--state ready|grilling|prototyping] what a card from an ask does next; it reads New until set.
86
- ready refuses while the card has an open question
104
+ ready refuses while the card has an open question, and
105
+ a card without --size, a notes file, or the id of each
106
+ answered question in that file, since a builder starts
107
+ from it; it resolves those answered questions
87
108
  [--held | --unheld | --pick-up-at <time>] holds or resumes the card on the owner's words; the board
88
109
  server records the owner's own Hold and Resume taps. A later
89
110
  --held/--unheld overrides a tap; --unheld also clears a held status.
@@ -96,6 +117,9 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
96
117
  --held holds it with no time; --pick-up-at "" resumes it
97
118
  [--ticket <id>] links a ticket in the board\u2019s source
98
119
  [--title <t>] renames the card
120
+ [--check "<name> <args>"] adds or changes the code check on the card's owner step,
121
+ as builder done --check takes it, clearing a broken mark;
122
+ --check "" removes it. Refused on a card with no owner step
99
123
  [--size small|large] [--model opus|sonnet] what builder pick reads while the hub's Claude model is
100
124
  Auto: a small card gets Claude Sonnet, any other Opus, and
101
125
  --model wins over --size; "" clears either
@@ -124,7 +148,8 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
124
148
  questions, noting "Acted on: build started".
125
149
  --status building refuses while the item is held or a dep isn't done, merged or dropped,
126
150
  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;
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;
128
153
  done, merged and dropped print "unblocked: <wave>/<id>" and append card-unblocked {key}
129
154
  once per unblocking, including held cards: start each unheld card next.
130
155
  building and fixing count as working only while a builder record on the card is open.
@@ -135,7 +160,9 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
135
160
  Every --status or --clear-status records status_at; a later status change overrides the tap.
136
161
  --status done, merged or dropped closes the card: it resolves each of its questions (topic
137
162
  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
163
+ 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
139
166
  item add <wave>[/<id>] --title <t> [--notes <t>] a new card, planned (waiting when it has --deps); never
140
167
  [--ticket <id>] [--deps <id>,<wave>/<id>] hand-edit wave.json to add one. Refuses an id the wave has.
141
168
  [--notes-file <file>] --plan is an alias of --notes-file
@@ -165,29 +192,36 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
165
192
  Refuses a route that isn't another board, and an ask
166
193
  that isn't New
167
194
  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.
195
+ <minutes> (default 5, the away cadence); run it at the
196
+ end of each GitHub tick.
170
197
  --idle: the board shows no open PR, so never late; with
171
198
  neither flag it is idle while no card has an open PR.
172
199
  Then sends the queued pushes, each on its own, unless
173
200
  pushes are held (overnight, snoozed, the weekend or off)
174
201
  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
202
+ --phase <wave>/<id>,\u2026 [--phase \u2026] phases, all from one wave file, one --phase per phase
203
+ (--note <t> | --file <f>) in build order. Each card is then blocked by every card
204
+ of the phase before (its other deps stay), so one
205
+ phase's cards build side by side, and the note goes to
206
+ lineup-<slug>.md beside the wave file.
207
+ Todo shows it as a band of its cards in order, numbered
208
+ by phase, its goal on its name, and Notes opens the
180
209
  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
210
+ another open wave. Prints the order by card label, one
211
+ phase's cards by commas and the phases by arrows
212
+ lineup add <slug> <wave>/<id> [--after <wave>/<id>] puts a card in the wave as a phase of its own, last
213
+ unless --after's phase, and links the phases again
184
214
  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
215
+ its phase goes if it held only that card, and the phases
216
+ are linked again
217
+ lineup set <slug> [--title <t>] [--goal <t>] renames the wave, sets its goal, rewrites its note or
218
+ [--note <t> | --file <f>] puts its cards in new phases (--phase per phase, every
219
+ [--phase <wave>/<id>,\u2026 \u2026] card once) and links them again. A card's \u22EF in its
220
+ band swaps it with the Todo card before or after it,
221
+ each taking the other's phase
222
+ lineup finish <slug> the wave's band leaves Todo; its cards keep their
189
223
  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
224
+ A band's Triage sends wave-triage {lineup}, or {wave} for a wave file's band; it reads
191
225
  Triaging\u2026 until you reply to that event, then "Triage: N open questions" while questions on topic
192
226
  lineup:<slug> or wave:<slug> are open or answered
193
227
  tickets backfill [--dry-run] gives every open card without a ticket one: the first
@@ -217,6 +251,11 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
217
251
  no link recorded; reactions: git_merged on each recorded link
218
252
  of a merged card. record is the command to run once sent.
219
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
258
+ listed in data/incoming.json or reviewed before
220
259
  review digest pending reviews as JSON; due weekday mornings from 09:00
221
260
  review digest sent <reviewer> --ts <ts> records a digest in data/review-digests.json;
222
261
  --channel <dm> --items <key,key> marks today sent in the Slack identity timezone
@@ -230,11 +269,22 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
230
269
  settings the board's settings as JSON; flags default off, optional
231
270
  settings appear when set: {"auto_merge": false,
232
271
  "auto_build": false, "auto_pull": false,
233
- "auto_triage": false, "github_login": "owner"}
234
- settings set auto-merge on|off on: the session merges an approved PR once CI is green and
272
+ "max_implementers": 10,
273
+ "auto_triage": false, "auto_review": false,
274
+ "idle_notification": false, "skip_draft": false,
275
+ "prs": true, "github_login": "owner"}
276
+ settings set auto-merge on|off on: the hub's tick merges an approved PR once CI is green and
235
277
  the merge state is CLEAN. off (the default): the card
236
- shows the owner a Merge button, and the session merges on its
237
- card-merge event. Board Settings' toggle writes it too
278
+ shows the owner a Merge button, and the board server merges
279
+ when it is tapped. Board Settings' toggle writes it too
280
+ settings set skip-draft on|off on: the hub's poll marks a card's draft PR ready for
281
+ review once its CI passes. off (the default): the draft
282
+ waits in In Review on the owner's review and Ready
283
+ settings set prs on|off on (the default): a board on GitHub opens a PR for each
284
+ card. off: no PR; the card lands on main and main is
285
+ pushed, which needs Push main after a land granted.
286
+ A local board never opens PRs. Without auto-merge, a
287
+ card with no PR waits in In Review for the owner's Merge
238
288
  build <wave>/<id> --site <route> asks the hub to run this card's .kickflip/build; prints
239
289
  --asked "<owner's words>" started and exits. install is an alias. Only the owner's
240
290
  Serve / Build and Auto-build start a card run, so --asked
@@ -250,16 +300,29 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
250
300
  the hub asks gh api user. Pass "" to clear it
251
301
  settings set auto-build on|off on: each push builds its card. Off by default.
252
302
  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.
303
+ settings set auto-pull on|off Auto implement Todo cards. on: on every board change the
304
+ board server starts and launches the build of the top
305
+ ready Todo cards nothing blocks, holds or asks about, one
306
+ 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.
258
310
  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
311
+ settings set max-implementers <1..99> Maximum parallel implementers, 10 by default: the cards
312
+ building or waiting in Orchestrator review the pull fills
313
+ up to. Planners and cards waiting on the owner don't count.
314
+ Only on the owner's word
315
+ settings set auto-review on|off on: while the board is idle and nothing is pulled, the board
316
+ server sends pr-review {pr, head, repo, title, author, url,
317
+ touches, asked} for one incoming PR at a time that touches
318
+ this board's cards or asks the owner for review. Off by
319
+ default. Only on the owner's word
320
+ settings set auto-triage on|off on: once a night, from the middle of the hub's quiet hours
321
+ to their end, when two or more cards wait on open
322
+ questions, nothing builds and something changed since the
323
+ last triage, the board server declares topic
324
+ overnight-triage-<date> ("Overnight triage <date>") and
325
+ sends overnight-triage {date, topic, cards}; the pull
263
326
  waits until you reply to it. Off by default. Only on the
264
327
  owner's word
265
328
  settings set tickets <json> the board's ticket source, as Board Settings' Tickets row
@@ -286,8 +349,13 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
286
349
  opened, it keeps that record's kind and worktree.
287
350
  [--dispatch] --dispatch: the session runs this right before it launches
288
351
  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.
352
+ is open at launch and the card reads preheating; the agent's
353
+ own builder start takes it up. On a record the Build tap
354
+ opened it changes nothing and prints "already open". The
355
+ record keeps dispatched: tap or session, whoever opened it.
356
+ --agent <other name> refuses while the tap's main record
357
+ is open and not taken up.
358
+ A build's start clears the card's Next.
291
359
  A build (--kind build, claude or codex) on an open card sets
292
360
  --status building first, refusing what that refuses;
293
361
  Prints "ref: <ref>", the card's label, which commit subjects start with.
@@ -297,7 +365,11 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
297
365
  ~/.claude/kickflip/worktrees/<id> on a new branch <id> from
298
366
  the board's branch of its clone (--repo overrides the clone), kept on
299
367
  the record and printed as "worktree: <path>". Refuses when
300
- that path or branch exists
368
+ that path or branch exists, unless it is a leftover
369
+ holding no work, which it remakes. A build's main record
370
+ on a board makes it without --worktree when it keeps none
371
+ (a taken-up or resumed record's, or the worktree the
372
+ card's last record left while it is still there)
301
373
  builder progress <wave>/<id> --phase <phase> one status line: a phase, reading, building, testing,
302
374
  (<detail> | --text <d>) [--agent <name>] checking or landing, and a present-tense detail of at most
303
375
  80 characters in plain words, which the owner reads at a
@@ -307,14 +379,46 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
307
379
  for another agent, and the card "Testing \xB7 <detail>"
308
380
  [--eta <ISO time> | --clear-eta] an ETA shows as a line under the card's status
309
381
  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
382
+ the card's status shows the newest one from an agent still working. builder progress refuses
383
+ without an open record for that agent, as builder call does
311
384
  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
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
387
+ record with unverified judgment calls it hands the card
388
+ back: --status checking with Next "Answer the builder's
389
+ questions" when a call starts "Question:", else "Verify
390
+ the builder's judgment calls"; --next is refused there.
391
+ Else, on a board, the main build's record that keeps a
392
+ worktree finishes the card, as land does, --text its
393
+ closing line: lands it, closes the card and its ticket
394
+ and, with the board's "Push main after a land" on, pushes
395
+ the branch to origin; on a GitHub board it pushes the
396
+ card's branch, opens a draft PR and moves the card to it,
397
+ and on a plain git remote pushes the branch for the
398
+ owner's merge (item set --pushed). A finish that fails
399
+ refuses with the reason and leaves the record open; one
400
+ refused while another agent's record is open names it.
401
+ --next stops a build without finishing it. Closing the
402
+ last record of a card left In Progress with no Next is
403
+ refused without --next, which sets it
404
+ [--owner-step <t>] a step only the owner can do once the code lands: the card
405
+ then waits in In Review with it as its Next, its ticket
406
+ open, until the owner's Done; refused on a card already
407
+ done, merged or dropped. land takes it too
408
+ [--check "<name> <args>"] with --owner-step: a read-only check on the step's final
409
+ evidence, which the hub's poll runs on each check once the
410
+ code lands, closing the card when it passes: tag
411
+ <owner/repo> <pattern>, npm <package> <version-or-range>,
412
+ run <owner/repo> <workflow> (a successful run since it was
413
+ attached), gh <owner/repo> <api-path> <field>=<value>, or
414
+ url <address> [<status> | <text>]. An unknown check or bad
415
+ arguments are refused. Three runs in a row that can't run
416
+ mark it broken and append owner-check-broken {key, check,
417
+ error}; fix it with item set --check. land takes it too
317
418
  builder takes --dir <dir> for a wave file in <dir>; its records go in <dir>/agents
419
+ 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
318
422
  builder pick <wave>/<id> who builds the card, from the hub's Codex share and the pause
319
423
  line, and for claude which model, from the hub's Claude model
320
424
  and the card's --size and --model: "codex: <reason>" or
@@ -327,33 +431,67 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
327
431
  its calls and worktree. The thread shows them, collapsed
328
432
  builder calls <wave>/<id> [--agent <name>] prints the record's calls, numbered, then "verified <time>"
329
433
  or "not verified yet"
434
+ builder ask <wave>/<id> <question> [--agent <name>] a running builder's one-line question for the board's session,
435
+ which it asks at any point and carries on meanwhile: a thread
436
+ line named by its model ("Claude Opus builder"), never unread,
437
+ and a builder-ask {key, ask, agent, text} event. Prints its
438
+ id, ask<n>. Needs --site and an open builder record
439
+ builder answer <wave>/<id> <ask> --text <t> the session's answer to that ask, a thread line named
440
+ Orchestrator that leaves the card quiet
441
+ builder defer <wave>/<id> <ask> --text <t> the session's deferral of an ask it can't answer confidently:
442
+ the same thread line, and a "Question: <ask> (deferred to the
443
+ owner)" call on the asking agent's record. After its review the
444
+ session puts it to the owner as a card question with
445
+ "ask": "<ask>"; builder verified refuses until then
446
+ builder asks <wave>/<id> prints each ask, then its answer, its deferral or "waiting on
447
+ the orchestrator"
330
448
  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
449
+ [--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
451
+ call verified) it finishes the card in the same step, as
452
+ builder done does, --note its closing line (else "Judgment
453
+ calls verified."). While a question on the card is open it
454
+ says so and the card waits; a finish that fails keeps the
455
+ mark, the card checking, and says why
332
456
  land <wave>/<id> --text <t> [--repo <dir>] brings the worktree its builder record keeps (else
333
457
  [--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;
458
+ ~/.claude/kickflip/worktrees/<id> of --repo) into the board's
459
+ branch (the one its builders branch from, else main), one
460
+ line per step: refuses unless the repo has that branch
461
+ checked out, naming it, or while the worktree has uncommitted
462
+ changes or adds no commit; rebases onto the branch when it
463
+ moved, refusing on a conflict with the rebase undone; refuses
464
+ a new ADR named with a number instead of a timestamp; refuses
465
+ unless its full node --test suite passes, naming each failing
466
+ test and saving the TAP output under ~/.claude/kickflip/logs;
467
+ fast-forwards the branch in the repo;
341
468
  restarts the hub when its code changed, else each board
342
469
  server when board/ changed; removes the worktree and its
343
- branch. Never pushes. One land runs at a time on the host: a
470
+ branch. Without --site it never pushes. One land runs at a time on the host: a
344
471
  second refuses, naming the card landing. With --site, --text <what was checked>
345
472
  is required, and once it lands the card closes as item set
346
473
  --status done --text does; it refuses first what that close
347
- would. KICKFLIP_RESTART_CMD='["<cmd>"]' replaces
474
+ would. With --site it is builder done's finish: the push
475
+ after a land, and on a GitHub or plain-remote board the
476
+ branch push (and draft PR) instead of the fast-forward.
477
+ --owner-step <t>, as builder done takes it, keeps the
478
+ landed card in In Review on that step until the owner's
479
+ Done, or until its --check passes. KICKFLIP_RESTART_CMD='["<cmd>"]' replaces
348
480
  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"),
481
+ merge <wave>/<id> [--after-approval "<why trivial>"] merges the card's PRs (gh pr merge --squash) in its order and
482
+ [--confirmed "<owner's words>"] records it merged, once GitHub reads every open one approved,
483
+ its checks green with none pending and its merge state CLEAN,
484
+ the test the Merge button uses; a merged or closed PR is left
485
+ as it is. Refuses, naming each reason ("pending: 2 checks"),
353
486
  commits after the newest approval unless --after-approval
354
487
  says why they're trivial (its closing line says so), and a
355
488
  card with manual steps unless --confirmed quotes the owner.
356
- A refusal also goes in the thread while the owner's Merge waits
489
+ A refusal also goes in the thread while the owner's Merge waits.
490
+ On a card with no PR waiting for the owner's Merge, which
491
+ the board server runs on the tap: rebases onto the base as
492
+ it is now, reruns the tests, fast-forwards, closes the card
493
+ and pushes when Push main is on; a conflict or failing
494
+ tests launch a headless lander that lands it
357
495
  arch file <wave>/<id> turns the architecture review audit card's answered findings
358
496
  into cards in the review's wave: titled with the picked
359
497
  option's label, its detail, practice and the owner's note as
@@ -362,18 +500,19 @@ creating the issue and running item set <wave>/<id> --ticket <id>.
362
500
  Prints "filed: <ref> from <id>", "skipped: <id>", and
363
501
  "waiting: <ids>" while any are unanswered; once none is, it
364
502
  closes the audit card as done
365
- push --kind action|prompt|fault --title <t> runs kickflip push with these flags, adding
503
+ push --kind fault|reminder --title <t> runs kickflip push with these flags, adding
366
504
  [--body <b>] [--url </route/path>] "\xB7 and N replies" to the body for the replies
367
505
  [--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
506
+ command (none before its first). A session pushes only a
507
+ fault, what only the owner can fix, or a reminder the
508
+ owner asked for in words ("push me when it's all done"):
509
+ the board server pushes each card's Action needed and the
510
+ hub a session's prompt, so any other --kind is refused. It records the
511
+ push in data/push-state.json only when kickflip push
512
+ succeeds, and exits with kickflip push's code. Without --now
374
513
  it queues until the tick ends (checked) or the hub's
375
514
  next check; --now sends at once unless pushes are held
376
515
  (overnight, snoozed or the weekend). --urgent: a
377
516
  production fault only; it beats those holds. While
378
517
  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};
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};