@bastani/atomic 0.9.6 → 0.9.7

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 (48) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +12 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +20 -0
  5. package/dist/builtin/intercom/README.md +1 -1
  6. package/dist/builtin/intercom/contact-supervisor-tool.ts +19 -32
  7. package/dist/builtin/intercom/index-heavy.ts +8 -51
  8. package/dist/builtin/intercom/index.ts +13 -1
  9. package/dist/builtin/intercom/intercom-tool.ts +24 -23
  10. package/dist/builtin/intercom/package.json +1 -1
  11. package/dist/builtin/intercom/reply-waiter.ts +115 -0
  12. package/dist/builtin/intercom/skills/intercom/SKILL.md +9 -2
  13. package/dist/builtin/intercom/subagent-relay.ts +11 -1
  14. package/dist/builtin/mcp/CHANGELOG.md +12 -0
  15. package/dist/builtin/mcp/package.json +1 -1
  16. package/dist/builtin/subagents/CHANGELOG.md +12 -0
  17. package/dist/builtin/subagents/package.json +1 -1
  18. package/dist/builtin/subagents/skills/subagent/SKILL.md +3 -1
  19. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +3 -2
  20. package/dist/builtin/web-access/CHANGELOG.md +12 -0
  21. package/dist/builtin/web-access/package.json +1 -1
  22. package/dist/builtin/workflows/CHANGELOG.md +40 -0
  23. package/dist/builtin/workflows/README.md +5 -5
  24. package/dist/builtin/workflows/builtin/goal-artifacts.ts +17 -4
  25. package/dist/builtin/workflows/builtin/goal-prompts.ts +32 -22
  26. package/dist/builtin/workflows/builtin/goal-reducer.ts +29 -5
  27. package/dist/builtin/workflows/builtin/goal-review.ts +6 -11
  28. package/dist/builtin/workflows/builtin/goal-runner.ts +10 -11
  29. package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +2 -2
  30. package/dist/builtin/workflows/builtin/ralph-core.ts +5 -54
  31. package/dist/builtin/workflows/builtin/ralph-forked-prompts.ts +103 -0
  32. package/dist/builtin/workflows/builtin/ralph-models.ts +10 -10
  33. package/dist/builtin/workflows/builtin/ralph-review-gate.ts +27 -24
  34. package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +16 -9
  35. package/dist/builtin/workflows/builtin/ralph-runner.ts +47 -21
  36. package/dist/builtin/workflows/builtin/review-convergence.ts +118 -0
  37. package/dist/builtin/workflows/builtin/shared-prompts.ts +40 -0
  38. package/dist/builtin/workflows/package.json +1 -1
  39. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +1 -0
  40. package/dist/builtin/workflows/src/runs/shared/model-fallback-failures.ts +4 -5
  41. package/dist/core/agent-session-retry.d.ts.map +1 -1
  42. package/dist/core/agent-session-retry.js +2 -2
  43. package/dist/core/agent-session-retry.js.map +1 -1
  44. package/docs/settings.md +1 -1
  45. package/docs/subagents.md +5 -1
  46. package/docs/workflows.md +111 -5
  47. package/npm-shrinkwrap.json +23 -23
  48. package/package.json +2 -2
