pantheon-opencode 1.5.2 → 1.6.0-beta.10

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 (146) hide show
  1. package/.pantheon/code-mode/README.md +4 -2
  2. package/.pantheon/code-mode/checkpoint_session.py +138 -102
  3. package/.pantheon/code-mode/compress-inline.py +102 -57
  4. package/.pantheon/code-mode/manifest.json +2 -8
  5. package/AGENTS.global.md +182 -0
  6. package/AGENTS.md +10 -891
  7. package/CHANGELOG.md +343 -0
  8. package/README.md +577 -79
  9. package/README.pt-BR.md +345 -60
  10. package/bin/pantheon-init.mjs +26 -3
  11. package/commands/pantheon-audit.md +7 -1
  12. package/commands/pantheon-consolidate.md +1 -1
  13. package/commands/pantheon-deepwork.md +14 -3
  14. package/commands/pantheon-focus.md +17 -0
  15. package/commands/pantheon-optimize.md +2 -2
  16. package/commands/pantheon-vision.md +61 -12
  17. package/package.json +30 -11
  18. package/plugin.json +2 -2
  19. package/pyproject.toml +11 -16
  20. package/scripts/build-agents-md.mjs +376 -58
  21. package/scripts/changelog-merge.mjs +161 -0
  22. package/scripts/check-plugin-v2-coverage.mjs +213 -0
  23. package/scripts/ci-validate-yaml.py +7 -5
  24. package/scripts/{code_mode_server.py → code_mode.py} +119 -27
  25. package/scripts/cost.mjs +244 -41
  26. package/scripts/doctor.mjs +236 -29
  27. package/scripts/install/agents-md.mjs +4 -4
  28. package/scripts/install/cli-ui.mjs +1 -1
  29. package/scripts/install/config-migration.mjs +319 -6
  30. package/scripts/install/health-check.mjs +26 -37
  31. package/scripts/install/interactive.mjs +7 -7
  32. package/scripts/install/migrate.mjs +1 -1
  33. package/scripts/install/opencode-version.mjs +246 -4
  34. package/scripts/install/opencode.mjs +627 -179
  35. package/scripts/install/plugin.mjs +43 -9
  36. package/scripts/install/shared.mjs +120 -12
  37. package/scripts/install/vision-prompt.mjs +161 -0
  38. package/scripts/install-mcp.mjs +28 -9
  39. package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
  40. package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
  41. package/scripts/{memory_mcp_server.py → memory_mcp.py} +372 -198
  42. package/scripts/postinstall.mjs +0 -7
  43. package/scripts/probe-context-rehydrate.mjs +36 -13
  44. package/scripts/probe-pantheon-cost.mjs +71 -15
  45. package/scripts/prune.mjs +2 -2
  46. package/scripts/run-node-tests.mjs +189 -0
  47. package/scripts/sync-artifacts.mjs +26 -13
  48. package/scripts/sync-tui.mjs +24 -3
  49. package/scripts/test-opencode-v2-sandbox.sh +1913 -0
  50. package/scripts/validate-routing.mjs +43 -3
  51. package/scripts/validate-sandbox-install.mjs +138 -0
  52. package/scripts/versioning.mjs +13 -1
  53. package/scripts/vision-status.mjs +682 -0
  54. package/skills-lock.json +15 -16
  55. package/src/agents/aphrodite.md +20 -37
  56. package/src/agents/apollo.md +7 -3
  57. package/src/agents/athena.md +63 -35
  58. package/src/agents/demeter.md +8 -28
  59. package/src/agents/gaia.md +1 -1
  60. package/src/agents/hephaestus.md +10 -21
  61. package/src/agents/hermes.md +15 -38
  62. package/src/agents/iris.md +1 -1
  63. package/src/agents/mnemosyne.md +23 -60
  64. package/src/agents/nyx.md +1 -1
  65. package/src/agents/prometheus.md +16 -20
  66. package/src/agents/talos.md +10 -2
  67. package/src/agents/themis.md +5 -5
  68. package/src/agents/zeus.md +99 -130
  69. package/src/instructions/agent-return-format.instructions.md +18 -3
  70. package/src/instructions/backend-standards.instructions.md +2 -0
  71. package/src/instructions/frontend-standards.instructions.md +3 -1
  72. package/src/instructions/memory-protocol.instructions.md +11 -110
  73. package/src/instructions/zeus-anti-stall.instructions.md +14 -61
  74. package/src/instructions/zeus-communication-rules.instructions.md +0 -1
  75. package/src/instructions/zeus-council-synthesis.instructions.md +32 -260
  76. package/src/instructions/zeus-memory-operations.instructions.md +26 -0
  77. package/src/instructions/zeus-timeout-retry.instructions.md +40 -87
  78. package/src/mcp/{code_mode_server.py → code_mode.py} +119 -27
  79. package/src/mcp/eval_store.py +2 -2
  80. package/src/mcp/mcp_codemap_module.py +559 -129
  81. package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
  82. package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
  83. package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +373 -250
  84. package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +396 -40
  85. package/src/mcp/requirements-mcp.txt +10 -6
  86. package/src/mcp/requirements-vision.txt +4 -2
  87. package/src/pantheon/cost-command.ts +399 -101
  88. package/src/pantheon/delegation-enforce.ts +10 -1
  89. package/src/pantheon/model-command.ts +18 -7
  90. package/src/pantheon/presets.d.mts +0 -8
  91. package/src/pantheon/presets.mjs +5 -32
  92. package/src/pantheon/todo-enforcer.ts +1 -1
  93. package/src/pantheon/v2-bounded-read.ts +640 -0
  94. package/src/pantheon/v2-delegation-enforce.ts +214 -0
  95. package/src/pantheon/v2-execute-after.ts +186 -0
  96. package/src/pantheon/v2-tools.ts +429 -0
  97. package/src/pantheon/v2-unsupported.d.mts +11 -0
  98. package/src/pantheon/v2-unsupported.mjs +90 -0
  99. package/src/pantheon/v2-vision.ts +622 -0
  100. package/src/pantheon/vision-local.ts +531 -0
  101. package/src/pantheon/vision.ts +1161 -116
  102. package/src/plugin-v2.ts +1244 -288
  103. package/src/plugin.ts +12 -9
  104. package/src/plugins/tui/dist/server.d.ts +1 -2
  105. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  106. package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
  107. package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
  108. package/src/plugins/tui/dist/tui-v2.js +659 -0
  109. package/src/plugins/tui/dist/tui-v2.js.map +1 -0
  110. package/src/plugins/tui/dist/tui.d.ts +85 -82
  111. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  112. package/src/plugins/tui/dist/tui.js +72 -59
  113. package/src/plugins/tui/index-v2.ts +6 -0
  114. package/src/plugins/tui/package-lock.json +5504 -1341
  115. package/src/plugins/tui/package.json +25 -15
  116. package/src/plugins/tui/src/index-v2.tsx +734 -0
  117. package/src/plugins/tui/src/index.tsx +106 -85
  118. package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
  119. package/src/plugins/tui/tsconfig.build.json +5 -0
  120. package/src/plugins/tui/tsconfig.typecheck.json +5 -0
  121. package/src/plugins/tui/tsdown.config.ts +22 -2
  122. package/src/plugins/tui/tui-v2.ts +2 -0
  123. package/src/plugins/tui/tui.ts +3 -0
  124. package/src/routing.yml +11 -22
  125. package/src/skills/README.md +34 -39
  126. package/src/skills/agent-coordination/SKILL.md +7 -2
  127. package/src/skills/artifact-management/SKILL.md +12 -5
  128. package/src/skills/auto-continue/SKILL.md +54 -95
  129. package/src/skills/code-review-checklist/SKILL.md +5 -5
  130. package/src/skills/context-compression/SKILL.md +26 -879
  131. package/src/skills/session-goal/SKILL.md +14 -5
  132. package/src/skills/spec-driven-development/SKILL.md +3 -1
  133. package/src/skills/tdd-with-agents/SKILL.md +18 -10
  134. package/.pantheon/code-mode/eval-llm-judge.py +0 -255
  135. package/.pantheon/code-mode/eval-monte-carlo.py +0 -368
  136. package/.pantheon/code-mode/eval-run.py +0 -197
  137. package/.pantheon/code-mode/eval-static.py +0 -321
  138. package/.pantheon/code-mode/session-end-save.py +0 -186
  139. package/.pantheon/code-mode/session-end-save.sh +0 -18
  140. package/opencode.json +0 -58
  141. package/scripts/recover-zenodo-deposition.mjs +0 -146
  142. package/scripts/test-opencode-v1-v2-sandbox.sh +0 -862
  143. package/scripts/validate-zenodo-release.mjs +0 -154
  144. package/scripts/zenodo-artifact.mjs +0 -94
  145. package/scripts/zenodo-release-state.mjs +0 -53
  146. package/src/skills/orchestration-workflow/SKILL.md +0 -311
