loki-mode 7.90.2 → 7.91.1

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 CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.90.2
6
+ # Loki Mode v7.91.1
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
408
408
 
409
409
  ---
410
410
 
411
- **v7.90.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.91.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.90.2
1
+ 7.91.1
package/autonomy/loki CHANGED
@@ -9834,7 +9834,25 @@ cmd_doctor() {
9834
9834
  echo -e " ${GREEN}PASS${NC} ANTHROPIC_API_KEY is set"
9835
9835
  pass_count=$((pass_count + 1))
9836
9836
  elif command -v claude &>/dev/null; then
9837
- echo -e " ${DIM} -- ${NC} ANTHROPIC_API_KEY not set (Claude CLI uses its own login)"
9837
+ # Zero-network check: warn if the Claude OAuth login has expired. An
9838
+ # expired token would otherwise pass and then 401 mid-build (the build
9839
+ # stalls at BOOTSTRAP). Mirror run.sh's fail-fast preflight here so a
9840
+ # user catches it with `loki doctor` BEFORE starting a build.
9841
+ _claude_creds="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
9842
+ if [ -s "$_claude_creds" ] && [ "$(python3 -c "
9843
+ import json, sys, time
9844
+ try:
9845
+ d = json.load(open(sys.argv[1]))
9846
+ exp = d.get('claudeAiOauth', {}).get('expiresAt')
9847
+ print('expired' if isinstance(exp,(int,float)) and exp>0 and (exp/1000.0)<=(time.time()+60) else 'ok')
9848
+ except Exception:
9849
+ print('ok')
9850
+ " "$_claude_creds" 2>/dev/null || echo ok)" = "expired" ]; then
9851
+ echo -e " ${YELLOW}WARN${NC} Claude login has EXPIRED -- run 'claude login' before a build (it would otherwise stall)"
9852
+ warn_count=$((warn_count + 1))
9853
+ else
9854
+ echo -e " ${DIM} -- ${NC} ANTHROPIC_API_KEY not set (Claude CLI uses its own login)"
9855
+ fi
9838
9856
  fi
9839
9857
  if [ -n "${OPENAI_API_KEY:-}" ]; then
9840
9858
  echo -e " ${GREEN}PASS${NC} OPENAI_API_KEY is set"
package/autonomy/run.sh CHANGED
@@ -218,6 +218,11 @@ if [[ -z "${LOKI_RUNNING_FROM_TEMP:-}" ]] && [[ "${BASH_SOURCE[0]}" == "${0}" ]]
218
218
  # BUG-XC-011: Set trap BEFORE exec so the temp file gets cleaned up
219
219
  trap 'rm -f "$TEMP_SCRIPT"' EXIT
220
220
  export LOKI_RUNNING_FROM_TEMP=1
221
+ # Record the EXACT temp-copy path so the post-exec cleanup trap deletes THIS
222
+ # temp file and NEVER the canonical source (root cause of the recurring
223
+ # "run.sh self-deleted on build spawn" bug: an inherited LOKI_RUNNING_FROM_TEMP
224
+ # skips the self-copy block, leaving BASH_SOURCE[0]=the real run.sh).
225
+ export LOKI_TEMP_SCRIPT_PATH="$TEMP_SCRIPT"
221
226
  export LOKI_ORIGINAL_SCRIPT_DIR="$SCRIPT_DIR"
222
227
  export LOKI_ORIGINAL_PROJECT_DIR="$PROJECT_DIR"
223
228
  exec "$TEMP_SCRIPT" "$@"
@@ -227,9 +232,14 @@ fi
227
232
  SCRIPT_DIR="${LOKI_ORIGINAL_SCRIPT_DIR:-$SCRIPT_DIR}"
228
233
  PROJECT_DIR="${LOKI_ORIGINAL_PROJECT_DIR:-$PROJECT_DIR}"
229
234
 
230
- # Clean up temp script on exit (only when running from temp copy)
231
- if [[ "${LOKI_RUNNING_FROM_TEMP:-}" == "1" ]]; then
232
- trap 'rm -f "${BASH_SOURCE[0]}" 2>/dev/null' EXIT
235
+ # Clean up ONLY the recorded temp copy, and ONLY if it is a real temp file.
236
+ # Deleting BASH_SOURCE[0] here was the bug: an inherited LOKI_RUNNING_FROM_TEMP
237
+ # made BASH_SOURCE[0] the canonical source, so run.sh deleted itself on spawned
238
+ # builds. Guard on the recorded path being a real /tmp/loki-run-* file.
239
+ if [[ "${LOKI_RUNNING_FROM_TEMP:-}" == "1" ]] \
240
+ && [[ -n "${LOKI_TEMP_SCRIPT_PATH:-}" ]] \
241
+ && [[ "${LOKI_TEMP_SCRIPT_PATH}" == /tmp/loki-run-* || "${LOKI_TEMP_SCRIPT_PATH}" == "${TMPDIR:-/tmp}"loki-run-* ]]; then
242
+ trap 'rm -f "${LOKI_TEMP_SCRIPT_PATH}" 2>/dev/null' EXIT
233
243
  fi
234
244
 
235
245
  #===============================================================================
@@ -829,6 +839,12 @@ CONCURRENCY_CRITICAL_THRESHOLD=${LOKI_CONCURRENCY_CRITICAL_THRESHOLD:-95}
829
839
  GATE_CLEAR_LIMIT=${LOKI_GATE_CLEAR_LIMIT:-3}
830
840
  GATE_ESCALATE_LIMIT=${LOKI_GATE_ESCALATE_LIMIT:-5}
831
841
  GATE_PAUSE_LIMIT=${LOKI_GATE_PAUSE_LIMIT:-10}
842
+ # Workspace resolution: the build runs against TARGET_DIR, defaulting to the
843
+ # launch cwd. A caller can pin a per-build workspace by exporting
844
+ # LOKI_TARGET_DIR (and the matching LOKI_DIR=<dir>/.loki); the dashboard
845
+ # /api/control/start `workspace` param (Track-1 S1) does exactly this so a
846
+ # hosted build runs in its own dir instead of the engine's repo. Every
847
+ # $TARGET_DIR/.loki reference below then resolves under that workspace.
832
848
  TARGET_DIR="${LOKI_TARGET_DIR:-$(pwd)}"
833
849
  PARALLEL_BLOG=${LOKI_PARALLEL_BLOG:-false}
834
850
  AUTO_MERGE=${LOKI_AUTO_MERGE:-true}
@@ -1378,6 +1394,34 @@ emit_event_json() {
1378
1394
  log_debug "Event: $event_type - $json_data"
1379
1395
  }
1380
1396
 
1397
+ # Per-stage timeline event (v7.91.x). Emits one stage_complete record per
1398
+ # quality-gate / build stage so the SaaS timeline can show where build time
1399
+ # goes within an iteration (the provider call itself is already bracketed by
1400
+ # iteration_start/iteration_complete). This is purely ADDITIVE: it appends one
1401
+ # event line via emit_event_json and never changes a gate verdict, gate exit
1402
+ # code, or control flow. Duration is computed by the caller (whole-second
1403
+ # resolution, sufficient for multi-second gates) and carried on the event so
1404
+ # the SaaS does not have to infer it from coarse ISO timestamps.
1405
+ # emit_stage_complete <stage_name> <status: pass|fail> <start_epoch_seconds>
1406
+ # Event shape:
1407
+ # {type:"stage_complete", timestamp, data:{stage, status, duration_s, iteration}}
1408
+ # Best-effort: any failure is swallowed so it can never block the build.
1409
+ emit_stage_complete() {
1410
+ local stage="$1"
1411
+ local status="$2"
1412
+ local t0="$3"
1413
+ local now dur
1414
+ now=$(date +%s 2>/dev/null) || return 0
1415
+ [ -n "$t0" ] || return 0
1416
+ dur=$(( now - t0 ))
1417
+ [ "$dur" -ge 0 ] 2>/dev/null || dur=0
1418
+ emit_event_json "stage_complete" \
1419
+ "stage=$stage" \
1420
+ "status=$status" \
1421
+ "duration_s=$dur" \
1422
+ "iteration=${ITERATION_COUNT:-0}" 2>/dev/null || true
1423
+ }
1424
+
1381
1425
  # Trust-layer metrics event writer (benchmark program section 3). Appends one
1382
1426
  # durable record per trust event to .loki/metrics/trust-events.jsonl via the
1383
1427
  # Python writer (single source of truth for the JSONL schema). This is ADDITIVE
@@ -1428,6 +1472,30 @@ _loki_trust_run_id() {
1428
1472
  printf '%s' ""
1429
1473
  }
1430
1474
 
1475
+ # _advance_current_phase: atomically advance currentPhase in orchestrator.json.
1476
+ # This is the single source of truth for the build dashboard and the BFF
1477
+ # reconciliation gate (isTerminalPhase). The engine initialises it to "BOOTSTRAP"
1478
+ # at session start; call this to advance through the SDLC lifecycle phases.
1479
+ # Args: $1 = the new phase value (REASONING, BUILDING, VERIFYING, COMPLETED, etc.)
1480
+ _advance_current_phase() {
1481
+ local new_phase="${1:?phase required}"
1482
+ local loki_dir="${LOKI_DIR:-${TARGET_DIR:-.}}/.loki"
1483
+ local orch="$loki_dir/state/orchestrator.json"
1484
+ [ -f "$orch" ] || return 0
1485
+ # Values are passed via argv (not interpolated into the source) so a phase or
1486
+ # path containing quotes can never break or inject into the python.
1487
+ python3 -c "
1488
+ import json, sys
1489
+ f, phase = sys.argv[1], sys.argv[2]
1490
+ try:
1491
+ d = json.load(open(f))
1492
+ d['currentPhase'] = phase
1493
+ json.dump(d, open(f, 'w'))
1494
+ except (json.JSONDecodeError, OSError):
1495
+ pass
1496
+ " "$orch" "$new_phase" 2>/dev/null || true
1497
+ }
1498
+
1431
1499
  # Usage: record_trust_event_bash <event_type> [key=value ...]
1432
1500
  # Pass LOKI_TRUST_RUN_ID in the environment to override the resolved id (the
1433
1501
  # run_start site sets it to the freshly minted id so the first event matches).
@@ -1808,6 +1876,37 @@ validate_api_keys() {
1808
1876
  local masked="${key_value:0:8}...${key_value: -4}"
1809
1877
  log_info "API key $key_var: $masked (${#key_value} chars)"
1810
1878
 
1879
+ # Fail-fast auth preflight (v7.91): for Claude OAuth logins, a present-but-
1880
+ # EXPIRED token passes the presence check above but then 401s on the first
1881
+ # provider call, leaving the build stalled at BOOTSTRAP with no clear cause.
1882
+ # Catch that here with a zero-network, zero-token local expiry check so the
1883
+ # user gets an instant, copy-pasteable fix instead of a silent stall.
1884
+ # Opt out with LOKI_SKIP_AUTH_PREFLIGHT=1 (offline/odd setups).
1885
+ if [[ "$provider" == "claude" && "${LOKI_SKIP_AUTH_PREFLIGHT:-}" != "1" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
1886
+ local _creds="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
1887
+ if [[ -s "$_creds" ]]; then
1888
+ local _expired
1889
+ _expired=$(python3 -c "
1890
+ import json, sys, time
1891
+ try:
1892
+ d = json.load(open(sys.argv[1]))
1893
+ exp = d.get('claudeAiOauth', {}).get('expiresAt')
1894
+ # expiresAt is epoch milliseconds; compare with a 60s safety margin.
1895
+ if isinstance(exp, (int, float)) and exp > 0 and (exp / 1000.0) <= (time.time() + 60):
1896
+ print('expired')
1897
+ except Exception:
1898
+ pass # unreadable/unknown schema -> do not block (fail open, let the call decide)
1899
+ " "$_creds" 2>/dev/null || true)
1900
+ if [[ "$_expired" == "expired" ]]; then
1901
+ log_error "Your Claude Code login has expired -- the build would stall instead of running."
1902
+ log_error "Fix it in one step, then retry:"
1903
+ log_error " claude login"
1904
+ log_error "(or set ANTHROPIC_API_KEY, or LOKI_SKIP_AUTH_PREFLIGHT=1 to bypass this check)"
1905
+ return 1
1906
+ fi
1907
+ fi
1908
+ fi
1909
+
1811
1910
  return 0
1812
1911
  }
1813
1912
 
@@ -6128,6 +6227,152 @@ audit_log() {
6128
6227
  echo "$log_entry" >> "$audit_file"
6129
6228
  }
6130
6229
 
6230
+ #===============================================================================
6231
+ # Engine-owned workspace git-init (Plan #16, Option A-1)
6232
+ #===============================================================================
6233
+
6234
+ # Resolve a path to its physical absolute form (symlinks + .. collapsed) using
6235
+ # whatever is available; falls back to the input unchanged. Portable across the
6236
+ # BSD (macOS) and GNU userlands the engine runs on.
6237
+ _loki_resolve_path() {
6238
+ local p="${1:-}"
6239
+ [ -n "$p" ] || { printf '%s' ""; return 0; }
6240
+ if command -v realpath >/dev/null 2>&1; then
6241
+ realpath "$p" 2>/dev/null && return 0
6242
+ fi
6243
+ # python3 is a hard engine dependency; use it as the portable fallback.
6244
+ python3 - "$p" <<'PYRESOLVE' 2>/dev/null && return 0
6245
+ import os, sys
6246
+ print(os.path.realpath(sys.argv[1]))
6247
+ PYRESOLVE
6248
+ printf '%s' "$p"
6249
+ }
6250
+
6251
+ # True (0) when TARGET_DIR is an engine-owned, freshly-minted build workspace
6252
+ # that Loki may auto-git-init without surprising a user. Two honest signals:
6253
+ # 1. LOKI_AUTO_GIT_INIT=1 -- explicit opt-in (non-SaaS automation).
6254
+ # 2. LOKI_TARGET_DIR is set AND realpath-contained under one of the
6255
+ # colon-separated LOKI_WORKSPACE_ROOTS dirs (the v7.91.0 SaaS route: the
6256
+ # BFF mints <root>/<buildId> and the server pins LOKI_TARGET_DIR to it).
6257
+ # A user's own folder (`loki start ./prd.md`, no workspace, roots unset) is
6258
+ # NEVER engine-owned -- it must not get a silent .git. Realpath containment (not
6259
+ # a prefix string match) so /root/build-other does not match /root/build.
6260
+ _loki_workspace_is_engine_owned() {
6261
+ [ "${LOKI_AUTO_GIT_INIT:-0}" = "1" ] && return 0
6262
+
6263
+ local roots_raw="${LOKI_WORKSPACE_ROOTS:-}"
6264
+ [ -n "${LOKI_TARGET_DIR:-}" ] || return 1
6265
+ [ -n "$roots_raw" ] || return 1
6266
+
6267
+ local ws_real root_real
6268
+ ws_real="$(_loki_resolve_path "${TARGET_DIR:-.}")"
6269
+ [ -n "$ws_real" ] || return 1
6270
+
6271
+ local IFS=':'
6272
+ local root
6273
+ for root in $roots_raw; do
6274
+ [ -n "$root" ] || continue
6275
+ root_real="$(_loki_resolve_path "$root")"
6276
+ [ -n "$root_real" ] || continue
6277
+ # Exact match or contained: ws == root, or ws starts with root + "/".
6278
+ if [ "$ws_real" = "$root_real" ] || case "$ws_real" in "$root_real"/*) true ;; *) false ;; esac; then
6279
+ return 0
6280
+ fi
6281
+ done
6282
+ return 1
6283
+ }
6284
+
6285
+ # Plan #16 Option A-1: establish git in an engine-owned build workspace so the
6286
+ # review/verify gate (run_code_review) and branch-isolation chain can actually
6287
+ # run. Without git history, run_code_review's diff resolves empty and the gate
6288
+ # SKIPS (silently reporting PASS) -- so a build that was never reviewed could
6289
+ # earn a VERIFIED receipt. This makes the gate RUN; it does NOT force a green
6290
+ # (a build whose review/tests fail still gets an honest verdict).
6291
+ #
6292
+ # Constraints honored:
6293
+ # - Engine-owned workspaces ONLY (see _loki_workspace_is_engine_owned). A
6294
+ # user's own folder is never silently git-init'd.
6295
+ # - Already a git repo (user's repo OR the engine source tree) -> NO-OP. Only
6296
+ # a non-git workspace is initialized.
6297
+ # - One INITIAL commit (not a bare init): a zero-commit unborn HEAD breaks the
6298
+ # start-SHA capture (git rev-parse HEAD) and re-trips the HEAD~1 skip. The
6299
+ # commit uses --allow-empty because a greenfield workspace at build start
6300
+ # holds only .loki/ (the spec lands in .loki/specs/), which .loki/.gitignore
6301
+ # excludes -> nothing to stage -> a bare commit would fail and leave an
6302
+ # unborn HEAD. --allow-empty guarantees a real HEAD either way.
6303
+ # - Neutral repo-local identity (loki-build) so the initial commit AND the
6304
+ # later commit_session_changes commit never inherit a developer's global git
6305
+ # identity. Repo-local sticks on a freshly-init'd workspace (no revert hook).
6306
+ # - Secrets never committed: brownfield files are staged through the same
6307
+ # secret-scan guard (_commit_path_looks_secret / _commit_scan_secret_file)
6308
+ # used by commit_session_changes; any offender unstages the whole set and
6309
+ # the initial commit falls back to --allow-empty (HEAD still established).
6310
+ maybe_git_init_engine_workspace() {
6311
+ command -v git >/dev/null 2>&1 || return 0
6312
+ _loki_workspace_is_engine_owned || return 0
6313
+
6314
+ local ws="${TARGET_DIR:-.}"
6315
+ [ -d "$ws" ] || return 0
6316
+
6317
+ # Already a git repo (user repo or engine source tree): do nothing.
6318
+ if git -C "$ws" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
6319
+ return 0
6320
+ fi
6321
+
6322
+ log_info "Engine-owned workspace is not a git repo; initializing for review/verify gates"
6323
+
6324
+ if ! git -C "$ws" init -q >/dev/null 2>&1; then
6325
+ log_warn "git init failed in engine workspace; review gate will skip (non-fatal)"
6326
+ return 0
6327
+ fi
6328
+
6329
+ # Neutral repo-local identity for the initial AND session-end commits.
6330
+ git -C "$ws" config user.name "loki-build" >/dev/null 2>&1 || true
6331
+ git -C "$ws" config user.email "loki-build@autonomi.dev" >/dev/null 2>&1 || true
6332
+
6333
+ # Self-ignore .loki/ runtime state so no commit ever stages it (mirrors
6334
+ # setup_agent_branch). Idempotent.
6335
+ mkdir -p "$ws/.loki" 2>/dev/null || true
6336
+ [ -f "$ws/.loki/.gitignore" ] || printf '*\n' > "$ws/.loki/.gitignore" 2>/dev/null || true
6337
+
6338
+ # Stage everything except .loki/ and an obvious-secret-path denylist (same
6339
+ # first-cut excludes commit_session_changes uses). The scan loop below is the
6340
+ # real guarantee for nested/weak secrets.
6341
+ git -C "$ws" add -A \
6342
+ ':!.loki' ':!.loki/' \
6343
+ ':!.env' ':!.env.*' ':!*.env' \
6344
+ ':!*.key' ':!*.pem' ':!*.p12' ':!*.keystore' \
6345
+ ':!id_rsa*' ':!*.token' ':!credentials*' >/dev/null 2>&1 || true
6346
+
6347
+ # Secret-scan staged files. ANY offender -> unstage all (safe default: never
6348
+ # commit a possible secret). The --allow-empty commit below still runs so a
6349
+ # real HEAD is established regardless.
6350
+ local _offenders=""
6351
+ local _f
6352
+ while IFS= read -r -d '' _f; do
6353
+ [ -f "$ws/$_f" ] || continue
6354
+ if _commit_path_looks_secret "$ws/$_f" || _commit_scan_secret_file "$ws/$_f"; then
6355
+ _offenders="${_offenders}${_offenders:+, }${_f}"
6356
+ fi
6357
+ done < <(git -C "$ws" diff --cached --name-only -z 2>/dev/null)
6358
+
6359
+ if [ -n "$_offenders" ]; then
6360
+ git -C "$ws" reset >/dev/null 2>&1 || true
6361
+ log_warn "Initial commit: possible secret in ${_offenders}; left unstaged (baseline commit will be empty)"
6362
+ fi
6363
+
6364
+ # ONE initial commit. --allow-empty: a greenfield workspace stages nothing
6365
+ # (only .loki/, which is ignored), so a bare commit would fail and leave an
6366
+ # unborn HEAD -- the exact failure mode this whole block exists to avoid.
6367
+ if git -C "$ws" commit --allow-empty -q -m "loki: initial build workspace baseline" >/dev/null 2>&1; then
6368
+ log_info "Initialized git in engine workspace (initial baseline commit created)"
6369
+ audit_log "WORKSPACE_GIT_INIT" "workspace=$ws"
6370
+ else
6371
+ log_warn "Initial baseline commit failed; review gate may skip (non-fatal)"
6372
+ fi
6373
+ return 0
6374
+ }
6375
+
6131
6376
  #===============================================================================
6132
6377
  # Branch Protection for Agent Changes
6133
6378
  #===============================================================================
@@ -9612,17 +9857,89 @@ run_code_review() {
9612
9857
  # common case) because git ignores the exclude pathspec for paths it does not
9613
9858
  # track. ':(exclude).git/' is harmless (git never diffs .git/) and is kept
9614
9859
  # only for parity with the evidence-gate exclusion list.
9615
- local _review_pathspec=(-- . ':(exclude).loki/' ':(exclude).git/' ':(exclude)**/.loki/**')
9616
- local diff_content
9617
- diff_content=$(git -C "${TARGET_DIR:-.}" diff HEAD~1 "${_review_pathspec[@]}" 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --cached "${_review_pathspec[@]}" 2>/dev/null || echo "")
9860
+ # Finding #596 + Plan #16: exclude .loki/, .git/, AND the standard dependency
9861
+ # / build noise dirs. The A-2 temp-index `git add -A` below stages EVERY
9862
+ # untracked file, and a fresh greenfield workspace has no root .gitignore yet
9863
+ # (only .loki/.gitignore, scoped to .loki/). Without these excludes a build
9864
+ # that ran `npm install` / `pip install` before writing a .gitignore would
9865
+ # stage node_modules/ (or .venv/, dist/, build/) into the temp index, bloat
9866
+ # the review diff to multi-MB, overflow the reviewer prompt, force NO_OUTPUT,
9867
+ # and defeat the gate -- exactly the Finding #596 class. The diff pathspec
9868
+ # filters the OUTPUT regardless of what the temp index staged, so this one
9869
+ # list fixes both diff_content and changed_files. Mirrors the metrics-path
9870
+ # noise set (run.sh:12592-12598) plus __pycache__ and vendor.
9871
+ local _review_pathspec=(-- . \
9872
+ ':(exclude).loki/' ':(exclude).git/' ':(exclude)**/.loki/**' \
9873
+ ':(exclude)node_modules/' ':(exclude)**/node_modules/**' \
9874
+ ':(exclude)venv/' ':(exclude).venv/' ':(exclude)**/venv/**' ':(exclude)**/.venv/**' \
9875
+ ':(exclude)dist/' ':(exclude)build/' ':(exclude)**/dist/**' ':(exclude)**/build/**' \
9876
+ ':(exclude)__pycache__/' ':(exclude)**/__pycache__/**' \
9877
+ ':(exclude)vendor/' ':(exclude)**/vendor/**')
9878
+
9879
+ # Plan #16 (A-2): make the review diff base robust to shallow/fresh history,
9880
+ # and surface NEW (untracked) files -- the whole greenfield build is new
9881
+ # files, which `git diff <base>` (tracked-only) never shows.
9882
+ #
9883
+ # Base preference, mirroring the proven metrics-path fallback chain:
9884
+ # 1. ${_LOKI_RUN_START_SHA} when it resolves to a commit -- the correct
9885
+ # per-run baseline (captured at run start; also what the summary/"Review
9886
+ # the work" command uses). After A-1, a fresh workspace has exactly one
9887
+ # commit at iteration 1, so HEAD~1 does NOT resolve -- the start-SHA is
9888
+ # what makes the gate run on iteration 1.
9889
+ # 2. HEAD~1 when it resolves (the live engine-source case, deep history).
9890
+ # 3. the git empty-tree object (computed, never a hardcoded SHA-1 constant,
9891
+ # so it survives a SHA-256 repo) so a single-commit repo still yields a
9892
+ # real diff against an empty baseline instead of an empty string.
9893
+ local _review_base=""
9894
+ if [ -n "${_LOKI_RUN_START_SHA:-}" ] && \
9895
+ git -C "${TARGET_DIR:-.}" rev-parse --verify --quiet "${_LOKI_RUN_START_SHA}^{commit}" >/dev/null 2>&1; then
9896
+ _review_base="${_LOKI_RUN_START_SHA}"
9897
+ elif git -C "${TARGET_DIR:-.}" rev-parse --verify --quiet 'HEAD~1^{commit}' >/dev/null 2>&1; then
9898
+ _review_base="HEAD~1"
9899
+ else
9900
+ _review_base="$(git -C "${TARGET_DIR:-.}" hash-object -t tree /dev/null 2>/dev/null || echo "")"
9901
+ fi
9902
+
9903
+ # Build the review diff against a THROWAWAY index (GIT_INDEX_FILE points at a
9904
+ # fresh, nonexistent path) so a `git add -A` captures the full working tree --
9905
+ # new + modified files alike -- WITHOUT touching the real index. This avoids
9906
+ # disturbing any other porcelain consumer (the evidence hard gate's status
9907
+ # read, create_checkpoint's `git stash create`, commit_session_changes). The
9908
+ # `.loki/.gitignore` (`*`) keeps runtime state out; the pathspec is a belt-
9909
+ # and-suspenders exclude. `diff --cached <base>` then compares that staged
9910
+ # snapshot to the baseline -- a real unified diff the reviewer can read.
9911
+ local diff_content=""
9912
+ local changed_files=""
9913
+ if [ -n "$_review_base" ]; then
9914
+ local _rev_idx
9915
+ _rev_idx="$(mktemp -u "${TMPDIR:-/tmp}/loki-revidx.XXXXXX")"
9916
+ GIT_INDEX_FILE="$_rev_idx" git -C "${TARGET_DIR:-.}" add -A 2>/dev/null || true
9917
+ diff_content=$(GIT_INDEX_FILE="$_rev_idx" git -C "${TARGET_DIR:-.}" diff --cached "$_review_base" "${_review_pathspec[@]}" 2>/dev/null || echo "")
9918
+ changed_files=$(GIT_INDEX_FILE="$_rev_idx" git -C "${TARGET_DIR:-.}" diff --cached --name-only "$_review_base" "${_review_pathspec[@]}" 2>/dev/null || echo "")
9919
+ rm -f "$_rev_idx" 2>/dev/null || true
9920
+ fi
9921
+
9618
9922
  if [ -z "$diff_content" ]; then
9619
- log_info "Code review: No diff to review, skipping"
9923
+ # Honesty (Finding #596 class): a silent PASS on an empty diff is a trust
9924
+ # gap ONLY when the run actually produced changes we failed to diff.
9925
+ # Distinguish a genuine no-op iteration (nothing changed -> legitimate
9926
+ # PASS) from "changes exist but we could not compute a diff" (must not
9927
+ # pass silently). Use the same .loki/.git-excluding porcelain the
9928
+ # evidence gate uses to detect real changes independent of the diff base.
9929
+ local _dirty
9930
+ _dirty=$(git -C "${TARGET_DIR:-.}" status --porcelain "${_review_pathspec[@]}" 2>/dev/null | head -1 || echo "")
9931
+ if [ -n "$_dirty" ] || [ -n "$changed_files" ]; then
9932
+ # Return non-zero so the gate dispatcher records a failure (it calls
9933
+ # track_gate_failure on the else branch). Do NOT call track_gate_failure
9934
+ # here: it echoes its count to stdout (callers capture it) and the
9935
+ # dispatcher would double-count.
9936
+ log_warn "Code review: workspace has changes but the review diff is empty (could not compute a diff base); NOT passing the gate silently"
9937
+ return 1
9938
+ fi
9939
+ log_info "Code review: no changes this iteration, skipping (genuine no-op)"
9620
9940
  return 0
9621
9941
  fi
9622
9942
 
9623
- local changed_files
9624
- changed_files=$(git -C "${TARGET_DIR:-.}" diff --name-only HEAD~1 "${_review_pathspec[@]}" 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --name-only --cached "${_review_pathspec[@]}" 2>/dev/null || echo "")
9625
-
9626
9943
  log_header "CODE REVIEW: $review_id"
9627
9944
 
9628
9945
  # Phase 3 (v7.0.0): managed code-review council. When the flag is on,
@@ -15968,27 +16285,33 @@ if __name__ == "__main__":
15968
16285
  # Static analysis gate
15969
16286
  if [ "${PHASE_STATIC_ANALYSIS:-true}" = "true" ]; then
15970
16287
  log_info "Quality gate: static analysis..."
16288
+ local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
15971
16289
  if enforce_static_analysis; then
15972
16290
  clear_gate_failure "static_analysis"
15973
16291
  else
16292
+ _stg_ok=fail
15974
16293
  local sa_count
15975
16294
  sa_count=$(track_gate_failure "static_analysis")
15976
16295
  gate_failures="${gate_failures}static_analysis,"
15977
16296
  log_warn "Static analysis FAILED ($sa_count consecutive) - findings injected into next iteration"
15978
16297
  fi
16298
+ emit_stage_complete "static_analysis" "$_stg_ok" "$_stg_t0"
15979
16299
  fi
15980
16300
  # Secure-by-default scan (v7.87.0). Advisory by default (never
15981
16301
  # blocks); records .loki/quality/security-findings.json each
15982
16302
  # iteration. Blocks only on un-waived HIGH when LOKI_SECURE_GATE=block.
15983
16303
  log_info "Quality gate: security scan (advisory)..."
16304
+ local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
15984
16305
  if run_secure_scan; then
15985
16306
  clear_gate_failure "security_scan"
15986
16307
  else
16308
+ _stg_ok=fail
15987
16309
  local sec_count
15988
16310
  sec_count=$(track_gate_failure "security_scan")
15989
16311
  gate_failures="${gate_failures}security_scan,"
15990
16312
  log_warn "Security gate BLOCKED ($sec_count consecutive) - un-waived HIGH findings (LOKI_SECURE_GATE=block)"
15991
16313
  fi
16314
+ emit_stage_complete "security_scan" "$_stg_ok" "$_stg_t0"
15992
16315
  # BUG-ST-002: Check pause signal between quality gates
15993
16316
  if [ -f "${TARGET_DIR:-.}/.loki/PAUSE" ] || [ -f "${TARGET_DIR:-.}/.loki/STOP" ]; then
15994
16317
  log_warn "Pause/stop signal detected between quality gates - deferring remaining gates"
@@ -16002,11 +16325,13 @@ if __name__ == "__main__":
16002
16325
  # Test coverage gate
16003
16326
  if [ "${PHASE_UNIT_TESTS:-true}" = "true" ]; then
16004
16327
  log_info "Quality gate: test suite (pass/fail)..."
16328
+ local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
16005
16329
  # F49: isolate HOME so the project's suite cannot pollute the
16006
16330
  # user's real home when it execs the generated app.
16007
16331
  if _loki_with_app_sandbox enforce_test_coverage; then
16008
16332
  clear_gate_failure "test_coverage"
16009
16333
  else
16334
+ _stg_ok=fail
16010
16335
  local tc_count
16011
16336
  tc_count=$(track_gate_failure "test_coverage")
16012
16337
  gate_failures="${gate_failures}test_coverage,"
@@ -16020,6 +16345,7 @@ if __name__ == "__main__":
16020
16345
  log_warn "Test suite gate FAILED ($tc_count consecutive) - must pass next iteration"
16021
16346
  fi
16022
16347
  fi
16348
+ emit_stage_complete "test_suite" "$_stg_ok" "$_stg_t0"
16023
16349
  fi
16024
16350
  # BUG-ST-002: Check pause signal between quality gates (after test coverage)
16025
16351
  if [ -f "${TARGET_DIR:-.}/.loki/PAUSE" ] || [ -f "${TARGET_DIR:-.}/.loki/STOP" ]; then
@@ -16032,26 +16358,32 @@ if __name__ == "__main__":
16032
16358
  # Mock integrity gate (P0-3): block on CRITICAL/HIGH mock problems.
16033
16359
  if [ "${LOKI_GATE_MOCK:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
16034
16360
  log_info "Quality gate: mock integrity..."
16361
+ local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
16035
16362
  if enforce_mock_integrity; then
16036
16363
  clear_gate_failure "mock_integrity"
16037
16364
  else
16365
+ _stg_ok=fail
16038
16366
  local mk_count
16039
16367
  mk_count=$(track_gate_failure "mock_integrity")
16040
16368
  gate_failures="${gate_failures}mock_integrity,"
16041
16369
  log_warn "Mock integrity gate FAILED ($mk_count consecutive) - CRITICAL/HIGH mock problems"
16042
16370
  fi
16371
+ emit_stage_complete "mock_integrity" "$_stg_ok" "$_stg_t0"
16043
16372
  fi
16044
16373
  # Test mutation integrity gate (P0-3): block on HIGH test-fitting.
16045
16374
  if [ "${LOKI_GATE_MUTATION:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
16046
16375
  log_info "Quality gate: test mutation integrity..."
16376
+ local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
16047
16377
  if enforce_mutation_integrity; then
16048
16378
  clear_gate_failure "mutation_integrity"
16049
16379
  else
16380
+ _stg_ok=fail
16050
16381
  local mt_count
16051
16382
  mt_count=$(track_gate_failure "mutation_integrity")
16052
16383
  gate_failures="${gate_failures}mutation_integrity,"
16053
16384
  log_warn "Mutation integrity gate FAILED ($mt_count consecutive) - HIGH test-fitting detected"
16054
16385
  fi
16386
+ emit_stage_complete "mutation_integrity" "$_stg_ok" "$_stg_t0"
16055
16387
  fi
16056
16388
  # LSP diagnostics gate (P1-5 bash-route parity, v7.51.0; default-on
16057
16389
  # advisory-surfacing as of v7.57.0). Closes the parity gap: the Bun
@@ -16087,6 +16419,7 @@ if __name__ == "__main__":
16087
16419
  # runLSPDiagnosticsWriter: cwd=REPO_ROOT, --root=ctx.cwd).
16088
16420
  if { [ "${LOKI_GATE_LSP_DIAGNOSTICS:-true}" = "true" ] || [ "${LOKI_GATE_LSP_DIAGNOSTICS:-true}" = "1" ]; } && [ "$ITERATION_COUNT" -gt 0 ]; then
16089
16421
  log_info "Quality gate: LSP diagnostics..."
16422
+ local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
16090
16423
  # WRITER: route-neutral Python, same program as the Bun route.
16091
16424
  if [ "${LOKI_GATE_LSP_WRITER:-1}" != "0" ]; then
16092
16425
  ( cd "$PROJECT_DIR" && LOKI_DIR="${TARGET_DIR:-.}/.loki" python3 -m mcp.lsp_proxy --write-diagnostics --root "${TARGET_DIR:-.}" ) >/dev/null 2>&1 || true
@@ -16120,6 +16453,7 @@ else:
16120
16453
  fi
16121
16454
  case "$_lsp_verdict" in
16122
16455
  block*)
16456
+ _stg_ok=fail
16123
16457
  local _lsp_e _lsp_w
16124
16458
  _lsp_e=$(printf '%s' "$_lsp_verdict" | awk '{print $2}')
16125
16459
  _lsp_w=$(printf '%s' "$_lsp_verdict" | awk '{print $3}')
@@ -16146,6 +16480,7 @@ else:
16146
16480
  log_info "LSP diagnostics: no lsp-diagnostics.json artifact (lsp not available) -- gate did not run"
16147
16481
  ;;
16148
16482
  esac
16483
+ emit_stage_complete "lsp_diagnostics" "$_stg_ok" "$_stg_t0"
16149
16484
  fi
16150
16485
  # Semantic test-authenticity gate -- mid-iteration ADVISORY arm
16151
16486
  # (v7.57.0 default-on surfacing). Clones the mock arm (~15126)
@@ -16198,9 +16533,11 @@ else:
16198
16533
  # Code review gate (upgraded from advisory, with escalation)
16199
16534
  if [ "$PHASE_CODE_REVIEW" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
16200
16535
  log_info "Quality gate: code review..."
16536
+ local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
16201
16537
  if run_code_review; then
16202
16538
  clear_gate_failure "code_review"
16203
16539
  else
16540
+ _stg_ok=fail
16204
16541
  local cr_count
16205
16542
  cr_count=$(track_gate_failure "code_review")
16206
16543
  # BUG-QG-007: Always append to gate_failures regardless of escalation tier
@@ -16225,7 +16562,7 @@ else:
16225
16562
  esac
16226
16563
  fi
16227
16564
  if [ "$_phase1_overrode" = "true" ]; then
16228
- : # BLOCK lifted; continue without escalation
16565
+ _stg_ok=pass # BLOCK lifted; continue without escalation
16229
16566
  elif [ "$cr_count" -ge "$GATE_PAUSE_LIMIT" ]; then
16230
16567
  log_error "Gate escalation: code_review failed $cr_count times (>= $GATE_PAUSE_LIMIT) - forcing PAUSE for human intervention"
16231
16568
  echo "PAUSE" > "${TARGET_DIR:-.}/.loki/signals/GATE_ESCALATION"
@@ -16255,6 +16592,7 @@ else:
16255
16592
  bun "${SCRIPT_DIR}/../loki-ts/dist/loki.js" internal phase1-hooks reflect "$ITERATION_COUNT" 2>/dev/null || true
16256
16593
  fi
16257
16594
  fi
16595
+ emit_stage_complete "code_review" "$_stg_ok" "$_stg_t0"
16258
16596
  fi
16259
16597
  # Auto-generate docs (default-on) BEFORE the staleness check and the
16260
16598
  # gate, so neither nags the user to run 'loki docs generate' by hand.
@@ -16269,26 +16607,32 @@ else:
16269
16607
  # Documentation quality gate - Gate 7 (Documentation Coverage)
16270
16608
  if [ "${LOKI_GATE_DOC_COVERAGE:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
16271
16609
  log_info "Quality gate: documentation coverage..."
16610
+ local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
16272
16611
  if run_doc_quality_gate; then
16273
16612
  clear_gate_failure "doc_coverage"
16274
16613
  else
16614
+ _stg_ok=fail
16275
16615
  local dc_count
16276
16616
  dc_count=$(track_gate_failure "doc_coverage")
16277
16617
  gate_failures="${gate_failures}doc_coverage,"
16278
16618
  log_warn "Documentation coverage gate: Score below threshold ($dc_count consecutive)"
16279
16619
  fi
16620
+ emit_stage_complete "doc_coverage" "$_stg_ok" "$_stg_t0"
16280
16621
  fi
16281
16622
  # Magic Modules debate gate - Gate 12 (v6.77.0)
16282
16623
  if [ "${LOKI_GATE_MAGIC_DEBATE:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
16283
16624
  log_info "Quality gate: magic modules debate..."
16625
+ local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
16284
16626
  if run_magic_debate_gate; then
16285
16627
  clear_gate_failure "magic_debate"
16286
16628
  else
16629
+ _stg_ok=fail
16287
16630
  local md_count
16288
16631
  md_count=$(track_gate_failure "magic_debate")
16289
16632
  gate_failures="${gate_failures}magic_debate,"
16290
16633
  log_warn "Magic Modules debate gate: BLOCK severity detected ($md_count consecutive)"
16291
16634
  fi
16635
+ emit_stage_complete "magic_debate" "$_stg_ok" "$_stg_t0"
16292
16636
  fi
16293
16637
  # Store gate failures for prompt injection
16294
16638
  if [ -n "$gate_failures" ]; then
@@ -17782,6 +18126,13 @@ main() {
17782
18126
  # resume/rollback sees the restored state. Zero behavior change for local.
17783
18127
  _loki_object_store_hydrate_checkpoints || true
17784
18128
 
18129
+ # Plan #16 (A-1): establish git in an engine-owned, non-git build workspace
18130
+ # BEFORE branch setup + start-SHA capture (both need a resolvable HEAD), so
18131
+ # the per-iteration review/verify gate actually runs instead of skipping on
18132
+ # an empty diff. No-op for the engine source tree, a user's own repo, or a
18133
+ # non-engine-owned folder. See maybe_git_init_engine_workspace.
18134
+ maybe_git_init_engine_workspace
18135
+
17785
18136
  # Setup agent branch protection (isolates agent changes to a feature branch)
17786
18137
  setup_agent_branch
17787
18138
 
@@ -17852,7 +18203,10 @@ main() {
17852
18203
  # Cleanup parallel streams
17853
18204
  cleanup_parallel_streams
17854
18205
  else
17855
- # Standard mode: single session
18206
+ # Standard mode: single session. Advance phase from BOOTSTRAP to BUILDING
18207
+ # before the first iteration so the dashboard shows real progress, not
18208
+ # a stuck "Planning" state.
18209
+ _advance_current_phase "BUILDING"
17856
18210
  run_autonomous "$PRD_PATH" || result=$?
17857
18211
  fi
17858
18212
 
@@ -17939,6 +18293,13 @@ main() {
17939
18293
  generate_proof_of_run "$result" || true
17940
18294
  fi
17941
18295
 
18296
+ # Advance currentPhase to COMPLETED so the BFF reconciliation gate and the
18297
+ # engine's own is_completed() recognise this session as terminal. Write the
18298
+ # file-system COMPLETED marker (checked by is_completed() in the main loop).
18299
+ _advance_current_phase "COMPLETED"
18300
+ local loki_dir="${LOKI_DIR:-${TARGET_DIR:-.}}/.loki"
18301
+ echo "Session completed at $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$loki_dir/COMPLETED" 2>/dev/null || true
18302
+
17942
18303
  # Finish-and-own (v7.88.0): write a plain-English ownership handoff
17943
18304
  # (HANDOFF.md) for a non-technical owner. Runs AFTER the proof so the
17944
18305
  # "is it working?" verdict reads the receipt's honest headline. Default-on,