Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .ladle/components.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { GlobalProvider } from '@ladle/react';

import '../src/client/index.css';

const THEME_KEY = 'brunch-ladle-theme';

export const Provider: GlobalProvider = ({ children }) => {
return (
<div className="min-h-screen bg-background p-6 text-foreground">
{children}
</div>
);
};
5 changes: 5 additions & 0 deletions .ladle/theme.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import 'tailwindcss';
@import '../src/client/index.css';

@source "../src/client/components/**/*.tsx";
@source "../src/client/**/*.stories.tsx";
10 changes: 2 additions & 8 deletions .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@
"mcpServers": {
"shadcn": {
"command": "npx",
"args": [
"shadcn@latest",
"mcp"
]
},
"forge_extension": {
"url": "http://localhost:58459/mcp"
"args": ["shadcn@latest", "mcp"]
}
}
}
}
56 changes: 51 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,54 @@ Open http://localhost:5173.
| `npm run test` | Run test suite (vitest) |
| `npm run verify` | Full gate: lint + format + test + build |
| `npm run fix` | Auto-fix lint + format issues |
| `npm run seed <scenario>` | Seed a database with a named fixture scenario |

## Fixture scenarios

Seed the dev database with pre-built project states for testing and development:

```bash
# List available scenarios
npm run seed

# Seed into the default brunch.db (what npm run dev reads)
npm run seed issue-tracker-all-phases-closed

# Seed into a specific file
npm run seed issue-tracker-scope-closed ./tmp/test.db

# Wipe and re-seed
rm brunch.db brunch.db-shm brunch.db-wal
npm run seed issue-tracker-all-phases-closed
```

**Programmatic scenarios** — skeleton fixtures with minimal turns, no realistic parts:

| Scenario | State |
|---|---|
| `scope-closed` | Scope phase closed, design not started |
| `design-active` | Scope closed, one design turn |
| `requirements-ready` | Scope + design closed, requirements reviewed |
| `criteria-ready` | + requirements closed, criteria reviewed |
| `all-phases-closed` | All four phases closed |
| `forced-close-all-phases-closed` | All four phases closed, with design closed via user-forced closure |
| `low-readiness-all-phases-closed` | All four phases closed, with a synthetic low-readiness scope closure for export-caveat testing |

**Manifest scenarios** — rich fixtures with realistic interview content, structured parts, knowledge items, and cross-kind edges (domain: tiny issue tracker):

| Scenario | State | Items | Edges |
|---|---|---|---|
| `issue-tracker-scope-closed` | Scope closed (5 turns + proposal/confirm) | 12 (goals, terms, contexts, constraints) | 3 |
| `issue-tracker-design-active` | + 2 design turns | 18 (+ decisions, assumptions) | 7 |
| `issue-tracker-requirements-ready` | + design closed, requirements reviewed | 23 (+ 5 requirements, mixed review) | 10 |
| `issue-tracker-criteria-ready` | + requirements closed, criteria reviewed | 27 (+ 4 criteria, mixed review) | 14 |
| `issue-tracker-all-phases-closed` | All phases closed | 27 | 14 |

### Source tracing

- **Programmatic**: `src/server/fixtures/scenarios.ts` — inline seed functions
- **Manifest**: `src/server/fixtures/manifests/issue-tracker.json` — static JSON content; `src/server/fixtures/manifest.ts` — seeder that wires manifests through DB functions
- Naming convention: `issue-tracker-*` scenarios come from the manifest; unprefixed ones are programmatic

## Architecture

Expand Down Expand Up @@ -88,15 +136,13 @@ src/

## Current state

**Working**: Scope-phase interview with structured questions, observer entity extraction, entity sidebar, conversation persistence and resume, project management.

**Known issue**: Structured turn card does not render during live streaming — appears only after page refresh. Server persists correctly; hydration from DB works. Fix is next on the critical path (see `memory/PLAN.md` slice 6c).
**Working**: Full four-phase interview (scope → design → requirements → criteria), phase-aware observer extraction across all 8 canonical knowledge kinds, explicit phase outcomes with closure provenance, requirements and criteria review with approve/reject state, knowledge workspace, markdown export, project dashboard with workflow state, fixture scenarios with rich seeded content.

**Not yet built**: Phase transitions (7), design/requirements/criteria phases (8-10), decision revisit/branching (11), entity lifecycle API (12), spec export (13), npx distribution (14). See `memory/PLAN.md` for the full roadmap.
**Not yet built**: Review lifecycle refinement (13a), npx distribution (14). See `memory/PLAN.md` for the full roadmap.

## Tests

67 tests across 7 test files covering DB operations, app routes, core logic, interview flow, observer extraction, parts serialization, and context builders. Provider calls are mocked for CI; prompt quality depends on manual evaluation.
223 tests across 24 test files covering DB operations, app routes, core logic, interview flow, observer extraction, parts serialization, context builders, workspace hydration/controller/data, client components, phase-close logic, and build boundaries. Provider calls are mocked for CI; prompt quality depends on manual evaluation.

