@klhapp/skillmux 1.11.2 → 1.13.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 (104) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +20 -10
  3. package/bin/skillmux.js +89 -0
  4. package/docs/README.md +4 -4
  5. package/docs/cli.md +15 -5
  6. package/docs/concepts.md +4 -3
  7. package/docs/configuration.md +22 -5
  8. package/docs/deployment.md +3 -3
  9. package/docs/getting-started.md +32 -7
  10. package/docs/releasing.md +17 -2
  11. package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
  12. package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
  13. package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
  14. package/docs/sdd/cli-remote-target-parity/think.md +183 -0
  15. package/docs/sdd/cli-surface-consistency/think.md +149 -0
  16. package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
  17. package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
  18. package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
  19. package/docs/sdd/feat-packaging-deployment/review.md +95 -0
  20. package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
  21. package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
  22. package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
  23. package/docs/sdd/npm-native-binaries/review.md +103 -0
  24. package/docs/sdd/npm-native-binaries/spec.md +148 -0
  25. package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
  26. package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
  27. package/docs/sdd/runtime-resource-hardening/review.md +86 -0
  28. package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
  29. package/docs/sdd/security-hardening-phase/think.md +129 -0
  30. package/docs/sdd/security-observability/review.md +96 -0
  31. package/docs/sdd/security-observability/spec.md +55 -0
  32. package/docs/sdd/skill-provenance-update/review.md +152 -0
  33. package/docs/sdd/skill-provenance-update/schema.json +193 -0
  34. package/docs/sdd/skill-provenance-update/spec.md +134 -0
  35. package/docs/sdd/supply-chain-hardening/review.md +50 -0
  36. package/docs/sdd/supply-chain-hardening/spec.md +42 -0
  37. package/docs/sdd/target-marker-rehome/spec.md +33 -0
  38. package/docs/skill-management.md +1 -1
  39. package/docs/troubleshooting.md +18 -2
  40. package/package.json +15 -8
  41. package/src/adapters.ts +0 -438
  42. package/src/audit.ts +0 -21
  43. package/src/cli.ts +0 -928
  44. package/src/clients.ts +0 -368
  45. package/src/commands/audit.ts +0 -85
  46. package/src/commands/config.ts +0 -224
  47. package/src/commands/context.ts +0 -104
  48. package/src/commands/core.ts +0 -56
  49. package/src/commands/doctor.ts +0 -97
  50. package/src/commands/eval.ts +0 -88
  51. package/src/commands/init.ts +0 -672
  52. package/src/commands/install.ts +0 -155
  53. package/src/commands/local-vault.ts +0 -60
  54. package/src/commands/models.ts +0 -10
  55. package/src/commands/outdated.ts +0 -115
  56. package/src/commands/project.ts +0 -580
  57. package/src/commands/report.ts +0 -66
  58. package/src/commands/scan.ts +0 -71
  59. package/src/commands/shared.ts +0 -38
  60. package/src/commands/skill.ts +0 -33
  61. package/src/commands/sync.ts +0 -233
  62. package/src/commands/target.ts +0 -225
  63. package/src/commands/update.ts +0 -277
  64. package/src/completions.ts +0 -196
  65. package/src/concurrency-limiter.ts +0 -61
  66. package/src/config-service.ts +0 -398
  67. package/src/config-watcher.ts +0 -253
  68. package/src/config.ts +0 -583
  69. package/src/context.ts +0 -188
  70. package/src/db-audit.ts +0 -286
  71. package/src/db-index.ts +0 -238
  72. package/src/db.ts +0 -3
  73. package/src/deployment.ts +0 -39
  74. package/src/doctor.ts +0 -205
  75. package/src/eval.ts +0 -302
  76. package/src/global-flags.ts +0 -46
  77. package/src/init-agents.ts +0 -329
  78. package/src/init-instructions.ts +0 -192
  79. package/src/init.ts +0 -389
  80. package/src/install.ts +0 -232
  81. package/src/lifecycle.ts +0 -51
  82. package/src/logger.ts +0 -26
  83. package/src/manifest.ts +0 -352
  84. package/src/mcp-registration.ts +0 -89
  85. package/src/metrics.ts +0 -121
  86. package/src/models.ts +0 -20
  87. package/src/output.ts +0 -214
  88. package/src/project-setup.ts +0 -36
  89. package/src/prompts.ts +0 -124
  90. package/src/provenance.ts +0 -99
  91. package/src/rate-limiter.ts +0 -137
  92. package/src/readiness.ts +0 -30
  93. package/src/redact.ts +0 -52
  94. package/src/router-core.ts +0 -666
  95. package/src/rrf.ts +0 -31
  96. package/src/scan.ts +0 -300
  97. package/src/server.ts +0 -930
  98. package/src/setup.ts +0 -145
  99. package/src/snapshot.ts +0 -135
  100. package/src/stats.ts +0 -340
  101. package/src/sync.ts +0 -482
  102. package/src/toml-writer.ts +0 -51
  103. package/src/types.ts +0 -212
  104. package/src/vault.ts +0 -207
