Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
73081ce
FE-710: Settle thread substrate as option (q) in SPEC/PLAN
kostandinang May 14, 2026
4d45803
FE-710: Land thread substrate — migration, schema, store, tests
kostandinang May 14, 2026
58c8f6f
FE-710: Inline thread collapsible scaffold — server helpers, stream i…
kostandinang May 14, 2026
28e4825
FE-710: Fix review findings — thread ownership, invariant enforcement…
kostandinang May 14, 2026
41d1742
FE-710: Consolidate interview-thread lookup, scope turn-count query
kostandinang May 14, 2026
65e86a2
FE-710: Add unified chat UX design brief
kostandinang May 14, 2026
e84fa50
FE-710: Refactor ThreadCollapsible to brief tone — icons, mode labels…
kostandinang May 14, 2026
e238992
FE-710: ThreadCollapsible inline input + live SSE streaming for side-…
kostandinang May 14, 2026
8158027
FE-710: Route 'Chat' action to inline ThreadCollapsible when thread e…
kostandinang May 14, 2026
ef863f6
FE-710: Add POST /api/specifications/:id/threads endpoint
kostandinang May 14, 2026
3b1164a
FE-710: Update PLAN execution pointer after slices 5-7
kostandinang May 14, 2026
44f9a6c
FE-710: ThreadCollapsible edit mode + patch staging
kostandinang May 14, 2026
4dfe732
FE-710: Eager thread creation cutover — openFor always routes inline
kostandinang May 14, 2026
d133469
FE-710: Update PLAN execution pointer after slices 8-9
kostandinang May 14, 2026
c4dd4c4
FE-710: Delete SideChatPopover and clean up dead code
kostandinang May 14, 2026
82f425e
FE-710: Update PLAN — thread substrate + popover cutover arc is complete
kostandinang May 14, 2026
6ca8806
FE-710: Turn-zero kickoff for side-chat threads
kostandinang May 14, 2026
2fad286
FE-710: All acceptance criteria met — frontier ready to close
kostandinang May 14, 2026
b3ab28e
FE-710: Close frontier — update acceptance wording, move to Recently …
kostandinang May 14, 2026
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
76 changes: 53 additions & 23 deletions HANDOFF.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,76 @@
# Handoff

> Generated by `ln-handoff` at 2026-05-13. Read this file to resume work.
> Generated by `ln-build` at 2026-05-14. Read this file to resume work.
> This file is volatile transfer state only. After its contents are reconciled into canonical docs or superseded by a newer handoff, overwrite or delete it.

## Goal

Ship the continuous workspace shell (FE-709) — cumulative phase sections, one chat runtime, sidebar scroll-spy, preserved phase addressability.
Land the **thread substrate** for FE-710 (chat-runtime-threads) and advance to the next slice.

## Session State

- **Last completed skill**: `ln-build` — Steps 1–4 of CONTINUOUS_WORKSPACE_HYBRID.md migration plan + helper extraction all committed
- **Flow position**: `plan → scope → build (×3) → handoff → review → scope → build (helper extraction) → scope → build (Step 4) → ready for PR`
- **Last completed skill**: `ln-build` — substrate-landing slice implemented and verified (1261/1261 tests, full `npm run verify` gate passed).
- **Before that**: prior session ran `ln-design` → `ln-scope` → `ln-spec` → `ln-handoff`; produced the scope card, settled D153/D154/A94, opened PR #138 with the planning baseline.
- **Flow position**: `design → scope → spec → handoff → build (done) → [scope next slice]`

## Commits on branch (6 total)
## What landed

1. `ed183421` FE-709: Plan umbrella frontier items + assign continuous-workspace
2. `372bba97` FE-709: Replace per-phase InterviewView with ContinuousWorkspaceView
3. `fa712f54` FE-709: Extract useContinuousWorkspaceController
4. `5292dbfb` FE-709: Sidebar scroll-spy highlighting via WorkspaceFocusContext
5. `f60eac9` FE-709: Extract shared controller helpers and enrichBottomArtifact to core
6. `7a35eee` FE-709: Retire route-first test assumptions
### Migration 0020 — thread substrate

## Remaining work
- `drizzle/0020_thread_substrate.sql` + journal entry
- Created `thread` table: `id`, `chat_id`, `kind` (interview/side/reconciliation/qa/agent_run), `target_item_id`, `context_spec`, `kickoff_turn_id`, `invoked_in_turn_id`, `active_turn_id`, `status`, `created_at`
- Partial unique index `thread_interview_unique` on `(chat_id) WHERE kind = 'interview'`
- Migrated `turn.chat_id` → `turn.thread_id` (NOT NULL)
- Dropped `chat.kind` and `chat.active_turn_id` (chat is now a pure container)
- Data migration: seeded one interview thread per existing chat; mapped turn rows through thread join

