loki-mode 7.5.16 → 7.5.27

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 (47) hide show
  1. package/README.md +10 -9
  2. package/SKILL.md +14 -14
  3. package/VERSION +1 -1
  4. package/autonomy/completion-council.sh +38 -9
  5. package/autonomy/lib/claude-flags.sh +132 -0
  6. package/autonomy/lib/mcp-config.sh +160 -0
  7. package/autonomy/lib/project-graph.sh +675 -0
  8. package/autonomy/lib/voter-agents.sh +356 -0
  9. package/autonomy/loki +72 -100
  10. package/autonomy/run.sh +95 -186
  11. package/bin/loki +10 -0
  12. package/dashboard/__init__.py +1 -1
  13. package/dashboard/requirements.txt +13 -8
  14. package/dashboard/server.py +53 -22
  15. package/dashboard/static/index.html +298 -299
  16. package/docs/INSTALLATION.md +54 -21
  17. package/docs/retrospectives/v7.5.15-fleet-postmortem.md +325 -0
  18. package/docs/retrospectives/v7.5.15-honesty-audit.md +136 -0
  19. package/docs/retrospectives/v7.5.15-llm-failure-modes.md +49 -0
  20. package/loki-ts/data/finding-schema.json +74 -0
  21. package/loki-ts/data/model-pricing.json +12 -0
  22. package/loki-ts/dist/loki.js +109 -108
  23. package/mcp/__init__.py +1 -1
  24. package/mcp/lsp_proxy.py +713 -0
  25. package/mcp/requirements.txt +9 -3
  26. package/mcp/tests/__init__.py +0 -0
  27. package/mcp/tests/test_lsp_proxy.py +377 -0
  28. package/memory/app_graph.py +153 -0
  29. package/memory/storage.py +6 -1
  30. package/memory/tests/test_app_graph.py +134 -0
  31. package/package.json +4 -3
  32. package/providers/claude.sh +115 -4
  33. package/providers/codex.sh +2 -2
  34. package/providers/loader.sh +4 -4
  35. package/providers/model_catalog.json +0 -9
  36. package/providers/models.sh +1 -2
  37. package/references/multi-provider.md +26 -35
  38. package/references/prompt-repetition.md +1 -1
  39. package/references/quality-control.md +1 -1
  40. package/skills/00-index.md +3 -3
  41. package/skills/model-selection.md +11 -14
  42. package/skills/providers.md +17 -57
  43. package/skills/quality-gates.md +2 -2
  44. package/skills/troubleshooting.md +1 -1
  45. package/src/integrations/github/action-handler.js +3 -2
  46. package/src/protocols/tools/start-project.js +1 -1
  47. package/providers/gemini.sh +0 -343
@@ -810,7 +810,7 @@ async def agent_card() -> dict:
810
810
  "agents": 41,
811
811
  "swarms": 8,
812
812
  "quality_gates": 9,
813
- "providers": ["claude", "codex", "gemini", "cline", "aider"],
813
+ "providers": ["claude", "codex", "cline", "aider"],
814
814
  "streaming": True,
815
815
  "pushNotifications": False,
816
816
  "stateTransitionHistory": True,
@@ -3168,8 +3168,17 @@ def _parse_time_range(time_range: str) -> Optional[datetime]:
3168
3168
  return datetime.now(timezone.utc) - delta
3169
3169
 
3170
3170
 
3171
- def _read_events(time_range: str = "7d", max_events: int = 10000) -> list:
3172
- """Read events from .loki/events.jsonl with time filter and size limits."""
3171
+ def _read_events(time_range: str = "7d", max_events: int = 10000, type_prefix: Optional[str] = None) -> list:
3172
+ """Read events from .loki/events.jsonl with time filter and size limits.
3173
+
3174
+ Args:
3175
+ time_range: e.g. "7d", "24h", "30m". Events older than the cutoff are dropped.
3176
+ max_events: hard cap on the number of returned events.
3177
+ type_prefix: when set (non-empty), only return events whose ``type`` field
3178
+ starts with this prefix. Backward compatible: when None or empty,
3179
+ no type filtering is applied. Used by v7.5.22 Phase D for filtering
3180
+ ``claude_hook_*`` events without adding a new endpoint.
3181
+ """
3173
3182
  events_file = _get_loki_dir() / "events.jsonl"
3174
3183
  if not events_file.exists():
3175
3184
  return []
@@ -3208,6 +3217,10 @@ def _read_events(time_range: str = "7d", max_events: int = 10000) -> list:
3208
3217
  continue
3209
3218
  except (ValueError, TypeError):
3210
3219
  pass # Keep events with unparseable timestamps
3220
+ # Optional type-prefix filter (v7.5.22 Phase D).
3221
+ if type_prefix:
3222
+ if not str(event.get("type", "")).startswith(type_prefix):
3223
+ continue
3211
3224
  events.append(event)
3212
3225
  except json.JSONDecodeError:
3213
3226
  pass
@@ -3401,9 +3414,6 @@ _DEFAULT_PRICING = {
3401
3414
  "haiku": {"input": 1.00, "output": 5.00},
3402
3415
  # OpenAI Codex
3403
3416
  "gpt-5.3-codex": {"input": 1.50, "output": 12.00},
3404
- # Google Gemini
3405
- "gemini-3-pro": {"input": 1.25, "output": 10.00},
3406
- "gemini-3-flash": {"input": 0.10, "output": 0.40},
3407
3417
  }
3408
3418
 
3409
3419
  # Active pricing - starts with defaults, updated from .loki/pricing.json
