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.
- foundry_implementation_actor-0.1.0/.github/workflows/ci.yml +48 -0
- foundry_implementation_actor-0.1.0/.github/workflows/release.yml +40 -0
- foundry_implementation_actor-0.1.0/.gitignore +5 -0
- foundry_implementation_actor-0.1.0/CLAUDE.md +90 -0
- foundry_implementation_actor-0.1.0/PKG-INFO +217 -0
- foundry_implementation_actor-0.1.0/README.md +197 -0
- foundry_implementation_actor-0.1.0/adr/ADR-FIA-0001-the-machinery-leaves-the-capability.md +145 -0
- foundry_implementation_actor-0.1.0/adr/README.md +19 -0
- foundry_implementation_actor-0.1.0/adr/template.md +28 -0
- foundry_implementation_actor-0.1.0/pyproject.toml +43 -0
- foundry_implementation_actor-0.1.0/scripts/probe_grounding.py +67 -0
- foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/__init__.py +38 -0
- foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/cli.py +107 -0
- foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/config.py +405 -0
- foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/correlation.py +193 -0
- foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/engine.py +434 -0
- foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/grounding.py +197 -0
- foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/handler.py +220 -0
- foundry_implementation_actor-0.1.0/src/foundry_implementation_actor/schemas/agentic-context.schema.yaml +143 -0
- foundry_implementation_actor-0.1.0/tests/conftest.py +118 -0
- foundry_implementation_actor-0.1.0/tests/fixtures/broken/actor-agentic-context.yaml +14 -0
- foundry_implementation_actor-0.1.0/tests/fixtures/valid/actor-agentic-context.yaml +28 -0
- foundry_implementation_actor-0.1.0/tests/test_cli.py +53 -0
- foundry_implementation_actor-0.1.0/tests/test_config.py +193 -0
- foundry_implementation_actor-0.1.0/tests/test_engine.py +144 -0
- foundry_implementation_actor-0.1.0/tests/test_grounding.py +155 -0
- foundry_implementation_actor-0.1.0/tests/test_handler.py +114 -0
- foundry_implementation_actor-0.1.0/tests/test_portability.py +68 -0
- 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,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.
|