A freestyle stage's tools are LLM-authored in the execution_plan. At run time an
unresolvable tool name is silently dropped (resolve -> null -> mapNotNull), so the
stage runs toolless — e.g. a misspelled task_update means the implementation stage
quietly loses task tracking, invisible without a live run.
ExecutionPlanCompiler now takes the registered-tool universe and rejects a plan that
names an unknown tool, with a message identifying the tool and stage. FreestyleDriver
already turns a compile failure into a rejection the architect retries. The param
defaults to empty (validation skipped) so existing callers are unaffected; Main feeds
it toolRegistry.all().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A disposable Markdown view of the board, grouped by status with checkboxes
and claimants — rendered from the event log, never a source of truth.
TaskMarkdown.render (pure) backs GET /tasks/export[?project=].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /tasks/sync-git advances task status from commit messages: a mention
moves a task to IN_PROGRESS, a closing keyword (fixes/closes/resolves
auth-12) walks it the legal path to DONE. Only ever advances (never
regresses or touches BLOCKED/CANCELLED), so re-running is idempotent; each
advance leaves a [git <sha>] note. Acts on commits in the request body or
reads the repo's recent log (GitCommandCommitReader) — wire it to a git
hook or CI step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ranked exact/substring search: TaskSearch (all terms AND, ranked title >
key > goal > criteria > notes) over one project or the whole board via
TaskService.search. Surfaced as GET /tasks?q=, a read-only task_search tool
for agents, and a `/` filter in the TUI board.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a cross-project task board to the TUI (T / palette): a roster fetched
from GET /tasks with vim-style nav, refresh, and an enter-to-open detail
pane (goal, criteria, paths, links, notes) built from the list payload — no
extra fetch. Server: TaskService.listAll() and a project-optional GET /tasks
(scoped with ?project=, whole board without; recent-first).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The context bundle resolved only TASK and DOC link targets; ARTIFACT and
SESSION stayed raw. Add two ports (TaskArtifactResolver/TaskSessionResolver)
with apps/server adapters: artifacts resolve to producing stage/session +
content excerpt from CAS, sessions to status/intent/workflow. Unresolved
targets still fall back to raw links. Wired through the tool and REST bundle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CRUD, lifecycle transitions, work-graph links and notes over /tasks;
share link-kind inference (TaskTargetKinds) between the REST route and
task_update tool. End-to-end route tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds task tracking as a first-class event-sourced aggregate (ADR-0012). The
event log stays the only source of truth; the board is a projection, and all of
a project's task events live in one stream (tasks:<projectId>).
core:
- new core:tasks module mirroring core:sessions: TaskStatus/State/Reducer/
BoardProjector/CounterProjection/Repository + TaskService write path that
appends events via the existing EventStore (no new storage)
- task events in core:events (sealed marker TaskEvent), registered in
Serialization.kt; status is derived by the reducer from lifecycle facts
- work-graph edges: TaskLinkType + typed TaskTargetKind on the link, so bundle
resolution dispatches on the recorded kind instead of guessing from the id
- delete is a soft tombstone (TaskDeletedEvent); cancel stays a visible outcome
surfaces:
- agent tools (Tier T2 mutators, T1 read): task_create / task_update /
task_delete / task_context, registered in both the main and per-workspace
tool registries
- REST GET /tasks/{id}/context for external agents
context bundle (TaskContextAssembler):
- task fields + acceptance criteria + relevant files + resolved dependency
tasks (live status) + raw related links + notes, with a compact render()
- semantic enrichment via a TaskKnowledgeRetriever port bridged to the existing
L3 retriever (late-bound holder for composition-root ordering)
- ADR/doc resolution via a TaskDocumentResolver port + filesystem adapter
(docs/decisions/adr-*.md, padding-agnostic; *.md paths)
Unit-verified across core:tasks / infrastructure:tools / apps:server; full
gradlew check green. Not yet live-QA'd end-to-end against a running server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the grant system's wider scopes real and add a revoke producer.
Before: grants were loaded per-session (the gate folds only the running
session's own stream), so GrantScope.PROJECT — though declared and matched
in the engine — was dead in practice, and there was no GLOBAL scope or any
way to revoke a grant (ApprovalGrantExpiredEvent had no producer).
- GrantScope: add GLOBAL(toolName); make PROJECT tool-bound. Every
operator-creatable scope is now tool-bound so a grant can never be a
blanket "approve everything" (that's YOLO mode).
- DefaultApprovalEngine.scopeMatches: GLOBAL matches the bound tool in any
context; PROJECT matches when the session's projectId AND tool match.
- Cross-session ledger: PROJECT/GLOBAL grants are appended to a reserved
GRANT_LEDGER_SESSION_ID stream instead of a session's. The approval gate
now unions the ledger's grants with the session's, and derives projectId
from the bound workspace root (ProjectIdentity.of) so PROJECT grants match
later sessions on the same repo. SESSION/STAGE grants still live in (and
die with) their session.
- Revoke: ClientMessage.RevokeGrant appends ApprovalGrantExpiredEvent to the
ledger (reducer already drops it); ListGrants/GrantList expose the active
standing grants for the TUI viewer.
- Drop the server-side T2 grant ceiling per operator request: a grant may
now authorize any tier (incl. destructive T3/T4). Tool-binding is retained
as the remaining guard.
Backend only; the TUI scope picker + grants viewer follow. core:events,
core:approvals, core:kernel, apps:server compile; approvals/events/kernel/
server suites green; new GrantScopeMatchingTest (5) covers GLOBAL/PROJECT
match, project isolation, and the no-cap T4 path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On session start, discover CLAUDE.md and AGENTS.md at the bound workspace root and inject
their (concatenated, header-labeled) contents as an L0 system context entry for every stage —
mirroring the .correx/project.toml ProjectProfile path end to end. Recorded as
AgentInstructionsBoundEvent (invariant #9) so replay reads the recorded fact, not the live
file; folded into SessionState.boundAgentInstructions and rendered by buildAgentInstructionsEntry
right after the project profile. AgentInstructionsLoader reads root-only, both files if present.
Bound via ServerModule.bindAgentInstructions alongside bindProjectProfile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New WS query: ClientMessage.ListFiles{sessionId} -> ServerMessage.FileList{paths}
(@SerialName "file.list"). StreamQueries.listFiles resolves the session's bound
workspace root (latest SessionWorkspaceBoundEvent) and lists it via WorkspaceFiles —
a path-only walk that reuses RepoMapIndexer's directory-ignore convention but, unlike
the indexer, never reads file contents and includes every file type (configs/assets,
not just source+markdown), sorted and capped. Tests: WorkspaceFiles (types/ignore/cap/
missing) + file.list wire round-trip. Go @ autocomplete consumes this next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
64a90e7 gated the probe on a static [[providers]] llamacpp url, missing the primary
managed path (sample-config.toml's [[models]], where correx spawns llama-server at
[models].host:port and registers no provider entry). Fall back to that host:port when
[[models]] is configured so the LLAMA_SERVER subject is monitored on the common setup.
Surfaced while drafting the A§4 live-QA plan. Compile green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the built-but-unregistered LlamaServerHealthProbe (266bbf0) into the health monitor:
when a `llamacpp` provider URL is configured, add an HttpLlamaLivenessClient-backed probe
(dedicated CIO client, 3s per-ping timeout, shutdown-hook close) to the probe list. Gated
on a configured provider URL so a model-less box never reports a spurious LLAMA_SERVER
DEGRADED. Replaces the Main.kt TODO(wiring). main() is not unit-tested; the probe + client
are already covered by 266bbf0 — compile + :apps:server:test + detekt green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the display-only ArchitectContradictionChecker (eae0a0c) live: ServerModule.start()
subscribes to the architect stage's `design` ArtifactCreatedEvent, resolves the decision
text (prefers the design schema's `approach` field; falls back to the capped artifact
text), runs checker.check(...), and appends the non-null PossibleContradictionFlaggedEvent.
Live-only and never halts a stage (runCatching). Checker built in Main.kt gated on
project.enabled (same L3 "project:" namespace ProjectMemoryService.persist populates) and
threaded into ServerModule as a nullable param. Replaces the standing TODO(wiring) in Main.kt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promote an auto-captured idea from the cross-session board into the curated per-repo
profile at <workspaceRoot>/.correx/project.toml as a `conventions` entry. New
IdeaPromotedEvent tombstones the idea off the board (IdeaReader treats it like a
discard) and records the promotion as a fact; the server PromoteIdea handler loads
the profile, appends the idea text (deduped via ProjectProfile.withConvention), and
writes it back. Adds ProjectProfileWriter (escape-aware TOML serialize/write) and
makes SimpleToml's reader unescape \\ and \" symmetrically with the writers
(fixes a pre-existing read/write asymmetry surfaced by the round-trip test).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /sessions/{id}/approve-sources extracts distinct hosts from a source list
(SourceListHosts.extract) and emits one EgressHostsGrantedEvent for the session, so
the live per-session egress allowlist auto-clears subsequent web_fetches to those
hosts — approve the list once instead of per-URL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pause narrations are tagged with a pauseKey; a per-session pending-pause set gates
the lane worker so a resolved/resumed pause is skipped instead of blending with the
current status. ApprovalDecisionResolved drops the specific request's pause;
OrchestrationResumed clears all session pauses. Replay-safe (no recorded events change).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PossibleContradictionFlaggedEvent + RelatedDecision (registered + serialization test)
- ArchitectContradictionChecker: L3 similarity retrieval over prior decisions,
threshold-gated, fake-testable; non-blocking (surfaces candidates only)
- live wiring left as documented TODO (needs an architect-decision emit hook +
in-session decision embedding into L3)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Conservative type-declaration pattern (class/interface/struct/enum/record);
no method capture to avoid false positives. (BACKLOG I)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- EventStoreLatencyProbe: times a side-effect-free lastGlobalSequence read,
wired into HealthMonitor; HealthConfig.eventStoreWarnLatencyMs (BACKLOG A §4)
- LlamaServerHealthProbe: liveness + tokens/sec degradation trend, injectable
client (HttpLlamaLivenessClient over Ktor); deterministic unit tests
- llama probe left unregistered (TODO) — HttpClient not in monitor scope yet
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- repomap: tag prefix match requires trailing ':' so /repo can't match /repo2
(L3RepoKnowledgeRetriever filter + ProjectMemoryService write; the existing
existsByTurnIdPrefix check already included the delimiter)
- WorkspaceStateProbe fingerprint joins entries with newline to match docstring
- regression test for /repo vs /repo2 non-collision
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The repo-map turnId tag and the L3 retriever filter used 'repomap:$repoRoot' with no
delimiter, so a retriever for /repo also matched /repo2 entries (startsWith). Add a trailing
':' to the tag (no-stateKey branch) and the retriever filter so the repoRoot boundary is
explicit. Test: /repo retrieval no longer leaks /repo2 entries, still matches its own
with/without a stateKey suffix.
First slice of the plan-generation pipeline epic. A pure-Kotlin PlanLinter runs over the
compiled freestyle ExecutionPlan (WorkflowGraph) before it is locked, complementing
ExecutionPlanCompiler (which throws on unreachable/unknown-kind/bad-edge) with the checks
it does NOT make:
- HARD unproduced_need — a stage needs an artifact no stage produces. Real gap: only the
TOML loader checked this; the freestyle compiler did not, so such plans compiled and
failed at runtime.
- HARD trap_state — a stage with no path to the terminal (inescapable loop / dead end).
Uses terminal-reachability, NOT "any cycle", so legitimate verdict-gated review loops pass.
- SOFT stage_count / fan_out / empty_brief / duplicate_brief — scored, never blocking.
FreestyleDriver lints after compile and records PlanLintCompletedEvent (pure function of
the recorded plan → replay-safe, no env observation in v1); a hard failure rejects the plan
(ExecutionPlanRejectedEvent source="lint") before the operator is asked or the plan locks,
so a broken plan never executes. Pays off now for single-plan freestyle; becomes the
per-candidate filter when multi-candidate generation (Slice 2) lands.
Deferred to later slices: file-path grounding + symbol resolution (needs an index),
token-budget/ADR-bypass checks, and a dedicated :core:planning module.
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.
Surface the health subsystem (llama/event-store/disk probes) in the TUI,
mirroring the session-stats pane: ClientMessage.GetHealthChecks ->
ServerMessage.HealthChecks (health.checks, reuses HealthReport) ->
StreamQueries.healthChecks via HealthInspectionService; Go OverlayHealth on
key H + palette 'health checks', pull-based fetch, per-subject status with
degraded loud. Empty/disabled -> visible fallback (no blank/lie). Go+Kotlin
golden tests pin the wire contract field-for-field. Observability spec §4/§3.
Third HealthProbe (after disk + llama): times a cheap lastGlobalSequence()
read as an event-store responsiveness heartbeat — DEGRADED on throw or when
latency exceeds eventStoreLatencyWarnMs (default 500). Read-only by design
(no probe writes into the source-of-truth log; Invariant #1). Plugs into the
existing HealthMonitor; no new events. Observability spec §4.
Also wraps a long line in LlamaServerHealthProbeTest to clear a detekt
MaxLineLength finding introduced in aaf896d.
Second HealthProbe (after disk-watermark): liveness via GET /health on the
configured llama base URL + tokens/sec degradation watch derived from recorded
InferenceCompletedEvents (windowed average vs configurable threshold, opt-in via
llamaTpsWarnBelow). Plugs into the existing HealthMonitor edge-detection loop;
no new events. Observability spec §4.
ListIdeas -> idea.list (StreamQueries folds the board from the log, replay-neutral)
and DiscardIdea -> append IdeaDiscardedEvent landed in the capturing session, then
reply with the refreshed board. IdeaDto on the wire.
Map WorkflowProposedEvent to a workflow.proposed ServerMessage carrying the
candidates + original request. No new ClientMessage: a pick reuses StartSession
(launch the chosen workflow seeded with the request) and the manual-answer slot
reuses ChatInput.
Surfaces the clarification loop over the wire: ServerMessage.ClarificationRequired
(clarification.required, carrying the structured questions) is mapped from
ClarificationRequestedEvent; ClientMessage.ClarificationResponse carries the
operator's answers and is routed to orchestrator.submitClarification on both
the global and session streams. Reuses the core ClarificationQuestion/Answer
types as the wire shape. Round-trip tests for both directions.
Makes the research feature runnable end-to-end, off by default.
- config: [tools.research] (enabled, searxng_url, max_results, max_fetch_bytes).
- registration: web_search/web_fetch are built into BOTH the default and per-workspace
tool registries when research.enabled, sharing one HTTP client threaded from Main
(none built on the static path). Egress stays harness-enforced: web_fetch is T2
(operator-approved) and the existing NetworkHostRule still applies.
- workflow: examples/workflows/research.toml — decompose → gather → report, with the
three artifact schemas and prompts. Fan-out (search per sub-question, fetch per source)
runs as repeated tool calls inside the gather stage (Correx has no parallel agents);
per-source synthesis into the dossier is the compression step, so the report stage
consumes summaries, never raw pages. ResearchWorkflowTest validates the graph contract.
To run: set [tools.research].enabled, register the 3 [[artifacts]], copy research.toml +
prompts + schemas into the workflows dir, start SearXNG. Launch like any workflow (the
T2 fetch approval surfaces as an approval card; the report opens in the artifact viewer).
Follow-ups (noted, not blocking): batch fetch-approval at the source-list level (§3),
a dedicated SourceFetched/LowQualityExtraction event (quality + content hash are already
in tool-result metadata), dynamic per-session egress allowlist, and the web approval client (§6).
Health states are events. A HealthMonitor polls registered HealthProbes on an
interval and appends HealthDegraded/HealthRestored only on a status transition
(hysteresis), so health is observable, replayable, and renderable like every
other fact in the log. The probe reads nondeterministic environment state and
records it at observe-time (Invariant #9); HealthProjection rebuilds current
health by replay, never re-probing (Invariant #8). Monitor is live-only, started
in ServerModule.start alongside narration; seeded from the projection so a
restart does not re-emit a degraded the log already records.
First concrete probe: DiskWatermarkProbe — event-log + CAS size and growth rate,
warning before the 2am surprise. Read surface mirrors `correx stats`:
GET /health/checks replays the system session and `correx health` renders it.
Health events ride the reserved __system__ session, filtered from operator
session listings.
LLAMA_SERVER (liveness + tokens/sec trend) and EVENT_STORE (append/fsync latency)
probes plug into the same HealthProbe framework as follow-ups.
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.
Implements observability-spec §2 + §3-tier-1: metrics as a pure fold over the
event log, no third pillar, no OTel. Every metric is replayable over the full
historical log because it derives from events other contexts already emit — no
new EventPayload subclasses, so no serialization registry change.
- MetricsProjection: Projection<MetricsState> folding inference/tool/approval/
stage/workflow events into raw sums; approval request→decision latency is
correlated in-fold via a transient pending-request map.
- MetricsInspectionService: rebuilds via DefaultEventReplayer, derives read-side
ratios (token throughput, avg approval wait, session wall-time accounting split
into inference / tool / approval-wait / other) into MetricsReport.
- GET /sessions/{id}/metrics route, mirroring the replay endpoint.
- `correx stats <sessionId>` CLI client (--json passthrough), mirroring replay.
- Tests: seeded-event fold correctness with controlled timestamps (7) + CLI
render smoke tests (4).
Implement T1 of observability epic: every server log line carries sessionId
(and stageId where known) via SLF4J MDC, propagated across coroutine
suspension points.
Changes:
- Create com.correx.apps.server.logging.Correlation: withSessionContext()
helper that sets MDC (sessionId, stageId), wraps block in MDCContext(),
and restores prior values on exit; supports safe nesting
- Wrap launchSessionRun() orchestrator loop in withSessionContext(sessionId)
so all session logs carry the sessionId
- Wrap GlobalStreamHandler ChatInput and StartChatSession onUserInput call
sites in withSessionContext(sessionId)
- Wrap SessionStreamHandler ChatInput onUserInput in withSessionContext(sessionId)
- Update log4j2.properties patterns: append
%notEmpty{ [%X{sessionId}]}%notEmpty{ [%X{stageId}]} before %msg
on both console and file appenders
- Add kotlinx-coroutines-slf4j:1.9.0 dependency for MDCContext
Test (6 tests):
- withSessionContext sets/restores sessionId ✓
- withSessionContext sets/restores stageId ✓
- Nested contexts stack and restore correctly ✓
- MDC cleared when stageId not provided ✓
- Propagation across coroutine boundaries via MDCContext ✓
- Cleanup after block exit ✓
Acceptance:
- Zero behavior change; no core module touched ✓
- All imports verified as used ✓
- No bare try-catch; try/finally used correctly ✓
- Detekt-compliant ✓
- Full test suite passes (120 tests, 119 passed, 1 skipped) ✓
A stale RUNNING session skipped by resumeAbandoned stayed RUNNING in
every projection — the TUI showed a phantom running workflow that no
process was executing, and each boot re-logged the same skip. Parking
now emits OrchestrationPaused(reason=ABANDONED_STALE): status becomes
PAUSED (truthful — not executing, resumable via POST /resume) and
subsequent boots skip it on status alone.
#2: approval pauses narrate from ApprovalRequestedEvent (carries
tool/tier/preview/stage directly) instead of OrchestrationPaused +
a pending-approvals map the next event had not yet populated — the
narrator always lost that race and produced generic text.
#3: NarrationTrigger now carries the triggering event's stageId; the
narration status line and RouterNarrationEvent.stageId use it instead
of the router projection's currentStageId, which terminal events have
already cleared ('Stage: none').
#9: ExecutionPlanLocked -> plan.locked and ArtifactValidated ->
artifact.validated mapped to the TUI (Go: feed lines, plan shows the
compiled stage chain); ArtifactValidating explicitly dropped.
#10: blank router turn guard — a length-finish with empty text now
emits an explanatory turn naming the max_tokens budget instead of a
silent blank; blank steering responses no longer emit steering notes.
#12: resumeAbandoned only auto-resumes sessions whose last event is
within orchestration.resume_abandoned_max_age_minutes (default 24h,
0 disables); staler sessions stay parked for POST /resume. Knob is
TUI-editable and persisted.
launchSessionRun and launchSessionResumeWithRehydrate each inlined their
own copy of the freestyle phase-2 handoff with different guard sets — the
run path had none, so a failed planning run still called lockAndRun and
emitted a spurious ExecutionPlanRejected. Both now call one handoff that
checks graph id, planning result, and an existing plan lock before
locking and running phase 2.
resumeAbandonedSessions (boot pickup of RUNNING sessions) and the
restart-while-approval-pending subscription still used the bare
launchSessionResume: no artifact-cache rehydrate and no freestyle
plan-lock handoff. A freestyle session killed mid-planning was grabbed
by the boot path before the operator could POST /resume, completed the
planning graph, and stranded — WorkflowCompleted but never locked
(live repro: session b36b0e45, 2026-06-12).
Both paths now use launchSessionResumeWithRehydrate; the bare launcher
is deleted.
Three gaps found by live QA (kill between planning-complete and plan lock):
- launchSessionResumeWithRehydrate never handed off to FreestyleDriver, so a
resumed freestyle session stranded at planning-complete with no plan lock
and no phase 2; now locks+runs when planning completed and no
ExecutionPlanLockedEvent exists yet.
- resume route 400'd on phase-2 sessions (workflowId freestyle-<sid> is
compiled, never registered); now recompiles the locked plan after
rehydrating the artifact cache.
- orchestrator.resume threw when currentStageId was terminal ('done') or
unknown to the graph; now returns WorkflowResult.Completed.
Chat sessions never emitted ProjectProfileBoundEvent — only the workflow
path did — so router triage always saw a null profile and could not cite
conventions or commands. Extract the binding from launchSessionRun into
ServerModule.bindProjectProfile and call it from handleStartChatSession.
h1-h3 headings become the file's symbols, so design docs and GDDs are
embedded and retrievable alongside code. .md files now also participate in
workspace fingerprinting (INDEXED_EXTENSIONS derives from pattern keys).
Adds 'gd' to SYMBOL_PATTERNS (func/class_name/class/signal/enum at column 0,
optional static prefix). Because INDEXED_EXTENSIONS derives from the pattern
keys, .gd files now also participate in WorkspaceStateProbe fingerprinting,
making fp: state keys content-sensitive for Godot repos.
- Add existsByTurnIdPrefix(prefix) to L3MemoryStore interface; implemented in
InMemoryL3MemoryStore (mutex-guarded) and TurboVecL3MemoryStore (metadata map).
All test fakes (TrackingL3MemoryStore, CapturingStore) updated.
- Introduce RepoMapIndexerPort interface; RepoMapIndexer implements it. Allows
injection of test doubles without framework overhead.
- ProjectMemoryService.indexAndRecord: reads latest WorkspaceStateObservedEvent
from event store (invariant #9 — recorded fact, not re-probe); skips scan+embed
when L3 already holds entries for "repomap:<root>:<stateKey>"; passes stateKey
to RepoMapComputedEvent; embeds each scanned entry after append; per-entry
failures warn-logged (CancellationException rethrown).
- Tests: InMemoryL3MemoryStoreTest +2 cases; new ProjectMemoryServiceReuseTest
covers same-key skip, changed-key re-index, no-observation always-reindex,
and embedding turnId tag verification.