@lovek4d/kickflip 0.5.0 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CONTEXT.md +47 -42
  2. package/README.md +1 -1
  3. package/SKILL.md +16 -16
  4. package/bin/kickflip.mjs +26 -17
  5. package/bin/wait-events.mjs +2 -2
  6. package/board/agents.mjs +2 -0
  7. package/board/bin/board.mjs +65 -18
  8. package/board/domain.mjs +7 -7
  9. package/board/finish.mjs +4 -4
  10. package/board/headless.mjs +5 -4
  11. package/board/on-host.mjs +1 -1
  12. package/board/public/app-asks.js +6 -6
  13. package/board/public/app-banner.js +7 -13
  14. package/board/public/app-cards.js +51 -27
  15. package/board/public/app-core.js +26 -8
  16. package/board/public/app-header.js +2 -1
  17. package/board/public/app-lanes.js +55 -23
  18. package/board/public/app-needs.js +129 -4
  19. package/board/public/app-reader.js +1 -1
  20. package/board/public/app-snapshot.js +6 -5
  21. package/board/public/app-threads.js +70 -59
  22. package/board/public/app.js +38 -8
  23. package/board/public/attach.js +2 -2
  24. package/board/public/dialogs.js +9 -39
  25. package/board/public/hub-kit.js +4 -2
  26. package/board/public/linear-key.js +46 -35
  27. package/board/public/new-task.js +17 -38
  28. package/board/public/prototype-kit.js +28 -3
  29. package/board/public/prototype-switcher.js +42 -107
  30. package/board/public/shell.js +7 -5
  31. package/board/public/sidebar.js +44 -5
  32. package/board/public/style.css +108 -37
  33. package/board/public/ui-cards.js +41 -30
  34. package/board/public/ui-header.js +2 -2
  35. package/board/public/ui-order.js +12 -10
  36. package/board/public/ui-questions.js +25 -38
  37. package/board/public/ui-sidebar.js +18 -12
  38. package/board/public/ui-threads.js +3 -3
  39. package/board/public/ui.js +40 -8
  40. package/board/public/view-hub-settings.js +11 -10
  41. package/board/public/view-hub.js +17 -3
  42. package/board/public/view-questions.js +142 -199
  43. package/board/public/view-settings.js +191 -72
  44. package/board/server.mjs +19 -20
  45. package/board/store.mjs +20 -15
  46. package/board/transcript.mjs +2 -0
  47. package/board/worktree.mjs +24 -20
  48. package/hooks-plugin/hooks/board-data-gate.mjs +50 -0
  49. package/hooks-plugin/hooks/hooks.json +9 -0
  50. package/hub/boards.mjs +4 -4
  51. package/hub/front-door.mjs +5 -5
  52. package/hub/github.mjs +3 -3
  53. package/hub/hub.mjs +11 -11
  54. package/hub/linear.mjs +1 -2
  55. package/hub/live.mjs +2 -2
  56. package/hub/locks.mjs +1 -1
  57. package/hub/pr-report.mjs +2 -2
  58. package/hub/presence.mjs +1 -1
  59. package/hub/push.mjs +2 -2
  60. package/hub/readiness.mjs +1 -2
  61. package/hub/review-send.mjs +1 -0
  62. package/hub/secrets.mjs +2 -0
  63. package/hub/self-restart.mjs +2 -0
  64. package/hub/sessions.mjs +2 -2
  65. package/hub/share.mjs +1 -0
  66. package/hub/slack.mjs +2 -2
  67. package/hub/stream.mjs +10 -13
  68. package/hub/try.mjs +6 -6
  69. package/hub/upgrade.mjs +1 -1
  70. package/hub/usage.mjs +3 -3
  71. package/hub/views.mjs +1 -1
  72. package/package.json +2 -1
  73. package/shared/audit.mjs +4 -0
  74. package/shared/fake-gh.mjs +17 -4
  75. package/shared/service.mjs +4 -4
  76. package/shared/usage-gate.mjs +1 -1
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var E=Object.defineProperty;var r=(e,s)=>E(e,"name",{value:s,configurable:!0});import{readiness as B,reportSlack as H}from"../../hub/readiness.mjs";import{readSlack as L,setSlack as D}from"../../hub/slack.mjs";import{routeRecord as y}from"../../hub/routes.mjs";import m from"node:fs";import g from"node:path";import{pathToFileURL as M}from"node:url";import*as n from"../store.mjs";import{boardWorktree as W,builderRepo as G,joiningLand as z,landInTurn as J,worktreesDir as F}from"../worktree.mjs";import{fileFindings as U}from"../arch-review.mjs";import{finishCard as x,landHeld as K}from"../finish.mjs";import{ITEM_STATUSES as Q}from"../domain.mjs";import{boardAt as V,readManifest as X}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";const Y=`usage: node board.mjs <command> [--site <route>] ...
2
+ var H=Object.defineProperty;var r=(e,a)=>H(e,"name",{value:a,configurable:!0});import{readiness as L}from"../../hub/readiness.mjs";import{readSlack as D,setSlack as W}from"../../hub/slack.mjs";import{routeRecord as v}from"../../hub/routes.mjs";import g from"node:fs";import y from"node:path";import{pathToFileURL as M}from"node:url";import*as n from"../store.mjs";import{boardWorktree as O,builderRepo as z,joiningLand as G,landInTurn as J,worktreesDir as U}from"../worktree.mjs";import{fileFindings as F}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 S}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";const V=`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.
@@ -28,9 +28,12 @@ command fails and changes nothing.
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?, reask_of?, ask?, suggested?}; topic is declared, item:<wave>/<id>, item:ask:<eventId>
31
+ source?, context?, url?, urgent?, round_of?, reask_of?, ask?, suggested?, prototype_gate?}; 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
33
  a board over ~/.claude/waves. A question on a done or merged card reopens it.
34
+ A question on an item: topic whose card has a grill or prototype planner running is staged, not published,
35
+ whoever adds it: it shows nowhere until the planner finishes, fails or is stopped, when every staged question
36
+ on the card publishes in the order added, with one owner push. The command says "staged" and why.
34
37
  url is http(s), or a site page such as prototype-<topic>.html?q=<id>, stored as given.
35
38
  Prototype options keep take order, ids matching take keys; recommended: true tags the recommendation.
36
39
  suggested: {option, card, answer} pre-checks the option the owner already chose on an earlier card, with a
@@ -47,6 +50,9 @@ command fails and changes nothing.
47
50
  and printed as "replaced: <old id>". round_of names the question on the same topic whose
48
51
  prototype-request this answers; the question needs a prototype url, takes the next round
49
52
  number, and resolves that one, noting "Followed by prototype round N".
53
+ prototype_gate: true marks a grill's closing "Prototype round first?" question (options yes, no, on a card's
54
+ topic): the owner's yes moves the card to prototyping and starts its prototype planner, a no moves it to ready
55
+ once no question on it is left open, the board doing both with no orchestrator step.
50
56
  reask_of names a resolved question on the same topic that the owner asked to ask again
51
57
  (reask-request): it resolves that one again, noting "Asked again as <new id>", printed as "resolved: <old id>".
52
58
  ask names a builder ask the session deferred on the item (builder defer): the question puts it to the
@@ -57,6 +63,10 @@ command fails and changes nothing.
57
63
  prototype page until an answer settles its round: a pick, with a note or none, or an answer to
58
64
  an "Is this it?" confirm already on the board. A question already resolved, such as one an
59
65
  answer with a note and no option resolved, is left as it is
66
+ question move <id>,<id>,\u2026 --to <wave>/<id> [--block <wave>/<id>,\u2026] merges open questions that ask one thing onto
67
+ one card: the first id moves to that card (topic item:<card>), the others resolve "Duplicate of
68
+ <card>'s question <first id>". Each --block card, in order, is blocked by the one before it (the
69
+ first by --to) and related to --to
60
70
  prototype add <topic> --title <t> --take <K>:<label> records a prototype in data/prototypes.json: the board
