Skip to content

Commit 33fc2d5

Browse files
jmcentireclaude
andcommitted
v0.5.0: Goodhart tests — hidden acceptance criteria for contract compliance
Add adversarial hidden tests that implementation agents never see, countering Goodhart's Law (optimizing for visible test inputs rather than true contract compliance). Graduated-disclosure remediation re-implements failing components with behavioral hints that get progressively more specific. New features: - Goodhart test author (single LLM call, adversarial prompt) - Hidden test storage isolated from visible tests - Polish-phase Goodhart evaluation + remediation loop - Graduated hints: Level 1 (behavioral), Level 2 (contract invariant) - Wizard, lifecycle polish phase, integrator improvements - Fix Haiku pricing in budget tests (0.80 -> 1.00) 1573 tests, all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1087285 commit 33fc2d5

23 files changed

Lines changed: 2131 additions & 40 deletions

CLAUDE.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Every agent follows 3 phases: Research -> Plan+Evaluate -> Execute. Research and
3535
2. **Shape** -- (Optional) Produce a Shape Up pitch: appetite, breadboard, rabbit holes, no-gos
3636
3. **Decompose** -- Task -> DecompositionNode tree (2-7 components), guided by shaping context
3737
3. **Contract** -- For each component (leaves first), generate ComponentContract
38-
4. **Test** -- For each contract, generate ContractTestSuite with executable tests
38+
4. **Test** -- For each contract, generate ContractTestSuite with executable tests + hidden Goodhart tests
3939
5. **Validate** -- Mechanical gate: all refs resolve, no cycles, test code parses
4040
6. **Implement** -- Each component independently by code_author agent, verified by contract tests
4141
7. **Integrate** -- Parent components: glue code wiring children, parent-level tests
@@ -85,6 +85,19 @@ Opt-in (`monitoring_enabled: true`). Pact-generated code embeds `PACT:<project_h
8585

8686
**Budget hypervisor**: `estimate_tokens()` provides content-aware token estimation (symbol ratio → chars/token: 3.5 for code, 4.5 for prose). `record_tokens_validated()` cross-validates reported vs estimated counts using `max()` for conservative accounting. The `claude_code` backend no longer falls back to `len(text) // 4`. The `claude_code_team` backend now tracks spend via estimation.
8787

88+
### Goodhart Tests (Hidden Acceptance Criteria)
89+
90+
During the Test phase, Pact generates two test suites per component: **visible tests** (shown to the implementation agent) and **Goodhart tests** (hidden, never shared with agents). This counters Goodhart's Law: when agents can see all tests, they optimize for passing those specific inputs rather than truly satisfying the contract.
91+
92+
Goodhart tests are adversarial — they probe for hardcoded returns, boundary-adjacent inputs, invariant generalization, and postcondition universality. They live in `.pact/contracts/<cid>/goodhart/` and are never loaded by `load_all_test_suites()` or `render_handoff_brief()`.
93+
94+
During the **polish phase**, Goodhart tests run against all implementations. Failures trigger **graduated-disclosure remediation**: the component is re-implemented with behavioral hints (not exact errors) that get more specific on each attempt (max 2 by default). The agent never sees the actual test code.
95+
96+
- **Level 1**: Vague behavioral hint from test description (e.g., "your add() function may not correctly handle the commutative property")
97+
- **Level 2**: Specific contract invariant (e.g., "The contract requires: add(a,b) == add(b,a). Your implementation appears to violate this.")
98+
99+
Config: `max_goodhart_attempts` in pact.yaml (default: 2). Cost: ~$0.07/component for generation (1 LLM call, no research/plan).
100+
88101
### Casual-Pace Scheduling
89102

90103
Poll-based, not event-loop. Agents invoked for focused bursts, state fully persisted between bursts.
@@ -162,7 +175,7 @@ src/pact/
162175
state.json # Run lifecycle state
163176
audit.jsonl # All actions + decisions
164177
decomposition/ # Tree + decisions
165-
contracts/ # Per-component contracts + tests
178+
contracts/ # Per-component contracts + tests + goodhart/
166179
implementations/ # Per-component code + attempts/
167180
standards.json # Global standards (auto-generated after decomposition)
168181
tasks.json # Phased task list (auto-generated after decomposition)
@@ -235,7 +248,7 @@ pact incident <project-dir> <id> # Show incident details + diagnostic repor
235248
## Testing
236249

