@lovek4d/kickflip 0.9.0 → 0.10.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.
package/SKILL.md CHANGED
@@ -112,21 +112,21 @@ These make a site Claude answers: a board, or a page joined with `--watch`.
112
112
 
113
113
  Kickflip owns reviewer links, reactions and weekday digests ([kickflip owns the reviewer record](docs/adr/2026-11-01-2347-kickflip-owns-the-reviewer-record.md)). `slack-bridge` is optional: kickflip reads its topic files only as a fallback. Each board's `board.json` and each wave keep `reviewers: [{name, slack_user, dm, github}]`; new waves copy the board's list. The board server adds and removes reviewers itself: Board settings picks a Slack member and a GitHub login from lists the server fetches, and the hub's next poll sends an added reviewer every PR already in review. Its `reviewers-added {add}` and `reviewers-removed {remove}` events are record-only events, which the watcher holds and prints on the next wake; take no action on them ([the server adds reviewers from fetched lists](docs/adr/2026-11-05-2100-the-server-adds-reviewers-from-fetched-lists.md)). `board.mjs --site <route> reviewers add --file <json>` (`reviewers set --file` replaces the list) stays for scripts.
114
114
 
115
- **The hub sends reviewer Slack messages itself**, as the owner, through the Slack user token pasted on Hub › Integrations › Slack and kept with Linear's key in `~/.claude/kickflip/secrets.json` ([the hub sends reviewer Slack messages with its own token](docs/adr/2026-11-05-0141-the-hub-sends-reviewer-slack-messages-with-its-own-token.md)). The session sends nothing to Slack for reviewers and records none of it. Slack messages hang off PRs, not cards: each board's `data/pr-slack.json` holds every PR's link to each reviewer, its bumps and their reactions ([Slack sends hang off PRs and go out from the hub tick](docs/adr/2026-11-07-0300-slack-sends-hang-off-prs-and-go-out-from-the-hub-tick.md)). Each reviewer gets one thread per board for each Slack day, in the DM the hub opens with them, and every review post goes in it as a reply ([review posts go into one day thread per board](docs/adr/2026-11-11-1600-review-posts-go-into-one-day-thread-per-board.md)). On each poll the hub reads every board's `board.mjs --site <route> review due` and sends what it owes:
115
+ **The hub sends reviewer Slack messages itself**, as the owner, through the Slack user token pasted on Hub › Integrations › Slack and kept with Linear's key in `~/.claude/kickflip/secrets.json` ([the hub sends reviewer Slack messages with its own token](docs/adr/2026-11-05-0141-the-hub-sends-reviewer-slack-messages-with-its-own-token.md)). The session sends nothing to Slack for reviewers and records none of it. Slack messages hang off PRs, not cards: each board's `data/pr-slack.json` holds every PR's link to each reviewer and their reactions ([Slack sends hang off PRs and go out from the hub tick](docs/adr/2026-11-07-0300-slack-sends-hang-off-prs-and-go-out-from-the-hub-tick.md)). Each reviewer gets one thread per board for each Slack day, in the DM the hub opens with them, and every review post goes in it as a reply ([review posts go into one day thread per board](docs/adr/2026-11-11-1600-review-posts-go-into-one-day-thread-per-board.md)). The thread carries only its opener, each PR's link and the digest ([a day thread carries only each PR's link](docs/adr/2026-11-12-0044-a-day-thread-carries-only-each-prs-link.md)). On each poll the hub reads every board's `board.mjs --site <route> review due` and sends what it owes:
116
116
  - **The day's thread:** the weekday digest opens it when it is due before anything else is posted that day; otherwise a “<board title> reviews” header line does, and a digest due later goes in as one reply listing the PRs.
117
- - **Replies:** each opens with the reviewer's @mention, so Slack tells them, then repo#n linking the PR. Each open, non-draft PR gets its own link reply, “ready for review: <PR title> (+N/-M, K files)”. A push to the PR (“<n> new commits”), a `pr comment` or `pr reply` (“comment: …”) and Updated (“updated: …”) are brief bumps in the thread of the day they are sent. A link posted top-level before the day threads keeps its own thread for its bumps until its PR closes.
118
- - **Reactions:** every link and bump about a PR gets `git_approved` once anyone approves, then `git_merged` on merge or `x` on a close. The header and the digest get none.
117
+ - **Replies:** each opens with the reviewer's @mention, so Slack tells them, then repo#n linking the PR. Each open, non-draft PR gets its own link reply, “ready for review: <PR title> (+N/-M, K files)”. Nothing else about a PR posts: no push, comment or Updated bump.
118
+ - **Reactions:** every posted message about one PR (its links, a one-PR digest reply, and bumps sent before they stopped) gets `git_approved` once anyone approves, then `git_merged` on merge or `x` on a close. The header and a digest reply naming several PRs get none.
119
119
  - **Digest:** “<board title> digest: <n> PRs pending your review”, with one item per PR, “<PR link> · <card title>”, plus “ (<n> days old)” for a link over two days old.
120
120
 
121
121
  Each message is recorded as it posts. A failed send stays owed and goes again next poll; failures go to the hub log only. While Slack is not ready (no token, or Slack refused it), `review due` answers `waiting` and the hub sends nothing; a refused token pushes once while any board has reviewers. The digest is owed to each reviewer on weekdays from 09:00 to 17:00 in the Slack identity timezone (`board.mjs slack set --timezone Area/City`; with none set, the host timezone), until its header and every reply have gone; a day missed by 17:00 is skipped. `board.mjs slack` reads the identity in `~/.claude/kickflip/slack.json`.
122
122
 
123
- **Reviewers ask the board in the day thread** ([reviewers ask the board in the day thread](docs/adr/2026-11-12-0000-reviewers-ask-the-board-in-the-day-thread.md)). The hub hears each day thread for 7 days after it opens, and each new message wakes you with a `slack-question {reviewer, channel, thread_ts, ts, from, answer}` event, which holds where the message is, never its words. `from` is `reviewer` or `owner`; the board's own posts and your answers wake nobody. Read it with `board.mjs --site <route> slack thread <event>`, which prints the thread and marks the message with →. With `answer: true` (every reviewer message, and the owner's when it names @kickflip), do one of two things:
124
- - **A question:** `slack answer <event> --text "<answer>"` posts "🤖 " and your answer in the thread, at most 600 characters. Keep it brief, and speak only about this board's cards, PRs and code. Answers stay in Slack; record nothing on the board.
123
+ **Reviewers ask the board in the day thread** ([reviewers ask the board in the day thread](docs/adr/2026-11-12-0000-reviewers-ask-the-board-in-the-day-thread.md)). The hub hears each day thread for 7 days after it opens, and each new message wakes you with a `slack-question {reviewer, channel, thread_ts, ts, from, answer}` event, which holds where the message is, never its words. `from` is `reviewer` or `owner`; the board's own posts and your answers wake nobody. Read it with `board.mjs --site <route> slack thread <event>`, which prints the thread and marks the message with →. With `answer: true` (every reviewer message, and the owner's when it names @kickflip), you may reply; you don't owe one. Reply only when the message asks something; a thanks or an acknowledgement gets nothing. Then do one of two things:
124
+ - **A question:** `slack answer <event> --text "<answer>"` posts "🤖 " and your answer in the thread, at most 250 characters. Keep it brief, and speak only about this board's cards, PRs and code. Answers stay in Slack; record nothing on the board.
125
125
  - **A request for work**, whatever its size: `slack task <event> <file> --title <t> --notes-from <draft.md>` files a new card grilling for the owner's review, with the Slack message as its first thread line. Never file a reviewer's request any other way, and never build it Quick. Then `slack answer` saying it is filed for the owner.
126
126
 
127
127
  An owner's message with `answer: false` is context: read it, reply nothing. Until the token has `im:history`, the hub pushes once, the Integrations Slack row says to reinstall the app, and sends go on.
128
128
 