61
71
  [--take <K>:<label> \u2026] [--card <wave>/<id>] serves prototype-<topic>.html from its one template, with
62
72
  the title, a back arrow to --card (else the board), the
@@ -104,7 +114,11 @@ command fails and changes nothing.
104
114
  ready refuses while the card has an open question, and
105
115
  a card without --size, a notes file, or the id of each
106
116
  answered question in that file, since a builder starts
107
- from it; it resolves those answered questions
117
+ from it; it resolves those answered questions. On a card
118
+ an Implement message waits on you for, it releases the
119
+ build: the board server starts it at once, and it prints
120
+ "released: <key> starts building now"; one a dep still
121
+ blocks is bumped to the top of Todo instead ("bumped: \u2026")
108
122
  [--held | --unheld | --pick-up-at <time>] holds or resumes the card on the owner's words; the board
109
123
  server records the owner's own Hold and Resume taps. A later
110
124
  --held/--unheld overrides a tap; --unheld also clears a held status.
@@ -135,6 +149,9 @@ command fails and changes nothing.
135
149
  --parent "" clears it
136
150
  [--related <id>,<wave>/<id>] replaces the cards it is Related to, which both cards
137
151
  show; --related "" clears it, on both
152
+ [--relate <id>,<wave>/<id>] adds cards to its Related to, keeping the rest
153
+ [--unrelate <id>,<wave>/<id>] takes cards off its Related to, on both cards; neither
154
+ goes with --related
138
155
  [--install <url> [--install-sha <sha>] a device build the owner installs from the card's Install
139
156
  [--install-label <text>] | --clear-install] button, stamped built_at now: itms-services://?action=
140
157
  download-manifest&url=https://\u2026/manifest.plist for iOS,
@@ -163,15 +180,22 @@ command fails and changes nothing.
163
180
  for each. It refuses while one has no answer yet; [--force] closes it and resolves that one too.
164
181
  --status done refuses a done card the owner messaged since it closed: it reads reopened, and closes
165
182
  again only on the owner's Done
166
- item add <wave>[/<id>] --title <t> [--notes <t>] a new card, planned (waiting when it has --deps); never
183
+ item add <wave>[/<id>] --title <t> [--notes <t>] a new card, unstarted (waiting when it has --deps); never
167
184
  [--ticket <id>] [--deps <id>,<wave>/<id>] hand-edit wave.json to add one. Refuses an id the wave has.
168
185
  [--notes-file <file>] --plan is an alias of --notes-file
169
186
  [--pick-up-at <time>] holds it until then, as item set's --pick-up-at
170
187
  [--size small|large] [--model opus|sonnet] as item set's
188
+ [--state ready] a Quick build: the pull takes it as it is, so it needs
189
+ --size and --notes-file. Without it the board grills the
190
+ card as soon as it lands in Todo
171
191
  [--parent <wave>/<id>] the card it was spun out of, such as one filed from
172
192
  another card's thread, grill or answer: it reads as a
173
193
  follow-up, and its Parent lists it in Children. A card
174
194
  from the + or New task has none
195
+ [--state ready|grilling|prototyping] files it at once, as item set's --state; the session's
196
+ [--relate <id>,<wave>/<id>] own form, linking it with --relate in the same call.
197
+ Without --state it reads New and a card-added event
198
+ wakes the board's session, which links and files it
175
199
  Without /<id>, the card takes its ticket's id (GH-<n> for issue #n); pass an id only to import a
176
200
  card. Prints "<label> added as <key>": use the key in later commands
177
201
  item relate-ask <eventId> <wave>/<id> --summary <t> folds an ask about an existing item into it: the ask's
@@ -241,21 +265,31 @@ command fails and changes nothing.
241
265
  --channel <dm>
242
266
  review react <wave>/<id> <reviewer> <emoji> records a reaction once on that reviewer's link
243
267
  integrations readiness rows as one JSON line
244
- integrations report --slack yes|no with --site: reports whether this session has Claude's
245
- Slack connector, in ~/.claude/kickflip/integrations.json
246
- review due one JSON line of the Slack sends owed, from local state:
247
- {dms: [{key, reviewer, channel, text, record}], reactions:
268
+ review updated <wave>/<id> --text "<what changed>" posts \u201C\u{1F916} Updated: <what changed>\u201D under each recorded link
269
+ of a reviewer who hasn't approved, as you, through Hub's
270
+ Slack token
271
+ review due one JSON line of the Slack sends the hub owes, from local state:
272
+ {dms: [{key, reviewer, slack_user, channel, text, record}], reactions:
248
273
  [{key, reviewer, channel, ts, emoji, record}], digest:
249
274
  review digest's answer while due (with record), else null}.
250
275
  dms: a PR link to each reviewer of an open, non-draft PR with
251
276
  no link recorded; reactions: git_merged on each recorded link
252
277
  of a merged card. record is the command to run once sent.
253
278
  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
279
+ review post <pr> --file <review.md> posts the session's words as one Comment review (never an
280
+ --read-at <time> approval or a request for changes) on an incoming PR at
281
+ its listed head, as the board's github-login (else gh's
282
+ account), under "\u{1F916} Claude review from the <board title>
283
+ board" and the <!-- claude-bridge --> marker, and records
284
+ it posted with the review's link. Refuses words that carry
285
+ the header or marker, and a PR that got
286
+ another review after that time. The PR must be one the hub
258
287
  listed in data/incoming.json or reviewed before
288
+ review record <pr> --state posted|needs the session's review of an incoming PR that was not posted
289
+ --text <markdown> | --file <path> through review post, in data/reviews.json: posted for a PR
290
+ it chose to say nothing on, needs while it waits on the
291
+ owner, which puts it in Needs you and pushes. Same PRs as
292
+ review post
259
293
  review digest pending reviews as JSON; due weekday mornings from 09:00
260
294
  review digest sent <reviewer> --ts <ts> records a digest in data/review-digests.json;
261
295
  --channel <dm> --items <key,key> marks today sent in the Slack identity timezone
@@ -269,7 +303,7 @@ command fails and changes nothing.
269
303
  settings the board's settings as JSON; flags default off, optional
270
304
  settings appear when set: {"auto_merge": false,
271
305
  "auto_build": false, "auto_pull": false,
272
- "max_implementers": 10,
306
+ "max_implementers": 10, "max_planners": 3,
273
307
  "auto_triage": false, "auto_review": false,
274
308
  "idle_notification": false, "skip_draft": false,
275
309
  "prs": true, "github_login": "owner"}
@@ -304,14 +338,20 @@ command fails and changes nothing.
304
338
  board server starts and launches the build of the top
305
339
  ready Todo cards nothing blocks, holds or asks about, one
306
340
  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.
341
+ once, and launches a grill on each card that arrives in
342
+ Todo, up to max-planners at a time (an arrival: not ready
343
+ or held, with no planner record and no questions
344
+ yet). Off by default.
310
345
  Only on the owner's word
311
346
  settings set max-implementers <1..99> Maximum parallel implementers, 10 by default: the cards
312
347
  building or waiting in Orchestrator review the pull fills
313
348
  up to. Planners and cards waiting on the owner don't count.
349
+ It limits the pull only: an Implement the owner asks for
350
+ starts past it.
314
351
  Only on the owner's word
352
+ settings set max-planners <1..99> Maximum parallel planners, 3 by default: the grills and
353
+ prototypes running at once; arrivals past it wait in board
354
+ order. Only on the owner's word
315
355
  settings set auto-review on|off on: while the board is idle and nothing is pulled, the board
316
356
  server sends pr-review {pr, head, repo, title, author, url,
317
357
  touches, asked} for one incoming PR at a time that touches
