loki-mode 11.17.1 → 11.17.5

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 (109) hide show
  1. package/SKILL.md +3 -3
  2. package/VERSION +1 -1
  3. package/autonomy/grill.sh +24 -2
  4. package/autonomy/hooks/precompact-flush.sh +51 -0
  5. package/autonomy/hooks/validate-bash.sh +895 -5
  6. package/autonomy/lib/chat.sh +8 -3
  7. package/autonomy/lib/claude-flags.sh +143 -6
  8. package/autonomy/lib/claude_permissions.py +24 -4
  9. package/autonomy/lib/decision_record.py +92 -3
  10. package/autonomy/lib/efficiency_cost.py +102 -0
  11. package/autonomy/lib/modernize/py_capture.py +62 -3
  12. package/autonomy/lib/prd-enrich.sh +28 -2
  13. package/autonomy/lib/voter-agents.sh +61 -6
  14. package/autonomy/lib/wiki-generator.py +58 -10
  15. package/autonomy/lib/wiki_llm.py +41 -2
  16. package/autonomy/loki +391 -28
  17. package/autonomy/quickstart.sh +1 -1
  18. package/autonomy/run.sh +201 -10
  19. package/autonomy/trigger-server.py +70 -2
  20. package/completions/_loki +3 -0
  21. package/completions/loki.bash +2 -1
  22. package/dashboard/__init__.py +1 -1
  23. package/dashboard/audit.py +204 -17
  24. package/docs/CLI-REFERENCE.md +4 -1
  25. package/docs/EVALUATING.md +2 -2
  26. package/docs/INSTALLATION.md +1 -1
  27. package/docs/environment-variables.md +1 -0
  28. package/docs/trackers.md +6 -0
  29. package/docs/v10/BOARD.md +2 -1
  30. package/docs/v10/CLI-MODERN.md +8 -5
  31. package/docs/v10/DECISIONS.md +16 -1
  32. package/docs/v10/ENGINE.md +1 -0
  33. package/docs/v10/FAILURE-CLASSES.md +60 -1
  34. package/docs/v10/FOUNDER-QUEUE.md +1 -0
  35. package/docs/v10/GUIDE-MODERNIZE.md +9 -4
  36. package/docs/v10/PLAN-V12.md +896 -0
  37. package/docs/v10/PROGRESS.md +7 -0
  38. package/docs/v11/COST-HALF.md +3 -1
  39. package/docs/visual-evidence.md +2 -0
  40. package/loki-ts/dist/loki.js +1104 -1057
  41. package/mcp/__init__.py +1 -1
  42. package/memory/retrieval.py +15 -3
  43. package/memory/schemas.py +43 -1
  44. package/memory/storage.py +11 -0
  45. package/package.json +2 -2
  46. package/packages/control-plane/dist/server.js +989 -693
  47. package/packages/control-plane/src/server/legacy/routes-control.ts +5 -1
  48. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  49. package/providers/claude.sh +1 -0
  50. package/providers/loader.sh +26 -0
  51. package/providers/model_routes.sh +31 -0
  52. package/src/policies/claude_permissions.js +20 -2
  53. package/web-app/dist/assets/{AdminPage-OVzN6eLm.js → AdminPage-C0v2p3jN.js} +1 -1
  54. package/web-app/dist/assets/{Avatar-hCCrxrCZ.js → Avatar-Cgc0QepK.js} +1 -1
  55. package/web-app/dist/assets/{Badge-CDcpkhbJ.js → Badge-1X4wQ0sU.js} +1 -1
  56. package/web-app/dist/assets/{Button-8mwWlhEu.js → Button-CG79n6-I.js} +1 -1
  57. package/web-app/dist/assets/{CockpitPage-D8cf3u22.js → CockpitPage-rSw2BTKk.js} +1 -1
  58. package/web-app/dist/assets/{ComparePage-CIgkbDRY.js → ComparePage-B9EVp__S.js} +1 -1
  59. package/web-app/dist/assets/{ErrorBoundary-99kJH9g2.js → ErrorBoundary-DLQKY8LF.js} +1 -1
  60. package/web-app/dist/assets/{EvidenceReceiptPanel-C9fj32eS.js → EvidenceReceiptPanel-DE0L1L2Q.js} +1 -1
  61. package/web-app/dist/assets/{GitHubIssuesPanel-De43YQ_C.js → GitHubIssuesPanel-DlKtYrtk.js} +1 -1
  62. package/web-app/dist/assets/{GitHubPRsPanel-7g7ZTnkx.js → GitHubPRsPanel-C5c5tY3x.js} +1 -1
  63. package/web-app/dist/assets/{HomePage-CcNA24ny.js → HomePage-ob1natMp.js} +2 -2
  64. package/web-app/dist/assets/{LoginPage-BYjlb-IK.js → LoginPage-CCQBgvzB.js} +1 -1
  65. package/web-app/dist/assets/{MagicPage-C0Dwt9Na.js → MagicPage-CKSuC0Pp.js} +1 -1
  66. package/web-app/dist/assets/{MetricsPage-jDGmdcgg.js → MetricsPage-nq_ygAuG.js} +1 -1
  67. package/web-app/dist/assets/{NotFoundPage-C_ZO8tLE.js → NotFoundPage-B6gVLTAn.js} +1 -1
  68. package/web-app/dist/assets/{ProjectPage-B5wnurfQ.js → ProjectPage-pUnvGriO.js} +3 -3
  69. package/web-app/dist/assets/{ProjectsPage--EyKt5-D.js → ProjectsPage-DMFPGLkE.js} +1 -1
  70. package/web-app/dist/assets/{SettingsPage-cB0lnfti.js → SettingsPage-DAB_cp87.js} +1 -1
  71. package/web-app/dist/assets/{ShowcasePage-Dl16FDkK.js → ShowcasePage-DTsx_XxI.js} +1 -1
  72. package/web-app/dist/assets/{SystemSettingsPage-BmAXd4V2.js → SystemSettingsPage-C2UUqWhW.js} +1 -1
  73. package/web-app/dist/assets/{TeamsPage-BFkWVOZe.js → TeamsPage-C4Sws9U7.js} +1 -1
  74. package/web-app/dist/assets/{TemplatesPage-Bf2kmEUM.js → TemplatesPage-B1-79O4n.js} +1 -1
  75. package/web-app/dist/assets/{TerminalOutput-BItv9Wa6.js → TerminalOutput-CDSVt5TB.js} +1 -1
  76. package/web-app/dist/assets/{activity-Df2Awg6I.js → activity-CBEULbio.js} +1 -1
  77. package/web-app/dist/assets/{bell-D-1ztTsJ.js → bell-Dp-j77Qo.js} +1 -1
  78. package/web-app/dist/assets/{bot-A4qgZsKi.js → bot-Dh1FlTEJ.js} +1 -1
  79. package/web-app/dist/assets/{check-BPfFpF0c.js → check-C_7N7W4q.js} +1 -1
  80. package/web-app/dist/assets/{chevron-left-CfSJvmDi.js → chevron-left-DtZJiu3T.js} +1 -1
  81. package/web-app/dist/assets/{circle-alert-wx2d1dzb.js → circle-alert-CJK4sNof.js} +1 -1
  82. package/web-app/dist/assets/{clock-pQMai6l-.js → clock--0bfAUkP.js} +1 -1
  83. package/web-app/dist/assets/{cloud-DWL470r6.js → cloud-CRa9nwiN.js} +1 -1
  84. package/web-app/dist/assets/{code-xml-BaEQpQLZ.js → code-xml-4gRrVGxx.js} +1 -1
  85. package/web-app/dist/assets/{database-h-N7WkQN.js → database-C-A2USGl.js} +1 -1
  86. package/web-app/dist/assets/{dollar-sign-B5oYiD27.js → dollar-sign-BshJt4ML.js} +1 -1
  87. package/web-app/dist/assets/{file-code-corner-fbTtTy0o.js → file-code-corner-Bd--i_IM.js} +1 -1
  88. package/web-app/dist/assets/{file-plus-PaF6HMo8.js → file-plus-YiNg-aWn.js} +1 -1
  89. package/web-app/dist/assets/{globe-Ct-GlWID.js → globe-CfyHbAbI.js} +1 -1
  90. package/web-app/dist/assets/{hammer-C1oZFX51.js → hammer-BlGC-yqz.js} +1 -1
  91. package/web-app/dist/assets/{index-C2egrRhz.js → index-BlCg8mla.js} +3 -3
  92. package/web-app/dist/assets/{layers-Dt5ZK4v2.js → layers-DLiA0AcW.js} +1 -1
  93. package/web-app/dist/assets/{loader-circle-DrpMziic.js → loader-circle-CkOfS_Q6.js} +1 -1
  94. package/web-app/dist/assets/{lock-DOez8MmK.js → lock-C5vRE8HM.js} +1 -1
  95. package/web-app/dist/assets/{package-ClGIkerr.js → package-BPzp43h4.js} +1 -1
  96. package/web-app/dist/assets/{plus-C004i9ua.js → plus-C4x6sD5J.js} +1 -1
  97. package/web-app/dist/assets/{refresh-cw-Ddfk-7Sy.js → refresh-cw-DZn-gB9J.js} +1 -1
  98. package/web-app/dist/assets/{rotate-ccw-Bu1KgDjS.js → rotate-ccw-rH63YhnY.js} +1 -1
  99. package/web-app/dist/assets/{scroll-text-3q1lW5pm.js → scroll-text-DTvZMQuh.js} +1 -1
  100. package/web-app/dist/assets/{server-pd6x4HW1.js → server-DmwCdhKT.js} +1 -1
  101. package/web-app/dist/assets/{shield-alert-B9jGJepT.js → shield-alert-Dn5Rtkhx.js} +1 -1
  102. package/web-app/dist/assets/{thumbs-up-Cdcc9nQf.js → thumbs-up-D_lHdH-1.js} +1 -1
  103. package/web-app/dist/assets/{trash-2-foSojXHK.js → trash-2-CVfeP0Fb.js} +1 -1
  104. package/web-app/dist/assets/{trending-down-CTTiqlJ1.js → trending-down-ouPrjBEm.js} +1 -1
  105. package/web-app/dist/assets/{trending-up-B_MgmRAx.js → trending-up-7H96_KlR.js} +1 -1
  106. package/web-app/dist/assets/{upload-g6BfALzG.js → upload-kTTgyfD6.js} +1 -1
  107. package/web-app/dist/assets/{usePolling-DrHwvzuh.js → usePolling-BFIMPvNM.js} +1 -1
  108. package/web-app/dist/assets/{user-COp75m7l.js → user-UDo6EQfn.js} +1 -1
  109. package/web-app/dist/index.html +1 -1
