Skip to content

AtipSpec design

AtipSpec separates planning assistance, local technical checks, and trusted acceptance. Each has a different authority and a different failure mode.

Three lifetimes

The architecture contract defines local structural rules. Living specs preserve capability-scoped requirement and criterion IDs across changes. Deliveries hold the proposed specification, tasks, test mappings, evidence, technical review, risk exceptions and approvals. Archival preserves an acceptance receipt.

The CLI computes state from these artifacts and Git. A task-done record is the assistant's declaration of progress, not semantic correctness. A command exit code records an execution result, not complete test coverage. A review proof pointer is a claim that the technical reviewer and human QA must assess.

Local acceptance

The human decides in the conversation; the assistant records the decision with atipspec accept. There are three decisions: the proposal (scenarios, approach, tests and risk; the spec, for legacy deliveries), the reviewed result, and the contract with the decisions that affect it, once per project. None is added by the mechanism below; the client's permission prompt confirms the same decision.

Each decision is presented by a view that prints an approval code: proposal <slug>, report <slug> or proposal contract. The code is the first 40 bits of the content's sha256 in Crockford base32 (XXXX-XXXX), easy to read and dictate; typing ignores case and the hyphen. It is not a secret. It binds the record to the version presented: if the content changed in between, accept --code refuses without revealing the current code. It makes the client's permission prompt show what is being approved, and it makes the assistant go through the presentation view.

The record stores the hash of the accepted content, per-file digests, the code, who (--by, a declared label that defaults to git's identity), when, and the channel: terminal when someone typed the code at the CLI prompt in an interactive terminal with no agent marker; client-permission when Claude Code is the only agent marker, it does not run through the Agent SDK or claude -p (which may not load the project's settings, and whose prompts go to a program), the interpreter received the command line its ask rule matches (atipspec accept or python -m atipspec accept) and the CLI found its AtipSpec permission rules intact in .claude/settings.json; assistant otherwise. Claude Code passes its marker to every process it starts, so another agent's marker next to it means that agent ran the command, outside Claude Code's rules. Agent markers in the environment (CLAUDECODE, CODEX_SANDBOX, CODEX_THREAD_ID, COPILOT_CLI, CURSOR_AGENT, GEMINI_CLI, AI_AGENT) can only lower the channel, never raise it. The gate compares the hash with the files on every run, so a change made after the acceptance, by a model or by anyone, is visible, names the file, and sends the delivery back to the person. Guided deliveries need the proposal record; a spec and plan pair does not replace it. The contract's record, .atipspec/approvals/contract.json, hashes the contract and each decision that affects it without their status lines: a decision added, edited or marked accepted by hand afterwards leaves the acceptance outdated. Delivery approvals hash contract.md whole, as signed approvals always have, so the status lines accept contract writes leave them outdated; proposal contract names those approvals before the human decides. Records live outside the tree fingerprint and carry no signature: they bind content and state the channel, not identity.

What this covers: a cooperative but hasty assistant that would record an approval on its own through the normal path. It cannot do so without the code, the record says how it was made, and with the Claude Code rules the client asks the person first and denies its edit tools the approval and evidence records. What it does not cover: an agent that sets out to bypass the protections, for example by writing files from a script or deleting review-rounds.json with a shell command, which restarts the round count from the review.md in the folder, by running atipspec accept inside a script or program Claude Code starts, such as an agent that sets none of the markers (the CLI sees only its own process, so the record still says client-permission), by running the command in a terminal without an agent marker and typing the code itself, by starting the session outside the project root so the settings do not load, or through managed settings or hooks that answer permission prompts. approval-subject stays, because provider mode needs it; computing a hash was never the barrier. The authenticated path is an external trust policy.

Two gates

Without external trust, check validates grammar, coverage, the person's acceptance records, the declared scope, the form of the criteria, exact evidence commands/results, the tests named per criterion, freshness and local contract rules. Success is checked. Because candidate files are author-editable, it does not claim authenticated execution or human acceptance. Without a policy, deliver also requires the recorded acceptance of the current result.

Both gates fail closed. The base commit, contract rules and freshness come from git, so without git check is an error rather than a warning. A plan must declare a scope narrower than the repository (strict_scope), and requirements and criteria must pass the form, vague-term and fit-criterion checks (strict_criteria); both are on for new projects. trust: required in config.yaml turns a missing policy into an error and disables local acceptance, and a branch cannot drop it for its own merge. Invalid policy values stop every command before it runs.

