From 71f07b05bf27fd649598098f476ef77d5d2fedfe Mon Sep 17 00:00:00 2001 From: Fahmi Harun <34875577+kuker24@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:18:27 +0700 Subject: [PATCH] feat: adapt AI LABS wave (img2threejs, reticle, ui-skills, anti-slop merge) --- CHANGELOG.md | 7 +- README.md | 11 ++- docs/architecture.md | 4 +- docs/mcp.md | 6 ++ docs/routing.md | 2 +- docs/skills.md | 4 +- docs/warehouse-inventory.md | 17 ++++ lib/cli.py | 18 ++++ lib/doctor.py | 27 +++++- lib/install.py | 98 +++++++++++++------ rules/00-routing.md | 4 +- rules/03-prose-discipline.md | 2 + skills/img2threejs/NOTICE.md | 10 ++ skills/img2threejs/SKILL.md | 50 ++++++++++ skills/impeccable/SKILL.md | 3 +- skills/impeccable/reference/taste-guard.md | 2 + skills/install-anti-slop/NOTICE.md | 3 + skills/install-anti-slop/SKILL.md | 2 +- templates/AGENTS.md | 2 +- tests/test_doctor.py | 80 ++++++++++++++++ tests/test_evaluation_matrix.py | 18 ++++ tests/test_install.py | 106 ++++++++++++++++++++- tests/test_migration.py | 2 +- tests/test_routing.py | 16 ++++ tests/test_skills.py | 6 +- vendor/license-audit.json | 5 + vendor/mcp-policy.json | 11 +++ vendor/mcp-wanted.json | 17 ++++ vendor/skill-allowlist.txt | 1 + vendor/skill-policy.json | 3 + vendor/sources.json | 14 +++ 31 files changed, 498 insertions(+), 53 deletions(-) create mode 100644 skills/img2threejs/NOTICE.md create mode 100644 skills/img2threejs/SKILL.md diff --git a/CHANGELOG.md b/CHANGELOG.md index dabac51..b7bd945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,13 @@ ## Unreleased -Catalog hygiene patch. No new skills, no new MCP, no foreign harness runtime. Totals stay **62** (46 model-invoked, 16 manual slash commands). +Catalog update for AI LABS 8-repo wave adaptation and hygiene patch. Totals become **63** (47 model-invoked, 16 manual slash commands). +- Add first-party model-invoked specialist `img2threejs` for code-only procedural Three.js TypeScript model reconstruction from reference object images (editable Group factory, visible vs. inferred geometry spec, screenshot loop gate; no mesh blob downloads). +- Add optional `reticle` MCP (`opencode-bf reticle enable` / `disable`) registering local perception server (`npx -y @reticlehq/server mcp`). `FOREIGN_ON_DEMAND`, server is FSL-1.1-ALv2, perception only, never an auto-implementer; default verification remains `playwright-qa` / `chrome-devtools-axi`. +- Add optional `ui-skills` remote MCP (`opencode-bf ui-skills enable` / `disable`) registering `https://www.ui-skills.com/mcp` for design-skill lookup only. Product UI remains Design Bank + Impeccable + Design V2 atoms + shadcn; `BANK_MISS` never generates from random ui-skills documents. +- Anti-slop selective merge: incorporate visual and copy anti-patterns (no default purple-gradient hero, glowing mesh, Inter-on-white-card slop, fake testimonials, or "Let's dive in" clichés) into `skills/impeccable/reference/taste-guard.md` and `rules/03-prose-discipline.md` with attribution to `miqdadbadjuber/anti-slop` MIT in `skills/install-anti-slop/NOTICE.md`. Zero extra catalog skills added. +- Document warehouse dispositions for AI LABS 8 wave: `img2threejs` (NEW), `reticle` (FOREIGN_ON_DEMAND), `ui-skills` (FOREIGN_ON_DEMAND), `chisel` hooks (REJECT), `ouroboros`/Q00 (REJECT), `swiftui-skills` (DEFER), `caliper` (FOREIGN_ON_DEMAND maintainer CLI), and `anti-slop` (MERGE). - Sync stale user-facing catalog copy (`59` / `43 model-invoked`) to the measured tree in `README.md`, `docs/skills.md`, and `docs/architecture.md`. Historical release entries keep their original numbers. - `skill-stocktake`: replace the schema-only sweep with a verdict protocol. Every catalog item resolves to exactly one of `KEEP`, `COMPRESS`, `UPDATE`, `MERGE → `, or `RETIRE`, backed by an existence pass, a currency pass, and cited evidence. The skill never rewrites or deletes another skill; `COMPRESS`/`UPDATE` hand off to `writing-for-agents` or `prompt-optimizer`. - `eval-harness`: add `references/skill-utility.md`, an A/B utility gate that scores a task with the skill (Run B) against the same task without it (Run A). Quality parity with lower token or latency passes; `A ≈ B` on the skill's own claimed task is evidence for `RETIRE` or `COMPRESS`. Deterministic assertions first, no external harness, no API key. diff --git a/README.md b/README.md index 5e7f62e..60444d0 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # OpenCodeBestFriend Production-ready capability layer for OpenCode: -62 routed skills (core + Wave 2/3 warehouse specialists), MCP, Codebase Memory, +63 routed skills (core + Wave 2/3 warehouse specialists), MCP, Codebase Memory, Design Bank, Design Intelligence, SmartDoc, browser and verification tooling. OpenCodeBestFriend is an installer and runtime overlay for [OpenCode](https://opencode.ai). It is **not** Claude Code, **not** a model provider, and **not** a dump of a developer home directory. ## What it is -- 62 skills: 46 model-invoked, 16 manual slash commands +- 63 skills: 47 model-invoked, 16 manual slash commands - A thin `AGENTS.md` router (lazy, one primary specialist) - Core MCP: Codebase Memory, Context7, shadcn - Design Bank discovery or download (media is **not** in git) @@ -97,6 +97,7 @@ Default: repository evidence first. Then at most one specialist. | Photoreal / media | `visual-studio` | | Scroll-led storytelling | `scroll-craft` | | Scroll-driven 3D / camera world | `scroll-world` | +| Procedural Three.js object from image | `img2threejs` | | Deterministic HTML composition video | `hyperframes` | | Documents (PDF/DOCX/answer/extract/review) | `smartdoc` | | Reusable book/module knowledge | `smartbook-ingest` | @@ -125,10 +126,12 @@ Core (installed): Optional: - `serena` — host binary may exist; MCP is **not** registered unless you run `opencode-bf serena enable` -- `exa` — `FOREIGN_ON_DEMAND`; installer never adds, removes, or overwrites it - `stitch` — `opencode-bf stitch enable` registers Google Stitch as a remote comp/mock source. Not an owned core server and not a production UI implementer: hand screens to `found-this-design` or `impeccable` before shipping. Keys are never written into config, only referenced as `{env:STITCH_API_KEY}`, or omitted with `--oauth`. `opencode-bf stitch disable` removes only that server key. Absent is not a `doctor` failure; a malformed entry fails closed. +- `reticle` — `opencode-bf reticle enable` registers Reticle as a local perception server (`npx -y @reticlehq/server mcp`). `FOREIGN_ON_DEMAND`. Server package is FSL-1.1-ALv2 (competing-use clause); SDK packages (Apache-2.0) are not vendored. Never an auto-implementer; default verification remains `playwright-qa` / `chrome-devtools-axi`. `opencode-bf reticle disable` removes only that server key. Absent is not a `doctor` failure; a malformed entry fails closed. +- `ui-skills` — `opencode-bf ui-skills enable` registers UI Skills (`https://www.ui-skills.com/mcp`) as an optional remote MCP server. `FOREIGN_ON_DEMAND` for design-skill lookup only. Product UI remains Design Bank + Impeccable + Design V2 atoms + shadcn; `BANK_MISS` never generates from a random ui-skills document. `opencode-bf ui-skills disable` removes only that server key. Absent is not a `doctor` failure; a malformed entry fails closed. +- `exa` — `FOREIGN_ON_DEMAND`; installer never adds, removes, or overwrites it -NVIDIA SkillEvaluator is `FOREIGN_ON_DEMAND` in the same sense: a maintainer may run it externally for embedding-based overlap scoring or live catalog evaluation. The installer never adds it, `doctor` does not fail when it is absent, and a malformed MCP entry for it fails closed like any other schema violation. +NVIDIA SkillEvaluator is `FOREIGN_ON_DEMAND` in the same sense: a maintainer may run it externally for embedding-based overlap scoring or live catalog evaluation. Caliper is `FOREIGN_ON_DEMAND` similarly: a maintainer may `pipx install caliper-eval` off-tree for prompt/agent benchmark evaluation. Neither is vendored into `lib/`, the installer never adds them, `doctor` does not fail when they are absent, and a malformed MCP entry fails closed like any other schema violation. The installer merges only owned MCP keys. Provider, model, permissions, plugins, and foreign MCP stay yours. diff --git a/docs/architecture.md b/docs/architecture.md index a6e8f90..95245f0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,8 +10,8 @@ ┌───────────────────┼────────────────────┐ ▼ ▼ ▼ Skills MCP Rules - 46 automatic Codebase Memory Verification - 16 manual Context7 Engineering + 47 automatic Codebase Memory Verification + 16 manual Context7 Engineering shadcn │ ▼ diff --git a/docs/mcp.md b/docs/mcp.md index 19d241d..ffc6875 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -14,6 +14,8 @@ Optional: - `serena` — `opencode-bf serena enable` if the binary is on PATH - `stitch` — `opencode-bf stitch enable` (remote comp/mock source only; auth via `{env:STITCH_API_KEY}` or `--oauth`) +- `reticle` — `opencode-bf reticle enable` (local stdio via `npx -y @reticlehq/server mcp`; perception only, never auto-implementer) +- `ui-skills` — `opencode-bf ui-skills enable` (remote HTTP `https://www.ui-skills.com/mcp`; design-skill lookup only) - `exa` — foreign; never add/remove/overwrite Merge is parse-aware. Comment-free JSON is rewritten with `json.dumps`. JSONC with comments is patched surgically (owned MCP keys only). If surgical merge cannot be verified, install fails closed instead of destroying comments. @@ -23,3 +25,7 @@ Doctor reports `CONFIGURED` for owned MCP entries present in config. That is not `opencode-bf serena enable` adds Serena only if absent. JSONC comments, provider keys, and foreign MCP are preserved via the same surgical merge as core MCP. Invalid config fails closed. `opencode-bf stitch enable` configures Google Stitch as an optional remote comp/mock server (`https://stitch.googleapis.com/mcp`). It is not an owned core server and not a UI implementer. Keys are never written directly to config, only referenced via `{env:STITCH_API_KEY}` or omitted when using `--oauth`. `opencode-bf stitch disable` surgically removes only the stitch server key. + +`opencode-bf reticle enable` configures Reticle as an optional local perception MCP server (`npx -y @reticlehq/server mcp`). It is `FOREIGN_ON_DEMAND`. The server package is FSL-1.1-ALv2 (competing-use clause); SDK packages (Apache-2.0) are not vendored. Reticle is never an auto-implementer; after a feature is done, default verification remains `playwright-qa` or `chrome-devtools-axi`. Reticle is extra perception if the user enabled it. `opencode-bf reticle disable` surgically removes only the reticle server key. Absent is not a doctor failure; a malformed entry fails closed. + +`opencode-bf ui-skills enable` configures UI Skills as an optional remote MCP server (`https://www.ui-skills.com/mcp`). It is `FOREIGN_ON_DEMAND` for design-skill lookup only (`list_skills`, `get_skill`). Product UI remains Design Bank + Impeccable + Design V2 atoms + shadcn; `BANK_MISS` never generates from a random ui-skills document. `opencode-bf ui-skills disable` surgically removes only the ui-skills server key. Absent is not a doctor failure; a malformed entry fails closed. diff --git a/docs/routing.md b/docs/routing.md index 98e9fb4..3fc95bb 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -22,7 +22,7 @@ MANUAL_NOT_INVOKED Never list unused tools as used. -UI direction from the bank routes to `found-this-design` first, which stops before component implementation. Visual UI and UI atoms (buttons, inputs, cards, nav) route to `impeccable` after Design V2 shortlist; BANK_MISS ≠ generate (+ shadcn/Design V2 internal). Stitch MCP is for screen/comp generation only, then found-this-design or impeccable with Design V2 atom shortlisting; never implement production UI from Stitch alone. Motion UI routes to `emil-design-eng`. Still/ads/non-UI surface route to `visual-studio`. Scroll-led stories route to `scroll-craft`, while continuous camera 3D fly-throughs route to `scroll-world`. +UI direction from the bank routes to `found-this-design` first, which stops before component implementation. Visual UI and UI atoms (buttons, inputs, cards, nav) route to `impeccable` after Design V2 shortlist; BANK_MISS ≠ generate (+ shadcn/Design V2 internal). Stitch MCP is for screen/comp generation only, then found-this-design or impeccable with Design V2 atom shortlisting; never implement production UI from Stitch alone. UI Skills MCP is design-skill lookup only; product UI remains Design Bank + Impeccable + Design V2 atoms + shadcn; BANK_MISS ≠ generate from a random ui-skills document. Motion UI routes to `emil-design-eng`. Still/ads/non-UI surface route to `visual-studio`. Scroll-led stories route to `scroll-craft`, while continuous camera 3D fly-throughs route to `scroll-world`. Procedural Three.js object models from reference images route to `img2threejs`. Browser verification follows four explicit doors: exploratory application UI routes to `playwright-qa`, persistent multi-account sessions route to `browser-act`, observed Chromium cause routes to `chrome-devtools-axi`, and button handler sequential undo / shared-store side effects route to `click-path-audit`. diff --git a/docs/skills.md b/docs/skills.md index bc376f0..5d0b7e3 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -2,10 +2,10 @@ Policy: `vendor/skill-policy.json` plus `vendor/skill-allowlist.txt`. -- 46 model-invoked skills live under `skills/` and install to `~/.config/opencode/skills/` (core + Wave 2/3 warehouse specialists) +- 47 model-invoked skills live under `skills/` and install to `~/.config/opencode/skills/` (core + Wave 2/3 warehouse specialists) - 16 manual skills live under `manual-skills/` and install to `~/.config/opencode/bestfriend/skills/` plus `commands/` -`smartdoc` is per-job document intelligence. `smartbook-ingest` compiles reusable local knowledge. `humanizer` cleans user-facing prose tells (`/unslop` is its manual alias). `academic` manages scholarly research, writing, and peer review. `hyperframes` handles deterministic HTML-to-MP4 video composition. `diagram-design` crafts editorial HTML/SVG diagrams. Warehouse diagnostics include `agent-architecture-audit` (agent stack layers), `cost-aware-llm-pipeline` (token budgeting), `eval-harness` (benchmarks), `prompt-optimizer` (prompt refinement), and `skill-stocktake` (catalog hygiene). Wave 3 adds `api-design`, `contract-first`, `automation-audit-ops`, `code-tour`, and `click-path-audit`. Handwriting is a SmartDoc renderer, not a skill. +`smartdoc` is per-job document intelligence. `smartbook-ingest` compiles reusable local knowledge. `humanizer` cleans user-facing prose tells (`/unslop` is its manual alias). `academic` manages scholarly research, writing, and peer review. `hyperframes` handles deterministic HTML-to-MP4 video composition. `diagram-design` crafts editorial HTML/SVG diagrams. `img2threejs` reconstructs procedural Three.js models from reference images. Warehouse diagnostics include `agent-architecture-audit` (agent stack layers), `cost-aware-llm-pipeline` (token budgeting), `eval-harness` (benchmarks), `prompt-optimizer` (prompt refinement), and `skill-stocktake` (catalog hygiene). Wave 3 adds `api-design`, `contract-first`, `automation-audit-ops`, `code-tour`, and `click-path-audit`. Handwriting is a SmartDoc renderer, not a skill. OpenCode 1.18.x has no `disable-model-invocation` field. Manual skills must not be copied into the discovered skills directory. diff --git a/docs/warehouse-inventory.md b/docs/warehouse-inventory.md index 33da526..5d73239 100644 --- a/docs/warehouse-inventory.md +++ b/docs/warehouse-inventory.md @@ -350,3 +350,20 @@ This inventory establishes the contract for warehouse skills across the five ana | `affaan-m/ECC` | `windows-desktop-e2e` | **REJECT** | `-` | Language/framework pattern sprawl already covered by repo context + Context7 docs | `-` | | `affaan-m/ECC` | `workspace-surface-audit` | **DEFER** | `-` | Specialized domain procedure deferred for future warehouse wave evaluation | `-` | | `affaan-m/ECC` | `x-api` | **REJECT** | `-` | Third-party vendor operations and niche business workflows; reject proprietary automation | `-` | + +--- + +## Wave AI LABS 8 Evaluation + +Evaluation and disposition contract for the AI LABS 8-repo wave (procedural 3D, perception, hooks, UI registries, harness runtimes, mobile platforms, eval benchmarks, anti-slop): + +| Candidate / Repo | Decision | BestFriend Target | Reason | +| :--- | :---: | :--- | :--- | +| `img2threejs` | **NEW** | `skills/img2threejs` | Procedural Three.js TypeScript Group reconstruction from reference object image; quality-gated, no downloaded mesh blobs. | +| `reticle` | **FOREIGN_ON_DEMAND** | `mcp.reticle` | Optional visual perception MCP (`npx -y @reticlehq/server mcp`). Server licensed under FSL-1.1-ALv2; not vendored. Perception only, never auto-implementer. | +| `chisel` (hooks) | **REJECT** | `-` | Session/prompt/tool hooks coupled to Claude Code runtime. Context Guard remains NOT_PORTED. | +| `ui-skills` | **FOREIGN_ON_DEMAND** | `mcp.ui-skills` | Optional remote MCP (`https://www.ui-skills.com/mcp`) for design-skill lookup only. Product UI remains Design Bank + Impeccable + Design V2 + shadcn. | +| `ouroboros` / Q00 | **REJECT** | `-` | Autonomous evolution harness / continuous-learning runtime rejected. Interview primitives already live in `grill-with-docs` / `ask-matt` / `to-spec`. | +| `swiftui-skills` | **DEFER** | `-` | Apple platform / Xcode 26 ecosystem deferred. Target platform gate remains Linux x86_64 and OpenCode 1.18.x. | +| `caliper` | **FOREIGN_ON_DEMAND** | `-` | Benchmark CLI runner (`caliper-eval`). Maintainer may run off-tree via pipx; zero `lib/` vendor coupling. | +| `anti-slop` | **MERGE** | `install-anti-slop`, `impeccable`, `rules/03-prose-discipline.md` | Anti-pattern guardrails merged into existing taste, prose, and linting references; zero extra catalog skills. | diff --git a/lib/cli.py b/lib/cli.py index 919fd53..1d4e3b9 100755 --- a/lib/cli.py +++ b/lib/cli.py @@ -24,9 +24,13 @@ cmd_install, cmd_restore, cmd_restore_list, + cmd_reticle_disable, + cmd_reticle_enable, cmd_serena_enable, cmd_stitch_disable, cmd_stitch_enable, + cmd_ui_skills_disable, + cmd_ui_skills_enable, cmd_uninstall, ) from lib.integrity import cmd_verify # noqa: E402 @@ -104,6 +108,12 @@ def build_parser() -> argparse.ArgumentParser: st.add_argument("action", choices=["enable", "disable"]) st.add_argument("--oauth", action="store_true", help="use OAuth/Bearer auth instead of STITCH_API_KEY header") + ret = sub.add_parser("reticle", help="optional Reticle local perception MCP") + ret.add_argument("action", choices=["enable", "disable"]) + + uis = sub.add_parser("ui-skills", help="optional UI Skills remote MCP") + uis.add_argument("action", choices=["enable", "disable"]) + sd = sub.add_parser("smartdoc", help="document profiles, extract, status") add_smartdoc_cli(sd) sb = sub.add_parser("smartbook", help="reusable SmartBook lifecycle") @@ -163,6 +173,14 @@ def main(argv: list[str] | None = None) -> int: if args.action == "enable": return cmd_stitch_enable(oauth=args.oauth) return cmd_stitch_disable() + if cmd == "reticle": + if args.action == "enable": + return cmd_reticle_enable() + return cmd_reticle_disable() + if cmd == "ui-skills": + if args.action == "enable": + return cmd_ui_skills_enable() + return cmd_ui_skills_disable() if cmd == "smartdoc": return dispatch_smartdoc(args) if cmd == "smartbook": diff --git a/lib/doctor.py b/lib/doctor.py index 5a93d53..b735353 100644 --- a/lib/doctor.py +++ b/lib/doctor.py @@ -118,13 +118,14 @@ def mcp_status_map() -> dict[str, str]: try: data = jsonc.load_path(cfg) except (OSError, json.JSONDecodeError, ValueError): - return {k: "FAIL" for k in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "exa")} + return {k: "FAIL" for k in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "reticle", "ui-skills", "exa")} mcp = data.get("mcp") or {} owned = {"codebase-memory-mcp", "context7", "shadcn"} - for name in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "exa"): + optional = {"serena", "stitch", "reticle", "ui-skills", "exa"} + for name in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "reticle", "ui-skills", "exa"): spec = mcp.get(name) if spec is None: - out[name] = "OPTIONAL_ABSENT" if name in {"serena", "stitch", "exa"} else "FAIL" + out[name] = "OPTIONAL_ABSENT" if name in optional else "FAIL" continue if not isinstance(spec, dict): out[name] = "FAIL" @@ -144,6 +145,26 @@ def mcp_status_map() -> dict[str, str]: continue out[name] = "CONFIGURED" continue + if name == "reticle": + typ = spec.get("type") + cmd = spec.get("command") + if typ != "local" or not isinstance(cmd, list) or not cmd: + out[name] = "FAIL" + continue + out[name] = "CONFIGURED" + continue + if name == "ui-skills": + typ = spec.get("type") + url = spec.get("url") + if typ != "remote" or url != "https://www.ui-skills.com/mcp": + out[name] = "FAIL" + continue + headers = spec.get("headers") + if headers is not None and not isinstance(headers, dict): + out[name] = "FAIL" + continue + out[name] = "CONFIGURED" + continue if name not in owned: out[name] = "FOREIGN" continue diff --git a/lib/install.py b/lib/install.py index 821f578..3fac7e8 100644 --- a/lib/install.py +++ b/lib/install.py @@ -872,7 +872,7 @@ def take(src: Path, dest: Path) -> None: "modelInvokedSkills": meta["model"], "manualSkills": meta["manual"], "ownedMcp": list(OWNED_MCP), - "optionalMcp": ["serena", "stitch", "exa"], + "optionalMcp": ["serena", "stitch", "reticle", "ui-skills", "exa"], "designBank": { "root": bank_root, "source": bank_source, @@ -1293,29 +1293,15 @@ def cmd_serena_enable() -> int: return 0 -def cmd_stitch_enable(oauth: bool = False) -> int: - if not oauth and not os.environ.get("STITCH_API_KEY", "").strip(): - die("STITCH_API_KEY environment variable is empty (set STITCH_API_KEY or use --oauth)") - spec: dict[str, object] = { - "type": "remote", - "url": "https://stitch.googleapis.com/mcp", - "enabled": True, - } - if not oauth: - # API-key mode: suppress OpenCode's automatic OAuth-on-401 so a bad key - # surfaces as an auth error instead of starting a browser OAuth flow. - spec["oauth"] = False - spec["headers"] = { - "X-Goog-Api-Key": "{env:STITCH_API_KEY}", - } +def _optional_mcp_enable(name: str, spec: dict[str, object], already_present_msg: str | None = None) -> int: path = target_config_path() path.parent.mkdir(parents=True, exist_ok=True) if not path.is_file(): path.write_text( - jsonc.dumps({"$schema": "https://opencode.ai/config.json", "mcp": {"stitch": spec}}), + jsonc.dumps({"$schema": "https://opencode.ai/config.json", "mcp": {name: spec}}), encoding="utf-8", ) - info(f"enabled stitch MCP in {path}") + info(f"enabled {name} MCP in {path}") return 0 raw = path.read_text(encoding="utf-8") try: @@ -1327,28 +1313,28 @@ def cmd_stitch_enable(oauth: bool = False) -> int: mcp = data.get("mcp") or {} if not isinstance(mcp, dict): die("OPENCODE_CONFIG_INVALID mcp") - if "stitch" in mcp: - info("stitch MCP already present; not overwriting (run `stitch disable` first to change auth mode)") + if name in mcp: + info(already_present_msg or f"{name} MCP already present; not overwriting") return 0 if jsonc.contains_comments(raw): try: - merged = jsonc.upsert_mcp_servers(raw, {"stitch": spec}) + merged = jsonc.upsert_mcp_servers(raw, {name: spec}) jsonc.loads(merged) path.write_text(merged if merged.endswith("\n") else merged + "\n", encoding="utf-8") except Exception as exc: die(f"OPENCODE_CONFIG_JSONC_SURGICAL_FAILED: {exc}") else: - mcp["stitch"] = spec + mcp[name] = spec data["mcp"] = mcp path.write_text(jsonc.dumps(data), encoding="utf-8") - info(f"enabled stitch MCP in {path}") + info(f"enabled {name} MCP in {path}") return 0 -def cmd_stitch_disable() -> int: +def _optional_mcp_disable(name: str) -> int: path = target_config_path() if not path.is_file(): - info("stitch MCP not present; nothing to disable") + info(f"{name} MCP not present; nothing to disable") return 0 raw = path.read_text(encoding="utf-8") try: @@ -1360,19 +1346,71 @@ def cmd_stitch_disable() -> int: mcp = data.get("mcp") or {} if not isinstance(mcp, dict): die("OPENCODE_CONFIG_INVALID mcp") - if "stitch" not in mcp: - info("stitch MCP not present; nothing to disable") + if name not in mcp: + info(f"{name} MCP not present; nothing to disable") return 0 if jsonc.contains_comments(raw): try: - merged = jsonc.remove_mcp_servers(raw, ["stitch"]) + merged = jsonc.remove_mcp_servers(raw, [name]) jsonc.loads(merged) path.write_text(merged if merged.endswith("\n") else merged + "\n", encoding="utf-8") except Exception as exc: die(f"OPENCODE_CONFIG_JSONC_SURGICAL_FAILED: {exc}") else: - del data["mcp"]["stitch"] + del data["mcp"][name] path.write_text(jsonc.dumps(data), encoding="utf-8") - info(f"disabled stitch MCP in {path}") + info(f"disabled {name} MCP in {path}") return 0 + +def cmd_stitch_enable(oauth: bool = False) -> int: + if not oauth and not os.environ.get("STITCH_API_KEY", "").strip(): + die("STITCH_API_KEY environment variable is empty (set STITCH_API_KEY or use --oauth)") + spec: dict[str, object] = { + "type": "remote", + "url": "https://stitch.googleapis.com/mcp", + "enabled": True, + } + if not oauth: + # API-key mode: suppress OpenCode's automatic OAuth-on-401 so a bad key + # surfaces as an auth error instead of starting a browser OAuth flow. + spec["oauth"] = False + spec["headers"] = { + "X-Goog-Api-Key": "{env:STITCH_API_KEY}", + } + return _optional_mcp_enable( + "stitch", + spec, + already_present_msg="stitch MCP already present; not overwriting (run `stitch disable` first to change auth mode)", + ) + + +def cmd_stitch_disable() -> int: + return _optional_mcp_disable("stitch") + + +def cmd_reticle_enable() -> int: + spec: dict[str, object] = { + "type": "local", + "command": ["npx", "-y", "@reticlehq/server", "mcp"], + "enabled": True, + } + return _optional_mcp_enable("reticle", spec) + + +def cmd_reticle_disable() -> int: + return _optional_mcp_disable("reticle") + + +def cmd_ui_skills_enable() -> int: + spec: dict[str, object] = { + "type": "remote", + "url": "https://www.ui-skills.com/mcp", + "enabled": True, + } + return _optional_mcp_enable("ui-skills", spec) + + +def cmd_ui_skills_disable() -> int: + return _optional_mcp_disable("ui-skills") + diff --git a/rules/00-routing.md b/rules/00-routing.md index 31b7d7b..7a79510 100644 --- a/rules/00-routing.md +++ b/rules/00-routing.md @@ -60,10 +60,12 @@ Do not infer a model provider from a logical model name. Treat custom-gateway al - Visual UI once a world is chosen, the brief is already visual, or creating UI atoms (buttons, inputs, cards, nav): UI atoms → impeccable after Design V2 shortlist; BANK_MISS ≠ generate. Design V2 shortlist `kind=component` is an internal stage, never a separate specialist route. Do not run `/found-this-design` for atomic components. - Design Intelligence is an internal, lazy retrieval stage of Impeccable `new-work`, never a primary route or specialist. Design V2 is the same: an offline user bank, never a specialist. - Stitch MCP: screen/comp generation only; then found-this-design or impeccable + Design V2 atoms. Never implement production UI from Stitch alone. Treat existing Stitch screens as approved comps; local atom shortlist remains mandatory. +- UI Skills MCP: design-skill lookup only. Product UI remains Design Bank + Impeccable + Design V2 atoms + shadcn. BANK_MISS ≠ generate from a random ui-skills document. - Installable UI components: MCP `shadcn` only. Do not add Magic UI, Kibo, 21st.dev, or community UI MCP servers. - Use the hub only when cwd has `components.json`. Never silent `shadcn init` on this adapter, a backend or Python tree, or a non-UI cwd. - Scroll-led storytelling (scroll is the timeline, scrollytelling, signature interaction): `/scroll-craft`. Ordinary scrollable UI stays `/impeccable`. `/scroll-craft` plus Continuous World: Scroll Craft writes the brief, then `/scroll-world`. - Continuous camera fly-through, diorama, or 3D-world landing: `/scroll-world` even if the request says scroll. +- Procedural Three.js object from image: `/img2threejs` (factory Group, editable primitives, visible vs inferred spec). Not `scroll-world` (camera fly-through), not `hyperframes` (HTML-to-MP4), not `visual-studio` (photoreal stills/media), and not `impeccable` (product UI). - Deterministic HTML composition rendered to video: `/hyperframes` (headless Chrome + FFmpeg). Not `visual-studio`, not `emil-design-eng`. Ordinary scrollable UI stays `/impeccable`. - Photoreal stills / ads / identity with no UI surface: `/visual-studio`. - Motion after Impeccable: `/emil-design-eng`. @@ -104,7 +106,7 @@ Do not infer a model provider from a logical model name. Treat custom-gateway al ## Plugins and extra MCP - No extra marketplace plugins. Foundation = skills + MCP + thin AGENTS.md + runtime helpers. -- User MCP: `codebase-memory-mcp`, `context7`, and `shadcn` on; `serena` and `stitch` absent until a human enables them; `exa` foreign. +- User MCP: `codebase-memory-mcp`, `context7`, and `shadcn` on; `serena`, `stitch`, `reticle`, and `ui-skills` absent until a human enables them; `exa` foreign. - ECC / other harness overlays: `FOREIGN_ON_DEMAND`. Never add, remove, or merge foreign harness control planes or continuous-learning runtimes. Individual warehouse procedures ported in Wave 2 (agent-architecture-audit, cost-aware-llm-pipeline, eval-harness, prompt-optimizer, skill-stocktake) and Wave 3 (api-design, contract-first, automation-audit-ops, code-tour, click-path-audit) are first-party MIT skills. If external ECC is already present in user environment, do not merge and do not shadow. - FOREIGN vendor packs (e.g. `mongodb/agent-skills`, `supabase/agent-skills`, `vercel-labs/agent-skills`) stay off the overlay; user may `npx skills add mongodb/agent-skills|supabase/agent-skills` locally; never `frontend-design` for product UI. - Never auto-edit rules or skills from a learning log (no `/learn`, `/evolve`, or session-end skill writers). diff --git a/rules/03-prose-discipline.md b/rules/03-prose-discipline.md index 7b6ae03..c38ec45 100644 --- a/rules/03-prose-discipline.md +++ b/rules/03-prose-discipline.md @@ -15,6 +15,8 @@ This is not a skill. It does not auto-apply. It does not rewrite code. - Preserve legitimate technical terms (`interface`, `vector`, `primitive`, `surface`, `modality`) when they are the real word. - Preserve facts: names, dates, numbers, URLs, citations, code, commands, flags, API ids, quotes, negations, security and legal wording. - Cut words that do no work. +- Cut kickoff slop and AI promotional clichés (“Let’s dive in”, “Supercharge your workflow”, “In this fast-paced world”, “Unleash the power of”). +- Cut fake social proof: never author synthetic testimonials, fabricated reviews, or unverified star ratings. - Never “polish” source code, tests, or command output as if they were marketing copy. For full prose rewrites and systematic AI-tell removal, use the `humanizer` specialist (manual slash alias: `/unslop`). diff --git a/skills/img2threejs/NOTICE.md b/skills/img2threejs/NOTICE.md new file mode 100644 index 0000000..095d97d --- /dev/null +++ b/skills/img2threejs/NOTICE.md @@ -0,0 +1,10 @@ +# Notice: img2threejs + +Method inspiration adapted from [img2threejs/img2threejs](https://github.com/img2threejs/img2threejs). + +Upstream licensed under the Apache License, Version 2.0 (the "License"). +You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + +Synthesized and rewritten into OpenCodeBestFriend as an original first-party MIT procedural Three.js TypeScript generator. Zero verbatim upstream text or runtime copied. diff --git a/skills/img2threejs/SKILL.md b/skills/img2threejs/SKILL.md new file mode 100644 index 0000000..61d9feb --- /dev/null +++ b/skills/img2threejs/SKILL.md @@ -0,0 +1,50 @@ +--- +name: img2threejs +description: Use when the user wants a code-only procedural Three.js model reconstructed from a reference object image (factory Group, editable primitives, lighting, animation). Not for scroll-led pages (scroll-craft), camera/diorama worlds (scroll-world), HTML-to-MP4 (hyperframes), photoreal/image generation (visual-studio), or product UI (impeccable / found-this-design). +compatibility: opencode +license: MIT +--- + +# img2threejs + +Procedural 3D reconstruction specialist generating clean, editable TypeScript Three.js `Group` factories from a single reference object image. + +Unlike downloaded GLTF/OBJ mesh blobs or heavy photogrammetry pipelines, img2threejs creates human-readable, parametric code using Three.js primitives (`BoxGeometry`, `CylinderGeometry`, `ExtrudeGeometry`, shaders/materials, lights). + +## Boundaries & Handoffs + +| Need | Primary Route | +|---|---| +| Reconstruct isolated object as procedural Three.js code | **`img2threejs`** | +| Continuous 3D fly-through, diorama, camera world landing | `scroll-world` | +| Scroll-driven timeline storytelling website | `scroll-craft` | +| Deterministic HTML composition rendered to MP4 video | `hyperframes` | +| Photoreal product stills, ads, VFX, or raster asset packs | `visual-studio` | +| Web landing page, dashboard, or application UI | `impeccable` / `found-this-design` | + +## Progressive Pipeline + +1. **Validate Image**: Inspect the reference image. The target must be a discrete object or prop (hardware, furniture, device, vehicle, stylized mascot), not an entire interactive landing page or panoramic scene. +2. **Written Spec (Visible vs. Inferred)**: + - Identify visible geometry, proportions, materials, colors, and lighting. + - Explicitly document **inferred hidden faces** (rear, underside, interior). A single viewpoint cannot reveal occluded sides. + - Propose the hierarchical scene breakdown (`Group` root with named child components). +3. **Factory Pass**: + - Author a standalone TypeScript module exporting a factory function `createModel(options?): THREE.Group`. + - Use procedural primitives and standard materials (`MeshStandardMaterial`, `MeshPhysicalMaterial`). + - Group logically for articulation or animation if requested (pivot points, rotators). + - No downloaded mesh packs, external `.gltf`/`.obj` URLs, or opaque binary blobs. Output must be diffable TypeScript. +4. **Screenshot Compare & Loop Gate**: + - Render the procedural model and visually inspect side-by-side with the reference image. + - Step disposition must be one of: + - `continue`: Visual parity acceptable; finalize and document inputs. + - `refine-spec`: Proportions or structural hierarchy wrong; adjust written specification. + - `refine-code`: Materials, alignments, or primitive parameters off; edit TypeScript. + - `request-input`: Occluded or ambiguous details require user decision. + - `stop`: Reached best procedural approximation, or model complexity exceeds procedural primitives. + +## Hard Rules & Honesty Gates + +1. **Honesty on Occlusion**: Never pretend a single image provides full 360-degree truth. Clearly mark unseen faces as inferred. If the user demands photogrammetric precision that procedural primitives cannot achieve, state "cannot reach requested fidelity" and stop. +2. **Token & Resource Discipline**: Procedural 3D code generation and screenshot verification are token-intensive. Never auto-run on generic website briefs or landing pages. +3. **No Heavy Toolchain**: Output is clean, dependency-light Three.js code. Do not introduce Python mesh reconstruction packages, neural rendering runtimes, or external API dependencies. diff --git a/skills/impeccable/SKILL.md b/skills/impeccable/SKILL.md index b003898..aa8b472 100644 --- a/skills/impeccable/SKILL.md +++ b/skills/impeccable/SKILL.md @@ -22,7 +22,8 @@ Core principles: - If empty: report `BANK_MISS`; fall back to shadcn MCP only if cwd contains `components.json`. Never invent arbitrary hex, radius, or typography. - If hits exist: pick or take the top role-exact card, record `id`, `provider`, `role`, and `local_path` into `.impeccable/atoms.json` in user project cwd. - Implementation must mimic the structure and tokens of that atom + the visual world pack if pinned by `found-this-design`. - - If the user provides an existing Stitch screen / mock, treat it as an approved comp (comp-first build path); do not generate code directly from Stitch without shortlisting local Design V2 atoms for components. + - If the user provides an existing Stitch screen / mock, treat it as an approved comp (comp-first build path); do not generate code directly from Stitch without shortlisting local Design V2 atoms for components. + - If UI Skills MCP is present, use it for design-skill lookup only; do not generate code directly from ui-skills documents without Design V2 atom shortlisting. - Forbidden: `image_gen`, vendor `frontend-design`, or ungrounded model taste. - World or page-level layout still routes to `found-this-design` first when no world is pinned. diff --git a/skills/impeccable/reference/taste-guard.md b/skills/impeccable/reference/taste-guard.md index 9a336cb..b366f0c 100644 --- a/skills/impeccable/reference/taste-guard.md +++ b/skills/impeccable/reference/taste-guard.md @@ -38,9 +38,11 @@ When guidelines diverge, resolve in strict order: - Single palette lock: choose neutral base + one primary accent role; maintain accent consistency throughout the page. - Corner radius consistency: uniform scale across cards, inputs, and buttons unless a distinct token hierarchy is documented. - Inherit existing design system tokens and component libraries where present. +- **Anti-Slop Visual Fences**: Reject unexamined category templates: no default dark purple-gradient hero, glowing cyan/purple mesh backgrounds, or ungrounded Inter-on-white-card layouts unless explicitly requested. ### 5. Product Truth & Factual Content - **Zero Hallucinated Precision**: Never invent precise statistics (`94.2%`, `4.8x`), fake customer logos, fake review quotes, or fake partner endorsements. +- **Anti-Slop Copy & Proof Fences**: Never generate artificial testimonials, fake review carousels, or empty AI kickoff clichés ("Let's dive in", "Supercharge your workflow"). - If demonstration data is needed in greenfield work, label it clearly as synthetic demonstration data. - Refinement preserves existing copy and claims; redesign replaces presentation while preserving factual truths. diff --git a/skills/install-anti-slop/NOTICE.md b/skills/install-anti-slop/NOTICE.md index bd3ac8a..45e41e2 100644 --- a/skills/install-anti-slop/NOTICE.md +++ b/skills/install-anti-slop/NOTICE.md @@ -18,3 +18,6 @@ This skill vendors and adapts the Anti-Slop Oxlint plugin originally authored by - `effect`: opt-in Effect service layer rules for direct Effect dependencies. - Added safe removal, update, idempotency checks, and collision detection. - Strictly segregated from core OCBF Python dependencies (no Oxlint forced onto OCBF itself). + +## Additional Attribution +Prose and UI anti-slop guidelines in `rules/03-prose-discipline.md` and `skills/impeccable/reference/taste-guard.md` incorporate concepts and patterns adapted from [miqdadbadjuber/anti-slop](https://github.com/miqdadbadjuber/anti-slop) (MIT License, Copyright (c) 2024 Miqdad Badjuber). Zero extra catalog skills or external runtime dependencies added. diff --git a/skills/install-anti-slop/SKILL.md b/skills/install-anti-slop/SKILL.md index 7cd6c5d..048a141 100644 --- a/skills/install-anti-slop/SKILL.md +++ b/skills/install-anti-slop/SKILL.md @@ -13,7 +13,7 @@ Vendored from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (MIT, ## Core Boundaries 1. **Opt-In Only**: Load this skill ONLY when the user explicitly requests Anti-Slop (e.g. "pasang anti-slop", "audit anti-slop", "hapus anti-slop"). Never auto-load during ordinary coding or non-TS/JS tasks. -2. **Distinct from `/unslop`**: `/unslop` is for user-facing prose cleanup. `install-anti-slop` is for static Oxlint linting of TypeScript/JavaScript code. +2. **Distinct from `/unslop` and UI Craft**: `/unslop` and `rules/03-prose-discipline.md` handle prose cleanup. UI template anti-patterns (e.g. default purple gradient mesh, Inter-on-white-card slop, fake testimonials) live in `skills/impeccable/reference/taste-guard.md`. `install-anti-slop` is strictly for static Oxlint linting of TypeScript/JavaScript code. 3. **No OCBF Core Coupling**: Never add Oxlint or Anti-Slop to OCBF's core Python codebase or dependencies. 4. **Exact Version Coupling**: Keep `oxlint` and `@oxlint/plugins` on the exact same version. 5. **No Blind Global Rewrites**: Linter findings identify patterns; resolve root causes with inference, `satisfies`, and boundary validation rather than casts or fake comments. diff --git a/templates/AGENTS.md b/templates/AGENTS.md index 982bf36..d3c7cb4 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -33,7 +33,7 @@ repo/file → Codebase Memory MCP first (skip if no project for cwd) → Serena ## Specialists (load one) -UI direction → skill `found-this-design` then `impeccable`. UI atoms (button, input, card, nav) after world/brief → impeccable after Design V2 shortlist; BANK_MISS ≠ generate (never `found-this-design` for buttons). Motion UI (easing, hover, seam) → `emil-design-eng`. Still/ads/non-UI surface → `visual-studio`. Scroll-led story → `scroll-craft`. Camera/3D world/diorama → `scroll-world`. Registry → shadcn MCP. Design Intelligence and Design V2 are internal to Impeccable `new-work`, never a route. Stitch MCP = screen/comp generation only; then found-this-design or impeccable + Design V2 atoms. Never implement production UI from Stitch alone. +UI direction → skill `found-this-design` then `impeccable`. UI atoms (button, input, card, nav) after world/brief → impeccable after Design V2 shortlist; BANK_MISS ≠ generate (never `found-this-design` for buttons). Motion UI (easing, hover, seam) → `emil-design-eng`. Still/ads/non-UI surface → `visual-studio`. Scroll-led story → `scroll-craft`. Camera/3D world/diorama → `scroll-world`. Object image to procedural Three.js → `img2threejs`. Registry → shadcn MCP. Design Intelligence and Design V2 are internal to Impeccable `new-work`, never a route. Stitch MCP = screen/comp generation only; then found-this-design or impeccable + Design V2 atoms. Never implement production UI from Stitch alone. UI Skills MCP = design-skill lookup only; product UI remains Design Bank + Impeccable + Design V2 atoms + shadcn; BANK_MISS ≠ generate from a random ui-skills document. Browser QA → skill `playwright-qa`. Explicit/session BrowserAct → `browser-act`. Observed cause → `chrome-devtools-axi` after `opencode-chromium-cdp` (`127.0.0.1:9223`). Never Google Chrome. Project E2E suites (Playwright Test/Cypress) stay authoritative for regressions. diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 0d78ee1..133570c 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -299,6 +299,86 @@ def test_doctor_stitch_valid_configured_passes(self): self.assertEqual(rc, 0, buf.getvalue()) self.assertIn("CONFIGURED mcp:stitch", buf.getvalue()) + def test_doctor_reticle_missing_does_not_fail(self): + self._install() + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 0, buf.getvalue()) + self.assertIn("OPTIONAL_ABSENT mcp:reticle", buf.getvalue()) + + def test_doctor_reticle_invalid_schema_fails(self): + self._install() + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + data = jsonc.loads(cfg.read_text(encoding="utf-8")) + data["mcp"]["reticle"] = { + "type": "remote", + "url": "https://example.invalid", + "enabled": True, + } + cfg.write_text(jsonc.dumps(data), encoding="utf-8") + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 1, buf.getvalue()) + self.assertIn("FAIL mcp:reticle", buf.getvalue()) + + def test_doctor_reticle_valid_configured_passes(self): + self._install() + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + data = jsonc.loads(cfg.read_text(encoding="utf-8")) + data["mcp"]["reticle"] = { + "type": "local", + "command": ["npx", "-y", "@reticlehq/server", "mcp"], + "enabled": True, + } + cfg.write_text(jsonc.dumps(data), encoding="utf-8") + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 0, buf.getvalue()) + self.assertIn("CONFIGURED mcp:reticle", buf.getvalue()) + + def test_doctor_ui_skills_missing_does_not_fail(self): + self._install() + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 0, buf.getvalue()) + self.assertIn("OPTIONAL_ABSENT mcp:ui-skills", buf.getvalue()) + + def test_doctor_ui_skills_invalid_schema_fails(self): + self._install() + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + data = jsonc.loads(cfg.read_text(encoding="utf-8")) + data["mcp"]["ui-skills"] = { + "type": "local", + "command": ["ui-skills-bin"], + "enabled": True, + } + cfg.write_text(jsonc.dumps(data), encoding="utf-8") + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 1, buf.getvalue()) + self.assertIn("FAIL mcp:ui-skills", buf.getvalue()) + + def test_doctor_ui_skills_valid_configured_passes(self): + self._install() + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + data = jsonc.loads(cfg.read_text(encoding="utf-8")) + data["mcp"]["ui-skills"] = { + "type": "remote", + "url": "https://www.ui-skills.com/mcp", + "enabled": True, + } + cfg.write_text(jsonc.dumps(data), encoding="utf-8") + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 0, buf.getvalue()) + self.assertIn("CONFIGURED mcp:ui-skills", buf.getvalue()) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_evaluation_matrix.py b/tests/test_evaluation_matrix.py index 329fa92..9296084 100644 --- a/tests/test_evaluation_matrix.py +++ b/tests/test_evaluation_matrix.py @@ -88,6 +88,24 @@ def test_case_12_browser_act_on_user_named_session(self): self.assertIn("Explicit multi-account or persistent browser sessions: `/browser-act`", self.routing) self.assertIn("Use only when the user explicitly requests BrowserAct, specifies a browser-act CLI command, or requires pre-configured persistent/multi-account sessions", self.browser_act) + def test_case_13_image_to_threejs_procedural_model(self): + # Scenario 13: Procedural Three.js object from image -> img2threejs + self.assertIn("Object image to procedural Three.js → `img2threejs`", self.agents) + self.assertIn("Procedural Three.js object from image: `/img2threejs`", self.routing) + img2threejs = (ROOT / "skills" / "img2threejs" / "SKILL.md").read_text(encoding="utf-8") + self.assertIn("Reconstruct isolated object as procedural Three.js code", img2threejs) + + def test_case_14_scroll_factory_world_not_img2threejs(self): + # Scenario 14: Scroll factory world -> scroll-world NOT img2threejs + self.assertIn("Continuous camera fly-through, diorama, or 3D-world landing: `/scroll-world`", self.routing) + img2threejs = (ROOT / "skills" / "img2threejs" / "SKILL.md").read_text(encoding="utf-8") + self.assertIn("camera/diorama worlds (scroll-world)", img2threejs) + + def test_case_15_dashboard_ui_impeccable_not_img2threejs(self): + # Scenario 15: Dashboard UI -> impeccable NOT img2threejs + img2threejs = (ROOT / "skills" / "img2threejs" / "SKILL.md").read_text(encoding="utf-8") + self.assertIn("product UI (impeccable / found-this-design)", img2threejs) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_install.py b/tests/test_install.py index d85d2f5..3b1af75 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -19,9 +19,13 @@ backup_relevant, cmd_install, cmd_restore, + cmd_reticle_disable, + cmd_reticle_enable, cmd_serena_enable, cmd_stitch_disable, cmd_stitch_enable, + cmd_ui_skills_disable, + cmd_ui_skills_enable, cmd_uninstall, ) from lib.doctor import cmd_doctor, cmd_skills_verify, isolation_check # noqa: E402 @@ -115,7 +119,7 @@ def test_fresh_install_idempotent_uninstall(self): self.assertEqual(sha256(self.sentinel), self.sentinel_hash) self.assertEqual({p.name for p in (self.tmp / ".claude").iterdir()}, {"sentinel.txt"}) skills = list((self.tmp / ".config" / "opencode" / "skills").iterdir()) - self.assertEqual(len([p for p in skills if p.is_dir()]), 46) + self.assertEqual(len([p for p in skills if p.is_dir()]), 47) cmds = list((self.tmp / ".config" / "opencode" / "commands").glob("*.md")) self.assertEqual(len(cmds), 16) self.assertEqual(cmd_skills_verify(), 0) @@ -130,7 +134,7 @@ def test_fresh_install_idempotent_uninstall(self): rc2 = cmd_install() self.assertEqual(rc2, 0) - self.assertEqual(len([p for p in (self.tmp / ".config" / "opencode" / "skills").iterdir() if p.is_dir()]), 46) + self.assertEqual(len([p for p in (self.tmp / ".config" / "opencode" / "skills").iterdir() if p.is_dir()]), 47) self.assertEqual(len(list((self.tmp / ".config" / "opencode" / "commands").glob("*.md"))), 16) bashrc = (self.tmp / ".bashrc").read_text(encoding="utf-8") self.assertEqual(bashrc.count("OPENCODEBESTFRIEND:BEGIN"), 1) @@ -439,6 +443,104 @@ def test_stitch_disable_invalid_config_fail_closed(self): cmd_stitch_disable() self.assertEqual(cfg.read_text(encoding="utf-8"), "{ not valid json") + def test_reticle_enable_and_disable(self): + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + cfg.parent.mkdir(parents=True, exist_ok=True) + cfg.write_text( + """{ + // keep this comment + "model": "keep-me-model", + "mcp": { + "foreign-weather": { + "type": "remote", + "url": "https://example.invalid/mcp", + "enabled": true + } + } +} +""", + encoding="utf-8", + ) + self.assertEqual(cmd_reticle_enable(), 0) + text = cfg.read_text(encoding="utf-8") + self.assertIn("// keep this comment", text) + data = jsonc.loads(text) + self.assertIn("reticle", data["mcp"]) + ret_spec = data["mcp"]["reticle"] + self.assertEqual(ret_spec["type"], "local") + self.assertEqual(ret_spec["command"], ["npx", "-y", "@reticlehq/server", "mcp"]) + self.assertTrue(ret_spec["enabled"]) + + # Idempotent enable + self.assertEqual(cmd_reticle_enable(), 0) + + # Disable removes only reticle + self.assertEqual(cmd_reticle_disable(), 0) + text2 = cfg.read_text(encoding="utf-8") + self.assertIn("// keep this comment", text2) + data2 = jsonc.loads(text2) + self.assertIn("foreign-weather", data2["mcp"]) + self.assertNotIn("reticle", data2["mcp"]) + + def test_ui_skills_enable_and_disable(self): + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + cfg.parent.mkdir(parents=True, exist_ok=True) + cfg.write_text( + """{ + // keep this comment + "model": "keep-me-model", + "mcp": { + "foreign-weather": { + "type": "remote", + "url": "https://example.invalid/mcp", + "enabled": true + } + } +} +""", + encoding="utf-8", + ) + self.assertEqual(cmd_ui_skills_enable(), 0) + text = cfg.read_text(encoding="utf-8") + self.assertIn("// keep this comment", text) + data = jsonc.loads(text) + self.assertIn("ui-skills", data["mcp"]) + ui_spec = data["mcp"]["ui-skills"] + self.assertEqual(ui_spec["type"], "remote") + self.assertEqual(ui_spec["url"], "https://www.ui-skills.com/mcp") + self.assertTrue(ui_spec["enabled"]) + + # Idempotent enable + self.assertEqual(cmd_ui_skills_enable(), 0) + + # Disable removes only ui-skills + self.assertEqual(cmd_ui_skills_disable(), 0) + text2 = cfg.read_text(encoding="utf-8") + self.assertIn("// keep this comment", text2) + data2 = jsonc.loads(text2) + self.assertIn("foreign-weather", data2["mcp"]) + self.assertNotIn("ui-skills", data2["mcp"]) + + def test_reticle_invalid_config_fail_closed(self): + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + cfg.parent.mkdir(parents=True, exist_ok=True) + cfg.write_text("{ not valid json", encoding="utf-8") + with self.assertRaises(SystemExit): + cmd_reticle_enable() + with self.assertRaises(SystemExit): + cmd_reticle_disable() + self.assertEqual(cfg.read_text(encoding="utf-8"), "{ not valid json") + + def test_ui_skills_invalid_config_fail_closed(self): + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + cfg.parent.mkdir(parents=True, exist_ok=True) + cfg.write_text("{ not valid json", encoding="utf-8") + with self.assertRaises(SystemExit): + cmd_ui_skills_enable() + with self.assertRaises(SystemExit): + cmd_ui_skills_disable() + self.assertEqual(cfg.read_text(encoding="utf-8"), "{ not valid json") + def test_restore_prior_product_tree(self): product = self.tmp / ".local" / "share" / "opencode-bestfriend" / "product" product.mkdir(parents=True) diff --git a/tests/test_migration.py b/tests/test_migration.py index b8c4323..eca812e 100644 --- a/tests/test_migration.py +++ b/tests/test_migration.py @@ -106,7 +106,7 @@ def test_migrate_claudebestfriend_1_4_2_to_ocbf(self): (self.tmp / ".local" / "share" / "opencode-bestfriend" / "components" / "installer").exists() ) skills = list((self.tmp / ".config" / "opencode" / "skills").iterdir()) - self.assertEqual(len([p for p in skills if p.is_dir()]), 46) + self.assertEqual(len([p for p in skills if p.is_dir()]), 47) with redirect_stdout(io.StringIO()): self.assertEqual(cmd_verify(), 0) self.assertEqual(cmd_doctor(), 0) diff --git a/tests/test_routing.py b/tests/test_routing.py index 6a68359..2a67f45 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -56,6 +56,7 @@ def test_mappings(self): "supabase-ops": "supabase-ops", "mongodb-ops": "mongodb-ops", "vercel-ops": "vercel-ops", + "img2threejs": "img2threejs", } for label, needle in expected.items(): self.assertIn(needle, blob, label) @@ -193,6 +194,21 @@ def test_scroll_routes_have_explicit_boundaries(self): self.assertIn("alone are not enough", skill) self.assertIn("Do not implement worldflight here.", skill) + def test_img2threejs_routing_boundary(self): + self.assertIn("Object image to procedural Three.js → `img2threejs`", self.agents) + self.assertIn("Procedural Three.js object from image: `/img2threejs`", self.routing) + self.assertIn("Procedural Three.js object models from reference images route to `img2threejs`", (ROOT / "docs" / "routing.md").read_text(encoding="utf-8")) + skill = (ROOT / "skills" / "img2threejs" / "SKILL.md").read_text(encoding="utf-8") + self.assertIn("Not for scroll-led pages (scroll-craft)", skill) + self.assertIn("camera/diorama worlds (scroll-world)", skill) + self.assertIn("product UI (impeccable / found-this-design)", skill) + + def test_ui_skills_routing_boundary(self): + needle = "UI Skills MCP = design-skill lookup only; product UI remains Design Bank + Impeccable + Design V2 atoms + shadcn; BANK_MISS ≠ generate from a random ui-skills document." + self.assertIn(needle, self.agents) + self.assertIn("UI Skills MCP: design-skill lookup only", self.routing) + self.assertIn("BANK_MISS ≠ generate from a random ui-skills document", (ROOT / "docs" / "routing.md").read_text(encoding="utf-8")) + def test_no_context_guard_rule(self): self.assertFalse((ROOT / "rules" / "04-context-guard.md").exists()) diff --git a/tests/test_skills.py b/tests/test_skills.py index e4e50db..fde7c92 100644 --- a/tests/test_skills.py +++ b/tests/test_skills.py @@ -56,11 +56,11 @@ def jaccard(left: set[str], right: set[str]) -> float: class SkillPolicyTests(unittest.TestCase): def test_counts(self): allow, skills, model, manual = load_policy(ROOT) - self.assertEqual(len(allow), 62) - self.assertEqual(len(model), 46) + self.assertEqual(len(allow), 63) + self.assertEqual(len(model), 47) self.assertEqual(len(manual), 16) self.assertEqual(set(allow), set(skills)) - for name in ("supabase-ops", "mongodb-ops", "vercel-ops"): + for name in ("supabase-ops", "mongodb-ops", "vercel-ops", "img2threejs"): self.assertIn(name, allow) self.assertIn(name, model) diff --git a/vendor/license-audit.json b/vendor/license-audit.json index 69479b1..43ba32d 100644 --- a/vendor/license-audit.json +++ b/vendor/license-audit.json @@ -313,6 +313,11 @@ "license": "MIT", "evidence": "SKILL.md frontmatter (first-party MIT adapter)", "redistribution": "mit" + }, + "img2threejs": { + "license": "MIT", + "evidence": "SKILL.md frontmatter (first-party MIT rewrite; NOTICE.md)", + "redistribution": "mit" } } } diff --git a/vendor/mcp-policy.json b/vendor/mcp-policy.json index 7619cd8..2936575 100644 --- a/vendor/mcp-policy.json +++ b/vendor/mcp-policy.json @@ -38,6 +38,17 @@ "enabled": false, "transport": "http", "url": "https://stitch.googleapis.com/mcp" + }, + "reticle": { + "enabled": false, + "transport": "stdio", + "command": "npx", + "args": ["-y", "@reticlehq/server", "mcp"] + }, + "ui-skills": { + "enabled": false, + "transport": "http", + "url": "https://www.ui-skills.com/mcp" } } } diff --git a/vendor/mcp-wanted.json b/vendor/mcp-wanted.json index 7d79bea..3b85be0 100644 --- a/vendor/mcp-wanted.json +++ b/vendor/mcp-wanted.json @@ -46,6 +46,23 @@ "transport": "http", "url": "https://stitch.googleapis.com/mcp", "status": "ON_DEMAND_ABSENT" + }, + "reticle": { + "wanted": false, + "ownedIfAdded": false, + "scope": "user", + "transport": "stdio", + "command": "npx", + "args": ["-y", "@reticlehq/server", "mcp"], + "status": "FOREIGN_ON_DEMAND" + }, + "ui-skills": { + "wanted": false, + "ownedIfAdded": false, + "scope": "user", + "transport": "http", + "url": "https://www.ui-skills.com/mcp", + "status": "FOREIGN_ON_DEMAND" } } } diff --git a/vendor/skill-allowlist.txt b/vendor/skill-allowlist.txt index de1d04a..6999d92 100644 --- a/vendor/skill-allowlist.txt +++ b/vendor/skill-allowlist.txt @@ -30,6 +30,7 @@ grill-with-docs grilling humanizer hyperframes +img2threejs impeccable improve-codebase-architecture install-anti-slop diff --git a/vendor/skill-policy.json b/vendor/skill-policy.json index 24fd67a..8c21506 100644 --- a/vendor/skill-policy.json +++ b/vendor/skill-policy.json @@ -98,6 +98,9 @@ "hyperframes": { "invocation": "model" }, + "img2threejs": { + "invocation": "model" + }, "impeccable": { "invocation": "model" }, diff --git a/vendor/sources.json b/vendor/sources.json index 293d55b..14714ad 100644 --- a/vendor/sources.json +++ b/vendor/sources.json @@ -51,6 +51,20 @@ "status": "optional-on-demand", "note": "Remote MCP. Screen and mock comp generation only. No secrets stored in git; configured via STITCH_API_KEY environment variable or OAuth." }, + "reticle": { + "package": "@reticlehq/server", + "transport": "stdio", + "via": "npx", + "license": "FSL-1.1-ALv2", + "status": "foreign-on-demand", + "note": "Optional visual perception MCP. Server licensed under FSL-1.1-ALv2; not vendored. Enabled via opencode-bf reticle enable." + }, + "ui-skills": { + "url": "https://www.ui-skills.com/mcp", + "transport": "http", + "status": "foreign-on-demand", + "note": "Optional remote MCP for design skill lookup only. Enabled via opencode-bf ui-skills enable." + }, "browser-act": { "repository": "https://github.com/browser-act/skills", "version": "1.3.0",