@@ -10,10 +10,12 @@ Orchestration scripts executed via the `pantheon-code-mode` MCP server
10
10
  | `checkpoint_session.py` | Python | Full checkpoint management: init, save, status, resume, list, health, archive, cleanup |
11
11
  | `checkpoint-session.sh` | Bash | Shell wrapper for `checkpoint_session.py` |
12
12
  | `compress-inline.py` | Python | Inline context compression — score, compress, stats, batch modes |
13
- | `session-end-save.py` | Python | Session-end Vector DB backup scaffold |
14
- | `session-end-save.sh` | Bash | Shell wrapper for `session-end-save.py` |
15
13
  | `example-sync.sh` | Bash | Minimal demo script for testing `execute_code_script` |
16
14
 
15
+ Session persistence is handled by the persistence MCP server. The code-mode
16
+ payload contains orchestration, checkpoint, compression, and evaluation
17
+ helpers only; it does not export or back up a database.
18
+
17
19
  ## checkpoint_session.py Commands
18
20
 
19
21
  | Command | Description |
@@ -23,8 +23,8 @@ from __future__ import annotations
23
23
 
24
24
  import hashlib
25
25
  import json
26
- import sys
27
26
  import shutil
27
+ import sys
28
28
  from datetime import UTC, datetime, timedelta
