singularity-forge/docs/dev/proposals/db-first-planning-state.md
Mikael Hugo f3571475d5
Some checks are pending
CI / detect-changes (push) Waiting to run
CI / docs-check (push) Blocked by required conditions
CI / lint (push) Blocked by required conditions
CI / build (push) Blocked by required conditions
CI / integration-tests (push) Blocked by required conditions
CI / windows-portability (push) Blocked by required conditions
CI / rtk-portability (linux, blacksmith-4vcpu-ubuntu-2404) (push) Blocked by required conditions
CI / rtk-portability (macos, macos-15) (push) Blocked by required conditions
CI / rtk-portability (windows, blacksmith-4vcpu-windows-2025) (push) Blocked by required conditions
docs: DB-first planning state migration proposal
Design doc for moving SF's milestone planning state from
markdown-as-source-of-truth to DB-as-source-of-truth, with markdown
becoming a render target.

463 lines, ~4500 words. Includes:
- Survey of all markdown artifacts under .sf/milestones/M*/ and
  who writes/reads each today (drift authoritative-ness is
  ambiguous in most cases)
- MVP picks *-VALIDATION.md as first artifact to migrate — three
  read-site fixes, no schema change, the doctor's
  db_projection_validation_drift check retires immediately
- Hybrid editing UX (option c): CONTEXT-DRAFT and in-progress PLAN
  stay LLM-writable markdown; tool-call-bounded artifacts
  (validate_milestone, complete_slice, etc.) become DB-first with
  generated <!-- generated --> headers
- 5-phase rollout plan
- Open question flagged: git atomicity for milestone-level
  syncMilestoneLevelFiles calls — needs explicit tracing before
  Phase 4/5

No source-code changes. Implementation comes later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 06:35:02 +02:00

34 KiB
Raw Permalink Blame History

Proposal: DB-First Planning State — Markdown as Render Target

Status: Draft
Date: 2026-05-15
Author: Autonomous session + design review
Relates to: .sf/PRINCIPLES.md L1, docs/adr/0000-purpose-to-software-compiler.md


Executive Summary

SF's principle states "SQLite is the canonical structured store … treat .sf/sf.db as the first place for planning hierarchy." The implementation has not followed this: milestone artifact files (CONTEXT.md, ROADMAP.md, VALIDATION.md, SUMMARY.md, and their slice/task counterparts) are written and edited as primary sources of truth, with DB rows either reconciled after the fact or only partially kept in sync. The result is observable drift that costs the autonomous orchestrator agent turns and derails long runs.

This proposal describes how to complete the inversion: DB rows become the canonical source, markdown files become a deterministic render of that state, and the doctor/reconciler logic collapses to near-zero because there is nothing to reconcile.


1. Current State Survey

1.1 Artifact Inventory

The table below covers the artifact types found under .sf/milestones/M*/ (surveyed against M001-6377a4 as the reference case, confirmed by grep across the extension source).

