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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/SECURITY.md +15 -0
- package/VISION.md +281 -0
- package/adapters/hooks/hook-stub.mjs +21 -0
- package/adapters/hooks/hook-stub.test.mjs +43 -0
- package/adapters/hooks/universal-hook.mjs +348 -0
- package/adapters/hooks/universal-hook.test.mjs +294 -0
- package/adapters/instructions/generate.mjs +114 -0
- package/core/fixtures/bad-recipe.yaml +5 -0
- package/core/fixtures/capability-graph.json +10 -0
- package/core/fixtures/skill-with-metadata/SKILL.md +15 -0
- package/core/registry/capability-docs.mjs +145 -0
- package/core/registry/capability-graph-compiler.mjs +90 -0
- package/core/registry/capability-graph.mjs +53 -0
- package/core/registry/capability-kind.mjs +9 -0
- package/core/registry/cli-enrich.mjs +316 -0
- package/core/registry/cli-enrich.test.mjs +119 -0
- package/core/registry/discover.mjs +379 -0
- package/core/registry/enrich.mjs +213 -0
- package/core/registry/enrich.test.mjs +53 -0
- package/core/registry/eval-harvest.mjs +93 -0
- package/core/registry/eval-harvest.test.mjs +64 -0
- package/core/registry/registry.mjs +197 -0
- package/core/router/concept-vector.mjs +91 -0
- package/core/router/concept-vector.test.mjs +36 -0
- package/core/router/curated-trigger.test.mjs +28 -0
- package/core/router/dense-embedder.mjs +242 -0
- package/core/router/fusion.mjs +18 -0
- package/core/router/hybrid-router.mjs +375 -0
- package/core/router/intent-evidence.mjs +60 -0
- package/core/router/prompt-hygiene.mjs +31 -0
- package/core/router/route-entry.mjs +74 -0
- package/core/router/router-cli.mjs +218 -0
- package/core/router/router-cli.test.mjs +25 -0
- package/core/router/router-eval.mjs +166 -0
- package/core/router/router-live.test.mjs +89 -0
- package/core/router/router.test.mjs +838 -0
- package/core/router/scorer.mjs +72 -0
- package/core/router/sparse-retriever.mjs +79 -0
- package/core/router/tokenize.mjs +42 -0
- package/core/state/bundle-state.mjs +124 -0
- package/core/state/bundle-state.test.mjs +175 -0
- package/core/state/config.mjs +115 -0
- package/core/state/feedback.mjs +129 -0
- package/core/state/feedback.test.mjs +181 -0
- package/core/state/runtime-root.test.mjs +22 -0
- package/core/state/stack-detect.mjs +102 -0
- package/core/state/stack-detect.test.mjs +64 -0
- package/core/surface/config-sync-backends.mjs +224 -0
- package/core/surface/connector-targets.mjs +205 -0
- package/core/surface/discover-hooks.mjs +151 -0
- package/core/surface/discover-hooks.test.mjs +63 -0
- package/core/surface/discover-surface.test.mjs +49 -0
- package/core/surface/extra-hosts.mjs +48 -0
- package/core/surface/extra-hosts.test.mjs +24 -0
- package/core/surface/hook-targets.mjs +102 -0
- package/core/surface/surface-copy-targets.mjs +37 -0
- package/core/surface/surface-inventory.mjs +106 -0
- package/core/surface/surface-matrix.mjs +133 -0
- package/core/surface/surface-matrix.test.mjs +90 -0
- package/docs/router-eval-set.jsonl +38 -0
- package/hooks.manifest.yaml +16 -0
- package/package.json +101 -0
- package/recipe.yaml +247 -0
- package/recipes/foundry.yaml +27 -0
- package/recipes/manifest.yaml +18 -0
- package/recipes/roles/backend-data.yaml +26 -0
- package/recipes/roles/coding.yaml +26 -0
- package/recipes/roles/frontend.yaml +14 -0
- package/recipes/roles/research.yaml +31 -0
- package/recipes/roles/secure.yaml +27 -0
- package/router.config.yaml +120 -0
- package/scripts/bundles.mjs +131 -0
- package/scripts/doctor.mjs +117 -0
- package/scripts/embedded-hooks.mjs +27 -0
- package/scripts/hosts.mjs +60 -0
- package/scripts/link/link-connectors.mjs +190 -0
- package/scripts/link/link-connectors.test.mjs +111 -0
- package/scripts/link/link-hooks.mjs +259 -0
- package/scripts/link/link-hooks.test.mjs +112 -0
- package/scripts/link/link-surfaces.mjs +188 -0
- package/scripts/link/link-surfaces.test.mjs +76 -0
- package/scripts/load.mjs +143 -0
- package/scripts/package-contract.test.mjs +26 -0
- package/scripts/surface-inventory.mjs +6 -0
- package/scripts/sync/agents-surface.mjs +55 -0
- package/scripts/sync/agents-surface.test.mjs +18 -0
- package/scripts/sync/auto-sync.mjs +135 -0
- package/scripts/sync/auto-sync.test.mjs +41 -0
- package/scripts/sync/import-surfaces.mjs +331 -0
- package/scripts/sync/import-surfaces.test.mjs +106 -0
- package/scripts/sync/sync-config.mjs +230 -0
- package/scripts/sync/sync-config.test.mjs +141 -0
- package/scripts/sync/sync-surfaces.mjs +229 -0
- package/scripts/sync/sync-surfaces.test.mjs +66 -0
- package/scripts/tui-actions.mjs +59 -0
- package/scripts/tui-actions.test.mjs +53 -0
- package/scripts/tui.mjs +782 -0
- package/scripts/uninstall-all.mjs +39 -0
- package/server/mcp-server.mjs +129 -0
- package/server/mcp-server.test.mjs +175 -0
- package/skills/portawhip/SKILL.md +80 -0
- 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
|
+
[](https://www.npmjs.com/package/portawhip)
|
|
8
|
+
[](https://www.npmjs.com/package/portawhip)
|
|
9
|
+
[](https://github.com/VVeb1250/portawhip/actions/workflows/ci.yml)
|
|
10
|
+
[](https://nodejs.org/)
|
|
11
|
+
[](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
|
+
});
|