@lovek4d/kickflip 0.9.0 → 0.10.1
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/CONTEXT.md +2 -2
- package/SKILL.md +8 -8
- package/bin/event-lines.mjs +1 -1
- package/board/bin/board.mjs +20 -23
- package/board/domain.mjs +7 -7
- package/board/headless.mjs +3 -3
- package/board/public/app-cards.js +31 -8
- package/board/public/prototype-kit.js +1 -0
- package/board/public/prototype.html +6 -0
- package/board/public/style.css +2 -2
- package/board/public/ui-cards.js +6 -1
- package/board/public/ui.js +1 -1
- package/board/public/view-questions.js +8 -4
- package/board/public/view-settings.js +9 -3
- package/board/render-check.mjs +8 -8
- package/board/server.mjs +17 -17
- package/board/store.mjs +29 -29
- package/hub/github.mjs +1 -1
- package/hub/hub.mjs +11 -11
- package/hub/pr-report.mjs +2 -2
- package/hub/presence.mjs +1 -1
- package/hub/review-send.mjs +1 -1
- package/hub/try.mjs +12 -11
- package/hub/upgrade.mjs +1 -1
- package/package.json +1 -1
- package/shared/colors.css +3 -3
package/board/bin/board.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var H=Object.defineProperty;var
|
|
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
|
|
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,
|
|
345
|
-
channel,
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
@@ -379,9 +380,10 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
379
380
|
settings appear when set: {"auto_merge": false,
|
|
380
381
|
"auto_build": false, "auto_pull": false,
|
|
381
382
|
"max_implementers": 10, "max_planners": 3,
|
|
382
|
-
"
|
|
383
|
-
"
|
|
384
|
-
"
|
|
383
|
+
"max_serves": 10, "auto_triage": false,
|
|
384
|
+
"auto_review": false, "idle_notification": false,
|
|
385
|
+
"skip_draft": false, "prs": true,
|
|
386
|
+
"github_login": "owner"}
|
|
385
387
|
settings set auto-merge on|off on: the hub's tick merges an approved PR once CI is green and
|
|
386
388
|
the merge state is CLEAN. off (the default): the card
|
|
387
389
|
shows the owner a Merge button, and the board server merges
|
|
@@ -536,11 +538,6 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
536
538
|
arguments are refused. Three runs in a row that can't run
|
|
537
539
|
mark it broken and append owner-check-broken {key, check,
|
|
538
540
|
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
541
|
[--title "<type>(<scope>): <summary>"] the draft PR's title on a GitHub board, a Conventional
|
|
545
542
|
subject of at most 72 characters (else the branch's first
|
|
546
543
|
commit subject); refused in any other shape before
|
|
@@ -703,4 +700,4 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
703
700
|
(overnight, snoozed or the weekend). --urgent: a
|
|
704
701
|
production fault only; it beats those holds. While
|
|
705
702
|
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};
|
|
703
|
+
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};
|