Artifact Example path Written by Read by DB representation today Authoritative today
M*-CONTEXT.md M001-6377a4/M001-6377a4-CONTEXT.md Bootstrap agent via write_file; record-promoter.js (migration path); migrate/writer.js state-db.js buildRegistryAndFindActive (title extraction); reasoning-assist.js (injects into prompts); auto-start.js (presence check); auto-direct-dispatch.js Partial: milestones.vision, vision_meeting_json, product_research_json, boundary_map_markdown, requirement_coverage hold structured fragments; the full narrative prose has no DB column File — title is extracted from prose; structured fields exist but are not always populated
M*-CONTEXT-DRAFT.md M001-6377a4/M001-6377a4-CONTEXT-DRAFT.md Bootstrap agent via write_file; triage-resolution.js state-db.js (presence check: draft → active milestone not yet planned); auto-start.js None File — only signal is presence/absence
M*-ROADMAP.md M001-6377a4/M001-6377a4-ROADMAP.md renderRoadmapFromDb in markdown-renderer.js; record-promoter.js; migrate/writer.js state-db.js (fallback title when DB title blank); doctor-engine-checks.js (detectMissingPlanningRowsFromProjection); post-execution-checks.js (checkMilestoneIntegrity); commands-ship.js; commands-handlers.js DB: milestones + slices tables hold all slice hierarchy, sequence, status, depends Ambiguous — DB is authoritative for status/hierarchy; ROADMAP.md is a render target in theory but is also the fallback when DB is empty
M*-ROADMAP.json M001-6377a4/M001-6377a4-ROADMAP.json writeRoadmapJsonProjection (called from renderRoadmapFromDb) doctor-engine-checks.js (projectionDriftIssues) — read-only drift check DB: same milestones/slices tables Projection — correctly labelled as "export/recovery only; DB remains authoritative" in doctor warning
M*-VALIDATION.md M001-6377a4/M001-6377a4-VALIDATION.md handleValidateMilestone in tools/validate-milestone.js (DB write first, then disk render) state-db.js readMilestoneValidationVerdict (terminal check); doctor-engine-checks.js projectionDriftIssues; workspace-index.js (validationVerdict field); auto-prompts.js (injected as context); reflection.js; complete-milestone.js (re-reads verdict from file); auto-worktree.js DB: assessments table (full_content, status/verdict, scope=milestone-validation) AmbiguoushandleValidateMilestone writes DB first, then file; but complete-milestone.js re-reads the file, and workspace-index.js reads the file, not the DB
M*-SUMMARY.md (milestone-level, e.g. M001-6377a4-SUMMARY.md) handleCompleteMilestone in tools/complete-milestone.js state-db.js buildRegistryAndFindActive (title fallback for complete milestones) DB: milestone_evidence (narrative rendered) + milestone status/completed_at; no full_summary_md column on milestones table File — absence of SUMMARY.md can block the completing-milestone phase even when DB says complete
S*-SUMMARY.md slices/S01/S01-SUMMARY.md Agent via write_file (autonomous solver), then renderSliceSummary backfills from DB slices.full_summary_md summary-helpers.js (prior task context injection); doctor completeness check DB: slices.full_summary_md TEXT column Ambiguous — file written by agent first, DB column populated by setSliceSummaryMd; renderer then writes back from DB
S*-UAT.md slices/S01/S01-UAT.md Agent via write_file; renderSliceSummary (writes from slices.full_uat_md) Doctor completeness check DB: slices.full_uat_md TEXT column; slices.uat_verdict Ambiguous — same dual-write pattern as SUMMARY.md
S*/tasks/T*-SUMMARY.md slices/S01/tasks/T01-SUMMARY.md Agent via write_file; renderTaskSummary (from tasks.full_summary_md) summary-helpers.js prior-task context; doctor DB: tasks.full_summary_md TEXT column Ambiguous — task summary lives in DB but file is what the agent reads next turn
S*-PLAN.md / T*-PLAN.md slices/S03/S03-PLAN.md renderSlicePlanFromDb / renderTaskPlanFromDb in markdown-renderer.js post-execution-checks.js; agent via read_file DB: slices.goal, success_criteria, full_summary_md; tasks.full_plan_md, description, etc. DB-first in theory — renderers exist; but slice PLAN is sometimes skipped if full_plan_md is empty
M*-CONTEXT-DRAFT.md (anchor) anchors/plan-slice.json plan tooling auto-dispatch.js (planning anchor gate) DB: none File

1.2 Key Readers of Each Artifact

Who reads VALIDATION.md:

  • state-db.js:readMilestoneValidationVerdict — terminal verdict check; blocks completing the milestone if not terminal
  • workspace-index.js:buildWorkspaceIndex — populates validationVerdict on each milestone entry (used by TUI, context injection)
  • auto-prompts.js:buildValidateMilestonePrompt — reads file as context block for re-validation rounds
  • complete-milestone.js:handleCompleteMilestone — re-reads file verdict as guard before marking complete
  • reflection.js:readValidationFiles — reads all milestone VALIDATION files for the reflection corpus
  • doctor-engine-checks.js:projectionDriftIssues — compares file verdict vs DB assessments.status

Who reads CONTEXT.md:

  • state-db.js:buildRegistryAndFindActive — title extraction fallback when milestones.title is blank
  • reasoning-assist.js — injects context into prompt blocks
  • auto-start.js — presence check (CONTEXT exists → bootstrap complete)
  • auto-direct-dispatch.js — reads slice CONTEXT for dispatch context

