loki-mode 7.129.5 → 8.0.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 (74) hide show
  1. package/README.md +81 -5
  2. package/SKILL.md +66 -5
  3. package/VERSION +1 -1
  4. package/autonomy/app-runner.sh +37 -0
  5. package/autonomy/completion-council.sh +862 -28
  6. package/autonomy/council-v2.sh +39 -0
  7. package/autonomy/crash.sh +3 -2
  8. package/autonomy/grill.sh +42 -0
  9. package/autonomy/hooks/validate-bash.sh +301 -4
  10. package/autonomy/lib/cockpit-render.sh +8 -2
  11. package/autonomy/lib/cr-rematerialize.py +101 -4
  12. package/autonomy/lib/deadline.py +957 -0
  13. package/autonomy/lib/dependency-setup.sh +205 -0
  14. package/autonomy/lib/done-recognition.sh +45 -0
  15. package/autonomy/lib/efficiency_cost.py +13 -6
  16. package/autonomy/lib/no_mock_scan.py +793 -0
  17. package/autonomy/lib/prd-enrich.sh +42 -0
  18. package/autonomy/lib/proof-analytics-props.py +69 -0
  19. package/autonomy/lib/proof-generator.py +282 -95
  20. package/autonomy/lib/proof-template.html +15 -12
  21. package/autonomy/lib/proof-verify.py +151 -102
  22. package/autonomy/lib/requirements_contract.py +848 -0
  23. package/autonomy/lib/sdk-mode.sh +103 -0
  24. package/autonomy/lib/secret-scan.sh +139 -0
  25. package/autonomy/lib/spec-expand.sh +208 -0
  26. package/autonomy/lib/tree_digest.py +266 -0
  27. package/autonomy/lib/voter-agents.sh +58 -8
  28. package/autonomy/lib/workspace_diff.py +124 -0
  29. package/autonomy/loki +189 -17
  30. package/autonomy/playwright-verify.sh +501 -0
  31. package/autonomy/prd-checklist.sh +17 -8
  32. package/autonomy/provider-offer.sh +111 -0
  33. package/autonomy/run.sh +4417 -676
  34. package/autonomy/sandbox.sh +42 -0
  35. package/autonomy/spec-interrogation.sh +148 -5
  36. package/autonomy/spec.sh +118 -1
  37. package/autonomy/telemetry.sh +133 -7
  38. package/autonomy/verify.sh +107 -0
  39. package/bin/loki +110 -3
  40. package/completions/_loki +10 -0
  41. package/completions/loki.bash +1 -1
  42. package/dashboard/__init__.py +1 -1
  43. package/dashboard/audit.py +96 -7
  44. package/dashboard/build_supervisor.py +1619 -0
  45. package/dashboard/control.py +8 -0
  46. package/dashboard/prompt_optimizer.py +7 -0
  47. package/dashboard/server.py +577 -22
  48. package/dashboard/static/trust.html +1 -1
  49. package/docs/ARCHITECTURE-OVERVIEW.md +207 -0
  50. package/docs/AUTONOMI-ECOSYSTEM.md +107 -0
  51. package/docs/INSTALLATION.md +19 -2
  52. package/docs/MODEL-EQUIVALENCE-HARNESS-PLAN.md +70 -0
  53. package/docs/PLANS-INDEX.md +33 -0
  54. package/docs/PRIVACY.md +29 -1
  55. package/docs/SIGNED-RECEIPTS.md +102 -0
  56. package/docs/SONNET5-DEFAULT-PLAN.md +1 -1
  57. package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +114 -0
  58. package/docs/V8-AGENT-SDK-PLAN.md +692 -0
  59. package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +45 -0
  60. package/docs/V8-MAJOR-RELEASE-PLAN.md +137 -0
  61. package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +82 -0
  62. package/docs/V8-RUNTIME-TRUTH-2026-07-25.md +232 -0
  63. package/docs/V8-SDK-RESEARCH-RAW.md +129 -0
  64. package/docs/alternative-installations.md +4 -4
  65. package/loki-ts/dist/loki.js +674 -285
  66. package/loki-ts/package.json +2 -0
  67. package/mcp/__init__.py +1 -1
  68. package/package.json +7 -6
  69. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  70. package/providers/claude.sh +75 -0
  71. package/providers/codex.sh +85 -13
  72. package/references/sdk-mode.md +106 -0
  73. package/skills/model-selection.md +1 -1
  74. package/skills/quality-gates.md +22 -0