```bash
npm test
Expand Down
75 changes: 75 additions & 0 deletions docs/design/BROWNFIELD_EXPLORATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Brownfield Exploration Design

> Design exploration from 2026-04-12. Referenced by SPEC.md D82, D83.
> Status: **approved direction** — First-turn exploration via prompt + core tools.

## Shape

No new module boundary. Brownfield exploration is a prompt/context/tool-configuration concern:

1. **Tool set:** In brownfield mode, the interviewer agent receives core tools (read, grep, find, ls, bash) alongside interview tools (ask_question, propose_phase_closure).
2. **System prompt:** A brownfield variant of the scope system prompt instructs the agent to explore the codebase before asking its first scope question.
3. **Context builder:** `buildInterviewerContext()` receives the project's `cwd` and `mode` (greenfield/brownfield) to construct the appropriate first-turn prompt.

## Interviewer configuration change

```typescript
// interview.ts — createInterviewerAgent()
const tools = {
ask_question,
...(closeable ? { propose_phase_closure } : {}),
...(mode === 'brownfield' ? createCoreTools(projectCwd) : {}),
}

const instructions = mode === 'brownfield'
? getBrownfieldSystemPrompt(phase)
: getSystemPrompt(phase)
```

## First-turn UX

The user sees one continuous streamed turn:
1. Agent uses tool calls (read_file, grep, find_files) to explore the codebase
2. Agent synthesizes findings into a summary
3. Agent transitions into the first scope question, grounded in what it found
4. Observer extracts knowledge items from this turn as usual

## Data model change

The `project` table needs a `mode` field:
```typescript
mode: text('mode', { enum: ['greenfield', 'brownfield'] }).notNull().default('greenfield')
```

The project's `cwd` is stored alongside it (needed for tool factory):
```typescript
cwd: text('cwd') // absolute path where the project was created
```

## First-screen routing

The client's project creation flow asks:
- "New concept from scratch" → greenfield
- "Feature within existing codebase" → brownfield

Both create a project record; brownfield sets `mode: 'brownfield'` and stores `cwd`.

## Design alternatives considered

- **B (Separate pre-interview agent call):** Dedicated exploration agent runs before the first chat turn. Clean separation but adds latency (two serial agent calls) and the exploration is non-interactive — user can't guide it or interrupt.
- **A (Conditional tool set only):** Just add tools, no special prompt. Relies entirely on the agent figuring out it should explore. Too unpredictable.

## Prompt engineering notes

The brownfield system prompt should:
- Name the project directory explicitly
- Instruct: "Before asking your first scope question, use your tools to explore the codebase"
- Suggest a strategy: look for README, package.json/Cargo.toml/etc., directory structure, then key files
- Set a budget: "Spend no more than 5-8 tool calls on exploration before synthesizing"
- Transition: "Once you have a working understanding, summarize what you found and begin scope questions grounded in that context"

## Open questions

- Should brownfield mode persist core tool access throughout the entire interview, or only on the first turn? (Probably throughout — the user might say "look at the auth module" during design phase.)
- Should the exploration summary be stored as a special data part for context rebuilding? (Probably not for V1 — it's just part of the first turn's assistant_parts.)
- Does the `ToolLoopAgent` `stopWhen: stepCountIs(4)` need to increase for brownfield first turns? (Likely yes — exploration needs more steps.)
63 changes: 63 additions & 0 deletions docs/design/LOCAL_STORAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Local-First Storage Design

> Design exploration from 2026-04-12. Referenced by SPEC.md D81.
> Status: **approved direction** — BrunchProject struct with shallow walk-up.

## Shape

```typescript
interface BrunchProject {
root: string // absolute path to .brunch/
dbPath: string // .brunch/brunch.db
configPath: string // .brunch/config.json (future)
cwd: string // the directory where .brunch/ was found or created
}

/** Walk up from cwd looking for .brunch/ (max ~5 levels, stop at home or root).
* Returns null if not found. */
function findBrunchProject(cwd: string): BrunchProject | null

/** Create .brunch/ in cwd. Throws if it already exists. */
function initBrunchProject(cwd: string): BrunchProject

