Browse Source

Merge pull request #352 from alvinunreal/carto-codemap

rename cartography to codemap
Alvin 3 months ago
parent
commit
59e5213229
48 changed files with 1911 additions and 1343 deletions
  1. 13 18
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 199 0
      .slim/codemap.json
  3. 11 0
      AGENTS.md
  4. 3 3
      README.md
  5. 100 198
      codemap.md
  6. 0 58
      docs/cartography.md
  7. 59 0
      docs/codemap.md
  8. 1 1
      docs/installation.md
  9. 2 2
      docs/quick-reference.md
  10. 6 6
      docs/skills.md
  11. 43 0
      scripts/codemap.md
  12. 1 1
      scripts/verify-release-artifact.ts
  13. 8 8
      src/agents/index.test.ts
  14. 1 1
      src/cli/codemap.md
  15. 3 3
      src/cli/custom-skills.ts
  16. 6 3
      src/cli/providers.test.ts
  17. 4 4
      src/codemap.md
  18. 29 0
      src/council/codemap.md
  19. 25 74
      src/hooks/apply-patch/codemap.md
  20. 26 15
      src/hooks/auto-update-checker/codemap.md
  21. 18 10
      src/hooks/codemap.md
  22. 33 0
      src/hooks/delegate-task-retry/codemap.md
  23. 32 0
      src/hooks/filter-available-skills/codemap.md
  24. 30 0
      src/hooks/foreground-fallback/codemap.md
  25. 30 0
      src/hooks/json-error-recovery/codemap.md
  26. 17 6
      src/hooks/phase-reminder/codemap.md
  27. 17 7
      src/hooks/post-file-tool-nudge/codemap.md
  28. 58 0
      src/hooks/todo-continuation/codemap.md
  29. 24 20
      src/index.ts
  30. 64 0
      src/interview/codemap.md
  31. 0 2
      src/mcp/codemap.md
  32. 34 0
      src/multiplexer/codemap.md
  33. 40 0
      src/multiplexer/tmux/codemap.md
  34. 44 0
      src/multiplexer/zellij/codemap.md
  35. 0 456
      src/skills/cartography/scripts/cartographer.py
  36. 0 87
      src/skills/cartography/scripts/test_cartographer.py
  37. 40 0
      src/skills/codemap.md
  38. 11 9
      src/skills/codemap/README.md
  39. 19 16
      src/skills/codemap/SKILL.md
  40. 36 0
      src/skills/codemap/codemap.md
  41. 483 0
      src/skills/codemap/scripts/codemap.mjs
  42. 129 0
      src/skills/codemap/scripts/codemap.test.ts
  43. 36 0
      src/skills/simplify/codemap.md
  44. 87 255
      src/tools/codemap.md
  45. 42 34
      src/tools/lsp/config.test.ts
  46. 6 6
      src/tools/lsp/config.ts
  47. 31 24
      src/tools/lsp/utils.test.ts
  48. 10 16
      src/tools/lsp/utils.ts

+ 13 - 18
.github/ISSUE_TEMPLATE/bug-report.yml

@@ -9,18 +9,13 @@ body:
       value: |
         Thanks for filing a bug. Keep it short, but include enough detail for us to reproduce it.
   - type: textarea
-    id: happened
+    id: summary
     attributes:
-      label: What happened?
-      description: Describe the bug clearly.
-      placeholder: The plugin crashes when I run...
-    validations:
-      required: true
-  - type: textarea
-    id: expected
-    attributes:
-      label: What did you expect to happen?
-      placeholder: It should...
+      label: What happened, and what did you expect?
+      description: Describe the bug and the expected behavior.
+      placeholder: |
+        What happened: The plugin crashes when I run...
+        Expected: It should...
     validations:
       required: true
   - type: textarea
@@ -33,36 +28,36 @@ body:
         2. Open...
         3. See...
     validations:
-      required: true
+      required: false
   - type: textarea
     id: config
     attributes:
-      label: Relevant config
-      description: Paste the relevant part of your OpenCode or plugin config. Remove secrets first.
+      label: oh-my-opencode.json
+      description: Paste your oh-my-opencode.json config.
       render: jsonc
     validations:
-      required: true
+      required: false
   - type: input
     id: opencode-version
     attributes:
       label: OpenCode version
       placeholder: 0.4.12
     validations:
-      required: true
+      required: false
   - type: input
     id: plugin-version
     attributes:
       label: oh-my-opencode-slim version
       placeholder: 0.0.0
     validations:
-      required: true
+      required: false
   - type: input
     id: os
     attributes:
       label: Operating system
       placeholder: macOS 15.4 / Ubuntu 24.04 / Windows 11
     validations:
-      required: true
+      required: false
   - type: textarea
     id: logs
     attributes:

+ 199 - 0
.slim/codemap.json

@@ -0,0 +1,199 @@
+{
+  "metadata": {
+    "version": "1.0.0",
+    "last_run": "2026-04-20T21:09:45.113Z",
+    "root": "/Users/alvin/repos/oh-my-opencode-slim",
+    "include_patterns": [
+      "src/**/*.ts",
+      "src/**/*.d.ts",
+      "scripts/**/*.ts",
+      "package.json",
+      "tsconfig.json",
+      "biome.json",
+      "README.md",
+      "AGENTS.md"
+    ],
+    "exclude_patterns": [
+      "opencode/**",
+      "docs/**",
+      "dist/**",
+      "node_modules/**",
+      ".git/**",
+      "**/*.test.ts",
+      "**/*.spec.ts",
+      "**/test/**",
+      "**/*.snap"
+    ],
+    "exceptions": []
+  },
+  "file_hashes": {
+    "AGENTS.md": "a23a101768d4acb25542d54ab9243dd8",
+    "README.md": "60cc9be49a1495bc0a34a6463d610436",
+    "biome.json": "f708d9fb1676b9eb7159e317522ef6c9",
+    "package.json": "8345cbd15291d8f4874325ebdb8baab9",
+    "scripts/generate-schema.ts": "007f340e39adf6c3fd76feda72b71df1",
+    "scripts/verify-opencode-host-smoke.ts": "a87fdb08b123501edf81618a49bc421d",
+    "scripts/verify-release-artifact.ts": "83259be1926459412809013ce16e6fbb",
+    "src/agents/council-master.ts": "f013987f32d30178efb3bfa3030b3ce8",
+    "src/agents/council.ts": "afa3ed4c40b2f91bdc907b850a7e68d3",
+    "src/agents/councillor.ts": "e12284c5f632ba071e97924608375f9c",
+    "src/agents/designer.ts": "6b5786ea6de1fb41b367103824762c9d",
+    "src/agents/explorer.ts": "3213b5fd3c335be5fcc5f2ce3cafde95",
+    "src/agents/fixer.ts": "663d386c6597febc62344a554f90ded8",
+    "src/agents/index.ts": "6a3a02833a57119cb59393624b07b365",
+    "src/agents/librarian.ts": "6959db9e4917acaa14b9beefe465d0c9",
+    "src/agents/observer.ts": "e957caf6f289675bbbd47d795d4ff0bd",
+    "src/agents/oracle.ts": "93d69d90e29898dd6c760ada395e5730",
+    "src/agents/orchestrator.ts": "99daeea34b019990b7e3d86f8ad6454a",
+    "src/background/background-manager.ts": "03e5bf857bbb41210a354a6526caa2b3",
+    "src/background/index.ts": "af7f2545c2f37a547c3edfe18f856b95",
+    "src/background/multiplexer-session-manager.ts": "e06903121bfdbb5237a094422f49fbe9",
+    "src/background/subagent-depth.ts": "ea2d2a5949d50fa8b63307ca220a9f4c",
+    "src/cli/config-io.ts": "e9048becbe09e618f07853ea9050b840",
+    "src/cli/config-manager.ts": "7f2960f55aaebab21d822c586c2b12eb",
+    "src/cli/custom-skills.ts": "da74e53dfd5f570e97a99ea4fd1d0440",
+    "src/cli/index.ts": "759900abd74812e03982e7b42c6774ec",
+    "src/cli/install.ts": "3b9db79a1eaf21605ace124f356c6c8f",
+    "src/cli/model-key-normalization.ts": "7f988cc8109c95382b9ece9730e2a7a5",
+    "src/cli/paths.ts": "77054651c36e730aa3b32682fd26fe70",
+    "src/cli/providers.ts": "12ee3947cd27554e90d34760a3952b6a",
+    "src/cli/skills.ts": "4b3a3aec7ff891608c56d242a0f517f6",
+    "src/cli/system.ts": "b5464d7661ab1c8e196159641ee3bbed",
+    "src/cli/types.ts": "6b3468226ad733b8c4a601677a98a11e",
+    "src/config/agent-mcps.ts": "7e94663fd4a9a827615da53b320dd774",
+    "src/config/constants.ts": "77e199c035c2ee76d6cfa1680c245c67",
+    "src/config/council-schema.ts": "a97d9742d1ef8a68ab5d73a2411a3fed",
+    "src/config/index.ts": "713c6bc1ca211a4746ec8cebcfc94b6e",
+    "src/config/loader.ts": "8419310b841733920d03b99d6204387e",
+    "src/config/schema.ts": "f482c6dc189581037912698db88d62ab",
+    "src/config/utils.ts": "bc3af4a86874329f638a374ac7c00701",
+    "src/council/council-manager.ts": "40603ed65f34d7fd29d2e18e1d5fe97a",
+    "src/council/index.ts": "24cab5b06b4bfd91d2496692650eb18a",
+    "src/hooks/apply-patch/codec.ts": "fce9edab08aab27b5c09bdb46c201203",
+    "src/hooks/apply-patch/errors.ts": "fd2c9d9d185494f2f8b22862bd14700b",
+    "src/hooks/apply-patch/execution-context.ts": "b44fb8ae4c672ab7c0b18fb1aa1915a0",
+    "src/hooks/apply-patch/index.ts": "835302f13810b8cb88d92367d3fc034e",
+    "src/hooks/apply-patch/matching.ts": "2ea9569179cf01c0c010f257cd9278d3",
+    "src/hooks/apply-patch/operations.ts": "2ea0bbef64fcb6bd07a8adf35a490aeb",
+    "src/hooks/apply-patch/patch.ts": "65f24cc7d01d80eeda3016469d8e32ed",
+    "src/hooks/apply-patch/prepared-changes.ts": "bf504dea1fc8723f593093a6c8d9a829",
+    "src/hooks/apply-patch/resolution.ts": "45305823564edf4c9cdf972f5b03859a",
+    "src/hooks/apply-patch/rewrite.ts": "2ba1d58233a4093bf0d5ffde0ad8ff75",
+    "src/hooks/apply-patch/test-helpers.ts": "27b74cc1c0dec6c9dfdbbea4a9724468",
+    "src/hooks/apply-patch/types.ts": "bff517a2050313703b3e8c4af35617d0",
+    "src/hooks/auto-update-checker/cache.ts": "306b85a4beef7fd9959ecdfc655f8c3c",
+    "src/hooks/auto-update-checker/checker.ts": "18a6a25b534a3be31d57b2f5a401f235",
+    "src/hooks/auto-update-checker/constants.ts": "c46dcf24c3184965314f008ede59b7c7",
+    "src/hooks/auto-update-checker/index.ts": "3df09b0bec208c0c962c8f2bd0dab177",
+    "src/hooks/auto-update-checker/types.ts": "b53cb3c5c541d65da160d83451433499",
+    "src/hooks/chat-headers.ts": "2586390fd72f4e19da4d06a6e770aa8f",
+    "src/hooks/delegate-task-retry/guidance.ts": "a121a7fc081422351f4d5b2044aa6024",
+    "src/hooks/delegate-task-retry/hook.ts": "709bd483063a2090fff5b1048861b5c1",
+    "src/hooks/delegate-task-retry/index.ts": "7b78edb6f10cfee10b2c117ca2287378",
+    "src/hooks/delegate-task-retry/patterns.ts": "5e4919da29af630e4e2ec37df0b58025",
+    "src/hooks/filter-available-skills/index.ts": "4cac7bea2a22f57d602de1df22203baa",
+    "src/hooks/foreground-fallback/index.ts": "154b2a954447c70e2bccc68b58262d62",
+    "src/hooks/image-hook.ts": "94e07e33eac132eeb339d4b51f7dfca8",
+    "src/hooks/index.ts": "569c94462498575319218d2b9ac62895",
+    "src/hooks/json-error-recovery/hook.ts": "55f1268777de23ed5546c8f2f0a5b424",
+    "src/hooks/json-error-recovery/index.ts": "c54900170ea905776e973e30b5dd95f4",
+    "src/hooks/phase-reminder/index.ts": "55c78ab86f3b26a071c8e2f639831b8b",
+    "src/hooks/post-file-tool-nudge/index.ts": "7a23d01b3396c4018015e0e90629c45d",
+    "src/hooks/todo-continuation/index.ts": "4bc29a79ce7d85acc120c87a02cd09d2",
+    "src/hooks/todo-continuation/todo-hygiene.ts": "64009dcc4d7b88cc031cb0467ccb0387",
+    "src/index.ts": "f23fb25218b3f510051456e00d4646b5",
+    "src/interview/dashboard.ts": "dbe6703d036ff16952c98f5cc0066e5c",
+    "src/interview/document.ts": "c8c35c9042fdef497925c89ce1dba1b4",
+    "src/interview/helpers.ts": "b95a7e299bb4ab38ab66a272b3ba3612",
+    "src/interview/index.ts": "ab5c9a50b6c08826cfd53233cac75f38",
+    "src/interview/manager.ts": "62c337961c1e5638c3531c33a097c247",
+    "src/interview/parser.ts": "f555be74e939ac8a0e9eaf8fe2b38e11",
+    "src/interview/prompts.ts": "ff6e3cd2e95c407662b143af8db615fc",
+    "src/interview/server.ts": "486e31b94f0353a838a017510931bc50",
+    "src/interview/service.ts": "46f17024044edbcc2a2b8c8f5879f1d4",
+    "src/interview/types.ts": "2614f59dcf6fbbf1d7285644f98149a6",
+    "src/interview/ui.ts": "5f03d5500ed3bd9e5e0fd7449353dc20",
+    "src/mcp/context7.ts": "4e02e8ef204b6eb7e99a3209078428b5",
+    "src/mcp/grep-app.ts": "f76cb0ffb3484b16d55f27729e80e864",
+    "src/mcp/index.ts": "92464b907264ebd630e12a42ae6eee67",
+    "src/mcp/types.ts": "a67078f79aa8b99c41fb5be5d9fa9319",
+    "src/mcp/websearch.ts": "7c507eff1d6f9c01d3ccb928ea648ca7",
+    "src/multiplexer/factory.ts": "5ca22092bbe54953c620aed005485398",
+    "src/multiplexer/index.ts": "7117f0624caae306854253b90852e0b9",
+    "src/multiplexer/tmux/index.ts": "7873a9b809fa16f3266d16bc2d8f702c",
+    "src/multiplexer/types.ts": "09477cd693fa449950d53b0a856e3561",
+    "src/multiplexer/zellij/index.ts": "16b9534fafc904e84faaf862d3a67d37",
+    "src/skills/codemap/README.md": "fbb3e9fd31ae685b87e630df96c3c60a",
+    "src/skills/simplify/README.md": "2786c6e4e6b9f972193353b49741c8e3",
+    "src/tools/ast-grep/cli.ts": "94eea47198f97a4169f009e5249c3f7f",
+    "src/tools/ast-grep/constants.ts": "ef016f4d4c5a6861fed9c28e968cad07",
+    "src/tools/ast-grep/downloader.ts": "eda4a6bc69a3290a2e54f4d46c639bc1",
+    "src/tools/ast-grep/index.ts": "a2e6261cdd8f4ddfd5d89dcd5ad175eb",
+    "src/tools/ast-grep/tools.ts": "3f7c2c65cffd5273b0cd6c849800176d",
+    "src/tools/ast-grep/types.ts": "34ad28b5b1e9617b584f082dba9a427c",
+    "src/tools/ast-grep/utils.ts": "1dd3b2133c4b8c847a26eea0423bc0b2",
+    "src/tools/background.ts": "1b979e158a1ca515fae2755729d449aa",
+    "src/tools/council.ts": "4edec9c98d6e6d4082e5ee4eba0aa9c7",
+    "src/tools/index.ts": "f31e1c25b9af8375dba09a4b16766566",
+    "src/tools/lsp/client.ts": "c8821a8c2cc21c51c26ffb50759e7411",
+    "src/tools/lsp/config-store.ts": "e10072482fbb91b9214fa3bf333aa033",
+    "src/tools/lsp/config.ts": "13aeb4cab14b73db792f6411cc775f8a",
+    "src/tools/lsp/constants.ts": "06f4618b3937e6aaa6ec4076c2e9ed51",
+    "src/tools/lsp/index.ts": "913a5e5cb7f366a05e618905e65b4900",
+    "src/tools/lsp/tools.ts": "023d712c7a06ab9e43851826114309a0",
+    "src/tools/lsp/types.ts": "e11904f9c738f10670abbfbaa185004d",
+    "src/tools/lsp/utils.ts": "c64858938255ab3c964195106af9bff9",
+    "src/tools/smartfetch/binary.ts": "a65d816f46ebef11c39bda1764f82bb7",
+    "src/tools/smartfetch/cache.ts": "9a4e272b897b6914f0925919357bfce1",
+    "src/tools/smartfetch/constants.ts": "1ba20e00a4d3f4717eba62f381f9cd4c",
+    "src/tools/smartfetch/index.ts": "5bbf7898199c2764351dac4bc0b28b84",
+    "src/tools/smartfetch/network.ts": "307d17ba103ad69727c362aafa1a41ee",
+    "src/tools/smartfetch/secondary-model.ts": "83b6d543783bb05ded285da19871f2a4",
+    "src/tools/smartfetch/tool.ts": "03e91727dc3d408bdb7f751ac647c0de",
+    "src/tools/smartfetch/types.ts": "2576efe959365f34b7160c409fb54d26",
+    "src/tools/smartfetch/utils.ts": "ab169376765be6079f24f55862d9a90b",
+    "src/utils/agent-variant.ts": "d1d7492fd659516534845335e9bcd87a",
+    "src/utils/compat.ts": "806de91aefa1d164b004c6ca465d8700",
+    "src/utils/env.ts": "b76fbfea11c340337f6bdd8a9c87bb69",
+    "src/utils/index.ts": "cc35eee0a38f8dd094af5e3b26757804",
+    "src/utils/internal-initiator.ts": "64f4189f18ade892f92c0b30188dcd30",
+    "src/utils/logger.ts": "a73dd89ea1e97870b93d3387be122baf",
+    "src/utils/polling.ts": "b1d9c52df1fae7391234d0f5476d53b5",
+    "src/utils/session.ts": "a6d5dfb749b70bb3b96fee2d0428e1f9",
+    "src/utils/zip-extractor.ts": "11e6d1913e049f46099bb61d4a77e62b",
+    "tsconfig.json": "1d2bb6e93a43366843785a156c8e538a"
+  },
+  "folder_hashes": {
+    ".": "fe9dff93976b069d3333b562d3b87e0b",
+    "scripts": "7ebdcbc44fd1e155c3ef2cc3baecb925",
+    "src": "b3f31c80cf6fc23f609ebd934a5b9662",
+    "src/agents": "b3d9f47d074a62136012bc593054fd10",
+    "src/background": "10d8da779e14ca2191ecad616eb202f3",
+    "src/cli": "b2ffa3472c3b00b2f78d81a02913633c",
+    "src/config": "6dace0dbca5ca35c432ae47c243dd0f2",
+    "src/council": "446f3c30a7c879e8e22383929fdf1c87",
+    "src/hooks": "815f9b023ad0562f55a71e8cd15b656d",
+    "src/hooks/apply-patch": "d3bd03747a4c2ecd199fb68706d4c8d2",
+    "src/hooks/auto-update-checker": "c407065b8052ab293b8c78e643a19297",
+    "src/hooks/delegate-task-retry": "2624117607d8404122e82836c4c74114",
+    "src/hooks/filter-available-skills": "42214100c13e02e3b197f1e7cb1b586a",
+    "src/hooks/foreground-fallback": "7d31d4b918d1e1e1b674dece98e10c4d",
+    "src/hooks/json-error-recovery": "c8a245f5f48918279aa3d7724a573c75",
+    "src/hooks/phase-reminder": "80f01bd7edd895fcd3950a44b21a4d3a",
+    "src/hooks/post-file-tool-nudge": "e01c0aa6e649ec049c068d6a1b2006f9",
+    "src/hooks/todo-continuation": "b2a921fee7ba60ec8fc6cb055611907d",
+    "src/interview": "9284e82da3ddb9c5f140028c0c8c03ee",
+    "src/mcp": "5f5fc5fbb54bf9944063483cee8be88f",
+    "src/multiplexer": "14d687861c06ee71e303f57fb5ecc8bd",
+    "src/multiplexer/tmux": "796d0d51b317bdb05c54dcf257fb5597",
+    "src/multiplexer/zellij": "35ab5e99b43ac4da5a085d1e331da37b",
+    "src/skills": "c93b75814e75bc85966fba4513b81b24",
+    "src/skills/codemap": "1e82ef833612703b786daceb091f2422",
+    "src/skills/simplify": "9c745d8113135e3103af5f1a49d67dfe",
+    "src/tools": "2383d46a3f3878c98702e0470aca0ebb",
+    "src/tools/ast-grep": "2d4ad34fd02c6d068766dd38e826f2a8",
+    "src/tools/lsp": "952fb176aeb025f24ad127b8ca43e3eb",
+    "src/tools/smartfetch": "2b137fd3f65e105b005650b862b14fd7",
+    "src/utils": "7280f2365b7cbed95d7553dfb79dd238"
+  }
+}

+ 11 - 0
AGENTS.md

@@ -249,3 +249,14 @@ OpenCode has a built-in `/review` command that automatically performs comprehens
 - Council manager (multi-LLM orchestration) is in `src/council/`
 - Tmux utilities are in `src/utils/tmux.ts`
 - 468 tests across 35 files — run `bun test` to verify
+
+## Repository Map
+
+A full codemap is available at `codemap.md` in the project root.
+
+Before working on any task, read `codemap.md` to understand:
+- Project architecture and entry points
+- Directory responsibilities and design patterns
+- Data flow and integration points between modules
+
+For deep work on a specific folder, also read that folder's `codemap.md`.

+ 3 - 3
README.md

@@ -71,7 +71,7 @@ The default generated configuration looks like this:
       "orchestrator": { "model": "openai/gpt-5.4", "variant": "high", "skills": ["*"], "mcps": ["*", "!context7"] },
       "oracle": { "model": "openai/gpt-5.4", "variant": "high", "skills": ["simplify"], "mcps": [] },
       "librarian": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
-      "explorer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": ["cartography"], "mcps": [] },
+      "explorer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": [] },
       "designer": { "model": "openai/gpt-5.4-mini", "variant": "medium", "skills": ["agent-browser"], "mcps": [] },
       "fixer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": [] }
     }
@@ -465,7 +465,7 @@ Use this section as a map: start with installation, then jump to features, confi
 | **[Interview](docs/interview.md)** | Turn rough ideas into a structured markdown spec through a browser-based Q&A flow |
 | **[Multiplexer Integration](docs/multiplexer-integration.md)** | Watch agents work live in Tmux or Zellij panes |
 | **[Todo Continuation](docs/todo-continuation.md)** | Auto-continue orchestrator sessions with cooldowns and safety checks |
-| **[Cartography](docs/cartography.md)** | Generate hierarchical codemaps to understand large codebases faster |
+| **[Codemap](docs/codemap.md)** | Generate hierarchical codemaps to understand large codebases faster |
 
 ### ⚙️ Config & Reference
 
@@ -473,7 +473,7 @@ Use this section as a map: start with installation, then jump to features, confi
 |-----|----------------|
 | **[Configuration](docs/configuration.md)** | Config file locations, JSONC support, prompt overrides, and full option reference |
 | **[Maintainer Guide](docs/maintainers.md)** | Issue triage rules, label meanings, support routing, and repo maintenance workflow |
