foundry-implementation-actor 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. foundry_implementation_actor-0.1.0/.github/workflows/ci.yml +48 -0
  2. foundry_implementation_actor-0.1.0/.github/workflows/release.yml +40 -0
  3. foundry_implementation_actor-0.1.0/.gitignore +5 -0
  4. foundry_implementation_actor-0.1.0/CLAUDE.md +90 -0
  5. foundry_implementation_actor-0.1.0/PKG-INFO +217 -0
  6. foundry_implementation_actor-0.1.0/README.md +197 -0
  7. foundry_implementation_actor-0.1.0/adr/ADR-FIA-0001-the-machinery-leaves-the-capability.md +145 -0
  8. foundry_implementation_actor-0.1.0/adr/README.md +19 -0
  9. foundry_implementation_actor-0.1.0/adr/template.md +28 -0
  10. foundry_implementation_actor-0.1.0/pyproject.toml +43 -0
  11. foundry_implementation_actor-0.1.0/scripts/probe_grounding.py +67 -0
  12. foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/__init__.py +38 -0
  13. foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/cli.py +107 -0
  14. foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/config.py +405 -0
  15. foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/correlation.py +193 -0
  16. foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/engine.py +434 -0
  17. foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/grounding.py +197 -0
  18. foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/handler.py +220 -0
  19. foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/schemas/agentic-context.schema.yaml +143 -0
  20. foundry_implementation_actor-0.1.0/tests/conftest.py +118 -0
  21. foundry_implementation_actor-0.1.0/tests/fixtures/broken/actor-agentic-context.yaml +14 -0
  22. foundry_implementation_actor-0.1.0/tests/fixtures/valid/actor-agentic-context.yaml +28 -0
  23. foundry_implementation_actor-0.1.0/tests/test_cli.py +53 -0
  24. foundry_implementation_actor-0.1.0/tests/test_config.py +193 -0
  25. foundry_implementation_actor-0.1.0/tests/test_engine.py +144 -0
  26. foundry_implementation_actor-0.1.0/tests/test_grounding.py +155 -0
  27. foundry_implementation_actor-0.1.0/tests/test_handler.py +114 -0
  28. foundry_implementation_actor-0.1.0/tests/test_portability.py +68 -0
  29. foundry_implementation_actor-0.1.0/uv.lock +191 -0
