loki-mode 6.61.0 → 6.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/app-runner.sh +34 -8
  4. package/autonomy/completion-council.sh +70 -32
  5. package/autonomy/issue-parser.sh +4 -7
  6. package/autonomy/loki +234 -118
  7. package/autonomy/notification-checker.py +49 -23
  8. package/autonomy/run.sh +162 -79
  9. package/autonomy/sandbox.sh +91 -24
  10. package/bin/loki-mode.js +1 -2
  11. package/bin/postinstall.js +10 -4
  12. package/dashboard/__init__.py +1 -1
  13. package/dashboard/control.py +46 -36
  14. package/dashboard/server.py +49 -27
  15. package/docs/BUG-AUDIT-v6.61.0.md +957 -0
  16. package/docs/INSTALLATION.md +2 -2
  17. package/events/bus.py +129 -28
  18. package/events/bus.ts +41 -27
  19. package/events/emit.sh +1 -1
  20. package/integrations/openclaw/README.md +139 -0
  21. package/integrations/openclaw/SKILL.md +88 -0
  22. package/integrations/openclaw/bridge/__init__.py +1 -0
  23. package/integrations/openclaw/bridge/__main__.py +88 -0
  24. package/integrations/openclaw/bridge/schema_map.py +180 -0
  25. package/integrations/openclaw/bridge/watcher.py +100 -0
  26. package/integrations/openclaw/scripts/format-progress.sh +80 -0
  27. package/integrations/openclaw/scripts/poll-status.sh +74 -0
  28. package/integrations/vibe-kanban.md +289 -0
  29. package/mcp/__init__.py +1 -1
  30. package/mcp/server.py +96 -73
  31. package/memory/consolidation.py +21 -6
  32. package/memory/engine.py +53 -26
  33. package/memory/layers/index_layer.py +16 -3
  34. package/memory/layers/timeline_layer.py +16 -3
  35. package/memory/retrieval.py +4 -1
  36. package/memory/schemas.py +4 -2
  37. package/memory/storage.py +25 -4
  38. package/memory/token_economics.py +9 -2
  39. package/memory/vector_index.py +2 -2
  40. package/package.json +3 -1
  41. package/providers/cline.sh +5 -4
  42. package/providers/codex.sh +27 -5
  43. package/providers/gemini.sh +59 -23
  44. package/providers/loader.sh +3 -2
  45. package/skills/parallel-workflows.md +9 -7
  46. package/state/__init__.py +10 -0
  47. package/state/index.ts +18 -0
  48. package/state/manager.py +1801 -0
  49. package/state/manager.ts +1774 -0
  50. package/state/sqlite_backend.py +188 -0
  51. package/state/test_manager.py +703 -0
  52. package/state/test_manager.ts +366 -0
  53. package/templates/README.md +19 -4
  54. package/templates/dashboard.md +45 -0
  55. package/templates/data-pipeline.md +45 -0
  56. package/templates/game.md +48 -0
  57. package/templates/microservice.md +49 -0
  58. package/templates/npm-library.md +42 -0
  59. package/templates/rest-api.md +170 -33
  60. package/templates/slack-bot.md +48 -0
  61. package/templates/web-scraper.md +45 -0
  62. package/web-app/server.py +245 -151
  63. package/templates/saas-app.md +0 -42
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v6.61.0
6
+ # Loki Mode v6.62.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -267,4 +267,4 @@ The following features are documented in skill modules but not yet fully automat
267
267
  | Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
268
268
  | Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
269
269
 
270
- **v6.61.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
270
+ **v6.62.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.61.0
1
+ 6.62.0
@@ -42,6 +42,7 @@ _APP_RUNNER_PORT=""
42
42
  _APP_RUNNER_PID=""
43
43
  _APP_RUNNER_URL=""
44
44
  _APP_RUNNER_IS_DOCKER=false
45
+ _APP_RUNNER_DOCKER_CONTAINER=""
45
46
  _APP_RUNNER_HAS_SETSID=false
