Skip to content

recon(sprint-u0): web dashboard census, colour/contrast/streaming/focus findings #620

recon(sprint-u0): web dashboard census, colour/contrast/streaming/focus findings

recon(sprint-u0): web dashboard census, colour/contrast/streaming/focus findings #620

Workflow file for this run

name: "Konjo Quality Gate — Wall 2"
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: konjo-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
CARGO_INCREMENTAL: 0 # disable for reproducible CI builds
# ── Gate summary job — all other jobs must pass before merge ──────────────────
# Added at the bottom; listed here for visibility.
jobs:
# ════════════════════════════════════════════════════════════════════════════
# Gate 0 — Doc Staleness (kiban's `decays:` convention)
# ════════════════════════════════════════════════════════════════════════════
# Doc-Integrity sprint (2026-07-24): `docs/LOOP_ENGINEERING_ROADMAP.md` asserted
# four capability gaps that were already closed on `main` — caught by a kill-test,
# not by CI, because no gate checked whether a "current state" doc still matched
# the code. This job is that gate. It is a HARD gate — not `continue-on-error:
# true` — deliberately: an unstamped `decays: state` doc, or one whose
# `verified-against` has drifted too far behind HEAD, fails the scan. Softening
# this later reproduces the exact failure this job exists to close.
doc-staleness:
name: "G0 · Doc Staleness"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # decays: state docs are checked by commit distance from verified-against
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install kiban's doc-staleness checker
env:
# Pinned deliberately — matches .konjo/kiban.ref (the session-plane pin).
# Bump both together; a kiban change should not silently reach this gate.
KIBAN_REF: "v1.8.0"
run: |
pip install --quiet pyyaml
git clone --quiet --depth 1 --branch "$KIBAN_REF" https://github.com/konjoai/kiban.git /tmp/kiban
- name: "Scan for stale decays: state docs"
run: python3 /tmp/kiban/bin/konjo-doc-staleness scan --repo .
# ════════════════════════════════════════════════════════════════════════════
# Gate 1 — Static Analysis (format + lint + audit + deny)
# ════════════════════════════════════════════════════════════════════════════
static:
name: "G1 · Static Analysis"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
shared-key: konjo-${{ github.repository }}
- name: rustfmt
run: cargo fmt --all -- --check
- name: clippy — zero warnings, all deny flags
run: |
cargo clippy --workspace --all-targets --all-features -- \
-D warnings \
-D clippy::unwrap_used \
-D clippy::expect_used \
-D clippy::panic \
-D clippy::todo \
-D clippy::unimplemented \
-D clippy::dbg_macro \
-D clippy::print_stdout \
-D clippy::print_stderr \
-W clippy::cognitive_complexity
- name: clippy pedantic (advisory — non-blocking)
# ADVISORY BY DESIGN, not deferred debt: clippy::pedantic is a large,
# opinionated style/naming lint tier, not a correctness or security
# signal — the standard clippy tier above already hard-fails on
# actual bugs. Kept permanently soft. Next step, if ever: revisit
# only if pedantic violations start correlating with real bugs
# slipping through.
continue-on-error: true
run: |
cargo clippy --workspace --all-targets -- -D clippy::pedantic 2>&1 \
| grep "^error" | head -20 || true
echo "Pedantic lints above are advisory — fix over time."
- name: cargo audit — zero known vulnerabilities
# HARD GATE since Sprint S4 Phase 3. Sprint S10, Phase 2 (2026-07-27)
# re-triaged this from scratch — an external audit found 11
# advisories affecting locked versions that this gate's own
# `.cargo/audit.toml` had drifted out of sync with (a supply-chain
# config that doesn't run cleanly is a decorative gate, per that
# sprint's own framing). The sqlx 0.7 -> 0.8.6 upgrade resolved the
# rustls-webpki/sqlx-format-injection chain outright (gone from the
# dependency graph, not just ignored). What remains, all named
# individually in `.cargo/audit.toml`: RUSTSEC-2023-0071 (rsa) — a
# Cargo.lock entry unreachable in the actual build graph (`cargo tree
# -i rsa` prints nothing) regardless of sqlx's major version; four
# `unmaintained`/`unsound` findings on crates none of which is a
# direct workspace dependency (paste/proc-macro-error/rustls-pemfile/
# lru, all 2+ hops transitive). A new, untriaged advisory still fails
# this step.
run: |
cargo +stable install cargo-audit --locked --quiet 2>/dev/null || true
cargo audit
- name: cargo deny — license + advisory + bans
# HARD GATE since Sprint S4 Phase 3. `.konjo/deny.toml` re-verified
# in Sprint S10, Phase 2 (2026-07-27) after the sqlx upgrade changed
# the dependency graph's license set — `webpki-roots`'s newer version
# dropped its MPL-2.0 exception in favor of CDLA-Permissive-2.0
# (updated in `[licenses] allow`), and three now-unused license
# allowances (`CC0-1.0`/`OpenSSL`/`Unicode-DFS-2016`) were removed
# rather than left as dead config cargo-deny would otherwise flag
# `unmatched`. `cargo deny check` returns a clean verdict: advisories
# ok, bans ok, licenses ok, sources ok.
run: |
cargo +stable install cargo-deny --locked --quiet 2>/dev/null || true
cargo deny --config .konjo/deny.toml check
- name: dead code — zero tolerance
run: |
OUTPUT=$(RUSTFLAGS="-W dead_code" cargo check --workspace 2>&1)
COUNT=$(echo "$OUTPUT" | grep -c "warning: dead_code" || true)
echo "Dead code warnings: $COUNT"
if [ "$COUNT" -gt 0 ]; then
echo "$OUTPUT" | grep "warning: dead_code" | head -20
echo "::error::$COUNT dead code warning(s). Remove all dead code before merging."
exit 1
fi
- name: Scope assertion — no multi-tenant surface (Sprint S12, Phase 6)
# HARD GATE. Sprint S12 removed lopi's multi-tenant surface (lopi-app,
# open_for_customer, github_installations, CustomerTier/Stripe) as a
# decision, not a fact the code enforces on its own — nothing stops a
# future PR from reintroducing it one file at a time. This mechanizes
# the decision so it can't drift back by omission. See
# .konjo/scripts/scope_assert.py's own docstring for the exact term
# list and why "installation" (the bare English word) is deliberately
# narrowed to the specific removed identifiers instead.
run: python3 .konjo/scripts/scope_assert.py
- name: Reachability check (advisory) — pub mod not reachable from the binary
# ADVISORY BY DESIGN — Sprint F0. `dead_code` above certifies rustc
# reachability (a `pub` item in a library crate is never dead by
# rustc's definition, no matter whether the shipped binary calls
# it) — a different property from "reachable when you actually run
# `lopi`". Sprint F0 found `lopi-remote::whatsapp` passing the
# dead-code gate while being completely unreferenced anywhere else
# in the workspace (see CHANGELOG.md, docs/security/TRIFECTA_PATHS.md).
# This is a grep-shaped heuristic (see the script's own docstring
# for its exact definition and known false-positive/negative
# shapes), not a real call-graph analyzer — it must never fail the
# build on its own. It exists purely so the next feature that
# quietly becomes unreachable shows up here before it also lingers
# in the README.
continue-on-error: true
run: python3 .konjo/scripts/reachability_check.py
- name: Soft-gate convention lint
# HARD GATE — Sprint S4 Phase 2. Makes the `KNOWN DEBT, verified
# <date>` / `ADVISORY BY DESIGN` convention this file already
# follows by hand mechanical: any future `continue-on-error: true`
# step added without one of those two markers fails here, instead
# of quietly joining the pile of undocumented soft gates the
# convention exists to prevent.
run: python3 .konjo/scripts/soft_gate_lint.py .github/workflows/*.yml
# ════════════════════════════════════════════════════════════════════════════
# Gate 1b — Static Analysis, web/ (Forge dashboard — SvelteKit, separate
# npm ecosystem from the Rust `static` job above, so it gets its own job
# rather than a step wedged into a Rust runner)
# ════════════════════════════════════════════════════════════════════════════
# Added Sprint S11, Phase 2 (2026-07-27): `web/` had zero `npm audit` CI
# gate at all before this — the Rust half of the repo has had `cargo
# audit`/`cargo deny` as hard gates since Sprint S4 Phase 3 (see `static`
# above), but a real `npm audit` on `web/` found 13 advisories (4 high, 7
# moderate, 2 low) with no CI step ever catching drift. This job mirrors
# `static`'s `cargo audit` step exactly: `npm ci` for a reproducible,
# lockfile-exact install (the `npm install` equivalent of `cargo audit`
# reading `Cargo.lock`), then a hard-fail audit at `--audit-level=high`.
#
# Phase 2 also upgraded every flagged runtime-shipped package to a patched
# version — `svelte` 4.2.0 -> 5.56.8 (the six SSR/XSS advisories, e.g.
# GHSA-rcqx-6q8c-2c42, GHSA-pr6f-5x2q-rwfp, all fixed only from 5.56.8;
# see `.konjo/killtests/S11/KT-S11.2.md` for the companion finding that no
# live sink in this app's component tree actually exercised those
# vulnerable capabilities — upgraded anyway as hygiene, not because an
# exploit path was closed), `@sveltejs/kit` 2.59.1 -> 2.70.1, `devalue`
# 5.8.0 -> 5.8.2, `@sveltejs/vite-plugin-svelte` 3.x -> 5.1.1 + `vite` 5.x
# -> 6.4.3 (paired major bumps — the moderate vite-plugin-svelte/vitefu
# chain and the high/moderate esbuild+vite path-traversal advisories both
# required leaving the vite 5 line), `svelte-check` 3.x -> 4.7.4 (clears a
# transitive `brace-expansion`/`glob`/`minimatch`/`rimraf`/`sander`/
# `sorcery`/`svelte-preprocess` chain, all high), and `cookie` pinned to
# `^0.7.0` via `package.json`'s `overrides` (GHSA-pxg6-pf52-xh8x —
# `@sveltejs/kit@2.70.1`, the latest stable line, still declares
# `cookie: ^0.6.0` itself; only the unstable `3.0.0-next` prerelease line
# bumps that to `cookie ^2.0.0`, so an override was the only way to get
# the patched `cookie` without adopting an unstable SvelteKit). Net
# result, re-verified with a clean `rm -rf node_modules && npm ci && npm
# audit`: 0 vulnerabilities — this gate does not need an ignore mechanism
# (npm audit has no scoped per-advisory allow-list the way
# `.cargo/audit.toml` does for cargo-audit; none was needed here since
# the audit is fully clean, not because one was swallowed).
web-audit:
name: "G1b · npm audit (web/)"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: npm ci — reproducible, lockfile-exact install
working-directory: web
run: npm ci
- name: npm audit — zero high/critical advisories
# HARD GATE since Sprint S11, Phase 2. `--audit-level=high` matches
# this repo's Rust-side bar (`cargo audit` fails on ANY known
# advisory reachable in the build graph); `high` rather than `low`
# here because npm audit's low/moderate tier includes a much wider
# long tail of dev-toolchain-only findings (see the KT-S11.2
# comment above) that would make this gate noisy without adding
# real signal — runtime-shipped packages are held to the same
# standard as `cargo audit` via the upgrade work itself (0
# vulnerabilities at any severity as of Phase 2), not by loosening
# what this gate checks.
working-directory: web
run: npm audit --audit-level=high
# ════════════════════════════════════════════════════════════════════════════
# Gate 2 — Tests + Coverage (≥ 80% line coverage)
# ════════════════════════════════════════════════════════════════════════════
coverage:
name: "G2 · Tests + Coverage"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2
with:
shared-key: konjo-${{ github.repository }}
- name: Install cargo-nextest + cargo-llvm-cov
run: |
cargo +stable install cargo-nextest --locked --quiet 2>/dev/null || true
cargo +stable install cargo-llvm-cov --locked --quiet 2>/dev/null || true
- name: Eval-executor regression suite (A1 — HARD GATE, 24 fixtures)
# The Research-1 reliability probe's 24 labelled fixtures (real
# pass/fail + the 7 gaming patterns) are a committed regression suite
# for the tiered eval executor. Every eval change must keep scoring all
# 24 correctly, decide objective failures without a judge call, and
# catch every gaming pattern. Deliberately NOT continue-on-error: a
# gate that could pass when it errors is the one thing an evaluator
# can't do, so this blocks the merge, unlike the soft coverage step.
run: cargo test -p lopi-agent --test eval_regression --locked
- name: Run tests with coverage
# KNOWN DEBT, verified 2026-07 (backend-1 Phase 0): `--fail-under-lines
# 80` makes this step itself exit nonzero below the 80% floor. Real
# measured coverage is 68.34% — see "Coverage gate" below for the
# number, method, and next step. Kept soft here too so this step's
# own exit code doesn't duplicate that gate's verdict.
continue-on-error: true
run: |
cargo llvm-cov nextest \
--workspace \
--all-features \
--lcov \
--output-path lcov.info \
--fail-under-lines 80
- name: Upload coverage to Codecov
if: always()
uses: codecov/codecov-action@v4
with:
files: lcov.info
fail_ci_if_error: false # don't fail CI on Codecov outage
- name: Coverage gate (80% floor, 95% target)
# KNOWN DEBT, verified 2026-07 (backend-1 Phase 0): real workspace
# line coverage, measured correctly (see the LF/LH fix below), is
# 68.34% (23355 lines found, 15960 hit) — below the 80% floor, but
# not by a catastrophic margin. Not yet triaged into which crates
# need tests. Next step: identify the lowest-coverage crates from
# lcov.info, add tests until ≥80%, then remove this line.
#
# Also fixed this sprint, independent of the soft/hard question:
# `cargo llvm-cov report --json` does NOT support `--workspace` (it
# errors outright if you pass it) and silently defaults to the root
# binary crate's own package scope — 24 files, ~26%, nowhere near
# the real number. The previous version of this step was reading
# that undercounted report and would have graded coverage on ~1/7th
# of the workspace even after being flipped to hard-fail. Parsing
# the `--workspace`-scoped lcov.info the prior step already wrote
# (summing LF/LH directly) is the correct source of truth.
continue-on-error: true
run: |
COVERAGE=$(python3 -c "
lf = lh = 0
try:
with open('lcov.info') as f:
for line in f:
if line.startswith('LF:'):
lf += int(line.strip().split(':')[1])
elif line.startswith('LH:'):
lh += int(line.strip().split(':')[1])
print(f'{100*lh/lf:.2f}' if lf else '0.0')
except Exception:
print('0.0') # conservative: fail if we can't parse
")
echo "Line coverage: ${COVERAGE}%"
python3 -c "
cov = float('${COVERAGE}')
if cov < 80:
print(f'::error::Coverage {cov:.1f}% < 80% gate. Add tests before merging.')
exit(1)
elif cov < 95:
print(f'::warning::Coverage {cov:.1f}% < 95% target. Aim higher.')
"
- name: Coverage floor gate (never regress below the locked value)
# HARD GATE — Konjo Forward Pillar 2: main is truth, gates only move up.
# The 80%/95% step above stays soft (real coverage isn't there yet), but
# the bar lopi HAS cleared (locked in .konjo/coverage-floor.txt, 68.34%
# as of Sprint S4) must never be allowed to slide back down un-noticed.
# Reuses the same LF:/LH: summation as the soft gate above — see that
# step's comment for why `cargo llvm-cov report --json` cannot be used
# instead (it silently under-scopes to ~1/7th of the workspace).
# Ratcheting up is allowed and expected: a PR that raises coverage may
# bump .konjo/coverage-floor.txt to the new value in the same PR.
run: |
python3 .konjo/scripts/coverage_floor_check.py \
--lcov lcov.info \
--floor-file .konjo/coverage-floor.txt
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-lcov
path: lcov.info
# ════════════════════════════════════════════════════════════════════════════
# Gate 3 — Mutation Testing (≤ 10% survival on changed files)
# Runs only on pull requests (too slow for push to main)
# ════════════════════════════════════════════════════════════════════════════
mutation:
name: "G3 · Mutation Testing"
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: konjo-${{ github.repository }}
- name: Install cargo-mutants
run: cargo +stable install cargo-mutants --locked --quiet 2>/dev/null || true
- name: Identify changed Rust files
id: changed
run: |
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \
| grep '\.rs$' | grep -v 'test' | head -30 | tr '\n' ' ' | xargs || true)
# Use heredoc to safely write multi-line-safe value to GITHUB_OUTPUT
echo "files=$CHANGED" >> "$GITHUB_OUTPUT"
echo "Changed Rust files to mutate:"
echo "$CHANGED"
- name: Run mutation testing on changed files
if: steps.changed.outputs.files != ''
run: |
cargo mutants \
--in-diff <(git diff origin/${{ github.base_ref }}...HEAD) \
--timeout 60 \
--jobs 2 \
2>&1 | tee mutation_report.txt
- name: Evaluate mutation survival rate
if: steps.changed.outputs.files != ''
run: |
# `grep -c` counts matching *lines*, not occurrences — cargo-mutants'
# own one-line summary ("N mutants tested in Xm: A missed, B caught,
# C unviable") always contains both the literal words "missed" and
# "caught" regardless of A/B, so the old `grep -c "caught"`/
# `grep -c "missed"` always matched exactly 1 line each, pinning
# CAUGHT=1 MISSED=1 (50% survival) on every run with any output at
# all. Parse the actual counts out of that summary line instead.
SUMMARY=$(grep -E "mutants tested in" mutation_report.txt | tail -1)
MISSED=$(echo "$SUMMARY" | grep -oE '[0-9]+ missed' | sed -E 's/ missed//')
CAUGHT=$(echo "$SUMMARY" | grep -oE '[0-9]+ caught' | sed -E 's/ caught//')
MISSED=${MISSED:-0}
CAUGHT=${CAUGHT:-0}
TOTAL=$((CAUGHT + MISSED))
echo "Mutations: caught=$CAUGHT missed=$MISSED total=$TOTAL"
if [ "$TOTAL" -gt 0 ]; then
SURVIVAL=$(echo "scale=1; $MISSED * 100 / $TOTAL" | bc)
echo "Mutation survival rate: ${SURVIVAL}%"
python3 -c "
rate = float('${SURVIVAL}')
if rate > 10:
print(f'::error::Mutation survival {rate:.1f}% > 10% gate.')
print('Tests are insufficient. Surviving mutants indicate untested paths.')
exit(1)
elif rate > 0:
print(f'::warning::Mutation survival {rate:.1f}% — aim for 0%')
"
fi
- name: Upload mutation report
if: always() && steps.changed.outputs.files != ''
uses: actions/upload-artifact@v4
with:
name: mutation-report
path: mutation_report.txt
# ════════════════════════════════════════════════════════════════════════════
# Gate 4 — Complexity + Size + DRY
# ════════════════════════════════════════════════════════════════════════════
complexity:
name: "G4 · Complexity + Size + DRY"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: konjo-${{ github.repository }}
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Cognitive complexity gate (clippy)
run: |
# Any function with cognitive complexity > 15 fails the gate
VIOLATIONS=$(cargo clippy --workspace --all-targets -- \
-A clippy::all \
-W clippy::cognitive_complexity \
--message-format json 2>/dev/null \
| python3 -c "
import json, sys
count = 0
for line in sys.stdin:
try:
msg = json.loads(line)
if msg.get('reason') == 'compiler-message':
m = msg['message']
if 'cognitive_complexity' in str(m.get('code', {})):
print(m['rendered'])
count += 1
except:
pass
sys.exit(0 if count == 0 else 1)
" || true)
if [ -n "$VIOLATIONS" ]; then
echo "$VIOLATIONS"
echo "::error::Functions with cognitive complexity > 15 found. Refactor them."
exit 1
fi
echo "Cognitive complexity: all functions ≤ 15 ✓"
- name: File size gate (changed files on PR, all files on push) — scoped to *.rs and *.py only
# Scope, stated explicitly per Sprint F0 (konjoai/lopi): this gate has
# only ever scanned Rust and Python (`\.(rs|py)$`). It does not cover
# `web/` (TypeScript/Svelte) or `macos/` (Swift) — those ecosystems
# have their own size conventions and were never audited against a
# 500-line limit. `web/src/lib/stores/stack.ts` is ~2,200 lines and
# is *not* a gate violation under the current scope; it is not
# silently passing a check that was meant to catch it. Extending
# this gate to web/macos, and the stack.ts split that would require
# first, is tracked as follow-up work, not done here (Sprint F0 is
# measurement/documentation only — see NEXT_SESSION_PROMPT.md).
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
SCAN_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \
| grep -E '\.(rs|py)$' | grep -v target || true)
else
SCAN_FILES=$(find . \( -name "*.rs" -o -name "*.py" \) | grep -v target)
fi
OVERSIZED=""
for f in $SCAN_FILES; do
[ -f "$f" ] || continue
LC=$(wc -l < "$f")
if [ "$LC" -gt 500 ]; then
OVERSIZED="${OVERSIZED}${f}: ${LC} lines\n"
fi
done
if [ -n "$OVERSIZED" ]; then
printf "Files exceeding 500-line limit:\n%b" "$OVERSIZED"
echo "::error::File(s) exceeding 500-line limit. Split into smaller modules."
exit 1
fi
echo "File sizes: all within 500-line limit ✓"
- name: DRY check
run: |
python3 .konjo/scripts/dry_check.py \
--threshold 0.85 \
--min-lines 20 \
--report dry_report.json 2>&1
COUNT=$(python3 -c "import json; d=json.load(open('dry_report.json')); print(d['count'])")
if [ "$COUNT" -gt 0 ]; then
echo "::error::$COUNT DRY violation(s). Abstract duplicate code into shared functions."
exit 1
fi
- name: Function length gate (<= 50 lines, ratchet against the locked ceiling)
# HARD GATE — Sprint S13R, Phase B, decision item 3. CLAUDE.md's "function
# body <= 50 lines" claim had zero mechanical enforcement before this (only a
# WARNING-tier Wall-3 LLM review question that cannot block a merge — see
# `PHASE0-STOP-RULE.md`). Real measured count against this branch: 74
# functions over 50 lines, workspace-wide. Hard-failing at 0 immediately
# would block every PR on pre-existing debt unrelated to it, so this ratchets
# the same way the coverage floor does: `.konjo/function-length-ceiling.txt`
# locks the current count, and only a rise above it fails. Lower the ceiling
# file as functions get split; never raise it without a recorded reason.
run: |
python3 .konjo/scripts/function_length_check.py \
--ceiling-file .konjo/function-length-ceiling.txt
- name: Indexing floor gate (raw `[0]`/`[1]` sites, ratchet against the locked ceiling)
# HARD GATE — Sprint S13R, Phase D. A raw index panics on an out-of-bounds
# slice; this ratchets the standing count the same way the coverage floor and
# function-length ceiling do. `.konjo/indexing-floor.txt` locks the current
# count (211, re-measured this sprint with a precisely stated method — see
# that file's own comment for why it doesn't match the S13 brief's earlier,
# less precisely specified "202"). Never regresses above it; ratchet down as
# sites convert to `.first()`/`.get(n)`.
run: |
python3 .konjo/scripts/indexing_floor_check.py \
--ceiling-file .konjo/indexing-floor.txt
- name: Documentation gate (rustdoc)
# KNOWN DEBT, verified 2026-07-29 (Sprint S13R, Phase B — decision item 2):
# re-measured rather than assumed. The debt grew past what backend-1 Phase 0
# named: `lopi-agent` now has 11 unresolved links (StreamEvent x4, parse_line,
# Self::from_str, is_stale, staleness_warning, describe x2, TranscriptEvent::Other),
# `lopi-orchestrator` has 8 (reserve, estimate, ladder, detect, report,
# is_favorable, types, JobScheduler, TopologyHint), and a new one appeared in
# `lopi-mcp` (crate::config::LoopConfig in allowlist.rs) that Phase 0 never
# named at all — the same systemic pattern (bare `[Type]`/`[func]` links inside
# doc comments not resolving), just wider than last measured. Decision (Phase B):
# this is real, scattered, multi-crate work, not a small fix-now item, so it
# stays soft rather than a rushed hard-flip that would just move the debt to a
# bypass. Owner: whichever sprint next touches `lopi-agent`/`lopi-orchestrator`
# docs; target: before Sprint S14 closes, re-measure with the exact command
# below and either clear it or re-file with a new date, so this marker can't
# silently go stale itself. Hard-failing now would abort the doc build on
# links, not missing docs, before missing_docs coverage is even checked
# workspace-wide. Next step: grep for `//!`/`///` comments with bare
# `[Type]`/`[func]` links in the three crates named above, qualify each with
# its full path per lopi-core's already-fixed precedent, confirm
# -D missing_docs also passes clean workspace-wide, then remove this line.
continue-on-error: true
run: |
RUSTDOCFLAGS="-D missing_docs -D rustdoc::broken_intra_doc_links" \
cargo doc --workspace --no-deps 2>&1 | tee rustdoc_output.txt
if grep -q "error" rustdoc_output.txt; then
echo "::error::Missing documentation on public APIs. Add /// doc comments."
exit 1
fi
# ════════════════════════════════════════════════════════════════════════════
# Gate 4b — Fuzz (Sprint S12, Phase 2) — short budget, PR-only
# ════════════════════════════════════════════════════════════════════════════
# Three targets over the parsers that eat attacker-influenceable input:
# MCP server responses (jsonrpc.rs), the `claude` CLI's own stream-json
# output (claude_events.rs), and GitHub webhook bodies pre-HMAC (github.rs).
# 60s/target on PR, deliberately short — the sprint brief's own framing:
# "A fuzz gate that adds ten minutes to every PR gets disabled, which is
# the S10 anti-goal in a different costume." A longer scheduled run
# (cron, not wired here) is the place for deeper campaigns.
#
fuzz:
name: "G4b · Fuzz (60s × 3 targets)"
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
# KNOWN DEBT, verified 2026-07-27: these harnesses (fuzz/, a separate
# cargo-fuzz workspace) were authored and reasoned through against the
# real parser APIs, but could not be compiled or run in the environment
# that wrote them (no nightly toolchain, no cargo-fuzz, no crates.io
# network access — see the commit that added this job). Next step: once
# a real CI run confirms all three targets compile and run clean, remove
# this continue-on-error and add `fuzz` to konjo-gate's `needs:` so a
# genuine crash actually blocks merge instead of being logged only.
#
# Sprint S13R, Phase C added a repo-root `rust-toolchain.toml` pinning
# 1.88.0 for the MSRV gate — rustup resolves that file for ANY cargo
# invocation anywhere under this checkout, which silently overrode
# `dtolnay/rust-toolchain@nightly`'s default and broke `cargo install
# cargo-fuzz` here (`cargo-platform@0.3.3 requires rustc 1.91`, confirmed
# via a real CI failure on PR #184 the day this job first exercised the
# new pin). Every cargo invocation below now pins `+nightly` explicitly —
# a `+toolchain` CLI override outranks a `rust-toolchain.toml` file in
# rustup's resolution order, so this job keeps using nightly regardless
# of the repo-root MSRV pin.
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
with:
shared-key: konjo-fuzz-${{ github.repository }}
workspaces: fuzz -> target
- name: Install cargo-fuzz
run: cargo +nightly install cargo-fuzz --locked
- name: Fuzz jsonrpc_response_fuzz (MCP server responses)
run: cargo +nightly fuzz run jsonrpc_response_fuzz -- -max_total_time=60
working-directory: fuzz
- name: Fuzz claude_events_fuzz (claude CLI stream-json)
run: cargo +nightly fuzz run claude_events_fuzz -- -max_total_time=60
working-directory: fuzz
- name: Fuzz github_webhook_fuzz (GitHub webhook bodies, pre-HMAC)
run: cargo +nightly fuzz run github_webhook_fuzz -- -max_total_time=60
working-directory: fuzz
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crashes
path: fuzz/artifacts/
if-no-files-found: ignore
# ════════════════════════════════════════════════════════════════════════════
# Gate K — konjo-gates (Sprint S13R, Phase A3 — "connect the pilot")
# ════════════════════════════════════════════════════════════════════════════
# Runs kiban's own CI-plane orchestrator (`bin/konjo-gates`) against
# `.konjo/profile.yml` (authored in kiban as `profiles/lopi.yml`, Phase 13 —
# see LEDGER.md's `Lopi-Gate-Reconciliation-1`). Deliberately additive, not a
# replacement for G0-G5 above: per that reconciliation, most of lopi's real
# checks stay repo-native (kiban has no detector for coverage-floor parsing,
# cognitive-complexity-from-clippy-JSON, the DRY scanner, or the soft-gate
# lint) — this job runs kiban-native gates lopi never had at all (G-POLARITY,
# the CLAUDE.md section contract, the security-glob threat-model gate, the
# one-way-door/prove trailer checks) plus one genuinely promoted repo-native
# check (`cargo-audit`, now dispatched through kiban's generic tool runner
# too — redundant with G1's own `cargo audit` step by design, the same
# "double-check" precedent `profiles/squish.yml`/`profiles/vectro.yml` set).
# `--no-self-test`: the self-test/eval-corpus gate needs a cassette recorded
# against a live model for this profile's specialist set, which no session
# has done yet for lopi (recorded in NEXT_SESSION_PROMPT.md as carried work).
# G-POLARITY and the CLAUDE.md contract both ship `advisory: true` in the
# profile (WARN, not FAIL) — the standard adoption ramp for a repo with an
# existing baseline (see the standing count recorded in LEDGER.md) — so this
# job is a HARD gate on its other checks (secrets, one-way-door, threat
# model, can-fail, the promoted cargo-audit dispatch) without retroactively
# blocking merges on the pre-existing polarity/contract baseline.
konjo-gates:
name: "GK · konjo-gates (kiban profile)"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # one_way_door/prove/threat_model read commit trailers from git log
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
shared-key: konjo-${{ github.repository }}
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install kiban at the pinned ref
env:
# Matches .konjo/kiban.ref and G0's KIBAN_REF — bump all three together.
KIBAN_REF: "v1.8.0"
run: |
pip install --quiet pyyaml
# .konjo/profile.yml's contract_gates names cargo-deny, a genuine
# _TOOL_SCOPE entry in konjo_gates_py's dispatcher (unlike e.g.
# dead-code/complexity, which are documentation-only names konjo-gates
# can't run), so it really does invoke it, not just document that lopi's
# own CI checks it too. Install alongside cargo-audit so it actually runs
# instead of ERROR-ing as "not installed" (confirmed live: PR #184's first
# run on this job). cargo-mutants is deliberately NOT installed here: the
# profile's `mutation:` field is set to a "none"-prefixed value precisely
# so it is *not* dispatched through this generic path — `newonly`'s
# line-diff net-new mechanism can't scope cargo-mutants' timing-carrying
# log output the way it can a linter's stable per-finding lines (also
# confirmed live, same PR: every run "found" net-new lines regardless of
# content). G3 above is the real, survival-rate-based mutation gate.
cargo +stable install cargo-audit --locked --quiet 2>/dev/null || true
cargo +stable install cargo-deny --locked --quiet 2>/dev/null || true
git clone --quiet --depth 1 --branch "$KIBAN_REF" https://github.com/konjoai/kiban.git /tmp/kiban
- name: Run konjo-gates against .konjo/profile.yml
run: |
PYTHONPATH=/tmp/kiban python3 /tmp/kiban/bin/konjo-gates \
--profile .konjo/profile.yml \
--base "origin/${{ github.base_ref || 'main' }}" \
--no-self-test
# ════════════════════════════════════════════════════════════════════════════
# Gate 5 — Adversarial Review (Wall 3, PRs only)
# Uses Claude Opus — distinct from builder model (Sonnet)
# ════════════════════════════════════════════════════════════════════════════
review:
name: "G5 · Adversarial Review (Wall 3)"
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
needs: [static, coverage] # only run if basic gates pass
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install anthropic SDK
# ADVISORY BY DESIGN: legitimately safe to soft-fail (not a gate, a
# prerequisite): if this fails, `_load_anthropic()` inside
# konjo_review.py raises ImportError, which main() already catches
# and treats as a soft WARNING (returns 0) rather than crashing —
# see the review step's own comment below. There is no scenario
# where this step failing silently produces a false "approved."
continue-on-error: true
run: pip install anthropic --quiet
- name: Generate diff
# ADVISORY BY DESIGN: legitimately safe to soft-fail: the next step
# defensively checks for a missing/empty pr.diff and treats that as
# "nothing to review," not a blocker — see below. Without that
# guard this would have been a real gap (a git/fetch hiccup here
# could otherwise crash the review script with an uncaught
# exception, which Python exits as code 1 — indistinguishable from
# a genuine BLOCKER).
continue-on-error: true
run: |
git diff origin/${{ github.base_ref }}...HEAD > pr.diff
echo "Diff size: $(wc -c < pr.diff) bytes"
- name: Run Konjo Adversarial Review
id: review
# ADVISORY BY DESIGN: legitimately safe to soft-fail: this step's
# own run: block never aborts (the `&& / ||` pattern below always
# captures REVIEW_EXIT and the empty-diff guard means a missing
# pr.diff can't reach the script at all), so continue-on-error here
# is a defensive no-op, not a gate. The actual gate is REVIEW_EXIT's
# value, hard-enforced by "Fail if BLOCKER verdict" below.
continue-on-error: true
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
# A diff-generation hiccup (see the step above) must read as
# "nothing to review," never as a script crash that Python would
# exit 1 for — which "Fail if BLOCKER verdict" cannot distinguish
# from a genuine blocker verdict.
if [ ! -s pr.diff ]; then
echo "REVIEW_EXIT=0" >> "$GITHUB_ENV"
echo "[konjo-review] pr.diff missing or empty — skipping review, not treating as a blocker." >&2
else
# --json writes JSON to stdout; --output writes the human report separately.
# Run twice so we get both formats: once for the human report, once for JSON.
python3 .konjo/scripts/konjo_review.py \
--diff-file pr.diff \
--output review_report.md \
2>/dev/null \
&& echo "REVIEW_EXIT=0" >> "$GITHUB_ENV" \
|| echo "REVIEW_EXIT=$?" >> "$GITHUB_ENV"
fi
cat review_report.md 2>/dev/null || echo "(no review report generated — API key may be missing)"
- name: Post review comment to PR
if: always()
continue-on-error: true # ADVISORY BY DESIGN: cosmetic UX only — a comment-post failure must never affect the verdict already captured in REVIEW_EXIT
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let body = '';
try {
body = fs.readFileSync('review_report.md', 'utf8');
} catch {
body = '<!-- konjo-review: report file not found -->';
}
const header = `<!-- konjo-adversarial-review -->
> **Konjo Adversarial Review** — Wall 3 · Model: claude-opus-5
> Independent critic session. The builder wrote the code; this reviewer came in cold.
`;
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: header + body
});
- name: Fail if BLOCKER verdict
# Non-negotiable per backend-1 Phase 0: a genuine BLOCKER must hard
# block. Distinguishes REVIEW_EXIT == 1 (the script ran to
# completion and found a real blocker — see konjo_review.py's
# main(), which is the actual source of truth, not its stale exit
# code docstring) from any other nonzero (API/config/script
# failure), which the script already treats as a soft
# "WARNING — could not reach API" and returns 0 for. That means
# this step firing at all now only happens on a genuine blocker, so
# it must not itself be soft.
if: env.REVIEW_EXIT == '1'
run: |
echo "::error::Konjo Adversarial Review found BLOCKER(s). Fix them before merging."
exit 1
# ════════════════════════════════════════════════════════════════════════════
# Final gate — merge requires ALL jobs to pass
# ════════════════════════════════════════════════════════════════════════════
konjo-gate:
name: "Konjo Gate — All Walls Clear"
runs-on: ubuntu-latest
if: always()
# backend-1 Phase 0 fix: this used to omit `mutation` and `review`
# entirely, so G3 (mutation testing) and G5 (Wall 3 adversarial review —
# including the BLOCKER hard-fail above) could fail outright without
# blocking merge. Both are added below; both are also legitimately
# `skipped` on a push-to-main run (they're PR-only jobs), which must not
# itself count as a failure.
needs: [doc-staleness, static, web-audit, coverage, complexity, mutation, review, konjo-gates]
steps:
- name: Evaluate all required gates
run: |
# Map job results to pass/fail. `skipped` is only acceptable for
# mutation/review, and only because they're gated on
# `github.event_name == 'pull_request'` — a push-to-main run is
# expected to skip them, not fail them.
DOC_STALENESS="${{ needs.doc-staleness.result }}"
STATIC="${{ needs.static.result }}"
WEB_AUDIT="${{ needs.web-audit.result }}"
COVERAGE="${{ needs.coverage.result }}"
COMPLEXITY="${{ needs.complexity.result }}"
MUTATION="${{ needs.mutation.result }}"
REVIEW="${{ needs.review.result }}"
KONJO_GATES="${{ needs.konjo-gates.result }}"
echo "doc-staleness: $DOC_STALENESS"
echo "static: $STATIC"
echo "web-audit: $WEB_AUDIT"
echo "coverage: $COVERAGE"
echo "complexity: $COMPLEXITY"
echo "mutation: $MUTATION"
echo "review: $REVIEW"
echo "konjo-gates: $KONJO_GATES"
FAILED=""
[ "$DOC_STALENESS" != "success" ] && FAILED="$FAILED doc-staleness"
[ "$STATIC" != "success" ] && FAILED="$FAILED static"
[ "$WEB_AUDIT" != "success" ] && FAILED="$FAILED web-audit"
[ "$COVERAGE" != "success" ] && FAILED="$FAILED coverage"
[ "$COMPLEXITY" != "success" ] && FAILED="$FAILED complexity"
[ "$MUTATION" != "success" ] && [ "$MUTATION" != "skipped" ] && FAILED="$FAILED mutation"
[ "$REVIEW" != "success" ] && [ "$REVIEW" != "skipped" ] && FAILED="$FAILED review"
[ "$KONJO_GATES" != "success" ] && FAILED="$FAILED konjo-gates"
if [ -n "$FAILED" ]; then
echo "::error::Gate(s) failed:$FAILED — merge blocked."
exit 1
fi
echo "All required Konjo quality gates passed ✓"
echo "The code is seaworthy."