129
- **Updated.** After a behavior change on a card whose PR link a reviewer has, the builder's `builder done` takes `--updated "<what changed>"`, and refuses without it or `--updated none`. `board.mjs --site <route> review updated <file>/<id> --text "<what changed>"` does the same outside a build. Either one queues “updated: <what changed>” for every reviewer with a posted link on the card's PRs. Changes only to comments, names, docs or tests stay silent.
129
+ **You write in a day thread only when asked** ([a day thread carries only each PR's link](docs/adr/2026-11-12-0044-a-day-thread-carries-only-each-prs-link.md)): to answer a message there that asks something, or when the owner explicitly asks you to message a reviewer. Then `board.mjs --site <route> slack say <reviewer> --text "<words>"` posts "🤖 " and the words, at most 250 characters, in that reviewer's day thread for today, opening it with the "<board title> reviews" header when none is open. Never volunteer an update, a push, a comment or a status in Slack: a reviewer reads the PR on GitHub.
130
130
 
131
131
  **Rules** ([rules live on Settings, at hub and board level](docs/adr/2026-10-29-1639-rules-live-on-settings-at-hub-and-board-level.md)). On a board, the watcher prints `rule hub <text>` and `rule board <text>` lines after the `event` lines. They are the owner's standing instructions: follow them as you follow this skill. Hub rules apply to every board, and the board's own come on top. A `rules {scope, added, edited, removed}` event means the owner saved them on hub or board Settings. Read the printed list, then write one reply (`board.mjs reply <eventId>`, shown under the Rules section) saying what you will do differently. When the owner says "codify" in a thread, run `board.mjs --site <route> rules add --by claude --text "<the rule>"`. Add it at board level unless it concerns every board; then pass `--hub`. `rules list` shows both lists with their ids, for `rules edit` and `rules remove`.
132
132
 
@@ -1 +1 @@
1
- var h=Object.defineProperty;var t=(e,a)=>h(e,"name",{value:a,configurable:!0});import l from"node:path";import{waitsAgainLine as c}from"../board/domain.mjs";import{cardName as u}from"../board/store.mjs";import{routeRecord as d}from"../hub/routes.mjs";const b=["item-message","ask","ask-edited"],f=t((e,a)=>l.join(e,"data",`.shown-${a}.json`),"shownFile"),k=["main-pulled","settings","reviewers-removed","reviewers-added","question-reopen","repo-changed","ping","hello","card-hold","card-done","card-unbump","card-unblocked","grill-more","prototype-request","visual-request","usage-resumed"],p=t(e=>{try{return JSON.parse(e).kind}catch{return null}},"kindOf"),v=t(e=>k.includes(p(e)),"recordOnly"),o={"owner-answer":t(e=>`answer received to question ${e.data?.id}: act on it on its card`,"owner-answer"),ping:t(e=>`ping ${e.id} from the hub answered`,"ping"),hello:t(e=>`ping ${e.id} from the hub answered`,"hello"),"overnight-triage":t(e=>`Overnight triage ${e.data?.date} asks Claude to reply: ${e.data?.cards?.length} cards wait on open questions; run the triage, then board.mjs reply ${e.id}`,"overnight-triage"),"pr-review":t(e=>`review asked on PR #${e.data?.pr}: the board was idle; read its diff and review post it`,"pr-review"),"orchestrator-waits":t(e=>[e.data?.added?.length&&`waiting on you now: ${e.data.added.join(", ")}`,e.data?.again?.length&&c(e.data.again,Date.parse(e.ts))].filter(Boolean).join(" \xB7 "),"orchestrator-waits"),"check-github":t(e=>e.data?.reason==="morning"?"the daily wake: run one GitHub tick, then walk every lane top-down once: clear each blocked-until condition that holds, and act on any card stuck with no wait; a thread line on each card you act on, nothing else":"GitHub check asked: run one GitHub tick","check-github"),"github-change":t(e=>`${e.data?.prs?.length??0} PRs changed on GitHub: run one GitHub tick`,"github-change"),"review-answer":t(e=>e.data?.answer==="skip"?`skip answered on the review of PR #${e.data?.pr}: record it posted, post nothing`:`post answered on the review of PR #${e.data?.pr}: review post it`,"review-answer"),"plan-waves":t(e=>`Plan waves asked on ${e.data?.keys?.length??0} Todo cards: triage them into waves and ask the owner about them`,"plan-waves"),"arch-review":t(e=>`architecture review asked on ${e.data?.key}: review ${e.data?.repo} on ${e.data?.branch} and file the findings`,"arch-review"),rules:t(e=>`${e.data?.scope==="hub"?"hub":"board"} rules changed: the rule lines below are the new ones; follow them from now`,"rules"),"prototype-overdue":t(e=>`prototype overdue on ${e.data?.key}: dispatch its prototype agent now`,"prototype-overdue"),"slack-question":t(e=>e.data?.answer?`${e.data.from==="owner"?"the owner":e.data?.reviewer} asked the board in ${e.data?.reviewer}'s Slack review thread: slack thread ${e.id}, then answer only about this board with slack answer ${e.id} --text, or file a request for work with slack task ${e.id} <file>`:`the owner wrote in ${e.data?.reviewer}'s Slack review thread without @kickflip: slack thread ${e.id} for context; no reply`,"slack-question")},s={"item-message":t((e,a)=>String(a.data?.key).startsWith("ask:")&&a.data?.intent==="build"?`Implement asked on ${e}: file it with item add --from-ask --phase ready, with its --size and notes`:a.data?.intent==="build"?`Implement asked on ${e}: it is staging, the message in its notes; judge it against the whole board, then item set --phase ready --text "<what you checked>", or --phase grilling or prototyping`:`message received from ${e}'s thread: reply in its thread`,"item-message"),"card-bump":t(e=>`${e} bumped: handle its waits lines first; with none, leave it`,"card-bump"),"ask-edited":t(e=>`${e} edited: use the new text, and say what changed if you had started`,"ask-edited"),"ask-withdrawn":t(e=>`${e} deleted: drop it`,"ask-withdrawn"),"card-done":t(e=>`${e} marked done`,"card-done"),"card-hold":t((e,a)=>`${e} ${a.data?.hold?"held":"resumed"}`,"card-hold"),"card-unblocked":t(e=>`${e} unblocked`,"card-unblocked"),"prototype-request":t(e=>`prototype asked on ${e}`,"prototype-request"),"visual-request":t(e=>`visual asked on ${e}: the board launches its visual agent`,"visual-request"),"reask-request":t(e=>`question asked again on ${e}: ask it again with question add reask_of`,"reask-request"),"builder-resume":t((e,a)=>`${e}'s pause cleared (${a.data?.cleared}): tell its builder in your session to carry on where it stopped`,"builder-resume"),"builder-ask":t((e,a)=>`${a.data?.ask} from ${e}'s builder: ${a.data?.text} (builder answer it, or builder defer it)`,"builder-ask"),"owner-check-broken":t((e,a)=>`${e}'s owner-step check can't run: ${a.data?.error}; fix it with item set --check, or drop it`,"owner-check-broken"),"card-added":t(e=>`${e} added by an agent: link it and file it: item notes, then item set --relate and --phase`,"card-added"),ask:t((e,a)=>`new task ${a.id} received as ${e}: sort that New card: item notes, then item set <its key> --phase grilling or prototyping (ready, with --size, for a Quick build); merge it into a card with item relate-ask ${a.id} <card> --summary; or answer it with reply <its key>`,"ask")};async function x(e,a){let r;try{r=JSON.parse(a)}catch{return null}if(typeof r?.kind!="string")return null;const n=typeof r.data?.key=="string"?r.data.key:r.kind.startsWith("ask-")&&typeof r.data?.id=="string"?`ask:${r.data.id}`:r.kind==="ask"?`ask:${r.id}`:null,i=n&&await u(e,n).catch(()=>n);return d(o,r.kind)?o[r.kind](r):i&&d(s,r.kind)?s[r.kind](i,r):i?`${r.kind} on ${i}`:`${r.kind} received`}t(x,"plainLine");export{s as ON_CARD,b as OWNER_WORDS,o as PLAIN,k as RECORD_ONLY,x as plainLine,v as recordOnly,f as shownFile};
1
+ var h=Object.defineProperty;var t=(e,a)=>h(e,"name",{value:a,configurable:!0});import l from"node:path";import{waitsAgainLine as c}from"../board/domain.mjs";import{cardName as u}from"../board/store.mjs";import{routeRecord as d}from"../hub/routes.mjs";const f=["item-message","ask","ask-edited"],b=t((e,a)=>l.join(e,"data",`.shown-${a}.json`),"shownFile"),k=["main-pulled","settings","reviewers-removed","reviewers-added","question-reopen","repo-changed","ping","hello","card-hold","card-done","card-unbump","card-unblocked","grill-more","prototype-request","visual-request","usage-resumed"],p=t(e=>{try{return JSON.parse(e).kind}catch{return null}},"kindOf"),v=t(e=>k.includes(p(e)),"recordOnly"),o={"owner-answer":t(e=>`answer received to question ${e.data?.id}: act on it on its card`,"owner-answer"),ping:t(e=>`ping ${e.id} from the hub answered`,"ping"),hello:t(e=>`ping ${e.id} from the hub answered`,"hello"),"overnight-triage":t(e=>`Overnight triage ${e.data?.date} asks Claude to reply: ${e.data?.cards?.length} cards wait on open questions; run the triage, then board.mjs reply ${e.id}`,"overnight-triage"),"pr-review":t(e=>`review asked on PR #${e.data?.pr}: the board was idle; read its diff and review post it`,"pr-review"),"orchestrator-waits":t(e=>[e.data?.added?.length&&`waiting on you now: ${e.data.added.join(", ")}`,e.data?.again?.length&&c(e.data.again,Date.parse(e.ts))].filter(Boolean).join(" \xB7 "),"orchestrator-waits"),"check-github":t(e=>e.data?.reason==="morning"?"the daily wake: run one GitHub tick, then walk every lane top-down once: clear each blocked-until condition that holds, and act on any card stuck with no wait; a thread line on each card you act on, nothing else":"GitHub check asked: run one GitHub tick","check-github"),"github-change":t(e=>`${e.data?.prs?.length??0} PRs changed on GitHub: run one GitHub tick`,"github-change"),"review-answer":t(e=>e.data?.answer==="skip"?`skip answered on the review of PR #${e.data?.pr}: record it posted, post nothing`:`post answered on the review of PR #${e.data?.pr}: review post it`,"review-answer"),"plan-waves":t(e=>`Plan waves asked on ${e.data?.keys?.length??0} Todo cards: triage them into waves and ask the owner about them`,"plan-waves"),"arch-review":t(e=>`architecture review asked on ${e.data?.key}: review ${e.data?.repo} on ${e.data?.branch} and file the findings`,"arch-review"),rules:t(e=>`${e.data?.scope==="hub"?"hub":"board"} rules changed: the rule lines below are the new ones; follow them from now`,"rules"),"prototype-overdue":t(e=>`prototype overdue on ${e.data?.key}: dispatch its prototype agent now`,"prototype-overdue"),"slack-question":t(e=>e.data?.answer?`${e.data.from==="owner"?"the owner":e.data?.reviewer} wrote to the board in ${e.data?.reviewer}'s Slack review thread: slack thread ${e.id}, then, only if it asks something, answer about this board alone with slack answer ${e.id} --text (250 characters at most), or file a request for work with slack task ${e.id} <file>; a thanks gets no reply`:`the owner wrote in ${e.data?.reviewer}'s Slack review thread without @kickflip: slack thread ${e.id} for context; no reply`,"slack-question")},s={"item-message":t((e,a)=>String(a.data?.key).startsWith("ask:")&&a.data?.intent==="build"?`Implement asked on ${e}: file it with item add --from-ask --phase ready, with its --size and notes`:a.data?.intent==="build"?`Implement asked on ${e}: it is staging, the message in its notes; judge it against the whole board, then item set --phase ready --text "<what you checked>", or --phase grilling or prototyping`:`message received from ${e}'s thread: reply in its thread`,"item-message"),"card-bump":t(e=>`${e} bumped: handle its waits lines first; with none, leave it`,"card-bump"),"ask-edited":t(e=>`${e} edited: use the new text, and say what changed if you had started`,"ask-edited"),"ask-withdrawn":t(e=>`${e} deleted: drop it`,"ask-withdrawn"),"card-done":t(e=>`${e} marked done`,"card-done"),"card-hold":t((e,a)=>`${e} ${a.data?.hold?"held":"resumed"}`,"card-hold"),"card-unblocked":t(e=>`${e} unblocked`,"card-unblocked"),"prototype-request":t(e=>`prototype asked on ${e}`,"prototype-request"),"visual-request":t(e=>`visual asked on ${e}: the board launches its visual agent`,"visual-request"),"reask-request":t(e=>`question asked again on ${e}: ask it again with question add reask_of`,"reask-request"),"builder-resume":t((e,a)=>`${e}'s pause cleared (${a.data?.cleared}): tell its builder in your session to carry on where it stopped`,"builder-resume"),"builder-ask":t((e,a)=>`${a.data?.ask} from ${e}'s builder: ${a.data?.text} (builder answer it, or builder defer it)`,"builder-ask"),"owner-check-broken":t((e,a)=>`${e}'s owner-step check can't run: ${a.data?.error}; fix it with item set --check, or drop it`,"owner-check-broken"),"card-added":t(e=>`${e} added by an agent: link it and file it: item notes, then item set --relate and --phase`,"card-added"),ask:t((e,a)=>`new task ${a.id} received as ${e}: sort that New card: item notes, then item set <its key> --phase grilling or prototyping (ready, with --size, for a Quick build); merge it into a card with item relate-ask ${a.id} <card> --summary; or answer it with reply <its key>`,"ask")};async function x(e,a){let r;try{r=JSON.parse(a)}catch{return null}if(typeof r?.kind!="string")return null;const n=typeof r.data?.key=="string"?r.data.key:r.kind.startsWith("ask-")&&typeof r.data?.id=="string"?`ask:${r.data.id}`:r.kind==="ask"?`ask:${r.id}`:null,i=n&&await u(e,n).catch(()=>n);return d(o,r.kind)?o[r.kind](r):i&&d(s,r.kind)?s[r.kind](i,r):i?`${r.kind} on ${i}`:`${r.kind} received`}t(x,"plainLine");export{s as ON_CARD,f as OWNER_WORDS,o as PLAIN,k as RECORD_ONLY,x as plainLine,v as recordOnly,b as shownFile};
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var H=Object.defineProperty;var r=(e,a)=>H(e,"name",{value:a,configurable:!0});import{readiness as W}from"../../hub/readiness.mjs";import{readSlack as D,setSlack as M}from"../../hub/slack.mjs";import{routeRecord as y}from"../../hub/routes.mjs";import g from"node:fs";import x from"node:path";import{pathToFileURL as L}from"node:url";import*as n from"../store.mjs";import{boardWorktree as z,builderRepo as F,joiningLand as J,landInTurn as G,mergeHelper as U,worktreesDir as K}from"../worktree.mjs";import{pidAlive as Q}from"../headless.mjs";import{fileFindings as V}from"../arch-review.mjs";import{finishCard as $,landHeld as X}from"../finish.mjs";import{bump as S,queueLines as Z,stallCard as Y,stopBuilder as ee,stopLand as te}from"../stop.mjs";import{boardAt as ae}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";import{checkTakesRender as se}from"../render-check.mjs";import{prototypeBase as ne}from"../prototype-base.mjs";const re=`usage: node board.mjs <command> [--site <route>] ...
2
+ var H=Object.defineProperty;var i=(e,a)=>H(e,"name",{value:a,configurable:!0});import{readiness as W}from"../../hub/readiness.mjs";import{readSlack as D,setSlack as M}from"../../hub/slack.mjs";import{routeRecord as y}from"../../hub/routes.mjs";import g from"node:fs";import x from"node:path";import{pathToFileURL as L}from"node:url";import*as n from"../store.mjs";import{boardWorktree as z,builderRepo as F,joiningLand as J,landInTurn as G,mergeHelper as K,worktreesDir as U}from"../worktree.mjs";import{pidAlive as Q}from"../headless.mjs";import{fileFindings as V}from"../arch-review.mjs";import{finishCard as $,landHeld as X}from"../finish.mjs";import{bump as S,queueLines as Z,stallCard as Y,stopBuilder as ee,stopLand as te}from"../stop.mjs";import{boardAt as ae}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";import{checkTakesRender as se}from"../render-check.mjs";import{prototypeBase as ne}from"../prototype-base.mjs";const ie=`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 item files live. Without --site, they come from ~/.claude/waves.
@@ -330,27 +330,28 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
330
330
  slack thread <eventId> a slack-question event's day thread, read from Slack through
331
331
  Hub's token: one message a line, named board, you or the
332
332
  reviewer; \u2192 marks the event's message
333
- slack answer <eventId> --text <t> posts "\u{1F916} " and the answer, at most 600 characters, as a reply
333
+ slack answer <eventId> --text <t> posts "\u{1F916} " and the answer, at most 250 characters, as a reply
334
334
  in that thread. Refuses an owner's message that names no
335
335
  @kickflip, which expects no reply
336
+ slack say <reviewer> --text <t> posts "\u{1F916} " and the words, at most 250 characters, in the
337
+ reviewer's day thread for today, opening it with the
338
+ "<board> reviews" header when none is open; only on the
339
+ owner's explicit ask
336
340
  slack task <eventId> <file> --title <t> a request for work in that thread: a new card in item file
337
341
  (--notes <md> | --notes-from <draft.md>) <file>, filed grilling for the owner's review, never ready;
338
342
  the Slack message is its first thread line, by its writer
339
343
  integrations readiness rows as one JSON line
340
- review updated <file>/<id> --text "<what changed>" queues \u201Cupdated: <what changed>\u201D for every reviewer with a
341
- posted link on the card's PRs; the hub's next poll sends it
342
344
  review due one JSON line of the Slack sends the hub owes, from local state
343
345
  and data/pr-slack.json: {date, links: [{pr, key, reviewer,
344
- slack_user, head, text}], replies: [{pr, id, reviewer,
345
- channel, thread_ts, text}], reactions: [{pr, reviewer,
346
- reply?, channel, ts, emoji}], digest: review digest's
347
- answer while due, else null}. links: each open, non-draft
348
- PR's link to each reviewer without one; links and replies
349
- with no thread_ts go in the reviewer's thread for date;
350
- reactions: git_approved once anyone approved, git_merged
351
- on merge, x on close, on every link and reply. The hub
352
- sends and records each. While Slack isn't ready: {waiting}
353
- and nothing owed
346
+ slack_user, text}], reactions: [{pr, reviewer, reply?,
347
+ channel, ts, emoji}], digest: review digest's answer while
348
+ due, else null}. links: each open, non-draft PR's link to
349
+ each reviewer without one, in the reviewer's thread for
350
+ date; no other message about a PR is owed. reactions:
351
+ git_approved once anyone approved, git_merged on merge, x
352
+ on close, on every posted link and reply. The hub sends
353
+ and records each. While Slack isn't ready: {waiting} and
354
+ nothing owed
354
355
  review post <pr> --file <review.md> posts the session's words as one Comment review (never an
355
356
  --read-at <time> approval or a request for changes) on an incoming PR at
356
357
  its listed head, as the board's github-login (else gh's
@@ -536,11 +537,6 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
536
537
  arguments are refused. Three runs in a row that can't run
537
538
  mark it broken and append owner-check-broken {key, check,
538
539
  error}; fix it with item set --check. land takes it too
539
- [--updated "<what changed>" | --updated none] on a card whose PR link a reviewer already has, done
540
- refuses without it: the text queues \u201Cupdated: <what
541
- changed>\u201D for each reviewer, as review updated does;
542
- none sends nothing, for a change only to comments, names,
543
- docs or tests
544
540
  [--title "<type>(<scope>): <summary>"] the draft PR's title on a GitHub board, a Conventional