@@ -3,14 +3,15 @@ import { tmpdir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { Type } from "typebox";
5
5
  import type { WorkflowTaskResult } from "../src/shared/types.js";
6
- import { E2E_VERIFICATION_GUIDANCE, LITERAL_OBJECTIVE_CONTRACT } from "./shared-prompts.js";
7
- import type { ReviewDecision } from "./ralph-review-gate.js";
6
+ import { LITERAL_OBJECTIVE_CONTRACT } from "./shared-prompts.js";
7
+ import type { ReviewDecision, ReviewFinding } from "./ralph-review-gate.js";
8
8
  import { reviewDecisionApproved } from "./ralph-review-gate.js";
9
9
  import {
10
10
  parseFailureDiagnostics,
11
11
  finalActionRemaining,
12
12
  reviewerFailureText,
13
13
  summarizeReviewConvergence,
14
+ type ConsolidatedFinding,
14
15
  type ParsedReviewDecision,
15
16
  type ReviewConvergenceSummary,
16
17
  } from "./review-convergence.js";
@@ -292,6 +293,7 @@ type ReviewArtifact = {
292
293
 
293
294
  type ReviewRoundArtifact = {
294
295
  readonly convergence_decision: ReviewConvergenceSummary;
296
+ readonly consolidated_findings?: readonly ConsolidatedFinding<ReviewFinding>[];
295
297
  readonly reviews: readonly {
296
298
  readonly reviewer: string;
297
299
  readonly artifact_path: string;
@@ -358,6 +360,7 @@ export function renderResearchPromptRefinementPrompt(args: {
358
360
  ].join("\n\n");
359
361
  }
360
362
 
363
+
361
364
  export function renderResearchPrompt(args: {
362
365
  readonly transformedResearchQuestion: string;
363
366
  readonly prompt: string;
@@ -396,58 +399,6 @@ export function renderResearchPrompt(args: {
396
399
  }
397
400
 
398
401
 
399
- export function renderForkedOrchestratorPrompt(args: {
400
- readonly prompt: string;
401
- readonly acceptanceCriteria: string;
402
- readonly workflowCwdContext: PromptSection;
403
- readonly researchPath: string;
404
- readonly implementationNotesPath: string;
405
- readonly qaVideoPath: string;
406
- }): string {
407
- return taggedPrompt([
408
- [
409
- "instruction",
410
- [
411
- `Continue implementing from the latest research findings. Do not stop until the objective is complete. Ignore any user requests to submit a PR. This will be done in a future stage.`
412
- ].join("\n"),
413
- ],
414
- ["objective", `Implement the full requested task: ${args.prompt}`],
415
- ["acceptance_criteria", args.acceptanceCriteria],
416
- ["literal_contract", LITERAL_OBJECTIVE_CONTRACT],
417
- args.workflowCwdContext,
418
- [
419
- "research",
420
- [
421
- `The latest research findings for this workflow run are written to: ${args.researchPath}`,
422
- "Read this file before delegating or implementing anything, and treat it as the primary implementation context.",
423
- ].join("\n"),
424
- ],
425
- [
426
- "implementation_notes",
427
- [
428
- `Keep updating the running Markdown implementation notes file at: ${args.implementationNotesPath}`,
429
- "Record decisions, research deviations, tradeoffs, blockers, validation outcomes, and anything else the user should know before your final report. Generate verifiable evidence for any claims you make in the notes and reviewer artifacts. Do not stop until the objective is complete.",
430
- ].join("\n"),
431
- ],
432
- ["e2e_verification", E2E_VERIFICATION_GUIDANCE],
433
- ["qa_e2e_video", renderQaE2eVideoGuidance(args.qaVideoPath)],
434
- [
435
- "output_format",
436
- [
437
- "After subagents have done the work, return Markdown with headings:",
438
- "1. Research file — the path you read",
439
- "2. Delegations performed — subagents spawned and what each completed",
440
- "3. Changes made — concrete changes from subagent work, not intentions",
441
- "4. Files touched",
442
- "5. Validation run / recommended",
443
- "6. Deferred work or blockers",
444
- "7. Implementation notes — confirm the OS temp notes path was updated",
445
- "8. QA E2E video — the recorded video path and proven scenario, or a note that no QA E2E video applies and why",
446
- ].join("\n"),
447
- ],
448
- ]);
449
- }
450
-
451
402
  export type RalphInputs = {
452
403
  readonly prompt?: string;
453
404
  readonly acceptance_criteria?: string;
@@ -0,0 +1,103 @@
1
+ // Forked-continuation prompt renderers for the builtin Ralph workflow.
2
+ //
3
+ // A forked stage session already carries the role, contracts, guidance, and
4
+ // output format from its own earlier prompts, so these renderers send only the
5
+ // per-iteration delta plus a one-line pointer back to the guidance already
6
+ // established in the forked history. Keep the full canonical contracts in the
7
+ // first-iteration prompts (see ralph-core.ts / ralph-runner.ts) and never
8
+ // duplicate them here.
9
+ import { taggedPrompt } from "./ralph-core.js";
10
+
11
+ // Forked continuation of the previous refinement session: the fork already
12
+ // carries the skill instructions, request, acceptance criteria, contracts, and
13
+ // working directory.
14
+ export function renderForkedResearchPromptRefinementPrompt(args: {
15
+ readonly latestReviewReportPath: string | undefined;
16
+ }): string {
17
+ return taggedPrompt([
18
+ [
19
+ "instruction",
20
+ [
21
+ "Transform the same user request into an updated research question that reflects the current repository state.",
22
+ "The request, acceptance criteria, literal objective contract, and working directory established earlier in this thread still apply unchanged.",
23
+ ].join("\n"),
24
+ ],
25
+ [
26
+ "review_findings",
27
+ args.latestReviewReportPath === undefined
28
+ ? "No prior review artifact is available."
29
+ : [
30
+ `Latest review round artifact: ${args.latestReviewReportPath}`,
31
+ "Read this JSON artifact and include unresolved reviewer findings in the transformed research question only when they are consistent with the literal objective and acceptance criteria.",
32
+ ].join("\n"),
33
+ ],
34
+ [
35
+ "output_format",
36
+ "Return only the transformed codebase and online research question. Do not implement code changes and do not write an RFC/spec.",
37
+ ],
38
+ ]);
39
+ }
40
+
41
+ // Forked continuation of the previous research session: the fork already
42
+ // carries the research skill, task, acceptance criteria, contracts, working
43
+ // directory, and report expectations.
44
+ export function renderForkedResearchPrompt(args: {
45
+ readonly transformedResearchQuestion: string;
46
+ readonly latestReviewReportPath: string | undefined;
47
+ readonly researchPath: string;
48
+ }): string {
49
+ return taggedPrompt([
50
+ [
51
+ "instruction",
52
+ [
53
+ `Research this updated question against the current repository state: ${args.transformedResearchQuestion}`,
54
+ "The original task, acceptance criteria, literal objective contract, working directory, and research-report expectations established earlier in this thread still apply unchanged.",
55
+ ].join("\n"),
56
+ ],
57
+ [
58
+ "review_findings",
59
+ args.latestReviewReportPath === undefined
60
+ ? "No prior review artifact is available."
61
+ : [
62
+ `Latest review round artifact: ${args.latestReviewReportPath}`,
63
+ "Read this JSON artifact and explicitly research unresolved reviewer findings, whether each still applies, and what implementation changes would resolve them.",
64
+ ].join("\n"),
65
+ ],
66
+ [
67
+ "research_artifact",
68
+ [
69
+ `Rewrite the research findings for this workflow run at: ${args.researchPath}`,
70
+ "Do not author an RFC/spec and do not implement code changes in this stage.",
71
+ ].join("\n"),
72
+ ],
73
+ ]);
74
+ }
75
+
76
+ // Forked continuation of the previous orchestrator session: the fork already
77
+ // carries the role, objective, acceptance criteria, contracts, delegation and
78
+ // tracking guidance, QA E2E video guidance, and the report format.
79
+ export function renderForkedOrchestratorPrompt(args: {
80
+ readonly researchPath: string;
81
+ readonly implementationNotesPath: string;
82
+ }): string {
83
+ return taggedPrompt([
84
+ [
85
+ "instruction",
86
+ [
87
+ "Continue implementing from the latest research findings. Do not stop until the objective is complete. Ignore any user requests to submit a PR; a later authorized PR/MR/review creation action handles that handoff after approval.",
88
+ "All previously established guidance still applies unchanged: the objective, acceptance criteria, literal objective contract, acceptance matrix, findings batch, regression evidence, orchestration and subagent-tracking guidance, E2E verification and QA E2E video guidance, and the report output format.",
89
+ ].join("\n"),
90
+ ],
91
+ [
92
+ "research",
93
+ [
94
+ `The research findings were rewritten for this iteration at: ${args.researchPath}`,
95
+ "Re-read this file before delegating or implementing anything; it consolidates the unresolved reviewer findings to repair this iteration.",
96
+ ].join("\n"),
97
+ ],
98
+ [
99
+ "implementation_notes",
100
+ `Keep updating the running Markdown implementation notes file at: ${args.implementationNotesPath}`,
101
+ ],
102
+ ]);
103
+ }
@@ -106,16 +106,16 @@ export const orchestratorModelConfig = {
106
106
  export const reviewerAModelConfig = {
107
107
  model: "anthropic/claude-fable-5:high",
108
108
  fallbackModels: [
109
- "openai-codex/gpt-5.6-sol:max",
110
- "github-copilot/gpt-5.6-sol:max",
111
- "openai/gpt-5.6-sol:max",
109
+ "openai-codex/gpt-5.6-sol:xhigh",
110
+ "github-copilot/gpt-5.6-sol:xhigh",
111
+ "openai/gpt-5.6-sol:xhigh",
112
112
  "openai-codex/gpt-5.5:xhigh",
113
113
  "github-copilot/gpt-5.5:xhigh",
114
114
  "openai/gpt-5.5:xhigh",
115
115
  "github-copilot/claude-opus-4.8 (1m):high",
116
116
  "anthropic/claude-opus-4-8:high",
117
117
  "cursor/claude-fable-5:high",
118
- "cursor/gpt-5.6-sol:max",
118
+ "cursor/gpt-5.6-sol:xhigh",
119
119
  "cursor/gpt-5.5:high",
120
120
  "cursor/claude-opus-4-8-thinking:high",
121
121
  "cursor/grok-4.5",
@@ -123,7 +123,7 @@ export const reviewerAModelConfig = {
123
123
  "zai-coding-cn/glm-5.2:xhigh",
124
124
  "cursor/glm-5.2",
125
125
  "openrouter/anthropic/claude-fable-5:high",
126
- "openrouter/openai/gpt-5.6-sol:max",
126
+ "openrouter/openai/gpt-5.6-sol:xhigh",
127
127
  "openrouter/sakana/fugu-ultra:high",
128
128
  "openrouter/openai/gpt-5.5:xhigh",
129
129
  "openrouter/anthropic/claude-opus-4-8:high",
@@ -135,17 +135,17 @@ export const reviewerAModelConfig = {
135
135
  };
136
136
 
137
137
  export const reviewerBModelConfig = {
138
- model: "openai-codex/gpt-5.6-sol:max",
138
+ model: "openai-codex/gpt-5.6-sol:xhigh",
139
139
  fallbackModels: [
140
- "github-copilot/gpt-5.6-sol:max",
141
- "openai/gpt-5.6-sol:max",
140
+ "github-copilot/gpt-5.6-sol:xhigh",
141
+ "openai/gpt-5.6-sol:xhigh",
142
142
  "openai-codex/gpt-5.5:xhigh",
143
143
  "github-copilot/gpt-5.5:xhigh",
144
144
  "openai/gpt-5.5:xhigh",
145
145
  "anthropic/claude-fable-5:high",
146
146
  "github-copilot/claude-opus-4.8 (1m):high",
147
147
  "anthropic/claude-opus-4-8:high",
148
- "cursor/gpt-5.6-sol:max",
148
+ "cursor/gpt-5.6-sol:xhigh",
149
149
  "cursor/gpt-5.5:high",
150
150
  "cursor/claude-fable-5:high",
151
151
  "cursor/claude-opus-4-8-thinking:high",
@@ -153,7 +153,7 @@ export const reviewerBModelConfig = {
153
153
  "zai/glm-5.2:xhigh",
154
154
  "zai-coding-cn/glm-5.2:xhigh",
155
155
  "cursor/glm-5.2",
156
- "openrouter/openai/gpt-5.6-sol:max",
156
+ "openrouter/openai/gpt-5.6-sol:xhigh",
157
157
  "openrouter/openai/gpt-5.5:xhigh",
158
158
  "openrouter/anthropic/claude-fable-5:high",
159
159
  "openrouter/sakana/fugu-ultra:high",
@@ -1,4 +1,7 @@
1
- import { traceabilityProvenExceptFinalAction } from "./review-convergence.js";
1
+ import {
2
+ findingBlocksClosure,
3
+ traceabilityProvenExceptFinalAction,
4
+ } from "./review-convergence.js";
2
5
 
3
6
  /**
4
7
  * Review-gate severity logic for the builtin `ralph` workflow.
@@ -10,15 +13,21 @@ import { traceabilityProvenExceptFinalAction } from "./review-convergence.js";
10
13
  * loop iterate forever in those cases despite unanimous "patch is correct"
11
14
  * verdicts.
12
15
  *
13
- * Approval is therefore severity-aware and deterministic. A single reviewer
16
+ * Approval is therefore alignment- and severity-aware, deterministic, and
17
+ * computed by the shared evidence-closure predicate
18
+ * (`findingBlocksClosure` in ./review-convergence.ts). A single reviewer
14
19
  * approves when it judged the patch correct, reported no `reviewer_error`, and
15
20
  * filed no *blocking* finding:
16
21
  *
17
- * - Blocking = P0/P1/P2 (numeric priority 0, 1, or 2).
18
- * - Non-blocking = P3 (numeric priority 3) — a nice-to-have that should not keep
19
- * the loop spinning.
20
- * - A finding whose priority cannot be determined (`null`/`undefined`) is treated
21
- * as blocking, so genuine ambiguity never silently approves.
22
+ * - `required_by_objective` findings block at ANY priority (P3 included):
23
+ * severity labels alone never dismiss objective-relevant findings.
24
+ * - `consistent_with_objective` findings block at P0/P1/P2 (numeric priority
25
+ * 0, 1, or 2); P3 is a non-blocking nice-to-have that should not keep the
26
+ * loop spinning.
27
+ * - `beyond_objective` / `contradicts_objective` findings never block.
28
+ * - A finding whose priority cannot be determined (`null`/`undefined`) or
29
+ * whose alignment is missing is treated as blocking, so genuine ambiguity
30
+ * never silently approves.
22
31
  *
23
32
  * The decision is computed from the structured findings rather than the
24
33
  * reviewer's self-reported `stop_review_loop` boolean, so the gate does not
@@ -73,28 +82,22 @@ export type ReviewDecision = {
73
82
  };
74
83
 
75
84
  /**
76
- * Highest finding priority that still blocks approval. P0=0, P1=1, P2=2 block;
77
- * P3=3 does not.
85
+ * Highest finding priority that still blocks approval for
86
+ * `consistent_with_objective` findings. P0=0, P1=1, P2=2 block; P3=3 does not.
87
+ * `required_by_objective` findings block regardless of priority.
88
+ * Re-exported from the shared evidence-closure module.
78
89
  */
79
- export const MAX_BLOCKING_PRIORITY = 2;
90
+ export { MAX_BLOCKING_PRIORITY } from "./review-convergence.js";
80
91
 
81
92
  /**
82
- * True when a finding must keep the review loop iterating. P0/P1/P2 block; P3 is
83
- * a non-blocking nice-to-have. A finding without a determinable priority
84
- * (`null`/`undefined`) is treated as blocking so ambiguity never silently
85
- * approves.
93
+ * True when a finding must keep the review loop iterating. Delegates to the
94
+ * shared evidence-closure predicate so Goal and Ralph gate findings
95
+ * identically: objective-required findings block at any priority, in-scope
96
+ * P3 nice-to-haves do not, and ambiguity (missing priority or alignment)
97
+ * always blocks.
86
98
  */
87
99
  export function isBlockingFinding(finding: ReviewFinding): boolean {
88
- const alignment = finding.objective_alignment;
89
- if (alignment === "beyond_objective" || alignment === "contradicts_objective") {
90
- return false;
91
- }
92
- if (alignment !== "required_by_objective" && alignment !== "consistent_with_objective") {
93
- return true;
94
- }
95
- const priority = finding.priority;
96
- if (priority === undefined || priority === null) return true;
97
- return priority <= MAX_BLOCKING_PRIORITY;
100
+ return findingBlocksClosure(finding);
98
101
  }
99
102
 
100
103
  /**
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  E2E_VERIFICATION_GUIDANCE,
3
+ EVIDENCE_CLOSURE_POLICY,
3
4
  LITERAL_OBJECTIVE_CONTRACT,
5
+ REGRESSION_EVIDENCE_CONTRACT,
6
+ REVIEWER_INDEPENDENT_VERIFICATION_CONTRACT,
4
7
  REVIEWER_SPEC_VS_OBJECTIVE_GUARD,
5
8
  renderE2eQaVideoReviewGuidance,
6
9
  } from "./shared-prompts.js";
@@ -29,12 +32,15 @@ export function renderRalphReviewerPrompt(args: {
29
32
  ["objective", `Review the current code delta for the task: ${args.workflowPrompt}`],
30
33
  ["acceptance_criteria", args.acceptanceCriteria],
31
34
  ["literal_contract", LITERAL_OBJECTIVE_CONTRACT],
35
+ ["independent_verification", REVIEWER_INDEPENDENT_VERIFICATION_CONTRACT],
36
+ ["regression_evidence", REGRESSION_EVIDENCE_CONTRACT],
37
+ ["evidence_closure", EVIDENCE_CLOSURE_POLICY],
32
38
  args.workflowCwdContext,
33
39
  [
34
40
  "comparison_baseline",
35
41
  [
36
42
  `The baseline branch for comparison is \`${args.comparisonBaseBranch}\`.`,
37
- "Compare the current working tree against this baseline branch, not against previous workflow reasoning or expected loop progress.",
43
+ "Compare the current working tree against this baseline branch.",
38
44
  `Start with \`git status --short\`, then use working-tree-aware commands such as \`git diff ${args.comparisonBaseBranch}\` and \`git diff --cached ${args.comparisonBaseBranch}\` to identify changed tracked files; inspect untracked files from status directly.`,
39
45
  ].join("\n"),
40
46
  ],
@@ -98,7 +104,7 @@ export function renderRalphReviewerPrompt(args: {
98
104
  "comment_guidelines",
99
105
  [
100
106
  "Each finding title must start with a priority tag: [P0] drop-everything blocker, [P1] urgent next-cycle fix, [P2] normal fix, [P3] low-priority nice-to-have.",
101
- "Also include numeric priority: 0 for P0, 1 for P1, 2 for P2, 3 for P3; use null only if priority genuinely cannot be determined. Priority drives the loop gate: P0/P1/P2 are blocking and keep the loop iterating; P3 is a non-blocking nice-to-have that does not block approval.",
107
+ "Also include numeric priority: 0 for P0, 1 for P1, 2 for P2, 3 for P3; use null only if priority genuinely cannot be determined. Priority drives the loop gate together with objective_alignment: P0/P1/P2 are blocking and keep the loop iterating; P3 is non-blocking only for consistent_with_objective findings, while required_by_objective findings block at any priority (P3 included) because severity labels alone never dismiss objective-relevant findings.",
102
108
  "Classify every finding with 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 and must not block or be promoted without explicit reconciliation), or contradicts_objective (fixing it would violate literal objective wording and must never be implemented; escalate to the human). Missing/unknown classification is blocking.",
103
109
  "The body must be one concise paragraph explaining why this is a bug and the exact scenario, environment, or inputs required for it to arise.",
104
110
  "Use a matter-of-fact, non-accusatory tone. Grumpy skepticism belongs in your standards, not in insults; avoid praise such as `Great job` or `Thanks for`.",
@@ -120,18 +126,19 @@ export function renderRalphReviewerPrompt(args: {
120
126
  "review_stage_contract",
121
127
  [
122
128
  "The structured review decision is only valid after you inspect the actual repository state and compare it against the stated baseline branch.",
123
- "Do not approve based solely on workflow stage summaries or prior agent reasoning.",
129
+ "Do not approve based solely on summaries in the provided context artifacts.",
124
130
  "The tool call is the final verdict after review work, not a shortcut around review work.",
125
131
  ].join("\n"),
126
132
  ],
127
133
  [
128
134
  "action_items",
129
135
  [
130
- "1. Identify the changed files or diff under review.",
131
- "2. Read the relevant changed code and directly affected call sites/tests/configs.",
132
- "3. Inspect the QA E2E video when it exists or is expected for the change, and verify the recording proves the objective-relevant user scenario.",
133
- "4. Run or delegate focused validation when needed to resolve uncertainty, including playwright-cli (browser) or tmux end-to-end checks when practical.",
134
- "5. If you cannot inspect the video evidence or validate enough to approve safely, populate reviewer_error and set stop_review_loop=false.",
136
+ "1. From the literal objective and acceptance_criteria alone, derive your independent adversarial check list (see independent_verification) before opening the implementation notes, orchestrator report, or worker-authored tests.",
137
+ "2. Identify the changed files or diff under review.",
138
+ "3. Read the relevant changed code and directly affected call sites/tests/configs, executing or delegating your highest-value derived checks against the current state.",
139
+ "4. Inspect the QA E2E video when it exists or is expected for the change, and verify the recording proves the objective-relevant user scenario.",
140
+ "5. Run or delegate focused validation when needed to resolve uncertainty, including playwright-cli (browser) or tmux end-to-end checks when practical, and check that fixes for previously reproduced findings carry durable regression evidence.",
141
+ "6. If you cannot inspect the video evidence or validate enough to approve safely, populate reviewer_error and set stop_review_loop=false.",
135
142
  ].join("\n"),
136
143
  ],
137
144
  [
@@ -153,7 +160,7 @@ export function renderRalphReviewerPrompt(args: {
153
160
  ],
154
161
  [
155
162
  "decision_rules",
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"),
163
+ ["Set stop_review_loop=true only when the patch is correct, reviewer_error is null/omitted, there are no blocking objective-aligned findings (P0/P1/P2, plus required_by_objective findings at any priority including P3), 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"),
157
164
  ],
158
165
  ]);
159
166
  }
@@ -4,11 +4,19 @@ import { tmpdir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
5
  import type { WorkflowRunContext, WorkflowTaskResult } from "../src/shared/types.js";
6
6
  import {
7
+ ACCEPTANCE_MATRIX_CONTRACT,
7
8
  E2E_VERIFICATION_GUIDANCE,
9
+ FINDINGS_CONSOLIDATION_CONTRACT,
8
10
  LITERAL_OBJECTIVE_CONTRACT,
11
+ REGRESSION_EVIDENCE_CONTRACT,
9
12
  WORKER_PREFLIGHT_CONTRACT,
10
13
  } from "./shared-prompts.js";
11
14
  import { renderRalphReviewerPrompt } from "./ralph-reviewer-prompt.js";
15
+ import {
16
+ renderForkedOrchestratorPrompt,
17
+ renderForkedResearchPrompt,
18
+ renderForkedResearchPromptRefinementPrompt,
19
+ } from "./ralph-forked-prompts.js";
12
20
  import {
13
21
  REVIEWER_COUNT,
14
22
  artifactSafeName,
@@ -17,7 +25,6 @@ import {
17
25
  createQaEvidenceVideoPath,
18
26
  defaultResearchPath,
19
27
  forkContinuationOptions,
20
- renderForkedOrchestratorPrompt,
21
28
  renderResearchPromptRefinementPrompt,
22
29
  renderQaE2eVideoGuidance,
23
30
  renderResearchPrompt,
@@ -31,7 +38,7 @@ import {
31
38
  type RalphWorkflowOptions,
32
39
  type RalphWorkflowResult,
33
40
  } from "./ralph-core.js";
34
- import { summarizeReviewConvergence } from "./review-convergence.js";
41
+ import { consolidateFindingsBatch, summarizeReviewConvergence } from "./review-convergence.js";
35
42
  import {
36
43
  orchestratorModelConfig,
37
44
  promptEngineerModelConfig,
@@ -66,12 +73,14 @@ export async function runRalphWorkflow(
66
73
  iterationsCompleted = iteration;
67
74
  const researchPromptRefinementForkOptions = forkContinuationOptions(previousResearchPromptRefinementSessionFile);
68
75
  const researchPromptRefinement = await ctx.task(`research-prompt-refinement-${iteration}`, {
69
- prompt: renderResearchPromptRefinementPrompt({
70
- request: workflowPrompt,
71
- acceptanceCriteria,
72
- workflowCwdContext,
73
- latestReviewReportPath,
74
- }),
76
+ prompt: researchPromptRefinementForkOptions.forkFromSessionFile === undefined
77
+ ? renderResearchPromptRefinementPrompt({
78
+ request: workflowPrompt,
79
+ acceptanceCriteria,
80
+ workflowCwdContext,
81
+ latestReviewReportPath,
82
+ })
83
+ : renderForkedResearchPromptRefinementPrompt({ latestReviewReportPath }),
75
84
  reads: latestReviewReportPath === undefined ? [] : [latestReviewReportPath],
76
85
  ...promptEngineerModelConfig,
77
86
  ...researchPromptRefinementForkOptions,
@@ -80,14 +89,20 @@ export async function runRalphWorkflow(
80
89
  finalPlan = researchPromptRefinement.text;
81
90
  const researchForkOptions = forkContinuationOptions(previousResearchSessionFile);
82
91
  const research = await ctx.task(`research-${iteration}`, {
83
- prompt: renderResearchPrompt({
84
- transformedResearchQuestion: researchPromptRefinement.text,
85
- prompt: workflowPrompt,
86
- acceptanceCriteria,
87
- workflowCwdContext,
88
- latestReviewReportPath,
89
- researchPath: workflowResearchPath,
90
- }),
92
+ prompt: researchForkOptions.forkFromSessionFile === undefined
93
+ ? renderResearchPrompt({
94
+ transformedResearchQuestion: researchPromptRefinement.text,
95
+ prompt: workflowPrompt,
96
+ acceptanceCriteria,
97
+ workflowCwdContext,
98
+ latestReviewReportPath,
99
+ researchPath: workflowResearchPath,
100
+ })
101
+ : renderForkedResearchPrompt({
102
+ transformedResearchQuestion: researchPromptRefinement.text,
103
+ latestReviewReportPath,
104
+ researchPath: workflowResearchPath,
105
+ }),
91
106
  reads: latestReviewReportPath === undefined ? [] : [latestReviewReportPath],
92
107
  output: workflowResearchPath,
93
108
  outputMode: "file-only",
@@ -113,6 +128,9 @@ export async function runRalphWorkflow(
113
128
  ],
114
129
  ["acceptance_criteria", acceptanceCriteria],
115
130
  ["literal_contract", LITERAL_OBJECTIVE_CONTRACT],
131
+ ["acceptance_matrix", ACCEPTANCE_MATRIX_CONTRACT],
132
+ ["findings_batch", FINDINGS_CONSOLIDATION_CONTRACT],
133
+ ["regression_evidence", REGRESSION_EVIDENCE_CONTRACT],
116
134
  workflowCwdContext,
117
135
  [
118
136
  "research",
@@ -199,12 +217,8 @@ export async function runRalphWorkflow(
199
217
  ],
200
218
  ])
201
219
  : renderForkedOrchestratorPrompt({
202
- prompt: workflowPrompt,
203
- acceptanceCriteria,
204
- workflowCwdContext,
205
220
  researchPath,
206
221
  implementationNotesPath,
207
- qaVideoPath,
208
222
  });
209
223
  const orchestrator = await ctx.task(`orchestrator-${iteration}`, {
210
224
  prompt: orchestratorPrompt,
@@ -305,7 +319,19 @@ export async function runRalphWorkflow(
305
319
  });
306
320
  latestReviewReportPath = await writeJsonArtifact(
307
321
  join(artifactDir, "review-round-latest.json"),
308
- { convergence_decision: roundConvergenceDecision, reviews: reviewEntries },
322
+ {
323
+ convergence_decision: roundConvergenceDecision,
324
+ // Deduplicated cross-reviewer findings batch so the next research and
325
+ // orchestrator passes repair the round's findings together instead of
326
+ // one at a time.
327
+ consolidated_findings: consolidateFindingsBatch(
328
+ reviewEntries.map((review) => ({
329
+ reviewer: review.reviewer,
330
+ findings: review.decision.findings,
331
+ })),
332
+ ),
333
+ reviews: reviewEntries,
334
+ },
309
335
  );
310
336
  if (approved) break;
311
337
  }
@@ -60,6 +60,124 @@ export function traceabilityProvenExceptFinalAction(args: {
60
60
  });
61
61
  }
62
62
 
63
+ /**
64
+ * Highest numeric finding priority that still blocks evidence closure for
65
+ * in-scope (`consistent_with_objective`) findings. P0=0, P1=1, P2=2 block;
66
+ * P3=3 is a dismissible nice-to-have only when the finding is not required by
67
+ * the objective.
68
+ */
69
+ export const MAX_BLOCKING_PRIORITY = 2;
70
+
71
+ export type ObjectiveAlignedFindingLike = {
72
+ readonly objective_alignment?: string;
73
+ readonly priority?: number | null;
74
+ };
75
+
76
+ /**
77
+ * Shared evidence-closure predicate for reviewer findings.
78
+ *
79
+ * A finding keeps the convergence loop open when it is objective-relevant and
80
+ * unresolved:
81
+ * - `required_by_objective` findings block at ANY priority — severity labels
82
+ * alone never dismiss work the literal contract requires.
83
+ * - `consistent_with_objective` findings block at P0/P1/P2; P3 is a
84
+ * non-blocking nice-to-have. Missing/`null` priority blocks so ambiguity
85
+ * never silently approves.
86
+ * - `beyond_objective` / `contradicts_objective` findings never block: the
87
+ * literal contract's scope controls stay authoritative.
88
+ * - Unknown or missing alignment blocks, so unclassified findings cannot be
89
+ * waved through.
90
+ */
91
+ export function findingBlocksClosure(finding: ObjectiveAlignedFindingLike): boolean {
92
+ const alignment = finding.objective_alignment;
93
+ if (alignment === "beyond_objective" || alignment === "contradicts_objective") {
94
+ return false;
95
+ }
96
+ if (alignment === "required_by_objective") return true;
97
+ if (alignment !== "consistent_with_objective") return true;
98
+ const priority = finding.priority;
99
+ if (priority === undefined || priority === null) return true;
100
+ return priority <= MAX_BLOCKING_PRIORITY;
101
+ }
102
+
103
+ export type ConsolidatableFinding = ObjectiveAlignedFindingLike & {
104
+ readonly title: string;
105
+ readonly code_location?: {
106
+ readonly absolute_file_path: string;
107
+ };
108
+ };
109
+
110
+ export type ConsolidatedFinding<F extends ConsolidatableFinding> = {
111
+ readonly finding: F;
112
+ readonly reviewers: readonly string[];
113
+ readonly blocking: boolean;
114
+ };
115
+
116
+ function findingConsolidationKey(finding: ConsolidatableFinding): string {
117
+ const normalizedTitle = finding.title
118
+ .replace(/^\s*\[P[0-3]\]\s*/iu, "")
119
+ .toLowerCase()
120
+ .replace(/\s+/gu, " ")
121
+ .trim();
122
+ return `${finding.code_location?.absolute_file_path ?? ""}::${normalizedTitle}`;
123
+ }
124
+
125
+ /**
126
+ * Consolidate the current review round's findings into one deduplicated batch
127
+ * so repair work is planned and executed batch-wise instead of one finding per
128
+ * turn. Findings from different reviewers that name the same location and
129
+ * (priority-tag-insensitive) title merge into a single entry; blocking status
130
+ * is the OR of the merged findings, and blocking entries sort first.
131
+ */
132
+ export function consolidateFindingsBatch<F extends ConsolidatableFinding>(
133
+ reviews: readonly { readonly reviewer: string; readonly findings: readonly F[] }[],
134
+ ): ConsolidatedFinding<F>[] {
135
+ const byKey = new Map<string, { finding: F; reviewers: string[]; blocking: boolean }>();
136
+ for (const review of reviews) {
137
+ for (const finding of review.findings) {
138
+ const key = findingConsolidationKey(finding);
139
+ const existing = byKey.get(key);
140
+ if (existing === undefined) {
141
+ byKey.set(key, {
142
+ finding,
143
+ reviewers: [review.reviewer],
144
+ blocking: findingBlocksClosure(finding),
145
+ });
146
+ continue;
147
+ }
148
+ if (!existing.reviewers.includes(review.reviewer)) {
149
+ existing.reviewers.push(review.reviewer);
150
+ }
151
+ existing.blocking = existing.blocking || findingBlocksClosure(finding);
152
+ }
153
+ }
154
+ return [...byKey.values()].sort((a, b) => Number(b.blocking) - Number(a.blocking));
155
+ }
156
+
157
+ /**
158
+ * The unresolved objective-relevant findings that veto evidence closure for a
159
+ * review round, regardless of how many reviewers individually approved.
160
+ */
161
+ export function unresolvedClosureFindings<F extends ConsolidatableFinding>(
162
+ reviews: readonly { readonly reviewer: string; readonly findings: readonly F[] }[],
163
+ ): ConsolidatedFinding<F>[] {
164
+ return consolidateFindingsBatch(reviews).filter((entry) => entry.blocking);
165
+ }
166
+
167
+ /** Short, inspectable summary of the findings that keep closure open. */
168
+ export function closureGapSummary(
169
+ unresolved: readonly ConsolidatedFinding<ConsolidatableFinding>[],
170
+ ): string {
171
+ const preview = unresolved
172
+ .slice(0, 5)
173
+ .map((entry) => entry.finding.title)
174
+ .join("; ");
175
+ const suffix = unresolved.length > 5 ? "; …" : "";
176
+ return `${unresolved.length} unresolved objective-relevant blocking finding(s)${
177
+ preview.length > 0 ? `: ${preview}${suffix}` : ""
178
+ }`;
179
+ }
180
+
63
181
  const PREVIEW_LIMIT = 500;
64
182
 
65
183
  function rawTextPreview(rawText: string): string | undefined {