pantheon-opencode 1.4.3 → 1.5.0-beta.2

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 (99) hide show
  1. package/.pantheon/code-mode/README.md +93 -0
  2. package/.pantheon/code-mode/checkpoint-session.py +7 -0
  3. package/.pantheon/code-mode/checkpoint-session.sh +11 -0
  4. package/.pantheon/code-mode/checkpoint_session.py +481 -0
  5. package/.pantheon/code-mode/compress-inline.py +450 -0
  6. package/.pantheon/code-mode/eval-llm-judge.py +255 -0
  7. package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
  8. package/.pantheon/code-mode/eval-run.py +197 -0
  9. package/.pantheon/code-mode/eval-static.py +321 -0
  10. package/.pantheon/code-mode/example-sync.sh +5 -0
  11. package/.pantheon/code-mode/session-end-save.py +186 -0
  12. package/.pantheon/code-mode/session-end-save.sh +18 -0
  13. package/.pantheon/code-mode/test-allowed.sh +5 -0
  14. package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
  15. package/.pantheon/code-mode/test-env.sh +7 -0
  16. package/.pantheon/code-mode/test-prlimit.sh +9 -0
  17. package/.pantheon/code-mode/test-timeout.sh +6 -0
  18. package/README.md +161 -1101
  19. package/README.pt-BR.md +222 -0
  20. package/bin/pantheon-init.mjs +30 -11
  21. package/opencode.json +1 -0
  22. package/package.json +14 -3
  23. package/plugin.json +15 -0
  24. package/scripts/_pantheon_paths.py +48 -7
  25. package/scripts/ci-validate-yaml.py +7 -2
  26. package/scripts/code_mode_server.py +33 -2
  27. package/scripts/cost.mjs +30 -14
  28. package/scripts/doctor.mjs +135 -127
  29. package/scripts/generate-preset-docs.mjs +127 -66
  30. package/scripts/generate-prompts.sh +2 -2
  31. package/scripts/install/cli-ui.mjs +8 -1
  32. package/scripts/install/config-migration.mjs +401 -0
  33. package/scripts/install/health-check.mjs +3 -3
  34. package/scripts/install/model-picker.mjs +72 -23
  35. package/scripts/install/opencode-version.mjs +47 -0
  36. package/scripts/install/opencode.mjs +261 -88
  37. package/scripts/install/plugin.mjs +112 -42
  38. package/scripts/install/shared.mjs +13 -158
  39. package/scripts/install-mcp.mjs +44 -232
  40. package/scripts/mcp_persistence_server.py +382 -16
  41. package/scripts/mcp_resources_server.py +144 -40
  42. package/scripts/memory_mcp_server.py +2 -2
  43. package/scripts/probe-context-rehydrate.mjs +240 -0
  44. package/scripts/probe-pantheon-cost.mjs +304 -0
  45. package/scripts/recover-zenodo-deposition.mjs +146 -0
  46. package/scripts/sync-tui.mjs +32 -7
  47. package/scripts/test-opencode-v1-v2-sandbox.sh +944 -0
  48. package/scripts/uninstall.mjs +298 -954
  49. package/scripts/validate-zenodo-release.mjs +154 -0
  50. package/scripts/zenodo-artifact.mjs +21 -0
  51. package/scripts/zenodo-release-state.mjs +53 -0
  52. package/src/agents/zeus.md +25 -14
  53. package/src/mcp/_pantheon_paths.py +48 -7
  54. package/src/mcp/code_mode_server.py +33 -2
  55. package/src/mcp/eval_store.py +1 -1
  56. package/src/mcp/mcp_persistence_server.py +382 -16
  57. package/src/mcp/mcp_resources_server.py +144 -40
  58. package/src/mcp/pantheon_vision_server.py +1 -1
  59. package/src/mcp/toon_codec.py +491 -0
  60. package/src/pantheon/background-job-board.ts +67 -1
  61. package/src/pantheon/cost-command.ts +60 -39
  62. package/src/pantheon/delegate-manager.ts +669 -0
  63. package/src/pantheon/delegation-classifier.ts +307 -0
  64. package/src/pantheon/delegation-enforce.ts +13 -12
  65. package/src/pantheon/delegation-finalize.ts +374 -0
  66. package/src/pantheon/delegation-notify.ts +212 -0
  67. package/src/pantheon/delegation-toast.ts +73 -0
  68. package/src/pantheon/delegation.ts +1120 -0
  69. package/src/pantheon/goal-loop.ts +2 -2
  70. package/src/pantheon/goal-store.ts +1 -1
  71. package/src/pantheon/hashline/tool.ts +2 -2
  72. package/src/pantheon/model-command.ts +2 -1
  73. package/src/pantheon/permission-eval.ts +284 -0
  74. package/src/pantheon/step-cap.ts +110 -0
  75. package/src/pantheon/token-opt.ts +781 -0
  76. package/src/pantheon/toon-codec.ts +481 -0
  77. package/src/pantheon/tui-version-check.ts +1 -3
  78. package/src/pantheon/v2-bridge.ts +129 -0
  79. package/src/pantheon/v2-events.ts +131 -0
  80. package/src/pantheon/v2-hooks.ts +287 -0
  81. package/src/pantheon/v2-tool-definitions.ts +331 -0
  82. package/src/pantheon/zeus-delegate-with-retry.ts +406 -0
  83. package/src/plugin-v2/index.ts +21 -0
  84. package/src/plugin-v2.ts +824 -0
  85. package/src/plugin.ts +236 -42
  86. package/src/plugins/tui/dist/server.d.ts +1 -1
  87. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  88. package/src/plugins/tui/dist/server.js +1 -1
  89. package/src/plugins/tui/dist/server.js.map +1 -1
  90. package/src/plugins/tui/dist/tui.d.ts +10 -7
  91. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  92. package/src/plugins/tui/dist/tui.js +11 -8
  93. package/src/plugins/tui/dist/tui.js.map +1 -1
  94. package/src/plugins/tui/dist/tui.tsx +22 -16
  95. package/src/plugins/tui/package-lock.json +51 -51
  96. package/src/plugins/tui/package.json +7 -7
  97. package/src/plugins/tui/src/index.tsx +28 -24
  98. package/src/skills/README.md +0 -2
  99. package/src/pantheon/tool-context.ts +0 -41
