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
95 changes: 95 additions & 0 deletions HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Handoff — 2026-04-12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HANDOFF.md:1 — Process: this PR title doesn’t follow the required {issue-id}: {Linear issue title...} convention (e.g. FE-534: ...) per (Rule: AGENTS.md).

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.


## Phase in flow

```
grill ✓ → spec ✓ → plan ✓ → [design ✓] → scope ✓ → build ✓ → review ✓ → [sync]
```

- **Last completed skill**: `ln-review` — reviewed slices 17a and 14, no high-impact findings
- **Current skill**: `ln-handoff` + `ln-sync` (in progress)
- **Next action**: `/ln-sync` to refresh SPEC.md and PLAN.md after the implementation burst, then `/ln-scope` for slice 14a

## Session summary

Three slices landed plus a sync pass. The session started from the prior handoff which had slice 14's scope card ready.

1. **ln-sync** — Refreshed PLAN.md and SPEC.md. Trimmed slice 17 completion block to 4 lines, updated parallelism notes (17 done), updated dependency graph. Updated SPEC.md coverage table with actual test counts (interview.test.ts 10→11, InterviewWorkspace.test.tsx 21→22, workspace-loader.test.ts 2→3, export.test.ts 6→9, added manifest.test.ts).

2. **ln-scope → ln-build** for slice 17a (debug route removal + shiki decoupling):
- Replaced `CodeBlock` in `tool.tsx` with plain `<pre><code>` for JSON rendering
- Removed `preloadRichCodeHighlighter` from `markdown-rendering.tsx`
- Removed `/debug` route from router
- Deleted `ComponentDebug.tsx` + `debug-surface.tsx`
- Created `ai-elements.stories.tsx` Ladle story with full showcase
- Updated `build-boundary.test.ts` (no-shiki oracle, 1050KB budget) and `capability-boundaries.test.ts`
- Retired invariant I30 (moot), updated I28/I29/I32
- Commit: `78f7e89`

3. **ln-scope → ln-build** for slice 14 (local-first storage + npx distribution):
- Created `project.ts` — `BrunchProject` interface, `findBrunchProject` (walk-up), `initBrunchProject`, `resolveBrunchProject`
- Created `launcher.ts` — Express serving API + static dist/ on one port, opens browser
- Created `cli.ts` — bin entry for `npx @hashintel/brunch`
- Fixed `db.ts` — migrations path resolved via `import.meta.url` instead of relative `./drizzle`
- Updated `index.ts` — dev server uses `resolveBrunchProject` when no `BRUNCH_DB` env var
- Added `open` dependency, `bin` entry in `package.json`
- 11 new tests (project.test.ts: 8, launcher.test.ts: 3)
- New invariant I100 (project resolution + launcher seam)
- Commits: `006b9b8`, `6306579`

4. **ln-review** of slices 17a and 14 — clean, no high-impact findings. One medium oracle gap: launcher-serves-static test doesn't create a mock dist/ (deferred to outer-loop manual testing).

## In-flight state

### Review findings (from ln-review, all deferred)

1. **tool.tsx: repeated `<pre><code>` pattern** — depth/low — three identical pre/code blocks in ToolInput/ToolOutput. Acceptable per YAGNI.
2. **launcher.test.ts unused import** — coupling/low — `writeFileSync` imported but unused.
3. **launcher-serves-static oracle gap** — oracle-coverage/medium — test 2 doesn't actually test static serving (no mock dist/). Deferred to outer-loop manual npx walkthrough.
4. **launcher.ts owns resolution + serving** — coupling/low — `launch(cwd)` does both project resolution and server setup. Fine for single caller.
5. **db.ts + launcher.ts: duplicated `__dirname` pattern** — coupling/low — standard ESM boilerplate, not worth abstracting.
6. **index.ts dev/production divergence** — depth/low — intentional, well-bounded.
7. **All oracle coverage met** except finding #3. Lexicon alignment clean.

### Key design insight preserved from prior session

(Carried forward from prior handoff — still relevant for 14a)
- Answered question cards are **read-only** — re-answering routes through the revisit model (Phase 8), not a casual UI toggle
- Primary blue needs adjustment to match Hash logo (lighter sky blue) — not yet resolved via Figma
- Badges should explore mono font — not yet implemented

## Priority note from user

