loki-mode 7.78.0 → 7.80.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/autonomy/loki CHANGED
@@ -125,6 +125,16 @@ if [ -f "$_LOKI_SCRIPT_DIR/lib/git-pr-advisory.sh" ]; then
125
125
  source "$_LOKI_SCRIPT_DIR/lib/git-pr-advisory.sh"
126
126
  fi
127
127
 
128
+ # Unified config-file support (#691). Canonical LOKI_CONFIG_MAP + the config-file
129
+ # loader (loki_maybe_apply_config_file / loki_apply_config_file) and the
130
+ # example/schema/validate generators. Side-effect-free on source: defines the
131
+ # array + functions, exports nothing. The main() pre-pass and run.sh's YAML
132
+ # parsers both call into this single lib so the mapping cannot drift.
133
+ if [ -f "$_LOKI_SCRIPT_DIR/lib/config-map.sh" ]; then
134
+ # shellcheck source=autonomy/lib/config-map.sh
135
+ source "$_LOKI_SCRIPT_DIR/lib/config-map.sh"
136
+ fi
137
+
128
138
  # Resolve the script's real path (handles symlinks)
129
139
  resolve_script_path() {
130
140
  local script="$1"
@@ -1473,6 +1483,24 @@ cmd_start() {
1473
1483
  issue_mode_args+=("--detach")
1474
1484
  shift
1475
1485
  ;;
1486
+ # #691: unified config-file aliases. Already applied by the main()
1487
+ # pre-pass (loki_maybe_apply_config_file); these arms only consume
1488
+ # the flag+path so the loop does not misread the path as the PRD
1489
+ # positional, and do NOT forward it to the run.sh exec target.
1490
+ --config|--vars|--env-file)
1491
+ # Guard the shift 2: a dangling flag (path omitted, flag is the
1492
+ # last arg) would underflow shift 2 and, under set -euo pipefail,
1493
+ # abort the command SILENTLY. Give an honest actionable error.
1494
+ if [ "$#" -ge 2 ]; then
1495
+ shift 2
1496
+ else
1497
+ echo "loki: $1 requires a file path (e.g. --config .loki/config.yaml)" >&2
1498
+ exit 1
1499
+ fi
1500
+ ;;
1501
+ --config=*|--vars=*|--env-file=*)
1502
+ shift
1503
+ ;;
1476
1504
  -*)
1477
1505
  echo -e "${RED}Unknown option: $1${NC}"
1478
1506
  exit 1
@@ -2899,6 +2927,133 @@ cmd_resume() {
2899
2927
  fi
2900
2928
  }
2901
2929
 
