cloving 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -21
- package/assets/watch-fonts/fira-code-OFL.md +93 -0
- package/assets/watch-fonts/fira-code-latin-400-normal.woff2 +0 -0
- package/assets/watch-fonts/fira-code-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-OFL.md +93 -0
- package/assets/watch-fonts/heebo-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-latin-700-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-latin-800-normal.woff2 +0 -0
- package/assets/watch-fonts/manifest.json +112 -0
- package/assets/watch-fonts/space-grotesk-OFL.md +93 -0
- package/assets/watch-fonts/space-grotesk-latin-400-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-600-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-700-normal.woff2 +0 -0
- package/dist/src/authority/commands.d.ts +1 -0
- package/dist/src/authority/commands.js +25 -4
- package/dist/src/authority/dispatch-decision.d.ts +1 -0
- package/dist/src/authority/dispatch-decision.js +7 -4
- package/dist/src/authority/events.js +2 -2
- package/dist/src/authority/handoff-chain.d.ts +24 -0
- package/dist/src/authority/handoff-chain.js +46 -0
- package/dist/src/authority/reconciliation.d.ts +1 -0
- package/dist/src/authority/reconciliation.js +18 -11
- package/dist/src/authority/transitions.d.ts +1 -1
- package/dist/src/authority/transitions.js +3 -0
- package/dist/src/broker/actor/authority-queries.d.ts +4 -0
- package/dist/src/broker/actor/authority-queries.js +51 -2
- package/dist/src/broker/actor/delivery-intents.d.ts +1 -0
- package/dist/src/broker/actor/effect-delivery.js +9 -0
- package/dist/src/broker/authority-admission.js +13 -5
- package/dist/src/chats/analysis-batches.d.ts +18 -0
- package/dist/src/chats/analysis-batches.js +15 -1
- package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
- package/dist/src/chats/analysis-checkpoint.js +31 -0
- package/dist/src/chats/analysis-input.d.ts +7 -1
- package/dist/src/chats/analysis-input.js +20 -6
- package/dist/src/chats/analysis-merge.d.ts +16 -0
- package/dist/src/chats/analysis-merge.js +134 -0
- package/dist/src/chats/analysis-prompt.d.ts +8 -0
- package/dist/src/chats/analysis-prompt.js +27 -1
- package/dist/src/chats/analysis-proposal.d.ts +2 -0
- package/dist/src/chats/analysis-proposal.js +9 -0
- package/dist/src/chats/analysis-shape.d.ts +1 -0
- package/dist/src/chats/analysis-shape.js +11 -4
- package/dist/src/chats/reference-mapping.d.ts +16 -0
- package/dist/src/chats/reference-mapping.js +153 -0
- package/dist/src/chats/workflow-generation-projection.d.ts +8 -0
- package/dist/src/chats/workflow-generation-projection.js +75 -0
- package/dist/src/chats/workflow-generation-source.d.ts +25 -0
- package/dist/src/chats/workflow-generation-source.js +124 -0
- package/dist/src/chats/workflow-generation-text.d.ts +23 -0
- package/dist/src/chats/workflow-generation-text.js +74 -0
- package/dist/src/chats/workflow-generation.d.ts +69 -0
- package/dist/src/chats/workflow-generation.js +142 -0
- package/dist/src/cli/render.d.ts +1 -0
- package/dist/src/cli/render.js +60 -13
- package/dist/src/cli/usage.js +5 -2
- package/dist/src/cli/watch-board-view.d.ts +12 -1
- package/dist/src/cli/watch-board-view.js +57 -11
- package/dist/src/cli/watch-board.d.ts +7 -1
- package/dist/src/cli/watch-board.js +42 -10
- package/dist/src/cli/watch-client.d.ts +1 -1
- package/dist/src/cli/watch-client.js +7 -3
- package/dist/src/cli/watch-model.d.ts +10 -4
- package/dist/src/cli/watch-model.js +35 -58
- package/dist/src/cli/watch-page.js +21 -4
- package/dist/src/cli/watch-phases.js +3 -3
- package/dist/src/cli/watch-pr-command.d.ts +5 -2
- package/dist/src/cli/watch-pr-command.js +41 -4
- package/dist/src/cli/watch-pr-identity.d.ts +14 -2
- package/dist/src/cli/watch-pr-identity.js +25 -0
- package/dist/src/cli/watch-pr-observer.d.ts +1 -0
- package/dist/src/cli/watch-pr-observer.js +48 -6
- package/dist/src/cli/watch-pull-request.d.ts +10 -0
- package/dist/src/cli/watch-pull-request.js +130 -0
- package/dist/src/cli/watch-reads.d.ts +7 -1
- package/dist/src/cli/watch-reads.js +42 -20
- package/dist/src/cli/watch-render.d.ts +14 -1
- package/dist/src/cli/watch-render.js +2 -1
- package/dist/src/cli/watch-server.js +33 -2
- package/dist/src/cli/watch-turn.js +1 -1
- package/dist/src/cli/watch-view.d.ts +7 -0
- package/dist/src/cli/watch-view.js +51 -3
- package/dist/src/daemon/analysis-worker.d.ts +0 -2
- package/dist/src/daemon/analysis-worker.js +71 -19
- package/dist/src/daemon/chats-batch-binding.d.ts +23 -0
- package/dist/src/daemon/chats-batch-binding.js +99 -0
- package/dist/src/daemon/chats-capture.d.ts +18 -0
- package/dist/src/daemon/chats-capture.js +194 -0
- package/dist/src/daemon/chats-checkpoints.d.ts +41 -0
- package/dist/src/daemon/chats-checkpoints.js +186 -0
- package/dist/src/daemon/chats-files.d.ts +29 -2
- package/dist/src/daemon/chats-files.js +142 -3
- package/dist/src/daemon/chats-follow-up.d.ts +38 -0
- package/dist/src/daemon/chats-follow-up.js +180 -0
- package/dist/src/daemon/chats-question.d.ts +15 -0
- package/dist/src/daemon/chats-question.js +93 -0
- package/dist/src/daemon/chats-worker.d.ts +1 -0
- package/dist/src/daemon/chats-worker.js +52 -51
- package/dist/src/daemon/claude-turn.d.ts +2 -1
- package/dist/src/daemon/claude-turn.js +1 -1
- package/dist/src/daemon/claude-worker.d.ts +0 -2
- package/dist/src/daemon/claude-worker.js +6 -11
- package/dist/src/daemon/daemon.d.ts +5 -1
- package/dist/src/daemon/daemon.js +38 -10
- package/dist/src/daemon/delivery-router.js +6 -0
- package/dist/src/daemon/github-calls.d.ts +4 -0
- package/dist/src/daemon/github-calls.js +16 -7
- package/dist/src/daemon/github-ci.d.ts +6 -0
- package/dist/src/daemon/github-ci.js +170 -0
- package/dist/src/daemon/github-merge-call.d.ts +14 -0
- package/dist/src/daemon/github-merge-call.js +73 -0
- package/dist/src/daemon/github-merge.d.ts +3 -5
- package/dist/src/daemon/github-merge.js +52 -95
- package/dist/src/daemon/github-methods.d.ts +28 -0
- package/dist/src/daemon/github-methods.js +80 -16
- package/dist/src/daemon/github-operation.d.ts +2 -0
- package/dist/src/daemon/github-retry.d.ts +6 -0
- package/dist/src/daemon/github-retry.js +49 -0
- package/dist/src/daemon/github-ticket-status.js +27 -1
- package/dist/src/daemon/github-worker.js +2 -1
- package/dist/src/daemon/handoff-worker.d.ts +10 -0
- package/dist/src/daemon/handoff-worker.js +232 -0
- package/dist/src/daemon/issue-queue.d.ts +1 -0
- package/dist/src/daemon/issue-queue.js +94 -42
- package/dist/src/daemon/notify-journal.d.ts +18 -0
- package/dist/src/daemon/notify-journal.js +108 -0
- package/dist/src/daemon/notify-message.js +2 -1
- package/dist/src/daemon/notify.js +176 -37
- package/dist/src/daemon/retrying-turn.d.ts +3 -0
- package/dist/src/daemon/retrying-turn.js +55 -9
- package/dist/src/daemon/ticket-dependency-waits.d.ts +18 -0
- package/dist/src/daemon/ticket-dependency-waits.js +60 -0
- package/dist/src/daemon/ticket-registration.d.ts +8 -4
- package/dist/src/daemon/ticket-registration.js +76 -22
- package/dist/src/daemon/ticket-source.d.ts +4 -1
- package/dist/src/daemon/ticket-source.js +8 -2
- package/dist/src/decision-visibility.d.ts +2 -0
- package/dist/src/decision-visibility.js +10 -0
- package/dist/src/execution-pin.d.ts +4 -0
- package/dist/src/execution-pin.js +23 -2
- package/dist/src/host/start-delivery.d.ts +2 -1
- package/dist/src/host/start-delivery.js +1 -1
- package/dist/src/host/step-bridge-host.d.ts +1 -0
- package/dist/src/host/step-bridge-host.js +16 -0
- package/dist/src/host/world-reconciliation.d.ts +1 -0
- package/dist/src/host/world-reconciliation.js +3 -0
- package/dist/src/host/world-runs.js +5 -0
- package/dist/src/issue-identity.d.ts +6 -0
- package/dist/src/issue-identity.js +23 -0
- package/dist/src/service/queue-runs.d.ts +3 -0
- package/dist/src/service/queue-runs.js +39 -0
- package/dist/src/service/registry.d.ts +8 -2
- package/dist/src/service/registry.js +20 -8
- package/dist/src/service/replies.d.ts +2 -0
- package/dist/src/service/run-handlers.js +44 -12
- package/dist/src/service/service.d.ts +3 -0
- package/dist/src/service/service.js +3 -1
- package/dist/src/service/submission-handler.d.ts +1 -0
- package/dist/src/service/submission-handler.js +92 -13
- package/dist/src/service/worker-totals.js +25 -1
- package/dist/src/step-bridge.d.ts +9 -0
- package/dist/src/steps/handoff.d.ts +2 -0
- package/dist/src/steps/handoff.js +13 -0
- package/dist/src/ticket-dependencies.d.ts +1 -0
- package/dist/src/ticket-dependencies.js +15 -0
- package/dist/src/ticket-status.js +7 -4
- package/dist/src/token-prices.d.ts +2 -0
- package/dist/src/token-prices.js +41 -9
- package/dist/src/worker-usage.d.ts +9 -0
- package/dist/src/worker-usage.js +1 -1
- package/dist/src/workflow-admission.d.ts +1 -0
- package/dist/src/workflow-candidate.js +1 -0
- package/dist/src/workflow-contracts.d.ts +2 -0
- package/dist/src/workflow-contracts.js +8 -3
- package/dist/src/workflow-declaration.d.ts +11 -0
- package/dist/src/workflow-declaration.js +71 -15
- package/dist/src/workflow-discovery.d.ts +18 -0
- package/dist/src/workflow-discovery.js +33 -0
- package/dist/src/workflow-refusals.d.ts +6 -0
- package/dist/src/workflow-refusals.js +39 -0
- package/dist/src/workflows.d.ts +2 -1
- package/dist/src/workflows.js +1 -0
- package/dist/tools/admit-workflow.d.ts +13 -0
- package/dist/tools/admit-workflow.js +94 -0
- package/dist/tools/self-host-project.d.ts +17 -0
- package/dist/tools/self-host-project.js +177 -0
- package/dist/tools/self-host.js +27 -81
- package/dist/tools/workflow-bundle-dependencies.d.ts +5 -0
- package/dist/tools/workflow-bundle-dependencies.js +93 -0
- package/dist/tools/workflow-bundles.d.ts +20 -0
- package/dist/tools/workflow-bundles.js +173 -0
- package/dist/tools/workflow-candidate-counts.d.ts +8 -0
- package/dist/tools/workflow-candidate-counts.js +251 -0
- package/dist/tools/workflow-candidate-program.d.ts +11 -0
- package/dist/tools/workflow-candidate-program.js +78 -0
- package/dist/tools/workflow-candidate-source.d.ts +6 -0
- package/dist/tools/workflow-candidate-source.js +107 -0
- package/dist/tools/workflow-candidate-syntax.d.ts +15 -0
- package/dist/tools/workflow-candidate-syntax.js +219 -0
- package/dist/tools/workflow-folder-capture.d.ts +17 -0
- package/dist/tools/workflow-folder-capture.js +216 -0
- package/dist/tools/workflow-folders.d.ts +33 -0
- package/dist/tools/workflow-folders.js +150 -0
- package/dist/tools/workflow-source-effects.d.ts +10 -0
- package/dist/tools/workflow-source-effects.js +213 -0
- package/dist/workflows/analyze-chats.d.ts +6 -1
- package/dist/workflows/analyze-chats.declaration.json +14 -1
- package/dist/workflows/analyze-chats.js +188 -11
- package/dist/workflows/develop-prd.js +71 -3
- package/dist/workflows/develop-ticket.js +5 -0
- package/dist/workflows/ticket-prompts.js +12 -2
- package/package.json +21 -3
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { operationalDecision } from "../decision-visibility.js";
|
|
2
|
+
import { workflowRefusals, } from "../workflow-refusals.js";
|
|
1
3
|
import { isTicketStatus } from "../ticket-status.js";
|
|
4
|
+
import { isPromotionId } from "../workflow-contracts.js";
|
|
2
5
|
import { priceFields, statusTurns } from "./watch-usage.js";
|
|
3
6
|
import { priceOf, usageOf } from "../worker-usage.js";
|
|
4
7
|
import { priceText } from "./usage-render.js";
|
|
5
|
-
import { boardsOf, currentStepOf, roundOf, startWorkflowOf, } from "./watch-board.js";
|
|
8
|
+
import { boardsOf, currentStepOf, roundOf, stagesFor, startWorkflowOf, } from "./watch-board.js";
|
|
6
9
|
import { decisionOf, phasesOf } from "./watch-phases.js";
|
|
10
|
+
import { pullOf } from "./watch-pull-request.js";
|
|
11
|
+
import { establishedIssue } from "./watch-pr-identity.js";
|
|
7
12
|
import { displayRun } from "./watch-paths.js";
|
|
8
13
|
const textLimit = 600;
|
|
9
14
|
const activityLimit = 400;
|
|
@@ -156,54 +161,6 @@ export function decisionCommands(decision, client) {
|
|
|
156
161
|
deny: `${client} deny ${decisionId} --text "<feedback>" ${binding}`,
|
|
157
162
|
};
|
|
158
163
|
}
|
|
159
|
-
function githubOf(records) {
|
|
160
|
-
const started = record(records.started);
|
|
161
|
-
const operation = list(parsed(text(started["operationId"])));
|
|
162
|
-
const result = record(records.result);
|
|
163
|
-
const completed = result["outcome"] === "completed";
|
|
164
|
-
return {
|
|
165
|
-
runId: text(operation[2]),
|
|
166
|
-
round: Number(operation[1]),
|
|
167
|
-
method: text(started["method"]),
|
|
168
|
-
report: completed ? record(parsed(text(result["result"]))) : {},
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
function pullRequestOf(results) {
|
|
172
|
-
const opened = results
|
|
173
|
-
.filter((item) => item.method === "open-pr")
|
|
174
|
-
.map((item) => item.report)
|
|
175
|
-
.findLast((report) => text(report["url"]).startsWith("https://"));
|
|
176
|
-
if (opened === undefined)
|
|
177
|
-
return null;
|
|
178
|
-
return { number: Number(opened["number"]), url: text(opened["url"]) };
|
|
179
|
-
}
|
|
180
|
-
function reviewsOf(results) {
|
|
181
|
-
return results
|
|
182
|
-
.filter((item) => item.method === "wait-review")
|
|
183
|
-
.flatMap(({ round, report }) => {
|
|
184
|
-
const score = count(report["score"]);
|
|
185
|
-
if (score === null)
|
|
186
|
-
return [];
|
|
187
|
-
return [{ round, commit: text(report["commit"]), score }];
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
function mergeCommitOf(results) {
|
|
191
|
-
const merged = results
|
|
192
|
-
.filter((item) => item.method === "merge")
|
|
193
|
-
.map((item) => stamp(item.report["merged"]));
|
|
194
|
-
return merged.find((commit) => commit !== null) ?? null;
|
|
195
|
-
}
|
|
196
|
-
function pullOf(inputs, runId) {
|
|
197
|
-
const results = inputs.githubRecords
|
|
198
|
-
.map(githubOf)
|
|
199
|
-
.filter((item) => item.runId === runId)
|
|
200
|
-
.toSorted((first, second) => first.round - second.round);
|
|
201
|
-
return {
|
|
202
|
-
pullRequest: pullRequestOf(results),
|
|
203
|
-
reviews: reviewsOf(results),
|
|
204
|
-
mergeCommit: mergeCommitOf(results),
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
164
|
function feedbackOf(value) {
|
|
208
165
|
const fields = record(value);
|
|
209
166
|
return {
|
|
@@ -219,28 +176,43 @@ function eventOf(value) {
|
|
|
219
176
|
type: text(fields["type"]),
|
|
220
177
|
};
|
|
221
178
|
}
|
|
179
|
+
function outcomeOf(value) {
|
|
180
|
+
const fields = record(value);
|
|
181
|
+
const { promotionId, ticketStatus, issueUpdate } = fields;
|
|
182
|
+
return {
|
|
183
|
+
outcome: stamp(fields["kind"]),
|
|
184
|
+
...(isPromotionId(promotionId) ? { promotionId } : {}),
|
|
185
|
+
...(isTicketStatus(ticketStatus) ? { ticketStatus } : {}),
|
|
186
|
+
...(issueUpdate === "failed" ? { issueUpdate } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
222
189
|
function runOf(inputs, value) {
|
|
223
190
|
const status = record(record(value)["status"]);
|
|
224
191
|
const runId = text(status["runId"]);
|
|
225
192
|
const workflowId = text(status["workflowId"]);
|
|
226
193
|
const budget = record(status["budget"]);
|
|
227
194
|
const ticketId = text(status["ticketId"]);
|
|
228
|
-
const
|
|
195
|
+
const issue = workflowId === "develop-issue"
|
|
196
|
+
? establishedIssue(inputs.repository ?? null, status["issueIdentity"])
|
|
197
|
+
: null;
|
|
229
198
|
return {
|
|
230
199
|
runId,
|
|
231
200
|
workflowId,
|
|
201
|
+
parentRunId: stamp(status["parentRunId"]),
|
|
202
|
+
chainId: stamp(status["chainId"]) ?? runId,
|
|
203
|
+
childRunIds: texts(status["childRunIds"]),
|
|
232
204
|
ticketId: ticketId === "" ? null : ticketId,
|
|
205
|
+
...(issue === null ? {} : { issueIdentity: issue }),
|
|
233
206
|
title: stamp(status["title"]),
|
|
234
|
-
|
|
235
|
-
...(isTicketStatus(ticketStatus) ? { ticketStatus } : {}),
|
|
236
|
-
...(record(status["outcome"])["issueUpdate"] === "failed"
|
|
237
|
-
? { issueUpdate: "failed" }
|
|
238
|
-
: {}),
|
|
207
|
+
...outcomeOf(status["outcome"]),
|
|
239
208
|
latestAt: stamp(status["latestAt"]),
|
|
240
209
|
claudeCostUsd: priceOf(status["claudeCostUsd"]),
|
|
241
210
|
estimatedCostUsd: priceOf(status["estimatedCostUsd"]),
|
|
242
211
|
usage: usageOf(status["usage"]),
|
|
243
|
-
currentStep: currentStepOf(list(record(value)["events"]),
|
|
212
|
+
currentStep: currentStepOf(list(record(value)["events"]), {
|
|
213
|
+
workflowId,
|
|
214
|
+
stages: stagesFor(inputs.workflowStages, workflowId),
|
|
215
|
+
}),
|
|
244
216
|
round: roundOf(list(record(value)["events"])),
|
|
245
217
|
state: text(status["state"]),
|
|
246
218
|
code: text(record(status["reason"])["code"]),
|
|
@@ -249,13 +221,16 @@ function runOf(inputs, value) {
|
|
|
249
221
|
limit: count(budget["limit"]),
|
|
250
222
|
decisions: inputs.decisions
|
|
251
223
|
.filter((decision) => record(decision)["runId"] === runId)
|
|
252
|
-
.map((decision) => decisionCommands(decision, inputs.client)),
|
|
224
|
+
.map((decision) => decisionCommands(operationalDecision(record(decision), workflowId), inputs.client)),
|
|
253
225
|
events: list(record(value)["events"]).map(eventOf),
|
|
254
226
|
turns: statusTurns(inputs.turns, status),
|
|
255
227
|
phases: phasesOf(list(record(value)["events"]), workflowId),
|
|
256
228
|
decision: decisionOf(list(record(value)["events"])),
|
|
257
229
|
feedback: list(status["feedback"]).map(feedbackOf),
|
|
258
|
-
...pullOf(
|
|
230
|
+
...pullOf({
|
|
231
|
+
records: inputs.githubRecords,
|
|
232
|
+
repository: workflowId === "develop-issue" ? (inputs.repository ?? null) : null,
|
|
233
|
+
}, runId),
|
|
259
234
|
};
|
|
260
235
|
}
|
|
261
236
|
function began(run) {
|
|
@@ -270,6 +245,8 @@ export function watchModel(inputs) {
|
|
|
270
245
|
const workflows = boardsOf({ ...inputs, completionCandidates: candidates }, runs);
|
|
271
246
|
return {
|
|
272
247
|
workflows,
|
|
248
|
+
...(inputs.queue === undefined ? {} : { queue: inputs.queue }),
|
|
249
|
+
workflowRefusals: workflowRefusals(inputs.workflowRefusals),
|
|
273
250
|
startWorkflowId: startWorkflowOf(workflows),
|
|
274
251
|
updatedAt: inputs.updatedAt,
|
|
275
252
|
service: inputs.service,
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { cardReply } from "./watch-card-reply.js";
|
|
2
|
+
import { record } from "../protocol.js";
|
|
2
3
|
import { watchCardClient } from "./watch-card-client.js";
|
|
3
4
|
import { AddCard, CardInput, cardKey, cardSubmitKey, } from "./watch-card-view.js";
|
|
4
5
|
import { watchClient } from "./watch-client.js";
|
|
5
6
|
import { priceOf, tokenCount, usageOf } from "../worker-usage.js";
|
|
6
7
|
import { priceText, usageText, totalsSuffix } from "./usage-render.js";
|
|
7
8
|
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";
|
|
9
|
+
import { Board, boardSizing, Card, HandoffLinks, Column, columnCollapsed, CardReview, WorkflowList, cardReview, cardStep, completionMark, observationText, staleObservation, } from "./watch-board-view.js";
|
|
9
10
|
import { boardWorkflow, watchHash, watchLocation, watchTitle, } from "./watch-navigation.js";
|
|
10
11
|
import { clockTime, completedDetail, counted, elapsed, groupActivity, pageTitle, phaseTime, runName, runTone, selectedRun, toneOf, turnTime, waitingDetail, } from "./watch-render.js";
|
|
11
12
|
import { Command, Decision, Events, Group, Item, Turn, Work, } from "./watch-turn.js";
|
|
12
|
-
import { clove, Banner, BannerDetail, BoardPage, Header, Page, Phases, PullRequest, Review, Run, } from "./watch-view.js";
|
|
13
|
+
import { clove, Banner, BannerDetail, BoardPage, Header, Page, Phases, PullRequest, Review, RefusedWorkflows, Queue, Run, } from "./watch-view.js";
|
|
13
14
|
const functions = [
|
|
15
|
+
record,
|
|
16
|
+
Queue,
|
|
14
17
|
cardReply,
|
|
15
18
|
cardSubmitKey,
|
|
16
19
|
cardKey,
|
|
@@ -29,8 +32,11 @@ const functions = [
|
|
|
29
32
|
cardReview,
|
|
30
33
|
cardStep,
|
|
31
34
|
completionMark,
|
|
35
|
+
observationText,
|
|
36
|
+
staleObservation,
|
|
32
37
|
CardReview,
|
|
33
38
|
Card,
|
|
39
|
+
HandoffLinks,
|
|
34
40
|
Column,
|
|
35
41
|
columnCollapsed,
|
|
36
42
|
WorkflowList,
|
|
@@ -65,6 +71,7 @@ const functions = [
|
|
|
65
71
|
Review,
|
|
66
72
|
Run,
|
|
67
73
|
Header,
|
|
74
|
+
RefusedWorkflows,
|
|
68
75
|
BoardPage,
|
|
69
76
|
Page,
|
|
70
77
|
];
|
|
@@ -93,6 +100,15 @@ const dark = `
|
|
|
93
100
|
--bad: #FF7B72; --bad-tint: rgba(255,123,114,0.14);
|
|
94
101
|
`;
|
|
95
102
|
const style = `
|
|
103
|
+
@font-face { font-family: "Fira Code"; font-style: normal; font-weight: 400; font-display: swap; src: url("/watch-fonts-v1/fira-code-latin-400-normal.woff2") format("woff2"); }
|
|
104
|
+
@font-face { font-family: "Fira Code"; font-style: normal; font-weight: 500; font-display: swap; src: url("/watch-fonts-v1/fira-code-latin-500-normal.woff2") format("woff2"); }
|
|
105
|
+
@font-face { font-family: "Heebo"; font-style: normal; font-weight: 500; font-display: swap; src: url("/watch-fonts-v1/heebo-latin-500-normal.woff2") format("woff2"); }
|
|
106
|
+
@font-face { font-family: "Heebo"; font-style: normal; font-weight: 700; font-display: swap; src: url("/watch-fonts-v1/heebo-latin-700-normal.woff2") format("woff2"); }
|
|
107
|
+
@font-face { font-family: "Heebo"; font-style: normal; font-weight: 800; font-display: swap; src: url("/watch-fonts-v1/heebo-latin-800-normal.woff2") format("woff2"); }
|
|
108
|
+
@font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 400; font-display: swap; src: url("/watch-fonts-v1/space-grotesk-latin-400-normal.woff2") format("woff2"); }
|
|
109
|
+
@font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 500; font-display: swap; src: url("/watch-fonts-v1/space-grotesk-latin-500-normal.woff2") format("woff2"); }
|
|
110
|
+
@font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 600; font-display: swap; src: url("/watch-fonts-v1/space-grotesk-latin-600-normal.woff2") format("woff2"); }
|
|
111
|
+
@font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 700; font-display: swap; src: url("/watch-fonts-v1/space-grotesk-latin-700-normal.woff2") format("woff2"); }
|
|
96
112
|
:root {${light}
|
|
97
113
|
--heading-font: "Heebo", system-ui, sans-serif;
|
|
98
114
|
--body-font: "Space Grotesk", system-ui, sans-serif;
|
|
@@ -111,6 +127,8 @@ body:has(.board-page) { padding-bottom: 0; overflow: hidden; }
|
|
|
111
127
|
.board-page { height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
|
|
112
128
|
.board-page > header, .board-page > .down { flex-shrink: 0; }
|
|
113
129
|
.start-run { flex-shrink: 0; max-height: 45dvh; overflow-y: auto; }
|
|
130
|
+
.workflow-refusals { flex-shrink: 0; max-height: 25dvh; overflow-y: auto; overflow-wrap: anywhere; }
|
|
131
|
+
.workflow-refusals h2 { margin: 8px 0; }
|
|
114
132
|
a { color: var(--accent-strong); }
|
|
115
133
|
header { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; padding: 22px 0 10px; color: var(--muted); font-size: 12px; }
|
|
116
134
|
.brand { display: inline-flex; align-items: center; gap: 9px; color: var(--ink); font: 800 26px/1.2 var(--heading-font); }
|
|
@@ -142,7 +160,7 @@ h2, h3 { font-family: var(--heading-font); font-weight: 700; }
|
|
|
142
160
|
.column.collapsed { flex: 0 0 ${String(boardSizing.collapsedColumnWidth)}px; min-width: ${String(boardSizing.collapsedColumnWidth)}px; padding: 6px; }
|
|
143
161
|
.collapsed .column-name { writing-mode: vertical-rl; padding: 0; }
|
|
144
162
|
.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); }
|
|
163
|
+
.column-name button:hover, .start-toggle:hover, .add-card:hover, .add-card-form button:hover { background: var(--surface); }
|
|
146
164
|
.add-card, .add-card-form button { color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); margin: 4px 8px 4px 0; }
|
|
147
165
|
.add-card { width: 100%; text-align: left; }
|
|
148
166
|
.add-card-form { padding: 8px 0; overflow-wrap: anywhere; }
|
|
@@ -249,7 +267,6 @@ export function pageDocument(actionToken = null) {
|
|
|
249
267
|
"<!doctype html>",
|
|
250
268
|
'<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Cloving</title>',
|
|
251
269
|
`<link rel="icon" href="data:image/svg+xml,${encodeURIComponent(icon)}">`,
|
|
252
|
-
'<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Heebo:wght@500;700;800&family=Space+Grotesk:wght@400..700&display=swap" media="print" onload="this.media=\'all\'">',
|
|
253
270
|
`<style>${style}</style></head><body>`,
|
|
254
271
|
'<div id="app"></div>',
|
|
255
272
|
`<script type="module">${script}</script></body></html>`,
|
|
@@ -8,9 +8,9 @@ const effect = { open: "effect.requested", close: "effect.reported" };
|
|
|
8
8
|
const issueWorkflow = "develop-issue";
|
|
9
9
|
export const issueSteps = [
|
|
10
10
|
{ name: "Work tree", operation: "github-prepare" },
|
|
11
|
-
{ name: "
|
|
11
|
+
{ name: "Agent turn", operation: "claude-turn" },
|
|
12
12
|
{ name: "Pull request", operation: "github-open-pr" },
|
|
13
|
-
{ name: "
|
|
13
|
+
{ name: "Code review", operation: "github-wait-review" },
|
|
14
14
|
{ name: "Merge", operation: "github-merge" },
|
|
15
15
|
];
|
|
16
16
|
const verdicts = {
|
|
@@ -56,7 +56,7 @@ function ticketSpans(events) {
|
|
|
56
56
|
return [
|
|
57
57
|
...admission(events),
|
|
58
58
|
{
|
|
59
|
-
name: "
|
|
59
|
+
name: "Agent turn",
|
|
60
60
|
startedAt: firstAt(events, effect.open),
|
|
61
61
|
endedAt: closedAt(events, effect),
|
|
62
62
|
ending: "done",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PrIdentity, type
|
|
1
|
+
import { type PrIdentity, type PrReadResult } from "./watch-pr-identity.ts";
|
|
2
2
|
export interface WatchCommandOutput {
|
|
3
3
|
readonly stdout: string;
|
|
4
4
|
readonly stderr: string;
|
|
@@ -12,4 +12,7 @@ export interface WatchCommandRequest {
|
|
|
12
12
|
}
|
|
13
13
|
export type WatchCommandRunner = (request: WatchCommandRequest) => Promise<WatchCommandOutput>;
|
|
14
14
|
export declare function prArguments(identity: PrIdentity): readonly string[];
|
|
15
|
-
export declare function prCommandResult(identity: PrIdentity,
|
|
15
|
+
export declare function prCommandResult(identity: PrIdentity, reply: {
|
|
16
|
+
readonly output: WatchCommandOutput;
|
|
17
|
+
readonly now: number;
|
|
18
|
+
}): PrReadResult;
|
|
@@ -15,15 +15,52 @@ function commandFailure(output) {
|
|
|
15
15
|
/^HTTP\/[^ ]+ (\d{3})/m.exec(output.stdout)?.[1]);
|
|
16
16
|
if (status === 429 ||
|
|
17
17
|
/rate limit/i.test(output.stderr) ||
|
|
18
|
-
|
|
18
|
+
headerValues(output.stdout, "x-ratelimit-remaining").includes("0"))
|
|
19
19
|
return "rate-limit";
|
|
20
20
|
if (output.status === 4 || [401, 403, 404].includes(status))
|
|
21
21
|
return "access";
|
|
22
22
|
return "command";
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
return
|
|
24
|
+
function headerValues(stdout, name) {
|
|
25
|
+
if (!/^HTTP\/[^ ]+ \d{3}/.test(stdout))
|
|
26
|
+
return [];
|
|
27
|
+
const headers = stdout.split(/\r?\n\r?\n/, 1)[0] ?? "";
|
|
28
|
+
return headers.split(/\r?\n/).flatMap((line) => {
|
|
29
|
+
const separator = line.indexOf(":");
|
|
30
|
+
return line.slice(0, separator).toLowerCase() === name
|
|
31
|
+
? [line.slice(separator + 1).trim()]
|
|
32
|
+
: [];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function millisecondsOf(value) {
|
|
36
|
+
if (!/^\d+$/.test(value))
|
|
37
|
+
return [];
|
|
38
|
+
const milliseconds = Number(value) * 1000;
|
|
39
|
+
return Number.isSafeInteger(milliseconds) ? [milliseconds] : [];
|
|
40
|
+
}
|
|
41
|
+
function retryDeadline(output, now) {
|
|
42
|
+
const delays = headerValues(output.stdout, "retry-after")
|
|
43
|
+
.flatMap(millisecondsOf)
|
|
44
|
+
.map((delay) => now + delay)
|
|
45
|
+
.filter(Number.isSafeInteger);
|
|
46
|
+
const resets = headerValues(output.stdout, "x-ratelimit-remaining").includes("0")
|
|
47
|
+
? headerValues(output.stdout, "x-ratelimit-reset").flatMap(millisecondsOf)
|
|
48
|
+
: [];
|
|
49
|
+
const deadlines = [...delays, ...resets];
|
|
50
|
+
return deadlines.length === 0 ? now + 60000 : Math.max(now, ...deadlines);
|
|
51
|
+
}
|
|
52
|
+
export function prCommandResult(identity, reply) {
|
|
53
|
+
const { output, now } = reply;
|
|
54
|
+
if (output.status !== 0 || output.errorCode !== null) {
|
|
55
|
+
const failure = commandFailure(output);
|
|
56
|
+
return {
|
|
57
|
+
status: "unavailable",
|
|
58
|
+
failure,
|
|
59
|
+
...(failure === "rate-limit"
|
|
60
|
+
? { retryAt: retryDeadline(output, now) }
|
|
61
|
+
: {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
27
64
|
const envelope = /^HTTP\/[^ ]+ 200[^\n]*\r?\n[\s\S]*?\r?\n\r?\n([\s\S]*)$/.exec(output.stdout);
|
|
28
65
|
if (envelope?.[1] === undefined)
|
|
29
66
|
return { status: "unavailable", failure: "response" };
|
|
@@ -5,14 +5,26 @@ export interface PrIdentity {
|
|
|
5
5
|
readonly number: number;
|
|
6
6
|
}
|
|
7
7
|
export type PrFailure = "configuration" | "identity" | "command" | "access" | "rate-limit" | "response" | "timeout";
|
|
8
|
-
export type
|
|
8
|
+
export type PrReadResult = {
|
|
9
9
|
readonly status: "merged" | "unmerged";
|
|
10
10
|
} | {
|
|
11
11
|
readonly status: "unavailable";
|
|
12
12
|
readonly failure: PrFailure;
|
|
13
|
+
readonly retryAt?: number;
|
|
13
14
|
};
|
|
14
|
-
export
|
|
15
|
+
export type PrObservation = (PrReadResult | {
|
|
16
|
+
readonly status: "pending";
|
|
17
|
+
}) & {
|
|
18
|
+
readonly retryAt?: number;
|
|
19
|
+
readonly lastSuccess?: {
|
|
20
|
+
readonly status: "merged" | "unmerged";
|
|
21
|
+
readonly readAt: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export declare function prResponse(identity: PrIdentity, value: unknown): PrReadResult;
|
|
15
25
|
export type PrRepository = Omit<PrIdentity, "number">;
|
|
26
|
+
export declare function establishedIssue(repository: PrRepository | null, value: unknown): PrIdentity | null;
|
|
27
|
+
export declare function sameIssue(first: PrIdentity | null | undefined, second: PrIdentity | null | undefined): boolean;
|
|
16
28
|
export declare function prRepository(origin: string): PrRepository | null;
|
|
17
29
|
export declare function prIdentity(repository: PrRepository, pullRequest: {
|
|
18
30
|
readonly number: number;
|
|
@@ -31,6 +31,31 @@ export function prResponse(identity, value) {
|
|
|
31
31
|
return { status: "unavailable", failure: "response" };
|
|
32
32
|
return { status: merged ? "merged" : "unmerged" };
|
|
33
33
|
}
|
|
34
|
+
export function establishedIssue(repository, value) {
|
|
35
|
+
const fields = record(value) ?? {};
|
|
36
|
+
const number = fields["number"];
|
|
37
|
+
if (repository === null ||
|
|
38
|
+
typeof number !== "number" ||
|
|
39
|
+
!Number.isSafeInteger(number) ||
|
|
40
|
+
number <= 0)
|
|
41
|
+
return null;
|
|
42
|
+
const owner = fields["owner"];
|
|
43
|
+
const name = fields["repository"];
|
|
44
|
+
if (typeof owner !== "string" ||
|
|
45
|
+
typeof name !== "string" ||
|
|
46
|
+
owner.toLowerCase() !== repository.owner ||
|
|
47
|
+
name.toLowerCase() !== repository.repository)
|
|
48
|
+
return null;
|
|
49
|
+
return { ...repository, number };
|
|
50
|
+
}
|
|
51
|
+
export function sameIssue(first, second) {
|
|
52
|
+
return (first != null &&
|
|
53
|
+
second != null &&
|
|
54
|
+
first.host === second.host &&
|
|
55
|
+
first.owner === second.owner &&
|
|
56
|
+
first.repository === second.repository &&
|
|
57
|
+
first.number === second.number);
|
|
58
|
+
}
|
|
34
59
|
export function prRepository(origin) {
|
|
35
60
|
const address = origin
|
|
36
61
|
.replace(/^git@([^:/]+):/, "ssh://git@$1/")
|
|
@@ -2,6 +2,7 @@ import type { WatchPullRequest } from "./watch-render.ts";
|
|
|
2
2
|
import { type WatchCommandRunner } from "./watch-pr-command.ts";
|
|
3
3
|
import { type PrObservation, type PrRepository } from "./watch-pr-identity.ts";
|
|
4
4
|
export interface PrObserver {
|
|
5
|
+
readonly repository?: PrRepository | null;
|
|
5
6
|
readonly observe: (pullRequest: WatchPullRequest) => PrObservation | null;
|
|
6
7
|
readonly close: () => void;
|
|
7
8
|
}
|
|
@@ -9,13 +9,23 @@ const systemClock = {
|
|
|
9
9
|
};
|
|
10
10
|
},
|
|
11
11
|
};
|
|
12
|
+
function observationOf(entry, reply) {
|
|
13
|
+
const { result, now } = reply;
|
|
14
|
+
if (result.status !== "unavailable")
|
|
15
|
+
return { ...result, lastSuccess: { status: result.status, readAt: now } };
|
|
16
|
+
const lastSuccess = entry.result?.lastSuccess;
|
|
17
|
+
return { ...result, ...(lastSuccess === undefined ? {} : { lastSuccess }) };
|
|
18
|
+
}
|
|
12
19
|
async function readPr(state, attempt) {
|
|
13
20
|
const { entry, controller } = attempt;
|
|
14
21
|
const deadline = { expired: false };
|
|
15
22
|
const timeout = { status: "unavailable", failure: "timeout" };
|
|
16
23
|
const cancel = state.clock.after(() => {
|
|
17
24
|
deadline.expired = true;
|
|
18
|
-
entry.result =
|
|
25
|
+
entry.result = observationOf(entry, {
|
|
26
|
+
result: timeout,
|
|
27
|
+
now: state.clock.now(),
|
|
28
|
+
});
|
|
19
29
|
controller.abort();
|
|
20
30
|
}, 5000);
|
|
21
31
|
controller.signal.addEventListener("abort", cancel, { once: true });
|
|
@@ -28,7 +38,9 @@ async function readPr(state, attempt) {
|
|
|
28
38
|
directory: state.settings.directory,
|
|
29
39
|
signal: controller.signal,
|
|
30
40
|
}));
|
|
31
|
-
return deadline.expired
|
|
41
|
+
return deadline.expired
|
|
42
|
+
? timeout
|
|
43
|
+
: prCommandResult(entry.identity, { output, now: state.clock.now() });
|
|
32
44
|
}
|
|
33
45
|
catch {
|
|
34
46
|
return deadline.expired
|
|
@@ -43,14 +55,32 @@ async function readPr(state, attempt) {
|
|
|
43
55
|
function pump(state) {
|
|
44
56
|
if (state.closed || state.active !== null)
|
|
45
57
|
return;
|
|
46
|
-
|
|
58
|
+
state.wake?.();
|
|
59
|
+
state.wake = null;
|
|
60
|
+
if (state.queue.length === 0)
|
|
61
|
+
return;
|
|
62
|
+
const now = state.clock.now();
|
|
63
|
+
const position = state.queue.findIndex((entry) => (state.retryDeadlines.get(entry.identity.host) ?? 0) <= now);
|
|
64
|
+
if (position === -1) {
|
|
65
|
+
const next = Math.min(...state.queue.map((entry) => state.retryDeadlines.get(entry.identity.host) ?? now));
|
|
66
|
+
state.wake = state.clock.after(() => {
|
|
67
|
+
state.wake = null;
|
|
68
|
+
pump(state);
|
|
69
|
+
}, Math.min(next - now, 2147483647));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const [entry] = state.queue.splice(position, 1);
|
|
47
73
|
if (entry === undefined)
|
|
48
74
|
return;
|
|
49
75
|
const controller = new AbortController();
|
|
50
76
|
state.active = controller;
|
|
51
77
|
entry.started = state.clock.now();
|
|
52
78
|
const completed = (result) => {
|
|
53
|
-
|
|
79
|
+
if (state.closed)
|
|
80
|
+
return;
|
|
81
|
+
entry.result = observationOf(entry, { result, now: state.clock.now() });
|
|
82
|
+
if (result.status === "unavailable" && result.failure === "rate-limit")
|
|
83
|
+
state.retryDeadlines.set(entry.identity.host, Math.max(state.retryDeadlines.get(entry.identity.host) ?? 0, result.retryAt ?? state.clock.now() + 60000));
|
|
54
84
|
entry.pending = false;
|
|
55
85
|
state.active = null;
|
|
56
86
|
pump(state);
|
|
@@ -59,6 +89,11 @@ function pump(state) {
|
|
|
59
89
|
completed({ status: "unavailable", failure: "command" });
|
|
60
90
|
});
|
|
61
91
|
}
|
|
92
|
+
function currentObservation(state, entry) {
|
|
93
|
+
const result = entry.result ?? { status: "pending" };
|
|
94
|
+
const retryAt = state.retryDeadlines.get(entry.identity.host) ?? 0;
|
|
95
|
+
return retryAt > state.clock.now() ? { ...result, retryAt } : result;
|
|
96
|
+
}
|
|
62
97
|
function needsRead(entry, now) {
|
|
63
98
|
if (entry === undefined)
|
|
64
99
|
return true;
|
|
@@ -72,6 +107,8 @@ export function prObserver(settings) {
|
|
|
72
107
|
clock: settings.clock ?? systemClock,
|
|
73
108
|
cache: new Map(),
|
|
74
109
|
queue: [],
|
|
110
|
+
retryDeadlines: new Map(),
|
|
111
|
+
wake: null,
|
|
75
112
|
active: null,
|
|
76
113
|
closed: false,
|
|
77
114
|
};
|
|
@@ -85,7 +122,9 @@ export function prObserver(settings) {
|
|
|
85
122
|
const key = JSON.stringify(identity);
|
|
86
123
|
const known = state.cache.get(key);
|
|
87
124
|
if (state.closed || !needsRead(known, state.clock.now()))
|
|
88
|
-
return known
|
|
125
|
+
return known === undefined
|
|
126
|
+
? { status: "pending" }
|
|
127
|
+
: currentObservation(state, known);
|
|
89
128
|
const entry = {
|
|
90
129
|
identity,
|
|
91
130
|
result: known?.result ?? null,
|
|
@@ -95,13 +134,16 @@ export function prObserver(settings) {
|
|
|
95
134
|
state.cache.set(key, entry);
|
|
96
135
|
state.queue.push(entry);
|
|
97
136
|
pump(state);
|
|
98
|
-
return entry
|
|
137
|
+
return currentObservation(state, entry);
|
|
99
138
|
};
|
|
100
139
|
return {
|
|
140
|
+
repository: settings.repository,
|
|
101
141
|
observe,
|
|
102
142
|
close: () => {
|
|
103
143
|
state.closed = true;
|
|
104
144
|
state.queue.length = 0;
|
|
145
|
+
state.wake?.();
|
|
146
|
+
state.wake = null;
|
|
105
147
|
state.active?.abort();
|
|
106
148
|
},
|
|
107
149
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { WatchRun } from "./watch-render.ts";
|
|
2
|
+
import { type PrRepository } from "./watch-pr-identity.ts";
|
|
3
|
+
export interface GithubRecords {
|
|
4
|
+
readonly started: unknown;
|
|
5
|
+
readonly result: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare function pullOf(inputs: {
|
|
8
|
+
readonly records: readonly GithubRecords[];
|
|
9
|
+
readonly repository: PrRepository | null;
|
|
10
|
+
}, runId: string): Pick<WatchRun, "pullRequest" | "reviews" | "mergeCommit" | "mergeProof">;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { operationIdentity } from "../operation-identity.js";
|
|
2
|
+
import { prIdentity, } from "./watch-pr-identity.js";
|
|
3
|
+
function record(value) {
|
|
4
|
+
return typeof value === "object" && value !== null ? { ...value } : {};
|
|
5
|
+
}
|
|
6
|
+
function text(value) {
|
|
7
|
+
return typeof value === "string" ? value : "";
|
|
8
|
+
}
|
|
9
|
+
function stamp(value) {
|
|
10
|
+
return typeof value === "string" ? value : null;
|
|
11
|
+
}
|
|
12
|
+
function parsed(line) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(line);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function list(value) {
|
|
21
|
+
return Array.isArray(value) ? value.map((item) => item) : [];
|
|
22
|
+
}
|
|
23
|
+
function count(value) {
|
|
24
|
+
return typeof value === "number" ? value : null;
|
|
25
|
+
}
|
|
26
|
+
function attributed(started, operation) {
|
|
27
|
+
const stage = list(operation[5]);
|
|
28
|
+
const checked = operationIdentity({
|
|
29
|
+
round: operation[1],
|
|
30
|
+
run: operation[2],
|
|
31
|
+
phase: operation[3],
|
|
32
|
+
operation: operation[4],
|
|
33
|
+
...(operation[5] === null
|
|
34
|
+
? {}
|
|
35
|
+
: { stage: { id: stage[0], visit: stage[1], branch: stage[2] } }),
|
|
36
|
+
...(operation[6] === null ? {} : { key: operation[6] }),
|
|
37
|
+
});
|
|
38
|
+
return (checked.kind === "valid" &&
|
|
39
|
+
checked.value === JSON.stringify(operation) &&
|
|
40
|
+
operation[3] === "pull-request" &&
|
|
41
|
+
operation[4] === `github-${text(started["method"])}` &&
|
|
42
|
+
(started["runId"] === undefined || started["runId"] === operation[2]));
|
|
43
|
+
}
|
|
44
|
+
function githubOf(records) {
|
|
45
|
+
const started = record(records.started);
|
|
46
|
+
const operation = list(parsed(text(started["operationId"])));
|
|
47
|
+
const result = record(records.result);
|
|
48
|
+
const completed = result["outcome"] === "completed";
|
|
49
|
+
return {
|
|
50
|
+
runId: text(operation[2]),
|
|
51
|
+
round: Number(operation[1]),
|
|
52
|
+
method: text(started["method"]),
|
|
53
|
+
completed,
|
|
54
|
+
attributed: attributed(started, operation) && !Object.hasOwn(result, "reason"),
|
|
55
|
+
report: completed ? record(parsed(text(result["result"]))) : {},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function pullRequestOf(results) {
|
|
59
|
+
const opened = results
|
|
60
|
+
.filter((item) => item.method === "open-pr")
|
|
61
|
+
.map((item) => item.report)
|
|
62
|
+
.findLast((report) => text(report["url"]).startsWith("https://"));
|
|
63
|
+
if (opened === undefined)
|
|
64
|
+
return null;
|
|
65
|
+
return { number: Number(opened["number"]), url: text(opened["url"]) };
|
|
66
|
+
}
|
|
67
|
+
function reviewsOf(results) {
|
|
68
|
+
return results
|
|
69
|
+
.filter((item) => item.method === "wait-review")
|
|
70
|
+
.flatMap(({ round, report }) => {
|
|
71
|
+
const score = count(report["score"]);
|
|
72
|
+
if (score === null)
|
|
73
|
+
return [];
|
|
74
|
+
return [{ round, commit: text(report["commit"]), score }];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function mergeCommitOf(results) {
|
|
78
|
+
const merged = results
|
|
79
|
+
.filter((item) => item.method === "merge")
|
|
80
|
+
.map((item) => stamp(item.report["merged"]));
|
|
81
|
+
return merged.find((commit) => commit !== null) ?? null;
|
|
82
|
+
}
|
|
83
|
+
function mergeProofOf(results, context) {
|
|
84
|
+
if (context.repository === null || context.pullRequest === null)
|
|
85
|
+
return null;
|
|
86
|
+
const repository = context.repository;
|
|
87
|
+
const identity = prIdentity(repository, context.pullRequest);
|
|
88
|
+
if (identity === null)
|
|
89
|
+
return null;
|
|
90
|
+
const opened = results.filter((item) => item.method === "open-pr" && item.completed && item.attributed);
|
|
91
|
+
if (opened.length === 0 ||
|
|
92
|
+
opened.some(({ report }) => report["number"] !== identity.number ||
|
|
93
|
+
typeof report["url"] !== "string" ||
|
|
94
|
+
prIdentity(repository, {
|
|
95
|
+
number: identity.number,
|
|
96
|
+
url: report["url"],
|
|
97
|
+
}) === null))
|
|
98
|
+
return null;
|
|
99
|
+
const merged = results.filter((item) => item.method === "merge" &&
|
|
100
|
+
item.completed &&
|
|
101
|
+
item.attributed &&
|
|
102
|
+
(typeof item.report["reason"] !== "string" ||
|
|
103
|
+
Object.hasOwn(item.report, "merged")));
|
|
104
|
+
if (merged.length === 0 ||
|
|
105
|
+
merged.some(({ report }) => report["number"] !== identity.number ||
|
|
106
|
+
typeof report["merged"] !== "string" ||
|
|
107
|
+
report["merged"].trim() === "" ||
|
|
108
|
+
Object.hasOwn(report, "reason")))
|
|
109
|
+
return null;
|
|
110
|
+
return new Set(merged.map(({ report }) => report["merged"])).size === 1
|
|
111
|
+
? identity
|
|
112
|
+
: null;
|
|
113
|
+
}
|
|
114
|
+
export function pullOf(inputs, runId) {
|
|
115
|
+
const results = inputs.records
|
|
116
|
+
.map(githubOf)
|
|
117
|
+
.filter((item) => item.runId === runId)
|
|
118
|
+
.toSorted((first, second) => first.round - second.round);
|
|
119
|
+
const pullRequest = pullRequestOf(results);
|
|
120
|
+
const mergeProof = mergeProofOf(results, {
|
|
121
|
+
repository: inputs.repository,
|
|
122
|
+
pullRequest,
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
pullRequest,
|
|
126
|
+
reviews: reviewsOf(results),
|
|
127
|
+
mergeCommit: mergeCommitOf(results),
|
|
128
|
+
...(mergeProof === null ? {} : { mergeProof }),
|
|
129
|
+
};
|
|
130
|
+
}
|