@codyswann/lisa 4.55.2 → 4.55.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/check-orphaned-branches.mjs +49 -13
- package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +9 -1
- package/all/copy-overwrite/scripts/lisa-hooks/block-blind-automerge.sh +10 -5
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +4 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +73 -2
- package/all/copy-overwrite/scripts/lisa-hooks/guard-dedupe.bash +63 -29
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +2 -1
- package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.sh +2 -1
- package/all/copy-overwrite/scripts/lisa-run-gates.mjs +7 -2
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +50 -14
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +36 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +92 -80
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-managed-file-edits.sh +76 -2
- package/dist/opencode/plugin-templates/block-no-verify.sh +3 -0
- package/dist/opencode/plugin-templates/guard-dedupe.bash +66 -29
- package/dist/opencode/plugin-templates/parity-safety-net-heredoc.py +3 -0
- package/dist/opencode/plugin-templates/parity-safety-net.sh +5 -1
- package/dist/opencode/support-file-resolver.d.ts.map +1 -1
- package/dist/opencode/support-file-resolver.js +3 -1
- package/dist/opencode/support-file-resolver.js.map +1 -1
- package/expo/copy-overwrite/plugins/withAndroidSplashNoClientExit.js +23 -21
- package/expo/create-only/.github/required-checks.json +1 -1
- package/nestjs/create-only/.github/required-checks.json +1 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +38 -15
- package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +15 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +37 -13
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-write-ticket/SKILL.md +15 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +56 -7
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +36 -11
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-prd-intake/SKILL.md +12 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +1 -18
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-verify/SKILL.md +0 -17
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +18 -35
- package/plugins/lisa/.codex-plugin/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +66 -12
- package/plugins/lisa/.codex-plugin/skills/lisa-rework-triage/SKILL.md +2 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-linear/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-track/SKILL.md +32 -4
- package/plugins/lisa/.codex-plugin/skills/lisa-tracker-build-intake/SKILL.md +15 -1
- package/plugins/lisa/hooks/block-blind-automerge.sh +13 -5
- package/plugins/lisa/hooks/block-direct-issue-create.sh +7 -0
- package/plugins/lisa/hooks/block-instruction-file-edits.sh +3 -0
- package/plugins/lisa/hooks/block-managed-file-edits.sh +76 -2
- package/plugins/lisa/hooks/block-no-verify.sh +3 -0
- package/plugins/lisa/hooks/block-shell-json-parsing.sh +3 -0
- package/plugins/lisa/hooks/guard-dedupe.bash +66 -29
- package/plugins/lisa/hooks/parity-safety-net-heredoc.py +3 -0
- package/plugins/lisa/hooks/parity-safety-net.sh +5 -1
- package/plugins/lisa/hooks/worktree-binding-guard.mjs +3 -0
- package/plugins/lisa/hooks/worktree-binding-guard.sh +5 -1
- package/plugins/lisa/rules/eager/00-rule-index.md +1 -0
- package/plugins/lisa/rules/eager/base-rules.md +3 -1
- package/plugins/lisa/rules/eager/withdrawn-rulings.md +14 -3
- package/plugins/lisa/rules/reference/automation-runbook-contract.md +1 -1
- package/plugins/lisa/rules/reference/bdd-e2e-coverage.md +1 -1
- package/plugins/lisa/rules/reference/blocker-containment.md +3 -3
- package/plugins/lisa/rules/reference/claim-archaeology.md +1 -1
- package/plugins/lisa/rules/reference/claim-evidence-mapping.md +1 -1
- package/plugins/lisa/rules/reference/claim-time-guards.md +1 -1
- package/plugins/lisa/rules/reference/control-reachability.md +1 -1
- package/plugins/lisa/rules/reference/convergent-review.md +1 -1
- package/plugins/lisa/rules/reference/credential-substrate-precedence.md +1 -1
- package/plugins/lisa/rules/reference/dependency-decision-records.md +1 -1
- package/plugins/lisa/rules/reference/dependency-internalization-kit.md +1 -1
- package/plugins/lisa/rules/reference/dependency-trust-classes.md +1 -1
- package/plugins/lisa/rules/reference/deployed-state-readback.md +1 -1
- package/plugins/lisa/rules/reference/derived-branch-plan.md +1 -1
- package/plugins/lisa/rules/reference/design-source-of-truth.md +1 -1
- package/plugins/lisa/rules/reference/design-value-binding.md +1 -1
- package/plugins/lisa/rules/reference/documentation-source-paths.md +1 -1
- package/plugins/lisa/rules/reference/leaf-only-lifecycle.md +1 -1
- package/plugins/lisa/rules/reference/observability-audit.md +1 -1
- package/plugins/lisa/rules/reference/prd-definition-of-ready.md +1 -1
- package/plugins/lisa/rules/reference/prd-lifecycle-rollup.md +1 -1
- package/plugins/lisa/rules/reference/pre-flight-autofill.md +1 -1
- package/plugins/lisa/rules/reference/promotion-contract.md +1 -1
- package/plugins/lisa/rules/reference/readiness-rubric.md +1 -1
- package/plugins/lisa/rules/reference/ready-role-filing.md +37 -1
- package/plugins/lisa/rules/reference/rejection-detection.md +1 -1
- package/plugins/lisa/rules/reference/repo-scope-split.md +9 -10
- package/plugins/lisa/rules/reference/reset-seed-coverage.md +1 -1
- package/plugins/lisa/rules/reference/usage-accounting.md +1 -1
- package/plugins/lisa/rules/reference/work-item-definition-of-ready.md +1 -1
- package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +85 -17
- package/plugins/lisa/scripts/qa-signal-lifecycle.mjs +5 -2
- package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +38 -15
- package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +15 -1
- package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +37 -13
- package/plugins/lisa/skills/lisa-jira-write-ticket/SKILL.md +15 -1
- package/plugins/lisa/skills/lisa-linear-access/SKILL.md +56 -7
- package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +36 -11
- package/plugins/lisa/skills/lisa-linear-prd-intake/SKILL.md +12 -2
- package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +1 -18
- package/plugins/lisa/skills/lisa-linear-verify/SKILL.md +0 -17
- package/plugins/lisa/skills/lisa-linear-write-issue/SKILL.md +18 -35
- package/plugins/lisa/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +66 -12
- package/plugins/lisa/skills/lisa-rework-triage/SKILL.md +2 -1
- package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
- package/plugins/lisa/skills/lisa-setup-linear/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
- package/plugins/lisa/skills/lisa-track/SKILL.md +32 -4
- package/plugins/lisa/skills/lisa-tracker-build-intake/SKILL.md +15 -1
- package/plugins/lisa-agy/hooks/block-blind-automerge.sh +13 -5
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +7 -0
- package/plugins/lisa-agy/hooks/block-instruction-file-edits.sh +3 -0
- package/plugins/lisa-agy/hooks/block-managed-file-edits.sh +76 -2
- package/plugins/lisa-agy/hooks/block-shell-json-parsing.sh +3 -0
- package/plugins/lisa-agy/hooks/guard-dedupe.bash +66 -29
- package/plugins/lisa-agy/hooks/parity-safety-net-heredoc.py +3 -0
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +5 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +85 -17
- package/plugins/lisa-agy/scripts/qa-signal-lifecycle.mjs +5 -2
- package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +38 -15
- package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +15 -1
- package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +37 -13
- package/plugins/lisa-agy/skills/lisa-jira-write-ticket/SKILL.md +15 -1
- package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +56 -7
- package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +36 -11
- package/plugins/lisa-agy/skills/lisa-linear-prd-intake/SKILL.md +12 -2
- package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +1 -18
- package/plugins/lisa-agy/skills/lisa-linear-verify/SKILL.md +0 -17
- package/plugins/lisa-agy/skills/lisa-linear-write-issue/SKILL.md +18 -35
- package/plugins/lisa-agy/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +66 -12
- package/plugins/lisa-agy/skills/lisa-rework-triage/SKILL.md +2 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
- package/plugins/lisa-agy/skills/lisa-setup-linear/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
- package/plugins/lisa-agy/skills/lisa-track/SKILL.md +32 -4
- package/plugins/lisa-agy/skills/lisa-tracker-build-intake/SKILL.md +15 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-blind-automerge.sh +13 -5
- package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +7 -0
- package/plugins/lisa-copilot/hooks/block-instruction-file-edits.sh +3 -0
- package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +76 -2
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +3 -0
- package/plugins/lisa-copilot/hooks/block-shell-json-parsing.sh +3 -0
- package/plugins/lisa-copilot/hooks/guard-dedupe.bash +66 -29
- package/plugins/lisa-copilot/hooks/parity-safety-net-heredoc.py +3 -0
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +5 -1
- package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +3 -0
- package/plugins/lisa-copilot/hooks/worktree-binding-guard.sh +5 -1
- package/plugins/lisa-copilot/rules/eager/00-rule-index.md +1 -0
- package/plugins/lisa-copilot/rules/eager/base-rules.md +3 -1
- package/plugins/lisa-copilot/rules/eager/withdrawn-rulings.md +14 -3
- package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +1 -1
- package/plugins/lisa-copilot/rules/reference/bdd-e2e-coverage.md +1 -1
- package/plugins/lisa-copilot/rules/reference/blocker-containment.md +3 -3
- package/plugins/lisa-copilot/rules/reference/claim-archaeology.md +1 -1
- package/plugins/lisa-copilot/rules/reference/claim-evidence-mapping.md +1 -1
- package/plugins/lisa-copilot/rules/reference/claim-time-guards.md +1 -1
- package/plugins/lisa-copilot/rules/reference/control-reachability.md +1 -1
- package/plugins/lisa-copilot/rules/reference/convergent-review.md +1 -1
- package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +1 -1
- package/plugins/lisa-copilot/rules/reference/dependency-decision-records.md +1 -1
- package/plugins/lisa-copilot/rules/reference/dependency-internalization-kit.md +1 -1
- package/plugins/lisa-copilot/rules/reference/dependency-trust-classes.md +1 -1
- package/plugins/lisa-copilot/rules/reference/deployed-state-readback.md +1 -1
- package/plugins/lisa-copilot/rules/reference/derived-branch-plan.md +1 -1
- package/plugins/lisa-copilot/rules/reference/design-source-of-truth.md +1 -1
- package/plugins/lisa-copilot/rules/reference/design-value-binding.md +1 -1
- package/plugins/lisa-copilot/rules/reference/documentation-source-paths.md +1 -1
- package/plugins/lisa-copilot/rules/reference/leaf-only-lifecycle.md +1 -1
- package/plugins/lisa-copilot/rules/reference/observability-audit.md +1 -1
- package/plugins/lisa-copilot/rules/reference/prd-definition-of-ready.md +1 -1
- package/plugins/lisa-copilot/rules/reference/prd-lifecycle-rollup.md +1 -1
- package/plugins/lisa-copilot/rules/reference/pre-flight-autofill.md +1 -1
- package/plugins/lisa-copilot/rules/reference/promotion-contract.md +1 -1
- package/plugins/lisa-copilot/rules/reference/readiness-rubric.md +1 -1
- package/plugins/lisa-copilot/rules/reference/ready-role-filing.md +37 -1
- package/plugins/lisa-copilot/rules/reference/rejection-detection.md +1 -1
- package/plugins/lisa-copilot/rules/reference/repo-scope-split.md +9 -10
- package/plugins/lisa-copilot/rules/reference/reset-seed-coverage.md +1 -1
- package/plugins/lisa-copilot/rules/reference/usage-accounting.md +1 -1
- package/plugins/lisa-copilot/rules/reference/work-item-definition-of-ready.md +1 -1
- package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +85 -17
- package/plugins/lisa-copilot/scripts/qa-signal-lifecycle.mjs +5 -2
- package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +38 -15
- package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +15 -1
- package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +37 -13
- package/plugins/lisa-copilot/skills/lisa-jira-write-ticket/SKILL.md +15 -1
- package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +56 -7
- package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +36 -11
- package/plugins/lisa-copilot/skills/lisa-linear-prd-intake/SKILL.md +12 -2
- package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +1 -18
- package/plugins/lisa-copilot/skills/lisa-linear-verify/SKILL.md +0 -17
- package/plugins/lisa-copilot/skills/lisa-linear-write-issue/SKILL.md +18 -35
- package/plugins/lisa-copilot/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +66 -12
- package/plugins/lisa-copilot/skills/lisa-rework-triage/SKILL.md +2 -1
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
- package/plugins/lisa-copilot/skills/lisa-setup-linear/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
- package/plugins/lisa-copilot/skills/lisa-track/SKILL.md +32 -4
- package/plugins/lisa-copilot/skills/lisa-tracker-build-intake/SKILL.md +15 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-blind-automerge.sh +13 -5
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +7 -0
- package/plugins/lisa-cursor/hooks/block-instruction-file-edits.sh +3 -0
- package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +76 -2
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +3 -0
- package/plugins/lisa-cursor/hooks/block-shell-json-parsing.sh +3 -0
- package/plugins/lisa-cursor/hooks/guard-dedupe.bash +66 -29
- package/plugins/lisa-cursor/hooks/parity-safety-net-heredoc.py +3 -0
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +5 -1
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +3 -0
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.sh +5 -1
- package/plugins/lisa-cursor/rules/00-rule-index.mdc +1 -0
- package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/base-rules.mdc +3 -1
- package/plugins/lisa-cursor/rules/bdd-e2e-coverage-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +3 -3
- package/plugins/lisa-cursor/rules/claim-archaeology-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/claim-evidence-mapping-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/claim-time-guards-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/control-reachability-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/convergent-review-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/dependency-decision-records-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/dependency-internalization-kit-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/dependency-trust-classes-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/deployed-state-readback-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/derived-branch-plan-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/design-source-of-truth-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/design-value-binding-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/documentation-source-paths-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/leaf-only-lifecycle-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/observability-audit-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/prd-definition-of-ready-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/prd-lifecycle-rollup-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/pre-flight-autofill-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/promotion-contract-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/readiness-rubric-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/ready-role-filing-reference.mdc +37 -1
- package/plugins/lisa-cursor/rules/rejection-detection-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/repo-scope-split-reference.mdc +9 -10
- package/plugins/lisa-cursor/rules/reset-seed-coverage-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/usage-accounting-reference.mdc +1 -1
- package/plugins/lisa-cursor/rules/withdrawn-rulings.mdc +14 -3
- package/plugins/lisa-cursor/rules/work-item-definition-of-ready-reference.mdc +1 -1
- package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +85 -17
- package/plugins/lisa-cursor/scripts/qa-signal-lifecycle.mjs +5 -2
- package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +38 -15
- package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +15 -1
- package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +37 -13
- package/plugins/lisa-cursor/skills/lisa-jira-write-ticket/SKILL.md +15 -1
- package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +56 -7
- package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +36 -11
- package/plugins/lisa-cursor/skills/lisa-linear-prd-intake/SKILL.md +12 -2
- package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +1 -18
- package/plugins/lisa-cursor/skills/lisa-linear-verify/SKILL.md +0 -17
- package/plugins/lisa-cursor/skills/lisa-linear-write-issue/SKILL.md +18 -35
- package/plugins/lisa-cursor/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +66 -12
- package/plugins/lisa-cursor/skills/lisa-rework-triage/SKILL.md +2 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
- package/plugins/lisa-cursor/skills/lisa-setup-linear/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
- package/plugins/lisa-cursor/skills/lisa-track/SKILL.md +32 -4
- package/plugins/lisa-cursor/skills/lisa-tracker-build-intake/SKILL.md +15 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/materialized-artifacts.json +11 -0
- package/plugins/src/base/hooks/block-blind-automerge.sh +10 -5
- package/plugins/src/base/hooks/block-direct-issue-create.sh +4 -0
- package/plugins/src/base/hooks/block-managed-file-edits.sh +73 -2
- package/plugins/src/base/hooks/guard-dedupe.bash +63 -29
- package/plugins/src/base/hooks/parity-safety-net.sh +2 -1
- package/plugins/src/base/hooks/worktree-binding-guard.sh +2 -1
- package/plugins/src/base/rules/eager/00-rule-index.md +1 -0
- package/plugins/src/base/rules/eager/base-rules.md +3 -1
- package/plugins/src/base/rules/eager/withdrawn-rulings.md +14 -3
- package/plugins/src/base/rules/reference/automation-runbook-contract.md +1 -1
- package/plugins/src/base/rules/reference/bdd-e2e-coverage.md +1 -1
- package/plugins/src/base/rules/reference/blocker-containment.md +3 -3
- package/plugins/src/base/rules/reference/claim-archaeology.md +1 -1
- package/plugins/src/base/rules/reference/claim-evidence-mapping.md +1 -1
- package/plugins/src/base/rules/reference/claim-time-guards.md +1 -1
- package/plugins/src/base/rules/reference/control-reachability.md +1 -1
- package/plugins/src/base/rules/reference/convergent-review.md +1 -1
- package/plugins/src/base/rules/reference/credential-substrate-precedence.md +1 -1
- package/plugins/src/base/rules/reference/dependency-decision-records.md +1 -1
- package/plugins/src/base/rules/reference/dependency-internalization-kit.md +1 -1
- package/plugins/src/base/rules/reference/dependency-trust-classes.md +1 -1
- package/plugins/src/base/rules/reference/deployed-state-readback.md +1 -1
- package/plugins/src/base/rules/reference/derived-branch-plan.md +1 -1
- package/plugins/src/base/rules/reference/design-source-of-truth.md +1 -1
- package/plugins/src/base/rules/reference/design-value-binding.md +1 -1
- package/plugins/src/base/rules/reference/documentation-source-paths.md +1 -1
- package/plugins/src/base/rules/reference/leaf-only-lifecycle.md +1 -1
- package/plugins/src/base/rules/reference/observability-audit.md +1 -1
- package/plugins/src/base/rules/reference/prd-definition-of-ready.md +1 -1
- package/plugins/src/base/rules/reference/prd-lifecycle-rollup.md +1 -1
- package/plugins/src/base/rules/reference/pre-flight-autofill.md +1 -1
- package/plugins/src/base/rules/reference/promotion-contract.md +1 -1
- package/plugins/src/base/rules/reference/readiness-rubric.md +1 -1
- package/plugins/src/base/rules/reference/ready-role-filing.md +37 -1
- package/plugins/src/base/rules/reference/rejection-detection.md +1 -1
- package/plugins/src/base/rules/reference/repo-scope-split.md +9 -10
- package/plugins/src/base/rules/reference/reset-seed-coverage.md +1 -1
- package/plugins/src/base/rules/reference/usage-accounting.md +1 -1
- package/plugins/src/base/rules/reference/work-item-definition-of-ready.md +1 -1
- package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +85 -17
- package/plugins/src/base/scripts/qa-signal-lifecycle.mjs +5 -2
- package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +38 -15
- package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +15 -1
- package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +37 -13
- package/plugins/src/base/skills/lisa-jira-write-ticket/SKILL.md +15 -1
- package/plugins/src/base/skills/lisa-linear-access/SKILL.md +56 -7
- package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +36 -11
- package/plugins/src/base/skills/lisa-linear-prd-intake/SKILL.md +12 -2
- package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +1 -18
- package/plugins/src/base/skills/lisa-linear-verify/SKILL.md +0 -17
- package/plugins/src/base/skills/lisa-linear-write-issue/SKILL.md +18 -35
- package/plugins/src/base/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
- package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +66 -12
- package/plugins/src/base/skills/lisa-rework-triage/SKILL.md +2 -1
- package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
- package/plugins/src/base/skills/lisa-setup-linear/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
- package/plugins/src/base/skills/lisa-track/SKILL.md +32 -4
- package/plugins/src/base/skills/lisa-tracker-build-intake/SKILL.md +15 -1
- package/scripts/build-plugins.sh +4 -0
- package/scripts/copy-opencode-plugin-templates.mjs +5 -1
- package/scripts/lisa-enforcement-fallback.sh +9 -1
- package/scripts/two-channel-couplings.json +5 -33
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +551 -13
- package/typescript/create-only/.github/required-checks.json +1 -1
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* node scripts/check-skipped-required-checks.mjs [rootDir] [--json]
|
|
16
16
|
* node scripts/check-skipped-required-checks.mjs --vacuity [--fail-on-vacuous]
|
|
17
17
|
* node scripts/check-skipped-required-checks.mjs --pr=1234 [--repo=OWNER/NAME]
|
|
18
|
+
* node scripts/check-skipped-required-checks.mjs --outcomes (reads LISA_JOB_RESULTS, LISA_JOB_NAMES, LISA_GATE_MOMENT)
|
|
19
|
+
*
|
|
20
|
+
* `--outcomes` is the arm the `🔒 Skipped Required Checks` job rests its verdict
|
|
21
|
+
* on. Without it, this reports only the vestigial `skip_jobs` token arm, which
|
|
22
|
+
* is not a verdict about whether a required context skipped.
|
|
18
23
|
*
|
|
19
24
|
* `--vacuity` is the WIRED form of the third bullet: it resolves the pull
|
|
20
25
|
* request itself (`--pr`, else the Actions event payload, else `GITHUB_REF`,
|
|
@@ -31,13 +36,79 @@
|
|
|
31
36
|
*
|
|
32
37
|
* Two of the three live here:
|
|
33
38
|
*
|
|
34
|
-
* - **Skipped** (
|
|
35
|
-
*
|
|
36
|
-
*
|
|
39
|
+
* - **Skipped** (`--outcomes` arm): GitHub counts a `skipped` required check
|
|
40
|
+
* as SATISFIED. The `🔒 Skipped Required Checks` job hands this arm every
|
|
41
|
+
* job result of its own workflow run, and it FAILS when a ruleset-required
|
|
42
|
+
* context's job concluded `skipped`. Offline, BLOCKING in every
|
|
43
|
+
* enforcement mode.
|
|
37
44
|
* - **Vacuous** (`--pr` arm): the check really ran and really reported
|
|
38
45
|
* `success`, having done no work — measured on CodeRabbit posting
|
|
39
46
|
* `success — "Review rate limited"`. Live, per-PR, REPORTING ONLY.
|
|
40
47
|
*
|
|
48
|
+
* ## `--outcomes` — why the verdict rests on OUTCOMES, and the token arm is VESTIGIAL
|
|
49
|
+
*
|
|
50
|
+
* This guard used to answer "can a skip silence a required check?" by reading
|
|
51
|
+
* `skip_jobs` TOKENS out of the caller's workflow and comparing what each was
|
|
52
|
+
* declared to silence against `required_contexts`. `skip_jobs` was then retired
|
|
53
|
+
* in favour of gate levels in `.lisa.config.json`, which skip a job through the
|
|
54
|
+
* gate plan and leave no token behind. The token arm kept running and kept
|
|
55
|
+
* printing `✅ 0 skip_jobs token(s) examined; none silences a ruleset-required
|
|
56
|
+
* status check`. MEASURED on a caller repository in the portfolio: a required
|
|
57
|
+
* `🧾 BDD Behavior Contract` context concluded `skipped` on seven of its sixty
|
|
58
|
+
* most recent merges, and this job was green on every one. It was green on
|
|
59
|
+
* exactly the condition it was built to refuse, because it inspected a
|
|
60
|
+
* declaration that had moved rather than the outcome the declaration was about.
|
|
61
|
+
*
|
|
62
|
+
* So the verdict now rests on the OUTCOME — what each required context's job
|
|
63
|
+
* actually concluded in this run. That is mechanism-independent: it holds
|
|
64
|
+
* whether the skip came from a gate level, a token, or a hand-written `if:`, so
|
|
65
|
+
* it survives the next replacement of whatever silences checks. A guard that
|
|
66
|
+
* inspects a declaration is correct only until the declaration moves.
|
|
67
|
+
*
|
|
68
|
+
* The token arm is RETAINED because a caller may still pass `skip_jobs`, and an
|
|
69
|
+
* undeclared or spaced token is still worth naming. It is VESTIGIAL: its
|
|
70
|
+
* findings can only ADD failures, and its clean result prints a count — never a
|
|
71
|
+
* ✅, never a claim that nothing was silenced. On its own (no `--outcomes`) it
|
|
72
|
+
* is not a verdict about skipped required contexts, and its report says so.
|
|
73
|
+
*
|
|
74
|
+
* How the outcome arm sees results, and what it deliberately does not claim:
|
|
75
|
+
*
|
|
76
|
+
* - **No token.** The job `needs:` every other job in `quality.yml` with
|
|
77
|
+
* `if: always()`, so it starts once every sibling has concluded, and reads
|
|
78
|
+
* their results from `toJSON(needs)`. Reading check runs through the API
|
|
79
|
+
* would need `checks: read`, which callers do not grant and a called
|
|
80
|
+
* workflow cannot request — and #3599's reduction to offline arms holds.
|
|
81
|
+
* - **Names on the ` / ` boundary, and a prefix that says whose they are.**
|
|
82
|
+
* `needs` carries results but no names, so the workflow carries a job id →
|
|
83
|
+
* display name map beside it (a test derives it from the jobs). A job in a
|
|
84
|
+
* called workflow posts `<caller job> / <job name>`, and the CALLER's job
|
|
85
|
+
* name is not visible from inside, so a context matches a job by equality or
|
|
86
|
+
* by a suffix bounded at ` / ` — see {@link producesContext}. A NAME MATCH IS
|
|
87
|
+
* NOT OWNERSHIP: `Some Other Workflow / 🧹 Lint` matches the local `🧹 Lint`
|
|
88
|
+
* while being posted by a workflow this run cannot see, and reporting the
|
|
89
|
+
* local result for it would be a false green. So a context is judged only
|
|
90
|
+
* under the ONE prefix this run posts — declared as
|
|
91
|
+
* `ruleset.context_prefix`, else inferred by majority — and every other
|
|
92
|
+
* prefix is reported NOT EXAMINED. See {@link owningPrefix}.
|
|
93
|
+
* - **Only at `pull-request`.** Other moments skip most jobs by design and gate
|
|
94
|
+
* no merge; see {@link MERGE_GATE_MOMENT}.
|
|
95
|
+
* - **Only what this run posts.** An external app, another workflow, or a
|
|
96
|
+
* matrix leg named at runtime posts a context no job here can speak for. It
|
|
97
|
+
* is listed as NOT EXAMINED, never silently counted — GitHub records that
|
|
98
|
+
* outcome, and this run cannot see it.
|
|
99
|
+
* - **Refusals FAIL, in every mode.** Zero required contexts examined, an
|
|
100
|
+
* untranscribed snapshot, or results that cannot be read each fail. "Examined
|
|
101
|
+
* nothing" must never render like "found nothing" (see `OUTCOME_REFUSALS`).
|
|
102
|
+
*
|
|
103
|
+
* The job checks this prover out from the WORKFLOW's own repository at the
|
|
104
|
+
* workflow's own commit (`job.workflow_repository@job.workflow_sha`), never
|
|
105
|
+
* from the caller's `scripts/`. `quality.yml` is consumed at `@main`; `scripts/`
|
|
106
|
+
* arrives by `lisa apply` at whatever version the caller pinned. A caller's copy
|
|
107
|
+
* predating this arm ignores `--outcomes` and prints the vestigial token line as
|
|
108
|
+
* though it were a verdict — the false green this arm replaces. The workflow
|
|
109
|
+
* defines the arguments, so the code reading them must come from the same
|
|
110
|
+
* revision.
|
|
111
|
+
*
|
|
41
112
|
* ## Where this runs
|
|
42
113
|
*
|
|
43
114
|
* Lisa's `quality.yml` runs this on every pull request. EVERY arm is offline:
|
|
@@ -347,6 +418,9 @@ export const VIOLATIONS = Object.freeze({
|
|
|
347
418
|
reviewCarried: "review_evidence_carried_unreviewed",
|
|
348
419
|
reviewObjected: "review_evidence_objected",
|
|
349
420
|
reviewObjectionUnread: "review_evidence_objection_unread",
|
|
421
|
+
requiredSkipped: "required_context_skipped",
|
|
422
|
+
requiredNotRun: "required_context_not_run",
|
|
423
|
+
requiredAmbiguous: "required_context_ambiguous",
|
|
350
424
|
});
|
|
351
425
|
|
|
352
426
|
/**
|
|
@@ -454,8 +528,20 @@ const ENFORCEMENT_MODES = Object.freeze(["error", "warn"]);
|
|
|
454
528
|
* both that a context is ruleset-required AND that a token it actually skips
|
|
455
529
|
* silences it; that is a reviewed state, and shipping past it is the exact
|
|
456
530
|
* defect this file exists to refuse.
|
|
531
|
+
*
|
|
532
|
+
* The three outcome kinds block for a stronger reason still: they are not a
|
|
533
|
+
* declaration at all but an OBSERVATION. A required context whose job concluded
|
|
534
|
+
* `skipped` in this very run is the false green itself, measured; a result that
|
|
535
|
+
* cannot be read, or a context two jobs could post, is a verdict this guard
|
|
536
|
+
* cannot render, and rendering it green anyway is the collapse the outcome arm
|
|
537
|
+
* was built to stop.
|
|
457
538
|
*/
|
|
458
|
-
const ALWAYS_BLOCKING = Object.freeze([
|
|
539
|
+
const ALWAYS_BLOCKING = Object.freeze([
|
|
540
|
+
VIOLATIONS.suppressesRequired,
|
|
541
|
+
VIOLATIONS.requiredSkipped,
|
|
542
|
+
VIOLATIONS.requiredNotRun,
|
|
543
|
+
VIOLATIONS.requiredAmbiguous,
|
|
544
|
+
]);
|
|
459
545
|
|
|
460
546
|
/**
|
|
461
547
|
* Violation kinds that NEVER fail the build, in any enforcement mode.
|
|
@@ -3946,16 +4032,350 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
3946
4032
|
};
|
|
3947
4033
|
}
|
|
3948
4034
|
|
|
4035
|
+
/**
|
|
4036
|
+
* Selects the OUTCOME arm — the one the `🔒 Skipped Required Checks` job rests
|
|
4037
|
+
* its verdict on. See the `--outcomes` section of this file's header.
|
|
4038
|
+
*/
|
|
4039
|
+
export const OUTCOMES_FLAG = "--outcomes";
|
|
4040
|
+
|
|
4041
|
+
/**
|
|
4042
|
+
* The environment the outcome arm reads, exactly as `quality.yml` sets it.
|
|
4043
|
+
*
|
|
4044
|
+
* Environment rather than flags because two of the three are JSON built from
|
|
4045
|
+
* expressions, and `toJSON(needs)` carries job OUTPUTS. An expression spliced
|
|
4046
|
+
* into a `run:` line is a shell-injection surface; placed in `env:` it is inert
|
|
4047
|
+
* data.
|
|
4048
|
+
*
|
|
4049
|
+
* - `results` — `toJSON(needs)`: each sibling job's final `result`.
|
|
4050
|
+
* - `names` — job id → the display name GitHub composes into the context.
|
|
4051
|
+
* - `moment` — the workflow's `moment` input.
|
|
4052
|
+
*/
|
|
4053
|
+
export const OUTCOME_ENV = Object.freeze({
|
|
4054
|
+
results: "LISA_JOB_RESULTS",
|
|
4055
|
+
names: "LISA_JOB_NAMES",
|
|
4056
|
+
moment: "LISA_GATE_MOMENT",
|
|
4057
|
+
});
|
|
4058
|
+
|
|
4059
|
+
/**
|
|
4060
|
+
* The one moment whose run is the merge gate a ruleset's required checks read.
|
|
4061
|
+
*
|
|
4062
|
+
* Every other moment skips most jobs BY DESIGN — a `continuous:dev` caller wants
|
|
4063
|
+
* its E2E gates and nothing else — and gates no merge. Judging those runs would
|
|
4064
|
+
* redden every nightly over a skip no pull request can merge on, and a gate that
|
|
4065
|
+
* is red every night gets deleted rather than read.
|
|
4066
|
+
*/
|
|
4067
|
+
export const MERGE_GATE_MOMENT = "pull-request";
|
|
4068
|
+
|
|
4069
|
+
/**
|
|
4070
|
+
* Job results that show the job was scheduled to run.
|
|
4071
|
+
*
|
|
4072
|
+
* `failure` and `cancelled` are here rather than in a finding: GitHub does NOT
|
|
4073
|
+
* count either as satisfying a required check, so the context already blocks
|
|
4074
|
+
* the merge by itself. This arm exists for the one result GitHub DOES count as
|
|
4075
|
+
* satisfied while proving nothing.
|
|
4076
|
+
*/
|
|
4077
|
+
const OUTCOMES_THAT_RAN = Object.freeze(["success", "failure", "cancelled"]);
|
|
4078
|
+
|
|
4079
|
+
/**
|
|
4080
|
+
* Why the outcome arm rendered no verdict, as stable tokens.
|
|
4081
|
+
*
|
|
4082
|
+
* Each one FAILS, in every enforcement mode. `warn` exists so a fresh install
|
|
4083
|
+
* does not redden on hygiene findings; it is not a licence to print "examined
|
|
4084
|
+
* nothing" in the shape of "found nothing", because that collapse is how this
|
|
4085
|
+
* job came to be green over a skipped required context.
|
|
4086
|
+
*/
|
|
4087
|
+
export const OUTCOME_REFUSALS = Object.freeze({
|
|
4088
|
+
untrusted: "outcomes_snapshot_untrusted",
|
|
4089
|
+
unreadable: "outcomes_results_unreadable",
|
|
4090
|
+
examinedNothing: "outcomes_examined_nothing",
|
|
4091
|
+
});
|
|
4092
|
+
|
|
4093
|
+
/**
|
|
4094
|
+
* Whether a job with this display name posts this required context.
|
|
4095
|
+
*
|
|
4096
|
+
* A job in a called workflow posts `<caller job name> / <job name>`, and the
|
|
4097
|
+
* caller's job name is not visible from inside the called workflow without a
|
|
4098
|
+
* token. So the match is exact equality OR a suffix bounded by the ` / `
|
|
4099
|
+
* separator: `🧹 Lint` matches `🔍 Quality Checks / 🧹 Lint` and never
|
|
4100
|
+
* `🔍 Quality Checks / 🐢 Slow Lint Rules`, and a bare `Lint` never matches
|
|
4101
|
+
* `Slow Lint`. Two job names that could both match one context are refused as
|
|
4102
|
+
* ambiguous by {@link evaluateRequiredOutcomes} rather than resolved by guessing.
|
|
4103
|
+
*
|
|
4104
|
+
* NAME MATCHING ALONE IS NOT OWNERSHIP, and this function is deliberately only
|
|
4105
|
+
* half the rule — see {@link owningPrefix}. `Some Other Workflow / 🧹 Lint`
|
|
4106
|
+
* satisfies this test against the local `🧹 Lint` job while being posted by a
|
|
4107
|
+
* workflow this run cannot see, and reporting the local result for it is a
|
|
4108
|
+
* FALSE GREEN of exactly the kind this file exists to refuse.
|
|
4109
|
+
*
|
|
4110
|
+
* @param {string} jobName - The job's display name
|
|
4111
|
+
* @param {string} context - A required context, verbatim
|
|
4112
|
+
* @returns {boolean} True when the job's NAME matches that context
|
|
4113
|
+
*/
|
|
4114
|
+
export function producesContext(jobName, context) {
|
|
4115
|
+
return context === jobName || context.endsWith(` / ${jobName}`);
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
/**
|
|
4119
|
+
* The prefix of a context matched by name, or `""` for an unprefixed match.
|
|
4120
|
+
*
|
|
4121
|
+
* @param {string} context - A required context
|
|
4122
|
+
* @param {string} jobName - The job name that matched it
|
|
4123
|
+
* @returns {string} The caller-job prefix the context carries
|
|
4124
|
+
*/
|
|
4125
|
+
function prefixOf(context, jobName) {
|
|
4126
|
+
return context === jobName
|
|
4127
|
+
? ""
|
|
4128
|
+
: context.slice(0, context.length - jobName.length - " / ".length);
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
/**
|
|
4132
|
+
* Which caller-job prefix this run OWNS, and how that was decided.
|
|
4133
|
+
*
|
|
4134
|
+
* The problem it solves: every context this run posts carries one prefix — the
|
|
4135
|
+
* caller's job name — and a context carrying any OTHER prefix belongs to a
|
|
4136
|
+
* workflow this run cannot see, even when its trailing segment happens to equal
|
|
4137
|
+
* a local job name. Matching on the name alone would report a local job's result
|
|
4138
|
+
* for somebody else's check.
|
|
4139
|
+
*
|
|
4140
|
+
* Two ways to know the prefix, in order:
|
|
4141
|
+
*
|
|
4142
|
+
* 1. **Declared.** `ruleset.context_prefix` in the per-repo declaration, when
|
|
4143
|
+
* present, is the caller's job name as transcribed by whoever transcribed
|
|
4144
|
+
* the contexts. Exact, and nothing is inferred.
|
|
4145
|
+
* 2. **Inferred, by majority.** This workflow posts a context for every gate —
|
|
4146
|
+
* dozens — while another workflow contributes one or two. So the prefix
|
|
4147
|
+
* accounting for the MOST required contexts is this run's. A tie is not
|
|
4148
|
+
* resolved: with no majority there is no evidence, and every matched context
|
|
4149
|
+
* is reported NOT EXAMINED rather than attributed by coin-toss.
|
|
4150
|
+
*
|
|
4151
|
+
* The failure direction is what makes the inference acceptable: a context whose
|
|
4152
|
+
* prefix is not the chosen one is never examined and never claimed, so being
|
|
4153
|
+
* wrong costs coverage that is reported as missing — never a green over a check
|
|
4154
|
+
* nobody read.
|
|
4155
|
+
*
|
|
4156
|
+
* @param {object} declaration - The per-repo declaration
|
|
4157
|
+
* @param {ReadonlyArray<{context: string, prefix: string}>} matches - Name matches
|
|
4158
|
+
* @returns {{prefix: string|null, source: string}} The owning prefix, and why
|
|
4159
|
+
*/
|
|
4160
|
+
export function owningPrefix(declaration, matches) {
|
|
4161
|
+
const declared = declaration.ruleset?.context_prefix;
|
|
4162
|
+
if (typeof declared === "string" && declared.trim() !== "") {
|
|
4163
|
+
return { prefix: declared, source: "declared in `ruleset.context_prefix`" };
|
|
4164
|
+
}
|
|
4165
|
+
const counts = new Map();
|
|
4166
|
+
for (const { prefix } of matches) {
|
|
4167
|
+
counts.set(prefix, (counts.get(prefix) ?? 0) + 1);
|
|
4168
|
+
}
|
|
4169
|
+
if (counts.size === 0) return { prefix: null, source: "no context matched" };
|
|
4170
|
+
const ranked = [...counts.entries()].sort(
|
|
4171
|
+
(left, right) => right[1] - left[1]
|
|
4172
|
+
);
|
|
4173
|
+
const [[prefix, top]] = ranked;
|
|
4174
|
+
if (ranked.length > 1 && ranked[1][1] === top) {
|
|
4175
|
+
return {
|
|
4176
|
+
prefix: null,
|
|
4177
|
+
source: `two prefixes match ${top} context(s) each, so which one this run posts is not evidenced`,
|
|
4178
|
+
};
|
|
4179
|
+
}
|
|
4180
|
+
return {
|
|
4181
|
+
prefix,
|
|
4182
|
+
source: `inferred from ${top} of ${matches.length} matched context(s)`,
|
|
4183
|
+
};
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
/**
|
|
4187
|
+
* Judges each required context by what its job DID in this run.
|
|
4188
|
+
*
|
|
4189
|
+
* Pure: the whole verdict is a function of the snapshot, the results and the
|
|
4190
|
+
* names, so every branch is testable without a workflow run.
|
|
4191
|
+
*
|
|
4192
|
+
* @param {object} declaration - The per-repo declaration
|
|
4193
|
+
* @param {Record<string, {result?: string}>} results - `toJSON(needs)`, parsed
|
|
4194
|
+
* @param {Record<string, string>} names - Job id → display name
|
|
4195
|
+
* @returns {{examined: {context: string, job: string, result: string}[], notExamined: string[], violations: object[], prefix: {prefix: string|null, source: string}}} What was judged, what could not be seen, the findings, and whose contexts it judged
|
|
4196
|
+
*/
|
|
4197
|
+
export function evaluateRequiredOutcomes(declaration, results, names) {
|
|
4198
|
+
const required = Array.isArray(declaration.required_contexts)
|
|
4199
|
+
? declaration.required_contexts
|
|
4200
|
+
: [];
|
|
4201
|
+
// TWO PASSES, and the first one is the ownership rule. A name match says a
|
|
4202
|
+
// context COULD be this job's; only the prefix says it IS. See `owningPrefix`.
|
|
4203
|
+
const matched = required.flatMap(context => {
|
|
4204
|
+
const jobs = Object.keys(names).filter(job =>
|
|
4205
|
+
producesContext(names[job], context)
|
|
4206
|
+
);
|
|
4207
|
+
return jobs.length === 0
|
|
4208
|
+
? []
|
|
4209
|
+
: [{ context, jobs, prefix: prefixOf(context, names[jobs[0]]) }];
|
|
4210
|
+
});
|
|
4211
|
+
const prefix = owningPrefix(declaration, matched);
|
|
4212
|
+
const owned = new Map(
|
|
4213
|
+
matched
|
|
4214
|
+
.filter(match => prefix.prefix !== null && match.prefix === prefix.prefix)
|
|
4215
|
+
.map(match => [match.context, match.jobs])
|
|
4216
|
+
);
|
|
4217
|
+
|
|
4218
|
+
const examined = [];
|
|
4219
|
+
const notExamined = [];
|
|
4220
|
+
const violations = [];
|
|
4221
|
+
for (const context of required) {
|
|
4222
|
+
const jobs = owned.get(context) ?? [];
|
|
4223
|
+
if (jobs.length === 0) {
|
|
4224
|
+
notExamined.push(context);
|
|
4225
|
+
continue;
|
|
4226
|
+
}
|
|
4227
|
+
if (jobs.length > 1) {
|
|
4228
|
+
violations.push({
|
|
4229
|
+
kind: VIOLATIONS.requiredAmbiguous,
|
|
4230
|
+
token: context,
|
|
4231
|
+
contexts: [context],
|
|
4232
|
+
message: `\`${context}\` is ruleset-required and could be posted by more than one job in this run (${jobs.map(job => `\`${job}\``).join(", ")}). Which outcome it carries cannot be decided without guessing, so it is refused rather than resolved. Rename a job so that no display name ends in \` / \` followed by another.`,
|
|
4233
|
+
});
|
|
4234
|
+
continue;
|
|
4235
|
+
}
|
|
4236
|
+
const [job] = jobs;
|
|
4237
|
+
const result = String(results[job]?.result ?? "");
|
|
4238
|
+
examined.push({ context, job, result });
|
|
4239
|
+
violations.push(...outcomeViolations(context, job, result));
|
|
4240
|
+
}
|
|
4241
|
+
return { examined, notExamined, violations, prefix };
|
|
4242
|
+
}
|
|
4243
|
+
|
|
4244
|
+
/**
|
|
4245
|
+
* Findings for one examined required context.
|
|
4246
|
+
*
|
|
4247
|
+
* @param {string} context - Required context
|
|
4248
|
+
* @param {string} job - The job that posts it
|
|
4249
|
+
* @param {string} result - That job's result in this run
|
|
4250
|
+
* @returns {object[]} Zero or one violation
|
|
4251
|
+
*/
|
|
4252
|
+
function outcomeViolations(context, job, result) {
|
|
4253
|
+
if (result === "skipped") {
|
|
4254
|
+
return [
|
|
4255
|
+
{
|
|
4256
|
+
kind: VIOLATIONS.requiredSkipped,
|
|
4257
|
+
token: context,
|
|
4258
|
+
contexts: [context],
|
|
4259
|
+
message: `\`${context}\` is ruleset-required and its job \`${job}\` concluded \`skipped\`. GitHub counts a SKIPPED required status check as SATISFIED, so this pull request can merge on a check that ran zero steps. The mechanism does not change the outcome — a gate declared \`off\` at this moment, a \`skip_jobs\` token and a job condition all leave the same false green. Run the gate, or de-require the context in the ruleset and re-transcribe \`required_contexts\`.`,
|
|
4260
|
+
},
|
|
4261
|
+
];
|
|
4262
|
+
}
|
|
4263
|
+
if (OUTCOMES_THAT_RAN.includes(result)) return [];
|
|
4264
|
+
return [
|
|
4265
|
+
{
|
|
4266
|
+
kind: VIOLATIONS.requiredNotRun,
|
|
4267
|
+
token: context,
|
|
4268
|
+
contexts: [context],
|
|
4269
|
+
message: `\`${context}\` is ruleset-required, but this run holds no readable result for its job \`${job}\` (read ${JSON.stringify(result)}). A result nobody can read is not evidence that the check ran, so it is refused rather than assumed. The job is most likely missing from the \`needs:\` of \`🔒 Skipped Required Checks\`.`,
|
|
4270
|
+
},
|
|
4271
|
+
];
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
/**
|
|
4275
|
+
* Parses the two JSON inputs, refusing anything that is not a plain object.
|
|
4276
|
+
*
|
|
4277
|
+
* @param {Record<string, string|undefined>} env - Environment
|
|
4278
|
+
* @returns {{results?: Record<string, {result?: string}>, names?: Record<string, string>, error?: string}} The parsed inputs, or why they could not be read
|
|
4279
|
+
*/
|
|
4280
|
+
function readOutcomeEnv(env) {
|
|
4281
|
+
const parsed = {};
|
|
4282
|
+
for (const key of ["results", "names"]) {
|
|
4283
|
+
const variable = OUTCOME_ENV[key];
|
|
4284
|
+
let value;
|
|
4285
|
+
try {
|
|
4286
|
+
value = JSON.parse(String(env[variable] ?? ""));
|
|
4287
|
+
} catch {
|
|
4288
|
+
return {
|
|
4289
|
+
error: `\`${variable}\` is not JSON, so no job result could be read. The \`🔒 Skipped Required Checks\` job sets it; outside that job \`${OUTCOMES_FLAG}\` has nothing to judge.`,
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
4292
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4293
|
+
return {
|
|
4294
|
+
error: `\`${variable}\` parsed to ${JSON.stringify(value)}, not an object keyed by job id.`,
|
|
4295
|
+
};
|
|
4296
|
+
}
|
|
4297
|
+
parsed[key] = value;
|
|
4298
|
+
}
|
|
4299
|
+
const names = Object.values(parsed.names);
|
|
4300
|
+
if (names.length === 0 || names.some(name => typeof name !== "string")) {
|
|
4301
|
+
return {
|
|
4302
|
+
error: `\`${OUTCOME_ENV.names}\` must map at least one job id to its display name, and every name must be a string.`,
|
|
4303
|
+
};
|
|
4304
|
+
}
|
|
4305
|
+
return parsed;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
/**
|
|
4309
|
+
* The OUTCOME arm: did each ruleset-required context this run posts actually run?
|
|
4310
|
+
*
|
|
4311
|
+
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
4312
|
+
* @param {object} declaration - The per-repo declaration
|
|
4313
|
+
* @param {{env?: Record<string, string|undefined>, trust?: {trusted: boolean, reason: string}}} [options] - Environment and snapshot trust
|
|
4314
|
+
* @returns {{moment: string, applicable: boolean, examined: object[], notExamined: string[], violations: object[], refusal: {kind: string, reason: string}|null}|undefined} The inspection, or undefined when `--outcomes` was not passed
|
|
4315
|
+
*/
|
|
4316
|
+
export function inspectOutcomes(argv, declaration, options = {}) {
|
|
4317
|
+
if (!argv.includes(OUTCOMES_FLAG)) return undefined;
|
|
4318
|
+
const env = options.env ?? process.env;
|
|
4319
|
+
const moment = String(env[OUTCOME_ENV.moment] ?? "").trim();
|
|
4320
|
+
const base = {
|
|
4321
|
+
moment,
|
|
4322
|
+
applicable: true,
|
|
4323
|
+
examined: [],
|
|
4324
|
+
notExamined: [],
|
|
4325
|
+
violations: [],
|
|
4326
|
+
refusal: null,
|
|
4327
|
+
};
|
|
4328
|
+
if (moment !== "" && moment !== MERGE_GATE_MOMENT) {
|
|
4329
|
+
return { ...base, applicable: false };
|
|
4330
|
+
}
|
|
4331
|
+
/**
|
|
4332
|
+
* @param {string} kind - One of `OUTCOME_REFUSALS`
|
|
4333
|
+
* @param {string} reason - Why nothing was judged
|
|
4334
|
+
* @returns {object} The refused inspection
|
|
4335
|
+
*/
|
|
4336
|
+
const refuse = (kind, reason) => ({ ...base, refusal: { kind, reason } });
|
|
4337
|
+
if (moment === "") {
|
|
4338
|
+
return refuse(
|
|
4339
|
+
OUTCOME_REFUSALS.unreadable,
|
|
4340
|
+
`\`${OUTCOME_ENV.moment}\` is empty, so this cannot tell whether this run is the \`${MERGE_GATE_MOMENT}\` run a merge is gated on. Refused rather than assumed either way.`
|
|
4341
|
+
);
|
|
4342
|
+
}
|
|
4343
|
+
if (options.trust?.trusted === false) {
|
|
4344
|
+
return refuse(
|
|
4345
|
+
OUTCOME_REFUSALS.untrusted,
|
|
4346
|
+
"`required_contexts` has never been transcribed from a live ruleset (see NOT CHECKED above), so which job results a merge depends on is unknown. Examining no required context is not the same as finding none skipped."
|
|
4347
|
+
);
|
|
4348
|
+
}
|
|
4349
|
+
const read = readOutcomeEnv(env);
|
|
4350
|
+
if (read.error !== undefined) {
|
|
4351
|
+
return refuse(OUTCOME_REFUSALS.unreadable, read.error);
|
|
4352
|
+
}
|
|
4353
|
+
const evaluated = evaluateRequiredOutcomes(
|
|
4354
|
+
declaration,
|
|
4355
|
+
read.results,
|
|
4356
|
+
read.names
|
|
4357
|
+
);
|
|
4358
|
+
const refusal =
|
|
4359
|
+
evaluated.examined.length > 0
|
|
4360
|
+
? null
|
|
4361
|
+
: {
|
|
4362
|
+
kind: OUTCOME_REFUSALS.examinedNothing,
|
|
4363
|
+
reason: `Not one of the ${evaluated.notExamined.length} context(s) in \`required_contexts\` was judged, so no outcome was examined, and a guard that examined nothing must not render like one that found nothing. Either no context this workflow posts is in the snapshot — transcribe the real ruleset — or the contexts were renamed, or this run does not own their prefix (${evaluated.prefix.source}). Names are matched byte for byte on the \` / \` boundary, and a context is judged only under the prefix this run posts; \`ruleset.context_prefix\` states that prefix outright.`,
|
|
4364
|
+
};
|
|
4365
|
+
return { ...base, ...evaluated, refusal };
|
|
4366
|
+
}
|
|
4367
|
+
|
|
3949
4368
|
/**
|
|
3950
4369
|
* Runs the guard.
|
|
3951
4370
|
*
|
|
3952
4371
|
* Every arm is OFFLINE. There is no network read and no token anywhere in this
|
|
3953
4372
|
* path: the required-context rules answer from the committed snapshot when
|
|
3954
|
-
* {@link snapshotTrust} believes it, and refuse when it does not.
|
|
4373
|
+
* {@link snapshotTrust} believes it, and refuse when it does not. The outcome
|
|
4374
|
+
* arm reads job results the workflow hands it through the environment.
|
|
3955
4375
|
*
|
|
3956
4376
|
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
3957
|
-
* @param {object} [options] - Injection seams forwarded to {@link inspectVacuity}
|
|
3958
|
-
* @returns {{violations: object[], checked: number, tokens: string[], enforcement: string, trust: {trusted: boolean, reason: string}, recipe: string, pr: string|undefined, evidenceChecked: number, vacuity: object|undefined}} The result
|
|
4377
|
+
* @param {object} [options] - Injection seams forwarded to {@link inspectVacuity}; `env` also reaches {@link inspectOutcomes}
|
|
4378
|
+
* @returns {{violations: object[], checked: number, tokens: string[], enforcement: string, trust: {trusted: boolean, reason: string}, recipe: string, pr: string|undefined, evidenceChecked: number, vacuity: object|undefined, outcomes: object|undefined}} The result
|
|
3959
4379
|
*/
|
|
3960
4380
|
export function runGuard(argv, options = {}) {
|
|
3961
4381
|
if (argv.includes(RETIRED_REMOTE_FLAG)) {
|
|
@@ -3991,6 +4411,16 @@ export function runGuard(argv, options = {}) {
|
|
|
3991
4411
|
});
|
|
3992
4412
|
if (vacuity !== undefined) violations.push(...vacuity.violations);
|
|
3993
4413
|
|
|
4414
|
+
// The OUTCOME arm, which the CI job's verdict rests on. Its findings are
|
|
4415
|
+
// ALWAYS_BLOCKING and its refusal fails in every mode, so nothing the
|
|
4416
|
+
// vestigial token arm above reports — clean or otherwise — can turn its red
|
|
4417
|
+
// into a pass.
|
|
4418
|
+
const outcomes = inspectOutcomes(argv, declaration, {
|
|
4419
|
+
env: options.env,
|
|
4420
|
+
trust,
|
|
4421
|
+
});
|
|
4422
|
+
if (outcomes !== undefined) violations.push(...outcomes.violations);
|
|
4423
|
+
|
|
3994
4424
|
return {
|
|
3995
4425
|
violations,
|
|
3996
4426
|
checked: result.checked,
|
|
@@ -4003,6 +4433,7 @@ export function runGuard(argv, options = {}) {
|
|
|
4003
4433
|
verdict: vacuity?.verdict,
|
|
4004
4434
|
waiveRate: vacuity?.waiveRate,
|
|
4005
4435
|
vacuity,
|
|
4436
|
+
outcomes,
|
|
4006
4437
|
};
|
|
4007
4438
|
}
|
|
4008
4439
|
|
|
@@ -4120,7 +4551,7 @@ export function violationBlocks(violation, policy) {
|
|
|
4120
4551
|
*
|
|
4121
4552
|
* @param {object} result - Guard result
|
|
4122
4553
|
* @param {object} policy - Active CLI policy
|
|
4123
|
-
* @returns {{blocking: object[], refusal: object|null, refusalBlocks: boolean, failed: boolean}} CLI outcome
|
|
4554
|
+
* @returns {{blocking: object[], refusal: object|null, refusalBlocks: boolean, outcomesRefusal: object|null, failed: boolean}} CLI outcome
|
|
4124
4555
|
*/
|
|
4125
4556
|
function cliOutcome(result, policy) {
|
|
4126
4557
|
const blocking = result.violations.filter(violation =>
|
|
@@ -4129,11 +4560,14 @@ function cliOutcome(result, policy) {
|
|
|
4129
4560
|
const refusal = result.vacuity?.refusal ?? null;
|
|
4130
4561
|
const refusalBlocks =
|
|
4131
4562
|
refusal !== null && (!policy.warnOnly || policy.requireReviewEvidence);
|
|
4563
|
+
// Unconditional: see OUTCOME_REFUSALS for why `warn` cannot downgrade it.
|
|
4564
|
+
const outcomesRefusal = result.outcomes?.refusal ?? null;
|
|
4132
4565
|
const failed =
|
|
4133
4566
|
blocking.length > 0 ||
|
|
4134
4567
|
(!result.trust.trusted && !policy.warnOnly) ||
|
|
4135
|
-
refusalBlocks
|
|
4136
|
-
|
|
4568
|
+
refusalBlocks ||
|
|
4569
|
+
outcomesRefusal !== null;
|
|
4570
|
+
return { blocking, refusal, refusalBlocks, outcomesRefusal, failed };
|
|
4137
4571
|
}
|
|
4138
4572
|
|
|
4139
4573
|
/**
|
|
@@ -4202,7 +4636,7 @@ function appendSnapshotRefusal(lines, result, policy) {
|
|
|
4202
4636
|
""
|
|
4203
4637
|
);
|
|
4204
4638
|
process.stderr.write(
|
|
4205
|
-
`::${policy.warnOnly ? "warning" : "error"} title=Skipped-required checks NOT CHECKED::${result.trust.reason.split("\n")[0]}\n`
|
|
4639
|
+
`::${policy.warnOnly && result.outcomes?.applicable !== true ? "warning" : "error"} title=Skipped-required checks NOT CHECKED::${result.trust.reason.split("\n")[0]}\n`
|
|
4206
4640
|
);
|
|
4207
4641
|
}
|
|
4208
4642
|
|
|
@@ -4243,8 +4677,12 @@ function appendCleanResult(lines, result, refusal) {
|
|
|
4243
4677
|
return;
|
|
4244
4678
|
}
|
|
4245
4679
|
|
|
4680
|
+
// VESTIGIAL, and worded so it cannot be read as a verdict. This line used to
|
|
4681
|
+
// be `✅ … none silences a ruleset-required status check`, and it printed
|
|
4682
|
+
// exactly that, over zero tokens, on a pull request that merged with a
|
|
4683
|
+
// required context skipped. The count is real; the count is all it may claim.
|
|
4246
4684
|
lines.push(
|
|
4247
|
-
|
|
4685
|
+
`ℹ️ ${result.checked} \`skip_jobs\` token(s) examined by the VESTIGIAL declaration arm. \`skip_jobs\` is retired, so this is NOT a verdict that no required context was skipped — only the outcome arm (\`${OUTCOMES_FLAG}\`, run by the \`🔒 Skipped Required Checks\` job) observes that.`
|
|
4248
4686
|
);
|
|
4249
4687
|
if (result.pr === undefined) return;
|
|
4250
4688
|
const settleNote =
|
|
@@ -4256,6 +4694,18 @@ function appendCleanResult(lines, result, refusal) {
|
|
|
4256
4694
|
);
|
|
4257
4695
|
}
|
|
4258
4696
|
|
|
4697
|
+
/**
|
|
4698
|
+
* What a finding count was counted across, naming both arms when both ran.
|
|
4699
|
+
*
|
|
4700
|
+
* @param {object} result - Guard result
|
|
4701
|
+
* @returns {string} The scope phrase
|
|
4702
|
+
*/
|
|
4703
|
+
function findingScope(result) {
|
|
4704
|
+
const tokens = `${result.checked} \`skip_jobs\` token(s)`;
|
|
4705
|
+
if (result.outcomes?.applicable !== true) return tokens;
|
|
4706
|
+
return `${result.outcomes.examined.length} required-context outcome(s) and ${tokens}`;
|
|
4707
|
+
}
|
|
4708
|
+
|
|
4259
4709
|
/**
|
|
4260
4710
|
* Appends annotations and prose for every finding.
|
|
4261
4711
|
*
|
|
@@ -4267,7 +4717,7 @@ function appendCleanResult(lines, result, refusal) {
|
|
|
4267
4717
|
*/
|
|
4268
4718
|
function appendFindingReport(lines, result, policy, blocking) {
|
|
4269
4719
|
lines.push(
|
|
4270
|
-
`${blocking.length > 0 ? "❌" : "⚠️"} ${result.violations.length} violation(s) across ${result
|
|
4720
|
+
`${blocking.length > 0 ? "❌" : "⚠️"} ${result.violations.length} violation(s) across ${findingScope(result)}:`,
|
|
4271
4721
|
""
|
|
4272
4722
|
);
|
|
4273
4723
|
for (const violation of result.violations) {
|
|
@@ -4347,6 +4797,93 @@ function appendReviewWaiverGuidance(lines) {
|
|
|
4347
4797
|
);
|
|
4348
4798
|
}
|
|
4349
4799
|
|
|
4800
|
+
/**
|
|
4801
|
+
* Appends what the outcome arm examined, what it could not see, and why it
|
|
4802
|
+
* refused — so "examined and found nothing" never reads like "examined nothing".
|
|
4803
|
+
*
|
|
4804
|
+
* @param {string[]} lines - Report lines
|
|
4805
|
+
* @param {object|undefined} outcomes - The outcome inspection
|
|
4806
|
+
* @returns {void}
|
|
4807
|
+
*/
|
|
4808
|
+
function appendOutcomeReport(lines, outcomes) {
|
|
4809
|
+
if (outcomes === undefined) return;
|
|
4810
|
+
if (!outcomes.applicable) {
|
|
4811
|
+
lines.push(
|
|
4812
|
+
`ℹ️ **Outcome arm not applicable** — this run's gate moment is \`${outcomes.moment}\`. A ruleset's required checks gate a merge from the \`${MERGE_GATE_MOMENT}\` run; this run gates none, so NOT ONE required context was examined here, and nothing in this report is a verdict about a pull request.`,
|
|
4813
|
+
""
|
|
4814
|
+
);
|
|
4815
|
+
return;
|
|
4816
|
+
}
|
|
4817
|
+
appendOutcomeRefusal(lines, outcomes.refusal);
|
|
4818
|
+
appendExaminedOutcomes(lines, outcomes);
|
|
4819
|
+
appendUnexaminedContexts(lines, outcomes.notExamined);
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
/**
|
|
4823
|
+
* Appends and annotates an outcome inspection that judged nothing.
|
|
4824
|
+
*
|
|
4825
|
+
* @param {string[]} lines - Report lines
|
|
4826
|
+
* @param {{kind: string, reason: string}|null} refusal - The refusal, if any
|
|
4827
|
+
* @returns {void}
|
|
4828
|
+
*/
|
|
4829
|
+
function appendOutcomeRefusal(lines, refusal) {
|
|
4830
|
+
if (refusal === null) return;
|
|
4831
|
+
lines.push(
|
|
4832
|
+
`⛔ **NOT EXAMINED** (\`${refusal.kind}\`) — the outcome arm judged no required context, and will not report that none was skipped. This FAILS in every enforcement mode.`,
|
|
4833
|
+
"",
|
|
4834
|
+
refusal.reason,
|
|
4835
|
+
""
|
|
4836
|
+
);
|
|
4837
|
+
process.stderr.write(
|
|
4838
|
+
`::error title=${refusal.kind}::${refusal.reason.split("\n")[0]}\n`
|
|
4839
|
+
);
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4842
|
+
/**
|
|
4843
|
+
* Appends every examined required context with the result its job reported.
|
|
4844
|
+
*
|
|
4845
|
+
* @param {string[]} lines - Report lines
|
|
4846
|
+
* @param {{examined: {context: string, job: string, result: string}[], violations: object[]}} outcomes - The outcome inspection
|
|
4847
|
+
* @returns {void}
|
|
4848
|
+
*/
|
|
4849
|
+
function appendExaminedOutcomes(lines, outcomes) {
|
|
4850
|
+
if (outcomes.examined.length === 0) return;
|
|
4851
|
+
const count = outcomes.examined.length;
|
|
4852
|
+
const findings = outcomes.violations.length;
|
|
4853
|
+
const owned =
|
|
4854
|
+
outcomes.prefix?.prefix === undefined || outcomes.prefix?.prefix === null
|
|
4855
|
+
? ""
|
|
4856
|
+
: ` Contexts judged are those this run posts, under the prefix ${JSON.stringify(outcomes.prefix.prefix)} (${outcomes.prefix.source}); any other prefix belongs to a workflow this run cannot see.`;
|
|
4857
|
+
lines.push(
|
|
4858
|
+
findings === 0
|
|
4859
|
+
? `✅ ${count} ruleset-required context(s) examined against this run's job results; none was skipped.${owned}`
|
|
4860
|
+
: `❌ ${count} ruleset-required context(s) examined against this run's job results; ${findings} did not prove it ran (see the violations below).${owned}`,
|
|
4861
|
+
"",
|
|
4862
|
+
...outcomes.examined.map(
|
|
4863
|
+
({ context, job, result }) =>
|
|
4864
|
+
`- \`${context}\` ← job \`${job}\`: \`${result || "(no result)"}\``
|
|
4865
|
+
),
|
|
4866
|
+
""
|
|
4867
|
+
);
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4870
|
+
/**
|
|
4871
|
+
* Appends the required contexts no job in this run posts.
|
|
4872
|
+
*
|
|
4873
|
+
* @param {string[]} lines - Report lines
|
|
4874
|
+
* @param {ReadonlyArray<string>} notExamined - Contexts this run cannot see
|
|
4875
|
+
* @returns {void}
|
|
4876
|
+
*/
|
|
4877
|
+
function appendUnexaminedContexts(lines, notExamined) {
|
|
4878
|
+
if (notExamined.length === 0) return;
|
|
4879
|
+
lines.push(
|
|
4880
|
+
`Not examined — ${notExamined.length} required context(s) this run does not post: an external app, another workflow (including one whose job name matches a local one but whose prefix says it is not this run's), or a matrix leg named at runtime. GitHub records their outcomes; this run cannot see them and makes no claim about them.`,
|
|
4881
|
+
"",
|
|
4882
|
+
...notExamined.map(context => `- \`${context}\``),
|
|
4883
|
+
""
|
|
4884
|
+
);
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4350
4887
|
/**
|
|
4351
4888
|
* Builds the complete human-readable report.
|
|
4352
4889
|
*
|
|
@@ -4360,6 +4897,7 @@ function buildTextReport(result, policy, outcome) {
|
|
|
4360
4897
|
appendVerdictReport(lines, result);
|
|
4361
4898
|
appendSnapshotRefusal(lines, result, policy);
|
|
4362
4899
|
appendInspectionRefusal(lines, outcome);
|
|
4900
|
+
appendOutcomeReport(lines, result.outcomes);
|
|
4363
4901
|
if (result.violations.length === 0) {
|
|
4364
4902
|
appendCleanResult(lines, result, outcome.refusal);
|
|
4365
4903
|
} else {
|