@lovek4d/kickflip 0.0.0-stage → 0.3.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 +513 -0
- package/LICENSE +57 -0
- package/README.md +64 -2
- package/SKILL.md +142 -0
- package/bin/doctor.mjs +4 -0
- package/bin/install.mjs +3 -0
- package/bin/kickflip.mjs +180 -0
- package/bin/statusline.mjs +5 -0
- package/bin/wait-events.mjs +3 -0
- package/board/arch-review.mjs +5 -0
- package/board/bin/agent.mjs +2 -0
- package/board/bin/board.mjs +379 -0
- package/board/bin/builder-stop-hook.mjs +6 -0
- package/board/build-script.mjs +1 -0
- package/board/domain.mjs +5 -0
- package/board/on-host.mjs +1 -0
- package/board/public/app-arch.js +75 -0
- package/board/public/app-asks.js +188 -0
- package/board/public/app-banner.js +48 -0
- package/board/public/app-cards.js +393 -0
- package/board/public/app-core.js +130 -0
- package/board/public/app-header.js +126 -0
- package/board/public/app-lanes.js +142 -0
- package/board/public/app-needs.js +31 -0
- package/board/public/app-snapshot.js +131 -0
- package/board/public/app-threads.js +167 -0
- package/board/public/app.html +86 -0
- package/board/public/app.js +438 -0
- package/board/public/attach.js +84 -0
- package/board/public/dialogs.js +358 -0
- package/board/public/hub-kit.js +168 -0
- package/board/public/hub.css +170 -0
- package/board/public/kickflip.png +0 -0
- package/board/public/linear-key.js +141 -0
- package/board/public/prompts.js +64 -0
- package/board/public/prototype-kit.js +193 -0
- package/board/public/prototype-switcher.js +226 -0
- package/board/public/prototype.html +55 -0
- package/board/public/router.js +226 -0
- package/board/public/rules-editor.js +127 -0
- package/board/public/shell.js +298 -0
- package/board/public/sidebar.css +219 -0
- package/board/public/sidebar.js +359 -0
- package/board/public/style.css +665 -0
- package/board/public/toasts.js +181 -0
- package/board/public/ui-cards.js +218 -0
- package/board/public/ui-enums.js +10 -0
- package/board/public/ui-header.js +93 -0
- package/board/public/ui-hub.js +112 -0
- package/board/public/ui-links.js +150 -0
- package/board/public/ui-markdown.js +80 -0
- package/board/public/ui-order.js +18 -0
- package/board/public/ui-page-header.js +65 -0
- package/board/public/ui-prompts.js +39 -0
- package/board/public/ui-questions.js +151 -0
- package/board/public/ui-sidebar.js +215 -0
- package/board/public/ui-snapshot.js +62 -0
- package/board/public/ui-threads.js +90 -0
- package/board/public/ui.js +247 -0
- package/board/public/view-frame.js +93 -0
- package/board/public/view-hub-settings.js +282 -0
- package/board/public/view-hub.js +150 -0
- package/board/public/view-notes.js +32 -0
- package/board/public/view-polling.js +87 -0
- package/board/public/view-questions.js +411 -0
- package/board/public/view-settings.js +383 -0
- package/board/review.mjs +1 -0
- package/board/server.mjs +17 -0
- package/board/store.mjs +15 -0
- package/board/tickets.mjs +3 -0
- package/board/worktree.mjs +15 -0
- package/docs/hub-session.md +11 -0
- package/hooks-plugin/.claude-plugin/plugin.json +5 -0
- package/hooks-plugin/hooks/codex-share-auto.mjs +23 -0
- package/hooks-plugin/hooks/hooks.json +54 -0
- package/hooks-plugin/hooks/usage-gate.mjs +18 -0
- package/hooks-plugin/hooks/watcher-stop.mjs +41 -0
- package/hub/awake.mjs +1 -0
- package/hub/board-repo.mjs +1 -0
- package/hub/boards.mjs +4 -0
- package/hub/close-board.mjs +1 -0
- package/hub/duration.mjs +1 -0
- package/hub/files.mjs +1 -0
- package/hub/github.mjs +4 -0
- package/hub/home.mjs +1 -0
- package/hub/hub.mjs +19 -0
- package/hub/icon-180.png +0 -0
- package/hub/icon-192.png +0 -0
- package/hub/icon-512.png +0 -0
- package/hub/icons.mjs +1 -0
- package/hub/linear.mjs +2 -0
- package/hub/live.mjs +2 -0
- package/hub/locks.mjs +1 -0
- package/hub/new-board.mjs +2 -0
- package/hub/permission-hook.mjs +3 -0
- package/hub/pr-report.mjs +1 -0
- package/hub/presence.mjs +1 -0
- package/hub/prompts.mjs +2 -0
- package/hub/push.mjs +2 -0
- package/hub/readiness.mjs +2 -0
- package/hub/repos.mjs +3 -0
- package/hub/restart-board.mjs +1 -0
- package/hub/routes.mjs +1 -0
- package/hub/rules.mjs +1 -0
- package/hub/schedule.mjs +1 -0
- package/hub/sessions.mjs +2 -0
- package/hub/shell.mjs +2 -0
- package/hub/slack.mjs +2 -0
- package/hub/stream.mjs +19 -0
- package/hub/sw.js +144 -0
- package/hub/trust.mjs +2 -0
- package/hub/try.mjs +4 -0
- package/hub/upgrade.mjs +1 -0
- package/hub/usage.mjs +3 -0
- package/hub/views.mjs +1 -0
- package/package.json +26 -4
- package/shared/codex.mjs +1 -0
- package/shared/colors.css +15 -0
- package/shared/fake-gh.mjs +54 -0
- package/shared/repos.mjs +1 -0
- package/shared/service.mjs +33 -0
- package/shared/skills.mjs +2 -0
- package/shared/usage-gate.mjs +1 -0
- package/starter/cross-site.mjs +1 -0
- package/starter/public/claude.js +68 -0
- package/starter/public/index.html +49 -0
- package/starter/server.mjs +4 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var P=Object.defineProperty;var n=(e,t)=>P(e,"name",{value:t,configurable:!0});import{readiness as N,reportSlack as E}from"../../hub/readiness.mjs";import{readSlack as L,setSlack as H}from"../../hub/slack.mjs";import{routeRecord as k}from"../../hub/routes.mjs";import w from"node:fs";import m from"node:path";import{pathToFileURL as B}from"node:url";import*as i from"../store.mjs";import{boardWorktree as D,builderRepo as M,holdingLandLock as W,land as z,worktreesDir as G}from"../worktree.mjs";import{fileFindings as J}from"../arch-review.mjs";import{ITEM_STATUSES as F}from"../domain.mjs";import{readManifest as U}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";import{pushKindError as K}from"../../hub/push.mjs";const Q=`usage: node board.mjs <command> [--site <route>] ...
|
|
3
|
+
|
|
4
|
+
Writes a kickflip board's data files. --site <route> picks ~/.claude/kickflip/sites/<route>, and its
|
|
5
|
+
board.json says where the board's waves live. Without --site, waves come from ~/.claude/waves.
|
|
6
|
+
Paths resolve under $HOME, so HOME=<dir> points every command at a copy.
|
|
7
|
+
A --file argument of - reads stdin.
|
|
8
|
+
item add creates a ticket in the board\u2019s source unless --ticket <id> links one.
|
|
9
|
+
Internal defaults to INT; GitHub and Linear create an issue from the title and notes, Linear in the board's team and
|
|
10
|
+
project through Hub's Linear key. A linked ticket must exist in the source; an Internal one no other card holds.
|
|
11
|
+
Asks have no ticket; item set --from-ask creates one when the card has none.
|
|
12
|
+
A card moves its ticket one way: --status building or fixing \u2192 In Progress, --pr \u2192 In Review, --status done or
|
|
13
|
+
merged \u2192 Done (a GitHub issue closes), only for a ticket assigned to the owner or created by kickflip. A ticket that
|
|
14
|
+
can't move prints one "ticket <id>: \u2026" line; the card's change still stands.
|
|
15
|
+
A Linear board without Hub's key hands Linear to the session: a linked id is checked on its pattern alone, and a card
|
|
16
|
+
needing an issue, or a ticket to move, prints a "ticket" line saying what to do through your Linear MCP, such as
|
|
17
|
+
creating the issue and running item set <wave>/<id> --ticket <id>.
|
|
18
|
+
|
|
19
|
+
reply <eventId> (--html <text> | --file <path>) data/replies/<eventId>.html. A reply to an ask resolves its
|
|
20
|
+
answered questions (topic item:ask:<eventId>), and one to
|
|
21
|
+
wave-triage its wave's, noting "Acted on: replied". A
|
|
22
|
+
reply to todo-triage ends that triage
|
|
23
|
+
thread <wave>/<id> --text <t> [--link <url>] one line in the item's thread; ask:<eventId> in place
|
|
24
|
+
[--who claude|love|<name>] [--via github|slack] of <wave>/<id> writes in that ask's thread
|
|
25
|
+
A Claude thread line, or a reply to an ask or item-message, rewrites the body of the thread's
|
|
26
|
+
queued push (tag ask-<eventId> or item-<wave>/<id>) to the message's first line, keeping
|
|
27
|
+
"\xB7 and N replies", and prints the new body. It never pushes: the thread's unread mark carries it
|
|
28
|
+
topic add <id> --title <t> [--summary <s>] a question topic in data/owner.json
|
|
29
|
+
question add --file <json> one question or an array of them:
|
|
30
|
+
{id, topic, question, options: [{id, label, recommended?, detail?, diagram?, practice?}],
|
|
31
|
+
source?, context?, url?, urgent?, round_of?}; topic is declared, item:<wave>/<id>, item:ask:<eventId>
|
|
32
|
+
for a New card not sorted yet, lineup:<slug> for a wave's triage, or wave:<slug> for a wave file's triage on
|
|
33
|
+
a board over ~/.claude/waves. A merged card's topic is refused; a question on a done card reopens it.
|
|
34
|
+
url is http(s), or a site page such as prototype-<topic>.html?q=<id>, stored as given.
|
|
35
|
+
Prototype options keep take order, ids matching take keys; recommended: true tags the recommendation.
|
|
36
|
+
Waiting on you shows fitting takes in a grid, or a chooser above one full-width take for a single take or if any
|
|
37
|
+
scrolls or fills the screen.
|
|
38
|
+
diagram, with practice: rows of 1-4 boxes [[{text, tone?, to?}]], tone quiet|info|notice|
|
|
39
|
+
danger|success, to the next row's box indexes (default all); practice: ["<line>", \u2026]
|
|
40
|
+
A prototype url's topic must be recorded (prototype add), its option ids a non-empty subset of the
|
|
41
|
+
take keys in take order, without repeats, and every take file written; a hand-written prototype-*.html
|
|
42
|
+
is refused. On an item: topic, each other question answered and still waiting on Claude is
|
|
43
|
+
resolved, noting "Followed by <new id>", and printed as "resolved: <old id>". Any other unresolved
|
|
44
|
+
question on the same topic that links the same prototype is resolved, noting "Replaced by <new id>",
|
|
45
|
+
and printed as "replaced: <old id>". round_of names the question on the same topic whose
|
|
46
|
+
prototype-request this answers; the question needs a prototype url, takes the next round
|
|
47
|
+
number, and resolves that one, noting "Followed by prototype round N".
|
|
48
|
+
The board server pushes the new open questions itself, under the card's tag (item-<wave>/<id>, or
|
|
49
|
+
ask-<eventId>) or topic-<topic> off the cards
|
|
50
|
+
question resolve <id> --note <text> sets resolved and resolved_at; refuses a question on a
|
|
51
|
+
prototype page until an answer settles its round: a pick, with a note or none, or an answer to
|
|
52
|
+
an "Is this it?" confirm already on the board. A question already resolved, such as one an
|
|
53
|
+
answer with a note and no option resolved, is left as it is
|
|
54
|
+
prototype add <topic> --title <t> --take <K>:<label> records a prototype in data/prototypes.json: the board
|
|
55
|
+
[--take <K>:<label> \u2026] [--card <wave>/<id>] serves prototype-<topic>.html from its one template, with
|
|
56
|
+
the title, a back arrow to --card (else the board), the
|
|
57
|
+
take pill and a stage. topic: a-z, 0-9 and "-"; K: one
|
|
58
|
+
capital letter, in take order; a label never starts with
|
|
59
|
+
"A. ", since the pill shows it alone. Prints each take
|
|
60
|
+
file to write,
|
|
61
|
+
public/prototypes/<topic>/<K>.js, which exports
|
|
62
|
+
render(stage, kit) (board/public/prototype-kit.js lists kit).
|
|
63
|
+
--card starts that card: it leaves Todo
|
|
64
|
+
prototype list one line per recorded prototype
|
|
65
|
+
prototype rm <topic> drops the record, keeping its take files; refuses while an
|
|
66
|
+
open question links it
|
|
67
|
+
pr <wave>/<id> one JSON line: the card's PR state, checks and new events,
|
|
68
|
+
using the board's repo, github-login and the card's gh_since
|
|
69
|
+
item set <wave>/<id> [--status <s> | --clear-status] edits the item in its wave file
|
|
70
|
+
[--next <t> | --clear-next] [--pr <n>] status: ${F.join(", ")}
|
|
71
|
+
an empty --next "" clears it, as --clear-next does
|
|
72
|
+
naming another unfinished card requires it in --deps
|
|
73
|
+
[--pushed <branch>] on a board whose repo is a plain git remote (no GitHub):
|
|
74
|
+
the card's branch is pushed there, so it waits In Review
|
|
75
|
+
on the owner's merge. Push it first (git push origin
|
|
76
|
+
<branch>). The hub's repo fetch moves it to Done once the
|
|
77
|
+
branch is merged into the board's branch or gone from the
|
|
78
|
+
remote. Clears building/fixing as --pr does
|
|
79
|
+
[--gh-since <ISO time>] records the last handled GitHub event, an ISO-8601 UTC
|
|
80
|
+
time such as 2026-10-23T11:00:00Z, stored as given
|
|
81
|
+
[--from-ask <eventId> --text <t>] the ask it came from: its New card gives way to this item,
|
|
82
|
+
whose thread opens with the ask and its thread, then --text
|
|
83
|
+
as the item's first line; it leaves a card it was folded into; refuses a card
|
|
84
|
+
from another ask (use item relate-ask)
|
|
85
|
+
[--state ready|grilling|prototyping] what a card from an ask does next; it reads New until set.
|
|
86
|
+
ready refuses while the card has an open question
|
|
87
|
+
[--held | --unheld | --pick-up-at <time>] holds or resumes the card on the owner's words; the board
|
|
88
|
+
server records the owner's own Hold and Resume taps. A later
|
|
89
|
+
--held/--unheld overrides a tap; --unheld also clears a held status.
|
|
90
|
+
--pick-up-at holds it until an ISO time such as
|
|
91
|
+
2026-11-04T09:00 (the Mac's local time without an
|
|
92
|
+
offset; a past time is refused), stored as UTC
|
|
93
|
+
pick_up_at. Within a minute of it the board server
|
|
94
|
+
resumes the card, adds a thread line, and appends
|
|
95
|
+
card-unblocked {key, until} unless a dep is unfinished.
|
|
96
|
+
--held holds it with no time; --pick-up-at "" resumes it
|
|
97
|
+
[--ticket <id>] links a ticket in the board\u2019s source
|
|
98
|
+
[--title <t>] renames the card
|
|
99
|
+
[--size small|large] [--model opus|sonnet] what builder pick reads while the hub's Claude model is
|
|
100
|
+
Auto: a small card gets Claude Sonnet, any other Opus, and
|
|
101
|
+
--model wins over --size; "" clears either
|
|
102
|
+
[--notes-file <file>] the item's notes: a .md file inside its wave's folder,
|
|
103
|
+
named relative to it, holding the session's understanding
|
|
104
|
+
of the task so far. The open card's Notes link opens it,
|
|
105
|
+
or without one the item's short notes text. --plan is an
|
|
106
|
+
alias. Resolves answered questions still waiting on
|
|
107
|
+
Claude, noting "Recorded in the card's notes", and
|
|
108
|
+
prints "resolved: <id>" for each
|
|
109
|
+
[--deps <id>,<wave>/<id>] replaces its blocked-by list; --deps "" clears it
|
|
110
|
+
[--parent <wave>/<id>] sets its Parent, refusing itself or a card below it;
|
|
111
|
+
--parent "" clears it
|
|
112
|
+
[--related <id>,<wave>/<id>] replaces the cards it is Related to, which both cards
|
|
113
|
+
show; --related "" clears it, on both
|
|
114
|
+
[--install <url> [--install-sha <sha>] a device build the owner installs from the card's Install
|
|
115
|
+
[--install-label <text>] | --clear-install] button, stamped built_at now: itms-services://?action=
|
|
116
|
+
download-manifest&url=https://\u2026/manifest.plist for iOS,
|
|
117
|
+
an https:// .apk URL for Android. --install-sha is the
|
|
118
|
+
head it was built from: the badge reads "Install (stale)"
|
|
119
|
+
once the branch moves past it. Hidden once the card is Done
|
|
120
|
+
[--text <t>] with --status done or dropped, required on a site: the
|
|
121
|
+
card's closing line, written last in its thread. merged
|
|
122
|
+
writes "Merged PR #n." unless --text says otherwise
|
|
123
|
+
builder start for a build sets building itself. --status building resolves the card's answered
|
|
124
|
+
questions, noting "Acted on: build started".
|
|
125
|
+
--status building refuses while the item is held or a dep isn't done, merged or dropped,
|
|
126
|
+
and while the newest owner answer to its questions that link a prototype-*.html page picks
|
|
127
|
+
no take; a pick's note is the build's guidance;
|
|
128
|
+
done, merged and dropped print "unblocked: <wave>/<id>" and append card-unblocked {key}
|
|
129
|
+
once per unblocking, including held cards: start each unheld card next.
|
|
130
|
+
building and fixing count as working only while a builder record on the card is open.
|
|
131
|
+
--pr clears building/fixing and closes ordinary builders; install builders keep running.
|
|
132
|
+
checking is a builder's hand-back of work it couldn't land: In Progress, reading "Orchestrator
|
|
133
|
+
review", until the owning session takes it on; it unblocks nothing.
|
|
134
|
+
The board server records the owner's card-done in the wave file itself.
|
|
135
|
+
Every --status or --clear-status records status_at; a later status change overrides the tap.
|
|
136
|
+
--status done, merged or dropped closes the card: it resolves each of its questions (topic
|
|
137
|
+
item:<wave>/<id>) that has an answer, noting "Closed with the card", and prints "resolved: <id>"
|
|
138
|
+
for each. It refuses while one has no answer yet; [--force] closes it and resolves that one too
|
|
139
|
+
item add <wave>[/<id>] --title <t> [--notes <t>] a new card, planned (waiting when it has --deps); never
|
|
140
|
+
[--ticket <id>] [--deps <id>,<wave>/<id>] hand-edit wave.json to add one. Refuses an id the wave has.
|
|
141
|
+
[--notes-file <file>] --plan is an alias of --notes-file
|
|
142
|
+
[--pick-up-at <time>] holds it until then, as item set's --pick-up-at
|
|
143
|
+
[--size small|large] [--model opus|sonnet] as item set's
|
|
144
|
+
[--parent <wave>/<id>] the card it was spun out of, such as one filed from
|
|
145
|
+
another card's thread, grill or answer: it reads as a
|
|
146
|
+
follow-up, and its Parent lists it in Children. A card
|
|
147
|
+
from the + or New task has none
|
|
148
|
+
Without /<id>, the card takes its ticket's id (GH-<n> for issue #n); pass an id only to import a
|
|
149
|
+
card. Prints "<label> added as <key>": use the key in later commands
|
|
150
|
+
item relate-ask <eventId> <wave>/<id> --summary <t> folds an ask about an existing item into it: the ask's
|
|
151
|
+
card goes, its message and thread show in the item's
|
|
152
|
+
thread marked Moved, then the summary line. Again with
|
|
153
|
+
another item moves it there
|
|
154
|
+
ask progress <eventId> [--next <text>] keeps an answered ask in In Review, --next as its next
|
|
155
|
+
line, until the owner's ask-done (Mark as done, or the Done
|
|
156
|
+
beside Send in its thread) or ask done is newer. Use it when the answer
|
|
157
|
+
waits on the owner, such as a pick between prototypes
|
|
158
|
+
ask done <eventId> moves the ask to Done, as the owner's Done button does
|
|
159
|
+
ask unfold <eventId> moves a folded ask back out to a card of its own
|
|
160
|
+
ask move <eventId> --to <route> sends a New ask to another board in the hub's manifest:
|
|
161
|
+
a New card there with the owner's text, attachments and
|
|
162
|
+
"Moved from <this board>", which wakes its session; here the
|
|
163
|
+
ask goes to Done with a thread line linking to that card.
|
|
164
|
+
A card the ask became is dropped, unless it has started.
|
|
165
|
+
Refuses a route that isn't another board, and an ask
|
|
166
|
+
that isn't New
|
|
167
|
+
checked [--every <minutes> | --idle] data/checked.json: GitHub was just checked, next in
|
|
168
|
+
<minutes> (default the polling window's, as kickflip
|
|
169
|
+
polling prints); run it at the end of each GitHub tick.
|
|
170
|
+
--idle: the board shows no open PR, so never late; with
|
|
171
|
+
neither flag it is idle while no card has an open PR.
|
|
172
|
+
Then sends the queued pushes, each on its own, unless
|
|
173
|
+
pushes are held (overnight, snoozed, the weekend or off)
|
|
174
|
+
lineup create <slug> --title <t> --goal <t> a wave, in the owner's words: a named set of cards in
|
|
175
|
+
--cards <wave>/<id>,\u2026 (--note <t> | --file <f>) build order, all from one wave file. Each card is then
|
|
176
|
+
blocked by the one before it (its other deps stay), and
|
|
177
|
+
the note goes to lineup-<slug>.md beside the wave file.
|
|
178
|
+
The board's Waves section shows it with its goal and
|
|
179
|
+
order, its cards carry its chip, and Notes opens the
|
|
180
|
+
goal, the order and the note. Refuses a card already in
|
|
181
|
+
another open wave. Prints the order by card label
|
|
182
|
+
lineup add <slug> <wave>/<id> [--after <wave>/<id>] puts a card in the wave, last unless --after, and chains
|
|
183
|
+
the order again
|
|
184
|
+
lineup remove <slug> <wave>/<id> takes a card out: it loses its links to the wave's cards,
|
|
185
|
+
and the card after it is blocked by the one before
|
|
186
|
+
lineup set <slug> [--title <t>] [--goal <t>] renames the wave, sets its goal or rewrites its note
|
|
187
|
+
[--note <t> | --file <f>]
|
|
188
|
+
lineup finish <slug> the wave leaves the Waves section; its cards keep their
|
|
189
|
+
links. A wave whose cards are all Done leaves it too
|
|
190
|
+
The Waves section's Triage sends wave-triage {lineup}, or {wave} for a wave file's row; the row reads
|
|
191
|
+
Triaging\u2026 until you reply to that event, then "Triage: N open questions" while questions on topic
|
|
192
|
+
lineup:<slug> or wave:<slug> are open or answered
|
|
193
|
+
tickets backfill [--dry-run] gives every open card without a ticket one: the first
|
|
194
|
+
id in its title (or legacy linear list) from the board's
|
|
195
|
+
source is linked, else a new ticket is made. Done cards are
|
|
196
|
+
left alone. Prints each card's result; --dry-run changes and
|
|
197
|
+
calls nothing
|
|
198
|
+
reviewers set --file <json> the board's reviewers in board.json, replacing them:
|
|
199
|
+
[{name, slack_user, dm, github}], as in wave.json.
|
|
200
|
+
The settings page lists them; new waves copy them
|
|
201
|
+
reviewers add --file <json> one reviewer or an array; replaces the same name
|
|
202
|
+
reviewers remove <name>... refuses a name the board doesn't have
|
|
203
|
+
slack Slack identity as JSON; imports slack-bridge config once
|
|
204
|
+
slack set [--self-dm D..] [--user U..] sets hub Slack identity in ~/.claude/kickflip/slack.json
|
|
205
|
+
[--domain x] [--timezone Area/City]
|
|
206
|
+
review sent <wave>/<id> <reviewer> --ts <ts> records the PR link's message in the item
|
|
207
|
+
--channel <dm>
|
|
208
|
+
review react <wave>/<id> <reviewer> <emoji> records a reaction once on that reviewer's link
|
|
209
|
+
integrations readiness rows as one JSON line
|
|
210
|
+
integrations report --slack yes|no with --site: reports whether this session has Claude's
|
|
211
|
+
Slack connector, in ~/.claude/kickflip/integrations.json
|
|
212
|
+
review due one JSON line of the Slack sends owed, from local state:
|
|
213
|
+
{dms: [{key, reviewer, channel, text, record}], reactions:
|
|
214
|
+
[{key, reviewer, channel, ts, emoji, record}], digest:
|
|
215
|
+
review digest's answer while due (with record), else null}.
|
|
216
|
+
dms: a PR link to each reviewer of an open, non-draft PR with
|
|
217
|
+
no link recorded; reactions: git_merged on each recorded link
|
|
218
|
+
of a merged card. record is the command to run once sent.
|
|
219
|
+
While Slack isn't ready: {waiting} and nothing owed
|
|
220
|
+
review digest pending reviews as JSON; due weekday mornings from 09:00
|
|
221
|
+
review digest sent <reviewer> --ts <ts> records a digest in data/review-digests.json;
|
|
222
|
+
--channel <dm> --items <key,key> marks today sent in the Slack identity timezone
|
|
223
|
+
rules list the hub rules, then with --site the board's, one
|
|
224
|
+
"<hub|board> <id> <owner|claude> <text>" line each
|
|
225
|
+
rules add --text <t> [--hub] [--by claude] a rule in board.json, or with --hub in the hub's
|
|
226
|
+
settings.json for every board; prints its id.
|
|
227
|
+
--by claude when you codify one on the owner's word
|
|
228
|
+
rules edit <id> --text <t> [--hub] rewords a rule, keeping who added it
|
|
229
|
+
rules remove <id> [--hub] refuses an id the list doesn't have
|
|
230
|
+
settings the board's settings as JSON; flags default off, optional
|
|
231
|
+
settings appear when set: {"auto_merge": false,
|
|
232
|
+
"auto_build": false, "auto_pull": false,
|
|
233
|
+
"auto_triage": false, "github_login": "owner"}
|
|
234
|
+
settings set auto-merge on|off on: the session merges an approved PR once CI is green and
|
|
235
|
+
the merge state is CLEAN. off (the default): the card
|
|
236
|
+
shows the owner a Merge button, and the session merges on its
|
|
237
|
+
card-merge event. Board Settings' toggle writes it too
|
|
238
|
+
build <wave>/<id> --site <route> asks the hub to run this card's .kickflip/build; prints
|
|
239
|
+
--asked "<owner's words>" started and exits. install is an alias. Only the owner's
|
|
240
|
+
Serve / Build and Auto-build start a card run, so --asked
|
|
241
|
+
must quote the owner's ask, card message or answer note on
|
|
242
|
+
this board that asked for it; the words go in a thread
|
|
243
|
+
line on the card. The hub records data/try/<id>.log and
|
|
244
|
+
writes failures to the thread. Web Visit opens
|
|
245
|
+
https://<host>:<try port>/ at its root
|
|
246
|
+
main set --install <url> [--install-sha <sha>] publishes main's app install to data/main.json
|
|
247
|
+
settings set github-login <login> the GitHub account Claude posts as on this board. Its
|
|
248
|
+
comments carrying <!-- claude-bridge --> are Claude\u2019s own
|
|
249
|
+
and dropped; its other comments are the owner\u2019s. Unset,
|
|
250
|
+
the hub asks gh api user. Pass "" to clear it
|
|
251
|
+
settings set auto-build on|off on: each push builds its card. Off by default.
|
|
252
|
+
Main builds after every merge either way
|
|
253
|
+
settings set auto-pull on|off on: while no builder runs and nothing waits in Orchestrator
|
|
254
|
+
review, the board server sends card-pull {key} for the top
|
|
255
|
+
Todo card nothing blocks, holds or asks about, ready cards
|
|
256
|
+
first; with none, card-plan {key} for the top unready
|
|
257
|
+
card to grill or prototype. Off by default.
|
|
258
|
+
Only on the owner's word
|
|
259
|
+
settings set auto-triage on|off on: while the board is idle and Todo changed since the last
|
|
260
|
+
triage, at most once a day and before any pull, the board
|
|
261
|
+
server declares topic todo-triage-<date> ("Todo triage
|
|
262
|
+
<date>") and sends todo-triage {date, topic}; the pull
|
|
263
|
+
waits until you reply to it. Off by default. Only on the
|
|
264
|
+
owner's word
|
|
265
|
+
settings set tickets <json> the board's ticket source, as Board Settings' Tickets row
|
|
266
|
+
saves it: {"source":"internal","prefix":"OPS"},
|
|
267
|
+
{"source":"github","account":"<gh login>"} or
|
|
268
|
+
{"source":"linear","workspace","team","project"}. A
|
|
269
|
+
GitHub account runs the board's issue calls as that gh
|
|
270
|
+
login; without one they run as gh's active account
|
|
271
|
+
builder start <wave>/<id> [--agent <name>] [--kind <claude|codex|prototype|grill|install|\u2026>] [--label <who>]
|
|
272
|
+
[--model <opus|sonnet>] the Claude model running the builder, recorded and printed as
|
|
273
|
+
"model: Claude Sonnet" (kind claude takes builder pick's model;
|
|
274
|
+
codex prints "model: Codex"). The thread's agent line reads it: "Claude
|
|
275
|
+
Sonnet builder started"; the label carries no model word. A
|
|
276
|
+
build refuses the model the hub's Claude model rules out.
|
|
277
|
+
A prototype defaults to sonnet and a grill to opus:
|
|
278
|
+
"Claude Sonnet prototype started"
|
|
279
|
+
[--phase <phase> --progress <detail>] one record per agent on the item; --agent defaults to main.
|
|
280
|
+
[--eta <ISO time>] --phase and --progress take what builder progress takes.
|
|
281
|
+
--kind install: a build of the card's install, whose
|
|
282
|
+
Install badge reads Building\u2026 meanwhile. --kind claude or
|
|
283
|
+
codex refuses the one builder pick rules out; --kind build
|
|
284
|
+
records the pick's kind and prints "kind: <kind>. <reason>";
|
|
285
|
+
on the open record a Build tap started or --dispatch
|
|
286
|
+
opened, it keeps that record's kind and worktree.
|
|
287
|
+
[--dispatch] --dispatch: the session runs this right before it launches
|
|
288
|
+
an agent of any kind (build, prototype, grill) so the record
|
|
289
|
+
is open at launch and the card reads Starting; the agent's
|
|
290
|
+
own builder start takes it up.
|
|
291
|
+
A build (--kind build, claude or codex) on an open card sets
|
|
292
|
+
--status building first, refusing what that refuses;
|
|
293
|
+
Prints "ref: <ref>", the card's label, which commit subjects start with.
|
|
294
|
+
Resolves the card's answered questions, noting "Acted on:
|
|
295
|
+
builder started"
|
|
296
|
+
[--worktree [--repo <dir>]] --worktree: a git worktree for the builder at
|
|
297
|
+
~/.claude/kickflip/worktrees/<id> on a new branch <id> from
|
|
298
|
+
the board's branch of its clone (--repo overrides the clone), kept on
|
|
299
|
+
the record and printed as "worktree: <path>". Refuses when
|
|
300
|
+
that path or branch exists
|
|
301
|
+
builder progress <wave>/<id> --phase <phase> one status line: a phase, reading, building, testing,
|
|
302
|
+
(<detail> | --text <d>) [--agent <name>] checking or landing, and a present-tense detail of at most
|
|
303
|
+
80 characters in plain words, which the owner reads at a
|
|
304
|
+
glance; a longer detail is refused with its length. Hashes,
|
|
305
|
+
test counts and file names go in the hand-back. The thread
|
|
306
|
+
line reads "Testing: <detail>", "Codex testing: <detail>"
|
|
307
|
+
for another agent, and the card "Testing \xB7 <detail>"
|
|
308
|
+
[--eta <ISO time> | --clear-eta] an ETA shows as a line under the card's status
|
|
309
|
+
A progress line, --progress included, needs --site: it is a status line in the item's thread, and
|
|
310
|
+
the card's status shows the newest one from an agent still working
|
|
311
|
+
builder done <wave>/<id> [--agent <name>] marks that agent's record done; the item's thread
|
|
312
|
+
[--next <t>] shows when it started and finished; clears building/fixing
|
|
313
|
+
when the card has no open builder records left. Closing the
|
|
314
|
+
last one of a card left In Progress with no Next is refused
|
|
315
|
+
without --next, which sets it; item set --status checking
|
|
316
|
+
and land close the records themselves
|
|
317
|
+
builder takes --dir <dir> for a wave file in <dir>; its records go in <dir>/agents
|
|
318
|
+
builder pick <wave>/<id> who builds the card, from the hub's Codex share and the pause
|
|
319
|
+
line, and for claude which model, from the hub's Claude model
|
|
320
|
+
and the card's --size and --model: "codex: <reason>" or
|
|
321
|
+
"claude sonnet: <reason>". Pass that kind to builder start
|
|
322
|
+
--kind, and launch that model
|
|
323
|
+
builder call <wave>/<id> <judgment call> appends one line to the agent's open builder record: a
|
|
324
|
+
[--agent <name>] judgment call as the builder makes it, or a "Question: \u2026"
|
|
325
|
+
for the session; refused with no open record. A builder start
|
|
326
|
+
on a record with unverified calls resumes that build, keeping
|
|
327
|
+
its calls and worktree. The thread shows them, collapsed
|
|
328
|
+
builder calls <wave>/<id> [--agent <name>] prints the record's calls, numbered, then "verified <time>"
|
|
329
|
+
or "not verified yet"
|
|
330
|
+
builder verified <wave>/<id> [--agent <name>] the session's mark that it checked the record's calls; land
|
|
331
|
+
[--note <t>] refuses while any record on the card has a call after it
|
|
332
|
+
land <wave>/<id> --text <t> [--repo <dir>] brings the worktree its builder record keeps (else
|
|
333
|
+
[--dir <dir>]
|
|
334
|
+
~/.claude/kickflip/worktrees/<id> of --repo) into main, one line
|
|
335
|
+
per step: refuses while it has uncommitted changes or adds no
|
|
336
|
+
commit; rebases onto main when main moved, refusing on a
|
|
337
|
+
conflict with the rebase undone; refuses unless its full
|
|
338
|
+
node --test suite passes, naming each failing test and
|
|
339
|
+
saving the TAP output under ~/.claude/kickflip/logs;
|
|
340
|
+
fast-forwards main in the repo;
|
|
341
|
+
restarts the hub when its code changed, else each board
|
|
342
|
+
server when board/ changed; removes the worktree and its
|
|
343
|
+
branch. Never pushes. One land runs at a time on the host: a
|
|
344
|
+
second refuses, naming the card landing. With --site, --text <what was checked>
|
|
345
|
+
is required, and once it lands the card closes as item set
|
|
346
|
+
--status done --text does; it refuses first what that close
|
|
347
|
+
would. KICKFLIP_RESTART_CMD='["<cmd>"]' replaces
|
|
348
|
+
the restarts, called with hub or board <route>
|
|
349
|
+
merge <wave>/<id> [--after-approval "<why trivial>"] merges the card's PR (gh pr merge --squash) and records it merged,
|
|
350
|
+
[--confirmed "<owner's words>"] once GitHub reads it approved, its checks green with none
|
|
351
|
+
pending and its merge state CLEAN, the test the Merge button
|
|
352
|
+
uses. Refuses, naming each reason ("pending: 2 checks"),
|
|
353
|
+
commits after the newest approval unless --after-approval
|
|
354
|
+
says why they're trivial (its closing line says so), and a
|
|
355
|
+
card with manual steps unless --confirmed quotes the owner.
|
|
356
|
+
A refusal also goes in the thread while the owner's Merge waits
|
|
357
|
+
arch file <wave>/<id> turns the architecture review audit card's answered findings
|
|
358
|
+
into cards in the review's wave: titled with the picked
|
|
359
|
+
option's label, its detail, practice and the owner's note as
|
|
360
|
+
notes, each after the wave's last card, its question resolved
|
|
361
|
+
"Filed as <ref>."; a Skip pick resolves its question alone.
|
|
362
|
+
Prints "filed: <ref> from <id>", "skipped: <id>", and
|
|
363
|
+
"waiting: <ids>" while any are unanswered; once none is, it
|
|
364
|
+
closes the audit card as done
|
|
365
|
+
push --kind action|prompt|fault --title <t> runs kickflip push with these flags, adding
|
|
366
|
+
[--body <b>] [--url </route/path>] "\xB7 and N replies" to the body for the replies
|
|
367
|
+
[--tag <tag>] [--now | --urgent] written since the site's last push through this
|
|
368
|
+
command (none before its first). The board server pushes
|
|
369
|
+
each card's Action needed itself, so this is for a fault,
|
|
370
|
+
what only the owner can fix. It records the push in
|
|
371
|
+
data/push-state.json only when kickflip push succeeds,
|
|
372
|
+
and exits with kickflip push's code. Refused without an
|
|
373
|
+
allowed --kind: news stays on the board. Without --now
|
|
374
|
+
it queues until the tick ends (checked) or the hub's
|
|
375
|
+
next check; --now sends at once unless pushes are held
|
|
376
|
+
(overnight, snoozed or the weekend). --urgent: a
|
|
377
|
+
production fault only; it beats those holds. While
|
|
378
|
+
notifications are off every push is dropped, urgent too
|
|
379
|
+
KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class o extends Error{static{n(this,"UsageError")}}const V=new Set(["clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","hub","clear-install","force","worktree","dry-run","dispatch"]),X=new Set(["take"]);function Z(e){const t=Object.create(null),s=[];for(let a=0;a<e.length;a++){const r=e[a];if(!r.startsWith("--")){s.push(r);continue}const d=r.slice(2);if(X.has(d)){if(a+1>=e.length)throw new o(`--${d} needs a value`);(t[d]||=[]).push(e[++a]);continue}if(Object.hasOwn(t,d))throw new o(`--${d} is given twice`);if(V.has(d))t[d]=!0;else if(a+1<e.length)t[d]=e[++a];else throw new o(`--${d} needs a value`)}return{flags:t,rest:s}}n(Z,"parse");const Y=["html","text","note","notes","summary","title","body","next","progress","install-label","confirmed","asked"],ee=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function te(e){const t=e.trim();if(!t||/\s/.test(t))return!1;if(ee.test(t))return!0;try{return w.statSync(t).isFile()}catch{return!1}}n(te,"looksLikePath");function se(e,t){for(const s of Y){if(typeof t[s]!="string"||!te(t[s]))continue;const a=e.includes("file")?`; for a file use --file ${t[s].trim()}`:"";throw new o(`--${s} takes text, not a file path (${t[s].trim()})${a}`)}if(typeof t.file=="string"&&t.file!=="-"&&!w.existsSync(t.file))throw new o(`--file ${t.file}: no such file`)}n(se,"checkInputs");const b=n(e=>w.readFileSync(e==="-"?0:e,"utf8"),"readInput");function v(e){if(!e)throw new o("--file <path> is required");try{return JSON.parse(b(e))}catch(t){throw new i.BoardError(`${e}: ${t.message}`)}}n(v,"readJSONInput");function y(e){if(e.plan!==void 0&&e["notes-file"]!==void 0)throw new o("pass --notes-file or --plan, not both");return e["notes-file"]??e.plan}n(y,"notesFileOf");function O(e){if(e.note!==void 0&&e.file!==void 0)throw new o("pass --note or --file, not both");return e.file===void 0?e.note:b(e.file)}n(O,"noteOf");const g={pr:[["wave/id"],[],async(e,[t])=>({lines:[JSON.stringify(await i.prReport(e,t))]})],reply:[["eventId"],["html","file"],(e,[t],s)=>{if(s.html===void 0==(s.file===void 0))throw new o("reply takes --html or --file");return i.writeReply(e,t,s.html??b(s.file))}],thread:[["wave/id"],["text","link","who","via"],(e,[t],s)=>i.appendThread(e,t,s)],"topic add":[["id"],["title","summary"],(e,[t],s)=>i.addTopic(e,{id:t,...s})],"question add":[[],["file"],async(e,t,s)=>{const a=v(s.file);return{...await i.addQuestions(e,a),asked:[a].flat()}}],"question resolve":[["id"],["note"],(e,[t],s)=>i.resolveQuestion(e,t,s.note)],"prototype add":[["topic"],["title","take","card"],(e,[t],s)=>i.addPrototype(e,{topic:t,title:s.title,card:s.card,takes:s.take})],"prototype list":[[],[],e=>i.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[t])=>i.removePrototype(e,t)],"item set":[["wave/id"],["status","clear-status","next","clear-next","pr","pushed","gh-since","from-ask","state","held","unheld","pick-up-at","notes-file","plan","deps","parent","related","install","install-sha","install-label","clear-install","title","size","model","force","ticket","text"],(e,[t],s,a)=>i.setItem(e,t,{status:s.status,clearStatus:s["clear-status"],next:s.next,clearNext:s["clear-next"],pr:s.pr,pushed:s.pushed,ghSince:s["gh-since"],fromAsk:s["from-ask"],state:s.state,held:s.held,unheld:s.unheld,pickUpAt:s["pick-up-at"],notesFile:y(s),deps:s.deps,parent:s.parent,related:s.related,install:s.install,installSha:s["install-sha"],installLabel:s["install-label"],clearInstall:s["clear-install"],title:s.title,size:s.size,model:s.model,force:s.force,ticket:s.ticket,text:s.text},a)],"item add":[["wave[/id]"],["title","notes","deps","notes-file","plan","ticket","pick-up-at","parent","size","model"],(e,[t],s,a)=>i.addItem(e,t,{title:s.title,notes:s.notes,deps:s.deps,notesFile:y(s),ticket:s.ticket,pickUpAt:s["pick-up-at"],parent:s.parent,size:s.size,model:s.model},a)],"item relate-ask":[["eventId","wave/id"],["summary"],(e,[t,s],a)=>i.relateAsk(e,t,s,a.summary)],"ask progress":[["eventId"],["next"],(e,[t],s)=>i.markAsk(e,t,"progress",s.next)],"ask done":[["eventId"],[],(e,[t])=>i.markAsk(e,t,"done")],"ask unfold":[["eventId"],[],(e,[t])=>i.unfoldAsk(e,t)],"ask move":[["eventId"],["to"],(e,[t],s)=>i.moveAsk(e,t,s.to)],"tickets backfill":[[],["dry-run"],(e,t,s,a)=>i.backfillTickets(e,{dryRun:s["dry-run"]},a)],"lineup create":[["slug"],["title","goal","cards","note","file"],(e,[t],s)=>i.createLineup(e,t,{title:s.title,goal:s.goal,cards:s.cards,note:O(s)})],"lineup add":[["slug","wave/id"],["after"],(e,[t,s],a)=>i.changeLineup(e,t,{add:s,after:a.after})],"lineup remove":[["slug","wave/id"],[],(e,[t,s])=>i.changeLineup(e,t,{remove:s})],"lineup set":[["slug"],["title","goal","note","file"],(e,[t],s)=>i.changeLineup(e,t,{title:s.title,goal:s.goal,note:O(s)})],"lineup finish":[["slug"],[],(e,[t])=>i.finishLineup(e,t)],slack:[[],[],()=>({lines:[JSON.stringify(L())]})],"slack set":[[],["self-dm","user","domain","timezone"],async(e,t,s)=>{const a={"self-dm":"self_dm",user:"owner_slack_user",domain:"slack_domain",timezone:"timezone"};try{return{lines:[JSON.stringify(await H(Object.fromEntries(Object.entries(s).map(([r,d])=>[a[r],d]))))]}}catch(r){throw new i.BoardError(r.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await N.rows())]})],"integrations report":[[],["slack"],async(e,t,s)=>{if(!["yes","no"].includes(s.slack))throw new o("integrations report takes --slack yes|no");if(!e)throw new i.BoardError("integrations report needs --site <route>");const a=m.basename(e);return E(s.slack==="yes",{route:a,title:U().sites?.[a]?.title||(await i.boardConfig(e)).title}),{lines:[`Slack connector reported: ${s.slack}.`]}}],"review sent":[["wave/id","reviewer"],["ts","channel"],(e,[t,s],a)=>i.reviewSent(e,t,s,a)],"review react":[["wave/id","reviewer","emoji"],[],(e,[t,s,a])=>i.reviewReact(e,t,s,a)],"review digest":[[],[],async(e,t,s,a)=>({lines:[JSON.stringify(await i.reviewDigest(e,a))]})],"review due":[[],[],async(e,t,s,a)=>({lines:[JSON.stringify(await i.reviewDue(e,a))]})],"review digest sent":[["reviewer"],["ts","channel","items"],(e,[t],s,a)=>i.reviewDigestSent(e,t,s,a)],"reviewers set":[[],["file"],(e,t,s)=>i.setReviewers(e,v(s.file))],"reviewers add":[[],["file"],(e,t,s)=>i.addReviewers(e,v(s.file))],"reviewers remove":[["...name"],[],(e,t)=>i.removeReviewers(e,t)],"rules list":[[],[],e=>i.listRules(e)],"rules add":[[],["text","hub","by"],(e,t,s)=>i.addRuleTo(e,s)],"rules edit":[["id"],["text","hub"],(e,[t],s)=>i.editRuleIn(e,t,s)],"rules remove":[["id"],["hub"],(e,[t],s)=>i.removeRuleFrom(e,t,s)],settings:[[],[],async e=>({lines:[JSON.stringify(await i.readBoardSettings(e))]})],build:[["wave/id"],["asked"],async(e,[t],s)=>{if(!e)throw new i.BoardError("build needs --site <route>");i.parseKey(t);const a=await i.askedGate(e,t,s.asked);try{const r=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:m.basename(e),key:t}),signal:AbortSignal.timeout(1e4)}),d=await r.json();if(!r.ok)throw new i.BoardError(d.error||"The hub refused the build.");return{lines:["started",...(await i.appendThread(e,t,{text:a})).lines]}}catch(r){throw r instanceof i.BoardError?r:new i.BoardError(`Could not start the build: ${r.message}`)}}],"main set":[[],["install","install-sha"],(e,t,s)=>i.setMainInstall(e,{install:s.install,installSha:s["install-sha"]})],"settings set":[["name","value"],[],(e,[t,s])=>i.setBoardSetting(e,t,s)],"builder start":[["wave/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[t],{worktree:s,repo:a,...r})=>{if(a!==void 0&&!s)throw new o("--repo goes with --worktree");return i.startBuilder(e,t,{...r,worktree:s&&(()=>D(e,t.split("/")[1],{repo:a}))})}],"builder progress":[["wave/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[t,...s],a)=>{if(s.length&&a.text!==void 0)throw new o("builder progress takes its text once: as words or as --text");if(!s.length&&a.text===void 0)throw new o("builder progress needs its text, as words or as --text");return i.builderProgress(e,t,a.text??s.join(" "),{...a,clearEta:a["clear-eta"]})}],"builder done":[["wave/id"],["dir","agent","next"],(e,[t],s)=>i.stopBuilder(e,t,{...s,requireNext:!0})],"builder pick":[["wave/id"],[],(e,[t])=>i.pickBuilder(e,t)],"builder call":[["wave/id","...text"],["dir","agent"],(e,[t,...s],a)=>i.recordCall(e,t,s.join(" "),a)],"builder calls":[["wave/id"],["dir","agent"],(e,[t],s)=>i.listCalls(e,t,s)],"builder verified":[["wave/id"],["dir","agent","note"],(e,[t],s)=>i.verifyCalls(e,t,s)],land:[["wave/id"],["repo","dir","text"],async(e,[t],s)=>{const a=t.split("/")[1];if(e&&s.text===void 0)throw new o(`land needs --text "<what was checked>", which closes ${t} as done once it lands`);if(!e&&s.text!==void 0)throw new o("--text closes the card, which needs --site");return W(e?await i.cardName(e,t):t,async()=>{e&&await i.checkClose(e,t,s.text),await i.checkCallsVerified(e,t,{dir:s.dir});const r=await i.recordedWorktree(e,t,{dir:s.dir}),d=m.resolve(s.repo??r?.repo??M(e).repo);return z({path:r?.path??m.join(G(),a),branch:r?.branch??a,repo:d}),e?i.setItem(e,t,{status:"done",text:s.text}):{lines:[]}})}],merge:[["wave/id"],["after-approval","confirmed"],(e,[t],s)=>i.mergeCard(e,t,{afterApproval:s["after-approval"],confirmed:s.confirmed})],"arch file":[["wave/id"],[],(e,[t])=>e?J(e,t):Promise.reject(new i.BoardError("arch file needs --site <route>"))],checked:[[],["every","idle"],(e,t,s)=>i.writeChecked(e,s.every,{idle:s.idle})],push:[[],["kind","title","body","url","tag","urgent","now"],(e,t,s)=>{const a=K(s.kind);if(a)throw new o(a);if(s.title===void 0)throw new o("push needs --title");return i.sendPush(e,s)}]};g.install=g.build;const l=n((e,t)=>i.cardName(e,t),"card"),C=n((e,t)=>`${t}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),T=n(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function ae(e){const t=e["pick-up-at"];return[e.status,e["clear-status"]&&"status cleared",e.next!==void 0&&(e.next.trim()?"next line set":"next line cleared"),e["clear-next"]&&"next line cleared",e.pr!==void 0&&`PR #${e.pr}`,e.pushed!==void 0&&`pushed ${e.pushed}`,e["gh-since"]&&`GitHub read to ${e["gh-since"]}`,e["from-ask"]&&`from task ${e["from-ask"]}`,e.state,e.held&&"held",e.unheld&&"resumed",t!==void 0&&(t?`held until ${t}`:"resumed"),y(e)!==void 0&&"notes recorded",e.deps!==void 0&&(e.deps?`blocked by ${e.deps}`:"deps cleared"),e.parent!==void 0&&(e.parent?`parent ${e.parent}`:"parent cleared"),e.related!==void 0&&(e.related?`related to ${e.related.split(",").join(", ")}`:"related cleared"),e.install!==void 0&&"install published",e["clear-install"]&&"install cleared",e.title!==void 0&&"renamed",e.ticket!==void 0&&`ticket ${e.ticket}`].filter(Boolean).join(", ")}n(ae,"itemChanges");const ie={reply:n(async(e,[t])=>`reply written to ${await i.eventCard(e,t)}`,"reply"),thread:n(async(e,[t])=>`thread line on ${await l(e,t)}`,"thread"),"topic add":n((e,[t])=>`topic ${t} added`,"topic add"),"question add":n(async(e,t,s,{asked:a})=>{const r=[...new Set(a.map(c=>c.topic))],d=r.length!==1?"":r[0].startsWith("item:")?` on ${await l(e,r[0].slice(5))}`:` on topic ${r[0]}`;return`${C(a.map(c=>c.id),"question")} added${d}`},"question add"),"question resolve":n((e,[t])=>`question ${t} resolved`,"question resolve"),"prototype add":n(async(e,[t],s)=>`prototype ${t} recorded${s.card?` for ${await l(e,s.card)}`:""}`,"prototype add"),"prototype rm":n((e,[t])=>`prototype ${t} removed`,"prototype rm"),"item set":n(async(e,[t],s)=>`${await l(e,t)}: ${ae(s)}`,"item set"),"item add":n(async(e,t,s,{key:a})=>`${await l(e,a)} added as ${a}`,"item add"),"item relate-ask":n(async(e,[t,s])=>`task ${t} folded into ${await l(e,s)}`,"item relate-ask"),"ask progress":n(async(e,[t])=>`${await l(e,`ask:${t}`)}: in progress`,"ask progress"),"ask done":n(async(e,[t])=>`${await l(e,`ask:${t}`)}: done`,"ask done"),"ask unfold":n((e,[t])=>`task ${t} back on a card of its own`,"ask unfold"),"ask move":n((e,[t],s)=>`task ${t} moved to /${s.to}`,"ask move"),checked:n((e,t,s,{every:a})=>a===null?"GitHub checked; no open PR":`GitHub checked; next in ${a} min`,"checked"),"lineup create":n((e,[t])=>`lineup ${t} created`,"lineup create"),"lineup add":n(async(e,[t,s])=>`${await l(e,s)} added to lineup ${t}`,"lineup add"),"lineup remove":n(async(e,[t,s])=>`${await l(e,s)} taken out of lineup ${t}`,"lineup remove"),"lineup set":n((e,[t])=>`lineup ${t} updated`,"lineup set"),"lineup finish":n((e,[t])=>`lineup ${t} finished`,"lineup finish"),"review sent":n(async(e,[t,s])=>`review link to ${s} recorded on ${await l(e,t)}`,"review sent"),"review react":n(async(e,[t,s,a])=>`${a} on ${s}'s review link recorded on ${await l(e,t)}`,"review react"),"review digest sent":n((e,[t])=>`digest to ${t} recorded`,"review digest sent"),"reviewers set":n(()=>"reviewers replaced","reviewers set"),"reviewers add":n(()=>"reviewers added","reviewers add"),"reviewers remove":n((e,t)=>`${C(t,"reviewer")} removed`,"reviewers remove"),"rules add":n((e,t,s)=>`${s.hub?"hub":"board"} rule added`,"rules add"),"rules edit":n((e,[t],s)=>`${s.hub?"hub":"board"} rule ${t} reworded`,"rules edit"),"rules remove":n((e,[t],s)=>`${s.hub?"hub":"board"} rule ${t} removed`,"rules remove"),"builder start":n(async(e,[t],s)=>`${await l(e,t)}: ${T(s)} started`,"builder start"),"builder progress":n(async(e,[t])=>`progress line on ${await l(e,t)}`,"builder progress"),"builder done":n(async(e,[t],s)=>`${await l(e,t)}: ${T(s)} done`,"builder done"),"builder call":n(async(e,[t])=>`judgment call recorded on ${await l(e,t)}`,"builder call"),"builder verified":n(async(e,[t])=>`${await l(e,t)}: judgment calls verified`,"builder verified"),land:n(async(e,[t])=>e?`${await l(e,t)}: landed and done`:null,"land"),merge:n(async(e,[t])=>`${await l(e,t)}: merged`,"merge"),"arch file":n(async(e,[t])=>`findings of ${await l(e,t)} filed`,"arch file")};async function ne(e,t={}){try{const{flags:s,rest:a}=Z(e),r=[3,2,1].map(h=>a.slice(0,h).join(" ")).find(h=>k(g,h)),d=k(g,r);if(!d)throw new o(a.length?`unknown command "${a.slice(0,2).join(" ")}"`:"no command given");const[c,x,R]=d,u=a.slice(r.split(" ").length),_=c.at(-1)?.startsWith("..."),q=c.at(-1)?.endsWith("?")?c.length-1:c.length;if(_?u.length<q:u.length!==c.length)throw new o(`${r} takes ${c.map(h=>`<${h.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:$,...p}=s,S=Object.keys(p).filter(h=>!x.includes(h));if(S.length)throw new o(`${r} does not take ${S.map(h=>"--"+h).join(", ")}`);se(x,p);const I=$===void 0?null:await i.siteDir($),f=await R(I,u,p,t),A=k(ie,r),j=A&&await A(I,u,p,f);j&&console.log(j);for(const h of typeof f=="object"&&f?.lines||[])console.log(h);return 0}catch(s){if(s instanceof o)return console.error(`${s.message} (node board.mjs --help lists the commands)`),2;if(s instanceof i.BoardError)return console.error(s.message),s.exitCode??1;throw s}}if(n(ne,"main"),process.argv[1]&&import.meta.url===B(w.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(Q):process.exitCode=await ne(e)}export{Q as USAGE,ne as main};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var y=Object.defineProperty;var c=(s,o)=>y(s,"name",{value:o,configurable:!0});import g from"node:fs";import m from"node:os";const h=120*1e3,w=5*1e3,_=/(?:\bbuilder|\bagent\.mjs)\s+start\s+["']?([^\s"';|&]+)/g,p=c((s,o)=>{const n=new RegExp(`--${o}(?:=|\\s+)(?:"([^"]*)"|'([^']*)'|([^\\s"';|&)]+))`,"g");let e;for(const r of s.matchAll(n))e=r[1]??r[2]??r[3];return e},"flagIn");function k(s){const o=[];for(const n of s.matchAll(_)){const e=s.slice(n.index),r=e.slice(0,e.search(/\n|;|&&|\|\||\|(?!\|)|$/));o.push({key:n[1],site:p(r,"site")??p(s.slice(0,n.index),"site")??null,agent:p(r,"agent")??"main",dir:p(r,"dir")})}return o}c(k,"startsIn");const $=c(s=>s.replace(/^~(?=\/|$)/,m.homedir()).replace(/^\$(?:HOME|\{HOME\})(?=\/|$)/,m.homedir()),"expandHome");function b(s){const o=[],n=new Map;for(const e of s.split(`
|
|
3
|
+
`)){const r=e.includes('"assistant"')&&/ start/.test(e)&&/builder|agent\.mjs/.test(e),l=n.size&&e.includes("tool_result")&&[...n.keys()].some(i=>e.includes(i));if(!r&&!l)continue;let t;try{t=JSON.parse(e)}catch{continue}const a=Date.parse(t.timestamp),u=Array.isArray(t.message?.content)?t.message.content:[];if(t.type==="assistant"&&!Number.isNaN(a))for(const i of u){if(i?.type!=="tool_use"||typeof i.input?.command!="string")continue;const d=k(i.input.command).map(f=>({...f,from:a-w,to:a+h}));o.push(...d),i.id&&d.length&&n.set(i.id,d)}else if(t.type==="user"&&!Number.isNaN(a))for(const i of u){const d=i?.type==="tool_result"&&n.get(i.tool_use_id);if(d){for(const f of d)f.to=Math.max(f.to,a+h);n.delete(i.tool_use_id)}}}return o}c(b,"startsInTranscript");function S(s){const o=new Set;for(const n of s.split(`
|
|
4
|
+
`)){if(!n.includes('"toolUseResult"')||!/"backgroundTaskId"|"taskId"|"async_launched"/.test(n))continue;let e;try{e=JSON.parse(n).toolUseResult}catch{continue}const r=e?.backgroundTaskId??e?.taskId??(e?.status==="async_launched"?e.agentId:void 0);typeof r=="string"&&o.add(r)}return o}c(S,"backgroundIdsIn");async function I(){const s=JSON.parse(g.readFileSync(0,"utf8")),o=s?.agent_transcript_path;if(typeof o!="string"||!o)throw new Error("no agent_transcript_path on stdin");const n=g.readFileSync(o,"utf8"),e=b(n);if(!e.length)return;const r=S(n);if((s.background_tasks??[]).some(t=>r.has(t.id)))return;const l=await import("../store.mjs");for(const t of e)try{if(t.site!==null&&!/^[\w-]+$/.test(t.site))throw new Error(`--site ${t.site} is not a route`);if(t.dir?.includes("$"))throw new Error(`--dir ${t.dir} names a shell variable`);if(t.key.includes("$")||t.agent.includes("$"))throw new Error("the key or --agent names a shell variable");const a=t.site===null?null:await l.siteDir(t.site),u=t.dir===void 0?void 0:$(t.dir);await l.stopBuilder(a,t.key,{dir:u,agent:t.agent,startedBetween:[t.from,t.to]})}catch(a){process.stderr.write(`builder-stop-hook: ${t.key}${t.agent==="main"?"":` --agent ${t.agent}`}: ${a.message}
|
|
5
|
+
`)}}c(I,"main");try{await I()}catch(s){process.stderr.write(`builder-stop-hook: ${s.message}
|
|
6
|
+
`)}process.exitCode=0;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var i=Object.defineProperty;var e=(r,t)=>i(r,"name",{value:t,configurable:!0});import u from"node:fs";import s from"node:path";function p(r){if(typeof r!="string"||!s.isAbsolute(r))return"Build script must be an absolute path.";let t;try{t=u.lstatSync(r)}catch{return"Build script must be an existing regular file."}return t.isFile()?t.uid!==process.getuid()?"Build script must be owned by the current user.":t.mode&18?"Build script must not be writable by group or others.":t.mode&64?null:"Build script must be executable.":"Build script must be an existing regular file."}e(p,"buildScriptError");export{p as buildScriptError};
|
package/board/domain.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
var et=Object.defineProperty;var u=(e,n)=>et(e,"name",{value:n,configurable:!0});import{routeRecord as E}from"../hub/routes.mjs";import tt from"node:fs";import{ticketBadge as Re,ticketIn as nt}from"./tickets.mjs";import{reviewLinks as st,reviewTime as ot,reviewersOf as rt}from"./review.mjs";import{remoteFromInput as at}from"../hub/repos.mjs";import Me from"node:vm";const xe=Me.createContext({});for(const e of["ui-enums.js","ui-order.js"])Me.runInContext(tt.readFileSync(new URL(`./public/${e}`,import.meta.url),"utf8"),xe);const{BUTTON:G,ACTION_KIND:C,BUILDER_KIND:me}=xe.BoardUI,{laneOrder:lt}=xe.BoardUI,Ne=["item-message","mark-ready","grill-more","prototype-request"],ln=["planned","waiting","building","fixing","checking","held","merged","done","dropped"],it=30*6e4,dt="build-refused",ut=864e5,ke=u(e=>e.lane==="done"||e.lane==="asks","closedLane"),ct=u(e=>`wave:${e}`,"waveFold"),pt=7*ut,Le=u((e,n)=>({id:e,url:n?.workspace?`https://linear.app/${n.workspace}/issue/${e}`:null}),"linearRef"),ft=u((e,n)=>String(e).replace(/^S(\d+)$/,`${n||"INT"}-$1`),"internalRef"),Be=u((e,n,s)=>nt(s,n.ticket)?n.ticket:e.internal===!0?ft(n.id,s?.prefix):n.id,"refOf"),gt=u((e,n,s,o)=>Re(e||(n?{source:"internal",prefix:"INT"}:null),s,o)||(n?{id:String(n),url:null,source:null}:null),"ticketOf");function mt(e,n,s,o){const r=(e.status||"").toLowerCase();return r==="merged"||r==="done"||r==="dropped"||n?.state==="MERGED"||n?.state==="CLOSED"?"done":s||r==="checking"?"progress":o?"review":n&&n.state==="OPEN"&&!n.isDraft?n.checks?.state==="fail"?"progress":"review":n?"progress":"todo"}u(mt,"laneOf");const Te=u(e=>{const n=(e.status||"").toLowerCase();return n==="dropped"||e.pr?.state==="CLOSED"&&n!=="merged"&&n!=="done"},"droppedCard");function kt(e){if(!e.some(o=>o.kind==="work")||e.some(o=>o.lane!=="done"))return null;const n=e.filter(o=>o.lane==="done"),s=n.filter(Te).length;return{merged:n.length-s,dropped:s}}u(kt,"finishedOf");function wt(e,n,s){return Object.fromEntries(e.map(o=>{const r=E(n?.reviews,o.github),d=r==="APPROVED"?"approved":r==="CHANGES_REQUESTED"?"changes":Object.hasOwn(s,o.name)?"sent":"unsent";return[o.name,{state:d,since:d==="sent"?E(s,o.name):null,github:o.github||null}]}))}u(wt,"reviewOf");function ht(e,n,s,o,r){return s?.kind===me.install?"builder":o==="checking"?"checking":s?"builder":r&&e==="review"?"owner":e==="progress"&&n?.state==="OPEN"&&n.isDraft&&n.checks?.state==="pass"?"ready":e==="review"||e==="stale"?n?.reviewDecision==="APPROVED"?"merge":"reviewers":null}u(ht,"waitsOnOf");const Ae=/(^|\/)prototype-[\w-]+\.html/,yt={reviewers:"Waiting for PR reviewers",merge:"Approved, merges once CI and the merge state allow"},je={waiting:"Approved, Merge shows once CI and the merge state allow",ready:"Approved and green: tap Merge",merging:"Merging"},bt="Fix the failing CI",Pe="Tap Done once it is settled",Ue=u(e=>(e.who==="claude"||e.via==="github")&&!["status","agent","ask-link"].includes(e.kind),"countsUnread");function vt({slug:e,wave:n,it:s,order:o,pr:r,repo:d,remote:g,base:c,builder:l,agents:f,worktree:h,eta:i,thread:m,events:S,working:U,said:B,askedAt:K,topicItems:q,boardReviewers:a,now:$,bumpedAt:Q,holdAsk:L,closedAt:T,mergeAt:y,autoMerge:O,tickets:_}){const ne=s.status==="merged"||!!s.merged_at||r?.state==="MERGED"||r?.state==="CLOSED",X=!!T&&String(T)>String(s.status_at||"");X&&(s={...s,status:"done"});const se=String((X?T:s.status_at)||""),M=s.status==="done"&&!ne?[...B.filter(k=>k.kind==="item-message").map(k=>k.ts),...K].map(String).filter(k=>k>se).sort().pop():void 0;M&&(s={...s,status:void 0,next:void 0});const he=!!M&&B.some(k=>String(k.ts)>se&&(k.state==="waiting"||k.state==="working")),W=`${e}/${s.id}`,V=rt(n,a),le=Object.fromEntries(Object.entries(st(n,s,W,q)).map(([k,ae])=>[k,ot(ae.ts)])),R=Object.values(le).filter(Boolean).sort()[0]||null,N=[...m.map(k=>k.ts),...S.map(k=>k.ts)].sort().pop()||null,ye=[...m.filter(k=>Ue({...k,who:k.who||"claude"})).map(k=>k.ts),...S.map(k=>k.reply?.at)].filter(Boolean).sort(),Y=S.filter(k=>k.kind==="grill-more").pop(),F=!s.pr&&s.pushed?.branch?{branch:s.pushed.branch,at:s.pushed.at||null,host:g?.host||null}:null;let x=mt(s,r,f.some(k=>k.started&&!k.done&&te(k)),F);M&&x==="todo"&&(x="progress"),x==="review"&&R&&$-Date.parse(R)>pt&&(x="stale");let I=ht(x,r,l,(s.status||"").toLowerCase(),F);const j=I==="builder"&&r?.state==="OPEN"&&!r.isDraft,ie=x==="progress"&&!I&&r?.state==="OPEN"&&!r.isDraft,z=!!M&&x==="progress"&&!I&&!ie&&!he;z&&(I="owner");const oe=L&&String(L.ts)>String(s.held_at||""),J=x==="todo"&&(oe?!!L.data.hold:s.status==="held"||!!s.held),be=J&&(oe?L.data.until:s.pick_up_at)||null,Z=x==="todo"||!J&&x==="progress"&&I!=="ready"&&I!=="checking",ve=m.filter(k=>(k.who||"claude")==="claude"&&k.kind!=="status").map(k=>String(k.ts)).sort().pop()||"",de=I==="merge"&&!!y&&String(y)>ve,re=m.filter(k=>k.kind!=="status").sort((k,ae)=>String(k.ts).localeCompare(String(ae.ts))).at(-1),ue=x==="todo"&&re?.kind===dt?re.text:null,ce=M||f.filter(te).map(k=>k.started).filter(Boolean).map(String).sort().pop()||s.queued_at||["building","fixing","checking"].includes(String(s.status||"").toLowerCase())&&s.status_at||null,pe=[X&&T,s.status_at,s.merged_at,r?.mergedAt].filter(Boolean).map(String).sort().pop()||null,fe=F&&I==="owner"?`merge ${F.branch} into ${c}`:I==="merge"&&!O?je[de?"merging":"waiting"]:yt[I];return{key:W,kind:"work",order:o,wave:e,waveTitle:n.title||e,id:s.id,ref:Be(n,s,_),title:s.title,status:s.status||null,created:s.created||null,progressSince:ce,closedAt:x==="done"?pe:null,internal:n.internal===!0,repo:n.internal===!0&&!n.repo?null:d,lane:x,waitsOn:I,builder:I==="checking"?null:l,agents:f,worktree:h??null,eta:i,working:U,reopened:!!M,awaitsDone:z,queued:x!=="done"&&!!s.queued_at,deps:s.deps||[],held:J,heldUntil:be,risky:!!s.risky,bumpable:Z,bumpedAt:Z&&Q||null,holdable:x!=="done",hasNotes:!!(s.plan||String(s.notes||"").trim()),finishable:x!=="done",merging:de,refusal:ue,manualSteps:s.manual_steps||[],fromAsk:s.from_ask||null,next:j||x==="done"||I==="checking"?null:ie?bt:fe||s.next||null,branch:s.branch||r?.headRefName||F?.branch||null,pushed:x==="done"?null:F,ticket:_?.source==="linear"&&!s.ticket&&s.linear?.length?null:gt(_,s.ticket,s.ticket||(n.internal?Be(n,s,_):s.linear?.[0]),{repo:d,key:W}),ticketSource:_?.source||null,linear:(s.linear||[]).map(k=>_?.source==="linear"&&Re(_,k,{key:W,repo:d})||Le(k,_)),pr:s.pr&&d?{number:s.pr,url:`https://github.com/${d}/pull/${s.pr}`,...r||{}}:null,reviewers:V.map(k=>k.name),review:wt(V,r,le),inReviewSince:R,threadCount:m.length+S.length,lastTs:N,othersAt:ye,grilling:!!(Y&&!Y.reply)||f.some(k=>k.kind==="grill"&&k.started&&!k.done),state:s.state||null,updatedAt:r?.mergedAt||r?.updatedAt||s.merged_at||l?.at||N||null,ask:null,fold:null,action:null,install:x==="done"?null:St(s.install,r)}}u(vt,"workItem");function St(e,n){if(!e?.url)return null;const s=e.sha?String(e.sha).toLowerCase():null,o=n?.headRefOid?String(n.headRefOid).toLowerCase():null;return{url:e.url,label:e.label||null,builtAt:e.built_at||null,sha:s,older:!!(s&&o&&!o.startsWith(s))}}u(St,"installOf");function Ot(e){if(e?.state!=="OPEN")return[`PR is ${String(e?.state||"unknown").toLowerCase()}`];const{state:n,pending:s,failing:o}=e.checks||{};return[e.isDraft&&"it is a draft",e.reviewDecision!=="APPROVED"&&`review: ${String(e.reviewDecision||"none").toLowerCase().replace(/_/g," ")}`,n==="fail"&&`failing: ${o?.length?o.join(", "):"a check"}`,n==="pending"&&`pending: ${s?H(s,"check"):"checks"}`,e.mergeStateStatus!=="CLEAN"&&`merge state: ${String(e.mergeStateStatus||"unknown").toLowerCase()}`].filter(Boolean)}u(Ot,"mergeBlockers");const $t=u(e=>!Ot(e).length,"mergeable");function xt(e,n,s){if(e.held||ke(e))return null;const o=`#item=${encodeURIComponent(e.key)}`;if(We(e)){const r=n.get(e.questions.first);return Ae.test(r?.url||"")?{kind:C.pick,url:`questions.html#${r.id}`,label:"Prototypes ready: pick a take"}:{kind:C.questions,url:`questions.html#${e.questions.first}`,label:`Answer ${H(e.questions.open,"question")}`}}return e.waitsOn==="ready"?{kind:C.ready,url:e.pr.url,label:"Tap Ready on GitHub"}:e.kind==="ask"&&e.waitsOn==="owner"?{kind:C.done,url:o,label:e.next||Pe}:e.waitsOn==="merge"&&e.manualSteps.length?{kind:C.steps,url:o,label:"Run the manual steps, then merge"}:e.waitsOn==="merge"&&!s&&!e.merging&&$t(e.pr)?{kind:C.merge,url:o,label:"Merge"}:null}u(xt,"actionOf");const We=u(e=>!!(e.questions&&e.questions.state!=="resolved"&&e.questions.open>0),"asking"),Fe=new Set([C.pick,C.questions]);function Bt(e){return ke(e)?null:We(e)?G.openQuestions:e.action?.kind===C.done?G.markDone:e.kind==="ask"?null:G.grill}u(Bt,"actionButtonOf");const At=u(e=>e.kind==="work"&&e.lane==="todo"&&!e.action&&!e.unpicked,"buildableOf");function Et(e,n){const o=e.filter(r=>r.topic===`item:${n}`&&Ae.test(r.url||"")).flatMap(r=>r.answers.map(d=>({q:r,a:d}))).sort((r,d)=>String(r.a.ts).localeCompare(String(d.a.ts))).at(-1);return o&&!(o.q.options||[]).some(r=>r.id===o.a.choice)?{id:o.q.id,note:o.a.note.replace(/\s+/g," ").trim()}:null}u(Et,"noTakePicked");const Ct="No take picked, so it isn't ready to implement",qt=Object.freeze({[C.pick]:G.openQuestions,[C.questions]:G.openQuestions,[C.ready]:G.github,[C.steps]:G.github,[C.merge]:G.merge,[C.done]:G.markDone});function It(e,n,s){e.action=xt(e,n,s),e.awaitsDone&&=!e.action,e.actionButton=Bt(e),e.resolvedBy=qt[e.action?.kind]||null,e.action?.kind===C.merge&&(e.next=je.ready),e.awaitsDone&&Object.assign(e,{lane:"review",next:Pe,bumpable:!1,bumpedAt:null});const o=e.lane==="todo"||e.lane==="progress"&&(e.waitsOn==="ready"||e.waitsOn==="owner");if(!e.action||!o)return;const r=e.lane==="todo"&&Fe.has(e.action.kind)?"todo":"review";Object.assign(e,{lane:r,waitsOn:e.waitsOn==="ready"?"ready":"owner",next:e.action.label}),r==="review"&&Object.assign(e,{bumpable:!1,bumpedAt:null})}u(It,"placeOwnerWait");const Ge=u(e=>e.queued||e.agents.some(te)||["building","fixing"].includes(String(e.status||"").toLowerCase()),"started"),_t="none set yet";function dn(e,n){const s=String(e.status||"").toLowerCase();if(e.next||e.pr||e.pushed||e.held||e.queued_at||["held","checking","done","merged","dropped"].includes(s))return!1;const o=n.filter(r=>r?.started);return!o.some(r=>!r.done)&&Ge({status:s,agents:o})}u(dn,"owesNext");function Dt(e,n=new Date){const s=new Date(e),o=s.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"});return s.toDateString()===n.toDateString()?o:`${s.getMonth()+1}/${s.getDate()} ${o}`}u(Dt,"askedLabel");function Rt(e,n,s,o,r,d,g){const c=n.find(y=>y.kind==="ask"&&y.eventId===e.id),l=e.data?.files||[],f=(c.text||"").trim().split(`
|
|
2
|
+
`)[0]||l.map(y=>y.name).join(", ")||"Ask",h=c.state==="replied"||c.state==="answered",i=r?.state==="progress"&&!(String(d||"")>String(r.at||"")),m=!i&&(!!r||!!d),S=h&&!m&&!i&&g.open>0,U=h&&!m&&!i&&!S&&g.answered>0,B=m||h&&!i&&!S&&!U,K=h&&!o&&!r?.moved&&[...n.filter(y=>y.who==="claude"&&y.kind!=="ask-link"&&y.kind!=="status").map(y=>y.ts),...n.map(y=>y.reply?.at)].filter(Boolean).sort().pop()||null,q=B||i||S||U,a=n.some(y=>y.who==="love"&&y.kind!=="ask"&&(y.state==="waiting"||y.state==="working")),$=q?"answered":c.state,Q=q?null:s||null,L=[...n.filter(Ue).map(y=>y.ts),...n.map(y=>y.reply?.at)].filter(Boolean).sort(),T=[...n.map(y=>y.ts),...L,...i?[r.at]:[]].filter(Boolean).sort().pop();return{key:`ask:${e.id}`,kind:"ask",order:0,wave:null,waveTitle:null,id:e.id,ref:q?Dt(e.ts):"New",isNew:!q,title:f,status:null,internal:!1,repo:null,lane:i?"progress":B?"asks":"todo",waitsOn:i&&!a?"owner":null,builder:null,agents:[],eta:null,working:n.some(y=>y.state==="working"),deps:[],held:!1,risky:!1,bumpable:!q,bumpedAt:Q,manualSteps:[],fromAsk:null,from:null,next:i&&r.next||null,hasNotes:!1,branch:null,finishable:q&&!B,linear:[],pr:null,reviewers:[],review:{},inReviewSince:null,progressSince:i&&r.at||null,closedAt:null,threadCount:n.length,lastTs:T,othersAt:q?L:[],grilling:!1,created:e.ts,updatedAt:q?T:e.ts,ask:{eventId:e.id,state:$,askedAt:e.ts,bumpedAt:Q,edited:!!c.edited,canEdit:!q,text:c.text,files:l,answerAt:K,from:c.from||null},fold:null,action:null}}u(Rt,"askItem");const Mt=new Set(["grill","prototype"]),ee=u(e=>Mt.has(e.kind),"plans"),te=u(e=>!ee(e),"builds");function Ee(e={}){return Object.entries(e).filter(([,n])=>n&&typeof n=="object").map(([n,s])=>({name:n,kind:Qe(s.kind)||null,model:s.model||(s.kind==="opus"||s.kind==="sonnet"?s.kind:null),label:s.label||null,started:s.started||null,eta:s.done?null:s.eta||null,done:s.done||null,tapped:s.tapped===!0,calls:Array.isArray(s.calls)?s.calls:[],verified:s.verified||null})).sort((n,s)=>String(n.started||"").localeCompare(String(s.started||""))||n.name.localeCompare(s.name))}u(Ee,"agentsOf");function Nt(e={}){return Object.values(e).filter(n=>n?.worktree?.path).sort((n,s)=>String(n.started||"").localeCompare(String(s.started||""))).at(-1)?.worktree.path??null}u(Nt,"worktreeOf");function Lt(e,n=[],s){const o=e.filter(i=>i.started&&!i.done),r=o.every(ee),d=r?o:o.filter(te),g=d.map(i=>String(i.started||"")).sort()[0];if(g===void 0)return null;const c=n.filter(i=>i.kind==="status"&&String(i.ts)>=g&&(r||!i.planner)).sort((i,m)=>String(i.ts).localeCompare(String(m.ts))).pop(),l=d.some(i=>i.kind===me.install)?me.install:Qe(d.at(-1).kind),f=c?.ts||g;return{progress:c?c.phase?c.text.replace(": "," \xB7 "):c.text||null:null,phase:c?.phase||null,waiting:!c&&d.some(i=>i.tapped),at:f,kind:l,planner:r,silentSince:f,stalled:s-Date.parse(f)>=it}}u(Lt,"builderOf");const Ce={opus:"Claude Opus",sonnet:"Claude Sonnet",codex:"Codex"},Tt=u(({model:e,kind:n})=>Ce[e]||Ce[n]||(n==="claude"?Ce.opus:null),"modelName"),Qe=u(e=>e==="opus"||e==="sonnet"?"claude":e,"providerOf");function qe(e){const n=Tt(e),{kind:s}=e,o=e.label?.replace(/\bbuilder\b/,"implementer").replace(/\bBuilder\b/,"Implementer"),r=u(g=>g[0].toLowerCase()+g.slice(1),"lower");if(n&&ee(e))return`${n} ${s}`;if(n)return`${n} ${r(o?.replace(/^(?:claude\s+)?(?:opus|sonnet|codex)\b\s*/i,"")||"implementer")}`;const d=s?s[0].toUpperCase()+s.slice(1):"";return d?o?o.toLowerCase().includes(s.toLowerCase())?o:`${d} ${r(o)}`:d:o||"Implementer"}u(qe,"agentName");const Ke=u((e,n)=>!!e.resolved&&(!e.resolved_at||String(e.resolved_at)>=String(n)),"resolvedSince");function jt(e,n,s=[]){const o=n.filter(f=>f.data?.id===e.id),r=o[o.length-1]||null,d=!!(e.resolved&&e.resolved_at&&r&&String(r.ts)>e.resolved_at),g=e.resolved&&!d,c=g?null:s.filter(f=>f.data?.question===e.id&&String(f.ts)>String(r?.ts||"")).at(-1),l=g?"resolved":c?"requested":r?r.reply?"replied":"answered":"open";return{...e,state:l,request:c?{id:c.id,ts:c.ts,note:c.data.note||"",choice:c.data.choice||null}:null,answers:o.map(f=>({ts:f.ts,choice:f.data.choice||null,note:f.data.note||"",files:f.data.files||[],reply:f.reply,acted:Ke(e,f.ts)}))}}u(jt,"ownerQuestion");const we=u(e=>e.round??(Ae.test(e.url||"")?1:0),"roundOf");function Pt(e,n){const s=u((o,r)=>o.confirm_of||(o.options||[]).some(d=>d.id===r.choice),"settles");return e.filter(o=>we(o)===n&&o.answers.length).map(o=>[o,o.answers.at(-1)]).filter(([o,r])=>s(o,r)).map(([,o])=>o).sort((o,r)=>String(o.ts).localeCompare(String(r.ts))).at(-1)}u(Pt,"roundSettledBy");function Ut(e){const n=e.find(r=>r.state==="requested");if(n)return{n:we(n)+1,state:"making"};const s=Math.max(0,...e.map(we));if(!s)return null;const o=Pt(e,s);return o?{n:s,state:"closed",take:o.choice}:e.some(r=>we(r)===s&&r.state!=="resolved")?{n:s,state:"back"}:null}u(Ut,"roundState");function Wt(e,n,s=new Set){const o=new Set(e.map(d=>d.id)),r=[...e];return n.some(d=>!o.has(d.topic))&&r.push({id:"",title:"Other"}),r.map(d=>{const g=n.filter(h=>(o.has(h.topic)?h.topic:"")===d.id),c=u(h=>g.filter(i=>i.state===h).length,"count"),l=g.every(h=>h.state==="resolved")||s.has(d.id)&&!c("open"),f=g.length&&l?"resolved":"open";return{...d,state:f,questions:g.map(h=>h.id),open:c("open"),answered:c("answered"),replied:c("replied"),requested:c("requested"),resolved:c("resolved"),round:Ut(g)}}).filter(d=>d.questions.length)}u(Wt,"ownerTopics");const Ft={"mark-ready":"Mark ready","grill-more":"Grill please","prototype-request":"Prototype please"},Gt=u((e,n)=>{if(e.kind!=="prototype-request"||!n.has(e.data.question))return e.data.text||E(Ft,e.kind)||"";const s=n.get(e.data.question),o=(s.options||[]).find(r=>r.id===e.data.choice);return[`Prototype please:${s.question}`,o&&`${o.id}. ${o.label}`,e.data.note].filter(Boolean).join(`
|
|
3
|
+
\u2192 `)},"actionText"),Ie=u(e=>[...e.filter(n=>n.who==="claude"&&n.kind!=="status"&&n.kind!=="agent").map(n=>n.ts),...e.filter(n=>n.kind==="ask").map(n=>n.reply?.at)].filter(Boolean).map(String).sort().pop()||"","claudeAtOf");function Ve(e,{lines:n=[],events:s=[],owner:o={},working:r={},fromAsk:d=null,links:g=[],agents:c={},moved:l=[]}){const f=e.startsWith("ask:")?[e.slice(4)]:[d,...l].filter(Boolean),h=new Set([e,...f.map(a=>`ask:${a}`)]),i=new Map((o.items||[]).filter(a=>a.topic===`item:${e}`).map(a=>[a.id,a])),m=new Set([...i.values()].map(a=>a.round_of).filter(Boolean)),S=Ee(c).filter(te).map(a=>String(a.started||"")).filter(Boolean),U=new Set(s.filter(a=>a.kind==="owner-answer"&&i.has(a.data?.id)).filter(a=>Ke(i.get(a.data.id),a.ts)||m.has(a.data.id)||S.some($=>$>String(a.ts))).map(a=>a.id)),B=[...Ee(c).flatMap(a=>[...a.started?[{ts:a.started,who:"claude",kind:"agent",text:`${qe(a)} started`,finished:!1,eta:a.done?null:a.eta,...ee(a)?{planner:!0}:{}}]:[],...a.done?[{ts:a.done,who:"claude",kind:"agent",text:`${qe(a)} finished`,finished:!0,eta:null,...ee(a)?{planner:!0}:{}}]:[],...a.calls.length?[{ts:a.done||a.calls.at(-1).at,who:"claude",kind:"calls",text:qe(a),calls:a.calls.map($=>$.text),verified:a.verified}]:[]]),...n.map(a=>({ts:a.ts,who:a.who||"claude",...a.kind==="status"?{kind:"status",...a.planner?{planner:!0}:{}}:{},text:a.text||"",html:a.html||null,link:a.link||null,via:a.via||null})),...s.filter(a=>h.has(a.data?.key)&&Ne.includes(a.kind)).map(a=>({ts:a.ts,who:"love",via:"site",kind:a.kind,text:Gt(a,i),files:a.data.files||[],eventId:a.id,reply:a.reply})),...s.filter(a=>a.kind==="owner-answer"&&i.has(a.data?.id)).map(a=>{const $=[a.data.label||a.data.choice,a.data.note].filter(Boolean).join(" \xB7 ");return{ts:a.ts,who:"love",via:"questions",kind:a.kind,text:`${i.get(a.data.id).question}
|
|
4
|
+
\u2192 ${$}`,files:a.data.files||[],eventId:a.id,reply:a.reply}}),...s.filter(a=>a.kind==="ask"&&f.includes(a.id)).flatMap(a=>[{ts:a.ts,who:"love",via:"ask",kind:a.kind,text:a.data?.text||"",files:a.data?.files||[],eventId:a.id,reply:a.reply,...l.includes(a.id)?{moved:!0}:{},...typeof a.data?.from?.title=="string"?{from:a.data.from.title}:{}},...g.map($=>({ts:a.ts,who:"claude",kind:"ask-link",text:`${$.became?"Became":"Added to"} ${$.ref}`,link:`#item=${encodeURIComponent($.key)}`,html:null,via:null}))])].sort((a,$)=>String(a.ts).localeCompare(String($.ts))),K=Ie(B),q=new Map(s.filter(a=>a.kind==="ask-edited").sort((a,$)=>String(a.ts).localeCompare(String($.ts))).map(a=>[a.data?.id,a.data?.text]));for(const a of B){if(!a.eventId)continue;(a.kind==="ask"||a.kind==="item-message")&&typeof q.get(a.eventId)=="string"&&Object.assign(a,{text:q.get(a.eventId),edited:!0});const $=a.kind==="ask"&&(g.length>0||!!a.moved);a.state=a.reply?"replied":$||U.has(a.eventId)||K>String(a.ts)?"answered":E(r,a.eventId)?"working":"waiting",a.canDelete=a.kind==="item-message"&&(a.state==="working"||a.state==="waiting"),a.canEdit=a.canDelete}return B}u(Ve,"threadOf");function Qt(e,n,s,o,r,d){const g=u((i,m)=>{const S=d.filter(B=>B.kind==="wave-triage"&&B.data?.[i]===m).pop(),U=r.filter(B=>B.topic===`${i}:${m}`&&(B.state==="open"||B.state==="answered")).length;return{running:!!(S&&!S.reply),questions:U}},"triageOf"),c=new Map(o.map(i=>[i.key,i])),l=u(i=>i.filter(m=>m.lane==="done"&&!Te(m)).length,"merged"),f=s.map(i=>({l:i,cards:i.keys.map(m=>c.get(m)).filter(Boolean)})).map(i=>({...i,open:!i.l.finished_at&&i.cards.some(m=>m.lane!=="done")})).sort((i,m)=>Number(i.open)-Number(m.open));for(const{l:i,cards:m}of f)for(const S of m)S.lineup={slug:i.slug,title:i.title};const h=f.filter(i=>i.open).map(({l:i,cards:m})=>({kind:"lineup",slug:i.slug,title:i.title,goal:i.goal,order:m.map(S=>({key:S.key,ref:S.ref})),merged:l(m),total:m.length,triage:g("lineup",i.slug)}));return e.fromWaves?[...h,...n.filter(i=>!i.finished).map(i=>({w:i,cards:o.filter(m=>m.wave===i.slug&&m.kind!=="ask")})).filter(({cards:i})=>i.length).map(({w:i,cards:m})=>({kind:"wave",slug:i.slug,title:i.title,goal:null,order:null,merged:l(m),total:m.length,triage:g("wave",i.slug)}))]:h}u(Qt,"waveRowsOf");function un(e){const{owner:n={},topicItems:s={},events:o=[],threads:r={},prs:d={},working:g={}}=e,c=e.now??Date.now(),l=o.filter(t=>t.data?.key&&Ne.includes(t.kind)),f=new Set(o.filter(t=>t.kind==="ask-withdrawn"&&!t.data?.key).map(t=>t.data?.id)),h=new Map(o.filter(t=>t.kind==="ask"&&!f.has(t.id)).map(t=>[t.id,t])),i=u((t,p)=>new Map(o.filter(w=>w.kind===t).sort((w,A)=>String(w.ts).localeCompare(String(A.ts))).map(w=>[w.data?.[p],w.ts])),"latest"),m=i("ask-bump","id"),S=i("card-unbump","key"),U=new Map([...i("card-bump","key")].filter(([t,p])=>!(String(S.get(t)||"")>String(p)))),B=i("card-done","key"),K=i("card-merge","key"),q=e.autoMerge===!0,a=e.tickets,$=new Map(o.filter(t=>t.kind==="card-hold").sort((t,p)=>String(t.ts).localeCompare(String(p.ts))).map(t=>[t.data?.key,t])),Q=new Map,L=e.askLinks||{},T=new Map;for(const[t,p]of Object.entries(L))h.has(t)&&T.set(p,[...T.get(p)||[],t]);const y=[],O=[],_=new Map,ne=new Map,X=new Map,se=new Map,M=e.github?null:at(e.remote);for(const{slug:t,wave:p,agents:w={}}of e.waves){const A=e.github||p.repo||null;y.push({slug:t,title:p.title||t,status:p.status||null,internal:!!p.internal,finished:null,reviewers:(p.reviewers||[]).map(v=>v.name)}),p.items.forEach((v,P)=>{const D=`${t}/${v.id}`,_e=T.get(D)||[],Oe=[v.from_ask,..._e].filter(b=>h.has(b)),ze=new Set(Oe.map(b=>`ask:${b}`)),De=[...E(r,D)||[],...Oe.flatMap(b=>E(r,`ask:${b}`)||[])];v.from_ask&&Q.set(v.from_ask,[...Q.get(v.from_ask)||[],{key:D,ref:Be(p,v,a),became:!0}]);const ge=Ve(D,{lines:De,events:o,owner:n,working:g,fromAsk:v.from_ask||null,moved:_e});for(const b of ge)b.eventId&&_.set(b.eventId,b.state);ne.set(D,Ie(ge));const Je=v.pr&&A&&E(d,`${A}#${v.pr}`)||null;O.push(vt({slug:t,wave:p,it:v,order:P,repo:A,remote:M,base:e.branch||"main",pr:Je,...(b=>({agents:b,builder:Lt(b,E(r,D),c),eta:b.map(Ze=>Ze.eta).filter(Boolean).sort().pop()||null}))(Ee(E(w,v.id))),worktree:Nt(E(w,v.id)),thread:De,events:[...l.filter(b=>b.data.key===D||ze.has(b.data.key)),...Oe.map(b=>h.get(b))],working:ge.some(b=>b.state==="working"),said:ge.filter(b=>b.who==="love"&&(b.kind==="item-message"||b.kind==="owner-answer")),askedAt:(n.items||[]).filter(b=>b.topic===`item:${D}`&&b.asked_at).map(b=>b.asked_at),topicItems:s,boardReviewers:e.reviewers||[],now:c,bumpedAt:U.get(D),holdAsk:$.get(D),closedAt:B.get(D),mergeAt:K.get(D),autoMerge:q,tickets:a}));const $e=O[O.length-1];se.set(D,{parent:v.parent?Xe(t,v.parent):null,related:(v.related||[]).map(b=>Xe(t,b))}),$e.lane==="done"&&X.set(D,$e.closedAt||String($e.updatedAt||""))})}const he=new Map(O.map(t=>[t.key,t])),W=new Map,V=e.try||{},le=e.autoBuild===!0,R=["web","app"].includes(V.kind)?V.kind:e.waves.some(({wave:t})=>t.items.some(p=>p.install?.url))?"app":"web";for(const t of O){if(t.kind!=="work"||t.lane==="done")continue;const p=E(V.cards,t.key)||{},w=p.head||t.pr?.headRefOid,A=R==="web"?p.sha:t.install?.sha,v=t.builder?.kind===me.install?"running":p.state||(R==="app"&&t.install?"ready":null);t.try={kind:R,state:v,stale:!!(A&&w&&!String(w).toLowerCase().startsWith(String(A).toLowerCase())),url:R==="web"?null:t.install?.url||null,port:R==="web"&&v==="ready"&&p.https||null,sha:A||null,changes:p.ahead>0,script:p.script===!0,autoBuild:le}}const N=V.main,ye=N?.script&&N.sha?{slug:N.slug,branch:N.branch||"main",sha:N.sha.slice(0,7),at:N.at,kind:R,state:N.state||null,url:R==="web"?null:e.main?.install?.url||null,port:R==="web"&&N.state==="ready"&&N.https||null}:null,Y=e.askProgress||{},F=i("ask-done","id"),x=o.filter(t=>t.kind==="owner-answer"),I=o.filter(t=>t.kind==="prototype-request"),j=(n.items||[]).map(t=>jt(t,x,I)),ie=u(t=>{const p=j.filter(w=>w.topic===t);return{open:p.filter(w=>w.state==="open").length,answered:p.filter(w=>w.state==="answered"||w.state==="requested").length}},"pendingOn");for(const t of h.values()){const p=`ask:${t.id}`,w=he.get(E(L,t.id)),A=[...Q.get(t.id)||[]];if(w&&!A.some(P=>P.key===w.key)&&A.push({key:w.key,ref:w.ref,became:!1}),A.length){W.set(p,A[0].key);continue}const v=Ve(p,{lines:E(r,p)||[],events:o,working:g,links:A});for(const P of v)P.eventId&&_.set(P.eventId,P.state);ne.set(p,Ie(v)),O.push(Rt(t,v,m.get(t.id),A.length>0||!!E(L,t.id),E(Y,t.id),F.get(t.id),ie(`item:${p}`)))}for(const t of y){const p=O.filter(w=>w.wave===t.slug);if(t.finished=kt(p),t.finished)for(const w of p)w.lane==="done"&&(w.fold=ct(t.slug))}for(const t of O)t.lineup=null;const z=new Set((n.topics||[]).map(t=>t.id)),oe=e.waves.flatMap(({slug:t,wave:p})=>(p.lineups||[]).map(w=>({...w,keys:w.items.map(A=>`${t}/${A}`)}))),J=u(()=>Wt([...n.topics||[],...O.filter(t=>!z.has(`item:${t.key}`)).map(t=>({id:`item:${t.key}`,title:`${t.ref}: ${t.title}`,...t.kind==="ask"?{askedAt:t.ask.askedAt}:{},summary:"Questions about this work item."})),...oe.filter(t=>!z.has(`lineup:${t.slug}`)).map(t=>({id:`lineup:${t.slug}`,title:t.title,summary:"Wave triage."})),...(e.fromWaves?y:[]).filter(t=>!z.has(`wave:${t.slug}`)).map(t=>({id:`wave:${t.slug}`,title:t.title,summary:"Wave triage."}))],j,new Set(O.filter(ke).map(t=>`item:${t.key}`))),"topicsOf"),be=new Map(J().map(t=>[t.id,t]));for(const t of O){const p=be.get(`item:${t.key}`);if(!p){t.questions=null,t.grilling||=t.state==="grilling";continue}const w=p.questions.filter(v=>j.find(P=>P.id===v)?.state!=="resolved"),A=p.questions.find(v=>j.find(P=>P.id===v)?.state==="open")||w[0]||p.questions[0];t.questions={topic:p.id,state:p.state,open:p.open,answered:p.answered,replied:p.replied,resolved:p.resolved,first:A}}const Z=new Map(O.map(t=>[t.key,t])),ve=new Map(j.map(t=>[t.id,t]));for(const t of O){t.blockedBy=t.kind!=="work"||t.lane==="done"?[]:t.deps.map(w=>Z.get(String(w).includes("/")?w:`${t.wave}/${w}`)).filter(w=>w&&w.lane!=="done"&&!w.reopened).map(He),It(t,ve,q),t.lane==="todo"&&t.kind==="work"&&!t.held&&Ge(t)&&(t.lane="progress"),t.unpicked=t.kind==="work"&&t.lane==="todo"&&!!Et(j,t.key),t.unpicked&&!t.action&&(t.next=Ct),t.buildable=At(t);const p=t.kind==="work"&&t.lane==="todo";t.unsorted=p&&!!t.fromAsk&&!t.state,t.ready=p&&t.state==="ready"&&!t.blockedBy.length&&!t.action,t.sortAt=Ht[t.lane](t)||t.updatedAt||null}Vt(O,Z,se),O.sort(lt);const de=Qt(e,y,oe,O,j,o),re=J(),ue=[...zt(e.prompts),...Yt(O,re,j)],ce=new Map((n.items||[]).map(t=>[t.id,t.topic])),pe=u(t=>t.kind==="owner-answer"?ce.has(t.data?.id)?/^item:(.+)$/.exec(ce.get(t.data.id))?.[1]||null:"":t.kind==="ask"?`ask:${t.id}`:t.data?.key||(t.kind.startsWith("ask-")&&t.data?.id?`ask:${t.data.id}`:null),"ownCardOf"),fe=u(t=>{const p=pe(t);return W.get(p)||p},"cardOf"),k=u(t=>{const p=t.startsWith("ask:")?t.slice(4):null,w=p&&E(Y,p)?.state==="done"?E(Y,p).at:null;return[B.get(t),p&&F.get(p),w,X.get(t)].filter(Boolean).sort().pop()||""},"closedAt"),ae=u(t=>{const p=fe(t);return p===null?!t.reply:!Z.has(p)||!(k(p)<String(E(g,t.id)))?!1:_.has(t.id)?_.get(t.id)==="working":!t.reply&&!W.has(pe(t))&&!(String(ne.get(p)||"")>String(t.ts))},"unanswered"),Se=o.filter(t=>E(g,t.id)&&ae(t)).map(t=>({id:t.id,kind:t.kind,key:fe(t),data:t.data,since:E(g,t.id)}));for(const t of O)t.working=Se.some(p=>p.key===t.key),t.kind==="work"&&t.lane==="progress"&&!t.builder&&!t.next&&t.waitsOn!=="checking"&&!t.working&&!t.queued&&(t.next=_t);const Ye=O.filter(t=>t.ask?.answerAt).sort((t,p)=>p.ask.answerAt.localeCompare(t.ask.answerAt)).map(t=>({key:t.key,id:t.id,text:t.title,at:t.ask.answerAt}));return{kind:R,main:ye,remote:M&&{host:M.host,name:`${M.host}/${M.path}`},waves:y,waveRows:de,items:O,moved:Object.fromEntries(W),answers:Ye,owner:j,ownerTopics:re,needs:ue,needsSummary:Jt(ue),working:Se.length,workingOn:Se,checks:Xt(e,c)}}u(un,"buildBoard");const He=u(e=>({key:e.key,ref:e.ref}),"linkOf"),Xe=u((e,n)=>String(n).includes("/")?String(n):`${e}/${n}`,"linkKey"),Kt=u((e,n)=>e.ref.localeCompare(n.ref,void 0,{numeric:!0}),"byRef");function Vt(e,n,s){const o=new Map,r=new Map,d=new Map,g=u((l,f,h)=>l.set(f,[...l.get(f)||[],h]),"add");for(const[l,f]of s){f.parent&&g(o,f.parent,l);for(const h of f.related)h!==l&&(g(r,l,h),g(r,h,l))}for(const l of e)for(const f of l.blockedBy||[])g(d,f.key,l.key);const c=u((l=[])=>[...new Set(l)].map(f=>n.get(f)).filter(Boolean).map(He).sort(Kt),"cards");for(const l of e){const f=s.get(l.key)?.parent;l.parent=f?{key:f,ref:n.get(f)?.ref||f.slice(f.indexOf("/")+1)}:null,l.followUp=!!f,l.children=c(o.get(l.key)),l.related=c(r.get(l.key)),l.waitsOnIt=c(d.get(l.key))}}u(Vt,"linkCards");const Ht={todo:u(e=>e.created,"todo"),progress:u(e=>e.progressSince,"progress"),review:u(e=>e.inReviewSince,"review"),stale:u(e=>e.inReviewSince,"stale"),done:u(e=>e.closedAt,"done"),asks:u(e=>e.ask.answerAt,"asks")},H=u((e,n)=>`${e} ${n}${e===1?"":"s"}`,"plural");function Xt({checked:e,listen:n,watcher:s,replyAt:o,threads:r={},working:d={},waves:g=[]},c){const l=Number(e?.every)||30,h=[o,s?.since,...Object.values(d),...Object.values(r).flat().filter(m=>(m.who||"claude")==="claude").map(m=>m.ts),...g.flatMap(m=>Object.values(m.agents||{}).flatMap(S=>Object.values(S||{})).map(S=>S?.done||S?.started))].filter(m=>typeof m=="string"&&!Number.isNaN(Date.parse(m))).sort((m,S)=>Date.parse(m)-Date.parse(S)).pop()||null,i=!!e?.idle;return{at:e?.at||null,every:i?null:l,idle:i,late:!i&&!!e?.at&&c-Date.parse(e.at)>2*l*6e4,watcher:{listening:"running",asleep:"asleep",waking:"waking"}[n]||"stopped",claudeAt:h}}u(Xt,"checksOf");function Yt(e,n,s){const o=new Set(e.map(c=>c.questions?.topic).filter(Boolean)),r=u(c=>c.find(l=>s.find(f=>f.id===l)?.state==="open")||c[0],"firstOpen"),d=n.filter(c=>c.state!=="resolved"&&c.open>0&&!o.has(c.id)).map(c=>{const l=`questions.html#${r(c.questions)}`,f=/^(?:lineup|wave):(.+)$/.exec(c.id)?.[1]||null;return{kind:"Question",title:c.title,text:`${c.title} \xB7 ${H(c.open,"question")}`,url:l,topic:c.id,waveRow:f,questions:{open:c.open,url:l},ready:null}}),g=e.filter(c=>!c.held).map(c=>{const l=c.questions&&c.questions.state!=="resolved"&&c.questions.open>0?{open:c.questions.open,url:`questions.html#${c.questions.first}`}:null,f=c.waitsOn==="ready"?c.pr.url:null,h=!l&&!f&&c.action?{label:c.action.label,url:c.action.url}:null;if(!l&&!f&&!h)return null;const i=`${c.ref}: ${c.title}`,m=[i,l&&H(l.open,"question"),f&&"Ready on GitHub",h?.label].filter(Boolean).join(" \xB7 ");return{kind:l?"Question":f?"Tap Ready":"Action needed",title:i,text:m,url:`#item=${encodeURIComponent(c.key)}`,key:c.key,questions:l,ready:f,action:h}}).filter(Boolean);return[...d,...g]}u(Yt,"needsOf");function zt(e=[]){return e.map(n=>{const s=n.questions?.length?`Question: ${n.questions.map(o=>o.question).join(" \xB7 ")}`:`${n.tool}: ${n.detail||n.action}`;return{kind:"Prompt",title:s,text:s,url:"",prompt:n}})}u(zt,"promptLines");function Jt(e){const n=e.filter(r=>r.prompt).length,s=e.reduce((r,d)=>r+(d.questions?.open||0),0),o=e.filter(r=>r.ready).length;return[n&&H(n,"prompt"),s&&H(s,"question"),o&&`${H(o,"draft")} waiting on Ready`].filter(Boolean).join(" \xB7 ")}u(Jt,"needsSummary");function cn(e){const n=u((...s)=>e.items.filter(o=>s.includes(o.lane)).length,"count");return{items:e.needs.map(({kind:s,text:o,url:r})=>({kind:s,text:o,url:r})),working:e.working,summary:e.needsSummary,lanes:{review:n("review","stale"),progress:n("progress"),todo:n("todo")},building:e.items.some(s=>(s.agents||[]).some(o=>!o.done))}}u(cn,"needsAnswer");function pn(e){const n=e.filter(l=>!ke(l)),s=u(l=>(l.agents||[]).filter(f=>!f.done),"running"),o=!n.some(l=>l.waitsOn==="checking"||s(l).some(te)),r=n.filter(l=>l.buildable&&[null,"ready"].includes(l.state??null)&&!l.unsorted&&!l.blockedBy.length&&!l.questions&&!l.grilling&&!l.working),d=[...r.filter(l=>l.state==="ready"),...r.filter(l=>l.state!=="ready")].map(l=>l.key),c=n.some(l=>l.grilling||s(l).some(ee))?[]:n.filter(l=>l.buildable&&l.state!=="ready"&&!l.held&&!l.questions?.open&&!l.questions?.answered&&!l.working).map(l=>l.key);return{idle:o,pullable:d,plannable:c}}u(pn,"pullOf");function fn(e){return e.filter(n=>n.lane==="todo").map(n=>JSON.stringify([n.key,n.title,n.deps||[]])).sort().join(`
|
|
5
|
+
`)}u(fn,"todoOf");const Zt=u(e=>e.startsWith("ask:")?`ask-${e.slice(4)}`:`item-${e}`,"pushTag");function gn(e){const n=u(d=>e.owner.filter(g=>g.topic===d&&g.state==="open").map(({id:g,question:c})=>({id:g,question:c})),"open"),s=[C.ready,C.merge],o=e.items.filter(d=>d.action).map(d=>{const g=Fe.has(d.action.kind)?n(d.questions.topic):[];return{tag:Zt(d.key),kind:d.action.kind,title:`${d.ref}: ${d.action.label}`,body:g[0]?.question||d.title,url:g.length?d.action.url:`#item=${encodeURIComponent(d.key)}`,now:!s.includes(d.action.kind),questions:g}}),r=e.needs.filter(d=>d.topic).map(d=>{const g=n(d.topic);return{tag:`topic-${d.topic}`,kind:C.questions,title:d.text,body:g[0]?.question||d.title,url:d.url,now:!0,questions:g}});return[...o,...r]}u(gn,"ownerActions");function mn(e,n){const s=e.owner.find(d=>d.id===n),o=/^item:([^/:]+\/[^/]+)$/.exec(s?.topic||"")?.[1],r=o&&e.items.find(d=>d.key===o);return!r||r.kind!=="work"||r.lane==="done"||r.held||r.blockedBy.length||r.action||r.unpicked||r.pr||r.pushed||![null,"planned","waiting"].includes(r.status)||r.agents.some(d=>!d.done)||e.owner.some(d=>d.topic===s.topic&&d.state==="open")?null:o}u(mn,"startedByAnswer");export{C as ACTION_KIND,me as BUILDER_KIND,it as BUILDER_STALLED_AFTER,dt as BUILD_REFUSED,G as BUTTON,Ne as ITEM_EVENTS,ln as ITEM_STATUSES,_t as NO_NEXT,Ct as NO_TAKE,un as buildBoard,te as builds,Ot as mergeBlockers,Tt as modelName,cn as needsAnswer,Et as noTakePicked,dn as owesNext,gn as ownerActions,jt as ownerQuestion,ee as plans,Qe as providerOf,pn as pullOf,Zt as pushTag,Be as refOf,we as roundOf,Pt as roundSettledBy,mn as startedByAnswer,Ve as threadOf,fn as todoOf,ct as waveFold};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var n=Object.defineProperty;var r=(e,t)=>n(e,"name",{value:t,configurable:!0});import d from"node:os";const f=r(()=>Object.values(d.networkInterfaces()).flat().map(e=>e.address),"ownAddresses"),a=r(e=>String(e||"").trim().replace(/^::ffff:/,""),"bare");function l(e,t=f()){const s=e.headers["x-forwarded-for"],o=s?String(s).split(",").at(-1):e.socket.remoteAddress;return t.includes(a(o))}r(l,"fromHost");export{l as fromHost};
|