@@ -0,0 +1,450 @@
1
+ #!/usr/bin/env python3
2
+ """compress-inline.py — Pantheon inline context compression.
3
+
4
+ Called by agents via execute_code_script('compress-inline.py') during
5
+ active sessions. Replaces third-party magic-compact plugin with a
6
+ Pantheon-native solution.
7
+
8
+ Modes:
9
+ score --text "<text>" → Score text, output priority JSON
10
+ compress --text "<text>" [--files "<files>"] → Scrub + score + 3-line summary
11
+ stats --text "<text>" → Token/line/char estimates
12
+ batch --files "file1,file2" → Read & compress multiple files
13
+
14
+ Optional (all modes):
15
+ --from-agent <name> Source agent for downstream relevance scoring
16
+ --to-agent <name> Target agent for downstream relevance scoring
17
+ --blocker Hint that entry was a blocker
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import importlib.util
24
+ import json
25
+ import os
26
+ import re
27
+ import sys
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ # Security scrubbing — delegated to canonical scripts/scrub_secrets.py (imported above).
32
+ _scripts_dir = Path(__file__).resolve().parents[2] / "scripts"
33
+ _spec = importlib.util.spec_from_file_location(
34
+ "scrub_secrets", _scripts_dir / "scrub-secrets.py"
35
+ )
36
+ _scrub_mod = importlib.util.module_from_spec(_spec)
37
+ _spec.loader.exec_module(_scrub_mod)
38
+ scrub = _scrub_mod.scrub
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Priority scoring engine (fully deterministic — no LLM)
43
+ # ---------------------------------------------------------------------------
44
+
45
+ # Each entry: (name, compiled_pattern, impact, risk, novelty)
46
+ # Names and values from .opencode/skills/context-compression/SKILL.md
47
+ KeywordEntry = tuple[str, re.Pattern, float, float, float]
48
+
49
+ KEYWORD_MAP: list[KeywordEntry] = [
50
+ # schema/migration
51
+ ("schema", re.compile(r"schema", re.IGNORECASE), 1.0, 1.0, 0.6),
52
+ ("migration", re.compile(r"migration", re.IGNORECASE), 1.0, 1.0, 0.4),
53
+ ("new table", re.compile(r"new\s+table", re.IGNORECASE), 1.0, 0.8, 0.9),
54
+ ("new column", re.compile(r"new\s+column", re.IGNORECASE), 0.7, 0.8, 0.6),
55
+ # auth/security
56
+ ("auth", re.compile(r"auth", re.IGNORECASE), 1.0, 1.0, 0.5),
57
+ ("login", re.compile(r"login", re.IGNORECASE), 1.0, 1.0, 0.4),
58
+ ("permission", re.compile(r"permission", re.IGNORECASE), 0.8, 1.0, 0.5),
59
+ ("role", re.compile(r"role", re.IGNORECASE), 0.8, 0.8, 0.4),
60
+ ("JWT", re.compile(r"JWT", re.IGNORECASE), 0.8, 1.0, 0.4),
61
+ ("OAuth", re.compile(r"OAuth", re.IGNORECASE), 0.8, 1.0, 0.5),
62
+ ("password", re.compile(r"password", re.IGNORECASE), 0.7, 1.0, 0.3),
63
+ ("encrypt", re.compile(r"encrypt", re.IGNORECASE), 0.7, 1.0, 0.5),
64
+ ("token", re.compile(r"token", re.IGNORECASE), 0.7, 0.8, 0.3),
65
+ ("security", re.compile(r"security", re.IGNORECASE), 0.8, 1.0, 0.4),
66
+ # database
67
+ ("index", re.compile(r"index", re.IGNORECASE), 0.5, 0.6, 0.3),
68
+ ("foreign key", re.compile(r"foreign\s+key", re.IGNORECASE), 0.8, 0.9, 0.5),
69
+ ("constraint", re.compile(r"constraint", re.IGNORECASE), 0.6, 0.7, 0.3),
70
+ # api
71
+ ("endpoint", re.compile(r"endpoint", re.IGNORECASE), 0.9, 0.6, 0.5),
72
+ ("route", re.compile(r"route", re.IGNORECASE), 0.8, 0.5, 0.4),
73
+ ("API", re.compile(r"\bAPI\b", re.IGNORECASE), 0.8, 0.5, 0.4),
74
+ # architecture
75
+ ("service", re.compile(r"service", re.IGNORECASE), 0.7, 0.4, 0.4),
76
+ # structure
77
+ ("new file", re.compile(r"new\s+file", re.IGNORECASE), 0.6, 0.3, 0.8),
78
+ # code-quality
79
+ ("refactor", re.compile(r"refactor", re.IGNORECASE), 0.5, 0.7, 0.6),
80
+ ("rename", re.compile(r"rename", re.IGNORECASE), 0.4, 0.6, 0.3),
81
+ ("delete", re.compile(r"delete", re.IGNORECASE), 0.5, 0.7, 0.2),
82
+ ("deprecat", re.compile(r"deprecat", re.IGNORECASE), 0.4, 0.4, 0.3),
83
+ # infrastructure
84
+ ("config", re.compile(r"config", re.IGNORECASE), 0.5, 0.6, 0.3),
85
+ ("Docker", re.compile(r"Docker", re.IGNORECASE), 0.7, 0.6, 0.3),
86
+ ("deploy", re.compile(r"deploy", re.IGNORECASE), 0.8, 0.8, 0.2),
87
+ # style
88
+ ("CSS", re.compile(r"\bCSS\b", re.IGNORECASE), 0.2, 0.1, 0.2),
89
+ ("style", re.compile(r"style", re.IGNORECASE), 0.2, 0.1, 0.2),
90
+ # trivial
91
+ ("typo", re.compile(r"typo", re.IGNORECASE), 0.0, 0.0, 0.0),
92
+ ("comment", re.compile(r"comment", re.IGNORECASE), 0.1, 0.0, 0.0),
93
+ # documentation
94
+ ("README", re.compile(r"README", re.IGNORECASE), 0.2, 0.0, 0.1),
95
+ ("docstring", re.compile(r"docstring", re.IGNORECASE), 0.2, 0.0, 0.1),
96
+ ]
97
+
98
+ WEIGHTS: dict[str, float] = {
99
+ "impact": 0.30,
100
+ "risk": 0.25,
101
+ "novelty": 0.20,
102
+ "blockers": 0.15,
103
+ "downstream": 0.10,
104
+ }
105
+
106
+ # Agent-pair downstream relevance table
107
+ # From .opencode/skills/context-compression/SKILL.md lines 110-118
108
+ AGENT_PAIR_TABLE: dict[tuple[str, str], float] = {
109
+ ("hermes", "hermes"): 1.0,
110
+ ("hermes", "aphrodite"): 0.9,
111
+ ("hermes", "demeter"): 0.8,
112
+ ("hermes", "themis"): 0.7,
113
+ ("hermes", "mnemosyne"): 0.3,
114
+ ("hermes", "hephaestus"): 0.6,
115
+ ("hermes", "prometheus"): 0.5,
116
+ ("aphrodite", "hermes"): 0.9,
117
+ ("aphrodite", "aphrodite"): 1.0,
118
+ ("aphrodite", "demeter"): 0.3,
119
+ ("aphrodite", "themis"): 0.7,
120
+ ("aphrodite", "mnemosyne"): 0.3,
121
+ ("aphrodite", "hephaestus"): 0.5,
122
+ ("aphrodite", "prometheus"): 0.3,
123
+ ("demeter", "hermes"): 0.9,
124
+ ("demeter", "aphrodite"): 0.3,
125
+ ("demeter", "demeter"): 1.0,
126
+ ("demeter", "themis"): 0.7,
127
+ ("demeter", "mnemosyne"): 0.3,
128
+ ("demeter", "hephaestus"): 0.6,
129
+ ("demeter", "prometheus"): 0.6,
130
+ ("themis", "hermes"): 0.8,
131
+ ("themis", "aphrodite"): 0.8,
132
+ ("themis", "demeter"): 0.8,
133
+ ("themis", "themis"): 1.0,
134
+ ("themis", "mnemosyne"): 0.5,
135
+ ("themis", "hephaestus"): 0.8,
136
+ ("themis", "prometheus"): 0.8,
137
+ ("hephaestus", "hermes"): 0.6,
138
+ ("hephaestus", "aphrodite"): 0.5,
139
+ ("hephaestus", "demeter"): 0.6,
140
+ ("hephaestus", "themis"): 0.7,
141
+ ("hephaestus", "mnemosyne"): 0.3,
142
+ ("hephaestus", "hephaestus"): 1.0,
143
+ ("hephaestus", "prometheus"): 0.4,
144
+ ("prometheus", "hermes"): 0.6,
145
+ ("prometheus", "aphrodite"): 0.4,
146
+ ("prometheus", "demeter"): 0.6,
147
+ ("prometheus", "themis"): 0.7,
148
+ ("prometheus", "mnemosyne"): 0.3,
149
+ ("prometheus", "hephaestus"): 0.4,
150
+ ("prometheus", "prometheus"): 1.0,
151
+ ("mnemosyne", "hermes"): 0.3,
152
+ ("mnemosyne", "aphrodite"): 0.3,
153
+ ("mnemosyne", "demeter"): 0.3,
154
+ ("mnemosyne", "themis"): 0.5,
155
+ ("mnemosyne", "mnemosyne"): 1.0,
156
+ ("mnemosyne", "hephaestus"): 0.3,
157
+ ("mnemosyne", "prometheus"): 0.3,
158
+ }
159
+
160
+ BLOCKER_KEYWORDS: list[re.Pattern] = [
161
+ re.compile(r"blocked", re.IGNORECASE),
162
+ re.compile(r"unblocked", re.IGNORECASE),
163
+ re.compile(r"blocker", re.IGNORECASE),
164
+ re.compile(r"depend", re.IGNORECASE),
165
+ re.compile(r"dependency", re.IGNORECASE),
166
+ ]
167
+
168
+
169
+ def compute_score(
170
+ text: str,
171
+ from_agent: str | None = None,
172
+ to_agent: str | None = None,
173
+ files_changed: int = 0,
174
+ blocker_hint: bool = False,
175
+ ) -> dict[str, Any]:
176
+ """Score *text* across 5 deterministic dimensions.
177
+
178
+ Returns dict with:
179
+ score, band, impact, risk, novelty, blockers, downstream, keywords_found
180
+ """
181
+ impact_scores: list[float] = []
182
+ risk_scores: list[float] = []
183
+ novelty_scores: list[float] = []
184
+ keywords_found: list[str] = []
185
+
186
+ for name, pattern, imp, rsk, nov in KEYWORD_MAP:
187
+ if pattern.search(text):
188
+ impact_scores.append(imp)
189
+ risk_scores.append(rsk)
190
+ novelty_scores.append(nov)
191
+ keywords_found.append(name)
192
+
193
+ impact = max(impact_scores) if impact_scores else 0.0
194
+ risk = max(risk_scores) if risk_scores else 0.0
195
+ novelty = max(novelty_scores) if novelty_scores else 0.0
196
+
197
+ # Novelty bonus for file count (overrides keyword score)
198
+ if files_changed >= 10:
199
+ novelty = max(novelty, 1.0)
200
+ elif files_changed >= 5:
201
+ novelty = max(novelty, 0.8)
202
+
203
+ # Blockers dimension — detect in text or use explicit hint
204
+ blockers: float = 0.0
205
+ for bp in BLOCKER_KEYWORDS:
206
+ if bp.search(text):
207
+ # unblocked/unblocks = positive blocker resolution
208
+ if re.search(r"unblock", text, re.IGNORECASE):
209
+ blockers = max(blockers, 0.8)
210
+ else:
211
+ blockers = max(blockers, 0.5)
212
+ if blocker_hint:
213
+ blockers = max(blockers, 0.8)
214
+
215
+ # Downstream relevance
216
+ downstream: float = 0.5 # default when agent pair unknown
217
+ if from_agent and to_agent:
218
+ key = (from_agent.lower().strip(), to_agent.lower().strip())
219
+ downstream = AGENT_PAIR_TABLE.get(key, 0.5)
220
+
221
+ score = (
222
+ impact * WEIGHTS["impact"]
223
+ + risk * WEIGHTS["risk"]
224
+ + novelty * WEIGHTS["novelty"]
225
+ + blockers * WEIGHTS["blockers"]
226
+ + downstream * WEIGHTS["downstream"]
227
+ )
228
+
229
+ # Priority band (from SKILL.md line 125-131)
230
+ if score >= 0.75:
231
+ band = "CRITICAL"
232
+ elif score >= 0.50:
233
+ band = "HIGH"
234
+ elif score >= 0.25:
235
+ band = "MEDIUM"
236
+ else:
237
+ band = "LOW"
238
+
239
+ return {
240
+ "score": round(score, 2),
241
+ "band": band,
242
+ "impact": impact,
243
+ "risk": risk,
244
+ "novelty": novelty,
245
+ "blockers": blockers,
246
+ "downstream": downstream,
247
+ "keywords_found": keywords_found,
248
+ }
249
+
250
+
251
+ # ---------------------------------------------------------------------------
252
+ # Stats
253
+ # ---------------------------------------------------------------------------
254
+
255
+
256
+ def compute_stats(text: str) -> dict[str, int]:
257
+ """Compute token estimate (len/4), line count, char count."""
258
+ return {
259
+ "tokens": max(1, len(text) // 4),
260
+ "lines": len(text.splitlines()),
261
+ "chars": len(text),
262
+ }
263
+
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # Compression
267
+ # ---------------------------------------------------------------------------
268
+
269
+
270
+ def compress(
271
+ text: str,
272
+ files: str = "",
273
+ from_agent: str | None = None,
274
+ to_agent: str | None = None,
275
+ ) -> str:
276
+ """Scrub + score + compress into ≤3 line summary."""
277
+ scrubbed = scrub(text)[0]
278
+
279
+ file_count = len([f for f in files.split(",") if f.strip()]) if files else 0
280
+ result = compute_score(
281
+ scrubbed,
282
+ from_agent=from_agent,
283
+ to_agent=to_agent,
284
+ files_changed=file_count,
285
+ )
286
+
287
+ # Summary = first meaningful line of scrubbed text, ≤80 chars
288
+ lines = [l.strip() for l in scrubbed.splitlines() if l.strip()]
289
+ summary: str = ""
290
+ if lines:
291
+ summary = lines[0][:80]
292
+ if len(lines[0]) > 80:
293
+ summary += "…"
294
+
295
+ output_parts = [f"- What changed: {summary}", f"- Priority: {result['band']} (score: {result['score']:.2f})"]
296
+ if files:
297
+ output_parts.append(f"- Files: {files}")
298
+
299
+ return "\n".join(output_parts)
300
+
301
+
302
+ # ---------------------------------------------------------------------------
303
+ # Batch processing
304
+ # ---------------------------------------------------------------------------
305
+
306
+
307
+ def process_batch(
308
+ file_paths: list[str],
309
+ from_agent: str | None = None,
310
+ to_agent: str | None = None,
311
+ ) -> str:
312
+ """Read multiple files, scrub + score + compress each."""
313
+ parts: list[str] = []
314
+ for fpath in file_paths:
315
+ fpath = fpath.strip()
316
+ if not fpath:
317
+ continue
318
+ try:
319
+ with open(fpath, encoding="utf-8") as fh:
320
+ content = fh.read()
321
+ except (OSError, IOError) as exc:
322
+ parts.append(f"- {fpath}: ERROR — {exc}")
323
+ continue
324
+
325
+ scrubbed = scrub(content)[0]
326
+ result = compute_score(
327
+ scrubbed,
328
+ from_agent=from_agent,
329
+ to_agent=to_agent,
330
+ files_changed=1,
331
+ )
332
+
333
+ # Summary: first meaningful line
334
+ content_lines = [l.strip() for l in scrubbed.splitlines() if l.strip()]
335
+ summary = "(empty)"
336
+ if content_lines:
337
+ summary = content_lines[0][:80]
338
+ if len(content_lines[0]) > 80:
339
+ summary += "…"
340
+
341
+ base = os.path.basename(fpath)
342
+ parts.append(f"### {base}")
343
+ parts.append(f"- What changed: {summary}")
344
+ parts.append(f"- Priority: {result['band']} (score: {result['score']:.2f})")
345
+ parts.append("")
346
+
347
+ return "\n".join(parts).rstrip()
348
+
349
+
350
+ # ---------------------------------------------------------------------------
351
+ # CLI
352
+ # ---------------------------------------------------------------------------
353
+
354
+
355
+ def main() -> None:
356
+ parser = argparse.ArgumentParser(
357
+ description="Pantheon inline compression — score, compress, or stat text.",
358
+ )
359
+ parser.add_argument("mode", choices=["score", "compress", "stats", "batch"], help="Operation mode")
360
+ parser.add_argument("--text", type=str, default="", help="Input text to process")
361
+ parser.add_argument(
362
+ "--files",
363
+ type=str,
364
+ default="",
365
+ help="Comma-separated file paths (batch mode, or for file count in scoring)",
366
+ )
367
+ parser.add_argument("--from-agent", type=str, default=None, help="Source agent for downstream relevance")
368
+ parser.add_argument("--to-agent", type=str, default=None, help="Target agent for downstream relevance")
369
+ parser.add_argument("--blocker", action="store_true", default=False, help="Hint that entry was a blocker")
370
+
371
+ args = parser.parse_args()
372
+
373
+ # -----------------------------------------------------------------------
374
+ # batch mode
375
+ # -----------------------------------------------------------------------
376
+ if args.mode == "batch":
377
+ if not args.files:
378
+ print("Error: --files is required for batch mode", file=sys.stderr)
379
+ sys.exit(1)
380
+ file_list = [f.strip() for f in args.files.split(",") if f.strip()]
381
+ output = process_batch(file_list, from_agent=args.from_agent, to_agent=args.to_agent)
382
+ print(output)
383
+ return
384
+
385
+ # -----------------------------------------------------------------------
386
+ # Empty input guard
387
+ # -----------------------------------------------------------------------
388
+ text = args.text
389
+ if not text:
390
+ if args.mode == "score":
391
+ print(
392
+ json.dumps(
393
+ {
394
+ "score": 0.0,
395
+ "band": "LOW",
396
+ "impact": 0.0,
397
+ "risk": 0.0,
398
+ "novelty": 0.0,
399
+ "blockers": 0.0,
400
+ "downstream": 0.5,
401
+ "keywords_found": [],
402
+ }
403
+ )
404
+ )
405
+ elif args.mode == "compress":
406
+ print("- What changed: (empty)")
407
+ print("- Priority: LOW (score: 0.00)")
408
+ elif args.mode == "stats":
409
+ print(json.dumps({"tokens": 0, "lines": 0, "chars": 0}))
410
+ return
411
+
412
+ # -----------------------------------------------------------------------
413
+ # score mode
414
+ # -----------------------------------------------------------------------
415
+ if args.mode == "score":
416
+ file_count = len([f for f in args.files.split(",") if f.strip()]) if args.files else 0
417
+ result = compute_score(
418
+ text,
419
+ from_agent=args.from_agent,
420
+ to_agent=args.to_agent,
421
+ files_changed=file_count,
422
+ blocker_hint=args.blocker,
423
+ )
424
+ print(json.dumps(result))
425
+ return
426
+
427
+ # -----------------------------------------------------------------------
428
+ # compress mode
429
+ # -----------------------------------------------------------------------
430
+ if args.mode == "compress":
431
+ output = compress(
432
+ text,
433
+ files=args.files,
434
+ from_agent=args.from_agent,
435
+ to_agent=args.to_agent,
436
+ )
437
+ print(output)
438
+ return
439
+
440
+ # -----------------------------------------------------------------------
441
+ # stats mode
442
+ # -----------------------------------------------------------------------
443
+ if args.mode == "stats":
444
+ stats = compute_stats(text)
445
+ print(json.dumps(stats))
446
+ return
447
+
448
+
449
+ if __name__ == "__main__":
450
+ main()