# InfraFactory Agent Working Agreement

For AI coding agents. Human contributors should use `CONTRIBUTING.md`.

## Mission
Build `infrafactory`, a Go CLI + SvelteKit UI that generates and validates OpenTofu across **AWS**, **GCP**, **Scaleway**, and **Genesys Cloud CCaaS** with deterministic, testable behavior. Each cloud's scenarios validate against a deterministic HTTP-level mock (fakeaws / fakegcp / mockway / fakegenesys); S3 routes through a small in-repo shim (`cmd/s3router/`) that fans traffic between SeaweedFS (data plane) and fakeaws (`?publicAccessBlock` subresource SeaweedFS doesn't model). See `CONCEPT.md` § "Third-Party Mock Integration".

## Source of Truth
1. `scenario.schema.json`
2. `infrafactory.yaml`
3. `CONCEPT.md` prose

Additional references:
- Auto-learning loop deep-dive: `docs/auto-learning-loop.md` — single explainer for the mock-server-bug classifier + fix/avoid extractors + diff-pattern templates + ratchets + sweep protocol + worked example
- ADRs: `docs/decisions/*.md`
- Prompts: `prompts/*.md`
- Pitfalls: `pitfalls/{cloud}.yaml` — provider-specific rules loaded at runtime by scenario `cloud` field
- Progress log: `STATUS.md`
- Backlog source of truth: `BACKLOG.md`

## Project File Ecosystem

