@lovek4d/kickflip 0.1.0 → 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 (120) hide show
  1. package/CONTEXT.md +239 -104
  2. package/README.md +54 -4
  3. package/SKILL.md +63 -42
  4. package/bin/doctor.mjs +4 -3
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +103 -40
  7. package/bin/statusline.mjs +4 -3
  8. package/bin/wait-events.mjs +2 -2
  9. package/board/arch-review.mjs +5 -0
  10. package/board/auto-review.mjs +2 -0
  11. package/board/bin/agent.mjs +1 -1
  12. package/board/bin/board.mjs +371 -82
  13. package/board/bin/builder-stop-hook.mjs +4 -3
  14. package/board/domain.mjs +8 -3
  15. package/board/finish.mjs +3 -0
  16. package/board/headless.mjs +3 -0
  17. package/board/on-host.mjs +1 -0
  18. package/board/permissions.mjs +2 -0
  19. package/board/public/app-arch.js +75 -0
  20. package/board/public/app-asks.js +66 -28
  21. package/board/public/app-banner.js +2 -2
  22. package/board/public/app-cards.js +354 -77
  23. package/board/public/app-core.js +40 -8
  24. package/board/public/app-header.js +33 -8
  25. package/board/public/app-lanes.js +238 -34
  26. package/board/public/app-needs.js +2 -0
  27. package/board/public/app-snapshot.js +45 -11
  28. package/board/public/app-threads.js +20 -21
  29. package/board/public/app.html +6 -2
  30. package/board/public/app.js +114 -33
  31. package/board/public/attach.js +39 -3
  32. package/board/public/dialogs.js +161 -118
  33. package/board/public/hub-kit.js +27 -7
  34. package/board/public/hub.css +26 -33
  35. package/board/public/linear-key.js +143 -0
  36. package/board/public/log.html +48 -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 +14 -10
  42. package/board/public/rules-editor.js +4 -5
  43. package/board/public/shell.js +21 -7
  44. package/board/public/sidebar.css +51 -11
  45. package/board/public/sidebar.js +124 -19
  46. package/board/public/style.css +244 -56
  47. package/board/public/toasts.js +5 -3
  48. package/board/public/ui-cards.js +236 -66
  49. package/board/public/ui-enums.js +5 -4
  50. package/board/public/ui-header.js +23 -14
  51. package/board/public/ui-hub.js +34 -16
  52. package/board/public/ui-links.js +18 -10
  53. package/board/public/ui-markdown.js +1 -1
  54. package/board/public/ui-order.js +21 -0
  55. package/board/public/ui-page-header.js +13 -8
  56. package/board/public/ui-patch.js +53 -0
  57. package/board/public/ui-phase-bars.js +17 -0
  58. package/board/public/ui-prompts.js +8 -8
  59. package/board/public/ui-questions.js +35 -11
  60. package/board/public/ui-sidebar.js +83 -21
  61. package/board/public/ui-snapshot.js +30 -13
  62. package/board/public/ui-threads.js +38 -23
  63. package/board/public/ui.js +96 -16
  64. package/board/public/view-frame.js +1 -1
  65. package/board/public/view-hub-settings.js +67 -44
  66. package/board/public/view-hub.js +105 -13
  67. package/board/public/view-notes.js +8 -5
  68. package/board/public/view-questions.js +123 -52
  69. package/board/public/view-settings.js +235 -62
  70. package/board/review.mjs +1 -0
  71. package/board/server.mjs +18 -7
  72. package/board/store.mjs +15 -6
  73. package/board/tickets.mjs +3 -3
  74. package/board/worktree.mjs +20 -13
  75. package/docs/hub-session.md +4 -4
  76. package/hooks-plugin/hooks/codex-share-auto.mjs +7 -5
  77. package/hooks-plugin/hooks/watcher-stop.mjs +41 -0
  78. package/hub/awake.mjs +1 -0
  79. package/hub/board-repo.mjs +1 -1
  80. package/hub/boards.mjs +4 -4
  81. package/hub/close-board.mjs +1 -1
  82. package/hub/duration.mjs +1 -0
  83. package/hub/github.mjs +3 -4
  84. package/hub/home.mjs +1 -1
  85. package/hub/hub.mjs +12 -10
  86. package/hub/incoming.mjs +2 -0
  87. package/hub/linear.mjs +2 -2
  88. package/hub/live.mjs +2 -2
  89. package/hub/new-board.mjs +2 -2
  90. package/hub/permission-denied-hook.mjs +2 -0
  91. package/hub/pr-report.mjs +3 -0
  92. package/hub/presence.mjs +1 -1
  93. package/hub/prompts.mjs +1 -1
  94. package/hub/push.mjs +2 -2
  95. package/hub/readiness.mjs +2 -0
  96. package/hub/repos.mjs +3 -2
  97. package/hub/restart-board.mjs +1 -0
  98. package/hub/routes.mjs +1 -1
  99. package/hub/schedule.mjs +1 -1
  100. package/hub/sessions.mjs +2 -2
  101. package/hub/shell.mjs +2 -1
  102. package/hub/slack.mjs +2 -0
  103. package/hub/stream.mjs +12 -12
  104. package/hub/sw.js +2 -2
  105. package/hub/trust.mjs +2 -0
  106. package/hub/try.mjs +8 -3
  107. package/hub/upgrade.mjs +1 -0
  108. package/hub/usage.mjs +3 -3
  109. package/hub/views.mjs +1 -1
  110. package/package.json +5 -1
  111. package/shared/card-prs.mjs +1 -0
  112. package/shared/colors.css +9 -6
  113. package/shared/fake-gh.mjs +105 -0
  114. package/shared/repos.mjs +1 -1
  115. package/shared/service.mjs +16 -16
  116. package/shared/skills.mjs +2 -0
  117. package/shared/usage-gate.mjs +1 -1
  118. package/starter/public/claude.js +3 -2
  119. package/bin/web-ts.mjs +0 -2
  120. package/board/public/view-polling.js +0 -87
