Add full shell, replaceable Omics adapter and bilingual interaction

Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
This commit is contained in:
Codex
2026-09-13 14:26:39 +02:00
parent 2d1b714ebe
commit d8a29bfbdd
207 changed files with 8570 additions and 2164 deletions
@@ -1,6 +1,7 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const { installRepairGate } = require("../memory/repair.js");
const { gateText } = require("../core/locale.js");
function gate({ answers, fail = false, resume = false }) {
let tool;
@@ -8,6 +9,8 @@ function gate({ answers, fail = false, resume = false }) {
const repair = { repair_id: "receipt", choice: null, status: "proposed", can_apply: true,
saved: false, indexed: false, options: [{ id: "fix", content: { detail: "Correction" } }] };
installRepairGate({ registerTool: def => { tool = def; } }, {
languageFor: () => "en",
gateText,
workflow: { activate() {}, phase: () => ({ id: "F4" }) },
memory: { execute: (_ctx, args) => {
calls.push(args);
@@ -208,8 +208,8 @@ test("buildArtifactGate derives approve/reject options from action.kind", () =>
action: { kind: "approve_reject" },
});
assert.deepEqual(w.options, [
{ id: "approve", label: "Salva e procedi", recommended: true },
{ id: "reject", label: "Rifiuta" },
{ id: "approve", label: "Save and proceed", label_i18n: "Save and proceed", recommended: true },
{ id: "reject", label: "Reject", label_i18n: "Reject" },
]);
});
@@ -217,7 +217,7 @@ test("buildArtifactGate: confirm -> single approve; view_only -> no options", ()
const confirm = buildArtifactGate({
id: "u1", phase: "F1", title: "t", artifact: { kind: "phase", data: {} }, action: { kind: "confirm" },
});
assert.deepEqual(confirm.options, [{ id: "approve", label: "Salva e procedi", recommended: true }]);
assert.deepEqual(confirm.options, [{ id: "approve", label: "Save and proceed", label_i18n: "Save and proceed", recommended: true }]);
const viewOnly = buildArtifactGate({
id: "u1", phase: "F1", title: "t", artifact: { kind: "phase", data: {} }, action: { kind: "view_only" },
});
@@ -214,7 +214,7 @@ test("appendLedgerSection appends only the phase's substantive decisions", () =>
const out = appendLedgerSection(data, decisions, ["concept_clarified", "ambiguity_open"]);
assert.equal(out.sections.length, 2);
const ledger = out.sections[1];
assert.equal(ledger.title, "Decisioni registrate in questa fase (dal ledger)");
assert.equal(ledger.title, "Decisions recorded in this phase (from the ledger)");
assert.equal(ledger.items.length, 1);
assert.equal(ledger.items[0].label, "concept_clarified: ablazione");
assert.equal(ledger.items[0].value, "solo transcatetere");
@@ -18,7 +18,9 @@ if (typeof globalThis.require === "undefined") {
}
const shell = { current: () => "" };
cp.execFileSync = (file, args, opts) => shell.current(file, args, opts);
cp.execFileSync = (file, args, opts) => args[1] === "ensure-interaction-language"
? JSON.stringify({ interaction_language: "it", workspace_language: "it" })
: shell.current(file, args, opts);
const META = JSON.stringify({
max_phase: 8,
@@ -23,7 +23,9 @@ if (typeof globalThis.require === "undefined") {
// swap `activeStub` instead of clobbering the dispatcher. Each test's existing
// `cp.execFileSync = fn` / restore-in-finally then Just Works.
let activeStub = cp.execFileSync;
const dispatcher = (...args) => activeStub(...args);
const dispatcher = (...args) => args[1]?.[1] === "ensure-interaction-language"
? JSON.stringify({ interaction_language: "it", workspace_language: "it" })
: activeStub(...args);
Object.defineProperty(cp, "execFileSync", {
configurable: true,
get: () => dispatcher,
@@ -30,6 +30,8 @@ async function loadGate() {
function phase8Stub(calls) {
return (_file, args) => {
calls.push(args);
if (args[0] === "session" && args[1] === "ensure-interaction-language")
return JSON.stringify({ interaction_language: "it", workspace_language: "it" });
if (args[0] === "phase" && args[1] === "meta") {
return JSON.stringify({
max_phase: 8,
@@ -0,0 +1,104 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const cp = require("node:child_process");
const { createRequire } = require("node:module");
const path = require("node:path");
const { createFakePi } = require("./fake_pi_runtime.js");
const GATE = path.join(__dirname, "..", "..", "tht-gate.js");
if (!globalThis.require) globalThis.require = createRequire(GATE);
let activeStub = cp.execFileSync;
const dispatcher = (...args) => activeStub(...args);
Object.defineProperty(cp, "execFileSync", {
configurable: true, get: () => dispatcher, set: fn => { activeStub = fn; },
});
for (const [language, workspaceLanguage, entry, title, yes, no] of [
["en", "it", "/riprendi-sessione s1", "Generate a datamart?", "Yes, generate the datamart", "No, skip the datamart"],
["it", "en", '/nuova-domanda "Pazienti"', "Vuoi generare un datamart?", "Sì, genera il datamart", "No, salta il datamart"],
["fr-FR", "it", "/riprendi-sessione s1", "Generate a datamart?", "Yes, generate the datamart", "No, skip the datamart"],
]) test(`manifest language ${language} governs every model turn and generated gate chrome`, async () => {
const previous = { exec: cp.execFileSync, session: process.env.THT_SESSION,
language: process.env.THT_INTERACTION_LANGUAGE, profile: process.env.THT_PROFILE };
process.env.THT_SESSION = "s1";
process.env.THT_INTERACTION_LANGUAGE = workspaceLanguage; // stale launch hints have no authority
process.env.THT_PROFILE = "server";
cp.execFileSync = (_file, args) => {
if (args[0] === "session" && args[1] === "ensure-interaction-language")
return JSON.stringify({ id: "s1", interaction_language: language, workspace_language: workspaceLanguage });
if (args[0] === "session" && args[1] === "retrieval-pack") return "Pazienti: età > 65";
if (args[0] === "phase" && args[1] === "meta")
return JSON.stringify({ max_phase: 8, phases: [{ num: 8, id: "F8", name: "datamart",
emits: ["datamart_requested", "datamart_declined"] }] });
if (args[0] === "phase" && args[1] === "show") return "Fase corrente: 8\n";
return "";
};
try {
const { default: installGate } = await import(GATE);
const { pi, ctx, tools } = createFakePi();
ctx.cwd = "/nonexistent-language-test";
installGate(pi);
await pi.emit("input", { text: entry, source: "rpc" });
for (let turn = 0; turn < 2; turn++) {
const injected = await pi.emit("before_agent_start", { systemPrompt: "BASE" });
assert.ok(injected.systemPrompt.includes(`interaction_language=${language}`));
assert.ok(injected.systemPrompt.includes(`workspace_language=${workspaceLanguage}`));
assert.match(injected.systemPrompt, /reviewer questions, explanations, and choices/);
}
let widget;
ctx.ui.input = async title => {
widget = JSON.parse(title);
return JSON.stringify({ id: widget.id, choices: ["skip"] });
};
await tools.get("reviewer_datamart").def.execute("call", { session: "s1" }, null, null, ctx);
assert.equal(widget.title, title);
assert.equal(widget.title_i18n, "Generate a datamart?");
assert.deepEqual(widget.options.map(o => [o.id, o.label]), [
["generate", yes], ["skip", no],
]);
} finally {
cp.execFileSync = previous.exec;
for (const [key, value] of [["THT_SESSION", previous.session],
["THT_INTERACTION_LANGUAGE", previous.language], ["THT_PROFILE", previous.profile]]) {
if (value === undefined) delete process.env[key]; else process.env[key] = value;
}
}
});
test("an unavailable manifest blocks model startup instead of trusting a launch hint", async () => {
const originalExec = cp.execFileSync;
const previousSession = process.env.THT_SESSION;
process.env.THT_SESSION = "unavailable";
cp.execFileSync = () => { throw new Error("storage unavailable"); };
try {
const { default: installGate } = await import(GATE);
const { pi, ctx } = createFakePi();
ctx.cwd = "/nonexistent-language-test";
installGate(pi);
await pi.emit("input", { text: "/riprendi-sessione unavailable", source: "rpc" });
await assert.rejects(pi.emit("before_agent_start", { systemPrompt: "BASE" }), /storage unavailable/);
} finally {
cp.execFileSync = originalExec;
if (previousSession === undefined) delete process.env.THT_SESSION;
else process.env.THT_SESSION = previousSession;
}
});
test("artifact and join chrome localize while SQL, decisions and generated text remain verbatim", () => {
const { buildArtifactGate, buildJoinReviewRequest } = require("../core/builders.js");
const artifact = { kind: "sql", data: "SELECT nome FROM pazienti WHERE stato = 'attivo'" };
const options = [{ id: "join-1", label: "Collega pazienti", detail: "p.id = v.id" }];
for (const [locale, save, reject, next] of [
["en", "Save and proceed", "Reject", "Continue"],
["it", "Salva e procedi", "Rifiuta", "Continua"],
["fr", "Save and proceed", "Reject", "Continue"],
]) {
const gate = buildArtifactGate({ id: "u1", phase: "F7", title: "SQL finale",
artifact, action: { kind: "approve_reject" }, locale });
assert.deepEqual(gate.options.map(o => [o.id, o.label]), [["approve", save], ["reject", reject]]);
assert.deepEqual(gate.artifact, artifact);
assert.equal(gate.title, "SQL finale");
const joins = buildJoinReviewRequest({ id: "u2", phase: "F4", title: "Join", options, locale });
assert.equal(joins.confirm_label, next);
assert.deepEqual(joins.options, options);
}
});
@@ -36,6 +36,8 @@ const JOIN_OPTIONS = [
function shellStub(calls, inputs = []) {
return (_file, args, options = {}) => {
calls.push(args.join(" "));
if (args[0] === "session" && args[1] === "ensure-interaction-language")
return JSON.stringify({ interaction_language: "en", workspace_language: "it" });
if (options.input) inputs.push(options.input);
if (args[0] === "phase" && args[1] === "meta") {
return JSON.stringify({ phases: [{ num: 4, id: "F4" }] });
@@ -1,6 +1,7 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const { createMemoryGate } = require("../memory/index.js");
const { gateText } = require("../core/locale.js");
const { createFakePi } = require("./fake_pi_runtime.js");
function setup({ phase = 8, response, failure = null } = {}) {
@@ -8,6 +9,8 @@ function setup({ phase = 8, response, failure = null } = {}) {
const calls = [];
const summary = { summary_id: "summary", items: [{ id: "rule", card: { subject: "Rule" } }] };
createMemoryGate({
languageFor: () => "en",
gateText,
workflow: { activate() {}, phase: () => ({ number: phase, id: "F" + phase }),
close: () => { calls.push("close"); return "closed"; } },
memory: { execute: () => JSON.stringify(summary),
@@ -1,6 +1,7 @@
const test = require("node:test");
const assert = require("node:assert");
const { createMemoryGate } = require("../memory/index.js");
const { gateText } = require("../core/locale.js");
const { buildMultiselectRequest } = require("../core/builders.js");
const { isReserved } = require("../core/reserved-labels.mjs");
const { createFakePi } = require("./fake_pi_runtime.js");
@@ -43,7 +44,7 @@ const MEMORY_OPTIONS = [
];
function setupRecall(choices) {
function setupRecall(choices, language = "it") {
const { pi, tools, ctx } = createFakePi();
const calls = [];
let descriptor;
@@ -53,6 +54,8 @@ function setupRecall(choices) {
return JSON.stringify({ id: descriptor.id, choices });
};
const memoryGate = createMemoryGate({
languageFor: () => language,
gateText,
workflow: {
activate: () => calls.push(["activate"]),
phase: () => ({ number: 8, id: "F8" }),
@@ -117,6 +120,19 @@ test("the Memory facade normalizes search hits and applies only the selected F2
assert.match(result.content[0].text, /1 decisioni.*La fase resta aperta/s);
});
test("English Memory chrome preserves Italian content and carries UI translation keys", async () => {
const { ctx, memoryGate, descriptor } = setupRecall(["first"], "en");
await memoryGate.reviewRecall(ctx, {
session: "s1", title: "Memory", options: MEMORY_OPTIONS, allow_empty: true, advance: false,
}, "F2");
assert.equal(descriptor().title, "Select memories to apply to the question");
assert.equal(descriptor().confirm_label, "Apply selected memories");
assert.equal(descriptor().confirm_label_i18n, "Apply selected memories");
assert.equal(descriptor().options[0].label, "Regola paziente attivo");
assert.equal(descriptor().options[0].detail, MEMORY_OPTIONS[0].description);
assert.equal(descriptor().options[0].label_i18n, undefined);
});
test("authoritative UUID card identities survive normalization into the reviewer widget", async () => {
const { ctx, memoryGate, descriptor } = setupRecall(["first"]);
const id = "mem-11111111-1111-4111-8111-111111111111";
@@ -26,7 +26,7 @@ case "$1 $2" in
"phase show") echo "Fase corrente: ${phase}";;
"phase meta") echo '{"max_phase":8,"phases":[{"num":2,"id":"F2","emits":[]},{"num":8,"id":"F8","emits":[]}]}';;
"memory summary") echo '{"summary_id":"saved-review","reviewed":true}';;
"session show") echo '{"status":"${status}"}';;
"session show"|"session ensure-interaction-language") echo '{"id":"s1","status":"${status}","interaction_language":"it","workspace_language":"it"}';;
*) echo "OK";;
esac
`;
@@ -11,12 +11,14 @@ test("con THT_SESSION il kickoff usa l'id fornito e NON crea la sessione", async
const bin = path.join(tmp, "bin");
fs.mkdirSync(bin);
const tht = path.join(bin, "tht");
fs.writeFileSync(tht, "#!/bin/sh\nexit 1\n", { mode: 0o755 });
fs.writeFileSync(tht, '#!/bin/sh\nif [ "$2" = "ensure-interaction-language" ]; then\n' +
'echo \'{"interaction_language":"it","workspace_language":"it"}\'\nelse\nexit 1\nfi\n', { mode: 0o755 });
const oldPath = process.env.PATH;
process.env.PATH = bin + ":" + oldPath;
process.env.THT_SESSION = "2026-06-27-100000-test";
try {
const { pi, ctx } = createFakePi();
ctx.cwd = tmp;
installGate(pi);
await pi.emit("input", { source: "rpc", text: '/nuova-domanda "x"' });
const injected = await pi.emit("before_agent_start", { systemPrompt: "" });
@@ -56,7 +58,9 @@ test("con retrieval pack persistito lo inietta senza chiedere tool call ridondan
fs.writeFileSync(
tht,
"#!/bin/sh\n" +
"printf \"# Retrieval pack\\n\\nTABELLA_CANDIDATA\\n\"\n",
'if [ "$2" = "ensure-interaction-language" ]; then\n' +
'echo \'{"interaction_language":"it","workspace_language":"it"}\'\nelse\n' +
"printf \"# Retrieval pack\\n\\nTABELLA_CANDIDATA\\n\"\nfi\n",
{ mode: 0o755 },
);
const oldPath = process.env.PATH;
@@ -1,5 +1,15 @@
const test = require("node:test");
const assert = require("node:assert");
const cp = require("node:child_process");
const { syncBuiltinESMExports } = require("node:module");
const originalExec = cp.execFileSync;
cp.execFileSync = (_file, args) => {
if (args[0] === "session" && args[1] === "ensure-interaction-language")
return JSON.stringify({ interaction_language: "it", workspace_language: "it" });
throw new Error("No retrieval pack in this fixture");
};
syncBuiltinESMExports();
test.after(() => { cp.execFileSync = originalExec; syncBuiltinESMExports(); });
const { createFakePi } = require("./fake_pi_runtime.js");
const installGate = require("../../tht-gate.js").default ?? require("../../tht-gate.js");
@@ -21,5 +21,5 @@ test("no-limbo: undefined (cancel) ri-presenta lo stesso widget", async () => {
const resp = await emitAndWait(ctx, descriptor);
assert.deepEqual(resp, { id: "u1", choices: ["a"] });
assert.equal(ctx.uiCalls.length, 2); // ri-presentato una volta
assert.ok(ctx.notifications.some((n) => /Esc non chiude/.test(n.message)));
assert.ok(ctx.notifications.some((n) => /Esc does not close/.test(n.message)));
});
@@ -10,7 +10,9 @@ const GATE = path.join(__dirname, "..", "..", "tht-gate.js");
globalThis.require = createRequire(GATE);
const shell = { current: () => "" };
cp.execFileSync = (file, args, options) => shell.current(file, args, options);
cp.execFileSync = (file, args, options) => args[1] === "ensure-interaction-language"
? JSON.stringify({ interaction_language: "it", workspace_language: "it" })
: shell.current(file, args, options);
const installGate = require(GATE).default ?? require(GATE);
@@ -541,7 +543,7 @@ test("F8 warns about pending indexing but preserves the saved review and finaliz
const { ctx, tools, calls } = await setupGate({ phase: 8, preview: [PROMOTION_CANDIDATE], indexed: false });
answerReview(ctx, [PROMOTION_CANDIDATE]);
await tools.get("reviewer_memory_promote").def.execute("pending", { session: "s1" }, null, null, ctx);
assert(ctx.notifications.some(({ message, level }) => level === "warning" && message.includes("Memory saved")));
assert(ctx.notifications.some(({ message, level }) => level === "warning" && message.includes("Memory salvata")));
assert(memoryPromotionMutationArgs(calls).some(args => args.includes("memory_summary_reviewed")));
});
test("F8 recovers a saved review without asking again or saving cards again", async () => {
+14 -7
View File
@@ -14,6 +14,7 @@
const SCHEMA_VERSION = 1;
const RESERVED = ["back", "exit", "other"];
const { gateText } = require("./locale.js");
const VALID_ACTION_KINDS = new Set(["confirm", "approve_reject", "view_only"]);
const VALID_INFO_LEVELS = new Set(["info", "warning", "error"]);
@@ -38,6 +39,7 @@ function requireArray(value, name, ctx) {
// highlight as "(consigliato)".
function buildSelectRequest({
id, phase, title, options, intro = null, recommended = null,
titleI18n,
}) {
requireString(title, "title", "select");
const opts = requireArray(options, "options", "select");
@@ -48,6 +50,7 @@ function buildSelectRequest({
schema_version: SCHEMA_VERSION,
widget: "select",
title,
...(titleI18n ? { title_i18n: titleI18n } : {}),
intro,
recommended,
options: [...opts],
@@ -61,7 +64,7 @@ function buildSelectRequest({
// `allowEmpty:false` with zero options is a broken widget and throws.
function buildMultiselectRequest({
id, phase, title, options, content = null, selected = [], allowEmpty = false,
selectionLabel, confirmLabel,
selectionLabel, confirmLabel, titleI18n, selectionLabelI18n, confirmLabelI18n,
}) {
requireString(title, "title", "multiselect");
const opts = requireArray(options, "options", "multiselect");
@@ -77,6 +80,7 @@ function buildMultiselectRequest({
schema_version: SCHEMA_VERSION,
widget: "multiselect",
title,
...(titleI18n ? { title_i18n: titleI18n } : {}),
allow_empty: allowEmpty,
options: opts.map((option) => ({ ...option, selected: selected.includes(option.id) })),
selected: [...selected],
@@ -84,13 +88,15 @@ function buildMultiselectRequest({
reserved: RESERVED,
...(selectionLabel ? { selection_label: selectionLabel } : {}),
...(confirmLabel ? { confirm_label: confirmLabel } : {}),
...(selectionLabelI18n ? { selection_label_i18n: selectionLabelI18n } : {}),
...(confirmLabelI18n ? { confirm_label_i18n: confirmLabelI18n } : {}),
};
}
// Build an `artifact-gate` ui_request: a rendered artifact + a disposition list
// (confirm / approve_reject / view_only). The decision is load-bearing on the
// document, so the artifact is never shown alone (spec §4.1).
function buildArtifactGate({ id, phase, title, artifact, action }) {
function buildArtifactGate({ id, phase, title, artifact, action, locale = "en" }) {
requireString(title, "title", "artifact-gate");
if (!artifact || typeof artifact !== "object") {
throw new Error("builders: artifact-gate requires an artifact object");
@@ -105,10 +111,10 @@ function buildArtifactGate({ id, phase, title, artifact, action }) {
}
const ACTION_OPTIONS = {
approve_reject: [
{ id: "approve", label: "Salva e procedi", recommended: true },
{ id: "reject", label: "Rifiuta" },
{ id: "approve", label: gateText(locale, "save"), label_i18n: gateText("en", "save"), recommended: true },
{ id: "reject", label: gateText(locale, "reject"), label_i18n: gateText("en", "reject") },
],
confirm: [{ id: "approve", label: "Salva e procedi", recommended: true }],
confirm: [{ id: "approve", label: gateText(locale, "save"), label_i18n: gateText("en", "save"), recommended: true }],
view_only: [],
};
return {
@@ -180,7 +186,7 @@ function buildSchemaLinkingRequest({ id, phase, title, tables }) {
// Build a blocking, read-only join review. The reviewer can accept the complete
// proposal or use Other to request a textual correction; individual joins are
// deliberately not selectable because omitting one could create a Cartesian product.
function buildJoinReviewRequest({ id, phase, title, options }) {
function buildJoinReviewRequest({ id, phase, title, options, locale = "en" }) {
requireString(title, "title", "join-review");
const opts = requireArray(options, "options", "join-review");
if (opts.length === 0) {
@@ -194,7 +200,8 @@ function buildJoinReviewRequest({ id, phase, title, options }) {
widget: "join-review",
title,
options: [...opts],
confirm_label: "Continue",
confirm_label: gateText(locale, "continue"),
confirm_label_i18n: gateText("en", "continue"),
reserved: RESERVED,
};
}
+4 -2
View File
@@ -156,12 +156,14 @@ function enrichCteResultColumns(payload, planTables, getColumns) {
// comes from persisted state, not model prose). `decisions` is session-show's ledger
// dump; `emits` is the phase's substantive decision-type list from workflow.yaml.
// No matching decisions -> data returned unchanged. Pure: returns a NEW object.
function appendLedgerSection(data, decisions, emits) {
function appendLedgerSection(data, decisions, emits, locale = "en") {
const { gateText } = require("./locale.js");
const emitSet = new Set(emits || []);
const rows = (decisions || []).filter((d) => d && emitSet.has(d.type));
if (rows.length === 0) return data;
const section = {
title: "Decisioni registrate in questa fase (dal ledger)",
title: gateText(locale, "ledgerTitle"),
title_i18n: gateText("en", "ledgerTitle"),
items: rows.map((d) => ({
label: `${d.type}: ${d.subject}`,
value: d.detail || "",
@@ -0,0 +1,41 @@
// Only deterministic chrome is translated. Model prose and domain payloads are opaque.
const en = {
save: "Save and proceed", reject: "Reject", continue: "Continue",
approvalPrompt: "Approve or reject?", chooseApproval: "Choose Save and proceed or Reject.",
datamartTitle: "Generate a datamart?", datamartYes: "Yes, generate the datamart",
datamartNo: "No, skip the datamart",
memoryTitle: "Select memories to apply to the question", memorySelection: "memories to apply",
memoryApply: "Apply selected memories", memoryEmpty: "No reusable memories for this question; continuing to the next phase.",
memorySummary: "Memory for future questions", archiveRepair: "Resolve archive conflict",
repairError: "The correction could not complete. Review the current archive state before retrying.",
ledgerTitle: "Decisions recorded in this phase (from the ledger)",
escapeHelp: "Esc does not close the gate: use Back / Exit / Other.",
inputHelp: "Answer through the gate widgets. To send free text to the model, start the line with '!'.",
memoryIndexWarning: "Memory saved. Index update is incomplete; an administrator can retry it in Memory management.",
backUsage: "Usage: /torna <session_id> [N]",
backInvalid: "Invalid target (current phase {phase}).",
decision: "Decision",
};
const it = {
save: "Salva e procedi", reject: "Rifiuta", continue: "Continua",
approvalPrompt: "Approvi o rifiuti?", chooseApproval: "Scegli «Salva e procedi» o «Rifiuta».",
datamartTitle: "Vuoi generare un datamart?", datamartYes: "Sì, genera il datamart",
datamartNo: "No, salta il datamart",
memoryTitle: "Seleziona le memory da applicare alla domanda", memorySelection: "memory da applicare",
memoryApply: "Applica le memory selezionate", memoryEmpty: "Nessuna memory riutilizzabile per questa domanda — passo alla fase successiva.",
memorySummary: "Memory per le domande future", archiveRepair: "Risolvi il conflitto negli archivi",
repairError: "La correzione non è stata completata. Verifica lo stato attuale degli archivi prima di riprovare.",
ledgerTitle: "Decisioni registrate in questa fase (dal ledger)",
escapeHelp: "Esc non chiude il gate: usa Torna indietro / Esci / Altro dalle opzioni.",
inputHelp: "Durante la sessione rispondi con i widget del gate. Per inviare testo libero al modello inizia la riga con '!'.",
memoryIndexWarning: "Memory salvata. L'aggiornamento dell'indice è incompleto; un amministratore può riprovarlo da Gestione Memory.",
backUsage: "Uso: /torna <session_id> [N]",
backInvalid: "Target non valido (fase corrente {phase}).",
decision: "Decisione",
};
const catalogs = { en, it };
function gateText(locale, key) {
const language = String(locale || "en").toLowerCase().split("-")[0];
return catalogs[language]?.[key] ?? en[key];
}
module.exports = { gateText };
+18 -15
View File
@@ -1,9 +1,7 @@
import { Type } from "typebox";
import { installRepairGate } from "./repair.js";
// Compatibility note: reviewer labels move verbatim from the composition root.
// Tickets #22 and #23 require observable parity; translating existing chrome is a
// separate product behavior change rather than part of these extractions.
// The composition root supplies manifest language; only deterministic chrome is localized.
function normalizedText(value) {
return String(value ?? "").trim().replace(/\s+/g, " ").toLowerCase();
@@ -31,7 +29,7 @@ function memoryOptionKey(option) {
}
function normalizeMemoryOptions(options) {
function normalizeMemoryOptions(options, locale, gateText) {
const seen = new Set();
const out = [];
for (const option of options) {
@@ -47,7 +45,7 @@ function normalizeMemoryOptions(options) {
].join(" ").match(MEMORY_ID_RE)?.[0]?.toLowerCase();
const fallbackDetail = [
option.decision?.type && option.decision?.subject
? `Decisione ${option.decision.type}: ${option.decision.subject}`
? `${gateText(locale, "decision")} ${option.decision.type}: ${option.decision.subject}`
: "",
option.decision?.detail ?? "",
].filter(Boolean).join("\n");
@@ -62,12 +60,15 @@ function normalizeMemoryOptions(options) {
}
function memorySelectionWidgetProps(options) {
function memorySelectionWidgetProps(options, locale, gateText) {
return {
title: "Seleziona le memory da applicare alla domanda",
title: gateText(locale, "memoryTitle"),
titleI18n: gateText("en", "memoryTitle"),
selected: options.filter((option) => option.recommended).map((option) => option.id),
selectionLabel: "memory da applicare",
confirmLabel: "Applica le memory selezionate",
selectionLabel: gateText(locale, "memorySelection"),
confirmLabel: gateText(locale, "memoryApply"),
selectionLabelI18n: gateText("en", "memorySelection"),
confirmLabelI18n: gateText("en", "memoryApply"),
};
}
@@ -78,10 +79,11 @@ function shouldSkipEmptyRecall({ meritCount, allowEmpty, advance }) {
async function reviewRecall(ctx, params, phase, dependencies) {
const { workflow, ledger, reviewer, waitForReviewer, toTextResult } = dependencies;
const { workflow, ledger, reviewer, waitForReviewer, toTextResult, languageFor, gateText } = dependencies;
try {
const { session, advance } = params;
const options = normalizeMemoryOptions(params.options);
const locale = languageFor(ctx, session);
const options = normalizeMemoryOptions(params.options, locale, gateText);
const typeError = ledger.validate(ctx, options, session);
if (typeError) return toTextResult(typeError);
const meritOptions = options
@@ -99,7 +101,7 @@ async function reviewRecall(ctx, params, phase, dependencies) {
advance,
})) {
await ctx.ui.notify(
"Nessuna memory riutilizzabile per questa domanda — passo alla fase successiva.",
gateText(locale, "memoryEmpty"),
"info",
);
workflow.advance(ctx, session);
@@ -113,7 +115,7 @@ async function reviewRecall(ctx, params, phase, dependencies) {
phase,
allowEmpty: params.allow_empty ?? false,
options: meritOptions,
...memorySelectionWidgetProps(options),
...memorySelectionWidgetProps(options, locale, gateText),
});
const response = await waitForReviewer(ctx, widget);
if (response.control === "freetext") {
@@ -161,7 +163,7 @@ async function reviewRecall(ctx, params, phase, dependencies) {
}
function installMemoryGate(pi, { workflow, memory, ledger, waitForReviewer, toTextResult }) {
function installMemoryGate(pi, { workflow, memory, ledger, waitForReviewer, toTextResult, languageFor, gateText }) {
pi.registerTool({
name: "reviewer_memory_promote",
label: "Review Memory summary",
@@ -188,7 +190,8 @@ function installMemoryGate(pi, { workflow, memory, ledger, waitForReviewer, toTe
}
const response = await waitForReviewer(ctx, {
id: `u${Date.now()}`, schema_version: 1, phase: phase.id,
widget: "memory-review", title: "Memory for future questions",
widget: "memory-review", title: gateText(languageFor(ctx, session), "memorySummary"),
title_i18n: gateText("en", "memorySummary"),
summary, reserved: ["other", "back", "exit"],
});
if (response.control) return toTextResult(
+6 -3
View File
@@ -1,6 +1,6 @@
import { Type } from "typebox";
export function installRepairGate(pi, { workflow, memory, waitForReviewer, toTextResult }) {
export function installRepairGate(pi, { workflow, memory, waitForReviewer, toTextResult, languageFor, gateText }) {
pi.registerTool({
name: "reviewer_archive_repair",
label: "Resolve Memory / Evidence conflict",
@@ -26,6 +26,7 @@ export function installRepairGate(pi, { workflow, memory, waitForReviewer, toTex
try {
if (!!params.repair_id === !!params.proposal)
return toTextResult("Supply either a new proposal or an existing repair_id.");
const locale = languageFor(ctx, params.session);
const run = args => JSON.parse(memory.execute(ctx,
[...args, "--session", params.session, "--json"]));
let repair = params.repair_id
@@ -36,7 +37,9 @@ export function installRepairGate(pi, { workflow, memory, waitForReviewer, toTex
const response = await waitForReviewer(ctx, {
id: `u${Date.now()}`, schema_version: 1,
phase: workflow.phase(ctx, params.session).id,
widget: "archive-repair", title: "Resolve archive conflict",
widget: "archive-repair", title: gateText(locale, "archiveRepair"),
title_i18n: gateText("en", "archiveRepair"),
...(error ? { error_i18n: gateText("en", "repairError") } : {}),
repair, error, reserved: ["other", "back", "exit"],
});
if (response.control) return toTextResult(response.control === "freetext"
@@ -62,7 +65,7 @@ export function installRepairGate(pi, { workflow, memory, waitForReviewer, toTex
return toTextResult("All repair proposals were rejected as inadequate. " +
"No archive was changed. Reformulate the options for the reviewer.");
} catch (failure) {
error = "The correction could not complete. Review the current archive state before retrying.";
error = gateText(locale, "repairError");
repair = run(["repair-show", "--repair-id", repair.repair_id]);
}
}