blun-king-cli 9.1.526 → 9.1.536

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 (44) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LIESMICH.txt +36 -1
  3. package/README.md +35 -1
  4. package/bin/agent-resume-snapshot.cjs +31 -0
  5. package/bin/assistant-message-offload-policy.cjs +21 -2
  6. package/bin/codebase-search-runtime.cjs +23 -0
  7. package/bin/empty-response-retry-policy.cjs +29 -0
  8. package/bin/fredrik-glm-provider.cjs +256 -0
  9. package/bin/history-offload-pressure-policy.cjs +33 -0
  10. package/bin/programmatic-context-isolation.cjs +25 -0
  11. package/bin/programmatic-tool-runtime.mjs +301 -0
  12. package/bin/skill-activation-performance-policy.cjs +9 -0
  13. package/bin/structured-subagent-output.cjs +252 -0
  14. package/bin/telegram-direct-focus-policy.cjs +25 -1
  15. package/bin/todo-list-turn-policy.cjs +111 -1
  16. package/bin/tool-result-offload-policy.cjs +29 -0
  17. package/bin/turn-thinking-policy.cjs +6 -15
  18. package/bin/turn-tool-performance-policy.cjs +5 -4
  19. package/bin/user-message-offload-policy.cjs +10 -1
  20. package/blun.mjs +709 -127
  21. package/codebase-index/README.md +70 -0
  22. package/codebase-index/codebase_index.py +358 -0
  23. package/fredrik-glm-profile.toml.example +26 -0
  24. package/package.json +25 -3
  25. package/scripts/check-active-work-steer-regression.js +46 -0
  26. package/scripts/check-codebase-search-packaging-regression.js +92 -0
  27. package/scripts/check-copy-command-regression.js +74 -0
  28. package/scripts/check-current-turn-read-pin-mutation-regression.js +72 -0
  29. package/scripts/check-current-turn-read-pin-regression.js +94 -0
  30. package/scripts/check-deepseek-native-max-regression.js +49 -0
  31. package/scripts/check-empty-response-effort-downgrade-regression.js +48 -0
  32. package/scripts/check-fredrik-glm-mutation-regression.js +18 -0
  33. package/scripts/check-fredrik-glm-regression.js +169 -0
  34. package/scripts/check-history-pressure-offload-regression.js +77 -0
  35. package/scripts/check-programmatic-context-isolation-regression.js +193 -0
  36. package/scripts/check-programmatic-tool-regression.js +294 -0
  37. package/scripts/check-resume-replay-regression.js +2 -0
  38. package/scripts/check-startup-swarm-command-regression.js +24 -0
  39. package/scripts/check-structured-subagent-output-regression.js +331 -0
  40. package/scripts/check-telegram-direct-work-resume-regression.js +53 -0
  41. package/scripts/check-todo-progress-regression.js +416 -0
  42. package/scripts/check-tool-schema-capacity-regression.js +40 -0
  43. package/scripts/programmatic-tool-runtime.test.mjs +365 -0
  44. package/scripts/structured-subagent-output.test.cjs +170 -0
