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: 57 additions & 0 deletions .claude/agents/cold-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: cold-read
description: Fresh-context reviewer for a finished PR in this repo — reads only the diff, the repo's docs, and the stated acceptance criteria, and reports gaps that affect correctness or the stated requirements. Fixes nothing.
effort: high
---

You are a cold reader. Your value is that you did not watch the work happen.

**What you read:** the PR diff, the repo's own documentation (`CLAUDE.md`, `README.md`,
`CHANGELOG.md`, `tests/fixtures/README.md`), and the acceptance criteria you were given.
That is the whole inventory.

**What you must not read:** the implementation chat, the ORCH transcript, the phase
contract's reasoning, or any account of how the change came to be. If someone offers
you that context, decline it. A verdict coloured by the author's assumptions is the
one thing a cold read cannot produce.

**What you may run:** this repository's checks, through `uv` only (the system
`python3` may be older than the package's floor): `uv sync --locked`, then
`uv run pytest` (with `uv run --python 3.11|3.12|3.14` for the matrix's Pythons, and
Node 24 or 22 first on `PATH`); `uv run ruff check .`; `uv run ruff format --check .`;
and the wheel job — `uv build` from a clean checkout, the wheel installed into a fresh
environment, `scripts/smoke_wheel.py` run there with a throwaway seed from
`openssl rand -base64 32`.

**What you report:** gaps that affect **correctness** or **the stated requirements**.
Specifically:

- a stated acceptance criterion the diff does not actually meet;
- a defect in the changed code — wrong behaviour, an unhandled case, a broken
invariant;
- a breach of the rule that the wrapper holds no key, reads no seed and computes
none of the format's hashes, or a guard test that can no longer fail;
- a claim in the diff (a comment, a doc line, a commit message, a PR-body assertion)
that is false against the code at this revision;
- a check the criteria required that the evidence does not show being run;
- a fixture whose stated provenance does not match what the fixture contains, or a
byte-equal assertion that is not actually byte-equal.

**What you leave alone:** style, naming, structure you would have done differently,
refactors the criteria did not ask for, and anything outside the diff. Preference is
not a finding.

**You fix nothing.** No edits, no commits, no pushes, no suggested patches applied.
Your output is a report.

Your report:

1. **What I ran** — the exact commands and their results, or an explicit statement
that you ran nothing and reviewed by reading only.
2. **Findings** — most severe first. Each one: file and line, what is wrong, and the
concrete scenario in which it is wrong. If a finding is a suspicion rather than a
confirmation, label it as such.
3. **Criteria** — each stated acceptance criterion, marked met / not met / cannot tell
from the diff, with one line of reasoning.
4. **Nothing found** is a complete and useful report. Say it plainly; do not
manufacture findings to justify the pass.
55 changes: 55 additions & 0 deletions .claude/agents/impl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
name: impl
description: IMPL agent for one gated-sprint phase in this repo — implements the phase on its own branch and reports evidence per CLAUDE.md. Spawned by an ORCH session with a phase contract from a sprint anchor issue.
effort: high
---

You are the IMPL agent for exactly one phase of a gated sprint in `typedstandards-python`.

Your phase contract arrives from the ORCH session: task, context, non-goals, binary
acceptance criteria with runnable checks, blast zone, riders. This file is the
standing part — what is true of every phase here regardless of what the contract says.

Ground rules:

- **Read before porting — verify, don't trust.** Read the sprint contract (anchor
issue) and your phase definition, then the referenced source material itself. A
premise in the contract that does not match the repo at HEAD gets flagged, not
silently resolved. Paths, commands, and line references in a contract are claims to
check, not facts to act on.
- **One branch per phase**, named as the phase plan specifies; PR to `main`. You do
not merge, do not push rollback tags, and never publish to PyPI — ORCH handles merge
and tags on evidence-pass. Never push to `main`.
- **Stay inside the declared blast zone.** Keep the diff confined to the paths the
phase names; repos and paths the contract marks read-only stay untouched (the CLI's
repository and the host template are read-only from here). Out-of-scope findings go
in the phase report as flags for later phases — do not fix them.
- **Follow CLAUDE.md**: the rule that the wrapper holds no key, reads no seed and
computes none of the format's hashes; the stakeholder boundary (neutral phrasing in
every artifact that lands in this public repo); and the push guard. `git commit -s`
on every commit — the `Signed-off-by:` email must match the commit author email
exactly.
- **Never bypass a guard.** If a hook or the pre-push guard blocks, resolve the cause
and rebuild the branch history so the flagged bytes never land in outgoing commits.
Surface the block in your report; escalate to the owner rather than working around it.

Phase report (your final message, mirrored into the PR body) — the evidence protocol
in CLAUDE.md, concretely:

