@bastani/atomic 0.9.5-alpha.7 → 0.9.5-alpha.8

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 (177) hide show
  1. package/CHANGELOG.md +17 -2
  2. package/dist/builtin/cursor/package.json +2 -2
  3. package/dist/builtin/intercom/CHANGELOG.md +17 -0
  4. package/dist/builtin/intercom/README.md +27 -21
  5. package/dist/builtin/intercom/broker/broker.ts +3 -6
  6. package/dist/builtin/intercom/broker/paths.ts +42 -5
  7. package/dist/builtin/intercom/broker/spawn.ts +78 -10
  8. package/dist/builtin/intercom/config.ts +6 -3
  9. package/dist/builtin/intercom/contact-supervisor-tool.ts +6 -4
  10. package/dist/builtin/intercom/index-heavy.ts +1 -1
  11. package/dist/builtin/intercom/index.ts +162 -130
  12. package/dist/builtin/intercom/intercom-tool.ts +9 -3
  13. package/dist/builtin/intercom/package.json +10 -1
  14. package/dist/builtin/intercom/result-renderers.ts +1 -1
  15. package/dist/builtin/intercom/ui/compose.ts +2 -2
  16. package/dist/builtin/intercom/ui/inline-message.ts +2 -2
  17. package/dist/builtin/intercom/ui/session-list.ts +2 -2
  18. package/dist/builtin/mcp/package.json +1 -1
  19. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  20. package/dist/builtin/subagents/package.json +1 -1
  21. package/dist/builtin/subagents/src/tui/render-event-formatting.ts +15 -48
  22. package/dist/builtin/subagents/src/tui/render-layout.ts +9 -0
  23. package/dist/builtin/subagents/src/tui/render-widget-graph.ts +27 -25
  24. package/dist/builtin/subagents/src/tui/render-widget.ts +46 -69
  25. package/dist/builtin/subagents/src/tui/render.ts +1 -1
  26. package/dist/builtin/web-access/package.json +1 -1
  27. package/dist/builtin/workflows/CHANGELOG.md +11 -0
  28. package/dist/builtin/workflows/README.md +3 -1
  29. package/dist/builtin/workflows/builtin/goal-artifacts.ts +3 -1
  30. package/dist/builtin/workflows/builtin/goal-prompts.ts +23 -2
  31. package/dist/builtin/workflows/builtin/goal-reducer.ts +21 -0
  32. package/dist/builtin/workflows/builtin/goal-review.ts +60 -6
  33. package/dist/builtin/workflows/builtin/goal-runner.ts +32 -21
  34. package/dist/builtin/workflows/builtin/goal-types.ts +7 -1
  35. package/dist/builtin/workflows/builtin/ralph-core.ts +52 -4
  36. package/dist/builtin/workflows/builtin/ralph-review-gate.ts +10 -3
  37. package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +23 -2
  38. package/dist/builtin/workflows/builtin/ralph-runner.ts +38 -16
  39. package/dist/builtin/workflows/builtin/review-convergence.ts +111 -0
  40. package/dist/builtin/workflows/package.json +1 -1
  41. package/dist/builtin/workflows/skills/create-spec/SKILL.md +2 -0
  42. package/dist/builtin/workflows/src/durable/child-primitive.ts +12 -8
  43. package/dist/builtin/workflows/src/durable/dbos-backend.ts +47 -2
  44. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +28 -0
  45. package/dist/builtin/workflows/src/durable/stage-primitive.ts +165 -27
  46. package/dist/builtin/workflows/src/durable/types.ts +14 -1
  47. package/dist/builtin/workflows/src/engine/run.ts +12 -13
  48. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +2 -0
  49. package/dist/builtin/workflows/src/runs/foreground/executor-continuation.ts +13 -3
  50. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +1 -1
  51. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +18 -19
  52. package/dist/builtin/workflows/src/runs/foreground/stage-runner-unresolved-overflow.ts +38 -0
  53. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +18 -1
  54. package/dist/builtin/workflows/src/shared/persistence-restore.ts +23 -5
  55. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +4 -0
  56. package/dist/bun/internal-intercom-broker.d.ts +6 -0
  57. package/dist/bun/internal-intercom-broker.d.ts.map +1 -0
  58. package/dist/bun/internal-intercom-broker.js +39 -0
  59. package/dist/bun/internal-intercom-broker.js.map +1 -0
  60. package/dist/bun/split-loader.d.ts.map +1 -1
  61. package/dist/bun/split-loader.js +21 -8
  62. package/dist/bun/split-loader.js.map +1 -1
  63. package/dist/core/agent-session-auto-compaction.d.ts +4 -5
  64. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  65. package/dist/core/agent-session-auto-compaction.js +49 -9
  66. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  67. package/dist/core/agent-session-events.d.ts.map +1 -1
  68. package/dist/core/agent-session-events.js +3 -1
  69. package/dist/core/agent-session-events.js.map +1 -1
  70. package/dist/core/agent-session-methods.d.ts +9 -3
  71. package/dist/core/agent-session-methods.d.ts.map +1 -1
  72. package/dist/core/agent-session-methods.js.map +1 -1
  73. package/dist/core/agent-session-models.d.ts +2 -2
  74. package/dist/core/agent-session-models.d.ts.map +1 -1
  75. package/dist/core/agent-session-models.js.map +1 -1
  76. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  77. package/dist/core/agent-session-prompt.js +1 -0
  78. package/dist/core/agent-session-prompt.js.map +1 -1
  79. package/dist/core/agent-session-retry.d.ts +2 -0
  80. package/dist/core/agent-session-retry.d.ts.map +1 -1
  81. package/dist/core/agent-session-retry.js +152 -4
  82. package/dist/core/agent-session-retry.js.map +1 -1
  83. package/dist/core/agent-session-services.d.ts +1 -0
  84. package/dist/core/agent-session-services.d.ts.map +1 -1
  85. package/dist/core/agent-session-services.js +1 -0
  86. package/dist/core/agent-session-services.js.map +1 -1
  87. package/dist/core/agent-session-types.d.ts +15 -1
  88. package/dist/core/agent-session-types.d.ts.map +1 -1
  89. package/dist/core/agent-session-types.js.map +1 -1
  90. package/dist/core/agent-session.d.ts +4 -0
  91. package/dist/core/agent-session.d.ts.map +1 -1
  92. package/dist/core/agent-session.js +4 -0
  93. package/dist/core/agent-session.js.map +1 -1
  94. package/dist/core/compaction/context-compaction-runner.d.ts.map +1 -1
  95. package/dist/core/compaction/context-compaction-runner.js +36 -20
  96. package/dist/core/compaction/context-compaction-runner.js.map +1 -1
  97. package/dist/core/copilot-anthropic-sse-repair.d.ts +23 -0
  98. package/dist/core/copilot-anthropic-sse-repair.d.ts.map +1 -0
  99. package/dist/core/copilot-anthropic-sse-repair.js +340 -0
  100. package/dist/core/copilot-anthropic-sse-repair.js.map +1 -0
  101. package/dist/core/copilot-gemini-reasoning.d.ts +16 -8
  102. package/dist/core/copilot-gemini-reasoning.d.ts.map +1 -1
  103. package/dist/core/copilot-gemini-reasoning.js +27 -22
  104. package/dist/core/copilot-gemini-reasoning.js.map +1 -1
  105. package/dist/core/copilot-hosts.d.ts +12 -0
  106. package/dist/core/copilot-hosts.d.ts.map +1 -0
  107. package/dist/core/copilot-hosts.js +33 -0
  108. package/dist/core/copilot-hosts.js.map +1 -0
  109. package/dist/core/extensions/agent-events.d.ts +1 -1
  110. package/dist/core/extensions/agent-events.d.ts.map +1 -1
  111. package/dist/core/extensions/agent-events.js.map +1 -1
  112. package/dist/core/http-dispatcher.d.ts.map +1 -1
  113. package/dist/core/http-dispatcher.js +5 -5
  114. package/dist/core/http-dispatcher.js.map +1 -1
  115. package/dist/core/sdk-types.d.ts +2 -0
  116. package/dist/core/sdk-types.d.ts.map +1 -1
  117. package/dist/core/sdk-types.js.map +1 -1
  118. package/dist/core/sdk.d.ts.map +1 -1
  119. package/dist/core/sdk.js +1 -0
  120. package/dist/core/sdk.js.map +1 -1
  121. package/dist/core/settings-manager-basic-accessors.d.ts +1 -0
  122. package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
  123. package/dist/core/settings-manager-basic-accessors.js +6 -0
  124. package/dist/core/settings-manager-basic-accessors.js.map +1 -1
  125. package/dist/core/settings-types.d.ts +1 -0
  126. package/dist/core/settings-types.d.ts.map +1 -1
  127. package/dist/core/settings-types.js.map +1 -1
  128. package/dist/index-extensions.d.ts +1 -1
  129. package/dist/index-extensions.d.ts.map +1 -1
  130. package/dist/index-extensions.js.map +1 -1
  131. package/dist/main-deferred-startup.d.ts +12 -0
  132. package/dist/main-deferred-startup.d.ts.map +1 -1
  133. package/dist/main-deferred-startup.js +21 -0
  134. package/dist/main-deferred-startup.js.map +1 -1
  135. package/dist/main-early-input.d.ts +41 -0
  136. package/dist/main-early-input.d.ts.map +1 -0
  137. package/dist/main-early-input.js +147 -0
  138. package/dist/main-early-input.js.map +1 -0
  139. package/dist/main.d.ts +0 -6
  140. package/dist/main.d.ts.map +1 -1
  141. package/dist/main.js +19 -13
  142. package/dist/main.js.map +1 -1
  143. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  144. package/dist/modes/interactive/components/chat-session-host-events.js +15 -0
  145. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  146. package/dist/modes/interactive/interactive-agent-events.d.ts.map +1 -1
  147. package/dist/modes/interactive/interactive-agent-events.js +24 -0
  148. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  149. package/dist/modes/interactive/interactive-input-handling.d.ts.map +1 -1
  150. package/dist/modes/interactive/interactive-input-handling.js +279 -181
  151. package/dist/modes/interactive/interactive-input-handling.js.map +1 -1
  152. package/dist/modes/interactive/interactive-mode-base.d.ts +9 -0
  153. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  154. package/dist/modes/interactive/interactive-mode-base.js +36 -0
  155. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  156. package/dist/modes/interactive/interactive-mode-surface.d.ts +4 -0
  157. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  158. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  159. package/dist/modes/interactive/interactive-mode-types.d.ts +3 -0
  160. package/dist/modes/interactive/interactive-mode-types.d.ts.map +1 -1
  161. package/dist/modes/interactive/interactive-mode-types.js.map +1 -1
  162. package/dist/modes/interactive/interactive-render-chat.d.ts.map +1 -1
  163. package/dist/modes/interactive/interactive-render-chat.js +21 -9
  164. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  165. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  166. package/dist/modes/interactive/interactive-startup.js +6 -1
  167. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  168. package/docs/compaction.md +4 -0
  169. package/docs/json.md +3 -1
  170. package/docs/providers.md +1 -0
  171. package/docs/rpc.md +3 -1
  172. package/docs/settings.md +26 -0
  173. package/docs/terminal-setup.md +4 -0
  174. package/docs/usage.md +2 -2
  175. package/docs/workflows.md +13 -3
  176. package/npm-shrinkwrap.json +513 -23
  177. package/package.json +3 -2