| File | Purpose | When to update |
|---|---|---|
| `docs/plans/<arc-name>-plan.md` | Goal-named variable-length arc plan (typically 2-4 slices). Tickets, exit criteria, autonomous-execution prompt. | When planning the next arc |
| `docs/NEXT_SESSION.md` | Fresh-context handoff — points at the active arc + open follow-ups | At end of each meaningful coding session |
| `docs/status/ARCHIVE.md` | Per-arc close-out narratives — durable history | At arc close-out |
| `STATUS.md` | Current phase + recent arc summaries | At end of each meaningful coding session |
| `BACKLOG.md` | M-ticket maintenance backlog (cross-arc work that doesn't fit the active arc). Stub today — no active M-tickets. Historical entries in `BACKLOG_ARCHIVE.md`. | When a maintenance need surfaces that doesn't belong in the active arc plan |
| `CONCEPT.md` | Durable architecture, contracts, design decisions | Only for major architecture/design shifts |
| `docs/decisions/*.md` | ADRs for decision-impacting changes | When change crosses ADR trigger threshold (see below) |

## Planning a New Arc

Arcs are **goal-named** and **variable-length** (typically 2-4 slices, sometimes up to 6 — but driven by the goal, not by a slice-count template). Adopted 2026-06-03 after the S89–S93 close-out (`docs/status/ARCHIVE.md` § "2026-06-03 S89–S93" → "Scaffold question").

The shape:

1. **Name the arc by goal**, not by slice numbers — e.g. "39/39 sustain validation", "fakegcp panic audit". Filename: `docs/plans/<arc-name>-plan.md` (kebab-case). The arc still numbers its slices sequentially (S94, S95, …) for cross-reference into commits / ARCHIVE entries.
2. **Write the plan**: Big picture (what + why), Slices table (as many as the goal needs, including any sweep / audit / investigation steps), Standing rules (inherit from prior arcs), per-slice motivation + tickets + exit criteria, autonomous-execution loop prompt, fresh-context checklist.
3. **No padding.** If the goal naturally fits in 2 slices, the plan is 2 slices. The 5-slice template from S54–S93 is retired.
4. **Mandatory close-out**: every arc, regardless of length, ends with a `docs/status/ARCHIVE.md` per-arc section + `docs/NEXT_SESSION.md` update. The close-out can be the last slice or folded into the last substantive PR — but it has to happen. (The ARCHIVE entries are the project's institutional memory and the only thing that makes a fresh session bootable.)
5. **Repoint** `STATUS.md` "Next arc planned" line + `docs/NEXT_SESSION.md` at the new plan.
6. **Get approval** from the user before kicking off the autonomous loop.

ADRs only when crossing the threshold below. Plan files are the slice ticket source — `BACKLOG.md` is only for cross-arc maintenance work (M-tickets).

## Fresh Context

When starting a new conversation, follow this checklist:

### 1) Load minimal context
1. `README.md`
2. `AGENTS.md` (this file)
3. `docs/NEXT_SESSION.md` (open follow-ups from prior session — read FIRST when starting work)
4. `STATUS.md`
5. `docs/plans/slices-*.md` for the active arc — see `docs/NEXT_SESSION.md` for the current arc pointer
6. `BACKLOG.md` (M-ticket maintenance backlog; slice tickets live in plan files)
7. `CONCEPT.md` (if major design context is needed)
8. `docs/decisions/README.md` (+ relevant ADRs)

Slice work is organised as goal-named arcs (variable-length, typically 2-4 slices). Each arc lives in `docs/plans/<arc-name>-plan.md` with an autonomous-execution loop prompt at the bottom. `docs/status/ARCHIVE.md` has per-arc close-out narratives. Historical 5-slice arcs (S54–S93) live under `docs/plans/slices-<a>-<b>-plan.md`; the naming convention shifted at the S94 boundary.

### 2) Preflight
```bash
git status --short
git branch --show-current
git log -1 --oneline
```
- If unexpected local changes appear, stop and ask the user.
- Confirm active arc in `docs/NEXT_SESSION.md` § "Suggested next arc" or "READ FIRST"; blockers in `STATUS.md`.
- Pick next uncompleted slice from the active `docs/plans/slices-*-plan.md`.
- For maintenance work not tied to an arc, see `BACKLOG.md` M-tickets.

### 3) Startup verification
```bash
go test -tags noui ./...    # Use -tags noui until ui/build/ exists
bash scripts/check_all.sh
```
If either fails, restore the repo to a green baseline before starting a new ticket.

### 4) Operational caveats
- Prefer `run` over manual `generate` + `test` — only `run` feeds prior iteration failures into LLM generation.
- Use `http://127.0.0.1:8080` for local Mockway checks (more reliable than `localhost`).
- Port 8080 conflicts are common — check for stale containers before `mock start`.
- Debug iterative behavior from `.infrafactory/runs/<scenario>/<run-id>/iterations/<n>/iteration.json`.
- `output/<scenario>/` is mutable (overwritten each run); immutable snapshots live under `.infrafactory/runs/<scenario>/<run-id>/generated/`.
- `CLAUDECODE` env var blocks nested claude — `unset CLAUDECODE` before `go run ./cmd/infrafactory run`.
- Mock rebuild required after any sibling-mock code change: `pkill -f <mock-bin>; cd ../<mock> && go build && ./<bin> --port <port> &`. For containerised runs use `make mocks-down-containers && make mocks-up-containers`.
- Build tag: `-tags noui` required when `ui/build/` doesn't exist. The `!noui` build requires `ui/build/`.
- Playwright e2e tests live in `ui/e2e/` (currently 51 tests, growing). Run with `make test` (Go unit + UI unit + Playwright).
- Visual baselines under `ui/e2e/visual.spec.ts-snapshots/` render live UI state — adding scenario YAMLs OR completing runs (which add rows to the Runs page) drifts them. Pre-commit hook auto-refreshes when `scenarios/training/*.yaml` changes (M56); for other drift, run `make ui-baseline-update` manually.
- `make run` builds everything and starts the UI at `http://127.0.0.1:4173`.
- `make up` is the one-shot bring-up: mockway + fakegcp + fakeaws + fakegenesys + SeaweedFS + UI in one command. `make down` tears down the mocks (Ctrl-C stops the UI).
- **Sweep protocol** (see `feedback_sweep_protocol.md` memory): treat failures as either (a) mock-server gaps → fix at source in `fakeaws`/`fakegcp`/`mockway`, never seed `pitfalls/*.yaml`; or (b) LLM-generated HCL mistakes → let auto-learning capture a pitfall. Four CI ratchets enforce pitfall purity: `TestPitfallsNoHumanSeeding` rejects `source: seed`/`static`; `TestPitfallsNoMockServerBugSeeds` rejects mock-actionable substrings (the entries belong in `docs/mock-gaps.md`); `TestPitfallsNoOPADuplication` rejects verbatim OPA-msg duplication; `TestPitfallsSourceEnum` fences the allowed source-field values to `descriptive` / `fix` / `avoid` only. After-sweep cleanup: `scripts/sweep_39.sh` runs `bin/pitfall-merge` to selectively restore — `avoid` entries (deletion-as-fix, grounded in a confirmed successful run) are preserved; `fix` + `descriptive` discarded as sweep noise. The sweep harness also classifies + retries transport failures: in-loop, if a scenario hits the transport shape (`terminal in {repair_budget_exhausted, stuck}` + `dur_s < 60` + only `_generate` or `_validate` stage failures, no `_test`) — Claude CLI rate-limit OR OpenTofu provider-registry blip — it retries ONCE. Emits `RETRY_TRANSPORT=N` (attempted) and `RETRY_RECOVERED=M` (succeeded on retry). End-of-sweep classifier re-applies the same predicate as a fallback for cases where the retry also failed → `transport_failed` distinct from `repair_budget_exhausted` so the deterministic pass count is uncontaminated. **mock-gaps drainage**: `docs/mock-gaps.md` accumulates entries from the classifier but never self-prunes. Drain it after each major sustain arc (or when it grows past ~10 entries): replay each `discovered_from` scenario; delete entries whose failure no longer reproduces; ship sibling-mock fixes for entries that still fail. Protocol detail in `docs/auto-learning-loop.md` § "mock-gaps drainage".
- **Cross-repo cascade commits**: lifecycle-parity work spans infrafactory + a sibling mock. Commit the mock-side change first (it's the dependency), then update infrafactory's e2e test or call sites that depend on the new mock behavior. All four repos use origin/main; push order matters.
- **Demo recording tooling**: `asciinema` (CLI demo via `./docs/demo/record.sh`) → `.cast` → `agg` → `.gif`; Playwright (UI demo via `make demo-ui`) → `.webm` → `gifski` → `.gif`. Build-time dep only; never advertised in user-facing docs.

## Execution Loop (mandatory)
1. Frame task with `docs/process/TICKET_TEMPLATE.md`.
2. Classify change: `implementation-only` or `decision-impacting`.
3. If `decision-impacting`, create/update ADR (`docs/decisions/NNNN-title.md`).
4. Implement smallest runnable vertical slice.
5. Add/update focused tests.
6. Run `go test ./...` (or report why not possible).
7. Sync docs: update `STATUS.md`, `BACKLOG.md` ticket status. Update `CONCEPT.md` for major shifts. Update `AGENTS.md` only when workflow changes.
8. Run hygiene check: `bash scripts/check_all.sh`.

## Sibling Mock Repos

Three first-party HTTP-level mocks + one third-party backend live alongside infrafactory:

- **mockway** (`../mockway`, github.com/redscaresu/mockway) — Scaleway mock; 280+ tests; runs on `:8080`. Apache-2.0, public.
- **fakegcp** (`../fakegcp`, github.com/redscaresu/fakegcp) — GCP mock; runs on `:8081`. Mockway-level test parity reached 2026-05-23 (881-line repository_test.go, FK violation tests, cascade delete tests). Memorystore + Cloud SQL + GKE + IAM + Storage + DNS + Pub/Sub + Secret Manager + Cloud Run + Cloud KMS (added 2026-05-31 in fakegcp@c7999b5).
- **fakeaws** (`../fakeaws`, github.com/redscaresu/fakeaws) — AWS mock; runs on `:8082`. Ships 10 services across 5 wire formats (IAM, S3, EC2, RDS, DynamoDB, EKS, SQS, Route53, Secrets Manager, KMS); aggregate handler coverage 82.4%; 17 codex review passes archived under `../fakeaws/docs/review-passes/`. EC2/IAM/Route53/DynamoDB substantially broadened 2026-05-30 → 2026-05-31 from a self-learning sweep (see fakeaws@348322d).
- **fakegenesys** (`../fakegenesys`, github.com/redscaresu/fakegenesys) — Genesys Cloud CCaaS mock; runs on `:8083`. Ships 15 resources across identity (user, group, location, auth_role, oauth_client), routing (queue + members + skill + wrapupcode + language + utilization singleton), and architect (datatable + rows + user_prompt + flow with multipart + lock/publish state machine + responsemanagement_response + idp_generic singleton). 4 codex review passes archived under `../fakegenesys/docs/review-passes/` (S112/S113 + S124's passes 3/4 closing on 2× NOTHING_TO_IMPROVE). Spec-driven fidelity via `specs/genesys-openapi.json`. Added 2026-06-06 (arc S108-S115); hardened in arc S123-S127 + sibling rollout S128-S130 (v0.2.0).
- **SeaweedFS** (`chrislusf/seaweedfs` container) — third-party S3 backend for `aws_s3_bucket` reads (`terraform-provider-aws` needs the full management surface; fakeaws's stripped S3 handler isn't enough). Runs on `:9090` via `docker-compose.mocks.yml`. Anonymous-mode `ListAllMyBuckets` returns empty even when buckets exist — use HEAD-by-name as the assertion path. Empirical evaluation log in `CONCEPT.md` § "Third-Party Mock Integration" (rejects Adobe S3Mock + Garage + LocalStack + MinIO).
- **s3router** (`cmd/s3router/`, S80) — in-repo reverse-proxy shim that listens on `:9091` and fans S3 traffic across SeaweedFS and fakeaws. `?publicAccessBlock` → fakeaws (SeaweedFS uniquely 501s on that subresource); everything else → SeaweedFS; `PUT/DELETE /<bucket>` fans out to both. `infrafactory.yaml` `s3.url` points at the shim, not SeaweedFS directly. Add a subresource to `fakeawsSubresources` in `main.go` only when a new SeaweedFS 501 surfaces. ADR-0015 § "S80 — S3 backend router" carries the rationale.

All four sibling repos are independent public OSS repos on origin/main; cross-repo work cascades (see "operational caveats" above). The s3router is part of the infrafactory repo, not a sibling.

When extending a sibling mock, mirror the per-bundle PR rule in `../fakeaws/concepts.md` — handler + tests + examples + scenario anchors + coverage_matrix.yaml + `LandedServices` flip all in one slice. The `TestFullCoverageAudit` + `TestRegressionSeedAuditManifestMatchesHandlers` audits in each mock repo enforce this.

**Provider smoke-harness pattern (canonical)**: every sibling fake uses the same `examples/provider_smoke_test.go` pattern — `examples/{working,misconfigured,updates}/<svc>/` directories auto-discovered, real provider binary run against the fake (no real-cloud credentials needed; the provider IS the wire-format validator). Gating is per-repo env var (`MOCKWAY_ENABLE_E2E` / `FAKEGCP_ENABLE_E2E` / `INFRAFACTORY_ENABLE_E2E` / `FAKEGENESYS_ENABLE_E2E`). When spawning a new sibling, copy this harness as-is — it's the cheapest correctness gate we have. Detail in each sibling's `AGENTS.md` § "Provider smoke harness".

**Contract-coverage convention (canonical)**: every sibling fake ships a `handlers/contract_audit_test.go` that enforces the `CRITICAL[<id>]:` / `MUST[<id>]:` docstring → `TestContract_<id>` test pairing. A wire-shape invariant the consuming provider depends on must NOT live as a comment alone — drift becomes a failed `go test`, not a missed code review. New sibling fakes inherit the file as part of the day-one OSS checklist (see `feedback_oss_mature_day_one.md` item 14). Reference impl: `../fakegenesys/handlers/contract_audit_test.go`. Convention rolled out across all 4 siblings in S127 (5th instance of the 4-PR cross-repo doc/code sweep pattern); existing wire-shape invariants in mockway/fakegcp/fakeaws bridged into the convention in S128–S130 (27 paired contracts across the family).

### Sibling-fake fidelity strategies

How each sibling fake decides what wire shape a handler SHOULD return (the smoke harness above is the correctness *gate*, not the discovery *source*). Quick reference for fresh agents:

| Fake | Strategy | Source of truth | Cost per new handler |
|---|---|---|---|
| **mockway** | Spec-driven | `specs/` tree (downloaded Scaleway OpenAPI YAML) | Low — spec gives shape upfront |
| **fakegcp** | Hybrid | GCP discovery docs (referenced ad-hoc, no `specs/` tree) | Medium — semi-discovered per handler |
| **fakeaws** | Reactive | `terraform-provider-aws` source + `TF_LOG=DEBUG` capture | High — ~1-2hr per service before handler is confident |
| **fakegenesys** | Spec-driven (mirrors mockway) | `specs/genesys-openapi.json` (filtered Genesys Swagger 2.0, ~200KB) | Low |

**Recommendation for new fakes**: prefer spec-driven if the target cloud publishes an OpenAPI / Swagger / discovery doc. For sibling extensions: opportunistic upgrade — existing handlers stable, new ones can adopt spec-driven if a spec is available.

Detail in each sibling's `AGENTS.md` § "Fidelity strategy".

## ADR Trigger Threshold
Create/update ADR when change affects:
- public CLI contract/wiring
- cross-package architecture boundaries
- schema semantics (`scenario.schema.json`, `infrafactory.yaml`)
- external dependency strategy (tofu/mockway/opa integration model)
- durable workflow governance

## Engineering Rules
- Keep command handlers thin; put logic in `internal/*` packages.
- Keep packages cohesive: `internal/cli`, `internal/config`, `internal/scenario`, `internal/generator`, `internal/harness`, `internal/feedback`, `internal/runstore`, `internal/api`.
- `ui/` — SvelteKit frontend (adapter-static, embedded via `go:embed`).
- Use explicit structs and typed errors.
- Keep behavior deterministic and tests hermetic where possible.
- **Assertion convention (Go tests, project-wide)**: default to `github.com/stretchr/testify` for assertions where possible. `assert.Equal` / `require.NoError` / `assert.Contains` over `if x != y { t.Fatalf(...) }`. Use `require` when a failure should stop the test (setup steps, anything whose failure would cause downstream nil-deref / panic). Use `assert` otherwise so multiple failures surface in one run. Don't bare-literal HTTP status codes — `http.StatusNoContent` not `204`. The qualifier "where possible" is real: if an if-fatalf block carries a fundamentally custom error message the assertion library can't express, leave it stdlib — don't force conversions that hurt readability. Same rule lives in every sibling fake's AGENTS.md.

## Quality Bar
- `go test ./...` passes for completed slices.
- Stubs must return explicit "not implemented" errors.
- No hidden side effects outside project paths.

## The auto-learning pipeline is load-bearing — never excuse its silence

When `infrafactory run` exits with `repair_budget_exhausted` or `stuck` on a failure that is NOT `IsMockServerBug`-classified, the dynamic loop's pipeline failed to extract a pitfall. Treat that as a real bug to investigate, NOT as "expected cold-start", "first run is allowed to fail", or "sweep 2 will fix it". Those framings hide systemic gaps.

Why this matters: 2026-06-06 sustain sweep 1 surfaced `genesys-architect-flow` exhausting its budget after 5 oscillating iterations. The "expected cold start" framing nearly let it pass — investigation revealed `resourceNameRe` had been hardcoded to `(scaleway|google|aws)_\w+` since the regex was written, so every Genesys failure produced an empty Resource, `ExtractDescriptivePitfall` returned nil, and **zero pitfalls had ever been auto-learned from Genesys runs**. Fixed in infrafactory#96.

Diagnostic protocol when a run exits without learning:
1. Grep the run's `app.log` for `pitfall_emitted`, `oscillation_pitfall_*`, `self_correction_pitfall_*`, `mock_gap_recorded`. None firing + failure not mock-classified ⇒ pipeline silently no-op'd.
2. Drop a scratch `_test.go` next to `internal/generator/pitfalls_learn.go`, call `IsMockServerBug`, `ExtractResourceFromDetail`, `ExtractDescriptivePitfall` against the failure detail. Empty Resource + nil pitfall ⇒ the regex or classifier doesn't recognise this resource family.
3. When adding a new cloud (or any new resource-name prefix), there are three sites to update: `resourceNameRe`, `addressRe`, `pitfallResourceMatchesCloud`. Miss one and the learner breaks silently. **Enforced**: `internal/cli/cloud_prefix_lockstep_test.go::TestCloudPrefixLockstep` parses all three sites and fails CI if they disagree on the cloud-prefix set (per ADR-0021).

## Scaleway Bootstrap (Layer 3 Prerequisites)

Layer 3 uses self-managed project lifecycle per ADR-0010. Generated HCL includes `scaleway_account_project` — infrafactory creates/destroys its own project. No pre-existing sandbox required.

**User must provide:**
1. Org-level API keys (IAM -> API Keys, organization-level permissions).
2. Env vars: `SCW_ACCESS_KEY`, `SCW_SECRET_KEY`.
3. Enable Layer 3: `validation.layers.sandbox_deploy.enabled: true` in `infrafactory.yaml`.

## Secrets
- Never commit `.env`, credentials, API keys, or private keys.
- `.gitignore` blocks common secret files (`.env`, `credentials.json`, `*.pem`, `*.key`).
- Pre-commit hook scans staged diffs for secret patterns (`SCW_ACCESS_KEY=`, `OPENROUTER_API_KEY=`, `BEGIN PRIVATE KEY`, etc.).
- If the hook blocks your commit, remove the secret from the file and use environment variables instead.
- Same protections apply to mockway and fakegcp repos.

## Safety
- Never revert/delete unrelated user changes.
- Never use destructive git commands without explicit request.
- If unexpected external changes appear, stop and ask the user.
