portawhip 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +162 -0
  4. package/SECURITY.md +15 -0
  5. package/VISION.md +281 -0
  6. package/adapters/hooks/hook-stub.mjs +21 -0
  7. package/adapters/hooks/hook-stub.test.mjs +43 -0
  8. package/adapters/hooks/universal-hook.mjs +348 -0
  9. package/adapters/hooks/universal-hook.test.mjs +294 -0
  10. package/adapters/instructions/generate.mjs +114 -0
  11. package/core/fixtures/bad-recipe.yaml +5 -0
  12. package/core/fixtures/capability-graph.json +10 -0
  13. package/core/fixtures/skill-with-metadata/SKILL.md +15 -0
  14. package/core/registry/capability-docs.mjs +145 -0
  15. package/core/registry/capability-graph-compiler.mjs +90 -0
  16. package/core/registry/capability-graph.mjs +53 -0
  17. package/core/registry/capability-kind.mjs +9 -0
  18. package/core/registry/cli-enrich.mjs +316 -0
  19. package/core/registry/cli-enrich.test.mjs +119 -0
  20. package/core/registry/discover.mjs +379 -0
  21. package/core/registry/enrich.mjs +213 -0
  22. package/core/registry/enrich.test.mjs +53 -0
  23. package/core/registry/eval-harvest.mjs +93 -0
  24. package/core/registry/eval-harvest.test.mjs +64 -0
  25. package/core/registry/registry.mjs +197 -0
  26. package/core/router/concept-vector.mjs +91 -0
  27. package/core/router/concept-vector.test.mjs +36 -0
  28. package/core/router/curated-trigger.test.mjs +28 -0
  29. package/core/router/dense-embedder.mjs +242 -0
  30. package/core/router/fusion.mjs +18 -0
  31. package/core/router/hybrid-router.mjs +375 -0
  32. package/core/router/intent-evidence.mjs +60 -0
  33. package/core/router/prompt-hygiene.mjs +31 -0
  34. package/core/router/route-entry.mjs +74 -0
  35. package/core/router/router-cli.mjs +218 -0
  36. package/core/router/router-cli.test.mjs +25 -0
  37. package/core/router/router-eval.mjs +166 -0
  38. package/core/router/router-live.test.mjs +89 -0
  39. package/core/router/router.test.mjs +838 -0
  40. package/core/router/scorer.mjs +72 -0
  41. package/core/router/sparse-retriever.mjs +79 -0
  42. package/core/router/tokenize.mjs +42 -0
  43. package/core/state/bundle-state.mjs +124 -0
  44. package/core/state/bundle-state.test.mjs +175 -0
  45. package/core/state/config.mjs +115 -0
  46. package/core/state/feedback.mjs +129 -0
  47. package/core/state/feedback.test.mjs +181 -0
  48. package/core/state/runtime-root.test.mjs +22 -0
  49. package/core/state/stack-detect.mjs +102 -0
  50. package/core/state/stack-detect.test.mjs +64 -0
  51. package/core/surface/config-sync-backends.mjs +224 -0
  52. package/core/surface/connector-targets.mjs +205 -0
  53. package/core/surface/discover-hooks.mjs +151 -0
  54. package/core/surface/discover-hooks.test.mjs +63 -0
  55. package/core/surface/discover-surface.test.mjs +49 -0
  56. package/core/surface/extra-hosts.mjs +48 -0
  57. package/core/surface/extra-hosts.test.mjs +24 -0
  58. package/core/surface/hook-targets.mjs +102 -0
  59. package/core/surface/surface-copy-targets.mjs +37 -0
  60. package/core/surface/surface-inventory.mjs +106 -0
  61. package/core/surface/surface-matrix.mjs +133 -0
  62. package/core/surface/surface-matrix.test.mjs +90 -0
  63. package/docs/router-eval-set.jsonl +38 -0
  64. package/hooks.manifest.yaml +16 -0
  65. package/package.json +101 -0
  66. package/recipe.yaml +247 -0
  67. package/recipes/foundry.yaml +27 -0
  68. package/recipes/manifest.yaml +18 -0
  69. package/recipes/roles/backend-data.yaml +26 -0
  70. package/recipes/roles/coding.yaml +26 -0
  71. package/recipes/roles/frontend.yaml +14 -0
  72. package/recipes/roles/research.yaml +31 -0
  73. package/recipes/roles/secure.yaml +27 -0
  74. package/router.config.yaml +120 -0
  75. package/scripts/bundles.mjs +131 -0
  76. package/scripts/doctor.mjs +117 -0
  77. package/scripts/embedded-hooks.mjs +27 -0
  78. package/scripts/hosts.mjs +60 -0
  79. package/scripts/link/link-connectors.mjs +190 -0
  80. package/scripts/link/link-connectors.test.mjs +111 -0
  81. package/scripts/link/link-hooks.mjs +259 -0
  82. package/scripts/link/link-hooks.test.mjs +112 -0
  83. package/scripts/link/link-surfaces.mjs +188 -0
  84. package/scripts/link/link-surfaces.test.mjs +76 -0
  85. package/scripts/load.mjs +143 -0
  86. package/scripts/package-contract.test.mjs +26 -0
  87. package/scripts/surface-inventory.mjs +6 -0
  88. package/scripts/sync/agents-surface.mjs +55 -0
  89. package/scripts/sync/agents-surface.test.mjs +18 -0
  90. package/scripts/sync/auto-sync.mjs +135 -0
  91. package/scripts/sync/auto-sync.test.mjs +41 -0
  92. package/scripts/sync/import-surfaces.mjs +331 -0
  93. package/scripts/sync/import-surfaces.test.mjs +106 -0
  94. package/scripts/sync/sync-config.mjs +230 -0
  95. package/scripts/sync/sync-config.test.mjs +141 -0
  96. package/scripts/sync/sync-surfaces.mjs +229 -0
  97. package/scripts/sync/sync-surfaces.test.mjs +66 -0
  98. package/scripts/tui-actions.mjs +59 -0
  99. package/scripts/tui-actions.test.mjs +53 -0
  100. package/scripts/tui.mjs +782 -0
  101. package/scripts/uninstall-all.mjs +39 -0
  102. package/server/mcp-server.mjs +129 -0
  103. package/server/mcp-server.test.mjs +175 -0
  104. package/skills/portawhip/SKILL.md +80 -0
  105. package/surface-matrix.yaml +93 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable user-facing changes are documented here.