29
29
  from pathlib import Path
30
30
 
@@ -236,79 +236,100 @@ def cmd_list(slug: str) -> None:
236
236
  print(f" [{n}] phase={phase} turns={turns} @ {ts}")
237
237
 
238
238
 
239
- def cmd_health(slug: str) -> None:
240
- """Validate session consistency."""
241
- sdir = get_slug_dir(slug)
242
- issues = []
243
-
244
- if not sdir.exists():
245
- print(f"❌ Session not found: {slug}")
246
- return
247
-
248
- session: dict = {}
249
- hb: dict = {}
250
-
251
- # Check session.json
252
- session_file = sdir / "session.json"
253
- if not session_file.exists():
254
- issues.append("Missing session.json")
255
- else:
256
- try:
257
- session = _read_json(session_file)
258
- required = ["task_id", "status", "start_time", "last_activity"]
259
- for field in required:
260
- if field not in session:
261
- issues.append(f"session.json missing field: {field}")
262
- except (json.JSONDecodeError, ValueError):
263
- issues.append("session.json is corrupted (invalid JSON)")
264
-
265
- # Check heartbeat.json
266
- hb_file = sdir / "heartbeat.json"
267
- if not hb_file.exists():
268
- issues.append("Missing heartbeat.json")
269
- else:
239
+ def _validate_session_file(path: Path) -> tuple[dict, list[str]]:
240
+ """Read session metadata and return any consistency issues."""
241
+ if not path.exists():
242
+ return {}, ["Missing session.json"]
243
+ try:
244
+ session = _read_json(path)
245
+ except (json.JSONDecodeError, ValueError):
246
+ return {}, ["session.json is corrupted (invalid JSON)"]
247
+ required = ["task_id", "status", "start_time", "last_activity"]
248
+ issues = [
249
+ f"session.json missing field: {field}"
250
+ for field in required
251
+ if field not in session
252
+ ]
253
+ return session, issues
254
+
255
+
256
+ def _validate_heartbeat_file(path: Path) -> list[str]:
257
+ """Read heartbeat metadata and return any consistency issues."""
258
+ if not path.exists():
259
+ return ["Missing heartbeat.json"]
260
+ try:
261
+ heartbeat = _read_json(path)
262
+ except (json.JSONDecodeError, ValueError):
263
+ return ["heartbeat.json is corrupted (invalid JSON)"]
264
+ if heartbeat.get("status") == "stalled":
265
+ return ["Session is marked as STALLED"]
266
+ return []
267
+
268
+
269
+ def _validate_checkpoints(sdir: Path) -> tuple[list[tuple[int, Path]], list[str]]:
270
+ """Validate checkpoint contents and sequential numbering."""
271
+ checkpoints = get_checkpoints(sdir)
272
+ issues: list[str] = []
273
+ for number, path in checkpoints:
270
274
  try:
