foundry-implementation-actor 0.2.1__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.
- foundry_implementation_actor-0.5.0/.github/workflows/ci.yml +132 -0
- foundry_implementation_actor-0.5.0/.github/workflows/release.yml +106 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/.gitignore +1 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/PKG-INFO +105 -20
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/README.md +101 -19
- foundry_implementation_actor-0.5.0/adr/ADR-FIA-0002-testing-leaves-this-actors-contract.md +104 -0
- foundry_implementation_actor-0.5.0/adr/ADR-FIA-0003-the-dev-test-agreement-has-no-home-yet.md +117 -0
- foundry_implementation_actor-0.5.0/adr/ADR-FIA-0004-the-three-amigos-round.md +132 -0
- foundry_implementation_actor-0.5.0/adr/ADR-FIA-0005-the-actor-ships-an-image.md +133 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/adr/README.md +4 -0
- foundry_implementation_actor-0.5.0/docker/Dockerfile +105 -0
- foundry_implementation_actor-0.5.0/examples/ACME.PARTS.CAP.SUP.007.WID-implementation/Dockerfile +39 -0
- foundry_implementation_actor-0.5.0/examples/ACME.PARTS.CAP.SUP.007.WID-implementation/actor-agentic-context.yaml +44 -0
- foundry_implementation_actor-0.5.0/examples/README.md +252 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/pyproject.toml +9 -1
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/__init__.py +13 -2
- {foundry_implementation_actor-0.2.1/tests/fixtures/valid → foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/cards}/actor-data.yaml +26 -0
- foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/cards/actor-message.yaml +32 -0
- foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/cards/actor-synchronous-messaging.yaml +56 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/cli.py +63 -6
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/config.py +18 -7
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/conformance.py +28 -14
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/engine.py +256 -17
- foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/instance.py +129 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/schemas/agentic-context.schema.yaml +16 -9
- foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/serve.py +129 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/tests/conftest.py +2 -4
- foundry_implementation_actor-0.5.0/tests/test_cards.py +96 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/tests/test_config.py +18 -4
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/tests/test_conformance.py +61 -8
- foundry_implementation_actor-0.5.0/tests/test_engine.py +344 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/tests/test_handler.py +1 -2
- foundry_implementation_actor-0.5.0/tests/test_instance.py +136 -0
- foundry_implementation_actor-0.5.0/uv.lock +396 -0
- foundry_implementation_actor-0.2.1/.github/workflows/ci.yml +0 -58
- foundry_implementation_actor-0.2.1/.github/workflows/release.yml +0 -49
- foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/cards/actor-data.yaml +0 -38
- foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/cards/actor-message.yaml +0 -17
- foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/cards/actor-synchronous-messaging.yaml +0 -21
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-agentic-context.yaml +0 -28
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-message.yaml +0 -17
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-synchronous-messaging.yaml +0 -21
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor.yaml +0 -8
- foundry_implementation_actor-0.2.1/tests/test_cards.py +0 -49
- foundry_implementation_actor-0.2.1/tests/test_engine.py +0 -144
- foundry_implementation_actor-0.2.1/uv.lock +0 -191
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/CLAUDE.md +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/adr/ADR-FIA-0001-the-machinery-leaves-the-capability.md +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/adr/template.md +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/scripts/probe_grounding.py +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/cards/actor.yaml +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/correlation.py +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/grounding.py +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/handler.py +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/tests/fixtures/broken/actor-agentic-context.yaml +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/tests/test_cli.py +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.0}/tests/test_grounding.py +0 -0
- {foundry_implementation_actor-0.2.1 → 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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: foundry-implementation-actor
|
|
3
|
-
Version: 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/,
|
|
49
|
-
- {name: stub, path: stub/,
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
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:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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,12 +129,17 @@ papeete-actor-synchronous-messaging lint-card \
|
|
|
97
129
|
"$(python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
|
|
98
130
|
```
|
|
99
131
|
|
|
100
|
-
A
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
106
143
|
|
|
107
144
|
Prose is not compared, on purpose: a use *should* name its real capability and its real peers, and
|
|
108
145
|
`actor.yaml`'s `name:` is its own identity and is required to differ.
|
|
@@ -111,7 +148,16 @@ Because the cards sit under `src/`, `tests/test_portability.py` greps them too
|
|
|
111
148
|
a knowledge tool name written into the actor's own definition fails the build exactly as it would
|
|
112
149
|
in the code.
|
|
113
150
|
|
|
114
|
-
##
|
|
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`
|
|
115
161
|
|
|
116
162
|
```
|
|
117
163
|
clone (full, not --depth 1)
|
|
@@ -126,6 +172,42 @@ clone (full, not --depth 1)
|
|
|
126
172
|
It never opens a pull request. Rendering a verdict and opening one belongs to whichever actor
|
|
127
173
|
orchestrates the pipeline, once its other members have also confirmed.
|
|
128
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
|
+
|
|
129
211
|
## Grounding is a precondition, not a request
|
|
130
212
|
|
|
131
213
|
`CLAUDE.md` at the working directory's root is loaded by the `claude` CLI **before the first
|
|
@@ -206,11 +288,14 @@ this can be an ordinary Pod.
|
|
|
206
288
|
## CLI
|
|
207
289
|
|
|
208
290
|
```bash
|
|
209
|
-
foundry-implementation-actor lint . # validate the sidecar
|
|
291
|
+
foundry-implementation-actor lint . # validate the sidecar and the cards
|
|
210
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)
|
|
211
295
|
```
|
|
212
296
|
|
|
213
|
-
`
|
|
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
|
|
214
299
|
checked further — UNMIGRATED is not the same as non-conformant, and migrating is the owning pair's
|
|
215
300
|
own act.
|
|
216
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/,
|
|
29
|
-
- {name: stub, path: stub/,
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
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:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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,12 +106,17 @@ papeete-actor-synchronous-messaging lint-card \
|
|
|
77
106
|
"$(python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
|
|
78
107
|
```
|
|
79
108
|
|
|
80
|
-
A
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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.
|
|
86
120
|
|
|
87
121
|
Prose is not compared, on purpose: a use *should* name its real capability and its real peers, and
|
|
88
122
|
`actor.yaml`'s `name:` is its own identity and is required to differ.
|
|
@@ -91,7 +125,16 @@ Because the cards sit under `src/`, `tests/test_portability.py` greps them too
|
|
|
91
125
|
a knowledge tool name written into the actor's own definition fails the build exactly as it would
|
|
92
126
|
in the code.
|
|
93
127
|
|
|
94
|
-
##
|
|
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`
|
|
95
138
|
|
|
96
139
|
```
|
|
97
140
|
clone (full, not --depth 1)
|
|
@@ -106,6 +149,42 @@ clone (full, not --depth 1)
|
|
|
106
149
|
It never opens a pull request. Rendering a verdict and opening one belongs to whichever actor
|
|
107
150
|
orchestrates the pipeline, once its other members have also confirmed.
|
|
108
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
|
+
|
|
109
188
|
## Grounding is a precondition, not a request
|
|
110
189
|
|
|
111
190
|
`CLAUDE.md` at the working directory's root is loaded by the `claude` CLI **before the first
|
|
@@ -186,11 +265,14 @@ this can be an ordinary Pod.
|
|
|
186
265
|
## CLI
|
|
187
266
|
|
|
188
267
|
```bash
|
|
189
|
-
foundry-implementation-actor lint . # validate the sidecar
|
|
268
|
+
foundry-implementation-actor lint . # validate the sidecar and the cards
|
|
190
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)
|
|
191
272
|
```
|
|
192
273
|
|
|
193
|
-
`
|
|
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
|
|
194
276
|
checked further — UNMIGRATED is not the same as non-conformant, and migrating is the owning pair's
|
|
195
277
|
own act.
|
|
196
278
|
|