Who reads ROADMAP.md:

  • doctor-engine-checks.js:detectMissingPlanningRowsFromProjection — if ROADMAP.md has slices but DB has none, warns
  • post-execution-checks.js:checkMilestoneIntegrity — reads ROADMAP.md to verify done slices have SUMMARY.md
  • commands-ship.js — reads roadmap for ship-gate check
  • state-db.js:534 — checks whether ROADMAP file exists as a proxy for "milestone has been planned"

Who reads SUMMARY.md (slice/task):

  • summary-helpers.js — injected as prior-task context into execute-task prompts
  • state-db.js:buildRegistryAndFindActive — reads milestone SUMMARY for title (complete milestone fallback)
  • markdown-renderer.js:detectStaleRenders — checks presence of SUMMARY.md for complete tasks

1.3 Authoritative-Today Summary

Artifact            | Source of truth today    | Drift risk
--------------------|--------------------------|------------------
CONTEXT.md          | File (prose)             | High — title field in DB often blank
CONTEXT-DRAFT.md    | File (presence signal)   | Low (no content read)
ROADMAP.md          | Ambiguous — DB for status, file for completeness check | Medium
ROADMAP.json        | DB (projection)          | Low — doctor already catches drift
VALIDATION.md       | File (re-read by complete-milestone) | High — this session's critical failure
SUMMARY.md (ms)     | File (blocking)          | High — absence caused stuck phase
SUMMARY.md (slice)  | Ambiguous (dual-write)   | Medium
UAT.md (slice)      | Ambiguous (dual-write)   | Medium
SUMMARY.md (task)   | Ambiguous (dual-write)   | Medium
PLAN.md (slice/task)| DB-first via renderers   | Low (renderer exists)

2. Proposed Model

2.1 Canonical DB Representation (Reusing Existing Tables)

The existing schema already holds or can hold the authoritative state for every artifact type listed above. No new tables are required for the MVP; a few additive column additions are needed for the full rollout.

VALIDATION.md → assessments table

Already done (partially). handleValidateMilestone already writes assessments (full_content, status, scope=milestone-validation) before writing the file. The gap is that three consumers re-read the file instead of querying the DB.

No schema change required.

ROADMAP.md → milestones + slices tables

Already fully represented. Every slice's id, title, status, sequence, risk, depends, and done state are in the slices table. The renderRoadmapFromDb function in markdown-renderer.js already generates correct markdown from these rows.

No schema change required.

SUMMARY.md (milestone) → milestones + milestone_evidence tables

The milestone complete-milestone handler writes narrative to milestone_evidence but there is no milestones.full_summary_md column. Adding one mirrors the pattern used by slices.full_summary_md and tasks.full_summary_md.

Additive schema change required:

ALTER TABLE milestones ADD COLUMN full_summary_md TEXT NOT NULL DEFAULT '';

SUMMARY.md (slice) → slices.full_summary_md

Already exists. setSliceSummaryMd writes this column. renderSliceSummary reads it and writes to disk.

No schema change required.

UAT.md (slice) → slices.full_uat_md

Already exists. Same pattern as SUMMARY.md.

No schema change required.

CONTEXT.md → milestones table (structured fields) + new full_context_md column

The milestones table already holds structured fields extracted from CONTEXT.md (vision, success_criteria, key_risks, proof_strategy, verification_contract/integration/operational/uat, definition_of_done, requirement_coverage, boundary_map_markdown). What is missing is a column for the full human-authored narrative prose (the "Project Description", "Why This Milestone", "User-Visible Outcome" sections).

Adding full_context_md TEXT NOT NULL DEFAULT '' allows the complete context to be stored and rendered on demand.

Additive schema change required:

ALTER TABLE milestones ADD COLUMN full_context_md TEXT NOT NULL DEFAULT '';

CONTEXT-DRAFT.md → milestones.status + new context_draft_md column

The draft is a signal that planning is in progress. The presence check can be replaced by a column that stores the draft content; status queued with non-empty context_draft_md replaces the file presence check.

Additive schema change required:

