loki-mode 7.81.1 → 7.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +88 -27
- package/autonomy/lib/config-map.sh +61 -15
- package/autonomy/loki +641 -45
- package/autonomy/run.sh +202 -31
- package/completions/_loki +84 -6
- package/completions/loki.bash +25 -7
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +36 -10
- package/dashboard/registry.py +67 -24
- package/dashboard/server.py +131 -74
- package/docs/INSTALLATION.md +2 -2
- package/events/bus.py +17 -1
- package/events/emit.sh +15 -1
- package/loki-ts/dist/loki.js +7 -4
- package/lokistore/cloud.py +17 -1
- package/lokistore/factory.py +6 -1
- package/lokistore/local.py +11 -3
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +16 -4
- package/memory/cross_project.py +74 -5
- package/memory/embeddings.py +19 -1
- package/memory/retrieval.py +81 -153
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/codex.sh +9 -4
- package/memory/tree_index.py +0 -499
- package/memory/tree_search.py +0 -305
package/autonomy/loki
CHANGED
|
@@ -265,6 +265,30 @@ _loki_new_session_exec() {
|
|
|
265
265
|
fi
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
# Actionable PRD-not-found exit. Both PRD-path existence guards in cmd_start route
|
|
269
|
+
# through here so they cannot drift: the user always learns the two recoveries
|
|
270
|
+
# (fix the path, or drop the arg to auto-generate from the codebase). If a sibling
|
|
271
|
+
# .md file looks like a near-miss of the bad path, surface it honestly as a hint.
|
|
272
|
+
# Prints to stderr and exits 1. Honest: only suggests a "did you mean" when a real
|
|
273
|
+
# .md sibling exists; never fabricates a path.
|
|
274
|
+
_prd_not_found_die() {
|
|
275
|
+
local bad_path="$1"
|
|
276
|
+
echo -e "${RED}Error: PRD file not found: $bad_path${NC}" >&2
|
|
277
|
+
echo -e "${YELLOW}Hint: provide a path to an existing .md/.json/.txt/.yaml/.yml file, or run 'loki start' with no args to analyze the current directory.${NC}" >&2
|
|
278
|
+
# Best-effort near-miss suggestion: if a .md file sits in the same directory
|
|
279
|
+
# as the bad path, point at it. Read-only; silent when nothing matches.
|
|
280
|
+
local dir base suggestion
|
|
281
|
+
dir="$(dirname "$bad_path" 2>/dev/null)" || dir="."
|
|
282
|
+
base="$(basename "$bad_path" 2>/dev/null)"
|
|
283
|
+
if [ -d "$dir" ] && [ -n "$base" ]; then
|
|
284
|
+
suggestion="$(ls -1 "$dir"/*.md 2>/dev/null | head -1)"
|
|
285
|
+
if [ -n "$suggestion" ]; then
|
|
286
|
+
echo -e "${YELLOW}Did you mean: $suggestion ?${NC}" >&2
|
|
287
|
+
fi
|
|
288
|
+
fi
|
|
289
|
+
exit 1
|
|
290
|
+
}
|
|
291
|
+
|
|
268
292
|
# Anonymous usage telemetry
|
|
269
293
|
PROJECT_DIR="$SKILL_DIR"
|
|
270
294
|
_TELEMETRY_SCRIPT="$SKILL_DIR/autonomy/telemetry.sh"
|
|
@@ -714,6 +738,7 @@ show_help() {
|
|
|
714
738
|
echo ""
|
|
715
739
|
echo "Session:"
|
|
716
740
|
echo " status [--json] Show current status (--json for machine-readable)"
|
|
741
|
+
echo " next Run the right next step for you (resume / ship / why)"
|
|
717
742
|
echo " stop Stop execution immediately"
|
|
718
743
|
echo " pause Pause after current session"
|
|
719
744
|
echo " resume Resume paused execution"
|
|
@@ -745,7 +770,7 @@ show_help() {
|
|
|
745
770
|
echo " version Show version"
|
|
746
771
|
echo " help Show this help ('loki help aliases' for old names)"
|
|
747
772
|
echo ""
|
|
748
|
-
echo "More commands (grill, spec, deploy, cleanup, init, watch, demo, web, api,"
|
|
773
|
+
echo "More commands (ship, grill, spec, deploy, cleanup, init, watch, demo, web, api,"
|
|
749
774
|
echo "logs, github, import, council, proof, audit, compliance, agent, template,"
|
|
750
775
|
echo "magic, docs, wiki, ci, test, bench, secrets, telemetry, crash, worktree,"
|
|
751
776
|
echo "failover, monitor, remote, ...) are dispatchable and documented via"
|
|
@@ -909,6 +934,7 @@ show_landing() {
|
|
|
909
934
|
echo -e "First time here? ${CYAN}loki doctor${NC} checks your setup (an AI provider CLI is required)."
|
|
910
935
|
echo ""
|
|
911
936
|
echo "Get started:"
|
|
937
|
+
echo -e " ${CYAN}loki quickstart${NC} Guided first build from your idea (no PRD needed)"
|
|
912
938
|
echo -e " ${CYAN}loki start ./prd.md${NC} Build from a spec (PRD file, GitHub issue, or no arg)"
|
|
913
939
|
echo -e " ${CYAN}loki demo${NC} Build a sample todo app end to end (real run)"
|
|
914
940
|
echo -e " ${CYAN}loki dashboard start${NC} Start the live run monitor (then: loki dashboard open)"
|
|
@@ -1386,6 +1412,13 @@ cmd_start() {
|
|
|
1386
1412
|
echo -e "${RED}--budget requires a numeric USD amount (e.g., --budget 5.00)${NC}"
|
|
1387
1413
|
exit 1
|
|
1388
1414
|
fi
|
|
1415
|
+
# Reject a non-positive budget: a 0 (or 0.00) cap makes
|
|
1416
|
+
# check_budget_limit pause before any work runs (cost >= 0 is
|
|
1417
|
+
# always true), which looks like a silent hang. Require > 0.
|
|
1418
|
+
if ! awk -v b="$2" 'BEGIN{exit !(b+0 > 0)}'; then
|
|
1419
|
+
echo -e "${RED}--budget must be greater than 0 (a 0 budget pauses before any work)${NC}" >&2
|
|
1420
|
+
exit 1
|
|
1421
|
+
fi
|
|
1389
1422
|
export LOKI_BUDGET_LIMIT="$2"
|
|
1390
1423
|
shift 2
|
|
1391
1424
|
else
|
|
@@ -1399,6 +1432,11 @@ cmd_start() {
|
|
|
1399
1432
|
echo -e "${RED}--budget requires a numeric USD amount (e.g., --budget=5.00)${NC}"
|
|
1400
1433
|
exit 1
|
|
1401
1434
|
fi
|
|
1435
|
+
# Reject a non-positive budget (see --budget arm above).
|
|
1436
|
+
if ! awk -v b="$budget_val" 'BEGIN{exit !(b+0 > 0)}'; then
|
|
1437
|
+
echo -e "${RED}--budget must be greater than 0 (a 0 budget pauses before any work)${NC}" >&2
|
|
1438
|
+
exit 1
|
|
1439
|
+
fi
|
|
1402
1440
|
export LOKI_BUDGET_LIMIT="$budget_val"
|
|
1403
1441
|
shift
|
|
1404
1442
|
;;
|
|
@@ -1613,6 +1651,10 @@ cmd_start() {
|
|
|
1613
1651
|
version=$(get_version)
|
|
1614
1652
|
local _ttfv_max_iter="${LOKI_MAX_ITERATIONS:-3}"
|
|
1615
1653
|
mkdir -p "$LOKI_DIR" 2>/dev/null || true
|
|
1654
|
+
# Reap stale per-PID temp PRDs from prior runs (the process exec-replaces
|
|
1655
|
+
# into run.sh, so these are never cleaned on exit and accumulate).
|
|
1656
|
+
find "$LOKI_DIR" -maxdepth 1 -name 'brief-prd-*.md' -mtime +1 -delete 2>/dev/null || true
|
|
1657
|
+
find "$LOKI_DIR" -maxdepth 1 -name 'quick-prd-*.md' -mtime +1 -delete 2>/dev/null || true
|
|
1616
1658
|
local brief_prd="$LOKI_DIR/brief-prd-$$.md"
|
|
1617
1659
|
synthesize_brief_prd "$brief_prd" "$brief_text"
|
|
1618
1660
|
prd_file="$brief_prd"
|
|
@@ -1670,9 +1712,7 @@ cmd_start() {
|
|
|
1670
1712
|
case "$prd_file" in
|
|
1671
1713
|
*.md|*.json|*.txt|*.yaml|*.yml)
|
|
1672
1714
|
if [ ! -f "$prd_file" ]; then
|
|
1673
|
-
|
|
1674
|
-
echo "Hint: provide a path to an existing .md/.json/.txt/.yaml/.yml file, or run 'loki start' with no args to analyze the current directory." >&2
|
|
1675
|
-
exit 1
|
|
1715
|
+
_prd_not_found_die "$prd_file"
|
|
1676
1716
|
fi
|
|
1677
1717
|
;;
|
|
1678
1718
|
esac
|
|
@@ -1695,8 +1735,7 @@ cmd_start() {
|
|
|
1695
1735
|
case "$prd_file" in
|
|
1696
1736
|
*.md|*.json|*.txt|*.yaml|*.yml)
|
|
1697
1737
|
if [ ! -f "$prd_file" ]; then
|
|
1698
|
-
|
|
1699
|
-
exit 1
|
|
1738
|
+
_prd_not_found_die "$prd_file"
|
|
1700
1739
|
fi
|
|
1701
1740
|
;;
|
|
1702
1741
|
esac
|
|
@@ -1997,7 +2036,13 @@ cmd_start() {
|
|
|
1997
2036
|
fi
|
|
1998
2037
|
if [ -n "$_existing_pid" ] && kill -0 "$_existing_pid" 2>/dev/null; then
|
|
1999
2038
|
echo -e "${RED}Error: another loki instance is running (pid $_existing_pid).${NC}" >&2
|
|
2000
|
-
|
|
2039
|
+
# 'loki stop' is folder-scoped by default (v7.7.30), so it only stops a
|
|
2040
|
+
# run in THIS directory. If the live instance is in another project,
|
|
2041
|
+
# 'loki stop' here reports "No active session" and the user is stuck in
|
|
2042
|
+
# a start-says-running / stop-says-clean loop. Point at the global stop
|
|
2043
|
+
# and at 'loki status' to find the live run.
|
|
2044
|
+
echo -e "${YELLOW}Run 'loki stop' here, or 'loki stop --all' if it is running in another project.${NC}" >&2
|
|
2045
|
+
echo -e "${YELLOW}See 'loki status' for the live run, then retry 'loki start'.${NC}" >&2
|
|
2001
2046
|
exit 1
|
|
2002
2047
|
fi
|
|
2003
2048
|
# PID is stale (file present but process gone). Log + remove + continue.
|
|
@@ -2938,6 +2983,323 @@ cmd_resume() {
|
|
|
2938
2983
|
fi
|
|
2939
2984
|
}
|
|
2940
2985
|
|
|
2986
|
+
# Resolve the terminal-run status from the same already-captured artifacts that
|
|
2987
|
+
# cmd_why reads (no new state, no spend). Echoes a single token: the resolved
|
|
2988
|
+
# status string (e.g. council_approved, paused, max_iterations_reached), or the
|
|
2989
|
+
# literal "none" when no run exists here yet. Used by cmd_next so its status->
|
|
2990
|
+
# action mapping reads exactly the same state cmd_why does (no drift).
|
|
2991
|
+
# Read-only; never fabricates.
|
|
2992
|
+
_loki_resolve_run_status() {
|
|
2993
|
+
local loki_dir="${LOKI_DIR:-.loki}"
|
|
2994
|
+
local state_file="$loki_dir/autonomy-state.json"
|
|
2995
|
+
if [ -n "${LOKI_SESSION_ID:-}" ] && [ -f "$loki_dir/sessions/${LOKI_SESSION_ID}/autonomy-state.json" ]; then
|
|
2996
|
+
state_file="$loki_dir/sessions/${LOKI_SESSION_ID}/autonomy-state.json"
|
|
2997
|
+
fi
|
|
2998
|
+
local completion_file="$loki_dir/state/completion.json"
|
|
2999
|
+
|
|
3000
|
+
if [ ! -f "$state_file" ] && [ ! -f "$completion_file" ]; then
|
|
3001
|
+
printf '%s\n' "none"
|
|
3002
|
+
return 0
|
|
3003
|
+
fi
|
|
3004
|
+
|
|
3005
|
+
_LOKI_NA_STATE="$state_file" _LOKI_NA_COMPLETION="$completion_file" python3 - <<'NASTATUS'
|
|
3006
|
+
import json, os
|
|
3007
|
+
def load(p):
|
|
3008
|
+
try:
|
|
3009
|
+
with open(p) as f: return json.load(f)
|
|
3010
|
+
except Exception: return {}
|
|
3011
|
+
state = load(os.environ.get("_LOKI_NA_STATE", ""))
|
|
3012
|
+
comp = load(os.environ.get("_LOKI_NA_COMPLETION", ""))
|
|
3013
|
+
# Mirror cmd_why's COMP_OUTCOME_ALIASES so a completion-only state (a rotated or
|
|
3014
|
+
# crashed run with no live status, only completion.json's outcome) resolves to
|
|
3015
|
+
# the canonical status loki next's action map keys on. Without this, an outcome
|
|
3016
|
+
# of "complete"/"max_iterations" drifted to the default "loki status" instead of
|
|
3017
|
+
# "loki ship"/"loki resume".
|
|
3018
|
+
COMP_OUTCOME_ALIASES = {
|
|
3019
|
+
"complete": "council_approved",
|
|
3020
|
+
"max_iterations": "max_iterations_reached",
|
|
3021
|
+
"intervention": "paused",
|
|
3022
|
+
}
|
|
3023
|
+
status = state.get("status")
|
|
3024
|
+
if not status:
|
|
3025
|
+
raw_outcome = comp.get("outcome") or ""
|
|
3026
|
+
status = COMP_OUTCOME_ALIASES.get(raw_outcome, raw_outcome) or "unknown"
|
|
3027
|
+
print(status)
|
|
3028
|
+
NASTATUS
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
# Map a resolved status to the single concrete next-action SENTENCE (the same
|
|
3032
|
+
# wording cmd_why prints under "What to do"). Echoes the action string. Kept in
|
|
3033
|
+
# lockstep with cmd_why's GUIDE map so why and next agree.
|
|
3034
|
+
_loki_next_action() {
|
|
3035
|
+
local status="${1:-unknown}"
|
|
3036
|
+
case "$status" in
|
|
3037
|
+
council_approved)
|
|
3038
|
+
echo "Review the diff and open a PR (loki ship, or git push + gh pr create)." ;;
|
|
3039
|
+
council_force_approved)
|
|
3040
|
+
echo "Review the diff carefully before merging -- convergence was not unanimous." ;;
|
|
3041
|
+
completion_promise_fulfilled)
|
|
3042
|
+
echo "Verify the promised outcome, then review and PR (loki ship)." ;;
|
|
3043
|
+
max_iterations_reached)
|
|
3044
|
+
echo "Raise LOKI_MAX_ITERATIONS (or narrow the spec), then resume (loki resume)." ;;
|
|
3045
|
+
max_retries_exceeded)
|
|
3046
|
+
echo "Read .loki/logs for the recurring error, fix the root cause, then re-run (loki why)." ;;
|
|
3047
|
+
failed)
|
|
3048
|
+
echo "Read the failure (loki why), fix it, then re-run." ;;
|
|
3049
|
+
policy_blocked)
|
|
3050
|
+
echo "Review the blocking finding (loki why); address it or use the documented override." ;;
|
|
3051
|
+
budget_exceeded)
|
|
3052
|
+
echo "Raise LOKI_BUDGET_LIMIT or accept the partial result, then resume (loki resume)." ;;
|
|
3053
|
+
paused)
|
|
3054
|
+
echo "Resume with: loki resume." ;;
|
|
3055
|
+
interrupted)
|
|
3056
|
+
echo "Resume with: loki resume." ;;
|
|
3057
|
+
stopped)
|
|
3058
|
+
echo "Start a new build (loki start), or resume if you meant to continue." ;;
|
|
3059
|
+
force_stopped)
|
|
3060
|
+
echo "Start a new build, or investigate why a force-stop was needed (loki why)." ;;
|
|
3061
|
+
running)
|
|
3062
|
+
echo "A build looks active (or crashed mid-run); check loki status." ;;
|
|
3063
|
+
none)
|
|
3064
|
+
echo "Run a build first: loki start <spec>." ;;
|
|
3065
|
+
*)
|
|
3066
|
+
echo "Check loki status and loki why for detail." ;;
|
|
3067
|
+
esac
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
# loki next -- one command that runs the right next step for you.
|
|
3071
|
+
# Reads the SAME already-captured state as cmd_why (no new state, no spend),
|
|
3072
|
+
# resolves the terminal status, and proposes the single mapped next step, then
|
|
3073
|
+
# runs it on confirmation. Default on a TTY is confirm-then-run; --yes runs it
|
|
3074
|
+
# without asking; --dry-run and any non-TTY context only PRINT (honest: never
|
|
3075
|
+
# auto-acts in CI / when piped). Strictly additive: reuses cmd_resume, cmd_ship,
|
|
3076
|
+
# cmd_why, and cmd_status; never touches the build path.
|
|
3077
|
+
cmd_next() {
|
|
3078
|
+
local assume_yes=false
|
|
3079
|
+
local dry_run=false
|
|
3080
|
+
case "${1:-}" in
|
|
3081
|
+
--help|-h|help)
|
|
3082
|
+
echo -e "${BOLD}Loki Mode -- run the right next step for you${NC}"
|
|
3083
|
+
echo ""
|
|
3084
|
+
echo "Usage: loki next [--yes] [--dry-run]"
|
|
3085
|
+
echo ""
|
|
3086
|
+
echo "Reads the last build's recorded outcome (the same state 'loki why'"
|
|
3087
|
+
echo "reads -- no new run, no cost) and carries you forward by running the"
|
|
3088
|
+
echo "single mapped next step: resume a paused/interrupted run, finish a"
|
|
3089
|
+
echo "council-approved build (loki ship), or route a failure to 'loki why'."
|
|
3090
|
+
echo ""
|
|
3091
|
+
echo "Default on a terminal: it shows the proposed step and asks before"
|
|
3092
|
+
echo "running it. Non-interactive (piped/CI) it only prints the step;"
|
|
3093
|
+
echo "it never acts on its own there."
|
|
3094
|
+
echo ""
|
|
3095
|
+
echo "Options:"
|
|
3096
|
+
echo " --yes Run the proposed step without asking"
|
|
3097
|
+
echo " --dry-run Print the proposed step only; do not run it"
|
|
3098
|
+
echo " --help, -h Show this help and exit"
|
|
3099
|
+
return 0
|
|
3100
|
+
;;
|
|
3101
|
+
--yes|-y) assume_yes=true ;;
|
|
3102
|
+
--dry-run) dry_run=true ;;
|
|
3103
|
+
"") : ;;
|
|
3104
|
+
*) echo -e "${RED}Unknown flag: $1${NC}" >&2; echo "Usage: loki next [--yes] [--dry-run]" >&2; return 1 ;;
|
|
3105
|
+
esac
|
|
3106
|
+
# Allow a second flag (e.g. --dry-run --yes order-agnostic).
|
|
3107
|
+
case "${2:-}" in
|
|
3108
|
+
--yes|-y) assume_yes=true ;;
|
|
3109
|
+
--dry-run) dry_run=true ;;
|
|
3110
|
+
esac
|
|
3111
|
+
|
|
3112
|
+
local status
|
|
3113
|
+
status="$(_loki_resolve_run_status)"
|
|
3114
|
+
if [ "$status" = "none" ]; then
|
|
3115
|
+
echo "loki next: no run found here yet." >&2
|
|
3116
|
+
echo "Run a build first: loki start <spec>" >&2
|
|
3117
|
+
return 1
|
|
3118
|
+
fi
|
|
3119
|
+
|
|
3120
|
+
local action_text
|
|
3121
|
+
action_text="$(_loki_next_action "$status")"
|
|
3122
|
+
|
|
3123
|
+
# Map the status to the concrete callable that carries the user forward.
|
|
3124
|
+
# action_cmd is a human label; the case below dispatches to the real function.
|
|
3125
|
+
local action_cmd=""
|
|
3126
|
+
case "$status" in
|
|
3127
|
+
paused|interrupted) action_cmd="loki resume" ;;
|
|
3128
|
+
council_approved|council_force_approved|completion_promise_fulfilled)
|
|
3129
|
+
action_cmd="loki ship" ;;
|
|
3130
|
+
max_iterations_reached|budget_exceeded) action_cmd="loki resume" ;;
|
|
3131
|
+
failed|max_retries_exceeded|policy_blocked|force_stopped)
|
|
3132
|
+
action_cmd="loki why" ;;
|
|
3133
|
+
*) action_cmd="loki status" ;;
|
|
3134
|
+
esac
|
|
3135
|
+
|
|
3136
|
+
echo -e "${BOLD}Loki: next${NC}"
|
|
3137
|
+
echo " Last outcome : $status"
|
|
3138
|
+
echo " Next step : $action_text"
|
|
3139
|
+
echo " Will run : $action_cmd"
|
|
3140
|
+
echo ""
|
|
3141
|
+
|
|
3142
|
+
# Honesty gate: only ACT on an interactive terminal with consent, or with
|
|
3143
|
+
# --yes. --dry-run and non-TTY always print-only.
|
|
3144
|
+
if [ "$dry_run" = true ]; then
|
|
3145
|
+
echo "(dry-run: not running. Run '$action_cmd' yourself, or 'loki next --yes'.)"
|
|
3146
|
+
return 0
|
|
3147
|
+
fi
|
|
3148
|
+
if [ "$assume_yes" != true ]; then
|
|
3149
|
+
if [ ! -t 0 ] || [ ! -t 1 ]; then
|
|
3150
|
+
echo "(non-interactive: not running. Run '$action_cmd', or 'loki next --yes'.)"
|
|
3151
|
+
return 0
|
|
3152
|
+
fi
|
|
3153
|
+
local reply=""
|
|
3154
|
+
printf "Run '%s' now? [y/N] " "$action_cmd"
|
|
3155
|
+
read -r reply || reply=""
|
|
3156
|
+
case "$reply" in
|
|
3157
|
+
y|Y|yes|YES) : ;;
|
|
3158
|
+
*) echo "Skipped. Run '$action_cmd' when ready."; return 0 ;;
|
|
3159
|
+
esac
|
|
3160
|
+
fi
|
|
3161
|
+
|
|
3162
|
+
# Dispatch to the real command. For budget/iteration caps, print the env-var
|
|
3163
|
+
# guidance first (the resume alone will not lift the cap), then resume.
|
|
3164
|
+
case "$status" in
|
|
3165
|
+
max_iterations_reached)
|
|
3166
|
+
echo "Note: raise the cap first if you want more iterations, e.g.:"
|
|
3167
|
+
echo " LOKI_MAX_ITERATIONS=<n> loki resume"
|
|
3168
|
+
echo ""
|
|
3169
|
+
;;
|
|
3170
|
+
budget_exceeded)
|
|
3171
|
+
echo "Note: raise the budget first if you want to continue, e.g.:"
|
|
3172
|
+
echo " LOKI_BUDGET_LIMIT=<usd> loki resume"
|
|
3173
|
+
echo ""
|
|
3174
|
+
;;
|
|
3175
|
+
esac
|
|
3176
|
+
case "$action_cmd" in
|
|
3177
|
+
"loki resume") cmd_resume ;;
|
|
3178
|
+
"loki ship") cmd_ship ;;
|
|
3179
|
+
"loki why") cmd_why ;;
|
|
3180
|
+
*) cmd_status ;;
|
|
3181
|
+
esac
|
|
3182
|
+
return $?
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
# loki ship -- the build is done, now get it out (review + PR advice in one flow).
|
|
3186
|
+
# Composes existing surfaces only: optionally opens the running app (--preview),
|
|
3187
|
+
# runs the SAME diff quality gates as cmd_review, and on a clean-enough result
|
|
3188
|
+
# prints the exact branch-aware PR command via the shared print_pr_advice (the
|
|
3189
|
+
# same advisory cmd_deploy uses). PRINT-ONLY for shipping: it NEVER pushes and
|
|
3190
|
+
# NEVER deploys; it prints (and clipboards, as the advisory already does) the
|
|
3191
|
+
# git push + PR command for YOU to run. If review finds HIGH/CRITICAL it stops
|
|
3192
|
+
# and points at the findings instead of advising a PR.
|
|
3193
|
+
cmd_ship() {
|
|
3194
|
+
local do_preview=false
|
|
3195
|
+
local review_args=()
|
|
3196
|
+
while [ $# -gt 0 ]; do
|
|
3197
|
+
case "${1:-}" in
|
|
3198
|
+
--help|-h|help)
|
|
3199
|
+
echo -e "${BOLD}Loki Mode -- finish the build: review, then PR advice (print-only)${NC}"
|
|
3200
|
+
echo ""
|
|
3201
|
+
echo "Usage: loki ship [--preview] [--yes] [review options]"
|
|
3202
|
+
echo ""
|
|
3203
|
+
echo "Runs the natural finish line in one command:"
|
|
3204
|
+
echo " 1. (with --preview) opens the app Loki built and started locally"
|
|
3205
|
+
echo " 2. runs 'loki review' on your uncommitted/branch diff (the same"
|
|
3206
|
+
echo " quality gates)"
|
|
3207
|
+
echo " 3. on a clean-enough result, PRINTS the exact branch-aware"
|
|
3208
|
+
echo " git push + pull-request command for YOU to run"
|
|
3209
|
+
echo ""
|
|
3210
|
+
echo "It is advisory only for shipping: it NEVER runs 'git push', NEVER"
|
|
3211
|
+
echo "creates a PR, and NEVER deploys. You run the printed command."
|
|
3212
|
+
echo "If review finds HIGH or CRITICAL findings it stops and points you"
|
|
3213
|
+
echo "at the findings instead of advising a PR (exit code 1)."
|
|
3214
|
+
echo ""
|
|
3215
|
+
echo "Options:"
|
|
3216
|
+
echo " --preview Open the running local app first (loki preview)"
|
|
3217
|
+
echo " --yes, -y Pass through to review (skip its confirmation prompt)"
|
|
3218
|
+
echo " --help, -h Show this help and exit"
|
|
3219
|
+
echo ""
|
|
3220
|
+
echo "Any other options are passed through to 'loki review'"
|
|
3221
|
+
echo "(e.g. --staged, --since <commit>, --severity high)."
|
|
3222
|
+
return 0
|
|
3223
|
+
;;
|
|
3224
|
+
--preview) do_preview=true ;;
|
|
3225
|
+
*) review_args+=("$1") ;;
|
|
3226
|
+
esac
|
|
3227
|
+
shift
|
|
3228
|
+
done
|
|
3229
|
+
|
|
3230
|
+
# 1. Optional preview of the running local app. Only when an app-runner state
|
|
3231
|
+
# file exists, so we never imply an app is running when it is not.
|
|
3232
|
+
if [ "$do_preview" = true ]; then
|
|
3233
|
+
local loki_dir="${LOKI_DIR:-.loki}"
|
|
3234
|
+
if [ -f "$loki_dir/state/app-runner.json" ] || [ -f "$loki_dir/app-runner.json" ]; then
|
|
3235
|
+
echo -e "${BOLD}Opening the running app...${NC}"
|
|
3236
|
+
cmd_preview || true
|
|
3237
|
+
echo ""
|
|
3238
|
+
else
|
|
3239
|
+
echo "(no running app detected; skipping preview. Start one with loki start.)"
|
|
3240
|
+
echo ""
|
|
3241
|
+
fi
|
|
3242
|
+
fi
|
|
3243
|
+
|
|
3244
|
+
# 2. Run the SAME diff quality gates as cmd_review. Capture its documented
|
|
3245
|
+
# exit code (0 clean, 1 HIGH, 2 CRITICAL). set -e safe: capture via || rc=$?.
|
|
3246
|
+
echo -e "${BOLD}Running quality gates on the diff (loki review)...${NC}"
|
|
3247
|
+
echo ""
|
|
3248
|
+
local rc=0
|
|
3249
|
+
cmd_review "${review_args[@]}" || rc=$?
|
|
3250
|
+
|
|
3251
|
+
if [ "$rc" -ge 1 ]; then
|
|
3252
|
+
echo ""
|
|
3253
|
+
if [ "$rc" -ge 2 ]; then
|
|
3254
|
+
echo -e "${RED}Review found CRITICAL findings. Not advising a PR yet.${NC}" >&2
|
|
3255
|
+
else
|
|
3256
|
+
echo -e "${YELLOW}Review found HIGH findings. Not advising a PR yet.${NC}" >&2
|
|
3257
|
+
fi
|
|
3258
|
+
echo "Fix the findings above (or run 'loki review' for the full report), then 'loki ship' again." >&2
|
|
3259
|
+
return 1
|
|
3260
|
+
fi
|
|
3261
|
+
|
|
3262
|
+
# 3. Clean enough: print the branch-aware PR command (the SAME advisory
|
|
3263
|
+
# cmd_deploy uses). Derive base/head exactly as cmd_deploy does.
|
|
3264
|
+
echo ""
|
|
3265
|
+
echo -e "${GREEN}Review is clean (no HIGH or CRITICAL findings).${NC}"
|
|
3266
|
+
echo ""
|
|
3267
|
+
local dir="."
|
|
3268
|
+
local head="" base=""
|
|
3269
|
+
head="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
|
|
3270
|
+
[ -n "$head" ] || head="HEAD"
|
|
3271
|
+
case "$head" in
|
|
3272
|
+
loki/*)
|
|
3273
|
+
if [ -s "$dir/.loki/state/base-branch.txt" ]; then
|
|
3274
|
+
base="$(head -n1 "$dir/.loki/state/base-branch.txt" 2>/dev/null || echo "")"
|
|
3275
|
+
fi
|
|
3276
|
+
;;
|
|
3277
|
+
esac
|
|
3278
|
+
if [ -z "$base" ]; then
|
|
3279
|
+
local origin_head=""
|
|
3280
|
+
origin_head="$(git -C "$dir" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "")"
|
|
3281
|
+
if [ -n "$origin_head" ]; then
|
|
3282
|
+
base="${origin_head##*/}"
|
|
3283
|
+
fi
|
|
3284
|
+
fi
|
|
3285
|
+
|
|
3286
|
+
if declare -F print_pr_advice >/dev/null 2>&1; then
|
|
3287
|
+
if [ -n "$base" ]; then
|
|
3288
|
+
print_pr_advice "$base" "$head" "$dir"
|
|
3289
|
+
else
|
|
3290
|
+
echo "Could not determine the PR base branch automatically; set it manually:"
|
|
3291
|
+
print_pr_advice "manually-set-base" "$head" "$dir"
|
|
3292
|
+
fi
|
|
3293
|
+
else
|
|
3294
|
+
echo "To open a pull request:"
|
|
3295
|
+
echo " git push -u origin ${head}"
|
|
3296
|
+
echo " Open a pull request for ${head} (set the base branch manually)."
|
|
3297
|
+
fi
|
|
3298
|
+
echo ""
|
|
3299
|
+
echo -e "${DIM}Loki does not push or deploy for you; run the command above when ready.${NC}"
|
|
3300
|
+
return 0
|
|
3301
|
+
}
|
|
3302
|
+
|
|
2941
3303
|
# loki why -- actionable failure/outcome diagnosis (B5).
|
|
2942
3304
|
# Reads the already-captured run artifacts (no new state): the terminal run state
|
|
2943
3305
|
# (.loki/<autonomy-state>.json: status, lastExitCode, iterationCount), the durable
|
|
@@ -2951,7 +3313,7 @@ cmd_why() {
|
|
|
2951
3313
|
--json) as_json=1 ;;
|
|
2952
3314
|
--help|-h) echo "Usage: loki why [--json] -- explain the last build's outcome and what to do next"; return 0 ;;
|
|
2953
3315
|
"" ) : ;;
|
|
2954
|
-
*) echo -e "${RED}Unknown flag: $1${NC}"; echo "Usage: loki why [--json]"; return 1 ;;
|
|
3316
|
+
*) echo -e "${RED}Unknown flag: $1${NC}" >&2; echo "Usage: loki why [--json]" >&2; return 1 ;;
|
|
2955
3317
|
esac
|
|
2956
3318
|
|
|
2957
3319
|
local loki_dir="${LOKI_DIR:-.loki}"
|
|
@@ -2985,7 +3347,10 @@ WHYJSON
|
|
|
2985
3347
|
# Human-readable report. The diagnosis maps the terminal status to a plain
|
|
2986
3348
|
# explanation + a concrete next action; everything is sourced from the files,
|
|
2987
3349
|
# nothing is invented.
|
|
3350
|
+
local _why_head_sha
|
|
3351
|
+
_why_head_sha="$(git rev-parse HEAD 2>/dev/null || echo "")"
|
|
2988
3352
|
_LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
|
|
3353
|
+
_LOKI_WHY_HEAD_SHA="$_why_head_sha" \
|
|
2989
3354
|
_LOKI_WHY_HANDOFFS="$loki_dir/memory/handoffs" python3 - <<'WHYTXT'
|
|
2990
3355
|
import json, os, glob
|
|
2991
3356
|
def load(p):
|
|
@@ -2994,7 +3359,22 @@ def load(p):
|
|
|
2994
3359
|
except Exception: return {}
|
|
2995
3360
|
state = load(os.environ.get("_LOKI_WHY_STATE", ""))
|
|
2996
3361
|
comp = load(os.environ.get("_LOKI_WHY_COMPLETION", ""))
|
|
2997
|
-
|
|
3362
|
+
|
|
3363
|
+
# completion.json uses a terminal-outcome vocabulary (complete / max_iterations /
|
|
3364
|
+
# failed / stopped / force_stopped / intervention) that differs from the GUIDE
|
|
3365
|
+
# keys below. Map it onto the GUIDE keys so the diagnosis is never "no mapping".
|
|
3366
|
+
# The live state-file status (running / exited / paused / ...) is preferred when
|
|
3367
|
+
# present; we only fall back to the (normalized) completion outcome.
|
|
3368
|
+
COMP_OUTCOME_ALIASES = {
|
|
3369
|
+
"complete": "council_approved",
|
|
3370
|
+
"max_iterations": "max_iterations_reached",
|
|
3371
|
+
"intervention": "paused",
|
|
3372
|
+
# failed / stopped / force_stopped already match GUIDE keys verbatim.
|
|
3373
|
+
}
|
|
3374
|
+
status = state.get("status")
|
|
3375
|
+
if not status:
|
|
3376
|
+
raw_outcome = comp.get("outcome") or ""
|
|
3377
|
+
status = COMP_OUTCOME_ALIASES.get(raw_outcome, raw_outcome) or "unknown"
|
|
2998
3378
|
exit_code = state.get("lastExitCode")
|
|
2999
3379
|
iters = state.get("iterationCount")
|
|
3000
3380
|
|
|
@@ -3026,10 +3406,25 @@ GUIDE = {
|
|
|
3026
3406
|
"Start a new build, or investigate why a force-stop was needed."),
|
|
3027
3407
|
"running": ("The recorded state says a build is still running (or crashed mid-run).",
|
|
3028
3408
|
"If no build is active it likely crashed; in durable mode (LOKI_DURABLE_STATE=1) a restart resumes, else loki start re-runs."),
|
|
3409
|
+
"exited": ("The build process exited mid-iteration (likely a crash, kill, or empty provider output).",
|
|
3410
|
+
"Read .loki/logs/ for the last error; if nothing is running it crashed -- re-run with loki start, or loki resume in durable mode."),
|
|
3029
3411
|
}
|
|
3030
3412
|
meaning, action = GUIDE.get(status, ("No diagnosis mapping for this status; see the raw fields below.",
|
|
3031
3413
|
"Check loki status and .loki/logs/ for detail."))
|
|
3032
3414
|
|
|
3415
|
+
# The completion.json branch/changes/PR belong to the LAST COMPLETED run. When
|
|
3416
|
+
# the live state shows a build that is still running or crashed mid-iteration
|
|
3417
|
+
# (running/exited), or when the completion record's head_sha no longer matches
|
|
3418
|
+
# the current git HEAD, those fields describe a PREVIOUS run -- not this one.
|
|
3419
|
+
# Label them honestly so a crashed run is never reported with a stale PR/branch.
|
|
3420
|
+
live_statuses = {"running", "exited"}
|
|
3421
|
+
head_sha = os.environ.get("_LOKI_WHY_HEAD_SHA", "")
|
|
3422
|
+
comp_head = comp.get("head_sha", "")
|
|
3423
|
+
comp_is_stale = bool(state.get("status") in live_statuses) or bool(
|
|
3424
|
+
head_sha and comp_head and comp_head != head_sha
|
|
3425
|
+
)
|
|
3426
|
+
comp_label = " (from previous completed run)" if comp_is_stale else ""
|
|
3427
|
+
|
|
3033
3428
|
print("Loki: why")
|
|
3034
3429
|
print("=" * 60)
|
|
3035
3430
|
print(f" Outcome : {status}")
|
|
@@ -3038,11 +3433,11 @@ if exit_code is not None:
|
|
|
3038
3433
|
if iters is not None:
|
|
3039
3434
|
print(f" Iterations : {iters}")
|
|
3040
3435
|
if comp.get("branch"):
|
|
3041
|
-
print(f" Branch : {comp['branch']}")
|
|
3436
|
+
print(f" Branch : {comp['branch']}{comp_label}")
|
|
3042
3437
|
if comp.get("files_changed") is not None:
|
|
3043
|
-
print(f" Changes : {comp.get('files_changed',0)} files (+{comp.get('insertions',0)}/-{comp.get('deletions',0)})")
|
|
3438
|
+
print(f" Changes : {comp.get('files_changed',0)} files (+{comp.get('insertions',0)}/-{comp.get('deletions',0)}){comp_label}")
|
|
3044
3439
|
if comp.get("pr_url"):
|
|
3045
|
-
print(f" PR : {comp['pr_url']}")
|
|
3440
|
+
print(f" PR : {comp['pr_url']}{comp_label}")
|
|
3046
3441
|
print()
|
|
3047
3442
|
print(f" What happened: {meaning}")
|
|
3048
3443
|
print(f" What to do : {action}")
|
|
@@ -3235,6 +3630,43 @@ cmd_status() {
|
|
|
3235
3630
|
fi
|
|
3236
3631
|
fi
|
|
3237
3632
|
|
|
3633
|
+
# v7.82: surface the single most valuable, most shareable fact -- where the
|
|
3634
|
+
# built app is running -- when the app runner reports a live app. Display-only
|
|
3635
|
+
# read of .loki/app-runner/state.json (same shape build_completion_summary
|
|
3636
|
+
# reads); silent when no app is running. The --json path (cmd_status_json) is
|
|
3637
|
+
# untouched, so machine output stays byte-identical.
|
|
3638
|
+
if [ -f "$LOKI_DIR/app-runner/state.json" ]; then
|
|
3639
|
+
local app_url
|
|
3640
|
+
app_url=$(_LOKI_APP_STATE="$LOKI_DIR/app-runner/state.json" python3 -c "
|
|
3641
|
+
import json, os
|
|
3642
|
+
try:
|
|
3643
|
+
d = json.load(open(os.environ['_LOKI_APP_STATE']))
|
|
3644
|
+
print(d.get('url', '') if d.get('status') == 'running' else '')
|
|
3645
|
+
except Exception:
|
|
3646
|
+
print('')" 2>/dev/null)
|
|
3647
|
+
if [ -n "$app_url" ]; then
|
|
3648
|
+
echo ""
|
|
3649
|
+
echo -e "${GREEN}App running:${NC} ${BOLD}${app_url}${NC} ${DIM}- open it to try it${NC}"
|
|
3650
|
+
fi
|
|
3651
|
+
fi
|
|
3652
|
+
|
|
3653
|
+
# v7.82: completion-aware one-liner. When a finished run left a completion
|
|
3654
|
+
# record, show its outcome and point at \`loki why\` for the full story. Reads
|
|
3655
|
+
# the persisted completion.json (no recompute); silent when absent.
|
|
3656
|
+
if [ -f "$LOKI_DIR/state/completion.json" ]; then
|
|
3657
|
+
local last_outcome
|
|
3658
|
+
last_outcome=$(_LOKI_CJ="$LOKI_DIR/state/completion.json" python3 -c "
|
|
3659
|
+
import json, os
|
|
3660
|
+
try:
|
|
3661
|
+
d = json.load(open(os.environ['_LOKI_CJ']))
|
|
3662
|
+
print(d.get('outcome', '') or '')
|
|
3663
|
+
except Exception:
|
|
3664
|
+
print('')" 2>/dev/null)
|
|
3665
|
+
if [ -n "$last_outcome" ]; then
|
|
3666
|
+
echo -e "${DIM}Last run: ${last_outcome} - run 'loki why' for details${NC}"
|
|
3667
|
+
fi
|
|
3668
|
+
fi
|
|
3669
|
+
|
|
3238
3670
|
echo ""
|
|
3239
3671
|
echo -e "${DIM} Tip: loki analyze context show - detailed token breakdown${NC}"
|
|
3240
3672
|
echo -e "${DIM} Tip: loki analyze code overview - codebase intelligence${NC}"
|
|
@@ -4817,7 +5249,7 @@ cmd_welcome_terminal() {
|
|
|
4817
5249
|
echo -e " ${CYAN}Cross-project memory${NC} Lessons compound, so agents stop repeating mistakes."
|
|
4818
5250
|
echo -e " ${CYAN}Provider-agnostic${NC} Claude, Codex, Cline, or Aider. Your keys, your infra."
|
|
4819
5251
|
echo ""
|
|
4820
|
-
echo -e " Quick start: ${BOLD}loki start ./prd.md${NC}"
|
|
5252
|
+
echo -e " Quick start: ${BOLD}loki quickstart${NC} (from your idea) or ${BOLD}loki start ./prd.md${NC} (from a spec)"
|
|
4821
5253
|
echo -e " Docs: ${BOLD}https://www.autonomi.dev/docs${NC}"
|
|
4822
5254
|
echo ""
|
|
4823
5255
|
if _loki_welcome_analytics_on; then
|
|
@@ -5767,12 +6199,20 @@ cmd_preview() {
|
|
|
5767
6199
|
public=true
|
|
5768
6200
|
;;
|
|
5769
6201
|
--provider)
|
|
5770
|
-
|
|
5771
|
-
#
|
|
5772
|
-
#
|
|
5773
|
-
#
|
|
5774
|
-
|
|
5775
|
-
|
|
6202
|
+
# Guard against a missing OR flag-shaped value: an unguarded
|
|
6203
|
+
# shift when --provider is the last arg underflows under set -e,
|
|
6204
|
+
# and a bare "--provider --public" would otherwise swallow the
|
|
6205
|
+
# next flag as the provider name.
|
|
6206
|
+
case "${2:-}" in
|
|
6207
|
+
-*|"")
|
|
6208
|
+
echo "loki preview --provider requires a value (cloudflared|ngrok)" >&2
|
|
6209
|
+
return 1
|
|
6210
|
+
;;
|
|
6211
|
+
*)
|
|
6212
|
+
provider="$2"
|
|
6213
|
+
shift
|
|
6214
|
+
;;
|
|
6215
|
+
esac
|
|
5776
6216
|
;;
|
|
5777
6217
|
--yes)
|
|
5778
6218
|
assume_yes=true
|
|
@@ -6122,11 +6562,14 @@ cmd_deploy() {
|
|
|
6122
6562
|
return 0
|
|
6123
6563
|
;;
|
|
6124
6564
|
--dir)
|
|
6125
|
-
|
|
6126
|
-
#
|
|
6127
|
-
#
|
|
6128
|
-
#
|
|
6129
|
-
|
|
6565
|
+
# Guard against a missing OR flag-shaped value: an unguarded
|
|
6566
|
+
# shift when --dir is the last arg underflows under set -e, and a
|
|
6567
|
+
# bare "--dir --no-clip" would otherwise treat the next flag as
|
|
6568
|
+
# the scan directory.
|
|
6569
|
+
case "${2:-}" in
|
|
6570
|
+
-*|"") echo "loki deploy --dir requires a directory path" >&2; return 1 ;;
|
|
6571
|
+
*) dir="$2"; shift ;;
|
|
6572
|
+
esac
|
|
6130
6573
|
;;
|
|
6131
6574
|
--no-clip)
|
|
6132
6575
|
do_clip=false
|
|
@@ -8013,7 +8456,7 @@ cmd_assets() {
|
|
|
8013
8456
|
;;
|
|
8014
8457
|
*)
|
|
8015
8458
|
echo -e "${RED}Unknown subcommand: $subcommand${NC}" >&2
|
|
8016
|
-
echo "Run 'loki assets --help' for usage."
|
|
8459
|
+
echo "Run 'loki assets --help' for usage." >&2
|
|
8017
8460
|
return 1
|
|
8018
8461
|
;;
|
|
8019
8462
|
esac
|
|
@@ -9464,11 +9907,17 @@ cmd_doctor() {
|
|
|
9464
9907
|
return 1
|
|
9465
9908
|
elif [ "$warn_count" -gt 0 ]; then
|
|
9466
9909
|
echo -e "${YELLOW}All required checks passed with some warnings.${NC}"
|
|
9467
|
-
return 0
|
|
9468
9910
|
else
|
|
9469
9911
|
echo -e "${GREEN}All checks passed. System is ready for Loki Mode.${NC}"
|
|
9470
|
-
return 0
|
|
9471
9912
|
fi
|
|
9913
|
+
# Setup verified (no required check failed): hand the user straight to a
|
|
9914
|
+
# first build with a copy-paste command, so they never dead-end here. Gated
|
|
9915
|
+
# on fail_count==0 above, so a failing setup is never told to build. The
|
|
9916
|
+
# --json path returns long before this code, so machine output is untouched.
|
|
9917
|
+
echo ""
|
|
9918
|
+
echo "Next: loki quickstart (guided first build from your idea, no PRD needed)"
|
|
9919
|
+
echo " or loki demo (builds a sample todo app end to end) or loki start ./prd.md"
|
|
9920
|
+
return 0
|
|
9472
9921
|
}
|
|
9473
9922
|
|
|
9474
9923
|
# JSON output for loki doctor --json
|
|
@@ -11081,6 +11530,10 @@ cmd_quick() {
|
|
|
11081
11530
|
# BUG-PU-005: Use unique filename to prevent race conditions when
|
|
11082
11531
|
# multiple simultaneous `loki quick` commands run in the same project
|
|
11083
11532
|
mkdir -p "$LOKI_DIR"
|
|
11533
|
+
# Reap stale per-PID temp PRDs from prior runs (the process exec-replaces
|
|
11534
|
+
# into run.sh, so these are never cleaned on exit and accumulate).
|
|
11535
|
+
find "$LOKI_DIR" -maxdepth 1 -name 'quick-prd-*.md' -mtime +1 -delete 2>/dev/null || true
|
|
11536
|
+
find "$LOKI_DIR" -maxdepth 1 -name 'brief-prd-*.md' -mtime +1 -delete 2>/dev/null || true
|
|
11084
11537
|
local quick_prd="$LOKI_DIR/quick-prd-$$.md"
|
|
11085
11538
|
cat > "$quick_prd" << QPRDEOF
|
|
11086
11539
|
# Quick Task
|
|
@@ -15482,12 +15935,22 @@ cmd_plan() {
|
|
|
15482
15935
|
done
|
|
15483
15936
|
|
|
15484
15937
|
if [ -z "$prd_file" ]; then
|
|
15938
|
+
# Under --json, a machine consumer expects JSON on every path, including
|
|
15939
|
+
# errors -- never ANSI-colored prose. Emit a structured error to stdout.
|
|
15940
|
+
if [ "$show_json" = true ]; then
|
|
15941
|
+
printf '{"error":"missing PRD file argument"}\n'
|
|
15942
|
+
return 2
|
|
15943
|
+
fi
|
|
15485
15944
|
echo -e "${RED}Usage: loki plan <PRD file>${NC}"
|
|
15486
15945
|
echo "Run 'loki plan --help' for usage."
|
|
15487
15946
|
return 2
|
|
15488
15947
|
fi
|
|
15489
15948
|
|
|
15490
15949
|
if [ ! -f "$prd_file" ]; then
|
|
15950
|
+
if [ "$show_json" = true ]; then
|
|
15951
|
+
printf '{"error":"PRD file not found","prd_file":"%s"}\n' "$prd_file"
|
|
15952
|
+
return 1
|
|
15953
|
+
fi
|
|
15491
15954
|
echo -e "${RED}PRD file not found: $prd_file${NC}"
|
|
15492
15955
|
return 1
|
|
15493
15956
|
fi
|
|
@@ -15528,6 +15991,60 @@ maybe_show_auto_plan() {
|
|
|
15528
15991
|
show_prd_plan "$abs_prd" "false" "false"
|
|
15529
15992
|
}
|
|
15530
15993
|
|
|
15994
|
+
# Suggest the closest known command for a typo (the did-you-mean hint).
|
|
15995
|
+
# Prints "Did you mean 'X'?" to stderr when an unknown command is within a
|
|
15996
|
+
# small Levenshtein distance of a real command. Silent (returns 1) when
|
|
15997
|
+
# nothing is close enough, so a genuinely-unknown command is not given a
|
|
15998
|
+
# misleading suggestion. Pure bash + awk (no extra deps); the candidate list
|
|
15999
|
+
# is the canonical set of top-level commands accepted by the dispatcher.
|
|
16000
|
+
_suggest_command() {
|
|
16001
|
+
local typo="$1"
|
|
16002
|
+
[ -n "$typo" ] || return 1
|
|
16003
|
+
# Canonical top-level command names (keep in sync with the dispatch case
|
|
16004
|
+
# below). Deprecated aliases are intentionally included so a typo of an
|
|
16005
|
+
# alias still resolves to a helpful pointer.
|
|
16006
|
+
local known="start run quick plan grill spec verify proof trust review \
|
|
16007
|
+
ultracode council demo dogfood heal modernize import issue github init template \
|
|
16008
|
+
status stop pause resume monitor watch watchdog dashboard web open preview share \
|
|
16009
|
+
memory context ctx checkpoint state report kpis cost metrics stats logs otel \
|
|
16010
|
+
syslog telemetry crash config provider doctor setup-skill onboard quickstart \
|
|
16011
|
+
welcome update self-update rollback failover cluster enterprise remote deploy docker \
|
|
16012
|
+
sandbox api mcp magic compound assets export wiki docs explain why audit compliance \
|
|
16013
|
+
secrets analyze optimize bench ci reset cleanup notify trigger voice sentrux \
|
|
16014
|
+
worktree wt projects cp rc trust-metrics serve agent code self_update test help version"
|
|
16015
|
+
local best
|
|
16016
|
+
best=$(printf '%s\n' $known | awk -v t="$typo" '
|
|
16017
|
+
function min3(a, b, c) { if (a < b) { if (a < c) return a; return c } if (b < c) return b; return c }
|
|
16018
|
+
function lev(s1, s2, n, m, i, j, prev, cur, cost, tmp) {
|
|
16019
|
+
n = length(s1); m = length(s2)
|
|
16020
|
+
if (n == 0) return m
|
|
16021
|
+
if (m == 0) return n
|
|
16022
|
+
for (j = 0; j <= m; j++) prev[j] = j
|
|
16023
|
+
for (i = 1; i <= n; i++) {
|
|
16024
|
+
cur[0] = i
|
|
16025
|
+
for (j = 1; j <= m; j++) {
|
|
16026
|
+
cost = (substr(s1, i, 1) == substr(s2, j, 1)) ? 0 : 1
|
|
16027
|
+
cur[j] = min3(prev[j] + 1, cur[j-1] + 1, prev[j-1] + cost)
|
|
16028
|
+
}
|
|
16029
|
+
for (j = 0; j <= m; j++) prev[j] = cur[j]
|
|
16030
|
+
}
|
|
16031
|
+
return prev[m]
|
|
16032
|
+
}
|
|
16033
|
+
{ d = lev(t, $1); if (d < bestd || NR == 1) { bestd = d; bestc = $1 } }
|
|
16034
|
+
END {
|
|
16035
|
+
# Only suggest when the edit distance is small relative to the typo
|
|
16036
|
+
# length: <=2 for short commands, <=3 for longer ones. This avoids
|
|
16037
|
+
# absurd suggestions (e.g. "xyz" -> "web").
|
|
16038
|
+
thr = (length(t) <= 4) ? 2 : 3
|
|
16039
|
+
if (bestd <= thr) print bestc
|
|
16040
|
+
}')
|
|
16041
|
+
if [ -n "$best" ]; then
|
|
16042
|
+
printf '%s\n' "$best"
|
|
16043
|
+
return 0
|
|
16044
|
+
fi
|
|
16045
|
+
return 1
|
|
16046
|
+
}
|
|
16047
|
+
|
|
15531
16048
|
# Main command dispatcher
|
|
15532
16049
|
main() {
|
|
15533
16050
|
# v7.5.18: early guard -- LOKI_PROVIDER=gemini is no longer supported.
|
|
@@ -15622,6 +16139,9 @@ main() {
|
|
|
15622
16139
|
why)
|
|
15623
16140
|
cmd_why "$@"
|
|
15624
16141
|
;;
|
|
16142
|
+
next)
|
|
16143
|
+
cmd_next "$@"
|
|
16144
|
+
;;
|
|
15625
16145
|
stats)
|
|
15626
16146
|
# CLI consolidation (Phase A): 'stats' is a deprecated alias of
|
|
15627
16147
|
# 'report session'. On the Bun route this arm is never reached
|
|
@@ -15645,6 +16165,9 @@ main() {
|
|
|
15645
16165
|
deploy)
|
|
15646
16166
|
cmd_deploy "$@"
|
|
15647
16167
|
;;
|
|
16168
|
+
ship)
|
|
16169
|
+
cmd_ship "$@"
|
|
16170
|
+
;;
|
|
15648
16171
|
open)
|
|
15649
16172
|
# CLI consolidation (Phase A): 'open' is a deprecated alias of 'preview'.
|
|
15650
16173
|
_deprecated_alias open preview "$@"
|
|
@@ -15952,8 +16475,12 @@ main() {
|
|
|
15952
16475
|
fi
|
|
15953
16476
|
;;
|
|
15954
16477
|
*)
|
|
15955
|
-
echo -e "${RED}Unknown command: $command${NC}"
|
|
15956
|
-
|
|
16478
|
+
echo -e "${RED}Unknown command: $command${NC}" >&2
|
|
16479
|
+
local _suggestion
|
|
16480
|
+
if _suggestion=$(_suggest_command "$command"); then
|
|
16481
|
+
echo "Did you mean 'loki ${_suggestion}'?" >&2
|
|
16482
|
+
fi
|
|
16483
|
+
echo "Run 'loki help' for usage." >&2
|
|
15957
16484
|
exit 1
|
|
15958
16485
|
;;
|
|
15959
16486
|
esac
|
|
@@ -18329,9 +18856,15 @@ try:
|
|
|
18329
18856
|
print('No relevant memories found')
|
|
18330
18857
|
else:
|
|
18331
18858
|
for i, r in enumerate(results, 1):
|
|
18332
|
-
|
|
18333
|
-
|
|
18334
|
-
score
|
|
18859
|
+
# Retrieval results expose _source/_score/_weighted_score and the
|
|
18860
|
+
# raw stored fields (pattern/description/goal/name), not
|
|
18861
|
+
# source/summary/score. Read the fields that actually exist so the
|
|
18862
|
+
# output is not a uniform [unknown] ... (score 0.00).
|
|
18863
|
+
source = r.get('_source', 'unknown')
|
|
18864
|
+
score = r.get('_score', r.get('_weighted_score', 0))
|
|
18865
|
+
summary = (r.get('pattern') or r.get('description') or r.get('goal')
|
|
18866
|
+
or (r.get('context') or {}).get('goal') or r.get('name')
|
|
18867
|
+
or 'No summary')[:80]
|
|
18335
18868
|
print(f'{i}. [{source}] {summary}... (score: {score:.2f})')
|
|
18336
18869
|
except ImportError as e:
|
|
18337
18870
|
print(f'Error: Required module not found - {e}')
|
|
@@ -21892,7 +22425,7 @@ cmd_trust_metrics() {
|
|
|
21892
22425
|
echo "Run 'loki trust-metrics' inside each project directory."
|
|
21893
22426
|
exit 2
|
|
21894
22427
|
;;
|
|
21895
|
-
*) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki trust-metrics --help' for usage."; exit 1 ;;
|
|
22428
|
+
*) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki trust-metrics --help' for usage." >&2; exit 1 ;;
|
|
21896
22429
|
esac
|
|
21897
22430
|
done
|
|
21898
22431
|
|
|
@@ -21947,7 +22480,7 @@ cmd_cost() {
|
|
|
21947
22480
|
--json) show_json=true; shift ;;
|
|
21948
22481
|
--last) last_n="${2:-0}"; shift 2 ;;
|
|
21949
22482
|
--last=*) last_n="${1#*=}"; shift ;;
|
|
21950
|
-
*) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki cost --help' for usage."; exit 1 ;;
|
|
22483
|
+
*) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki cost --help' for usage." >&2; exit 1 ;;
|
|
21951
22484
|
esac
|
|
21952
22485
|
done
|
|
21953
22486
|
|
|
@@ -22262,7 +22795,7 @@ cmd_metrics() {
|
|
|
22262
22795
|
echo "$response"
|
|
22263
22796
|
exit 0
|
|
22264
22797
|
;;
|
|
22265
|
-
*) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki metrics --help' for usage."; exit 1 ;;
|
|
22798
|
+
*) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki metrics --help' for usage." >&2; exit 1 ;;
|
|
22266
22799
|
esac
|
|
22267
22800
|
done
|
|
22268
22801
|
|
|
@@ -23336,12 +23869,15 @@ _code_diff() {
|
|
|
23336
23869
|
# Output shell completion scripts
|
|
23337
23870
|
cmd_completions() {
|
|
23338
23871
|
local shell="${1:-bash}"
|
|
23872
|
+
# Consume the subcommand so the install arm can read an optional shell
|
|
23873
|
+
# override as its own positional ($1). bash/zsh raw-output arms ignore $@.
|
|
23874
|
+
shift 2>/dev/null || true
|
|
23339
23875
|
local skill_dir
|
|
23340
|
-
|
|
23876
|
+
|
|
23341
23877
|
# Find the skill directory (where autonomy/loki is located)
|
|
23342
23878
|
skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
23343
23879
|
local completions_dir="$skill_dir/completions"
|
|
23344
|
-
|
|
23880
|
+
|
|
23345
23881
|
case "$shell" in
|
|
23346
23882
|
bash)
|
|
23347
23883
|
if [ -f "$completions_dir/loki.bash" ]; then
|
|
@@ -23359,24 +23895,77 @@ cmd_completions() {
|
|
|
23359
23895
|
exit 1
|
|
23360
23896
|
fi
|
|
23361
23897
|
;;
|
|
23898
|
+
install)
|
|
23899
|
+
# One-command install: detect the current shell, append a single
|
|
23900
|
+
# guarded line to the right rc file exactly once (idempotent via a
|
|
23901
|
+
# sentinel comment), then tell the user how to activate it. This
|
|
23902
|
+
# replaces the error-prone manual recipe with something that just
|
|
23903
|
+
# works after one command.
|
|
23904
|
+
local detected target_shell rc_file
|
|
23905
|
+
detected="$(basename "${SHELL:-}" 2>/dev/null)"
|
|
23906
|
+
# Allow an explicit override: loki completions install bash|zsh
|
|
23907
|
+
target_shell="${1:-$detected}"
|
|
23908
|
+
case "$target_shell" in
|
|
23909
|
+
bash)
|
|
23910
|
+
rc_file="${HOME}/.bashrc"
|
|
23911
|
+
;;
|
|
23912
|
+
zsh)
|
|
23913
|
+
rc_file="${HOME}/.zshrc"
|
|
23914
|
+
;;
|
|
23915
|
+
*)
|
|
23916
|
+
echo -e "${RED}Could not detect a supported shell (got: ${target_shell:-unknown}).${NC}" >&2
|
|
23917
|
+
echo "Specify one explicitly: loki completions install bash | loki completions install zsh" >&2
|
|
23918
|
+
exit 1
|
|
23919
|
+
;;
|
|
23920
|
+
esac
|
|
23921
|
+
local sentinel="# loki-mode completions"
|
|
23922
|
+
if [ -f "$rc_file" ] && grep -qF "$sentinel" "$rc_file" 2>/dev/null; then
|
|
23923
|
+
echo "Loki completions already installed in ${rc_file} (no change)."
|
|
23924
|
+
echo "Activate now with: source ${rc_file}"
|
|
23925
|
+
return 0
|
|
23926
|
+
fi
|
|
23927
|
+
# Append the sentinel plus a single eval line that re-loads the
|
|
23928
|
+
# completion script at shell start. eval (not redirect) is the
|
|
23929
|
+
# correct persistent form: it sources the script into the
|
|
23930
|
+
# interactive shell each time the rc file runs.
|
|
23931
|
+
{
|
|
23932
|
+
printf '\n%s\n' "$sentinel"
|
|
23933
|
+
printf '%s\n' "eval \"\$(loki completions ${target_shell})\""
|
|
23934
|
+
} >> "$rc_file" || {
|
|
23935
|
+
echo -e "${RED}Failed to write to ${rc_file}.${NC}" >&2
|
|
23936
|
+
exit 1
|
|
23937
|
+
}
|
|
23938
|
+
echo "Installed Loki ${target_shell} completions into ${rc_file}."
|
|
23939
|
+
echo "Activate now with: source ${rc_file} (or restart your shell)"
|
|
23940
|
+
return 0
|
|
23941
|
+
;;
|
|
23362
23942
|
*)
|
|
23363
23943
|
echo -e "${BOLD}Loki Shell Completions${NC}"
|
|
23364
23944
|
echo ""
|
|
23365
23945
|
echo "Output shell completion scripts for bash or zsh."
|
|
23366
23946
|
echo ""
|
|
23367
23947
|
echo "Usage: loki completions <shell>"
|
|
23948
|
+
echo " loki completions install [bash|zsh]"
|
|
23368
23949
|
echo ""
|
|
23369
23950
|
echo "Shells:"
|
|
23370
|
-
echo " bash
|
|
23371
|
-
echo " zsh
|
|
23951
|
+
echo " bash Bash completion script"
|
|
23952
|
+
echo " zsh Zsh completion script"
|
|
23953
|
+
echo " install Auto-install completions into your shell rc file"
|
|
23372
23954
|
echo ""
|
|
23373
23955
|
echo "Installation:"
|
|
23374
23956
|
echo ""
|
|
23375
|
-
echo "
|
|
23376
|
-
echo "
|
|
23957
|
+
echo " One command (recommended):"
|
|
23958
|
+
echo " loki completions install"
|
|
23959
|
+
echo ""
|
|
23960
|
+
echo " Manual (persistent), Bash:"
|
|
23961
|
+
echo " echo 'eval \"\$(loki completions bash)\"' >> ~/.bashrc"
|
|
23962
|
+
echo " Manual (persistent), Zsh:"
|
|
23963
|
+
echo " echo 'eval \"\$(loki completions zsh)\"' >> ~/.zshrc"
|
|
23377
23964
|
echo ""
|
|
23378
|
-
echo "
|
|
23379
|
-
echo "
|
|
23965
|
+
echo " Session only (current shell), Bash:"
|
|
23966
|
+
echo " eval \"\$(loki completions bash)\""
|
|
23967
|
+
echo " Session only (current shell), Zsh:"
|
|
23968
|
+
echo " eval \"\$(loki completions zsh)\""
|
|
23380
23969
|
echo ""
|
|
23381
23970
|
exit 1
|
|
23382
23971
|
;;
|
|
@@ -29444,7 +30033,7 @@ cmd_share() {
|
|
|
29444
30033
|
--private) visibility=""; shift ;;
|
|
29445
30034
|
--format) format="${2:-markdown}"; shift 2 ;;
|
|
29446
30035
|
--format=*) format="${1#*=}"; shift ;;
|
|
29447
|
-
*) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki share --help' for usage."; exit 1 ;;
|
|
30036
|
+
*) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki share --help' for usage." >&2; exit 1 ;;
|
|
29448
30037
|
esac
|
|
29449
30038
|
done
|
|
29450
30039
|
|
|
@@ -30143,7 +30732,14 @@ cmd_docker() {
|
|
|
30143
30732
|
local -a fwd=()
|
|
30144
30733
|
while [ $# -gt 0 ]; do
|
|
30145
30734
|
case "$1" in
|
|
30146
|
-
--image)
|
|
30735
|
+
--image)
|
|
30736
|
+
# Guard against a missing OR flag-shaped value so "--image
|
|
30737
|
+
# --dry-run" cannot swallow the next flag as the image ref.
|
|
30738
|
+
case "${2:-}" in
|
|
30739
|
+
-*|"") echo "loki docker --image requires a value (e.g., --image asklokesh/loki-mode:latest)" >&2; return 1 ;;
|
|
30740
|
+
*) shift; export LOKI_DOCKER_IMAGE="$1"; shift ;;
|
|
30741
|
+
esac
|
|
30742
|
+
;;
|
|
30147
30743
|
--dry-run) dry_run=1; shift ;;
|
|
30148
30744
|
--api) with_api=1; fwd+=("$1"); shift ;;
|
|
30149
30745
|
*) fwd+=("$1"); shift ;;
|