loki-mode 7.73.0 → 7.74.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/completion-council.sh +26 -0
- package/autonomy/lib/voter-agents.sh +43 -2
- package/autonomy/loki +237 -131
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/data/finding-schema.json +1 -0
- package/loki-ts/dist/loki.js +189 -189
- 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.74.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
406
406
|
|
|
407
407
|
---
|
|
408
408
|
|
|
409
|
-
**v7.
|
|
409
|
+
**v7.74.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.74.0
|
|
@@ -2812,8 +2812,34 @@ council_evaluate() {
|
|
|
2812
2812
|
# Re-derive complete count from the round file
|
|
2813
2813
|
local round_file="$COUNCIL_STATE_DIR/votes/round-${ITERATION_COUNT}.json"
|
|
2814
2814
|
local complete_count=0
|
|
2815
|
+
local members_present=0
|
|
2815
2816
|
if [ -f "$round_file" ]; then
|
|
2816
2817
|
complete_count=$(_RF="$round_file" python3 -c "import json, os; print(json.load(open(os.environ['_RF'])).get('complete_votes', 0))" 2>/dev/null || echo "0")
|
|
2818
|
+
# WAVE13 CRITICAL quorum gate: how many voters actually responded
|
|
2819
|
+
# (total_members records the ACTUAL returned count -- see
|
|
2820
|
+
# voter-agents.sh). A degraded/partial dispatch response must never
|
|
2821
|
+
# be honored as COMPLETE even if its (now quorum-aware) verdict
|
|
2822
|
+
# somehow read COMPLETE. This is defense-in-depth: the parser
|
|
2823
|
+
# already forces CONTINUE on undercount, but the completion-detection
|
|
2824
|
+
# trust core must independently assert full quorum before stopping.
|
|
2825
|
+
members_present=$(_RF="$round_file" python3 -c "import json, os; print(json.load(open(os.environ['_RF'])).get('total_members', 0))" 2>/dev/null || echo "0")
|
|
2826
|
+
fi
|
|
2827
|
+
# Normalize to integers (guard against empty/non-numeric on read failure)
|
|
2828
|
+
case "$complete_count" in (''|*[!0-9]*) complete_count=0 ;; esac
|
|
2829
|
+
case "$members_present" in (''|*[!0-9]*) members_present=0 ;; esac
|
|
2830
|
+
|
|
2831
|
+
# Quorum-presence gate (distinct from the DA-unanimity trigger below):
|
|
2832
|
+
# a COMPLETE verdict only stands when EXACTLY the expected council
|
|
2833
|
+
# responded. Any mismatch is a degraded response and fails closed:
|
|
2834
|
+
# - undercount (< COUNCIL_SIZE): missing voters are non-approval.
|
|
2835
|
+
# - overcount (> COUNCIL_SIZE): extra/unprompted findings (e.g. a
|
|
2836
|
+
# model adding a 4th 'devils-advocate' finding) would otherwise let
|
|
2837
|
+
# a low-approval-ratio response clear the fixed threshold=2. Both
|
|
2838
|
+
# directions must CONTINUE, so we assert exact quorum (== not <).
|
|
2839
|
+
if [ "$members_present" -ne "$COUNCIL_SIZE" ]; then
|
|
2840
|
+
log_warn "Council evaluate: COMPLETE rejected -- quorum mismatch ($members_present voters present, expected $COUNCIL_SIZE); failing closed to CONTINUE"
|
|
2841
|
+
council_write_transcript "${ITERATION_COUNT:-0}" "REJECTED" "false" "false" "$_eval_threshold"
|
|
2842
|
+
return 1 # CONTINUE
|
|
2817
2843
|
fi
|
|
2818
2844
|
|
|
2819
2845
|
if [ "$complete_count" -eq "$COUNCIL_SIZE" ] && [ "$COUNCIL_SIZE" -ge 2 ]; then
|
|
@@ -273,6 +273,7 @@ loki_council_dispatch_agents() {
|
|
|
273
273
|
_VA_ITER="$iteration" \
|
|
274
274
|
_VA_VDIR="$verdicts_dir" \
|
|
275
275
|
_VA_RFILE="$votes_dir/round-${iteration}.json" \
|
|
276
|
+
_VA_EXPECTED="${COUNCIL_SIZE:-3}" \
|
|
276
277
|
python3 -c '
|
|
277
278
|
import json, os, sys
|
|
278
279
|
from datetime import datetime, timezone
|
|
@@ -290,6 +291,26 @@ it = int(os.environ.get("_VA_ITER", "0") or 0)
|
|
|
290
291
|
vdir = os.environ["_VA_VDIR"]
|
|
291
292
|
rfile = os.environ["_VA_RFILE"]
|
|
292
293
|
|
|
294
|
+
# WAVE13 CRITICAL quorum fix: the quorum denominator MUST be the EXPECTED
|
|
295
|
+
# council size (COUNCIL_SIZE), never the number of findings the model happened
|
|
296
|
+
# to return. Pre-fix this parser computed threshold = (returned*2+2)//3, so a
|
|
297
|
+
# degraded response with a single APPROVE finding (returned=1) yielded
|
|
298
|
+
# threshold=1 and a COMPLETE verdict from a SINGLE voter, with the missing
|
|
299
|
+
# voters silently dropped. That fails OPEN on the completion-detection trust
|
|
300
|
+
# core. We now fail CLOSED: any undercount (returned < expected) forces a
|
|
301
|
+
# CONTINUE verdict so a partial/degraded model response can never reach
|
|
302
|
+
# COMPLETE on the returned subset. Design choice (Option 2): compute the
|
|
303
|
+
# verdict in-path (rather than sys.exit -> heuristic fallback) so the round
|
|
304
|
+
# file always records the actual returned count in total_members, making the
|
|
305
|
+
# downstream quorum assertion in completion-council.sh meaningful and locally
|
|
306
|
+
# testable without depending on the heuristic-path disk-state behavior.
|
|
307
|
+
try:
|
|
308
|
+
expected_count = int(os.environ.get("_VA_EXPECTED", "3") or 3)
|
|
309
|
+
except (TypeError, ValueError):
|
|
310
|
+
expected_count = 3
|
|
311
|
+
if expected_count < 1:
|
|
312
|
+
expected_count = 1
|
|
313
|
+
|
|
293
314
|
def to_legacy(vote: str) -> str:
|
|
294
315
|
v = (vote or "").upper()
|
|
295
316
|
if v == "APPROVE":
|
|
@@ -338,14 +359,34 @@ for idx, f in enumerate(findings, start=1):
|
|
|
338
359
|
if total == 0:
|
|
339
360
|
sys.exit(5)
|
|
340
361
|
|
|
341
|
-
threshold
|
|
342
|
-
|
|
362
|
+
# Quorum-aware threshold (WAVE13). threshold is computed against the EXPECTED
|
|
363
|
+
# council size so it can never shrink to 1 on a degraded response. Absent
|
|
364
|
+
# voters (total < expected) are treated as non-approval: the round is forced
|
|
365
|
+
# to CONTINUE and can never reach COMPLETE on the returned subset. With
|
|
366
|
+
# expected=3, threshold = ceil(2/3 * 3) = 2, so 1-of-3 (or any single voter)
|
|
367
|
+
# is structurally incapable of producing COMPLETE.
|
|
368
|
+
threshold = (expected_count * 2 + 2) // 3
|
|
369
|
+
if total != expected_count:
|
|
370
|
+
# Fail closed on ANY quorum mismatch:
|
|
371
|
+
# - undercount (total < expected): missing voters count as non-approval.
|
|
372
|
+
# - overcount (total > expected): extra/unprompted findings (e.g. a model
|
|
373
|
+
# adding a 4th finding) would otherwise let a low-approval-ratio response
|
|
374
|
+
# clear the fixed threshold. A degraded response in either direction must
|
|
375
|
+
# never reach COMPLETE on the returned subset.
|
|
376
|
+
verdict = "CONTINUE"
|
|
377
|
+
else:
|
|
378
|
+
verdict = "COMPLETE" if complete >= threshold else "CONTINUE"
|
|
343
379
|
round_data = {
|
|
344
380
|
"round": it,
|
|
345
381
|
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
346
382
|
"complete_votes": complete,
|
|
347
383
|
"continue_votes": total - complete,
|
|
384
|
+
# total_members records the ACTUAL number of voters that responded (not the
|
|
385
|
+
# expected size) so the completion-council quorum assertion can detect an
|
|
386
|
+
# undercount. expected_members records the size the verdict was judged
|
|
387
|
+
# against.
|
|
348
388
|
"total_members": total,
|
|
389
|
+
"expected_members": expected_count,
|
|
349
390
|
"threshold": threshold,
|
|
350
391
|
"verdict": verdict,
|
|
351
392
|
"votes": votes,
|
package/autonomy/loki
CHANGED
|
@@ -7396,10 +7396,10 @@ cmd_watch() {
|
|
|
7396
7396
|
return 0
|
|
7397
7397
|
;;
|
|
7398
7398
|
--once) run_once=true; shift ;;
|
|
7399
|
-
--interval) poll_interval="${2:-2}"; shift 2 ;;
|
|
7399
|
+
--interval) poll_interval="${2:-2}"; [ $# -ge 2 ] && shift 2 || shift ;;
|
|
7400
7400
|
--interval=*) poll_interval="${1#*=}"; shift ;;
|
|
7401
7401
|
--no-auto-start) no_auto_start=true; shift ;;
|
|
7402
|
-
--debounce) debounce="${2:-3}"; shift 2 ;;
|
|
7402
|
+
--debounce) debounce="${2:-3}"; [ $# -ge 2 ] && shift 2 || shift ;;
|
|
7403
7403
|
--debounce=*) debounce="${1#*=}"; shift ;;
|
|
7404
7404
|
-*)
|
|
7405
7405
|
echo -e "${RED}Unknown option: $1${NC}"
|
|
@@ -19877,11 +19877,11 @@ else:
|
|
|
19877
19877
|
case "$1" in
|
|
19878
19878
|
--name|-n)
|
|
19879
19879
|
name="${2:-}"
|
|
19880
|
-
shift 2
|
|
19880
|
+
if [ $# -ge 2 ]; then shift 2; else shift; fi
|
|
19881
19881
|
;;
|
|
19882
19882
|
--alias|-a)
|
|
19883
19883
|
alias="${2:-}"
|
|
19884
|
-
shift 2
|
|
19884
|
+
if [ $# -ge 2 ]; then shift 2; else shift; fi
|
|
19885
19885
|
;;
|
|
19886
19886
|
*)
|
|
19887
19887
|
shift
|
|
@@ -19897,10 +19897,21 @@ else:
|
|
|
19897
19897
|
exit 1
|
|
19898
19898
|
fi
|
|
19899
19899
|
|
|
19900
|
-
|
|
19900
|
+
# Route the read-modify-write through dashboard/registry.py so the
|
|
19901
|
+
# mutation is serialized under its fcntl lock and written atomically
|
|
19902
|
+
# (tempfile + os.replace), matching the v7.45.1 hardening that
|
|
19903
|
+
# run.sh registration and mark_project_stopped already use. The
|
|
19904
|
+
# inline open(...,'w') path here was unlocked and truncating, which
|
|
19905
|
+
# could lose a concurrent writer's entry or expose a 0-byte file to
|
|
19906
|
+
# a reader. Falls back to an atomic temp-file write if registry.py
|
|
19907
|
+
# cannot be imported at runtime (never a bare truncating open).
|
|
19908
|
+
_REGISTRY_FILE="$registry_file" _PROJ_PATH="$path" _PROJ_NAME="$name" _PROJ_ALIAS="$alias" \
|
|
19909
|
+
_REGISTRY_SKILL="${SKILL_DIR:-$_LOKI_SCRIPT_DIR/..}" python3 -c "
|
|
19901
19910
|
import json
|
|
19902
19911
|
import os
|
|
19912
|
+
import sys
|
|
19903
19913
|
import hashlib
|
|
19914
|
+
import tempfile
|
|
19904
19915
|
from datetime import datetime, timezone
|
|
19905
19916
|
|
|
19906
19917
|
registry_file = os.environ['_REGISTRY_FILE']
|
|
@@ -19908,41 +19919,62 @@ path = os.environ['_PROJ_PATH']
|
|
|
19908
19919
|
name = os.environ['_PROJ_NAME'] or os.path.basename(path)
|
|
19909
19920
|
alias = os.environ['_PROJ_ALIAS'] or None
|
|
19910
19921
|
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
data = json.load(f)
|
|
19922
|
+
sys.path.insert(0, os.environ.get('_REGISTRY_SKILL', '.'))
|
|
19923
|
+
try:
|
|
19924
|
+
from dashboard import registry
|
|
19925
|
+
except Exception:
|
|
19926
|
+
registry = None
|
|
19917
19927
|
|
|
19918
|
-
|
|
19919
|
-
|
|
19920
|
-
|
|
19921
|
-
if
|
|
19922
|
-
# Update existing
|
|
19923
|
-
projects[project_id]['name'] = name
|
|
19924
|
-
if alias:
|
|
19925
|
-
projects[project_id]['alias'] = alias
|
|
19926
|
-
projects[project_id]['updated_at'] = now
|
|
19927
|
-
print(f'Updated: {name}')
|
|
19928
|
+
if registry is not None:
|
|
19929
|
+
existing = registry.get_project(path)
|
|
19930
|
+
project = registry.register_project(path, name, alias)
|
|
19931
|
+
print('Updated: ' + name if existing else 'Registered: ' + name)
|
|
19928
19932
|
else:
|
|
19929
|
-
#
|
|
19930
|
-
|
|
19931
|
-
|
|
19932
|
-
|
|
19933
|
-
'
|
|
19934
|
-
|
|
19935
|
-
|
|
19936
|
-
'
|
|
19937
|
-
|
|
19938
|
-
|
|
19939
|
-
|
|
19940
|
-
|
|
19941
|
-
|
|
19942
|
-
|
|
19943
|
-
|
|
19944
|
-
|
|
19945
|
-
|
|
19933
|
+
# Fallback: atomic temp-file write (no truncating open) if registry.py
|
|
19934
|
+
# is unavailable. Mirrors register_project semantics (sha256 id).
|
|
19935
|
+
project_id = hashlib.sha256(path.encode()).hexdigest()[:12]
|
|
19936
|
+
try:
|
|
19937
|
+
with open(registry_file, 'r') as f:
|
|
19938
|
+
data = json.load(f)
|
|
19939
|
+
except (OSError, json.JSONDecodeError):
|
|
19940
|
+
data = {'version': '1.0', 'projects': {}}
|
|
19941
|
+
projects = data.get('projects', {})
|
|
19942
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
19943
|
+
if project_id in projects:
|
|
19944
|
+
projects[project_id]['name'] = name
|
|
19945
|
+
if alias:
|
|
19946
|
+
projects[project_id]['alias'] = alias
|
|
19947
|
+
projects[project_id]['updated_at'] = now
|
|
19948
|
+
print(f'Updated: {name}')
|
|
19949
|
+
else:
|
|
19950
|
+
projects[project_id] = {
|
|
19951
|
+
'id': project_id,
|
|
19952
|
+
'path': path,
|
|
19953
|
+
'name': name,
|
|
19954
|
+
'alias': alias,
|
|
19955
|
+
'registered_at': now,
|
|
19956
|
+
'updated_at': now,
|
|
19957
|
+
'last_accessed': None,
|
|
19958
|
+
'has_loki_dir': os.path.isdir(os.path.join(path, '.loki')),
|
|
19959
|
+
'status': 'active',
|
|
19960
|
+
}
|
|
19961
|
+
print(f'Registered: {name}')
|
|
19962
|
+
data['projects'] = projects
|
|
19963
|
+
reg_dir = os.path.dirname(registry_file) or '.'
|
|
19964
|
+
os.makedirs(reg_dir, exist_ok=True)
|
|
19965
|
+
fd, tmp = tempfile.mkstemp(dir=reg_dir, prefix='.projects.', suffix='.tmp')
|
|
19966
|
+
try:
|
|
19967
|
+
with os.fdopen(fd, 'w') as f:
|
|
19968
|
+
json.dump(data, f, indent=2)
|
|
19969
|
+
f.flush()
|
|
19970
|
+
os.fsync(f.fileno())
|
|
19971
|
+
os.replace(tmp, registry_file)
|
|
19972
|
+
except BaseException:
|
|
19973
|
+
try:
|
|
19974
|
+
os.unlink(tmp)
|
|
19975
|
+
except OSError:
|
|
19976
|
+
pass
|
|
19977
|
+
raise
|
|
19946
19978
|
|
|
19947
19979
|
print(f' Path: {path}')
|
|
19948
19980
|
if alias:
|
|
@@ -19958,34 +19990,69 @@ if alias:
|
|
|
19958
19990
|
exit 1
|
|
19959
19991
|
fi
|
|
19960
19992
|
|
|
19961
|
-
|
|
19993
|
+
# Route through registry.unregister_project so the delete is
|
|
19994
|
+
# serialized + atomic. Look the name up first to preserve the
|
|
19995
|
+
# "Removed: {name}" output. Falls back to an atomic temp-file write
|
|
19996
|
+
# if registry.py cannot be imported (never a truncating open).
|
|
19997
|
+
_REGISTRY_FILE="$registry_file" _IDENTIFIER="$identifier" \
|
|
19998
|
+
_REGISTRY_SKILL="${SKILL_DIR:-$_LOKI_SCRIPT_DIR/..}" python3 -c "
|
|
19962
19999
|
import json
|
|
19963
20000
|
import os
|
|
20001
|
+
import sys
|
|
20002
|
+
import tempfile
|
|
19964
20003
|
|
|
19965
20004
|
registry_file = os.environ['_REGISTRY_FILE']
|
|
19966
20005
|
identifier = os.environ['_IDENTIFIER']
|
|
19967
20006
|
|
|
19968
|
-
|
|
19969
|
-
|
|
19970
|
-
|
|
19971
|
-
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
for pid, project in projects.items():
|
|
19975
|
-
if pid == identifier or project['path'] == identifier or project.get('alias') == identifier:
|
|
19976
|
-
found_id = pid
|
|
19977
|
-
break
|
|
20007
|
+
sys.path.insert(0, os.environ.get('_REGISTRY_SKILL', '.'))
|
|
20008
|
+
try:
|
|
20009
|
+
from dashboard import registry
|
|
20010
|
+
except Exception:
|
|
20011
|
+
registry = None
|
|
19978
20012
|
|
|
19979
|
-
if
|
|
19980
|
-
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
|
|
20013
|
+
if registry is not None:
|
|
20014
|
+
existing = registry.get_project(identifier)
|
|
20015
|
+
if existing and registry.unregister_project(identifier):
|
|
20016
|
+
print('Removed: ' + existing['name'])
|
|
20017
|
+
else:
|
|
20018
|
+
print(f'Not found: {identifier}')
|
|
20019
|
+
sys.exit(1)
|
|
19986
20020
|
else:
|
|
19987
|
-
|
|
19988
|
-
|
|
20021
|
+
# Fallback: atomic temp-file write if registry.py is unavailable.
|
|
20022
|
+
try:
|
|
20023
|
+
with open(registry_file, 'r') as f:
|
|
20024
|
+
data = json.load(f)
|
|
20025
|
+
except (OSError, json.JSONDecodeError):
|
|
20026
|
+
data = {'version': '1.0', 'projects': {}}
|
|
20027
|
+
projects = data.get('projects', {})
|
|
20028
|
+
found_id = None
|
|
20029
|
+
for pid, project in projects.items():
|
|
20030
|
+
if pid == identifier or project['path'] == identifier or project.get('alias') == identifier:
|
|
20031
|
+
found_id = pid
|
|
20032
|
+
break
|
|
20033
|
+
if found_id:
|
|
20034
|
+
name = projects[found_id]['name']
|
|
20035
|
+
del projects[found_id]
|
|
20036
|
+
data['projects'] = projects
|
|
20037
|
+
reg_dir = os.path.dirname(registry_file) or '.'
|
|
20038
|
+
os.makedirs(reg_dir, exist_ok=True)
|
|
20039
|
+
fd, tmp = tempfile.mkstemp(dir=reg_dir, prefix='.projects.', suffix='.tmp')
|
|
20040
|
+
try:
|
|
20041
|
+
with os.fdopen(fd, 'w') as f:
|
|
20042
|
+
json.dump(data, f, indent=2)
|
|
20043
|
+
f.flush()
|
|
20044
|
+
os.fsync(f.fileno())
|
|
20045
|
+
os.replace(tmp, registry_file)
|
|
20046
|
+
except BaseException:
|
|
20047
|
+
try:
|
|
20048
|
+
os.unlink(tmp)
|
|
20049
|
+
except OSError:
|
|
20050
|
+
pass
|
|
20051
|
+
raise
|
|
20052
|
+
print(f'Removed: {name}')
|
|
20053
|
+
else:
|
|
20054
|
+
print(f'Not found: {identifier}')
|
|
20055
|
+
sys.exit(1)
|
|
19989
20056
|
"
|
|
19990
20057
|
;;
|
|
19991
20058
|
|
|
@@ -20051,87 +20118,119 @@ else:
|
|
|
20051
20118
|
echo -e "${BOLD}Syncing Project Registry${NC}"
|
|
20052
20119
|
echo ""
|
|
20053
20120
|
|
|
20054
|
-
|
|
20121
|
+
# Route through registry.sync_registry_with_discovery so the
|
|
20122
|
+
# discover->merge->save is serialized + atomic and uses the same
|
|
20123
|
+
# sha256 ids as the rest of the registry. Reproduce the prior
|
|
20124
|
+
# stdout (per-project "Added:" lines + summary) from its return
|
|
20125
|
+
# value. Falls back to an atomic temp-file write if registry.py
|
|
20126
|
+
# cannot be imported (never a truncating open).
|
|
20127
|
+
_REGISTRY_FILE="$registry_file" \
|
|
20128
|
+
_REGISTRY_SKILL="${SKILL_DIR:-$_LOKI_SCRIPT_DIR/..}" python3 -c "
|
|
20055
20129
|
import json
|
|
20056
20130
|
import os
|
|
20131
|
+
import sys
|
|
20057
20132
|
import hashlib
|
|
20133
|
+
import tempfile
|
|
20058
20134
|
from pathlib import Path
|
|
20059
20135
|
from datetime import datetime, timezone
|
|
20060
20136
|
|
|
20061
|
-
registry_file = '
|
|
20062
|
-
home = Path.home()
|
|
20063
|
-
|
|
20064
|
-
# Load registry
|
|
20065
|
-
with open(registry_file, 'r') as f:
|
|
20066
|
-
data = json.load(f)
|
|
20067
|
-
projects = data.get('projects', {})
|
|
20068
|
-
|
|
20069
|
-
# Discover projects
|
|
20070
|
-
search_paths = [
|
|
20071
|
-
home / 'git',
|
|
20072
|
-
home / 'projects',
|
|
20073
|
-
home / 'code',
|
|
20074
|
-
home / 'dev',
|
|
20075
|
-
home / 'workspace',
|
|
20076
|
-
home / 'src',
|
|
20077
|
-
]
|
|
20137
|
+
registry_file = os.environ['_REGISTRY_FILE']
|
|
20078
20138
|
|
|
20079
|
-
|
|
20139
|
+
sys.path.insert(0, os.environ.get('_REGISTRY_SKILL', '.'))
|
|
20140
|
+
try:
|
|
20141
|
+
from dashboard import registry
|
|
20142
|
+
except Exception:
|
|
20143
|
+
registry = None
|
|
20080
20144
|
|
|
20081
|
-
|
|
20082
|
-
|
|
20083
|
-
|
|
20145
|
+
if registry is not None:
|
|
20146
|
+
result = registry.sync_registry_with_discovery()
|
|
20147
|
+
for project in result['details']['added']:
|
|
20148
|
+
print(f\" Added: {project['name']}\")
|
|
20149
|
+
print('')
|
|
20150
|
+
print(f\"Added: {result['added']}, Missing: {result['missing']}, Total: {result['total']}\")
|
|
20151
|
+
else:
|
|
20152
|
+
# Fallback: atomic temp-file write if registry.py is unavailable.
|
|
20153
|
+
home = Path.home()
|
|
20084
20154
|
try:
|
|
20085
|
-
|
|
20086
|
-
|
|
20087
|
-
|
|
20088
|
-
|
|
20089
|
-
|
|
20090
|
-
|
|
20091
|
-
|
|
20092
|
-
|
|
20093
|
-
|
|
20094
|
-
|
|
20095
|
-
|
|
20096
|
-
|
|
20097
|
-
|
|
20098
|
-
|
|
20099
|
-
search_dir(search_path)
|
|
20155
|
+
with open(registry_file, 'r') as f:
|
|
20156
|
+
data = json.load(f)
|
|
20157
|
+
except (OSError, json.JSONDecodeError):
|
|
20158
|
+
data = {'version': '1.0', 'projects': {}}
|
|
20159
|
+
projects = data.get('projects', {})
|
|
20160
|
+
search_paths = [
|
|
20161
|
+
home / 'git',
|
|
20162
|
+
home / 'projects',
|
|
20163
|
+
home / 'code',
|
|
20164
|
+
home / 'dev',
|
|
20165
|
+
home / 'workspace',
|
|
20166
|
+
home / 'src',
|
|
20167
|
+
]
|
|
20168
|
+
discovered = []
|
|
20100
20169
|
|
|
20101
|
-
|
|
20102
|
-
|
|
20103
|
-
|
|
20170
|
+
def search_dir(path, depth=0, max_depth=3):
|
|
20171
|
+
if depth > max_depth:
|
|
20172
|
+
return
|
|
20173
|
+
try:
|
|
20174
|
+
if not path.is_dir():
|
|
20175
|
+
return
|
|
20176
|
+
loki_dir = path / '.loki'
|
|
20177
|
+
if loki_dir.is_dir():
|
|
20178
|
+
discovered.append(str(path))
|
|
20179
|
+
return
|
|
20180
|
+
for child in path.iterdir():
|
|
20181
|
+
if child.is_dir() and not child.name.startswith('.'):
|
|
20182
|
+
search_dir(child, depth + 1, max_depth)
|
|
20183
|
+
except (PermissionError, OSError):
|
|
20184
|
+
pass
|
|
20104
20185
|
|
|
20105
|
-
for
|
|
20106
|
-
|
|
20107
|
-
|
|
20108
|
-
|
|
20109
|
-
|
|
20110
|
-
|
|
20111
|
-
|
|
20112
|
-
|
|
20113
|
-
|
|
20114
|
-
|
|
20115
|
-
|
|
20116
|
-
|
|
20117
|
-
|
|
20118
|
-
|
|
20119
|
-
|
|
20120
|
-
|
|
20186
|
+
for search_path in search_paths:
|
|
20187
|
+
if search_path.exists():
|
|
20188
|
+
search_dir(search_path)
|
|
20189
|
+
|
|
20190
|
+
added = 0
|
|
20191
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
20192
|
+
for path in discovered:
|
|
20193
|
+
project_id = hashlib.sha256(path.encode()).hexdigest()[:12]
|
|
20194
|
+
if project_id not in projects:
|
|
20195
|
+
projects[project_id] = {
|
|
20196
|
+
'id': project_id,
|
|
20197
|
+
'path': path,
|
|
20198
|
+
'name': os.path.basename(path),
|
|
20199
|
+
'alias': None,
|
|
20200
|
+
'registered_at': now,
|
|
20201
|
+
'updated_at': now,
|
|
20202
|
+
'last_accessed': None,
|
|
20203
|
+
'has_loki_dir': True,
|
|
20204
|
+
'status': 'active',
|
|
20205
|
+
}
|
|
20206
|
+
added += 1
|
|
20207
|
+
print(f' Added: {os.path.basename(path)}')
|
|
20121
20208
|
|
|
20122
|
-
|
|
20123
|
-
|
|
20124
|
-
|
|
20125
|
-
|
|
20126
|
-
|
|
20127
|
-
missing += 1
|
|
20209
|
+
missing = 0
|
|
20210
|
+
for pid, project in list(projects.items()):
|
|
20211
|
+
if not os.path.isdir(project['path']):
|
|
20212
|
+
project['status'] = 'missing'
|
|
20213
|
+
missing += 1
|
|
20128
20214
|
|
|
20129
|
-
data['projects'] = projects
|
|
20130
|
-
|
|
20131
|
-
|
|
20215
|
+
data['projects'] = projects
|
|
20216
|
+
reg_dir = os.path.dirname(registry_file) or '.'
|
|
20217
|
+
os.makedirs(reg_dir, exist_ok=True)
|
|
20218
|
+
fd, tmp = tempfile.mkstemp(dir=reg_dir, prefix='.projects.', suffix='.tmp')
|
|
20219
|
+
try:
|
|
20220
|
+
with os.fdopen(fd, 'w') as f:
|
|
20221
|
+
json.dump(data, f, indent=2)
|
|
20222
|
+
f.flush()
|
|
20223
|
+
os.fsync(f.fileno())
|
|
20224
|
+
os.replace(tmp, registry_file)
|
|
20225
|
+
except BaseException:
|
|
20226
|
+
try:
|
|
20227
|
+
os.unlink(tmp)
|
|
20228
|
+
except OSError:
|
|
20229
|
+
pass
|
|
20230
|
+
raise
|
|
20132
20231
|
|
|
20133
|
-
print('')
|
|
20134
|
-
print(f'Added: {added}, Missing: {missing}, Total: {len(projects)}')
|
|
20232
|
+
print('')
|
|
20233
|
+
print(f'Added: {added}, Missing: {missing}, Total: {len(projects)}')
|
|
20135
20234
|
" 2>/dev/null
|
|
20136
20235
|
;;
|
|
20137
20236
|
|
|
@@ -22546,11 +22645,18 @@ _context_add() {
|
|
|
22546
22645
|
local loki_dir="${LOKI_DIR:-.loki}"
|
|
22547
22646
|
mkdir -p "$loki_dir/state"
|
|
22548
22647
|
local ctx_files="$loki_dir/state/context-files.json"
|
|
22549
|
-
|
|
22648
|
+
# Pass values via env vars (not string interpolation) so a filename
|
|
22649
|
+
# with an apostrophe or other special char cannot break the python
|
|
22650
|
+
# source (was a swallowed SyntaxError -> set -e abort -> file silently
|
|
22651
|
+
# not added). Matches the safe pattern used by the registry commands.
|
|
22652
|
+
_CTX_PATH="$file_path" _CTX_TOKENS="$est_tokens" _CTX_SIZE="$size" \
|
|
22653
|
+
_CTX_LINES="$lines" _CTX_FILE="$ctx_files" python3 -c "
|
|
22550
22654
|
import json, os
|
|
22551
|
-
path = '
|
|
22552
|
-
tokens =
|
|
22553
|
-
ctx_file = '
|
|
22655
|
+
path = os.environ['_CTX_PATH']
|
|
22656
|
+
tokens = int(os.environ['_CTX_TOKENS'])
|
|
22657
|
+
ctx_file = os.environ['_CTX_FILE']
|
|
22658
|
+
size = int(os.environ['_CTX_SIZE'])
|
|
22659
|
+
lines = int(os.environ['_CTX_LINES'])
|
|
22554
22660
|
try:
|
|
22555
22661
|
with open(ctx_file) as f:
|
|
22556
22662
|
files = json.load(f)
|
|
@@ -22558,7 +22664,7 @@ except:
|
|
|
22558
22664
|
files = []
|
|
22559
22665
|
# Avoid duplicates
|
|
22560
22666
|
files = [f for f in files if f.get('path') != path]
|
|
22561
|
-
files.append({'path': path, 'estimated_tokens': tokens, 'size':
|
|
22667
|
+
files.append({'path': path, 'estimated_tokens': tokens, 'size': size, 'lines': lines})
|
|
22562
22668
|
with open(ctx_file, 'w') as f:
|
|
22563
22669
|
json.dump(files, f, indent=2)
|
|
22564
22670
|
" 2>/dev/null
|
|
@@ -29753,7 +29859,7 @@ cmd_docker() {
|
|
|
29753
29859
|
local -a fwd=()
|
|
29754
29860
|
while [ $# -gt 0 ]; do
|
|
29755
29861
|
case "$1" in
|
|
29756
|
-
--image) shift; export LOKI_DOCKER_IMAGE="$
|
|
29862
|
+
--image) [ $# -ge 2 ] && { shift; export LOKI_DOCKER_IMAGE="$1"; } || { echo "loki docker --image requires a value" >&2; return 1; }; shift ;;
|
|
29757
29863
|
--dry-run) dry_run=1; shift ;;
|
|
29758
29864
|
--api) with_api=1; fwd+=("$1"); shift ;;
|
|
29759
29865
|
*) fwd+=("$1"); shift ;;
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.74.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
395
395
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
396
396
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
397
397
|
-v $(pwd):/workspace -w /workspace \
|
|
398
|
-
asklokesh/loki-mode:7.
|
|
398
|
+
asklokesh/loki-mode:7.74.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"findings": {
|
|
11
11
|
"type": "array",
|
|
12
12
|
"minItems": 1,
|
|
13
|
+
"description": "One finding per dispatched council voter. minItems is intentionally kept at 1 because this schema is SHARED with non-council consumers (loki-ts council.ts validateFinding and loki_finding_schema_path). It is NOT raised to the council size, since doing so would break those consumers. The real quorum gate lives in autonomy/lib/voter-agents.sh: the embedded parser judges the verdict against the EXPECTED council size (COUNCIL_SIZE) and forces CONTINUE when fewer voters respond than expected (fail closed). completion-council.sh additionally asserts full quorum (total_members == COUNCIL_SIZE) before honoring a COMPLETE verdict. WAVE13: a degraded response that returns fewer findings than the council size can NEVER produce COMPLETE.",
|
|
13
14
|
"items": {
|
|
14
15
|
"type": "object",
|
|
15
16
|
"additionalProperties": false,
|