271
- hb = _read_json(hb_file)
272
- if hb.get("status") == "stalled":
273
- issues.append("Session is marked as STALLED")
275
+ checkpoint = _read_json(path)
276
+ if checkpoint.get("version", 1) > SCHEMA_VERSION:
277
+ issues.append(f"checkpoint-{number}.json has newer schema version")
278
+ if "slug" not in checkpoint or "timestamp" not in checkpoint:
279
+ issues.append(f"checkpoint-{number}.json missing required fields")
274
280
  except (json.JSONDecodeError, ValueError):
275
- issues.append("heartbeat.json is corrupted (invalid JSON)")
281
+ issues.append(f"checkpoint-{number}.json is corrupted")
276
282
 
277
- # Check checkpoint integrity
278
- checkpoints = get_checkpoints(sdir)
279
283
  if checkpoints:
280
- for n, path in checkpoints:
281
- try:
282
- cp = _read_json(path)
283
- if cp.get("version", 1) > SCHEMA_VERSION:
284
- issues.append(f"checkpoint-{n}.json has newer schema version")
285
- if "slug" not in cp or "timestamp" not in cp:
286
- issues.append(f"checkpoint-{n}.json missing required fields")
287
- except (json.JSONDecodeError, ValueError):
288
- issues.append(f"checkpoint-{n}.json is corrupted")
289
-
290
- # Check sequential numbering
291
- numbers = [n for n, _ in checkpoints]
284
+ numbers = [number for number, _ in checkpoints]
292
285
  expected = list(range(numbers[0], numbers[-1] + 1))
293
286
  if numbers != expected:
294
287
  missing = set(expected) - set(numbers)
295
288
  issues.append(f"Missing checkpoints: {sorted(missing)}")
289
+ return checkpoints, issues
290
+
291
+
292
+ def _stale_activity_issue(
293
+ session_file: Path, heartbeat_file: Path, session: dict
294
+ ) -> list[str]:
295
+ """Return a stale-activity issue when both metadata files are present."""
296
+ if not session_file.exists() or not heartbeat_file.exists() or not session:
297
+ return []
298
+ try:
299
+ last_action = session.get("last_activity", "")
300
+ if last_action:
301
+ last_time = datetime.fromisoformat(last_action)
302
+ if datetime.now(UTC) - last_time > STALE_THRESHOLD:
303
+ return [
304
+ f"No activity for > {STALE_THRESHOLD.total_seconds() / 3600:.0f}h "
305
+ "(stale)"
306
+ ]
307
+ except (ValueError, TypeError):
308
+ return ["Could not parse last_activity timestamp"]
309
+ return []
310
+
311
+
312
+ def cmd_health(slug: str) -> None:
313
+ """Validate session consistency."""
314
+ sdir = get_slug_dir(slug)
315
+ if not sdir.exists():
316
+ print(f"❌ Session not found: {slug}")
317
+ return
318
+
319
+ session_file = sdir / "session.json"
320
+ heartbeat_file = sdir / "heartbeat.json"
321
+ session, issues = _validate_session_file(session_file)
322
+ issues.extend(_validate_heartbeat_file(heartbeat_file))
323
+ checkpoints, checkpoint_issues = _validate_checkpoints(sdir)
324
+ issues.extend(checkpoint_issues)
325
+ issues.extend(_stale_activity_issue(session_file, heartbeat_file, session))
296
326
 
297
- # Check for stale activity
298
- if session_file.exists() and hb_file.exists() and session:
299
- try:
300
- last_action = session.get("last_activity", "")
301
- if last_action:
302
- last_time = datetime.fromisoformat(last_action)
303
- if datetime.now(UTC) - last_time > STALE_THRESHOLD:
304
- issues.append(f"No activity for > {STALE_THRESHOLD.total_seconds() / 3600:.0f}h (stale)")
305
- except (ValueError, TypeError):
306
- issues.append("Could not parse last_activity timestamp")
307
-
308
- # Report
309
327
  if not issues:
310
328
  print(f"✅ Session '{slug}' is healthy")
311
- print(f" Checkpoints: {len(checkpoints)}, Status: {session.get('status', 'unknown')}")
329
+ print(
330
+ f" Checkpoints: {len(checkpoints)}, "
331
+ f"Status: {session.get('status', 'unknown')}"
332
+ )
312
333
  else:
313
334
  print(f"⚠️ Session '{slug}' has {len(issues)} issue(s):")
314
335
  for issue in issues:
@@ -324,7 +345,10 @@ def cmd_archive(slug: str) -> None:
324
345
 
325
346
  archive_dir = ARCHIVEDIR / slug
326
347
  if archive_dir.exists():
327
- print(f"Session '{slug}' already exists in archive. Remove it first or use a different slug.")
348
+ print(
349
+ f"Session '{slug}' already exists in archive. Remove it first "
350
+ "or use a different slug."
351
+ )
328
352
  return
329
353
 
330
354
  archive_dir.parent.mkdir(parents=True, exist_ok=True)
@@ -332,6 +356,53 @@ def cmd_archive(slug: str) -> None:
332
356
  print(f"📦 Session '{slug}' archived to .pantheon/deepwork/archive/{slug}/")
333
357
 
334
358
 
359
+ def _cleanup_stale_session(item: Path, now: datetime, dry_run: bool) -> None:
360
+ """Report a stale heartbeat for one session, if it can be read."""
361
+ heartbeat_file = item / "heartbeat.json"
362
+ if not heartbeat_file.exists():
363
+ return
364
+ try:
365
+ heartbeat = _read_json(heartbeat_file)
366
+ last_action = heartbeat.get("last_action", "")
367
+ if last_action and now - datetime.fromisoformat(last_action) > STALE_THRESHOLD:
368
+ if dry_run:
369
+ print(
370
+ f" [dry-run] Would flag stale session: {item.name}/ "
371
+ f"(last action: {last_action})"
372
+ )
373
+ else:
374
+ print(f" ⏸️ Session '{item.name}' stale since {last_action}")
375
+ except (ValueError, json.JSONDecodeError):
376
+ pass
377
+
378
+
379
+ def _cleanup_temp_files(item: Path, dry_run: bool) -> int:
380
+ """Remove temporary files from one session and return the count."""
381
+ removed_files = 0
382
+ for tmp_file in item.glob("*.tmp"):
383
+ if dry_run:
384
+ print(f" [dry-run] Would remove temp file: {tmp_file.name}")
385
+ else:
386
+ tmp_file.unlink()
387
+ removed_files += 1
388
+ print(f" Removed temp file: {item.name}/{tmp_file.name}")
389
+ return removed_files
390
+
391
+
392
+ def _cleanup_item(item: Path, now: datetime, dry_run: bool) -> tuple[int, int]:
393
+ """Clean one session directory and return (directories, files) removed."""
394
+ session_file = item / "session.json"
395
+ if not session_file.exists():
396
+ if dry_run:
397
+ print(f" [dry-run] Would remove orphan directory: {item.name}/")
398
+ return 0, 0
399
+ shutil.rmtree(item)
400
+ print(f" Removed orphan directory: {item.name}/")
401
+ return 1, 0
402
+ _cleanup_stale_session(item, now, dry_run)
403
+ return 0, _cleanup_temp_files(item, dry_run)
404
+
405
+
335
406
  def cmd_cleanup(dry_run: bool = False) -> None:
