cloving 1.0.1 → 1.1.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.
Files changed (177) hide show
  1. package/README.md +145 -14
  2. package/dist/src/authority/commands.js +10 -2
  3. package/dist/src/authority/reconciliation.d.ts +2 -0
  4. package/dist/src/authority/reconciliation.js +10 -1
  5. package/dist/src/authority/transitions.d.ts +1 -1
  6. package/dist/src/authority/transitions.js +2 -0
  7. package/dist/src/broker/actor/authority-session.d.ts +2 -0
  8. package/dist/src/broker/actor/authority-session.js +3 -4
  9. package/dist/src/chats/analysis-batches.d.ts +24 -0
  10. package/dist/src/chats/analysis-batches.js +83 -0
  11. package/dist/src/chats/analysis-contract.d.ts +80 -0
  12. package/dist/src/chats/analysis-contract.js +16 -0
  13. package/dist/src/chats/analysis-data.d.ts +3 -0
  14. package/dist/src/chats/analysis-data.js +85 -0
  15. package/dist/src/chats/analysis-input.d.ts +12 -0
  16. package/dist/src/chats/analysis-input.js +83 -0
  17. package/dist/src/chats/analysis-links.d.ts +2 -0
  18. package/dist/src/chats/analysis-links.js +25 -0
  19. package/dist/src/chats/analysis-prompt.d.ts +10 -0
  20. package/dist/src/chats/analysis-prompt.js +16 -0
  21. package/dist/src/chats/analysis-proposal.d.ts +2 -0
  22. package/dist/src/chats/analysis-proposal.js +32 -0
  23. package/dist/src/chats/analysis-provenance.d.ts +4 -0
  24. package/dist/src/chats/analysis-provenance.js +99 -0
  25. package/dist/src/chats/analysis-shape.d.ts +3 -0
  26. package/dist/src/chats/analysis-shape.js +228 -0
  27. package/dist/src/chats/analysis.d.ts +3 -0
  28. package/dist/src/chats/analysis.js +42 -0
  29. package/dist/src/chats/corpus-input.d.ts +5 -0
  30. package/dist/src/chats/corpus-input.js +288 -0
  31. package/dist/src/chats/destination.js +3 -1
  32. package/dist/src/chats/import.d.ts +8 -6
  33. package/dist/src/chats/import.js +195 -23
  34. package/dist/src/chats/input-file.d.ts +3 -0
  35. package/dist/src/chats/input-file.js +32 -0
  36. package/dist/src/chats/normalize.d.ts +11 -2
  37. package/dist/src/chats/normalize.js +18 -2
  38. package/dist/src/chats/previous-import.d.ts +5 -0
  39. package/dist/src/chats/previous-import.js +34 -0
  40. package/dist/src/chats/reader.d.ts +7 -1
  41. package/dist/src/chats/reader.js +46 -15
  42. package/dist/src/chats/redact-import.d.ts +14 -0
  43. package/dist/src/chats/redact-import.js +141 -0
  44. package/dist/src/chats/redaction.d.ts +41 -0
  45. package/dist/src/chats/redaction.js +161 -0
  46. package/dist/src/chats/revision-input.d.ts +3 -0
  47. package/dist/src/chats/revision-input.js +180 -0
  48. package/dist/src/chats/revision.d.ts +48 -0
  49. package/dist/src/chats/revision.js +94 -0
  50. package/dist/src/chats/selection.d.ts +25 -0
  51. package/dist/src/chats/selection.js +109 -0
  52. package/dist/src/chats/source.d.ts +23 -0
  53. package/dist/src/chats/source.js +135 -0
  54. package/dist/src/chats/structure.d.ts +57 -0
  55. package/dist/src/chats/structure.js +216 -0
  56. package/dist/src/cli/arguments.d.ts +8 -0
  57. package/dist/src/cli/arguments.js +94 -13
  58. package/dist/src/cli/client.d.ts +1 -0
  59. package/dist/src/cli/client.js +38 -1
  60. package/dist/src/cli/commands.d.ts +3 -0
  61. package/dist/src/cli/commands.js +29 -11
  62. package/dist/src/cli/main.d.ts +2 -1
  63. package/dist/src/cli/main.js +61 -14
  64. package/dist/src/cli/render.d.ts +2 -0
  65. package/dist/src/cli/render.js +70 -2
  66. package/dist/src/cli/usage-render.d.ts +3 -0
  67. package/dist/src/cli/usage-render.js +28 -0
  68. package/dist/src/cli/usage.d.ts +3 -2
  69. package/dist/src/cli/usage.js +38 -6
  70. package/dist/src/cli/watch-actions.d.ts +10 -0
  71. package/dist/src/cli/watch-actions.js +58 -0
  72. package/dist/src/cli/watch-board-view.d.ts +32 -0
  73. package/dist/src/cli/watch-board-view.js +88 -15
  74. package/dist/src/cli/watch-board.js +6 -1
  75. package/dist/src/cli/watch-card-client.d.ts +1 -0
  76. package/dist/src/cli/watch-card-client.js +44 -0
  77. package/dist/src/cli/watch-card-forms.d.ts +2 -0
  78. package/dist/src/cli/watch-card-forms.js +33 -0
  79. package/dist/src/cli/watch-card-reply.d.ts +5 -0
  80. package/dist/src/cli/watch-card-reply.js +34 -0
  81. package/dist/src/cli/watch-card-view.d.ts +40 -0
  82. package/dist/src/cli/watch-card-view.js +86 -0
  83. package/dist/src/cli/watch-client.d.ts +1 -0
  84. package/dist/src/cli/watch-client.js +118 -0
  85. package/dist/src/cli/watch-model.js +31 -14
  86. package/dist/src/cli/watch-page.d.ts +1 -1
  87. package/dist/src/cli/watch-page.js +71 -85
  88. package/dist/src/cli/watch-pr-board.d.ts +3 -0
  89. package/dist/src/cli/watch-pr-board.js +26 -0
  90. package/dist/src/cli/watch-pr-command.d.ts +15 -0
  91. package/dist/src/cli/watch-pr-command.js +37 -0
  92. package/dist/src/cli/watch-pr-identity.d.ts +20 -0
  93. package/dist/src/cli/watch-pr-identity.js +62 -0
  94. package/dist/src/cli/watch-pr-observer.d.ts +19 -0
  95. package/dist/src/cli/watch-pr-observer.js +108 -0
  96. package/dist/src/cli/watch-pr-settings.d.ts +8 -0
  97. package/dist/src/cli/watch-pr-settings.js +47 -0
  98. package/dist/src/cli/watch-reads.js +4 -0
  99. package/dist/src/cli/watch-render.d.ts +16 -2
  100. package/dist/src/cli/watch-render.js +10 -3
  101. package/dist/src/cli/watch-run-input.d.ts +14 -0
  102. package/dist/src/cli/watch-run-input.js +49 -0
  103. package/dist/src/cli/watch-server.d.ts +3 -0
  104. package/dist/src/cli/watch-server.js +44 -6
  105. package/dist/src/cli/watch-start-view.d.ts +18 -0
  106. package/dist/src/cli/watch-start-view.js +77 -0
  107. package/dist/src/cli/watch-turn.d.ts +1 -0
  108. package/dist/src/cli/watch-turn.js +7 -2
  109. package/dist/src/cli/watch-usage.d.ts +6 -0
  110. package/dist/src/cli/watch-usage.js +65 -0
  111. package/dist/src/cli/watch-view.d.ts +15 -0
  112. package/dist/src/cli/watch-view.js +61 -17
  113. package/dist/src/cli.js +48 -6
  114. package/dist/src/daemon/analysis-launch.d.ts +5 -0
  115. package/dist/src/daemon/analysis-launch.js +81 -0
  116. package/dist/src/daemon/analysis-token.d.ts +6 -0
  117. package/dist/src/daemon/analysis-token.js +88 -0
  118. package/dist/src/daemon/analysis-worker.d.ts +22 -0
  119. package/dist/src/daemon/analysis-worker.js +219 -0
  120. package/dist/src/daemon/chats-files.d.ts +15 -0
  121. package/dist/src/daemon/chats-files.js +113 -0
  122. package/dist/src/daemon/chats-worker.d.ts +12 -0
  123. package/dist/src/daemon/chats-worker.js +200 -0
  124. package/dist/src/daemon/claude-turn.d.ts +10 -0
  125. package/dist/src/daemon/claude-turn.js +96 -15
  126. package/dist/src/daemon/claude-worker.d.ts +4 -1
  127. package/dist/src/daemon/claude-worker.js +99 -47
  128. package/dist/src/daemon/codex-stream.js +16 -0
  129. package/dist/src/daemon/daemon.d.ts +3 -0
  130. package/dist/src/daemon/daemon.js +16 -7
  131. package/dist/src/daemon/github-methods.js +4 -0
  132. package/dist/src/daemon/github-ticket-status.d.ts +3 -0
  133. package/dist/src/daemon/github-ticket-status.js +268 -0
  134. package/dist/src/daemon/issue-queue.d.ts +1 -0
  135. package/dist/src/daemon/issue-queue.js +115 -24
  136. package/dist/src/daemon/operation-journal.d.ts +1 -0
  137. package/dist/src/daemon/operation-journal.js +4 -2
  138. package/dist/src/daemon/retrying-turn.d.ts +9 -0
  139. package/dist/src/daemon/retrying-turn.js +185 -0
  140. package/dist/src/host/world-reconciliation.d.ts +2 -0
  141. package/dist/src/host/world-reconciliation.js +6 -0
  142. package/dist/src/host/world-runs.js +9 -1
  143. package/dist/src/service/run-handlers.js +15 -23
  144. package/dist/src/service/worker-totals.d.ts +3 -0
  145. package/dist/src/service/worker-totals.js +28 -0
  146. package/dist/src/ticket-status.d.ts +8 -0
  147. package/dist/src/ticket-status.js +45 -0
  148. package/dist/src/token-prices.d.ts +19 -0
  149. package/dist/src/token-prices.js +76 -0
  150. package/dist/src/worker-usage.d.ts +22 -0
  151. package/dist/src/worker-usage.js +44 -0
  152. package/dist/src/workflow-candidate.d.ts +29 -0
  153. package/dist/src/workflow-candidate.js +59 -0
  154. package/dist/src/workflow-contracts.d.ts +2 -0
  155. package/dist/src/workflow-contracts.js +27 -5
  156. package/dist/src/workflow-declaration.d.ts +1 -1
  157. package/dist/src/workflow-declaration.js +3 -1
  158. package/dist/src/workflows.d.ts +1 -0
  159. package/dist/src/workflows.js +1 -0
  160. package/dist/tools/self-host-processes.js +7 -2
  161. package/dist/tools/self-host-workflows.d.ts +1 -1
  162. package/dist/tools/self-host-workflows.js +2 -2
  163. package/dist/tools/self-host.js +6 -6
  164. package/dist/tools/self-update-host.d.ts +7 -0
  165. package/dist/tools/self-update-host.js +48 -1
  166. package/dist/tools/self-update.d.ts +1 -0
  167. package/dist/tools/self-update.js +85 -13
  168. package/dist/workflows/analyze-chats.d.ts +10 -0
  169. package/dist/workflows/analyze-chats.declaration.json +9 -0
  170. package/dist/workflows/analyze-chats.js +73 -0
  171. package/dist/workflows/develop-ticket.d.ts +1 -7
  172. package/dist/workflows/develop-ticket.js +42 -141
  173. package/dist/workflows/issue-labels.d.ts +3 -1
  174. package/dist/workflows/issue-labels.js +34 -0
  175. package/dist/workflows/ticket-prompts.d.ts +35 -0
  176. package/dist/workflows/ticket-prompts.js +133 -0
  177. package/package.json +5 -3
