diff --git a/CHANGELOG.md b/CHANGELOG.md index 11c3cdb..f202ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ ## Unreleased -Catalog update for AI LABS 8-repo wave adaptation and hygiene patch. Totals become **63** (47 model-invoked, 16 manual slash commands). +Catalog update for AI LABS 8-repo wave adaptation, hygiene patch, and MarkItDown ingest. Totals become **64** (48 model-invoked, 16 manual slash commands). +- Add model-invoked specialist `markitdown` as a Markdown ingest converter (Office/PDF/HTML/CSV/XLSX/PPTX/EPUB/ZIP). Optional MCP `opencode-bf markitdown enable` registers local stdio (`uvx --from markitdown-mcp markitdown-mcp`). `FOREIGN_ON_DEMAND`. SmartDoc keeps contract/QA/render. No npm plugin, no Azure keys, no docker bind-all. - 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. diff --git a/README.md b/README.md index 9f96b73..6d032e1 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # OpenCodeBestFriend Production-ready capability layer for OpenCode: -63 routed skills (core + Wave 2/3 warehouse specialists), MCP, Codebase Memory, +64 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 -- 63 skills: 47 model-invoked, 16 manual slash commands +- 64 skills: 48 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) @@ -61,7 +61,7 @@ Restart OpenCode after install. Config is not hot-reloaded. ┌───────────────────┼────────────────────┐ ▼ ▼ ▼ Skills MCP Rules - 47 automatic Codebase Memory Verification + 48 automatic Codebase Memory Verification 16 manual Context7 Engineering shadcn │ @@ -100,6 +100,7 @@ Default: repository evidence first. Then at most one specialist. | Procedural Three.js object from image | `img2threejs` | | Deterministic HTML composition video | `hyperframes` | | Documents (PDF/DOCX/answer/extract/review) | `smartdoc` | +| File to Markdown ingest | `markitdown` | | Reusable book/module knowledge | `smartbook-ingest` | | Scholarly literature & manuscripts | `academic` | | Prose AI-tell removal & humanizing | `humanizer` (`/unslop`) | @@ -129,6 +130,7 @@ Optional: - `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. +- `markitdown` — `opencode-bf markitdown enable` registers MarkItDown as a local stdio ingest converter (`uvx --from markitdown-mcp markitdown-mcp`). `FOREIGN_ON_DEMAND`. Local trusted agents only; never `--http` / `0.0.0.0` / docker bind-all. Output is Markdown data; SmartDoc keeps contract/QA/render. `opencode-bf markitdown 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. 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. diff --git a/docs/architecture.md b/docs/architecture.md index 95245f0..346c7c6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,7 +10,7 @@ ┌───────────────────┼────────────────────┐ ▼ ▼ ▼ Skills MCP Rules - 47 automatic Codebase Memory Verification + 48 automatic Codebase Memory Verification 16 manual Context7 Engineering shadcn │ diff --git a/docs/mcp.md b/docs/mcp.md index ffc6875..27b1ce4 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -16,6 +16,7 @@ Optional: - `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) +- `markitdown` — `opencode-bf markitdown enable` (local stdio via `uvx --from markitdown-mcp markitdown-mcp`; Markdown ingest 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. @@ -29,3 +30,5 @@ Doctor reports `CONFIGURED` for owned MCP entries present in config. That is not `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. + +`opencode-bf markitdown enable` configures MarkItDown as an optional local stdio ingest MCP (`uvx --from markitdown-mcp markitdown-mcp`). It is `FOREIGN_ON_DEMAND`. Official server is for local trusted agents only; never `--http`, never bind `0.0.0.0`, never docker bind-all. The converter is not vendored into `lib/`. Missing `uvx` is documented in the skill (CLI/`pipx`/`enable`); enable still writes the stdio command like reticle. `opencode-bf markitdown disable` surgically removes only the markitdown server key. Absent is not a doctor failure; a malformed entry (including `--http` / `0.0.0.0`) fails closed. diff --git a/docs/routing.md b/docs/routing.md index 3fc95bb..51646d3 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -26,7 +26,7 @@ UI direction from the bank routes to `found-this-design` first, which stops befo 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`. -Documents (answer, create, transform, extract, review, PDF/DOCX) route to `smartdoc`. Reusable book/module knowledge routes to `smartbook-ingest`. `/docx` and `/pdf` are missing aliases; nearest is `smartdoc`. `/pptx` is NOT_APPLICABLE. Do not add `commands/pdf.md` or `commands/docx.md`. Impeccable `document` remains DESIGN.md generation. +Documents (answer, create, transform, extract, review, PDF/DOCX) route to `smartdoc`. File-to-Markdown ingest routes to `markitdown`. Reusable book/module knowledge routes to `smartbook-ingest`. `/docx` and `/pdf` are missing aliases; nearest is `smartdoc`. `/pptx` is NOT_APPLICABLE. Do not add `commands/pdf.md` or `commands/docx.md`. Impeccable `document` remains DESIGN.md generation. Prose AI-tell removal and natural tone polishing route to `humanizer` (`/unslop` is its manual alias). Scholarly research, academic manuscripts, and structured peer critique route to `academic`. Deterministic HTML composition rendered to video routes to `hyperframes`. Editorial technical diagrams (HTML/SVG) route to `diagram-design`. diff --git a/docs/skills.md b/docs/skills.md index 5d0b7e3..00bd23a 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -2,10 +2,10 @@ Policy: `vendor/skill-policy.json` plus `vendor/skill-allowlist.txt`. -- 47 model-invoked skills live under `skills/` and install to `~/.config/opencode/skills/` (core + Wave 2/3 warehouse specialists) +- 48 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. `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. +`smartdoc` is per-job document intelligence. `markitdown` converts Office/PDF/HTML/CSV/XLSX/PPTX/EPUB/ZIP to Markdown for ingest; SmartDoc keeps contract/QA/render. `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 5d73239..a655e53 100644 --- a/docs/warehouse-inventory.md +++ b/docs/warehouse-inventory.md @@ -367,3 +367,19 @@ Evaluation and disposition contract for the AI LABS 8-repo wave (procedural 3D, | `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. | + +--- + +## Wave MarkItDown + +Microsoft MarkItDown as an ingest converter, not a second document OS. SmartDoc keeps contract/QA/render. + +| Candidate / Repo | Decision | BestFriend Target | Reason | +| :--- | :---: | :--- | :--- | +| `microsoft/markitdown` CLI/lib | **NEW** | `skills/markitdown` | Thin first-party skill: convert Office/PDF/HTML/CSV/XLSX/PPTX/EPUB/ZIP to Markdown, then hand off. | +| `markitdown-mcp` official | **FOREIGN_ON_DEMAND** | `mcp.markitdown` | Optional local stdio (`uvx --from markitdown-mcp markitdown-mcp`). Local trusted agents only. | +| `opencode-markitdown` npm plugin | **REJECT** | `-` | Config-hook mutation forbidden. | +| community `trsdn-markitdown-mcp` | **REJECT** | `-` | Not Microsoft. | +| Azure Document Intelligence / Content Understanding | **DEFER** | `-` | No keys in config. | +| youtube / audio extras | **DEFER** | `-` | Out of document lane. | +| Duplicate SmartDoc modes | **REJECT** | `-` | SmartDoc keeps contract/QA/render. | diff --git a/lib/cli.py b/lib/cli.py index 1d4e3b9..99c4c64 100755 --- a/lib/cli.py +++ b/lib/cli.py @@ -24,6 +24,8 @@ cmd_install, cmd_restore, cmd_restore_list, + cmd_markitdown_disable, + cmd_markitdown_enable, cmd_reticle_disable, cmd_reticle_enable, cmd_serena_enable, @@ -111,6 +113,9 @@ def build_parser() -> argparse.ArgumentParser: ret = sub.add_parser("reticle", help="optional Reticle local perception MCP") ret.add_argument("action", choices=["enable", "disable"]) + md = sub.add_parser("markitdown", help="optional MarkItDown local ingest MCP") + md.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"]) @@ -177,6 +182,10 @@ def main(argv: list[str] | None = None) -> int: if args.action == "enable": return cmd_reticle_enable() return cmd_reticle_disable() + if cmd == "markitdown": + if args.action == "enable": + return cmd_markitdown_enable() + return cmd_markitdown_disable() if cmd == "ui-skills": if args.action == "enable": return cmd_ui_skills_enable() diff --git a/lib/doctor.py b/lib/doctor.py index b735353..0ae87ac 100644 --- a/lib/doctor.py +++ b/lib/doctor.py @@ -118,11 +118,11 @@ 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", "reticle", "ui-skills", "exa")} + return {k: "FAIL" for k in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "reticle", "ui-skills", "markitdown", "exa")} mcp = data.get("mcp") or {} owned = {"codebase-memory-mcp", "context7", "shadcn"} - optional = {"serena", "stitch", "reticle", "ui-skills", "exa"} - for name in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "reticle", "ui-skills", "exa"): + optional = {"serena", "stitch", "reticle", "ui-skills", "markitdown", "exa"} + for name in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "reticle", "ui-skills", "markitdown", "exa"): spec = mcp.get(name) if spec is None: out[name] = "OPTIONAL_ABSENT" if name in optional else "FAIL" @@ -165,6 +165,21 @@ def mcp_status_map() -> dict[str, str]: continue out[name] = "CONFIGURED" continue + if name == "markitdown": + typ = spec.get("type") + cmd = spec.get("command") + if typ != "local" or not isinstance(cmd, list) or not cmd: + out[name] = "FAIL" + continue + joined = " ".join(str(part) for part in cmd) + if "--http" in joined or "0.0.0.0" in joined: + out[name] = "FAIL" + continue + if cmd[0] != "uvx" or "markitdown-mcp" not in cmd: + 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 3fac7e8..e339ab3 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", "reticle", "ui-skills", "exa"], + "optionalMcp": ["serena", "stitch", "reticle", "ui-skills", "markitdown", "exa"], "designBank": { "root": bank_root, "source": bank_source, @@ -1402,6 +1402,19 @@ def cmd_reticle_disable() -> int: return _optional_mcp_disable("reticle") +def cmd_markitdown_enable() -> int: + spec: dict[str, object] = { + "type": "local", + "command": ["uvx", "--from", "markitdown-mcp", "markitdown-mcp"], + "enabled": True, + } + return _optional_mcp_enable("markitdown", spec) + + +def cmd_markitdown_disable() -> int: + return _optional_mcp_disable("markitdown") + + def cmd_ui_skills_enable() -> int: spec: dict[str, object] = { "type": "remote", diff --git a/rules/00-routing.md b/rules/00-routing.md index 7a79510..5bad3c7 100644 --- a/rules/00-routing.md +++ b/rules/00-routing.md @@ -37,7 +37,7 @@ Do not infer a model provider from a logical model name. Treat custom-gateway al - Throwaway evidence for one design question: `/prototype`. Not for production UI; skip ordinary implementation, ADHD, and `/arena`. - Unknown / hard bugs, regressions, measured slowdown: `/diagnosing-bugs`. Skip typos, known-cause, and test-first known fixes (`/tdd`). - Authoring SKILL.md / AGENTS.md / skill descriptions / context pointers: `/writing-for-agents` (distinct from `/skill-stocktake` which audits catalog hygiene). Workflow choice stays `/ask-matt`. -- Documents (answer, create, transform, extract, review, PDF/DOCX): `/smartdoc`. Reusable book/module knowledge: `/smartbook-ingest`. SmartDoc may read an existing SmartBook; that is not a second implementation specialist. Impeccable `document` stays DESIGN.md. +- Documents (answer, create, transform, extract, review, PDF/DOCX): `/smartdoc`. File to Markdown ingest: `/markitdown`. Reusable book/module knowledge: `/smartbook-ingest`. SmartDoc may read an existing SmartBook; that is not a second implementation specialist. Impeccable `document` stays DESIGN.md. - Prose AI-tell removal and natural tone polishing: `/humanizer`. Manual `/unslop` is an alias to the same specialist body. Do not auto-apply on ordinary code or diffs. Technical documentation structure stays `/technical-writing`. Code linting stays `/install-anti-slop`. Prompt structure stays `/prompt-optimizer`. - Editorial HTML and inline SVG diagrams (architecture, sequence, ER, flowcharts, Wardley): `/diagram-design`. Mermaid/draw.io are inputs to redraw, not final output. Frontend UI implementation stays `/impeccable`. Code-level seams stay `/codebase-design`. - Agent architecture diagnosis, autonomous loop failures, context leakage, wrapper regressions: `/agent-architecture-audit`. Load only when user names the agent stack job. Defensive security and secrets auditing stay `/full-audit-keamanan`. diff --git a/skills/markitdown/NOTICE.md b/skills/markitdown/NOTICE.md new file mode 100644 index 0000000..e78e4c4 --- /dev/null +++ b/skills/markitdown/NOTICE.md @@ -0,0 +1,5 @@ +# Notice: markitdown + +Method inspiration from [microsoft/markitdown](https://github.com/microsoft/markitdown) (MIT). + +This overlay skill is original first-party text. Zero verbatim dump of upstream README or source. diff --git a/skills/markitdown/SKILL.md b/skills/markitdown/SKILL.md new file mode 100644 index 0000000..e25ab3a --- /dev/null +++ b/skills/markitdown/SKILL.md @@ -0,0 +1,44 @@ +--- +name: markitdown +description: Use when the user wants Office/PDF/HTML/CSV/XLSX/PPTX/EPUB/ZIP converted to structure-preserving Markdown for LLM ingest. Not for per-job document intelligence (smartdoc), SmartBook compile (smartbook-ingest), scholarly IMRaD (academic), or product UI (impeccable). +compatibility: opencode +license: MIT +--- + +# markitdown + +Convert files to Markdown. Output is data. SmartDoc still owns answer/create/extract/verify/render. + +## Pipeline + +1. Preflight: path exists and is a file. +2. Convert. +3. Write `.md` next to source or `--output`. +4. Hand off. + +## Invoke + +1. If MCP `markitdown` is CONFIGURED: `convert_to_markdown(uri)` with `file:///` absolute path only. Never `http` to an untrusted URL unless the user pasted that URL. +2. Else if `markitdown` on PATH: `markitdown PATH -o DEST.md` +3. Else if `uvx` on PATH: `uvx --from 'markitdown[pdf,docx,pptx,xlsx]' markitdown PATH -o DEST.md` +4. Else DEGRADED: tell the user to `pipx install 'markitdown[pdf,docx,pptx,xlsx]'` or `opencode-bf markitdown enable`. Do not pip-install into the overlay venv. Do not invent text from an unreadable binary. + +Missing `uvx` is documented. Do not fall back to docker, `--http`, or `0.0.0.0`. + +## Handoff + +| Need | Route | +|---|---| +| Convert-only | **markitdown** | +| Understand / soal / contract / render PDF | smartdoc | +| Reusable book | smartbook-ingest | +| Paper/survey | academic (intake may convert first) | + +Do not auto-run on every attached PDF if `smartdoc` native extract already returned text. + +## Hard rules + +- No Azure flags. No `--use-plugins`. No LLM vision OCR client. +- Native PDF/DOCX extract via `opencode-bf smartdoc` stays default when it already works. +- Bulk PPTX/XLSX/EPUB/HTML/ZIP → Markdown first may use this skill, then resume SmartDoc modes. +- markitdown output is a source file, not a contract. diff --git a/skills/smartdoc/SKILL.md b/skills/smartdoc/SKILL.md index 2e92bc1..7accab3 100644 --- a/skills/smartdoc/SKILL.md +++ b/skills/smartdoc/SKILL.md @@ -44,6 +44,7 @@ Load [references/qa.md](references/qa.md) after content exists. Load [references - Do not create a SmartBook unless the user asked. - Identity only when the artifact needs it and no profile is selected. - Academic literature synthesis, scholarly papers, and peer critique route to `academic`; SmartDoc handles file intake, OCR, and output rendering. +- Bulk PPTX/XLSX/EPUB/HTML/ZIP → Markdown first may use `markitdown`, then resume SmartDoc modes. Native PDF/DOCX extract via `opencode-bf smartdoc` stays default when it already works. markitdown output is a source file, not a contract. - Ask only HIGH/CRITICAL questions whose answers change the artifact. HIGH confidence → proceed. - Never call a local score Turnitin. Never promise 0%. Never run a detector-evasion loop. - Handwriting is a renderer, not a skill. diff --git a/templates/AGENTS.md b/templates/AGENTS.md index d3c7cb4..ef45fb5 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -37,7 +37,7 @@ UI direction → skill `found-this-design` then `impeccable`. UI atoms (button, 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. -Auth/secret/payment/upload/webhook/privileged/public API → `full-audit-keamanan`. Measured LCP/INP/CLS/latency/bundle → `full-performance-audit`. GitHub → `gh-axi`. Hard unknown bug → `diagnosing-bugs`. Documents (PDF/DOCX/extract/review) → `smartdoc`. Reusable local knowledge → `smartbook-ingest`. +Auth/secret/payment/upload/webhook/privileged/public API → `full-audit-keamanan`. Measured LCP/INP/CLS/latency/bundle → `full-performance-audit`. GitHub → `gh-axi`. Hard unknown bug → `diagnosing-bugs`. Documents (PDF/DOCX/extract/review) → `smartdoc`. File → Markdown ingest → `markitdown`. Reusable local knowledge → `smartbook-ingest`. Prose AI-tells / humanize → skill `humanizer`. Slash `/unslop` is the same specialist, manual only. Technical writing structure → suggest `/technical-writing`. Academic literature / manuscript / peer-critique → skill `academic` (not `research`, not `smartdoc` unless file extract/render). Facts library/API → Context7; `research` only if repo lacking. Deterministic HTML video / render HTML to MP4 → skill `hyperframes` (not `visual-studio`, not `emil-design-eng`). Editorial diagram HTML/SVG → skill `diagram-design` (not `impeccable`). diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 133570c..8e689b5 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -379,6 +379,62 @@ def test_doctor_ui_skills_valid_configured_passes(self): self.assertEqual(rc, 0, buf.getvalue()) self.assertIn("CONFIGURED mcp:ui-skills", buf.getvalue()) + def test_doctor_markitdown_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:markitdown", buf.getvalue()) + + def test_doctor_markitdown_invalid_schema_fails(self): + self._install() + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + data = jsonc.loads(cfg.read_text(encoding="utf-8")) + data["mcp"]["markitdown"] = { + "type": "local", + "command": ["docker", "run", "--rm", "-i", "-p", "0.0.0.0:3001:3001", "markitdown-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, 1, buf.getvalue()) + self.assertIn("FAIL mcp:markitdown", buf.getvalue()) + + def test_doctor_markitdown_http_bind_fails(self): + self._install() + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + data = jsonc.loads(cfg.read_text(encoding="utf-8")) + data["mcp"]["markitdown"] = { + "type": "local", + "command": ["uvx", "--from", "markitdown-mcp", "markitdown-mcp", "--http", "0.0.0.0"], + "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:markitdown", buf.getvalue()) + + def test_doctor_markitdown_valid_configured_passes(self): + self._install() + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + data = jsonc.loads(cfg.read_text(encoding="utf-8")) + data["mcp"]["markitdown"] = { + "type": "local", + "command": ["uvx", "--from", "markitdown-mcp", "markitdown-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:markitdown", buf.getvalue()) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_evaluation_matrix.py b/tests/test_evaluation_matrix.py index 9296084..5171b2b 100644 --- a/tests/test_evaluation_matrix.py +++ b/tests/test_evaluation_matrix.py @@ -101,6 +101,25 @@ def test_case_14_scroll_factory_world_not_img2threejs(self): img2threejs = (ROOT / "skills" / "img2threejs" / "SKILL.md").read_text(encoding="utf-8") self.assertIn("camera/diorama worlds (scroll-world)", img2threejs) + def test_case_16_convert_pptx_to_markdown(self): + self.assertIn("File → Markdown ingest → `markitdown`", self.agents) + self.assertIn("File to Markdown ingest: `/markitdown`", self.routing) + markitdown = (ROOT / "skills" / "markitdown" / "SKILL.md").read_text(encoding="utf-8") + self.assertIn("Convert-only", markitdown) + self.assertIn("**markitdown**", markitdown) + + def test_case_17_answer_soal_in_pdf_smartdoc(self): + self.assertIn("Documents (PDF/DOCX/extract/review) → `smartdoc`", self.agents) + smartdoc = (ROOT / "skills" / "smartdoc" / "SKILL.md").read_text(encoding="utf-8") + self.assertIn("Understand / soal / contract / render PDF", (ROOT / "skills" / "markitdown" / "SKILL.md").read_text(encoding="utf-8")) + self.assertIn("soal", smartdoc) + + def test_case_18_ingest_into_smartbook(self): + self.assertIn("Reusable local knowledge → `smartbook-ingest`", self.agents) + markitdown = (ROOT / "skills" / "markitdown" / "SKILL.md").read_text(encoding="utf-8") + self.assertIn("Reusable book", markitdown) + self.assertIn("smartbook-ingest", markitdown) + 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") diff --git a/tests/test_install.py b/tests/test_install.py index 3b1af75..88bf3c7 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -19,6 +19,8 @@ backup_relevant, cmd_install, cmd_restore, + cmd_markitdown_disable, + cmd_markitdown_enable, cmd_reticle_disable, cmd_reticle_enable, cmd_serena_enable, @@ -119,7 +121,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()]), 47) + self.assertEqual(len([p for p in skills if p.is_dir()]), 48) cmds = list((self.tmp / ".config" / "opencode" / "commands").glob("*.md")) self.assertEqual(len(cmds), 16) self.assertEqual(cmd_skills_verify(), 0) @@ -134,7 +136,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()]), 47) + self.assertEqual(len([p for p in (self.tmp / ".config" / "opencode" / "skills").iterdir() if p.is_dir()]), 48) 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) @@ -541,6 +543,53 @@ def test_ui_skills_invalid_config_fail_closed(self): cmd_ui_skills_disable() self.assertEqual(cfg.read_text(encoding="utf-8"), "{ not valid json") + def test_markitdown_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_markitdown_enable(), 0) + text = cfg.read_text(encoding="utf-8") + self.assertIn("// keep this comment", text) + data = jsonc.loads(text) + self.assertIn("markitdown", data["mcp"]) + md_spec = data["mcp"]["markitdown"] + self.assertEqual(md_spec["type"], "local") + self.assertEqual(md_spec["command"], ["uvx", "--from", "markitdown-mcp", "markitdown-mcp"]) + self.assertTrue(md_spec["enabled"]) + + self.assertEqual(cmd_markitdown_enable(), 0) + + self.assertEqual(cmd_markitdown_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("markitdown", data2["mcp"]) + + def test_markitdown_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_markitdown_enable() + with self.assertRaises(SystemExit): + cmd_markitdown_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 eca812e..ad1da0f 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()]), 47) + self.assertEqual(len([p for p in skills if p.is_dir()]), 48) 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 2a67f45..ec698a6 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -57,6 +57,7 @@ def test_mappings(self): "mongodb-ops": "mongodb-ops", "vercel-ops": "vercel-ops", "img2threejs": "img2threejs", + "markitdown": "markitdown", } for label, needle in expected.items(): self.assertIn(needle, blob, label) @@ -140,6 +141,7 @@ def test_new_specialist_boundaries(self): "scroll-world", "diagram-design", "smartdoc", + "markitdown", "academic", "humanizer", "supabase-ops", @@ -203,6 +205,16 @@ def test_img2threejs_routing_boundary(self): self.assertIn("camera/diorama worlds (scroll-world)", skill) self.assertIn("product UI (impeccable / found-this-design)", skill) + def test_markitdown_routing_boundary(self): + self.assertIn("File → Markdown ingest → `markitdown`", self.agents) + self.assertIn("File to Markdown ingest: `/markitdown`", self.routing) + self.assertIn("File-to-Markdown ingest routes to `markitdown`", (ROOT / "docs" / "routing.md").read_text(encoding="utf-8")) + skill = (ROOT / "skills" / "markitdown" / "SKILL.md").read_text(encoding="utf-8") + self.assertIn("Not for per-job document intelligence (smartdoc)", skill) + self.assertIn("convert_to_markdown", skill) + smartdoc = (ROOT / "skills" / "smartdoc" / "SKILL.md").read_text(encoding="utf-8") + self.assertIn("markitdown output is a source file, not a contract", smartdoc) + 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) diff --git a/tests/test_skills.py b/tests/test_skills.py index fde7c92..98097a8 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), 63) - self.assertEqual(len(model), 47) + self.assertEqual(len(allow), 64) + self.assertEqual(len(model), 48) self.assertEqual(len(manual), 16) self.assertEqual(set(allow), set(skills)) - for name in ("supabase-ops", "mongodb-ops", "vercel-ops", "img2threejs"): + for name in ("supabase-ops", "mongodb-ops", "vercel-ops", "img2threejs", "markitdown"): self.assertIn(name, allow) self.assertIn(name, model) diff --git a/vendor/license-audit.json b/vendor/license-audit.json index 43ba32d..1defa82 100644 --- a/vendor/license-audit.json +++ b/vendor/license-audit.json @@ -99,6 +99,11 @@ "evidence": "mattpocock/skills engineering/implement renamed; MIT LICENSE", "redistribution": "mit" }, + "markitdown": { + "license": "MIT", + "evidence": "SKILL.md frontmatter + skills/markitdown/NOTICE.md (method inspiration from microsoft/markitdown MIT; zero verbatim dump)", + "redistribution": "mit" + }, "blast-radius": { "license": "MIT", "evidence": "cursor/plugins pstack 60c641e + vendor/licenses/PSTACK-MIT.txt", diff --git a/vendor/mcp-policy.json b/vendor/mcp-policy.json index 2936575..f3598ea 100644 --- a/vendor/mcp-policy.json +++ b/vendor/mcp-policy.json @@ -49,6 +49,12 @@ "enabled": false, "transport": "http", "url": "https://www.ui-skills.com/mcp" + }, + "markitdown": { + "enabled": false, + "transport": "stdio", + "command": "uvx", + "args": ["--from", "markitdown-mcp", "markitdown-mcp"] } } } diff --git a/vendor/mcp-wanted.json b/vendor/mcp-wanted.json index 3b85be0..4b19bbd 100644 --- a/vendor/mcp-wanted.json +++ b/vendor/mcp-wanted.json @@ -63,6 +63,15 @@ "transport": "http", "url": "https://www.ui-skills.com/mcp", "status": "FOREIGN_ON_DEMAND" + }, + "markitdown": { + "wanted": false, + "ownedIfAdded": false, + "scope": "user", + "transport": "stdio", + "command": "uvx", + "args": ["--from", "markitdown-mcp", "markitdown-mcp"], + "status": "FOREIGN_ON_DEMAND" } } } diff --git a/vendor/skill-allowlist.txt b/vendor/skill-allowlist.txt index 6999d92..5a17799 100644 --- a/vendor/skill-allowlist.txt +++ b/vendor/skill-allowlist.txt @@ -36,6 +36,7 @@ improve-codebase-architecture install-anti-slop interrogate maintain-verification-skill +markitdown matt-code-review matt-implement mongodb-ops diff --git a/vendor/skill-policy.json b/vendor/skill-policy.json index 8c21506..65d74c0 100644 --- a/vendor/skill-policy.json +++ b/vendor/skill-policy.json @@ -116,6 +116,9 @@ "maintain-verification-skill": { "invocation": "manual" }, + "markitdown": { + "invocation": "model" + }, "matt-code-review": { "invocation": "model" },