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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Changelog

## v1.3.0 - 2026-10-14

### Added

- Boundary register (`boundary_register.yaml`, format `boundary-register/1`,
shared with the factory and Constrain) plus `ComponentContract.boundary_identifiers`.
Every cross-boundary identifier (service name, audience, credential, principal,
grant/role, table/column, metric type, log field, env/config key, header,
claim) must cite file or observation records bound by SHA-256. Verification
is mechanical: exact `equal` values with near-miss hints, `absent` isolation
proofs, token matching, stale-digest detection, placeholder refusal, and
project-root path confinement.
- `pact boundary verify [project]` verifies the register and exits non-zero on
any error.
- `pact boundary cite --kind file|observation --path <rel> --value <literal>`
produces a verified record (sha256, line or observed_at) as JSON and refuses
when the literal is not where the record would claim.
- Interview, shaper, and contract-author prompts require citing a record or
raising an open question; implementer handoffs render registered literals.

### Changed

- `validate_all_contracts` now fails when a contract uses a cross-boundary
dependency (`external:` names, `network_call`, `database_*`/`http_*`/
`external_*` data-access side effects) that the register does not cover, or
when any register entry fails verification. Projects with no cross-boundary
usage are unaffected.
- `pact validate` now exits 1 when the contract gate fails.

## v1.2.0 - 2026-07-17

### Added
Expand Down
27 changes: 27 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pact checklist <project-dir> # Requirements quality checklist
pact production init <project-dir> # Scaffold optional production-readiness pack
pact production fingerprint <project-dir> # Print the source fingerprint for evidence
pact production validate <project-dir> # Validate production-readiness gate for a Pact-managed project
pact boundary verify [project-dir] # Verify boundary register records (exit 1 on any error)
pact boundary cite --kind file --path <rel> --value <literal> [--line N] [project-dir] # Produce a record (JSON)
pact assess <directory> # Architectural assessment (any codebase)
pact export-tasks <project-dir> # Export TASKS.md
pact handoff <project-dir> <id> # Render/validate handoff brief
Expand Down Expand Up @@ -87,6 +89,30 @@ passing after source or Pact artifact changes, or after the evidence ages out.
This is a deployment gate, not a runtime substitute; `live_validation.yaml`
must still prove the running environment matches the evidence being claimed.

### Boundary Register (never assume an identifier; cite the record)

Every identifier two parties must agree on (service name, audience, credential,
principal, grant/role, table/column, metric type, log field, env/config key,
header, claim) lives in `boundary_register.yaml` (format `boundary-register/1`,
shared with the factory and Constrain) or a contract's `boundary_identifiers`.
Each side (`party`: component id or `external:<system>`; `role`:
defines|grants|consumes|presents|denies; exact `value`) cites >=1 record: a
`file` record (path, line, sha256 of the whole file) or an `observation` record
(path, sha256, command, observed_at). `src/pact/boundary_register.py` verifies
them mechanically (token match, digest, placeholders, path containment,
`equal` vs `absent` relations); `validate_boundary_register()` in
`contracts.py` runs inside `validate_all_contracts()` as hard errors and also
fails any contract that uses an unregistered boundary (`external:` names,
`network_call`, `database_*`/`http_*`/`external_*` data-access side effects).
A stale digest means refresh the register from the source, never hand-edit it.
Prompts (interview, shaper, contract author) share `BOUNDARY_RULE_PROMPT`:
cite the record or raise an open question. Implementer handoffs render the
registered literals in tier 1. `pact boundary verify [project]` exits non-zero
on any error; `pact validate` now exits 1 on a failed gate. Records come from
`pact boundary cite --kind file|observation --path <rel> --value <literal>
[--line N] [--command "<cmd>"] [project]` (JSON out; exit 1 if the literal is
not where the record would claim), never typed by hand.

### Production-Ready Minimalism

Pact supports a lean/simplification review lens, but it is not a readiness
Expand Down Expand Up @@ -271,6 +297,7 @@ src/pact/
mcp_server.py # MCP server (FastMCP transport + PactMCPServer handlers)
tool_index.py # External tool enrichment (ctags, cscope, tree-sitter, kindex)
production.py # Optional production-readiness pack scaffold + validation
boundary_register.py # boundary-register/1: cross-boundary identifiers verified against records
schemas_production.py # Production-readiness artifact schemas

# Spec-kit capabilities (task list, analysis, checklist)
Expand Down
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,60 @@ types:

Tests automatically verify both acceptance and rejection: valid instances pass, invalid inputs fail with appropriate errors, and boundary values behave correctly. Implementations render these as Pydantic models (Python), Zod schemas (TypeScript), or class constructors with validation (JavaScript).

### Boundary Register: Never Assume an Identifier

