loki-mode 7.45.1 → 7.47.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 (112) hide show
  1. package/README.md +16 -12
  2. package/SKILL.md +5 -5
  3. package/VERSION +1 -1
  4. package/autonomy/CONSTITUTION.md +9 -2
  5. package/autonomy/completion-council.sh +113 -0
  6. package/autonomy/lib/sentrux-gate.sh +1 -1
  7. package/autonomy/loki +2 -2
  8. package/autonomy/run.sh +445 -96
  9. package/autonomy/spec-interrogation.sh +549 -0
  10. package/dashboard/__init__.py +1 -1
  11. package/dashboard/auth.py +117 -2
  12. package/dashboard/server.py +9 -10
  13. package/docs/ACKNOWLEDGEMENTS.md +1 -1
  14. package/docs/COMPARISON.md +10 -10
  15. package/docs/COMPETITIVE-ANALYSIS.md +2 -2
  16. package/docs/INSTALLATION.md +2 -2
  17. package/docs/OPEN-CORE-BOUNDARY.md +6 -5
  18. package/docs/P0-SWEEP-PLAN.md +163 -0
  19. package/docs/P2-SPEC-ROBUSTNESS-PLAN.md +192 -0
  20. package/docs/R9-OPEN-CORE-HOOKS-PLAN.md +2 -2
  21. package/docs/architecture/STATE-MACHINES.md +18 -19
  22. package/docs/architecture/bmad-loki-voice-agent-council-analysis.md +1 -1
  23. package/docs/auto-claude-comparison.md +16 -13
  24. package/docs/certification/01-core-concepts/lesson.md +12 -11
  25. package/docs/certification/01-core-concepts/quiz.md +6 -6
  26. package/docs/certification/05-troubleshooting/lesson.md +23 -13
  27. package/docs/certification/05-troubleshooting/quiz.md +3 -3
  28. package/docs/certification/README.md +1 -1
  29. package/docs/certification/answer-key.md +2 -2
  30. package/docs/certification/certification-exam.md +9 -9
  31. package/docs/competitive/bolt-new-analysis.md +2 -2
  32. package/docs/competitive/emergence-others-analysis.md +14 -14
  33. package/docs/competitive/replit-lovable-analysis.md +7 -7
  34. package/docs/cursor-comparison.md +15 -12
  35. package/docs/dashboard-guide.md +9 -7
  36. package/docs/enterprise/security.md +43 -3
  37. package/docs/prd-purple-lab-platform-v2.md +1 -1
  38. package/docs/prd-purple-lab-platform.md +3 -3
  39. package/docs/show-hn-post.md +3 -3
  40. package/loki-ts/dist/loki.js +2 -2
  41. package/mcp/__init__.py +1 -1
  42. package/package.json +2 -2
  43. package/plugins/loki-mode/.claude-plugin/plugin.json +2 -2
  44. package/plugins/loki-mode/README.md +1 -1
  45. package/references/magic-rarv-integration.md +1 -1
  46. package/references/quality-control.md +5 -5
  47. package/references/sdlc-phases.md +1 -2
  48. package/skills/00-index.md +1 -1
  49. package/skills/artifacts.md +1 -1
  50. package/skills/healing.md +1 -1
  51. package/skills/magic-modules.md +3 -3
  52. package/skills/quality-gates.md +52 -39
  53. package/skills/testing.md +1 -1
  54. package/web-app/dist/assets/{AdminPage-CKUOsWZW.js → AdminPage-CcCJ0Sjt.js} +1 -1
  55. package/web-app/dist/assets/{Avatar-CL9Id9Hi.js → Avatar-DK8kmayw.js} +1 -1
  56. package/web-app/dist/assets/{Badge-B12zwlD7.js → Badge-4uAWnemi.js} +1 -1
  57. package/web-app/dist/assets/{Button-CFLVoduT.js → Button-BBMk33tk.js} +1 -1
  58. package/web-app/dist/assets/ComparePage-bt9rwvST.js +1 -0
  59. package/web-app/dist/assets/{GitHubIssuesPanel-CSitxtAX.js → GitHubIssuesPanel-WDbH47UM.js} +1 -1
  60. package/web-app/dist/assets/{GitHubPRsPanel-BIT06FRo.js → GitHubPRsPanel-C2CiYtTx.js} +1 -1
  61. package/web-app/dist/assets/{HomePage-pU_0fGny.js → HomePage-BQk-MUjn.js} +4 -4
  62. package/web-app/dist/assets/{LoginPage-DTZtt2Yb.js → LoginPage-DMOZVGGL.js} +1 -1
  63. package/web-app/dist/assets/{MagicPage-10zfra8o.js → MagicPage-Bzp2Nt1z.js} +1 -1
  64. package/web-app/dist/assets/{MetricsPage-C-wiKUkv.js → MetricsPage-C39JVdsw.js} +1 -1
  65. package/web-app/dist/assets/{NotFoundPage-BDkcmhYe.js → NotFoundPage-6vT_U9UL.js} +1 -1
  66. package/web-app/dist/assets/{ProjectPage-CiCavQ8n.js → ProjectPage-BfFcZp-E.js} +3 -3
  67. package/web-app/dist/assets/{ProjectsPage-BLCXQwwC.js → ProjectsPage-CPMBf8Wt.js} +1 -1
  68. package/web-app/dist/assets/{SettingsPage-PkxtaMyg.js → SettingsPage-BnNN6ETl.js} +1 -1
  69. package/web-app/dist/assets/{ShowcasePage-iECp8Tha.js → ShowcasePage-WDrMf-cx.js} +1 -1
  70. package/web-app/dist/assets/{SystemSettingsPage-DS6Anno1.js → SystemSettingsPage-DX4jb2e8.js} +1 -1
  71. package/web-app/dist/assets/{TeamsPage-ls6h6bNL.js → TeamsPage-BCfqcXzu.js} +1 -1
  72. package/web-app/dist/assets/{TemplatesPage-Bk0QzlPt.js → TemplatesPage-CZvmimDj.js} +1 -1
  73. package/web-app/dist/assets/{TerminalOutput-4-1hWCtZ.js → TerminalOutput-BlRqFwWV.js} +1 -1
  74. package/web-app/dist/assets/{activity-DH3ih2nS.js → activity-CacZsUyr.js} +1 -1
  75. package/web-app/dist/assets/{bell-Gn17S6uv.js → bell-DK2qtHnk.js} +1 -1
  76. package/web-app/dist/assets/{bot-Cbycc3VE.js → bot-CkcUtHad.js} +1 -1
  77. package/web-app/dist/assets/{check-nIAqa-kf.js → check-CbCPjX3M.js} +1 -1
  78. package/web-app/dist/assets/{chevron-left-D2jcWDll.js → chevron-left-5NUKWw3i.js} +1 -1
  79. package/web-app/dist/assets/{circle-alert-CpL4Bhvt.js → circle-alert-S7uFoxC2.js} +1 -1
  80. package/web-app/dist/assets/{clock-IW4Wq86N.js → clock-CaQRrIrs.js} +1 -1
  81. package/web-app/dist/assets/{cloud-Cn8nNuH2.js → cloud-DBAX6c0r.js} +1 -1
  82. package/web-app/dist/assets/{code-xml-BiJBteXf.js → code-xml-De5-EXv3.js} +1 -1
  83. package/web-app/dist/assets/{copy-CnqkyNsi.js → copy-CUkT6k1v.js} +1 -1
  84. package/web-app/dist/assets/{database-CKSReqa5.js → database-BAWf1Gwt.js} +1 -1
  85. package/web-app/dist/assets/{dollar-sign-CDzDY64R.js → dollar-sign-Ji8zk86R.js} +1 -1
  86. package/web-app/dist/assets/{file-code-corner-Box4IwG1.js → file-code-corner-ChtXoBwS.js} +1 -1
  87. package/web-app/dist/assets/{file-plus-DpGqlXF8.js → file-plus-bFa37P76.js} +1 -1
  88. package/web-app/dist/assets/{folder-open-B57dAoBv.js → folder-open-DhXpXscO.js} +1 -1
  89. package/web-app/dist/assets/{git-commit-horizontal-BVbucmO5.js → git-commit-horizontal-DVPeDQ3j.js} +1 -1
  90. package/web-app/dist/assets/{globe-BkOnKl4x.js → globe-BPZgPeeu.js} +1 -1
  91. package/web-app/dist/assets/{hammer-DRbIQ4QU.js → hammer-jLCaujYH.js} +1 -1
  92. package/web-app/dist/assets/{index-CM_b_EhP.js → index-B-0iHBPO.js} +2 -2
  93. package/web-app/dist/assets/{layers-B78BiFiU.js → layers-B1vsrsFW.js} +1 -1
  94. package/web-app/dist/assets/{lightbulb-B-Itbm9g.js → lightbulb-C-uLoq9Y.js} +1 -1
  95. package/web-app/dist/assets/{loader-circle-Oq6NQhW2.js → loader-circle-JTfD-ZuM.js} +1 -1
  96. package/web-app/dist/assets/{lock-DbJ9zxbw.js → lock-G9rxD4gZ.js} +1 -1
  97. package/web-app/dist/assets/{mail-CzMRod6m.js → mail-BJ0PTN_V.js} +1 -1
  98. package/web-app/dist/assets/{package-WZ5osvej.js → package-CXClfLOO.js} +1 -1
  99. package/web-app/dist/assets/{plus-j08lFR-K.js → plus-EoL5OCB7.js} +1 -1
  100. package/web-app/dist/assets/{refresh-cw-CIr7E-g2.js → refresh-cw-BjREUnVq.js} +1 -1
  101. package/web-app/dist/assets/{rotate-ccw-gwoXxDeE.js → rotate-ccw-DahWX07H.js} +1 -1
  102. package/web-app/dist/assets/{save-B8fV_ZpE.js → save-Dek3gCn1.js} +1 -1
  103. package/web-app/dist/assets/{server-D5dO1paz.js → server-D6V1BAia.js} +1 -1
  104. package/web-app/dist/assets/{shield-alert-Du08zhdg.js → shield-alert-BtTK5Sxb.js} +1 -1
  105. package/web-app/dist/assets/{trash-2-DEKSVae5.js → trash-2-BT5o_g0r.js} +1 -1
  106. package/web-app/dist/assets/{trending-down-DBiXUtxJ.js → trending-down-D4Jk7KF3.js} +1 -1
  107. package/web-app/dist/assets/{trending-up-BgmK_tHq.js → trending-up-EQFTzhEo.js} +1 -1
  108. package/web-app/dist/assets/{upload-IaViyeVD.js → upload-JfI5lCSE.js} +1 -1
  109. package/web-app/dist/assets/{usePolling-PiRLqNu6.js → usePolling-BnhPUuGd.js} +1 -1
  110. package/web-app/dist/assets/{user-BB5J8wAF.js → user-DSUiUYtj.js} +1 -1
  111. package/web-app/dist/index.html +1 -1
  112. package/web-app/dist/assets/ComparePage-Dg0UdZAk.js +0 -1