The first delivery deadline is approaching. Priority order:
1. **Done**: slice 17 (UI refinement), slice 17a (shiki decoupling), slice 14 (local-first + npx)
2. **Must-have next**: slice 14a (brownfield/greenfield) — this must land
3. **Stretch**: slice 15 + 15a (knowledge-graph revisit) — may not land before deadline
4. **Deferred**: 13a (review lifecycle refinement), 16 (drizzle-kit audit)

## Persisted state

### Git
- **Branch**: `ln/fe-545-storage-and-distro`
- **Recent commits**: 6306579 → 006b9b8 → 415deb1 → 78f7e89
- **Working tree**: clean

### Artifacts
- `memory/SPEC.md` — **current** (coverage table updated, I30 retired, I28/I29/I32 updated, I100 added)
- `memory/PLAN.md` — **current** (slices 17a and 14 marked done, parallelism notes updated)
- `docs/design/LOCAL_STORAGE.md` — **current** (approved design, implemented in slice 14)

### Test status
264 tests: all pass. `npm run verify` green (0 lint errors, build succeeds).

## Resume prompt

```
I'm picking up from a build + review session. Read HANDOFF.md, then:
1. Run /ln-sync to refresh docs after the implementation burst (slices 17a + 14)
2. Then /ln-scope for slice 14a (greenfield/brownfield first-screen + exploration)

Key context: slice 14a depends on slice 14 which just landed. Branch is
ln/fe-545-storage-and-distro. The design doc is at docs/design/BROWNFIELD_EXPLORATION.md.

Priority: slice 14a is the last must-have for the first delivery deadline.
```
3 changes: 3 additions & 0 deletions drizzle/0007_project_mode.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE `project` ADD `mode` text NOT NULL DEFAULT 'greenfield';
--> statement-breakpoint
ALTER TABLE `project` ADD `cwd` text;
7 changes: 7 additions & 0 deletions drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
"when": 1775715000000,
"tag": "0006_phase_outcome_closure_basis",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1775800000000,
"tag": "0007_project_mode",
"breakpoints": true
}
]
}
26 changes: 9 additions & 17 deletions memory/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,11 @@
- Debt: actual npx publish/distribution testing, `--port` flag, graceful shutdown
- Unblocks: 14a (greenfield/brownfield), 16 (drizzle-kit audit)

14a. **Greenfield/brownfield first-screen + exploration** — First screen routes between greenfield (blank concept) and brownfield (existing codebase). Project records store `mode` and `cwd`. Brownfield adds core tools to interviewer, brownfield system prompt variant instructs explore-then-interview on first turn. Observer extracts from that turn as usual. `not-started`
- Requirements: → SPEC.md §Requirements #2, #3, #16
- Assumptions: → SPEC.md §Assumptions A7, A47
- Decisions: → SPEC.md §Decisions D32, D82, D83
- Candidate invariant goals: brownfield first turn is grounded in discovered codebase context; greenfield path is unchanged; observer extracts from exploration turn normally
- Invariants to respect: → SPEC.md §Invariants I16, I22, I24, I54
- Acceptance: create brownfield project → agent explores codebase → first scope question is grounded in findings; create greenfield project → existing scope flow unchanged
14a. **Greenfield/brownfield first-screen + exploration** `done`
- Shipped: project table stores `mode` (greenfield/brownfield) and `cwd`; dialog-based first-screen routes between modes; brownfield interviewer gets core tools + exploration system prompt + higher step budget (12 vs 4); server derives cwd from launcher; greenfield path unchanged
- Evidence: db.test.ts (5 new assertions), interview.test.ts (3 new), app.test.ts (3 new), ProjectList.test.tsx (updated), 274 tests pass, npm run verify green
- Design: `docs/design/BROWNFIELD_EXPLORATION.md`
- Debt: outer-loop manual brownfield walkthrough (A47 validation), brownfield prompt for non-scope phases

## Phase 8: Knowledge-Graph Revisit (stretch)

