diff --git a/docs/superpowers/plans/2026-07-01-workflow-contract-hardening.md b/docs/superpowers/plans/2026-07-01-workflow-contract-hardening.md new file mode 100644 index 00000000..ffc0134c --- /dev/null +++ b/docs/superpowers/plans/2026-07-01-workflow-contract-hardening.md @@ -0,0 +1,721 @@ +# Workflow Contract Hardening Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix the F6 CTE-approval dead-end, add a `tht`-mediated `schema_linking.json` writer/validator, and correct `SKILL.md` so it matches the real phase-advance contract. + +**Architecture:** Three coordinated harness changes (Pi gate JS + `tht` Python CLI + SKILL doc). All logic lives behind the `tht` CLI (unit-tested with pytest); the gate is thin glue that shells the CLI. Build order 2→3→1 so the SKILL documents the final contract. + +**Tech Stack:** Python 3.13 + Typer + pydantic + pytest (`typer.testing.CliRunner`); Node ESM Pi extension (`execFileSync`). + +## Global Constraints + +- Spec: `docs/superpowers/specs/2026-07-01-workflow-contract-hardening-design.md`. +- Work on branch `feat/workflow-contract-hardening` (already checked out). +- `tht` `-c/--config` is a PER-COMMAND option (append AFTER the subcommand). New CLI commands take `config: Path = CONFIG_OPT`. +- `--json` / machine-read stdout must be pristine (only the intended value). +- UI strings English; document CONTENT and CLI user messages stay Italian (matches existing `tht` CLI copy). +- Run harness tests from `harness/`: `cd harness && .venv/bin/pytest -q` (line-length 100, `ruff check .`). +- Do NOT change `_AUTO_ADVANCE_PHASES`, `workflow.yaml` semantics, or the F7 `sql_approved` path. +- Commit messages end with the `Co-Authored-By: Claude Opus 4.8 ` trailer. + +--- + +### Task 1: `tht cte next` command (Part 2 — CLI primitive for the F6 fix) + +**Files:** +- Modify: `harness/tht/cli/cte_cmd.py` (add a `next` command near `list_cmd`, ~line 153) +- Test: `harness/tests/test_cte_next.py` (create) + +**Interfaces:** +- Consumes: `tht.phase.next_cte(session_dir) -> str | None`; `cte_cmd._load_config_or_exit`, `load_session_or_exit`, `session_dir` (already imported in the module). +- Produces: `tht cte next --session ` prints the first unapproved CTE name on stdout (nothing when none) — consumed by the gate in Task 2. + +- [ ] **Step 1: Write the failing test** + +Create `harness/tests/test_cte_next.py`: + +```python +"""L1: `tht cte next` — the first plan CTE not yet approved (drives the F6 gate).""" +import json + +from typer.testing import CliRunner + +from tht.cli.cte_cmd import cte_app +from tht.config import DatabaseConfig +from tht.decisions import append_decision +from tht.session.store import create_session + + +def _db(): + return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106 + + +def _patch_cfg(monkeypatch, tmp_path): + from tht.cli import cte_cmd + + class FakePaths: + sessions = tmp_path + + class FakeCfg: + paths = FakePaths() + database = _db() + + monkeypatch.setattr(cte_cmd, "_load_config_or_exit", lambda _: FakeCfg()) + + +def test_cte_next_first_unapproved(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + (tmp_path / m.id / "cte_plan.json").write_text(json.dumps(["a", "b", "c"])) + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(cte_app, ["next", "--session", m.id]) + assert res.exit_code == 0, res.output + assert res.output.strip() == "a" + + +def test_cte_next_after_one_approval(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + (tmp_path / m.id / "cte_plan.json").write_text(json.dumps(["a", "b", "c"])) + append_decision(tmp_path / m.id, type="cte_approved", subject="a") + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(cte_app, ["next", "--session", m.id]) + assert res.exit_code == 0, res.output + assert res.output.strip() == "b" + + +def test_cte_next_empty_when_all_approved(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + (tmp_path / m.id / "cte_plan.json").write_text(json.dumps(["a"])) + append_decision(tmp_path / m.id, type="cte_approved", subject="a") + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(cte_app, ["next", "--session", m.id]) + assert res.exit_code == 0, res.output + assert res.output.strip() == "" +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd harness && .venv/bin/pytest tests/test_cte_next.py -q` +Expected: FAIL — `No such command 'next'` (exit_code != 0). + +- [ ] **Step 3: Add the `next` command** + +In `harness/tht/cli/cte_cmd.py`, immediately after the `@cte_app.command("list")` function (`list_cmd`), add: + +```python +@cte_app.command("next") +def next_cmd( + session: str = typer.Option(..., "--session"), + config: Path = CONFIG_OPT, +) -> None: + """Primo CTE del piano non ancora approvato (stdout pulito; vuoto se nessuno). + + Sorgente unica dell'ordine di approvazione CTE (F6): il gate lo usa per + registrare cte_approved sul NOME del CTE (non su 'phase:6').""" + from tht.phase import next_cte + + cfg = _load_config_or_exit(config) + load_session_or_exit(cfg, session) + nxt = next_cte(session_dir(cfg, session)) + if nxt: + typer.echo(nxt) +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd harness && .venv/bin/pytest tests/test_cte_next.py -q` +Expected: PASS (3 passed). + +- [ ] **Step 5: Lint + commit** + +```bash +cd harness && .venv/bin/ruff check tht/cli/cte_cmd.py tests/test_cte_next.py +git add harness/tht/cli/cte_cmd.py harness/tests/test_cte_next.py +git commit -m "feat(cte): add 'tht cte next' — first unapproved plan CTE + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 2: Gate approves each CTE by name (Part 2 — the bug fix) + +**Files:** +- Modify: `harness/.pi/extensions/tht-gate.js` (`reviewer_confirm` handler, the `kind === "cte_result" || kind === "sql"` branch, ~lines 623-641) + +**Interfaces:** +- Consumes: `tht cte next --session ` (Task 1); existing `tht(ctx, args)`, `relayIfThtFails`, `currentPhase`, `textResult`. +- Produces: no new interface — corrects the persisted decision to `cte_approved:` so `phase.approved_ctes`/`next_cte` recognize it and F6 can close. + +- [ ] **Step 1: Replace the combined cte_result/sql branch** + +In `harness/.pi/extensions/tht-gate.js`, find (inside the `reviewer_confirm` tool's `execute`, after the `kind === "cte_plan"` block): + +```javascript + if (kind === "cte_result" || kind === "sql") { + const dt = kind === "sql" ? "sql_approved" : "cte_approved"; + const err = relayIfThtFails( + ctx, + [ + "decision", + "add", + "--session", + session, + "--type", + dt, + "--subject", + `phase:${currentPhase(ctx, session)}`, + ], + "", + ); + if (err) return err; + return textResult(`${kind} approvato (sessione ${session}).`); + } +``` + +Replace it with: + +```javascript + if (kind === "cte_result") { + // The CTE under review is always next_cte (plan order is enforced by + // `tht cte test`). Approve it BY NAME: `cte_approved` keys on the CTE + // name (phase.approved_ctes / next_cte); a `phase:N` subject is rejected + // by `decision add` (exit 5) and never satisfies F6's advance prereq. + const cteName = tht(ctx, [ + "cte", + "next", + "--session", + session, + ]).trim(); + if (!cteName) { + return textResult( + `Nessun CTE in attesa di approvazione (sessione ${session}).`, + ); + } + const err = relayIfThtFails( + ctx, + ["decision", "add", "--session", session, "--type", "cte_approved", "--subject", cteName], + "", + ); + if (err) return err; + return textResult(`CTE '${cteName}' approvato (sessione ${session}).`); + } + if (kind === "sql") { + const err = relayIfThtFails( + ctx, + [ + "decision", + "add", + "--session", + session, + "--type", + "sql_approved", + "--subject", + `phase:${currentPhase(ctx, session)}`, + ], + "", + ); + if (err) return err; + return textResult(`SQL approvato (sessione ${session}).`); + } +``` + +- [ ] **Step 2: Verify the gate JS still parses and its unit suite is green** + +Run: `cd harness && node --check .pi/extensions/tht-gate.js && node --test .pi/extensions/gate/__tests__/` +Expected: no syntax error; existing gate tests PASS (this branch is glue — its behavior is guaranteed by Task 1's `cte next` tests + the deferred live F6 check per the spec's Risks). + +- [ ] **Step 3: Commit** + +```bash +git add harness/.pi/extensions/tht-gate.js +git commit -m "fix(gate): F6 approves each CTE by name, not 'phase:6' + +reviewer_confirm kind:'cte_result' registered cte_approved --subject +phase:6, which decision_cmd rejects (exit 5) and next_cte never +recognizes — dead-ending F6. Derive the CTE name from 'tht cte next' +(plan-order single source of truth) and approve by name. sql path +(sql_approved:phase:N) unchanged. + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 3: `store.set_schema_linking` (Part 3 — validate + write the artifact) + +**Files:** +- Modify: `harness/tht/session/store.py` (add `import json` if absent; add `set_schema_linking` near `set_question`, ~line 159) +- Test: `harness/tests/test_set_schema_linking.py` (create) + +**Interfaces:** +- Consumes: `SchemaLinking` model (`tht.session.models`); `load_session`, `touch_manifest` (already in `store.py`). +- Produces: `set_schema_linking(session_id: str, data: dict, sessions_root: Path) -> Path` — validates against `SchemaLinking` (raises `pydantic.ValidationError`), writes `schema_linking.json` deterministically, returns the path. Consumed by Task 4. + +- [ ] **Step 1: Write the failing test** + +Create `harness/tests/test_set_schema_linking.py`: + +```python +"""L1: store.set_schema_linking — validate against SchemaLinking, then write.""" +import json + +import pytest +from pydantic import ValidationError + +from tht.config import DatabaseConfig +from tht.session.models import SchemaLinking +from tht.session.store import create_session, set_schema_linking + + +def _db(): + return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106 + + +def test_writes_and_revalidates(tmp_path): + m = create_session("q", _db(), tmp_path) + data = { + "question": "q riscritta", + "candidates": [{"kind": "table", "name": "fact_x", "decision": "promoted"}], + "joins": [{"from": "a.k", "to": "b.k"}], + } + path = set_schema_linking(m.id, data, tmp_path) + assert path.exists() + reloaded = json.loads(path.read_text()) + assert reloaded["candidates"][0]["name"] == "fact_x" + assert reloaded["joins"][0]["from"] == "a.k" # 'from' alias round-trips + SchemaLinking.model_validate(reloaded) # re-validates clean + + +def test_rejects_invalid_and_writes_nothing(tmp_path): + m = create_session("q", _db(), tmp_path) + with pytest.raises(ValidationError): + set_schema_linking(m.id, {"question": "q", "bogus": 1}, tmp_path) # extra=forbid + assert not (tmp_path / m.id / "schema_linking.json").exists() +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd harness && .venv/bin/pytest tests/test_set_schema_linking.py -q` +Expected: FAIL — `ImportError: cannot import name 'set_schema_linking'`. + +- [ ] **Step 3: Implement `set_schema_linking`** + +In `harness/tht/session/store.py`: ensure `import json` is present near the top imports (add it if missing). Then add, right after the `set_question` function: + +```python +def set_schema_linking( + session_id: str, + data: dict, + sessions_root: Path, +) -> Path: + """Valida e scrive schema_linking.json (Fase 4) in modo deterministico. + + Valida `data` contro il modello SchemaLinking (ValidationError se invalido) PRIMA + di scrivere, così un artefatto malformato non tocca mai il disco. Ritorna il path. + """ + from tht.session.models import SchemaLinking + + load_session(session_id, sessions_root) + model = SchemaLinking.model_validate(data) + path = sessions_root / session_id / "schema_linking.json" + path.write_text( + json.dumps(model.model_dump(by_alias=True), indent=2, ensure_ascii=False) + ) + touch_manifest(session_id, sessions_root) + return path +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd harness && .venv/bin/pytest tests/test_set_schema_linking.py -q` +Expected: PASS (2 passed). + +- [ ] **Step 5: Lint + commit** + +```bash +cd harness && .venv/bin/ruff check tht/session/store.py tests/test_set_schema_linking.py +git add harness/tht/session/store.py harness/tests/test_set_schema_linking.py +git commit -m "feat(store): set_schema_linking validates then writes the F4 artifact + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 4: `tht session set-schema-linking` CLI (Part 3 — CLI surface) + +**Files:** +- Modify: `harness/tht/cli/session_cmd.py` (add command after `set_question_cmd`, ~line 111) +- Test: `harness/tests/test_set_schema_linking_cli.py` (create) + +**Interfaces:** +- Consumes: `store.set_schema_linking` (Task 3); `_load_config_or_exit`, `load_session_or_exit`, `CONFIG_OPT` (already in module). +- Produces: `tht session set-schema-linking --file ` — reads JSON (file or stdin `-`), validates+writes, exit 5 on bad JSON or `ValidationError`. Consumed by the gate in Task 5. + +- [ ] **Step 1: Write the failing test** + +Create `harness/tests/test_set_schema_linking_cli.py`: + +```python +"""L1: `tht session set-schema-linking` — stdin/file JSON → validate → write.""" +import json + +from typer.testing import CliRunner + +from tht.cli.session_cmd import session_app +from tht.config import DatabaseConfig +from tht.session.store import create_session + + +def _db(): + return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106 + + +def _patch_cfg(monkeypatch, tmp_path): + from tht.cli import session_cmd + + class FakePaths: + sessions = tmp_path + + class FakeCfg: + paths = FakePaths() + database = _db() + + monkeypatch.setattr(session_cmd, "_load_config_or_exit", lambda _: FakeCfg()) + + +_VALID = json.dumps({ + "question": "q", + "candidates": [{"kind": "table", "name": "fact_x", "decision": "promoted"}], +}) + + +def test_cli_stdin_writes(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(session_app, ["set-schema-linking", m.id, "--file", "-"], input=_VALID) + assert res.exit_code == 0, res.output + assert (tmp_path / m.id / "schema_linking.json").exists() + + +def test_cli_file_writes(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + p = tmp_path / "sl.json" + p.write_text(_VALID) + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(session_app, ["set-schema-linking", m.id, "--file", str(p)]) + assert res.exit_code == 0, res.output + assert (tmp_path / m.id / "schema_linking.json").exists() + + +def test_cli_invalid_json_exit5(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(session_app, ["set-schema-linking", m.id, "--file", "-"], input="{not json") + assert res.exit_code == 5 + assert not (tmp_path / m.id / "schema_linking.json").exists() + + +def test_cli_invalid_model_exit5(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke( + session_app, ["set-schema-linking", m.id, "--file", "-"], input='{"question":"q","bogus":1}' + ) + assert res.exit_code == 5 + assert not (tmp_path / m.id / "schema_linking.json").exists() +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd harness && .venv/bin/pytest tests/test_set_schema_linking_cli.py -q` +Expected: FAIL — `No such command 'set-schema-linking'`. + +- [ ] **Step 3: Add the command** + +In `harness/tht/cli/session_cmd.py`, right after `set_question_cmd` (before the `@session_app.command("show")` block), add: + +```python +@session_app.command("set-schema-linking") +def set_schema_linking_cmd( + session_id: str = typer.Argument(...), + file: str = typer.Option( + ..., "--file", "-f", + help="Path al JSON dello schema-linking, oppure '-' per leggere da stdin."), + config: Path = CONFIG_OPT, +) -> None: + """Valida (modello SchemaLinking) e scrive schema_linking.json deterministicamente.""" + import sys + + from pydantic import ValidationError + + from tht.session.store import set_schema_linking + + cfg = _load_config_or_exit(config) + load_session_or_exit(cfg, session_id) + raw = sys.stdin.read() if file == "-" else Path(file).read_text() + try: + data = json.loads(raw) + except json.JSONDecodeError as e: + typer.secho(f"ERRORE: JSON non valido: {e}", fg=typer.colors.RED, err=True) + raise typer.Exit(code=5) + try: + path = set_schema_linking(session_id, data, cfg.paths.sessions) + except ValidationError as e: + typer.secho(f"ERRORE: schema_linking non valido:\n{e}", fg=typer.colors.RED, err=True) + raise typer.Exit(code=5) + typer.secho(f"OK: schema_linking.json aggiornato ({path}).", fg=typer.colors.GREEN) +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd harness && .venv/bin/pytest tests/test_set_schema_linking_cli.py -q` +Expected: PASS (4 passed). + +- [ ] **Step 5: Lint + commit** + +```bash +cd harness && .venv/bin/ruff check tht/cli/session_cmd.py tests/test_set_schema_linking_cli.py +git add harness/tht/cli/session_cmd.py harness/tests/test_set_schema_linking_cli.py +git commit -m "feat(cli): 'tht session set-schema-linking' (file/stdin, validated) + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 5: Gate `write_schema_linking` tool + stdin support in `tht()` (Part 3 — gate glue) + +**Files:** +- Modify: `harness/.pi/extensions/tht-gate.js` (extend `tht()` ~line 147; add a `write_schema_linking` tool next to `rewrite_question`) + +**Interfaces:** +- Consumes: `tht session set-schema-linking --file -` (Task 4); existing `execFileSync`, `loadEnvFromDotenv`, `textResult`, `lockActive`. +- Produces: gate tool `write_schema_linking({session, schema_linking})` for the model to call in F4. + +- [ ] **Step 1: Add optional stdin `input` to the `tht()` helper** + +In `harness/.pi/extensions/tht-gate.js`, find: + +```javascript +function tht(ctx, args) { + loadEnvFromDotenv(ctx); + return execFileSync("tht", args, { cwd: ctx.cwd, encoding: "utf8" }); +} +``` + +Replace with (optional third arg; existing 2-arg callers pass `input: undefined` = no stdin): + +```javascript +function tht(ctx, args, input) { + loadEnvFromDotenv(ctx); + return execFileSync("tht", args, { cwd: ctx.cwd, encoding: "utf8", input }); +} +``` + +- [ ] **Step 2: Register the `write_schema_linking` tool** + +In `harness/.pi/extensions/tht-gate.js`, immediately after the `rewrite_question` tool registration block (`pi.registerTool({ name: "rewrite_question", ... });`), add: + +```javascript + pi.registerTool({ + name: "write_schema_linking", + label: "Scrittura schema_linking.json (validata)", + description: + "Scrive deterministicamente schema_linking.json validandolo contro il modello " + + "SchemaLinking via tht session set-schema-linking (evita edit a mano e la " + + "validazione manuale). schema_linking e' l'oggetto JSON completo: {question, " + + "candidates:[{kind:'table'|'column', name, evidence?, decision?}], joins:[{from, " + + "to, source?}], excluded:[{kind, name}], open_questions:[], concept_formulas:[]}.", + parameters: Type.Object({ + session: Type.String(), + schema_linking: Type.Any(), + }), + async execute(_id, params, _signal, _onUpdate, ctx) { + lockActive = true; + const { session, schema_linking } = params; + try { + tht( + ctx, + ["session", "set-schema-linking", session, "--file", "-"], + JSON.stringify(schema_linking), + ); + return textResult( + `schema_linking.json scritto e validato per la sessione ${session}.`, + ); + } catch (e) { + const cliMsg = (e.stderr || e.message || String(e)).toString().trim(); + return textResult(`${cliMsg} Correggi schema_linking e riprova.`); + } + }, + }); +``` + +- [ ] **Step 3: Verify the gate JS parses and its unit suite is green** + +Run: `cd harness && node --check .pi/extensions/tht-gate.js && node --test .pi/extensions/gate/__tests__/` +Expected: no syntax error; existing gate tests PASS (the tool is glue over the Task 4 CLI, which is unit-tested; live F4 check deferred per the spec). + +- [ ] **Step 4: Commit** + +```bash +git add harness/.pi/extensions/tht-gate.js +git commit -m "feat(gate): write_schema_linking tool (validated F4 artifact via CLI) + +tht() gains an optional stdin arg; the tool pipes the schema-linking +object to 'tht session set-schema-linking --file -', which validates +against SchemaLinking and returns the exact error on failure — so the +model stops hand-writing the artifact and validating with ad-hoc python. + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 6: `SKILL.md` — correct the advance contract + cheat-sheet (Part 1) + +**Files:** +- Modify: `harness/.pi/skills/tht-sessione/SKILL.md` + +**Interfaces:** +- Consumes: the behavior established in Tasks 1-5 (F6 approves by name; `write_schema_linking` exists). +- Produces: documentation only — no code depends on it. + +- [ ] **Step 1: Add the phase cheat-sheet** + +After the "Language contract" paragraph (the block ending `...ask the reviewer.`, ~line 24) and before `## Disciplines`, insert: + +```markdown +## 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"` (or `kind:"sql"` for F7). 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 `write_schema_linking`) | +| 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` | `reviewer_confirm kind:"sql"` | +| F8 datamart | — | `reviewer_confirm kind:"phase"` | +``` + +- [ ] **Step 2: Fix Discipline 2** + +Replace the Discipline 2 block (the `2. **The choice is the confirmation.** ... never as a redundant echo of a recorded choice.` paragraph): + +```markdown +2. **The choice records; the phase gate advances.** A `reviewer_decide`, or a + `reviewer_select` whose chosen option carries a `decision`, PERSISTS that decision — it + does NOT by itself advance the phase. To move to the next phase you MUST issue + `reviewer_confirm kind:"phase"` (F7 uses `kind:"sql"`), the deliberate "this phase is + done" gate. The `advance:true` flag on `reviewer_decide` is 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 a `reviewer_confirm` + that merely echoes a decision already recorded by a choice — the phase gate is a separate, + deliberate step, not an echo of a decision. +``` + +- [ ] **Step 3: Fix Phase 1 step 4 (remove the mis-placed `rewrite_question`)** + +Replace Phase 1 step 4 (`4. After the phase advance, update the question with the gate's rewrite_question ... never edit question.md by hand).`): + +```markdown +4. Closing Phase 1 advances to Phase 2 (Memories). The question is rewritten later, in + Phase 3 — do NOT call `rewrite_question` here. +``` + +- [ ] **Step 4: Fix Phase 3 steps 2-3 (F3 does NOT auto-advance)** + +Replace Phase 3 steps 2 and 3 (`2. Present in **a single** reviewer_decide(advance:true, allow_other:true). ...` through `... Never edit/write question.md manually.`): + +```markdown +2. Present in **a single** `reviewer_decide(advance:false, allow_other:true)`. The + "Confirm rewriting" option is `recommended:true` with + `{type:"question_rewritten", subject:"domanda", detail:""}`. +3. **Order matters:** (a) the `reviewer_decide` records `question_rewritten` → (b) call the + gate's `rewrite_question` tool, which runs `tht session set-question` to write + `question.md` (regenerates question + an "## Assunzioni" section; never edit it by hand) + → (c) close the phase with `reviewer_confirm kind:"phase"`. F3 does NOT auto-advance: + the `question_rewritten` decision alone does not move the phase. +``` + +- [ ] **Step 5: Fix Phase 4 (advance:false + use `write_schema_linking`)** + +In Phase 4 step 2, change `reviewer_decide(advance:true)` to `reviewer_decide(advance:false)`. + +Then replace Phase 4 step 5 (`5. Write schema_linking.json (the Phase 4 artifact) and close with reviewer_confirm kind:"phase". Do NOT run tht session check (that's Phase 5).`): + +```markdown +5. Persist `schema_linking.json` with the gate's `write_schema_linking` tool — it validates + the object against the `SchemaLinking` model 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:[{kind:"table"|"column", name, + evidence?, decision?:"promoted"|"excluded"|"pending"}], joins:[{from, to, source?}], + excluded:[{kind, name}], open_questions:[], concept_formulas:[]}`. Then close with + `reviewer_confirm kind:"phase"`. Do NOT run `tht session check` (that's Phase 5). +``` + +- [ ] **Step 6: Clarify Phase 2 close (substantive memory needs the gate)** + +In Phase 2, replace step 4 (`4. If no memory clears score 0.5, say so and close the phase quickly (reviewer_confirm kind:"phase" if the list is empty).`): + +```markdown +4. Closing: if memories were applied or rejected (substantive decisions), `advance:true` + no-ops — close with `reviewer_confirm kind:"phase"`. Only a truly empty memory phase + (nothing applied, nothing rejected) auto-advances via `advance:true`. +``` + +- [ ] **Step 7: Verify the edits read consistently** + +Run: `cd harness && grep -n "advance:true\|reviewer_confirm kind:\"phase\"\|write_schema_linking\|Phase map" .pi/skills/tht-sessione/SKILL.md` +Expected: the cheat-sheet section present; Phase 3 and Phase 4 now reference `reviewer_confirm kind:"phase"`; no remaining claim that a `reviewer_decide` "already advances" outside F2/F6. Read the four edited sections once to confirm no dangling references. + +- [ ] **Step 8: Commit** + +```bash +git add harness/.pi/skills/tht-sessione/SKILL.md +git commit -m "docs(skill): correct phase-advance contract + add phase cheat-sheet + +F3/F4 close with reviewer_confirm kind:'phase' (they do NOT auto-advance); +advance:true only auto-advances F2-empty/F6-skip; rewrite_question belongs +to F3 not F1; F4 uses the new write_schema_linking tool with the documented +SchemaLinking shape. Adds a per-phase artifact/close cheat-sheet. + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 7: Full-suite verification + branch wrap-up + +**Files:** none (verification only). + +- [ ] **Step 1: Run the whole harness suite + lint** + +Run: `cd harness && .venv/bin/pytest -q && .venv/bin/ruff check .` +Expected: all green (prior 269 + the new `cte next` (3) + `set_schema_linking` store (2) + CLI (4) tests), ruff clean. If a pre-existing L2/L0 test skips (no VPN/Docker), that's expected — only unexpected failures block. + +- [ ] **Step 2: Run the gate JS suite** + +Run: `cd harness && node --test .pi/extensions/gate/__tests__/` +Expected: existing gate unit suite green (no regressions from Tasks 2 and 5). + +- [ ] **Step 3: Report status** + +Summarize what shipped, and flag the DEFERRED live verification (needs VPN): drive the real stack through F4 (`write_schema_linking`) and F6 (approve a CTE → F6 closes) end-to-end. Note this closes the F6 dead-end found in session `2026-06-30-165708`. Do NOT claim live-verified — say "unit-verified; live check pending VPN". + +## Self-Review + +**Spec coverage:** Part 2 → Tasks 1-2 (cte next + gate by-name). Part 3 → Tasks 3-5 (store + CLI + gate tool). Part 1 → Task 6 (F3, Discipline 2, rewrite_question placement, F4 tool + shape, cheat-sheet, F2 close). Verification → Task 7. All spec sections mapped. + +**Placeholder scan:** every code/test step contains full code; every run step has an exact command + expected result. No TBD/TODO. + +**Type consistency:** `set_schema_linking(session_id, data, sessions_root) -> Path` defined in Task 3, consumed identically in Task 4. `tht cte next --session ` produced in Task 1, consumed in Task 2. `tht()` third arg `input` added in Task 5 Step 1 before first use in Step 2. Gate tool params `{session, schema_linking}` match the CLI `--file -` stdin contract.