4
+
5
+ ## 0.1.0 - 2026-07-12
6
+
7
+ First public npm release.
8
+
9
+ - Added an interactive TUI for inventory, sync previews, connectors, hooks, enrichment, and capabilities.
10
+ - Added public `portawhip`, `portawhip-router`, and `harness-router` executables.
11
+ - Added hybrid lexical and local semantic capability routing with confidence-based abstention.
12
+ - Added cross-host connector, command, agent, skill, and supported-hook synchronization.
13
+ - Added guarded config-sync profiles, live doctor checks, route evaluations, and feedback-aware ranking.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VVeb1250
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ <div align="center">
2
+
3
+ # portawhip
4
+
5
+ **One control plane for the tools, skills, MCP servers, and hooks used by your AI coding agents.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/portawhip?color=cb3837&logo=npm)](https://www.npmjs.com/package/portawhip)
8
+ [![npm downloads](https://img.shields.io/npm/dm/portawhip?color=2f80ed)](https://www.npmjs.com/package/portawhip)
9
+ [![CI](https://github.com/VVeb1250/portawhip/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/VVeb1250/portawhip/actions/workflows/ci.yml)
10
+ [![Node.js 20+](https://img.shields.io/badge/Node.js-20%2B-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org/)
11
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
+
13
+ Stop loading every capability into every prompt. Portawhip discovers what is installed, keeps agent hosts aligned, and surfaces only the capability that fits the task.
14
+
15
+ </div>
16
+
17
+ ## Try it
18
+
19
+ Launch the interactive control surface without installing anything globally:
20
+
21
+ ```bash
22
+ npx portawhip
23
+ ```
24
+
25
+ Or route a task directly:
26
+
27
+ ```bash
28
+ npx --package portawhip portawhip-router route --prompt "inspect this PDF and extract its tables"
29
+ ```
30
+
31
+ Portawhip returns a short, actionable pointer—or nothing. Abstaining on weak matches is a feature, so unrelated prompts stay clean.
32
+
33
+ ## What it solves
34
+
35
+ AI agent setups drift quickly: one host knows about an MCP server, another has the useful skill, and every tool description competes for context. Portawhip gives that sprawl one lightweight control plane:
36
+
37
+ - **Discover** capabilities already installed across Claude Code, Codex, Gemini CLI, Cursor, VS Code/Copilot, OpenCode, Zed, Windsurf, Cline, Pi, and Amp.
38
+ - **Route** with a hybrid lexical + local semantic engine instead of dumping the full catalog into context.
39
+ - **Sync** tools, skills, commands, agents, MCP configuration, and supported hooks across hosts.
40
+ - **Stay safe** with read-only status/preview defaults and explicit confirmation for writes in the TUI.
41
+ - **Learn quietly** from whether suggestions were used, without sending prompts to a hosted model.
42
+
43
+ ## Three ways to use it
44
+
45
+ ### 1. Interactive TUI
46
+
47
+ ```bash
48
+ npx portawhip
49
+ ```
50
+
51
+ The six tabs cover overview, config sync, connectors, hooks, enrichment, and the capability catalog. Press `h` or `?` for the key map. Repair/remove actions require a second keypress.
52
+
53
+ ### 2. Router CLI
54
+
55
+ ```bash
56
+ npx --package portawhip portawhip-router list --type skill
57
+ npx --package portawhip portawhip-router route --prompt "run an accessibility-focused browser test"
58
+ ```
59
+
60
+ Dense retrieval uses a local multilingual embedding model and warms in the background. Add `--dense-block` when deterministic full semantic retrieval matters more than startup latency.
61
+
62
+ ### 3. MCP connector
63
+
64
+ Add this stdio server to any MCP-compatible host:
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "harness-router": {
70
+ "command": "npx",
71
+ "args": ["--yes", "--package", "portawhip", "harness-router"]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ The server exposes:
78
+
79
+ - `route(query)` — find the best installed capability for a concrete action.
80
+ - `list_all(type?)` — inspect the catalog, optionally filtered by capability type.
81
+
82
+ ## Install a managed workspace
83
+
84
+ For connector/hook linking and repeatable team configuration, install the repository locally:
85
+
86
+ ```bash
87
+ git clone https://github.com/VVeb1250/portawhip.git
88
+ cd portawhip
89
+ npm ci
90
+ npm run doctor
91
+ ```
92
+
93
+ Then inspect before writing:
94
+
95
+ ```bash
96
+ npm run connectors
97
+ npm run hooks
98
+ npm run sync-config:preview
99
+ ```
100
+
101
+ Link supported hosts only when the preview looks right:
102
+
103
+ ```bash
104
+ npm run connectors:link -- --scope global
105
+ npm run hooks:link -- --scope global
106
+ ```
107
+
108
+ Global connector and hook changes are a trust boundary. Back up host configuration first; Portawhip does not silently activate third-party embedded hooks.
109
+
110
+ ## How it works
111
+
112
+ ```text
113
+ installed tools + skills + MCP servers + agent surfaces
114
+
115
+
116
+ live capability registry
117
+
118
+ lexical + local semantic rank
119
+
120
+ confidence / intent gates
121
+
122
+ ┌──────────────┼──────────────┐
123
+ ▼ ▼ ▼
124
+ MCP pull CLI / TUI optional hooks
125
+ ```
126
+
127
+ The loader delegates installation to maintained tools (`add-mcp`, `mise`, and `agent-skill-manager`) instead of rebuilding package management. The router combines curated entries from `recipe.yaml` with live discovery, then applies confidence, intent, and per-lane peakedness gates. See [VISION.md](VISION.md) for the design rationale.
128
+
129
+ ## Useful commands
130
+
131
+ | Goal | Command |
132
+ | --- | --- |
133
+ | Health check | `npm run doctor` |
134
+ | Interactive TUI | `npm run tui` |
135
+ | Connector status | `npm run connectors` |
136
+ | Hook status | `npm run hooks` |
137
+ | Config-sync preview | `npm run sync-config:preview` |
138
+ | Import preview | `npm run import:preview` |
139
+ | Route evaluation | `npm run route:eval` |
140
+ | Full tests | `npm test` |
141
+
142
+ ## Supported surfaces
143
+
144
+ Support is capability-specific: some hosts expose MCP configuration but no native lifecycle hooks. Portawhip reports those lanes as `mcp-only` or `unsupported` instead of pretending they are linked. The evidence-backed matrix lives in [docs/host-support.md](docs/host-support.md).
145
+
146
+ ## Privacy and safety
147
+
148
+ - Routing runs locally; the dense model is downloaded and cached on first use.
149
+ - Status and preview commands are read-only by default.
150
+ - Broad config writes and all-skills writes are blocked.
151
+ - Runtime package fallback through unpinned `npx --yes` is opt-in.
152
+ - Host-native permission controls still govern actual tool execution.
153
+
154
+ Please report security issues through the private process in [SECURITY.md](SECURITY.md), not a public issue.
155
+
156
+ ## Contributing
157
+
158
+ Bug reports, host adapters, routing eval cases, and documentation improvements are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) and the open [issues](https://github.com/VVeb1250/portawhip/issues).
159
+
160
+ ## License
161
+
162
+ MIT © portawhip contributors. See [LICENSE](LICENSE).
package/SECURITY.md ADDED
@@ -0,0 +1,15 @@
1
+ # Security policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are applied to the latest published npm version and the default branch.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please use GitHub's private vulnerability reporting for this repository. Do not open a public issue or include secrets, tokens, private prompts, or host configuration in a report.
10
+
11
+ Include a concise impact statement, affected version, reproduction steps, and any suggested mitigation. You should receive an acknowledgement within seven days.
12
+
13
+ ## Scope and trust boundaries
14
+
15
+ Portawhip suggests and configures capabilities; it does not bypass the host's permission model. Connector and hook linking can modify host configuration, so those writes remain explicit and should be preceded by a backup. Embedded third-party hooks are inventoried but never activated automatically.
package/VISION.md ADDED
@@ -0,0 +1,281 @@
1
+ # Vision — portable-harness-v2
2
+
3
+ > This is the "why" and "destination" doc. `PLAN.md` is the current
4
+ > execution phase (Smart Capability Router, Step 2+). If they ever conflict,
5
+ > re-derive PLAN.md from this file — this one is the anchor.
6
+
7
+ ## 0. Where this came from
8
+
9
+ Predecessor: `E:\portable-harness` ("paw"). Self-diagnosed broken
10
+ (`docs/STATUS.md` in that repo) before this project started:
11
+
12
+ - **router.py (set-routing): 0/10** in its own dogfood test — could not be
13
+ fixed, no external library solves it either (researched, confirmed).
14
+ - **ICM memory: SQLite corruption**, no WAL/backup/repair — data loss.
15
+ - **`sets.json`: 3,618 lines hardcoded registry**, 40 Python modules
16
+ (`team_kernel.py`, `mutation.py`, `verification.py`, `memory_mesh.py`,
17
+ `decision_mirror.py`, `voice.py`, ...) — scope grew far past what was ever
18
+ proven useful daily.
19
+ - **Health checks overclaimed**: doctor said tools were "healthy" when not
20
+ even installed (`lychee`/`hurl`).
21
+ - **The old `skill-router.py` hook is still installed and running live** on
22
+ this machine (`.claude/hooks/skill-router.py`, `.codex/hooks/skill-router.py`)
23
+ — and it misfired constantly *during this very project's own planning
24
+ conversation* (suggested `finance-billing-ops`, `homelab-pihole-dns`,
25
+ `unified-notifications-ops` while discussing harness architecture — zero
26
+ relevant hits). This is not a hypothetical failure mode; it was observed
27
+ directly, live, as evidence.
28
+
29
+ Owner's read on why v1 failed (verbatim intent): "ไม่เห็นผล ซับซ้อนแบบ
30
+ hardcode และ scope กว้างเกิน ที่ทำมาก็พัง แถมรู้สึกว่ามีคนทำแล้วอีก" — no
31
+ visible results, hardcoded complexity, scope too broad, it broke, and it
32
+ duplicates things that already exist elsewhere.
33
+
34
+ ## 1. One-sentence destination
35
+
36
+ **A thin, dynamic control-plane that loads capabilities (MCP / CLI / skills)
37
+ into whatever AI agent hosts + OS the user actually has, by delegating to
38
+ the best existing tool per capability type — and separately, a router that
39
+ surfaces the *right* capability at the *right* moment instead of dumping
40
+ everything into context.**
41
+
42
+ Two halves, built in this order:
43
+
44
+ 1. **Loader** (done — Step 1): "load however, use anywhere." No install
45
+ logic of our own.
46
+ 2. **Router** (in progress — PLAN.md Phase 0-4): "surface the right thing at
47
+ the right time." No routing logic reinvented where a host already does
48
+ it natively.
49
+
50
+ ## 2. Non-negotiable principles (the anti-repeat-v1 rules)
51
+
52
+ - **Delegate, don't rebuild.** Every capability type gets mapped to an
53
+ existing, actively-maintained tool before we write a line of install/route
54
+ logic ourselves. We only own the thin glue between them.
55
+ - **Hardcoding data is fine; hardcoding decision logic is not.** A recipe
56
+ list (which bundle has which tools) is data — allowed. A router that
57
+ decides set-routing by static if/else rules — not allowed (that's what
58
+ killed v1's `router.py`).
59
+ - **Live-probe, never overclaim.** Health/doctor checks must actually run
60
+ the thing and observe the result, not assert a cached/static status.
61
+ (v1's `lychee`/`hurl` overclaim bug is the cautionary tale.)
62
+ - **No daemon unless a proven gap demands it.** CLI/library-first. A
63
+ background service is the last resort, not the default (this is why
64
+ Phase 0 picked "pointer-only" over embedding the Docker-based `metamcp`
65
+ gateway — see §5).
66
+ - **Proven-gap before adding scope.** Don't add a capability, a wrapper, or
67
+ a phase because it seems useful — add it because a specific, observed gap
68
+ demands it. (v1's team-kernel/mutation/verification grew this way and
69
+ were never dogfooded into daily use.)
70
+ - **Cross-host, cross-OS by detection, not by list.** Never hardcode "these
71
+ are our 2 supported hosts." Ask the actual backend tool what it detects
72
+ on this machine, every run.
73
+ - **Silence is a valid, expected output.** A router/suggester that has
74
+ nothing confident to say must say nothing — not a low-confidence guess.
75
+ (Directly answers the noisy-hook failure mode observed live in-session.)
76
+
77
+ ## 3. Ownership map (who owns what)
78
+
79
+ Mapped against the general "harness = 7 components" framing (agent loop,
80
+ tool interface, guides, sensors/verification, memory/context, permissions,
81
+ orchestration):
82
+
83
+ | component | owner | why |
84
+ |---|---|---|
85
+ | Agent loop | the host itself (Claude Code / Codex / Gemini / ...) | not ours to build |
86
+ | Tool interface — MCP | **add-mcp** (`neon-solutions/add-mcp`) | consumer-side installer, no dependency on the target server adopting any SDK; real cross-host config writer with reversible managed blocks; `detectGlobalAgents()` gives live host detection with zero hardcoding |
87
+ | Tool interface — non-MCP CLI | **mise** (`jdx/mise`) | cross-OS dev-tool version manager, single Rust binary, legit organic adoption (30k⭐ over 3.5y, not inflated) |
88
+ | Guides / skill content + cross-host delivery | **asm** (`agent-skill-manager`) for install/sync; **ECC** for content (detect-only, not owned) | asm is the actively-maintained one of 3 skill-sync candidates (openskills has more stars but 5.5mo silent — risk); ECC ships real, honestly-tiered (Native/Adapter-backed/Instruction-backed/Reference-only) cross-host skill content already |
89
+ | Memory | **ICM** | researched 11 alternatives (mem0, agentmemory, Letta, Zep, Graphiti, Cognee, txtai, Memori, SQLite-Memory, memoirs) — none beat it on the actual constraint set (no-daemon + CLI + local + cross-host + cheap adapter) simultaneously; the real defect (SQLite durability) was already fixed upstream |
90
+ | Suggestion timing (what to route, when) | **ours to own** — this is the actual gap nobody else fills well | see PLAN.md; this is the one genuinely-unsolved piece |
91
+ | Permissions / guardrails | host-native hooks + existing security CLIs (nah/gitleaks/osv-scanner, inherited from v1's registry) | not re-litigated in this project yet |
92
+ | Orchestration (multi-agent team) | **deprioritized**, not rebuilt | v1's `team_kernel.py`/`mutation.py` had passing tests but zero daily-use proof; out of scope until the loader+router prove themselves first |
93
+
94
+ ## 4. External stack, with the evidence that earned each pick
95
+
96
+ Every one of these was chosen after checking real GitHub metadata (age,
97
+ push recency, star trajectory sanity — not just star count, since inflated
98
+ repos exist: ECC's own 224k★/34k forks in 5.5 months was flagged as
99
+ statistically implausible and NOT used as a quality signal for its own
100
+ routing claims) and, where possible, actually running the tool:
101
+
102
+ - **add-mcp** — ⭐242, TS, verified live: installed a real MCP server
103
+ (context7) into `~/.claude.json` and `~/.codex/config.toml` in Step 1.
104
+ - **mise** — ⭐30,321, Rust, created 2023 (organic ~24★/day over 3.5y),
105
+ verified live: installed ripgrep cross-platform via scoop-provided binary.
106
+ - **asm** — ⭐687, TS, pushed same day as evaluation (most active of 3 skill
107
+ candidates), verified live: installed the `pdf` skill from
108
+ `anthropics/skills` into both Claude Code and Codex skill dirs, and it
109
+ showed up as an available skill in this session immediately after.
110
+ - **ICM** — existing, already fixed upstream (WAL+backup+`icm repair`,
111
+ landed 2026-07-01 in the predecessor repo).
112
+ - **ECC** — already installed on this machine; used for skill *content*
113
+ only, detect-first, not owned or rebuilt.
114
+ - Rejected: agent-connector (wrong side of the MCP relationship — built for
115
+ authoring/distributing your own server, not consuming third-party ones),
116
+ openskills (10.5k⭐ but 5.5 months silent — maintenance risk), 4 of 5
117
+ Phase-0 gateway candidates (dead 11-13 months), `metamcp` (only
118
+ actively-maintained gateway, but Docker-first web app, not an embeddable
119
+ library — rejected for Phase 0, see docs/phase0-audit.md).
120
+
121
+ ## 5. Decisions made and why (chronological, so the reasoning survives)
122
+
123
+ 1. **Kill `router.py` (set-routing).** 0/10 in v1's own dogfood; researched
124
+ and confirmed no external router library solves this class of problem
125
+ either. Not attempted again in v2.
126
+ 2. **`skill_router.py` hook: kill the always-on PUSH form, keep the idea as
127
+ on-demand PULL.** Directly observed misfiring live in this project's own
128
+ planning conversation. The underlying idea (suggest without injecting
129
+ full content) is sound and is now the actual spec for the Phase 0-4
130
+ router in PLAN.md — the fix is *timing/mode*, not the concept.
131
+ 3. **Language: Node/TS for the loader, not Python (v1's language).** 4 of 5
132
+ delegated tools are npm packages; gluing in the same runtime avoids
133
+ cross-runtime subprocess overhead. (Router core in PLAN.md is intentionally
134
+ left language-agnostic beyond "must interop.")
135
+ 4. **`cross-spawn` over hand-rolled `shell:true`/`.cmd` detection.** Hit a
136
+ real Windows `EINVAL` bug spawning `.cmd` files directly, and a real
137
+ command-injection lint (unescaped args through `shell:true`) — fixed with
138
+ the standard library for this exact problem instead of a bespoke hack.
139
+ 5. **No hardcoded host list anywhere in the loader.** `hosts.mjs` asks
140
+ add-mcp's `detectGlobalAgents()` and asm's `config show` for the live
141
+ truth every run. The only static table is an 11-entry id-translation map
142
+ between the two tools' naming (`claude-code` ↔ `claude`) — necessary glue
143
+ data, not decision logic.
144
+ 6. **Self-healing retry over a hardcoded capability matrix.** When add-mcp
145
+ rejected a batch install because Claude Desktop doesn't support remote/
146
+ http transport, the fix reads *add-mcp's own error text* to narrow the
147
+ retry — not a matrix we maintain ourselves.
148
+ 7. **Phase 0 gateway decision: B (pointer-only), not A (embed metamcp).**
149
+ No candidate was embeddable + maintained + Windows-clean simultaneously;
150
+ the token-bloat problem A would solve is already handled natively by
151
+ Claude Code's own tool deferral (observed working throughout this
152
+ project), and duplicating that for other hosts is an unproven-gap bet
153
+ until Codex/Gemini's own behavior is checked.
154
+ 8. **Threshold/k moved out of scorer.mjs into `router.config.yaml`.** First
155
+ Phase 1 draft hardcoded `threshold=1, k=5` as JS defaults — caught on
156
+ review ("hard code มั้ยเนี่ย"). PLAN.md's own spec required these live in
157
+ recipe header or a config file, not code. Fixed: `core/config.mjs` reads
158
+ `router.config.yaml` (optional, same fallback values), `scorer.mjs` now
159
+ takes threshold/k as required call args with no built-in default.
160
+ 9. **Registry auto-discovers installed capabilities, not just recipe.yaml's
161
+ 3 curated entries.** Same review flagged that recipe.yaml itself was a
162
+ hand-typed list — "ไม่ว่าโหลดจากไหนก็ลิงค์กัน" (link whatever's loaded,
163
+ regardless of source). `core/discover.mjs` now pulls live state from the
164
+ same backends Step 1 already uses (`add-mcp.listInstalledServers()`,
165
+ `asm list --json`, `mise ls --json`) and merges it into the index;
166
+ curated entries win on id collision. Keyword triggers for auto entries
167
+ are inferred from each skill's own description text (a stopword-filtered
168
+ frequency extraction, `core/discover.mjs`'s `extractKeywords`).
169
+ Consequence, caught by testing against the real machine (371 installed
170
+ skills): threshold=1 let a single generic shared word ("write", common
171
+ across many skill descriptions) fire on "write a poem about the ocean" —
172
+ the exact noise failure this project exists to avoid. Fix: two
173
+ thresholds, not one — `recipeThreshold=1` for curated/deliberate
174
+ triggers, `threshold=2` for auto-inferred ones (`router.config.yaml`).
175
+ Also fixed a real dedup bug: the same skill installed identically across
176
+ multiple hosts (e.g. `pdf` in Claude Code + Codex + Cursor) was appearing
177
+ as 3 separate entries — now first-seen-wins per id, matching the pattern
178
+ already used for MCP server discovery.
179
+ 10. **Claude Code never called `route()` in Phase 2's live test (0/8);
180
+ Codex did (8/8).** Root cause, confirmed live in-session: Claude Code
181
+ defers MCP tool schemas behind `ToolSearch` until looked up by name;
182
+ the CLAUDE.md instruction never told the model to do that lookup first,
183
+ so the tool sat undiscovered. Codex has no such deferral, so the same
184
+ plain instruction worked there unmodified. Fix: `adapters/instructions/
185
+ generate.mjs` now has a Claude-Code-specific block that names the
186
+ `ToolSearch` step explicitly; AGENTS.md keeps the plain wording (already
187
+ proven 8/8). Still needs a fresh-session live re-check — can't be proven
188
+ from within a running session.
189
+ 11. **User explicitly commissioned Phase 2.5 (hybrid retrieval)**, wanting a
190
+ precise router with visible results after v1's high-noise failure — not
191
+ scope creep, a deliberate ask. But the delivered version had 3 real
192
+ bugs, found by audit: (a) never wired into the actually-deployed
193
+ `server/mcp-server.mjs` (still called plain `scorer.mjs`) — fixed via
194
+ one shared `core/route-entry.mjs` used by both the CLI and the server;
195
+ (b) live eval re-run showed 2 false positives on the exact
196
+ old-hook-regression prompts the eval set exists to catch, and no
197
+ threshold value could fix it (false positives scored *higher* than the
198
+ weakest true positive) — fixed by improving the scoring itself
199
+ (shared stopwords + idf-scaled single-word trigger credit in
200
+ `core/sparse-retriever.mjs`), not by picking a bigger magic number; (c)
201
+ `graphPath` had the same cwd-relative bug already fixed once for
202
+ recipe.yaml — fixed the same way. See `docs/phase2.5-verify.md`'s
203
+ "Correction" section for the full audit trail.
204
+
205
+ ## 6. Current state (2026-07-04)
206
+
207
+ **Cross-host coverage: mostly closed, see `HANDOFF.md`'s table for the
208
+ current per-host matrix.** Codex built `adapters/hooks/universal-hook.mjs`
209
+ + `scripts/link-hooks.mjs`/`link-connectors.mjs`, extending native push/
210
+ feedback hooks to claude-code, codex, and gemini-cli, with cursor/
211
+ copilot-cli/vscode/claude-desktop honestly reported as `unsupported`/
212
+ `mcp-only` (no confirmed native hook API). A duplicate-hook-firing bug
213
+ from two parallel implementations (mine, project-scoped Codex's) was found
214
+ and fixed same day — consolidated onto `universal-hook.mjs` at global
215
+ scope. `HANDOFF.md` has the exact commands used; don't rebuild any of
216
+ this without checking that table first.
217
+
218
+ - **Step 1 — Loader: done, proven live.** `recipe.yaml` + `scripts/load.mjs`
219
+ + `scripts/hosts.mjs` + `scripts/doctor.mjs`. 3/3 real installs verified
220
+ (MCP, CLI, skill), idempotent re-run verified, dynamic host detection
221
+ verified (7 MCP hosts, 5 skill hosts found on this machine with zero
222
+ hardcoded list), self-healing transport-mismatch retry verified.
223
+ - **Phase 0 — Router base decision: done.** Option B chosen and recorded
224
+ (`docs/phase0-audit.md`).
225
+ - **Phase 1 — Registry + scorer core: done, proven.** `core/registry.mjs`
226
+ (parses `recipe.yaml`'s `route:` blocks, fails loud on malformed ones,
227
+ merges in `core/discover.mjs`'s live auto-discovery, caches normalized
228
+ index at `.hp-state/route-index.json`), `core/scorer.mjs` (layer-1
229
+ keyword/trigger match, per-origin abstain threshold), `core/config.mjs`
230
+ + `router.config.yaml` (tunable, not hardcoded), `core/router-cli.mjs`
231
+ (`route --prompt`, `list --type`). 9/9 unit tests green (7 curated-only +
232
+ 2 discovery-merge); manual check against this machine's real installed
233
+ base (371 skills via asm, live MCP servers via add-mcp, CLI tools via
234
+ mise): curated triggers fire correctly, real unrelated prompts abstain,
235
+ no cross-host duplicate entries.
236
+ - **Phase 2 — Pull-mode MCP server: done, mostly proven** (see
237
+ `docs/phase2-verify.md`). `server/mcp-server.mjs` exposes `route`/
238
+ `list_all`; registered globally on 7 detected hosts via the recipe.yaml/
239
+ loader path (dogfooded); `adapters/instructions/generate.mjs` installed
240
+ an idempotent one-liner into the user's real `~/.claude/CLAUDE.md` and
241
+ `~/.codex/AGENTS.md` (backed up first). Only the literal "fresh session
242
+ calls route() unprompted" behavioral check is still open — needs
243
+ observing in a real new session, not provable from within this one.
244
+ - **Phase 3 — Push-mode Claude Code adapter: done, precision bar
245
+ deliberately deferred** (see `docs/phase3-verify.md`).
246
+ `adapters/claude-code/push-hook.mjs` + `scripts/install-push-hook.mjs`;
247
+ installed live into `~/.claude/settings.json` (backed up first) alongside
248
+ the still-active old `skill-router.py` hook (not disabled yet — user
249
+ choice, verify first). Hard requirement (silence on non-match) 5/5.
250
+ Should-match eval only 3/5 clean; 2 known misses (pdf, literal "grep")
251
+ logged as Phase 4 feedback-loop targets rather than hand-tuned now.
252
+ OpenCode adapter: not started (stub only, no OpenCode detected on this
253
+ machine yet).
254
+ - **Phase 4 — Feedback loop: done (usage weighting), embeddings not
255
+ attempted** (see `docs/phase4-verify.md`). `core/feedback.mjs`
256
+ (append-only JSONL, bounded ×0.5–×2.0 factor), wired into push-hook,
257
+ router-cli, and mcp-server (not `eval`, kept deterministic). New
258
+ `adapters/claude-code/feedback-mark-hook.mjs` (PostToolUse) resolves
259
+ tool calls back to capability ids. Found and fixed a real bug in the
260
+ same pass: `route.binary` field added because `ripgrep`'s mise `source`
261
+ ("ripgrep") never matched its actual invoked command ("rg"). Demoed:
262
+ an ignored suggestion drops below its threshold and stops appearing
263
+ (observed in 2 ignores, not literally 5 — same mechanism, converges
264
+ faster on this query). Dense embedding rerank (PLAN.md item 3):
265
+ deliberately not attempted — optional in the plan, no proven gap yet
266
+ that keyword+hybrid can't cover.
267
+ - **Explicitly not rebuilt / out of scope for now:** team-kernel,
268
+ mutation/verification loop, multi-agent orchestration (all existed in v1
269
+ with passing tests but no daily-use proof — revisit only if the loader+
270
+ router prove themselves first and a specific gap demands it).
271
+
272
+ ## 7. Destination check (how we'll know we're not lost)
273
+
274
+ If a future change doesn't map to one of these, question it before building:
275
+
276
+ - Does it reduce tokens or improve suggestion precision, measurably?
277
+ - Is it delegated to an existing, evidence-checked tool wherever possible?
278
+ - Is any new "list" pure data, with zero new decision logic bolted on?
279
+ - Was the gap actually observed (dogfooded), not just imagined?
280
+ - Does it still run without a daemon, or is the daemon a proven, not
281
+ assumed, requirement?
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ // Copied to a fixed path OUTSIDE the harness repo (under the user's home
3
+ // directory) at install time — see ensureStub() in scripts/link-hooks.mjs.
4
+ // Host hook configs point here, not directly at universal-hook.mjs, so that
5
+ // deleting the harness repo turns every hook invocation into a silent
6
+ // no-op instead of a per-turn "module not found" error. This file itself
7
+ // carries no repo-specific path, so it never goes stale and never needs
8
+ // cleanup on uninstall.
9
+
10
+ import { existsSync } from "node:fs";
11
+ import { pathToFileURL } from "node:url";
12
+
13
+ function targetArg(argv) {
14
+ const i = argv.indexOf("--target");
15
+ return i === -1 ? null : argv[i + 1];
16
+ }
17
+
18
+ const target = targetArg(process.argv);
19
+ if (target && existsSync(target)) {
20
+ await import(pathToFileURL(target).href);
21
+ }
@@ -0,0 +1,43 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import spawnSync from "cross-spawn";
4
+ import { mkdtempSync, rmSync, writeFileSync, existsSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join, resolve } from "node:path";
7
+
8
+ const STUB = resolve("adapters", "hooks", "hook-stub.mjs");
9
+
10
+ test("hook-stub: missing target is a silent no-op, not an error (the whole point of the stub)", () => {
11
+ const dir = mkdtempSync(join(tmpdir(), "harness-stub-"));
12
+ try {
13
+ const missingTarget = join(dir, "does-not-exist.mjs");
14
+ const result = spawnSync.sync(process.execPath, [STUB, "--target", missingTarget], { encoding: "utf8" });
15
+ assert.equal(result.status, 0);
16
+ assert.equal(result.stderr.trim(), "");
17
+ } finally {
18
+ rmSync(dir, { recursive: true, force: true });
19
+ }
20
+ });
21
+
22
+ test("hook-stub: existing target is imported and runs", () => {
23
+ const dir = mkdtempSync(join(tmpdir(), "harness-stub-"));
24
+ try {
25
+ const markerFile = join(dir, "ran.txt");
26
+ const targetPath = join(dir, "target.mjs");
27
+ writeFileSync(
28
+ targetPath,
29
+ `import { writeFileSync } from "node:fs";\nwriteFileSync(${JSON.stringify(markerFile)}, "ran");\n`,
30
+ );
31
+ const result = spawnSync.sync(process.execPath, [STUB, "--target", targetPath], { encoding: "utf8" });
32
+ assert.equal(result.status, 0);
33
+ assert.ok(existsSync(markerFile));
34
+ } finally {
35
+ rmSync(dir, { recursive: true, force: true });
36
+ }
37
+ });
38
+
39
+ test("hook-stub: no --target arg is also a silent no-op", () => {
40
+ const result = spawnSync.sync(process.execPath, [STUB], { encoding: "utf8" });
41
+ assert.equal(result.status, 0);
42
+ assert.equal(result.stderr.trim(), "");
43
+ });