-| **[Skills](docs/skills.md)** | Built-in and recommended skills such as `simplify`, `agent-browser`, and `cartography` |
+| **[Skills](docs/skills.md)** | Built-in and recommended skills such as `simplify`, `agent-browser`, and `codemap` |
 | **[MCPs](docs/mcps.md)** | `websearch`, `context7`, `grep_app`, and how MCP permissions work per agent |
 | **[Tools](docs/tools.md)** | Built-in tool capabilities like `webfetch`, LSP tools, code search, and formatters |
 

+ 100 - 198
codemap.md

@@ -2,209 +2,111 @@
 
 ## Project Responsibility
 
-**oh-my-opencode-slim** is a lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode. It provides a multi-agent system that enables specialized AI agents to work together under an orchestrator to optimize coding tasks for quality, speed, cost, and reliability.
-
-The plugin integrates with OpenCode to provide:
-- **Multi-agent orchestration** with specialized roles (Orchestrator, Explorer, Librarian, Oracle, Designer, Fixer)
-- **Background task management** for long-running async operations
-- **MCP (Model Context Protocol) integration** for external tools and services
-- **LSP (Language Server Protocol) tools** for code intelligence
-- **Code search capabilities** via grep and AST-grep
-- **Tmux integration** for visual task tracking
-- **Configuration system** with agent overrides and skill management
-- **CLI installer** for interactive setup and configuration
+`oh-my-opencode-slim` is an OpenCode plugin that adds a specialist-agent operating model on top of the host runtime. Its core job is to:
 
-## System Entry Points
-
-| File | Purpose | Key Exports |
-|------|---------|-------------|
-| `package.json` | Project manifest, dependencies, and build scripts | `oh-my-opencode-slim` CLI, `dist/index.js` main entry |
-| `src/index.ts` | Main plugin entry point | `OhMyOpenCodeLite` plugin, agent configs, tools, MCPs |
-| `src/cli/index.ts` | CLI installer entry point | `install` command, configuration management |
-| `tsconfig.json` | TypeScript compiler configuration | Build settings, type checking, declaration generation |
-
-### Build Artifacts
+- define orchestrator and specialist agents,
+- load layered plugin configuration and per-agent permissions,
+- expose additional tools and MCP integrations,
+- manage delegated/background work and terminal multiplexer visualization,
+- inject workflow-enforcement hooks,
+- ship install-time skills and a bootstrap CLI.
 
-- `dist/index.js` - Main plugin bundle (ESM)
-- `dist/index.d.ts` - TypeScript declarations
-- `dist/cli/index.js` - CLI bundle
-- `dist/cli/index.d.ts` - CLI TypeScript declarations
+This codemap intentionally covers the plugin repository itself and excludes the nested `opencode/` upstream checkout.
 
-### Published Files
+## System Entry Points
 
-- `dist/` - Built JavaScript and declarations
-- `src/skills/` - Skill definitions (included in npm package)
-- `README.md` - Documentation
-- `LICENSE` - MIT license
+| Path | Role |
+|---|---|
+| `package.json` | Package manifest, dependency graph, release scripts, published file list. |
+| `src/index.ts` | Main plugin bootstrap: wires agents, tools, MCPs, hooks, background managers, interview manager, and config merge behavior. |
+| `src/cli/index.ts` | CLI entrypoint for installation/bootstrap workflows. |
+| `src/config/schema.ts` | Source-of-truth runtime config schema used by validation and schema generation. |
+| `scripts/generate-schema.ts` | Generates `oh-my-opencode-slim.schema.json` from the Zod config schema. |
 
 ## Repository Directory Map
 
 | Directory | Responsibility Summary | Detailed Map |
-|-----------|------------------------|--------------|
-| `src/` | Main plugin entrypoint plus all feature modules that compose agents, tools, hooks, multiplexer support, and utils. | [View Map](src/codemap.md) |
-| `src/agents/` | Defines specialist agents and the orchestrator, with factories and override/permission helpers. | [View Map](src/agents/codemap.md) |
-| `src/cli/` | Installer CLI flow, config edits, provider setup, and skill installation helpers. | [View Map](src/cli/codemap.md) |
-| `src/config/` | Plugin configuration schemas, defaults, loaders, and MCP/agent override helpers. | [View Map](src/config/codemap.md) |
-| `src/multiplexer/` | Tmux/Zellij pane orchestration for child sessions. | [View Map](src/multiplexer/codemap.md) |
-| `src/hooks/` | Lifecycle hooks for message transforms, error recovery, and rate-limit fallbacks. | [View Map](src/hooks/codemap.md) |
-| `src/hooks/auto-update-checker/` | Startup update check hook with cache invalidation and optional auto-install. | [View Map](src/hooks/auto-update-checker/codemap.md) |
-| `src/hooks/phase-reminder/` | Orchestrator message transform hook that injects phase reminders. | [View Map](src/hooks/phase-reminder/codemap.md) |
-| `src/hooks/post-file-tool-nudge/` | Read/Write tool after-hook that queues ephemeral delegation nudges. | [View Map](src/hooks/post-file-tool-nudge/codemap.md) |
-| `src/hooks/delegate-task-retry/` | Error detection and retry guidance with pattern matching and assistance. | [View Map](src/hooks/delegate-task-retry/codemap.md) |
-| `src/hooks/foreground-fallback/` | Rate-limit fallback manager for interactive sessions. | [View Map](src/hooks/foreground-fallback/codemap.md) |
-| `src/hooks/json-error-recovery/` | JSON parse error detection and recovery helpers. | [View Map](src/hooks/json-error-recovery/codemap.md) |
-| `src/mcp/` | Built-in MCP registry and config types for remote connectors. | [View Map](src/mcp/codemap.md) |
-| `src/tools/` | Tool registry plus LSP, AST-grep, council, and webfetch implementations. | [View Map](src/tools/codemap.md) |
-| `src/tools/ast-grep/` | AST-grep CLI discovery, execution, and tool definitions. | [View Map](src/tools/ast-grep/codemap.md) |
-| `src/tools/lsp/` | LSP client stack and tool surface for definitions, diagnostics, and rename. | [View Map](src/tools/lsp/codemap.md) |
-| `src/utils/` | Shared helpers for tmux, environment variables, internal initiation, and config. | [View Map](src/utils/codemap.md) |
-
-## Architecture Overview
-
-### Plugin Initialization Flow
-
-```
-OpenCode loads plugin
-    ↓
-src/index.ts: OhMyOpenCodeLite(ctx)
-    ↓
-Load plugin config (src/config)
-    ↓
-Initialize agent configs (src/agents)
-    ↓
-Initialize multiplexer/session helpers (src/multiplexer + src/utils)
-    ↓
-Initialize MCPs (src/mcp)
-    ↓
-Initialize hooks (src/hooks)
-    ↓
-Register tools (src/tools)
-    ↓
-Return plugin object with:
-    - agent: Agent configurations
-    - tool: Tool implementations
-    - mcp: MCP configurations
-    - config: Config merger
-    - event: Event handlers
-    - hooks: Message transforms
-```
-
-### Key Integrations
-
-1. **Agent System** (`src/agents/`)
-   - Orchestrator delegates to specialized subagents
-   - Each agent has specific tools, permissions, and temperature settings
-   - MCP tools configured per agent based on role
-
-2. **Multiplexer Integration** (`src/multiplexer/`)
-   - Child-session pane management
-   - Session lifecycle monitoring
-   - Optional tmux/zellij visual tracking
-
-3. **Configuration** (`src/config/`)
-   - User, project, and preset config layers
-   - Agent overrides and custom prompts
-   - MCP availability and permissions
-
-4. **Tools** (`src/tools/`)
-   - Code search (grep, AST-grep)
-   - LSP integration (diagnostics, references, rename)
-   - Background task orchestration
-
-5. **MCP Integration** (`src/mcp/`)
-   - Built-in remote MCPs (websearch, context7, grep.app)
-   - Type-safe configuration
-   - Disabled MCP filtering
-
-6. **Hooks** (`src/hooks/`)
-   - Auto-update checking
-   - `apply_patch` stale-patch rescue with strict parsing, bounded LCS fallback, stateful same-path helper updates, and safe canonical rewrites only inside root/worktree
-   - Phase reminders for workflow compliance
-   - Post-read nudges for delegation
-
-## Development Workflow
-
-```bash
-# Build the project
-bun run build
-
-# Type checking
-bun run typecheck
-
-# Run tests
-bun test
-
-# Linting
-bun run lint
-
-# Format code
-bun run format
-
-# Run all checks (lint + format + organize imports)
-bun run check
-
-# CI mode checks (no auto-fix)
-bun run check:ci
-
-# Build and run with OpenCode
-bun run dev
-```
-
-## Key Dependencies
-
-| Dependency | Purpose |
-|------------|---------|
-| `@opencode-ai/plugin` | OpenCode plugin SDK |
-| `@opencode-ai/sdk` | OpenCode AI SDK |
-| `@modelcontextprotocol/sdk` | MCP protocol implementation |
-| `@ast-grep/cli` | AST-aware code search |
-| `vscode-jsonrpc` | JSON-RPC protocol |
-| `vscode-languageserver-protocol` | LSP protocol |
-| `zod` | Runtime validation |
-
-## Extension Points
-
-### Adding New Agents
-
-1. Create agent definition in `src/agents/`
-2. Add to agent factory registry
-3. Configure default model in `src/config/constants.ts`
-4. Add MCP configuration in `src/config/agent-mcps.ts`
-5. Add skill permissions in `src/cli/skills/`
-
-### Adding New Tools
-
-1. Implement tool in `src/tools/`
-2. Export from `src/tools/index.ts`
-3. Register in main plugin (`src/index.ts`)
-4. Configure agent permissions
-
-### Adding New MCPs
-
-1. Define MCP config in `src/mcp/`
-2. Add to `createBuiltinMcps` registry
-3. Configure agent access in `src/config/agent-mcps.ts`
-
-### Adding New Hooks
-
-1. Implement hook in `src/hooks/`
-2. Export factory function from `src/hooks/index.ts`
-3. Register in main plugin (`src/index.ts`)
-
-## Configuration Structure
-
-```typescript
-interface PluginConfig {
-  agents?: {
-    [agentName: string]: AgentOverrideConfig;
-  };
-  tmux?: TmuxConfig;
-  disabled_mcps?: McpName[];
-  background?: BackgroundTaskConfig;
-  presets?: Record<string, Partial<PluginConfig>>;
-}
-```
-
-## License
-
-MIT License - See [LICENSE](LICENSE) for details.
+|---|---|---|
+| `src/` | Main application surface that composes plugin bootstrap, runtime modules, and installer-facing code. | [View Map](src/codemap.md) |
+| `src/agents/` | Agent factory layer for orchestrator, specialists, council agents, and config/permission shaping. | [View Map](src/agents/codemap.md) |
+| `src/cli/` | Installer, config editing, provider preset generation, and built-in skill installation. | [View Map](src/cli/codemap.md) |
+| `src/config/` | Configuration schema, defaults, loaders, constant tables, and agent/MCP policy helpers. | [View Map](src/config/codemap.md) |
+| `src/council/` | Multi-model council orchestration and synthesis fallback flow. | [View Map](src/council/codemap.md) |
+| `src/hooks/` | Aggregated runtime hook surface for prompt transforms, recovery logic, nudges, and lifecycle policies. | [View Map](src/hooks/codemap.md) |
+| `src/hooks/apply-patch/` | Structured `apply_patch` parsing, matching, recovery, and rewrite pipeline. | [View Map](src/hooks/apply-patch/codemap.md) |
+| `src/hooks/auto-update-checker/` | Startup update detection, cache handling, and optional install prompt flow. | [View Map](src/hooks/auto-update-checker/codemap.md) |
+| `src/hooks/delegate-task-retry/` | Post-tool retry guidance for failed delegation attempts. | [View Map](src/hooks/delegate-task-retry/codemap.md) |
+| `src/hooks/filter-available-skills/` | Skill-visibility filtering based on agent permission policy. | [View Map](src/hooks/filter-available-skills/codemap.md) |
+| `src/hooks/foreground-fallback/` | Interactive-session fallback control path for rate-limit or degraded foreground execution. | [View Map](src/hooks/foreground-fallback/codemap.md) |
+| `src/hooks/json-error-recovery/` | JSON/tool-output recovery helpers for malformed model responses. | [View Map](src/hooks/json-error-recovery/codemap.md) |
+| `src/hooks/phase-reminder/` | Message-transform reminder enforcing orchestrator workflow phases. | [View Map](src/hooks/phase-reminder/codemap.md) |
+| `src/hooks/post-file-tool-nudge/` | Post-read/write reminder path that nudges delegation-aware next steps. | [View Map](src/hooks/post-file-tool-nudge/codemap.md) |
+| `src/hooks/todo-continuation/` | Auto-continue behavior for outstanding todo execution. | [View Map](src/hooks/todo-continuation/codemap.md) |
+| `src/interview/` | `/interview` feature: prompt/state orchestration, persistence, local UI, and dashboard mode. | [View Map](src/interview/codemap.md) |
+| `src/mcp/` | Built-in MCP registry and per-provider MCP definitions. | [View Map](src/mcp/codemap.md) |
+| `src/multiplexer/` | Terminal multiplexer abstraction layer with backend selection and health helpers. | [View Map](src/multiplexer/codemap.md) |
+| `src/multiplexer/tmux/` | tmux backend implementation for pane lifecycle and layout management. | [View Map](src/multiplexer/tmux/codemap.md) |
+| `src/multiplexer/zellij/` | zellij backend implementation for tab/pane lifecycle. | [View Map](src/multiplexer/zellij/codemap.md) |
+| `src/skills/` | Bundled install-time OpenCode skills shipped as static payloads. | [View Map](src/skills/codemap.md) |
+| `src/skills/codemap/` | Repository-mapping skill package and codemap state-management script. | [View Map](src/skills/codemap/codemap.md) |
+| `src/skills/simplify/` | Behavior-preserving simplification skill package. | [View Map](src/skills/simplify/codemap.md) |
+| `src/tools/` | Tool export surface and orchestration helpers for AST-grep, LSP, smartfetch, council, and background tools. | [View Map](src/tools/codemap.md) |
+| `src/tools/ast-grep/` | AST-grep binary management and AST-aware search/replace tool flow. | [View Map](src/tools/ast-grep/codemap.md) |
+| `src/tools/lsp/` | LSP client pool, transport, formatting, and workspace-edit application. | [View Map](src/tools/lsp/codemap.md) |
+| `src/tools/smartfetch/` | Fetch/extract/cache pipeline for web content and secondary-model summarization. | [View Map](src/tools/smartfetch/codemap.md) |
+| `src/utils/` | Cross-cutting helpers for logging, sessions, compatibility, zip extraction, and runtime metadata. | [View Map](src/utils/codemap.md) |
+| `scripts/` | Build/release validation and generated-artifact maintenance scripts. | [View Map](scripts/codemap.md) |
+
+## Runtime Control Flow
+
+1. **Plugin startup**
+   - OpenCode loads `src/index.ts`.
+   - Config is loaded and normalized through `src/config/`.
+   - Agent definitions are produced by `src/agents/`.
+   - Tool factories from `src/tools/` and MCP definitions from `src/mcp/` are registered.
+   - Hooks from `src/hooks/` are attached.
+   - Background execution, multiplexer support, and interview support are initialized.
+
+2. **Interactive request handling**
+   - The orchestrator prompt drives routing decisions.
+   - Tool calls resolve through `src/tools/` or built-in OpenCode tools.
+   - Hooks can transform prompts/messages or repair tool failures before/after execution.
+
+3. **Delegated execution**
+   - OpenCode child sessions are created by task/council flows and tracked by plugin utilities.
+   - `src/multiplexer/` optionally mirrors those sessions into tmux/zellij panes.
+   - Results flow back into the parent session through notifications/output polling.
+
+4. **Install/release path**
+   - `src/cli/` configures host OpenCode instances.
+   - `src/skills/` is copied into the user skill directory.
+   - `scripts/` validates generated schema, package completeness, and host-load behavior.
+
+## Key Cross-Module Integration Points
+
+- `src/index.ts` is the central composition root for nearly every runtime subsystem.
+- `src/config/` feeds `src/agents/`, `src/tools/lsp/`, session/delegation utilities, and MCP registration.
+- `src/cli/skills.ts` and `src/cli/custom-skills.ts` bridge install-time skill packaging with runtime permission policy.
+- Session/delegation utilities depend on `src/multiplexer/` and cooperate with helpers in `src/utils/`.
+- `src/tools/council.ts` delegates into `src/council/`.
+- `src/hooks/filter-available-skills/` and agent permission logic rely on shared skill names from the CLI/config layer.
+- `src/interview/` hooks into plugin command/event surfaces exposed by `src/index.ts`.
+
+## Root Assets
+
+- `README.md`: user-facing product overview, install docs, and agent descriptions.
+- `AGENTS.md`: agent operating conventions for this repository.
+- `biome.json`: formatting/lint policy.
+- `tsconfig.json`: TypeScript compiler settings.
+- `.slim/codemap.json`: codemap change-detection state for this repository.
+
+## Recommended Reading Order
+
+1. `codemap.md`
+2. `src/codemap.md`
+3. One of:
+   - `src/agents/codemap.md`
+   - `src/multiplexer/codemap.md`
+   - `src/tools/codemap.md`
+   - `src/hooks/codemap.md`
+4. Relevant subsystem sub-map for the task at hand

+ 0 - 58
docs/cartography.md

@@ -1,58 +0,0 @@
-# Cartography Skill
-
-Cartography is a **custom skill** bundled with this repo.
-
-It helps agents quickly build a high-quality mental model of an unfamiliar codebase by generating a structured *codemap* and tracking changes over time.
-
-## What it does
-
-Cartography is designed for repository understanding and hierarchical codemap generation:
-
-1. Selects relevant code/config files using LLM judgment
-2. Creates `.slim/cartography.json` for change tracking
-3. Generates `codemap.md` templates (per folder) for explorers to fill in
-
-## How to use
-
-Cartography is installed automatically by the `oh-my-opencode-slim` installer when custom skills are enabled.
-
-### Run it (manual / local)
-
-From a repo root (or with an explicit `--root`):
-
-```bash
-# Initialize mapping
-python3 cartographer.py init --root /repo --include "src/**/*.ts" --exclude "node_modules/**"
-
-# Check what changed
-python3 cartographer.py changes --root /repo
-
-# Update hashes
-python3 cartographer.py update --root /repo
-```
-
-## Outputs
-
-### `.slim/cartography.json`
-
-A change-tracking file with hashes for files/folders.
-
-### `codemap.md` (per folder)
-
-Empty templates created in each folder so an Explorer-style agent can fill in:
-
-- Responsibility
-- Design patterns
-- Data/control flow
-- Integration points
-
-## Screenshot
-
-The existing screenshot lives in `img/cartography.png`.
-
-![Cartography screenshot](../img/cartography.png)
-
-## Related
-
-- `src/skills/cartography/README.md` and `src/skills/cartography/SKILL.md` contain the skill’s internal docs.
-- `codemap.md` at the repo root is an example output/starting point.

+ 59 - 0
docs/codemap.md

@@ -0,0 +1,59 @@
+# Codemap Skill
+
+Codemap is a **custom skill** bundled with this repo.
+
+It helps agents quickly build a high-quality mental model of an unfamiliar codebase by generating a structured *codemap* and tracking changes over time.
+
+## What it does
+
+Codemap is designed for repository understanding and hierarchical codemap generation:
+
+1. Selects relevant code/config files using LLM judgment
+2. Creates `.slim/codemap.json` for change tracking
+3. Generates `codemap.md` templates (per folder) for fixers to fill in
+4. Migrates legacy `.slim/cartography.json` state to `.slim/codemap.json`
+
+## How to use
+
+Codemap is installed automatically by the `oh-my-opencode-slim` installer when custom skills are enabled.
+
+### Run it (manual / local)
+
+From a repo root (or with an explicit `--root`):
+
+```bash
+# Initialize mapping
+node codemap.mjs init --root /repo --include "src/**/*.ts" --exclude "node_modules/**"
+
+# Check what changed
+node codemap.mjs changes --root /repo
+
+# Update hashes
+node codemap.mjs update --root /repo
+```
+
+## Outputs
+
+### `.slim/codemap.json`
+
+A change-tracking file with hashes for files/folders.
+
+### `codemap.md` (per folder)
+
+Empty templates created in each folder so a Fixer-style agent can fill in:
+
+- Responsibility
+- Design patterns
+- Data/control flow
+- Integration points
+
+## Screenshot
+
+The existing screenshot lives in `img/cartography.png`.
+
+![Codemap screenshot](../img/cartography.png)
+
+## Related
+
+- `src/skills/codemap/README.md` and `src/skills/codemap/SKILL.md` contain the skill’s internal docs.
+- `codemap.md` at the repo root is an example output/starting point.

+ 1 - 1
docs/installation.md

@@ -258,5 +258,5 @@ See the [Multiplexer Integration Guide](multiplexer-integration.md) for more det
    ```bash
    npx skills remove agent-browser
    rm -rf ~/.config/opencode/skills/simplify
-   rm -rf ~/.config/opencode/skills/cartography
+   rm -rf ~/.config/opencode/skills/codemap
    ```

+ 2 - 2
docs/quick-reference.md

@@ -16,13 +16,13 @@
 | [Interview](interview.md) | `/interview` command, browser UI, dashboard mode, multi-session coordination |
 | [Multiplexer Integration](multiplexer-integration.md) | Real-time pane monitoring, layouts, troubleshooting |
 | [Todo Continuation](todo-continuation.md) | `auto_continue`, `/auto-continue`, cooldowns, safety gates |
-| [Cartography Skill](cartography.md) | Hierarchical codemap generation |
+| [Codemap Skill](codemap.md) | Hierarchical codemap generation |
 
 ## ⚙️ Config & Reference
 
 | Doc | Contents |
 |-----|----------|
-| [Skills](skills.md) | `simplify`, `agent-browser`, `cartography` — skills assignment syntax |
+| [Skills](skills.md) | `simplify`, `agent-browser`, `codemap` — skills assignment syntax |
 | [MCPs](mcps.md) | `websearch`, `context7`, `grep_app` — permissions per agent, global disable |
 | [Tools](tools.md) | Background tasks, LSP, code search (`ast_grep`), formatters |
 | [Configuration](configuration.md) | Config files, prompt overriding, JSONC, full option reference table |

+ 6 - 6
docs/skills.md