/** Find or create: find first, create if not found. */
function resolveBrunchProject(cwd: string): BrunchProject
```

## Walk-up behavior

Matches `.git` discovery semantics:
1. Check `cwd/.brunch/` — if found, use it
2. Walk up parent directories (max 5 levels)
3. Stop at filesystem root or user home directory
4. If not found anywhere, create `.brunch/` in the original `cwd`

## Integration with launcher

```typescript
// src/server/index.ts (or future bin entry)
const project = resolveBrunchProject(process.cwd())
const db = createDb(project.dbPath)
const app = createApp(db, project)
```

## .brunch/ directory structure

```
.brunch/
brunch.db # SQLite database (all projects/runs within this directory)
brunch.db-wal # WAL file (auto-created by SQLite)
brunch.db-shm # shared memory file (auto-created by SQLite)
config.json # future: user preferences, model config
```

## Design alternatives considered

- **A (Minimal):** Just change the default DB path to `.brunch/brunch.db`. No resolution logic, no struct. Too simple — doesn't handle subdirectory invocation or future config.
- **C (Explicit, no walk-up):** Only look in cwd. Simplest mental model but breaks when user runs from a subdirectory.

## Open questions

- Should `.brunch/` be added to common `.gitignore` templates? (Probably yes — DB contains API responses.)
- Should the walk-up depth be configurable? (Probably not — 5 levels is sufficient.)
- Should `resolveBrunchProject` print a message when it creates a new `.brunch/`? (Yes — first-run feedback.)
87 changes: 87 additions & 0 deletions docs/design/REVISIT_MODULE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Knowledge-Graph Revisit Module Design

> Design exploration from 2026-04-12. Referenced by SPEC.md D80, D84.
> Status: **approved direction** — C-flavored hybrid (state machine projected from DB).

## Shape

State machine lifecycle reconstructed from DB state on each HTTP request. No in-memory state survives between requests.

### Persisted state (new `revisit_session` table)

```typescript
interface RevisitSession {
id: number
projectId: number
status: 'planned' | 'active' | 'closing' | 'done' | 'aborted'
rootItemIds: number[] // items the user invalidated
affectedItemIds: number[] // cascade result
phasesToReopen: Phase[] // derived from affected items
anchorTurnId: number // highest primary-tree turn linked to affected items
threadRootTurnId: number | null // set when thread opens
createdAt: string
completedAt: string | null
}
```

### Projected state (read-only, reconstructed per request)

```typescript
type RevisitState =
| { status: 'none' }
| { status: 'planned'; session: RevisitSession; preview: CascadePreview }
| { status: 'active'; session: RevisitSession; resolved: number[]; remaining: number[] }
| { status: 'closing'; session: RevisitSession }
| { status: 'done'; session: RevisitSession }
```

### Module boundary (5 functions)

```typescript
/** Read-only: compute cascade without writing anything */
function previewCascade(projectId: number, itemIds: number[]): CascadePreview

/** planned: write the session, mark items invalidated */
function beginRevisit(projectId: number, itemIds: number[]): RevisitSession

/** active: open the secondary thread, reopen phases */
function openRevisitThread(sessionId: number, anchorTurnId: number): RevisitSession

/** active: mark one item resolved (called per-item as conversation progresses) */
function resolveRevisitItem(sessionId: number, itemId: number, outcome: 'confirmed' | 'edited' | 'removed'): RevisitState

/** closing → done: finalize when all items resolved */
function completeRevisit(sessionId: number): RevisitSession
```

## What it hides

- Graph traversal (BFS over all edge types, cycle detection)
- Which phases to reopen (derived from affected items' kind → phase mapping)
- Phase outcome supersession writes
- Review state reset for affected items
- Secondary thread turn creation with correct phase/ancestry
- Resolution completeness checking
- Anchor turn calculation (highest primary-turn provenance among affected items)

## HTTP mapping

| Step | Trigger | Writes |
|---|---|---|
| `previewCascade` | User selects items in edit mode | None (read-only) |
| `beginRevisit` | User confirms cascade | `revisit_session` + `turnKnowledgeItem(invalidated)` |
| `openRevisitThread` | Immediately after begin | Thread root turn + phase outcome supersession |
| `resolveRevisitItem` | Chat handler after each secondary turn | `turnKnowledgeItem(confirmed/edited)` per item |
| `completeRevisit` | When `remaining` reaches zero | Session status → done |

## Design alternatives considered

- **A (Minimal):** 2-method surface (`preview` + `invalidate`). Too coarse — doesn't model the interactive conversation lifecycle that spans many HTTP requests.
- **B (Event-driven):** Discrete event pipeline. Good auditability but ordering enforcement is runtime-only, not compile-time. Implementation style adopted (each step is stateless, state from DB).
- **C (Pure state machine):** In-memory stateful machine. Doesn't fit HTTP-per-request model without external persistence. Shape adopted, implementation adapted to DB-projected state.

## Open questions

- How does the secondary thread's chat endpoint differ from the primary? Same `/api/projects/:id/chat` with a `threadId` param, or separate route?
- Does `resolveRevisitItem` happen automatically when the observer processes a secondary-thread turn, or does it require explicit user action?
- What happens if the user closes the browser mid-revisit? The session stays `active` in DB — next launch should resume.
Loading