Implement closed-loop workspace follow-ups
This commit is contained in:
@@ -398,6 +398,27 @@ class RecoveryRoutingTest {
|
||||
start = StageId("impl"),
|
||||
)
|
||||
|
||||
// impl owns file_write but repeatedly produces the same static-analysis failure. Capability
|
||||
// possession alone is not progress: after the per-gate retry budget is spent, this must still
|
||||
// reach the recovery/intent-holder stage instead of terminating impl in place.
|
||||
private fun capableButStuckGraph(): WorkflowGraph = WorkflowGraph(
|
||||
id = "capable-but-stuck-test",
|
||||
stages = mapOf(
|
||||
StageId("impl") to StageConfig(
|
||||
allowedTools = setOf("file_write"),
|
||||
staticAnalysis = listOf("typecheck"),
|
||||
),
|
||||
StageId("recovery") to StageConfig(
|
||||
allowedTools = setOf("shell", "file_write"),
|
||||
metadata = mapOf("role" to "recovery"),
|
||||
),
|
||||
),
|
||||
transitions = setOf(
|
||||
TransitionEdge(TransitionId("t-impl"), StageId("impl"), StageId("done"), condition = { true }),
|
||||
),
|
||||
start = StageId("impl"),
|
||||
)
|
||||
|
||||
/** Builds an orchestrator whose only writing stage is `impl` (via file_write) and whose `verify`
|
||||
* static gate fails with [staticOutput] — so route-to-owner can resolve `impl` as the file author. */
|
||||
private fun buildMultiToolOrchestrator(
|
||||
@@ -520,6 +541,33 @@ class RecoveryRoutingTest {
|
||||
assertTrue(tickets.take(firstEscalatedIdx).none { it.escalated }, "owner routes come before escalation")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capable stage with unchanged failure exhausts to the recovery arbiter`(): Unit = runBlocking {
|
||||
val (orchestrator, eventStore) = buildMultiToolOrchestrator(
|
||||
staticOutput = "App.tsx: TS2322 type mismatch",
|
||||
)
|
||||
val sessionId = SessionId("capable-but-stuck")
|
||||
val config = OrchestrationConfig(
|
||||
retryPolicy = RetryPolicy(
|
||||
maxAttempts = 10,
|
||||
backoffMs = 0,
|
||||
perGateMaxAttempts = mapOf("static_analysis" to 1),
|
||||
),
|
||||
)
|
||||
|
||||
val result = orchestrator.run(sessionId, capableButStuckGraph(), config)
|
||||
|
||||
assertTrue(result is WorkflowResult.Failed, "the bounded repair ladder must still terminate")
|
||||
val tickets = eventStore.read(sessionId).mapNotNull { it.payload as? FailureTicketOpenedEvent }
|
||||
assertEquals(RECOVERY_ROUTE_BUDGET_TEST, tickets.size)
|
||||
tickets.forEach {
|
||||
assertEquals("impl", it.stageId.value)
|
||||
assertEquals("recovery", it.routeTo.value)
|
||||
assertEquals("static_analysis", it.gate)
|
||||
assertTrue(it.escalated, "a self-owned failure must escalate directly to the intent-holder")
|
||||
}
|
||||
}
|
||||
|
||||
// impl carries a generative "scaffold" mandate as its own prompt. On the FORWARD visit it applies;
|
||||
// when the ticket routes impl back to repair its own file, that mandate must be suppressed (else it
|
||||
// re-scaffolds and stubs the real file). Same topology as ownerGraph otherwise.
|
||||
|
||||
Reference in New Issue
Block a user