diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorPlanPatterns.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorPlanPatterns.kt index fadf9eb5..a7f29b83 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorPlanPatterns.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorPlanPatterns.kt @@ -69,25 +69,38 @@ internal fun mineSuccessfulPlanShapes(events: List, exclude: String } /** - * L0/SYSTEM entry naming the closest matching prior successful plan shape — but only for the PLANNING - * stage (the one producing an `execution_plan`), and only when resemblance clears [MIN_INTENT_OVERLAP] - * so an unrelated run's shape is not mistaken for guidance. + * L0/SYSTEM entry naming the closest matching prior successful plan shape, for the two stages that + * can act on it ([PLAN_SHAPE_CONSUMERS]) and only when resemblance clears [MIN_INTENT_OVERLAP] so an + * 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( sessionId: SessionId, stageConfig: StageConfig, ): List { - val isPlanningStage = stageConfig.produces.any { it.kind.id == "execution_plan" } - if (!isPlanningStage) return emptyList() + val consumerKind = planShapeConsumerKind(stageConfig.produces.map { it.kind.id }) ?: return emptyList() val here = initialIntent(sessionId)?.let(::intentKeywords).orEmpty() if (here.isEmpty()) return emptyList() val best = mineSuccessfulPlanShapes(eventStore.allEvents().toList(), exclude = sessionId.value) .map { it to keywordOverlap(here, it.intentKeywords) } .filter { it.second >= MIN_INTENT_OVERLAP } .maxByOrNull { it.second } ?: return emptyList() - val content = "## 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${best.first.stageSequence.joinToString(" → ")}" + val lead = if (consumerKind == "discovery") { + "## What a prior run of this kind of goal needed\nA prior run with a similar goal completed, " + + "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( ContextEntry( 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? = + 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 val INTENT_STOPWORDS = setOf( "the", "and", "for", "with", "that", "this", "add", "make", "use", "using", "into", "from", "all", diff --git a/core/kernel/src/test/kotlin/com/correx/core/kernel/orchestration/PlanPatternMiningTest.kt b/core/kernel/src/test/kotlin/com/correx/core/kernel/orchestration/PlanPatternMiningTest.kt index 13a0a749..c7f8fcaa 100644 --- a/core/kernel/src/test/kotlin/com/correx/core/kernel/orchestration/PlanPatternMiningTest.kt +++ b/core/kernel/src/test/kotlin/com/correx/core/kernel/orchestration/PlanPatternMiningTest.kt @@ -45,6 +45,14 @@ class PlanPatternMiningTest { 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 fun ev(sid: String, payload: EventPayload) = listOf( StoredEvent(