ALTER TABLE milestones ADD COLUMN context_draft_md TEXT NOT NULL DEFAULT '';

2.2 Markdown as a Projection: Lazy vs Eager

Decision: Eager generation on write, with lazy fallback on read.

The render target must be written to disk at the same transaction boundary as the DB write, for two reasons:

  1. Git atomicity: human reviewers see PR diffs. If the markdown file is absent or stale, the diff is hard to review. Committing DB state without a corresponding markdown update breaks review UX.
  2. Existing readers that read the file: complete-milestone.js, workspace-index.js, reflection.js, auto-prompts.js all read the file path directly. Migrating all call sites at once is a large change; eager disk writes allow incremental migration.

Lazy generation (render only when cat or sf plan show is called) is appealing in the steady state but breaks the git-atomicity requirement and existing consumers during the transition period. A lazy-only model is the long-term ideal (addressed in Section 7), but not safe for the MVP.

The projection contract:

DB write → render markdown → atomic disk write → git commit
                ↑
         deterministic function;
         same DB state → same markdown content every time

The rendered file gets a comment header:

<!-- generated from .sf/sf.db — do not edit directly; use `sf plan` commands -->

For CONTEXT.md and CONTEXT-DRAFT.md, the "do not edit" header is softer because they contain human-authored prose that has no other editing path during the bootstrap phase. Editing UX is addressed in Section 3.

2.3 Migration: Existing Markdown → DB Import

For each artifact type, the migration follows this pattern:

  1. Read the existing markdown file.
  2. Parse the structured content using existing parsers (parseRoadmap, extractVerdict, parseSummary, frontmatter parser).
  3. Upsert the parsed content into the appropriate DB column(s) using existing functions (e.g., upsertMilestonePlanning, setSliceSummaryMd, insertAssessment).
  4. Re-render the file from DB (which adds the <!-- generated --> header and normalizes formatting).
  5. If the re-rendered content differs from the original, commit as a single atomic operation.

One-time migration function location: src/resources/extensions/sf/migrate/db-first-import.js. It is called once by doctor --fix when the db_markdown_not_imported doctor code fires; never called on every startup.

The migration is idempotent: if assessments.full_content already matches the file's content hash, it is a no-op.


3. Editing UX

3.1 Options

(a) Edit markdown → parse back into DB (round-trip-safe)

Requires a robust bidirectional parser for every artifact type. CONTEXT.md is free-form prose; there is no safe round-trip parser. Any ambiguous or model-generated prose in CONTEXT.md would be lost, truncated, or corrupted on re-import. High implementation cost; fragile.

(b) Markdown is read-only generated; edits go through sf plan commands or sf headless db ...

Clean separation. Agents and operators must use structured commands to mutate state. The markdown file is authoritative only for reading/reviewing.

(c) Hybrid: drafts are markdown, promotion to canonical writes DB and regenerates markdown

CONTEXT-DRAFT.md remains agent-editable (it is the active working surface during bootstrap). Once plan_milestone is called, CONTEXT.md is generated from DB and becomes read-only.

Decision: Option (c) — hybrid.

Reasoning:

  • CONTEXT-DRAFT.md and slice/task PLAN.md are the primary LLM write surfaces during active work. Making them DB-only would require adding LLM tool calls for every sentence of the planning meeting. The bootstrap and planning flows are already LLM-driven with free-form file writes.
  • VALIDATION.md, ROADMAP.md, SUMMARY.md, and UAT.md are outputs of structured operations (validate_milestone, plan_milestone, complete_slice, complete_task). These have clean tool-call boundaries and can safely become DB-first with no editing path for the file itself.
  • The hybrid lets us complete the MVP without restructuring the bootstrap/planning LLM flow, while still eliminating the drift that caused the session failures.

Editing rules under the hybrid model:

