chore: deep audit, hardening, docs sync and workflow cleanup

This commit is contained in:
L'électron rare
2026-03-17 10:22:22 +01:00
parent bc23d85269
commit ec85ca5e5b
19 changed files with 1400 additions and 121 deletions
+51
View File
@@ -339,3 +339,54 @@ Reste à faire (futur) :
- [ ] MCP (Model Context Protocol) pour intégration outils
- [ ] WebRTC voice (streaming temps réel au lieu d'upload)
- [ ] Fine-tune personas dédié (OpenCharacter/Ditto methodology)
## Lot 17 — Deep Audit & Refactoring `[en cours]`
Objectif : audit complet du code, optimisations chirurgicales, documentation enrichie.
### Phase A — Analyse & documentation `[en cours]`
- [x] Deep audit TUI script (ops/v2/deep-audit.js) — security, perf, complexity, deps
- [x] Veille OSS enrichie (10 nouvelles categories: voice cloning, music gen, PDF, RAG, WebRTC, MCP, persona fine-tune)
- [x] Diagrammes Mermaid ajoutés (Context Store, Docker deploy, Inter-persona dialogue)
- [x] AGENTS.md refondu (matrice 10 agents, Mermaid skill routing, pipeline intervention)
- [ ] Consolidation PLAN.md et TODO.md avec etat reel
- [ ] Deep analyse code par agents (5 agents paralleles: api, web, packages, mascarade, v1+worker)
### Phase B — Refactoring code `[planifié]`
- [ ] ws-chat.ts: extraction modules (1449 LOC → 4 modules <400 LOC)
- [ ] app.ts: extraction routes + middleware + handlers (1292 LOC → 3 modules)
- [ ] writeFileSync → async dans ws-chat.ts (3 occurrences P2)
- [ ] console.log → structured logging (apps/api, apps/worker)
- [ ] React.memo + lazy load sur composants lourds (Chat, ChatHistory, VoiceChat, NodeEditor)
### Phase C — Infrastructure `[planifié]`
- [ ] Ajouter SearXNG au docker-compose (remplacer DuckDuckGo scraping)
- [ ] Ajouter MinerU/Docling (remplacer pdf-parse)
- [ ] Spike BGE-M3 embeddings (upgrade RAG)
- [ ] Deployer deep-audit.js sur kxkm-ai
### Phase D — Nouveaux node types `[planifié]`
- [ ] Node type `music_generation` (ACE-Step 1.5, <4GB VRAM)
- [ ] Node type `voice_clone` (XTTS-v2, zero-shot 6s reference)
- [ ] Node type `document_extraction` (MinerU/Docling)
## Lot 18 — Voice & MCP `[futur]`
Objectif : voix temps réel et intégration outils standardisée.
- [ ] XTTS-v2 voice cloning par persona (remplacer Piper pour voix uniques)
- [ ] LLMRTC WebRTC streaming (TypeScript SDK, VAD, barge-in)
- [ ] MCP SDK integration (personas comme MCP servers, tool-calling)
- [ ] PCL + OpenCharacter (pipeline fine-tune persona avancé)
- [ ] Chatterbox TTS evaluation (remplacement Piper qualité)
## Lot 19 — Music & Creative `[futur]`
- [ ] ACE-Step 1.5 production (génération musicale)
- [ ] `/compose` command (prompt → musique via Node Engine)
- [ ] Flux 2 dans ComfyUI (upgrade image gen)
- [ ] A2A Protocol evaluation (interop agents externes)
+58
View File
@@ -135,3 +135,61 @@
- [x] Docker — `Dockerfile` (multi-stage Node 22 alpine) + `docker-compose.yml` (5 services) + `.dockerignore`
- [ ] Documentation utilisateur
- [ ] Performance profiling
## P11 Lot 17 — Deep Audit & Refactoring
### Phase A — Analyse & documentation
- [x] Script TUI deep-audit.js (security, perf, complexity, deps) — `ops/v2/deep-audit.js`
- [x] Veille OSS enrichie 2026-03-17 (10 nouvelles catégories) — `docs/OSS_WATCH_2026-03-16.md`
- [x] Diagrammes Mermaid (Context Store, Docker, Inter-persona) — `docs/ARCHITECTURE.md`
- [x] AGENTS.md refondu (matrice 10 agents, Mermaid routing, pipeline) — `docs/AGENTS.md`
- [x] PLAN.md consolidé avec lots 17-19
- [x] TODO.md consolidé avec backlog Phase 6+
- [ ] Deep analyse code agents (api, web, packages, mascarade, v1+worker) — en cours
### Phase B — Refactoring code
- [ ] **P1** ws-chat.ts: extraction modules (1449 LOC → ~4×350 LOC)
- [ ] Extraire `ws-multimodal.ts` (vision, STT, TTS, PDF handlers)
- [ ] Extraire `ws-persona-router.ts` (pickResponders, inter-persona, memory)
- [ ] Extraire `ws-commands.ts` (slash commands, /web, /imagine)
- [ ] Garder `ws-chat.ts` core (WebSocket lifecycle, broadcast, rate limit)
- [ ] **P1** app.ts: extraction routes (1292 LOC → routes/ + middleware/)
- [ ] Extraire `routes/personas.ts`
- [ ] Extraire `routes/node-engine.ts`
- [ ] Extraire `routes/chat.ts`
- [ ] Extraire `middleware/auth.ts`
- [ ] **P2** writeFileSync → appendFile async dans ws-chat.ts (3 occurrences)
- [ ] **P2** console.log → logger structuré (apps/api, apps/worker)
- [ ] **P2** React.memo sur Chat, ChatHistory, VoiceChat, NodeEditor
- [ ] **P2** Lazy load: React.lazy + Suspense pour routes lourdes
### Phase C — Infrastructure
- [ ] SearXNG dans docker-compose (service searxng:8080, remplacer DuckDuckGo)
- [ ] MinerU/Docling dans docker-compose (remplacer pdf-parse)
- [ ] Spike BGE-M3 embeddings (upgrade nomic-embed-text)
- [ ] Déployer deep-audit.js sur kxkm-ai (cron quotidien)
- [ ] Créer utilisateur Discord **Pharmacius** (bot orchestrateur, bridge chat Discord ↔ KXKM)
### Phase D — Nouveaux node types
- [ ] `music_generation` node (ACE-Step 1.5, <4GB VRAM)
- [ ] `voice_clone` node (XTTS-v2, zero-shot 6s reference)
- [ ] `document_extraction` node (MinerU/Docling)
## P12 Lot 18 — Voice & MCP (futur)
- [ ] XTTS-v2 voice cloning par persona
- [ ] LLMRTC WebRTC streaming (TypeScript, VAD, barge-in)
- [ ] MCP SDK integration (personas = MCP servers)
- [ ] PCL + OpenCharacter pipeline fine-tune
- [ ] Chatterbox TTS evaluation
## P13 Lot 19 — Music & Creative (futur)
- [ ] ACE-Step 1.5 production
- [ ] `/compose` command (prompt → musique)
- [ ] Flux 2 dans ComfyUI
- [ ] A2A Protocol evaluation
+27 -8
View File
@@ -866,8 +866,17 @@ export async function createApp(): Promise<{ app: express.Express; personaRepo:
const voiceSamplesDir = path.resolve(process.cwd(), "data", "voice-samples");
await mkdir(voiceSamplesDir, { recursive: true });
const sampleName = persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
const sampleName = path.basename(persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_")).slice(0, 64);
if (!sampleName || sampleName === "." || sampleName === "..") {
res.status(400).json({ ok: false, error: "invalid_persona_name" });
return;
}
const samplePath = path.join(voiceSamplesDir, `${sampleName}.wav`);
// Boundary check: ensure resolved path stays within voiceSamplesDir
if (!path.resolve(samplePath).startsWith(voiceSamplesDir)) {
res.status(400).json({ ok: false, error: "path_traversal_blocked" });
return;
}
await writeFile(samplePath, buffer);
@@ -882,8 +891,13 @@ export async function createApp(): Promise<{ app: express.Express; personaRepo:
return;
}
const sampleName = persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
const samplePath = path.resolve(process.cwd(), "data", "voice-samples", `${sampleName}.wav`);
const voiceSamplesDir2 = path.resolve(process.cwd(), "data", "voice-samples");
const sampleName = path.basename(persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_")).slice(0, 64);
const samplePath = path.join(voiceSamplesDir2, `${sampleName}.wav`);
if (!sampleName || !path.resolve(samplePath).startsWith(voiceSamplesDir2)) {
res.status(400).json({ ok: false, error: "invalid_persona_name" });
return;
}
try {
const { unlink } = await import("node:fs/promises");
@@ -902,12 +916,17 @@ export async function createApp(): Promise<{ app: express.Express; personaRepo:
return;
}
const sampleName = persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
const samplePath = path.resolve(process.cwd(), "data", "voice-samples", `${sampleName}.wav`);
const voiceSamplesDir3 = path.resolve(process.cwd(), "data", "voice-samples");
const sampleName2 = path.basename(persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_")).slice(0, 64);
const samplePath2 = path.join(voiceSamplesDir3, `${sampleName2}.wav`);
if (!sampleName2 || !path.resolve(samplePath2).startsWith(voiceSamplesDir3)) {
res.json({ ok: true, data: { hasVoiceSample: false } });
return;
}
try {
await stat(samplePath);
res.json({ ok: true, data: { hasVoiceSample: true, samplePath: `data/voice-samples/${sampleName}.wav` } });
await stat(samplePath2);
res.json({ ok: true, data: { hasVoiceSample: true, samplePath: `data/voice-samples/${sampleName2}.wav` } });
} catch {
res.json({ ok: true, data: { hasVoiceSample: false } });
}
@@ -1019,7 +1038,7 @@ export async function createApp(): Promise<{ app: express.Express; personaRepo:
// Analytics — aggregate chat log stats
// -----------------------------------------------------------------------
app.get("/api/v2/analytics", async (_req, res) => {
app.get("/api/v2/analytics", requirePermission("ops:read"), async (_req: SessionRequest, res) => {
const logDir = path.join(process.cwd(), process.env.KXKM_LOCAL_DATA_DIR || "data", "chat-logs");
const stats = {
totalMessages: 0,
+25 -25
View File
@@ -76,6 +76,23 @@ export class ContextStore {
return path.join(this.options.dataDir, `${safe}.summary.json`);
}
private parseJson<T>(raw: string): T | null {
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}
private async readSummary(channel: string): Promise<ContextSummary | null> {
try {
const raw = await fs.readFile(this.summaryFile(channel), "utf-8");
return this.parseJson<ContextSummary>(raw);
} catch {
return null;
}
}
async init(): Promise<void> {
if (this.initialized) return;
await fs.mkdir(this.options.dataDir, { recursive: true });
@@ -109,14 +126,9 @@ export class ContextStore {
// 1. Load compacted summary
let summary = "";
try {
const raw = await fs.readFile(this.summaryFile(channel), "utf-8");
const data: ContextSummary = JSON.parse(raw);
if (data.summaryText) {
summary = `[Résumé des conversations précédentes]\n${data.summaryText}`;
}
} catch {
// No summary yet
const summaryData = await this.readSummary(channel);
if (summaryData?.summaryText) {
summary = `[Résumé des conversations précédentes]\n${summaryData.summaryText}`;
}
// 2. Load recent raw entries
@@ -197,13 +209,9 @@ export class ContextStore {
.join("\n")
.slice(0, 30000); // cap for LLM context
// Load existing summary
let existingSummary = "";
try {
const raw = await fs.readFile(this.summaryFile(channel), "utf-8");
const data: ContextSummary = JSON.parse(raw);
existingSummary = data.summaryText || "";
} catch { /* no previous summary */ }
// Load existing summary once to avoid redundant reads/parses.
const previousSummary = await this.readSummary(channel);
const existingSummary = previousSummary?.summaryText || "";
// Ask LLM to summarize
const prompt = existingSummary
@@ -236,19 +244,11 @@ export class ContextStore {
const summaryData: ContextSummary = {
channel,
summaryText: summaryText.slice(0, this.options.maxSummaryChars),
entriesCompacted: entries.length + (existingSummary ? 1 : 0),
entriesCompacted: (previousSummary?.entriesCompacted || 0) + entries.length,
lastCompactedAt: new Date().toISOString(),
totalCompactions: 1, // increment from existing
totalCompactions: (previousSummary?.totalCompactions || 0) + 1,
};
// Update total compactions
try {
const raw = await fs.readFile(this.summaryFile(channel), "utf-8");
const prev: ContextSummary = JSON.parse(raw);
summaryData.totalCompactions = (prev.totalCompactions || 0) + 1;
summaryData.entriesCompacted = (prev.entriesCompacted || 0) + entries.length;
} catch { /* first compaction */ }
await fs.writeFile(this.summaryFile(channel), JSON.stringify(summaryData, null, 2), "utf-8");
// Replace raw file with only recent entries
+31 -26
View File
@@ -1,5 +1,6 @@
import http from "node:http";
import fs from "node:fs";
import { promises as fsp } from "node:fs";
import path from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
@@ -498,18 +499,20 @@ async function synthesizeTTS(
error?: string;
};
if (result.status === "completed" && fs.existsSync(outputPath)) {
const audioBuffer = fs.readFileSync(outputPath);
const base64 = audioBuffer.toString("base64");
if (result.status === "completed") {
try {
const audioBuffer = await fsp.readFile(outputPath);
const base64 = audioBuffer.toString("base64");
// Broadcast audio to channel
broadcastFn(channel, { type: "audio", nick, data: base64, mimeType: "audio/wav" });
// Broadcast audio to channel
broadcastFn(channel, { type: "audio", nick, data: base64, mimeType: "audio/wav" });
} catch { /* file missing — ignore */ }
}
} catch (err) {
// TTS failure is non-critical, just log
console.error(`[tts] Synthesis failed for ${nick}: ${err instanceof Error ? err.message : String(err)}`);
} finally {
try { fs.unlinkSync(outputPath); } catch { /* ignore cleanup errors */ }
try { await fsp.unlink(outputPath); } catch { /* ignore cleanup errors */ }
}
}
@@ -892,8 +895,8 @@ export function attachWebSocketChat(server: http.Server, options: ChatOptions):
error?: string;
};
if (result.status === "completed" && fs.existsSync(outputPath)) {
const audioBuffer = fs.readFileSync(outputPath);
if (result.status === "completed") {
const audioBuffer = await fsp.readFile(outputPath);
const base64 = audioBuffer.toString("base64");
broadcast(info.channel, {
@@ -911,7 +914,7 @@ export function attachWebSocketChat(server: http.Server, options: ChatOptions):
type: "system",
text: `[Musique generee: "${musicPrompt}"]`,
});
fs.unlinkSync(outputPath);
fsp.unlink(outputPath).catch(() => {});
} else {
send(ws, { type: "system", text: `Composition echouee: ${result.error || "unknown"}` });
}
@@ -1237,7 +1240,7 @@ export function attachWebSocketChat(server: http.Server, options: ChatOptions):
const tmpFile = path.join("/tmp", `kxkm-audio-${Date.now()}.${ext}`);
try {
fs.writeFileSync(tmpFile, buffer);
await fsp.writeFile(tmpFile, buffer);
const scriptPath = path.resolve(
process.env.SCRIPTS_DIR || path.join(process.cwd(), "scripts"),
"transcribe_audio.py",
@@ -1267,36 +1270,38 @@ export function attachWebSocketChat(server: http.Server, options: ChatOptions):
} catch (err) {
analysis = `[Audio: ${filename} — erreur: ${err instanceof Error ? err.message : String(err)}]`;
} finally {
try { fs.unlinkSync(tmpFile); } catch { /* ignore cleanup errors */ }
try { await fsp.unlink(tmpFile); } catch { /* ignore cleanup errors */ }
}
} else if (mimeType === "application/pdf") {
const tmpFile = path.join("/tmp", `kxkm-pdf-${Date.now()}.pdf`);
try {
fs.writeFileSync(tmpFile, buffer);
await fsp.writeFile(tmpFile, buffer);
await acquireFileProcessor();
const pythonBin = process.env.PYTHON_BIN || "python3";
const scriptPath = path.join(process.env.SCRIPTS_DIR || "scripts", "extract_pdf_docling.py");
const { stdout, stderr } = await execFileAsync(pythonBin, [scriptPath, "--input", tmpFile], { timeout: 60_000 });
releaseFileProcessor();
if (stderr) console.log(`[upload] pdf: ${stderr.slice(-200)}`);
const result = JSON.parse(stdout.trim().split("\n").pop() || "{}");
if (result.text) {
analysis = `[PDF: ${filename}, ${result.pages || "?"} page(s)]\n${result.text}`;
} else {
analysis = `[PDF: ${filename} — extraction échouée: ${result.error || "unknown"}]`;
try {
const pythonBin = process.env.PYTHON_BIN || "python3";
const scriptPath = path.join(process.env.SCRIPTS_DIR || "scripts", "extract_pdf_docling.py");
const { stdout, stderr } = await execFileAsync(pythonBin, [scriptPath, "--input", tmpFile], { timeout: 60_000 });
if (stderr) console.log(`[upload] pdf: ${stderr.slice(-200)}`);
const result = JSON.parse(stdout.trim().split("\n").pop() || "{}");
if (result.text) {
analysis = `[PDF: ${filename}, ${result.pages || "?"} page(s)]\n${result.text}`;
} else {
analysis = `[PDF: ${filename} — extraction échouée: ${result.error || "unknown"}]`;
}
} finally {
releaseFileProcessor();
}
} catch (err) {
releaseFileProcessor();
analysis = `[PDF: ${filename} — erreur: ${err instanceof Error ? err.message : String(err)}]`;
} finally {
try { fs.unlinkSync(tmpFile); } catch {}
try { await fsp.unlink(tmpFile); } catch {}
}
} else if (isOfficeDocument(filename, mimeType)) {
// Word, Excel, PowerPoint, LibreOffice, RTF, EPUB
const ext = filename.split(".").pop() || "";
const tmpFile = path.join("/tmp", `kxkm-doc-${Date.now()}.${ext}`);
try {
fs.writeFileSync(tmpFile, buffer);
await fsp.writeFile(tmpFile, buffer);
const pythonBin = process.env.PYTHON_BIN || "python3";
const scriptPath = path.join(process.env.SCRIPTS_DIR || "scripts", "extract_document.py");
await acquireFileProcessor();
@@ -1318,7 +1323,7 @@ export function attachWebSocketChat(server: http.Server, options: ChatOptions):
} catch (err) {
analysis = `[Document: ${filename} — erreur: ${err instanceof Error ? err.message : String(err)}]`;
} finally {
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
try { await fsp.unlink(tmpFile); } catch { /* ignore */ }
}
} else {
analysis = `[Fichier: ${filename}, type: ${mimeType}, ${(size / 1024).toFixed(0)} KB]`;
+4 -1
View File
@@ -16,6 +16,7 @@ import NodeEditor from "./components/NodeEditor";
import TrainingDashboard from "./components/TrainingDashboard";
import Analytics from "./components/Analytics";
import Collectif from "./components/Collectif";
import ErrorBoundary from "./components/ErrorBoundary";
function parseHash(): { page: string; id: string } {
const hash = window.location.hash.replace(/^#\/?/, "");
@@ -188,7 +189,9 @@ export default function App() {
<Nav currentPage={route.page} session={session} onNavigate={navigate} />
<main className="app-main">
{error && <div className="banner">{error}</div>}
{renderPage()}
<ErrorBoundary>
{renderPage()}
</ErrorBoundary>
</main>
</div>
</div>
+1 -1
View File
@@ -495,7 +495,7 @@ export default function Chat() {
</div>
<div className="chat-body">
<div className="chat-messages" ref={messagesContainerRef}>
<div className="chat-messages" ref={messagesContainerRef} role="log" aria-live="polite">
{messages.map((msg) => (
<ChatMessage key={msg.id} msg={msg} getNickColor={getNickColor} channel={channel} />
))}
+59
View File
@@ -0,0 +1,59 @@
import { Component, type ReactNode, type ErrorInfo } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export default class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("[ErrorBoundary]", error, info.componentStack);
}
render() {
if (this.state.hasError) {
if (this.props.fallback) return this.props.fallback;
return (
<div style={{
padding: "2rem",
background: "#1a0a0a",
color: "#ff6b6b",
border: "1px solid #ff6b6b",
fontFamily: "monospace",
margin: "1rem",
}}>
<h2 style={{ margin: "0 0 1rem" }}>{">>> ERREUR SYSTEME <<<"}</h2>
<pre style={{ whiteSpace: "pre-wrap", fontSize: "0.85rem", color: "#ccc" }}>
{this.state.error?.message || "Erreur inconnue"}
</pre>
<button
onClick={() => this.setState({ hasError: false, error: null })}
style={{
marginTop: "1rem",
padding: "0.5rem 1rem",
background: "#333",
color: "#0f0",
border: "1px solid #0f0",
cursor: "pointer",
fontFamily: "monospace",
}}
>
F3=Retour
</button>
</div>
);
}
return this.props.children;
}
}
+126 -28
View File
@@ -1,40 +1,138 @@
# Agents, Sous-agents, Competences
> "L'infrastructure est une decision politique deployee." -- electron rare
## Orchestration
- Agent racine: Coordinateur
- Sous-agent analyse code: Explore (thorough)
- Sous-agent veille OSS: Explore + fetch web
- Cadence: synchroniser PLAN.md + TODO.md + docs/PROJECT_MEMORY.md apres chaque lot
- Agent racine: **Coordinateur** — planifie, arbitre, synchronise PLAN/TODO/docs
- Sous-agents specialises: analyse code, veille OSS, audit securite, optimisation
- Cadence: synchroniser PLAN.md + TODO.md + docs apres chaque lot
## Matrice des agents
## Matrice des agents (lot 17+)
| Agent | Competences | Taches actives | Etat |
| Agent | Competences | Perimetre | Etat |
|---|---|---|---|
| Coordinateur | planification, arbitrage, docs de pilotage | aligner plan/todo/docs avec etat reel | en cours |
| Securite | validation input, hardening runtime, limites | verifier invariants runtime + rate-limit scope | en cours |
| Backend V1/V2 | express/ws/api contracts, ollama integration | contrat storage API/worker, health contract | en cours |
| Node Engine | DAG, queue, runs, sandbox | robustesse pipeline + test parite schema | a enchainer |
| Personas | source/feedback/proposals/pharmacius | clarifier trace editoriale et exports training | a enchainer |
| Frontend | React/Vite, UX IRC, React Flow | consolidations surfaces chat/admin/engine | a enchainer |
| Ops/TUI | scripts, logs, rotate/purge, observabilite | run lot suivant + hygiene logs | en cours |
| Migration | parity, migrate, rollback | rehearsal migration V1->V2 sur Postgres local | a enchainer |
| Coordinateur | planification, arbitrage, docs de pilotage | PLAN.md, TODO.md, AGENTS.md, README.md | actif |
| Securite | validation input, hardening, rate-limit, RBAC | apps/api, ws-chat, packages/auth | veille |
| Backend API | Express, WS, Ollama, RAG, multimodal pipeline | apps/api/src/ | actif |
| Node Engine | DAG, queue, runs, sandbox, training adapters | packages/node-engine, apps/worker | actif |
| Personas | source/feedback/proposals/pharmacius, memoire | packages/persona-domain, ws-chat | actif |
| Frontend | React/Vite, UX Minitel, React Flow, chat, voice | apps/web/src/ | actif |
| Ops/TUI | scripts, logs, rotate/purge, health, audit | ops/v2/, scripts/ | actif |
| Training | DPO, SFT, Unsloth, eval, autoresearch, Ollama import | scripts/, packages/node-engine | actif |
| Multimodal | STT, TTS, vision, PDF, RAG, recherche web | apps/api/src/ws-chat.ts | actif |
| Veille OSS | recherche projets, libs, modeles, benchmarks | docs/OSS_WATCH, docs/HF_MODEL_RESEARCH | periodique |
## Sous-agents et skill routing
- Explore (audit): differences docs vs code, quick wins, risques
- Explore (veille): OSS comparables (ollama-js, bullmq, rete, flowise, node-red, localai)
- Skill resume issue/pr: a utiliser pour synthese PR/issue
- Skill suggest-fix-issue: a utiliser pour proposition de correction ciblee
```mermaid
flowchart TD
Coord[Coordinateur]
## Todo agents immediats
Coord --> SecAgent[Securite]
Coord --> BackAgent[Backend API]
Coord --> EngAgent[Node Engine]
Coord --> PersAgent[Personas]
Coord --> FrontAgent[Frontend]
Coord --> OpsAgent[Ops/TUI]
Coord --> TrainAgent[Training]
Coord --> MultiAgent[Multimodal]
Coord --> OSSAgent[Veille OSS]
- Coordinateur:
- fermer incoherences TODO/PLAN/README
- maintenir priorites lot-3 et lot-4
- Backend:
- maintenir garde-fou DB en prod
- harmoniser communication contrat storage
- Ops/TUI:
- executer lot-3
- analyser logs puis purger logs temporaires
SecAgent --> |audit| SecScan[Pattern scan P0/P1/P2]
SecAgent --> |fix| SecFix[Correctifs chirurgicaux]
BackAgent --> |analyse| APIAudit[Deep analyse app.ts, ws-chat.ts]
BackAgent --> |refactor| APISplit[Extraction modules]
EngAgent --> |test| EngTest[Tests unitaires node-engine]
EngAgent --> |extend| EngNew[Nouveaux node types]
PersAgent --> |pipeline| PersPipe[Editorial pipeline]
PersAgent --> |finetune| PersDPO[DPO + PCL methodology]
FrontAgent --> |ui| FrontUI[Minitel theme CSS]
FrontAgent --> |perf| FrontPerf[Memoization, lazy load]
OpsAgent --> |monitor| OpsHealth[health-check, deep-audit]
OpsAgent --> |deploy| OpsDeploy[Docker, kxkm-ai]
TrainAgent --> |train| TrainRun[Unsloth/TRL runs]
TrainAgent --> |eval| TrainEval[Scoring, registry]
MultiAgent --> |voice| MultiVoice[XTTS-v2, WebRTC]
MultiAgent --> |search| MultiSearch[SearXNG]
OSSAgent --> |web| OSSWeb[Recherche web]
OSSAgent --> |hf| OSSHF[HuggingFace models]
```
## Todo agents (lot 17)
### Coordinateur
- [ ] Consolider PLAN.md avec etat reel (lots 14-16 complets)
- [ ] Mettre a jour TODO.md avec backlog Phase 6+
- [ ] Synchroniser FEATURE_MAP.md matrice
- [ ] Documenter actions dans ops/v2/logs/
### Backend API
- [ ] Refactorer ws-chat.ts (1449 → <400 LOC par module)
- [ ] Refactorer app.ts (1292 → routes + middleware + handlers)
- [ ] Remplacer writeFileSync par async dans ws-chat.ts
- [ ] Ajouter error boundaries sur WebSocket handlers
### Node Engine
- [ ] Ajouter node type `music_generation` (ACE-Step 1.5)
- [ ] Ajouter node type `voice_clone` (XTTS-v2)
- [ ] Tester pipeline DPO end-to-end sur kxkm-ai
### Personas
- [ ] Evaluer PCL (Persona-Aware Contrastive Learning) pour coherence
- [ ] Evaluer OpenCharacter pour generation profils synthetiques
- [ ] Ajouter `/compose` command (generation musicale)
### Frontend
- [ ] Implementer lot 16 UI Minitel rose (phosphore, VIDEOTEX)
- [ ] Ajouter memoization (React.memo) sur composants lourds
- [ ] Lazy-load ChatHistory, VoiceChat, NodeEditor
### Ops/TUI
- [ ] Deployer deep-audit.js sur kxkm-ai
- [ ] Ajouter SearXNG au docker-compose
- [ ] Ajouter MinerU/Docling au docker-compose
### Training
- [ ] Spike BGE-M3 embeddings (remplacer nomic-embed-text)
- [ ] Tester ACE-Step 1.5 sur RTX 4090
- [ ] Evaluer Chatterbox TTS vs Piper
### Veille OSS
- [ ] Suivre LLMRTC (WebRTC voice TypeScript)
- [ ] Suivre A2A Protocol (interop agents)
- [ ] Suivre MCP SDK updates
## Pipeline d'intervention
```mermaid
stateDiagram-v2
[*] --> Analyse: agent lance
Analyse --> Findings: scan code/docs/web
Findings --> Triage: P0/P1/P2 classification
Triage --> Fix_P0: P0 critique
Triage --> Plan_P1: P1 important
Triage --> Backlog_P2: P2 mineur
Fix_P0 --> Test: correction chirurgicale
Plan_P1 --> Test: correction planifiee
Test --> Deploy: tests OK
Deploy --> Log: log + purge
Log --> [*]: cycle termine
Backlog_P2 --> [*]: ajoute au TODO
```
+94
View File
@@ -546,3 +546,97 @@ flowchart TD
| GET | `/api/v2/chat/history` | Liste fichiers de chat logs |
| GET | `/api/v2/chat/history/:date` | Messages d'un jour (pagine) |
| POST | `/api/v2/admin/retention-sweep` | Nettoyage runs anciens |
---
## 13. Context Store & Memoire persistante
Flux de compaction automatique du contexte conversationnel par canal.
```mermaid
flowchart TD
Msg["Message entrant (append)"]
Msg --> JSONL["channel.jsonl (append line)"]
JSONL --> SizeCheck{"Taille > 1 MB\net > 500 entries?"}
SizeCheck -- non --> Done["Fin"]
SizeCheck -- oui --> Split["Split 80/20"]
Split --> Old["80% anciennes entries"]
Split --> Recent["20% recentes entries"]
Old --> Summarize["LLM summarization\n(qwen3.5:9b via Ollama)"]
subgraph Summary["Compaction"]
Summarize --> Merge["Fusionner avec\nsummary existant"]
Merge --> Save["channel.summary.json"]
end
Recent --> Rewrite["Reecrire channel.jsonl\n(entries recentes seules)"]
Save --> Done2["Compaction terminee"]
Rewrite --> Done2
```
### Injection contexte dans le prompt
```mermaid
flowchart LR
Load["getContext(channel)"]
Load --> ReadSummary["Lire channel.summary.json"]
Load --> ReadRecent["Lire channel.jsonl (tail)"]
ReadSummary --> Combine["Combiner:\nResume + Echanges recents"]
ReadRecent --> Combine
Combine --> Cap["Tronquer a maxContextChars\n(default 16K)"]
Cap --> Inject["Injecter dans systemPrompt"]
```
---
## 14. Deploiement Docker
```mermaid
flowchart TD
subgraph Docker["Docker Compose"]
API["api:4180\n(Express + WS + RAG)"]
Worker["worker\n(poll Postgres)"]
Postgres["postgres:5432\n(persistence)"]
V1["v1:3333\n(reference, optionnel)"]
end
subgraph Host["Host (kxkm-ai)"]
Ollama["Ollama:11434\n(natif, RTX 4090)"]
Venv["Python venv\n(PyTorch, faster-whisper,\npiper-tts, Unsloth)"]
end
API --> Postgres
Worker --> Postgres
API --> Ollama
Worker --> Ollama
API --> Venv
Browser["Client Browser"] --> API
Browser --> V1
```
---
## 15. Inter-persona dialogue
```mermaid
sequenceDiagram
participant U as Utilisateur
participant WS as ws-chat
participant P1 as Persona A
participant P2 as Persona B (@mention)
U->>WS: "Hey @Schaeffer, que penses-tu?"
WS->>P1: route vers Schaeffer
P1-->>WS: reponse (mentionne @Radigue)
WS->>WS: detecter @mention dans reponse
WS->>P2: route vers Radigue (depth 2)
P2-->>WS: reponse (depth 2)
WS->>WS: depth >= 3? stop
WS-->>U: broadcast toutes les reponses
```
+141
View File
@@ -0,0 +1,141 @@
# Deep Audit Report — 2026-03-17
> "Le reel n'est jamais propre, mais il est toujours testable." -- electron rare
## Scope
Audit complet du monorepo kxkm_clown + repo mascarade/core.
5 agents paralleles (api, web, packages, mascarade, v1+worker) + TUI deep-audit.js.
## Metriques globales
| Metrique | Valeur |
| --- | --- |
| Erreurs TypeScript | **0** |
| Tests | **119 pass, 0 fail** |
| Security findings P0 | **0** (apres corrections) |
| Fichiers >500 LOC | **8** |
| Packages outdated | **2** |
## Corrections appliquees (chirurgicales)
### apps/api (P0)
| Fix | Sev | Detail |
| --- | --- | --- |
| Semaphore double-release PDF | P0 | try/finally restructure, un seul releaseFileProcessor() |
| Path traversal voice-sample | P0 | path.basename() + boundary check 3 endpoints |
| Analytics sans auth | P1 | requirePermission("ops:read") |
| 5x sync I/O handlers async | P2 | fsp.writeFile/readFile/unlink |
### apps/web (P0)
| Fix | Sev | Detail |
| --- | --- | --- |
| Pas d'Error Boundary | P0 | ErrorBoundary.tsx cree + integre dans App.tsx |
| Chat non accessible | P1 | role="log" aria-live="polite" |
### packages (P0)
| Fix | Sev | Detail |
| --- | --- | --- |
| core createId() non-crypto | P0 | Math.random → crypto.randomBytes(8) hex |
| auth timing leak verifyPassword | P0 | Buffer padding avant timingSafeEqual |
| persona-domain applyPatches unsafe | P0 | Allowlist fields + typeof type guard |
## Findings non-corriges (backlog)
### apps/api — P1
- ws-chat.ts: 1449 LOC (refactoring planifie → 4 modules)
- app.ts: 1292 LOC (extraction routes planifiee)
- Persona memory race condition (concurrent updatePersonaMemory)
- Session expiration not enforced (no periodic cleanup)
- RAG O(n) linear search (candidat HNSW/Annoy)
- Context store unlimited concurrent compactions
### apps/web — P1
- No React.memo on large components (Chat, ChatHistory, VoiceChat)
- No virtualization for message list (max 500 DOM nodes)
- Hash routing fragile (no route guards, no 404)
- Tab completion O(n) filtering on each keystroke
- WebSocket send silently fails if disconnected
- File upload no size validation before base64 encoding
### packages — P1
- **storage: 0 tests** (P0 — couverture critique manquante)
- node-engine sandbox.ts: 0 tests
- node-engine training.ts: 0 tests
- chat-domain channel normalization returns empty string
- persona-domain extractJsonBlock greedy regex
### mascarade (repo companion)
- server.py (52 KB), cluster.py (41 KB): fichiers volumineux
- MCP client FreeCAD sandboxing fragile (blocklist vs AST)
- P2P: deux implementations (asyncio vs libp2p) — maintenance burden
- 190+ settings dans config.py sans validation conflits
## Fichiers complexite elevee
| Fichier | LOC | Status |
| --- | --- | --- |
| apps/api/src/ws-chat.ts | 1449 | refactoring planifie |
| apps/api/src/app.ts | 1292 | refactoring planifie |
| packages/persona-domain/src/index.ts | 782 | acceptable |
| packages/node-engine/src/index.ts | 717 | acceptable |
| apps/web/src/components/ChatHistory.tsx | 602 | a surveiller |
| apps/web/src/components/Chat.tsx | 567 | a surveiller |
| packages/storage/src/index.ts | 551 | acceptable |
| apps/worker/src/index.ts | 548 | acceptable |
## Findings V1 + Worker (agent 5/5)
### V1 Code Quality: GOOD
- 27 modules V1 bien separes (chat-routing, commands, http-api, websocket, storage, etc.)
- smoke.js: 1199 lignes de tests d'integration comprehensive
- Ops TUI (health-check, queue-viewer, persona-manager, log-rotate): EXCELLENT quality
### Issues V1
| Issue | Sev | Detail |
| --- | --- | --- |
| Memory leak channelHistory | P1 | Map grows unbounded, no LRU eviction |
| Unhandled Promise in websocket.js | P1 | Async handlers not awaited in some paths |
| Docker runs as root | P1 | Missing USER node in Dockerfile |
| safeCompare() duplique | P2 | commands.js + http-api.js (extract to util) |
| Dead code workspace-package.js | P2 | **Supprime** (9 lignes, exports nothing) |
| ANSI helpers dupliques | P2 | 4+ scripts ops (consolider dans @kxkm/tui) |
| No graceful shutdown server.js | P2 | Missing SIGTERM/SIGINT handlers |
### Mascarade (repo companion)
- Orchestrateur LLM sophistique: 15 providers, P2P, MCP, fine-tuning
- server.py (52 KB), cluster.py (41 KB): fichiers volumineux
- Integration KXKM: coexistence Docker, partage Ollama, MCP potentiel
## Recommandations prioritaires
### Immediat (lot 17 suite)
1. **Refactoring ws-chat.ts** → ws-multimodal.ts, ws-persona-router.ts, ws-commands.ts
2. **Refactoring app.ts** → routes/personas.ts, routes/node-engine.ts, routes/chat.ts
3. **Tests storage** — couverture minimale repos Postgres
4. **Tests sandbox.ts + training.ts** — couverture node-engine
### Moyen terme (lot 18)
5. SearXNG dans docker-compose
6. Discord Pharmacius bot
7. React.memo + lazy load composants lourds
8. RAG: evaluer BGE-M3 embeddings
### Long terme (lot 19+)
9. Virtualization messages chat (react-window)
10. HNSW index pour RAG search
11. MCP integration personas ↔ outils
12. XTTS-v2 voice cloning
+123
View File
@@ -133,3 +133,126 @@ Sources : [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescrip
- **Pour KXKM** : concept seduisant — chaque persona KXKM pourrait publier un Agent Card et communiquer via A2A. Mais le protocole est encore immature (RC), et le overhead HTTPS/JSON-RPC est lourd pour des personas locales qui partagent deja un bus WebSocket. A surveiller pour interop avec des agents externes, pas pour la communication interne.
Sources : [A2A Specification](https://a2a-protocol.org/latest/specification/) | [Google A2A Blog](https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/) | [A2A GitHub](https://github.com/a2aproject/A2A)
---
## Update 2026-03-17 (deep audit session)
### 4. Voice Cloning & TTS — Alternatives a Piper
| Projet | Signal observe | Usage potentiel |
| --- | --- | --- |
| Coqui XTTS-v2 | Zero-shot voice cloning, 16 langues, reference quality | voix unique par persona via 6s de reference audio |
| OpenVoice v2 | Leger, zero-shot, controle emotion/accent/rythme | alternative rapide pour prototypage voix |
| Chatterbox | #1 trending HuggingFace TTS, bon ratio qualite/vitesse | candidat remplacement Piper |
| StyleTTS2 | Studio-quality narration, diffusion-based | personas "premium" narration longue |
| Bark | Transformer generatif, expressif, non-speech sounds | personas experimentales (rire, chant, bruits) |
**Recommandation** : deployer XTTS-v2 pour le clonage vocal personas (lot Phase 6). Garder Piper pour le fallback leger temps reel.
Sources : [Coqui XTTS-v2](https://huggingface.co/coqui/XTTS-v2) | [OpenVoice](https://github.com/myshell-ai/OpenVoice) | [Chatterbox](https://huggingface.co/resemble-ai/chatterbox) | [Resemble AI Guide](https://www.resemble.ai/best-open-source-ai-voice-cloning-tools/)
### 5. Music Generation — ACE-Step 1.5
| Projet | Signal observe | Usage potentiel |
| --- | --- | --- |
| ACE-Step 1.5 | Commercial-grade, <4GB VRAM, <10s RTX 3090, outperforms Suno v5 | generation musicale Node Engine |
| MusicGen (Meta) | Text/melody → musique, models 1.5B/3.3B | alternative stable si ACE-Step trop lourd |
**ACE-Step 1.5** (janvier 2026) : modele LM 0.6B-4B, Chain-of-Thought song blueprint, lyrics-conditioned. Ideal pour `/compose` command — generer de la musique concrete via prompts textuels.
Sources : [ACE-Step 1.5 GitHub](https://github.com/ace-step/ACE-Step-1.5) | [ACE-Step Paper](https://arxiv.org/abs/2602.00744) | [MusicGen](https://huggingface.co/facebook/musicgen-large)
### 6. PDF Extraction — Alternatives a pdf-parse
| Projet | Signal observe | Usage potentiel |
| --- | --- | --- |
| Docling (IBM) | Layout analysis + TableFormer, open source | remplacer pdf-parse pour tables/layout |
| MinerU | 84 langues OCR, heading detection, tables HTML, formulas | meilleur sur docs complexes |
| Dolphin (ByteDance) | Vision Transformer OCR, layout preservation | alternative pour documents scannes |
| OpenDataLoader | #1 benchmark overall (0.90), bounding boxes | meilleur precision globale |
**Recommandation** : MinerU comme premier choix (meilleur sur tables complexes + OCR 84 langues). Docling en fallback. OpenDataLoader a surveiller.
Sources : [Docling GitHub](https://github.com/docling-project/docling) | [MinerU](https://github.com/opendatalab/PDF-Extract-Kit) | [Benchmark](https://jimmysong.io/blog/pdf-to-markdown-open-source-deep-dive/)
### 7. SearXNG — Moteur de recherche self-hosted
- **Docker** : `searxng/searxng` image officielle, port 8080, ~300MB, tourne sur Raspberry Pi 4
- **Config** : `settings.yml` pour activer/desactiver les engines, format JSON API
- **Integration KXKM** : remplacer DuckDuckGo Lite scraping par `http://searxng:8080/search?q=X&format=json`
- **Docker Compose** : ajouter service `searxng` avec volume config persistant
Sources : [SearXNG Docker](https://github.com/searxng/searxng-docker) | [SearXNG Docs](https://docs.searxng.org/admin/installation-docker.html)
### 8. RAG Local — Libs TypeScript
| Projet | Signal observe | Usage potentiel |
| --- | --- | --- |
| embedJs | Node.js RAG framework, LLM-agnostic, embeddings | remplacement RAG custom si trop complexe |
| LangChain.js | Vector stores, document loaders, splitters, TypeScript | framework RAG complet, overhead |
**Embedding models recommandes** :
- `nomic-embed-text` (actuel) — bon rapport qualite/taille
- `BGE-M3` — meilleur score MTEB (63.0), MIT license, production-ready
- `all-minilm:l6-v2` — ultra-leger pour prototypage
**Recommandation** : garder le RAG custom actuel (simple, pas d'overhead framework). Evaluer BGE-M3 comme upgrade du modele d'embedding.
### 9. WebRTC Voice — Streaming temps reel
| Projet | Signal observe | Usage potentiel |
| --- | --- | --- |
| LLMRTC | TypeScript SDK, WebRTC, provider-agnostic, open source | SDK ideal pour WebRTC voice KXKM |
| FastRTC (HuggingFace) | Python, real-time audio/video, VAD, Gradio | backend Python pour STT/TTS streaming |
| Pipecat | Orchestrator frames, local LLM + STT + TTS, WebRTC | architecture reference voice pipeline |
**LLMRTC** est le meilleur candidat : TypeScript natif, WebRTC low-latency, VAD, barge-in, sentence-boundary TTS. Integration directe dans le WebSocket existant.
Sources : [LLMRTC](https://www.llmrtc.org/) | [FastRTC](https://huggingface.co/blog/fastrtc) | [Pipecat Voice Agent](https://webrtc.ventures/2026/02/building-an-open-source-voice-ai-agent-that-avoids-vendor-lock-in/)
### 10. Persona Fine-Tuning — Methodologies 2026
| Methode | Source | Application |
| --- | --- | --- |
| OpenCharacter | arXiv 2501.15427, large-scale synthetic personas | generation de profils persona synthetiques |
| PCL (Persona-Aware Contrastive Learning) | arXiv 2503.17662, annotation-free | ameliorer la coherence de role sans annotations |
| PIKA | arXiv 2510.06670, expert-level synthetic datasets | datasets post-training alignment |
| Constitutional AI for personas | arXiv 2511.01689, introspective data pipeline | shaping persona via constitution, pas prompt |
**Recommandation** : combiner OpenCharacter (generation profils) + PCL (consistency DPO) pour le pipeline fine-tune personas KXKM. Le DPO actuel est deja aligne — ajouter une passe PCL contrastive pour renforcer la coherence de role.
Sources : [OpenCharacter](https://arxiv.org/html/2501.15427v1) | [PCL](https://arxiv.org/html/2503.17662v1) | [PIKA](https://arxiv.org/html/2510.06670v1)
### 11. IRC Aesthetic Chat — Projets proches
| Projet | Signal observe | Usage potentiel |
| --- | --- | --- |
| Ollamarama-IRC | AI chatbot IRC, Ollama, multi-personalities | reference IRC + multi-persona local |
| Soulshack | IRC bot, multi-provider, MCP tools, shell access | reference IRC + tool-calling |
Sources : [Ollamarama-IRC](https://github.com/h1ddenpr0cess20/ollamarama-irc) | [Soulshack](https://github.com/pkdindustries/soulshack)
---
## Recommandations consolidees 2026-03-17
### Actions immediates (lot courant)
1. **SearXNG** : ajouter au docker-compose, remplacer DuckDuckGo scraping
2. **MinerU/Docling** : remplacer pdf-parse pour extraction PDF avancee
3. **BGE-M3** : tester comme upgrade embedding model RAG
### Moyen terme (lots 17-18)
1. **XTTS-v2** : deployer voice cloning pour personas
2. **LLMRTC** : spike WebRTC voice streaming
3. **ACE-Step 1.5** : integrer `/compose` dans Node Engine
4. **MCP SDK** : personas comme MCP servers (tool-calling)
### Long terme (recherche)
1. **PCL + OpenCharacter** : pipeline fine-tune persona avance
2. **Chatterbox** : evaluer comme remplacement Piper (meilleur qualite)
3. **A2A Protocol** : interop agents externes quand le protocole est stable
+65 -2
View File
@@ -1,23 +1,86 @@
# PLAN (kxkm-clown-v2)
Updated: 2026-03-16T06:48:45Z
Updated: 2026-03-17T08:45:00Z
## lot-0-cadrage [done]
- Description: Docs, architecture, feature map, agents, invariants, orchestration
- Depends on: none
## lot-1-socle [done]
- Description: Workspace V2, packages, scripts TUI, verification
- Depends on: lot-0-cadrage
## lot-2-domaines [done]
- Description: Auth, chat, storage, personas, node engine
- Depends on: lot-1-socle
## lot-3-surfaces [done]
- Description: Shell React/Vite, admin, chat, node engine, ops
- Depends on: lot-2-domaines
## lot-4-bascule [done]
- Description: Migration, parité, rollback, bascule
- Description: Migration, parite, rollback, bascule
- Depends on: lot-3-surfaces
## lot-5-production [done]
- Description: Training adapters, sandboxing, tests, turborepo, CI/CD, GitHub
- Depends on: lot-4-bascule
## lot-6-consolidation [done]
- Description: Deep analyse, correctifs securite, feature parity V2, deploy
- Depends on: lot-5-production
## lot-7-training [done]
- Description: PyTorch, Unsloth, TRL, DPO pipeline, autoresearch, Ollama import
- Depends on: lot-6-consolidation
## lot-8-multimodal [done]
- Description: RAG, STT, TTS, vision, PDF, recherche web, memoire persona
- Depends on: lot-7-training
## lot-9-chat-avance [done]
- Description: Chat vocal, inter-persona, multi-channel, analytics, 26 personas
- Depends on: lot-8-multimodal
## lot-10-generation [done]
- Description: ComfyUI, Sherlock, Picasso, diversification modeles, contexte 750MB
- Depends on: lot-9-chat-avance
## lot-11-mcp-personas [done]
- Description: MCP tool-calling personas, pipeline fine-tune
- Depends on: lot-10-generation
## lot-12-deep-audit [in-progress]
- Description: Deep audit code, refactoring, veille OSS, diagrammes, infrastructure
- Depends on: lot-11-mcp-personas
- Deliverables:
- ops/v2/deep-audit.js (TUI security/perf/complexity)
- docs/OSS_WATCH enrichi (voice, music, PDF, WebRTC, MCP, persona fine-tune)
- docs/ARCHITECTURE.md (3 Mermaid ajoutes)
- docs/AGENTS.md (matrice 10 agents, pipeline intervention)
- PLAN.md + TODO.md consolides
- Refactoring ws-chat.ts + app.ts
- SearXNG + MinerU/Docling docker
## lot-13-voice-mcp [planned]
- Description: XTTS-v2 voice cloning, LLMRTC WebRTC, MCP SDK, Discord Pharmacius
- Depends on: lot-12-deep-audit
## lot-14-music-creative [planned]
- Description: ACE-Step 1.5, /compose, Flux 2, A2A Protocol
- Depends on: lot-13-voice-mcp
+102 -16
View File
@@ -1,28 +1,114 @@
# TODO (kxkm-clown-v2)
Updated: 2026-03-16T06:48:45Z
Updated: 2026-03-17T08:45:00Z
## lot-0-cadrage
- [x] architecture (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-0-cadrage/architecture.csv
- [x] docs (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-0-cadrage/docs.csv
- [x] agents (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-0-cadrage/agents.csv
- [x] architecture (done)
- [x] docs (done)
- [x] agents (done)
## lot-1-socle
- [x] workspace (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-1-socle/workspace.csv
- [x] verification (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-1-socle/verification.csv
- [x] ops-tui (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-1-socle/ops-tui.csv
- [x] workspace (done)
- [x] verification (done)
- [x] ops-tui (done)
## lot-2-domaines
- [x] auth-chat (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-2-domaines/auth-chat.csv
- [x] personas (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-2-domaines/personas.csv
- [x] node-engine (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-2-domaines/node-engine.csv
- [x] auth-chat (done)
- [x] personas (done)
- [x] node-engine (done)
## lot-3-surfaces
- [x] web-shell (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-3-surfaces/web-shell.csv
- [x] persona-ui (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-3-surfaces/persona-ui.csv
- [x] engine-ui (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-3-surfaces/engine-ui.csv
- [x] web-shell (done)
- [x] persona-ui (done)
- [x] engine-ui (done)
## lot-4-bascule
- [x] migration (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-4-bascule/migration.csv
- [x] parity (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-4-bascule/parity.csv
- [x] cutover (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-4-bascule/cutover.csv
- [x] migration (done)
- [x] parity (done)
- [x] cutover (done)
## lot-5-production
- [x] training-adapters (done)
- [x] sandboxing (done)
- [x] tests (done)
- [x] turborepo (done)
- [x] ci-cd (done)
## lot-6-consolidation
- [x] deep-analyse (done)
- [x] security-fixes (done)
- [x] feature-parity (done)
- [x] deploy (done)
## lot-7-training
- [x] pytorch-unsloth (done)
- [x] dpo-pipeline (done)
- [x] autoresearch (done)
- [x] ollama-import (done)
## lot-8-multimodal
- [x] rag-local (done)
- [x] stt-whisper (done)
- [x] tts-piper (done)
- [x] vision-analysis (done)
- [x] pdf-extraction (done)
- [x] web-search (done)
- [x] persona-memory (done)
## lot-9-chat-avance
- [x] voice-chat (done)
- [x] inter-persona (done)
- [x] multi-channel (done)
- [x] analytics (done)
- [x] 26-personas (done)
## lot-10-generation
- [x] comfyui (done)
- [x] sherlock-picasso (done)
- [x] model-diversification (done)
- [x] context-750mb (done)
## lot-11-mcp-personas
- [x] mcp-tool-calling (done)
- [x] pipeline-finetune (done)
## lot-12-deep-audit
- [x] deep-audit-tui (done) | ops/v2/deep-audit.js
- [x] oss-watch-enriched (done) | docs/OSS_WATCH_2026-03-16.md
- [x] mermaid-diagrams (done) | docs/ARCHITECTURE.md
- [x] agents-matrix (done) | docs/AGENTS.md
- [x] plan-todo-consolidation (done) | PLAN.md + TODO.md
- [ ] deep-analyse-agents (in-progress) | 5 agents paralleles
- [ ] refactor-ws-chat (pending) | 1449 LOC → 4 modules
- [ ] refactor-app-ts (pending) | 1292 LOC → routes + middleware
- [ ] searxng-docker (pending) | docker-compose service
- [ ] mineru-docling (pending) | remplacer pdf-parse
- [ ] discord-pharmacius (pending) | bot Discord orchestrateur
- [ ] bge-m3-spike (pending) | upgrade embeddings RAG
## lot-13-voice-mcp
- [ ] xtts-v2-voice-cloning (planned)
- [ ] llmrtc-webrtc (planned)
- [ ] mcp-sdk-integration (planned)
- [ ] pcl-opencharacter (planned)
## lot-14-music-creative
- [ ] ace-step-production (planned)
- [ ] compose-command (planned)
- [ ] flux2-comfyui (planned)
- [ ] a2a-protocol (planned)
+475
View File
@@ -0,0 +1,475 @@
#!/usr/bin/env node
// Deep Audit TUI — analyse code quality, security, perf across the monorepo
// Usage: node ops/v2/deep-audit.js [--json] [--watch] [--fix] [--help]
//
// Checks:
// 1. TypeScript compilation (apps + packages)
// 2. Security patterns (path traversal, injection, SSRF)
// 3. Performance anti-patterns (sync I/O, missing cache, unbounded)
// 4. Dead code detection (unused exports)
// 5. Dependency health (outdated, duplicates)
// 6. Test coverage summary
// 7. File size / complexity metrics
const fs = require("fs");
const path = require("path");
const { execSync, exec } = require("child_process");
// ---------------------------------------------------------------------------
// ANSI helpers
// ---------------------------------------------------------------------------
const NO_COLOR = !!process.env.NO_COLOR;
const c = {
green: (s) => NO_COLOR ? s : `\x1b[32m${s}\x1b[0m`,
red: (s) => NO_COLOR ? s : `\x1b[31m${s}\x1b[0m`,
yellow: (s) => NO_COLOR ? s : `\x1b[33m${s}\x1b[0m`,
cyan: (s) => NO_COLOR ? s : `\x1b[36m${s}\x1b[0m`,
magenta: (s) => NO_COLOR ? s : `\x1b[35m${s}\x1b[0m`,
bold: (s) => NO_COLOR ? s : `\x1b[1m${s}\x1b[0m`,
dim: (s) => NO_COLOR ? s : `\x1b[2m${s}\x1b[0m`,
};
function stripAnsi(s) {
return s.replace(/\x1b\[[0-9;]*m/g, "");
}
function dot(ok) {
return ok ? c.green("●") : c.red("●");
}
function warn() {
return c.yellow("▲");
}
// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------
const args = process.argv.slice(2);
if (args.includes("--help") || args.includes("-h")) {
console.log(`
${c.bold("KXKM Deep Audit TUI")}
Usage: node ops/v2/deep-audit.js [options]
Options:
--json Output results as JSON (pipe-friendly)
--watch Re-run every 30 seconds
--verbose Show detailed findings
--help Show this help message
Checks:
TypeScript compilation, security patterns, performance anti-patterns,
dead code, dependency health, test results, file metrics.
Logs: ops/v2/logs/deep-audit-YYYY-MM-DD.log
`);
process.exit(0);
}
const FLAG_JSON = args.includes("--json");
const FLAG_WATCH = args.includes("--watch");
const FLAG_VERBOSE = args.includes("--verbose");
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const ROOT_DIR = path.resolve(__dirname, "../..");
const LOG_DIR = path.join(__dirname, "logs");
const APPS = ["apps/api", "apps/web", "apps/worker"];
const PACKAGES = ["packages/core", "packages/auth", "packages/chat-domain",
"packages/persona-domain", "packages/node-engine", "packages/storage",
"packages/ui", "packages/tui"];
// ---------------------------------------------------------------------------
// Logging
// ---------------------------------------------------------------------------
function ensureLogDir() {
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
}
function logFile() {
const d = new Date().toISOString().slice(0, 10);
return path.join(LOG_DIR, `deep-audit-${d}.log`);
}
function log(msg) {
const ts = new Date().toISOString();
const line = `[${ts}] ${stripAnsi(msg)}\n`;
fs.appendFileSync(logFile(), line, "utf-8");
}
// ---------------------------------------------------------------------------
// Checks
// ---------------------------------------------------------------------------
function runCmd(cmd, cwd = ROOT_DIR) {
try {
return { ok: true, out: execSync(cmd, { cwd, encoding: "utf-8", timeout: 60_000, stdio: ["pipe", "pipe", "pipe"] }).trim() };
} catch (e) {
return { ok: false, out: (e.stdout || "") + (e.stderr || "") };
}
}
// 1. TypeScript compilation
function checkTypeScript() {
const results = [];
for (const dir of [...APPS, ...PACKAGES]) {
const tsconfig = path.join(ROOT_DIR, dir, "tsconfig.json");
if (!fs.existsSync(tsconfig)) {
results.push({ dir, status: "skip", errors: 0, detail: "no tsconfig.json" });
continue;
}
const r = runCmd(`npx tsc --noEmit --pretty false 2>&1 | head -50`, path.join(ROOT_DIR, dir));
const errCount = (r.out.match(/error TS/g) || []).length;
results.push({ dir, status: errCount === 0 ? "ok" : "fail", errors: errCount, detail: r.out.slice(0, 500) });
}
return results;
}
// 2. Security pattern scan
function checkSecurity() {
const patterns = [
{ name: "eval()", pattern: /\beval\s*\(/g, severity: "P0" },
{ name: "child_process unsanitized", pattern: /exec\(\s*[`"'].*\$\{/g, severity: "P0" },
{ name: "path.join user input", pattern: /path\.join\(.*req\.(params|query|body)/g, severity: "P1" },
{ name: "innerHTML assignment", pattern: /\.innerHTML\s*=/g, severity: "P1" },
{ name: "dangerouslySetInnerHTML", pattern: /dangerouslySetInnerHTML/g, severity: "P1" },
{ name: "TODO security", pattern: /TODO.*secur|FIXME.*secur|HACK.*secur/gi, severity: "P2" },
{ name: "hardcoded secret", pattern: /(password|secret|token)\s*[:=]\s*["'][^"']{8,}/gi, severity: "P1" },
{ name: "fs.writeFileSync in handler", pattern: /writeFileSync/g, severity: "P2" },
];
const findings = [];
const dirs = [...APPS, ...PACKAGES].map(d => path.join(ROOT_DIR, d, "src"));
for (const dir of dirs) {
if (!fs.existsSync(dir)) continue;
const files = getAllFiles(dir, [".ts", ".tsx", ".js"]);
for (const file of files) {
const content = fs.readFileSync(file, "utf-8");
const lines = content.split("\n");
const relPath = path.relative(ROOT_DIR, file);
for (const p of patterns) {
if (
p.name === "hardcoded secret" &&
(/(^|\/)tests?\//.test(relPath) || /\.test\.[tj]sx?$/.test(relPath))
) {
continue;
}
let match;
const regex = new RegExp(p.pattern.source, p.pattern.flags);
while ((match = regex.exec(content)) !== null) {
const lineNum = content.slice(0, match.index).split("\n").length;
const snippet = lines[lineNum - 1]?.trim().slice(0, 120) || "";
if (p.name === "hardcoded secret" && snippet.includes("process.env.")) {
continue;
}
findings.push({
severity: p.severity,
pattern: p.name,
file: relPath,
line: lineNum,
snippet,
});
}
}
}
}
return findings.sort((a, b) => a.severity.localeCompare(b.severity));
}
// 3. Performance anti-patterns
function checkPerformance() {
const patterns = [
{ name: "readFileSync in non-init", pattern: /readFileSync/g, severity: "P1" },
{ name: "JSON.parse without try/catch", pattern: /JSON\.parse\(/g, severity: "P2", checkCatch: true },
{ name: "unbounded array push", pattern: /\.push\([^)]+\).*(?:while|for)\b/g, severity: "P2" },
{ name: "console.log in production", pattern: /console\.log\(/g, severity: "P2" },
];
const findings = [];
const dirs = APPS.map(d => path.join(ROOT_DIR, d, "src"));
for (const dir of dirs) {
if (!fs.existsSync(dir)) continue;
const files = getAllFiles(dir, [".ts", ".tsx"]);
for (const file of files) {
const content = fs.readFileSync(file, "utf-8");
const lines = content.split("\n");
for (const p of patterns) {
const regex = new RegExp(p.pattern.source, p.pattern.flags);
let match;
while ((match = regex.exec(content)) !== null) {
const lineNum = content.slice(0, match.index).split("\n").length;
if (p.name === "JSON.parse without try/catch" && hasNearbyTry(lines, lineNum)) {
continue;
}
findings.push({
severity: p.severity,
pattern: p.name,
file: path.relative(ROOT_DIR, file),
line: lineNum,
snippet: lines[lineNum - 1]?.trim().slice(0, 120) || "",
});
}
}
}
}
return findings;
}
function hasNearbyTry(lines, lineNum) {
const start = Math.max(0, lineNum - 8);
for (let i = lineNum - 1; i >= start; i--) {
const line = lines[i].trim();
if (!line) continue;
if (/^try\b/.test(line) || /\btry\s*\{/.test(line)) return true;
if (/^function\b|=>\s*\{|^for\b|^while\b/.test(line)) break;
}
return false;
}
// 4. File metrics
function checkMetrics() {
const metrics = [];
const dirs = [...APPS, ...PACKAGES].map(d => path.join(ROOT_DIR, d, "src"));
for (const dir of dirs) {
if (!fs.existsSync(dir)) continue;
const files = getAllFiles(dir, [".ts", ".tsx"]);
for (const file of files) {
const content = fs.readFileSync(file, "utf-8");
const lineCount = content.split("\n").length;
if (lineCount > 200) {
metrics.push({
file: path.relative(ROOT_DIR, file),
lines: lineCount,
sizeKB: Math.round(fs.statSync(file).size / 1024),
flag: lineCount > 500 ? "large" : lineCount > 300 ? "medium" : "ok",
});
}
}
}
return metrics.sort((a, b) => b.lines - a.lines);
}
// 5. Test results summary
function checkTests() {
const r = runCmd("npm run test:v2 2>&1 | tail -20");
const passMatch = r.out.match(/(\d+)\s+pass/i);
const failMatch = r.out.match(/(\d+)\s+fail/i);
return {
status: r.ok ? "ok" : "fail",
pass: passMatch ? parseInt(passMatch[1]) : 0,
fail: failMatch ? parseInt(failMatch[1]) : 0,
output: r.out.slice(-500),
};
}
// 6. Dependency check
function checkDeps() {
const r = runCmd("npm outdated --json 2>/dev/null || true");
try {
const outdated = JSON.parse(r.out || "{}");
return Object.entries(outdated).map(([pkg, info]) => ({
package: pkg,
current: info.current,
wanted: info.wanted,
latest: info.latest,
})).slice(0, 20);
} catch {
return [];
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function getAllFiles(dir, exts) {
const results = [];
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules" && entry.name !== "dist") {
results.push(...getAllFiles(full, exts));
} else if (entry.isFile() && exts.some(ext => entry.name.endsWith(ext))) {
results.push(full);
}
}
} catch { /* permission denied or missing */ }
return results;
}
// ---------------------------------------------------------------------------
// Display
// ---------------------------------------------------------------------------
function drawBox(title, content) {
const lines = content.split("\n");
const maxLen = Math.max(title.length + 4, ...lines.map(l => stripAnsi(l).length));
const w = Math.min(maxLen + 4, process.stdout.columns || 120);
const hr = "─".repeat(w - 2);
console.log(`${hr}`);
console.log(`${c.bold(c.cyan(title))}${" ".repeat(Math.max(0, w - stripAnsi(title).length - 4))}`);
console.log(`${hr}`);
for (const line of lines) {
const pad = Math.max(0, w - stripAnsi(line).length - 4);
console.log(`${line}${" ".repeat(pad)}`);
}
console.log(`${hr}`);
}
function formatSeverity(s) {
if (s === "P0") return c.red(c.bold("P0"));
if (s === "P1") return c.yellow("P1");
return c.dim("P2");
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function run() {
ensureLogDir();
const startTime = Date.now();
const ts = new Date().toISOString();
log(`=== Deep Audit started at ${ts} ===`);
if (!FLAG_JSON) {
console.clear();
console.log(c.bold(c.magenta("\n ╔══════════════════════════════════╗")));
console.log(c.bold(c.magenta(" ║ KXKM Deep Audit — 3615-KXKM ║")));
console.log(c.bold(c.magenta(" ╚══════════════════════════════════╝\n")));
}
const results = {};
// --- Security scan ---
if (!FLAG_JSON) process.stdout.write(c.dim(" Scanning security patterns... "));
const secFindings = checkSecurity();
results.security = secFindings;
const secP0 = secFindings.filter(f => f.severity === "P0").length;
const secP1 = secFindings.filter(f => f.severity === "P1").length;
if (!FLAG_JSON) {
console.log(`${secP0 === 0 ? dot(true) : dot(false)} ${secP0} P0, ${secP1} P1, ${secFindings.length} total`);
log(`Security: ${secP0} P0, ${secP1} P1, ${secFindings.length} total`);
if (FLAG_VERBOSE && secFindings.length > 0) {
const lines = secFindings.slice(0, 15).map(f =>
` ${formatSeverity(f.severity)} ${c.dim(f.file)}:${f.line}${f.pattern}`
).join("\n");
drawBox("Security Findings", lines);
}
}
// --- Performance scan ---
if (!FLAG_JSON) process.stdout.write(c.dim(" Scanning performance patterns... "));
const perfFindings = checkPerformance();
results.performance = perfFindings;
if (!FLAG_JSON) {
console.log(`${warn()} ${perfFindings.length} findings`);
log(`Performance: ${perfFindings.length} findings`);
}
// --- File metrics ---
if (!FLAG_JSON) process.stdout.write(c.dim(" Computing file metrics... "));
const metrics = checkMetrics();
results.metrics = metrics;
const largeFiles = metrics.filter(m => m.flag === "large").length;
if (!FLAG_JSON) {
console.log(`${largeFiles > 0 ? warn() : dot(true)} ${metrics.length} files >200 LOC, ${largeFiles} >500 LOC`);
log(`Metrics: ${metrics.length} files >200 LOC, ${largeFiles} >500 LOC`);
if (FLAG_VERBOSE && metrics.length > 0) {
const lines = metrics.slice(0, 10).map(m =>
` ${m.flag === "large" ? c.red("●") : c.yellow("●")} ${c.dim(m.file)}${m.lines} lines (${m.sizeKB} KB)`
).join("\n");
drawBox("Largest Files", lines);
}
}
// --- Deps ---
if (!FLAG_JSON) process.stdout.write(c.dim(" Checking dependencies... "));
const deps = checkDeps();
results.deps = deps;
if (!FLAG_JSON) {
console.log(`${deps.length > 5 ? warn() : dot(true)} ${deps.length} outdated`);
log(`Dependencies: ${deps.length} outdated`);
}
// --- TypeScript ---
if (!FLAG_JSON) process.stdout.write(c.dim(" TypeScript compilation... "));
const tsResults = checkTypeScript();
results.typescript = tsResults;
const tsErrors = tsResults.reduce((sum, r) => sum + r.errors, 0);
if (!FLAG_JSON) {
console.log(`${tsErrors === 0 ? dot(true) : dot(false)} ${tsErrors} errors across ${tsResults.length} packages`);
log(`TypeScript: ${tsErrors} errors`);
}
// --- Summary ---
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
if (FLAG_JSON) {
console.log(JSON.stringify(results, null, 2));
} else {
console.log("");
const summaryLines = [
`${c.bold("Security")}: ${secP0 === 0 ? c.green("0 P0") : c.red(`${secP0} P0`)} ${secP1} P1 ${secFindings.length} total`,
`${c.bold("Performance")}: ${perfFindings.length} anti-patterns detected`,
`${c.bold("Complexity")}: ${largeFiles} files >500 LOC, ${metrics.length} files >200 LOC`,
`${c.bold("TypeScript")}: ${tsErrors === 0 ? c.green("0 errors") : c.red(`${tsErrors} errors`)}`,
`${c.bold("Deps")}: ${deps.length} outdated packages`,
``,
`${c.dim(`Completed in ${elapsed}s — log: ${path.relative(ROOT_DIR, logFile())}`)}`,
].join("\n");
drawBox("Audit Summary", summaryLines);
}
log(`=== Deep Audit completed in ${elapsed}s ===`);
// Clean up old logs (keep 7 days)
cleanOldLogs(7);
}
function cleanOldLogs(maxDays) {
try {
const files = fs.readdirSync(LOG_DIR);
const cutoff = Date.now() - maxDays * 86400000;
for (const file of files) {
if (!file.startsWith("deep-audit-")) continue;
const filePath = path.join(LOG_DIR, file);
const stat = fs.statSync(filePath);
if (stat.mtimeMs < cutoff) {
fs.unlinkSync(filePath);
log(`Cleaned old log: ${file}`);
}
}
} catch { /* ignore */ }
}
// ---------------------------------------------------------------------------
// Entry
// ---------------------------------------------------------------------------
if (FLAG_WATCH) {
const interval = 30_000;
run();
setInterval(run, interval);
} else {
run().catch(err => {
console.error(c.red(`Fatal: ${err.message}`));
process.exit(1);
});
}
+7 -2
View File
@@ -58,8 +58,13 @@ export async function verifyPassword(password: string, hash: string): Promise<bo
if (!salt || !storedHex) return false;
const storedKey = Buffer.from(storedHex, "hex");
const derived = await scryptAsync(password, salt, SCRYPT_KEYLEN);
if (storedKey.length !== derived.length) return false;
return timingSafeEqual(storedKey, derived);
// Pad to equal length to prevent timing leak on corrupted stored hashes
const maxLen = Math.max(storedKey.length, derived.length);
const a = Buffer.alloc(maxLen);
const b = Buffer.alloc(maxLen);
storedKey.copy(a);
derived.copy(b);
return storedKey.length === derived.length && timingSafeEqual(a, b);
}
/* ------------------------------------------------------------------ */
+4 -1
View File
@@ -71,7 +71,10 @@ export function createIsoTimestamp(date = new Date()): string {
}
export function createId(prefix: string): string {
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
const bytes = globalThis.crypto?.getRandomValues
? globalThis.crypto.getRandomValues(new Uint8Array(8))
: require("node:crypto").randomBytes(8);
return `${prefix}_${Buffer.from(bytes).toString("hex")}`;
}
export function isUserRole(value: string): value is UserRole {
+7 -2
View File
@@ -621,10 +621,15 @@ export function parsePharmaciusResponse(raw: string, personaId: string): Persona
*/
export function applyPatches(persona: PersonaRecord, patches: PersonaPatch[]): PersonaRecord {
const result = { ...persona } as Record<string, unknown> & PersonaRecord;
const allowedFields = new Set<string>(["name", "model", "summary", "editable", "enabled"]);
for (const patch of patches) {
if (patch.field in persona) {
result[patch.field] = patch.after;
if (allowedFields.has(patch.field) && patch.field in persona) {
// Type guard: only apply if value type matches existing field type
const existing = (persona as unknown as Record<string, unknown>)[patch.field];
if (existing === undefined || typeof patch.after === typeof existing) {
result[patch.field] = patch.after;
}
}
}
-9
View File
@@ -1,9 +0,0 @@
const scope = process.argv[2] || "unknown";
const action = process.argv[3] || "status";
console.log(JSON.stringify({
ok: true,
scope,
action,
message: "V2 scaffold present. Real toolchain wiring is tracked in TODO.md.",
}));