@@ -19,7 +19,7 @@ Skills are installed via the `oh-my-opencode-slim` installer or manually with `n
 | Skill | Description | Assigned to by default |
 |-------|-------------|----------------------|
 | [`simplify`](#simplify) | Behavior-preserving code simplification | `oracle` |
-| [`cartography`](#cartography) | Repository codemap generation | `orchestrator`, `explorer` |
+| [`codemap`](#codemap) | Repository codemap generation | `orchestrator` |
 
 ---
 
@@ -43,13 +43,13 @@ Source: adapted from Addy Osmani's `code-simplification` skill and bundled local
 
 ---
 
-## cartography
+## codemap
 
 **Automated repository mapping through hierarchical codemaps.**
 
-`cartography` empowers the Orchestrator to build and maintain a deep architectural understanding of any codebase. Instead of reading thousands of lines of code on every task, agents refer to hierarchical `codemap.md` files describing the *why* and *how* of each directory.
+`codemap` empowers the Orchestrator to build and maintain a deep architectural understanding of any codebase. Instead of reading thousands of lines of code on every task, agents refer to hierarchical `codemap.md` files describing the *why* and *how* of each directory.
 
-**How to use:** Ask the Orchestrator to `run cartography`. It automatically detects whether to initialize a new map or update an existing one.
+**How to use:** Ask the Orchestrator to `run codemap`. It automatically detects whether to initialize a new map or update an existing one.
 
 **Why it's useful:**
 - **Instant onboarding** — understand unfamiliar codebases in seconds
@@ -57,7 +57,7 @@ Source: adapted from Addy Osmani's `code-simplification` skill and bundled local
 - **Change detection** — only modified folders are re-analyzed
 - **Timeless documentation** — focuses on high-level design, not implementation details
 
-See **[Cartography Skill](cartography.md)** for full documentation including manual commands and technical details.
+See **[Codemap Skill](codemap.md)** for full documentation including manual commands and technical details.
 
 ---
 
@@ -85,7 +85,7 @@ Control which skills each agent can use in `~/.config/opencode/oh-my-opencode-sl
   "presets": {
     "my-preset": {
       "orchestrator": {
-        "skills": ["cartography"]
+        "skills": ["codemap"]
       },
       "oracle": {
         "skills": ["simplify"]

+ 43 - 0
scripts/codemap.md

@@ -0,0 +1,43 @@
+# scripts/
+
+## Responsibility
+
+- Maintain repository-level build, packaging, and release validation automation.
+- Generate derived artifacts from source-of-truth types/schemas and verify that published outputs remain host-safe.
+- Provide pre- and post-packaging checks that prevent leaking local paths and validate plugin installability in external
+  OpenCode runtimes.
+
+## Design
+
+- `generate-schema.ts`
+  - Imports `PluginConfigSchema` from `src/config/schema.ts` and emits canonical JSON Schema via `z.toJSONSchema`.
+  - Writes `oh-my-opencode-slim.schema.json` with explicit `$schema`, `title`, and plugin description.
+- `verify-release-artifact.ts`
+  - Uses `spawnSync` + `npm pack --json --ignore-scripts`.
+  - Scans `dist/**/*` for leaked machine paths (`/Users/*`, `/home/*`).
+  - Validates required package payload keys (`package.json`, `dist/index.js`, `README.md`, `LICENSE`,
+    `src/skills/codemap/SKILL.md`, `src/skills/simplify/SKILL.md`, etc.).
+  - Performs clean install smoke by importing the installed `dist/index.js` default export in a temp project.
+- `verify-opencode-host-smoke.ts`
+  - Builds temporary OpenCode environment (bin from `bun add opencode-ai`), mounts the plugin tarball,
+    launches `opencode serve`, and probes `http://127.0.0.1:<port>/global/health`.
+  - Captures logs and fails on `failed to load plugin` and `cannot find module` patterns.
+- All scripts are executable boundary files (`#!/usr/bin/env bun` / Node), with explicit temp-dir lifecycle management
+  and defensive cleanup via `rmSync(..., { force: true, recursive: true })`.
+
+## Flow
+
+- `bun run build` invokes `scripts/generate-schema.ts` through `package.json#generate-schema` after type declaration generation.
+- `bun run verify:release` runs `verify-release-artifact.ts`: sanitize dist -> pack artifact -> validate files -> install/import check.
+- `bun run verify:host-smoke` runs `verify-opencode-host-smoke.ts`: pack tarball -> boot isolated host -> wait for health -> verify no plugin-load errors.
+- Both verification scripts are non-interactive and designed for CI/CD pre-publish gates.
+
+## Integration
+
+- Bound to `package.json` scripts for local dev and release pipelines.
+- Release verification depends on build outputs from `bun run build:plugin` and `bun run build:cli` because it expects
+  `dist/index.js`, `dist/cli/index.js`, and generated schema.
+- Package integrity expectations are mirrored by tests and release scripts that assert packaged skill metadata and
+  runtime files are present.
+- Smoke checks instantiate the same plugin entrypoint (`dist/index.js`) that `src/index.ts` exports,
+  catching runtime breakage before publishing.

+ 1 - 1
scripts/verify-release-artifact.ts

@@ -30,7 +30,7 @@ const packagedRequiredFiles = [
   'dist/cli/index.js',
   'oh-my-opencode-slim.schema.json',
   'src/skills/simplify/SKILL.md',
-  'src/skills/cartography/SKILL.md',
+  'src/skills/codemap/SKILL.md',
 ];
 
 function fail(message: string): never {

+ 8 - 8
src/agents/index.test.ts

@@ -219,7 +219,7 @@ describe('per-model variant in array config', () => {
 });
 
 describe('skill permissions', () => {
-  test('orchestrator gets cartography skill allowed by default', () => {
+  test('orchestrator gets codemap skill allowed by default', () => {
     const agents = createAgents();
     const orchestrator = agents.find((a) => a.name === 'orchestrator');
     expect(orchestrator).toBeDefined();
@@ -228,17 +228,17 @@ describe('skill permissions', () => {
     )?.skill as Record<string, string>;
     // orchestrator gets wildcard allow (from RECOMMENDED_SKILLS wildcard entry)
     expect(skillPerm?.['*']).toBe('allow');
-    // CUSTOM_SKILLS loop must also add a named cartography entry for orchestrator
-    expect(skillPerm?.cartography).toBe('allow');
+    // CUSTOM_SKILLS loop must also add a named codemap entry for orchestrator
+    expect(skillPerm?.codemap).toBe('allow');
   });
 
-  test('explorer gets cartography skill allowed by default', () => {
+  test('fixer does not get codemap skill allowed by default', () => {
     const agents = createAgents();
-    const explorer = agents.find((a) => a.name === 'explorer');
-    expect(explorer).toBeDefined();
-    const skillPerm = (explorer?.config.permission as Record<string, unknown>)
+    const fixer = agents.find((a) => a.name === 'fixer');
+    expect(fixer).toBeDefined();
+    const skillPerm = (fixer?.config.permission as Record<string, unknown>)
       ?.skill as Record<string, string>;
-    expect(skillPerm?.cartography).toBe('allow');
+    expect(skillPerm?.codemap).not.toBe('allow');
   });
 
   test('oracle gets requesting-code-review skill allowed by default', () => {

+ 1 - 1
src/cli/codemap.md

@@ -400,7 +400,7 @@ InstallConfig (preferences)
 
 | Name | Source | Agents | Description |
 |------|--------|--------|-------------|
-| `cartography` | src/skills/cartography | orchestrator, explorer | Repository understanding and hierarchical codemap generation |
+| `codemap` | src/skills/codemap | orchestrator | Repository understanding and hierarchical codemap generation |
 
 ### Permission-Only Skills (external, not installed)
 

+ 3 - 3
src/cli/custom-skills.ts

@@ -35,10 +35,10 @@ export const CUSTOM_SKILLS: CustomSkill[] = [
     sourcePath: 'src/skills/simplify',
   },
   {
-    name: 'cartography',
+    name: 'codemap',
     description: 'Repository understanding and hierarchical codemap generation',
-    allowedAgents: ['orchestrator', 'explorer'],
-    sourcePath: 'src/skills/cartography',
+    allowedAgents: ['orchestrator'],
+    sourcePath: 'src/skills/codemap',
   },
 ];
 

+ 6 - 3
src/cli/providers.test.ts

@@ -79,13 +79,16 @@ describe('providers', () => {
     // Oracle should have bundled simplify
     expect(agents.oracle.skills).toContain('simplify');
 
-    // Explorer should have bundled cartography
-    expect(agents.explorer.skills).toContain('cartography');
+    // Orchestrator should implicitly cover bundled codemap via '*'
+    expect(agents.orchestrator.skills).toContain('*');
 
     // Designer should have 'agent-browser'
     expect(agents.designer.skills).toContain('agent-browser');
 
-    // Fixer should have no skills by default (empty recommended list)
+    // Explorer should have no bundled skills by default
+    expect(agents.explorer.skills).toEqual([]);
+
+    // Fixer should have no bundled skills by default
     expect(agents.fixer.skills).toEqual([]);
   });
 

+ 4 - 4
src/codemap.md

@@ -1,20 +1,20 @@
 # src/
 
 ## Responsibility
-- `src/index.ts` delivers the oh-my-opencode-slim plugin by merging configuration, instantiating orchestrator/subagent definitions, wiring multiplexer helpers, built-in tools, MCPs, and lifecycle hooks so OpenCode sees a single cohesive module.
+- `src/index.ts` delivers the oh-my-opencode-slim plugin by merging configuration, instantiating orchestrator/subagent definitions, wiring session/delegation tracking, multiplexer helpers, built-in tools, MCPs, and lifecycle hooks so OpenCode sees a single cohesive module.
 - `config/`, `agents/`, `tools/`, `multiplexer/`, `hooks/`, and `utils/` contain the reusable building blocks (loader/schema/constants, agent factories/permission helpers, tool factories, child-session pane managers, hook implementations, and tmux/variant/log helpers) that power that entry point.
 - `cli/` exposes the install/update script (argument parsing + interactive prompts) that edits OpenCode config, installs recommended/custom skills, and updates provider credentials to bootstrap this plugin on a host machine.
 
 ## Design
 - Agent creation follows explicit factories (`agents/index.ts`, per-agent creators under `agents/`) with override/permission helpers (`config/utils.ts`, `cli/skills.ts`) so defaults live in `config/constants.ts`, prompts can be swapped via `config/loader.ts`, and variant labels propagate through `utils/agent-variant.ts`.
-- Multiplexer tooling composes `MultiplexerSessionManager` with `utils/tmux.ts`/`multiplexer/*` so child agent sessions automatically open and close panes while the main session keeps running.
+- Session orchestration combines `MultiplexerSessionManager` with `SubagentDepthTracker` and `multiplexer/*` so child agent sessions are depth-tracked, can surface in panes, and are cleaned up consistently.
 - Hooks are isolated (`hooks/auto-update-checker`, `phase-reminder`, `post-file-tool-nudge`) and exported via `hooks/index.ts`, so the plugin simply registers them via the `event`, `experimental.chat.system.transform`, `experimental.chat.messages.transform`, and `tool.execute.after` hooks defined in `index.ts`.
 - Supplemental tools (`tools/grep`, `tools/lsp`, `tools/quota`) bundle ripgrep, LSP helpers, and Antigravity quota calls behind the OpenCode `tool` interface and are mounted in `index.ts` alongside council/webfetch helpers.
 
 ## Flow
 - Startup: `index.ts` calls `loadPluginConfig` (user + project JSON + presets) to build a `PluginConfig`, passes it to `getAgentConfigs` (which uses `createAgents`, agent factories, `loadAgentPrompt`, and `getAgentMcpList`) and to `MultiplexerSessionManager`/`CouncilManager` so the in-memory state matches user overrides.
-- Plugin registration: `index.ts` registers agents, the tool map (`task`, council, `webfetch`, `ast_grep_*`, `lsp_*`), MCP definitions (`createBuiltinMcps`), and hooks (`createAutoUpdateCheckerHook`, `createPhaseReminderHook`, `createPostReadNudgeHook`); configuration hook merges those values back into the OpenCode config (default agent, permission rules parsed from `config/agent-mcps`, and MCP access policies).
-- Runtime: `MultiplexerSessionManager` observes `session.created` events to spawn panes via `utils/tmux` and closes them when sessions idle or are deleted, while `CouncilManager` manages parallel child sessions for council runs.
+- Plugin registration: `index.ts` registers agents, the tool map (council, `webfetch`, `ast_grep_*`, `lsp_*`), MCP definitions (`createBuiltinMcps`), and hooks (`createAutoUpdateCheckerHook`, `createPhaseReminderHook`, `createPostReadNudgeHook`); configuration hook merges those values back into the OpenCode config (default agent, permission rules parsed from `config/agent-mcps`, and MCP access policies).
+- Runtime: `MultiplexerSessionManager` observes `session.created` events to spawn panes via multiplexer backends and closes them when sessions idle or are deleted, while session events also feed `SubagentDepthTracker` so nested child sessions remain bounded.
 - CLI flow: `cli/install.ts` parses flags, optionally asks interactive prompts, checks OpenCode installation, adds plugin entries via `cli/config-manager.ts`, disables default agents, writes the lite config (`cli/config-io.ts`), and installs skills (`cli/skills.ts`, `cli/custom-skills.ts`).
 
 ## Integration

+ 29 - 0
src/council/codemap.md

@@ -0,0 +1,29 @@
+# src/council/
+
+## Responsibility
+
+- Orchestrate multi-agent council sessions by running configured councillors and synthesizing outputs with a master model.
+- Provide a configurable, defensive decision path for model-based consensus, including depth guarding and graceful degradation when partial failures occur.
+
+## Design
+
+- `src/council/index.ts` is a barrel export; implementation is in `council-manager.ts`.
+- `CouncilManager` is injected with `PluginInput`, optional `PluginConfig`, optional `SubagentDepthTracker`, and multiplexer settings.
+- Presets and schema are defined in `config/council-schema.ts` (`default_preset`, councillor/master timeout, retry rules, execution mode).
+- Uses shared formatting/parsing helpers from `utils/session.ts` so councillor/master prompting stays consistent with other session tools.
+
+## Flow
+
+- `runCouncil(prompt, presetName, parentSessionId)`:
+  - Validate depth limit and resolve preset.
+  - Abort early for missing config, unknown preset, or empty councillor set.
+  - Send a lightweight start notification into the parent session.
+  - Run councillors in configured mode (`parallel`/`serial`) with per-councillor timeout and retries.
+  - Aggregate completed responses; if none succeed, return failure.
+  - Run master synthesis on success; if master fails, fallback to a single completed councillor response with context.
+
+## Integration
+
+- Used by `createCouncilTool` in the tools layer for explicit user-triggered council sessions.
+- Depends on `SubagentDepthTracker` to prevent runaway nested delegation.
+- Reuses the same OpenCode session API contract (`client.session`) as background tasks and other agent orchestration modules.

+ 25 - 74
src/hooks/apply-patch/codemap.md

@@ -1,82 +1,33 @@
-# apply-patch codemap
+# apply-patch
 
-## Hook responsibility
+## Responsibility
 
-`src/hooks/apply-patch/` intercepts only the `apply_patch` tool before OpenCode executes it and, when it detects a stale but recoverable patch, rewrites only the canonical old lines so the native runtime can apply it without inventing new changes. If any patch path falls outside `root`/`worktree`, the hook blocks `apply_patch` before native execution.
+Provide a resilient preprocessor for `tool.execute.before` on `apply_patch` that rewrites recoverable stale hunks, validates workspace boundaries, and blocks unsafe patches before they reach OpenCode’s native patch executor.
 
-## Flow
-
-1. `index.ts` keeps the hook always active and calls `rewritePatchText(...)`.
-2. `operations.ts` remains a thin internal barrel over the concrete modules.
-3. `codec.ts` parses the `*** Begin Patch` / `*** End Patch` format and can serialize it again.
-4. `resolution.ts` resolves each chunk against the real file and canonicalizes safe tolerant matches.
-5. `matching.ts` implements exact matching and tolerant rescue paths (unicode, trim, prefix/suffix, and a bounded conservative LCS).
-6. `patch.ts` remains a compatibility shim; internal code imports the concrete modules.
-
-## Modules
-
-### `types.ts`
-- Shared patch-domain types.
-- Separates public contracts (`PatchChunk`, `PreparedChange`, etc.) from the logic.
-
-### `codec.ts`
-- `normalizeUnicode()` and `stripHeredoc()`.
-- `parsePatch()` converts text into hunks, and `parsePatchStrict()` provides the strict validation used by rewrite/prepare.
-- `formatPatch()` rebuilds the patch by reusing the lines from `new_lines` byte-for-byte; if an insertion is re-anchored, it also adds the required intact anchor line.
-
-### `matching.ts`
-- Exact/unicode/trim comparators.
-- `seek()` / `seekMatch()` and `list()` search for sequences.
-- `prefix()` / `suffix()` provide edge-based rescue.
-- `rescueByPrefixSuffix()` and `rescueByLcs()` recover stale chunks deterministically.
+## Design
 
-### `resolution.ts`
-- `readFileLines()` reads the file as logical lines; `deriveNewContent()` rebuilds it while preserving the detected physical EOL (`LF` or `CRLF`).
-- `resolveChunkStart()` uses `change_context` as the initial anchor.
-- `locateChunk()` chooses between exact match, canonicalized tolerant match, edge rescue, or LCS rescue.
-- `resolveUpdateChunks()` detects overlaps, prepares ordered hits, and handles the special anchored-insertion path for chunks without `old_lines`, including safe EOF canonicalization when the anchor only resolved through tolerant matching.
-- `deriveNewContent()` / `deriveNewContentFromText()` and `applyHits()` produce the final content while preserving `LF`/`CRLF` and the physical final-newline state in updates.
+- Entry point is `createApplyPatchHook` in `index.ts`, bound to `tool.execute.before`.
+- `rewritePatch` (`operations.ts`) is the main pipeline used by the hook and is backed by:
+  - `parseValidatedPatch` / `createPatchExecutionContext` (`execution-context.ts`) for patch parsing and path/state validation.
+  - `parsePatch` / `parsePatchStrict` / `formatPatch` (`codec.ts`) for patch AST conversion and serialization.
+  - `resolveChunkStart`, `locateChunk`, `resolveUpdateChunks`, `applyHits` (`resolution.ts`) for context matching.
+  - `resolveBy...` helpers in `matching.ts` (`seek`, `seekMatch`, `list`, `rescueByPrefixSuffix`, `rescueByLcs`) for tolerant matching.
+- `types.ts` defines domain contracts used across modules (`PatchChunk`, `PatchHunk`, `ResolvedChunk`, `ApplyPatchErrorKind`, etc.).
+- Error semantics are centralized in `errors.ts` (`ApplyPatchError`, `createApplyPatchBlockedError`, `createApplyPatchVerificationError`, `isApplyPatchError`) and surfaced in hook logging and thrown errors.
+- No additional runtime configuration is exposed; behavior is controlled by constant `APPLY_PATCH_RESCUE_OPTIONS` (`prefixSuffix` + `lcsRescue`).
 
-### `execution-context.ts`
-- `parseValidatedPatch()` centralizes upfront validation.
-- Owns the path guard, realpath/stat caches, and the staged file-state machine.
-- `createPatchExecutionContext()` is the shared entry point for rewrite/prepare.
-
-### `rewrite.ts`
-- `rewritePatchText()` rewrites update chunks when rescue or safe canonicalization happened.
-- It performs a global pre-scan of `add`/`delete`/`update`/`move` before rewriting anything.
-- `rewritePatch()` validates `Delete File` with the same staged state machine as `preparePatchChanges()`; this makes it fail if the file no longer exists in the prepared context (real missing file, double delete, or delete after a previous move/delete) before delegating to the native runtime.
-- `rewritePatch()` also detects when an `Update File` is no longer self-contained because it consumes staged state from an earlier hunk (for example `add -> update`, `move -> update`, or `update -> update`) and collapses that chain into a canonical form that is safe for the native runtime.
-- It keeps the merge/minimize/collapse helpers used for dependent update groups.
-
-### `prepared-changes.ts`
-- `preparePatchChanges()` converts hunks into filesystem changes while accumulating state per path to support multiple sequential `Update File` hunks on the same file.
-- `applyPreparedChanges()` is documented as an internal best-effort rollback helper that consumes the output of `preparePatchChanges()`, not as a universal transactional engine; it also revalidates the basic shape of the legacy array (types/text/normalized absolute paths) and filesystem invariants before touching disk.
-
-### `operations.ts`
-- Thin internal facade/barrel that preserves existing imports for the hook, tests, and compatibility shim.
-
-### `patch.ts`
-- Thin facade/barrel.
-- Re-exports only the stable public API used by the hook and the tests.
-
-## Invariants
+## Flow
 
-- The hook remains always active and has no public config.
-- Content provided through `new_lines` is neither normalized nor rewritten; it is only reused byte-for-byte, except for the intact anchor line that may be added for re-anchored insertions.
-- Updates preserve the detected physical `EOL` and whether the original file ended with a newline.
-- If a path falls outside `root`/`worktree`, the hook blocks `apply_patch` before native execution.
-- No new limits, flags, or runtime settings are introduced.
-- The scope of this rescue remains limited to `apply_patch`; it does not rewrite `edit` or `write`.
-- Errors remain descriptive and keep the `apply_patch verification failed` prefix where it already existed.
-- Normal rewriting remains limited to `update` chunks; only dependent chains between hunks may collapse a previous `add` into the equivalent final state to make the patch self-contained again.
-- If an exact resolution depends on the staged result of earlier hunks, the patch is no longer considered "intact" and is collapsed into a self-contained form before handoff to native.
-- `Delete File` shares the same staged semantics in both rewrite and prepare; an already invalid delete must not reach native.
-- Resolved chunks cannot overlap.
+1. `createApplyPatchHook` filters only `input.tool === 'apply_patch'`.
+2. It requires `output.args.patchText` to be a string.
+3. It resolves `root` and `worktree` from `input.directory` / `ctx.directory` / `ctx.worktree`.
+4. It calls `rewritePatch(root, patchText, options, worktree)`.
+5. On `result.changed`, it replaces `output.args.patchText` with canonicalized patch text.
+6. On failure, it normalizes to `ApplyPatchError`, logs `blocked | validation | verification | internal`, and rethrows so native execution is prevented.
 
-## Quick maintenance guide
+## Integration
 
-- Parsing or rendering issue for patches? → `codec.ts`
-- Issue locating stale lines? → `matching.ts` and `resolution.ts`
-- Issue writing files or moving paths? → `prepared-changes.ts`
-- Need to know what the real hook consumes? → `index.ts` and `patch.ts`
+- Consumed by `src/index.ts` through `createApplyPatchHook`.
+- Acts before native tool execution via OpenCode hook point `tool.execute.before`.
+- Downstream dependencies include `ctx.client` indirectly only for context, and `utils/logger` for structured hook telemetry.
+- Uses `Patch` parser/resolver modules to keep `new_lines` byte-preserving while only mutating stale anchors and chunk context.

+ 26 - 15
src/hooks/auto-update-checker/codemap.md

@@ -2,27 +2,38 @@
 
 ## Responsibility
 
-- Provides an OpenCode hook that reacts to `session.created`, ensures the hook only runs once per startup, and surfaces update information to the user via TUI toasts and logs.
-- Detects local development builds, cached installs, and running plugins pinned to specific versions so the hook can decide whether to notify, auto-update, or skip work.
+- Provide a startup hook that detects plugin update availability for `oh-my-opencode-slim`, reports status through TUI toasts, and optionally performs a cache-safe `bun install` refresh.
+- Handle local dev mode and pinned plugin versions distinctly (`file://`, pinned tags, and `latest` channel semantics).
 
 ## Design
 
-- `index.ts` orchestrates the hook lifecycle: it filters session events, defers the heavy work via `setTimeout`, and delegates version discovery and updates to helper functions while respecting the user’s `autoUpdate` and `showStartupToast` preferences.
-- `checker.ts` encapsulates environment-aware utilities (config path discovery, local dev detection, NPM registry fetching, and pinned-version mutation) plus memoized cache lookups so the hook can derive current, cached, and latest versions without duplicating logic.
-- `cache.ts` is responsible for invalidating cached installs (`node_modules`, `package.json`, `bun.lock`) before a fresh `bun install`, keeping the cached package state consistent with the server-provided latest version.
-- Shared `constants.ts` standardizes paths (cache directory, config locations, package name, registry URL) and fetch timeouts so the rest of the hook is configuration-free.
+- `createAutoUpdateCheckerHook(ctx, options)` in `index.ts` registers an `event` handler for `session.created` and guards one-time startup execution (`hasChecked`).
+- `runBackgroundUpdateCheck` performs version resolution and branches into:
+  - local-dev no-op path,
+  - pinned plugin notification,
+  - manual notification when `autoUpdate=false`,
+  - or auto-update execution path.
+- `checker.ts` is the core discovery layer and exports:
+  - `findPluginEntry`, `extractChannel`, `getCachedVersion`, `getLocalDevVersion`, `getLatestVersion`, `updatePinnedVersion`.
+- `cache.ts` owns cache preparation with `resolveInstallContext` and `preparePackageUpdate`.
+- `constants.ts` centralizes install and config-path constants (`CACHE_DIR`, `PACKAGE_NAME`, `NPM_REGISTRY_URL`, `NPM_FETCH_TIMEOUT`, config path aliases).
+- `types.ts` declares `AutoUpdateCheckerOptions`, `PluginEntryInfo`, config/package typed envelopes.
 
 ## Flow
 
-- On the first `session.created` event without a parent session, `createAutoUpdateCheckerHook` schedules `runBackgroundUpdateCheck` while immediately showing an initial toast (unless disabled) and short-circuiting for local dev builds.
-- `runBackgroundUpdateCheck` resolves the current plugin entry and cached version, determines the update channel via `extractChannel`, retrieves the latest dist-tag from `getLatestVersion`, and compares versions.
-- When an update is available, the hook either notifies the user or, if `autoUpdate` is on, updates the pinned entry in the OpenCode config (`updatePinnedVersion`), invalidates the cached package, and runs `bun install` safely (`runBunInstallSafe`) with a 60-second timeout before showing success/error toasts.
-- `checker.ts` supports the above flow with helpers (`getLocalDevVersion`, `findPluginEntry`, `getCachedVersion`, `extractChannel`, etc.) that read configs (`.opencode/*.jsonc`, global config paths) via `stripJsonComments` and `fs` operations.
-- `cache.ts` runs before reinstall to remove lingering plugin directories, dependency entries, and JSON-formatted `bun.lock` references, ensuring `runBunInstallSafe` operates on a clean slate.
+1. On first eligible `session.created` (root/no parent), schedule asynchronous update check.
+2. If local development plugin is detected (`getLocalDevVersion`), emit info toast and return.
+3. Resolve current version from `getCachedVersion` + plugin entry in config (`findPluginEntry`).
+4. Fetch channel metadata (`extractChannel` + `getLatestVersion`).
+5. If update is needed:
+   - pinned entry ⇒ notify only,
+   - unpinned and `autoUpdate=false` ⇒ notify only,
+   - unpinned and auto-update enabled ⇒ call `preparePackageUpdate`, then `runBunInstallSafe`.
+6. Surface success/failure via `ctx.client.tui.showToast` and `utils/logger`.
 
 ## Integration
 
-- Hooks into the OpenCode plugin lifecycle via `ctx.client.tui.showToast` and the `session.created` event, leveraging `PluginInput` to know the working directory and show UI feedback.
-- Reads OpenCode configuration files exposed by `../../cli/config-manager` to locate plugin entries, pinned versions, and local `file://` installs, so it stays aligned with the same config sources that enable plugin loading.
-- Uses shared `../../utils/logger` for tracing background operations and errors while talking to platform APIs (`fetch`, `Bun.spawn`, `fs`) to inspect, mutate, and reinstall the `oh-my-opencode-slim` package stored under `CACHE_DIR`.
-- Exposes `AutoUpdateCheckerOptions` for consumers (via `index.ts`) to opt out of toasts or automatic installs while still reusing the same checker/cache helpers.
+- Wired through `src/hooks/index.ts` and plugin initialization (`src/index.ts`) as an `event` hook.
+- Consumes `PluginInput.client.tui.showToast`, `PluginInput.directory`, `ctx.client` context, and reads config paths through `cli/config-manager` (`stripJsonComments`, `getOpenCodeConfigPaths`).
+- Runtime interactions use `crossSpawn` for `bun install`, Node `fs/path`, and `fetch` against `NPM_REGISTRY_URL`.
+- Export surface includes `getAutoUpdateInstallDir` and `AutoUpdateCheckerOptions` for testability and host-side overrides.

+ 18 - 10
src/hooks/codemap.md

@@ -4,25 +4,33 @@ This directory exposes the public hook entry points that feature code imports to
 
 ## Responsibility
 
-Acts as a single entry point that re-exports the factory functions and types for every hook implementation underneath `src/hooks/`, so other modules can import from a flat namespace without needing to know subpaths.
+Acts as the central export surface for all runtime hooks in `src/hooks/*`, so callers can register feature behavior from one namespace (`src/hooks/index.ts`) without depending on subfolder internals.
 
 ## Design
 
-- **Aggregator/re-export pattern**: `index.ts` consolidates all hook factories and types for the entire hooks subsystem.
-- **Factory-based design**: Each hook is a factory function that returns a hook object with specific hook points (e.g., `'tool.execute.after'`, `'experimental.chat.messages.transform'`, `'chat.headers'`).
-- **Modular architecture**: Each hook lives in its own subdirectory with internal components (hook implementation, patterns, guidance, etc.).
-- **Event-driven hooks**: Hooks respond to OpenCode plugin events and modify output before it reaches the LLM or UI.
+- **Aggregator/re-export pattern**: `src/hooks/index.ts` re-exports hook factories, classes, and types from submodules.
+- **Factory architecture**: Every feature module exports a `create*Hook` factory and returns an object implementing one or more OpenCode lifecycle surfaces (`tool.execute.before`, `tool.execute.after`, message/system transforms, `event`, command handlers).
+- **Submodule boundary**: Each behavior class/function set is isolated in a folder (`auto-update-checker/`, `foreground-fallback/`, etc.) with a local `index.ts` shim.
+- **Typed hook contracts**: The exported signatures use concrete OpenCode plugin types where available (`PluginInput`) and strict internal helper shapes for internal state.
 
 ## Flow
 
-1. **Import**: Feature modules import factories from `src/hooks/index.ts` (e.g., `createPhaseReminderHook`, `createJsonErrorRecoveryHook`).
-2. **Configure**: Call factory with any required context (e.g., `PluginInput` for client access).
-3. **Register**: Hook objects are registered with OpenCode's plugin system via the feature layer.
-4. **Execute**: At runtime, OpenCode invokes hook functions at specific points (tool execution, message transformation, event handling).
-5. **Modify**: Hooks inspect input/output and apply side-effects (inject text, modify headers, append guidance).
+1. Consumers import from `src/hooks/index.ts` (e.g. `createTodoContinuationHook`, `createPhaseReminderHook`).
+2. Plugin initialization in `src/index.ts` invokes hook factories with `PluginInput` and optional config.
+3. OpenCode dispatches lifecycle callbacks at tool, message, system, event, and command surfaces.
+4. Each implementation mutates the provided payload (e.g., `output.output`, `output.messages`, `output.system`) or triggers client APIs.
+5. Hook handlers produce side effects like retries, prompts, reminders, and metadata updates.
 
 ## Integration
 
+- `createAutoUpdateCheckerHook`: lifecycle `event` (`session.created`) and startup `ctx.client.tui` notifications.
+- `createApplyPatchHook`: `tool.execute.before` pre-processing for `apply_patch` arguments.
+- `createDelegateTaskRetryHook`, `createJsonErrorRecoveryHook`, `createPostFileToolNudgeHook`, `createFilterAvailableSkillsHook`: primarily `tool.execute.after` / message-transform surfaces.
+- `createPhaseReminderHook`, todo-continuation message system handlers: `experimental.chat.messages.transform` / `experimental.chat.system.transform`.
+- `ForegroundFallbackManager`: wired to the global event stream for foreground fallback on rate-limit conditions.
+- `createChatHeadersHook`, `processImageAttachments`: transport/attachments/header-level integrations.
+- All submodules are consumed through `src/index.ts`, which also wires `auto-continue` command integration from `todo-continuation`.
+
 ### Hook Points
 
 | Hook Point | Purpose | Hooks |

+ 33 - 0
src/hooks/delegate-task-retry/codemap.md

@@ -0,0 +1,33 @@
+# src/hooks/delegate-task-retry/
+
+## Responsibility
+
+- Detect delegate tool argument failures after execution and append structured, actionable retry guidance directly into string outputs so the model can recover in-place.
+
+## Design
+
+- `patterns.ts` defines the declarative error contract:
+  - `DelegateTaskErrorPattern` (`pattern`, `errorType`, `fixHint`)
+  - `DELEGATE_TASK_ERROR_PATTERNS`
+  - `detectDelegateTaskError(output: string): DetectedError | null`
+- `guidance.ts` implements `buildRetryGuidance(errorInfo)` and `extractAvailableList` to render fix text with optional `Available:` details from tool output.
+- `hook.ts` implements `createDelegateTaskRetryHook`:
+  - targets only the built-in `task` tool.
+  - only mutates when `output.output` is string.
+  - detects errors via `detectDelegateTaskError` and appends guidance once.
+- `index.ts` is a strict re-export boundary.
+
+## Flow
+
+1. At `tool.execute.after`, confirm tool is `task`.
+2. Verify output payload type is string.
+3. Quick-scan for generic error indicators (`[ERROR]`, `Invalid arguments`, `is not allowed...`).
+4. Match each configured `DELEGATE_TASK_ERROR_PATTERNS` substring in output.
+5. Resolve `DetectedError` and append `\n` + `buildRetryGuidance(...)`.
+
+## Integration
+
+- Registered via `src/hooks/index.ts` as a `tool.execute.after` hook for tool-call outputs.
+- No dependencies on scheduling/backends (`task` execution itself is delegated to OpenCode core), limiting blast radius.
+- Input/output compatibility is limited to OpenCode hook payload shape (`{ tool, output }`) and string-based error diagnostics.
+- Primarily consumed by `src/index.ts` where delegation robustness is needed for orchestrator tool usage.

+ 32 - 0
src/hooks/filter-available-skills/codemap.md

@@ -0,0 +1,32 @@
+# src/hooks/filter-available-skills/
+
+## Responsibility
+
+- Filter `<available_skills>` payload fragments in outgoing messages so they only include skills permitted for the active agent.
+
+## Design
+
+- Factory `createFilterAvailableSkillsHook(_ctx, config)` is defined in `index.ts` and implements `experimental.chat.messages.transform`.
+- `getCurrentAgent(messages)` scans backward for the latest user message and defaults to `orchestrator`.
+- `filterAvailableSkillsText(text, permissionRules)` is the pure transformation function used per message part.
+- Permissions flow:
+  - `getAgentOverride(config, agentName)` from `cli/config` resolves override arrays.
+  - `getSkillPermissionsForAgent` from `cli/skills` resolves canonical rules (`allow`, `ask`, `deny` wildcard).
+  - `isSkillAllowed` checks exact skill rule first, then `'*'` wildcard fallback.
+- `<available_skills>...</available_skills>` and nested `<skill>...</skill>` blocks are matched with regex extraction.
+
+## Flow
+
+1. In transform output, determine `agentName` via `getCurrentAgent`.
+2. Load `permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills)`.
+3. For each `text` part containing `<available_skills>`, run regex replacement:
+   - parse `<skill>` entries,
+   - keep only allowed names,
+   - fallback to `<available_skills>\nNo skills available.\n</available_skills>` when none match.
+4. Write transformed `part.text` back to `output.messages` in place.
+
+## Integration
+
+- Hook is wired in `src/hooks/index.ts` and consumed by plugin hook registration.
+- Executed in the message path prior to model call, so users do not see changed prompt text in UI, but the model receives constrained capabilities.
+- Depends on `cli/skills` and `config` modules, and `PluginInput` only for registration compatibility.

+ 30 - 0
src/hooks/foreground-fallback/codemap.md

@@ -0,0 +1,30 @@
+# src/hooks/foreground-fallback/
+
+## Responsibility
+
+- Keep foreground (interactive) sessions alive when rate limiting is detected by switching the same session to the next model in a configured fallback chain and re-prompting asynchronously.
+
+## Design
+
+- `index.ts` exports `ForegroundFallbackManager` and `isRateLimitError`.
+- `ForegroundFallbackManager` tracks per-session state in maps:
+  - `sessionModel`, `sessionAgent`, `sessionTried`, `lastTrigger`, `inProgress`.
+- `isRateLimitError(error)` performs regex checks over `{ message, data.statusCode, data.message, data.responseBody }`.
+- `handleEvent` is the event dispatcher for `message.updated`, `session.error`, `session.status`, `subagent.session.created`, `session.deleted`.
+- `resolveChain(agentName, currentModel)` defines deterministic chain choice (agent-specific > inferred from current model > flattened fallback).
+- `tryFallback` performs dedupe (`DEDUP_WINDOW_MS`), aborts stale attempts, selects next untried model, fetches last user message, calls `session.abort()`, then `session.promptAsync()` with same user parts and new model.
+
+## Flow
+
+1. Every relevant event arrives at `handleEvent`.
+2. On suspected rate-limit signal, `handleEvent` calls `tryFallback(sessionID)`.
+3. `tryFallback` skips if in-progress, duplicate within 5s, or no candidate chain.
+4. It resolves the active chain, marks attempted models (`sessionTried`), parses `provider/model` with `parseModel`, and fetches last user message via `client.session.messages`.
+5. If a user message exists, it aborts current turn with `session.abort`, then re-issues using `promptAsync` with parsed model.
+6. Success updates `sessionModel`; `session.deleted` path clears all per-session maps.
+
+## Integration
+
+- Integrated through `src/index.ts` into global plugin event stream.
+- Consumes `PluginInput['client']` session APIs and reads fallback chain configuration from plugin startup composition.
+- Works in parallel to background fallback logic in task manager without altering core session scheduling.

+ 30 - 0
src/hooks/json-error-recovery/codemap.md

@@ -0,0 +1,30 @@
+# src/hooks/json-error-recovery/
+
+## Responsibility
+
+- Detect likely JSON syntax/parse failures in tool outputs and append a strong, non-redundant recovery prompt so the model replays corrected JSON on retry.
+
+## Design
+
+- `hook.ts` contains the implementation with exported constants:
+  - `JSON_ERROR_TOOL_EXCLUDE_LIST`
+  - `JSON_ERROR_PATTERNS`
+  - `JSON_ERROR_REMINDER`
+- `createJsonErrorRecoveryHook(_ctx)` returns a `tool.execute.after` handler that appends reminder text when parsing failed.
+- `JSON_ERROR_REMINDER_MARKER` prevents recursive duplicate injection.
+- Exclusion is by lowercase tool name (`bash`, `read`, `glob`, web tools) through a `Set`.
+- Matching uses regex literals in `JSON_ERROR_PATTERNS` and short-circuits for non-string output.
+- `index.ts` only re-exports hook/constant surface.
+
+## Flow
+
+1. In `tool.execute.after`, normalize `input.tool` to lowercase and skip excluded tools.
+2. Skip when `output.output` is not a string.
+3. Skip if output already contains `JSON_ERROR_REMINDER_MARKER`.
+4. Evaluate all `JSON_ERROR_PATTERNS`; on match, append `\n${JSON_ERROR_REMINDER}` to `output.output`.
+
+## Integration
+
+- Exported from `src/hooks/index.ts` and attached to tool output lifecycle at plugin registration.
+- Only consumes hook payload contracts (`ToolExecuteAfterInput`, `ToolExecuteAfterOutput`) and standard string checks, making it generic across tools.
+- No direct dependency on tool internals; integrates by observing tool-call results before they are surfaced to the model.

+ 17 - 6
src/hooks/phase-reminder/codemap.md

@@ -1,19 +1,30 @@
 # src/hooks/phase-reminder/
 
-<!-- Explorer: Fill in this section with architectural understanding -->
-
 ## Responsibility
 
-Keep the orchestrator agent’s working memory on track by injecting a terse phase reminder directly into the payload sent to the API. Because the reminder lives in `experimental.chat.messages.transform`, it doesn’t surface in the UI until the next response is generated, yet it keeps the delegate→plan→execute→verify workflow in scope for every user turn.
+Keep orchestrator guidance aligned over long turns by prepending a phase reminder to the latest user message text before the next LLM request.
 
 ## Design
 
-Exports a single factory (`createPhaseReminderHook`) that supplies an `experimental.chat.messages.transform` handler. The hook stores the reminder template in `PHASE_REMINDER`, scopes mutation to the orchestrator (or default session) only, and rewrites the first text part of the last user message by prefixing it with the reminder plus a divider. Encapsulating this in a synchronous factory keeps the hook pluggable and compatible with the global hook registry.
+- `PHASE_REMINDER` constant is composed from `PHASE_REMINDER_TEXT` (`config/constants.ts`).
+- `createPhaseReminderHook()` returns a single `experimental.chat.messages.transform` handler.
+- Message filtering is role/agent-aware:
+  - locates the latest `'user'` role in `output.messages`,
+  - only mutates if no explicit agent or `agent === 'orchestrator'`,
+  - no-op for internal control messages containing `SLIM_INTERNAL_INITIATOR_MARKER`.
+- Mutation target is the first `text` part in that message; replacement is an in-place prefix.
+- Uses `SLIM_INTERNAL_INITIATOR_MARKER` from `../../utils` to avoid feedback loops.
 
 ## Flow
 
-When the hook fires, it inspects the outgoing messages array, walks backward to locate the last `'user'` role entry, and short-circuits if none exists. If the user message belongs to another agent, it skips mutation. Otherwise it finds the first text part, prepends the reminder block (and a separator) to the existing text, and leaves the rest of the payload untouched. Since it modifies `output.messages` just before the API call, downstream components (like UI) never see the reminder; it only influences the assistant’s reasoning in the next turn.
+1. On transform, scan backward through `messages` for last `info.role === 'user'`.
+2. If agent is non-orchestrator, return.
+3. Locate first part where `type === 'text'`.
+4. If marker exists, return.
+5. Prefix `part.text` with `PHASE_REMINDER + '\n\n---\n\n'`.
 
 ## Integration
 
-Registered through the shared hook registry, this module hooks the `experimental.chat.messages.transform` lifecycle event that runs right before OpenAI invocation. It only touches the orchestrator session’s outgoing message list, so its effect is indirect: the reminder guides every assistant response that follows the user turn, but no other module needs to call it explicitly.
+- Registered through `src/hooks/index.ts` and plugin-level hook wiring in `src/index.ts`.
+- Consumes `experimental.chat.messages.transform` and mutates the outgoing `messages` payload only.
+- Does not depend on stateful services; no network or client APIs are required.

+ 17 - 7
src/hooks/post-file-tool-nudge/codemap.md

@@ -1,19 +1,29 @@
 # src/hooks/post-file-tool-nudge/
 
-<!-- Explorer: Fill in this section with architectural understanding -->
-
 ## Responsibility
 
-Provide a lightweight safety net that reminds the orchestrator to stay in the delegation workflow whenever it reads or writes project files. Read/Write tool results are treated as evidence only; the hook now queues a one-shot reminder for the next system prompt instead of modifying the persisted tool output.
+Detect recent file interaction (`Read`/`Write`) and queue a one-shot workflow reminder that is injected on the next system prompt transform without mutating tool execution output.
 
 ## Design
 
-Exports a single factory (`createPostFileToolNudgeHook`) that returns handlers for `tool.execute.after`, `experimental.chat.system.transform`, and session lifecycle events. The tool hook records pending session IDs for Read/Write tools, while the system transform consumes each pending ID once and appends the existing workflow reminder only when the caller allows injection for that session. The event handler clears stale pending IDs when sessions are deleted. This preserves the delegation behavior without contaminating persisted tool output.
+- Factory `createPostFileToolNudgeHook(options?)` emits three handlers:
+  - `tool.execute.after`
+  - `experimental.chat.system.transform`
+  - `event`
+- A per-instance in-memory `pendingSessionIds: Set<string>` tracks sessions that recently ran file tools.
+- `FILE_TOOLS` is the canonical set `{ 'Read', 'read', 'Write', 'write' }`.
+- Injection is optional per session via `options.shouldInject?: (sessionID) => boolean`.
+- Cleanup path handles both `session.deleted` payload shapes (`properties.sessionID` and `properties.info.id`).
 
 ## Flow
-
-The hook is instantiated once and registered with the hook system. When a Read or Write tool completes, `tool.execute.after` verifies the tool name and session ID, then stores the session ID in an in-memory pending set. On the next `experimental.chat.system.transform` call for that session, the hook deletes the pending marker and appends the workflow reminder to the outgoing system prompt. Multiple Read/Write calls before the next model turn collapse into one reminder, non-orchestrator sessions can be consumed without injection, and deleted sessions are removed from the pending set.
+1. `tool.execute.after`: if tool is file tool and has `sessionID`, add it to `pendingSessionIds`.
+2. `experimental.chat.system.transform`: if session has pending marker, remove it and append `POST_FILE_TOOL_NUDGE` (`PHASE_REMINDER_TEXT`) to `output.system`.
+3. Optional `shouldInject` gate can consume without injecting.
+4. Additional `Read`/`Write` events before the same transform collapse to one reminder due to set semantics.
+5. `session.deleted` event removes stale session IDs from the set.
 
 ## Integration
 
-Plugged into the global hook registry, this module intercepts every tool response via the `tool.execute.after` lifecycle event, participates in system prompt transformation via `experimental.chat.system.transform`, and listens to session deletion events for cleanup. It intentionally does not mutate `output.output`, so file contents remain clean for persistence, replay, and compaction while the orchestrator still receives the intended delegation reminder.
+- Registered via `src/hooks/index.ts` and activated in plugin lifecycle registration.
+- Mutates `output.system` only, ensuring persisted file tool outputs remain untouched.
+- Consumed by orchestrator session flows that need anti-pattern mitigation (`inspect/edit` loops).

+ 58 - 0
src/hooks/todo-continuation/codemap.md

@@ -0,0 +1,58 @@
+# src/hooks/todo-continuation/
+
+## Responsibility
+
+Automatically continue orchestrator work when open todos remain, while enforcing
+multiple safeguards (question suppression, cooldown, explicit abort handling) to
+avoid runaway automation.
+
+## Design
+
+- `createTodoContinuationHook(ctx, config?)` in `index.ts` is the public
+  factory and returns the complete hook contract: command, tool, message,
+  system-transform, and event handlers.
+- Internal state is tracked by `ContinuationState` (per-session timers, session
+  tracking, suppression windows, request signatures, and auto-injection flags).
+- `todo-hygiene.ts` is extracted as a dedicated reminder state machine with
+  reasons: `general`, `delegation_resume`, `final_active` and exposes:
+  `createTodoHygiene`, `handleRequestStart`, `handleToolExecuteAfter`,
+  `handleChatSystemTransform`, and `handleEvent`.
+- Message analysis is implemented in `handleMessagesTransform` with helpers like
+  `getLastExternalUserMessage`, `isExternalUserMessage`, and `inferSessionID`,
+  and ignores internal markers via `SLIM_INTERNAL_INITIATOR_MARKER`.
+- Continuation prompt string uses `CONTINUATION_PROMPT`; reminder strings come
+  from `TODO_HYGIENE_REMINDER`, `TODO_DELEGATION_RESUME_REMINDER`,
+  `TODO_FINAL_ACTIVE_REMINDER`.
+
+## Flow
+
+### Auto-continuation
+
+1. `handleMessagesTransform` identifies the latest external user message and
+   request boundary signature, then calls `hygiene.handleRequestStart`.
+2. `handleCommandExecuteBefore` processes `command === 'auto_continue'` and
+   toggles auto-continuation with `enabled` on/off.
+3. `handleToolExecuteAfter` records request context and may queue a continuation
+   prompt using `ctx.client.session.prompt`.
+4. `handleChatSystemTransform` injects only one continuation reminder per request
+   round when guards pass.
+5. `handleEvent` and `handleChatMessage` update session registries and clear stale
+   state on `session.deleted` / new boundaries.
+
+### Todo hygiene
+
+1. `createTodoHygiene.handleToolExecuteAfter` observes post-tool activity to
+   arm reminders based on todo state transitions.
+2. `createTodoHygiene.handleChatSystemTransform` injects one reminder into
+   `output.system` if open todos remain and session is still injectable.
+3. Reminder priorities resolve deterministically, with `final_active` overriding
+   other reasons.
+4. Session deletion clears pending states through `clear()`/`clearCycle()`.
+
+## Integration
+
+- Imported from `src/hooks/index.ts` and wired in `src/index.ts`.
+- Relies on OpenCode APIs on `ctx.client.session` (`todo`, `messages`, `prompt`).
+- Uses utility helpers from `../../utils` (`log`, `createInternalAgentTextPart`,
+  `SLIM_INTERNAL_INITIATOR_MARKER`) and config via hook options
+  (`maxContinuations`, `cooldownMs`, `autoEnable`, `autoEnableThreshold`).

+ 24 - 20
src/index.ts

@@ -539,6 +539,23 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
     },
 
     event: async (input) => {
+      const event = input.event as {
+        type: string;
+        properties?: {
+          info?: { id?: string; parentID?: string; title?: string };
+          sessionID?: string;
+          status?: { type: string };
+        };
+      };
+
+      if (event.type === 'session.created') {
+        const childSessionId = event.properties?.info?.id;
+        const parentSessionId = event.properties?.info?.parentID;
+        if (depthTracker && childSessionId && parentSessionId) {
+          depthTracker.registerChild(parentSessionId, childSessionId);
+        }
+      }
+
       // Runtime model fallback for foreground agents (rate-limit detection)
       await foregroundFallback.handleEvent(input.event);
 
@@ -549,30 +566,13 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       await autoUpdateChecker.event(input);
 
       // Handle multiplexer pane spawning for OpenCode's Task tool sessions
-      await multiplexerSessionManager.onSessionCreated(
-        input.event as {
-          type: string;
-          properties?: {
-            info?: { id?: string; parentID?: string; title?: string };
-          };
-        },
-      );
+      await multiplexerSessionManager.onSessionCreated(event);
 
       // Handle session.status events for pane cleanup
-      await multiplexerSessionManager.onSessionStatus(
-        input.event as {
-          type: string;
-          properties?: { sessionID?: string; status?: { type: string } };
-        },
-      );
+      await multiplexerSessionManager.onSessionStatus(event);
 
       // Handle session.deleted events for pane cleanup
-      await multiplexerSessionManager.onSessionDeleted(
-        input.event as {
-          type: string;
-          properties?: { sessionID?: string };
-        },
-      );
+      await multiplexerSessionManager.onSessionDeleted(event);
 
       await interviewManager.handleEvent(
         input as {
@@ -597,6 +597,10 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
           | { info?: { id?: string }; sessionID?: string }
           | undefined;
         const sessionID = props?.info?.id ?? props?.sessionID;
+
+        if (depthTracker && sessionID) {
+          depthTracker.cleanup(sessionID);
+        }
         if (sessionID) {
           sessionAgentMap.delete(sessionID);
         }

+ 64 - 0
src/interview/codemap.md

@@ -0,0 +1,64 @@
+# src/interview/
+
+## Responsibility
+
+- Implement the `/interview` command feature end-to-end: parsing user invocation, building assistant prompts, tracking interview state, persisting markdown artifacts, and serving a local web UI.
+- Coordinate two execution modes: per-session self-hosted interview server (default) and shared dashboard mode for multi-process concurrency.
+- Keep interview lifecycle state synchronized between local markdown files, in-memory records, and dashboard caches for resumable/spec-recovery flows.
+
+## Design
+
+- `manager.ts` is the interview composition root and exposes a narrow adapter contract:
+  - `registerCommand`
+  - `handleCommandExecuteBefore`
+  - `handleEvent`
+- `manager.ts` decides runtime mode and wires a `createInterviewService(ctx, interviewConfig)` instance with one of:
+  - `createInterviewServer` (private per-process server), or
+  - dashboard orchestration via `tryBecomeDashboard`, `probeDashboard`, `readDashboardAuthFile`, and periodic dashboard fallback polling.
+- `service.ts` owns interview domain state and business rules:
+  - `interviewsById`, `activeInterviewIds`, `sessionBusy`
+  - `handleCommandExecuteBefore` for `/interview` handling
+  - `submitAnswers` and `handleNudgeAction` to inject internal prompts via `session.promptAsync`
+  - `getInterviewState`, `listInterviewFiles`, and `listInterviews`.
+- `document.ts` handles filesystem layout and transformation primitives: `createInterviewFilePath`, `resolveExistingInterviewPath`, `ensureInterviewFile`, `rewriteInterviewDocument`, `appendInterviewAnswers`, `readInterviewDocument`.
+- `parser.ts` validates structured agent output from `<interview_state>` blocks using `parseAssistantState` and `findLatestAssistantState` with `zod` schemas from `types.ts`.
+- `server.ts` exposes HTTP endpoints for dashboard and interview UI with typed handlers in `helpers.ts`.
+- `ui.ts` renders HTML views (`renderDashboardPage`, `renderInterviewPage`) for `/` and `/interview/{id}`.
+- `dashboard.ts` implements shared interview registry/auth and recovery mechanics (`createDashboardServer`, token auth, file rescan/rebuild, pending answers/nudges).
+- `prompts.ts` provides structured prompts for kickoff/resume/answer flows.
+- `types.ts` defines transport and domain contracts (`InterviewRecord`, `InterviewState`, `InterviewStateEntry`, schemas `RawInterviewStateSchema` / `RawQuestionSchema`).
+
+## Flow
+
+- `src/index.ts` initializes interview support via `createInterviewManager(ctx, config)`.
+- `manager.ts` computes effective port and dashboard flag (`interview.dashboard` / `interview.port > 0`) and selects mode:
+  - **Per-session**: instantiate `createInterviewService` + `createInterviewServer` on port `0`.
+  - **Dashboard**: attempt leadership with `tryBecomeDashboard`.
+- In dashboard leader mode:
+  - push state changes through local callback `setStatePushCallback`
+  - register created interviews and sessions into dashboard cache
+  - expose auth + scan discovery + file rehydration.
+- In dashboard client mode:
+  - resolve dashboard base URL and token
+  - register current process as session with `/api/register`
+  - deliver state via `pushStateViaHttp` / `/api/interviews/{id}/state`
+  - periodically poll `/api/interviews/{id}/pending` and `/api/interviews/{id}/nudge`.
+- `service.handleCommandExecuteBefore`:
+  - if no idea and no active interview -> ask for one
+  - if idea matches file slug -> `resumeInterview`
+  - else -> `createInterview` and inject kickoff/resume prompts.
+- `service.syncInterview` loads session messages, extracts latest valid assistant state, rewrites interview markdown, computes `InterviewState.mode`, and invokes state callbacks.
+- `handleEvent` listens for `session.status`/`session.deleted` to update busy flags, consume pending UI actions, refresh state, and mark interviews abandoned on session delete.
+- `server.ts` route flow:
+  - `/` and `/api/interviews` render list state
+  - `/interview/:id` renders interview UI
+  - `/api/interviews/:id/state` returns JSON state
+  - POST `/api/interviews/:id/answers` validates payload and forwards to `submitAnswers`
+  - POST `/api/interviews/:id/nudge` forwards action to `handleNudgeAction`.
+
+## Integration
+
+- Consumed by plugin bootstrap in `src/index.ts`; command pre-hook and event dispatch invoke the manager methods returned by `createInterviewManager`.
+- Integrated with OpenCode session API via `PluginInput.client.session` for message reads/prompt injection.
+- Exposed to users through local HTTP UI endpoints from whichever active server instance is running.
+- Used by tests in `interview/*.test.ts` for command mode transitions, server behavior, parsing, and command registration.

+ 0 - 2
src/mcp/codemap.md

@@ -1,7 +1,5 @@
 # src/mcp/
 
-<!-- Explorer: Fill in this section with architectural understanding -->
-
 ## Responsibility
 
 - Define and expose the built-in MCP endpoints (websearch, context7, grep.app) alongside the shared type aliases so the application can treat remote and local MCPs uniformly (`src/mcp/index.ts`, `src/mcp/types.ts`).

+ 34 - 0
src/multiplexer/codemap.md

@@ -0,0 +1,34 @@
+# src/multiplexer/
+
+## Responsibility
+
+- Abstract terminal multiplexer integration behind a unified interface for background session visualization.
+- Select the correct implementation based on configuration (`tmux`, `zellij`, `auto`, `none`) and runtime environment.
+- Expose shared utilities required by task orchestration and health-gating logic.
+
+## Design
+
+- `types.ts` is the boundary contract:
+  - `Multiplexer` (`spawnPane`, `closePane`, `applyLayout`, `isAvailable`, `isInsideSession`).
+  - `PaneResult` and `MultiplexerFactory`.
+  - `isServerRunning(serverUrl, timeoutMs, maxAttempts)` for shared health checks.
+- `factory.ts` implements mode selection and instance creation in `getMultiplexer`:
+  - direct construction for explicit `tmux`/`zellij`
+  - environment-based fallback in `auto` (`TMUX` vs `ZELLIJ`, else disabled)
+  - no caching: each call creates a fresh object to capture live environment (`TMUX_PANE` / `ZELLIJ`).
+- `index.ts` re-exports factories and contracts and both concrete implementations.
+- `startAvailabilityCheck` is a fire-and-forget availability preflight by calling `multiplexer.isAvailable()` asynchronously.
+- `getAutoMultiplexerType` is a pure helper used by tests/diagnostics to determine current effective backend.
+
+## Flow
+
+- `src/index.ts` computes `multiplexerConfig`, creates a one-shot probe instance in init, and starts `startAvailabilityCheck` for telemetry/log warming.
+- Consumers request concrete objects via `getMultiplexer(config)` and must handle `null` when disabled.
+- `MultiplexerSessionManager` consumes `spawnPane`, `closePane`, and `isServerRunning` in a unified lifecycle.
+- Concrete implementations apply their own platform-specific pane semantics while sharing the same abstractions.
+
+## Integration
+
+- Used by `background/background-manager.ts` for feature-gating background pane support and by `background/multiplexer-session-manager.ts` for session lifecycle hooks.
+- Implementations live in `src/multiplexer/tmux` and `src/multiplexer/zellij`; callers must pass `(sessionId, description, serverUrl, directory)`.
+- Unit tests in `src/multiplexer/factory.test.ts` validate mode selection, `none` behavior, and `auto` environment precedence.

+ 40 - 0
src/multiplexer/tmux/codemap.md

@@ -0,0 +1,40 @@
+# src/multiplexer/tmux/
+
+## Responsibility
+
+- Provide tmux-specific pane orchestration for attaching OpenCode child sessions to a split pane beside the current pane.
+- Handle lifecycle of spawned panes (create, rename, layout rebalancing, graceful close).
+- Resolve and cache tmux executable location for repeated operations.
+
+## Design
+
+- `TmuxMultiplexer` in `index.ts` implements `Multiplexer`.
+- `findBinary` uses platform command (`which` or `where`) and validates the binary via `-V`.
+- `isAvailable` caches `binaryPath` and `hasChecked` to avoid repeated lookups.
+- `targetPane` captures `process.env.TMUX_PANE` and is reused as `targetArgs()` for scoped tmux actions.
+- Command execution is performed with `crossSpawn` to support both Bun and Node process interfaces.
+- `quoteShellArg` provides shell-safe quoting used for directory/URL/session injection in `opencode` commands.
+
+## Flow
+
+- `spawnPane(sessionId, description, serverUrl, directory)`:
+  - ensure binary through `getBinary()`
+  - build command: `opencode attach <url> --session <sessionId> --dir <directory>`
+  - execute `tmux split-window -h -d -P -F '#{pane_id}' ...` with optional `-t <TMUX_PANE>`
+  - on success:
+    - rename pane with `select-pane -T` using first 30 chars of `description`
+    - call `applyLayout(storedLayout, storedMainPaneSize)`.
+- `applyLayout(layout, mainPaneSize)`:
+  - `select-layout` on current target
+  - for `main-*` layouts, update `main-pane-height|width` and re-select layout for deterministic size.
+- `closePane(paneId)`:
+  - `send-keys -t <pane> C-c`
+  - wait 250ms
+  - `kill-pane -t <pane>`
+  - on success, re-run `applyLayout` to rebalance panes.
+
+## Integration
+
+- Selected when `multiplexerConfig.type === 'tmux'` or auto mode resolves to tmux (`process.env.TMUX`).
+- Consumed by `MultiplexerSessionManager` in `background/multiplexer-session-manager.ts` for `session.created` spawn and completion cleanup.
+- Uses `ctx.directory` as working directory, OpenCode API URL as `serverUrl`, and session id as `opencode attach --session` target.

+ 44 - 0
src/multiplexer/zellij/codemap.md

@@ -0,0 +1,44 @@
+# src/multiplexer/zellij/
+
+## Responsibility
+
+- Implement zellij-backed pane orchestration for background/sub-agent tasks as an alternative to tmux.
+- Maintain a dedicated `opencode-agents` tab and route all spawned attach sessions into it.
+- Keep process cleanup and first-run reuse behavior to avoid repeated pane inflation.
+
+## Design
+
+- `ZellijMultiplexer` in `index.ts` implements `Multiplexer`.
+- `findBinary` is a simple `which/where zellij` probe with cached path.
+- `isInsideSession` checks `process.env.ZELLIJ`; `isAvailable` uses cached `binaryPath`.
+- First creation path builds/repurposes one dedicated tab (`opencode-agents`) via `ensureAgentTab` and tracks:
+  - `agentTabId`
+  - `firstPaneId`
+  - `firstPaneUsed`
+- Command composition is done by helper builders:
+  - `buildOpencodeAttachCommand`
+  - `buildShellLaunchCommand`
+- Layout is intentionally a no-op because zellij does not expose equivalent layout APIs used by this codebase.
+
+## Flow
+
+- `spawnPane(sessionId, description, serverUrl, directory)`:
+  - resolve zellij binary and call `ensureAgentTab`
+  - if first pane in the agent tab is free, execute attach command in-place via `runInPane`:
+    - `focus-pane --pane-id`
+    - `rename-pane`
+    - `write-chars` launch command + newline
+  - otherwise create a new pane via `new-pane --name <desc> --close-on-exit -- sh -lc <opencode attach ...>`.
+  - when called from user tab, temporarily switch to `agentTabId` and back to keep user context.
+  - return `{ success, paneId }` where pane ids are validated as `terminal_*`.
+- `closePane(paneId)`:
+  - `action write --pane-id <id> \u0003` (graceful SIGINT equivalent)
+  - wait 250ms
+  - `action close-pane --pane-id <id>`; treats exit codes `0` and `1` as successful closure.
+- `applyLayout` is intentionally no-op and retained for interface compatibility.
+
+## Integration
+
+- Selected by `getMultiplexer` in explicit `zellij` mode or env-driven `auto` when `process.env.ZELLIJ` is present.
+- Consumed by `MultiplexerSessionManager` as the pane backend in zellij environments.
+- UI attach command semantics are identical to tmux in argument shape: `opencode attach <url> --session <sessionId> --dir <directory>`, so background tasks remain config-agnostic across backends.

+ 0 - 456
src/skills/cartography/scripts/cartographer.py

@@ -1,456 +0,0 @@
-#!/usr/bin/env python3
-"""
-Cartographer - Repository mapping and change detection tool.
-
-Commands:
-  init     Initialize mapping (create hashes + empty codemaps)
-  changes  Show what changed (read-only, like git status)
-  update   Update hashes (like git commit)
-
-Usage:
-  cartographer.py init --root /path/to/repo --include "src/**/*.ts" --exclude "node_modules/**"
-  cartographer.py changes --root /path/to/repo
-  cartographer.py update --root /path/to/repo
-"""
-
-import argparse
-import hashlib
-import json
-import os
-import re
-import sys
-from datetime import datetime, timezone
-from pathlib import Path, PurePath
-from typing import Dict, List, Optional, Set, Tuple
-
-VERSION = "1.0.0"
-STATE_DIR = ".slim"
-STATE_FILE = "cartography.json"
-CODEMAP_FILE = "codemap.md"
-
-
-def load_gitignore(root: Path) -> List[str]:
-    """Load .gitignore patterns from the repository root."""
-    gitignore_path = root / ".gitignore"
-    patterns = []
-    if gitignore_path.exists():
-        with open(gitignore_path, "r", encoding="utf-8") as f:
-            for line in f:
-                line = line.strip()
-                if line and not line.startswith("#"):
-                    patterns.append(line)
-    return patterns
-
-
-class PatternMatcher:
-    """Efficiently match paths against multiple glob patterns using pre-compiled regex."""
-
-    def __init__(self, patterns: List[str]):
-        if not patterns:
-            self.regex = None
-            return
-
-        regex_parts = []
-        for pattern in patterns:
-            # Regex conversion logic
-            reg = re.escape(pattern)
-            reg = reg.replace(r'\*\*/', '(?:.*/)?')  # Recursive glob
-            reg = reg.replace(r'\*\*', '.*')
-            reg = reg.replace(r'\*', '[^/]*')  # Single level glob
-            reg = reg.replace(r'\?', '.')
-
-            if pattern.endswith('/'):
-                reg += '.*'
-
-            if pattern.startswith('/'):
-                reg = '^' + reg[1:]
-            else:
-                reg = '(?:^|.*/)' + reg
-            
-            regex_parts.append(f'(?:{reg}$)')
-        
-        # Combine all patterns into a single regex for speed
-        combined_regex = '|'.join(regex_parts)
-        self.regex = re.compile(combined_regex)
-
-    def matches(self, path: str) -> bool:
-        """Check if a path matches any of the patterns."""
-        if not self.regex:
-            return False
-        return bool(self.regex.search(path))
-
-
-def select_files(
-    root: Path,
-    include_patterns: List[str],
-    exclude_patterns: List[str],
-    exceptions: List[str],
-    gitignore_patterns: List[str],
-) -> List[Path]:
-    """Select files based on include/exclude patterns and exceptions."""
-    selected = []
-    
-    # Pre-compile matchers
-    include_matcher = PatternMatcher(include_patterns)
-    exclude_matcher = PatternMatcher(exclude_patterns)
-    gitignore_matcher = PatternMatcher(gitignore_patterns)
-    exception_set = set(exceptions)
-    
-    root_str = str(root)
-    
-    for dirpath, dirnames, filenames in os.walk(root_str):
-        # Skip hidden directories early by modifying dirnames in-place
-        dirnames[:] = [d for d in dirnames if not d.startswith(".")]
-        
-        rel_dir = os.path.relpath(dirpath, root_str)
-        if rel_dir == ".":
-            rel_dir = ""
-        
-        for filename in filenames:
-            rel_path = os.path.join(rel_dir, filename).replace("\\", "/")
-            if rel_path.startswith("./"):
-                rel_path = rel_path[2:]
-            
-            # Skip if ignored by .gitignore
-            if gitignore_matcher.matches(rel_path):
-                continue
-            
-            # Check explicit exclusions first
-            if exclude_matcher.matches(rel_path):
-                # Unless it's an exception
-                if rel_path not in exception_set:
-                    continue
-            
-            # Check inclusions
-            if include_matcher.matches(rel_path) or rel_path in exception_set:
-                selected.append(root / rel_path)
-    
-    return sorted(selected)
-
-
-def compute_file_hash(filepath: Path) -> str:
-    """Compute MD5 hash of file content."""
-    hasher = hashlib.md5()
-    try:
-        with open(filepath, "rb") as f:
-            for chunk in iter(lambda: f.read(8192), b""):
-                hasher.update(chunk)
-        return hasher.hexdigest()
-    except (IOError, OSError):
-        return ""
-
-
-def compute_folder_hash(folder: str, file_hashes: Dict[str, str]) -> str:
-    """Compute a stable hash for a folder based on its files."""
-    # Get all files in this folder
-    folder_files = sorted(
-        (path, hash_val)
-        for path, hash_val in file_hashes.items()
-        if path.startswith(folder + "/") or (folder == "." and "/" not in path)
-    )
-    
-    if not folder_files:
-        return ""
-    
-    # Hash the concatenation of path:hash pairs
-    hasher = hashlib.md5()
-    for path, hash_val in folder_files:
-        hasher.update(f"{path}:{hash_val}\n".encode())
-    return hasher.hexdigest()
-
-
-def get_folders_with_files(files: List[Path], root: Path) -> Set[str]:
-    """Get all unique folders that contain selected files."""
-    folders = set()
-    for f in files:
-        rel = f.relative_to(root)
-        # Add all parent directories
-        parts = rel.parts[:-1]  # Exclude filename
-        for i in range(len(parts)):
-            folders.add("/".join(parts[: i + 1]))
-    folders.add(".")  # Always include root
-    return folders
-
-
-def load_state(root: Path) -> Optional[dict]:
-    """Load the current cartography state."""
-    state_path = root / STATE_DIR / STATE_FILE
-    if state_path.exists():
-        try:
-            with open(state_path, "r", encoding="utf-8") as f:
-                return json.load(f)
-        except (json.JSONDecodeError, IOError):
-            return None
-    return None
-
-
-def save_state(root: Path, state: dict) -> None:
-    """Save the cartography state."""
-    state_dir = root / STATE_DIR
-    state_dir.mkdir(parents=True, exist_ok=True)
-    
-    state_path = state_dir / STATE_FILE
-    with open(state_path, "w", encoding="utf-8") as f:
-        json.dump(state, f, indent=2)
-
-
-def create_empty_codemap(folder_path: Path, folder_name: str) -> None:
-    """Create an empty codemap.md file with a header."""
-    codemap_path = folder_path / CODEMAP_FILE
-    if not codemap_path.exists():
-        content = f"""# {folder_name}/
-
-<!-- Explorer: Fill in this section with architectural understanding -->
-
-## Responsibility
-
-<!-- What is this folder's job in the system? -->
-
-## Design
-
-<!-- Key patterns, abstractions, architectural decisions -->
-
-## Flow
-
-<!-- How does data/control flow through this module? -->
-
-## Integration
-
-<!-- How does it connect to other parts of the system? -->
-"""
-        with open(codemap_path, "w", encoding="utf-8") as f:
-            f.write(content)
-
-
-def cmd_init(args: argparse.Namespace) -> int:
-    """Initialize mapping: create hashes and empty codemaps."""
-    root = Path(args.root).resolve()
-    
-    if not root.is_dir():
-        print(f"Error: {root} is not a directory", file=sys.stderr)
-        return 1
-    
-    # Load patterns
-    gitignore = load_gitignore(root)
-    include_patterns = args.include or ["**/*"]
-    exclude_patterns = args.exclude or []
-    exceptions = args.exception or []
-    
-    print(f"Scanning {root}...")
-    print(f"Include patterns: {include_patterns}")
-    print(f"Exclude patterns: {exclude_patterns}")
-    print(f"Exceptions: {exceptions}")
-    
-    # Select files
-    selected_files = select_files(
-        root, include_patterns, exclude_patterns, exceptions, gitignore
-    )
-    
-    print(f"Selected {len(selected_files)} files")
-    
-    # Compute file hashes
-    file_hashes: Dict[str, str] = {}
-    for f in selected_files:
-        rel_path = str(f.relative_to(root))
-        file_hashes[rel_path] = compute_file_hash(f)
-    
-    # Get folders and compute folder hashes
-    folders = get_folders_with_files(selected_files, root)
-    folder_hashes: Dict[str, str] = {}
-    for folder in folders:
-        folder_hashes[folder] = compute_folder_hash(folder, file_hashes)
-    
-    # Create state
-    state = {
-        "metadata": {
-            "version": VERSION,
-            "last_run": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
-            "root": str(root),
-            "include_patterns": include_patterns,
-            "exclude_patterns": exclude_patterns,
-            "exceptions": exceptions,
-        },
-        "file_hashes": file_hashes,
-        "folder_hashes": folder_hashes,
-    }
-    
-    # Save state
-    save_state(root, state)
-    print(f"Created {STATE_DIR}/{STATE_FILE}")
-    
-    # Create empty codemaps
-    for folder in folders:
-        if folder == ".":
-            folder_path = root
-            folder_name = root.name
-        else:
-            folder_path = root / folder
-            folder_name = folder
-        
-        create_empty_codemap(folder_path, folder_name)
-    
-    print(f"Created {len(folders)} empty codemap.md files")
-    
-    return 0
-
-
-def cmd_changes(args: argparse.Namespace) -> int:
-    """Show what changed since last update."""
-    root = Path(args.root).resolve()
-    
-    state = load_state(root)
-    if not state:
-        print("No cartography state found. Run 'init' first.", file=sys.stderr)
-        return 1
-    
-    # Get patterns from saved state
-    metadata = state.get("metadata", {})
-    include_patterns = metadata.get("include_patterns", ["**/*"])
-    exclude_patterns = metadata.get("exclude_patterns", [])
-    exceptions = metadata.get("exceptions", [])
-    
-    gitignore = load_gitignore(root)
-    
-    # Select current files
-    current_files = select_files(
-        root, include_patterns, exclude_patterns, exceptions, gitignore
-    )
-    
-    # Compute current hashes
-    current_hashes: Dict[str, str] = {}
-    for f in current_files:
-        rel_path = str(f.relative_to(root))
-        current_hashes[rel_path] = compute_file_hash(f)
-    
-    saved_hashes = state.get("file_hashes", {})
-    
-    # Find changes
-    added = set(current_hashes.keys()) - set(saved_hashes.keys())
-    removed = set(saved_hashes.keys()) - set(current_hashes.keys())
-    modified = {
-        path
-        for path in current_hashes.keys() & saved_hashes.keys()
-        if current_hashes[path] != saved_hashes[path]
-    }
-    
-    if not added and not removed and not modified:
-        print("No changes detected.")
-        return 0
-    
-    if added:
-        print(f"\n{len(added)} added:")
-        for path in sorted(added):
-            print(f"  + {path}")
-    
-    if removed:
-        print(f"\n{len(removed)} removed:")
-        for path in sorted(removed):
-            print(f"  - {path}")
-    
-    if modified:
-        print(f"\n{len(modified)} modified:")
-        for path in sorted(modified):
-            print(f"  ~ {path}")
-    
-    # Show affected folders
-    affected_folders = set()
-    for path in added | removed | modified:
-        parts = Path(path).parts[:-1]
-        for i in range(len(parts)):
-            affected_folders.add("/".join(parts[: i + 1]))
-        affected_folders.add(".")
-    
-    print(f"\n{len(affected_folders)} folders affected:")
-    for folder in sorted(affected_folders):
-        print(f"  {folder}/")
-    
-    return 0
-
-
-def cmd_update(args: argparse.Namespace) -> int:
-    """Update hashes and save state."""
-    root = Path(args.root).resolve()
-    
-    state = load_state(root)
-    if not state:
-        print("No cartography state found. Run 'init' first.", file=sys.stderr)
-        return 1
-    
-    # Get patterns from saved state
-    metadata = state.get("metadata", {})
-    include_patterns = metadata.get("include_patterns", ["**/*"])
-    exclude_patterns = metadata.get("exclude_patterns", [])
-    exceptions = metadata.get("exceptions", [])
-    
-    gitignore = load_gitignore(root)
-    
-    # Select current files
-    selected_files = select_files(
-        root, include_patterns, exclude_patterns, exceptions, gitignore
-    )
-    
-    # Compute new hashes
-    file_hashes: Dict[str, str] = {}
-    for f in selected_files:
-        rel_path = str(f.relative_to(root))
-        file_hashes[rel_path] = compute_file_hash(f)
-    
-    # Compute folder hashes
-    folders = get_folders_with_files(selected_files, root)
-    folder_hashes: Dict[str, str] = {}
-    for folder in folders:
-        folder_hashes[folder] = compute_folder_hash(folder, file_hashes)
-    
-    # Update state
-    state["metadata"]["last_run"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
-    state["file_hashes"] = file_hashes
-    state["folder_hashes"] = folder_hashes
-    
-    save_state(root, state)
-    print(f"Updated {STATE_DIR}/{STATE_FILE} with {len(file_hashes)} files")
-    
-    return 0
-
-
-def main() -> int:
-    parser = argparse.ArgumentParser(
-        description="Cartographer - Repository mapping and change detection"
-    )
-    subparsers = parser.add_subparsers(dest="command", help="Available commands")
-    
-    # Init command
-    init_parser = subparsers.add_parser("init", help="Initialize mapping")
-    init_parser.add_argument("--root", required=True, help="Repository root path")
-    init_parser.add_argument(
-        "--include", action="append", help="Glob patterns for files to include"
-    )
-    init_parser.add_argument(
-        "--exclude", action="append", help="Glob patterns for files to exclude"
-    )
-    init_parser.add_argument(
-        "--exception", action="append", help="Explicit file paths to include despite exclusions"
-    )
-    
-    # Changes command
-    changes_parser = subparsers.add_parser("changes", help="Show what changed")
-    changes_parser.add_argument("--root", required=True, help="Repository root path")
-    
-    # Update command
-    update_parser = subparsers.add_parser("update", help="Update hashes")
-    update_parser.add_argument("--root", required=True, help="Repository root path")
-    
-    args = parser.parse_args()
-    
-    if args.command == "init":
-        return cmd_init(args)
-    elif args.command == "changes":
-        return cmd_changes(args)
-    elif args.command == "update":
-        return cmd_update(args)
-    else:
-        parser.print_help()
-        return 1
-
-
-if __name__ == "__main__":
-    sys.exit(main())

+ 0 - 87
src/skills/cartography/scripts/test_cartographer.py

@@ -1,87 +0,0 @@
-import unittest
-import os
-import shutil
-import json
-import tempfile
-import hashlib
-from pathlib import Path
-from cartographer import PatternMatcher, compute_file_hash, compute_folder_hash, select_files
-
-class TestCartographer(unittest.TestCase):
-    def test_pattern_matcher(self):
-        patterns = ["node_modules/", "dist/", "*.log", "src/**/*.ts"]
-        matcher = PatternMatcher(patterns)
-        
-        # Directory patterns
-        self.assertTrue(matcher.matches("node_modules/foo.js"))
-        self.assertTrue(matcher.matches("vendor/node_modules/bar.js"))
-        self.assertTrue(matcher.matches("dist/main.js"))
-        self.assertTrue(matcher.matches("src/dist/output.js"))
-        
-        # Glob patterns
-        self.assertTrue(matcher.matches("error.log"))
-        self.assertTrue(matcher.matches("logs/access.log"))
-        
-        # Recursive glob patterns
-        self.assertTrue(matcher.matches("src/index.ts"))
-        self.assertTrue(matcher.matches("src/utils/helper.ts"))
-        
-        # Non-matches
-        self.assertFalse(matcher.matches("README.md"))
-        self.assertFalse(matcher.matches("tests/test.py"))
-
-    def test_compute_file_hash(self):
-        # Use binary mode to avoid any newline translation issues
-        with tempfile.NamedTemporaryFile(mode='wb', delete=False) as f:
-            f.write(b"test content")
-            f_path = f.name
-        
-        try:
-            h1 = compute_file_hash(Path(f_path))
-            # md5 of b"test content" is 9473fdd0d880a43c21b7778d34872157
-            expected = hashlib.md5(b"test content").hexdigest()
-            self.assertEqual(h1, expected)
-            self.assertEqual(h1, "9473fdd0d880a43c21b7778d34872157")
-        finally:
-            if os.path.exists(f_path):
-                os.unlink(f_path)
-
-    def test_compute_folder_hash(self):
-        file_hashes = {
-            "src/a.ts": "hash-a",
-            "src/b.ts": "hash-b",
-            "tests/test.ts": "hash-test"
-        }
-        
-        h1 = compute_folder_hash("src", file_hashes)
-        h2 = compute_folder_hash("src", file_hashes)
-        self.assertEqual(h1, h2)
-        
-        file_hashes_alt = {
-            "src/a.ts": "hash-a-modified",
-            "src/b.ts": "hash-b"
-        }
-        h3 = compute_folder_hash("src", file_hashes_alt)
-        self.assertNotEqual(h1, h3)
-
-    def test_select_files(self):
-        with tempfile.TemporaryDirectory() as tmpdir:
-            root = Path(tmpdir)
-            (root / "src").mkdir()
-            (root / "node_modules").mkdir()
-            (root / "src" / "index.ts").write_text("code")
-            (root / "src" / "index.test.ts").write_text("test")
-            (root / "node_modules" / "foo.js").write_text("dep")
-            (root / "package.json").write_text("{}")
-            
-            includes = ["src/**/*.ts", "package.json"]
-            excludes = ["**/*.test.ts", "node_modules/"]
-            exceptions = []
-            
-            selected = select_files(root, includes, excludes, exceptions, [])
-            
-            rel_selected = sorted([os.path.relpath(f, root) for f in selected])
-            self.assertEqual(rel_selected, ["package.json", "src/index.ts"])
-
-if __name__ == "__main__":
-    unittest.main()

+ 40 - 0
src/skills/codemap.md

@@ -0,0 +1,40 @@
+# src/skills/
+
+## Responsibility
+
+- Own metadata-driven OpenCode custom skills shipped with this package.
+- Maintain the skill contract artifacts (`SKILL.md`, `README.md`, per-skill helper files) that are copied into
+  `${configDir}/skills` at install time.
+- Preserve a canonical registry boundary: runtime code consumes skill definitions as data, not as executable
+  plugin dependencies.
+
+## Design
+
+- `CUSTOM_SKILLS` in `src/cli/custom-skills.ts` is the authoritative skill manifest for bundled
+  skills; each entry maps folder name + `sourcePath` to an install-time consumer.
+- `install.ts` runs `installCustomSkill()` which recursively copies `src/skills/codemap` and
+  `src/skills/simplify` into the OpenCode skills directory.
+- This directory is partitioned by skill:
+  - `src/skills/codemap/` (command-style repository mapping skill)
+  - `src/skills/simplify/` (readability/refactor guidance skill)
+- Files are considered static runtime payload. No plugin TS module in `src/` imports these files directly; they
+  are loaded by OpenCode via filesystem installation.
+
+## Flow
+
+- `bun run install` delegates to `src/cli/install.ts`, where `installCustomSkills` gates copying of
+  each `CUSTOM_SKILLS` entry.
+- `installCustomSkill()` computes `packageRoot`, validates `sourcePath`, then performs a recursive
+  directory copy via `copyDirRecursive()`.
+- During plugin release, the `files` whitelist in `package.json` must include `src/skills` so
+  `src/skills/**` survive `npm pack`.
+- OpenCode plugin startup discovers these installed folders and reads each `SKILL.md` as a prompt-level contract.
+
+## Integration
+
+- `src/cli/custom-skills.ts`: source-of-truth registry consumed by installer and permission helpers.
+- `src/cli/skills.ts:getSkillPermissionsForAgent()` auto-populates permission rules for
+  `codemap` and `simplify` when agent policy is derived from built-in recommendations.
+- `verify-release-artifact.ts` enforces artifact completeness by asserting `src/skills/simplify/SKILL.md`
+  and `src/skills/codemap/SKILL.md` are present in the tarball.
+- `package.json` scripts (`verify:release`, `build`) rely on these assets to ensure install-time skill availability.

+ 11 - 9
src/skills/cartography/README.md → src/skills/codemap/README.md

@@ -1,31 +1,33 @@
-# Cartography Skill
+# Codemap Skill
 
 Repository understanding and hierarchical codemap generation.
 
 ## Overview
 
-Cartography helps orchestrators map and understand codebases by:
+Codemap helps orchestrators map and understand codebases by:
 
 1. Selecting relevant code/config files using LLM judgment
-2. Creating `.slim/cartography.json` for change tracking
-3. Generating empty `codemap.md` templates for explorers to fill in
+2. Creating `.slim/codemap.json` for change tracking
+3. Generating empty `codemap.md` templates for fixers to fill in
+
+Legacy `.slim/cartography.json` state is migrated to `.slim/codemap.json` automatically.
 
 ## Commands
 
 ```bash
 # Initialize mapping
-python3 cartographer.py init --root /repo --include "src/**/*.ts" --exclude "node_modules/**"
+node codemap.mjs init --root /repo --include "src/**/*.ts" --exclude "node_modules/**"
 
 # Check what changed
-python3 cartographer.py changes --root /repo
+node codemap.mjs changes --root /repo
 
 # Update hashes
-python3 cartographer.py update --root /repo
+node codemap.mjs update --root /repo
 ```
 
 ## Outputs
 
-### .slim/cartography.json
+### .slim/codemap.json
 
 ```json
 {
@@ -46,7 +48,7 @@ python3 cartographer.py update --root /repo
 
 ### codemap.md (per folder)
 
-Empty templates created in each folder for explorers to fill with:
+Empty templates created in each folder for fixers to fill with:
 - Responsibility
 - Design patterns
 - Data/control flow

+ 19 - 16
src/skills/cartography/SKILL.md → src/skills/codemap/SKILL.md

@@ -1,9 +1,9 @@
 ---
-name: cartography
+name: codemap
 description: Generate comprehensive hierarchical codemaps for UNFAMILIAR repositories. Expensive operation - only use when explicitly asked for codebase documentation or initial repository mapping
 ---
 
-# Cartography Skill
+# Codemap Skill
 
 You help users understand and map repositories by creating hierarchical codemaps.
 
@@ -17,11 +17,15 @@ You help users understand and map repositories by creating hierarchical codemaps
 
 ### Step 1: Check for Existing State
 
-**First, check if `.slim/cartography.json` exists in the repo root.**
+**First, check if `.slim/codemap.json` exists in the repo root.**
 
-If it **exists**: Skip to Step 3 (Detect Changes) - no need to re-initialize.
+If it does not exist, check for legacy state at `.slim/cartography.json`.
 
-If it **doesn't exist**: Continue to Step 2 (Initialize).
+If legacy state exists: move `.slim/cartography.json` to `.slim/codemap.json`, then continue with change detection.
+
+If `.slim/codemap.json` exists: Skip to Step 3 (Detect Changes) - no need to re-initialize.
+
+If neither file exists: Continue to Step 2 (Initialize).
 
 ### Step 2: Initialize (Only if no state exists)
 
@@ -33,27 +37,27 @@ If it **doesn't exist**: Continue to Step 2 (Initialize).
      - Docs: `docs/**`, `*.md` (except root `README.md` if needed), `LICENSE`
      - Build/Deps: `node_modules/**`, `dist/**`, `build/**`, `*.min.js`
    - Respect `.gitignore` automatically
-3. **Run cartographer.py init**:
+3. **Run codemap.mjs init**:
 
 ```bash
-python3 ~/.config/opencode/skills/cartography/scripts/cartographer.py init \
+node ~/.config/opencode/skills/codemap/scripts/codemap.mjs init \
   --root ./ \
   --include "src/**/*.ts" \
   --exclude "**/*.test.ts" --exclude "dist/**" --exclude "node_modules/**"
 ```
 
 This creates:
-- `.slim/cartography.json` - File and folder hashes for change detection
+- `.slim/codemap.json` - File and folder hashes for change detection
 - Empty `codemap.md` files in all relevant subdirectories
 
-4. **Delegate to Explorer agents** - Spawn one explorer per folder to read code and fill in its specific `codemap.md` file.
+4. **Delegate codemap writing to Fixer agents** - Spawn one fixer per folder to read code and create or update its specific `codemap.md` file.
 
 ### Step 3: Detect Changes (If state already exists)
 
-1. **Run cartographer.py changes** to see what changed:
+1. **Run codemap.mjs changes** to see what changed:
 
 ```bash
-python3 ~/.config/opencode/skills/cartography/scripts/cartographer.py changes \
+node ~/.config/opencode/skills/codemap/scripts/codemap.mjs changes \
   --root ./
 ```
 
@@ -63,11 +67,11 @@ python3 ~/.config/opencode/skills/cartography/scripts/cartographer.py changes \
    - Modified files
    - Affected folders
 
-3. **Only update affected codemaps** - Spawn one explorer per affected folder to update its `codemap.md`.
+3. **Only update affected codemaps** - Spawn one fixer per affected folder to update its `codemap.md`.
 4. **Run update** to save new state:
 
 ```bash
-python3 ~/.config/opencode/skills/cartography/scripts/cartographer.py update \
+node ~/.config/opencode/skills/codemap/scripts/codemap.mjs update \
   --root ./
 ```
 
@@ -100,12 +104,11 @@ Before working on any task, read `codemap.md` to understand:
 For deep work on a specific folder, also read that folder's `codemap.md`.
 ```
 
-This is idempotent — repeated cartography runs will detect the existing section and skip. No duplication.
-
+This is idempotent — repeated codemap runs will detect the existing section and skip. No duplication.
 
 ## Codemap Content
 
-Explorers are granted write permissions for `codemap.md` files during this workflow. Use precise technical terminology to document the implementation:
+Fixers are responsible for writing `codemap.md` files during this workflow. Use precise technical terminology to document the implementation:
 
 - **Responsibility** - Define the specific role of this directory using standard software engineering terms (e.g., "Service Layer", "Data Access Object", "Middleware").
 - **Design Patterns** - Identify and name specific patterns used (e.g., "Observer", "Singleton", "Factory", "Strategy"). Detail the abstractions and interfaces.

+ 36 - 0
src/skills/codemap/codemap.md

@@ -0,0 +1,36 @@
+# src/skills/codemap/
+
+## Responsibility
+
+- Provide a command-style skill package that standardizes repository mapping workflows for unfamiliar codebases.
+- Define the task contract used by Orchestrator/fixer agents via `SKILL.md` and operational guidance via `README.md`.
+- Generate and evolve change-aware codemap state artifacts (`.slim/codemap.json`) and scaffold placeholders (`codemap.md`).
+
+## Design
+
+- Contract layer: `SKILL.md` (machine prompt contract) + `README.md` (human-facing operation notes).
+- Execution layer: `scripts/codemap.mjs` exports deterministic helper functions:
+  - `parseArgs(argv)`
+  - `cmdInit`, `cmdChanges`, `cmdUpdate`
+  - `selectFiles`, `computeFileHash`, `computeFolderHash`, `createEmptyCodemap`
+  - `loadState`, `saveState`, `migrateLegacyState`
+- Persistence model: JSON state at `.slim/codemap.json` with `metadata`, `file_hashes`, and `folder_hashes`.
+- Testing layer: `scripts/codemap.test.ts` validates pattern matching, hash determinism, and migration behavior.
+- The script intentionally avoids network and mutates only filesystem-local state and codemap templates.
+
+## Flow
+
+- Entry point `main(argv)` parses command and arguments (`init|changes|update`, `--root`, `--include`, `--exclude`, `--exception`) and dispatches via strict branches.
+- `cmdInit()` computes include/exclude candidate sets using `selectFiles()` and writes:
+  1) `.slim/codemap.json` via `saveState()`
+  2) one `codemap.md` per discovered folder via `createEmptyCodemap()`.
+- `cmdChanges()` reloads state (`loadState()` + `migrateLegacyState()`), recomputes current hashes, emits added/removed/modified diffs and affected folder list, and exits non-zero if state is absent.
+- `cmdUpdate()` recomputes full state from existing metadata and persists it, used after targeted fixers finish updates.
+- `codemap` skill invocation path in SKILL workflow is explicit: Step 1 checks `.slim/codemap.json` or `.slim/cartography.json`, then Step 2/3 selects init or incremental path.
+
+## Integration
+
+- Installed under OpenCode through `src/cli/custom-skills.ts` as `name: 'codemap'`, `sourcePath: 'src/skills/codemap'`.
+- `src/cli/install.ts` copies this directory into the user skill directory; OpenCode executes `scripts/codemap.mjs` from that context.
+- `src/hooks/filter-available-skills/index.ts` applies agent-level skill gating via names from `getSkillPermissionsForAgent()`.
+- `scripts/verify-release-artifact.ts` includes codemap skill metadata and runtime checks as required packaged files.

+ 483 - 0
src/skills/codemap/scripts/codemap.mjs

@@ -0,0 +1,483 @@
+#!/usr/bin/env node
+
+import { createHash } from 'node:crypto';
+import {
+  existsSync,
+  mkdirSync,
+  readdirSync,
+  readFileSync,
+  renameSync,
+  statSync,
+  writeFileSync,
+} from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+export const VERSION = '1.0.0';
+export const STATE_DIR = '.slim';
+export const STATE_FILE = 'codemap.json';
+export const LEGACY_STATE_FILE = 'cartography.json';
+export const CODEMAP_FILE = 'codemap.md';
+
+export class PatternMatcher {
+  regex;
+
+  constructor(patterns) {
+    if (!patterns.length) {
+      this.regex = null;
+      return;
+    }
+
+    const regexParts = patterns.map((pattern) => {
+      let reg = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+      reg = reg.replace(/\\\*\\\*\//g, '(?:.*/)?');
+      reg = reg.replace(/\\\*\\\*/g, '.*');
+      reg = reg.replace(/\\\*/g, '[^/]*');
+      reg = reg.replace(/\\\?/g, '.');
+
+      if (pattern.endsWith('/')) {
+        reg += '.*';
+      }
+
+      if (pattern.startsWith('/')) {
+        reg = `^${reg.slice(1)}`;
+      } else {
+        reg = `(?:^|.*/)${reg}`;
+      }
+
+      return `(?:${reg}$)`;
+    });
+
+    this.regex = new RegExp(regexParts.join('|'));
+  }
+
+  matches(filePath) {
+    if (!this.regex) return false;
+    return this.regex.test(filePath);
+  }
+}
+
+export function loadGitignore(root) {
+  const gitignorePath = path.join(root, '.gitignore');
+  if (!existsSync(gitignorePath)) return [];
+
+  return readFileSync(gitignorePath, 'utf8')
+    .split('\n')
+    .map((line) => line.trim())
+    .filter((line) => line && !line.startsWith('#'));
+}
+
+function walkFiles(root) {
+  const files = [];
+
+  function visit(currentDir) {
+    for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
+      const fullPath = path.join(currentDir, entry.name);
+      if (entry.isDirectory()) {
+        if (!entry.name.startsWith('.')) {
+          visit(fullPath);
+        }
+        continue;
+      }
+
+      if (entry.isFile()) {
+        files.push(fullPath);
+      }
+    }
+  }
+
+  visit(root);
+  return files.sort();
+}
+
+export function selectFiles(
+  root,
+  includePatterns,
+  excludePatterns,
+  exceptions,
+  gitignorePatterns,
+) {
+  const includeMatcher = new PatternMatcher(includePatterns);
+  const excludeMatcher = new PatternMatcher(excludePatterns);
+  const gitignoreMatcher = new PatternMatcher(gitignorePatterns);
+  const exceptionSet = new Set(exceptions);
+
+  return walkFiles(root).filter((fullPath) => {
+    let relPath = path.relative(root, fullPath).replaceAll(path.sep, '/');
+    if (relPath.startsWith('./')) {
+      relPath = relPath.slice(2);
+    }
+
+    if (gitignoreMatcher.matches(relPath)) return false;
+    if (excludeMatcher.matches(relPath) && !exceptionSet.has(relPath)) {
+      return false;
+    }
+
+    return includeMatcher.matches(relPath) || exceptionSet.has(relPath);
+  });
+}
+
+export function computeFileHash(filePath) {
+  try {
+    const buffer = readFileSync(filePath);
+    return createHash('md5').update(buffer).digest('hex');
+  } catch {
+    return '';
+  }
+}
+
+export function computeFolderHash(folder, fileHashes) {
+  const folderFiles = Object.entries(fileHashes)
+    .filter(
+      ([filePath]) =>
+        filePath.startsWith(`${folder}/`) ||
+        (folder === '.' && !filePath.includes('/')),
+    )
+    .sort(([a], [b]) => a.localeCompare(b));
+
+  if (!folderFiles.length) return '';
+
+  const hasher = createHash('md5');
+  for (const [filePath, hash] of folderFiles) {
+    hasher.update(`${filePath}:${hash}\n`);
+  }
+  return hasher.digest('hex');
+}
+
+export function getFoldersWithFiles(files, root) {
+  const folders = new Set(['.']);
+
+  for (const filePath of files) {
+    const relPath = path.relative(root, filePath).replaceAll(path.sep, '/');
+    const parts = relPath.split('/').slice(0, -1);
+    for (let i = 0; i < parts.length; i++) {
+      folders.add(parts.slice(0, i + 1).join('/'));
+    }
+  }
+
+  return folders;
+}
+
+export function migrateLegacyState(root) {
+  const stateDir = path.join(root, STATE_DIR);
+  const legacyPath = path.join(stateDir, LEGACY_STATE_FILE);
+  const statePath = path.join(stateDir, STATE_FILE);
+
+  if (existsSync(statePath) || !existsSync(legacyPath)) {
+    return false;
+  }
+
+  mkdirSync(stateDir, { recursive: true });
+  renameSync(legacyPath, statePath);
+  console.log(
+    `Migrated ${STATE_DIR}/${LEGACY_STATE_FILE} -> ${STATE_DIR}/${STATE_FILE}`,
+  );
+  return true;
+}
+
+export function loadState(root) {
+  migrateLegacyState(root);
+  const statePath = path.join(root, STATE_DIR, STATE_FILE);
+  if (!existsSync(statePath)) return null;
+
+  try {
+    return JSON.parse(readFileSync(statePath, 'utf8'));
+  } catch {
+    return null;
+  }
+}
+
+export function saveState(root, state) {
+  const stateDir = path.join(root, STATE_DIR);
+  mkdirSync(stateDir, { recursive: true });
+  writeFileSync(
+    path.join(stateDir, STATE_FILE),
+    `${JSON.stringify(state, null, 2)}\n`,
+  );
+}
+
+export function createEmptyCodemap(folderPath, folderName) {
+  const codemapPath = path.join(folderPath, CODEMAP_FILE);
+  if (existsSync(codemapPath)) return;
+
+  const content = `# ${folderName}/
+
+<!-- Fixer: Fill in this section with architectural understanding -->
+
+## Responsibility
+
+<!-- What is this folder's job in the system? -->
+
+## Design
+
+<!-- Key patterns, abstractions, architectural decisions -->
+
+## Flow
+
+<!-- How does data/control flow through this module? -->
+
+## Integration
+
+<!-- How does it connect to other parts of the system? -->
+`;
+
+  writeFileSync(codemapPath, content);
+}
+
+function buildState(
+  root,
+  includePatterns,
+  excludePatterns,
+  exceptions,
+  selectedFiles,
+) {
+  const fileHashes = {};
+  for (const filePath of selectedFiles) {
+    const relPath = path.relative(root, filePath).replaceAll(path.sep, '/');
+    fileHashes[relPath] = computeFileHash(filePath);
+  }
+
+  const folders = getFoldersWithFiles(selectedFiles, root);
+  const folderHashes = {};
+  for (const folder of folders) {
+    folderHashes[folder] = computeFolderHash(folder, fileHashes);
+  }
+
+  const state = {
+    metadata: {
+      version: VERSION,
+      last_run: new Date().toISOString(),
+      root,
+      include_patterns: includePatterns,
+      exclude_patterns: excludePatterns,
+      exceptions,
+    },
+    file_hashes: fileHashes,
+    folder_hashes: folderHashes,
+  };
+
+  return { state, folders };
+}
+
+export function cmdInit({ root, include = [], exclude = [], exception = [] }) {
+  const resolvedRoot = path.resolve(root);
+  if (!existsSync(resolvedRoot) || !statSync(resolvedRoot).isDirectory()) {
+    console.error(`Error: ${resolvedRoot} is not a directory`);
+    return 1;
+  }
+
+  const includePatterns = include.length ? include : ['**/*'];
+  const excludePatterns = exclude;
+  const exceptions = exception;
+  const gitignore = loadGitignore(resolvedRoot);
+
+  console.log(`Scanning ${resolvedRoot}...`);
+  console.log(`Include patterns: ${JSON.stringify(includePatterns)}`);
+  console.log(`Exclude patterns: ${JSON.stringify(excludePatterns)}`);
+  console.log(`Exceptions: ${JSON.stringify(exceptions)}`);
+
+  const selectedFiles = selectFiles(
+    resolvedRoot,
+    includePatterns,
+    excludePatterns,
+    exceptions,
+    gitignore,
+  );
+
+  console.log(`Selected ${selectedFiles.length} files`);
+
+  const { state, folders } = buildState(
+    resolvedRoot,
+    includePatterns,
+    excludePatterns,
+    exceptions,
+    selectedFiles,
+  );
+
+  saveState(resolvedRoot, state);
+  console.log(`Created ${STATE_DIR}/${STATE_FILE}`);
+
+  for (const folder of folders) {
+    const folderPath =
+      folder === '.' ? resolvedRoot : path.join(resolvedRoot, folder);
+    const folderName = folder === '.' ? path.basename(resolvedRoot) : folder;
+    createEmptyCodemap(folderPath, folderName);
+  }
+
+  console.log(`Created ${folders.size} empty codemap.md files`);
+  return 0;
+}
+
+export function cmdChanges({ root }) {
+  const resolvedRoot = path.resolve(root);
+  const state = loadState(resolvedRoot);
+  if (!state) {
+    console.error("No codemap state found. Run 'init' first.");
+    return 1;
+  }
+
+  const metadata = state.metadata ?? {};
+  const includePatterns = metadata.include_patterns ?? ['**/*'];
+  const excludePatterns = metadata.exclude_patterns ?? [];
+  const exceptions = metadata.exceptions ?? [];
+  const gitignore = loadGitignore(resolvedRoot);
+
+  const currentFiles = selectFiles(
+    resolvedRoot,
+    includePatterns,
+    excludePatterns,
+    exceptions,
+    gitignore,
+  );
+
+  const currentHashes = Object.fromEntries(
+    currentFiles.map((filePath) => [
+      path.relative(resolvedRoot, filePath).replaceAll(path.sep, '/'),
+      computeFileHash(filePath),
+    ]),
+  );
+
+  const savedHashes = state.file_hashes ?? {};
+  const currentPaths = new Set(Object.keys(currentHashes));
+  const savedPaths = new Set(Object.keys(savedHashes));
+
+  const added = [...currentPaths]
+    .filter((filePath) => !savedPaths.has(filePath))
+    .sort();
+  const removed = [...savedPaths]
+    .filter((filePath) => !currentPaths.has(filePath))
+    .sort();
+  const modified = [...currentPaths]
+    .filter((filePath) => savedPaths.has(filePath))
+    .filter((filePath) => currentHashes[filePath] !== savedHashes[filePath])
+    .sort();
+
+  if (!added.length && !removed.length && !modified.length) {
+    console.log('No changes detected.');
+    return 0;
+  }
+
+  if (added.length) {
+    console.log(`\n${added.length} added:`);
+    for (const filePath of added) console.log(`  + ${filePath}`);
+  }
+
+  if (removed.length) {
+    console.log(`\n${removed.length} removed:`);
+    for (const filePath of removed) console.log(`  - ${filePath}`);
+  }
+
+  if (modified.length) {
+    console.log(`\n${modified.length} modified:`);
+    for (const filePath of modified) console.log(`  ~ ${filePath}`);
+  }
+
+  const affectedFolders = new Set(['.']);
+  for (const filePath of [...added, ...removed, ...modified]) {
+    const parts = filePath.split('/').slice(0, -1);
+    for (let i = 0; i < parts.length; i++) {
+      affectedFolders.add(parts.slice(0, i + 1).join('/'));
+    }
+  }
+
+  const sortedFolders = [...affectedFolders].sort();
+  console.log(`\n${sortedFolders.length} folders affected:`);
+  for (const folder of sortedFolders) {
+    console.log(`  ${folder}/`);
+  }
+
+  return 0;
+}
+
+export function cmdUpdate({ root }) {
+  const resolvedRoot = path.resolve(root);
+  const state = loadState(resolvedRoot);
+  if (!state) {
+    console.error("No codemap state found. Run 'init' first.");
+    return 1;
+  }
+
+  const metadata = state.metadata ?? {};
+  const includePatterns = metadata.include_patterns ?? ['**/*'];
+  const excludePatterns = metadata.exclude_patterns ?? [];
+  const exceptions = metadata.exceptions ?? [];
+  const gitignore = loadGitignore(resolvedRoot);
+
+  const selectedFiles = selectFiles(
+    resolvedRoot,
+    includePatterns,
+    excludePatterns,
+    exceptions,
+    gitignore,
+  );
+
+  const { state: nextState } = buildState(
+    resolvedRoot,
+    includePatterns,
+    excludePatterns,
+    exceptions,
+    selectedFiles,
+  );
+
+  saveState(resolvedRoot, nextState);
+  console.log(
+    `Updated ${STATE_DIR}/${STATE_FILE} with ${selectedFiles.length} files`,
+  );
+  return 0;
+}
+
+export function parseArgs(argv) {
+  const [command, ...rest] = argv;
+  const options = { include: [], exclude: [], exception: [] };
+
+  for (let i = 0; i < rest.length; i++) {
+    const arg = rest[i];
+    const value = rest[i + 1];
+
+    if (!arg?.startsWith('--')) continue;
+    if (value === undefined || value.startsWith('--')) {
+      throw new Error(`Missing value for ${arg}`);
+    }
+
+    const key = arg.slice(2);
+    if (key === 'include' || key === 'exclude' || key === 'exception') {
+      options[key].push(value);
+    } else if (key === 'root') {
+      options.root = value;
+    } else {
+      throw new Error(`Unknown option: ${arg}`);
+    }
+
+    i++;
+  }
+
+  return { command, options };
+}
+
+export function main(argv = process.argv.slice(2)) {
+  try {
+    const { command, options } = parseArgs(argv);
+
+    if (!command || !options.root) {
+      console.error(
+        'Usage: codemap.mjs <init|changes|update> --root /path [--include glob] [--exclude glob] [--exception path]',
+      );
+      return 1;
+    }
+
+    if (command === 'init') return cmdInit(options);
+    if (command === 'changes') return cmdChanges(options);
+    if (command === 'update') return cmdUpdate(options);
+
+    console.error(`Unknown command: ${command}`);
+    return 1;
+  } catch (error) {
+    console.error(error instanceof Error ? error.message : String(error));
+    return 1;
+  }
+}
+
+const currentFilePath = fileURLToPath(import.meta.url);
+if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
+  process.exit(main());
+}

+ 129 - 0
src/skills/codemap/scripts/codemap.test.ts

@@ -0,0 +1,129 @@
+import { afterEach, describe, expect, mock, test } from 'bun:test';
+import {
+  existsSync,
+  mkdirSync,
+  mkdtempSync,
+  readFileSync,
+  rmSync,
+  writeFileSync,
+} from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+mock.restore();
+
+const {
+  computeFileHash,
+  computeFolderHash,
+  loadState,
+  PatternMatcher,
+  selectFiles,
+} = await import('./codemap.mjs');
+
+const tempDirs: string[] = [];
+
+function createTempDir() {
+  const dir = mkdtempSync(path.join(os.tmpdir(), 'codemap-'));
+  tempDirs.push(dir);
+  return dir;
+}
+
+afterEach(() => {
+  for (const dir of tempDirs.splice(0)) {
+    rmSync(dir, { force: true, recursive: true });
+  }
+});
+
+describe('PatternMatcher', () => {
+  test('matches expected paths', () => {
+    const matcher = new PatternMatcher([
+      'node_modules/',
+      'dist/',
+      '*.log',
+      'src/**/*.ts',
+    ]);
+
+    expect(matcher.matches('node_modules/foo.js')).toBe(true);
+    expect(matcher.matches('vendor/node_modules/bar.js')).toBe(true);
+    expect(matcher.matches('dist/main.js')).toBe(true);
+    expect(matcher.matches('src/dist/output.js')).toBe(true);
+    expect(matcher.matches('error.log')).toBe(true);
+    expect(matcher.matches('logs/access.log')).toBe(true);
+    expect(matcher.matches('src/index.ts')).toBe(true);
+    expect(matcher.matches('src/utils/helper.ts')).toBe(true);
+    expect(matcher.matches('README.md')).toBe(false);
+    expect(matcher.matches('tests/test.py')).toBe(false);
+  });
+});
+
+describe('hash helpers', () => {
+  test('computes file hash', () => {
+    const dir = createTempDir();
+    const filePath = path.join(dir, 'file.txt');
+    writeFileSync(filePath, 'test content');
+
+    expect(computeFileHash(filePath)).toBe('9473fdd0d880a43c21b7778d34872157');
+  });
+
+  test('computes stable folder hash', () => {
+    const fileHashes = {
+      'src/a.ts': 'hash-a',
+      'src/b.ts': 'hash-b',
+      'tests/test.ts': 'hash-test',
+    };
+
+    const hash1 = computeFolderHash('src', fileHashes);
+    const hash2 = computeFolderHash('src', fileHashes);
+    const hash3 = computeFolderHash('src', {
+      'src/a.ts': 'hash-a-modified',
+      'src/b.ts': 'hash-b',
+    });
+
+    expect(hash1).toBe(hash2);
+    expect(hash1).not.toBe(hash3);
+  });
+});
+
+describe('selectFiles', () => {
+  test('respects include and exclude patterns', () => {
+    const root = createTempDir();
+    mkdirSync(path.join(root, 'src'));
+    mkdirSync(path.join(root, 'node_modules'));
+    writeFileSync(path.join(root, 'src', 'index.ts'), 'code');
+    writeFileSync(path.join(root, 'src', 'index.test.ts'), 'test');
+    writeFileSync(path.join(root, 'node_modules', 'foo.js'), 'dep');
+    writeFileSync(path.join(root, 'package.json'), '{}');
+
+    const selected = selectFiles(
+      root,
+      ['src/**/*.ts', 'package.json'],
+      ['**/*.test.ts', 'node_modules/'],
+      [],
+      [],
+    ).map((filePath) =>
+      path.relative(root, filePath).split(path.sep).join('/'),
+    );
+
+    expect(selected).toEqual(['package.json', 'src/index.ts']);
+  });
+});
+
+describe('loadState', () => {
+  test('migrates legacy cartography state', () => {
+    const root = createTempDir();
+    const slimDir = path.join(root, '.slim');
+    mkdirSync(slimDir);
+
+    const legacyState = { metadata: { version: '1.0.0' } };
+    writeFileSync(
+      path.join(slimDir, 'cartography.json'),
+      JSON.stringify(legacyState),
+    );
+
+    expect(loadState(root)).toEqual(legacyState);
+    expect(existsSync(path.join(slimDir, 'cartography.json'))).toBe(false);
+    expect(
+      JSON.parse(readFileSync(path.join(slimDir, 'codemap.json'), 'utf8')),
+    ).toEqual(legacyState);
+  });
+});

+ 36 - 0
src/skills/simplify/codemap.md

@@ -0,0 +1,36 @@
+# src/skills/simplify/
+
+## Responsibility
+
+- Provide a behavior-preserving refactoring skill contract that constrains code cleanup to clarity-focused,
+  low-risk changes.
+- Define explicit quality gates (understand-before-edit, behavior parity, incremental simplification, rollback-friendly diffs)
+  for any simplification task.
+- Ship only metadata; no local runtime state machine is kept in this directory.
+
+## Design
+
+- Contract layer: `SKILL.md` is the executable prompt specification with explicit phases:
+  - pre-change understanding
+  - simplification candidate selection
+  - incremental transformation and verification
+  - final review checklist.
+- Documentation layer: `README.md` explains intent, source provenance, and plugin install behavior.
+- Policy model is declarative (`description`, allowed usage, checklist) consumed by the OpenCode skill executor,
+  without helper scripts or plugin code dependencies.
+
+## Flow
+
+- Agent discovery resolves `src/skills/simplify` as a custom skill entrypoint, then reads `SKILL.md` at runtime.
+- Runtime behavior is gated by `src/cli/custom-skills.ts` (`allowedAgents: ['oracle']`) and by skill permissions
+  computed in `getSkillPermissionsForAgent()`.
+- In practice the workflow is read-only and context-driven: simplify instructions require understanding of callers,
+  edge cases, and tests before mutation, then apply local, scoped refactors with validation.
+- Consumers (Fixer/Oracle/Reviewer tasks) rely on this contract as operational constraints, not as executable TypeScript.
+
+## Integration
+
+- Installed by plugin installer (`installCustomSkills`) using `src/cli/install.ts` via `installCustomSkill()`.
+- Permission surface is enforced by hook layer in `src/hooks/filter-available-skills/index.ts` (`permissionRules`).
+- Release integrity: `scripts/verify-release-artifact.ts` checks for `src/skills/simplify/SKILL.md` in package tarballs.
+- Operationally paired with codemap/fixer flows in `src/index.ts` orchestrations for post-feature readability hardening.

+ 87 - 255
src/tools/codemap.md

@@ -1,266 +1,98 @@
-# src/tools/ Codemap
-
 ## Responsibility
 
-The `src/tools/` directory provides the core tool implementations for the oh-my-opencode-slim plugin. It exposes three main categories of tools:
-
-1. **AST-grep** - AST-aware structural code search and replacement across 25+ languages
-2. **LSP** - Language Server Protocol integration for code intelligence (definition, references, diagnostics, rename)
-3. **Background Tasks** - Fire-and-forget agent task management with automatic notification
-
-These tools are consumed by the OpenCode plugin system and exposed to AI agents for code navigation, analysis, and modification tasks.
-
----
+- Expose plugin tool definitions for code intelligence and workflow tooling from
+  `src/tools/index.ts`.
+- Publish and compose three primary operational domains:
+  - AST pattern search/replace via `ast-grep/`.
+  - Language server tooling via `lsp/`.
+  - URL fetch/transform with optional secondary model via `smartfetch/`.
+- Provide runtime factories for orchestration helpers:
+  - `createCouncilTool` (`council.ts`).
+- Expose runtime entry contracts (`lspManager`, `setUserLspConfig`, utility
+  constants/types) for plugin bootstrap and config hooks.
 
 ## Design
 
-### Architecture Overview
-
-```
-src/tools/
-├── index.ts              # Central export point
-├── background.ts         # Background task tools (3 tools)
-├── ast-grep/
-│   ├── cli.ts            # CLI execution, path resolution, binary download
-│   ├── index.ts          # Module re-exports
-│   ├── types.ts          # TypeScript interfaces (CliLanguage, CliMatch, SgResult)
-│   ├── utils.ts          # Output formatting (formatSearchResult, formatReplaceResult)
-│   ├── constants.ts      # CLI path resolution, safety limits
-│   └── downloader.ts     # Binary auto-download for missing ast-grep
-└── lsp/
-    ├── client.ts         # LSP client & connection pooling (LSPServerManager singleton)
-    ├── config.ts         # Server discovery & language mapping
-    ├── constants.ts      # Built-in server configs (45+ servers), extensions, install hints
-    ├── index.ts          # Module re-exports
-    ├── types.ts          # LSP type re-exports (Diagnostic, Location, WorkspaceEdit, etc.)
-    ├── utils.ts          # Formatters & workspace edit application
-    ├── config-store.ts   # User LSP config runtime storage
-    └── tools.ts          # 4 tool definitions
-```
-
-### Key Patterns
-
-#### 1. Tool Definition Pattern
-All tools follow the OpenCode plugin tool schema:
-```typescript
-export const toolName: ToolDefinition = tool({
-  description: string,
-  args: { /* Zod schema */ },
-  execute: async (args, context) => { /* implementation */ }
-});
-```
-
-#### 2. CLI Abstraction Layer (ast-grep)
-The ast-grep module uses a CLI execution pattern:
-- **cli.ts**: Low-level subprocess spawning with timeout handling and JSON output parsing
-- **constants.ts**: CLI path resolution with fallback chain (cached binary → @ast-grep/cli → platform-specific → Homebrew → download)
-- **downloader.ts**: Binary auto-download for missing dependencies
-- **utils.ts**: Output formatting and truncation handling
-
-#### 3. Connection Pooling (LSP)
-The LSP module implements a singleton `LSPServerManager` with:
-- **Connection pooling**: Reuse LSP clients per workspace root (key: `root::serverId`)
-- **Reference counting**: Track active usage via `refCount`, increment on acquire, decrement on release
-- **Idle cleanup**: Auto-shutdown after 5 minutes of inactivity (check every 60s)
-- **Initialization tracking**: Prevent concurrent initialization races via `initPromise`
-
-#### 4. Safety Limits
-All tools enforce strict safety limits:
-- **Timeout**: 300s (ast-grep, LSP initialization)
-- **Output size**: 1MB (ast-grep)
-- **Match limits**: 500 matches (ast-grep), 200 diagnostics (LSP), 200 references (LSP)
-
-#### 5. Error Handling
-- Clear error messages with installation hints for missing binaries
-- Timeout handling with process cleanup
-- Truncation detection and reporting with reason codes
-- Graceful fallback chains for CLI resolution
-
----
+- `src/tools/index.ts` is the canonical export surface. It re-exports:
+  - `ast_grep_search`, `ast_grep_replace`.
+  - `lsp_diagnostics`, `lsp_find_references`, `lsp_goto_definition`,
+    `lsp_rename`, `lspManager`, `setUserLspConfig`.
+  - `createWebfetchTool`.
+  - `createCouncilTool`.
+- **Common tool schema pattern**: all tool files use `@opencode-ai/plugin/tool` or
+  `@opencode-ai/plugin` `tool` with typed schemas and `ToolDefinition` objects.
+- **AST-grep stack (`ast-grep/`)**:
+  - `cli.ts` handles invocation flow via `runSg`, `getAstGrepPath`,
+    `startBackgroundInit`.
+  - `types.ts` defines `CliLanguage`, `CliMatch`, `SgResult`.
+  - `constants.ts` owns binary resolution (`getSgCliPath`, `findSgCliPathSync`),
+    limits (`DEFAULT_MAX_MATCHES`, `DEFAULT_MAX_OUTPUT_BYTES`, `DEFAULT_TIMEOUT_MS`).
+  - `downloader.ts` implements `ensureAstGrepBinary` and release-specific
+    fallback download.
+  - `utils.ts` centralizes user-facing renderers.
+- **LSP stack (`lsp/`)**:
+  - `client.ts` implements `LSPServerManager` singleton (`lspManager`) and
+    `LSPClient` with `start`, `initialize`, `definition`, `references`,
+    `diagnostics`, `rename`, `stop`.
+  - `config.ts` resolves server selection via `findServerForExtension` and
+    `resolveServerCommand`.
+  - `config-store.ts` persists `setUserLspConfig`/`getUserLspConfig` from
+    OpenCode `lsp` config.
+  - `types.ts` exports protocol-aligned types (`Diagnostic`, `Location`,
+    `WorkspaceEdit`, `ServerLookupResult`, etc.).
+  - `utils.ts` handles formatting, root discovery, `withLspClient`, and
+    `applyWorkspaceEdit`.
+- **Smartfetch stack (`smartfetch/`)**:
+  - `tool.ts` defines `createWebfetchTool` and the complete execution path.
+  - `network.ts` enforces redirect policy, read limits, HTML/binary detection,
+    and `fetchWithRedirects`.
+  - `cache.ts` uses `CACHE` (`LRUCache`) and `buildCacheKey` for memoization.
+  - `utils.ts` normalizes and renders downloaded content (`extractFromHtml`,
+    `cleanFetchedMarkdown`, `joinRenderedContent`).
+  - `binary.ts` persists payloads with `saveBinary`.
+  - `secondary-model.ts` runs
+    `readSecondaryModelFromConfig`/`runSecondaryModelWithFallback`.
 
 ## Flow
 
-### AST-grep Tool Flow
-
-```
-User Request (ast_grep_search or ast_grep_replace)
-    ↓
-Tool definition (ast-grep/tools.ts)
-    ↓
-runSg() (cli.ts)
-    ├─→ getAstGrepPath()
-    │   ├─→ Check cached path
-    │   ├─→ findSgCliPathSync()
-    │   │   ├─→ Cached binary in ~/.cache
-    │   │   ├─→ @ast-grep/cli package
-    │   │   ├─→ Platform-specific package (@ast-grep/cli-*)
-    │   │   └─→ Homebrew (macOS)
-    │   └─→ ensureAstGrepBinary() → download if missing
-    └─→ Build args: pattern, lang, rewrite, globs, paths
-    ↓
-spawn([sg, 'run', '-p', pattern, '--lang', lang, ...])
-    ↓
-Parse JSON output → CliMatch[]
-    ↓
-Handle truncation (max_output_bytes, max_matches, timeout)
-    ↓
-formatSearchResult() / formatReplaceResult() (utils.ts)
-    ├─→ Group by file
-    ├─→ Truncate long text
-    └─→ Add summary
-    ↓
-Add empty result hints (getEmptyResultHint)
-    ↓
-Return formatted output
-```
-
-### LSP Tool Flow
-
-```
-User Request (e.g., lsp_goto_definition)
-    ↓
-Tool definition (lsp/tools.ts)
-    ↓
-withLspClient() (utils.ts)
-    ├─→ findServerForExtension() (config.ts)
-    │   ├─→ Match extension to BUILTIN_SERVERS
-    │   ├─→ Merge with user config from config-store
-    │   └─→ isServerInstalled() → PATH check
-    ├─→ findServerProjectRoot() → server-specific root patterns
-    └─→ lspManager.getClient() (client.ts)
-        ├─→ Check cache (root::serverId)
-        ├─→ If cached: increment refCount, return
-        └─→ If new:
-            ├─→ new LSPClient(root, server)
-            ├─→ client.start() → spawn server
-            ├─→ client.initialize() → LSP handshake
-            └─→ Store in pool with refCount=1
-    ↓
-client.definition() / references() / diagnostics() / rename()
-    ├─→ openFile() → textDocument/didOpen
-    └─→ Send LSP request
-    ↓
-Format result (formatLocation, formatDiagnostic, etc.)
-    ↓
-lspManager.releaseClient() → decrement refCount
-    ↓
-Return formatted output
-```
-
-**LSP Client Lifecycle:**
-```
-start()
-  ├─→ spawn(command)
-  ├─→ Create JSON-RPC connection (vscode-jsonrpc)
-  ├─→ Register handlers (diagnostics, configuration, window)
-  └─→ Wait for process to stabilize
-    ↓
-initialize()
-  ├─→ sendRequest('initialize', capabilities)
-  └─→ sendNotification('initialized')
-    ↓
-[Operational phase]
-  ├─→ openFile() → textDocument/didOpen
-  ├─→ definition() / references() / diagnostics() / rename()
-  └─→ Receive notifications (diagnostics)
-    ↓
-stop()
-  ├─→ sendRequest('shutdown')
-  ├─→ sendNotification('exit')
-  └─→ kill process
-```
+- **AST-grep path**:
+  - Tool executes (`ast_grep_search`/`ast_grep_replace`) and calls `runSg`.
+  - `runSg` builds CLI args, resolves `sg` via sync cache/path checks,
+    fallback download if missing, and executes with timeout.
+  - JSON output is parsed into `SgResult`, respecting truncation/error states.
+  - `formatSearchResult` / `formatReplaceResult` produce output.
+- **LSP path**:
+  - Tool executes one of four LSP handlers in `lsp/tools.ts`.
+  - `withLspClient` resolves extension using `findServerForExtension` and
+    computes workspace root.
+  - `lspManager.getClient` creates or reuses a pooled `LSPClient`, waits for
+    initialize, and increments/decrements `refCount`.
+  - Client opens file (`openFile`/`ensureDocumentSynced`) then issues protocol
+    requests.
+  - Formatters convert protocol data to plain output strings; edits are applied
+    with `applyWorkspaceEdit`.
+- **Smartfetch path**:
+  - `createWebfetchTool` validates permissions (`ctx.ask`) and timeout, reads
+    secondary model candidates, then checks cache (`CACHE`).
+  - If permitted, it probes llms docs, else performs fetch with redirect
+    fallback and capped read size.
+  - Response is decoded, content-type normalized, converted by format mode, and
+    optionally passed to secondary model (`runSecondaryModelWithFallback`).
+  - For binary, metadata-only or saved-result branches are selected based on
+    `save_binary`, size, and MIME type.
+- **Council**:
+  - `createCouncilTool` enforces caller guard (`council` / `orchestrator`) before
+    invoking `CouncilManager.runCouncil`.
 
 ## Integration
 
-### Dependencies
-
-#### External Dependencies
-- **@opencode-ai/plugin**: Tool definition schema (`tool`, `ToolDefinition`)
-- **vscode-jsonrpc**: LSP JSON-RPC protocol implementation
-- **vscode-languageserver-protocol**: LSP type definitions
-- **bun**: Subprocess spawning (`spawn`), file operations (`Bun.write`)
-- **which**: PATH resolution for CLI binaries
-
-#### Internal Dependencies
-- **src/config**: tool/config constants and schemas
-- **src/utils**: `extractZip` for binary extraction
-- **src/utils/logger**: Logging utilities
-
-### Consumers
-
-#### Direct Consumers
-- **src/index.ts**: Main plugin entry point imports all tools
-
-#### Tool Registry
-All tools are exported from `src/tools/index.ts`:
-```typescript
-export { ast_grep_replace, ast_grep_search } from './ast-grep';
-export { createBackgroundTools } from './background';
-export {
-  lsp_diagnostics,
-  lsp_find_references,
-  lsp_goto_definition,
-  lsp_rename,
-  lspManager,
-  setUserLspConfig,
-} from './lsp';
-```
-
-### Configuration
-
-#### LSP Server Configuration
-- **BUILTIN_SERVERS** (lsp/constants.ts): Pre-configured servers for 45+ languages
-- **LANGUAGE_EXTENSIONS** (lsp/constants.ts): Extension to LSP language ID mapping
-- **LSP_INSTALL_HINTS** (lsp/constants.ts): Installation instructions per server
-- **NearestRoot** (lsp/constants.ts): Factory for root pattern matching functions
-
-#### User LSP Configuration
-- **config-store.ts**: Runtime storage for user-provided LSP config from opencode.json
-- Merged at runtime: built-in servers + user config (user config overrides command/extensions/env, root patterns preserved from built-in)
-- Can disable servers with `"disabled": true`
-
-#### AST-grep Configuration
-- **CLI_LANGUAGES** (ast-grep/types.ts): Supported languages
-- **Safety limits**: Timeout (300s), max output (1MB), max matches (500)
-
-### Binary Management
-
-#### AST-grep (ast-grep/downloader.ts)
-- **Version**: 0.40.0 (synced with @ast-grep/cli package)
-- **Platforms**: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64, win32-arm64, win32-ia32
-- **Install location**: `~/.cache/oh-my-opencode-slim/bin/sg` (Linux/macOS), `%LOCALAPPDATA%\oh-my-opencode-slim\bin\sg.exe` (Windows)
-- **Fallback chain**: @ast-grep/cli → platform-specific package → Homebrew → download from GitHub
-
-### Performance Considerations
-
-- **Connection pooling**: LSP clients reused across tool calls
-- **Idle cleanup**: LSP clients shutdown after 5 minutes inactivity
-- **Output truncation**: Prevent memory issues with large outputs
-- **Timeout enforcement**: All subprocess operations have timeouts
-- **Caching**: CLI paths cached to avoid repeated filesystem checks
-- **Delegation helpers**: Retry guidance and council/webfetch tools complement OpenCode's built-in task flow
-
----
-
-## File-by-File Summary
-
-### Root Level
-- **index.ts**: Central export point for all tools
-
-### ast-grep/
-- **index.ts**: Re-exports ast-grep module and types
-- **cli.ts**: `runSg()`, `getAstGrepPath()`, `startBackgroundInit()`, `isCliAvailable()`, `ensureCliAvailable()` - CLI execution layer
-- **types.ts**: `CliLanguage`, `CliMatch`, `SgResult`, `CLI_LANGUAGES` - TypeScript interfaces
-- **utils.ts**: `formatSearchResult()`, `formatReplaceResult()`, `getEmptyResultHint()` - Output formatting
-- **constants.ts**: `findSgCliPathSync()`, `getSgCliPath()`, `setSgCliPath()`, `checkEnvironment()`, `formatEnvironmentCheck()`, safety limits
-- **downloader.ts**: `downloadAstGrep()`, `ensureAstGrepBinary()`, `getCacheDir()`, `getCachedBinaryPath()` - Binary management
-
-### lsp/
-- **index.ts**: Re-exports LSP module, tools, and types
-- **client.ts**: `LSPServerManager` (singleton), `LSPClient` class - full connection lifecycle management
-- **tools.ts**: 4 tools: `lsp_goto_definition`, `lsp_find_references`, `lsp_diagnostics`, `lsp_rename`
-- **types.ts**: LSP type re-exports from vscode-languageserver-protocol (`Diagnostic`, `Location`, `WorkspaceEdit`, etc.)
-- **utils.ts**: `withLspClient()`, `findServerProjectRoot()`, formatters, `applyWorkspaceEdit()`, `formatApplyResult()`
-- **config.ts**: `findServerForExtension()`, `getLanguageId()`, `isServerInstalled()`, `buildMergedServers()`
-- **config-store.ts**: `setUserLspConfig()`, `getUserLspConfig()`, `getAllUserLspConfigs()`, `hasUserLspConfig()`
-- **constants.ts**: `BUILTIN_SERVERS` (45+ servers), `LANGUAGE_EXTENSIONS`, `LSP_INSTALL_HINTS`, `NearestRoot()`, safety limits
+- `src/index.ts` imports these exports and injects them into plugin tool surfaces.
+- `setUserLspConfig` is called during plugin initialization so `findServerForExtension`
+  reflects active `lsp` config.
+- OpenCode-facing dependencies used directly in these modules:
+  - `@opencode-ai/plugin` / `@opencode-ai/plugin/tool` (`tool`, schemas).
+  - `vscode-jsonrpc` + `vscode-languageserver-protocol`.
+  - `which`, `lru-cache`, `bun` runtime APIs, network stack.
+  - DOM extraction libs in smartfetch.
+- Consumers include orchestrator/council agents, `@opencode` task runners, and
+  any extension tests that import tools/types from the `src/tools` modules.

+ 42 - 34
src/tools/lsp/config.test.ts

@@ -1,42 +1,44 @@
-import { beforeEach, describe, expect, mock, test } from 'bun:test';
+import {
+  afterEach,
+  beforeEach,
+  describe,
+  expect,
+  mock,
+  spyOn,
+  test,
+} from 'bun:test';
+import * as fs from 'node:fs';
+import * as os from 'node:os';
 import { join } from 'node:path';
 
-// Mock fs and os BEFORE importing the modules that use them
-mock.module('fs', () => ({
-  existsSync: mock(() => false),
-}));
-
-mock.module('os', () => ({
-  homedir: () => '/home/user',
-}));
-
-// Create a mock for which.sync
-const whichSyncMock = mock(() => null);
+const whichSyncMock = mock((..._args: unknown[]) => null as string | null);
 mock.module('which', () => ({
   sync: whichSyncMock,
   default: { sync: whichSyncMock },
 }));
 
-import { existsSync } from 'node:fs';
-// Now import the code to test
 import { findServerForExtension, isServerInstalled } from './config';
 
 describe('config', () => {
   beforeEach(() => {
-    (existsSync as any).mockClear();
-    (existsSync as any).mockImplementation(() => false);
+    spyOn(fs, 'existsSync').mockImplementation(() => false);
+    spyOn(os, 'homedir').mockReturnValue('/home/user');
     whichSyncMock.mockClear();
     whichSyncMock.mockReturnValue(null);
   });
 
+  afterEach(() => {
+    mock.restore();
+  });
+
   describe('isServerInstalled', () => {
     test('should return false if command is empty', () => {
       expect(isServerInstalled([])).toBe(false);
     });
 
     test('should detect absolute paths', () => {
-      (existsSync as any).mockImplementation(
-        (path: string) => path === '/usr/bin/lsp-server',
+      spyOn(fs, 'existsSync').mockImplementation(
+        (path: fs.PathLike) => path === '/usr/bin/lsp-server',
       );
       expect(isServerInstalled(['/usr/bin/lsp-server'])).toBe(true);
       expect(isServerInstalled(['/usr/bin/missing'])).toBe(false);
@@ -46,7 +48,6 @@ describe('config', () => {
       const originalPath = process.env.PATH;
       process.env.PATH = '/usr/local/bin:/usr/bin';
 
-      // Mock whichSync to return a path (simulating the command is found)
       whichSyncMock.mockReturnValue(
         join('/usr/bin', 'typescript-language-server'),
       );
@@ -65,8 +66,8 @@ describe('config', () => {
         'typescript-language-server',
       );
 
-      (existsSync as any).mockImplementation(
-        (path: string) => path === localBin,
+      spyOn(fs, 'existsSync').mockImplementation(
+        (path: fs.PathLike) => path === localBin,
       );
 
       expect(isServerInstalled(['typescript-language-server'])).toBe(true);
@@ -81,7 +82,6 @@ describe('config', () => {
         'typescript-language-server',
       );
 
-      // Mock whichSync to return the global bin path
       whichSyncMock.mockReturnValue(globalBin);
 
       expect(isServerInstalled(['typescript-language-server'])).toBe(true);
@@ -90,18 +90,20 @@ describe('config', () => {
 
   describe('findServerForExtension', () => {
     test('should skip deno for .ts when project is not a deno workspace', () => {
-      whichSyncMock.mockImplementation((cmd: string) =>
+      whichSyncMock.mockImplementation((cmd: unknown) =>
         cmd === 'typescript-language-server'
           ? join('/usr/bin', 'typescript-language-server')
           : null,
       );
-      (existsSync as any).mockImplementation((path: string) =>
-        path.includes('bun.lock'),
+      spyOn(fs, 'existsSync').mockImplementation((path: fs.PathLike) =>
+        path.toString().includes('bun.lock'),
       );
+
       const result = findServerForExtension(
         '.ts',
         '/workspace/project/src/index.ts',
       );
+
       expect(result.status).toBe('found');
       if (result.status === 'found') {
         expect(result.server.id).toBe('typescript');
@@ -109,13 +111,15 @@ describe('config', () => {
     });
 
     test('should prefer deno for .ts in a deno workspace', () => {
-      whichSyncMock.mockImplementation((cmd: string) =>
+      whichSyncMock.mockImplementation((cmd: unknown) =>
         cmd === 'deno' ? join('/usr/bin', 'deno') : null,
       );
-      (existsSync as any).mockImplementation((path: string) =>
-        path.includes('deno.json'),
+      spyOn(fs, 'existsSync').mockImplementation((path: fs.PathLike) =>
+        path.toString().includes('deno.json'),
       );
+
       const result = findServerForExtension('.ts', '/workspace/app/src/mod.ts');
+
       expect(result.status).toBe('found');
       if (result.status === 'found') {
         expect(result.server.id).toBe('deno');
@@ -123,10 +127,12 @@ describe('config', () => {
     });
 
     test('should return found for .py extension if installed (prefers ty)', () => {
-      whichSyncMock.mockImplementation((cmd: string) =>
+      whichSyncMock.mockImplementation((cmd: unknown) =>
         cmd === 'ty' ? join('/usr/bin', 'ty') : null,
       );
+
       const result = findServerForExtension('.py');
+
       expect(result.status).toBe('found');
       if (result.status === 'found') {
         expect(result.server.id).toBe('ty');
@@ -139,13 +145,13 @@ describe('config', () => {
     });
 
     test('should continue to later matching servers when earlier ones are unavailable', () => {
-      whichSyncMock.mockImplementation((cmd: string) =>
+      whichSyncMock.mockImplementation((cmd: unknown) =>
         cmd === 'typescript-language-server'
           ? join('/usr/bin', 'typescript-language-server')
           : null,
       );
-      (existsSync as any).mockImplementation((path: string) =>
-        path.includes('bun.lock'),
+      spyOn(fs, 'existsSync').mockImplementation((path: fs.PathLike) =>
+        path.toString().includes('bun.lock'),
       );
 
       const result = findServerForExtension(
@@ -160,13 +166,15 @@ describe('config', () => {
     });
 
     test('should return first applicable not_installed server if no match is launchable', () => {
-      (existsSync as any).mockImplementation((path: string) =>
-        path.includes('bun.lock'),
+      spyOn(fs, 'existsSync').mockImplementation((path: fs.PathLike) =>
+        path.toString().includes('bun.lock'),
       );
+
       const result = findServerForExtension(
         '.ts',
         '/workspace/project/src/index.ts',
       );
+
       expect(result.status).toBe('not_installed');
       if (result.status === 'not_installed') {
         expect(result.server.id).toBe('typescript');

+ 6 - 6
src/tools/lsp/config.ts

@@ -1,8 +1,8 @@
 // Simplified LSP config - uses OpenCode's lsp config from opencode.json
 // Falls back to BUILTIN_SERVERS if no user config exists
 
-import { existsSync } from 'node:fs';
-import { homedir } from 'node:os';
+import * as fs from 'node:fs';
+import * as os from 'node:os';
 import { dirname, join, resolve } from 'node:path';
 import whichSync from 'which';
 import { log } from '../../utils';
@@ -215,13 +215,13 @@ export function resolveServerCommand(
   const [cmd, ...args] = command;
 
   if (cmd.includes('/') || cmd.includes('\\')) {
-    return existsSync(cmd) ? command : null;
+    return fs.existsSync(cmd) ? command : null;
   }
 
   const isWindows = process.platform === 'win32';
   const ext = isWindows ? '.exe' : '';
 
-  const opencodeBin = join(homedir(), '.config', 'opencode', 'bin');
+  const opencodeBin = join(os.homedir(), '.config', 'opencode', 'bin');
   const searchPath =
     (process.env.PATH ?? '') + (isWindows ? ';' : ':') + opencodeBin;
 
@@ -237,10 +237,10 @@ export function resolveServerCommand(
 
   const localBinRoot = cwd ?? process.cwd();
   const localBin = join(localBinRoot, 'node_modules', '.bin', cmd);
-  if (existsSync(localBin)) {
+  if (fs.existsSync(localBin)) {
     return [localBin, ...args];
   }
-  if (existsSync(localBin + ext)) {
+  if (fs.existsSync(localBin + ext)) {
     return [localBin + ext, ...args];
   }
 

+ 31 - 24
src/tools/lsp/utils.test.ts

@@ -1,15 +1,14 @@
-import { beforeEach, describe, expect, mock, test } from 'bun:test';
-
-// Mock fs BEFORE importing modules
-mock.module('fs', () => ({
-  readFileSync: mock(() => ''),
-  writeFileSync: mock(),
-  unlinkSync: mock(),
-  existsSync: mock(() => true),
-  statSync: mock(() => ({ isDirectory: () => false })),
-}));
-
-import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
+import {
+  afterEach,
+  beforeEach,
+  describe,
+  expect,
+  mock,
+  spyOn,
+  test,
+} from 'bun:test';
+import * as fs from 'node:fs';
+
 import {
   applyWorkspaceEdit,
   filterDiagnosticsBySeverity,
@@ -22,9 +21,17 @@ import {
 
 describe('utils', () => {
   beforeEach(() => {
-    (readFileSync as any).mockClear();
-    (writeFileSync as any).mockClear();
-    (unlinkSync as any).mockClear();
+    spyOn(fs, 'readFileSync').mockImplementation((() => '') as any);
+    spyOn(fs, 'writeFileSync').mockImplementation(() => undefined);
+    spyOn(fs, 'unlinkSync').mockImplementation(() => undefined);
+    spyOn(fs, 'existsSync').mockImplementation(() => true);
+    spyOn(fs, 'statSync').mockImplementation((() => ({
+      isDirectory: () => false,
+    })) as any);
+  });
+
+  afterEach(() => {
+    mock.restore();
   });
 
   describe('uriToPath', () => {
@@ -96,7 +103,7 @@ describe('utils', () => {
     test('should apply single file edit', () => {
       const uri = 'file:///test.ts';
       const filePath = uriToPath(uri);
-      (readFileSync as any).mockReturnValue('line1\nline2\nline3');
+      spyOn(fs, 'readFileSync').mockReturnValue('line1\nline2\nline3' as any);
 
       const edit = {
         changes: {
@@ -115,12 +122,12 @@ describe('utils', () => {
       const result = applyWorkspaceEdit(edit as any);
       expect(result.success).toBe(true);
       expect(result.filesModified).toContain(filePath);
-      expect(writeFileSync).toHaveBeenCalled();
+      expect(fs.writeFileSync).toHaveBeenCalled();
     });
 
     test('should handle overlapping edits by sorting them in reverse order', () => {
       const uri = 'file:///test.ts';
-      (readFileSync as any).mockReturnValue('abcde');
+      spyOn(fs, 'readFileSync').mockReturnValue('abcde' as any);
 
       const edit = {
         changes: {
@@ -145,7 +152,7 @@ describe('utils', () => {
 
       const result = applyWorkspaceEdit(edit as any);
       expect(result.success).toBe(true);
-      const writtenContent = (writeFileSync as any).mock.calls[0][1];
+      const writtenContent = (fs.writeFileSync as any).mock.calls[0][1];
       expect(writtenContent).toBe('1b3de');
     });
 
@@ -156,7 +163,7 @@ describe('utils', () => {
 
       const result = applyWorkspaceEdit(edit as any);
       expect(result.success).toBe(true);
-      expect(writeFileSync).toHaveBeenCalledWith(
+      expect(fs.writeFileSync).toHaveBeenCalledWith(
         uriToPath('file:///new.ts'),
         '',
         'utf-8',
@@ -166,7 +173,7 @@ describe('utils', () => {
     test('should handle rename file operation', () => {
       const oldUri = 'file:///old.ts';
       const newUri = 'file:///new.ts';
-      (readFileSync as any).mockReturnValue('some content');
+      spyOn(fs, 'readFileSync').mockReturnValue('some content' as any);
 
       const edit = {
         documentChanges: [{ kind: 'rename', oldUri, newUri }],
@@ -174,12 +181,12 @@ describe('utils', () => {
 
       const result = applyWorkspaceEdit(edit as any);
       expect(result.success).toBe(true);
-      expect(writeFileSync).toHaveBeenCalledWith(
+      expect(fs.writeFileSync).toHaveBeenCalledWith(
         uriToPath(newUri),
         'some content',
         'utf-8',
       );
-      expect(unlinkSync).toHaveBeenCalledWith(uriToPath(oldUri));
+      expect(fs.unlinkSync).toHaveBeenCalledWith(uriToPath(oldUri));
     });
 
     test('should handle delete file operation', () => {
@@ -190,7 +197,7 @@ describe('utils', () => {
 
       const result = applyWorkspaceEdit(edit as any);
       expect(result.success).toBe(true);
-      expect(unlinkSync).toHaveBeenCalledWith(uriToPath(uri));
+      expect(fs.unlinkSync).toHaveBeenCalledWith(uriToPath(uri));
     });
 
     test('should return error if no edit provided', () => {

+ 10 - 16
src/tools/lsp/utils.ts

@@ -1,12 +1,6 @@
 // LSP Utilities - Essential formatters and helpers
 
-import {
-  existsSync,
-  readFileSync,
-  statSync,
-  unlinkSync,
-  writeFileSync,
-} from 'node:fs';
+import * as fs from 'node:fs';
 import { dirname, extname, join, resolve } from 'node:path';
 import { fileURLToPath } from 'node:url';
 import { log } from '../../utils/logger';
@@ -51,7 +45,7 @@ export function findWorkspaceRoot(filePath: string): string {
   let dir = resolve(filePath);
 
   try {
-    if (!statSync(dir).isDirectory()) {
+    if (!fs.statSync(dir).isDirectory()) {
       dir = dirname(dir);
     }
   } catch {
@@ -69,7 +63,7 @@ export function findWorkspaceRoot(filePath: string): string {
   let prevDir = '';
   while (dir !== prevDir) {
     for (const marker of markers) {
-      if (existsSync(join(dir, marker))) {
+      if (fs.existsSync(join(dir, marker))) {
         return dir;
       }
     }
@@ -221,7 +215,7 @@ function applyTextEditsToFile(
   edits: TextEdit[],
 ): { success: boolean; editCount: number; error?: string } {
   try {
-    const content = readFileSync(filePath, 'utf-8');
+    const content = fs.readFileSync(filePath, 'utf-8');
     const lines = content.split('\n');
 
     const sortedEdits = [...edits].sort((a, b) => {
@@ -256,7 +250,7 @@ function applyTextEditsToFile(
       }
     }
 
-    writeFileSync(filePath, lines.join('\n'), 'utf-8');
+    fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
     return { success: true, editCount: edits.length };
   } catch (err) {
     return {
@@ -318,7 +312,7 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
         if (change.kind === 'create') {
           try {
             const filePath = uriToPath(change.uri);
-            writeFileSync(filePath, '', 'utf-8');
+            fs.writeFileSync(filePath, '', 'utf-8');
             result.filesModified.push(filePath);
           } catch (err) {
             result.success = false;
@@ -328,9 +322,9 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
           try {
             const oldPath = uriToPath(change.oldUri);
             const newPath = uriToPath(change.newUri);
-            const content = readFileSync(oldPath, 'utf-8');
-            writeFileSync(newPath, content, 'utf-8');
-            unlinkSync(oldPath);
+            const content = fs.readFileSync(oldPath, 'utf-8');
+            fs.writeFileSync(newPath, content, 'utf-8');
+            fs.unlinkSync(oldPath);
             result.filesModified.push(newPath);
           } catch (err) {
             result.success = false;
@@ -339,7 +333,7 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
         } else if (change.kind === 'delete') {
           try {
             const filePath = uriToPath(change.uri);
-            unlinkSync(filePath);
+            fs.unlinkSync(filePath);
             result.filesModified.push(filePath);
           } catch (err) {
             result.success = false;