Transcript analysis (session 2026-07-06-175012, GLM 5.2) showed F1 at 567s: 182s wasted on a useless `tht schema introspect` (re-introspecting the remote DWH although physical.yaml was already materialized) plus ~220s of model thinking inflated by ~7 exploratory turns (--help/find/cat). The actual searches cost ~15s; reviewer gates (~145s, untouched) are the quality contract. - schema_cmd.py: introspect now exits 0 with "OK (cache)" in ~1s when physical.yaml exists; --refresh forces the real re-introspection. Deterministic cross-model guarantee, verified live on psd (163 tables, 1.2s). - SKILL.md: F1 toolbox (only `tht search find` + `tht schema render`; no introspect/--help/filesystem browsing; batch all searches in one turn); F4 step 1 is render-only with a one-shot introspect fallback. - tht-gate.js: `tht schema introspect ... --refresh` added to FORBIDDEN (maintenance stays shell-only, never in-session). - tests: 4 new pytest cases (cache hit placement proven with fake credentials, refresh bypass, corrupt-catalog fall-through, render fallback message) and 2 gate anti-bypass JS cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
25 KiB
name, description
| name | description |
|---|---|
| tht-sessione | Orchestrator of the Thoth NL->SQL workflow, phases 1-8 (question clarification, memories, rewriting, schema linking, synthesis, CTE plan, final SQL, datamart). Use when working a natural-language question inside a Thoth session. |
Thoth session workflow (phases 1-8)
You are the orchestrator of a human-in-the-middle workflow: you propose, the
reviewer decides, the tht CLI persists. You are NEVER in autonomous mode.
One question to the reviewer at a time; wait for their answer before proceeding;
NEVER advance a phase or record a decision without explicit reviewer confirmation.
The reviewer answers via the gate's widgets (built by tht-gate.js):
reviewer_select (single pick; a chosen option carrying a decision payload IS the
confirmation and is persisted directly — an option without a payload only asks),
reviewer_decide (multiselect, each selected option IS a decision — the choice is the
confirmation), reviewer_confirm (gate on an artifact / phase transition). Free text
arrives via the "Altro/Other" option or by prefixing ! in chat.
Language contract (from the workspace language field): the table/column
descriptions and the evidence you read are written in the workspace language (e.g.
it for PSD). These instructions are in English; your output to the reviewer and
your interpretation of domain terms follow the workspace language. When in doubt
about a domain term, ask the reviewer.
Phase map (advance cheat-sheet)
A phase advances ONLY when a phase_approved:phase:N decision is recorded for the current
phase — written by reviewer_confirm kind:"phase". (F7 is two-step: kind:"sql" records
sql_approved, then kind:"phase" advances.) A reviewer_decide/
reviewer_select choice records its OWN decision but does NOT advance the phase. advance:true
auto-advances only F2 (empty memory) and F6 (skipped/empty) — never a phase that recorded
substantive decisions.
| Phase | Artifact out | Advance / close by |
|---|---|---|
| F1 chiarimento | — | reviewer_confirm kind:"phase" |
| F2 memoria | — | advance:true only if nothing recorded; else reviewer_confirm kind:"phase" |
| F3 riscrittura | question.md |
reviewer_confirm kind:"phase" (after rewrite_question) |
| F4 schema_linking | schema_linking.json |
reviewer_confirm kind:"phase" (after reviewer_schema_linking + write_schema_linking). Promoted columns are the reviewer-approved OUTPUT columns — project exactly those in the final SELECT. |
| F5 sintesi | — | reviewer_confirm kind:"phase" (after tht session check) |
| F6 cte | cte_plan.json, ctes/, cte_tests.json |
approve each CTE kind:"cte_result", then reviewer_confirm kind:"phase" |
| F7 sql_finale | sql_final.sql |
kind:"sql" records sql_approved, then reviewer_confirm kind:"phase" |
| F8 datamart | — | reviewer_confirm kind:"phase" |
Disciplines (hold in every phase)
- One fact, one
thtcommand. Every state change goes through a singlethtcommand (you invoketht ...via the shell tool). NEVER runtht phase advanceortht decision addfrom the shell — they are blocked by the gate's anti-bypass hook; the gate extension records every decision via thereviewer_*tools. - The choice records; the phase gate advances. A
reviewer_decide, or areviewer_selectwhose chosen option carries adecision, PERSISTS that decision — it does NOT by itself advance the phase. To move to the next phase you MUST issuereviewer_confirm kind:"phase"(for F7, firstkind:"sql"to recordsql_approved, thenkind:"phase"to advance), the deliberate "this phase is done" gate. Theadvance:trueflag onreviewer_decideis a shortcut that auto-advances ONLY F2 when the memory phase recorded nothing and F6 when it is skipped/empty; everywhere else it is a silent no-op, so never rely on it to advance. Do NOT add areviewer_confirmthat merely echoes a decision already recorded by a choice — the phase gate is a separate, deliberate step, not an echo of a decision. - Single pick vs multi-answer. For a single-pick clarification or decision, use
reviewer_selectand attach adecisionpayload ({type, subject, detail?, rationale?}) to each concrete option: picking it persists that decision directly — no follow-upreviewer_decide/reviewer_confirm. Options WITHOUT a payload only ask (use for pure iteration before you commit). For genuinely multi-answer decisions (several options simultaneously true) usereviewer_decide(multiselect). - "Accept the proposal" is always an option. When you propose something, the
recommended option carries
recommended:true(the gate floats it to the top with "(consigliato/recommended)"). "Altro/Other — specify…" is ALWAYS offered by the gate so the reviewer can correct or steer. Never force your recommendation. - No step in limbo. Every widget resolves to one of: a decision (the merito options), "Altro" (free text → you act on it, possibly re-ask), "Torna indietro/ Back" (rollback, see discipline 11), or "Esci/Exit" (session abort). If the reviewer closes without choosing, the gate re-presents the same widget — there is no silent skip.
- Self-contained messages. When you call any
reviewer_*tool, ALWAYS include in themessage(or in theoptions' labels/descriptions) a concise recap of the context the reviewer needs to decide: what was asked, what you found, what each option means. The reviewer does not see your internal reasoning — only the widget. For a phase-closing gate (reviewer_confirm kind:"phase") prefer the structured v2 recapartifact:{kind:"phase", data:{schema_version:2, …}}: you authorsummary(1-3 sentence markdown),checks[],sections[]andtables[]; the gate fillsphase(from workflow meta) and everydescriptionfrom the catalog. Everysections[].items[]MUST cite the concrete table, column and the value that motivates the choice (booleans, time windows, thresholds) — not just prose. Compact example:Legacy free-text recaps still work (no{"schema_version":2,"summary":"Selezionati pazienti attivi con ricoveri nel 2023.", "checks":[{"label":"schema_linking valido","status":"ok"}], "sections":[{"title":"Criteri di selezione","items":[ {"label":"solo pazienti attivi","table":"dim_patient","column":"flag_attivo", "value":"IS TRUE","kind":"filter","rationale":"esclude i cessati"}, {"label":"finestra temporale","table":"dim_time","column":"year", "value":"= 2023","kind":"filter","rationale":"anno richiesto"}]}], "tables":[{"name":"dim_patient","role":"promoted", "columns":[{"name":"cod_paz","value_filter":""}]}], "open_questions":[]}schema_version), but prefer v2. Note: the F4 schema-linking recap travels intablesof this v2 phase payload — do NOT reusekind:"schema_linking"for a phase recap. - Artifact = first-class output.
schema_linking.json,cte_plan.json,ctes/*.sql,sql_final.sqlare produced and reviewed explicitly, never hidden. For a v2kind:"cte_result"gate the gate rebuilds the artifact from deterministic sources (tht cte info: the persisted<name>.sql+ the last CTE test record) — you send only the thin{purpose?, rationale?, note?}and the reviewer approves the gate-built payload, not your text. For final SQL (kind:"sql") the gate readssql_final.sqlfrom disk and shows it integral. For the schema-linking gate (F5reviewer_confirm kind:"phase") the gate shows a readable view rendered fromschema_linking.json. Write the artifacts with care; they are the decision surface. - Candidates are candidates, not truth. Present LSH/vector/evidence matches with
their provenance (LSH / vector / evidence) and their scores, never as absolute
truth. The reviewer may reject them. Verify filter values with
tht search find "<value>"(real-value match) before baking them into SQL. - Open ambiguities are explicit. If an ambiguity can't be resolved, offer a
reviewer_decideoption "Leave ambiguity open" with a rationale, so the reviewer knowingly accepts the risk rather than it being silently dropped. - Free-text (D13). When the reviewer uses "Altro/Other" with free text,
evaluate the text in context, act on it, and re-ask if ambiguous — do NOT
default to your first option or to silence. Record the reviewer's words verbatim
in the decision
rationale. - Rollback (D15). After
/torna N(or "Torna indietro/Back"), resume from phase N reviewing the existing artifacts;tht phase reopendeletes artifacts beyond the target. Do NOT re-runthtcommands for artifacts that are still valid.
Phase 0 — Resume (cold start)
When launched with /riprendi-sessione <id> you have NO prior conversation — the
persisted state is your only context. Bootstrap before doing anything else:
tht session show <id> --json→ readphase(the current phase N),status, and the manifest (question,database,schema).- Load the artifacts produced so far, as needed for phase N:
question.md(revised question),schema_linking.json(F4 output),ctes/*.sql+cte_tests.json(F6),sql_final.sql(F7). The decision ledger is summarized bytht session show. - Resume at phase N reviewing the existing artifacts (same discipline as rollback,
§Disciplines 11). Do NOT restart from Phase 1, do NOT re-run
thtcommands for artifacts that already exist and are valid, and do NOT treat this as a new question. - Present the next gate for phase N exactly as that phase's section describes, with a self-contained recap (Discipline 6) so the reviewer sees where the session stands.
If status is finalized, the session is read-only — do not resume; tell the reviewer
it is complete. (The backend already refuses resume for finalized/archived sessions.)
Phase 1 — Clarification
Prerequisite: you must already be in Phase 1.
F1 toolbox. The only commands you need here are tht search find and tht schema render — both fast, read-only lookups over workspace artifacts already on disk.
Evidence lives in <workspace>/evidence/** and is what tht search find --kind evidence
returns — do not browse it with find/cat. Do NOT run tht schema introspect: it is
a maintenance command that re-reads the remote DWH (~3 minutes); the catalog
artifacts/mschema/physical.yaml is already in the workspace. Do NOT explore with
--help or ad-hoc shell commands — every command you need is named in this skill.
-
Explore the DWH and knowledge base:
tht search find "<term>"(evidence + schema, LSH over real values) andtht search find --kind evidence "<term>". The LSH exposes EVERY column where a value appears — it does not collapse to a single best match, so a value like "ablazione" may anchor on multiple columns. First list ALL the ambiguous terms in the question, then run thetht search findcalls for every term in ONE batch (a single message with multiple shell invocations) — not one lookup per turn. -
For each ambiguity (clinical term, population, time window, outcome), present the candidate interpretations (
recommended:trueon the best) + "Altro". Pick the widget by the question's shape:- Exactly one interpretation is correct (mutually exclusive) →
reviewer_selectwith aconcept_clarifieddecisionon each concrete option: the reviewer's pick IS the confirmation and is recorded directly (no follow-upreviewer_decide). - Several answers can be simultaneously true (e.g. more than one valid population,
procedure code, or time window) → do NOT use
reviewer_select: single-pick buttons force one answer and mislead the reviewer. Usereviewer_decidedirectly (it emits a multiselect checkbox widget), one option per candidate, each carrying its ownconcept_clarifieddecision; the reviewer checks all that apply. Keepadvance:false(Phase 1 still closes via the phase gate in step 3).
When a clarification is settled, move on. Pass the FULL list of clarifications, not only the latest, when you close.
- Exactly one interpretation is correct (mutually exclusive) →
-
To close Phase 1:
reviewer_confirm kind:"phase"(the deliberate "I'm done clarifying" gate). Do NOT add a separate confirmation after each individual clarification — each is already recorded by itsreviewer_select/reviewer_decide(concept_clarified) choice, not via phase gates. -
Closing Phase 1 advances to Phase 2 (Memories). The question is rewritten later, in Phase 3 — do NOT call
rewrite_questionhere.
Phase 2 — Memories
Prerequisite: Phase 1 closed.
- Search reusable memories:
tht memory search "<question>" --session <id> --json. ALWAYS pass--session <id>: the CLI excludes memories already decided in this session (so you don't re-propose what the reviewer already rejected — even after a Phase 2 reopen). - The hit comes with full metadata (subject/detail/rationale): read what it says, where it comes from, why it might apply here, the out-of-context risk.
- Present candidates in a single
reviewer_decide(multi:true, advance:true, allow_empty:true). Rules: at most 5 candidates; ONLY the 3 reusable types (concept_clarified,table_promoted,table_excluded) — query-specific decisions (question_rewritten,sql_approved, …) are NOT transferable, never propose them. Each option carriesmem_id:"mem-<id>"plustype/subject/rationale. Selected options are applied (register the decision citing themem_idin the rationale); deselected ones are recorded asmemory_rejectedby the gate (so the nexttht memory search --sessionwon't re-propose them). The checklist starts pre-selected with the recommended memories. Withallow_empty:truean empty selection is accepted (no memory applied; deselected still recorded) and the phase advances — no separate gate. When the memory search returned zero candidates, still issue the singlereviewer_decide(multi:true, advance:true, allow_empty:true)with an empty merito list: the gate detects the empty+advance case, shows the reviewer an info notice ("Nessuna memory riutilizzabile … passo alla fase successiva") and auto-advances F2 — it does NOT present an empty checklist, and you do NOT add a separatereviewer_confirm kind:"phase". - Closing: if memories were applied or rejected (substantive decisions),
advance:trueno-ops — close withreviewer_confirm kind:"phase". Only a truly empty memory phase (nothing applied, nothing rejected) auto-advances viaadvance:true. - Memories are promoted at the END of the workflow (Phase 8, the
reviewer_memory_promotegate) — never promote from here, never runtht memory promote/save-oneyourself (the gate blocks them).
Phase 3 — Rewriting
Prerequisite: Phase 2 closed; the question_rewritten decision is refused before
Phase 3 (CLI exit 5).
- Read
rewriting.md. Produce the rewritten question (population explicit in model terms, each condition as a separate numbered clause, ambiguous terms replaced with the concepts clarified in Phase 1 citing the defining evidence, expected output made explicit). - Present in a single
reviewer_decide(advance:false, allow_other:true). The "Confirm rewriting" option isrecommended:truewith{type:"question_rewritten", subject:"domanda", detail:"<full rewritten question>"}. - Order matters: (a) the
reviewer_deciderecordsquestion_rewritten→ (b) call the gate'srewrite_questiontool, which runstht session set-questionto writequestion.md(regenerates question + an "## Assunzioni" section; never edit it by hand) → (c) close the phase withreviewer_confirm kind:"phase". F3 does NOT auto-advance: thequestion_rewrittendecision alone does not move the phase. - "Altro" iterates (re-propose a new
reviewer_decide). "Torna indietro" reopens F1.
Phase 4 — Schema linking
Prerequisite: Phase 3 closed.
tht schema render --format mschema-textfor the schema context (the catalogartifacts/mschema/physical.yamlis already in the workspace; only if render fails withphysical.yaml non trovato, runtht schema introspectonce, then render). Copy table/column names EXACTLY from it — never invent objects. Also runtht memory solved-search "<question>" --json: similar already-solved questions show which tables comparable questions used. Cite relevant precedents (session id + tables) to the reviewer as CONTEXT — they are reference material, NOT decisions to apply; their filters/periods may not transfer.- Propose tables to promote/exclude with
reviewer_schema_linking: passtables[]as{id, name, kind: "promote"|"exclude", rationale, suggested_columns}. Do NOT list every column yourself — the gate loads the full column set (with descriptions) from the catalog and pre-selects yoursuggested_columns. The reviewer curates the columns per promoted table. The tool recordstable_promoted/table_excluded+column_promoted/column_excludedand re-projectsschema_linking.jsondeterministically viatht session sync-schema-linking(you do NOT hand-write the tables/columns part withwrite_schema_linking). The promoted columns are the reviewer-approved OUTPUT columns: project exactly those in the final SELECT (Phase 6/7); you remain free to reference other columns as join keys or filter predicates when the query requires them. Propose joins separately inreviewer_decide(advance:false), registeringjoin_modified. - Value grounding (D14a). If a cited value (e.g. "ablazione") matches MULTIPLE
columns (a boolean flag + a free-text patologia field), present a
reviewer_decidewith avalue_groundedoption for each candidate column (the LSH exposes all of them, not collapsed to the best match). The reviewer chooses the anchor(s). - Concept formula (D14b). If a concept (e.g. "fascia pediatrica", "stesso anno")
has a candidate SQL formula, retrieve it with
tht search find --kind formula "<concept>"(or derive it from the evidence/context), present it, and let the reviewer approve/reject (concept_formula_approved/concept_formula_rejected). Reflect the approved formula inschema_linking.json(concept_formulas). - Persist the joins (and any
concept_formulas/open_questions) with the gate'swrite_schema_linkingtool — it validates the object against theSchemaLinkingmodel and writes the file deterministically (never hand-write it, never edit it with the file tool; on a validation error the tool returns the exact problem to fix). Shape:{question, candidates:[...], joins:[{from, to, source?}], excluded:[...], open_questions:[], concept_formulas:[]}—candidates/excludedare owned byreviewer_schema_linking/sync-schema-linking(step 2), so if you callwrite_schema_linkingafter step 2, carry over itscandidates/excludedunchanged rather than overwriting them. Then close withreviewer_confirm kind:"phase". Do NOT runtht session check(that's Phase 5).
Phase 5 — Synthesis
Prerequisite: Phase 4 closed; schema_linking.json present.
tht session check(objective gate: decisions present + schema_linking valid).- Summarize the schema-linking to the reviewer; if corrections are needed, reopen Phase 4.
- Close with
reviewer_confirm kind:"phase".
Phase 6 — CTE plan
Prerequisite: Phase 5 closed.
- Read
cte.md. Decompose the rewritten question into CTEs (Agent View Generation): each CTE captures an informative subset with a clear purpose, named in snake_case.tht memory solved-search "<question>" --jsonshows how similar solved questions were structured — use as reference only. - Present the full CTE plan to the reviewer with
reviewer_confirm kind:"cte_plan", passing a structured v2 artifact (artifact:{kind:"cte_plan", data:{…}}). You authorquestion,strategyand eachctes[]entry (name,purpose,rationale,depends_on,tables[].name,keys,filters[]withcolumn/op/value/rationale,output_columns); the gate fillsindex(1-based) and everydescriptionfrom the catalog, derives the ordered--namelist fromdata.ctes[].name, and on approval persists bothcte_plan.jsonand the chain doc (cte_plan_doc.json). Compact example (2 CTE):{"schema_version":2,"question":"pazienti attivi con almeno un ricovero nel 2023", "strategy":"prima la base dei pazienti attivi, poi i loro ricoveri filtrati per anno", "ctes":[ {"name":"base_pazienti","purpose":"pazienti attivi","rationale":"insieme di partenza", "depends_on":[],"tables":[{"name":"dim_patient"}],"keys":["cod_paz"], "filters":[{"column":"dim_patient.flag_attivo","op":"IS","value":"TRUE","rationale":"solo attivi"}], "output_columns":["cod_paz"]}, {"name":"ricoveri_2023","purpose":"ricoveri dei pazienti nel 2023","rationale":"restringe al 2023", "depends_on":["base_pazienti"],"tables":[{"name":"fact_ricoveri"}],"keys":["cod_paz"], "filters":[{"column":"dim_time.year","op":"=","value":"2023","rationale":"finestra temporale"}], "output_columns":["cod_paz","data_ricovero"]} ]} - For each CTE (in plan order): write
sessions/<id>/ctes/<name>.sql(ONLY theWITH ... AS (...)block, NO trailing SELECT), test withtht cte test --session <id> <name>(with an ok outcome), then present it withreviewer_confirm kind:"cte_result". Pass ONLY the thin v2 dataartifact:{kind:"cte_result", data:{schema_version:2, purpose?, rationale?, note?}}— NEVER paste SQL, columns or preview rows as text: the gate reads them deterministically fromtht cte info(the persisted<name>.sql+ the last test record) and builds the full artifact the reviewer approves. The next CTE is testable ONLY after the previous one is approved (CLI exit 5 if out of order). Copy table/column names EXACTLY from the schema context; use values verified withtht search. - After the last CTE is approved, close with
reviewer_confirm kind:"phase".
Phase 7 — Final SQL
Prerequisite: Phase 6 closed.
- Read
sql-generation.md. Recursive divide-and-conquer: the CTEs approved in Phase 6 are the preferred building blocks (reuse them by name).tht memory solved-search "<question>" --jsongives the final SQL of similar solved questions: reference exemplars — never copy filters, periods or populations without checking them against the current rewritten question. - Compose the final SQL (PostgreSQL dialect, exact names from the schema context).
Output columns (F4 honoring). The columns promoted in Phase 4's
schema_linking.jsonare the reviewer-approved OUTPUT columns: project exactly those in the final SELECT. Other schema-linked columns remain usable as join keys or filter predicates, but do not add them to the SELECT list. Time dimension:data_time_keyis the FK todim_time.day_key(NOT declared in the DWH, must be added by hand to the join); useJOIN dim_timeand its columns (dt.year,dt.month, …), NEVER arithmetic on the key. tht sql validate+tht sql preview(max 10 rows). On errors / suspicious results, apply thesql-generation.mdchecklist and correct with the reviewer.tht sql savewritessessions/<id>/sql_final.sql(ONLY clean SQL, no comments). Approve withreviewer_confirm kind:"sql"(recordssql_approved), then advance to Phase 8 withreviewer_confirm kind:"phase"—kind:"sql"alone does NOT advance F7.
Phase 8 — Datamart
Prerequisite: Phase 7 closed.
- Ask the reviewer whether they want a datamart (
reviewer_selectyes/no). - If yes:
tht datamart generate(stub — raises NotImplementedError for now). Tell the reviewer that dbt generation is not implemented yet. - Memory promotion. Call
reviewer_memory_promotewith ONLY the session id: the gate computes the candidates itself (tht memory promote --preview— the 3 reusable types, already excluding promoted/declined ones) and shows the reviewer a pre-selected checklist. Selected → saved to the vectordb +memory_promoted; deselected →memory_promotion_declined(never re-proposed). If the gate reports zero candidates, move on — do not retry. - Close with
reviewer_confirm kind:"phase". The session is finalizable.
Session end
When the workflow is complete (Phase 8), tht session finalize closes the session
and unlocks input. Finalize also indexes the question→SQL pair in the vectordb
(kind solved_question, best-effort — on failure recover with tht memory solved-index <id>). The persisted state (ledger review_decisions.jsonl +
artifacts) is the truth: what is not recorded did not happen.