AWS Builder Center
Transform the monolith, validated against the yardstick every step

Transform the monolith, validated against the yardstick every step

Series: Rewriting a PHP monolith to Python with AWS Transform custom + Kiro. Part 2 of 3: authoring the generic PHP-to-Python transformation skill and running the bulk rewrite with AWS Transform custom, graded against the Part-1 harness as it goes.

Series: Monolith Modernization - Kiro+AWS Transform (3 articles)

  1. 2
    Transform the monolith, validated against the yardstick every step This article
In Part 1  we built the thing everything else depends on: a black-box behavioral harness that captures what Kanboard actually does, over the network boundary (HTTP JSON-RPC + a Playwright UI layer + assertions on the shared database), and froze it green against the real PHP app. That harness is the sole oracle for this rewrite: there is no official Python Kanboard to grade against, so "correct" means the same suite that is green on PHP goes green on the Python build. By the end of Part 1 it covered 74% of Kanboard's documented features (77 of 104), every covered feature exercised and its effect asserted.
This post is the middle of the arc: teach AWS Transform custom how to do the rewrite, then run it, with the harness wired in as Transform's own validation gate. The division of labor holds throughout the series: Kiro builds the yardstick and closes the last mile; AWS Transform custom does the bulk, repeatable transformation and self-validates against the yardstick in the middle.
The subject and target are unchanged from Part 1: Kanboard v1.2.45 (PHP 7.4) → Python on FastAPI + SQLModel, re-exposing the same JSON-RPC contract over the same SQLite schema (a strangler swap: replace the implementation, hold the interface and the data).

What "74%" means, and why not more

74% isn't the harness falling short, it's the harness being honest. It only claims a feature is preserved when it can prove it over the black-box boundary. The other ~26% we deliberately scoped out rather than fake, because verifying it would need systems that aren't part of the app: external integrations wired through fixed config (LDAP/OAuth/two-factor, email and webhook delivery, reverse-proxy trust), CLI-only paths (CSV export, DB maintenance), and out-of-band UI (CAPTCHA, remember-me, drag-only controls). None of it is "PHP-specific", it's externally bound, so a behavioral oracle can't grade it alone. That's exactly the ground Kiro picks up in Part 3. The point for now: 74% is the ceiling of what's provable, and everything under it is genuinely proven.

Understanding the app first, with BRE

A rewrite you can't explain is a rewrite you can't trust. Before touching code we ran AWS Transform's managed business-rules-extraction (BRE) on Kanboard, it statically reconstructs the domains, business rules, the database schema, and a full feature inventory. It's covered end to end in the earlier post, From Monolith to Spec: Auto-Extracting Business Rules with AWS Transform custom , so we won't re-explain it, but running it is not optional busywork. It earns its place twice:
  • It gave Part 1 its coverage checklist, the feature inventory is what told the harness what to exercise.
  • In this part it makes the transformation itself faster and more accurate. Instead of re-deriving Kanboard's logic from ~166K lines of PHP on every run, the transformation agent starts from BRE's domains, rules, and reconstructed schema, so it spends its budget translating known behavior rather than rediscovering it. The understanding is captured once and reused as grounding.
So BRE isn't a side quest, it's the shared foundation both the yardstick (Part 1) and the rewrite (this part) are built on.

Why AWS Transform custom for this, and not just a coding agent

A fair question: you could open Kanboard in a coding agent like Kiro and grind through the port yourself. For a bulk, repeatable, well-specified migration, AWS Transform custom is the better tool, for concrete reasons:
  • Reusable. You author the transformation as a skill once and run it across many repositories. The PHP→Python skill we build here isn't Kanboard-specific, it's the asset you point at the next PHP app.
  • Continual learning. Transform captures learnings from each execution and auto-applies them on the next run and on other apps, the more you run it, the sharper it gets. We actually saw this between our two runs (more below), and it's documented in the Continual learning section of the AWS Transform custom docs .
  • Purpose-built for transformation. It's specialized for modernization, so its coverage of migration patterns and library mappings runs deeper than a general-purpose coding agent's.
  • Autonomous. You set the goal, the skill, and the validation gate, and it runs hands-off, no constant back-and-forth. The bulk of this migration came out of essentially two unattended runs.
  • Pay-as-you-go, per agent-minute (pricing ). You don't want to burn premium interactive-coding budget on tech-debt and migration grind, save that for feature work, and let a metered agent chew through the repetitive port.
  • It's a pipeline, not a single agent. Under the hood it runs a planner, an executor that parallelizes across sub-agents (with the context-chunking large repos need), a validator, and a self-debugger, it plans, executes, validates against the exit criteria, and when something fails it roots out the cause and fixes it until the goal is met. You could assemble that pipeline yourself around a coding agent; Transform ships it prebuilt.