2930
+ # loki why -- actionable failure/outcome diagnosis (B5).
2931
+ # Reads the already-captured run artifacts (no new state): the terminal run state
2932
+ # (.loki/<autonomy-state>.json: status, lastExitCode, iterationCount), the durable
2933
+ # completion record (.loki/state/completion.json: outcome, branch, files, pr_url),
2934
+ # and the latest structured handoff (.loki/memory/handoffs/*.md). Produces an
2935
+ # honest "what happened + what to do" report. Read-only; never fabricates -- if a
2936
+ # field was not captured it says so. --json emits the machine-readable record.
2937
+ cmd_why() {
2938
+ local as_json=0
2939
+ case "${1:-}" in
2940
+ --json) as_json=1 ;;
2941
+ --help|-h) echo "Usage: loki why [--json] -- explain the last build's outcome and what to do next"; return 0 ;;
2942
+ "" ) : ;;
2943
+ *) echo -e "${RED}Unknown flag: $1${NC}"; echo "Usage: loki why [--json]"; return 1 ;;
2944
+ esac
2945
+
2946
+ local loki_dir="${LOKI_DIR:-.loki}"
2947
+ # Session-namespaced state when LOKI_SESSION_ID is set (mirrors run.sh A6).
2948
+ local state_file="$loki_dir/autonomy-state.json"
2949
+ if [ -n "${LOKI_SESSION_ID:-}" ] && [ -f "$loki_dir/sessions/${LOKI_SESSION_ID}/autonomy-state.json" ]; then
2950
+ state_file="$loki_dir/sessions/${LOKI_SESSION_ID}/autonomy-state.json"
2951
+ fi
2952
+ local completion_file="$loki_dir/state/completion.json"
2953
+
2954
+ if [ ! -f "$state_file" ] && [ ! -f "$completion_file" ]; then
2955
+ echo "loki why: no run found here yet (no $state_file or $completion_file)." >&2
2956
+ echo "Run a build first: loki start <spec>" >&2
2957
+ return 1
2958
+ fi
2959
+
2960
+ if [ "$as_json" = "1" ]; then
2961
+ _LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" python3 - <<'WHYJSON'
2962
+ import json, os
2963
+ def load(p):
2964
+ try:
2965
+ with open(p) as f: return json.load(f)
2966
+ except Exception: return {}
2967
+ state = load(os.environ.get("_LOKI_WHY_STATE", ""))
2968
+ comp = load(os.environ.get("_LOKI_WHY_COMPLETION", ""))
2969
+ print(json.dumps({"state": state, "completion": comp}, indent=2))
2970
+ WHYJSON
2971
+ return 0
2972
+ fi
2973
+
2974
+ # Human-readable report. The diagnosis maps the terminal status to a plain
2975
+ # explanation + a concrete next action; everything is sourced from the files,
2976
+ # nothing is invented.
2977
+ _LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
2978
+ _LOKI_WHY_HANDOFFS="$loki_dir/memory/handoffs" python3 - <<'WHYTXT'
2979
+ import json, os, glob
2980
+ def load(p):
2981
+ try:
2982
+ with open(p) as f: return json.load(f)
2983
+ except Exception: return {}
2984
+ state = load(os.environ.get("_LOKI_WHY_STATE", ""))
2985
+ comp = load(os.environ.get("_LOKI_WHY_COMPLETION", ""))
2986
+ status = state.get("status") or comp.get("outcome") or "unknown"
2987
+ exit_code = state.get("lastExitCode")
2988
+ iters = state.get("iterationCount")
2989
+
2990
+ # status -> (one-line meaning, suggested next action). Honest + specific.
2991
+ GUIDE = {
2992
+ "council_approved": ("The completion council agreed the work is done and verified.",
2993
+ "Review the diff and open a PR (git push + gh pr create, or LOKI_AUTO_PR=1)."),
2994
+ "council_force_approved": ("Completion was force-approved (council could not fully converge).",
2995
+ "Review the diff carefully before merging -- convergence was not unanimous."),
2996
+ "completion_promise_fulfilled": ("The agent declared its explicit completion promise fulfilled.",
2997
+ "Verify the promised outcome, then review and PR."),
2998
+ "max_iterations_reached": ("The build hit the iteration cap before the council approved it.",
2999
+ "Inspect what is left (loki status), raise LOKI_MAX_ITERATIONS or narrow the spec, and resume."),
3000
+ "max_retries_exceeded": ("The build exhausted its retry budget on a repeating failure.",
3001
+ "Read .loki/logs for the recurring error (rate limit? failing test?), fix the root cause, then re-run."),
3002
+ "failed": ("The build ended in a failure state.",
3003
+ "Read .loki/logs/ + the handoff below for the failure, fix it, then re-run."),
3004
+ "policy_blocked": ("A policy/trust gate blocked completion.",
3005
+ "Review the blocking finding; address it or use the documented override path."),
3006
+ "budget_exceeded": ("The cost budget breaker paused the build.",
3007
+ "Raise LOKI_BUDGET_LIMIT or accept the partial result, then resume."),
3008
+ "paused": ("The build is paused (human-intervention signal).",
3009
+ "Resume with: loki resume."),
3010
+ "interrupted": ("The build was interrupted before a terminal state.",
3011
+ "Resume with: loki resume."),
3012
+ "stopped": ("The build was stopped by the operator.",
3013
+ "Start a new build with loki start, or resume if you meant to continue."),
3014
+ "force_stopped": ("The build was force-stopped.",
3015
+ "Start a new build, or investigate why a force-stop was needed."),
3016
+ "running": ("The recorded state says a build is still running (or crashed mid-run).",
3017
+ "If no build is active it likely crashed; in durable mode (LOKI_DURABLE_STATE=1) a restart resumes, else loki start re-runs."),
3018
+ }
3019
+ meaning, action = GUIDE.get(status, ("No diagnosis mapping for this status; see the raw fields below.",
3020
+ "Check loki status and .loki/logs/ for detail."))
3021
+
3022
+ print("Loki: why")
3023
+ print("=" * 60)
3024
+ print(f" Outcome : {status}")
3025
+ if exit_code is not None:
3026
+ print(f" Exit code : {exit_code}")
3027
+ if iters is not None:
3028
+ print(f" Iterations : {iters}")
3029
+ if comp.get("branch"):
3030
+ print(f" Branch : {comp['branch']}")
3031
+ if comp.get("files_changed") is not None:
3032
+ print(f" Changes : {comp.get('files_changed',0)} files (+{comp.get('insertions',0)}/-{comp.get('deletions',0)})")
3033
+ if comp.get("pr_url"):
3034
+ print(f" PR : {comp['pr_url']}")
3035
+ print()
3036
+ print(f" What happened: {meaning}")
3037
+ print(f" What to do : {action}")
3038
+
3039
+ # Surface the latest structured handoff (already-captured context), honestly.
3040
+ hd = sorted(glob.glob(os.path.join(os.environ.get("_LOKI_WHY_HANDOFFS",""), "*.md")))
3041
+ if hd:
3042
+ print()
3043
+ print(f" Latest handoff: {hd[-1]}")
3044
+ try:
3045
+ with open(hd[-1]) as f:
3046
+ head = "".join(f.readlines()[:8]).rstrip()
3047
+ for line in head.splitlines():
3048
+ print(f" {line}")
3049
+ except Exception:
3050
+ pass
3051
+ print()
3052
+ print(" (loki why --json for the raw record; loki status for live state.)")
3053
+ WHYTXT
3054
+ return 0
3055
+ }
3056
+
2902
3057
  # Show current status
