All workstreams D, E, B, C, F, A, G done and pushed; resume cold-start stall
resolved (open-item #1). Refresh the Git section: main @ cbb8e18, the three
stale merged branches deleted, only main remains. Clean handoff snapshot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
17 KiB
ThothII — Project State
Starting-point snapshot for new sessions. Last updated: 2026-06-30. Point a fresh session here ("read PROJECT_STATE.md") before substantial work.
What ThothII is
A human-in-the-loop datamart builder: it turns a natural-language question into validated SQL (and optionally a dbt datamart) through a deterministic 8-phase NL→SQL workflow, where the model proposes and a human reviewer decides at gates. The UI is meant to embed inside the Omics Portal (GSD design system) and is English.
Architecture — three layers
frontend (React, :5173) → backend (Fastify, :8787) → pi --mode rpc → tht / harness → DWH (read-only)
- harness/ — the Pi layer. A deterministic Python CLI
tht+ a Pi gate extension (.pi/extensions/tht-gate.js) that runs the 8-phase workflow and emits/consumes widget-descriptor JSON. Owns all persistence. Workflow truth isharness/workflow.yaml; orchestration rules areharness/.pi/skills/tht-sessione/SKILL.md. - backend/ — Fastify + TypeScript. A thin bridge: proxies REST routes to the
thtCLI (ThtRunner), manages Pi processes (PiProcessManager, one child per session), bridges Pi RPC events to SSE (SessionBridge+SseHub). No application database. - frontend/ — React 18 + base-ui + Tailwind + TanStack Query + Zustand. Chat-style
shell (
src/shell/AppShell.tsx); the live transcript is rebuilt in-memory from the SSE stream (src/store/sessionStore.ts), not persisted.
Persistence model (the load-bearing premise)
There is no verbatim chat store. Each workflow phase persists its own document into the
session directory, and that IS the persistence. A session = a directory under the
workspace's sessions/ path containing session_manifest.yaml + phase artifacts
(question.md, schema_linking.json, cte_plan.json, sql_final.sql,
validation_report.md, review_decisions.jsonl, …). A fresh Pi process resumes by reading
tht session show <id> + the on-disk artifacts — never by replaying chat.
The 8 phases (harness/workflow.yaml)
F1 chiarimento · F2 memoria · F3 riscrittura (question.md) · F4 schema_linking
(schema_linking.json) · F5 sintesi · F6 cte (cte_plan.json, cte_tests.json) ·
F7 sql_finale (sql_final.sql) · F8 datamart. Current phase is a fold over the decision
ledger (harness/tht/phase.py); statuses: open / closed / finalized.
How to run
Full stack (real Pi + DWH): ./scripts/run-stack.sh
Prereqs: VPN on; pi on PATH (configured model); harness/.env populated;
harness/config/tht.yaml → a workspace; deps installed in all three projects.
Opens frontend at http://localhost:5173 → backend :8787.
Individual dev:
- backend:
cd backend && npm run dev(tsx watch; env:PORT,THT_HARNESS_DIR,THT_BIN,PI_BIN,AUTH_MODE) - frontend:
cd frontend && npm run dev(Vite;VITE_BACKEND_URL→ backend) - harness install:
cd harness && python -m venv .venv && pip install -e ".[dev]"→thton PATH
How to test (all green as of 2026-06-30: harness 269 / backend 67 / frontend 87)
- harness:
cd harness && .venv/bin/pytest -q(5 L2/real-DB tests are deselected by default) - backend:
cd backend && npx vitest run· typechecknpx tsc --noEmit -p . - frontend:
cd frontend && npx vitest run· typechecknpx tsc -b· e2enpm run e2e(Playwright)
Config & workspaces
- Workspaces:
harness/workspaces/*.yaml(psd,tht-test,tht.example). A workspace sets the DB target and the absolutepaths.sessions/artifacts/indexes(psd → a separate repotht-workspace-psd/, NOT committed here). - Secrets live ONLY in
harness/.env(gitignored;THT_*— DB, DWH REST, vector, SSL CA…). Seeharness/.env.examplefor the variable list. - App settings (global):
backend/data/settings.json(gitignored) —{ workspace, provider, model, thinking }. The "New session" form is question-only; these settings supply the rest.
Conventions & contracts (don't relearn the hard way)
-c/--configis a PER-COMMAND option intht— append it AFTER the subcommand, never globally (ThtRunner.buildArgvhandles this).--jsonoutput must be pristine (only valid JSON on stdout).- UI strings are English. Document content stays in the workspace language (Italian for psd) because it's the real data; only chrome/labels are English.
- Settings are global, not per-question.
- TDD throughout; tests assert real behavior, not mocks. Frequent, scoped commits.
- Global user rules (
~/.claude/CLAUDE.md): think before coding, simplicity first, surgical changes, goal-driven verification.
UI/UX redesign + Resume — COMPLETE (2026-06-30)
Plan: ~/.claude/plans/foamy-forging-dahl.md. Memory: thothii-ui-redesign-inprogress.md.
All workstreams done and pushed to origin/main: D + E @ 0eeb3f7, B + C @ b056ff3,
F @ cef9ae4, A @ 0a13f71, G @ e8cdd00(scope) + cbb8e18(results). Nothing pending from this
plan. Per-workstream detail below for reference.
- D — DONE (
c12bdcd): session displayname= 3-5 Italian keywords via YAKE (no LLM), derived intht session new(CLI layer);create_sessioncore unchanged (name=Nonedefault).yakeadded toharness/pyproject.toml. TDDtests/test_session_name.py; harness 269 passed. - E — DONE (
0eeb3f7): rotating activity icon replaces the red dot inCentralStatus(inline, clickable → opens the panel);ModelActivityPanelis a 5-line expandable model-stream tail;WorkingSpinnerextracted to its own module; the separate spinner button- orphaned
Transcript.tsxremoved. Frontend 87/87, tsc clean. Live visual check DONE (2026-06-30): inline spinner opens the panel; 5-line collapsed tail; expand → full transcript.
- orphaned
- B — DONE (
b056ff3):WorkflowBaris now colored dots F1..F8, no phase-name text (amber-translucent=running, green=done, red=error, gray=pending; green connectors lead the active dot). Each dot carriesdata-state. Error is lightweight: storephaseErrorset when aninfolevel=errorarrives during the phase, cleared on the nextui_request(sessionStore.ts). All four states live-verified via Playwright. - C — DONE (
b056ff3): right sidebar — single-line denser rows (inline status dot + name,py-1), a 3-level type hierarchy via/impeccable(L1SESSIONSred/bold/wide-tracking · L2 section + group headers muted uppercase · L3 names normal-case), and the "No group" label removed (ungrouped sessions render after the last group; guarded so the empty-state still teaches when there are no groups). Live-verified. (Resume inSessionMenustays with A1.) - Tests: frontend 93/93 (was 87; +3 store
phaseError, +2WorkflowBardot-state, +1 AppShell no-"No group"),tsc -bclean. - F — DONE (uncommitted; live check deferred to G): single-select answers auto-confirm.
reviewer_selectoptions may carry adecisionpayload ({type, subject, detail?, rationale?}) and an optionaladvance; picking such an option persists the decision directly viatht decision add(shareddecisionAddArgshelper, also used byreviewer_decide) — no redundantreviewer_decide/reviewer_confirmgate. Options without a payload stay ask-only; back/exit/Other never persist. Pure logic extracted toresolveSelectOutcome/decisionAddArgs(exported, unit- tested). Contract docs updated:reviewer_selecttool desc +SKILL.md(widget summary, disciplines 2-3, Phase-1 single-pick) + theCLAUDE.mdgate note. Gate JS 33/33, harness 269. Live verification (model actually usesreviewer_select+decision, no follow-up gate, decision inreview_decisions.jsonl) deferred to G — it is model-behavior-dependent. - A — DONE (uncommitted): A1 —
SessionMenugains a Resume item (gated tostatus!=="finalized" && !archived), wired inAppShellto the existingdoResume→POST /sessions/:id/resume. 3 tests (SessionMenu.test.tsx); frontend 96/96, tsc clean. A2 — diagnosis-first clean-room repro shows the resume cold-start stall NO LONGER reproduces on pi 0.79.4 (8/8 chained into the tool calls, fresh + partway; GLM 5.2 now narrates AND emitstht session show+read SKILL.mdin-turn). The earlier narrate-and-stop predates the pi upgrade. Defense-in-depth applied:RIPRENDI_KICKOFFhardened to force the in-turn tool call (gate test + live regression 2/2). The cross-model angle (weaker/older models) lives in G. - G — DONE (
e8cdd00+cbb8e18): cross-model behavior matrix via a committed clean-room harness (harness/scripts/model-matrix.mjs). Tier 1 — kickoff + resume first-turn: all available models chain in-turn (zai/glm-5.2,deepseek/deepseek-v4-{pro,flash},aritmolab/qwen3.6-35b-a3b,zai/glm-4.5-air); the resume stall recurs on none (closes A's cross-model robustness).aritmolab/gemma4-26b-a4b= 404 unavailable at the endpoint (listed but not served) — infra gap, not a workflow issue. Tier 2 — F single-select auto-confirm verified live onglm-5.2: answering the firstreviewer_selectpersisted aconcept_clarifieddecision 0→1 with no follow-up gate (closes F's deferred live check). Full results: the G plan doc + memorythothii-cross-model-matrix. No prompt hardening needed.
Status: All UI-redesign + resume workstreams done and pushed — D, E, B, C, F, A, G.
Nothing pending from the plan. Optional nice-to-haves (not required): Tier-2 F/multiselect live for
the non-baseline models (cheap re-run with harness/scripts/model-matrix.mjs + the Tier-2 method),
and a one-off manual Playwright kebab→resume pass in the live UI.
Live verification + reviewer_select fix (2026-06-30, afternoon)
Drove the real stack (Playwright → backend → real Pi → GLM 5.2 → DWH) end-to-end.
- F1 hang fix (
418187a) VERIFIED LIVE. Answered an F1 reviewer widget; Pi resumed (model socket reopened) and the gate produced new output — vs the old silent hang. The transition "silent hang → gate re-presents/advances" provesctx.ui.inputnow resolves. - New bug found + fixed: reviewer_select
choicesvschoice. The gate'sreviewer_select(andreviewer_confirmreject) readresp.choice(singular) but the frontend uniformly sendschoices: [id](array) — so every single-select gate answered "Nessuna scelta ricevuta" and re-proposed forever (multiselect was fine; it already readchoices). Fix: a sharedselectedChoice(resp)helper (harness/.pi/extensions/tht-gate.js) reading the array; both handlers use it. TDD:gate/__tests__/gate_choice.test.jsRED→GREEN, full gate suite 28/28. VERIFIED LIVE: a single-select answer is now accepted and the workflow advances (2/4 → 3/4). - Resume cold-start STALL confirmed (open item #1). On
/riprendi-sessione, GLM 5.2 narrates the bootstrap step then ends the turn without the tool call → Pi idle, unrecoverable from the UI. Memory:thothii-resume-cold-start-stall.md. - GLM 5.2 F1 is slow (~3-4 min, ~50+ reads) but works — looks stuck but isn't; don't hit
"Stop and save" (it
POST /closes → kills Pi). Memory:thothii-glm52-f1-slow-not-stuck.md.
Earlier work — F1 reviewer-widget hang fix + multiselect guidance (committed 2026-06-30; authored 2026-06-29)
Two fixes, committed to main (7 files):
-
Bug: every reviewer widget hung "stuck with no output" after the human answered — F1 disambiguation (and any gate) dead-ended. Root cause, confirmed from Pi's own source (
@mariozechner/pi-coding-agentdist/modes/rpc/rpc-mode.js,createDialogPromise):ctx.ui.inputassigns its OWN RPC id (crypto.randomUUID) and correlatesextension_ui_responseon THAT id, silently dropping unknown ids. The gate puts a different id (u${Date.now()}) inside the descriptor carried intitle.SessionBridgewas replying with the descriptor id, so real Pi never resolvedctx.ui.input→ the model never continued. Fix:SessionBridgenow stores Pi's top-levelm.id(pendingPiId) on the incoming request and repliesextension_ui_response{ id: pendingPiId, value: <uiResponse JSON> }(value still carries the descriptor id, so the gate's internalresp.id === descriptor.idcheck holds). File:backend/src/bridge/session-bridge.ts. Full write-up: memorypi-ui-input-id-correlation.md.- The test double was masking it:
harness/tests/fake_pi/fake_pi_rpc.mjshad forcedm.id == descriptor.id. Corrected to mirror real Pi (distinctrandomUUIDtop-level id, correlate on it, drop unknown ids);test_fake_pi_contract.mjsgained a negative regression test ("respond with descriptor id → no follow-up"). - TDD:
backend/test/session-bridge.test.ts(unit) +backend/test/e2e-f1.test.ts(integration — now asserts the model's follow-up arrives after the answer) went RED→GREEN.
- The test double was masking it:
-
UX: multi-answer disambiguation —
harness/.pi/skills/tht-sessione/SKILL.mdPhase 1 now tells the model to usereviewer_decide(the existing multiselect/checkbox widget) when an ambiguity admits several simultaneously-true answers, instead of single-pickreviewer_select. Guidance-only — no new widget (frontend MultiselectWidgetalready exists).
Verified at commit time: backend npx vitest run 67/67 green; tsc --noEmit -p . OK;
fake-pi contract node --test test_fake_pi_contract.mjs 2/2 green. Verified LIVE
2026-06-30 (see the top "Live verification" section).
Most recent feature — Session management (MERGED to main @ 2c21e46)
Full session management modeled on Claude's UI, all three layers:
- Read-only "split view" panel (left drawer,
SessionDocumentsPanel) showing a session's phase documents read-only (reusesSqlViewer/SchemaLinkingViewer/MarkdownView). - Rename / Move to group / Archive / Delete via a kebab menu (
SessionMenu) → REST →tht session set-name/set-group/archive/unarchive/delete. Archive = a manifestarchivedflag (not a dir move); groups = a manifestgroupfield; delete = hardrmtree+ confirm. - Rail: collapsible group headers + "No group" + a separate Archive view.
- Resume correctness: read-only guard (HTTP 409 when
finalizedorarchived);PiProcessManager.spawnFornow has anew/resumemode (resume sends/riprendi-sessione <id>); a "Phase 0 — Resume" cold-start section inSKILL.md. - Design docs:
docs/superpowers/specs/2026-06-29-session-management-design.md+docs/superpowers/plans/2026-06-29-session-management.md.
⚠️ Open items / pending gates
- Resume cold-start stall — RESOLVED on pi 0.79.4 (workstream A, 2026-06-30). The earlier
narrate-and-stop (GLM 5.2 narrating the bootstrap step then ending the turn without the tool
call) no longer reproduces: a clean-room repro of the backend's exact resume handshake
chained into
tht session show+read SKILL.mdin-turn 8/8 (fresh + partway sessions). The pi upgrade is the likely fix. Defense-in-depth:RIPRENDI_KICKOFFhardened to force the in-turn tool call (gate test + live 2/2). Memory:thothii-resume-cold-start-stall.md. Remaining: the cross-model angle (older/weaker models) is folded into G; a full Playwright kebab→resume pass through the live UI is still worth one manual run (item 2). - Full Playwright live-stack verification (MANUAL, not yet run).
- Minor backlog (non-blocking): explicit id-traversal guard in
delete_session(today gated byload_session);close_sessioncould reuse_save_touched(DRY); delete-via-kebab integration test skipped (base-ui Menu portal not drivable in jsdom — the dialog itself is unit-tested); a couple of test-file lint nits. - DONE — F1 hang fix live-verified 2026-06-30 (see top section). The live verification
also surfaced + fixed the reviewer_select
choicesmismatch. - Resolved: settings use
zai/glm-5.2/medium (not deepseek-flash); GLM 5.2 drives F1 fine, just slowly (~3-4 min, ~50+ reads). To tell a truly stalled Pi from a merely-slow one, check its sockets/children. Memory:thothii-glm52-f1-slow-not-stuck.md.
Where design history lives
- Specs:
docs/superpowers/specs/· Plans:docs/superpowers/plans/ - SDD execution ledger (gitignored scratch):
.superpowers/sdd/progress.md - Auto-memory index:
~/.claude/projects/-Users-mp-projects-ThothII/memory/MEMORY.md(Pi RPC event vocabulary, ui.input id correlation, Omics Portal/GSD design system, resume cold-start stall, GLM 5.2 F1 slow≠stuck).
Git
main @ cbb8e18, pushed to origin (github.com/mptyl/ThothII); working tree clean, no stashes.
main is the only branch — the three stale branches (feat/settings-menu bcaf670,
feat/thothII-debugging d67409f, feat/ui-redesign-resume 0eeb3f7) were fully merged ancestors
(no unique work) and were deleted on 2026-06-30 (SHAs recoverable via reflog).
Redesign+resume commits on top of 0eeb3f7: b056ff3 (B/C dots+sidebar) · cef9ae4 (F
auto-confirm) · 0a13f71 (A resume menu + kickoff hardening) · e8cdd00 (G scope) · cbb8e18
(G results). Earlier today: 418187a (F1 hang fix) · d27afd0 (reviewer_select choices fix) ·
c12bdcd (D) · 0eeb3f7 (E).