package/autonomy/run.sh CHANGED
@@ -2634,6 +2634,27 @@ except Exception:
2634
2634
  fi
2635
2635
  fi
2636
2636
 
2637
+ # P2-2: assumption-ledger summary for proof-of-done. "done" means "done, plus
2638
+ # here are the N places your spec was ambiguous and what Loki assumed."
2639
+ # spec_ledger_counts echoes "<total> <high>"; defined when spec-interrogation.sh
2640
+ # is sourced (run.sh sources it in DISCOVERY). Guarded for safety.
2641
+ local assumptions_total=0 assumptions_high=0 assumption_lines=""
2642
+ if type spec_ledger_counts &>/dev/null; then
2643
+ local _ac
2644
+ _ac="$(spec_ledger_counts 2>/dev/null || echo '0 0')"
2645
+ assumptions_total="${_ac%% *}"
2646
+ assumptions_high="${_ac##* }"
2647
+ case "$assumptions_total" in ''|*[!0-9]*) assumptions_total=0 ;; esac
2648
+ case "$assumptions_high" in ''|*[!0-9]*) assumptions_high=0 ;; esac
2649
+ if [ "$assumptions_total" != "0" ]; then
2650
+ local _ledger_md="$loki_dir/assumptions/ledger.md"
2651
+ if [ -f "$_ledger_md" ]; then
2652
+ # Pull just the "## <id> ..." headings as a terse one-per-line list.
2653
+ assumption_lines="$(grep '^## ' "$_ledger_md" 2>/dev/null | sed 's/^## / - /' || true)"
2654
+ fi
2655
+ fi
2656
+ fi
2657
+
2637
2658
  # ---- Durable human-readable file: .loki/COMPLETION.txt --------------------
