This document tracks the phased implementation plan for the Reasoning Layer pipeline.
All decision data lives in Postgres — no markdown WAL files. Context logs and exports are derived views generated from the database on demand.
Goal: Promote the Decision model to a proper append-only WAL entry. Foundation for all subsequent phases.
| Change | Detail |
|---|---|
reasoningArc String? on Decision |
Captures the dialogue/context that led to the entry — richer than Q+A alone |
sessionId String? on Decision |
FK placeholder for Phase 2 RefiningSession; present in schema so Phase 2 needs no migration |
| Rollback validation — all write paths | supersedes_id must reference a valid existing decision. Previously missing from the Slack modal submit handler; now enforced everywhere |
reasoning_arc in all API responses |
Included in GET /decisions, GET /decisions/:id, GET /decisions/export, GET /decisions/export-since |
Migration 20260512000001_wal_upgrade |
ALTER TABLE decisions ADD COLUMN reasoning_arc TEXT; ADD COLUMN session_id TEXT |
The existing Q→A flow is unchanged. Routing a question and getting a single Slack reply still
creates a WAL entry immediately — reasoning_arc is null for fast-path entries. Phase 2
sessions will populate it.
Goal: Questions are fire-and-forget from the developer's perspective. Routing a question does not block the developer — they state an interim assumption and proceed immediately. Answers land asynchronously into the WAL and are surfaced at the next natural checkpoint, mirroring an async ask pattern where answers drift in rather than blocking the developer.
| Phase 1 fast path | Phase 2 async |
|---|---|
| Route question → wait → DM → resume | Route question → state assumption → proceed |
| Answer is a gate | Answer is an event that drifts in later |
| Developer blocked | Developer always moving |
model RefiningSession {
id String @id @default(uuid())
promptId String
prompt Prompt @relation(fields: [promptId], references: [id])
questionId String? @unique
question Question? @relation(fields: [questionId], references: [id])
topic String // short label for the topic being refined
// open | settled | tabled | abandoned
status String @default("open")
// decision | wont_do | table | branch | rollback | observation
outcome String?
// ID of the interim table WAL entry written when question was routed — superseded on settlement
interimDecisionId String?
createdAt DateTime @default(now())
settledAt DateTime?
messages SessionMessage[]
decisions Decision[]
@@map("refining_sessions")
}
model SessionMessage {
id String @id @default(uuid())
sessionId String
session RefiningSession @relation(fields: [sessionId], references: [id])
// developer | reviewer | ai
role String
content String
slackTs String? // Slack message ts if this turn came via Slack
createdAt DateTime @default(now())
@@map("session_messages")
}| Status | Meaning |
|---|---|
pending |
Generated, not yet answered or routed |
answered_locally |
Developer answered inline; WAL entry written immediately |
routed |
Sent to Slack; interim table WAL entry written; developer proceeds |
resolved |
Slack answer landed; final decision WAL entry written |
| Method | Path | What it does |
|---|---|---|
POST |
/api/sessions |
Start a refining session for a question or open topic |
GET |
/api/sessions/:id |
Full session state + message history |
POST |
/api/sessions/:id/messages |
Add a dialogue turn (developer, reviewer, or AI) |
POST |
/api/sessions/:id/settle |
Settle → writes final decision WAL entry; sets supersedes_id → interim table entry; reasoning_arc assembled from full message history |
POST |
/api/sessions/:id/branch |
Decompose → creates N child sessions; parent marked branched |
POST |
/api/sessions/:id/table |
Defer → creates a table WAL entry; session marked tabled |
POST |
/api/sessions/:id/abandon |
Explicitly close without a WAL entry |
GET |
/api/sessions/catch-up |
?repo=<path>&since=<iso> — returns decisions that landed + questions still in-flight since a timestamp |
When POST /api/questions/:id/route is called:
- Question is posted to the Slack thread as before
- A
RefiningSessionis opened withstatus: "open" - An interim
tableWAL entry is written immediately capturing the developer's working assumption:## <hex-id> — table — <date> In-flight: <question text> Proceeding with assumption: <assumption text supplied by developer> Routing to: @reviewer via Slack Triggers to revisit: Slack answer lands - The developer proceeds — no DM, no waiting
When a Slack answer lands (plain reply prefixed with /settle, /table, or /wont-do, or via the Settle button):
- Backend writes the final WAL entry (
decision,wont_do, etc.) - If an interim
tableentry exists for the question, the new entry setssupersedes_id→ that entry — the assumption is now resolved reasoning_arcis assembled from the fullSessionMessagehistory- A Slack DM is sent to the developer as FYI only: "Decision landed: [summary]. Your next session will pick this up."
- No developer action required to "resume"
Before Step 1 (submit task) of the pipeline, the agent calls GET /api/sessions/catch-up.
Response surfaces:
- Decisions that landed since the last session timestamp (resolved interim
tableentries) - Still in-flight questions (routed, not yet answered)
Claude surfaces these before the developer describes the new task:
"Since your last session: 2 decisions landed — [summary]. 1 question is still in-flight with @reviewer — proceeding with the prior assumption."
GET /api/prompts/:id/enriched gains a second pass:
- Past decisions (existing semantic search) — treated as hard constraints
- In-flight questions with their interim assumptions — flagged explicitly so Claude knows where the working assumptions live
A Slack reply that starts with /settle on the very first reply still settles immediately —
a session is opened and immediately closed. Backwards compatible; no API changes for existing callers.
# Catch-up cadence — runs before Step 1:
GET /api/sessions/catch-up?repo=<path>&since=<last_session_ts>
# After routing — Claude can participate in multi-turn sessions:
POST /sessions/:id/messages { "role": "ai", "content": "..." }
# When session is ready to settle (reviewer-driven, not developer-driven):
POST /sessions/:id/settle {
"outcome": "decision",
"answer": "...",
"rationale": "...",
"reasoning_arc": "Assembled summary of the dialogue arc"
}Goal: Track which files in a repo are governed by which decisions. Surface drift when code moves ahead of the WAL. Optionally block commits to tracked files without a corresponding WAL entry.
| Change | Detail |
|---|---|
TrackedArtifact + ArtifactDecisionLink schema |
Files linked to decisions via junction table |
GET/POST/DELETE /api/repos/:id/artifacts |
Track, list, and untrack files |
POST /api/repos/:id/artifacts/drift |
Drift detection — client passes file timestamps |
POST /api/artifacts/drift |
Convenience endpoint — repo path in body (used by pre-commit hook) |
POST/DELETE /api/decisions/:id/link-artifacts |
Link/unlink decisions to file paths; auto-tracks if not yet tracked |
| Enrichment Pass 2 | Linked decisions injected as hard constraints in GET /prompts/:id/enriched |
| Superseded decision flag fix | Now correctly detects when a constraint has been superseded by a newer rollback entry |
scripts/pre-commit |
Python3 hook — warn or block commits when staged files have unresolved drift |
scripts/install-coherence-hook.sh |
One-command installer: copies hook + wires git config core.hooksPath |
| Agent file v1.4.0 | Step 3b: after recording decisions, suggests linking them to tracked files |
Without this phase, decisions and code are disconnected — a decision is made in Slack, stored in Postgres, but nothing checks whether the relevant source files were actually updated to reflect it. The design↔code loop. Phase 3 closes it.
model TrackedArtifact {
id String @id @default(uuid())
repoId String
repo Repo @relation(fields: [repoId], references: [id])
filePath String // relative path within the repo
description String? // role this file plays (e.g. "canonical DB schema")
createdAt DateTime @default(now())
links ArtifactDecisionLink[]
@@unique([repoId, filePath])
@@map("tracked_artifacts")
}
model ArtifactDecisionLink {
artifactId String
artifact TrackedArtifact @relation(fields: [artifactId], references: [id])
decisionId String
decision Decision @relation(fields: [decisionId], references: [id])
@@id([artifactId, decisionId])
@@map("artifact_decision_links")
}| Method | Path | What it does |
|---|---|---|
POST |
/api/repos/:id/artifacts |
Track a file — body: { file_path, description? } |
DELETE |
/api/repos/:id/artifacts/:artifactId |
Untrack a file |
GET |
/api/repos/:id/artifacts |
List tracked files + their linked decisions |
GET |
/api/repos/:id/artifacts/drift |
Files whose linked decisions are newer than the file's last git commit timestamp |
POST |
/api/decisions/:id/link-artifacts |
Link a decision to one or more file paths — body: { file_paths: string[] } |
DELETE |
/api/decisions/:id/link-artifacts |
Unlink a decision from file paths |
GET /api/repos/:id/artifacts/drift works as follows:
- Fetch all
TrackedArtifactrows for the repo - For each artifact, fetch its linked decisions sorted by
createdAt desc - Compare the most recent linked decision's
createdAtagainst the file's last git commit timestamp (passed by the client as?file_timestamps=<json>or polled via the VS Code extension which has filesystem access) - Return artifacts where
latest_decision.createdAt > file_last_committed_at
Response shape:
{
"drifted": [
{
"file_path": "packages/backend/prisma/schema.prisma",
"description": "canonical DB schema",
"latest_decision": {
"hex_id": "a3f2e71",
"entry_type": "decision",
"question_text": "Should we add soft-delete to the users table?",
"answer": "Yes — add deletedAt nullable timestamp",
"created_at": "2026-05-12T10:00:00Z"
},
"file_last_committed_at": "2026-05-10T08:00:00Z"
}
]
}Runs a guided scan of the workspace:
- Reads the workspace file tree, excluding
node_modules/,.git/,dist/, build artifacts, and auto-generated files (detected by header markers) - Asks the developer: All / None / Interactive (default: Interactive)
- In Interactive mode, proposes each candidate file one at a time — developer accepts or declines, optionally editing the role description
- Accepted files are written to
POST /api/repos/:id/artifacts - A summary is shown: N files tracked, M declined
Single-file tracking. Tracks the currently open file with an optional description prompt.
GET /api/prompts/:id/enriched gains a second pass on top of semantic search:
- For the task's repo, fetch all
TrackedArtifactrows - If any file in the task's
openFilePathor the task content matches a tracked artifact, prepend its linked decisions as hard constraints at the top of the enriched prompt - If any linked decision has been superseded (has a
rollbackentry pointing to it), flag it explicitly: "⚠ Decisiona3f2e71has been superseded byb9c1d3e— use the newer entry as the constraint"
New VS Code command: Reasoning Layer: Generate Coherence Hook
Writes .githooks/pre-commit to the workspace root. On git commit:
- Reads the list of staged files
- Calls
GET /api/repos/:id/artifacts/driftwith the staged file paths - If any staged file is a tracked artifact with drift:
- Warn mode (default): prints the relevant decisions and continues the commit
- Block mode (opt-in via
reasoning-layer.hookMode: "block"): exits non-zero, blocking the commit until the developer acknowledges or creates a new WAL entry
Wired via git config core.hooksPath .githooks.
Hook behaviour is configurable in VS Code settings:
{
"reasoning-layer.hookMode": "warn" // "warn" | "block"
}| State | How the hook gets installed |
|---|---|
| Now (bridge) | Developer runs bash scripts/install-coherence-hook.sh once per repo |
| Extension Phase 1 (target) | Extension activate() detects the workspace git root, writes .githooks/pre-commit, and runs git config core.hooksPath .githooks silently on first activation — zero extra steps |
The scripts/pre-commit file and scripts/install-coherence-hook.sh are temporary. When the extension ships, the hook is bundled inside the .vsix and installed automatically. The shell scripts can be removed at that point.
Goal: Add two coherence cadences to the Claude Code agent file
(.claude/reasoning-layer.md) so drift is surfaced proactively without developer action.
| Change | Detail |
|---|---|
GET /api/decisions/:id/linked-artifacts |
Returns tracked artifacts linked to a specific decision — used by Cadence B |
| Agent file v1.5.0 — Step 0b (Cadence A) | Pre-task drift check: fetches all tracked artifacts, gets git timestamps, calls drift endpoint, surfaces stale decisions before task starts |
| Agent file v1.5.0 — Step 3c (Cadence B) | Post-decision propagation pass: after recording each decision, checks linked files and asks developer to update them in the same session |
Runs between Step 0 and Step 1 — before the task is even described.
- Fetches all tracked artifacts for the repo (
GET /api/repos/:id/artifacts) - Gets last git commit timestamp for each via
git log - Calls
POST /api/artifacts/driftwith the timestamp map - If any files are drifted, surfaces the linked decisions as hard constraints and asks: "Before we start — these decisions may not be reflected in the code yet. Address drift first?"
Developer answers yes → Claude factors in the constraints before the task. Developer answers no → proceeds, drift recorded, pre-commit hook is the backstop.
Runs after Step 3 for each recorded decision.
- Calls
GET /api/decisions/:id/linked-artifacts - If tracked files are linked, surfaces them and asks:
"Decision
a3f2e71is linked to these files — should I update them now?" - If developer confirms, Claude makes the code changes immediately in the same session
- The pre-commit hook verifies drift is cleared when they commit
This closes the decision→code loop in the same session rather than leaving it to chance.
| When drift is caught | |
|---|---|
| Phase 3 only | At git commit (hours or days later) |
| Phase 4 Cadence A added | At session start (before the task) |
| Phase 4 Cadence B added | Immediately after the decision, same session |
Goal: Generate readable markdown from Postgres on demand. The database is the source of truth; the markdown is a derived view.
GET /api/repos/:id/context-log
Renders the full WAL for a repo as narrative markdown:
## a3f2e71 — decision — 2026-05-12
**Question:** Should we add soft-delete to the users table?
**Decision:** Yes — add `deletedAt` nullable timestamp. Hard-delete remains available
for GDPR erasure requests only.
**Rationale:** Soft-delete gives us recoverability for accidental deletes without
complicating the data model. GDPR erasure is a rare, deliberate operation.
**Alternatives considered:** Hard-delete only (rejected — no recovery path);
event sourcing (rejected — over-engineered for this use case).
**Decided by:** @reviewer
---
## b9c1d3e — rollback — 2026-05-14
**Supersedes:** a3f2e71
**Decision:** Revert soft-delete — add a separate `deleted_users` archive table instead.
**Rationale:** Soft-delete required adding `WHERE deletedAt IS NULL` to every query.
The archive table approach keeps the main table clean.
---Accepts query params:
?since=<iso>— entries after a date?type=<entry_type>— filter by type?format=narrative(default) or?format=adr
| Change | Detail |
|---|---|
GET /api/repos/:id/context-log |
Full WAL as narrative markdown; also supports ?format=adr for ADR table layout |
?since=<iso> filter |
Returns only entries after the given timestamp — useful for incremental export |
?type=<entry_type> filter |
Filter by entry type (decision, rollback, wont_do, table, observation) |
| Superseded warnings inline | Rolled-back decisions show ⚠ Superseded by <hexId> so readers can't miss them |
Content-Disposition: attachment |
Browser download works out of the box — context_log.md |
| Narrative + ADR dual format | formatNarrative() renders narrative prose; formatAdr() renders decision tables with metadata |
| Agent file Step 0c | On-demand context log fetch added to .claude/reasoning-layer.md v1.6.0 |
Calls GET /api/repos/:id/context-log and writes the result to context_log.md in the
workspace root. Teams that want a committed, human-readable WAL in their repo can run this
after decisions land — same commit pattern as decision.log.md today.
The file is treated as a derived artifact (generated from Postgres, not maintained manually) and is gitignore-able if the team prefers database-only storage.
| Phase | Status | Key outcome |
|---|---|---|
| 1 — WAL Upgrade | ✅ Shipped | reasoningArc + sessionId on Decision; rollback validation on all write paths |
| 2 — Async Refining Session | ✅ Shipped | Fire-and-forget routing; interim table WAL entry; catch-up cadence; reviewer settles with /settle |
| 3 — Artifact Coherence | ✅ Shipped | Track files → link to decisions → drift detection → pre-commit hook → superseded decision warnings |
| 4 — Agent File Cadences | ✅ Shipped | Step 0b: pre-task drift check; Step 3c: post-decision propagation pass; drift window shrinks from days → seconds |
| 5 — Context Log Export | ✅ Shipped | GET /repos/:id/context-log renders full WAL as narrative/ADR markdown on demand from Postgres |
Phase 3 can be built independently of Phase 2 — it doesn't require sessions to exist. Recommended order: 3 → 2 → 4 → 5, since artifact coherence delivers immediate day-to-day value and Phase 2 sessions make Phase 4 cadences more meaningful.