@@ -110,6 +110,11 @@ complexity = os.environ.get("_VA_COMPLEXITY", "standard")
110
110
  _effort_bump = {"low": "medium", "medium": "high", "high": "xhigh"}
111
111
  def _effort(base):
112
112
  return _effort_bump.get(base, base) if complexity == "complex" else base
113
+ # Cost S7: the three base voters run on haiku for small/standard work; the
114
+ # complex tier keeps the original opus/sonnet roster. haiku takes no effort knob.
115
+ _lean = complexity != "complex"
116
+ def _model(default):
117
+ return "haiku" if _lean else default
113
118
  prd_clause = (
114
119
  f"PRD at {prd}. Read it for ground-truth requirements."
115
120
  if prd else
@@ -118,8 +123,7 @@ prd_clause = (
118
123
  agents = {
119
124
  "requirements-verifier": {
120
125
  "description": "Verifies the iteration delivered the PRD-required behavior.",
121
- "model": "opus",
122
- "effort": _effort("high"),
126
+ "model": _model("opus"),
123
127
  "tools": ["Read", "Grep", "Bash"],
124
128
  "prompt": (
125
129
  f"You are the requirements-verifier for iteration {iter_n}. "
@@ -131,8 +135,7 @@ agents = {
131
135
  },
132
136
  "test-auditor": {
133
137
  "description": "Audits test coverage and pass status for the iteration.",
134
- "model": "sonnet",
135
- "effort": _effort("high"),
138
+ "model": _model("sonnet"),
136
139
  "tools": ["Read", "Grep", "Bash"],
137
140
  "prompt": (
138
141
  f"You are the test-auditor for iteration {iter_n}. "
@@ -144,8 +147,7 @@ agents = {
144
147
  },
145
148
  "convergence-voter": {
146
149
  "description": "Checks code churn and progress signals to judge convergence.",
147
- "model": "sonnet",
148
- "effort": _effort("medium"),
150
+ "model": _model("sonnet"),
149
151
  "tools": ["Read", "Grep", "Bash"],
150
152
  "prompt": (
151
153
  f"You are the convergence-voter for iteration {iter_n} on tier {tier}. "
@@ -156,6 +158,9 @@ agents = {
156
158
  ),
157
159
  },
158
160
  }
161
+ if not _lean:
162
+ for _k, _b in (("requirements-verifier", "high"), ("test-auditor", "high"), ("convergence-voter", "medium")):
163
+ agents[_k]["effort"] = _effort(_b)
159
164
  print(json.dumps(agents, separators=(",", ":")))
160
165
  '
161
166
  return 0
@@ -289,6 +294,13 @@ loki_council_dispatch_agents() {
289
294
  local prompt
290
295
  prompt=$(printf 'Loki council iteration %s. Run each declared agent against the current workspace, return one finding per agent matching the provided JSON Schema. Be terse, be honest.' "$iteration")
291
296
 
297
+ # Cost S7: hand the council the PRD path so voters stop reporting "No PRD was
298
+ # provided". A missing/unreadable PRD adds nothing; voters then vote
299
+ # CANNOT_VALIDATE, which never approves.
300
+ if [ -n "$prd_path" ] && [ -r "$prd_path" ]; then
301
+ prompt="${prompt} PRD file: ${prd_path} (read it for ground-truth requirements)."
302
+ fi
303
+
292
304
  # Shared dispatch state (declared once, before either the SDK or claude arm).
293
305
  local response=""
294
306
  local rc=0
@@ -375,11 +387,54 @@ loki_council_dispatch_agents() {
375
387
  # (which has its own timeout + conservative defaults).
376
388
  _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
377
389
  ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
390
+ # Cost S7 lean child: council role flags (no hooks/MCP/tools on the main
391
+ # child) and --output-format json so usage lands in the child ledger. The
392
+ # envelope is unwrapped back to the {findings:[...]} object below.
393
+ local _va_lean=() _va_lf
394
+ if type loki_lean_child_flags >/dev/null 2>&1; then
395
+ while IFS= read -r _va_lf; do
396
+ [ -n "$_va_lf" ] && _va_lean+=("$_va_lf")
397
+ done < <(loki_lean_child_flags council 2>/dev/null)
398
+ fi
399
+ local _va_fmt=()
400
+ if loki_claude_flag_supported "--output-format"; then
401
+ _va_fmt=(--output-format json)
402
+ fi
378
403
  response=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" \
379
404
  env CAVEMAN_DEFAULT_MODE=off claude "${_LOKI_PERM_ARGV[@]}" \
405
+ ${_va_lean[@]+"${_va_lean[@]}"} \
406
+ ${_va_fmt[@]+"${_va_fmt[@]}"} \
380
407
  -p "$prompt" \
381
408
  --agents "$agents_json" \
382
409
  --json-schema "$schema_content" 2>"$stderr_log") || rc=$?
410
+ if [ "$rc" -eq 0 ] && [ -n "$response" ] && [ "${#_va_fmt[@]}" -gt 0 ]; then
411
+ if type loki_child_ledger_append >/dev/null 2>&1; then
412
+ loki_child_ledger_append council "$response" >/dev/null 2>&1 || true
413
+ fi
414
+ local _va_unwrapped _va_upy
415
+ if _va_upy="$(_loki_snapshot_py_tool)"; then
416
+ # Unwrap the CLI envelope: structured_output, else a JSON string in
417
+ # result. A bare {findings:[...]} object passes through unchanged. Any
418
+ # other shape leaves the response as-is, so the parser fails closed.
419
+ _va_unwrapped=$(printf '%s' "$response" | "$_va_upy" -I -S -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]; import json
420
+ try:
421
+ e = json.load(sys.stdin)
422
+ if isinstance(e, dict) and "findings" in e:
423
+ p = e
424
+ else:
425
+ p = e.get("structured_output") if isinstance(e, dict) else None
426
+ if not isinstance(p, dict):
427
+ r = e.get("result") if isinstance(e, dict) else None
428
+ p = json.loads(r) if isinstance(r, str) else None
429
+ if isinstance(p, dict):
430
+ sys.stdout.write(json.dumps(p))
431
+ else:
432
+ sys.exit(1)
433
+ except Exception:
434
+ sys.exit(1)' 2>/dev/null) || _va_unwrapped=""
435
+ if [ -n "$_va_unwrapped" ]; then response="$_va_unwrapped"; fi
436
+ fi
437
+ fi
383
438
  fi # end claude-dispatch guard (SDK path skips it, already has $response)
384
439
 
385
440
  # Shared fail-closed check for BOTH paths: any non-zero exit (claude/SDK
@@ -260,8 +260,10 @@ def _data_flow_diagram(index, modules, entries):
260
260
  return "\n".join(lines)
261
261
 
262
262
 
263
- def _llm_prose(section, context, fallback):
264
- """Get prose for a section from the LLM, or use the deterministic fallback."""
263
+ def _llm_prose(section, context, fallback, prose=None):
264
+ """Prose for a section: pre-batched text if given, else the LLM, else fallback."""
265
+ if prose is not None:
266
+ return prose if prose.strip() else fallback
265
267
  prompt = (
266
268
  "You are documenting a software project for a team wiki. Write a clear, "
267
269
  "factual " + section + " section in markdown. Be concise (under 250 words). "
@@ -275,6 +277,48 @@ def _llm_prose(section, context, fallback):
275
277
  return fallback
276
278
 
277
279
 
280
+ _BATCH_KEYS = ("architecture overview", "key modules summary", "data flow")
281
+ _BATCH_MARK = "=== SECTION: "
282
+
283
+
284
+ def _batch_prose(context):
285
+ """One LLM call for all three sections. Returns {key: prose}; missing keys
286
+ are absent so callers use their deterministic fallback (never a second call).
287
+ Output without any marker is taken as the overview only."""
288
+ prompt = (
289
+ "You are documenting a software project for a team wiki. Write three "
290
+ "clear, factual markdown sections, each under 250 words, in this exact "
291
+ "format. Each section starts on its own line with a marker line, then "
292
+ "prose:\n"
293
+ + "\n".join(_BATCH_MARK + k + " ===" for k in _BATCH_KEYS)
294
+ + "\nDo NOT use emojis. Do NOT use em dashes or en dashes. Do NOT "
295
+ "invent file paths. Output ONLY the three marked sections with prose "
296
+ "(no headings, no citations).\n\nPROJECT CONTEXT:\n" + context
297
+ )
298
+ out = invoke_llm(prompt, lean=True)
299
+ if not out or not out.strip():
300
+ return {}
301
+ found = {}
302
+ key = None
303
+ for line in out.splitlines():
304
+ t = line.strip()
305
+ if t.startswith(_BATCH_MARK) and t.endswith("==="):
306
+ name = t[len(_BATCH_MARK):-3].strip()
307
+ key = name if name in _BATCH_KEYS else None
308
+ if key:
309
+ found[key] = []
310
+ elif key:
311
+ found[key].append(line)
312
+ prose = {k: "\n".join(v).strip() for k, v in found.items() if "".join(v).strip()}
313
+ if not prose:
314
+ prose = {_BATCH_KEYS[0]: out.strip()}
315
+ missing = [k for k in _BATCH_KEYS if k not in prose]
316
+ if missing:
317
+ print("warning: wiki batched call missing sections (using fallback): "
318
+ + ", ".join(missing), file=sys.stderr)
319
+ return prose
320
+
321
+
278
322
  def _build_context(root, index, modules, entries):
279
323
  name = Path(root).name
280
324
  lines = [
@@ -296,14 +340,15 @@ def _build_context(root, index, modules, entries):
296
340
  return "\n".join(lines)
297
341
 
298
342
 
299
- def _section_overview(root, index, modules, entries, context):
343
+ def _section_overview(root, index, modules, entries, context, batch=None):
300
344
  name = Path(root).name
301
345
  fallback = (
302
346
  "%s is a software project with %d indexed source files. "
303
347
  "Entry points: %s. The largest modules by size are listed below."
304
348
  % (name, len(index["files"]), ", ".join(entries) or "not detected")
305
349
  )
306
- prose = _llm_prose("architecture overview", context, fallback)
350
+ prose = _llm_prose("architecture overview", context, fallback,
351
+ None if batch is None else batch.get("architecture overview", ""))
307
352
  # Code-derived citations: the entry points + top modules (real files).
308
353
  citations = []
309
354
  for e in entries[:3]:
@@ -318,7 +363,7 @@ def _section_overview(root, index, modules, entries, context):
318
363
  "body": prose, "citations": citations, "diagram": diagram}
319
364
 
320
365
 
321
- def _section_modules(root, modules, context):
366
+ def _section_modules(root, modules, context, batch=None):
322
367
  fallback_lines = ["The following modules carry most of the codebase:"]
323
368
  citations = []
324
369
  body_parts = []
@@ -326,6 +371,7 @@ def _section_modules(root, modules, context):
326
371
  "key modules summary",
327
372
  context,
328
373
  "Key modules of this project, with their primary definitions:",
374
+ None if batch is None else batch.get("key modules summary", ""),
329
375
  )
330
376
  body_parts.append(prose)
331
377
  body_parts.append("")
@@ -342,13 +388,14 @@ def _section_modules(root, modules, context):
342
388
  "body": "\n".join(body_parts), "citations": citations}
343
389
 
344
390
 
345
- def _section_data_flow(root, index, modules, entries, context):
391
+ def _section_data_flow(root, index, modules, entries, context, batch=None):
346
392
  fallback = (
347
393
  "Execution begins at the entry point(s) (%s) and flows through the "
348
394
  "key modules. Trace a request from the entry file into the modules it "
349
395
  "imports to follow the data path." % (", ".join(entries) or "not detected")
350
396
  )
351
- prose = _llm_prose("data flow", context, fallback)
397
+ prose = _llm_prose("data flow", context, fallback,
398
+ None if batch is None else batch.get("data flow", ""))
352
399
  citations = [{"file": e, "line": 1} for e in entries[:4]]
353
400
  if not citations and index["files"]:
354
401
  citations = [{"file": index["files"][0], "line": 1}]
@@ -444,10 +491,11 @@ def main(argv=None):
444
491
  context = _build_context(root, index, modules, entries)
445
492
 
446
493
  log("generating sections (%d files indexed) ..." % len(index["files"]))
494
+ batch = _batch_prose(context)
447
495
  sections = [
448
- _section_overview(root, index, modules, entries, context),
449
- _section_modules(root, modules, context),
450
- _section_data_flow(root, index, modules, entries, context),
496
+ _section_overview(root, index, modules, entries, context, batch),
497
+ _section_modules(root, modules, context, batch),
498
+ _section_data_flow(root, index, modules, entries, context, batch),
451
499
  ]
452
500
 
453
501
  # Enforce the grounding contract: validate every citation against disk.
@@ -22,11 +22,48 @@ import subprocess
22
22
  from pathlib import Path
23
23
 
24
24
 
25
- def invoke_llm(prompt, timeout=120):
25
+ _LEAN_FLAGS = (
26
+ ("--setting-sources", ["project"]),
27
+ ("--strict-mcp-config", []),
28
+ ("--mcp-config", ['{"mcpServers":{}}']),
29
+ ("--disable-slash-commands", []),
30
+ ("--tools=", []),
31
+ )
32
+
33
+
34
+ def lean_claude_flags():
35
+ """Flags for a one-shot text-only claude child (cost lever L1).
36
+
37
+ --bare only when an API key is present (OAuth-only hosts would fail auth);
38
+ otherwise narrowed setting sources. A flag the installed claude does not
39
+ list in --help is dropped, never passed. Returns [] when claude is absent.
40
+ """
41
+ claude = _which("claude")
42
+ if not claude:
43
+ return []
44
+ try:
45
+ help_txt = subprocess.run(
46
+ [claude, "--help"], capture_output=True, text=True, timeout=15,
47
+ stdin=subprocess.DEVNULL,
48
+ ).stdout
49
+ except (OSError, subprocess.SubprocessError):
50
+ return []
51
+ if os.environ.get("ANTHROPIC_API_KEY") and "--bare" in help_txt:
52
+ return ["--bare", "--tools="] if "--tools" in help_txt else ["--bare"]
53
+ flags = []
54
+ for name, vals in _LEAN_FLAGS:
55
+ if name.rstrip("=") in help_txt:
56
+ flags.append(name)
57
+ flags.extend(vals)
58
+ return flags
59
+
60
+
61
+ def invoke_llm(prompt, timeout=120, lean=False):
26
62
  """Return the LLM completion string, or None if unavailable.
27
63
 
28
64
  Honors LOKI_WIKI_LLM_STUB for CI. Otherwise shells out to the configured
29
- provider, mirroring loki docs `_docs_invoke_provider`.
65
+ provider, mirroring loki docs `_docs_invoke_provider`. lean=True adds the
66
+ lean child flags for the claude provider.
30
67
  """
31
68
  stub = os.environ.get("LOKI_WIKI_LLM_STUB")
32
69
  if stub is not None:
@@ -64,6 +101,8 @@ def invoke_llm(prompt, timeout=120):
64
101
  base = cmds.get(provider)
65
102
  if base is None or not _which(base[0]):
66
103
  return None
104
+ if lean and provider == "claude":
105
+ base = ["claude", "-p"] + lean_claude_flags() + [prompt]
67
106
  cmd = ([timeout_cmd, str(timeout)] + base) if timeout_cmd else base
68
107
  try:
69
108
  out = subprocess.run(