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
+112
View File
@@ -0,0 +1,112 @@
---
name: "Adr 0012 Task Tracking"
description: "Native task tracking as an event-sourced aggregate; tasks are a first-class work-graph node"
depth: 2
links: ["../index.md", "./adr-0001-event-sourcing.md", "./adr-0005-persistence-choice.md", "./adr-0006-event-store-append-only.md"]
---
# ADR 0012: native task tracking as an event-sourced aggregate
**status:** accepted
**date:** 23.06.2026 (June)
**deciders:** correx design team
---
## context
Correx is to gain **native task tracking** — a durable "task" (work item / issue) entity,
agent-first. Today the system has no concept of a task; it has `sessions` (agent runs) and
session/stage-scoped `artifacts`. A task is conceptually different: a durable unit of intent
that outlives any single session and *references* the artifacts and sessions that act on it.
An originating pitch proposed storing tasks as YAML/markdown files as the **source of truth**
with Postgres as a cache, plus a `GET /tasks/next` assignment scheduler. Those technical
choices are **rejected** here: they contradict correx's foundational invariants —
"the event log is the only source of truth; projections are disposable and rebuilt from
events" (`.correx/project.toml`, ADR-0001, ADR-0006). Adopting hand-edited files as truth
would create two masters (files vs. log) and forfeit replay/idempotency. We keep the *goal*
(native tasks) and realise it with correx's actual architecture.
This ADR covers the **foundational slice**: the aggregate, its events, and the work-graph
edge model. REST/CLI/agent-context-bundle/search are deliberately later phases.
## decision
**A task is a first-class event-sourced aggregate**, modelled on the existing `core:sessions`
aggregate (Status / State / Reducer / Projector / Repository / Counter), living in a new
`core:tasks` module. It is *not* an `ArtifactKind` — artifacts are session/stage-scoped LLM
outputs; a task is durable and cross-session.
### 1. event log is the source of truth
Tasks are rebuilt by folding events. Any future markdown/file representation is a *disposable
projection* (export), never the master copy.
### 2. per-project task stream
The `EventStore` is partitioned by `sessionId`. Since `SessionId` and `TaskId` are both
`TypeId`, all task events for a project live in one stream keyed `tasks:<projectId>`
(`TaskStreams.forProject`). One read rebuilds the whole board (`TaskBoard = Map<TaskId,
TaskState>`). The `tasks:` prefix keeps these streams distinguishable from real agent
sessions; when task streams are later wired into the live server, session-listing consumers
filter the prefix.
### 3. lifecycle as facts; status is derived
Mirroring sessions (whose events never carry `SessionStatus`), task events are facts
(`TaskClaimed`, `TaskBlocked`, `TaskCompleted`, …) and `DefaultTaskReducer` derives
`TaskStatus`. This keeps the `TaskStatus` enum out of the lowest module (`core:events`). A
`sealed interface TaskEvent { val taskId }` (a marker, *not* part of the serialized
`EventPayload` hierarchy) lets projections route any payload to its task without a giant `when`.
### 4. status workflow
`TODO → IN_PROGRESS → IN_REVIEW → DONE`, with `BLOCKED` (reversible; unblock returns to
IN_PROGRESS if claimed, else TODO) and `CANCELLED` (terminal, reopenable). Illegal
transitions are ignored and counted in `TaskState.invalidTransitions` (as `SessionState` does).
### 5. work-graph edges
`TaskLinkType` (sibling to the existing `ArtifactRelationshipType`):
`DEPENDS_ON, BLOCKS, IMPLEMENTS, RELATES_TO, PRODUCED, CONTEXT`. A link target is a plain id
string — another task, an `ArtifactId`, or a `SessionId` — plus a type. This is the work-graph
backbone the agent-context bundle (later phase) will traverse.
### 6. human-friendly ids
`TaskCounterProjection(projectId)` counts `TaskCreatedEvent`s in the project stream; the
numeric suffix of a key (`<prefix>-<n>`, e.g. `auth-142`) is the count of *created* tasks, so
ids never collide even after cancellation. The creating caller composes the key and stores it
in `TaskCreatedEvent.key`. (The creating service is a later phase; the projection and `key`
field land now.)
## consequences
**positive:**
* reuses the entire event-sourcing backbone (sequencing, idempotency, replay, projections);
no new persistence model, no second source of truth
* the work graph (tasks ↔ tasks/artifacts/sessions) is native, enabling later
"everything touching X" queries and the agent-context bundle
* deterministic, replayable task state; status derivation is unit-testable in isolation
**negative:**
* overloading the `sessionId`-partitioned stream key with `tasks:<projectId>` means
session-enumerating consumers must learn to filter the prefix once tasks go live
(acceptable; isolated to the live-wiring phase)
* per-project (not per-task) stream means rebuilding one task replays the project's task
events — fine at expected volumes; revisit with snapshots if a project's task log grows large
## alternatives considered
* **Task as an `ArtifactKind`:** rejected — artifacts are session/stage-scoped outputs;
tasks are durable, cross-session, and carry their own lifecycle/claiming.
* **YAML/markdown files as source of truth + Postgres cache (the pitch):** rejected —
violates ADR-0001/0006; creates two masters and loses replay/idempotency.
* **`GET /tasks/next` assignment scheduler (the pitch):** dropped — scheduling/prioritisation
is a separate hard problem; v1 offers `claim` (pull), not `assign` (push).
* **One stream per task:** cleaner per-task sequencing but pollutes session enumeration with
one stream per task and makes board/list queries fan out across many streams; the
per-project stream is the better default for a tracker that lists/boards constantly.
## status
Governs the foundational task-tracking slice. Re-evaluation expected when the REST surface,
the `GET /tasks/{id}/context` agent bundle, search, and git-driven auto status updates are
designed (those phases build on, and must not contradict, this ADR).
+126
View File
@@ -0,0 +1,126 @@
# Correx live-QA environment — bring-up runbook
The plans in `docs/qa/QA-*.md` cite **observable signals** (events, logs, files, TUI
renders). This runbook stands up the stack that produces them. Run it on a capable box
(JDK 21, Go ≥ 1.24, Docker, a GPU + GGUF model for any model-dependent plan).
Helper scripts live in `scripts/qa/` (see `scripts/qa/README.md`).
> **Low-RAM note:** a machine-local `~/.gradle/gradle.properties` on the dev box caps the
> Gradle/Kotlin heap (`-Xmx768m`, `parallel=false`) for a 1.9 GB machine. It is **not
> committed**. On a QA box with real RAM, delete it (or ignore it) so builds aren't throttled.
---
## Prerequisites
- **JDK 21** (`java -version` → 21).
- **Go ≥ 1.24** — the TUI builds via `GOTOOLCHAIN=auto` (go.mod pins `go 1.24.2`); the
toolchain auto-downloads if your `go` is older.
- **Docker** — only for SearXNG (research-workflow QA).
- **llama-server** binary + at least one **GGUF model** — for any model-dependent plan.
- (optional) an **embedder** model/endpoint — required for the architect-contradiction plan
and any L3-retrieval-dependent behavior (`[router.embedder] backend = "llamacpp"`).
---
## 1. Config
Correx reads `~/.config/correx/config.toml` plus `workflows/`, `prompts/`, `schemas/`
alongside it. Stale copies cause silent bugs (BACKLOG ops note), so **sync from the repo**:
```bash
scripts/qa/sync-config.sh # copies examples/workflows + prompts + docs/schemas
# and seeds config.toml from docs/sample-config.toml IF ABSENT
```
Then edit `~/.config/correx/config.toml` for the plan you're running:
- **Model** — pick ONE:
- *Managed* (`[[models]]` + `[models]`): correx spawns llama-server at `[models].host:port`
(default `127.0.0.1:10000`) and kills it on shutdown. Simplest.
- *Static* (`[[providers]]`, type `llamacpp`, `url`): you run llama-server yourself. **Use
this for the llama-health plan** so you can stop the server independently of correx.
- **`[project] enabled = true`** — required for project memory + the **architect-contradiction**
plan (it queries the `project:<repoRoot>` L3 namespace written at session end).
- **`[router.embedder] backend = "llamacpp"`** (not `noop`) — required wherever embeddings
matter (architect contradiction, L3 retrieval). `noop` makes every distance meaningless.
- **`[tools.research] enabled = true` + `searxng_url`** — for the research/egress plan.
- **`[health] enabled = true`** — for the llama-health plan; note `interval_ms` (probe cadence).
A `[[artifacts]]` entry must exist for every LLM-emitted artifact kind a workflow `produces`
(schemas under `~/.config/correx/schemas/`). `sync-config.sh` copies the schemas; confirm the
`[[artifacts]]` table in `config.toml` references the kinds your workflow uses.
## 2. llama-server
- **Managed:** nothing to start — correx launches it. The health probe pings
`http://<[models].host>:<[models].port>/health`.
- **External/static:** start your llama-server on the `url` host:port; its `/health` must 2xx.
```bash
llama-server -m ~/models/<model>.gguf --host 127.0.0.1 --port 10000
```
## 3. SearXNG (research-workflow QA only)
```bash
scripts/qa/searxng-up.sh # http://localhost:8888, JSON format enabled
# ... run research QA ...
scripts/qa/searxng-down.sh
```
> **Gotcha:** SearXNG ships with only the `html` output format; the research `web_search`
> needs the **JSON API**. `searxng-up.sh` writes a `settings.yml` enabling `formats: [html, json]`
> and mounts it. Without that, `web_search` returns nothing and the workflow stalls.
## 4. Start the server
```bash
./gradlew :apps:server:run # mainClass com.correx.apps.server.MainKt, listens on :8080
# or build a runnable dist once and reuse it:
./gradlew :apps:server:installDist
apps/server/build/install/server/bin/server
```
> **Gotcha:** do **not** rebuild jars while the server JVM is running — lazy classloading
> breaks (`NoClassDefFoundError`). Stop the server first, rebuild, restart.
## 5. Start the TUI
```bash
cd apps/tui-go
GOTOOLCHAIN=auto go build -o correx-tui .
./correx-tui -host localhost -port 8080 # flags default to localhost:8080
```
## 6. Evidence tools (what the plans cite)
| Source | Command / where |
|--------|-----------------|
| Event log (the truth) | `./gradlew :apps:cli:run --args="events <sessionId>"` or `GET /sessions/{id}/events` |
| Deterministic replay | `./gradlew :apps:cli:run --args="replay <sessionId>"` |
| System health | `./gradlew :apps:cli:run --args="health"` |
| Server logs | stdout of `:apps:server:run` — MDC carries `sessionId` |
| Files on disk | e.g. `<workspaceRoot>/.correx/project.toml` (idea-promotion plan) |
| TUI render | the running `correx-tui` |
(Once `:apps:cli:installDist` is built, `apps/cli/build/install/cli/bin/cli events <id>` works too.)
## Teardown
1. Stop the server (Ctrl-C) — a managed `[[models]]` llama-server is killed by the shutdown hook.
2. `scripts/qa/searxng-down.sh` if you started it.
3. External llama-server: stop it yourself.
---
## Per-plan env matrix
| QA plan | model | embedder=llamacpp | SearXNG | `project.enabled` | workspace `.correx/` |
|---------|:-----:|:-----------------:|:-------:|:-----------------:|:--------------------:|
| `QA-research-egress` | yes (tool-calling) | no | **yes** | no | no |
| `QA-architect-contradiction` | yes | **yes** | no | **yes** | repo root, 2 sessions same process |
| `QA-llama-health-probe` | yes (static path to kill) | no | no | no | no |
| `QA-idea-promotion` | router only (or seed event) | no | no | no | **yes** (bound workspace) |
| `QA-reviewer-static-first` | yes | no | no | no | seed `StaticFindingsRecordedEvent` |
| `QA-brief-echo-gate` | yes (the prod candidate) | no | no | no | analyst brief with criteria |
+42
View File
@@ -0,0 +1,42 @@
# QA Plan: architect contradiction-check (display-only) — `4e08510` `eae0a0c`
**Status:** DRAFT
**Run date / operator:**
**BACKLOG item:** §B-§4 "architect contradiction-check" — checker `eae0a0c`, live server hook `4e08510`.
---
## Preconditions
- [ ] **server build/branch:** `feat/backlog-burndown`.
- [ ] **llama-server + model:** a model that runs the `role_pipeline` (analyst→architect→…) and emits a `design` artifact with a populated `approach` field (see `docs/schemas/design.json`).
- [ ] **external deps:** none beyond the model.
- [ ] **config synced:** `[project] enabled = true` (the checker queries the `project:<repoRoot>` L3 namespace that `ProjectMemoryService.persist` writes); **`[router.embedder] backend = "llamacpp"`** with a real embedder URL — `noop` embeddings make every distance meaningless and the gate never fires. `[router.l3] backend = "in_memory"` is fine for a single run, but **note**: in-memory L3 is process-lifetime, so the two sessions below must run against the **same** server process.
- [ ] **fixtures/seed:** the same workspace/repo root for both sessions (the namespace is `project:<repoRoot>`).
## Acceptance gate (one sentence)
> When a later session's architect `design` decision is semantically near a **prior** session's recorded decision, a `PossibleContradictionFlaggedEvent` is appended — and it never halts the stage.
## Checks
| # | Action | Expected observable evidence | Result |
|---|--------|------------------------------|--------|
| 1 | Run **session A** end-to-end on a repo; architect commits a clear decision (e.g. "use Postgres for persistence"). Let the session **complete** (distillation runs on completion). | Session A completes; `correx events {A}` shows the architect `design` `ArtifactCreatedEvent`. The decision is now distilled into L3 under `project:<repoRoot>` (ProjectMemoryService.persist on completion). | |
| 2 | Run **session B** (same server process, same repo) whose architect decision **reverses** A (e.g. "use SQLite / drop Postgres"). | When B's architect `design` artifact is created, `correx events {B}` contains a `PossibleContradictionFlaggedEvent``decisionSummary` = B's `approach`, `related[]` lists A's decision with a `score ≥ 0.75` and `source` = the `project:<repoRoot>` turnId. | |
| 3 | Confirm non-interference. | Session B's architect stage **still passes** and proceeds to the planner — the flag is display-only (no `WorkflowFailedEvent`, no stage halt around the flag). | |
| 4 | Run **session C** with an architect decision unrelated to A (different domain). | **No** `PossibleContradictionFlaggedEvent` for C (nothing clears the 0.75 threshold). Absence is the evidence. | |
| 5 | Confirm self-exclusion. | The flag in check 2 never lists **B's own** in-flight decision as `related` (checker filters `entry.sessionId != current`). | |
Evidence sources: `correx events {id}`, server logs (the `[Orchestrator]`/checker warn line on a near hit is informative but the **event** is the gate).
## Out of scope (explicitly NOT covered this pass)
- In-session contradiction (two decisions inside one session) — by design the checker filters to PRIOR sessions; not built and not required.
- LLM-judged contradiction — v1 is similarity-only, display-only.
- ADR `alternativesConsidered` shape (BACKLOG marks this deliberately not taken).
## Disposition
- **PASS** → move the §B-§4 entry to `RETRO.md` with this run date + the `PossibleContradictionFlaggedEvent` evidence from check 2. Status: PASSED.
- **FAIL** → file numbered findings in `BACKLOG.md`. Most likely failure = embedder is `noop` or the two sessions ran against different processes (in-memory L3 lost) — fix the env, not the code, then re-run. Status: FAILED until green.
+55
View File
@@ -0,0 +1,55 @@
# QA Plan: cross-session grants (PROJECT/GLOBAL) + revoke — `c36d41b` `8df0ec7`
**Status:** DRAFT
**Run date / operator:**
**BACKLOG item:** operator-requested wider grants — see `RETRO.md` 2026-06-21 "wider grants + revoke" (the cross-session live-QA gate noted there). On PASS the gate clears and the entry stays in RETRO with the run date; on FAIL the misses refile into `BACKLOG.md`.
---
## Preconditions
- [ ] **server build/branch:** `feat/backlog-burndown`_rebuild only with the server stopped (lazy classloading breaks otherwise)_
- [ ] **llama-server + model:** a tool-calling-capable model that drives a **T2+** tool call reproducibly across runs. The `healthcheck` workflow's `write_script``file_write` (T3) is the reference fixture (used by the other QA demos); any workflow with a deterministic write/shell call works.
- [ ] **external deps:** none (no SearXNG / network needed — this exercises the approval gate, not research).
- [ ] **config synced:** `~/.config/correx` matches `examples/*` + `docs/schemas/*` (`scripts/qa/sync-config.sh`).
- [ ] **fixtures/seed:** two distinct workspace dirs — **W1** (e.g. `/home/kami/qa/repoA`) and **W2** (`/home/kami/qa/repoB`) — each launchable as a session workspace, for the PROJECT-isolation check. The TUI binds its cwd as the workspace (the `Hello` frame), so run the TUI from W1 vs W2 to switch projects.
## Acceptance gate (one sentence)
> A GLOBAL/PROJECT grant created in one session auto-clears its **bound tool** in a *different* session — recorded as `ApprovalDecisionResolved(AUTO_APPROVED, reason="grant:<id>")` with **no** `APPROVAL_PENDING` pause — scoped correctly (PROJECT only on the same workspace; tool-bound; T3/T4 honoured), and **revoking** it makes that tool prompt again — all visible in the grant ledger.
## Checks
The decisive signal throughout: a grant match emits `ApprovalDecisionResolvedEvent` with
`outcome = AUTO_APPROVED` and `reason = "grant:<grantId>"` and **falls through to execute**
(no `OrchestrationPausedEvent("APPROVAL_PENDING")`, no `ApprovalRequestedEvent` for that
invocation). The human path emits both of those. "It didn't prompt" must be backed by the
*absence* of `APPROVAL_PENDING` in the log, not just a TUI impression.
| # | Action | Expected observable evidence | Result |
|---|--------|------------------------------|--------|
| 1 | **Baseline.** Run a session (W1) to a T2+ tool call with no grant in force. | The call pauses for approval: `OrchestrationPausedEvent(reason="APPROVAL_PENDING")` + `ApprovalRequestedEvent(toolName="file_write", tier=T3)` in `correx events {id}`; the TUI shows the approval band. | |
| 2 | **Create GLOBAL grant.** At that prompt press `A`, then `g`. | Exactly one `ApprovalGrantCreatedEvent` in the **grant ledger** (`GET /sessions/__grant_ledger__/events`) with `scope` = `GLOBAL`, `toolName="file_write"`, `permittedTiers` including T3. The current call then resolves `ApprovalDecisionResolved(AUTO_APPROVED, reason="grant:<id>")` and executes. The TUI grants viewer (`G`) lists the grant (GLOBAL · file_write · ≤T3). | |
| 3 | **Cross-session auto-clear (headline).** Start a **second, separate** session that makes the same `file_write` call. | That session records `ApprovalDecisionResolved(AUTO_APPROVED, reason="grant:<id>")` and **no** `OrchestrationPausedEvent("APPROVAL_PENDING")` / **no** `ApprovalRequestedEvent` for the invocation; the tool executes. Proves the ledger is unioned across sessions. | |
| 4 | **Tool-binding.** In a session have the agent call a *different* T2+ tool (e.g. `shell`). | `shell` still raises `OrchestrationPausedEvent("APPROVAL_PENDING")` + `ApprovalRequestedEvent(toolName="shell")` — the GLOBAL grant is bound to `file_write` only, never a blanket open. | |
| 5 | **No tier cap (T3/T4).** Confirm the auto-clear in #3 was a **T3** call. (Optional: at a prompt, grant a tool whose permitted tiers reach T4 and drive a T4 call.) | The ≥T3 call resolves `AUTO_APPROVED` — the removed T2 ceiling no longer blocks high tiers. (The old build rejected `CreateGrant` tiers > T2.) | |
| 6a | **PROJECT scope — same repo.** Revoke the GLOBAL grant (#8). From the TUI in **W1**, drive `file_write`, press `A` then `p`. Then start another session whose cwd is **W1**. | Ledger `ApprovalGrantCreatedEvent` `scope = PROJECT`, `projectId` = canonical absolute path of W1 (e.g. `/home/kami/qa/repoA`). The new W1 session auto-clears `file_write` (`AUTO_APPROVED, reason="grant:<id>"`). | |
| 6b | **PROJECT scope — different repo.** Start a session whose cwd is **W2**, same `file_write` call. | The W2 session **prompts** (`APPROVAL_PENDING` + `ApprovalRequestedEvent`) — the PROJECT grant does not match a different workspace's derived `projectId`. | |
| 7 | **Persistence across restart.** Stop and restart the server; reopen the TUI grants viewer (`G`) / re-read `GET /sessions/__grant_ledger__/events`. | The standing grant is still listed (the ledger stream replays); a fresh session still auto-clears its tool. | |
| 8 | **Revoke.** In the grants viewer press `x` on the grant (or send `RevokeGrant`). | An `ApprovalGrantExpiredEvent(grantId=<id>)` is appended to the **ledger**; the viewer's next `grant.list` no longer lists it. A new session calling that tool now **prompts again** (`APPROVAL_PENDING` + `ApprovalRequestedEvent` reappear). | |
| 9 | **Session-grant does not leak (regression).** At a prompt press `A` then `s` (or Enter); then start a *different* session with the same call. | The SESSION grant is written to the **session's own** stream (not `__grant_ledger__`); the other session still **prompts** (`APPROVAL_PENDING`). Confirms SESSION scope stayed per-session. | |
| 10 | **Audit + replay.** `GET /sessions/__grant_ledger__/events`; `correx replay {consuming-session-id}`. | The ledger shows the create + expire pair (nothing deleted — invariant #9). Replay reproduces the same AUTO_APPROVED / PENDING decisions deterministically from the recorded grants (no re-prompt). | |
Evidence sources: the TUI grants viewer (`G`, backed by `ListGrants``grant.list`), `GET /sessions/__grant_ledger__/events` (the ledger is a real event stream — `correx events __grant_ledger__` if the CLI accepts the raw stream id), `correx events {id}` / `GET /sessions/{id}/events` for the consuming sessions, server logs (MDC sessionId), and `correx replay {id}` for determinism.
## Out of scope (explicitly NOT covered this pass)
- The §D standalone web-approval client (third client) — separate track, not built.
- **STAGE**-scope grants — internal, unchanged by this work.
- Time-based expiry (`expiresAt`): the TUI does not set one today, so a grant is standing-until-revoked; the engine *does* honour `expiresAt` if a future client sets it, but that path is not exercised here.
- The LLM-annotation command-card layer and other unrelated TUI overlays.
## Disposition
- **PASS** → the cross-session gate in `RETRO.md` (2026-06-21) clears; set this plan's Status: PASSED with the run date and the cited `ApprovalGrantCreatedEvent` / `AUTO_APPROVED reason="grant:<id>"` / `ApprovalGrantExpiredEvent` evidence.
- **FAIL** → file each failure as a numbered finding in `BACKLOG.md` (action + exact repro + the wrong/missing signal — e.g. "step 3: session B still emitted APPROVAL_PENDING → ledger union not consulted"), fix, then re-run only the failed checks. Status: FAILED until green.
+44
View File
@@ -0,0 +1,44 @@
# QA Plan: idea-board → project-profile promotion — `f107ff5`
**Status:** DRAFT
**Run date / operator:**
**BACKLOG item:** §E "Idea board → profile promotion".
---
## Preconditions
- [ ] **server build/branch:** `feat/backlog-burndown`.
- [ ] **llama-server + model:** a router model only if you capture the idea via the rubber-duck path (it must emit a ```` ```json {"ideas":[...]} ``` ```` block). To avoid model-dependence you may instead **seed an `IdeaCapturedEvent`** directly (see note below).
- [ ] **external deps:** none.
- [ ] **config synced:** standard.
- [ ] **fixtures/seed:** a session bound to a **workspace** (a `SessionWorkspaceBoundEvent` must exist for the capturing session — promotion writes into `<workspaceRoot>/.correx/project.toml` and no-ops without a bound workspace). Start the TUI/session from inside the target repo dir so the workspace resolves.
## Acceptance gate (one sentence)
> Promoting a captured idea appends its text as a `conventions` entry in `<workspaceRoot>/.correx/project.toml`, records an `IdeaPromotedEvent`, and removes the idea from the board.
## Checks
| # | Action | Expected observable evidence | Result |
|---|--------|------------------------------|--------|
| 1 | Capture an idea (rubber-duck `{"ideas":[...]}`, or seed an `IdeaCapturedEvent`). List the board (TUI `R`/idea overlay, or `ListIdeas` WS). | The idea shows on the board with a stable `ideaId`; `IdeaCapturedEvent` in the log. | |
| 2 | Note the pre-state of `<workspaceRoot>/.correx/project.toml` (may be absent). | File absent or its current `conventions = [...]` recorded. | |
| 3 | Send `PromoteIdea(ideaId)` (TUI promote key, or the WS `PromoteIdea` message). | `<workspaceRoot>/.correx/project.toml` now exists and its `conventions` list contains the idea text **verbatim** (escaped if it had quotes). Re-reading via `ProjectProfileLoader` round-trips it (no corruption). | |
| 4 | Re-read the log and the board. | An `IdeaPromotedEvent(ideaId, sessionId, text, timestampMs)` is appended; the idea **no longer appears** on the board (`activeIdeas` tombstones promoted ids like discards). | |
| 5 | Promote the **same** idea text again from a second idea (dedup). | `withConvention` does not duplicate an identical convention — the list has it once. | |
| 6 | Promote with **no bound workspace** (start a session with an unresolved working dir). | No file write, no `IdeaPromotedEvent` — the handler no-ops and just refreshes the board (safe). | |
> **Seeding without a model:** append an `IdeaCapturedEvent(ideaId, sessionId, text, timestampMs)` for a workspace-bound session via the event store, then drive `PromoteIdea`. This isolates the promotion path from router-prompt behavior.
Evidence sources: the file `<workspaceRoot>/.correx/project.toml` on disk, `correx events {id}`, the TUI board render.
## Out of scope (explicitly NOT covered this pass)
- The rubber-duck capture quality itself (covered by the separate idea-board live-QA, §F).
- Promotion into `about`/`commands` — v1 promotes into `conventions` only.
## Disposition
- **PASS** → move the §E "Idea board → profile promotion" bullet to `RETRO.md` with this run date + the on-disk `.correx/project.toml` evidence. Status: PASSED.
- **FAIL** → numbered findings (e.g. quote-escaping corruption, board not clearing). Status: FAILED until green.
+42
View File
@@ -0,0 +1,42 @@
# QA Plan: research egress allowlist + batch source-list approval — `ab7e4be` `4b0024f` `b098d87` `027ff1f`
**Status:** DRAFT
**Run date / operator:**
**BACKLOG item:** §D — "Dynamic per-session egress allowlist", "§3 batch fetch-approval at source-list level", "Dedicated `SourceFetched` / `LowQualityExtraction` events", plus the §D "Live-QA the full run" gate.
---
## Preconditions
- [ ] **server build/branch:** `feat/backlog-burndown`_rebuild only with the server stopped (lazy classloading breaks otherwise)_
- [ ] **llama-server + model:** a tool-calling-capable model (the research workflow drives `web_search`/`web_fetch` tool calls). Managed `[[models]]` or static `[[providers]]`.
- [ ] **external deps:** **SearXNG** at `[tools.research] searxng_url` with the JSON format enabled (`scripts/qa/searxng-up.sh`; see `docs/qa/ENV.md` §3). Outbound network for the fetched hosts.
- [ ] **config synced:** `~/.config/correx` matches `examples/*` + `docs/schemas/*` (`scripts/qa/sync-config.sh`); `research.toml` present; `[tools.research] enabled = true`.
- [ ] **fixtures/seed:** a research prompt that yields ≥2 distinct source hosts (e.g. "summarize the latest on <topic> citing official docs").
## Acceptance gate (one sentence)
> Approving a source list once clears every subsequent `web_fetch` to those hosts **without a per-URL T2 prompt**, and the fetch quality lands as dedicated `SourceFetched` / `LowQualityExtraction` events — while a host NOT on the list still raises T2.
## Checks
| # | Action | Expected observable evidence | Result |
|---|--------|------------------------------|--------|
| 1 | Start a research session; let it reach a state with candidate source URLs. | `web_search` T1 ran; the candidate URLs appear in the session (search result / dossier event). | |
| 2 | `POST /sessions/{id}/approve-sources` with the candidate URL list (2+ hosts). | Exactly **one** `EgressHostsGrantedEvent` in `correx events {id}``hosts` = the distinct lower-cased hostnames (ports stripped), `reason = "batch source-list approval"`. | |
| 3 | Let the workflow `web_fetch` each approved URL. | Each fetch to an approved host proceeds with **no `ApprovalRequestedEvent` (T2)**`NetworkHostRule` PROCEEDs via the per-session union. A `SourceFetchedEvent` (with `content_sha256` + quality) is recorded per successful fetch. | |
| 4 | Force a low-quality extraction (e.g. a near-empty / boilerplate page among the sources). | A `LowQualityExtractionEvent` is recorded (not just metadata on `ToolExecutionCompletedEvent`). | |
| 5 | Have the agent attempt a `web_fetch` to a host **not** in the approved list. | A T2 `ApprovalRequestedEvent` IS raised for that host (the allowlist is a union, not a blanket open). | |
| 6 | `correx replay {id}`. | Replay reproduces the same allowlist decisions deterministically (the grant + fetch events are the only inputs; no re-fetch). | |
Evidence sources: `correx events {id}` / `GET /sessions/{id}/events`, server logs (MDC sessionId), `correx replay {id}`.
## Out of scope (explicitly NOT covered this pass)
- The standalone §D web-approval client (third client) — separate track, not built.
- Headless-browser / LLM-based extraction (non-responsibilities).
## Disposition
- **PASS** → move the §D egress/batch/source-event bullets to `RETRO.md` with this run date + the cited `EgressHostsGrantedEvent` / `SourceFetchedEvent` evidence. Status: PASSED.
- **FAIL** → file each failure as a numbered finding in `BACKLOG.md` (action + repro + the wrong/missing signal). Status: FAILED until green.
+44
View File
@@ -0,0 +1,44 @@
# QA Plan: reviewer static-first (findings exclusion) — `447fc7a`
**Status:** DRAFT (PARTIAL — see gap)
**Run date / operator:**
**BACKLOG item:** §B-§5 "reviewer static-first" — runner + event + context-exclusion `447fc7a`; deterministic `static_check` stage seam + emitter `e46777e`.
---
## Gap up front (read first)
The exclusion **filter** is live-wired into reviewer context assembly; `StaticAnalysisRunner` + `StaticFindingsRecordedEvent` are built; and the deterministic **`static_check` stage seam + emitter now exist** (`e46777e`): the orchestrator runs any `stage_type = "static_check"` stage through `StaticCheckStageExecutor`, and `role_pipeline.toml` carries one between implementer and reviewer. The remaining gap is **activation**: that stage is a **no-op until** a sandboxed, workspace-scoped `CommandRunner` is wired into the orchestrator AND the stage sets `static_argv`. So checks 13 verify the **filter by injecting the event** (unchanged); check 4 is now an **activation** check (configure `static_argv` + wire a runner) rather than "blocked on an unbuilt seam".
## Preconditions
- [ ] **server build/branch:** `feat/backlog-burndown`.
- [ ] **llama-server + model:** a model that runs `review_loop` / `role_pipeline` reviewer stage.
- [ ] **external deps:** none.
- [ ] **config synced:** `review_loop.toml` (or role_pipeline) present.
- [ ] **fixtures/seed:** a way to append a `StaticFindingsRecordedEvent` for the session (manual event-store seed), carrying a finding that matches a compile/lint issue the code actually has.
## Acceptance gate (one sentence)
> When a `StaticFindingsRecordedEvent` has already recorded a static finding this session, the reviewer's assembled context **mechanically omits** any feedback entry that merely restates that finding.
## Checks
| # | Action | Expected observable evidence | Result |
|---|--------|------------------------------|--------|
| 1 | Seed a `StaticFindingsRecordedEvent` for the session with a known finding (e.g. an unused-import lint at `Foo.kt:12`). | The event is in `correx events {id}`. | |
| 2 | Drive the implementer→reviewer step so reviewer context is assembled. | The reviewer's input context (server log of the assembled prompt / context pack) **does not** contain a feedback entry restating the seeded static finding — `excludeStaticFindingsFromReview` stripped it. | |
| 3 | Seed a finding that does **not** match any retry-feedback entry. | Non-matching entries are untouched; only restatements of recorded static findings are dropped. | |
| 4 | **Activate** the `static_check` stage: wire a sandboxed `CommandRunner` into the orchestrator and set `static_argv` (e.g. `./gradlew detekt --console=plain`) on the role_pipeline stage, then run a workflow that produces a patch with a real lint/compile issue. | A `StaticFindingsRecordedEvent` with the tool's parsed findings appears in `correx events {id}`, and the reviewer never re-raises those issues. _(Activation is the open §B-§5 follow-up — the seam + emitter are built.)_ | |
Evidence sources: server logs of the assembled reviewer context / context pack, `correx events {id}`.
## Out of scope (explicitly NOT covered this pass)
- **Production runner activation** (open BACKLOG §B-§5 follow-up): wiring a sandboxed, workspace-scoped `CommandRunner` is itself live-QA-gated. The seam + emitter are built (`e46777e`) and unit-verified (`StaticCheckStageExecutorTest`, `StaticCheckStageTest`); check 4 exercises the activation once a runner is wired.
- The reviewer prompt half (shipped earlier, `3467826`).
## Disposition
- **PASS (partial)** → record in `RETRO.md` that the **filter** is live-verified (checks 13) and keep the §B-§5 *production-runner activation* follow-up open in BACKLOG. Do not claim the full static-first loop until check 4 passes.
- **FAIL** → numbered findings. Status: FAILED until green.
+52
View File
@@ -0,0 +1,52 @@
# QA Plan: TUI Bubble Tea v2 + native cursor — `d247b19` `85af2c6`
**Status:** DRAFT
**Run date / operator:**
**BACKLOG item:** none — operator-requested TUI work, already archived in `RETRO.md`
(2026-06-22). This plan does not gate a BACKLOG→RETRO move; it gates the *behavioral*
claims that `go test` can't prove (Shift+Enter, the real cursor, no render regressions),
which need a real kitty-protocol terminal.
---
## Preconditions
- [ ] **terminal:** kitty (or any kitty-keyboard-protocol terminal: ghostty / foot / wezterm / recent iTerm2) — Shift+Enter disambiguation depends on it.
- [ ] **tui build/branch:** `feat/backlog-burndown` at `85af2c6``cd apps/tui-go && GOTOOLCHAIN=auto go build -o /tmp/correx-tui .`
- [ ] **server up:** a correx server reachable at `--host/--port` (default `localhost:8080`) with at least one resumable session, so in-session views render. _Rebuild the server only while it's stopped (lazy classloading)._
- [ ] **config synced:** `~/.config/correx` matches `examples/*` (stale copies cause silent bugs).
- [ ] _(optional)_ `CORREX_TUI_LOG=/tmp/tui.log` to capture the `KEY type=… alt=… ` debug lines as corroborating evidence for the key checks.
## Acceptance gate (one sentence)
> The TUI is correct on v2 **iff** Shift+Enter inserts a newline in the composer without
> sending, a real blinking cursor tracks the caret in insert mode (and vanishes in normal
> mode / behind modals), and nothing in the render layer regressed from the v1 build.
## Checks
Evidence is the live TUI render (an allowed source) plus, where noted, the `CORREX_TUI_LOG` key lines.
| # | Action | Expected observable evidence | Result |
|---|--------|------------------------------|--------|
| 1 | Enter a session, press `i`, type a word | A blinking vertical-bar cursor sits at the caret; typing extends it. Log shows `KEY … type=keyRunes`. | |
| 2 | With text in the composer, press **Shift+Enter** | A newline is inserted; the input grows to a second row; nothing is submitted. Log line shows the Enter key with `shift` (no `submit`). | |
| 3 | Press **Enter** (bare) | The message submits (composer clears / turn appears). Confirms bare Enter still sends. | |
| 4 | Press **Ctrl+J**, then **Alt+Enter** (fallback paths) | Each inserts a newline without sending (parity with Shift+Enter). | |
| 5 | From insert mode press `esc` to normal mode | The blinking cursor disappears entirely (no stray bar left on screen). | |
| 6 | In insert mode, open the palette (`/` or `p`) | The modal's own filter caret blinks; **no** second cursor shows behind/around the modal. Close it → composer cursor returns. | |
| 7 | Leave the composer in insert mode and idle ~5s, then drag-select text with the mouse | Selection holds (screen isn't being redrawn out from under it) — confirms insert mode no longer forces the frame loop. | |
| 8 | Observe the terminal tab/title while in a session vs. idle | Title reads `correx — <session name>` in-session; `correx — <model>` (or `correx`) when idle. | |
| 9 | Visual regression sweep: open `?` help, stats, an `e` event inspector, a `^x` diff, the `t` artifacts viewer; resize the terminal narrow↔wide | Colors/borders/opaque panels look as before; modals scroll (`PgUp/PgDn`, `g/G`); diff viewer pages; no garbled ANSI, no off-by-one panel widths, alt-screen restores the shell cleanly on `q`. | |
| 10 | Walk the approval band (trigger a gate), `↑/↓` the queue, approve/reject/steer | Band behaves exactly as on v1 (single-key low tiers, arm→confirm T3+); steering caret still draws. | |
## Out of scope (explicitly NOT covered this pass)
- Mouse-wheel scroll (deliberately not added — would fight native text selection).
- Native cursor for the palette/files/steering/config carets (still frame-drawn `▏`; only the main composer moved to `View.Cursor`).
- Behavior on non-kitty terminals beyond the documented Ctrl+J/Alt+Enter fallback.
## Disposition
- **PASS** → set Status: PASSED with the run date; the RETRO 2026-06-22 rows for `d247b19`/`85af2c6` stand as live-verified.
- **FAIL** → file each failure as a numbered finding in `BACKLOG.md` (action + exact repro + the render/log signal that was wrong), fix, then re-run only the failed checks. Set Status: FAILED until green.
+37
View File
@@ -0,0 +1,37 @@
# Correx live-QA plans
Each `QA-<feature>.md` is the artifact that authorizes a `BACKLOG.md``RETRO.md` move:
it pins **observable evidence** (events, logs, files, TUI renders) for one shipped feature.
Template: `TEMPLATE.md`. Environment bring-up: **`ENV.md`** (+ `scripts/qa/`).
A feature is **not** "resolved" until its plan passes live. On PASS → move the BACKLOG entry
to RETRO with the run date + cited evidence; on FAIL → refile each miss as a numbered finding.
## Plans (this burndown)
| Plan | Feature | Commits | Needs |
|------|---------|---------|-------|
| `QA-research-egress.md` | per-session egress allowlist + batch source-list approval + dedicated source events | `ab7e4be` `4b0024f` `b098d87` `027ff1f` | model + **SearXNG** |
| `QA-architect-contradiction.md` | architect contradiction-check (display-only) | `4e08510` `eae0a0c` | model + **llamacpp embedder** + `project.enabled`, 2 sessions/1 process |
| `QA-llama-health-probe.md` | LLAMA_SERVER liveness probe | `64a90e7` `9eef936` `266bbf0` | static llama-server you can stop |
| `QA-idea-promotion.md` | idea-board → `.correx/project.toml` promotion | `f107ff5` | bound workspace (router model optional — can seed the event) |
| `QA-reviewer-static-first.md` | reviewer static-findings exclusion (PARTIAL — stage seam+emitter built `e46777e`; runner activation pending) | `447fc7a` `e46777e` | seed `StaticFindingsRecordedEvent` |
| `QA-brief-echo-gate.md` | **ARM-IT** plan: brief echo-back gate (off by default; risky to arm blind) | `1df7af5` | prod-candidate model |
| `QA-grants.md` | cross-session grants (PROJECT/GLOBAL) + revoke | `c36d41b` `8df0ec7` | model + a T2+ tool call, 2 sessions, 2 workspaces |
| `QA-tui-v2.md` | Bubble Tea v2 + Shift+Enter + native cursor + window title | `d247b19` `85af2c6` | **kitty-protocol terminal** + a server with a resumable session |
## Order of attack (suggested)
0. **`QA-tui-v2`** — cheapest of all: no model / network / GPU, just a kitty terminal + a server with a session to enter. Pure render/keyboard verification.
1. **`QA-llama-health-probe`** + **`QA-idea-promotion`** — cheapest, least model-dependent (health is liveness; promotion can be seeded). Quick wins.
2. **`QA-grants`** — no network/embedder; just a model that drives one repeatable T2+ tool call. The headline check (grant in session A → auto-clear in session B) is a strong, cheap signal.
3. **`QA-research-egress`** — once SearXNG is up.
4. **`QA-architect-contradiction`** — needs a real embedder + two sessions.
5. **`QA-brief-echo-gate`** — run this **before** arming the gate anywhere real; check 2 (100% parseable-echo across runs) is the go/no-go for production.
6. **`QA-reviewer-static-first`** — partial until the static-check stage seam lands.
## Still-open §F live-QA gates (no dedicated plan yet — same env applies)
These predate the burndown and remain in `BACKLOG.md` §F; author a plan from `TEMPLATE.md`
when you tackle one: Epic 15 (events/replay + MDC sessionIds), idea-board capture, clarification
loop + workflow-propose panel, the full research-workflow run, TUI kernel-steering + AMD gauge.