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:
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user