Expand Down Expand Up @@ -228,11 +225,8 @@
```
done ─────────────────────────────────────────────────────────────┐
Phase 1–6: all complete │
Phase 7: 14 done, 17 done, 17a done, 14a done │
──────────────────────────────────────────────────────────────────┘
Phase 7: 12b ──→ 14 (local-first storage + npx distribution)
14 ──→ 14a (greenfield/brownfield + exploration)
17 done
Phase 8: 12a ──→ 15 (edit mode + cascade preview) [stretch]
15 ──→ 15a (cascade execution + secondary threads) [stretch]
Phase 9: 14 ──→ 16 (drizzle-kit audit remediation)
Expand All @@ -241,9 +235,7 @@ Deferred: 12a + 12b ──→ 13a (review lifecycle refinement)

### Parallelism opportunities

- Phase 6 is fully done (11a, 11b, 11c, 12a, 12b all complete).
- **17 (UI refinement) is done.** 14 (local-first + npx) is the next unblocked slice.
- 14a (brownfield) depends on 14 landing first (needs the launcher and `.brunch/` resolution).
- 15 + 15a (knowledge-graph revisit) are stretch goals; they depend on 12a (knowledge workspace) which is done, but may not land before the first deadline.
- 13a (review lifecycle refinement) is explicitly deferred; it should collect rarer review variants after the revisit model stabilizes.
- 16 (drizzle-kit audit) should wait until 14 lands.
- Phases 1–7 fully done (14, 17, 17a, 14a all complete). **All must-haves for the first delivery deadline are shipped.**
- 15 + 15a (knowledge-graph revisit) are stretch goals; they depend on 12a (done) but may not land before the first deadline.
- 16 (drizzle-kit audit) is unblocked by 14 but deferred to post-distribution.
- 13a (review lifecycle refinement) is explicitly deferred.
21 changes: 12 additions & 9 deletions memory/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`.
| # | Invariant | Established by | Protected by | Proves |
| ---- | ---------------------------------------------------------- | ------------------------- | ------------------------------------ | ----------- |
| I100 | `.brunch/` project resolution with walk-up discovery, init-rejects-existing, and resolve-creates-or-finds semantics; launcher serves API from resolved DB path with drizzle migrations resolving via import.meta.url | Slice 14 | project.test.ts, launcher.test.ts | D10, D81 |
| I101 | Project mode (greenfield/brownfield) persists through schema, API, and interviewer configuration: brownfield gets core tools + exploration prompt + higher step budget; greenfield path is unchanged; server derives cwd from launcher context | Slice 14a | db.test.ts, interview.test.ts, app.test.ts, ProjectList.test.tsx | D32, D82, D83 |

### Client characterization

Expand Down Expand Up @@ -355,6 +356,7 @@ Detailed schema and mode-model rationale: `docs/design/INTERVIEW_MODE_MODEL.md`.

| Term | Definition |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **BrunchProject** | The resolved `.brunch/` directory struct: `{ root, dbPath, cwd }`. Discovered by `findBrunchProject` (walk-up), created by `initBrunchProject`, or resolved by `resolveBrunchProject`. Represents the local storage location, not the elicitation run. See D81, I100. |
| **project** | A spec elicitation run within a `.brunch/` directory. Has a name, a HEAD pointer (`active_turn_id`), and workflow/readiness state. Multiple projects can coexist in one `.brunch/` directory (different runs, versions, or feature scopes). |
| **turn** | A checkpoint in the interview history. Carries phase provenance plus typed interaction payloads and UI parts. Points to its parent turn. Turns belong to either the primary conversation or a secondary thread. |
| **active path** | The chain from HEAD to root in the primary conversation. Determines which turns, knowledge items, phase outcomes, and review state are currently trusted. Secondary threads inherit validity from their anchor turn on the active path. |
Expand Down Expand Up @@ -557,33 +559,34 @@ This projection difference is a deliberate design choice, not an implementation

