loki-mode 6.60.0 → 6.62.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/app-runner.sh +34 -8
- package/autonomy/completion-council.sh +70 -32
- package/autonomy/issue-parser.sh +4 -7
- package/autonomy/loki +238 -119
- package/autonomy/notification-checker.py +49 -23
- package/autonomy/run.sh +162 -79
- package/autonomy/sandbox.sh +91 -24
- package/bin/loki-mode.js +1 -2
- package/bin/postinstall.js +10 -4
- package/dashboard/__init__.py +1 -1
- package/dashboard/control.py +46 -36
- package/dashboard/database.py +21 -4
- package/dashboard/server.py +107 -78
- package/docs/BUG-AUDIT-v6.61.0.md +957 -0
- package/docs/INSTALLATION.md +2 -2
- package/events/bus.py +129 -28
- package/events/bus.ts +41 -27
- package/events/emit.sh +1 -1
- package/integrations/openclaw/README.md +139 -0
- package/integrations/openclaw/SKILL.md +88 -0
- package/integrations/openclaw/bridge/__init__.py +1 -0
- package/integrations/openclaw/bridge/__main__.py +88 -0
- package/integrations/openclaw/bridge/schema_map.py +180 -0
- package/integrations/openclaw/bridge/watcher.py +100 -0
- package/integrations/openclaw/scripts/format-progress.sh +80 -0
- package/integrations/openclaw/scripts/poll-status.sh +74 -0
- package/integrations/vibe-kanban.md +289 -0
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +96 -73
- package/memory/consolidation.py +21 -6
- package/memory/engine.py +53 -26
- package/memory/layers/index_layer.py +16 -3
- package/memory/layers/timeline_layer.py +16 -3
- package/memory/retrieval.py +4 -1
- package/memory/schemas.py +4 -2
- package/memory/storage.py +25 -4
- package/memory/token_economics.py +9 -2
- package/memory/vector_index.py +2 -2
- package/package.json +3 -1
- package/providers/cline.sh +5 -4
- package/providers/codex.sh +27 -5
- package/providers/gemini.sh +59 -23
- package/providers/loader.sh +3 -2
- package/skills/parallel-workflows.md +9 -7
- package/state/__init__.py +10 -0
- package/state/index.ts +18 -0
- package/state/manager.py +1801 -0
- package/state/manager.ts +1774 -0
- package/state/sqlite_backend.py +188 -0
- package/state/test_manager.py +703 -0
- package/state/test_manager.ts +366 -0
- package/templates/README.md +19 -4
- package/templates/dashboard.md +45 -0
- package/templates/data-pipeline.md +45 -0
- package/templates/game.md +48 -0
- package/templates/microservice.md +49 -0
- package/templates/npm-library.md +42 -0
- package/templates/rest-api.md +170 -33
- package/templates/slack-bot.md +48 -0
- package/templates/web-scraper.md +45 -0
- package/web-app/server.py +360 -191
- package/templates/saas-app.md +0 -42
package/autonomy/loki
CHANGED
|
@@ -696,13 +696,13 @@ cmd_start() {
|
|
|
696
696
|
shift
|
|
697
697
|
;;
|
|
698
698
|
--mirofish)
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
699
|
+
if [[ -n "${2:-}" ]] && [[ "${2:-}" != --* ]]; then
|
|
700
|
+
mirofish_url="$2"
|
|
701
|
+
shift 2
|
|
702
702
|
else
|
|
703
|
+
mirofish_url="http://localhost:5001"
|
|
703
704
|
shift
|
|
704
705
|
fi
|
|
705
|
-
shift
|
|
706
706
|
;;
|
|
707
707
|
--mirofish=*)
|
|
708
708
|
mirofish_url="${1#*=}"
|
|
@@ -1178,7 +1178,7 @@ list_running_sessions() {
|
|
|
1178
1178
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
1179
1179
|
# Avoid duplicates (if also in sessions/)
|
|
1180
1180
|
local dup=false
|
|
1181
|
-
for s in "${sessions[@]}"; do
|
|
1181
|
+
for s in ${sessions[@]+"${sessions[@]}"}; do
|
|
1182
1182
|
if [[ "$s" == "$sid:"* ]]; then dup=true; break; fi
|
|
1183
1183
|
done
|
|
1184
1184
|
if ! $dup; then
|
|
@@ -1186,7 +1186,7 @@ list_running_sessions() {
|
|
|
1186
1186
|
fi
|
|
1187
1187
|
fi
|
|
1188
1188
|
done
|
|
1189
|
-
printf '%s\n' "${sessions[@]}"
|
|
1189
|
+
printf '%s\n' ${sessions[@]+"${sessions[@]}"}
|
|
1190
1190
|
}
|
|
1191
1191
|
|
|
1192
1192
|
# Kill a PID with SIGTERM, wait, then SIGKILL if needed
|
|
@@ -1595,11 +1595,16 @@ cmd_resume() {
|
|
|
1595
1595
|
|
|
1596
1596
|
# Show current status
|
|
1597
1597
|
cmd_status() {
|
|
1598
|
-
# Check for
|
|
1598
|
+
# Check for flags
|
|
1599
1599
|
while [[ $# -gt 0 ]]; do
|
|
1600
1600
|
case "$1" in
|
|
1601
1601
|
--json) cmd_status_json; return $? ;;
|
|
1602
|
-
|
|
1602
|
+
--help|-h) echo "Usage: loki status [--json]"; return 0 ;;
|
|
1603
|
+
*)
|
|
1604
|
+
echo -e "${RED}Unknown flag: $1${NC}"
|
|
1605
|
+
echo "Usage: loki status [--json]"
|
|
1606
|
+
return 1
|
|
1607
|
+
;;
|
|
1603
1608
|
esac
|
|
1604
1609
|
done
|
|
1605
1610
|
|
|
@@ -1691,7 +1696,7 @@ cmd_status() {
|
|
|
1691
1696
|
|
|
1692
1697
|
# Check pending tasks
|
|
1693
1698
|
if [ -f "$LOKI_DIR/queue/pending.json" ]; then
|
|
1694
|
-
local task_count=$(jq '.tasks | length' "$LOKI_DIR/queue/pending.json" 2>/dev/null || echo "0")
|
|
1699
|
+
local task_count=$(jq 'if type == "array" then length elif .tasks then .tasks | length else 0 end' "$LOKI_DIR/queue/pending.json" 2>/dev/null || echo "0")
|
|
1695
1700
|
echo -e "${CYAN}Pending Tasks:${NC} $task_count"
|
|
1696
1701
|
fi
|
|
1697
1702
|
|
|
@@ -2784,24 +2789,26 @@ cmd_dashboard_start() {
|
|
|
2784
2789
|
echo "$host" > "${DASHBOARD_PID_DIR}/host"
|
|
2785
2790
|
echo "$url_scheme" > "${DASHBOARD_PID_DIR}/scheme"
|
|
2786
2791
|
|
|
2787
|
-
# Wait for
|
|
2788
|
-
|
|
2789
|
-
local
|
|
2790
|
-
local
|
|
2791
|
-
while [[ $
|
|
2792
|
-
if
|
|
2792
|
+
# Wait for dashboard HTTP endpoint to become ready (up to 10 seconds)
|
|
2793
|
+
local health_retries=20
|
|
2794
|
+
local health_interval=0.5
|
|
2795
|
+
local health_ok=false
|
|
2796
|
+
while [[ $health_retries -gt 0 ]]; do
|
|
2797
|
+
if curl -sf "http://${host}:${port}/api/status" >/dev/null 2>&1; then
|
|
2798
|
+
health_ok=true
|
|
2793
2799
|
break
|
|
2794
2800
|
fi
|
|
2795
|
-
|
|
2796
|
-
|
|
2801
|
+
# Also check if the process died
|
|
2802
|
+
if ! kill -0 "$new_pid" 2>/dev/null; then
|
|
2803
|
+
break
|
|
2804
|
+
fi
|
|
2805
|
+
sleep "$health_interval"
|
|
2806
|
+
health_retries=$((health_retries - 1))
|
|
2797
2807
|
done
|
|
2798
|
-
if
|
|
2799
|
-
echo -e "${YELLOW}Warning: Dashboard
|
|
2808
|
+
if ! $health_ok && kill -0 "$new_pid" 2>/dev/null; then
|
|
2809
|
+
echo -e "${YELLOW}Warning: Dashboard started but health check did not respond within timeout${NC}"
|
|
2800
2810
|
fi
|
|
2801
2811
|
|
|
2802
|
-
# Wait a moment and check if process is still running
|
|
2803
|
-
sleep 1
|
|
2804
|
-
|
|
2805
2812
|
if kill -0 "$new_pid" 2>/dev/null; then
|
|
2806
2813
|
local url="${url_scheme}://${host}:${port}"
|
|
2807
2814
|
echo -e "${GREEN}Dashboard server started${NC}"
|
|
@@ -3105,6 +3112,7 @@ cmd_web() {
|
|
|
3105
3112
|
;;
|
|
3106
3113
|
*)
|
|
3107
3114
|
# Treat unknown args as options to start (e.g., loki web --no-open)
|
|
3115
|
+
shift
|
|
3108
3116
|
cmd_web_start "$subcommand" "$@"
|
|
3109
3117
|
;;
|
|
3110
3118
|
esac
|
|
@@ -3147,10 +3155,18 @@ cmd_web_start() {
|
|
|
3147
3155
|
shift
|
|
3148
3156
|
;;
|
|
3149
3157
|
--port)
|
|
3158
|
+
if [[ -z "${2:-}" ]]; then
|
|
3159
|
+
echo -e "${RED}--port requires a port number${NC}"
|
|
3160
|
+
exit 1
|
|
3161
|
+
fi
|
|
3150
3162
|
port="$2"
|
|
3151
3163
|
shift 2
|
|
3152
3164
|
;;
|
|
3153
3165
|
--prd)
|
|
3166
|
+
if [[ -z "${2:-}" ]]; then
|
|
3167
|
+
echo -e "${RED}--prd requires a file path${NC}"
|
|
3168
|
+
exit 1
|
|
3169
|
+
fi
|
|
3154
3170
|
prd_file="$2"
|
|
3155
3171
|
shift 2
|
|
3156
3172
|
;;
|
|
@@ -3356,7 +3372,7 @@ cmd_web_stop() {
|
|
|
3356
3372
|
local pids_file="${LOKI_DIR}/purple-lab/child-pids.json"
|
|
3357
3373
|
if [ -f "$pids_file" ]; then
|
|
3358
3374
|
local tracked_pids
|
|
3359
|
-
tracked_pids=$(python3 -c "import json; [print(p) for p in json.load(open('
|
|
3375
|
+
tracked_pids=$(LOKI_PIDS_FILE="$pids_file" python3 -c "import json, os; [print(p) for p in json.load(open(os.environ['LOKI_PIDS_FILE']))]" 2>/dev/null || true)
|
|
3360
3376
|
if [ -n "$tracked_pids" ]; then
|
|
3361
3377
|
echo "Cleaning up Purple Lab build processes..."
|
|
3362
3378
|
for opid in $tracked_pids; do
|
|
@@ -3374,13 +3390,18 @@ cmd_web_stop() {
|
|
|
3374
3390
|
fi
|
|
3375
3391
|
|
|
3376
3392
|
# Also stop the Loki Dashboard if it was started by a Purple Lab session
|
|
3377
|
-
local
|
|
3378
|
-
|
|
3379
|
-
if [ -
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3393
|
+
local dash_port="${LOKI_DASHBOARD_PORT:-57374}"
|
|
3394
|
+
local dash_pid_file="${LOKI_DIR}/dashboard/dashboard.pid"
|
|
3395
|
+
if [ -f "$dash_pid_file" ]; then
|
|
3396
|
+
local dash_pid
|
|
3397
|
+
dash_pid=$(cat "$dash_pid_file" 2>/dev/null)
|
|
3398
|
+
if [ -n "$dash_pid" ] && kill -0 "$dash_pid" 2>/dev/null; then
|
|
3399
|
+
kill "$dash_pid" 2>/dev/null || true
|
|
3400
|
+
sleep 1
|
|
3401
|
+
kill -0 "$dash_pid" 2>/dev/null && kill -9 "$dash_pid" 2>/dev/null || true
|
|
3402
|
+
echo "Loki Dashboard stopped (port $dash_port)"
|
|
3403
|
+
fi
|
|
3404
|
+
rm -f "$dash_pid_file" 2>/dev/null || true
|
|
3384
3405
|
fi
|
|
3385
3406
|
}
|
|
3386
3407
|
|
|
@@ -3536,7 +3557,7 @@ cmd_github() {
|
|
|
3536
3557
|
|
|
3537
3558
|
# gh CLI
|
|
3538
3559
|
if command -v gh &>/dev/null; then
|
|
3539
|
-
echo -e " gh CLI: ${GREEN}installed$(gh --version 2>/dev/null | head -1 | sed 's/gh version /v/')${NC}"
|
|
3560
|
+
echo -e " gh CLI: ${GREEN}installed $(gh --version 2>/dev/null | head -1 | sed 's/gh version /v/')${NC}"
|
|
3540
3561
|
if gh auth status &>/dev/null 2>&1; then
|
|
3541
3562
|
echo -e " Auth: ${GREEN}authenticated${NC}"
|
|
3542
3563
|
else
|
|
@@ -4802,16 +4823,9 @@ cmd_watch() {
|
|
|
4802
4823
|
|
|
4803
4824
|
case "$watcher" in
|
|
4804
4825
|
fswatch)
|
|
4805
|
-
# Use fswatch
|
|
4806
|
-
fswatch -1 --latency "$debounce" "$prd_path" | while read -r _event; do
|
|
4807
|
-
_watch_trigger
|
|
4808
|
-
_watch_status_line
|
|
4809
|
-
# Re-attach fswatch for next change
|
|
4810
|
-
fswatch -1 --latency "$debounce" "$prd_path" > /dev/null 2>&1 || true
|
|
4811
|
-
done
|
|
4812
|
-
# Fallback: loop-based fswatch for continuous watching
|
|
4826
|
+
# Use fswatch with loop-based approach (avoids pipe subshell PID loss)
|
|
4813
4827
|
while true; do
|
|
4814
|
-
fswatch -1 --latency "$debounce" "$prd_path" > /dev/null 2>&1
|
|
4828
|
+
fswatch -1 --latency "$debounce" "$prd_path" > /dev/null 2>&1 || true
|
|
4815
4829
|
_watch_trigger
|
|
4816
4830
|
_watch_status_line
|
|
4817
4831
|
done
|
|
@@ -4920,14 +4934,14 @@ _export_json() {
|
|
|
4920
4934
|
fi
|
|
4921
4935
|
|
|
4922
4936
|
local json_output
|
|
4923
|
-
json_output=$(python3 << 'EXPORT_JSON'
|
|
4937
|
+
json_output=$(LOKI_DIR="$LOKI_DIR" LOKI_VERSION="$(get_version)" python3 << 'EXPORT_JSON'
|
|
4924
4938
|
import json, os, glob
|
|
4925
4939
|
from datetime import datetime
|
|
4926
4940
|
|
|
4927
4941
|
loki_dir = os.environ.get("LOKI_DIR", ".loki")
|
|
4928
4942
|
export = {
|
|
4929
4943
|
"exported_at": datetime.utcnow().isoformat() + "Z",
|
|
4930
|
-
"version": "
|
|
4944
|
+
"version": os.environ.get("LOKI_VERSION", "0.0.0"),
|
|
4931
4945
|
"session": {},
|
|
4932
4946
|
"queue": {},
|
|
4933
4947
|
"quality": {},
|
|
@@ -5045,7 +5059,7 @@ _export_csv() {
|
|
|
5045
5059
|
fi
|
|
5046
5060
|
|
|
5047
5061
|
local csv_output
|
|
5048
|
-
csv_output=$(python3 << 'EXPORT_CSV'
|
|
5062
|
+
csv_output=$(LOKI_DIR="$LOKI_DIR" python3 << 'EXPORT_CSV'
|
|
5049
5063
|
import json, os, csv, io
|
|
5050
5064
|
|
|
5051
5065
|
loki_dir = os.environ.get("LOKI_DIR", ".loki")
|
|
@@ -5058,7 +5072,8 @@ for queue in ["pending", "in-progress", "completed", "failed"]:
|
|
|
5058
5072
|
if os.path.exists(qf):
|
|
5059
5073
|
try:
|
|
5060
5074
|
with open(qf) as f:
|
|
5061
|
-
|
|
5075
|
+
data = json.load(f)
|
|
5076
|
+
tasks = data.get("tasks", data) if isinstance(data, dict) else data
|
|
5062
5077
|
for t in tasks:
|
|
5063
5078
|
writer.writerow([
|
|
5064
5079
|
queue,
|
|
@@ -5191,18 +5206,34 @@ cmd_config() {
|
|
|
5191
5206
|
|
|
5192
5207
|
# v6.0.0: Set a configuration value
|
|
5193
5208
|
cmd_config_set() {
|
|
5209
|
+
local use_global=false
|
|
5210
|
+
|
|
5211
|
+
# Check for --global flag
|
|
5212
|
+
if [[ "${1:-}" == "--global" ]]; then
|
|
5213
|
+
use_global=true
|
|
5214
|
+
shift
|
|
5215
|
+
fi
|
|
5216
|
+
|
|
5194
5217
|
local key="${1:-}"
|
|
5195
5218
|
local value="${2:-}"
|
|
5196
5219
|
|
|
5197
5220
|
if [[ -z "$key" || -z "$value" ]]; then
|
|
5198
|
-
echo -e "${RED}Usage: loki config set <key> <value>${NC}"
|
|
5221
|
+
echo -e "${RED}Usage: loki config set [--global] <key> <value>${NC}"
|
|
5199
5222
|
echo "Run 'loki config' for list of settable keys."
|
|
5200
5223
|
return 1
|
|
5201
5224
|
fi
|
|
5202
5225
|
|
|
5226
|
+
# Determine config directory: --global writes to ~/.config/loki-mode/
|
|
5227
|
+
local config_dir
|
|
5228
|
+
if $use_global; then
|
|
5229
|
+
config_dir="${HOME}/.config/loki-mode"
|
|
5230
|
+
else
|
|
5231
|
+
config_dir="$LOKI_DIR/config"
|
|
5232
|
+
fi
|
|
5233
|
+
|
|
5203
5234
|
# Ensure config directory exists
|
|
5204
|
-
mkdir -p "$
|
|
5205
|
-
local config_store="$
|
|
5235
|
+
mkdir -p "$config_dir"
|
|
5236
|
+
local config_store="$config_dir/settings.json"
|
|
5206
5237
|
|
|
5207
5238
|
# Initialize if not exists
|
|
5208
5239
|
if [ ! -f "$config_store" ]; then
|
|
@@ -5284,7 +5315,21 @@ for part in parts[:-1]:
|
|
|
5284
5315
|
if part not in current or not isinstance(current[part], dict):
|
|
5285
5316
|
current[part] = {}
|
|
5286
5317
|
current = current[part]
|
|
5287
|
-
|
|
5318
|
+
# Type coercion: try int, then float, then bool, else string
|
|
5319
|
+
def coerce_type(v):
|
|
5320
|
+
if v.lower() in ('true', 'false'):
|
|
5321
|
+
return v.lower() == 'true'
|
|
5322
|
+
try:
|
|
5323
|
+
return int(v)
|
|
5324
|
+
except ValueError:
|
|
5325
|
+
pass
|
|
5326
|
+
try:
|
|
5327
|
+
return float(v)
|
|
5328
|
+
except ValueError:
|
|
5329
|
+
pass
|
|
5330
|
+
return v
|
|
5331
|
+
|
|
5332
|
+
current[parts[-1]] = coerce_type(value)
|
|
5288
5333
|
|
|
5289
5334
|
with open(cfg_file, "w") as f:
|
|
5290
5335
|
json.dump(config, f, indent=2)
|
|
@@ -5319,8 +5364,7 @@ cmd_config_get() {
|
|
|
5319
5364
|
return 0
|
|
5320
5365
|
fi
|
|
5321
5366
|
|
|
5322
|
-
|
|
5323
|
-
export LOKI_CFG_KEY="$key"
|
|
5367
|
+
LOKI_CFG_FILE="$config_store" LOKI_CFG_KEY="$key" \
|
|
5324
5368
|
python3 << 'GET_CONFIG'
|
|
5325
5369
|
import json, os
|
|
5326
5370
|
cfg_file = os.environ["LOKI_CFG_FILE"]
|
|
@@ -5741,7 +5785,10 @@ cmd_doctor() {
|
|
|
5741
5785
|
echo -e " ${GREEN}PASS${NC} $sname ${DIM}$short_path${NC}"
|
|
5742
5786
|
pass_count=$((pass_count + 1))
|
|
5743
5787
|
elif [ -L "$sdir" ]; then
|
|
5744
|
-
|
|
5788
|
+
local _target
|
|
5789
|
+
_target=$(readlink "$sdir" 2>/dev/null || echo "unknown")
|
|
5790
|
+
echo -e " ${RED}FAIL${NC} $sname ${DIM}(broken symlink -> $_target)${NC}"
|
|
5791
|
+
echo -e " ${YELLOW}Fix: loki setup-skill${NC}"
|
|
5745
5792
|
fail_count=$((fail_count + 1))
|
|
5746
5793
|
else
|
|
5747
5794
|
echo -e " ${YELLOW}WARN${NC} $sname ${DIM}(not found - run 'loki setup-skill')${NC}"
|
|
@@ -6946,30 +6993,34 @@ cmd_init() {
|
|
|
6946
6993
|
ai-chatbot
|
|
6947
6994
|
)
|
|
6948
6995
|
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6996
|
+
# Template label lookup function (bash 3.2 compatible - no associative arrays)
|
|
6997
|
+
_get_template_label() {
|
|
6998
|
+
case "$1" in
|
|
6999
|
+
simple-todo-app) echo "Simple Todo App" ;;
|
|
7000
|
+
static-landing-page) echo "Static Landing Page" ;;
|
|
7001
|
+
api-only) echo "REST API (No Frontend)" ;;
|
|
7002
|
+
rest-api) echo "REST API" ;;
|
|
7003
|
+
rest-api-auth) echo "REST API with Auth" ;;
|
|
7004
|
+
cli-tool) echo "CLI Tool" ;;
|
|
7005
|
+
discord-bot) echo "Discord Bot" ;;
|
|
7006
|
+
chrome-extension) echo "Chrome Extension" ;;
|
|
7007
|
+
blog-platform) echo "Blog Platform" ;;
|
|
7008
|
+
full-stack-demo) echo "Full-Stack Demo" ;;
|
|
7009
|
+
web-scraper) echo "Web Scraper" ;;
|
|
7010
|
+
data-pipeline) echo "Data Pipeline" ;;
|
|
7011
|
+
dashboard) echo "Analytics Dashboard" ;;
|
|
7012
|
+
game) echo "Browser Game" ;;
|
|
7013
|
+
slack-bot) echo "Slack Bot" ;;
|
|
7014
|
+
npm-library) echo "npm Library" ;;
|
|
7015
|
+
microservice) echo "Microservice" ;;
|
|
7016
|
+
mobile-app) echo "Mobile App" ;;
|
|
7017
|
+
saas-app) echo "SaaS Application" ;;
|
|
7018
|
+
saas-starter) echo "SaaS Starter Kit" ;;
|
|
7019
|
+
e-commerce) echo "E-Commerce Store" ;;
|
|
7020
|
+
ai-chatbot) echo "AI Chatbot (RAG)" ;;
|
|
7021
|
+
*) echo "$1" ;;
|
|
7022
|
+
esac
|
|
7023
|
+
}
|
|
6973
7024
|
|
|
6974
7025
|
local template_count=${#TEMPLATE_NAMES[@]}
|
|
6975
7026
|
|
|
@@ -7067,7 +7118,7 @@ cmd_init() {
|
|
|
7067
7118
|
else
|
|
7068
7119
|
echo ","
|
|
7069
7120
|
fi
|
|
7070
|
-
printf ' {"name": "%s", "label": "%s"}' "$tname" "$
|
|
7121
|
+
printf ' {"name": "%s", "label": "%s"}' "$tname" "$(_get_template_label "$tname")"
|
|
7071
7122
|
done
|
|
7072
7123
|
echo ""
|
|
7073
7124
|
echo "]"
|
|
@@ -7082,7 +7133,7 @@ cmd_init() {
|
|
|
7082
7133
|
elif [[ "$tname" == "mobile-app" ]]; then
|
|
7083
7134
|
echo -e " ${DIM}Complex:${NC}"
|
|
7084
7135
|
fi
|
|
7085
|
-
printf " %2d. ${CYAN}%-22s${NC} %s\n" "$idx" "$tname" "$
|
|
7136
|
+
printf " %2d. ${CYAN}%-22s${NC} %s\n" "$idx" "$tname" "$(_get_template_label "$tname")"
|
|
7086
7137
|
idx=$((idx + 1))
|
|
7087
7138
|
done
|
|
7088
7139
|
echo ""
|
|
@@ -7105,7 +7156,7 @@ cmd_init() {
|
|
|
7105
7156
|
elif [[ "$tname" == "mobile-app" ]]; then
|
|
7106
7157
|
echo -e " ${DIM}Complex:${NC}"
|
|
7107
7158
|
fi
|
|
7108
|
-
printf " %2d. %-22s %s\n" "$idx" "$tname" "$
|
|
7159
|
+
printf " %2d. %-22s %s\n" "$idx" "$tname" "$(_get_template_label "$tname")"
|
|
7109
7160
|
idx=$((idx + 1))
|
|
7110
7161
|
done
|
|
7111
7162
|
echo ""
|
|
@@ -7119,7 +7170,27 @@ cmd_init() {
|
|
|
7119
7170
|
|
|
7120
7171
|
template_name="${TEMPLATE_NAMES[$((choice - 1))]}"
|
|
7121
7172
|
echo ""
|
|
7122
|
-
echo -e "Selected: ${CYAN}$template_name${NC} ($
|
|
7173
|
+
echo -e "Selected: ${CYAN}$template_name${NC} ($(_get_template_label "$template_name"))"
|
|
7174
|
+
fi
|
|
7175
|
+
|
|
7176
|
+
# Validate template_name against known list before filesystem lookup
|
|
7177
|
+
local _tpl_valid=false
|
|
7178
|
+
for _tpl_check in "${TEMPLATE_NAMES[@]}"; do
|
|
7179
|
+
if [[ "$_tpl_check" == "$template_name" ]]; then
|
|
7180
|
+
_tpl_valid=true
|
|
7181
|
+
break
|
|
7182
|
+
fi
|
|
7183
|
+
done
|
|
7184
|
+
if ! $_tpl_valid; then
|
|
7185
|
+
echo -e "${RED}Unknown template: $template_name${NC}"
|
|
7186
|
+
echo ""
|
|
7187
|
+
echo "Available templates:"
|
|
7188
|
+
for tname in "${TEMPLATE_NAMES[@]}"; do
|
|
7189
|
+
echo " $tname"
|
|
7190
|
+
done
|
|
7191
|
+
echo ""
|
|
7192
|
+
echo "Run 'loki init --list' for details."
|
|
7193
|
+
exit 1
|
|
7123
7194
|
fi
|
|
7124
7195
|
|
|
7125
7196
|
# Resolve template file
|
|
@@ -7161,6 +7232,19 @@ cmd_init() {
|
|
|
7161
7232
|
target_dir="$(pwd)"
|
|
7162
7233
|
fi
|
|
7163
7234
|
|
|
7235
|
+
# Guard: check if target directory has an active .loki/ session
|
|
7236
|
+
if [[ -n "$project_name" ]] && [[ -d "$target_dir/.loki" ]]; then
|
|
7237
|
+
if [[ -f "$target_dir/.loki/loki.pid" ]]; then
|
|
7238
|
+
local _guard_pid
|
|
7239
|
+
_guard_pid=$(cat "$target_dir/.loki/loki.pid" 2>/dev/null)
|
|
7240
|
+
if [[ -n "$_guard_pid" ]] && kill -0 "$_guard_pid" 2>/dev/null; then
|
|
7241
|
+
echo -e "${RED}Cannot initialize: active session running in $target_dir. Stop it first.${NC}"
|
|
7242
|
+
return 1
|
|
7243
|
+
fi
|
|
7244
|
+
fi
|
|
7245
|
+
echo -e "${YELLOW}Reinitializing existing .loki/ directory in $target_dir${NC}"
|
|
7246
|
+
fi
|
|
7247
|
+
|
|
7164
7248
|
# Build config JSON
|
|
7165
7249
|
local config_json
|
|
7166
7250
|
config_json=$(cat <<ENDJSON
|
|
@@ -7246,7 +7330,7 @@ ENDGITIGNORE
|
|
|
7246
7330
|
if [[ -n "$project_name" ]]; then
|
|
7247
7331
|
echo -e " ${GREEN}CREATE${NC} $target_dir/"
|
|
7248
7332
|
fi
|
|
7249
|
-
echo -e " ${GREEN}CREATE${NC} $target_dir/prd.md ($
|
|
7333
|
+
echo -e " ${GREEN}CREATE${NC} $target_dir/prd.md ($(_get_template_label "$template_name") template)"
|
|
7250
7334
|
echo -e " ${GREEN}CREATE${NC} $target_dir/.loki/"
|
|
7251
7335
|
echo -e " ${GREEN}CREATE${NC} $target_dir/.loki/loki.config.json"
|
|
7252
7336
|
echo -e " ${GREEN}CREATE${NC} $target_dir/README.md"
|
|
@@ -7278,8 +7362,10 @@ ENDGITIGNORE
|
|
|
7278
7362
|
echo "$config_json" > "$target_dir/.loki/loki.config.json"
|
|
7279
7363
|
|
|
7280
7364
|
# Only write README if it does not already exist
|
|
7365
|
+
local did_write_readme=false
|
|
7281
7366
|
if [[ ! -f "$target_dir/README.md" ]]; then
|
|
7282
7367
|
echo "$readme_content" > "$target_dir/README.md"
|
|
7368
|
+
did_write_readme=true
|
|
7283
7369
|
fi
|
|
7284
7370
|
|
|
7285
7371
|
# Git init (unless --no-git or already in a git repo)
|
|
@@ -7297,9 +7383,9 @@ ENDGITIGNORE
|
|
|
7297
7383
|
echo -e "${GREEN}Project scaffolded:${NC} $target_dir"
|
|
7298
7384
|
echo ""
|
|
7299
7385
|
echo " Files created:"
|
|
7300
|
-
echo -e " prd.md ${DIM}$
|
|
7386
|
+
echo -e " prd.md ${DIM}$(_get_template_label "$template_name") template${NC}"
|
|
7301
7387
|
echo -e " .loki/loki.config.json ${DIM}project configuration${NC}"
|
|
7302
|
-
if
|
|
7388
|
+
if $did_write_readme; then
|
|
7303
7389
|
echo -e " README.md ${DIM}project readme${NC}"
|
|
7304
7390
|
fi
|
|
7305
7391
|
if $did_git_init; then
|
|
@@ -10617,24 +10703,25 @@ cmd_worktree() {
|
|
|
10617
10703
|
fi
|
|
10618
10704
|
|
|
10619
10705
|
# Validate JSON is parseable and contains required fields
|
|
10620
|
-
if ! python3
|
|
10621
|
-
import json, sys
|
|
10706
|
+
if ! LOKI_SIGNAL_FILE="$signal_file" python3 << 'MERGE_VALIDATE_PY'
|
|
10707
|
+
import json, sys, os
|
|
10622
10708
|
try:
|
|
10623
|
-
data = json.load(open('
|
|
10709
|
+
data = json.load(open(os.environ['LOKI_SIGNAL_FILE']))
|
|
10624
10710
|
assert data.get('branch'), 'missing branch'
|
|
10625
10711
|
assert data.get('worktree'), 'missing worktree'
|
|
10626
10712
|
except Exception as e:
|
|
10627
10713
|
print(str(e), file=sys.stderr)
|
|
10628
10714
|
sys.exit(1)
|
|
10629
|
-
|
|
10715
|
+
MERGE_VALIDATE_PY
|
|
10716
|
+
then
|
|
10630
10717
|
echo -e "${RED}Invalid or corrupted merge signal file${NC}"
|
|
10631
10718
|
exit 1
|
|
10632
10719
|
fi
|
|
10633
10720
|
|
|
10634
10721
|
local branch
|
|
10635
|
-
branch=$(python3 -c "import json; print(json.load(open('
|
|
10722
|
+
branch=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['branch'])")
|
|
10636
10723
|
local worktree_path
|
|
10637
|
-
worktree_path=$(python3 -c "import json; print(json.load(open('
|
|
10724
|
+
worktree_path=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['worktree'])")
|
|
10638
10725
|
|
|
10639
10726
|
if git merge --no-ff "$branch" -m "merge($name): auto-merge from parallel worktree"; then
|
|
10640
10727
|
echo -e "${GREEN}Merge successful${NC}"
|
|
@@ -10771,30 +10858,46 @@ cmd_state() {
|
|
|
10771
10858
|
esac
|
|
10772
10859
|
done
|
|
10773
10860
|
|
|
10774
|
-
PYTHONPATH="${SKILL_DIR:-.}"
|
|
10775
|
-
|
|
10776
|
-
|
|
10861
|
+
PYTHONPATH="${SKILL_DIR:-.}" \
|
|
10862
|
+
LOKI_STATE_QUERY_TYPE="$query_type" \
|
|
10863
|
+
LOKI_STATE_AGENT_ID="$agent_id" \
|
|
10864
|
+
LOKI_STATE_EVENT_TYPE="$event_type" \
|
|
10865
|
+
LOKI_STATE_TOPIC="$topic" \
|
|
10866
|
+
LOKI_STATE_CLUSTER_ID="$cluster_id" \
|
|
10867
|
+
LOKI_STATE_MIGRATION_ID="$migration_id" \
|
|
10868
|
+
LOKI_STATE_LIMIT="$limit" \
|
|
10869
|
+
LOKI_STATE_SKILL_DIR="${SKILL_DIR:-.}" \
|
|
10870
|
+
python3 << 'STATE_QUERY_PY'
|
|
10871
|
+
import json, sys, os
|
|
10872
|
+
sys.path.insert(0, os.environ.get('LOKI_STATE_SKILL_DIR', '.'))
|
|
10777
10873
|
from state.sqlite_backend import SqliteStateBackend
|
|
10778
10874
|
db = SqliteStateBackend()
|
|
10779
10875
|
|
|
10780
|
-
query_type = '
|
|
10876
|
+
query_type = os.environ.get('LOKI_STATE_QUERY_TYPE', '')
|
|
10877
|
+
agent_id = os.environ.get('LOKI_STATE_AGENT_ID', '') or None
|
|
10878
|
+
event_type = os.environ.get('LOKI_STATE_EVENT_TYPE', '') or None
|
|
10879
|
+
topic = os.environ.get('LOKI_STATE_TOPIC', '') or None
|
|
10880
|
+
cluster_id = os.environ.get('LOKI_STATE_CLUSTER_ID', '') or None
|
|
10881
|
+
migration_id = os.environ.get('LOKI_STATE_MIGRATION_ID', '') or None
|
|
10882
|
+
limit = int(os.environ.get('LOKI_STATE_LIMIT', '20'))
|
|
10883
|
+
|
|
10781
10884
|
if query_type == 'events':
|
|
10782
10885
|
results = db.query_events(
|
|
10783
|
-
event_type=
|
|
10784
|
-
agent_id=
|
|
10785
|
-
migration_id=
|
|
10786
|
-
limit=
|
|
10886
|
+
event_type=event_type,
|
|
10887
|
+
agent_id=agent_id,
|
|
10888
|
+
migration_id=migration_id,
|
|
10889
|
+
limit=limit
|
|
10787
10890
|
)
|
|
10788
10891
|
elif query_type == 'messages':
|
|
10789
10892
|
results = db.query_messages(
|
|
10790
|
-
topic=
|
|
10791
|
-
cluster_id=
|
|
10792
|
-
limit=
|
|
10893
|
+
topic=topic,
|
|
10894
|
+
cluster_id=cluster_id,
|
|
10895
|
+
limit=limit
|
|
10793
10896
|
)
|
|
10794
10897
|
elif query_type == 'checkpoints':
|
|
10795
10898
|
results = db.query_checkpoints(
|
|
10796
|
-
migration_id=
|
|
10797
|
-
limit=
|
|
10899
|
+
migration_id=migration_id,
|
|
10900
|
+
limit=limit
|
|
10798
10901
|
)
|
|
10799
10902
|
else:
|
|
10800
10903
|
print(f'Unknown query type: {query_type}')
|
|
@@ -10807,7 +10910,8 @@ else:
|
|
|
10807
10910
|
print(json.dumps(r, indent=2))
|
|
10808
10911
|
print('---')
|
|
10809
10912
|
print(f'{len(results)} result(s)')
|
|
10810
|
-
|
|
10913
|
+
STATE_QUERY_PY
|
|
10914
|
+
2>&1 || {
|
|
10811
10915
|
echo -e "${RED}Error querying state database${NC}"
|
|
10812
10916
|
return 1
|
|
10813
10917
|
}
|
|
@@ -13961,14 +14065,17 @@ try {
|
|
|
13961
14065
|
mkdir -p .loki
|
|
13962
14066
|
local config_file=".loki/config.json"
|
|
13963
14067
|
if [ -f "$config_file" ]; then
|
|
13964
|
-
|
|
13965
|
-
|
|
13966
|
-
|
|
14068
|
+
LOKI_TELEM_CFG="$config_file" LOKI_TELEM_ENDPOINT="$endpoint" \
|
|
14069
|
+
python3 << 'TELEM_ENABLE_PY'
|
|
14070
|
+
import json, os
|
|
14071
|
+
cfg = os.environ['LOKI_TELEM_CFG']
|
|
14072
|
+
ep = os.environ['LOKI_TELEM_ENDPOINT']
|
|
14073
|
+
with open(cfg) as f:
|
|
13967
14074
|
config = json.load(f)
|
|
13968
|
-
config['otel_endpoint'] =
|
|
13969
|
-
with open(
|
|
14075
|
+
config['otel_endpoint'] = ep
|
|
14076
|
+
with open(cfg, 'w') as f:
|
|
13970
14077
|
json.dump(config, f, indent=2)
|
|
13971
|
-
|
|
14078
|
+
TELEM_ENABLE_PY
|
|
13972
14079
|
else
|
|
13973
14080
|
echo "{\"otel_endpoint\": \"$endpoint\"}" > "$config_file"
|
|
13974
14081
|
fi
|
|
@@ -13984,14 +14091,15 @@ with open('$config_file', 'w') as f:
|
|
|
13984
14091
|
|
|
13985
14092
|
local config_file=".loki/config.json"
|
|
13986
14093
|
if [ -f "$config_file" ]; then
|
|
13987
|
-
python3
|
|
13988
|
-
import json
|
|
13989
|
-
|
|
14094
|
+
LOKI_TELEM_CFG="$config_file" python3 << 'TELEM_DISABLE_PY'
|
|
14095
|
+
import json, os
|
|
14096
|
+
cfg = os.environ['LOKI_TELEM_CFG']
|
|
14097
|
+
with open(cfg) as f:
|
|
13990
14098
|
config = json.load(f)
|
|
13991
14099
|
config.pop('otel_endpoint', None)
|
|
13992
|
-
with open(
|
|
14100
|
+
with open(cfg, 'w') as f:
|
|
13993
14101
|
json.dump(config, f, indent=2)
|
|
13994
|
-
|
|
14102
|
+
TELEM_DISABLE_PY
|
|
13995
14103
|
fi
|
|
13996
14104
|
|
|
13997
14105
|
echo -e " Telemetry disabled"
|
|
@@ -14158,9 +14266,10 @@ cmd_remote() {
|
|
|
14158
14266
|
prd_abs="$(cd "$(dirname "$prd_file")" && pwd)/$(basename "$prd_file")"
|
|
14159
14267
|
fi
|
|
14160
14268
|
|
|
14161
|
-
# Start dashboard in background if enabled
|
|
14269
|
+
# Start dashboard in background if enabled, with cleanup trap
|
|
14162
14270
|
if [ "${LOKI_DASHBOARD:-true}" = "true" ]; then
|
|
14163
14271
|
cmd_api start >/dev/null 2>&1 &
|
|
14272
|
+
trap 'cmd_api stop >/dev/null 2>&1 || true' EXIT INT TERM
|
|
14164
14273
|
fi
|
|
14165
14274
|
|
|
14166
14275
|
local version=$(get_version)
|
|
@@ -14759,9 +14868,9 @@ METRICS_SCRIPT
|
|
|
14759
14868
|
fi
|
|
14760
14869
|
|
|
14761
14870
|
echo "Uploading metrics report..."
|
|
14762
|
-
local gist_desc="Loki Mode productivity report - ${project_name:-project} ($(date +%Y-%m-%d))"
|
|
14763
14871
|
local project_name
|
|
14764
14872
|
project_name=$(basename "$(pwd)")
|
|
14873
|
+
local gist_desc="Loki Mode productivity report - ${project_name:-project} ($(date +%Y-%m-%d))"
|
|
14765
14874
|
local gist_url
|
|
14766
14875
|
gist_url=$(gh gist create "$tmpfile" --desc "$gist_desc" --public 2>&1)
|
|
14767
14876
|
local gist_exit=$?
|
|
@@ -16764,12 +16873,13 @@ except Exception:
|
|
|
16764
16873
|
# Gate 2: Security Scan
|
|
16765
16874
|
_ci_scan() {
|
|
16766
16875
|
local pattern="$1" sev="$2" cat="$3" finding="$4" suggestion="$5"
|
|
16876
|
+
# Only scan added lines (starting with "+") to avoid flagging deleted code
|
|
16767
16877
|
while IFS= read -r match; do
|
|
16768
16878
|
[ -z "$match" ] && continue
|
|
16769
16879
|
local ml
|
|
16770
16880
|
ml=$(echo "$match" | cut -d: -f1)
|
|
16771
16881
|
_ci_add_finding "diff" "$ml" "$sev" "$cat" "$finding" "$suggestion"
|
|
16772
|
-
done < <(echo "$diff_content" | grep -
|
|
16882
|
+
done < <(echo "$diff_content" | grep -n '^+' | grep -E "$pattern" 2>/dev/null || true)
|
|
16773
16883
|
}
|
|
16774
16884
|
|
|
16775
16885
|
# Hardcoded secrets
|
|
@@ -16830,6 +16940,7 @@ except Exception:
|
|
|
16830
16940
|
# --- Test suggestions ---
|
|
16831
16941
|
local test_suggestions=""
|
|
16832
16942
|
if [ "$ci_test_suggest" = true ] && [ -n "$changed_files" ]; then
|
|
16943
|
+
export LOKI_CI_CHANGED_FILES="$changed_files"
|
|
16833
16944
|
test_suggestions=$(python3 << 'TEST_SUGGEST_PY'
|
|
16834
16945
|
import sys, os
|
|
16835
16946
|
|
|
@@ -16859,8 +16970,15 @@ for f in changed:
|
|
|
16859
16970
|
suggestions.append({"file": f, "test_file": test_path, "alt_test_file": alt_path,
|
|
16860
16971
|
"framework": "pytest", "hint": f"Add unit tests for {name} module"})
|
|
16861
16972
|
elif ext in (".js", ".ts", ".jsx", ".tsx"):
|
|
16862
|
-
|
|
16863
|
-
|
|
16973
|
+
if ext == ".tsx":
|
|
16974
|
+
test_ext = ".test.tsx"
|
|
16975
|
+
elif ext == ".jsx":
|
|
16976
|
+
test_ext = ".test.jsx"
|
|
16977
|
+
elif ext == ".ts":
|
|
16978
|
+
test_ext = ".test.ts"
|
|
16979
|
+
elif ext == ".js":
|
|
16980
|
+
test_ext = ".test.js"
|
|
16981
|
+
else:
|
|
16864
16982
|
test_ext = f".test{ext}"
|
|
16865
16983
|
test_path = os.path.join(dirpath, f"{name}{test_ext}")
|
|
16866
16984
|
suggestions.append({"file": f, "test_file": test_path,
|
|
@@ -17034,6 +17152,7 @@ CI_JSON_OUT
|
|
|
17034
17152
|
if [ -n "${test_suggestions:-}" ] && [ "$test_suggestions" != "[]" ]; then
|
|
17035
17153
|
echo "### Test Suggestions"
|
|
17036
17154
|
echo ""
|
|
17155
|
+
export LOKI_CI_TEST_SUGG="${test_suggestions}"
|
|
17037
17156
|
python3 -c "
|
|
17038
17157
|
import json, os
|
|
17039
17158
|
tests = json.loads(os.environ.get('LOKI_CI_TEST_SUGG', '[]'))
|
|
@@ -18770,7 +18889,7 @@ cmd_share() {
|
|
|
18770
18889
|
tmpfile=$(mktemp "/tmp/loki-share-XXXXXX.$ext")
|
|
18771
18890
|
|
|
18772
18891
|
echo "Generating session report..."
|
|
18773
|
-
if !
|
|
18892
|
+
if ! "$0" report --format "$format" > "$tmpfile" 2>/dev/null; then
|
|
18774
18893
|
echo -e "${RED}Failed to generate session report${NC}"
|
|
18775
18894
|
rm -f "$tmpfile"
|
|
18776
18895
|
exit 1
|