Sprint U-0: web dashboard recon (WIP) #613
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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." |