loki-mode 7.128.1 → 7.129.0
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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/council-v2.sh +41 -1
- package/autonomy/lib/cockpit-render.sh +28 -2
- package/autonomy/lib/cr-rematerialize.py +120 -0
- package/autonomy/lib/done-recognition.sh +48 -0
- package/autonomy/lib/voter-agents.sh +13 -3
- package/autonomy/loki +10 -4
- package/autonomy/run.sh +51 -2
- package/autonomy/verify.sh +38 -8
- package/dashboard/__init__.py +1 -1
- package/dashboard/registry.py +44 -3
- package/docs/INSTALLATION.md +2 -2
- package/docs/RARV-C-100X-PLAN.md +278 -0
- package/docs/RARV-C-CHANGE-MAP.md +5 -0
- package/docs/RARV-C-LOOP-EFFICIENCY-PLAN.md +5 -0
- package/loki-ts/data/code-review-schema.json +39 -0
- package/loki-ts/data/council-v2-schema.json +45 -0
- package/loki-ts/data/done-recognition-schema.json +51 -0
- package/loki-ts/dist/cockpit.js +9 -9
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.129.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
408
408
|
|
|
409
409
|
---
|
|
410
410
|
|
|
411
|
-
**v7.
|
|
411
|
+
**v7.129.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.129.0
|
package/autonomy/council-v2.sh
CHANGED
|
@@ -296,7 +296,47 @@ Respond ONLY with a valid JSON object. No markdown fencing."
|
|
|
296
296
|
# inline prefix here is belt-and-suspenders so the carve-out is
|
|
297
297
|
# self-documenting and robust to sourcing order. No-op when caveman
|
|
298
298
|
# is absent.
|
|
299
|
-
|
|
299
|
+
#
|
|
300
|
+
# STRUCTURED VERDICT (v8.x): when the CLI supports --json-schema,
|
|
301
|
+
# force valid JSON so the sed-carve below never has to rescue
|
|
302
|
+
# prose-wrapped output. --json-schema takes INLINE content, not a
|
|
303
|
+
# path (CLI 2.1.207 rejects a path). On any failure fall through to
|
|
304
|
+
# the plain -p call + sed-carve (unchanged, fail-closed to REJECT).
|
|
305
|
+
# Opt out LOKI_REVIEW_JSON_SCHEMA=off. claude branch only.
|
|
306
|
+
local _c2_schema_dir _c2_schema
|
|
307
|
+
_c2_schema_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
308
|
+
_c2_schema="${_c2_schema_dir}/loki-ts/data/council-v2-schema.json"
|
|
309
|
+
result=""
|
|
310
|
+
if [ "${LOKI_REVIEW_JSON_SCHEMA:-on}" != "off" ] && [ -f "$_c2_schema" ] \
|
|
311
|
+
&& type loki_claude_flag_supported >/dev/null 2>&1 \
|
|
312
|
+
&& loki_claude_flag_supported "--json-schema"; then
|
|
313
|
+
local _c2_schema_content _c2_json
|
|
314
|
+
_c2_schema_content="$(cat "$_c2_schema" 2>/dev/null)" || _c2_schema_content=""
|
|
315
|
+
if [ -n "$_c2_schema_content" ]; then
|
|
316
|
+
_c2_json="$(echo "$full_prompt" | CAVEMAN_DEFAULT_MODE=off claude "${_c2_argv[@]}" -p \
|
|
317
|
+
--json-schema "$_c2_schema_content" --output-format json 2>/dev/null)" || _c2_json=""
|
|
318
|
+
if [ -n "$_c2_json" ]; then
|
|
319
|
+
# Pull the schema-validated object out of the envelope.
|
|
320
|
+
result="$(printf '%s' "$_c2_json" | python3 -c 'import sys,json
|
|
321
|
+
try:
|
|
322
|
+
e=json.load(sys.stdin)
|
|
323
|
+
p=e.get("structured_output")
|
|
324
|
+
if not isinstance(p,dict):
|
|
325
|
+
r=e.get("result")
|
|
326
|
+
p=json.loads(r) if isinstance(r,str) else None
|
|
327
|
+
if isinstance(p,dict) and str(p.get("verdict","")).upper() in ("APPROVE","REJECT"):
|
|
328
|
+
sys.stdout.write(json.dumps(p))
|
|
329
|
+
else:
|
|
330
|
+
sys.exit(1)
|
|
331
|
+
except Exception:
|
|
332
|
+
sys.exit(1)' 2>/dev/null)" || result=""
|
|
333
|
+
fi
|
|
334
|
+
fi
|
|
335
|
+
fi
|
|
336
|
+
# Fall through to the plain text call (+ sed-carve) on any miss.
|
|
337
|
+
if [ -z "$result" ]; then
|
|
338
|
+
result=$(echo "$full_prompt" | CAVEMAN_DEFAULT_MODE=off claude "${_c2_argv[@]}" -p 2>/dev/null || echo '{"verdict":"REJECT","reasoning":"review execution failed","issues":[]}')
|
|
339
|
+
fi
|
|
300
340
|
else
|
|
301
341
|
result='{"verdict":"REJECT","reasoning":"reviewer CLI unavailable","issues":[]}'
|
|
302
342
|
fi
|
|
@@ -131,20 +131,46 @@ fleet = []
|
|
|
131
131
|
runs = []
|
|
132
132
|
try:
|
|
133
133
|
from dashboard import registry
|
|
134
|
+
# Self-heal: mark dead-pid "running" zombies as stopped before reading the
|
|
135
|
+
# fleet, so the cockpit does not show 100+ frozen "BUILDING" entries from
|
|
136
|
+
# reaped/crashed sessions. Best-effort; a failure here never blocks the render.
|
|
137
|
+
try:
|
|
138
|
+
registry.reconcile_stale_runs()
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
134
141
|
runs = registry.get_fleet_runs(include_inactive=True)
|
|
135
142
|
except Exception:
|
|
136
143
|
runs = []
|
|
137
144
|
|
|
138
145
|
for r in runs:
|
|
146
|
+
_running = bool(r.get("running"))
|
|
147
|
+
_status = (r.get("status") or "").lower()
|
|
148
|
+
# A not-running entry must not display an in-flight phase (BUILDING/etc): the
|
|
149
|
+
# reconciled registry status is authoritative for dead runs, so a stopped or
|
|
150
|
+
# stale run shows its status, not a frozen phase left in a stale state file.
|
|
151
|
+
if _running:
|
|
152
|
+
_phase = r.get("phase", "") or ""
|
|
153
|
+
elif _status in ("stale", "stopped", "completed", "failed"):
|
|
154
|
+
_phase = _status
|
|
155
|
+
else:
|
|
156
|
+
_phase = r.get("phase", "") or _status
|
|
139
157
|
fleet.append({
|
|
140
158
|
"name": r.get("name") or os.path.basename(r.get("path", "") or "project"),
|
|
141
159
|
"path": r.get("path", "") or "",
|
|
142
|
-
"phase":
|
|
160
|
+
"phase": _phase,
|
|
143
161
|
"iteration": r.get("iteration", 0) or 0,
|
|
144
162
|
"status": r.get("status", "") or "",
|
|
145
|
-
"running":
|
|
163
|
+
"running": _running,
|
|
146
164
|
})
|
|
147
165
|
|
|
166
|
+
# Live-first ordering: running builds, then everything else by most-recent.
|
|
167
|
+
# Keeps the useful runs at the top when the fleet has many stopped/stale entries.
|
|
168
|
+
def _fleet_sort_key(e):
|
|
169
|
+
st = (e.get("status") or "").lower()
|
|
170
|
+
rank = 0 if e.get("running") else (1 if st not in ("stale", "stopped") else 2)
|
|
171
|
+
return (rank, -(e.get("iteration") or 0))
|
|
172
|
+
fleet.sort(key=_fleet_sort_key)
|
|
173
|
+
|
|
148
174
|
# Focused run: --repo wins; else first running fleet run; else cwd.
|
|
149
175
|
focus_path = focus or cwd
|
|
150
176
|
focus_entry = None
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Re-materialize a `claude --json-schema --output-format json` code-review
|
|
3
|
+
envelope into the LEGACY 'VERDICT: X\\nFINDINGS:\\n- [severity] description'
|
|
4
|
+
text, so the downstream text parsers (_classify_verdict, _severity_is_blocking,
|
|
5
|
+
_count_nonblocking_findings in autonomy/run.sh, and the TS mirror) stay
|
|
6
|
+
byte-identical. This is the adapter that lets the code-review verdict path adopt
|
|
7
|
+
structured output WITHOUT rewriting any consumer.
|
|
8
|
+
|
|
9
|
+
Contract:
|
|
10
|
+
- Reads the raw CLI envelope JSON from env _LOKI_CR_JSON (or stdin if unset).
|
|
11
|
+
- Writes legacy text to the path in env _LOKI_CR_OUT (or stdout if unset).
|
|
12
|
+
- FAIL-CLOSED: exits non-zero on ANY structural miss so the bash caller falls
|
|
13
|
+
through to the text path. NEVER emits 'VERDICT: PASS' on a miss.
|
|
14
|
+
- T1 SAFETY (cross-field enforcement JSON Schema cannot express): if ANY finding
|
|
15
|
+
is Critical or High, the verdict is FORCED to FAIL regardless of the model's
|
|
16
|
+
emitted verdict token. A self-contradictory PASS+Critical can never be waved
|
|
17
|
+
through as non-blocking.
|
|
18
|
+
|
|
19
|
+
Live-verified envelope shape (claude 2.1.207): top-level 'structured_output'
|
|
20
|
+
object is the payload; 'result' is the same JSON as a string (fallback);
|
|
21
|
+
'stop_reason' is 'tool_use' for a valid structured turn.
|
|
22
|
+
|
|
23
|
+
Exit codes (all non-zero == fail-closed, caller falls through to text):
|
|
24
|
+
0 success 2 no input/output target 3 envelope not JSON / not a dict
|
|
25
|
+
4 wrong stop_reason 5 no usable payload 6 missing/invalid verdict
|
|
26
|
+
7 write error
|
|
27
|
+
"""
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
_BLOCKING = {"CRITICAL", "HIGH"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def rematerialize(raw):
|
|
36
|
+
"""Return legacy VERDICT/FINDINGS text, or raise ValueError(exit_code)."""
|
|
37
|
+
try:
|
|
38
|
+
env = json.loads(raw)
|
|
39
|
+
except Exception:
|
|
40
|
+
raise ValueError(3)
|
|
41
|
+
if not isinstance(env, dict):
|
|
42
|
+
raise ValueError(3)
|
|
43
|
+
|
|
44
|
+
# A valid structured turn ends with tool_use. A refusal / max_tokens / other
|
|
45
|
+
# stop_reason means the schema was not honored -> fail-closed.
|
|
46
|
+
if env.get("stop_reason") not in (None, "tool_use"):
|
|
47
|
+
raise ValueError(4)
|
|
48
|
+
|
|
49
|
+
payload = env.get("structured_output")
|
|
50
|
+
if not isinstance(payload, dict):
|
|
51
|
+
res = env.get("result")
|
|
52
|
+
if isinstance(res, str):
|
|
53
|
+
try:
|
|
54
|
+
payload = json.loads(res)
|
|
55
|
+
except Exception:
|
|
56
|
+
payload = None
|
|
57
|
+
if not isinstance(payload, dict):
|
|
58
|
+
raise ValueError(5)
|
|
59
|
+
|
|
60
|
+
verdict = str(payload.get("verdict", "")).strip().upper()
|
|
61
|
+
if verdict not in ("PASS", "FAIL"):
|
|
62
|
+
raise ValueError(6)
|
|
63
|
+
|
|
64
|
+
findings = payload.get("findings")
|
|
65
|
+
if not isinstance(findings, list):
|
|
66
|
+
findings = []
|
|
67
|
+
|
|
68
|
+
body = []
|
|
69
|
+
has_blocking = False
|
|
70
|
+
for f in findings:
|
|
71
|
+
if not isinstance(f, dict):
|
|
72
|
+
continue
|
|
73
|
+
sev = str(f.get("severity", "")).strip()
|
|
74
|
+
desc = str(f.get("description", "")).strip()
|
|
75
|
+
if not sev or not desc:
|
|
76
|
+
continue
|
|
77
|
+
if sev.upper() in _BLOCKING:
|
|
78
|
+
has_blocking = True
|
|
79
|
+
# Bracketed severity form matches _severity_is_blocking /
|
|
80
|
+
# _count_nonblocking_findings regexes exactly.
|
|
81
|
+
body.append("- [" + sev + "] " + desc)
|
|
82
|
+
|
|
83
|
+
# T1: a Critical/High finding forces FAIL even if the model said PASS.
|
|
84
|
+
if has_blocking:
|
|
85
|
+
verdict = "FAIL"
|
|
86
|
+
|
|
87
|
+
lines = ["VERDICT: " + verdict, "FINDINGS:"]
|
|
88
|
+
if body:
|
|
89
|
+
lines.extend(body)
|
|
90
|
+
else:
|
|
91
|
+
lines.append("- None")
|
|
92
|
+
return "\n".join(lines) + "\n"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main():
|
|
96
|
+
raw = os.environ.get("_LOKI_CR_JSON")
|
|
97
|
+
if raw is None:
|
|
98
|
+
raw = sys.stdin.read()
|
|
99
|
+
out_path = os.environ.get("_LOKI_CR_OUT", "")
|
|
100
|
+
if not raw:
|
|
101
|
+
return 2
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
text = rematerialize(raw)
|
|
105
|
+
except ValueError as e:
|
|
106
|
+
return int(e.args[0])
|
|
107
|
+
|
|
108
|
+
if out_path:
|
|
109
|
+
try:
|
|
110
|
+
with open(out_path, "w") as fp:
|
|
111
|
+
fp.write(text)
|
|
112
|
+
except OSError:
|
|
113
|
+
return 7
|
|
114
|
+
else:
|
|
115
|
+
sys.stdout.write(text)
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
sys.exit(main())
|
|
@@ -51,6 +51,54 @@
|
|
|
51
51
|
_loki_done_recog_invoke() {
|
|
52
52
|
local prompt="$1"
|
|
53
53
|
command -v claude >/dev/null 2>&1 || return 1
|
|
54
|
+
|
|
55
|
+
# STRUCTURED VERDICT (v8.x): when the CLI supports --json-schema, force valid
|
|
56
|
+
# JSON so parse_object never has to brace-slice prose. --json-schema takes
|
|
57
|
+
# INLINE content, not a path (CLI 2.1.207 rejects a path). On any miss (flag
|
|
58
|
+
# unsupported, empty, no structured_output) fall through to the plain -p call
|
|
59
|
+
# below; parse_object then handles it and defaults to inconclusive (safe).
|
|
60
|
+
# Opt out LOKI_REVIEW_JSON_SCHEMA=off.
|
|
61
|
+
local _dr_schema_dir _dr_schema
|
|
62
|
+
_dr_schema_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
63
|
+
_dr_schema="${_dr_schema_dir}/loki-ts/data/done-recognition-schema.json"
|
|
64
|
+
if [ "${LOKI_REVIEW_JSON_SCHEMA:-on}" != "off" ] && [ -f "$_dr_schema" ] \
|
|
65
|
+
&& type loki_claude_flag_supported >/dev/null 2>&1 \
|
|
66
|
+
&& loki_claude_flag_supported "--json-schema"; then
|
|
67
|
+
local _dr_schema_content _dr_json _dr_obj
|
|
68
|
+
_dr_schema_content="$(cat "$_dr_schema" 2>/dev/null)" || _dr_schema_content=""
|
|
69
|
+
if [ -n "$_dr_schema_content" ]; then
|
|
70
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
71
|
+
_dr_json=$(CAVEMAN_DEFAULT_MODE=off timeout "${LOKI_DONE_RECOG_TIMEOUT}" \
|
|
72
|
+
claude --dangerously-skip-permissions -p "$prompt" \
|
|
73
|
+
--json-schema "$_dr_schema_content" --output-format json 2>/dev/null) || _dr_json=""
|
|
74
|
+
else
|
|
75
|
+
_dr_json=$(CAVEMAN_DEFAULT_MODE=off \
|
|
76
|
+
claude --dangerously-skip-permissions -p "$prompt" \
|
|
77
|
+
--json-schema "$_dr_schema_content" --output-format json 2>/dev/null) || _dr_json=""
|
|
78
|
+
fi
|
|
79
|
+
if [ -n "$_dr_json" ]; then
|
|
80
|
+
_dr_obj=$(printf '%s' "$_dr_json" | python3 -c 'import sys,json
|
|
81
|
+
try:
|
|
82
|
+
e=json.load(sys.stdin)
|
|
83
|
+
p=e.get("structured_output")
|
|
84
|
+
if not isinstance(p,dict):
|
|
85
|
+
r=e.get("result")
|
|
86
|
+
p=json.loads(r) if isinstance(r,str) else None
|
|
87
|
+
if isinstance(p,dict) and "requirements" in p:
|
|
88
|
+
sys.stdout.write(json.dumps(p))
|
|
89
|
+
else:
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
except Exception:
|
|
92
|
+
sys.exit(1)' 2>/dev/null) || _dr_obj=""
|
|
93
|
+
if [ -n "$_dr_obj" ]; then
|
|
94
|
+
printf '%s' "$_dr_obj"
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
# fall through to the plain text call (parse_object handles it, inconclusive-safe)
|
|
100
|
+
fi
|
|
101
|
+
|
|
54
102
|
local rc=0
|
|
55
103
|
local out=""
|
|
56
104
|
if command -v timeout >/dev/null 2>&1; then
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# autonomy/lib/voter-agents.sh -- Phase C (v7.5.20) bash side.
|
|
3
3
|
#
|
|
4
4
|
# Builds the JSON declaration of the council voter agents and orchestrates a
|
|
5
|
-
# single `claude --agents <json> --json-schema <
|
|
5
|
+
# single `claude --agents <json> --json-schema <inline-content>` dispatch per iteration.
|
|
6
6
|
# Replaces the ad-hoc per-voter heuristic loop in council_aggregate_votes when
|
|
7
7
|
# the locally installed Claude CLI supports both flags.
|
|
8
8
|
#
|
|
@@ -176,7 +176,7 @@ loki_finding_schema_path() {
|
|
|
176
176
|
# --json-schema in `claude --help`. Either missing -> return 1 (fallback).
|
|
177
177
|
# 2. Build the agents JSON via loki_voter_agents_json.
|
|
178
178
|
# 3. Invoke `claude --dangerously-skip-permissions -p <prompt>
|
|
179
|
-
# --agents <json> --json-schema <
|
|
179
|
+
# --agents <json> --json-schema <inline-content>` and capture stdout.
|
|
180
180
|
# Any non-zero exit or empty stdout -> return 1 (fallback).
|
|
181
181
|
# 4. Parse the response via python3 (no jq dependency): extract
|
|
182
182
|
# findings[].{role, vote, reason, confidence}. On parse failure -> 1.
|
|
@@ -236,6 +236,16 @@ loki_council_dispatch_agents() {
|
|
|
236
236
|
local schema_path
|
|
237
237
|
schema_path=$(loki_finding_schema_path) || return 1
|
|
238
238
|
|
|
239
|
+
# v8.x FIX: `claude --json-schema` takes the schema JSON as INLINE CONTENT,
|
|
240
|
+
# not a file path. Live-verified on CLI 2.1.207: passing a path errors with
|
|
241
|
+
# "--json-schema is not valid JSON: Unrecognized token '/'" -> rc!=0 ->
|
|
242
|
+
# fail-closed to the heuristic path, so this structured dispatch was SILENTLY
|
|
243
|
+
# DEAD (never actually schema-constrained the council). Read the file and pass
|
|
244
|
+
# its content. Fail-closed if unreadable/empty (return 1 -> heuristic path).
|
|
245
|
+
local schema_content
|
|
246
|
+
schema_content=$(cat "$schema_path" 2>/dev/null) || return 1
|
|
247
|
+
[ -n "$schema_content" ] || return 1
|
|
248
|
+
|
|
239
249
|
# 3. Invoke claude. Guard against absent binary or non-zero exit.
|
|
240
250
|
command -v claude >/dev/null 2>&1 || return 1
|
|
241
251
|
|
|
@@ -274,7 +284,7 @@ loki_council_dispatch_agents() {
|
|
|
274
284
|
env CAVEMAN_DEFAULT_MODE=off claude --dangerously-skip-permissions \
|
|
275
285
|
-p "$prompt" \
|
|
276
286
|
--agents "$agents_json" \
|
|
277
|
-
--json-schema "$
|
|
287
|
+
--json-schema "$schema_content" 2>"$stderr_log") || rc=$?
|
|
278
288
|
if [ "$rc" -ne 0 ] || [ -z "$response" ]; then
|
|
279
289
|
return 1
|
|
280
290
|
fi
|
package/autonomy/loki
CHANGED
|
@@ -826,7 +826,7 @@ show_help() {
|
|
|
826
826
|
echo "New here? Try these in order:"
|
|
827
827
|
echo " loki doctor Check your setup is ready (a few seconds)"
|
|
828
828
|
echo " loki quickstart Guided first build from your idea (no PRD needed)"
|
|
829
|
-
echo " loki start ./prd.md Build from a spec (PRD file, GitHub issue, or no arg)"
|
|
829
|
+
echo " loki start ./prd.md Build from a spec (PRD file, GitHub/GitLab/Jira/Azure issue, or no arg)"
|
|
830
830
|
echo " quickstart = guided first build; quick = one small task (3 iters max)."
|
|
831
831
|
echo " Docs: https://github.com/asklokesh/loki-mode | Report a problem: loki crash"
|
|
832
832
|
echo ""
|
|
@@ -846,7 +846,7 @@ show_help() {
|
|
|
846
846
|
echo "Commands:"
|
|
847
847
|
echo ""
|
|
848
848
|
echo "Build:"
|
|
849
|
-
echo " start [SPEC] Start a build (PRD file, GitHub issue, or no arg)"
|
|
849
|
+
echo " start [SPEC] Start a build (PRD file, GitHub/GitLab/Jira/Azure issue, or no arg)"
|
|
850
850
|
echo " plan <PRD-file> Dry-run analysis: complexity, cost, execution plan"
|
|
851
851
|
echo " quickstart [idea] Guided first build: setup, idea, template, plan, go"
|
|
852
852
|
echo ""
|
|
@@ -1043,7 +1043,7 @@ show_landing() {
|
|
|
1043
1043
|
echo -e " ${CYAN}loki tour${NC} See a real result now (no provider, no key, no spend)"
|
|
1044
1044
|
echo -e " ${CYAN}loki doctor${NC} Check your setup is ready (a few seconds)"
|
|
1045
1045
|
echo -e " ${CYAN}loki quickstart${NC} Guided first build from your idea (no PRD needed)"
|
|
1046
|
-
echo -e " ${CYAN}loki start ./prd.md${NC} Build from a spec (PRD file, GitHub issue, or no arg)"
|
|
1046
|
+
echo -e " ${CYAN}loki start ./prd.md${NC} Build from a spec (PRD file, GitHub/GitLab/Jira/Azure issue, or no arg)"
|
|
1047
1047
|
echo " quickstart = guided first build; quick = one small task (3 iters max)."
|
|
1048
1048
|
echo ""
|
|
1049
1049
|
echo -e "Need help? ${CYAN}loki help${NC} lists every command."
|
|
@@ -8821,7 +8821,13 @@ if council:
|
|
|
8821
8821
|
fleet = s.get('fleet') or []
|
|
8822
8822
|
if fleet:
|
|
8823
8823
|
cap = 8 # cap so a huge registry (e.g. 171 runs) never floods the summary
|
|
8824
|
-
|
|
8824
|
+
active = sum(1 for r in fleet if r.get("running"))
|
|
8825
|
+
# Header shows active vs total so a long tail of finished/stale runs reads
|
|
8826
|
+
# honestly (e.g. "0 active / 136 total") instead of an alarming bare count.
|
|
8827
|
+
if active:
|
|
8828
|
+
print(f" Fleet ({active} active / {len(fleet)} total):")
|
|
8829
|
+
else:
|
|
8830
|
+
print(f" Fleet ({len(fleet)} total, none active):")
|
|
8825
8831
|
for r in fleet[:cap]:
|
|
8826
8832
|
mark = "*" if r.get("running") else " "
|
|
8827
8833
|
print(f" {mark} {r.get('name','?'):<28} iter {r.get('iteration',0)} {r.get('phase') or r.get('status') or ''}")
|
package/autonomy/run.sh
CHANGED
|
@@ -5911,8 +5911,15 @@ compute_codebase_signature() {
|
|
|
5911
5911
|
# truncating the output and misaligning the path<->hash pairing. Drop
|
|
5912
5912
|
# them; their removal from the list is itself the detected change.
|
|
5913
5913
|
gitc_deleted=$(git ls-files --deleted -z 2>/dev/null | tr '\0' '\n')
|
|
5914
|
+
# Exclude .loki/ AND Loki's own session-end artifacts (HANDOFF.md,
|
|
5915
|
+
# USAGE.md) written+committed to the repo ROOT by the completion path
|
|
5916
|
+
# (LOKI_HANDOFF / commit_session_changes, both default-on). Those are
|
|
5917
|
+
# runtime OUTPUT of the session, not user codebase; counting them would
|
|
5918
|
+
# make the next PRD-reuse check see a spurious "codebase changed" and
|
|
5919
|
+
# flip reuse->update (the signature is persisted per-iteration BEFORE
|
|
5920
|
+
# the after-loop HANDOFF write, so it can never account for them).
|
|
5914
5921
|
gitc_paths=$( { git ls-files -z 2>/dev/null; git ls-files --others --exclude-standard -z 2>/dev/null; } \
|
|
5915
|
-
| tr '\0' '\n' | grep -vE '(^|/)\.loki(/|$)' | LC_ALL=C sort -u )
|
|
5922
|
+
| tr '\0' '\n' | grep -vE '(^|/)\.loki(/|$)|^HANDOFF\.md$|^USAGE\.md$' | LC_ALL=C sort -u )
|
|
5916
5923
|
if [ -n "$gitc_deleted" ]; then
|
|
5917
5924
|
gitc_paths=$(printf '%s\n' "$gitc_paths" | grep -vxF -f <(printf '%s\n' "$gitc_deleted") || true)
|
|
5918
5925
|
fi
|
|
@@ -5933,6 +5940,7 @@ compute_codebase_signature() {
|
|
|
5933
5940
|
-o -name build -o -name .next -o -name target -o -name vendor \
|
|
5934
5941
|
-o -name __pycache__ -o -name .venv -o -name venv \) -prune -o \
|
|
5935
5942
|
-type f -print 2>/dev/null \
|
|
5943
|
+
| grep -vE '^\./(HANDOFF|USAGE)\.md$' \
|
|
5936
5944
|
| while IFS= read -r f; do
|
|
5937
5945
|
local sz
|
|
5938
5946
|
sz=$(stat -f%z "$f" 2>/dev/null || stat -c%s "$f" 2>/dev/null || echo 0)
|
|
@@ -9658,8 +9666,16 @@ sys.stdout.write(t.strip())
|
|
|
9658
9666
|
local output
|
|
9659
9667
|
# Pass matched files explicitly (node --test globbing is
|
|
9660
9668
|
# Node-version-sensitive); quote each so paths with spaces survive.
|
|
9669
|
+
# Force --test-reporter=tap: Node 20+ made the human "spec" reporter
|
|
9670
|
+
# the default for a TTY and Node 26 emits it even under capture
|
|
9671
|
+
# ("i pass N" / check+cross marks) instead of the TAP "# pass N" /
|
|
9672
|
+
# "ok N - " lines that BOTH the count parser below AND
|
|
9673
|
+
# _loki_zero_tests_executed rely on. TAP has been stable since Node 18,
|
|
9674
|
+
# so forcing it restores a deterministic, version-independent format
|
|
9675
|
+
# without touching any parser. Older node ignores nothing here (tap is
|
|
9676
|
+
# a valid built-in reporter on every supported version).
|
|
9661
9677
|
output=$(cd "${TARGET_DIR:-.}" && timeout "${LOKI_GATE_TIMEOUT:-300}" \
|
|
9662
|
-
node --test "${_nt_files[@]}" 2>&1) || test_passed=false
|
|
9678
|
+
node --test --test-reporter=tap "${_nt_files[@]}" 2>&1) || test_passed=false
|
|
9663
9679
|
# tail -14 so node's TAP summary block (# tests / # pass N / # fail N,
|
|
9664
9680
|
# ~10 lines) survives truncation for the best-effort count parse below.
|
|
9665
9681
|
details="node --test: $(echo "$output" | tail -14 | tr '\n' ' ')"
|
|
@@ -10994,6 +11010,39 @@ _dispatch_reviewer() {
|
|
|
10994
11010
|
# then deletes its flag and emits nothing). Set inline, not via
|
|
10995
11011
|
# the helper, so the carve-out holds even when the helper is
|
|
10996
11012
|
# out of scope. No-op when caveman is absent.
|
|
11013
|
+
# STRUCTURED VERDICT (v8.x): when the CLI supports --json-schema,
|
|
11014
|
+
# force valid JSON and re-materialize the LEGACY VERDICT/FINDINGS text
|
|
11015
|
+
# so every downstream consumer (_classify_verdict,
|
|
11016
|
+
# _severity_is_blocking, _count_nonblocking_findings, mergeability,
|
|
11017
|
+
# DA arm, aggregate.json) stays byte-identical. Fail-closed: any
|
|
11018
|
+
# failure falls through to the text path below (which itself may leave
|
|
11019
|
+
# an empty file -> NO_VERDICT -> inconclusive block). NEVER a PASS on a
|
|
11020
|
+
# miss. --json-schema takes INLINE content, not a path (CLI 2.1.207
|
|
11021
|
+
# rejects a path). Opt out with LOKI_REVIEW_JSON_SCHEMA=off.
|
|
11022
|
+
local _cr_root _cr_here _cr_schema _cr_remat
|
|
11023
|
+
_cr_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
11024
|
+
_cr_here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11025
|
+
_cr_schema="${_cr_root}/loki-ts/data/code-review-schema.json"
|
|
11026
|
+
_cr_remat="${_cr_here}/lib/cr-rematerialize.py"
|
|
11027
|
+
if [ "${LOKI_REVIEW_JSON_SCHEMA:-on}" != "off" ] \
|
|
11028
|
+
&& [ -f "$_cr_schema" ] && [ -f "$_cr_remat" ] \
|
|
11029
|
+
&& type loki_claude_flag_supported >/dev/null 2>&1 \
|
|
11030
|
+
&& loki_claude_flag_supported "--json-schema"; then
|
|
11031
|
+
local _cr_schema_content _cr_json _cr_rc=0
|
|
11032
|
+
_cr_schema_content="$(cat "$_cr_schema" 2>/dev/null)" || _cr_schema_content=""
|
|
11033
|
+
if [ -n "$_cr_schema_content" ]; then
|
|
11034
|
+
_cr_json="$(CAVEMAN_DEFAULT_MODE=off \
|
|
11035
|
+
claude "${_rv_argv[@]}" -p "$prompt_text" \
|
|
11036
|
+
--json-schema "$_cr_schema_content" \
|
|
11037
|
+
--output-format json 2>/dev/null)" || _cr_rc=$?
|
|
11038
|
+
if [ "$_cr_rc" -eq 0 ] && [ -n "$_cr_json" ] \
|
|
11039
|
+
&& _LOKI_CR_JSON="$_cr_json" _LOKI_CR_OUT="$review_output" \
|
|
11040
|
+
python3 "$_cr_remat"; then
|
|
11041
|
+
return 0
|
|
11042
|
+
fi
|
|
11043
|
+
fi
|
|
11044
|
+
# fall through to the text path below (fail-closed)
|
|
11045
|
+
fi
|
|
10997
11046
|
CAVEMAN_DEFAULT_MODE=off \
|
|
10998
11047
|
claude "${_rv_argv[@]}" -p "$prompt_text" \
|
|
10999
11048
|
--output-format text > "$review_output" 2>/dev/null
|
package/autonomy/verify.sh
CHANGED
|
@@ -427,6 +427,14 @@ _verify_zero_tests_executed() {
|
|
|
427
427
|
verify_gate_tests() {
|
|
428
428
|
local tree="$1"
|
|
429
429
|
local timeout_s="${LOKI_GATE_TIMEOUT:-300}"
|
|
430
|
+
# Bounded-exec prefix: "timeout <secs>" / "gtimeout <secs>", or EMPTY when
|
|
431
|
+
# neither is on PATH (bare macOS, or a shadowed PATH). A bare `timeout ...`
|
|
432
|
+
# would exit 127 (command not found) and be misread as a test FAILURE ->
|
|
433
|
+
# spurious BLOCKED verdict. Degrade to running without a wall-clock cap
|
|
434
|
+
# instead. Mirrors run.sh's with_timeout discipline.
|
|
435
|
+
local _vt_bin _vt
|
|
436
|
+
_vt_bin="$(_verify_runtime_timeout_bin)"
|
|
437
|
+
if [ -n "$_vt_bin" ]; then _vt="$_vt_bin $timeout_s"; else _vt=""; fi
|
|
430
438
|
local runner="none"
|
|
431
439
|
local rc=0
|
|
432
440
|
local out=""
|
|
@@ -434,13 +442,13 @@ verify_gate_tests() {
|
|
|
434
442
|
if [ -f "$tree/package.json" ]; then
|
|
435
443
|
if grep -q '"vitest"' "$tree/package.json" 2>/dev/null; then
|
|
436
444
|
runner="vitest"
|
|
437
|
-
out="$(cd "$tree" &&
|
|
445
|
+
out="$(cd "$tree" && $_vt npx vitest run 2>&1)" || rc=$?
|
|
438
446
|
elif grep -q '"jest"' "$tree/package.json" 2>/dev/null; then
|
|
439
447
|
runner="jest"
|
|
440
|
-
out="$(cd "$tree" &&
|
|
448
|
+
out="$(cd "$tree" && $_vt npx jest --passWithNoTests --forceExit 2>&1)" || rc=$?
|
|
441
449
|
elif grep -q '"mocha"' "$tree/package.json" 2>/dev/null; then
|
|
442
450
|
runner="mocha"
|
|
443
|
-
out="$(cd "$tree" &&
|
|
451
|
+
out="$(cd "$tree" && $_vt npx mocha 2>&1)" || rc=$?
|
|
444
452
|
fi
|
|
445
453
|
fi
|
|
446
454
|
|
|
@@ -480,7 +488,7 @@ verify_gate_tests() {
|
|
|
480
488
|
if [ "$has_python" = "true" ]; then
|
|
481
489
|
if command -v pytest >/dev/null 2>&1; then
|
|
482
490
|
runner="pytest"
|
|
483
|
-
out="$(cd "$tree" &&
|
|
491
|
+
out="$(cd "$tree" && $_vt pytest --tb=short 2>&1)" || rc=$?
|
|
484
492
|
elif command -v python3 >/dev/null 2>&1; then
|
|
485
493
|
# #139 parity: pytest ABSENT but a Python suite exists -> run it with
|
|
486
494
|
# stdlib unittest (zero third-party deps) instead of going blindly
|
|
@@ -490,7 +498,7 @@ verify_gate_tests() {
|
|
|
490
498
|
# a "Ran 0 tests" line detected by the zero-test guard on the run.sh
|
|
491
499
|
# mirror) so this never green-washes an empty suite.
|
|
492
500
|
runner="unittest"
|
|
493
|
-
out="$(cd "$tree" &&
|
|
501
|
+
out="$(cd "$tree" && $_vt python3 -m unittest discover -p 'test_*.py' 2>&1)" || rc=$?
|
|
494
502
|
else
|
|
495
503
|
# Applicable but cannot run -> inconclusive (Entanglement 2).
|
|
496
504
|
_verify_add_gate "tests" "inconclusive" "pytest" "python project detected but no pytest/python3 on PATH" "true"
|
|
@@ -502,7 +510,7 @@ verify_gate_tests() {
|
|
|
502
510
|
if [ "$runner" = "none" ] && [ -f "$tree/go.mod" ]; then
|
|
503
511
|
if command -v go >/dev/null 2>&1; then
|
|
504
512
|
runner="go-test"
|
|
505
|
-
out="$(cd "$tree" &&
|
|
513
|
+
out="$(cd "$tree" && $_vt go test ./... 2>&1)" || rc=$?
|
|
506
514
|
else
|
|
507
515
|
_verify_add_gate "tests" "inconclusive" "go-test" "go project detected but go not on PATH" "true"
|
|
508
516
|
return 0
|
|
@@ -512,7 +520,7 @@ verify_gate_tests() {
|
|
|
512
520
|
if [ "$runner" = "none" ] && [ -f "$tree/Cargo.toml" ]; then
|
|
513
521
|
if command -v cargo >/dev/null 2>&1; then
|
|
514
522
|
runner="cargo-test"
|
|
515
|
-
out="$(cd "$tree" &&
|
|
523
|
+
out="$(cd "$tree" && $_vt cargo test 2>&1)" || rc=$?
|
|
516
524
|
else
|
|
517
525
|
_verify_add_gate "tests" "inconclusive" "cargo-test" "rust project detected but cargo not on PATH" "true"
|
|
518
526
|
return 0
|
|
@@ -550,7 +558,13 @@ verify_gate_tests() {
|
|
|
550
558
|
runner="node-test"
|
|
551
559
|
# Pass matched files explicitly (node --test globbing is
|
|
552
560
|
# Node-version-sensitive); quote each so paths with spaces survive.
|
|
553
|
-
|
|
561
|
+
# Force --test-reporter=tap: Node 20+ defaults to the human "spec"
|
|
562
|
+
# reporter (Node 26 emits it even under capture), whose "i pass N" /
|
|
563
|
+
# check-mark lines the count parser + zero-tests detector below do NOT
|
|
564
|
+
# match; they expect TAP "# pass N" / "ok N - ". TAP is stable since
|
|
565
|
+
# Node 18, so forcing it restores a version-independent format with no
|
|
566
|
+
# parser change. Parity with autonomy/run.sh:9662.
|
|
567
|
+
out="$(cd "$tree" && $_vt node --test --test-reporter=tap "${_nt_files[@]}" 2>&1)" || rc=$?
|
|
554
568
|
fi
|
|
555
569
|
fi
|
|
556
570
|
|
|
@@ -564,6 +578,22 @@ verify_gate_tests() {
|
|
|
564
578
|
return 0
|
|
565
579
|
fi
|
|
566
580
|
|
|
581
|
+
# #82 (Python 3.12+): `python3 -m unittest` exits 5 (NOT 0) when it discovers
|
|
582
|
+
# ZERO tests -- e.g. a suite of pytest-style bare `def test_*` functions that
|
|
583
|
+
# unittest (TestCase-only) cannot collect. That non-zero exit would otherwise
|
|
584
|
+
# fall into the fail branch below and record tests=fail -> BLOCKED, when the
|
|
585
|
+
# honest classification is "applicable but ran no real tests" = inconclusive
|
|
586
|
+
# -> CONCERNS. Route the unittest zero-discovery case through the same guard
|
|
587
|
+
# the rc==0 path uses (its unittest arm matches "Ran 0 tests"/"NO TESTS RAN"
|
|
588
|
+
# but was unreachable behind rc==0). Scoped to unittest so pytest/go/cargo/node
|
|
589
|
+
# non-zero exits still correctly record fail.
|
|
590
|
+
if [ "$rc" -eq 5 ] && [ "$runner" = "unittest" ] \
|
|
591
|
+
&& _verify_zero_tests_executed "$runner" "$out" "${_nt_files[@]:-}"; then
|
|
592
|
+
_verify_add_gate "tests" "inconclusive" "$runner" \
|
|
593
|
+
"runner ran but discovered zero tests (source_without_runnable_tests)" "true"
|
|
594
|
+
return 0
|
|
595
|
+
fi
|
|
596
|
+
|
|
567
597
|
if [ "$rc" -eq 0 ]; then
|
|
568
598
|
# #82: a green run that executed ZERO real tests is a mini fake-green.
|
|
569
599
|
# Record it as HONEST inconclusive (forces at-least-CONCERNS), NOT pass
|
package/dashboard/__init__.py
CHANGED
package/dashboard/registry.py
CHANGED
|
@@ -396,6 +396,41 @@ def prune_missing_projects(running_ids: Optional[set] = None) -> list[dict]:
|
|
|
396
396
|
return pruned
|
|
397
397
|
|
|
398
398
|
|
|
399
|
+
def reconcile_stale_runs() -> int:
|
|
400
|
+
"""Self-heal the registry: mark dead-pid in-flight entries as stopped.
|
|
401
|
+
|
|
402
|
+
`loki start` records status="running" for a build, and an exit trap marks it
|
|
403
|
+
stopped on clean shutdown. But a reaped session, a crash, or a SIGKILL skips
|
|
404
|
+
that trap, leaving the entry frozen at "running"/"building" forever with a
|
|
405
|
+
dead pid. Over time these zombies accumulate (a fleet of 130+ "BUILDING"
|
|
406
|
+
entries with no live process), polluting the cockpit and dashboard.
|
|
407
|
+
|
|
408
|
+
This reconciles reality: for every entry whose recorded pid is NOT alive but
|
|
409
|
+
whose status is still in-flight (running/building/active/in_progress), set
|
|
410
|
+
status="stopped". Non-destructive (the entry is kept, just corrected) and
|
|
411
|
+
never touches a live-pid entry or an already-terminal status. Best-effort and
|
|
412
|
+
concurrency-safe (runs under the registry lock with the atomic save).
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
The number of entries reconciled (0 when the registry is already clean).
|
|
416
|
+
"""
|
|
417
|
+
IN_FLIGHT = {"running", "building", "active", "in_progress"}
|
|
418
|
+
reconciled = 0
|
|
419
|
+
with _registry_lock():
|
|
420
|
+
registry = _load_registry()
|
|
421
|
+
projects = registry.get("projects", {})
|
|
422
|
+
changed = False
|
|
423
|
+
for project in projects.values():
|
|
424
|
+
status = str(project.get("status") or "").lower()
|
|
425
|
+
if status in IN_FLIGHT and not _pid_alive(project.get("pid")):
|
|
426
|
+
project["status"] = "stopped"
|
|
427
|
+
reconciled += 1
|
|
428
|
+
changed = True
|
|
429
|
+
if changed:
|
|
430
|
+
_save_registry(registry)
|
|
431
|
+
return reconciled
|
|
432
|
+
|
|
433
|
+
|
|
399
434
|
def check_project_health(identifier: str) -> dict:
|
|
400
435
|
"""
|
|
401
436
|
Check the health status of a project.
|
|
@@ -754,11 +789,17 @@ def get_fleet_runs(include_inactive: bool = True) -> list[dict]:
|
|
|
754
789
|
running = _pid_alive(pid)
|
|
755
790
|
snap = _read_project_run_snapshot(path)
|
|
756
791
|
|
|
757
|
-
# A live pid is authoritative for "running"
|
|
758
|
-
# registry
|
|
759
|
-
#
|
|
792
|
+
# A live pid is authoritative for "running". When the pid is DEAD but the
|
|
793
|
+
# registry still says the run is in-flight (running/building/active), the
|
|
794
|
+
# run was reaped or crashed without its exit trap marking it stopped -- it
|
|
795
|
+
# is a stale zombie, NOT still building. Report it honestly as "stale" so
|
|
796
|
+
# the fleet does not show 100+ frozen "BUILDING" entries. A dead pid with
|
|
797
|
+
# a terminal registry status (stopped/completed/failed) keeps that status.
|
|
798
|
+
reg_status = (p.get("status") or "unknown").lower()
|
|
760
799
|
if running:
|
|
761
800
|
status = "running"
|
|
801
|
+
elif reg_status in ("running", "building", "active", "in_progress"):
|
|
802
|
+
status = "stale"
|
|
762
803
|
else:
|
|
763
804
|
status = p.get("status") or "unknown"
|
|
764
805
|
|