237250
```bash
238-
make test # 1482 tests, ~7s
251+
make test # 1573 tests, ~7s
239252
make test-quick # Stop on first failure
240253
```
241254

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,24 @@ Interview -----> Shape (opt) -----> Decompose -----> Contract -----> Test
8282
Integrate (glue + parent tests)
8383
|
8484
v
85+
Polish (Goodhart tests + regression check)
86+
|
87+
v
8588
Diagnose (on failure)
8689
```
8790

88-
**Nine phases, all mechanical gates:**
91+
**Ten phases, all mechanical gates:**
8992

9093
1. **Interview** -- Identify risks, ambiguities, ask clarifying questions
9194
2. **Shape** -- (Optional) Produce a Shape Up pitch: appetite, breadboard, rabbit holes, no-gos
9295
3. **Decompose** -- Task into 2-7 component tree, guided by shaping context if present
9396
4. **Contract** -- Each component gets a typed interface contract
94-
5. **Test** -- Each contract gets executable tests (the enforcement)
97+
5. **Test** -- Each contract gets executable tests plus hidden Goodhart tests (adversarial acceptance criteria the implementation agent never sees)
9598
6. **Validate** -- Mechanical gate: refs resolve, no cycles, tests parse
9699
7. **Implement** -- Each component built independently by a code agent
97100
8. **Integrate** -- Parent components composed via glue code
98-
9. **Diagnose** -- On failure: I/O tracing, root cause, recovery
101+
9. **Polish** -- Cross-component regression check + Goodhart test evaluation with graduated-disclosure remediation
102+
10. **Diagnose** -- On failure: I/O tracing, root cause, recovery
99103

100104
## Health Monitoring
101105

@@ -277,7 +281,7 @@ my-project/
277281
state.json # Run lifecycle
278282
audit.jsonl # Full audit trail
279283
decomposition/ # Tree + decisions
280-
contracts/ # Per-component interfaces + tests
284+
contracts/ # Per-component interfaces + tests + goodhart/
281285
implementations/ # Per-component code
282286
compositions/ # Integration glue
283287
learnings/ # Accumulated learnings
@@ -288,7 +292,7 @@ my-project/
288292

289293
```bash
290294
make dev # Install with LLM backend support
291-
make test # Run full test suite (1501 tests)
295+
make test # Run full test suite (1573 tests)
292296
make test-quick # Stop on first failure
293297
make clean # Remove venv and caches
294298
```

docs/index.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,11 @@ <h3>Wavefront Scheduling</h3>
554554
<h3>Prompt Caching</h3>
555555
<p>Static prompt prefixes cached across API calls. 50-70% input token savings. Cache hit rates tracked in budget metrics. Research results persisted and reused across phases.</p>
556556
</div>
557+
<div class="feature-card">
558+
<span class="feature-icon">&#x1f3af;</span>
559+
<h3>Hidden Acceptance Criteria</h3>
560+
<p>Goodhart tests: adversarial hidden tests the agent never sees. Catches hardcoded returns, missing validation, and invariants that hold only for visible inputs. Graduated-disclosure remediation on failure.</p>
561+
</div>
557562
<div class="feature-card">
558563
<span class="feature-icon">&#x1f6e1;&#xfe0f;</span>
559564
<h3>Drift Detection</h3>
@@ -655,7 +660,7 @@ <h2>If AI writes the code, who debugs it at 3am?</h2>
655660
<div class="step"><span class="arrow">&#9656;</span> <span>Fixer agent spawned with contract context</span></div>
656661
<div class="step"><span class="arrow">&#9656;</span> <span>Reproducer test added to contract</span></div>
657662
<div class="step"><span class="arrow">&#9656;</span> <span>Implementation rebuilt from scratch</span></div>
658-
<div class="step"><span class="arrow">&#9656;</span> <span><span class="label">1,501 tests pass</span> <span class="dim">// verified</span></span></div>
663+
<div class="step"><span class="arrow">&#9656;</span> <span><span class="label">1,573 tests pass</span> <span class="dim">// verified</span></span></div>
659664
<div class="step"><span class="arrow">&#9656;</span> <span>Contract got stricter. Bug can't recur.</span></div>
660665
</div>
661666
</div>

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pact-agents"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
description = "Contract-first multi-agent software engineering. Contracts before code. Tests as law. Agents that can't cheat."
55
readme = "README.md"
66
license = {text = "MIT"}

src/pact/agents/test_author.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,147 @@ async def author_tests(
351351
)
352352

353353
return suite, research, plan
354+
355+
356+
# ── Goodhart (Hidden) Test Author ─────────────────────────────────
357+
358+
GOODHART_SYSTEM = """You are an adversarial test author for contract-driven development.
359+
Your job is to generate hidden acceptance tests that catch implementations that
360+
"teach to the test" — passing visible tests through shortcuts rather than truly
361+
satisfying the contract.
362+
363+
Think like a code reviewer who suspects the implementation was written by an agent
364+
that could see the exact test inputs and assertions. Ask yourself:
365+
- What shortcuts could an agent take if it only saw the visible tests?
366+
- Hardcoded returns that happen to match visible test inputs
367+
- Missing validation that visible tests don't exercise
368+
- Invariants that hold only for the specific values in visible tests
369+
- Boundary conditions adjacent to (but distinct from) visible edge cases
370+
- Postconditions that should hold generally but visible tests only check with specific values
371+
372+
Key principles:
373+
- Tests verify behavior at boundaries (inputs/outputs), not internals
374+
- All test functions MUST be prefixed with test_goodhart_
375+
- Each test's description field MUST explain the behavioral property being tested,
376+
NOT the specific assertion. These descriptions become graduated hints during remediation.
377+
- Dependencies must be mocked — tests verify one component in isolation
378+
- Generated code must be syntactically valid
379+
- Do NOT duplicate coverage already in the visible tests — find gaps"""
380+
381+
GOODHART_SYSTEM_TS = """You are an adversarial test author for contract-driven development.
382+
Your job is to generate hidden acceptance tests that catch implementations that
383+
"teach to the test" — passing visible tests through shortcuts rather than truly
384+
satisfying the contract.
385+
386+
Think like a code reviewer who suspects the implementation was written by an agent
387+
that could see the exact test inputs and assertions. Ask yourself:
388+
- What shortcuts could an agent take if it only saw the visible tests?
389+
- Hardcoded returns that happen to match visible test inputs
390+
- Missing validation that visible tests don't exercise
391+
- Invariants that hold only for the specific values in visible tests
392+
- Boundary conditions adjacent to (but distinct from) visible edge cases
393+
- Postconditions that should hold generally but visible tests only check with specific values
394+
395+
Key principles:
396+
- Tests verify behavior at boundaries (inputs/outputs), not internals
397+
- All test names MUST contain "goodhart" (e.g., "goodhart: should handle...")
398+
- Each test's description field MUST explain the behavioral property being tested,
399+
NOT the specific assertion. These descriptions become graduated hints during remediation.
400+
- Dependencies must be mocked — tests verify one component in isolation
401+
- Generated code must be syntactically valid TypeScript for Vitest
402+
- Do NOT duplicate coverage already in the visible tests — find gaps"""
403+
404+
405+
async def author_goodhart_tests(
406+
agent: AgentBase,
407+
contract: ComponentContract,
408+
visible_suite: ContractTestSuite,
409+
dependency_contracts: dict[str, ComponentContract] | None = None,
410+
language: str = "python",
411+
) -> ContractTestSuite:
412+
"""Generate adversarial hidden tests (single LLM call, no research/plan).
413+
414+
These tests are never shown to the implementation agent. They catch
415+
Goodhart's Law violations: implementations that optimize for visible
416+
test inputs rather than truly satisfying the contract.
417+
418+
Args:
419+
agent: The LLM agent backend.
420+
contract: The component contract.
421+
visible_suite: The visible test suite (for gap analysis).
422+
dependency_contracts: Contracts for dependencies (mock info).
423+
language: Test language — "python", "typescript", or "javascript".
424+
425+
Returns:
426+
ContractTestSuite with adversarial test cases.
427+
"""
428+
contract_summary = _render_focused_contract(contract)
429+
430+
# Summarize visible tests so the LLM knows what's already covered
431+
visible_summary = "\n".join(
432+
f" - {tc.id}: {tc.description} [{tc.category}] (function: {tc.function})"
433+
for tc in visible_suite.test_cases
434+
)
435+
436+
dep_mock_info = ""
437+
if dependency_contracts:
438+
for dep_id, dc in dependency_contracts.items():
439+
dep_mock_info += f"\nDependency '{dep_id}' functions to mock:\n"
440+
for func in dc.functions:
441+
inputs_str = ", ".join(f"{i.name}: {i.type_ref}" for i in func.inputs)
442+
dep_mock_info += f" - {func.name}({inputs_str}) -> {func.output_type}\n"
443+
444+
# Select language-specific system prompt and instructions
445+
if language in ("typescript", "javascript"):
446+
system_prompt = GOODHART_SYSTEM_TS
447+
import_hint = f"import {{ ... }} from '../src/{contract.component_id}'"
448+
framework = "Vitest"
449+
test_lang = language
450+
else:
451+
system_prompt = GOODHART_SYSTEM
452+
import_hint = f"from src.{contract.component_id} import *"
453+
framework = "pytest"
454+
test_lang = "python"
455+
456+
prompt = f"""Generate adversarial hidden acceptance tests for component '{contract.name}'.
457+
458+
Contract:
459+
{contract_summary}
460+
461+
Visible tests already covering this contract:
462+
{visible_summary}
463+
464+
{f"Dependencies to mock:{dep_mock_info}" if dep_mock_info else ""}
465+
466+
Requirements:
467+
- component_id must be "{contract.component_id}"
468+
- contract_version must be {contract.version}
469+
- All test functions prefixed with test_goodhart_ (Python) or described as "goodhart: ..." (TS/JS)
470+
- Each test_case description must explain the BEHAVIORAL PROPERTY, not the assertion
471+
Good: "The add function should be commutative for all numeric inputs"
472+
Bad: "Test that add(2,3) equals add(3,2)"
473+
- Find gaps in visible test coverage — do NOT duplicate existing tests
474+
- Focus on: hardcoded-return detection, boundary adjacency, invariant generalization,
475+
postcondition universality, input-space exploration beyond visible values
476+
- Import from: {import_hint}
477+
- Use {framework} conventions
478+
- test_language must be "{test_lang}"
479+
- generated_code must contain the COMPLETE test file"""
480+
481+
cache_prefix = f"Contract:\n{contract_summary}\n\nVisible tests:\n{visible_summary}"
482+
483+
suite, in_tok, out_tok = await agent.assess_cached(
484+
ContractTestSuite, prompt, system_prompt, cache_prefix=cache_prefix,
485+
)
486+
487+
# Ensure required fields
488+
suite.component_id = contract.component_id
489+
suite.contract_version = contract.version
490+
suite.test_language = language
491+
492+
logger.info(
493+
"Goodhart tests authored for %s: %d cases (%d tokens)",
494+
contract.component_id, len(suite.test_cases), in_tok + out_tok,
495+
)
496+
497+
return suite

src/pact/budget.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,16 @@
1919
# Built-in defaults — overridable via config.yaml model_pricing
2020
# Format: model_id -> (input_cost_per_million, output_cost_per_million)
2121
DEFAULT_MODEL_PRICING: dict[str, tuple[float, float]] = {
22-
# Anthropic
23-
"claude-haiku-4-5-20251001": (0.80, 4.00),
22+
# Anthropic — https://platform.claude.com/docs/en/about-claude/pricing
23+
"claude-haiku-3": (0.25, 1.25),
24+
"claude-haiku-3-5": (0.80, 4.00),
25+
"claude-haiku-4-5-20251001": (1.00, 5.00),
26+
"claude-sonnet-4": (3.00, 15.00),
2427
"claude-sonnet-4-5-20250929": (3.00, 15.00),
28+
"claude-sonnet-4-6": (3.00, 15.00),
29+
"claude-opus-4": (15.00, 75.00),
30+
"claude-opus-4-1": (15.00, 75.00),
31+
"claude-opus-4-5": (5.00, 25.00),
2532
"claude-opus-4-6": (5.00, 25.00),
2633
# OpenAI
2734
"gpt-4o": (2.50, 10.00),

src/pact/cli.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@ def main() -> None:
251251
p_pricing.add_argument("--export", action="store_true", help="Export pricing to ~/.config/pact/model_pricing.json")
252252
p_pricing.add_argument("--json", action="store_true", dest="json_output", help="Output as JSON")
253253

254+
# wizard
255+
p_wizard = subparsers.add_parser("wizard", help="Guided project setup wizard")
256+
p_wizard.add_argument("project_dir", help="Project directory path")
257+
p_wizard.add_argument("--config", default=None, metavar="FILE", help="JSON/YAML config file for non-interactive mode")
258+
p_wizard.add_argument("--budget", type=float, default=None, help="Override budget (skips budget question)")
259+
254260
args = parser.parse_args()
255261

256262
if args.verbose:
@@ -332,6 +338,67 @@ def main() -> None:
332338
cmd_health(args)
333339
elif args.command == "pricing":
334340
cmd_pricing(args)
341+
elif args.command == "wizard":
342+
cmd_wizard(args)
343+
344+
345+
def cmd_wizard(args: argparse.Namespace) -> None:
346+
"""Guided project setup wizard."""
347+
from pathlib import Path
348+
349+
import yaml
350+
351+
from pact.project import ProjectManager
352+
from pact.wizard import (
353+
build_wizard_questions,
354+
generate_pact_yaml,
355+
generate_sops_md,
356+
generate_task_md,
357+
load_wizard_config_from_file,
358+
run_wizard_interactive,
359+
)
360+
361+
project_dir = args.project_dir
362+
363+
if args.config:
364+
config = load_wizard_config_from_file(Path(args.config))
365+
else:
366+
questions = build_wizard_questions()
367+
print("Pact Project Wizard")
368+
print("=" * 40)
369+
print("Answer each question to configure your project.")
370+
print("Press Enter to accept defaults shown in [brackets].\n")
371+
config = run_wizard_interactive(questions)
372+
373+
if args.budget is not None:
374+
config.budget = args.budget
375+
376+
project = ProjectManager(project_dir)
377+
project.init(budget=config.budget)
378+
379+
project.task_path.write_text(generate_task_md(config))
380+
project.sops_path.write_text(generate_sops_md(config))
381+
382+
pact_yaml = generate_pact_yaml(config)
383+
with open(project.config_path, "w") as f:
384+
yaml.dump(pact_yaml, f, default_flow_style=False, sort_keys=False)
385+
386+
print(f"\nProject initialized: {project.project_dir}")
387+
print(f" task.md - Pre-filled from your description")
388+
print(f" sops.md - Tailored for {config.language}")
389+
print(f" pact.yaml - Configured with your preferences")
390+
391+
if config.run_interview:
392+
print("\nStarting interview phase...")
393+
asyncio.run(cmd_interview(argparse.Namespace(
394+
project_dir=project_dir, verbose=False,
395+
)))
396+
else:
397+
print(f"\nNext steps:")
398+
print(f" 1. Review and refine task.md")
399+
print(f" 2. pact interview {project_dir}")
400+
print(f" 3. pact approve {project_dir}")
401+
print(f" 4. pact run {project_dir}")
335402

336403

337404
def cmd_pricing(args: argparse.Namespace) -> None:

src/pact/decomposer.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from pact.agents.base import AgentBase
1717
from pact.agents.contract_author import author_contract
18-
from pact.agents.test_author import author_tests
18+
from pact.agents.test_author import author_goodhart_tests, author_tests
1919
from pact.contracts import validate_all_contracts
2020
from pact.project import ProjectManager
2121
from pact.schemas import (
@@ -418,6 +418,19 @@ async def decompose_and_contract(
418418
"tests",
419419
f"{component_id}: {len(suite.test_cases)} cases",
420420
)
421+
422+
# Generate Goodhart (hidden) tests — skip if already exist
423+
if not project.load_goodhart_suite(component_id):
424+
goodhart_suite = await author_goodhart_tests(
425+
agent, contract, suite,
426+
dependency_contracts=dep_contracts,
427+
language=project.language,
428+
)
429+
project.save_goodhart_suite(goodhart_suite)
430+
project.append_audit(
431+
"goodhart_tests",
432+
f"{component_id}: {len(goodhart_suite.test_cases)} hidden cases",
433+
)
421434
else:
422435
logger.info("Skipping tests for %s — already exist", component_id)
423436

0 commit comments

Comments
 (0)