foundry-implementation-actor 0.2.0__tar.gz → 0.5.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 (55) hide show
  1. foundry_implementation_actor-0.5.0/.github/workflows/ci.yml +132 -0
  2. foundry_implementation_actor-0.5.0/.github/workflows/release.yml +106 -0
  3. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/.gitignore +1 -0
  4. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/CLAUDE.md +9 -1
  5. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/PKG-INFO +109 -14
  6. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/README.md +105 -13
  7. foundry_implementation_actor-0.5.0/adr/ADR-FIA-0002-testing-leaves-this-actors-contract.md +104 -0
  8. foundry_implementation_actor-0.5.0/adr/ADR-FIA-0003-the-dev-test-agreement-has-no-home-yet.md +117 -0
  9. foundry_implementation_actor-0.5.0/adr/ADR-FIA-0004-the-three-amigos-round.md +132 -0
  10. foundry_implementation_actor-0.5.0/adr/ADR-FIA-0005-the-actor-ships-an-image.md +133 -0
  11. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/adr/README.md +4 -0
  12. foundry_implementation_actor-0.5.0/docker/Dockerfile +105 -0
  13. foundry_implementation_actor-0.5.0/examples/ACME.PARTS.CAP.SUP.007.WID-implementation/Dockerfile +39 -0
  14. foundry_implementation_actor-0.5.0/examples/ACME.PARTS.CAP.SUP.007.WID-implementation/actor-agentic-context.yaml +44 -0
  15. foundry_implementation_actor-0.5.0/examples/README.md +252 -0
  16. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/pyproject.toml +9 -1
  17. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/__init__.py +14 -2
  18. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/cards/actor-data.yaml +26 -0
  19. foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/cards/actor-message.yaml +32 -0
  20. foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/cards/actor-synchronous-messaging.yaml +56 -0
  21. foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/cli.py +174 -0
  22. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/config.py +18 -7
  23. foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/conformance.py +133 -0
  24. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/engine.py +256 -17
  25. foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/instance.py +129 -0
  26. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/schemas/agentic-context.schema.yaml +16 -9
  27. foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/serve.py +129 -0
  28. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/tests/conftest.py +2 -4
  29. foundry_implementation_actor-0.5.0/tests/test_cards.py +96 -0
  30. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/tests/test_config.py +18 -4
  31. foundry_implementation_actor-0.5.0/tests/test_conformance.py +164 -0
  32. foundry_implementation_actor-0.5.0/tests/test_engine.py +344 -0
  33. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/tests/test_handler.py +1 -2
  34. foundry_implementation_actor-0.5.0/tests/test_instance.py +136 -0
  35. foundry_implementation_actor-0.5.0/uv.lock +396 -0
  36. foundry_implementation_actor-0.2.0/.github/workflows/ci.yml +0 -58
  37. foundry_implementation_actor-0.2.0/.github/workflows/release.yml +0 -49
  38. foundry_implementation_actor-0.2.0/src/foundry_implementation_actor/cards/actor-message.yaml +0 -17
  39. foundry_implementation_actor-0.2.0/src/foundry_implementation_actor/cards/actor-synchronous-messaging.yaml +0 -21
  40. foundry_implementation_actor-0.2.0/src/foundry_implementation_actor/cli.py +0 -107
  41. foundry_implementation_actor-0.2.0/tests/fixtures/valid/actor-agentic-context.yaml +0 -28
  42. foundry_implementation_actor-0.2.0/tests/test_cards.py +0 -49
  43. foundry_implementation_actor-0.2.0/tests/test_engine.py +0 -144
  44. foundry_implementation_actor-0.2.0/uv.lock +0 -191
  45. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/adr/ADR-FIA-0001-the-machinery-leaves-the-capability.md +0 -0
  46. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/adr/template.md +0 -0
  47. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/scripts/probe_grounding.py +0 -0
  48. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/cards/actor.yaml +0 -0
  49. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/correlation.py +0 -0
  50. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/grounding.py +0 -0
  51. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/handler.py +0 -0
  52. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/tests/fixtures/broken/actor-agentic-context.yaml +0 -0
  53. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/tests/test_cli.py +0 -0
  54. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/tests/test_grounding.py +0 -0
  55. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/tests/test_portability.py +0 -0