336
407
  """Clean orphaned sessions and temp files.
337
408
 
@@ -347,47 +418,12 @@ def cmd_cleanup(dry_run: bool = False) -> None:
347
418
  now = datetime.now(UTC)
348
419
  removed_dirs = 0
349
420
  removed_files = 0
350
-
351
421
  for item in sorted(DEEPDIR.iterdir()):
352
422
  if not item.is_dir() or item.name == "archive" or item.name.startswith("."):
353
423
  continue
354
-
355
- session_file = item / "session.json"
356
- hb_file = item / "heartbeat.json"
357
-
358
- # Check if it's an orphan (no session.json)
359
- if not session_file.exists():
360
- if dry_run:
361
- print(f" [dry-run] Would remove orphan directory: {item.name}/")
362
- else:
363
- shutil.rmtree(item)
364
- removed_dirs += 1
365
- print(f" Removed orphan directory: {item.name}/")
366
- continue
367
-
368
- # Check for stale sessions
369
- if hb_file.exists():
370
- try:
371
- hb = _read_json(hb_file)
372
- last_action = hb.get("last_action", "")
373
- if last_action:
374
- last_time = datetime.fromisoformat(last_action)
375
- if now - last_time > STALE_THRESHOLD:
376
- if dry_run:
377
- print(f" [dry-run] Would flag stale session: {item.name}/ (last action: {last_action})")
378
- else:
379
- print(f" ⏸️ Session '{item.name}' stale since {last_action}")
380
- except (ValueError, json.JSONDecodeError):
381
- pass
382
-
383
- # Clean temp files
384
- for tmp_file in item.glob("*.tmp"):
385
- if dry_run:
386
- print(f" [dry-run] Would remove temp file: {tmp_file.name}")
387
- else:
388
- tmp_file.unlink()
389
- removed_files += 1
390
- print(f" Removed temp file: {item.name}/{tmp_file.name}")
424
+ dirs, files = _cleanup_item(item, now, dry_run)
425
+ removed_dirs += dirs
426
+ removed_files += files
391
427
 
392
428
  if not dry_run:
393
429
  total = removed_dirs + removed_files
@@ -164,6 +164,62 @@ BLOCKER_KEYWORDS: list[re.Pattern] = [
164
164
  re.compile(r"depend", re.IGNORECASE),
165
165
  re.compile(r"dependency", re.IGNORECASE),
166
166
  ]
167
+ FILE_NOVELTY_HIGH = 10
168
+ FILE_NOVELTY_MEDIUM = 5
169
+ PRIORITY_CRITICAL = 0.75
170
+ PRIORITY_HIGH = 0.50
171
+ PRIORITY_MEDIUM = 0.25
172
+ SUMMARY_MAX_CHARS = 80
173
+
174
+
175
+ def _keyword_scores(
176
+ text: str,
177
+ ) -> tuple[list[float], list[float], list[float], list[str]]:
178
+ """Return impact, risk, novelty, and matching keyword scores."""
179
+ impact_scores: list[float] = []
180
+ risk_scores: list[float] = []
181
+ novelty_scores: list[float] = []
182
+ keywords_found: list[str] = []
183
+ for name, pattern, impact, risk, novelty in KEYWORD_MAP:
184
+ if pattern.search(text):
185
+ impact_scores.append(impact)
186
+ risk_scores.append(risk)
187
+ novelty_scores.append(novelty)
188
+ keywords_found.append(name)
189
+ return impact_scores, risk_scores, novelty_scores, keywords_found
190
+
191
+
192
+ def _blocker_score(text: str, blocker_hint: bool) -> float:
193
+ """Return the blocker score detected in text or supplied explicitly."""
194
+ blockers = 0.0
195
+ for pattern in BLOCKER_KEYWORDS:
196
+ if pattern.search(text):
197
+ # unblocked/unblocks = positive blocker resolution
198
+ blockers = max(
199
+ blockers, 0.8 if re.search(r"unblock", text, re.IGNORECASE) else 0.5
200
+ )
201
+ if blocker_hint:
202
+ blockers = max(blockers, 0.8)
203
+ return blockers
204
+
205
+
206
+ def _downstream_score(from_agent: str | None, to_agent: str | None) -> float:
207
+ """Return downstream relevance for an agent pair."""
208
+ if not from_agent or not to_agent:
209
+ return 0.5
210
+ key = (from_agent.lower().strip(), to_agent.lower().strip())
211
+ return AGENT_PAIR_TABLE.get(key, 0.5)
212
+
213
+
214
+ def _priority_band(score: float) -> str:
215
+ """Map a score to the documented priority band."""
216
+ if score >= PRIORITY_CRITICAL:
217
+ return "CRITICAL"
218
+ if score >= PRIORITY_HIGH:
219
+ return "HIGH"
220
+ if score >= PRIORITY_MEDIUM:
221
+ return "MEDIUM"
222
+ return "LOW"
167
223
 
168
224
 
169
225
  def compute_score(
@@ -178,45 +234,20 @@ def compute_score(
178
234
  Returns dict with:
179
235
  score, band, impact, risk, novelty, blockers, downstream, keywords_found
180
236
  """
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)
237
+ impact_scores, risk_scores, novelty_scores, keywords_found = _keyword_scores(text)
192
238
 
