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.
- 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.0 → foundry_implementation_actor-0.5.0}/.gitignore +1 -0
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/CLAUDE.md +9 -1
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/PKG-INFO +109 -14
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/README.md +105 -13
- 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.0 → 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.0 → foundry_implementation_actor-0.5.0}/pyproject.toml +9 -1
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/__init__.py +14 -2
- {foundry_implementation_actor-0.2.0 → 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.5.0/src/foundry_implementation_actor/cli.py +174 -0
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/config.py +18 -7
- foundry_implementation_actor-0.5.0/src/foundry_implementation_actor/conformance.py +133 -0
- {foundry_implementation_actor-0.2.0 → 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.0 → 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.0 → 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.0 → foundry_implementation_actor-0.5.0}/tests/test_config.py +18 -4
- foundry_implementation_actor-0.5.0/tests/test_conformance.py +164 -0
- foundry_implementation_actor-0.5.0/tests/test_engine.py +344 -0
- {foundry_implementation_actor-0.2.0 → 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.0/.github/workflows/ci.yml +0 -58
- foundry_implementation_actor-0.2.0/.github/workflows/release.yml +0 -49
- foundry_implementation_actor-0.2.0/src/foundry_implementation_actor/cards/actor-message.yaml +0 -17
- foundry_implementation_actor-0.2.0/src/foundry_implementation_actor/cards/actor-synchronous-messaging.yaml +0 -21
- foundry_implementation_actor-0.2.0/src/foundry_implementation_actor/cli.py +0 -107
- foundry_implementation_actor-0.2.0/tests/fixtures/valid/actor-agentic-context.yaml +0 -28
- foundry_implementation_actor-0.2.0/tests/test_cards.py +0 -49
- foundry_implementation_actor-0.2.0/tests/test_engine.py +0 -144
- foundry_implementation_actor-0.2.0/uv.lock +0 -191
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/adr/ADR-FIA-0001-the-machinery-leaves-the-capability.md +0 -0
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/adr/template.md +0 -0
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/scripts/probe_grounding.py +0 -0
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/cards/actor.yaml +0 -0
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/correlation.py +0 -0
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/grounding.py +0 -0
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/src/foundry_implementation_actor/handler.py +0 -0
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/tests/fixtures/broken/actor-agentic-context.yaml +0 -0
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/tests/test_cli.py +0 -0
- {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.5.0}/tests/test_grounding.py +0 -0
- {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
|
|
@@ -29,7 +29,7 @@ There is no separate lint/format command configured in this repo.
|
|
|
29
29
|
|
|
30
30
|
## Architecture
|
|
31
31
|
|
|
32
|
-
|
|
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.
|
|
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,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
|
-
##
|
|
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
|
-
`
|
|
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/,
|
|
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,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
|
-
##
|
|
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
|
-
`
|
|
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
|
|