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
@@ -19,4 +19,5 @@ export declare function Queue(props: {
19
19
  export declare function Brand(): Shown;
20
20
  export declare function HeaderStatus(props: HeaderProps): Shown;
21
21
  export declare function HeaderActions(props: HeaderProps): Shown;
22
+ export declare function Version(props: HeaderProps): Shown;
22
23
  export declare function Header(props: HeaderProps): Shown;
@@ -16,7 +16,8 @@ export function queueLanes(model) {
16
16
  const queue = model.queue;
17
17
  if (queue === undefined)
18
18
  return null;
19
- return `Queue: ${String(queue["busy"])} of ${String(queue["lanes"])} lanes busy`;
19
+ const state = typeof queue["state"] === "string" ? `${queue["state"]}, ` : "";
20
+ return `Queue: ${state}${String(queue["busy"])} of ${String(queue["lanes"])} lanes busy`;
20
21
  }
21
22
  export function skippedText(value) {
22
23
  const ticket = record(value) ?? {};
@@ -93,12 +94,19 @@ export function HeaderActions(props) {
93
94
  }),
94
95
  });
95
96
  }
97
+ export function Version(props) {
98
+ const version = props.model?.serviceVersion;
99
+ if (version === undefined)
100
+ return null;
101
+ return element("div", { class: "version", children: `cloving ${version}` });
102
+ }
96
103
  export function Header(props) {
97
104
  return element("header", {
98
105
  children: [
99
106
  component(Brand, {}),
100
107
  component(HeaderStatus, props),
101
108
  component(HeaderActions, props),
109
+ component(Version, props),
102
110
  ],
103
111
  });
104
112
  }
@@ -29,6 +29,7 @@ export interface WatchInputs {
29
29
  readonly client: string;
30
30
  readonly updatedAt: string;
31
31
  readonly service: string;
32
+ readonly serviceVersion?: string;
32
33
  readonly runs: readonly unknown[];
33
34
  readonly completionCandidates?: readonly unknown[];
34
35
  readonly decisions: readonly unknown[];
@@ -269,6 +269,9 @@ export function watchModel(inputs) {
269
269
  startWorkflowId: startWorkflowOf(workflows),
270
270
  updatedAt: inputs.updatedAt,
271
271
  service: inputs.service,
272
+ ...(inputs.serviceVersion === undefined
273
+ ? {}
274
+ : { serviceVersion: inputs.serviceVersion }),
272
275
  runs: runs.toSorted((first, second) => began(first) < began(second) ? 1 : -1),
273
276
  };
274
277
  }
@@ -15,7 +15,7 @@ import { boardWorkflow, watchHash, watchLocation, watchTitle, } from "./watch-na
15
15
  import { clockTime, completedDetail, counted, elapsed, groupActivity, pageTitle, phaseTime, runName, runTone, selectedRun, toneOf, turnTime, waitingDetail, } from "./watch-render.js";
16
16
  import { Command, Decision, Events, Group, Item, Turn, Work, } from "./watch-turn.js";
17
17
  import { Banner, BannerDetail, BoardPage, Page, Phases, PullRequest, Review, RefusedWorkflows, Run, } from "./watch-view.js";