The rest of this post is that workflow: author the skill, wire the harness in as the gate, and run.
Transform flow

Authoring the transformation skill

In AWS Transform custom, a transformation definition is a skill: a SKILL.md plus an optional references/ folder of docs loaded on demand, which improves over time through continual learning. For how skills are structured and published, see the AWS Transform custom documentation .
The design decision that shapes this whole post: the skill is generic, and everything app-specific lives in the execution config. One reusable PHP → Python (FastAPI + SQLModel) transformation skill, no Kanboard classes, routes, or tables anywhere in it, plus project-specific execution: the contract to hold, the grounding, and the version pins are supplied via additionalPlanContext, and the validation command is defined in an execution config (shown here as YAML; JSON works too). Same skill, next PHP app, new config.
And keeping it generic isn't only about reuse: forcing the skill to cover the full breadth of PHP patterns, not just the ones this app happens to use, makes the resulting migration more robust, not narrower.

The process

I didn't hand-write the skill. I gave atx a prompt of guiding principles, what to build and the rules it must never break, and let it author the skill. The essence: migrate any PHP app to idiomatic FastAPI + SQLModel, strangler pattern (preserve the contract and the schema, replace the implementation), cover the whole surface (language, interface, data, cross-cutting concerns, the user-facing layer, and verification), and treat an external black-box test suite as the oracle. The full guiding-principles prompt is in Appendix A.
atx then ran a short authoring interview. The pivotal exchange was whether it could scan an example codebase to ground the skill, and my answer captures the whole idea, here's an example app, but make the skill generic:
> Yes, scan the real app to discover the full breadth of PHP patterns actually present (data access, custom MVC + DI, procedures/models/validators, events/subscribers, traits, magic methods, closures). Use it to make coverage comprehensive, ``but the skill must stay generic``: generalize every pattern to a ``pattern → Python idiom`` mapping, use ``no app-specific identifiers`` anywhere, keep framework detection general (Laravel/Symfony/Slim/none), and scan only the app source.
That's the trick to a reusable skill: point it at a real monolith so its pattern coverage is grounded in reality, then force it to generalize every example. It also asked about defaults, and this is where you supply anything org-specific the agent can't infer: your coding standards, the specific libraries or frameworks your team standardizes on, target version pins, preferred project layout. Feed in what only you know about your environment; let the skill stay general about everything else.
What that produced: a single generic skill plus its on-demand reference docs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
php-to-python/
├── SKILL.md # phases, CRITICAL rules, exit criteria
└── references/ # loaded on demand, when a pattern appears
├── language-and-type-mapping.md
├── stdlib-and-builtins-mapping.md
├── build-and-dependencies.md
├── api-layer-migration.md
├── data-access-migration.md
├── error-and-exception-model.md
├── config-auth-logging.md
├── testing-and-validation.md
├── di-and-events-migration.md
├── common-pitfalls.md
└── verification-checklist.md

The review is human-in-the-loop

A generated skill is a draft, not a deliverable. Every review gate here is human-in-the-loop, I read the output and directed the fixes before anything was published. Two things worth knowing to look for, in plain terms:
  • No real app names leaked into the examples. The whole value of a generic skill evaporates if a real class or route name slips into a "worked example." A quick search of the draft turned up exactly one, a Kanboard procedure class in a sample, which I had replaced with a generic name and re-checked clean.
  • It had to cover the UI and side-effects, not just the API. The first draft focused on the API: endpoints, parameters, JSON shapes. It said little about what users see and what happens behind the scenes. So I added three rules: rebuild the real screens and web-only features people use; reproduce the side-effects an action triggers even when it returns nothing (a notification created, an activity logged, an audit row written); and build the UI so tests can find things the way a person does, by visible labels, button text, and roles, not by the HTML underneath. The harness checks exactly these behaviors, so a skill that skips them sends the rewrite into avoidable red loops.