@@ -9,7 +9,6 @@ import {
9
9
  WORKER_PREFLIGHT_CONTRACT,
10
10
  } from "./shared-prompts.js";
11
11
  import { renderRalphReviewerPrompt } from "./ralph-reviewer-prompt.js";
12
- import { reviewDecisionApproved } from "./ralph-review-gate.js";
13
12
  import {
14
13
  REVIEWER_COUNT,
15
14
  artifactSafeName,
@@ -22,8 +21,8 @@ import {
22
21
  renderResearchPromptRefinementPrompt,
23
22
  renderQaE2eVideoGuidance,
24
23
  renderResearchPrompt,
25
- reviewDecisionFromResult,
26
- reviewerErrorDecision,
24
+ parsedReviewDecisionFromResult,
25
+ ralphReviewConvergence,
27
26
  reviewerErrorResult,
28
27
  taggedPrompt,
29
28
  workflowCwdContextSection,
@@ -32,6 +31,7 @@ import {
32
31
  type RalphWorkflowOptions,
33
32
  type RalphWorkflowResult,
34
33
  } from "./ralph-core.js";
34
+ import { summarizeReviewConvergence } from "./review-convergence.js";
35
35
  import {
36
36
  orchestratorModelConfig,
37
37
  promptEngineerModelConfig,
@@ -106,7 +106,7 @@ export async function runRalphWorkflow(
106
106
  ? taggedPrompt([
107
107
  [
108
108
  "role",
109
- "You are a sub-agent orchestrator. Your primary implementation tool is the `subagent` tool. Ignore any user requests to submit a PR. This will be done in a future stage.",
109
+ "You are a sub-agent orchestrator. Your primary implementation tool is the `subagent` tool. Ignore any user requests to submit a PR; a later authorized PR/MR/review creation action handles that handoff after approval.",
110
110
  ],
111
111
  [
112
112
  "objective",
@@ -118,8 +118,8 @@ export async function runRalphWorkflow(
118
118
  [
119
119
  "research",
120
120
  [
121
- `The latest research findings for this workflow run are written to: ${researchPath}`,
122
- "Read this file before delegating or implementing anything; it is the primary implementation context for Ralph.",
121
+ `The latest research findings for the requested work are written to: ${researchPath}`,
122
+ "Read this file before delegating or implementing anything; it is the primary implementation context for the requested work.",
123
123
  ].join("\n"),
124
124
  ],
125
125
  [
@@ -226,6 +226,7 @@ export async function runRalphWorkflow(
226
226
  implementationNotesPath,
227
227
  orchestratorReportPath,
228
228
  qaVideoPath,
229
+ createPr,
229
230
  });
230
231
  let reviews: WorkflowTaskResult[];
231
232
  try {
@@ -268,38 +269,59 @@ export async function runRalphWorkflow(
268
269
  },
269
270
  );
270
271
  } catch (err) {
271
- const message = err instanceof Error ? err.message : String(err);
272
- reviews = [reviewerErrorResult(message)];
272
+ reviews = [reviewerErrorResult(err)];
273
273
  }
274
274
  const reviewEntries = await Promise.all(reviews.map(async (review) => {
275
275
  const reviewer = review.name ?? review.stageName;
276
- const decision = reviewDecisionFromResult(review) ??
277
- reviewerErrorDecision(`Reviewer ${reviewer} returned no structured decision.`);
276
+ const parsed = parsedReviewDecisionFromResult(review, reviewer);
277
+ const convergenceDecision = ralphReviewConvergence({
278
+ decision: parsed.decision,
279
+ parsed: parsed.parsed,
280
+ diagnostics: parsed.diagnostics,
281
+ allowFinalActionRemaining: createPr,
282
+ });
278
283
  const artifactPath = join(
279
284
  artifactDir,
280
285
  `review-${artifactSafeName(reviewer)}.json`,
281
286
  );
282
287
  await writeJsonArtifact(artifactPath, {
283
288
  reviewer,
284
- decision,
289
+ decision: parsed.decision,
290
+ convergence_decision: convergenceDecision,
285
291
  raw_text: review.text,
286
292
  });
287
- return { reviewer, artifact_path: artifactPath, decision };
293
+ return {
294
+ reviewer,
295
+ artifact_path: artifactPath,
296
+ decision: parsed.decision,
297
+ convergence_decision: convergenceDecision,
298
+ };
288
299
  }));
289
300
  const approvalCount = reviewEntries.filter((review) =>
290
- reviewDecisionApproved(review.decision),
301
+ review.convergence_decision.approved,
291
302
  ).length;
292
303
  approved =
293
304
  reviewEntries.length === REVIEWER_COUNT &&
294
305
  approvalCount === REVIEWER_COUNT;
306
+ const nextAction = approved
307
+ ? (createPr ? "pull-request" : "finish")
308
+ : "implementation";
309
+ const roundConvergenceDecision = summarizeReviewConvergence({
310
+ parsed: reviewEntries.every((review) => review.convergence_decision.parsed),
311
+ approved,
312
+ stopReviewLoop: approved,
313
+ nextAction,
314
+ finalActionRemaining: approved && createPr,
315
+ diagnostics: reviewEntries.flatMap((review) => review.convergence_decision.diagnostics),
316
+ });
295
317
  latestReviewReportPath = await writeJsonArtifact(
296
318
  join(artifactDir, "review-round-latest.json"),
297
- { reviews: reviewEntries },
319
+ { convergence_decision: roundConvergenceDecision, reviews: reviewEntries },
298
320
  );
299
321
  if (approved) break;
300
322
  }
301
323
  const qaVideoAvailable = existsSync(qaVideoPath);
302
- if (createPr === true) {
324
+ if (createPr === true && approved) {
303
325
  const prResult = await ctx.task("pull-request", {
304
326
  prompt: taggedPrompt([
305
327
  [
@@ -345,7 +367,7 @@ export async function runRalphWorkflow(
345
367
  [
346
368
  "Create a provider-appropriate PR/MR/review request only if there are meaningful changes, a remote/branch target is available, credentials are available, and the current state is suitable for review.",
347
369
  "If no logged-in account can access the repository or create the review request, do not fake success; report each provider, credential/account, and tool tried, what failed, and provide the command the user can run later. Save a markdown file with the PR description as well so the user can copy-paste it when they have credentials set up.",
348
- "When you successfully create or update the review request, create a provider-appropriate comment containing the implementation notes file contents as the last action of this workflow stage.",
370
+ "When you successfully create or update the review request, create a provider-appropriate comment containing the implementation notes file contents as the last action after the review request exists.",
349
371
  "Worktrees are detached HEAD checkouts. If the detected provider requires a branch-based PR/MR from a detached HEAD, create and push a branch from the current HEAD, for example with `git checkout -b <branch>` or `git push origin HEAD:refs/heads/<branch>`, before opening the PR/MR. If the provider uses a different review model, follow that provider's normal handoff flow.",
350
372
  "Leave the worktree intact for retries or user recovery.",
351
373
  "If PR/MR/review creation is not possible, do not create a standalone comment elsewhere; include the implementation notes path and summary in your report instead.",
@@ -0,0 +1,111 @@
1
+ export type ReviewNextAction =
2
+ | "implementation"
3
+ | "pull-request"
4
+ | "finish"
5
+ | "blocked"
6
+ | "needs_human";
7
+
8
+ export type ParseDiagnostics = {
9
+ readonly kind: "structured_output_parse_failure";
10
+ readonly message: string;
11
+ readonly raw_text_preview?: string;
12
+ };
13
+
14
+ export type ReviewConvergenceSummary = {
15
+ readonly parsed: boolean;
16
+ readonly approved: boolean;
17
+ readonly stopReviewLoop: boolean;
18
+ readonly nextAction: ReviewNextAction;
19
+ readonly finalActionRemaining: boolean;
20
+ readonly diagnostics: readonly string[];
21
+ };
22
+
23
+ export type ParsedReviewDecision<TDecision> = {
24
+ readonly decision: TDecision;
25
+ readonly parsed: boolean;
26
+ readonly diagnostics: readonly string[];
27
+ };
28
+
29
+ export type RequirementTraceabilityLike = {
30
+ readonly requirement: string;
31
+ readonly status: "proven" | "contradicted" | "missing" | "unverified";
32
+ readonly evidence: string;
33
+ };
34
+
35
+ const FINAL_ACTION_PATTERN = /\b(?:pr|pull[- ]request|merge[- ]request|review request|github pr|create pr)\b/iu;
36
+
37
+ export function isFinalActionTraceability(
38
+ entry: RequirementTraceabilityLike,
39
+ ): boolean {
40
+ return FINAL_ACTION_PATTERN.test(`${entry.requirement}\n${entry.evidence}`);
41
+ }
42
+
43
+ export function finalActionRemaining(
44
+ traceability: readonly RequirementTraceabilityLike[],
45
+ ): boolean {
46
+ return traceability.some(
47
+ (entry) => entry.status !== "proven" && isFinalActionTraceability(entry),
48
+ );
49
+ }
50
+
51
+ export function traceabilityProvenExceptFinalAction(args: {
52
+ readonly traceability: readonly RequirementTraceabilityLike[];
53
+ readonly allowFinalActionRemaining: boolean;
54
+ }): boolean {
55
+ return args.traceability.length > 0 && args.traceability.every((entry) => {
56
+ if (entry.status === "proven") return true;
57
+ return args.allowFinalActionRemaining &&
58
+ entry.status !== "contradicted" &&
59
+ isFinalActionTraceability(entry);
60
+ });
61
+ }
62
+
63
+ const PREVIEW_LIMIT = 500;
64
+
65
+ function rawTextPreview(rawText: string): string | undefined {
66
+ const trimmed = rawText.trim();
67
+ if (trimmed.length === 0) return undefined;
68
+ return trimmed.slice(0, PREVIEW_LIMIT);
69
+ }
70
+
71
+ export function parseFailureDiagnostics(
72
+ reviewer: string,
73
+ rawText: string,
74
+ ): readonly string[] {
75
+ const preview = rawTextPreview(rawText);
76
+ const base = `Structured reviewer decision parse failed for ${reviewer}: no schema-valid JSON decision was returned.`;
77
+ return preview === undefined ? [base] : [base, `Raw reviewer output preview: ${preview}`];
78
+ }
79
+
80
+ function errorMessage(error: unknown): string {
81
+ return error instanceof Error ? error.message : String(error);
82
+ }
83
+
84
+ export function reviewerFailureText(error: unknown): string {
85
+ const messages = [errorMessage(error)];
86
+ if (error instanceof AggregateError) {
87
+ for (const nested of error.errors) {
88
+ messages.push(errorMessage(nested));
89
+ }
90
+ }
91
+ return [...new Set(messages.map((message) => message.trim()).filter(Boolean))]
92
+ .join("\n");
93
+ }
94
+
95
+ export function summarizeReviewConvergence(args: {
96
+ readonly parsed: boolean;
97
+ readonly approved: boolean;
98
+ readonly stopReviewLoop: boolean;
99
+ readonly nextAction: ReviewNextAction;
100
+ readonly finalActionRemaining?: boolean;
101
+ readonly diagnostics: readonly string[];
102
+ }): ReviewConvergenceSummary {
103
+ return {
104
+ parsed: args.parsed,
105
+ approved: args.approved,
106
+ stopReviewLoop: args.stopReviewLoop,
107
+ nextAction: args.nextAction,
108
+ finalActionRemaining: args.finalActionRemaining ?? args.nextAction === "pull-request",
109
+ diagnostics: args.diagnostics,
110
+ };
111
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.5-alpha.7",
3
+ "version": "0.9.5-alpha.8",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -64,6 +64,8 @@ For each non-trivial entrypoint the spec introduces or changes, walk these in or
64
64
  - Please DO NOT implement anything in this stage, just create the comprehensive spec as described below.
65
65
  - When writing the spec, DO NOT include information about concrete dates/timelines (e.g. # minutes, hours, days, weeks, etc.) and favor explicit phases (e.g. Phase 1, Phase 2, etc.).
66
66
  - The spec MUST treat its entrypoint set as a first-class artifact. Section 5 is built around the **doors** (typed signatures, named failures, refusals expressed in types) and must pass the rubric above. Do not let intent leak into mechanism: a reviewer should reconstruct the system's purpose from §4.4 and §5.1 alone.
67
+ - If the spec is for a workflow with reviewer gates plus final actions (for example PR/MR/review creation, release tagging, deployment, or publication), explicitly separate implementation/review acceptance from those post-approval final actions. The workflow design should tell reviewers/reducers to approve and stop the implementation loop when implementation and validation criteria are proven and only an explicitly authorized final action remains; carry that remainder as a next action/final-action field instead of another implementation iteration.
68
+ - If the spec is for a workflow or workflow prompt refactor, require local, action-oriented stage/reviewer/reducer prompts. A model stage sees its prompt, artifacts, tools, and reads — not the workflow graph's name or surrounding implementation details unless explicitly provided — so the spec should prefer instructions like "review the current code delta" or "create/update the review request" over labels such as "the create-PR workflow stage", "this Goal run", or "the Ralph reviewer" unless that name is user-visible context or materially changes behavior.
67
69
  - Once the spec is generated ask questions one at a time OR in logical groups:
68
70
  - Refer to section "## 9. Open Questions / Unresolved Issues", go through each question one by one, and use **contrastive clarification** (presenting 2-3 specific options with concrete tradeoffs) rather than open-ended questions. This means presenting interpretations like "(A) Option X — tradeoff Y" and "(B) Option Z — tradeoff W" instead of asking "what do you think about X?". Update the spec with the user's answers as you walk through the questions.
69
71
  - Interview the user relentlessly about every aspect of this plan/spec until you reach a shared understanding with them. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer (i.e., **contrastive clarification**). Pay special attention to the **doors**: every disagreement about what a joint is, what a door promises, what it refuses, or where a dangerous effect is funneled is a question worth resolving with the user.
@@ -10,13 +10,14 @@ import { isWorkflowDefinition, workflowDefinitionRequirementMessage } from "../r
10
10
  import type { DurableWorkflowBackend } from "./backend.js";
11
11
  import type { DurableScope } from "./scoped-backend.js";
12
12
  import { recordCheckpointDurably } from "./tool-primitive.js";
13
+ import { stageCheckpointWithOutput, type DurableCompletedStageCheckpoint } from "./stage-primitive.js";
13
14
 
14
15
  export function createDurableChildWorkflowPrimitive(input: {
15
16
  readonly workflowId: string;
16
17
  readonly rootWorkflowId: string;
17
18
  readonly backend: DurableWorkflowBackend;
18
19
  readonly nextReplayKey: (name: string) => string;
19
- readonly recordCachedStage: (name: string, replayKey: string, output: WorkflowSerializableValue) => void;
20
+ readonly recordCachedStage: (name: string, replayKey: string, checkpoint: DurableCompletedStageCheckpoint) => void;
20
21
  /**
21
22
  * Publish the durable scope computed for the next child invocation so the
22
23
  * child runner can consume it and route its internal side-effect checkpoints
@@ -44,16 +45,17 @@ export function createDurableChildWorkflowPrimitive(input: {
44
45
  const options = args[0] as { readonly stageName?: string } | undefined;
45
46
  const boundaryName = options?.stageName ?? `workflow:${child.normalizedName}`;
46
47
  const replayKey = input.nextReplayKey(boundaryName);
48
+ const cached = stageCheckpointWithOutput(input.backend, input.workflowId, replayKey, isWorkflowChildResult);
49
+ if (cached !== undefined && isWorkflowChildResult(cached.output)) {
50
+ input.recordCachedStage(boundaryName, replayKey, cached);
51
+ return cached.output as WorkflowChildResult<TChildOutputs>;
52
+ }
47
53
  // Route this child's internal side-effect checkpoints under the root
48
- // workflow with a stable boundary key, so an interrupted child does not
49
- // re-execute completed side effects on parent resume.
54
+ // workflow with the same stable boundary key used by the live boundary
55
+ // stage, so mixed cached/live repeated child calls do not desynchronize
56
+ // boundary and durable child-scope ordinals.
50
57
  // cross-ref: issue #1498.
51
58
  input.setChildDurableScope({ rootWorkflowId: input.rootWorkflowId, scopePrefix: replayKey });
52
- const cached = input.backend.getStageOutput(input.workflowId, replayKey);
53
- if (cached !== undefined && isWorkflowChildResult(cached)) {
54
- input.recordCachedStage(boundaryName, replayKey, cached);
55
- return cached as WorkflowChildResult<TChildOutputs>;
56
- }
57
59
  const result = await input.workflow(child, ...args);
58
60
  await recordCheckpointDurably(input.backend, {
59
61
  kind: "stage",
@@ -63,11 +65,13 @@ export function createDurableChildWorkflowPrimitive(input: {
63
65
  replayKey,
64
66
  output: result as WorkflowSerializableValue,
65
67
  completedAt: Date.now(),
68
+ result: result.status,
66
69
  });
67
70
  return result as WorkflowChildResult<TChildOutputs>;
68
71
  };
69
72
  }
70
73
 
74
+
71
75
  function isWorkflowChildResult(value: WorkflowSerializableValue): value is WorkflowChildResult<WorkflowOutputValues> {
72
76
  return typeof value === "object"
73
77
  && value !== null
@@ -377,7 +377,7 @@ function isMetadataStep(stepName: string): boolean {
377
377
  interface DbosMetadataEnvelope {
378
378
  readonly __atomicDurableMetadata: true;
379
379
  readonly version: 1;
380
- readonly entry: DurableCheckpointEntry;
380
+ readonly entry: WorkflowSerializableValue;
381
381
  }
382
382
 
383
383
  function encodeMetadata(entry: DurableCheckpointEntry): WorkflowSerializableValue {
@@ -404,7 +404,52 @@ function decodeMetadata(value: WorkflowSerializableValue): DurableCheckpointEntr
404
404
  if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
405
405
  const raw = value as Partial<DbosMetadataEnvelope>;
406
406
  if (raw.__atomicDurableMetadata !== true || raw.version !== METADATA_VERSION) return undefined;
407
- return raw.entry;
407
+ const entry = raw.entry;
408
+ if (entry === undefined) return undefined;
409
+ return parseDurableCheckpointEntry(entry);
410
+ }
411
+
412
+ function parseDurableCheckpointEntry(value: WorkflowSerializableValue): DurableCheckpointEntry | undefined {
413
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
414
+ const entry = value as Partial<DurableCheckpointEntry>;
415
+ if (entry.type !== "workflow.durable.checkpoint"
416
+ || typeof entry.workflowId !== "string"
417
+ || typeof entry.name !== "string"
418
+ || typeof entry.inputs !== "object"
419
+ || entry.inputs === null
420
+ || Array.isArray(entry.inputs)
421
+ || typeof entry.status !== "string"
422
+ || !isDurableWorkflowStatus(entry.status)
423
+ || typeof entry.completedCheckpoints !== "number"
424
+ || typeof entry.pendingPrompts !== "number"
425
+ || typeof entry.ts !== "number"
426
+ || (entry.label !== undefined && typeof entry.label !== "string")
427
+ || (entry.rootWorkflowId !== undefined && typeof entry.rootWorkflowId !== "string")
428
+ || (entry.resumable !== undefined && typeof entry.resumable !== "boolean")) {
429
+ return undefined;
430
+ }
431
+ return {
432
+ type: "workflow.durable.checkpoint",
433
+ workflowId: entry.workflowId,
434
+ name: entry.name,
435
+ inputs: entry.inputs,
436
+ status: entry.status,
437
+ completedCheckpoints: entry.completedCheckpoints,
438
+ pendingPrompts: entry.pendingPrompts,
439
+ ts: entry.ts,
440
+ ...(entry.label !== undefined ? { label: entry.label } : {}),
441
+ ...(entry.rootWorkflowId !== undefined ? { rootWorkflowId: entry.rootWorkflowId } : {}),
442
+ ...(entry.resumable !== undefined ? { resumable: entry.resumable } : {}),
443
+ };
444
+ }
445
+
446
+ function isDurableWorkflowStatus(value: string): value is DurableWorkflowStatus {
447
+ return value === "running"
448
+ || value === "paused"
449
+ || value === "completed"
450
+ || value === "failed"
451
+ || value === "cancelled"
452
+ || value === "blocked";
408
453
  }
409
454
 
410
455
  function dbosStatusToDurable(status: string): DurableWorkflowStatus {
@@ -51,6 +51,14 @@ export interface DbosCheckpointEnvelope extends WorkflowSerializableObject {
51
51
  readonly sessionId?: string;
52
52
  readonly sessionFile?: string;
53
53
  readonly completedAt: number;
54
+ readonly startedAt?: number;
55
+ readonly endedAt?: number;
56
+ readonly durationMs?: number;
57
+ readonly result?: string;
58
+ readonly model?: string;
59
+ readonly fastMode?: boolean;
60
+ readonly attemptedModels?: WorkflowSerializableValue;
61
+ readonly modelAttempts?: WorkflowSerializableValue;
54
62
  }
55
63
 
56
64
  /**
@@ -82,6 +90,14 @@ export function encodeCheckpoint(cp: DurableCheckpoint): DbosCheckpointEnvelope
82
90
  replayKey: s.replayKey,
83
91
  ...(s.sessionId !== undefined ? { sessionId: s.sessionId } : {}),
84
92
  ...(s.sessionFile !== undefined ? { sessionFile: s.sessionFile } : {}),
93
+ ...(s.startedAt !== undefined ? { startedAt: s.startedAt } : {}),
94
+ ...(s.endedAt !== undefined ? { endedAt: s.endedAt } : {}),
95
+ ...(s.durationMs !== undefined ? { durationMs: s.durationMs } : {}),
96
+ ...(s.result !== undefined ? { result: s.result } : {}),
97
+ ...(s.model !== undefined ? { model: s.model } : {}),
98
+ ...(s.fastMode !== undefined ? { fastMode: s.fastMode } : {}),
99
+ ...(s.attemptedModels !== undefined ? { attemptedModels: [...s.attemptedModels] } : {}),
100
+ ...(s.modelAttempts !== undefined ? { modelAttempts: s.modelAttempts as WorkflowSerializableValue } : {}),
85
101
  };
86
102
  }
87
103
 
@@ -144,9 +160,21 @@ function decodeEnvelope(workflowId: string, env: DbosCheckpointEnvelope): Durabl
144
160
  ...(env.hasOutput !== false && env.output !== undefined ? { output: env.output } : {}),
145
161
  ...(env.sessionId !== undefined ? { sessionId: env.sessionId } : {}),
146
162
  ...(env.sessionFile !== undefined ? { sessionFile: env.sessionFile } : {}),
163
+ ...(typeof env.startedAt === "number" ? { startedAt: env.startedAt } : {}),
164
+ ...(typeof env.endedAt === "number" ? { endedAt: env.endedAt } : {}),
165
+ ...(typeof env.durationMs === "number" ? { durationMs: env.durationMs } : {}),
166
+ ...(typeof env.result === "string" ? { result: env.result } : {}),
167
+ ...(typeof env.model === "string" ? { model: env.model } : {}),
168
+ ...(typeof env.fastMode === "boolean" ? { fastMode: env.fastMode } : {}),
169
+ ...(isStringArray(env.attemptedModels) ? { attemptedModels: env.attemptedModels } : {}),
170
+ ...(Array.isArray(env.modelAttempts) ? { modelAttempts: env.modelAttempts as DurableStageCheckpoint["modelAttempts"] } : {}),
147
171
  } as DurableStageCheckpoint;
148
172
  }
149
173
 
174
+
175
+ function isStringArray(value: WorkflowSerializableValue | undefined): value is readonly string[] {
176
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
177
+ }
150
178
  function decodeLegacy(
151
179
  workflowId: string,
152
180
  stepName: string,