cloving 1.4.0 → 1.5.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 +100 -15
- package/dist/src/broker/actor/authority-health.d.ts +9 -0
- package/dist/src/broker/actor/authority-health.js +24 -0
- package/dist/src/broker/actor/authority-runtime.d.ts +4 -2
- package/dist/src/broker/actor/authority-runtime.js +29 -20
- package/dist/src/broker/actor/authority-session.d.ts +6 -19
- package/dist/src/broker/actor/authority-session.js +66 -88
- package/dist/src/broker/actor/authority-settings.d.ts +2 -1
- package/dist/src/broker/actor/authority-settings.js +4 -1
- package/dist/src/broker/actor/authority-upgrades.d.ts +5 -1
- package/dist/src/broker/actor/authority-upgrades.js +2 -1
- package/dist/src/broker/actor/authority-watchdog.d.ts +6 -2
- package/dist/src/broker/actor/authority-watchdog.js +31 -9
- package/dist/src/broker/actor/authority.js +3 -1
- package/dist/src/broker/actor/projection-memory.d.ts +43 -0
- package/dist/src/broker/actor/projection-memory.js +207 -0
- package/dist/src/broker/actor/runtime-parts.d.ts +5 -0
- package/dist/src/broker/actor/runtime-parts.js +8 -3
- package/dist/src/broker/authority-restore.js +1 -1
- package/dist/src/broker/effect-upgrade.d.ts +1 -0
- package/dist/src/broker/effect-upgrade.js +4 -1
- package/dist/src/cli/arguments.js +1 -0
- package/dist/src/cli/client.d.ts +3 -1
- package/dist/src/cli/client.js +4 -4
- package/dist/src/cli/command-requests.d.ts +30 -1
- package/dist/src/cli/command-requests.js +43 -2
- package/dist/src/cli/commands.js +57 -48
- package/dist/src/cli/config-notice.js +5 -2
- package/dist/src/cli/doctor-retention.d.ts +2 -0
- package/dist/src/cli/doctor-retention.js +93 -0
- package/dist/src/cli/doctor.js +5 -0
- package/dist/src/cli/init-prompt.js +1 -1
- package/dist/src/cli/mcp-server.js +4 -4
- package/dist/src/cli/mcp-tools.d.ts +14 -14
- package/dist/src/cli/mcp-tools.js +212 -50
- package/dist/src/cli/queue-arguments.d.ts +15 -0
- package/dist/src/cli/queue-arguments.js +31 -0
- package/dist/src/cli/queue-render.d.ts +3 -0
- package/dist/src/cli/queue-render.js +26 -0
- package/dist/src/cli/reload-render.d.ts +2 -0
- package/dist/src/cli/reload-render.js +12 -0
- package/dist/src/cli/render.js +37 -8
- package/dist/src/cli/report-github-reader.js +1 -1
- package/dist/src/cli/runtime-arguments.d.ts +17 -3
- package/dist/src/cli/runtime-arguments.js +23 -19
- package/dist/src/cli/toolchain.d.ts +1 -0
- package/dist/src/cli/toolchain.js +1 -0
- package/dist/src/cli/usage.d.ts +2 -2
- package/dist/src/cli/usage.js +40 -5
- package/dist/src/cli/watch-api-document.d.ts +430 -0
- package/dist/src/cli/watch-api-document.js +291 -0
- package/dist/src/cli/watch-api-run.d.ts +56 -0
- package/dist/src/cli/watch-api-run.js +112 -0
- package/dist/src/cli/watch-api.d.ts +17 -0
- package/dist/src/cli/watch-api.js +230 -0
- package/dist/src/cli/watch-header-view.d.ts +1 -0
- package/dist/src/cli/watch-header-view.js +9 -1
- package/dist/src/cli/watch-model.d.ts +1 -0
- package/dist/src/cli/watch-model.js +3 -0
- package/dist/src/cli/watch-page.js +3 -1
- package/dist/src/cli/watch-reads.d.ts +1 -0
- package/dist/src/cli/watch-reads.js +6 -5
- package/dist/src/cli/watch-render.d.ts +1 -0
- package/dist/src/cli/watch-server.js +18 -4
- package/dist/src/cli.js +50 -43
- package/dist/src/configuration-display.js +2 -0
- package/dist/src/configuration-schema.d.ts +5 -0
- package/dist/src/daemon/claude-recovery.js +13 -9
- package/dist/src/daemon/claude-worker.d.ts +3 -3
- package/dist/src/daemon/claude-worker.js +10 -11
- package/dist/src/daemon/daemon-adapters.d.ts +7 -0
- package/dist/src/daemon/daemon-adapters.js +7 -3
- package/dist/src/daemon/daemon-loops.d.ts +2 -0
- package/dist/src/daemon/daemon-loops.js +17 -11
- package/dist/src/daemon/daemon.js +71 -60
- package/dist/src/daemon/github-attach.js +77 -4
- package/dist/src/daemon/github-calls.js +1 -1
- package/dist/src/daemon/github-methods.js +6 -2
- package/dist/src/daemon/issue-queue.d.ts +3 -0
- package/dist/src/daemon/issue-queue.js +66 -73
- package/dist/src/daemon/merge-turns.d.ts +10 -0
- package/dist/src/daemon/merge-turns.js +37 -0
- package/dist/src/daemon/queue-control.d.ts +13 -0
- package/dist/src/daemon/queue-control.js +86 -0
- package/dist/src/daemon/queue-journal.d.ts +20 -0
- package/dist/src/daemon/queue-journal.js +89 -0
- package/dist/src/daemon/record-archive.d.ts +19 -0
- package/dist/src/daemon/record-archive.js +82 -0
- package/dist/src/daemon/record-sweep.d.ts +24 -0
- package/dist/src/daemon/record-sweep.js +147 -0
- package/dist/src/daemon/retrying-turn.js +4 -4
- package/dist/src/daemon/settings-reload.d.ts +26 -0
- package/dist/src/daemon/settings-reload.js +82 -0
- package/dist/src/daemon/turn-details.d.ts +4 -0
- package/dist/src/daemon/turn-details.js +33 -0
- package/dist/src/daemon/worker-profile.d.ts +1 -0
- package/dist/src/daemon/worker-profile.js +4 -0
- package/dist/src/daemon/workflow-generations.d.ts +7 -4
- package/dist/src/daemon/workflow-generations.js +22 -14
- package/dist/src/daemon/workflow-reload.d.ts +41 -0
- package/dist/src/daemon/workflow-reload.js +244 -0
- package/dist/src/daemon/workflow-replay.d.ts +2 -1
- package/dist/src/daemon/workflow-replay.js +7 -3
- package/dist/src/doctor-report.d.ts +2 -2
- package/dist/src/doctor-report.js +2 -0
- package/dist/src/private-config.schema.json +31 -0
- package/dist/src/project-settings.d.ts +15 -1
- package/dist/src/project-settings.js +33 -1
- package/dist/src/queue-state.d.ts +26 -0
- package/dist/src/queue-state.js +34 -0
- package/dist/src/record-retention.d.ts +46 -0
- package/dist/src/record-retention.js +81 -0
- package/dist/src/service/active-tickets.js +5 -0
- package/dist/src/service/dispatcher.d.ts +4 -0
- package/dist/src/service/dispatcher.js +33 -0
- package/dist/src/service/queue-handlers.d.ts +4 -0
- package/dist/src/service/queue-handlers.js +32 -0
- package/dist/src/service/registry.d.ts +28 -2
- package/dist/src/service/registry.js +48 -0
- package/dist/src/service/replies.d.ts +16 -9
- package/dist/src/service/replies.js +5 -2
- package/dist/src/service/run-handlers.js +15 -10
- package/dist/src/service/service.d.ts +7 -3
- package/dist/src/service/service.js +16 -31
- package/dist/src/service/worker-totals.d.ts +4 -0
- package/dist/src/service/worker-totals.js +7 -0
- package/dist/src/service/workflow-reload.d.ts +35 -0
- package/dist/src/service/workflow-reload.js +52 -0
- package/dist/src/storage/decision-snapshots.d.ts +1 -2
- package/dist/src/storage/decision-snapshots.js +1 -7
- package/dist/src/storage/domain-changes.d.ts +1 -1
- package/dist/src/storage/domain-changes.js +1 -0
- package/dist/src/storage/domain-schema.d.ts +4 -1
- package/dist/src/storage/domain-schema.js +6 -2
- package/dist/src/storage/effect-schema.d.ts +1 -0
- package/dist/src/storage/effect-schema.js +4 -0
- package/dist/src/storage/projection-changes.d.ts +12 -0
- package/dist/src/storage/projection-changes.js +194 -0
- package/dist/src/storage/projection-lookups.d.ts +7 -0
- package/dist/src/storage/projection-lookups.js +30 -0
- package/dist/src/workflow-declaration.d.ts +1 -0
- package/dist/src/workflow-declaration.js +3 -0
- package/dist/tools/compile-workflows.js +3 -0
- package/dist/tools/project-configuration.d.ts +7 -2
- package/dist/tools/project-configuration.js +30 -1
- package/dist/tools/self-host-processes.js +37 -13
- package/dist/tools/self-host-project.d.ts +2 -0
- package/dist/tools/self-host-project.js +21 -3
- package/dist/tools/self-host.js +34 -24
- package/dist/tools/workflow-candidate-program.d.ts +4 -2
- package/dist/tools/workflow-candidate-program.js +30 -11
- package/dist/tools/workflow-candidate-source.d.ts +1 -0
- package/dist/tools/workflow-candidate-source.js +11 -3
- package/package.json +2 -2
- package/templates/develop/README.md +81 -4
- package/templates/develop/develop/flow-tests.ts +1 -1
- package/templates/develop/develop/interruption-tests.ts +157 -0
- package/templates/develop/develop/local.ts +1 -0
- package/templates/develop/develop/option-tests.ts +138 -9
- package/templates/develop/develop/red-green.ts +55 -0
- package/templates/develop/develop/report.ts +31 -11
- package/templates/develop/develop/round.ts +1 -0
- package/templates/develop/develop/scenarios.ts +27 -0
- package/templates/develop/develop/turn.ts +26 -6
- package/templates/develop/develop.declaration.json +1 -1
- package/templates/develop/develop.test.ts +4 -0
- package/templates/develop/template.json +29 -2
|
@@ -113,6 +113,19 @@ function operatorDeclaration(staged, compiled) {
|
|
|
113
113
|
functionName: workflow.workflowId.slice(workflow.workflowId.lastIndexOf("//") + 2),
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
+
async function recordProject(settings) {
|
|
117
|
+
const { project, recordedProject } = settings;
|
|
118
|
+
if (recordedProject === undefined)
|
|
119
|
+
return;
|
|
120
|
+
for (const name of [
|
|
121
|
+
"flow.mjs.__wf_tmp.js.debug.json",
|
|
122
|
+
"steps.mjs.debug.json",
|
|
123
|
+
]) {
|
|
124
|
+
const path = join(project, "artifacts", name);
|
|
125
|
+
const text = await readFile(path, "utf8");
|
|
126
|
+
await writeFile(path, text.replaceAll(`${project}/`, `${recordedProject}/`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
116
129
|
async function writeManifests(project, compiled) {
|
|
117
130
|
const manifests = {};
|
|
118
131
|
for (const [functionName, manifest] of Object.entries(compiled)) {
|
|
@@ -126,6 +139,13 @@ async function writeManifests(project, compiled) {
|
|
|
126
139
|
return manifests;
|
|
127
140
|
}
|
|
128
141
|
export async function preparedProject(settings) {
|
|
142
|
+
const prepared = await preparedBundle(settings);
|
|
143
|
+
const modules = join(dirname(settings.project), "workflow-bundles/node_modules");
|
|
144
|
+
await rm(modules, { force: true });
|
|
145
|
+
await symlink(join(settings.project, "node_modules"), modules);
|
|
146
|
+
return prepared;
|
|
147
|
+
}
|
|
148
|
+
export async function preparedBundle(settings) {
|
|
129
149
|
const { product, project } = settings;
|
|
130
150
|
await initializeProject(settings);
|
|
131
151
|
const folders = await loadWorkflowFolders(settings, {
|
|
@@ -133,6 +153,7 @@ export async function preparedProject(settings) {
|
|
|
133
153
|
});
|
|
134
154
|
const staged = await stageCandidates(project, folders.candidates);
|
|
135
155
|
const compiled = await compile(settings);
|
|
156
|
+
await recordProject(settings);
|
|
136
157
|
const combined = staged.map((candidate) => operatorDeclaration({ ...candidate, project }, compiled));
|
|
137
158
|
const manifests = await writeManifests(project, workflowManifests(compiled, combined));
|
|
138
159
|
await writeFile(join(project, "declarations.json"), JSON.stringify(combined));
|
|
@@ -145,9 +166,6 @@ export async function preparedProject(settings) {
|
|
|
145
166
|
repository: folders.repository,
|
|
146
167
|
directory: join(dirname(project), "workflow-bundles"),
|
|
147
168
|
});
|
|
148
|
-
const modules = join(dirname(project), "workflow-bundles/node_modules");
|
|
149
|
-
await rm(modules, { force: true });
|
|
150
|
-
await symlink(join(project, "node_modules"), modules);
|
|
151
169
|
return {
|
|
152
170
|
workflows: folders.workflows,
|
|
153
171
|
repository: folders.repository,
|
package/dist/tools/self-host.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { turnHarness, harnessDefaults, nativeHarness, } from "../src/harness-defaults.js";
|
|
2
2
|
import { absoluteWorkflowFolder } from "../src/project-settings.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { retentionSettings, } from "../src/record-retention.js";
|
|
4
|
+
import { configurationDigests, configuredTurnSettings, projectConfiguration, projectRoot, topLevel, } from "./project-configuration.js";
|
|
5
5
|
import { parseCommands, parseCommandSeconds, } from "../src/command-settings.js";
|
|
6
|
-
import {
|
|
7
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { writeFile } from "node:fs/promises";
|
|
8
7
|
import { homedir } from "node:os";
|
|
9
8
|
import { dirname, extname, join, resolve } from "node:path";
|
|
10
9
|
import { pathToFileURL } from "node:url";
|
|
@@ -66,19 +65,12 @@ function checkedWorker(name) {
|
|
|
66
65
|
});
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (isProfilesFile(value))
|
|
76
|
-
return value.profiles;
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
throw new Error("The daemon did not start: invalid-profiles");
|
|
80
|
-
}
|
|
81
|
-
throw new Error("The daemon did not start: invalid-profiles");
|
|
68
|
+
function startRefusal(error) {
|
|
69
|
+
if (error instanceof Error && error.message === "invalid-profiles")
|
|
70
|
+
throw new Error("The daemon did not start: invalid-profiles", {
|
|
71
|
+
cause: error,
|
|
72
|
+
});
|
|
73
|
+
throw error;
|
|
82
74
|
}
|
|
83
75
|
function queueLanes() {
|
|
84
76
|
const value = environment["CLOVING_QUEUE_LANES"];
|
|
@@ -153,6 +145,8 @@ if (opening.kind !== "started") {
|
|
|
153
145
|
process.exit(1);
|
|
154
146
|
}
|
|
155
147
|
process.stdout.write(${JSON.stringify(guide(workflows))});
|
|
148
|
+
process.send?.("started");
|
|
149
|
+
process.channel?.unref();
|
|
156
150
|
const stop = async () => { await opening.daemon.close(); process.exit(0); };
|
|
157
151
|
process.once("SIGINT", stop);
|
|
158
152
|
process.once("SIGTERM", stop);
|
|
@@ -193,11 +187,9 @@ function guide(workflows) {
|
|
|
193
187
|
"",
|
|
194
188
|
].join("\n");
|
|
195
189
|
}
|
|
196
|
-
const tokenPrices =
|
|
190
|
+
const { tokenPrices, fallbackModels, profiles } = await configuredTurnSettings(configuration).catch(startRefusal);
|
|
197
191
|
const commands = parseCommands(environment["CLOVING_COMMANDS"]);
|
|
198
192
|
const commandSeconds = parseCommandSeconds(environment["CLOVING_COMMAND_SECONDS"]);
|
|
199
|
-
const fallbackModels = parseFallbackModels(environment["CLOVING_FALLBACK_MODELS"]);
|
|
200
|
-
const profiles = await profilesFile();
|
|
201
193
|
const schedules = parseSchedulesSetting(environment["CLOVING_SCHEDULES"]);
|
|
202
194
|
const worker = selectedWorker();
|
|
203
195
|
function nativeCommands() {
|
|
@@ -219,14 +211,18 @@ const ticketCommand = listed("CLOVING_TICKET_COMMAND");
|
|
|
219
211
|
const queueDailySpendLimitUsd = queueSpendLimit();
|
|
220
212
|
const lanes = queueLanes();
|
|
221
213
|
const queueMilliseconds = queueInterval();
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
project,
|
|
214
|
+
const retention = retentionSettings(environment);
|
|
215
|
+
const places = {
|
|
225
216
|
repository,
|
|
226
217
|
home: homedir(),
|
|
227
218
|
xdgConfigHome: environment["XDG_CONFIG_HOME"],
|
|
219
|
+
};
|
|
220
|
+
const folders = {
|
|
221
|
+
...places,
|
|
228
222
|
userFolder: absoluteWorkflowFolder(environment["CLOVING_WORKFLOW_FOLDER"]),
|
|
229
|
-
}
|
|
223
|
+
};
|
|
224
|
+
const profilesPath = environment["CLOVING_PROFILES"];
|
|
225
|
+
const manifests = await preparedProject({ product, project, ...folders });
|
|
230
226
|
await launchers({
|
|
231
227
|
commands,
|
|
232
228
|
commandSeconds,
|
|
@@ -238,6 +234,7 @@ await launchers({
|
|
|
238
234
|
harness,
|
|
239
235
|
shell: environment["SHELL"] ?? "/bin/sh",
|
|
240
236
|
profiles,
|
|
237
|
+
settingDigests: configurationDigests(configuration, profiles),
|
|
241
238
|
command,
|
|
242
239
|
ticketCommand,
|
|
243
240
|
nextCommand,
|
|
@@ -245,11 +242,24 @@ await launchers({
|
|
|
245
242
|
queueLanes: lanes,
|
|
246
243
|
queueDailySpendLimitUsd,
|
|
247
244
|
resetQueue: flags.includes("--reset-queue"),
|
|
245
|
+
retention,
|
|
248
246
|
notifyCommand: listed("CLOVING_NOTIFY_COMMAND"),
|
|
249
247
|
githubCommand: listed("CLOVING_GH_COMMAND"),
|
|
250
248
|
requiredChecks: listed("CLOVING_REQUIRED_CHECKS") ?? null,
|
|
251
249
|
checkCommand: listed("CLOVING_CHECK_COMMAND"),
|
|
252
250
|
privateTexts: listed("CLOVING_PRIVATE_TEXTS"),
|
|
251
|
+
reloadCommand: [
|
|
252
|
+
process.execPath,
|
|
253
|
+
join(product, `tools/self-host-reload${extname(import.meta.filename)}`),
|
|
254
|
+
JSON.stringify({
|
|
255
|
+
directory,
|
|
256
|
+
...places,
|
|
257
|
+
...(profilesPath === undefined
|
|
258
|
+
? {}
|
|
259
|
+
: { profilesFile: resolve(profilesPath) }),
|
|
260
|
+
fileVariables: Object.keys(environment).filter((name) => process.env[name] === undefined),
|
|
261
|
+
}),
|
|
262
|
+
],
|
|
253
263
|
}, manifests);
|
|
254
264
|
process.exitCode = await hostedPrograms({
|
|
255
265
|
environment,
|
|
@@ -5,9 +5,11 @@ export interface CandidatePackageOptions {
|
|
|
5
5
|
readonly helpers?: Readonly<Record<string, string>>;
|
|
6
6
|
}
|
|
7
7
|
export declare function resolvedSymbol(checker: ts.TypeChecker, symbol: ts.Symbol | undefined): ts.Symbol | undefined;
|
|
8
|
-
export
|
|
8
|
+
export interface CandidateProgram {
|
|
9
9
|
readonly program: ts.Program;
|
|
10
10
|
readonly source: ts.SourceFile;
|
|
11
11
|
readonly sources: readonly ts.SourceFile[];
|
|
12
12
|
readonly modules: ReadonlyMap<string, ts.SourceFile>;
|
|
13
|
-
}
|
|
13
|
+
}
|
|
14
|
+
export declare function candidateProgram(text: string, installed?: CandidatePackageOptions): CandidateProgram;
|
|
15
|
+
export declare function candidateCompiler(installed?: CandidatePackageOptions): (text: string) => CandidateProgram;
|
|
@@ -10,9 +10,12 @@ export function resolvedSymbol(checker, symbol) {
|
|
|
10
10
|
return checker.getAliasedSymbol(symbol);
|
|
11
11
|
}
|
|
12
12
|
export function candidateProgram(text, installed = {}) {
|
|
13
|
+
return candidateCompiler(installed)(text);
|
|
14
|
+
}
|
|
15
|
+
export function candidateCompiler(installed = {}) {
|
|
13
16
|
const packageRoot = resolve(installed.packageRoot ?? candidatePackageRoot);
|
|
14
17
|
const candidate = join(packageRoot, "candidate.ts");
|
|
15
|
-
const sources = new Map([[candidate,
|
|
18
|
+
const sources = new Map([[candidate, ""]]);
|
|
16
19
|
for (const [name, content] of Object.entries(installed.helpers ?? {})) {
|
|
17
20
|
if (workflowHelperPath("candidate.ts", `./${name}`) !== name)
|
|
18
21
|
throw new Error("invalid-workflow-helper");
|
|
@@ -32,12 +35,18 @@ export function candidateProgram(text, installed = {}) {
|
|
|
32
35
|
noEmit: true,
|
|
33
36
|
};
|
|
34
37
|
const host = trustedHost({ sources, options, entries, packageRoot });
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
return (text) => {
|
|
39
|
+
sources.set(candidate, text);
|
|
40
|
+
const program = ts.createProgram([...sources.keys(), ...entries.values()], options, host);
|
|
41
|
+
return compiledCandidate(program, { candidate, sources, entries });
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function compiledCandidate(program, input) {
|
|
45
|
+
const source = program.getSourceFile(input.candidate);
|
|
37
46
|
if (!source)
|
|
38
47
|
throw new Error("candidate-analysis-unavailable");
|
|
39
48
|
const modules = new Map();
|
|
40
|
-
for (const [name, file] of entries) {
|
|
49
|
+
for (const [name, file] of input.entries) {
|
|
41
50
|
const declaration = program.getSourceFile(file);
|
|
42
51
|
if (!declaration)
|
|
43
52
|
throw new Error("candidate-surface-unavailable");
|
|
@@ -47,7 +56,7 @@ export function candidateProgram(text, installed = {}) {
|
|
|
47
56
|
program,
|
|
48
57
|
source,
|
|
49
58
|
modules,
|
|
50
|
-
sources: [...sources.keys()].map((name) => {
|
|
59
|
+
sources: [...input.sources.keys()].map((name) => {
|
|
51
60
|
const entry = program.getSourceFile(name);
|
|
52
61
|
if (!entry)
|
|
53
62
|
throw new Error("candidate-analysis-unavailable");
|
|
@@ -55,6 +64,21 @@ export function candidateProgram(text, installed = {}) {
|
|
|
55
64
|
}),
|
|
56
65
|
};
|
|
57
66
|
}
|
|
67
|
+
function sourceReader(readFile) {
|
|
68
|
+
const declarations = new Map();
|
|
69
|
+
return (file, languageVersion) => {
|
|
70
|
+
const content = readFile(file);
|
|
71
|
+
if (content === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const cached = declarations.get(file);
|
|
74
|
+
if (cached?.text === content)
|
|
75
|
+
return cached;
|
|
76
|
+
const source = ts.createSourceFile(file, content, languageVersion, true);
|
|
77
|
+
if (file.endsWith(".d.ts"))
|
|
78
|
+
declarations.set(file, source);
|
|
79
|
+
return source;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
58
82
|
function trustedHost(input) {
|
|
59
83
|
const { sources, options, entries, packageRoot } = input;
|
|
60
84
|
const libraries = dirname(ts.getDefaultLibFilePath(options)) + sep;
|
|
@@ -72,12 +96,7 @@ function trustedHost(input) {
|
|
|
72
96
|
const host = ts.createCompilerHost(options);
|
|
73
97
|
host.readFile = readFile;
|
|
74
98
|
host.fileExists = (file) => readFile(file) !== undefined;
|
|
75
|
-
host.getSourceFile = (
|
|
76
|
-
const content = readFile(file);
|
|
77
|
-
return content === undefined
|
|
78
|
-
? undefined
|
|
79
|
-
: ts.createSourceFile(file, content, languageVersion, true);
|
|
80
|
-
};
|
|
99
|
+
host.getSourceFile = sourceReader(readFile);
|
|
81
100
|
host.resolveModuleNameLiterals = (names, containingFile) => names.map(({ text: name }) => {
|
|
82
101
|
const installed = entries.get(name);
|
|
83
102
|
if (installed)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type CandidatePackageOptions } from "./workflow-candidate-program.ts";
|
|
2
2
|
import type { CandidateAnalysis, InstalledWorkflowExports } from "../src/workflow-candidate.ts";
|
|
3
|
+
export declare function createWorkflowCandidateAnalyzer(installed?: CandidatePackageOptions): (text: string) => ReturnType<typeof analyzeWorkflowCandidate>;
|
|
3
4
|
export declare function analyzeWorkflowCandidate(text: string, installed?: CandidatePackageOptions): {
|
|
4
5
|
readonly analysis: CandidateAnalysis;
|
|
5
6
|
readonly installedExports: InstalledWorkflowExports;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { passiveCandidateModule, workflowShape, } from "./workflow-candidate-shape.js";
|
|
2
2
|
import ts from "typescript";
|
|
3
3
|
import { dirname, relative, sep } from "node:path";
|
|
4
|
-
import { candidateProgram, resolvedSymbol, } from "./workflow-candidate-program.js";
|
|
4
|
+
import { candidateCompiler, candidateProgram, resolvedSymbol, } from "./workflow-candidate-program.js";
|
|
5
5
|
import { protectedEffectFact, commandWaitCall, unsupportedCandidateNode, } from "./workflow-candidate-syntax.js";
|
|
6
6
|
import { literalField } from "./workflow-candidate-fields.js";
|
|
7
7
|
import { sourceEffectNodes } from "./workflow-source-effects.js";
|
|
8
8
|
import { candidateEffectTotals } from "./workflow-candidate-counts.js";
|
|
9
9
|
import { helperEffectTotals } from "./workflow-candidate-helper-counts.js";
|
|
10
|
+
export function createWorkflowCandidateAnalyzer(installed = {}) {
|
|
11
|
+
const compile = candidateCompiler(installed);
|
|
12
|
+
const localSources = Object.keys(installed.helpers ?? {});
|
|
13
|
+
return (text) => analyzeProgram(compile(text), localSources);
|
|
14
|
+
}
|
|
10
15
|
function exportSnapshot(modules, checker) {
|
|
11
16
|
return Object.fromEntries([...modules].map(([name, declaration]) => {
|
|
12
17
|
const symbol = checker.getSymbolAtLocation(declaration);
|
|
@@ -90,7 +95,10 @@ function mergeHelpers(modules, checker) {
|
|
|
90
95
|
});
|
|
91
96
|
}
|
|
92
97
|
export function analyzeWorkflowCandidate(text, installed = {}) {
|
|
93
|
-
|
|
98
|
+
return analyzeProgram(candidateProgram(text, installed), Object.keys(installed.helpers ?? {}));
|
|
99
|
+
}
|
|
100
|
+
function analyzeProgram(compiled, localSources) {
|
|
101
|
+
const { program, source, sources, modules } = compiled;
|
|
94
102
|
const checker = program.getTypeChecker();
|
|
95
103
|
const symbols = {
|
|
96
104
|
checker,
|
|
@@ -125,7 +133,7 @@ export function analyzeWorkflowCandidate(text, installed = {}) {
|
|
|
125
133
|
const facts = sources.map((entry) => analyzedSource({ ...symbols, source: entry }, { program, workflow: source }));
|
|
126
134
|
return {
|
|
127
135
|
analysis: combineFacts({
|
|
128
|
-
localSources
|
|
136
|
+
localSources,
|
|
129
137
|
facts,
|
|
130
138
|
expanded: helperEffectTotals({
|
|
131
139
|
symbols: { ...symbols, source },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloving",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Local durable development workflow service",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"check:format": "prettier --check src tools quality test eslint.config.mjs package.json tsconfig.json tsconfig.build.json",
|
|
129
129
|
"test:quality": "node --test test/quality/*.test.ts",
|
|
130
130
|
"record:test-durations": "node tools/record-test-durations.ts",
|
|
131
|
-
"test:scale": "node --test --test-concurrency=1 test/authority-opening-scale.benchmark.ts",
|
|
131
|
+
"test:scale": "node --test --test-concurrency=1 test/authority-opening-scale.benchmark.ts test/status-scale.benchmark.ts",
|
|
132
132
|
"check": "node tools/quality-pending.ts",
|
|
133
133
|
"test:concurrency": "node tools/quality-pending.ts"
|
|
134
134
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Develop template decisions
|
|
2
2
|
|
|
3
|
-
`template.json` describes the
|
|
3
|
+
`template.json` describes the six decisions used by the develop template.
|
|
4
4
|
The default workflow in `develop.ts` accepts the declared ticket input, selects
|
|
5
5
|
from the queue, opens a pull request, waits for Greptile at 5/5, and calls
|
|
6
6
|
`mergeWhenGreen` with `check: "beforeMerge"`. Its declaration and helper files
|
|
@@ -31,8 +31,9 @@ step bridge. The alternative configurations use `develop/option-tests.ts`, with
|
|
|
31
31
|
point IDs in `develop/scenarios.ts`. For an edited copy, replace the default
|
|
32
32
|
`flowTests` and `refusalTests` imports in `develop.test.ts` with
|
|
33
33
|
`optionTests as flowTests`, then run every `flowTests` scenario. Keep the shared
|
|
34
|
-
turn
|
|
35
|
-
|
|
34
|
+
turn, partial-helper and interruption tests. `develop/interruption-tests.ts`
|
|
35
|
+
follows the selected point IDs, so it tests each configuration. Package tests apply each source edit, validate
|
|
36
|
+
its requirements, and test ten complete configurations through the folder
|
|
36
37
|
loader and the installed package, covering every option.
|
|
37
38
|
|
|
38
39
|
`develop/turn.ts` implements the development turn for either a resolved
|
|
@@ -44,6 +45,22 @@ allow the initial turn and three fix turns; the workflow must stop after that.
|
|
|
44
45
|
The package tests also load small compositions of this helper through folder
|
|
45
46
|
admission for both input forms and test both forms in complete workflows.
|
|
46
47
|
|
|
48
|
+
`developReport` in `develop/report.ts` runs the turn again once when its result
|
|
49
|
+
has the reason `claude-turn-interrupted`. This occurs when the daemon cannot
|
|
50
|
+
resume an open turn after a restart. Before the daemon reports this reason, it
|
|
51
|
+
stops the processes of the earlier turn in the work tree. The new turn has the same target, so the
|
|
52
|
+
daemon gives it the same work tree. Its scope adds the key `continuation`, and
|
|
53
|
+
its prompt tells the worker that the earlier turn stopped before its report.
|
|
54
|
+
The worker checks the work tree, the commits since the start of the round, and
|
|
55
|
+
the ticket before it continues.
|
|
56
|
+
|
|
57
|
+
After a second interruption of the same turn, `developReport` returns a blocked
|
|
58
|
+
outcome with the reason `turn-interrupted`. `develop/round.ts` and
|
|
59
|
+
`develop/local.ts` return this outcome before they use the report. A
|
|
60
|
+
pull-request workflow then adds `cloving: blocked` to the GitHub issue of the
|
|
61
|
+
ticket. Other results that are not `completed` fail the run. The default effect
|
|
62
|
+
limit of 31 includes one more turn in each of the four rounds.
|
|
63
|
+
|
|
47
64
|
`develop/partial.ts` implements the person's partial-work question.
|
|
48
65
|
`develop/partial-choice.ts` connects its answer to a development report and the
|
|
49
66
|
next round, or returns a blocked outcome without opening a review or merging. `reviewPartial`
|
|
@@ -68,10 +85,70 @@ Task text keeps its full contents, including any issue-looking lines, without
|
|
|
68
85
|
selecting an issue to label, close or return to the queue.
|
|
69
86
|
|
|
70
87
|
The first option of each point is the default: ticket input, queue selection,
|
|
71
|
-
Greptile at 5/5, merge after checks, and return
|
|
88
|
+
no additional verification, Greptile at 5/5, merge after checks, and return
|
|
89
|
+
partial work to the queue.
|
|
72
90
|
Selecting `person` at selection, acceptance and partial work, with `local` at
|
|
73
91
|
merge, describes the existing reviewed local-branch process.
|
|
74
92
|
|
|
93
|
+
The `verify` point defaults to `none`, preserving the current round and
|
|
94
|
+
declaration. Select `red-green` with either `green` or `person` at `merge`.
|
|
95
|
+
Import `verifyRedGreen` from `develop/red-green.ts` in `develop/round.ts`.
|
|
96
|
+
Immediately after `openPull`, before constructing the review subject or
|
|
97
|
+
calling `acceptWork`, add:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const verification = await verifyRedGreen({
|
|
101
|
+
cycleId: input.cycleId,
|
|
102
|
+
head: pull.commit,
|
|
103
|
+
round,
|
|
104
|
+
});
|
|
105
|
+
if (verification !== null && "kind" in verification) return verification;
|
|
106
|
+
if (verification !== null)
|
|
107
|
+
return repair({ cycleId: input.cycleId, round, report, result: verification });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Add `"command"` to `workflow.capabilities`, `"commands": ["red-green"]` to
|
|
111
|
+
`workflow`, and `"command": ["run:red-green"]` to `methods` in
|
|
112
|
+
`develop.declaration.json`. Increase `workflow.effectLimit` by 4, from 31 to
|
|
113
|
+
35, allowing one verification command in each of the four development rounds.
|
|
114
|
+
Set `selection.verify` to `"red-green"` in `develop/scenarios.ts` and run the
|
|
115
|
+
option tests. The local merge option does not support this verification.
|
|
116
|
+
|
|
117
|
+
The helper calls the operator command on the pull request's immutable head with
|
|
118
|
+
scope `{ phase: "verify", operation: "red-green", round }`.
|
|
119
|
+
`accepted` and `no-test-change` proceed to review. `false-red`,
|
|
120
|
+
`red-not-assertion`, `skipped-test` and `green-failed` pass the verdict and full
|
|
121
|
+
tool output through `repair` to the next fix turn. These fixes share the
|
|
122
|
+
existing four-round limit with review and merge fixes. `prepare-failed`, a
|
|
123
|
+
failed command or an invalid result blocks immediately with
|
|
124
|
+
`red-green-unavailable`, without a fix turn.
|
|
125
|
+
|
|
126
|
+
Register `red-green` in `CLOVING_COMMANDS` as an argument array containing the
|
|
127
|
+
absolute path of the installed `cloving` executable followed by `"red-green"`.
|
|
128
|
+
Configure the project file `.cloving/config.json`, for example:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"version": 1,
|
|
133
|
+
"checks": {
|
|
134
|
+
"redGreen": {
|
|
135
|
+
"test": ["node", "--test"],
|
|
136
|
+
"testFiles": ["test/"],
|
|
137
|
+
"prepare": ["npm", "ci", "--ignore-scripts"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The tool appends changed test paths to `test`; optional `prepare` runs in each
|
|
144
|
+
temporary worktree. The daemon passes this configuration as
|
|
145
|
+
`CLOVING_RED_GREEN`. For direct tool use, set that variable to the `redGreen`
|
|
146
|
+
object. The repository needs `refs/remotes/origin/HEAD` to identify the base.
|
|
147
|
+
Increase `CLOVING_COMMAND_SECONDS`, for example to `600`, to cover both
|
|
148
|
+
preparation and test runs. Optional `assertionPattern` and `skipPattern`
|
|
149
|
+
override the defaults for non-terminal `node --test` output. See the package
|
|
150
|
+
README's `cloving red-green` documentation for the tool's complete contract.
|
|
151
|
+
|
|
75
152
|
Each option has an `id`, `label`, `summary`, `change`, `requires` and `needs`.
|
|
76
153
|
`change` describes the step call or declaration edit for the setup agent.
|
|
77
154
|
It is guidance for editing source, rather than an executable patch.
|
|
@@ -127,7 +127,7 @@ flowTests.push({
|
|
|
127
127
|
flowTests.push({
|
|
128
128
|
name: "CLV-221 the default workflow merges only a reviewed pull after the checks",
|
|
129
129
|
run: async () => {
|
|
130
|
-
using bridge = fakeStepBridge({ effects: effects(), effectLimit:
|
|
130
|
+
using bridge = fakeStepBridge({ effects: effects(), effectLimit: 31 });
|
|
131
131
|
assert.deepEqual(await develop(input), {
|
|
132
132
|
kind: "handoff_requested",
|
|
133
133
|
cycleId: input.cycleId,
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import {
|
|
3
|
+
fakeStepBridge,
|
|
4
|
+
type ScenarioRequest,
|
|
5
|
+
} from "cloving/workflows/testing";
|
|
6
|
+
import { develop } from "../develop.ts";
|
|
7
|
+
import {
|
|
8
|
+
approved,
|
|
9
|
+
callName,
|
|
10
|
+
completeCalls,
|
|
11
|
+
requestInput,
|
|
12
|
+
scenarioEffects,
|
|
13
|
+
} from "./option-tests.ts";
|
|
14
|
+
import { developReport, ticketInput } from "./report.ts";
|
|
15
|
+
import { completed, input } from "./scenarios.ts";
|
|
16
|
+
|
|
17
|
+
const interrupted = {
|
|
18
|
+
kind: "failed" as const,
|
|
19
|
+
operationId: "interrupted-turn",
|
|
20
|
+
reason: "claude-turn-interrupted",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function turns(requests: readonly ScenarioRequest[]) {
|
|
24
|
+
return requests.flatMap((request) =>
|
|
25
|
+
request[0] === "effect" && request[1].method === "run-turn"
|
|
26
|
+
? [request[1]]
|
|
27
|
+
: [],
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const interruptionTests = [
|
|
32
|
+
{
|
|
33
|
+
name: "CLV-272 an interrupted turn runs again once in the same work tree",
|
|
34
|
+
run: async () => {
|
|
35
|
+
const scenario = scenarioEffects();
|
|
36
|
+
using bridge = fakeStepBridge({
|
|
37
|
+
effects: {
|
|
38
|
+
...scenario,
|
|
39
|
+
"claude run-turn": [interrupted, ...scenario["claude run-turn"]],
|
|
40
|
+
},
|
|
41
|
+
decisions: [approved, approved],
|
|
42
|
+
});
|
|
43
|
+
assert.equal((await develop(requestInput())).kind, "handoff_requested");
|
|
44
|
+
const expected = completeCalls();
|
|
45
|
+
expected.splice(expected.indexOf("run-turn"), 0, "run-turn");
|
|
46
|
+
assert.deepEqual(bridge.requests.map(callName), expected);
|
|
47
|
+
const [first, second] = turns(bridge.requests);
|
|
48
|
+
assert.ok(first && second);
|
|
49
|
+
assert.deepEqual(first.scope, {
|
|
50
|
+
phase: "implement",
|
|
51
|
+
operation: "claude-turn",
|
|
52
|
+
round: 1,
|
|
53
|
+
});
|
|
54
|
+
assert.ok(!first.payload.includes("stopped before its report"));
|
|
55
|
+
assert.deepEqual(second.scope, {
|
|
56
|
+
phase: "implement",
|
|
57
|
+
operation: "claude-turn",
|
|
58
|
+
round: 1,
|
|
59
|
+
key: "continuation",
|
|
60
|
+
});
|
|
61
|
+
assert.equal(second.target, first.target);
|
|
62
|
+
assert.ok(second.payload.endsWith(first.payload));
|
|
63
|
+
for (const sentence of [
|
|
64
|
+
"The earlier turn of round 1 stopped before its report.",
|
|
65
|
+
"The work tree holds the work of that turn.",
|
|
66
|
+
"Before you continue, check the work tree, the commits since the start of the round, and the ticket.",
|
|
67
|
+
])
|
|
68
|
+
assert.ok(second.payload.includes(sentence), sentence);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "CLV-272 a second interruption of the same turn blocks the run",
|
|
73
|
+
run: async () => {
|
|
74
|
+
using bridge = fakeStepBridge({
|
|
75
|
+
effects: {
|
|
76
|
+
...scenarioEffects(),
|
|
77
|
+
"claude run-turn": [interrupted, interrupted],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
assert.deepEqual(await develop(requestInput()), {
|
|
81
|
+
kind: "blocked",
|
|
82
|
+
cycleId: requestInput().cycleId,
|
|
83
|
+
reason: "turn-interrupted",
|
|
84
|
+
evidenceId: "implement",
|
|
85
|
+
});
|
|
86
|
+
assert.deepEqual(bridge.requests.map(callName), stoppedCalls());
|
|
87
|
+
const labels = bridge.requests.flatMap((request) =>
|
|
88
|
+
request[0] === "effect" && request[1].method === "label"
|
|
89
|
+
? [request[1].payload]
|
|
90
|
+
: [],
|
|
91
|
+
);
|
|
92
|
+
for (const payload of labels.slice(1))
|
|
93
|
+
assert.match(payload, /"add":\["cloving: blocked"\]/);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "CLV-272 an interrupted fix turn continues with its round and feedback",
|
|
98
|
+
run: async () => {
|
|
99
|
+
using bridge = fakeStepBridge({
|
|
100
|
+
effects: {
|
|
101
|
+
"claude run-turn": [
|
|
102
|
+
interrupted,
|
|
103
|
+
completed({ summary: "Fixed.\nTicket status: complete" }),
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
const output = "Check output.\n";
|
|
108
|
+
const report = await developReport({
|
|
109
|
+
input: ticketInput(input),
|
|
110
|
+
round: 2,
|
|
111
|
+
feedback: { reason: "check-failed", output, remaining: ["Add it."] },
|
|
112
|
+
});
|
|
113
|
+
assert.ok(!("kind" in report));
|
|
114
|
+
assert.equal(report.summary, "Fixed.\nTicket status: complete");
|
|
115
|
+
const [first, second] = turns(bridge.requests);
|
|
116
|
+
assert.ok(first && second);
|
|
117
|
+
assert.equal(first.scope.round, 2);
|
|
118
|
+
assert.equal(second.scope.round, 2);
|
|
119
|
+
assert.equal(second.scope.key, "continuation");
|
|
120
|
+
assert.ok(
|
|
121
|
+
second.payload.startsWith(
|
|
122
|
+
"The earlier turn of round 2 stopped before its report.",
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
assert.ok(second.payload.endsWith(first.payload));
|
|
126
|
+
assert.ok(second.payload.endsWith(output));
|
|
127
|
+
assert.match(
|
|
128
|
+
second.payload,
|
|
129
|
+
/This is fix round 2\. Address check-failed/,
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
...[
|
|
134
|
+
{ kind: "failed" as const, operationId: "turn", reason: "worker-failed" },
|
|
135
|
+
{ kind: "refused" as const, reason: "budget-exhausted" },
|
|
136
|
+
].map((failure) => ({
|
|
137
|
+
name: `CLV-272 a ${failure.reason} continuation fails the run`,
|
|
138
|
+
run: async () => {
|
|
139
|
+
using bridge = fakeStepBridge({
|
|
140
|
+
effects: {
|
|
141
|
+
...scenarioEffects(),
|
|
142
|
+
"claude run-turn": [interrupted, failure],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
await assert.rejects(develop(requestInput()), {
|
|
146
|
+
message: failure.reason,
|
|
147
|
+
});
|
|
148
|
+
assert.deepEqual(bridge.requests.map(callName), stoppedCalls());
|
|
149
|
+
},
|
|
150
|
+
})),
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
function stoppedCalls(): string[] {
|
|
154
|
+
const complete = completeCalls();
|
|
155
|
+
const started = complete.slice(0, complete.indexOf("run-turn") + 1);
|
|
156
|
+
return started.concat("run-turn", started.includes("label") ? ["label"] : []);
|
|
157
|
+
}
|
|
@@ -13,6 +13,7 @@ export async function localRound(
|
|
|
13
13
|
): Promise<CycleOutcome | DevelopFeedback> {
|
|
14
14
|
const { input, round } = state;
|
|
15
15
|
const report = await developReport(state);
|
|
16
|
+
if ("kind" in report) return report;
|
|
16
17
|
const partial = await partialChoice({ input, round, report });
|
|
17
18
|
if (partial !== null) return partial;
|
|
18
19
|
if (
|