Only after those human review passes did the skill get published.

Wiring the harness in as the validation gate

Transform is only as good as what it validates against, and our gate isn't mvn test or a quick pytest, it's the full behavioral harness. To make Transform run it during the migration, we packaged it as an atx client-side skill  called harness-validation, a capability the agent discovers from the project's .aws/atx/skills/ directory (see Appendix B). It wraps and reuses the exact same harness, one source of truth, and owns the whole lifecycle: reset the database, start the app, replay Layer 1 (data) + Layer 2 (API) + Layer 3 (UI), and run the harness self-tests. Exit code 0 only when everything is green.
The same harness validates both the PHP v1.2.45 baseline and the Python rewrite, you just point it at whichever app is running, which is what makes "green on PHP must go green on Python" a literal, runnable statement. It exposes two modes:
  • Quick validation, Layer 1 + Layer 2 (data + API) only. Fast; use it while iterating.
  • Final validation, all three layers plus the self-tests. Slower (a real browser drives the UI); run it before declaring the migration done.
And a human-in-the-loop step that matters: I built the validation skill with Kiro and dry-ran it green against the PHP baseline first, shaking out the ordinary flakiness you get when a real browser and a single-threaded dev server are in the loop, before wiring it into Transform. Test the yardstick yourself before the transformation is allowed to depend on it.

The execution config

Everything project-specific lives in an execution config the generic skill reads at run time, YAML here, though JSON works too. The parts that matter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
transformationName: php-to-python
codeRepositoryPath: kanboard/ # the PHP app; Python is generated into kanboard/pyapp/

# one-time env setup so the validation harness can always run
buildCommand: "pip install httpx pytest hypothesis playwright && python -m playwright install chromium && (pip install -r pyapp/requirements.txt || true)"

additionalPlanContext: |
TARGET: idiomatic Python 3.12+ on FastAPI + SQLModel, preserving the external contract.
STRANGLER: re-expose the SAME JSON-RPC 2.0 API; keep the existing database schema unchanged.
GROUNDING: the BRE output + the exact API-surface inventory (do not re-derive from source).
COVERAGE: validation asserts more than the API — build the web UI users interact with (findable
by visible labels/roles) and the web-only features, and reproduce the behind-the-scenes
side-effects (activity, notifications, transition and audit rows) an action triggers.
VALIDATE: run ONLY through the harness-validation client-side skill — quick validation while
iterating, final validation before declaring done; when red, fix the Python app, never the harness.
Three prerequisites worth stating up front rather than as lessons:
  • Raise the shell timeout. The validation command is a full behavioral harness (readiness wait + three layers including a real browser + self-tests), minutes, not seconds. Transform's per-command shell timeout must be set high enough or it kills the run mid-validation and never sees it go green: ATX_SHELL_TIMEOUT=3600.
  • Keep the plan context tight. additionalPlanContext has a 4,096-character limit; the real one is a bit longer than the excerpt above but stays comfortably under it.
  • Give the agent a real browser through MCP. The harness drives Playwright itself for its Layer-3 checks, but we also configured the Playwright MCP server (in ~/.aws/atx/mcp.json) so Transform's agent could drive a browser directly during the run: open the generated app, click through a flow, and see why a UI check was red instead of just that it was. The harness stays the pass/fail gate; the Playwright MCP gives the agent eyes to reproduce, diagnose, and confirm UI behavior while it works, which is what turns a red Layer-3 result into a fix instead of a guess.