@@ -0,0 +1,185 @@
1
+ <!-- plane_project: SMX -->
2
+ <!-- plane_module: fetch-outcome-flywheel -->
3
+
4
+ # Spec: Fetch-outcome routing quality flywheel
5
+
6
+ ## Goal
7
+
8
+ Skillmux records what it proposed and never what the agent took. `resolve_skill`
9
+ writes an audit row (`src/router-core.ts:406`, `src/db.ts:292`) holding the
10
+ query, retrieval capability, candidates, and latency. `fetch_skill` writes
11
+ nothing. Every quality number the project can produce today comes from the
12
+ hand-curated holdout set in `eval/queries.json`.
13
+
14
+ Close the loop. Log fetch outcomes, correlate each one to the resolve that
15
+ produced it, and turn real traffic into two things that do not exist yet: a
16
+ measured acceptance signal in `skillmux report`, and a path to grow the eval set
17
+ from observed behavior instead of hand authorship.
18
+
19
+ Because that correlation joins raw user queries to the skills people actually
20
+ opened, the audit store also gets its own file, a retention window, and a
21
+ cleanup path in this same change.
22
+
23
+ ## Storage
24
+
25
+ Audit data moves out of `index.sqlite3` and into `audit.sqlite3`, in the same
26
+ `state_dir`. Both files sit under the directory operators already persist
27
+ (`docs/deployment.md:292`), so deployment and backup instructions gain a file
28
+ rather than a procedure.
29
+
30
+ The split is cheap because audit data is already self-contained. The only read
31
+ is a plain `SELECT ... FROM audit` (`src/stats.ts:150`) with no join to `skills`
32
+ or `vectors`, and `computeStats` derives per-skill counts from the stored
33
+ candidates JSON rather than from the index. The resolve-to-fetch join added here
34
+ is audit-to-audit, so both tables live in the new file and never reach across.
35
+
36
+ Separating the files buys four things that motivate doing it now rather than
37
+ later:
38
+
39
+ 1. Copying or backing up the derived index no longer carries raw user queries.
40
+ 2. Retention and file permissions can differ between telemetry and index state.
41
+ 3. Reclaiming space after a prune no longer locks the index. The new file is
42
+ created with `PRAGMA auto_vacuum = INCREMENTAL`, which the existing file
43
+ cannot be given without a full rewrite.
44
+ 4. Audit writes stop contending with an index rebuild for the single WAL writer.
45
+
46
+ Paths resolve as they do today: `~/.local/state/skillmux/audit.sqlite3` for a CLI
47
+ install (`src/config.ts:111`), `/data/audit.sqlite3` in a container
48
+ (`src/config.ts:255`).
49
+
50
+ ## Acceptance Criteria
51
+
52
+ ### Storage and migration
53
+
54
+ 1. Audit data is read and written in `audit.sqlite3` under `state_dir`, opened
55
+ separately from `index.sqlite3`, with WAL journaling and
56
+ `auto_vacuum = INCREMENTAL` set at creation.
57
+ 2. On first open after upgrade, existing `audit` rows in `index.sqlite3` are
58
+ copied into `audit.sqlite3` within a single transaction and the old table is
59
+ dropped. The migration is idempotent: a second run finds nothing to move and
60
+ changes nothing.
61
+
62
+ ### Correlation
63
+
64
+ 3. A `resolve_skill` response carries a `request_id` string, unique per request,
65
+ in both the text content and `structuredContent`. The audit row written for
66
+ that resolve stores the same value.
67
+ 4. Audit rows written before this feature, which have no `request_id`, still
68
+ load, report, and render without error.
69
+ 5. `fetch_skill` accepts an optional `request_id`. When it names a known resolve,
70
+ the recorded fetch links to that resolve's audit row.
71
+ 6. `fetch_skill` called without a `request_id` succeeds and records an
72
+ uncorrelated fetch.
73
+ 7. `fetch_skill` called with an unknown or malformed `request_id` succeeds and
74
+ records an uncorrelated fetch. Skill delivery never fails because telemetry
75
+ could not correlate.
76
+ 8. Each recorded fetch stores timestamp, `skill_id`, `request_id` or null,
77
+ originating audit row id or null, and `rank_at_resolve`. `rank_at_resolve` is
78
+ the fetched skill's rank in that resolve's shortlist, or null when the fetch
79
+ is uncorrelated or the skill was absent from that shortlist.
80
+
81
+ ### Reporting
82
+
83
+ 9. `skillmux report` reports `acceptance_rate`, `observed_mrr`, and
84
+ `top1_acceptance_rate` over resolves that returned at least one candidate. A
85
+ resolve counts as accepted when at least one correlated fetch exists.
86
+ `observed_mrr` uses the reciprocal rank of the first fetched candidate.
87
+ 10. When a window contains no correlated fetches, `report` marks the acceptance
88
+ signal unavailable rather than printing `0.000`, and states how many
89
+ uncorrelated fetches it saw.
90
+ 11. `report` lists the top queries that returned candidates and received no
91
+ correlated fetch, as a list distinct from the existing empty-shortlist list.
92
+
93
+ ### Retention and cleanup
94
+
95
+ 12. Configuration exposes `audit.retention_days`, defaulting to 90. A value of
96
+ `0` disables pruning and retains rows indefinitely. Invalid values are
97
+ rejected at config load in the existing error style.
98
+ 13. Pruning deletes resolve rows and fetch rows older than the window, each by
99
+ its own timestamp. A fetch whose resolve row has been pruned reports as
100
+ uncorrelated rather than erroring, reusing the path that already exists for
101
+ fetches that carried no `request_id`.
102
+ 14. Pruning runs once at server startup and at most once per 24 hours while the
103
+ server runs. It never blocks readiness and never blocks a resolve.
104
+ 15. `skillmux audit prune [--older-than <window>] [--dry-run] [--yes] [--json]`
105
+ prunes on demand and reports rows removed per table. `--dry-run` performs no
106
+ writes and reports what would be removed. `--older-than` accepts the window
107
+ syntax already used by `--since` (`src/stats.ts:4`).
108
+ 16. After a prune, incremental vacuum reclaims file space, and the operation
109
+ never takes a lock on `index.sqlite3`.
110
+
111
+ ### Eval promotion
112
+
113
+ 17. `skillmux eval promote --since <window> --yes` writes eval cases shaped
114
+ `{ query, split: "observed", relevant_skill_ids }`, where the skill ids are
115
+ those actually fetched, deduplicated by normalized query. It writes to a path
116
+ other than `eval/queries.json` unless one is given explicitly, and warns that
117
+ promoted cases contain raw user queries. Output validates under
118
+ `parseEvalCases` (`src/eval.ts:15`).
119
+ 18. `eval promote` never rewrites an existing case. A query already present in
120
+ the target file is skipped and counted in the summary.
121
+
122
+ ### Cross-cutting
123
+
124
+ 19. `report`, `audit prune`, and `eval promote` honor `--json` against the
125
+ documented output schema. Existing exit-code behavior is unchanged.
126
+
127
+ ## Scope
128
+
129
+ - A separate `audit.sqlite3` store with an `openAudit(stateDir)` entry point
130
+ beside `openIndex`, and a one-time transactional migration of existing rows.
131
+ - `request_id` generation, storage, and wire exposure on `resolve_skill`.
132
+ - Optional `request_id` input on `fetch_skill` (`src/server.ts:112`) and a fetch
133
+ audit table, following the migration pattern already in `src/db.ts:52`.
134
+ - New quality metrics in `StatsResponse` (`src/stats.ts:23`), the text renderer,
135
+ and the JSON output.
136
+ - `audit.retention_days` configuration, automatic pruning, and a
137
+ `skillmux audit prune` subcommand.
138
+ - `skillmux eval promote` producing `EvalCase` records.
139
+ - Contract and docs updates: `docs/schema.json`, `docs/mcp-routing.md` (tool
140
+ contract, recommended agent behavior, audit data), `docs/cli.md` (report, the
141
+ new audit command, eval), and `docs/deployment.md` (the second state file).
142
+
143
+ ## Out of Scope
144
+
145
+ - **Time-window heuristic correlation.** Correlation is by explicit `request_id`
146
+ only. Guessing which resolve a bare fetch belongs to manufactures signal that
147
+ reads as measurement, which is worse than a stated gap.
148
+ - **Query redaction and targeted deletion.** Retention is age-based only. There
149
+ is no facility to delete one user's queries or to hash queries at rest.
150
+ - Any use of observed data to change ranking automatically. No retraining, no
151
+ threshold tuning, no reweighting. This feature measures and proposes, and a
152
+ human promotes.
153
+ - Per-client or per-context routing profiles.
154
+ - Changes to the retrieval pipeline, fusion, or reranker behavior.
155
+ - Exporting audit data to an external log or metrics pipeline.
156
+ - Any dashboard or web view. `report` and `--json` are the surface.
157
+
158
+ ## Tasks
159
+
160
+ Ordered so that later slices land in the new store rather than being moved
161
+ twice.
162
+
163
+ 1. **Split the audit store.** Add `openAudit(stateDir)`, create `audit.sqlite3`
164
+ with WAL and incremental auto-vacuum, migrate existing rows out of
165
+ `index.sqlite3` transactionally, and repoint `src/stats.ts` and
166
+ `src/router-core.ts` at the new handle.
167
+ 2. **`request_id` on resolve.** Add the column, generate the id in
168
+ `router-core`, thread it into the audit insert and both response channels,
169
+ and update `docs/schema.json`.
170
+ 3. **Fetch outcome logging.** Add the fetch table and insert helper, the optional
171
+ `request_id` on the `fetch_skill` input schema, a correlation lookup that
172
+ degrades to uncorrelated on any miss, and rank resolution against the stored
173
+ candidates.
174
+ 4. **Retention and cleanup.** Add `audit.retention_days`, the prune routine with
175
+ incremental vacuum, the startup and daily triggers, and the
176
+ `skillmux audit prune` subcommand.
177
+ 5. **Quality metrics in report.** Extend `computeStats`, the renderer, and the
178
+ JSON shape with acceptance rate, observed MRR, top-1 acceptance, unused
179
+ shortlist queries, and the signal-unavailable case.
180
+ 6. **`skillmux eval promote`.** Window parsing reusing `parseSince`, case
181
+ construction, dedupe against the target file, the `--yes` gate, `--dry-run`,
182
+ and `--json`.
183
+ 7. **Docs pass.** MCP routing contract and audit section, CLI report, audit and
184
+ eval sections, deployment note on the second state file, and the standing note
185
+ that `fetch_skill` remains usable without a prior resolve.
@@ -0,0 +1,103 @@
1
+ # Review — npm-native-binaries
2
+
3
+ Branch `feat/npm-native-binaries`, 7 commits, 24 files, +1243/-79.
4
+ Fan-out: compliance, security, correctness, performance, tests, build, suite.
5
+
6
+ ## Verdict
7
+
8
+ Spec 15/15. `bunx tsc --noEmit` clean. Suite 954/954.
9
+
10
+ ## AC Status
11
+
12
+ | Issue | Criterion | Status | Notes |
13
+ |---|---|---|---|
14
+ | SMX-142 | build:binaries compiles five targets through the sharp stub | ✅ Done | `tests/build-binaries.test.ts` builds all five |
15
+ | SMX-143 | build:npm-packages emits platform packages with os, cpu, bin, 0o755 | ✅ Done | `tests/package-npm-binaries.test.ts` |
16
+ | SMX-144 | Platform packages carry onnxruntime libraries, darwin-x64 exempt | ✅ Done | Verified against the real payload, not a fixture |
17
+ | SMX-145 | Root package.json points bin at the launcher and pins five optional deps | ✅ Done | Verified from a packed tarball |
18
+ | SMX-146 | Polyglot shebang runs under bun-only and node-only | ✅ Done | Both directions covered with PATH narrowed |
19
+ | SMX-147 | Launcher resolves the package, forwards argv, exit code and signals | ✅ Done | Also verified in both global install layouts |
20
+ | SMX-148 | Launcher injects the library path so local inference works | ✅ Done | `embeddings: 1 backfilled` from the packaged binary |
21
+ | SMX-149 | SKILLMUX_BINARY overrides the resolved executable | ✅ Done | |
22
+ | SMX-150 | Missing platform package fails loudly | ✅ Done | |
23
+ | SMX-151 | Dockerfile entrypoint is the binary, healthcheck drops bun and node | ✅ Done | |
24
+ | SMX-152 | full ships libonnxruntime.so.1, slim stays lexical | ⚠️ Manual-verify | Verified on linux/arm64 only; amd64 needs CI |
25
+ | SMX-153 | docker-smoke.sh passes for slim and full on amd64 and arm64 | ⚠️ Manual-verify | Both arm64 lanes pass locally. amd64 verified only as far as the cross-compiled executable running under emulation; the full lane needs CI |
26
+ | SMX-154 | npm pack ships bin/skillmux.js and not src/ | ✅ Done | |
27
+ | SMX-155 | Release workflow publishes platform packages before the root | ✅ Done | Ordering asserted in `tests/release-workflow.test.ts` |
28
+ | SMX-156 | All five executables attached to the GitHub Release | ✅ Done | Added mid-implementation, see back-edge below |
29
+
30
+ Issue keys are listed so `sdd-cleanup` does not have to match criteria to
31
+ issues by title. Use this column rather than the titles.
32
+
33
+ The post-review run posted the two MANUAL-VERIFY verdicts and none of the
34
+ thirteen PASS ones. That was first read as a title-matching failure, which
35
+ was wrong. The comment timestamps show only two writes, two seconds apart,
36
+ both successful, so it was not a rate limit either; and SMX-152 matched
37
+ despite its criterion text not being a substring of its title, while SMX-149
38
+ went uncommented despite being a clean substring. The discriminator was the
39
+ verdict value, not the text: the recipe skipped every PASS row. Fixed in
40
+ sdd-cleanup on 2026-09-14, along with a verdict-line format violation and a
41
+ missing Plane rate-limit guard.
42
+
43
+ ## Findings
44
+
45
+ ### Accepted and fixed
46
+
47
+ **`bun run build` produced an executable that cannot embed.** The root `build`
48
+ script still ran a raw `bun build --compile`, which skips the sharp stub, so the
49
+ executable it produced silently lost local inference. `ci.yml` and `README.md`
50
+ both invoke it. `build` now routes through `scripts/build-binaries.ts host`.
51
+
52
+ The review reported this as "the test will fail", which was not true: the suite
53
+ was green before and after, because the script string still matched its
54
+ assertion. The defect was real; the stated symptom was not.
55
+
56
+ ### Rejected
57
+
58
+ **`SKILLMUX_BINARY` is spawned without validation** (security, Medium). Not a
59
+ vulnerability. An attacker who can set environment variables for your shell can
60
+ already run code as you; `PATH` and `NODE_OPTIONS` are the same shape. esbuild
61
+ ships `ESBUILD_BINARY_PATH` with identical semantics. Validating it would remove
62
+ the escape hatch the troubleshooting docs point at without closing any path.
63
+
64
+ **The launcher misses the package under a global install** (correctness, High).
65
+ Tested rather than argued. Installed the packed tarball into a temporary npm
66
+ prefix and placed the platform package in both layouts npm produces, then ran
67
+ the launcher from `/` so the working directory could not contribute:
68
+
69
+ ```
70
+ 佈局 A hoisted, lib/node_modules/@klhapp/skillmux-darwin-arm64 -> 1.12.0
71
+ 佈局 B nested, .../skillmux/node_modules/@klhapp/... -> 1.12.0
72
+ ```
73
+
74
+ `require.resolve` with `paths: [here]` walks every ancestor `node_modules`, which
75
+ covers both. The finding also reviewed a stale diff, since the `build` fix was
76
+ still uncommitted when the pass ran.
77
+
78
+ ## Needs decision
79
+
80
+ **`bin` in the platform packages.** Discovered while testing the global install:
81
+ installing a platform package globally alongside the root package fails with an
82
+ `EEXIST` on `$PREFIX/bin/skillmux`, because both declare `bin.skillmux`. The
83
+ normal flow never hits this, since npm links bins only for the package being
84
+ installed, not its dependencies.
85
+
86
+ esbuild declares `bin` in its platform packages so a package manager can link the
87
+ native executable directly and skip the JavaScript shim on machines without
88
+ Node. Our polyglot shebang already removes that need, so the field buys little
89
+ and can conflict. Removing it contradicts the spec, which names `bin` explicitly
90
+ in AC2, so this is left as written pending a decision.
91
+
92
+ ## Back-edge to spec
93
+
94
+ One, recorded during implementation. The launcher's missing-package message
95
+ sends every platform to the releases page, but only the two Linux executables
96
+ were attached there. The spec gained an acceptance criterion for attaching all
97
+ five before the change was made, rather than widening scope silently.
98
+
99
+ ## Test flake
100
+
101
+ One run of the suite reported a single failure that did not recur across three
102
+ subsequent clean runs, and the failing test name was not captured. Unresolved
103
+ and unattributed. Worth watching on CI rather than assuming it is gone.
@@ -0,0 +1,148 @@
1
+ ## Goal
2
+
3
+ Ship Skillmux as native per-platform executables on npm so that `npm install`,
4
+ `npx`, and `bun add -g` all work with no runtime prerequisite, while keeping
5
+ local ONNX embedding inference working on every platform that has an
6
+ onnxruntime build.
7
+
8
+ ## Acceptance Criteria
9
+
10
+ - [ ] `bun run build:binaries` compiles five executables into `dist/bin/` for
11
+ `bun-darwin-arm64`, `bun-darwin-x64`, `bun-linux-x64`, `bun-linux-arm64`,
12
+ and `bun-windows-x64`, each built through a bundler plugin that resolves
13
+ `sharp` to `scripts/stubs/sharp.ts`.
14
+ - [ ] `bun run build:npm-packages` emits `dist/npm/@klhapp/skillmux-<platform>-<arch>/`
15
+ for each target, where `<platform>` is `process.platform` (`win32`, not
16
+ `windows`), each containing a `package.json` whose `version` equals the root
17
+ `version` and which declares `os`, `cpu`, and `bin`, plus the executable at
18
+ mode `0o755`.
19
+ - [ ] Each platform package except `@klhapp/skillmux-darwin-x64` also contains
20
+ that platform's onnxruntime runtime libraries; `darwin-x64` ships without
21
+ them because `onnxruntime-node` publishes no `darwin/x64` artifacts.
22
+ - [ ] Root `package.json` sets `bin` to `{ "skillmux": "./bin/skillmux.js" }`
23
+ and lists all five platform packages under `optionalDependencies` pinned to
24
+ the exact root version.
25
+ - [ ] `bin/skillmux.js` starts with a `#!/bin/sh` polyglot line that execs
26
+ whichever of `bun` or `node` exists, and runs correctly with neither `node`
27
+ nor `bun` absent from `PATH` in turn.
28
+ - [ ] The launcher resolves the platform package via `require.resolve` against
29
+ `__dirname`, `dirname(process.argv[1])`, and `process.cwd()`, forwards
30
+ `process.argv.slice(2)` with `stdio: "inherit"`, mirrors the child exit code,
31
+ and forwards `SIGINT` and `SIGTERM`.
32
+ - [ ] The launcher prepends the platform package's library directory to
33
+ `DYLD_FALLBACK_LIBRARY_PATH` on darwin, `LD_LIBRARY_PATH` on linux, and
34
+ `PATH` on win32, so that `skillmux index` against a local-inference config
35
+ reports `embeddings: N backfilled` rather than a dlopen failure.
36
+ - [ ] `SKILLMUX_BINARY` overrides the resolved executable path.
37
+ - [ ] With no matching platform package installed, the launcher exits non-zero
38
+ with a message naming the missing package and the standalone download
39
+ procedure, and never falls back to a partially working mode silently.
40
+ - [ ] `Dockerfile` sets `ENTRYPOINT ["/usr/local/bin/skillmux"]`, keeps
41
+ `CMD ["serve", "--transport", "http"]`, and its `HEALTHCHECK` probes
42
+ `http://127.0.0.1:3000/health/ready` without invoking `bun` or `node`.
43
+ - [ ] The `full` stage ships `libonnxruntime.so.1` with `LD_LIBRARY_PATH` set;
44
+ the `slim` stage does not ship it, preserving `retrieval_capability`
45
+ `hybrid` for full and `lexical` for slim.
46
+ - [ ] `bash tests/docker-smoke.sh` passes unchanged for both `slim` and `full`
47
+ on `linux/amd64` and `linux/arm64`.
48
+ - [ ] `npm pack --dry-run` on the root package lists `bin/skillmux.js` and does
49
+ not list `src/`.
50
+ - [ ] The release workflow publishes all five platform packages before the root
51
+ package, and GitHub Release assets keep the existing `skillmux-linux-amd64`
52
+ and `skillmux-linux-arm64` names.
53
+ - [ ] All five executables are attached to the GitHub Release, not only the two
54
+ Linux ones. The launcher's missing-package message sends users of every
55
+ platform to that page, so a macOS or Windows reader must find something
56
+ there.
57
+
58
+ ## Scope
59
+
60
+ The binary build and packaging scripts, the launcher, the `sharp` build-time
61
+ stub, root `package.json` metadata, the `Dockerfile` migration to the compiled
62
+ binary, `tests/package-metadata.test.ts` and `tests/dockerfile.test.ts`
63
+ updates, release workflow changes, and installation documentation in
64
+ `README.md`, `docs/getting-started.md`, `docs/deployment.md`, and
65
+ `docs/troubleshooting.md`.
66
+
67
+ ## Out of Scope
68
+
69
+ Porting the codebase off Bun to Node. Changing retrieval, routing, config, or
70
+ CLI behavior. Restoring local inference on `darwin-x64`. Publishing
71
+ `win32-arm64`, which `bun build --compile` does not target. Changing the
72
+ remote-inference or reranker paths. Altering the GHCR and Docker Hub tagging
73
+ scheme.
74
+
75
+ ## Tasks
76
+
77
+ 1. Add the `sharp` stub and `scripts/build-binaries.ts`, with a test asserting
78
+ all five executables are produced and the host one reports the root version.
79
+ 2. Add `scripts/package-npm-binaries.ts` and the platform-package layout test,
80
+ including the onnxruntime library payload and `darwin-x64` exemption.
81
+ 3. Write `bin/skillmux.js` and its launcher tests: runtime selection, argv and
82
+ exit-code forwarding, signal forwarding, library-path injection, override,
83
+ and the missing-package error.
84
+ 4. Switch root `package.json` to the launcher and update
85
+ `tests/package-metadata.test.ts`.
86
+ 5. Migrate the `Dockerfile` to the compiled binary and update
87
+ `tests/dockerfile.test.ts`; verify both smoke lanes.
88
+ 6. Extend the release workflow to build and publish the platform packages.
89
+ This must also keep the root `optionalDependencies` pins in step with the
90
+ version Release Please writes, or every release ships a root package
91
+ pointing at the previous release's platform packages. The pins are exact,
92
+ and `tests/package-metadata.test.ts` fails when they drift, so a stale pin
93
+ turns the Release Please PR red rather than shipping quietly.
94
+ 7. Update the installation and troubleshooting documentation. This must state
95
+ which platforms get local inference and which fall back to lexical recall,
96
+ and must say why `darwin-x64` is the exception, including that the exception
97
+ is expected to be temporary. See "darwin-x64 is an upstream regression"
98
+ below.
99
+
100
+ ## Verified Constraints
101
+
102
+ These were established empirically on 2026-09-13 against Skillmux 1.12.0 with
103
+ Bun 1.4.2 and Node v24.21.0. They are the reason the design looks the way it
104
+ does; re-deriving them costs an hour.
105
+
106
+ - A bun-compiled binary cannot load `onnxruntime-node` unaided. The `.node`
107
+ addon is embedded and extracted to a temp path, but its dylib is not, and
108
+ `@rpath/libonnxruntime.1.24.3.dylib` then resolves to that temp directory.
109
+ Pointing `DYLD_FALLBACK_LIBRARY_PATH` at the real library directory fixes it.
110
+ - `--external` does not help. A compiled binary performs no `node_modules`
111
+ resolution at runtime; an external specifier fails as
112
+ `Cannot find package '<name>' imported from /$bunfs/root/<binary>`. The same
113
+ applies to a nested bare import reached through an absolute-path dynamic
114
+ import.
115
+ - `@huggingface/transformers` eagerly requires `sharp`, which text embedding
116
+ never uses. Replacing it with a throwing stub at bundle time is what makes
117
+ `models download` and `index` succeed in the compiled binary.
118
+ - `bun install` honours `os` and `cpu` for registry packages; installing
119
+ `esbuild` pulls only `@esbuild/darwin-arm64`. `npm` rejects a mismatched
120
+ platform package with `notsup`.
121
+ - `bun add -g` symlinks the `bin` JS file directly, so a `#!/usr/bin/env node`
122
+ shebang fails on a Bun-only machine. The `#!/bin/sh` polyglot line runs under
123
+ bun-only, node-only, and both.
124
+
125
+ ## darwin-x64 is an upstream regression, not a permanent limit
126
+
127
+ `onnxruntime-node` 1.24.3 ships `bin/napi-v6/darwin/arm64` but no `darwin/x64`
128
+ directory at all, which is why that target needs the `onnxruntime-node` stub to
129
+ compile and loses local inference. This is not a deliberate platform drop.
130
+ It is tracked upstream as a packaging regression introduced in the 1.23.x
131
+ series, where the Intel macOS artifact is still built in CI but does not reach
132
+ the npm tarball:
133
+
134
+ - https://github.com/microsoft/onnxruntime/issues/27961
135
+ - https://github.com/huggingface/transformers.js/issues/1634
136
+
137
+ `onnxruntime-node` 1.23.2 is the last release that carries the Intel macOS
138
+ binding.
139
+
140
+ `hasNativeOnnxruntime` probes the filesystem rather than consulting a hardcoded
141
+ list, so if a future `onnxruntime-node` restores `darwin/x64`, that target stops
142
+ being stubbed and starts carrying its library payload with no code change. Keep
143
+ it that way. The alternative, vendoring the 1.23.2 binding into the darwin-x64
144
+ package alone, mixes ONNX Runtime versions across platforms and should only be
145
+ considered if the upstream regression proves permanent.
146
+
147
+ <!-- plane_project: SMX -->
148
+ <!-- plane_module: npm-native-binaries -->
@@ -0,0 +1,56 @@
1
+ # PR 3 private implementation packet: calibration removal
2
+
3
+ This file is internal, ignored, and must never be staged or committed.
4
+
5
+ ## Goal
6
+
7
+ Remove Skillmux's obsolete threshold-calibration product surface and all code
8
+ used only to implement it. Ranking evaluation remains. Existing
9
+ `calibrate.sqlite3` files on operator disks are never opened, migrated, edited,
10
+ or deleted by the new code.
11
+
12
+ ## Required behavior
13
+
14
+ 1. `skillmux calibrate ...` is removed from command dispatch, help, Docker
15
+ command policy, completions, and public adapter interfaces. Invoking it must
16
+ fail non-zero with a concise 2.0 migration message that says threshold
17
+ calibration was removed and points users to `skillmux eval` for ranking
18
+ evaluation.
19
+ 2. Remove calibration from local/remote capability types and responses. Delete
20
+ calibration admin endpoint declarations and handlers rather than returning
21
+ 501 placeholders. Requests to the old routes follow the server's ordinary
22
+ unknown-route behavior.
23
+ 3. Remove calibration-only implementation: dataset generator, dataset decision
24
+ parser, optimizer, certification gates, resume/checkpoint/persistence,
25
+ calibration timing/retry plumbing, apply/config mutation, and calibration
26
+ fingerprints or watchers that have no non-calibration consumer.
27
+ 4. Remove decision-threshold code that has no remaining runtime consumer.
28
+ Do not alter ranked resolution, retrieval, reranking, evaluation, audit
29
+ reporting, or skill delivery behavior.
30
+ 5. Remove `inference.calibration` from accepted configuration. A legacy
31
+ `[inference.calibration]` block must fail with an actionable message saying
32
+ it is obsolete in 2.0 and should be deleted. Existing obsolete threshold
33
+ errors remain actionable.
34
+ 6. Remove calibration-only source files and tests. Update shared tests rather
35
+ than weakening unrelated assertions. Ranking-evaluation tests remain.
36
+ 7. Remove calibration references from current CLI help, completions, server
37
+ capabilities, generated public schema, configuration examples, and focused
38
+ command/API documentation required for truthful current surfaces. Leave the
39
+ broad documentation rewrite and migration guide to PR 4 unless a statement
40
+ would otherwise advertise a command or endpoint that no longer exists.
41
+ 8. Do not unlink, rename, migrate, truncate, or write any existing
42
+ `calibrate.sqlite3`. Add a regression test proving startup/doctor/eval leaves
43
+ a sentinel calibration database byte-for-byte unchanged.
44
+ 9. Do not stage or commit anything under `docs/sdd/`. Preserve all ignored
45
+ local artifacts.
46
+
47
+ ## Verification
48
+
49
+ - Start with failing public-interface tests for the removed command,
50
+ capabilities/routes, legacy config error, and untouched sentinel database.
51
+ - Work in red-green-refactor cycles and commit each logical cycle.
52
+ - Search tracked source, tests, schemas, examples, CLI help, and completions for
53
+ stale executable calibration surfaces.
54
+ - Run `bun test`, `bun x tsc --noEmit`, `npm run build`, and `git diff --check`.
55
+ - Return commits, deleted files, verification totals, and any intentionally
56
+ deferred documentation references.
@@ -0,0 +1,41 @@
1
+ ## Goal
2
+
3
+ Make deterministic built-in target directories an application rule rather than
4
+ duplicated absolute paths in the Skillmux manifest.
5
+
6
+ ## Acceptance Criteria
7
+
8
+ - [ ] `agent-skills`, `claude-code`, and `codex` targets resolve their native
9
+ directories from their names when syncing, managing projects, or rendering
10
+ target information.
11
+ - [ ] New or rewritten manifest entries for those built-in targets omit `dir`.
12
+ - [ ] Existing manifests that contain a built-in target `dir` remain readable
13
+ and continue to resolve the built-in native directory, without using a
14
+ non-standard persisted path as an override.
15
+ - [ ] A custom target requires `dir`, and its configured directory remains the
16
+ path used by all target operations.
17
+ - [ ] The CLI exposes a deliberate migration path that removes redundant
18
+ built-in `dir` fields without changing target markers, managed links, or
19
+ project pins.
20
+ - [ ] Tests cover built-in resolution, legacy manifest compatibility, custom
21
+ target behavior, and migration idempotence.
22
+
23
+ ## Scope
24
+
25
+ Manifest parsing, serialization, target-directory resolution, relevant CLI
26
+ output and operations, documentation, and migration tests.
27
+
28
+ ## Out of Scope
29
+
30
+ Changing native directory conventions, moving target directories or markers,
31
+ retargeting skills, changing custom target paths, or automatically mutating a
32
+ manifest during ordinary sync.
33
+
34
+ ## Tasks
35
+
36
+ 1. Define the manifest and target-resolution contract, including compatibility
37
+ with legacy built-in `dir` fields.
38
+ 2. Add tests for resolution and an explicit idempotent migration command.
39
+ 3. Route target, sync, init, and project operations through the shared
40
+ resolver and update manifest serialization.
41
+ 4. Document the built-in/custom distinction and migration workflow.
@@ -0,0 +1,86 @@
1
+ # Review: Runtime Resource Hardening
2
+
3
+ Branch `feat/runtime-resource-hardening` vs `main`, 5 commits (a6ec66a, 933315c, 8b7d66b, ac27004,
4
+ 654974c — the last is the post-review fix described below).
5
+ Reviewed manually — `goose run --recipe sdd-review` hit the same 24h Bifrost token-quota
6
+ exhaustion seen during slice 1 (`token limit exceeded (279006/250000, resets every 24h)`);
7
+ fell back to the skill's documented manual path (sequential focus passes + native test/build runs).
8
+
9
+ ## Test Execution
10
+
11
+ `bun test`: **787 pass, 0 fail, 2006 expect() calls** (58 files, 24.14s) at initial review; **792 pass, 0 fail, 2013 expect() calls** after the post-review fix commit (5 new unit tests for `releaseOnStreamClose`).
12
+
13
+ ## Build Check
14
+
15
+ `bunx tsc --noEmit`: **clean**, exit 0.
16
+
17
+ ## Compliance
18
+
19
+ | Criterion | Status | Notes |
20
+ |---|---|---|
21
+ | AC1: default bounds (1 MiB body, 100 concurrent) apply with no `[server]` config | ✅ Done | `?? DEFAULT_MAX_BODY_BYTES` / `?? DEFAULT_MAX_CONCURRENT_REQUESTS`; the 413-default test runs against a config with no `[server]` section at all, confirming this is the true zero-config path, not an overridden one. |
22
+ | AC2: oversized body → 413 before full buffering | ✅ Done | Native `Bun.serve` `maxRequestBodySize`; verified empirically earlier this session (413 returned pre-handler). |
23
+ | AC3: once `max_concurrent_requests` in-flight, next request → 503 | ✅ Done | Fixed post-review (commit 654974c): `releaseOnStreamClose` now defers the release until the response body stream actually drains or is cancelled, so long-lived SSE connections hold their slot for their real duration. See Correctness finding below for detail and why HTTP-level round-trip coverage wasn't added. |
24
+ | AC4: both bounds configurable via `[server]` | ✅ Done | zod schema + wiring; tested with `max_body_bytes = 100`, `max_concurrent_requests = 0`. |
25
+ | AC5: `allowed_hosts` unset → embedding/reranker unaffected | ✅ Done | `assertHostAllowed` no-ops on `undefined`/`[]`; covered by the unset-allowlist reranker test and pre-existing embedding tests. |
26
+ | AC6: disallowed embedding host → `RemoteInferenceError(configuration)` pre-fetch | ✅ Done | Tested; asserts zero requests reached the mock fetch. |
27
+ | AC7: disallowed reranker host → same | ✅ Done | Tested; asserts zero requests reached the mock fetch. |
28
+ | AC8: allowed host → proceeds unchanged | ✅ Done | Tested for both embedding and reranker. |
29
+
30
+ No schema.json exists for this feature (config-shape-only change, not a new API surface) — consistent with the spec's own scope.
31
+
32
+ ## Security
33
+
34
+ - Host-allowlist reuse (`assertHostAllowed`/`extractHost`) is unmodified from slice 1 (already reviewed then); no new bypass surface — exact-match on `URL.hostname`, so userinfo tricks (`http://evil@allowed.example.com/`) and suffix tricks (`allowed.example.com.evil.com`) don't bypass it.
35
+ - Reranker's host check is skipped when `docs.length === 0` (early return before the check) — harmless, since that path never reaches `fetch` either.
36
+ - Concurrency/body-size checks run before CORS, rate-limiting, and auth — correct ordering for resource protection (cheapest, most attacker-facing check first).
37
+ - The SSE connection-exhaustion gap (the "hold the connection open" attack a concurrency bound is meant to stop) had a real security dimension — see Correctness below; it's now fixed.
38
+
39
+ ## Correctness — Resolved (was Needs Decision)
40
+
41
+ User chose option 1 (wrap the stream body to release on close/cancel). Implemented in commit
42
+ 654974c as `releaseOnStreamClose` (`src/concurrency-limiter.ts`), wired into the fetch handler's
43
+ try/catch in place of the immediate `finally`-release. Verified with 5 new unit tests
44
+ (`tests/concurrency-limiter.test.ts`): held-while-open, released-on-full-drain,
45
+ released-on-cancel, no-body short-circuits immediately, release-at-most-once (idempotent).
46
+
47
+ An HTTP-level round-trip regression test (open a real SSE stream via the MCP transport, assert
48
+ 503 while held, cancel, assert 200 after) was attempted and abandoned: Bun's own `fetch()` client,
49
+ called from the same process as the server under test, does not resolve with response headers
50
+ until the first body chunk is enqueued — so a deliberately-idle open SSE stream (no data pushed)
51
+ never lets the client-side `fetch()` promise resolve, and the test hangs. Confirmed via a minimal
52
+ Bun.serve probe that a real out-of-process client (`curl`) *does* receive headers immediately for
53
+ the same handler shape — this is a same-process test-harness limitation specific to Bun's `fetch()`,
54
+ not a gap in production behavior. Retained to Hindsight (`infra` bank) as a gotcha for future
55
+ streaming-endpoint tests in this repo.
56
+
57
+ **Original finding (for the record): concurrency slot released on Response construction, not stream closure, for SSE responses.**
58
+
59
+ `src/server.ts`'s wrapper:
60
+ ```ts
61
+ if (!concurrencyLimiter.tryAcquire()) { ... }
62
+ try {
63
+ return await handleHttpRequest(req, server);
64
+ } finally {
65
+ concurrencyLimiter.release();
66
+ }
67
+ ```
68
+ `handleHttpRequest` resolves as soon as it returns a `Response` — including a `Response(readableStream, { headers: {"Content-Type": "text/event-stream"} })` from the MCP SDK's `webStandardStreamableHttp.js` (used for both the GET SSE notification channel and POST streaming replies). The `finally` fires at that point, not when the stream is actually drained/canceled by the client disconnecting. Net effect: `max_concurrent_requests` bounds the *synchronous handling* portion of a request, not the *lifetime of an open streaming connection* — the code's own comment ("protects against connection exhaustion") oversells what's implemented, and an attacker opening many SSE streams isn't actually capped by this bound.
69
+
70
+ This wasn't caught by tests because the added HTTP-level test for AC3 deliberately uses `max_concurrent_requests = 0` (a synchronous-rejection check) rather than a real concurrent-streaming scenario — a reasonable choice to avoid a flaky test, but it means the streaming gap has no regression coverage either.
71
+
72
+ **Options** (design decision, not auto-fixed):
73
+ 1. Wrap the returned `Response`'s body (when it's a `ReadableStream`) so `release()` fires on stream `cancel()`/close instead of on `Response` construction — closes the real gap, adds complexity to the wrapper.
74
+ 2. Accept current behavior as "bounds concurrent request-*processing* work, not connection lifetime," and correct the comment/docs to say so precisely, deferring true streaming-connection bounding to a later slice.
75
+ 3. Add a separate, explicit cap on concurrent SSE streams (distinct from the request-processing counter) if connection exhaustion is the primary threat model for this transport.
76
+
77
+ ## Test Coverage (static)
78
+
79
+ Adequate. `releaseOnStreamClose` is covered directly at the unit level (5 tests); the wiring itself
80
+ (`res.body → releaseOnStreamClose(res.body, () => concurrencyLimiter.release())`) is a single line
81
+ verified by `tsc --noEmit` and the unchanged 413/503 HTTP tests staying green (buffered-body
82
+ responses drain through the wrapper almost instantly, so existing behavior is unaffected).
83
+
84
+ ## Performance
85
+
86
+ No concerns — `ConcurrencyLimiter.tryAcquire`/`release` are O(1) synchronous counter ops; `maxRequestBodySize` is native to Bun with no manual buffering.