Artifact Editing path File state
CONTEXT-DRAFT.md Agent writes via write_file during bootstrap; content stored in milestones.context_draft_md on save Editable (agent/human)
CONTEXT.md plan_milestone call generates from DB fields + draft content; file becomes read-only <!-- generated --> header
ROADMAP.md plan_milestone / sf plan update-slice commands; file is regenerated <!-- generated --> header
ROADMAP.json Regenerated alongside ROADMAP.md // generated comment
VALIDATION.md validate_milestone tool call; file regenerated from DB <!-- generated --> header
SUMMARY.md (slice/task) complete_slice / complete_task tool calls; field written to DB, file generated <!-- generated --> header
UAT.md complete_slice tool call (uat_md param); field written to DB <!-- generated --> header
SUMMARY.md (milestone) complete_milestone tool call; milestone full_summary_md column written, file generated <!-- generated --> header

For operators who need to edit a generated file (e.g., to correct a validation rationale), the path is:

  1. sf headless db -- "UPDATE assessments SET full_content = '...' WHERE milestone_id = 'M001'" — or via sf plan validate --update-rationale.
  2. The update triggers a projection re-render (file is rewritten from DB).

4. Stale-Detection and Reconciliation

4.1 Current Reconciliation Cost

Today, doctor-engine-checks.js:projectionDriftIssues performs two checks:

  1. ROADMAP.json drift: compares DB slice order/status against JSON file. Fires db_projection_roadmap_drift warning.
  2. VALIDATION.md verdict drift: compares DB assessments.status against file frontmatter verdict. Fires db_projection_validation_drift warning. This is the check that fired during the M001-6377a4 session ("verdict in markdown said needs-remediation but DB said needs-attention").

Additionally, detectStaleRenders in markdown-renderer.js checks checkbox states and missing SUMMARY.md files. workspace-index.js reads VALIDATION files independently of the DB. state-db.js reads CONTEXT.md for title extraction when the DB title is blank.

Each of these is a potential source of drift. The autonomous orchestrator spent turns reasoning about the discrepancy before acting on it.

4.2 After DB-First Migration: Doctor Changes

For VALIDATION.md: The db_projection_validation_drift check is eliminated entirely. The file is always regenerated from DB by handleValidateMilestone. The doctor check becomes:

if (validationPath exists AND full_content in assessments is empty) {
  issue: db_validation_content_missing — file exists but DB has no stored content;
         run `sf doctor --fix` to import the file into DB.
  severity: info, fixable: true
}

This inverts the check: instead of asking "does the file match the DB?", it asks "is the DB populated for this file?". Once the one-time import runs, the check never fires again.

For ROADMAP.json: The db_projection_roadmap_drift check remains as a sanity assertion during the transition period. After full rollout, it is removed: the JSON file is always regenerated alongside ROADMAP.md by renderRoadmapFromDb.

For SUMMARY.md / UAT.md missing files: detectStaleRenders currently detects when a complete task/slice has content in DB but no file on disk. After DB-first, the render is triggered eagerly by complete_task/complete_slice, so the file is never missing. The check becomes a recovery path only (fired when the disk write failed mid-way).

For CONTEXT.md title fallback: state-db.js:buildRegistryAndFindActive falls back to reading CONTEXT.md when milestones.title is blank. After DB-first, the full_context_md column is populated on every plan_milestone call, and milestones.title is always written alongside it. The file-system fallback path is removed from buildRegistryAndFindActive.

For CONTEXT.md presence check in auto-start.js: The check if (!contextFile && draftFile) activeMilestoneHasDraft = true is replaced by checking milestones.context_draft_md IS NOT NULL AND context_draft_md != ''.

Net result: the doctor has nothing to reconcile for these artifact types. The autonomous orchestrator spends zero turns on drift detection. The estimated ~20% reconciliation overhead in early autonomous runs goes to near-zero.


5. Migration Plan

5.1 MVP: *-VALIDATION.md

Why first:

  • Smallest file (frontmatter + ~30 lines); parser (extractVerdict) already exists and is battle-tested.
  • The doctor already detects drift (db_projection_validation_drift), providing a clear before/after signal.
  • handleValidateMilestone already writes DB before disk — the pattern is already correct at the write site. The remaining work is fixing the three read sites.
  • The session failure was specifically caused by stale VALIDATION.md verdict. This is the highest-pain point.

Step-by-step for VALIDATION.md MVP:

Step V1: Fix read sites to prefer DB.