The run itself, from the repo root:
1
2
ATX_SHELL_TIMEOUT=3600
atx custom def exec -n php-to-python -p kanboard -g file://docs/atx-exec.yaml -x -t
One gotcha, invisible until you run a real harness as the gate: the app-under-test now has a lifecycle. If a validation run is interrupted, the server it started can keep holding the database file and the port, so the next run can't reset the DB or bind, a self-perpetuating wedge. The fix is lifecycle hygiene owned by the validation skill: reset the DB with a retry, free the port before launch, and route everything through one managed entry point so an interrupted attempt can't poison the next.

Running it, and the one lesson that shaped the whole series

I ran it twice, and the two runs are the most instructive part of this post.
First run. I pointed the skill at Kanboard with the harness as the gate, and it went fully green after changes, Layer 1, Layer 2, Layer 3, self-tests, all passing. And yet the result was only a ~42% migration (44 of 104 features, 104 being the full feature inventory BRE extracted from the app). Not because anything was broken, because the harness at that point only measured ~42% of the app's features. The agent built exactly what its gate measured and stopped. The uncovered features, notifications, the analytics reports, the automatic-actions engine, imports, custom roles, produced no signal at all, not even "this is missing." A green run against a partial oracle looks finished and isn't.
That is the lesson the whole series is built around, stated more sharply than Part 1 could:
> Coverage is the ceiling, and not just for the grader. It's the ceiling for the generator. An agent optimizes to its gate. Whatever the yardstick doesn't measure, the rewrite won't build.
So the fix wasn't to nag the transformation, it was to strengthen the yardstick. That sent me back to Part 1's work: expand the harness's feature coverage from ~42% to 74% (driving the missing behaviors through the UI layer, where Kanboard's event subscribers actually fire), and sharpen the skill so it knew to reproduce those side-effects and web-only flows.
Second run. Same skill, same command, now against the stronger gate. This time the transformation built materially more, and drove the Python app to full green on the entire 74% harness: Layer 1 (data/schema), Layer 2 (the whole JSON-RPC contract, 159 of 160 methods; the one exclusion needs a live LDAP backend), Layer 3 (every UI flow, including the behind-the-scenes side-effects the API path silently skips), and the harness self-tests. Verified by re-running the final validation independently, from a fresh database, with the harness confirmed byte-for-byte unchanged.
That before/after is a controlled experiment in the series' thesis: hold the skill essentially constant, strengthen the oracle, and the generator builds more. The harness is what pushes the rewrite.
Continual learning in action. The second run didn't start from scratch on everything it had already figured out. Transform had captured a learning from the first run (about preserving the JSON-RPC contract's response shapes) and auto-applied it on the second, the mechanism referenced earlier under "why Transform." The point is the loop is real: learnings captured on one run are applied automatically on the next, and across many apps that accumulation is what compounds.
What the transformation run reported when it finished, the harness gate green plus its own summary:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ atx custom def exec -n php-to-python -p kanboard -g file://docs/atx-exec.yaml -x -t
... Transform scaffolds pyapp/, translates the PHP, and iterates against the harness ...

[validate] PASS: all layers + self-tests green # Layer 1+2+3 + self-tests, 159/160 methods

Continual learning: applied 1 lesson from a prior run (api-contract-parity).

Changes Summary:
Files changed: 34
Lines added: 1681
Lines deleted: 48
Initial commit: c1337dd3 (captured at start)

Agent Minutes used: 1995.25
Conversation log saved: ~/.aws/atx/custom/20260724_124330_d143f7fc/logs/...

Cost

Real agent-minutes, from the run metadata:
ActivityAgent-minutes
Authoring the transformation skill353.94
Transformation run 1 (→ 42%, against the initial harness)2,507.81
Transformation run 2 (→ full green, against the 74% harness)1,995.25
Combined4,857.00
On the Kiro side, building and dry-running the harness-validation skill and reviewing each step ran about 63 Kiro Power credits (Claude Opus 4.8). The two tools meter separately: AWS Transform bills agent-minutes, Kiro bills credits.
For current pricing, see the AWS Transform pricing page  rather than converting these minutes to a dollar figure that will go stale.

Where this leaves us, and the hand-off to Part 3

