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
@@ -4,12 +4,20 @@ import {
4
4
  type ScenarioRequest,
5
5
  } from "cloving/workflows/testing";
6
6
  import { develop } from "../develop.ts";
7
- import { commit, input, completed, effects, selection } from "./scenarios.ts";
7
+ import {
8
+ commit,
9
+ input,
10
+ completed,
11
+ effects,
12
+ selection,
13
+ repairVerdicts,
14
+ verificationEffects,
15
+ } from "./scenarios.ts";
8
16
 
9
17
  const taskText =
10
18
  "Develop the submitted task text.\n\nGitHub issue: acme/demo#42 (open)";
11
19
 
12
- function requestInput() {
20
+ export function requestInput() {
13
21
  return selection.input === "ticket"
14
22
  ? input
15
23
  : {
@@ -19,7 +27,7 @@ function requestInput() {
19
27
  };
20
28
  }
21
29
 
22
- function scenarioEffects() {
30
+ export function scenarioEffects() {
23
31
  return {
24
32
  ...effects(),
25
33
  "claude run-turn": [
@@ -33,10 +41,13 @@ function scenarioEffects() {
33
41
  completed({ summary: "Reviewed.", reviewVerdict: "accept" }),
34
42
  ],
35
43
  "github partial-issue": [completed({ ticketStatus: "partial" })],
44
+ "command run:red-green": [
45
+ completed({ verdict: "accepted", output: "Passed." }),
46
+ ],
36
47
  };
37
48
  }
38
49
 
39
- function completeCalls(): string[] {
50
+ export function completeCalls(): string[] {
40
51
  const review: Readonly<Record<string, string>> = {
41
52
  greptile: "wait-review",
42
53
  person: "approval",
@@ -46,15 +57,17 @@ function completeCalls(): string[] {
46
57
  if (selection.merge === "local") return acceptance;
47
58
  return ["prepare"].concat(
48
59
  selection.input === "ticket" ? ["label"] : [],
49
- ["run-turn", "open-pr", review[selection.accept] ?? "unknown-review"],
60
+ ["run-turn", "open-pr"],
61
+ selection.verify === "red-green" ? ["run:red-green"] : [],
62
+ [review[selection.accept] ?? "unknown-review"],
50
63
  selection.merge === "person" ? ["approval", "merge"] : ["merge"],
51
64
  selection.input === "ticket" ? ["close-issue"] : [],
52
65
  );
53
66
  }
54
67
 
55
- const approved = { decision: "approved" as const, answer: null };
68
+ export const approved = { decision: "approved" as const, answer: null };
56
69
 
57
- function callName(request: ScenarioRequest): string {
70
+ export function callName(request: ScenarioRequest): string {
58
71
  if (request[0] === "effect") return request[1].method;
59
72
  if (request[0] === "waitForDecision") return request[1].kind;
60
73
  return request[0];
@@ -67,7 +80,7 @@ export const optionTests = [
67
80
  using bridge = fakeStepBridge({
68
81
  effects: scenarioEffects(),
69
82
  decisions: [approved, approved],
70
- effectLimit: 27,
83
+ effectLimit: selection.verify === "red-green" ? 35 : 31,
71
84
  });
72
85
  assert.deepEqual(await develop(requestInput()), {
73
86
  kind: "handoff_requested",
@@ -171,4 +184,120 @@ export const optionTests = [
171
184
  assert.equal(updates.length, Number(selection.partial === "queue"));
172
185
  },
173
186
  },
174
- ];
187
+ ].concat(selection.verify === "red-green" ? verificationTests() : []);
188
+
189
+ function verificationTests() {
190
+ return repairTests().concat(passingTests(), unavailableTests(), limitTests());
191
+ }
192
+
193
+ function repairTests() {
194
+ return repairVerdicts.map((verdict) => ({
195
+ name: `CLV-249 ${verdict} starts a fix turn with the full tool output`,
196
+ run: async () => {
197
+ using bridge = fakeStepBridge({
198
+ effects: verificationEffects([verdict, "accepted"]),
199
+ decisions: [approved, approved],
200
+ effectLimit: 35,
201
+ });
202
+ assert.equal((await develop(requestInput())).kind, "handoff_requested");
203
+ const expected = completeCalls();
204
+ expected.splice(
205
+ expected.indexOf("run:red-green") + 1,
206
+ 0,
207
+ "run-turn",
208
+ "open-pr",
209
+ "run:red-green",
210
+ );
211
+ assert.deepEqual(bridge.requests.map(callName), expected);
212
+ const fix = bridge.requests.find(
213
+ (request) =>
214
+ request[0] === "effect" &&
215
+ request[1].method === "run-turn" &&
216
+ request[1].scope.round === 2,
217
+ );
218
+ assert.ok(fix && fix[0] === "effect");
219
+ assert.ok(fix[1].payload.includes(`Address ${verdict}`));
220
+ assert.ok(fix[1].payload.endsWith(`${verdict}\nFull test output.`));
221
+ },
222
+ }));
223
+ }
224
+
225
+ function passingTests() {
226
+ return ["accepted", "no-test-change"].map((verdict) => ({
227
+ name: `CLV-249 ${verdict} continues to review and merge`,
228
+ run: async () => {
229
+ using bridge = fakeStepBridge({
230
+ effects: verificationEffects([verdict]),
231
+ decisions: [approved, approved],
232
+ });
233
+ assert.equal((await develop(requestInput())).kind, "handoff_requested");
234
+ assert.deepEqual(bridge.requests.map(callName), completeCalls());
235
+ },
236
+ }));
237
+ }
238
+
239
+ function limitTests() {
240
+ return [
241
+ {
242
+ name: "CLV-249 verification repairs stop after four development rounds",
243
+ run: async () => {
244
+ using bridge = fakeStepBridge({
245
+ effects: verificationEffects(Array(4).fill("false-red")),
246
+ effectLimit: 35,
247
+ });
248
+ assert.deepEqual(await develop(requestInput()), {
249
+ kind: "blocked",
250
+ cycleId: input.cycleId,
251
+ reason: "false-red",
252
+ evidenceId: "merge",
253
+ });
254
+ const names = bridge.requests.map(callName);
255
+ assert.equal(names.filter((name) => name === "run-turn").length, 4);
256
+ assert.equal(
257
+ names.filter((name) => name === "run:red-green").length,
258
+ 4,
259
+ );
260
+ assert.ok(!names.includes("wait-review") && !names.includes("merge"));
261
+ },
262
+ },
263
+ ];
264
+ }
265
+
266
+ function unavailableTests() {
267
+ const unavailable = [
268
+ completed({
269
+ verdict: "prepare-failed",
270
+ output: "Dependency preparation failed.",
271
+ }),
272
+ { kind: "failed" as const, operationId: "command", reason: "exit-1" },
273
+ { kind: "unavailable" as const, reason: "cloving-host-unavailable" },
274
+ { ...completed(null), result: "invalid JSON" },
275
+ completed(null),
276
+ completed({ verdict: "unknown", output: "Unknown verdict." }),
277
+ completed({ verdict: "accepted" }),
278
+ completed({ verdict: "false-red", output: 42 }),
279
+ ];
280
+ return unavailable.map((result, index) => ({
281
+ name: `CLV-249 unavailable verification ${String(index)} blocks without a fix turn`,
282
+ run: async () => {
283
+ using bridge = fakeStepBridge({
284
+ effects: {
285
+ ...verificationEffects(["accepted"]),
286
+ "command run:red-green": [result],
287
+ },
288
+ });
289
+ assert.deepEqual(await develop(requestInput()), {
290
+ kind: "blocked",
291
+ cycleId: input.cycleId,
292
+ reason: "red-green-unavailable",
293
+ evidenceId: "red-green",
294
+ });
295
+ const complete = completeCalls();
296
+ const expected = complete.slice(0, complete.indexOf("run:red-green") + 1);
297
+ assert.deepEqual(
298
+ bridge.requests.map(callName),
299
+ expected.concat(selection.input === "ticket" ? ["label"] : []),
300
+ );
301
+ },
302
+ }));
303
+ }
@@ -0,0 +1,55 @@
1
+ import { runCommand } from "cloving/workflows/steps/run-command";
2
+ import type { CycleOutcome } from "cloving/workflows";
3
+
4
+ type VerificationFeedback = {
5
+ readonly reason: string;
6
+ readonly output: string;
7
+ };
8
+
9
+ export async function verifyRedGreen(input: {
10
+ readonly cycleId: string;
11
+ readonly head: string;
12
+ readonly round: number;
13
+ }): Promise<CycleOutcome | VerificationFeedback | null> {
14
+ const unavailable: CycleOutcome = {
15
+ kind: "blocked",
16
+ cycleId: input.cycleId,
17
+ reason: "red-green-unavailable",
18
+ evidenceId: "red-green",
19
+ };
20
+ try {
21
+ const result = await runCommand({
22
+ name: "red-green",
23
+ scope: { phase: "verify", operation: "red-green", round: input.round },
24
+ input: { head: input.head },
25
+ });
26
+ if (result.kind !== "completed") return unavailable;
27
+ const feedback = verdictFeedback(result.result);
28
+ return feedback === undefined ? unavailable : feedback;
29
+ } catch {
30
+ return unavailable;
31
+ }
32
+ }
33
+
34
+ function verdictFeedback(
35
+ text: string | null,
36
+ ): VerificationFeedback | null | undefined {
37
+ const value: unknown = JSON.parse(text ?? "null");
38
+ if (
39
+ typeof value !== "object" ||
40
+ value === null ||
41
+ !("verdict" in value) ||
42
+ typeof value.verdict !== "string" ||
43
+ !("output" in value) ||
44
+ typeof value.output !== "string"
45
+ )
46
+ return undefined;
47
+ if (["accepted", "no-test-change"].includes(value.verdict)) return null;
48
+ if (
49
+ ["false-red", "red-not-assertion", "skipped-test", "green-failed"].includes(
50
+ value.verdict,
51
+ )
52
+ )
53
+ return { reason: value.verdict, output: value.output };
54
+ return undefined;
55
+ }
@@ -4,7 +4,7 @@ import {
4
4
  type ProtectedResult,
5
5
  type TicketStatus,
6
6
  } from "cloving/workflows";
7
- import { developTurn } from "./turn.ts";
7
+ import { developTurn, type DevelopTurnInput } from "./turn.ts";
8
8
 
9
9
  export interface DevelopInput {
10
10
  readonly cycleId: string;
@@ -72,16 +72,36 @@ export interface DevelopState {
72
72
 
73
73
  export async function developReport(
74
74
  state: DevelopState,
75
- ): Promise<DevelopReport> {
76
- const { input, round, feedback } = state;
77
- return reportOf(
78
- await developTurn({
79
- cycleId: input.cycleId,
80
- repository: input.repository,
81
- ticket: input.ticket,
82
- round,
83
- feedback,
84
- }),
75
+ ): Promise<DevelopReport | CycleOutcome> {
76
+ const first = await developTurn(turnRequest(state, "initial"));
77
+ if (!interrupted(first)) return reportOf(first);
78
+ const second = await developTurn(turnRequest(state, "continuation"));
79
+ if (!interrupted(second)) return reportOf(second);
80
+ return {
81
+ kind: "blocked",
82
+ cycleId: state.input.cycleId,
83
+ reason: "turn-interrupted",
84
+ evidenceId: "implement",
85
+ };
86
+ }
87
+
88
+ function turnRequest(
89
+ state: DevelopState,
90
+ attempt: "initial" | "continuation",
91
+ ): DevelopTurnInput {
92
+ return {
93
+ cycleId: state.input.cycleId,
94
+ repository: state.input.repository,
95
+ ticket: state.input.ticket,
96
+ round: state.round,
97
+ attempt,
98
+ feedback: state.feedback,
99
+ };
100
+ }
101
+
102
+ function interrupted(result: ProtectedResult): boolean {
103
+ return (
104
+ result.kind !== "completed" && result.reason === "claude-turn-interrupted"
85
105
  );
86
106
  }
87
107
 
@@ -13,6 +13,7 @@ export async function developRound(state: {
13
13
  const { input, round } = state;
14
14
  try {
15
15
  const report = await developReport(state);
16
+ if ("kind" in report) return report;
16
17
  const pull = await openPull({ input, round });
17
18
  const subject = {
18
19
  target: pull.target,
@@ -2,6 +2,7 @@ export const commit = "a".repeat(40);
2
2
  export const selection = {
3
3
  input: "ticket",
4
4
  select: "queue",
5
+ verify: "none",
5
6
  accept: "greptile",
6
7
  merge: "green",
7
8
  partial: "queue",
@@ -52,6 +53,32 @@ export function effects() {
52
53
  };
53
54
  }
54
55
 
56
+ export const repairVerdicts = [
57
+ "false-red",
58
+ "red-not-assertion",
59
+ "skipped-test",
60
+ "green-failed",
61
+ ];
62
+
63
+ export function verificationEffects(verdicts: readonly string[]) {
64
+ const scenario = effects();
65
+ return {
66
+ ...scenario,
67
+ "claude run-turn": verdicts.map(() =>
68
+ completed({ summary: "Implemented.\nTicket status: complete" }),
69
+ ),
70
+ "github open-pr": verdicts.map(() =>
71
+ completed({ number: 7, url: "pull/7", commit }),
72
+ ),
73
+ "command run:red-green": verdicts.map((verdict) =>
74
+ completed({ verdict, output: `${verdict}\nFull test output.` }),
75
+ ),
76
+ "claude run-turn:review": [
77
+ completed({ summary: "Reviewed.", reviewVerdict: "accept" }),
78
+ ],
79
+ };
80
+ }
81
+
55
82
  export function partialEffects(
56
83
  pullRequest: number,
57
84
  remaining: readonly string[],
@@ -7,6 +7,7 @@ export interface DevelopTurnInput {
7
7
  readonly ticket?: string;
8
8
  readonly prompt?: string;
9
9
  readonly round: number;
10
+ readonly attempt?: "initial" | "continuation";
10
11
  readonly feedback: {
11
12
  readonly reason: string;
12
13
  readonly output: string;
@@ -50,19 +51,38 @@ function turnPrompt(input: DevelopTurnInput): string {
50
51
  ].join("\n");
51
52
  }
52
53
 
54
+ function attemptPrompt(input: DevelopTurnInput): string {
55
+ const prompt = turnPrompt(input);
56
+ if (input.attempt !== "continuation") return prompt;
57
+ return [
58
+ `The earlier turn of round ${String(input.round)} stopped before its report.`,
59
+ "The work tree holds the work of that turn.",
60
+ "Before you continue, check the work tree, the commits since the start of the round, and the ticket.",
61
+ "",
62
+ prompt,
63
+ ].join("\n");
64
+ }
65
+
66
+ function turnScope(input: DevelopTurnInput) {
67
+ if (input.attempt !== "continuation")
68
+ return { phase: "implement", operation: "claude-turn", round: input.round };
69
+ return {
70
+ phase: "implement",
71
+ operation: "claude-turn",
72
+ round: input.round,
73
+ key: "continuation",
74
+ };
75
+ }
76
+
53
77
  export async function developTurn(
54
78
  input: DevelopTurnInput,
55
79
  ): Promise<ProtectedResult> {
56
80
  return await protectedEffect({
57
- scope: {
58
- phase: "implement",
59
- operation: "claude-turn",
60
- round: input.round,
61
- },
81
+ scope: turnScope(input),
62
82
  capability: "claude",
63
83
  method: "run-turn",
64
84
  target: input.cycleId,
65
- payload: turnPrompt(input),
85
+ payload: attemptPrompt(input),
66
86
  units: 1,
67
87
  });
68
88
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "workflow": {
3
3
  "workflowId": "develop",
4
- "effectLimit": 27,
4
+ "effectLimit": 31,
5
5
  "capabilities": ["claude", "github"],
6
6
  "queue": true,
7
7
  "input": {
@@ -1,5 +1,6 @@
1
1
  import { flowTests } from "./develop/flow-tests.ts";
2
2
  import { refusalTests } from "./develop/refusal-tests.ts";
3
+ import { interruptionTests } from "./develop/interruption-tests.ts";
3
4
  import assert from "node:assert/strict";
4
5
  import { test } from "node:test";
5
6
  import { fakeStepBridge } from "cloving/workflows/testing";
@@ -10,6 +11,9 @@ import { queuePartial } from "./develop/queue-partial.ts";
10
11
  for (const scenario of flowTests.concat(refusalTests))
11
12
  await test(scenario.name, scenario.run);
12
13
 
14
+ for (const scenario of interruptionTests)
15
+ await test(scenario.name, scenario.run);
16
+
13
17
  await test("CLV-221 invalid partial rounds never dispatch an effect or open a question", async () => {
14
18
  using bridge = fakeStepBridge({});
15
19
  for (const round of [0, -1, 1.5, 5, NaN, Infinity]) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
3
  "name": "develop",
4
- "summary": "Develop a ticket or task with explicit choices for selection, acceptance, merge and partial work. The first option at each point is the hands-off default.",
4
+ "summary": "Develop a ticket or task with explicit choices for selection, verification, acceptance, merge and partial work. The first option at each point is the hands-off default.",
5
5
  "points": [
6
6
  {
7
7
  "id": "input",
@@ -52,6 +52,33 @@
52
52
  }
53
53
  ]
54
54
  },
55
+ {
56
+ "id": "verify",
57
+ "question": "How should the tests be verified before review?",
58
+ "options": [
59
+ {
60
+ "id": "none",
61
+ "label": "No additional verification",
62
+ "summary": "Keep the current development, review and merge checks.",
63
+ "change": "Leave develop/round.ts and the declaration unchanged for verification.",
64
+ "requires": {},
65
+ "needs": []
66
+ },
67
+ {
68
+ "id": "red-green",
69
+ "label": "Red and green test check",
70
+ "summary": "Verify an assertion failure on the base and passing tests on the pull request head before review.",
71
+ "change": "In develop/round.ts import verifyRedGreen from develop/red-green.ts. After openPull and before acceptWork, call verifyRedGreen({ cycleId: input.cycleId, head: pull.commit, round }). Return a blocked outcome immediately; pass non-null feedback to repair({ cycleId: input.cycleId, round, report, result: verification }). In develop.declaration.json add the command capability to workflow.capabilities and \"commands\": [\"red-green\"] to workflow, and command: [\"run:red-green\"] to methods. Add 4 to workflow.effectLimit. Configure the operator red-green command and checks.redGreen.",
72
+ "requires": {
73
+ "merge": [
74
+ "green",
75
+ "person"
76
+ ]
77
+ },
78
+ "needs": []
79
+ }
80
+ ]
81
+ },
55
82
  {
56
83
  "id": "accept",
57
84
  "question": "Who accepts the developed work?",
@@ -143,7 +170,7 @@
143
170
  "id": "person",
144
171
  "label": "A person decides",
145
172
  "summary": "Ask what to do with partial work. Use the answer as feedback for a bounded continuation, or stop and leave it for the person.",
146
- "change": "For a pull-request workflow, in develop/round.ts import partialChoice from develop/partial-choice.ts. Immediately after developReport, call partialChoice({ input, round, report }) and return any non-null result. develop/local.ts already does this. The helper calls waitForDecision({ gateId: partialGateId, kind: \"question\", prompt: partialQuestion }); an explicit answer feeds the next bounded turn and an empty answer stops.",
173
+ "change": "For a pull-request workflow, in develop/round.ts import partialChoice from develop/partial-choice.ts. Immediately after the line that returns a blocked outcome of developReport, call partialChoice({ input, round, report }) and return any non-null result. develop/local.ts already does this. The helper calls waitForDecision({ gateId: partialGateId, kind: \"question\", prompt: partialQuestion }); an explicit answer feeds the next bounded turn and an empty answer stops.",
147
174
  "requires": {},
148
175
  "needs": []
149
176
  }