@@ -1,24 +1,25 @@
1
1
  #!/usr/bin/env node
2
- var _=Object.defineProperty;var l=(e,s)=>_(e,"name",{value:s,configurable:!0});import{routeRecord as S}from"../../hub/routes.mjs";import g from"node:fs";import f from"node:path";import{spawnSync as O}from"node:child_process";import{fileURLToPath as C,pathToFileURL as E}from"node:url";import*as n from"../store.mjs";import{makeWorktree as q,land as T,skillRepo as L,worktreesDir as N}from"../worktree.mjs";import{ITEM_STATUSES as F}from"../domain.mjs";import{boardAt as K,readManifest as M}from"../../hub/boards.mjs";import{boardRepo as D}from"../../hub/board-repo.mjs";import{pushKindError as B}from"../../hub/push.mjs";import{migrateOnStart as H}from"../../bin/install.mjs";const U=`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.
6
6
  Paths resolve under $HOME, so HOME=<dir> points every command at a copy.
7
7
  A --file argument of - reads stdin.
8
- item add and followup add create a ticket in the board\u2019s source unless --ticket <id> links one.
8
+ item add creates a ticket in the board\u2019s source unless --ticket <id> links one.
9
9
  Internal defaults to INT; GitHub and Linear create an issue from the title and notes, Linear in the board's team and
10
10
  project through Hub's Linear key. A linked ticket must exist in the source; an Internal one no other card holds.
11
11
  Asks have no ticket; item set --from-ask creates one when the card has none.
12
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> (followup set for a follow-up).
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
- reply <eventId> (--html <text> | --file <path>) data/replies/<eventId>.html
20
- working <eventId> marks an ask or thread message started; the page
21
- shows "Working on it" until a reply or later thread line
19
+ reply <eventId> (--html <text> | --file <path>) data/replies/<eventId>.html. A reply to an ask resolves its
20
+ answered questions (topic item:ask:<eventId>), and one to
21
+ wave-triage its wave's, noting "Acted on: replied". A
22
+ reply to overnight-triage ends that triage
22
23
  thread <wave>/<id> --text <t> [--link <url>] one line in the item's thread; ask:<eventId> in place
23
24
  [--who claude|love|<name>] [--via github|slack] of <wave>/<id> writes in that ask's thread
24
25
  A Claude thread line, or a reply to an ask or item-message, rewrites the body of the thread's
@@ -27,8 +28,9 @@ creating the issue and running item set <wave>/<id> --ticket <id> (followup set
27
28
  topic add <id> --title <t> [--summary <s>] a question topic in data/owner.json
28
29
  question add --file <json> one question or an array of them:
29
30
  {id, topic, question, options: [{id, label, recommended?, detail?, diagram?, practice?}],
30
- source?, context?, url?, urgent?}; topic is declared, item:<wave>/<id>, or item:ask:<eventId>
31
- for a New card not sorted yet.
31
+ source?, context?, url?, urgent?, round_of?, ask?}; topic is declared, item:<wave>/<id>, item:ask:<eventId>
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 question on a done or merged card reopens it.
32
34
  url is http(s), or a site page such as prototype-<topic>.html?q=<id>, stored as given.
33
35
  Prototype options keep take order, ids matching take keys; recommended: true tags the recommendation.
34
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
@@ -37,12 +39,20 @@ creating the issue and running item set <wave>/<id> --ticket <id> (followup set
37
39
  danger|success, to the next row's box indexes (default all); practice: ["<line>", \u2026]
38
40
  A prototype url's topic must be recorded (prototype add), its option ids a non-empty subset of the
39
41
  take keys in take order, without repeats, and every take file written; a hand-written prototype-*.html
40
- is refused. On an item: topic, each question answered and still waiting on Claude is resolved,
41
- noting "Followed by <new id>", and printed as "resolved: <old id>". Any other unresolved question
42
- on the same topic that links the same prototype is resolved, noting "Replaced by <new id>", and
43
- printed as "replaced: <old id>". Answering or resolving a question drops its queued push tagged
44
- q-<id> on this site.
45
- question resolve <id> --note <text> sets resolved and resolved_at
42
+ is refused. On an item: topic, each other question answered and still waiting on Claude is
43
+ resolved, noting "Followed by <new id>", and printed as "resolved: <old id>". Any other unresolved
44
+ question on the same topic that links the same prototype is resolved, noting "Replaced by <new id>",
45
+ and printed as "replaced: <old id>". round_of names the question on the same topic whose
46
+ prototype-request this answers; the question needs a prototype url, takes the next round
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.
50
+ The board server pushes the new open questions itself, under the card's tag (item-<wave>/<id>, or
51
+ ask-<eventId>) or topic-<topic> off the cards
52
+ question resolve <id> --note <text> sets resolved and resolved_at; refuses a question on a
53
+ prototype page until an answer settles its round: a pick, with a note or none, or an answer to
54
+ an "Is this it?" confirm already on the board. A question already resolved, such as one an
55
+ answer with a note and no option resolved, is left as it is
46
56
  prototype add <topic> --title <t> --take <K>:<label> records a prototype in data/prototypes.json: the board
47
57
  [--take <K>:<label> \u2026] [--card <wave>/<id>] serves prototype-<topic>.html from its one template, with
48
58
  the title, a back arrow to --card (else the board), the
@@ -51,21 +61,58 @@ creating the issue and running item set <wave>/<id> --ticket <id> (followup set
51
61
  "A. ", since the pill shows it alone. Prints each take
52
62
  file to write,
53
63
  public/prototypes/<topic>/<K>.js, which exports
54
- render(stage, kit) (board/public/prototype-kit.js lists kit)
64
+ render(stage, kit) (board/public/prototype-kit.js lists kit).
65
+ --card starts that card: it leaves Todo
55
66
  prototype list one line per recorded prototype
56
67
  prototype rm <topic> drops the record, keeping its take files; refuses while an
57
68
  open question links it
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
58
79
  item set <wave>/<id> [--status <s> | --clear-status] edits the item in its wave file
59
- [--next <t> | --clear-next] [--pr <n>] status: ${F.join(", ")}
80
+ [--next <t> | --clear-next] status: ${K.join(", ")}
60
81
  an empty --next "" clears it, as --clear-next does
61
82
  naming another unfinished card requires it in --deps
62
- [--from-ask <eventId>] the ask it came from: its New card gives way to this item,
63
- whose thread opens with the ask and its thread; it leaves
64
- a card it was folded into
65
- [--held | --unheld] --held sets held: true, for the owner's card-hold;
66
- --unheld clears it and a held status
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
86
+ [--pushed <branch>] on a board whose repo is a plain git remote (no GitHub):
87
+ the card's branch is pushed there, so it waits In Review
88
+ on the owner's merge. Push it first (git push origin
89
+ <branch>). The hub's repo fetch moves it to Done once the
90
+ branch is merged into the board's branch or gone from the
91
+ remote. Clears building/fixing as --pr does
92
+ [--gh-since <ISO time>] records the last handled GitHub event, an ISO-8601 UTC
93
+ time such as 2026-10-23T11:00:00Z, stored as given
94
+ [--from-ask <eventId> --text <t>] the ask it came from: its New card gives way to this item,
95
+ whose thread opens with the ask and its thread, then --text
96
+ as the item's first line; it leaves a card it was folded into; refuses a card
97
+ from another ask (use item relate-ask), and one with no
98
+ state when --state is missing
99
+ [--state ready|grilling|prototyping] what a card from an ask does next; it reads New until set.
100
+ ready refuses while the card has an open question
101
+ [--held | --unheld | --pick-up-at <time>] holds or resumes the card on the owner's words; the board
102
+ server records the owner's own Hold and Resume taps. A later
103
+ --held/--unheld overrides a tap; --unheld also clears a held status.
104
+ --pick-up-at holds it until an ISO time such as
105
+ 2026-11-04T09:00 (the Mac's local time without an
106
+ offset; a past time is refused), stored as UTC
107
+ pick_up_at. Within a minute of it the board server
108
+ resumes the card, adds a thread line, and appends
109
+ card-unblocked {key, until} unless a dep is unfinished.
110
+ --held holds it with no time; --pick-up-at "" resumes it
67
111
  [--ticket <id>] links a ticket in the board\u2019s source
68
112
  [--title <t>] renames the card
113
+ [--size small|large] [--model opus|sonnet] what builder pick reads while the hub's Claude model is
114
+ Auto: a small card gets Claude Sonnet, any other Opus, and
115
+ --model wins over --size; "" clears either
69
116
  [--notes-file <file>] the item's notes: a .md file inside its wave's folder,
70
117
  named relative to it, holding the session's understanding
71
118
  of the task so far. The open card's Notes link opens it,
@@ -74,31 +121,49 @@ creating the issue and running item set <wave>/<id> --ticket <id> (followup set
74
121
  Claude, noting "Recorded in the card's notes", and
75
122
  prints "resolved: <id>" for each
76
123
  [--deps <id>,<wave>/<id>] replaces its blocked-by list; --deps "" clears it
124
+ [--parent <wave>/<id>] sets its Parent, refusing itself or a card below it;
125
+ --parent "" clears it
126
+ [--related <id>,<wave>/<id>] replaces the cards it is Related to, which both cards
127
+ show; --related "" clears it, on both
77
128
  [--install <url> [--install-sha <sha>] a device build the owner installs from the card's Install
78
129
  [--install-label <text>] | --clear-install] button, stamped built_at now: itms-services://?action=
79
130
  download-manifest&url=https://\u2026/manifest.plist for iOS,
80
131
  an https:// .apk URL for Android. --install-sha is the
81
132
  head it was built from: the badge reads "Install (stale)"
82
133
  once the branch moves past it. Hidden once the card is Done
134
+ [--text <t>] with --status done or dropped, required on a site: the
135
+ card's closing line, written last in its thread. merged
136
+ writes "Merged PR #n." unless --text says otherwise
137
+ builder start for a build sets building itself. --status building resolves the card's answered
138
+ questions, noting "Acted on: build started".
83
139
  --status building refuses while the item is held or a dep isn't done, merged or dropped,
84
- and while the newest owner answer to its questions that link a prototype-*.html page has a
85
- note: post the final version with prototype add and question add first.
86
- [--confirmed "<owner's words>"] passes when that note itself says go: the words must quote
87
- it, and they go in a thread line on the card;
140
+ and while the newest owner answer to its questions that link a prototype-*.html page picks
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;
88
143
  done, merged and dropped print "unblocked: <wave>/<id>" and append card-unblocked {key}
89
144
  once per unblocking, including held cards: start each unheld card next.
90
145
  building and fixing count as working only while a builder record on the card is open.
91
146
  --pr clears building/fixing and closes ordinary builders; install builders keep running.
92
- checking is a builder's hand-back: In Progress, reading "Orchestrator review",
93
- until the owning session verifies it and sets done; it unblocks nothing.
94
- Every --status or --clear-status records status_at, which ends the Done button's "Closing\u2026"
95
- for the owner's card-done; apply a card-done with --status done.
147
+ checking is a builder's hand-back of work it couldn't land: In Progress, reading "Orchestrator
148
+ review", until the owning session takes it on; it unblocks nothing.
149
+ The board server records the owner's card-done in the wave file itself.
150
+ Every --status or --clear-status records status_at; a later status change overrides the tap.
96
151
  --status done, merged or dropped closes the card: it resolves each of its questions (topic
97
152
  item:<wave>/<id>) that has an answer, noting "Closed with the card", and prints "resolved: <id>"
98
- for each. It refuses while one has no answer yet; [--force] closes it and resolves that one too
99
- item add <wave>/<id> --title <t> [--notes <t>] a new card, planned (waiting when it has --deps); never
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
156
+ item add <wave>[/<id>] --title <t> [--notes <t>] a new card, planned (waiting when it has --deps); never
100
157
  [--ticket <id>] [--deps <id>,<wave>/<id>] hand-edit wave.json to add one. Refuses an id the wave has.
101
158
  [--notes-file <file>] --plan is an alias of --notes-file
159
+ [--pick-up-at <time>] holds it until then, as item set's --pick-up-at
160
+ [--size small|large] [--model opus|sonnet] as item set's
161
+ [--parent <wave>/<id>] the card it was spun out of, such as one filed from
162
+ another card's thread, grill or answer: it reads as a
163
+ follow-up, and its Parent lists it in Children. A card
164
+ from the + or New task has none
165
+ Without /<id>, the card takes its ticket's id (GH-<n> for issue #n); pass an id only to import a
166
+ card. Prints "<label> added as <key>": use the key in later commands
102
167
  item relate-ask <eventId> <wave>/<id> --summary <t> folds an ask about an existing item into it: the ask's
103
168
  card goes, its message and thread show in the item's
104
169
  thread marked Moved, then the summary line. Again with
@@ -116,18 +181,40 @@ creating the issue and running item set <wave>/<id> --ticket <id> (followup set
116
181
  A card the ask became is dropped, unless it has started.
117
182
  Refuses a route that isn't another board, and an ask
118
183
  that isn't New
119
- hello-ack <eventId> answers the hub's hello: data/handshake.json records that the
120
- session hears the board, which clears the setup checklist's
121
- Events step. Refuses an older hello than the newest
122
184
  checked [--every <minutes> | --idle] data/checked.json: GitHub was just checked, next in
123
- <minutes> (default 30); run it at the end of each monitor tick.
124
- --idle: the board shows no open PR, so never late.
185
+ <minutes> (default 5, the away cadence); run it at the
186
+ end of each GitHub tick.
187
+ --idle: the board shows no open PR, so never late; with
188
+ neither flag it is idle while no card has an open PR.
125
189
  Then sends the queued pushes, each on its own, unless
126
190
  pushes are held (overnight, snoozed, the weekend or off)
127
- followup add <wave> --id <id> --title <t> [--from <item id>] [--ticket <id>]
128
- followup done <wave> <id> [--force] closes the follow-up's card as item set --status done does
129
- followup set <wave> <id> --ticket <id> links the follow-up's ticket in the board's source
130
- tickets backfill [--dry-run] gives every open card and follow-up without a ticket one: the first
191
+ lineup create <slug> --title <t> --goal <t> a wave, in the owner's words: a named set of cards in
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
199
+ goal, the order and the note. Refuses a card already in
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
204
+ lineup remove <slug> <wave>/<id> takes a card out: it loses its links to the wave's cards,
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
213
+ links. A wave whose cards are all Done leaves it too
214
+ A band's Triage sends wave-triage {lineup}, or {wave} for a wave file's band; it reads
215
+ Triaging\u2026 until you reply to that event, then "Triage: N open questions" while questions on topic
216
+ lineup:<slug> or wave:<slug> are open or answered
217
+ tickets backfill [--dry-run] gives every open card without a ticket one: the first
131
218
  id in its title (or legacy linear list) from the board's
132
219
  source is linked, else a new ticket is made. Done cards are
133
220
  left alone. Prints each card's result; --dry-run changes and
@@ -137,6 +224,31 @@ creating the issue and running item set <wave>/<id> --ticket <id> (followup set
137
224
  The settings page lists them; new waves copy them
138
225
  reviewers add --file <json> one reviewer or an array; replaces the same name
139
226
  reviewers remove <name>... refuses a name the board doesn't have
227
+ slack Slack identity as JSON; imports slack-bridge config once
228
+ slack set [--self-dm D..] [--user U..] sets hub Slack identity in ~/.claude/kickflip/slack.json
229
+ [--domain x] [--timezone Area/City]
230
+ review sent <wave>/<id> <reviewer> --ts <ts> records the PR link's message in the item
231
+ --channel <dm>
232
+ review react <wave>/<id> <reviewer> <emoji> records a reaction once on that reviewer's link
233
+ integrations readiness rows as one JSON line
234
+ integrations report --slack yes|no with --site: reports whether this session has Claude's
235
+ Slack connector, in ~/.claude/kickflip/integrations.json
236
+ review due one JSON line of the Slack sends owed, from local state:
237
+ {dms: [{key, reviewer, channel, text, record}], reactions:
238
+ [{key, reviewer, channel, ts, emoji, record}], digest:
239
+ review digest's answer while due (with record), else null}.
240
+ dms: a PR link to each reviewer of an open, non-draft PR with
241
+ no link recorded; reactions: git_merged on each recorded link
242
+ of a merged card. record is the command to run once sent.
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
249
+ review digest pending reviews as JSON; due weekday mornings from 09:00
250
+ review digest sent <reviewer> --ts <ts> records a digest in data/review-digests.json;
251
+ --channel <dm> --items <key,key> marks today sent in the Slack identity timezone
140
252
  rules list the hub rules, then with --site the board's, one
141
253
  "<hub|board> <id> <owner|claude> <text>" line each
142
254
  rules add --text <t> [--hub] [--by claude] a rule in board.json, or with --hub in the hub's
@@ -144,64 +256,241 @@ creating the issue and running item set <wave>/<id> --ticket <id> (followup set
144
256
  --by claude when you codify one on the owner's word
145
257
  rules edit <id> --text <t> [--hub] rewords a rule, keeping who added it
146
258
  rules remove <id> [--hub] refuses an id the list doesn't have
147
- settings the board's settings as JSON, each false when board.json
148
- leaves it out: {"auto_merge": false, "auto_build": false}
149
- settings set auto-merge on|off on: monitor merges an approved PR once CI is green and
259
+ settings the board's settings as JSON; flags default off, optional
260
+ settings appear when set: {"auto_merge": false,
261
+ "auto_build": false, "auto_pull": false,
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"}
267
+ settings set auto-merge on|off on: the session merges an approved PR once CI is green and
150
268
  the merge state is CLEAN. off (the default): the card
151
- shows the owner a Merge button, and monitor merges on its
269
+ shows the owner a Merge button, and the session merges on its
152
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
153
274
  build <wave>/<id> --site <route> asks the hub to run this card's .kickflip/build; prints
154
- started and exits. install is an alias. The hub records
155
- data/try/<id>.log and writes failures to the thread
275
+ --asked "<owner's words>" started and exits. install is an alias. Only the owner's
276
+ Serve / Build and Auto-build start a card run, so --asked
277
+ must quote the owner's ask, card message or answer note on
278
+ this board that asked for it; the words go in a thread
279
+ line on the card. The hub records data/try/<id>.log and
280
+ writes failures to the thread. Web Visit opens
281
+ https://<host>:<try port>/ at its root
156
282
  main set --install <url> [--install-sha <sha>] publishes main's app install to data/main.json
283
+ settings set github-login <login> the GitHub account Claude posts as on this board. Its
284
+ comments carrying <!-- claude-bridge --> are Claude\u2019s own
285
+ and dropped; its other comments are the owner\u2019s. Unset,
286
+ the hub asks gh api user. Pass "" to clear it
157
287
  settings set auto-build on|off on: each push builds its card. Off by default.
158
288
  Main builds after every merge either way
159
- builder start <wave>/<id> [--agent <name>] [--kind <opus|codex|sonnet|install|\u2026>] [--label <who>]
160
- [--progress <text>] [--eta <ISO time>] one record per agent on the item; --agent defaults to main.
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.
295
+ Only on the owner's word
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
322
+ waits until you reply to it. Off by default. Only on the
323
+ owner's word
324
+ settings set tickets <json> the board's ticket source, as Board Settings' Tickets row
325
+ saves it: {"source":"internal","prefix":"OPS"},
326
+ {"source":"github","account":"<gh login>"} or
327
+ {"source":"linear","workspace","team","project"}. A
328
+ GitHub account runs the board's issue calls as that gh
329
+ login; without one they run as gh's active account
330
+ builder start <wave>/<id> [--agent <name>] [--kind <claude|codex|prototype|grill|install|\u2026>] [--label <who>]
331
+ [--model <opus|sonnet>] the Claude model running the builder, recorded and printed as
332
+ "model: Claude Sonnet" (kind claude takes builder pick's model;
333
+ codex prints "model: Codex"). The thread's agent line reads it: "Claude
334
+ Sonnet builder started"; the label carries no model word. A
335
+ build refuses the model the hub's Claude model rules out.
336
+ A prototype defaults to sonnet and a grill to opus:
337
+ "Claude Sonnet prototype started"
338
+ [--phase <phase> --progress <detail>] one record per agent on the item; --agent defaults to main.
339
+ [--eta <ISO time>] --phase and --progress take what builder progress takes.
161
340
  --kind install: a build of the card's install, whose
162
- Install badge reads Building\u2026 meanwhile
341
+ Install badge reads Building\u2026 meanwhile. --kind claude or
342
+ codex refuses the one builder pick rules out; --kind build
343
+ records the pick's kind and prints "kind: <kind>. <reason>";
344
+ on the open record a Build tap started or --dispatch
345
+ opened, it keeps that record's kind and worktree.
346
+ [--dispatch] --dispatch: the session runs this right before it launches
347
+ an agent of any kind (build, prototype, grill) so the record
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.
355
+ A build (--kind build, claude or codex) on an open card sets
356
+ --status building first, refusing what that refuses;
357
+ Prints "ref: <ref>", the card's label, which commit subjects start with.
358
+ Resolves the card's answered questions, noting "Acted on:
359
+ builder started"
163
360
  [--worktree [--repo <dir>]] --worktree: a git worktree for the builder at
164
361
  ~/.claude/kickflip/worktrees/<id> on a new branch <id> from
165
362
  the board's branch of its clone (--repo overrides the clone), kept on
166
363
  the record and printed as "worktree: <path>". Refuses when
167
- that path or branch exists
168
- builder progress <wave>/<id> (<text> | --text <t>) one short present-tense line
169
- [--agent <name>]
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)
369
+ builder progress <wave>/<id> --phase <phase> one status line: a phase, reading, building, testing,
370
+ (<detail> | --text <d>) [--agent <name>] checking or landing, and a present-tense detail of at most
371
+ 80 characters in plain words, which the owner reads at a
372
+ glance; a longer detail is refused with its length. Hashes,
373
+ test counts and file names go in the hand-back. The thread
374
+ line reads "Testing: <detail>", "Codex testing: <detail>"
375
+ for another agent, and the card "Testing \xB7 <detail>"
170
376
  [--eta <ISO time> | --clear-eta] an ETA shows as a line under the card's status
171
377
  A progress line, --progress included, needs --site: it is a status line in the item's thread, and
172
- 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
173
380
  builder done <wave>/<id> [--agent <name>] marks that agent's record done; the item's thread
174
- shows when it started and finished; clears building/fixing
175
- when the card has no open builder records left
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
176
404
  builder takes --dir <dir> for a wave file in <dir>; its records go in <dir>/agents
177
- land <wave>/<id> [--repo <dir>] [--dir <dir>] brings the worktree its builder record keeps (else
178
- ~/.claude/kickflip/worktrees/<id> of --repo) into main, one line
179
- per step: refuses while it has uncommitted changes or adds no
180
- commit; rebases onto main when main moved, refusing on a
181
- conflict with the rebase undone; refuses unless its full
182
- node --test suite passes, naming each failing test and
183
- saving the TAP output under ~/.claude/kickflip/logs;
184
- fast-forwards main in the repo;
405
+ builder pick <wave>/<id> who builds the card, from the hub's Codex share and the pause
406
+ line, and for claude which model, from the hub's Claude model
407
+ and the card's --size and --model: "codex: <reason>" or
408
+ "claude sonnet: <reason>". Pass that kind to builder start
409
+ --kind, and launch that model
410
+ builder call <wave>/<id> <judgment call> appends one line to the agent's open builder record: a
411
+ [--agent <name>] judgment call as the builder makes it, or a "Question: \u2026"
412
+ for the session; refused with no open record. A builder start
413
+ on a record with unverified calls resumes that build, keeping
414
+ its calls and worktree. The thread shows them, collapsed
415
+ builder calls <wave>/<id> [--agent <name>] prints the record's calls, numbered, then "verified <time>"
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"
431
+ builder verified <wave>/<id> [--agent <name>] the session's mark that it checked the record's calls; land
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
439
+ land <wave>/<id> --text <t> [--repo <dir>] brings the worktree its builder record keeps (else
440
+ [--dir <dir>]
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;
185
451
  restarts the hub when its code changed, else each board
186
452
  server when board/ changed; removes the worktree and its
187
- branch. Never pushes. KICKFLIP_RESTART_CMD='["<cmd>"]' replaces
453
+ branch. Without --site it never pushes. One land runs at a time on the host: a
454
+ second refuses, naming the card landing. With --site, --text <what was checked>
455
+ is required, and once it lands the card closes as item set
456
+ --status done --text does; it refuses first what that close
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
188
463
  the restarts, called with hub or board <route>
189
- push --kind action|prompt|fault --title <t> runs kickflip push with these flags, adding
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"),
469
+ commits after the newest approval unless --after-approval
470
+ says why they're trivial (its closing line says so), and a
471
+ card with manual steps unless --confirmed quotes the owner.
472
+ A refusal also goes in the thread while the owner's Merge waits
473
+ arch file <wave>/<id> turns the architecture review audit card's answered findings
474
+ into cards in the review's wave: titled with the picked
475
+ option's label, its detail, practice and the owner's note as
476
+ notes, each after the wave's last card, its question resolved
477
+ "Filed as <ref>."; a Skip pick resolves its question alone.
478
+ Prints "filed: <ref> from <id>", "skipped: <id>", and
479
+ "waiting: <ids>" while any are unanswered; once none is, it
480
+ closes the audit card as done
481
+ push --kind fault|reminder --title <t> runs kickflip push with these flags, adding
190
482
  [--body <b>] [--url </route/path>] "\xB7 and N replies" to the body for the replies
191
483
  [--tag <tag>] [--now | --urgent] written since the site's last push through this
192
- command (none before its first). For an item, tag it
193
- ask-<eventId> or item-<wave>/<id>; a card whose action
194
- changes pushes again under its tag. It records the
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
195
489
  push in data/push-state.json only when kickflip push
196
- succeeds, and exits with kickflip push's code.
197
- --kind: action when a card's Action needed appears or
198
- changes, fault for what only the owner can fix. Refused
199
- without one: news stays on the board.
200
- Without --now it queues until the tick ends (checked) or
201
- the hub's next check: drafts on Ready, Merge. --now sends
202
- at once unless pushes are held (overnight, snoozed or the
203
- weekend): questions, steps only the owner can run,
204
- prototypes asked for. --urgent: a production fault only;
205
- it beats those holds. While notifications are off every
206
- push is dropped, urgent too
207
- KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class r extends Error{static{l(this,"UsageError")}}const W=new Set(["clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","hub","clear-install","force","worktree","dry-run"]),J=new Set(["take"]),G=f.join(f.dirname(C(import.meta.url)),"..","..","bin","kickflip.mjs");function z(){if(!process.env.KICKFLIP_PUSH_CMD)return[process.execPath,G,"push"];try{const e=JSON.parse(process.env.KICKFLIP_PUSH_CMD);if(Array.isArray(e)&&e.length&&e.every(s=>typeof s=="string"))return e}catch{}throw new n.BoardError("KICKFLIP_PUSH_CMD must be a JSON array of strings")}l(z,"pushCommand");async function Q(e,s){const t=B(s.kind);if(t)throw new r(t);if(s.title===void 0)throw new r("push needs --title");const a=new Date,i=await n.repliesSincePush(e,a),o=n.withReplies(s.body,i),d={kind:s.kind,title:s.title,body:o||void 0,url:n.pushUrl(s.url),tag:s.tag},[c,...p]=z(),u=[...p,...Object.entries(d).flatMap(([v,m])=>m===void 0?[]:[`--${v}`,m]),...s.urgent?["--urgent"]:[],...s.now?["--now"]:[]],h=O(c,u,{stdio:"inherit"});if(h.status!==0)throw Object.assign(new n.BoardError(`kickflip push failed (${h.error?.message||`exit ${h.status??h.signal}`}); the push was not recorded`),{exitCode:h.status||1});return n.recordPush(e,a)}l(Q,"push");function X(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 o=i.slice(2);if(J.has(o)){if(a+1>=e.length)throw new r(`--${o} needs a value`);(s[o]||=[]).push(e[++a]);continue}if(Object.hasOwn(s,o))throw new r(`--${o} is given twice`);if(W.has(o))s[o]=!0;else if(a+1<e.length)s[o]=e[++a];else throw new r(`--${o} needs a value`)}return{flags:s,rest:t}}l(X,"parse");const V=["html","text","note","notes","summary","title","body","next","progress","install-label","confirmed"],Y=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function Z(e){const s=e.trim();if(!s||/\s/.test(s))return!1;if(Y.test(s))return!0;try{return g.statSync(s).isFile()}catch{return!1}}l(Z,"looksLikePath");function ee(e,s){for(const t of V){if(typeof s[t]!="string"||!Z(s[t]))continue;const a=e.includes("file")?`; for a file use --file ${s[t].trim()}`:"";throw new r(`--${t} takes text, not a file path (${s[t].trim()})${a}`)}if(typeof s.file=="string"&&s.file!=="-"&&!g.existsSync(s.file))throw new r(`--file ${s.file}: no such file`)}l(ee,"checkInputs");const A=l(e=>g.readFileSync(e==="-"?0:e,"utf8"),"readInput");function x(e){if(!e)throw new r("--file <path> is required");try{return JSON.parse(A(e))}catch(s){throw new n.BoardError(`${e}: ${s.message}`)}}l(x,"readJSONInput");function P(e){if(e.plan!==void 0&&e["notes-file"]!==void 0)throw new r("pass --notes-file or --plan, not both");return e["notes-file"]??e.plan}l(P,"notesFileOf");const b={reply:[["eventId"],["html","file"],(e,[s],t)=>{if(t.html===void 0==(t.file===void 0))throw new r("reply takes --html or --file");return n.writeReply(e,s,t.html??A(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"],(e,s,t)=>n.addQuestions(e,x(t.file))],"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","from-ask","held","unheld","notes-file","plan","deps","install","install-sha","install-label","clear-install","title","force","ticket","confirmed"],(e,[s],t,a)=>n.setItem(e,s,{status:t.status,clearStatus:t["clear-status"],next:t.next,clearNext:t["clear-next"],pr:t.pr,fromAsk:t["from-ask"],held:t.held,unheld:t.unheld,notesFile:P(t),deps:t.deps,install:t.install,installSha:t["install-sha"],installLabel:t["install-label"],clearInstall:t["clear-install"],title:t.title,force:t.force,ticket:t.ticket,confirmed:t.confirmed},a)],"item add":[["wave/id"],["title","notes","deps","notes-file","plan","ticket"],(e,[s],t,a)=>n.addItem(e,s,{title:t.title,notes:t.notes,deps:t.deps,notesFile:P(t),ticket:t.ticket},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)],"followup add":[["wave"],["id","title","from","ticket"],(e,[s],t,a)=>n.addFollowup(e,s,t,a)],"tickets backfill":[[],["dry-run"],(e,s,t,a)=>n.backfillTickets(e,{dryRun:t["dry-run"]},a)],"followup done":[["wave","id"],["force"],(e,[s,t],a)=>n.finishFollowup(e,s,t,{force:a.force})],"followup set":[["wave","id"],["ticket"],(e,[s,t],a)=>n.setFollowupTicket(e,s,t,a.ticket)],"reviewers set":[[],["file"],(e,s,t)=>n.setReviewers(e,x(t.file))],"reviewers add":[[],["file"],(e,s,t)=>n.addReviewers(e,x(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"],[],async(e,[s])=>{if(!e)throw new n.BoardError("build needs --site <route>");n.parseKey(s);try{const t=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:f.basename(e),key:s}),signal:AbortSignal.timeout(1e4)}),a=await t.json();if(!t.ok)throw new n.BoardError(a.error||"The hub refused the build.");return{lines:["started"]}}catch(t){throw t instanceof n.BoardError?t:new n.BoardError(`Could not start the build: ${t.message}`)}}],"main set":[[],["install","install-sha"],(e,s,t)=>n.setMainInstall(e,{install:t.install,installSha:t["install-sha"]})],"settings set":[["name","on|off"],[],(e,[s,t])=>n.setBoardSetting(e,s,t)],"builder start":[["wave/id"],["label","progress","dir","agent","kind","eta","worktree","repo"],(e,[s],{worktree:t,repo:a,...i})=>{if(a!==void 0&&!t)throw new r("--repo goes with --worktree");const o=M(),d=e?K(e,o):null,c=d?D(d.route,o):{},p=!!c.github,u=a??(p&&c.repo||void 0),h=p&&c.branch||"main";return n.startBuilder(e,s,{...i,worktree:t&&(()=>q(s.split("/")[1],{repo:u,base:h,remote:p}))})}],"builder progress":[["wave/id","...text?"],["text","dir","agent","eta","clear-eta"],(e,[s,...t],a)=>{if(t.length&&a.text!==void 0)throw new r("builder progress takes its text once: as words or as --text");if(!t.length&&a.text===void 0)throw new r("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"],(e,[s],t)=>n.stopBuilder(e,s,t)],land:[["wave/id"],["repo","dir"],async(e,[s],t)=>{const a=s.split("/")[1],i=await n.recordedWorktree(e,s,{dir:t.dir}),o=f.resolve(t.repo??i?.repo??L());return T({path:i?.path??f.join(N(),a),branch:i?.branch??a,repo:o}),{lines:[]}}],working:[["eventId"],[],(e,[s])=>n.setWorking(e,s)],"hello-ack":[["eventId"],[],(e,[s])=>n.ackHello(e,s)],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)=>Q(e,t)]};b.install=b.build;async function te(e,s={}){try{const{flags:t,rest:a}=X(e),i=S(b,`${a[0]} ${a[1]}`)?`${a[0]} ${a[1]}`:a[0],o=S(b,i);if(!o)throw new r(a.length?`unknown command "${a.slice(0,2).join(" ")}"`:"no command given");const[d,c,p]=o,u=a.slice(i.split(" ").length),h=d.at(-1)?.startsWith("..."),v=d.at(-1)?.endsWith("?")?d.length-1:d.length;if(h?u.length<v:u.length!==d.length)throw new r(`${i} takes ${d.map(w=>`<${w.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:m,...y}=t,I=Object.keys(y).filter(w=>!c.includes(w));if(I.length)throw new r(`${i} does not take ${I.map(w=>"--"+w).join(", ")}`);ee(c,y);const $=m===void 0?null:await n.siteDir(m),k=await p($,u,y,s),{file:j,lines:R=[]}=typeof k=="object"&&k?k:{file:k};i.startsWith("builder ")?console.log(`${i.slice(8)} ${u[0]}`):j!==void 0&&console.log(`${i} \u2192 ${j}`);for(const w of R)console.log(w);return 0}catch(t){if(t instanceof r)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(l(te,"main"),process.argv[1]&&import.meta.url===E(g.realpathSync(process.argv[1])).href){H();const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(U):process.exitCode=await te(e)}export{U as USAGE,te as main};
490
+ succeeds, and exits with kickflip push's code. Without --now
491
+ it queues until the tick ends (checked) or the hub's
492
+ next check; --now sends at once unless pushes are held
493
+ (overnight, snoozed or the weekend). --urgent: a
494
+ production fault only; it beats those holds. While
495
+ notifications are off every push is dropped, urgent too
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};