@@ -349,7 +389,7 @@ command fails and changes nothing.
349
389
  opened, it keeps that record's kind and worktree.
350
390
  [--dispatch] --dispatch: the session runs this right before it launches
351
391
  an agent of any kind (build, prototype, grill) so the record
352
- is open at launch and the card reads preheating; the agent's
392
+ is open at launch and the card reads starting; the agent's
353
393
  own builder start takes it up. On a record the Build tap
354
394
  opened it changes nothing and prints "already open". The
355
395
  record keeps dispatched: tap or session, whoever opened it.
@@ -424,6 +464,13 @@ command fails and changes nothing.
424
464
  and the card's --size and --model: "codex: <reason>" or
425
465
  "claude sonnet: <reason>". Pass that kind to builder start
426
466
  --kind, and launch that model
467
+ audit [<wave>/<id>] the board's audit log of the last 30 days, data/audit.jsonl:
468
+ each call the code made on its own (builder-pick, pull,
469
+ released-start, launch-held, arrival-planner, relaunch,
470
+ lander, triage, pr-review, push, pick-up, auto-merge,
471
+ ready-draft, session-resume, compact) as "<at> <call>
472
+ [<key>]: <result>. <reason>" and the inputs it read; with
473
+ a card, only that card's. The hub's own: kickflip audit
427
474
  builder call <wave>/<id> <judgment call> appends one line to the agent's open builder record: a
428
475
  [--agent <name>] judgment call as the builder makes it, or a "Question: \u2026"
429
476
  for the session; refused with no open record. A builder start
@@ -515,4 +562,4 @@ command fails and changes nothing.
515
562
  (overnight, snoozed or the weekend). --urgent: a
516
563
  production fault only; it beats those holds. While
517
564
  notifications are off every push is dropped, urgent too
