An undocumented tool is, from inside an agent session, indistinguishable from a tool that was never written. The framework shipped 26 git wrappers and named 6 of them in its resident index docs — 23% discoverability, with pr-review.sh among the missing. The observable consequence was an agent obeying Constitution gate 7 as best it could see it, reaching for raw curl, sending GitHub's APPROVE to a Gitea host, and getting HTTP 200 with the review silently filed PENDING. Three times. That is not a discipline failure and no amount of prose fixes it. Four changes, each converting a rule that decayed into a mechanism that cannot: - check-tools-index.sh (new, CI-blocking): every tool in an enforced suite must be named in a resident index doc, and every tool an index names must exist. The git suite is enforced now; other suites report coverage without failing, so the ratchet tightens one reviewed PR at a time instead of landing as one sweep. The enforced list is framework-owned rather than a marker inside operator-owned TOOLS.md — a doc marker would let an operator silence the gate on exactly the host where it matters most. Carries --self-test, because a checker that only ever passes is indistinguishable from one that is not running. - TOOLS-REFERENCE.md: complete 28-entry git index, plus the APPROVED/APPROVE dialect note that explains why pr-review.sh is not a formality. - mosaic-worktree.sh + wrapper-guard.sh (upstreamed): the rule "big work goes on a work filesystem" already existed in prose, and 255 GB accumulated in $HOME across 842 directories anyway, under five simultaneous placement conventions on one host. The helper therefore exposes no placement decision — given a branch name, every path is derived from `git worktree list --porcelain`. Worktrees rather than clones because enumerability is the only thing that makes reclaim safe, and reclaim is by evidence (clean tree + no unpushed commits), never by size or age. The guard blocks three mechanically-detectable mistakes and nothing else: a checkout into $HOME, a raw provider-API write to an endpoint that has a wrapper, and the literal APPROVE event. Reads pass untouched. - STANDARDS.md: model tiering as a standard, named by capability class so it survives a model generation. Start cheapest, escalate on evidence, benchmark before demoting a task class, and keep the class->model binding in operator config with the DB-backed config service as the end state. Registering the guard in runtime/claude/settings.json is the point of upstreaming it: ~/.claude/settings.json is a framework-managed copy, so a hand-added hook there is destroyed by the next upgrade. In the template it survives, and it reaches every host instead of one.
152 lines
6.6 KiB
YAML
152 lines
6.6 KiB
YAML
# &node_image is the pre-baked CI base built by .woodpecker/ci-image.yml:
|
|
# node:24-alpine + python3/make/g++/postgresql-client + pnpm + a warm pnpm
|
|
# store. The install step resolves from the baked store (--prefer-offline)
|
|
# instead of paying a ~731s cold fetch + native compile every run.
|
|
variables:
|
|
- &node_image 'git.mosaicstack.dev/mosaicstack/stack/ci-base:latest'
|
|
- &enable_pnpm 'corepack enable'
|
|
|
|
when:
|
|
# PR + manual CI run on any branch — the pull_request pipeline is the merge gate.
|
|
# push CI is restricted to protected branches (main) so a feature-branch push no
|
|
# longer fires a redundant SECOND pipeline alongside its PR pipeline. This ~halves
|
|
# CI load on the storage-constrained runner with zero loss of gating (branch
|
|
# protection requires no push/ci status context; main still gets full push CI).
|
|
- event: [pull_request, manual]
|
|
- event: push
|
|
branch: main
|
|
|
|
# Turbo remote cache (turbo.mosaicstack.dev) is configured via Woodpecker
|
|
# repository-level environment variables (TURBO_API, TURBO_TEAM, TURBO_TOKEN).
|
|
# This avoids from_secret which is blocked on pull_request events.
|
|
# If the env vars aren't set, turbo falls back to local cache only.
|
|
|
|
steps:
|
|
install:
|
|
image: *node_image
|
|
commands:
|
|
- corepack enable
|
|
# python3/make/g++ are baked into ci-base; --prefer-offline resolves from
|
|
# the baked pnpm store.
|
|
- pnpm install --frozen-lockfile --prefer-offline
|
|
|
|
# Blocking gate: public framework package must contain no operator-specific
|
|
# personal data or private $HOME defaults. Runs early (no node_modules needed).
|
|
sanitization:
|
|
image: *node_image
|
|
commands:
|
|
- apk add --no-cache bash
|
|
- bash packages/mosaic/framework/tools/quality/scripts/verify-sanitized.sh
|
|
# Resident line-count ceiling over framework-owned resident files
|
|
# (Constitution + dispatcher + each RUNTIME.md slice). See DESIGN §7 / R9.
|
|
- bash packages/mosaic/framework/tools/quality/scripts/check-resident-budget.sh --self-test
|
|
- bash packages/mosaic/framework/tools/quality/scripts/check-resident-budget.sh
|
|
# Test-membership guard (#1017): also first link of test:framework-shell.
|
|
# Invoked from BOTH surfaces it audits (F2, PR #1018) — the guard is link
|
|
# [0] of the pnpm chain, so severing that chain would silence it together
|
|
# with everything it guards; this direct line keeps one instrument running.
|
|
- bash packages/mosaic/framework/tools/quality/scripts/check-test-enumeration.sh
|
|
# Tool-index gate: a shipped wrapper that appears in no resident index doc
|
|
# is undiscoverable from inside a session, and an agent that cannot learn a
|
|
# wrapper exists reaches for raw curl instead — which is how a Gitea review
|
|
# got filed PENDING three times. Ships-and-documented is one commit, or red.
|
|
- bash packages/mosaic/framework/tools/quality/scripts/check-tools-index.sh --self-test
|
|
- bash packages/mosaic/framework/tools/quality/scripts/check-tools-index.sh
|
|
# Hermetic regression for issue-close.sh (#1081): mocks tea/curl onto PATH
|
|
# and sandboxes a throwaway git repo, so it resolves no real credentials and
|
|
# joins CI directly rather than the exclusions file.
|
|
- bash packages/mosaic/framework/tools/git/test-issue-close-fail-closed.sh
|
|
|
|
# Blocking gate (#791): a framework upgrade must never write or delete an
|
|
# operator-owned path. The HARD GATE proves an unanticipated operator sentinel
|
|
# survives a keep-mode reseed byte-identical (with rsync present AND absent —
|
|
# keep mode is a single cp-based path that must not depend on rsync), and that a
|
|
# corrupt/empty/missing manifest aborts fail-closed leaving operator files
|
|
# untouched (B2/B3). The rollback gate proves a mid-sync failure is rolled back
|
|
# from the pre-update snapshot (B1). The durable-snapshot gate (#791 PR2) proves
|
|
# the retained, operator-scoped pre-update backup is taken before any mutation
|
|
# (0700/0600, secret never logged, retention-pruned) and that the post-sync
|
|
# verify net restores any operator file a manifest bug lets the sync touch. The
|
|
# migration matrix pins the v2→v3 contract-file semantics. Pure bash, no
|
|
# node_modules — runs early alongside sanitization.
|
|
upgrade-guard:
|
|
image: *node_image
|
|
commands:
|
|
- apk add --no-cache bash rsync
|
|
- bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-manifest-guard.sh
|
|
- bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh
|
|
- bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-durable-snapshot.sh
|
|
- bash packages/mosaic/framework/tools/quality/scripts/test-install-migration.sh
|
|
|
|
typecheck:
|
|
image: *node_image
|
|
commands:
|
|
- *enable_pnpm
|
|
- pnpm typecheck
|
|
depends_on:
|
|
- install
|
|
- sanitization
|
|
- upgrade-guard
|
|
|
|
# lint, format, and test are independent — run in parallel after typecheck
|
|
lint:
|
|
image: *node_image
|
|
commands:
|
|
- *enable_pnpm
|
|
- pnpm lint
|
|
depends_on:
|
|
- typecheck
|
|
|
|
format:
|
|
image: *node_image
|
|
commands:
|
|
- *enable_pnpm
|
|
- pnpm format:check
|
|
depends_on:
|
|
- typecheck
|
|
|
|
test:
|
|
image: *node_image
|
|
environment:
|
|
# Avoid the namespace-level Woodpecker DB service named "postgres".
|
|
# The Kubernetes backend exposes service containers by step name.
|
|
DATABASE_URL: postgresql://mosaic:mosaic@ci-postgres:5432/mosaic
|
|
commands:
|
|
- *enable_pnpm
|
|
# openssl (#912) is the wake HMAC signer: the digest H1/H2, beacon B12,
|
|
# and install I8 legs hard-require it in CI. It is baked into ci-base via
|
|
# Dockerfile.ci, but ci-base only rebuilds on push-to-main/tag — this
|
|
# `apk add` guarantees openssl is present on PR pipelines too (and is a
|
|
# fast no-op once the rebuilt image already ships it).
|
|
- apk add --no-cache openssl
|
|
# postgresql-client (pg_isready) is baked into ci-base.
|
|
# Wait up to 60s for CI postgres to be ready; fail fast if it never comes up.
|
|
- |
|
|
ready=0
|
|
for i in $(seq 1 60); do
|
|
if pg_isready -h ci-postgres -p 5432 -U mosaic; then
|
|
ready=1
|
|
break
|
|
fi
|
|
echo "Waiting for ci-postgres ($i/60)..."
|
|
sleep 1
|
|
done
|
|
if [ "$ready" -ne 1 ]; then
|
|
echo "ci-postgres did not become ready" >&2
|
|
exit 1
|
|
fi
|
|
# Run migrations (DATABASE_URL is set in environment above)
|
|
- pnpm --filter @mosaicstack/db run db:migrate
|
|
# Run all tests
|
|
- pnpm test
|
|
depends_on:
|
|
- typecheck
|
|
|
|
services:
|
|
ci-postgres:
|
|
image: pgvector/pgvector:pg17
|
|
environment:
|
|
POSTGRES_USER: mosaic
|
|
POSTGRES_PASSWORD: mosaic
|
|
POSTGRES_DB: mosaic
|