Migration v7 β v8 via Agent Teams. Parallel teammates, script-heavy, JSON-driven task decomposition.
Migrates any Lutece plugin/module/library from any version before 8 to v8 with a team of teammates. The Lead (you) orchestrates, specialized teammates execute in parallel, and bash scripts handle all mechanical work.
Prerequisites: subagent or teammate dispatch, or the sequential fallback (using-lutecepowers, section Subagents and teams).
Confirm the current directory is a Lutece project (pom.xml with lutece-plugin/module/library packaging).
mkdir -p .migration
bash ${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/scripts/scan-project.sh . > .migration/scan.json
Keep the migration's own scratch out of the diff, once, now β a reviewer should never see it, and
git add -A at the end would otherwise stage it. The e2e bench (e2e/) is part of it: a local test tool,
never committed with the plugin:
# a file not ending with a newline would glue the first entry to its last line, silently
[ -s .gitignore ] && [ -n "$(tail -c1 .gitignore)" ] && echo >> .gitignore
for p in 'target/' 'logs/' 'java.io.tmpdir/' '.migration/' '*.log' 'e2e/'; do
grep -qxF "$p" .gitignore 2>/dev/null || echo "$p" >> .gitignore
done
Read .migration/scan.json and show the user:
summary.persistence): hasJpa β the JPA model is kept on the EclipseLink of the container (patterns/persistence-patterns.md Β§1); hasSpringJdbc β Spring JDBC kept as a library (Β§9)For every Lutece dependency in scan.json:
v8Status: "available" β OK (already cloned in ~/.lutece-references/); latestRelease / latestSnapshot are the published versions the pom will namev8Status: "published" β the artefact exists in the Lutece repositories at the versions listed; clone its sources into ~/.lutece-references/ as the dependency-references rule says (the clone, not an edit of the hook)v8Status: "to-resolve" β nothing found locally nor published: find the repository and check its v8 branch (dependency-references rule; v8 lives on develop, the pom parent must be 8.x)~/.lutece-references/, clone it there yourself (dependency-references rule); adding it to the REPOS list of ${LUTECEPOWERS_ROOT}/hooks/sync-references is the lead's job, afterwards, outside the migrated repository. The hook (it clones develop and fetches the v7 branches). Teammates can then search reference sources for ALL dependencies, not just the repositories listed in the hook.bash ${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/scripts/task-splitter.sh .migration/scan.json .migration
Read the output to know how many teammates to spawn.
From here the lead only orchestrates and never edits files (on Claude Code with Agent Teams: Shift+Tab). On a harness without dispatch, execute the teammates below yourself, one after the other, in the Phase D order.
Config Migrator (1 teammate)
${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/teammates/config-migrator.md.migration/tasks-config.jsonwebapp/WEB-INF/web.xml and of the *_context.xml files (deleted by it once the Java Migrators are done)Verifier (1 teammate)
${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/teammates/verifier.mdJava Migrator(s) (1-3, based on scan.json recommendation)
${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/teammates/java-migrator.md.migration/tasks-java-0.json, .migration/tasks-java-1.json, .migration/tasks-java-2.json.migration/tasks-java-homes.json (Home and interface files, excluded from the other partitions)Template Migrator (0-1, if templates/JSP exist)
${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/teammates/template-migrator.md.migration/tasks-template.jsonmigrate-template-mechanical.sh with --no-webxml (web.xml belongs to the Config Migrator)Test Migrator (0-1, if test files exist)
${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/teammates/test-migrator.md.migration/tasks-test.jsonWhen spawning each teammate, provide the text below with ${LUTECEPOWERS_ROOT} replaced by the literal absolute path from your session context. A teammate does not see that context and may have no such shell variable.
LUTECEPOWERS_ROOT=${LUTECEPOWERS_ROOT} (export it in your shell before running any script)
Read your instruction file at [path to teammates/*.md].
Read your task assignment at [path to .migration/tasks-*.json] (Java Migrator 0: also .migration/tasks-java-homes.json).
Execute all steps in your instructions. Use scripts from ${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/scripts/.
Pattern files are at ${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/patterns/ β load only when needed.
Reference implementations: always search ~/.lutece-references/ before writing any new pattern; each clone also carries the v7 branches (see using-lutecepowers, Mandatory reads) to compare a pattern before and after migration.
Run verify-file.sh after each file you complete.
Wire the dependency graph:
Config Migrator ββββββββββββββββββββββββββββββββββββββββ (no blockers, runs first)
β
ββββ Java Migrator 0 ββ
ββββ Java Migrator 1 ββ€ (blocked by Config Migrator)
ββββ Java Migrator 2 ββ
β
ββββ Template Migrator ββ (blocked by ALL Java Migrators)
ββββ Test Migrator ββββββ€ (blocked by Config + at least 1 Java Migrator)
β
ββββ Verifier: Final Build (blocked by ALL above)
*_context.xml files once ALL Java Migrators completeTeammate answers travel through a channel that truncates and may arrive late: every teammate also writes
.migration/report-<teammate>.md (what it changed, what it could not, what the next one must know) when it
finishes, and the Lead reads those files, not the channel, to decide the next phase.
While teammates work:
bash ${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/scripts/progress-report.sh .
When the Verifier reports compile BUILD SUCCESS, 0 failures and 0 errors in target/surefire-reports/*.txt (the global-pom sets testFailureIgnore=true, so BUILD SUCCESS alone says nothing about the tests) and verify-migration.sh: 0 FAIL, spawn a Reviewer teammate:
LUTECEPOWERS_ROOT=${LUTECEPOWERS_ROOT} (literal path, export it in your shell)
Read your instruction file at ${LUTECEPOWERS_ROOT}/agents/lutece-v8-reviewer.md.
Review this project for v8 compliance. Do NOT modify any files.
Reference implementations: ~/.lutece-references/
Why a teammate? The Lead does not edit or review files itself after Phase B. The reviewer runs as a read-only teammate (or a read-only subagent, or inline when no dispatch exists) and reports findings without modifying files.
Process the reviewer's findings:
lutece-e2e skill)A migration that compiles and whose unit tests pass has proved nothing about the screens. Unit tests cover almost none of a Lutece plugin, and the defects that hurt are the ones they cannot see: a portlet rendering an empty string, a form the browser closes because it is nested where HTML forbids it, a screen answering 500 on an unknown id.
Invoke the lutece-e2e skill on the project and follow it. It materialises e2e/, runs the stack, inventories
every screen and action, and reports. One command afterwards: KEEP=1 ./e2e/run.sh.
Read its report with the migration in mind:
lutece-e2e documents them.e2e/ is a defect of lutece-e2e, not of the plugin. Report it so
it goes back into that skill.Then fix what it found, and go to Phase H. The gate runs the bench again.
This phase is a loop, not a checkpoint. Run the gate, fix what is red, run it again. Keep going until it passes. A migration is done when the gate says so, never when someone judges the remaining red acceptable.
bash ${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/scripts/final-gate.sh .
Each turn of the loop:
The only way out other than green is a red you attribute, with evidence, to something the plugin cannot fix: a core defect, or a missing capability of the container. Name it, prove it, and carry it into the hand-over as an open item.
Stop the loop and ask when the same red comes back a third time after three different fixes. That is a sign the diagnosis is wrong, not the fix, and another round will not find it.
The gate passes when ALL of the following are true:
-Dmaven.compiler.showWarnings=true -Dmaven.compiler.showDeprecation=true: deprecation, unchecked, rawtypes, serial⦠all fixed, never suppressed), and surefire reports with 0 failures and 0 errorsThen:
*_context.xml files, then the Verifier to run the final sweep and remove .migration/verify-migration.sh results (PASS/FAIL/WARN counts)mvn clean install -Dmaven.test.skip=true)mvn clean lutece:exploded antrun:run -Dlutece-test-hsql test): tests run, failures, errors, skipped from target/surefire-reports/*.txtbash ${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/scripts/final-gate.sh . re-measures the checks, the unit tests read from surefire and the e2e bench, and refuses the migration while any of them is red. Run it after every batch of fixes, not once at the end: a fix to a portlet invalidates the tests that asserted on its old rendering, and a fix to a defect turns the scenario pinning it red. Drop .migration/gate-required in the project at the start of the migration and the plugin's Stop hook will not let a turn end while the gate is red.git status --porcelain | grep '^??'), and tell the user to stage them with git add -A, never git commit -a. beans.xml and the test microprofile-config.properties are new files: commit -a silently leaves them out, and the plugin then fails at the next clone with UnsatisfiedResolutionException in the Home static initializer. ST05 fails while they are untracked.using-lutecepowers, Mandatory reads β ALL teammates search ~/.lutece-references/ before writing new patternsverify-file.sh after each file, not just at the endThe scan reports counts only β these patterns require human judgment, no mechanical sed:
JPA entities (persistence.hasJpa) β equals/hashCode including a collection attribute, and a new object attached to a relation without cascade = PERSIST (inverse side included) before a flush: both pass with Hibernate and fail at runtime with EclipseLink (persistence-patterns.md Β§6). Only the e2e campaign on a fresh bench proves them.
shutdownServiceImpls β Classes implementing fr.paris.lutece.portal.service.init.ShutdownService. With CDI-managed @ApplicationScoped beans, replace with Jakarta-native @PreDestroy on a shutdown method. Drop the interface entirely if process() does nothing meaningful (no real cleanup work). When real cleanup exists:
// Before
public class XService implements ShutdownService {
@Override public String getName() { return "XService"; }
@Override public void process() { client.close(); }
}
// After
public class XService {
@PreDestroy void shutdown() { client.close(); }
}
Java Migrators handle this case-by-case. Don't auto-replace via sed β getName() may have legitimate uses elsewhere.
All in ${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/scripts/:
| Script | Purpose | Used by |
|---|---|---|
scan-project.sh |
Full project scan β JSON | Lead (Phase A) |
task-splitter.sh |
JSON scan β per-teammate task files | Lead (Phase B) |
migrate-java-mechanical.sh |
javaxβjakarta + SpringβCDI + net.sf.json imports | Java Migrators |
migrate-template-mechanical.sh |
BO macros + null-safety (--no-webxml for the Template Migrator) |
Template Migrator |
extract-context-beans.sh |
Spring context XML β JSON catalog | Config Migrator |
verify-migration.sh |
every check of verification/checks.md, optional --json mode |
Verifier |
verify-file.sh |
Per-file verification subset | All teammates |
final-gate.sh |
Postcondition: checks + compiler warnings + surefire + e2e, refuses a red migration (--help, --no-e2e) |
Lead (Phase G, after every fix) |
add-liquibase-headers.sh |
Liquibase headers on SQL files | Config Migrator |
restore-line-endings.sh |
Restores the endings HEAD had on files the editor converted (check LE01) | Verifier |
progress-report.sh |
Migration progress display | Lead (Phase E) |
All in ${LUTECEPOWERS_ROOT}/skills/lutece-migration-v8-agent-teams/patterns/:
| File | Content | Loaded by |
|---|---|---|
cdi-patterns.md |
CDI scopes, injection, producers, singleton, Models, Pager, Key Imports | Java Migrators (always) |
events-patterns.md |
Event/listener migration | Java Migrators (if events) |
cache-patterns.md |
EhCacheβJCache | Java Migrators (if cache) |
rest-patterns.md |
JerseyβJAX-RS, filters, providers | Java Migrators (if REST) |
mvc-patterns.md |
@RequestParam, CSRF auto-filter, @ModelAttribute | Java Migrators (if JspBean/XPage) |
template-macros.md |
v8 Freemarker macros, jQueryβvanilla JS | Template Migrator |
fileupload-patterns.md |
FileItemβMultipartItem | Java Migrators (if fileupload) |
json-patterns.md |
json-libβJackson | Java Migrators (if net.sf.json) |
deprecation-fixes.md |
What each deprecated API is replaced by (RBAC/workgroup User overloads, getModel(), Strings.CS, getInstance(), reflection, task signatures) |
Java Migrators (always, short) |
core-8x-moves.md |
Core APIs that moved or shrank (XSL to plugin-xmltransformer, ContentService without cache, Parser in library-core-utils), reflection-instantiated classes | Java Migrators + Config Migrator (always, short) |
sql-liquibase.md |
Header, one small changeset per concern, precondition on tables another plugin owns, runAfter, AUTO_INCREMENT on a table shipped with an id 0, why the upgrade path is proven on a taken-over database |
Config Migrator, Verifier |
persistence-patterns.md |
JPA kept on EclipseLink (persistence-3.1), JPQL/native SQL rules, entity rules, Spring JDBC as library |
Java Migrators + Config Migrator (if persistence.hasJpa or hasSpringJdbc) |