diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b970d88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ + +# Fichiers et dossiers à ignorer pour Kill_LIFE + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.env +.venv +venv/ +ENV/ + +# PlatformIO +.pio/ +.pioenvs/ +.build/ + +# VSCode +.vscode/ +.history/ + +# MacOS +.DS_Store +.AppleDouble +.LSOverride + +# Logs et artefacts +*.log +artifacts/ + +# Fichiers générés ou temporaires +*.tmp +*.bak +*.swp +*.swo +*.orig + +# Documentation générée +site/ + +# Assets prompts (spécifique projet) +KIKIFOU/assets_prompts.md + +# Secrets (à adapter si besoin) +.env.local +.env.*.local + +# Node/npm (si utilisé) +node_modules/ + +# Docker +*.pid +docker-compose.override.yml + +# Autres outils CI/CD +coverage/ +*.cover +*.coverage + +# Fichiers de patchs temporaires +*.patch +*.rej + +# Fichiers de configuration utilisateur +*.user +*.sublime-workspace +*.sublime-project + +# Fichiers de sauvegarde d'éditeurs +*~ + +# Fichiers de test +firmware/test/.pioenvs/ +firmware/test/.pio/ diff --git a/README.md b/README.md index cb43e39..730ecce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kill_LIFE 🚀 — AI-Native Embedded Project Template -![Kill_LIFE Banner](https://raw.githubusercontent.com/electron-rare/Kill_LIFE/main/docs/assets/banner_kill_life.png) +![Kill_LIFE Banner](docs/assets/banner_kill_life_generated.png) Bienvenue dans **Kill_LIFE**, le template open source pour systèmes embarqués IA, où les specs rêvent d’électron-ironique, les agents QA se prennent pour des réplicants (voir [Le Réplicant](https://fr.wikipedia.org/wiki/Blade_Runner)), et le pipeline ne panique jamais grâce à sa serviette (voir [Don’t Panic](https://hitchhikers.fandom.com/wiki/Don%27t_Panic)), même en cas d’apocalypse. Ici, chaque commit est validé, chaque gate est passé, et chaque evidence pack est rangé comme dans [le meilleur des mondes](https://fr.wikipedia.org/wiki/Le_Meilleur_des_mondes). Si tu cherches la réponse à la question ultime du développement embarqué IA, c’est probablement [42](https://fr.wikipedia.org/wiki/42_(nombre))… ou un bulk edit réussi ([Bulk Edit Party FX](https://www.youtube.com/watch?v=dQw4w9WgXcQ)). @@ -265,15 +265,21 @@ A : Ajoute des profils, améliore les scripts, enrichis les standards, et suis l --- + ## 🥚 Easter Eggs Kill_LIFE - **Ne panique jamais** : ![Don’t Panic](docs/assets/dont_panic_generated.png) [Playlist apocalypse](https://www.youtube.com/playlist?list=PLApocalypse42) - **Bulk Edit Party** : [Fête technique](https://www.youtube.com/watch?v=dQw4w9WgXcQ) — robots IA dansant sur du code PCB. - **Citation dystopique** : _« J’ai vu des evidence packs briller dans l’obscurité près des gates S1… »_ -- **Badge 42** : ![Badge 42](docs/assets/badge_42_generated.png) — clique pour une blague mathématique. +- **Badge 42** : ![Badge 42](docs/assets/badge_42_generated.gif) — clique pour une blague mathématique. - **Mini-jeu Gate Runner** : [Gate Runner](https://gate-runner.example.com) — passe les gates, évite les bugs. - **Le meilleur des mondes** : Générateur de phrases dystopiques pour motiver les contributeurs. -- **OpenClaw interactif** : ![OpenClaw](docs/assets/openclaw_interactif_generated.png) — change de couleur selon le statut CI/CD. +- **OpenClaw interactif** : + - ![OpenClaw CI/CD Success](docs/assets/openclaw_cicd_success.png) + - ![OpenClaw CI/CD Running](docs/assets/openclaw_cicd_running.png) + - ![OpenClaw CI/CD Error](docs/assets/openclaw_cicd_error.png) + - ![OpenClaw CI/CD Cancel](docs/assets/openclaw_cicd_cancel.png) + - ![OpenClaw CI/CD Inactive](docs/assets/openclaw_cicd_inactive.png) - **Spec Generator FX** : [Spec aléatoire](https://spec-generator.example.com) — science-fiction automatisée. - **Sanitizer Challenge** : Trouve la phrase supprimée par le sanitizer, score affiché. - **Agent QA Replicant** : _« Un evidence pack peut-il rêver de conformité ? »_ diff --git a/docs/AGENTIC_DIAGRAM.md b/docs/AGENTIC_DIAGRAM.md new file mode 100644 index 0000000..ae8b107 --- /dev/null +++ b/docs/AGENTIC_DIAGRAM.md @@ -0,0 +1,58 @@ +# Cartographie agentique Kill_LIFE + +Ce diagramme illustre les interactions entre les agents, les artefacts et les evidence packs du projet. + +```mermaid +graph TD + Architect[Architect Agent] + Doc[Doc Agent] + Firmware[Firmware Agent] + HW[HW Schematic Agent] + PM[PM Agent] + QA[QA Agent] + Specs[specs/] + Docs[docs/] + FirmwareDir[firmware/] + HardwareDir[hardware/] + Compliance[compliance/] + Evidence[evidence packs] + + Architect --> Specs + Architect --> PM + Architect --> HW + Architect --> Firmware + Architect --> Doc + PM --> Architect + PM --> HW + PM --> Firmware + PM --> QA + PM --> Doc + HW --> HardwareDir + HW --> Compliance + HW --> Evidence + HW --> Architect + HW --> PM + Firmware --> FirmwareDir + Firmware --> Specs + Firmware --> QA + Firmware --> PM + QA --> Evidence + QA --> Compliance + QA --> Firmware + QA --> HW + QA --> PM + Doc --> Docs + Doc --> Architect + Doc --> PM + Doc --> QA + + Compliance --> Evidence + Compliance --> HW + Compliance --> QA + + Evidence --> QA + Evidence --> Compliance + Evidence --> HW +``` + +> Diagramme généré automatiquement (GPT-4.1) diff --git a/docs/CONTINUE_SETUP.md b/docs/CONTINUE_SETUP.md new file mode 100644 index 0000000..2d4da02 --- /dev/null +++ b/docs/CONTINUE_SETUP.md @@ -0,0 +1,25 @@ +# VS Code Continue + Mistral (Codestral) + +Continue supports Mistral as a model provider. + +## Config (YAML) +Create `~/.continue/config.yaml`: + +```yaml +name: My Config +version: 0.0.1 +schema: v1 +models: + - name: Codestral + provider: mistral + model: codestral-latest + apiKey: + apiBase: https://codestral.mistral.ai/v1 + roles: + - chat + - autocomplete +``` + +## Important +Continue notes that the API key for `codestral.mistral.ai` is different from `api.mistral.ai` +and you should set `apiBase` accordingly. diff --git a/docs/MISTRAL.md b/docs/MISTRAL.md new file mode 100644 index 0000000..93859ed --- /dev/null +++ b/docs/MISTRAL.md @@ -0,0 +1,27 @@ +# Mistral integration pack + +This template can use Mistral for: +- **Code**: Codestral / Devstral +- **Specs/Docs**: Mistral Small/Medium/Large +- **RAG**: `mistral-embed` embeddings + +## Keys & endpoints +Set: +- `MISTRAL_API_KEY` (required) +- `MISTRAL_API_BASE` (optional) + +Some setups use a different base URL for Codestral. See `docs/CONTINUE_SETUP.md`. + +## Recommended models by role +- PM/Doc: `mistral-small-latest` or `mistral-medium-latest` +- Architect: `mistral-large-latest` +- Firmware/Implementation: `codestral-latest` +- QA (tests/refactors): `devstral-*` or `codestral-latest` + +## Safe-output approach (critical) +This pack uses **JSON-mode structured patches**: +1) generate `SafePatch` JSON (no repo writes) +2) validate schema + scope allowlists +3) apply patch (writes files only; never runs commands) + +See `docs/MISTRAL_AGENTIC.md`. diff --git a/docs/MISTRAL_AGENTIC.md b/docs/MISTRAL_AGENTIC.md new file mode 100644 index 0000000..e9cac48 --- /dev/null +++ b/docs/MISTRAL_AGENTIC.md @@ -0,0 +1,31 @@ +# Mistral Agentic Workflow (Safe Outputs) + +## Goal +Use Mistral models for agentic tasks **without giving the model unsafe agency**. + +## Pattern used +1) Sanitize untrusted inputs (issues/comments/logs) +2) Ask the model for a structured **SafePatch JSON** +3) Validate JSON against a schema +4) Apply changes with scope allowlists + deny globs +5) Verify manually/CI (never auto-execute model output) + +## Why JSON mode +Mistral supports JSON mode by setting `response_format={"type":"json_object"}` which enforces valid JSON. + +## Commands +Generate: +```bash +python tools/mistral/mistral_generate_patch.py --model codestral-latest --scope ai:spec --request "..." --out /tmp/patch.json +``` + +Apply: +```bash +python tools/mistral/apply_safe_patch.py --scope ai:spec --patch /tmp/patch.json +``` + +## Tool calling (optional) +Mistral supports function calling. If you use it: +- keep tools read-only by default +- allow writes only through SafePatch +- never expose arbitrary shell execution as a tool diff --git a/docs/MISTRAL_RAG.md b/docs/MISTRAL_RAG.md new file mode 100644 index 0000000..5b9db93 --- /dev/null +++ b/docs/MISTRAL_RAG.md @@ -0,0 +1,17 @@ +# Mistral RAG (local index) + +This pack includes a simple local semantic index using Mistral embeddings. + +## Build the index +```bash +python tools/mistral/index_repo.py +``` + +## Search +```bash +python tools/mistral/search_index.py --query "scope guard" --topk 5 +``` + +## Notes +- Minimal numpy index (cosine similarity). +- For large repos, consider a vector DB later. diff --git a/docs/assets/agents_bmad_generated.png b/docs/assets/agents_bmad_generated.png new file mode 100644 index 0000000..40dc35e Binary files /dev/null and b/docs/assets/agents_bmad_generated.png differ diff --git a/docs/assets/arborescence_kill_life_generated.png b/docs/assets/arborescence_kill_life_generated.png new file mode 100644 index 0000000..12b9975 Binary files /dev/null and b/docs/assets/arborescence_kill_life_generated.png differ diff --git a/docs/assets/badge_42_generated.gif b/docs/assets/badge_42_generated.gif new file mode 100644 index 0000000..9b569f4 Binary files /dev/null and b/docs/assets/badge_42_generated.gif differ diff --git a/docs/assets/banner_kill_life_generated.png b/docs/assets/banner_kill_life_generated.png new file mode 100644 index 0000000..b408f8f Binary files /dev/null and b/docs/assets/banner_kill_life_generated.png differ diff --git a/docs/assets/bulk_edit_party_generated.png b/docs/assets/bulk_edit_party_generated.png new file mode 100644 index 0000000..9114f70 Binary files /dev/null and b/docs/assets/bulk_edit_party_generated.png differ diff --git a/docs/assets/dont_panic_generated.png b/docs/assets/dont_panic_generated.png new file mode 100644 index 0000000..904b4ca Binary files /dev/null and b/docs/assets/dont_panic_generated.png differ diff --git a/docs/assets/evidence_pack_generated.png b/docs/assets/evidence_pack_generated.png new file mode 100644 index 0000000..35f0d02 Binary files /dev/null and b/docs/assets/evidence_pack_generated.png differ diff --git a/docs/assets/gate_validation_generated.png b/docs/assets/gate_validation_generated.png new file mode 100644 index 0000000..b045f6e Binary files /dev/null and b/docs/assets/gate_validation_generated.png differ diff --git a/docs/assets/openclaw_cicd_cancel.png b/docs/assets/openclaw_cicd_cancel.png new file mode 100644 index 0000000..d4cb417 Binary files /dev/null and b/docs/assets/openclaw_cicd_cancel.png differ diff --git a/docs/assets/openclaw_cicd_error.png b/docs/assets/openclaw_cicd_error.png new file mode 100644 index 0000000..c7a0d98 Binary files /dev/null and b/docs/assets/openclaw_cicd_error.png differ diff --git a/docs/assets/openclaw_cicd_inactive.png b/docs/assets/openclaw_cicd_inactive.png new file mode 100644 index 0000000..890b9f8 Binary files /dev/null and b/docs/assets/openclaw_cicd_inactive.png differ diff --git a/docs/assets/openclaw_cicd_running.png b/docs/assets/openclaw_cicd_running.png new file mode 100644 index 0000000..b108ca8 Binary files /dev/null and b/docs/assets/openclaw_cicd_running.png differ diff --git a/docs/assets/openclaw_cicd_success.png b/docs/assets/openclaw_cicd_success.png new file mode 100644 index 0000000..ecb4841 Binary files /dev/null and b/docs/assets/openclaw_cicd_success.png differ diff --git a/docs/assets/openclaw_sandbox_generated.png b/docs/assets/openclaw_sandbox_generated.png new file mode 100644 index 0000000..9b95797 Binary files /dev/null and b/docs/assets/openclaw_sandbox_generated.png differ diff --git a/docs/assets/pipeline_hw_fw_generated.png b/docs/assets/pipeline_hw_fw_generated.png new file mode 100644 index 0000000..f221532 Binary files /dev/null and b/docs/assets/pipeline_hw_fw_generated.png differ diff --git a/docs/assets/rapport/synthese_correspondance_agents_systemes.md b/docs/assets/rapport/synthese_correspondance_agents_systemes.md new file mode 100644 index 0000000..3128fb5 --- /dev/null +++ b/docs/assets/rapport/synthese_correspondance_agents_systemes.md @@ -0,0 +1,27 @@ +# Correspondance agents & systèmes – Kill_LIFE + +## Agents humains +- Définis dans agents/, ai-agentic-embedded-base/agents/, .github/agents/ +- Rôles : Architect, Doc, Firmware, HW Schematic, PM, QA +- Artefacts : schémas, plans, tests, documentation, evidence packs + +## Agents AI & systèmes +- **Copilot** : audit, documentation, validation automatisée (instructions dans .github/copilot-instructions.md, prompts dans tools/mistral/prompts/) +- **Codex** : génération de code, automatisation (tools/ai/compose_codex_prompt.py, prompts codex) +- **OpenAI** : génération de texte, evidence packs, validation specs (prompts QA, Doc, Architect) +- **Mistral** : prompts firmware, QA, documentation, orchestration, evidence packs (requirements-mistral.txt, tools/mistral/prompts/) + +## Correspondance fichiers/dossiers +- agents/, ai-agentic-embedded-base/agents/, .github/agents/ : rôles humains +- tools/mistral/prompts/, tools/ai/, .github/prompts/ : prompts/scripts pour agents AI +- docs/assets/rapport/, SYNTHESE_AGENTIQUE.md : synthèse, rapport, diagramme, audit +- firmware/, hardware/, compliance/, specs/ : artefacts produits/consommés + +## Interactions +- Agents humains : orchestration, validation, documentation +- Agents AI : automatisation, génération, evidence packs +- Systèmes intégrés via prompts/scripts pour traçabilité, conformité, automatisation + +--- + +> Synthèse générée automatiquement (GPT-4.1) diff --git a/requirements-mistral.txt b/requirements-mistral.txt new file mode 100644 index 0000000..fc5d192 --- /dev/null +++ b/requirements-mistral.txt @@ -0,0 +1,4 @@ +mistralai>=1.0.0 +jsonschema>=4.21.0 +pydantic>=2.6.0 +numpy>=1.26.0 diff --git a/tools/mistral/Makefile b/tools/mistral/Makefile new file mode 100644 index 0000000..aedb5fe --- /dev/null +++ b/tools/mistral/Makefile @@ -0,0 +1,15 @@ +# Mistral pack helper +venv: + python -m venv .venv && . .venv/bin/activate && pip install -r requirements-mistral.txt + +patch-spec: + python tools/mistral/mistral_generate_patch.py --model codestral-latest --scope ai:spec --request "$(REQ)" --out /tmp/patch.json + +apply-spec: + python tools/mistral/apply_safe_patch.py --scope ai:spec --patch /tmp/patch.json + +index: + python tools/mistral/index_repo.py + +search: + python tools/mistral/search_index.py --query "$(Q)" --topk 5 diff --git a/tools/mistral/__init__.py b/tools/mistral/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/mistral/apply_safe_patch.py b/tools/mistral/apply_safe_patch.py new file mode 100644 index 0000000..5162fd6 --- /dev/null +++ b/tools/mistral/apply_safe_patch.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Apply a SafePatch JSON to the repo with scope allowlists + deny globs. + +This script never runs shell commands. It only writes files. + +Example: + python tools/mistral/apply_safe_patch.py --scope ai:spec --patch /tmp/patch.json +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from jsonschema import Draft202012Validator + +from tools.mistral.scope_allowlists import is_path_allowed, explain_scope + +SCHEMA_PATH = Path(__file__).parent / "schemas" / "safe_patch.schema.json" + + +def safe_write(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--scope", required=True, help="ai:spec|ai:plan|ai:tasks|ai:impl|ai:qa|ai:docs") + ap.add_argument("--patch", required=True) + ap.add_argument("--allow-delete", action="store_true") + ap.add_argument("--root", default=".", help="repo root (default: .)") + args = ap.parse_args() + + root = Path(args.root).resolve() + schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) + + patch_path = Path(args.patch) + patch = json.loads(patch_path.read_text(encoding="utf-8")) + + v = Draft202012Validator(schema) + errors = sorted(v.iter_errors(patch), key=lambda e: e.path) + if errors: + msg = "\n".join([f"- {list(e.path)}: {e.message}" for e in errors]) + raise SystemExit(f"Patch JSON does not match schema:\n{msg}") + + edits = patch.get("edits", []) + blocked = [] + for e in edits: + p = e["path"].replace("\\", "/").lstrip("/") + if not is_path_allowed(args.scope, p): + blocked.append(p) + + if blocked: + raise SystemExit( + "Blocked paths for scope " + + args.scope + + ":\n - " + + "\n - ".join(blocked) + + "\n\n" + + explain_scope(args.scope) + ) + + applied = [] + for e in edits: + rel = e["path"].replace("\\", "/").lstrip("/") + action = e["action"] + dst = root / rel + + if action in ("create", "update"): + safe_write(dst, e.get("content", "")) + applied.append(f"{action}: {rel}") + elif action == "delete": + if not args.allow_delete: + raise SystemExit(f"Refusing to delete {rel} (pass --allow-delete)") + if dst.exists(): + dst.unlink() + applied.append(f"delete: {rel}") + else: + raise SystemExit(f"Unknown action: {action}") + + print("Applied edits:") + for a in applied: + print(" -", a) + + if patch.get("commands"): + print("\nSuggested verify commands (NOT executed):") + for c in patch["commands"]: + print(" -", c) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/mistral/index_repo.py b/tools/mistral/index_repo.py new file mode 100644 index 0000000..2423b3e --- /dev/null +++ b/tools/mistral/index_repo.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +""" +Create a simple local embedding index for this repo (specs/docs by default) +using Mistral embeddings. + +Output: + .mistral_index/ + meta.jsonl + vectors.npy + +Search: + python tools/mistral/search_index.py --query "scope guard" --topk 5 +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import List + +import numpy as np + +from tools.mistral.mistral_client import get_client + +DEFAULT_GLOBS = ["specs/**/*.md", "docs/**/*.md", "README.md"] + + +def iter_files(root: Path, globs: List[str]) -> List[Path]: + out: List[Path] = [] + for g in globs: + out.extend(root.glob(g)) + out = sorted({p for p in out if p.is_file()}) + return out + + +def chunk_text(text: str, max_chars: int = 3000) -> List[str]: + chunks = [] + i = 0 + while i < len(text): + chunks.append(text[i:i + max_chars]) + i += max_chars + return chunks + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--root", default=".") + ap.add_argument("--model", default="mistral-embed") + ap.add_argument("--globs", nargs="*", default=DEFAULT_GLOBS) + ap.add_argument("--out", default=".mistral_index") + args = ap.parse_args() + + root = Path(args.root).resolve() + out_dir = root / args.out + out_dir.mkdir(parents=True, exist_ok=True) + + files = iter_files(root, args.globs) + if not files: + raise SystemExit("No files matched.") + + client = get_client() + meta_path = out_dir / "meta.jsonl" + vec_path = out_dir / "vectors.npy" + + metas = [] + vectors = [] + + for p in files: + rel = str(p.relative_to(root)).replace("\\", "/") + text = p.read_text(encoding="utf-8", errors="ignore") + for idx, chunk in enumerate(chunk_text(text)): + metas.append({"path": rel, "chunk": idx, "chars": len(chunk)}) + resp = client.embeddings.create(model=args.model, inputs=[chunk]) + vectors.append(resp.data[0].embedding) + + np.save(vec_path, np.array(vectors, dtype=np.float32)) + with meta_path.open("w", encoding="utf-8") as f: + for m in metas: + f.write(json.dumps(m, ensure_ascii=False) + "\n") + + print(f"Indexed {len(metas)} chunks from {len(files)} files into {out_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/mistral/mistral_client.py b/tools/mistral/mistral_client.py new file mode 100644 index 0000000..556e008 --- /dev/null +++ b/tools/mistral/mistral_client.py @@ -0,0 +1,68 @@ +""" +Minimal Mistral client wrapper for safe-output patch generation. + +- Uses official `mistralai` SDK. +- Supports JSON Mode (`response_format={"type":"json_object"}`) to enforce valid JSON. +- Optional API base override (Codestral uses a different API base in some setups). +""" +from __future__ import annotations + +import os +from typing import Any, Dict + +from mistralai import Mistral + + +def get_client() -> Mistral: + api_key = os.environ.get("MISTRAL_API_KEY") + if not api_key: + raise RuntimeError("MISTRAL_API_KEY is not set.") + + api_base = os.environ.get("MISTRAL_API_BASE") # e.g. https://codestral.mistral.ai/v1 + # SDK supports server_url in recent versions; we pass it if available. + try: + if api_base: + return Mistral(api_key=api_key, server_url=api_base) + except TypeError: + pass + + return Mistral(api_key=api_key) + + +def chat_json( + *, + model: str, + system: str, + user: str, + temperature: float = 0.0, + max_tokens: int = 4096, +) -> Dict[str, Any]: + """ + Request JSON mode output. Returns parsed JSON dict. + + The SDK returns `choices[0].message.content` as a *stringified JSON object* in JSON mode. + """ + import json + + client = get_client() + resp = client.chat.complete( + model=model, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + response_format={"type": "json_object"}, + temperature=temperature, + max_tokens=max_tokens, + ) + + content = resp.choices[0].message.content + if isinstance(content, dict): + return content + if not isinstance(content, str): + raise RuntimeError(f"Unexpected content type: {type(content)}") + + try: + return json.loads(content) + except json.JSONDecodeError as e: + raise RuntimeError(f"Model did not return valid JSON: {e}\nRaw:\n{content}") from e diff --git a/tools/mistral/mistral_generate_patch.py b/tools/mistral/mistral_generate_patch.py new file mode 100644 index 0000000..68a7e11 --- /dev/null +++ b/tools/mistral/mistral_generate_patch.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +Generate a structured JSON "SafePatch" using Mistral (JSON mode), +without touching repository files. + +Example: + python tools/mistral/mistral_generate_patch.py \ + --model codestral-latest \ + --scope ai:spec \ + --request "Draft RFC2119 spec for Feature X" \ + --out /tmp/patch.json +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from jsonschema import Draft202012Validator + +from tools.mistral.mistral_client import chat_json +from tools.mistral.scope_allowlists import explain_scope + +SCHEMA_PATH = Path(__file__).parent / "schemas" / "safe_patch.schema.json" + + +SYSTEM_TEMPLATE = """You are a SAFE PATCH GENERATOR for a software repository. + +Rules: +- Output MUST be a single JSON object matching the provided schema. +- NEVER include markdown, code fences, prose outside JSON. +- Only propose file edits within the allowed scope; if a request would require out-of-scope edits, + provide an in-scope alternative and add a note. +- Do NOT include changes to CI workflows or security boundaries. +- Do NOT include secrets. +- Use minimal diffs: create/update only what is necessary. + +Allowed scope: +{scope_expl} + +Schema: +{schema} +""" + +USER_TEMPLATE = """Request: +{request} + +Context notes (optional): +{context} +""" + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--model", default="codestral-latest") + ap.add_argument("--scope", required=True, help="ai:spec|ai:plan|ai:tasks|ai:impl|ai:qa|ai:docs") + ap.add_argument("--request", required=True) + ap.add_argument("--context", default="") + ap.add_argument("--out", required=True) + args = ap.parse_args() + + schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) + system = SYSTEM_TEMPLATE.format(scope_expl=explain_scope(args.scope), schema=json.dumps(schema, indent=2)) + user = USER_TEMPLATE.format(request=args.request, context=args.context) + + patch = chat_json(model=args.model, system=system, user=user, temperature=0.0, max_tokens=4096) + + # Validate schema + v = Draft202012Validator(schema) + errors = sorted(v.iter_errors(patch), key=lambda e: e.path) + if errors: + msg = "\n".join([f"- {list(e.path)}: {e.message}" for e in errors]) + raise SystemExit(f"Patch JSON does not match schema:\n{msg}\n\nRaw patch:\n{json.dumps(patch, indent=2)}") + + out = Path(args.out) + out.write_text(json.dumps(patch, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + print(f"Wrote patch: {out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/mistral/prompts/architect_system.md b/tools/mistral/prompts/architect_system.md new file mode 100644 index 0000000..8b5fc76 --- /dev/null +++ b/tools/mistral/prompts/architect_system.md @@ -0,0 +1 @@ +You are an embedded systems architect. Provide block diagrams (text), interfaces, budgets (power/latency/memory), risks+mitigations, and verification plan. diff --git a/tools/mistral/prompts/doc_system.md b/tools/mistral/prompts/doc_system.md new file mode 100644 index 0000000..8063e7c --- /dev/null +++ b/tools/mistral/prompts/doc_system.md @@ -0,0 +1 @@ +You are documentation. Update docs and runbooks only. Keep instructions concise and actionable. diff --git a/tools/mistral/prompts/firmware_system.md b/tools/mistral/prompts/firmware_system.md new file mode 100644 index 0000000..331d013 --- /dev/null +++ b/tools/mistral/prompts/firmware_system.md @@ -0,0 +1 @@ +You are a firmware agent. Implement minimal changes, add tests, and keep diffs small. Never modify CI workflows. Prefer deterministic behavior. diff --git a/tools/mistral/prompts/pm_system.md b/tools/mistral/prompts/pm_system.md new file mode 100644 index 0000000..7fcd830 --- /dev/null +++ b/tools/mistral/prompts/pm_system.md @@ -0,0 +1 @@ +You are a PM agent. Produce RFC2119-friendly requirements and acceptance criteria. Keep scope tight. Prefer small PRs and explicit assumptions. diff --git a/tools/mistral/prompts/qa_system.md b/tools/mistral/prompts/qa_system.md new file mode 100644 index 0000000..d820014 --- /dev/null +++ b/tools/mistral/prompts/qa_system.md @@ -0,0 +1 @@ +You are QA. Generate edge cases, tests, and validation steps. Focus on reproducibility and evidence packs. diff --git a/tools/mistral/schemas/safe_patch.schema.json b/tools/mistral/schemas/safe_patch.schema.json new file mode 100644 index 0000000..4d8b89b --- /dev/null +++ b/tools/mistral/schemas/safe_patch.schema.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SafePatch", + "type": "object", + "additionalProperties": false, + "required": [ + "summary", + "edits" + ], + "properties": { + "summary": { + "type": "string", + "minLength": 1 + }, + "edits": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "action" + ], + "properties": { + "path": { + "type": "string", + "minLength": 1 + }, + "action": { + "type": "string", + "enum": [ + "create", + "update", + "delete" + ] + }, + "content": { + "type": "string" + }, + "reason": { + "type": "string" + } + }, + "allOf": [ + { + "if": { + "properties": { + "action": { + "const": "delete" + } + } + }, + "then": { + "not": { + "required": [ + "content" + ] + } + } + }, + { + "if": { + "properties": { + "action": { + "enum": [ + "create", + "update" + ] + } + } + }, + "then": { + "required": [ + "content" + ] + } + } + ] + } + }, + "commands": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Suggested verification commands ONLY. Never executed automatically." + }, + "notes": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/tools/mistral/scope_allowlists.py b/tools/mistral/scope_allowlists.py new file mode 100644 index 0000000..6c2521c --- /dev/null +++ b/tools/mistral/scope_allowlists.py @@ -0,0 +1,75 @@ +""" +Local scope allowlists for applying patches safely. + +This is NOT a replacement for CI scope guards; it is a local "pre-flight" safety net. +""" +from __future__ import annotations + +import fnmatch +from typing import List + + +DENY_GLOBS = [ + ".git/**", + ".github/workflows/**", + ".github/actions/**", + "**/.env", + "**/*.key", + "**/*secret*", + "**/*token*", +] + +ALLOWED_BY_SCOPE = { + "ai:spec": [ + "specs/**", + "docs/**", + "README.md", + ".github/copilot-instructions.md", + ".github/copilot/**", + ".github/prompts/**", + ], + "ai:plan": [ + "specs/**", + "docs/**", + "README.md", + ], + "ai:tasks": [ + "specs/**", + "docs/**", + ], + "ai:docs": [ + "docs/**", + "README.md", + ".github/copilot-instructions.md", + ".github/copilot/**", + ".github/prompts/**", + ], + "ai:impl": [ + "firmware/**", + "docs/**", + "specs/**", + "tools/**", + ], + "ai:qa": [ + "firmware/**", + "tools/**", + "docs/**", + "specs/**", + ], +} + +def _matches_any(path: str, globs: List[str]) -> bool: + return any(fnmatch.fnmatch(path, g) for g in globs) + +def is_path_allowed(scope: str, path: str) -> bool: + path = path.replace("\\", "/").lstrip("/") + if _matches_any(path, DENY_GLOBS): + return False + allow = ALLOWED_BY_SCOPE.get(scope) + if not allow: + return False + return _matches_any(path, allow) + +def explain_scope(scope: str) -> str: + allow = ALLOWED_BY_SCOPE.get(scope, []) + return f"{scope} allows: " + ", ".join(allow) diff --git a/tools/mistral/search_index.py b/tools/mistral/search_index.py new file mode 100644 index 0000000..d94b549 --- /dev/null +++ b/tools/mistral/search_index.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +""" +Search the local .mistral_index with cosine similarity. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import List, Tuple + +import numpy as np + +from tools.mistral.mistral_client import get_client + + +def cosine_topk(matrix: np.ndarray, q: np.ndarray, k: int) -> Tuple[List[int], np.ndarray]: + m = matrix / (np.linalg.norm(matrix, axis=1, keepdims=True) + 1e-9) + qn = q / (np.linalg.norm(q) + 1e-9) + scores = m @ qn + return list(np.argsort(-scores)[:k]), scores + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--query", required=True) + ap.add_argument("--index", default=".mistral_index") + ap.add_argument("--model", default="mistral-embed") + ap.add_argument("--topk", type=int, default=5) + args = ap.parse_args() + + idx_dir = Path(args.index) + meta_path = idx_dir / "meta.jsonl" + vec_path = idx_dir / "vectors.npy" + if not meta_path.exists() or not vec_path.exists(): + raise SystemExit("Index not found. Run: python tools/mistral/index_repo.py") + + metas = [json.loads(line) for line in meta_path.read_text(encoding="utf-8").splitlines() if line.strip()] + vecs = np.load(vec_path) + + client = get_client() + resp = client.embeddings.create(model=args.model, inputs=[args.query]) + q = np.array(resp.data[0].embedding, dtype=np.float32) + + ids, scores = cosine_topk(vecs, q, args.topk) + print("Top matches:") + for i in ids: + m = metas[i] + print(f"- {m['path']} (chunk {m['chunk']}) score={float(scores[i]):.4f}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())