loki-mode 7.5.15 → 7.5.17
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 +174 -0
- package/autonomy/loki +11 -4
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +87 -0
- package/dashboard/static/index.html +305 -134
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +3 -3
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.5.
|
|
6
|
+
# Loki Mode v7.5.17
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
381
381
|
|
|
382
382
|
---
|
|
383
383
|
|
|
384
|
-
**v7.5.
|
|
384
|
+
**v7.5.17 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.5.
|
|
1
|
+
7.5.17
|
|
@@ -470,12 +470,174 @@ with open(state_file, 'w') as f:
|
|
|
470
470
|
"threshold=$effective_threshold" \
|
|
471
471
|
"result=$([ $approve_count -ge $effective_threshold ] && echo 'APPROVED' || echo 'REJECTED')" 2>/dev/null || true
|
|
472
472
|
|
|
473
|
+
# Write transcript for this council round (Path A: council_vote path)
|
|
474
|
+
local _ct_outcome
|
|
475
|
+
_ct_outcome=$([ $approve_count -ge $effective_threshold ] && echo "APPROVED" || echo "REJECTED")
|
|
476
|
+
local _ct_triggered="false"
|
|
477
|
+
local _ct_flipped="false"
|
|
478
|
+
if [ $approve_count -eq $COUNCIL_SIZE ] && [ $COUNCIL_SIZE -ge 2 ]; then
|
|
479
|
+
_ct_triggered="true"
|
|
480
|
+
fi
|
|
481
|
+
# contrarian_flipped: DA voted REJECT/CANNOT_VALIDATE causing approve_count drop
|
|
482
|
+
# Detect by checking if approve dropped from unanimous (COUNCIL_SIZE) to less
|
|
483
|
+
# We infer flip if triggered AND final approve < COUNCIL_SIZE
|
|
484
|
+
if [ "$_ct_triggered" = "true" ] && [ $approve_count -lt $COUNCIL_SIZE ]; then
|
|
485
|
+
_ct_flipped="true"
|
|
486
|
+
fi
|
|
487
|
+
council_write_transcript "${ITERATION_COUNT:-0}" "$_ct_outcome" "$_ct_triggered" "$_ct_flipped" "$effective_threshold"
|
|
488
|
+
|
|
473
489
|
if [ $approve_count -ge $effective_threshold ]; then
|
|
474
490
|
return 0 # Council says DONE
|
|
475
491
|
fi
|
|
476
492
|
return 1 # Council says CONTINUE
|
|
477
493
|
}
|
|
478
494
|
|
|
495
|
+
#===============================================================================
|
|
496
|
+
# Council Transcript Writer - persists per-iteration council round as JSON
|
|
497
|
+
#
|
|
498
|
+
# Arguments:
|
|
499
|
+
# $1 - iteration number
|
|
500
|
+
# $2 - outcome: APPROVED | REJECTED | BLOCKED_BY_GATE
|
|
501
|
+
# $3 - contrarian_triggered: true | false
|
|
502
|
+
# $4 - contrarian_flipped: true | false
|
|
503
|
+
# $5 - effective_threshold: votes needed for approval (0 = unknown/sentinel)
|
|
504
|
+
#
|
|
505
|
+
# Output: .loki/council/transcripts/iter-<N>-<TIMESTAMP>.json
|
|
506
|
+
#===============================================================================
|
|
507
|
+
|
|
508
|
+
council_write_transcript() {
|
|
509
|
+
local iteration="${1:-${ITERATION_COUNT:-0}}"
|
|
510
|
+
local outcome="${2:-REJECTED}"
|
|
511
|
+
local contrarian_triggered="${3:-false}"
|
|
512
|
+
local contrarian_flipped="${4:-false}"
|
|
513
|
+
local effective_threshold="${5:-0}"
|
|
514
|
+
local timestamp
|
|
515
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
516
|
+
# Remove colons and hyphens from timestamp for filename safety
|
|
517
|
+
local ts_safe="${timestamp//[:\-]/}"
|
|
518
|
+
local iteration_id="iter-${iteration}-${ts_safe}"
|
|
519
|
+
local transcript_dir="$COUNCIL_STATE_DIR/transcripts"
|
|
520
|
+
mkdir -p "$transcript_dir"
|
|
521
|
+
local transcript_file="$transcript_dir/${iteration_id}.json"
|
|
522
|
+
|
|
523
|
+
# Read prd preview from state or prd file
|
|
524
|
+
local task_or_prd=""
|
|
525
|
+
if [ -n "$COUNCIL_PRD_PATH" ] && [ -f "$COUNCIL_PRD_PATH" ]; then
|
|
526
|
+
task_or_prd=$(head -5 "$COUNCIL_PRD_PATH" | tr '\n' ' ' | cut -c1-200)
|
|
527
|
+
fi
|
|
528
|
+
|
|
529
|
+
local round_file="$COUNCIL_STATE_DIR/votes/round-${iteration}.json"
|
|
530
|
+
local da_file="$COUNCIL_STATE_DIR/votes/devils-advocate-round-${iteration}.json"
|
|
531
|
+
|
|
532
|
+
_IT="$iteration" _TS="$timestamp" _IID="$iteration_id" \
|
|
533
|
+
_OUTCOME="$outcome" _CT="$contrarian_triggered" _CF="$contrarian_flipped" \
|
|
534
|
+
_TASK="$task_or_prd" _PRD="${COUNCIL_PRD_PATH:-}" \
|
|
535
|
+
_ROUND_FILE="${round_file}" _DA_FILE="${da_file}" \
|
|
536
|
+
_MEMBERS_DIR="$COUNCIL_STATE_DIR/votes/iteration-${iteration}" \
|
|
537
|
+
_THRESHOLD="$effective_threshold" \
|
|
538
|
+
_OUT="$transcript_file" \
|
|
539
|
+
python3 -c "
|
|
540
|
+
import json, os, pathlib, re
|
|
541
|
+
|
|
542
|
+
iteration_id = os.environ['_IID']
|
|
543
|
+
voters = []
|
|
544
|
+
|
|
545
|
+
# Priority 1: structured round file (Path B -- council_aggregate_votes)
|
|
546
|
+
rfile = pathlib.Path(os.environ['_ROUND_FILE'])
|
|
547
|
+
if rfile.exists():
|
|
548
|
+
try:
|
|
549
|
+
rd = json.loads(rfile.read_text())
|
|
550
|
+
for v in rd.get('votes', []):
|
|
551
|
+
voters.append({
|
|
552
|
+
'name': v.get('role', 'unknown'),
|
|
553
|
+
'role_index': v.get('member', 0),
|
|
554
|
+
'verdict': 'APPROVE' if v.get('vote') == 'COMPLETE' else 'REJECT',
|
|
555
|
+
'reasoning': v.get('reason', ''),
|
|
556
|
+
'issues': [],
|
|
557
|
+
'is_contrarian': False,
|
|
558
|
+
})
|
|
559
|
+
except Exception:
|
|
560
|
+
pass
|
|
561
|
+
|
|
562
|
+
# Priority 2: member txt files (Path A -- council_vote)
|
|
563
|
+
if not voters:
|
|
564
|
+
mdir = pathlib.Path(os.environ['_MEMBERS_DIR'])
|
|
565
|
+
roles = ['requirements_verifier', 'test_auditor', 'devils_advocate']
|
|
566
|
+
if mdir.exists():
|
|
567
|
+
for mf in sorted(mdir.glob('member-*.txt')):
|
|
568
|
+
content = mf.read_text(errors='replace').strip()
|
|
569
|
+
vote_match = re.search(r'VOTE\s*:\s*(APPROVE|REJECT|CANNOT_VALIDATE)', content)
|
|
570
|
+
reason_match = re.search(r'REASON\s*:\s*(.+?)(?:\n|\$)', content)
|
|
571
|
+
issues = []
|
|
572
|
+
for im in re.finditer(r'ISSUES\s*:\s*(CRITICAL|HIGH|MEDIUM|LOW)\s*:\s*(.+?)(?:\n|\$)', content):
|
|
573
|
+
issues.append({'severity': im.group(1), 'description': im.group(2).strip()})
|
|
574
|
+
idx = int(re.sub(r'\D', '', mf.stem) or '0') - 1
|
|
575
|
+
role = roles[idx % len(roles)] if idx >= 0 else 'unknown'
|
|
576
|
+
voters.append({
|
|
577
|
+
'name': role,
|
|
578
|
+
'role_index': idx + 1,
|
|
579
|
+
'verdict': vote_match.group(1) if vote_match else 'REJECT',
|
|
580
|
+
'reasoning': reason_match.group(1).strip() if reason_match else '',
|
|
581
|
+
'issues': issues,
|
|
582
|
+
'is_contrarian': False,
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
# Add DA voter if triggered
|
|
586
|
+
ct = os.environ['_CT'] == 'true'
|
|
587
|
+
cf = os.environ['_CF'] == 'true'
|
|
588
|
+
if ct:
|
|
589
|
+
da_challenges = []
|
|
590
|
+
dafile = pathlib.Path(os.environ['_DA_FILE'])
|
|
591
|
+
if dafile.exists():
|
|
592
|
+
try:
|
|
593
|
+
da = json.loads(dafile.read_text())
|
|
594
|
+
details = da.get('details', '')
|
|
595
|
+
if details and details != 'none':
|
|
596
|
+
da_challenges = [d.strip() for d in details.split(';') if d.strip()]
|
|
597
|
+
except Exception:
|
|
598
|
+
pass
|
|
599
|
+
# Also check contrarian.txt for reasoning
|
|
600
|
+
cfile = pathlib.Path(os.environ['_MEMBERS_DIR']) / 'contrarian.txt'
|
|
601
|
+
da_reasoning = ''
|
|
602
|
+
da_verdict = 'REJECT' if cf else 'APPROVE'
|
|
603
|
+
if cfile.exists():
|
|
604
|
+
content = cfile.read_text(errors='replace').strip()
|
|
605
|
+
reason_match = re.search(r'REASON\s*:\s*(.+?)(?:\n|\$)', content)
|
|
606
|
+
if reason_match:
|
|
607
|
+
da_reasoning = reason_match.group(1).strip()
|
|
608
|
+
voters.append({
|
|
609
|
+
'name': 'devils_advocate',
|
|
610
|
+
'role_index': len(voters) + 1,
|
|
611
|
+
'verdict': da_verdict,
|
|
612
|
+
'reasoning': da_reasoning,
|
|
613
|
+
'issues': [],
|
|
614
|
+
'challenges': da_challenges,
|
|
615
|
+
'is_contrarian': True,
|
|
616
|
+
'triggered': True,
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
task_or_prd = os.environ.get('_TASK', '')[:200]
|
|
620
|
+
non_contrarian = [v for v in voters if not v.get('is_contrarian')]
|
|
621
|
+
transcript = {
|
|
622
|
+
'iteration_id': iteration_id,
|
|
623
|
+
'iteration': int(os.environ['_IT']),
|
|
624
|
+
'timestamp': os.environ['_TS'],
|
|
625
|
+
'task_or_prd': task_or_prd,
|
|
626
|
+
'prd_path': os.environ.get('_PRD', ''),
|
|
627
|
+
'voters': voters,
|
|
628
|
+
'outcome': os.environ['_OUTCOME'],
|
|
629
|
+
'contrarian_triggered': ct,
|
|
630
|
+
'contrarian_flipped': cf,
|
|
631
|
+
'approve_count': sum(1 for v in non_contrarian if v.get('verdict') == 'APPROVE'),
|
|
632
|
+
'reject_count': sum(1 for v in non_contrarian if v.get('verdict') in ('REJECT', 'CANNOT_VALIDATE')),
|
|
633
|
+
'threshold': int(os.environ.get('_THRESHOLD', '0')),
|
|
634
|
+
'total_members': len(non_contrarian),
|
|
635
|
+
}
|
|
636
|
+
with open(os.environ['_OUT'], 'w') as f:
|
|
637
|
+
json.dump(transcript, f, indent=2)
|
|
638
|
+
" || log_warn "Failed to write council transcript"
|
|
639
|
+
}
|
|
640
|
+
|
|
479
641
|
#===============================================================================
|
|
480
642
|
# Evidence Gathering - Collect data for council review
|
|
481
643
|
#===============================================================================
|
|
@@ -1353,6 +1515,9 @@ council_evaluate() {
|
|
|
1353
1515
|
return 1 # CONTINUE - can't complete with critical failures
|
|
1354
1516
|
fi
|
|
1355
1517
|
|
|
1518
|
+
# Compute threshold using the same ceiling(2/3) formula as council_vote and council_aggregate_votes
|
|
1519
|
+
local _eval_threshold=$(( (COUNCIL_SIZE * 2 + 2) / 3 ))
|
|
1520
|
+
|
|
1356
1521
|
# Step 1: Aggregate votes from all members
|
|
1357
1522
|
local aggregate_result
|
|
1358
1523
|
aggregate_result=$(council_aggregate_votes)
|
|
@@ -1372,14 +1537,23 @@ council_evaluate() {
|
|
|
1372
1537
|
da_result=$(council_devils_advocate_review "$ITERATION_COUNT")
|
|
1373
1538
|
if [ "$da_result" = "OVERRIDE_CONTINUE" ]; then
|
|
1374
1539
|
log_warn "Council evaluate: devil's advocate overrode unanimous COMPLETE"
|
|
1540
|
+
# Write transcript: DA triggered and flipped the outcome (Path B)
|
|
1541
|
+
council_write_transcript "${ITERATION_COUNT:-0}" "REJECTED" "true" "true" "$_eval_threshold"
|
|
1375
1542
|
return 1 # CONTINUE
|
|
1376
1543
|
fi
|
|
1544
|
+
# Write transcript: DA triggered but did NOT flip (Path B, unanimous COMPLETE confirmed)
|
|
1545
|
+
council_write_transcript "${ITERATION_COUNT:-0}" "APPROVED" "true" "false" "$_eval_threshold"
|
|
1546
|
+
else
|
|
1547
|
+
# Write transcript: not unanimous, DA not triggered (Path B)
|
|
1548
|
+
council_write_transcript "${ITERATION_COUNT:-0}" "APPROVED" "false" "false" "$_eval_threshold"
|
|
1377
1549
|
fi
|
|
1378
1550
|
|
|
1379
1551
|
log_info "Council evaluate: verdict is COMPLETE"
|
|
1380
1552
|
return 0 # COMPLETE (should stop)
|
|
1381
1553
|
fi
|
|
1382
1554
|
|
|
1555
|
+
# Write transcript: aggregate voted CONTINUE (Path B)
|
|
1556
|
+
council_write_transcript "${ITERATION_COUNT:-0}" "REJECTED" "false" "false" "$_eval_threshold"
|
|
1383
1557
|
log_info "Council evaluate: verdict is CONTINUE"
|
|
1384
1558
|
return 1 # CONTINUE
|
|
1385
1559
|
}
|
package/autonomy/loki
CHANGED
|
@@ -7052,9 +7052,10 @@ cmd_sentrux() {
|
|
|
7052
7052
|
case "$sub" in
|
|
7053
7053
|
baseline)
|
|
7054
7054
|
if ! sentrux_available; then
|
|
7055
|
-
echo -e "${YELLOW}sentrux not installed.${NC} Install
|
|
7055
|
+
echo -e "${YELLOW}sentrux not installed.${NC} Install with one of:" >&2
|
|
7056
7056
|
echo " brew install sentrux/tap/sentrux" >&2
|
|
7057
|
-
echo "
|
|
7057
|
+
echo " curl -fsSL https://raw.githubusercontent.com/sentrux/sentrux/main/install.sh | sh" >&2
|
|
7058
|
+
echo " See also: https://github.com/sentrux/sentrux" >&2
|
|
7058
7059
|
return 2
|
|
7059
7060
|
fi
|
|
7060
7061
|
if sentrux_baseline_save "$target"; then
|
|
@@ -7068,7 +7069,10 @@ cmd_sentrux() {
|
|
|
7068
7069
|
;;
|
|
7069
7070
|
gate)
|
|
7070
7071
|
if ! sentrux_available; then
|
|
7071
|
-
echo -e "${YELLOW}sentrux not installed.${NC}
|
|
7072
|
+
echo -e "${YELLOW}sentrux not installed.${NC} Install with one of:" >&2
|
|
7073
|
+
echo " brew install sentrux/tap/sentrux" >&2
|
|
7074
|
+
echo " curl -fsSL https://raw.githubusercontent.com/sentrux/sentrux/main/install.sh | sh" >&2
|
|
7075
|
+
echo " See also: https://github.com/sentrux/sentrux" >&2
|
|
7072
7076
|
return 2
|
|
7073
7077
|
fi
|
|
7074
7078
|
local diff verdict before after
|
|
@@ -7097,7 +7101,10 @@ cmd_sentrux() {
|
|
|
7097
7101
|
status)
|
|
7098
7102
|
if ! sentrux_available; then
|
|
7099
7103
|
echo "sentrux: not installed (optional)"
|
|
7100
|
-
echo "install
|
|
7104
|
+
echo "install with one of:"
|
|
7105
|
+
echo " brew install sentrux/tap/sentrux"
|
|
7106
|
+
echo " curl -fsSL https://raw.githubusercontent.com/sentrux/sentrux/main/install.sh | sh"
|
|
7107
|
+
echo " See also: https://github.com/sentrux/sentrux"
|
|
7101
7108
|
return 0
|
|
7102
7109
|
fi
|
|
7103
7110
|
local v
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -3782,6 +3782,93 @@ async def force_council_review():
|
|
|
3782
3782
|
return {"success": True, "message": "Council review requested"}
|
|
3783
3783
|
|
|
3784
3784
|
|
|
3785
|
+
@app.get("/api/council/transcripts")
|
|
3786
|
+
async def get_council_transcripts(
|
|
3787
|
+
limit: int = Query(default=20, ge=1, le=200),
|
|
3788
|
+
since: Optional[str] = Query(default=None),
|
|
3789
|
+
iter_min: Optional[int] = Query(default=None, ge=0),
|
|
3790
|
+
):
|
|
3791
|
+
"""List council transcript records, sorted descending by iteration number.
|
|
3792
|
+
|
|
3793
|
+
Query params:
|
|
3794
|
+
limit int, default=20, max=200
|
|
3795
|
+
since ISO8601 string (optional), filter to transcripts after this time
|
|
3796
|
+
iter_min int (optional), filter to iteration >= N
|
|
3797
|
+
"""
|
|
3798
|
+
# Validate query params before any early-return so invalid inputs always get 400.
|
|
3799
|
+
since_dt = None
|
|
3800
|
+
if since:
|
|
3801
|
+
try:
|
|
3802
|
+
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
|
3803
|
+
except ValueError:
|
|
3804
|
+
raise HTTPException(status_code=400, detail="Invalid 'since' timestamp format; expected ISO8601")
|
|
3805
|
+
|
|
3806
|
+
transcripts_dir = _get_loki_dir() / "council" / "transcripts"
|
|
3807
|
+
if not transcripts_dir.exists():
|
|
3808
|
+
return {"transcripts": [], "total": 0, "latest_id": None}
|
|
3809
|
+
|
|
3810
|
+
records = []
|
|
3811
|
+
for f in sorted(transcripts_dir.glob("iter-*.json"), reverse=True):
|
|
3812
|
+
try:
|
|
3813
|
+
rec = json.loads(f.read_text())
|
|
3814
|
+
except Exception:
|
|
3815
|
+
logger.warning("Skipping corrupt council transcript file: %s", f.name)
|
|
3816
|
+
continue
|
|
3817
|
+
if not isinstance(rec, dict):
|
|
3818
|
+
logger.warning("Skipping non-object council transcript file: %s", f.name)
|
|
3819
|
+
continue
|
|
3820
|
+
if not isinstance(rec.get("iteration_id"), str):
|
|
3821
|
+
logger.warning("Skipping transcript missing iteration_id field: %s", f.name)
|
|
3822
|
+
continue
|
|
3823
|
+
if since_dt is not None:
|
|
3824
|
+
ts_str = rec.get("timestamp", "")
|
|
3825
|
+
try:
|
|
3826
|
+
ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
3827
|
+
except (ValueError, AttributeError):
|
|
3828
|
+
continue
|
|
3829
|
+
if ts <= since_dt:
|
|
3830
|
+
continue
|
|
3831
|
+
if iter_min is not None and rec.get("iteration", 0) < iter_min:
|
|
3832
|
+
continue
|
|
3833
|
+
records.append(rec)
|
|
3834
|
+
if len(records) >= limit:
|
|
3835
|
+
break
|
|
3836
|
+
|
|
3837
|
+
return {
|
|
3838
|
+
"transcripts": records,
|
|
3839
|
+
"total": len(records),
|
|
3840
|
+
"latest_id": records[0].get("iteration_id") if records else None,
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
|
|
3844
|
+
@app.get("/api/council/transcripts/{iteration_id}")
|
|
3845
|
+
async def get_council_transcript(iteration_id: str):
|
|
3846
|
+
"""Fetch a single council transcript by iteration_id.
|
|
3847
|
+
|
|
3848
|
+
Returns the record body or 404 if not found.
|
|
3849
|
+
Path traversal attempts (containing '/' or '..') are rejected with 404.
|
|
3850
|
+
"""
|
|
3851
|
+
# Reject path traversal: iteration_id must be a plain filename component.
|
|
3852
|
+
if "/" in iteration_id or "\\" in iteration_id or ".." in iteration_id:
|
|
3853
|
+
raise HTTPException(status_code=404, detail="Transcript not found")
|
|
3854
|
+
transcript_file = _get_loki_dir() / "council" / "transcripts" / f"{iteration_id}.json"
|
|
3855
|
+
if not transcript_file.exists():
|
|
3856
|
+
raise HTTPException(status_code=404, detail="Transcript not found")
|
|
3857
|
+
try:
|
|
3858
|
+
rec = json.loads(transcript_file.read_text())
|
|
3859
|
+
except Exception:
|
|
3860
|
+
raise HTTPException(
|
|
3861
|
+
status_code=410,
|
|
3862
|
+
detail=f"Transcript file for {iteration_id} is corrupt; admin should inspect or remove it",
|
|
3863
|
+
)
|
|
3864
|
+
if not isinstance(rec, dict):
|
|
3865
|
+
raise HTTPException(
|
|
3866
|
+
status_code=410,
|
|
3867
|
+
detail=f"Transcript file for {iteration_id} is corrupt; admin should inspect or remove it",
|
|
3868
|
+
)
|
|
3869
|
+
return rec
|
|
3870
|
+
|
|
3871
|
+
|
|
3785
3872
|
# =============================================================================
|
|
3786
3873
|
# Context Window Tracking API (v5.40.0)
|
|
3787
3874
|
# =============================================================================
|