2638
2659
  {
2639
2660
  echo "Loki Mode run summary"
@@ -2668,6 +2689,14 @@ except Exception:
2668
2689
  echo "$evidence_inconclusive_line"
2669
2690
  echo ""
2670
2691
  fi
2692
+ if [ "$assumptions_total" != "0" ]; then
2693
+ echo "Spec assumptions recorded: $assumptions_total ($assumptions_high high-severity)"
2694
+ echo " These are places your spec was ambiguous and what Loki assumed. See .loki/assumptions/ledger.md"
2695
+ if [ -n "$assumption_lines" ]; then
2696
+ echo "$assumption_lines"
2697
+ fi
2698
+ echo ""
2699
+ fi
2671
2700
  echo "Review the work:"
2672
2701
  echo " $review_cmd"
2673
2702
  echo ""
@@ -2691,6 +2720,8 @@ except Exception:
2691
2720
  _LOKI_CS_DELEGATE_BRANCH="$delegate_branch" \
2692
2721
  _LOKI_CS_PR_URL="$pr_url" \
2693
2722
  _LOKI_CS_TS="$ts" \
2723
+ _LOKI_CS_ASSUMPTIONS_TOTAL="$assumptions_total" \
2724
+ _LOKI_CS_ASSUMPTIONS_HIGH="$assumptions_high" \
2694
2725
  _LOKI_CS_OUT_FILE="$loki_dir/state/completion.json" \
2695
2726
  python3 -c "
2696
2727
  import json, os, tempfile
@@ -2710,6 +2741,8 @@ rec = {
2710
2741
  'delegate_branch': os.environ.get('_LOKI_CS_DELEGATE_BRANCH', ''),
2711
2742
  'pr_url': os.environ.get('_LOKI_CS_PR_URL', ''),
2712
2743
  'timestamp': os.environ.get('_LOKI_CS_TS', ''),
2744
+ 'assumptions_total': i(os.environ.get('_LOKI_CS_ASSUMPTIONS_TOTAL')),
2745
+ 'assumptions_high': i(os.environ.get('_LOKI_CS_ASSUMPTIONS_HIGH')),
2713
2746
  }
2714
2747
  d = os.path.dirname(out)
2715
2748
  fd, tmp = tempfile.mkstemp(dir=d, suffix='.json')
@@ -7035,7 +7068,6 @@ enforce_test_coverage() {
7035
7068
 
7036
7069
  local min_coverage="${LOKI_MIN_COVERAGE:-80}"
7037
7070
  local test_passed=true
7038
- local coverage_pct=0
7039
7071
  local test_runner="none"
7040
7072
  local details=""
7041
7073
 
@@ -7262,12 +7294,12 @@ TREOF
7262
7294
  if [ "$test_passed" = "true" ]; then
7263
7295
  touch "$quality_dir/unit-tests.pass"
7264
7296
  rm -f "$loki_dir/signals/TESTS_FAILED" 2>/dev/null || true
7265
- log_info "Test coverage gate: $test_runner passed"
7297
+ log_info "Test suite gate: $test_runner passed"
7266
7298
  return 0
7267
7299
  else
7268
7300
  rm -f "$quality_dir/unit-tests.pass"
7269
7301
  echo "tests_failed" > "$loki_dir/signals/TESTS_FAILED" 2>/dev/null || true
7270
- log_warn "Test coverage gate: $test_runner FAILED"
7302
+ log_warn "Test suite gate: $test_runner FAILED"
7271
7303
  return 1
7272
7304
  fi
7273
7305
  }
@@ -7380,7 +7412,7 @@ run_doc_staleness_check() {
7380
7412
  }
7381
7413
 
7382
7414
  # ============================================================================
7383
- # Documentation Quality Gate - Gate 11 (v6.75.0)
7415
+ # Documentation Quality Gate - Gate 7 (Documentation Coverage)
7384
7416
  # Checks README, documentation freshness, and package API docs
7385
7417
  # ============================================================================
7386
7418
 
@@ -7533,6 +7565,139 @@ run_magic_debate_gate() {
7533
7565
  return 0
7534
7566
  }
7535
7567
 
7568
+ # ============================================================================
7569
+ # Mock Integrity Gate (P0-3): wire tests/detect-mock-problems.sh as a blocking
7570
+ # gate. The detector scans test files for mock patterns that mask real failures
7571
+ # (tautological assertions, inline-mock-only tests, conditional/empty bodies,
7572
+ # high internal-mock ratios). Invoked with --strict so it exits 1 iff CRITICAL
7573
+ # or HIGH findings exist; MED/LOW never block (they are routed to a findings
7574
+ # file for next-iteration injection). Opt out with LOKI_GATE_MOCK=false.
7575
+ #
7576
+ # Scan-target note: the wrapper exports LOKI_SCAN_DIR=TARGET_DIR at the detector
7577
+ # invocation, and the detector honors it (tests/detect-mock-problems.sh:23), so
7578
+ # the gate scans the target project, not the loki-mode tree. When LOKI_SCAN_DIR
7579
+ # is unset the detector falls back to its own repo (the default for loki-mode's
7580
+ # own test run); the wrapper always sets it, so the target is what gets scanned.
7581
+ # ============================================================================
7582
+ enforce_mock_integrity() {
7583
+ local loki_dir="${TARGET_DIR:-.}/.loki"
7584
+ local quality_dir="$loki_dir/quality"
7585
+ mkdir -p "$quality_dir"
7586
+ local findings_file="$quality_dir/mock-findings.txt"
7587
+ local detector="$SCRIPT_DIR/../tests/detect-mock-problems.sh"
7588
+ local gate_timeout="${LOKI_GATE_TIMEOUT:-300}"
7589
+
7590
+ if [ ! -f "$detector" ]; then
7591
+ log_info "Mock integrity gate: detector not found, skipping (inconclusive)"
7592
+ rm -f "$findings_file" 2>/dev/null || true
7593
+ return 0
7594
+ fi
7595
+
7596
+ local output rc
7597
+ output=$(cd "${TARGET_DIR:-.}" && LOKI_SCAN_DIR="${TARGET_DIR:-.}" \
7598
+ timeout "$gate_timeout" bash "$detector" --strict 2>&1)
7599
+ rc=$?
7600
+
7601
+ # timeout exit 124 -- treat as inconclusive (do not block on a hang)
7602
+ if [ "$rc" -eq 124 ]; then
7603
+ log_warn "Mock integrity gate: detector timed out after ${gate_timeout}s -- inconclusive"
7604
+ rm -f "$findings_file" 2>/dev/null || true
7605
+ return 0
7606
+ fi
7607
+
7608
+ if [ "$rc" -ne 0 ]; then
7609
+ # --strict exits 1 iff CRITICAL or HIGH found. Persist per-finding text.
7610
+ {
7611
+ echo "# Mock integrity findings (CRITICAL/HIGH block this iteration)"
7612
+ echo "$output" | grep -E '\[(CRITICAL|HIGH|MEDIUM|LOW)\]' || true
7613
+ } > "$findings_file"
7614
+ log_warn "Mock integrity gate: CRITICAL/HIGH mock problems detected -- BLOCK"
7615
+ return 1
7616
+ fi
7617
+
7618
+ # Pass: record any MED/LOW findings for injection, then clear the block file.
7619
+ local med_low
7620
+ med_low=$(echo "$output" | grep -E '\[(MEDIUM|LOW)\]' || true)
7621
+ if [ -n "$med_low" ]; then
7622
+ {
7623
+ echo "# Mock integrity advisory findings (MED/LOW, non-blocking)"
7624
+ echo "$med_low"
7625
+ } > "$findings_file"
7626
+ else
7627
+ rm -f "$findings_file" 2>/dev/null || true
7628
+ fi
7629
+ log_info "Mock integrity gate: PASS"
7630
+ return 0
7631
+ }
7632
+
7633
+ # ============================================================================
7634
+ # Test Mutation Integrity Gate (P0-3): wire tests/detect-test-mutations.sh as a
7635
+ # blocking gate. The detector flags assertion-value mutations that look like
7636
+ # test-fitting (tests changed to match buggy output). We do NOT pass --strict:
7637
+ # --strict blocks on ANY finding (over-blocks on MED/LOW). Instead we parse
7638
+ # stdout and block only when a [HIGH] line is present; MED/LOW are routed to a
7639
+ # findings file for next-iteration injection. Opt out with LOKI_GATE_MUTATION=false.
7640
+ #
7641
+ # Scan-target note: same as the mock gate -- the wrapper exports
7642
+ # LOKI_SCAN_DIR=TARGET_DIR and the detector honors it
7643
+ # (tests/detect-test-mutations.sh:33), so the gate scans the target project, not
7644
+ # the loki-mode tree. The Check-5 git history is also read from that directory.
7645
+ # ============================================================================
7646
+ enforce_mutation_integrity() {
7647
+ local loki_dir="${TARGET_DIR:-.}/.loki"
7648
+ local quality_dir="$loki_dir/quality"
7649
+ mkdir -p "$quality_dir"
7650
+ local findings_file="$quality_dir/mutation-findings.txt"
7651
+ local detector="$SCRIPT_DIR/../tests/detect-test-mutations.sh"
7652
+ local gate_timeout="${LOKI_GATE_TIMEOUT:-300}"
7653
+
7654
+ if [ ! -f "$detector" ]; then
7655
+ log_info "Mutation integrity gate: detector not found, skipping (inconclusive)"
7656
+ rm -f "$findings_file" 2>/dev/null || true
7657
+ return 0
7658
+ fi
7659
+
7660
+ local output rc
7661
+ # No --strict: it over-blocks on MED/LOW. Decide on [HIGH] lines instead.
7662
+ output=$(cd "${TARGET_DIR:-.}" && LOKI_SCAN_DIR="${TARGET_DIR:-.}" \
7663
+ timeout "$gate_timeout" bash "$detector" 2>&1)
7664
+ rc=$?
7665
+
7666
+ if [ "$rc" -eq 124 ]; then
7667
+ log_warn "Mutation integrity gate: detector timed out after ${gate_timeout}s -- inconclusive"
7668
+ rm -f "$findings_file" 2>/dev/null || true
7669
+ return 0
7670
+ fi
7671
+
7672
+ local high_count
7673
+ high_count=$(echo "$output" | grep -c '\[HIGH\]' || true)
7674
+ # grep -c returns 0 with no matches but may print empty under set -e edge; normalize.
7675
+ [ -z "$high_count" ] && high_count=0
7676
+
7677
+ if [ "$high_count" -gt 0 ]; then
7678
+ {
7679
+ echo "# Test mutation findings (HIGH blocks this iteration)"
7680
+ echo "$output" | grep -E '\[(HIGH|MEDIUM|MED|LOW)\]' || true
7681
+ } > "$findings_file"
7682
+ log_warn "Mutation integrity gate: $high_count HIGH test-fitting finding(s) -- BLOCK"
7683
+ return 1
7684
+ fi
7685
+
7686
+ # Pass: route any MED/LOW findings to injection file, else clear it.
7687
+ local med_low
7688
+ med_low=$(echo "$output" | grep -E '\[(MEDIUM|MED|LOW)\]' || true)
7689
+ if [ -n "$med_low" ]; then
7690
+ {
7691
+ echo "# Test mutation advisory findings (MED/LOW, non-blocking)"
7692
+ echo "$med_low"
7693
+ } > "$findings_file"
7694
+ else
7695
+ rm -f "$findings_file" 2>/dev/null || true
7696
+ fi
7697
+ log_info "Mutation integrity gate: PASS"
7698
+ return 0
7699
+ }
7700
+
7536
7701
  # ============================================================================
7537
7702
  # 3-Reviewer Parallel Code Review (v5.35.0)
7538
7703
  # Specialist pool from skills/quality-gates.md with blind review
@@ -7785,6 +7950,97 @@ MANAGED_REVIEW
7785
7950
  return 0
7786
7951
  }