| File | Tests | Protects |
| ----------------------------- | ----- | ----------------------------------------------------- |
| db.test.ts | 43 | I5, I6, I9, I10, I11, I20, I48, I54, I72, I87, I98 |
| db.test.ts | 46 | I5, I6, I9, I10, I11, I20, I48, I54, I72, I87, I98, I101 |
| knowledge.test.ts | 1 | I48 |
| app.test.ts | 41 | I1, I2, I3, I7, I14, I21, I23, I44, I48, I54, I72, I87, I98, I99 |
| app.test.ts | 44 | I1, I2, I3, I7, I14, I21, I23, I44, I48, I54, I72, I87, I98, I99, I101 |
| core.test.ts | 10 | I12, I13, I18, I72, I87 |
| interview.test.ts | 11 | I16, I72, I87 |
| interview.test.ts | 14 | I16, I72, I87, I101 |
| parts.test.ts | 15 | I17, I18, I44, I54, I72 |
| context.test.ts | 15 | I19, I44, I48, I54, I87 |
| observer.test.ts | 9 | I20, I21, I44, I48, I54 |
| phase-close.test.ts | 13 | I72 |
| turn-response.test.ts | 4 | I44 |
| InterviewWorkspace.test.tsx | 22 | I23, I24, I44, I48, I54, I72 |
| ProjectList.test.tsx | 3 | I24 |
| ProjectList.test.tsx | 4 | I24, I101 |
| workspace-data.test.ts | 7 | I24, I48, I72 |
| chat-hydration.test.ts | 3 | I24 |
| workspace-controller.test.tsx | 3 | I24, I48 |
| client-mutation.test.ts | 3 | I24 |
| chat-hydration.test.ts | 2 | I24 |
| workspace-controller.test.tsx | 6 | I24, I48 |
| client-mutation.test.ts | 6 | I24 |
| EntitySidebar.test.tsx | 1 | I87 |
| code-block.test.tsx | 4 | I24, I26 |
| markdown-rendering.test.tsx | 3 | I24, I31 |
| message.test.tsx | 2 | I24, I27 |
| build-boundary.test.ts | 1 | I24, I28, I32 |
| capability-boundaries.test.ts | 2 | I24, I29 |
| KnowledgeWorkspace.test.tsx | 5 | I24, I48 |
| workspace-loader.test.ts | 3 | I24 |
| workspace-loader.test.ts | 7 | I24 |
| project.test.ts | 8 | I100 |
| launcher.test.ts | 3 | I5, I100 |
| export-loader.test.ts | 1 | D26, D65, D66, D70 |
| api-types.test.ts | 5 | — |
| export-loader.test.ts | 3 | D26, D65, D66, D70 |
| ExportPreview.test.tsx | 2 | D26, D65, D66, D70 |
| export.test.ts | 9 | D26, D65, D66, D70 |
| manifest.test.ts | 1 | — |
Expand Down
6 changes: 4 additions & 2 deletions src/client/components/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';

import { cn } from '@/lib/utils';

import type { WorkflowPhase, WorkflowPhaseStatus } from '../../shared/api-types.js';

// ── Stage sidebar (expanded, 240px) ──────────────────────────────────

export interface StageItem {
Expand Down Expand Up @@ -115,8 +117,8 @@ export function StageSidebar({

// ── Phase sidebar (narrow, 48px) — collapsed view ────────────────────

export type Phase = 'scope' | 'design' | 'requirements' | 'criteria';
export type PhaseStatus = 'unstarted' | 'in_progress' | 'closed';
export type Phase = WorkflowPhase;
export type PhaseStatus = WorkflowPhaseStatus;

const phaseOrder: Phase[] = ['scope', 'design', 'requirements', 'criteria'];

Expand Down
8 changes: 5 additions & 3 deletions src/client/mutations/project-mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { createProjectResponseSchema } from '../../shared/api-types.js';
import type { CreateProjectRequest, CreateProjectResponse } from '../../shared/api-types.js';
import { postJsonMutation, useClientMutation } from './client-mutation.js';

type CreateProjectInput = Omit<CreateProjectRequest, 'cwd'>;

export interface CreateProjectMutationState {
readonly createProject: (name: string) => Promise<CreateProjectResponse>;
readonly createProject: (input: CreateProjectInput) => Promise<CreateProjectResponse>;
readonly isPending: boolean;
readonly errorMessage: string | null;
readonly clearError: () => void;
Expand All @@ -23,8 +25,8 @@ export function useCreateProjectMutation(): CreateProjectMutationState {
);

return {
createProject: async (name: string) => {
const project = await mutation.run({ name });
createProject: async ({ name, mode }: CreateProjectInput) => {
const project = await mutation.run({ name, ...(mode ? { mode } : {}) });
void navigate({ to: '/project/$id', params: { id: String(project.id) } });
return project;
},
Expand Down
2 changes: 2 additions & 0 deletions src/client/routes/InterviewWorkspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ function createProjectState({
project: {
id: projectId,
name: `Project ${projectId}`,
mode: 'greenfield',
cwd: null,
active_turn_id: 1,
created_at: '2026-04-03 10:00:00',
updated_at: '2026-04-03 10:00:00',
Expand Down
Loading