Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 27 additions & 30 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
name: CI

# The check list is dev.json, run by scripts/dev/check.sh (the development kit's vendored runner).
# `check` sets the toolchains up from the manifest and runs every step. `test` is the operating
# system and Go version matrix: its job names are required by branch protection, and its legs run
# the Go steps of dev.json natively on each platform.

on:
pull_request:
push:
Expand Down Expand Up @@ -49,37 +54,29 @@ jobs:
env:
CGO_ENABLED: "0"

registry-template:
name: MCP registry server.json template
check:
name: check
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Validate rendered template against the vendored schema
shell: bash
# The schema copy is the one the release workflow's pinned mcp-publisher
# enforces; bump both together (ADR-0013).
run: |
pip3 install --quiet jsonschema==4.23.0
sed 's/__VERSION__/9.9.9-rc.1/g' build/mcp-registry/server.json.tmpl > /tmp/server.json
python3 - <<'EOF'
import json, jsonschema, sys
schema = json.load(open('build/mcp-registry/server.schema-2025-12-11.json'))
doc = json.load(open('/tmp/server.json'))
errors = [
'/'.join(map(str, e.path)) + ' - ' + e.message
for e in jsonschema.Draft202012Validator(schema).iter_errors(doc)
]
# Server-side rules the registry enforces beyond its schema, learned
# from live rejections (v0.5.0-rc.1): an OCI package carries its
# version in the identifier tag and must not have a version member.
for i, package in enumerate(doc.get('packages', [])):
if package.get('registryType') == 'oci':
if 'version' in package:
errors.append(f'packages/{i} - OCI packages must not have a version member')
if ':' not in package.get('identifier', ''):
errors.append(f'packages/{i} - OCI identifier must carry the version as its tag')
for e in errors:
print('INVALID:', e, file=sys.stderr)
sys.exit(1 if errors else 0)
EOF
with:
persist-credentials: false
# First, so that a manifest that does not parse fails before the long steps start.
- name: The manifest parses and its first step runs
run: scripts/dev/check.sh --manifest dev.json --only local-paths
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: stable
cache: true
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Setup steps
run: scripts/dev/check.sh --setup
# Includes the MCP registry server.json template check (scripts/check-registry-template.py).
- name: Checks
run: scripts/dev/check.sh --all
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@

