loki-mode 7.5.17 → 7.5.28

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 +26 -3
  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 +685 -0
  8. package/autonomy/lib/voter-agents.sh +356 -0
  9. package/autonomy/loki +108 -111
  10. package/autonomy/run.sh +95 -186
  11. package/bin/loki +12 -1
  12. package/dashboard/__init__.py +1 -1
  13. package/dashboard/requirements.txt +13 -8
  14. package/dashboard/server.py +33 -15
  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 +198 -172
  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
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env bash
2
+ # autonomy/lib/voter-agents.sh -- Phase C (v7.5.20) bash side.
3
+ #
4
+ # Builds the JSON declaration of the council voter agents and orchestrates a
5
+ # single `claude --agents <json> --json-schema <path>` dispatch per iteration.
6
+ # Replaces the ad-hoc per-voter heuristic loop in council_aggregate_votes when
7
+ # the locally installed Claude CLI supports both flags.
8
+ #
9
+ # Vote enum (per architect, BINDING):
10
+ # APPROVE | REJECT | CANNOT_VALIDATE
11
+ #
12
+ # Translation to the legacy aggregator shape used by council_evaluate and
13
+ # council_write_transcript:
14
+ # APPROVE -> COMPLETE
15
+ # REJECT -> CONTINUE
16
+ # CANNOT_VALIDATE -> CONTINUE (mirrors managed council _vote_to_legacy)
17
+ #
18
+ # Public API (all functions are pure: read env + filesystem, write stdout).
19
+ # loki_voter_agents_json -- emits the 3-voter agents JSON
20
+ # loki_devils_advocate_json <summary> -- emits a one-key JSON for the DA
21
+ # loki_finding_schema_path -- echoes absolute path to schema
22
+ # loki_council_dispatch_agents <iter> <prd_path>
23
+ # -- runs the single claude call,
24
+ # writes verdict files and the
25
+ # aggregator round file. Returns 0
26
+ # on success, 1 on any fallback
27
+ # condition (caller falls through
28
+ # to existing heuristic dispatch).
29
+
30
+ # Guard against double-source.
31
+ if [ "${__LOKI_VOTER_AGENTS_SH_LOADED:-0}" = "1" ]; then
32
+ return 0 2>/dev/null || true
33
+ fi
34
+ __LOKI_VOTER_AGENTS_SH_LOADED=1
35
+
36
+ # Resolve repo root once. This file lives at autonomy/lib/voter-agents.sh.
37
+ __LOKI_VA_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
38
+ __LOKI_VA_REPO_ROOT="$(cd "${__LOKI_VA_LIB_DIR}/../.." 2>/dev/null && pwd)"
39
+
40
+ # ---------- Voter agents JSON ----------
41
+ # Emits a top-level JSON object with exactly 3 keys (the 3 base voters).
42
+ # Required env:
43
+ # LOKI_ITER -- iteration number (int-ish)
44
+ # LOKI_PRD_PATH -- PRD path (may be empty)
45
+ # Optional env:
46
+ # LOKI_RARV_TIER -- planning|development|fast (informational only)
47
+ #
48
+ # Returns 0 always. Emits `{}` when required env missing.
49
+ loki_voter_agents_json() {
50
+ local iter="${LOKI_ITER:-}"
51
+ if [ -z "$iter" ]; then
52
+ printf '%s' '{}'
53
+ return 0
54
+ fi
55
+ local prd="${LOKI_PRD_PATH:-}"
56
+ local tier="${LOKI_RARV_TIER:-development}"
57
+
58
+ _VA_ITER="$iter" \
59
+ _VA_PRD="$prd" \
60
+ _VA_TIER="$tier" \
61
+ _VA_COMPLEXITY="${LOKI_COMPLEXITY:-standard}" \
62
+ python3 -c '
63
+ import json, os
64
+ iter_n = os.environ.get("_VA_ITER", "0")
65
+ prd = os.environ.get("_VA_PRD", "")
66
+ tier = os.environ.get("_VA_TIER", "development")
67
+ complexity = os.environ.get("_VA_COMPLEXITY", "standard")
68
+ # Effort selection mirrors Bun route loki-ts/src/providers/claude_flags.ts::effortForTier
69
+ # so bash and Bun emit the same effort levels under all LOKI_COMPLEXITY values.
70
+ # Architect roster (standard complexity): requirements-verifier=high, test-auditor=high,
71
+ # convergence-voter=medium. Under complex, each shifts up one notch (medium->high, high->xhigh).
72
+ _effort_bump = {"low": "medium", "medium": "high", "high": "xhigh"}
73
+ def _effort(base):
74
+ return _effort_bump.get(base, base) if complexity == "complex" else base
75
+ prd_clause = (
76
+ f"PRD at {prd}. Read it for ground-truth requirements."
77
+ if prd else
78
+ "No PRD path provided; rely on session context."
79
+ )
80
+ agents = {
81
+ "requirements-verifier": {
82
+ "description": "Verifies the iteration delivered the PRD-required behavior.",
83
+ "model": "opus",
84
+ "effort": _effort("high"),
85
+ "tools": ["Read", "Grep", "Bash"],
86
+ "prompt": (
87
+ f"You are the requirements-verifier for iteration {iter_n}. "
88
+ f"{prd_clause} Compare current code+tests against requirements. "
89
+ "Emit a single finding object with role=requirements-verifier, "
90
+ "vote in {APPROVE,REJECT,CANNOT_VALIDATE}, terse reason, "
91
+ "confidence float in [0,1]."
92
+ ),
93
+ },
94
+ "test-auditor": {
95
+ "description": "Audits test coverage and pass status for the iteration.",
96
+ "model": "sonnet",
97
+ "effort": _effort("high"),
98
+ "tools": ["Read", "Grep", "Bash"],
99
+ "prompt": (
100
+ f"You are the test-auditor for iteration {iter_n}. "
101
+ "Inspect test logs, coverage, and pass/fail counts. "
102
+ "Emit a single finding object with role=test-auditor, "
103
+ "vote in {APPROVE,REJECT,CANNOT_VALIDATE}, terse reason, "
104
+ "confidence float in [0,1]."
105
+ ),
106
+ },
107
+ "convergence-voter": {
108
+ "description": "Checks code churn and progress signals to judge convergence.",
109
+ "model": "sonnet",
110
+ "effort": _effort("medium"),
111
+ "tools": ["Read", "Grep", "Bash"],
112
+ "prompt": (
113
+ f"You are the convergence-voter for iteration {iter_n} on tier {tier}. "
114
+ "Examine git diff trends, queue depth, and stagnation signals. "
115
+ "Emit a single finding object with role=convergence-voter, "
116
+ "vote in {APPROVE,REJECT,CANNOT_VALIDATE}, terse reason, "
117
+ "confidence float in [0,1]."
118
+ ),
119
+ },
120
+ }
121
+ print(json.dumps(agents, separators=(",", ":")))
122
+ '
123
+ return 0
124
+ }
125
+
126
+ # ---------- Devil's advocate agent JSON ----------
127
+ # Emits a single-key JSON object describing the conditional 4th voter.
128
+ # Arg $1: terse base findings summary used to brief the DA prompt.
129
+ loki_devils_advocate_json() {
130
+ local summary="${1:-}"
131
+ _VA_SUMMARY="$summary" python3 -c '
132
+ import json, os
133
+ summary = os.environ.get("_VA_SUMMARY", "")[:1000]
134
+ agents = {
135
+ "devils-advocate": {
136
+ "description": "Skeptical re-review when the first three voters unanimously approve.",
137
+ "model": "opus",
138
+ "effort": "xhigh",
139
+ "tools": ["Read", "Grep", "Bash"],
140
+ "prompt": (
141
+ "You are the devils-advocate. The first three voters unanimously "
142
+ "voted APPROVE. Find the strongest reason this is wrong. "
143
+ "Base voter summary:\n"
144
+ f"{summary}\n"
145
+ "Emit one finding with role=devils-advocate, "
146
+ "vote in {APPROVE,REJECT,CANNOT_VALIDATE}, terse reason, "
147
+ "confidence float in [0,1]."
148
+ ),
149
+ },
150
+ }
151
+ print(json.dumps(agents, separators=(",", ":")))
152
+ '
153
+ return 0
154
+ }
155
+
156
+ # ---------- Finding schema path ----------
157
+ # Echo the absolute path to the schema file Dev-A maintains.
158
+ # Returns 1 if the file is missing; the path string is still printed so callers
159
+ # and tests can assert on the resolved location.
160
+ loki_finding_schema_path() {
161
+ local schema="${__LOKI_VA_REPO_ROOT}/loki-ts/data/finding-schema.json"
162
+ printf '%s' "$schema"
163
+ if [ -f "$schema" ]; then
164
+ return 0
165
+ fi
166
+ return 1
167
+ }
168
+
169
+ # ---------- Council dispatch via --agents + --json-schema ----------
170
+ # Orchestrator. Args:
171
+ # $1 -- iteration number (required, int-ish)
172
+ # $2 -- prd path (may be empty)
173
+ #
174
+ # Behavior:
175
+ # 1. Source autonomy/lib/claude-flags.sh; require both --agents and
176
+ # --json-schema in `claude --help`. Either missing -> return 1 (fallback).
177
+ # 2. Build the agents JSON via loki_voter_agents_json.
178
+ # 3. Invoke `claude --dangerously-skip-permissions -p <prompt>
179
+ # --agents <json> --json-schema <path>` and capture stdout.
180
+ # Any non-zero exit or empty stdout -> return 1 (fallback).
181
+ # 4. Parse the response via python3 (no jq dependency): extract
182
+ # findings[].{role, vote, reason, confidence}. On parse failure -> 1.
183
+ # 5. Write per-voter verdicts/<role>-iter-<iter>.json AND the aggregator
184
+ # round file votes/round-<iter>.json in the existing shape consumed by
185
+ # council_evaluate + council_write_transcript.
186
+ # 6. Return 0 on success.
187
+ loki_council_dispatch_agents() {
188
+ local iteration="${1:-}"
189
+ local prd_path="${2:-}"
190
+ if [ -z "$iteration" ]; then
191
+ return 1
192
+ fi
193
+ if [ -z "${COUNCIL_STATE_DIR:-}" ]; then
194
+ return 1
195
+ fi
196
+
197
+ # 1a. Custom council size gate (added per v7.5.21 Phase C reviewer council
198
+ # Opus #2 HIGH finding): the Phase C dispatch hardcodes 3 voters, so when a
199
+ # user sets LOKI_COUNCIL_SIZE != 3 (documented public env in Docker README,
200
+ # wiki Environment-Variables, certification lesson), the dispatch helper
201
+ # would silently emit total_members=3 + threshold=2, breaking the unanimous
202
+ # devil's-advocate check at completion-council.sh that compares
203
+ # complete_count -eq COUNCIL_SIZE. Cheapest correct fix: fall back to the
204
+ # heuristic path which respects COUNCIL_SIZE, until a multi-voter dispatch
205
+ # ships in a later phase. Default COUNCIL_SIZE=3 -> Phase C dispatch active.
206
+ if [ "${COUNCIL_SIZE:-3}" != "3" ]; then
207
+ return 1
208
+ fi
209
+
210
+ # 1b. Managed-council bypass gate (Opus #2 MEDIUM finding): if the
211
+ # experimental managed council is enabled, defer to it -- otherwise its
212
+ # member output would be shadowed by the round file the dispatch writes.
213
+ if [ "${LOKI_EXPERIMENTAL_MANAGED_COUNCIL:-false}" = "true" ]; then
214
+ return 1
215
+ fi
216
+
217
+ # 1c. Flag support gate.
218
+ # shellcheck disable=SC1091
219
+ . "${__LOKI_VA_LIB_DIR}/claude-flags.sh" 2>/dev/null || return 1
220
+ if ! loki_claude_flag_supported "--agents"; then
221
+ return 1
222
+ fi
223
+ if ! loki_claude_flag_supported "--json-schema"; then
224
+ return 1
225
+ fi
226
+
227
+ # 2. Build agents JSON. Empty result means we cannot proceed.
228
+ local agents_json
229
+ agents_json=$(LOKI_ITER="$iteration" LOKI_PRD_PATH="$prd_path" \
230
+ LOKI_RARV_TIER="${LOKI_RARV_TIER:-development}" \
231
+ loki_voter_agents_json)
232
+ if [ -z "$agents_json" ] || [ "$agents_json" = "{}" ]; then
233
+ return 1
234
+ fi
235
+
236
+ local schema_path
237
+ schema_path=$(loki_finding_schema_path) || return 1
238
+
239
+ # 3. Invoke claude. Guard against absent binary or non-zero exit.
240
+ command -v claude >/dev/null 2>&1 || return 1
241
+
242
+ local prompt
243
+ 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")
244
+
245
+ # Capture stderr to a per-iteration log so hung / failing claude
246
+ # invocations are diagnosable instead of silently swallowed. Per Opus #2
247
+ # LOW finding: stored under COUNCIL_STATE_DIR, no PII risk beyond the
248
+ # prompt itself which already lives in the dispatch log.
249
+ local response
250
+ local rc=0
251
+ local stderr_log="$COUNCIL_STATE_DIR/votes/dispatch-stderr-${iteration}.log"
252
+ mkdir -p "$(dirname "$stderr_log")" 2>/dev/null || true
253
+ response=$(claude --dangerously-skip-permissions \
254
+ -p "$prompt" \
255
+ --agents "$agents_json" \
256
+ --json-schema "$schema_path" 2>"$stderr_log") || rc=$?
257
+ if [ "$rc" -ne 0 ] || [ -z "$response" ]; then
258
+ return 1
259
+ fi
260
+
261
+ # 4 + 5. Parse + materialize state files.
262
+ local verdicts_dir="$COUNCIL_STATE_DIR/verdicts"
263
+ local votes_dir="$COUNCIL_STATE_DIR/votes"
264
+ mkdir -p "$verdicts_dir" "$votes_dir" 2>/dev/null || return 1
265
+
266
+ _VA_RESP="$response" \
267
+ _VA_ITER="$iteration" \
268
+ _VA_VDIR="$verdicts_dir" \
269
+ _VA_RFILE="$votes_dir/round-${iteration}.json" \
270
+ python3 -c '
271
+ import json, os, sys
272
+ from datetime import datetime, timezone
273
+
274
+ try:
275
+ resp = json.loads(os.environ["_VA_RESP"])
276
+ except Exception:
277
+ sys.exit(2)
278
+
279
+ findings = resp.get("findings") if isinstance(resp, dict) else None
280
+ if not isinstance(findings, list) or not findings:
281
+ sys.exit(3)
282
+
283
+ it = int(os.environ.get("_VA_ITER", "0") or 0)
284
+ vdir = os.environ["_VA_VDIR"]
285
+ rfile = os.environ["_VA_RFILE"]
286
+
287
+ def to_legacy(vote: str) -> str:
288
+ v = (vote or "").upper()
289
+ if v == "APPROVE":
290
+ return "COMPLETE"
291
+ return "CONTINUE"
292
+
293
+ votes = []
294
+ complete = 0
295
+ total = 0
296
+ for idx, f in enumerate(findings, start=1):
297
+ if not isinstance(f, dict):
298
+ continue
299
+ role = str(f.get("role") or f.get("name") or f"voter-{idx}")
300
+ raw_vote = str(f.get("vote") or "CANNOT_VALIDATE").upper()
301
+ if raw_vote not in ("APPROVE", "REJECT", "CANNOT_VALIDATE"):
302
+ raw_vote = "CANNOT_VALIDATE"
303
+ reason = (f.get("reason") or "").strip()
304
+ try:
305
+ confidence = float(f.get("confidence", 0.0))
306
+ except Exception:
307
+ confidence = 0.0
308
+ legacy = to_legacy(raw_vote)
309
+ total += 1
310
+ if legacy == "COMPLETE":
311
+ complete += 1
312
+ # Per-voter file (architect-specified shape).
313
+ per = {
314
+ "role": role,
315
+ "vote": raw_vote,
316
+ "reason": reason,
317
+ "confidence": confidence,
318
+ "iteration": it,
319
+ }
320
+ try:
321
+ with open(os.path.join(vdir, f"{role}-iter-{it}.json"), "w") as fp:
322
+ json.dump(per, fp, indent=2)
323
+ except OSError:
324
+ sys.exit(4)
325
+ votes.append({
326
+ "member": idx,
327
+ "role": role,
328
+ "vote": legacy,
329
+ "reason": reason,
330
+ })
331
+
332
+ if total == 0:
333
+ sys.exit(5)
334
+
335
+ threshold = (total * 2 + 2) // 3
336
+ verdict = "COMPLETE" if complete >= threshold else "CONTINUE"
337
+ round_data = {
338
+ "round": it,
339
+ "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
340
+ "complete_votes": complete,
341
+ "continue_votes": total - complete,
342
+ "total_members": total,
343
+ "threshold": threshold,
344
+ "verdict": verdict,
345
+ "votes": votes,
346
+ "source": "voter-agents-dispatch",
347
+ }
348
+ try:
349
+ with open(rfile, "w") as fp:
350
+ json.dump(round_data, fp, indent=2)
351
+ except OSError:
352
+ sys.exit(6)
353
+ ' || return 1
354
+
355
+ return 0
356
+ }