193
239
  impact = max(impact_scores) if impact_scores else 0.0
194
240
  risk = max(risk_scores) if risk_scores else 0.0
195
241
  novelty = max(novelty_scores) if novelty_scores else 0.0
196
242
 
197
243
  # Novelty bonus for file count (overrides keyword score)
198
- if files_changed >= 10:
244
+ if files_changed >= FILE_NOVELTY_HIGH:
199
245
  novelty = max(novelty, 1.0)
200
- elif files_changed >= 5:
246
+ elif files_changed >= FILE_NOVELTY_MEDIUM:
201
247
  novelty = max(novelty, 0.8)
202
248
 
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)
249
+ blockers = _blocker_score(text, blocker_hint)
250
+ downstream = _downstream_score(from_agent, to_agent)
220
251
 
221
252
  score = (
222
253
  impact * WEIGHTS["impact"]
@@ -226,19 +257,9 @@ def compute_score(
226
257
  + downstream * WEIGHTS["downstream"]
227
258
  )
228
259
 
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
260
  return {
240
261
  "score": round(score, 2),
241
- "band": band,
262
+ "band": _priority_band(score),
242
263
  "impact": impact,
243
264
  "risk": risk,
244
265
  "novelty": novelty,
@@ -285,14 +306,17 @@ def compress(
285
306
  )
286
307
 
287
308
  # Summary = first meaningful line of scrubbed text, ≤80 chars
288
- lines = [l.strip() for l in scrubbed.splitlines() if l.strip()]
309
+ lines = [line.strip() for line in scrubbed.splitlines() if line.strip()]
289
310
  summary: str = ""
290
311
  if lines:
291
- summary = lines[0][:80]
292
- if len(lines[0]) > 80:
312
+ summary = lines[0][:SUMMARY_MAX_CHARS]
313
+ if len(lines[0]) > SUMMARY_MAX_CHARS:
293
314
  summary += "…"
294
315
 
295
- output_parts = [f"- What changed: {summary}", f"- Priority: {result['band']} (score: {result['score']:.2f})"]
316
+ output_parts = [
317
+ f"- What changed: {summary}",
318
+ f"- Priority: {result['band']} (score: {result['score']:.2f})",
319
+ ]
296
320
  if files:
297
321
  output_parts.append(f"- Files: {files}")
298
322
 
@@ -311,14 +335,14 @@ def process_batch(
311
335
  ) -> str:
312
336
  """Read multiple files, scrub + score + compress each."""
313
337
  parts: list[str] = []
314
- for fpath in file_paths:
315
- fpath = fpath.strip()
338
+ for raw_path in file_paths:
339
+ fpath = raw_path.strip()
316
340
  if not fpath:
317
341
  continue
318
342
  try:
319
343
  with open(fpath, encoding="utf-8") as fh:
320
344
  content = fh.read()
321
- except (OSError, IOError) as exc:
345
+ except OSError as exc:
322
346
  parts.append(f"- {fpath}: ERROR — {exc}")
323
347
  continue
324
348
 
@@ -331,11 +355,11 @@ def process_batch(
331
355
  )
332
356
 
333
357
  # Summary: first meaningful line
334
- content_lines = [l.strip() for l in scrubbed.splitlines() if l.strip()]
358
+ content_lines = [line.strip() for line in scrubbed.splitlines() if line.strip()]
335
359
  summary = "(empty)"
336
360
  if content_lines:
337
- summary = content_lines[0][:80]
338
- if len(content_lines[0]) > 80:
361
+ summary = content_lines[0][:SUMMARY_MAX_CHARS]
362
+ if len(content_lines[0]) > SUMMARY_MAX_CHARS:
339
363
  summary += "…"
340
364
 
341
365
  base = os.path.basename(fpath)
@@ -356,7 +380,9 @@ def main() -> None:
356
380
  parser = argparse.ArgumentParser(
357
381
  description="Pantheon inline compression — score, compress, or stat text.",
358
382
  )
359
- parser.add_argument("mode", choices=["score", "compress", "stats", "batch"], help="Operation mode")
383
+ parser.add_argument(
384
+ "mode", choices=["score", "compress", "stats", "batch"], help="Operation mode"
385
+ )
360
386
  parser.add_argument("--text", type=str, default="", help="Input text to process")
361
387
  parser.add_argument(
362
388
  "--files",
@@ -364,9 +390,24 @@ def main() -> None:
364
390
  default="",
365
391
  help="Comma-separated file paths (batch mode, or for file count in scoring)",
366
392
  )
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")
393
+ parser.add_argument(
394
+ "--from-agent",
395
+ type=str,
396
+ default=None,
397
+ help="Source agent for downstream relevance",
398
+ )
399
+ parser.add_argument(
400
+ "--to-agent",
401
+ type=str,
402
+ default=None,
403
+ help="Target agent for downstream relevance",
404
+ )
405
+ parser.add_argument(
406
+ "--blocker",
407
+ action="store_true",
408
+ default=False,
409
+ help="Hint that entry was a blocker",
410
+ )
370
411
 
371
412
  args = parser.parse_args()
372
413
 
@@ -378,7 +419,9 @@ def main() -> None:
378
419
  print("Error: --files is required for batch mode", file=sys.stderr)
379
420
  sys.exit(1)
380
421
  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)
422
+ output = process_batch(
423
+ file_list, from_agent=args.from_agent, to_agent=args.to_agent
424
+ )
382
425
  print(output)
383
426
  return
384
427
 
@@ -413,7 +456,9 @@ def main() -> None:
413
456
  # score mode
414
457
  # -----------------------------------------------------------------------
415
458
  if args.mode == "score":
416
- file_count = len([f for f in args.files.split(",") if f.strip()]) if args.files else 0
459
+ file_count = (
460
+ len([f for f in args.files.split(",") if f.strip()]) if args.files else 0
461
+ )
417
462
  result = compute_score(
418
463
  text,
419
464
  from_agent=args.from_agent,
@@ -3,15 +3,9 @@
3
3
  "scripts": {
4
4
  "checkpoint-session.py": "b70f6667ba287622d6353ada2612c7f2fb70d4ee170d456ecd7ee2b6ead891aa",
5
5
  "checkpoint-session.sh": "5b07e7c6c309e1db5c63f1f409eeb300a446bb9598899d94fdb13d94b56eb5f1",
6
- "checkpoint_session.py": "8c52919eae450147742553f11faa347644c9e79f7acffa2fcc4452a5ed294a0a",
7
- "compress-inline.py": "d3185cef07ff8b57d5a98d6c06a5b71504186ad2112fdb5a1ff48b0214d9cd3c",
8
- "eval-llm-judge.py": "6d25df589c0f6cffa4766d0ad8eb86ac958960bc8c0f59e93098c7bf8effbbe8",
9
- "eval-monte-carlo.py": "64c5eba9b072bec1f3eb713654da7edb9df46041395fdc4cb65730720135f8b1",
10
- "eval-run.py": "5abfdac7d67d66838dd7ee402383e205ae39c0d912deb8a3a9a9a9c4d05952f5",
11
- "eval-static.py": "33573295cf82a3c98e5f4c230ac376a40d1286ac16ff3aa031987396cde083b5",
6
+ "checkpoint_session.py": "487b4e3525ba03102fab1cd6283bb95d7489cd1848c72cf875884ac88b448ff7",
7
+ "compress-inline.py": "083ecc446cfe281be8f4aaca678eb2ae93adcbfef165f1746fb5bd364778f9d1",
12
8
  "example-sync.sh": "7fce84c8dc5a9d25d441a4cb59549a2731910e72d6ef82df45ac5ca6ea66c729",
13
- "session-end-save.py": "11088a722642d022e5e2ebad78292db4aa4803087b32e261b6eb069e9b991c48",
14
- "session-end-save.sh": "5fb410143bf62ed7d7c912805b00ae79171127b1b8ff0825db0eb4bf03b595a8",
15
9
  "test-allowed.sh": "723b116490044ffb403092c74b6bb94ee37330fecf453241f4cca22977ae8cdf",
16
10
  "test-auto-continue-checkpoint.sh": "de2641a4da0f26e9653f320d44763e5b5ed698cf9c701c103c5c82f91175376f",
17
11
  "test-env.sh": "6ea48a044451e5938e28fcb709368a7249a7d1b42615b190e40682ad26d4d1f9",