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
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createWriteStream, mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { codexStream } from "./codex-stream.js";
|
|
5
|
-
import { priceOf, usageOf } from "../worker-usage.js";
|
|
5
|
+
import { priceOf, usageOf, } from "../worker-usage.js";
|
|
6
6
|
import { commandModel } from "../token-prices.js";
|
|
7
7
|
import { analysisCommand, analysisEnvironment } from "./analysis-launch.js";
|
|
8
8
|
function parsed(line) {
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { type TokenPrices } from "../token-prices.ts";
|
|
2
1
|
import { type TurnProfiles } from "../turn-profile.ts";
|
|
3
2
|
import type { Delivery } from "../broker/actor/delivery-intents.ts";
|
|
4
3
|
import { type Worker } from "./claude-turn.ts";
|
|
5
4
|
import { type RetrySettings } from "./retrying-turn.ts";
|
|
6
5
|
import { type WorkerReport } from "./operation-journal.ts";
|
|
7
6
|
export interface ClaudeWorkerSettings extends RetrySettings {
|
|
8
|
-
readonly tokenPrices?: TokenPrices;
|
|
9
7
|
readonly worker?: Worker;
|
|
10
8
|
readonly profiles?: TurnProfiles;
|
|
11
9
|
readonly workflowId?: (runId: string) => Promise<string | undefined>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { ticketStatus } from "../ticket-status.js";
|
|
3
|
+
import { sumPrices } from "../worker-usage.js";
|
|
3
4
|
import { analysisWorker } from "./analysis-worker.js";
|
|
4
5
|
import { analysisMethod } from "./analysis-launch.js";
|
|
5
|
-
import { estimatedCostUsd, priceForModel, } from "../token-prices.js";
|
|
6
6
|
import { profileCommand, } from "../turn-profile.js";
|
|
7
7
|
import {} from "./claude-turn.js";
|
|
8
8
|
import { startRetriedTurn } from "./retrying-turn.js";
|
|
@@ -41,8 +41,7 @@ async function reconciled(parts, intent) {
|
|
|
41
41
|
await written(folder, { name: "result.json", value: ending });
|
|
42
42
|
return replied({ receipt: key, worker }, ending);
|
|
43
43
|
}
|
|
44
|
-
async function resultText(tree,
|
|
45
|
-
const { result, settings } = turn;
|
|
44
|
+
async function resultText(tree, result) {
|
|
46
45
|
const fields = {
|
|
47
46
|
ticketStatus: ticketStatus(result.text),
|
|
48
47
|
summary: result.text.slice(0, claudeLimits.summaryCharacters),
|
|
@@ -51,10 +50,8 @@ async function resultText(tree, turn) {
|
|
|
51
50
|
costUsd: result.costUsd,
|
|
52
51
|
usage: result.usage,
|
|
53
52
|
model: result.model,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
price: priceForModel(settings.tokenPrices ?? {}, result.model),
|
|
57
|
-
}),
|
|
53
|
+
...(result.attempts === undefined ? {} : { attempts: result.attempts }),
|
|
54
|
+
estimatedCostUsd: sumPrices((result.attempts ?? []).map((attempt) => attempt.estimatedCostUsd)),
|
|
58
55
|
sessionId: result.sessionId,
|
|
59
56
|
turns: result.turns,
|
|
60
57
|
};
|
|
@@ -84,9 +81,7 @@ async function resultText(tree, turn) {
|
|
|
84
81
|
async function outcome(running, end) {
|
|
85
82
|
const endedAt = new Date().toISOString();
|
|
86
83
|
const { result } = end;
|
|
87
|
-
const report = result === undefined
|
|
88
|
-
? {}
|
|
89
|
-
: await resultText(running.tree, { result, settings: running.settings });
|
|
84
|
+
const report = result === undefined ? {} : await resultText(running.tree, result);
|
|
90
85
|
const { text, reason } = report;
|
|
91
86
|
if (reason === undefined &&
|
|
92
87
|
text !== undefined &&
|
|
@@ -104,7 +99,7 @@ async function outcome(running, end) {
|
|
|
104
99
|
}
|
|
105
100
|
async function concluded(parts, running) {
|
|
106
101
|
const { intent, key } = running;
|
|
107
|
-
const recorded = await outcome(
|
|
102
|
+
const recorded = await outcome(running, await running.turn.ended);
|
|
108
103
|
await written(running.folder, { name: "result.json", value: recorded });
|
|
109
104
|
await parts.settings.report(reportOf({ intent, commandId: `claude-turn:${key}`, worker }, recorded));
|
|
110
105
|
}
|
|
@@ -3,8 +3,10 @@ import { type WorkflowHandler } from "../host/world-host.ts";
|
|
|
3
3
|
import type { TurnProfiles } from "../turn-profile.ts";
|
|
4
4
|
import type { TokenPrices } from "../token-prices.ts";
|
|
5
5
|
import type { DeclaredWorkflow } from "../workflow-declaration.ts";
|
|
6
|
+
import type { WorkflowRefusal } from "../workflow-refusals.ts";
|
|
6
7
|
import type { Worker } from "./claude-turn.ts";
|
|
7
8
|
export interface DaemonSettings {
|
|
9
|
+
readonly fallbackModels?: readonly string[];
|
|
8
10
|
readonly tokenPrices?: TokenPrices;
|
|
9
11
|
readonly worker?: Worker;
|
|
10
12
|
readonly profiles?: TurnProfiles;
|
|
@@ -13,6 +15,7 @@ export interface DaemonSettings {
|
|
|
13
15
|
readonly repository: string;
|
|
14
16
|
readonly manifests: Readonly<Record<string, string | undefined>>;
|
|
15
17
|
readonly declarations?: readonly DeclaredWorkflow[];
|
|
18
|
+
readonly workflowRefusals?: readonly WorkflowRefusal[];
|
|
16
19
|
readonly workflows: Readonly<Record<string, WorkflowFunction | undefined>>;
|
|
17
20
|
readonly handler: WorkflowHandler;
|
|
18
21
|
readonly command: readonly string[];
|
|
@@ -21,6 +24,7 @@ export interface DaemonSettings {
|
|
|
21
24
|
readonly notifyCommand?: readonly string[];
|
|
22
25
|
readonly notifyMilliseconds?: number;
|
|
23
26
|
readonly queueMilliseconds?: number;
|
|
27
|
+
readonly queueLanes?: number;
|
|
24
28
|
readonly resetQueue?: boolean;
|
|
25
29
|
readonly githubCommand?: readonly string[];
|
|
26
30
|
readonly checkCommand?: readonly string[];
|
|
@@ -46,4 +50,4 @@ export declare const bridgeTiming: {
|
|
|
46
50
|
pollMilliseconds: number;
|
|
47
51
|
retryMilliseconds: number;
|
|
48
52
|
};
|
|
49
|
-
export declare function startDaemon(
|
|
53
|
+
export declare function startDaemon(givenSettings: DaemonSettings): Promise<DaemonOpening>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises";
|
|
1
|
+
import { mkdir, realpath } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { startDelivery, } from "../host/start-delivery.js";
|
|
4
4
|
import { registerStepBridge, sdkStepCall, stepBridge, } from "../host/step-bridge-host.js";
|
|
@@ -6,8 +6,10 @@ import { startHost } from "../host/world-host.js";
|
|
|
6
6
|
import { worldLoop } from "../host/world-reconciliation.js";
|
|
7
7
|
import { cancelled, workflowOutcome, taggedRun, taggedRuns, } from "../host/world-runs.js";
|
|
8
8
|
import { startService, } from "../service/service.js";
|
|
9
|
+
import { workflowPin } from "../broker/workflow-pin.js";
|
|
9
10
|
import { claudeWorker } from "./claude-worker.js";
|
|
10
11
|
import { deliveryRouter } from "./delivery-router.js";
|
|
12
|
+
import { handoffWorker } from "./handoff-worker.js";
|
|
11
13
|
import { githubWorker } from "./github-worker.js";
|
|
12
14
|
import { chatsWorker } from "./chats-worker.js";
|
|
13
15
|
import { worldDirectory } from "./host-environment.js";
|
|
@@ -72,11 +74,16 @@ function githubOf(settings, given) {
|
|
|
72
74
|
}
|
|
73
75
|
function adapters(settings, options) {
|
|
74
76
|
const { directory, repository, command } = settings;
|
|
75
|
-
const { given, table } = options;
|
|
76
|
-
const
|
|
77
|
+
const { given, table, registrations } = options;
|
|
78
|
+
const functions = Object.fromEntries(table.map((row) => [row.workflowId, settings.workflows[row.functionName]]));
|
|
79
|
+
const workflows = Object.fromEntries(registrations.map((registration) => [
|
|
80
|
+
workflowPin(registration.manifest),
|
|
81
|
+
functions[registration.workflowId],
|
|
82
|
+
]));
|
|
77
83
|
const document = async (digest) => (await given).document(digest);
|
|
78
84
|
const report = async (effect) => (await given).reportEffect(effect);
|
|
79
85
|
const worker = claudeWorker({
|
|
86
|
+
fallbackModels: settings.fallbackModels ?? [],
|
|
80
87
|
tokenPrices: settings.tokenPrices ?? {},
|
|
81
88
|
worker: settings.worker ?? "claude",
|
|
82
89
|
profiles: settings.profiles ?? {},
|
|
@@ -99,18 +106,36 @@ function adapters(settings, options) {
|
|
|
99
106
|
runs: taggedRuns,
|
|
100
107
|
cancel: cancelled,
|
|
101
108
|
input: async (runId) => (await given).input(runId),
|
|
102
|
-
workflow:
|
|
109
|
+
workflow: ({ workflow }) => Promise.resolve(workflows[workflow]),
|
|
103
110
|
});
|
|
104
|
-
|
|
111
|
+
const handoff = handoffOf(settings, given);
|
|
112
|
+
return { worker, github, runs, chats, handoff };
|
|
105
113
|
}
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
function handoffOf(settings, given) {
|
|
115
|
+
return handoffWorker({
|
|
116
|
+
runtimeDirectory: settings.runtimeDirectory,
|
|
117
|
+
journal: join(settings.directory, "handoffs"),
|
|
118
|
+
document: async (digest) => (await given).document(digest),
|
|
119
|
+
runStatus: async (runId) => (await (await given).state()).runs[runId]?.status,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async function daemonRegistrations(settings) {
|
|
108
123
|
const table = ticketRegistrationTable({
|
|
109
124
|
repository: settings.repository,
|
|
110
125
|
command: settings.ticketCommand,
|
|
111
126
|
nextCommand: settings.nextCommand,
|
|
127
|
+
githubCommand: settings.githubCommand,
|
|
112
128
|
}, settings.declarations);
|
|
113
129
|
const admitted = await ticketRegistrations(settings.manifests, { table });
|
|
130
|
+
return { table, admitted };
|
|
131
|
+
}
|
|
132
|
+
export async function startDaemon(givenSettings) {
|
|
133
|
+
const settings = {
|
|
134
|
+
...givenSettings,
|
|
135
|
+
repository: await realpath(givenSettings.repository),
|
|
136
|
+
};
|
|
137
|
+
const { directory } = settings;
|
|
138
|
+
const { table, admitted } = await daemonRegistrations(settings);
|
|
114
139
|
if (admitted.kind === "rejected")
|
|
115
140
|
return {
|
|
116
141
|
kind: "refused",
|
|
@@ -118,25 +143,27 @@ export async function startDaemon(settings) {
|
|
|
118
143
|
detail: admitted.detail,
|
|
119
144
|
};
|
|
120
145
|
const given = Promise.withResolvers();
|
|
121
|
-
const { worker, github, runs, chats } = adapters(settings, {
|
|
146
|
+
const { worker, github, runs, chats, handoff } = adapters(settings, {
|
|
122
147
|
given: given.promise,
|
|
123
148
|
table,
|
|
149
|
+
registrations: admitted.registrations,
|
|
124
150
|
});
|
|
125
151
|
await mkdir(join(directory, "state"), { recursive: true });
|
|
126
152
|
await mkdir(settings.runtimeDirectory, { recursive: true, mode: 0o700 });
|
|
127
153
|
const opening = await startService({
|
|
128
154
|
stateDirectory: join(directory, "state"),
|
|
129
155
|
runtimeDirectory: settings.runtimeDirectory,
|
|
156
|
+
workflowRefusals: settings.workflowRefusals ?? [],
|
|
130
157
|
workflows: admitted.registrations.map((registration) => ({
|
|
131
158
|
...registration,
|
|
132
159
|
...(settings.queueMilliseconds !== undefined &&
|
|
133
160
|
registration.workflowId === "develop-issue"
|
|
134
|
-
? { admission:
|
|
161
|
+
? { admission: settings.queueLanes ?? 1 }
|
|
135
162
|
: {}),
|
|
136
163
|
})),
|
|
137
164
|
delivery: deliveryRouter({
|
|
138
165
|
runs,
|
|
139
|
-
effects: { claude: worker, github, chats },
|
|
166
|
+
effects: { claude: worker, github, chats, handoff },
|
|
140
167
|
}),
|
|
141
168
|
effects: { leaseMilliseconds: turnLeaseMilliseconds },
|
|
142
169
|
host: (ports) => {
|
|
@@ -181,6 +208,7 @@ async function runningDaemon(settings, resources) {
|
|
|
181
208
|
directory: settings.directory,
|
|
182
209
|
runtimeDirectory: settings.runtimeDirectory,
|
|
183
210
|
intervalMilliseconds: settings.queueMilliseconds,
|
|
211
|
+
lanes: settings.queueLanes ?? 1,
|
|
184
212
|
resetQueue: settings.resetQueue ?? false,
|
|
185
213
|
});
|
|
186
214
|
return {
|
|
@@ -25,6 +25,12 @@ export function deliveryRouter(routes) {
|
|
|
25
25
|
return runs.reconcile(intent);
|
|
26
26
|
return workerOf(routes, intent)?.reconcile(intent, signal) ?? refusal();
|
|
27
27
|
},
|
|
28
|
+
retry: (intent, signal) => {
|
|
29
|
+
if (!protectedEffect(intent))
|
|
30
|
+
return refusal();
|
|
31
|
+
return (workerOf(routes, intent)?.retry?.(intent, signal) ??
|
|
32
|
+
Promise.resolve({ kind: "unknown" }));
|
|
33
|
+
},
|
|
28
34
|
stop: (intent, signal) => {
|
|
29
35
|
if (!protectedEffect(intent))
|
|
30
36
|
return runs.stop(intent);
|
|
@@ -24,6 +24,10 @@ export interface Finding {
|
|
|
24
24
|
readonly line: number | null;
|
|
25
25
|
readonly body: string;
|
|
26
26
|
}
|
|
27
|
+
export declare function githubText(text: string, settings: {
|
|
28
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
29
|
+
readonly privateTexts?: readonly string[];
|
|
30
|
+
}): string;
|
|
27
31
|
export declare function calls(settings: CallSettings, signal: AbortSignal): Calls;
|
|
28
32
|
export declare function scoreOf(comments: unknown, commit: string): Scored | undefined;
|
|
29
33
|
export declare function findingsOf(comments: unknown, commit: string): readonly Finding[];
|
|
@@ -22,17 +22,26 @@ function privateEnvironment(environment) {
|
|
|
22
22
|
function failureDetail(error, place) {
|
|
23
23
|
const stderr = field(error, "stderr");
|
|
24
24
|
const text = typeof stderr === "string" ? stderr : "";
|
|
25
|
-
|
|
25
|
+
return {
|
|
26
|
+
detail: githubText(text, place).slice(-2_000),
|
|
27
|
+
status: Number(/\(HTTP (\d{3})\)/.exec(text)?.[1]),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function githubText(text, settings) {
|
|
31
|
+
const environment = { ...process.env, ...settings.environment };
|
|
32
|
+
const secrets = Object.entries(environment)
|
|
26
33
|
.filter(([name]) => /TOKEN|SECRET|PASSWORD|KEY/i.test(name))
|
|
27
34
|
.map(([, value]) => value);
|
|
28
|
-
const
|
|
35
|
+
const privateTexts = [
|
|
36
|
+
...(settings.privateTexts ?? []),
|
|
37
|
+
...privateEnvironment(environment),
|
|
38
|
+
];
|
|
39
|
+
const hidden = [...secrets, ...privateTexts]
|
|
29
40
|
.filter((value) => typeof value === "string" && value !== "")
|
|
30
41
|
.sort((left, right) => right.length - left.length);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
status: Number(/\(HTTP (\d{3})\)/.exec(text)?.[1]),
|
|
35
|
-
};
|
|
42
|
+
return hidden.reduce((redacted, value) => redacted.replaceAll(privateTexts.includes(value)
|
|
43
|
+
? new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi")
|
|
44
|
+
: value, "[hidden]"), text);
|
|
36
45
|
}
|
|
37
46
|
async function output(command, place) {
|
|
38
47
|
try {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { githubText } from "./github-calls.js";
|
|
3
|
+
import { githubRead } from "./github-retry.js";
|
|
4
|
+
import { field, githubTiming, repositoryPath, } from "./github-operation.js";
|
|
5
|
+
function isCheckRun(value) {
|
|
6
|
+
const conclusion = field(value, "conclusion");
|
|
7
|
+
const details = field(value, "details_url");
|
|
8
|
+
const app = field(value, "app");
|
|
9
|
+
return (typeof field(value, "id") === "number" &&
|
|
10
|
+
typeof field(value, "name") === "string" &&
|
|
11
|
+
typeof field(value, "status") === "string" &&
|
|
12
|
+
(conclusion === null || typeof conclusion === "string") &&
|
|
13
|
+
(details === null || typeof details === "string") &&
|
|
14
|
+
(app === null || typeof field(app, "slug") === "string"));
|
|
15
|
+
}
|
|
16
|
+
const passing = new Set(["success", "neutral", "skipped"]);
|
|
17
|
+
function latestRuns(checks, required) {
|
|
18
|
+
const latest = new Map();
|
|
19
|
+
for (const check of checks) {
|
|
20
|
+
const name = check.name;
|
|
21
|
+
if (required === undefined
|
|
22
|
+
? check.app?.slug !== "github-actions"
|
|
23
|
+
: !required.has(name))
|
|
24
|
+
continue;
|
|
25
|
+
const previous = latest.get(name);
|
|
26
|
+
if (previous === undefined || check.id > previous.id)
|
|
27
|
+
latest.set(name, check);
|
|
28
|
+
}
|
|
29
|
+
return [...latest.values()];
|
|
30
|
+
}
|
|
31
|
+
function list(value) {
|
|
32
|
+
return Array.isArray(value) ? value.map((item) => item) : [];
|
|
33
|
+
}
|
|
34
|
+
async function checkRuns(operation, commit) {
|
|
35
|
+
const pages = JSON.parse(await githubRead(operation, [
|
|
36
|
+
"api",
|
|
37
|
+
"--paginate",
|
|
38
|
+
"--slurp",
|
|
39
|
+
`${repositoryPath}/commits/${commit}/check-runs?filter=all&per_page=100`,
|
|
40
|
+
]));
|
|
41
|
+
if (!Array.isArray(pages) || pages.length === 0)
|
|
42
|
+
throw new Error("github-call-failed");
|
|
43
|
+
return list(pages).flatMap((page) => {
|
|
44
|
+
const checks = field(page, "check_runs");
|
|
45
|
+
if (!Array.isArray(checks) || !checks.every(isCheckRun))
|
|
46
|
+
throw new Error("github-call-failed");
|
|
47
|
+
return checks;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function unavailable(operation, error) {
|
|
51
|
+
operation.signal.throwIfAborted();
|
|
52
|
+
const detail = field(error, "detail");
|
|
53
|
+
return `CI diagnostics unavailable: ${githubText(typeof detail === "string" ? detail : "GitHub logs could not be read", operation.settings)}`;
|
|
54
|
+
}
|
|
55
|
+
async function failedSteps(operation, place) {
|
|
56
|
+
try {
|
|
57
|
+
const view = JSON.parse(await operation.calls.gh(["run", "view", place.run, "--json", "jobs"]));
|
|
58
|
+
return list(field(view, "jobs"))
|
|
59
|
+
.filter((job) => String(field(job, "databaseId")) === place.job)
|
|
60
|
+
.flatMap((job) => list(field(job, "steps")))
|
|
61
|
+
.filter((step) => typeof field(step, "conclusion") === "string" &&
|
|
62
|
+
!passing.has(String(field(step, "conclusion"))))
|
|
63
|
+
.map((step) => `Failed step: ${String(field(step, "name"))}`)
|
|
64
|
+
.join("\n");
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return unavailable(operation, error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function failedCheck(operation, check) {
|
|
71
|
+
const summary = [
|
|
72
|
+
`Failed check: ${check.name} (${check.conclusion ?? "null"})`,
|
|
73
|
+
];
|
|
74
|
+
const details = check.details_url ?? "";
|
|
75
|
+
const run = /\/actions\/runs\/(\d+)/.exec(details)?.[1];
|
|
76
|
+
let log = "Failed log unavailable.";
|
|
77
|
+
if (run !== undefined) {
|
|
78
|
+
const job = /\/job\/(\d+)/.exec(details)?.[1];
|
|
79
|
+
summary.push(await failedSteps(operation, { run, job }));
|
|
80
|
+
log = await failedLog(operation, { run, job });
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
summary: githubText(summary.join("\n"), operation.settings),
|
|
84
|
+
log: githubText(log, operation.settings),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function failedLog(operation, place) {
|
|
88
|
+
try {
|
|
89
|
+
return await operation.calls.gh([
|
|
90
|
+
"run",
|
|
91
|
+
"view",
|
|
92
|
+
place.run,
|
|
93
|
+
"--log-failed",
|
|
94
|
+
...(place.job === undefined ? [] : ["--job", place.job]),
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
operation.signal.throwIfAborted();
|
|
99
|
+
if (place.job === undefined)
|
|
100
|
+
return unavailable(operation, error);
|
|
101
|
+
return operation.calls
|
|
102
|
+
.gh(["api", `${repositoryPath}/actions/jobs/${place.job}/logs`])
|
|
103
|
+
.catch((failure) => unavailable(operation, failure));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function failedOutput(operation, checks) {
|
|
107
|
+
const reports = [];
|
|
108
|
+
for (const check of checks)
|
|
109
|
+
reports.push(await failedCheck(operation, check));
|
|
110
|
+
const summary = reports.map((report) => report.summary).join("\n");
|
|
111
|
+
const available = Math.max(0, 6_000 - summary.length - reports.length);
|
|
112
|
+
const allowance = Math.floor(available / reports.length);
|
|
113
|
+
return [
|
|
114
|
+
summary,
|
|
115
|
+
...reports.map((report) => allowance === 0 ? "" : report.log.slice(-allowance)),
|
|
116
|
+
]
|
|
117
|
+
.join("\n")
|
|
118
|
+
.slice(0, 6_000);
|
|
119
|
+
}
|
|
120
|
+
function requiredChecks(operation) {
|
|
121
|
+
const setting = operation.settings.environment?.["CLOVING_REQUIRED_CHECKS"] ??
|
|
122
|
+
process.env["CLOVING_REQUIRED_CHECKS"];
|
|
123
|
+
if (setting === undefined)
|
|
124
|
+
return undefined;
|
|
125
|
+
const names = JSON.parse(setting);
|
|
126
|
+
if (!Array.isArray(names) ||
|
|
127
|
+
!names.every((name) => typeof name === "string" && name.trim() !== ""))
|
|
128
|
+
throw new Error("invalid-payload");
|
|
129
|
+
return new Set(names);
|
|
130
|
+
}
|
|
131
|
+
function pendingChecks(checks, names) {
|
|
132
|
+
return [...names].filter((name) => !checks.some((check) => check.name === name && check.status === "completed"));
|
|
133
|
+
}
|
|
134
|
+
export async function waitForCi(operation, commit) {
|
|
135
|
+
const { pollMilliseconds: poll, ciGraceMilliseconds: grace, ciLimitMilliseconds: limit, } = {
|
|
136
|
+
...githubTiming,
|
|
137
|
+
ciGraceMilliseconds: 60_000,
|
|
138
|
+
ciLimitMilliseconds: 5_400_000,
|
|
139
|
+
...operation.settings.timing,
|
|
140
|
+
};
|
|
141
|
+
const started = Date.now();
|
|
142
|
+
const required = requiredChecks(operation);
|
|
143
|
+
const names = new Set(required);
|
|
144
|
+
for (;;) {
|
|
145
|
+
const checks = latestRuns(await checkRuns(operation, commit), required);
|
|
146
|
+
for (const check of checks)
|
|
147
|
+
names.add(check.name);
|
|
148
|
+
const failed = checks.filter((check) => check.status === "completed" && !passing.has(check.conclusion ?? ""));
|
|
149
|
+
if (failed.length > 0)
|
|
150
|
+
return {
|
|
151
|
+
reason: "ci-failed",
|
|
152
|
+
output: await failedOutput(operation, failed),
|
|
153
|
+
};
|
|
154
|
+
const pending = pendingChecks(checks, names);
|
|
155
|
+
if (pending.length === 0 &&
|
|
156
|
+
(required !== undefined ||
|
|
157
|
+
names.size > 0 ||
|
|
158
|
+
Date.now() - started >= grace))
|
|
159
|
+
return undefined;
|
|
160
|
+
const remaining = limit - (Date.now() - started);
|
|
161
|
+
if (remaining <= 0)
|
|
162
|
+
return {
|
|
163
|
+
reason: "ci-pending",
|
|
164
|
+
output: `CI wait limit reached for ${commit}. Pending checks: ${pending.join(", ") || "waiting for GitHub Actions checks"}`,
|
|
165
|
+
};
|
|
166
|
+
await delay(Math.min(poll, remaining), undefined, {
|
|
167
|
+
signal: operation.signal,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Operation } from "./github-operation.ts";
|
|
2
|
+
export interface ReviewedPull {
|
|
3
|
+
readonly number: number;
|
|
4
|
+
readonly commit: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function knownMerge(operation: Operation, expected: ReviewedPull & {
|
|
7
|
+
readonly updated: string;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
readonly head: string;
|
|
10
|
+
readonly merged: string | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function mergePull(operation: Operation, pull: ReviewedPull & {
|
|
13
|
+
readonly updated: string;
|
|
14
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { field, githubTiming, repositoryPath, } from "./github-operation.js";
|
|
3
|
+
import { githubRead, retryGithub } from "./github-retry.js";
|
|
4
|
+
function commitOf(value, name) {
|
|
5
|
+
if (typeof value !== "object" || value === null)
|
|
6
|
+
throw new Error("github-call-failed");
|
|
7
|
+
const commit = Reflect.get(value, name);
|
|
8
|
+
if (typeof commit !== "string")
|
|
9
|
+
throw new Error("github-call-failed");
|
|
10
|
+
return commit;
|
|
11
|
+
}
|
|
12
|
+
export async function knownMerge(operation, expected) {
|
|
13
|
+
const pull = JSON.parse(await githubRead(operation, [
|
|
14
|
+
"api",
|
|
15
|
+
`${repositoryPath}/pulls/${String(expected.number)}`,
|
|
16
|
+
]));
|
|
17
|
+
const head = commitOf(field(pull, "head"), "sha");
|
|
18
|
+
if (field(pull, "merged") !== true)
|
|
19
|
+
return { head, merged: undefined };
|
|
20
|
+
if (head !== expected.commit && head !== expected.updated)
|
|
21
|
+
throw new Error("merged-other-commit");
|
|
22
|
+
return { head, merged: commitOf(pull, "merge_commit_sha") };
|
|
23
|
+
}
|
|
24
|
+
async function mergedNow(operation, pull) {
|
|
25
|
+
const reply = JSON.parse(await operation.calls.gh([
|
|
26
|
+
"api",
|
|
27
|
+
"-X",
|
|
28
|
+
"PUT",
|
|
29
|
+
`${repositoryPath}/pulls/${String(pull.number)}/merge`,
|
|
30
|
+
"-f",
|
|
31
|
+
"merge_method=merge",
|
|
32
|
+
"-f",
|
|
33
|
+
`sha=${pull.commit}`,
|
|
34
|
+
]));
|
|
35
|
+
return commitOf(reply, "sha");
|
|
36
|
+
}
|
|
37
|
+
async function waitForHead(operation, pull) {
|
|
38
|
+
const timing = operation.settings.timing ?? githubTiming;
|
|
39
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
40
|
+
if (attempt > 0)
|
|
41
|
+
await delay(timing.pollMilliseconds, undefined, {
|
|
42
|
+
signal: operation.signal,
|
|
43
|
+
});
|
|
44
|
+
const current = JSON.parse(await githubRead(operation, [
|
|
45
|
+
"api",
|
|
46
|
+
`${repositoryPath}/pulls/${String(pull.number)}`,
|
|
47
|
+
]));
|
|
48
|
+
if (commitOf(field(current, "head"), "sha") === pull.commit)
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
throw new Error("head-not-updated");
|
|
52
|
+
}
|
|
53
|
+
export function mergePull(operation, pull) {
|
|
54
|
+
return retryGithub(operation, {
|
|
55
|
+
action: async (attempt) => {
|
|
56
|
+
const known = attempt > 0 ? await knownMerge(operation, pull) : undefined;
|
|
57
|
+
if (known?.merged !== undefined)
|
|
58
|
+
return known.merged;
|
|
59
|
+
const updated = { number: pull.number, commit: pull.updated };
|
|
60
|
+
if (pull.updated !== pull.commit)
|
|
61
|
+
await waitForHead(operation, updated);
|
|
62
|
+
return mergedNow(operation, updated);
|
|
63
|
+
},
|
|
64
|
+
statuses: pull.updated === pull.commit ? [] : [405, 409],
|
|
65
|
+
}).catch(async (error) => {
|
|
66
|
+
if (!(error instanceof Error) || error.message !== "github-network-failed")
|
|
67
|
+
throw error;
|
|
68
|
+
const known = await knownMerge(operation, pull);
|
|
69
|
+
if (known.merged !== undefined)
|
|
70
|
+
return known.merged;
|
|
71
|
+
throw error;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { type Operation } from "./github-operation.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
readonly commit: string;
|
|
5
|
-
}
|
|
2
|
+
import { type CiResult } from "./github-ci.ts";
|
|
3
|
+
import { type ReviewedPull } from "./github-merge-call.ts";
|
|
6
4
|
interface SuccessfulMerge {
|
|
7
5
|
readonly number: number;
|
|
8
6
|
readonly commit: string;
|
|
@@ -13,6 +11,6 @@ interface MergeRepair {
|
|
|
13
11
|
readonly reason: "check-failed" | "merge-conflict";
|
|
14
12
|
readonly output: string;
|
|
15
13
|
}
|
|
16
|
-
export type MergeResult = SuccessfulMerge | MergeRepair;
|
|
14
|
+
export type MergeResult = SuccessfulMerge | MergeRepair | CiResult;
|
|
17
15
|
export declare function merge(operation: Operation, pull: ReviewedPull): Promise<MergeResult>;
|
|
18
16
|
export {};
|