@kontextmind/kxm 0.7.188 → 0.7.190
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/.claude-plugin/marketplace.json +1 -1
- package/docs/README.md +1 -0
- package/docs/contributing/ci-and-release.md +1 -1
- package/docs/contributing/development.md +2 -2
- package/docs/contributing/writing-docs.md +9 -2
- package/docs/evals/planner-bakeoff-2026-10/judge/argon.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/judge/journal.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/judge/reviewer.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/judge/weekly.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/judge/wiki.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/judge/writer2.json +18 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/argon.txt +30 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/journal.txt +22 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/reviewer.txt +28 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/shared.txt +20 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/weekly.txt +23 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/wiki.txt +27 -0
- package/docs/evals/planner-bakeoff-2026-10/prompts/writer2.txt +40 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/argon-fable.txt +68 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/argon-sonnet.txt +69 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/journal-fable.txt +60 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/journal-sonnet.txt +79 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/reviewer-fable.txt +74 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/reviewer-sonnet.txt +95 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/weekly-fable.txt +63 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/weekly-sonnet.txt +66 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/wiki-fable.txt +72 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/wiki-sonnet.txt +79 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/writer2-fable.txt +73 -0
- package/docs/evals/planner-bakeoff-2026-10/raw/writer2-sonnet.txt +79 -0
- package/docs/evals/planner-bakeoff-2026-10/score.py +304 -0
- package/docs/evals/planner-bakeoff-2026-10.md +164 -0
- package/package.json +2 -2
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/scripts/check-plan-links.d.mts +4 -0
- package/scripts/check-plan-links.mjs +234 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
## Outcome
|
|
2
|
+
Replace writer #2 `pi-qwen3-coder-plus-openrouter` with `pi-qwen3-coder-next-openrouter` (model `openrouter/qwen/qwen3-coder-next`, vendor alibaba, permission edit, harness pi). Writer #1 and #3–#6 stay in their current order. The old route is retired, not deleted.
|
|
3
|
+
|
|
4
|
+
**Why qwen3-coder-next:**
|
|
5
|
+
- It is a Qwen model, so the OpenRouter route is allowed. Qwen has no native harness.
|
|
6
|
+
- It is the closest successor to a coder-specialised model.
|
|
7
|
+
- At 0.12 / 0.80 it is the cheapest candidate.
|
|
8
|
+
- It has a flat price, so there is no long-context premium like qwen3.7-plus's 0.96 / 3.84 tier at 256000 tokens and above.
|
|
9
|
+
- Its 262144 context is smaller than the alternatives. That is acceptable for edit-sized writer prompts.
|
|
10
|
+
|
|
11
|
+
**Tool-support assumption (labeled):** the packet does not show `supported_parameters`. I assume `qwen3-coder-next` supports tools because it is a coder model. The writer must verify this against the live OpenRouter models API before admitting the route (see Risks and guards).
|
|
12
|
+
|
|
13
|
+
**Fallback if tool support is not shown:** `qwen/qwen3.7-plus` is the second choice. It would need the same check, and the premium tier must be recorded.
|
|
14
|
+
|
|
15
|
+
## Files
|
|
16
|
+
- `.kxm/models/pi-qwen3-coder-next-openrouter.yaml`: schema `kxm.model.v2`, harness pi, vendor alibaba, permissions include edit. Add alias `qwen-next-openrouter-pi`.
|
|
17
|
+
- Model inventory file: add the new id. This must come before the routes change.
|
|
18
|
+
- Routes file: add the new route as admitted. Mark `pi-qwen3-coder-plus-openrouter` retired in a comment, remove it from the admitted list, and remove it from the writer roster so #2 is the new route. Keep the old alias out of the active lists.
|
|
19
|
+
- Inventory: keep the old id with a retired comment. Do not reuse its alias.
|
|
20
|
+
- `.kxm/prices.yaml`: add the `qwen/qwen3-coder-next` row at 0.12 / 0.80. Keep the `qwen/qwen3-coder-plus` row at its last list price. Update the date and sha256 together.
|
|
21
|
+
- Writer roster definition (assignment runner): swap #2 only.
|
|
22
|
+
- `plans/evidence/<date>-writer-2-qwen3-coder-next.md`: evidence note.
|
|
23
|
+
|
|
24
|
+
## Tests
|
|
25
|
+
- Roster-policy tests: update the hardcoded #2 id. Assert that #1 and #3–#6 are unchanged and in order (Argon high, Argon medium, Flash medium, Flash high).
|
|
26
|
+
- Add a test that the retired route is not admitted and not on the roster.
|
|
27
|
+
- Add a test that the new id exists in inventory before routes admit it. This should fail if the order is wrong.
|
|
28
|
+
- Prices loader test: the sha256 and date match, and the old row is still present.
|
|
29
|
+
- Brake test: no `moonshotai/*` route through Pi/OpenRouter.
|
|
30
|
+
- Model file schema test: `kxm.model.v2`, vendor alibaba, edit permission.
|
|
31
|
+
- Run `npm run verify`.
|
|
32
|
+
|
|
33
|
+
## Docs
|
|
34
|
+
Update every place that names writer #2 or `qwen3-coder-plus`:
|
|
35
|
+
- harness-routing
|
|
36
|
+
- config reference
|
|
37
|
+
- CLI reference
|
|
38
|
+
- assignment runner doc
|
|
39
|
+
- glossary, only if it names the route
|
|
40
|
+
- changelog
|
|
41
|
+
- `plans/implementation-plan.md` Tracking (Decided and the relevant slice)
|
|
42
|
+
- generated `dist`, only if a grep shows the string embedded. If it is, regenerate rather than hand-edit.
|
|
43
|
+
|
|
44
|
+
Do not rewrite historical evidence or changelog entries that describe the old route as it was.
|
|
45
|
+
|
|
46
|
+
## Risks and guards
|
|
47
|
+
- **Tool support is unproven in the packet.** Before admission, the writer must check `supported_parameters` for tools in the live models API. If it is absent, do not admit. Fall back to the next candidate that shows tools, or leave #2 vacant with the gap recorded. Never admit on assumption alone.
|
|
48
|
+
- **Moonshot `kimi-k2.7-code` is refused.** Moonshot has a native kimi harness, and billing a native vendor through Pi/OpenRouter is the brake case. It is the only candidate that falls under the brake.
|
|
49
|
+
- **Meta `muse-spark-open` is not auto-admitted.** It is an admission question, and it is also the most expensive per token.
|
|
50
|
+
- **Price pin:** the loader refuses a date or sha256 mismatch. Update both in the same commit and recompute the hash after the final edit.
|
|
51
|
+
- **Order dependency:** inventory must contain the id before routes admit it.
|
|
52
|
+
- **404 risk repeats:** record the verification date, 2026-10-17, in the evidence note.
|
|
53
|
+
- **No dual names or alias for the retired route.** Old names fail closed.
|
|
54
|
+
|
|
55
|
+
## Sequence
|
|
56
|
+
1. Verify `qwen3-coder-next` live and tool-capable. Record the result in the evidence note.
|
|
57
|
+
2. Add the model file.
|
|
58
|
+
3. Add the inventory entry, with the retired comment on the old id.
|
|
59
|
+
4. Add the prices row, then update the date and recompute the sha256.
|
|
60
|
+
5. Update routes: admit the new route, retire the old one, update the roster.
|
|
61
|
+
6. Update the tests.
|
|
62
|
+
7. Update the docs, changelog and Tracking.
|
|
63
|
+
8. Regenerate `dist` if needed.
|
|
64
|
+
9. Run `npm run verify`.
|
|
65
|
+
|
|
66
|
+
## Non-goals
|
|
67
|
+
- Do not change writer #1, the planner, `reviewer-cli`, or `reviewer-arch`.
|
|
68
|
+
- Do not reorder Argon or Flash.
|
|
69
|
+
- Do not add a harness preferences overlay.
|
|
70
|
+
- Do not delete the old price row.
|
|
71
|
+
- Do not add a compatibility alias.
|
|
72
|
+
- Do not run a side-by-side model comparison.
|
|
73
|
+
|
|
74
|
+
## Open questions
|
|
75
|
+
- Does `qwen/qwen3-coder-next` list tool support? The packet does not show `supported_parameters` for any candidate.
|
|
76
|
+
- What is the exact evidence-note filename convention, and where do the inventory and routes files live?
|
|
77
|
+
- What is the last list price of `qwen3-coder-plus`? It is not in the packet, so keep the row as it is.
|
|
78
|
+
- Does effort stay medium for the new route? I assume yes.
|
|
79
|
+
- Is a 262144 context enough for assignment prompts? This is unmeasured.
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Score the committed planner bake-off, or refuse to invent a new one.
|
|
3
|
+
|
|
4
|
+
Scoring reads raw plans, blinded judge JSON, and the price catalogs.
|
|
5
|
+
It does not call a model.
|
|
6
|
+
|
|
7
|
+
``--run`` is the regeneration path. It does not run unless the native
|
|
8
|
+
``claude`` binary is on PATH and ``claude auth status`` succeeds, or
|
|
9
|
+
``ANTHROPIC_API_KEY`` is set. This checkout had neither, so the committed
|
|
10
|
+
plans came from Cursor subagents. See the report for that transport.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import math
|
|
18
|
+
import re
|
|
19
|
+
import shutil
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
ROOT = Path(__file__).resolve().parent
|
|
24
|
+
REPO = ROOT.parents[2]
|
|
25
|
+
RAW = ROOT / "raw"
|
|
26
|
+
JUDGE = ROOT / "judge"
|
|
27
|
+
PROMPTS = ROOT / "prompts"
|
|
28
|
+
|
|
29
|
+
TASKS = ("argon", "reviewer", "writer2", "weekly", "wiki", "journal")
|
|
30
|
+
MODELS = ("fable", "sonnet")
|
|
31
|
+
DIMS = ("correctness", "completeness", "risk", "sequencing", "conciseness")
|
|
32
|
+
|
|
33
|
+
# Blind labels from random.seed(20261017), shuffled per task.
|
|
34
|
+
LABELS = {
|
|
35
|
+
"argon": {"A": "fable", "B": "sonnet"},
|
|
36
|
+
"reviewer": {"A": "sonnet", "B": "fable"},
|
|
37
|
+
"writer2": {"A": "fable", "B": "sonnet"},
|
|
38
|
+
"weekly": {"A": "sonnet", "B": "fable"},
|
|
39
|
+
"wiki": {"A": "sonnet", "B": "fable"},
|
|
40
|
+
"journal": {"A": "fable", "B": "sonnet"},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Checklist over the full raw plan. A hit is a case-insensitive regex.
|
|
44
|
+
# This is a scripted cross-check, not the official score. The official
|
|
45
|
+
# score is the blinded gpt-5.6-sol judge.
|
|
46
|
+
CHECKS: dict[str, list[tuple[str, str]]] = {
|
|
47
|
+
"argon": [
|
|
48
|
+
("argon high on the roster", r"gemini-4-argon-high|argon-high"),
|
|
49
|
+
("argon medium on the roster", r"gemini-4-argon-medium|argon-medium"),
|
|
50
|
+
("argon tried before flash", r"argon-medium[\s\S]{0,80}flash|argon-high[\s\S]{0,120}flash"),
|
|
51
|
+
("grok stays primary", r"grok-grok-4-7"),
|
|
52
|
+
("flash medium stays", r"flash-medium|gemini-3-8-flash-medium"),
|
|
53
|
+
("flash high stays", r"flash-high|gemini-3-8-flash-high"),
|
|
54
|
+
("no OpenRouter Google route", r"openrouter"),
|
|
55
|
+
("list price 2 and 10", r"\$2|inputPerMillion: 2|2 / 10|\$2 and \$10"),
|
|
56
|
+
("sha256 pin", r"sha256"),
|
|
57
|
+
("planner stays fable", r"claude-fable"),
|
|
58
|
+
("harness-routing doc", r"harness-routing"),
|
|
59
|
+
("tracking updated", r"implementation-plan|Tracking"),
|
|
60
|
+
],
|
|
61
|
+
"reviewer": [
|
|
62
|
+
("new primary id", r"codex-gpt-6-1-sol"),
|
|
63
|
+
("5.6 stays last", r"codex-gpt-5-6-sol"),
|
|
64
|
+
("pi fallback stays", r"pi-qwen3-8-flash"),
|
|
65
|
+
("pro is an alias", r"alias"),
|
|
66
|
+
("tier boundary", r"272000"),
|
|
67
|
+
("sha256 pin", r"sha256"),
|
|
68
|
+
("arch critic unchanged", r"reviewer-arch"),
|
|
69
|
+
("native brake", r"openai-codex|openrouter"),
|
|
70
|
+
("agents table", r"AGENTS\.md"),
|
|
71
|
+
("harness-run scripts", r"harness-run"),
|
|
72
|
+
],
|
|
73
|
+
"writer2": [
|
|
74
|
+
("old route retired", r"retired"),
|
|
75
|
+
("old price row kept", r"qwen3-coder-plus"),
|
|
76
|
+
("moonshot refused", r"moonshot|kimi"),
|
|
77
|
+
("evidence note", r"plans/evidence"),
|
|
78
|
+
("sha256 pin", r"sha256"),
|
|
79
|
+
("argon order untouched", r"[Aa]rgon"),
|
|
80
|
+
("tool gate", r"tool"),
|
|
81
|
+
("inventory before admit", r"inventory"),
|
|
82
|
+
("qwen3.7-plus considered", r"qwen3\.7-plus"),
|
|
83
|
+
("meta not auto-admitted", r"meta|muse"),
|
|
84
|
+
],
|
|
85
|
+
"weekly": [
|
|
86
|
+
("workflow file", r"\.github/workflows"),
|
|
87
|
+
("routes file untouched", r"routes\.yaml"),
|
|
88
|
+
("auth failure fails closed", r"401|403"),
|
|
89
|
+
("date and hash together", r"sha256"),
|
|
90
|
+
("cli-only rows kept", r"CLI-only|pi`, `grok`"),
|
|
91
|
+
("pr body lists ids", r"added"),
|
|
92
|
+
("no auto-merge", r"auto-merge|gh pr merge"),
|
|
93
|
+
("unit tests", r"[Tt]est"),
|
|
94
|
+
("harness-routing doc", r"harness-routing"),
|
|
95
|
+
("changelog", r"CHANGELOG"),
|
|
96
|
+
],
|
|
97
|
+
"wiki": [
|
|
98
|
+
("under .kxm/knowledge", r"\.kxm/knowledge"),
|
|
99
|
+
("compile stays off", r"wiki-compile|ingest"),
|
|
100
|
+
("architecture page", r"architecture"),
|
|
101
|
+
("glossary link", r"glossary"),
|
|
102
|
+
("ci pattern", r"CI / required|control-plane"),
|
|
103
|
+
("runbooks", r"runbook"),
|
|
104
|
+
("no new npm script", r"npm script"),
|
|
105
|
+
("doc links", r"docs/README"),
|
|
106
|
+
("not inside compiler wiki/", r"wiki/"),
|
|
107
|
+
("no secrets", r"secret"),
|
|
108
|
+
],
|
|
109
|
+
"journal": [
|
|
110
|
+
("no new store", r"lands a store|no store|does not land a store|not land a store|No changes to store"),
|
|
111
|
+
("workflow_not_found locked", r"workflow_not_found"),
|
|
112
|
+
("no relabel", r"relabel"),
|
|
113
|
+
("design note", r"design note|runtime-journal"),
|
|
114
|
+
("no invented version bump", r"version"),
|
|
115
|
+
("tracking", r"Tracking|implementation-plan"),
|
|
116
|
+
("no auto-promote", r"auto-promot"),
|
|
117
|
+
("refusal test", r"[Tt]est"),
|
|
118
|
+
],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def estimate_tokens(text: str) -> int:
|
|
123
|
+
"""Ceiling of characters divided by 4. Not a provider tokenizer."""
|
|
124
|
+
return math.ceil(len(text) / 4)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def load_fable_rates() -> tuple[float, float]:
|
|
128
|
+
text = (REPO / ".kxm" / "prices.yaml").read_text()
|
|
129
|
+
match = re.search(
|
|
130
|
+
r"id: anthropic/fable\b.*?inputPerMillion:\s*([0-9.]+)\s*\n\s*outputPerMillion:\s*([0-9.]+)",
|
|
131
|
+
text,
|
|
132
|
+
re.S,
|
|
133
|
+
)
|
|
134
|
+
if not match:
|
|
135
|
+
raise SystemExit("prices.yaml has no anthropic/fable tier")
|
|
136
|
+
return float(match.group(1)), float(match.group(2))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def load_sonnet_rates() -> tuple[float, float, str]:
|
|
140
|
+
text = (REPO / ".kxm" / "prices.yaml").read_text()
|
|
141
|
+
if re.search(r"id: anthropic/claude-sonnet-5\.5\b", text):
|
|
142
|
+
match = re.search(
|
|
143
|
+
r"id: anthropic/claude-sonnet-5\.5\b.*?inputPerMillion:\s*([0-9.]+)\s*\n\s*outputPerMillion:\s*([0-9.]+)",
|
|
144
|
+
text,
|
|
145
|
+
re.S,
|
|
146
|
+
)
|
|
147
|
+
if not match:
|
|
148
|
+
raise SystemExit("sonnet row in prices.yaml has no tier")
|
|
149
|
+
return float(match.group(1)), float(match.group(2)), "prices.yaml"
|
|
150
|
+
inventory = (REPO / ".kxm" / "models" / "inventory.yaml").read_text()
|
|
151
|
+
match = re.search(
|
|
152
|
+
r"id: anthropic/claude-sonnet-5\.5\n(?:.*\n){0,20}?\s*standard:\n\s*inputPerMillion:\s*([0-9.]+)\n\s*outputPerMillion:\s*([0-9.]+)",
|
|
153
|
+
inventory,
|
|
154
|
+
)
|
|
155
|
+
if not match:
|
|
156
|
+
raise SystemExit("inventory.yaml has no anthropic/claude-sonnet-5.5 standard tier")
|
|
157
|
+
return float(match.group(1)), float(match.group(2)), "inventory.yaml"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def cost(tokens_in: int, tokens_out: int, in_rate: float, out_rate: float) -> float:
|
|
161
|
+
return (tokens_in * in_rate + tokens_out * out_rate) / 1_000_000
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def prompt_text(task: str) -> str:
|
|
165
|
+
shared = (PROMPTS / "shared.txt").read_text()
|
|
166
|
+
packet = (PROMPTS / f"{task}.txt").read_text()
|
|
167
|
+
return shared + "\n" + packet
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def checklist(task: str, text: str) -> tuple[int, int, list[str]]:
|
|
171
|
+
hits = []
|
|
172
|
+
missed = []
|
|
173
|
+
for name, pattern in CHECKS[task]:
|
|
174
|
+
if re.search(pattern, text, re.I):
|
|
175
|
+
hits.append(name)
|
|
176
|
+
else:
|
|
177
|
+
missed.append(name)
|
|
178
|
+
return len(hits), len(CHECKS[task]), missed
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def unblind(task: str) -> dict[str, dict]:
|
|
182
|
+
payload = json.loads((JUDGE / f"{task}.json").read_text())
|
|
183
|
+
labels = LABELS[task]
|
|
184
|
+
out = {}
|
|
185
|
+
for label, model in labels.items():
|
|
186
|
+
key = "planA" if label == "A" else "planB"
|
|
187
|
+
scores = payload[key]
|
|
188
|
+
out[model] = {
|
|
189
|
+
"label": label,
|
|
190
|
+
"scores": {dim: scores[dim] for dim in DIMS},
|
|
191
|
+
"total": sum(scores[dim] for dim in DIMS),
|
|
192
|
+
}
|
|
193
|
+
out["winner_label"] = payload["winner"]
|
|
194
|
+
winner = {"A": labels["A"], "B": labels["B"], "tie": "tie"}[payload["winner"]]
|
|
195
|
+
out["winner"] = winner
|
|
196
|
+
out["rationale"] = payload["rationale"]
|
|
197
|
+
return out
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def score() -> dict:
|
|
201
|
+
fable_in, fable_out = load_fable_rates()
|
|
202
|
+
sonnet_in, sonnet_out, sonnet_source = load_sonnet_rates()
|
|
203
|
+
rates = {
|
|
204
|
+
"fable": (fable_in, fable_out, "prices.yaml anthropic/fable"),
|
|
205
|
+
"sonnet": (sonnet_in, sonnet_out, sonnet_source + " anthropic/claude-sonnet-5.5"),
|
|
206
|
+
}
|
|
207
|
+
tasks = []
|
|
208
|
+
totals = {model: {"judge": 0, "tokens_in": 0, "tokens_out": 0, "cost": 0.0, "wins": 0} for model in MODELS}
|
|
209
|
+
for task in TASKS:
|
|
210
|
+
judged = unblind(task)
|
|
211
|
+
row = {"task": task, "winner": judged["winner"], "rationale": judged["rationale"], "models": {}}
|
|
212
|
+
for model in MODELS:
|
|
213
|
+
text = (RAW / f"{task}-{model}.txt").read_text()
|
|
214
|
+
prompt = prompt_text(task)
|
|
215
|
+
tokens_in = estimate_tokens(prompt)
|
|
216
|
+
tokens_out = estimate_tokens(text)
|
|
217
|
+
in_rate, out_rate, source = rates[model]
|
|
218
|
+
price = cost(tokens_in, tokens_out, in_rate, out_rate)
|
|
219
|
+
got, total, missed = checklist(task, text)
|
|
220
|
+
entry = {
|
|
221
|
+
"judge": judged[model],
|
|
222
|
+
"words": len(text.split()),
|
|
223
|
+
"tokens_in_estimate": tokens_in,
|
|
224
|
+
"tokens_out_estimate": tokens_out,
|
|
225
|
+
"list_cost_usd": round(price, 6),
|
|
226
|
+
"price_source": source,
|
|
227
|
+
"checklist_hits": got,
|
|
228
|
+
"checklist_total": total,
|
|
229
|
+
"checklist_missed": missed,
|
|
230
|
+
}
|
|
231
|
+
row["models"][model] = entry
|
|
232
|
+
totals[model]["judge"] += judged[model]["total"]
|
|
233
|
+
totals[model]["tokens_in"] += tokens_in
|
|
234
|
+
totals[model]["tokens_out"] += tokens_out
|
|
235
|
+
totals[model]["cost"] += price
|
|
236
|
+
if judged["winner"] in totals:
|
|
237
|
+
totals[judged["winner"]]["wins"] += 1
|
|
238
|
+
tasks.append(row)
|
|
239
|
+
for model in MODELS:
|
|
240
|
+
totals[model]["list_cost_usd"] = round(totals[model]["cost"], 6)
|
|
241
|
+
return {
|
|
242
|
+
"token_method": "ceil(characters/4) on prompts/<task>.txt plus prompts/shared.txt, and on raw/<task>-<model>.txt. Not provider usage.",
|
|
243
|
+
"max_judge_per_task": 25,
|
|
244
|
+
"tasks": tasks,
|
|
245
|
+
"totals": totals,
|
|
246
|
+
"rates": {
|
|
247
|
+
"fable_input": fable_in,
|
|
248
|
+
"fable_output": fable_out,
|
|
249
|
+
"sonnet_input": sonnet_in,
|
|
250
|
+
"sonnet_output": sonnet_out,
|
|
251
|
+
"sonnet_source": sonnet_source,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def credentials_present() -> list[str]:
|
|
257
|
+
missing = []
|
|
258
|
+
if shutil.which("claude") is None:
|
|
259
|
+
missing.append("claude CLI is not on PATH (kxm's planner one-shot spawns claude -p)")
|
|
260
|
+
if not (Path.home() / ".claude").exists() and not (Path.home() / ".config" / "claude").exists():
|
|
261
|
+
missing.append("no ~/.claude or ~/.config/claude auth directory")
|
|
262
|
+
# Names only. Do not print values.
|
|
263
|
+
import os
|
|
264
|
+
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|
265
|
+
missing.append("ANTHROPIC_API_KEY is unset")
|
|
266
|
+
return missing
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def run_refused() -> int:
|
|
270
|
+
missing = credentials_present()
|
|
271
|
+
sys.stderr.write(
|
|
272
|
+
"planner bake-off regeneration refused.\n"
|
|
273
|
+
"kxm's planner one-shot is: claude -p --model <model> --effort medium "
|
|
274
|
+
"--output-format json, plus the read-only permission arguments in "
|
|
275
|
+
"plugins/kxm/src/harness.ts (oneShotReadOnlyArgs claude).\n"
|
|
276
|
+
"There is no kxm flag that overrides the planner model without editing "
|
|
277
|
+
".kxm/roles/planner.yaml and admitting the route. This bake-off must not do that.\n"
|
|
278
|
+
"kxm routing benchmark prints placeholder numbers and does not call a model.\n"
|
|
279
|
+
)
|
|
280
|
+
if missing:
|
|
281
|
+
sys.stderr.write("Missing:\n")
|
|
282
|
+
for item in missing:
|
|
283
|
+
sys.stderr.write(f"- {item}\n")
|
|
284
|
+
sys.stderr.write("Refusing to simulate plans.\n")
|
|
285
|
+
return 2
|
|
286
|
+
sys.stderr.write("Credentials appear present. This script still does not dispatch; use the native claude one-shot with prompts/*.txt.\n")
|
|
287
|
+
return 0
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def main(argv: list[str]) -> int:
|
|
291
|
+
parser = argparse.ArgumentParser()
|
|
292
|
+
parser.add_argument("--run", action="store_true", help="attempt a live regeneration; fail closed without credentials")
|
|
293
|
+
parser.add_argument("--score", action="store_true", help="score the committed raw plans")
|
|
294
|
+
args = parser.parse_args(argv)
|
|
295
|
+
if args.run:
|
|
296
|
+
return run_refused()
|
|
297
|
+
payload = score()
|
|
298
|
+
json.dump(payload, sys.stdout, indent=2)
|
|
299
|
+
sys.stdout.write("\n")
|
|
300
|
+
return 0
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
if __name__ == "__main__":
|
|
304
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Planner bake-off, October 2026
|
|
2
|
+
|
|
3
|
+
This page records a scored comparison of Claude Fable 5.1 and Claude Sonnet 5.5 on six KXM planning tasks. It is for maintainers deciding whether the planner primary should stay Fable. Routing is unchanged.
|
|
4
|
+
|
|
5
|
+
## Recommendation
|
|
6
|
+
|
|
7
|
+
Keep Fable as the planner primary. Treat Sonnet 5.5 as a fallback candidate, not a replacement.
|
|
8
|
+
|
|
9
|
+
Sonnet scored 126 and Fable scored 110, out of 150, on a blinded judge. Sonnet won four tasks and Fable won two. The two Fable wins include the Argon roster, which is the task closest to an admission decision, and Sonnet's Argon plan admitted only the medium pin and placed it after Flash. Six tasks, one judge, and a transport that is not the KXM planner route are not enough to replace the primary. List price favors Sonnet by about five to one, and a native Claude subscription records both as unmetered, so the invoice may not move.
|
|
10
|
+
|
|
11
|
+
## How this run was scored
|
|
12
|
+
|
|
13
|
+
The planner roster in `.kxm/roles/planner.yaml` is `claude-fable` at effort medium, then `pi-qwen3-8-flash-openrouter`. The native one-shot is `claude -p --model <model> --effort medium --output-format json` with the read-only arguments in `oneShotReadOnlyArgs` for `claude`. There is no command flag that swaps the planner model without editing the role and admitting a route. `kxm routing benchmark` prints placeholder numbers and does not call a model.
|
|
14
|
+
|
|
15
|
+
This machine has no `claude` binary, no Claude auth directory, and no `ANTHROPIC_API_KEY`. The committed plans are from Cursor subagents pinned to `claude-fable-5-1-thinking-medium` and `claude-sonnet-5-5-medium`. Both received the same prompt. They were told not to read the checkout, because the landed change would leak the answer. The Argon plans still name `pi-qwen3-coder-plus-openrouter` as writer number two, which the current tree no longer has, so they do not look like a read of the solution.
|
|
16
|
+
|
|
17
|
+
The judge is `gpt-5.6-sol-medium`. Labels were shuffled with seed `20261017` and stored in `score.py`. The judge scored a section-level condensation of each plan, the same sections for both arms, not the raw file byte for byte. Full text is under `raw/`. A keyword checklist in `score.py` scores the full text and does not pick the winner.
|
|
18
|
+
|
|
19
|
+
Rubric, 0 to 5 each: correctness against what shipped (or, for the open journal gap, against the written constraints), completeness of files, tests, and docs, risk and fail-closed guards, sequencing, and conciseness. The cap in the prompt was 700 words.
|
|
20
|
+
|
|
21
|
+
Tokens are `ceil(characters / 4)` on `prompts/` and `raw/`. That is not provider usage. Cache tokens were not observed, so cache rates are not applied. Fable uses the flat tier in `.kxm/prices.yaml` for `anthropic/fable`: $10 input and $50 output per 1M tokens. Sonnet 5.5 has no row in `.kxm/prices.yaml`. Its list estimate uses the standard tier in `.kxm/models/inventory.yaml` for `anthropic/claude-sonnet-5.5`: $2 and $10. `score.py` reads both files.
|
|
22
|
+
|
|
23
|
+
Twelve plans ran in one parallel wave from 2026-10-17T17:48:29Z to 2026-10-17T17:51:03Z, 154 seconds. The transport did not return per-plan latency, so the table does not invent one.
|
|
24
|
+
|
|
25
|
+
Regenerate scores with `python3 docs/evals/planner-bakeoff-2026-10/score.py --score`. `score.py --run` exits 2 until the Claude CLI or `ANTHROPIC_API_KEY` is present, and it does not invent plans.
|
|
26
|
+
|
|
27
|
+
## Totals
|
|
28
|
+
|
|
29
|
+
| Task | Fable | Sonnet | Winner |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| Argon writer roster | 19 | 16 | Fable |
|
|
32
|
+
| CLI reviewer switch | 18 | 23 | Sonnet |
|
|
33
|
+
| Writer number two | 13 | 21 | Sonnet |
|
|
34
|
+
| Weekly catalog refresh | 22 | 21 | Fable |
|
|
35
|
+
| Knowledge wiki seed | 18 | 21 | Sonnet |
|
|
36
|
+
| Runtime journal gap | 20 | 24 | Sonnet |
|
|
37
|
+
| Total (max 150) | 110 | 126 | Sonnet, 4 of 6 |
|
|
38
|
+
|
|
39
|
+
| Task | Input tokens | Fable output | Sonnet output |
|
|
40
|
+
| --- | --- | --- | --- |
|
|
41
|
+
| Argon writer roster | 895 | 1501 | 1359 |
|
|
42
|
+
| CLI reviewer switch | 924 | 1361 | 1306 |
|
|
43
|
+
| Writer number two | 1002 | 1318 | 1344 |
|
|
44
|
+
| Weekly catalog refresh | 800 | 1400 | 1246 |
|
|
45
|
+
| Knowledge wiki seed | 940 | 1295 | 1252 |
|
|
46
|
+
| Runtime journal gap | 661 | 1279 | 1349 |
|
|
47
|
+
| Total | 5222 | 8154 | 7856 |
|
|
48
|
+
|
|
49
|
+
| Model | List rate per 1M | List cost | Wins |
|
|
50
|
+
| --- | --- | --- | --- |
|
|
51
|
+
| Fable 5.1 | $10 / $50, from `.kxm/prices.yaml` | $0.460 | 2 |
|
|
52
|
+
| Sonnet 5.5 | $2 / $10, from inventory, not `.kxm/prices.yaml` | $0.089 | 4 |
|
|
53
|
+
|
|
54
|
+
Input tokens are identical because the prompts were identical. Wall time for the wave is 154 seconds, shared.
|
|
55
|
+
|
|
56
|
+
The keyword checklist matched every required topic on eleven of the twelve plans. Sonnet's Argon plan missed "argon high on the roster" and "argon tried before flash". The checklist does not encode the shipped writer-number-two model, so a perfect checklist there does not mean the plan matched what shipped.
|
|
57
|
+
|
|
58
|
+
## Argon writer roster
|
|
59
|
+
|
|
60
|
+
Shipped in pull request 401: Argon high and medium on the writer roster, ahead of Flash medium (the safety net) and Flash high (last, so the file stays on a roster). Low was inventoried and not rostered. Planner primary stayed Fable.
|
|
61
|
+
|
|
62
|
+
| Dimension | Fable | Sonnet |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| Correctness | 3 | 1 |
|
|
65
|
+
| Completeness | 3 | 2 |
|
|
66
|
+
| Risk | 5 | 5 |
|
|
67
|
+
| Sequencing | 3 | 3 |
|
|
68
|
+
| Conciseness | 5 | 5 |
|
|
69
|
+
| Total | 19 | 16 |
|
|
70
|
+
|
|
71
|
+
Fable admitted high and medium and put them ahead of Flash, but swapped Flash medium and high and described the roster as living in `.kxm/routes.yaml`. Sonnet admitted only medium and appended it after both Flash entries. Both refused an OpenRouter Google route. List cost $0.084 versus $0.015.
|
|
72
|
+
|
|
73
|
+
## CLI reviewer switch
|
|
74
|
+
|
|
75
|
+
Shipped in pull request 402: `codex-gpt-6-1-sol` at effort low, then `pi-qwen3-8-flash-openrouter`, then `codex-gpt-5-6-sol`. Pro is an alias on the price row. Architecture review stayed Fable.
|
|
76
|
+
|
|
77
|
+
| Dimension | Fable | Sonnet |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| Correctness | 3 | 5 |
|
|
80
|
+
| Completeness | 3 | 4 |
|
|
81
|
+
| Risk | 4 | 5 |
|
|
82
|
+
| Sequencing | 3 | 5 |
|
|
83
|
+
| Conciseness | 5 | 4 |
|
|
84
|
+
| Total | 18 | 23 |
|
|
85
|
+
|
|
86
|
+
Both named the shipped order, the pro alias, the native-vendor brake, and the doc set. The judge marked Sonnet higher because Fable placed the roster reorder inside `.kxm/routes.yaml`. Neither named `workforce-names.mjs`, the review template, or the generated bundles. List cost $0.077 versus $0.015.
|
|
87
|
+
|
|
88
|
+
## Writer number two
|
|
89
|
+
|
|
90
|
+
Shipped in pull request 403: `pi-qwen3-7-plus-openrouter`, after a live check that the model lists tools. The old coder-plus route was retired and its price row kept. The packet withheld tool support and told the planner to fail closed.
|
|
91
|
+
|
|
92
|
+
| Dimension | Fable | Sonnet |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| Correctness | 2 | 3 |
|
|
95
|
+
| Completeness | 4 | 3 |
|
|
96
|
+
| Risk | 2 | 5 |
|
|
97
|
+
| Sequencing | 2 | 5 |
|
|
98
|
+
| Conciseness | 3 | 5 |
|
|
99
|
+
| Total | 13 | 21 |
|
|
100
|
+
|
|
101
|
+
Both defaulted to `qwen3-coder-next`, which is not what shipped. Sonnet refused to admit on that assumption and named `qwen3.7-plus` as the fallback if tools were missing. Fable scheduled the tool check after the route edit and moved the old alias onto the new route. List cost $0.076 versus $0.015.
|
|
102
|
+
|
|
103
|
+
## Weekly catalog refresh
|
|
104
|
+
|
|
105
|
+
Shipped in pull request 404: `.github/workflows/refresh-models.yml`, a catalog module, a script, and a unit test. Monday 13:00 UTC. One pull request when the catalog changes. Routes are not edited.
|
|
106
|
+
|
|
107
|
+
| Dimension | Fable | Sonnet |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| Correctness | 5 | 4 |
|
|
110
|
+
| Completeness | 4 | 4 |
|
|
111
|
+
| Risk | 5 | 5 |
|
|
112
|
+
| Sequencing | 4 | 5 |
|
|
113
|
+
| Conciseness | 4 | 3 |
|
|
114
|
+
| Total | 22 | 21 |
|
|
115
|
+
|
|
116
|
+
Both chose Monday 06:00 UTC and marked it as a choice, which the packet allowed. Both kept CLI-only rows, refused a token, refused route edits, and refused auto-merge. The one-point edge is not a decision by itself. List cost $0.078 versus $0.014.
|
|
117
|
+
|
|
118
|
+
## Knowledge wiki seed
|
|
119
|
+
|
|
120
|
+
Shipped in pull request 405: hand-written pages under `.kxm/knowledge/`, not under `wiki/` and not under a handbook directory. Compile and ingest stayed unselected. No new test, script, or route edit.
|
|
121
|
+
|
|
122
|
+
| Dimension | Fable | Sonnet |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| Correctness | 3 | 4 |
|
|
125
|
+
| Completeness | 3 | 3 |
|
|
126
|
+
| Risk | 4 | 5 |
|
|
127
|
+
| Sequencing | 4 | 5 |
|
|
128
|
+
| Conciseness | 4 | 4 |
|
|
129
|
+
| Total | 18 | 21 |
|
|
130
|
+
|
|
131
|
+
Sonnet put pages at `.kxm/knowledge/` and did not invent a browser flag. Fable used `.kxm/knowledge/handbook/` and named `KXM_BROWSER=steel`, which the packet said not to invent. Both refused ingest and a second glossary. List cost $0.074 versus $0.014.
|
|
132
|
+
|
|
133
|
+
## Runtime journal gap
|
|
134
|
+
|
|
135
|
+
Nothing has shipped. The constraint is: no event-store or hub-store schema change while S5 is open, do not relabel a Runtime run as a webhook run, and prefer a design note plus tests that lock `workflow_not_found`.
|
|
136
|
+
|
|
137
|
+
| Dimension | Fable | Sonnet |
|
|
138
|
+
| --- | --- | --- |
|
|
139
|
+
| Correctness | 4 | 5 |
|
|
140
|
+
| Completeness | 5 | 5 |
|
|
141
|
+
| Risk | 4 | 5 |
|
|
142
|
+
| Sequencing | 4 | 5 |
|
|
143
|
+
| Conciseness | 3 | 4 |
|
|
144
|
+
| Total | 20 | 24 |
|
|
145
|
+
|
|
146
|
+
Both refused a store and refused invented schema versions. Sonnet stopped at characterization tests and a design note. Fable also changed the CLI message, which is more than the smallest step and still not a schema change. List cost $0.071 versus $0.015.
|
|
147
|
+
|
|
148
|
+
## Caveats
|
|
149
|
+
|
|
150
|
+
- Six tasks is a small sample. One task is still open, so its correctness score is against constraints, not a diff.
|
|
151
|
+
- The judge saw condensations. Weekly is a one-point result and could flip. Argon, the reviewer switch, writer number two, and the journal gap are wider.
|
|
152
|
+
- The judge is one model, GPT-5.6 Sol, which is the CLI critic family. It was blind to the author and not blind to the shipped outcome.
|
|
153
|
+
- Token counts and list cost are estimates. Provider usage was not returned. A Claude subscription would record `costBasis: unmetered` for both models.
|
|
154
|
+
- Sonnet's price is not in `.kxm/prices.yaml`. Quoting it from the inventory is a list estimate, and the price loader would mark that row missing.
|
|
155
|
+
- Cursor subagents are not `claude -p` and not the KXM planner route. Effort was medium on both, matching the roster.
|
|
156
|
+
- Pull request 398, the plan-link repair, was not run. The six tasks are the five roster and docs slices from 401 through 405, plus the open Runtime journal gap.
|
|
157
|
+
- Plans were instructed not to use tools. A subagent that ignored that instruction could have read the landed diff. The Argon plans still describe the pre-change writer number two, which argues against that leak, and it is not proof.
|
|
158
|
+
|
|
159
|
+
## Related
|
|
160
|
+
|
|
161
|
+
- [Harness routing](reference/harness-routing.md)
|
|
162
|
+
- [Configuration reference](reference/config-reference.md)
|
|
163
|
+
- [CLI reference](reference/cli-reference.md)
|
|
164
|
+
- Raw plans, prompts, judge JSON, and `score.py` live in `docs/evals/planner-bakeoff-2026-10/`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontextmind/kxm",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.190",
|
|
4
4
|
"description": "KXM local-first multi-agent orchestration and operator dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "KontextMind",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"test:coverage:complete": "npm run build && node scripts/run-bounded.mjs 2400000 node --disable-warning=ExperimentalWarning --experimental-strip-types --test --test-force-exit --test-concurrency=4 --experimental-test-coverage --test-coverage-lines=93 --test-coverage-branches=80 --test-coverage-functions=93 --test-coverage-include=plugins/kxm/src/**/*.ts --test-coverage-exclude=plugins/kxm/src/server.ts --test-coverage-exclude=plugins/kxm/src/mcp-server.ts --test-coverage-exclude=plugins/kxm/src/runtime-supervisor.ts --test-coverage-include=packages/core/*/src/**/*.ts test/core/*.test.ts test/simulations/*.test.ts packages/core/*/tests/unit/*.test.ts",
|
|
45
45
|
"test:coverage": "npm run test:coverage:core",
|
|
46
46
|
"typecheck": "tsc --noEmit",
|
|
47
|
-
"lint:docs": "markdownlint-cli2",
|
|
47
|
+
"lint:docs": "markdownlint-cli2 && node scripts/check-plan-links.mjs",
|
|
48
48
|
"check:versions": "node scripts/check-versions.mjs",
|
|
49
49
|
"check": "npm run typecheck && npm run lint:docs && npm run check:versions && node scripts/workforce-lint.mjs",
|
|
50
50
|
"verify": "npm test && npm run check && npm run check:generated",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "kxm",
|
|
4
4
|
"displayName": "KXM",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.190",
|
|
6
6
|
"description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "KontextMind",
|
|
@@ -25462,7 +25462,7 @@ function sessionTokenFixHint(policy) {
|
|
|
25462
25462
|
}
|
|
25463
25463
|
|
|
25464
25464
|
// plugins/kxm/src/mcp-server.ts
|
|
25465
|
-
var VERSION = "0.7.
|
|
25465
|
+
var VERSION = "0.7.190";
|
|
25466
25466
|
var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
25467
25467
|
var inbox = /* @__PURE__ */ new Map();
|
|
25468
25468
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
package/plugins/kxm/package.json
CHANGED
|
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
|
|
|
11
11
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
12
12
|
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.7.
|
|
14
|
+
const VERSION = "0.7.190";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|