@@ -0,0 +1,70 @@
1
+ # codebase-index
2
+
3
+ Lokaler semantischer Index über einen Git-Code-Baum — F1 aus Papas
4
+ Feature-Liste. Konzept + Messprotokoll:
5
+ `handoffs/codebase-verstaendnis-lokal-konzept.md`.
6
+
7
+ Der Index ist ein **Lese-Einstieg, keine Wahrheit**: exakte Namen bleiben
8
+ `grep`/`glob`; Begriffe ohne bekannten Namen gehen an den Index. Jede
9
+ Antwort trägt die Selbstauskunft ("semantisch, kann falsch liegen") und
10
+ den Index-Stand.
11
+
12
+ ## Voraussetzungen
13
+
14
+ Auf dieser Maschine bereits vorhanden (kein Installations-Kapitel):
15
+ Python 3.11, `fastembed` 0.8.0 (ONNX, CPU), `numpy`, `psutil`.
16
+ Modell: `BAAI/bge-small-en-v1.5` (33 MB, einmaliger HF-Cache-Download).
17
+
18
+ ## Benutzung
19
+
20
+ ```bash
21
+ # Erstaufbau (Hintergrund-Task! ~40 min auf dem App-Baum, 38k Chunks)
22
+ python codebase_index.py build <repo>
23
+
24
+ # Delta nach Änderungen (Content-Hash je Datei; Ziel < 30 s)
25
+ python codebase_index.py update <repo>
26
+
27
+ # Frage stellen (Top-5 mit Score, Frische, Selbstauskunft)
28
+ python codebase_index.py query <repo> "wo wird die retry-Kappe gesetzt?"
29
+
30
+ # Qualitäts-Gate: Stichprobe aus dem Subjekt-Baum, >= 2/3 in Top-5
31
+ python codebase_index.py sample <repo>
32
+ ```
33
+
34
+ Index-Ort: `~/.blun/codebase-index/<workspace-hash>/` mit
35
+ `vectors.npy` (float32-Matrix), `manifest.json` (Hashes, Spans, Meta,
36
+ Stand) und `query-log.jsonl` (jede Query — der Moat: welche Begriffe
37
+ nichts trafen, wächst mit jeder Session). Löschen des Ordners = sauberer
38
+ Rückweg, Neuaufbau jederzeit reproduzierbar.
39
+
40
+ ## Design-Entscheidungen (alle an Messungen festgemacht)
41
+
42
+ - **Streaming in vorallokierte Matrix** (`np.lib.format.open_memmap`),
43
+ niemals Vektoren-Liste: deterministische Schreibweise, kein
44
+ Doppelbestand. **Korrektur zur Erst-Einordnung:** die ~9-GB-RAM-Spitze
45
+ kommt NICHT von der Liste (~60-100 MB), sondern von der ONNX-Runtime
46
+ (Thread-Pool + Arena) — Run 4 mit memmap läuft ebenfalls bei ~8,9 GB.
47
+ Mitigation (Thread-Begrenzung/Batch) wird separat gemessen.
48
+ - **Inkrementell statt Vollindex:** Vollindex CPU = 2389 s (~40 min,
49
+ Run 3, unbelastet). Erstaufbau als Hintergrund-Task, danach nur Delta:
50
+ Content-Hash je Datei im Manifest, geänderte neu einbetten, gelöschte
51
+ entfernen. Gate: Delta < 30 s (gemessen, nicht geschätzt).
52
+ - **Frische-Auskunft in jeder Query-Antwort:** Index-HEAD vs. Repo-HEAD,
53
+ bei Abweichung "update fällig". Ein Werkzeug ohne Frische-Angabe
54
+ erzeugt Verdikte auf Altstand.
55
+ - **Stichprobe aus dem SUBJEKT-Baum:** Run 3 scheiterte mit 0/3 an einem
56
+ Repo-Mix (Erwartungsdateien aus dem SDK, Subjekt war der App-Baum).
57
+ Die Gate-Queries zeigen auf Dateien, die via `git ls-files` im Subjekt
58
+ verifiziert sind (`api-chat.js`, `terminal-manager.js`,
59
+ `renderer-view-shell.js`).
60
+
61
+ ## Gemessene Werte (App-Baum, 1191 Dateien / 37901 Chunks)
62
+
63
+ | Messung | Wert | Quelle |
64
+ | --- | --- | --- |
65
+ | Vollindex CPU | Run 3: 2389 s (Liste) · Run 4: 2431 s (memmap) | handoffs/f1-benchmark-run3.log, f1-build-run4.log |
66
+ | Query-Latenz | Ø 10,9 ms | Run 3 |
67
+ | Index-Größe | 58,2 MB float32 | Run 3/4 |
68
+ | RAM-Spitze Bau | Run 3: 10,5 GB · Run 4 (memmap): ~8,9-10,2 GB — Treiber ONNX-Arena (Probe: 4,2 GB schon bei 2048 Chunks; OMP_NUM_THREADS 4/2 gemessen: KEIN Effekt auf RSS) | Run 3 rss_peak, Run 4 psutil, f1-onnx-rss-probe |
69
+ | Delta-Update | **3,0 s / 3,1 s am App-Baum (Gate <30 s PASS)** · Mechanik 0,8 s (Mini-Repo) | update-Läufe 03.08. |
70
+ | Stichprobe | V2 (namensbasiert): 0/3 FAIL, Ground-Truth-schwach · **V3 (paraphrasiert): jina-code 3/3 PASS (Top-1 überall), bge 2/3** | sample-Läufe 03.08., Commit fe0b69d |
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env python3
2
+ """codebase-index — lokaler semantischer Index ueber einen Git-Code-Baum.
3
+
4
+ F1-Bau nach Konzept (handoffs/codebase-verstaendnis-lokal-konzept.md),
5
+ Auflagen Dieter 43102:
6
+ - Streaming in vorallokierte float32-Matrix (memmap), KEINE Vektoren-Liste
7
+ (Run-3-Befund: Liste trieb RAM-Spitze auf 10,5 GB).
8
+ - Inkrementell: Content-Hash je Datei im Manifest, Delta statt Vollindex.
9
+ - Query mit Frische-Auskunft + Selbstauskunft in JEDER Antwort.
10
+ - Query-Fehlschlag-Log lokal (query-log.jsonl) — der Moat.
11
+
12
+ Aufruf:
13
+ python codebase_index.py build <repo>
14
+ python codebase_index.py update <repo>
15
+ python codebase_index.py query <repo> "<frage>" [--top 5]
16
+ python codebase_index.py sample <repo>
17
+
18
+ Index-Ort: ~/.blun/codebase-index/<workspace-hash>/ (sichtbar, mit Rueckweg).
19
+ """
20
+ import hashlib
21
+ import json
22
+ import os
23
+ import subprocess
24
+ import sys
25
+ import time
26
+ from datetime import datetime, timezone
27
+
28
+ import numpy as np
29
+
30
+ CODE_EXT = (
31
+ ".ts", ".tsx", ".js", ".mjs", ".cjs", ".py", ".html", ".css",
32
+ ".json", ".md", ".toml", ".yml", ".yaml", ".sh", ".ps1", ".sql",
33
+ )
34
+ CHUNK_SIZE = 1000
35
+ CHUNK_OVERLAP = 100
36
+ EMBED_DIM = 384
37
+ BATCH = 256
38
+ MODEL_NAME = "BAAI/bge-small-en-v1.5"
39
+
40
+ # Iteration 2 (Dieter 43135): Rausch-Filter — Backup-Kopien und Vendor-
41
+ # Buelle machten 66% aller Index-Rows aus und dominieren Top-5.
42
+ EXCLUDE_PREFIXES = ("backup-", "vendor/")
43
+
44
+ # Aktives Modell + Dim zur Laufzeit (wird per --model ueberschrieben).
45
+ _active_model_name = MODEL_NAME
46
+ _active_dim = EMBED_DIM
47
+
48
+ # Stichprobe V3 (Dieter 43141): Fragen PARAPHRASIERT aus gelesenem
49
+ # Dateiinhalt — keine wörtlichen Bezeichner/Funktionsnamen/Kommentare aus
50
+ # den Dateien (misst semantische Suche, nicht Wortgleichheit).
51
+ # api-chat.js: routet Chat-Nachrichten ans Modell, Fallback-Kette, Stream.
52
+ # terminal-manager.js: startet Shell-Prozesse mit Sandbox-/Approval-Modi.
53
+ # renderer-view-shell.js: wechselt Login-/Willkommens-/Chat-Ansicht.
54
+ SAMPLE_QUERIES = [
55
+ ("how are conversation messages routed to the right model with a fallback chain", "api-chat"),
56
+ ("starting sandboxed shell processes with approval modes and output history", "terminal-manager"),
57
+ ("switching between sign-in screen, welcome page and main conversation view", "renderer-view-shell"),
58
+ ]
59
+
60
+ SELSTAUSKUNFT = (
61
+ "HINWEIS: semantischer Index, kann falsch liegen — Fundstelle vor "
62
+ "Verwendung in der Datei verifizieren."
63
+ )
64
+
65
+
66
+ def index_dir(repo: str) -> str:
67
+ raw = os.path.abspath(repo) + "|" + _active_model_name
68
+ key = hashlib.sha256(raw.encode()).hexdigest()[:12]
69
+ return os.path.join(os.path.expanduser("~"), ".blun", "codebase-index", key)
70
+
71
+
72
+ def git_files(repo: str) -> list[str]:
73
+ out = subprocess.run(
74
+ ["git", "ls-files"], cwd=repo, capture_output=True, text=True, check=True
75
+ ).stdout.splitlines()
76
+ return [
77
+ f for f in out
78
+ if f.lower().endswith(CODE_EXT) and not f.startswith(EXCLUDE_PREFIXES)
79
+ ]
80
+
81
+
82
+ def git_head(repo: str) -> str:
83
+ return subprocess.run(
84
+ ["git", "rev-parse", "--short", "HEAD"],
85
+ cwd=repo, capture_output=True, text=True, check=True,
86
+ ).stdout.strip()
87
+
88
+
89
+ def sha_file(path: str) -> str:
90
+ h = hashlib.sha256()
91
+ with open(path, "rb") as fh:
92
+ for block in iter(lambda: fh.read(1 << 20), b""):
93
+ h.update(block)
94
+ return h.hexdigest()
95
+
96
+
97
+ def chunk_text(text: str) -> list[str]:
98
+ step = CHUNK_SIZE - CHUNK_OVERLAP
99
+ return [
100
+ text[i : i + CHUNK_SIZE]
101
+ for i in range(0, len(text), step)
102
+ if text[i : i + CHUNK_SIZE].strip()
103
+ ]
104
+
105
+
106
+ def load_model():
107
+ from fastembed import TextEmbedding
108
+
109
+ return TextEmbedding(_active_model_name)
110
+
111
+
112
+ def collect_chunks(repo: str, files: list[str]):
113
+ """Liest Dateien, liefert (texts, meta, file_spans, read_errors)."""
114
+ texts, meta, spans, read_errors = [], [], {}, 0
115
+ for rel in files:
116
+ try:
117
+ with open(os.path.join(repo, rel), encoding="utf-8", errors="replace") as fh:
118
+ chunks = chunk_text(fh.read())
119
+ except OSError:
120
+ read_errors += 1
121
+ continue
122
+ start = len(texts)
123
+ for idx, chunk in enumerate(chunks):
124
+ texts.append(chunk)
125
+ meta.append(f"{rel}#{idx}")
126
+ spans[rel] = [start, len(texts)]
127
+ return texts, meta, spans, read_errors
128
+
129
+
130
+ def embed_into(model, texts: list[str], matrix, offset: int) -> None:
131
+ """Streamt Embeddings batchweise DIREKT in die (vorallokierte) Matrix."""
132
+ for i in range(0, len(texts), BATCH):
133
+ vecs = list(model.embed(texts[i : i + BATCH]))
134
+ matrix[offset + i : offset + i + len(vecs)] = np.asarray(
135
+ vecs, dtype=np.float32
136
+ )
137
+ if i % (BATCH * 8) == 0:
138
+ print(f"PROGRESS embedded={offset + i}", flush=True)
139
+
140
+
141
+ def write_matrix(dirpath: str, total: int):
142
+ return np.lib.format.open_memmap(
143
+ os.path.join(dirpath, "vectors.npy"),
144
+ mode="w+", dtype=np.float32, shape=(total, _active_dim),
145
+ )
146
+
147
+
148
+ def save_manifest(dirpath: str, repo: str, files_hashes: dict, spans: dict,
149
+ meta: list[str], build_s: float) -> None:
150
+ manifest = {
151
+ "repo": os.path.abspath(repo),
152
+ "head": git_head(repo),
153
+ "built_at": datetime.now(timezone.utc).isoformat(),
154
+ "model": _active_model_name,
155
+ "dim": _active_dim,
156
+ "chunk_size": CHUNK_SIZE,
157
+ "chunk_overlap": CHUNK_OVERLAP,
158
+ "chunks": len(meta),
159
+ "build_s": round(build_s, 2),
160
+ "files": files_hashes,
161
+ "spans": spans,
162
+ "meta": meta,
163
+ }
164
+ with open(os.path.join(dirpath, "manifest.json"), "w", encoding="utf-8") as fh:
165
+ json.dump(manifest, fh)
166
+
167
+
168
+ def load_manifest(dirpath: str) -> dict:
169
+ with open(os.path.join(dirpath, "manifest.json"), encoding="utf-8") as fh:
170
+ return json.load(fh)
171
+
172
+
173
+ def cmd_build(repo: str) -> None:
174
+ t0 = time.perf_counter()
175
+ files = git_files(repo)
176
+ print(f"BUILD files={len(files)} repo={repo}")
177
+ texts, meta, spans, read_errors = collect_chunks(repo, files)
178
+ print(f"CHUNKS total={len(texts)} read_errors={read_errors}")
179
+
180
+ dirpath = index_dir(repo)
181
+ os.makedirs(dirpath, exist_ok=True)
182
+ matrix = write_matrix(dirpath, len(texts))
183
+ model = load_model()
184
+ t_embed = time.perf_counter()
185
+ embed_into(model, texts, matrix, 0)
186
+ matrix.flush()
187
+ build_s = time.perf_counter() - t0
188
+ embed_s = time.perf_counter() - t_embed
189
+
190
+ hashes = {rel: sha_file(os.path.join(repo, rel)) for rel in spans}
191
+ save_manifest(dirpath, repo, hashes, spans, meta, build_s)
192
+ mb = len(texts) * _active_dim * 4 / 1e6
193
+ print(
194
+ f"DONE build_s={build_s:.1f} embed_s={embed_s:.1f} chunks={len(texts)} "
195
+ f"index_mb={mb:.1f} dir={dirpath}",
196
+ flush=True,
197
+ )
198
+
199
+
200
+ def cmd_update(repo: str) -> None:
201
+ """Delta: geaenderte/neue Dateien neu einbetten, geloeschte entfernen."""
202
+ t0 = time.perf_counter()
203
+ dirpath = index_dir(repo)
204
+ man = load_manifest(dirpath)
205
+ old_hashes: dict = man["files"]
206
+ old_spans: dict = man["spans"]
207
+ old_meta: list[str] = man["meta"]
208
+ old_mat = np.load(os.path.join(dirpath, "vectors.npy"))
209
+
210
+ files = git_files(repo)
211
+ texts_new, meta_new, spans_new, read_errors = collect_chunks(repo, files)
212
+ new_hashes = {rel: sha_file(os.path.join(repo, rel)) for rel in spans_new}
213
+
214
+ changed = {r for r, h in new_hashes.items() if old_hashes.get(r) != h}
215
+ deleted = set(old_hashes) - set(new_hashes)
216
+ if not changed and not deleted:
217
+ print(f"UPDATE noop delta_s={time.perf_counter() - t0:.1f} head={git_head(repo)}")
218
+ return
219
+
220
+ # Behaltene Dateien: weder geaendert noch geloescht.
221
+ kept_rels = [r for r in old_spans if r not in changed and r not in deleted]
222
+
223
+ changed_chunks: dict[str, list[str]] = {}
224
+ for rel in sorted(changed):
225
+ with open(os.path.join(repo, rel), encoding="utf-8", errors="replace") as fh:
226
+ changed_chunks[rel] = chunk_text(fh.read())
227
+
228
+ total = sum(old_spans[r][1] - old_spans[r][0] for r in kept_rels) + sum(
229
+ len(c) for c in changed_chunks.values()
230
+ )
231
+ matrix = write_matrix(dirpath, total)
232
+ model = load_model()
233
+
234
+ spans_out, meta_out, cursor = {}, [], 0
235
+ for rel in kept_rels:
236
+ a, b = old_spans[rel]
237
+ matrix[cursor : cursor + (b - a)] = old_mat[a:b]
238
+ spans_out[rel] = [cursor, cursor + (b - a)]
239
+ meta_out.extend(old_meta[a:b])
240
+ cursor += b - a
241
+ for rel in sorted(changed_chunks):
242
+ chunks = changed_chunks[rel]
243
+ embed_into(model, chunks, matrix, cursor)
244
+ spans_out[rel] = [cursor, cursor + len(chunks)]
245
+ meta_out.extend(f"{rel}#{i}" for i in range(len(chunks)))
246
+ cursor += len(chunks)
247
+ matrix.flush()
248
+
249
+ save_manifest(dirpath, repo, new_hashes, spans_out, meta_out,
250
+ time.perf_counter() - t0)
251
+ print(
252
+ f"UPDATE changed={len(changed)} deleted={len(deleted)} "
253
+ f"delta_s={time.perf_counter() - t0:.1f} chunks={total}",
254
+ flush=True,
255
+ )
256
+
257
+
258
+ def freshness(repo: str, man: dict) -> str:
259
+ current = git_head(repo)
260
+ same = "== HEAD" if current == man["head"] else f"Index {man['head']} != HEAD {current} — 'update' faellig"
261
+ return f"Index-Stand: {man['head']} ({man['built_at']}), {man['chunks']} Chunks | {same}"
262
+
263
+
264
+ def cmd_query(repo: str, question: str, top: int) -> int:
265
+ dirpath = index_dir(repo)
266
+ man = load_manifest(dirpath)
267
+ mat = np.load(os.path.join(dirpath, "vectors.npy"))
268
+ norms = np.linalg.norm(mat, axis=1, keepdims=True)
269
+ mat_n = mat / np.maximum(norms, 1e-12)
270
+
271
+ model = load_model()
272
+ t0 = time.perf_counter()
273
+ qv = np.asarray(list(model.embed([question])), dtype=np.float32)[0]
274
+ qv = qv / max(np.linalg.norm(qv), 1e-12)
275
+ scores = mat_n @ qv
276
+ idx = np.argsort(scores)[::-1][:top]
277
+ latency_ms = (time.perf_counter() - t0) * 1000
278
+
279
+ hits = [(man["meta"][i], round(float(scores[i]), 4)) for i in idx]
280
+ print(SELSTAUSKUNFT)
281
+ print(freshness(repo, man))
282
+ for m, s in hits:
283
+ print(f" {s:.4f} {m}")
284
+ print(f"query_ms={latency_ms:.1f}")
285
+
286
+ log_path = os.path.join(dirpath, "query-log.jsonl")
287
+ with open(log_path, "a", encoding="utf-8") as fh:
288
+ fh.write(json.dumps({
289
+ "ts": datetime.now(timezone.utc).isoformat(),
290
+ "query": question,
291
+ "top": hits,
292
+ "query_ms": round(latency_ms, 1),
293
+ "index_head": man["head"],
294
+ }) + "\n")
295
+ return 0
296
+
297
+
298
+ def cmd_sample(repo: str) -> int:
299
+ """Gate: Stichprobe aus dem Subjekt-Baum, Ziel >= 2/3 in Top-5."""
300
+ dirpath = index_dir(repo)
301
+ man = load_manifest(dirpath)
302
+ mat = np.load(os.path.join(dirpath, "vectors.npy"))
303
+ norms = np.linalg.norm(mat, axis=1, keepdims=True)
304
+ mat_n = mat / np.maximum(norms, 1e-12)
305
+ model = load_model()
306
+
307
+ hits_count = 0
308
+ for question, expected in SAMPLE_QUERIES:
309
+ qv = np.asarray(list(model.embed([question])), dtype=np.float32)[0]
310
+ qv = qv / max(np.linalg.norm(qv), 1e-12)
311
+ scores = mat_n @ qv
312
+ idx = np.argsort(scores)[::-1][:5]
313
+ hits = [(man["meta"][i], round(float(scores[i]), 4)) for i in idx]
314
+ ok = any(expected in m for m, _ in hits)
315
+ hits_count += ok
316
+ print(f"SAMPLE {'HIT ' if ok else 'MISS'} {question!r} expected={expected}")
317
+ for m, s in hits:
318
+ print(f" {s:.4f} {m}")
319
+ verdict = "PASS" if hits_count >= 2 else "FAIL"
320
+ print(f"SAMPLE_RESULT {hits_count}/3 gate=2/3 -> {verdict}")
321
+ return 0 if hits_count >= 2 else 1
322
+
323
+
324
+ def main() -> int:
325
+ global _active_model_name, _active_dim
326
+ if len(sys.argv) < 3:
327
+ print(__doc__)
328
+ return 2
329
+ if "--model" in sys.argv:
330
+ _active_model_name = sys.argv[sys.argv.index("--model") + 1]
331
+ from fastembed import TextEmbedding
332
+
333
+ dims = {m["model"]: m.get("dim") for m in TextEmbedding.list_supported_models()}
334
+ if _active_model_name not in dims:
335
+ print(f"MODEL_UNKNOWN {_active_model_name} — nicht im fastembed-Angebot")
336
+ return 2
337
+ _active_dim = int(dims[_active_model_name])
338
+ print(f"MODEL {_active_model_name} dim={_active_dim}")
339
+ cmd, repo = sys.argv[1], sys.argv[2]
340
+ if cmd == "build":
341
+ cmd_build(repo)
342
+ return 0
343
+ if cmd == "update":
344
+ cmd_update(repo)
345
+ return 0
346
+ if cmd == "query":
347
+ top = 5
348
+ if "--top" in sys.argv:
349
+ top = int(sys.argv[sys.argv.index("--top") + 1])
350
+ return cmd_query(repo, sys.argv[3], top)
351
+ if cmd == "sample":
352
+ return cmd_sample(repo)
353
+ print(f"unknown command: {cmd}")
354
+ return 2
355
+
356
+
357
+ if __name__ == "__main__":
358
+ sys.exit(main())
@@ -0,0 +1,26 @@
1
+ # Add only to Fredrik's active profile config.toml. Keep King as the default.
2
+
3
+ [providers."fredrik:zai"]
4
+ type = "openai_compatible"
5
+ base_url = "https://api.z.ai/api/coding/paas/v4"
6
+ api_key_file = "secrets/zai-api-key.txt"
7
+
8
+ [models."fredrik/glm-5.3-flash"]
9
+ provider = "fredrik:zai"
10
+ model = "glm-5.3-flash"
11
+ display_name = "GLM 5.3 Flash"
12
+ max_context_size = 1000000
13
+ max_output_size = 128000
14
+ capabilities = ["tool_use", "always_thinking"]
15
+ support_efforts = ["low", "high", "max"]
16
+ default_effort = "max"
17
+
18
+ [models."fredrik/glm-5.3"]
19
+ provider = "fredrik:zai"
20
+ model = "glm-5.3"
21
+ display_name = "GLM 5.3"
22
+ max_context_size = 1000000
23
+ max_output_size = 128000
24
+ capabilities = ["tool_use", "always_thinking"]
25
+ support_efforts = ["low", "high", "max"]
26
+ default_effort = "max"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.526",
3
+ "version": "9.1.536",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "node --test test/*.test.js",
12
- "prepack": "node scripts/check-release-metadata.js && node scripts/check-shell-terminal-isolation-regression.js && node scripts/check-todo-loop-regression.js && node scripts/check-telegram-loop-exactly-once-regression.js && node scripts/check-session-picker-resume-metrics-regression.js && node scripts/check-bundled-agent-spine-regression.js && node scripts/check-reload-agent-spine-regression.js && node scripts/check-todo-recovery-catalog-regression.js && node scripts/check-historical-tool-result-preview-regression.js && node scripts/check-session-start-hook-context-regression.js && node scripts/check-queue-controls-regression.js && node scripts/check-approval-queue-shortcuts-regression.js && node scripts/check-approval-observability-regression.js && node scripts/check-slash-escape-regression.js && node scripts/check-telegram-bridge-watchdog.js && node scripts/check-resume-replay-regression.js && node scripts/check-session-cancel-regression.js && node scripts/check-plugin-startup-regression.js && node scripts/check-active-profile-plugin-startup.js && node scripts/check-mcp-startup-wait-budget.js",
12
+ "prepack": "node scripts/check-release-metadata.js && node scripts/check-current-turn-read-pin-regression.js && node scripts/check-current-turn-read-pin-mutation-regression.js && node scripts/check-active-work-steer-regression.js && node scripts/check-empty-response-effort-downgrade-regression.js && node scripts/check-tool-schema-capacity-regression.js && node scripts/check-history-pressure-offload-regression.js && node scripts/check-startup-swarm-command-regression.js && node scripts/check-telegram-direct-work-resume-regression.js && node scripts/check-fredrik-glm-regression.js && node scripts/check-fredrik-model-picker-regression.js && node scripts/check-fredrik-glm-mutation-regression.js && node scripts/check-deepseek-native-max-regression.js && node scripts/check-programmatic-tool-regression.js && node scripts/check-programmatic-context-isolation-regression.js && node scripts/check-structured-subagent-output-regression.js && node scripts/check-codebase-search-packaging-regression.js && node scripts/check-copy-command-regression.js && node scripts/check-shell-terminal-isolation-regression.js && node scripts/check-todo-loop-regression.js && node scripts/check-todo-progress-regression.js && node scripts/check-telegram-loop-exactly-once-regression.js && node scripts/check-session-picker-resume-metrics-regression.js && node scripts/check-bundled-agent-spine-regression.js && node scripts/check-reload-agent-spine-regression.js && node scripts/check-todo-recovery-catalog-regression.js && node scripts/check-historical-tool-result-preview-regression.js && node scripts/check-session-start-hook-context-regression.js && node scripts/check-queue-controls-regression.js && node scripts/check-approval-queue-shortcuts-regression.js && node scripts/check-approval-observability-regression.js && node scripts/check-slash-escape-regression.js && node scripts/check-telegram-bridge-watchdog.js && node scripts/check-resume-replay-regression.js && node scripts/check-session-cancel-regression.js && node scripts/check-plugin-startup-regression.js && node scripts/check-active-profile-plugin-startup.js && node scripts/check-mcp-startup-wait-budget.js",
13
13
  "release:verify": "node scripts/check-release-metadata.js --external",