Files to change:

  • complete-milestone.js:handleCompleteMilestone — line 136-142: replace extractVerdict(readFileSync(validationPath)) with getMilestoneValidationAssessment(milestoneId)?.status.
  • workspace-index.js:buildWorkspaceIndex — replace file-read loop with getMilestoneValidationAssessment(milestone.id)?.status.
  • state-db.js:readMilestoneValidationVerdict — replace file-read with getMilestoneValidationAssessment(milestoneId)?.status; keep file-read as fallback only when DB row is absent (for repos that have not yet run the migration).

Rollback: trivially revert the three call-site changes. No schema change, no data loss.

Readers that do NOT switch to DB in this step:

  • auto-prompts.js:buildValidateMilestonePrompt — still reads the file as a human-readable context block (acceptable; the file is generated from DB so it will be correct).
  • reflection.js:readValidationFiles — still reads files for reflection corpus (acceptable; same reason).

Tests:

  • Add a test in src/resources/extensions/sf/tests/ that calls handleValidateMilestone, then checks that getMilestoneValidationAssessment returns the verdict — and that the same verdict is returned by readMilestoneValidationVerdict without any file on disk.
  • Extend doctor-engine-checks tests to confirm that db_projection_validation_drift no longer fires for a freshly validated milestone.

Step V2: Add <!-- generated --> header to VALIDATION.md output.

In renderValidationMarkdown inside tools/validate-milestone.js, prepend:

<!-- generated from .sf/sf.db — do not edit directly; use validate_milestone tool -->

Update verdict-parser.js:extractVerdict to skip this comment line when parsing verdicts from files.

Tests: confirm that extractVerdict still works on files with the header; confirm that the doctor check does not false-positive on the header.

Step V3: One-time import doctor fix.

Add doctor code db_validation_content_missing: fires when a VALIDATION.md file exists for a milestone but assessments has no row for scope=milestone-validation for that milestone. Doctor --fix calls insertAssessment with the file content.

Tests: set up a milestone with a VALIDATION.md but no DB row; confirm doctor fires and fix imports it.

Step V4: Remove db_projection_validation_drift doctor check.

Once V1V3 are in and the migration has run, the drift check is provably unreachable (the file is always generated from DB). Remove it to reduce noise.

Rollback at any step: each step is independently revertible. The DB assessments rows are not deleted during any step, so reverting the read-site changes restores the file-read behavior.


5.2 Incremental Rollout to CONTEXT.md

Dependencies: CONTEXT.md is not currently in the DB at the full-prose level. The full_context_md schema addition must land first.

Sequence:

  1. Add milestones.full_context_md TEXT NOT NULL DEFAULT '' migration (schema version bump in sf-db-schema.js).
  2. Update plan_milestone tool handler to write the full context prose to full_context_md in addition to the existing structured fields.
  3. Update state-db.js:buildRegistryAndFindActive title extraction to read from DB title first (already done for non-empty titles); add fallback to extract title from full_context_md before falling back to file.
  4. Update triage-resolution.js CONTEXT-DRAFT.md seed to also write milestones.context_draft_md.
  5. Add renderContextFromDb function to markdown-renderer.js that generates CONTEXT.md from full_context_md (or structured fields if full_context_md is empty).
  6. Update plan_milestone to call renderContextFromDb after writing DB, replacing the direct write_file instruction in the LLM prompt for the CONTEXT.md write.
  7. Doctor check: db_context_missing fires when CONTEXT.md exists but full_context_md is empty; --fix imports file content.
  8. CONTEXT-DRAFT.md: auto-start.js presence check replaced by milestones.context_draft_md IS NOT NULL.

Tests: planning flow integration test that verifies plan_milestone writes full_context_md to DB and regenerates CONTEXT.md with correct content and header.


5.3 Incremental Rollout to ROADMAP.md

