cloving 1.0.1 → 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 +251 -20
- 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 +33 -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 +3 -0
- package/dist/src/authority/reconciliation.js +21 -5
- package/dist/src/authority/transitions.d.ts +1 -1
- package/dist/src/authority/transitions.js +5 -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/authority-session.d.ts +2 -0
- package/dist/src/broker/actor/authority-session.js +3 -4
- 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 +42 -0
- package/dist/src/chats/analysis-batches.js +97 -0
- package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
- package/dist/src/chats/analysis-checkpoint.js +31 -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 +18 -0
- package/dist/src/chats/analysis-input.js +97 -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-merge.d.ts +16 -0
- package/dist/src/chats/analysis-merge.js +134 -0
- package/dist/src/chats/analysis-prompt.d.ts +18 -0
- package/dist/src/chats/analysis-prompt.js +42 -0
- package/dist/src/chats/analysis-proposal.d.ts +4 -0
- package/dist/src/chats/analysis-proposal.js +41 -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 +4 -0
- package/dist/src/chats/analysis-shape.js +235 -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/reference-mapping.d.ts +16 -0
- package/dist/src/chats/reference-mapping.js +153 -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/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/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 +3 -0
- package/dist/src/cli/render.js +122 -7
- 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 +42 -7
- 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 +44 -1
- package/dist/src/cli/watch-board-view.js +137 -18
- package/dist/src/cli/watch-board.d.ts +7 -1
- package/dist/src/cli/watch-board.js +48 -11
- 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 +122 -0
- package/dist/src/cli/watch-model.d.ts +10 -4
- package/dist/src/cli/watch-model.js +61 -67
- package/dist/src/cli/watch-page.d.ts +1 -1
- package/dist/src/cli/watch-page.js +89 -86
- package/dist/src/cli/watch-phases.js +3 -3
- 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 +18 -0
- package/dist/src/cli/watch-pr-command.js +74 -0
- package/dist/src/cli/watch-pr-identity.d.ts +32 -0
- package/dist/src/cli/watch-pr-identity.js +87 -0
- package/dist/src/cli/watch-pr-observer.d.ts +20 -0
- package/dist/src/cli/watch-pr-observer.js +150 -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-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 +44 -18
- package/dist/src/cli/watch-render.d.ts +29 -2
- package/dist/src/cli/watch-render.js +11 -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 +77 -8
- 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 +8 -3
- 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 +22 -0
- package/dist/src/cli/watch-view.js +111 -19
- 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 +20 -0
- package/dist/src/daemon/analysis-worker.js +271 -0
- 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 +42 -0
- package/dist/src/daemon/chats-files.js +252 -0
- 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 +13 -0
- package/dist/src/daemon/chats-worker.js +201 -0
- package/dist/src/daemon/claude-turn.d.ts +11 -0
- package/dist/src/daemon/claude-turn.js +96 -15
- package/dist/src/daemon/claude-worker.d.ts +2 -1
- package/dist/src/daemon/claude-worker.js +92 -45
- package/dist/src/daemon/codex-stream.js +16 -0
- package/dist/src/daemon/daemon.d.ts +8 -1
- package/dist/src/daemon/daemon.js +51 -14
- 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 +84 -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.d.ts +3 -0
- package/dist/src/daemon/github-ticket-status.js +294 -0
- 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 +2 -0
- package/dist/src/daemon/issue-queue.js +186 -43
- 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/operation-journal.d.ts +1 -0
- package/dist/src/daemon/operation-journal.js +4 -2
- package/dist/src/daemon/retrying-turn.d.ts +12 -0
- package/dist/src/daemon/retrying-turn.js +231 -0
- 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 +3 -0
- package/dist/src/host/world-reconciliation.js +9 -0
- package/dist/src/host/world-runs.js +14 -1
- 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 +54 -30
- 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.d.ts +3 -0
- package/dist/src/service/worker-totals.js +52 -0
- 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.d.ts +8 -0
- package/dist/src/ticket-status.js +48 -0
- package/dist/src/token-prices.d.ts +21 -0
- package/dist/src/token-prices.js +108 -0
- package/dist/src/worker-usage.d.ts +31 -0
- package/dist/src/worker-usage.js +44 -0
- package/dist/src/workflow-admission.d.ts +1 -0
- package/dist/src/workflow-candidate.d.ts +29 -0
- package/dist/src/workflow-candidate.js +60 -0
- package/dist/src/workflow-contracts.d.ts +4 -0
- package/dist/src/workflow-contracts.js +32 -5
- package/dist/src/workflow-declaration.d.ts +12 -1
- package/dist/src/workflow-declaration.js +71 -13
- 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 +3 -1
- package/dist/src/workflows.js +2 -0
- package/dist/tools/admit-workflow.d.ts +13 -0
- package/dist/tools/admit-workflow.js +94 -0
- package/dist/tools/self-host-processes.js +7 -2
- package/dist/tools/self-host-project.d.ts +17 -0
- package/dist/tools/self-host-project.js +177 -0
- 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 +30 -84
- 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/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 +15 -0
- package/dist/workflows/analyze-chats.declaration.json +22 -0
- package/dist/workflows/analyze-chats.js +250 -0
- package/dist/workflows/develop-prd.js +71 -3
- package/dist/workflows/develop-ticket.d.ts +1 -7
- package/dist/workflows/develop-ticket.js +47 -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 +143 -0
- package/package.json +25 -5
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,20 @@
|
|
|
1
|
+
import type { AdapterOutcome, DeliveryIntent } from "../broker/actor/delivery-intents.ts";
|
|
2
|
+
import { type RetrySettings } from "./retrying-turn.ts";
|
|
3
|
+
import { type WorkerReport } from "./operation-journal.ts";
|
|
4
|
+
interface Settings extends RetrySettings {
|
|
5
|
+
readonly journal: string;
|
|
6
|
+
readonly document: (digest: string) => Promise<string | undefined>;
|
|
7
|
+
readonly report: (report: WorkerReport) => Promise<unknown>;
|
|
8
|
+
readonly stopMilliseconds?: number;
|
|
9
|
+
}
|
|
10
|
+
interface AnalysisWorker {
|
|
11
|
+
readonly deliver: (intent: DeliveryIntent, signal: AbortSignal) => Promise<AdapterOutcome | {
|
|
12
|
+
readonly receipt: string;
|
|
13
|
+
readonly worker: string;
|
|
14
|
+
}>;
|
|
15
|
+
readonly reconcile: (intent: DeliveryIntent, signal: AbortSignal) => Promise<AdapterOutcome>;
|
|
16
|
+
readonly stop: (intent: DeliveryIntent, signal: AbortSignal) => Promise<AdapterOutcome>;
|
|
17
|
+
readonly close: () => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare function analysisWorker(settings: Settings): AnalysisWorker;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { analysisFollowUp, analysisFollowUpPrompt, analysisLaunchBytes, } from "../chats/analysis-prompt.js";
|
|
4
|
+
import { prepareAnalysisBatch } from "../chats/analysis-batches.js";
|
|
5
|
+
import { isAnalysisSnapshot, analysisDocumentBytes, } from "../chats/analysis-input.js";
|
|
6
|
+
import { record } from "../protocol.js";
|
|
7
|
+
import { sumPrices } from "../worker-usage.js";
|
|
8
|
+
import { startRetriedTurn } from "./retrying-turn.js";
|
|
9
|
+
import { analysisAvailable, analysisMethod } from "./analysis-launch.js";
|
|
10
|
+
import { analysisToken } from "./analysis-token.js";
|
|
11
|
+
import { located, readRecord, writeRecord, replied, reportOf, interrupted, } from "./operation-journal.js";
|
|
12
|
+
function own(intent) {
|
|
13
|
+
return (intent.kind === "protected-effect" &&
|
|
14
|
+
intent.capability === "claude" &&
|
|
15
|
+
intent.method === analysisMethod &&
|
|
16
|
+
intent.target !== undefined &&
|
|
17
|
+
intent.payload !== undefined &&
|
|
18
|
+
intent.leaseGeneration !== undefined);
|
|
19
|
+
}
|
|
20
|
+
function failed(reason) {
|
|
21
|
+
return { kind: "failed", reason };
|
|
22
|
+
}
|
|
23
|
+
function firstBatch(snapshot) {
|
|
24
|
+
const batch = prepareAnalysisBatch(snapshot, {
|
|
25
|
+
nextRecordIndex: 0,
|
|
26
|
+
completedBatchCount: 0,
|
|
27
|
+
previousResult: null,
|
|
28
|
+
});
|
|
29
|
+
if (batch.kind === "refused")
|
|
30
|
+
throw new Error(batch.code);
|
|
31
|
+
if (batch.nextRecordIndex !== batch.totalRecords)
|
|
32
|
+
throw new Error("analysis-too-large");
|
|
33
|
+
return batch;
|
|
34
|
+
}
|
|
35
|
+
function promptOf(text, target) {
|
|
36
|
+
if (text !== undefined && Buffer.byteLength(text) > analysisDocumentBytes)
|
|
37
|
+
throw new Error("analysis-too-large");
|
|
38
|
+
const payload = record(JSON.parse(text ?? "null"));
|
|
39
|
+
if (payload === undefined)
|
|
40
|
+
throw new Error("analysis-input-refused");
|
|
41
|
+
const snapshot = payload["snapshot"];
|
|
42
|
+
if (!isAnalysisSnapshot(snapshot) || snapshot.importId !== target)
|
|
43
|
+
throw new Error("analysis-input-refused");
|
|
44
|
+
const followUp = "followUp" in payload ? analysisFollowUp(payload["followUp"]) : undefined;
|
|
45
|
+
const data = batchInput(snapshot, payload);
|
|
46
|
+
const prompt = turnPrompt(data, followUp);
|
|
47
|
+
if (analysisLaunchBytes(prompt) > analysisDocumentBytes)
|
|
48
|
+
throw new Error("analysis-too-large");
|
|
49
|
+
return prompt;
|
|
50
|
+
}
|
|
51
|
+
function turnPrompt(data, followUp) {
|
|
52
|
+
const context = data.context === undefined ? {} : { context: data.context };
|
|
53
|
+
if (followUp === undefined)
|
|
54
|
+
return data.prompt;
|
|
55
|
+
return analysisFollowUpPrompt(data.snapshot, { ...followUp, ...context });
|
|
56
|
+
}
|
|
57
|
+
function batchInput(snapshot, payload) {
|
|
58
|
+
if (!("context" in payload)) {
|
|
59
|
+
const batch = firstBatch(snapshot);
|
|
60
|
+
return { snapshot: batch.snapshot, prompt: batch.prompt };
|
|
61
|
+
}
|
|
62
|
+
const context = record(payload["context"]);
|
|
63
|
+
if (context === undefined || Object.keys(context).length !== 3)
|
|
64
|
+
throw new Error("analysis-input-refused");
|
|
65
|
+
const position = [context["batchIndex"], context["firstRecordIndex"]];
|
|
66
|
+
if (!position.every((value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0) ||
|
|
67
|
+
Number(position[0]) > 3)
|
|
68
|
+
throw new Error("analysis-input-refused");
|
|
69
|
+
if (context["previousResult"] !== null &&
|
|
70
|
+
record(context["previousResult"]) === undefined)
|
|
71
|
+
throw new Error("analysis-input-refused");
|
|
72
|
+
return {
|
|
73
|
+
snapshot,
|
|
74
|
+
context,
|
|
75
|
+
prompt: JSON.stringify({ evidence: snapshot, context }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function cleaned(folder) {
|
|
79
|
+
await Promise.all(["configuration", "working"].map((name) => rm(join(folder, name), { recursive: true, force: true })));
|
|
80
|
+
}
|
|
81
|
+
async function privateRecord(folder, entry) {
|
|
82
|
+
await writeRecord(folder, { ...entry, permissions: "private" });
|
|
83
|
+
}
|
|
84
|
+
function outcome(end) {
|
|
85
|
+
const endedAt = new Date().toISOString();
|
|
86
|
+
if (end.stopped)
|
|
87
|
+
return { outcome: "stopped", endedAt };
|
|
88
|
+
const result = end.result;
|
|
89
|
+
if (result === undefined)
|
|
90
|
+
return { ...interrupted("analysis-turn-failed"), endedAt };
|
|
91
|
+
const fields = {
|
|
92
|
+
costUsd: result.costUsd,
|
|
93
|
+
usage: result.usage,
|
|
94
|
+
model: result.model,
|
|
95
|
+
...(result.attempts === undefined ? {} : { attempts: result.attempts }),
|
|
96
|
+
sessionId: result.sessionId,
|
|
97
|
+
turns: result.turns,
|
|
98
|
+
estimatedCostUsd: sumPrices((result.attempts ?? []).map((attempt) => attempt.estimatedCostUsd)),
|
|
99
|
+
};
|
|
100
|
+
if (end.exitCode !== 0 || result.failed)
|
|
101
|
+
return {
|
|
102
|
+
outcome: "failed",
|
|
103
|
+
reason: "analysis-turn-failed",
|
|
104
|
+
endedAt,
|
|
105
|
+
result: JSON.stringify(fields),
|
|
106
|
+
};
|
|
107
|
+
if (Buffer.byteLength(result.text, "utf8") > 16_000)
|
|
108
|
+
return {
|
|
109
|
+
outcome: "failed",
|
|
110
|
+
reason: "analysis-reply-too-large",
|
|
111
|
+
endedAt,
|
|
112
|
+
result: JSON.stringify(fields),
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
outcome: "completed",
|
|
116
|
+
endedAt,
|
|
117
|
+
result: JSON.stringify({ ...fields, reply: result.text }),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function running(processId) {
|
|
121
|
+
if (typeof processId !== "number")
|
|
122
|
+
return false;
|
|
123
|
+
try {
|
|
124
|
+
process.kill(processId, 0);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function reconcile(parts, intent) {
|
|
132
|
+
if (!own(intent))
|
|
133
|
+
return failed("unsupported-effect");
|
|
134
|
+
const { key, folder } = located(parts.settings.journal, intent.operationId);
|
|
135
|
+
const result = await readRecord(folder, "result.json");
|
|
136
|
+
if (result !== undefined) {
|
|
137
|
+
await cleaned(folder);
|
|
138
|
+
return replied({ receipt: key, worker: "claude" }, result);
|
|
139
|
+
}
|
|
140
|
+
if (parts.active.has(key))
|
|
141
|
+
return { kind: "accepted", receipt: key, worker: "claude" };
|
|
142
|
+
const started = await readRecord(folder, "started.json");
|
|
143
|
+
if (started === undefined)
|
|
144
|
+
return { kind: "absent" };
|
|
145
|
+
if (running(record(started)?.["processId"]))
|
|
146
|
+
return { kind: "unknown", evidence: key };
|
|
147
|
+
const ending = interrupted("analysis-turn-failed");
|
|
148
|
+
await privateRecord(folder, { name: "result.json", value: ending });
|
|
149
|
+
await cleaned(folder);
|
|
150
|
+
return replied({ receipt: key, worker: "claude" }, ending);
|
|
151
|
+
}
|
|
152
|
+
async function begun(parts, opening) {
|
|
153
|
+
const { intent, folder, key, directory, prompt } = opening;
|
|
154
|
+
const started = {
|
|
155
|
+
operationId: intent.operationId,
|
|
156
|
+
target: intent.target,
|
|
157
|
+
mode: "analysis",
|
|
158
|
+
startedAt: new Date().toISOString(),
|
|
159
|
+
};
|
|
160
|
+
const turn = await startRetriedTurn({
|
|
161
|
+
mode: "analysis",
|
|
162
|
+
analysisToken: opening.token,
|
|
163
|
+
command: [],
|
|
164
|
+
folder,
|
|
165
|
+
directory,
|
|
166
|
+
prompt,
|
|
167
|
+
}, { ...parts.settings, metadata: started });
|
|
168
|
+
const done = turn.ended
|
|
169
|
+
.then(async (end) => {
|
|
170
|
+
const result = outcome(end);
|
|
171
|
+
await privateRecord(folder, { name: "result.json", value: result });
|
|
172
|
+
await cleaned(folder);
|
|
173
|
+
await parts.settings.report(reportOf({ intent, worker: "claude", commandId: `analysis-turn:${key}` }, result));
|
|
174
|
+
})
|
|
175
|
+
.catch(() => undefined)
|
|
176
|
+
.finally(() => parts.active.delete(key));
|
|
177
|
+
parts.active.set(key, { turn, done });
|
|
178
|
+
}
|
|
179
|
+
function cancelled(parts, signal) {
|
|
180
|
+
return signal.aborted || parts.stopping.signal.aborted;
|
|
181
|
+
}
|
|
182
|
+
async function deliver(parts, call) {
|
|
183
|
+
const { intent, signal } = call;
|
|
184
|
+
if (!own(intent))
|
|
185
|
+
return failed("unsupported-effect");
|
|
186
|
+
const known = await reconcile(parts, intent);
|
|
187
|
+
if (known.kind !== "absent")
|
|
188
|
+
return known;
|
|
189
|
+
const { key, folder } = located(parts.settings.journal, intent.operationId);
|
|
190
|
+
const text = await parts.settings.document(intent.payload);
|
|
191
|
+
let prompt;
|
|
192
|
+
try {
|
|
193
|
+
prompt = promptOf(text, intent.target);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return failed(error instanceof Error && error.message === "analysis-too-large"
|
|
197
|
+
? "analysis-too-large"
|
|
198
|
+
: "analysis-input-refused");
|
|
199
|
+
}
|
|
200
|
+
await mkdir(join(folder, "configuration"), { recursive: true, mode: 0o700 });
|
|
201
|
+
if (!(await analysisAvailable(folder))) {
|
|
202
|
+
await cleaned(folder);
|
|
203
|
+
return failed("analysis-worker-unavailable");
|
|
204
|
+
}
|
|
205
|
+
if (cancelled(parts, signal)) {
|
|
206
|
+
await cleaned(folder);
|
|
207
|
+
return failed("analysis-turn-failed");
|
|
208
|
+
}
|
|
209
|
+
const credential = await analysisToken(folder, signal);
|
|
210
|
+
if (cancelled(parts, signal)) {
|
|
211
|
+
await cleaned(folder);
|
|
212
|
+
return failed("analysis-turn-failed");
|
|
213
|
+
}
|
|
214
|
+
if ("error" in credential) {
|
|
215
|
+
await cleaned(folder);
|
|
216
|
+
return {
|
|
217
|
+
...failed("analysis-worker-unavailable"),
|
|
218
|
+
result: JSON.stringify({ error: credential.error }),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const directory = join(folder, "working");
|
|
222
|
+
await mkdir(directory, { mode: 0o700 });
|
|
223
|
+
await begun(parts, {
|
|
224
|
+
intent,
|
|
225
|
+
key,
|
|
226
|
+
folder,
|
|
227
|
+
directory,
|
|
228
|
+
prompt,
|
|
229
|
+
token: credential.token,
|
|
230
|
+
});
|
|
231
|
+
return { receipt: key, worker: "claude" };
|
|
232
|
+
}
|
|
233
|
+
async function halted(parts, active) {
|
|
234
|
+
active.turn.stop(parts.settings.stopMilliseconds ?? 2_000);
|
|
235
|
+
await active.done;
|
|
236
|
+
}
|
|
237
|
+
function delivered(parts, call) {
|
|
238
|
+
if (cancelled(parts, call.signal))
|
|
239
|
+
return Promise.resolve(failed("analysis-turn-failed"));
|
|
240
|
+
const key = call.intent.operationId;
|
|
241
|
+
const known = parts.preparing.get(key);
|
|
242
|
+
if (known !== undefined)
|
|
243
|
+
return known;
|
|
244
|
+
const signal = AbortSignal.any([call.signal, parts.stopping.signal]);
|
|
245
|
+
const pending = deliver(parts, { ...call, signal }).finally(() => parts.preparing.delete(key));
|
|
246
|
+
parts.preparing.set(key, pending);
|
|
247
|
+
return pending;
|
|
248
|
+
}
|
|
249
|
+
export function analysisWorker(settings) {
|
|
250
|
+
const parts = {
|
|
251
|
+
settings,
|
|
252
|
+
active: new Map(),
|
|
253
|
+
preparing: new Map(),
|
|
254
|
+
stopping: new AbortController(),
|
|
255
|
+
};
|
|
256
|
+
return {
|
|
257
|
+
deliver: (intent, signal) => delivered(parts, { intent, signal }),
|
|
258
|
+
reconcile: (intent) => reconcile(parts, intent),
|
|
259
|
+
stop: async (intent) => {
|
|
260
|
+
const active = parts.active.get(located(settings.journal, intent.operationId).key);
|
|
261
|
+
if (active !== undefined)
|
|
262
|
+
await halted(parts, active);
|
|
263
|
+
return reconcile(parts, intent);
|
|
264
|
+
},
|
|
265
|
+
close: async () => {
|
|
266
|
+
parts.stopping.abort();
|
|
267
|
+
await Promise.all(parts.preparing.values());
|
|
268
|
+
await Promise.all([...parts.active.values()].map((active) => halted(parts, active)));
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type AnalysisSnapshot } from "../chats/analysis-input.ts";
|
|
2
|
+
import { type AnalysisCapture } from "./chats-capture.ts";
|
|
3
|
+
interface AnalysisBatchBindingInput {
|
|
4
|
+
readonly runId: string;
|
|
5
|
+
readonly importId: string;
|
|
6
|
+
readonly snapshot: AnalysisSnapshot | undefined;
|
|
7
|
+
readonly batch?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export declare function boundAnalysisBatch(directory: string, input: AnalysisBatchBindingInput): Promise<{
|
|
10
|
+
capture: AnalysisCapture;
|
|
11
|
+
snapshot: AnalysisSnapshot;
|
|
12
|
+
batch: {
|
|
13
|
+
batchIndex: number;
|
|
14
|
+
nextRecordIndex: number;
|
|
15
|
+
};
|
|
16
|
+
previous: import("../chats/analysis-checkpoint.ts").AnalysisCheckpoint;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function previousCheckpoint(folder: string, input: {
|
|
19
|
+
readonly capture: AnalysisCapture;
|
|
20
|
+
readonly batchIndex: number;
|
|
21
|
+
readonly snapshot: AnalysisSnapshot;
|
|
22
|
+
}): Promise<import("../chats/analysis-checkpoint.ts").AnalysisCheckpoint>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { record } from "../protocol.js";
|
|
3
|
+
import { initialAnalysisCheckpoint, batchAnalysisCheckpoint, } from "../chats/analysis-checkpoint.js";
|
|
4
|
+
import { analysisEvidence, } from "../chats/analysis-input.js";
|
|
5
|
+
import { mergeAnalysisBatch } from "../chats/analysis-merge.js";
|
|
6
|
+
import { prepareAnalysisBatch } from "../chats/analysis-batches.js";
|
|
7
|
+
import { checkpointProposal } from "../chats/analysis-proposal.js";
|
|
8
|
+
import { findAnalysisCapture, readCapturedAnalysisSnapshot, } from "./chats-capture.js";
|
|
9
|
+
import { boundedAnalysisFile, inspectAnalysisFiles } from "./chats-files.js";
|
|
10
|
+
export async function boundAnalysisBatch(directory, input) {
|
|
11
|
+
const capture = await findAnalysisCapture(directory, {
|
|
12
|
+
runId: input.runId,
|
|
13
|
+
importId: input.importId,
|
|
14
|
+
});
|
|
15
|
+
if (capture === undefined)
|
|
16
|
+
throw new Error("analysis-output-failed");
|
|
17
|
+
const snapshot = await readCapturedAnalysisSnapshot(directory, capture);
|
|
18
|
+
const binding = record(input.batch);
|
|
19
|
+
const batchIndex = binding?.["batchIndex"] ?? 0;
|
|
20
|
+
if (typeof batchIndex !== "number")
|
|
21
|
+
throw new Error("analysis-output-failed");
|
|
22
|
+
const previous = await previousCheckpoint(join(directory, `capture-${capture.key}`), {
|
|
23
|
+
capture,
|
|
24
|
+
batchIndex,
|
|
25
|
+
snapshot,
|
|
26
|
+
});
|
|
27
|
+
const prepared = prepareAnalysisBatch(snapshot, {
|
|
28
|
+
nextRecordIndex: previous.nextRecordIndex,
|
|
29
|
+
completedBatchCount: previous.completedBatchCount,
|
|
30
|
+
previousResult: previous.result,
|
|
31
|
+
});
|
|
32
|
+
if (prepared.kind === "refused")
|
|
33
|
+
throw new Error("analysis-output-failed");
|
|
34
|
+
if (input.snapshot !== undefined &&
|
|
35
|
+
JSON.stringify(input.batch === undefined ? snapshot : prepared.snapshot) !==
|
|
36
|
+
JSON.stringify(input.snapshot))
|
|
37
|
+
throw new Error("analysis-output-failed");
|
|
38
|
+
if (!matchingBatchBinding(input.batch, {
|
|
39
|
+
capturedDigest: capture.digest,
|
|
40
|
+
batchIndex,
|
|
41
|
+
nextRecordIndex: prepared.nextRecordIndex,
|
|
42
|
+
totalRecords: capture.totalRecords,
|
|
43
|
+
}))
|
|
44
|
+
throw new Error("analysis-output-failed");
|
|
45
|
+
return {
|
|
46
|
+
capture,
|
|
47
|
+
snapshot,
|
|
48
|
+
batch: { batchIndex, nextRecordIndex: prepared.nextRecordIndex },
|
|
49
|
+
previous,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function matchingBatchBinding(binding, expected) {
|
|
53
|
+
if (binding === undefined)
|
|
54
|
+
return expected.nextRecordIndex === expected.totalRecords;
|
|
55
|
+
const fields = record(binding);
|
|
56
|
+
return ["capturedDigest", "batchIndex", "nextRecordIndex"].every((name) => fields?.[name] === Reflect.get(expected, name));
|
|
57
|
+
}
|
|
58
|
+
export async function previousCheckpoint(folder, input) {
|
|
59
|
+
const { capture, batchIndex } = input;
|
|
60
|
+
if (!Number.isSafeInteger(batchIndex) || batchIndex < 0 || batchIndex > 3)
|
|
61
|
+
throw new Error("analysis-input-refused");
|
|
62
|
+
const initial = initialAnalysisCheckpoint(capture);
|
|
63
|
+
if (batchIndex === 0)
|
|
64
|
+
return initial;
|
|
65
|
+
const name = `analysis-checkpoint-${String(batchIndex)}.json`;
|
|
66
|
+
const text = await boundedAnalysisFile(join(folder, name), {
|
|
67
|
+
permissions: 0o600,
|
|
68
|
+
});
|
|
69
|
+
const checkpoint = restoreCheckpoint(text, input);
|
|
70
|
+
await inspectAnalysisFiles(folder, {
|
|
71
|
+
cycleId: `checkpoint-${String(batchIndex)}`,
|
|
72
|
+
analysis: JSON.stringify(checkpoint) + "\n",
|
|
73
|
+
proposal: checkpointProposal(checkpoint),
|
|
74
|
+
});
|
|
75
|
+
return checkpoint;
|
|
76
|
+
}
|
|
77
|
+
function restoreCheckpoint(text, input) {
|
|
78
|
+
const value = record(JSON.parse(text));
|
|
79
|
+
const result = record(value?.["result"]);
|
|
80
|
+
const nextRecordIndex = value?.["nextRecordIndex"];
|
|
81
|
+
if (result === undefined || typeof nextRecordIndex !== "number")
|
|
82
|
+
throw new Error("analysis-output-failed");
|
|
83
|
+
const merged = mergeAnalysisBatch({
|
|
84
|
+
previousResult: null,
|
|
85
|
+
candidate: Object.fromEntries(Object.entries(result).filter(([key]) => key !== "limitations")),
|
|
86
|
+
evidence: analysisEvidence(input.snapshot),
|
|
87
|
+
nextRecordIndex,
|
|
88
|
+
});
|
|
89
|
+
if (!merged.accepted)
|
|
90
|
+
throw new Error("analysis-output-failed");
|
|
91
|
+
const checkpoint = batchAnalysisCheckpoint(input.capture, {
|
|
92
|
+
batchIndex: input.batchIndex - 1,
|
|
93
|
+
nextRecordIndex,
|
|
94
|
+
result: merged.result,
|
|
95
|
+
});
|
|
96
|
+
if (text !== JSON.stringify(checkpoint) + "\n")
|
|
97
|
+
throw new Error("analysis-output-failed");
|
|
98
|
+
return checkpoint;
|
|
99
|
+
}
|