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.
Files changed (167) hide show
  1. package/README.md +100 -15
  2. package/dist/src/broker/actor/authority-health.d.ts +9 -0
  3. package/dist/src/broker/actor/authority-health.js +24 -0
  4. package/dist/src/broker/actor/authority-runtime.d.ts +4 -2
  5. package/dist/src/broker/actor/authority-runtime.js +29 -20
  6. package/dist/src/broker/actor/authority-session.d.ts +6 -19
  7. package/dist/src/broker/actor/authority-session.js +66 -88
  8. package/dist/src/broker/actor/authority-settings.d.ts +2 -1
  9. package/dist/src/broker/actor/authority-settings.js +4 -1
  10. package/dist/src/broker/actor/authority-upgrades.d.ts +5 -1
  11. package/dist/src/broker/actor/authority-upgrades.js +2 -1
  12. package/dist/src/broker/actor/authority-watchdog.d.ts +6 -2
  13. package/dist/src/broker/actor/authority-watchdog.js +31 -9
  14. package/dist/src/broker/actor/authority.js +3 -1
  15. package/dist/src/broker/actor/projection-memory.d.ts +43 -0
  16. package/dist/src/broker/actor/projection-memory.js +207 -0
  17. package/dist/src/broker/actor/runtime-parts.d.ts +5 -0
  18. package/dist/src/broker/actor/runtime-parts.js +8 -3
  19. package/dist/src/broker/authority-restore.js +1 -1
  20. package/dist/src/broker/effect-upgrade.d.ts +1 -0
  21. package/dist/src/broker/effect-upgrade.js +4 -1
  22. package/dist/src/cli/arguments.js +1 -0
  23. package/dist/src/cli/client.d.ts +3 -1
  24. package/dist/src/cli/client.js +4 -4
  25. package/dist/src/cli/command-requests.d.ts +30 -1
  26. package/dist/src/cli/command-requests.js +43 -2
  27. package/dist/src/cli/commands.js +57 -48
  28. package/dist/src/cli/config-notice.js +5 -2
  29. package/dist/src/cli/doctor-retention.d.ts +2 -0
  30. package/dist/src/cli/doctor-retention.js +93 -0
  31. package/dist/src/cli/doctor.js +5 -0
  32. package/dist/src/cli/init-prompt.js +1 -1
  33. package/dist/src/cli/mcp-server.js +4 -4
  34. package/dist/src/cli/mcp-tools.d.ts +14 -14
  35. package/dist/src/cli/mcp-tools.js +212 -50
  36. package/dist/src/cli/queue-arguments.d.ts +15 -0
  37. package/dist/src/cli/queue-arguments.js +31 -0
  38. package/dist/src/cli/queue-render.d.ts +3 -0
  39. package/dist/src/cli/queue-render.js +26 -0
  40. package/dist/src/cli/reload-render.d.ts +2 -0
  41. package/dist/src/cli/reload-render.js +12 -0
  42. package/dist/src/cli/render.js +37 -8
  43. package/dist/src/cli/report-github-reader.js +1 -1
  44. package/dist/src/cli/runtime-arguments.d.ts +17 -3
  45. package/dist/src/cli/runtime-arguments.js +23 -19
  46. package/dist/src/cli/toolchain.d.ts +1 -0
  47. package/dist/src/cli/toolchain.js +1 -0
  48. package/dist/src/cli/usage.d.ts +2 -2
  49. package/dist/src/cli/usage.js +40 -5
  50. package/dist/src/cli/watch-api-document.d.ts +430 -0
  51. package/dist/src/cli/watch-api-document.js +291 -0
  52. package/dist/src/cli/watch-api-run.d.ts +56 -0
  53. package/dist/src/cli/watch-api-run.js +112 -0
  54. package/dist/src/cli/watch-api.d.ts +17 -0
  55. package/dist/src/cli/watch-api.js +230 -0
  56. package/dist/src/cli/watch-header-view.d.ts +1 -0
  57. package/dist/src/cli/watch-header-view.js +9 -1
  58. package/dist/src/cli/watch-model.d.ts +1 -0
  59. package/dist/src/cli/watch-model.js +3 -0
  60. package/dist/src/cli/watch-page.js +3 -1
  61. package/dist/src/cli/watch-reads.d.ts +1 -0
  62. package/dist/src/cli/watch-reads.js +6 -5
  63. package/dist/src/cli/watch-render.d.ts +1 -0
  64. package/dist/src/cli/watch-server.js +18 -4
  65. package/dist/src/cli.js +50 -43
  66. package/dist/src/configuration-display.js +2 -0
  67. package/dist/src/configuration-schema.d.ts +5 -0
  68. package/dist/src/daemon/claude-recovery.js +13 -9
  69. package/dist/src/daemon/claude-worker.d.ts +3 -3
  70. package/dist/src/daemon/claude-worker.js +10 -11
  71. package/dist/src/daemon/daemon-adapters.d.ts +7 -0
  72. package/dist/src/daemon/daemon-adapters.js +7 -3
  73. package/dist/src/daemon/daemon-loops.d.ts +2 -0
  74. package/dist/src/daemon/daemon-loops.js +17 -11
  75. package/dist/src/daemon/daemon.js +71 -60
  76. package/dist/src/daemon/github-attach.js +77 -4
  77. package/dist/src/daemon/github-calls.js +1 -1
  78. package/dist/src/daemon/github-methods.js +6 -2
  79. package/dist/src/daemon/issue-queue.d.ts +3 -0
  80. package/dist/src/daemon/issue-queue.js +66 -73
  81. package/dist/src/daemon/merge-turns.d.ts +10 -0
  82. package/dist/src/daemon/merge-turns.js +37 -0
  83. package/dist/src/daemon/queue-control.d.ts +13 -0
  84. package/dist/src/daemon/queue-control.js +86 -0
  85. package/dist/src/daemon/queue-journal.d.ts +20 -0
  86. package/dist/src/daemon/queue-journal.js +89 -0
  87. package/dist/src/daemon/record-archive.d.ts +19 -0
  88. package/dist/src/daemon/record-archive.js +82 -0
  89. package/dist/src/daemon/record-sweep.d.ts +24 -0
  90. package/dist/src/daemon/record-sweep.js +147 -0
  91. package/dist/src/daemon/retrying-turn.js +4 -4
  92. package/dist/src/daemon/settings-reload.d.ts +26 -0
  93. package/dist/src/daemon/settings-reload.js +82 -0
  94. package/dist/src/daemon/turn-details.d.ts +4 -0
  95. package/dist/src/daemon/turn-details.js +33 -0
  96. package/dist/src/daemon/worker-profile.d.ts +1 -0
  97. package/dist/src/daemon/worker-profile.js +4 -0
  98. package/dist/src/daemon/workflow-generations.d.ts +7 -4
  99. package/dist/src/daemon/workflow-generations.js +22 -14
  100. package/dist/src/daemon/workflow-reload.d.ts +41 -0
  101. package/dist/src/daemon/workflow-reload.js +244 -0
  102. package/dist/src/daemon/workflow-replay.d.ts +2 -1
  103. package/dist/src/daemon/workflow-replay.js +7 -3
  104. package/dist/src/doctor-report.d.ts +2 -2
  105. package/dist/src/doctor-report.js +2 -0
  106. package/dist/src/private-config.schema.json +31 -0
  107. package/dist/src/project-settings.d.ts +15 -1
  108. package/dist/src/project-settings.js +33 -1
  109. package/dist/src/queue-state.d.ts +26 -0
  110. package/dist/src/queue-state.js +34 -0
  111. package/dist/src/record-retention.d.ts +46 -0
  112. package/dist/src/record-retention.js +81 -0
  113. package/dist/src/service/active-tickets.js +5 -0
  114. package/dist/src/service/dispatcher.d.ts +4 -0
  115. package/dist/src/service/dispatcher.js +33 -0
  116. package/dist/src/service/queue-handlers.d.ts +4 -0
  117. package/dist/src/service/queue-handlers.js +32 -0
  118. package/dist/src/service/registry.d.ts +28 -2
  119. package/dist/src/service/registry.js +48 -0
  120. package/dist/src/service/replies.d.ts +16 -9
  121. package/dist/src/service/replies.js +5 -2
  122. package/dist/src/service/run-handlers.js +15 -10
  123. package/dist/src/service/service.d.ts +7 -3
  124. package/dist/src/service/service.js +16 -31
  125. package/dist/src/service/worker-totals.d.ts +4 -0
  126. package/dist/src/service/worker-totals.js +7 -0
  127. package/dist/src/service/workflow-reload.d.ts +35 -0
  128. package/dist/src/service/workflow-reload.js +52 -0
  129. package/dist/src/storage/decision-snapshots.d.ts +1 -2
  130. package/dist/src/storage/decision-snapshots.js +1 -7
  131. package/dist/src/storage/domain-changes.d.ts +1 -1
  132. package/dist/src/storage/domain-changes.js +1 -0
  133. package/dist/src/storage/domain-schema.d.ts +4 -1
  134. package/dist/src/storage/domain-schema.js +6 -2
  135. package/dist/src/storage/effect-schema.d.ts +1 -0
  136. package/dist/src/storage/effect-schema.js +4 -0
  137. package/dist/src/storage/projection-changes.d.ts +12 -0
  138. package/dist/src/storage/projection-changes.js +194 -0
  139. package/dist/src/storage/projection-lookups.d.ts +7 -0
  140. package/dist/src/storage/projection-lookups.js +30 -0
  141. package/dist/src/workflow-declaration.d.ts +1 -0
  142. package/dist/src/workflow-declaration.js +3 -0
  143. package/dist/tools/compile-workflows.js +3 -0
  144. package/dist/tools/project-configuration.d.ts +7 -2
  145. package/dist/tools/project-configuration.js +30 -1
  146. package/dist/tools/self-host-processes.js +37 -13
  147. package/dist/tools/self-host-project.d.ts +2 -0
  148. package/dist/tools/self-host-project.js +21 -3
  149. package/dist/tools/self-host.js +34 -24
  150. package/dist/tools/workflow-candidate-program.d.ts +4 -2
  151. package/dist/tools/workflow-candidate-program.js +30 -11
  152. package/dist/tools/workflow-candidate-source.d.ts +1 -0
  153. package/dist/tools/workflow-candidate-source.js +11 -3
  154. package/package.json +2 -2
  155. package/templates/develop/README.md +81 -4
  156. package/templates/develop/develop/flow-tests.ts +1 -1
  157. package/templates/develop/develop/interruption-tests.ts +157 -0
  158. package/templates/develop/develop/local.ts +1 -0
  159. package/templates/develop/develop/option-tests.ts +138 -9
  160. package/templates/develop/develop/red-green.ts +55 -0
  161. package/templates/develop/develop/report.ts +31 -11
  162. package/templates/develop/develop/round.ts +1 -0
  163. package/templates/develop/develop/scenarios.ts +27 -0
  164. package/templates/develop/develop/turn.ts +26 -6
  165. package/templates/develop/develop.declaration.json +1 -1
  166. package/templates/develop/develop.test.ts +4 -0
  167. package/templates/develop/template.json +29 -2