Names, tokens, roles, grants, metric types, log fields, headers and service
names that two parties must agree on are boundary identifiers. Pact never
assumes one. Each is entered in `boundary_register.yaml` at the project root
(format `boundary-register/1`, shared with the factory and Constrain), or in a
contract's `boundary_identifiers`, and every side cites a record that the gate
re-verifies mechanically:

```yaml
schema: "boundary-register/1"
entries:
- id: log-level-field
kind: field # name|endpoint|credential|audience|principal|grant|role|table|column|field|metric|label|env|config-key|topic|queue|bucket|secret|service|header|claim|enum|other
relation: equal # equal | absent (isolation: the subject must NOT appear)
description: The log platform only derives severity from this JSON key
sides:
- party: external:cloud-logging # component id or external:<system>
role: defines # defines|grants|consumes|presents|denies
value: severity # exact literal, case-sensitive
records:
- {kind: observation, path: evidence/log_entry.json, sha256: <hex>,
command: "gcloud logging read ... --limit 1 --format json",
observed_at: "2026-10-14T12:00:00Z"}
- party: api
role: consumes
value: severity
records:
- {kind: file, path: src/api/logging.py, line: 12, sha256: <hex>}
```

Every violation is an error, never a warning: mismatched values (with a
"near-miss" note when they differ only by case or separators), a side with no
records (UNVERIFIED), a stale digest (refresh the register from the source;
never hand-edit it to fit), a cited line that does not contain the value as a
token (`mea-auth` does not match `mea-auth-runtime`), a placeholder value, a
path outside the project root, and, for `absent`, a denies record that
contains the subject. A boundary that is used must be registered: contracts
that declare `external:<system>` dependencies/requires/targets, `network_call`
side effects, or `database_*`/`http_*`/`external_*` data-access side effects
fail `pact validate` (and the decomposer gate and MCP `pact_validate`) until a
register entry covers them. Projects with no cross-boundary usage are
unaffected. Contract and interview prompts tell the model to raise an open
question rather than guess; registered literals are rendered into every
implementer handoff. Run `pact boundary verify [project]` to check on its own.

Records are produced, never typed. `pact boundary cite --kind file|observation
--path <rel> --value <literal> [--line N] [--command "<cmd>"] [project]` reads
the file under the project root (same path safety as the verifier), hashes the
whole file, finds the first line containing the value as a token (or confirms
`--line`), and for observations requires `--command` and stamps `observed_at`
in UTC. It prints the record as JSON and exits 1, writing nothing, when the
literal is not where the record would claim.

## Audit Repo Separation

Pact supports a two-repo separation-of-privilege model where the coding agent and auditing agent operate in different repositories:
Expand Down Expand Up @@ -388,6 +442,8 @@ Either, neither, or both. Defaults: both off (sequential, single-attempt).
| `pact production status <project>` | Show production-readiness status without blocking |
| `pact production fingerprint <project>` | Print the current source fingerprint for production evidence |
| `pact production validate <project>` | Validate a Pact-managed production gate and exit non-zero on blockers |
| `pact boundary verify [project]` | Verify the boundary register against its records; exit non-zero on any error |
| `pact boundary cite --kind file\|observation --path <rel> --value <literal> [project]` | Produce a verified record (sha256, line or observed_at) as JSON; exit 1 if the literal is not there |
| `pact spec apply <project> <file>` | Apply an AI-authored JSON/YAML build spec |
| `pact spec show <file>` | Normalize and inspect an AI-authored build spec |
| `pact agent spec-author [--apply]` | Run one constrained spec-author agent |
Expand Down
37 changes: 37 additions & 0 deletions REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# REVIEW.md -- Pact

Guidance for automated review of Pact changes. Architecture and commands: `CLAUDE.md`.

## Always check

- Mechanical gates stay mechanical: no LLM call inside `contracts.py`,
`boundary_register.py`, `production_validation.py`, `analyzer.py`, or `checklist_gen.py`.
- New validation that is meant to block returns errors through `validate_all_contracts`
(or the production gate); it is never downgraded to a logged warning.
- Cross-boundary identifiers (names, tokens, roles, grants, metric/log fields, headers)
are never assumed: they come from `boundary_register.yaml` / `boundary_identifiers`
with records that verify. A register edited to fit a stale digest is a defect.
- Schema changes to `ComponentContract` and other persisted models load existing
`interface.json` files (new fields have defaults; malformed LLM data must not make a
contract unloadable).
- New behavior has pytest coverage under `tests/` that fails without the change; CLI
commands that gate exit non-zero on failure.
- Agent prompts keep the context fence as the first line and stay concise (no rule lists
appended to every prompt without need).
- Record/evidence paths are confined to the project root (no absolute paths, no `..`,
no symlink escape).

## Style

- Several source files use CRLF or mixed line endings; edits must preserve them (no
whole-file EOL churn in diffs).
- Prefer stdlib; runtime dependencies are pydantic, pyyaml, pytest (see `pyproject.toml`).
- Docstrings state the why and the invariant a module upholds.
- Error messages name the artifact, entry, and reason so an operator can act without
reading code.