518
- KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class l extends Error{static{r(this,"UsageError")}}const Z=new Set(["clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","hub","clear-install","force","worktree","dry-run","dispatch"]);function ee(e){const s=Object.create(null),t=[];for(let a=0;a<e.length;a++){const i=e[a];if(!i.startsWith("--")){t.push(i);continue}const d=i.slice(2);if(Z.has(d)){if(Object.hasOwn(s,d))throw new l(`--${d} is given twice`);s[d]=!0}else if(a+1<e.length)(s[d]||=[]).push(e[++a]);else throw new l(`--${d} needs a value`)}return{flags:s,rest:t}}r(ee,"parse");function te(e,s,t){const a=new Set(s.filter(o=>o.startsWith("...")).map(o=>o.slice(3))),i=new Set(s.map(o=>o.replace(/^\.\.\./,""))),d=Object.keys(t).filter(o=>!i.has(o));if(d.length)throw new l(`${e} does not take ${d.map(o=>"--"+o).join(", ")}`);return Object.fromEntries(Object.entries(t).map(([o,c])=>{if(c===!0||a.has(o))return[o,c];if(c.length>1)throw new l(`--${o} is given twice`);return[o,c[0]]}))}r(te,"flagsOf");const se=["html","text","note","notes","summary","title","body","next","progress","install-label","confirmed","asked"],ae=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function ne(e){const s=e.trim();if(!s||/\s/.test(s))return!1;if(ae.test(s))return!0;try{return m.statSync(s).isFile()}catch{return!1}}r(ne,"looksLikePath");function re(e,s){for(const t of se){if(typeof s[t]!="string"||!ne(s[t]))continue;const a=e.includes("file")?`; for a file use --file ${s[t].trim()}`:"";throw new l(`--${t} takes text, not a file path (${s[t].trim()})${a}`)}if(typeof s.file=="string"&&s.file!=="-"&&!m.existsSync(s.file))throw new l(`--file ${s.file}: no such file`)}r(re,"checkInputs");const f=r(e=>m.readFileSync(e==="-"?0:e,"utf8"),"readInput");function $(e){if(!e)throw new l("--file <path> is required");try{return JSON.parse(f(e))}catch(s){throw new n.BoardError(`${e}: ${s.message}`)}}r($,"readJSONInput");function R(e){if(e.plan!==void 0&&e["notes-file"]!==void 0)throw new l("pass --notes-file or --plan, not both");return e["notes-file"]??e.plan}r(R,"notesFileOf");function O(e){if(e.note!==void 0&&e.file!==void 0)throw new l("pass --note or --file, not both");return e.file===void 0?e.note:f(e.file)}r(O,"noteOf");const k={pr:[["wave/id"],[],async(e,[s])=>({lines:(await n.prReport(e,s)).map(t=>JSON.stringify(t))})],"pr comment":[["pr"],["body"],(e,[s],t)=>{if(t.body===void 0)throw new l('pr comment needs --body "<what Claude says>"');return n.postPR(e,s,{body:t.body})}],"pr reply":[["pr","comment-id"],["body"],(e,[s,t],a)=>{if(a.body===void 0)throw new l('pr reply needs --body "<what Claude says>"');return n.postPR(e,s,{replyTo:t,body:a.body})}],reply:[["eventId"],["html","file"],(e,[s],t)=>{if(t.html===void 0==(t.file===void 0))throw new l("reply takes --html or --file");return n.writeReply(e,s,t.html??f(t.file))}],thread:[["wave/id"],["text","link","who","via"],(e,[s],t)=>n.appendThread(e,s,t)],"topic add":[["id"],["title","summary"],(e,[s],t)=>n.addTopic(e,{id:s,...t})],"question add":[[],["file"],async(e,s,t)=>{const a=$(t.file);return{...await n.addQuestions(e,a),asked:[a].flat()}}],"question resolve":[["id"],["note"],(e,[s],t)=>n.resolveQuestion(e,s,t.note)],"prototype add":[["topic"],["title","...take","card"],(e,[s],t)=>n.addPrototype(e,{topic:s,title:t.title,card:t.card,takes:t.take})],"prototype list":[[],[],e=>n.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[s])=>n.removePrototype(e,s)],"item set":[["wave/id"],["status","clear-status","next","clear-next","...pr","pushed","gh-since","from-ask","state","held","unheld","pick-up-at","notes-file","plan","deps","parent","related","install","install-sha","install-label","clear-install","title","size","model","force","ticket","text","check"],async(e,[s],t,a)=>(t.status==="done"&&e&&await n.checkNotReopened(e,s),n.setItem(e,s,{status:t.status,clearStatus:t["clear-status"],next:t.next,clearNext:t["clear-next"],pr:t.pr,pushed:t.pushed,ghSince:t["gh-since"],fromAsk:t["from-ask"],state:t.state,held:t.held,unheld:t.unheld,pickUpAt:t["pick-up-at"],notesFile:R(t),deps:t.deps,parent:t.parent,related:t.related,install:t.install,installSha:t["install-sha"],installLabel:t["install-label"],clearInstall:t["clear-install"],title:t.title,size:t.size,model:t.model,force:t.force,ticket:t.ticket,text:t.text,check:t.check},a))],"item add":[["wave[/id]"],["title","notes","deps","notes-file","plan","ticket","pick-up-at","parent","size","model"],(e,[s],t,a)=>n.addItem(e,s,{title:t.title,notes:t.notes,deps:t.deps,notesFile:R(t),ticket:t.ticket,pickUpAt:t["pick-up-at"],parent:t.parent,size:t.size,model:t.model},a)],"item relate-ask":[["eventId","wave/id"],["summary"],(e,[s,t],a)=>n.relateAsk(e,s,t,a.summary)],"ask progress":[["eventId"],["next"],(e,[s],t)=>n.markAsk(e,s,"progress",t.next)],"ask done":[["eventId"],[],(e,[s])=>n.markAsk(e,s,"done")],"ask unfold":[["eventId"],[],(e,[s])=>n.unfoldAsk(e,s)],"ask move":[["eventId"],["to"],(e,[s],t)=>n.moveAsk(e,s,t.to)],"tickets backfill":[[],["dry-run"],(e,s,t,a)=>n.backfillTickets(e,{dryRun:t["dry-run"]},a)],"lineup create":[["slug"],["title","goal","...phase","note","file"],(e,[s],t)=>n.createLineup(e,s,{title:t.title,goal:t.goal,phases:t.phase,note:O(t)})],"lineup add":[["slug","wave/id"],["after"],(e,[s,t],a)=>n.changeLineup(e,s,{add:t,after:a.after})],"lineup remove":[["slug","wave/id"],[],(e,[s,t])=>n.changeLineup(e,s,{remove:t})],"lineup set":[["slug"],["title","goal","note","file","...phase"],(e,[s],t)=>n.changeLineup(e,s,{title:t.title,goal:t.goal,note:O(t),phases:t.phase})],"lineup finish":[["slug"],[],(e,[s])=>n.finishLineup(e,s)],slack:[[],[],()=>({lines:[JSON.stringify(L())]})],"slack set":[[],["self-dm","user","domain","timezone"],async(e,s,t)=>{const a={"self-dm":"self_dm",user:"owner_slack_user",domain:"slack_domain",timezone:"timezone"};try{return{lines:[JSON.stringify(await D(Object.fromEntries(Object.entries(t).map(([i,d])=>[a[i],d]))))]}}catch(i){throw new n.BoardError(i.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await B.rows())]})],"integrations report":[[],["slack"],async(e,s,t)=>{if(!["yes","no"].includes(t.slack))throw new l("integrations report takes --slack yes|no");if(!e)throw new n.BoardError("integrations report needs --site <route>");const a=g.basename(e);return H(t.slack==="yes",{route:a,title:X().sites?.[a]?.title||(await n.boardConfig(e)).title}),{lines:[`Slack connector reported: ${t.slack}.`]}}],"review sent":[["wave/id","reviewer"],["ts","channel"],(e,[s,t],a)=>n.reviewSent(e,s,t,a)],"review react":[["wave/id","reviewer","emoji"],[],(e,[s,t,a])=>n.reviewReact(e,s,t,a)],"review record":[["pr"],["state","text","file","url"],(e,[s],t)=>{if(t.text===void 0==(t.file===void 0))throw new l("review record takes --text or --file");return n.recordReview(e,s,{state:t.state,text:t.text??f(t.file),url:t.url})}],"review digest":[[],[],async(e,s,t,a)=>({lines:[JSON.stringify(await n.reviewDigest(e,a))]})],"review due":[[],[],async(e,s,t,a)=>({lines:[JSON.stringify(await n.reviewDue(e,a))]})],"review digest sent":[["reviewer"],["ts","channel","items"],(e,[s],t,a)=>n.reviewDigestSent(e,s,t,a)],"reviewers set":[[],["file"],(e,s,t)=>n.setReviewers(e,$(t.file))],"reviewers add":[[],["file"],(e,s,t)=>n.addReviewers(e,$(t.file))],"reviewers remove":[["...name"],[],(e,s)=>n.removeReviewers(e,s)],"rules list":[[],[],e=>n.listRules(e)],"rules add":[[],["text","hub","by"],(e,s,t)=>n.addRuleTo(e,t)],"rules edit":[["id"],["text","hub"],(e,[s],t)=>n.editRuleIn(e,s,t)],"rules remove":[["id"],["hub"],(e,[s],t)=>n.removeRuleFrom(e,s,t)],settings:[[],[],async e=>({lines:[JSON.stringify(await n.readBoardSettings(e))]})],build:[["wave/id"],["asked"],async(e,[s],t)=>{if(!e)throw new n.BoardError("build needs --site <route>");n.parseKey(s);const a=await n.askedGate(e,s,t.asked);try{const i=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:g.basename(e),key:s}),signal:AbortSignal.timeout(1e4)}),d=await i.json();if(!i.ok)throw new n.BoardError(d.error||"The hub refused the build.");return{lines:["started",...(await n.appendThread(e,s,{text:a})).lines]}}catch(i){throw i instanceof n.BoardError?i:new n.BoardError(`Could not start the build: ${i.message}`)}}],"main set":[[],["install","install-sha"],(e,s,t)=>n.setMainInstall(e,{install:t.install,installSha:t["install-sha"]})],"settings set":[["name","value"],[],(e,[s,t])=>n.setBoardSetting(e,s,t)],"builder start":[["wave/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[s],{worktree:t,repo:a,dispatch:i,...d})=>{if(a!==void 0&&!t)throw new l("--repo goes with --worktree");const o=r(p=>W(e,s.split("/")[1],{repo:a,...p}),"make"),c=e&&d.dir===void 0&&V(e);return n.startBuilder(e,s,{...d,dispatch:i?"session":void 0,worktree:t?o:void 0,ownWorktree:c?o:void 0})}],"builder progress":[["wave/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[s,...t],a)=>{if(t.length&&a.text!==void 0)throw new l("builder progress takes its text once: as words or as --text");if(!t.length&&a.text===void 0)throw new l("builder progress needs its text, as words or as --text");return n.builderProgress(e,s,a.text??t.join(" "),{...a,clearEta:a["clear-eta"]})}],"builder done":[["wave/id"],["dir","agent","next","owner-step","check","text"],async(e,[s],{"owner-step":t,check:a,text:i,...d})=>{if(t!==void 0&&(!e||d.dir))throw new l("--owner-step goes on a board card: pass --site, not --dir");if(a!==void 0&&t===void 0)throw new l("--check goes with --owner-step: it is the code check that closes the card once the step is done");t!==void 0&&await n.setItem(e,s,{ownerStep:t,check:a});const o=e&&!d.dir,c=o&&await n.handBackCalls(e,s,d);if(c)return c;if(o&&d.next===void 0&&await n.finishesCard(e,s,d)){if(i===void 0)throw new n.BoardError(`builder done finishes ${s}: pass --text "<what you checked>", or --next "<what happens next>" to stop without finishing`);return x(e,s,{text:i,ownerStep:t})}if(i!==void 0)throw new l("--text goes on a builder done that finishes the card: the main build, with a worktree and no --next");return n.stopBuilder(e,s,{...d,requireNext:!0})}],"builder pick":[["wave/id"],[],(e,[s])=>n.pickBuilder(e,s)],"builder call":[["wave/id","...text"],["dir","agent"],(e,[s,...t],a)=>n.recordCall(e,s,t.join(" "),a)],"builder calls":[["wave/id"],["dir","agent"],(e,[s],t)=>n.listCalls(e,s,t)],"builder ask":[["wave/id","...text"],["agent"],(e,[s,...t],a)=>n.askOrchestrator(e,s,t.join(" "),a)],"builder answer":[["wave/id","ask"],["text"],(e,[s,t],a)=>n.answerBuilder(e,s,t,a.text)],"builder defer":[["wave/id","ask"],["text"],(e,[s,t],a)=>n.answerBuilder(e,s,t,a.text,{deferred:!0})],"builder asks":[["wave/id"],[],(e,[s])=>n.listAsks(e,s)],"builder verified":[["wave/id"],["dir","agent","note"],async(e,[s],t)=>{await n.verifyCalls(e,s,t);const a=e&&!t.dir?await n.verifiedFinish(e,s):null;if(!a)return{};if(a.waits.length)return{lines:[`${await n.cardName(e,s)} finishes once the owner answers ${a.waits.join(", ")}`]};try{return await x(e,s,{text:t.note??"Judgment calls verified."})}catch(i){throw i instanceof n.BoardError?Object.assign(new n.BoardError(`judgment calls verified, but ${await n.cardName(e,s)} didn't finish: ${i.message}`),{exitCode:i.exitCode}):i}}],land:[["wave/id"],["repo","dir","text","owner-step","check"],async(e,[s],t)=>{const a=s.split("/")[1];if(e&&t.text===void 0)throw new l(`land needs --text "<what was checked>", which closes ${s} as done once it lands`);if(!e&&t.text!==void 0)throw new l("--text closes the card, which needs --site");if(!e&&t["owner-step"]!==void 0)throw new l("--owner-step keeps a board card open, which needs --site");if(t.check!==void 0&&t["owner-step"]===void 0)throw new l("--check goes with --owner-step: it is the code check that closes the card once the step is done");if(e&&t.dir===void 0)return x(e,s,{text:t.text,ownerStep:t["owner-step"],check:t.check,repo:t.repo});const i=e?await n.cardName(e,s):void 0,d=await z(i??s,async()=>{e&&await n.checkClose(e,s,t.text,t["owner-step"],t.check),await n.checkCallsVerified(e,s,{dir:t.dir});const o=await n.recordedWorktree(e,s,{dir:t.dir}),{repo:c,base:p}=G(e,{repo:t.repo??o?.repo});return await J({path:o?.path??g.join(F(),a),branch:o?.branch??a,repo:g.resolve(c),base:p,ref:i}),e?n.setItem(e,s,{status:"done",text:t.text,ownerStep:t["owner-step"],check:t.check}):{lines:[]}});return d.joined?{lines:[`joined the land already running for ${i??s}`]}:d}],lander:[["wave/id"],[],async(e,[s])=>{if(!e)throw new l("lander needs --site <route>");return await n.relaunchLander(e,s),{lines:[`${await n.cardName(e,s)} is back at checking; the server launches a lander for it within a minute`]}}],merge:[["wave/id"],["after-approval","confirmed"],async(e,[s],t)=>e&&await n.heldMerge(e,s)?K(e,s):n.mergeCard(e,s,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["wave/id"],[],(e,[s])=>e?U(e,s):Promise.reject(new n.BoardError("arch file needs --site <route>"))],checked:[[],["every","idle"],(e,s,t)=>n.writeChecked(e,t.every,{idle:t.idle})],push:[[],["kind","title","body","url","tag","urgent","now"],(e,s,t)=>{if(t.kind!=="fault"&&t.kind!=="reminder")throw new l(`a session pushes only --kind fault or reminder${t.kind===void 0||t.kind===!0?"":`, not "${t.kind}"`}: the board server pushes Action needed, and news goes on the board`);if(t.title===void 0)throw new l("push needs --title");return n.sendPush(e,t)}]};k.install=k.build;const h=r((e,s)=>n.cardName(e,s),"card"),q=r((e,s)=>`${s}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),C=r(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function ie(e){const s=e["pick-up-at"];return[e.status,e["clear-status"]&&"status cleared",e.next!==void 0&&(e.next.trim()?"next line set":"next line cleared"),e["clear-next"]&&"next line cleared",e.pr!==void 0&&`PR #${e.pr}`,e.pushed!==void 0&&`pushed ${e.pushed}`,e["gh-since"]&&`GitHub read to ${e["gh-since"]}`,e["from-ask"]&&`from task ${e["from-ask"]}`,e.state,e.held&&"held",e.unheld&&"resumed",s!==void 0&&(s?`held until ${s}`:"resumed"),R(e)!==void 0&&"notes recorded",e.deps!==void 0&&(e.deps?`blocked by ${e.deps}`:"deps cleared"),e.parent!==void 0&&(e.parent?`parent ${e.parent}`:"parent cleared"),e.related!==void 0&&(e.related?`related to ${e.related.split(",").join(", ")}`:"related cleared"),e.install!==void 0&&"install published",e["clear-install"]&&"install cleared",e.title!==void 0&&"renamed",e.ticket!==void 0&&`ticket ${e.ticket}`,e.check!==void 0&&(e.check.trim()?`check ${e.check.trim().split(/\s+/).join(" ")}`:"check removed")].filter(Boolean).join(", ")}r(ie,"itemChanges");const oe={reply:r(async(e,[s])=>`reply written to ${await n.eventCard(e,s)}`,"reply"),thread:r(async(e,[s])=>`thread line on ${await h(e,s)}`,"thread"),"topic add":r((e,[s])=>`topic ${s} added`,"topic add"),"question add":r(async(e,s,t,{asked:a})=>{const i=[...new Set(a.map(o=>o.topic))],d=i.length!==1?"":i[0].startsWith("item:")?` on ${await h(e,i[0].slice(5))}`:` on topic ${i[0]}`;return`${q(a.map(o=>o.id),"question")} added${d}`},"question add"),"question resolve":r((e,[s])=>`question ${s} resolved`,"question resolve"),"prototype add":r(async(e,[s],t)=>`prototype ${s} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":r((e,[s])=>`prototype ${s} removed`,"prototype rm"),"item set":r(async(e,[s],t)=>`${await h(e,s)}: ${ie(t)}`,"item set"),"item add":r(async(e,s,t,{key:a})=>`${await h(e,a)} added as ${a}`,"item add"),"item relate-ask":r(async(e,[s,t])=>`task ${s} folded into ${await h(e,t)}`,"item relate-ask"),"ask progress":r(async(e,[s])=>`${await h(e,`ask:${s}`)}: in progress`,"ask progress"),"ask done":r(async(e,[s])=>`${await h(e,`ask:${s}`)}: done`,"ask done"),"ask unfold":r((e,[s])=>`task ${s} back on a card of its own`,"ask unfold"),"ask move":r((e,[s],t)=>`task ${s} moved to /${t.to}`,"ask move"),checked:r((e,s,t,{every:a})=>a===null?"GitHub checked; no open PR":`GitHub checked; next in ${a} min`,"checked"),"lineup create":r((e,[s])=>`lineup ${s} created`,"lineup create"),"lineup add":r(async(e,[s,t])=>`${await h(e,t)} added to lineup ${s}`,"lineup add"),"lineup remove":r(async(e,[s,t])=>`${await h(e,t)} taken out of lineup ${s}`,"lineup remove"),"lineup set":r((e,[s])=>`lineup ${s} updated`,"lineup set"),"lineup finish":r((e,[s])=>`lineup ${s} finished`,"lineup finish"),"review sent":r(async(e,[s,t])=>`review link to ${t} recorded on ${await h(e,s)}`,"review sent"),"review react":r(async(e,[s,t,a])=>`${a} on ${t}'s review link recorded on ${await h(e,s)}`,"review react"),"review digest sent":r((e,[s])=>`digest to ${s} recorded`,"review digest sent"),"review record":r((e,[s],t)=>`review of PR #${s} recorded: ${t.state==="needs"?"needs you":"posted"}`,"review record"),"reviewers set":r(()=>"reviewers replaced","reviewers set"),"reviewers add":r(()=>"reviewers added","reviewers add"),"reviewers remove":r((e,s)=>`${q(s,"reviewer")} removed`,"reviewers remove"),"rules add":r((e,s,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":r((e,[s],t)=>`${t.hub?"hub":"board"} rule ${s} reworded`,"rules edit"),"rules remove":r((e,[s],t)=>`${t.hub?"hub":"board"} rule ${s} removed`,"rules remove"),"builder start":r(async(e,[s],t,a)=>`${await h(e,s)}: ${a.alreadyOpen?"already open: the Implement tap opened its record; launch the agent, whose own builder start takes it up":`${C(t)} started`}`,"builder start"),"builder progress":r(async(e,[s])=>`progress line on ${await h(e,s)}`,"builder progress"),"builder done":r(async(e,[s],t,a)=>`${await h(e,s)}: ${a?.said??`${C(t)} done${t["owner-step"]===void 0?"":`; owner step "${t["owner-step"].trim()}"`}`}`,"builder done"),"builder call":r(async(e,[s])=>`judgment call recorded on ${await h(e,s)}`,"builder call"),"builder ask":r(async(e,[s],t,{id:a})=>`${await h(e,s)}: asked the orchestrator, ${a}`,"builder ask"),"builder answer":r(async(e,[s,t])=>`${await h(e,s)}: ${t} answered`,"builder answer"),"builder defer":r(async(e,[s,t])=>`${await h(e,s)}: ${t} deferred to the owner`,"builder defer"),"builder verified":r(async(e,[s],t,a)=>`${await h(e,s)}: judgment calls verified${a?.said?`; ${a.said}`:""}`,"builder verified"),land:r(async(e,[s],t,a)=>e?a?.said?`${await h(e,s)}: ${a.said}`:t["owner-step"]===void 0?`${await h(e,s)}: landed and done`:`${await h(e,s)}: landed; waits on the owner step "${t["owner-step"].trim()}"`:null,"land"),merge:r(async(e,[s],t,a)=>`${await h(e,s)}: ${a?.said??"merged"}`,"merge"),"arch file":r(async(e,[s])=>`findings of ${await h(e,s)} filed`,"arch file")};async function de(e,s={}){try{const{flags:t,rest:a}=ee(e),i=[3,2,1].map(u=>a.slice(0,u).join(" ")).find(u=>y(k,u)),d=y(k,i);if(!d)throw new l(a.length?`unknown command "${a.slice(0,2).join(" ")}"`:"no command given");const[o,c,p]=d,w=a.slice(i.split(" ").length),_=o.at(-1)?.startsWith("..."),T=o.at(-1)?.endsWith("?")?o.length-1:o.length;if(_?w.length<T:w.length!==o.length)throw new l(`${i} takes ${o.map(u=>`<${u.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:A,...N}=t;if(A?.length>1)throw new l("--site is given twice");const P=A?.[0],b=te(i,c,N);re(c,b);const j=P===void 0?null:await n.siteDir(P),v=await p(j,w,b,s),S=y(oe,i),I=S&&await S(j,w,b,v);I&&console.log(I);for(const u of typeof v=="object"&&v?.lines||[])console.log(u);return 0}catch(t){if(t instanceof l)return console.error(`${t.message} (node board.mjs --help lists the commands)`),2;if(t instanceof n.BoardError)return console.error(t.message),t.exitCode??1;throw t}}if(r(de,"main"),process.argv[1]&&import.meta.url===M(m.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(Y):process.exitCode=await de(e)}export{Y as USAGE,de as main};
565
+ KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class l extends Error{static{r(this,"UsageError")}}const X=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 a=Object.create(null),t=[];for(let s=0;s<e.length;s++){const i=e[s];if(!i.startsWith("--")){t.push(i);continue}const o=i.slice(2);if(X.has(o)){if(Object.hasOwn(a,o))throw new l(`--${o} is given twice`);a[o]=!0}else if(s+1<e.length)(a[o]||=[]).push(e[++s]);else throw new l(`--${o} needs a value`)}return{flags:a,rest:t}}r(Y,"parse");function Z(e,a,t){const s=new Set(a.filter(d=>d.startsWith("...")).map(d=>d.slice(3))),i=new Set(a.map(d=>d.replace(/^\.\.\./,""))),o=Object.keys(t).filter(d=>!i.has(d));if(o.length)throw new l(`${e} does not take ${o.map(d=>"--"+d).join(", ")}`);return Object.fromEntries(Object.entries(t).map(([d,c])=>{if(c===!0||s.has(d))return[d,c];if(c.length>1)throw new l(`--${d} is given twice`);return[d,c[0]]}))}r(Z,"flagsOf");const ee=["html","text","note","notes","summary","title","body","next","progress","install-label","confirmed","asked"],te=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function ae(e){const a=e.trim();if(!a||/\s/.test(a))return!1;if(te.test(a))return!0;try{return g.statSync(a).isFile()}catch{return!1}}r(ae,"looksLikePath");function se(e,a){for(const t of ee){if(typeof a[t]!="string"||!ae(a[t]))continue;const s=e.includes("file")?`; for a file use --file ${a[t].trim()}`:"";throw new l(`--${t} takes text, not a file path (${a[t].trim()})${s}`)}if(typeof a.file=="string"&&a.file!=="-"&&!g.existsSync(a.file))throw new l(`--file ${a.file}: no such file`)}r(se,"checkInputs");const w=r(e=>g.readFileSync(e==="-"?0:e,"utf8"),"readInput");function $(e){if(!e)throw new l("--file <path> is required");try{return JSON.parse(w(e))}catch(a){throw new n.BoardError(`${e}: ${a.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 C(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:w(e.file)}r(C,"noteOf");const f={pr:[["wave/id"],[],async(e,[a])=>({lines:(await n.prReport(e,a)).map(t=>JSON.stringify(t))})],"pr comment":[["pr"],["body"],(e,[a],t)=>{if(t.body===void 0)throw new l('pr comment needs --body "<what Claude says>"');return n.postPR(e,a,{body:t.body})}],"pr reply":[["pr","comment-id"],["body"],(e,[a,t],s)=>{if(s.body===void 0)throw new l('pr reply needs --body "<what Claude says>"');return n.postPR(e,a,{replyTo:t,body:s.body})}],reply:[["eventId"],["html","file"],(e,[a],t)=>{if(t.html===void 0==(t.file===void 0))throw new l("reply takes --html or --file");return n.writeReply(e,a,t.html??w(t.file))}],thread:[["wave/id"],["text","link","who","via"],(e,[a],t)=>n.appendThread(e,a,t)],"topic add":[["id"],["title","summary"],(e,[a],t)=>n.addTopic(e,{id:a,...t})],"question add":[[],["file"],async(e,a,t)=>{const s=$(t.file);return{...await n.addQuestions(e,s),asked:[s].flat()}}],"question resolve":[["id"],["note"],(e,[a],t)=>n.resolveQuestion(e,a,t.note)],"question move":[["ids"],["to","block"],(e,[a],t)=>n.moveQuestions(e,a.split(",").filter(Boolean),t.to,(t.block||"").split(",").filter(Boolean))],"prototype add":[["topic"],["title","...take","card"],(e,[a],t)=>n.addPrototype(e,{topic:a,title:t.title,card:t.card,takes:t.take})],"prototype list":[[],[],e=>n.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[a])=>n.removePrototype(e,a)],"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","relate","unrelate","install","install-sha","install-label","clear-install","title","size","model","force","ticket","text","check"],async(e,[a],t,s)=>(t.status==="done"&&e&&await n.checkNotReopened(e,a),t.status==="building"&&e&&S(e)&&await n.ensureWorktree(e,a,i=>O(e,a.split("/")[1],i)),n.setItem(e,a,{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,relate:t.relate,unrelate:t.unrelate,install:t.install,installSha:t["install-sha"],installLabel:t["install-label"],clearInstall:t["clear-install"],title:t.title,size:t.size,model:t.model,force:t.force,ticket:t.ticket,text:t.text,check:t.check},s))],"item add":[["wave[/id]"],["title","notes","deps","notes-file","plan","ticket","pick-up-at","parent","size","model","state","relate"],(e,[a],t,s)=>n.addItem(e,a,{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,state:t.state,relate:t.relate,intake:!0},s)],"item relate-ask":[["eventId","wave/id"],["summary"],(e,[a,t],s)=>n.relateAsk(e,a,t,s.summary)],"ask progress":[["eventId"],["next"],(e,[a],t)=>n.markAsk(e,a,"progress",t.next)],"ask done":[["eventId"],[],(e,[a])=>n.markAsk(e,a,"done")],"ask unfold":[["eventId"],[],(e,[a])=>n.unfoldAsk(e,a)],"ask move":[["eventId"],["to"],(e,[a],t)=>n.moveAsk(e,a,t.to)],"tickets backfill":[[],["dry-run"],(e,a,t,s)=>n.backfillTickets(e,{dryRun:t["dry-run"]},s)],"lineup create":[["slug"],["title","goal","...phase","note","file"],(e,[a],t)=>n.createLineup(e,a,{title:t.title,goal:t.goal,phases:t.phase,note:C(t)})],"lineup add":[["slug","wave/id"],["after"],(e,[a,t],s)=>n.changeLineup(e,a,{add:t,after:s.after})],"lineup remove":[["slug","wave/id"],[],(e,[a,t])=>n.changeLineup(e,a,{remove:t})],"lineup set":[["slug"],["title","goal","note","file","...phase"],(e,[a],t)=>n.changeLineup(e,a,{title:t.title,goal:t.goal,note:C(t),phases:t.phase})],"lineup finish":[["slug"],[],(e,[a])=>n.finishLineup(e,a)],slack:[[],[],()=>({lines:[JSON.stringify(D())]})],"slack set":[[],["self-dm","user","domain","timezone"],async(e,a,t)=>{const s={"self-dm":"self_dm",user:"owner_slack_user",domain:"slack_domain",timezone:"timezone"};try{return{lines:[JSON.stringify(await W(Object.fromEntries(Object.entries(t).map(([i,o])=>[s[i],o]))))]}}catch(i){throw new n.BoardError(i.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await L.rows())]})],"review updated":[["wave/id"],["text"],(e,[a],t)=>n.reviewUpdated(e,a,t.text)],"review sent":[["wave/id","reviewer"],["ts","channel"],(e,[a,t],s)=>n.reviewSent(e,a,t,s)],"review react":[["wave/id","reviewer","emoji"],[],(e,[a,t,s])=>n.reviewReact(e,a,t,s)],"review record":[["pr"],["state","text","file"],(e,[a],t)=>{if(t.text===void 0==(t.file===void 0))throw new l("review record takes --text or --file");return n.recordReview(e,a,{state:t.state,text:t.text??w(t.file)})}],"review post":[["pr"],["file","read-at"],(e,[a],t)=>{if(t.file===void 0)throw new l("review post needs --file <review.md>");if(t["read-at"]===void 0)throw new l("review post needs --read-at <time>, when you read the PR, so a review that landed since is caught");return n.postIncomingReview(e,a,{words:w(t.file),readAt:t["read-at"]})}],"review digest":[[],[],async(e,a,t,s)=>({lines:[JSON.stringify(await n.reviewDigest(e,s))]})],"review due":[[],[],async(e,a,t,s)=>({lines:[JSON.stringify(await n.reviewDue(e,s))]})],"review digest sent":[["reviewer"],["ts","channel","items"],(e,[a],t,s)=>n.reviewDigestSent(e,a,t,s)],"reviewers set":[[],["file"],(e,a,t)=>n.setReviewers(e,$(t.file))],"reviewers add":[[],["file"],(e,a,t)=>n.addReviewers(e,$(t.file))],"reviewers remove":[["...name"],[],(e,a)=>n.removeReviewers(e,a)],"rules list":[[],[],e=>n.listRules(e)],"rules add":[[],["text","hub","by"],(e,a,t)=>n.addRuleTo(e,t)],"rules edit":[["id"],["text","hub"],(e,[a],t)=>n.editRuleIn(e,a,t)],"rules remove":[["id"],["hub"],(e,[a],t)=>n.removeRuleFrom(e,a,t)],settings:[[],[],async e=>({lines:[JSON.stringify(await n.readBoardSettings(e))]})],build:[["wave/id"],["asked"],async(e,[a],t)=>{if(!e)throw new n.BoardError("build needs --site <route>");n.parseKey(a);const s=await n.askedGate(e,a,t.asked);try{const i=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:y.basename(e),key:a}),signal:AbortSignal.timeout(1e4)}),o=await i.json();if(!i.ok)throw new n.BoardError(o.error||"The hub refused the build.");return{lines:["started",...(await n.appendThread(e,a,{text:s})).lines]}}catch(i){throw i instanceof n.BoardError?i:new n.BoardError(`Could not start the build: ${i.message}`)}}],"main set":[[],["install","install-sha"],(e,a,t)=>n.setMainInstall(e,{install:t.install,installSha:t["install-sha"]})],"settings set":[["name","value"],[],(e,[a,t])=>n.setBoardSetting(e,a,t)],"builder start":[["wave/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[a],{worktree:t,repo:s,dispatch:i,...o})=>{if(s!==void 0&&!t)throw new l("--repo goes with --worktree");const d=r(p=>O(e,a.split("/")[1],{repo:s,...p}),"make"),c=e&&o.dir===void 0&&S(e);return n.startBuilder(e,a,{...o,dispatch:i?"session":void 0,worktree:t?d:void 0,ownWorktree:c?d:void 0})}],"builder progress":[["wave/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[a,...t],s)=>{if(t.length&&s.text!==void 0)throw new l("builder progress takes its text once: as words or as --text");if(!t.length&&s.text===void 0)throw new l("builder progress needs its text, as words or as --text");return n.builderProgress(e,a,s.text??t.join(" "),{...s,clearEta:s["clear-eta"]})}],"builder done":[["wave/id"],["dir","agent","next","owner-step","check","text"],async(e,[a],{"owner-step":t,check:s,text:i,...o})=>{if(t!==void 0&&(!e||o.dir))throw new l("--owner-step goes on a board card: pass --site, not --dir");if(s!==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,a,{ownerStep:t,check:s});const d=e&&!o.dir,c=d&&await n.handBackCalls(e,a,o);if(c)return c;if(d&&o.next===void 0&&await n.finishesCard(e,a,o)){if(i===void 0)throw new n.BoardError(`builder done finishes ${a}: pass --text "<what you checked>", or --next "<what happens next>" to stop without finishing`);return x(e,a,{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,a,{...o,requireNext:!0})}],"builder pick":[["wave/id"],[],(e,[a])=>n.pickBuilder(e,a)],audit:[["...wave/id?"],[],(e,[a,...t])=>{if(t.length)throw new l("audit takes one <wave/id> at most");return n.auditLog(e,a)}],"builder call":[["wave/id","...text"],["dir","agent"],(e,[a,...t],s)=>n.recordCall(e,a,t.join(" "),s)],"builder calls":[["wave/id"],["dir","agent"],(e,[a],t)=>n.listCalls(e,a,t)],"builder ask":[["wave/id","...text"],["agent"],(e,[a,...t],s)=>n.askOrchestrator(e,a,t.join(" "),s)],"builder answer":[["wave/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text)],"builder defer":[["wave/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text,{deferred:!0})],"builder asks":[["wave/id"],[],(e,[a])=>n.listAsks(e,a)],"builder verified":[["wave/id"],["dir","agent","note"],async(e,[a],t)=>{await n.verifyCalls(e,a,t);const s=e&&!t.dir?await n.verifiedFinish(e,a):null;if(!s)return{};if(s.waits.length)return{lines:[`${await n.cardName(e,a)} finishes once the owner answers ${s.waits.join(", ")}`]};try{return await x(e,a,{text:t.note??"Judgment calls verified."})}catch(i){throw i instanceof n.BoardError?Object.assign(new n.BoardError(`judgment calls verified, but ${await n.cardName(e,a)} didn't finish: ${i.message}`),{exitCode:i.exitCode}):i}}],land:[["wave/id"],["repo","dir","text","owner-step","check"],async(e,[a],t)=>{const s=a.split("/")[1];if(e&&t.text===void 0)throw new l(`land needs --text "<what was checked>", which closes ${a} 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,a,{text:t.text,ownerStep:t["owner-step"],check:t.check,repo:t.repo});const i=e?await n.cardName(e,a):void 0,o=await G(i??a,async()=>{e&&await n.checkClose(e,a,t.text,t["owner-step"],t.check),await n.checkCallsVerified(e,a,{dir:t.dir});const d=await n.recordedWorktree(e,a,{dir:t.dir}),{repo:c,base:p}=z(e,{repo:t.repo??d?.repo});return await J({path:d?.path??y.join(U(),s),branch:d?.branch??s,repo:y.resolve(c),base:p,ref:i}),e?n.setItem(e,a,{status:"done",text:t.text,ownerStep:t["owner-step"],check:t.check}):{lines:[]}});return o.joined?{lines:[`joined the land already running for ${i??a}`]}:o}],lander:[["wave/id"],[],async(e,[a])=>{if(!e)throw new l("lander needs --site <route>");return await n.relaunchLander(e,a),{lines:[`${await n.cardName(e,a)} is back at checking; the server launches a lander for it within a minute`]}}],merge:[["wave/id"],["after-approval","confirmed"],async(e,[a],t)=>e&&await n.heldMerge(e,a)?K(e,a):n.mergeCard(e,a,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["wave/id"],[],(e,[a])=>e?F(e,a):Promise.reject(new n.BoardError("arch file needs --site <route>"))],checked:[[],["every","idle"],(e,a,t)=>n.writeChecked(e,t.every,{idle:t.idle})],push:[[],["kind","title","body","url","tag","urgent","now"],(e,a,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)}]};f.install=f.build;const h=r((e,a)=>n.cardName(e,a),"card"),T=r((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),_=r(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function ne(e){const a=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",a!==void 0&&(a?`held until ${a}`:"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.relate!==void 0&&`related to ${e.relate.split(",").join(", ")} added`,e.unrelate!==void 0&&`unrelated from ${e.unrelate.split(",").join(", ")}`,e.install!==void 0&&"install published",e["clear-install"]&&"install cleared",e.title!==void 0&&"renamed",e.ticket!==void 0&&`ticket ${e.ticket}`,e.check!==void 0&&(e.check.trim()?`check ${e.check.trim().split(/\s+/).join(" ")}`:"check removed")].filter(Boolean).join(", ")}r(ne,"itemChanges");const re={reply:r(async(e,[a])=>`reply written to ${await n.eventCard(e,a)}`,"reply"),thread:r(async(e,[a])=>`thread line on ${await h(e,a)}`,"thread"),"topic add":r((e,[a])=>`topic ${a} added`,"topic add"),"question add":r(async(e,a,t,{asked:s,staged:i})=>{const o=[...new Set(s.map(c=>c.topic))],d=o.length!==1?"":o[0].startsWith("item:")?` on ${await h(e,o[0].slice(5))}`:` on topic ${o[0]}`;return`${T(s.map(c=>c.id),"question")} ${i===s.length?"staged":"added"}${d}`},"question add"),"question resolve":r((e,[a])=>`question ${a} resolved`,"question resolve"),"question move":r((e,[a],t)=>`questions ${a} merged onto ${t.to}`,"question move"),"prototype add":r(async(e,[a],t)=>`prototype ${a} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":r((e,[a])=>`prototype ${a} removed`,"prototype rm"),"item set":r(async(e,[a],t)=>`${await h(e,a)}: ${ne(t)}`,"item set"),"item add":r(async(e,a,t,{key:s})=>`${await h(e,s)} added as ${s}`,"item add"),"item relate-ask":r(async(e,[a,t])=>`task ${a} folded into ${await h(e,t)}`,"item relate-ask"),"ask progress":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: in progress`,"ask progress"),"ask done":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: done`,"ask done"),"ask unfold":r((e,[a])=>`task ${a} back on a card of its own`,"ask unfold"),"ask move":r((e,[a],t)=>`task ${a} moved to /${t.to}`,"ask move"),checked:r((e,a,t,{every:s})=>s===null?"GitHub checked; no open PR":`GitHub checked; next in ${s} min`,"checked"),"lineup create":r((e,[a])=>`lineup ${a} created`,"lineup create"),"lineup add":r(async(e,[a,t])=>`${await h(e,t)} added to lineup ${a}`,"lineup add"),"lineup remove":r(async(e,[a,t])=>`${await h(e,t)} taken out of lineup ${a}`,"lineup remove"),"lineup set":r((e,[a])=>`lineup ${a} updated`,"lineup set"),"lineup finish":r((e,[a])=>`lineup ${a} finished`,"lineup finish"),"review sent":r(async(e,[a,t])=>`review link to ${t} recorded on ${await h(e,a)}`,"review sent"),"review react":r(async(e,[a,t,s])=>`${s} on ${t}'s review link recorded on ${await h(e,a)}`,"review react"),"review digest sent":r((e,[a])=>`digest to ${a} recorded`,"review digest sent"),"review record":r((e,[a],t)=>`review of PR #${a} recorded: ${t.state==="needs"?"needs you":"posted"}`,"review record"),"reviewers set":r(()=>"reviewers replaced","reviewers set"),"reviewers add":r(()=>"reviewers added","reviewers add"),"reviewers remove":r((e,a)=>`${T(a,"reviewer")} removed`,"reviewers remove"),"rules add":r((e,a,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} reworded`,"rules edit"),"rules remove":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} removed`,"rules remove"),"builder start":r(async(e,[a],t,s)=>`${await h(e,a)}: ${s.alreadyOpen?"already open: the Implement tap opened its record; launch the agent, whose own builder start takes it up":`${_(t)} started`}`,"builder start"),"builder progress":r(async(e,[a])=>`progress line on ${await h(e,a)}`,"builder progress"),"builder done":r(async(e,[a],t,s)=>`${await h(e,a)}: ${s?.said??`${_(t)} done${t["owner-step"]===void 0?"":`; owner step "${t["owner-step"].trim()}"`}`}`,"builder done"),"builder call":r(async(e,[a])=>`judgment call recorded on ${await h(e,a)}`,"builder call"),"builder ask":r(async(e,[a],t,{id:s})=>`${await h(e,a)}: asked the orchestrator, ${s}`,"builder ask"),"builder answer":r(async(e,[a,t])=>`${await h(e,a)}: ${t} answered`,"builder answer"),"builder defer":r(async(e,[a,t])=>`${await h(e,a)}: ${t} deferred to the owner`,"builder defer"),"builder verified":r(async(e,[a],t,s)=>`${await h(e,a)}: judgment calls verified${s?.said?`; ${s.said}`:""}`,"builder verified"),land:r(async(e,[a],t,s)=>e?s?.said?`${await h(e,a)}: ${s.said}`:t["owner-step"]===void 0?`${await h(e,a)}: landed and done`:`${await h(e,a)}: landed; waits on the owner step "${t["owner-step"].trim()}"`:null,"land"),merge:r(async(e,[a],t,s)=>`${await h(e,a)}: ${s?.said??"merged"}`,"merge"),"arch file":r(async(e,[a])=>`findings of ${await h(e,a)} filed`,"arch file")};async function ie(e,a={}){try{const{flags:t,rest:s}=Y(e),i=[3,2,1].map(u=>s.slice(0,u).join(" ")).find(u=>v(f,u)),o=v(f,i);if(!o)throw new l(s.length?`unknown command "${s.slice(0,2).join(" ")}"`:"no command given");const[d,c,p]=o,m=s.slice(i.split(" ").length),N=d.at(-1)?.startsWith("..."),B=d.at(-1)?.endsWith("?")?d.length-1:d.length;if(N?m.length<B:m.length!==d.length)throw new l(`${i} takes ${d.map(u=>`<${u.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:P,...E}=t;if(P?.length>1)throw new l("--site is given twice");const A=P?.[0],k=Z(i,c,E);se(c,k);const j=A===void 0?null:await n.siteDir(A),b=await p(j,m,k,a),q=v(re,i),I=q&&await q(j,m,k,b);I&&console.log(I);for(const u of typeof b=="object"&&b?.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(ie,"main"),process.argv[1]&&import.meta.url===M(g.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(V):process.exitCode=await ie(e)}export{V as USAGE,ie as main};