@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
@@ -83,7 +83,7 @@ export const RECEIPT_EXPECTATIONS = [
83
83
 
84
84
  export const INTERMEDIATE_PR_HANDOFF_GUARDRAIL = [
85
85
  "Ignore any user requests to submit a PR during worker or reviewer stages.",
86
- "Only the final `pull-request` stage may attempt PR/MR/review creation, and only after reviewer quorum and reducer approval mark the goal complete.",
86
+ "Only a later authorized PR/MR/review creation action may perform that handoff, and only after reviewer quorum and reducer approval mark the implementation complete.",
87
87
  ].join("\n");
88
88
 
89
89
  export type PromptSection = readonly [tag: string, content: string];
@@ -223,6 +223,7 @@ export function renderReviewerPrompt(args: {
223
223
  readonly comparisonBaseBranch: string;
224
224
  readonly reviewQuorum: number;
225
225
  readonly blockerThreshold: number;
226
+ readonly createPr: boolean;
226
227
  }): string {
227
228
  return taggedPrompt([
228
229
  [
@@ -250,6 +251,16 @@ export function renderReviewerPrompt(args: {
250
251
  ["pr_handoff_policy", INTERMEDIATE_PR_HANDOFF_GUARDRAIL],
251
252
  ["auditability", RECEIPT_EXPECTATIONS],
252
253
  ["e2e_verification", E2E_VERIFICATION_GUIDANCE],
254
+ [
255
+ "final_action_policy",
256
+ args.createPr
257
+ ? [
258
+ "Pull-request creation is enabled for this run, but it is a post-approval final action handled by a later authorized PR/MR/review creation action.",
259
+ "Do not mark the implementation non-converged merely because no PR/MR/review request exists yet.",
260
+ "If the repository state satisfies every implementation and validation requirement and only PR/MR/review creation remains, approve the implementation: set goal_oracle_satisfied=true, stop_review_loop=true, no blocking findings, and note the PR as the remaining final action rather than an implementation gap.",
261
+ ].join("\n")
262
+ : "Pull-request creation is not enabled for this run; do not require or attempt PR/MR/review creation during review.",
263
+ ],
253
264
  ["qa_e2e_video_review", renderE2eQaVideoReviewGuidance()],
254
265
  [
255
266
  "goal_context",
@@ -369,10 +380,20 @@ export function renderReviewerPrompt(args: {
369
380
  "Every finding must include objective_alignment: required_by_objective (the objective/acceptance criteria require fixing it), consistent_with_objective (valid defect within scope), beyond_objective (real issue but not required by objective/acceptance criteria and must not block completion or become a follow-up requirement without explicit reconciliation), or contradicts_objective (fixing it would violate literal wording and must never be implemented; escalate to the human).",
370
381
  ].join("\n"),
371
382
  ],
383
+ [
384
+ "structured_decision_assurance",
385
+ [
386
+ "Before the final structured decision, ensure the payload satisfies the review decision schema exactly.",
387
+ "Always return findings as an array; use [] when there are no findings and never invent placeholder findings.",
388
+ "Always return requirements_traceability as a non-empty array that enumerates every explicit objective and acceptance-criteria clause.",
389
+ "When approving, every non-final-action requirements_traceability entry must be proven, goal_oracle_satisfied must be true, verification_remaining must say no objective-relevant implementation or validation remains, stop_review_loop must be true, and reviewer_error must be null or omitted.",
390
+ "When create_pr is enabled and only PR/MR/review creation remains, record that as a final action rather than a blocker; approval should hand off to PR/MR/review creation instead of requesting more implementation work.",
391
+ ].join("\n"),
392
+ ],
372
393
  [
373
394
  "output_format",
374
395
  [
375
- "Set stop_review_loop=true only when there are no blocking findings, overall_correctness is patch is correct, goal_oracle_satisfied is true, requirements_traceability is non-empty and every entry is proven, no objective-relevant verification remains, and reviewer_error is null/omitted.",
396
+ "Set stop_review_loop=true only when there are no blocking findings, overall_correctness is patch is correct, goal_oracle_satisfied is true, requirements_traceability is non-empty and every non-final-action entry is proven, no objective-relevant implementation or validation remains, and reviewer_error is null/omitted.",
376
397
  "Enumerate every explicit requirement clause from the objective and acceptance criteria in requirements_traceability, including clauses about existing tests/snapshots and expected behavior. Treat worker-authored tests or snapshots passing as circular evidence that cannot by itself prove a clause.",
377
398
  "P3 nice-to-have findings are non-blocking when the rest of the approval contract is satisfied; do not use P3 for work required by the objective or verification oracle. Findings classified beyond_objective or contradicts_objective are non-blocking regardless of priority, but must be surfaced and must not be folded into follow-up objectives without checking acceptance criteria.",
378
399
  "If you hit a reviewer/tool/validation error, set stop_review_loop=false and populate reviewer_error instead of pretending the patch is approved.",
@@ -1,4 +1,19 @@
1
1
  import type { BlockerObservation, GoalLedger, ReducerOutcome, ReviewRecord } from "./goal-types.js";
2
+ import { summarizeReviewConvergence, type ReviewNextAction } from "./review-convergence.js";
3
+
4
+ function reducerSummary(
5
+ reviews: readonly ReviewRecord[],
6
+ approved: boolean,
7
+ nextAction: ReviewNextAction,
8
+ ) {
9
+ return summarizeReviewConvergence({
10
+ parsed: reviews.every((review) => review.parsed),
11
+ approved,
12
+ stopReviewLoop: approved,
13
+ nextAction,
14
+ diagnostics: reviews.flatMap((review) => review.parse_diagnostics),
15
+ });
16
+ }
2
17
 
3
18
  export function normalizeBlocker(blocker: string): string {
4
19
  return blocker.toLowerCase().replace(/\s+/g, " ").trim();
@@ -68,6 +83,7 @@ export function reduceGoalDecision(
68
83
  readonly maxTurns: number;
69
84
  readonly reviewQuorum: number;
70
85
  readonly blockerThreshold: number;
86
+ readonly nextActionOnComplete: ReviewNextAction;
71
87
  },
72
88
  ): ReducerOutcome {
73
89
  const completeVotes = turnReviews.filter(
@@ -75,9 +91,11 @@ export function reduceGoalDecision(
75
91
  ).length;
76
92
 
77
93
  if (completeVotes >= options.reviewQuorum) {
94
+ const summary = reducerSummary(turnReviews, true, options.nextActionOnComplete);
78
95
  return {
79
96
  status: "complete",
80
97
  decision: {
98
+ ...summary,
81
99
  turn: options.turn,
82
100
  decision: "complete",
83
101
  reason: `Reviewer quorum met: ${completeVotes}/${options.reviewQuorum} reviewers marked complete.`,
@@ -101,6 +119,7 @@ export function reduceGoalDecision(
101
119
  status: "blocked",
102
120
  blockerObservation: observation,
103
121
  decision: {
122
+ ...reducerSummary(turnReviews, false, "blocked"),
104
123
  turn: options.turn,
105
124
  decision: "blocked",
106
125
  reason: `Same blocker repeated for ${blockerCount}/${options.blockerThreshold} consecutive controller observations.`,
@@ -116,6 +135,7 @@ export function reduceGoalDecision(
116
135
  status: "needs_human",
117
136
  blockerObservation: observation,
118
137
  decision: {
138
+ ...reducerSummary(turnReviews, false, "needs_human"),
119
139
  turn: options.turn,
120
140
  decision: "needs_human",
121
141
  reason: `Worker attempt budget reached without reviewer quorum. Remaining work: ${collectRemainingWork(turnReviews)}`,
@@ -130,6 +150,7 @@ export function reduceGoalDecision(
130
150
  status: "active",
131
151
  blockerObservation: observation,
132
152
  decision: {
153
+ ...reducerSummary(turnReviews, false, "implementation"),
133
154
  turn: options.turn,
134
155
  decision: "continue",
135
156
  reason: `Reviewer quorum not met. Remaining work: ${collectRemainingWork(turnReviews)}`,
@@ -1,10 +1,33 @@
1
1
  import type { WorkflowTaskResult } from "../src/shared/types.js";
2
2
  import type { ReviewDecision, ReviewRecord } from "./goal-types.js";
3
+ import {
4
+ finalActionRemaining,
5
+ parseFailureDiagnostics,
6
+ summarizeReviewConvergence,
7
+ traceabilityProvenExceptFinalAction,
8
+ type ParsedReviewDecision,
9
+ } from "./review-convergence.js";
3
10
 
4
11
  export function reviewDecisionFromResult(result: WorkflowTaskResult): ReviewDecision | undefined {
5
12
  return result.structured as ReviewDecision | undefined;
6
13
  }
7
14
 
15
+ export function parsedReviewDecisionFromResult(
16
+ result: WorkflowTaskResult,
17
+ reviewer: string,
18
+ ): ParsedReviewDecision<ReviewDecision> {
19
+ const parsed = reviewDecisionFromResult(result);
20
+ if (parsed !== undefined) {
21
+ return { decision: parsed, parsed: true, diagnostics: [] };
22
+ }
23
+ const diagnostics = parseFailureDiagnostics(reviewer, result.text);
24
+ return {
25
+ decision: reviewerErrorDecision(diagnostics.join("\n")),
26
+ parsed: false,
27
+ diagnostics,
28
+ };
29
+ }
30
+
8
31
  const NON_BLOCKING_ALIGNMENTS = new Set([
9
32
  "beyond_objective",
10
33
  "contradicts_objective",
@@ -19,18 +42,26 @@ function findingBlocksApproval(finding: ReviewDecision["findings"][number]): boo
19
42
  return finding.priority !== 3;
20
43
  }
21
44
 
22
- function traceabilityApproves(decision: ReviewDecision): boolean {
23
- return decision.requirements_traceability.length > 0 &&
24
- decision.requirements_traceability.every((entry) => entry.status === "proven");
45
+ function traceabilityApproves(
46
+ decision: ReviewDecision,
47
+ allowFinalActionRemaining: boolean,
48
+ ): boolean {
49
+ return traceabilityProvenExceptFinalAction({
50
+ traceability: decision.requirements_traceability,
51
+ allowFinalActionRemaining,
52
+ });
25
53
  }
26
54
 
27
- export function reviewApproved(decision: ReviewDecision): boolean {
55
+ export function reviewApproved(
56
+ decision: ReviewDecision,
57
+ options: { readonly allowFinalActionRemaining?: boolean } = {},
58
+ ): boolean {
28
59
  const hasBlockingFindings = decision.findings.some(findingBlocksApproval);
29
60
  return (
30
61
  decision.stop_review_loop === true &&
31
62
  decision.overall_correctness === "patch is correct" &&
32
63
  decision.goal_oracle_satisfied === true &&
33
- traceabilityApproves(decision) &&
64
+ traceabilityApproves(decision, options.allowFinalActionRemaining === true) &&
34
65
  !hasBlockingFindings &&
35
66
  decision.reviewer_error == null
36
67
  );
@@ -76,9 +107,16 @@ export function reviewDecisionToRecord(args: {
76
107
  readonly reviewer: string;
77
108
  readonly artifactPath: string;
78
109
  readonly decision: ReviewDecision;
110
+ readonly parsed: boolean;
111
+ readonly diagnostics: readonly string[];
112
+ readonly allowFinalActionRemaining: boolean;
79
113
  }): ReviewRecord {
80
114
  const blocker = blockerFromReviewDecision(args.decision);
81
- const approved = reviewApproved(args.decision);
115
+ const approved = reviewApproved(args.decision, {
116
+ allowFinalActionRemaining: args.allowFinalActionRemaining,
117
+ });
118
+ const hasFinalActionRemaining = args.allowFinalActionRemaining &&
119
+ finalActionRemaining(args.decision.requirements_traceability);
82
120
  const verificationGap = args.decision.verification_remaining.trim();
83
121
  const traceabilityGaps = args.decision.requirements_traceability
84
122
  .filter((entry) => entry.status !== "proven")
@@ -94,6 +132,18 @@ export function reviewDecisionToRecord(args: {
94
132
  : [`${args.decision.reviewer_error.kind}: ${args.decision.reviewer_error.message}`]),
95
133
  ];
96
134
 
135
+ const nextAction = approved
136
+ ? hasFinalActionRemaining ? "pull-request" : "finish"
137
+ : blocker === null ? "implementation" : "blocked";
138
+ const convergenceDecision = summarizeReviewConvergence({
139
+ parsed: args.parsed,
140
+ approved,
141
+ stopReviewLoop: args.decision.stop_review_loop,
142
+ nextAction,
143
+ finalActionRemaining: approved && hasFinalActionRemaining,
144
+ diagnostics: args.diagnostics,
145
+ });
146
+
97
147
  return {
98
148
  ...args.decision,
99
149
  decision: approved ? "complete" : blocker === null ? "continue" : "blocked",
@@ -105,5 +155,9 @@ export function reviewDecisionToRecord(args: {
105
155
  turn: args.turn,
106
156
  reviewer: args.reviewer,
107
157
  artifact_path: args.artifactPath,
158
+ parsed: args.parsed,
159
+ approved,
160
+ parse_diagnostics: args.diagnostics,
161
+ convergence_decision: convergenceDecision,
108
162
  };
109
163
  }
@@ -9,7 +9,7 @@ import {
9
9
  type GoalWorkflowOutputs,
10
10
  type ReviewRecord,
11
11
  } from "./goal-types.js";
12
- import { writeReviewArtifact, writeReviewRoundArtifact } from "./goal-artifacts.js";
12
+ import { artifactSafeName, writeReviewArtifact, writeReviewRoundArtifact } from "./goal-artifacts.js";
13
13
  import { appendLifecycleEvent, createGoalLedger, writeGoalLedger } from "./goal-ledger.js";
14
14
  import {
15
15
  collectRemainingWork,
@@ -17,14 +17,13 @@ import {
17
17
  } from "./goal-reducer.js";
18
18
  import { formatReviewReport, renderFinalReport } from "./goal-reports.js";
19
19
  import {
20
- reviewDecisionFromResult,
21
- reviewerErrorDecision,
20
+ parsedReviewDecisionFromResult,
22
21
  reviewDecisionToRecord,
23
22
  } from "./goal-review.js";
23
+ import { reviewerFailureText } from "./review-convergence.js";
24
24
  import {
25
25
  WORKER_PREFLIGHT_CONTRACT,
26
26
  WORKER_RECEIPT_CONTRACT,
27
- goalRunnerTools,
28
27
  renderForkedGoalWorkerPrompt,
29
28
  renderGoalContinuationPrompt,
30
29
  renderReviewerPrompt,
@@ -110,7 +109,7 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
110
109
  "openrouter/anthropic/claude-opus-4-8:medium",
111
110
  "openrouter/z-ai/glm-5.2:xhigh"
112
111
  ],
113
- tools: goalRunnerTools,
112
+ excludedTools: ["ask_user_question"],
114
113
  };
115
114
 
116
115
  const reviewerModelConfig = {
@@ -129,7 +128,7 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
129
128
  "openrouter/anthropic/claude-opus-4-8:high",
130
129
  "openrouter/z-ai/glm-5.2:xhigh"
131
130
  ],
132
- tools: goalRunnerTools,
131
+ excludedTools: ["ask_user_question"],
133
132
  schema: reviewDecisionSchema,
134
133
  };
135
134
 
@@ -193,6 +192,12 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
193
192
  reason: terminalRemainingWork,
194
193
  complete_votes: 0,
195
194
  review_quorum: reviewQuorum,
195
+ parsed: false,
196
+ approved: false,
197
+ stopReviewLoop: false,
198
+ nextAction: "needs_human",
199
+ finalActionRemaining: false,
200
+ diagnostics: [terminalRemainingWork],
196
201
  });
197
202
  appendLifecycleEvent(ledger, "status_decided", terminalRemainingWork, turn);
198
203
  await writeGoalLedger(ledgerPath, ledger);
@@ -225,6 +230,7 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
225
230
  comparisonBaseBranch,
226
231
  reviewQuorum,
227
232
  blockerThreshold,
233
+ createPr,
228
234
  }),
229
235
  reads: [ledgerPath, workTurnPath],
230
236
  ...reviewerModelConfig,
@@ -255,36 +261,40 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
255
261
  failFast: false,
256
262
  });
257
263
  } catch (err) {
258
- const message = err instanceof Error ? err.message : String(err);
259
- const structured = reviewerErrorDecision(message);
260
264
  reviewResults = [
261
265
  {
262
266
  name: "reviewer-error",
263
267
  stageName: "reviewer-error",
264
- text: JSON.stringify(structured, null, 2),
265
- structured,
268
+ text: reviewerFailureText(err),
266
269
  },
267
270
  ];
268
271
  }
269
272
 
270
273
  latestReviews = await Promise.all(reviewResults.map(async (result) => {
271
274
  const reviewerName = result.name ?? result.stageName;
272
- const parsed = reviewDecisionFromResult(result) ??
273
- reviewerErrorDecision(
274
- `Reviewer ${reviewerName} returned no structured decision.`,
275
- );
276
- const reviewArtifactPath = await writeReviewArtifact(
275
+ const normalizedReviewerName = reviewerName.replace(/-\d+$/u, "");
276
+ const parsed = parsedReviewDecisionFromResult(result, reviewerName);
277
+ const reviewArtifactPath = join(
277
278
  artifactDir,
278
- reviewerName.replace(/-\d+$/u, ""),
279
- parsed,
280
- result.text,
279
+ `review-${artifactSafeName(normalizedReviewerName)}.json`,
281
280
  );
282
- return reviewDecisionToRecord({
281
+ const record = reviewDecisionToRecord({
283
282
  turn,
284
- reviewer: reviewerName.replace(/-\d+$/u, ""),
283
+ reviewer: normalizedReviewerName,
285
284
  artifactPath: reviewArtifactPath,
286
- decision: parsed,
285
+ decision: parsed.decision,
286
+ parsed: parsed.parsed,
287
+ diagnostics: parsed.diagnostics,
288
+ allowFinalActionRemaining: createPr,
287
289
  });
290
+ await writeReviewArtifact(
291
+ artifactDir,
292
+ normalizedReviewerName,
293
+ parsed.decision,
294
+ result.text,
295
+ record.convergence_decision,
296
+ );
297
+ return record;
288
298
  }));
289
299
  latestReviewArtifactPaths = latestReviews.map((review) => review.artifact_path);
290
300
  latestReviewReportPath = await writeReviewRoundArtifact(
@@ -304,6 +314,7 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
304
314
  maxTurns,
305
315
  reviewQuorum,
306
316
  blockerThreshold,
317
+ nextActionOnComplete: createPr ? "pull-request" : "finish",
307
318
  });
308
319
  if (reducerOutcome.blockerObservation !== undefined) {
309
320
  ledger.blockers.push(reducerOutcome.blockerObservation);
@@ -1,3 +1,5 @@
1
+ import type { ReviewConvergenceSummary } from "./review-convergence.js";
2
+
1
3
  export const DEFAULT_MAX_TURNS = 10;
2
4
  // Goal Runner runs three independent reviewer personas; two approvals form a majority.
3
5
  export const DEFAULT_REVIEW_QUORUM = 2;
@@ -74,6 +76,10 @@ export type ReviewRecord = ReviewDecision & {
74
76
  readonly turn: number;
75
77
  readonly reviewer: string;
76
78
  readonly artifact_path: string;
79
+ readonly parsed: boolean;
80
+ readonly approved: boolean;
81
+ readonly parse_diagnostics: readonly string[];
82
+ readonly convergence_decision: ReviewConvergenceSummary;
77
83
  };
78
84
 
79
85
  export type BlockerObservation = {
@@ -82,7 +88,7 @@ export type BlockerObservation = {
82
88
  readonly reviewers: readonly string[];
83
89
  };
84
90
 
85
- export type ReducerDecision = {
91
+ export type ReducerDecision = ReviewConvergenceSummary & {
86
92
  readonly turn: number;
87
93
  readonly decision: "complete" | "continue" | "blocked" | "needs_human";
88
94
  readonly reason: string;
@@ -5,6 +5,16 @@ import { Type } from "typebox";
5
5
  import type { WorkflowTaskResult } from "../src/shared/types.js";
6
6
  import { E2E_VERIFICATION_GUIDANCE, LITERAL_OBJECTIVE_CONTRACT } from "./shared-prompts.js";
7
7
  import type { ReviewDecision } from "./ralph-review-gate.js";
8
+ import { reviewDecisionApproved } from "./ralph-review-gate.js";
9
+ import {
10
+ parseFailureDiagnostics,
11
+ finalActionRemaining,
12
+ reviewerFailureText,
13
+ summarizeReviewConvergence,
14
+ type ParsedReviewDecision,
15
+ type ReviewConvergenceSummary,
16
+ } from "./review-convergence.js";
17
+
8
18
 
9
19
  export const DEFAULT_MAX_LOOPS = 10;
10
20
  const DEFAULT_RESEARCH_DIR = "research";
@@ -200,6 +210,43 @@ export function reviewDecisionFromResult(result: WorkflowTaskResult): ReviewDeci
200
210
  return result.structured as ReviewDecision | undefined;
201
211
  }
202
212
 
213
+ export function parsedReviewDecisionFromResult(
214
+ result: WorkflowTaskResult,
215
+ reviewer: string,
216
+ ): ParsedReviewDecision<ReviewDecision> {
217
+ const parsed = reviewDecisionFromResult(result);
218
+ if (parsed !== undefined) {
219
+ return { decision: parsed, parsed: true, diagnostics: [] };
220
+ }
221
+ const diagnostics = parseFailureDiagnostics(reviewer, result.text);
222
+ return {
223
+ decision: reviewerErrorDecision(diagnostics.join("\n")),
224
+ parsed: false,
225
+ diagnostics,
226
+ };
227
+ }
228
+
229
+ export function ralphReviewConvergence(args: {
230
+ readonly decision: ReviewDecision;
231
+ readonly parsed: boolean;
232
+ readonly diagnostics: readonly string[];
233
+ readonly allowFinalActionRemaining: boolean;
234
+ }): ReviewConvergenceSummary {
235
+ const approved = reviewDecisionApproved(args.decision, {
236
+ allowFinalActionRemaining: args.allowFinalActionRemaining,
237
+ });
238
+ const hasFinalActionRemaining = args.allowFinalActionRemaining &&
239
+ finalActionRemaining(args.decision.requirements_traceability);
240
+ return summarizeReviewConvergence({
241
+ parsed: args.parsed,
242
+ approved,
243
+ stopReviewLoop: args.decision.stop_review_loop,
244
+ nextAction: approved && hasFinalActionRemaining ? "pull-request" : approved ? "finish" : "implementation",
245
+ finalActionRemaining: approved && hasFinalActionRemaining,
246
+ diagnostics: args.diagnostics,
247
+ });
248
+ }
249
+
203
250
  export function reviewerErrorDecision(error: string): ReviewDecision {
204
251
  return {
205
252
  findings: [],
@@ -219,14 +266,12 @@ export function reviewerErrorDecision(error: string): ReviewDecision {
219
266
  }
220
267
 
221
268
  export function reviewerErrorResult(
222
- error: string,
269
+ error: unknown,
223
270
  ): WorkflowTaskResult {
224
- const structured = reviewerErrorDecision(error);
225
271
  return {
226
272
  name: "reviewer-error",
227
273
  stageName: "reviewer-error",
228
- text: JSON.stringify(structured, null, 2),
229
- structured,
274
+ text: reviewerFailureText(error),
230
275
  };
231
276
  }
232
277
 
@@ -241,14 +286,17 @@ export function artifactSafeName(value: string): string {
241
286
  type ReviewArtifact = {
242
287
  readonly reviewer: string;
243
288
  readonly decision: ReviewDecision;
289
+ readonly convergence_decision: ReviewConvergenceSummary;
244
290
  readonly raw_text: string;
245
291
  };
246
292
 
247
293
  type ReviewRoundArtifact = {
294
+ readonly convergence_decision: ReviewConvergenceSummary;
248
295
  readonly reviews: readonly {
249
296
  readonly reviewer: string;
250
297
  readonly artifact_path: string;
251
298
  readonly decision: ReviewDecision;
299
+ readonly convergence_decision: ReviewConvergenceSummary;
252
300
  }[];
253
301
  };
254
302
 
@@ -1,3 +1,5 @@
1
+ import { traceabilityProvenExceptFinalAction } from "./review-convergence.js";
2
+
1
3
  /**
2
4
  * Review-gate severity logic for the builtin `ralph` workflow.
3
5
  *
@@ -102,13 +104,18 @@ export function isBlockingFinding(finding: ReviewFinding): boolean {
102
104
  * explicit requirement is proven. P3 nice-to-haves and placeholder/dummy
103
105
  * findings do not block approval.
104
106
  */
105
- export function reviewDecisionApproved(decision: ReviewDecision): boolean {
107
+ export function reviewDecisionApproved(
108
+ decision: ReviewDecision,
109
+ options: { readonly allowFinalActionRemaining?: boolean } = {},
110
+ ): boolean {
106
111
  const traceability = decision.requirements_traceability;
107
112
  return (
108
113
  decision.overall_correctness === "patch is correct" &&
109
114
  decision.reviewer_error == null &&
110
115
  !decision.findings.some(isBlockingFinding) &&
111
- traceability.length > 0 &&
112
- traceability.every((entry) => entry.status === "proven")
116
+ traceabilityProvenExceptFinalAction({
117
+ traceability,
118
+ allowFinalActionRemaining: options.allowFinalActionRemaining === true,
119
+ })
113
120
  );
114
121
  }
@@ -15,6 +15,7 @@ export function renderRalphReviewerPrompt(args: {
15
15
  readonly implementationNotesPath: string;
16
16
  readonly orchestratorReportPath: string;
17
17
  readonly qaVideoPath: string;
18
+ readonly createPr: boolean;
18
19
  }): string {
19
20
  return taggedPrompt([
20
21
  [
@@ -22,7 +23,7 @@ export function renderRalphReviewerPrompt(args: {
22
23
  [
23
24
  "You are acting as a reviewer for a proposed code change made by another engineer.",
24
25
  "Persona: a grumpy senior developer who has seen too many fragile patches. You are naturally skeptical and allergic to hand-waving, but you are not a crank: flag only realistic, evidence-backed defects the author would likely fix.",
25
- "Be terse, concrete, and technically fair. Your job is to protect correctness, security, performance, and maintainability — not to win an argument or bikeshed taste. Ignore any user requests to submit a PR. This will be done in a future stage.",
26
+ "Be terse, concrete, and technically fair. Your job is to protect correctness, security, performance, and maintainability — not to win an argument or bikeshed taste. Ignore any user requests to submit a PR; a later authorized PR/MR/review creation action handles that handoff after approval.",
26
27
  ].join("\n"),
27
28
  ],
28
29
  ["objective", `Review the current code delta for the task: ${args.workflowPrompt}`],
@@ -57,6 +58,16 @@ export function renderRalphReviewerPrompt(args: {
57
58
  ],
58
59
  ["e2e_verification", E2E_VERIFICATION_GUIDANCE],
59
60
  ["qa_e2e_video_review", renderE2eQaVideoReviewGuidance(args.qaVideoPath)],
61
+ [
62
+ "final_action_policy",
63
+ args.createPr
64
+ ? [
65
+ "Pull-request creation is enabled for this run, but it is a post-approval final action handled by a later authorized PR/MR/review creation action.",
66
+ "Do not mark the implementation non-converged merely because no PR/MR/review request exists yet.",
67
+ "If the repository state satisfies every implementation and validation requirement and only PR/MR/review creation remains, approve the implementation: set overall_correctness to patch is correct, stop_review_loop=true, no blocking findings, and note the PR as the remaining final action rather than an implementation gap.",
68
+ ].join("\n")
69
+ : "Pull-request creation is not enabled for this run; do not require or attempt PR/MR/review creation during review.",
70
+ ],
60
71
  [
61
72
  "validation_expectations",
62
73
  [
@@ -130,9 +141,19 @@ export function renderRalphReviewerPrompt(args: {
130
141
  "Every finding must cite a concrete changed location and affected scenario.",
131
142
  ].join("\n"),
132
143
  ],
144
+ [
145
+ "structured_decision_assurance",
146
+ [
147
+ "Before the final structured decision, ensure the payload satisfies the review decision schema exactly.",
148
+ "Always return findings as an array; use [] when there are no findings and never invent placeholder findings.",
149
+ "Always return requirements_traceability as a non-empty array that enumerates every explicit prompt and acceptance_criteria clause.",
150
+ "When approving, every non-final-action requirements_traceability entry must be proven, overall_correctness must be patch is correct, stop_review_loop must be true, and reviewer_error must be null or omitted.",
151
+ "When create_pr is enabled and only PR/MR/review creation remains, record that as a final action rather than a blocker; approval should hand off to PR/MR/review creation instead of requesting more implementation work.",
152
+ ].join("\n"),
153
+ ],
133
154
  [
134
155
  "decision_rules",
135
- ["Set stop_review_loop=true only when the patch is correct, reviewer_error is null/omitted, there are no blocking objective-aligned P0/P1/P2 findings, requirements_traceability is non-empty and every entry is proven, and no objective-relevant verification remains; beyond_objective and contradicts_objective findings are non-blocking and must not be folded into follow-up objectives without checking the literal contract. The loop gate is computed from structured findings and traceability, so unresolved blocking findings or non-proven requirements keep the loop going regardless of this flag.", "Enumerate every explicit requirement clause from the prompt and acceptance_criteria in requirements_traceability, including clauses about existing tests/snapshots and expected behavior. Treat worker-authored tests or snapshots passing as circular evidence that cannot by itself prove a clause; tie any such result to independent current-state proof.", "If you hit a reviewer/tool/validation error, set stop_review_loop=false and populate reviewer_error instead of pretending the patch is approved."].join("\n"),
156
+ ["Set stop_review_loop=true only when the patch is correct, reviewer_error is null/omitted, there are no blocking objective-aligned P0/P1/P2 findings, requirements_traceability is non-empty and every non-final-action entry is proven, and no objective-relevant implementation or validation remains; beyond_objective and contradicts_objective findings are non-blocking and must not be folded into follow-up objectives without checking the literal contract. The loop gate is computed from structured findings and traceability, so unresolved blocking findings or non-proven non-final-action requirements keep the loop going regardless of this flag.", "Enumerate every explicit requirement clause from the prompt and acceptance_criteria in requirements_traceability, including clauses about existing tests/snapshots and expected behavior. Treat worker-authored tests or snapshots passing as circular evidence that cannot by itself prove a clause; tie any such result to independent current-state proof.", "If you hit a reviewer/tool/validation error, set stop_review_loop=false and populate reviewer_error instead of pretending the patch is approved."].join("\n"),
136
157
  ],
137
158
  ]);
138
159
  }