@lovek4d/kickflip 0.10.0 → 0.11.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/CONTEXT.md +6 -6
- package/SKILL.md +7 -7
- package/bin/event-lines.mjs +1 -1
- package/board/bin/board.mjs +25 -20
- package/board/domain.mjs +7 -7
- package/board/public/app-cards.js +31 -8
- package/board/public/app.js +22 -2
- package/board/public/dialogs.js +2 -2
- package/board/public/new-task.js +2 -2
- package/board/public/prototype-kit.js +1 -0
- package/board/public/prototype.html +6 -0
- package/board/public/style.css +7 -2
- package/board/public/ui-cards.js +11 -3
- package/board/public/ui-enums.js +1 -1
- package/board/public/ui.js +10 -2
- 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 +32 -32
- package/hub/hub.mjs +11 -11
- package/hub/presence.mjs +1 -1
- package/hub/review-send.mjs +1 -1
- package/hub/slack.mjs +4 -2
- package/hub/try.mjs +12 -11
- package/hub/upgrade.mjs +1 -1
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
package/board/bin/board.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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
|
|
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 M,setSlack as D}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.
|
|
6
6
|
Paths resolve under $HOME, so HOME=<dir> points every command at a copy.
|
|
7
7
|
A --file argument of - reads stdin.
|
|
8
|
-
Every new card is a task, keyed T-<n>, with no ticket, until
|
|
9
|
-
|
|
8
|
+
Every new card is a task, keyed T-<n>, with no ticket, until it goes ready (the board makes one, on a board with a
|
|
9
|
+
source), item add or item set --new-ticket (the orchestrator, for a Quick task), or --ticket <id> gives it one. A ticket comes
|
|
10
10
|
from the board's source, GitHub or Linear, made from the card's title and notes, Linear in the board's team and project
|
|
11
11
|
through Hub's Linear key; a linked one must exist there. A board whose source is none has no tickets.
|
|
12
12
|
A card moves its ticket one way: a build's start \u2192 In Progress, --pr, --pushed or a verified merge-ready build \u2192 In
|
|
@@ -21,11 +21,11 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
21
21
|
questions; one to wave-triage resolves its wave's,
|
|
22
22
|
noting "Acted on: replied". A
|
|
23
23
|
reply to overnight-triage ends that triage
|
|
24
|
-
[--title <t>] retitles the New or
|
|
25
|
-
reply <file>/<id> (--text <md> | --file <path>) answers the New card an ask became: the
|
|
26
|
-
[--title <t>] its thread and the card
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
[--title <t>] retitles the New or discussing card the ask became
|
|
25
|
+
reply <file>/<id> (--text <md> | --file <path>) answers the New or discussing card an ask became: the
|
|
26
|
+
[--title <t>] answer goes in its thread and the card reads
|
|
27
|
+
discussing in Todo, --title as its title. Refuses a
|
|
28
|
+
card sorted another way or that isn't one an ask became
|
|
29
29
|
thread <file>/<id> --text <t> [--link <url>] one line in the item's thread; ask:<eventId> in place
|
|
30
30
|
[--who claude|love|<name>] [--via github|slack] of <file>/<id> writes in that ask's thread
|
|
31
31
|
A Claude thread line, or a reply to an ask or item-message, rewrites the body of the thread's
|
|
@@ -34,6 +34,8 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
34
34
|
visual add <file>/<id> --file <page.html> --title <t> a visual agent's page: copied to public/visuals/<id>/<slug>.html
|
|
35
35
|
(a new name each time), linked by a visual line in the
|
|
36
36
|
item's thread; prints the page's link
|
|
37
|
+
item visual <file>/<id> --text <t> asks the board for a visual on the card, as the thread's
|
|
38
|
+
image does: a visual agent draws what --text says
|
|
37
39
|
topic add <id> --title <t> [--summary <s>] a question topic in data/owner.json
|
|
38
40
|
question add --file <json> one question or an array of them:
|
|
39
41
|
{id, topic, question, options: [{id, label, recommended?, detail?, diagram?, practice?}],
|
|
@@ -66,7 +68,7 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
66
68
|
topic): the owner's yes moves the card to prototyping and starts its prototype planner, a no stages it
|
|
67
69
|
once no question on it is left open, the board doing both with no orchestrator step. The answer stays
|
|
68
70
|
on the card as its gates.prototype, so a regrill or a split child never asks it again: question add
|
|
69
|
-
refuses a prototype_gate
|
|
71
|
+
refuses a prototype_gate whose answer the card keeps, and a yes is spent once the card
|
|
70
72
|
moves to prototyping on it.
|
|
71
73
|
reask_of names a resolved question on the same topic that the owner asked to ask again
|
|
72
74
|
(reask-request): it resolves that one again, noting "Asked again as <new id>", printed as "resolved: <old id>".
|
|
@@ -148,9 +150,11 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
148
150
|
from another ask (use item relate-ask), an ask that is
|
|
149
151
|
already a card, naming it (sort that card instead), and
|
|
150
152
|
one with no phase when --phase is missing
|
|
151
|
-
[--phase ready|grilling|prototyping]
|
|
153
|
+
[--phase ready|discussing|grilling|prototyping] a Todo card's phase: what it does next; it reads New
|
|
152
154
|
until set, which sorts a New card an ask became (an
|
|
153
|
-
answered one leaves In Review for Todo).
|
|
155
|
+
answered one leaves In Review for Todo). discussing is
|
|
156
|
+
a conversation in its thread, which reply on a New card
|
|
157
|
+
also sets. Refused on a card in another lane, whose phase
|
|
154
158
|
the board sets, and as --state, its old name.
|
|
155
159
|
Staging is the board's: it stages a card on the owner's
|
|
156
160
|
Send, Implement or Reply on a ready card, and refuses
|
|
@@ -160,8 +164,8 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
160
164
|
a card without --size, a notes file, or the id of each
|
|
161
165
|
answered question in that file, since a builder starts
|
|
162
166
|
from it; it resolves those answered questions.
|
|
163
|
-
grilling or prototyping on a staging or
|
|
164
|
-
|
|
167
|
+
grilling or prototyping on a discussing, staging or
|
|
168
|
+
ready card has the board start that round's planner.
|
|
165
169
|
A ready card whose block ended waits on your re-check:
|
|
166
170
|
ready again with --text "<what you re-checked>", which
|
|
167
171
|
goes into its notes, releases it to the pull
|
|
@@ -249,9 +253,9 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
249
253
|
from the + or New task has none
|
|
250
254
|
[--split-of <file>/<id>] a card split from that one: sets it as the Parent, as
|
|
251
255
|
--parent does, and copies its gates, the owner's prototype
|
|
252
|
-
|
|
256
|
+
answer, so its grill never asks it again.
|
|
253
257
|
Refuses --parent beside it
|
|
254
|
-
[--phase grilling|prototyping|ready]
|
|
258
|
+
[--phase discussing|grilling|prototyping|ready] files it at once, as item set's --phase; the session's
|
|
255
259
|
[--relate <id>,<file>/<id>] own form, linking it with --relate in the same call.
|
|
256
260
|
ready is for a Quick build ask (--from-ask), with --size
|
|
257
261
|
and --notes or --notes-from, since a builder starts from it
|
|
@@ -338,7 +342,7 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
338
342
|
"<board> reviews" header when none is open; only on the
|
|
339
343
|
owner's explicit ask
|
|
340
344
|
slack task <eventId> <file> --title <t> a request for work in that thread: a new card in item file
|
|
341
|
-
(--notes <md> | --notes-from <draft.md>) <file>, filed
|
|
345
|
+
(--notes <md> | --notes-from <draft.md>) <file>, filed discussing for the owner's review, never ready;
|
|
342
346
|
the Slack message is its first thread line, by its writer
|
|
343
347
|
integrations readiness rows as one JSON line
|
|
344
348
|
review due one JSON line of the Slack sends the hub owes, from local state
|
|
@@ -380,9 +384,10 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
380
384
|
settings appear when set: {"auto_merge": false,
|
|
381
385
|
"auto_build": false, "auto_pull": false,
|
|
382
386
|
"max_implementers": 10, "max_planners": 3,
|
|
383
|
-
"
|
|
384
|
-
"
|
|
385
|
-
"
|
|
387
|
+
"max_serves": 10, "auto_triage": false,
|
|
388
|
+
"auto_review": false, "idle_notification": false,
|
|
389
|
+
"skip_draft": false, "prs": true,
|
|
390
|
+
"github_login": "owner"}
|
|
386
391
|
settings set auto-merge on|off on: the hub's tick merges an approved PR once CI is green and
|
|
387
392
|
the merge state is CLEAN. off (the default): the card
|
|
388
393
|
shows the owner a Merge button, and the board server merges
|
|
@@ -699,4 +704,4 @@ moves every Linear board to no ticket source. If the source can't make a ticket,
|
|
|
699
704
|
(overnight, snoozed or the weekend). --urgent: a
|
|
700
705
|
production fault only; it beats those holds. While
|
|
701
706
|
notifications are off every push is dropped, urgent too
|
|
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};
|
|
707
|
+
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 q(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(q,"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)],"item visual":[["file/id"],["text"],(e,[a],t)=>n.requestVisual(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=q(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(M())]})],"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 D(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,q(t.file))],"reviewers add":[[],["file"],(e,a,t)=>n.addReviewers(e,q(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"),R=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 reads discussing, the owner's turn`:`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`${R(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})=>`${R(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)=>`${R(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};
|