@@ -8,16 +8,18 @@ export interface WorkflowGeneration {
8
8
  readonly handler: WorkflowHandler;
9
9
  }
10
10
  export type WorkflowBundleLoader = (directory: string) => Promise<WorkflowGeneration>;
11
- export declare function workflowGenerations(settings: {
12
- readonly directory: string;
13
- readonly repository: string;
11
+ export interface CurrentGeneration {
14
12
  readonly registrations: readonly WorkflowRegistration[];
15
13
  readonly table: readonly {
16
14
  readonly workflowId: string;
17
15
  readonly functionName: string;
18
16
  }[];
19
- readonly workflows: Readonly<Record<string, WorkflowFunction | undefined>>;
17
+ readonly workflows: Readonly<Record<string, unknown>>;
20
18
  readonly handler: WorkflowHandler;
19
+ }
20
+ export declare function workflowGenerations(settings: CurrentGeneration & {
21
+ readonly directory: string;
22
+ readonly repository: string;
21
23
  readonly loadBundle?: WorkflowBundleLoader;
22
24
  }): {
23
25
  resolve: (manifest: ExecutionManifest | undefined) => Promise<{
@@ -25,6 +27,7 @@ export declare function workflowGenerations(settings: {
25
27
  handler: WorkflowHandler;
26
28
  } | undefined>;
27
29
  available: (manifest: ExecutionManifest) => Promise<ExecutionManifest | undefined>;
30
+ replace: (source: CurrentGeneration) => void;
28
31
  };
29
32
  export declare function prepareWorkflowGenerations(ports: {
30
33
  readonly state: HostPorts["state"];
@@ -1,26 +1,31 @@
1
1
  import { join } from "node:path";
2
2
  import { workflowRegistry, } from "../service/registry.js";
3
3
  import { workflowPin } from "../broker/workflow-pin.js";
4
+ function isWorkflowFunction(value) {
5
+ return typeof value === "function";
6
+ }
4
7
  export function workflowGenerations(settings) {
5
- const functions = Object.fromEntries(settings.table.map((row) => [
6
- row.workflowId,
7
- settings.workflows[row.functionName],
8
- ]));
9
- const registry = workflowRegistry(settings.registrations, {
10
- workflowBundles: {
11
- directory: join(settings.directory, "workflow-bundles"),
12
- repository: settings.repository,
13
- },
8
+ const workflowBundles = {
9
+ directory: join(settings.directory, "workflow-bundles"),
10
+ repository: settings.repository,
11
+ };
12
+ const generation = (source) => ({
13
+ functions: new Map(source.table.map((row) => [
14
+ row.workflowId,
15
+ source.workflows[row.functionName],
16
+ ])),
17
+ registry: workflowRegistry(source.registrations, { workflowBundles }),
18
+ handler: source.handler,
14
19
  });
20
+ let current = generation(settings);
15
21
  const resolve = async (manifest) => {
16
22
  if (manifest === undefined)
17
23
  return undefined;
24
+ const { functions, registry, handler } = current;
18
25
  const registered = registry.byPin(workflowPin(manifest));
19
26
  if (registered !== undefined) {
20
- const workflow = functions[registered.workflowId];
21
- return workflow === undefined
22
- ? undefined
23
- : { workflow, handler: settings.handler };
27
+ const workflow = functions.get(registered.workflowId);
28
+ return isWorkflowFunction(workflow) ? { workflow, handler } : undefined;
24
29
  }
25
30
  if (manifest.bundleDigest === undefined ||
26
31
  settings.loadBundle === undefined)
@@ -30,7 +35,7 @@ export function workflowGenerations(settings) {
30
35
  return undefined;
31
36
  try {
32
37
  const generation = await settings.loadBundle(directory);
33
- const workflow = Object.values(generation.workflows).find((candidate) => typeof candidate === "function" &&
38
+ const workflow = Object.values(generation.workflows).find((candidate) => isWorkflowFunction(candidate) &&
34
39
  Reflect.get(candidate, "workflowId") === manifest.workflowId);
35
40
  return workflow === undefined
36
41
  ? undefined
@@ -43,6 +48,9 @@ export function workflowGenerations(settings) {
43
48
  return {
44
49
  resolve,
45
50
  available: async (manifest) => (await resolve(manifest)) === undefined ? undefined : manifest,
51
+ replace: (source) => {
52
+ current = generation(source);
53
+ },
46
54
  };
47
55
  }
48
56
  export async function prepareWorkflowGenerations(ports, generations) {
@@ -0,0 +1,41 @@
1
+ import type { WorkflowReloadPort } from "../service/workflow-reload.ts";
2
+ import { type DeclaredWorkflow } from "../workflow-declaration.ts";
3
+ import type { DaemonSettings } from "./daemon-adapters.ts";
4
+ import type { WorkflowBundleLoader, workflowGenerations } from "./workflow-generations.ts";
5
+ import { type TurnSettingsSource } from "./settings-reload.ts";
6
+ export declare function queueWorkflowId(settings: {
7
+ readonly declarations?: readonly DeclaredWorkflow[];
8
+ }): string | undefined;
9
+ export declare function daemonRegistrations(settings: DaemonSettings): Promise<{
10
+ table: readonly import("./ticket-registration.ts").TicketRegistration[];
11
+ admitted: {
12
+ readonly kind: "rejected";
13
+ readonly detail: string;
14
+ } | {
15
+ registrations: {
16
+ admission?: "when-idle" | number;
17
+ queue?: boolean;
18
+ workflowId: string;
19
+ manifest: import("../workflow-admission.ts").ExecutionManifest;
20
+ availability: "available" | "unavailable";
21
+ submission: import("../submission.ts").SubmissionContract;
22
+ resolver: import("../service/registry.ts").Resolver;
23
+ effectLimit?: number;
24
+ stages?: readonly import("../workflow-declaration.ts").WorkflowStage[];
25
+ handoffs?: readonly string[];
26
+ commands?: readonly string[];
27
+ commandWaits?: import("../workflow-declaration.ts").CommandWaits;
28
+ }[];
29
+ kind: "admitted";
30
+ };
31
+ }>;
32
+ type ReloadSettings = DaemonSettings & {
33
+ readonly reloadCommand: readonly string[];
34
+ readonly loadBundle: WorkflowBundleLoader;
35
+ readonly turnSettings: TurnSettingsSource;
36
+ };
37
+ type Generations = Pick<ReturnType<typeof workflowGenerations>, "replace">;
38
+ export declare function workflowReload(settings: ReloadSettings & {
39
+ readonly generations: Generations;
40
+ }): WorkflowReloadPort;
41
+ export {};
@@ -0,0 +1,244 @@
1
+ import { execFile } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { promisify } from "node:util";
4
+ import { record } from "../protocol.js";
5
+ import { serviceLimits } from "../service/service.js";
6
+ import { isWorkflowDeclaration, } from "../workflow-declaration.js";
7
+ import { workflowRefusals, } from "../workflow-refusals.js";
8
+ import { ticketRegistrations, ticketRegistrationTable, } from "./ticket-registration.js";
9
+ import { scheduledDeclarations } from "./workflow-schedules.js";
10
+ import { keptDigests, profileRefusal, reloadedSettings, settingChanges, } from "./settings-reload.js";
11
+ const executed = promisify(execFile);
12
+ function refused(code, detail) {
13
+ return { kind: "refused", code, detail };
14
+ }
15
+ export function queueWorkflowId(settings) {
16
+ return settings.declarations?.find((declaration) => declaration.queue === true)?.workflowId;
17
+ }
18
+ export async function daemonRegistrations(settings) {
19
+ const conflict = settings.workflowRefusals?.find((refusal) => refusal.codes.includes("queue-workflow-conflict"));
20
+ if (conflict !== undefined)
21
+ return {
22
+ table: [],
23
+ admitted: {
24
+ kind: "rejected",
25
+ detail: `queue-workflow-conflict: ${conflict.scope} ${conflict.label}`,
26
+ },
27
+ };
28
+ const table = ticketRegistrationTable({
29
+ repository: settings.repository,
30
+ command: settings.ticketCommand,
31
+ nextCommand: settings.nextCommand,
32
+ githubCommand: settings.githubCommand,
33
+ }, settings.declarations);
34
+ const admitted = await ticketRegistrations(settings.manifests, {
35
+ table,
36
+ commands: settings.commands ?? {},
37
+ ...(settings.bundleDigest === undefined
38
+ ? {}
39
+ : { bundleDigest: settings.bundleDigest }),
40
+ });
41
+ return {
42
+ table,
43
+ admitted: admitted.kind === "rejected"
44
+ ? admitted
45
+ : {
46
+ ...admitted,
47
+ registrations: admitted.registrations.map((registration) => ({
48
+ ...registration,
49
+ ...(settings.queueMilliseconds !== undefined &&
50
+ registration.workflowId === queueWorkflowId(settings)
51
+ ? { admission: settings.queueLanes ?? 1, queue: true }
52
+ : {}),
53
+ })),
54
+ },
55
+ };
56
+ }
57
+ function declaredWorkflows(value) {
58
+ if (!Array.isArray(value))
59
+ return undefined;
60
+ const declarations = [];
61
+ for (const entry of value) {
62
+ const { functionName, ...declaration } = record(entry) ?? {};
63
+ if (typeof functionName !== "string" || !isWorkflowDeclaration(declaration))
64
+ return undefined;
65
+ declarations.push({ ...declaration, functionName });
66
+ }
67
+ return declarations;
68
+ }
69
+ function manifestPaths(value) {
70
+ const fields = record(value);
71
+ if (fields === undefined)
72
+ return undefined;
73
+ const entries = Object.entries(fields);
74
+ const paths = entries.filter((entry) => typeof entry[1] === "string");
75
+ return paths.length === entries.length
76
+ ? Object.fromEntries(paths)
77
+ : undefined;
78
+ }
79
+ function parsedOutput(output) {
80
+ try {
81
+ return record(JSON.parse(output));
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ }
87
+ function outputRefusal(value) {
88
+ const error = value?.["error"];
89
+ if (typeof error === "string")
90
+ return refused("reload-failed", error);
91
+ const refusal = record(value?.["refusal"]);
92
+ const code = refusal?.["code"];
93
+ const detail = refusal?.["detail"];
94
+ return typeof code === "string" && typeof detail === "string"
95
+ ? refused(code, detail)
96
+ : undefined;
97
+ }
98
+ function preparedBundle(output) {
99
+ const value = parsedOutput(output);
100
+ const refusal = outputRefusal(value);
101
+ if (refusal !== undefined)
102
+ return refusal;
103
+ const bundleDigest = value?.["bundleDigest"];
104
+ const manifests = manifestPaths(value?.["manifests"]);
105
+ const declarations = declaredWorkflows(value?.["declarations"]);
106
+ const settings = reloadedSettings(value);
107
+ if (typeof bundleDigest !== "string" ||
108
+ manifests === undefined ||
109
+ declarations === undefined ||
110
+ settings === undefined)
111
+ return refused("reload-failed", "The reload command printed no workflow bundle.");
112
+ return {
113
+ kind: "prepared",
114
+ bundleDigest,
115
+ manifests,
116
+ declarations,
117
+ workflowRefusals: workflowRefusals(value?.["workflowRefusals"]),
118
+ ...settings,
119
+ };
120
+ }
121
+ async function prepared(command) {
122
+ const [executable = "", ...fixed] = command;
123
+ const output = await executed(executable, fixed, {
124
+ encoding: "utf8",
125
+ maxBuffer: 16 * 1024 * 1024,
126
+ }).then((result) => result.stdout, (error) => String(Reflect.get(Object(error), "stdout")));
127
+ return preparedBundle(output);
128
+ }
129
+ function restartFacts(declarations) {
130
+ const schedules = new Map(declarations
131
+ .filter((declaration) => declaration.schedule !== undefined)
132
+ .map((declaration) => [declaration.workflowId, declaration.schedule]));
133
+ return JSON.stringify({
134
+ queue: queueWorkflowId({ declarations }) ?? null,
135
+ schedules: [...schedules.keys()]
136
+ .sort()
137
+ .map((workflowId) => [workflowId, schedules.get(workflowId)]),
138
+ });
139
+ }
140
+ function refusalName(refusal) {
141
+ return `${refusal.scope} ${refusal.label}`;
142
+ }
143
+ function addedRefusals(current, next) {
144
+ const known = new Set(current.map(refusalName));
145
+ const added = next.filter((refusal) => !known.has(refusalName(refusal)));
146
+ if (added.length === 0)
147
+ return undefined;
148
+ const reasons = added.map((refusal) => `${refusalName(refusal)}: ${refusal.codes.join(", ")}`);
149
+ return refused("reload-failed", `The reload gives new workflow refusals: ${reasons.join("; ")}`);
150
+ }
151
+ async function reloaded(settings, bundle) {
152
+ const scheduled = scheduledDeclarations({ declarations: bundle.declarations, schedules: settings.schedules }, {
153
+ repository: settings.repository,
154
+ documentBytes: serviceLimits.documentBytes,
155
+ });
156
+ if (scheduled.kind === "refused")
157
+ return refused("reload-failed", `${scheduled.reason}: ${scheduled.detail}`);
158
+ if (restartFacts(scheduled.declarations) !==
159
+ restartFacts(settings.declarations ?? []))
160
+ return refused("reload-needs-restart", "The queue workflow or a schedule changed. Restart the service.");
161
+ let generation;
162
+ try {
163
+ generation = await settings.loadBundle(join(settings.directory, "workflow-bundles", bundle.bundleDigest));
164
+ }
165
+ catch (error) {
166
+ return refused("reload-failed", `The new bundle did not load: ${String(error)}`);
167
+ }
168
+ const { table, admitted } = await daemonRegistrations({
169
+ ...settings,
170
+ bundleDigest: bundle.bundleDigest,
171
+ manifests: bundle.manifests,
172
+ declarations: scheduled.declarations,
173
+ workflowRefusals: bundle.workflowRefusals,
174
+ });
175
+ if (admitted.kind === "rejected")
176
+ return refused("reload-failed", admitted.detail);
177
+ settings.generations.replace({
178
+ registrations: admitted.registrations,
179
+ table,
180
+ workflows: generation.workflows,
181
+ handler: generation.handler,
182
+ });
183
+ return {
184
+ kind: "reloaded",
185
+ bundleDigest: bundle.bundleDigest,
186
+ workflows: admitted.registrations,
187
+ workflowRefusals: bundle.workflowRefusals,
188
+ };
189
+ }
190
+ function reloadLine(outcome, digests) {
191
+ if (outcome.kind === "refused")
192
+ return `Workflow reload: refused ${outcome.code}: ${outcome.detail}\n`;
193
+ return `Workflow reload: ${outcome.kind} ${String(digests.previous)} -> ${String(digests.current)}\n`;
194
+ }
195
+ async function workflowPart(settings, prepared) {
196
+ const { state, bundle } = prepared;
197
+ const added = addedRefusals(state.workflowRefusals, bundle.workflowRefusals);
198
+ if (added !== undefined)
199
+ return added;
200
+ if (bundle.bundleDigest === state.bundleDigest)
201
+ return { kind: "unchanged", workflowRefusals: bundle.workflowRefusals };
202
+ return reloaded(settings, bundle);
203
+ }
204
+ async function reloadOutcome(settings, state) {
205
+ const kept = (outcome) => ({ outcome, state });
206
+ const bundle = await prepared(settings.reloadCommand);
207
+ if (bundle.kind === "refused")
208
+ return kept(bundle);
209
+ const profiles = profileRefusal(settings, bundle.turnSettings);
210
+ if (profiles !== undefined)
211
+ return kept(refused(profiles.code, profiles.detail));
212
+ const workflows = await workflowPart(settings, { state, bundle });
213
+ if (workflows.kind === "refused")
214
+ return kept(workflows);
215
+ const changes = settingChanges(state.settingDigests, bundle.settingDigests);
216
+ settings.turnSettings.replace(bundle.turnSettings);
217
+ return {
218
+ outcome: { ...workflows, ...changes },
219
+ state: {
220
+ bundleDigest: workflows.kind === "reloaded"
221
+ ? workflows.bundleDigest
222
+ : state.bundleDigest,
223
+ workflowRefusals: workflows.workflowRefusals,
224
+ settingDigests: keptDigests(state.settingDigests, {
225
+ digests: bundle.settingDigests,
226
+ reloadedFields: changes.reloadedFields,
227
+ }),
228
+ },
229
+ };
230
+ }
231
+ export function workflowReload(settings) {
232
+ let state = {
233
+ bundleDigest: settings.bundleDigest,
234
+ workflowRefusals: workflowRefusals(settings.workflowRefusals),
235
+ settingDigests: settings.settingDigests ?? {},
236
+ };
237
+ return async () => {
238
+ const previous = state.bundleDigest;
239
+ const step = await reloadOutcome(settings, state);
240
+ state = step.state;
241
+ process.stdout.write(reloadLine(step.outcome, { previous, current: state.bundleDigest }));
242
+ return step.outcome;
243
+ };
244
+ }
@@ -4,8 +4,9 @@ interface ReplayRun {
4
4
  readonly runId: string;
5
5
  readonly workflowId?: string;
6
6
  }
7
+ type ReplayState = Pick<AuthorityState, "details" | "runs">;
7
8
  interface ReplayPorts {
8
- readonly state: () => Promise<Pick<AuthorityState, "details">>;
9
+ readonly state: () => Promise<ReplayState>;
9
10
  readonly run: (receipt: string) => Promise<ReplayRun | undefined>;
10
11
  readonly available: (runId: string, workflowId?: string) => Promise<boolean>;
11
12
  readonly submit: (command: Readonly<Record<string, unknown>>) => Promise<{
@@ -18,8 +18,8 @@ async function located(ports, receipt) {
18
18
  return undefined;
19
19
  }
20
20
  async function observedReplay(ports, replay) {
21
- const { runId, receipt, available } = replay;
22
- const observation = (await ports.state()).details[runId]?.observation;
21
+ const { state, runId, receipt, available } = replay;
22
+ const observation = state.details[runId]?.observation;
23
23
  const unavailable = observation?.reason === "pinned-workflow-unavailable";
24
24
  if (available !== unavailable)
25
25
  return true;
@@ -49,7 +49,11 @@ export function pinnedWorkflowHandler(handler, ports) {
49
49
  if (run === undefined)
50
50
  return new Response("unknown-world-run", { status: 503 });
51
51
  const available = await ports.available(run.runId, run.workflowId);
52
- if (!(await observedReplay(ports, { runId: run.runId, receipt, available })))
52
+ const state = await ports.state();
53
+ if (state.runs[run.runId]?.status === "cancelled")
54
+ return Response.json({ ok: true });
55
+ const replay = { state, runId: run.runId, receipt, available };
56
+ if (!(await observedReplay(ports, replay)))
53
57
  return new Response("workflow-observation-unavailable", { status: 503 });
54
58
  if (available)
55
59
  return handler(request, run.runId);
@@ -1,8 +1,8 @@
1
1
  import { type SubmissionContract } from "./submission.ts";
2
- export declare const doctorCodes: readonly ["git-repository", "node-version", "harnesses", "default-harness", "gh-login", "github-origin", "greptile-review", "settings-files", "no-workflow", "runtime-directory", "service", "ticket-command", "next-command", "check-command", "queue-interval", "private-texts"];
2
+ export declare const doctorCodes: readonly ["git-repository", "node-version", "harnesses", "default-harness", "gh-login", "github-origin", "greptile-review", "settings-files", "no-workflow", "runtime-directory", "service", "ticket-command", "next-command", "check-command", "queue-interval", "private-texts", "retention"];
3
3
  export type DoctorCode = (typeof doctorCodes)[number];
4
4
  export interface DoctorObservation {
5
- readonly result: "ok" | "fail" | "skip";
5
+ readonly result: "ok" | "warn" | "fail" | "skip";
6
6
  readonly detail: string;
7
7
  }
8
8
  export interface DoctorCheck extends DoctorObservation {
@@ -16,6 +16,7 @@ export const doctorCodes = [
16
16
  "check-command",
17
17
  "queue-interval",
18
18
  "private-texts",
19
+ "retention",
19
20
  ];
20
21
  export const doctorFixes = {
21
22
  "git-repository": "Change to the repository, or run: git init",
@@ -34,6 +35,7 @@ export const doctorFixes = {
34
35
  "check-command": "Set checks.beforeMerge to an executable list: ${EDITOR:-vi} .cloving/config.json",
35
36
  "queue-interval": "Set an interval from 10 to 3600: CLOVING_QUEUE_SECONDS=30 cloving doctor",
36
37
  "private-texts": `Set a nonblank private text: CLOVING_PRIVATE_TEXTS='["TEXT TO KEEP PRIVATE"]' cloving doctor`,
38
+ retention: "Set a retention period: cloving config set retention.days 30 --private",
37
39
  };
38
40
  export function doctorExitCode(checks) {
39
41
  return checks.some((check) => check.required && check.result !== "ok")
@@ -294,6 +294,37 @@
294
294
  }
295
295
  },
296
296
  "additionalProperties": false
297
+ },
298
+ "retention": {
299
+ "type": "object",
300
+ "properties": {
301
+ "days": {
302
+ "anyOf": [
303
+ {
304
+ "type": "integer",
305
+ "minimum": 1
306
+ },
307
+ {
308
+ "const": null
309
+ }
310
+ ]
311
+ },
312
+ "action": {
313
+ "enum": ["archive", "delete"]
314
+ },
315
+ "archiveFolder": {
316
+ "anyOf": [
317
+ {
318
+ "type": "string",
319
+ "pattern": "^/"
320
+ },
321
+ {
322
+ "const": null
323
+ }
324
+ ]
325
+ }
326
+ },
327
+ "additionalProperties": false
297
328
  }
298
329
  },
299
330
  "additionalProperties": false,
@@ -2,7 +2,13 @@ import { type Configuration, type RedGreenSettings } from "./configuration-schem
2
2
  import type { TurnProfiles } from "./turn-profile.ts";
3
3
  import type { TokenPrices } from "./token-prices.ts";
4
4
  export type SettingSource = "environment" | "private" | "project";
5
- export type SettingValue = string | number | readonly string[] | TurnProfiles | TokenPrices | RedGreenSettings;
5
+ export declare const reloadedSettingFields: readonly string[];
6
+ export interface TurnSettings {
7
+ readonly profiles: TurnProfiles;
8
+ readonly fallbackModels: readonly string[];
9
+ readonly tokenPrices: TokenPrices;
10
+ }
11
+ export type SettingValue = null | string | number | readonly string[] | TurnProfiles | TokenPrices | RedGreenSettings;
6
12
  export declare const settingVariables: Readonly<Record<string, string>>;
7
13
  export declare function absoluteWorkflowFolder(folder: string | undefined): string | undefined;
8
14
  export declare function configurationRoot(environment: Readonly<NodeJS.ProcessEnv>, home: string): string;
@@ -16,4 +22,12 @@ export declare function resolveSettings(environment: Readonly<NodeJS.ProcessEnv>
16
22
  readonly profiles: TurnProfiles;
17
23
  readonly harness?: Configuration["harness"];
18
24
  readonly sources: Readonly<Record<string, SettingSource>>;
25
+ readonly values: Readonly<Record<string, SettingValue>>;
19
26
  };
27
+ interface ResolvedFields {
28
+ readonly environment: Readonly<NodeJS.ProcessEnv>;
29
+ readonly values: Readonly<Record<string, SettingValue>>;
30
+ readonly profiles: TurnProfiles;
31
+ }
32
+ export declare function settingDigests(resolved: ResolvedFields, digest: (text: string) => string): Readonly<Record<string, string>>;
33
+ export {};
@@ -1,4 +1,10 @@
1
- import { configuration, } from "./configuration-schema.js";
1
+ import { configuration, settingFields, } from "./configuration-schema.js";
2
+ export const reloadedSettingFields = [
3
+ "fallbackModels",
4
+ "profiles",
5
+ "tokenPrices",
6
+ "workflows.folder",
7
+ ];
2
8
  export const settingVariables = {
3
9
  "checks.beforeMerge": "CLOVING_CHECK_COMMAND",
4
10
  "checks.required": "CLOVING_REQUIRED_CHECKS",
@@ -18,6 +24,9 @@ export const settingVariables = {
18
24
  "selfUpdate.everySeconds": "CLOVING_SELF_UPDATE_SECONDS",
19
25
  "github.command": "CLOVING_GH_COMMAND",
20
26
  "workflows.folder": "CLOVING_WORKFLOW_FOLDER",
27
+ "retention.days": "CLOVING_RETENTION_DAYS",
28
+ "retention.action": "CLOVING_RETENTION_ACTION",
29
+ "retention.archiveFolder": "CLOVING_RETENTION_ARCHIVE_FOLDER",
21
30
  };
22
31
  const environmentOnly = [
23
32
  "CLOVING_RUNTIME_DIR",
@@ -65,6 +74,7 @@ export function configurationValues(values) {
65
74
  selfUpdate: values.selfUpdate,
66
75
  github: values.github,
67
76
  workflows: values.workflows,
77
+ retention: values.retention,
68
78
  };
69
79
  for (const [name, group] of Object.entries(groups)) {
70
80
  if (group === undefined)
@@ -126,8 +136,30 @@ export function resolveSettings(environment, files) {
126
136
  environment: resolved,
127
137
  profiles,
128
138
  sources,
139
+ values,
129
140
  ...(privateConfiguration.harness === undefined
130
141
  ? {}
131
142
  : { harness: privateConfiguration.harness }),
132
143
  };
133
144
  }
145
+ function resolvedValue(resolved, field) {
146
+ if (field === "profiles")
147
+ return resolved.profiles;
148
+ const variable = resolved.variables[field];
149
+ return variable === undefined
150
+ ? resolved.values[field]
151
+ : resolved.environment[variable];
152
+ }
153
+ export function settingDigests(resolved, digest) {
154
+ const fields = {
155
+ ...resolved,
156
+ variables: {
157
+ ...settingVariables,
158
+ ...harnessCommandMapping(resolved.values["harness.name"]),
159
+ },
160
+ };
161
+ return Object.fromEntries(settingFields().map((field) => [
162
+ field,
163
+ digest(JSON.stringify(resolvedValue(fields, field) ?? null)),
164
+ ]));
165
+ }
@@ -0,0 +1,26 @@
1
+ import type { Fields } from "./protocol.ts";
2
+ export declare const queueFailureLimit = 3;
3
+ export interface QueueDrain {
4
+ readonly startedAt: number;
5
+ readonly drainedAt: number | null;
6
+ }
7
+ export interface QueueControl {
8
+ readonly failedRunIds: readonly string[];
9
+ readonly drain: QueueDrain | undefined;
10
+ readonly error: string | undefined;
11
+ }
12
+ export type QueueCommand = "resume" | "drain";
13
+ export type QueueChange = {
14
+ readonly kind: "changed" | "replayed" | "conflicting-command";
15
+ } | {
16
+ readonly kind: "unavailable";
17
+ readonly detail: string;
18
+ };
19
+ export interface QueuePort {
20
+ readonly control: () => QueueControl | undefined;
21
+ readonly change: (request: {
22
+ readonly command: QueueCommand;
23
+ readonly commandId: string;
24
+ }) => Promise<QueueChange>;
25
+ }
26
+ export declare function queueStateFields(control: QueueControl, busy: number): Fields;
@@ -0,0 +1,34 @@
1
+ export const queueFailureLimit = 3;
2
+ function stopReason(control) {
3
+ if (control.error !== undefined)
4
+ return control.error;
5
+ if (control.failedRunIds.length >= queueFailureLimit)
6
+ return "failure-limit";
7
+ return undefined;
8
+ }
9
+ function drainState(drain, busy) {
10
+ return drain.drainedAt === null || busy > 0 ? "draining" : "drained";
11
+ }
12
+ function stateName(control, busy) {
13
+ if (control.error !== undefined)
14
+ return "stopped";
15
+ if (control.drain !== undefined)
16
+ return drainState(control.drain, busy);
17
+ if (stopReason(control) !== undefined)
18
+ return "stopped";
19
+ return "running";
20
+ }
21
+ export function queueStateFields(control, busy) {
22
+ const reason = stopReason(control);
23
+ return {
24
+ state: stateName(control, busy),
25
+ stop: reason === undefined ? null : { reason },
26
+ failedRunIds: [...control.failedRunIds],
27
+ drain: control.drain === undefined
28
+ ? null
29
+ : {
30
+ startedAt: control.drain.startedAt,
31
+ drainedAt: control.drain.drainedAt,
32
+ },
33
+ };
34
+ }