Design doc for three coordinated harness fixes derived from the 2026-06-30-165708 session analysis: correct SKILL.md phase-advance contract, fix the F6 cte_approved subject bug, and add a tht-mediated schema_linking.json writer/validator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.4 KiB
Workflow contract hardening — design
Date: 2026-07-01 · Status: approved (design) · Scope: harness only (Pi gate +
thtCLI + SKILL.md)
Problem
Analysis of the last live session (2026-06-30-165708, GLM 5.2 — Pi transcript
~/.pi/agent/sessions/--Users-mp-projects-ThothII-harness--/2026-06-30T16-57-09-634Z_*.jsonl)
showed the model spending 63 of 77 tool calls on exploration / reverse-engineering the
harness (reading tht-gate.js 3×, phase.py, decision_cmd.py, session/models.py; ~10
--help probes) instead of executing the workflow. Three concrete defects drive this, all
verified against source:
-
SKILL.mdmisdescribes phase advancement. Ground truth:tht phase advance(no--auto) writesphase_approvedunconditionally (phase_cmd.py:99-105);reviewer_decide(advance:true)callsadvance --auto, which advances only whenauto_advance_eligible— phase ∈{2,6}and zero substantive decisions (phase.py:_AUTO_ADVANCE_PHASES,auto_advance_eligible). So every phase that records real decisions (F1, F3, F4, F5, F6-with-CTEs, F7, F8) closes only viareviewer_confirm kind:"phase";advance:trueeffectively auto-advances just F2-empty / F6-skipped. ButSKILL.mdPhase 3 (lines 170-171) says "the reviewer_decide already advances" and tells the model NOT to add a phase gate — false. This is the exact cause of the model's thrash at transcript turns 30-38 (question_rewritten recorded, phase stayed at 3). -
F6 CTE approval is broken (dead-ends the workflow). The gate's
reviewer_confirm kind:"cte_result"registerscte_approved --subject phase:6(tht-gate.js:623-640), butdecision_cmd.py:49-70rejectscte_approvedunlesssubjectis a CTE name in the persisted plan (approved_ctes/next_ctekey on the CTE name) → exit 5 every time. The last session died here (transcript turn 63). -
schema_linking.jsonhas no writer. In F4 the model hand-writes the artifact against the internalSchemaLinkingpydantic model and validates it with ad-hocpython -cone-liners (transcript turns 45-52, including a venv-vs-system-python misfire) because there is nothtcommand to persist/validate it — unlikequestion.md(set-question).
Goals
- Make the F6 CTE-approval path work end-to-end.
- Give the model a
tht-mediated way to persist+validateschema_linking.json. - Correct
SKILL.mdso it matches the real advance contract, and give the model a per-phase cheat-sheet so it stops discovering the CLI at runtime.
Non-goals
- No rewrite of the working 8-phase workflow,
phase.py, orworkflow.yamlsemantics. - No change to
_AUTO_ADVANCE_PHASESor the auto-advance eligibility rule (it is intentional: only trivially-empty phases auto-advance). - Not touching the F7
sqlpath (sql_approved --subject phase:7is correct — no in-plan check).
Part 2 — Fix F6 CTE approval (chosen approach: A2, gate derives next_cte)
The CTE being approved by reviewer_confirm kind:"cte_result" is always next_cte (the
first unapproved CTE in plan order) — tht cte test <name> already enforces name == next_cte
(cte_cmd.py:44-54). So the gate can derive the name authoritatively instead of trusting a
model-supplied value.
Changes
harness/tht/cli/cte_cmd.py: addtht cte next --session <id>→ prints the barenext_ctename on stdout (empty output when all CTEs are approved / no plan). Pristine stdout (machine contract). Reusestht.phase.next_cte.harness/.pi/extensions/tht-gate.js,reviewer_confirmkind:"cte_result"branch: replace--subject phase:${currentPhase}with the name fromtht cte next --session <id>; registerdecision add --type cte_approved --subject <name>. Ifcte nextis empty, return a textResult explaining there is no CTE awaiting approval (no-op, should not occur in the normal plan-order flow).kind:"sql"branch unchanged.
Tests
- pytest
tests/test_cte_next.py(new):cte nextreturns the first unapproved CTE, then the next after an approval, then empty when all approved. RED→GREEN. - pytest contract test (extend existing decision/cte test):
decision add --type cte_approved --subject <valid CTE name>succeeds andnext_cteadvances;--subject phase:6exits 5 (documents the regression the gate previously hit). - The gate
execute()glue is L2/live-verified by repo convention (tht-gate.js:21-24); the behavioral guarantee is carried by the two pytest tests above + a deferred live F6 check.
Part 3 — schema_linking.json writer/validator
Mirror the question.md pattern (rewrite_question gate tool → tht session set-question →
store.set_question).
SchemaLinking shape to document + validate (session/models.py, extra=forbid):
question:str, candidates:[{kind:"table"|"column", name, evidence?:[str], decision?:"promoted"|"excluded"|"pending", signals?:{}, grounded_values?:[{}]}],
joins:[{from, to, source?, decision?}] (JSON key is from, aliased),
excluded:[{kind, name}], open_questions:[str], concept_formulas:[{}].
Changes
harness/tht/session/store.py: addset_schema_linking(session_id, data: dict, sessions_path)→SchemaLinking.model_validate(data)(raises on invalid), then writesschema_linking.jsondeterministically (json.dumps(model.model_dump(by_alias=True), indent=2, ensure_ascii=False)).harness/tht/cli/session_cmd.py: addtht session set-schema-linking <id> --file <path>(accepts-for stdin). Reads JSON, callsset_schema_linking; onValidationErrorprints the pydantic error to stderr and exits 5 (so the model gets a precise, actionable message); on success printsOK: schema_linking.json aggiornato (...).harness/.pi/extensions/tht-gate.js: add awrite_schema_linkingtool (parallel torewrite_question): params{session, schema_linking: object}; passes the JSON totht session set-schema-linking <id> --file -viaexecFileSyncinput(stdin, no temp file); relays validation errors back to the model viarelayIfThtFails.
Tests
- pytest
tests/test_set_schema_linking.py(new): valid dict → file written + re-validates; invalid (extra key / badkind) →ValidationError/ CLI exit 5, file not written;fromalias round-trips. RED→GREEN.
Part 1 — SKILL.md surgical corrections (done last, reflects Parts 2-3)
Targeted edits, not a rewrite:
- Phase 3: close with
reviewer_confirm kind:"phase"; delete the "the reviewer_decide already advances" claim; set the rewritingreviewer_decidetoadvance:false. Keep the order (decide recordsquestion_rewritten→rewrite_questionwritesquestion.md). - Discipline 2: restate the advance rule accurately — every substantive phase closes with
reviewer_confirm kind:"phase"(F7:kind:"sql");advance:trueauto-advances ONLY F2 when memory recorded nothing and F6 when skipped/empty; never rely on it elsewhere. - Phase 1 step 4 vs Phase 3: disambiguate where
rewrite_questionis invoked (it belongs to the Phase 3 flow afterquestion_rewritten; the Phase 1 mention is removed/redirected). - Phase 4: replace "write
schema_linking.json" with "callwrite_schema_linking"; add the documentedSchemaLinkingshape so the model does not readsession/models.py. - Phase 6: keep "approve each CTE in plan order via
reviewer_confirm kind:"cte_result""; no signature change (A2 derives the name), close F6 withreviewer_confirm kind:"phase". - Add a top cheat-sheet table: one row per phase → {artifact out, closing gate}. This is the state-machine at a glance that the model currently lacks.
Build order & testing
Order: Part 2 → Part 3 → Part 1 (SKILL documents the final contract). TDD on Parts 2-3
(pytest RED→GREEN; run cd harness && .venv/bin/pytest -q and the gate suite
node --test under .pi/extensions/gate/__tests__ where applicable). Part 1 is documentation;
verify by re-reading against the corrected contract. Full live F6/F4 verification through the
real stack is deferred (needs VPN — see PROJECT_STATE open items).
Risks
- Gate glue is not unit-tested (repo convention): the F6 and
write_schema_linkingwiring intht-gate.jsrely on pytest CLI contracts + a deferred live check. Mitigation: keep the gate changes minimal and push all logic behind the CLI (which IS tested). --file -/ stdin handling in the CLI must be covered by a test (empty stdin, invalid JSON) so the gate path can't hang or write a partial file.