ROADMAP.md is already partially DB-first: renderRoadmapFromDb exists and is called by the slice/task completion path. The remaining work is:

  1. Ensure plan_milestone always calls renderRoadmapFromDb instead of (or in addition to) writing ROADMAP.md via direct file instruction.
  2. Replace state-db.js:534 file-existence proxy (resolveMilestoneFile(basePath, activeMilestone.id, 'ROADMAP') !== null) with a DB check (getMilestoneSlices(milestoneId).length > 0).
  3. Replace post-execution-checks.js:checkMilestoneIntegrity ROADMAP read with a direct DB query for done slices, then check SUMMARY.md presence separately.
  4. Add <!-- generated --> header; update parseRoadmap to skip the header line.
  5. Doctor check: db_roadmap_not_imported fires when ROADMAP.md has slices not in DB; --fix calls migrateHierarchyToDb (already exists in md-importer.js).

5.4 Incremental Rollout to SUMMARY.md (Milestone)

  1. Add milestones.full_summary_md TEXT NOT NULL DEFAULT '' schema migration.
  2. Update handleCompleteMilestone to write full_summary_md to the milestones table.
  3. Add renderMilestoneSummaryFromDb to markdown-renderer.js.
  4. Call the renderer from handleCompleteMilestone (replacing the direct saveFile call or running alongside it during transition).
  5. Update state-db.js:buildRegistryAndFindActive title fallback for complete milestones to read from milestones.full_summary_md (or milestones.title) before reading the file.
  6. This eliminates the "SUMMARY.md missing → completing-milestone phase stuck" failure mode: the DB row is written first, and the render is a deterministic step.

5.5 Rollout Order Summary

Phase Artifact Required schema change Estimated effort
MVP VALIDATION.md None Small (3 read-site changes + tests)
2 SUMMARY.md (slice, task) None (columns exist) Small (add header, fix stale-render fallback)
3 ROADMAP.md None Medium (dispatch-side guard change + test)
4 SUMMARY.md (milestone) milestones.full_summary_md Medium
5 CONTEXT.md milestones.full_context_md Large (LLM flow change)
6 CONTEXT-DRAFT.md milestones.context_draft_md Small (presence check replacement)

