feat: automate mac mcp cad host bootstrap #17

Merged
electron-rare merged 1 commits from feat/mac-mcp-cad-host-bootstrap into main 2026-03-09 05:12:52 +00:00
16 changed files with 878 additions and 36 deletions
+18 -5
View File
@@ -22,7 +22,7 @@ References canoniques:
## Chemins supportes
- `kicad`: `tools/hw/run_kicad_mcp.sh`
- `validate-specs`: `python3 tools/validate_specs.py --mcp`
- `validate-specs`: `tools/run_validate_specs_mcp.sh`
- `knowledge-base`: `tools/run_knowledge_base_mcp.sh`
- `github-dispatch`: `tools/run_github_dispatch_mcp.sh`
- `freecad`: `tools/run_freecad_mcp.sh`
@@ -43,7 +43,11 @@ Les micro-serveurs `kicad_kic_ai` de `mascarade` restent suivis comme surfaces a
## Prerequis
- le repo compagnon `mascarade` existe en voisin (`../mascarade`) ou via `MASCARADE_DIR`
- Docker est disponible pour `kicad`, `freecad` et `openscad`
- les runtimes hote suivants sont auto-detectes si presents:
- KiCad app macOS avec `kicad-cli`
- FreeCAD app macOS avec `freecadcmd`
- OpenSCAD CLI local; si plusieurs binaires existent, le chemin CLI stable est prefere au snapshot `.app`
- Docker reste le fallback pour `kicad`, `freecad` et `openscad`
- `node` est disponible pour le build/API compagnon
- le venv `mascarade/core/.venv` existe, ou `MASCARADE_CORE_PYTHON` pointe vers un Python avec les dependances knowledge base (`httpx`)
- `KNOWLEDGE_BASE_PROVIDER` selectionne `memos` ou `docmost`
@@ -93,8 +97,8 @@ Le fichier versionne [mcp.json](../mcp.json) pointe vers les serveurs MCP reelle
},
"validate-specs": {
"type": "local",
"command": "python3",
"args": ["tools/validate_specs.py", "--mcp"],
"command": "bash",
"args": ["tools/run_validate_specs_mcp.sh"],
"tools": ["*"]
},
"knowledge-base": {
@@ -181,6 +185,15 @@ Etat de validation courant:
- `codex mcp list` contient bien `kicad`, `validate-specs`, `knowledge-base`, `github-dispatch`, `freecad`, `openscad`, `huggingface` et `playwright`
- `Playwright MCP` est valide sur le Mac cible via `npx -y @playwright/mcp@latest --help`
- seul reliquat Mac connu: le worktree `/Users/electron/mascarade` est dirty, donc aucun `git pull` n'a ete force sur ce clone
- execution cible sur le Mac operateur reel validee avec enregistrement effectif des serveurs dans Codex
## Sources hote macOS retenues
- KiCad: application locale macOS detectee dans `/Applications/KiCad/KiCad.app`
- FreeCAD: releases officielles `https://github.com/FreeCAD/FreeCAD/releases`
- OpenSCAD:
- snapshot app possible via `https://files.openscad.org/snapshots/OpenSCAD-2026.03.07.dmg`
- pour le runtime MCP local, le binaire CLI `OpenSCAD 2021.01` est prefere quand il est installe et repond correctement en non-interactif
## Usage
@@ -188,7 +201,7 @@ Depuis `Kill_LIFE`:
```bash
tools/hw/run_kicad_mcp.sh
python3 tools/validate_specs.py --mcp
tools/run_validate_specs_mcp.sh
tools/run_knowledge_base_mcp.sh
tools/run_github_dispatch_mcp.sh
tools/run_freecad_mcp.sh
@@ -0,0 +1,77 @@
# 18) Plan d'enchainement autonome des lots utiles
Last updated: 2026-03-09 06:06:59
Ce plan est regenere localement par `tools/autonomous_next_lots.py`.
## Objectif
Detecter les deltas utiles a traiter, prioriser le prochain lot executable,
mettre a jour un plan/todo operateur, puis relancer les validations associees.
## Regles de priorite
1. lot dirty avec validations requises cassables
2. lot dirty avec validations advisory ou docs
3. repo clean mais en retard sur le remote
4. regime stable sans lot local detecte
## Etat Git courant
- branche: `## feat/mac-mcp-cad-host-bootstrap`
- dirty paths: `16`
- ahead: `0`
- behind: `0`
### Fichiers dirty detectes
- `docs/MCP_SETUP.md`
- `docs/plans/README.md`
- `mcp.json`
- `tools/bootstrap_mac_mcp.sh`
- `tools/hw/cad_stack.sh`
- `tools/hw/run_kicad_mcp.sh`
- `tools/lib/runtime_home.sh`
- `tools/mcp_smoke_common.py`
- `tools/run_github_dispatch_mcp.sh`
- `tools/run_knowledge_base_mcp.sh`
- `tools/validate_specs_mcp_smoke.py`
- `docs/plans/18_plan_enchainement_autonome_des_lots_utiles.md`
- `docs/plans/18_todo_enchainement_autonome_des_lots_utiles.md`
- `tools/autonomous_next_lots.py`
- `tools/run_autonomous_next_lots.sh`
- `tools/run_validate_specs_mcp.sh`
copilot-pull-request-reviewer[bot] commented 2026-03-09 05:13:55 +00:00 (Migrated from github.com)
Review

This generated plan includes ephemeral local Git state (branch header, dirty path count/list, timestamps). As a tracked doc, this will churn frequently and may not be meaningful to other contributors. Consider keeping the runbook content versioned, but moving per-run state into untracked artifacts (or only writing it when explicitly requested).

This generated plan includes ephemeral local Git state (branch header, dirty path count/list, timestamps). As a tracked doc, this will churn frequently and may not be meaningful to other contributors. Consider keeping the runbook content versioned, but moving per-run state into untracked artifacts (or only writing it when explicitly requested).
## Lots detectes
### 1. `mcp-runtime` — Alignement MCP runtime local
Stabiliser les launchers MCP, le bootstrap Mac, la resolution du repo compagnon et la doc operateur associee.
- references: `docs/plans/15_plan_mcp_runtime_alignment.md`, `docs/plans/17_plan_target_architecture_mcp_agentics_2028.md`
- validations: `3` done, `2` advisory, `0` blocked
### 2. `cad-mcp-host` — Runtime CAD host-first
Qualifier KiCad, FreeCAD et OpenSCAD en host-first sur macOS tout en gardant le fallback conteneur operable.
- references: `docs/plans/16_plan_cad_modeling_stack.md`, `docs/plans/17_plan_target_architecture_mcp_agentics_2028.md`
- validations: `4` done, `0` advisory, `0` blocked
### 3. `python-local` — Execution Python repo-locale
Garder les scripts et smokes sur l'interpreteur repo-local plutot que sur le Python systeme.
- references: `docs/plans/15_plan_mcp_runtime_alignment.md`
- validations: `1` done, `0` advisory, `0` blocked
## Questions a poser seulement si besoin reel
- Aucune question bloquante detectee sur ce cycle.
## Commandes operateur
- `bash tools/run_autonomous_next_lots.sh status`
- `bash tools/run_autonomous_next_lots.sh run`
- `bash tools/run_autonomous_next_lots.sh json`
@@ -0,0 +1,38 @@
# 18) TODO enchainement autonome des lots utiles
Last updated: 2026-03-09 06:06:59
Ce fichier est regenere localement par `tools/autonomous_next_lots.py`.
## `mcp-runtime` — Alignement MCP runtime local
- done: lot detecte (Stabiliser les launchers MCP, le bootstrap Mac, la resolution du repo compagnon et la doc operateur associee.)
- done: `bash tools/bootstrap_mac_mcp.sh codex`
resume: codex mcp add openscad --env MASCARADE_DIR=/Users/electron/mascarade-main -- bash /Users/electron/Kill_LIFE/tools/run_openscad_mcp.sh | codex mcp add huggingface --url https://huggingface.co/mcp --bearer-token-env-var HUGGINGFACE_API_KEY | codex mcp add playwright -- npx -y @playwright/mcp@latest
copilot-pull-request-reviewer[bot] commented 2026-03-09 05:13:54 +00:00 (Migrated from github.com)
Review

This generated TODO file includes user-specific absolute paths in the validation summaries (e.g., /Users/.../Kill_LIFE/...). Committing machine-local paths makes the doc noisy and can leak local environment details. Regenerate with sanitized/relative paths (or avoid embedding raw command output in tracked docs).

  resume: codex mcp add openscad --env MASCARADE_DIR=$MASCARADE_DIR -- bash tools/run_openscad_mcp.sh | codex mcp add huggingface --url https://huggingface.co/mcp --bearer-token-env-var HUGGINGFACE_API_KEY | codex mcp add playwright -- npx -y @playwright/mcp@latest
This generated TODO file includes user-specific absolute paths in the validation summaries (e.g., `/Users/.../Kill_LIFE/...`). Committing machine-local paths makes the doc noisy and can leak local environment details. Regenerate with sanitized/relative paths (or avoid embedding raw command output in tracked docs). ```suggestion resume: codex mcp add openscad --env MASCARADE_DIR=$MASCARADE_DIR -- bash tools/run_openscad_mcp.sh | codex mcp add huggingface --url https://huggingface.co/mcp --bearer-token-env-var HUGGINGFACE_API_KEY | codex mcp add playwright -- npx -y @playwright/mcp@latest ```
- done: `bash tools/bootstrap_mac_mcp.sh json`
resume: } | } | }
- done: `.venv/bin/python tools/validate_specs_mcp_smoke.py --json --quick`
resume: {"status": "ready", "protocol_version": "2025-03-26", "server_name": "validate-specs", "tool_count": 2, "checks": ["initialize", "tools/list"], "error": null}
- advisory: `.venv/bin/python tools/knowledge_base_mcp_smoke.py --json --quick`
resume: {"status": "degraded", "protocol_version": "2025-03-26", "server_name": "knowledge-base", "provider": "memos", "tool_count": 4, "checks": ["initialize", "tools/list"], "secret_configured": false, "live_validation": "missing_secret", "error": "memos auth missing"}
- advisory: `.venv/bin/python tools/github_dispatch_mcp_smoke.py --json --quick`
resume: {"status": "degraded", "protocol_version": "2025-03-26", "server_name": "github-dispatch", "tool_count": 3, "checks": ["initialize", "tools/list"], "token_configured": false, "live_requested": false, "live_validation": "missing_secret", "error": "GitHub dispatch auth missing"}
## `cad-mcp-host` — Runtime CAD host-first
- done: lot detecte (Qualifier KiCad, FreeCAD et OpenSCAD en host-first sur macOS tout en gardant le fallback conteneur operable.)
- done: `bash tools/hw/run_kicad_mcp.sh --doctor`
resume: KICAD_PYTHON_STDERR_LOG_LEVEL=WARNING | REQUESTED_RUNTIME=auto | SELECTED_RUNTIME=host
- done: `bash tools/hw/cad_stack.sh doctor`
resume: 1.0.2 | PlatformIO Core, version 6.1.19 | OpenSCAD version 2021.01
- done: `.venv/bin/python tools/freecad_mcp_smoke.py --quick --json`
resume: {"status": "ready", "protocol_version": "2025-03-26", "server_name": "freecad", "tool_count": 4, "checks": ["initialize", "tools/list", "get_runtime_info"], "error": null}
- done: `.venv/bin/python tools/openscad_mcp_smoke.py --quick --json`
resume: {"status": "ready", "protocol_version": "2025-03-26", "server_name": "openscad", "tool_count": 4, "checks": ["initialize", "tools/list", "get_runtime_info"], "error": null}
## `python-local` — Execution Python repo-locale
- done: lot detecte (Garder les scripts et smokes sur l'interpreteur repo-local plutot que sur le Python systeme.)
- done: `bash tools/test_python.sh --venv-dir .venv --suite stable`
resume: ---------------------------------------------------------------------- | Ran 3 tests in 0.014s | OK
+1
View File
@@ -25,6 +25,7 @@ Ces plans sont des **runbooks** : tu peux les suivre tels quels, ou les transfor
15. [Plan d'alignement MCP local](15_plan_mcp_runtime_alignment.md)
16. [Plan de stack CAD modelling locale](16_plan_cad_modeling_stack.md)
17. [Plan cible MCP + agentics + A2A](17_plan_target_architecture_mcp_agentics_2028.md)
18. [Plan d'enchainement autonome des lots utiles](18_plan_enchainement_autonome_des_lots_utiles.md)
---
+2 -2
View File
@@ -8,8 +8,8 @@
},
"validate-specs": {
"type": "local",
"command": "python3",
"args": ["tools/validate_specs.py", "--mcp"],
"command": "bash",
"args": ["tools/run_validate_specs_mcp.sh"],
"tools": ["*"]
},
"knowledge-base": {
+456
View File
@@ -0,0 +1,456 @@
#!/usr/bin/env python3
"""Detect, validate, and document the next useful local lots."""
from __future__ import annotations
import argparse
import json
import re
import subprocess
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PLAN_DOC = ROOT / "docs" / "plans" / "18_plan_enchainement_autonome_des_lots_utiles.md"
TODO_DOC = ROOT / "docs" / "plans" / "18_todo_enchainement_autonome_des_lots_utiles.md"
@dataclass(frozen=True)
class Validation:
name: str
cmd: tuple[str, ...]
required: bool = True
@dataclass(frozen=True)
class Lot:
key: str
title: str
description: str
priority: int
paths: tuple[str, ...]
plan_refs: tuple[str, ...]
validations: tuple[Validation, ...]
LOTS: tuple[Lot, ...] = (
Lot(
key="mcp-runtime",
title="Alignement MCP runtime local",
description=(
"Stabiliser les launchers MCP, le bootstrap Mac, la resolution du repo "
"compagnon et la doc operateur associee."
),
priority=10,
paths=(
"docs/MCP_SETUP.md",
"mcp.json",
"tools/bootstrap_mac_mcp.sh",
"tools/lib/runtime_home.sh",
"tools/mcp_smoke_common.py",
"tools/run_github_dispatch_mcp.sh",
"tools/run_knowledge_base_mcp.sh",
"tools/run_validate_specs_mcp.sh",
"tools/validate_specs_mcp_smoke.py",
),
plan_refs=(
"docs/plans/15_plan_mcp_runtime_alignment.md",
"docs/plans/17_plan_target_architecture_mcp_agentics_2028.md",
),
validations=(
Validation(
name="bootstrap_codex_dry_run",
cmd=("bash", "tools/bootstrap_mac_mcp.sh", "codex"),
),
Validation(
name="bootstrap_json_dry_run",
cmd=("bash", "tools/bootstrap_mac_mcp.sh", "json"),
),
Validation(
name="validate_specs_mcp_smoke",
cmd=(".venv/bin/python", "tools/validate_specs_mcp_smoke.py", "--json", "--quick"),
),
Validation(
name="knowledge_base_mcp_smoke",
cmd=(".venv/bin/python", "tools/knowledge_base_mcp_smoke.py", "--json", "--quick"),
required=False,
),
Validation(
name="github_dispatch_mcp_smoke",
cmd=(".venv/bin/python", "tools/github_dispatch_mcp_smoke.py", "--json", "--quick"),
required=False,
),
),
),
Lot(
key="cad-mcp-host",
title="Runtime CAD host-first",
description=(
"Qualifier KiCad, FreeCAD et OpenSCAD en host-first sur macOS tout en "
"gardant le fallback conteneur operable."
),
priority=20,
paths=(
"docs/MCP_SETUP.md",
"tools/hw/cad_stack.sh",
"tools/hw/run_kicad_mcp.sh",
"tools/run_freecad_mcp.sh",
"tools/run_openscad_mcp.sh",
),
plan_refs=(
"docs/plans/16_plan_cad_modeling_stack.md",
"docs/plans/17_plan_target_architecture_mcp_agentics_2028.md",
),
validations=(
Validation(
name="kicad_doctor",
cmd=("bash", "tools/hw/run_kicad_mcp.sh", "--doctor"),
),
Validation(
name="cad_stack_doctor",
cmd=("bash", "tools/hw/cad_stack.sh", "doctor"),
),
Validation(
name="freecad_mcp_smoke",
cmd=(".venv/bin/python", "tools/freecad_mcp_smoke.py", "--quick", "--json"),
),
Validation(
name="openscad_mcp_smoke",
cmd=(".venv/bin/python", "tools/openscad_mcp_smoke.py", "--quick", "--json"),
),
),
),
Lot(
key="python-local",
title="Execution Python repo-locale",
description=(
"Garder les scripts et smokes sur l'interpreteur repo-local plutot que "
"sur le Python systeme."
),
priority=30,
paths=(
"tools/run_validate_specs_mcp.sh",
"tools/validate_specs_mcp_smoke.py",
"tools/test_python.sh",
),
plan_refs=("docs/plans/15_plan_mcp_runtime_alignment.md",),
validations=(
Validation(
name="python_stable_suite",
cmd=("bash", "tools/test_python.sh", "--venv-dir", ".venv", "--suite", "stable"),
),
),
),
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("mode", choices=("status", "run", "json"), nargs="?", default="status")
parser.add_argument("--max-lots", type=int, default=0, help="Limit execution to the first N detected lots.")
parser.add_argument(
"--no-write",
action="store_true",
help="Do not rewrite plan/todo markdown files.",
)
return parser.parse_args()
def now_label() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def run_cmd(cmd: tuple[str, ...]) -> subprocess.CompletedProcess[str]:
return subprocess.run(
list(cmd),
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
def git_status() -> tuple[str, list[str], int, int]:
proc = run_cmd(("git", "status", "--short", "--branch"))
if proc.returncode != 0:
raise SystemExit(proc.stderr.strip() or proc.stdout.strip() or "git status failed")
lines = proc.stdout.splitlines()
branch = lines[0] if lines else "## unknown"
ahead = 0
behind = 0
match = re.search(r"\[([^\]]+)\]", branch)
if match:
for chunk in match.group(1).split(","):
chunk = chunk.strip()
if chunk.startswith("ahead "):
ahead = int(chunk.split()[1])
elif chunk.startswith("behind "):
behind = int(chunk.split()[1])
dirty_paths: list[str] = []
for line in lines[1:]:
if not line:
continue
payload = line[3:]
if " -> " in payload:
payload = payload.split(" -> ", 1)[1]
dirty_paths.append(payload.strip())
return branch, dirty_paths, ahead, behind
def matches_path(candidate: str, tracked: str) -> bool:
return tracked == candidate or tracked.startswith(candidate + "/")
def detect_lots(dirty_paths: list[str]) -> list[Lot]:
matched: list[Lot] = []
for lot in LOTS:
if any(matches_path(path, dirty) for path in lot.paths for dirty in dirty_paths):
matched.append(lot)
matched.sort(key=lambda item: item.priority)
return matched
def output_tail(text: str) -> str:
lines = [line.rstrip() for line in text.splitlines() if line.strip()]
if not lines:
return ""
return " | ".join(lines[-3:])
copilot-pull-request-reviewer[bot] commented 2026-03-09 05:13:56 +00:00 (Migrated from github.com)
Review

output_tail (and the subsequent embedding of command outputs into the generated docs) can capture machine-specific absolute paths and other environment details. This is already visible in the committed docs/plans/18_* artifacts (e.g., /Users/... paths in command summaries), which creates noisy diffs and can unintentionally leak local filesystem layout into the repo. Consider redacting absolute paths (e.g., replace ROOT with . / $REPO), truncating/normalizing outputs more aggressively, and/or not persisting raw validation output into tracked markdown.

def _sanitize_summary_line(line: str) -> str:
    """Redact machine-specific details from a single summary line."""
    # Normalize common absolute paths to stable tokens to avoid leaking local layout.
    repo_root = str(ROOT)
    if repo_root and repo_root in line:
        line = line.replace(repo_root, "$REPO")

    # Also hide the current user's home directory if present.
    home_dir = str(Path.home())
    if home_dir and home_dir in line:
        line = line.replace(home_dir, "$HOME")

    # Truncate very long lines to keep summaries compact and diffs manageable.
    max_len = 200
    if len(line) > max_len:
        line = line[: max_len - 3] + "..."

    return line


def output_tail(text: str) -> str:
    lines = [line.rstrip() for line in text.splitlines() if line.strip()]
    if not lines:
        return ""
    tail = lines[-3:]
    sanitized = [_sanitize_summary_line(line) for line in tail]
    return " | ".join(sanitized)
`output_tail` (and the subsequent embedding of command outputs into the generated docs) can capture machine-specific absolute paths and other environment details. This is already visible in the committed `docs/plans/18_*` artifacts (e.g., `/Users/...` paths in command summaries), which creates noisy diffs and can unintentionally leak local filesystem layout into the repo. Consider redacting absolute paths (e.g., replace `ROOT` with `.` / `$REPO`), truncating/normalizing outputs more aggressively, and/or not persisting raw validation output into tracked markdown. ```suggestion def _sanitize_summary_line(line: str) -> str: """Redact machine-specific details from a single summary line.""" # Normalize common absolute paths to stable tokens to avoid leaking local layout. repo_root = str(ROOT) if repo_root and repo_root in line: line = line.replace(repo_root, "$REPO") # Also hide the current user's home directory if present. home_dir = str(Path.home()) if home_dir and home_dir in line: line = line.replace(home_dir, "$HOME") # Truncate very long lines to keep summaries compact and diffs manageable. max_len = 200 if len(line) > max_len: line = line[: max_len - 3] + "..." return line def output_tail(text: str) -> str: lines = [line.rstrip() for line in text.splitlines() if line.strip()] if not lines: return "" tail = lines[-3:] sanitized = [_sanitize_summary_line(line) for line in tail] return " | ".join(sanitized) ```
def classify_blocker(output: str) -> str | None:
lower = output.lower()
if any(token in lower for token in ("auth", "token", "permission", "credential", "secret")):
return "Secrets/identifiants manquants pour une validation live."
if "docker" in lower:
return "Runtime Docker/CAD indisponible ou non accessible."
if "not found" in lower or "no such file" in lower:
return "Chemin ou outil attendu manquant."
return None
def execute_lot(lot: Lot) -> list[dict[str, object]]:
results: list[dict[str, object]] = []
for validation in lot.validations:
proc = run_cmd(validation.cmd)
combined = "\n".join(part for part in (proc.stdout, proc.stderr) if part).strip()
if proc.returncode == 0:
status = "done"
elif validation.required:
status = "blocked"
else:
status = "advisory"
results.append(
{
"name": validation.name,
"cmd": list(validation.cmd),
"required": validation.required,
"status": status,
"returncode": proc.returncode,
"summary": output_tail(combined),
"blocker": classify_blocker(combined) if proc.returncode != 0 else None,
}
)
if status == "blocked":
break
return results
def render_plan(
branch: str,
dirty_paths: list[str],
ahead: int,
behind: int,
lots: list[Lot],
results: dict[str, list[dict[str, object]]],
) -> str:
lines = [
"# 18) Plan d'enchainement autonome des lots utiles",
"",
f"Last updated: {now_label()}",
"",
"Ce plan est regenere localement par `tools/autonomous_next_lots.py`.",
"",
"## Objectif",
"",
"Detecter les deltas utiles a traiter, prioriser le prochain lot executable,",
"mettre a jour un plan/todo operateur, puis relancer les validations associees.",
"",
"## Regles de priorite",
"",
"1. lot dirty avec validations requises cassables",
"2. lot dirty avec validations advisory ou docs",
"3. repo clean mais en retard sur le remote",
"4. regime stable sans lot local detecte",
"",
"## Etat Git courant",
"",
f"- branche: `{branch}`",
f"- dirty paths: `{len(dirty_paths)}`",
f"- ahead: `{ahead}`",
f"- behind: `{behind}`",
"",
]
if dirty_paths:
lines.append("### Fichiers dirty detectes")
lines.append("")
for path in dirty_paths:
lines.append(f"- `{path}`")
lines.append("")
lines.append("## Lots detectes")
lines.append("")
if not lots:
lines.extend(
[
"- Aucun lot local utile detecte.",
"- Si le repo est clean et a jour, le prochain lot utile devient un chantier decide par l'operateur.",
"",
]
)
else:
for index, lot in enumerate(lots, start=1):
lines.append(f"### {index}. `{lot.key}` — {lot.title}")
lines.append("")
lines.append(lot.description)
lines.append("")
refs = ", ".join(f"`{ref}`" for ref in lot.plan_refs)
lines.append(f"- references: {refs}")
lot_results = results.get(lot.key, [])
if lot_results:
done = sum(1 for item in lot_results if item["status"] == "done")
advisory = sum(1 for item in lot_results if item["status"] == "advisory")
blocked = sum(1 for item in lot_results if item["status"] == "blocked")
lines.append(
f"- validations: `{done}` done, `{advisory}` advisory, `{blocked}` blocked"
)
else:
lines.append("- validations: non executees sur ce cycle")
lines.append("")
blockers = sorted(
{
str(item["blocker"])
for lot_items in results.values()
for item in lot_items
if item.get("status") == "blocked" and item.get("blocker")
}
)
lines.append("## Questions a poser seulement si besoin reel")
lines.append("")
if blockers:
for blocker in blockers:
lines.append(f"- {blocker}")
else:
lines.append("- Aucune question bloquante detectee sur ce cycle.")
lines.append("")
lines.append("## Commandes operateur")
lines.append("")
lines.append("- `bash tools/run_autonomous_next_lots.sh status`")
lines.append("- `bash tools/run_autonomous_next_lots.sh run`")
lines.append("- `bash tools/run_autonomous_next_lots.sh json`")
lines.append("")
return "\n".join(lines)
def render_todo(lots: list[Lot], results: dict[str, list[dict[str, object]]]) -> str:
lines = [
"# 18) TODO enchainement autonome des lots utiles",
"",
f"Last updated: {now_label()}",
"",
"Ce fichier est regenere localement par `tools/autonomous_next_lots.py`.",
"",
]
if not lots:
lines.extend(
[
"- done: aucun lot local utile detecte",
"- next: attendre un nouveau delta ou choisir explicitement un chantier",
"",
]
)
return "\n".join(lines)
for lot in lots:
lines.append(f"## `{lot.key}` — {lot.title}")
lines.append("")
lines.append(f"- done: lot detecte ({lot.description})")
lot_results = results.get(lot.key, [])
if not lot_results:
lines.append("- pending: validations non executees")
else:
for item in lot_results:
cmd = " ".join(item["cmd"])
status = str(item["status"])
summary = str(item["summary"] or "aucun detail")
lines.append(f"- {status}: `{cmd}`")
lines.append(f" resume: {summary}")
lines.append("")
return "\n".join(lines)
def write_docs(plan_text: str, todo_text: str) -> None:
PLAN_DOC.write_text(plan_text + "\n", encoding="utf-8")
TODO_DOC.write_text(todo_text + "\n", encoding="utf-8")
def main() -> int:
args = parse_args()
branch, dirty_paths, ahead, behind = git_status()
lots = detect_lots(dirty_paths)
if args.max_lots > 0:
lots = lots[: args.max_lots]
results: dict[str, list[dict[str, object]]] = {}
if args.mode == "run":
for lot in lots:
results[lot.key] = execute_lot(lot)
plan_text = render_plan(branch, dirty_paths, ahead, behind, lots, results)
todo_text = render_todo(lots, results)
should_write = not args.no_write and args.mode != "json"
copilot-pull-request-reviewer[bot] commented 2026-03-09 05:13:56 +00:00 (Migrated from github.com)
Review

The default status mode rewrites the plan/todo markdown files because should_write is true for any mode except json. This makes simply checking status dirty the working tree (and causes constant timestamp churn). Consider only writing docs in run mode, or gating writes behind an explicit --write flag (keeping status read-only by default).

    should_write = not args.no_write and args.mode == "run"
The default `status` mode rewrites the plan/todo markdown files because `should_write` is true for any mode except `json`. This makes simply checking status dirty the working tree (and causes constant timestamp churn). Consider only writing docs in `run` mode, or gating writes behind an explicit `--write` flag (keeping `status` read-only by default). ```suggestion should_write = not args.no_write and args.mode == "run" ```
if should_write:
write_docs(plan_text, todo_text)
payload = {
"branch": branch,
"dirty_paths": dirty_paths,
"ahead": ahead,
"behind": behind,
"lots": [
{
"key": lot.key,
"title": lot.title,
"priority": lot.priority,
"plan_refs": list(lot.plan_refs),
"results": results.get(lot.key, []),
}
for lot in lots
],
"plan_doc": str(PLAN_DOC.relative_to(ROOT)),
"todo_doc": str(TODO_DOC.relative_to(ROOT)),
}
if args.mode == "json":
print(json.dumps(payload, ensure_ascii=True, indent=2))
return 0
print(f"branch: {branch}")
print(f"dirty_paths: {len(dirty_paths)}")
print(f"lots: {', '.join(lot.key for lot in lots) if lots else 'none'}")
print(f"plan_doc: {payload['plan_doc']}")
print(f"todo_doc: {payload['todo_doc']}")
if args.mode == "run":
for lot in lots:
lot_results = results.get(lot.key, [])
done = sum(1 for item in lot_results if item["status"] == "done")
advisory = sum(1 for item in lot_results if item["status"] == "advisory")
blocked = sum(1 for item in lot_results if item["status"] == "blocked")
print(f"{lot.key}: done={done} advisory={advisory} blocked={blocked}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+14 -4
View File
@@ -5,11 +5,12 @@ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MODE="${1:-codex}"
KILL_LIFE_DIR="$REPO_DIR"
MASCARADE_DIR="${MASCARADE_DIR:-$(cd "$REPO_DIR/../mascarade" 2>/dev/null && pwd || true)}"
CODEX_BIN="${CODEX_BIN:-codex}"
INCLUDE_PLAYWRIGHT=1
APPLY=0
source "$REPO_DIR/tools/lib/runtime_home.sh"
usage() {
cat <<'EOF'
Usage:
@@ -59,8 +60,8 @@ emit_json() {
},
"validate-specs": {
"type": "local",
"command": "python3",
"args": ["$KILL_LIFE_DIR/tools/validate_specs.py", "--mcp"],
"command": "bash",
"args": ["$KILL_LIFE_DIR/tools/run_validate_specs_mcp.sh"],
"tools": ["*"]
},
"knowledge-base": {
@@ -161,7 +162,7 @@ codex_add() {
emit_codex() {
codex_add kicad --env "MASCARADE_DIR=$MASCARADE_DIR" -- bash "$KILL_LIFE_DIR/tools/hw/run_kicad_mcp.sh"
codex_add validate-specs -- python3 "$KILL_LIFE_DIR/tools/validate_specs.py" --mcp
codex_add validate-specs -- bash "$KILL_LIFE_DIR/tools/run_validate_specs_mcp.sh"
codex_add knowledge-base --env "MASCARADE_DIR=$MASCARADE_DIR" -- bash "$KILL_LIFE_DIR/tools/run_knowledge_base_mcp.sh"
codex_add github-dispatch --env "MASCARADE_DIR=$MASCARADE_DIR" -- bash "$KILL_LIFE_DIR/tools/run_github_dispatch_mcp.sh"
codex_add freecad --env "MASCARADE_DIR=$MASCARADE_DIR" -- bash "$KILL_LIFE_DIR/tools/run_freecad_mcp.sh"
@@ -204,6 +205,15 @@ while [[ $# -gt 0 ]]; do
done
[[ -n "$KILL_LIFE_DIR" ]] || { echo "Unable to resolve Kill_LIFE path" >&2; exit 1; }
if [[ -z "${MASCARADE_DIR:-}" ]]; then
MASCARADE_DIR="$(
kill_life_resolve_mascarade_dir \
"$REPO_DIR" \
"core/mascarade/integrations/knowledge_base.py" \
"core/mascarade/integrations/github_dispatch.py" \
"finetune/kicad_mcp_server"
)"
fi
[[ -n "$MASCARADE_DIR" ]] || { echo "Unable to resolve mascarade companion path" >&2; exit 1; }
case "$MODE" in
+107 -15
View File
@@ -52,6 +52,54 @@ compose() {
docker compose -f "$COMPOSE_FILE" "$@"
}
resolve_host_kicad_cli() {
if [ -n "${KICAD_CLI:-}" ] && [ -x "${KICAD_CLI}" ]; then
printf '%s' "$KICAD_CLI"
return 0
fi
if [ -x /Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli ]; then
printf '%s' /Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli
return 0
fi
command -v kicad-cli 2>/dev/null || true
}
resolve_host_freecad_cmd() {
if [ -n "${FREECAD_CMD:-}" ] && [ -x "${FREECAD_CMD}" ]; then
printf '%s' "$FREECAD_CMD"
return 0
fi
if [ -x /Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd ]; then
printf '%s' /Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd
return 0
fi
command -v freecadcmd 2>/dev/null || command -v FreeCADCmd 2>/dev/null || true
}
resolve_host_openscad() {
local -a candidates=()
local candidate=""
if [ -n "${OPENSCAD_BIN:-}" ] && [ -x "${OPENSCAD_BIN}" ]; then
candidates+=("${OPENSCAD_BIN}")
fi
if command -v openscad >/dev/null 2>&1; then
candidates+=("$(command -v openscad)")
fi
if [ -x /Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD ]; then
candidates+=(/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD)
fi
for candidate in "${candidates[@]}"; do
if "$candidate" --version >/dev/null 2>&1; then
printf '%s' "$candidate"
return 0
fi
done
return 1
}
ensure_service_up() {
local service="$1"
if ! compose ps --status running --services | grep -qx "$service"; then
@@ -140,25 +188,51 @@ up_cmd() {
}
doctor_cmd() {
ensure_service_up kicad-headless
ensure_service_up freecad-headless
ensure_service_up openscad-headless
local host_kicad=""
local host_freecad=""
local host_openscad=""
host_kicad="$(resolve_host_kicad_cli)"
host_freecad="$(resolve_host_freecad_cmd)"
host_openscad="$(resolve_host_openscad)"
if [ -z "$host_kicad" ]; then
ensure_service_up kicad-headless
fi
if [ -z "$host_freecad" ]; then
ensure_service_up freecad-headless
fi
if [ -z "$host_openscad" ]; then
ensure_service_up openscad-headless
fi
ensure_service_up platformio
run_shell_as_host_user \
kicad-headless \
/workspace/.cad-home/kicad-headless \
'mkdir -p "$HOME" && kicad-cli version'
if [ -n "$host_kicad" ]; then
"$host_kicad" version
else
run_shell_as_host_user \
kicad-headless \
/workspace/.cad-home/kicad-headless \
'mkdir -p "$HOME" && kicad-cli version'
fi
run_shell_in_service_user \
freecad-headless \
/workspace/.cad-home/freecad-headless \
'mkdir -p "$HOME" && freecadcmd -c "import FreeCAD; print(\".\".join(FreeCAD.Version()[:3]))"'
if [ -n "$host_freecad" ]; then
"$host_freecad" -c 'import FreeCAD; print(".".join(FreeCAD.Version()[:3]))'
else
run_shell_in_service_user \
freecad-headless \
/workspace/.cad-home/freecad-headless \
'mkdir -p "$HOME" && freecadcmd -c "import FreeCAD; print(\".\".join(FreeCAD.Version()[:3]))"'
fi
run_shell_as_host_user \
openscad-headless \
/workspace/.cad-home/openscad-headless \
'mkdir -p "$HOME" && openscad --version'
if [ -n "$host_openscad" ]; then
"$host_openscad" --version
else
run_shell_as_host_user \
openscad-headless \
/workspace/.cad-home/openscad-headless \
'mkdir -p "$HOME" && openscad --version'
fi
run_shell_as_host_user \
platformio \
@@ -177,6 +251,12 @@ doctor_mcp_cmd() {
}
kicad_cli_cmd() {
local host_kicad=""
host_kicad="$(resolve_host_kicad_cli)"
if [ -n "$host_kicad" ]; then
"$host_kicad" "$@"
return
fi
ensure_service_up kicad-headless
run_tool_as_host_user \
kicad-headless \
@@ -187,6 +267,12 @@ kicad_cli_cmd() {
}
freecad_cmd() {
local host_freecad=""
host_freecad="$(resolve_host_freecad_cmd)"
if [ -n "$host_freecad" ]; then
"$host_freecad" "$@"
return
fi
ensure_service_up freecad-headless
run_tool_in_service_user \
freecad-headless \
@@ -197,6 +283,12 @@ freecad_cmd() {
}
openscad_cmd() {
local host_openscad=""
host_openscad="$(resolve_host_openscad)"
if [ -n "$host_openscad" ]; then
"$host_openscad" "$@"
return
fi
ensure_service_up openscad-headless
run_tool_as_host_user \
openscad-headless \
+11 -1
View File
@@ -69,6 +69,7 @@ build_pythonpath() {
value="${PYTHONPATH}"
else
candidates=(
/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages
/usr/lib/kicad/lib/python3/dist-packages
/usr/local/lib/kicad/lib/python3/dist-packages
/usr/lib/python3/dist-packages
@@ -96,6 +97,11 @@ resolve_probe_python() {
return 0
fi
if [ -x /Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3.9 ]; then
printf '%s' /Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3.9
return 0
fi
if [ -x "$SERVER_DIR/venv/bin/python" ]; then
printf '%s' "$SERVER_DIR/venv/bin/python"
return 0
@@ -184,7 +190,11 @@ export KICAD_MCP_IMAGE="${KICAD_MCP_IMAGE:-kill_life_cad-kicad-mcp:latest}"
export KICAD_MCP_LOG_LEVEL="${KICAD_MCP_LOG_LEVEL:-warn}"
export KICAD_PYTHON_STDERR_LOG_LEVEL="${KICAD_PYTHON_STDERR_LOG_LEVEL:-WARNING}"
export KICAD_PYTHON_FILE_LOG_LEVEL="${KICAD_PYTHON_FILE_LOG_LEVEL:-INFO}"
MASCARADE_DIR="${MASCARADE_DIR:-$REPO_PARENT/mascarade}"
MASCARADE_DIR="$(
kill_life_resolve_mascarade_dir \
"$ROOT_DIR" \
"finetune/kicad_mcp_server"
)"
SERVER_DIR="$MASCARADE_DIR/finetune/kicad_mcp_server"
ENTRYPOINT="${KICAD_MCP_ENTRYPOINT:-$SERVER_DIR/dist/index.js}"
NODE_BIN="${NODE_BIN:-node}"
2
+46
View File
@@ -41,3 +41,49 @@ kill_life_runtime_home_init() {
kill_life_runtime_home_ensure() {
mkdir -p "$RUNTIME_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME"
}
kill_life_abs_path() {
local input="$1"
(cd "$input" >/dev/null 2>&1 && pwd)
}
kill_life_resolve_mascarade_dir() {
local root_dir="$1"
shift || true
local -a required_paths=("$@")
local -a candidates=()
local candidate=""
local abs_candidate=""
local required=""
local ok=1
if [ -n "${MASCARADE_DIR:-}" ]; then
candidates+=("$MASCARADE_DIR")
fi
candidates+=(
"$root_dir/../mascarade"
"$root_dir/../mascarade-main"
)
for candidate in "${candidates[@]}"; do
[ -n "$candidate" ] || continue
abs_candidate="$(kill_life_abs_path "$candidate" || true)"
[ -n "$abs_candidate" ] || continue
ok=0
for required in "${required_paths[@]}"; do
if [ -n "$required" ] && [ ! -e "$abs_candidate/$required" ]; then
ok=1
break
fi
done
if [ "$ok" -eq 0 ]; then
printf '%s' "$abs_candidate"
return 0
fi
done
return 1
}
+29 -3
View File
@@ -15,9 +15,35 @@ from typing import Any
PROTOCOL_VERSION = "2025-03-26"
ENV_ASSIGN_RE = re.compile(r"^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$")
DEFAULT_MASCARADE_DIR = Path(
os.environ.get("MASCARADE_DIR", Path(__file__).resolve().parents[2] / "mascarade")
).resolve()
def resolve_default_mascarade_dir() -> Path:
env_value = os.environ.get("MASCARADE_DIR", "").strip()
candidates = []
if env_value:
candidates.append(Path(env_value))
root = Path(__file__).resolve().parents[2]
candidates.extend([root / "mascarade", root / "mascarade-main"])
preferred_paths = (
"core/mascarade/integrations/knowledge_base.py",
"core/mascarade/integrations/github_dispatch.py",
"finetune/kicad_mcp_server",
)
for candidate in candidates:
resolved = candidate.resolve()
if all((resolved / rel).exists() for rel in preferred_paths):
return resolved
for candidate in candidates:
resolved = candidate.resolve()
if (resolved / "core/mascarade").exists():
return resolved
return (root / "mascarade").resolve()
DEFAULT_MASCARADE_DIR = resolve_default_mascarade_dir()
class SmokeError(RuntimeError):
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHON_BIN="$ROOT_DIR/.venv/bin/python"
if [[ ! -x "$PYTHON_BIN" ]]; then
PYTHON_BIN="python3"
fi
exec "$PYTHON_BIN" "$ROOT_DIR/tools/autonomous_next_lots.py" "$@"
+7 -2
View File
@@ -2,11 +2,15 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MASCARADE_DIR="${MASCARADE_DIR:-$ROOT_DIR/../mascarade}"
MASCARADE_ENV_FILE="${MASCARADE_ENV_FILE:-$MASCARADE_DIR/.env}"
SERVER_SCRIPT="$ROOT_DIR/tools/github_dispatch_mcp.py"
source "$ROOT_DIR/tools/lib/runtime_home.sh"
kill_life_runtime_home_init "$ROOT_DIR" "github-dispatch-mcp"
MASCARADE_DIR="$(
kill_life_resolve_mascarade_dir \
"$ROOT_DIR" \
"core/mascarade/integrations/github_dispatch.py"
)"
MASCARADE_ENV_FILE="${MASCARADE_ENV_FILE:-$MASCARADE_DIR/.env}"
copilot-pull-request-reviewer[bot] commented 2026-03-09 05:13:55 +00:00 (Migrated from github.com)
Review

MASCARADE_DIR is resolved via command substitution under set -euo pipefail. If kill_life_resolve_mascarade_dir fails, the script will exit at the assignment without printing a helpful error. Handle the failure explicitly and emit an actionable message (expected repo locations, how to override via MASCARADE_DIR).

if [[ -z "${MASCARADE_DIR:-}" ]]; then
  if ! MASCARADE_DIR="$(
    kill_life_resolve_mascarade_dir \
      "$ROOT_DIR" \
      "core/mascarade/integrations/github_dispatch.py"
  )"; then
    cat >&2 <<EOF
ERROR: Unable to resolve the Mascarade repository directory (MASCARADE_DIR).

The helper 'kill_life_resolve_mascarade_dir' failed while looking for:
  core/mascarade/integrations/github_dispatch.py
relative to:
  $ROOT_DIR

Make sure the Mascarade repository is checked out in an expected location
(for example as a sibling or subdirectory of this project), or override
autodetection by setting the MASCARADE_DIR environment variable explicitly,
e.g.:

  MASCARADE_DIR=/path/to/mascarade \\
    "$0" "$@"

EOF
    exit 1
  fi
fi
`MASCARADE_DIR` is resolved via command substitution under `set -euo pipefail`. If `kill_life_resolve_mascarade_dir` fails, the script will exit at the assignment without printing a helpful error. Handle the failure explicitly and emit an actionable message (expected repo locations, how to override via `MASCARADE_DIR`). ```suggestion if [[ -z "${MASCARADE_DIR:-}" ]]; then if ! MASCARADE_DIR="$( kill_life_resolve_mascarade_dir \ "$ROOT_DIR" \ "core/mascarade/integrations/github_dispatch.py" )"; then cat >&2 <<EOF ERROR: Unable to resolve the Mascarade repository directory (MASCARADE_DIR). The helper 'kill_life_resolve_mascarade_dir' failed while looking for: core/mascarade/integrations/github_dispatch.py relative to: $ROOT_DIR Make sure the Mascarade repository is checked out in an expected location (for example as a sibling or subdirectory of this project), or override autodetection by setting the MASCARADE_DIR environment variable explicitly, e.g.: MASCARADE_DIR=/path/to/mascarade \\ "$0" "$@" EOF exit 1 fi fi ```
load_mascarade_env() {
[[ -r "$MASCARADE_ENV_FILE" ]] || return 0
@@ -62,5 +66,6 @@ fi
}
kill_life_runtime_home_ensure
export MASCARADE_DIR
export PYTHONPATH="$MASCARADE_DIR/core${PYTHONPATH:+:$PYTHONPATH}"
exec "$CORE_PYTHON" "$SERVER_SCRIPT" "$@"
+7 -2
View File
@@ -2,11 +2,15 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MASCARADE_DIR="${MASCARADE_DIR:-$ROOT_DIR/../mascarade}"
MASCARADE_ENV_FILE="${MASCARADE_ENV_FILE:-$MASCARADE_DIR/.env}"
SERVER_SCRIPT="$ROOT_DIR/tools/knowledge_base_mcp.py"
source "$ROOT_DIR/tools/lib/runtime_home.sh"
kill_life_runtime_home_init "$ROOT_DIR" "knowledge-base-mcp"
MASCARADE_DIR="$(
kill_life_resolve_mascarade_dir \
"$ROOT_DIR" \
"core/mascarade/integrations/knowledge_base.py"
)"
MASCARADE_ENV_FILE="${MASCARADE_ENV_FILE:-$MASCARADE_DIR/.env}"
copilot-pull-request-reviewer[bot] commented 2026-03-09 05:13:55 +00:00 (Migrated from github.com)
Review

MASCARADE_DIR is resolved via command substitution under set -euo pipefail. If kill_life_resolve_mascarade_dir fails, the script will exit at the assignment with no actionable error. Consider handling the failure explicitly (e.g., MASCARADE_DIR="$(... || true)" followed by a check that prints guidance about expected locations / MASCARADE_DIR).

    "core/mascarade/integrations/knowledge_base.py" \
    || true
)"
if [[ -z "$MASCARADE_DIR" || ! -d "$MASCARADE_DIR" ]]; then
  echo "Failed to resolve MASCARADE_DIR via kill_life_resolve_mascarade_dir." >&2
  echo "ROOT_DIR=$ROOT_DIR" >&2
  echo "Ensure Mascarade core is available (e.g., at \$ROOT_DIR/core/mascarade) and retry." >&2
  exit 1
fi
`MASCARADE_DIR` is resolved via command substitution under `set -euo pipefail`. If `kill_life_resolve_mascarade_dir` fails, the script will exit at the assignment with no actionable error. Consider handling the failure explicitly (e.g., `MASCARADE_DIR="$(... || true)"` followed by a check that prints guidance about expected locations / `MASCARADE_DIR`). ```suggestion "core/mascarade/integrations/knowledge_base.py" \ || true )" if [[ -z "$MASCARADE_DIR" || ! -d "$MASCARADE_DIR" ]]; then echo "Failed to resolve MASCARADE_DIR via kill_life_resolve_mascarade_dir." >&2 echo "ROOT_DIR=$ROOT_DIR" >&2 echo "Ensure Mascarade core is available (e.g., at \$ROOT_DIR/core/mascarade) and retry." >&2 exit 1 fi ```
load_mascarade_env() {
[[ -r "$MASCARADE_ENV_FILE" ]] || return 0
@@ -62,5 +66,6 @@ fi
}
kill_life_runtime_home_ensure
export MASCARADE_DIR
export PYTHONPATH="$MASCARADE_DIR/core${PYTHONPATH:+:$PYTHONPATH}"
exec "$CORE_PYTHON" "$SERVER_SCRIPT" "$@"
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SERVER_SCRIPT="$ROOT_DIR/tools/validate_specs.py"
VENV_DIR="${KILL_LIFE_VENV_DIR:-$ROOT_DIR/.venv}"
detect_python() {
local candidate="${KILL_LIFE_PYTHON:-}"
if [[ -n "$candidate" && -x "$candidate" ]]; then
printf '%s' "$candidate"
return 0
fi
candidate="$VENV_DIR/bin/python"
if [[ -x "$candidate" ]]; then
printf '%s' "$candidate"
return 0
fi
if command -v python3 >/dev/null 2>&1; then
if python3 -c 'import yaml' >/dev/null 2>&1; then
printf '%s' "$(command -v python3)"
return 0
fi
fi
return 1
}
PYTHON_BIN="$(detect_python || true)"
if [[ "${1:-}" == "--doctor" ]]; then
cat <<EOF
ROOT_DIR=$ROOT_DIR
SERVER_SCRIPT=$SERVER_SCRIPT
VENV_DIR=$VENV_DIR
PYTHON_BIN=$PYTHON_BIN
EOF
exit 0
fi
[[ -n "$PYTHON_BIN" && -x "$PYTHON_BIN" ]] || {
echo "Missing validate-specs python runtime. Expected $VENV_DIR/bin/python or a python3 with PyYAML." >&2
exit 1
}
if [[ "$#" -eq 0 ]]; then
exec "$PYTHON_BIN" "$SERVER_SCRIPT" --mcp
fi
exec "$PYTHON_BIN" "$SERVER_SCRIPT" "$@"
+2 -2
View File
@@ -18,7 +18,7 @@ from mcp_smoke_common import (
)
ROOT = Path(__file__).resolve().parents[1]
SERVER = ROOT / "tools" / "validate_specs.py"
SERVER = ROOT / "tools" / "run_validate_specs_mcp.sh"
def parse_args() -> argparse.Namespace:
@@ -31,7 +31,7 @@ def parse_args() -> argparse.Namespace:
def main() -> int:
args = parse_args()
proc = spawn_server(["python3", str(SERVER), "--mcp"], ROOT)
proc = spawn_server(["bash", str(SERVER)], ROOT)
payload = {
"status": "failed",
"protocol_version": None,