From f816d952d889d2c04cbc860e134cf9e59f2a2a8e Mon Sep 17 00:00:00 2001 From: kikashy <35789537+kikashy@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:46:27 -0400 Subject: [PATCH] Adopt the development kit: dev.json, scripts/dev/check.sh, AGENTS.md and CLAUDE.md, CI dev.json lists the checks in order. Three are new: the machine-local-path grep, the agent-docs sync check and git diff --check. The rest are what CI ran: gofmt, go vet, go test, the bundled conformance suite, the build and the MCP registry template validation (moved to scripts/check-registry-template.py, which renders the template in memory instead of through a temporary file). scripts/dev/check.sh and scripts/sync-agent-docs.sh are vendored from the development kit v0.1.0. AGENTS.md and CLAUDE.md carry the shared rules and a section for this repository. CI gains a `check` job that runs the manifest; the `test` matrix keeps its job names, which branch protection requires. The local-path grep skips one test vector that holds a file URI, and the trailing space in the ADR template is removed so git diff --check passes. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> --- .github/workflows/ci.yml | 57 ++++++++++++++---------------- .gitignore | 3 ++ AGENTS.md | 48 +++++++++++++++++++++++++ CLAUDE.md | 48 +++++++++++++++++++++++++ CONTRIBUTING.md | 4 +++ README.md | 7 ++++ dev.json | 23 ++++++++++++ docs/adr/template.md | 2 +- scripts/check-registry-template.py | 30 ++++++++++++++++ scripts/dev/check.sh | 51 ++++++++++++++++++++++++++ scripts/sync-agent-docs.sh | 23 ++++++++++++ 11 files changed, 265 insertions(+), 31 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 dev.json create mode 100755 scripts/check-registry-template.py create mode 100755 scripts/dev/check.sh create mode 100755 scripts/sync-agent-docs.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52bfeb87..72634108 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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 diff --git a/.gitignore b/.gitignore index a85cbf56..b65883a1 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ .vscode/* !.vscode/tasks.json + +# Python virtual environment of the development kit (dev.json). +/.venv/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..78efd13a --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..78efd13a --- /dev/null +++ b/CLAUDE.md @@ -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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65f5d03b..398bb58d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 ./... diff --git a/README.md b/README.md index f2efacfa..abee21f8 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/dev.json b/dev.json new file mode 100644 index 00000000..766e074e --- /dev/null +++ b/dev.json @@ -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} + ] +} diff --git a/docs/adr/template.md b/docs/adr/template.md index 678a88f4..86faa94d 100644 --- a/docs/adr/template.md +++ b/docs/adr/template.md @@ -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} diff --git a/scripts/check-registry-template.py b/scripts/check-registry-template.py new file mode 100755 index 00000000..e1802132 --- /dev/null +++ b/scripts/check-registry-template.py @@ -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) diff --git a/scripts/dev/check.sh b/scripts/dev/check.sh new file mode 100755 index 00000000..762a3dc4 --- /dev/null +++ b/scripts/dev/check.sh @@ -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 diff --git a/scripts/sync-agent-docs.sh b/scripts/sync-agent-docs.sh new file mode 100755 index 00000000..3fdf82d1 --- /dev/null +++ b/scripts/sync-agent-docs.sh @@ -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