@@ -3616,8 +3626,6 @@ _PROVIDER_LABELS = {
3616
3626
  "sonnet": "Sonnet 4.5",
3617
3627
  "haiku": "Haiku 4.5",
3618
3628
  "gpt-5.3-codex": "GPT-5.3 Codex",
3619
- "gemini-3-pro": "Gemini 3 Pro",
3620
- "gemini-3-flash": "Gemini 3 Flash",
3621
3629
  }
3622
3630
 
3623
3631
  _MODEL_PROVIDERS = {
@@ -3625,8 +3633,6 @@ _MODEL_PROVIDERS = {
3625
3633
  "sonnet": "claude",
3626
3634
  "haiku": "claude",
3627
3635
  "gpt-5.3-codex": "codex",
3628
- "gemini-3-pro": "gemini",
3629
- "gemini-3-flash": "gemini",
3630
3636
  "cline-default": "cline",
3631
3637
  "aider-default": "aider",
3632
3638
  }
@@ -3786,19 +3792,21 @@ async def force_council_review():
3786
3792
  async def get_council_transcripts(
3787
3793
  limit: int = Query(default=20, ge=1, le=200),
3788
3794
  since: Optional[str] = Query(default=None),
3789
- iter_min: Optional[int] = Query(default=None),
3795
+ iter_min: Optional[int] = Query(default=None, ge=0),
3796
+ type_prefix: Optional[str] = Query(default=None),
3790
3797
  ):
3791
3798
  """List council transcript records, sorted descending by iteration number.
3792
3799
 
3793
3800
  Query params:
3794
- limit int, default=20, max=200
3795
- since ISO8601 string (optional), filter to transcripts after this time
3796
- iter_min int (optional), filter to iteration >= N
3801
+ limit int, default=20, max=200
3802
+ since ISO8601 string (optional), filter to transcripts after this time
3803
+ iter_min int (optional), filter to iteration >= N
3804
+ type_prefix str (optional), v7.5.22 Phase D. When set, the response also
3805
+ includes a ``hook_events`` array of matching .loki/events.jsonl
3806
+ entries whose ``type`` starts with this prefix (e.g.
3807
+ ``claude_hook_``). Unset -> behavior unchanged.
3797
3808
  """
3798
- transcripts_dir = _get_loki_dir() / "council" / "transcripts"
3799
- if not transcripts_dir.exists():
3800
- return {"transcripts": [], "total": 0, "latest_id": None}
3801
-
3809
+ # Validate query params before any early-return so invalid inputs always get 400.
3802
3810
  since_dt = None
3803
3811
  if since:
3804
3812
  try:
@@ -3806,6 +3814,13 @@ async def get_council_transcripts(
3806
3814
  except ValueError:
3807
3815
  raise HTTPException(status_code=400, detail="Invalid 'since' timestamp format; expected ISO8601")
3808
3816
 
3817
+ transcripts_dir = _get_loki_dir() / "council" / "transcripts"
3818
+ if not transcripts_dir.exists():
3819
+ response: dict = {"transcripts": [], "total": 0, "latest_id": None}
3820
+ if type_prefix:
3821
+ response["hook_events"] = _read_events(type_prefix=type_prefix)
3822
+ return response
3823
+
3809
3824
  records = []
3810
3825
  for f in sorted(transcripts_dir.glob("iter-*.json"), reverse=True):
3811
3826
  try:
@@ -3816,6 +3831,9 @@ async def get_council_transcripts(
3816
3831
  if not isinstance(rec, dict):
3817
3832
  logger.warning("Skipping non-object council transcript file: %s", f.name)
3818
3833
  continue
3834
+ if not isinstance(rec.get("iteration_id"), str):
3835
+ logger.warning("Skipping transcript missing iteration_id field: %s", f.name)
3836
+ continue
3819
3837
  if since_dt is not None:
3820
3838
  ts_str = rec.get("timestamp", "")
3821
3839
  try:
@@ -3830,11 +3848,15 @@ async def get_council_transcripts(
3830
3848
  if len(records) >= limit:
3831
3849
  break
3832
3850
 
3833
- return {
3851
+ response = {
3834
3852
  "transcripts": records,
3835
3853
  "total": len(records),
3836
- "latest_id": records[0]["iteration_id"] if records else None,
3854
+ "latest_id": records[0].get("iteration_id") if records else None,
3837
3855
  }
3856
+ # v7.5.22 Phase D: opt-in hook-event passthrough via _read_events filter.
3857
+ if type_prefix:
3858
+ response["hook_events"] = _read_events(type_prefix=type_prefix)
3859
+ return response
3838
3860
 
3839
3861
 
3840
3862
  @app.get("/api/council/transcripts/{iteration_id}")
@@ -3851,9 +3873,18 @@ async def get_council_transcript(iteration_id: str):
3851
3873
  if not transcript_file.exists():
3852
3874
  raise HTTPException(status_code=404, detail="Transcript not found")
3853
3875
  try:
3854
- return json.loads(transcript_file.read_text())
3876
+ rec = json.loads(transcript_file.read_text())
3855
3877
  except Exception:
3856
- raise HTTPException(status_code=500, detail="Corrupt transcript file")
3878
+ raise HTTPException(
3879
+ status_code=410,
3880
+ detail=f"Transcript file for {iteration_id} is corrupt; admin should inspect or remove it",
3881
+ )
3882
+ if not isinstance(rec, dict):
3883
+ raise HTTPException(
3884
+ status_code=410,
3885
+ detail=f"Transcript file for {iteration_id} is corrupt; admin should inspect or remove it",
3886
+ )
3887
+ return rec
3857
3888
 
3858
3889
 
3859
3890
  # =============================================================================