7787
7952
 
7953
+ # _dispatch_reviewer: single-reviewer provider invocation, factored out of
7954
+ # run_code_review so the blind-council loop AND the Devil's-Advocate re-review
7955
+ # (P0-4) share ONE dispatch path. This preserves the load-bearing claude trust
7956
+ # guards (no --model/Fable routing, --bare, --disallowedTools, caveman OFF) for
7957
+ # both callers; a hand-written parallel dispatcher would drift from them.
7958
+ # Args: $1 = prompt text, $2 = output file path. Writes the model reply to $2.
7959
+ _dispatch_reviewer() {
7960
+ local prompt_text="$1"
7961
+ local review_output="$2"
7962
+ case "${PROVIDER_NAME:-claude}" in
7963
+ claude)
7964
+ # SECURITY-REVIEW MODEL GUARD (evidence-based routing, item 4b):
7965
+ # Reviewers deliberately do NOT pass --model, so they run on
7966
+ # the account default model and are NEVER routed to Fable by a
7967
+ # mid-flight model override or LOKI_FABLE_ARCHITECT (those only
7968
+ # rewrite the iteration's tier_param, not this dispatch). This
7969
+ # must stay true. The official model-config docs CONTRADICT
7970
+ # routing security review to Fable: Fable's safety classifiers
7971
+ # refuse cybersecurity content, and in non-interactive (-p)
7972
+ # mode a flagged request ends the turn with stop_reason
7973
+ # "refusal" instead of a transparent Opus re-run. A refused
7974
+ # security reviewer would return no VERDICT and break the
7975
+ # unanimous-council gate. Defensive-cyber capability lives in
7976
+ # Mythos 5 (Project Glasswing), not Fable. If a future change
7977
+ # adds --model here, the security-sentinel reviewer must be
7978
+ # pinned to opus, never fable.
7979
+ # EMBED 2 + 3 (v7.33.0). This is a trust-gate council subcall.
7980
+ # $prompt_text is fully self-contained (the diff, changed files,
7981
+ # checks, and strict VERDICT/FINDINGS output format), output is
7982
+ # captured to $review_output, and it deliberately does NOT pass
7983
+ # --model or go through buildAutoFlags. So:
7984
+ # EMBED 2 (--bare): the prompt needs no hooks/LSP/CLAUDE.md/
7985
+ # MCP discovery, so --bare is safe and cheaper. Opt out
7986
+ # LOKI_BARE_SUBCALLS=0.
7987
+ # EMBED 3 (--disallowedTools): raise the cost of a reviewer
7988
+ # casually mutating the tree (a parallel agent once ran
7989
+ # `git reset --hard` and wiped uncommitted work). Deny
7990
+ # Edit/Write/NotebookEdit + git mutation forms (incl. the
7991
+ # git -C / --git-dir evasions); read-only git stays allowed.
7992
+ # Guardrail, not a sandbox -- echo>/sed -i/etc. remain; the
7993
+ # real net is commit-before-agent-wave. Opt out
7994
+ # LOKI_REVIEW_TOOL_GUARD=0. See loki_review_guard_denylist.
7995
+ local _rv_argv=("--dangerously-skip-permissions")
7996
+ if type loki_subcall_bare_enabled >/dev/null 2>&1 && loki_subcall_bare_enabled; then
7997
+ _rv_argv+=("--bare")
7998
+ fi
7999
+ if type loki_review_guard_enabled >/dev/null 2>&1 && loki_review_guard_enabled; then
8000
+ _rv_argv+=("--disallowedTools" "$(loki_review_guard_denylist)")
8001
+ fi
8002
+ # EMBED 3b (--allowedTools, #167): positive least-privilege
8003
+ # allowlist. DEFAULT OFF (opt-in LOKI_REVIEW_ALLOWLIST=1).
8004
+ # Emitted ALONGSIDE the denylist: verified live (claude
8005
+ # 2.1.177) that deny precedence holds even under
8006
+ # --dangerously-skip-permissions, so the denylist still
8007
+ # hard-blocks mutations while this narrows the surface to
8008
+ # read/inspect tools. See loki_review_allowlist.
8009
+ if type loki_review_allowlist_enabled >/dev/null 2>&1 && loki_review_allowlist_enabled; then
8010
+ _rv_argv+=("--allowedTools" "$(loki_review_allowlist)")
8011
+ fi
8012
+ # caveman HARD-SUPPRESS (parsed output): this is a trust-gate
8013
+ # subcall whose output is parsed for "^VERDICT:" + findings. A
8014
+ # globally-active caveman would compress/reword that line and
8015
+ # silently flip the verdict, so we UNCONDITIONALLY disable
8016
+ # caveman here with CAVEMAN_DEFAULT_MODE=off (the activate hook
8017
+ # then deletes its flag and emits nothing). Set inline, not via
8018
+ # the helper, so the carve-out holds even when the helper is
8019
+ # out of scope. No-op when caveman is absent.
8020
+ CAVEMAN_DEFAULT_MODE=off \
8021
+ claude "${_rv_argv[@]}" -p "$prompt_text" \
8022
+ --output-format text > "$review_output" 2>/dev/null
8023
+ ;;
8024
+ codex)
8025
+ codex exec --full-auto --skip-git-repo-check "$prompt_text" \
8026
+ > "$review_output" 2>/dev/null
8027
+ ;;
8028
+ cline)
8029
+ invoke_cline_capture "$prompt_text" \
8030
+ > "$review_output" 2>/dev/null
8031
+ ;;
8032
+ aider)
8033
+ invoke_aider_capture "$prompt_text" \
8034
+ > "$review_output" 2>/dev/null
8035
+ ;;
8036
+ *)
8037
+ echo "VERDICT: PASS" > "$review_output"
8038
+ echo "FINDINGS:" >> "$review_output"
8039
+ echo "- [Low] Unknown provider, review skipped" >> "$review_output"
8040
+ ;;
8041
+ esac
8042
+ }
8043
+
7788
8044
  run_code_review() {
7789
8045
  local loki_dir="${TARGET_DIR:-.}/.loki"
7790
8046
  local review_dir="$loki_dir/quality/reviews"
@@ -7873,7 +8129,7 @@ MANAGED_SELECTION
7873
8129
  # Select specialists via keyword scoring (python3 reads files, not env vars)
7874
8130
  # Loads from agents/types.json when available, falls back to hardcoded pool (v6.7.0)
7875
8131
  # v7.4.20: gate legacy-healing-auditor on healing-mode signals to match
7876
- # the documented contract in skills/quality-gates.md (Gate 10).
8132
+ # the documented contract in skills/quality-gates.md (conditional backward-compat auditor, not one of the 8 numbered gates).
7877
8133
  local healing_active="false"
7878
8134
  if [ "${LOKI_HEAL_MODE:-}" = "true" ] || [ "${LOKI_HEAL_MODE:-}" = "1" ]; then
7879
8135
  healing_active="true"
@@ -7969,7 +8225,7 @@ if files_path and os.path.exists(files_path):
7969
8225
  search_text = diff_text + " " + files_text
7970
8226
 
7971
8227
  # v7.4.20: gate legacy-healing-auditor on healing-mode signals to match
7972
- # skills/quality-gates.md (Gate 10) which documents it as conditional. The
8228
+ # skills/quality-gates.md (conditional backward-compat auditor, not one of the 8 numbered gates) which documents it as conditional. The
7973
8229
  # auditor BLOCKs on missing characterization tests / missing adapters, which
7974
8230
  # is a contract a greenfield project never agreed to maintain. agentbudget
7975
8231
  # regression: the auditor pinned 9 of 10 iterations to forced PAUSE because
@@ -8097,91 +8353,11 @@ BUILD_PROMPT
8097
8353
 
8098
8354
  log_step "Dispatching reviewer: $reviewer_name"
8099
8355
 
8100
- # Launch blind review in background (provider-specific)
8356
+ # Launch blind review in background (shared dispatch helper).
8101
8357
  (
8102
8358
  local prompt_text
8103
8359
  prompt_text=$(cat "$review_prompt_file")
8104
- case "${PROVIDER_NAME:-claude}" in
8105
- claude)
8106
- # SECURITY-REVIEW MODEL GUARD (evidence-based routing, item 4b):
8107
- # Reviewers deliberately do NOT pass --model, so they run on
8108
- # the account default model and are NEVER routed to Fable by a
8109
- # mid-flight model override or LOKI_FABLE_ARCHITECT (those only
8110
- # rewrite the iteration's tier_param, not this dispatch). This
8111
- # must stay true. The official model-config docs CONTRADICT
8112
- # routing security review to Fable: Fable's safety classifiers
8113
- # refuse cybersecurity content, and in non-interactive (-p)
8114
- # mode a flagged request ends the turn with stop_reason
8115
- # "refusal" instead of a transparent Opus re-run. A refused
8116
- # security reviewer would return no VERDICT and break the
8117
- # unanimous-council gate. Defensive-cyber capability lives in
8118
- # Mythos 5 (Project Glasswing), not Fable. If a future change
8119
- # adds --model here, the security-sentinel reviewer must be
8120
- # pinned to opus, never fable.
8121
- # EMBED 2 + 3 (v7.33.0). This is a 3-reviewer council
8122
- # subcall. $prompt_text is fully self-contained (built above
8123
- # into $review_prompt_file with the diff, changed files,
8124
- # checks, and strict VERDICT/FINDINGS output format), output
8125
- # is captured to $review_output, and it deliberately does NOT
8126
- # pass --model or go through buildAutoFlags. So:
8127
- # EMBED 2 (--bare): the prompt needs no hooks/LSP/CLAUDE.md/
8128
- # MCP discovery, so --bare is safe and cheaper. Opt out
8129
- # LOKI_BARE_SUBCALLS=0.
8130
- # EMBED 3 (--disallowedTools): raise the cost of a reviewer
8131
- # casually mutating the tree (a parallel agent once ran
8132
- # `git reset --hard` and wiped uncommitted work). Deny
8133
- # Edit/Write/NotebookEdit + git mutation forms (incl. the
8134
- # git -C / --git-dir evasions); read-only git stays allowed.
8135
- # Guardrail, not a sandbox -- echo>/sed -i/etc. remain; the
8136
- # real net is commit-before-agent-wave. Opt out
8137
- # LOKI_REVIEW_TOOL_GUARD=0. See loki_review_guard_denylist.
8138
- local _rv_argv=("--dangerously-skip-permissions")
8139
- if type loki_subcall_bare_enabled >/dev/null 2>&1 && loki_subcall_bare_enabled; then
8140
- _rv_argv+=("--bare")
8141
- fi
8142
- if type loki_review_guard_enabled >/dev/null 2>&1 && loki_review_guard_enabled; then
8143
- _rv_argv+=("--disallowedTools" "$(loki_review_guard_denylist)")
8144
- fi
8145
- # EMBED 3b (--allowedTools, #167): positive least-privilege
8146
- # allowlist. DEFAULT OFF (opt-in LOKI_REVIEW_ALLOWLIST=1).
8147
- # Emitted ALONGSIDE the denylist: verified live (claude
8148
- # 2.1.177) that deny precedence holds even under
8149
- # --dangerously-skip-permissions, so the denylist still
8150
- # hard-blocks mutations while this narrows the surface to
8151
- # read/inspect tools. See loki_review_allowlist.
8152
- if type loki_review_allowlist_enabled >/dev/null 2>&1 && loki_review_allowlist_enabled; then
8153
- _rv_argv+=("--allowedTools" "$(loki_review_allowlist)")
8154
- fi
8155
- # caveman HARD-SUPPRESS (parsed output): this is a trust-gate
8156
- # subcall whose output is parsed for "^VERDICT:" + findings. A
8157
- # globally-active caveman would compress/reword that line and
8158
- # silently flip the verdict, so we UNCONDITIONALLY disable
8159
- # caveman here with CAVEMAN_DEFAULT_MODE=off (the activate hook
8160
- # then deletes its flag and emits nothing). Set inline, not via
8161
- # the helper, so the carve-out holds even when the helper is
8162
- # out of scope. No-op when caveman is absent.
8163
- CAVEMAN_DEFAULT_MODE=off \
8164
- claude "${_rv_argv[@]}" -p "$prompt_text" \
8165
- --output-format text > "$review_output" 2>/dev/null
8166
- ;;
8167
- codex)
8168
- codex exec --full-auto --skip-git-repo-check "$prompt_text" \
8169
- > "$review_output" 2>/dev/null
8170
- ;;
8171
- cline)
8172
- invoke_cline_capture "$prompt_text" \
8173
- > "$review_output" 2>/dev/null
8174
- ;;
8175
- aider)
8176
- invoke_aider_capture "$prompt_text" \
8177
- > "$review_output" 2>/dev/null
8178
- ;;
8179
- *)
8180
- echo "VERDICT: PASS" > "$review_output"
8181
- echo "FINDINGS:" >> "$review_output"
8182
- echo "- [Low] Unknown provider, review skipped" >> "$review_output"
8183
- ;;
8184
- esac
8360
+ _dispatch_reviewer "$prompt_text" "$review_output"
8185
8361
  ) &