2903
3058
  cmd_status() {
2904
3059
  # Check for flags
@@ -6701,6 +6856,21 @@ cmd_run() {
6701
6856
  run_detached=true
6702
6857
  shift
6703
6858
  ;;
6859
+ # #691: unified config-file aliases. Applied by the main() pre-pass;
6860
+ # consume here so the path is not misread as the issue-ref positional
6861
+ # and is not forwarded to cmd_start.
6862
+ --config|--vars|--env-file)
6863
+ # Guard shift 2 against a dangling flag (silent set -e abort).
6864
+ if [ "$#" -ge 2 ]; then
6865
+ shift 2
6866
+ else
6867
+ echo "loki: $1 requires a file path (e.g. --config .loki/config.yaml)" >&2
6868
+ exit 1
6869
+ fi
6870
+ ;;
6871
+ --config=*|--vars=*|--env-file=*)
6872
+ shift
6873
+ ;;
6704
6874
  -*)
6705
6875
  echo -e "${RED}Unknown option: $1${NC}"
6706
6876
  echo "Run 'loki run --help' for usage."
@@ -8109,15 +8279,27 @@ cmd_config() {
8109
8279
  get)
8110
8280
  cmd_config_get "$@"
8111
8281
  ;;
8282
+ example)
8283
+ cmd_config_example "$@"
8284
+ ;;
8285
+ schema)
8286
+ cmd_config_schema "$@"
8287
+ ;;
8288
+ validate)
8289
+ cmd_config_validate "$@"
8290
+ ;;
8112
8291
  *)
8113
- echo -e "${YELLOW}Usage: loki config [show|init|edit|path|set|get]${NC}"
8114
- echo ""
8115
- echo " show Show current configuration (default)"
8116
- echo " init Create a config file from template"
8117
- echo " edit Open config file in editor"
8118
- echo " path Show config file paths"
8119
- echo " set KEY VALUE Set a configuration value"
8120
- echo " get KEY Get a configuration value"
8292
+ echo -e "${YELLOW}Usage: loki config [show|init|edit|path|set|get|example|schema|validate]${NC}"
8293
+ echo ""
8294
+ echo " show Show current configuration (default)"
8295
+ echo " init Create a config file from template"
8296
+ echo " edit Open config file in editor"
8297
+ echo " path Show config file paths"
8298
+ echo " set KEY VALUE Set a configuration value"
8299
+ echo " get KEY Get a configuration value"
8300
+ echo " example Print an annotated config template (for --config)"
8301
+ echo " schema Print the key -> env-var mapping table"
8302
+ echo " validate FILE Validate a config file (--config) without applying it"
8121
8303
  echo ""
8122
8304
  echo "Settable keys (v6.0.0):"
8123
8305
  echo " maxTier Cost ceiling: opus, sonnet, haiku (default: opus)"
@@ -8295,6 +8477,41 @@ SET_CONFIG
8295
8477
  esac
8296
8478
  }
8297
8479
 
