mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
606c00e484
Trunk on main has been RED since 2026-06-08 (last greenc9c3e270). Restores every blocking gate to green on a branch. Five fix categories: 1. security / dangerous-pattern: add install-claude.sh, install-agy.sh, and install.sh to the curl|bash --exclude list in validate.yml. install.sh gained an AGY install-instructions echo (curl|bash) on 2026-06-09; the two new install scripts mirror the already-excluded install-codex/opencode set. 2. lint / markdownlint: fix 3 pre-existing failures without deleting content — MD037 (backtick the glob paths so * isn't read as emphasis) in dirty-main-attribution; two MD004 (rewrap so a continuation '+' isn't read as a plus-style bullet) in ag-s43tg-resume + skill-prune-phase2. 3. skill-flow danglers: the Phase-2 skill prune (40ee1c34f) deleted brainstorm/design/complexity/ratchet/audit-report/learning/ operating-loop-skill/cass-memory but left consumes/metadata.dependencies/ context_rel edges pointing at them. Removed the dangling edges from discovery, refactor, eval-outcomes, rpi, operationalize, agy-native, perf, test, acfs. converge (thin memo) now consumes the whitelisted command-help artifact; converge + beads-bv (orphaned when operating-loop-skill was pruned) added to skill-flow-standalone.txt with rationale. 4. derived-artifact drift: regenerated catalog.json, registry.json, context-map.md, and the 10 skills-codex .agentops-generated.json twins + manifest via scripts/regen-all.sh after the frontmatter edits. 5. stale bats: brainstorm-discovery-ideation.bats followed the deliberate bd->br tracker migration (f650d41bb) — assert br / ban bd — and the codex anti-leak guard now targets the real codex twin (skills-codex/discovery) instead of the shared Claude reference (which legitimately uses AskUserQuestion). install-sh-runtime-detection.bats updated to the new multi-vendor install-suggestion output format. Did NOT touch tests/scripts/validate-skill-disposition-schema.bats: its "bad revision origin/main" failure is a CI-env artifact (checkout lacks the origin/main named ref); all 24 cases pass locally with the ref present and the 4 guarded files unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2065 lines
106 KiB
YAML
2065 lines
106 KiB
YAML
name: Validate
|
|
|
|
# First-principles rebuild (ag-877 / ag-5ck, 2026-05-28): 67 fine-grained jobs
|
|
# collapsed to ~10 purpose-grouped jobs. The 59 scripts/{check,validate}-* are
|
|
# the safety logic and are preserved verbatim; only the GitHub-job orchestration
|
|
# was re-shaped. Each purpose job runs its family of scripts as path-gated steps
|
|
# (per-step `if: needs.changes.outputs.X`), with advisory checks marked
|
|
# `continue-on-error: true` so they report without blocking. Design:
|
|
# spec/teardown-2026-05-28/gate-rebuild-first-principles.md
|
|
#
|
|
# Coverage invariant (the no-strangler safety net): every script invoked by the
|
|
# previous validate.yml is still invoked here. Proof:
|
|
# comm -23 \
|
|
# <(git show main:.github/workflows/validate.yml | grep -oE 'scripts/[a-z0-9./_-]+\.(sh|py)' | sort -u) \
|
|
# <(grep -oE 'scripts/[a-z0-9./_-]+\.(sh|py)' .github/workflows/validate.yml | sort -u)
|
|
# MUST be empty.
|
|
|
|
on:
|
|
# Local validation is the release authority for routine direct-main work.
|
|
# Keep Actions as an explicit/manual or release-tag backstop so push-to-main
|
|
# does not consume Actions quota or become a serialization bottleneck.
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
workflow_dispatch:
|
|
pull_request:
|
|
branches: [main]
|
|
# Merge queue: GitHub validates queued PRs on the combined merge_group ref.
|
|
# The `summary` required check MUST run here or the queue hangs forever. The
|
|
# `changes` job forces a full run on merge_group (see the release step) so the
|
|
# path-filter — which has no PR base in-queue — can't skip validation.
|
|
merge_group:
|
|
|
|
# Cancel superseded PR runs so update-branch / force-push churn during a merge
|
|
# train doesn't leave stale runs eating the (20-slot, free-plan) concurrency pool
|
|
# — the self-DoS that produced a 19-deep queue on 2026-06-06. NEVER cancels main
|
|
# or tag pushes (cancel-in-progress is false unless this is a pull_request).
|
|
concurrency:
|
|
group: validate-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
jobs:
|
|
changes:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
go: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.go }}
|
|
skills: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.skills }}
|
|
hooks: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.hooks }}
|
|
docs: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.docs }}
|
|
eval: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.eval }}
|
|
codex: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.codex }}
|
|
shell: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.shell }}
|
|
bats: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.bats }}
|
|
ci: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.ci }}
|
|
contracts: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.contracts }}
|
|
goals: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.goals }}
|
|
learning: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.learning }}
|
|
markdown: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.markdown }}
|
|
corpus: ${{ steps.release.outputs.release == 'true' || steps.filter.outputs.corpus }}
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- name: Detect release tag push or merge-queue (force full run)
|
|
id: release
|
|
shell: bash
|
|
run: |
|
|
# merge_group has no PR base for the path-filter, so force the full
|
|
# validation suite — the queue must validate the combined ref in full.
|
|
if [[ "${GITHUB_REF}" == refs/tags/v* || "${GITHUB_EVENT_NAME}" == "merge_group" ]]; then
|
|
echo "release=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "release=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
- uses: dorny/paths-filter@v4
|
|
if: steps.release.outputs.release != 'true'
|
|
id: filter
|
|
with:
|
|
filters: |
|
|
go:
|
|
- 'cli/**'
|
|
- 'go.mod'
|
|
- 'go.sum'
|
|
- 'tests/windows/**'
|
|
skills:
|
|
- 'skills/**'
|
|
- 'skills-codex/**'
|
|
- 'skills-codex-overrides/**'
|
|
- 'tests/skills/**'
|
|
hooks:
|
|
- 'lib/**'
|
|
- 'cli/embedded/**'
|
|
docs:
|
|
- 'docs/**'
|
|
- 'README.md'
|
|
- 'CHANGELOG.md'
|
|
- 'PRODUCT.md'
|
|
- 'SKILL-TIERS.md'
|
|
eval:
|
|
- 'evals/**'
|
|
- 'cli/internal/eval/**'
|
|
- 'cli/cmd/ao/eval*'
|
|
- 'schemas/eval-*'
|
|
codex:
|
|
- 'skills-codex/**'
|
|
- 'skills-codex-overrides/**'
|
|
shell:
|
|
- '**/*.sh'
|
|
- 'scripts/**'
|
|
bats:
|
|
- '**/*.bats'
|
|
ci:
|
|
- '.github/**'
|
|
contracts:
|
|
- 'schemas/**'
|
|
- 'docs/contracts/**'
|
|
# Claude workflows (ag-jy8gj): the workflow-governance drift gate is a
|
|
# bidirectional identity match between .claude/workflows/*.js and the
|
|
# `workflows:` ledger section in docs/contracts/skill-dispositions.yaml.
|
|
# A .js add/remove/rename must re-run that gate, so the workflow dir is
|
|
# a contracts trigger (the ledger half is already covered by docs/contracts/**).
|
|
- '.claude/workflows/**'
|
|
# redteam-pack target globs (ag-nl1u): every file the
|
|
# security redteam pack
|
|
# (skills/security/references/agentops-redteam-pack.json)
|
|
# asserts behavioral contracts against MUST re-run the
|
|
# contracts-sync canaries when edited — otherwise a break to a
|
|
# guarded file lands without the canary that guards it (the #634
|
|
# regression). The guard test
|
|
# tests/scripts/test-pathfilter-gate-coverage.sh asserts this
|
|
# list stays a superset of the pack's target globs.
|
|
- 'AGENTS.md'
|
|
- 'docs/ARCHITECTURE.md'
|
|
- 'docs/CI-CD.md'
|
|
- 'docs/strategic-direction.md'
|
|
- 'docs/standards/shell-script-standards.md'
|
|
- 'skills/security/**'
|
|
# AGENTS tiered-split siblings (ag-g9ex): scripts/validate-agents-split.sh
|
|
# validates AGENTS.md AND these four siblings (size cap, existence,
|
|
# bidirectional links). The gate triggers on contracts (added with
|
|
# this list), so a sibling-only edit re-runs the split gate that
|
|
# guards it — without these globs an AGENTS-WORKFLOW.md-only edit
|
|
# would skip its own gate (the #634 class). The guard test
|
|
# tests/scripts/test-pathfilter-gate-coverage.bats asserts every
|
|
# AGENTS*.md the split script reads is covered by the contracts filter.
|
|
- 'AGENTS-WORKFLOW.md'
|
|
- 'AGENTS-CI.md'
|
|
- 'AGENTS-CODEX.md'
|
|
- 'AGENTS-RUNTIME.md'
|
|
goals:
|
|
# correctness/doctrine gates that police GOALS.md + the executable
|
|
# spec scenarios (ag-n4m7): a GOALS.md-only or scenario-only edit
|
|
# must trigger the directive↔scenario link gates, which previously
|
|
# ran only on go/docs/ci and silently SKIPPED on these paths (the
|
|
# #591/#593 phantom-scenario regression). docs/** already covers
|
|
# the ADR; GOALS.md + spec/scenarios/** are added explicitly here.
|
|
- 'GOALS.md'
|
|
- 'spec/scenarios/**'
|
|
- 'docs/adr/ADR-0003*'
|
|
learning:
|
|
- '.agents/learnings/**'
|
|
markdown:
|
|
- '**/*.md'
|
|
corpus:
|
|
- '.agents/**/*.md'
|
|
- '.agents/**/*.jsonl'
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# go-gate-shadow — PB3 migration authority lane.
|
|
#
|
|
# Runs the single Go gate entrypoint in CI and emits per-check GitHub
|
|
# annotations + JSON evidence. The job is required and enforces workflow
|
|
# parity for non-deferred blocking scripts, while the legacy purpose jobs
|
|
# remain blocking until the final dual-orchestration collapse.
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
go-gate-shadow:
|
|
needs: [changes]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Set up Go
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: '1.26.3'
|
|
cache-dependency-path: cli/go.sum
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.14'
|
|
|
|
- name: Install gate dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y jq shellcheck
|
|
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
|
|
sudo chmod +x /usr/local/bin/yq
|
|
pip install jsonschema pyyaml
|
|
|
|
- name: Build ao
|
|
run: |
|
|
mkdir -p bin
|
|
(cd cli && go build -o ../bin/ao ./cmd/ao)
|
|
|
|
- name: Run Go gate shadow
|
|
id: go_gate
|
|
shell: bash
|
|
run: |
|
|
set +e
|
|
WORKTREE_DISPOSITION_CI_SKIP=1 \
|
|
./bin/ao gate check --full --json --github-annotations --workflow-coverage --require-workflow-parity \
|
|
> ao-gate-report.json
|
|
rc=$?
|
|
jq '.run.summary' ao-gate-report.json
|
|
jq '.coverage | {workflow_script_count, registry_script_count, missing_script_count, missing_blocking_count, missing_advisory_count, missing_deferred_count, registry_only_script_count, missing_blocking_scripts, missing_advisory_scripts, missing_deferred_scripts, registry_only_scripts}' ao-gate-report.json
|
|
echo "exit_code=$rc" >> "$GITHUB_OUTPUT"
|
|
exit "$rc"
|
|
|
|
- name: Upload Go gate report
|
|
if: always()
|
|
uses: actions/upload-artifact@v6
|
|
with:
|
|
name: ao-gate-shadow-report
|
|
path: ao-gate-report.json
|
|
if-no-files-found: error
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# skill-gates — REQUIRED. The consolidated skill-authoring gate surface:
|
|
# every check that guards a SKILL.md / .feature / derived-skill-surface edit,
|
|
# grouped into one named job (ag-87sv). Pure regroup of already-live gates —
|
|
# no new gate logic. Absorbs from skills-integrity (heal --strict,
|
|
# validate-skill-schema, validate-skill-frontmatter, validate-skill-body-refs)
|
|
# + contracts-sync (validate-skill-flow) + doctrine-proof
|
|
# (check-scenario-test-linkage), and adds the six-surface drift sweep
|
|
# `regen-all.sh --check`. Required: listed in `summary.needs`.
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
skill-gates:
|
|
needs: [changes]
|
|
if: >-
|
|
needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true' ||
|
|
needs.changes.outputs.contracts == 'true' || needs.changes.outputs.go == 'true' ||
|
|
needs.changes.outputs.shell == 'true' || needs.changes.outputs.docs == 'true' ||
|
|
needs.changes.outputs.goals == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Set up Go (skill-body-refs + regen-all against live CLI)
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.docs == 'true'
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: '1.26.3'
|
|
cache-dependency-path: cli/go.sum
|
|
|
|
- name: Set up Python
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.docs == 'true'
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.14'
|
|
|
|
- name: Install yq (skill-schema)
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
|
|
sudo chmod +x /usr/local/bin/yq
|
|
|
|
- name: Install jq
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.docs == 'true'
|
|
run: sudo apt-get install -y jq
|
|
|
|
- name: Install python schema deps
|
|
# Must cover every trigger that runs the drift sweep below (incl. docs/go/shell),
|
|
# otherwise a docs-only change runs regen-all.sh --check without PyYAML installed.
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: pip install jsonschema pyyaml
|
|
|
|
- name: Run skill structural integrity checks (heal --strict)
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x skills/heal-skill/scripts/heal.sh
|
|
bash skills/heal-skill/scripts/heal.sh --strict
|
|
|
|
- name: Validate SKILL.md frontmatter against schema
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-skill-schema.sh
|
|
./scripts/validate-skill-schema.sh --verbose
|
|
|
|
- name: Validate SKILL.md frontmatter against v2 schema
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-skill-frontmatter.sh
|
|
bash scripts/validate-skill-frontmatter.sh
|
|
|
|
- name: Validate skill-body command/flag refs against live CLI
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-skill-body-refs.sh
|
|
bash scripts/validate-skill-body-refs.sh
|
|
|
|
- name: Validate skill-flow connectivity and closed consumes vocabulary
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-skill-flow.sh
|
|
bash scripts/validate-skill-flow.sh
|
|
|
|
- name: Scenario→test linkage gate (scripts/check-scenario-test-linkage.sh)
|
|
# Every Gherkin Scenario in skills/*/references/*.feature must carry a
|
|
# @covered-by:<test-path> tag OR be allowlisted (doc-only).
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.goals == 'true'
|
|
run: |
|
|
chmod +x scripts/check-scenario-test-linkage.sh
|
|
./scripts/check-scenario-test-linkage.sh
|
|
|
|
- name: Six-surface derived-artifact drift sweep (regen-all.sh --check)
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/regen-all.sh
|
|
bash scripts/regen-all.sh --check
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# correctness — go build/test/coverage/complexity, embedded sync, CLI
|
|
# integration, JSON-flag consistency, bats, python smoke, native Windows
|
|
# smoke, and the advisory `ao doctor` dead-reference check. Windows folds in
|
|
# as a matrix leg (its smoke runs `tests/windows/*.ps1`, no scripts/ family).
|
|
# Absorbs: go-build, cli-integration, json-flag-consistency, bats-tests,
|
|
# smoke-test, windows-smoke, doctor-check.
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
correctness:
|
|
needs: [changes]
|
|
if: >-
|
|
needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' ||
|
|
needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' ||
|
|
needs.changes.outputs.bats == 'true' || needs.changes.outputs.skills == 'true' ||
|
|
needs.changes.outputs.docs == 'true'
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, windows-latest]
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 2
|
|
|
|
# ── bats runs FIRST, on the pristine checkout, before the Go build/test
|
|
# steps below. The Go test phase shares this working tree and a cli/ test
|
|
# exercises git plumbing that can leave the shared repo HEAD moved
|
|
# (observed: detached at origin/main), which would make the workflow
|
|
# self-tests in the bats suite read stale validate.yml. Running bats up
|
|
# front — exactly the pristine-checkout + npm-bats setup the standalone
|
|
# bats-tests job used pre-rebuild — keeps it pollution-free (ag-877).
|
|
# bats needs only the runner's preinstalled python+PyYAML/jq/ripgrep; it
|
|
# does not need Go or a built ao.
|
|
- name: Install bats
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.bats == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.docs == 'true')
|
|
run: sudo npm install -g bats@1.12.0
|
|
|
|
- name: Run bats tests
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.bats == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.docs == 'true')
|
|
run: |
|
|
echo "=== Running bats tests (4-way parallel) ==="
|
|
# bats --jobs needs GNU parallel; the serial run was ~237s (55% of the
|
|
# correctness critical path). --no-parallelize-within-files preserves
|
|
# ordering inside each file; only independent files run concurrently.
|
|
sudo apt-get install -y parallel >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y parallel; }
|
|
bats --jobs 4 --no-parallelize-within-files --print-output-on-failure tests/scripts/*.bats
|
|
echo "✅ Bats tests passed"
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: '1.26.3'
|
|
cache-dependency-path: cli/go.sum
|
|
|
|
- name: Set up Python (smoke-test)
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true')
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.14'
|
|
|
|
- name: Install PyYAML for bats python helpers
|
|
# setup-python@v6 provisions a clean interpreter WITHOUT the runner's
|
|
# preinstalled PyYAML. Several bats suites run inside this job invoke
|
|
# `python3 ... import yaml` (audit-skill-metadata, generate-ci-jobs-table),
|
|
# so reinstall it whenever we've shadowed the system python.
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: python3 -m pip install pyyaml
|
|
|
|
# ── go-build (Linux) ──────────────────────────────────────────────────
|
|
- name: Build ao CLI
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: |
|
|
echo "=== Building ao CLI ==="
|
|
cd cli
|
|
go build -o /tmp/ao-test ./cmd/ao
|
|
echo "✅ ao CLI builds successfully"
|
|
|
|
# Micro-epic 9 (C8): content-hash snapshot bracket. Captures the
|
|
# pre-test state of ~/.agents/ and then diffs after the Go test
|
|
# phase, catching any test that accidentally mutates the global
|
|
# hub via a missing HOME override. Defeats the os.Chtimes mtime
|
|
# bypass that the mtime-only gate could not see. Gate is
|
|
# warn-only here (|| true) because CI runs on ephemeral
|
|
# hub content and false positives from uninitialised hub state
|
|
# should not block the pipeline until a baseline is in place.
|
|
- name: Capture pre-test ~/.agents content hash snapshot
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: |
|
|
echo "=== Capturing agents-hub content-hash snapshot (pre-test) ==="
|
|
snap="$(bash scripts/check-agents-hash-snapshot.sh capture 2>/dev/null || echo "")"
|
|
if [[ -n "$snap" && -f "$snap" ]]; then
|
|
echo "HASH_GATE_SNAPSHOT=$snap" >> "$GITHUB_ENV"
|
|
echo "captured snapshot: $snap"
|
|
else
|
|
echo "snapshot capture skipped (shasum missing?)"
|
|
fi
|
|
|
|
- name: Run Go tests with race detection and coverage
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: |
|
|
echo "=== Running Go tests ==="
|
|
cd cli
|
|
set -o pipefail
|
|
go test -race -shuffle=on -coverprofile=coverage.out -covermode=atomic ./... -v 2>&1 | tee /tmp/go-test-output.txt
|
|
echo ""
|
|
echo "=== Coverage Summary ==="
|
|
go tool cover -func=coverage.out | tail -1
|
|
echo "✅ Go tests passed"
|
|
|
|
- name: Enforce cmd/ao coverage floor
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: |
|
|
echo "=== Enforcing cli/cmd/ao coverage floor ==="
|
|
# Ratchet target: 85% (source epic evolve-cycle-6-coverage-85pct).
|
|
# Current floor is set in scripts/check-cmd-ao-coverage.sh; bump it
|
|
# one whole percent each time real coverage rises.
|
|
bash scripts/check-cmd-ao-coverage.sh --profile cli/coverage.out
|
|
|
|
- name: Diff post-test ~/.agents content hash snapshot
|
|
if: always() && runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: |
|
|
echo "=== Diffing agents-hub content-hash snapshot (post-test) ==="
|
|
if [[ -z "${HASH_GATE_SNAPSHOT:-}" || ! -f "${HASH_GATE_SNAPSHOT:-}" ]]; then
|
|
echo "::notice::hash gate inconclusive — no pre-test snapshot captured"
|
|
elif bash scripts/check-agents-hash-snapshot.sh diff "$HASH_GATE_SNAPSHOT"; then
|
|
echo "✅ agents-hub content-hash gate: clean"
|
|
else
|
|
echo "::warning::agents-hub content-hash drifted during Go test phase"
|
|
echo "::warning::This means a test mutated ~/.agents/ without honoring HOME isolation."
|
|
echo "::warning::Investigate which test wrote to the real hub instead of a tempdir."
|
|
fi
|
|
|
|
- name: Warn about slow test packages
|
|
if: always() && runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: |
|
|
echo "=== Test Package Duration Analysis ==="
|
|
THRESHOLD=45
|
|
FOUND_SLOW=0
|
|
|
|
if [[ ! -f /tmp/go-test-output.txt ]]; then
|
|
echo "No test output file found; skipping."
|
|
exit 0
|
|
fi
|
|
|
|
while IFS= read -r line; do
|
|
if [[ "$line" =~ ^(ok|FAIL)[[:space:]]+([^[:space:]]+)[[:space:]]+([0-9]+(\.[0-9]+)?)s ]]; then
|
|
pkg="${BASH_REMATCH[2]}"
|
|
elapsed="${BASH_REMATCH[3]}"
|
|
is_slow=$(awk -v e="$elapsed" -v t="$THRESHOLD" 'BEGIN { print (e > t) ? "1" : "0" }')
|
|
if [[ "$is_slow" == "1" ]]; then
|
|
echo "::warning::Slow test package ${pkg}: ${elapsed}s exceeds ${THRESHOLD}s threshold"
|
|
FOUND_SLOW=$((FOUND_SLOW + 1))
|
|
fi
|
|
fi
|
|
done < /tmp/go-test-output.txt
|
|
|
|
if [[ "$FOUND_SLOW" -gt 0 ]]; then
|
|
echo "⚠️ Found $FOUND_SLOW slow package(s) exceeding ${THRESHOLD}s threshold"
|
|
else
|
|
echo "✅ All test packages completed within ${THRESHOLD}s"
|
|
fi
|
|
|
|
- name: Verify embedded lib/skills in sync
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: |
|
|
echo "=== Verifying embedded lib/skills are in sync ==="
|
|
cd cli && make sync-hooks
|
|
if ! git diff --exit-code -- embedded/; then
|
|
echo ""
|
|
echo "❌ Embedded lib/skills are out of sync with source files."
|
|
echo " Run 'cd cli && make sync-hooks' and commit the result."
|
|
exit 1
|
|
fi
|
|
echo "✅ Embedded lib/skills are in sync"
|
|
|
|
- name: Enforce Go complexity budget on changed files
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
BASE_REF_NAME: ${{ github.base_ref }}
|
|
run: |
|
|
echo "=== Enforcing Go complexity budget ==="
|
|
GOBIN=/usr/local/bin go install github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0
|
|
if [[ "$EVENT_NAME" == "pull_request" ]]; then
|
|
git fetch --no-tags --depth=1 origin "$BASE_REF_NAME"
|
|
BASE_REF="origin/$BASE_REF_NAME"
|
|
else
|
|
BASE_REF="HEAD~1"
|
|
fi
|
|
./scripts/check-go-complexity.sh --base "$BASE_REF" --warn 15 --fail 25
|
|
|
|
# Swallowed-json guard (agentops-tqc.3 / planning-rule f-2026-04-29-002).
|
|
# Blocking. golangci-lint's errcheck does NOT catch the blank-discard
|
|
# swallow (`b, _ := json.Marshal(...)`) because check-blank is off, and
|
|
# turning it on globally would explode unrelated discards — so this
|
|
# dedicated step runs errcheck -blank filtered to conventional `json.`
|
|
# selector call sites (textual, not alias-resolving).
|
|
- name: Enforce checked json.Marshal/Unmarshal returns
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: |
|
|
echo "=== Enforcing checked json.Marshal/Unmarshal returns ==="
|
|
GOBIN=/usr/local/bin go install github.com/kisielk/errcheck@v1.20.0
|
|
bash scripts/check-json-marshal-checked.sh
|
|
|
|
- name: Upload coverage to Codecov
|
|
if: runner.os == 'Linux' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
uses: codecov/codecov-action@v7
|
|
with:
|
|
files: ./cli/coverage.out
|
|
flags: cli
|
|
fail_ci_if_error: false
|
|
|
|
- name: Upload coverage artifact
|
|
if: always() && runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: go-coverage
|
|
path: cli/coverage.out
|
|
retention-days: 7
|
|
|
|
# ── cli-integration (Linux) ───────────────────────────────────────────
|
|
- name: Build ao CLI (integration)
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: cd cli && make build
|
|
|
|
- name: Run CLI commands integration test
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: bash tests/integration/test-cli-commands.sh
|
|
|
|
- name: Run v2.18 commands integration test
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: bash tests/integration/test-v218-commands.sh
|
|
|
|
- name: Run release smoke test
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: bash scripts/release-smoke-test.sh --skip-build
|
|
|
|
# ── json-flag-consistency (Linux; was its own job, needs go-build → go||ci) ─
|
|
- name: Run JSON flag consistency tests
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: |
|
|
chmod +x tests/cli/test-json-flag-consistency.sh
|
|
./tests/cli/test-json-flag-consistency.sh
|
|
|
|
# ── smoke-test (Linux; python) ────────────────────────────────────────
|
|
- name: Run smoke tests
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true')
|
|
run: |
|
|
chmod +x tests/smoke-test.sh
|
|
./tests/smoke-test.sh --verbose
|
|
|
|
# ── doctor-check (Linux; advisory; was needs go-build → go||ci) ───────
|
|
- name: Run ao doctor (advisory dead-reference check)
|
|
if: runner.os == 'Linux' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
continue-on-error: true
|
|
run: |
|
|
echo "=== Running ao doctor ==="
|
|
/tmp/ao-test doctor 2>/dev/null || true
|
|
echo ""
|
|
echo "Note: doctor may report warnings for CI-expected missing tools."
|
|
echo "This job catches stale references and dead commands."
|
|
|
|
# ── windows-smoke (Windows matrix leg) ────────────────────────────────
|
|
- name: Run native Windows smoke tests
|
|
if: runner.os == 'Windows' && (needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
shell: pwsh
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: .\tests\windows\test-windows-smoke.ps1
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# lint — shellcheck, markdownlint, skill-lint. Absorbs: shellcheck,
|
|
# markdownlint, skill-lint. (No scripts/ family — inline + action + tests/.)
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
lint:
|
|
needs: [changes]
|
|
if: >-
|
|
needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true' ||
|
|
needs.changes.outputs.markdown == 'true' || needs.changes.outputs.skills == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Run markdownlint
|
|
if: needs.changes.outputs.markdown == 'true' || needs.changes.outputs.ci == 'true'
|
|
uses: DavidAnson/markdownlint-cli2-action@v23
|
|
with:
|
|
globs: |
|
|
**/*.md
|
|
|
|
- name: Install ShellCheck
|
|
if: needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: sudo apt-get install -y shellcheck
|
|
|
|
- name: Run ShellCheck
|
|
if: needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
echo "=== Running ShellCheck ==="
|
|
find . -name "*.sh" -type f \
|
|
-not -path "./.git/*" \
|
|
-print0 | xargs -0 -r shellcheck --severity=error || {
|
|
echo "ShellCheck found errors"
|
|
exit 1
|
|
}
|
|
echo "✅ ShellCheck passed"
|
|
|
|
- name: Install skill lint dependencies
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y jq ripgrep
|
|
|
|
- name: Run skill linting
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x tests/skills/run-all.sh
|
|
bash tests/skills/run-all.sh
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# security — secret/dangerous-pattern scan + full security toolchain gate.
|
|
# Absorbs: security-scan, security-toolchain-gate.
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
security:
|
|
needs: [changes]
|
|
if: >-
|
|
needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' ||
|
|
needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Scan for secrets
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
echo "=== Scanning for secrets ==="
|
|
# Common secret patterns
|
|
patterns=(
|
|
"password.*=.*['\"][^'\"]{8,}['\"]"
|
|
"api[_-]?key.*=.*['\"][^'\"]{16,}['\"]"
|
|
"secret.*=.*['\"][^'\"]{8,}['\"]"
|
|
"(access|auth|refresh|bearer)[_-]?token.*=.*['\"][^'\"]{16,}['\"]"
|
|
"AWS[_A-Z]*=.*['\"][A-Z0-9]{16,}['\"]"
|
|
)
|
|
|
|
found=0
|
|
for pattern in "${patterns[@]}"; do
|
|
if grep -r -i -E "$pattern" \
|
|
--exclude-dir=.git \
|
|
--exclude-dir=tests \
|
|
--exclude-dir=testdata \
|
|
--exclude-dir=cli/testdata \
|
|
--exclude-dir=dist \
|
|
--exclude-dir=.agents \
|
|
--exclude-dir=.tmp \
|
|
--exclude-dir=.gc \
|
|
--exclude="*.md" \
|
|
--exclude="*.jsonl" \
|
|
--exclude="*.sh" \
|
|
--exclude="*_test.go" \
|
|
--exclude="validate.yml" \
|
|
--binary-files=without-match \
|
|
. 2>/dev/null | grep -v 'os\.Getenv(' | grep -v 'regexp\.MustCompile(' ; then
|
|
found=1
|
|
fi
|
|
done
|
|
|
|
if [[ $found -eq 1 ]]; then
|
|
echo "⚠️ Potential secrets found - review above"
|
|
exit 1
|
|
fi
|
|
echo "✅ No secrets detected"
|
|
|
|
- name: Check for dangerous patterns
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
echo "=== Checking for dangerous patterns ==="
|
|
# Patterns that could be dangerous in scripts
|
|
# Note: validate.sh files use eval safely for CLI validation
|
|
dangerous=(
|
|
"rm -rf /"
|
|
"curl.*\| *sh"
|
|
"curl.*\| *bash"
|
|
"wget.*\| *sh"
|
|
)
|
|
|
|
found=0
|
|
for pattern in "${dangerous[@]}"; do
|
|
if grep -r -E "$pattern" \
|
|
--include="*.sh" \
|
|
--exclude-dir=.git \
|
|
--exclude-dir=tests \
|
|
--exclude-dir=cli/testdata \
|
|
--exclude="install-opencode.sh" \
|
|
--exclude="install-codex.sh" \
|
|
--exclude="install-codex-plugin.sh" \
|
|
--exclude="install-codex-native-skills.sh" \
|
|
--exclude="install-claude.sh" \
|
|
--exclude="install-agy.sh" \
|
|
--exclude="install.sh" \
|
|
--exclude="ci-local-release.sh" \
|
|
. 2>/dev/null; then
|
|
echo "Found: $pattern"
|
|
found=1
|
|
fi
|
|
done
|
|
|
|
if [[ $found -eq 1 ]]; then
|
|
echo "⚠️ Dangerous patterns found"
|
|
exit 1
|
|
fi
|
|
echo "✅ No dangerous patterns"
|
|
|
|
- name: Set up Go (security toolchain)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: '1.26.3'
|
|
cache-dependency-path: cli/go.sum
|
|
|
|
- name: Set up Python (security toolchain)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.14'
|
|
|
|
- name: Install scanner tools
|
|
# 3-attempt exponential-backoff retry per soc-z7qq item 40
|
|
# (covers transient trivy/hadolint network timeouts).
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
retry() {
|
|
local max=3
|
|
local delay=5
|
|
local attempt=1
|
|
while true; do
|
|
if "$@"; then
|
|
return 0
|
|
fi
|
|
if [[ $attempt -ge $max ]]; then
|
|
echo "command failed after $max attempts: $*" >&2
|
|
return 1
|
|
fi
|
|
echo "attempt $attempt failed; retrying in ${delay}s..." >&2
|
|
sleep "$delay"
|
|
delay=$((delay * 2))
|
|
attempt=$((attempt + 1))
|
|
done
|
|
}
|
|
retry python -m pip install --upgrade pip
|
|
retry python -m pip install semgrep ruff radon
|
|
retry env GOBIN=/usr/local/bin go install github.com/securego/gosec/v2/cmd/gosec@latest
|
|
retry env GOBIN=/usr/local/bin go install github.com/zricethezav/gitleaks/v8@latest
|
|
retry env GOBIN=/usr/local/bin go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
|
|
retry bash -c 'curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin'
|
|
retry bash -c 'curl -sL https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -o /tmp/hadolint && chmod +x /tmp/hadolint && sudo mv /tmp/hadolint /usr/local/bin/hadolint'
|
|
|
|
- name: Run security toolchain gate
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/security-gate.sh
|
|
./scripts/security-gate.sh --mode quick
|
|
env:
|
|
SECURITY_GATE_OUTPUT_DIR: ${{ runner.temp }}/agentops-security
|
|
TOOLCHAIN_OUTPUT_DIR: ${{ runner.temp }}/agentops-tooling
|
|
|
|
- name: Upload security gate artifacts
|
|
if: always() && (needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true')
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: security-gate
|
|
path: ${{ runner.temp }}/agentops-security/
|
|
retention-days: 7
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# skills-integrity — SKILL.md schema/frontmatter/body-ref/dependency
|
|
# validation, structural heal, headless-runtime skills, and plugin-load
|
|
# (manifests + symlink ban + local-only .agents + structure). Absorbs:
|
|
# skill-schema, skill-frontmatter, skill-body-refs, skill-dependency-check,
|
|
# skill-integrity, validate-headless-runtime-skills, plugin-load-test.
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
skills-integrity:
|
|
needs: [changes]
|
|
if: >-
|
|
needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true' ||
|
|
needs.changes.outputs.contracts == 'true' || needs.changes.outputs.go == 'true' ||
|
|
needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' ||
|
|
needs.changes.outputs.corpus == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Set up Go (skill-body-refs against live CLI)
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: '1.26.3'
|
|
cache-dependency-path: cli/go.sum
|
|
|
|
- name: Set up Python
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.ci == 'true'
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.14'
|
|
|
|
- name: Install python schema deps
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: pip install jsonschema pyyaml
|
|
|
|
- name: Validate skill dependency references resolve
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
python3 - <<'PY'
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
skills_dir = Path("skills")
|
|
skills = {
|
|
p.name for p in skills_dir.iterdir()
|
|
if p.is_dir() and (p / "SKILL.md").exists()
|
|
}
|
|
|
|
missing = []
|
|
for skill in sorted(skills):
|
|
content = (skills_dir / skill / "SKILL.md").read_text(encoding="utf-8")
|
|
match = re.match(r"^---\n(.*?)\n---\n", content, re.S)
|
|
if not match:
|
|
continue
|
|
|
|
in_dependencies = False
|
|
for line in match.group(1).splitlines():
|
|
stripped = line.strip()
|
|
if stripped.startswith("dependencies:"):
|
|
in_dependencies = True
|
|
continue
|
|
if in_dependencies and stripped.startswith("- "):
|
|
dep = stripped[2:].split("#", 1)[0].strip().strip('"').strip("'")
|
|
if dep and dep not in skills:
|
|
missing.append((skill, dep))
|
|
continue
|
|
if in_dependencies and stripped and not stripped.startswith("-"):
|
|
in_dependencies = False
|
|
|
|
if missing:
|
|
print("Unresolved skill dependencies:")
|
|
for skill, dep in missing:
|
|
print(f" - {skill} -> {dep}")
|
|
sys.exit(1)
|
|
|
|
print(f"Skill dependencies resolved: {len(skills)} skills checked.")
|
|
PY
|
|
|
|
- name: Validate headless runtime skills
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-headless-runtime-skills.sh
|
|
./scripts/validate-headless-runtime-skills.sh
|
|
|
|
- name: Validate manifests against versioned schemas
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-manifests.sh
|
|
./scripts/validate-manifests.sh --repo-root "$GITHUB_WORKSPACE"
|
|
|
|
- name: Check for broken symlinks
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
echo "=== Checking for symlinks ==="
|
|
|
|
# Symlinks break when plugins installed standalone from GitHub
|
|
symlinks=$(find . -type l -not -path "./.git/*" 2>/dev/null || true)
|
|
if [[ -n "$symlinks" ]]; then
|
|
echo "❌ Found symlinks that will break standalone installation:"
|
|
echo "$symlinks"
|
|
echo ""
|
|
echo "Replace symlinks with actual files for standalone plugin compatibility."
|
|
exit 1
|
|
fi
|
|
|
|
echo "✅ No symlinks found"
|
|
|
|
- name: Enforce local-only .agents state
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-no-tracked-agents.sh
|
|
bash scripts/check-no-tracked-agents.sh
|
|
|
|
- name: Corpus secret-scan (committed .agents + canon)
|
|
run: |
|
|
chmod +x scripts/check-corpus-secret-scan.sh
|
|
bash scripts/check-corpus-secret-scan.sh
|
|
|
|
- name: Test plugin directory structure
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
echo "=== Testing plugin structure (unified) ==="
|
|
|
|
failed=0
|
|
|
|
# Check skills/ directory at root
|
|
if [[ -d "skills" ]]; then
|
|
skill_count=0
|
|
for skill in skills/*/; do
|
|
[[ ! -d "$skill" ]] && continue
|
|
skill_name=$(basename "$skill")
|
|
|
|
# skills/_*/ holds scaffolding, not real skills — skip it in
|
|
# the directory-structure gate.
|
|
[[ "$skill_name" == _* ]] && continue
|
|
|
|
# Each skill must have SKILL.md
|
|
if [[ ! -f "${skill}SKILL.md" ]]; then
|
|
echo "❌ $skill_name: missing SKILL.md"
|
|
failed=1
|
|
continue
|
|
fi
|
|
|
|
# SKILL.md must have YAML frontmatter with name field
|
|
if ! head -1 "${skill}SKILL.md" | grep -q "^---$"; then
|
|
echo "❌ $skill_name: SKILL.md missing YAML frontmatter"
|
|
failed=1
|
|
continue
|
|
fi
|
|
|
|
if ! grep -q "^name:" "${skill}SKILL.md"; then
|
|
echo "❌ $skill_name: SKILL.md missing 'name' in frontmatter"
|
|
failed=1
|
|
continue
|
|
fi
|
|
|
|
skill_count=$((skill_count + 1))
|
|
done
|
|
echo "✅ $skill_count skills valid"
|
|
else
|
|
echo "❌ No skills/ directory found"
|
|
failed=1
|
|
fi
|
|
|
|
# Check agents/ directory (optional)
|
|
if [[ -d "agents" ]]; then
|
|
agent_count=$(find agents -name "*.md" -type f | wc -l | tr -d ' ')
|
|
echo "✅ $agent_count agents found"
|
|
fi
|
|
|
|
[[ $failed -eq 1 ]] && exit 1
|
|
echo ""
|
|
echo "✅ Plugin structure valid"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# contracts-sync — every derived-artifact / drift / contract-compat gate +
|
|
# the AgentOps contract canaries. Absorbs: registry-check,
|
|
# validate-registry-drift, cli-docs-parity, validate-context-map-drift,
|
|
# validate-skill-domain-map-golden, validate-sku-catalog-drift,
|
|
# check-skill-catalog-drift (advisory), validate-bounded-contexts-drift,
|
|
# embedded-sync, validate-ci-policy-parity, contract-compatibility-gate,
|
|
# validate-contracts-structural-floor, agentops-contract-canaries.
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
contracts-sync:
|
|
needs: [changes]
|
|
if: >-
|
|
needs.changes.outputs.skills == 'true' || needs.changes.outputs.docs == 'true' ||
|
|
needs.changes.outputs.ci == 'true' || needs.changes.outputs.hooks == 'true' ||
|
|
needs.changes.outputs.eval == 'true' || needs.changes.outputs.go == 'true' ||
|
|
needs.changes.outputs.contracts == 'true' || needs.changes.outputs.shell == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Set up Go
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.eval == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.ci == 'true'
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: '1.26.3'
|
|
cache-dependency-path: cli/go.sum
|
|
|
|
- name: Set up Python
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.14'
|
|
|
|
- name: Install PyYAML
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: pip install pyyaml
|
|
|
|
- name: Install jq
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.eval == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: sudo apt-get install -y jq
|
|
|
|
- name: Validate CI policy parity (golden-file diff)
|
|
if: needs.changes.outputs.ci == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.docs == 'true'
|
|
run: |
|
|
python3 -m pip install --user pyyaml
|
|
chmod +x scripts/generate-ci-jobs-table.sh scripts/validate-ci-policy-parity.sh
|
|
./scripts/validate-ci-policy-parity.sh
|
|
|
|
- name: Check skill registry drift (registries-drift lesson)
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-registry-drift.sh
|
|
./scripts/check-registry-drift.sh
|
|
|
|
- name: Check bounded-contexts drift (soc-zxia.2 Phase 2)
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-bounded-contexts-drift.sh
|
|
./scripts/check-bounded-contexts-drift.sh
|
|
|
|
- name: Check workflow governance drift (ag-jy8gj — js<->ledger bijection + kind/BC/role)
|
|
# Bidirectional: every .claude/workflows/*.js has a `workflows:` ledger row
|
|
# carrying kind: workflow + a Bounded Context (domain) + a hexagonal_role,
|
|
# and every kind: workflow ledger row has a matching .js (no stale rows).
|
|
# Workflows are Claude-only — no Codex twin required.
|
|
if: needs.changes.outputs.contracts == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-workflow-governance.sh
|
|
./scripts/check-workflow-governance.sh
|
|
|
|
- name: Check skill-domain-map golden (soc-zxia.3 Phase 3)
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/generate-skill-domain-map.sh
|
|
./scripts/generate-skill-domain-map.sh --check
|
|
|
|
- name: Validate context map is in sync with SKILL.md frontmatter
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.contracts == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-context-map-drift.sh
|
|
bash scripts/validate-context-map-drift.sh
|
|
|
|
- name: Verify embedded lib/skills are in sync
|
|
# MUST include `skills`: the embedded copies mirror skills/standards/references/*,
|
|
# and skills/compile/scripts/compile.sh (using-agentops embed retired, ag-s43tg), so a
|
|
# skills-only change can drift the embeds. Omitting `skills` here let #776's
|
|
# standards edit reach main with empty embeds (main-red 2026-06-06). `contracts`
|
|
# added for defense-in-depth.
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.contracts == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-embedded-sync.sh
|
|
./scripts/validate-embedded-sync.sh
|
|
|
|
- name: Verify CLI docs are up to date
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/generate-cli-reference.sh
|
|
./scripts/generate-cli-reference.sh --check
|
|
|
|
- name: Verify registry.json is up to date
|
|
# Trigger on ANY input scripts/generate-registry.sh actually reads
|
|
# (skills/, hooks/, evals/, cli/cmd/ao/) — not just skills/. The MVP
|
|
# filter missed `hooks` and `eval` and `go`, which let PR #362 land
|
|
# session-pr-counter.sh without re-running this check; the drift sat on
|
|
# main until a workflow-touching PR (#364) re-triggered it. soc-xhp6
|
|
# closes the path-filter-SKIPPED-≠-drift-absent gap. See
|
|
# .agents/learnings/2026-05-20-path-filter-skipped-not-absent.md.
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.eval == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.contracts == 'true'
|
|
run: |
|
|
chmod +x scripts/generate-registry.sh
|
|
./scripts/generate-registry.sh --check
|
|
|
|
- name: Validate SKU capability catalog (drift + linkage + coverage)
|
|
# ag-cbm: the SKU capability catalog (registry.json schema v2) is a derived
|
|
# JOIN of SKILL.md frontmatter + skill-dispositions.yaml + SKILL-TIERS.md +
|
|
# the live `ao` cobra tree + validate.yml gate jobs + packs/agentops.
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.contracts == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-sku-catalog-drift.sh
|
|
./scripts/validate-sku-catalog-drift.sh
|
|
|
|
- name: Check skill catalog drift (advisory)
|
|
# I0 advisory wiring for scripts/check-skill-catalog-drift.sh (soc-dspz).
|
|
# Annotates when skills/catalog.json is out of sync with skills/*/SKILL.md
|
|
# frontmatter, without blocking the merge. Regenerate via
|
|
# scripts/generate-skill-catalog.sh.
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
continue-on-error: true
|
|
run: |
|
|
set +e
|
|
chmod +x scripts/check-skill-catalog-drift.sh
|
|
scripts/check-skill-catalog-drift.sh
|
|
rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "::warning::skills/catalog.json drift detected (advisory; rc=$rc). Regenerate via scripts/generate-skill-catalog.sh."
|
|
fi
|
|
# advisory: never propagate non-zero
|
|
exit 0
|
|
|
|
- name: Run contract compatibility check
|
|
if: needs.changes.outputs.contracts == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-contract-compatibility.sh
|
|
./scripts/check-contract-compatibility.sh
|
|
|
|
- name: Run next-work contract parity check
|
|
if: needs.changes.outputs.contracts == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-next-work-contract-parity.sh
|
|
./scripts/validate-next-work-contract-parity.sh
|
|
|
|
- name: Structural enforcement floor for all contracts
|
|
if: needs.changes.outputs.contracts == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-contracts-structural-floor.sh
|
|
./scripts/check-contracts-structural-floor.sh
|
|
|
|
- name: Outcomes holdout-leak gate (deny-by-default)
|
|
if: needs.changes.outputs.contracts == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-outcomes-holdout-leak.sh
|
|
# Scan any committed Outcomes rubric/score payload fixtures; empty set = pass.
|
|
mapfile -t payloads < <(find evals .agents/tests -type f -name '*outcomes*.json' 2>/dev/null || true)
|
|
if [ "${#payloads[@]}" -gt 0 ]; then
|
|
scripts/check-outcomes-holdout-leak.sh "${payloads[@]}"
|
|
else
|
|
echo "ok: no committed Outcomes payloads to scan (gate armed for future fixtures)"
|
|
fi
|
|
|
|
# ── agentops-contract-canaries (heavy bespoke setup: bd, gocyclo, bats) ──
|
|
- name: Cache pinned Go tool binaries
|
|
if: needs.changes.outputs.contracts == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: |
|
|
~/go/bin
|
|
~/.local/bin
|
|
key: canary-tools-${{ runner.os }}-gocyclo-v0.6.0-bd-v1.0.3
|
|
|
|
- name: Install AgentOps contract canary dependencies
|
|
if: needs.changes.outputs.contracts == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
sudo apt-get install -y jq ripgrep
|
|
sudo npm install -g bats@1.12.0
|
|
# Skip `go install` on cache hit — binaries are version-pinned and
|
|
# the cache key encodes both versions, so any pin bump invalidates.
|
|
if [ ! -x "$HOME/go/bin/gocyclo" ]; then
|
|
GOBIN="$HOME/go/bin" go install github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0
|
|
fi
|
|
echo "$HOME/go/bin" >> "$GITHUB_PATH"
|
|
if [ ! -x "$HOME/.local/bin/bd" ]; then
|
|
GOBIN="$HOME/.local/bin" go install github.com/steveyegge/beads/cmd/bd@v1.0.3
|
|
fi
|
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
bd_eval_root="$RUNNER_TEMP/agentops-bd"
|
|
mkdir -p "$bd_eval_root"
|
|
git -C "$bd_eval_root" init --quiet
|
|
(
|
|
cd "$bd_eval_root"
|
|
"$HOME/.local/bin/bd" init --non-interactive --skip-agents --skip-hooks --quiet
|
|
)
|
|
echo "BEADS_DIR=$bd_eval_root/.beads" >> "$GITHUB_ENV"
|
|
|
|
- name: Check CLI surface counts (fast pre-flight)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/update-cli-surface-counts.sh
|
|
scripts/update-cli-surface-counts.sh
|
|
|
|
- name: Run official AgentOps contract canaries
|
|
if: needs.changes.outputs.contracts == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/test-agentops-contract-canaries.sh
|
|
scripts/test-agentops-contract-canaries.sh
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# codex-parity — the 7 Codex runtime/artifact/contract/parity checks.
|
|
# skills-codex/ is manually maintained; audit drift with
|
|
# scripts/audit-codex-parity.sh. Absorbs: validate-codex-runtime-sections,
|
|
# validate-codex-generated-artifacts, validate-codex-backbone-prompts,
|
|
# validate-codex-override-coverage, validate-codex-rpi-contract,
|
|
# validate-codex-lifecycle-guards, validate-codex-parity-drift.
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
codex-parity:
|
|
needs: [changes]
|
|
if: >-
|
|
needs.changes.outputs.codex == 'true' || needs.changes.outputs.skills == 'true' ||
|
|
needs.changes.outputs.ci == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Set up Python (parity-drift)
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.14'
|
|
|
|
- name: Validate Codex runtime sections
|
|
run: |
|
|
chmod +x scripts/validate-codex-runtime-sections.sh
|
|
./scripts/validate-codex-runtime-sections.sh
|
|
|
|
- name: Validate Codex generated artifacts
|
|
run: |
|
|
chmod +x scripts/validate-codex-generated-artifacts.sh
|
|
./scripts/validate-codex-generated-artifacts.sh --scope head
|
|
|
|
- name: Validate Codex backbone prompts
|
|
run: |
|
|
chmod +x scripts/validate-codex-backbone-prompts.sh
|
|
./scripts/validate-codex-backbone-prompts.sh
|
|
|
|
- name: Validate Codex override coverage
|
|
run: |
|
|
chmod +x scripts/validate-codex-override-coverage.sh
|
|
./scripts/validate-codex-override-coverage.sh
|
|
|
|
- name: Validate Codex RPI contract
|
|
run: |
|
|
bash scripts/validate-codex-rpi-contract.sh
|
|
|
|
- name: Validate Codex lifecycle guards
|
|
run: |
|
|
chmod +x scripts/validate-codex-lifecycle-guards.sh
|
|
./scripts/validate-codex-lifecycle-guards.sh
|
|
|
|
- name: Check Codex parity drift (GOALS.md directive D7)
|
|
run: |
|
|
chmod +x scripts/check-codex-parity-drift.sh
|
|
./scripts/check-codex-parity-drift.sh
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# doctrine-proof — flywheel/goals/wiring/corpus/finding-registry/memrl/
|
|
# sovereignty/three-gap proofs PLUS the spec-linkage gates (executable-spec
|
|
# link integrity, scenario↔test linkage, AGENTS.md tiered split, docs↔
|
|
# learning references). Both clusters prove the GOALS/doctrine/spec
|
|
# apparatus is internally consistent and share the Go+ao+Python setup.
|
|
# Absorbs: validate-flywheel-proof, validate-flywheel-compounding-snapshot,
|
|
# validate-goals-validate, validate-three-gap-supergate,
|
|
# validate-wiring-closure, validate-corpus-freshness,
|
|
# validate-finding-registry, memrl-health, validate-sovereignty-proof-citations,
|
|
# executable-spec-link-integrity, validate-scenario-test-linkage,
|
|
# validate-agents-split, validate-docs-learning-references.
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
doctrine-proof:
|
|
needs: [changes]
|
|
if: >-
|
|
needs.changes.outputs.go == 'true' || needs.changes.outputs.skills == 'true' ||
|
|
needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' ||
|
|
needs.changes.outputs.eval == 'true' || needs.changes.outputs.ci == 'true' ||
|
|
needs.changes.outputs.docs == 'true' || needs.changes.outputs.contracts == 'true' ||
|
|
needs.changes.outputs.learning == 'true' || needs.changes.outputs.goals == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
# fetch-depth: 0 required so the three-gap-supergate Gap 1
|
|
# --strict-coverage advisory step can resolve `main..HEAD`.
|
|
fetch-depth: 0
|
|
|
|
- name: Set up Go
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.eval == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.goals == 'true'
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: '1.26.3'
|
|
cache-dependency-path: cli/go.sum
|
|
|
|
- name: Build ao binary
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.eval == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.goals == 'true'
|
|
run: |
|
|
cd cli && go build -o bin/ao ./cmd/ao
|
|
|
|
- name: Run flywheel-proof gate (GOALS.md gate id flywheel-proof, weight 7)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.eval == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/proof-run.sh
|
|
./scripts/proof-run.sh
|
|
|
|
- name: Validate flywheel-compounding snapshot (GOALS.md gate id flywheel-compounding-snapshot, weight 5 — G1)
|
|
if: needs.changes.outputs.docs == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-flywheel-compounding-snapshot.sh
|
|
./scripts/check-flywheel-compounding-snapshot.sh
|
|
|
|
- name: Validate GOALS.md structure (GOALS.md gate id goals-validate, weight 5)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.goals == 'true'
|
|
run: |
|
|
cli/bin/ao goals validate --json | jq -e '.valid == true'
|
|
|
|
- name: F1 executable-spec link e2e (epic soc-58nt)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.goals == 'true'
|
|
run: |
|
|
chmod +x tests/e2e/goals-scenarios-link.sh
|
|
bash tests/e2e/goals-scenarios-link.sh
|
|
|
|
- name: F2 scenario-satisfaction gate e2e (epic soc-58nt)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.goals == 'true'
|
|
run: |
|
|
chmod +x tests/e2e/goals-measure-scenarios.sh
|
|
bash tests/e2e/goals-measure-scenarios.sh
|
|
|
|
- name: F3 domain-scoped RPI e2e (epic soc-58nt)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x tests/e2e/rpi-phased-domain.sh
|
|
bash tests/e2e/rpi-phased-domain.sh
|
|
|
|
- name: F4 trace-chain e2e (epic soc-58nt)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x tests/e2e/goals-trace-chain.sh
|
|
bash tests/e2e/goals-trace-chain.sh
|
|
|
|
- name: F5 auto re-steer e2e (epic soc-58nt)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x tests/e2e/goals-steer-auto.sh
|
|
bash tests/e2e/goals-steer-auto.sh
|
|
|
|
- name: Run three-gap super-gates (GOALS.md gate id three-gap-supergate, weight 5)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-three-gap-supergate.sh
|
|
./scripts/check-three-gap-supergate.sh --gap=all
|
|
|
|
# soc-33bw — advisory PR-commit-to-council mapping for Gap 1. The
|
|
# --strict-coverage flag ships with backwards-compatible default OFF.
|
|
# This step exercises the opt-in path for visibility; failures DO NOT
|
|
# block the gate yet (the trailing `|| echo` keeps exit code zero).
|
|
# NOTE: do NOT use the step-level GH Actions opt-out keyword for this
|
|
# advisory behavior — validate-ci-policy-parity.sh's awk parser
|
|
# misattributes that keyword to the parent job and flips the job's
|
|
# blocking classification.
|
|
- name: Gap 1 --strict-coverage (advisory; soc-33bw / soc-w6vh.6)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
./scripts/check-three-gap-supergate.sh \
|
|
--gap=council-coverage --strict-coverage \
|
|
|| echo "ADVISORY-WARN: --strict-coverage non-blocking failure (soc-33bw)"
|
|
|
|
- name: Verify all scripts/skills/hooks are wired (GOALS.md gate id wiring-closure, weight 7)
|
|
# ag-g9ex: + goals — wiring-closure greps GOALS.md/GOALS.yaml to confirm
|
|
# every check-*.sh is referenced; a GOALS.md-only edit that de-wires a
|
|
# gate must re-run this check (GOALS.md is covered only by the goals filter).
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.goals == 'true'
|
|
run: |
|
|
chmod +x scripts/check-wiring-closure.sh
|
|
timeout 60 bash scripts/check-wiring-closure.sh
|
|
|
|
- name: Run corpus-freshness gate (GOALS.md gate id corpus-freshness, weight 4 — Directive D11)
|
|
if: needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true'
|
|
env:
|
|
# CI runners are greenfield (no snapshot dir); the gate is structural and exercises
|
|
# the SKIP path. The freshness check is meaningful on operator machines where
|
|
# `ao corpus snapshot` runs on a regular cadence.
|
|
AGENTOPS_CORPUS_FRESHNESS_SKIP: '1'
|
|
run: |
|
|
chmod +x scripts/check-corpus-freshness.sh
|
|
./scripts/check-corpus-freshness.sh
|
|
|
|
- name: Validate finding-registry contract (GOALS.md gate id finding-registry, weight 4 — A2 audit follow-up)
|
|
if: needs.changes.outputs.contracts == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-finding-registry.sh
|
|
./scripts/check-finding-registry.sh
|
|
|
|
- name: Check MemRL feedback loop wiring
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-memrl-health.sh
|
|
./scripts/check-memrl-health.sh
|
|
|
|
- name: Validate sovereignty-proof citations resolve at HEAD
|
|
# soc-vuu6.32 — every file:line citation on docs/sovereignty-proof/ must
|
|
# resolve at HEAD or this gate fails before the proof page becomes a lie.
|
|
if: needs.changes.outputs.docs == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.contracts == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-sovereignty-proof-citations.sh
|
|
./scripts/validate-sovereignty-proof-citations.sh
|
|
|
|
# ── spec-linkage (merged in) ──────────────────────────────────────────
|
|
- name: Validate AGENTS.md tiered-split contract
|
|
# soc-vuu6.3 — AGENTS.md stays <=250 lines; AGENTS-{WORKFLOW,CI,CODEX,
|
|
# RUNTIME}.md exist; links are bidirectional.
|
|
# ag-g9ex: + contracts — AGENTS.md and the four siblings are covered by
|
|
# the contracts filter, so a sibling-only edit re-runs the split gate that
|
|
# guards it (the #634 path-filter-gap class).
|
|
if: needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.contracts == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-agents-split.sh
|
|
./scripts/validate-agents-split.sh
|
|
|
|
- name: Validate docs/plans + docs/learnings do not reference absent .agents/learnings paths (soc-w6vh.5.1)
|
|
if: needs.changes.outputs.docs == 'true' || needs.changes.outputs.learning == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-docs-learning-references.sh
|
|
./scripts/check-docs-learning-references.sh
|
|
|
|
- name: Hooks-runtime drift gate — live-facing docs/ stay hookless (ag-rryf)
|
|
# Companion to sync-skill-counts.sh: fails when a live-facing doc
|
|
# reintroduces a live hooks runtime (hooks/hooks.json, a hooks/*.sh path,
|
|
# `ao hooks`, or a bare session-*.sh hook ref) without a hookless/opt-in/
|
|
# historical hedge. Archival + opt-in-subsystem surfaces are out of scope
|
|
# (see the script header). Guards the ag-t1ca de-hooking against regression.
|
|
if: needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.shell == 'true'
|
|
run: |
|
|
chmod +x scripts/check-doc-hooks-drift.sh
|
|
./scripts/check-doc-hooks-drift.sh
|
|
|
|
- name: Directive-to-scenario link lint (ao goals scenarios --lint, blocking)
|
|
# T1 (≤5min). Blocking (soc-x7y9f): the directive↔scenario link lint
|
|
# fails the job on any broken edge. The broader whole-chain orphan/gap
|
|
# audit (ao goals trace --orphans) stays warn-only below.
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.goals == 'true'
|
|
run: |
|
|
echo "=== ao goals scenarios --lint ==="
|
|
cli/bin/ao goals scenarios --lint
|
|
|
|
- name: Whole-chain orphan/gap audit (ao goals trace --orphans, warn-only)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.goals == 'true'
|
|
run: |
|
|
echo "=== ao goals trace --orphans ==="
|
|
cli/bin/ao goals trace --orphans || echo "WARN: trace --orphans found issues (warn-only, F1.6 / soc-58nt.1.9; out of soc-x7y9f scope, tracked under soc-gqhrz)"
|
|
|
|
- name: No-orphan provenance gate (ao provenance trace --orphans --strict, blocking)
|
|
# T1 (≤5min, ag-x31t.6). Blocking: generalizes goals_trace_orphans onto
|
|
# the provenance graph. Asserts the strict audit CATCHES every seeded
|
|
# orphan fixture (artifact node with no inbound authored/inferred edge)
|
|
# and PASSES once an inbound edge wires the artifact back to a directive.
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.goals == 'true'
|
|
run: |
|
|
chmod +x scripts/check-provenance-orphans.sh
|
|
AO_BIN="$PWD/cli/bin/ao" ./scripts/check-provenance-orphans.sh
|
|
|
|
- name: Committed provenance ledger gate (schema + tamper-evident hash chain, blocking)
|
|
# T1 (≤5min, ag-8jf97). Blocking: docs/provenance/ledger.jsonl is the
|
|
# declared append-only SOT in CLAUDE.md ("ledger wins on disagreement").
|
|
# This gate requires it to EXIST, be schema-valid per line, and be an
|
|
# intact hash chain (via `ao provenance verify`) — so a tampered field,
|
|
# forged hash, or reordered row fails the build instead of becoming a
|
|
# lying instrument.
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.shell == 'true'
|
|
run: |
|
|
chmod +x scripts/validate-provenance-ledger.sh
|
|
AO_BIN="$PWD/cli/bin/ao" ./scripts/validate-provenance-ledger.sh --gate
|
|
|
|
- name: Witness Dolt→JSONL cross-check gate (scripts/witness-dolt-jsonl-crosscheck.sh)
|
|
# T1 (≤5min, ag-lmdx.3). Blocking: the watcher-that-watches-the-watcher.
|
|
# Re-derives the hash-chained JSONL witness FROM the Dolt projection and
|
|
# hash-compares against the committed witness — the inverse of drrebuild
|
|
# (#646, which rebuilds Dolt FROM the witness). Hermetic: CI has no live
|
|
# Dolt, so it drives committed fixtures (faithful + tampered) through the
|
|
# re-derive+hash-compare helper and asserts the gate fires correctly.
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.shell == 'true'
|
|
run: |
|
|
chmod +x scripts/witness-dolt-jsonl-crosscheck.sh
|
|
./scripts/witness-dolt-jsonl-crosscheck.sh
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# eval — eval baseline-audit drift gate, eval-skill-delta dry-run,
|
|
# workbench golden verify, retrieval-quality bench. Absorbs:
|
|
# agentops-eval-baseline-audit, eval-skill-delta, eval-workbench-verify,
|
|
# retrieval-quality.
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
eval:
|
|
needs: [changes]
|
|
if: >-
|
|
needs.changes.outputs.eval == 'true' || needs.changes.outputs.go == 'true' ||
|
|
needs.changes.outputs.ci == 'true' || needs.changes.outputs.skills == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
# eval-skill-delta diffs HEAD~1 for skills/ changes.
|
|
fetch-depth: 2
|
|
|
|
- name: Set up Go
|
|
if: needs.changes.outputs.eval == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: '1.26.3'
|
|
cache-dependency-path: cli/go.sum
|
|
|
|
- name: Set up Python (workbench)
|
|
if: needs.changes.outputs.eval == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.14'
|
|
|
|
# ── agentops-eval-baseline-audit (drift-only) ─────────────────────────
|
|
- name: Build ao binary
|
|
if: needs.changes.outputs.eval == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
mkdir -p cli/bin
|
|
(cd cli && go build -o bin/ao ./cmd/ao)
|
|
|
|
- name: Run eval baseline-audit (drift-only)
|
|
if: needs.changes.outputs.eval == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
# Drift-only gate: fail on stale_suite_hashes (real drift between a
|
|
# promoted baseline and its current suite SHA). policy_mismatch_count
|
|
# is reported informationally — under the "stop tracking agents
|
|
# runtime state" policy (commit 3f1566fd) baselines are operator-
|
|
# local, so a fresh clone always reports missing_compare_baselines.
|
|
out="$(./cli/bin/ao eval baseline-audit --root evals/agentops-core --json)"
|
|
echo "$out"
|
|
stale=$(printf '%s' "$out" | jq '(.stale_suite_hashes // []) | length' 2>/dev/null || echo "-1")
|
|
mismatch=$(printf '%s' "$out" | jq '.policy_mismatch_count // 0' 2>/dev/null || echo "-1")
|
|
if [[ "$stale" == "-1" || "$mismatch" == "-1" ]]; then
|
|
echo "FAIL: could not parse baseline-audit output" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$stale" -gt 0 ]]; then
|
|
echo "FAIL: stale_suite_hashes=$stale (a promoted baseline's suite SHA drifted)" >&2
|
|
exit 1
|
|
fi
|
|
echo "ok: stale_suite_hashes=0 (info: policy_mismatch_count=$mismatch)"
|
|
|
|
# ── eval-skill-delta (structural gate) ────────────────────────────────
|
|
# practice-citations RETIRED 2026-05-19. Advisory job with 79% failure
|
|
# rate; blocked zero merges. The underlying validator script remains
|
|
# available at scripts/validate-practice-citations.sh for ad-hoc local use.
|
|
- name: Check for skills changes
|
|
if: needs.changes.outputs.eval == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
id: skilldelta
|
|
run: |
|
|
if git diff --name-only HEAD~1 -- 'skills/**' | grep -q .; then
|
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Validate eval harness dry-run
|
|
if: (needs.changes.outputs.eval == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true') && steps.skilldelta.outputs.changed == 'true'
|
|
run: |
|
|
chmod +x scripts/eval-agent-harness.sh
|
|
result=$(bash scripts/eval-agent-harness.sh --task go-01 --agent echo --dry-run 2>/dev/null | tail -1)
|
|
echo "$result" | jq -e '.skipped == true' > /dev/null
|
|
echo "eval harness dry-run: valid JSON, skipped=true"
|
|
|
|
# ── eval-workbench-verify ─────────────────────────────────────────────
|
|
- name: Install workbench Python dependencies
|
|
if: needs.changes.outputs.eval == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
cd evals/workbench/python-api
|
|
python -m venv .venv
|
|
source .venv/bin/activate
|
|
pip install fastapi uvicorn httpx pytest ruff black
|
|
|
|
- name: Verify workbench golden state (D10 head-to-head delta)
|
|
if: needs.changes.outputs.eval == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-eval-workbench.sh
|
|
bash scripts/check-eval-workbench.sh
|
|
|
|
- name: Upload scorecard-latest.json (D10 delta artifact)
|
|
if: always() && (needs.changes.outputs.eval == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true')
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: workbench-scorecard
|
|
path: evals/workbench/scorecard-latest.json
|
|
if-no-files-found: warn
|
|
|
|
# ── retrieval-quality ─────────────────────────────────────────────────
|
|
- name: Build ao CLI (retrieval bench)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: cd cli && go build -o /tmp/ao-test ./cmd/ao
|
|
|
|
- name: Run retrieval bench (offline corpus)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
cd cli
|
|
/tmp/ao-test retrieval-bench --json | tee /tmp/retrieval-report.json
|
|
PRECISION=$(python3 -c "import json; r=json.load(open('/tmp/retrieval-report.json')); print(r.get('avg_precision_at_k', r.get('avg_p_at_k', 0)))")
|
|
echo "Precision@K: $PRECISION"
|
|
# Warn-then-fail ratchet: advisory only in v1 (exit 0 always).
|
|
if python3 -c "exit(0 if float('$PRECISION') >= 0.1 else 1)" 2>/dev/null; then
|
|
echo "✅ Retrieval quality above minimum threshold"
|
|
else
|
|
echo "⚠️ WARN: Retrieval precision below 0.1 — flywheel may be degraded"
|
|
fi
|
|
|
|
- name: Run retrieval comparison smoke
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: AGENTOPS_RETRIEVAL_SMOKE_AO=/tmp/ao-test bash scripts/retrieval-quality-smoke.sh
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# skill-eval — T1 (≤5min), changed-files-scoped. Gates each CHANGED skill's
|
|
# SKILL.md through Jeff Emanuel's `ms` (meta_skill) lint + validate via
|
|
# scripts/skill-eval.sh (ag-yzoz, #726). Scoping mirrors the eval job's
|
|
# skilldelta pattern: `git diff --name-only` against the PR merge base, kept
|
|
# to skills/<id>/** so ms runs ONLY for the skills the PR touched.
|
|
#
|
|
# ms is a pinned Rust binary (v0.1.2, git+meta_skill@b0bb6bd). The setup step
|
|
# installs it and gates on `ms --version` BEFORE the gate runs. If the install
|
|
# fails the job HARD-FAILS (red) — it never green-skips. A silent skip-on-
|
|
# absent-ms recreates the exact "no skill evaluation" gap this gate closes
|
|
# (the same anti-pattern scripts/skill-eval.sh refuses internally).
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
skill-eval:
|
|
needs: [changes]
|
|
if: >-
|
|
needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
runs-on: ubuntu-latest
|
|
# T1 (≤5min) in steady state: the pinned `ms` binary is cache-keyed on its
|
|
# rev, so every run after the first cold build restores it in seconds and
|
|
# the gate finishes well under the tier budget. The 12-min ceiling is only
|
|
# cold-build headroom — building ms from git source on nightly is the one
|
|
# slow path, and it must not be cancelled mid-compile (a cancel would look
|
|
# like a flaky red rather than the deterministic hard-fail this gate owns).
|
|
timeout-minutes: 12
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
# The changed-skill diff is computed against the PR merge base, so the
|
|
# full history (or at least the base ref) must be present.
|
|
fetch-depth: 0
|
|
|
|
# ms v0.1.2 transitively depends on `fsqlite-types`, which uses
|
|
# `#![feature(portable_simd)]` — that requires the NIGHTLY channel
|
|
# (stable rejects it with E0554). Pin nightly so the install compiles.
|
|
- name: Set up Rust toolchain (nightly — ms needs portable_simd)
|
|
uses: dtolnay/rust-toolchain@nightly
|
|
|
|
# Cache the compiled `ms` binary keyed on its pinned rev. A cache hit
|
|
# restores ~/.cargo/bin/ms directly, so post-first-run gates skip the
|
|
# multi-minute git+nightly compile and stay inside the T1 budget. The key
|
|
# changes only when MS_REV changes, so the cache is correct-by-pinning.
|
|
- name: Cache pinned ms binary
|
|
id: ms-cache
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: ~/.cargo/bin/ms
|
|
key: ms-bin-b0bb6bd5-nightly-${{ runner.os }}
|
|
|
|
# Pinned `ms` install. HARD-FAIL on any install failure — never
|
|
# green-skip. The `ms --version` gate proves the binary is on PATH and is
|
|
# the pinned 0.1.2 before the eval gate runs; if either the install or the
|
|
# version probe fails, the step (and the job) goes RED. On a cache hit the
|
|
# compile is skipped, but the version gate ALWAYS runs.
|
|
- name: Install pinned ms (meta_skill v0.1.2) and gate on --version
|
|
run: |
|
|
set -euo pipefail
|
|
MS_REV=b0bb6bd541abc6389a272d03f80d4fad108c1df6 # ms 0.1.2
|
|
if [[ "${{ steps.ms-cache.outputs.cache-hit }}" == "true" ]] && ms --version >/dev/null 2>&1; then
|
|
echo "ms restored from cache: $(ms --version)"
|
|
elif ! cargo +nightly install \
|
|
--git https://github.com/Dicklesworthstone/meta_skill \
|
|
--rev "$MS_REV" \
|
|
--bin ms \
|
|
--locked; then
|
|
echo "::error::skill-eval: pinned ms (meta_skill v0.1.2 @ ${MS_REV}) install FAILED. Refusing to green-skip — the skill-eval gate cannot run without ms, and a silent skip recreates the no-evaluation gap this gate exists to close." >&2
|
|
exit 1
|
|
fi
|
|
if ! ms --version; then
|
|
echo "::error::skill-eval: ms installed but \`ms --version\` failed — binary not on PATH or broken. Hard-failing rather than skipping." >&2
|
|
exit 1
|
|
fi
|
|
got="$(ms --version | awk '{print $2}')"
|
|
if [[ "$got" != "0.1.2" ]]; then
|
|
echo "::error::skill-eval: pinned ms version mismatch — expected 0.1.2, got '${got}'. Hard-failing." >&2
|
|
exit 1
|
|
fi
|
|
echo "ms pinned-install OK: $(ms --version)"
|
|
|
|
# Compute the set of skills whose skills/<id>/** changed in this PR, then
|
|
# run scripts/skill-eval.sh for each. Mirrors the eval job's skilldelta
|
|
# convention (git diff --name-only). For a push (no PR), fall back to the
|
|
# last commit (HEAD~1).
|
|
- name: Eval changed skills
|
|
run: |
|
|
set -euo pipefail
|
|
chmod +x scripts/skill-eval.sh
|
|
|
|
if [[ -n "${GITHUB_BASE_REF:-}" ]]; then
|
|
base="origin/${GITHUB_BASE_REF}"
|
|
git fetch --no-tags --depth=1 origin "${GITHUB_BASE_REF}" || true
|
|
range="${base}...HEAD"
|
|
else
|
|
range="HEAD~1...HEAD"
|
|
fi
|
|
|
|
# skills/<id>/** → unique <id>, excluding _fixtures/_scaffolding.
|
|
mapfile -t changed < <(
|
|
git diff --name-only "$range" -- 'skills/**' \
|
|
| awk -F/ 'NF>=2 {print $2}' \
|
|
| grep -vE '^_' \
|
|
| sort -u
|
|
)
|
|
|
|
if [[ "${#changed[@]}" -eq 0 ]]; then
|
|
echo "No changed skills/<id>/** in ${range}; skill-eval has nothing to gate."
|
|
exit 0
|
|
fi
|
|
|
|
echo "Changed skills to evaluate: ${changed[*]}"
|
|
rc=0
|
|
for id in "${changed[@]}"; do
|
|
if [[ ! -f "skills/${id}/SKILL.md" ]]; then
|
|
echo "::notice::skills/${id}/ has no SKILL.md (deleted or non-skill dir); skipping."
|
|
continue
|
|
fi
|
|
echo "::group::skill-eval ${id}"
|
|
if ! scripts/skill-eval.sh "$id"; then
|
|
echo "::error::skill-eval gate FAILED for skill '${id}' (blocking ms finding)."
|
|
rc=1
|
|
fi
|
|
echo "::endgroup::"
|
|
done
|
|
exit "$rc"
|
|
|
|
# ── I0-INFORMATIONAL retrieval-probe receipt (ag-iyu4) ────────────────
|
|
# Wires scripts/skill-probe-i0.sh — the deterministic lexical trigger
|
|
# ranker (scan_descriptions.py --probe, ag-7led) — as an I0 step: it RUNS
|
|
# and REPORTS a per-skill JSON receipt artifact (.agents/ao/skill-eval/
|
|
# <id>.json, uploaded below), but it is NOT a PR check. I0 =
|
|
# informational, not required and not advisory: `continue-on-error: true`
|
|
# lives INSIDE this existing job rather than as a separate job, so it adds
|
|
# no new GitHub check and cannot block (or even annotate-as-advisory) a
|
|
# merge. The step's only finding is a NON-DETERMINISTIC probe (byte-diff
|
|
# across two runs), surfaced as a `::warning::` — that determinism
|
|
# assertion is what must hold before the probe is ever promoted to a
|
|
# blocking gate.
|
|
#
|
|
# GATE-PROMOTION GUARD — 2-WEEK STABILITY BASELINE: do NOT promote
|
|
# `--probe` from this I0 lane to a blocking gate until this receipt lane
|
|
# has run green and byte-stable across the corpus for ≥2 weeks of merges.
|
|
# See docs/contracts/ci-jobs.yaml (skill-eval entry) and the
|
|
# `trigger_probes:` note in skills/skill-builder/SKILL.md.
|
|
- name: Retrieval probe receipt (I0-informational; not a PR check)
|
|
# Runs on every skill-eval invocation. continue-on-error so a
|
|
# non-deterministic probe never blocks — it only reports.
|
|
continue-on-error: true
|
|
run: |
|
|
set +e
|
|
chmod +x scripts/skill-probe-i0.sh
|
|
scripts/skill-probe-i0.sh skills .agents/ao/skill-eval
|
|
rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "::warning::skill-probe-i0: a probe was non-deterministic (rc=${rc}). I0-informational — not blocking; the determinism assertion exists to catch this BEFORE any gate promotion."
|
|
fi
|
|
# I0: never propagate non-zero from the informational lane.
|
|
exit 0
|
|
|
|
- name: Upload retrieval-probe receipts (I0 artifact)
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: skill-retrieval-probe-receipts
|
|
path: .agents/ao/skill-eval/*.json
|
|
# Receipts live under the hidden .agents/ tree, so hidden-file
|
|
# inclusion is required or the upload would silently skip them.
|
|
include-hidden-files: true
|
|
if-no-files-found: ignore
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# process-hygiene — quarantine-empty, test-count non-regression, doc-release
|
|
# stabilization, file-manifest overlap, and the advisory test-staleness,
|
|
# swarm-evidence, and lint-evidence-lines checks. Absorbs:
|
|
# validate-quarantine-empty, validate-test-count-noregress, doc-release-gate,
|
|
# file-manifest-overlap, check-test-staleness (advisory), swarm-evidence
|
|
# (advisory), lint-evidence-lines (advisory).
|
|
#
|
|
# learning-coherence RETIRED 2026-05-19. Dead-by-design: triggered only by
|
|
# changes to .agents/learnings/** paths, which are gitignored. The validator
|
|
# script remains at scripts/validate-learning-coherence.sh for ad-hoc local
|
|
# use against the operator's .agents/learnings/ corpus.
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
process-hygiene:
|
|
needs: [changes]
|
|
if: >-
|
|
needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' ||
|
|
needs.changes.outputs.ci == 'true' || needs.changes.outputs.go == 'true' ||
|
|
needs.changes.outputs.shell == 'true' || needs.changes.outputs.docs == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
# validate-test-count-noregress needs the PR base ref for the
|
|
# before/after per-package test-count diff.
|
|
fetch-depth: 0
|
|
|
|
- name: Run doc-release stabilization gate
|
|
if: needs.changes.outputs.docs == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x tests/docs/validate-doc-release.sh
|
|
./tests/docs/validate-doc-release.sh
|
|
|
|
- name: Skill-count SSOT red-green acceptance (cp-9wvq)
|
|
if: needs.changes.outputs.docs == 'true' || needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x tests/docs/test-skill-count-ssot.sh
|
|
./tests/docs/test-skill-count-ssot.sh
|
|
|
|
- name: Check tests/_quarantine/ is empty (GOALS.md directive D3)
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.hooks == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-quarantine-empty.sh
|
|
./scripts/check-quarantine-empty.sh
|
|
|
|
- name: Validate manifest overlap script
|
|
if: needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-file-manifest-overlap.sh
|
|
bash -n scripts/check-file-manifest-overlap.sh
|
|
OVERLAP_TMP=$(mktemp)
|
|
CLEAN_TMP=$(mktemp)
|
|
trap 'rm -f "$OVERLAP_TMP" "$CLEAN_TMP"' EXIT
|
|
# Self-test: overlapping manifests should fail
|
|
echo '[{"id":"1","subject":"A","files":["a.go"]},{"id":"2","subject":"B","files":["a.go"]}]' > "$OVERLAP_TMP"
|
|
if scripts/check-file-manifest-overlap.sh "$OVERLAP_TMP"; then
|
|
echo "expected overlapping manifests to fail" >&2
|
|
exit 1
|
|
fi
|
|
# Self-test: non-overlapping should pass
|
|
echo '[{"id":"1","subject":"A","files":["a.go"]},{"id":"2","subject":"B","files":["b.go"]}]' > "$CLEAN_TMP"
|
|
scripts/check-file-manifest-overlap.sh "$CLEAN_TMP"
|
|
|
|
- name: Test-count non-regression gate (scripts/check-test-count-regression.sh)
|
|
# Pass the base ref via env (not `${{ }}` interpolation inside run:) so a
|
|
# branch name carrying shell metacharacters cannot inject into the runner
|
|
# (semgrep run-shell-injection / CWE-78). The script reads $BASE_REF.
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
env:
|
|
BASE_REF: origin/${{ github.base_ref }}
|
|
run: |
|
|
chmod +x scripts/check-test-count-regression.sh
|
|
./scripts/check-test-count-regression.sh
|
|
|
|
- name: Test-isolation ratchet (scripts/check-test-isolation.sh)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
run: |
|
|
chmod +x scripts/check-test-isolation.sh
|
|
./scripts/check-test-isolation.sh
|
|
|
|
- name: Check for stale tests (advisory)
|
|
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.shell == 'true' || needs.changes.outputs.ci == 'true'
|
|
continue-on-error: true
|
|
run: |
|
|
chmod +x scripts/check-test-staleness.sh
|
|
bash scripts/check-test-staleness.sh
|
|
|
|
- name: Validate swarm evidence files (advisory; if present)
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
continue-on-error: true
|
|
run: |
|
|
chmod +x scripts/validate-swarm-evidence.sh
|
|
bash scripts/validate-swarm-evidence.sh
|
|
|
|
- name: Validate file manifests in evidence (advisory; if present)
|
|
if: needs.changes.outputs.skills == 'true' || needs.changes.outputs.ci == 'true'
|
|
continue-on-error: true
|
|
run: |
|
|
EVIDENCE_DIR=".agents/swarm/results"
|
|
if [[ ! -d "$EVIDENCE_DIR" ]]; then
|
|
echo "SKIP: no evidence directory — nothing to check"
|
|
exit 0
|
|
fi
|
|
MANIFEST_FILES=$(find "$EVIDENCE_DIR" -name '*.json' -type f -exec jq -e '.files_modified // empty' {} + 2>/dev/null || true)
|
|
if [[ -z "$MANIFEST_FILES" ]]; then
|
|
echo "SKIP: no file manifests in evidence — nothing to check"
|
|
exit 0
|
|
fi
|
|
echo "File manifests found in evidence — overlap check delegated to file-manifest-overlap step"
|
|
|
|
- name: Lint Evidence claims (pre-AP#7; advisory)
|
|
# I0 advisory wiring for scripts/lint-evidence-lines.sh (soc-dspz). Runs
|
|
# the pre-AP#7 check on PR-body `Evidence:` lines so authors get an
|
|
# annotation before the strict verify-pr-evidence-claims gate in summary.
|
|
if: github.event_name == 'pull_request'
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set +e
|
|
chmod +x scripts/lint-evidence-lines.sh
|
|
scripts/lint-evidence-lines.sh "${{ github.event.pull_request.number }}"
|
|
rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "::warning::lint-evidence-lines.sh found issues (advisory; rc=$rc)"
|
|
fi
|
|
# advisory: never propagate non-zero
|
|
exit 0
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# summary — aggregate verdict over the purpose jobs PLUS AP#7 mechanical
|
|
# Evidence-claim verification (was validate-pr-evidence-claims). AP#7 must
|
|
# run after all sibling jobs so their per-job logs are available, so it lives
|
|
# here rather than in process-hygiene. Invokes scripts/verify-gate-claim.sh.
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
summary:
|
|
needs: [changes, go-gate-shadow, correctness, lint, security, skill-gates, skills-integrity, contracts-sync, codex-parity, doctrine-proof, eval, skill-eval, process-hygiene]
|
|
runs-on: ubuntu-latest
|
|
if: always()
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Check results
|
|
run: |
|
|
echo "=== CI Summary ==="
|
|
echo "go-gate-shadow: ${{ needs.go-gate-shadow.result }}"
|
|
echo "correctness: ${{ needs.correctness.result }}"
|
|
echo "lint: ${{ needs.lint.result }}"
|
|
echo "security: ${{ needs.security.result }}"
|
|
echo "skill-gates: ${{ needs.skill-gates.result }}"
|
|
echo "skills-integrity: ${{ needs.skills-integrity.result }}"
|
|
echo "contracts-sync: ${{ needs.contracts-sync.result }}"
|
|
echo "codex-parity: ${{ needs.codex-parity.result }}"
|
|
echo "doctrine-proof: ${{ needs.doctrine-proof.result }}"
|
|
echo "eval: ${{ needs.eval.result }}"
|
|
echo "skill-eval: ${{ needs.skill-eval.result }}"
|
|
echo "process-hygiene: ${{ needs.process-hygiene.result }}"
|
|
|
|
# Fail on actual failures. On release tags, skipped purpose jobs are
|
|
# also failures because validate.yml must be a full exact-SHA release
|
|
# verdict, not a path-filtered partial signal.
|
|
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
|
|
echo ""
|
|
echo "❌ Some checks failed (failure detected)"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
|
release_needs_json='${{ toJson(needs) }}'
|
|
release_skipped="$(
|
|
NEEDS_JSON="$release_needs_json" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
# All purpose jobs run on release tags (changes sets every output
|
|
# true), so no job should be skipped. None are allowlisted.
|
|
allowed_skips = set()
|
|
|
|
needs = json.loads(os.environ["NEEDS_JSON"])
|
|
unexpected = sorted(
|
|
name
|
|
for name, metadata in needs.items()
|
|
if metadata.get("result") == "skipped" and name not in allowed_skips
|
|
)
|
|
|
|
for name in unexpected:
|
|
print(name)
|
|
PY
|
|
)"
|
|
if [[ -n "$release_skipped" ]]; then
|
|
echo ""
|
|
echo "❌ Release-tag Validate had unexpected skipped jobs; skipped release lanes are not a release verdict"
|
|
printf '%s\n' "$release_skipped" | sed 's/^/- /'
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "✅ All checks passed"
|
|
|
|
# ── AP#7 — verify Evidence claims in PR body (was validate-pr-evidence-claims) ──
|
|
- name: Extract Evidence claims from PR body
|
|
id: claims
|
|
if: github.event_name == 'pull_request'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
body="$(gh pr view "${{ github.event.pull_request.number }}" --json body --jq .body)"
|
|
claims="$(printf '%s\n' "$body" | sed -n 's/^Evidence:[[:space:]]*//p' || true)"
|
|
if [[ -z "$claims" ]]; then
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
echo "::notice::No Evidence: line in PR body; AP#7 verification skipped"
|
|
exit 0
|
|
fi
|
|
printf '%s\n' "$claims" > /tmp/evidence-claims.txt
|
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
echo "count=$(wc -l < /tmp/evidence-claims.txt | tr -d ' ')" >> "$GITHUB_OUTPUT"
|
|
echo "Evidence claims to verify ($(wc -l < /tmp/evidence-claims.txt | tr -d ' ')):"
|
|
cat /tmp/evidence-claims.txt
|
|
|
|
- name: Fetch this workflow run's logs (per-job, durable)
|
|
if: github.event_name == 'pull_request' && steps.claims.outputs.skip != 'true'
|
|
id: fetch_log
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Per-job log fetch (soc-1nsx). The per-job endpoint serves each
|
|
# completed job's log as soon as that job finishes, with no whole-run
|
|
# eventual-consistency window. We page through the run's jobs, fetch
|
|
# each completed (non-skipped) sibling's log, and concatenate.
|
|
: > /tmp/run.log
|
|
page=1
|
|
jobs_seen=0
|
|
jobs_with_logs=0
|
|
while : ; do
|
|
page_json="$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs?per_page=100&page=${page}")"
|
|
page_count="$(jq '.jobs | length' <<<"$page_json")"
|
|
if [[ "${page_count}" -eq 0 ]]; then
|
|
break
|
|
fi
|
|
jobs_seen=$((jobs_seen + page_count))
|
|
while IFS=$'\t' read -r job_id job_conclusion; do
|
|
if [[ -z "${job_conclusion}" || "${job_conclusion}" = "skipped" ]]; then
|
|
continue
|
|
fi
|
|
if gh api "repos/${{ github.repository }}/actions/jobs/${job_id}/logs" >> /tmp/run.log 2>/dev/null; then
|
|
jobs_with_logs=$((jobs_with_logs + 1))
|
|
fi
|
|
done < <(jq -r '.jobs[] | "\(.id)\t\(.conclusion // "")"' <<<"$page_json")
|
|
if [[ "${page_count}" -lt 100 ]]; then
|
|
break
|
|
fi
|
|
page=$((page + 1))
|
|
done
|
|
log_bytes="$(wc -c < /tmp/run.log)"
|
|
echo "fetched per-job logs: ${jobs_with_logs}/${jobs_seen} jobs, ${log_bytes} bytes"
|
|
if [[ "${jobs_with_logs}" -eq 0 ]]; then
|
|
echo "log_ok=false" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::No completed sibling jobs returned logs (${jobs_seen} jobs total); skipping AP#7 verification this run."
|
|
else
|
|
echo "log_ok=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Verify each Evidence claim
|
|
if: github.event_name == 'pull_request' && steps.claims.outputs.skip != 'true' && steps.fetch_log.outputs.log_ok == 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
chmod +x scripts/verify-gate-claim.sh
|
|
failures=0
|
|
total=0
|
|
while IFS= read -r claim; do
|
|
[[ -z "$claim" ]] && continue
|
|
total=$((total + 1))
|
|
if scripts/verify-gate-claim.sh --log /tmp/run.log \
|
|
"pr-${{ github.event.pull_request.number }}" "$claim" >/dev/null; then
|
|
echo "::notice::Evidence claim verified: $claim"
|
|
else
|
|
failures=$((failures + 1))
|
|
echo "::error::Evidence claim absent from workflow logs: $claim"
|
|
fi
|
|
done < /tmp/evidence-claims.txt
|
|
if [[ "$failures" -gt 0 ]]; then
|
|
echo "::error::$failures of $total Evidence claim(s) failed verification (ship-loop anti-pattern #7)"
|
|
exit 1
|
|
fi
|
|
echo "::notice::All $total Evidence claim(s) verified against workflow run logs"
|
|
|
|
- name: AP#7 verification unavailable (no sibling logs)
|
|
if: github.event_name == 'pull_request' && steps.claims.outputs.skip != 'true' && steps.fetch_log.outputs.log_ok != 'true'
|
|
run: |
|
|
echo "::notice::AP#7 verification skipped — no completed sibling jobs returned logs. The PR's Evidence: line was extracted but cannot be cross-checked against per-job logs in this CI shape (likely: only this job ran, or all siblings were skipped)."
|