loki-mode 6.66.0 → 6.67.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 +3 -2
- package/VERSION +1 -1
- package/autonomy/hooks/migration-hooks.sh +170 -0
- package/autonomy/loki +1018 -1
- package/autonomy/notification-checker.py +4 -3
- package/autonomy/run.sh +22 -10
- package/autonomy/tui.sh +471 -0
- package/completions/_loki +57 -0
- package/completions/loki.bash +39 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +20 -6
- package/docs/INSTALLATION.md +1 -1
- package/events/bus.py +9 -1
- package/events/emit.sh +2 -2
- package/mcp/__init__.py +1 -1
- package/memory/namespace.py +14 -11
- package/memory/schemas.py +155 -0
- package/memory/storage.py +29 -4
- package/package.json +1 -1
- package/references/legacy-healing-patterns.md +352 -0
- package/skills/00-index.md +13 -1
- package/skills/healing.md +491 -0
- package/skills/quality-gates.md +31 -2
- package/skills/troubleshooting.md +33 -0
package/autonomy/loki
CHANGED
|
@@ -37,6 +37,13 @@ log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
|
|
37
37
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
38
38
|
log_debug() { echo -e "${CYAN}[DEBUG]${NC} $*"; }
|
|
39
39
|
|
|
40
|
+
# Source TUI library if available (spinners, progress bars, tables, diffs)
|
|
41
|
+
_LOKI_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
42
|
+
if [ -f "$_LOKI_SCRIPT_DIR/tui.sh" ]; then
|
|
43
|
+
# shellcheck source=tui.sh
|
|
44
|
+
source "$_LOKI_SCRIPT_DIR/tui.sh"
|
|
45
|
+
fi
|
|
46
|
+
|
|
40
47
|
# Resolve the script's real path (handles symlinks)
|
|
41
48
|
resolve_script_path() {
|
|
42
49
|
local script="$1"
|
|
@@ -421,6 +428,7 @@ show_help() {
|
|
|
421
428
|
echo " checkpoint|cp Save/restore session checkpoints"
|
|
422
429
|
echo " projects Multi-project registry management"
|
|
423
430
|
echo " audit [cmd] Agent audit log and quality scanning (log|scan)"
|
|
431
|
+
echo " heal <path> Legacy system healing (archaeology, stabilize, modernize)"
|
|
424
432
|
echo " review [opts] Standalone code review with quality gates (diff, staged, PR, files)"
|
|
425
433
|
echo " optimize Optimize prompts based on session history"
|
|
426
434
|
echo " enterprise Enterprise feature management (tokens, OIDC)"
|
|
@@ -442,6 +450,8 @@ show_help() {
|
|
|
442
450
|
echo " plan <PRD> Dry-run PRD analysis: complexity, cost, and execution plan"
|
|
443
451
|
echo " ci [opts] CI/CD quality gate integration (--pr, --report, --github-comment)"
|
|
444
452
|
echo " test [opts] AI-powered test generation (--file, --dir, --changed, --dry-run)"
|
|
453
|
+
echo " context [cmd] Context window management (show|files|tools|add|clear)"
|
|
454
|
+
echo " code [cmd] Codebase intelligence (overview|symbols|deps|hotspots|diff)"
|
|
445
455
|
echo " report [opts] Session report generator (--format text|markdown|html, --output)"
|
|
446
456
|
echo " share [opts] Share session report as GitHub Gist (--private, --format)"
|
|
447
457
|
echo " version Show version"
|
|
@@ -1700,13 +1710,40 @@ cmd_status() {
|
|
|
1700
1710
|
echo -e "${CYAN}Pending Tasks:${NC} $task_count"
|
|
1701
1711
|
fi
|
|
1702
1712
|
|
|
1703
|
-
# Check budget
|
|
1713
|
+
# Check budget with visual gauge
|
|
1704
1714
|
if [ -f "$LOKI_DIR/metrics/budget.json" ]; then
|
|
1705
1715
|
local budget_limit budget_used budget_remaining
|
|
1706
1716
|
budget_limit=$(LOKI_BUDGET_FILE="$LOKI_DIR/metrics/budget.json" python3 -c "import json,os; d=json.load(open(os.environ['LOKI_BUDGET_FILE'])); print(d.get('budget_limit', 0))" 2>/dev/null || echo "0")
|
|
1707
1717
|
budget_used=$(LOKI_BUDGET_FILE="$LOKI_DIR/metrics/budget.json" python3 -c "import json,os; d=json.load(open(os.environ['LOKI_BUDGET_FILE'])); print(round(d.get('budget_used', 0), 2))" 2>/dev/null || echo "0")
|
|
1708
1718
|
if [ "$budget_limit" != "0" ]; then
|
|
1709
1719
|
echo -e "${CYAN}Budget:${NC} \$$budget_used / \$$budget_limit"
|
|
1720
|
+
# Show budget gauge if TUI is loaded
|
|
1721
|
+
if type context_gauge &>/dev/null; then
|
|
1722
|
+
local used_cents remaining_cents limit_cents
|
|
1723
|
+
used_cents=$(echo "scale=0; $budget_used * 100" | bc 2>/dev/null || echo "0")
|
|
1724
|
+
limit_cents=$(echo "scale=0; $budget_limit * 100" | bc 2>/dev/null || echo "100")
|
|
1725
|
+
# Render as integer cents for the gauge
|
|
1726
|
+
used_cents=${used_cents%.*}
|
|
1727
|
+
limit_cents=${limit_cents%.*}
|
|
1728
|
+
[ -z "$used_cents" ] && used_cents=0
|
|
1729
|
+
[ -z "$limit_cents" ] && limit_cents=100
|
|
1730
|
+
context_gauge "$used_cents" "$limit_cents" "Budget"
|
|
1731
|
+
fi
|
|
1732
|
+
else
|
|
1733
|
+
echo -e "${CYAN}Cost:${NC} \$$budget_used (no limit)"
|
|
1734
|
+
fi
|
|
1735
|
+
fi
|
|
1736
|
+
|
|
1737
|
+
# Context window usage (token tracking)
|
|
1738
|
+
if [ -f "$LOKI_DIR/state/context-usage.json" ]; then
|
|
1739
|
+
local ctx_used ctx_total
|
|
1740
|
+
ctx_total=$(python3 -c "import json; d=json.load(open('$LOKI_DIR/state/context-usage.json')); print(d.get('window_size', 200000))" 2>/dev/null || echo "200000")
|
|
1741
|
+
ctx_used=$(python3 -c "import json; d=json.load(open('$LOKI_DIR/state/context-usage.json')); print(d.get('used_tokens', 0))" 2>/dev/null || echo "0")
|
|
1742
|
+
if type context_gauge &>/dev/null; then
|
|
1743
|
+
context_gauge "$ctx_used" "$ctx_total" "Context"
|
|
1744
|
+
else
|
|
1745
|
+
local ctx_pct=$((ctx_used * 100 / ctx_total))
|
|
1746
|
+
echo -e "${CYAN}Context:${NC} ${ctx_pct}% (${ctx_used} / ${ctx_total} tokens)"
|
|
1710
1747
|
fi
|
|
1711
1748
|
fi
|
|
1712
1749
|
|
|
@@ -1718,6 +1755,10 @@ cmd_status() {
|
|
|
1718
1755
|
echo -e "${CYAN}Dashboard:${NC} http://127.0.0.1:$port/"
|
|
1719
1756
|
fi
|
|
1720
1757
|
fi
|
|
1758
|
+
|
|
1759
|
+
echo ""
|
|
1760
|
+
echo -e "${DIM} Tip: loki context show - detailed token breakdown${NC}"
|
|
1761
|
+
echo -e "${DIM} Tip: loki code overview - codebase intelligence${NC}"
|
|
1721
1762
|
}
|
|
1722
1763
|
|
|
1723
1764
|
# JSON output for loki status --json
|
|
@@ -8600,6 +8641,418 @@ with open(manifest_path, 'w') as f:
|
|
|
8600
8641
|
echo -e "View details: loki migrate --status ${migration_id}"
|
|
8601
8642
|
}
|
|
8602
8643
|
|
|
8644
|
+
#===============================================================================
|
|
8645
|
+
# loki heal - Legacy System Healing (v6.67.0)
|
|
8646
|
+
# Inspired by Amazon AGI Lab's "How Agentic AI Helps Heal Systems We Can't Replace"
|
|
8647
|
+
# Modernize legacy codebases incrementally without breaking existing behavior.
|
|
8648
|
+
#===============================================================================
|
|
8649
|
+
|
|
8650
|
+
cmd_heal_help() {
|
|
8651
|
+
echo -e "${BOLD}loki heal${NC} - Legacy system healing (v6.67.0)"
|
|
8652
|
+
echo ""
|
|
8653
|
+
echo "Heal legacy codebases by understanding their real behaviors -- the quirks,"
|
|
8654
|
+
echo "delays, error states, and invisible dependencies -- then modernizing"
|
|
8655
|
+
echo "incrementally while preserving institutional logic."
|
|
8656
|
+
echo ""
|
|
8657
|
+
echo "Usage: loki heal <path-to-codebase> [options]"
|
|
8658
|
+
echo ""
|
|
8659
|
+
echo "Phases:"
|
|
8660
|
+
echo " archaeology Extract knowledge, map dependencies, catalog friction"
|
|
8661
|
+
echo " stabilize Add observability and tests without changing behavior"
|
|
8662
|
+
echo " isolate Create adapter boundaries between components"
|
|
8663
|
+
echo " modernize Replace components one at a time behind adapters"
|
|
8664
|
+
echo " validate Verify behavioral equivalence with baseline"
|
|
8665
|
+
echo ""
|
|
8666
|
+
echo "Options:"
|
|
8667
|
+
echo " --phase PHASE Start from specific phase (default: archaeology)"
|
|
8668
|
+
echo " --resume Resume healing from last checkpoint"
|
|
8669
|
+
echo " --status Show healing progress"
|
|
8670
|
+
echo " --report Generate healing report"
|
|
8671
|
+
echo " --strict Block ALL behavioral changes without approval"
|
|
8672
|
+
echo " --archaeology-only Extract knowledge only, don't modify code"
|
|
8673
|
+
echo " --friction-map Show friction map for target codebase"
|
|
8674
|
+
echo " --compliance PRESET Compliance mode (healthcare|fintech|government)"
|
|
8675
|
+
echo " --provider NAME AI provider (default: claude)"
|
|
8676
|
+
echo " --parallel N Parallel healing agents (default: 1)"
|
|
8677
|
+
echo " --no-dashboard Disable web dashboard"
|
|
8678
|
+
echo " --dry-run Show healing plan without executing"
|
|
8679
|
+
echo ""
|
|
8680
|
+
echo "Environment Variables:"
|
|
8681
|
+
echo " LOKI_HEAL_MODE Enable healing mode (auto-set by 'loki heal')"
|
|
8682
|
+
echo " LOKI_HEAL_PHASE Override starting phase"
|
|
8683
|
+
echo " LOKI_HEAL_PRESERVE_FRICTION Warn before removing friction points (default: true)"
|
|
8684
|
+
echo " LOKI_HEAL_STRICT Block all behavioral changes (default: false)"
|
|
8685
|
+
echo ""
|
|
8686
|
+
echo "Examples:"
|
|
8687
|
+
echo " loki heal ./legacy-app # Full healing pipeline"
|
|
8688
|
+
echo " loki heal ./legacy-app --phase archaeology # Knowledge extraction only"
|
|
8689
|
+
echo " loki heal ./legacy-app --archaeology-only # Extract without modifying"
|
|
8690
|
+
echo " loki heal ./legacy-app --resume # Resume from checkpoint"
|
|
8691
|
+
echo " loki heal ./legacy-app --strict # Strict behavioral preservation"
|
|
8692
|
+
echo " loki heal --status # Show healing progress"
|
|
8693
|
+
echo " loki heal --friction-map ./legacy-app # View friction map"
|
|
8694
|
+
}
|
|
8695
|
+
|
|
8696
|
+
cmd_heal() {
|
|
8697
|
+
local codebase_path=""
|
|
8698
|
+
local phase="${LOKI_HEAL_PHASE:-archaeology}"
|
|
8699
|
+
local do_resume="false"
|
|
8700
|
+
local do_status="false"
|
|
8701
|
+
local do_report="false"
|
|
8702
|
+
local do_friction_map="false"
|
|
8703
|
+
local archaeology_only="false"
|
|
8704
|
+
local strict="${LOKI_HEAL_STRICT:-false}"
|
|
8705
|
+
local compliance=""
|
|
8706
|
+
local provider="${LOKI_PROVIDER:-claude}"
|
|
8707
|
+
local parallel="1"
|
|
8708
|
+
local no_dashboard="false"
|
|
8709
|
+
local dry_run="false"
|
|
8710
|
+
|
|
8711
|
+
if [ $# -eq 0 ]; then
|
|
8712
|
+
cmd_heal_help
|
|
8713
|
+
return 0
|
|
8714
|
+
fi
|
|
8715
|
+
|
|
8716
|
+
while [[ $# -gt 0 ]]; do
|
|
8717
|
+
case "$1" in
|
|
8718
|
+
--help|-h)
|
|
8719
|
+
cmd_heal_help
|
|
8720
|
+
return 0
|
|
8721
|
+
;;
|
|
8722
|
+
--status)
|
|
8723
|
+
do_status="true"
|
|
8724
|
+
shift
|
|
8725
|
+
;;
|
|
8726
|
+
--report)
|
|
8727
|
+
do_report="true"
|
|
8728
|
+
shift
|
|
8729
|
+
;;
|
|
8730
|
+
--friction-map)
|
|
8731
|
+
do_friction_map="true"
|
|
8732
|
+
shift
|
|
8733
|
+
;;
|
|
8734
|
+
--phase)
|
|
8735
|
+
if [[ -z "${2:-}" ]]; then
|
|
8736
|
+
echo -e "${RED}Error: --phase requires a value (archaeology|stabilize|isolate|modernize|validate)${NC}"
|
|
8737
|
+
return 1
|
|
8738
|
+
fi
|
|
8739
|
+
phase="$2"
|
|
8740
|
+
shift 2
|
|
8741
|
+
;;
|
|
8742
|
+
--phase=*)
|
|
8743
|
+
phase="${1#*=}"
|
|
8744
|
+
shift
|
|
8745
|
+
;;
|
|
8746
|
+
--resume)
|
|
8747
|
+
do_resume="true"
|
|
8748
|
+
shift
|
|
8749
|
+
;;
|
|
8750
|
+
--strict)
|
|
8751
|
+
strict="true"
|
|
8752
|
+
shift
|
|
8753
|
+
;;
|
|
8754
|
+
--archaeology-only)
|
|
8755
|
+
archaeology_only="true"
|
|
8756
|
+
phase="archaeology"
|
|
8757
|
+
shift
|
|
8758
|
+
;;
|
|
8759
|
+
--compliance)
|
|
8760
|
+
if [[ -z "${2:-}" ]]; then
|
|
8761
|
+
echo -e "${RED}Error: --compliance requires a value${NC}"
|
|
8762
|
+
return 1
|
|
8763
|
+
fi
|
|
8764
|
+
compliance="$2"
|
|
8765
|
+
shift 2
|
|
8766
|
+
;;
|
|
8767
|
+
--provider)
|
|
8768
|
+
if [[ -z "${2:-}" ]]; then
|
|
8769
|
+
echo -e "${RED}Error: --provider requires a value${NC}"
|
|
8770
|
+
return 1
|
|
8771
|
+
fi
|
|
8772
|
+
provider="$2"
|
|
8773
|
+
shift 2
|
|
8774
|
+
;;
|
|
8775
|
+
--parallel)
|
|
8776
|
+
if [[ -z "${2:-}" ]]; then
|
|
8777
|
+
echo -e "${RED}Error: --parallel requires a value${NC}"
|
|
8778
|
+
return 1
|
|
8779
|
+
fi
|
|
8780
|
+
parallel="$2"
|
|
8781
|
+
shift 2
|
|
8782
|
+
;;
|
|
8783
|
+
--no-dashboard)
|
|
8784
|
+
no_dashboard="true"
|
|
8785
|
+
shift
|
|
8786
|
+
;;
|
|
8787
|
+
--dry-run)
|
|
8788
|
+
dry_run="true"
|
|
8789
|
+
shift
|
|
8790
|
+
;;
|
|
8791
|
+
-*)
|
|
8792
|
+
echo -e "${RED}Unknown option: $1${NC}"
|
|
8793
|
+
echo "Run 'loki heal --help' for usage."
|
|
8794
|
+
return 1
|
|
8795
|
+
;;
|
|
8796
|
+
*)
|
|
8797
|
+
if [ -z "$codebase_path" ]; then
|
|
8798
|
+
codebase_path="$1"
|
|
8799
|
+
else
|
|
8800
|
+
echo -e "${RED}Error: Unexpected argument: $1${NC}"
|
|
8801
|
+
return 1
|
|
8802
|
+
fi
|
|
8803
|
+
shift
|
|
8804
|
+
;;
|
|
8805
|
+
esac
|
|
8806
|
+
done
|
|
8807
|
+
|
|
8808
|
+
# Route to subcommands
|
|
8809
|
+
if [ "$do_status" = "true" ]; then
|
|
8810
|
+
local heal_dir="${codebase_path:-.}/.loki/healing"
|
|
8811
|
+
if [[ ! -d "$heal_dir" ]]; then
|
|
8812
|
+
echo -e "${YELLOW}No healing session found.${NC}"
|
|
8813
|
+
echo "Start one with: loki heal <path-to-codebase>"
|
|
8814
|
+
return 0
|
|
8815
|
+
fi
|
|
8816
|
+
local progress_file="$heal_dir/healing-progress.json"
|
|
8817
|
+
if [[ -f "$progress_file" ]]; then
|
|
8818
|
+
echo -e "${BOLD}Healing Progress${NC}"
|
|
8819
|
+
echo ""
|
|
8820
|
+
python3 -c "
|
|
8821
|
+
import json, sys
|
|
8822
|
+
with open(sys.argv[1]) as f:
|
|
8823
|
+
data = json.load(f)
|
|
8824
|
+
print(f\" Codebase: {data.get('codebase', '?')}\")
|
|
8825
|
+
print(f\" Started: {data.get('started', '?')}\")
|
|
8826
|
+
print(f\" Overall Health: {data.get('overall_health', 0):.0%}\")
|
|
8827
|
+
print()
|
|
8828
|
+
for c in data.get('components', []):
|
|
8829
|
+
pct = c.get('characterization_passing', 0) / max(c.get('characterization_tests', 1), 1)
|
|
8830
|
+
print(f\" [{c.get('phase', '?'):12s}] {c.get('name', '?')}\")
|
|
8831
|
+
print(f\" Friction: {c.get('friction_resolved', 0)}/{c.get('friction_points', 0)} resolved\")
|
|
8832
|
+
print(f\" Tests: {c.get('characterization_passing', 0)}/{c.get('characterization_tests', 0)} passing\")
|
|
8833
|
+
print(f\" Health: {c.get('health_score', 0):.0%}\")
|
|
8834
|
+
print()
|
|
8835
|
+
" "$progress_file" 2>/dev/null || echo " Error reading progress file."
|
|
8836
|
+
else
|
|
8837
|
+
echo " No progress data yet. Healing may still be in archaeology phase."
|
|
8838
|
+
fi
|
|
8839
|
+
return 0
|
|
8840
|
+
fi
|
|
8841
|
+
|
|
8842
|
+
if [ "$do_friction_map" = "true" ]; then
|
|
8843
|
+
local friction_file="${codebase_path:-.}/.loki/healing/friction-map.json"
|
|
8844
|
+
if [[ ! -f "$friction_file" ]]; then
|
|
8845
|
+
echo -e "${YELLOW}No friction map found.${NC}"
|
|
8846
|
+
echo "Run archaeology first: loki heal ${codebase_path:-.} --phase archaeology"
|
|
8847
|
+
return 0
|
|
8848
|
+
fi
|
|
8849
|
+
echo -e "${BOLD}Friction Map${NC}"
|
|
8850
|
+
echo ""
|
|
8851
|
+
python3 -c "
|
|
8852
|
+
import json, sys
|
|
8853
|
+
with open(sys.argv[1]) as f:
|
|
8854
|
+
data = json.load(f)
|
|
8855
|
+
for f in data.get('frictions', []):
|
|
8856
|
+
safe = 'YES' if f.get('safe_to_remove') else 'NO'
|
|
8857
|
+
cls = f.get('classification', 'unknown')
|
|
8858
|
+
print(f\" [{f.get('id', '?')}] {f.get('location', '?')}\")
|
|
8859
|
+
print(f\" Behavior: {f.get('behavior', '?')}\")
|
|
8860
|
+
print(f\" Classification: {cls}\")
|
|
8861
|
+
print(f\" Safe to remove: {safe}\")
|
|
8862
|
+
if f.get('evidence'):
|
|
8863
|
+
print(f\" Evidence: {f.get('evidence')}\")
|
|
8864
|
+
print()
|
|
8865
|
+
" "$friction_file" 2>/dev/null || echo " Error reading friction map."
|
|
8866
|
+
return 0
|
|
8867
|
+
fi
|
|
8868
|
+
|
|
8869
|
+
if [ "$do_report" = "true" ]; then
|
|
8870
|
+
local heal_dir="${codebase_path:-.}/.loki/healing"
|
|
8871
|
+
if [[ ! -d "$heal_dir" ]]; then
|
|
8872
|
+
echo -e "${YELLOW}No healing session found.${NC}"
|
|
8873
|
+
return 0
|
|
8874
|
+
fi
|
|
8875
|
+
echo -e "${BOLD}Healing Report${NC}"
|
|
8876
|
+
echo ""
|
|
8877
|
+
echo " Friction map: $(python3 -c "import json; print(len(json.load(open('$heal_dir/friction-map.json')).get('frictions', [])))" 2>/dev/null || echo '0') points"
|
|
8878
|
+
echo " Failure modes: $(python3 -c "import json; print(len(json.load(open('$heal_dir/failure-modes.json')).get('modes', [])))" 2>/dev/null || echo '0') cataloged"
|
|
8879
|
+
echo " Institutional knowledge: $(wc -l < "$heal_dir/institutional-knowledge.md" 2>/dev/null || echo '0') lines"
|
|
8880
|
+
echo " Characterization tests: $(find "$heal_dir/characterization-tests/" -name "*.json" 2>/dev/null | wc -l | tr -d ' ') tests"
|
|
8881
|
+
echo ""
|
|
8882
|
+
return 0
|
|
8883
|
+
fi
|
|
8884
|
+
|
|
8885
|
+
# Validate codebase path
|
|
8886
|
+
if [ -z "$codebase_path" ]; then
|
|
8887
|
+
echo -e "${RED}Error: Codebase path is required${NC}"
|
|
8888
|
+
echo "Usage: loki heal <path-to-codebase> [options]"
|
|
8889
|
+
return 1
|
|
8890
|
+
fi
|
|
8891
|
+
|
|
8892
|
+
if [[ ! -d "$codebase_path" ]]; then
|
|
8893
|
+
echo -e "${RED}Error: Directory not found: $codebase_path${NC}"
|
|
8894
|
+
return 1
|
|
8895
|
+
fi
|
|
8896
|
+
|
|
8897
|
+
# Validate phase
|
|
8898
|
+
case "$phase" in
|
|
8899
|
+
archaeology|stabilize|isolate|modernize|validate) ;;
|
|
8900
|
+
*)
|
|
8901
|
+
echo -e "${RED}Error: Invalid phase: $phase${NC}"
|
|
8902
|
+
echo "Valid phases: archaeology, stabilize, isolate, modernize, validate"
|
|
8903
|
+
return 1
|
|
8904
|
+
;;
|
|
8905
|
+
esac
|
|
8906
|
+
|
|
8907
|
+
# Initialize healing directory
|
|
8908
|
+
local heal_dir="$codebase_path/.loki/healing"
|
|
8909
|
+
mkdir -p "$heal_dir"/{behavioral-baseline,characterization-tests}
|
|
8910
|
+
|
|
8911
|
+
# Initialize healing state files if they don't exist
|
|
8912
|
+
[[ ! -f "$heal_dir/friction-map.json" ]] && echo '{"frictions":[]}' > "$heal_dir/friction-map.json"
|
|
8913
|
+
[[ ! -f "$heal_dir/failure-modes.json" ]] && echo '{"modes":[]}' > "$heal_dir/failure-modes.json"
|
|
8914
|
+
[[ ! -f "$heal_dir/institutional-knowledge.md" ]] && echo "# Institutional Knowledge Registry" > "$heal_dir/institutional-knowledge.md"
|
|
8915
|
+
|
|
8916
|
+
# Initialize or update healing progress
|
|
8917
|
+
if [[ ! -f "$heal_dir/healing-progress.json" ]] || [ "$do_resume" != "true" ]; then
|
|
8918
|
+
python3 -c "
|
|
8919
|
+
import json
|
|
8920
|
+
from datetime import datetime
|
|
8921
|
+
progress = {
|
|
8922
|
+
'codebase': '$codebase_path',
|
|
8923
|
+
'started': datetime.now().isoformat(),
|
|
8924
|
+
'current_phase': '$phase',
|
|
8925
|
+
'strict_mode': $( [ "$strict" = "true" ] && echo "True" || echo "False" ),
|
|
8926
|
+
'components': [],
|
|
8927
|
+
'overall_health': 0.0
|
|
8928
|
+
}
|
|
8929
|
+
with open('$heal_dir/healing-progress.json', 'w') as f:
|
|
8930
|
+
json.dump(progress, f, indent=2)
|
|
8931
|
+
" || true
|
|
8932
|
+
fi
|
|
8933
|
+
|
|
8934
|
+
emit_event healing cli start "phase=$phase" "codebase=$codebase_path" "strict=$strict" 2>/dev/null || true
|
|
8935
|
+
|
|
8936
|
+
echo -e "${BOLD}Legacy System Healing${NC}"
|
|
8937
|
+
echo ""
|
|
8938
|
+
echo -e " Codebase: $codebase_path"
|
|
8939
|
+
echo -e " Phase: $phase"
|
|
8940
|
+
echo -e " Strict: $strict"
|
|
8941
|
+
echo -e " Provider: $provider"
|
|
8942
|
+
if [ "$archaeology_only" = "true" ]; then
|
|
8943
|
+
echo -e " Mode: archaeology-only (no modifications)"
|
|
8944
|
+
fi
|
|
8945
|
+
echo ""
|
|
8946
|
+
|
|
8947
|
+
if [ "$dry_run" = "true" ]; then
|
|
8948
|
+
echo -e "${CYAN}Dry run: Showing healing plan without executing${NC}"
|
|
8949
|
+
echo ""
|
|
8950
|
+
echo " Phase 1: Archaeology"
|
|
8951
|
+
echo " - Map dependency graph"
|
|
8952
|
+
echo " - Scan for friction points (sleeps, retries, magic values)"
|
|
8953
|
+
echo " - Extract institutional knowledge from comments"
|
|
8954
|
+
echo " - Write characterization tests for critical paths"
|
|
8955
|
+
echo ""
|
|
8956
|
+
echo " Phase 2: Stabilize"
|
|
8957
|
+
echo " - Add logging/observability without behavior changes"
|
|
8958
|
+
echo " - Extract hardcoded config values"
|
|
8959
|
+
echo " - Add type annotations where possible"
|
|
8960
|
+
echo ""
|
|
8961
|
+
echo " Phase 3: Isolate"
|
|
8962
|
+
echo " - Define component boundaries"
|
|
8963
|
+
echo " - Create adapter interfaces"
|
|
8964
|
+
echo " - Add integration tests at boundaries"
|
|
8965
|
+
echo ""
|
|
8966
|
+
echo " Phase 4: Modernize"
|
|
8967
|
+
echo " - Replace components one at a time behind adapters"
|
|
8968
|
+
echo " - Verify characterization tests after each replacement"
|
|
8969
|
+
echo ""
|
|
8970
|
+
echo " Phase 5: Validate"
|
|
8971
|
+
echo " - Compare outputs with pre-healing baseline"
|
|
8972
|
+
echo " - Generate healing report"
|
|
8973
|
+
return 0
|
|
8974
|
+
fi
|
|
8975
|
+
|
|
8976
|
+
# Build healing prompt for the AI provider
|
|
8977
|
+
local heal_prompt="You are performing legacy system healing on the codebase at '${codebase_path}'.
|
|
8978
|
+
|
|
8979
|
+
IMPORTANT: Read skills/healing.md for the full healing protocol.
|
|
8980
|
+
|
|
8981
|
+
Current phase: ${phase}
|
|
8982
|
+
Strict mode: ${strict}
|
|
8983
|
+
Archaeology only: ${archaeology_only}
|
|
8984
|
+
|
|
8985
|
+
HEALING RULES:
|
|
8986
|
+
1. FRICTION IS SEMANTICS: Before removing any 'quirky' code (sleeps, retries, magic values), verify it is not an undocumented business rule. Document all friction in .loki/healing/friction-map.json.
|
|
8987
|
+
2. LEARN THROUGH FAILURE: Deliberately probe error paths and edge cases. Catalog every failure mode in .loki/healing/failure-modes.json. Each failure teaches you about the system.
|
|
8988
|
+
3. CHARACTERIZE BEFORE MODIFYING: Write tests that capture CURRENT behavior (not intended behavior) before changing anything.
|
|
8989
|
+
4. PRESERVE INSTITUTIONAL LOGIC: Extract business rules from comments, git history, error messages, and test fixtures into .loki/healing/institutional-knowledge.md.
|
|
8990
|
+
5. INCREMENTAL ONLY: Change ONE component at a time. Verify ALL characterization tests pass after each change.
|
|
8991
|
+
|
|
8992
|
+
Phase-specific instructions:
|
|
8993
|
+
- archaeology: Map dependencies, catalog friction, extract knowledge, write characterization tests. Do NOT modify source code.
|
|
8994
|
+
- stabilize: Add logging and observability. Extract config. Add type hints. Do NOT change behavior.
|
|
8995
|
+
- isolate: Create adapter interfaces at component boundaries. Add integration tests.
|
|
8996
|
+
- modernize: Replace components behind adapters. Verify characterization tests pass.
|
|
8997
|
+
- validate: Compare outputs with behavioral baseline. Generate healing report.
|
|
8998
|
+
|
|
8999
|
+
$([ "$strict" = "true" ] && echo "STRICT MODE: You MUST NOT change any observable behavior without creating a .loki/signals/HUMAN_REVIEW_NEEDED signal and waiting for approval.")
|
|
9000
|
+
$([ "$archaeology_only" = "true" ] && echo "ARCHAEOLOGY ONLY: Do NOT modify any source files. Only create files in .loki/healing/.")
|
|
9001
|
+
|
|
9002
|
+
Begin healing now. Follow the RARV cycle for each action."
|
|
9003
|
+
|
|
9004
|
+
# Execute with the appropriate provider
|
|
9005
|
+
local heal_exit=0
|
|
9006
|
+
case "$provider" in
|
|
9007
|
+
claude)
|
|
9008
|
+
local run_args=(--dangerously-skip-permissions -p "$heal_prompt" --output-format stream-json --verbose)
|
|
9009
|
+
(cd "$codebase_path" && claude "${run_args[@]}" 2>&1) | \
|
|
9010
|
+
while IFS= read -r line; do
|
|
9011
|
+
if echo "$line" | python3 -c "
|
|
9012
|
+
import sys, json
|
|
9013
|
+
try:
|
|
9014
|
+
d = json.loads(sys.stdin.read())
|
|
9015
|
+
if d.get('type') == 'assistant':
|
|
9016
|
+
for item in d.get('message', {}).get('content', []):
|
|
9017
|
+
if item.get('type') == 'text':
|
|
9018
|
+
print(item.get('text', ''), end='')
|
|
9019
|
+
except Exception: pass
|
|
9020
|
+
" 2>/dev/null; then
|
|
9021
|
+
true
|
|
9022
|
+
fi
|
|
9023
|
+
done && heal_exit=0 || heal_exit=$?
|
|
9024
|
+
;;
|
|
9025
|
+
codex)
|
|
9026
|
+
(cd "$codebase_path" && codex exec --full-auto "$heal_prompt" 2>&1) || heal_exit=$?
|
|
9027
|
+
;;
|
|
9028
|
+
gemini)
|
|
9029
|
+
(cd "$codebase_path" && gemini --approval-mode=yolo "$heal_prompt" 2>&1) || heal_exit=$?
|
|
9030
|
+
;;
|
|
9031
|
+
cline)
|
|
9032
|
+
(cd "$codebase_path" && cline -y "$heal_prompt" 2>&1) || heal_exit=$?
|
|
9033
|
+
;;
|
|
9034
|
+
aider)
|
|
9035
|
+
local aider_model="${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
|
|
9036
|
+
local aider_flags="${LOKI_AIDER_FLAGS:-}"
|
|
9037
|
+
# shellcheck disable=SC2086
|
|
9038
|
+
(cd "$codebase_path" && aider --message "$heal_prompt" --yes-always --no-auto-commits --model "$aider_model" $aider_flags 2>&1) || heal_exit=$?
|
|
9039
|
+
;;
|
|
9040
|
+
esac
|
|
9041
|
+
|
|
9042
|
+
emit_event healing cli complete "phase=$phase" "exit=$heal_exit" 2>/dev/null || true
|
|
9043
|
+
|
|
9044
|
+
if [ "$heal_exit" -eq 0 ]; then
|
|
9045
|
+
echo ""
|
|
9046
|
+
echo -e "${GREEN}Healing phase '${phase}' complete.${NC}"
|
|
9047
|
+
echo -e "View progress: loki heal --status ${codebase_path}"
|
|
9048
|
+
echo -e "View friction: loki heal --friction-map ${codebase_path}"
|
|
9049
|
+
else
|
|
9050
|
+
echo ""
|
|
9051
|
+
echo -e "${YELLOW}Healing phase '${phase}' finished with warnings (exit: $heal_exit).${NC}"
|
|
9052
|
+
echo -e "Check: loki heal --status ${codebase_path}"
|
|
9053
|
+
fi
|
|
9054
|
+
}
|
|
9055
|
+
|
|
8603
9056
|
cmd_migrate() {
|
|
8604
9057
|
local codebase_path=""
|
|
8605
9058
|
local target=""
|
|
@@ -10149,6 +10602,9 @@ main() {
|
|
|
10149
10602
|
optimize)
|
|
10150
10603
|
cmd_optimize "$@"
|
|
10151
10604
|
;;
|
|
10605
|
+
heal)
|
|
10606
|
+
cmd_heal "$@"
|
|
10607
|
+
;;
|
|
10152
10608
|
migrate)
|
|
10153
10609
|
cmd_migrate "$@"
|
|
10154
10610
|
;;
|
|
@@ -10203,6 +10659,12 @@ main() {
|
|
|
10203
10659
|
share)
|
|
10204
10660
|
cmd_share "$@"
|
|
10205
10661
|
;;
|
|
10662
|
+
context|ctx)
|
|
10663
|
+
cmd_context "$@"
|
|
10664
|
+
;;
|
|
10665
|
+
code)
|
|
10666
|
+
cmd_code "$@"
|
|
10667
|
+
;;
|
|
10206
10668
|
version|--version|-v)
|
|
10207
10669
|
cmd_version
|
|
10208
10670
|
;;
|
|
@@ -14949,6 +15411,561 @@ METRICS_SCRIPT
|
|
|
14949
15411
|
fi
|
|
14950
15412
|
}
|
|
14951
15413
|
|
|
15414
|
+
# Context window management (inspired by Kiro CLI /context command)
|
|
15415
|
+
# Shows token usage breakdown and context window utilization
|
|
15416
|
+
cmd_context() {
|
|
15417
|
+
local subcommand="${1:-show}"
|
|
15418
|
+
shift 2>/dev/null || true
|
|
15419
|
+
|
|
15420
|
+
case "$subcommand" in
|
|
15421
|
+
--help|-h|help)
|
|
15422
|
+
echo -e "${BOLD}loki context${NC} - Context window management"
|
|
15423
|
+
echo ""
|
|
15424
|
+
echo "Usage: loki context <subcommand>"
|
|
15425
|
+
echo ""
|
|
15426
|
+
echo "Subcommands:"
|
|
15427
|
+
echo " show Show context window usage breakdown (default)"
|
|
15428
|
+
echo " files List files currently in context with token estimates"
|
|
15429
|
+
echo " tools Show tool token usage per origin (MCP, native)"
|
|
15430
|
+
echo " add Add file to context (@file reference expansion)"
|
|
15431
|
+
echo " clear Clear accumulated context (start fresh conversation)"
|
|
15432
|
+
echo ""
|
|
15433
|
+
echo "Examples:"
|
|
15434
|
+
echo " loki context # Show context usage"
|
|
15435
|
+
echo " loki context files # List context files"
|
|
15436
|
+
echo " loki context tools # Show tool token costs"
|
|
15437
|
+
echo " loki context add src/main.py # Add file to context"
|
|
15438
|
+
echo ""
|
|
15439
|
+
echo "Inspired by: Kiro CLI /context command (kiro.dev)"
|
|
15440
|
+
return 0
|
|
15441
|
+
;;
|
|
15442
|
+
show)
|
|
15443
|
+
_context_show "$@"
|
|
15444
|
+
;;
|
|
15445
|
+
files)
|
|
15446
|
+
_context_files "$@"
|
|
15447
|
+
;;
|
|
15448
|
+
tools)
|
|
15449
|
+
_context_tools "$@"
|
|
15450
|
+
;;
|
|
15451
|
+
add)
|
|
15452
|
+
_context_add "$@"
|
|
15453
|
+
;;
|
|
15454
|
+
clear)
|
|
15455
|
+
_context_clear "$@"
|
|
15456
|
+
;;
|
|
15457
|
+
*)
|
|
15458
|
+
echo -e "${RED}Unknown subcommand: $subcommand${NC}"
|
|
15459
|
+
echo "Run 'loki context help' for usage."
|
|
15460
|
+
return 1
|
|
15461
|
+
;;
|
|
15462
|
+
esac
|
|
15463
|
+
}
|
|
15464
|
+
|
|
15465
|
+
_context_show() {
|
|
15466
|
+
local loki_dir="${LOKI_DIR:-.loki}"
|
|
15467
|
+
|
|
15468
|
+
if [ ! -d "$loki_dir" ]; then
|
|
15469
|
+
echo -e "${YELLOW}No active session.${NC}"
|
|
15470
|
+
return 0
|
|
15471
|
+
fi
|
|
15472
|
+
|
|
15473
|
+
section_header "Context Window Usage" 2>/dev/null || echo -e "\n${BOLD}Context Window Usage${NC}"
|
|
15474
|
+
|
|
15475
|
+
# Read context tracker data if available
|
|
15476
|
+
local ctx_file="$loki_dir/state/context-usage.json"
|
|
15477
|
+
if [ -f "$ctx_file" ]; then
|
|
15478
|
+
local total_tokens used_tokens input_tokens output_tokens cache_tokens
|
|
15479
|
+
total_tokens=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(d.get('window_size', 200000))" 2>/dev/null || echo "200000")
|
|
15480
|
+
used_tokens=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(d.get('used_tokens', 0))" 2>/dev/null || echo "0")
|
|
15481
|
+
input_tokens=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(d.get('input_tokens', 0))" 2>/dev/null || echo "0")
|
|
15482
|
+
output_tokens=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(d.get('output_tokens', 0))" 2>/dev/null || echo "0")
|
|
15483
|
+
cache_tokens=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(d.get('cache_read_tokens', 0))" 2>/dev/null || echo "0")
|
|
15484
|
+
|
|
15485
|
+
# Display gauge
|
|
15486
|
+
if type context_gauge &>/dev/null; then
|
|
15487
|
+
context_gauge "$used_tokens" "$total_tokens" "Window"
|
|
15488
|
+
else
|
|
15489
|
+
local pct=$((used_tokens * 100 / total_tokens))
|
|
15490
|
+
echo -e " ${CYAN}Context:${NC} ${pct}% used (${used_tokens} / ${total_tokens} tokens)"
|
|
15491
|
+
fi
|
|
15492
|
+
echo ""
|
|
15493
|
+
|
|
15494
|
+
# Breakdown
|
|
15495
|
+
echo -e " ${BOLD}Breakdown${NC} ${DIM}(estimated)${NC}"
|
|
15496
|
+
echo -e " ${DIM}Input tokens:${NC} $(printf '%8s' "$input_tokens")"
|
|
15497
|
+
echo -e " ${DIM}Output tokens:${NC} $(printf '%8s' "$output_tokens")"
|
|
15498
|
+
echo -e " ${DIM}Cache reads:${NC} $(printf '%8s' "$cache_tokens")"
|
|
15499
|
+
else
|
|
15500
|
+
echo -e " ${DIM}No context tracking data yet.${NC}"
|
|
15501
|
+
echo -e " ${DIM}Context usage is tracked during active sessions.${NC}"
|
|
15502
|
+
fi
|
|
15503
|
+
|
|
15504
|
+
# Show token costs if budget file exists
|
|
15505
|
+
local budget_file="$loki_dir/metrics/budget.json"
|
|
15506
|
+
if [ -f "$budget_file" ]; then
|
|
15507
|
+
echo ""
|
|
15508
|
+
echo -e " ${BOLD}Cost${NC}"
|
|
15509
|
+
local budget_used budget_limit
|
|
15510
|
+
budget_used=$(python3 -c "import json; d=json.load(open('$budget_file')); print(round(d.get('budget_used', 0), 4))" 2>/dev/null || echo "0")
|
|
15511
|
+
budget_limit=$(python3 -c "import json; d=json.load(open('$budget_file')); print(d.get('budget_limit', 0))" 2>/dev/null || echo "0")
|
|
15512
|
+
|
|
15513
|
+
if [ "$budget_limit" != "0" ]; then
|
|
15514
|
+
echo -e " ${DIM}Spent:${NC} \$${budget_used} / \$${budget_limit}"
|
|
15515
|
+
local remaining
|
|
15516
|
+
remaining=$(echo "scale=4; $budget_limit - $budget_used" | bc 2>/dev/null || echo "0")
|
|
15517
|
+
echo -e " ${DIM}Remaining:${NC} \$${remaining}"
|
|
15518
|
+
else
|
|
15519
|
+
echo -e " ${DIM}Spent:${NC} \$${budget_used} (no budget limit set)"
|
|
15520
|
+
fi
|
|
15521
|
+
fi
|
|
15522
|
+
echo ""
|
|
15523
|
+
}
|
|
15524
|
+
|
|
15525
|
+
_context_files() {
|
|
15526
|
+
local loki_dir="${LOKI_DIR:-.loki}"
|
|
15527
|
+
local ctx_files="$loki_dir/state/context-files.json"
|
|
15528
|
+
|
|
15529
|
+
section_header "Context Files" 2>/dev/null || echo -e "\n${BOLD}Context Files${NC}"
|
|
15530
|
+
|
|
15531
|
+
if [ -f "$ctx_files" ]; then
|
|
15532
|
+
python3 << 'PYEOF'
|
|
15533
|
+
import json, os
|
|
15534
|
+
|
|
15535
|
+
ctx_file = os.environ.get("LOKI_DIR", ".loki") + "/state/context-files.json"
|
|
15536
|
+
try:
|
|
15537
|
+
with open(ctx_file) as f:
|
|
15538
|
+
files = json.load(f)
|
|
15539
|
+
total = 0
|
|
15540
|
+
for entry in files:
|
|
15541
|
+
name = entry.get("path", "unknown")
|
|
15542
|
+
tokens = entry.get("estimated_tokens", 0)
|
|
15543
|
+
total += tokens
|
|
15544
|
+
print(f" {tokens:>8} tokens {name}")
|
|
15545
|
+
print(f"\n {'Total:':>8} {total} tokens")
|
|
15546
|
+
except Exception:
|
|
15547
|
+
print(" No context files tracked yet.")
|
|
15548
|
+
PYEOF
|
|
15549
|
+
else
|
|
15550
|
+
echo -e " ${DIM}No context files tracked.${NC}"
|
|
15551
|
+
echo ""
|
|
15552
|
+
echo " Add files with: loki context add <file>"
|
|
15553
|
+
echo " Or use @path syntax in your prompt:"
|
|
15554
|
+
echo " @src/main.py - inject file contents"
|
|
15555
|
+
echo " @src/ - inject directory tree"
|
|
15556
|
+
fi
|
|
15557
|
+
echo ""
|
|
15558
|
+
}
|
|
15559
|
+
|
|
15560
|
+
_context_tools() {
|
|
15561
|
+
local loki_dir="${LOKI_DIR:-.loki}"
|
|
15562
|
+
|
|
15563
|
+
section_header "Tool Token Usage" 2>/dev/null || echo -e "\n${BOLD}Tool Token Usage${NC}"
|
|
15564
|
+
|
|
15565
|
+
# Read MCP config and estimate tool token usage
|
|
15566
|
+
local mcp_config="$loki_dir/mcp/config.json"
|
|
15567
|
+
if [ -f "$mcp_config" ]; then
|
|
15568
|
+
python3 << 'PYEOF'
|
|
15569
|
+
import json, os
|
|
15570
|
+
|
|
15571
|
+
config_file = os.environ.get("LOKI_DIR", ".loki") + "/mcp/config.json"
|
|
15572
|
+
try:
|
|
15573
|
+
with open(config_file) as f:
|
|
15574
|
+
config = json.load(f)
|
|
15575
|
+
servers = config.get("mcpServers", {})
|
|
15576
|
+
total = 0
|
|
15577
|
+
for name, server in servers.items():
|
|
15578
|
+
tools = server.get("tools", [])
|
|
15579
|
+
# Estimate ~500 tokens per tool definition
|
|
15580
|
+
est = len(tools) * 500 if tools else 500
|
|
15581
|
+
total += est
|
|
15582
|
+
print(f" {est:>6} tokens {name} ({len(tools) if tools else '?'} tools)")
|
|
15583
|
+
print(f"\n Total MCP tool overhead: ~{total} tokens per request")
|
|
15584
|
+
except Exception:
|
|
15585
|
+
print(" No MCP servers configured.")
|
|
15586
|
+
PYEOF
|
|
15587
|
+
else
|
|
15588
|
+
echo -e " ${DIM}Native tools only (no MCP servers configured).${NC}"
|
|
15589
|
+
fi
|
|
15590
|
+
|
|
15591
|
+
echo ""
|
|
15592
|
+
echo -e " ${DIM}Native tools: ~2000 tokens (bash, read, write, glob, grep)${NC}"
|
|
15593
|
+
echo -e " ${DIM}Tip: Large MCP tool descriptions impact performance.${NC}"
|
|
15594
|
+
echo ""
|
|
15595
|
+
}
|
|
15596
|
+
|
|
15597
|
+
_context_add() {
|
|
15598
|
+
local file_path="$1"
|
|
15599
|
+
if [ -z "$file_path" ]; then
|
|
15600
|
+
echo -e "${RED}Usage: loki context add <file-or-dir>${NC}"
|
|
15601
|
+
echo ""
|
|
15602
|
+
echo "Adds a file's content (or directory tree) to the context."
|
|
15603
|
+
echo "Equivalent to @path inline reference syntax."
|
|
15604
|
+
return 1
|
|
15605
|
+
fi
|
|
15606
|
+
|
|
15607
|
+
# Strip @ prefix if user types it
|
|
15608
|
+
file_path="${file_path#@}"
|
|
15609
|
+
|
|
15610
|
+
if [ -d "$file_path" ]; then
|
|
15611
|
+
echo -e "${BOLD}Directory tree: ${file_path}${NC}"
|
|
15612
|
+
echo ""
|
|
15613
|
+
if type tree_display &>/dev/null; then
|
|
15614
|
+
tree_display "$file_path"
|
|
15615
|
+
else
|
|
15616
|
+
find "$file_path" -maxdepth 3 -not -path '*/\.*' | head -50
|
|
15617
|
+
fi
|
|
15618
|
+
echo ""
|
|
15619
|
+
|
|
15620
|
+
# Count files and estimate tokens
|
|
15621
|
+
local file_count
|
|
15622
|
+
file_count=$(find "$file_path" -type f -not -path '*/\.*' | wc -l)
|
|
15623
|
+
echo -e "${DIM}$file_count files in directory${NC}"
|
|
15624
|
+
elif [ -f "$file_path" ]; then
|
|
15625
|
+
local size
|
|
15626
|
+
size=$(wc -c < "$file_path")
|
|
15627
|
+
# Rough estimate: 1 token per 4 bytes
|
|
15628
|
+
local est_tokens=$((size / 4))
|
|
15629
|
+
local lines
|
|
15630
|
+
lines=$(wc -l < "$file_path")
|
|
15631
|
+
|
|
15632
|
+
echo -e "${BOLD}File: ${file_path}${NC}"
|
|
15633
|
+
echo -e " ${DIM}Size:${NC} $size bytes"
|
|
15634
|
+
echo -e " ${DIM}Lines:${NC} $lines"
|
|
15635
|
+
echo -e " ${DIM}Tokens:${NC} ~$est_tokens (estimated)"
|
|
15636
|
+
|
|
15637
|
+
# Store in context files list
|
|
15638
|
+
local loki_dir="${LOKI_DIR:-.loki}"
|
|
15639
|
+
mkdir -p "$loki_dir/state"
|
|
15640
|
+
local ctx_files="$loki_dir/state/context-files.json"
|
|
15641
|
+
python3 -c "
|
|
15642
|
+
import json, os
|
|
15643
|
+
path = '$file_path'
|
|
15644
|
+
tokens = $est_tokens
|
|
15645
|
+
ctx_file = '$ctx_files'
|
|
15646
|
+
try:
|
|
15647
|
+
with open(ctx_file) as f:
|
|
15648
|
+
files = json.load(f)
|
|
15649
|
+
except:
|
|
15650
|
+
files = []
|
|
15651
|
+
# Avoid duplicates
|
|
15652
|
+
files = [f for f in files if f.get('path') != path]
|
|
15653
|
+
files.append({'path': path, 'estimated_tokens': tokens, 'size': $size, 'lines': $lines})
|
|
15654
|
+
with open(ctx_file, 'w') as f:
|
|
15655
|
+
json.dump(files, f, indent=2)
|
|
15656
|
+
" 2>/dev/null
|
|
15657
|
+
|
|
15658
|
+
echo -e " ${GREEN}Added to context.${NC}"
|
|
15659
|
+
else
|
|
15660
|
+
echo -e "${RED}Not found: $file_path${NC}"
|
|
15661
|
+
return 1
|
|
15662
|
+
fi
|
|
15663
|
+
}
|
|
15664
|
+
|
|
15665
|
+
_context_clear() {
|
|
15666
|
+
local loki_dir="${LOKI_DIR:-.loki}"
|
|
15667
|
+
rm -f "$loki_dir/state/context-files.json"
|
|
15668
|
+
rm -f "$loki_dir/state/context-usage.json"
|
|
15669
|
+
echo -e "${GREEN}Context cleared.${NC}"
|
|
15670
|
+
echo -e "${DIM}A new conversation will start fresh.${NC}"
|
|
15671
|
+
}
|
|
15672
|
+
|
|
15673
|
+
# Codebase intelligence (inspired by Kiro CLI /code command)
|
|
15674
|
+
# Provides workspace overview, symbol search, and pattern matching
|
|
15675
|
+
cmd_code() {
|
|
15676
|
+
local subcommand="${1:-help}"
|
|
15677
|
+
shift 2>/dev/null || true
|
|
15678
|
+
|
|
15679
|
+
case "$subcommand" in
|
|
15680
|
+
--help|-h|help)
|
|
15681
|
+
echo -e "${BOLD}loki code${NC} - Codebase intelligence"
|
|
15682
|
+
echo ""
|
|
15683
|
+
echo "Usage: loki code <subcommand>"
|
|
15684
|
+
echo ""
|
|
15685
|
+
echo "Subcommands:"
|
|
15686
|
+
echo " overview Generate workspace overview (file types, LOC, structure)"
|
|
15687
|
+
echo " symbols Search for symbols (functions, classes, variables)"
|
|
15688
|
+
echo " deps Show dependency graph for a file or module"
|
|
15689
|
+
echo " hotspots Find code hotspots (most-changed files, complexity)"
|
|
15690
|
+
echo " diff Show colored diff of recent changes"
|
|
15691
|
+
echo ""
|
|
15692
|
+
echo "Examples:"
|
|
15693
|
+
echo " loki code overview # Full codebase overview"
|
|
15694
|
+
echo " loki code overview --silent # Compact output"
|
|
15695
|
+
echo " loki code symbols 'class.*Service' # Find service classes"
|
|
15696
|
+
echo " loki code deps src/main.py # Show dependencies"
|
|
15697
|
+
echo " loki code hotspots --top 10 # Top 10 changed files"
|
|
15698
|
+
echo " loki code diff # Colored diff of uncommitted changes"
|
|
15699
|
+
echo ""
|
|
15700
|
+
echo "Inspired by: Kiro CLI /code command (kiro.dev)"
|
|
15701
|
+
return 0
|
|
15702
|
+
;;
|
|
15703
|
+
overview)
|
|
15704
|
+
_code_overview "$@"
|
|
15705
|
+
;;
|
|
15706
|
+
symbols)
|
|
15707
|
+
_code_symbols "$@"
|
|
15708
|
+
;;
|
|
15709
|
+
deps)
|
|
15710
|
+
_code_deps "$@"
|
|
15711
|
+
;;
|
|
15712
|
+
hotspots)
|
|
15713
|
+
_code_hotspots "$@"
|
|
15714
|
+
;;
|
|
15715
|
+
diff)
|
|
15716
|
+
_code_diff "$@"
|
|
15717
|
+
;;
|
|
15718
|
+
*)
|
|
15719
|
+
echo -e "${RED}Unknown subcommand: $subcommand${NC}"
|
|
15720
|
+
echo "Run 'loki code help' for usage."
|
|
15721
|
+
return 1
|
|
15722
|
+
;;
|
|
15723
|
+
esac
|
|
15724
|
+
}
|
|
15725
|
+
|
|
15726
|
+
_code_overview() {
|
|
15727
|
+
local silent=false
|
|
15728
|
+
local target_dir="."
|
|
15729
|
+
while [[ $# -gt 0 ]]; do
|
|
15730
|
+
case "$1" in
|
|
15731
|
+
--silent|-s) silent=true; shift ;;
|
|
15732
|
+
*) target_dir="$1"; shift ;;
|
|
15733
|
+
esac
|
|
15734
|
+
done
|
|
15735
|
+
|
|
15736
|
+
if $silent; then
|
|
15737
|
+
echo -e "${BOLD}$(basename "$(cd "$target_dir" && pwd)")${NC}"
|
|
15738
|
+
else
|
|
15739
|
+
section_header "Codebase Overview: $(basename "$(cd "$target_dir" && pwd)")" 2>/dev/null || echo -e "\n${BOLD}Codebase Overview${NC}"
|
|
15740
|
+
fi
|
|
15741
|
+
|
|
15742
|
+
# Language breakdown
|
|
15743
|
+
python3 << PYEOF
|
|
15744
|
+
import os, collections, sys
|
|
15745
|
+
|
|
15746
|
+
target = "$target_dir"
|
|
15747
|
+
silent = $( $silent && echo "True" || echo "False" )
|
|
15748
|
+
|
|
15749
|
+
# File extension to language mapping
|
|
15750
|
+
EXT_MAP = {
|
|
15751
|
+
'.py': 'Python', '.js': 'JavaScript', '.ts': 'TypeScript', '.tsx': 'TypeScript',
|
|
15752
|
+
'.jsx': 'JavaScript', '.go': 'Go', '.rs': 'Rust', '.java': 'Java',
|
|
15753
|
+
'.rb': 'Ruby', '.php': 'PHP', '.c': 'C', '.cpp': 'C++', '.h': 'C/C++ Header',
|
|
15754
|
+
'.cs': 'C#', '.swift': 'Swift', '.kt': 'Kotlin', '.scala': 'Scala',
|
|
15755
|
+
'.sh': 'Shell', '.bash': 'Shell', '.zsh': 'Shell',
|
|
15756
|
+
'.html': 'HTML', '.css': 'CSS', '.scss': 'SCSS', '.vue': 'Vue',
|
|
15757
|
+
'.md': 'Markdown', '.json': 'JSON', '.yaml': 'YAML', '.yml': 'YAML',
|
|
15758
|
+
'.toml': 'TOML', '.xml': 'XML', '.sql': 'SQL',
|
|
15759
|
+
'.dockerfile': 'Docker', '.tf': 'Terraform', '.proto': 'Protobuf',
|
|
15760
|
+
}
|
|
15761
|
+
|
|
15762
|
+
SKIP_DIRS = {'.git', 'node_modules', '.loki', '__pycache__', '.venv', 'venv',
|
|
15763
|
+
'dist', 'build', '.next', 'target', '.tox', '.mypy_cache',
|
|
15764
|
+
'vendor', '.cargo', 'coverage', '.pytest_cache'}
|
|
15765
|
+
|
|
15766
|
+
lang_files = collections.Counter()
|
|
15767
|
+
lang_lines = collections.Counter()
|
|
15768
|
+
total_files = 0
|
|
15769
|
+
total_lines = 0
|
|
15770
|
+
dir_count = 0
|
|
15771
|
+
|
|
15772
|
+
for root, dirs, files in os.walk(target):
|
|
15773
|
+
# Skip hidden and build directories
|
|
15774
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith('.')]
|
|
15775
|
+
dir_count += 1
|
|
15776
|
+
for f in files:
|
|
15777
|
+
ext = os.path.splitext(f)[1].lower()
|
|
15778
|
+
if ext in EXT_MAP:
|
|
15779
|
+
lang = EXT_MAP[ext]
|
|
15780
|
+
lang_files[lang] += 1
|
|
15781
|
+
total_files += 1
|
|
15782
|
+
try:
|
|
15783
|
+
fp = os.path.join(root, f)
|
|
15784
|
+
lines = sum(1 for _ in open(fp, 'rb'))
|
|
15785
|
+
lang_lines[lang] += lines
|
|
15786
|
+
total_lines += lines
|
|
15787
|
+
except:
|
|
15788
|
+
pass
|
|
15789
|
+
|
|
15790
|
+
if not silent:
|
|
15791
|
+
print(f"\n Files: {total_files} Lines: {total_lines:,} Dirs: {dir_count}")
|
|
15792
|
+
print()
|
|
15793
|
+
|
|
15794
|
+
# Language table
|
|
15795
|
+
print(" Language Files Lines %")
|
|
15796
|
+
print(" " + "-" * 40)
|
|
15797
|
+
for lang, count in lang_lines.most_common(15):
|
|
15798
|
+
pct = (count / total_lines * 100) if total_lines > 0 else 0
|
|
15799
|
+
files = lang_files[lang]
|
|
15800
|
+
bar_len = int(pct / 5)
|
|
15801
|
+
bar = "=" * bar_len
|
|
15802
|
+
print(f" {lang:<16} {files:>5} {count:>7,} {pct:>4.1f}% {bar}")
|
|
15803
|
+
|
|
15804
|
+
if not silent:
|
|
15805
|
+
# Show directory structure (top-level)
|
|
15806
|
+
print()
|
|
15807
|
+
print(" Top-level structure:")
|
|
15808
|
+
entries = sorted(os.listdir(target))
|
|
15809
|
+
for entry in entries:
|
|
15810
|
+
if entry.startswith('.') and entry not in ('.github',):
|
|
15811
|
+
continue
|
|
15812
|
+
full = os.path.join(target, entry)
|
|
15813
|
+
if os.path.isdir(full) and entry not in SKIP_DIRS:
|
|
15814
|
+
subcount = sum(1 for _, _, fs in os.walk(full) for f in fs)
|
|
15815
|
+
print(f" {entry + '/':30s} ({subcount} files)")
|
|
15816
|
+
elif os.path.isfile(full):
|
|
15817
|
+
size = os.path.getsize(full)
|
|
15818
|
+
if size > 1024:
|
|
15819
|
+
print(f" {entry:30s} ({size // 1024}KB)")
|
|
15820
|
+
PYEOF
|
|
15821
|
+
echo ""
|
|
15822
|
+
}
|
|
15823
|
+
|
|
15824
|
+
_code_symbols() {
|
|
15825
|
+
local pattern="${1:-.}"
|
|
15826
|
+
local file_type="${2:-}"
|
|
15827
|
+
|
|
15828
|
+
section_header "Symbol Search: $pattern" 2>/dev/null || echo -e "\n${BOLD}Symbol Search${NC}"
|
|
15829
|
+
|
|
15830
|
+
# Search for function/class definitions using ripgrep or grep
|
|
15831
|
+
local search_patterns=(
|
|
15832
|
+
"^(def|function|func|fn|pub fn|async def|class|interface|struct|enum|type|export (const|function|class|interface))\s+.*${pattern}"
|
|
15833
|
+
)
|
|
15834
|
+
|
|
15835
|
+
if command -v rg &>/dev/null; then
|
|
15836
|
+
rg -n --no-heading -e "^(def |function |func |fn |pub fn |async def |class |interface |struct |enum |type |export (const|function|class|interface) ).*${pattern}" \
|
|
15837
|
+
--type-add 'code:*.{py,js,ts,tsx,go,rs,java,rb,php,c,cpp,h,cs,swift,kt}' \
|
|
15838
|
+
-t code \
|
|
15839
|
+
--glob '!node_modules' --glob '!.git' --glob '!dist' --glob '!build' \
|
|
15840
|
+
. 2>/dev/null | head -50
|
|
15841
|
+
else
|
|
15842
|
+
grep -rn "^\(def\|function\|func\|fn\|class\|interface\|struct\|enum\|type\).*${pattern}" \
|
|
15843
|
+
--include='*.py' --include='*.js' --include='*.ts' --include='*.go' \
|
|
15844
|
+
--include='*.rs' --include='*.java' --include='*.rb' \
|
|
15845
|
+
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist \
|
|
15846
|
+
. 2>/dev/null | head -50
|
|
15847
|
+
fi
|
|
15848
|
+
echo ""
|
|
15849
|
+
}
|
|
15850
|
+
|
|
15851
|
+
_code_deps() {
|
|
15852
|
+
local target_file="$1"
|
|
15853
|
+
if [ -z "$target_file" ]; then
|
|
15854
|
+
echo -e "${RED}Usage: loki code deps <file>${NC}"
|
|
15855
|
+
return 1
|
|
15856
|
+
fi
|
|
15857
|
+
|
|
15858
|
+
section_header "Dependencies: $target_file" 2>/dev/null || echo -e "\n${BOLD}Dependencies${NC}"
|
|
15859
|
+
|
|
15860
|
+
if [ ! -f "$target_file" ]; then
|
|
15861
|
+
echo -e "${RED}File not found: $target_file${NC}"
|
|
15862
|
+
return 1
|
|
15863
|
+
fi
|
|
15864
|
+
|
|
15865
|
+
local ext="${target_file##*.}"
|
|
15866
|
+
echo -e " ${BOLD}Imports/Requires:${NC}"
|
|
15867
|
+
|
|
15868
|
+
case "$ext" in
|
|
15869
|
+
py)
|
|
15870
|
+
grep -n "^import\|^from.*import" "$target_file" 2>/dev/null | sed 's/^/ /'
|
|
15871
|
+
;;
|
|
15872
|
+
js|ts|tsx|jsx)
|
|
15873
|
+
grep -n "import\|require(" "$target_file" 2>/dev/null | sed 's/^/ /'
|
|
15874
|
+
;;
|
|
15875
|
+
go)
|
|
15876
|
+
grep -n "\"" "$target_file" 2>/dev/null | grep -v "//" | sed 's/^/ /'
|
|
15877
|
+
;;
|
|
15878
|
+
rs)
|
|
15879
|
+
grep -n "^use\|^extern crate" "$target_file" 2>/dev/null | sed 's/^/ /'
|
|
15880
|
+
;;
|
|
15881
|
+
java)
|
|
15882
|
+
grep -n "^import" "$target_file" 2>/dev/null | sed 's/^/ /'
|
|
15883
|
+
;;
|
|
15884
|
+
rb)
|
|
15885
|
+
grep -n "^require\|^require_relative" "$target_file" 2>/dev/null | sed 's/^/ /'
|
|
15886
|
+
;;
|
|
15887
|
+
*)
|
|
15888
|
+
echo -e " ${DIM}Unsupported file type: .$ext${NC}"
|
|
15889
|
+
;;
|
|
15890
|
+
esac
|
|
15891
|
+
|
|
15892
|
+
echo ""
|
|
15893
|
+
echo -e " ${BOLD}Referenced by:${NC}"
|
|
15894
|
+
local basename_file
|
|
15895
|
+
basename_file=$(basename "$target_file" ".$ext")
|
|
15896
|
+
if command -v rg &>/dev/null; then
|
|
15897
|
+
rg -l "$basename_file" --glob '!node_modules' --glob '!.git' --glob '!dist' . 2>/dev/null | \
|
|
15898
|
+
grep -v "^${target_file}$" | head -20 | sed 's/^/ /'
|
|
15899
|
+
else
|
|
15900
|
+
grep -rl "$basename_file" --exclude-dir=node_modules --exclude-dir=.git . 2>/dev/null | \
|
|
15901
|
+
grep -v "^${target_file}$" | head -20 | sed 's/^/ /'
|
|
15902
|
+
fi
|
|
15903
|
+
echo ""
|
|
15904
|
+
}
|
|
15905
|
+
|
|
15906
|
+
_code_hotspots() {
|
|
15907
|
+
local top_n=10
|
|
15908
|
+
while [[ $# -gt 0 ]]; do
|
|
15909
|
+
case "$1" in
|
|
15910
|
+
--top) top_n="${2:-10}"; shift 2 ;;
|
|
15911
|
+
--top=*) top_n="${1#*=}"; shift ;;
|
|
15912
|
+
*) shift ;;
|
|
15913
|
+
esac
|
|
15914
|
+
done
|
|
15915
|
+
|
|
15916
|
+
section_header "Code Hotspots (top $top_n)" 2>/dev/null || echo -e "\n${BOLD}Code Hotspots${NC}"
|
|
15917
|
+
|
|
15918
|
+
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
|
|
15919
|
+
echo -e " ${RED}Not a git repository.${NC}"
|
|
15920
|
+
return 1
|
|
15921
|
+
fi
|
|
15922
|
+
|
|
15923
|
+
echo -e " ${BOLD}Most Changed Files (last 90 days):${NC}"
|
|
15924
|
+
echo ""
|
|
15925
|
+
git log --since="90 days ago" --pretty=format: --name-only 2>/dev/null | \
|
|
15926
|
+
sort | uniq -c | sort -rn | head -"$top_n" | \
|
|
15927
|
+
while read -r count file; do
|
|
15928
|
+
[ -z "$file" ] && continue
|
|
15929
|
+
printf " %4d changes %s\n" "$count" "$file"
|
|
15930
|
+
done
|
|
15931
|
+
|
|
15932
|
+
echo ""
|
|
15933
|
+
echo -e " ${BOLD}Largest Files by LOC:${NC}"
|
|
15934
|
+
echo ""
|
|
15935
|
+
find . -type f \( -name '*.py' -o -name '*.js' -o -name '*.ts' -o -name '*.go' \
|
|
15936
|
+
-o -name '*.rs' -o -name '*.java' -o -name '*.sh' \) \
|
|
15937
|
+
-not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' \
|
|
15938
|
+
-exec wc -l {} \; 2>/dev/null | sort -rn | head -"$top_n" | \
|
|
15939
|
+
while read -r lines file; do
|
|
15940
|
+
printf " %6d lines %s\n" "$lines" "$file"
|
|
15941
|
+
done
|
|
15942
|
+
echo ""
|
|
15943
|
+
}
|
|
15944
|
+
|
|
15945
|
+
_code_diff() {
|
|
15946
|
+
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
|
|
15947
|
+
echo -e "${RED}Not a git repository.${NC}"
|
|
15948
|
+
return 1
|
|
15949
|
+
fi
|
|
15950
|
+
|
|
15951
|
+
# Use delta if available, otherwise colored diff
|
|
15952
|
+
if command -v delta &>/dev/null; then
|
|
15953
|
+
git diff "$@" | delta
|
|
15954
|
+
else
|
|
15955
|
+
git diff "$@" | while IFS= read -r line; do
|
|
15956
|
+
case "$line" in
|
|
15957
|
+
diff*) echo -e "${BOLD}${line}${NC}" ;;
|
|
15958
|
+
---*) echo -e "${BOLD}${line}${NC}" ;;
|
|
15959
|
+
+++*) echo -e "${BOLD}${line}${NC}" ;;
|
|
15960
|
+
@@*) echo -e "${CYAN}${line}${NC}" ;;
|
|
15961
|
+
+*) echo -e "${GREEN}${line}${NC}" ;;
|
|
15962
|
+
-*) echo -e "${RED}${line}${NC}" ;;
|
|
15963
|
+
*) echo "$line" ;;
|
|
15964
|
+
esac
|
|
15965
|
+
done
|
|
15966
|
+
fi
|
|
15967
|
+
}
|
|
15968
|
+
|
|
14952
15969
|
# Output shell completion scripts
|
|
14953
15970
|
cmd_completions() {
|
|
14954
15971
|
local shell="${1:-bash}"
|