.vscode/*
!.vscode/tasks.json

# Python virtual environment of the development kit (dev.json).
/.venv/
48 changes: 48 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Working in this repository

This file is read by Codex as `AGENTS.md` and by Claude as `CLAUDE.md`; the two are the same text.
Edit `AGENTS.md` and run `scripts/sync-agent-docs.sh` to regenerate `CLAUDE.md`.

## Before you open a pull request

- Run `scripts/dev/check.sh` (or `jpdev check` where the kit is installed) once. It runs the
checks listed in `dev.json` in order and prints one summary block.
- Paste that summary block in the pull request body, under a "Checks" heading.
- If a check fails: read its first lines in the block, fix the cause, re-run that one check with
`--only NAME`, then run the whole list once more. Do not loop on the whole list.
- Start no CI watch. Push, report once, and stop; CI owns everything deterministic.

## Rules

- Write test vectors and fixtures by hand. Never record them "from a run" of the code under test.
- Keep machine-local paths and personal names out of tracked files, commit messages and pull
request bodies. Refer to people by role or handle where a name is needed.
- Sign off every commit (`git commit -s`). Never force-push; never rewrite published history.
- End a process you started by its process id. Never use a pattern kill (`pkill`, `killall`).
- One pull request per issue or batch. State what changed and why in plain, factual wording.
- A change to a public surface (a schema, a contract, a command's output) says so in the pull
request and changes its vectors and documentation in the same pull request.
- If a safeguard stops you (a permission prompt, a refused command, a failed gate), stop and say
so in your report. Do not work around it.

## Reports

Your final report is short: the pull request and its head commit, what changed, the check
summary block, and anything you stopped on or chose not to do.

## This repository

- The checks are `dev.json`, run by `scripts/dev/check.sh`; the `check` job in CI runs the same
list. The `test` job is a matrix (one leg per operating system and Go version, the Go 1.24 floor
included) whose leg names branch protection requires; its legs run the Go steps natively and
are the one place the list is repeated in the workflow. A Go step added to `dev.json` is added
to the matrix as well.
- `go.mod` carries `go 1.24.0`: the runtime reads through `os.Root`, a Go 1.24 standard-library
type. Do not raise the floor without saying so in the pull request.
- The specification is owned by `judgment-pack-spec`; the specification artifacts embedded here
are release-pinned and byte-exact (`.gitattributes`). Never add a runtime fetch of mutable
specification branches.
- Every pull request carries a one-line `Material-decision impact:` declaration, and a material
decision needs the recorded cross-vendor review described in `docs/adr/README.md`.
- Releases follow `docs/releasing.md` and `VERSIONING.md`; the release workflow is not part of
the `check` job.
48 changes: 48 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Working in this repository

This file is read by Codex as `AGENTS.md` and by Claude as `CLAUDE.md`; the two are the same text.
Edit `AGENTS.md` and run `scripts/sync-agent-docs.sh` to regenerate `CLAUDE.md`.

## Before you open a pull request

- Run `scripts/dev/check.sh` (or `jpdev check` where the kit is installed) once. It runs the
checks listed in `dev.json` in order and prints one summary block.
- Paste that summary block in the pull request body, under a "Checks" heading.
- If a check fails: read its first lines in the block, fix the cause, re-run that one check with
`--only NAME`, then run the whole list once more. Do not loop on the whole list.
- Start no CI watch. Push, report once, and stop; CI owns everything deterministic.

## Rules

- Write test vectors and fixtures by hand. Never record them "from a run" of the code under test.
- Keep machine-local paths and personal names out of tracked files, commit messages and pull
request bodies. Refer to people by role or handle where a name is needed.
- Sign off every commit (`git commit -s`). Never force-push; never rewrite published history.
- End a process you started by its process id. Never use a pattern kill (`pkill`, `killall`).
- One pull request per issue or batch. State what changed and why in plain, factual wording.
- A change to a public surface (a schema, a contract, a command's output) says so in the pull
request and changes its vectors and documentation in the same pull request.
- If a safeguard stops you (a permission prompt, a refused command, a failed gate), stop and say
so in your report. Do not work around it.

## Reports

Your final report is short: the pull request and its head commit, what changed, the check
summary block, and anything you stopped on or chose not to do.

## This repository

- The checks are `dev.json`, run by `scripts/dev/check.sh`; the `check` job in CI runs the same
list. The `test` job is a matrix (one leg per operating system and Go version, the Go 1.24 floor
included) whose leg names branch protection requires; its legs run the Go steps natively and
are the one place the list is repeated in the workflow. A Go step added to `dev.json` is added
to the matrix as well.
- `go.mod` carries `go 1.24.0`: the runtime reads through `os.Root`, a Go 1.24 standard-library
type. Do not raise the floor without saying so in the pull request.
- The specification is owned by `judgment-pack-spec`; the specification artifacts embedded here
are release-pinned and byte-exact (`.gitattributes`). Never add a runtime fetch of mutable
specification branches.
- Every pull request carries a one-line `Material-decision impact:` declaration, and a material
decision needs the recorded cross-vendor review described in `docs/adr/README.md`.
- Releases follow `docs/releasing.md` and `VERSIONING.md`; the release workflow is not part of
the `check` job.
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ can pick it up. You're welcome to take it back while it's still open.

## Before opening a pull request

`scripts/dev/check.sh` runs every check CI runs, from the list in `dev.json` (`--setup` installs
the Python dependency of one check first); run it once and paste its summary block in the pull
request body. The commands below are its Go steps:

```bash
env GO111MODULE=on go fmt ./...
env GO111MODULE=on go vet ./...
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,13 @@ env GO111MODULE=on go test ./...
env GO111MODULE=on CGO_ENABLED=0 go build -trimpath ./cmd/jpack
```

`scripts/dev/check.sh` runs every check CI runs, in order, and prints one summary block of passes,
failures and skips; `--setup` runs the install steps first and `--only NAME` re-runs one check. The
checks are listed in [`dev.json`](dev.json), the manifest of the Judgment Pack development kit
(v0.1.0), whose runner is vendored here unchanged. `AGENTS.md` and `CLAUDE.md` carry the same
instructions for agent sessions, shared rules first and this repository's own after them; CI checks
that the two are identical (`scripts/sync-agent-docs.sh` writes `CLAUDE.md` from `AGENTS.md`).

See [CONTRIBUTING.md](CONTRIBUTING.md), [docs/architecture.md](docs/architecture.md), and the
[maintainer release runbook](docs/releasing.md).

Expand Down
23 changes: 23 additions & 0 deletions dev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"version": 1,
"toolchains": {
"go": {},
"python": {"version": "3.12", "venv": ".venv"}
},
"setup": [
{"name": "python-deps", "run": "python3 -m pip install --disable-pip-version-check -q jsonschema==4.23.0", "toolchain": "python", "timeout": 600}
],
"checks": [
{"name": "local-paths", "run": "if git grep -nE '(^|[^A-Za-z0-9_])/(home|tmp)/[A-Za-z0-9_.-]+' -- ':!*.png' ':!*.jpg' ':!internal/policy/testdata/candidate-uri-cases.json'; then echo 'a machine-local path is in tracked text'; exit 1; fi", "toolchain": "shell"},
{"name": "agent-docs", "run": "scripts/sync-agent-docs.sh --check .", "toolchain": "shell", "summary": "^sync-agent-docs:"},
{"name": "diff-check", "run": "git diff --check \"$(git hash-object -t tree /dev/null)\"", "toolchain": "shell"},
{"name": "go-fmt", "run": "unformatted=$(gofmt -l $(go list -f '{{.Dir}}' ./...)); if [ -n \"$unformatted\" ]; then echo 'not gofmt-formatted:'; echo \"$unformatted\"; exit 1; fi", "toolchain": "go", "timeout": 600},
{"name": "go-vet", "run": "go vet ./...", "toolchain": "go", "timeout": 3000},
{"name": "go-test", "run": "go test ./...", "toolchain": "go",
"summary": "^(ok|FAIL|---) ", "timeout": 3000},
{"name": "conformance", "run": "go run ./cmd/jpack spec test-conformance --quiet", "toolchain": "go", "timeout": 1200},
{"name": "build", "run": "CGO_ENABLED=0 go build -trimpath -o /dev/null ./cmd/jpack", "toolchain": "go", "timeout": 1200},
{"name": "registry-template", "run": "python3 scripts/check-registry-template.py", "toolchain": "python",
"summary": "^registry template:", "timeout": 300}
]
}
2 changes: 1 addition & 1 deletion docs/adr/template.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
status: proposed # proposed | accepted | deprecated | superseded by NNNN
date: YYYY-MM-DD
deciders:
deciders:
---

# {short title naming the problem and the chosen solution}
Expand Down
30 changes: 30 additions & 0 deletions scripts/check-registry-template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Validate the MCP registry server.json template, rendered at a sample version, against the
vendored schema. The schema copy is the one the release workflow's pinned mcp-publisher
enforces; bump both together (ADR-0013). Needs jsonschema (see dev.json setup)."""
import json
import sys

import jsonschema

template = open('build/mcp-registry/server.json.tmpl', encoding='utf-8').read()
doc = json.loads(template.replace('__VERSION__', '9.9.9-rc.1'))
schema = json.load(open('build/mcp-registry/server.schema-2025-12-11.json', encoding='utf-8'))
errors = [
'/'.join(map(str, e.path)) + ' - ' + e.message
for e in jsonschema.Draft202012Validator(schema).iter_errors(doc)
]
# Server-side rules the registry enforces beyond its schema, learned from live rejections
# (v0.5.0-rc.1): an OCI package carries its version in the identifier tag and must not have a
# version member.
for i, package in enumerate(doc.get('packages', [])):
if package.get('registryType') == 'oci':
if 'version' in package:
errors.append(f'packages/{i} - OCI packages must not have a version member')
if ':' not in package.get('identifier', ''):
errors.append(f'packages/{i} - OCI identifier must carry the version as its tag')
for e in errors:
print('INVALID:', e, file=sys.stderr)
if not errors:
print('registry template: valid')
sys.exit(1 if errors else 0)
51 changes: 51 additions & 0 deletions scripts/dev/check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# scripts/dev/check.sh [--setup] [--only a,b] [--skip a,b] [--all] [--manifest FILE]
# Runs the checks of dev.json in order and prints one summary block; exit status = failures.
# Stops at the first failure unless --all. --setup runs the manifest's setup steps instead.
# A step whose requires_env variable is empty is skipped, and said to be; when CI is set that is a
# failure unless the step is marked optional: true. Standard library python3 only. jpdev sets JPDEV_WRAP to add its caps around each step.
root=${JPDEV_ROOT:-$(cd "$(dirname "$0")" && git rev-parse --show-toplevel)} || exit 2
exec python3 -I - "$root" "$@" <<'PY'
import json, os, re, signal, subprocess, sys, time
root, args = sys.argv[1], sys.argv[2:]
opt = {'only': set(), 'skip': set(), 'all': False, 'setup': False, 'manifest': root + '/dev.json'}
while args:
a = args.pop(0)
if a in ('--only', '--skip'): opt[a[2:]] = set(args.pop(0).split(','))
elif a == '--manifest': opt['manifest'] = args.pop(0)
elif a in ('--all', '--setup'): opt[a[2:]] = True
else: sys.exit('check.sh: unknown argument ' + a)
m = json.load(open(opt['manifest'], encoding='utf-8'))
steps = m.get('setup', []) if opt['setup'] else m['checks'] + ([dict(name='parity', **m['parity'])] if 'parity' in m else [])
unknown = (opt['only'] | opt['skip']) - set(s['name'] for s in steps)
if unknown: print('check.sh: no step named ' + ', '.join(sorted(unknown)), file=sys.stderr); sys.exit(2)
wrap, logdir, rows, failed = os.environ.get('JPDEV_WRAP'), os.environ.get('JPDEV_LOGDIR'), [], 0
for c in steps:
n = c['name']
if (opt['only'] and n not in opt['only']) or n in opt['skip']: continue
if failed and not opt['all']: rows.append((n, 'NOT RUN', '', [])); continue
if c.get('requires_env') and not os.environ.get(c['requires_env']):
# A skip is a failure under CI (CI is set) unless the step says optional: true.
if os.environ.get('CI') and not c.get('optional'): failed += 1; rows.append((n, 'FAIL', 'needs %s, which CI must set (or mark the step optional)' % c['requires_env'], []))
else: rows.append((n, 'SKIP', 'needs ' + c['requires_env'], []))
continue
cmd = ['bash', '-c', c['run']]
if wrap: cmd = [wrap, c.get('toolchain', 'shell'), '--'] + cmd
tmo = c.get('timeout', 1200)
t0 = time.time(); print('check: running ' + n, file=sys.stderr, flush=True)
p = subprocess.Popen(cmd, cwd=os.path.join(root, c.get('cwd', '.')), stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, errors='replace', start_new_session=True, env=dict(os.environ, JPDEV_STEP_TIMEOUT=str(tmo)))
try: out, _ = p.communicate(timeout=None if wrap else tmo)
except subprocess.TimeoutExpired: os.killpg(p.pid, signal.SIGKILL); out = p.communicate()[0] + '\ncheck: stopped for time\n'
lines, took = out.splitlines(), '%ds' % (time.time() - t0)
if logdir: os.makedirs(logdir, exist_ok=True); open('%s/%s.log' % (logdir, n), 'w').write(out)
hit = [l.strip() for l in lines if c.get('summary') and re.search(c['summary'], l)]
note = [' ' + l for l in (hit if len(hit) <= 3 else ['%d lines matched; last: %s' % (len(hit), hit[-1])])]
if p.returncode == 0: rows.append((n, 'PASS', took, note)); continue
failed += 1
rows.append((n, 'FAIL', '%s, exit %d' % (took, p.returncode), note + [' --'] + [' ' + l for l in lines[:40]] + ([' ... %d more lines' % (len(lines) - 40)] if len(lines) > 40 else [])))
print('== %s summary (%s) ==' % ('setup' if opt['setup'] else 'check', os.path.basename(opt['manifest'])))
for n, s, head, extra in rows: print('\n'.join(['%-8s %-24s %s' % (s, n, head)] + extra))
print('== %d passed, %d failed, %d skipped, %d not run ==' % tuple(sum(1 for r in rows if r[1] == k) for k in ('PASS', 'FAIL', 'SKIP', 'NOT RUN')))
sys.exit(min(failed, 255))
PY
23 changes: 23 additions & 0 deletions scripts/sync-agent-docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# sync-agent-docs.sh [--check] [DIR]
# CLAUDE.md is generated from AGENTS.md (the source) in DIR (default templates/ of the kit;
# a repository that vendors the pair passes its root). --check writes nothing and fails when
# CLAUDE.md is not exactly what the generator would write.
set -eu
check=0
if [ "${1:-}" = --check ]; then check=1; shift; fi
dir=${1:-$(cd "$(dirname "$0")/.." && pwd)/templates}
src=$dir/AGENTS.md
dst=$dir/CLAUDE.md
[ -f "$src" ] || { echo "sync-agent-docs: $src is missing" >&2; exit 2; }
if [ "$check" -eq 1 ]; then
if [ -f "$dst" ] && cmp -s "$src" "$dst"; then
echo "sync-agent-docs: CLAUDE.md matches AGENTS.md"
else
echo "sync-agent-docs: CLAUDE.md differs from AGENTS.md; run scripts/sync-agent-docs.sh" >&2
exit 1
fi
else
cp "$src" "$dst"
echo "sync-agent-docs: wrote $dst"
fi
Loading