6. Non-Goals

  • Does NOT replace .sf/journal/*.jsonl (append-only event log; complementary to DB; no change).
  • Does NOT replace ADRs / proposals in docs/ (human-authored; promoted-only; not generated). See PRINCIPLES.md L4: "promote only reviewed plans, specs, and ADRs to docs/".
  • Does NOT abolish markdown. Markdown files under .sf/milestones/ remain on disk and are committed to git. They are readable by humans and LLMs. The only change is that they are generated artifacts rather than primary state. The git history of these files remains meaningful as a timeline of validation and completion events.
  • Does NOT change the slice PLAN.md or task PLAN.md write flow. These are already DB-first (via renderSlicePlanFromDb / renderTaskPlanFromDb). The full_plan_md column exists on tasks. No migration needed.
  • Does NOT change .sf/runtime/ files (solver checkpoint, unit runtime record). These are ephemeral operational state, not planning state.
  • Does NOT change docs/records/ workflow. The record-promoter.js promotes human-authored records to milestone shells; those shells still use CONTEXT-DRAFT.md as the initial planning surface.
  • Does NOT gate on a single DB technology. The proposal assumes SQLite via sf-db.js. No change to the database engine.

7. Open Questions

Q1: How are DB-stored rich-text fields exported to markdown (rendering rules)?

The renderValidationMarkdown and renderRoadmapMarkdown functions in markdown-renderer.js and validate-milestone.js already implement this for their respective artifact types. For CONTEXT.md, the rich-text is free-form prose stored verbatim in full_context_md; the renderer wraps it in the standard header/metadata block. No special encoding or schema migration is needed for plain markdown prose fields.

For structured list fields (e.g., milestones.success_criteria stored as JSON array), the existing renderListEntry helper in markdown-renderer.js handles the serialization. The schema migration path (adding a column) is gated on the existing columnExists helper and the versioned migration table in sf-db-schema.js.

Unresolved: What happens when the free-form prose in full_context_md contains markdown that conflicts with the generated header/section structure? The current normalizeMarkdownBlockSpacing helper handles some cases, but adversarial LLM-authored content with duplicate H1 headings or frontmatter blocks could corrupt the render. A content sanitizer for known problematic patterns should be added to the render pipeline before Phase 5 (CONTEXT.md).

Q2: Should there be a "view in markdown" command as the canonical viewer?

sf plan show <milestone-id> could render CONTEXT.md, ROADMAP.md, and VALIDATION.md on demand (lazy) without requiring the files to be on disk. This would enable a future lazy-only model. However, during the transition period, files must remain on disk for git diff and existing tool/agent consumers. The "view in markdown" command is worth adding as a convenience but does not replace disk presence in the MVP.

Q3: Git diff UX — are DB state changes and markdown changes always committed atomically?

This is the hardest open question. The current flow is:

DB write → disk write → autonomous loop continues → git commit (at unit boundary)

The git commit at unit boundary picks up all dirty files, which includes the regenerated markdown. So DB state changes and markdown changes should land in the same commit in the normal flow.

Risk: if the session is interrupted between the DB write and the git commit, the DB has the new state but git does not. On the next run, the markdown is regenerated from DB (correct), but the git diff will show both the state change and the render as a single combined diff — which is correct and expected.

Unresolved: for milestone-level artifacts (VALIDATION.md, CONTEXT.md, ROADMAP.md), the commit happens after validate_milestone/plan_milestone/complete_milestone returns. These tool calls run inside the agent turn, not at a unit boundary. The postUnitPostVerification flow (called after execute-task) would not catch a VALIDATION.md written during a validate-milestone unit. The commit hook for milestone tool calls needs explicit investigation — it may already be handled by the worktree merge path (auto-worktree.js:syncMilestoneLevelFiles), but this needs verification before Phase 4/5 rollout.


Appendix A: File-to-DB Column Mapping

File DB table DB column(s)
M*-VALIDATION.md assessments full_content, status (= verdict), scope=milestone-validation
M*-ROADMAP.md milestones + slices title, status, sequence, risk, depends
M*-ROADMAP.json Same as ROADMAP.md Generated alongside ROADMAP.md
M*-CONTEXT.md milestones full_context_md (additive), vision, success_criteria, key_risks, proof_strategy, verification_*, definition_of_done, requirement_coverage, boundary_map_markdown
M*-CONTEXT-DRAFT.md milestones context_draft_md (additive)
M*-SUMMARY.md milestones full_summary_md (additive), status=complete, completed_at
S*-SUMMARY.md slices full_summary_md
S*-UAT.md slices full_uat_md, uat_verdict
T*-SUMMARY.md tasks full_summary_md
S*-PLAN.md slices goal, success_criteria, proof_level, integration_closure, planning_meeting_json
T*-PLAN.md tasks full_plan_md, description, verify, inputs, expected_output

Appendix B: Doctor Check Disposition

Doctor code Status after full rollout
db_projection_validation_drift Removed — file is always generated from DB; drift is impossible
db_projection_roadmap_drift Removed (Phase 3) — ROADMAP.json always generated alongside ROADMAP.md
db_missing_planning_rows_from_projection Retained — still needed during legacy migration; becomes a one-time import prompt
db_validation_content_missing Added (MVP) — file exists but DB has no assessment row
db_context_missing Added (Phase 5) — CONTEXT.md exists but full_context_md is empty
db_roadmap_not_imported Added (Phase 3) — ROADMAP.md has slices not in DB
Stale render checks in detectStaleRenders Simplified — reduced to recovery path only; normal-flow drift is impossible

Appendix C: Concrete Example — What Changes for M001-6377a4

The session failure was:

  1. M001-6377a4-VALIDATION.md frontmatter said verdict: needs-remediation.
  2. assessments table said status: needs-attention.
  3. doctor detected drift and reported it.
  4. Autonomous spent turns reconciling before re-running validation.

After MVP (Step V1):

  • complete-milestone.js checks getMilestoneValidationAssessment('M001-6377a4').status instead of reading the file. If the DB says needs-attention, the milestone is blocked with needs-attention — correctly.
  • workspace-index.js populates validationVerdict from DB — no file read.
  • The stale VALIDATION.md is still on disk (and would be visible in git) but it is not consulted by any decision-making code.
  • The doctor check db_projection_validation_drift no longer fires because the read sites no longer compare file vs DB.

A sf doctor --fix run would then re-render VALIDATION.md from the DB assessments.full_content, overwriting the stale needs-remediation frontmatter with the correct needs-attention verdict.