loki-mode 7.5.15 → 7.5.16
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 +168 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +74 -0
- package/dashboard/static/index.html +305 -134
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- 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.16
|
|
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.16 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.5.
|
|
1
|
+
7.5.16
|
|
@@ -470,12 +470,171 @@ 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"
|
|
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
|
+
#
|
|
504
|
+
# Output: .loki/council/transcripts/iter-<N>-<TIMESTAMP>.json
|
|
505
|
+
#===============================================================================
|
|
506
|
+
|
|
507
|
+
council_write_transcript() {
|
|
508
|
+
local iteration="${1:-${ITERATION_COUNT:-0}}"
|
|
509
|
+
local outcome="${2:-REJECTED}"
|
|
510
|
+
local contrarian_triggered="${3:-false}"
|
|
511
|
+
local contrarian_flipped="${4:-false}"
|
|
512
|
+
local timestamp
|
|
513
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
514
|
+
# Remove colons and hyphens from timestamp for filename safety
|
|
515
|
+
local ts_safe="${timestamp//[:\-]/}"
|
|
516
|
+
local iteration_id="iter-${iteration}-${ts_safe}"
|
|
517
|
+
local transcript_dir="$COUNCIL_STATE_DIR/transcripts"
|
|
518
|
+
mkdir -p "$transcript_dir"
|
|
519
|
+
local transcript_file="$transcript_dir/${iteration_id}.json"
|
|
520
|
+
|
|
521
|
+
# Read prd preview from state or prd file
|
|
522
|
+
local task_or_prd=""
|
|
523
|
+
if [ -n "$COUNCIL_PRD_PATH" ] && [ -f "$COUNCIL_PRD_PATH" ]; then
|
|
524
|
+
task_or_prd=$(head -5 "$COUNCIL_PRD_PATH" | tr '\n' ' ' | cut -c1-200)
|
|
525
|
+
fi
|
|
526
|
+
|
|
527
|
+
local round_file="$COUNCIL_STATE_DIR/votes/round-${iteration}.json"
|
|
528
|
+
local da_file="$COUNCIL_STATE_DIR/votes/devils-advocate-round-${iteration}.json"
|
|
529
|
+
|
|
530
|
+
_IT="$iteration" _TS="$timestamp" _IID="$iteration_id" \
|
|
531
|
+
_OUTCOME="$outcome" _CT="$contrarian_triggered" _CF="$contrarian_flipped" \
|
|
532
|
+
_TASK="$task_or_prd" _PRD="${COUNCIL_PRD_PATH:-}" \
|
|
533
|
+
_ROUND_FILE="${round_file}" _DA_FILE="${da_file}" \
|
|
534
|
+
_MEMBERS_DIR="$COUNCIL_STATE_DIR/votes/iteration-${iteration}" \
|
|
535
|
+
_OUT="$transcript_file" \
|
|
536
|
+
python3 -c "
|
|
537
|
+
import json, os, pathlib, re
|
|
538
|
+
|
|
539
|
+
iteration_id = os.environ['_IID']
|
|
540
|
+
voters = []
|
|
541
|
+
|
|
542
|
+
# Priority 1: structured round file (Path B -- council_aggregate_votes)
|
|
543
|
+
rfile = pathlib.Path(os.environ['_ROUND_FILE'])
|
|
544
|
+
if rfile.exists():
|
|
545
|
+
try:
|
|
546
|
+
rd = json.loads(rfile.read_text())
|
|
547
|
+
for v in rd.get('votes', []):
|
|
548
|
+
voters.append({
|
|
549
|
+
'name': v.get('role', 'unknown'),
|
|
550
|
+
'role_index': v.get('member', 0),
|
|
551
|
+
'verdict': 'APPROVE' if v.get('vote') == 'COMPLETE' else 'REJECT',
|
|
552
|
+
'reasoning': v.get('reason', ''),
|
|
553
|
+
'issues': [],
|
|
554
|
+
'is_contrarian': False,
|
|
555
|
+
})
|
|
556
|
+
except Exception:
|
|
557
|
+
pass
|
|
558
|
+
|
|
559
|
+
# Priority 2: member txt files (Path A -- council_vote)
|
|
560
|
+
if not voters:
|
|
561
|
+
mdir = pathlib.Path(os.environ['_MEMBERS_DIR'])
|
|
562
|
+
roles = ['requirements_verifier', 'test_auditor', 'devils_advocate']
|
|
563
|
+
if mdir.exists():
|
|
564
|
+
for mf in sorted(mdir.glob('member-*.txt')):
|
|
565
|
+
content = mf.read_text(errors='replace').strip()
|
|
566
|
+
vote_match = re.search(r'VOTE\s*:\s*(APPROVE|REJECT|CANNOT_VALIDATE)', content)
|
|
567
|
+
reason_match = re.search(r'REASON\s*:\s*(.+?)(?:\n|\$)', content)
|
|
568
|
+
issues = []
|
|
569
|
+
for im in re.finditer(r'ISSUES\s*:\s*(CRITICAL|HIGH|MEDIUM|LOW)\s*:\s*(.+?)(?:\n|\$)', content):
|
|
570
|
+
issues.append({'severity': im.group(1), 'description': im.group(2).strip()})
|
|
571
|
+
idx = int(re.sub(r'\D', '', mf.stem) or '0') - 1
|
|
572
|
+
role = roles[idx % len(roles)] if idx >= 0 else 'unknown'
|
|
573
|
+
voters.append({
|
|
574
|
+
'name': role,
|
|
575
|
+
'role_index': idx + 1,
|
|
576
|
+
'verdict': vote_match.group(1) if vote_match else 'REJECT',
|
|
577
|
+
'reasoning': reason_match.group(1).strip() if reason_match else '',
|
|
578
|
+
'issues': issues,
|
|
579
|
+
'is_contrarian': False,
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
# Add DA voter if triggered
|
|
583
|
+
ct = os.environ['_CT'] == 'true'
|
|
584
|
+
cf = os.environ['_CF'] == 'true'
|
|
585
|
+
if ct:
|
|
586
|
+
da_challenges = []
|
|
587
|
+
dafile = pathlib.Path(os.environ['_DA_FILE'])
|
|
588
|
+
if dafile.exists():
|
|
589
|
+
try:
|
|
590
|
+
da = json.loads(dafile.read_text())
|
|
591
|
+
details = da.get('details', '')
|
|
592
|
+
if details and details != 'none':
|
|
593
|
+
da_challenges = [d.strip() for d in details.split(';') if d.strip()]
|
|
594
|
+
except Exception:
|
|
595
|
+
pass
|
|
596
|
+
# Also check contrarian.txt for reasoning
|
|
597
|
+
cfile = pathlib.Path(os.environ['_MEMBERS_DIR']) / 'contrarian.txt'
|
|
598
|
+
da_reasoning = ''
|
|
599
|
+
da_verdict = 'REJECT' if cf else 'APPROVE'
|
|
600
|
+
if cfile.exists():
|
|
601
|
+
content = cfile.read_text(errors='replace').strip()
|
|
602
|
+
reason_match = re.search(r'REASON\s*:\s*(.+?)(?:\n|\$)', content)
|
|
603
|
+
if reason_match:
|
|
604
|
+
da_reasoning = reason_match.group(1).strip()
|
|
605
|
+
voters.append({
|
|
606
|
+
'name': 'devils_advocate',
|
|
607
|
+
'role_index': len(voters) + 1,
|
|
608
|
+
'verdict': da_verdict,
|
|
609
|
+
'reasoning': da_reasoning,
|
|
610
|
+
'issues': [],
|
|
611
|
+
'challenges': da_challenges,
|
|
612
|
+
'is_contrarian': True,
|
|
613
|
+
'triggered': True,
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
task_or_prd = os.environ.get('_TASK', '')[:200]
|
|
617
|
+
non_contrarian = [v for v in voters if not v.get('is_contrarian')]
|
|
618
|
+
transcript = {
|
|
619
|
+
'iteration_id': iteration_id,
|
|
620
|
+
'iteration': int(os.environ['_IT']),
|
|
621
|
+
'timestamp': os.environ['_TS'],
|
|
622
|
+
'task_or_prd': task_or_prd,
|
|
623
|
+
'prd_path': os.environ.get('_PRD', ''),
|
|
624
|
+
'voters': voters,
|
|
625
|
+
'outcome': os.environ['_OUTCOME'],
|
|
626
|
+
'contrarian_triggered': ct,
|
|
627
|
+
'contrarian_flipped': cf,
|
|
628
|
+
'approve_count': sum(1 for v in non_contrarian if v.get('verdict') == 'APPROVE'),
|
|
629
|
+
'reject_count': sum(1 for v in non_contrarian if v.get('verdict') in ('REJECT', 'CANNOT_VALIDATE')),
|
|
630
|
+
'threshold': 2,
|
|
631
|
+
'total_members': len(non_contrarian),
|
|
632
|
+
}
|
|
633
|
+
with open(os.environ['_OUT'], 'w') as f:
|
|
634
|
+
json.dump(transcript, f, indent=2)
|
|
635
|
+
" || log_warn "Failed to write council transcript"
|
|
636
|
+
}
|
|
637
|
+
|
|
479
638
|
#===============================================================================
|
|
480
639
|
# Evidence Gathering - Collect data for council review
|
|
481
640
|
#===============================================================================
|
|
@@ -1372,14 +1531,23 @@ council_evaluate() {
|
|
|
1372
1531
|
da_result=$(council_devils_advocate_review "$ITERATION_COUNT")
|
|
1373
1532
|
if [ "$da_result" = "OVERRIDE_CONTINUE" ]; then
|
|
1374
1533
|
log_warn "Council evaluate: devil's advocate overrode unanimous COMPLETE"
|
|
1534
|
+
# Write transcript: DA triggered and flipped the outcome (Path B)
|
|
1535
|
+
council_write_transcript "${ITERATION_COUNT:-0}" "REJECTED" "true" "true"
|
|
1375
1536
|
return 1 # CONTINUE
|
|
1376
1537
|
fi
|
|
1538
|
+
# Write transcript: DA triggered but did NOT flip (Path B, unanimous COMPLETE confirmed)
|
|
1539
|
+
council_write_transcript "${ITERATION_COUNT:-0}" "APPROVED" "true" "false"
|
|
1540
|
+
else
|
|
1541
|
+
# Write transcript: not unanimous, DA not triggered (Path B)
|
|
1542
|
+
council_write_transcript "${ITERATION_COUNT:-0}" "APPROVED" "false" "false"
|
|
1377
1543
|
fi
|
|
1378
1544
|
|
|
1379
1545
|
log_info "Council evaluate: verdict is COMPLETE"
|
|
1380
1546
|
return 0 # COMPLETE (should stop)
|
|
1381
1547
|
fi
|
|
1382
1548
|
|
|
1549
|
+
# Write transcript: aggregate voted CONTINUE (Path B)
|
|
1550
|
+
council_write_transcript "${ITERATION_COUNT:-0}" "REJECTED" "false" "false"
|
|
1383
1551
|
log_info "Council evaluate: verdict is CONTINUE"
|
|
1384
1552
|
return 1 # CONTINUE
|
|
1385
1553
|
}
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -3782,6 +3782,80 @@ 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),
|
|
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
|
+
transcripts_dir = _get_loki_dir() / "council" / "transcripts"
|
|
3799
|
+
if not transcripts_dir.exists():
|
|
3800
|
+
return {"transcripts": [], "total": 0, "latest_id": None}
|
|
3801
|
+
|
|
3802
|
+
since_dt = None
|
|
3803
|
+
if since:
|
|
3804
|
+
try:
|
|
3805
|
+
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
|
3806
|
+
except ValueError:
|
|
3807
|
+
raise HTTPException(status_code=400, detail="Invalid 'since' timestamp format; expected ISO8601")
|
|
3808
|
+
|
|
3809
|
+
records = []
|
|
3810
|
+
for f in sorted(transcripts_dir.glob("iter-*.json"), reverse=True):
|
|
3811
|
+
try:
|
|
3812
|
+
rec = json.loads(f.read_text())
|
|
3813
|
+
except Exception:
|
|
3814
|
+
logger.warning("Skipping corrupt council transcript file: %s", f.name)
|
|
3815
|
+
continue
|
|
3816
|
+
if not isinstance(rec, dict):
|
|
3817
|
+
logger.warning("Skipping non-object council transcript file: %s", f.name)
|
|
3818
|
+
continue
|
|
3819
|
+
if since_dt is not None:
|
|
3820
|
+
ts_str = rec.get("timestamp", "")
|
|
3821
|
+
try:
|
|
3822
|
+
ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
3823
|
+
except (ValueError, AttributeError):
|
|
3824
|
+
continue
|
|
3825
|
+
if ts <= since_dt:
|
|
3826
|
+
continue
|
|
3827
|
+
if iter_min is not None and rec.get("iteration", 0) < iter_min:
|
|
3828
|
+
continue
|
|
3829
|
+
records.append(rec)
|
|
3830
|
+
if len(records) >= limit:
|
|
3831
|
+
break
|
|
3832
|
+
|
|
3833
|
+
return {
|
|
3834
|
+
"transcripts": records,
|
|
3835
|
+
"total": len(records),
|
|
3836
|
+
"latest_id": records[0]["iteration_id"] if records else None,
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
|
|
3840
|
+
@app.get("/api/council/transcripts/{iteration_id}")
|
|
3841
|
+
async def get_council_transcript(iteration_id: str):
|
|
3842
|
+
"""Fetch a single council transcript by iteration_id.
|
|
3843
|
+
|
|
3844
|
+
Returns the record body or 404 if not found.
|
|
3845
|
+
Path traversal attempts (containing '/' or '..') are rejected with 404.
|
|
3846
|
+
"""
|
|
3847
|
+
# Reject path traversal: iteration_id must be a plain filename component.
|
|
3848
|
+
if "/" in iteration_id or "\\" in iteration_id or ".." in iteration_id:
|
|
3849
|
+
raise HTTPException(status_code=404, detail="Transcript not found")
|
|
3850
|
+
transcript_file = _get_loki_dir() / "council" / "transcripts" / f"{iteration_id}.json"
|
|
3851
|
+
if not transcript_file.exists():
|
|
3852
|
+
raise HTTPException(status_code=404, detail="Transcript not found")
|
|
3853
|
+
try:
|
|
3854
|
+
return json.loads(transcript_file.read_text())
|
|
3855
|
+
except Exception:
|
|
3856
|
+
raise HTTPException(status_code=500, detail="Corrupt transcript file")
|
|
3857
|
+
|
|
3858
|
+
|
|
3785
3859
|
# =============================================================================
|
|
3786
3860
|
# Context Window Tracking API (v5.40.0)
|
|
3787
3861
|
# =============================================================================
|