package/autonomy/loki CHANGED
@@ -143,6 +143,15 @@ if [ -f "$_LOKI_SCRIPT_DIR/lib/config-map.sh" ]; then
143
143
  source "$_LOKI_SCRIPT_DIR/lib/config-map.sh"
144
144
  fi
145
145
 
146
+ # v8.1: one-switch SDK activation resolver (LOKI_SDK_MODE=off|judges|full,
147
+ # default off). Idempotent; run.sh sources+resolves it too. Sourced here so any
148
+ # in-CLI SDK judge site sees the resolved per-site flags. No-op when mode unset.
149
+ if [ -f "$_LOKI_SCRIPT_DIR/lib/sdk-mode.sh" ]; then
150
+ # shellcheck source=autonomy/lib/sdk-mode.sh
151
+ source "$_LOKI_SCRIPT_DIR/lib/sdk-mode.sh"
152
+ loki_sdk_resolve_mode
153
+ fi
154
+
146
155
  # Resolve the script's real path (handles symlinks)
147
156
  resolve_script_path() {
148
157
  local script="$1"
@@ -824,6 +833,7 @@ show_help() {
824
833
  echo "Usage: loki <command> [options]"
825
834
  echo ""
826
835
  echo "New here? Try these in order:"
836
+ echo " loki welcome Friendly first-run opener (what Loki is, next steps)"
827
837
  echo " loki doctor Check your setup is ready (a few seconds)"
828
838
  echo " loki quickstart Guided first build from your idea (no PRD needed)"
829
839
  echo " loki start ./prd.md Build from a spec (PRD file, GitHub/GitLab/Jira/Azure issue, or no arg)"
@@ -856,6 +866,7 @@ show_help() {
856
866
  echo " stop Stop execution immediately"
857
867
  echo " pause Pause after current session"
858
868
  echo " resume Resume paused execution"
869
+ echo " steer \"<note>\" Nudge a running build (needs LOKI_PROMPT_INJECTION=1)"
859
870
  echo ""
860
871
  echo "Verify / trust:"
861
872
  echo " verify [base] Deterministic PR verification (CI-gate exit codes)"
@@ -910,6 +921,8 @@ show_help() {
910
921
  echo " --budget USD Set cost budget limit (display in dashboard/status)"
911
922
  echo " --bmad-project PATH Use BMAD Method project artifacts as input"
912
923
  echo " --openspec PATH Use OpenSpec change directory as input"
924
+ echo " --config PATH Load settings from a config file (.yaml/.json/.env). Alias: --env-file, --vars"
925
+ echo " Precedence: CLI flag > env var > --config file > defaults. Secrets via \${VAR}."
913
926
  echo ""
914
927
  echo "Deprecated alias 'run': 'loki run <issue-ref>' still works as an alias"
915
928
  echo "for 'loki start'. See its options with 'loki run --help'."
@@ -3328,6 +3341,52 @@ cmd_pause() {
3328
3341
  fi
3329
3342
  }
3330
3343
 
3344
+ # RUN-25 iter 22 (Wave D #3): `loki steer "<note>"` writes an operator directive
3345
+ # the running loop actually reads (.loki/HUMAN_INPUT.md), replacing the dead
3346
+ # steering.md hint the cockpit used to advertise (nothing read it). The loop only
3347
+ # INJECTS the note when LOKI_PROMPT_INJECTION=1 (default off, for safety), so we
3348
+ # write the note and tell the operator the exact flag -- honest: the control now
3349
+ # genuinely persists the directive instead of silently no-op'ing.
3350
+ cmd_steer() {
3351
+ case "${1:-}" in
3352
+ --help|-h|help|"")
3353
+ echo -e "${BOLD}Loki Mode -- steer the running build${NC}"
3354
+ echo ""
3355
+ echo "Usage: loki steer \"<guidance>\""
3356
+ echo ""
3357
+ echo "Writes your guidance to .loki/HUMAN_INPUT.md, which the running"
3358
+ echo "loop reads at the next iteration boundary (when LOKI_PROMPT_INJECTION=1)."
3359
+ echo ""
3360
+ echo " loki steer \"focus on the auth flow, skip the docs for now\""
3361
+ echo ""
3362
+ echo "The loop injects the note only when prompt injection is enabled:"
3363
+ echo " LOKI_PROMPT_INJECTION=1 loki start ... (or export it before starting)"
3364
+ [ -z "${1:-}" ] && return 2
3365
+ return 0
3366
+ ;;
3367
+ esac
3368
+ if [ ! -d "$LOKI_DIR" ]; then
3369
+ echo -e "${YELLOW}No .loki directory found.${NC} Start a session first: loki start"
3370
+ return 1
3371
+ fi
3372
+ local note="$*"
3373
+ # Append with a timestamp so multiple steers accumulate; the reader consumes
3374
+ # and clears the file each iteration, so this is the pending-directive buffer.
3375
+ {
3376
+ printf '## Operator directive (%s)\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
3377
+ printf '%s\n\n' "$note"
3378
+ } >> "$LOKI_DIR/HUMAN_INPUT.md"
3379
+ emit_event session cli steer "chars=${#note}" 2>/dev/null || true
3380
+ echo -e "${GREEN}Steering note written to .loki/HUMAN_INPUT.md${NC}"
3381
+ if [ "${LOKI_PROMPT_INJECTION:-false}" != "1" ] && [ "${LOKI_PROMPT_INJECTION:-false}" != "true" ]; then
3382
+ echo -e "${YELLOW}Note: prompt injection is OFF, so the loop will NOT read this yet.${NC}"
3383
+ echo -e "${DIM}Enable it: export LOKI_PROMPT_INJECTION=1 (then the next iteration ingests the note).${NC}"
3384
+ else
3385
+ echo -e "${DIM}The next iteration will read and apply it.${NC}"
3386
+ fi
3387
+ return 0
3388
+ }
3389
+
3331
3390
  # Resume paused execution
3332
3391
  cmd_resume() {
3333
3392
  # v7.6.2 B-13 fix: --help must print help, not act on session state.
@@ -3835,6 +3894,8 @@ WHYJSON
3835
3894
  _LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
3836
3895
  _LOKI_WHY_HEAD_SHA="$_why_head_sha" \
3837
3896
  _LOKI_WHY_LAST_ERROR="$loki_dir/state/LAST_ERROR.json" \
3897
+ _LOKI_WHY_UNCERTAINTY="$loki_dir/signals/UNCERTAINTY_ESCALATION" \
3898
+ _LOKI_WHY_CONVERGENCE="$loki_dir/council/convergence.log" \
3838
3899
  _LOKI_WHY_HANDOFFS="$loki_dir/memory/handoffs" python3 - <<'WHYTXT'
3839
3900
  import json, os, glob
3840
3901
  def load(p):
@@ -3926,6 +3987,32 @@ print()
3926
3987
  print(f" What happened: {meaning}")
3927
3988
  print(f" What to do : {action}")
3928
3989
 
3990
+ # RUN-25 iter 22b/iter 23 (Wave D #6): when a run is PAUSED / live and actually
3991
+ # STUCK, name the REAL stall reason instead of the generic "resume". The runner
3992
+ # drops .loki/signals/UNCERTAINTY_ESCALATION when its proactive stuck-detector
3993
+ # fires (repeated no-progress / uncertainty), and council/convergence.log records
3994
+ # consecutive-no-change. Surface those so "why is it stuck" has a real answer, not
3995
+ # a generic resume hint. Reads existing files only (no new state, no spend).
3996
+ _unc = os.environ.get("_LOKI_WHY_UNCERTAINTY", "")
3997
+ if _unc and os.path.exists(_unc):
3998
+ print()
3999
+ print(" Stall reason : the runner's proactive stuck-detector escalated")
4000
+ print(" (UNCERTAINTY_ESCALATION): repeated no-progress or")
4001
+ print(" low-confidence iterations. It is not making headway on its own.")
4002
+ # Add the consecutive-no-change count from the convergence log tail if present.
4003
+ _conv = os.environ.get("_LOKI_WHY_CONVERGENCE", "")
4004
+ if _conv and os.path.exists(_conv):
4005
+ try:
4006
+ with open(_conv) as _cf:
4007
+ _tail = _cf.read().splitlines()[-1:]
4008
+ if _tail:
4009
+ print(f" Latest convergence signal: {_tail[0][:120]}")
4010
+ except Exception:
4011
+ pass
4012
+ print(" Unstick it : give the loop a concrete next step:")
4013
+ print(" loki steer \"<what to try next>\" (needs LOKI_PROMPT_INJECTION=1)")
4014
+ print(" or narrow the spec / fix the blocker, then loki resume.")
4015
+
3929
4016
  # Surface the structured per-iteration error record, if one exists at
3930
4017
  # .loki/state/LAST_ERROR.json (the run.sh writer that produces it ships in the
3931
4018
  # SAME v7.92.0 release; on an older engine the file is simply absent and this
@@ -9372,14 +9459,40 @@ cmd_assets() {
9372
9459
  esac
9373
9460
  }
9374
9461
 
9375
- # Guard: check if output file exists and warn before overwriting
9462
+ # Guard: check if output file exists and warn before overwriting.
9463
+ #
9464
+ # NON-INTERACTIVE SAFETY: this is the single chokepoint every export format
9465
+ # (_export_json / _export_markdown / _export_csv / _export_timeline) routes
9466
+ # through, so the guard lives here once rather than in four callers. Without it
9467
+ # the prompt BLOCKS FOREVER whenever stdin is not closed and not answerable -- a
9468
+ # CI lane, a pipeline, or a test harness that leaves stdin attached. That wedged
9469
+ # local-ci for 40+ minutes at tests/cli/test-alias-forwarding.sh (the runner sat
9470
+ # at 0.0% CPU in state S waiting on a read that could never be satisfied).
9471
+ #
9472
+ # Semantics mirror the established precedent at the BMAD prompt (search
9473
+ # LOKI_AUTO_CONFIRM): LOKI_AUTO_CONFIRM=true (falling back to CI) means ANSWER
9474
+ # YES, and echoes an auditable "y (auto-confirmed)" line. A non-interactive
9475
+ # shell WITHOUT that opt-in FAILS CLOSED: it refuses the overwrite and names the
9476
+ # env var, because silently clobbering a user's file in CI is the worse failure.
9477
+ # `read` is `|| reply=""` guarded so an EOF cannot abort a `set -e` caller.
9376
9478
  _export_check_overwrite() {
9377
9479
  local output="$1"
9378
9480
  if [ -n "$output" ] && [ -f "$output" ]; then
9379
9481
  echo -e "${YELLOW}Warning: File already exists: $output${NC}"
9482
+ local _auto="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
9483
+ if [ "$_auto" = "true" ]; then
9484
+ echo -n "Overwrite? [y/N] "
9485
+ echo "y (auto-confirmed)"
9486
+ return 0
9487
+ fi
9488
+ if [ ! -t 0 ]; then
9489
+ echo "Export cancelled (non-interactive: refusing to overwrite '$output'." >&2
9490
+ echo "Set LOKI_AUTO_CONFIRM=true to overwrite, or choose a new output path.)" >&2
9491
+ return 1
9492
+ fi
9380
9493
  echo -n "Overwrite? [y/N] "
9381
9494
  local reply
9382
- read -r reply
9495
+ read -r reply || reply=""
9383
9496
  case "$reply" in
9384
9497
  [yY]|[yY][eE][sS]) return 0 ;;
9385
9498
  *) echo "Export cancelled."; return 1 ;;
@@ -10044,7 +10157,18 @@ cmd_config_edit() {
10044
10157
 
10045
10158
  if [ -z "$config_file" ]; then
10046
10159
  echo -e "${YELLOW}No config file found.${NC}"
10047
- read -p "Create one? [Y/n]: " create
10160
+ # Non-interactive safety (same class as _export_check_overwrite): a bare
10161
+ # `read` here blocks forever when stdin is attached but unanswerable.
10162
+ # This prompt DEFAULTS TO YES, so auto-confirm and non-interactive both
10163
+ # take the create path; only the explicit interactive "n" declines.
10164
+ local create=""
10165
+ local _auto="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
10166
+ if [ "$_auto" = "true" ] || [ ! -t 0 ]; then
10167
+ echo "Create one? [Y/n]: Y (non-interactive default; run 'loki config init' to control this)"
10168
+ create="Y"
10169
+ else
10170
+ read -r -p "Create one? [Y/n]: " create || create=""
10171
+ fi
10048
10172
  create="${create:-Y}"
10049
10173
  if [[ "$create" =~ ^[Yy] ]]; then
10050
10174
  cmd_config_init
@@ -10555,15 +10679,25 @@ cmd_doctor() {
10555
10679
  # Check if at least one provider is installed (detect_any_provider is the
10556
10680
  # shared helper from provider-offer.sh, extracted from this exact loop).
10557
10681
  if ! detect_any_provider; then
10558
- echo -e " ${RED}FAIL${NC} No AI provider CLI installed -- at least one is required"
10559
- echo -e " ${YELLOW}Install: npm install -g @anthropic-ai/claude-code${NC}"
10560
- fail_count=$((fail_count + 1))
10561
- # v7.29.0: on a TTY (non-json), append the consent-gated install offer.
10562
- # In report mode the helper is a no-op on non-TTY/CI, so the doctor
10563
- # output stays byte-identical for the bun-parity matrix. --json never
10564
- # reaches here (cmd_doctor dispatches to cmd_doctor_json earlier).
10565
- if declare -f offer_provider_install >/dev/null 2>&1; then
10566
- offer_provider_install report || true
10682
+ # The bundled Claude Agent SDK runs the main loop with no separate CLI,
10683
+ # but ONLY when it is genuinely usable: binary extracted, credentials
10684
+ # present, and the SDK loop actually active. detect_bundled_sdk_provider
10685
+ # fails closed on any doubt, so when it returns false we emit today's
10686
+ # blocker verbatim. Mirrored in loki-ts/src/commands/doctor.ts.
10687
+ if declare -f detect_bundled_sdk_provider >/dev/null 2>&1 && detect_bundled_sdk_provider; then
10688
+ echo -e " ${GREEN}PASS${NC} Bundled Claude Agent SDK is usable -- no separate CLI needed"
10689
+ pass_count=$((pass_count + 1))
10690
+ else
10691
+ echo -e " ${RED}FAIL${NC} No AI provider CLI installed -- at least one is required"
10692
+ echo -e " ${YELLOW}Install: npm install -g @anthropic-ai/claude-code${NC}"
10693
+ fail_count=$((fail_count + 1))
10694
+ # v7.29.0: on a TTY (non-json), append the consent-gated install offer.
10695
+ # In report mode the helper is a no-op on non-TTY/CI, so the doctor
10696
+ # output stays byte-identical for the bun-parity matrix. --json never
10697
+ # reaches here (cmd_doctor dispatches to cmd_doctor_json earlier).
10698
+ if declare -f offer_provider_install >/dev/null 2>&1; then
10699
+ offer_provider_install report || true
10700
+ fi
10567
10701
  fi
10568
10702
  fi
10569
10703
  echo ""
@@ -12977,7 +13111,12 @@ cmd_init() {
12977
13111
  return 1
12978
13112
  fi
12979
13113
  fi
12980
- echo -e "${YELLOW}Reinitializing existing .loki/ directory${NC}"
13114
+ # stderr, not stdout: `loki init --json` writes a machine-readable
13115
+ # template list to stdout, and this human-facing banner was landing in
13116
+ # the middle of it -- the second `init` in a directory emitted invalid
13117
+ # JSON, so any tool parsing it broke on reinit but worked on first run.
13118
+ # Diagnostics belong on stderr; stdout is the data channel.
13119
+ echo -e "${YELLOW}Reinitializing existing .loki/ directory${NC}" >&2
12981
13120
  fi
12982
13121
 
12983
13122
  local version=$(get_version)
@@ -13286,7 +13425,10 @@ print(p or '')
13286
13425
  return 1
13287
13426
  fi
13288
13427
  fi
13289
- echo -e "${YELLOW}Reinitializing existing .loki/ directory in $target_dir${NC}"
13428
+ # stderr for the same reason as the sibling banner above: stdout is the
13429
+ # machine-readable channel. Fixing only the path the failing test
13430
+ # exercised would leave this one to resurface the same bug.
13431
+ echo -e "${YELLOW}Reinitializing existing .loki/ directory in $target_dir${NC}" >&2
13290
13432
  fi
13291
13433
 
13292
13434
  # Build config JSON
@@ -16692,7 +16834,7 @@ def _resolve_session_pin(alias):
16692
16834
  # override-path clamp). A 'sonnet' override dispatches sonnet.
16693
16835
  # - No override (session pin): the runner maps alias -> tier ->
16694
16836
  # resolve_model_for_tier -> clamp(model, real_tier). Use _resolve_session_pin.
16695
- # A 'sonnet' session pin dispatches OPUS (development tier).
16837
+ # A 'sonnet' session pin dispatches SONNET (development tier; CLAUDE_DEFAULT_DEVELOPMENT=sonnet since v7.104.0).
16696
16838
  # This is the task-568 fix: price the actually-dispatched model on BOTH routes.
16697
16839
  _is_override = (_session_model_source == '.loki/state/model-override')
16698
16840
  if _is_override:
@@ -17508,7 +17650,17 @@ main() {
17508
17650
  && _loki_telemetry_enabled; then
17509
17651
  _loki_disclose_telemetry_once
17510
17652
  fi
17511
- loki_telemetry "cli_command" "command=$command" 2>/dev/null || true
17653
+ # The command value is passed through the FIXED ALLOWLIST at the boundary
17654
+ # (_loki_known_command, autonomy/telemetry.sh): a real subcommand goes out
17655
+ # verbatim, anything else becomes the literal "other". Without this, the
17656
+ # honest typo `loki ./client-acme-merger-prd.md` put the raw file name on the
17657
+ # wire, because the payload builder is a pass-through dict, not an allowlist.
17658
+ # Fail closed: if the sanitizer is somehow undefined, send "other".
17659
+ if declare -f _loki_known_command >/dev/null 2>&1; then
17660
+ loki_telemetry "cli_command" "command=$(_loki_known_command "$command")" 2>/dev/null || true
17661
+ else
17662
+ loki_telemetry "cli_command" "command=other" 2>/dev/null || true
17663
+ fi
17512
17664
 
17513
17665
  # Unified config-file pre-pass (#691). For session commands ONLY, honor
17514
17666
  # LOKI_CONFIG_FILE / --config|--vars|--env-file and load the file BEFORE the
@@ -17581,6 +17733,9 @@ main() {
17581
17733
  pause)
17582
17734
  cmd_pause "$@"
17583
17735
  ;;
17736
+ steer)
17737
+ cmd_steer "$@"
17738
+ ;;
17584
17739
  resume)
17585
17740
  cmd_resume "$@"
17586
17741
  ;;
@@ -29473,6 +29628,20 @@ except Exception:
29473
29628
  fi
29474
29629
 
29475
29630
  # Gate 2: Security Scan
29631
+ # Deny filter for the secret patterns below. This scanner sees RAW DIFF
29632
+ # LINES with no file context, so unlike the other two copies of this matcher
29633
+ # (autonomy/lib/secret-scan.sh, verify.sh) it cannot skip test paths -- it
29634
+ # has to judge the VALUE. A credential whose value literally says "test",
29635
+ # "example", "xxx", or "not-called" is a fixture; real leaked keys are not
29636
+ # named after their own fakeness.
29637
+ #
29638
+ # Concretely, this is what reported 2 CRITICAL findings on the v8 PR:
29639
+ # sk-ant-xxx (loki-ts/test/runner/oauth_dev.test.ts)
29640
+ # sk-ant-test-not-called (loki-ts/tests/runner/quality_gates.test.ts)
29641
+ # Both exist to prove the SDK path fails closed without a real key. Flagging
29642
+ # them trains people to ignore CRITICAL, which is how a real one gets missed.
29643
+ local _ci_secret_deny='(test|example|sample|fixture|dummy|fake|placeholder|redacted|changeme|your[-_]|xxx+|not[-_]called|\$\{|\$[A-Za-z_]|process\.env|os\.(environ|getenv)|<[^>]*>)'
29644
+
29476
29645
  _ci_scan() {
29477
29646
  local pattern="$1" sev="$2" cat="$3" finding="$4" suggestion="$5"
29478
29647
  # Only scan added lines (starting with "+") to avoid flagging deleted code
@@ -29481,7 +29650,10 @@ except Exception:
29481
29650
  local ml
29482
29651
  ml=$(echo "$match" | cut -d: -f1)
29483
29652
  _ci_add_finding "diff" "$ml" "$sev" "$cat" "$finding" "$suggestion"
29484
- done < <(echo "$diff_content" | grep -n '^+' | grep -E "$pattern" 2>/dev/null || true)
29653
+ done < <(printf '%s\n' "$diff_content" \
29654
+ | grep -n '^+' \
29655
+ | grep -E "$pattern" 2>/dev/null \
29656
+ | grep -vEi "$_ci_secret_deny" 2>/dev/null || true)
29485
29657
  }
29486
29658
 
29487
29659
  # Hardcoded secrets