46
47
  _APP_RUNNER_CRASH_COUNT=0
47
48
  _APP_RUNNER_RESTART_COUNT=0
@@ -145,7 +146,8 @@ _detect_port() {
145
146
  fi
146
147
  local port
147
148
  # Handle both simple (HOST:CONTAINER) and IP-bound (IP:HOST:CONTAINER) port formats
148
- port=$(grep -E '^\s*-\s*"?[0-9]' "$compose_file" 2>/dev/null | head -1 | sed 's/.*- *"*//;s/".*//;' | awk -F: '{print $(NF-1)}')
149
+ # Also handle port ranges like "8080-8090:8080-8090" by taking the first port
150
+ port=$(grep -E '^\s*-\s*"?[0-9]' "$compose_file" 2>/dev/null | head -1 | sed 's/.*- *"*//;s/".*//;' | awk -F: '{print $(NF-1)}' | awk -F- '{print $1}')
149
151
  _APP_RUNNER_PORT="${port:-8080}"
150
152
  ;;
151
153
  *docker\ build*)
@@ -244,7 +246,11 @@ app_runner_init() {
244
246
  if [ -f "$dir/Dockerfile" ]; then
245
247
  if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
246
248
  _detect_port "docker build"
247
- _APP_RUNNER_METHOD="docker build -t loki-app . && docker run -d -p ${_APP_RUNNER_PORT}:${_APP_RUNNER_PORT} --name loki-app-container loki-app"
249
+ # Include project hash in container name to avoid collisions across projects
250
+ local _project_hash
251
+ _project_hash=$(echo "$dir" | (md5sum 2>/dev/null || md5 -r 2>/dev/null || echo "$$") | cut -c1-8)
252
+ _APP_RUNNER_DOCKER_CONTAINER="loki-app-${_project_hash}"
253
+ _APP_RUNNER_METHOD="docker build -t loki-app . && docker run -d -p ${_APP_RUNNER_PORT}:${_APP_RUNNER_PORT} --name ${_APP_RUNNER_DOCKER_CONTAINER} loki-app"
248
254
  _APP_RUNNER_IS_DOCKER=true
249
255
  _write_detection "dockerfile" "$_APP_RUNNER_METHOD"
250
256
  log_info "App Runner: detected Dockerfile"
@@ -431,12 +437,28 @@ app_runner_start() {
431
437
  # Start the process in a new process group
432
438
  if command -v setsid >/dev/null 2>&1; then
433
439
  _APP_RUNNER_HAS_SETSID=true
434
- (cd "$dir" && setsid bash -c "$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
440
+ # Use setsid with a PID file so we capture the actual child PID (the process
441
+ # group leader) rather than the subshell PID, which would orphan the app.
442
+ local _pgid_file="$_APP_RUNNER_DIR/app.pgid.$$"
443
+ (cd "$dir" && setsid bash -c 'echo $$ > "'"$_pgid_file"'"; exec '"$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
444
+ local _subshell_pid=$!
445
+ # Wait briefly for the pgid file to appear, then read the real PGID
446
+ local _pgid_wait=0
447
+ while [ ! -s "$_pgid_file" ] && [ "$_pgid_wait" -lt 10 ]; do
448
+ sleep 0.1
449
+ _pgid_wait=$(( _pgid_wait + 1 ))
450
+ done
451
+ if [ -s "$_pgid_file" ]; then
452
+ _APP_RUNNER_PID=$(cat "$_pgid_file")
453
+ else
454
+ _APP_RUNNER_PID=$_subshell_pid
455
+ fi
456
+ rm -f "$_pgid_file"
435
457
  else
436
458
  _APP_RUNNER_HAS_SETSID=false
437
459
  (cd "$dir" && bash -c "$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
460
+ _APP_RUNNER_PID=$!
438
461
  fi
439
- _APP_RUNNER_PID=$!
440
462
  # Register with central PID registry if available
441
463
  if type register_pid &>/dev/null; then
442
464
  register_pid "$_APP_RUNNER_PID" "app-runner" "method=$_APP_RUNNER_METHOD"
@@ -494,8 +516,10 @@ app_runner_stop() {
494
516
 
495
517
  # Docker cleanup
496
518
  if [ "$_APP_RUNNER_IS_DOCKER" = true ]; then
497
- docker stop loki-app-container 2>/dev/null || true
498
- docker rm loki-app-container 2>/dev/null || true
519
+ if [ -n "$_APP_RUNNER_DOCKER_CONTAINER" ]; then
520
+ docker stop "$_APP_RUNNER_DOCKER_CONTAINER" 2>/dev/null || true
521
+ docker rm "$_APP_RUNNER_DOCKER_CONTAINER" 2>/dev/null || true
522
+ fi
499
523
  if echo "$_APP_RUNNER_METHOD" | grep -q "docker compose"; then
500
524
  (cd "${TARGET_DIR:-.}" && docker compose down 2>/dev/null) || true
501
525
  fi
@@ -704,8 +728,10 @@ app_runner_cleanup() {
704
728
 
705
729
  # Docker-specific cleanup
706
730
  if [ "$_APP_RUNNER_IS_DOCKER" = true ]; then
707
- docker stop loki-app-container 2>/dev/null || true
708
- docker rm loki-app-container 2>/dev/null || true
731
+ if [ -n "$_APP_RUNNER_DOCKER_CONTAINER" ]; then
732
+ docker stop "$_APP_RUNNER_DOCKER_CONTAINER" 2>/dev/null || true
733
+ docker rm "$_APP_RUNNER_DOCKER_CONTAINER" 2>/dev/null || true
734
+ fi
709
735
  if echo "$_APP_RUNNER_METHOD" | grep -q "docker compose"; then
710
736
  (cd "${TARGET_DIR:-.}" && docker compose down 2>/dev/null) || true
711
737
  fi
@@ -48,6 +48,10 @@ if ! [[ "$COUNCIL_CHECK_INTERVAL" =~ ^[1-9][0-9]*$ ]]; then
48
48
  COUNCIL_CHECK_INTERVAL=5
49
49
  fi
50
50
  COUNCIL_MIN_ITERATIONS=${LOKI_COUNCIL_MIN_ITERATIONS:-3}
51
+ # BUG-QG-012: Enforce minimum of 1 to prevent council approving at iteration 0
52
+ if [ "$COUNCIL_MIN_ITERATIONS" -lt 1 ] 2>/dev/null; then
53
+ COUNCIL_MIN_ITERATIONS=1
54
+ fi
51
55
  COUNCIL_CONVERGENCE_WINDOW=${LOKI_COUNCIL_CONVERGENCE_WINDOW:-3}
52
56
  COUNCIL_STAGNATION_LIMIT=${LOKI_COUNCIL_STAGNATION_LIMIT:-5}
53
57
  COUNCIL_DONE_SIGNAL_LIMIT=${LOKI_COUNCIL_DONE_SIGNAL_LIMIT:-10}
@@ -84,6 +88,7 @@ council_init() {
84
88
  COUNCIL_PRD_PATH="$prd_path"
85
89
  COUNCIL_CONSECUTIVE_NO_CHANGE=0
86
90
  COUNCIL_DONE_SIGNALS=0
91
+ COUNCIL_TOTAL_DONE_SIGNALS=0
87
92
  COUNCIL_LAST_DIFF_HASH=""
88
93
 
89
94
  mkdir -p "$COUNCIL_STATE_DIR"
@@ -131,7 +136,11 @@ council_track_iteration() {
131
136
  local staged_hash
132
137
  staged_hash=$(git diff --cached --stat 2>/dev/null | (md5sum 2>/dev/null || md5 -r 2>/dev/null) | cut -d' ' -f1 || echo "unknown")
133
138
 
134
- local combined_hash="${current_diff_hash}-${staged_hash}"
139
+ # Include latest commit hash so committed changes are detected (BUG-QG-004)
140
+ local commit_hash
141
+ commit_hash=$(git log --oneline -1 2>/dev/null | cut -d' ' -f1 || echo "unknown")
142
+
143
+ local combined_hash="${current_diff_hash}-${staged_hash}-${commit_hash}"
135
144
 
136
145
  if [ "$combined_hash" = "$COUNCIL_LAST_DIFF_HASH" ]; then
137
146
  ((COUNCIL_CONSECUTIVE_NO_CHANGE++))
@@ -248,6 +257,9 @@ council_vote() {
248
257
  log_header "COMPLETION COUNCIL - Iteration $ITERATION_COUNT"
249
258
  log_info "Convening ${COUNCIL_SIZE}-member council..."
250
259
 
260
+ # Compute threshold using ceiling(2/3) formula, consistent with council_aggregate_votes
261
+ local effective_threshold=$(( (COUNCIL_SIZE * 2 + 2) / 3 ))
262
+
251
263
  # Gather evidence for council members
252
264
  local evidence_file="$vote_dir/evidence.md"
253
265
  council_gather_evidence "$evidence_file" "$prd_path"
@@ -287,30 +299,57 @@ council_vote() {
287
299
  # is based only on issues below the severity threshold
288
300
  if [ "$vote_result" = "REJECT" ] && [ "$COUNCIL_SEVERITY_THRESHOLD" != "low" ] && [ -n "$member_issues" ]; then
289
301
  local has_blocking_issue=false
302
+ local non_blocking_count=0
303
+ local total_issue_count=0
290
304
  local severity_order="critical high medium low"
291
305
  local threshold_reached=false
292
306
 
293
307
  while IFS= read -r issue_line; do
294
308
  local issue_severity
295
309
  issue_severity=$(echo "$issue_line" | grep -oE "(CRITICAL|HIGH|MEDIUM|LOW)" | head -1 | tr '[:upper:]' '[:lower:]')
310
+ [ -z "$issue_severity" ] && continue
311
+ ((total_issue_count++))
296
312
  # Reset per issue line so previous iterations don't poison the check
297
313
  threshold_reached=false
298
314
  # Check if this severity meets or exceeds the threshold
315
+ local is_blocking=false
299
316
  for sev in $severity_order; do
300
317
  if [ "$sev" = "$issue_severity" ] && [ "$threshold_reached" = "false" ]; then
301
318
  has_blocking_issue=true
319
+ is_blocking=true
302
320
  break
303
321
  fi
304
322
  if [ "$sev" = "$COUNCIL_SEVERITY_THRESHOLD" ]; then
305
323
  threshold_reached=true
306
324
  fi
307
325
  done
326
+ if [ "$is_blocking" = "false" ]; then
327
+ ((non_blocking_count++))
328
+ fi
308
329
  done <<< "$member_issues"
309
330
 
331
+ # Apply error budget: if no blocking issues, check non-blocking ratio
310
332
  if [ "$has_blocking_issue" = "false" ]; then
311
- log_info " Member $member ($role): REJECT overridden to APPROVE (issues below ${COUNCIL_SEVERITY_THRESHOLD} threshold)"
312
- vote_result="APPROVE"
333
+ local budget_exceeded=false
334
+ if [ "$total_issue_count" -gt 0 ] && [ "$COUNCIL_ERROR_BUDGET" != "0.0" ] && [ "$COUNCIL_ERROR_BUDGET" != "0" ]; then
335
+ # Check if non-blocking issue ratio exceeds the error budget
336
+ budget_exceeded=$(_NB="$non_blocking_count" _TOTAL="$total_issue_count" _BUDGET="$COUNCIL_ERROR_BUDGET" python3 -c "
337
+ import os
338
+ nb = int(os.environ['_NB'])
339
+ total = int(os.environ['_TOTAL'])
340
+ budget = float(os.environ['_BUDGET'])
341
+ ratio = nb / total if total > 0 else 0.0
342
+ print('true' if ratio > budget else 'false')
343
+ " 2>/dev/null || echo "false")
344
+ fi
345
+ if [ "$budget_exceeded" = "true" ]; then
346
+ log_info " Member $member ($role): REJECT maintained (non-blocking issue ratio exceeds error budget ${COUNCIL_ERROR_BUDGET})"
347
+ else
348
+ log_info " Member $member ($role): REJECT overridden to APPROVE (issues below ${COUNCIL_SEVERITY_THRESHOLD} threshold, within error budget)"
349
+ vote_result="APPROVE"
350
+ fi
313
351
  fi
352
+
314
353
  fi
315
354
 
316
355
  if [ "$vote_result" = "APPROVE" ]; then
@@ -333,7 +372,7 @@ council_vote() {
333
372
  done
334
373
 
335
374
  # Anti-sycophancy check: if unanimous APPROVE, run devil's advocate
336
- if [ $approve_count -eq $COUNCIL_SIZE ] && [ $COUNCIL_SIZE -ge 3 ]; then
375
+ if [ $approve_count -eq $COUNCIL_SIZE ] && [ $COUNCIL_SIZE -ge 2 ]; then
337
376
  log_warn "Unanimous approval detected - running anti-sycophancy check..."
338
377
  local contrarian_verdict
339
378
  contrarian_verdict=$(council_devils_advocate "$evidence_file" "$vote_dir")
@@ -356,7 +395,7 @@ council_vote() {
356
395
  _COUNCIL_APPROVE="$approve_count" \
357
396
  _COUNCIL_REJECT="$reject_count" \
358
397
  _COUNCIL_ITERATION="${ITERATION_COUNT:-0}" \
359
- _COUNCIL_THRESHOLD="$COUNCIL_THRESHOLD" \
398
+ _COUNCIL_THRESHOLD="$effective_threshold" \
360
399
  python3 -c "
361
400
  import json, os
362
401
  from datetime import datetime, timezone
@@ -387,7 +426,7 @@ with open(state_file, 'w') as f:
387
426
  " || log_warn "Failed to record council vote results"
388
427
 
389
428
  echo ""
390
- log_info "Council verdict: $approve_count APPROVE / $reject_count REJECT (threshold: $COUNCIL_THRESHOLD)"
429
+ log_info "Council verdict: $approve_count APPROVE / $reject_count REJECT (threshold: $effective_threshold)"
391
430
  echo -e "$verdicts"
392
431
  echo ""
393
432
 
@@ -396,10 +435,10 @@ with open(state_file, 'w') as f:
396
435
  "iteration=$ITERATION_COUNT" \
397
436
  "approve=$approve_count" \
398
437
  "reject=$reject_count" \
399
- "threshold=$COUNCIL_THRESHOLD" \
400
- "result=$([ $approve_count -ge $COUNCIL_THRESHOLD ] && echo 'APPROVED' || echo 'REJECTED')" 2>/dev/null || true
438
+ "threshold=$effective_threshold" \
439
+ "result=$([ $approve_count -ge $effective_threshold ] && echo 'APPROVED' || echo 'REJECTED')" 2>/dev/null || true
401
440
 
402
- if [ $approve_count -ge $COUNCIL_THRESHOLD ]; then
441
+ if [ $approve_count -ge $effective_threshold ]; then
403
442
  return 0 # Council says DONE
404
443
  fi
405
444
  return 1 # Council says CONTINUE
@@ -754,27 +793,27 @@ ISSUES: CRITICAL:description (optional, one per line per issue)"
754
793
  claude)
755
794
  if command -v claude &>/dev/null; then
756
795
  local council_model="${PROVIDER_MODEL_FAST:-haiku}"
757
- verdict=$(echo "$prompt" | claude --model "$council_model" -p 2>/dev/null | tail -5)
796
+ verdict=$(echo "$prompt" | claude --model "$council_model" -p 2>/dev/null | tail -20)
758
797
  fi
759
798
  ;;
760
799
  codex)
761
800
  if command -v codex &>/dev/null; then
762
- verdict=$(codex exec --full-auto "$prompt" 2>/dev/null | tail -5)
801
+ verdict=$(codex exec --full-auto "$prompt" 2>/dev/null | tail -20)
763
802
  fi
764
803
  ;;
765
804
  gemini)
766
805
  if command -v gemini &>/dev/null; then
767
- verdict=$(echo "$prompt" | gemini 2>/dev/null | tail -5)
806
+ verdict=$(echo "$prompt" | gemini 2>/dev/null | tail -20)
768
807
  fi
769
808
  ;;
770
809
  cline)
771
810
  if command -v cline &>/dev/null; then
772
- verdict=$(cline -y "$prompt" 2>/dev/null | tail -5)
811
+ verdict=$(cline -y "$prompt" 2>/dev/null | tail -20)
773
812
  fi
774
813
  ;;
775
814
  aider)
776
815
  if command -v aider &>/dev/null; then
777
- verdict=$(aider --message "$prompt" --yes-always --no-auto-commits --no-git 2>/dev/null | tail -5)
816
+ verdict=$(aider --message "$prompt" --yes-always --no-auto-commits --no-git 2>/dev/null | tail -20)
778
817
  fi
779
818
  ;;
780
819
  esac
@@ -808,19 +847,13 @@ council_devils_advocate() {
808
847
  local evidence
809
848
  evidence=$(cat "$evidence_file" 2>/dev/null || echo "No evidence available")
810
849
 
811
- # Read all previous approvals
812
- local prev_verdicts=""
813
- for f in "$vote_dir"/member-*.txt; do
814
- [ -f "$f" ] && prev_verdicts="${prev_verdicts}\n$(cat "$f")"
815
- done
850
+ # BUG-QG-009: Do NOT show prior verdicts to devil's advocate (blind review)
851
+ # Previous code read member-*.txt files here, biasing the contrarian reviewer
816
852
 
817
853
  local prompt="ANTI-SYCOPHANCY CHECK: All council members unanimously APPROVED this project.
818
854
 
819
855
  Your job is to be the CONTRARIAN. Find ANY reason this should NOT be approved.
820
856
 
821
- Previous verdicts:
822
- ${prev_verdicts}
823
-
824
857
  EVIDENCE:
825
858
  ${evidence}
826
859
 
@@ -843,27 +876,27 @@ REASON: your reasoning"
843
876
  claude)
844
877
  if command -v claude &>/dev/null; then
845
878
  local council_model="${PROVIDER_MODEL_FAST:-haiku}"
846
- verdict=$(echo "$prompt" | claude --model "$council_model" -p 2>/dev/null | tail -5)
879
+ verdict=$(echo "$prompt" | claude --model "$council_model" -p 2>/dev/null | tail -20)
847
880
  fi
848
881
  ;;
849
882
  codex)
850
883
  if command -v codex &>/dev/null; then
851
- verdict=$(codex exec --full-auto "$prompt" 2>/dev/null | tail -5)
884
+ verdict=$(codex exec --full-auto "$prompt" 2>/dev/null | tail -20)
852
885
  fi
853
886
  ;;
854
887
  gemini)
855
888
  if command -v gemini &>/dev/null; then
856
- verdict=$(echo "$prompt" | gemini 2>/dev/null | tail -5)
889
+ verdict=$(echo "$prompt" | gemini 2>/dev/null | tail -20)
857
890
  fi
858
891
  ;;
859
892
  cline)
860
893
  if command -v cline &>/dev/null; then
861
- verdict=$(cline -y "$prompt" 2>/dev/null | tail -5)
894
+ verdict=$(cline -y "$prompt" 2>/dev/null | tail -20)
862
895
  fi
863
896
  ;;
864
897
  aider)
865
898
  if command -v aider &>/dev/null; then
866
- verdict=$(aider --message "$prompt" --yes-always --no-auto-commits --no-git 2>/dev/null | tail -5)
899
+ verdict=$(aider --message "$prompt" --yes-always --no-auto-commits --no-git 2>/dev/null | tail -20)
867
900
  fi
868
901
  ;;
869
902
  esac
@@ -985,10 +1018,15 @@ council_evaluate_member() {
985
1018
  # If code is still changing, work may not be done
986
1019
  local current_diff_hash
987
1020
  current_diff_hash=$(git diff --stat HEAD 2>/dev/null | (md5sum 2>/dev/null || md5 -r 2>/dev/null) | cut -d' ' -f1 || echo "unknown")
988
- if [ "$COUNCIL_CONSECUTIVE_NO_CHANGE" -eq 0 ] && [ "$ITERATION_COUNT" -gt "$COUNCIL_MIN_ITERATIONS" ]; then
989
- # Code is still actively changing -- likely not done
990
- vote="CONTINUE"
991
- reasons="${reasons}code still changing between iterations; "
1021
+ if [ "$COUNCIL_CONSECUTIVE_NO_CHANGE" -gt 0 ] && [ "$ITERATION_COUNT" -gt "$COUNCIL_MIN_ITERATIONS" ]; then
1022
+ # Code has stopped changing -- stagnation, not necessarily done
1023
+ # (BUG-QG-011: previously inverted -- forced CONTINUE when code was changing,
1024
+ # which penalized active progress. Now: stagnation with no passing checks = CONTINUE)
1025
+ if [ "$vote" = "COMPLETE" ]; then
1026
+ : # Other checks passed despite stagnation -- allow COMPLETE
1027
+ else
1028
+ reasons="${reasons}code stagnated with failing checks; "
1029
+ fi
992
1030
  fi
993
1031
 
994
1032
  # Check 3: Are there uncaught errors in logs?
@@ -1296,7 +1334,7 @@ council_evaluate() {
1296
1334
  complete_count=$(_RF="$round_file" python3 -c "import json, os; print(json.load(open(os.environ['_RF'])).get('complete_votes', 0))" 2>/dev/null || echo "0")
1297
1335
  fi
1298
1336
 
1299
- if [ "$complete_count" -eq "$COUNCIL_SIZE" ] && [ "$COUNCIL_SIZE" -ge 3 ]; then
1337
+ if [ "$complete_count" -eq "$COUNCIL_SIZE" ] && [ "$COUNCIL_SIZE" -ge 2 ]; then
1300
1338
  # Step 3: Unanimous -- run devil's advocate
1301
1339
  local da_result
1302
1340
  da_result=$(council_devils_advocate_review "$ITERATION_COUNT")
@@ -258,8 +258,7 @@ parse_github_issue() {
258
258
 
259
259
  # Parse the reference
260
260
  local parsed_ref
261
- parsed_ref=$(parse_issue_ref "$issue_ref")
262
- if [ $? -ne 0 ]; then
261
+ if ! parsed_ref=$(parse_issue_ref "$issue_ref"); then
263
262
  return 1
264
263
  fi
265
264
 
@@ -272,9 +271,7 @@ parse_github_issue() {
272
271
 
273
272
  # Fetch issue data
274
273
  local issue_data
275
- issue_data=$(gh issue view "$number" --repo "$owner/$repo" --json number,title,body,labels,assignees,milestone,state,url,createdAt,author 2>&1)
276
-
277
- if [ $? -ne 0 ]; then
274
+ if ! issue_data=$(gh issue view "$number" --repo "$owner/$repo" --json number,title,body,labels,assignees,milestone,state,url,createdAt,author 2>&1); then
278
275
  log_error "Failed to fetch issue: $issue_data"
279
276
  return 1
280
277
  fi
@@ -612,8 +609,8 @@ main() {
612
609
 
613
610
  # Parse the issue
614
611
  local result
615
- result=$(parse_github_issue "$issue_ref" "$format")
616
- local exit_code=$?
612
+ local exit_code=0
613
+ result=$(parse_github_issue "$issue_ref" "$format") || exit_code=$?
617
614
 
618
615
  if [ $exit_code -ne 0 ]; then
619
616
  exit $exit_code