tenseleyflow/loader / 68fd28c

Browse files

Add pressure-pass clarify reviews

Authored by espadonne
SHA
68fd28c6038d3de538f2b9e3a0e71b851f0895d0
Parents
67878a4
Tree
06f6b37

5 changed files

StatusFile+-
M src/loader/runtime/clarify_strategy.py 239 3
M src/loader/runtime/workflow_lanes.py 30 3
M src/loader/runtime/workflow_policy.py 69 2
M tests/test_clarify_strategy.py 49 0
M tests/test_workflow_policy.py 23 0
src/loader/runtime/clarify_strategy.pymodified
@@ -18,6 +18,22 @@ class ClarifySlot(StrEnum):
1818
     LIKELY_TOUCHPOINTS = "likely_touchpoints"
1919
 
2020
 
21
+class ClarifyStage(StrEnum):
22
+    """High-level interview stage for bounded clarify mode."""
23
+
24
+    INTENT = "intent"
25
+    BOUNDARIES = "boundaries"
26
+    READINESS = "readiness"
27
+
28
+
29
+class ClarifyPressureKind(StrEnum):
30
+    """Which kind of pressure pass the next clarify round should apply."""
31
+
32
+    EXAMPLE = "example"
33
+    TRADEOFF = "tradeoff"
34
+    ASSUMPTION = "assumption"
35
+
36
+
2137
 _DEFAULT_SLOT_ORDER = [
2238
     ClarifySlot.DESIRED_OUTCOME,
2339
     ClarifySlot.NON_GOALS,
@@ -58,6 +74,10 @@ class ClarifyAssessment:
5874
     unresolved_slots: list[ClarifySlot] = field(default_factory=list)
5975
     unresolved_questions: list[str] = field(default_factory=list)
6076
     focus_slot: ClarifySlot | None = None
77
+    stage: ClarifyStage = ClarifyStage.INTENT
78
+    pressure_kind: ClarifyPressureKind | None = None
79
+    pressure_pass_complete: bool = False
80
+    missing_readiness_gates: list[str] = field(default_factory=list)
6181
 
6282
 
6383
 def assess_clarify_snapshot(
@@ -65,6 +85,8 @@ def assess_clarify_snapshot(
6585
     task: str,
6686
     answer: str,
6787
     snapshot: ClarifySnapshot,
88
+    round_index: int = 1,
89
+    pressure_pass_complete: bool = False,
6890
 ) -> ClarifyAssessment:
6991
     """Determine which clarify slots remain unresolved after one round."""
7092
 
@@ -74,6 +96,17 @@ def assess_clarify_snapshot(
7496
     normalized_answer = answer.strip()
7597
     answer_is_short = len(re.findall(r"\w+", normalized_answer)) < 4
7698
     answer_is_broad = _answer_uses_broad_language(normalized_answer)
99
+    effective_pressure_pass_complete = (
100
+        pressure_pass_complete or _answer_demonstrates_pressure_pass(normalized_answer)
101
+    )
102
+    missing_readiness_gates: list[str] = []
103
+
104
+    non_goals_explicit = ClarifySlot.NON_GOALS.value in explicit and bool(
105
+        [item for item in snapshot.non_goals if item.strip()]
106
+    )
107
+    decision_boundaries_explicit = ClarifySlot.DECISION_BOUNDARIES.value in explicit and bool(
108
+        [item for item in snapshot.decision_boundaries if item.strip()]
109
+    )
77110
 
78111
     if not normalized_answer:
79112
         unresolved_questions.append(
@@ -89,7 +122,7 @@ def assess_clarify_snapshot(
89122
         unresolved_questions.append(
90123
             "The desired outcome is still not explicit enough to guide execution."
91124
         )
92
-    if ClarifySlot.NON_GOALS.value not in explicit or any(
125
+    if not non_goals_explicit or any(
93126
         "anything not confirmed" in item.lower() for item in snapshot.non_goals
94127
     ):
95128
         unresolved_slots.append(ClarifySlot.NON_GOALS)
@@ -109,7 +142,7 @@ def assess_clarify_snapshot(
109142
         unresolved_questions.append(
110143
             "Constraints are still too implicit for a safe implementation pass."
111144
         )
112
-    if ClarifySlot.DECISION_BOUNDARIES.value not in explicit:
145
+    if not decision_boundaries_explicit:
113146
         unresolved_slots.append(ClarifySlot.DECISION_BOUNDARIES)
114147
         unresolved_questions.append(
115148
             "Decision boundaries are still too fuzzy for autonomous execution."
@@ -131,14 +164,54 @@ def assess_clarify_snapshot(
131164
         unresolved_questions.append(
132165
             "The clarified scope still uses broad or ambiguous language."
133166
         )
167
+
168
+    if not non_goals_explicit:
169
+        missing_readiness_gates.append("non_goals")
170
+    if not decision_boundaries_explicit:
171
+        missing_readiness_gates.append("decision_boundaries")
172
+    if round_index >= 2 and not effective_pressure_pass_complete:
173
+        missing_readiness_gates.append("pressure_pass")
174
+
175
+    pressure_kind = _choose_pressure_kind(
176
+        round_index=round_index,
177
+        answer_is_broad=answer_is_broad,
178
+        missing_readiness_gates=missing_readiness_gates,
179
+        pressure_pass_complete=effective_pressure_pass_complete,
180
+        unresolved_slots=ordered_slots,
181
+    )
182
+    if pressure_kind == ClarifyPressureKind.EXAMPLE:
183
+        unresolved_questions.append(
184
+            "Loader still needs a concrete example or counterexample before planning."
185
+        )
186
+    elif pressure_kind == ClarifyPressureKind.TRADEOFF:
187
+        unresolved_questions.append(
188
+            "Loader still needs an explicit tradeoff or stop boundary before planning."
189
+        )
190
+    elif pressure_kind == ClarifyPressureKind.ASSUMPTION:
191
+        unresolved_questions.append(
192
+            "Loader still needs one challenged assumption before it should proceed."
193
+        )
194
+
195
+    stage = _resolve_stage(
196
+        unresolved_slots=ordered_slots,
197
+        missing_readiness_gates=missing_readiness_gates,
198
+    )
134199
     return ClarifyAssessment(
135200
         unresolved_slots=ordered_slots,
136201
         unresolved_questions=list(dict.fromkeys(unresolved_questions)),
137202
         focus_slot=ordered_slots[0] if ordered_slots else None,
203
+        stage=stage,
204
+        pressure_kind=pressure_kind,
205
+        pressure_pass_complete=effective_pressure_pass_complete,
206
+        missing_readiness_gates=list(dict.fromkeys(missing_readiness_gates)),
138207
     )
139208
 
140209
 
141
-def build_clarify_question(task: str, focus_slot: ClarifySlot | str | None) -> str:
210
+def build_clarify_question(
211
+    task: str,
212
+    focus_slot: ClarifySlot | str | None,
213
+    pressure_kind: ClarifyPressureKind | str | None = None,
214
+) -> str:
142215
     """Render one targeted question for the current clarify focus slot."""
143216
 
144217
     slot = (
@@ -148,6 +221,93 @@ def build_clarify_question(task: str, focus_slot: ClarifySlot | str | None) -> s
148221
         if focus_slot
149222
         else ClarifySlot.DESIRED_OUTCOME
150223
     )
224
+    pressure = (
225
+        pressure_kind
226
+        if isinstance(pressure_kind, ClarifyPressureKind)
227
+        else ClarifyPressureKind(pressure_kind)
228
+        if pressure_kind
229
+        else None
230
+    )
231
+
232
+    if pressure == ClarifyPressureKind.EXAMPLE:
233
+        prompts = {
234
+            ClarifySlot.DESIRED_OUTCOME: (
235
+                "What is one concrete example of the finished outcome, and one nearby "
236
+                "result that should still count as out of scope?"
237
+            ),
238
+            ClarifySlot.NON_GOALS: (
239
+                "What is one tempting broader change I should avoid even if it seems helpful?"
240
+            ),
241
+            ClarifySlot.ACCEPTANCE_CRITERIA: (
242
+                "What concrete example would prove this is done, and what shortcut "
243
+                "would still be wrong?"
244
+            ),
245
+            ClarifySlot.CONSTRAINTS: (
246
+                "What is one concrete invariant I must preserve, and what change would violate it?"
247
+            ),
248
+            ClarifySlot.DECISION_BOUNDARIES: (
249
+                "Give one example of a choice I may make alone and one example that "
250
+                "should force me to stop and confirm."
251
+            ),
252
+            ClarifySlot.LIKELY_TOUCHPOINTS: (
253
+                "Which file should change first, and which nearby file should I "
254
+                "explicitly leave alone?"
255
+            ),
256
+        }
257
+        return prompts[slot]
258
+
259
+    if pressure == ClarifyPressureKind.TRADEOFF:
260
+        prompts = {
261
+            ClarifySlot.DESIRED_OUTCOME: (
262
+                "What result matters most here, and what broader improvement should I "
263
+                "still avoid chasing?"
264
+            ),
265
+            ClarifySlot.NON_GOALS: (
266
+                "What should stay unchanged even if changing it would make the "
267
+                "implementation easier?"
268
+            ),
269
+            ClarifySlot.ACCEPTANCE_CRITERIA: (
270
+                "What outcome would count as success, and what tempting shortcut "
271
+                "should still count as failure?"
272
+            ),
273
+            ClarifySlot.CONSTRAINTS: (
274
+                "What must stay true even if it makes the change slower or less sweeping?"
275
+            ),
276
+            ClarifySlot.DECISION_BOUNDARIES: (
277
+                "Which decision may I take on my own, and which one should I stop "
278
+                "and confirm before proceeding?"
279
+            ),
280
+            ClarifySlot.LIKELY_TOUCHPOINTS: (
281
+                "Which file should I focus on, and what file or surface should stay unchanged?"
282
+            ),
283
+        }
284
+        return prompts[slot]
285
+
286
+    if pressure == ClarifyPressureKind.ASSUMPTION:
287
+        prompts = {
288
+            ClarifySlot.DESIRED_OUTCOME: (
289
+                "What assumption about the desired outcome am I most likely to get "
290
+                "wrong if I act now?"
291
+            ),
292
+            ClarifySlot.NON_GOALS: (
293
+                "What assumption about scope should I not make without checking first?"
294
+            ),
295
+            ClarifySlot.ACCEPTANCE_CRITERIA: (
296
+                "What assumption about 'done' would be risky to make without your confirmation?"
297
+            ),
298
+            ClarifySlot.CONSTRAINTS: (
299
+                "What assumption about constraints would be unsafe for me to guess?"
300
+            ),
301
+            ClarifySlot.DECISION_BOUNDARIES: (
302
+                "What decision would be risky for me to assume I can make without checking?"
303
+            ),
304
+            ClarifySlot.LIKELY_TOUCHPOINTS: (
305
+                "What assumption about the right touchpoint or file would be most "
306
+                "dangerous if I guessed wrong?"
307
+            ),
308
+        }
309
+        return prompts[slot]
310
+
151311
     prompts = {
152312
         ClarifySlot.DESIRED_OUTCOME: (
153313
             "What concrete outcome should this change achieve when it's done?"
@@ -183,6 +343,30 @@ def describe_clarify_slot(slot: ClarifySlot | str | None) -> str:
183343
     return _SLOT_LABELS[resolved]
184344
 
185345
 
346
+def describe_clarify_stage(stage: ClarifyStage | str | None) -> str:
347
+    """Render a friendly clarify-stage label."""
348
+
349
+    if stage is None:
350
+        return "general"
351
+    resolved = stage if isinstance(stage, ClarifyStage) else ClarifyStage(stage)
352
+    return resolved.value
353
+
354
+
355
+def describe_clarify_pressure_kind(
356
+    pressure_kind: ClarifyPressureKind | str | None,
357
+) -> str:
358
+    """Render a friendly pressure-pass label."""
359
+
360
+    if pressure_kind is None:
361
+        return "none"
362
+    resolved = (
363
+        pressure_kind
364
+        if isinstance(pressure_kind, ClarifyPressureKind)
365
+        else ClarifyPressureKind(pressure_kind)
366
+    )
367
+    return resolved.value
368
+
369
+
186370
 def _prioritize_slots(
187371
     slots: list[ClarifySlot],
188372
     *,
@@ -203,6 +387,35 @@ def _prioritize_slots(
203387
     return ordered
204388
 
205389
 
390
+def _resolve_stage(
391
+    *,
392
+    unresolved_slots: list[ClarifySlot],
393
+    missing_readiness_gates: list[str],
394
+) -> ClarifyStage:
395
+    if missing_readiness_gates:
396
+        return ClarifyStage.READINESS
397
+    if ClarifySlot.DESIRED_OUTCOME in unresolved_slots:
398
+        return ClarifyStage.INTENT
399
+    return ClarifyStage.BOUNDARIES
400
+
401
+
402
+def _choose_pressure_kind(
403
+    *,
404
+    round_index: int,
405
+    answer_is_broad: bool,
406
+    missing_readiness_gates: list[str],
407
+    pressure_pass_complete: bool,
408
+    unresolved_slots: list[ClarifySlot],
409
+) -> ClarifyPressureKind | None:
410
+    if round_index < 2 or pressure_pass_complete or not unresolved_slots:
411
+        return None
412
+    if answer_is_broad:
413
+        return ClarifyPressureKind.EXAMPLE
414
+    if any(gate in {"non_goals", "decision_boundaries"} for gate in missing_readiness_gates):
415
+        return ClarifyPressureKind.TRADEOFF
416
+    return ClarifyPressureKind.ASSUMPTION
417
+
418
+
206419
 def _answer_uses_broad_language(answer: str) -> bool:
207420
     lowered = answer.lower()
208421
     if not lowered:
@@ -217,6 +430,29 @@ def _answer_uses_broad_language(answer: str) -> bool:
217430
             "fix it",
218431
             "something",
219432
             "somehow",
433
+            "maybe",
434
+            "around there",
435
+        )
436
+    )
437
+
438
+
439
+def _answer_demonstrates_pressure_pass(answer: str) -> bool:
440
+    lowered = answer.lower()
441
+    if not lowered:
442
+        return False
443
+    return any(
444
+        phrase in lowered
445
+        for phrase in (
446
+            "do not",
447
+            "don't",
448
+            "keep",
449
+            "leave",
450
+            "unchanged",
451
+            "out of scope",
452
+            "avoid",
453
+            "only",
454
+            "stop and ask",
455
+            "confirm first",
220456
         )
221457
     )
222458
 
src/loader/runtime/workflow_lanes.pymodified
@@ -9,7 +9,13 @@ from pathlib import Path
99
 from typing import Any
1010
 
1111
 from ..llm.base import Message, Role, ToolCall
12
-from .clarify_strategy import ClarifySnapshot, build_clarify_question, describe_clarify_slot
12
+from .clarify_strategy import (
13
+    ClarifySnapshot,
14
+    build_clarify_question,
15
+    describe_clarify_pressure_kind,
16
+    describe_clarify_slot,
17
+    describe_clarify_stage,
18
+)
1319
 from .dod import DefinitionOfDone, DefinitionOfDoneStore
1420
 from .events import AgentEvent, TurnSummary
1521
 from .executor import ToolExecutor
@@ -67,6 +73,10 @@ class WorkflowLaneRunner:
6773
             reason_summary="clarify gathered enough boundaries to proceed",
6874
             unresolved_slots=[],
6975
             focus_slot=None,
76
+            stage="intent",
77
+            pressure_kind=None,
78
+            pressure_pass_complete=False,
79
+            missing_readiness_gates=[],
7080
         )
7181
 
7282
         for round_index in range(1, max_rounds + 1):
@@ -79,6 +89,8 @@ class WorkflowLaneRunner:
7989
                 rounds=rounds,
8090
                 unresolved_questions=review.unresolved_questions,
8191
                 unresolved_slots=review.unresolved_slots,
92
+                stage=review.stage,
93
+                pressure_kind=review.pressure_kind,
8294
             )
8395
             rounds.append((question, answer))
8496
             review = self.workflow_policy.review_clarify(
@@ -87,6 +99,7 @@ class WorkflowLaneRunner:
8799
                 snapshot=self._clarify_snapshot(task, latest_brief),
88100
                 round_index=round_index,
89101
                 max_rounds=max_rounds,
102
+                pressure_pass_complete=review.pressure_pass_complete,
90103
             )
91104
             if review.should_continue:
92105
                 append_timeline(
@@ -301,6 +314,8 @@ class WorkflowLaneRunner:
301314
         rounds: list[tuple[str, str]],
302315
         unresolved_questions: list[str],
303316
         unresolved_slots: list[str],
317
+        stage: str | None,
318
+        pressure_kind: str | None,
304319
     ) -> tuple[ClarifyBrief, str, str]:
305320
         ask_tool = self.agent.registry.get("AskUserQuestion")
306321
         assert ask_tool is not None
@@ -311,6 +326,8 @@ class WorkflowLaneRunner:
311326
                 rounds=rounds,
312327
                 unresolved_questions=unresolved_questions,
313328
                 unresolved_slots=unresolved_slots,
329
+                stage=stage,
330
+                pressure_kind=pressure_kind,
314331
             ),
315332
             tools=[ask_tool.to_schema()],
316333
             max_tokens=500,
@@ -328,6 +345,7 @@ class WorkflowLaneRunner:
328345
                 task,
329346
                 response.content,
330347
                 unresolved_slots,
348
+                pressure_kind,
331349
             )
332350
             title = None
333351
             options = None
@@ -410,6 +428,8 @@ class WorkflowLaneRunner:
410428
         rounds: list[tuple[str, str]],
411429
         unresolved_questions: list[str],
412430
         unresolved_slots: list[str],
431
+        stage: str | None,
432
+        pressure_kind: str | None,
413433
     ) -> str:
414434
         history_lines = []
415435
         for index, (question, answer) in enumerate(rounds, start=1):
@@ -422,13 +442,19 @@ class WorkflowLaneRunner:
422442
         unresolved = "\n".join(f"- {item}" for item in unresolved_questions) or "- none"
423443
         focus_slot = unresolved_slots[0] if unresolved_slots else None
424444
         focus_label = describe_clarify_slot(focus_slot)
445
+        stage_label = describe_clarify_stage(stage)
446
+        pressure_label = describe_clarify_pressure_kind(pressure_kind)
425447
         return (
426448
             "Clarify the task before planning or implementation.\n\n"
427449
             f"Task: {task}\n"
428450
             f"Round: {round_index}\n"
451
+            f"Stage: {stage_label}\n"
429452
             f"Focus slot: {focus_label}\n"
453
+            f"Pressure pass: {pressure_label}\n"
430454
             "Ask exactly one focused question via AskUserQuestion.\n"
431
-            "Use the unresolved questions and prior answers to tighten scope.\n\n"
455
+            "Use the unresolved questions and prior answers to tighten scope.\n"
456
+            "If a pressure pass is active, prefer examples, tradeoffs, or "
457
+            "challenged assumptions over generic restatement.\n\n"
432458
             "Unresolved questions:\n"
433459
             f"{unresolved}\n\n"
434460
             "Prior clarify history:\n"
@@ -516,12 +542,13 @@ class WorkflowLaneRunner:
516542
         task: str,
517543
         response_content: str,
518544
         unresolved_slots: list[str],
545
+        pressure_kind: str | None,
519546
     ) -> str:
520547
         match = re.search(r"([A-Z][^?]+\?)", response_content)
521548
         if match:
522549
             return match.group(1).strip()
523550
         focus_slot = unresolved_slots[0] if unresolved_slots else None
524
-        return build_clarify_question(task, focus_slot)
551
+        return build_clarify_question(task, focus_slot, pressure_kind)
525552
 
526553
     @staticmethod
527554
     def _clarify_snapshot(task: str, brief: ClarifyBrief) -> ClarifySnapshot:
src/loader/runtime/workflow_policy.pymodified
@@ -9,7 +9,12 @@ from enum import StrEnum
99
 from pathlib import Path
1010
 from typing import Any
1111
 
12
-from .clarify_strategy import ClarifySnapshot, assess_clarify_snapshot, describe_clarify_slot
12
+from .clarify_strategy import (
13
+    ClarifySnapshot,
14
+    assess_clarify_snapshot,
15
+    describe_clarify_pressure_kind,
16
+    describe_clarify_slot,
17
+)
1318
 from .workflow_signals import WorkflowSignalExtractor, WorkflowSignalPacket
1419
 
1520
 
@@ -172,6 +177,10 @@ class ClarifyReview:
172177
     unresolved_questions: list[str] = field(default_factory=list)
173178
     unresolved_slots: list[str] = field(default_factory=list)
174179
     focus_slot: str | None = None
180
+    stage: str | None = None
181
+    pressure_kind: str | None = None
182
+    pressure_pass_complete: bool = False
183
+    missing_readiness_gates: list[str] = field(default_factory=list)
175184
 
176185
 
177186
 @dataclass(slots=True)
@@ -498,6 +507,7 @@ class WorkflowPolicy:
498507
         snapshot: ClarifySnapshot,
499508
         round_index: int,
500509
         max_rounds: int,
510
+        pressure_pass_complete: bool = False,
501511
     ) -> ClarifyReview:
502512
         """Determine whether clarify should continue for another round."""
503513
 
@@ -505,12 +515,36 @@ class WorkflowPolicy:
505515
             task=task,
506516
             answer=answer,
507517
             snapshot=snapshot,
518
+            round_index=round_index,
519
+            pressure_pass_complete=pressure_pass_complete,
508520
         )
509521
         unresolved = list(assessment.unresolved_questions)
510522
         focus_slot = assessment.focus_slot.value if assessment.focus_slot else None
511523
         focus_label = describe_clarify_slot(assessment.focus_slot)
524
+        pressure_kind = (
525
+            assessment.pressure_kind.value if assessment.pressure_kind is not None else None
526
+        )
527
+        pressure_label = describe_clarify_pressure_kind(assessment.pressure_kind)
528
+        readiness_gates = list(assessment.missing_readiness_gates)
512529
 
513530
         if unresolved and round_index < max_rounds:
531
+            if assessment.pressure_kind is not None:
532
+                return ClarifyReview(
533
+                    should_continue=True,
534
+                    reason_code="clarify_pressure_pass_required",
535
+                    reason_summary=(
536
+                        "clarify still needs a "
537
+                        f"{pressure_label} pass around {focus_label}"
538
+                    ),
539
+                    unresolved_questions=unresolved,
540
+                    unresolved_slots=[slot.value for slot in assessment.unresolved_slots],
541
+                    focus_slot=focus_slot,
542
+                    stage=assessment.stage.value,
543
+                    pressure_kind=pressure_kind,
544
+                    pressure_pass_complete=assessment.pressure_pass_complete,
545
+                    missing_readiness_gates=readiness_gates,
546
+                )
547
+
514548
             return ClarifyReview(
515549
                 should_continue=True,
516550
                 reason_code="clarify_follow_up_needed",
@@ -521,9 +555,30 @@ class WorkflowPolicy:
521555
                 unresolved_questions=unresolved,
522556
                 unresolved_slots=[slot.value for slot in assessment.unresolved_slots],
523557
                 focus_slot=focus_slot,
558
+                stage=assessment.stage.value,
559
+                pressure_kind=pressure_kind,
560
+                pressure_pass_complete=assessment.pressure_pass_complete,
561
+                missing_readiness_gates=readiness_gates,
524562
             )
525563
 
526564
         if unresolved:
565
+            if not assessment.pressure_pass_complete and round_index >= 2:
566
+                return ClarifyReview(
567
+                    should_continue=False,
568
+                    reason_code="clarify_budget_exhausted_without_pressure_pass",
569
+                    reason_summary=(
570
+                        "clarify budget exhausted before Loader completed a "
571
+                        "bounded pressure pass"
572
+                    ),
573
+                    unresolved_questions=unresolved,
574
+                    unresolved_slots=[slot.value for slot in assessment.unresolved_slots],
575
+                    focus_slot=focus_slot,
576
+                    stage=assessment.stage.value,
577
+                    pressure_kind=pressure_kind,
578
+                    pressure_pass_complete=assessment.pressure_pass_complete,
579
+                    missing_readiness_gates=readiness_gates,
580
+                )
581
+
527582
             return ClarifyReview(
528583
                 should_continue=False,
529584
                 reason_code="clarify_budget_exhausted",
@@ -531,15 +586,27 @@ class WorkflowPolicy:
531586
                 unresolved_questions=unresolved,
532587
                 unresolved_slots=[slot.value for slot in assessment.unresolved_slots],
533588
                 focus_slot=focus_slot,
589
+                stage=assessment.stage.value,
590
+                pressure_kind=pressure_kind,
591
+                pressure_pass_complete=assessment.pressure_pass_complete,
592
+                missing_readiness_gates=readiness_gates,
534593
             )
535594
 
536595
         return ClarifyReview(
537596
             should_continue=False,
538597
             reason_code="clarify_complete",
539
-            reason_summary="clarify gathered enough boundaries to proceed",
598
+            reason_summary=(
599
+                "clarify gathered enough boundaries and completed a bounded pressure pass"
600
+                if assessment.pressure_pass_complete
601
+                else "clarify gathered enough boundaries to proceed"
602
+            ),
540603
             unresolved_questions=[],
541604
             unresolved_slots=[],
542605
             focus_slot=None,
606
+            stage=assessment.stage.value,
607
+            pressure_kind=pressure_kind,
608
+            pressure_pass_complete=assessment.pressure_pass_complete,
609
+            missing_readiness_gates=readiness_gates,
543610
         )
544611
 
545612
     def assess_artifact_freshness(
tests/test_clarify_strategy.pymodified
@@ -3,8 +3,10 @@
33
 from __future__ import annotations
44
 
55
 from loader.runtime.clarify_strategy import (
6
+    ClarifyPressureKind,
67
     ClarifySlot,
78
     ClarifySnapshot,
9
+    ClarifyStage,
810
     assess_clarify_snapshot,
911
     build_clarify_question,
1012
 )
@@ -33,3 +35,50 @@ def test_build_clarify_question_targets_requested_slot() -> None:
3335
 
3436
     assert "out of scope" in question.lower()
3537
 
38
+
39
+def test_assess_clarify_snapshot_requests_tradeoff_pressure_pass_on_later_round() -> None:
40
+    assessment = assess_clarify_snapshot(
41
+        task="Improve Loader runtime behavior.",
42
+        answer="Focus on src/loader/runtime/conversation.py.",
43
+        snapshot=ClarifySnapshot(
44
+            task_statement="Improve Loader runtime behavior.",
45
+            explicit_sections=["desired_outcome", "likely_touchpoints"],
46
+            desired_outcome=["Make the runtime flow more disciplined."],
47
+            likely_touchpoints=["src/loader/runtime/conversation.py"],
48
+        ),
49
+        round_index=2,
50
+    )
51
+
52
+    assert assessment.stage == ClarifyStage.READINESS
53
+    assert assessment.pressure_kind == ClarifyPressureKind.TRADEOFF
54
+    assert assessment.pressure_pass_complete is False
55
+    assert "non_goals" in assessment.missing_readiness_gates
56
+    assert "decision_boundaries" in assessment.missing_readiness_gates
57
+
58
+
59
+def test_assess_clarify_snapshot_marks_pressure_pass_complete_for_boundary_answer() -> None:
60
+    assessment = assess_clarify_snapshot(
61
+        task="Improve Loader runtime behavior.",
62
+        answer="Keep the CLI unchanged and do not broaden the UX without confirming first.",
63
+        snapshot=ClarifySnapshot(
64
+            task_statement="Improve Loader runtime behavior.",
65
+            explicit_sections=["desired_outcome", "non_goals", "decision_boundaries"],
66
+            desired_outcome=["Make the runtime flow more disciplined."],
67
+            non_goals=["Keep the CLI unchanged."],
68
+            decision_boundaries=["Confirm before broad UX changes."],
69
+        ),
70
+        round_index=2,
71
+    )
72
+
73
+    assert assessment.pressure_pass_complete is True
74
+    assert "pressure_pass" not in assessment.missing_readiness_gates
75
+
76
+
77
+def test_build_clarify_question_can_render_pressure_pass_question() -> None:
78
+    question = build_clarify_question(
79
+        "Tighten the runtime behavior.",
80
+        ClarifySlot.NON_GOALS,
81
+        ClarifyPressureKind.TRADEOFF,
82
+    )
83
+
84
+    assert "unchanged" in question.lower() or "avoid" in question.lower()
tests/test_workflow_policy.pymodified
@@ -91,6 +91,29 @@ def test_workflow_policy_requests_follow_up_when_clarify_answer_is_still_ambiguo
9191
     assert review.focus_slot == "likely_touchpoints"
9292
 
9393
 
94
+def test_workflow_policy_requests_pressure_pass_on_later_clarify_round() -> None:
95
+    policy = WorkflowPolicy()
96
+
97
+    review = policy.review_clarify(
98
+        task="Improve Loader runtime behavior.",
99
+        answer="Focus on src/loader/runtime/conversation.py.",
100
+        snapshot=ClarifySnapshot(
101
+            task_statement="Improve Loader runtime behavior.",
102
+            explicit_sections=["desired_outcome", "likely_touchpoints"],
103
+            desired_outcome=["Make the runtime flow more disciplined."],
104
+            likely_touchpoints=["src/loader/runtime/conversation.py"],
105
+        ),
106
+        round_index=2,
107
+        max_rounds=4,
108
+    )
109
+
110
+    assert review.should_continue is True
111
+    assert review.reason_code == "clarify_pressure_pass_required"
112
+    assert review.stage == "readiness"
113
+    assert review.pressure_kind == "tradeoff"
114
+    assert review.pressure_pass_complete is False
115
+
116
+
94117
 def test_workflow_timeline_entry_round_trips() -> None:
95118
     entry = WorkflowTimelineEntry(
96119
         timestamp="2026-04-07T12:00:00Z",