@@ -0,0 +1,132 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+
4
+ # NO CREDENTIAL. Nothing here authenticates to anything: no lab repo, no knowledge registry, no
5
+ # PyPI upload, no image push. Everything this pipeline asserts about THIS package, it asserts from
6
+ # source and from artifacts it built itself — the wheel it just built, and an image built from
7
+ # that wheel rather than from a published one.
8
+ #
9
+ # It does reach the network for inputs: public base images and the npm package the engine drives.
10
+ # That is fetching, not authenticating, and the distinction is the one that matters here — no
11
+ # secret is available to this workflow, so nothing it runs can reach a real capability's repo.
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v5
19
+
20
+ - name: the test suite
21
+ run: uv run --extra dev pytest -q
22
+
23
+ - name: build
24
+ run: uv build
25
+
26
+ # ── the two gates a contract-shipping package runs against its own artifact ────────────
27
+ #
28
+ # The suite above proves the code works in a source checkout. Neither of these does — they
29
+ # prove the WHEEL works, which is a different claim and the one a consumer actually depends
30
+ # on. A schema left out of the build passes every test in tests/ and fails here.
31
+
32
+ - name: the wheel must carry its contract
33
+ run: |
34
+ uv venv /tmp/probe
35
+ uv pip install --python /tmp/probe/bin/python -q dist/*.whl
36
+ /tmp/probe/bin/foundry-implementation-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-implementation
37
+ /tmp/probe/bin/foundry-implementation-actor show examples/ACME.PARTS.CAP.SUP.007.WID-implementation \
38
+ --registry reg.example.com | tee /tmp/show.txt
39
+ grep -q 'reg.example.com/acme.parts/sup.007.wid/backend:<version>' /tmp/show.txt
40
+
41
+ - name: the wheel must carry the actor's own cards
42
+ # The `-actor` suffix asserts a papeete-actor underneath, which lint-card can check
43
+ # (ADR-ECO-0022). The suite proves the cards are conformant in a source checkout; this
44
+ # proves they SHIPPED. A card folder left out of the build passes every test in tests/ and
45
+ # makes the name a claim the artifact cannot honour. `papeete-actor-synchronous-messaging`
46
+ # is already a runtime dependency, so its gate is in the probe venv with nothing to add.
47
+ run: |
48
+ /tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
49
+ "$(/tmp/probe/bin/python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
50
+
51
+ - name: the gate must run
52
+ # A gate that cannot fail is not a gate. Lint a deliberately non-conformant sidecar with
53
+ # the INSTALLED wheel and require a non-zero exit.
54
+ run: |
55
+ if /tmp/probe/bin/foundry-implementation-actor lint tests/fixtures/broken; then
56
+ echo "::error::lint accepted a non-conformant sidecar — the gate is not running"
57
+ exit 1
58
+ fi
59
+ echo "the gate refused a non-conformant sidecar, as it should"
60
+
61
+ - name: grounding renders a CLAUDE.md whose imports resolve
62
+ # The property the whole design rests on: an @-import naming a file that is not there is a
63
+ # session grounded in nothing, and it looks identical to one grounded correctly.
64
+ run: /tmp/probe/bin/python scripts/probe_grounding.py examples/ACME.PARTS.CAP.SUP.007.WID-implementation
65
+
66
+ # ── the other half of what this package ships ─────────────────────────────────────────────────
67
+ #
68
+ # ADR-FIA-0005: a use is a sidecar, and the image is what turns one into a running actor. The
69
+ # job above proves the WHEEL works. This one proves the claim a consumer actually depends on —
70
+ # that a folder containing one YAML file builds into an actor that boots and answers its doors.
71
+ # Nothing here reaches a registry: the image is built, run and thrown away in the runner.
72
+ image:
73
+ runs-on: ubuntu-latest
74
+ steps:
75
+ - uses: actions/checkout@v4
76
+ - uses: astral-sh/setup-uv@v5
77
+
78
+ - name: build the wheel this image will carry
79
+ # The image installs the wheel from dist/ rather than from PyPI, so that an image tagged
80
+ # <version> cannot contain some other build of <version> — see docker/Dockerfile.
81
+ run: uv build
82
+
83
+ - name: the base image
84
+ run: docker build -f docker/Dockerfile -t foundry-implementation-actor:ci .
85
+
86
+ - name: the image and the wheel are one release
87
+ # Two artifacts, one version. An image whose wheel says something else is the failure this
88
+ # arrangement creates, so it is the one CI asserts.
89
+ run: |
90
+ wheel="$(ls dist/*.whl | sed -E 's/.*-([0-9][^-]*)-py3.*/\1/')"
91
+ in_image="$(docker run --rm --entrypoint python foundry-implementation-actor:ci \
92
+ -c 'from foundry_implementation_actor import version; print(version())')"
93
+ echo "wheel=$wheel image=$in_image"
94
+ [ "$wheel" = "$in_image" ] || { echo "::error::image carries $in_image, wheel is $wheel"; exit 1; }
95
+
96
+ - name: a use is a sidecar
97
+ # The example folder holds exactly one file besides its Dockerfile. If this builds, the
98
+ # claim in the README is true of a real folder rather than of prose.
99
+ run: |
100
+ test "$(ls examples/ACME.PARTS.CAP.SUP.007.WID-implementation | grep -cv '^Dockerfile$')" = "1"
101
+ docker build --build-arg ACTOR_IMAGE=foundry-implementation-actor:ci \
102
+ -t use:ci examples/ACME.PARTS.CAP.SUP.007.WID-implementation
103
+
104
+ - name: the rendered cards are what a caller is validated against
105
+ # Rendering is the construction that replaces the hand copy conformance.check was built to
106
+ # police. Assert it inside the image, against the cards the image actually runs.
107
+ run: |
108
+ docker run --rm use:ci foundry-implementation-actor lint /actor
109
+
110
+ - name: it boots, and answers the doors its cards declare
111
+ # The credentials are deliberately junk. The engine refuses to construct without a
112
+ # GITHUB_TOKEN — which is itself worth having, and is why they are set rather than omitted
113
+ # — and no door is actually exercised here: a 400 means routed and refused the payload, a
114
+ # 404 would mean the route was never built.
115
+ run: |
116
+ docker run -d --name smoke -p 18080:8080 \
117
+ -e GITHUB_TOKEN=ci -e CLAUDE_CODE_OAUTH_TOKEN=ci use:ci
118
+ for _ in $(seq 1 30); do
119
+ curl -fsS -m 2 http://localhost:18080/health >/dev/null 2>&1 && break
120
+ sleep 1
121
+ done
122
+ curl -fsS -m 5 http://localhost:18080/health | grep -q ok
123
+ for door in implement-task assess-task; do
124
+ code="$(curl -sS -o /dev/null -w '%{http_code}' -m 5 -X POST \
125
+ -H 'content-type: application/json' -d '{}' "http://localhost:18080/$door")"
126
+ [ "$code" = "400" ] || { echo "::error::POST /$door answered $code, expected 400 (routed, bad payload)"; exit 1; }
127
+ done
128
+ code="$(curl -sS -o /dev/null -w '%{http_code}' -m 5 -X POST \
129
+ -H 'content-type: application/json' -d '{}' http://localhost:18080/no-such-door)"
130
+ [ "$code" = "404" ] || { echo "::error::an undeclared door answered $code, expected 404"; exit 1; }
131
+ docker logs smoke | grep -q actor-started
132
+ docker rm -f smoke
@@ -0,0 +1,106 @@
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 # PyPI Trusted Publishing
11
+ contents: read
12
+ packages: write # the image, to this repo's own container registry
13
+
14
+ jobs:
15
+ publish:
16
+ runs-on: ubuntu-latest
17
+ environment: pypi
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: astral-sh/setup-uv@v5
21
+
22
+ - name: build
23
+ # No fetch step and no external token: the contract is committed here. A release depends
24
+ # on nothing but this checkout and PyPI.
25
+ run: uv build
26
+
27
+ - name: the wheel must carry its contract
28
+ run: |
29
+ uv venv /tmp/probe
30
+ uv pip install --python /tmp/probe/bin/python -q dist/*.whl
31
+ /tmp/probe/bin/foundry-implementation-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-implementation
32
+
33
+ - name: the wheel must carry the actor's own cards
34
+ # Same reason as the line above, for the other half of what this package ships. The
35
+ # `-actor` suffix asserts a papeete-actor underneath (ADR-ECO-0022); a wheel published
36
+ # without its cards makes the name a claim the artifact cannot honour, and a published
37
+ # version is not something to discover that from.
38
+ run: |
39
+ /tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
40
+ "$(/tmp/probe/bin/python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
41
+
42
+ - name: grounding renders a CLAUDE.md whose imports resolve
43
+ # BEFORE the upload, not after. A wheel that ships without its schema, or whose grounding
44
+ # emits an @-import to a file it never wrote, produces sessions grounded in nothing that
45
+ # look identical to correct ones. That is not something to discover from a published
46
+ # version.
47
+ run: /tmp/probe/bin/python scripts/probe_grounding.py examples/ACME.PARTS.CAP.SUP.007.WID-implementation
48
+
49
+ - name: publish to PyPI
50
+ run: uv publish --trusted-publishing always
51
+
52
+ # ── the image, from the same tag and the same checkout ────────────────────────────────────────
53
+ #
54
+ # TWO ARTIFACTS, ONE RELEASE (ADR-FIA-0005). A use pins an image; an embedder pins a wheel; both
55
+ # are this package at this version. A tag that produced one and not the other leaves a consumer
56
+ # unable to take the release at all — so this job builds the wheel again from the same checkout
57
+ # rather than installing the one just uploaded: the image must not depend on PyPI having already
58
+ # indexed it, and must not be able to pick up a different build of the same version.
59
+ #
60
+ # It runs AFTER the wheel is published, not beside it. A failed upload should not leave an image
61
+ # published for a version that does not exist on PyPI; the reverse — a wheel with the image still
62
+ # to come — is recoverable by re-running this job alone.
63
+ image:
64
+ needs: publish
65
+ runs-on: ubuntu-latest
66
+ steps:
67
+ - uses: actions/checkout@v4
68
+ - uses: astral-sh/setup-uv@v5
69
+
70
+ - name: the version is the tag
71
+ # The tag is what a consumer writes in a FROM line. A tag that disagrees with
72
+ # pyproject.toml would publish an image whose own `version()` contradicts its name, which
73
+ # is exactly the confusion the CI gate above exists to prevent for the wheel.
74
+ id: version
75
+ run: |
76
+ tag="${GITHUB_REF_NAME#v}"
77
+ declared="$(uv run --quiet python -c 'import tomllib,pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
78
+ [ "$tag" = "$declared" ] || { echo "::error::tag $GITHUB_REF_NAME does not match pyproject version $declared"; exit 1; }
79
+ echo "version=$tag" >> "$GITHUB_OUTPUT"
80
+
81
+ - name: build
82
+ run: uv build
83
+
84
+ - uses: docker/login-action@v3
85
+ with:
86
+ registry: ghcr.io
87
+ username: ${{ github.actor }}
88
+ password: ${{ secrets.GITHUB_TOKEN }}
89
+
90
+ - name: the image, tagged and latest
91
+ run: |
92
+ image="ghcr.io/${{ github.repository }}"
93
+ docker build -f docker/Dockerfile \
94
+ -t "$image:${{ steps.version.outputs.version }}" \
95
+ -t "$image:latest" .
96
+ docker push "$image:${{ steps.version.outputs.version }}"
97
+ docker push "$image:latest"
98
+
99
+ - name: a use built on it is still an actor
100
+ # BEFORE the image is something anyone can pin — the same reason the grounding probe runs
101
+ # before the upload above. An image that builds but produces a use that does not conform is
102
+ # not something to discover from a published tag.
103
+ run: |
104
+ docker build --build-arg ACTOR_IMAGE=ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }} \
105
+ -t use:release examples/ACME.PARTS.CAP.SUP.007.WID-implementation
106
+ docker run --rm use:release foundry-implementation-actor lint /actor
@@ -3,3 +3,4 @@ dist/
3
3
  __pycache__/
4
4
  .venv/
5
5
  .pytest_cache/
6
+ .serena/
@@ -29,7 +29,7 @@ There is no separate lint/format command configured in this repo.
29
29
 
30
30
  ## Architecture
31
31
 
32
- Six modules under `src/foundry_implementation_actor/`:
32
+ Seven modules under `src/foundry_implementation_actor/`:
33
33
 
34
34
  - **`config.py`** — `CapabilityConfig`. The heart. Loads the sidecar and derives **every**
35
35
  rendering of the capability id from two declared fields (`capability`, `source_repo`). Also
@@ -44,6 +44,10 @@ Six modules under `src/foundry_implementation_actor/`:
44
44
  opens a pull request.
45
45
  - **`correlation.py`** — the two ids and the step vocabulary. Moved **verbatim** from the repo this
46
46
  was extracted from; its code below the docstring is byte-identical.
47
+ - **`conformance.py`** — `check(folder)`. Compares a use's cards against the definition's, on
48
+ the derived wire contract only (door ids, each door's request/completion schema and engine).
49
+ Prose and `actor.yaml`'s `name:` are deliberately not compared — a use should name its own
50
+ capability. `lint` runs it beside the sidecar gate.
47
51
  - **`cli.py`** — argparse wiring only, no logic of its own.
48
52
 
49
53
  Beside them, two folders of committed contract, both shipped in the wheel:
@@ -56,6 +60,10 @@ Beside them, two folders of committed contract, both shipped in the wheel:
56
60
 
57
61
  ## Core invariants that any change must preserve
58
62
 
63
+ - **A use's cards are a hand copy, and hand copies drift.** Both folders pass `lint-card`
64
+ independently, and neither gate looks at the other — so a definition that gains a field leaves
65
+ every un-updated use linting green and refusing callers at runtime. `conformance.check` is the
66
+ only thing that catches it. Compare the DERIVED contract, never the prose.
59
67
  - **The `-actor` suffix is a claim, and it is checked.** `ADR-ECO-0022`: a package ending in
60
68
  `-actor` asserts a `papeete-actor` underneath, and one that ships no conformant card is
61
69
  misnamed. `tests/test_cards.py` runs `lint-card` on `cards/` in the suite; CI and the release
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: foundry-implementation-actor
3
- Version: 0.2.0
3
+ Version: 0.5.0
4
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
5
  Project-URL: Homepage, https://github.com/papeete-hub/foundry-implementation-actor
6
6
  Author-email: Papeete Consulting <yoann.remy@outlook.com>
@@ -16,6 +16,9 @@ Requires-Dist: papeete-version>=0.1.0
16
16
  Requires-Dist: pyyaml>=6.0
17
17
  Provides-Extra: dev
18
18
  Requires-Dist: pytest>=8.0; extra == 'dev'
19
+ Provides-Extra: serve
20
+ Requires-Dist: papeete-actor-synchronous-messaging-http>=0.4.0; extra == 'serve'
21
+ Requires-Dist: papeete-observability>=0.1.0; extra == 'serve'
19
22
  Description-Content-Type: text/markdown
20
23
 
21
24
  # foundry-implementation-actor
@@ -32,6 +35,10 @@ features *of* the framework (`ADR-ECO-0022`).
32
35
  pip install foundry-implementation-actor
33
36
  ```
34
37
 
38
+ > **New here?** [`examples/`](examples/) walks through a complete, working use of this actor —
39
+ > what the workflow is, what it is for, and how to instantiate one for your own capability. Every
40
+ > command in it runs with no credentials and no network.
41
+
35
42
  ## What it is
36
43
 
37
44
  The actor's **definition** — its four cards, and the machinery behind them. It carries **no
@@ -45,8 +52,8 @@ capability: ACME.PARTS.CAP.SUP.007.WID
45
52
  source_repo: acme-lab/ACME.PARTS.CAP.SUP.007.WID-implementation
46
53
  registry_repo: acme-lab/acme-governance
47
54
  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}
55
+ - {name: backend, path: backend/, dockerfile: backend/deployment/local}
56
+ - {name: stub, path: stub/, dockerfile: stub/deployment/local}
50
57
  ground_in:
51
58
  - name: business
52
59
  answers: the WHAT/WHY — domain vision, business events, ubiquitous language
@@ -60,7 +67,27 @@ ground_in:
60
67
  load: on-demand
61
68
  ```
62
69
 
63
- and four lines in the consuming entrypoint:
70
+ and, beside it, a four-line Dockerfile:
71
+
72
+ ```dockerfile
73
+ FROM ghcr.io/papeete-hub/foundry-implementation-actor:0.5.0
74
+ RUN pip install --no-cache-dir kpack==2.0.1 kontract==0.1.0 # what this sidecar's ground_in names
75
+ COPY actor-agentic-context.yaml /actor/
76
+ RUN foundry-implementation-actor render-cards /actor && foundry-implementation-actor lint /actor
77
+ ```
78
+
79
+ **That is the whole repository.** No cards, no entrypoint, no Python. The cards are rendered from
80
+ the definition in the image, the entrypoint is `foundry-implementation-actor serve`, and the only
81
+ hand-written line naming anything is the `pip install` of the knowledge tools this capability's own
82
+ `ground_in:` argv happens to name — which is the consumer's fact by design (ADR-FIA-0005).
83
+
84
+ A second capability instantiates the same actor by writing that sidecar. Nothing here is
85
+ subclassed, hooked, or configured with a strategy object — there is one shape, and it is this one.
86
+
87
+ ### Embedding it instead
88
+
89
+ A consumer that wants its own base image, or the actor inside a larger process, skips all of the
90
+ above and wires it in four lines:
64
91
 
65
92
  ```python
66
93
  from foundry_implementation_actor import CapabilityConfig, ClaudeCodeEngine, make_implement_task
@@ -71,8 +98,10 @@ actor = Actor.from_card(".", mailbox=mailbox,
71
98
  actions={"implement-task": make_implement_task(config)})
72
99
  ```
73
100
 
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.
101
+ `assess-task` needs no entry here: it is a query with an engine and no handler, so the engine's own
102
+ judgement is the reply. `pip install foundry-implementation-actor` is enough for this; the mailbox
103
+ and the observability backend that `serve` needs live in the `[serve]` extra, so an embedder is not
104
+ handed an HTTP server it did not ask for.
76
105
 
77
106
  ## The definition, and a use
78
107
 
@@ -81,11 +110,14 @@ four cards — who it is, the data it knows, the messages it exchanges, and the
81
110
  door it answers — and they ship in the wheel, reachable as `cards_path()`. They name no capability,
82
111
  because which capability an instance serves is not part of what the actor *is*.
83
112
 
84
- A **use** of this actor is one capability's own folder: its own copy of those four cards, named for
85
- the capability it serves, beside the `actor-agentic-context.yaml` that binds it to that capability's
86
- repository and knowledge base. Today that folder is a static repository, and the copy is made by
87
- hand. Once an instance can be spawned from a capability id alone, the cards here are what it would
88
- be rendered from — which is why they live in the wheel rather than in an `examples/` folder.
113
+ A **use** of this actor is one capability's own folder: the `actor-agentic-context.yaml` that binds
114
+ it to that capability's repository and knowledge base, and the four cards named for the capability
115
+ it serves — **rendered from the definition, not copied.** `foundry-implementation-actor
116
+ render-cards` writes them, the image runs it at `docker build` time, and a use's repository
117
+ therefore holds one hand-written YAML file. That is why the cards live in the wheel rather than in
118
+ an `examples/` folder: they are build input, not documentation (ADR-FIA-0005).
119
+
120
+ They used to be copied by hand, which is why the gate below exists.
89
121
 
90
122
  The split is what the name asserts. `ADR-ECO-0022` makes the `-actor` suffix an obligation: a
91
123
  package ending in `-actor` claims a `papeete-actor` underneath, *"and a `<use>-<tier>-actor` that
@@ -97,11 +129,35 @@ papeete-actor-synchronous-messaging lint-card \
97
129
  "$(python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
98
130
  ```
99
131
 
132
+ A hand copy drifts: both folders pass `lint-card` independently, and neither gate has an opinion
133
+ about the other. `foundry-implementation-actor lint` therefore runs a second check —
134
+ `conformance.check` — comparing the use's cards against the definition's on the **derived wire
135
+ contract**: the set of doors, and each door's `request_schema`, `completion_schema` and `engine`.
136
+ Those derivations already fold in the data dictionary and the message catalog, so a renamed item or
137
+ a changed reference lands in the payload a caller is validated against.
138
+
139
+ Rendering makes that gate cheap to pass rather than redundant, and both are worth having: a use
140
+ still carrying a hand copy from before 0.5.0, or one pinned to an older image, is exactly the case
141
+ that can be wrong — and a construction that cannot drift is still better than a check that catches
142
+ drift, which is why the rendering exists at all.
143
+
144
+ Prose is not compared, on purpose: a use *should* name its real capability and its real peers, and
145
+ `actor.yaml`'s `name:` is its own identity and is required to differ.
146
+
100
147
  Because the cards sit under `src/`, `tests/test_portability.py` greps them too — a capability id or
101
148
  a knowledge tool name written into the actor's own definition fails the build exactly as it would
102
149
  in the code.
103
150
 
104
- ## What one request does
151
+ ## Two doors
152
+
153
+ | door | verb | what it does |
154
+ |---|---|---|
155
+ | `implement-task` | request | builds the increment, commits, pushes a branch, publishes one image per touched component |
156
+ | `assess-task` | query | answers whether a proposed acceptance surface can be delivered. Writes nothing |
157
+
158
+ Both name the same engine. `Actor.judge()` hands it the door id, and it dispatches on that.
159
+
160
+ ### `implement-task`
105
161
 
106
162
  ```
107
163
  clone (full, not --depth 1)
@@ -116,6 +172,42 @@ clone (full, not --depth 1)
116
172
  It never opens a pull request. Rendering a verdict and opening one belongs to whichever actor
117
173
  orchestrates the pipeline, once its other members have also confirmed.
118
174
 
175
+ ## The three amigos round
176
+
177
+ Before any of that, the actor that will black-box test the increment says what it intends to
178
+ assert, and this one answers whether that can be delivered:
179
+
180
+ ```
181
+ orchestration ──▶ testing: propose the acceptance surface for this task
182
+ ◀── expectations[] — each with a stable id, a statement, a handle
183
+ orchestration ──▶ implementation: assess-task — can you deliver this?
184
+ ◀── feasible + objections[] + commitments[]
185
+
186
+ agreed → the surface goes to implement-task AND test-task, on every attempt
187
+ disagreed → orchestration stops and hands the objections back to a human
188
+ ```
189
+
190
+ **Exactly one round, then a person.** No counter-proposal and no negotiation loop: a three-amigos
191
+ meeting converges because a human is in the room, and here the human *is* the tie-breaker. *"The
192
+ task does not determine this"* is a legitimate answer, and it is the most useful one.
193
+
194
+ `assess-task` is a **query**, not an action — *"a promise to answer, from this actor's own state
195
+ and nothing invented."* It clones read-only, grounds itself exactly as the implement door does, and
196
+ is invoked with `Read,Glob,Grep` and no `Write`, `Edit` or `Bash`. A door that **cannot** write
197
+ beats a door asked not to. It registers no handler either: with an engine and no handler,
198
+ `Actor.receive()` returns the judged dict as the reply, and there is nothing to contain because
199
+ nothing is produced.
200
+
201
+ What it commits to is a **promise, not a report** — nothing has been built when it answers. That
202
+ distinction is the whole point (`ADR-FIA-0004`): a tester deriving its assertions from what was
203
+ already built can only ever confirm the build. The failure this prevents is real and already in the
204
+ wild — an e2e suite carrying three fixture ids its own comment admits were *"discovered black-box
205
+ against the running container"*, against a task card that never named them.
206
+
207
+ `acceptance_surface` then rides along as an **optional** field on `implement-task`. Without it the
208
+ door behaves exactly as before; with it, where it is more specific than the definition of done, it
209
+ wins.
210
+
119
211
  ## Grounding is a precondition, not a request
120
212
 
121
213
  `CLAUDE.md` at the working directory's root is loaded by the `claude` CLI **before the first
@@ -196,11 +288,14 @@ this can be an ordinary Pod.
196
288
  ## CLI
197
289
 
198
290
  ```bash
199
- foundry-implementation-actor lint . # validate the sidecar
291
+ foundry-implementation-actor lint . # validate the sidecar and the cards
200
292
  foundry-implementation-actor show . --registry reg.example.com # every derived rendering
293
+ foundry-implementation-actor render-cards . # write the four cards from the wheel
294
+ foundry-implementation-actor serve . # boot it (needs the `serve` extra)
201
295
  ```
202
296
 
203
- `lint` is the gate CI runs. A sidecar declaring some other `context:` is read, warned, and not
297
+ `render-cards` and `serve` are what the image runs, and are usable anywhere the wheel is. `lint` is
298
+ the gate CI runs. A sidecar declaring some other `context:` is read, warned, and not
204
299
  checked further — UNMIGRATED is not the same as non-conformant, and migrating is the owning pair's
205
300
  own act.
206
301
 
@@ -12,6 +12,10 @@ features *of* the framework (`ADR-ECO-0022`).
12
12
  pip install foundry-implementation-actor
13
13
  ```
14
14
 
15
+ > **New here?** [`examples/`](examples/) walks through a complete, working use of this actor —
16
+ > what the workflow is, what it is for, and how to instantiate one for your own capability. Every
17
+ > command in it runs with no credentials and no network.
18
+
15
19
  ## What it is
16
20
 
17
21
  The actor's **definition** — its four cards, and the machinery behind them. It carries **no
@@ -25,8 +29,8 @@ capability: ACME.PARTS.CAP.SUP.007.WID
25
29
  source_repo: acme-lab/ACME.PARTS.CAP.SUP.007.WID-implementation
26
30
  registry_repo: acme-lab/acme-governance
27
31
  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}
32
+ - {name: backend, path: backend/, dockerfile: backend/deployment/local}
33
+ - {name: stub, path: stub/, dockerfile: stub/deployment/local}
30
34
  ground_in:
31
35
  - name: business
32
36
  answers: the WHAT/WHY — domain vision, business events, ubiquitous language
@@ -40,7 +44,27 @@ ground_in:
40
44
  load: on-demand
41
45
  ```
42
46
 
43
- and four lines in the consuming entrypoint:
47
+ and, beside it, a four-line Dockerfile:
48
+
49
+ ```dockerfile
50
+ FROM ghcr.io/papeete-hub/foundry-implementation-actor:0.5.0
51
+ RUN pip install --no-cache-dir kpack==2.0.1 kontract==0.1.0 # what this sidecar's ground_in names
52
+ COPY actor-agentic-context.yaml /actor/
53
+ RUN foundry-implementation-actor render-cards /actor && foundry-implementation-actor lint /actor
54
+ ```
55
+
56
+ **That is the whole repository.** No cards, no entrypoint, no Python. The cards are rendered from
57
+ the definition in the image, the entrypoint is `foundry-implementation-actor serve`, and the only
58
+ hand-written line naming anything is the `pip install` of the knowledge tools this capability's own
59
+ `ground_in:` argv happens to name — which is the consumer's fact by design (ADR-FIA-0005).
60
+
61
+ A second capability instantiates the same actor by writing that sidecar. Nothing here is
62
+ subclassed, hooked, or configured with a strategy object — there is one shape, and it is this one.
63
+
64
+ ### Embedding it instead
65
+
66
+ A consumer that wants its own base image, or the actor inside a larger process, skips all of the
67
+ above and wires it in four lines:
44
68
 
45
69
  ```python
46
70
  from foundry_implementation_actor import CapabilityConfig, ClaudeCodeEngine, make_implement_task
@@ -51,8 +75,10 @@ actor = Actor.from_card(".", mailbox=mailbox,
51
75
  actions={"implement-task": make_implement_task(config)})
52
76
  ```
53
77
 
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.
78
+ `assess-task` needs no entry here: it is a query with an engine and no handler, so the engine's own
79
+ judgement is the reply. `pip install foundry-implementation-actor` is enough for this; the mailbox
80
+ and the observability backend that `serve` needs live in the `[serve]` extra, so an embedder is not
81
+ handed an HTTP server it did not ask for.
56
82
 
57
83
  ## The definition, and a use
58
84
 
@@ -61,11 +87,14 @@ four cards — who it is, the data it knows, the messages it exchanges, and the
61
87
  door it answers — and they ship in the wheel, reachable as `cards_path()`. They name no capability,
62
88
  because which capability an instance serves is not part of what the actor *is*.
63
89
 
64
- A **use** of this actor is one capability's own folder: its own copy of those four cards, named for
65
- the capability it serves, beside the `actor-agentic-context.yaml` that binds it to that capability's
66
- repository and knowledge base. Today that folder is a static repository, and the copy is made by
67
- hand. Once an instance can be spawned from a capability id alone, the cards here are what it would
68
- be rendered from — which is why they live in the wheel rather than in an `examples/` folder.
90
+ A **use** of this actor is one capability's own folder: the `actor-agentic-context.yaml` that binds
91
+ it to that capability's repository and knowledge base, and the four cards named for the capability
92
+ it serves — **rendered from the definition, not copied.** `foundry-implementation-actor
93
+ render-cards` writes them, the image runs it at `docker build` time, and a use's repository
94
+ therefore holds one hand-written YAML file. That is why the cards live in the wheel rather than in
95
+ an `examples/` folder: they are build input, not documentation (ADR-FIA-0005).
96
+
97
+ They used to be copied by hand, which is why the gate below exists.
69
98
 
70
99
  The split is what the name asserts. `ADR-ECO-0022` makes the `-actor` suffix an obligation: a
71
100
  package ending in `-actor` claims a `papeete-actor` underneath, *"and a `<use>-<tier>-actor` that
@@ -77,11 +106,35 @@ papeete-actor-synchronous-messaging lint-card \
77
106
  "$(python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
78
107
  ```
79
108
 
109
+ A hand copy drifts: both folders pass `lint-card` independently, and neither gate has an opinion
110
+ about the other. `foundry-implementation-actor lint` therefore runs a second check —
111
+ `conformance.check` — comparing the use's cards against the definition's on the **derived wire
112
+ contract**: the set of doors, and each door's `request_schema`, `completion_schema` and `engine`.
113
+ Those derivations already fold in the data dictionary and the message catalog, so a renamed item or
114
+ a changed reference lands in the payload a caller is validated against.
115
+
116
+ Rendering makes that gate cheap to pass rather than redundant, and both are worth having: a use
117
+ still carrying a hand copy from before 0.5.0, or one pinned to an older image, is exactly the case
118
+ that can be wrong — and a construction that cannot drift is still better than a check that catches
119
+ drift, which is why the rendering exists at all.
120
+
121
+ Prose is not compared, on purpose: a use *should* name its real capability and its real peers, and
122
+ `actor.yaml`'s `name:` is its own identity and is required to differ.
123
+
80
124
  Because the cards sit under `src/`, `tests/test_portability.py` greps them too — a capability id or
81
125
  a knowledge tool name written into the actor's own definition fails the build exactly as it would
82
126
  in the code.
83
127
 
84
- ## What one request does
128
+ ## Two doors
129
+
130
+ | door | verb | what it does |
131
+ |---|---|---|
132
+ | `implement-task` | request | builds the increment, commits, pushes a branch, publishes one image per touched component |
133
+ | `assess-task` | query | answers whether a proposed acceptance surface can be delivered. Writes nothing |
134
+
135
+ Both name the same engine. `Actor.judge()` hands it the door id, and it dispatches on that.
136
+
137
+ ### `implement-task`
85
138
 
86
139
  ```
87
140
  clone (full, not --depth 1)
@@ -96,6 +149,42 @@ clone (full, not --depth 1)
96
149
  It never opens a pull request. Rendering a verdict and opening one belongs to whichever actor
97
150
  orchestrates the pipeline, once its other members have also confirmed.
98
151
 
152
+ ## The three amigos round
153
+
154
+ Before any of that, the actor that will black-box test the increment says what it intends to
155
+ assert, and this one answers whether that can be delivered:
156
+
157
+ ```
158
+ orchestration ──▶ testing: propose the acceptance surface for this task
159
+ ◀── expectations[] — each with a stable id, a statement, a handle
160
+ orchestration ──▶ implementation: assess-task — can you deliver this?
161
+ ◀── feasible + objections[] + commitments[]
162
+
163
+ agreed → the surface goes to implement-task AND test-task, on every attempt
164
+ disagreed → orchestration stops and hands the objections back to a human
165
+ ```
166
+
167
+ **Exactly one round, then a person.** No counter-proposal and no negotiation loop: a three-amigos
168
+ meeting converges because a human is in the room, and here the human *is* the tie-breaker. *"The
169
+ task does not determine this"* is a legitimate answer, and it is the most useful one.
170
+
171
+ `assess-task` is a **query**, not an action — *"a promise to answer, from this actor's own state
172
+ and nothing invented."* It clones read-only, grounds itself exactly as the implement door does, and
173
+ is invoked with `Read,Glob,Grep` and no `Write`, `Edit` or `Bash`. A door that **cannot** write
174
+ beats a door asked not to. It registers no handler either: with an engine and no handler,
175
+ `Actor.receive()` returns the judged dict as the reply, and there is nothing to contain because
176
+ nothing is produced.
177
+
178
+ What it commits to is a **promise, not a report** — nothing has been built when it answers. That
179
+ distinction is the whole point (`ADR-FIA-0004`): a tester deriving its assertions from what was
180
+ already built can only ever confirm the build. The failure this prevents is real and already in the
181
+ wild — an e2e suite carrying three fixture ids its own comment admits were *"discovered black-box
182
+ against the running container"*, against a task card that never named them.
183
+
184
+ `acceptance_surface` then rides along as an **optional** field on `implement-task`. Without it the
185
+ door behaves exactly as before; with it, where it is more specific than the definition of done, it
186
+ wins.
187
+
99
188
  ## Grounding is a precondition, not a request
100
189
 
101
190
  `CLAUDE.md` at the working directory's root is loaded by the `claude` CLI **before the first
@@ -176,11 +265,14 @@ this can be an ordinary Pod.
176
265
  ## CLI
177
266
 
178
267
  ```bash
179
- foundry-implementation-actor lint . # validate the sidecar
268
+ foundry-implementation-actor lint . # validate the sidecar and the cards
180
269
  foundry-implementation-actor show . --registry reg.example.com # every derived rendering
270
+ foundry-implementation-actor render-cards . # write the four cards from the wheel
271
+ foundry-implementation-actor serve . # boot it (needs the `serve` extra)
181
272
  ```
182
273
 
183
- `lint` is the gate CI runs. A sidecar declaring some other `context:` is read, warned, and not
274
+ `render-cards` and `serve` are what the image runs, and are usable anywhere the wheel is. `lint` is
275
+ the gate CI runs. A sidecar declaring some other `context:` is read, warned, and not
184
276
  checked further — UNMIGRATED is not the same as non-conformant, and migrating is the owning pair's
185
277
  own act.
186
278