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/--configis a PER-COMMAND option (append AFTER the subcommand). New CLI commands takeconfig: 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
thtCLI copy). - Run harness tests from
harness/:cd harness && .venv/bin/pytest -q(line-length 100,ruff check .). - Do NOT change
_AUTO_ADVANCE_PHASES,workflow.yamlsemantics, or the F7sql_approvedpath. - 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 anextcommand nearlist_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
nextcommand
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_confirmhandler, thekind === "cte_result" || kind === "sql"branch, ~lines 623-641)
Interfaces:
-
Consumes:
tht cte next --session <id>(Task 1); existingtht(ctx, args),relayIfThtFails,currentPhase,textResult. -
Produces: no new interface — corrects the persisted decision to
cte_approved:<cte name>sophase.approved_ctes/next_cterecognize 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(addimport jsonif absent; addset_schema_linkingnearset_question, ~line 159) - Test:
harness/tests/test_set_schema_linking.py(create)
Interfaces:
-
Consumes:
SchemaLinkingmodel (tht.session.models);load_session,touch_manifest(already instore.py). -
Produces:
set_schema_linking(session_id: str, data: dict, sessions_root: Path) -> Path— validates againstSchemaLinking(raisespydantic.ValidationError), writesschema_linking.jsondeterministically, 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 afterset_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 orValidationError. 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(extendtht()~line 147; add awrite_schema_linkingtool next torewrite_question)
Interfaces:
-
Consumes:
tht session set-schema-linking <id> --file -(Task 4); existingexecFileSync,loadEnvFromDotenv,textResult,lockActive. -
Produces: gate tool
write_schema_linking({session, schema_linking})for the model to call in F4. -
Step 1: Add optional stdin
inputto thetht()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_linkingtool
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_linkingexists). -
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.