@@ -1,3 +1,7 @@
1
+ import { isTicketStatus } from "../ticket-status.js";
2
+ import { priceFields, statusTurns } from "./watch-usage.js";
3
+ import { priceOf, usageOf } from "../worker-usage.js";
4
+ import { priceText } from "./usage-render.js";
1
5
  import { boardsOf, currentStepOf, roundOf, startWorkflowOf, } from "./watch-board.js";
2
6
  import { decisionOf, phasesOf } from "./watch-phases.js";
3
7
  import { displayRun } from "./watch-paths.js";
@@ -59,7 +63,7 @@ function failed(part) {
59
63
  return [{ kind: "error", text: shown }];
60
64
  }
61
65
  function ended(line) {
62
- const cost = count(line["total_cost_usd"]);
66
+ const cost = priceOf(line["total_cost_usd"]);
63
67
  return [
64
68
  {
65
69
  kind: "result",
@@ -79,13 +83,6 @@ export function activityOf(line) {
79
83
  return parts(value).flatMap(failed);
80
84
  return value["type"] === "result" ? ended(value) : [];
81
85
  }
82
- function costOf(lines) {
83
- const costs = lines
84
- .map((line) => record(parsed(line)))
85
- .filter((value) => value["type"] === "result")
86
- .map((value) => count(value["total_cost_usd"]));
87
- return costs.at(-1) ?? null;
88
- }
89
86
  function concise(items) {
90
87
  return items.map((item, index) => {
91
88
  const before = items[index - 1];
@@ -103,8 +100,9 @@ function shortened(run) {
103
100
  turns: run.turns.map((turn) => ({
104
101
  ...turn,
105
102
  activity: concise(turn.activity.map((item) => {
106
- const cost = item.costUsd;
107
- const spent = cost === undefined ? "" : ` (cost ${cost.toFixed(2)} USD)`;
103
+ const spent = item.kind === "result"
104
+ ? ` (cost ${priceText(item.costUsd, turn.estimatedCostUsd)})`
105
+ : "";
108
106
  return { kind: item.kind, text: `${cut(item.text)}${spent}` };
109
107
  })).slice(-activityLimit),
110
108
  })),
@@ -118,14 +116,26 @@ export function turnOf(records) {
118
116
  const report = record(parsed(text(result["result"])));
119
117
  return {
120
118
  key: records.key,
119
+ operationId: text(started["operationId"]),
120
+ usage: usageOf(report["usage"]),
121
121
  runId: text(operation[2]),
122
122
  target: text(started["target"]),
123
123
  branch: text(started["branch"]),
124
124
  worktree: text(started["worktree"]),
125
125
  startedAt: text(started["startedAt"]),
126
126
  outcome: records.result === null ? "in work" : outcome,
127
- costUsd: costOf(records.lines),
128
- activity: records.lines.flatMap(activityOf).slice(-activityLimit - 1),
127
+ costUsd: priceOf(report["costUsd"]),
128
+ estimatedCostUsd: priceOf(report["estimatedCostUsd"]),
129
+ activity: records.lines
130
+ .flatMap(activityOf)
131
+ .map((item) => item.kind === "result"
132
+ ? {
133
+ kind: item.kind,
134
+ text: item.text,
135
+ ...priceFields(report["costUsd"]),
136
+ }
137
+ : item)
138
+ .slice(-activityLimit - 1),
129
139
  endedAt: stamp(result["endedAt"]),
130
140
  activeAt: records.activeAt ?? null,
131
141
  commits: texts(report["commits"]),
@@ -215,14 +225,21 @@ function runOf(inputs, value) {
215
225
  const workflowId = text(status["workflowId"]);
216
226
  const budget = record(status["budget"]);
217
227
  const ticketId = text(status["ticketId"]);
228
+ const ticketStatus = record(status["outcome"])["ticketStatus"];
218
229
  return {
219
230
  runId,
220
231
  workflowId,
221
232
  ticketId: ticketId === "" ? null : ticketId,
222
233
  title: stamp(status["title"]),
223
234
  outcome: stamp(record(status["outcome"])["kind"]),
235
+ ...(isTicketStatus(ticketStatus) ? { ticketStatus } : {}),
236
+ ...(record(status["outcome"])["issueUpdate"] === "failed"
237
+ ? { issueUpdate: "failed" }
238
+ : {}),
224
239
  latestAt: stamp(status["latestAt"]),
225
- claudeCostUsd: count(status["claudeCostUsd"]) ?? 0,
240
+ claudeCostUsd: priceOf(status["claudeCostUsd"]),
241
+ estimatedCostUsd: priceOf(status["estimatedCostUsd"]),
242
+ usage: usageOf(status["usage"]),
226
243
  currentStep: currentStepOf(list(record(value)["events"]), workflowId),
227
244
  round: roundOf(list(record(value)["events"])),
228
245
  state: text(status["state"]),
@@ -234,7 +251,7 @@ function runOf(inputs, value) {
234
251
  .filter((decision) => record(decision)["runId"] === runId)
235
252
  .map((decision) => decisionCommands(decision, inputs.client)),
236
253
  events: list(record(value)["events"]).map(eventOf),
237
- turns: inputs.turns.filter((turn) => turn.runId === runId),
254
+ turns: statusTurns(inputs.turns, status),
238
255
  phases: phasesOf(list(record(value)["events"]), workflowId),
239
256
  decision: decisionOf(list(record(value)["events"])),
240
257
  feedback: list(status["feedback"]).map(feedbackOf),
@@ -1 +1 @@
1
- export declare function pageDocument(): string;
1
+ export declare function pageDocument(actionToken?: string | null): string;
@@ -1,9 +1,27 @@
1
- import { Board, Card, CardReview, WorkflowList, cardReview, cardStep, completionMark, } from "./watch-board-view.js";
1
+ import { cardReply } from "./watch-card-reply.js";
2
+ import { watchCardClient } from "./watch-card-client.js";
3
+ import { AddCard, CardInput, cardKey, cardSubmitKey, } from "./watch-card-view.js";
4
+ import { watchClient } from "./watch-client.js";
5
+ import { priceOf, tokenCount, usageOf } from "../worker-usage.js";
6
+ import { priceText, usageText, totalsSuffix } from "./usage-render.js";
7
+ import { startPrompts, StartCard, StartRun } from "./watch-start-view.js";
8
+ import { Board, boardSizing, Card, Column, columnCollapsed, CardReview, WorkflowList, cardReview, cardStep, completionMark, } from "./watch-board-view.js";
2
9
  import { boardWorkflow, watchHash, watchLocation, watchTitle, } from "./watch-navigation.js";
3
10
  import { clockTime, completedDetail, counted, elapsed, groupActivity, pageTitle, phaseTime, runName, runTone, selectedRun, toneOf, turnTime, waitingDetail, } from "./watch-render.js";
4
11
  import { Command, Decision, Events, Group, Item, Turn, Work, } from "./watch-turn.js";
5
- import { clove, Banner, BannerDetail, Header, Page, Phases, PullRequest, Review, Run, } from "./watch-view.js";
12
+ import { clove, Banner, BannerDetail, BoardPage, Header, Page, Phases, PullRequest, Review, Run, } from "./watch-view.js";
6
13
  const functions = [
14
+ cardReply,
15
+ cardSubmitKey,
16
+ cardKey,
17
+ AddCard,
18
+ CardInput,
19
+ priceOf,
20
+ tokenCount,
21
+ usageOf,
22
+ priceText,
23
+ usageText,
24
+ totalsSuffix,
7
25
  watchHash,
8
26
  watchLocation,
9
27
  boardWorkflow,
@@ -13,6 +31,8 @@ const functions = [
13
31
  completionMark,
14
32
  CardReview,
15
33
  Card,
34
+ Column,
35
+ columnCollapsed,
16
36
  WorkflowList,
17
37
  Board,
18
38
  counted,
@@ -35,6 +55,9 @@ const functions = [
35
55
  Command,
36
56
  Decision,
37
57
  Events,
58
+ startPrompts,
59
+ StartCard,
60
+ StartRun,
38
61
  BannerDetail,
39
62
  Banner,
40
63
  Phases,
@@ -42,6 +65,7 @@ const functions = [
42
65
  Review,
43
66
  Run,
44
67
  Header,
68
+ BoardPage,
45
69
  Page,
46
70
  ];
47
71
  const light = `
@@ -83,28 +107,57 @@ const style = `
83
107
  :root[data-theme="dark"] {${dark}}
84
108
  * { box-sizing: border-box; }
85
109
  body { margin: 0 auto; max-width: 1800px; padding: 0 20px 96px; }
110
+ body:has(.board-page) { padding-bottom: 0; overflow: hidden; }
111
+ .board-page { height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
112
+ .board-page > header, .board-page > .down { flex-shrink: 0; }
113
+ .start-run { flex-shrink: 0; max-height: 45dvh; overflow-y: auto; }
86
114
  a { color: var(--accent-strong); }
87
115
  header { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; padding: 22px 0 10px; color: var(--muted); font-size: 12px; }
88
116
  .brand { display: inline-flex; align-items: center; gap: 9px; color: var(--ink); font: 800 26px/1.2 var(--heading-font); }
89
117
  .brand svg { width: 30px; height: 30px; color: var(--accent); }
90
- a:focus-visible, button:focus-visible, summary:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; box-shadow: 0 0 0 6px var(--code-bg); }
118
+ a:focus-visible, button:focus-visible, summary:focus-visible, input:focus-visible, textarea:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; box-shadow: 0 0 0 6px var(--code-bg); }
91
119
  h2, h3 { font-family: var(--heading-font); font-weight: 700; }
92
- .workspace { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 24px; margin-top: 20px; }
93
- .workflows { display: flex; flex-direction: column; gap: 8px; }
94
- .workflows a { display: grid; gap: 2px; border-bottom: 3px solid transparent; padding: 10px; color: inherit; text-decoration: none; overflow-wrap: anywhere; }
120
+ .start-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); gap: 16px; margin-top: 12px; }
121
+ .start-card { min-width: 0; padding: 14px; border-radius: 14px; background: var(--surface); box-shadow: inset 0 0 0 1px var(--line); }
122
+ .start-card h3 { margin: 0; }
123
+ .start-card p { margin: 6px 0 12px; }
124
+ .start-card .command { grid-template-columns: minmax(0, 1fr); margin: 0; }
125
+ .start-card button { justify-self: end; }
126
+ .workspace { display: flex; width: 100%; margin-top: 12px; flex: 1; min-height: 0; }
127
+ .workflows { display: flex; flex-direction: row; flex-shrink: 0; gap: 8px; min-width: 0; margin-top: 12px; padding: 6px; overflow-x: auto; overflow-y: hidden; }
128
+ .workflows a { display: grid; flex-shrink: 0; white-space: nowrap; gap: 2px; border-bottom: 3px solid transparent; padding: 10px; color: inherit; text-decoration: none; }
95
129
  .workflows a[aria-current="page"] { border-bottom-color: var(--accent); background: var(--surface); border-radius: 8px 8px 0 0; }
96
130
  .workflows span, .card small { color: var(--muted); font-size: 12px; }
97
- .board { min-width: 0; }
98
- .board h2 { margin: 0 0 14px; font-size: 24px; }
99
- .columns { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(230px, 1fr); gap: 12px; overflow-x: auto; padding: 6px 6px 24px; }
100
- .column { min-width: 0; padding: 10px; border-radius: 14px; box-shadow: inset 0 0 0 1px var(--line); background: var(--surface-2); }
101
- .column-name { margin: 0 0 10px; padding: 8px 4px; color: var(--muted); font: 500 12px/1.5 var(--code-font); }
131
+ .board { width: 100%; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
132
+ .board h2 { margin: 0 0 8px; font-size: 24px; flex-shrink: 0; }
133
+ .columns { display: flex; align-items: flex-start; gap: ${String(boardSizing.gap)}px; flex: 1; min-height: 0; overflow-x: scroll; overflow-y: hidden; padding: 6px 6px 12px; scrollbar-color: var(--muted) var(--surface-2); }
134
+ @supports selector(::-webkit-scrollbar) { .columns { scrollbar-color: auto; } }
135
+ .columns::-webkit-scrollbar { height: 12px; }
136
+ .columns::-webkit-scrollbar-track { background: var(--surface-2); }
137
+ .columns::-webkit-scrollbar-thumb { background: var(--muted); border-radius: 6px; }
138
+ .column { flex: 1 0 ${String(boardSizing.minimumColumnWidth)}px; min-width: ${String(boardSizing.minimumColumnWidth)}px; max-height: 100%; display: flex; flex-direction: column; overflow: hidden; padding: 10px; border-radius: 14px; box-shadow: inset 0 0 0 1px var(--line); background: var(--surface-2); }
139
+ .column-name { display: flex; align-items: center; gap: 6px; flex-shrink: 0; margin: 0; padding: 4px 0 10px; color: var(--muted); font: 500 12px/1.5 var(--code-font); }
140
+ .column-name > span:first-child { flex: 1; }
141
+ .column-cards { min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; }
142
+ .column.collapsed { flex: 0 0 ${String(boardSizing.collapsedColumnWidth)}px; min-width: ${String(boardSizing.collapsedColumnWidth)}px; padding: 6px; }
143
+ .collapsed .column-name { writing-mode: vertical-rl; padding: 0; }
144
+ .column-name button, .start-toggle { color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); margin: 0; }
145
+ .column-name button:hover, .start-toggle:hover { background: var(--surface); }
146
+ .add-card, .add-card-form button { color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); margin: 4px 8px 4px 0; }
147
+ .add-card { width: 100%; text-align: left; }
148
+ .add-card-form { padding: 8px 0; overflow-wrap: anywhere; }
149
+ .add-card-form label { display: block; color: var(--ink); margin-bottom: 8px; }
150
+ .add-card-form input, .add-card-form textarea { display: block; width: 100%; min-width: 0; box-sizing: border-box; padding: 8px; border: 1px solid var(--line); border-radius: 6px; font: inherit; color: var(--ink); background: var(--surface); }
151
+ .add-card-form textarea { resize: vertical; }
152
+ .add-card-form button:disabled { opacity: 0.6; cursor: default; }
153
+ .add-card-form p { font-size: 12px; margin: 8px 0; }
102
154
  .card { display: grid; gap: 6px; margin-bottom: 10px; padding: 12px; border-radius: 14px; box-shadow: inset 0 0 0 1px var(--line), var(--shadow); color: var(--ink); overflow-wrap: anywhere; background: var(--surface); }
103
155
  .card:hover { outline: 1px solid var(--accent-strong); }
104
- .card-run { display: grid; gap: 8px; color: inherit; text-decoration: none; }
105
- .card-run strong { font-family: var(--heading-font); font-weight: 700; }
156
+ .card-run { display: grid; gap: 4px; min-width: 0; color: inherit; text-decoration: none; }
157
+ .card-run strong { font-family: var(--heading-font); font-weight: 700; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
106
158
  .completion-mark { font-size: 12px; }
107
159
  .card-step, .card-review, .card-cost, .card small { font: 400 12px/1.6 var(--code-font); font-variant-numeric: tabular-nums; }
160
+ .card-step, .card-review { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
108
161
  .card-step { color: var(--muted); }
109
162
  .card-step.active { color: var(--accent-strong); }
110
163
  .card-review { color: var(--muted); }
@@ -180,84 +233,17 @@ button:hover { background: var(--code-head); }
180
233
  .events { margin-top: 18px; padding-bottom: 10px; }
181
234
  .events > summary { color: var(--code-fg); }
182
235
  .down { font-weight: 600; }
183
- @media (max-width: 700px) { body { padding-inline: 12px; } .workspace { grid-template-columns: minmax(0, 1fr); gap: 12px; } .workflows { flex-direction: row; overflow-x: auto; padding: 6px; } .workflows a { flex-shrink: 0; } }
236
+ @media (max-width: 700px) { body { padding-inline: 12px; } }
184
237
  `;
185
- const loader = `
186
- const state = { model: null, stale: false, copied: "", label: "", run: "", loading: false, again: false };
187
- function moved(next, followed) {
188
- const arrived = next === "waiting_gate" && next !== state.run;
189
- state.run = next;
190
- if (arrived) scrollTo(0, 0);
191
- else if (followed && next === "running") scrollTo(0, document.body.scrollHeight);
192
- }
193
- function copy(command) {
194
- const shown = (label) => {
195
- state.copied = command;
196
- state.label = label;
197
- draw();
198
- };
199
- navigator.clipboard.writeText(command).then(
200
- () => { shown("Copied"); },
201
- () => { shown("Copy failed"); },
202
- );
203
- }
204
- function draw() {
205
- const { model, stale } = state;
206
- const selected = location.hash.slice(1);
207
- const followed = innerHeight + scrollY >= document.body.scrollHeight - 80;
208
- const copying = { command: state.copied, label: state.label, onCopy: copy };
209
- const now = new Date().toISOString();
210
- const page = component(Page, { model, selected, now, stale, copying });
211
- render(page, document.getElementById("app"));
212
- if (model === null) return;
213
- document.title = watchTitle(model, selected);
214
- const { runId } = watchLocation(selected);
215
- moved(model.runs.find((run) => run.runId === runId)?.state ?? "", followed);
216
- }
217
- async function withSelectedRun(model) {
218
- const { runId } = watchLocation(location.hash);
219
- if (runId === null || model.runs.some((run) => run.runId === runId)) return model;
220
- const detail = await fetch("data.json?runId=" + encodeURIComponent(runId), { cache: "no-store" });
221
- if (detail.status === 404) return model;
222
- if (!detail.ok) throw new Error("watch-run-not-available");
223
- const { run } = await detail.json();
224
- if (run?.runId !== runId) throw new Error("watch-run-mismatch");
225
- return { ...model, runs: [...model.runs, run] };
226
- }
227
- async function load() {
228
- state.again = state.loading;
229
- if (state.loading) return;
230
- state.loading = true;
231
- try {
232
- const reply = await fetch("data.json", { cache: "no-store" });
233
- if (!reply.ok) throw new Error("watch-not-available");
234
- state.model = await withSelectedRun(await reply.json());
235
- state.stale = false;
236
- } catch {
237
- state.stale = true;
238
- }
239
- state.loading = false;
240
- draw();
241
- if (state.again) load();
242
- }
243
- function listen() {
244
- const source = new EventSource("events");
245
- source.onmessage = () => { load(); };
246
- source.onerror = () => { state.stale = true; draw(); };
247
- }
248
- addEventListener("hashchange", () => { state.run = ""; draw(); load(); });
249
- draw();
250
- listen();
251
- setInterval(draw, 1000);
252
- setInterval(load, 15000);
253
- `;
254
- export function pageDocument() {
238
+ export function pageDocument(actionToken = null) {
255
239
  const icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${clove.viewBox}">${clove.paths.map((d) => `<path fill="#0BBF64" d="${d}"/>`).join("")}</svg>`;
256
240
  const script = [
257
241
  'import { h as element, h as component, render } from "./preact.mjs";',
258
242
  `const clove = ${JSON.stringify(clove)};`,
243
+ `const actionToken = ${JSON.stringify(actionToken)};`,
259
244
  ...functions.map((item) => item.toString()),
260
- loader,
245
+ watchCardClient,
246
+ watchClient,
261
247
  ].join("\n");
262
248
  return [
263
249
  "<!doctype html>",
@@ -0,0 +1,3 @@
1
+ import type { PrObserver } from "./watch-pr-observer.ts";
2
+ import type { WatchModel } from "./watch-render.ts";
3
+ export declare function observedBoard(model: WatchModel, observer: PrObserver): WatchModel;
@@ -0,0 +1,26 @@
1
+ function observedCard(context, card) {
2
+ if (card.completionMark?.text !== "Needs attention")
3
+ return card;
4
+ const run = context.model.runs.find((run) => run.runId === card.runId);
5
+ if (run?.workflowId !== "develop-issue" ||
6
+ run.state !== "completed" ||
7
+ run.outcome !== "blocked" ||
8
+ run.pullRequest === null)
9
+ return card;
10
+ return {
11
+ ...card,
12
+ pullRequestObservation: context.observer.observe(run.pullRequest),
13
+ };
14
+ }
15
+ export function observedBoard(model, observer) {
16
+ return {
17
+ ...model,
18
+ workflows: model.workflows.map((workflow) => ({
19
+ ...workflow,
20
+ columns: workflow.columns.map((column) => ({
21
+ ...column,
22
+ cards: column.cards.map((card) => observedCard({ model, observer }, card)),
23
+ })),
24
+ })),
25
+ };
26
+ }
@@ -0,0 +1,15 @@
1
+ import { type PrIdentity, type PrObservation } from "./watch-pr-identity.ts";
2
+ export interface WatchCommandOutput {
3
+ readonly stdout: string;
4
+ readonly stderr: string;
5
+ readonly status: number | null;
6
+ readonly errorCode: string | null;
7
+ }
8
+ export interface WatchCommandRequest {
9
+ readonly command: readonly string[];
10
+ readonly directory: string;
11
+ readonly signal: AbortSignal;
12
+ }
13
+ export type WatchCommandRunner = (request: WatchCommandRequest) => Promise<WatchCommandOutput>;
14
+ export declare function prArguments(identity: PrIdentity): readonly string[];
15
+ export declare function prCommandResult(identity: PrIdentity, output: WatchCommandOutput): PrObservation;
@@ -0,0 +1,37 @@
1
+ import { prResponse, } from "./watch-pr-identity.js";
2
+ export function prArguments(identity) {
3
+ return [
4
+ "api",
5
+ "--method",
6
+ "GET",
7
+ "--hostname",
8
+ identity.host,
9
+ `repos/${identity.owner}/${identity.repository}/pulls/${String(identity.number)}`,
10
+ "--include",
11
+ ];
12
+ }
13
+ function commandFailure(output) {
14
+ const status = Number(/\(HTTP (\d{3})\)/.exec(output.stderr)?.[1] ??
15
+ /^HTTP\/[^ ]+ (\d{3})/m.exec(output.stdout)?.[1]);
16
+ if (status === 429 ||
17
+ /rate limit/i.test(output.stderr) ||
18
+ /x-ratelimit-remaining: 0\s*$/im.test(output.stdout))
19
+ return "rate-limit";
20
+ if (output.status === 4 || [401, 403, 404].includes(status))
21
+ return "access";
22
+ return "command";
23
+ }
24
+ export function prCommandResult(identity, output) {
25
+ if (output.status !== 0 || output.errorCode !== null)
26
+ return { status: "unavailable", failure: commandFailure(output) };
27
+ const envelope = /^HTTP\/[^ ]+ 200[^\n]*\r?\n[\s\S]*?\r?\n\r?\n([\s\S]*)$/.exec(output.stdout);
28
+ if (envelope?.[1] === undefined)
29
+ return { status: "unavailable", failure: "response" };
30
+ try {
31
+ const response = JSON.parse(envelope[1]);
32
+ return prResponse(identity, response);
33
+ }
34
+ catch {
35
+ return { status: "unavailable", failure: "response" };
36
+ }
37
+ }
@@ -0,0 +1,20 @@
1
+ export interface PrIdentity {
2
+ readonly host: string;
3
+ readonly owner: string;
4
+ readonly repository: string;
5
+ readonly number: number;
6
+ }
7
+ export type PrFailure = "configuration" | "identity" | "command" | "access" | "rate-limit" | "response" | "timeout";
8
+ export type PrObservation = {
9
+ readonly status: "merged" | "unmerged";
10
+ } | {
11
+ readonly status: "unavailable";
12
+ readonly failure: PrFailure;
13
+ };
14
+ export declare function prResponse(identity: PrIdentity, value: unknown): PrObservation;
15
+ export type PrRepository = Omit<PrIdentity, "number">;
16
+ export declare function prRepository(origin: string): PrRepository | null;
17
+ export declare function prIdentity(repository: PrRepository, pullRequest: {
18
+ readonly number: number;
19
+ readonly url: string;
20
+ }): PrIdentity | null;
@@ -0,0 +1,62 @@
1
+ import { record } from "../protocol.js";
2
+ function responseNames(fields) {
3
+ const repository = record(record(fields["base"])?.["repo"]) ?? {};
4
+ return [
5
+ fields["html_url"],
6
+ repository["html_url"],
7
+ repository["full_name"],
8
+ repository["name"],
9
+ record(repository["owner"])?.["login"],
10
+ ];
11
+ }
12
+ export function prResponse(identity, value) {
13
+ const fields = record(value) ?? {};
14
+ const expected = `https://${identity.host}/${identity.owner}/${identity.repository}`;
15
+ const strings = responseNames(fields);
16
+ if (typeof fields["number"] !== "number" ||
17
+ strings.some((part) => typeof part !== "string"))
18
+ return { status: "unavailable", failure: "response" };
19
+ const wanted = [
20
+ `${expected}/pull/${String(identity.number)}`,
21
+ expected,
22
+ `${identity.owner}/${identity.repository}`,
23
+ identity.repository,
24
+ identity.owner,
25
+ ];
26
+ if (fields["number"] !== identity.number ||
27
+ strings.some((part, index) => String(part).toLowerCase() !== String(wanted[index]).toLowerCase()))
28
+ return { status: "unavailable", failure: "identity" };
29
+ const merged = fields["merged"];
30
+ if (typeof merged !== "boolean")
31
+ return { status: "unavailable", failure: "response" };
32
+ return { status: merged ? "merged" : "unmerged" };
33
+ }
34
+ export function prRepository(origin) {
35
+ const address = origin
36
+ .replace(/^git@([^:/]+):/, "ssh://git@$1/")
37
+ .replace(/\r?\n$/, "");
38
+ const matched = /^(?:https:\/\/|ssh:\/\/git@)([a-zA-Z0-9.-]+)\/([a-zA-Z0-9-]+)\/([a-zA-Z0-9_.-]+)$/.exec(address);
39
+ const [, host, owner, name] = matched ?? [];
40
+ if (host === undefined || owner === undefined || name === undefined)
41
+ return null;
42
+ if (host
43
+ .split(".")
44
+ .some((label) => !/^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label)))
45
+ return null;
46
+ const repository = name.replace(/\.git$/, "");
47
+ if (![host, owner].every((part) => /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(part)) ||
48
+ ["", ".", ".."].includes(repository))
49
+ return null;
50
+ return {
51
+ host: host.toLowerCase(),
52
+ owner: owner.toLowerCase(),
53
+ repository: repository.toLowerCase(),
54
+ };
55
+ }
56
+ export function prIdentity(repository, pullRequest) {
57
+ const { number, url } = pullRequest;
58
+ if (!Number.isSafeInteger(number) || number <= 0)
59
+ return null;
60
+ const expected = `https://${repository.host}/${repository.owner}/${repository.repository}/pull/${String(number)}`;
61
+ return url.toLowerCase() === expected ? { ...repository, number } : null;
62
+ }
@@ -0,0 +1,19 @@
1
+ import type { WatchPullRequest } from "./watch-render.ts";
2
+ import { type WatchCommandRunner } from "./watch-pr-command.ts";
3
+ import { type PrObservation, type PrRepository } from "./watch-pr-identity.ts";
4
+ export interface PrObserver {
5
+ readonly observe: (pullRequest: WatchPullRequest) => PrObservation | null;
6
+ readonly close: () => void;
7
+ }
8
+ export interface PrClock {
9
+ readonly now: () => number;
10
+ readonly after: (expired: () => void, milliseconds: number) => () => void;
11
+ }
12
+ export interface PrObserverSettings {
13
+ readonly clock?: PrClock;
14
+ readonly repository: PrRepository | null;
15
+ readonly directory: string;
16
+ readonly command: readonly string[] | null;
17
+ readonly execute: WatchCommandRunner;
18
+ }
19
+ export declare function prObserver(settings: PrObserverSettings): PrObserver;
@@ -0,0 +1,108 @@
1
+ import { prArguments, prCommandResult, } from "./watch-pr-command.js";
2
+ import { prIdentity, } from "./watch-pr-identity.js";
3
+ const systemClock = {
4
+ now: Date.now,
5
+ after: (expired, milliseconds) => {
6
+ const timer = setTimeout(expired, milliseconds);
7
+ return () => {
8
+ clearTimeout(timer);
9
+ };
10
+ },
11
+ };
12
+ async function readPr(state, attempt) {
13
+ const { entry, controller } = attempt;
14
+ const deadline = { expired: false };
15
+ const timeout = { status: "unavailable", failure: "timeout" };
16
+ const cancel = state.clock.after(() => {
17
+ deadline.expired = true;
18
+ entry.result = timeout;
19
+ controller.abort();
20
+ }, 5000);
21
+ controller.signal.addEventListener("abort", cancel, { once: true });
22
+ try {
23
+ const output = await Promise.resolve().then(() => state.settings.execute({
24
+ command: [
25
+ ...(state.settings.command ?? []),
26
+ ...prArguments(entry.identity),
27
+ ],
28
+ directory: state.settings.directory,
29
+ signal: controller.signal,
30
+ }));
31
+ return deadline.expired ? timeout : prCommandResult(entry.identity, output);
32
+ }
33
+ catch {
34
+ return deadline.expired
35
+ ? timeout
36
+ : { status: "unavailable", failure: "command" };
37
+ }
38
+ finally {
39
+ cancel();
40
+ controller.signal.removeEventListener("abort", cancel);
41
+ }
42
+ }
43
+ function pump(state) {
44
+ if (state.closed || state.active !== null)
45
+ return;
46
+ const entry = state.queue.shift();
47
+ if (entry === undefined)
48
+ return;
49
+ const controller = new AbortController();
50
+ state.active = controller;
51
+ entry.started = state.clock.now();
52
+ const completed = (result) => {
53
+ entry.result = result;
54
+ entry.pending = false;
55
+ state.active = null;
56
+ pump(state);
57
+ };
58
+ readPr(state, { entry, controller }).then(completed, () => {
59
+ completed({ status: "unavailable", failure: "command" });
60
+ });
61
+ }
62
+ function needsRead(entry, now) {
63
+ if (entry === undefined)
64
+ return true;
65
+ if (entry.pending || entry.result?.status === "merged")
66
+ return false;
67
+ return now - entry.started >= 60000;
68
+ }
69
+ export function prObserver(settings) {
70
+ const state = {
71
+ settings,
72
+ clock: settings.clock ?? systemClock,
73
+ cache: new Map(),
74
+ queue: [],
75
+ active: null,
76
+ closed: false,
77
+ };
78
+ const configured = settings.repository !== null && Boolean(settings.command?.[0]?.trim());
79
+ const observe = (pullRequest) => {
80
+ if (!configured)
81
+ return { status: "unavailable", failure: "configuration" };
82
+ const identity = prIdentity(settings.repository, pullRequest);
83
+ if (identity === null)
84
+ return { status: "unavailable", failure: "identity" };
85
+ const key = JSON.stringify(identity);
86
+ const known = state.cache.get(key);
87
+ if (state.closed || !needsRead(known, state.clock.now()))
88
+ return known?.result ?? null;
89
+ const entry = {
90
+ identity,
91
+ result: known?.result ?? null,
92
+ started: Infinity,
93
+ pending: true,
94
+ };
95
+ state.cache.set(key, entry);
96
+ state.queue.push(entry);
97
+ pump(state);
98
+ return entry.result;
99
+ };
100
+ return {
101
+ observe,
102
+ close: () => {
103
+ state.closed = true;
104
+ state.queue.length = 0;
105
+ state.active?.abort();
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,8 @@
1
+ import type { WatchCommandRunner } from "./watch-pr-command.ts";
2
+ import { type PrObserver } from "./watch-pr-observer.ts";
3
+ export interface WatchPrSettings {
4
+ readonly repository: string | undefined;
5
+ readonly githubCommand: string | undefined;
6
+ readonly execute: WatchCommandRunner;
7
+ }
8
+ export declare function watchPrObserver(settings: WatchPrSettings): Promise<PrObserver>;