The Python rewrite passes 100% of the behavioral oracle, every harness layer green, reproduced independently. Precisely stated: the 74% of Kanboard's features the harness measures now behave identically on the Python app, over the same schema and the same JSON-RPC contract. That's a strong, strong result: not "we ported everything," but "everything the yardstick can prove, is proven."
What's left is exactly the externally-bound and out-of-band features from the "why only 74%" section, the ones a black-box oracle can't grade on its own. In Part 3, Kiro picks up there: completing those remaining features (standing in external integrations with test doubles so they can be verified) to push the migration toward a genuinely 100%-migrated app, then verifying the result beyond the harness with dedicated unit tests (standing in external systems with those doubles) and a shadow run of the PHP and Python apps side by side. That's the last mile, the interactive, developer-in-the-loop work that a bulk transformation validated by a behavioral oracle can't reach on its own.

Appendix A, the skill-authoring prompt (guiding principles)

This is the shape of the prompt handed to atx to author the generic php-to-python skill. It's guiding principles, not a line-by-line spec, you set direction and the rules that must never break; Transform does the authoring. It never names the subject app: app-specifics are discovered by the skill's discovery scan or supplied via additionalPlanContext at execution time.
You don't have to write something this complete up front. You can either start with a comprehensive guiding-principles prompt like the one below, or start from a one-line goal ("migrate this PHP app to Python on FastAPI + SQLModel") and let the authoring interview draw the rest out of you, building the skill up by chatting with atx iteratively. Either path lands in the same place.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Author a general-purpose `php-to-python` transformation skill (a SKILL.md plus a references/ folder),
following AWS Transform custom best practices. These are guiding principles — you do the authoring.

Objective & guiding principle
- Migrate any PHP app to idiomatic, typed Python on FastAPI + SQLModel — a faithful behavioral port,
never a line-by-line transliteration.
- Strangler pattern: preserve the external contract (same interface — HTTP/RPC/CLI: paths, params,
response and error shapes) and operate over the unchanged data schema. Replace the implementation;
hold the interface and the data.
- Correctness is judged by an external, black-box behavioral test suite provided as the validation
command. Treat it as the oracle: iterate until it is green; never modify the suite to pass.
- Keep the skill generic — no app-specific classes, routes, or tables. Discover everything
app-specific at run time (a discovery scan + additionalPlanContext), including version pins.

Cover the whole surface — do NOT narrow to any single layer
- Discovery first: detect the build/dependency system, the framework (or none), the interface
surface, the data-access style and DB engine, the auth model, background jobs/CLI, templating/UI,
and notable libraries; assess risk; read version pins, the contract to preserve, and the validation
command from context. This decides what else runs.
- The full language surface: types and data structures, OOP constructs, traits, magic methods,
closures, generators, string/number/truthiness semantics, and PHP built-ins → Python stdlib — and
call out behaviors that silently differ.
- The interface/API layer: re-implement it preserving the contract exactly.
- The data/persistence layer: map it over the unchanged schema; do not migrate or mutate the schema.
- Cross-cutting concerns as present: auth/permissions, sessions, middleware, configuration, logging,
caching, events/subscribers and the side-effects they produce, and background/scheduled jobs.
- The user-facing layer: rebuild the screens and web-only features, make the UI locatable by visible
labels/roles (not internal markup), and reproduce the side-effects an action causes even when
nothing is returned to the caller.
- Verification: scan for leftover PHP constructs, build, run the validation command, and check every
migration against an explicit exit-criteria checklist. Never declare success while validation is red.

Follow AWS Transform custom authoring best practices: mark must-follow rules with CRITICAL:/IMPORTANT:,
give exact literal commands where it matters, order the work into phases, and include generic (never
app-specific) worked before/after examples. Move reusable assets — pattern→idiom mappings, dependency
maps, examples, checklists — into references/ so they load on demand. Model the overall structure on a
proven community transformation (see Appendix C).

Appendix B, the harness as a client-side validation skill

