@@ -92,6 +92,28 @@ class ClarifyRepoFact: |
| 92 | 92 | return f"`{self.path}`: {self.summary}" |
| 93 | 93 | |
| 94 | 94 | |
| 95 | +@dataclass(slots=True) |
| 96 | +class ClarifyBriefHints: |
| 97 | + """Grounded hints that can strengthen a persisted clarify brief.""" |
| 98 | + |
| 99 | + likely_touchpoints: list[str] = field(default_factory=list) |
| 100 | + constraints: list[str] = field(default_factory=list) |
| 101 | + assumptions: list[str] = field(default_factory=list) |
| 102 | + acceptance_criteria: list[str] = field(default_factory=list) |
| 103 | + |
| 104 | + def has_content(self) -> bool: |
| 105 | + """Return whether any grounded hint is available.""" |
| 106 | + |
| 107 | + return any( |
| 108 | + ( |
| 109 | + self.likely_touchpoints, |
| 110 | + self.constraints, |
| 111 | + self.assumptions, |
| 112 | + self.acceptance_criteria, |
| 113 | + ) |
| 114 | + ) |
| 115 | + |
| 116 | + |
| 95 | 117 | @dataclass(slots=True) |
| 96 | 118 | class ClarifyGrounding: |
| 97 | 119 | """Cheap workspace evidence that clarify mode can reference.""" |
@@ -191,6 +213,81 @@ class ClarifyGrounding: |
| 191 | 213 | return self.prompt_block() |
| 192 | 214 | return "\n".join(lines) |
| 193 | 215 | |
| 216 | + def brief_hints(self) -> ClarifyBriefHints: |
| 217 | + """Return grounded hints for clarify brief synthesis and fallback repair.""" |
| 218 | + |
| 219 | + primary_path = self.primary_touchpoint() |
| 220 | + secondary_path = self.secondary_touchpoint() |
| 221 | + primary_fact = self.primary_fact() |
| 222 | + secondary_fact = self.secondary_fact() |
| 223 | + |
| 224 | + likely_touchpoints = [ |
| 225 | + path for path in [primary_path, secondary_path] if path is not None |
| 226 | + ][:2] |
| 227 | + |
| 228 | + constraints: list[str] = [] |
| 229 | + if primary_path is not None: |
| 230 | + constraints.append( |
| 231 | + f"Keep the primary implementation scoped to `{primary_path}` " |
| 232 | + "unless evidence requires a wider edit." |
| 233 | + ) |
| 234 | + if secondary_path is not None: |
| 235 | + constraints.append( |
| 236 | + f"Preserve existing behavior in `{secondary_path}` unless the user broadens scope." |
| 237 | + ) |
| 238 | + |
| 239 | + assumptions: list[str] = [] |
| 240 | + for fact in [primary_fact, secondary_fact]: |
| 241 | + if fact is None: |
| 242 | + continue |
| 243 | + assumptions.append( |
| 244 | + f"Workspace evidence: `{fact.path}` currently contains `{fact.summary}`." |
| 245 | + ) |
| 246 | + |
| 247 | + acceptance_criteria: list[str] = [] |
| 248 | + if primary_path is not None: |
| 249 | + acceptance_criteria.append( |
| 250 | + f"Primary work stays scoped to `{primary_path}`." |
| 251 | + ) |
| 252 | + if secondary_path is not None: |
| 253 | + acceptance_criteria.append( |
| 254 | + f"Nearby surface `{secondary_path}` stays unchanged unless " |
| 255 | + "the user confirms otherwise." |
| 256 | + ) |
| 257 | + |
| 258 | + return ClarifyBriefHints( |
| 259 | + likely_touchpoints=likely_touchpoints, |
| 260 | + constraints=constraints, |
| 261 | + assumptions=assumptions, |
| 262 | + acceptance_criteria=acceptance_criteria, |
| 263 | + ) |
| 264 | + |
| 265 | + def brief_prompt_block(self) -> str: |
| 266 | + """Render brief-oriented grounding hints for the brief synthesis prompt.""" |
| 267 | + |
| 268 | + hints = self.brief_hints() |
| 269 | + if not hints.has_content(): |
| 270 | + return self.prompt_block() |
| 271 | + |
| 272 | + lines: list[str] = [] |
| 273 | + if hints.likely_touchpoints: |
| 274 | + lines.append( |
| 275 | + "- Seed likely touchpoints: " + ", ".join(hints.likely_touchpoints) |
| 276 | + ) |
| 277 | + if hints.constraints: |
| 278 | + lines.append( |
| 279 | + "- Preserve constraints: " + "; ".join(hints.constraints) |
| 280 | + ) |
| 281 | + if hints.assumptions: |
| 282 | + lines.append( |
| 283 | + "- Grounded assumptions: " + "; ".join(hints.assumptions) |
| 284 | + ) |
| 285 | + if hints.acceptance_criteria: |
| 286 | + lines.append( |
| 287 | + "- Scope acceptance criteria: " + "; ".join(hints.acceptance_criteria) |
| 288 | + ) |
| 289 | + return "\n".join(lines) |
| 290 | + |
| 194 | 291 | def primary_touchpoint(self) -> str | None: |
| 195 | 292 | """Return the best available repo anchor for a focused question.""" |
| 196 | 293 | |