- branch, head SHA and `git diff --numstat main...HEAD`, with an explicit blast-zone
statement;
- full output of every check CI gates on, pasted rather than summarized: `uv sync --locked`
then `uv run pytest` on Python 3.11, 3.12 and 3.14 with Node 24, and on 3.12 with
Node 22; `uv run ruff check .`; `uv run ruff format --check .`; and the wheel job
(`uv build` from a clean checkout, the wheel installed into a fresh environment,
`scripts/smoke_wheel.py` run there). Use `uv` for every Python run;
- each acceptance criterion's red, then its green;
- gitleaks over the outgoing range: `gitleaks git --log-opts="main..HEAD" --no-banner`;
- fixture provenance — which source each fixture derives from, at which commit, with
its SHA-256, and the byte-equal assertions called out explicitly;
- the model you ran on;
- everything flagged-not-fixed, and every contract premise that did not survive the
check.

Report outcomes faithfully — a red test, a skipped step, or a partial phase is
reported as such, never smoothed over.
96 changes: 96 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# On every push and pull request:
# - test: the suite on Python 3.11, 3.12 and 3.14, on Ubuntu 24.04 and macOS 15, on Node 24,
# plus one cell on Ubuntu 24.04, Python 3.12, Node 22 (G0 D2 = A). `uv sync --locked` builds
# the editable install, whose hatchling hook vendors @typedstandards/cli from package-lock.json,
# so the tests drive the tree a wheel ships.
# - lint: ruff check and ruff format --check.
# - wheel: builds the sdist and, from it, the wheel, installs the wheel into a fresh environment,
# and runs scripts/smoke_wheel.py there with a throwaway seed.
# Each job name is stable: the ruleset on main requires these checks by name.
# The workflow holds no key and reads no repository secret.
# Each action is pinned to a full commit SHA, measured with git ls-remote, with its tag in a comment.
name: ci

on:
push:
pull_request:

permissions:
contents: read

jobs:
test:
name: test (py${{ matrix.python }}, ${{ matrix.os }}, node ${{ matrix.node }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15]
python: ["3.11", "3.12", "3.14"]
node: ["24"]
include:
- os: ubuntu-24.04
python: "3.12"
node: "22"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node }}
package-manager-cache: false
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "0.11.25"
python-version: ${{ matrix.python }}
enable-cache: false
- run: node --version && npm --version && uv --version
- run: uv sync --locked
- run: uv run python --version
- run: uv run pytest

lint:
name: lint
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
package-manager-cache: false
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "0.11.25"
python-version: "3.12"
enable-cache: false
- run: uv sync --locked
- run: uv run ruff check .
- run: uv run ruff format --check .

