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));
}