18
- import { Brand, clove, Header, HeaderActions, HeaderStatus, Queue, queueLanes, skippedText, } from "./watch-header-view.js";
18
+ import { Brand, clove, Header, HeaderActions, HeaderStatus, Queue, queueLanes, skippedText, Version, } from "./watch-header-view.js";
19
19
  const functions = [
20
20
  scheduleStatusLine,
21
21
  scheduledText,
@@ -32,6 +32,7 @@ const functions = [
32
32
  Brand,
33
33
  HeaderStatus,
34
34
  HeaderActions,
35
+ Version,
35
36
  cardReply,
36
37
  cardSubmitKey,
37
38
  cardKey,
@@ -284,6 +285,7 @@ button:hover { background: var(--code-head); }
284
285
  .events { margin-top: 18px; padding-bottom: 10px; }
285
286
  .events > summary { color: var(--code-fg); }
286
287
  .down { font-weight: 600; }
288
+ .version { position: fixed; left: 12px; bottom: 8px; font: 12px/1.4 var(--code-font); color: var(--muted); pointer-events: none; }
287
289
  @media (max-width: 700px) { body { padding-inline: 12px; } }
288
290
  `;
289
291
  export function pageDocument(actionToken = null) {
@@ -20,6 +20,7 @@ export interface WatchRead {
20
20
  readonly schedules?: ShownSchedules;
21
21
  readonly queue?: Fields;
22
22
  readonly service: string;
23
+ readonly serviceVersion?: string;
23
24
  readonly workflows: readonly string[];
24
25
  readonly workflowRefusals: readonly WorkflowRefusal[];
25
26
  readonly workflowStages?: WorkflowStages;
@@ -1,6 +1,6 @@
1
1
  import { establishedIssue } from "./watch-pr-identity.js";
2
2
  import { shownSchedules } from "./schedule-render.js";
3
- import { isWorkflowStages, } from "../workflow-declaration.js";
3
+ import { workflowStagesOf, } from "../workflow-declaration.js";
4
4
  import { record } from "../protocol.js";
5
5
  import { send } from "./client.js";
6
6
  import { activeRun, recentRun } from "./watch-board.js";
@@ -192,8 +192,8 @@ function stoppedIssues(runs, context) {
192
192
  : [];
193
193
  }));
194
194
  }
195
- function stageMetadata(value) {
196
- return Object.fromEntries(Object.entries(record(value) ?? {}).flatMap(([workflowId, stages]) => isWorkflowStages(stages) ? [[workflowId, stages]] : []));
195
+ function versionOf(value) {
196
+ return typeof value === "string" ? { serviceVersion: value } : {};
197
197
  }
198
198
  async function service(reader, now) {
199
199
  const status = await asked(reader, {
@@ -226,7 +226,8 @@ async function service(reader, now) {
226
226
  ? {}
227
227
  : { queue: record(status["queue"]) ?? {} }),
228
228
  workflows: list(status["workflows"]).filter((value) => typeof value === "string"),
229
- workflowStages: stageMetadata(status["workflowStages"]),
229
+ workflowStages: workflowStagesOf(status["workflowStages"]),
230
+ ...versionOf(status["serviceVersion"]),
230
231
  workflowRefusals: workflowRefusals(status["workflowRefusals"]),
231
232
  summaries,
232
233
  runs,
@@ -252,7 +253,7 @@ async function singleRun(reader, runId) {
252
253
  return {
253
254
  ...unavailable,
254
255
  service: "up",
255
- workflowStages: stageMetadata(metadata["workflowStages"]),
256
+ workflowStages: workflowStagesOf(metadata["workflowStages"]),
256
257
  runs: [value],
257
258
  decisions: await decisionsOf(reader),
258
259
  };
@@ -153,6 +153,7 @@ export interface WatchRun {
153
153
  readonly mergeCommit: string | null;
154
154
  }
155
155
  export interface WatchModel {
156
+ readonly serviceVersion?: string;
156
157
  readonly schedules?: ShownSchedules;
157
158
  readonly queue?: Fields;
158
159
  readonly workflowRefusals?: readonly WorkflowRefusal[];
@@ -1,4 +1,5 @@
1
1
  import { watchAction, watchActions, watchHost, } from "./watch-actions.js";
2
+ import { watchApiChange, watchApiReply } from "./watch-api.js";
2
3
  import { observedBoard } from "./watch-pr-board.js";
3
4
  import { isPhoneAction, phoneAction, phoneRead } from "./watch-phone.js";
4
5
  import { watchPrObserver } from "./watch-pr-settings.js";
@@ -30,6 +31,7 @@ const fontFiles = new Map([
30
31
  new URL(`assets/watch-fonts/${name}`, import.meta.resolve("#package")),
31
32
  ]));
32
33
  const libraryFile = fileURLToPath(import.meta.resolve("preact"));
34
+ const pageChange = () => "data: change\n\n";
33
35
  function parsed(text) {
34
36
  try {
35
37
  const value = JSON.parse(text);
@@ -139,6 +141,14 @@ async function answered(watch, request) {
139
141
  if (font !== null)
140
142
  return font;
141
143
  const url = new URL(String(request.url), "http://localhost");
144
+ const api = await watchApiReply({
145
+ method: request.method ?? "",
146
+ url,
147
+ model: () => snapshot(watch, null),
148
+ run: (runId) => snapshot(watch, runId),
149
+ });
150
+ if (api !== null)
151
+ return api;
142
152
  if (request.method === "GET")
143
153
  return readReply(watch, url);
144
154
  if (request.method !== "POST" || watch.actions === null)
@@ -187,13 +197,14 @@ function replied(response, reply) {
187
197
  response.writeHead(reply.status, {
188
198
  "content-type": reply.type,
189
199
  "cache-control": reply.cache ?? "no-store",
200
+ ...(reply.allow === undefined ? {} : { allow: reply.allow }),
190
201
  });
191
202
  response.end(reply.body);
192
203
  }
193
204
  function subscribed(stream, exchange) {
194
- const { request, response } = exchange;
205
+ const { request, response, message } = exchange;
195
206
  const changed = () => {
196
- response.write("data: change\n\n");
207
+ response.write(message());
197
208
  };
198
209
  response.writeHead(200, {
199
210
  "content-type": "text/event-stream",
@@ -223,11 +234,14 @@ export async function watchServer(settings) {
223
234
  if (watchHost(request) &&
224
235
  request.method === "GET" &&
225
236
  request.url === "/events") {
226
- subscribed(stream, { request, response });
237
+ subscribed(stream, { request, response, message: pageChange });
227
238
  return;
228
239
  }
229
240
  answered(watch, request).then((reply) => {
230
- replied(response, reply);
241
+ if (reply === "stream")
242
+ subscribed(stream, { request, response, message: watchApiChange });
243
+ else
244
+ replied(response, reply);
231
245
  }, () => {
232
246
  replied(response, { ...notFound, status: 500, body: "failed" });
233
247
  });
package/dist/src/cli.js CHANGED
@@ -4,7 +4,7 @@ import { terminateHarness } from "./cli/harness-processes.js";
4
4
  import { settingsEnvironment } from "./cli/project-settings.js";
5
5
  import { constants as systemConstants } from "node:os";
6
6
  import { addAbortListener } from "node:events";
7
- import { execFile, execFileSync, spawn } from "node:child_process";
7
+ import { execFile, execFileSync, spawn, } from "node:child_process";
8
8
  import { existsSync, realpathSync } from "node:fs";
9
9
  import { parseArgs } from "node:util";
10
10
  import { delimiter, dirname, extname, join, resolve } from "node:path";
@@ -60,43 +60,50 @@ const watchCommand = async (request) => {
60
60
  listener[Symbol.dispose]();
61
61
  }
62
62
  };
63
+ function spawnedHarness(request) {
64
+ try {
65
+ return spawn(request.executable, request.arguments, {
66
+ cwd: request.directory,
67
+ env: request.environment,
68
+ stdio: "inherit",
69
+ });
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ }
63
75
  function startHarness(request) {
64
76
  return new Promise((settle) => {
65
- try {
66
- const child = spawn(request.executable, request.arguments, {
67
- cwd: request.directory,
68
- env: request.environment,
69
- stdio: "inherit",
70
- });
71
- const interrupt = () => undefined;
72
- const terminate = () => {
73
- terminateHarness(child.pid, {
74
- snapshot: () => execFileSync("/bin/ps", ["-axo", "pid=,ppid="], {
75
- encoding: "utf8",
76
- timeout: 1000,
77
- }),
78
- kill: (identifier, signal) => process.kill(identifier, signal),
79
- });
80
- };
81
- const cleanup = () => {
82
- process.off("SIGINT", interrupt);
83
- process.off("SIGTERM", terminate);
84
- };
85
- process.on("SIGINT", interrupt);
86
- process.on("SIGTERM", terminate);
87
- child.once("error", () => {
88
- cleanup();
89
- settle(null);
77
+ const interrupt = () => undefined;
78
+ const terminate = () => {
79
+ terminateHarness(child?.pid, {
80
+ snapshot: () => execFileSync("/bin/ps", ["-axo", "pid=,ppid="], {
81
+ encoding: "utf8",
82
+ timeout: 1000,
83
+ }),
84
+ kill: (identifier, signal) => process.kill(identifier, signal),
90
85
  });
91
- child.once("close", (status, signal) => {
92
- cleanup();
93
- settle(status ??
94
- (signal === null ? 1 : 128 + systemConstants.signals[signal]));
95
- });
96
- }
97
- catch {
86
+ };
87
+ const cleanup = () => {
88
+ process.off("SIGINT", interrupt);
89
+ process.off("SIGTERM", terminate);
90
+ };
91
+ process.on("SIGINT", interrupt);
92
+ process.on("SIGTERM", terminate);
93
+ const child = spawnedHarness(request);
94
+ if (child === undefined) {
95
+ cleanup();
98
96
  settle(null);
97
+ return;
99
98
  }
99
+ child.once("error", () => {
100
+ cleanup();
101
+ settle(null);
102
+ });
103
+ child.once("close", (status, signal) => {
104
+ cleanup();
105
+ settle(status ?? (signal === null ? 1 : 128 + systemConstants.signals[signal]));
106
+ });
100
107
  });
101
108
  }
102
109
  function startArguments(input) {
@@ -122,16 +129,6 @@ function startArguments(input) {
122
129
  }
123
130
  function toolProcess(input) {
124
131
  const program = join(import.meta.dirname, "..", "tools", `${input.tool}${extname(import.meta.filename)}`);
125
- const child = spawn(process.execPath, [program, ...input.argumentsList], {
126
- detached: input.tool === "red-green-command",
127
- stdio: input.tool === "red-green-command"
128
- ? ["inherit", "inherit", "inherit", "ipc"]
129
- : "inherit",
130
- env: {
131
- ...process.env,
132
- PATH: [dirname(process.execPath), process.env["PATH"]].join(delimiter),
133
- },
134
- });
135
132
  let stopping = false;
136
133
  const interrupt = () => {
137
134
  stopping = true;
@@ -143,6 +140,16 @@ function toolProcess(input) {
143
140
  };
144
141
  process.on("SIGINT", interrupt);
145
142
  process.on("SIGTERM", terminate);
143
+ const child = spawn(process.execPath, [program, ...input.argumentsList], {
144
+ detached: input.tool === "red-green-command",
145
+ stdio: input.tool === "red-green-command"
146
+ ? ["inherit", "inherit", "inherit", "ipc"]
147
+ : "inherit",
148
+ env: {
149
+ ...process.env,
150
+ PATH: [dirname(process.execPath), process.env["PATH"]].join(delimiter),
151
+ },
152
+ });
146
153
  return new Promise((settle) => {
147
154
  child.once("close", (status) => {
148
155
  setImmediate(() => {
@@ -13,6 +13,8 @@ const defaults = {
13
13
  "watch.port": 3663,
14
14
  "watch.actions": "on",
15
15
  "github.command": ["gh"],
16
+ "retention.days": 30,
17
+ "retention.action": "archive",
16
18
  };
17
19
  function effectiveValue(setting) {
18
20
  const { field, value } = setting;
@@ -51,6 +51,11 @@ export interface Configuration {
51
51
  readonly workflows?: {
52
52
  readonly folder?: string;
53
53
  };
54
+ readonly retention?: {
55
+ readonly days?: number | null;
56
+ readonly action?: "archive" | "delete";
57
+ readonly archiveFolder?: string | null;
58
+ };
54
59
  }
55
60
  export declare function configuration(value: unknown, scope: "project" | "private"): Configuration;
56
61
  export declare function settingScope(field: string): "project" | "private";
@@ -78,9 +78,19 @@ export function recordedTree(started) {
78
78
  }
79
79
  export async function resumePlan(settings, request) {
80
80
  const { folder, started, intent, signal } = request;
81
- const resume = await readResume(folder, started);
82
81
  const tree = recordedTree(started);
83
- if (resume === undefined || tree === undefined)
82
+ if (tree === undefined)
83
+ return "claude-turn-interrupted";
84
+ const locked = await resumeWorktree({
85
+ ...settings,
86
+ directory: tree.path,
87
+ folder,
88
+ signal,
89
+ });
90
+ if (locked !== null)
91
+ return locked.reason;
92
+ const resume = await readResume(folder, started);
93
+ if (resume === undefined)
84
94
  return "claude-turn-interrupted";
85
95
  const profile = await selectedProfile(settings, intent);
86
96
  if (typeof profile === "string")
@@ -93,13 +103,7 @@ export async function resumePlan(settings, request) {
93
103
  sessionId: resume.sessionId,
94
104
  }) === undefined)
95
105
  return "claude-turn-interrupted";
96
- const locked = await resumeWorktree({
97
- ...settings,
98
- directory: tree.path,
99
- folder,
100
- signal,
101
- });
102
- return locked === null ? { resume, tree, profile, harness } : locked.reason;
106
+ return { resume, tree, profile, harness };
103
107
  }
104
108
  function priorEnd(record) {
105
109
  const field = (name) => recordedField(record, name);
@@ -1,10 +1,10 @@
1
1
  import { type HarnessSettings } from "../harness-defaults.ts";
2
- import { type TurnProfiles } from "../turn-profile.ts";
3
2
  import type { Delivery, DeliveryIntent } from "../broker/actor/delivery-intents.ts";
4
3
  import { type RetrySettings } from "./retrying-turn.ts";
5
4
  import { type WorkerReport } from "./operation-journal.ts";
6
- export interface ClaudeWorkerSettings extends RetrySettings, HarnessSettings {
7
- readonly profiles?: TurnProfiles;
5
+ import type { TurnSettings } from "../project-settings.ts";
6
+ export interface ClaudeWorkerSettings extends Pick<RetrySettings, "retryDelay">, HarnessSettings {
7
+ readonly turnSettings: () => TurnSettings;
8
8
  readonly workflowId?: (runId: string) => Promise<string | undefined>;
9
9
  readonly repository: string;
10
10
  readonly worktrees: string;
@@ -1,13 +1,13 @@
1
1
  import { turnHarness } from "../harness-defaults.js";
2
2
  import { attachmentEnvironment } from "./attachments.js";
3
- import { profileCommand, } from "../turn-profile.js";
3
+ import { profileCommand } from "../turn-profile.js";
4
4
  import {} from "./claude-turn.js";
5
5
  import { turnOutcome } from "./turn-report.js";
6
6
  import { startRetriedTurn } from "./retrying-turn.js";
7
7
  import { located, readRecord as read, replied, reportOf, writeRecord as written, } from "./operation-journal.js";
8
8
  import { firstTurn, treeFailure, workTree, } from "./work-tree.js";
9
9
  import { recordedHarness, interruptedReply, processAlive, uncertainProcess, resumePlan, recordedField, resumableHarness, recoverTurn, turnIntent, } from "./claude-recovery.js";
10
- import { selectedProfile } from "./worker-profile.js";
10
+ import { checkedProfiles, selectedProfile } from "./worker-profile.js";
11
11
  export const claudeLimits = {
12
12
  summaryCharacters: 16_000,
13
13
  resultBytes: 48_000,
@@ -51,12 +51,8 @@ async function resumed(parts, request) {
51
51
  return interruptedReply(request, "claude-turn-interrupted");
52
52
  if (aborted())
53
53
  return failure("claude-start-aborted");
54
- const plan = await resumePlan(parts.settings, {
55
- folder,
56
- started,
57
- intent,
58
- signal,
59
- });
54
+ const current = parts.settings.turnSettings();
55
+ const plan = await resumePlan({ ...parts.settings, ...current }, { folder, started, intent, signal });
60
56
  if (aborted())
61
57
  return failure("claude-start-aborted");
62
58
  if (typeof plan === "string")
@@ -73,6 +69,7 @@ async function resumed(parts, request) {
73
69
  ...plan,
74
70
  signal,
75
71
  prompt,
72
+ turnSettings: current,
76
73
  });
77
74
  return { kind: "accepted", receipt: key, worker };
78
75
  }
@@ -105,6 +102,7 @@ async function begun(parts, start) {
105
102
  prompt,
106
103
  }, {
107
104
  ...parts.settings,
105
+ ...start.turnSettings,
108
106
  ...(start.resume === undefined ? {} : { resume: start.resume }),
109
107
  stopping: stopping.controller,
110
108
  metadata: {
@@ -143,7 +141,8 @@ async function delivered(parts, call) {
143
141
  const known = await reconciled(parts, call);
144
142
  if (known.kind !== "absent")
145
143
  return known;
146
- const profile = await selectedProfile(settings, intent);
144
+ const current = settings.turnSettings();
145
+ const profile = await selectedProfile({ ...settings, ...current }, intent);
147
146
  if (typeof profile === "string")
148
147
  return failure(profile);
149
148
  const prompt = await settings.document(intent.payload);
@@ -171,6 +170,7 @@ async function delivered(parts, call) {
171
170
  profile,
172
171
  harness: turnHarness(settings, profile),
173
172
  signal,
173
+ turnSettings: current,
174
174
  });
175
175
  return { receipt: place.key, worker };
176
176
  }
@@ -225,8 +225,7 @@ export function claudeWorker(settings) {
225
225
  if (settings.command.length === 0)
226
226
  throw new Error("invalid-claude-command");
227
227
  turnHarness(settings);
228
- for (const profile of Object.values(settings.profiles ?? {}))
229
- turnHarness(settings, profile);
228
+ checkedProfiles(settings, settings.turnSettings().profiles);
230
229
  const parts = {
231
230
  settings,
232
231
  active: new Map(),
@@ -12,12 +12,15 @@ import type { LocalCommands } from "../command-settings.ts";
12
12
  import type { DeclaredWorkflow } from "../workflow-declaration.ts";
13
13
  import type { WorkflowRefusal } from "../workflow-refusals.ts";
14
14
  import type { HarnessSettings } from "../harness-defaults.ts";
15
+ import type { RetentionSettings } from "../record-retention.ts";
16
+ import { type SettingDigests, type TurnSettingsSource } from "./settings-reload.ts";
15
17
  export interface DaemonSettings extends HarnessSettings {
16
18
  readonly commands?: LocalCommands;
17
19
  readonly commandSeconds?: number;
18
20
  readonly fallbackModels?: readonly string[];
19
21
  readonly tokenPrices?: TokenPrices;
20
22
  readonly profiles?: TurnProfiles;
23
+ readonly settingDigests?: SettingDigests;
21
24
  readonly directory: string;
22
25
  readonly runtimeDirectory: string;
23
26
  readonly repository: string;
@@ -28,6 +31,7 @@ export interface DaemonSettings extends HarnessSettings {
28
31
  readonly workflows: Readonly<Record<string, WorkflowFunction | undefined>>;
29
32
  readonly handler: WorkflowHandler;
30
33
  readonly loadBundle?: WorkflowBundleLoader;
34
+ readonly reloadCommand?: readonly string[];
31
35
  readonly command: readonly string[];
32
36
  readonly ticketCommand?: readonly string[];
33
37
  readonly nextCommand?: readonly string[];
@@ -35,6 +39,8 @@ export interface DaemonSettings extends HarnessSettings {
35
39
  readonly notifyMilliseconds?: number;
36
40
  readonly queueMilliseconds?: number;
37
41
  readonly scheduleClock?: () => number;
42
+ readonly retention?: RetentionSettings;
43
+ readonly retentionClock?: () => number;
38
44
  readonly schedules?: unknown;
39
45
  readonly queueLanes?: number;
40
46
  readonly queueDailySpendLimitUsd?: number;
@@ -47,6 +53,7 @@ export interface DaemonSettings extends HarnessSettings {
47
53
  }
48
54
  export interface Adapters {
49
55
  readonly worker: ClaudeWorker;
56
+ readonly turnSettings: TurnSettingsSource;
50
57
  readonly github: GithubWorker;
51
58
  readonly runs: StartDelivery;
52
59
  readonly handoff: EffectDelivery;
@@ -7,6 +7,7 @@ import { githubWorker } from "./github-worker.js";
7
7
  import { commandWorker } from "./command-worker.js";
8
8
  import { handoffWorker } from "./handoff-worker.js";
9
9
  import { resumableHarness } from "./claude-recovery.js";
10
+ import { turnSettingsSource, } from "./settings-reload.js";
10
11
  function githubOf(settings, given) {
11
12
  const { directory, repository, checkCommand } = settings;
12
13
  return githubWorker({
@@ -42,11 +43,14 @@ export function adapters(settings, options) {
42
43
  const { given, generations } = options;
43
44
  const document = async (digest) => (await given).document(digest);
44
45
  const report = async (effect) => (await given).reportEffect(effect);
45
- const worker = claudeWorker({
46
+ const turnSettings = turnSettingsSource({
47
+ profiles: settings.profiles ?? {},
46
48
  fallbackModels: settings.fallbackModels ?? [],
47
49
  tokenPrices: settings.tokenPrices ?? {},
50
+ });
51
+ const worker = claudeWorker({
48
52
  ...workerHarness(settings),
49
- profiles: settings.profiles ?? {},
53
+ turnSettings: turnSettings.current,
50
54
  workflowId: async (runId) => (await given).workflow.id(runId),
51
55
  repository,
52
56
  document,
@@ -72,7 +76,7 @@ export function adapters(settings, options) {
72
76
  document,
73
77
  report,
74
78
  });
75
- return { worker, github, runs, handoff, commands };
79
+ return { worker, github, runs, handoff, commands, turnSettings };
76
80
  }
77
81
  function handoffOf(settings, given) {
78
82
  return handoffWorker({
@@ -1,7 +1,9 @@
1
1
  import type { scheduleConnection } from "./schedule-control.ts";
2
+ import type { queueConnection } from "./queue-control.ts";
2
3
  import { workflowSchedules } from "./workflow-schedules.ts";
3
4
  interface LoopSettings extends ParametersOfSchedules {
4
5
  readonly scheduleConnection?: ReturnType<typeof scheduleConnection>;
6
+ readonly queueConnection?: ReturnType<typeof queueConnection>;
5
7
  readonly notifyCommand?: readonly string[];
6
8
  readonly notifyMilliseconds?: number;
7
9
  readonly queueMilliseconds?: number;
@@ -1,6 +1,22 @@
1
1
  import { workflowSchedules } from "./workflow-schedules.js";
2
2
  import { issueQueue } from "./issue-queue.js";
3
3
  import { notifyRuns } from "./notify.js";
4
+ async function startedQueue(settings) {
5
+ if (settings.queueMilliseconds === undefined ||
6
+ settings.queueWorkflowId === undefined)
7
+ return undefined;
8
+ const queue = issueQueue({
9
+ directory: settings.directory,
10
+ runtimeDirectory: settings.runtimeDirectory,
11
+ intervalMilliseconds: settings.queueMilliseconds,
12
+ lanes: settings.queueLanes ?? 1,
13
+ resetQueue: settings.resetQueue ?? false,
14
+ workflowId: settings.queueWorkflowId,
15
+ });
16
+ settings.queueConnection?.connect(queue.port);
17
+ await queue.ready;
18
+ return queue;
19
+ }
4
20
  export async function daemonLoops(settings) {
5
21
  const schedules = await workflowSchedules(settings);
6
22
  settings.scheduleConnection?.connect(schedules.request);
@@ -15,17 +31,7 @@ export async function daemonLoops(settings) {
15
31
  ? {}
16
32
  : { intervalMilliseconds: settings.notifyMilliseconds }),
17
33
  });
18
- const queue = settings.queueMilliseconds === undefined ||
19
- settings.queueWorkflowId === undefined
20
- ? undefined
21
- : issueQueue({
22
- directory: settings.directory,
23
- runtimeDirectory: settings.runtimeDirectory,
24
- intervalMilliseconds: settings.queueMilliseconds,
25
- lanes: settings.queueLanes ?? 1,
26
- resetQueue: settings.resetQueue ?? false,
27
- workflowId: settings.queueWorkflowId,
28
- });
34
+ const queue = await startedQueue(settings);
29
35
  return {
30
36
  stop: async () => {
31
37
  await schedules.stop();