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