loki-mode 7.66.1 → 7.68.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/run.sh CHANGED
@@ -1725,12 +1725,25 @@ detect_complexity() {
1725
1725
  # Markdown PRD: count headers and checkboxes
1726
1726
  feature_count=$(grep -c "^##\|^- \[" "$prd_path" 2>/dev/null || echo "0")
1727
1727
  fi
1728
+ # WAVE8 FIX run.sh-provider-F1 (HIGH): grep -c prints "0" AND exits 1 on
1729
+ # zero matches; with the '|| echo "0"' fallback that yields "0\n0", which
1730
+ # crashes the integer tests below ([: 0\n0: integer expression expected)
1731
+ # and silently drops complexity from simple->standard. Strip to digits
1732
+ # after every assignment path (jq, both greps), mirroring file_count:1688.
1733
+ # "0\n0" -> "00" -> arithmetically 0.
1734
+ feature_count="${feature_count:-0}"
1735
+ feature_count="${feature_count//[^0-9]/}"
1736
+ feature_count="${feature_count:-0}"
1728
1737
 
1729
1738
  # Count distinct sections (h2/h3 headers) for structural complexity (#74)
1730
1739
  local section_count=0
1731
1740
  if [[ "$prd_path" != *.json ]]; then
1732
1741
  section_count=$(grep -c "^##\|^###" "$prd_path" 2>/dev/null || echo "0")
1733
1742
  fi
1743
+ # WAVE8 FIX run.sh-provider-F1: same grep -c double-output guard.
1744
+ section_count="${section_count:-0}"
1745
+ section_count="${section_count//[^0-9]/}"
1746
+ section_count="${section_count:-0}"
1734
1747
 
1735
1748
  # PRD complexity uses content length, feature count, AND structural depth (#74)
1736
1749
  # A PRD with multiple sections or substantial content is not "simple" even with few project files
@@ -1919,61 +1932,32 @@ get_provider_tier_param() {
1919
1932
  }
1920
1933
 
1921
1934
  #===============================================================================
1922
- # Provider Spawn Timeout (v6.0.0)
1923
- # Wraps provider invocation with timeout + retries.
1924
- # Default: 120s timeout, 2 retries.
1935
+ # Provider Spawn Timeout (removed WAVE9 / provider-F2)
1936
+ #
1937
+ # A former invoke_with_timeout() helper (v6.0.0) wrapped a command in
1938
+ # `timeout <s> "$@"` with a retry loop. It was never wired to the main
1939
+ # provider invocation and is intentionally not revived, for two reasons:
1940
+ #
1941
+ # 1. No safe generous default. The main provider call is a long-running
1942
+ # autonomous coding agent. Any fixed timeout short enough to catch a
1943
+ # hang would also kill legitimate multi-minute iterations, and there is
1944
+ # no "generous enough" value that is both safe and useful by default.
1945
+ # 2. Wrong retry semantics. The helper re-ran the same command on timeout.
1946
+ # Re-running a coding agent mid-work (it may have already edited files)
1947
+ # is actively harmful, not protective.
1948
+ #
1949
+ # The main invocation is also a pipeline (`claude | tee | python3`), which a
1950
+ # positional-arg `timeout "$@"` wrapper cannot wrap at all. Interrupting a
1951
+ # hung provider is handled by the SIGINT trap (kill_provider_child) instead.
1952
+ #
1953
+ # The `loki config spawn_timeout` / `spawn_retries` knobs (autonomy/loki) and
1954
+ # the config->env mapping in this file (`'spawn_timeout':'LOKI_SPAWN_TIMEOUT'`
1955
+ # in the config loader) still export LOKI_SPAWN_TIMEOUT / LOKI_SPAWN_RETRIES,
1956
+ # but nothing consumes them now. The mapping line is intentionally left in place
1957
+ # (a config-schema test may enumerate it); the full inert-knob removal spans
1958
+ # autonomy/loki too and is a separate cross-file follow-up.
1925
1959
  #===============================================================================
1926
1960
 
1927
- PROVIDER_SPAWN_TIMEOUT=${LOKI_SPAWN_TIMEOUT:-120}
1928
- PROVIDER_SPAWN_RETRIES=${LOKI_SPAWN_RETRIES:-2}
1929
-
1930
- # Invoke a command with timeout and retry logic
1931
- # Usage: invoke_with_timeout <timeout_seconds> <retries> <command...>
1932
- invoke_with_timeout() {
1933
- local timeout="$1"
1934
- local max_retries="$2"
1935
- shift 2
1936
-
1937
- local attempt=0
1938
- while [ $attempt -le $max_retries ]; do
1939
- if [ $attempt -gt 0 ]; then
1940
- log_warn "Provider spawn retry $attempt/$max_retries..."
1941
- fi
1942
-
1943
- local exit_code=0
1944
- # Use timeout command if available (GNU coreutils or macOS)
1945
- if command -v timeout &>/dev/null; then
1946
- timeout "$timeout" "$@"
1947
- exit_code=$?
1948
- elif command -v gtimeout &>/dev/null; then
1949
- gtimeout "$timeout" "$@"
1950
- exit_code=$?
1951
- else
1952
- # Fallback: no timeout wrapper, run directly
1953
- log_warn "timeout/gtimeout not available - running without timeout enforcement"
1954
- "$@"
1955
- exit_code=$?
1956
- fi
1957
-
1958
- # Exit code 124 = timeout
1959
- if [ $exit_code -eq 124 ]; then
1960
- log_warn "Provider spawn timed out after ${timeout}s (attempt $((attempt+1))/$((max_retries+1)))"
1961
- ((attempt++))
1962
- continue
1963
- fi
1964
-
1965
- return $exit_code
1966
- done
1967
-
1968
- log_error "Provider spawn failed after $((max_retries+1)) attempts (timeout=${timeout}s)"
1969
- # Crash friction (retry_loop): provider spawn exhausted all retries -- a
1970
- # clear threshold (not a single retry). Best-effort, never blocks.
1971
- if type loki_crash_friction &>/dev/null; then
1972
- loki_crash_friction "retry_loop" "provider spawn failed after $((max_retries+1)) attempts" >/dev/null 2>&1 || true
1973
- fi
1974
- return 124
1975
- }
1976
-
1977
1961
  #===============================================================================
1978
1962
  # GitHub Integration Functions (v4.1.0)
1979
1963
  #===============================================================================
@@ -8927,6 +8911,63 @@ _dispatch_reviewer() {
8927
8911
  esac
8928
8912
  }
8929
8913
 
8914
+ # WAVE8 FIX run.sh-F1/F3 (CRITICAL/HIGH): SAFE-DEFAULT verdict classification.
8915
+ # Given a reviewer file, extract the VERDICT: line (tolerant of leading
8916
+ # markdown like '**VERDICT:**' or '# VERDICT:' so fewer reviewers fall to
8917
+ # NO_VERDICT) and classify it as one of: FAIL, PASS, AMBIGUOUS, NONE.
8918
+ # FAIL -> verdict text contains FAIL/REJECT/BLOCK (verbose suffixes like
8919
+ # "FAIL - [Critical] SQLi", "FAIL.", "FAIL (3 criticals)" all match)
8920
+ # PASS -> verdict text contains PASS/APPROVE (and NOT a fail token); this
8921
+ # preserves the deliberate "PASS with concerns" = pass semantics.
8922
+ # AMBIGUOUS -> a VERDICT: line exists but matches neither (unparseable token).
8923
+ # Callers MUST treat this as non-passing (safe direction), never pass.
8924
+ # NONE -> no parseable VERDICT: line at all (empty / missing).
8925
+ # FAIL-first ordering means a verdict naming both (rare) blocks -- the safe way.
8926
+ # Mirrors the council's _council_parse_vote: parse-miss defaults to the safe
8927
+ # (blocking) direction, never to pass.
8928
+ _classify_verdict() {
8929
+ local file="$1"
8930
+ [ -f "$file" ] && [ -s "$file" ] || { echo "NONE"; return 0; }
8931
+ local verdict
8932
+ # Tolerant anchor: optional leading whitespace, then optional markdown
8933
+ # markers (* # >), then optional whitespace, then VERDICT:. This rescues
8934
+ # '**VERDICT:** FAIL', '# VERDICT: PASS', '> VERDICT: FAIL' that the strict
8935
+ # '^VERDICT:' anchor missed (those previously became NO_VERDICT and dropped
8936
+ # the reviewer's dissent).
8937
+ verdict=$(grep -iE "^[[:space:]]*[*#>]*[[:space:]]*VERDICT:" "$file" \
8938
+ | head -1 \
8939
+ | sed -E 's/^[[:space:]]*[*#>]*[[:space:]]*[Vv][Ee][Rr][Dd][Ii][Cc][Tt]:[*[:space:]]*//' \
8940
+ | tr '[:lower:]' '[:upper:]')
8941
+ # Classify on the FIRST verdict TOKEN only, not a substring scan of the whole
8942
+ # despaced line. A whole-line scan is asymmetric and wrong: "PASS, no failures
8943
+ # found" or "PASS - no blocking issues" contain FAIL/BLOCK as substrings and
8944
+ # would misclassify a valid PASS as FAIL (a false-block, and worse, it breaks
8945
+ # the unanimous-PASS Devil's-Advocate trigger -> indirect false-PASS). Take
8946
+ # the leading alphabetic run as the verdict word: "FAIL - [Critical] x" ->
8947
+ # FAIL, "PASS, no failures" -> PASS. Strip leading markdown emphasis first.
8948
+ verdict=$(printf '%s' "$verdict" | sed -E 's/^[*_`[:space:]]+//')
8949
+ local _vtok
8950
+ _vtok=$(printf '%s' "$verdict" | sed -E 's/[^A-Z].*$//')
8951
+ if [ -z "$_vtok" ]; then echo "NONE"; return 0; fi
8952
+ case "$_vtok" in
8953
+ FAIL|FAILED|FAILURE|REJECT|REJECTED|BLOCK|BLOCKED) echo "FAIL" ;;
8954
+ PASS|PASSED|APPROVE|APPROVED|OK) echo "PASS" ;;
8955
+ *) echo "AMBIGUOUS" ;;
8956
+ esac
8957
+ }
8958
+
8959
+ # WAVE8 FIX run.sh-F2 (HIGH): SAFE-DEFAULT severity detection. Returns 0
8960
+ # (blocking) if the reviewer file names a Critical or High severity finding in
8961
+ # any realistic emitted form: bracketed '[Critical]', bold '**Critical**',
8962
+ # 'Severity: High', or a bullet line '- Critical' / '* High'. The strict
8963
+ # bracket-only match previously missed unbracketed forms, so a FAIL naming an
8964
+ # unbracketed Critical was treated as non-blocking. BSD/GNU portable (no \b).
8965
+ _severity_is_blocking() {
8966
+ local file="$1"
8967
+ [ -f "$file" ] || return 1
8968
+ grep -qiE '(\[(critical|high)\])|(\*\*[[:space:]]*(critical|high)[[:space:]]*\*\*)|(severity:?[[:space:]]*(critical|high))|(^[[:space:]]*[-*][[:space:]]+(critical|high)([[:space:]:.,*]|$))' "$file"
8969
+ }
8970
+
8930
8971
  run_code_review() {
8931
8972
  local loki_dir="${TARGET_DIR:-.}/.loki"
8932
8973
  local review_dir="$loki_dir/quality/reviews"
@@ -9284,21 +9325,27 @@ BUILD_PROMPT
9284
9325
  continue
9285
9326
  fi
9286
9327
 
9287
- # Extract verdict
9328
+ # Extract + classify verdict (WAVE8 FIX run.sh-F1/F3). _classify_verdict
9329
+ # uses a markdown-tolerant anchor (rescues '**VERDICT:** FAIL') and a
9330
+ # SAFE-DEFAULT contract: FAIL=any FAIL/REJECT/BLOCK token (so verbose
9331
+ # "FAIL - [Critical] SQLi" / "FAIL." / "FAIL (3 criticals)" all count as
9332
+ # FAIL, previously mis-counted as PASS); PASS=PASS/APPROVE; AMBIGUOUS=a
9333
+ # verdict line that parses to neither; NONE=no parseable verdict line.
9288
9334
  local verdict
9289
- verdict=$(grep -i "^VERDICT:" "$review_output" | head -1 | sed 's/^VERDICT:[[:space:]]*//' | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')
9290
-
9291
- # FIX A2: a "real verdict" is the PRESENCE of a non-empty VERDICT: line,
9292
- # not a specific token. A non-empty file with NO VERDICT line (garbage or
9293
- # a truncated reply) previously counted as PASS and could approve the gate
9294
- # on a meaningless file; now it is a non-verdict (not real, not a pass).
9295
- # We deliberately keep the original non-FAIL=pass semantics for any file
9296
- # that DOES carry a verdict line (PASS, APPROVE, "PASS with concerns",
9297
- # etc. all count as pass) so verbose-but-real verdicts are never
9298
- # false-blocked. The only added block relative to shipped behavior is the
9299
- # zero-real-verdicts (all-empty) case.
9300
- if [ -z "$verdict" ]; then
9301
- log_warn "Reviewer $reviewer_name produced no VERDICT line (empty or unparseable reply)"
9335
+ verdict=$(_classify_verdict "$review_output")
9336
+
9337
+ # FIX A2 + WAVE8 FIX run.sh-F1/F3: a "real verdict" is a parseable
9338
+ # VERDICT line that classifies cleanly to PASS or FAIL. NONE (no usable
9339
+ # verdict line) AND AMBIGUOUS (a verdict line whose token is neither PASS
9340
+ # nor FAIL, e.g. "VERDICT: UNCLEAR") are BOTH routed to the NO_VERDICT
9341
+ # path. This is the SAFE-DEFAULT contract: an unparseable token must NOT
9342
+ # silently pass. It cannot count toward pass_count, and merely bumping
9343
+ # fail_count would be inert (only has_blocking / review_inconclusive gate
9344
+ # the return). So we treat it as a non-real verdict; the
9345
+ # real_verdict_count < reviewer_count check below then makes the review
9346
+ # inconclusive -> bounded retry -> block (FIX 3 machinery).
9347
+ if [ "$verdict" = "NONE" ] || [ "$verdict" = "AMBIGUOUS" ]; then
9348
+ log_warn "Reviewer $reviewer_name returned no usable verdict (empty, unparseable, or ambiguous token)"
9302
9349
  verdicts_summary="${verdicts_summary}${reviewer_name}:NO_VERDICT "
9303
9350
  ((no_output_count++))
9304
9351
  continue
@@ -9306,8 +9353,9 @@ BUILD_PROMPT
9306
9353
  ((real_verdict_count++))
9307
9354
  if [ "$verdict" = "FAIL" ]; then
9308
9355
  ((fail_count++))
9309
- # Check for Critical/High severity findings
9310
- if grep -qiE "\[(Critical|High)\]" "$review_output"; then
9356
+ # Check for Critical/High severity findings (bracketed OR unbracketed
9357
+ # OR bold OR 'Severity:' OR bullet form -- WAVE8 FIX run.sh-F2).
9358
+ if _severity_is_blocking "$review_output"; then
9311
9359
  has_blocking=true
9312
9360
  log_error "BLOCKING: $reviewer_name found Critical/High severity issues"
9313
9361
  else
@@ -9320,16 +9368,25 @@ BUILD_PROMPT
9320
9368
  verdicts_summary="${verdicts_summary}${reviewer_name}:${verdict:-UNKNOWN} "
9321
9369
  done
9322
9370
 
9323
- # Finding #596 FIX A2: zero real verdicts when reviewers were expected =>
9324
- # INCONCLUSIVE => blocking. Optional bounded retry first (LOKI_REVIEW_RETRY=1,
9325
- # default on) so a transient empty-output blip does not hard-block; the retry
9326
- # re-runs the whole review with the (now .loki-excluded) diff. Opt out of the
9327
- # block entirely with LOKI_REVIEW_INCONCLUSIVE_BLOCK=0 (records, never blocks).
9371
+ # Finding #596 FIX A2 + WAVE8 FIX run.sh-F3: a review is INCONCLUSIVE (=>
9372
+ # blocking) whenever FEWER reviewers returned a usable verdict than were
9373
+ # dispatched. The original gate only fired on real_verdict_count==0 (ALL
9374
+ # reviewers empty); a MIXED review (e.g. 1 of 3 NO_VERDICT, 2 PASS) silently
9375
+ # passed on the surviving majority and dropped the malformed reviewer's
9376
+ # potential dissent (Devil's Advocate never fired). Now ANY NO_VERDICT
9377
+ # reviewer makes the review inconclusive: a dropped reviewer is a dropped
9378
+ # vote, and the safe direction is to refuse to pass on a partial council.
9379
+ # The markdown-tolerant anchor in _classify_verdict already rescues most
9380
+ # real-but-wrapped verdicts, so this fires only on genuinely unusable output.
9381
+ # Optional bounded retry first (LOKI_REVIEW_RETRY=1, default on) so a
9382
+ # transient empty-output blip does not hard-block; the retry re-runs the
9383
+ # whole review with the (now .loki-excluded) diff. Opt out of the block
9384
+ # entirely with LOKI_REVIEW_INCONCLUSIVE_BLOCK=0 (records, never blocks).
9328
9385
  local review_inconclusive=false
9329
- if [ "$reviewer_count" -gt 0 ] && [ "$real_verdict_count" -eq 0 ]; then
9386
+ if [ "$reviewer_count" -gt 0 ] && [ "$real_verdict_count" -lt "$reviewer_count" ]; then
9330
9387
  review_inconclusive=true
9331
- log_error "CODE REVIEW INCONCLUSIVE: 0 of $reviewer_count reviewers returned a usable verdict (no_output=$no_output_count)"
9332
- log_error " An all-empty review proves nothing; refusing to pass the gate on zero real verdicts."
9388
+ log_error "CODE REVIEW INCONCLUSIVE: only $real_verdict_count of $reviewer_count reviewers returned a usable verdict (no_output=$no_output_count)"
9389
+ log_error " A partial review drops dissent; refusing to pass the gate without every reviewer's verdict."
9333
9390
  if [ "${LOKI_REVIEW_RETRY:-1}" = "1" ] && [ "${_LOKI_REVIEW_RETRYING:-0}" != "1" ]; then
9334
9391
  log_warn " Retrying code review once (LOKI_REVIEW_RETRY=1)..."
9335
9392
  _LOKI_REVIEW_RETRYING=1 run_code_review
@@ -9437,9 +9494,12 @@ BUILD_DA_PROMPT
9437
9494
  _dispatch_reviewer "$da_prompt_text" "$da_output" || true
9438
9495
 
9439
9496
  if [ -f "$da_output" ] && [ -s "$da_output" ]; then
9497
+ # WAVE8 FIX run.sh-F1/F2: classify with the shared SAFE-DEFAULT
9498
+ # helpers so a verbose DA "VERDICT: FAIL - [Critical] ..." (and
9499
+ # AMBIGUOUS tokens) and an unbracketed Critical/High both block.
9440
9500
  local da_verdict
9441
- da_verdict=$(grep -i "^VERDICT:" "$da_output" | head -1 | sed 's/^VERDICT:[[:space:]]*//' | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')
9442
- if [ "$da_verdict" = "FAIL" ] && grep -qiE "\[(Critical|High)\]" "$da_output"; then
9501
+ da_verdict=$(_classify_verdict "$da_output")
9502
+ if { [ "$da_verdict" = "FAIL" ] || [ "$da_verdict" = "AMBIGUOUS" ]; } && _severity_is_blocking "$da_output"; then
9443
9503
  has_blocking=true
9444
9504
  # Audit accuracy: aggregate.json was written above (line ~8429)
9445
9505
  # with has_blocking=false (entering this block requires a
@@ -9465,7 +9525,7 @@ DA_AGG_PATCH
9465
9525
  log_error "DEVIL'S ADVOCATE: found Critical/High issue the unanimous council missed -- BLOCK"
9466
9526
  {
9467
9527
  echo "DEVILS_ADVOCATE_BLOCK: Critical/High found after unanimous PASS"
9468
- grep -iE "\[(Critical|High)\]" "$da_output" || true
9528
+ grep -iE '(\[(critical|high)\])|(\*\*[[:space:]]*(critical|high)[[:space:]]*\*\*)|(severity:?[[:space:]]*(critical|high))|(^[[:space:]]*[-*][[:space:]]+(critical|high)([[:space:]:.,*]|$))' "$da_output" || true
9469
9529
  } >> "$review_dir/$review_id/anti-sycophancy.txt"
9470
9530
  else
9471
9531
  log_info "Devil's Advocate: no additional Critical/High issues found"
@@ -9487,16 +9547,16 @@ DA_AGG_PATCH
9487
9547
  return 1
9488
9548
  fi
9489
9549
 
9490
- # Finding #596 FIX A2: an inconclusive review (zero real verdicts, retry
9491
- # already exhausted or disabled) blocks unless explicitly opted out. This is
9492
- # the 'verified before done' promise: a review that produced no usable verdict
9493
- # cannot stand in for a real review.
9550
+ # Finding #596 FIX A2 + WAVE8 FIX run.sh-F3: an inconclusive review (fewer
9551
+ # usable verdicts than reviewers, retry already exhausted or disabled) blocks
9552
+ # unless explicitly opted out. This is the 'verified before done' promise: a
9553
+ # review missing any reviewer's verdict cannot stand in for a full review.
9494
9554
  if [ "$review_inconclusive" = "true" ]; then
9495
9555
  if [ "${LOKI_REVIEW_INCONCLUSIVE_BLOCK:-1}" = "0" ]; then
9496
- log_warn "Code review inconclusive (0/$reviewer_count real verdicts) but LOKI_REVIEW_INCONCLUSIVE_BLOCK=0 - not blocking"
9556
+ log_warn "Code review inconclusive ($real_verdict_count/$reviewer_count real verdicts) but LOKI_REVIEW_INCONCLUSIVE_BLOCK=0 - not blocking"
9497
9557
  return 0
9498
9558
  fi
9499
- log_error "CODE REVIEW BLOCKED: inconclusive (0/$reviewer_count reviewers returned a usable verdict)"
9559
+ log_error "CODE REVIEW BLOCKED: inconclusive ($real_verdict_count/$reviewer_count reviewers returned a usable verdict)"
9500
9560
  log_error " Review details: $review_dir/$review_id/ ; opt out with LOKI_REVIEW_INCONCLUSIVE_BLOCK=0"
9501
9561
  return 1
9502
9562
  fi
@@ -9889,6 +9949,16 @@ CPEOF
9889
9949
  find "$checkpoint_dir" -maxdepth 1 -type d -name "cp-*" 2>/dev/null \
9890
9950
  | while read -r p; do basename "$p"; done | sort -t'-' -k3 -n \
9891
9951
  | head -n "$to_remove" | while read -r old_cp; do
9952
+ # WAVE9 (checkpoint leak): also delete the anchored worktree-snapshot
9953
+ # ref so its stash commit becomes eligible for `git gc`. Without this,
9954
+ # refs/loki/cp/<id> (and its commit) leaked forever even after the
9955
+ # checkpoint directory was pruned. Targeted deletion of exactly the
9956
+ # ids being pruned ONLY -- never a blanket refs/loki/cp/* sweep, since
9957
+ # git refs are shared across worktrees of one repo while checkpoint
9958
+ # dirs are per-TARGET_DIR; a parallel worktree may still need a ref we
9959
+ # are not pruning here. `|| true` because not every checkpoint has a
9960
+ # ref (only those where `git stash create` returned a non-empty sha).
9961
+ git update-ref -d "refs/loki/cp/${old_cp}" 2>/dev/null || true
9892
9962
  old_cp="${checkpoint_dir}/${old_cp}"
9893
9963
  rm -rf "$old_cp" 2>/dev/null || true
9894
9964
  done
@@ -11403,7 +11473,11 @@ try:
11403
11473
  storage = MemoryStorage(f'{target_dir}/.loki/memory')
11404
11474
  retriever = MemoryRetrieval(storage)
11405
11475
  context = {'goal': goal, 'phase': phase}
11406
- results = retriever.retrieve_task_aware(context, top_k=3)
11476
+ # The autonomous RARV loop opts into persist_boost so retrieved memories are
11477
+ # reinforced on disk ("use it or lose it"). Manual surfaces (loki memory CLI,
11478
+ # dashboard, MCP) keep the default persist_boost=False so a human browsing
11479
+ # memories does not silently inflate their importance.
11480
+ results = retriever.retrieve_task_aware(context, top_k=3, persist_boost=True)
11407
11481
  if results:
11408
11482
  print('RELEVANT MEMORIES:')
11409
11483
  for r in results[:3]:
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.66.1"
10
+ __version__ = "7.68.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -8675,6 +8675,17 @@ def _get_migration_imports():
8675
8675
  return _migration_imports
8676
8676
 
8677
8677
 
8678
+ def _get_migration_terminal_phase():
8679
+ """Return the last phase in the migration PHASE_ORDER (the terminal phase),
8680
+ or None if the migration engine is unavailable. Used to let the terminal
8681
+ phase be advanced/completed without a successor to_phase (WAVE9 F1)."""
8682
+ try:
8683
+ from dashboard.migration_engine import PHASE_ORDER
8684
+ return PHASE_ORDER[-1] if PHASE_ORDER else None
8685
+ except (ImportError, IndexError):
8686
+ return None
8687
+
8688
+
8678
8689
  @app.get("/api/migration/list", dependencies=[Depends(auth.require_scope("read"))])
8679
8690
  def list_migrations_endpoint():
8680
8691
  """List all migrations."""
@@ -8825,7 +8836,16 @@ def advance_migration(migration_id: str, request_body: dict):
8825
8836
  MigrationPipeline, list_migrations = imports
8826
8837
  from_phase = request_body.get("from_phase")
8827
8838
  to_phase = request_body.get("to_phase")
8828
- if not from_phase or not to_phase:
8839
+ # The terminal phase (the last in PHASE_ORDER, e.g. "verify") has no
8840
+ # successor, so check_phase_gate can never pass for it and to_phase is
8841
+ # meaningless. Without this carve-out the terminal phase could never be
8842
+ # completed via the API, so overall_status could never reach "completed"
8843
+ # (WAVE9 migration-F1). For the terminal phase we require only from_phase
8844
+ # and skip the gate; advance_phase still validates the phase and the
8845
+ # (ValueError, RuntimeError) -> 409 handler below preserves idempotency.
8846
+ terminal_phase = _get_migration_terminal_phase()
8847
+ is_terminal = terminal_phase is not None and from_phase == terminal_phase
8848
+ if not from_phase or (not to_phase and not is_terminal):
8829
8849
  raise HTTPException(status_code=400, detail="from_phase and to_phase are required")
8830
8850
  # Load pipeline and check phase gate before the try/except to let
8831
8851
  # HTTPException and FileNotFoundError propagate naturally.
@@ -8833,9 +8853,10 @@ def advance_migration(migration_id: str, request_body: dict):
8833
8853
  pipeline = MigrationPipeline.load(migration_id)
8834
8854
  except FileNotFoundError:
8835
8855
  raise HTTPException(status_code=404, detail=f"Migration not found: {migration_id}")
8836
- passed, reason = pipeline.check_phase_gate(from_phase, to_phase)
8837
- if not passed:
8838
- raise HTTPException(status_code=409, detail=reason)
8856
+ if not is_terminal:
8857
+ passed, reason = pipeline.check_phase_gate(from_phase, to_phase)
8858
+ if not passed:
8859
+ raise HTTPException(status_code=409, detail=reason)
8839
8860
  try:
8840
8861
  result = pipeline.advance_phase(from_phase)
8841
8862
  return asdict(result) if hasattr(result, '__dataclass_fields__') else result
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.66.1
5
+ **Version:** v7.68.0
6
6
 
7
7
  ---
8
8
 
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
395
395
  # Run Loki Mode in Docker (Claude provider, API-key auth)
396
396
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
397
397
  -v $(pwd):/workspace -w /workspace \
398
- asklokesh/loki-mode:7.66.1 start ./my-spec.md
398
+ asklokesh/loki-mode:7.68.0 start ./my-spec.md
399
399
  ```
400
400
 
401
401
  ##### docker compose + .env (no host install)