@@ -0,0 +1,48 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+
4
+ # NO CREDENTIAL. This pipeline reaches nothing outside its own checkout — no registry, no lab
5
+ # repo, no PyPI. Everything it asserts, it asserts from source and from a wheel it built itself.
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v5
13
+
14
+ - name: the test suite
15
+ run: uv run --extra dev pytest -q
16
+
17
+ - name: build
18
+ run: uv build
19
+
20
+ # ── the two gates a contract-shipping package runs against its own artifact ────────────
21
+ #
22
+ # The suite above proves the code works in a source checkout. Neither of these does — they
23
+ # prove the WHEEL works, which is a different claim and the one a consumer actually depends
24
+ # on. A schema left out of the build passes every test in tests/ and fails here.
25
+
26
+ - name: the wheel must carry its contract
27
+ run: |
28
+ uv venv /tmp/probe
29
+ uv pip install --python /tmp/probe/bin/python -q dist/*.whl
30
+ /tmp/probe/bin/foundry-implementation-actor lint tests/fixtures/valid
31
+ /tmp/probe/bin/foundry-implementation-actor show tests/fixtures/valid \
32
+ --registry reg.example.com | tee /tmp/show.txt
33
+ grep -q 'reg.example.com/acme.parts/sup.007.wid/backend:<version>' /tmp/show.txt
34
+
35
+ - name: the gate must run
36
+ # A gate that cannot fail is not a gate. Lint a deliberately non-conformant sidecar with
37
+ # the INSTALLED wheel and require a non-zero exit.
38
+ run: |
39
+ if /tmp/probe/bin/foundry-implementation-actor lint tests/fixtures/broken; then
40
+ echo "::error::lint accepted a non-conformant sidecar — the gate is not running"
41
+ exit 1
42
+ fi
43
+ echo "the gate refused a non-conformant sidecar, as it should"
44
+
45
+ - name: grounding renders a CLAUDE.md whose imports resolve
46
+ # The property the whole design rests on: an @-import naming a file that is not there is a
47
+ # session grounded in nothing, and it looks identical to one grounded correctly.
48
+ run: /tmp/probe/bin/python scripts/probe_grounding.py tests/fixtures/valid
@@ -0,0 +1,40 @@
1
+ name: release
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+
6
+ # PyPI Trusted Publishing (OIDC) — no token is stored anywhere. The one-time setup is a pending
7
+ # publisher on pypi.org naming this repo and this workflow; after the first release it becomes a
8
+ # normal publisher. See README.
9
+ permissions:
10
+ id-token: write
11
+ contents: read
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+ environment: pypi
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v5
20
+
21
+ - name: build
22
+ # No fetch step and no external token: the contract is committed here. A release depends
23
+ # on nothing but this checkout and PyPI.
24
+ run: uv build
25
+
26
+ - name: the wheel must carry its contract
27
+ run: |
28
+ uv venv /tmp/probe
29
+ uv pip install --python /tmp/probe/bin/python -q dist/*.whl
30
+ /tmp/probe/bin/foundry-implementation-actor lint tests/fixtures/valid
31
+
32
+ - name: grounding renders a CLAUDE.md whose imports resolve
33
+ # BEFORE the upload, not after. A wheel that ships without its schema, or whose grounding
34
+ # emits an @-import to a file it never wrote, produces sessions grounded in nothing that
35
+ # look identical to correct ones. That is not something to discover from a published
36
+ # version.
37
+ run: /tmp/probe/bin/python scripts/probe_grounding.py tests/fixtures/valid
38
+
39
+ - name: publish to PyPI
40
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,5 @@
1
+ dist/
2
+ *.egg-info/
3
+ __pycache__/
4
+ .venv/
5
+ .pytest_cache/
@@ -0,0 +1,90 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this
4
+ repository.
5
+
6
+ ## What this is
7
+
8
+ `foundry-implementation-actor` runs a headless Claude Code implementation session against one
9
+ capability's own repo, then commits, pushes and publishes what it produced. It is the **machinery**
10
+ — it carries no capability of its own. The capability arrives in a sidecar
11
+ (`actor-agentic-context.yaml`, contract `foundry-implementation-actor/agentic-context/v1`) that the
12
+ consuming repo writes.
13
+
14
+ Extracted from one capability's implementation repo, where it sat beside that capability's own
15
+ source. See `adr/ADR-FIA-0001-*.md` and README.md's "Where this came from".
16
+
17
+ ## Commands
18
+
19
+ ```bash
20
+ uv run --extra dev pytest -q # full suite (what CI runs)
21
+ uv run --extra dev pytest -q tests/test_config.py # one file
22
+ uv run --extra dev pytest -q tests/test_config.py::test_name # one test
23
+ uv build # sdist/wheel (hatchling)
24
+ uv run foundry-implementation-actor lint <folder> # validate a sidecar
25
+ uv run foundry-implementation-actor show <folder> --registry r # every derived rendering
26
+ ```
27
+
28
+ There is no separate lint/format command configured in this repo.
29
+
30
+ ## Architecture
31
+
32
+ Six modules under `src/foundry_implementation_actor/`:
33
+
34
+ - **`config.py`** — `CapabilityConfig`. The heart. Loads the sidecar and derives **every**
35
+ rendering of the capability id from two declared fields (`capability`, `source_repo`). Also
36
+ carries `lint()` and the `Report` it returns.
37
+ - **`grounding.py`** — runs each `ground_in` entry's `fetch:`, writes the envelope **inside the
38
+ clone** as Markdown, and renders the `CLAUDE.md` that makes the eager ones enter the session's
39
+ window before turn one.
40
+ - **`engine.py`** — `ClaudeCodeEngine`. Satisfies the `Engine` port from
41
+ `papeete-actor-synchronous-messaging`. Clones, branches, grounds, runs `claude --print`, streams
42
+ and projects every turn to the log. Never commits.
43
+ - **`handler.py`** — `make_implement_task(config)`. Containment, commit, push, publish. Never
44
+ opens a pull request.
45
+ - **`correlation.py`** — the two ids and the step vocabulary. Moved **verbatim** from the repo this
46
+ was extracted from; its code below the docstring is byte-identical.
47
+ - **`cli.py`** — argparse wiring only, no logic of its own.
48
+
49
+ ## Core invariants that any change must preserve
50
+
51
+ - **No capability literal, ever.** `tests/test_portability.py` greps `src/` for the originating
52
+ instance's identifiers *and* for any knowledge tool name (`kpack`, `kontract`, …), and fails on
53
+ either. The tools a capability grounds itself in are the consumer's dependencies. A fourth
54
+ knowledge source is a `ground_in` entry, never a change here.
55
+ - **The image ref is a three-way contract.** `<registry>/<capability_path>/<component>:<version>`
56
+ — peer actors recompute the identical string and parse it back apart. It is `config.image_ref`
57
+ output or nothing. **Never invent a tag scheme.**
58
+ - **The write boundary is stated once**, as `components[].path`. It used to be declared in the
59
+ sidecar *and* hardcoded in the enforcing module. Do not reintroduce a second copy.
60
+ - **Component resolution is longest-prefix**, not the path's first segment. The shortcut is correct
61
+ only while every component root is one segment deep.
62
+ - **The observability record schema is the contract; step names are not.** `{event: "step", step,
63
+ phase, duration_ms}`, `{event: "event", step, **fields}`, `{event: "result", cost_usd,
64
+ input_tokens, output_tokens}`, plus `correlation_id`/`task_id` as structured metadata. Rename a
65
+ step freely; do not change a record's shape.
66
+ - **Every emitted log line is budgeted to 64 KB.** Loki rejects an oversized line outright rather
67
+ than truncating it, so one large `Read` would silently delete exactly the turn worth reading.
68
+ - **The clone is full, never `--depth 1`.** `papeete_version.compute()` runs `git describe --tags`
69
+ against it and needs the matching tag's commit reachable.
70
+ - **The generated `CLAUDE.md` appends** to one the repo already commits. Never overwrite.
71
+ - **Never set `ANTHROPIC_API_KEY` in a container running this.** In `claude -p` non-interactive
72
+ mode an API key in the environment is always preferred over `CLAUDE_CODE_OAUTH_TOKEN`, silently
73
+ routing every session through metered billing. There is no warning; the only symptom is the bill.
74
+
75
+ ## Scope discipline
76
+
77
+ Generic across **capabilities**, not across actor *kinds*. No hooks, no base classes, no strategy
78
+ objects for a hypothetical second consumer. If a change would only make sense for an actor that
79
+ does not exist yet, it does not belong here.
80
+
81
+ Design rationale lives in `adr/`. Add a new ADR (copy `adr/template.md`) for any decision of
82
+ similar weight rather than only writing it into code comments.
83
+
84
+ ## Releasing
85
+
86
+ Tag-triggered (`v*`) via `.github/workflows/release.yml`, publishing to PyPI through Trusted
87
+ Publishing (OIDC) — no stored token. The release job builds the wheel, installs it into a throwaway
88
+ venv, and renders a `CLAUDE.md` from a fixture sidecar, asserting its `@`-imports resolve, before
89
+ publishing. `ci.yml` runs the suite plus two gates — *the wheel must carry its contract* and *the
90
+ gate must run* — and reaches nothing outside its own checkout.
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.5
2
+ Name: foundry-implementation-actor
3
+ Version: 0.1.0
4
+ Summary: Runs a headless Claude Code implementation session against one capability's own repo — a papeete-actor for one use, with the capability supplied by a sidecar.
5
+ Project-URL: Homepage, https://github.com/papeete-hub/foundry-implementation-actor
6
+ Author-email: Papeete Consulting <yoann.remy@outlook.com>
7
+ License-Expression: MIT
8
+ Keywords: actor-model,agentic,capability,claude-code,papeete
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Topic :: Software Development :: Code Generators
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: papeete-actor-synchronous-messaging>=0.2.1
15
+ Requires-Dist: papeete-version>=0.1.0
16
+ Requires-Dist: pyyaml>=6.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8.0; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # foundry-implementation-actor
22
+
23
+ Runs a headless [Claude Code](https://claude.com/claude-code) implementation session against one
24
+ capability's own repository, then commits, pushes and publishes what it produced — inside a write
25
+ boundary the actor enforces rather than requests.
26
+
27
+ An **actor, for one use**, with a [`papeete-actor`](https://github.com/papeete-hub/papeete-actor)
28
+ underneath. The `-actor` suffix is that claim; `papeete-actor-*` names, by contrast, are transverse
29
+ features *of* the framework (`ADR-ECO-0022`).
30
+
31
+ ```bash
32
+ pip install foundry-implementation-actor
33
+ ```
34
+
35
+ ## What it is
36
+
37
+ The machinery. It carries **no capability of its own** — the capability it serves arrives in a
38
+ sidecar the consuming repo writes:
39
+
40
+ ```yaml
41
+ # actor-agentic-context.yaml
42
+ context: foundry-implementation-actor/agentic-context/v1
43
+ engine: claude-code
44
+ capability: ACME.PARTS.CAP.SUP.007.WID
45
+ source_repo: acme-lab/ACME.PARTS.CAP.SUP.007.WID-implementation
46
+ registry_repo: acme-lab/acme-governance
47
+ components:
48
+ - {name: backend, path: backend/, tests: backend/tests/, dockerfile: backend/deployment/local}
49
+ - {name: stub, path: stub/, tests: stub/tests/, dockerfile: stub/deployment/local}
50
+ ground_in:
51
+ - name: business
52
+ answers: the WHAT/WHY — domain vision, business events, ubiquitous language
53
+ fetch: [kpack, pack, "{capability}", --deep, --compact, --registry-repo, "{registry_repo}"]
54
+ into: .foundry/business.md
55
+ load: eager
56
+ - name: process
57
+ answers: the HOW — aggregates, commands, policies, read-models, bus, api, JSON schemas
58
+ fetch: [kontract, fetch, "{capability}", --compact, --registry-repo, "{registry_repo}"]
59
+ into: .foundry/process.md
60
+ load: on-demand
61
+ ```
62
+
63
+ and four lines in the consuming entrypoint:
64
+
65
+ ```python
66
+ from foundry_implementation_actor import CapabilityConfig, ClaudeCodeEngine, make_implement_task
67
+
68
+ config = CapabilityConfig.load(".")
69
+ actor = Actor.from_card(".", mailbox=mailbox,
70
+ engines={config.engine: ClaudeCodeEngine(config)},
71
+ actions={"implement-task": make_implement_task(config)})
72
+ ```
73
+
74
+ A second capability instantiates the same actor by writing that file. Nothing here is subclassed,
75
+ hooked, or configured with a strategy object — there is one shape, and it is this one.
76
+
77
+ ## What one request does
78
+
79
+ ```
80
+ clone (full, not --depth 1)
81
+ → checkout impl/TASK-NNN
82
+ → run every ground_in fetch, write it into the clone, render CLAUDE.md
83
+ → claude --print --output-format stream-json --permission-mode acceptEdits
84
+ → git add <each component root>; refuse anything staged outside them
85
+ → commit as the actor, push the branch
86
+ → buildctl build + push one image per touched component
87
+ ```
88
+
89
+ It never opens a pull request. Rendering a verdict and opening one belongs to whichever actor
90
+ orchestrates the pipeline, once its other members have also confirmed.
91
+
92
+ ## Grounding is a precondition, not a request
93
+
94
+ `CLAUDE.md` at the working directory's root is loaded by the `claude` CLI **before the first
95
+ turn**, and its `@relative/path.md` imports resolve eagerly at the same moment. Verified live in
96
+ this actor's exact invocation shape: a question only the fetched context could answer came back in
97
+ `num_turns: 1` with zero tool calls.
98
+
99
+ So the envelopes are written **inside the clone** and named from a generated `CLAUDE.md`. The
100
+ earlier arrangement — fetch into a sibling tempdir, then ask the session in prose to "read it
101
+ before you start" — was not broken, it was *advisory*: whether the context entered the window was
102
+ the model's choice, re-made every session, and a session that skipped it looked exactly like one
103
+ that had read it. An `@`-import resolves relative to the file containing it, so the sibling-tempdir
104
+ arrangement could not have been fixed by writing a better prompt.
105
+
106
+ If the repo commits its own `CLAUDE.md`, the generated block is **appended**, never substituted.
107
+
108
+ ### The two tiers
109
+
110
+ | `load:` | cost | when |
111
+ |---|---|---|
112
+ | `eager` | its full token weight, every session, unconditionally | the session cannot do the work without it |
113
+ | `on-demand` | one line — its `answers:` and its path | useful sometimes; the session opens it if the task needs it |
114
+
115
+ Choose from a measurement of the envelope, not from taste. One such read has already been recorded
116
+ as putting *"40 kB of JSON on screen"*.
117
+
118
+ ## One capability id, eight renderings, zero literals
119
+
120
+ Every identifier is derived from `capability` and `source_repo`. `foundry-implementation-actor
121
+ show` prints the table for a given sidecar:
122
+
123
+ | rendering | from |
124
+ |---|---|
125
+ | `ACME.PARTS.CAP.SUP.007.WID` | `capability` |
126
+ | `acme-lab/ACME.PARTS.CAP.SUP.007.WID-implementation` | `source_repo` |
127
+ | `acme.parts/sup.007.wid` | the id, split at its `CAP` segment |
128
+ | `acme.parts.cap.sup.007.wid-backend` | the image name `papeete-version` versions |
129
+ | `acme-parts-cap-sup-007-wid-implementation-TASK-042-` | the clone's tempdir prefix |
130
+ | `ACME.PARTS.CAP.SUP.007.WID-implementation` | git `user.name` |
131
+ | `acme-parts-cap-sup-007-wid-implementation@users.noreply.github.com` | git `user.email` |
132
+ | `<registry>/acme.parts/sup.007.wid/<component>:<version>` | the published image ref |
133
+
134
+ **The image ref is a three-way contract.** Peer actors recompute the identical string and parse it
135
+ back apart, so it is derivation output or nothing. Never invent a tag scheme.
136
+
137
+ ## The write boundary is stated once
138
+
139
+ `components[].path` — and nothing else. It used to be declared in the sidecar *and* hardcoded in
140
+ the module that actually enforces containment, which is how a boundary comes to be stated twice
141
+ and eventually stated differently.
142
+
143
+ Containment is `git add <each root>`, never `-A` and never a bare `.`, then an assertion that every
144
+ staged path starts with one of them. A staged path outside is **refused**, the index reset, and the
145
+ door answers a refusal. The session's own prompt only asks for the boundary; this is what holds it.
146
+
147
+ Which component a staged path belongs to is resolved by **longest declared prefix**, not by taking
148
+ the path's first segment — that shortcut is correct only while every root is one segment deep, and
149
+ reports the wrong component the day one of them is `src/gateway/`.
150
+
151
+ ## Credentials
152
+
153
+ Two, both passed at run time, never baked into an image:
154
+
155
+ | variable | what for |
156
+ |---|---|
157
+ | `GITHUB_TOKEN` | fine-grained PAT: `contents:write` on `source_repo`, read-only `contents` on whatever the `ground_in` fetches resolve through |
158
+ | `CLAUDE_CODE_OAUTH_TOKEN` | from `claude setup-token` on a machine with a browser, tied to a Pro/Max/Team/Enterprise subscription |
159
+
160
+ > **Do not also set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN`.** In `claude -p`
161
+ > non-interactive mode an API key present in the environment is ALWAYS preferred over
162
+ > `CLAUDE_CODE_OAUTH_TOKEN`, silently routing every session through metered billing. There is no
163
+ > warning and no visible difference in the transcript; the only symptom is the bill.
164
+
165
+ Publishing additionally needs `IMAGE_REGISTRY` and `BUILDKIT_HOST`. There is no Docker daemon and
166
+ no docker socket anywhere in this design — `buildctl` is a client, which is why an actor running
167
+ this can be an ordinary Pod.
168
+
169
+ ## CLI
170
+
171
+ ```bash
172
+ foundry-implementation-actor lint . # validate the sidecar
173
+ foundry-implementation-actor show . --registry reg.example.com # every derived rendering
174
+ ```
175
+
176
+ `lint` is the gate CI runs. A sidecar declaring some other `context:` is read, warned, and not
177
+ checked further — UNMIGRATED is not the same as non-conformant, and migrating is the owning pair's
178
+ own act.
179
+
180
+ ## Observability
181
+
182
+ One JSON object per step, so a dashboard reads them with the same `| json` it uses for the session
183
+ transcript and tells them apart by `event`:
184
+
185
+ ```
186
+ {level, event: "step", step, phase: start|ok|failed, duration_ms, error}
187
+ {level, event: "event", step, **fields}
188
+ {level, event: "result", cost_usd, input_tokens, output_tokens}
189
+ correlation_id, task_id # structured metadata
190
+ ```
191
+
192
+ **That schema is the contract; the step names are not.** Step names may be renamed freely — a
193
+ drill-down discovers them. `correlation_id` is the W3C trace id the caller propagated, not a second
194
+ identifier invented here, so it pastes straight from Loki into Tempo.
195
+
196
+ Every emitted line is budgeted to 64 KB. Loki's `max_line_size` is 256 KB with
197
+ `max_line_size_truncate: false` — an oversized line is **rejected outright**, not trimmed, so one
198
+ large `Read` would silently delete exactly the turn worth reading while leaving the rest intact.
199
+
200
+ ## Where this came from
201
+
202
+ Extracted from one capability's own implementation actor, where 891 lines of this machinery sat
203
+ beside the business capability's source. Both files that moved said so themselves: the engine's
204
+ docstring called extraction *"a near-mechanical move"*, and the sidecar's header said it was kept
205
+ structural *"specifically so extracting this into a real published package later (once proven) is a
206
+ move, not a rewrite."*
207
+
208
+ `adr/` records the decisions. Design rationale belongs there, not in commit messages.
209
+
210
+ ## Development
211
+
212
+ ```bash
213
+ uv run --extra dev pytest -q # what CI runs
214
+ uv build
215
+ ```
216
+
217
+ There is no separate lint/format command configured in this repo.
@@ -0,0 +1,197 @@
1
+ # foundry-implementation-actor
2
+
3
+ Runs a headless [Claude Code](https://claude.com/claude-code) implementation session against one
4
+ capability's own repository, then commits, pushes and publishes what it produced — inside a write
5
+ boundary the actor enforces rather than requests.
6
+
7
+ An **actor, for one use**, with a [`papeete-actor`](https://github.com/papeete-hub/papeete-actor)
8
+ underneath. The `-actor` suffix is that claim; `papeete-actor-*` names, by contrast, are transverse
9
+ features *of* the framework (`ADR-ECO-0022`).
10
+
11
+ ```bash
12
+ pip install foundry-implementation-actor
13
+ ```
14
+
15
+ ## What it is
16
+
17
+ The machinery. It carries **no capability of its own** — the capability it serves arrives in a
18
+ sidecar the consuming repo writes:
19
+
20
+ ```yaml
21
+ # actor-agentic-context.yaml
22
+ context: foundry-implementation-actor/agentic-context/v1
23
+ engine: claude-code
24
+ capability: ACME.PARTS.CAP.SUP.007.WID
25
+ source_repo: acme-lab/ACME.PARTS.CAP.SUP.007.WID-implementation
26
+ registry_repo: acme-lab/acme-governance
27
+ components:
28
+ - {name: backend, path: backend/, tests: backend/tests/, dockerfile: backend/deployment/local}
29
+ - {name: stub, path: stub/, tests: stub/tests/, dockerfile: stub/deployment/local}
30
+ ground_in:
31
+ - name: business
32
+ answers: the WHAT/WHY — domain vision, business events, ubiquitous language
33
+ fetch: [kpack, pack, "{capability}", --deep, --compact, --registry-repo, "{registry_repo}"]
34
+ into: .foundry/business.md
35
+ load: eager
36
+ - name: process
37
+ answers: the HOW — aggregates, commands, policies, read-models, bus, api, JSON schemas
38
+ fetch: [kontract, fetch, "{capability}", --compact, --registry-repo, "{registry_repo}"]
39
+ into: .foundry/process.md
40
+ load: on-demand
41
+ ```
42
+
43
+ and four lines in the consuming entrypoint:
44
+
45
+ ```python
46
+ from foundry_implementation_actor import CapabilityConfig, ClaudeCodeEngine, make_implement_task
47
+
48
+ config = CapabilityConfig.load(".")
49
+ actor = Actor.from_card(".", mailbox=mailbox,
50
+ engines={config.engine: ClaudeCodeEngine(config)},
51
+ actions={"implement-task": make_implement_task(config)})
52
+ ```
53
+
54
+ A second capability instantiates the same actor by writing that file. Nothing here is subclassed,
55
+ hooked, or configured with a strategy object — there is one shape, and it is this one.
56
+
57
+ ## What one request does
58
+
59
+ ```
60
+ clone (full, not --depth 1)
61
+ → checkout impl/TASK-NNN
62
+ → run every ground_in fetch, write it into the clone, render CLAUDE.md
63
+ → claude --print --output-format stream-json --permission-mode acceptEdits
64
+ → git add <each component root>; refuse anything staged outside them
65
+ → commit as the actor, push the branch
66
+ → buildctl build + push one image per touched component
67
+ ```
68
+
69
+ It never opens a pull request. Rendering a verdict and opening one belongs to whichever actor
70
+ orchestrates the pipeline, once its other members have also confirmed.
71
+
72
+ ## Grounding is a precondition, not a request
73
+
74
+ `CLAUDE.md` at the working directory's root is loaded by the `claude` CLI **before the first
75
+ turn**, and its `@relative/path.md` imports resolve eagerly at the same moment. Verified live in
76
+ this actor's exact invocation shape: a question only the fetched context could answer came back in
77
+ `num_turns: 1` with zero tool calls.
78
+
79
+ So the envelopes are written **inside the clone** and named from a generated `CLAUDE.md`. The
80
+ earlier arrangement — fetch into a sibling tempdir, then ask the session in prose to "read it
81
+ before you start" — was not broken, it was *advisory*: whether the context entered the window was
82
+ the model's choice, re-made every session, and a session that skipped it looked exactly like one
83
+ that had read it. An `@`-import resolves relative to the file containing it, so the sibling-tempdir
84
+ arrangement could not have been fixed by writing a better prompt.
85
+
86
+ If the repo commits its own `CLAUDE.md`, the generated block is **appended**, never substituted.
87
+
88
+ ### The two tiers
89
+
90
+ | `load:` | cost | when |
91
+ |---|---|---|
92
+ | `eager` | its full token weight, every session, unconditionally | the session cannot do the work without it |
93
+ | `on-demand` | one line — its `answers:` and its path | useful sometimes; the session opens it if the task needs it |
94
+
95
+ Choose from a measurement of the envelope, not from taste. One such read has already been recorded
96
+ as putting *"40 kB of JSON on screen"*.
97
+
98
+ ## One capability id, eight renderings, zero literals
99
+
100
+ Every identifier is derived from `capability` and `source_repo`. `foundry-implementation-actor
101
+ show` prints the table for a given sidecar:
102
+
103
+ | rendering | from |
104
+ |---|---|
105
+ | `ACME.PARTS.CAP.SUP.007.WID` | `capability` |
106
+ | `acme-lab/ACME.PARTS.CAP.SUP.007.WID-implementation` | `source_repo` |
107
+ | `acme.parts/sup.007.wid` | the id, split at its `CAP` segment |
108
+ | `acme.parts.cap.sup.007.wid-backend` | the image name `papeete-version` versions |
109
+ | `acme-parts-cap-sup-007-wid-implementation-TASK-042-` | the clone's tempdir prefix |
110
+ | `ACME.PARTS.CAP.SUP.007.WID-implementation` | git `user.name` |
111
+ | `acme-parts-cap-sup-007-wid-implementation@users.noreply.github.com` | git `user.email` |
112
+ | `<registry>/acme.parts/sup.007.wid/<component>:<version>` | the published image ref |
113
+
114
+ **The image ref is a three-way contract.** Peer actors recompute the identical string and parse it
115
+ back apart, so it is derivation output or nothing. Never invent a tag scheme.
116
+
117
+ ## The write boundary is stated once
118
+
119
+ `components[].path` — and nothing else. It used to be declared in the sidecar *and* hardcoded in
120
+ the module that actually enforces containment, which is how a boundary comes to be stated twice
121
+ and eventually stated differently.
122
+
123
+ Containment is `git add <each root>`, never `-A` and never a bare `.`, then an assertion that every
124
+ staged path starts with one of them. A staged path outside is **refused**, the index reset, and the
125
+ door answers a refusal. The session's own prompt only asks for the boundary; this is what holds it.
126
+
127
+ Which component a staged path belongs to is resolved by **longest declared prefix**, not by taking
128
+ the path's first segment — that shortcut is correct only while every root is one segment deep, and
129
+ reports the wrong component the day one of them is `src/gateway/`.
130
+
131
+ ## Credentials
132
+
133
+ Two, both passed at run time, never baked into an image:
134
+
135
+ | variable | what for |
136
+ |---|---|
137
+ | `GITHUB_TOKEN` | fine-grained PAT: `contents:write` on `source_repo`, read-only `contents` on whatever the `ground_in` fetches resolve through |
138
+ | `CLAUDE_CODE_OAUTH_TOKEN` | from `claude setup-token` on a machine with a browser, tied to a Pro/Max/Team/Enterprise subscription |
139
+
140
+ > **Do not also set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN`.** In `claude -p`
141
+ > non-interactive mode an API key present in the environment is ALWAYS preferred over
142
+ > `CLAUDE_CODE_OAUTH_TOKEN`, silently routing every session through metered billing. There is no
143
+ > warning and no visible difference in the transcript; the only symptom is the bill.
144
+
145
+ Publishing additionally needs `IMAGE_REGISTRY` and `BUILDKIT_HOST`. There is no Docker daemon and
146
+ no docker socket anywhere in this design — `buildctl` is a client, which is why an actor running
147
+ this can be an ordinary Pod.
148
+
149
+ ## CLI
150
+
151
+ ```bash
152
+ foundry-implementation-actor lint . # validate the sidecar
153
+ foundry-implementation-actor show . --registry reg.example.com # every derived rendering
154
+ ```
155
+
156
+ `lint` is the gate CI runs. A sidecar declaring some other `context:` is read, warned, and not
157
+ checked further — UNMIGRATED is not the same as non-conformant, and migrating is the owning pair's
158
+ own act.
159
+
160
+ ## Observability
161
+
162
+ One JSON object per step, so a dashboard reads them with the same `| json` it uses for the session
163
+ transcript and tells them apart by `event`:
164
+
165
+ ```
166
+ {level, event: "step", step, phase: start|ok|failed, duration_ms, error}
167
+ {level, event: "event", step, **fields}
168
+ {level, event: "result", cost_usd, input_tokens, output_tokens}
169
+ correlation_id, task_id # structured metadata
170
+ ```
171
+
172
+ **That schema is the contract; the step names are not.** Step names may be renamed freely — a
173
+ drill-down discovers them. `correlation_id` is the W3C trace id the caller propagated, not a second
174
+ identifier invented here, so it pastes straight from Loki into Tempo.
175
+
176
+ Every emitted line is budgeted to 64 KB. Loki's `max_line_size` is 256 KB with
177
+ `max_line_size_truncate: false` — an oversized line is **rejected outright**, not trimmed, so one
178
+ large `Read` would silently delete exactly the turn worth reading while leaving the rest intact.
179
+
180
+ ## Where this came from
181
+
182
+ Extracted from one capability's own implementation actor, where 891 lines of this machinery sat
183
+ beside the business capability's source. Both files that moved said so themselves: the engine's
184
+ docstring called extraction *"a near-mechanical move"*, and the sidecar's header said it was kept
185
+ structural *"specifically so extracting this into a real published package later (once proven) is a
186
+ move, not a rewrite."*
187
+
188
+ `adr/` records the decisions. Design rationale belongs there, not in commit messages.
189
+
190
+ ## Development
191
+
192
+ ```bash
193
+ uv run --extra dev pytest -q # what CI runs
194
+ uv build
195
+ ```
196
+
197
+ There is no separate lint/format command configured in this repo.