With an external versioned policy, success is verified. The gate additionally requires signed CI evidence, product and engineering approvals preceding verification, QA acceptance, criterion-to-task mappings, committed candidate content and corporate rules. Blocker/major deferrals require signed risk approval with expiry. Contract changes require new accepted related decisions, with trusted engineering approval. Deleted contracts and missing history fail.

Trust modes

Two modes carry the same gate. In ssh mode, authority is an enrolled key: humans sign approvals, a collector signs CI evidence, and a compromised key is revoked by rolling the policy. In provider mode, authority is the provider's account and the CI platform's identity: the protected worker reads the PR approvals live with a read-only token, and the evidence carries an artifact attestation that binds its digest to the candidate repository, the pull request, and a trusted workflow outside the candidate repository pinned to one commit, verified with the platform's own tool. The workflow lives outside because in a pull request the platform runs the workflows the pull request carries: a workflow of the candidate repository is part of the change it would attest. Provider mode was chosen as the default recommendation because it removes key custody, the cost that kept most organizations from running the trusted gate at all, while keeping the properties that matter: content-bound approvals, revocation detected on every run, evidence that cannot be replaced after the fact. What it cannot carry is an expiry, so risk exceptions stay in ssh mode.

Authority and cryptography

Detached SSH signatures bind exact artifact bytes to an identity enrolled in an organization-controlled policy outside the candidate repository. Each artifact also binds repository and policy digest. The policy's digest is pinned in protected CI. A candidate-local allowed-signers file would not be a trust anchor, so the gate refuses candidate-local policies.

A human key is only as independent as its custody. An agent with that key can sign; no CLI can infer human presence from a signature. Human identity enrollment, key isolation, installed verifier integrity and protected branch configuration belong to the organization's trust boundary.

For GitHub/GitLab, a collector reads existing human approvals, checks role, author separation and head/content marker, then signs the normalized record. The gate re-fetches provider state on every check so a cached signed response cannot override a revocation. Provider failures fail closed.

CI collection has a separate boundary: the orchestrator authenticates the producer workflow, source SHA, run result and artifact provenance before the collector uses attest. That command does not authenticate an arbitrary CI URL. Candidate code must never execute with signing keys. Use ephemeral execution and a separate protected collector; a second step on a compromised worker is insufficient isolation.

Freshness without circular hashes

The source/specification fingerprint excludes delivery evidence, review, deferral, approval and report files. Those files describe the candidate and must not recursively invalidate themselves. Approval subjects solve the remaining dependency problem: risk approval binds the review and deferral terms; QA acceptance binds those plus the signed evidence set. Changing excluded files therefore invalidates the relevant acceptance even when the source hash stays constant.

deliver changes the delivery's own fingerprint (the folder moves and the living spec absorbs it), so the receipt also stores code_tree: the hash of everything outside .atipspec/. Nothing AtipSpec writes afterwards changes it, and any code change does. merge-check compares it with the branch head before the merge, and checks that every open delivery the branch touches is accepted: the gap between the acceptance and the merge is closed by the same hash, not by trust in the branch.

Durable identities

Living specs merge by requirement ID, keeping criterion IDs. Titles can change without changing identity. atipspec ids gives each delivery a short random prefix no ID visible in the checkout or the team's refs uses, so parallel branches do not collide; the numbered IDs of legacy deliveries, allocated above the maximum found in living, active and archived artifacts, stay valid. Three-way comparison detects concurrent edits to the same requirement or criterion ownership collisions, and check reports duplicate IDs. Every requirement and acceptance criterion receives a stable ID from the start.

Operational limits

Contract rules check paths, patterns, dependencies and required commands; they do not prove architectural correctness. Test mappings and proof pointers need semantic review. Git excludes ignored files from source fingerprints, so keep build inputs and lockfiles tracked and reproduce the execution environment. Reports are indexes and summaries of signed evidence, not certifications. Archived reports describe historical acceptance. merge-check compares the whole code tree, so bringing the target branch into a delivered branch, or any later fix, reopens the delivery: revert the archive commit, then verify, review, accept and deliver again. Bring in the target branch with a merge before the final verification; one delivery per branch keeps that cheap. In local mode the receipt is a declaration in the branch: an edit to its code_tree shows in the pull request's diff. With a trust policy, merge-check does not take the receipt's word: it checks the delivery again at the commit before the archive commit, with the files archived with it, as deliver did (in provider mode with the pull request's reviews of that commit), requires that commit to hold the code at the head, and compares the living spec, each requirement the delivery lists included, with what deliver writes from it. Pilots report actual sourced observations with sample sizes and limitations; they do not invent ROI.