Two coupled gaps from tracing a real run:
1. Freestyle implements in phase 2 via stages compiled from the architect's
execution_plan (ExecutionPlanCompiler sets allowedTools = stage.tools), so the
static allow-lists never reach it and architect_freestyle.md banned every tool
but the file four. Teach the architect to thread an analysis-referenced task
through the plan: implementing stages get task_context/task_update and claim +
submit_for_review; the final/review stage completes it. No task referenced → no
task tools, and the plan never creates one.
2. Give the analyst task_create so the work is framed as a tracked item up front
(role_pipeline + freestyle). "Read-only" for the analyst means it writes no
files; a task is an event-log entry, not a file write — task_create is T2, so
opening one is approval-gated. The analyst names the new id in the analysis so
the implementer claims it and the reviewer completes it; the implementer now
creates only as a fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tool availability is a strict per-stage allowlist, so the task doctrine was
inert in role_pipeline — no stage granted the tools. Add them where they fit
each stage's tier and role, and point the stage prompts at the lifecycle:
- analyst (read-only): task_search/task_context to find related or duplicate
work and ground the analysis.
- implementer: full set — claim before working, submit_for_review once
verification passes, create one if the work warrants tracking.
- reviewer: task_context to judge against the task's own criteria, task_update
to complete it on an approved verdict.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two reviewer-reliability tracks, both deterministic and unit-verified (no model/network).
§B-§5 — static_check stage seam. The StaticAnalysisRunner + StaticFindingsRecordedEvent +
reviewer-context filter already existed; what was missing was a stage that runs the tool and
emits the event. Added:
- StaticCheckStageExecutor: reads stage metadata (static_tool/static_argv), runs the configured
command via StaticAnalysisRunner, returns a StaticFindingsRecordedEvent. No-op (empty findings)
when no runner is wired or no command is configured — safe to carry unconfigured.
- A deterministic-stage seam in DefaultSessionOrchestrator.enterStage: any stage with
metadata["stage_type"] == "static_check" is run by the executor instead of the LLM subagent,
then advances on its (unconditional) exit edge.
- TomlWorkflowLoader: stage_type/static_tool/static_argv fields → StageConfig.metadata.
- role_pipeline.toml: a static_check stage between implementer and reviewer (no-op until
static_argv + a CommandRunner are set; activation is live-QA-gated, see StaticAnalysisRunner doc).
§B-§6 — critique-outcome producer. The CritiqueFinding type + CriticCalibrationProjection existed
but nothing fed them. Added:
- CritiqueFindingsRecordedEvent (+ CritiqueVerdict): the producing side — a critic's findings +
verdict for one review iteration, carrying modelHash for per-model calibration.
- CritiqueOutcomeCorrelator: pure loop-resolution logic deciding UPHELD (fixed between rounds) /
DISMISSED (persisted into an approved final) / INCONCLUSIVE (open at a non-approved terminal),
per critic (role + modelHash) and per finding id.
- A hook in completeWorkflow/failWorkflow that correlates recorded findings into
CritiqueOutcomeCorrelatedEvents at loop resolution — no-op when none recorded, idempotent.
(LLM-side finding emission stays a separate model-gated activation.)
Tests: StaticCheckStageExecutorTest (4), StaticCheckStageTest integration (1, fake runner →
event + transition), CritiqueOutcomeCorrelatorTest (8), CritiqueCalibrationWiringTest integration
(1, seeded findings → outcomes at completion), updated RolePipelineWorkflowTest. Full suites for
core:events/kernel/critique, infrastructure:workflow, testing:integration green; detekt clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Run operator-configured static-analysis commands (compiler/detekt/formatters) as a
deterministic harness step on the producing stage, before the LLM reviewer. A stage
declares `static_analysis = [commands]`; after it produces its artifact, each command
runs in the workspace root and the results are recorded in a StaticAnalysisCompletedEvent
(invariant #9 env observation — recorded live, folded on replay, never re-run). A
non-clean command fails the stage retryably with its output fed back verbatim (§2:
compiler/test output is ground truth), so the implementer fixes it and only static-clean
code ever reaches the reviewer. This makes §5's "reviewer context excludes static findings"
mechanical — the findings are resolved upstream, so the reviewer can't waste inference on
what detekt catches for free; no output-parsing, no context plumbing.
- StaticAnalysisCompletedEvent + StaticAnalysisFinding (core:events) + registration.
- StaticAnalysisRunner seam + ProcessStaticAnalysisRunner (whitespace-split argv, stderr
merged, timeout→nonzero exit) in core:kernel; wired (nullable) into OrchestratorEngines
and the server. Null runner / no workspace root → logged no-op, never blocks the run.
- StageConfig.staticAnalysis + `static_analysis` TOML field; runStaticAnalysis folded into
a runPostStageGates sequence (produces → grounding → echo → static analysis).
- Documented `static_analysis` on the role_pipeline implementer stage (commented; commands
are workspace-specific). The reviewer prompt half shipped in 3467826.
New brief_echo stage before the planner in role_pipeline: the model restates
the analyst brief as a structured artifact, and a deterministic gate
(checkBriefEcho, opt-in via brief_echo=true) diffs the restatement against the
original brief. On divergence — dropped requirements (Jaccard coverage < 0.34)
or hallucinated files — it emits BriefEchoMismatchEvent and fails the stage
retryably, so a misread brief never reaches plan generation.
The diff (BriefEchoDiff) is a pure function of two recorded artifacts, so it is
replay-safe and records no observation; the event fires only on mismatch, for
audit. Symbols are recorded but non-blocking in v1. Mirrors the groundBrief-
References gate. role_pipeline only; freestyle_planning wiring is a follow-up.
Runtime install (like research): copy brief_echo.json + brief_echo.md and the
[[artifacts]] block into ~/.config/correx.
The reviewer prompt told the model to judge "the analysis requirements," but
the reviewer stage only needed [patch, impl_plan] — it never actually received
the analysis artifact, so the acceptance criteria it was meant to check against
were never in its context. A reliability gate checking against criteria it
can't see is theater.
- reviewer now needs `analysis`, so the diff is judged against concrete,
pre-stated acceptance criteria (§5 narrow question) rather than whole files
against taste; the §3 minItems enforcement guarantees those criteria are
non-empty.
- reviewer.md operationalizes §5's two principles: evaluate the diff against
each requirement (findings must map to a requirement/plan-step/defect), and
don't re-report what deterministic tools already catch (compiler/detekt/tests
— trust verification, point at failures, don't re-derive them).
This is the workflow-level realization of §5. The full static-first
infrastructure (running static tools as a pipeline step and mechanically
excluding their findings from reviewer context) needs a static-findings event
representation and is deferred. §6 critic calibration needs runtime history
before it can say anything (spec ordering #5).
Local analysts hallucinate file paths the same as planners. A stage can now
opt in with `ground_references = true`: after it produces its brief, every
workspace-relative file path the brief named is checked for existence, and a
path that doesn't exist fails the stage (retryable) with the misses fed back
("Reference only files you have read") so the model re-grounds.
Grounding probes the filesystem directly rather than the repo map — the map
is recency-ranked and depth/extension-limited, not a complete existence
oracle, so "absent from the map" can't be a hard failure. Existence is a
nondeterministic observation, so the result is recorded as a
BriefGroundingCheckedEvent (invariant #9); replay reads it back and never
re-probes.
- BriefGroundingCheckedEvent + GroundingReference, registered in eventModule
- BriefReferenceExtractor: pure, deterministic pull of relative file-path
references from a brief artifact JSON (needs a '/' and a dotted extension;
skips modules, bare names, URLs, absolute/parent paths — so coarse
"subsystem" mentions the brief is allowed to make aren't false-flagged)
- SessionOrchestrator.groundBriefReferences on the post-verifyProduces path,
gated by stage metadata; uses the existing worldProbe
- TomlWorkflowLoader `ground_references` → metadata; role_pipeline analyst
opts in
Scoped to file references with a '/' + extension (not bare basenames, which
a depth-limited probe can't resolve), so a flagged miss is high-confidence.
The symbol-grounding half of §3 is left for a real symbol index.
Scope creep — an implementer writing files it never declared — is the
dominant local-model failure that compiler/tests don't catch. A stage can
now declare a `writes` manifest (workspace-relative globs); a FILE_WRITE
that resolves inside the workspace but outside the declared set is raised
as PATH_OUTSIDE_MANIFEST → BLOCK, so the model gets the violation as tool
feedback and retries within scope (the §2 bounded-retry signal).
Implemented as a plane-2 rule beside PathContainmentRule (same effect-based
dispatch on FILE_WRITE, same symlink-safe WorldProbe resolution, same
recorded observations so replay reads them back — invariant #9). An empty
manifest is unrestricted; out-of-workspace targets stay PathContainmentRule's
concern, so the two rules don't double-flag.
- StageConfig.writeManifest + TomlWorkflowLoader `writes` parsing
- ToolCallAssessmentInput.writeManifest, threaded from the active stage via
SessionOrchestrator.runPlane2Assessment
- new ManifestContainmentRule, registered in the server assessor
- shared candidatePathStrings extracted so both path rules judge the same
target set
- role_pipeline.toml documents the option on the implementer stage
The dedicated ManifestViolationEvent the spec names is not added: the
violation is already recorded replayably in ToolCallAssessedEvent (issue +
observations + BLOCK) and surfaced in the TUI rationale band. A first-class
event is worth adding only once reconciliation consumes it.