8186
8362
  pids+=($!)
8187
8363
  register_pid "$!" "code-reviewer" "name=$reviewer_name"
@@ -8314,12 +8490,108 @@ AGG_SCRIPT
8314
8490
  "iteration=$ITERATION_COUNT"
8315
8491
 
8316
8492
  # Anti-sycophancy check: unanimous PASS is suspicious
8317
- if [ "$pass_count" -eq "$reviewer_count" ] && [ "$fail_count" -eq 0 ]; then
8493
+ if [ "$pass_count" -eq "$reviewer_count" ] && [ "$fail_count" -eq 0 ] && [ "$reviewer_count" -gt 0 ]; then
8318
8494
  log_warn "ANTI-SYCOPHANCY: All $reviewer_count reviewers passed unanimously"
8319
8495
  log_warn "Devil's advocate note: Unanimous approval may indicate insufficient scrutiny"
8320
8496
  log_warn "Consider manual review of $review_dir/$review_id/"
8321
8497
  echo "UNANIMOUS_PASS: All reviewers approved - potential sycophancy risk" \
8322
8498
  >> "$review_dir/$review_id/anti-sycophancy.txt"
8499
+
8500
+ # P0-4: Devil's-Advocate re-review. The bare warning above was INERT --
8501
+ # it never changed the verdict. Now, on unanimous PASS, dispatch ONE
8502
+ # additional adversarial reviewer (reusing _dispatch_reviewer so the
8503
+ # same trust guards + provider routing apply) whose sole job is to find
8504
+ # a Critical/High issue the unanimous council missed. If it does, we set
8505
+ # has_blocking=true so the EXISTING blocking decision below fires and the
8506
+ # gate returns 1. Runs in the FOREGROUND (no &) so has_blocking mutates
8507
+ # this (parent) shell, not a subshell. Opt out LOKI_GATE_DEVILS_ADVOCATE=false.
8508
+ if [ "${LOKI_GATE_DEVILS_ADVOCATE:-true}" = "true" ]; then
8509
+ log_info "Devil's Advocate: re-reviewing unanimous PASS for missed Critical/High issues..."
8510
+ local da_output="$review_dir/$review_id/devils-advocate.txt"
8511
+ local da_prompt_file="$review_dir/$review_id/devils-advocate-prompt.txt"
8512
+ export LOKI_DA_PROMPT_DIFF_FILE="$diff_file"
8513
+ export LOKI_DA_PROMPT_FILES_FILE="$files_file"
8514
+ export LOKI_DA_PROMPT_OUT="$da_prompt_file"
8515
+ python3 << 'BUILD_DA_PROMPT'
8516
+ import os
8517
+
8518
+ with open(os.environ["LOKI_DA_PROMPT_FILES_FILE"], "r") as f:
8519
+ files = f.read().strip()
8520
+ with open(os.environ["LOKI_DA_PROMPT_DIFF_FILE"], "r") as f:
8521
+ diff = f.read().strip()
8522
+
8523
+ prompt = f"""You are a Devil's Advocate reviewer. Three independent reviewers ALL approved this change. Unanimous approval is a red flag for insufficient scrutiny. Your SOLE job is to find a Critical or High severity issue they missed.
8524
+
8525
+ Be adversarial and concrete. Hunt for: security holes, data loss, race conditions, broken error handling, silent failures, off-by-one and boundary bugs, resource leaks, injection, and logic that does not match intent. Do NOT rubber-stamp. If after genuine effort you find no Critical/High issue, say so honestly -- do not invent one.
8526
+
8527
+ Files changed:
8528
+ {files}
8529
+
8530
+ Diff:
8531
+ {diff}
8532
+
8533
+ Output format (STRICT - follow exactly):
8534
+ VERDICT: PASS or FAIL
8535
+ FINDINGS:
8536
+ - [severity] description (file:line)
8537
+ Severity levels: Critical, High, Medium, Low
8538
+
8539
+ Output VERDICT: FAIL only if you found a real Critical or High issue. Otherwise output VERDICT: PASS."""
8540
+
8541
+ with open(os.environ["LOKI_DA_PROMPT_OUT"], "w") as f:
8542
+ f.write(prompt)
8543
+ BUILD_DA_PROMPT
8544
+ unset LOKI_DA_PROMPT_DIFF_FILE LOKI_DA_PROMPT_FILES_FILE LOKI_DA_PROMPT_OUT
8545
+
8546
+ local da_prompt_text
8547
+ da_prompt_text=$(cat "$da_prompt_file")
8548
+ # Foreground (no &) so a Critical/High finding can set has_blocking
8549
+ # in THIS shell. || true so a non-zero CLI exit under set -e does not
8550
+ # abort the gate; a missing/empty reply is treated as no finding.
8551
+ _dispatch_reviewer "$da_prompt_text" "$da_output" || true
8552
+
8553
+ if [ -f "$da_output" ] && [ -s "$da_output" ]; then
8554
+ local da_verdict
8555
+ da_verdict=$(grep -i "^VERDICT:" "$da_output" | head -1 | sed 's/^VERDICT:[[:space:]]*//' | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')
8556
+ if [ "$da_verdict" = "FAIL" ] && grep -qiE "\[(Critical|High)\]" "$da_output"; then
8557
+ has_blocking=true
8558
+ # Audit accuracy: aggregate.json was written above (line ~8429)
8559
+ # with has_blocking=false (entering this block requires a
8560
+ # unanimous PASS, so the field was necessarily false). The DA
8561
+ # only ever raises it false->true, so patch the persisted
8562
+ # record to reflect the final outcome. Targeted field update
8563
+ # (not a re-move of the write) keeps every other reader of
8564
+ # aggregate.json undisturbed.
8565
+ export LOKI_DA_AGG_FILE="$review_dir/$review_id/aggregate.json"
8566
+ python3 << 'DA_AGG_PATCH' || true
8567
+ import json, os
8568
+ agg_file = os.environ["LOKI_DA_AGG_FILE"]
8569
+ try:
8570
+ with open(agg_file) as f:
8571
+ data = json.load(f)
8572
+ data["has_blocking"] = True
8573
+ with open(agg_file, "w") as f:
8574
+ json.dump(data, f, indent=2)
8575
+ except (OSError, ValueError):
8576
+ pass
8577
+ DA_AGG_PATCH
8578
+ unset LOKI_DA_AGG_FILE
8579
+ log_error "DEVIL'S ADVOCATE: found Critical/High issue the unanimous council missed -- BLOCK"
8580
+ {
8581
+ echo "DEVILS_ADVOCATE_BLOCK: Critical/High found after unanimous PASS"
8582
+ grep -iE "\[(Critical|High)\]" "$da_output" || true
8583
+ } >> "$review_dir/$review_id/anti-sycophancy.txt"
8584
+ else
8585
+ log_info "Devil's Advocate: no additional Critical/High issues found"
8586
+ echo "DEVILS_ADVOCATE_PASS: no Critical/High beyond unanimous council" \
8587
+ >> "$review_dir/$review_id/anti-sycophancy.txt"
8588
+ fi
8589
+ else
8590
+ log_warn "Devil's Advocate: no usable output (treating as no finding)"
8591
+ echo "DEVILS_ADVOCATE_NO_OUTPUT: reviewer produced no usable reply" \
8592
+ >> "$review_dir/$review_id/anti-sycophancy.txt"
8593
+ fi
8594
+ fi
8323
8595
  fi
8324
8596
 
8325
8597
  # Blocking decision
@@ -11342,6 +11614,17 @@ build_prompt() {
11342
11614
  gate_failure_context="${gate_failure_context}FIX THESE ISSUES BEFORE PROCEEDING WITH NEW WORK."
11343
11615
  fi
11344
11616
 
11617
+ # P2-2: high-severity spec-assumption context. When DISCOVERY recorded any
11618
+ # high-severity assumption (the spec was ambiguous in a high-impact place),
11619
+ # surface it to the build agent so it implements with the gap in view (or
11620
+ # fixes the spec) instead of obliviously coding past it. spec_ledger_prompt_block
11621
+ # is defined when spec-interrogation.sh is sourced (run.sh sources it in
11622
+ # DISCOVERY); guarded so build_prompt is safe when the module is absent.
11623
+ local assumption_context=""
11624
+ if type spec_ledger_prompt_block &>/dev/null; then
11625
+ assumption_context="$(spec_ledger_prompt_block 2>/dev/null || true)"
11626
+ fi
11627
+
11345
11628
  # Human directive injection (from HUMAN_INPUT.md)
11346
11629
  # NOTE: Do NOT unset LOKI_HUMAN_INPUT here - build_prompt runs in a subshell
11347
11630
  # (command substitution) so unset would not affect the parent shell.
@@ -11594,15 +11877,15 @@ except Exception:
11594
11877
  else
11595
11878
  if [ $retry -eq 0 ]; then
11596
11879
  if [ -n "$prd" ]; then
11597
- echo "Loki Mode with PRD at $prd. $update_instruction $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11880
+ echo "Loki Mode with PRD at $prd. $update_instruction $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11598
11881
  else
11599
- echo "Loki Mode. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11882
+ echo "Loki Mode. $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11600
11883
  fi
11601
11884
  else
11602
11885
  if [ -n "$prd" ]; then
11603
- echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11886
+ echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11604
11887
  else
11605
- echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11888
+ echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11606
11889
  fi
11607
11890
  fi
11608
11891
  fi
@@ -11707,6 +11990,7 @@ except Exception:
11707
11990
  fi
11708
11991
  [ -n "$human_directive" ] && printf '%s\n' "$human_directive"
11709
11992
  [ -n "$gate_failure_context" ] && printf '%s\n' "$gate_failure_context"
11993
+ [ -n "$assumption_context" ] && printf '%s\n' "$assumption_context"
11710
11994
  [ -n "$queue_tasks" ] && printf '%s\n' "$queue_tasks"
11711
11995
  [ -n "$bmad_context" ] && printf '%s\n' "$bmad_context"
11712
11996
  [ -n "$openspec_context" ] && printf '%s\n' "$openspec_context"
@@ -12827,6 +13111,22 @@ except Exception:
12827
13111
  fi
12828
13112
  fi
12829
13113
 
13114
+ # P2-1: Spec interrogation (DISCOVERY phase, BEFORE iteration 1 begins
13115
+ # coding). Auto-detects spec ambiguities/contradictions/underspecification
13116
+ # via the Devil's-Advocate grill + prd-analyzer, classifies them with a
13117
+ # deterministic severity, and records every gap as a first-class assumption
13118
+ # under .loki/assumptions/. Default-on; LOKI_SPEC_GRILL=0 opts out.
13119
+ # Provider-aware and degrades cleanly (no provider -> prd-analyzer
13120
+ # assumptions only, no fabricated questions). Best-effort: never blocks the
13121
+ # run. The completion-side teeth are council_assumption_ledger_gate.
13122
+ if [ -f "${SCRIPT_DIR}/spec-interrogation.sh" ]; then
13123
+ # shellcheck disable=SC1090
13124
+ . "${SCRIPT_DIR}/spec-interrogation.sh" 2>/dev/null || true
13125
+ if type spec_interrogation_run &>/dev/null; then
13126
+ spec_interrogation_run "$prd_path" || true
13127
+ fi
13128
+ fi
13129
+
12830
13130
  # Auto-derive completion promise from PRD (v6.10.0)
12831
13131
  # When PRD exists but no explicit promise, auto-derive one and switch to checkpoint mode
12832
13132
  if [ -n "$prd_path" ] && [ -f "$prd_path" ] && [ -z "$COMPLETION_PROMISE" ]; then
@@ -12988,6 +13288,18 @@ except Exception as exc:
12988
13288
  local prompt
12989
13289
  prompt=$(build_prompt "$retry" "$prd_path" "$ITERATION_COUNT")
12990
13290
 
13291
+ # P2-2 auto-acknowledgment lifecycle: build_prompt just injected the
13292
+ # high-severity spec assumptions into the prompt (assumption_context), so
13293
+ # the agent has now SEEN them. Mark them acknowledged so the completion
13294
+ # gate is not a permanent dead-end in autonomous (non-TTY) mode where no
13295
+ # human can ever set confirmed=true. This is the opposite of silent
13296
+ # autocorrect: the gap was recorded, prompt-injected, and is surfaced in
13297
+ # proof-of-done. LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1 disables auto-ack so
13298
+ # only a human confirmation clears the block (the helper checks the knob).
13299
+ if type spec_ledger_acknowledge_all &>/dev/null; then
13300
+ spec_ledger_acknowledge_all 2>/dev/null || true
13301
+ fi
13302
+
12991
13303
  # BUG #5 fix: Clear LOKI_HUMAN_INPUT in the parent shell after build_prompt
12992
13304
  # consumed it. build_prompt runs in a subshell (command substitution), so
12993
13305
  # any unset inside it does not affect the parent. Clear here to prevent
@@ -13963,14 +14275,14 @@ if __name__ == "__main__":
13963
14275
  fi
13964
14276
  # Test coverage gate
13965
14277
  if [ "${PHASE_UNIT_TESTS:-true}" = "true" ]; then
13966
- log_info "Quality gate: test coverage..."
14278
+ log_info "Quality gate: test suite (pass/fail)..."
13967
14279
  if enforce_test_coverage; then
13968
14280
  clear_gate_failure "test_coverage"
13969
14281
  else
13970
14282
  local tc_count
13971
14283
  tc_count=$(track_gate_failure "test_coverage")
13972
14284
  gate_failures="${gate_failures}test_coverage,"
13973
- log_warn "Test coverage gate FAILED ($tc_count consecutive) - must pass next iteration"
14285
+ log_warn "Test suite gate FAILED ($tc_count consecutive) - must pass next iteration"
13974
14286
  fi
13975
14287
  fi
13976
14288
  # BUG-ST-002: Check pause signal between quality gates (after test coverage)
@@ -13981,6 +14293,30 @@ if __name__ == "__main__":
13981
14293
  fi
13982
14294
  continue
13983
14295
  fi
14296
+ # Mock integrity gate (P0-3): block on CRITICAL/HIGH mock problems.
14297
+ if [ "${LOKI_GATE_MOCK:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
14298
+ log_info "Quality gate: mock integrity..."
14299
+ if enforce_mock_integrity; then
14300
+ clear_gate_failure "mock_integrity"
14301
+ else
14302
+ local mk_count
14303
+ mk_count=$(track_gate_failure "mock_integrity")
14304
+ gate_failures="${gate_failures}mock_integrity,"
14305
+ log_warn "Mock integrity gate FAILED ($mk_count consecutive) - CRITICAL/HIGH mock problems"
14306
+ fi
14307
+ fi
14308
+ # Test mutation integrity gate (P0-3): block on HIGH test-fitting.
14309
+ if [ "${LOKI_GATE_MUTATION:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
14310
+ log_info "Quality gate: test mutation integrity..."
14311
+ if enforce_mutation_integrity; then
14312
+ clear_gate_failure "mutation_integrity"
14313
+ else
14314
+ local mt_count
14315
+ mt_count=$(track_gate_failure "mutation_integrity")
14316
+ gate_failures="${gate_failures}mutation_integrity,"
14317
+ log_warn "Mutation integrity gate FAILED ($mt_count consecutive) - HIGH test-fitting detected"
14318
+ fi
14319
+ fi
13984
14320
  # Code review gate (upgraded from advisory, with escalation)
13985
14321
  if [ "$PHASE_CODE_REVIEW" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
13986
14322
  log_info "Quality gate: code review..."
@@ -14052,7 +14388,7 @@ if __name__ == "__main__":
14052
14388
  if [ "$ITERATION_COUNT" -gt 0 ]; then
14053
14389
  run_doc_staleness_check
14054
14390
  fi
14055
- # Documentation quality gate - Gate 11 (v6.75.0)
14391
+ # Documentation quality gate - Gate 7 (Documentation Coverage)
14056
14392
  if [ "${LOKI_GATE_DOC_COVERAGE:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
14057
14393
  log_info "Quality gate: documentation coverage..."
14058
14394
  if run_doc_quality_gate; then
@@ -14276,6 +14612,17 @@ if __name__ == "__main__":
14276
14612
  log_warn "Completion claim rejected: held-out spec-eval gate found failing held-out acceptance check(s)."
14277
14613
  log_warn " Details under .loki/council/heldout-block.json ; opt out with LOKI_HELDOUT_GATE=0"
14278
14614
  # Fall through; keep iterating until the held-out checks pass.
14615
+ # P2-2: the assumption ledger gate must also guard the DEFAULT
14616
+ # completion-promise route, not only the interval-gated council path.
14617
+ # Otherwise an agent can self-assert "done" while a high-severity spec
14618
+ # assumption is still unresolved, bypassing the spec-robustness gate.
14619
+ # Mirrors the evidence/held-out gate arms above. Opt-out: the gate's
14620
+ # own LOKI_ASSUMPTION_GATE=0 (returns 0 immediately when disabled, so
14621
+ # this branch never fires). Gate output is printed by the gate itself.
14622
+ elif [ "$_completion_claimed" = 1 ] && type council_assumption_ledger_gate &>/dev/null && ! council_assumption_ledger_gate; then
14623
+ log_warn "Completion claim rejected: assumption ledger gate found unresolved high-severity spec assumption(s)."
14624
+ log_warn " Details under .loki/council/assumption-block.json ; opt out with LOKI_ASSUMPTION_GATE=0"
14625
+ # Fall through; keep iterating until high-sev assumptions resolve.
14279
14626
  elif [ "$_completion_claimed" = 1 ]; then
14280
14627
  echo ""
14281
14628
  if [ -n "$COMPLETION_PROMISE" ]; then
@@ -14751,6 +15098,8 @@ check_human_intervention() {
14751
15098
  log_info "Council force-review: blocked by evidence hard gate"
14752
15099
  elif type council_heldout_gate &>/dev/null && ! council_heldout_gate; then
14753
15100
  log_info "Council force-review: blocked by held-out spec-eval hard gate"
15101
+ elif type council_assumption_ledger_gate &>/dev/null && ! council_assumption_ledger_gate; then
15102
+ log_info "Council force-review: blocked by assumption ledger hard gate"
14754
15103
  elif type council_vote &>/dev/null && council_vote; then
14755
15104
  log_header "COMPLETION COUNCIL: FORCE REVIEW - PROJECT COMPLETE"
14756
15105
  # BUG #17 fix: Write COMPLETED marker, generate council report, and