Skip to content

fix: 保留活动会话使用的插件缓存路径 #5886

fix: 保留活动会话使用的插件缓存路径

fix: 保留活动会话使用的插件缓存路径 #5886

Workflow file for this run

name: CI
on:
push:
branches: [main, dev, experimental/dev]
pull_request:
branches: [main, dev, experimental/dev]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
name: Detect CI lanes
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
full_suite: ${{ steps.classify.outputs.full_suite }}
docs_changed: ${{ steps.classify.outputs.docs_changed }}
docs_only: ${{ steps.classify.outputs.docs_only }}
ts_changed: ${{ steps.classify.outputs.ts_changed }}
rust_changed: ${{ steps.classify.outputs.rust_changed }}
native_changed: ${{ steps.classify.outputs.native_changed }}
shared_config_changed: ${{ steps.classify.outputs.shared_config_changed }}
reason: ${{ steps.classify.outputs.reason }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Classify changed paths
id: classify
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref || '' }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha || '' }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || '' }}
PUSH_BEFORE_SHA: ${{ github.event.before || '' }}
PUSH_AFTER_SHA: ${{ github.sha }}
run: |
set -euo pipefail
full_suite=false
reason=targeted
base_sha=""
head_sha="$PUSH_AFTER_SHA"
if [[ "$EVENT_NAME" == "pull_request" ]]; then
base_sha="$PR_BASE_SHA"
head_sha="$PR_HEAD_SHA"
if [[ "$PR_BASE_REF" == "main" ]]; then
full_suite=true
reason="pull request targets main"
fi
elif [[ "$EVENT_NAME" == "push" ]]; then
base_sha="$PUSH_BEFORE_SHA"
head_sha="$PUSH_AFTER_SHA"
if [[ "$REF_NAME" == "main" ]]; then
full_suite=true
reason="push to main"
fi
else
full_suite=true
reason="unknown event: $EVENT_NAME"
fi
if [[ -z "$base_sha" || "$base_sha" =~ ^0+$ || -z "$head_sha" ]]; then
full_suite=true
reason="missing or first-push base sha"
fi
: > changed-files.tsv
if [[ "$full_suite" == "false" ]]; then
if ! git cat-file -e "$base_sha^{commit}" || ! git cat-file -e "$head_sha^{commit}"; then
full_suite=true
reason="unable to resolve diff commits"
else
git diff --name-status "$base_sha" "$head_sha" > changed-files.tsv
fi
fi
export full_suite reason
node <<'NODE'
const fs = require('node:fs');
const lines = fs.readFileSync('changed-files.tsv', 'utf8')
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const out = {
full_suite: process.env.full_suite === 'true',
docs_changed: false,
docs_only: false,
ts_changed: false,
rust_changed: false,
native_changed: false,
shared_config_changed: false,
reason: process.env.reason || 'targeted',
};
function addOutput(key, value) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`);
}
function pathsFromNameStatus(line) {
const parts = line.split(/\t+/);
const status = parts[0] || '';
if (/^[RC]/.test(status)) return parts.slice(1, 3).filter(Boolean);
return parts.slice(1, 2).filter(Boolean);
}
function isDocs(path) {
return /^docs\//.test(path)
|| /^missions\//.test(path)
|| /^\.github\/(ISSUE_TEMPLATE\/|PULL_REQUEST_TEMPLATE\.md$)/.test(path)
|| /^(README|CHANGELOG|CONTRIBUTING|COVERAGE|DEMO|RELEASE_BODY|RELEASE_PROTOCOL)\.md$/.test(path);
}
function isTs(path) {
return /^src\/.*\.(ts|tsx|mts|cts)$/.test(path)
|| /^src\/compat\/fixtures\//.test(path)
|| /^playground\/.*\.(ts|tsx|js|mjs|cjs|json)$/.test(path);
}
function isRust(path) {
return /^crates\//.test(path) || path === 'Cargo.lock' || path === 'Cargo.toml';
}
function isNative(path) {
return path === 'dist-workspace.toml'
|| /^crates\/omx-(api|explore|mux|runtime-core|runtime|sparkshell)\//.test(path);
}
function isSharedConfig(path) {
return /^\.github\//.test(path)
|| /^templates\//.test(path)
|| /^prompts\//.test(path)
|| /^skills\//.test(path)
|| /^plugins\//.test(path)
|| /^biome\.json$/.test(path)
|| /^tsconfig.*\.json$/.test(path)
|| /^package(-lock)?\.json$/.test(path)
|| path === 'Cargo.lock'
|| path === 'Cargo.toml'
|| path === 'dist-workspace.toml';
}
function isGjcPlanningEvidence(path) {
return /^\.gjc\/(plans|ultragoal|quality-gates?|planning-evidence)\//.test(path)
|| /^\.gjc\/[^/]+\/(quality-gates?|planning-evidence)\//.test(path)
|| /^\.gjc\/[^/]+\/(prd|test-spec|plan|quality-gate)[^/]*\.(md|json|jsonl|txt)$/.test(path);
}
function isGjcRuntimeAffecting(path) {
return /^\.gjc\/(runtime|state|sessions|team|hooks|notifications)\//.test(path)
|| /^\.gjc\/(config|settings|policy)\.(json|toml|ya?ml)$/.test(path);
}
const paths = lines.flatMap(pathsFromNameStatus);
if (paths.length === 0 && !out.full_suite) {
out.full_suite = true;
out.reason = 'empty diff or classifier uncertainty';
}
for (const path of paths) {
if (isDocs(path)) out.docs_changed = true;
if (isTs(path)) out.ts_changed = true;
if (isRust(path)) out.rust_changed = true;
if (isNative(path)) out.native_changed = true;
if (isSharedConfig(path)) out.shared_config_changed = true;
if (isGjcRuntimeAffecting(path)) out.ts_changed = true;
}
const allDocs = paths.length > 0 && paths.every(isDocs);
out.docs_only = allDocs;
const allGjcPlanningEvidence = paths.length > 0 && paths.every(isGjcPlanningEvidence);
const gjcRuntimePaths = paths.filter(isGjcRuntimeAffecting);
if (!out.full_suite && allGjcPlanningEvidence) {
out.reason = 'gjc planning/evidence artifacts only';
} else if (!out.full_suite && gjcRuntimePaths.length > 0) {
out.reason = `gjc runtime-affecting artifact(s): ${gjcRuntimePaths.join(', ')}`;
}
// Fail closed for shared config, workflow, version, lockfile, native packaging, or ambiguous repo assets.
const known = (path) => isDocs(path) || isTs(path) || isRust(path) || isNative(path) || isSharedConfig(path) || isGjcPlanningEvidence(path) || isGjcRuntimeAffecting(path);
const unknown = paths.filter((path) => !known(path));
if (out.shared_config_changed || unknown.length > 0) {
out.full_suite = true;
out.reason = out.shared_config_changed
? 'shared config, workflow, manifest, lockfile, or packaging change'
: `unclassified path(s): ${unknown.join(', ')}`;
}
for (const [key, value] of Object.entries(out)) addOutput(key, value);
NODE
- name: Summarize lane decision
run: |
{
echo '## CI lane decision'
echo ''
echo '- full_suite: `${{ steps.classify.outputs.full_suite }}`'
echo '- docs_changed: `${{ steps.classify.outputs.docs_changed }}`'
echo '- docs_only: `${{ steps.classify.outputs.docs_only }}`'
echo '- ts_changed: `${{ steps.classify.outputs.ts_changed }}`'
echo '- rust_changed: `${{ steps.classify.outputs.rust_changed }}`'
echo '- native_changed: `${{ steps.classify.outputs.native_changed }}`'
echo '- shared_config_changed: `${{ steps.classify.outputs.shared_config_changed }}`'
echo '- reason: `${{ steps.classify.outputs.reason }}`'
} >> "$GITHUB_STEP_SUMMARY"
docs-check:
name: Docs Check
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [changes]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.docs_changed == 'true' }}
steps:
- uses: actions/checkout@v7
- name: Check documentation whitespace
run: git diff --check HEAD~1..HEAD || git diff --check
rustfmt:
name: Rust Format
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [changes]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.rust_changed == 'true' || needs.changes.outputs.native_changed == 'true' }}
steps:
- uses: actions/checkout@v7
- name: Setup Rust
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
components: rustfmt
- name: Check Rust formatting
run: cargo fmt --all --check
clippy:
name: Rust Clippy
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [changes]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.rust_changed == 'true' || needs.changes.outputs.native_changed == 'true' }}
steps:
- uses: actions/checkout@v7
- name: Install Rust build prerequisites
run: .github/scripts/install-prereqs.sh build-essential -- cc
- name: Setup Rust
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
components: clippy
- name: Restore Rust build cache
uses: Swatinem/rust-cache@v2
- name: Run clippy
run: cargo clippy --workspace --all-targets -- -D warnings
rust-tests:
name: Rust Tests + Coverage Signal
runs-on: ubuntu-latest
timeout-minutes: 45
needs: [changes]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.rust_changed == 'true' || needs.changes.outputs.native_changed == 'true' }}
steps:
- uses: actions/checkout@v7
- name: Install Rust build prerequisites
run: .github/scripts/install-prereqs.sh build-essential -- cc
- name: Setup Rust
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
components: llvm-tools-preview
- name: Restore Rust build cache
uses: Swatinem/rust-cache@v2
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run Rust tests with coverage summaries
shell: bash
run: |
mkdir -p coverage/rust
cargo llvm-cov --workspace --summary-only | tee coverage/rust/workspace-summary.txt
cargo llvm-cov --manifest-path crates/omx-sparkshell/Cargo.toml --summary-only | tee coverage/rust/omx-sparkshell-summary.txt
- name: Add Rust coverage summary to job summary
shell: bash
run: |
{
echo '## Rust Test Coverage Summary'
echo ''
echo '### Workspace'
echo '```text'
cat coverage/rust/workspace-summary.txt
echo '```'
echo ''
echo '### omx-sparkshell manifest'
echo '```text'
cat coverage/rust/omx-sparkshell-summary.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [changes]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.ts_changed == 'true' || needs.changes.outputs.shared_config_changed == 'true' }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
typecheck:
name: Typecheck
runs-on: ubuntu-latest
timeout-minutes: 25
needs: [changes]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.ts_changed == 'true' || needs.changes.outputs.shared_config_changed == 'true' }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx tsc --noEmit
- run: npm run check:no-unused
build-dist:
name: Build dist artifact
runs-on: ubuntu-latest
timeout-minutes: 25
needs: [changes]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.ts_changed == 'true' || needs.changes.outputs.rust_changed == 'true' || needs.changes.outputs.native_changed == 'true' || needs.changes.outputs.shared_config_changed == 'true' }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- name: Upload prebuilt dist artifact
uses: actions/upload-artifact@v4
with:
name: ci-dist-node20
path: dist/
if-no-files-found: error
ralplan-preflight-macos:
name: Ralplan preflight security (macOS)
runs-on: macos-latest
timeout-minutes: 30
needs: [changes]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.ts_changed == 'true' || needs.changes.outputs.shared_config_changed == 'true' }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- name: Verify Darwin descriptor-relative neutralization
run: |
node --test dist/ralplan/__tests__/documented-leader-preflight.test.js
node --test --test-name-pattern='does not reactivate a neutralized routing-only Ralplan seed' dist/hooks/__tests__/keyword-detector.test.js
node --test --test-name-pattern='does not advertise a neutralized Ralplan seed in SessionStart context' dist/scripts/__tests__/codex-native-hook.test.js
test:
name: Test (Node ${{ matrix.node-version }} / ${{ matrix.lane }})
runs-on: ubuntu-latest
timeout-minutes: 60
needs: [changes, build-dist]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.ts_changed == 'true' || needs.changes.outputs.shared_config_changed == 'true' }}
strategy:
fail-fast: false
matrix:
include:
- node-version: 20
lane: team-state-runtime
test-roots: >-
dist/team/__tests__
dist/state/__tests__
dist/ralph/__tests__
dist/ralplan/__tests__
dist/runtime/__tests__
catalog-check: false
- node-version: 20
lane: hooks-notify-platform
test-roots: >-
dist/hooks/__tests__
dist/hooks/code-simplifier/__tests__
dist/hooks/extensibility/__tests__
dist/notifications/__tests__
dist/mcp/__tests__
dist/hud/__tests__
dist/verification/__tests__
dist/openclaw/__tests__
catalog-check: false
- node-version: 20
lane: cli-core-rest
test-roots: >-
dist/cli/__tests__
dist/agents/__tests__
dist/autoresearch/__tests__
dist/catalog/__tests__
dist/compat/__tests__
dist/config/__tests__
dist/modes/__tests__
dist/pipeline/__tests__
dist/planning/__tests__
dist/scripts/__tests__
dist/session-history/__tests__
dist/subagents/__tests__
dist/utils/__tests__
dist/visual/__tests__
catalog-check: true
- node-version: 22
lane: smoke
catalog-check: false
steps:
- uses: actions/checkout@v7
- name: Install test prerequisites
run: .github/scripts/install-prereqs.sh build-essential tmux -- cc tmux
- name: Setup Rust for harness-backed Node tests
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- name: Download prebuilt dist artifact
uses: actions/download-artifact@v8
with:
name: ci-dist-node20
path: dist
- name: Run grouped full-suite lane
if: matrix.lane != 'smoke'
env:
# Self-hosted runner lanes can leave platform/process handles alive
# after all tests report. Force exit only after Node's test runner
# finishes so successful suites do not hang indefinitely.
OMX_NODE_TEST_FORCE_EXIT: ${{ (matrix.lane == 'team-state-runtime' || matrix.lane == 'hooks-notify-platform') && '1' || '' }}
run: |
node dist/scripts/run-test-files.js ${{ matrix.test-roots }}
- name: Verify generated catalog docs
if: matrix.catalog-check == true
run: node dist/scripts/generate-catalog-docs.js --check
- name: Run cross-rebase smoke command
if: matrix.lane == 'smoke'
run: npm run test:team:cross-rebase-smoke:compiled
- name: Run remaining smoke suite
if: matrix.lane == 'smoke'
run: |
node --test \
dist/cli/__tests__/packaged-script-resolution.test.js \
dist/cli/__tests__/package-bin-contract.test.js \
dist/cli/__tests__/explore.test.js \
dist/cli/__tests__/sparkshell-cli.test.js \
dist/hooks/__tests__/explore-routing.test.js \
dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js \
dist/scripts/__tests__/smoke-packed-install.test.js \
dist/verification/__tests__/explore-harness-release-workflow.test.js \
dist/compat/__tests__/*.test.js
coverage-team-critical:
name: Coverage Gate (Team Critical)
runs-on: ubuntu-latest
timeout-minutes: 45
needs: [changes, build-dist]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.ts_changed == 'true' || needs.changes.outputs.shared_config_changed == 'true' }}
steps:
- uses: actions/checkout@v7
- name: Install test prerequisites
run: .github/scripts/install-prereqs.sh tmux -- tmux
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
- run: npm ci
- name: Download prebuilt dist artifact
uses: actions/download-artifact@v8
with:
name: ci-dist-node20
path: dist
- name: Run team/state coverage gate
run: npm run coverage:team-critical:compiled
- name: Add coverage summary to job summary
run: |
node -e '
const fs = require("node:fs");
const summary = JSON.parse(fs.readFileSync("coverage/team/coverage-summary.json", "utf8"));
const total = summary.total;
const lines = [
"## Team/State Coverage Summary",
"",
"| Metric | Pct | Covered / Total |",
"|---|---:|---:|",
`| Lines | ${total.lines.pct}% | ${total.lines.covered} / ${total.lines.total} |`,
`| Functions | ${total.functions.pct}% | ${total.functions.covered} / ${total.functions.total} |`,
`| Branches | ${total.branches.pct}% | ${total.branches.covered} / ${total.branches.total} |`,
`| Statements | ${total.statements.pct}% | ${total.statements.covered} / ${total.statements.total} |`
];
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${lines.join("\n")}\n`);
'
- name: Upload team coverage artifact
uses: actions/upload-artifact@v4
with:
name: team-critical-coverage
path: coverage/team/
ralph-persistence-gate:
name: Ralph Persistence Gate
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [changes, build-dist]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.ts_changed == 'true' || needs.changes.outputs.shared_config_changed == 'true' }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
- run: npm ci
- name: Download prebuilt dist artifact
uses: actions/download-artifact@v8
with:
name: ci-dist-node20
path: dist
- name: Run Ralph persistence verification matrix
run: npm run test:ralph-persistence:compiled
build:
name: Build (Full Source Build)
runs-on: ubuntu-latest
timeout-minutes: 45
needs: [changes, build-dist]
if: ${{ needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.rust_changed == 'true' || needs.changes.outputs.native_changed == 'true' || needs.changes.outputs.shared_config_changed == 'true' }}
steps:
- uses: actions/checkout@v7
- name: Install Rust build prerequisites
run: .github/scripts/install-prereqs.sh build-essential -- cc
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
- name: Setup Rust
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
- name: Restore Rust build cache
uses: Swatinem/rust-cache@v2
- run: npm ci
- name: Download prebuilt dist artifact
uses: actions/download-artifact@v8
with:
name: ci-dist-node20
path: dist
- run: npm run build:explore:release
- run: npm run build:sparkshell
- run: npm run build:api
ci-status:
name: CI Status
if: always()
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [changes, docs-check, rustfmt, clippy, rust-tests, lint, typecheck, build-dist, ralplan-preflight-macos, test, coverage-team-critical, ralph-persistence-gate, build]
steps:
- name: Check required job results
shell: bash
env:
FULL_SUITE: ${{ needs.changes.outputs.full_suite }}
DOCS_CHANGED: ${{ needs.changes.outputs.docs_changed }}
TS_CHANGED: ${{ needs.changes.outputs.ts_changed }}
RUST_CHANGED: ${{ needs.changes.outputs.rust_changed }}
NATIVE_CHANGED: ${{ needs.changes.outputs.native_changed }}
SHARED_CONFIG_CHANGED: ${{ needs.changes.outputs.shared_config_changed }}
CHANGES_RESULT: ${{ needs.changes.result }}
DOCS_CHECK_RESULT: ${{ needs.docs-check.result }}
RUSTFMT_RESULT: ${{ needs.rustfmt.result }}
CLIPPY_RESULT: ${{ needs.clippy.result }}
RUST_TESTS_RESULT: ${{ needs.rust-tests.result }}
LINT_RESULT: ${{ needs.lint.result }}
TYPECHECK_RESULT: ${{ needs.typecheck.result }}
BUILD_DIST_RESULT: ${{ needs.build-dist.result }}
RALPLAN_PREFLIGHT_MACOS_RESULT: ${{ needs.ralplan-preflight-macos.result }}
TEST_RESULT: ${{ needs.test.result }}
COVERAGE_TEAM_CRITICAL_RESULT: ${{ needs.coverage-team-critical.result }}
RALPH_PERSISTENCE_GATE_RESULT: ${{ needs.ralph-persistence-gate.result }}
BUILD_RESULT: ${{ needs.build.result }}
REASON: ${{ needs.changes.outputs.reason }}
run: |
set -euo pipefail
failures=0
failing_active_gates=()
unexpected_inactive_gates=()
bool_any() {
for value in "$@"; do
[[ "$value" == "true" ]] && return 0
done
return 1
}
check_job() {
local name="$1"
local result="$2"
local active="$3"
if [[ "$active" == "true" ]]; then
if [[ "$result" != "success" ]]; then
echo "::error::$name must succeed when active, got $result"
failing_active_gates+=("$name=$result")
failures=$((failures + 1))
fi
else
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
echo "::error::$name inactive lane should be success or skipped, got $result"
unexpected_inactive_gates+=("$name=$result")
failures=$((failures + 1))
fi
fi
}
echo "CI lane reason: ${REASON:-unknown}"
echo " full_suite: $FULL_SUITE"
echo " docs_changed: $DOCS_CHANGED"
echo " ts_changed: $TS_CHANGED"
echo " rust_changed: $RUST_CHANGED"
echo " native_changed: $NATIVE_CHANGED"
echo " shared_config_changed: $SHARED_CONFIG_CHANGED"
echo " changes: $CHANGES_RESULT"
echo " docs-check: $DOCS_CHECK_RESULT"
echo " rustfmt: $RUSTFMT_RESULT"
echo " clippy: $CLIPPY_RESULT"
echo " rust-tests: $RUST_TESTS_RESULT"
echo " lint: $LINT_RESULT"
echo " typecheck: $TYPECHECK_RESULT"
echo " build-dist: $BUILD_DIST_RESULT"
echo " ralplan-preflight-macos: $RALPLAN_PREFLIGHT_MACOS_RESULT"
echo " test: $TEST_RESULT"
echo " coverage-team-critical: $COVERAGE_TEAM_CRITICAL_RESULT"
echo " ralph-persistence-gate: $RALPH_PERSISTENCE_GATE_RESULT"
echo " build: $BUILD_RESULT"
check_job changes "$CHANGES_RESULT" true
docs_active=false
rust_active=false
ts_active=false
dist_active=false
build_active=false
if [[ "$FULL_SUITE" == "true" ]] || [[ "$DOCS_CHANGED" == "true" ]]; then docs_active=true; fi
if [[ "$FULL_SUITE" == "true" ]] || bool_any "$RUST_CHANGED" "$NATIVE_CHANGED"; then rust_active=true; fi
if [[ "$FULL_SUITE" == "true" ]] || bool_any "$TS_CHANGED" "$SHARED_CONFIG_CHANGED"; then ts_active=true; fi
if [[ "$FULL_SUITE" == "true" ]] || bool_any "$TS_CHANGED" "$RUST_CHANGED" "$NATIVE_CHANGED" "$SHARED_CONFIG_CHANGED"; then dist_active=true; fi
if [[ "$FULL_SUITE" == "true" ]] || bool_any "$RUST_CHANGED" "$NATIVE_CHANGED" "$SHARED_CONFIG_CHANGED"; then build_active=true; fi
check_job docs-check "$DOCS_CHECK_RESULT" "$docs_active"
check_job rustfmt "$RUSTFMT_RESULT" "$rust_active"
check_job clippy "$CLIPPY_RESULT" "$rust_active"
check_job rust-tests "$RUST_TESTS_RESULT" "$rust_active"
check_job lint "$LINT_RESULT" "$ts_active"
check_job typecheck "$TYPECHECK_RESULT" "$ts_active"
check_job build-dist "$BUILD_DIST_RESULT" "$dist_active"
check_job ralplan-preflight-macos "$RALPLAN_PREFLIGHT_MACOS_RESULT" "$ts_active"
check_job test "$TEST_RESULT" "$ts_active"
check_job coverage-team-critical "$COVERAGE_TEAM_CRITICAL_RESULT" "$ts_active"
check_job ralph-persistence-gate "$RALPH_PERSISTENCE_GATE_RESULT" "$ts_active"
check_job build "$BUILD_RESULT" "$build_active"
if (( failures > 0 )); then
if (( ${#failing_active_gates[@]} > 0 )); then
echo "::error::Actual failing active CI gate(s): ${failing_active_gates[*]}"
fi
if (( ${#unexpected_inactive_gates[@]} > 0 )); then
echo "::error::Unexpected inactive CI gate result(s): ${unexpected_inactive_gates[*]}"
fi
echo "::error::Lane-selection reason: ${REASON:-unknown}; full_suite=${FULL_SUITE:-unknown}"
echo "::error::$failures CI status requirement(s) failed"
exit 1
fi
echo "All required CI checks passed for active lanes"