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
@@ -0,0 +1,47 @@
1
+ import { prRepository } from "./watch-pr-identity.js";
2
+ import { prObserver } from "./watch-pr-observer.js";
3
+ function githubCommand(given) {
4
+ if (given === undefined)
5
+ return ["gh"];
6
+ try {
7
+ const parsed = JSON.parse(given);
8
+ if (!Array.isArray(parsed) ||
9
+ !parsed.every((part) => typeof part === "string"))
10
+ return null;
11
+ return parsed.map(String);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export async function watchPrObserver(settings) {
18
+ const directory = settings.repository;
19
+ const command = githubCommand(settings.githubCommand);
20
+ const output = directory === undefined
21
+ ? null
22
+ : await settings
23
+ .execute({
24
+ command: [
25
+ "git",
26
+ "config",
27
+ "--local",
28
+ "--get-all",
29
+ "remote.origin.url",
30
+ ],
31
+ directory,
32
+ signal: AbortSignal.timeout(5000),
33
+ })
34
+ .catch(() => null);
35
+ if (output?.errorCode === "ENOENT" || output?.errorCode === "EACCES")
36
+ return {
37
+ observe: () => ({ status: "unavailable", failure: "command" }),
38
+ close: () => undefined,
39
+ };
40
+ const repository = output?.status === 0 ? prRepository(output.stdout) : null;
41
+ return prObserver({
42
+ repository,
43
+ directory: directory ?? "",
44
+ command,
45
+ execute: settings.execute,
46
+ });
47
+ }
@@ -84,6 +84,8 @@ function matchingCompletion(status, ticketIds) {
84
84
  return (status.state === "completed" &&
85
85
  status["workflowId"] === "develop-issue" &&
86
86
  record(status["outcome"])?.["kind"] === "handoff_requested" &&
87
+ record(status["outcome"])?.["ticketStatus"] !== "partial" &&
88
+ record(status["outcome"])?.["issueUpdate"] !== "failed" &&
87
89
  typeof ticketId === "string" &&
88
90
  ticketIds.has(ticketId));
89
91
  }
@@ -171,6 +173,8 @@ function stoppedTicketIds(runs, now) {
171
173
  return new Set(stopped.flatMap((status) => {
172
174
  const ticketId = status["ticketId"];
173
175
  return status.state === "completed" &&
176
+ record(status["outcome"])?.["ticketStatus"] !== "partial" &&
177
+ record(status["outcome"])?.["issueUpdate"] !== "failed" &&
174
178
  typeof ticketId === "string" &&
175
179
  ticketId !== ""
176
180
  ? [ticketId]
@@ -1,9 +1,15 @@
1
+ import type { CardForm } from "./watch-run-input.ts";
2
+ import type { WorkerUsage } from "../worker-usage.ts";
3
+ import type { PrObservation } from "./watch-pr-identity.ts";
1
4
  export interface WatchItem {
2
5
  readonly kind: string;
3
6
  readonly text: string;
4
7
  readonly costUsd?: number;
5
8
  }
6
9
  export interface WatchTurn {
10
+ readonly estimatedCostUsd: number | null;
11
+ readonly operationId: string;
12
+ readonly usage: WorkerUsage;
7
13
  readonly key: string;
8
14
  readonly runId: string;
9
15
  readonly target: string;
@@ -56,9 +62,11 @@ export interface WatchStep {
56
62
  readonly startedAt: string | null;
57
63
  }
58
64
  export interface WatchCard {
65
+ readonly estimatedCostUsd: number | null;
59
66
  readonly runId: string;
60
67
  readonly title: string | null;
61
- readonly claudeCostUsd: number;
68
+ readonly claudeCostUsd: number | null;
69
+ readonly usage: WorkerUsage;
62
70
  readonly currentStep: WatchStep | null;
63
71
  readonly endedAt: string | null;
64
72
  readonly decisionWaiting: boolean;
@@ -66,6 +74,7 @@ export interface WatchCard {
66
74
  readonly greptileScore: number | null;
67
75
  readonly pullRequestNumber: number | null;
68
76
  readonly completionMark: WatchCompletionMark | null;
77
+ readonly pullRequestObservation: PrObservation | null;
69
78
  }
70
79
  export interface WatchCompletionMark {
71
80
  readonly text: string;
@@ -83,11 +92,15 @@ export interface WatchWorkflow {
83
92
  readonly columns: readonly WatchColumn[];
84
93
  }
85
94
  export interface WatchRun {
95
+ readonly issueUpdate?: "failed";
96
+ readonly ticketStatus?: "complete" | "partial";
97
+ readonly estimatedCostUsd: number | null;
86
98
  readonly ticketId: string | null;
87
99
  readonly title: string | null;
88
100
  readonly outcome: string | null;
89
101
  readonly latestAt: string | null;
90
- readonly claudeCostUsd: number;
102
+ readonly claudeCostUsd: number | null;
103
+ readonly usage: WorkerUsage;
91
104
  readonly currentStep: WatchStep;
92
105
  readonly round: number;
93
106
  readonly runId: string;
@@ -108,6 +121,7 @@ export interface WatchRun {
108
121
  readonly mergeCommit: string | null;
109
122
  }
110
123
  export interface WatchModel {
124
+ readonly cardForms?: readonly CardForm[];
111
125
  readonly workflows: readonly WatchWorkflow[];
112
126
  readonly startWorkflowId: string | null;
113
127
  readonly updatedAt: string;
@@ -1,3 +1,4 @@
1
+ import { priceText } from "./usage-render.js";
1
2
  export function counted(count, noun) {
2
3
  return `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
3
4
  }
@@ -49,7 +50,7 @@ export function runTone(run) {
49
50
  return toneOf(run.state);
50
51
  }
51
52
  export function runName(run) {
52
- return run.turns[0]?.target ?? run.runId.slice(0, 12);
53
+ return run.turns[0]?.target || run.runId.slice(0, 12);
53
54
  }
54
55
  export function waitingDetail(run, now) {
55
56
  const opened = run.phases.find((phase) => phase.state === "active");
@@ -60,7 +61,7 @@ export function waitingDetail(run, now) {
60
61
  export function completedDetail(run) {
61
62
  const [first] = run.events;
62
63
  const last = run.events.at(-1);
63
- const cost = run.turns.reduce((sum, turn) => sum + (turn.costUsd ?? 0), 0);
64
+ const cost = priceText(run.claudeCostUsd, run.estimatedCostUsd);
64
65
  const time = first === undefined || last === undefined
65
66
  ? ""
66
67
  : ` in ${elapsed(first.recordedAt, last.recordedAt)}`;
@@ -68,7 +69,13 @@ export function completedDetail(run) {
68
69
  approved: "You approved the work. ",
69
70
  denied: "You denied the work. ",
70
71
  };
71
- return `${verdicts[run.decision] ?? ""}The run completed${time} · cost ${cost.toFixed(2)} USD.`;
72
+ const ticket = run.ticketStatus === undefined
73
+ ? ""
74
+ : ` Ticket status: ${run.ticketStatus}.`;
75
+ const update = run.issueUpdate === "failed"
76
+ ? " Issue status update failed; check the GitHub operation."
77
+ : "";
78
+ return `${verdicts[run.decision] ?? ""}The run completed${time} · cost ${cost}.${ticket}${update}`;
72
79
  }
73
80
  export function phaseTime(phase, now) {
74
81
  if (phase.startedAt === null)
@@ -0,0 +1,14 @@
1
+ import { type ServiceRequest } from "../protocol.ts";
2
+ export interface CardField {
3
+ readonly name: string;
4
+ readonly label: string;
5
+ readonly multiline: boolean;
6
+ readonly optional: boolean;
7
+ }
8
+ export interface CardForm {
9
+ readonly workflowId: string;
10
+ readonly source: "ticket" | "prompt" | "input";
11
+ readonly fields: readonly CardField[];
12
+ }
13
+ export declare function builtinCardForms(): readonly CardForm[];
14
+ export declare function cardRunRequest(forms: readonly CardForm[], value: unknown): ServiceRequest | null;
@@ -0,0 +1,49 @@
1
+ import { record } from "../protocol.js";
2
+ export function builtinCardForms() {
3
+ const entries = [
4
+ ["develop-issue", "ticket", "Issue number"],
5
+ ["develop-ticket", "ticket", "Ticket ID"],
6
+ ["develop-prd", "prompt", "Idea"],
7
+ ["develop-prompt", "prompt", "Task"],
8
+ ];
9
+ return entries.map(([workflowId, name, label]) => ({
10
+ workflowId,
11
+ source: name === "ticket" ? "ticket" : "prompt",
12
+ fields: [{ name, label, multiline: name === "prompt", optional: false }],
13
+ }));
14
+ }
15
+ function validFields(form, fields) {
16
+ if (Object.keys(fields).some((name) => !form.fields.some((field) => field.name === name)))
17
+ return false;
18
+ return form.fields.every((field) => {
19
+ if (field.optional && !Object.hasOwn(fields, field.name))
20
+ return true;
21
+ const text = fields[field.name];
22
+ return typeof text === "string" && text.trim() !== "";
23
+ });
24
+ }
25
+ export function cardRunRequest(forms, value) {
26
+ const body = record(value);
27
+ if (body === undefined ||
28
+ Object.keys(body).some((key) => !["workflowId", "fields", "commandId"].includes(key)))
29
+ return null;
30
+ const { workflowId, commandId } = body;
31
+ const form = forms.find((item) => item.workflowId === workflowId);
32
+ const fields = record(body["fields"]);
33
+ if (form === undefined ||
34
+ fields === undefined ||
35
+ typeof commandId !== "string" ||
36
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(commandId))
37
+ return null;
38
+ if (!validFields(form, fields))
39
+ return null;
40
+ return {
41
+ operation: "run",
42
+ parameters: {
43
+ workflowId,
44
+ source: form.source,
45
+ submission: fields,
46
+ commandId,
47
+ },
48
+ };
49
+ }
@@ -1,5 +1,8 @@
1
+ import { type WatchPrSettings } from "./watch-pr-settings.ts";
1
2
  export declare const defaultWatchPort = 3663;
2
3
  export interface WatchSettings {
4
+ readonly actions?: string;
5
+ readonly pullRequests?: WatchPrSettings;
3
6
  readonly runtimeDirectory: string;
4
7
  readonly port: number;
5
8
  readonly client: string;
@@ -1,3 +1,6 @@
1
+ import { watchAction, watchActions, watchHost, } from "./watch-actions.js";
2
+ import { observedBoard } from "./watch-pr-board.js";
3
+ import { watchPrObserver } from "./watch-pr-settings.js";
1
4
  import { readdir, readFile, stat } from "node:fs/promises";
2
5
  import { createServer, } from "node:http";
3
6
  import { homedir } from "node:os";
@@ -8,6 +11,7 @@ import { turnOf, watchModel } from "./watch-model.js";
8
11
  import { pageDocument } from "./watch-page.js";
9
12
  import { watchStream } from "./watch-stream.js";
10
13
  export const defaultWatchPort = 3663;
14
+ const forbidden = { status: 403, type: "text/plain", body: "refused" };
11
15
  const notFound = { status: 404, type: "text/plain", body: "not found" };
12
16
  const libraryFile = fileURLToPath(import.meta.resolve("preact"));
13
17
  function parsed(text) {
@@ -75,7 +79,7 @@ async function snapshot(watch, runId) {
75
79
  turns(watch),
76
80
  githubRecords(watch),
77
81
  ]);
78
- return watchModel({
82
+ const model = watchModel({
79
83
  ...read,
80
84
  home: homedir(),
81
85
  client: watch.settings.client,
@@ -83,17 +87,34 @@ async function snapshot(watch, runId) {
83
87
  turns: journal,
84
88
  githubRecords: github,
85
89
  });
90
+ const shown = runId === null ? observedBoard(model, watch.observer) : model;
91
+ return {
92
+ ...shown,
93
+ cardForms: watch.actions?.forms.filter((form) => shown.workflows.some((workflow) => workflow.workflowId === form.workflowId)) ?? [],
94
+ };
86
95
  }
87
96
  async function answered(watch, request) {
97
+ if (!watchHost(request))
98
+ return forbidden;
88
99
  const url = new URL(String(request.url), "http://localhost");
89
- const path = url.pathname;
90
- if (request.method !== "GET")
100
+ if (request.method === "GET")
101
+ return readReply(watch, url);
102
+ if (request.method !== "POST" ||
103
+ url.pathname !== "/actions/run" ||
104
+ watch.actions === null)
91
105
  return notFound;
106
+ const body = await watchAction(watch.actions, request).catch(() => null);
107
+ return body === null
108
+ ? forbidden
109
+ : { status: 200, type: "application/json", body };
110
+ }
111
+ async function readReply(watch, url) {
112
+ const path = url.pathname;
92
113
  if (path === "/")
93
114
  return {
94
115
  status: 200,
95
116
  type: "text/html; charset=utf-8",
96
- body: pageDocument(),
117
+ body: pageDocument(watch.actions?.token ?? null),
97
118
  };
98
119
  if (path === "/preact.mjs")
99
120
  return {
@@ -132,9 +153,14 @@ function subscribed(stream, exchange) {
132
153
  changed();
133
154
  request.once("close", stream.subscribe(changed));
134
155
  }
135
- export function watchServer(settings) {
156
+ export async function watchServer(settings) {
157
+ const observer = await observerFor(settings);
136
158
  const watch = {
159
+ actions: settings.actions === "off"
160
+ ? null
161
+ : await watchActions(settings.runtimeDirectory),
137
162
  settings,
163
+ observer,
138
164
  reader: watchReader(settings.runtimeDirectory),
139
165
  streams: new Map(),
140
166
  github: new Map(),
@@ -145,7 +171,9 @@ export function watchServer(settings) {
145
171
  retryMilliseconds: 1000,
146
172
  });
147
173
  const server = createServer((request, response) => {
148
- if (request.method === "GET" && request.url === "/events") {
174
+ if (watchHost(request) &&
175
+ request.method === "GET" &&
176
+ request.url === "/events") {
149
177
  subscribed(stream, { request, response });
150
178
  return;
151
179
  }
@@ -157,6 +185,7 @@ export function watchServer(settings) {
157
185
  });
158
186
  return new Promise((settle) => {
159
187
  server.once("error", (error) => {
188
+ observer.close();
160
189
  stream.close();
161
190
  settings.complain(`watch-not-started: ${error.message}\n`);
162
191
  settle(1);
@@ -166,6 +195,7 @@ export function watchServer(settings) {
166
195
  settings.announce(`Watch page: http://127.0.0.1:${String(bound)}/\n`);
167
196
  });
168
197
  const closed = () => {
198
+ observer.close();
169
199
  stream.close();
170
200
  server.close();
171
201
  server.closeAllConnections();
@@ -174,3 +204,11 @@ export function watchServer(settings) {
174
204
  settings.stopped.then(closed, closed);
175
205
  });
176
206
  }
207
+ function observerFor(settings) {
208
+ if (settings.pullRequests !== undefined)
209
+ return watchPrObserver(settings.pullRequests);
210
+ return Promise.resolve({
211
+ observe: () => ({ status: "unavailable", failure: "configuration" }),
212
+ close: () => undefined,
213
+ });
214
+ }
@@ -0,0 +1,18 @@
1
+ import { type Shown } from "./watch-element.ts";
2
+ import type { Copying, WatchModel } from "./watch-render.ts";
3
+ interface StartPrompt {
4
+ readonly workflowId: string;
5
+ readonly sentence: string;
6
+ readonly prompt: string;
7
+ }
8
+ export declare function startPrompts(): readonly StartPrompt[];
9
+ export declare function StartCard(props: {
10
+ readonly item: StartPrompt;
11
+ readonly copying: Copying;
12
+ }): Shown;
13
+ export declare function StartRun(props: {
14
+ readonly workflows: WatchModel["workflows"];
15
+ readonly copying: Copying;
16
+ readonly shown?: boolean;
17
+ }): Shown;
18
+ export {};
@@ -0,0 +1,77 @@
1
+ import { component, element } from "./watch-element.js";
2
+ import { Command } from "./watch-turn.js";
3
+ export function startPrompts() {
4
+ return [
5
+ {
6
+ workflowId: "develop-issue",
7
+ sentence: "Work on a GitHub issue through a pull request.",
8
+ prompt: `Start a Cloving run for a GitHub issue in this repository.
9
+ Run: cloving run develop-issue --ticket ISSUE_NUMBER
10
+ Use the issue number that I give on the last line.
11
+ Report the run ID. Then run cloving status RUN_ID until the run completes or stops, and tell me the result and the pull request.
12
+ Do not push, merge or change labels yourself.
13
+ Issue number:`,
14
+ },
15
+ {
16
+ workflowId: "develop-ticket",
17
+ sentence: "Work on a ticket with your review of the diff.",
18
+ prompt: `Start a Cloving run for a ticket in this repository.
19
+ Run: cloving run develop-ticket --ticket TICKET_ID
20
+ Use the ticket ID that I give on the last line.
21
+ When the run waits for a decision, run cloving questions, show me the diff and the decision, and give me the approve and deny commands.
22
+ Run only the command that I choose. Do not decide for me.
23
+ Ticket ID:`,
24
+ },
25
+ {
26
+ workflowId: "develop-prd",
27
+ sentence: "Turn an idea into a GitHub issue.",
28
+ prompt: `Start a Cloving run that turns an idea into a GitHub issue.
29
+ Run: cloving run develop-prd --prompt "IDEA"
30
+ Use the idea that I give on the last line as IDEA.
31
+ When the run completes, show me the new issue.
32
+ Add the label cloving: ready only when I tell you to.
33
+ Idea:`,
34
+ },
35
+ {
36
+ workflowId: "develop-prompt",
37
+ sentence: "Do a small task with your review of the diff.",
38
+ prompt: `Start a Cloving run for a small task in this repository.
39
+ Run: cloving run develop-prompt --prompt "TASK"
40
+ Use the task that I give on the last line as TASK.
41
+ When the run waits for a decision, run cloving questions, show me the diff, and give me the approve and deny commands.
42
+ Run only the command that I choose. Do not decide for me.
43
+ Task:`,
44
+ },
45
+ ];
46
+ }
47
+ export function StartCard(props) {
48
+ const { item, copying } = props;
49
+ return element("article", {
50
+ class: "start-card",
51
+ children: [
52
+ element("h3", { children: item.workflowId }),
53
+ element("p", { children: item.sentence }),
54
+ component(Command, {
55
+ label: "Agent prompt",
56
+ command: item.prompt,
57
+ copyLabel: "Copy the prompt",
58
+ copying,
59
+ }),
60
+ ],
61
+ });
62
+ }
63
+ export function StartRun(props) {
64
+ const registered = startPrompts().filter((item) => props.workflows.some((workflow) => workflow.workflowId === item.workflowId));
65
+ const cards = registered.map((item) => component(StartCard, {
66
+ key: item.workflowId,
67
+ item,
68
+ copying: props.copying,
69
+ }));
70
+ return element("section", {
71
+ class: "start-run",
72
+ id: "start-run",
73
+ "aria-label": "Start a run",
74
+ hidden: props.shown !== true,
75
+ children: element("div", { class: "start-cards", children: cards }),
76
+ });
77
+ }
@@ -17,6 +17,7 @@ export declare function Work(props: {
17
17
  export declare function Command(props: {
18
18
  readonly label: string;
19
19
  readonly command: string;
20
+ readonly copyLabel?: string;
20
21
  readonly copying: Copying;
21
22
  }): Shown;
22
23
  export declare function Decision(props: {
@@ -1,3 +1,4 @@
1
+ import { priceText, usageText } from "./usage-render.js";
1
2
  import { component, element } from "./watch-element.js";
2
3
  import { clockTime, counted, groupActivity, turnTime, } from "./watch-render.js";
3
4
  export function Item(props) {
@@ -30,7 +31,7 @@ export function Turn(props) {
30
31
  const { turn, now } = props;
31
32
  const groups = groupActivity(turn.activity);
32
33
  const working = turn.outcome === "in work";
33
- const cost = turn.costUsd === null ? "" : `${turn.costUsd.toFixed(2)} USD`;
34
+ const cost = priceText(turn.costUsd, turn.estimatedCostUsd);
34
35
  const heading = ["Claude turn", turn.outcome, turnTime(turn, now), cost];
35
36
  const place = `Branch ${turn.branch} · work tree ${turn.worktree}`;
36
37
  const activity = groups.map((items, index) => component(Group, { items, open: working && index === groups.length - 1 }));
@@ -42,6 +43,10 @@ export function Turn(props) {
42
43
  children: heading.filter((part) => part !== "").join(" · "),
43
44
  }),
44
45
  element("p", { class: "meta", children: place }),
46
+ element("p", {
47
+ class: "turn-usage",
48
+ children: usageText(turn.usage),
49
+ }),
45
50
  element("div", { class: "activity", children: activity }),
46
51
  ],
47
52
  });
@@ -77,7 +82,7 @@ export function Command(props) {
77
82
  const onClick = () => {
78
83
  copying.onCopy(command);
79
84
  };
80
- const shown = copying.command === command ? copying.label : "Copy";
85
+ const shown = copying.command === command ? copying.label : (props.copyLabel ?? "Copy");
81
86
  return element("div", {
82
87
  class: "command code-window",
83
88
  children: [
@@ -0,0 +1,6 @@
1
+ import { type Fields } from "../protocol.ts";
2
+ import type { WatchTurn } from "./watch-render.ts";
3
+ export declare function priceFields(value: unknown): {
4
+ readonly costUsd?: number;
5
+ };
6
+ export declare function statusTurns(turns: readonly WatchTurn[], status: Fields): readonly WatchTurn[];
@@ -0,0 +1,65 @@
1
+ import { record } from "../protocol.js";
2
+ import { priceOf, usageOf } from "../worker-usage.js";
3
+ function text(value) {
4
+ return typeof value === "string" ? value : "";
5
+ }
6
+ function emptyTurn(operationId, runId) {
7
+ return {
8
+ operationId,
9
+ runId,
10
+ key: operationId,
11
+ usage: usageOf(null),
12
+ costUsd: null,
13
+ estimatedCostUsd: null,
14
+ target: "",
15
+ branch: "",
16
+ worktree: "",
17
+ startedAt: "",
18
+ endedAt: null,
19
+ activeAt: null,
20
+ outcome: "ended",
21
+ activity: [],
22
+ commits: [],
23
+ files: [],
24
+ uncommitted: [],
25
+ summary: "",
26
+ };
27
+ }
28
+ export function priceFields(value) {
29
+ const cost = priceOf(value);
30
+ return cost === null ? {} : { costUsd: cost };
31
+ }
32
+ export function statusTurns(turns, status) {
33
+ const runId = text(status["runId"]);
34
+ const journal = turns.filter((turn) => turn.runId === runId);
35
+ if (!Array.isArray(status["workerTurns"]))
36
+ return journal;
37
+ const ended = status["workerTurns"].map((value) => {
38
+ const fields = record(value) ?? {};
39
+ const operationId = text(fields["operationId"]);
40
+ const known = journal.find((turn) => turn.operationId === operationId) ??
41
+ emptyTurn(operationId, runId);
42
+ return {
43
+ ...known,
44
+ runId,
45
+ operationId,
46
+ outcome: known.outcome === "in work" ? "ended" : known.outcome,
47
+ activeAt: null,
48
+ usage: usageOf(fields["usage"]),
49
+ costUsd: priceOf(fields["costUsd"]),
50
+ estimatedCostUsd: priceOf(fields["estimatedCostUsd"]),
51
+ activity: known.activity.map((item) => item.kind === "result"
52
+ ? {
53
+ kind: item.kind,
54
+ text: item.text,
55
+ ...priceFields(fields["costUsd"]),
56
+ }
57
+ : item),
58
+ };
59
+ });
60
+ return [
61
+ ...ended,
62
+ ...journal.filter((turn) => turn.outcome === "in work" &&
63
+ !ended.some((item) => item.operationId === turn.operationId)),
64
+ ];
65
+ }
@@ -1,5 +1,7 @@
1
+ import type { CardControls } from "./watch-card-view.ts";
1
2
  import { type Shown } from "./watch-element.ts";
2
3
  import { type Copying, type WatchModel, type WatchPhase, type WatchRun } from "./watch-render.ts";
4
+ import { type ColumnControls } from "./watch-board-view.ts";
3
5
  interface Timed {
4
6
  readonly run: WatchRun;
5
7
  readonly now: string;
@@ -24,16 +26,29 @@ export declare function PullRequest(props: {
24
26
  export declare function Run(props: Timed & {
25
27
  readonly copying: Copying;
26
28
  }): Shown;
29
+ export interface StartControls {
30
+ readonly shown: boolean;
31
+ readonly onToggle: () => void;
32
+ }
27
33
  export declare function Header(props: {
28
34
  readonly model: WatchModel | null;
29
35
  readonly stale: boolean;
36
+ readonly start?: StartControls;
30
37
  }): Shown;
31
38
  export interface PageProps {
39
+ readonly cards?: CardControls;
32
40
  readonly model: WatchModel | null;
33
41
  readonly selected: string;
34
42
  readonly now: string;
35
43
  readonly stale: boolean;
36
44
  readonly copying: Copying;
45
+ readonly columns?: ColumnControls;
46
+ readonly start?: StartControls;
37
47
  }
38
48
  export declare function Page(props: PageProps): Shown;
49
+ export declare function BoardPage(props: PageProps & {
50
+ readonly model: WatchModel;
51
+ readonly workflowId: string | null;
52
+ readonly down: Shown;
53
+ }): Shown;
39
54
  export {};