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.
- package/README.md +145 -14
- package/dist/src/authority/commands.js +10 -2
- package/dist/src/authority/reconciliation.d.ts +2 -0
- package/dist/src/authority/reconciliation.js +10 -1
- package/dist/src/authority/transitions.d.ts +1 -1
- package/dist/src/authority/transitions.js +2 -0
- package/dist/src/broker/actor/authority-session.d.ts +2 -0
- package/dist/src/broker/actor/authority-session.js +3 -4
- package/dist/src/chats/analysis-batches.d.ts +24 -0
- package/dist/src/chats/analysis-batches.js +83 -0
- package/dist/src/chats/analysis-contract.d.ts +80 -0
- package/dist/src/chats/analysis-contract.js +16 -0
- package/dist/src/chats/analysis-data.d.ts +3 -0
- package/dist/src/chats/analysis-data.js +85 -0
- package/dist/src/chats/analysis-input.d.ts +12 -0
- package/dist/src/chats/analysis-input.js +83 -0
- package/dist/src/chats/analysis-links.d.ts +2 -0
- package/dist/src/chats/analysis-links.js +25 -0
- package/dist/src/chats/analysis-prompt.d.ts +10 -0
- package/dist/src/chats/analysis-prompt.js +16 -0
- package/dist/src/chats/analysis-proposal.d.ts +2 -0
- package/dist/src/chats/analysis-proposal.js +32 -0
- package/dist/src/chats/analysis-provenance.d.ts +4 -0
- package/dist/src/chats/analysis-provenance.js +99 -0
- package/dist/src/chats/analysis-shape.d.ts +3 -0
- package/dist/src/chats/analysis-shape.js +228 -0
- package/dist/src/chats/analysis.d.ts +3 -0
- package/dist/src/chats/analysis.js +42 -0
- package/dist/src/chats/corpus-input.d.ts +5 -0
- package/dist/src/chats/corpus-input.js +288 -0
- package/dist/src/chats/destination.js +3 -1
- package/dist/src/chats/import.d.ts +8 -6
- package/dist/src/chats/import.js +195 -23
- package/dist/src/chats/input-file.d.ts +3 -0
- package/dist/src/chats/input-file.js +32 -0
- package/dist/src/chats/normalize.d.ts +11 -2
- package/dist/src/chats/normalize.js +18 -2
- package/dist/src/chats/previous-import.d.ts +5 -0
- package/dist/src/chats/previous-import.js +34 -0
- package/dist/src/chats/reader.d.ts +7 -1
- package/dist/src/chats/reader.js +46 -15
- package/dist/src/chats/redact-import.d.ts +14 -0
- package/dist/src/chats/redact-import.js +141 -0
- package/dist/src/chats/redaction.d.ts +41 -0
- package/dist/src/chats/redaction.js +161 -0
- package/dist/src/chats/revision-input.d.ts +3 -0
- package/dist/src/chats/revision-input.js +180 -0
- package/dist/src/chats/revision.d.ts +48 -0
- package/dist/src/chats/revision.js +94 -0
- package/dist/src/chats/selection.d.ts +25 -0
- package/dist/src/chats/selection.js +109 -0
- package/dist/src/chats/source.d.ts +23 -0
- package/dist/src/chats/source.js +135 -0
- package/dist/src/chats/structure.d.ts +57 -0
- package/dist/src/chats/structure.js +216 -0
- package/dist/src/cli/arguments.d.ts +8 -0
- package/dist/src/cli/arguments.js +94 -13
- package/dist/src/cli/client.d.ts +1 -0
- package/dist/src/cli/client.js +38 -1
- package/dist/src/cli/commands.d.ts +3 -0
- package/dist/src/cli/commands.js +29 -11
- package/dist/src/cli/main.d.ts +2 -1
- package/dist/src/cli/main.js +61 -14
- package/dist/src/cli/render.d.ts +2 -0
- package/dist/src/cli/render.js +70 -2
- package/dist/src/cli/usage-render.d.ts +3 -0
- package/dist/src/cli/usage-render.js +28 -0
- package/dist/src/cli/usage.d.ts +3 -2
- package/dist/src/cli/usage.js +38 -6
- package/dist/src/cli/watch-actions.d.ts +10 -0
- package/dist/src/cli/watch-actions.js +58 -0
- package/dist/src/cli/watch-board-view.d.ts +32 -0
- package/dist/src/cli/watch-board-view.js +88 -15
- package/dist/src/cli/watch-board.js +6 -1
- package/dist/src/cli/watch-card-client.d.ts +1 -0
- package/dist/src/cli/watch-card-client.js +44 -0
- package/dist/src/cli/watch-card-forms.d.ts +2 -0
- package/dist/src/cli/watch-card-forms.js +33 -0
- package/dist/src/cli/watch-card-reply.d.ts +5 -0
- package/dist/src/cli/watch-card-reply.js +34 -0
- package/dist/src/cli/watch-card-view.d.ts +40 -0
- package/dist/src/cli/watch-card-view.js +86 -0
- package/dist/src/cli/watch-client.d.ts +1 -0
- package/dist/src/cli/watch-client.js +118 -0
- package/dist/src/cli/watch-model.js +31 -14
- package/dist/src/cli/watch-page.d.ts +1 -1
- package/dist/src/cli/watch-page.js +71 -85
- package/dist/src/cli/watch-pr-board.d.ts +3 -0
- package/dist/src/cli/watch-pr-board.js +26 -0
- package/dist/src/cli/watch-pr-command.d.ts +15 -0
- package/dist/src/cli/watch-pr-command.js +37 -0
- package/dist/src/cli/watch-pr-identity.d.ts +20 -0
- package/dist/src/cli/watch-pr-identity.js +62 -0
- package/dist/src/cli/watch-pr-observer.d.ts +19 -0
- package/dist/src/cli/watch-pr-observer.js +108 -0
- package/dist/src/cli/watch-pr-settings.d.ts +8 -0
- package/dist/src/cli/watch-pr-settings.js +47 -0
- package/dist/src/cli/watch-reads.js +4 -0
- package/dist/src/cli/watch-render.d.ts +16 -2
- package/dist/src/cli/watch-render.js +10 -3
- package/dist/src/cli/watch-run-input.d.ts +14 -0
- package/dist/src/cli/watch-run-input.js +49 -0
- package/dist/src/cli/watch-server.d.ts +3 -0
- package/dist/src/cli/watch-server.js +44 -6
- package/dist/src/cli/watch-start-view.d.ts +18 -0
- package/dist/src/cli/watch-start-view.js +77 -0
- package/dist/src/cli/watch-turn.d.ts +1 -0
- package/dist/src/cli/watch-turn.js +7 -2
- package/dist/src/cli/watch-usage.d.ts +6 -0
- package/dist/src/cli/watch-usage.js +65 -0
- package/dist/src/cli/watch-view.d.ts +15 -0
- package/dist/src/cli/watch-view.js +61 -17
- package/dist/src/cli.js +48 -6
- package/dist/src/daemon/analysis-launch.d.ts +5 -0
- package/dist/src/daemon/analysis-launch.js +81 -0
- package/dist/src/daemon/analysis-token.d.ts +6 -0
- package/dist/src/daemon/analysis-token.js +88 -0
- package/dist/src/daemon/analysis-worker.d.ts +22 -0
- package/dist/src/daemon/analysis-worker.js +219 -0
- package/dist/src/daemon/chats-files.d.ts +15 -0
- package/dist/src/daemon/chats-files.js +113 -0
- package/dist/src/daemon/chats-worker.d.ts +12 -0
- package/dist/src/daemon/chats-worker.js +200 -0
- package/dist/src/daemon/claude-turn.d.ts +10 -0
- package/dist/src/daemon/claude-turn.js +96 -15
- package/dist/src/daemon/claude-worker.d.ts +4 -1
- package/dist/src/daemon/claude-worker.js +99 -47
- package/dist/src/daemon/codex-stream.js +16 -0
- package/dist/src/daemon/daemon.d.ts +3 -0
- package/dist/src/daemon/daemon.js +16 -7
- package/dist/src/daemon/github-methods.js +4 -0
- package/dist/src/daemon/github-ticket-status.d.ts +3 -0
- package/dist/src/daemon/github-ticket-status.js +268 -0
- package/dist/src/daemon/issue-queue.d.ts +1 -0
- package/dist/src/daemon/issue-queue.js +115 -24
- package/dist/src/daemon/operation-journal.d.ts +1 -0
- package/dist/src/daemon/operation-journal.js +4 -2
- package/dist/src/daemon/retrying-turn.d.ts +9 -0
- package/dist/src/daemon/retrying-turn.js +185 -0
- package/dist/src/host/world-reconciliation.d.ts +2 -0
- package/dist/src/host/world-reconciliation.js +6 -0
- package/dist/src/host/world-runs.js +9 -1
- package/dist/src/service/run-handlers.js +15 -23
- package/dist/src/service/worker-totals.d.ts +3 -0
- package/dist/src/service/worker-totals.js +28 -0
- package/dist/src/ticket-status.d.ts +8 -0
- package/dist/src/ticket-status.js +45 -0
- package/dist/src/token-prices.d.ts +19 -0
- package/dist/src/token-prices.js +76 -0
- package/dist/src/worker-usage.d.ts +22 -0
- package/dist/src/worker-usage.js +44 -0
- package/dist/src/workflow-candidate.d.ts +29 -0
- package/dist/src/workflow-candidate.js +59 -0
- package/dist/src/workflow-contracts.d.ts +2 -0
- package/dist/src/workflow-contracts.js +27 -5
- package/dist/src/workflow-declaration.d.ts +1 -1
- package/dist/src/workflow-declaration.js +3 -1
- package/dist/src/workflows.d.ts +1 -0
- package/dist/src/workflows.js +1 -0
- package/dist/tools/self-host-processes.js +7 -2
- package/dist/tools/self-host-workflows.d.ts +1 -1
- package/dist/tools/self-host-workflows.js +2 -2
- package/dist/tools/self-host.js +6 -6
- package/dist/tools/self-update-host.d.ts +7 -0
- package/dist/tools/self-update-host.js +48 -1
- package/dist/tools/self-update.d.ts +1 -0
- package/dist/tools/self-update.js +85 -13
- package/dist/workflows/analyze-chats.d.ts +10 -0
- package/dist/workflows/analyze-chats.declaration.json +9 -0
- package/dist/workflows/analyze-chats.js +73 -0
- package/dist/workflows/develop-ticket.d.ts +1 -7
- package/dist/workflows/develop-ticket.js +42 -141
- package/dist/workflows/issue-labels.d.ts +3 -1
- package/dist/workflows/issue-labels.js +34 -0
- package/dist/workflows/ticket-prompts.d.ts +35 -0
- package/dist/workflows/ticket-prompts.js +133 -0
- package/package.json +5 -3
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { priceText, usageText, totalsSuffix } from "./usage-render.js";
|
|
1
2
|
import { component, element } from "./watch-element.js";
|
|
2
3
|
import { clockTime, completedDetail, counted, elapsed, phaseTime, runName, runTone, waitingDetail, } from "./watch-render.js";
|
|
3
|
-
import { Board, WorkflowList } from "./watch-board-view.js";
|
|
4
|
+
import { Board, WorkflowList, } from "./watch-board-view.js";
|
|
4
5
|
import { boardWorkflow, watchHash, watchLocation } from "./watch-navigation.js";
|
|
5
6
|
import { Decision, Events, Turn } from "./watch-turn.js";
|
|
7
|
+
import { StartRun } from "./watch-start-view.js";
|
|
6
8
|
export const clove = {
|
|
7
9
|
viewBox: "0.6625858545303345 0.6630800366401672 46.674827575683594 46.673831939697266",
|
|
8
10
|
paths: [
|
|
@@ -140,6 +142,16 @@ export function Run(props) {
|
|
|
140
142
|
];
|
|
141
143
|
return [
|
|
142
144
|
component(Banner, { run, now }),
|
|
145
|
+
element("section", {
|
|
146
|
+
class: "run-usage",
|
|
147
|
+
children: [
|
|
148
|
+
element("h3", { children: `Run totals${totalsSuffix(run.state)}` }),
|
|
149
|
+
element("p", { children: usageText(run.usage) }),
|
|
150
|
+
element("p", {
|
|
151
|
+
children: priceText(run.claudeCostUsd, run.estimatedCostUsd),
|
|
152
|
+
}),
|
|
153
|
+
],
|
|
154
|
+
}),
|
|
143
155
|
component(Phases, { phases: run.phases, now }),
|
|
144
156
|
component(PullRequest, { run }),
|
|
145
157
|
run.decisions.map((decision) => component(Decision, { decision, turn, copying })),
|
|
@@ -173,6 +185,16 @@ export function Header(props) {
|
|
|
173
185
|
],
|
|
174
186
|
}),
|
|
175
187
|
stamp,
|
|
188
|
+
props.start === undefined
|
|
189
|
+
? null
|
|
190
|
+
: element("button", {
|
|
191
|
+
type: "button",
|
|
192
|
+
class: "start-toggle",
|
|
193
|
+
"aria-controls": "start-run",
|
|
194
|
+
"aria-expanded": String(props.start.shown),
|
|
195
|
+
onClick: props.start.onToggle,
|
|
196
|
+
children: "Start a run",
|
|
197
|
+
}),
|
|
176
198
|
],
|
|
177
199
|
});
|
|
178
200
|
}
|
|
@@ -203,21 +225,43 @@ export function Page(props) {
|
|
|
203
225
|
: component(Run, { key: run.runId, run, now, copying }),
|
|
204
226
|
];
|
|
205
227
|
}
|
|
228
|
+
return component(BoardPage, { ...props, model, workflowId, down });
|
|
229
|
+
}
|
|
230
|
+
export function BoardPage(props) {
|
|
231
|
+
const { model, workflowId, down, now, stale, copying } = props;
|
|
206
232
|
const workflow = model.workflows.find((item) => item.workflowId === workflowId);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
233
|
+
const start = props.start ?? { shown: false, onToggle: () => undefined };
|
|
234
|
+
return element("div", {
|
|
235
|
+
class: "board-page",
|
|
236
|
+
children: [
|
|
237
|
+
component(Header, { model, stale, start }),
|
|
238
|
+
down,
|
|
239
|
+
component(StartRun, {
|
|
240
|
+
workflows: model.workflows,
|
|
241
|
+
copying,
|
|
242
|
+
shown: start.shown,
|
|
243
|
+
}),
|
|
244
|
+
component(WorkflowList, {
|
|
245
|
+
workflows: model.workflows,
|
|
246
|
+
selected: workflowId,
|
|
247
|
+
}),
|
|
248
|
+
element("div", {
|
|
249
|
+
class: "workspace",
|
|
250
|
+
children: [
|
|
251
|
+
workflow === undefined
|
|
252
|
+
? element("p", {
|
|
253
|
+
class: "meta",
|
|
254
|
+
children: "No workflows registered.",
|
|
255
|
+
})
|
|
256
|
+
: component(Board, {
|
|
257
|
+
workflow,
|
|
258
|
+
now,
|
|
259
|
+
controls: props.columns,
|
|
260
|
+
cardForm: model.cardForms?.find((form) => form.workflowId === workflowId),
|
|
261
|
+
cards: props.cards,
|
|
262
|
+
}),
|
|
263
|
+
],
|
|
264
|
+
}),
|
|
265
|
+
],
|
|
266
|
+
});
|
|
223
267
|
}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,11 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { addAbortListener } from "node:events";
|
|
3
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
4
|
import { delimiter, dirname, extname, join } from "node:path";
|
|
4
5
|
import { main } from "./cli/main.js";
|
|
5
6
|
import { clientCommand, runtimeDirectory } from "./cli/runtime-directory.js";
|
|
6
7
|
import { commandUsage } from "./cli/usage.js";
|
|
7
8
|
const [command, ...options] = process.argv.slice(2);
|
|
8
|
-
|
|
9
|
+
const watchCommand = async (request) => {
|
|
10
|
+
const [command = "", ...argumentsList] = request.command;
|
|
11
|
+
const completed = Promise.withResolvers();
|
|
12
|
+
const stderr = { text: "" };
|
|
13
|
+
const execution = execFile(command, argumentsList, {
|
|
14
|
+
cwd: request.directory,
|
|
15
|
+
signal: request.signal,
|
|
16
|
+
killSignal: "SIGKILL",
|
|
17
|
+
maxBuffer: 1024 * 1024,
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
}, (error, stdout) => {
|
|
20
|
+
const code = error === null ? 0 : error.code;
|
|
21
|
+
completed.resolve({
|
|
22
|
+
stdout,
|
|
23
|
+
stderr: stderr.text,
|
|
24
|
+
status: typeof code === "number" ? code : null,
|
|
25
|
+
errorCode: typeof code === "string" ? code : null,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
execution.stderr?.on("data", (chunk) => {
|
|
29
|
+
stderr.text += chunk.toString();
|
|
30
|
+
});
|
|
31
|
+
const closed = new Promise((resolve) => execution.once("close", () => {
|
|
32
|
+
resolve();
|
|
33
|
+
}));
|
|
34
|
+
const listener = addAbortListener(request.signal, () => {
|
|
35
|
+
execution.kill("SIGKILL");
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
await closed;
|
|
39
|
+
return await completed.promise;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
listener[Symbol.dispose]();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function started(runtime, argumentsList) {
|
|
9
46
|
const program = join(import.meta.dirname, "..", "tools", `self-host${extname(import.meta.filename)}`);
|
|
10
47
|
const located = [
|
|
11
48
|
"--runtime",
|
|
@@ -15,7 +52,7 @@ function started(runtime) {
|
|
|
15
52
|
"--client",
|
|
16
53
|
clientCommand(process.env),
|
|
17
54
|
];
|
|
18
|
-
const child = spawn(process.execPath, [program, ...located, ...
|
|
55
|
+
const child = spawn(process.execPath, [program, ...located, ...argumentsList], {
|
|
19
56
|
stdio: "inherit",
|
|
20
57
|
env: {
|
|
21
58
|
...process.env,
|
|
@@ -48,9 +85,10 @@ async function start() {
|
|
|
48
85
|
}
|
|
49
86
|
const runtime = runtimeDirectory(process.env);
|
|
50
87
|
if (runtime !== undefined) {
|
|
51
|
-
let status = await started(runtime);
|
|
88
|
+
let status = await started(runtime, options);
|
|
89
|
+
const restarting = options.filter((option) => option !== "--reset-queue");
|
|
52
90
|
while (status === 75)
|
|
53
|
-
status = await started(runtime);
|
|
91
|
+
status = await started(runtime, restarting);
|
|
54
92
|
return status === 0 ? 0 : 1;
|
|
55
93
|
}
|
|
56
94
|
process.stderr.write("runtime-directory-unknown: Set CLOVING_RUNTIME_DIR to the runtime directory of the service.\n");
|
|
@@ -59,4 +97,8 @@ async function start() {
|
|
|
59
97
|
if (command === "start")
|
|
60
98
|
process.exitCode = await start();
|
|
61
99
|
else
|
|
62
|
-
await main(process.argv.slice(2)
|
|
100
|
+
await main(process.argv.slice(2), {
|
|
101
|
+
repository: process.env["CLOVING_WATCH_REPOSITORY"],
|
|
102
|
+
githubCommand: process.env["CLOVING_GH_COMMAND"],
|
|
103
|
+
execute: watchCommand,
|
|
104
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { analysisInstruction } from "../chats/analysis-prompt.ts";
|
|
2
|
+
export declare const analysisMethod = "run-turn:analyze-chats";
|
|
3
|
+
export declare function analysisCommand(): readonly string[];
|
|
4
|
+
export declare function analysisEnvironment(folder: string): NodeJS.ProcessEnv;
|
|
5
|
+
export declare function analysisAvailable(folder: string): Promise<boolean>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { analysisInstruction } from "../chats/analysis-prompt.js";
|
|
5
|
+
export { analysisInstruction } from "../chats/analysis-prompt.js";
|
|
6
|
+
export const analysisMethod = "run-turn:analyze-chats";
|
|
7
|
+
const controls = [
|
|
8
|
+
"--safe-mode",
|
|
9
|
+
"--restricted",
|
|
10
|
+
"--strict-mcp-config",
|
|
11
|
+
"--disable-slash-commands",
|
|
12
|
+
"--no-chrome",
|
|
13
|
+
"--no-session-persistence",
|
|
14
|
+
];
|
|
15
|
+
export function analysisCommand() {
|
|
16
|
+
return [
|
|
17
|
+
"claude",
|
|
18
|
+
...controls,
|
|
19
|
+
"--print",
|
|
20
|
+
"--output-format",
|
|
21
|
+
"stream-json",
|
|
22
|
+
"--verbose",
|
|
23
|
+
"--tools",
|
|
24
|
+
"",
|
|
25
|
+
"--disallowedTools",
|
|
26
|
+
"mcp__*",
|
|
27
|
+
"--mcp-config",
|
|
28
|
+
'{"mcpServers":{}}',
|
|
29
|
+
"--setting-sources",
|
|
30
|
+
"",
|
|
31
|
+
"--settings",
|
|
32
|
+
'{"disableAllHooks":true}',
|
|
33
|
+
"--permission-prompts",
|
|
34
|
+
"none",
|
|
35
|
+
"--max-turns",
|
|
36
|
+
"1",
|
|
37
|
+
"--max-budget-usd",
|
|
38
|
+
"5",
|
|
39
|
+
"--model",
|
|
40
|
+
"sonnet",
|
|
41
|
+
"--system-prompt",
|
|
42
|
+
analysisInstruction,
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
export function analysisEnvironment(folder) {
|
|
46
|
+
return {
|
|
47
|
+
PATH: process.env["PATH"],
|
|
48
|
+
HOME: join(folder, "configuration"),
|
|
49
|
+
CLAUDE_CONFIG_DIR: join(folder, "configuration"),
|
|
50
|
+
TMPDIR: join(folder, "configuration"),
|
|
51
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function supportedVersion(line) {
|
|
55
|
+
const match = /^2\.(0|[1-9]\d*)\.(0|[1-9]\d*) \(Claude Code\)$/.exec(line.trim());
|
|
56
|
+
if (match?.[1] === undefined || match[2] === undefined)
|
|
57
|
+
return false;
|
|
58
|
+
const minor = BigInt(match[1]);
|
|
59
|
+
return minor > 1n || (minor === 1n && BigInt(match[2]) >= 302n);
|
|
60
|
+
}
|
|
61
|
+
export async function analysisAvailable(folder) {
|
|
62
|
+
const probe = (flag) => promisify(execFile)("claude", [flag], {
|
|
63
|
+
cwd: folder,
|
|
64
|
+
env: analysisEnvironment(folder),
|
|
65
|
+
timeout: 5_000,
|
|
66
|
+
maxBuffer: 131_072,
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
const version = await probe("--version");
|
|
70
|
+
if (!supportedVersion(version.stdout))
|
|
71
|
+
return false;
|
|
72
|
+
const help = await probe("--help");
|
|
73
|
+
const advertised = new Set(help.stdout.match(/--[a-zA-Z][a-zA-Z0-9-]*/g) ?? []);
|
|
74
|
+
return analysisCommand()
|
|
75
|
+
.filter((argument) => argument.startsWith("--"))
|
|
76
|
+
.every((flag) => advertised.has(flag));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
function failed(category) {
|
|
3
|
+
return { error: "analysis-token-command-" + category };
|
|
4
|
+
}
|
|
5
|
+
function commandOf(setting) {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = JSON.parse(setting);
|
|
8
|
+
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
const entries = parsed;
|
|
11
|
+
if (entries.every((entry) => typeof entry === "string" && entry.length > 0))
|
|
12
|
+
return entries;
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function tokenOf(chunks) {
|
|
20
|
+
const token = Buffer.concat(chunks)
|
|
21
|
+
.toString("utf8")
|
|
22
|
+
.replace(/(?:\r\n|[\r\n])$/, "");
|
|
23
|
+
return /^[!-~]{1,512}$/.test(token) ? { token } : failed("output-invalid");
|
|
24
|
+
}
|
|
25
|
+
function captured(command, request) {
|
|
26
|
+
return new Promise((settle) => {
|
|
27
|
+
const [program = "", ...arguments_] = command;
|
|
28
|
+
const child = spawn(program, arguments_, {
|
|
29
|
+
cwd: request.folder,
|
|
30
|
+
shell: false,
|
|
31
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
32
|
+
signal: request.signal,
|
|
33
|
+
killSignal: "SIGKILL",
|
|
34
|
+
});
|
|
35
|
+
const chunks = [];
|
|
36
|
+
let bytes = 0;
|
|
37
|
+
let stopped = false;
|
|
38
|
+
const stop = (category) => {
|
|
39
|
+
stopped = true;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
child.kill("SIGKILL");
|
|
42
|
+
child.stdout.destroy();
|
|
43
|
+
child.stderr.destroy();
|
|
44
|
+
settle(failed(category));
|
|
45
|
+
};
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
stop("timeout");
|
|
48
|
+
}, 10_000);
|
|
49
|
+
const received = (chunk, output) => {
|
|
50
|
+
if (stopped)
|
|
51
|
+
return;
|
|
52
|
+
bytes += chunk.length;
|
|
53
|
+
if (bytes > 4_096) {
|
|
54
|
+
stop("output-too-large");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
output?.push(chunk);
|
|
58
|
+
};
|
|
59
|
+
child.stdout.on("data", (chunk) => {
|
|
60
|
+
received(chunk, chunks);
|
|
61
|
+
});
|
|
62
|
+
child.stderr.on("data", (chunk) => {
|
|
63
|
+
received(chunk);
|
|
64
|
+
});
|
|
65
|
+
child.once("error", () => {
|
|
66
|
+
stop("failed");
|
|
67
|
+
});
|
|
68
|
+
child.once("close", (code) => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
if (!stopped)
|
|
71
|
+
settle(code === 0 ? tokenOf(chunks) : failed("failed"));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export async function analysisToken(folder, signal) {
|
|
76
|
+
const setting = process.env["CLOVING_ANALYSIS_TOKEN_COMMAND"];
|
|
77
|
+
if (setting === undefined)
|
|
78
|
+
return failed("unset");
|
|
79
|
+
const command = commandOf(setting);
|
|
80
|
+
if (command === undefined)
|
|
81
|
+
return failed("invalid");
|
|
82
|
+
try {
|
|
83
|
+
return await captured(command, { folder, signal });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return failed("failed");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AdapterOutcome, DeliveryIntent } from "../broker/actor/delivery-intents.ts";
|
|
2
|
+
import { type TokenPrices } from "../token-prices.ts";
|
|
3
|
+
import { type RetrySettings } from "./retrying-turn.ts";
|
|
4
|
+
import { type WorkerReport } from "./operation-journal.ts";
|
|
5
|
+
interface Settings extends RetrySettings {
|
|
6
|
+
readonly tokenPrices?: TokenPrices;
|
|
7
|
+
readonly journal: string;
|
|
8
|
+
readonly document: (digest: string) => Promise<string | undefined>;
|
|
9
|
+
readonly report: (report: WorkerReport) => Promise<unknown>;
|
|
10
|
+
readonly stopMilliseconds?: number;
|
|
11
|
+
}
|
|
12
|
+
interface AnalysisWorker {
|
|
13
|
+
readonly deliver: (intent: DeliveryIntent, signal: AbortSignal) => Promise<AdapterOutcome | {
|
|
14
|
+
readonly receipt: string;
|
|
15
|
+
readonly worker: string;
|
|
16
|
+
}>;
|
|
17
|
+
readonly reconcile: (intent: DeliveryIntent, signal: AbortSignal) => Promise<AdapterOutcome>;
|
|
18
|
+
readonly stop: (intent: DeliveryIntent, signal: AbortSignal) => Promise<AdapterOutcome>;
|
|
19
|
+
readonly close: () => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare function analysisWorker(settings: Settings): AnalysisWorker;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { analysisPrompt } from "../chats/analysis-prompt.js";
|
|
4
|
+
import { isAnalysisSnapshot } from "../chats/analysis-input.js";
|
|
5
|
+
import { record } from "../protocol.js";
|
|
6
|
+
import { estimatedCostUsd, priceForModel, } from "../token-prices.js";
|
|
7
|
+
import { startRetriedTurn } from "./retrying-turn.js";
|
|
8
|
+
import { analysisAvailable, analysisMethod } from "./analysis-launch.js";
|
|
9
|
+
import { analysisToken } from "./analysis-token.js";
|
|
10
|
+
import { located, readRecord, writeRecord, replied, reportOf, interrupted, } from "./operation-journal.js";
|
|
11
|
+
function own(intent) {
|
|
12
|
+
return (intent.kind === "protected-effect" &&
|
|
13
|
+
intent.capability === "claude" &&
|
|
14
|
+
intent.method === analysisMethod &&
|
|
15
|
+
intent.target !== undefined &&
|
|
16
|
+
intent.payload !== undefined &&
|
|
17
|
+
intent.leaseGeneration !== undefined);
|
|
18
|
+
}
|
|
19
|
+
function failed(reason) {
|
|
20
|
+
return { kind: "failed", reason };
|
|
21
|
+
}
|
|
22
|
+
function snapshotOf(text) {
|
|
23
|
+
try {
|
|
24
|
+
return record(JSON.parse(text ?? "null"))?.["snapshot"];
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function cleaned(folder) {
|
|
31
|
+
await Promise.all(["configuration", "working"].map((name) => rm(join(folder, name), { recursive: true, force: true })));
|
|
32
|
+
}
|
|
33
|
+
async function privateRecord(folder, entry) {
|
|
34
|
+
await writeRecord(folder, { ...entry, permissions: "private" });
|
|
35
|
+
}
|
|
36
|
+
function outcome(end, prices) {
|
|
37
|
+
const endedAt = new Date().toISOString();
|
|
38
|
+
if (end.stopped)
|
|
39
|
+
return { outcome: "stopped", endedAt };
|
|
40
|
+
const result = end.result;
|
|
41
|
+
if (result === undefined)
|
|
42
|
+
return { ...interrupted("analysis-turn-failed"), endedAt };
|
|
43
|
+
const fields = {
|
|
44
|
+
costUsd: result.costUsd,
|
|
45
|
+
usage: result.usage,
|
|
46
|
+
model: result.model,
|
|
47
|
+
sessionId: result.sessionId,
|
|
48
|
+
turns: result.turns,
|
|
49
|
+
estimatedCostUsd: estimatedCostUsd(result.usage, {
|
|
50
|
+
worker: "claude",
|
|
51
|
+
price: priceForModel(prices, result.model),
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
if (end.exitCode !== 0 || result.failed)
|
|
55
|
+
return {
|
|
56
|
+
outcome: "failed",
|
|
57
|
+
reason: "analysis-turn-failed",
|
|
58
|
+
endedAt,
|
|
59
|
+
result: JSON.stringify(fields),
|
|
60
|
+
};
|
|
61
|
+
if (Buffer.byteLength(result.text, "utf8") > 16_000)
|
|
62
|
+
return {
|
|
63
|
+
outcome: "failed",
|
|
64
|
+
reason: "analysis-reply-too-large",
|
|
65
|
+
endedAt,
|
|
66
|
+
result: JSON.stringify(fields),
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
outcome: "completed",
|
|
70
|
+
endedAt,
|
|
71
|
+
result: JSON.stringify({ ...fields, reply: result.text }),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function running(processId) {
|
|
75
|
+
if (typeof processId !== "number")
|
|
76
|
+
return false;
|
|
77
|
+
try {
|
|
78
|
+
process.kill(processId, 0);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function reconcile(parts, intent) {
|
|
86
|
+
if (!own(intent))
|
|
87
|
+
return failed("unsupported-effect");
|
|
88
|
+
const { key, folder } = located(parts.settings.journal, intent.operationId);
|
|
89
|
+
const result = await readRecord(folder, "result.json");
|
|
90
|
+
if (result !== undefined) {
|
|
91
|
+
await cleaned(folder);
|
|
92
|
+
return replied({ receipt: key, worker: "claude" }, result);
|
|
93
|
+
}
|
|
94
|
+
if (parts.active.has(key))
|
|
95
|
+
return { kind: "accepted", receipt: key, worker: "claude" };
|
|
96
|
+
const started = await readRecord(folder, "started.json");
|
|
97
|
+
if (started === undefined)
|
|
98
|
+
return { kind: "absent" };
|
|
99
|
+
if (running(record(started)?.["processId"]))
|
|
100
|
+
return { kind: "unknown", evidence: key };
|
|
101
|
+
const ending = interrupted("analysis-turn-failed");
|
|
102
|
+
await privateRecord(folder, { name: "result.json", value: ending });
|
|
103
|
+
await cleaned(folder);
|
|
104
|
+
return replied({ receipt: key, worker: "claude" }, ending);
|
|
105
|
+
}
|
|
106
|
+
async function begun(parts, opening) {
|
|
107
|
+
const { intent, folder, key, directory, prompt } = opening;
|
|
108
|
+
const started = {
|
|
109
|
+
operationId: intent.operationId,
|
|
110
|
+
target: intent.target,
|
|
111
|
+
mode: "analysis",
|
|
112
|
+
startedAt: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
const turn = await startRetriedTurn({
|
|
115
|
+
mode: "analysis",
|
|
116
|
+
analysisToken: opening.token,
|
|
117
|
+
command: [],
|
|
118
|
+
folder,
|
|
119
|
+
directory,
|
|
120
|
+
prompt,
|
|
121
|
+
}, { ...parts.settings, metadata: started });
|
|
122
|
+
const done = turn.ended
|
|
123
|
+
.then(async (end) => {
|
|
124
|
+
const result = outcome(end, parts.settings.tokenPrices ?? {});
|
|
125
|
+
await privateRecord(folder, { name: "result.json", value: result });
|
|
126
|
+
await cleaned(folder);
|
|
127
|
+
await parts.settings.report(reportOf({ intent, worker: "claude", commandId: `analysis-turn:${key}` }, result));
|
|
128
|
+
})
|
|
129
|
+
.catch(() => undefined)
|
|
130
|
+
.finally(() => parts.active.delete(key));
|
|
131
|
+
parts.active.set(key, { turn, done });
|
|
132
|
+
}
|
|
133
|
+
function cancelled(parts, signal) {
|
|
134
|
+
return signal.aborted || parts.stopping.signal.aborted;
|
|
135
|
+
}
|
|
136
|
+
async function deliver(parts, call) {
|
|
137
|
+
const { intent, signal } = call;
|
|
138
|
+
if (!own(intent))
|
|
139
|
+
return failed("unsupported-effect");
|
|
140
|
+
const known = await reconcile(parts, intent);
|
|
141
|
+
if (known.kind !== "absent")
|
|
142
|
+
return known;
|
|
143
|
+
const { key, folder } = located(parts.settings.journal, intent.operationId);
|
|
144
|
+
const text = await parts.settings.document(intent.payload);
|
|
145
|
+
const snapshot = snapshotOf(text);
|
|
146
|
+
if (!isAnalysisSnapshot(snapshot) || snapshot.importId !== intent.target)
|
|
147
|
+
return failed("analysis-input-refused");
|
|
148
|
+
await mkdir(join(folder, "configuration"), { recursive: true, mode: 0o700 });
|
|
149
|
+
if (!(await analysisAvailable(folder))) {
|
|
150
|
+
await cleaned(folder);
|
|
151
|
+
return failed("analysis-worker-unavailable");
|
|
152
|
+
}
|
|
153
|
+
if (cancelled(parts, signal)) {
|
|
154
|
+
await cleaned(folder);
|
|
155
|
+
return failed("analysis-turn-failed");
|
|
156
|
+
}
|
|
157
|
+
const credential = await analysisToken(folder, signal);
|
|
158
|
+
if (cancelled(parts, signal)) {
|
|
159
|
+
await cleaned(folder);
|
|
160
|
+
return failed("analysis-turn-failed");
|
|
161
|
+
}
|
|
162
|
+
if ("error" in credential) {
|
|
163
|
+
await cleaned(folder);
|
|
164
|
+
return {
|
|
165
|
+
...failed("analysis-worker-unavailable"),
|
|
166
|
+
result: JSON.stringify({ error: credential.error }),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const directory = join(folder, "working");
|
|
170
|
+
await mkdir(directory, { mode: 0o700 });
|
|
171
|
+
await begun(parts, {
|
|
172
|
+
intent,
|
|
173
|
+
key,
|
|
174
|
+
folder,
|
|
175
|
+
directory,
|
|
176
|
+
prompt: analysisPrompt(snapshot),
|
|
177
|
+
token: credential.token,
|
|
178
|
+
});
|
|
179
|
+
return { receipt: key, worker: "claude" };
|
|
180
|
+
}
|
|
181
|
+
async function halted(parts, active) {
|
|
182
|
+
active.turn.stop(parts.settings.stopMilliseconds ?? 2_000);
|
|
183
|
+
await active.done;
|
|
184
|
+
}
|
|
185
|
+
function delivered(parts, call) {
|
|
186
|
+
if (cancelled(parts, call.signal))
|
|
187
|
+
return Promise.resolve(failed("analysis-turn-failed"));
|
|
188
|
+
const key = call.intent.operationId;
|
|
189
|
+
const known = parts.preparing.get(key);
|
|
190
|
+
if (known !== undefined)
|
|
191
|
+
return known;
|
|
192
|
+
const signal = AbortSignal.any([call.signal, parts.stopping.signal]);
|
|
193
|
+
const pending = deliver(parts, { ...call, signal }).finally(() => parts.preparing.delete(key));
|
|
194
|
+
parts.preparing.set(key, pending);
|
|
195
|
+
return pending;
|
|
196
|
+
}
|
|
197
|
+
export function analysisWorker(settings) {
|
|
198
|
+
const parts = {
|
|
199
|
+
settings,
|
|
200
|
+
active: new Map(),
|
|
201
|
+
preparing: new Map(),
|
|
202
|
+
stopping: new AbortController(),
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
deliver: (intent, signal) => delivered(parts, { intent, signal }),
|
|
206
|
+
reconcile: (intent) => reconcile(parts, intent),
|
|
207
|
+
stop: async (intent) => {
|
|
208
|
+
const active = parts.active.get(located(settings.journal, intent.operationId).key);
|
|
209
|
+
if (active !== undefined)
|
|
210
|
+
await halted(parts, active);
|
|
211
|
+
return reconcile(parts, intent);
|
|
212
|
+
},
|
|
213
|
+
close: async () => {
|
|
214
|
+
parts.stopping.abort();
|
|
215
|
+
await Promise.all(parts.preparing.values());
|
|
216
|
+
await Promise.all([...parts.active.values()].map((active) => halted(parts, active)));
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AnalysisSnapshot } from "../chats/analysis-input.ts";
|
|
2
|
+
export declare function analysisDirectory(runtime: string, importId: string): Promise<string>;
|
|
3
|
+
export declare function boundedAnalysisFile(path: string, options?: {
|
|
4
|
+
readonly permissions?: number;
|
|
5
|
+
}): Promise<string>;
|
|
6
|
+
export declare function readAnalysisSnapshot(directory: string, importId: string): Promise<AnalysisSnapshot>;
|
|
7
|
+
export declare function matchingPrivateFile(path: string, text: string): Promise<void>;
|
|
8
|
+
interface AnalysisOutput {
|
|
9
|
+
readonly cycleId: string;
|
|
10
|
+
readonly analysis: string;
|
|
11
|
+
readonly proposal: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function inspectAnalysisFiles(directory: string, output: AnalysisOutput): Promise<"complete" | "absent">;
|
|
14
|
+
export declare function saveAnalysisFiles(directory: string, output: AnalysisOutput): Promise<void>;
|
|
15
|
+
export {};
|