merge: integrate feat/backlog-burndown into master
master had advanced 16 commits past feat/backlog-burndown's base, and the two branches independently built four of the same features. Resolved 26 conflicts. Overlap features — kept master's implementation (more complete / production-wired / more robust), dropped the feature branch's parallel constellation: - llama-server health probe: kept master's event-store-backed tps probe; dropped the branch's LlamaLivenessClient (liveness-only, throughput unwired). - event-store probe: kept master's EventStoreHealthProbe; dropped EventStoreLatencyProbe. - brief echo-back gate: kept master's BriefEchoDiff (Jaccard, tolerates rewording); dropped the branch's exact-set-diff BriefEchoComparator/Extractor. - static-first reviewer: kept master's command/exit-code gate (ProcessStaticAnalysisRunner, wired); dropped the branch's structured-finding static_check stage (no-op seam). Its structured-findings model is filed as a follow-up in BACKLOG. Feature-branch net-new work brought in and kept (master had none): - native task tracking (aggregate, agent tools wired into analyst/implementer/reviewer, dependency graph + gates, decompose, REST/CLI, TUI task board) - critique-outcome producer (role-rel §6 — master had deferred it) - stage-level plan checkpointing (C-A2, folded into runPostStageGates) - CLAUDE.md/AGENTS.md L0 standing context - cross-session grants + TUI (grant scopes/revoke, @ picker, session resume browser) Verified: full Gradle compile (all modules + tests) green; tests pass for core:events, core:kernel, infrastructure:workflow, apps:server, apps:cli, testing:integration; tui-go go build + go test green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import com.correx.core.approvals.ApprovalProjector
|
||||
import com.correx.core.approvals.DefaultApprovalReducer
|
||||
import com.correx.core.approvals.DefaultApprovalRepository
|
||||
import com.correx.core.approvals.domain.DefaultApprovalEngine
|
||||
import com.correx.core.artifacts.DefaultArtifactReducer
|
||||
import com.correx.core.events.events.CritiqueFinding
|
||||
import com.correx.core.events.events.CritiqueFindingsRecordedEvent
|
||||
import com.correx.core.events.events.CritiqueOutcome
|
||||
import com.correx.core.events.events.CritiqueOutcomeCorrelatedEvent
|
||||
import com.correx.core.events.events.CritiqueRole
|
||||
import com.correx.core.events.events.CritiqueSeverity
|
||||
import com.correx.core.events.events.CritiqueVerdict
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.NewEvent
|
||||
import com.correx.core.events.types.EventId
|
||||
import com.correx.core.events.execution.RetryPolicy
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.events.types.TransitionId
|
||||
import com.correx.core.inference.InferenceRepository
|
||||
import com.correx.core.inference.InferenceState
|
||||
import com.correx.core.journal.DecisionJournalProjector
|
||||
import com.correx.core.journal.DefaultDecisionJournalReducer
|
||||
import com.correx.core.journal.DefaultDecisionJournalRepository
|
||||
import com.correx.core.kernel.orchestration.DefaultOrchestrationReducer
|
||||
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
|
||||
import com.correx.core.kernel.orchestration.OrchestrationConfig
|
||||
import com.correx.core.kernel.orchestration.OrchestrationProjector
|
||||
import com.correx.core.kernel.orchestration.OrchestrationRepository
|
||||
import com.correx.core.kernel.orchestration.OrchestratorEngines
|
||||
import com.correx.core.kernel.orchestration.OrchestratorRepositories
|
||||
import com.correx.core.kernel.retry.DefaultRetryCoordinator
|
||||
import com.correx.core.risk.DefaultRiskAssessor
|
||||
import com.correx.core.sessions.DefaultSessionRepository
|
||||
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
|
||||
import com.correx.core.sessions.projections.replay.EventReplayer
|
||||
import com.correx.core.transitions.graph.StageConfig
|
||||
import com.correx.core.transitions.graph.TransitionEdge
|
||||
import com.correx.core.transitions.graph.WorkflowGraph
|
||||
import com.correx.core.validation.pipeline.ValidationPipeline
|
||||
import com.correx.infrastructure.persistence.InMemoryEventStore
|
||||
import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository
|
||||
import com.correx.testing.contracts.fixtures.artifactstore.NoopArtifactStore
|
||||
import com.correx.testing.fixtures.InferenceFixtures
|
||||
import com.correx.testing.fixtures.context.ContextFixtures
|
||||
import com.correx.testing.fixtures.cyclePolicyMissingValidator
|
||||
import com.correx.testing.fixtures.transitions.TransitionFixtures
|
||||
import com.correx.testing.kernel.MockSessionEventReplayer
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.datetime.Clock
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* End-to-end proof that the reviewer-loop runtime correlates recorded critique findings into
|
||||
* outcome events at loop resolution (BACKLOG §B-§6), and that those events feed the
|
||||
* `:core:critique` calibration projection. The LLM-side emission of findings is mocked here by
|
||||
* seeding [CritiqueFindingsRecordedEvent]s directly (that half is a separate model-gated step).
|
||||
*/
|
||||
class CritiqueCalibrationWiringTest {
|
||||
|
||||
private val eventStore = InMemoryEventStore()
|
||||
private val sessionReplayer = MockSessionEventReplayer()
|
||||
private val sessionRepository = DefaultSessionRepository(sessionReplayer)
|
||||
private val orchestrationRepository = OrchestrationRepository(
|
||||
DefaultEventReplayer(eventStore, OrchestrationProjector(DefaultOrchestrationReducer())),
|
||||
)
|
||||
private val inferenceRepository = InferenceRepository(
|
||||
object : EventReplayer<InferenceState> {
|
||||
override fun rebuild(sessionId: SessionId) = InferenceState()
|
||||
},
|
||||
)
|
||||
private val approvalRepository = DefaultApprovalRepository(
|
||||
DefaultEventReplayer(eventStore, ApprovalProjector(DefaultApprovalReducer())),
|
||||
)
|
||||
private val decisionJournalRepository = DefaultDecisionJournalRepository(
|
||||
DefaultEventReplayer(eventStore, DecisionJournalProjector(DefaultDecisionJournalReducer())),
|
||||
)
|
||||
private val retryCoordinator = DefaultRetryCoordinator(eventStore)
|
||||
|
||||
private val repositories = OrchestratorRepositories(
|
||||
eventStore = eventStore,
|
||||
inferenceRepository = inferenceRepository,
|
||||
orchestrationRepository = orchestrationRepository,
|
||||
sessionRepository = sessionRepository,
|
||||
artifactRepository = LiveArtifactRepository(eventStore, DefaultArtifactReducer()),
|
||||
approvalRepository = approvalRepository,
|
||||
)
|
||||
|
||||
private val orchestrator = DefaultSessionOrchestrator(
|
||||
repositories = repositories,
|
||||
engines = OrchestratorEngines(
|
||||
transitionResolver = TransitionFixtures.simpleResolver(),
|
||||
contextPackBuilder = ContextFixtures.simpleBuilder(),
|
||||
inferenceRouter = InferenceFixtures.fixedRouter(),
|
||||
validationPipeline = ValidationPipeline(validators = listOf(cyclePolicyMissingValidator())),
|
||||
approvalEngine = DefaultApprovalEngine(),
|
||||
riskAssessor = DefaultRiskAssessor(),
|
||||
),
|
||||
retryCoordinator = retryCoordinator,
|
||||
artifactStore = NoopArtifactStore(),
|
||||
decisionJournalRepository = decisionJournalRepository,
|
||||
)
|
||||
|
||||
private val defaultConfig = OrchestrationConfig(retryPolicy = RetryPolicy(maxAttempts = 1, backoffMs = 0))
|
||||
|
||||
private fun finding(id: String) = CritiqueFinding(
|
||||
id = id,
|
||||
role = CritiqueRole.CODE_REVIEWER,
|
||||
severity = CritiqueSeverity.MAJOR,
|
||||
category = "correctness",
|
||||
message = "msg-$id",
|
||||
)
|
||||
|
||||
private suspend fun seedReview(sessionId: SessionId, iteration: Int, verdict: CritiqueVerdict, findings: List<CritiqueFinding>) {
|
||||
eventStore.append(
|
||||
NewEvent(
|
||||
EventMetadata(EventId("rev-$iteration"), sessionId, Clock.System.now(), 1, null, null),
|
||||
CritiqueFindingsRecordedEvent(
|
||||
sessionId = sessionId,
|
||||
stageId = StageId("reviewer"),
|
||||
role = CritiqueRole.CODE_REVIEWER,
|
||||
modelHash = "model-A",
|
||||
iteration = iteration,
|
||||
verdict = verdict,
|
||||
findings = findings,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `recorded findings are correlated at completion and feed the calibration projection`(): Unit = runBlocking {
|
||||
val sessionId = SessionId("calib-1")
|
||||
|
||||
// Two review rounds: f1 fixed between them (→ UPHELD); f2 persists to an approved final (→ DISMISSED).
|
||||
seedReview(sessionId, 1, CritiqueVerdict.CHANGES_REQUESTED, listOf(finding("f1"), finding("f2")))
|
||||
seedReview(sessionId, 2, CritiqueVerdict.APPROVED, listOf(finding("f2")))
|
||||
|
||||
val stageA = StageId("A")
|
||||
val done = StageId("done")
|
||||
val graph = WorkflowGraph(
|
||||
id = "calib-wiring",
|
||||
stages = mapOf(stageA to StageConfig(), done to StageConfig()),
|
||||
transitions = setOf(TransitionEdge(id = TransitionId("t1"), from = stageA, to = done, condition = { true })),
|
||||
start = stageA,
|
||||
)
|
||||
|
||||
orchestrator.run(sessionId, graph, defaultConfig)
|
||||
|
||||
// The orchestrator emits one outcome per recorded finding at completion — the events the
|
||||
// :core:critique calibration projection consumes (its folding is covered by that module's
|
||||
// DefaultCriticCalibrationReducerTest).
|
||||
val outcomes = eventStore.read(sessionId).mapNotNull { it.payload as? CritiqueOutcomeCorrelatedEvent }
|
||||
assertEquals(2, outcomes.size, "one outcome per recorded finding")
|
||||
assertEquals(CritiqueOutcome.UPHELD, outcomes.single { it.findingId == "f1" }.outcome)
|
||||
assertEquals(CritiqueOutcome.DISMISSED, outcomes.single { it.findingId == "f2" }.outcome)
|
||||
assertTrue(outcomes.all { it.modelHash == "model-A" && it.role == CritiqueRole.CODE_REVIEWER })
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,13 @@ import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.events.types.TransitionId
|
||||
import com.correx.core.events.events.RepoKnowledgeHit
|
||||
import com.correx.core.kernel.orchestration.buildAgentInstructionsEntry
|
||||
import com.correx.core.kernel.orchestration.buildArtifactKindVocabularyEntry
|
||||
import com.correx.core.kernel.orchestration.buildProjectProfileEntry
|
||||
import com.correx.core.kernel.orchestration.buildRelevantFilesEntry
|
||||
import com.correx.core.kernel.orchestration.buildRetryFeedbackEntry
|
||||
import com.correx.core.kernel.orchestration.criticArtifactIds
|
||||
import com.correx.core.sessions.BoundAgentInstructions
|
||||
import com.correx.core.sessions.BoundProjectProfile
|
||||
import com.correx.core.transitions.graph.StageConfig
|
||||
import com.correx.core.transitions.graph.TransitionEdge
|
||||
@@ -87,6 +89,21 @@ class ContextFeedbackTest {
|
||||
assertEquals("projectProfile", entry.sourceType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `agent instructions render as single L0 entry`() {
|
||||
val entry = buildAgentInstructionsEntry(
|
||||
BoundAgentInstructions(
|
||||
sources = listOf("CLAUDE.md", "AGENTS.md"),
|
||||
content = "# CLAUDE.md\n\nbe careful\n\n# AGENTS.md\n\nuse tools",
|
||||
),
|
||||
)
|
||||
assertEquals(ContextLayer.L0, entry.layer)
|
||||
assertEquals(EntryRole.SYSTEM, entry.role)
|
||||
assertEquals("agentInstructions", entry.sourceType)
|
||||
assertTrue(entry.content.contains("# CLAUDE.md"), "content: ${entry.content}")
|
||||
assertTrue(entry.content.contains("# AGENTS.md"), "content: ${entry.content}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `criticArtifactIds resolves the from-stage produces on a back-edge`() {
|
||||
val graph = graphWith(
|
||||
|
||||
@@ -10,7 +10,9 @@ import com.correx.core.events.events.WorkflowStartedEvent
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.events.types.TransitionId
|
||||
import com.correx.core.events.events.AgentInstructionsBoundEvent
|
||||
import com.correx.core.events.events.ProjectProfileBoundEvent
|
||||
import com.correx.core.sessions.BoundAgentInstructions
|
||||
import com.correx.core.sessions.BoundProjectProfile
|
||||
import com.correx.core.sessions.BoundWorkspace
|
||||
import com.correx.core.sessions.DefaultSessionReducer
|
||||
@@ -327,6 +329,50 @@ class DefaultSessionReducerTest {
|
||||
assertEquals("kernel project", result.boundProjectProfile?.about)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AgentInstructionsBoundEvent reduces into boundAgentInstructions`() {
|
||||
val result = reducer.reduce(
|
||||
state = initialState(),
|
||||
event = stored(
|
||||
sessionId = sessionId,
|
||||
payload = AgentInstructionsBoundEvent(
|
||||
sessionId = sessionId,
|
||||
workspaceRoot = "/repo",
|
||||
sources = listOf("CLAUDE.md", "AGENTS.md"),
|
||||
content = "# CLAUDE.md\n\nbe careful",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
BoundAgentInstructions(
|
||||
sources = listOf("CLAUDE.md", "AGENTS.md"),
|
||||
content = "# CLAUDE.md\n\nbe careful",
|
||||
),
|
||||
result.boundAgentInstructions,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `boundAgentInstructions is preserved by subsequent unrelated events`() {
|
||||
val withInstructions = initialState().copy(
|
||||
boundAgentInstructions = BoundAgentInstructions(
|
||||
sources = listOf("CLAUDE.md"),
|
||||
content = "# CLAUDE.md\n\nrules",
|
||||
),
|
||||
)
|
||||
|
||||
val result = reducer.reduce(
|
||||
state = withInstructions,
|
||||
event = stored(
|
||||
sessionId = sessionId,
|
||||
payload = WorkflowStartedEvent(sessionId, workflowId = "wf", startStageId = StageId("s1")),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(listOf("CLAUDE.md"), result.boundAgentInstructions?.sources)
|
||||
}
|
||||
|
||||
private fun initialState() =
|
||||
SessionState(
|
||||
status = SessionStatus.CREATED
|
||||
|
||||
Reference in New Issue
Block a user