fix(harness): remediation difetti review — gate↔CLI, D15, D7/D6, D14, robustezza

Implementazione del piano di remediation progressiva sui difetti emersi
dall'analisi dell'harness. Tutto verificato: 214 test Python (incl. L0 su
Postgres reale), 14 test JS del gate, ruff pulito.

Blocco 1 (CRITICA, integrazione gate↔CLI):
- phase advance: gate usa --auto + exit 6; reviewer_confirm kind:phase fa
  advance esplicito che applica i prerequisiti (prima non avanzava per le
  fasi a conferma umana).
- cte plan riceve i --name dal gate (param names); set-question con id
  posizionale; skill `tht search find`; nuovo comando `tht memory save-one`
  con dedup hash client-side in save_one_memory.

Blocco 2 (D15, stato post-rollback):
- campo `phase` su DecisionRecord + effective_decisions phase-aware per i
  subject "a nome" (cte_approved ecc.); _compute_promotions e finalize sulla
  vista effective; finalize confronta col piano CTE effettivo, non glob;
  `decision add --retracts` + comando `decision retract`.

Blocco 3 (D7 read-only + D6 manifest):
- assert_read_only su tutti e quattro i codepath (direct + REST);
- manifest author/summary/updated_at/updated_by/schema_version popolati +
  helper touch_manifest sulle mutazioni.

Blocco 4-5 (D14a/D14b):
- decision_min_phase data-driven via `emits:` in workflow.yaml;
- formula evidence: status auto, search_formulas, gruppo CLI `tht formula`,
  `search find --kind formula`, load_evidence_dir salta i .sql.md.

Blocco 6 (robustezza):
- taskdoc slice promoted_tables + bound enforced; report escaping/bound +
  rsplit note; filtro kind reader REST/direct; conteggio upserted robusto;
  guard REST run_query non-list; LSH disallineato -> LshIndexError.

Blocco 7 (pulizia):
- dead code gate e KIND_TO_TABLE morto rimossi; doc Postgres-only
  (README + connection.py).