8480
+ # #691: print an annotated config template generated from LOKI_CONFIG_MAP, so it
8481
+ # can never drift from the parsers. Suitable for `loki start --config <path>`.
8482
+ cmd_config_example() {
8483
+ if ! declare -f loki_config_generate_example >/dev/null 2>&1; then
8484
+ echo -e "${RED}config example: config-map.sh helper not loaded${NC}" >&2
8485
+ return 1
8486
+ fi
8487
+ loki_config_generate_example
8488
+ }
8489
+
8490
+ # #691: print the machine-readable key -> LOKI_ENV_VAR mapping table.
8491
+ cmd_config_schema() {
8492
+ if ! declare -f loki_config_generate_schema >/dev/null 2>&1; then
8493
+ echo -e "${RED}config schema: config-map.sh helper not loaded${NC}" >&2
8494
+ return 1
8495
+ fi
8496
+ loki_config_generate_schema
8497
+ }
8498
+
8499
+ # #691: validate a config file (--config) WITHOUT applying it. Reports
8500
+ # unresolved ${VAR} refs, raw-secret literals (error), and per-value validation
8501
+ # failures. Non-zero exit on any failure.
8502
+ cmd_config_validate() {
8503
+ local file="${1:-}"
8504
+ if [ -z "$file" ]; then
8505
+ echo -e "${RED}Usage: loki config validate <file>${NC}" >&2
8506
+ return 2
8507
+ fi
8508
+ if ! declare -f loki_config_validate_file >/dev/null 2>&1; then
8509
+ echo -e "${RED}config validate: config-map.sh helper not loaded${NC}" >&2
8510
+ return 1
8511
+ fi
8512
+ loki_config_validate_file "$file"
8513
+ }
8514
+
8298
8515
  # v6.0.0: Get a configuration value
8299
8516
  cmd_config_get() {
8300
8517
  local key="${1:-}"
@@ -10820,6 +11037,25 @@ cmd_quick() {
10820
11037
  fi
10821
11038
  fi
10822
11039
 
11040
+ # #691: drop the unified config-file aliases from the task description. The
11041
+ # main() pre-pass already applied the file; without this, a trailing
11042
+ # `--config f.env` would be swept into task_desc (cmd_quick has no arg loop).
11043
+ local _quick_args=()
11044
+ local _skip_next=false
11045
+ local _qa
11046
+ for _qa in "$@"; do
11047
+ if [ "$_skip_next" = "true" ]; then
11048
+ _skip_next=false
11049
+ continue
11050
+ fi
11051
+ case "$_qa" in
11052
+ --config|--vars|--env-file) _skip_next=true; continue ;;
11053
+ --config=*|--vars=*|--env-file=*) continue ;;
11054
+ esac
11055
+ _quick_args+=("$_qa")
11056
+ done
11057
+ set -- "${_quick_args[@]+"${_quick_args[@]}"}"
11058
+
10823
11059
  local task_desc="$*"
10824
11060
  local version=$(get_version)
10825
11061
  local max_iter="${LOKI_MAX_ITERATIONS:-3}"
@@ -15306,6 +15542,32 @@ main() {
15306
15542
  # runs for the 8 ported commands). Marker file: ~/.loki-first-run.
15307
15543
  loki_telemetry "cli_command" "command=$command" 2>/dev/null || true
15308
15544
 
15545
+ # Unified config-file pre-pass (#691). For session commands ONLY, honor
15546
+ # LOKI_CONFIG_FILE / --config|--vars|--env-file and load the file BEFORE the
15547
+ # per-command arg loop runs. Running here means: config exports survive the
15548
+ # exec into run.sh, and the subsequent CLI arg loop still overrides them
15549
+ # (CLI > config). The pre-pass SCANS without consuming; the per-command
15550
+ # loops carry consume-and-ignore arms for the three aliases. Gated to
15551
+ # start/run/quick so status/config/stop never trigger a config load.
15552
+ case "$command" in
15553
+ start|run|quick)
15554
+ if declare -f loki_maybe_apply_config_file >/dev/null 2>&1; then
15555
+ loki_maybe_apply_config_file "$@"
15556
+ fi
15557
+ # #691: documented observation hook. When LOKI_CONFIG_DUMP=1, print
15558
+ # the resolved LOKI_* environment (after the config pre-pass AND any
15559
+ # ambient env) and exit 0 WITHOUT running a build. Lets operators and
15560
+ # tests verify exactly which values a --config file resolves to,
15561
+ # without spend. The per-command CLI arg loops do not run on this
15562
+ # path, so CLI-flag overrides are NOT reflected by the dump; it shows
15563
+ # the config+env resolution only.
15564
+ if [ "${LOKI_CONFIG_DUMP:-}" = "1" ]; then
15565
+ env | grep -E '^LOKI_' | LC_ALL=C sort
15566
+ exit 0
15567
+ fi
15568
+ ;;
15569
+ esac
15570
+
15309
15571
  case "$command" in
15310
15572
  run)
15311
15573
  cmd_run "$@"
@@ -15346,6 +15608,9 @@ main() {
15346
15608
  status)
15347
15609
  cmd_status "$@"
15348
15610
  ;;
15611
+ why)
15612
+ cmd_why "$@"
15613
+ ;;
15349
15614
  stats)
15350
15615
  # CLI consolidation (Phase A): 'stats' is a deprecated alias of
15351
15616
  # 'report session'. On the Bun route this arm is never reached