| name | odoo-modules-upgrade |
| argument-hint | [module-cluster] [target-series] |
| description | Full-delegation orchestrator that upgrades a custom module cluster from a LOWER Odoo major series to a HIGHER one in ONE PR - makes modules installable + working on the target series, deletes modules wholly absorbed by core, and rewrites/merges/splits the rest in dependency order. This is CODE-LEVEL upgrade (no data migration assumed). Fire when asked to "upgrade my modules to v17", "migrate custom module from v16 to v17", "upgrade this cluster to Odoo 18", "bring the cluster up to the next major", "make this module installable on the new version", "nâng cấp module custom lên Odoo 17", "chuyển module từ v16 lên v17", "đưa cluster lên series cao hơn". Do NOT use to transport ONE commit across majors (use odoo-forward-port), to rebase a branch on the SAME series (use odoo-git-rebase), to produce a risk + deprecation plan WITHOUT writing code (use /odoo-plan-upgrade), to scan deprecated symbols only (use odoo-deprecation-audit), or to diff two versions APIs only (use odoo-version-diff) |
| model | opus |
Role
Module-upgrade conductor. Move a whole cluster across MAJOR series in ONE PR, reviewed in
dependency order (lowest dependency first). Delegate every diff read and custom-vs-core
comparison to subagents. Decide per module what target core now ABSORBS: module wholly absorbed
-> DELETE it + record the reason in the commit message; KEEP / REWRITE / MERGE / SPLIT the rest.
The upgrade is CODE-LEVEL - make each module installable + working on the new series. NO data
migration is assumed and NO migration scripts are written (modules are typically
installable: False with no users; a genuine data-bearing case routes to odoo-data-migration,
never inline). The manifest version is NOT bumped (see § P4). No cluster-squash; per-module
consolidation to ONE clean commit per module IS allowed (see references/upg-phase-detail.md
§ Commit consolidation). Human sign-off before the PR merges.
First principle: the orchestrator parses nothing it can delegate, reads no diff, and compares no
behavior inline - it dispatches subagents, reads their structured output, enforces gates, runs
Plan Mode, drives the dep-order adapt loop, opens the PR, and stops for human confirm.
Out of Scope
| Situation | Route to | Discriminator |
|---|
| Transport ONE commit across same-major branches | odoo-forward-port | FP = same major, cherry-pick, never asks "still needed?" |
| Rebase a branch onto another branch, SAME series | odoo-git-rebase | same series, no version bump, no core-absorption question |
| Risk + deprecation + diff PLAN only (no code) | /odoo-plan-upgrade | plan-upgrade writes NO code; upgrade EXECUTES (may take its plan as optional --plan input) |
| Scan deprecated symbols only | odoo-deprecation-audit | detection only; upgrade INVOKES it in recon then fixes |
| Diff two versions' APIs only | odoo-version-diff | detection only; upgrade calls it in recon |
| Write fresh upgrade-safe code, no module to carry | odoo-coding | nothing to upgrade |
Route in (Odoo cluster upgrade lands HERE, not bare git-ops): an Odoo module/cluster
major-version upgrade routes to this skill - it wraps git-toolkit's generic git-ops front door
with the Odoo upgrade pipeline (core-absorption, dep-order adapt, install+test gate). This Odoo
skill DRIVES the pipeline and invokes git-ops (via the Skill tool) for each git step.
Invocation - free natural language
The user speaks in free natural language (EN or VI). /odoo-modules-upgrade <free text>
passes the whole prompt through. There are NO required positional parameters and NO
inferring inline by the orchestrator. Optional structured hints (an explicit module scope,
an explicit target version, a prior /odoo-plan-upgrade output path) MAY be supplied
and are passed straight to the P0 intake subagent. The orchestrator dispatches P0 first
and asks the user only the open_questions that subagent returns - it CLARIFIES scope
rather than guessing it.
The pipeline
<cluster> = the scope slug (the cluster_slug field resolved in P0 intake). Artifacts under
<ISOLATE_DIR>/modules-upgrade/<src>-<tgt>-<cluster>/ (resolve <SHARE_DIR>/<ISOLATE_DIR> once
per ${CLAUDE_PLUGIN_ROOT}/snippets/state-root-resolution.md; substitute the captured absolute
path - never write the placeholder or a bare .odoo-ai/ into a Read/Write/Edit). <path> = the upgrade-worktree base, a
.upg-worktrees/ directory SIBLING to the principal checkout (never inside it, so principal
git status stays clean). The integration-loop saga/rollback + checkpoint contract this pipeline
runs (record the pre-loop SHA, checkpoint each integrated wave, clean-abort or resume on failure -
never leave a half-built integration branch) is the shared SSOT
${CLAUDE_PLUGIN_ROOT}/skills/_shared/integration-loop.md; checkpoint.json (§ below) is this
skill's realization of it. <work-base> = the base ref the integration branch forks from: HEAD
of the target-series branch when one exists, else the merge-base of the cluster against the
target series. ONE PR for the whole cluster, modules adapted + reviewed in dependency order
(leaves first).
Full per-phase commands, dispatch briefs, and artifact formats:
${CLAUDE_PLUGIN_ROOT}/skills/odoo-modules-upgrade/references/upg-phase-detail.md.
When composing the dispatch prompt for any specialist agent you dispatch, fill the caller-side
skeleton in ${CLAUDE_PLUGIN_ROOT}/snippets/dispatch-brief.md (read it by path) plus the target
agent's family delta; never inline that file verbatim into a hard-leaf brief.
Sequence invariant (non-negotiable order). Pipeline order is
recon/classify -> (conditional) solution-design -> Plan Mode gate -> odoo-coding execution.
NO odoo-coding / odoo-coder / odoo-backend-coder / odoo-frontend-coder dispatch may happen
before the Plan Mode gate. When a design trigger fires for a commit/module,
odoo-solution-design MUST complete and its design doc MUST be approved BEFORE the gate for that
commit/module. A tiny/mechanical change (design-trigger skip rows) bypasses design but STILL
passes through the gate. Design is conditional; Plan Mode is not.
P0 - Intake / resolve [STOP if open_questions non-empty].
Goal: turn the free-text ask into structured inputs - infer series, map to the OSM profile,
auto-detect candidate modules, propose a cluster, CLARIFY scope. Dispatch 1x intake subagent
(sonnet). Brief: (1) read the CURRENT BRANCH NAME and infer the Odoo series (17.0-* -> 17.0);
cross-check against the MAX manifest version series on disk and raise a disagreement as an
open_question rather than trusting the branch name alone. EXCEPTION (Viindoo Standard/Internal
profile, per ${CLAUDE_PLUGIN_ROOT}/snippets/upg-conventions.md: manifests carry a SHORT version
with NO series prefix): skip the cross-check and resolve the source series from branch + profile -
do NOT raise a false disagree; (2) map to the OSM profile via set_active_version +
list_available_profiles + profile_inspect; report repos + module set; (3) auto-detect
CANDIDATE MODULES by scanning __manifest__.py for a version major series LESS THAN target
(e.g. 16.0.x when target is 17.0) AND for modules depending on such a stale module;
installable: False is a weak hint only. For Viindoo short-form manifests (no series prefix) the
version scan yields nothing - detect by profile membership + branch series instead; (4) determine
SOURCE version from the matched profile / manifest version and TARGET from the NL ask (or next
major if implied); (5) if MODULE SCOPE is not explicit, do NOT guess - return the candidate list +
a proposed cluster (seeded from the dependency closure of the confirmed targets, not naming/path
proximity) as open_questions. Emit dependency hints from manifest depends.
Output: intake.md - {resolved_series, matched_profile, source_version, target_version,
candidate_modules[], proposed_cluster[], dependency_hints, open_questions[]}.
Gate: if open_questions non-empty, present the candidate list + proposed cluster, ask the
user to confirm/narrow scope, then resume P1.
P1 - Recon [graph + deprecation + diff + transitive-symbol, parallel].
Goal: build the dependency DAG; get per-module deprecated-symbol fix list + platform API
delta + a transitive symbol-survival survey grounded at target. Four parallel dispatches:
(a) 1x Explore (sonnet) reads each __manifest__.py depends -> emits {module, depends[]}
for every module in the confirmed cluster -> orchestrator topo-sorts to leaves-first order
(cheap, deterministic); (b) odoo-deprecation-audit (via Skill tool, sonnet) for source +
TARGET version + module list (it runs the TARGET-version survival pass); (c) odoo-version-diff
(via Skill tool, sonnet) for source->target delta; (d) P1d Transitive Symbol Survey
(Explore, sonnet, read-only) - scans cluster source for every symbol referencing an external/core
dep and grounds each at target, emitting blockers[] that gate P3/P4 (full brief: phase-detail § P1d).
Output: graph.md (DAG + topo order), deprecation.md, version-delta.md, transitive-symbol-survey.md.
Assert DAG is acyclic: if a cycle is found, surface the cycle edges as
DONE_WITH_CONCERNS and ask the user to break the cycle before P2 proceeds (do not
hard-fail - cycles exist in real custom clusters and require human resolution).
Assert all dependencies exist at the target.
P2 - Core-absorption comparison [per module, dep order, parallel within waves].
Principle: Upgrading a module to a newer version of Odoo mean code / docs upgrading. It should be considered
a new feature development without requiring:
- any backward compatibility with previous Odoo version or the module itself
- any migration script.
- any version bump.
Goal: for each module in dep order, decide DELETE-absorbed / KEEP / REWRITE(api) /
REWRITE(model) / MERGE / SPLIT by comparing custom behavior vs target-version core.
Per module in dep order (topo order from graph.md): dispatch 1x odoo-diff-comparator
(sonnet, opus for cluster-wide) + invoke odoo-gap-analysis (via Skill tool, sonnet) in
parallel. Modules at the SAME DAG depth (same wave) may be dispatched in parallel; a module
is NOT dispatched until its in-cluster dependencies have finished P2. Dispatch concurrency
follows ${CLAUDE_PLUGIN_ROOT}/skills/_shared/concurrency-guard.md Mode B.
When the CHP capability probe is positive (Agent Team mode on), TaskCreate one task per dispatched
work-item, inject TASK_ID + REPLY_TO: <this skill's current orchestrating context> (main when the
main-context driver invoked this skill; do NOT hardcode a literal main if running nested inside a
non-lead agent) + NOTIFY: into each teammate brief,
poll TaskList/TaskGet for status, and read each result from the teammate's SendMessage push (NEVER
from the .output transcript) - per ${CLAUDE_PLUGIN_ROOT}/snippets/agent-team-protocol.md. When
off, dispatch + collect as today.
Comparator brief: "compare the module's nghiệp vụ / ý đồ / expected outcomes /
acceptance criteria against target-version CORE (<target>). Classify each feature per
${CLAUDE_PLUGIN_ROOT}/skills/odoo-modules-upgrade/references/upg-classification-table.md;
if the WHOLE module is provided by core, return verdict=DELETE-absorbed with the core
module/feature that replaces it."
Output per module: absorption/<module>.md - {per-feature classification, evidence
(OSM citation), proposed action, deferred_work items (below)}.
P2 also reconciles version-anchored deferred work [same delegated per-module read - no
separate scan or dispatch]. The odoo-diff-comparator dispatch above already reads this
module's FULL source in its own context; that SAME read additionally scans every in-scope file
(.py incl. __manifest__.py, tests, .xml, .js, .csv, .po, README/docs) for a
case-insensitive marker set - TODO/todo/Todo/ToDo/@todo, FIXME, XXX, HACK - and
classifies each hit against the target series X (<target_version>): anchored to X or to an
already-passed LOWER version -> DUE (execute NOW, in THIS upgrade); anchored to a HIGHER
future version -> DEFERRED (leave it untouched); no parseable version anchor ->
UNANCHORED - record it and flag it for the human, never silently absorbed into DUE nor
silently dropped. The comparator returns this as a compact deferred_work block inside
absorption/<module>.md (never raw grep output) - the orchestrator only CONSUMES that structured
list; it never greps module source itself for this. For a module that survives (KEEP/REWRITE/
MERGE/SPLIT), every DUE item becomes a REAL upgrade work-item: P4 folds it into that module's SAME
implement -> P4b review -> P5 test path, not a side note (see § P4).
P2b - Hard-call design [mandatory route-out for specific verdicts].
Route to odoo-solution-design (odoo-solution-architect opus) when P2 returns ANY of
the following verdicts (tied to the verdict enum, not to a fuzzy "ambiguous" judgment):
MERGE (any merge, regardless of apparent clarity)
SPLIT (any split)
RECONCILE (always - data-divergence or new-feature wire-in; the SSOT/wire-in choice is architectural)
MIXED (always - route the whole module to design to resolve the mixed per-feature verdicts)
REWRITE(model) where at least one field type changes
DELETE-absorbed (with risk) (any absorption verdict carrying risk flags)
REWRITE(api) when the adaptation changes the module's PUBLIC MODEL SURFACE (adds/removes/
retypes a stored field or changes a public method signature on an extended core model), OR
touches > 5 call sites of the changed API, OR spans >= 2 modules.
KEEP (with adaptation) when the adaptation meets the odoo-solution-design § When to invoke
non-trivial criterion (overrides create/write/unlink, a method with >=3 override-chain
entries, a cross-model computed chain, or a multi-company/branch change).
These verdicts represent architectural or data decisions that must NOT be auto-decided by
the comparator or orchestrator. A trivial REWRITE(api)/KEEP - a localized deprecation-fix or
call-site swap confined to <= 5 call sites in 1 module with no public-surface change - SKIPS
design and goes straight to P3. DELETE-absorbed (no risk) and OBSOLETE never route (the module
is removed, not adapted). Reuse the non-trivial criterion from
${CLAUDE_PLUGIN_ROOT}/skills/odoo-solution-design/SKILL.md § When to invoke - do NOT invent a
third definition. Full table: references/upg-phase-detail.md § P2b.
Emit the Continuation Contract and YIELD. On re-entry, read design_doc from the returned
contract's inputs, record it, and proceed to P3.
P3 - Plan gate [Plan Mode].
Goal: human approves the per-module DELETE/KEEP/REWRITE/MERGE/SPLIT table + install
order BEFORE any code is written or module deleted.
Plan-Mode enter/exit is the SHARED SSOT
${CLAUDE_PLUGIN_ROOT}/snippets/planning-gate-contract.md § Plan-Mode enter/exit + plan_mode_active
- upgrade REUSES it for this gate rather than defining its own:
EnterPlanMode iff
plan_mode_active is absent/false, then write the plan CONTENT (upgrade-specific, authored here,
NOT routed through odoo-planning): per-module
classification table (DELETE -> list the core feature that replaces each AND inline the
behavioral-equivalence summary from absorption/<module>.md - the proof that every
override is equivalent or no-op; REWRITE/MERGE/SPLIT -> adapt tier + design link);
topo-sorted adapt order (leaves first); manifest bumps required; design-doc links for
any P2b module. For every surviving module (KEEP/REWRITE/MERGE/SPLIT), inline its DUE
version-anchored deferred-work count and list every UNANCHORED marker from
absorption/<module>.md deferred_work for human awareness (never silently absorbed or
dropped).
For EVERY module with a DELETE verdict, the plan table MUST inline:
(a) the absorbing_core_feature or OBSOLETE reason,
(b) the behavioral-equivalence proof summary (from signal #5 in absorption/<module>.md),
(c) a per-DELETE explicit confirmation prompt: "Confirm DELETE ? [y/N]"
(separate from the overall plan approval).
ExitPlanMode on approve: the user approves in the Plan Mode UI AND provides per-DELETE confirms.
These per-DELETE [y/N] confirms plus the behavioral-equivalence proof are an upgrade-specific
irreversible-delete human gate layered ON TOP of the shared plan approval - never replaced by it.
After approval: write plan.md (RECORD of the approved plan; SSOT for P4+).
This gate covers the irreversible DELETE decisions. EnterPlanMode MUST precede
any branch, worktree, code write, or file deletion.
P4 - Adapt [per module, dep order, child worktrees].
Goal: make each module installable + working at the target series.
Create the JOB-tier integration worktree: invoke the git-toolkit:git-ops skill (via the Skill
tool) to add a worktree (branch upg/<src>-<tgt>-<cluster>, worktree <path>/upg-integration, base <work-base>).
Per module in dep order: invoke the odoo-coding skill (via the Skill tool) at the ADAPT tier
(per ${CLAUDE_PLUGIN_ROOT}/skills/odoo-modules-upgrade/references/upg-triage-table.md)
in a child worktree off integration. odoo-coding owns the backend/frontend split, coder fan-out
(via its odoo-coder per-module coordinator), and model, and grounds any ported OWL/QWeb/SCSS against
${CLAUDE_PLUGIN_ROOT}/skills/_shared/odoo-frontend-fidelity.md (do NOT dispatch raw odoo-coder,
odoo-backend-coder, or odoo-frontend-coder).
For DELETE-absorbed and OBSOLETE modules: invoke odoo-coding to run the dangling-reference
sweep first (grep repo for model names, XML IDs, group xmlids, env.ref targets), then
invoke the git-toolkit:git-ops skill (via the Skill tool) for the directory removal, staging, and commit in the child worktree
(op: rm -r module dir + stage deletion + commit -s; confirmed: yes - user confirmed DELETE
at P3 Plan Mode gate; commit message per § Git / PR conventions absorbed/obsolete-delete form);
drop it from every depender's depends in their manifests.
For KEEP/REWRITE/MERGE/SPLIT: prepend this module's blockers[] from P1d
transitive-symbol-survey.md as a PREEMPTIVE FIX LIST, apply the breaking-change catalog from
${CLAUDE_PLUGIN_ROOT}/skills/odoo-modules-upgrade/references/upg-classification-table.md,
the per-module deprecation fix list from P1, and this module's DUE version-anchored
deferred-work items from P2's absorption/<module>.md deferred_work block (executed NOW as
REAL work-items in the SAME implement -> P4b review -> P5 test path, not a side note; DEFERRED
and UNANCHORED items stay untouched in source); flip installable: False -> True; do NOT bump the
manifest version (keep the existing short form); set auto_install/application only when a
manifest-comment breadcrumb directs (NO auto-detect of "bridge").
The commit inside each child worktree is produced by odoo-coding via the git-toolkit:git-ops
skill (its coders write files, never run git); message per § Git / PR conventions.
Converge each child worktree back to integration (serialized); remove child worktree.
Principal-checkout-lock: NEVER check out or switch the principal checkout yourself.
Materialize any needed branch by invoking the git-toolkit:git-ops skill to add a worktree.
P4b - Code-review loop [odoo-code-review -> odoo-code-reviewer; fix via odoo-coding; cap 3].
After P4 adapts the cluster into the integration worktree, dispatch odoo-code-review (via the
Skill tool) per adapted module IN DEP ORDER (leaves first), scoped to that module's adapt diff
(TARGET: worktree:<path>/upg-integration, module-scoped; DELETE-absorbed/OBSOLETE modules have no
adapt diff - skip them). On any CRITICAL/HIGH finding for a module, dispatch odoo-coding (same
ADAPT tier) to fix to root cause, then RE-REVIEW that module; MED/LOW are recorded for the P7 PR
review, not blocking. Cap at 3 review->fix iterations per module: a 3rd iteration still CRITICAL/HIGH
STOPS and escalates BLOCKED per ETHOS #7. Proceed to P5 ONLY when every adapted module's review
returns no CRITICAL/HIGH. Full delegation - the orchestrator dispatches reviewer + fixer, never
reviews or fixes inline. Write <module>: reviewed in checkpoint.json. Brief + loop protocol:
references/upg-phase-detail.md P4b. This is the in-pipeline review; the final pre-merge dep-order
review stays at P7 (two review points total).
P4b acceptance hand-off (consumption clause). Each dispatched odoo-code-review's
Continuation Contract MAY carry a next: odoo-acceptance entry - its Phase A.5 emits this
whenever a module's render_check_set reaches beyond that module (dependents bind a changed
symbol). READ it, but do NOT act on it per module here: it is superseded by ONE cluster-wide
dispatch at P5.8 below, which reuses P1's graph.md reverse-closure instead of re-deriving it
per module. This is the clause that was missing before - without it the hand-off was silently
dropped on the floor.
P5 - Install + test gate [ephemeral instance, wave-by-wave, demo=on].
Goal: prove the whole cluster installs + tests green on a fresh target DB, bottom-up
wave by wave (one wave = one DAG depth level, leaves first). Installing wave-by-wave
localizes failures and allows resume to skip proven waves.
Run the instance with demo=on (no separate framework-validation phase): a module that
flips installable: False -> True is scanned by the target's FULL suite for the first time -
from v18 base.TestInvisibleField + hr.TestSelfAccessProfile run there and need demo data
(demo default is version-keyed - F0 ${CLAUDE_PLUGIN_ROOT}/snippets/odoo-version-pivots.md;
gate stays demo=on regardless). The P4b review MUST cover ACL / .sudo() for every create/
write/compute override on a widely-used core model. Cross-ref
${CLAUDE_PLUGIN_ROOT}/skills/odoo-modules-upgrade/references/runbot-parity-checklist.md.
Dispatch odoo-instance (via Skill tool, L2 human gate applies). Create the instance
once; then for each wave: dispatch init for that wave's modules, then run-tests for that
wave. Record per-wave green in checkpoint.json (status installed) and install-test.md.
On failure in a wave, dispatch odoo-backend-debugger or odoo-ui-debugger to diagnose
to root cause; feed the diagnosis back to P4 for the affected module. Resume P5 from the
failing wave (skip already-green waves per checkpoint.json). Loop until all waves green.
Output: install-test.md - {per-wave + per-module install ok?, test result, root-cause if red}.
P5.7 - i18n reconcile [gated-on by default; auto-SKIP].
Goal: keep translations intact across the upgrade WITHOUT regenerating them. Gated-on by
default; AUTO-SKIP when the cluster changed no translatable surface (no add/remove/rename of
a translatable field, label, view string, or selection). When it runs, wire the existing
odoo-i18n skill (no new i18n logic) against a fresh instance with the existing .po loaded:
re-export -> git-ops diff-review each .po against its committed version + adjudicate every loss
(NEVER blind-regenerate - a fresh-DB export with no load step destroys existing msgstr) ->
hand-translate only the residual untranslated entries -> reload with -u. Detail: phase-detail § P5.7.
P5.8 - Acceptance (odoo-acceptance) stage [MANDATORY, cluster-wide, narrow escape only].
Goal: prove the whole upgraded cluster works end-to-end on a real running instance/UI - the SAME
acceptance rigor new-module development applies, so an upgrade is not held to a lighter bar just
because it is an upgrade. P5's ephemeral-DB install+test proves the code loads and the unit/
integration suite passes; it does NOT prove the cluster behaves correctly for a real user across
roles/state/search - closing that gap is this stage's job.
Compute the cluster-wide verify scope by invoking ${CLAUDE_PLUGIN_ROOT}/snippets/acceptance-scope.md
directly - do NOT re-derive the reverse-closure: changed_set = every surviving (KEEP/REWRITE/
MERGE/SPLIT) module in plan.md; the dependent-module reverse-closure comes for free from P1's
graph.md DAG.
Invoke the odoo-acceptance skill (via the Skill tool) ONCE for the whole cluster (never per
module). Fill the dispatch brief per ${CLAUDE_PLUGIN_ROOT}/snippets/dispatch-brief.md (read it by
path): INPUTS = the changed_set above, scope_hint = graph.md + absorption/*.md,
odoo_version = target series; INSTANCE_HANDLE from P5 if still live (reuse - never
re-provision; else pass none provisioned and odoo-acceptance still scopes + plans its oracle,
then emits NEEDS_NEXT -> odoo-instance). ACCEPTANCE (by pointer) = each surviving module's
business behavior recorded in absorption/<module>.md and any P2b design doc's §9 - NEVER a
pre-built oracle: odoo-acceptance authors its OWN independent oracle at its own Phase 1 from
that requirement/intent, the same oracle-independence guarantee the new-module lifecycle
protects. Do NOT hand it the implementation.
MANDATORY - narrow escape only. An upgrade cluster's surviving modules have in-cluster
dependents by construction, so the blast-radius bar this stage exists for is met almost always -
this is NOT an opt-in hand-off. Skip it ONLY when graph.md proves the cluster is a true
dependency leaf with ZERO in-repo dependents AND no behavioral surface (no views, no models any
other module consumes) - record that proof explicitly in install-test.md; never skip silently.
The upgrade is not DONE until this stage returns ACCEPTED (PASS), or the narrow-escape condition
above is explicitly met and recorded.
Gate tier: L2 (human) - present the acceptance verdict (or the recorded narrow-escape) ALONGSIDE
the P6 sign-off below so the human sees ONE combined decision, not a surprise extra step later.
Output: <ISOLATE_DIR>/qa/<slug>-acceptance-report.md (odoo-acceptance's own artifact), referenced
from install-test.md.
P6 - Gate [STOP, human sign-off].
Present plan.md + absorption/* summaries + install-test.md (including the list of
DELETED-absorbed modules + their reasons) + the P5.8 acceptance verdict (or its recorded
narrow-escape). Wait for human sign-off before the PR.
P7 - PR + review [human merge].
Pre-PR checklist (extends P6 sign-off): run the Runbot parity gates
(${CLAUDE_PLUGIN_ROOT}/skills/odoo-modules-upgrade/references/runbot-parity-checklist.md), then
add a convention-compliance pass (manifest version-form + always-invisible XML comment + rename
via old_technical_name - per ${CLAUDE_PLUGIN_ROOT}/snippets/upg-conventions.md), a perf-lens
pass (no per-record mapped() aggregate on a high-volume model - use grouped _read_group), and
an i18n pass (P5.7 ran or was correctly auto-SKIPPED).
Push branch and open PR: invoke the git-toolkit:git-ops skill (via the Skill tool) to push the
branch, then to create the PR - resolve upstream org/repo and base from git remote get-url origin.
No cluster-squash (per-module consolidation is allowed - see references/upg-phase-detail.md § Commit consolidation).
Delegate a dep-order code review of the integration worktree before human merge (via the
plugin's review capability, passing worktree:<path>/upg-integration and asking it to
review modules in dependency order). Wait for human merge.
Hard rules
- Principal-checkout-lock. NEVER check out or switch the principal (main)
checkout off its branch. Materialize any needed branch by invoking the
git-toolkit:git-ops skill to add a worktree.
- Plan Mode before any delete or code write. The P3 Plan Mode gate (the shared
${CLAUDE_PLUGIN_ROOT}/snippets/planning-gate-contract.md Plan-Mode gate, reused for this
approval) covers the irreversible DELETE decisions; no directory removal, no code changes, no
worktree branch until P4 post-gate.
- No cluster-squash; per-module consolidation allowed. Never collapse the whole cluster
into one opaque commit - the per-module commit messages are the upgrade record. Consolidating
a single module's WIP/fixup commits into ONE clean commit per module IS allowed (capability:
references/upg-phase-detail.md § Commit consolidation - invoke git-toolkit:git-ops;
tree-identity verified via git diff --quiet). Commit message formats: § Git / PR conventions.
- ONE PR per cluster. All modules in one PR, reviewed in dep order.
- Code-level only; migration scripts NEVER inline; BLOCKED on data-at-risk. The workflow is
CODE-LEVEL only. This skill NEVER writes migration scripts - inline, as a P4 step, or otherwise.
If a module genuinely needs a data migration script, the pipeline reports BLOCKED and routes
the case to
odoo-data-migration; the upgrade itself does not emit the script.
Data-at-risk detection: if a candidate module is currently installable: True
AND it defines stored non-computed fields OR has noupdate="1" data records, the P2
comparator flags data_at_risk: true in absorption/<module>.md. A data_at_risk
module that receives a REWRITE(model) or DELETE verdict MUST ESCALATE: the pipeline
reports BLOCKED status and requires explicit human decision before proceeding code-only.
The fresh P5 ephemeral DB cannot detect data loss - proceeding code-only on a
data-at-risk module is a production-incident risk.
- DELETE-absorbed = delegated directory removal + dep cleanup + commit message, with two mandatory gates.
A module wholly absorbed by core is removed entirely, not set to
installable: False.
The commit message carries the reason (which core feature replaces it). No directory removal is
permitted without: (a) signal #5 behavioral-equivalence proof recorded in
absorption/<module>.md (all overrides enumerated + each proved equivalent or no-op);
AND (b) an explicit per-DELETE human acknowledgment at P3, issued as a SEPARATE
confirmation step, distinct from the overall plan approval. A single "approve plan"
does NOT satisfy (b) - each DELETE row requires its own explicit confirm.
For OBSOLETE verdict (module is moot at target, not absorbed by a named feature): the
commit message uses upg: delete <module> - obsolete at <tgt> (<reason>) - do NOT
invent a fake absorbing_core_feature.
- Acceptance is mandatory (narrow escape only). P5.8 dispatches
odoo-acceptance ONCE for
the whole surviving cluster before the P6 human gate - mirroring the rigor a new module build
gets. This is NOT opt-in: skip it only when graph.md proves the cluster is a true dependency
leaf with zero in-repo dependents and no behavioral surface, and record that proof - never skip
silently. The upgrade is not DONE without an ACCEPTED verdict or a recorded narrow-escape.
Checkpoint / resume
The pipeline writes a progress ledger at <ISOLATE_DIR>/modules-upgrade/<src>-<tgt>-<cluster>/checkpoint.json
after each module completes a phase. Schema:
{
"<module_name>": "pending | absorbed | designed | adapted | reviewed | installed | done"
}
Status progression:
pending - initial state for all modules at P0.
absorbed - P2 comparison complete, verdict recorded in absorption/<module>.md.
designed - P2b design doc received (only for modules routed to design).
adapted - P4 code changes committed and merged to integration worktree.
reviewed - P4b code-review loop returned no CRITICAL/HIGH for the module.
installed - module's wave passed P5 install + test green.
done - module fully processed (installed green + verified).
Resume behavior (per-phase skip rules - prevents overwriting completed work on crash/resume):
- P2 skips modules with status in {absorbed, designed, adapted, reviewed, installed, done}.
- P4 skips modules with status in {adapted, reviewed, installed, done}.
- P4b skips modules with status in {reviewed, installed, done}.
- P5 skips modules at the wave level with status in {installed, done}.
On crash or credit exhaustion, restart the orchestrator: it reads
checkpoint.json
and resumes from the first module that has not yet reached the target phase's skip-threshold.
P5 per-wave records green in the ledger so re-runs do not re-install proven waves.
Cluster / dependency handling
- DAG built ONCE in P1 by a subagent; the orchestrator only topo-sorts it (cheap).
- P2->P4 run per module in dep order (leaves first).
- P5 installs bottom-up WAVE BY WAVE (one wave per DAG depth level), recording per-wave
green so failures localize and resume skips proven waves. ONE PR for the cluster (P7).
Git / PR conventions
Git delegation contract: ${CLAUDE_PLUGIN_ROOT}/snippets/git-delegation.md.
- Branch:
upg/<src>-<tgt>-<cluster> (e.g. upg/16.0-17.0-l10n_vn).
- Integration worktree: invoke
git-toolkit:git-ops to add a worktree (branch
upg/<src>-<tgt>-<cluster>, worktree <path>/upg-integration, base <work-base>).
- Commit messages (adapt):
upg: <module> <src>-><tgt> - <KEEP|REWRITE|MERGE|SPLIT> <summary>.
- Commit messages (absorbed delete):
upg: delete <module> - absorbed by core <core-module/feature> in <tgt> (no custom delta remains).
- Commit messages (obsolete delete):
upg: delete <module> - obsolete at <tgt> (<one-line reason>).
- Push to fork: invoke
git-toolkit:git-ops to push branch upg/<src>-<tgt>-<cluster> to the fork
remote (resolve fork remote URL from git remote get-url origin or a dedicated fork remote).
- PR: invoke
git-toolkit:git-ops to create the PR (resolve upstream org/repo and base from
git remote get-url origin; no cluster-squash - per-module consolidation allowed -
see references/upg-phase-detail.md § Commit consolidation).
MCP tools
Pick the right tool first. Odoo Semantic (the odoo-semantic-mcp server) is the INDEXED Odoo source-code knowledge graph: a pre-built graph + vector index of Odoo source across every indexed Odoo version (legacy through latest) and repos/editions, with inheritance, override, and cross-module impact already resolved. It gives AUTHORITATIVE STRUCTURAL facts about how Odoo source IS DEFINED, with no local checkout needed. Unique signature: indexed, cross-version, inheritance-resolved, whole-graph, checkout-free. It is a STATIC index with NO runtime/live data.
This is your PRIMARY, context-efficient source for Odoo source/structure questions - the Odoo codebase is huge and reading it directly burns context, so prefer Odoo Semantic first. Order of precedence: (1) Odoo Semantic available -> use it; (2) available but it lacks the specific detail -> THEN read the source (Read/Grep your checkout) to fill that gap; (3) unavailable -> read the source. Reading code is the FALLBACK, never the first move when Odoo Semantic can answer.
Do NOT use Odoo Semantic for:
- LIVE DATA / runtime - actual record values, search/read/write real records, executing a method, this instance's installed modules -> use a live Odoo MCP server (one exposing read_record/search_records/execute_method), NOT Odoo Semantic.
Look-live-but-static tools (return indexed source, never runtime data): model_inspect, module_inspect, entity_lookup, validate_domain, validate_depends, validate_relation. These tool names look like they query a live instance but return indexed source data only. If you need live records, Odoo Semantic is the wrong server.
Session bootstrap (call once at session start):
set_active_version(odoo_version='17.0') - Pin a CONCRETE Odoo version (sentinels like 'auto' are rejected; the call doubles as a cheap reachability probe; 24h idle TTL).
Primary tools:
api_version_diff - Structured diff of an API symbol or scope across two Odoo versions: new, changed, removed, deprecated items.
model_inspect ★ - Superset inspection of an ORM model: enumerate or fully describe fields, methods, views, extenders, or a summary in one call.
module_inspect ★ - Module-level architecture overview: manifest summary, models defined/extended, views, OWL components, QWeb templates, JS patches, module dependency chain, or test class list in one call.
check_module_exists - Verify module availability, edition (CE/EE/Viindoo), and cross-version presence.
find_deprecated_usage - Scan the indexed codebase for usages of deprecated API patterns.
entity_lookup ★ - Single-entity drill-down by ID: field, method, or view with full inheritance chain and source module.
lookup_core_api - Verify Odoo core API symbol signature, status (stable/deprecated/removed), and replacement.
validate_depends ⊕ - Validate compute method's @api.depends('a.b', ...) paths; flag id and suggest typos.
cli_help - Look up odoo-bin subcommand flags, their status, and replacement for deprecated flags.
list_available_profiles ☆ - Enumerate which tenant profiles exist in the server index.
profile_inspect - Profile-level introspection discriminator (ADR-0028): inspect a tenant profile's composition in one call.
Standalone-first fallback
When OSM is unreachable, the pipeline degrades but does not stop. P0 intake derives
series from the branch name + reads manifest files directly (no OSM needed). P1 recon
reads __manifest__.py depends from disk for the DAG; odoo-deprecation-audit and
odoo-version-diff each have their own standalone fallback (disk-fallback-protocol).
P2 comparator falls back to disk reads of the source module + the target checkout per
${CLAUDE_PLUGIN_ROOT}/snippets/disk-fallback-protocol.md. Label all artifacts
grounded: local-source (not OSM-indexed). The install+test gate (P5) is unaffected.
Continuation Contract
When the run finishes (or pauses at a gate), append a Continuation Contract block per
${CLAUDE_PLUGIN_ROOT}/snippets/continuation-contract.md (status / produced / next).
produced lists intake.md, graph.md, deprecation.md, version-delta.md,
transitive-symbol-survey.md, absorption/*.md, plan.md, install-test.md,
<ISOLATE_DIR>/qa/<slug>-acceptance-report.md, checkpoint.json, and the PR URL.
When P2b routes a module out to design, next: odoo-solution-design with the Continuation
Contract payload and the run YIELDS. Additive output for the run-harness - does not change
anything produced above.
Note: this workflow has TWO review points - the P4b in-pipeline code-review loop (per module,
dep order, fix-until-clean, findings folded into the module rows of install-test.md) and the
P7 final dep-order PR review (pre-merge). Both are required; neither substitutes for the other.