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.
- package/README.md +10 -9
- package/SKILL.md +14 -14
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +26 -3
- package/autonomy/lib/claude-flags.sh +132 -0
- package/autonomy/lib/mcp-config.sh +160 -0
- package/autonomy/lib/project-graph.sh +685 -0
- package/autonomy/lib/voter-agents.sh +356 -0
- package/autonomy/loki +108 -111
- package/autonomy/run.sh +95 -186
- package/bin/loki +12 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/requirements.txt +13 -8
- package/dashboard/server.py +33 -15
- package/dashboard/static/index.html +298 -299
- package/docs/INSTALLATION.md +54 -21
- package/docs/retrospectives/v7.5.15-fleet-postmortem.md +325 -0
- package/docs/retrospectives/v7.5.15-honesty-audit.md +136 -0
- package/docs/retrospectives/v7.5.15-llm-failure-modes.md +49 -0
- package/loki-ts/data/finding-schema.json +74 -0
- package/loki-ts/data/model-pricing.json +12 -0
- package/loki-ts/dist/loki.js +198 -172
- package/mcp/__init__.py +1 -1
- package/mcp/lsp_proxy.py +713 -0
- package/mcp/requirements.txt +9 -3
- package/mcp/tests/__init__.py +0 -0
- package/mcp/tests/test_lsp_proxy.py +377 -0
- package/memory/app_graph.py +153 -0
- package/memory/storage.py +6 -1
- package/memory/tests/test_app_graph.py +134 -0
- package/package.json +4 -3
- package/providers/claude.sh +115 -4
- package/providers/codex.sh +2 -2
- package/providers/loader.sh +4 -4
- package/providers/model_catalog.json +0 -9
- package/providers/models.sh +1 -2
- package/references/multi-provider.md +26 -35
- package/references/prompt-repetition.md +1 -1
- package/references/quality-control.md +1 -1
- package/skills/00-index.md +3 -3
- package/skills/model-selection.md +11 -14
- package/skills/providers.md +17 -57
- package/skills/quality-gates.md +2 -2
- package/skills/troubleshooting.md +1 -1
- package/src/integrations/github/action-handler.js +3 -2
- package/src/protocols/tools/start-project.js +1 -1
- 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
|
+
}
|