14
14
  "postinstall": "node scripts/fix-node-pty-perms.js"
15
15
  },
@@ -32,10 +32,21 @@
32
32
  "files": [
33
33
  "bin/",
34
34
  "blun.mjs",
35
+ "codebase-index/",
35
36
  "dist-web/",
36
37
  "native/",
37
38
  "release-planned-removals.json",
38
39
  "scripts/check-package-regression.js",
40
+ "scripts/check-fredrik-glm-regression.js",
41
+ "scripts/check-fredrik-glm-mutation-regression.js",
42
+ "scripts/check-deepseek-native-max-regression.js",
43
+ "scripts/check-programmatic-tool-regression.js",
44
+ "scripts/check-programmatic-context-isolation-regression.js",
45
+ "scripts/programmatic-tool-runtime.test.mjs",
46
+ "scripts/check-structured-subagent-output-regression.js",
47
+ "scripts/structured-subagent-output.test.cjs",
48
+ "scripts/check-codebase-search-packaging-regression.js",
49
+ "scripts/check-copy-command-regression.js",
39
50
  "scripts/check-active-profile-plugin-startup.js",
40
51
  "scripts/check-approval-queue-shortcuts-regression.js",
41
52
  "scripts/check-bundled-agent-spine-regression.js",
@@ -52,15 +63,25 @@
52
63
  "scripts/check-session-picker-resume-metrics-regression.js",
53
64
  "scripts/check-release-metadata.js",
54
65
  "scripts/check-historical-tool-result-preview-regression.js",
66
+ "scripts/check-current-turn-read-pin-regression.js",
67
+ "scripts/check-current-turn-read-pin-mutation-regression.js",
68
+ "scripts/check-active-work-steer-regression.js",
69
+ "scripts/check-empty-response-effort-downgrade-regression.js",
70
+ "scripts/check-tool-schema-capacity-regression.js",
71
+ "scripts/check-history-pressure-offload-regression.js",
72
+ "scripts/check-startup-swarm-command-regression.js",
73
+ "scripts/check-telegram-direct-work-resume-regression.js",
55
74
  "scripts/check-session-start-hook-context-regression.js",
56
75
  "scripts/check-telegram-loop-exactly-once-regression.js",
57
76
  "scripts/check-todo-recovery-catalog-regression.js",
58
77
  "scripts/check-todo-loop-regression.js",
78
+ "scripts/check-todo-progress-regression.js",
59
79
  "scripts/fix-node-pty-perms.js",
60
80
  "standard-skills/",
61
81
  "standard-tools/",
62
82
  "agent-spine-plugin/",
63
83
  "telegram-plugin/",
84
+ "fredrik-glm-profile.toml.example",
64
85
  "CHANGELOG.md",
65
86
  "LIESMICH.txt"
66
87
  ],
