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
@@ -0,0 +1,17 @@
package com.correx.core.config
import kotlinx.serialization.Serializable
/**
* Standing agent instructions discovered at the bound workspace root (`CLAUDE.md` and/or
* `AGENTS.md`). Unlike the curated `.correx/project.toml` ProjectProfile, these are the
* free-form markdown briefs the operator already maintains for coding agents: injected as
* L0 for every stage of every session bound to the workspace.
*/
@Serializable
data class AgentInstructions(
val sources: List<String> = emptyList(),
val content: String = "",
) {
fun isEmpty(): Boolean = sources.isEmpty() && content.isBlank()
}
@@ -0,0 +1,35 @@
package com.correx.core.config
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
/**
* Discovers standing agent instructions (`CLAUDE.md`, `AGENTS.md`) at the bound workspace
* root only — no parent walk, no nested lookup. Mirrors [ProjectProfileLoader]: the loaded
* snapshot is bound as an event so replay reads the recorded fact, never the live file.
*/
object AgentInstructionsLoader {
private val FILE_NAMES = listOf("CLAUDE.md", "AGENTS.md")
fun load(workspaceRoot: String): AgentInstructions {
val sections = mutableListOf<String>()
val sources = mutableListOf<String>()
for (name in FILE_NAMES) {
val contents = readIfPresent(Paths.get(workspaceRoot, name))
if (contents == null || contents.isBlank()) continue
sources.add(name)
sections.add("# $name\n\n${contents.trim()}")
}
if (sections.isEmpty()) return AgentInstructions()
return AgentInstructions(sources = sources, content = sections.joinToString("\n\n"))
}
private fun readIfPresent(path: Path): String? {
if (!Files.exists(path)) return null
return runCatching { Files.readString(path) }.getOrElse { e ->
System.err.println("Warning: Failed to read agent instructions at $path: ${e.message}")
null
}
}
}
@@ -57,8 +57,11 @@ internal object SimpleToml {
var depth = 0
var inQuotes = false
var quoteChar = ' '
var escaped = false
for (ch in value) {
when {
escaped -> escaped = false
ch == '\\' && inQuotes && quoteChar == '"' -> escaped = true
(ch == '"' || ch == '\'') && !inQuotes -> { inQuotes = true; quoteChar = ch }
ch == quoteChar && inQuotes -> inQuotes = false
ch == '[' && !inQuotes -> depth++
@@ -92,8 +95,13 @@ internal object SimpleToml {
var current = StringBuilder()
var inQuotes = false
var quoteChar = ' '
var escaped = false
for (ch in content) {
when {
// Keep escape sequences (e.g. \") intact so they reach stripQuotes verbatim; an
// escaped quote must not be treated as the string's closing delimiter.
escaped -> { current.append(ch); escaped = false }
ch == '\\' && inQuotes && quoteChar == '"' -> { current.append(ch); escaped = true }
(ch == '"' || ch == '\'') && !inQuotes -> { inQuotes = true; quoteChar = ch; current.append(ch) }
ch == quoteChar && inQuotes -> { inQuotes = false; current.append(ch) }
ch == ',' && !inQuotes -> { result.add(stripQuotes(current.toString().trim())); current = StringBuilder() }
@@ -104,10 +112,39 @@ internal object SimpleToml {
return result
}
/**
* Strips the surrounding quotes from [value] and, for double-quoted strings, unescapes the
* `\\` and `\"` sequences emitted by the writers (mirrors [ProjectProfileWriter]/[CorrexConfigWriter]).
*/
private fun stripQuotes(value: String): String {
val isDoubleQuoted = value.startsWith("\"") && value.endsWith("\"")
val isSingleQuoted = value.startsWith("'") && value.endsWith("'")
return if (isDoubleQuoted || isSingleQuoted) value.substring(1, value.length - 1) else value
val isDoubleQuoted = value.startsWith("\"") && value.endsWith("\"") && value.length >= 2
val isSingleQuoted = value.startsWith("'") && value.endsWith("'") && value.length >= 2
return when {
isDoubleQuoted -> unescapeDoubleQuoted(value.substring(1, value.length - 1))
isSingleQuoted -> value.substring(1, value.length - 1)
else -> value
}
}
/** Reverses the writer's escaping: `\\` -> `\`, `\"` -> `"`; a trailing lone `\` is kept as-is. */
private fun unescapeDoubleQuoted(value: String): String {
if ('\\' !in value) return value
val sb = StringBuilder(value.length)
var i = 0
while (i < value.length) {
val ch = value[i]
if (ch == '\\' && i + 1 < value.length) {
val next = value[i + 1]
if (next == '\\' || next == '"') {
sb.append(next)
i += 2
continue
}
}
sb.append(ch)
i++
}
return sb.toString()
}
}
@@ -0,0 +1,64 @@
package com.correx.core.config
import java.nio.file.Files
/**
* Serializes a [ProjectProfile] back to TOML text that [ProjectProfileLoader] reads identically:
* the invariant is `load(write(profile)) == profile` for every field the loader understands
* (about, conventions, commands).
*
* Like [CorrexConfigWriter] this **regenerates** the file from the profile object — it does not
* preserve hand-written comments. Empty sections are skipped: a blank `about` and an empty
* `conventions`/`commands` produce no output for that part (the loader reconstructs the defaults
* from their absence), so a [ProjectProfile.isEmpty] profile serializes to an empty string.
*/
object ProjectProfileWriter {
/** Renders [profile] as TOML. String values escape `\` and `"`; empty sections are skipped. */
fun serialize(profile: ProjectProfile): String {
val b = StringBuilder()
if (profile.about.isNotBlank()) {
b.append("about = ").append(str(profile.about)).append('\n')
}
if (profile.conventions.isNotEmpty()) {
b.append("conventions = ").append(list(profile.conventions)).append('\n')
}
if (profile.commands.isNotEmpty()) {
if (b.isNotEmpty()) b.append('\n')
b.append("[commands]\n")
profile.commands.forEach { (key, value) ->
b.append(key).append(" = ").append(str(value)).append('\n')
}
}
return b.toString()
}
/**
* Writes [profile] to `<workspaceRoot>/.correx/project.toml`, creating the `.correx/` directory
* if it is missing. Overwrites any existing file (the writer owns the file once a save happens).
*/
fun write(workspaceRoot: String, profile: ProjectProfile) {
val path = ProjectProfileLoader.profilePath(workspaceRoot)
Files.createDirectories(path.parent)
Files.writeString(path, serialize(profile))
}
private fun str(v: String): String = "\"" + v.replace("\\", "\\\\").replace("\"", "\\\"") + "\""
private fun list(v: List<String>): String = "[" + v.joinToString(", ") { str(it) } + "]"
}
/**
* Appends [text] to this profile's conventions, de-duplicated by exact string match. A blank
* [text] is ignored (returns the profile unchanged). Used when promoting an idea-board entry into
* the curated profile so repeated promotions of the same text don't pile up duplicate conventions.
*/
fun ProjectProfile.withConvention(text: String): ProjectProfile {
val trimmed = text.trim()
if (trimmed.isBlank() || trimmed in conventions) return this
return copy(conventions = conventions + trimmed)
}
@@ -0,0 +1,57 @@
package com.correx.core.config
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Files
import java.nio.file.Path
class AgentInstructionsLoaderTest {
@Test
fun `both files present yields both sections and sources`(@TempDir root: Path) {
Files.writeString(root.resolve("CLAUDE.md"), "Claude rules")
Files.writeString(root.resolve("AGENTS.md"), "Agents rules")
val loaded = AgentInstructionsLoader.load(root.toString())
assertEquals(listOf("CLAUDE.md", "AGENTS.md"), loaded.sources)
assertTrue(loaded.content.contains("# CLAUDE.md"), "content: ${loaded.content}")
assertTrue(loaded.content.contains("Claude rules"), "content: ${loaded.content}")
assertTrue(loaded.content.contains("# AGENTS.md"), "content: ${loaded.content}")
assertTrue(loaded.content.contains("Agents rules"), "content: ${loaded.content}")
assertFalse(loaded.isEmpty())
}
@Test
fun `only one file present yields that one`(@TempDir root: Path) {
Files.writeString(root.resolve("AGENTS.md"), "Agents only")
val loaded = AgentInstructionsLoader.load(root.toString())
assertEquals(listOf("AGENTS.md"), loaded.sources)
assertTrue(loaded.content.contains("# AGENTS.md"), "content: ${loaded.content}")
assertTrue(loaded.content.contains("Agents only"), "content: ${loaded.content}")
assertFalse(loaded.content.contains("CLAUDE.md"), "content: ${loaded.content}")
}
@Test
fun `neither file present is empty`(@TempDir root: Path) {
val loaded = AgentInstructionsLoader.load(root.toString())
assertTrue(loaded.isEmpty())
assertEquals(AgentInstructions(), loaded)
}
@Test
fun `blank file is skipped`(@TempDir root: Path) {
Files.writeString(root.resolve("CLAUDE.md"), " \n ")
Files.writeString(root.resolve("AGENTS.md"), "Real content")
val loaded = AgentInstructionsLoader.load(root.toString())
assertEquals(listOf("AGENTS.md"), loaded.sources)
}
}
@@ -0,0 +1,79 @@
package com.correx.core.config
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertSame
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
class ProjectProfileWriterTest {
@TempDir
lateinit var tempDir: Path
@Test
fun `write then load round-trips about conventions and commands including embedded quotes`() {
val profile = ProjectProfile(
about = "Event-sourced kernel \"the\" orchestrator, Kotlin/JVM 21",
conventions = listOf(
"no bare try-catch",
"prefer the \"narrow\" interface", // a convention with a double-quote
),
commands = mapOf(
"build" to "./gradlew build",
"test" to "./gradlew check",
),
)
ProjectProfileWriter.write(tempDir.toString(), profile)
val loaded = ProjectProfileLoader.load(tempDir.toString())
assertEquals(profile.about, loaded.about)
assertEquals(profile.conventions, loaded.conventions)
assertEquals(profile.commands, loaded.commands)
assertEquals(profile, loaded)
}
@Test
fun `an empty profile serializes to an empty string and skips empty sections`() {
val serialized = ProjectProfileWriter.serialize(ProjectProfile())
assertEquals("", serialized)
assertFalse(serialized.contains("[commands]"))
}
@Test
fun `blank about is omitted but conventions and commands are still written`() {
val serialized = ProjectProfileWriter.serialize(
ProjectProfile(
about = " ",
conventions = listOf("c1"),
commands = mapOf("run" to "x"),
),
)
assertFalse(serialized.contains("about ="), "blank about must be skipped")
assertTrue(serialized.contains("conventions = [\"c1\"]"))
assertTrue(serialized.contains("[commands]"))
}
@Test
fun `withConvention appends a new convention`() {
val updated = ProjectProfile(conventions = listOf("a")).withConvention("b")
assertEquals(listOf("a", "b"), updated.conventions)
}
@Test
fun `withConvention de-dupes an exact existing convention`() {
val profile = ProjectProfile(conventions = listOf("no bare try-catch"))
val updated = profile.withConvention("no bare try-catch")
assertSame(profile, updated, "an exact duplicate must return the same profile unchanged")
assertEquals(listOf("no bare try-catch"), updated.conventions)
}
@Test
fun `withConvention ignores blank text`() {
val profile = ProjectProfile(conventions = listOf("a"))
assertSame(profile, profile.withConvention(" "))
}
}