-
Notifications
You must be signed in to change notification settings - Fork 0
1070 lines (983 loc) · 56 KB
/
Copy pathkonjo-gate.yml
File metadata and controls
1070 lines (983 loc) · 56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: "Konjo Quality Gate — Wall 2"
on:
pull_request:
branches: [main]
push:
branches: [main]
# Opt-in only -- review-pipeline Sprint P2b, section 4's real call site for
# `konjo/mutation-hunt`. Not a default gate on every PR: the loop spends real model
# tokens per round (see .konjo/kiban's plugins/konjo/skills/mutation-hunt/SKILL.md),
# so it runs only when a human explicitly dispatches it against one crate.
workflow_dispatch:
inputs:
crate:
description: "Crate to run the mutation-hunt loop against (e.g. lopi-ratelimit)"
required: true
type: string
round_cap:
description: "Max rounds before the loop stops and suggests a waiver"
required: false
default: "3"
type: string
diff_base_ref:
description: "Branch the production change under test is diffed against (cargo mutants --in-diff scope) -- must NOT be the same commit as the checked-out branch, or --in-diff sees no production lines changed"
required: false
default: "main"
type: string
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.19.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
with:
fetch-depth: 0 # Gate-Tiering-1, A4: scope_assert.py's --base-ref needs the real merge-base
- 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.
#
# Gate-Tiering-1, A4: diff-scoped to the PR's own changed files via
# --base-ref on a pull_request run — a BLOCKING gate must not fail a
# PR over a pre-existing term elsewhere in the tree that PR never
# touched. Push-to-main runs (no base ref) keep the full-tree scan.
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
python3 .konjo/scripts/scope_assert.py --base-ref "origin/${{ github.base_ref }}"
else
python3 .konjo/scripts/scope_assert.py
fi
- 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
# HARD GATE — Gate-Tiering-1. Previously carried `--fail-under-lines
# 80` and `continue-on-error: true` together (backend-1 Phase 0),
# which meant a genuine compile/test failure here was ALSO swallowed
# by the same softness that was only meant to cover the coverage
# percentage being below floor — "the tests pass" is not negotiable
# (A3, this sprint) and must block on its own exit code. The 80%/95%
# threshold check is `--fail-under-lines`'s job; it now lives only in
# the "Coverage gate" step below, which stays soft, so a low
# percentage can't masquerade as a build/test break here.
run: |
cargo llvm-cov nextest \
--workspace \
--all-features \
--lcov \
--output-path lcov.info
- 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-08-11 (Gate-Tiering-1, re-verifying
# backend-1 Phase 0's original finding): 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 promote this step back to BLOCKING once
# kiban's B2 promotion criteria (passing kill-test + a measured
# false-positive rate below its stated ceiling) are met for it —
# until then it stays ADVISORY tier per LEDGER.md's Gate-Tiering-1
# entry.
#
# Also fixed 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)
# KNOWN DEBT, verified 2026-08-11 (Gate-Tiering-1): was a HARD GATE
# under Konjo Forward Pillar 2 ("main is truth, gates only move up")
# — that principle is unchanged, but A3 of this sprint's brief
# explicitly scopes `coverage`'s BLOCKING authority down to "the
# tests pass" only, so this percentage-based ratchet moves to
# ADVISORY tier alongside the 80%/95% gate above, not because the
# ratchet itself is wrong. Next step: once the 80%/95% gate above is
# promoted back to BLOCKING (kiban B2 criteria: passing kill-test +
# measured false-positive rate under the stated ceiling), promote
# this floor-ratchet alongside it — a floor is only meaningful once
# the thing it floors is enforced again.
#
# 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.
continue-on-error: true
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
with:
fetch-depth: 0 # Gate-Tiering-1, A4: dry_check.py/function_length_check.py/indexing_floor_check.py's --base-ref need the real merge-base (also fixes the pre-existing file-size gate's own base-ref diff below, which needed this all along)
- 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
# ADVISORY tier as of Gate-Tiering-1 (this job, G4, is entirely
# advisory — see LEDGER.md and konjo-gate.yml's aggregator comment).
# Diff-scoped on PR runs (A4): `--changed-only --base-ref` instead of
# a full-tree scan, so the report reflects what the PR touched.
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
python3 .konjo/scripts/dry_check.py \
--threshold 0.85 \
--min-lines 20 \
--changed-only --base-ref "origin/${{ github.base_ref }}" \
--report dry_report.json 2>&1
else
python3 .konjo/scripts/dry_check.py \
--threshold 0.85 \
--min-lines 20 \
--report dry_report.json 2>&1
fi
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)
# ADVISORY tier as of Gate-Tiering-1 (this job, G4, is entirely
# advisory). Sprint S13R, Phase B, decision item 3's original
# reasoning is unchanged below; only the tier changed. 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. 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.
#
# A4: diff-scoped to the PR's own changed files via --base-ref on a
# pull_request run — the ceiling file still holds the full-tree
# ratchet value that `main` continues to update on push, so a
# diff-scoped PR count compared against it is a lighter-weight
# PR-time signal, not the authoritative ratchet check.
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
python3 .konjo/scripts/function_length_check.py \
--ceiling-file .konjo/function-length-ceiling.txt \
--base-ref "origin/${{ github.base_ref }}"
else
python3 .konjo/scripts/function_length_check.py \
--ceiling-file .konjo/function-length-ceiling.txt
fi
- name: Indexing floor gate (raw `[0]`/`[1]` sites, ratchet against the locked ceiling)
# ADVISORY tier as of Gate-Tiering-1 (this job, G4, is entirely
# advisory). Sprint S13R, Phase D's original reasoning is unchanged
# below; only the tier changed. 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"). Ratchet down as sites
# convert to `.first()`/`.get(n)`.
#
# A4: diff-scoped to the PR's own changed files via --base-ref on a
# pull_request run — see the function-length gate's comment above
# for the same tradeoff against the full-tree-calibrated ceiling.
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
python3 .konjo/scripts/indexing_floor_check.py \
--ceiling-file .konjo/indexing-floor.txt \
--base-ref "origin/${{ github.base_ref }}"
else
python3 .konjo/scripts/indexing_floor_check.py \
--ceiling-file .konjo/indexing-floor.txt
fi
- 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.19.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
# ADVISORY BY DESIGN — Gate-Tiering-1: demoted from a hard block
# (backend-1 Phase 0's "non-negotiable" framing, kept below for
# history). A single nondeterministic LLM verdict with no measured
# false-positive rate must not hold unappealable authority over
# merge; the verdict is still fully visible in the posted PR comment
# above and in REVIEW_EXIT, and `review` is still named in the
# aggregator's ADVISORY report (see gate_verdict.sh) whenever it
# doesn't succeed.
#
# Original backend-1 Phase 0 reasoning, preserved: 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.
continue-on-error: true
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 BLOCKING-tier jobs to pass
# ════════════════════════════════════════════════════════════════════════════
# 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 in that job) could fail outright without blocking merge.
# Both were added to `needs:` 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.
#
# Gate-Tiering-1: that all-eight-must-succeed rule is exactly the problem
# that sprint fixed. `needs:` is unchanged on purpose — every job below
# still *runs* and is still visible in the checks UI — but the step's
# verdict logic (now `.konjo/scripts/gate_verdict.sh`, extracted so it's
# testable outside Actions — see `test_gate_tiering_killtest.sh`) now
# reads two named tiers instead of one flat list:
#
# BLOCKING — deterministic, fast, catches real breakage:
# static (fmt/clippy/audit/deny/dead-code/scope/soft-gate-lint),
# coverage (test-failure only — see that job's own comments for why the
# 80%-floor and floor-ratchet steps inside it are now soft)
#
# ADVISORY — reports, annotates, never blocks:
# doc-staleness, web-audit, complexity, mutation, review, konjo-gates
#
# A `gate:override` PR label plus a `Konjo-Override: <reason>` trailer in
# the PR body is the documented break-glass past a BLOCKING failure — see
# gate_verdict.sh's own header. LEDGER.md's `Gate-Tiering-1` entry has the
# full per-job tier rationale and the promotion criteria back to BLOCKING.
konjo-gate:
name: "Konjo Gate — All Walls Clear"
runs-on: ubuntu-latest
if: always()
needs: [doc-staleness, static, web-audit, coverage, complexity, mutation, review, konjo-gates]
steps:
- uses: actions/checkout@v4
- name: Evaluate all required gates
env:
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 }}
GATE_OVERRIDE_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'gate:override') }}
PR_BODY: ${{ github.event.pull_request.body }}
ACTOR: ${{ github.actor }}
run: bash .konjo/scripts/gate_verdict.sh