### Step 5 — Route collapse decision (DEFERRED)
### Schema (`src/server/schema.ts`)

Per CONTINUOUS_WORKSPACE_HYBRID.md §Migration Step 5: decide whether phase routes should become redirects or search-param aliases. This is a design decision, not urgent — the continuous center pane works correctly with current routes. Phase routes currently act as focus addresses into the shared workspace surface, which is the intended hybrid behavior.
- Added `thread` table definition with all columns and partial unique index
- Removed `kind` and `active_turn_id` from `chat`
- Changed `turn.chat_id` → `turn.thread_id` (NOT NULL, FK to thread)

### PR submission
### Specification store (`src/server/db/specification-store.ts`)

Branch `ka/fe-709-continuous-workspace` is ready for `gt submit`. All steps are complete except the deferred design decision (Step 5).
- `insertSpecificationWithInterviewChat` now creates spec + chat + interview thread atomically
- Added `getInterviewThreadIdForSpecification` helper
- `createTurn` routes through interview thread (validates parent in same thread)
- `advanceHead` mirrors `active_turn_id` to interview thread (was chat)

## Test status
### Capabilities (`src/server/capabilities.ts`)

1213/1214 pass. 1 pre-existing flake in InterviewView.test.tsx (`renders live workspace-tool activity during the submitted pre-stream generating window` — tool detail assertion, unrelated to FE-709).
- `getChatById` now joins through thread to source `kind` and `active_turn_id` from interview thread
- `getPrimaryChatFromCapability` delegates to updated `getChatById`
- `submitTurnResponseFromCapability` validates turn ownership through `specification_id` (was `chat_id`)

## Open questions (unchanged from prior handoff)
### Tests (`src/server/chat-substrate.test.ts`)

- Should the sticky center-pane header update to show the *focused* (scrolled-to) phase rather than always the *active* phase? Deferred to UX review.
- IntersectionObserver thresholds may need tuning for short sections. Manual walkthrough showed no issues.
- Should deep-link target become `?phase=design` search param or keep legacy phase paths? Deferred to Step 5.
- Full rewrite: 14 tests covering schema assertions, atomic quad creation, thread uniqueness, turn writes, head mirroring, atomicity, and read-path equivalence

### Blast-radius fixes

- `src/server/context.test.ts` — turn fixtures updated `chat_id: null` → `thread_id: 0`

## Decisions and assumptions

| Item | Type | Status | Source |
| --- | --- | --- | --- |
| Eager interview-thread creation in createSpecification | assumption | validated | scope card → code proves it |
| `chat.kind` and `chat.active_turn_id` retire entirely | decision | validated | grep clean; no readers remain |

## Repo state

- **Branch**: `ka/fe-710-chat-runtime-threads`
- **PR**: #138 (open, planning baseline only)
- **Dirty files**: all substrate changes uncommitted (ready for commit)
- **Test status**: 1261/1261 passing; `npm run verify` gate clean

## Next steps

1. Commit the substrate-landing slice on the current branch.
2. Scope the next slice: in-stream collapsible UI for non-interview thread kinds + `SideChatPopover` retirement cutover. This is still inside FE-710; same branch.
3. Manual walkthrough: `npm run dev`, create a spec, advance through a few grounding turns, reload, confirm continuity.

## Retirement rule

Delete this file after PR is submitted or after Step 5 is decided.
- Overwrite or delete this file once the next slice is scoped and the substrate commit is verified.
64 changes: 64 additions & 0 deletions drizzle/0020_thread_substrate.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
-- 1. Create thread table
CREATE TABLE `thread` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`chat_id` integer NOT NULL,
`kind` text NOT NULL,
`target_item_id` integer,
`context_spec` text,
`kickoff_turn_id` integer,
`invoked_in_turn_id` integer,
`active_turn_id` integer,
`status` text DEFAULT 'open' NOT NULL,
`created_at` text DEFAULT (datetime('now')) NOT NULL,
FOREIGN KEY (`chat_id`) REFERENCES `chat`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`target_item_id`) REFERENCES `knowledge_item`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`kickoff_turn_id`) REFERENCES `turn`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`invoked_in_turn_id`) REFERENCES `turn`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`active_turn_id`) REFERENCES `turn`(`id`) ON UPDATE no action ON DELETE no action
);--> statement-breakpoint

