feat: automate mac mcp cad host bootstrap #17
@@ -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`
|
||||
|
||||
|
|
||||
## 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
|
||||
|
This generated TODO file includes user-specific absolute paths in the validation summaries (e.g., 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:])
|
||||
|
`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"
|
||||
|
The default 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())
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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" "$@"
|
||||
@@ -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}"
|
||||
|
||||
|
`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" "$@"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
`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" "$@"
|
||||
|
||||
@@ -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" "$@"
|
||||
@@ -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,
|
||||
|
||||
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).