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:
2026-06-26 11:30:41 +00:00
259 changed files with 17681 additions and 674 deletions
@@ -17,6 +17,12 @@ private const val TERMINAL = "done"
class ExecutionPlanCompiler(
private val registry: ArtifactKindRegistry,
// Names of every registered tool. A stage that references a tool the runtime can't resolve
// is dropped silently at run time (mapNotNull), so the stage runs toolless — invisible
// without a live run. Validating here turns that into a compile-time rejection (which the
// freestyle driver feeds back for a retry). Empty = the caller didn't supply the tool
// universe, so validation is skipped (preserves the bare `ExecutionPlanCompiler(registry)`).
private val knownTools: Set<String> = emptySet(),
) {
private val mapper = JsonMapper.builder()
.addModule(kotlinModule())
@@ -27,6 +33,7 @@ class ExecutionPlanCompiler(
val plan = runCatching { mapper.readValue<ExecutionPlanModel>(planJson) }
.getOrElse { throw WorkflowValidationException("Failed to parse execution_plan JSON: ${it.message}") }
if (plan.stages.isEmpty()) throw WorkflowValidationException("execution_plan has no stages")
validateTools(plan)
val stageMap = plan.stages.associate { s ->
// `produces` is the unique slot name referenced by needs/edges; `kind` selects the
@@ -34,10 +41,20 @@ class ExecutionPlanCompiler(
val kindId = s.kind ?: s.produces
val kind = registry.get(kindId)
?: throw WorkflowValidationException("Unknown artifact kind '$kindId' in stage '${s.id}'")
// Write-guard manifest = static per-stage globs UNION the plan-derived set (BACKLOG
// §B-§2). Plan stages carry no separate static `writes` glob list today, so the static
// baseline here is empty and the plan-derived paths form the manifest; the union is kept
// explicit so a future static source widens rather than replaces. Sorted for a
// deterministic, replay-stable manifest.
val writeManifest = PlanDerivedManifest.combine(
staticManifest = emptyList(),
planDerived = PlanDerivedManifest.deriveAllowedWrites(s),
).sorted()
StageId(s.id) to StageConfig(
produces = listOf(TypedArtifactSlot(name = ArtifactId(s.produces), kind = kind)),
needs = s.needs.map { ArtifactId(it) }.toSet(),
allowedTools = s.tools.toSet(),
writeManifest = writeManifest,
metadata = mapOf("promptInline" to s.prompt),
)
}
@@ -71,6 +88,22 @@ class ExecutionPlanCompiler(
return WorkflowGraph(id = workflowId, stages = stageMap, transitions = transitions, start = start)
}
/**
* Every tool a stage names must be a registered tool, else the runtime silently drops it
* (resolve → null → mapNotNull) and the stage runs without it. Skipped when [knownTools] is
* empty (the caller didn't supply the tool universe).
*/
private fun validateTools(plan: ExecutionPlanModel) {
if (knownTools.isEmpty()) return
val offenders = plan.stages.flatMap { s -> s.tools.filter { it !in knownTools }.map { s.id to it } }
if (offenders.isEmpty()) return
val detail = offenders.joinToString(", ") { (stage, tool) -> "'$tool' in stage '$stage'" }
throw WorkflowValidationException(
"execution_plan references unknown tool(s): $detail — valid tools: " +
knownTools.sorted().joinToString(", "),
)
}
/**
* A field-equals edge can only ever fire if the producing stage's kind schema declares
* that field — the kind's schema is also the LLM response format, so an undeclared field
@@ -15,6 +15,12 @@ data class PlanStage(
val kind: String? = null,
val needs: List<String> = emptyList(),
val tools: List<String> = emptyList(),
// Workspace-relative file paths/globs this stage declares it will write (BACKLOG §B-§2).
// Drives the plan-derived diff manifest: these are unioned with the static per-stage
// write globs to form the enforced write-guard manifest. Absent/empty = no plan-derived
// contribution (the static manifest, if any, still applies). The planner emits this under
// the JSON key `writes`.
val writes: List<String> = emptyList(),
)
data class PlanEdge(
@@ -0,0 +1,48 @@
package com.correx.infrastructure.workflow
/**
* Plan-derived diff manifest (BACKLOG §B-§2).
*
* The write-guard manifest enforced by the plane-2 `ManifestContainmentRule` was originally a
* STATIC per-stage `writes = [globs]` set (role-reliability §2). This object DERIVES the
* allowed-write set for a stage from the confirmed `impl_plan` artifact instead — the file
* paths/globs a [PlanStage] declares it produces/touches — so the guard tracks the actual locked
* plan rather than only hand-written globs.
*
* The derived set is not a replacement: it AUGMENTS the static baseline. [combine] unions the two,
* and the resulting set is fed verbatim into `StageConfig.writeManifest`, so plan-derived paths are
* matched with the SAME glob/containment semantics the static manifest already uses — no parallel
* matcher is introduced.
*
* ## Extraction rule
* Write targets are read from [PlanStage.writes]: the workspace-relative file paths/globs the
* plan-stage explicitly declares it will write. `produces`/`kind`/`needs` name artifact *slots*
* (logical outputs), not filesystem paths, so they are deliberately NOT treated as write targets.
* Derivation is deterministic and lenient: a stage with no [PlanStage.writes] yields an empty set,
* blank/whitespace entries are dropped, and duplicates collapse.
*/
object PlanDerivedManifest {
/**
* The set of workspace-relative paths/globs [planStage] is permitted to write, derived from its
* declared [PlanStage.writes]. Lenient: a stage with no declared write targets yields the empty
* set (the static manifest, if any, still governs). Deterministic for a given input.
*/
fun deriveAllowedWrites(planStage: PlanStage): Set<String> =
planStage.writes
.map { it.trim() }
.filter { it.isNotEmpty() }
.toSet()
/**
* Unions the static per-stage write globs with the plan-derived set. A write is allowed if it
* matches EITHER source. Blank entries are dropped so the combined set carries only matchable
* globs. The static path is never narrowed — the result only ever widens the static baseline by
* the plan-derived paths.
*/
fun combine(staticManifest: Collection<String>, planDerived: Set<String>): Set<String> =
(staticManifest + planDerived)
.map { it.trim() }
.filter { it.isNotEmpty() }
.toSet()
}
@@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class ExecutionPlanCompilerTest {
@@ -70,6 +71,37 @@ class ExecutionPlanCompilerTest {
assertEquals(setOf("patch"), reviewStage.needs.map { it.value }.toSet())
}
@Test
fun `plan-declared writes are derived into the stage write manifest`() {
val plan = """
{
"goal": "implement a feature",
"stages": [
{
"id": "impl",
"prompt": "Implement it",
"produces": "patch",
"needs": [],
"tools": ["file_write"],
"writes": ["core/feature/**", "docs/feature.md"]
}
],
"edges": [
{ "from": "impl", "to": "done", "condition": { "type": "always_true" } }
]
}
""".trimIndent()
val graph = compiler.compile(plan, "manifest-workflow")
val stage = graph.stages.values.single()
assertEquals(setOf("core/feature/**", "docs/feature.md"), stage.writeManifest.toSet())
}
@Test
fun `a stage without declared writes has an empty write manifest`() {
val graph = compiler.compile(validPlan, "no-writes-workflow")
assertTrue(graph.stages.values.all { it.writeManifest.isEmpty() })
}
@Test
fun `edge referencing unknown from-stage throws WorkflowValidationException`() {
val bad = validPlan.replace("\"from\": \"analyse\"", "\"from\": \"nonexistent\"")
@@ -241,4 +273,53 @@ class ExecutionPlanCompilerTest {
fun `malformed JSON throws WorkflowValidationException`() {
assertThrows<WorkflowValidationException> { compiler.compile("not-json", "bad-workflow") }
}
// A compiler that knows the tool universe, so plan tool names are validated.
private val toolAware = ExecutionPlanCompiler(
registry,
knownTools = setOf("ShellTool", "file_write", "task_context", "task_update"),
)
@Test
fun `unknown tool name throws WorkflowValidationException naming the offender`() {
// The runtime would silently drop a misspelled tool; here it fails to compile instead.
val bad = validPlan.replace("\"tools\": [\"ShellTool\"]", "\"tools\": [\"task_updates\"]")
val ex = assertThrows<WorkflowValidationException> { toolAware.compile(bad, "bad-workflow") }
assertEquals(true, ex.message?.contains("task_updates"))
assertEquals(true, ex.message?.contains("analyse"))
}
@Test
fun `plan threading registered task tools compiles`() {
val plan = """
{
"goal": "implement and review a tracked task",
"stages": [
{
"id": "implement", "prompt": "claim auth-1, build, submit", "produces": "patch",
"needs": [], "tools": ["file_write", "task_context", "task_update"]
},
{
"id": "review", "prompt": "review and complete auth-1", "produces": "patch",
"needs": ["patch"], "tools": ["task_context", "task_update"]
}
],
"edges": [
{ "from": "implement", "to": "review", "condition": { "type": "always_true" } },
{ "from": "review", "to": "done", "condition": { "type": "always_true" } }
]
}
""".trimIndent()
val graph = toolAware.compile(plan, "tracked-workflow")
val implement = graph.stages.values.first { it.metadata["promptInline"] == "claim auth-1, build, submit" }
assertTrue(implement.allowedTools.containsAll(setOf("task_context", "task_update")))
}
@Test
fun `tool validation is skipped when the tool universe is unknown`() {
// The default compiler has no knownTools, so an arbitrary tool name still compiles.
val plan = validPlan.replace("\"tools\": [\"ShellTool\"]", "\"tools\": [\"anything_goes\"]")
val graph = compiler.compile(plan, "unvalidated-workflow")
assertEquals(2, graph.stages.size)
}
}
@@ -69,5 +69,12 @@ class FreestylePlanningWorkflowTest {
assertTrue(architectToDone.condition is ArtifactValidated)
assertEquals(2, graph.transitions.size)
// analyst frames the work: search + open one task or decompose into a graph (the architect
// threads the named task into the plan).
assertEquals(
setOf("file_read", "ShellTool", "task_search", "task_context", "task_create", "task_decompose"),
graph.stages[StageId("analyst")]!!.allowedTools,
)
}
}
@@ -0,0 +1,65 @@
package com.correx.infrastructure.workflow
import java.nio.file.FileSystems
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class PlanDerivedManifestTest {
@Test
fun `derives the declared write paths of a plan stage`() {
val stage = PlanStage(id = "impl", writes = listOf("core/a/B.kt", "docs/notes.md"))
assertEquals(setOf("core/a/B.kt", "docs/notes.md"), PlanDerivedManifest.deriveAllowedWrites(stage))
}
@Test
fun `a stage with no declared writes derives an empty set`() {
val stage = PlanStage(id = "analyse", produces = "patch", needs = listOf("x"))
assertTrue(PlanDerivedManifest.deriveAllowedWrites(stage).isEmpty())
}
@Test
fun `multiple files are all derived and blank entries dropped`() {
val stage = PlanStage(id = "impl", writes = listOf("a.kt", " ", "b.kt", "", "c.kt"))
assertEquals(setOf("a.kt", "b.kt", "c.kt"), PlanDerivedManifest.deriveAllowedWrites(stage))
}
@Test
fun `combine unions the static globs with the plan-derived set`() {
val combined = PlanDerivedManifest.combine(
staticManifest = listOf("core/**"),
planDerived = setOf("docs/x.md"),
)
assertEquals(setOf("core/**", "docs/x.md"), combined)
}
@Test
fun `combine drops blank entries and de-duplicates across sources`() {
val combined = PlanDerivedManifest.combine(
staticManifest = listOf("core/**", " ", "shared.kt"),
planDerived = setOf("shared.kt", "docs/x.md", ""),
)
assertEquals(setOf("core/**", "shared.kt", "docs/x.md"), combined)
}
@Test
fun `combined manifest matches via the same glob semantics the static manifest uses`() {
// The combined set is fed verbatim into StageConfig.writeManifest, which the plane-2
// ManifestContainmentRule matches with FileSystems glob PathMatchers. Assert that a write
// allowed only by the plan-derived entry, one allowed only by the static glob, and one in
// neither resolve as expected under that exact matcher — no parallel matcher is introduced.
val combined = PlanDerivedManifest.combine(
staticManifest = listOf("core/**"),
planDerived = setOf("docs/x.md"),
)
val matchers = combined.map { FileSystems.getDefault().getPathMatcher("glob:$it") }
fun allowed(rel: String) = matchers.any { it.matches(Path.of(rel)) }
assertTrue(allowed("core/a/B.kt"), "static glob should allow core/a/B.kt")
assertTrue(allowed("docs/x.md"), "plan-derived path should allow docs/x.md")
assertFalse(allowed("apps/server/Main.kt"), "a path in neither source is denied")
}
}
@@ -7,8 +7,11 @@ import com.correx.core.artifacts.kind.JsonSchemaProperty
import com.correx.core.events.types.StageId
import com.correx.core.transitions.conditions.ArtifactFieldEquals
import com.correx.core.transitions.conditions.FieldOperator
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.writeText
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@@ -93,4 +96,32 @@ class ReviewLoopWorkflowTest {
val changes = graph.transitions.first { it.to == StageId("implement") }.condition as ArtifactFieldEquals
assertEquals(FieldOperator.NEQ, changes.operator)
}
private fun repoFile(rel: String): Path? {
var dir: Path? = Path.of(System.getProperty("user.dir")).toAbsolutePath()
while (dir != null) {
val candidate = dir.resolve(rel)
if (candidate.exists()) return candidate
dir = dir.parent
}
return null
}
@Test
fun `shipped review_loop grants the task tools to both stages`() {
val path = repoFile("examples/workflows/review_loop.toml")
assumeTrue(path != null, "review_loop.toml not found from ${System.getProperty("user.dir")}")
val graph = loader.load(path!!)
// implement claims/submits and may create a task, alongside its file write.
assertEquals(
setOf("file_write", "task_create", "task_update", "task_context", "task_search"),
graph.stages[StageId("implement")]!!.allowedTools,
)
// review reads the task and completes it on an approved verdict.
assertEquals(
setOf("task_context", "task_update"),
graph.stages[StageId("review")]!!.allowedTools,
)
}
}
@@ -71,4 +71,28 @@ class RolePipelineWorkflowTest {
.condition as ArtifactFieldEquals
assertEquals(FieldOperator.NEQ, loop.operator)
}
@Test
fun `task tools are granted to the right stages`() {
val path = repoFile("examples/workflows/role_pipeline.toml")
assumeTrue(path != null, "role_pipeline.toml not found from ${System.getProperty("user.dir")}")
val graph = TomlWorkflowLoader(registry).load(path!!)
// analyst opens + searches (read-only on files); creation is approval-gated (T2).
assertEquals(
setOf("file_read", "ShellTool", "task_search", "task_context", "task_create"),
graph.stages[StageId("analyst")]!!.allowedTools,
)
// implementer owns the lifecycle: claim/submit + the full file set.
assertEquals(
setOf("file_read", "file_write", "file_edit", "ShellTool") +
setOf("task_create", "task_update", "task_context", "task_search"),
graph.stages[StageId("implementer")]!!.allowedTools,
)
// reviewer reads the task and completes it on an approved verdict.
assertEquals(
setOf("task_context", "task_update"),
graph.stages[StageId("reviewer")]!!.allowedTools,
)
}
}