-- 2. Partial unique index: exactly one interview thread per chat
CREATE UNIQUE INDEX `thread_interview_unique` ON `thread` (`chat_id`) WHERE kind = 'interview';--> statement-breakpoint

-- 3. Seed one interview thread per existing chat
INSERT INTO `thread` (`chat_id`, `kind`, `active_turn_id`)
SELECT `id`, 'interview', `active_turn_id` FROM `chat`;--> statement-breakpoint

-- 4. Recreate turn with thread_id instead of chat_id
CREATE TABLE `turn_new` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`specification_id` integer NOT NULL,
`thread_id` integer NOT NULL,
`parent_turn_id` integer,
`phase` text NOT NULL,
`turn_kind` text DEFAULT 'question' NOT NULL,
`question` text DEFAULT '' NOT NULL,
`why` text,
`impact` text,
`answer` text,
`is_resolution` integer DEFAULT false NOT NULL,
`user_parts` text,
`assistant_parts` text,
`created_at` text DEFAULT (datetime('now')) NOT NULL,
FOREIGN KEY (`specification_id`) REFERENCES `specification`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`thread_id`) REFERENCES `thread`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`parent_turn_id`) REFERENCES `turn_new`(`id`) ON UPDATE no action ON DELETE no action
);--> statement-breakpoint
INSERT INTO `turn_new` (`id`, `specification_id`, `thread_id`, `parent_turn_id`, `phase`, `turn_kind`, `question`, `why`, `impact`, `answer`, `is_resolution`, `user_parts`, `assistant_parts`, `created_at`)
SELECT t.`id`, t.`specification_id`, th.`id`, t.`parent_turn_id`, t.`phase`, t.`turn_kind`, t.`question`, t.`why`, t.`impact`, t.`answer`, t.`is_resolution`, t.`user_parts`, t.`assistant_parts`, t.`created_at`
FROM `turn` t
JOIN `thread` th ON th.`chat_id` = t.`chat_id` AND th.`kind` = 'interview';--> statement-breakpoint
DROP TABLE `turn`;--> statement-breakpoint
ALTER TABLE `turn_new` RENAME TO `turn`;--> statement-breakpoint

-- 5. Recreate chat without kind and active_turn_id
CREATE TABLE `chat_new` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`specification_id` integer NOT NULL,
`created_at` text DEFAULT (datetime('now')) NOT NULL,
FOREIGN KEY (`specification_id`) REFERENCES `specification`(`id`) ON UPDATE no action ON DELETE no action
);--> statement-breakpoint
INSERT INTO `chat_new` (`id`, `specification_id`, `created_at`)
SELECT `id`, `specification_id`, `created_at` FROM `chat`;--> statement-breakpoint
DROP TABLE `chat`;--> statement-breakpoint
ALTER TABLE `chat_new` RENAME TO `chat`;
7 changes: 7 additions & 0 deletions drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@
"when": 1776360000000,
"tag": "0019_reconciliation_need_agent_columns",
"breakpoints": true
},
{
"idx": 20,
"version": "7",
"when": 1776370000000,
"tag": "0020_thread_substrate",
"breakpoints": true
}
]
}
27 changes: 14 additions & 13 deletions memory/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ The May 2026 intent-spec, multi-chat, changeset-ledger, prompt/context, and agen
### Active

1. `agent-fixture-substrate` — branch-complete off main, reconciling — FE-705 integration substrate for JSONL agent capability CLI and LLM-as-user probes.
2. `chat-runtime-threads` — FE-710; Track 2 of the runtime umbrella; substrate sub-RFC settled on option (q) new `thread` table; first slice lands the substrate without UI cutover.

### Next

1. `chat-runtime-threads` — Track 2 of the runtime umbrella; immediate successor to continuous-workspace, unblocker for Tracks 3 and 5. First slice should be a sub-RFC on the thread substrate shape (p / q / r).
2. `intent-graph-semantics` — highest-coordination semantic substrate after FE-705 reconciliation.
3. `changeset-ledger` — Track 4 of the runtime umbrella; parallel with Track 2; semantic history spine needed before canonical proposal acceptance, direct-edit atomicity, and productized scenario options.
4. `thread-context-provision` — Track 5 of the runtime umbrella; after Track 2 lands the thread substrate.
5. `reconciliation-runtime` — Track 3 of the runtime umbrella; after Track 2 + Track 4 provide thread substrate and durable attribution.
6. `graph-review-scenario-options` — artifact-only critique/probe lane; can advance in parallel with FE-700 if it does not commit canonical graph truth.
7. `productized-scenario-options` — user-facing acceleration surface after FE-700 semantics, FE-701 changesets, and graph-review probes.
1. `intent-graph-semantics` — highest-coordination semantic substrate after FE-705 reconciliation.
2. `changeset-ledger` — Track 4 of the runtime umbrella; parallel with Track 2; semantic history spine needed before canonical proposal acceptance, direct-edit atomicity, and productized scenario options.
3. `thread-context-provision` — Track 5 of the runtime umbrella; after Track 2 lands the thread substrate.
4. `reconciliation-runtime` — Track 3 of the runtime umbrella; after Track 2 + Track 4 provide thread substrate and durable attribution.
5. `graph-review-scenario-options` — artifact-only critique/probe lane; can advance in parallel with FE-700 if it does not commit canonical graph truth.
6. `productized-scenario-options` — user-facing acceleration surface after FE-700 semantics, FE-701 changesets, and graph-review probes.