The harness is delivered to Transform as a client-side skill, an agent capability discovered from the project's .aws/atx/skills/ directory (see the AWS Transform custom docs for client-side skills ). The skill is thin on purpose: it doesn't reimplement any validation logic, it drives the exact behavioral harness we already built. Its scripts/validate.py resets the SQLite database, starts the app under test, waits until it serves, then invokes python -m harness.runner (Layers 1–3, data, API, UI) followed by the pytest self-tests, and returns a single exit code (0 = all green). Because it points at the one repo-root harness, there's a single source of truth, and it runs against either implementation, the PHP baseline or the Python rewrite.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
---
name: harness-validation
description: \>-
Validate a Kanboard implementation (PHP baseline or Python rewrite) against the frozen Part 1
behavioral harness — the sole oracle. Resets the DB, starts the app, replays Layer 1 (data) +
Layer 2 (API) + Layer 3 (UI) via `harness.runner`, then runs the Part-1 `pytest` self-tests.
Exit code 0 means every layer is green.
---

# harness-validation

The single, correct way to run behavioral validation for this project. It wraps the Part-1 black-box
harness (the sole oracle): the same suite green on PHP v1.2.45 must go green on the Python app. It
does not reimplement anything — it calls the Part-1 `harness.runner` (Layers 1–3) and `pytest`.

## How to run it

Quick validation (fast inner loop — data + API only):

python \<skill-dir\>/scripts/validate.py --quick

Final validation (all layers + Part-1 self-tests, before declaring done):

python \<skill-dir\>/scripts/validate.py # the Python rewrite
python \<skill-dir\>/scripts/validate.py --stack php # the PHP v1.2.45 baseline

The script owns the whole lifecycle and locates the repo-root Part-1 harness itself. In order it:
1. Resets the DB (deletes the shared SQLite file so the app recreates a fresh schema on startup).
2. Starts the app and waits until it serves at KANBOARD_BASE_URL.
3. Replays behavior via `python -m harness.runner` — Part-1 Layer 1 (data), Layer 2 (API), Layer 3 (UI).
4. Runs the Part-1 harness self-tests via `pytest`.

Exit code 0 = all layers + self-tests green. Non-zero = read the per-failure report naming the layer.

## CRITICAL rules
- Never run `harness.runner`, a single `--layer`, the seeder, or `pytest` directly — standalone they
bypass the DB reset + app startup and fail spuriously. Always go through this script.
- Never edit the harness, the tests, or the golden data to make validation pass. When red, fix the
generated Python app and re-run the SAME command until exit code 0.

The Kiro prompt used to build this skill

The validation skill itself was written with Kiro. The prompt was, in essence:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Package our existing black-box behavioral harness as an AWS Transform custom client-side skill named
`harness-validation`, so the transformation agent can run it as its validation gate.

- Do NOT reimplement any validation logic. The skill wraps and reuses the existing harness at the
repo root — its `harness.runner` (Layer 1 data / Layer 2 API / Layer 3 UI) and the `pytest`
self-tests. It must locate that harness itself (walk up to the directory that contains it) so there
is exactly one source of truth.
- Write a SKILL.md (name + description, how/when to use) and a scripts/validate.py that owns the
whole lifecycle: reset the database to a fresh file, start the app under test and wait until it
serves, run the harness layers, then run the self-tests; return exit code 0 only if all green.
- The same skill must validate either implementation — accept a way to select which app to start
(the PHP baseline or the Python rewrite), defaulting sensibly.
- Provide two modes: quick (data + API only) for fast iteration, and full (all layers + self-tests)
for the final gate.
- Harden the lifecycle for repeated/interrupted runs: retry the DB reset if the file is locked, free
the app's port before launch, and kill the whole process tree on teardown so an orphaned server
can'
t wedge the next run.
- Bake into SKILL.md as CRITICAL rules: never edit the harness/tests/golden data to pass; when red,
fix the app under test; never invoke the raw runner or pytest directly — always go through the script.
Because the skill only wraps the harness, this is a small, one-time piece of glue, and Kiro dry-ran it green against the PHP baseline before it was ever handed to Transform.

Appendix C, a proven example skill to model on

For a real, published transformation skill to structure yours on (the discovery scan + conditional phases + exit-criteria pattern this one follows), see the community jboss-to-springboot sample in the AWS Transform custom samples repository:

Series: Monolith Modernization - Kiro+AWS Transform (3 articles)

  1. 2
    Transform the monolith, validated against the yardstick every step This article
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article