Blocco 0 (parziale): test di compatibilità firma gate↔CLI
(tests/integration). Rinviati: fake-Pi runtime completo, artifact-gate da
disco (#23), parità eligibility REST/direct (#28), unificazione
reserved-labels (#30), memory_rejected da deselezione (#33).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 17:16:51 +02:00
co-authored by Claude Opus 4.8
parent daf33f77fe
commit c4d130828f
36 changed files with 912 additions and 83 deletions
+33 -27
View File
@@ -14,8 +14,8 @@
// - the input lock + the `input` hook (entry detection + free-input block + `!` steer)
// - the kickoff injection (before_agent_start) + the two kickoff payloads
// - the agent_end prose safety net
// - the exit-code contracts with the CLI (5 = gate refusal, 6 = needs human,
// 7 = not-ready silent no-op)
// - the exit-code contracts with the CLI (5 = gate refusal, 6 = needs human /
// auto-advance not ready -> silent no-op)
// - textResult / tht() / relayIfThtFails / advanceIfReady helpers
//
// TESTING: the pure builders are L1-tested (./gate/__tests__/). This file is the
@@ -29,11 +29,8 @@ import {
buildSelectRequest,
buildMultiselectRequest,
buildArtifactGate,
buildInfoRequest,
buildFreetextRequest,
withChildLinkage,
} from "./gate/builders.js";
import { ALTRO, BACK_LABEL, QUIT_LABEL, CONTROL_LABELS, isReserved, stripReserved } from "./reserved-labels.mjs";
import { isReserved, stripReserved } from "./reserved-labels.mjs";
// --- anti-bypass block lists (spec D4, verbatim from source L169-177) -----------
const FORBIDDEN = [
@@ -116,29 +113,22 @@ function phaseName(ctx, num) {
const p = meta.phases.find((x) => x.num === num);
return p ? p.name : "?";
}
function maxPhase(ctx) {
return phaseMeta(ctx).max_phase;
}
// The schema-linking phase = the phase whose artifacts_out contains schema_linking.json.
function schemaLinkingPhase(ctx) {
const meta = phaseMeta(ctx);
const p = meta.phases.find((x) => x.artifacts_out.includes("schema_linking.json"));
return p ? p.num : 5;
}
function currentPhase(ctx, session) {
const out = tht(ctx, ["phase", "show", "--session", session]);
const m = out.match(/Fase corrente:\s*(\d+)/);
return m ? parseInt(m[1], 10) : 1;
}
// tht phase advance --if-ready: exit 7 = not ready (silent no-op), others propagated.
// tht phase advance --auto: exit 6 = not ready / needs human (silent no-op), others propagated.
// Used for the fire-and-forget auto-advance of the auto phases (F2 memory, F6 cte) after a
// reviewer_decide: it only advances when the phase is auto-eligible (zero substantive
// decisions + prerequisites met), otherwise the CLI exits 6 and we no-op.
function advanceIfReady(ctx, session) {
try {
tht(ctx, ["phase", "advance", "--if-ready", "--session", session]);
tht(ctx, ["phase", "advance", "--auto", "--session", session]);
return { advanced: true };
} catch (e) {
if (e.status === 7) return { advanced: false };
if (e.status === 6) return { advanced: false };
return { advanced: false, error: (e.stderr || e.message || String(e)).toString().trim() };
}
}
@@ -191,7 +181,6 @@ export default function (pi) {
let lockActive = false;
let lastSteered = false;
let pendingKickoff = null;
let ollamaReady = false;
let activeSessionId = null;
// 1) ANTI-BYPASS tool_call hook (spec D4, verbatim). Blocks direct phase/decision
@@ -270,7 +259,6 @@ export default function (pi) {
lockActive = false;
lastSteered = false;
pendingKickoff = null;
ollamaReady = false;
activeSessionId = null;
_phaseMetaCache = null;
});
@@ -431,6 +419,8 @@ export default function (pi) {
data: Type.Any(),
version: Type.Optional(Type.Number()),
}),
// kind:"cte_plan" only -- ordered list of CTE names to persist (tht cte plan --name).
names: Type.Optional(Type.Array(Type.String())),
}),
async execute(_id, params, _signal, _onUpdate, ctx) {
lockActive = true;
@@ -453,15 +443,28 @@ export default function (pi) {
if (resp.control === "exit") return textResult("Il reviewer vuole uscire.");
// approved -> execute the privileged action via the CLI.
if (kind === "phase") {
// Try auto-advance first; exit 6 = needs human (already handled by this dialog).
const r = advanceIfReady(ctx, session);
if (!r.advanced && r.error) return textResult(`${r.error} ${PHASE_RECOVERY}`);
// Explicit human approval: advance unconditionally except for unmet
// prerequisites. Plain `phase advance` (no --auto) enforces advance_problems
// and exits 6 with the missing items, which relayIfThtFails surfaces.
const err = relayIfThtFails(ctx, ["phase", "advance", "--session", session], PHASE_RECOVERY);
if (err) return err;
return textResult(`Fase approvata (sessione ${session}).`);
}
if (kind === "cte_plan") {
const err = relayIfThtFails(ctx, ["cte", "plan", "--session", session], "");
// The CTE plan is the ordered list of CTE names; the model passes them in
// params.names (tht cte plan requires at least one --name).
const names = Array.isArray(params.names) ? params.names : [];
if (names.length === 0) {
return textResult(
"Nessun nome CTE fornito: il piano CTE richiede l'elenco ordinato dei CTE " +
"(parametro names di reviewer_confirm).",
);
}
const planArgs = ["cte", "plan", "--session", session];
for (const n of names) planArgs.push("--name", n);
const err = relayIfThtFails(ctx, planArgs, "");
if (err) return err;
return textResult(`CTE plan approvato (sessione ${session}).`);
return textResult(`CTE plan approvato (${names.length} CTE, sessione ${session}).`);
}
if (kind === "cte_result" || kind === "sql") {
const dt = kind === "sql" ? "sql_approved" : "cte_approved";
@@ -499,7 +502,10 @@ export default function (pi) {
assumps = [assumps];
}
}
const args = ["session", "set-question", "--session", session, "--question", question];
// `tht session set-question` takes the session id as a positional argument
// (the `session` command group uses positional ids, unlike phase/cte/decision
// which use --session).
const args = ["session", "set-question", session, "--question", question];
if (Array.isArray(assumps)) {
for (const a of assumps) args.push("--assumption", String(a));
}
+7 -6
View File
@@ -60,7 +60,7 @@ about a domain term, ask the reviewer.
`schema_linking.json`. Write the artifacts with care; they are the decision surface.
8. **Candidates are candidates, not truth.** Present LSH/vector/evidence matches with
their **provenance** (LSH / vector / evidence) and their scores, never as absolute
truth. The reviewer may reject them. Verify filter values with `tht search
truth. The reviewer may reject them. Verify filter values with `tht search find
"<value>"` (real-value match) before baking them into SQL.
9. **Open ambiguities are explicit.** If an ambiguity can't be resolved, offer a
`reviewer_decide` option "Leave ambiguity open" with a rationale, so the reviewer
@@ -77,8 +77,8 @@ about a domain term, ask the reviewer.
Prerequisite: you must already be in Phase 1.
1. Explore the DWH and knowledge base: `tht search "<term>"` (evidence + schema, LSH
over real values) and `tht search --kind evidence "<term>"`. The LSH exposes
1. Explore the DWH and knowledge base: `tht search find "<term>"` (evidence + schema, LSH
over real values) and `tht search find --kind evidence "<term>"`. The LSH exposes
EVERY column where a value appears — it does not collapse to a single best match,
so a value like "ablazione" may anchor on multiple columns.
2. For each ambiguity (clinical term, population, time window, outcome), present a
@@ -154,9 +154,10 @@ Prerequisite: Phase 3 closed.
with a `value_grounded` option for each candidate column (the LSH exposes all of
them, not collapsed to the best match). The reviewer chooses the anchor(s).
4. **Concept formula (D14b).** If a concept (e.g. "fascia pediatrica", "stesso anno")
has a candidate SQL formula (found in the evidence or derived from context),
present it and let the reviewer approve/reject
(`concept_formula_approved`/`concept_formula_rejected`).
has a candidate SQL formula, retrieve it with `tht search find --kind formula
"<concept>"` (or derive it from the evidence/context), present it, and let the
reviewer approve/reject (`concept_formula_approved`/`concept_formula_rejected`).
Reflect the approved formula in `schema_linking.json` (`concept_formulas`).
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).
@@ -42,7 +42,7 @@ by hand to the join.
- Do the filters (WHERE/HAVING) reflect ALL the conditions of the rewritten question?
- Are aggregations, groupings and orderings the required ones?
- Empty or zero result: almost always indicates a problem in conditions or joins.
Verify the filter values with `tht search "<value>"` (match on real values).
Verify the filter values with `tht search find "<value>"` (match on real values).
- Do the joins follow those promoted in `schema_linking.json`? (exception: the FK
`data_time_key → dim_time.day_key` is not declared, see the time section.)
- Time analyses: are you using `JOIN dim_time` and not key arithmetic?