545
541
  subject of at most 72 characters (else the branch's first
546
542
  commit subject); refused in any other shape before
@@ -703,4 +699,4 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
703
699
  (overnight, snoozed or the weekend). --urgent: a
704
700
  production fault only; it beats those holds. While
705
701
  notifications are off every push is dropped, urgent too
706
- KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class d extends Error{static{r(this,"UsageError")}}const ie=new Set(["new-ticket","done","clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","wait","hub","clear-install","force","worktree","dispatch","writes"]);function oe(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 l=i.slice(2);if(ie.has(l)){if(Object.hasOwn(a,l))throw new d(`--${l} is given twice`);a[l]=!0}else if(s+1<e.length)(a[l]||=[]).push(e[++s]);else throw new d(`--${l} needs a value`)}return{flags:a,rest:t}}r(oe,"parse");function de(e,a,t){const s=new Set(a.filter(o=>o.startsWith("...")).map(o=>o.slice(3))),i=new Set(a.map(o=>o.replace(/^\.\.\./,""))),l=Object.keys(t).filter(o=>!i.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||s.has(o))return[o,c];if(c.length>1)throw new d(`--${o} is given twice`);return[o,c[0]]}))}r(de,"flagsOf");const le=["text","note","notes","heading","summary","title","body","next","progress","install-label","confirmed","asked","task","reason"],he=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function ce(e){const a=e.trim();if(!a||/\s/.test(a))return!1;if(he.test(a))return!0;try{return g.statSync(a).isFile()}catch{return!1}}r(ce,"looksLikePath");function ue(e,a){for(const t of le){if(typeof a[t]!="string"||!ce(a[t]))continue;const s=e.includes("file")?`; for a file use --file ${a[t].trim()}`:"";throw new d(`--${t} takes text, not a file path (${a[t].trim()})${s}`)}if(typeof a.file=="string"&&a.file!=="-"&&!g.existsSync(a.file))throw new d(`--file ${a.file}: no such file`)}r(ue,"checkInputs");const m=r(e=>g.readFileSync(e==="-"?0:e,"utf8"),"readInput");function R(e){if(!e)throw new d("--file <path> is required");try{return JSON.parse(m(e))}catch(a){throw new n.BoardError(`${e}: ${a.message}`)}}r(R,"readJSONInput");function C(e,a){const t=["notes-file","plan"].find(s=>e[s]!==void 0);if(t)throw new d(`--${t} is gone: the board names a card's notes file, <id>-notes.md, and writes it itself; ${a}`)}r(C,"refuseNotesFile");function T(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:m(e.file)}r(T,"noteOf");async function pe(e,a,{"owner-step":t,check:s,text:i,title:l,...o}){if(t!==void 0&&(!e||o.dir))throw new d("--owner-step goes on a board card: pass --site, not --dir");if(s!==void 0&&t===void 0)throw new d("--check goes with --owner-step: it is the code check that closes the card once the step is done");t!==void 0&&await n.setItem(e,a,{ownerStep:t,check:s});const c=e&&!o.dir,u=c&&await n.handBackCalls(e,a,o);if(u)return u;if(c&&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 $(e,a,{text:i,ownerStep:t,title:l})}if(c&&await n.isHelper(e,a,o.agent)){if(i===void 0)throw new d('a helper hands back its result: pass --text "<what you did or found>"');return n.stopBuilder(e,a,{...o,note:i})}if(c&&i!==void 0&&await n.isPlanner(e,a,o.agent))return n.stopPlanner(e,a,{...o,note:i,requireNext:!0});if(i!==void 0)throw new d("--text goes on a builder done that finishes the card, the main build with a worktree and no --next, or on a helper's or a planner's");return n.stopBuilder(e,a,{...o,requireNext:!0})}r(pe,"builderDone");const b={pr:[["file/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 d('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 d('pr reply needs --body "<what Claude says>"');return n.postPR(e,a,{replyTo:t,body:s.body})}],reply:[["eventId"],["text","file","title"],(e,[a],t)=>{if(t.text===void 0==(t.file===void 0))throw new d("reply takes --text or --file");const s=t.text??m(t.file);return a.includes("/")?n.answerCard(e,a,s,t.title):n.writeReply(e,a,s,t.title)}],thread:[["file/id"],["text","link","who","via"],(e,[a],t)=>n.appendMessage(e,a,t)],"visual add":[["file/id"],["file","title"],(e,[a],t)=>n.addVisual(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=R(t.file);return{...await n.addQuestions(e,s,process.env.KICKFLIP_RENDER_CHECK==="off"?void 0:se),asked:[s].flat()}}],"question resolve":[["id"],["note"],(e,[a],t)=>n.resolveQuestion(e,a,t.note)],"question settle":[["id"],["note"],(e,[a],t)=>n.settleQuestion(e,a,t.note)],"question publish":[["key"],[],async(e,[a])=>({ids:await n.publishScreened(e,a)})],"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","served","...state"],(e,[a],t)=>n.addPrototype(e,{topic:a,title:t.title,card:t.card,takes:t.take,served:t.served,states:t.state})],"prototype base":[[],[],e=>ne(e)],"prototype list":[[],[],e=>n.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[a])=>n.removePrototype(e,a)],"item set":[["file/id"],["done","split-into","status","clear-status","next","clear-next","...pr","pushed","gh-since","from-ask","phase","state","held","unheld","pick-up-at","notes-file","plan","deps","blocked-until","block-reason","parent","related","relate","unrelate","install","install-sha","install-label","clear-install","title","size","model","force","ticket","new-ticket","text","check"],async(e,[a],t,s)=>(C(t,`add to its notes with item notes ${a} --heading "<heading>" --file <draft.md>`),t.done&&e&&await n.checkNotReopened(e,a),n.setItem(e,a,{done:t.done,splitInto:t["split-into"],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"],phase:t.phase,state:t.state,held:t.held,unheld:t.unheld,pickUpAt:t["pick-up-at"],deps:t.deps,blockedUntil:t["blocked-until"],blockReason:t["block-reason"],parent:t.parent,related:t.related,relate:t.relate,unrelate:t.unrelate,install:t.install,installSha:t["install-sha"],installLabel:t["install-label"],clearInstall:t["clear-install"],title:t.title,size:t.size,model:t.model,force:t.force,ticket:t.ticket,newTicket:t["new-ticket"],text:t.text,check:t.check},s))],"item add":[["file[/id]"],["title","notes","notes-from","deps","notes-file","plan","ticket","new-ticket","pick-up-at","held","parent","split-of","size","model","phase","state","relate","from-ask","text"],(e,[a],t,s)=>{if(C(t,"seed a new card's notes with item add --notes-from <draft.md>"),t.notes!==void 0&&t["notes-from"]!==void 0)throw new d("pass --notes or --notes-from, not both");return n.addItem(e,a,{title:t.title,notes:t.notes,notesFrom:t["notes-from"],deps:t.deps,ticket:t.ticket,newTicket:t["new-ticket"],pickUpAt:t["pick-up-at"],held:t.held,parent:t.parent,splitOf:t["split-of"],size:t.size,model:t.model,phase:t.phase,state:t.state,relate:t.relate,fromAsk:t["from-ask"],text:t.text,intake:!0},s)}],"item notes":[["file/id"],["heading","file","from"],(e,[a],t)=>{if(t.heading===void 0&&t.from===void 0)throw new d(`item notes needs --heading "<the section's title>"`);if(t.file===void 0==(t.from===void 0))throw new d("item notes takes --file <draft.md> or --from <file>/<id>");return n.addNotes(e,a,t)}],"item relate-ask":[["eventId","file/id"],["summary","title"],(e,[a,t],s)=>n.relateAsk(e,a,t,s.summary,s.title)],"ask unfold":[["eventId"],[],(e,[a])=>n.unfoldAsk(e,a)],"ask move":[["eventId"],["to"],(e,[a],t)=>n.moveAsk(e,a,t.to)],"wave create":[["slug"],["title","goal","...card","note","file"],(e,[a],t)=>n.createWave(e,a,{title:t.title,goal:t.goal,cards:t.card,note:T(t)})],"wave add":[["slug","file/id"],["after"],(e,[a,t],s)=>n.changeWave(e,a,{add:t,after:s.after})],"wave remove":[["slug","file/id"],[],(e,[a,t])=>n.changeWave(e,a,{remove:t})],"wave set":[["slug","...file/id?"],["after","title","goal","note","file"],(e,[a,...t],s)=>{if(t.length>1)throw new d("wave set takes <slug> [<file/id>]");return n.changeWave(e,a,{card:t[0],after:s.after,title:s.title,goal:s.goal,note:T(s)})}],"wave finish":[["slug"],[],(e,[a])=>n.finishWave(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 M(Object.fromEntries(Object.entries(t).map(([i,l])=>[s[i],l]))))]}}catch(i){throw new n.BoardError(i.message)}}],"slack thread":[["eventId"],[],(e,[a])=>n.slackThread(p(e,"slack thread"),a)],"slack answer":[["eventId"],["text"],(e,[a],t)=>n.slackAnswer(p(e,"slack answer"),a,t.text)],"slack task":[["eventId","file"],["title","notes","notes-from"],(e,[a,t],s)=>{if(s.notes!==void 0&&s["notes-from"]!==void 0)throw new d("pass --notes or --notes-from, not both");return n.slackTask(p(e,"slack task"),a,t,{title:s.title,notes:s.notes,notesFrom:s["notes-from"]})}],integrations:[[],[],async()=>({lines:[JSON.stringify(await W.rows())]})],"review updated":[["file/id"],["text"],(e,[a],t)=>n.reviewUpdated(e,a,t.text)],"review record":[["pr"],["state","text","file"],(e,[a],t)=>{if(t.text===void 0==(t.file===void 0))throw new d("review record takes --text or --file");return n.recordReview(e,a,{state:t.state,text:t.text??m(t.file)})}],"review post":[["pr"],["file","read-at"],(e,[a],t)=>{if(t.file===void 0)throw new d("review post needs --file <review.md>");if(t["read-at"]===void 0)throw new d("review post needs --read-at <time>, when you read the PR, so a review that landed since is caught");return n.postIncomingReview(e,a,{words:m(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))]})],"reviewers set":[[],["file"],(e,a,t)=>n.setReviewers(e,R(t.file))],"reviewers add":[[],["file"],(e,a,t)=>n.addReviewers(e,R(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:[["file/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:x.basename(e),key:a}),signal:AbortSignal.timeout(1e4)}),l=await i.json();if(!i.ok)throw new n.BoardError(l.error||"The hub refused the build.");return{lines:["started",...(await n.appendMessage(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":[["file/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[a],{worktree:t,repo:s,dispatch:i,...l})=>{if(s!==void 0&&!t)throw new d("--repo goes with --worktree");const o=r(u=>z(e,a.split("/")[1],{repo:s,...u}),"make"),c=e&&l.dir===void 0&&ae(e);return n.startBuilder(e,a,{...l,dispatch:i?"session":void 0,worktree:t?o:void 0,ownWorktree:c?o:void 0})}],"builder progress":[["file/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[a,...t],s)=>{if(t.length&&s.text!==void 0)throw new d("builder progress takes its text once: as words or as --text");if(!t.length&&s.text===void 0)throw new d("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":[["file/id"],["dir","agent","next","owner-step","check","text","updated","title"],async(e,[a],{updated:t,...s})=>{const i=e&&!s.dir;if(i&&t===void 0&&await n.cardLinked(e,a))throw new n.BoardError(`a reviewer has ${a}'s PR link: pass --updated "<what changed>" for its thread, or --updated none for a change only to comments, names, docs or tests`);const l=await pe(e,a,s);return i&&t!==void 0&&t.trim().toLowerCase()!=="none"&&await n.reviewUpdated(e,a,t),l}],"builder pick":[["file/id"],[],(e,[a])=>n.pickBuilder(e,a)],"agent launch":[["file/id"],["name","task","writes","model"],(e,[a],t)=>n.requestHelper(e,a,t)],"agent wait":[["file/id"],["name","agent","timeout"],(e,[a],t)=>n.waitHelper(e,a,{...t,alive:Q})],"agent merge":[["file/id"],["name"],async(e,[a],t)=>{const{record:s,card:i}=await n.helperOf(e,a,t.name);if(!s.done)throw new n.BoardError(`helper ${t.name} is still open; run agent wait ${a} --name ${t.name} first`);if(!s.writes||!s.worktree)throw new n.BoardError(`helper ${t.name} writes no code, so there is nothing to merge`);if(!i)throw new n.BoardError(`${a} has no worktree to merge helper ${t.name} into`);return U(i,s.worktree,await n.cardName(e,a))}],audit:[["...file/id?"],[],(e,[a,...t])=>{if(t.length)throw new d("audit takes one <file/id> at most");return n.auditLog(e,a)}],"builder call":[["file/id","...text"],["dir","agent"],(e,[a,...t],s)=>n.recordCall(e,a,t.join(" "),s)],"builder calls":[["file/id"],["dir","agent"],(e,[a],t)=>n.listCalls(e,a,t)],"builder ask":[["file/id","...text"],["agent"],(e,[a,...t],s)=>n.askOrchestrator(e,a,t.join(" "),s)],"builder answer":[["file/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text)],"builder defer":[["file/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text,{deferred:!0})],"builder asks":[["file/id"],["wait","timeout"],(e,[a],t)=>n.listAsks(e,a,t)],"builder verified":[["file/id"],["dir","agent","note","title"],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.held)return{said:`${await n.cardName(e,a)} didn't land: ${s.held}`};if(s.waits.length)return{lines:[`${await n.cardName(e,a)} finishes once the owner answers ${s.waits.join(", ")}`]};try{return await $(e,a,{text:t.note??"Judgment calls verified.",title:t.title})}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:[["file/id"],["repo","dir","text","owner-step","check"],async(e,[a],t)=>{const s=a.split("/")[1];if(e&&t.text===void 0)throw new d(`land needs --text "<what was checked>", which closes ${a} as done once it lands`);if(!e&&t.text!==void 0)throw new d("--text closes the card, which needs --site");if(!e&&t["owner-step"]!==void 0)throw new d("--owner-step keeps a board card open, which needs --site");if(t.check!==void 0&&t["owner-step"]===void 0)throw new d("--check goes with --owner-step: it is the code check that closes the card once the step is done");if(e&&t.dir===void 0)return $(e,a,{text:t.text,ownerStep:t["owner-step"],check:t.check,repo:t.repo});const i=e?await n.cardName(e,a):void 0,l=await J(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 o=await n.recordedWorktree(e,a,{dir:t.dir}),{repo:c,base:u}=F(e,{repo:t.repo??o?.repo});return await G({path:o?.path??x.join(K(),s),branch:o?.branch??s,repo:x.resolve(c),base:u,ref:i}),e?n.setItem(e,a,{done:!0,text:t.text,ownerStep:t["owner-step"],check:t.check}):{lines:[]}});return l.joined?{lines:[`joined the land already running for ${i??a}`]}:l}],"builder stop":[["file/id"],[],(e,[a])=>ee(p(e,"builder stop"),a)],"item stall":[["file/id"],["reason"],(e,[a],t)=>Y(p(e,"item stall"),a,t.reason)],"land stop":[["file/id"],[],(e,[a])=>te(p(e,"land stop"),a)],"land queue":[[],[],()=>Z()],"item bump":[["file/id"],[],(e,[a])=>S(p(e,"item bump"),a,!0)],"item unbump":[["file/id"],[],(e,[a])=>S(p(e,"item unbump"),a,!1)],lander:[["file/id"],[],async(e,[a])=>{if(!e)throw new d("lander needs --site <route>");return await n.relaunchLander(e,a),{lines:[`${await n.cardName(e,a)} is handed back again; the server launches a lander for it within a minute`]}}],merge:[["file/id"],["after-approval","confirmed"],async(e,[a],t)=>e&&await n.heldMerge(e,a)?X(e,a):n.mergeCard(e,a,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["file/id"],[],(e,[a])=>e?V(e,a):Promise.reject(new n.BoardError("arch file needs --site <route>"))],push:[[],["kind","title","body","url","tag","urgent","now"],(e,a,t)=>{if(t.kind!=="fault"&&t.kind!=="reminder")throw new d(`a session pushes only --kind fault or reminder${t.kind===void 0||t.kind===!0?"":`, not "${t.kind}"`}: the board server pushes Action needed, and news goes on the board`);if(t.title===void 0)throw new d("push needs --title");return n.sendPush(e,t)}]};b.install=b.build;function p(e,a){if(!e)throw new d(`${a} needs --site <route>`);return e}r(p,"needSite");const h=r((e,a)=>n.cardName(e,a),"card"),q=r((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),N=r(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function we(e){const a=e["pick-up-at"];return[e.done&&(e["split-into"]?`done, split into ${e["split-into"].split(",").join(", ")}`:"done"),e.next!==void 0&&(e.next.trim()?"next line set":"next line cleared"),e["clear-next"]&&"next line cleared",e.pr!==void 0&&`PR #${e.pr}`,e.pushed!==void 0&&`pushed ${e.pushed}`,e["gh-since"]&&`GitHub read to ${e["gh-since"]}`,e["from-ask"]&&`from task ${e["from-ask"]}`,e.phase,e.held&&"held",e.unheld&&"resumed",a!==void 0&&(a?`held until ${a}`:"resumed"),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["new-ticket"]&&"ticket made",e.check!==void 0&&(e.check.trim()?`check ${e.check.trim().split(/\s+/).join(" ")}`:"check removed")].filter(Boolean).join(", ")}r(we,"itemChanges");const me={reply:r(async(e,[a])=>a.includes("/")?`answered ${await h(e,a)}: it waits in In Review for the owner's Done`:`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 l=[...new Set(s.map(c=>c.topic))],o=l.length!==1?"":l[0].startsWith("item:")?` on ${await h(e,l[0].slice(5))}`:` on topic ${l[0]}`;return`${q(s.map(c=>c.id),"question")} ${i===s.length?"staged":"added"}${o}`},"question add"),"question resolve":r((e,[a])=>`question ${a} resolved`,"question resolve"),"question settle":r((e,[a])=>`question ${a} settled`,"question settle"),"question publish":r(async(e,[a],t,{ids:s})=>`${q(s,"question")} on ${await h(e,a)} passed to the owner`,"question publish"),"question move":r((e,[a],t)=>`questions ${a} merged onto ${t.to}`,"question move"),"prototype add":r(async(e,[a],t)=>`prototype ${a} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":r((e,[a])=>`prototype ${a} removed`,"prototype rm"),"item set":r(async(e,[a],t)=>`${await h(e,a)}: ${we(t)}`,"item set"),"item add":r(async(e,a,t,{key:s})=>`${await h(e,s)} added as ${s}`,"item add"),"item notes":r(async(e,[a],t,{file:s,heading:i})=>`${await h(e,a)}: notes section "${i}" added to ${s}`,"item notes"),"item relate-ask":r(async(e,[a,t])=>`task ${a} folded into ${await h(e,t)}`,"item relate-ask"),"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"),"wave create":r((e,[a])=>`wave ${a} created`,"wave create"),"wave add":r(async(e,[a,t])=>`${await h(e,t)} added to wave ${a}`,"wave add"),"wave remove":r(async(e,[a,t])=>`${await h(e,t)} taken out of wave ${a}`,"wave remove"),"wave set":r((e,[a])=>`wave ${a} updated`,"wave set"),"wave finish":r((e,[a])=>`wave ${a} finished`,"wave finish"),"review record":r((e,[a],t)=>`review of PR #${a} recorded: ${t.state==="needs"?"needs you":"posted"}`,"review record"),"reviewers set":r(()=>"reviewers replaced","reviewers set"),"reviewers add":r(()=>"reviewers added","reviewers add"),"reviewers remove":r((e,a)=>`${q(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":`${N(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??`${N(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 fe(e,a={}){try{const{flags:t,rest:s}=oe(e),i=[3,2,1].map(w=>s.slice(0,w).join(" ")).find(w=>y(b,w)),l=y(b,i);if(!l)throw new d(s.length?`unknown command "${s.slice(0,2).join(" ")}"`:"no command given");const[o,c,u]=l,f=s.slice(i.split(" ").length),_=o.at(-1)?.startsWith("..."),B=o.at(-1)?.endsWith("?")?o.length-1:o.length;if(_?f.length<B:f.length!==o.length)throw new d(`${i} takes ${o.map(w=>`<${w.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:I,...E}=t;if(I?.length>1)throw new d("--site is given twice");const P=I?.[0],k=de(i,c,E);ue(c,k);const A=P===void 0?null:await n.siteDir(P),v=await u(A,f,k,a),O=y(me,i),j=O&&await O(A,f,k,v);j&&console.log(j);for(const w of typeof v=="object"&&v?.lines||[])console.log(w);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(r(fe,"main"),process.argv[1]&&import.meta.url===L(g.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(re):process.exitCode=await fe(e)}export{re as USAGE,fe as main};
702
+ KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class d extends Error{static{i(this,"UsageError")}}const re=new Set(["new-ticket","done","clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","wait","hub","clear-install","force","worktree","dispatch","writes"]);function oe(e){const a=Object.create(null),t=[];for(let s=0;s<e.length;s++){const r=e[s];if(!r.startsWith("--")){t.push(r);continue}const l=r.slice(2);if(re.has(l)){if(Object.hasOwn(a,l))throw new d(`--${l} is given twice`);a[l]=!0}else if(s+1<e.length)(a[l]||=[]).push(e[++s]);else throw new d(`--${l} needs a value`)}return{flags:a,rest:t}}i(oe,"parse");function de(e,a,t){const s=new Set(a.filter(o=>o.startsWith("...")).map(o=>o.slice(3))),r=new Set(a.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||s.has(o))return[o,c];if(c.length>1)throw new d(`--${o} is given twice`);return[o,c[0]]}))}i(de,"flagsOf");const le=["text","note","notes","heading","summary","title","body","next","progress","install-label","confirmed","asked","task","reason"],he=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function ce(e){const a=e.trim();if(!a||/\s/.test(a))return!1;if(he.test(a))return!0;try{return g.statSync(a).isFile()}catch{return!1}}i(ce,"looksLikePath");function ue(e,a){for(const t of le){if(typeof a[t]!="string"||!ce(a[t]))continue;const s=e.includes("file")?`; for a file use --file ${a[t].trim()}`:"";throw new d(`--${t} takes text, not a file path (${a[t].trim()})${s}`)}if(typeof a.file=="string"&&a.file!=="-"&&!g.existsSync(a.file))throw new d(`--file ${a.file}: no such file`)}i(ue,"checkInputs");const m=i(e=>g.readFileSync(e==="-"?0:e,"utf8"),"readInput");function R(e){if(!e)throw new d("--file <path> is required");try{return JSON.parse(m(e))}catch(a){throw new n.BoardError(`${e}: ${a.message}`)}}i(R,"readJSONInput");function C(e,a){const t=["notes-file","plan"].find(s=>e[s]!==void 0);if(t)throw new d(`--${t} is gone: the board names a card's notes file, <id>-notes.md, and writes it itself; ${a}`)}i(C,"refuseNotesFile");function T(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:m(e.file)}i(T,"noteOf");async function pe(e,a,{"owner-step":t,check:s,text:r,title:l,...o}){if(t!==void 0&&(!e||o.dir))throw new d("--owner-step goes on a board card: pass --site, not --dir");if(s!==void 0&&t===void 0)throw new d("--check goes with --owner-step: it is the code check that closes the card once the step is done");t!==void 0&&await n.setItem(e,a,{ownerStep:t,check:s});const c=e&&!o.dir,p=c&&await n.handBackCalls(e,a,o);if(p)return p;if(c&&o.next===void 0&&await n.finishesCard(e,a,o)){if(r===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 $(e,a,{text:r,ownerStep:t,title:l})}if(c&&await n.isHelper(e,a,o.agent)){if(r===void 0)throw new d('a helper hands back its result: pass --text "<what you did or found>"');return n.stopBuilder(e,a,{...o,note:r})}if(c&&r!==void 0&&await n.isPlanner(e,a,o.agent))return n.stopPlanner(e,a,{...o,note:r,requireNext:!0});if(r!==void 0)throw new d("--text goes on a builder done that finishes the card, the main build with a worktree and no --next, or on a helper's or a planner's");return n.stopBuilder(e,a,{...o,requireNext:!0})}i(pe,"builderDone");const b={pr:[["file/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 d('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 d('pr reply needs --body "<what Claude says>"');return n.postPR(e,a,{replyTo:t,body:s.body})}],reply:[["eventId"],["text","file","title"],(e,[a],t)=>{if(t.text===void 0==(t.file===void 0))throw new d("reply takes --text or --file");const s=t.text??m(t.file);return a.includes("/")?n.answerCard(e,a,s,t.title):n.writeReply(e,a,s,t.title)}],thread:[["file/id"],["text","link","who","via"],(e,[a],t)=>n.appendMessage(e,a,t)],"visual add":[["file/id"],["file","title"],(e,[a],t)=>n.addVisual(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=R(t.file);return{...await n.addQuestions(e,s,process.env.KICKFLIP_RENDER_CHECK==="off"?void 0:se),asked:[s].flat()}}],"question resolve":[["id"],["note"],(e,[a],t)=>n.resolveQuestion(e,a,t.note)],"question settle":[["id"],["note"],(e,[a],t)=>n.settleQuestion(e,a,t.note)],"question publish":[["key"],[],async(e,[a])=>({ids:await n.publishScreened(e,a)})],"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","served","...state"],(e,[a],t)=>n.addPrototype(e,{topic:a,title:t.title,card:t.card,takes:t.take,served:t.served,states:t.state})],"prototype base":[[],[],e=>ne(e)],"prototype list":[[],[],e=>n.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[a])=>n.removePrototype(e,a)],"item set":[["file/id"],["done","split-into","status","clear-status","next","clear-next","...pr","pushed","gh-since","from-ask","phase","state","held","unheld","pick-up-at","notes-file","plan","deps","blocked-until","block-reason","parent","related","relate","unrelate","install","install-sha","install-label","clear-install","title","size","model","force","ticket","new-ticket","text","check"],async(e,[a],t,s)=>(C(t,`add to its notes with item notes ${a} --heading "<heading>" --file <draft.md>`),t.done&&e&&await n.checkNotReopened(e,a),n.setItem(e,a,{done:t.done,splitInto:t["split-into"],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"],phase:t.phase,state:t.state,held:t.held,unheld:t.unheld,pickUpAt:t["pick-up-at"],deps:t.deps,blockedUntil:t["blocked-until"],blockReason:t["block-reason"],parent:t.parent,related:t.related,relate:t.relate,unrelate:t.unrelate,install:t.install,installSha:t["install-sha"],installLabel:t["install-label"],clearInstall:t["clear-install"],title:t.title,size:t.size,model:t.model,force:t.force,ticket:t.ticket,newTicket:t["new-ticket"],text:t.text,check:t.check},s))],"item add":[["file[/id]"],["title","notes","notes-from","deps","notes-file","plan","ticket","new-ticket","pick-up-at","held","parent","split-of","size","model","phase","state","relate","from-ask","text"],(e,[a],t,s)=>{if(C(t,"seed a new card's notes with item add --notes-from <draft.md>"),t.notes!==void 0&&t["notes-from"]!==void 0)throw new d("pass --notes or --notes-from, not both");return n.addItem(e,a,{title:t.title,notes:t.notes,notesFrom:t["notes-from"],deps:t.deps,ticket:t.ticket,newTicket:t["new-ticket"],pickUpAt:t["pick-up-at"],held:t.held,parent:t.parent,splitOf:t["split-of"],size:t.size,model:t.model,phase:t.phase,state:t.state,relate:t.relate,fromAsk:t["from-ask"],text:t.text,intake:!0},s)}],"item notes":[["file/id"],["heading","file","from"],(e,[a],t)=>{if(t.heading===void 0&&t.from===void 0)throw new d(`item notes needs --heading "<the section's title>"`);if(t.file===void 0==(t.from===void 0))throw new d("item notes takes --file <draft.md> or --from <file>/<id>");return n.addNotes(e,a,t)}],"item relate-ask":[["eventId","file/id"],["summary","title"],(e,[a,t],s)=>n.relateAsk(e,a,t,s.summary,s.title)],"ask unfold":[["eventId"],[],(e,[a])=>n.unfoldAsk(e,a)],"ask move":[["eventId"],["to"],(e,[a],t)=>n.moveAsk(e,a,t.to)],"wave create":[["slug"],["title","goal","...card","note","file"],(e,[a],t)=>n.createWave(e,a,{title:t.title,goal:t.goal,cards:t.card,note:T(t)})],"wave add":[["slug","file/id"],["after"],(e,[a,t],s)=>n.changeWave(e,a,{add:t,after:s.after})],"wave remove":[["slug","file/id"],[],(e,[a,t])=>n.changeWave(e,a,{remove:t})],"wave set":[["slug","...file/id?"],["after","title","goal","note","file"],(e,[a,...t],s)=>{if(t.length>1)throw new d("wave set takes <slug> [<file/id>]");return n.changeWave(e,a,{card:t[0],after:s.after,title:s.title,goal:s.goal,note:T(s)})}],"wave finish":[["slug"],[],(e,[a])=>n.finishWave(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 M(Object.fromEntries(Object.entries(t).map(([r,l])=>[s[r],l]))))]}}catch(r){throw new n.BoardError(r.message)}}],"slack thread":[["eventId"],[],(e,[a])=>n.slackThread(u(e,"slack thread"),a)],"slack answer":[["eventId"],["text"],(e,[a],t)=>n.slackAnswer(u(e,"slack answer"),a,t.text)],"slack say":[["reviewer"],["text"],(e,[a],t)=>n.slackSay(u(e,"slack say"),a,t.text)],"slack task":[["eventId","file"],["title","notes","notes-from"],(e,[a,t],s)=>{if(s.notes!==void 0&&s["notes-from"]!==void 0)throw new d("pass --notes or --notes-from, not both");return n.slackTask(u(e,"slack task"),a,t,{title:s.title,notes:s.notes,notesFrom:s["notes-from"]})}],integrations:[[],[],async()=>({lines:[JSON.stringify(await W.rows())]})],"review record":[["pr"],["state","text","file"],(e,[a],t)=>{if(t.text===void 0==(t.file===void 0))throw new d("review record takes --text or --file");return n.recordReview(e,a,{state:t.state,text:t.text??m(t.file)})}],"review post":[["pr"],["file","read-at"],(e,[a],t)=>{if(t.file===void 0)throw new d("review post needs --file <review.md>");if(t["read-at"]===void 0)throw new d("review post needs --read-at <time>, when you read the PR, so a review that landed since is caught");return n.postIncomingReview(e,a,{words:m(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))]})],"reviewers set":[[],["file"],(e,a,t)=>n.setReviewers(e,R(t.file))],"reviewers add":[[],["file"],(e,a,t)=>n.addReviewers(e,R(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:[["file/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 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:x.basename(e),key:a}),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.appendMessage(e,a,{text:s})).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,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":[["file/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[a],{worktree:t,repo:s,dispatch:r,...l})=>{if(s!==void 0&&!t)throw new d("--repo goes with --worktree");const o=i(p=>z(e,a.split("/")[1],{repo:s,...p}),"make"),c=e&&l.dir===void 0&&ae(e);return n.startBuilder(e,a,{...l,dispatch:r?"session":void 0,worktree:t?o:void 0,ownWorktree:c?o:void 0})}],"builder progress":[["file/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[a,...t],s)=>{if(t.length&&s.text!==void 0)throw new d("builder progress takes its text once: as words or as --text");if(!t.length&&s.text===void 0)throw new d("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":[["file/id"],["dir","agent","next","owner-step","check","text","title"],(e,[a],t)=>pe(e,a,t)],"builder pick":[["file/id"],[],(e,[a])=>n.pickBuilder(e,a)],"agent launch":[["file/id"],["name","task","writes","model"],(e,[a],t)=>n.requestHelper(e,a,t)],"agent wait":[["file/id"],["name","agent","timeout"],(e,[a],t)=>n.waitHelper(e,a,{...t,alive:Q})],"agent merge":[["file/id"],["name"],async(e,[a],t)=>{const{record:s,card:r}=await n.helperOf(e,a,t.name);if(!s.done)throw new n.BoardError(`helper ${t.name} is still open; run agent wait ${a} --name ${t.name} first`);if(!s.writes||!s.worktree)throw new n.BoardError(`helper ${t.name} writes no code, so there is nothing to merge`);if(!r)throw new n.BoardError(`${a} has no worktree to merge helper ${t.name} into`);return K(r,s.worktree,await n.cardName(e,a))}],audit:[["...file/id?"],[],(e,[a,...t])=>{if(t.length)throw new d("audit takes one <file/id> at most");return n.auditLog(e,a)}],"builder call":[["file/id","...text"],["dir","agent"],(e,[a,...t],s)=>n.recordCall(e,a,t.join(" "),s)],"builder calls":[["file/id"],["dir","agent"],(e,[a],t)=>n.listCalls(e,a,t)],"builder ask":[["file/id","...text"],["agent"],(e,[a,...t],s)=>n.askOrchestrator(e,a,t.join(" "),s)],"builder answer":[["file/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text)],"builder defer":[["file/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text,{deferred:!0})],"builder asks":[["file/id"],["wait","timeout"],(e,[a],t)=>n.listAsks(e,a,t)],"builder verified":[["file/id"],["dir","agent","note","title"],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.held)return{said:`${await n.cardName(e,a)} didn't land: ${s.held}`};if(s.waits.length)return{lines:[`${await n.cardName(e,a)} finishes once the owner answers ${s.waits.join(", ")}`]};try{return await $(e,a,{text:t.note??"Judgment calls verified.",title:t.title})}catch(r){throw r instanceof n.BoardError?Object.assign(new n.BoardError(`judgment calls verified, but ${await n.cardName(e,a)} didn't finish: ${r.message}`),{exitCode:r.exitCode}):r}}],land:[["file/id"],["repo","dir","text","owner-step","check"],async(e,[a],t)=>{const s=a.split("/")[1];if(e&&t.text===void 0)throw new d(`land needs --text "<what was checked>", which closes ${a} as done once it lands`);if(!e&&t.text!==void 0)throw new d("--text closes the card, which needs --site");if(!e&&t["owner-step"]!==void 0)throw new d("--owner-step keeps a board card open, which needs --site");if(t.check!==void 0&&t["owner-step"]===void 0)throw new d("--check goes with --owner-step: it is the code check that closes the card once the step is done");if(e&&t.dir===void 0)return $(e,a,{text:t.text,ownerStep:t["owner-step"],check:t.check,repo:t.repo});const r=e?await n.cardName(e,a):void 0,l=await J(r??a,async()=>{e&&await n.checkClose(e,a,t.text,t["owner-step"],t.check),await n.checkCallsVerified(e,a,{dir:t.dir});const o=await n.recordedWorktree(e,a,{dir:t.dir}),{repo:c,base:p}=F(e,{repo:t.repo??o?.repo});return await G({path:o?.path??x.join(U(),s),branch:o?.branch??s,repo:x.resolve(c),base:p,ref:r}),e?n.setItem(e,a,{done:!0,text:t.text,ownerStep:t["owner-step"],check:t.check}):{lines:[]}});return l.joined?{lines:[`joined the land already running for ${r??a}`]}:l}],"builder stop":[["file/id"],[],(e,[a])=>ee(u(e,"builder stop"),a)],"item stall":[["file/id"],["reason"],(e,[a],t)=>Y(u(e,"item stall"),a,t.reason)],"land stop":[["file/id"],[],(e,[a])=>te(u(e,"land stop"),a)],"land queue":[[],[],()=>Z()],"item bump":[["file/id"],[],(e,[a])=>S(u(e,"item bump"),a,!0)],"item unbump":[["file/id"],[],(e,[a])=>S(u(e,"item unbump"),a,!1)],lander:[["file/id"],[],async(e,[a])=>{if(!e)throw new d("lander needs --site <route>");return await n.relaunchLander(e,a),{lines:[`${await n.cardName(e,a)} is handed back again; the server launches a lander for it within a minute`]}}],merge:[["file/id"],["after-approval","confirmed"],async(e,[a],t)=>e&&await n.heldMerge(e,a)?X(e,a):n.mergeCard(e,a,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["file/id"],[],(e,[a])=>e?V(e,a):Promise.reject(new n.BoardError("arch file needs --site <route>"))],push:[[],["kind","title","body","url","tag","urgent","now"],(e,a,t)=>{if(t.kind!=="fault"&&t.kind!=="reminder")throw new d(`a session pushes only --kind fault or reminder${t.kind===void 0||t.kind===!0?"":`, not "${t.kind}"`}: the board server pushes Action needed, and news goes on the board`);if(t.title===void 0)throw new d("push needs --title");return n.sendPush(e,t)}]};b.install=b.build;function u(e,a){if(!e)throw new d(`${a} needs --site <route>`);return e}i(u,"needSite");const h=i((e,a)=>n.cardName(e,a),"card"),q=i((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),N=i(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function we(e){const a=e["pick-up-at"];return[e.done&&(e["split-into"]?`done, split into ${e["split-into"].split(",").join(", ")}`:"done"),e.next!==void 0&&(e.next.trim()?"next line set":"next line cleared"),e["clear-next"]&&"next line cleared",e.pr!==void 0&&`PR #${e.pr}`,e.pushed!==void 0&&`pushed ${e.pushed}`,e["gh-since"]&&`GitHub read to ${e["gh-since"]}`,e["from-ask"]&&`from task ${e["from-ask"]}`,e.phase,e.held&&"held",e.unheld&&"resumed",a!==void 0&&(a?`held until ${a}`:"resumed"),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["new-ticket"]&&"ticket made",e.check!==void 0&&(e.check.trim()?`check ${e.check.trim().split(/\s+/).join(" ")}`:"check removed")].filter(Boolean).join(", ")}i(we,"itemChanges");const me={reply:i(async(e,[a])=>a.includes("/")?`answered ${await h(e,a)}: it waits in In Review for the owner's Done`:`reply written to ${await n.eventCard(e,a)}`,"reply"),thread:i(async(e,[a])=>`thread line on ${await h(e,a)}`,"thread"),"topic add":i((e,[a])=>`topic ${a} added`,"topic add"),"question add":i(async(e,a,t,{asked:s,staged:r})=>{const l=[...new Set(s.map(c=>c.topic))],o=l.length!==1?"":l[0].startsWith("item:")?` on ${await h(e,l[0].slice(5))}`:` on topic ${l[0]}`;return`${q(s.map(c=>c.id),"question")} ${r===s.length?"staged":"added"}${o}`},"question add"),"question resolve":i((e,[a])=>`question ${a} resolved`,"question resolve"),"question settle":i((e,[a])=>`question ${a} settled`,"question settle"),"question publish":i(async(e,[a],t,{ids:s})=>`${q(s,"question")} on ${await h(e,a)} passed to the owner`,"question publish"),"question move":i((e,[a],t)=>`questions ${a} merged onto ${t.to}`,"question move"),"prototype add":i(async(e,[a],t)=>`prototype ${a} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":i((e,[a])=>`prototype ${a} removed`,"prototype rm"),"item set":i(async(e,[a],t)=>`${await h(e,a)}: ${we(t)}`,"item set"),"item add":i(async(e,a,t,{key:s})=>`${await h(e,s)} added as ${s}`,"item add"),"item notes":i(async(e,[a],t,{file:s,heading:r})=>`${await h(e,a)}: notes section "${r}" added to ${s}`,"item notes"),"item relate-ask":i(async(e,[a,t])=>`task ${a} folded into ${await h(e,t)}`,"item relate-ask"),"ask unfold":i((e,[a])=>`task ${a} back on a card of its own`,"ask unfold"),"ask move":i((e,[a],t)=>`task ${a} moved to /${t.to}`,"ask move"),"wave create":i((e,[a])=>`wave ${a} created`,"wave create"),"wave add":i(async(e,[a,t])=>`${await h(e,t)} added to wave ${a}`,"wave add"),"wave remove":i(async(e,[a,t])=>`${await h(e,t)} taken out of wave ${a}`,"wave remove"),"wave set":i((e,[a])=>`wave ${a} updated`,"wave set"),"wave finish":i((e,[a])=>`wave ${a} finished`,"wave finish"),"review record":i((e,[a],t)=>`review of PR #${a} 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,a)=>`${q(a,"reviewer")} removed`,"reviewers remove"),"rules add":i((e,a,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":i((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} reworded`,"rules edit"),"rules remove":i((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} removed`,"rules remove"),"builder start":i(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":`${N(t)} started`}`,"builder start"),"builder progress":i(async(e,[a])=>`progress line on ${await h(e,a)}`,"builder progress"),"builder done":i(async(e,[a],t,s)=>`${await h(e,a)}: ${s?.said??`${N(t)} done${t["owner-step"]===void 0?"":`; owner step "${t["owner-step"].trim()}"`}`}`,"builder done"),"builder call":i(async(e,[a])=>`judgment call recorded on ${await h(e,a)}`,"builder call"),"builder ask":i(async(e,[a],t,{id:s})=>`${await h(e,a)}: asked the orchestrator, ${s}`,"builder ask"),"builder answer":i(async(e,[a,t])=>`${await h(e,a)}: ${t} answered`,"builder answer"),"builder defer":i(async(e,[a,t])=>`${await h(e,a)}: ${t} deferred to the owner`,"builder defer"),"builder verified":i(async(e,[a],t,s)=>`${await h(e,a)}: judgment calls verified${s?.said?`; ${s.said}`:""}`,"builder verified"),land:i(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:i(async(e,[a],t,s)=>`${await h(e,a)}: ${s?.said??"merged"}`,"merge"),"arch file":i(async(e,[a])=>`findings of ${await h(e,a)} filed`,"arch file")};async function fe(e,a={}){try{const{flags:t,rest:s}=oe(e),r=[3,2,1].map(w=>s.slice(0,w).join(" ")).find(w=>y(b,w)),l=y(b,r);if(!l)throw new d(s.length?`unknown command "${s.slice(0,2).join(" ")}"`:"no command given");const[o,c,p]=l,f=s.slice(r.split(" ").length),_=o.at(-1)?.startsWith("..."),B=o.at(-1)?.endsWith("?")?o.length-1:o.length;if(_?f.length<B:f.length!==o.length)throw new d(`${r} takes ${o.map(w=>`<${w.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:I,...E}=t;if(I?.length>1)throw new d("--site is given twice");const A=I?.[0],k=de(r,c,E);ue(c,k);const P=A===void 0?null:await n.siteDir(A),v=await p(P,f,k,a),O=y(me,r),j=O&&await O(P,f,k,v);j&&console.log(j);for(const w of typeof v=="object"&&v?.lines||[])console.log(w);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(fe,"main"),process.argv[1]&&import.meta.url===L(g.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(ie):process.exitCode=await fe(e)}export{ie as USAGE,fe as main};
@@ -1,8 +1,8 @@
1
- var X=Object.defineProperty;var l=(t,e)=>X(t,"name",{value:e,configurable:!0});import J from"node:os";import{BRIEF_LINE_LIMIT as Z}from"./hub-lines.mjs";import T from"node:path";import{fileURLToPath as ee}from"node:url";import{launchAgent as W}from"./agents.mjs";import{processStarts as D,ps as M}from"../hub/live.mjs";import{boardAt as O}from"../hub/boards.mjs";import{readHubRules as te}from"../hub/rules.mjs";import{builderRepo as F}from"./worktree.mjs";import{standardWords as L}from"./commit-message.mjs";import{appendStatus as oe,BoardError as re,briefFacts as B,readBoardRules as ne,recordedSession as se,recordSession as E}from"./store.mjs";import{HELPER as ae,plans as ie,VISUAL as he}from"./domain.mjs";const de=ee(new URL("./bin/board.mjs",import.meta.url)),$=l(t=>`node ${de} --site ${t}`,"cli"),Y=l(t=>`git -c core.commentChar=';' rebase ${t}`,"rebase"),le="a draft .md of your own outside the board's data folder (such as in /tmp)",ce=l((t,e,r)=>`\`${t} item notes ${e} --heading "${r}" --file <draft.md>\``,"notesCommand"),I=l((t,e="Rules from the hub and this board:")=>t.length?[e,...t.map(r=>`- ${r}`),""]:[],"ruleLines"),q=l(t=>t.length?["Related cards: read their notes for context; don't build their scope.",...t.map(e=>`- ${e.ref} "${e.title}" (${e.relation}, ${e.lane}): ${e.notes?`notes ${e.notes}`:"no notes"}, thread ${e.thread}`),""]:[],"linkLines"),S=l(t=>`The card's thread, which holds the owner's messages (who: love): ${t}. Read it live.`,"threadLine"),H=l(({route:t,key:e,worktree:r,base:i})=>[`- With no unverified calls, done finishes the card itself: it rebases onto ${i}, reruns the full test suite, fast-forwards ${i} and closes the card (on a board with pull requests it pushes the branch and opens a draft PR instead, titled by \`--title "<type>(<scope>): <what the PR does>"\`, a Conventional subject of at most 72 characters written from the whole diff, else the branch's first commit subject; with Auto-merge off it stops before the fast-forward, and the card waits for the owner's Merge).`,`- On a conflict it undoes its rebase and names the files: run \`${Y(i)}\` in ${r}, resolve each conflict keeping both sides' intent, continue with \`git -c core.commentChar=';' rebase --continue\`, rerun the tests, and run done again.`,"- On failing tests, fix them, commit, and run done again.","- While another card is landing, done waits its turn and then lands; leave it running.","- Once a reviewer has the card's PR link, done refuses without `--updated \"<what changed>\"`, which goes in that link's Slack thread, or `--updated none` for a change only to comments, names, docs or tests.",`- Finishing never goes to the orchestrator. If done refuses for a reason you can't fix, record it with \`${$(t)} builder call ${e} "<why>"\` and run done again: with the call it hands the card back.`],"finishSteps"),j=l(({route:t,key:e})=>`\`${$(t)} builder done ${e} --text "<what you checked>"\``,"finishCommand"),ue=l((t,e)=>[`5. To run a separate part of the work in parallel, ask the board for a helper: \`${t} agent launch ${e} --name <short-name> --task "<the whole task, with the files and what to hand back>"\`, adding --writes when it changes code or runs tests (it gets its own worktree off your branch, and waits for a free implementer slot). Then \`${t} agent wait ${e} --name <short-name>\`, which prints its hand-back; it returns within 10 minutes, so run it again while it says still running, and do the task yourself if it says the helper exited. Merge a writing helper's work with \`${t} agent merge ${e} --name <short-name>\` before you test and finish.`],"helperSteps");function pe({route:t,key:e,ref:r,title:i,notes:n,thread:a,worktree:h,base:u,rules:p=[],links:c=[]}){const d=$(t);return[`You are the headless builder for card ${r} (${e}, "${i}") on the kickflip board /${t}. The board launched you; nobody watches this session, so never wait on a person. Work only in ${h}. Never restart the hub or a board server, and never push.`,"",`1. Run \`${d} builder start ${e} --kind build\`. If it refuses, stop.`,`2. Report progress with \`${d} builder progress ${e} --phase reading|building|testing|checking|landing "<plain, 80 characters or fewer>"\`, at least every 10 minutes. The board adds the phase word; don't start the text with it. The phases:`," - reading: the notes and the code, until the first test is written."," - building: the tests and the code, the ADR and docs, and the commit."," - testing: the full suite run before you finish."," - checking: your own review of the diff before you hand back; the Orchestrator's review of your calls continues on the same 4th bar."," - landing: set by the finish step itself; you never report it.",`3. Decide, record the call (step 4) and carry on: the orchestrator reviews your calls after the build. Ask it only when you are blocked, meaning every way forward would likely be thrown away: the spec contradicts itself or the code, a rule needs the owner's go before code is written, or the answer is a fact only the orchestrator has. Then run \`${d} builder ask ${e} "<question>"\`, do any work the answer can't change, and wait with \`${d} builder asks ${e} --wait\`, which returns within 10 minutes, so run it again while it says still waiting. If no answer comes in 30 minutes, record the question as a call, "Question: <question>", and hand back.`,`4. Record each judgment call the notes leave open with \`${d} builder call ${e} "<call>"\` instead of guessing silently or asking. A problem you find outside your card is a call "Found: <what>" for the orchestrator to file at review; ask about it at once only when it stalls other cards' builds or lands.`,...ue(d,e),"",n?`Spec: the card's notes, ${n}, all sections, where later sections override earlier ones.`:"The card has no notes file: build from its title, and record what you had to decide as calls.",`${S(a)} Read it with the notes: a message of the owner's is an instruction, but where it contradicts the notes, the notes win; that contradiction counts as blocked (step 3), so ask with \`${d} builder ask ${e} "<question>"\` and wait as step 3 says. Read the thread again in checking, before you finish, for anything the owner wrote during the build.`,"",...q(c),...I(p),"Write tests first, and run the full suite once, in the foreground, at the end. Where the repo keeps ADRs, write one named with a timestamp: docs/adr/YYYY-MM-DD-HHMM-<slug>.md.",`Commit with \`git -c core.commentChar=';' commit -F <file>\`: ${L(r)}. Land refuses any other shape.`,"",`To finish, run ${j({route:t,key:e})}.`,"- If you recorded a judgment call, it hands the card back for the orchestrator to verify your calls; stop there: the orchestrator's verification finishes the card.",...H({route:t,key:e,worktree:h,base:u}),"","Never push."].join(`
1
+ var X=Object.defineProperty;var l=(t,e)=>X(t,"name",{value:e,configurable:!0});import J from"node:os";import{BRIEF_LINE_LIMIT as Z}from"./hub-lines.mjs";import T from"node:path";import{fileURLToPath as ee}from"node:url";import{launchAgent as W}from"./agents.mjs";import{processStarts as D,ps as M}from"../hub/live.mjs";import{boardAt as F}from"../hub/boards.mjs";import{readHubRules as te}from"../hub/rules.mjs";import{builderRepo as O}from"./worktree.mjs";import{standardWords as L}from"./commit-message.mjs";import{appendStatus as oe,BoardError as re,briefFacts as B,readBoardRules as ne,recordedSession as se,recordSession as E}from"./store.mjs";import{HELPER as ae,plans as ie,VISUAL as he}from"./domain.mjs";const de=ee(new URL("./bin/board.mjs",import.meta.url)),$=l(t=>`node ${de} --site ${t}`,"cli"),Y=l(t=>`git -c core.commentChar=';' rebase ${t}`,"rebase"),le="a draft .md of your own outside the board's data folder (such as in /tmp)",ce=l((t,e,r)=>`\`${t} item notes ${e} --heading "${r}" --file <draft.md>\``,"notesCommand"),I=l((t,e="Rules from the hub and this board:")=>t.length?[e,...t.map(r=>`- ${r}`),""]:[],"ruleLines"),q=l(t=>t.length?["Related cards: read their notes for context; don't build their scope.",...t.map(e=>`- ${e.ref} "${e.title}" (${e.relation}, ${e.lane}): ${e.notes?`notes ${e.notes}`:"no notes"}, thread ${e.thread}`),""]:[],"linkLines"),S=l(t=>`The card's thread, which holds the owner's messages (who: love): ${t}. Read it live.`,"threadLine"),H=l(({route:t,key:e,worktree:r,base:i})=>[`- With no unverified calls, done finishes the card itself: it rebases onto ${i}, reruns the full test suite, fast-forwards ${i} and closes the card (on a board with pull requests it pushes the branch and opens a draft PR instead, titled by \`--title "<type>(<scope>): <what the PR does>"\`, a Conventional subject of at most 72 characters written from the whole diff, else the branch's first commit subject; with Auto-merge off it stops before the fast-forward, and the card waits for the owner's Merge).`,`- On a conflict it undoes its rebase and names the files: run \`${Y(i)}\` in ${r}, resolve each conflict keeping both sides' intent, continue with \`git -c core.commentChar=';' rebase --continue\`, rerun the tests, and run done again.`,"- On failing tests, fix them, commit, and run done again.","- While another card is landing, done waits its turn and then lands; leave it running.",`- Finishing never goes to the orchestrator. If done refuses for a reason you can't fix, record it with \`${$(t)} builder call ${e} "<why>"\` and run done again: with the call it hands the card back.`],"finishSteps"),j=l(({route:t,key:e})=>`\`${$(t)} builder done ${e} --text "<what you checked>"\``,"finishCommand"),ue=l((t,e)=>[`5. To run a separate part of the work in parallel, ask the board for a helper: \`${t} agent launch ${e} --name <short-name> --task "<the whole task, with the files and what to hand back>"\`, adding --writes when it changes code or runs tests (it gets its own worktree off your branch, and waits for a free implementer slot). Then \`${t} agent wait ${e} --name <short-name>\`, which prints its hand-back; it returns within 10 minutes, so run it again while it says still running, and do the task yourself if it says the helper exited. Merge a writing helper's work with \`${t} agent merge ${e} --name <short-name>\` before you test and finish.`],"helperSteps");function pe({route:t,key:e,ref:r,title:i,notes:n,thread:a,worktree:h,base:u,rules:p=[],links:c=[]}){const d=$(t);return[`You are the headless builder for card ${r} (${e}, "${i}") on the kickflip board /${t}. The board launched you; nobody watches this session, so never wait on a person. Work only in ${h}. Never restart the hub or a board server, and never push.`,"",`1. Run \`${d} builder start ${e} --kind build\`. If it refuses, stop.`,`2. Report progress with \`${d} builder progress ${e} --phase reading|building|testing|checking|landing "<plain, 80 characters or fewer>"\`, at least every 10 minutes. The board adds the phase word; don't start the text with it. The phases:`," - reading: the notes and the code, until the first test is written."," - building: the tests and the code, the ADR and docs, and the commit."," - testing: the full suite run before you finish."," - checking: your own review of the diff before you hand back; the Orchestrator's review of your calls continues on the same 4th bar."," - landing: set by the finish step itself; you never report it.",`3. Decide, record the call (step 4) and carry on: the orchestrator reviews your calls after the build. Ask it only when you are blocked, meaning every way forward would likely be thrown away: the spec contradicts itself or the code, a rule needs the owner's go before code is written, or the answer is a fact only the orchestrator has. Then run \`${d} builder ask ${e} "<question>"\`, do any work the answer can't change, and wait with \`${d} builder asks ${e} --wait\`, which returns within 10 minutes, so run it again while it says still waiting. If no answer comes in 30 minutes, record the question as a call, "Question: <question>", and hand back.`,`4. Record each judgment call the notes leave open with \`${d} builder call ${e} "<call>"\` instead of guessing silently or asking. A problem you find outside your card is a call "Found: <what>" for the orchestrator to file at review; ask about it at once only when it stalls other cards' builds or lands.`,...ue(d,e),"",n?`Spec: the card's notes, ${n}, all sections, where later sections override earlier ones.`:"The card has no notes file: build from its title, and record what you had to decide as calls.",`${S(a)} Read it with the notes: a message of the owner's is an instruction, but where it contradicts the notes, the notes win; that contradiction counts as blocked (step 3), so ask with \`${d} builder ask ${e} "<question>"\` and wait as step 3 says. Read the thread again in checking, before you finish, for anything the owner wrote during the build.`,"",...q(c),...I(p),"Write tests first, and run the full suite once, in the foreground, at the end. Where the repo keeps ADRs, write one named with a timestamp: docs/adr/YYYY-MM-DD-HHMM-<slug>.md.",`Commit with \`git -c core.commentChar=';' commit -F <file>\`: ${L(r)}. Land refuses any other shape.`,"",`To finish, run ${j({route:t,key:e})}.`,"- If you recorded a judgment call, it hands the card back for the orchestrator to verify your calls; stop there: the orchestrator's verification finishes the card.",...H({route:t,key:e,worktree:h,base:u}),"","Never push."].join(`
2
2
  `)}l(pe,"builderBrief");function K({route:t,key:e,ref:r,title:i,thread:n,worktree:a,base:h,answers:u=[],resumed:p=!1,why:c}){const d=$(t);return[p?`The board resumes you, the builder of card ${r} (${e}, "${i}") on the kickflip board /${t}, to land your build: ${c??"the orchestrator verified your judgment calls"}, and it waits only to land. Nobody watches this session, so never wait on a person. Work only in ${a}, never restart the hub or a board server, and never push.`:`You are the headless lander for card ${r} (${e}, "${i}") on the kickflip board /${t}. Its build is done and the orchestrator verified its judgment calls; it waits only to land. Nobody watches this session, so never wait on a person. Work only in ${a}, never restart the hub or a board server, and never push.`,"",`1. Run \`${d} builder start ${e} --kind build --label Lander --phase landing --progress "taking up the verified build"\`. If it refuses, stop.`,`2. Report progress with \`${d} builder progress ${e} --phase landing "<plain, 80 characters or fewer>"\`. The board adds the phase word; don't start the text with it.`,...u.length?["3. The owner answered the builder's deferred asks. Where an answer changes the build, change it, with its tests, and commit:",...u.map(o=>`- ${o.ask} "${o.question}": ${o.answer}`)]:[],"",S(n),`Commit with \`git -c core.commentChar=';' commit -F <file>\`: ${L(r)}. Land refuses any other shape, so reword a commit the build left in another.`,"",`To finish, run ${j({route:t,key:e})}.`,...H({route:t,key:e,worktree:a,base:h}),"","Never push."].join(`
3
3
  `)}l(K,"landerBrief");function _({route:t,key:e,ref:r,title:i,thread:n,worktree:a,base:h,pr:u,resumed:p=!1}){const c=$(t);return[...p?[`The board resumes you, the builder of card ${r}, for this run: the lines below are your brief for it.`]:[],`You are the headless rebase run for card ${r} (${e}, "${i}") on the kickflip board /${t}. Its PR #${u} conflicts with ${h}; rebase its branch onto ${h} so the PR is clean again. Nobody watches this session, so never wait on a person. Work only in ${a}, never restart the hub or a board server, and never push.`,"",`1. Run \`${c} builder start ${e} --kind build --label Rebase --phase building --progress "Rebase run started \xB7 fetching ${h}"\`. If it refuses, stop.`,`2. Run \`git fetch origin\`, then \`${Y(`origin/${h}`)}\`. Resolve each conflict keeping both sides' intent, \`git add\` it and continue with \`git -c core.commentChar=';' rebase --continue\`. Report each step with \`${c} builder progress ${e} --phase building "<line>"\`, the board adds the phase word, in these words: "Rebasing onto ${h} \xB7 resolving <n> files", then "Rebasing onto ${h} \xB7 suite running".`,"3. Run the full test suite once, in the foreground. Fix what the rebase broke, and commit each fix with `git -c core.commentChar=';' commit -F <file>`. A push with commits of its own is no longer rebase-only, so a merge after an approval won't count it as trivial.",`4. If a conflict or a failing test is past what you can resolve with confidence, run \`git -c core.commentChar=';' rebase --abort\` if a rebase is under way, record it with \`${c} builder call ${e} "Rebase failed in <the files or tests> \xB7 <why>"\`, and run the finish step: with the call it hands the card to the orchestrator.`,"",S(n),"",`To finish, run ${j({route:t,key:e})}, the text saying what you resolved. It reruns the suite and pushes the branch to PR #${u} with --force-with-lease; the PR stays as it is.`,"- If it refuses because someone pushed to the branch since your fetch, fetch and rebase again (step 2), then run done again.","- On failing tests, fix them, commit, and run done again.","","Never push."].join(`
4
4
  `)}l(_,"rebaseBrief");function fe({route:t,key:e,ref:r,title:i,notes:n,thread:a,kind:h,repo:u,rules:p=[],links:c=[],text:d,files:o=[],question:s,note:w,choice:f,prototype:b=null}){const m=$(t),g=h==="grill";return[`You are the headless ${g?"grill":"prototype"} planner for card ${r} (${e}, "${i}") on the kickflip board /${t}. The board launched you; nobody watches this session, so never wait on a person: the owner answers on the board, and a later planner or builder picks up from their answers. Read and run in ${u}; write no output code ${g?"there":`in ${u} itself`}, never restart the hub or a board server, and never push.`,"",`1. Run \`${m} builder start ${e} --kind ${h}\`. If it refuses, stop.`,`2. Gather the context yourself: ${n?`the card's notes, ${n}, all sections, where later sections override earlier ones; `:"the card has no notes file yet; "}its thread, ${a}, the repo's docs/adr and CONTEXT.md, and the cards its notes and thread link.`,`3. Report progress with \`${m} builder progress ${e} "<plain, 80 characters or fewer>"\` at least every 10 minutes.`,g?`4. Find every decision the card leaves open: read the code it touches and answer what the code answers yourself. Post each decision left as a question for the owner with \`${m} question add --file <json>\` on topic item:${e}, one per decision, your recommendation first. Once you finish, the orchestrator screens them before the owner sees them, settling the ones the board already answers and passing the owner the rest. Invoke the grilling skill with the Skill tool and run it as one round: its questions go to the board with question add, never into the chat, and you never wait for an answer; after the round, finish. (With no Skill tool, follow this step as written.) When the code shows the card has separable parts, each a build of its own, propose splitting it into a wave of smaller cards as one question ahead of the prototype question: its options "Split as proposed" and "Keep as one card", your recommendation first. Slice it as ~/.claude/skills/to-tickets/SKILL.md step 3, "Draft vertical slices", says (read that step; you can't invoke the skill, and skip its quiz and publish steps). The first card is the thinnest end-to-end path. The question's context lists each proposed card with its title, its scope, the files it touches and the earlier proposed cards it waits on, or none: a card waits on an earlier one only when it builds on that card or shares its seam (the same function, component, CLI command or doc paragraph); sharing only a file waits on nothing. Write the same breakdown in your notes section. On Split the orchestrator files the cards when the card stages; never file them yourself.`+(b?` The card keeps the owner's answer to its prototype question, ${b}, so ask no "Prototype round first?" question: once every question you ask is answered, or as you finish when you ask none, the board moves the card ${b==="yes"?"to prototyping":"to staging"}.`:' End every grill with one more question, "Prototype round first?", whose options are yes and no, with "prototype_gate": true in its `question add --file` entry: once the owner has answered every question the board moves the card to prototyping on yes, or to staging on no, so nothing else moves it. Recommend yes when the card changes the hub\'s or a board\'s UI, no otherwise. Ask it even when it is your only question.'):`4. Invoke the prototype skill with the Skill tool and build the round as take files, one per option: record them with \`${m} prototype add <topic> --card ${e} --take <K>:<label> \u2026\`, write the take files it prints, built from the repo's existing components, helpers and scripts fed mock data (the prototype template loads the board's scripts, so a take reuses the board's components; a hub-page take reuses them by path or copy), each take showing the states it needs inside itself, then post one question whose url opens prototype-<topic>.html with \`${m} question add --file <json>\` on topic item:${e}, never asking in the chat. Build in situ only where one of the board's rules asks for it: then, run \`${m} prototype base\`, which makes it or moves it to main and prints its path, whether its build script is there and the listener a page loads. When the build script is missing, write one there that serves the app and prints "port <n>" (board/build-script.mjs). Add one page to the worktree, at a path no other round uses, built from the repo's real components, helpers and scripts fed mock data, fully interactive; it reads the take from ?variant= and the state from ?state=, and loads the listener so \`KickflipPrototype.on((take, state) => \u2026)\` re-renders it in place when the viewer switches. Record it with \`${m} prototype add <topic> --card ${e} --served <path> --take <K>:<label> \u2026 --state <key>:<label> \u2026\`, one take per option and one state for each state that matters to the question, then post one question whose url opens prototype-<topic>.html with \`${m} question add --file <json>\` on topic item:${e}, never asking in the chat. (With no Skill tool, follow this step as written.)`,...d?[` The owner asked for this ${g?"grill":"prototype"}, writing: "${d}"`]:[],...o.length?[` The owner attached: ${o.map(v=>v.path).join(", ")}.`]:[],...s?[` The owner asked for prototypes of question ${s}${w?`, noting "${w}"`:""}${f?`, leaning to option ${f}`:""}: draw that question's next round, its question naming round_of ${s}.`]:[],`5. Record what you learned in the card's notes, naming each answered question's id: write it in ${le}, then run ${ce(m,e,`${g?"Grill":"Prototype"} (<MM/DD>)`)}, which appends it as a section. The notes file is read-only; never write into it. Anything you leave open becomes a question with question add, never only a line in the notes, so the card can't go ready past it. A question's text is one line of at most ${Z} characters (question add refuses a longer one); put the detail in its context and the options' details, which take markdown.`,...g?[`6. Where the owner asked for prototypes, or a rule here wants a UI prototype first, end the card \`${m} item set ${e} --phase prototyping\` instead and skip the prototype question. Otherwise ${b?"the board":"the prototype question"} is what moves the card on: never set it ready yourself, and never leave a Next that asks the owner to mark it ready.`]:[],"",...q(c),...I(p,"Rules from the hub and this board, written for the orchestrator: where one tells you to push, land, restart the hub or a board server, or do the orchestrator's work, it isn't yours; you never push or restart:"),`To finish, run \`${m} builder done ${e} --text "<what you asked>"\`.`,"","Never push."].join(`
5
5
  `)}l(fe,"plannerBrief");function we({route:t,key:e,ref:r,title:i,notes:n,thread:a,repo:h,agent:u,rules:p=[],links:c=[],text:d,files:o=[]}){const s=$(t);return[`You are the headless visual agent for card ${r} (${e}, "${i}") on the kickflip board /${t}. The owner asked, in the card's thread, for a visual: one HTML page that answers in a visual way, such as a diagram, a flow or a comparison. It is not a prototype or a grill: no question, no takes. The board launched you; nobody watches this session, so never wait on a person. Read and run in ${h}; write no code there, never restart the hub or a board server, and never push.`,"",`1. Run \`${s} builder start ${e} --kind visual --agent ${u}\`. If it refuses, stop.`,`2. Gather what the page needs: ${n?`the card's notes, ${n}; `:""}its thread, ${a}, and the code or docs it is about.`,d?` The owner wrote: "${d}"`:" The owner typed nothing: draw what the thread was last discussing.",...o.length?[` The owner attached: ${o.map(w=>w.path).join(", ")}.`]:[],"3. Write one HTML file outside the repo, such as in a temporary folder. Keep its own CSS and script inline; it may load a library such as Mermaid or D3 from a CDN. Make it read well at phone width (390px) and on a desktop, in the dark and the light theme (prefers-color-scheme).",`4. Place it with \`${s} visual add ${e} --file <page.html> --title "<what it shows>"\`, which copies it to the board and links it in the thread.`,"","Write nothing else: no notes, questions, card state, Next or progress lines.","",...q(c),...I(p,"Rules from the hub and this board, written for the orchestrator: where one tells you to push, land, restart the hub or a board server, or do the orchestrator's work, it isn't yours; you never push or restart:"),`To finish, run \`${s} builder done ${e} --agent ${u}\`.`,"","Never push."].join(`
6
6
  `)}l(we,"visualBrief");function me({route:t,key:e,ref:r,title:i,notes:n,thread:a,agent:h,task:u,writes:p,cwd:c,rules:d=[],links:o=[]}){const s=$(t);return[`You are the headless helper ${h} on card ${r} (${e}, "${i}") on the kickflip board /${t}. Another agent working on the card asked the board for you to do one task beside it, and waits on your hand-back. Nobody watches this session, so never wait on a person. Never restart the hub or a board server, and never push.`,p?`Work only in ${c}, your own worktree, branched from the card's: commit your work there with \`git -c core.commentChar=';' commit -F <file>\`: ${L(r)}. The card's builder merges your branch, so leave nothing uncommitted.`:`Read and run in ${c}; change no files there and commit nothing.`,"","The task:",u,"",`1. Run \`${s} builder start ${e} --kind helper --agent ${h}\`. If it refuses, stop.`,`2. Report progress with \`${s} builder progress ${e} --agent ${h} --phase reading|building|testing|checking "<plain, 80 characters or fewer>"\` at least every 10 minutes; the board adds your name and the phase word.`,"3. Do only the task. Don't ask the orchestrator or the owner, record no judgment calls and post no questions: put any decision you had to make in your hand-back.",n?` The card's notes, for context: ${n}.`:"",` ${S(a)}`,"",...q(o),...I(d,"Rules from the hub and this board, written for the orchestrator: where one tells you to push, land, restart the hub or a board server, or do the orchestrator's work, it isn't yours; you never push or restart:"),`To finish, run \`${s} builder done ${e} --agent ${h} --text "<your hand-back: what you did or found, ${p?"what you committed, ":""}and any decision you made>"\`.`,"","Never push."].filter((w,f,b)=>w!==""||b[f-1]!=="").join(`
7
- `)}l(me,"helperBrief");const be=6e4;function ge(t,{starts:e=D}={}){const r=t.filter(n=>n.pid&&n.at).map(n=>n.pid),i=r.length&&e(r)||new Map;return t.filter(n=>{const a=i.get(n.pid);return a!==void 0&&a<=Date.parse(n.at)&&a>=Date.parse(n.at)-be})}l(ge,"launchedAgents");function Ne(t,{starts:e=D,kill:r=l((n,a)=>process.kill(-n,a),"kill"),killAfter:i=5e3}={}){const n=ge(t,{starts:e});for(const{pid:a}of n){try{r(a,"SIGTERM")}catch{continue}setTimeout(()=>{try{r(a,"SIGKILL")}catch{}},i).unref()}return n.length}l(Ne,"stopAgents");async function Pe(t,{grace:e=5e3}={}){const r=["-A","-o","pid=,ppid=,pgid=,args="],i=M(r)??M(r);if(i===null)throw new re("ps gave no answer, so nothing was stopped; run it again");const n=i.split(`
8
- `).map(o=>/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/.exec(o)).filter(Boolean).map(([,o,s,w,f])=>({pid:Number(o),ppid:Number(s),pgid:Number(w),args:f})),a=new Set(n.filter(o=>t.includes(o.pgid)).map(o=>o.pid));for(let o=!0;o;){o=!1;for(const s of n)!a.has(s.pid)&&a.has(s.ppid)&&(a.add(s.pid),o=!0)}const h=n.find(o=>o.pid===process.pid)?.pgid,u=n.filter(o=>a.has(o.pid)&&o.pgid>1&&o.pgid!==h),p=[...new Set(u.map(o=>o.pgid))],c=l(o=>{for(const s of p)try{process.kill(-s,o)}catch{}},"signal");c("SIGTERM");const d=Date.now()+e;for(;u.some(o=>A(o.pid))&&Date.now()<d;)await new Promise(o=>setTimeout(o,50));return u.some(o=>A(o.pid))&&c("SIGKILL"),u.map(({pid:o,args:s})=>({pid:o,args:s}))}l(Pe,"stopRuns");const A=l(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"pidAlive");async function Ce(t,e,{launch:r,handBack:i,alive:n=A,held:a=l(()=>!1,"held"),stopped:h=new Set,paused:u=new Set,limitedUntil:p=l(()=>null,"limitedUntil"),atLimit:c=l(async()=>{},"atLimit")}){const d={};for(const[o,s]of Object.entries(t)){if(!e.has(o))continue;if(h.has(o)){d[o]={...s,handedBack:!0};continue}if(u.has(o)||s.pid!==null&&n(s.pid)||s.handedBack){d[o]=s;continue}const w={key:o,...s.kind?{kind:s.kind}:{},...s.model?{model:s.model}:{}},f=s.pid!==null?p(s):null;if(f){await c(w,f),d[o]={...s,pid:null};continue}if(a(s.kind??"claude")){d[o]=s;continue}if(s.pid!==null&&s.relaunched){await i(w),d[o]={...s,handedBack:!0};continue}const{key:b,...m}=w;d[o]={...m,...await r(w),relaunched:s.pid!==null||!!s.relaunched}}return d}l(Ce,"reviveBuilders");const G=l((t,e)=>T.join(J.homedir(),".claude","kickflip","logs",`${e}-${t}-${new Date().toISOString().replace(/[:.]/g,"-")}.log`),"logFile");async function $e(t,{key:e,kind:r="claude",model:i,agent:n="main",text:a,files:h,question:u,note:p,choice:c,task:d,writes:o,worktree:s},{lander:w=!1}={}){const f=await B(t,e),b=O(t)?.route??T.basename(t),m=[...te(),...await ne(t)].map(Q=>Q.text),{base:g,repo:v}=F(t),k=ie({kind:r}),R=r===ae,N=!k&&!R&&!w&&f.rebasePR!==null,P=r===he,C=k?v:R?s??f.worktree??v:f.worktree,y={...f,route:b,base:g,repo:v,rules:m,kind:r,agent:n,text:a,files:h,question:u,note:p,choice:c,task:d,writes:o,cwd:C,pr:f.rebasePR},V=P?"Visual agent":R?`Helper ${n}`:k?"Planner":w?"Lander":N?"Rebase run":"Builder",z=P?we(y):R?me(y):k?fe(y):w?K(y):N?_(y):pe(y),x=await W({kind:k||R?"claude":r,model:i,cwd:C,site:t,prompt:z,log:G(e.split("/")[1],V.split(" ")[0].toLowerCase())});return await E(t,e,x.sessionId,x.pid,n,T.basename(x.log)),x}l($e,"launchBuilder");const We=l(({ref:t,worktree:e,cleared:r})=>`The board paused your build of card ${t}, and the pause has cleared: ${r}. Carry on where you stopped, in ${e}, following your brief as before. Your builder record is still open, so don't run builder start again.`,"resumeBrief");async function U(t,{key:e,kind:r="claude",model:i,session:n,brief:a,fresh:h=!1,freshLaunch:u={}}){if(r!=="claude"||!n||h)return{...await $e(t,{key:e,kind:r,model:i},u),resumed:!1};const p=await B(t,e),c=await W({kind:r,model:i,cwd:n.worktree??p.worktree,site:t,prompt:a(p),resume:n.id,log:G(e.split("/")[1],"builder")});return await E(t,e,c.sessionId??n.id,c.pid,"main",T.basename(c.log)),{...c,resumed:!0}}l(U,"resumeBuilder");async function De(t,{key:e,kind:r="claude",model:i},{why:n,fresh:a=!1}={}){const h=await se(t,e),u=await B(t,e),p=u.rebasePR!==null,c=h&&(!p||h.worktree===u.worktree)?h:null,d=O(t)?.route??T.basename(t),{base:o}=F(t),s=l(b=>(p?_:K)({...b,route:d,base:o,pr:b.rebasePR,why:n,resumed:!0}),"brief"),w={lander:!p};let f;try{f=await U(t,{key:e,kind:r,model:i,session:c,brief:s,fresh:a,freshLaunch:w})}catch(b){if(!c)throw b;f=await U(t,{key:e,kind:r,model:i,session:null,brief:s,freshLaunch:w})}return f.resumed&&await oe(t,e,"builder-resumed-to-land",{session:f.sessionId??c.id}),f}l(De,"landByBuilder");export{de as BOARD_CLI,pe as builderBrief,me as helperBrief,De as landByBuilder,K as landerBrief,$e as launchBuilder,ge as launchedAgents,A as pidAlive,fe as plannerBrief,_ as rebaseBrief,We as resumeBrief,U as resumeBuilder,Ce as reviveBuilders,Ne as stopAgents,Pe as stopRuns,we as visualBrief};
7
+ `)}l(me,"helperBrief");const be=6e4;function ge(t,{starts:e=D}={}){const r=t.filter(n=>n.pid&&n.at).map(n=>n.pid),i=r.length&&e(r)||new Map;return t.filter(n=>{const a=i.get(n.pid);return a!==void 0&&a<=Date.parse(n.at)&&a>=Date.parse(n.at)-be})}l(ge,"launchedAgents");function Ne(t,{starts:e=D,kill:r=l((n,a)=>process.kill(-n,a),"kill"),killAfter:i=5e3}={}){const n=ge(t,{starts:e});for(const{pid:a}of n){try{r(a,"SIGTERM")}catch{continue}setTimeout(()=>{try{r(a,"SIGKILL")}catch{}},i).unref()}return n.length}l(Ne,"stopAgents");async function Ce(t,{grace:e=5e3}={}){const r=["-A","-o","pid=,ppid=,pgid=,args="],i=M(r)??M(r);if(i===null)throw new re("ps gave no answer, so nothing was stopped; run it again");const n=i.split(`
8
+ `).map(o=>/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/.exec(o)).filter(Boolean).map(([,o,s,w,f])=>({pid:Number(o),ppid:Number(s),pgid:Number(w),args:f})),a=new Set(n.filter(o=>t.includes(o.pgid)).map(o=>o.pid));for(let o=!0;o;){o=!1;for(const s of n)!a.has(s.pid)&&a.has(s.ppid)&&(a.add(s.pid),o=!0)}const h=n.find(o=>o.pid===process.pid)?.pgid,u=n.filter(o=>a.has(o.pid)&&o.pgid>1&&o.pgid!==h),p=[...new Set(u.map(o=>o.pgid))],c=l(o=>{for(const s of p)try{process.kill(-s,o)}catch{}},"signal");c("SIGTERM");const d=Date.now()+e;for(;u.some(o=>A(o.pid))&&Date.now()<d;)await new Promise(o=>setTimeout(o,50));return u.some(o=>A(o.pid))&&c("SIGKILL"),u.map(({pid:o,args:s})=>({pid:o,args:s}))}l(Ce,"stopRuns");const A=l(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"pidAlive");async function Pe(t,e,{launch:r,handBack:i,alive:n=A,held:a=l(()=>!1,"held"),stopped:h=new Set,paused:u=new Set,limitedUntil:p=l(()=>null,"limitedUntil"),atLimit:c=l(async()=>{},"atLimit")}){const d={};for(const[o,s]of Object.entries(t)){if(!e.has(o))continue;if(h.has(o)){d[o]={...s,handedBack:!0};continue}if(u.has(o)||s.pid!==null&&n(s.pid)||s.handedBack){d[o]=s;continue}const w={key:o,...s.kind?{kind:s.kind}:{},...s.model?{model:s.model}:{}},f=s.pid!==null?p(s):null;if(f){await c(w,f),d[o]={...s,pid:null};continue}if(a(s.kind??"claude")){d[o]=s;continue}if(s.pid!==null&&s.relaunched){await i(w),d[o]={...s,handedBack:!0};continue}const{key:b,...m}=w;d[o]={...m,...await r(w),relaunched:s.pid!==null||!!s.relaunched}}return d}l(Pe,"reviveBuilders");const G=l((t,e)=>T.join(J.homedir(),".claude","kickflip","logs",`${e}-${t}-${new Date().toISOString().replace(/[:.]/g,"-")}.log`),"logFile");async function $e(t,{key:e,kind:r="claude",model:i,agent:n="main",text:a,files:h,question:u,note:p,choice:c,task:d,writes:o,worktree:s},{lander:w=!1}={}){const f=await B(t,e),b=F(t)?.route??T.basename(t),m=[...te(),...await ne(t)].map(Q=>Q.text),{base:g,repo:v}=O(t),k=ie({kind:r}),R=r===ae,N=!k&&!R&&!w&&f.rebasePR!==null,C=r===he,P=k?v:R?s??f.worktree??v:f.worktree,y={...f,route:b,base:g,repo:v,rules:m,kind:r,agent:n,text:a,files:h,question:u,note:p,choice:c,task:d,writes:o,cwd:P,pr:f.rebasePR},V=C?"Visual agent":R?`Helper ${n}`:k?"Planner":w?"Lander":N?"Rebase run":"Builder",z=C?we(y):R?me(y):k?fe(y):w?K(y):N?_(y):pe(y),x=await W({kind:k||R?"claude":r,model:i,cwd:P,site:t,prompt:z,log:G(e.split("/")[1],V.split(" ")[0].toLowerCase())});return await E(t,e,x.sessionId,x.pid,n,T.basename(x.log)),x}l($e,"launchBuilder");const We=l(({ref:t,worktree:e,cleared:r})=>`The board paused your build of card ${t}, and the pause has cleared: ${r}. Carry on where you stopped, in ${e}, following your brief as before. Your builder record is still open, so don't run builder start again.`,"resumeBrief");async function U(t,{key:e,kind:r="claude",model:i,session:n,brief:a,fresh:h=!1,freshLaunch:u={}}){if(r!=="claude"||!n||h)return{...await $e(t,{key:e,kind:r,model:i},u),resumed:!1};const p=await B(t,e),c=await W({kind:r,model:i,cwd:n.worktree??p.worktree,site:t,prompt:a(p),resume:n.id,log:G(e.split("/")[1],"builder")});return await E(t,e,c.sessionId??n.id,c.pid,"main",T.basename(c.log)),{...c,resumed:!0}}l(U,"resumeBuilder");async function De(t,{key:e,kind:r="claude",model:i},{why:n,fresh:a=!1}={}){const h=await se(t,e),u=await B(t,e),p=u.rebasePR!==null,c=h&&(!p||h.worktree===u.worktree)?h:null,d=F(t)?.route??T.basename(t),{base:o}=O(t),s=l(b=>(p?_:K)({...b,route:d,base:o,pr:b.rebasePR,why:n,resumed:!0}),"brief"),w={lander:!p};let f;try{f=await U(t,{key:e,kind:r,model:i,session:c,brief:s,fresh:a,freshLaunch:w})}catch(b){if(!c)throw b;f=await U(t,{key:e,kind:r,model:i,session:null,brief:s,freshLaunch:w})}return f.resumed&&await oe(t,e,"builder-resumed-to-land",{session:f.sessionId??c.id}),f}l(De,"landByBuilder");export{de as BOARD_CLI,pe as builderBrief,me as helperBrief,De as landByBuilder,K as landerBrief,$e as launchBuilder,ge as launchedAgents,A as pidAlive,fe as plannerBrief,_ as rebaseBrief,We as resumeBrief,U as resumeBuilder,Pe as reviveBuilders,Ne as stopAgents,Ce as stopRuns,we as visualBrief};