@@ -79,7 +100,8 @@
79
100
  "type": "commonjs",
80
101
  "dependencies": {
81
102
  "blun-king-cli": "^9.1.62",
82
- "node-addon-api": "^7.1.1"
103
+ "node-addon-api": "^7.1.1",
104
+ "quickjs-emscripten": "0.32.0"
83
105
  },
84
106
  "devDependencies": {}
85
107
  }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const packageRoot = process.env.BLUN_PACKAGE_UNDER_TEST
8
+ ? path.resolve(process.env.BLUN_PACKAGE_UNDER_TEST)
9
+ : path.resolve(__dirname, '..');
10
+ const policy = require(path.join(packageRoot, 'bin', 'turn-thinking-policy.cjs'));
11
+ const bundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
12
+
13
+ function assert(condition, message) {
14
+ if (!condition) throw new Error(`ACTIVE_WORK_STEER_REGRESSION: ${message}`);
15
+ }
16
+
17
+ assert(
18
+ policy.selectThinkingEffortForTurn('Wo stehst du?', 'user') === 'low',
19
+ 'an idle standalone status question must retain the fast conversation path',
20
+ );
21
+
22
+ for (const text of [
23
+ 'Wo stehst du?',
24
+ 'Kurzes Update',
25
+ 'Danke, gute Arbeit',
26
+ ]) {
27
+ assert(
28
+ policy.selectThinkingEffortForBufferedSteer(text, 'user') === undefined,
29
+ `buffered active-work steer must preserve the running turn: ${text}`,
30
+ );
31
+ }
32
+
33
+ assert(
34
+ bundle.includes('if (steerEfforts.some((effort) => effort === void 0)) return;'),
35
+ 'active-work steers no longer fall through to the running turn configuration',
36
+ );
37
+ assert(
38
+ bundle.includes('additionalMessages: steers.messages,'),
39
+ 'preserved active-work steers are not appended to the next model request',
40
+ );
41
+ assert(
42
+ bundle.includes('this.turn.commitPendingSteers(steers);'),
43
+ 'preserved active-work steers are not committed after request assembly',
44
+ );
45
+
46
+ process.stdout.write('active-work-steer-regression PASS\n');
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const { spawnSync } = require('node:child_process');
5
+ const { mkdtempSync, readFileSync, rmSync, writeFileSync } = require('node:fs');
6
+ const { tmpdir } = require('node:os');
7
+ const { join } = require('node:path');
8
+ const { pathToFileURL } = require('node:url');
9
+
10
+ const root = join(__dirname, '..');
11
+ const bundlePath = join(root, 'blun.mjs');
12
+ const scriptPath = join(root, 'codebase-index', 'codebase_index.py');
13
+ const readmePath = join(root, 'codebase-index', 'README.md');
14
+ const runtimePath = join(root, 'bin', 'codebase-search-runtime.cjs');
15
+
16
+ function readUtf8(path) {
17
+ return readFileSync(path, 'utf8');
18
+ }
19
+
20
+ function pythonCommand() {
21
+ for (const command of ['python', 'python3']) {
22
+ const result = spawnSync(command, ['--version'], { encoding: 'utf8', windowsHide: true });
23
+ if (result.status === 0) return command;
24
+ }
25
+ throw new Error('CODEBASE_SEARCH_PYTHON_MISSING');
26
+ }
27
+
28
+ function assertPackageContract() {
29
+ const packageJson = JSON.parse(readUtf8(join(root, 'package.json')));
30
+ assert.ok(packageJson.files.includes('codebase-index/'), 'package files must include codebase-index/');
31
+ assert.ok(packageJson.scripts.prepack.includes('check-codebase-search-packaging-regression.js'), 'prepack must run the packaging gate');
32
+
33
+ const source = readUtf8(bundlePath);
34
+ assert.match(source, /resolveCodebaseIndexScript\(import\.meta\.url, process\.env\["CODEBASE_INDEX_SCRIPT"\]\)/u);
35
+ assert.match(source, /\.\/bin\/codebase-search-runtime\.cjs/u);
36
+ const start = source.indexOf('function resolveScriptPath()');
37
+ const end = source.indexOf('\nvar resolveCodebaseIndexScript', start);
38
+ assert.ok(start >= 0 && end > start, 'CodebaseSearch resolver region missing');
39
+ assert.doesNotMatch(source.slice(start, end), /\.\.\/\.\.\/\.\.\/\.\.\/\.\.\/codebase-index/u, 'source-tree traversal must not survive packaging');
40
+ }
41
+
42
+ function assertResolverContract() {
43
+ const { resolveCodebaseIndexScript } = require(runtimePath);
44
+ const bundleUrl = pathToFileURL(bundlePath).href;
45
+ assert.equal(resolveCodebaseIndexScript(bundleUrl), scriptPath, 'default resolution must select the packaged script');
46
+
47
+ const temp = mkdtempSync(join(tmpdir(), 'blun-codebase-search-'));
48
+ try {
49
+ const override = join(temp, 'override.py');
50
+ writeFileSync(override, 'print("override")\n', 'utf8');
51
+ assert.equal(resolveCodebaseIndexScript(bundleUrl, override), override, 'existing absolute override must win');
52
+ assert.equal(resolveCodebaseIndexScript(bundleUrl, join(temp, 'missing.py')), '', 'missing override must fail closed');
53
+ assert.equal(resolveCodebaseIndexScript(bundleUrl, 'relative.py'), '', 'relative override must fail closed');
54
+ assert.equal(resolveCodebaseIndexScript('', override), '', 'missing module URL must fail closed');
55
+ } finally {
56
+ rmSync(temp, { recursive: true, force: true });
57
+ }
58
+ }
59
+
60
+ function assertPythonSource() {
61
+ const source = readUtf8(scriptPath);
62
+ assert.match(source, /def cmd_build\(repo: str\)/u);
63
+ assert.match(source, /def cmd_update\(repo: str\)/u);
64
+ assert.match(source, /def cmd_query\(repo: str, question: str, top: int\)/u);
65
+ assert.match(source, /HINWEIS: semantischer Index/u);
66
+ assert.match(readUtf8(readmePath), /fastembed/u);
67
+
68
+ const cache = mkdtempSync(join(tmpdir(), 'blun-codebase-pycache-'));
69
+ try {
70
+ const result = spawnSync(pythonCommand(), ['-m', 'py_compile', scriptPath], {
71
+ encoding: 'utf8',
72
+ env: { ...process.env, PYTHONPYCACHEPREFIX: cache },
73
+ windowsHide: true,
74
+ });
75
+ assert.equal(result.status, 0, `codebase_index.py syntax failed: ${result.stderr || result.stdout}`);
76
+ } finally {
77
+ rmSync(cache, { recursive: true, force: true });
78
+ }
79
+ }
80
+
81
+ function assertMutationCoverage() {
82
+ const runtime = readUtf8(runtimePath);
83
+ assert.match(runtime, /existsSync\(packaged\) \? packaged : ''/u, 'packaged path existence gate missing');
84
+ assert.match(runtime, /!isAbsolute\(envPath\) \|\| !existsSync\(envPath\)/u, 'override boundary missing');
85
+ assert.doesNotMatch(runtime, /process\.cwd/u, 'resolver must not search arbitrary working directories');
86
+ }
87
+
88
+ assertPackageContract();
89
+ assertResolverContract();
90
+ assertPythonSource();
91
+ assertMutationCoverage();
92
+ process.stdout.write('codebase-search-packaging PASS\n');