### Parallel / Low-conflict

Expand Down Expand Up @@ -74,15 +74,16 @@ The May 2026 intent-spec, multi-chat, changeset-ledger, prompt/context, and agen
### chat-runtime-threads

- **Name:** Chat runtime — thread substrate + in-stream rendering (Conversational Workspace Runtime — Track 2)
- **Linear:** unassigned in this plan snapshot
- **Linear:** FE-710
- **Kind:** structural
- **Status:** not-started
- **Objective:** Add a thread primitive to the chat substrate, render threads inline as collapsibles in the main chat surface (Cursor-style), and retire the SideChatPopover and transient staged-patches strip. Decide the thread substrate shape via a sub-RFC: (p) `parent_chat_id` on `chat`, (q) new `thread` table, or (r) UI-only rendering.
- **Why now / unlocks:** Track 1 (workspace shell) ships, providing the stable host. Threads are the critical unblocker for reconciliation absorption into the chat surface (Track 3), `#` mention / turn-zero / context provision (Track 5), and the retirement of the V3.1 popover and staged-patches surfaces. Supersedes the prior side-chat V4a persistence horizon — persistent side-chat history becomes the main chat stream where threads stay collapsed.
- **Acceptance:** Thread kinds (`interview`, `side`, `reconciliation`, `qa`) are representable in the substrate; threads render inline as collapsibles in the unified chat surface; SideChatPopover retires as cutover; transient staged-patches strip retires (replaced by in-thread mutation state); turn-zero (`turn_kind='kickoff'`) becomes the universal thread entry.
- **Status:** in-progress
- **Objective:** Add a `thread` primitive between chat and turn, render threads inline as collapsibles in the main chat surface (Cursor-style), and retire `SideChatPopover` and the transient staged-patches strip. Substrate sub-RFC settled: option (q) new `thread` table; chat collapses to a pure container; flat threads with `thread.invoked_in_turn_id` for inline agent runs (no nested threads in V1).
- **Why now / unlocks:** Track 1 (workspace shell) shipped, providing the stable host. Threads are the critical unblocker for reconciliation absorption into the chat surface (Track 3), `#` mention / turn-zero / context provision (Track 5), changeset attribution (Track 4), and the retirement of the V3.1 popover and staged-patches surfaces. Supersedes the prior side-chat V4a persistence horizon — persistent side-chat history becomes the main chat stream where threads stay collapsed.
- **Acceptance:** `thread` table exists with kinds (`interview`, `side`, `reconciliation`, `qa`, `agent_run`); `turn.thread_id` replaces `turn.chat_id`; `chat.kind` and `chat.active_turn_id` retire; threads render inline as collapsibles in the unified chat surface; `SideChatPopover` retires as cutover; transient staged-patches strip retires (replaced by in-thread mutation state); turn-zero (`turn_kind='kickoff'`) becomes the universal thread entry; agent runs render inline via `thread.invoked_in_turn_id`.
- **Verification:** Thread substrate schema/migration tests, in-stream collapsible rendering tests, manual walkthroughs for thread creation/display/collapse per kind, regression on existing interview flow.
- **Traceability:** A82, A83, A88; D86, D87, D110, D114, D138, D146; I111, I113.
- **Traceability:** Requirements 39 (updated), 45; A88, A94; D86, D87, D110, D114, D138, D146, D153, D154; I111 (extended), I113.
- **Design docs:** `docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md` §3.2 + §5 Track 2; `docs/design/MULTI_CHAT.md`; `docs/design/SIDE_CHAT.md`.
- **Current execution pointer:** substrate-landing slice landed (migration 0020, schema/store/test rewrite). Next slice: in-stream collapsible UI for non-interview thread kinds, `SideChatPopover` retirement cutover.

### reconciliation-runtime

Expand Down
Loading
Loading