wheel:
name: wheel (build, install, smoke)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
package-manager-cache: false
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "0.11.25"
python-version: "3.12"
enable-cache: false
# uv build makes the sdist, then the wheel from the unpacked sdist.
- run: uv build
- run: ls -l dist && unzip -l dist/*.whl | grep -c '_vendor/node_modules/'
- run: uv venv "$RUNNER_TEMP/smoke"
- run: uv pip install --python "$RUNNER_TEMP/smoke" dist/*.whl
- name: smoke check in the fresh environment, with a throwaway seed
run: |
TYPEDSTANDARDS_SIGNING_SEED_B64="$(openssl rand -base64 32)" "$RUNNER_TEMP/smoke/bin/python" scripts/smoke_wheel.py
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# The vendored CLI, written by hatch_build.py at every wheel or editable build.
/src/typedstandards/_vendor/
/node_modules/
/dist/
/build/
.venv/
__pycache__/
*.py[cod]
.pytest_cache/
.ruff_cache/
7 changes: 7 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[extend]
useDefault = true

[[allowlists]]
description = "Ed25519 did:key identifiers (base58btc, multicodec 0xed01) encode a public key; they are not secrets (hub ADR-0030)"
regexTarget = "match"
regexes = ['''key:z6Mk[1-9A-HJ-NP-Za-km-z]{44}''']
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changelog

## Unreleased

- `sign`, `withdraw`, `attest`, `view` and `verify`: pass-throughs to `@typedstandards/cli` 0.2.0,
vendored into the wheel at build time and run as a child process with the inherited environment.
Each returns the CLI's stdout parsed as JSON.
- A Node locator: `TYPEDSTANDARDS_NODE`, then `node` on `PATH`; floor 20.19.0.
- Exit codes 1 to 4 raise `VerificationError`, `UsageError`, `SeedError` and `InternalError`, under
`CliError`; a missing or old Node raises `NodeLocatorError`.
- `verify` drops a bundle's top-level `trustRegistry` before the CLI sees it (typedstandards#136).
- `CLI_VERSION = "0.2.0"` and `cli_version()`.
97 changes: 97 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# CLAUDE.md

`typedstandards` on PyPI: a thin Python wrapper that runs `@typedstandards/cli` (vendored, pinned
exactly in `package.json` and `package-lock.json`) as a child process. Python `>=3.11`; hatchling
with a build hook (`hatch_build.py`) that vendors the CLI; `uv` for everything.

## Development loop

The system `python3` may be older than 3.11: run Python only through `uv`. Under a Node version
manager a non-interactive shell may have no `node` on `PATH`; load it first
(`eval "$(fnm env)" && fnm use 24`, or your equivalent).

- `uv sync` — creates `.venv` and installs the package editable. The build hook runs
`npm ci --omit=dev --ignore-scripts` into `src/typedstandards/_vendor/` (git-ignored), so tests
drive the same tree a wheel ships, never a global CLI or `npx`. It re-runs when `pyproject.toml`,
`package.json`, `package-lock.json` or `hatch_build.py` change; force it with
`uv sync --reinstall-package typedstandards`.
- Another Python: `uv run --python 3.11 pytest`. Another Node: put it first on `PATH`, or set
`TYPEDSTANDARDS_NODE`.

The checks CI runs (`.github/workflows/ci.yml`):

- `test (py<3.11|3.12|3.14>, <ubuntu-24.04|macos-15>, node 24)` and
`test (py3.12, ubuntu-24.04, node 22)` — `uv sync --locked`, then `uv run pytest`; the gate is
`0 failed`.
- `lint` — `uv run ruff check .` and `uv run ruff format --check .`.
- `wheel (build, install, smoke)` — `uv build` (the sdist, then the wheel from it), the wheel
installed into a fresh environment, and `scripts/smoke_wheel.py` run there with a throwaway seed.

## Node floors

The wrapper and the CLI need Node 20.19 or later (the CLI's `engines.node`). A
`@typedstandards/host-core` site build needs Node 22 or later. The README states both.

## The wrapper holds no key

It holds no key, reads no signing seed, and computes none of the format's hashes (content hash,
envelope hash, node id): the CLI reads `TYPEDSTANDARDS_SIGNING_SEED_B64` from the environment it
inherits and does all of the format's work. Guard tests, which must keep failing on an offender:

- `tests/test_guards.py` with `tests/guards.py`: no module under `src/typedstandards` names the
seed variable; no call passes `env=` or changes the process environment; no module imports
`hashlib` (or `hmac`, or hashlib's underscore modules) except P2's `pin.py`, whose digest is a
signed assertion; at run time, every child inherits the environment unchanged and the wrapper
never reads the seed variable. Each scanner is also driven over a tree of offenders.

Test code may generate a random seed and set it with `monkeypatch.setenv`; package code never
touches one. A failing test never prints environment values.

## Secret hygiene

Never `cat`/`head`/`tail`/dump `.env*`, `auth.json`, `credentials*`, `*.pem`, `*.key`, `~/.ssh`,
`~/.aws`. Read only by key **name** (`grep`/`jq` a field, never a value) or a command the tool
exposes; never load-and-print a credentials file, even redacted.

## Evidence protocol (gated sprint phases)

Every phase report (PR body and anchor-issue comment) carries:

- the phase **branch**, head SHA and `git diff --numstat main...HEAD`, with the blast zone stated;
- each acceptance criterion's **red** (the failing assertion's output) and its **green**, pasted;
- the **full suite output** on every Python and Node the matrix names, the clean-checkout wheel
build and its smoke check, and gitleaks over the outgoing range;
- **fixture provenance**: each fixture's source, commit and SHA-256 (`tests/fixtures/README.md`),
with the byte-equal assertions called out;
- the **model** the phase ran on; everything flagged and not fixed.

The orchestrator re-verifies evidence before merging; numbers an implementer reports do not pass a
gate on their own.

## Rollback tags

Bracket every phase merge: `rollback/pre-produce-py-p<n>` at the pre-merge anchor and
`rollback/produce-py-p<n>-merged` at the merge commit. The orchestrator pushes them, not
implementation sessions.

## Push guard

A global pre-push guard (gitleaks plus a keyword list) scans the added lines of every outgoing
commit, so a fix on top does not clear an earlier commit: the flagged bytes must be absent from all
pushed history. Pushes go to the owner as one command, after `gitleaks git --log-opts="main..HEAD"`
over the outgoing range is clean. Never bypass the guard and never tune its patterns on your own
initiative. `.gitleaks.toml` allows only Ed25519 `did:key` identifiers, which are public keys.

## Phrasing, commits, merges, releases

- Neutral phrasing everywhere: no stakeholder, organisation or person is named. This repository is
public and its history is permanent.
- `git commit -s` on every commit; the `Signed-off-by:` email must equal the author email exactly.
Commits are signed (SSH).
- Work lands by PR to `main` as merge commits; never push to `main`. Merging is the orchestrator's
call on evidence in a gated sprint, the owner's otherwise.
- Publishing to PyPI is the owner's act, from a tested script with a `DRY_RUN` mode.
- A CLI upgrade reaches users as a wrapper release that moves the pin: `package.json`,
`package-lock.json` (`npm install --package-lock-only --ignore-scripts`) and `CLI_VERSION`
together; `tests/test_version.py` fails on any one left behind.
- `CHANGELOG.md` is a factual per-version record; changes collect under `## Unreleased`.
Loading
Loading