## Skip

- `dist/`, `.pact/`, `.kin/`, generated `standards.json`, `tasks.json`, `TASKS.md`.
- `tests/smoke/` (generated by `pact adopt`).
- Local `config.yaml` operator tuning.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pact-agents"
version = "1.2.0"
version = "1.3.0"
description = "Contract-first multi-agent software engineering. Contracts before code. Tests as law. Agents that can't cheat."
readme = "README.md"
license = {text = "MIT"}
Expand Down
2 changes: 1 addition & 1 deletion src/pact/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Pact — contract-first multi-agent software engineering."""

__version__ = "1.2.0"
__version__ = "1.3.0"
2 changes: 2 additions & 0 deletions src/pact/agents/code_author.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ async def author_code(
language: str = "python",
strategic_context: str = "",
processing_register: str = "",
boundary_entries: list | None = None,
) -> ImplementationResult:
"""Generate implementation code following the Research-First Protocol.

Expand Down Expand Up @@ -269,6 +270,7 @@ async def author_code(
log_key_preamble=log_preamble,
strategic_context=strategic_context,
processing_register=processing_register,
boundary_entries=boundary_entries,
)

# The handoff brief is the largest cacheable block
Expand Down
21 changes: 19 additions & 2 deletions src/pact/agents/contract_author.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import TYPE_CHECKING

from pact.agents.base import AgentBase
from pact.boundary_register import BOUNDARY_RULE_PROMPT
from pact.readiness import ReadinessProfile
from pact.agents.research import plan_and_evaluate, research_phase
from pact.schemas import (
Expand Down Expand Up @@ -46,7 +47,9 @@
classification and affected fields.
- authority: declare data domain patterns this component owns (empty list if none).
If domains is non-empty, rationale is required explaining why this component is
authoritative for those domains."""
authoritative for those domains.

""" + BOUNDARY_RULE_PROMPT


async def author_contract(
Expand All @@ -62,9 +65,14 @@ async def author_contract(
processing_register: str = "",
type_registry: "TypeRegistry | None" = None,
readiness_profile: ReadinessProfile | None = None,
boundary_register_context: str = "",
) -> tuple[ComponentContract, ResearchReport, PlanEvaluation]:
"""Generate a ComponentContract following the Research-First Protocol.

``boundary_register_context`` is the rendered project boundary register
(see ``pact.boundary_register.render_register_for_prompt``): the only
source of identifier values the author may cite.

Returns:
Tuple of (contract, research_report, plan_evaluation).
"""
Expand Down Expand Up @@ -172,6 +180,8 @@ async def author_contract(
cache_parts.append(dep_stubs)
if decisions_summary:
cache_parts.append(decisions_summary)
if boundary_register_context:
cache_parts.append(boundary_register_context)
cache_prefix = "\n\n".join(cache_parts)

# Dynamic prompt (component-specific)
Expand Down Expand Up @@ -202,7 +212,14 @@ async def author_contract(
(not vague phrases like "handles data" — describe the exact data and purpose),
and list side_effects with type, classification, fields, and rationale
- authority: set domains this component owns (empty list if non-authoritative),
with rationale if domains is non-empty"""
with rationale if domains is non-empty
- boundary_identifiers: for every identifier this component shares with another
party (other components or external:<system>), add a boundary-register/1 entry

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author is told to copy register entries verbatim, but reusing an entry id in boundary_identifiers trips the duplicate-id hard error; instruct 'reference, do not re-add' or tolerate identical copies.

whose sides cite verifiable records. Use only values you can cite from the
context above; never invent one. If you cannot cite it, do not guess: leave
the entry out (the validation gate then refuses the unregistered boundary until
a human supplies the record) and state what is missing in an invariant that
starts with 'OPEN QUESTION:'"""

contract, in_tok, out_tok = await agent.assess_cached(
ComponentContract, prompt, CONTRACT_SYSTEM, cache_prefix=cache_prefix,
Expand Down
8 changes: 7 additions & 1 deletion src/pact/agents/shaper.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import logging

from pact.agents.base import AgentBase
from pact.boundary_register import BOUNDARY_RULE_PROMPT
from pact.schemas_shaping import (
Appetite,
ShapingPitch,
Expand Down Expand Up @@ -294,7 +295,12 @@ async def close(self) -> None:
await self._agent.close()


SYSTEM_PROMPT = "You are a product shaper following Shape Up methodology."
SYSTEM_PROMPT = (
"You are a product shaper following Shape Up methodology.\n\n"
+ BOUNDARY_RULE_PROMPT
+ "\nTreat every uncited boundary identifier as a rabbit hole: name it and "
"the system that owns it, never fill it in."
)

SYSTEM_PROMPT_ENRICHMENT = (
"You are a product shaper performing deep analysis. "
Expand Down
Loading
Loading