feat(acr): deliver the prior plan shape to discovery, not only the planner (#305)

Store 3's read side already existed but gated on `produces execution_plan`, so the
one stage #305 is actually about — discovery — still started cold. The gate now keys
on the produced artifact kind being a plan-shape consumer (execution_plan OR
discovery), with a discovery-specific framing: the prior run's stage list read as a
checklist of surfaces this task-family touches, to inspect now rather than at stage 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 13:35:17 +04:00
parent 34895b3d54
commit d52a94e5b2
2 changed files with 39 additions and 8 deletions
@@ -69,25 +69,38 @@ internal fun mineSuccessfulPlanShapes(events: List<StoredEvent>, exclude: String
} }
/** /**
* L0/SYSTEM entry naming the closest matching prior successful plan shape — but only for the PLANNING * L0/SYSTEM entry naming the closest matching prior successful plan shape, for the two stages that
* stage (the one producing an `execution_plan`), and only when resemblance clears [MIN_INTENT_OVERLAP] * can act on it ([PLAN_SHAPE_CONSUMERS]) and only when resemblance clears [MIN_INTENT_OVERLAP] so an
* so an unrelated run's shape is not mistaken for guidance. * unrelated run's shape is not mistaken for guidance.
*
* The planner reuses the shape as structure. **Discovery** reads the same fact for a different
* purpose (#305 §Store 3, "discovery starts warm"): the stage list of a completed run of this
* task-family is the cheapest available statement of what this kind of goal ends up needing, so
* discovery can go look at those areas now instead of finding them out at stage 6. Same mined fact,
* two framings — hence one function with a per-stage lead-in.
*/ */
internal suspend fun SessionOrchestrator.successfulPlanShapeEntries( internal suspend fun SessionOrchestrator.successfulPlanShapeEntries(
sessionId: SessionId, sessionId: SessionId,
stageConfig: StageConfig, stageConfig: StageConfig,
): List<ContextEntry> { ): List<ContextEntry> {
val isPlanningStage = stageConfig.produces.any { it.kind.id == "execution_plan" } val consumerKind = planShapeConsumerKind(stageConfig.produces.map { it.kind.id }) ?: return emptyList()
if (!isPlanningStage) return emptyList()
val here = initialIntent(sessionId)?.let(::intentKeywords).orEmpty() val here = initialIntent(sessionId)?.let(::intentKeywords).orEmpty()
if (here.isEmpty()) return emptyList() if (here.isEmpty()) return emptyList()
val best = mineSuccessfulPlanShapes(eventStore.allEvents().toList(), exclude = sessionId.value) val best = mineSuccessfulPlanShapes(eventStore.allEvents().toList(), exclude = sessionId.value)
.map { it to keywordOverlap(here, it.intentKeywords) } .map { it to keywordOverlap(here, it.intentKeywords) }
.filter { it.second >= MIN_INTENT_OVERLAP } .filter { it.second >= MIN_INTENT_OVERLAP }
.maxByOrNull { it.second } ?: return emptyList() .maxByOrNull { it.second } ?: return emptyList()
val content = "## A plan shape that worked before\nA prior run with a similar goal completed " + val lead = if (consumerKind == "discovery") {
"successfully using this stage sequence — reuse its structure where it fits, adapt where the " + "## What a prior run of this kind of goal needed\nA prior run with a similar goal completed, " +
"goal differs:\n${best.first.stageSequence.joinToString(" → ")}" "and its work broke down into these stages — treat it as a checklist of surfaces this kind " +
"of task ends up touching, and inspect them now rather than discovering them mid-run. It is " +
"evidence from another run, not a scope decision for this one:\n"
} else {
"## A plan shape that worked before\nA prior run with a similar goal completed successfully " +
"using this stage sequence — reuse its structure where it fits, adapt where the goal " +
"differs:\n"
}
val content = lead + best.first.stageSequence.joinToString("")
return listOf( return listOf(
ContextEntry( ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()), id = ContextEntryId(UUID.randomUUID().toString()),
@@ -101,6 +114,16 @@ internal suspend fun SessionOrchestrator.successfulPlanShapeEntries(
) )
} }
/**
* The artifact kind that makes a stage a plan-shape consumer, or null when none of [producedKinds]
* does. Keyed on the artifact kind a stage *produces*, not its stage id, so a workflow can name its
* discovery/planning stages anything.
*/
internal fun planShapeConsumerKind(producedKinds: List<String>): String? =
producedKinds.firstOrNull { it in PLAN_SHAPE_CONSUMERS }
private val PLAN_SHAPE_CONSUMERS = setOf("execution_plan", "discovery")
private const val MIN_INTENT_OVERLAP = 0.34 private const val MIN_INTENT_OVERLAP = 0.34
private val INTENT_STOPWORDS = setOf( private val INTENT_STOPWORDS = setOf(
"the", "and", "for", "with", "that", "this", "add", "make", "use", "using", "into", "from", "all", "the", "and", "for", "with", "that", "this", "add", "make", "use", "using", "into", "from", "all",
@@ -45,6 +45,14 @@ class PlanPatternMiningTest {
assertEquals(0.0, keywordOverlap(a, emptySet())) assertEquals(0.0, keywordOverlap(a, emptySet()))
} }
@Test
fun `discovery and planning stages consume plan shapes, other stages do not`() {
assertEquals("discovery", planShapeConsumerKind(listOf("discovery")))
assertEquals("execution_plan", planShapeConsumerKind(listOf("execution_plan")))
assertEquals(null, planShapeConsumerKind(listOf("dod", "design")))
assertEquals(null, planShapeConsumerKind(emptyList()))
}
private var seq = 0L private var seq = 0L
private fun ev(sid: String, payload: EventPayload) = listOf( private fun ev(sid: String, payload: EventPayload) = listOf(
StoredEvent( StoredEvent(