Files
ThothII/docs/superpowers/plans/2026-07-01-workflow-contract-hardening.md
T

28 KiB

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 <noreply@anthropic.com> 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 <id> 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:

"""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:

@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
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 <noreply@anthropic.com>"

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 <id> (Task 1); existing tht(ctx, args), relayIfThtFails, currentPhase, textResult.

  • Produces: no new interface — corrects the persisted decision to cte_approved:<cte name> 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):

			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:

			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
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 <noreply@anthropic.com>"

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:

"""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:

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
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 <noreply@anthropic.com>"

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 <id> --file <path|-> — 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:

"""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:

@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
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 <noreply@anthropic.com>"

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 <id> --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:

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):

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:

	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
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 <noreply@anthropic.com>"

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:

## 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):

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).):

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.):

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:"<full rewritten question>"}`.
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).):

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).):

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
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 <noreply@anthropic.com>"

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 <id> 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.