From 918eab8cdd2b61bd4f44ccd0acee250b245ac987 Mon Sep 17 00:00:00 2001
From: Jeremy Anderson
v3.1.1 — Codename: Ankh of Jah (local-coder-mesh build) This build includes the local-coder-mesh integration — see CHANGES.md for the full list of changes vs upstream v3.1.
+ Author: Jeremy Anderson <info@dcos.net>
+ Web: https://dcos.net
+
A PySide6 desktop application for orchestrating local AI/ML tool stacks across a 10-layer architecture.
@@ -238,7 +239,7 @@ ai_lsc/
### Quick Install
```bash
-git clone https://github.com/your-username/ai-lsc.git
+git clone https://github.com/dcos-net/ai-lsc.git
cd ai-lsc
pip install -e .
```
diff --git a/TODO.md b/TODO.md
index 2cd4be4..711fd3c 100755
--- a/TODO.md
+++ b/TODO.md
@@ -3,7 +3,7 @@
## Recent progress (v3.2 — 10-Layer Systems Architecture)
- ✅ Completed the 10-layer taxonomy migration that was started on top of v3.1.1b: `defaults.py` (108-tool seed), all 11 modular layer files (186 tools), `stack/connections.py` (92 static layers + 5 dynamic loop lookups), CATEGORY_MAP cascade (221 categories), validator level range, README/quickstart/docs. See [CHANGES.md](CHANGES.md) → v3.2.
-- ✅ Repaired `ui/pages/db_manager.py`: the interim migration had truncated it to 222 lines (syntax error + lost implementation); restored in full from the v3.1.1b routing tarball and migrated its CATEGORY_MAP.
+- ✅ Repaired `ui/pages/db_manager.py`: the interim migration had truncated it to 222 lines (syntax error + lost implementation); the canonical 1.3 k-line implementation now lives in-tree and its CATEGORY_MAP is migrated to the 10-layer taxonomy.
- ✅ Reconciled `kanban` into the layer files (was defaults-only) — merged registry is now 186 tools.
- ✅ Preserved all operational registry metadata (installers, launchers, deps, flags, licenses, filesystem blocks) that the master-target defaults rewrite had stripped; curl|sh policy markers untouched (C-05).
- ✅ `open_webui` vs `openwebui` orphan gap: confirmed moot — only `openwebui` exists in the registry and it has a STACK_WIRINGS entry.
diff --git a/bootstrap.sh b/bootstrap.sh
index 661c51b..95f3ae4 100755
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -68,34 +68,36 @@ if command -v pipx &>/dev/null && pipx list 2>/dev/null | grep -q "ai-lsc"; then
fi
# ── Ensure AI_BASE exists ─────────────────────────────────────
-if [ ! -d "$AI_BASE" ]; then
- if [ "$(id -u)" -eq 0 ]; then
- mkdir -p "$AI_BASE"
- info "Created ${AI_BASE} (running as root)"
+# Step-down (early-return) logic per Unix philosophy: each branch
+# resolves the situation and falls through; the bottom of the block
+# is the success path.
+if [ -d "$AI_BASE" ]; then
+ : # already present — nothing to do
+elif [ "$(id -u)" -eq 0 ]; then
+ mkdir -p "$AI_BASE"
+ info "Created ${AI_BASE} (running as root)"
+else
+ # Non-root path: prompt for action, escalate if possible.
+ echo ""
+ echo -e "${YELLOW}${AI_BASE} does not exist.${NC}"
+ echo " This is the managed working directory for all AI tools."
+ echo ""
+ read -p "Create ${AI_BASE} now? [Y/n] " -n 1 -r
+ echo
+ # User declined the canonical path → fall through to alternative.
+ if [[ $REPLY =~ ^[Nn]$ ]]; then
+ read -p "Alternative base directory? [${SCRIPT_DIR}/ai-stack] " ALT_BASE
+ ALT_BASE="${ALT_BASE:-${SCRIPT_DIR}/ai-stack}"
+ mkdir -p "$ALT_BASE"
+ warn "Using ${ALT_BASE}"
+ AI_BASE="$ALT_BASE"
+ export AI_LSC_BASE_DIR="$AI_BASE"
+ elif command -v sudo &>/dev/null; then
+ sudo mkdir -p "$AI_BASE"
+ sudo chown "$(id -u):$(id -g)" "$AI_BASE"
+ info "Created ${AI_BASE}"
else
- echo ""
- echo -e "${YELLOW}${AI_BASE} does not exist.${NC}"
- echo " This is the managed working directory for all AI tools."
- echo ""
- read -p "Create ${AI_BASE} now? [Y/n] " -n 1 -r
- echo
- if [[ ! $REPLY =~ ^[Nn]$ ]]; then
- if command -v sudo &>/dev/null; then
- sudo mkdir -p "$AI_BASE"
- sudo chown "$(id -u):$(id -g)" "$AI_BASE"
- info "Created ${AI_BASE}"
- else
- error "Need sudo/root to create ${AI_BASE}. Create it manually and re-run."
- fi
- else
- echo ""
- read -p "Alternative base directory? [${SCRIPT_DIR}/ai-stack] " ALT_BASE
- ALT_BASE="${ALT_BASE:-${SCRIPT_DIR}/ai-stack}"
- mkdir -p "$ALT_BASE"
- warn "Using ${ALT_BASE}"
- AI_BASE="$ALT_BASE"
- export AI_LSC_BASE_DIR="$AI_BASE"
- fi
+ error "Need sudo/root to create ${AI_BASE}. Create it manually and re-run."
fi
fi
diff --git a/docs/ADR-003-workspace-tab.md b/docs/ADR-003-workspace-tab.md
index 8ab66ab..7cddbb6 100755
--- a/docs/ADR-003-workspace-tab.md
+++ b/docs/ADR-003-workspace-tab.md
@@ -64,7 +64,7 @@ The emoji encoding lets the user scan the tab bar at a glance and see what kind
### Sub-tab closing
-Sub-tabs are closable via the standard × button. Closing a sub-tab calls `stop_polling()` on the page (for CLI tools) but does **NOT** stop the underlying tool — that's the user's call from the Stack Editor. The empty-state placeholder is restored when all sub-tabs are closed.
+Sub-tabs are closable via the standard × button. Closing a sub-tab calls `stop_polling()` on the page (for CLI tools) but does **NOT** stop the underlying tool — that's the user's call from the Stack Editor. When all sub-tabs are closed the empty-state placeholder is shown again.
## Refresh model
diff --git a/pyproject.toml b/pyproject.toml
index e27ce53..48da486 100755
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,7 +13,10 @@ readme = "README.md"
license = {text = "AGPL-3.0-or-later"}
requires-python = ">=3.11"
authors = [
- {name = "AI-LSC Contributors"},
+ {name = "Jeremy Anderson", email = "info@dcos.net"},
+]
+maintainers = [
+ {name = "Jeremy Anderson", email = "info@dcos.net"},
]
keywords = [
"ai", "llm", "local-ai", "stack-management", "pyside6",
diff --git a/scripts/apply_10layer_taxonomy.py b/scripts/apply_10layer_taxonomy.py
index 259d803..b8399a8 100755
--- a/scripts/apply_10layer_taxonomy.py
+++ b/scripts/apply_10layer_taxonomy.py
@@ -198,10 +198,10 @@ EXTRA_CATEGORY_LAYERS: dict[str, str] = {
"Observability": "Human Interface & System Operations",
}
-# The v3.1.1b db_manager (restored from the routing tarball) carries a
+# The v3.1.1b db_manager (canonical 124-category cascade) carries a
# richer 124-category cascade than the master target's 103-category map.
-# These alt-only categories are preserved and translated to the 10-layer
-# taxonomy so the categorisation cascade keeps its full coverage.
+# These alt-only categories are translated to the 10-layer taxonomy so
+# the categorisation cascade keeps its full coverage.
ALT_CATEGORY_TRANSLATIONS: dict[str, str] = {
"AI Assistant Platform": "Human Interface & System Operations",
"AI Augmentation": "Multi-Agent Orchestration Runtimes",
@@ -665,7 +665,7 @@ SECTION_RENAMES = [
"# L3: GPU Runtime → Layer 3: GPU Acceleration & Optimization"),
("# L4: Inference Engines",
"# L4: Inference Engines → Layer 4: Local Inference Engines"),
- ("# L6: AI Endpoints (→ the restored \"Routing\" layer in the 11-layer taxonomy)",
+ ("# L6: AI Endpoints (→ the L5 \"Routing\" layer in the 11-layer taxonomy)",
"# L6: AI Endpoints → Layer 5: Intelligent API Routers & Proxies (10-layer taxonomy)"),
("# L10: Intelligent Routing (folded into \"Orchestrators\" in the 11-layer taxonomy)",
"# L10: Intelligent Routing → Layer 5: Intelligent API Routers & Proxies (10-layer taxonomy)"),
@@ -754,8 +754,8 @@ def category_map_lines(cmap: dict[str, dict]) -> str:
def stage_category_map() -> None:
- # read the current CATEGORY_MAP from the restored v3.1.1b db_manager
- # (full 124-category cascade in the old 11-layer taxonomy)
+ # Read the current CATEGORY_MAP from the canonical v3.1.1b db_manager
+ # (full 124-category cascade in the old 11-layer taxonomy).
dbm = SRC / "ai_lsc" / "ui" / "pages" / "db_manager.py"
content = dbm.read_text(encoding="utf-8")
m = re.search(
diff --git a/src/ai_lsc/manifest/support.py b/src/ai_lsc/manifest/support.py
index 08e21c3..11b47e6 100755
--- a/src/ai_lsc/manifest/support.py
+++ b/src/ai_lsc/manifest/support.py
@@ -76,26 +76,32 @@ class ManifestSupport:
@staticmethod
def build_system_context(manifest: dict[str, Any]) -> str:
- """Build a flat system-prompt text block from manifest data."""
+ """Build a flat system-prompt text block from manifest data.
+
+ Source-of-truth boundary: this method renders a *derived* view of
+ the manifest dict. The manifest file itself is an optional
+ convenience; if absent, callers fall back to defaults — the
+ registry layer files in :mod:`ai_lsc.registry.layers` remain
+ the authoritative source for tool definitions.
+ """
+ # (manifest_key, label) pairs in display order. Each entry whose
+ # manifest value is truthy becomes one line in the prompt.
+ _FIELDS: tuple[tuple[str, str], ...] = (
+ ("description", "Description"),
+ ("language", "Language"),
+ ("entry_point", "Entry Point"),
+ ("architecture", "Architecture"),
+ ("environment_notes", "Environment"),
+ )
project = manifest.get("project", "Unknown Project")
- description = manifest.get("description", "")
- language = manifest.get("language", "")
- entry = manifest.get("entry_point", "")
- architecture = manifest.get("architecture", "")
- environment = manifest.get("environment_notes", "")
dependencies = manifest.get("dependencies", [])
parts = [f"Project: {project}"]
- if description:
- parts.append(f"Description: {description}")
- if language:
- parts.append(f"Language: {language}")
- if entry:
- parts.append(f"Entry Point: {entry}")
- if architecture:
- parts.append(f"Architecture: {architecture}")
- if environment:
- parts.append(f"Environment: {environment}")
+ parts.extend(
+ f"{label}: {manifest.get(key)}"
+ for key, label in _FIELDS
+ if manifest.get(key)
+ )
if dependencies:
parts.append(f"Dependencies: {', '.join(dependencies)}")
@@ -110,15 +116,15 @@ class ManifestSupport:
base = Path(base_dir)
patterns = manifest.get("context_files", [])
exclude = set(manifest.get("exclude", []))
- files: list[str] = []
- for pattern in patterns:
- full_pattern = str(base / pattern)
- matched = glob_mod.glob(full_pattern, recursive=True)
- for f in matched:
- if Path(f).is_file() and not any(ex in f for ex in exclude):
- files.append(f)
- return files
+ # Single-pass comprehension: expand every pattern, keep only
+ # regular files whose path does not contain any excluded token.
+ return [
+ f
+ for pattern in patterns
+ for f in glob_mod.glob(str(base / pattern), recursive=True)
+ if Path(f).is_file() and not any(ex in f for ex in exclude)
+ ]
@staticmethod
def load_jcl(path: str | Path) -> list[dict[str, Any]]:
diff --git a/src/ai_lsc/registry/layers/routing.py b/src/ai_lsc/registry/layers/routing.py
index c976693..7b5fae5 100755
--- a/src/ai_lsc/registry/layers/routing.py
+++ b/src/ai_lsc/registry/layers/routing.py
@@ -1,18 +1,18 @@
"""Registry entries for the Routing layer (L5).
-Restored layer (v3.1.1b): model gateways, LLM proxies, request routers,
-mesh transport, and the model-routing tier that sits between the Engines
-(who serve weights) and the Orchestrators (who build agent workflows on
-top of a single OpenAI-compatible endpoint). This re-unites the old
-13-layer model's "AI Endpoints" tier (LiteLLM, model routers, API
-gateways) that had been folded into Orchestrators during the 13-to-10
-reorg, plus the mesh-aware clients that ride on it.
+L5 (Intelligent API Routers & Proxies) holds the model-routing tier
+that sits between the Engines (who serve weights) and the Orchestrators
+(who build agent workflows on top of a single OpenAI-compatible
+endpoint): model gateways, LLM proxies, request routers, mesh
+transport, and the mesh-aware clients that ride on it.
-This module is consumed by :mod:`ai_lsc.registry.loader`.
+This module is consumed by :mod:`ai_lsc.registry.loader`. The loader
+merges by tool, not by filename, so tools may be regrouped across
+files in a future pass without a behavior change.
-Structural fields (layer, level) follow the 10-Layer Systems
-Architecture Taxonomy; tools may be regrouped across files in a
-future pass — the loader merges by tool, not by filename.
+Source of truth: the entries below. The merged registry produced by
+``loader.load_merged_registry()`` is a derived view; if any manifest
+or cached ``ecosystem.json`` disagrees, the source in this file wins.
"""
TOOLS: dict[str, dict] = {
diff --git a/src/ai_lsc/registry/openengineer/importer.py b/src/ai_lsc/registry/openengineer/importer.py
index 4d99e68..a1feba7 100755
--- a/src/ai_lsc/registry/openengineer/importer.py
+++ b/src/ai_lsc/registry/openengineer/importer.py
@@ -52,6 +52,12 @@ _OE_CONTENT_DIRS: list[str] = [
# File extensions we attempt to parse.
_OE_PARSE_EXTENSIONS: set[str] = {".md", ".markdown", ".txt"}
+# Root-level files that are repo meta, not OE content.
+_OE_ROOT_SKIP_NAMES: frozenset[str] = frozenset({
+ "README.md", "CONTRIBUTING.md", "CHARTER.md",
+ "LICENSE", "ROADMAP.md",
+})
+
class OpenEngineerImporter:
"""Import Open Engineer files into StandardTemplate objects.
@@ -164,51 +170,53 @@ class OpenEngineerImporter:
overrides = stack_config_overrides or {}
templates: list[StandardTemplate] = []
- # Scan known OE content directories
+ # Pass 1 — known OE content subdirs (templates inferred from OE fields).
for subdir_name in _OE_CONTENT_DIRS:
subdir = root / subdir_name
if not subdir.is_dir():
continue
+ templates.extend(self._scan_dir(subdir, overrides=overrides, drop_unknown=False))
- for md_file in sorted(subdir.iterdir()):
- if md_file.suffix.lower() not in _OE_PARSE_EXTENSIONS:
- continue
- if md_file.name.startswith("."):
- continue
+ # Pass 2 — root-level standalone OE files (drop unknowns so README/LICENSE/etc.
+ # do not pollute the import). The _OE_ROOT_SKIP_NAMES filter applies here only.
+ templates.extend(self._scan_dir(root, overrides=None, drop_unknown=True, skip=_OE_ROOT_SKIP_NAMES))
- try:
- tpl_id = self._derive_template_id(
- md_file, md_file.stem
- )
- override = overrides.get(tpl_id)
- template = self.import_file(md_file, stack_config=override)
- templates.append(template)
- except Exception:
- # Don't let one bad file stop the import
- continue
+ return sorted(templates, key=lambda t: t.source_file)
- # Also scan root for standalone OE files
- for md_file in sorted(root.iterdir()):
+ def _scan_dir(
+ self,
+ subdir: Path,
+ *,
+ overrides: dict[str, dict[str, Any]] | None = None,
+ drop_unknown: bool = False,
+ skip: frozenset[str] | None = None,
+ ) -> list[StandardTemplate]:
+ """Import every parseable file directly inside *subdir*.
+
+ Single-responsibility scan loop: yields one StandardTemplate per
+ parseable markdown file. Errors per file are swallowed so one bad
+ file does not abort the scan.
+ """
+ results: list[StandardTemplate] = []
+ for md_file in sorted(subdir.iterdir()):
if not md_file.is_file():
continue
if md_file.suffix.lower() not in _OE_PARSE_EXTENSIONS:
continue
- if md_file.name in {
- "README.md", "CONTRIBUTING.md", "CHARTER.md",
- "LICENSE", "ROADMAP.md",
- }:
- continue
if md_file.name.startswith("."):
continue
-
+ if skip and md_file.name in skip:
+ continue
try:
- template = self.import_file(md_file)
- if template.source_type != "unknown":
- templates.append(template)
+ tpl_id = self._derive_template_id(md_file, md_file.stem)
+ override = (overrides or {}).get(tpl_id)
+ template = self.import_file(md_file, stack_config=override)
except Exception:
continue
-
- return sorted(templates, key=lambda t: t.source_file)
+ if drop_unknown and template.source_type == "unknown":
+ continue
+ results.append(template)
+ return results
# ── Bulk convert to AI-LSC format ──────────────────────────────
diff --git a/src/ai_lsc/runtime/installer.py b/src/ai_lsc/runtime/installer.py
index 789ec20..057cde6 100755
--- a/src/ai_lsc/runtime/installer.py
+++ b/src/ai_lsc/runtime/installer.py
@@ -574,6 +574,8 @@ class InstallerManager:
"""
dest = os.path.join(self.tools_root, tool_id)
git_dir = os.path.join(dest, ".git")
+
+ # Step-down: each branch resolves one situation and assigns msg.
if os.path.isdir(git_dir):
try:
subprocess.run(
@@ -586,36 +588,22 @@ class InstallerManager:
"git pull failed for %s (%s); re-cloning fresh",
tool_id, exc,
)
- backup = dest + ".bak." + str(int(time.time()))
- shutil.move(dest, backup)
- os.makedirs(dest, exist_ok=True)
- subprocess.run(
- ["git", "clone", pkg, dest],
- check=True, timeout=600,
- )
+ backup = self._backup_existing(dest)
+ self._clone_fresh(pkg, dest)
msg = (
f"Git source re-cloned (pull failed, "
f"old copy at {backup}): {dest}"
)
elif os.path.exists(dest):
# Dir exists but is not a git repo — back it up and clone.
- backup = dest + ".bak." + str(int(time.time()))
- shutil.move(dest, backup)
- os.makedirs(dest, exist_ok=True)
- subprocess.run(
- ["git", "clone", pkg, dest],
- check=True, timeout=600,
- )
+ backup = self._backup_existing(dest)
+ self._clone_fresh(pkg, dest)
msg = (
f"Git source cloned (existing non-git dir backed up "
f"at {backup}): {dest}"
)
else:
- os.makedirs(dest, exist_ok=True)
- subprocess.run(
- ["git", "clone", pkg, dest],
- check=True, timeout=600,
- )
+ self._clone_fresh(pkg, dest)
msg = f"Git source cloned: {dest}"
if post_install:
@@ -638,6 +626,9 @@ class InstallerManager:
"""
dest = os.path.join(self.tools_root, tool_id)
git_dir = os.path.join(dest, ".git")
+
+ # Step-down: each branch resolves one situation, runs the
+ # appropriate clone/pull+yarn sequence, and assigns msg.
if os.path.isdir(git_dir):
try:
subprocess.run(
@@ -653,12 +644,8 @@ class InstallerManager:
"git pull / yarn install failed for %s (%s); re-cloning",
tool_id, exc,
)
- backup = dest + ".bak." + str(int(time.time()))
- shutil.move(dest, backup)
- os.makedirs(dest, exist_ok=True)
- subprocess.run(
- ["git", "clone", pkg, dest], check=True, timeout=600,
- )
+ backup = self._backup_existing(dest)
+ self._clone_fresh(pkg, dest)
subprocess.run(
["yarn", "install"], cwd=dest, check=True, timeout=300,
)
@@ -667,12 +654,8 @@ class InstallerManager:
f"old copy at {backup}): {dest}"
)
elif os.path.exists(dest):
- backup = dest + ".bak." + str(int(time.time()))
- shutil.move(dest, backup)
- os.makedirs(dest, exist_ok=True)
- subprocess.run(
- ["git", "clone", pkg, dest], check=True, timeout=600,
- )
+ backup = self._backup_existing(dest)
+ self._clone_fresh(pkg, dest)
subprocess.run(
["yarn", "install"], cwd=dest, check=True, timeout=300,
)
@@ -681,10 +664,7 @@ class InstallerManager:
f"up at {backup}): {dest}"
)
else:
- os.makedirs(dest, exist_ok=True)
- subprocess.run(
- ["git", "clone", pkg, dest], check=True, timeout=600,
- )
+ self._clone_fresh(pkg, dest)
subprocess.run(
["yarn", "install"], cwd=dest, check=True, timeout=300,
)
@@ -694,6 +674,27 @@ class InstallerManager:
self._run_post_install(tool_id, post_install)
return msg
+ # ── git install helpers ────────────────────────────────────────
+
+ @staticmethod
+ def _backup_existing(dest: str) -> str:
+ """Move *dest* aside to ``
")
)
- is_error = "?" in msg["identity"]
- style_key = (
- "user" if msg["is_user"]
- else ("error" if is_error else "assistant")
- )
- s = bubble_styles[style_key]
- label_color = (
- "#e0e0e0" if msg["is_user"]
- else ("#e74c3c" if is_error else "#d35400")
- )
- label = (
- "You" if msg["is_user"]
- else f"{msg['identity']}"
- )
- border_css = (
- f"border: {s['border']};" if "border" in s else ""
- )
- html += (
+ if style_key == "user":
+ label = _identity_label["user"]
+ else:
+ color = _identity_color[style_key]
+ label = f"{msg['identity']}"
+ border_css = f"border: {s['border']};" if "border" in s else ""
+ return (
f"