Spec-Driven Development pipeline for Odoo: 15 tools take a request from an immutable spec through architecture to module code or live configuration, then require a persisted verdict backed by evidence from a real Odoo instance, with fail-closed gates, human-approved batches in a declared environment, Odoo's own CSV/Excel importer, and a journal that reverses the data written through the plugin.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-odoo-sdd
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:fhidalgodev/dsh-odoo-sdd
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
Spec-Driven Development for Odoo
⚡ Summary
dsh-odoo-sdd turns DeepSeek Harness into a specification-driven (SDD)
Odoo development pipeline. Two ideas hold it together:
- Closed feedback loop — the agent installs and upgrades modules, reads the
server traceback and retries against a real, running Odoo instance. The
plugin never starts Docker or
odoo-bin: you point it at an instance you already have (dev/staging) through a gitignored.env, and the tools speak plain JSON-RPC. - Pipeline safety — every phase persists to disk, the gates are fail-closed
on an explicit
APPROVEDmarker, three consecutive failures force a root-cause diagnosis, verify/fix iterations are capped,stop.mdhalts everything, and verdicts are honest: a failed verification is persisted as FAILED and can never be reported as success. - No change without a spec — a request that arrives after the pipeline closed ("now also change X" in the same chat) is a new change, so it gets its own (small) spec or an explicit waiver from you. The guard covers instance mutations and source edits alike, and says which of the three ways out is available instead of quietly editing.
[!NOTE] What it is not: an infrastructure orchestrator, a credential manager over chat, or an auto-committer. It writes no commit and never asks you for a password.
Requirements
| Need | Why |
|---|---|
| DSH ≥ 0.1.2-rc.1 on Node ≥ 20 | the plugin mounts as a Cordis bundle and uses the tools service |
| An existing Odoo instance reachable over HTTP(S) | the loop needs a real server to install into and read tracebacks from |
| A disposable dev/staging database | verification installs modules and writes test data |
| (optional) a Playwright browser tool | only for the UI layer of verification and for narrated-manual captures; without it those scenarios are marked for manual checking |
(optional) playwright-core ≥ 1.63.0 |
ships with the plugin as an optional dependency; the capture motor drives Chromium, Chrome or Edge, and answers NEEDS_BROWSER with the one-line remedy when none is present |
🔭 How it works
flowchart TD
A(["Idea or request"]) --> C1["1 CLARIFY<br/>interview and security questions"]
C1 --> R2["2 READ_SPEC<br/>immutable spec.md"]
R2 -->|APPROVED| A3["3 ARCHITECTURE<br/>models, views, security, test plan"]
A3 -->|APPROVED| W4["4 WRITE_CODE<br/>module source and OCA docs"]
W4 --> V5["5 VERIFY<br/>static, install, RPC, UI"]
V5 -->|PASSED| D9(["handoff.md - DONE"])
V5 -->|FAILED| F6["FIX_LOOP<br/>root cause, max 5 iterations"]
F6 --> V5
W4 -.-> M7["odoo_module install or upgrade"]
M7 -.->|traceback| E8["odoo_errors"]
E8 -.-> F6
The specification is the single source of truth and it is immutable: the code adapts to the spec, never the other way around. Gates are human-answered by default, and can be delegated to a human-proxy agent when you choose the autonomous mode.
✨ Key features
- 🎯 Spec before code, always.
spec.mdcarries numbered acceptance criteria;DONEis unreachable without a persistedPASSEDverdict on disk. - 🧬 Stable requirement identity. Every requirement block gets a stable
REQ-<AREA>-NNid (section slug + ordinal) plus a fingerprint committed toreq-baseline.json. Rewording a requirement under an existing id is DRIFT: the advance is refused until you either revert it or accept it explicitly (sdd_phase operation=baseline), so a plan or a test never silently refers to a requirement that no longer says what it said. - 🔗 Bidirectional coverage. Annotate code and tests with
# REQ-<AREA>-NNandodoo_validate module_dir=… spec_dir=…checks both directions — requirements with no implementation, and orphaned annotations whose id the spec no longer declares. Floors:must/shallblock at 1.0,shouldis reported at 0.8,mayis informational. Each requirement lands on the laddercovered / untested / test-only / unimplemented. - 🔍 Ambiguity lint (advisory).
mark_spec_loadedreports unquantified adjectives, vague quantifiers,TBDplaceholders, passives with no actor and and/or compounds, each with a concrete rewrite — advice, never a gate. - 🧪 Tests that can actually fail.
odoo_testschecks the suite an install log cannot see: a test file Odoo never imports (nothing loads it), a test that asserts nothing, a swallowed exception,cr.commit()breaking the per-test rollback,time.sleep, skips — and maps every acceptance criterion to the test that mentions it.scaffoldwrites RED stubs (they raise until implemented) instead of vacuous passes. - 🔴 RED before GREEN on a bug fix. In
mode=bugthe pipeline refuses to leaveWRITE_CODEuntiltest-plan.mdrecords the reproduction that FAILED first (red (evidence: …), orpass (red first: …)once fixed) — a regression test that never failed proves nothing. - 🔁 A real feedback loop.
odoo_module installreturns the server's own output or traceback;odoo_errorsreadsir.logging; failures become a persisted FAILED verdict instead of a hopeful summary. - 🧾 Not only modules. The same machine also runs a functional spec
(
mode=functional): configure a live instance and load data in human-approved batches, with Odoo's own importer for CSV/Excel, and close with a runbook a person can repeat. → The functional path - 🔒 Credentials are not consent. The first socket of a project needs an
explicit human authorization bound to
url + db + user(.sdd/grants.json). - ⏪ Rollback that tells the truth. Checkpoints snapshot files and journal
every
odoo_executepre-image, and a restore always reports the files created after the checkpoint. What it cannot undo, it says so. - 🧱 Documentation as a gate.
odoo_docsproduces the OCAreadme/fragments, the Appsindex.htmland a mandatory changelog entry — and it works on an existing module with no spec, no phase and no instance. - 🧪 Instance-free static layers.
odoo_validate(structure + ACL coherence) andodoo_security_scan(raw SQL,sudo(),auth="none", QWebt-raw…) give findings withfile:linebefore anything is installed. - 📖 Manuals written like a story, checked like code.
odoo_manualreads the functional canon of a model from the instance (buttons and the states that show them, required fields, notebook pages, menus, access rules, the exactraise UserErrorsites), plans a storyboard, lints the prose against the canon, captures annotated screenshots read-only by default, and renders a manual a person can follow — a labelled screenshot per instruction, with the recovery path when the canon says the button can raise. - 🧑⚖️ Policy the model cannot relax. Allowlist, guards and delegation mode all require native human approval to change.
- 🤖 Supervised or autonomous. The same pipeline runs with a human answering
each gate, or with a human-proxy agent and unattended goal rounds until
DONEorBLOCKED. - 📁 Specs where you want them. Keep them beside each project, or collect every project's specs in one folder you can search.
- 🖥️ Linux, macOS and Windows. Paths, atomic writes and
.envpermissions are handled per platform and tested on Windows in CI.
🚀 Quick start
1. Install into a profile
dsh plugin --profile web add dsh-odoo-sdd
That installs the published package from the npm registry — no clone, no
build, nothing to compile on your side. dsh plugin is a thin pnpm forwarder:
it runs pnpm add inside the profile directory and then registers the bundle
(dsh.profile.bundles). Two consequences worth knowing:
- pnpm must be on your
PATH(dsh pluginreports it when it is not). - Any pnpm spec works, so you can pin a version:
dsh plugin --profile web add dsh-odoo-sdd@0.18.0.
Prefer plain npm — a project that depends on the plugin, or a CI job?
npm install dsh-odoo-sdd # 0.18.0, published with a provenance attestation
[!IMPORTANT] Restart DSH and refresh the browser tab after installing. Client-side changes (the Odoo SDD settings panel) load from the installed package.
Working on the plugin itself? Install the checkout instead. lib/ is build
output and is not committed; npm install builds it through the prepare
hook, and you can always ask for it explicitly:
git clone https://github.com/fhidalgodev/dsh-odoo-sdd && cd dsh-odoo-sdd
npm install # devDependencies: typescript, then prepare builds lib/
npm run host:deps # optional peers, needed to compile (no-save)
npm run build # emits lib/ — required, package main is lib/index.js
dsh plugin --profile odoo add .
[!NOTE] A git install runs that
preparebuild on the consumer's machine, and pnpm blocks dependency build scripts until they are allowed: the command tells you the exact key to add underallowBuildsin the profile'spnpm-workspace.yaml. A registry install needs none of that — the tarball already shipslib/.
dsh plugin add records the bundle in the profile's package.json
(dsh.profile.bundles), and the package ships a Cordis patch
(cordis.patch.yml) that inserts its own row — so there is no manual
composition step. dsh --profile <name> --dump-config prints the composed tree
without booting anything.
2. Give it credentials (once per project)
Ask the agent to run odoo_setup mode=check. It writes a secret-free
scaffold and you fill the password yourself:
odoo_setup mode=interactive url=http://localhost:8069 db=odoo_dev username=admin
# then fill ODOO_PASSWORD (an Odoo API key is recommended) in the printed file
odoo_setup mode=authorize # asks YOU, once, to authorize that exact target
Or do it by hand — the plugin looks for the first of these that exists:
| # | Location | Scope |
|---|---|---|
| 1 | ODOO_SDD_ENV_FILE |
explicit environment override |
| 2 | <project>/.sdd/instances/<name>.env |
one target per file, the active instance |
| 3 | <project>/.sdd/.env |
single-target layout (also listed as the instance default) |
| 4 | ~/.config/dsh-odoo-sdd/.env (honors $XDG_CONFIG_HOME) |
user scope — one set of dev credentials for every project |
| 5 | <project>/.env |
legacy location, still supported (reported as legacy) |
Several targets in one project. A workspace that needs a local CE server, a
staging EE server and a client's instance no longer overwrites its only .env:
each target gets its own file under .sdd/instances/, and one is active.
odoo_setup mode=instance instance=list # what exists, non-secret identity
odoo_setup mode=instance instance=add name=staging url=https://stg.example.com db=stg username=ci
# fill the secret in .sdd/instances/staging.env by hand, then:
odoo_setup mode=authorize name=staging # grants THAT target
odoo_setup mode=instance instance=use name=staging # activate it FOR THIS SESSION
odoo_setup mode=instance instance=remove name=staging # needs confirm_destructive=true
odoo_setup mode=revoke name=staging # drops only that target's grant
The active target belongs to the SESSION, not the project. Each chat keeps its
own pointer (.sdd/instances/active-<sessionId>.json), so two sessions on one
project can work in two environments at once — one on staging, another on a
client's server — without either moving the other.
A session that has not chosen is ASKED, never given a default. The first Odoo operation in a new chat refuses and returns the question, with the available targets and their non-secret identity:
This session has no environment chosen yet, and this project defines 2 instance(s):
- acme: https://… db=acme user=admin
- globex: https://… db=globex user=admin (the project's last used)
ASK THE DEVELOPER which one THIS session should use, then record the answer:
- an existing one: `odoo_setup mode=instance instance=use name=<name>`
- a new one: `odoo_setup mode=instance instance=add name=<name> url=… db=… username=…`, …
Inheriting the project's last-used target is convenient exactly once and dangerous
afterwards: another session switching environments would silently redirect a
working one, and its tool answers would still just say OK. One question per
session buys that away. .sdd/instances/active.json is kept only as the
suggestion shown in the question. A project with no named instances (just
.sdd/.env, the single-target layout) is never asked, and
requireInstanceChoice: false restores the old inheritance for anyone who
prefers it.
Authorization is per target (url+db+user), so two instances on the same
server are two grants. authorize takes the target's NAME, which is what makes
switching recoverable: grants expire (12 h), and requiring activation before
authorization used to be a closed circle — use demands a live grant for the
target, while authorize only ever saw the active one. Naming it breaks the
circle: authorize staging without leaving main.
Activating a target that has no live grant is still refused — the pointer decides
where mutations go, so it cannot be switched to something nobody approved. With
several instances and none active, every connection tool refuses instead
of picking one by order, and an old .sdd/.env keeps working untouched as
default until you choose to move it yourself.
mkdir -p ~/.config/dsh-odoo-sdd && cd ~/.config/dsh-odoo-sdd
cp <plugin>/.env.example .env && chmod 600 .env
# fill: ODOO_URL, ODOO_DB, ODOO_USERNAME, and ONE secret:
# ODOO_PASSWORD=<account password> or ODOO_API_KEY=<api key>
The secret has two accepted names. ODOO_PASSWORD and ODOO_API_KEY are both
read, and the API key is the recommended one: Odoo accepts it wherever it accepts
a password over RPC (Settings → Users → API Keys, and use a key with the global
scope). If both are set, ODOO_PASSWORD wins and the plugin reports that the
other was ignored, so a rotated credential cannot go unnoticed.
There is one place an API key does not work, and it is Odoo's rule, not this
plugin's: odoo_session and odoo_import authenticate a web session, and
Odoo authenticates interactive logins with interactive: True while only
consulting the API-key table otherwise. Configure an account password if you need
those two; everything else works with the key.
Which API the plugin speaks
| Server | Transport | Credential |
|---|---|---|
| Odoo 19+ | POST /json/2/<model>/<method> — named parameters, Authorization: Bearer |
API key |
| Any version | POST /jsonrpc — execute_kw |
password or API key |
On 19+ the plugin uses the modern JSON-2 API: no uid round-trip, named
parameters validated by Odoo itself, read-replica routing for @api.readonly
methods, and errors that arrive as an HTTP status with a structured body. It falls
back to classic JSON-RPC automatically in three cases: the route is missing, the server
predates it, or the credential cannot be used there at all. That last one matters:
JSON-2 authenticates with Authorization: Bearer only, so a target configured with an
account PASSWORD answers 401 on every call — and a 401 means the request never ran, so
serving it over the other transport cannot double-apply anything. A call the server actually
PROCESSED and refused (403, 422, a domain error) is never re-sent. Pin
the choice with odooApi: auto | json2 | jsonrpc in the panel or with
odoo_config; json2 reports a missing route instead of degrading quietly.
Business actions (action_*, button_*, do_*) always use execute_kw: JSON-2
has no positional-argument form, and inventing a position→name mapping for a
method whose signature the plugin does not know is exactly the kind of silent
mis-send it refuses to make.
[!WARNING] Never paste a password into the chat, a spec, a commit or an issue. The plugin refuses a group/world-readable
.env, redacts the secret from every tool output, and stores session cookies in.sdd/session.json(mode 600) without ever returning them to the model.
3. Ask for the thing you want
Implement a sale order approval module for Odoo 19, using the SDD workflow.
The agent picks up odoo-sdd-workflow from the session skill catalog and follows
the protocol. If you want to be explicit — or you want to be sure the full
instructions are loaded — start your message with /odoo-sdd-workflow.
For configuration and data work on a running instance, ask for that instead:
Set up company, taxes and chart of accounts in my dev instance, then import
this customers.csv — functional SDD, dev environment, no production.
That selects odoo-functional-sdd (or /odoo-functional-sdd explicitly) and
the functional spec mode described below.
🧭 The five phases
| Phase | What happens | Gate to leave it |
|---|---|---|
| CLARIFY | Intent recorded (mode create/bug, licensed, and optionally translations — see below) and the security interview answered: groups, ACLs, record rules, sudo() justification, public routes |
sdd_phase clarify |
| READ_SPEC | spec.md is assimilated: business context, numbered acceptance criteria, constraints, target Odoo version. Writing code here is forbidden. |
APPROVED + mark_spec_loaded |
| ARCHITECTURE | Models, views (including extra view types and a search view where they matter), reports, security matrix and test-plan.md |
APPROVED |
| WRITE_CODE | The module is implemented with version-pinned Odoo patterns and its OCA documentation. In mode=bug the failing reproduction comes FIRST and is recorded in test-plan.md as red (evidence: …) |
static gates green |
| VERIFY | Ascending pyramid: static → install/upgrade → RPC/data → UI (Playwright) only for critical flows | persisted PASSED verdict |
| FIX_LOOP | Root-cause fixes. 3 consecutive failures force a consultant diagnosis; 5 iterations force BLOCKED |
honest verdict |
Are the strings translated? (translations)
CLARIFY asks it, next to the licensing question, and the answer is recorded on the spec — not as a global setting, exactly like the edition. One workspace can hold a client project that ships in Spanish and an internal fix that ships no languages at all.
sdd_phase operation=clarify spec_id=<id> mode=… licensed=… translations=["es_VE","es_PA"]
- Omitting
translationsmeans the work is not translated. That is a normal answer, so unlikemodeandlicensedit is NOT a fail-closed gate. - The codes are gettext (
llorll_CC:es_VE,es_PA,pt_BR,fr). A malformed one is refused while it can still be corrected, instead of becoming a file the server would never load. - Declaring languages makes
odoo_i18npart of the plan and puts the matching acceptance criterion intest-plan.md: "i18n/<lang>.poexists and no exported term is left with an emptymsgstr" — whichodoo_i18n operation=checkverifies. sdd_phase statusreports them, so "did we translate this?" is answerable without openingstate.json.
In ARCHITECTURE the agent also asks about the things that are cheap to decide
early and expensive to discover late: extra view types beyond form/tree
(including a search view for how a model is searched — custom filters,
favorites), reports (PDF via ir.actions.report/QWeb, SQL, CSV/XLSX, an
external tool), web tours (onboarding, test, or none — with the asset bundle
that loads them, because a tour no bundle loads never runs) and demo data
(which files, and what for). Each one is answered explicitly — "form + tree
only", "no reports needed", "no tours needed", "no demo data". Those are
guide decisions: recorded in ## Views / ## Reports / ## Tours /
## Demo data and surfaced as warnings in sdd_phase status, non-blocking by
design — the security model is the only fail-closed content gate. The
version-by-version tour API, the HttpCase that executes a tour and the demo
traps live in resources/references/tours-and-demo.md; the test cycle, the case
classes per version and what makes a test trustworthy live in
resources/references/odoo-tests.md.
Per-spec artifacts (all on disk, resumable):
specs/<NNN>-<slug>/
├── spec.md · architecture.md · test-plan.md
├── verify-verdict.txt # honest persisted verdict
├── state.json # phase, failures, iterations
├── kb.json # decisions, discarded options, blockers, diagnoses
├── docs-report.md · security-report.md
└── handoff.md # written by sdd_handoff when the run closes
🧩 The functional path (configure and import)
Not every Odoo job is code. Setting up a company, its taxes, its users and its master data is configuration and data, and it happens on a live instance — where a wrong click is not a failed test but a real record. The same SDD machine covers it with a different middle phase and stricter closing rules.
graph TD
C1["1 CLARIFY<br/>objective, instance, ENVIRONMENT"] --> R2["2 READ_SPEC<br/>spec.md, criteria, sources"]
R2 -->|APPROVED| A3["3 ARCHITECTURE<br/>to-be process, batches, risks"]
A3 -->|APPROVED| X4["4 APPLY_CONFIG<br/>discovery + approved batches"]
X4 --> V5["5 VERIFY<br/>re-read the records, evidence per AC"]
V5 -->|PASSED| D9(["runbook.md + handoff.md - DONE"])
V5 -->|FAILED| F6["FIX_LOOP"]
F6 --> X4
style X4 fill:#1e1e2e,stroke:#f9e2af,stroke-width:2px,color:#cdd6f4
| Development run | Functional run | |
|---|---|---|
Chosen at CLARIFY |
mode=create or mode=bug |
mode=functional |
| Middle phase | WRITE_CODE (module source) |
APPLY_CONFIG (batches against the instance) |
| Deliverable | module + OCA docs | configured instance + functional-runbook.md |
| Skill | odoo-sdd-workflow |
odoo-functional-sdd |
How a change reaches the instance. Nothing is written "to see what happens":
- Discovery first, under its OWN approval: which models, which fields, how many records. Reads are not mutations, but an approved scope is what stops "just looking around" from turning into a change.
- Plan: the design becomes batches. Each one declares its destination and environment, the version and capabilities used, company and context, the acceptance criteria it covers, its ordered operations, record identity, preconditions, expected result, risks, recovery and manual steps.
- Approve: the human sees the exact batch and approves it through the native approval seam. The receipt is bound to the hashes of the spec, the design, the plan and the batch — change any of them and the approval is void.
- Apply: one operation at a time, re-checking those hashes, persisting each operation's state before the call and after the result.
- An unknown outcome is not a retry. A timeout after a mutation may mean
Odoo already committed, so the operation is marked
indeterminate, the batch stops and the run parks until a human reconciles it. - Close honestly:
sdd_phase succeeddemands an explicitpassper acceptance criterion, the security review is mandatory, and so is the runbook (who does it, in which company, prerequisites, the verified menu path, the steps with their field labels, the expected result, how to check it and how to undo it) — whatever the documentation policy says.
The environment is declared, never assumed. ODOO_SDD_ENVIRONMENT on the
target says dev, staging or production. A plan that declares a different
environment than the target is refused (environment-mismatch), an undeclared
target asks for it (NEEDS_ENVIRONMENT), and production additionally needs a
declared backup reference plus its own approval. High-risk changes are proven in
staging first.
Imports go through Odoo's importer, never through a hand-written parser:
odoo_import use=prepare file=... model=res.partner # uploads, with its own approval
odoo_import use=preview ... # what ODOO read: sheets, headers, sample
odoo_import use=map ... # one decision per column, no blanks
odoo_import use=plan ... # becomes an `apply` batch
odoo_functional operation=approve / apply # the batch path, unchanged
The version contract is explicit (majors 10–19: file/import_id + JSONP on the
old endpoint, ufile/id + JSON on the new one, do/execute_import for the
apply), and a version outside the verified families is refused with what to
investigate instead of guessed. A file that changed after the upload invalidates
the mapping; a nextrow in the answer means the importer stopped mid-file and is
reported as partial, never as success — and the rows it counted are never
re-sent. The session cookie stays in .sdd/session.json (mode 600) and out of
every tool result.
[!NOTE] The functional path needs an instance, and the importer needs a web session: run
odoo_sessiononce beforeodoo_import use=prepare.
🧰 The 18 tools
| Tool | Purpose |
|---|---|
odoo_connect |
Probe the instance: server version + authentication. Masked report; distinguishes NEEDS_SETUP / NEEDS_SECRET / DEFERRED / SKIPPED states (never asks for secrets in chat). |
odoo_setup |
Onboarding: check (cascade + gitignore + delegation mode), interactive (secret-free chmod-600 scaffold), authorize (ask the DEVELOPER, through native approval, for a connection grant bound to the current url/db/user), revoke (drop the grants), purge (plan first, then — with confirm_destructive=true plus human approval — remove only the plugin's own state under .sdd/), later, skip, reset, autonomy (supervised | autonomous, human-approved). Secrets are never accepted as parameters. |
odoo_module |
info / install / upgrade on ir.module.module (button_immediate_*). Returns the server's own output or traceback, redacted — the closed feedback loop. |
odoo_execute |
Generic RPC (execute_kw) against the instance: any public method of any model is callable, and what it needs is decided by what it is. READS (search, search_read, search_count, read, read_group, fields_get, name_get, name_search, default_get, exists, check_access_rights, check_access_rule) run freely, with fields/limit/order/offset for projection and paging (a fractional or negative offset is refused, never clamped); CRUD mutations (create/write/unlink) require confirm_destructive=true AND the model in executeAllowlist, and are journaled so the data undo can replay them; every business action (action_*, button_*, do_*…, e.g. confirming an order) runs with confirm_destructive=true and no allowlist, because the plugin cannot replay its effect — it is never journaled, no undo exists for it, and a timeout is reported as an INDETERMINATE outcome instead of a plain failure — the same rule now applies to CRUD, because a create whose answer never arrived may already have committed and reporting it as a retryable failure is how one effect becomes two. Declare precondition/postcondition (the same shape a batch uses: {domain, expect, count}) to carry a state guard before the call and a state proof after it: if the precondition fails, nothing is sent; if the postcondition fails, the answer says the call WAS sent and the instance may be half-changed, and the CRUD mutation keeps its journal receipt (created id + pre-image) so it can still be compensated; without one, OK only means the server accepted the call. A private method (_name) is refused, and that wall is Odoo's own: execute_kw dispatches through get_public_method, which answers AccessError for _…, init, @api.private and the internal attribute names — so read the model in the source (the native/custom repositories are reported by odoo_config mode=read) and call the public button or action that wraps it (sale.order._create_invoices is private; the sale.advance.payment.inv wizard exposes the public create_invoices()). For several operations, or when a human approval per run is wanted, use a functional batch with kind: "method". context is forwarded verbatim — use allowed_company_ids/company_id on multi-company instances — and the server still applies its own ACL. A large answer is BOUNDED and the cut is MARKED: the text says how many characters were dropped, that what you see is the beginning (not a complete JSON document) and how to page with fields/limit/offset — and for a mutation it explicitly forbids repeating the call to get more text. |
odoo_validate |
LOCAL, instance-free module structure check: __manifest__.py present + depends, declared data XML files exist, security/ir.model.access.csv when models are declared. Returns file:line findings plus the module_dir and project root it resolved (a relative path resolves against the session's folder, never the process cwd). |
odoo_errors |
Reads recent ir.logging server errors — the remote equivalent of fetching environment logs. |
odoo_session |
Mints a passwordless web session (the connect_as_user pattern) stored in .sdd/session.json (chmod 600) for Playwright UI tests. The cookie itself is never returned. |
sdd_phase |
The phase state machine: init, status (logbook summary, spec directory and specs location), mark_spec_loaded, advance (fail-closed gates: the host is asked for approval natively and the approval_marker/approval_source stay as the documented backstop for hosts with no approval seam), fail (failure ladder + FAILED verdict), succeed (PASSED verdict; refused outside VERIFY/FIX_LOOP, and unless every AC the SPEC declares has a row in test-plan.md reading an explicit pass — an omitted criterion is not a passing one, and the verdict records the fingerprint of the evidence it used so DONE refuses a verdict made stale by an edit), rollback (restore a checkpoint OF THIS SPEC and return to the phase that mode works in — WRITE_CODE, or APPLY_CONFIG for a functional spec), diagnose, waive (record — with native approval — that THIS session may work without a spec: the developer's "leave it to your judgement", kept verbatim in .sdd/waiver.json, reported by status and by the handoff; revoke: true drops it and needs no approval). |
sdd_checkpoint |
The rollback surface: create (snapshots the workspace, becomes the active checkpoint), list, restore (files, plus — with restore_data=true and confirm_destructive=true — the journaled data mutations: the undo runs under the company context the mutation used, turns read shapes into write values, marks each op so a retry never compensates it twice, refuses a journal from another destination, and reports every field it could not restore; it always REPORTS files created after the checkpoint and deletes them only with remove_created=true), drop, journal. |
odoo_docs |
Documentation for a module, usable on its own (no spec, phase, checkpoint or instance), so an existing module can simply be documented: check (OCA fragments mapped to Diátaxis, version scheme, changelog, index.html, docstrings, xpath comments, OWL directives → ERROR/WARN with file:line), plan, scaffold (create-only skeletons, never overwrites) and report (persists docs-report.md; APPROVED only when nothing is still a scaffold). The changelog entry is mandatory for any change to a released module. |
odoo_security_scan |
Local static security review (no instance needed): raw SQL by concatenation, eval/exec/pickle, hardcoded secrets, unjustified sudo(), auth="none", disabled CSRF, QWeb t-raw. Findings carry file:line + a fix hint; any ERROR blocks DONE. |
odoo_manual |
Narrated user manuals built on evidence. canon reads the functional canon of a model from the instance (views, header buttons and the states that reveal them, required fields, notebook pages, actions, menus, access rules, groups, raise UserError/ValidationError and _sql_constraints sites) and persists it with a fingerprint: the manual may only describe what the canon says. plan turns the canon into a storyboard (flows, ordered steps, one sentence per step): re-running it MERGES, so the narration, captures and prose already written survive. lint checks the storyboard and the rendered prose against the canon (CANON-STALE, TARGET-UNKNOWN, PLACEHOLDER-UNBOUND, ACTS-MISSING, RECOVERY-MISSING, AI-tell patterns in Spanish and English → ERROR/WARN with the exact line). explore returns the ARIA snapshot of a real screen so no xml id or label is guessed. capture drives a browser: read-only steps always run, and action steps (new/fill/select/press/click) require allow_actions=true — which the guard treats as a mutation. render writes the manual and an Apps page (create-only unless overwrite=true). import turns a Playwright codegen recording into a flow and refuses any recording that carries credentials. Manuals live where Settings says (manualsMode: specs next to the spec, docs under manualsDir, module as OCA fragments under the module's readme/manual/); standalone work without a spec never blocks on one. canon takes the interview — as_user (who the manual is for), admin, module/modules and groups — and records it: the inventory then covers EVERY menu and EVERY action the module owns (the action a menu opens, even a native one), and every model arrives with its fields (label, type, required, relation/selection, groups), its form and list buttons, its statusbar states and its per-group access rows. plan opens one flow per menu and one per access-group walk, and seeds a docs checklist with one empty cell per field and per button. lint reports MODULE-COVERAGE, INTERVIEW-MISSING, FIELD-COVERAGE, BUTTON-COVERAGE, WORKFLOW-COVERAGE and GROUP-COVERAGE while a menu, an action, a field, a button, the workflow or a group is still uncovered — a skip with a reason is documented coverage, and ROLE-UNVERIFIED warns because a capture runs as the connected user. render adds a table per model (type, required, relation, groups, and your sentence) and a section per access group. odoo_docs adds a ## User manual section to the documentation verdict as soon as a storyboard exists — and stays untouched when there is none. |
odoo_tests |
The testing half of the pipeline, usable on its own (no spec, phase, checkpoint or instance): check scans the Python tests for what an install log cannot show — a tests/*.py that tests/__init__.py never imports (Odoo only loads what is imported, so the test never runs), a class that is not an Odoo test case, a test_* that asserts nothing or asserts something trivially true, a swallowed exception, time.sleep, explicit skips, cr.commit() inside a test (it breaks the per-test rollback), SavepointCase on 17+, an HttpCase without start_tour — with assertion density as an informational metric. plan maps every acceptance criterion of the spec to the test that mentions it (by AC id or by the REQ-<AREA>-NN id of its requirement) and lists the criteria with no test. scaffold writes one RED stub per uncovered criterion (body raises NotImplementedError, never a vacuous pass), create-only and idempotent, adding the import so the file actually loads. |
sdd_handoff |
Writes specs/<id>/handoff.md (final phase, verdict, decisions, blockers, checkpoints, the COMPLETE per-spec data journal, effective config, next steps) when the run closes. |
odoo_config |
Reads or updates the persistent configuration and answers "which project am I in?": the resolved root, its provenance (session cwd / configured / process cwd), the specs base, the effective spec directory and the config file in use. |
odoo_i18n |
Translations through Odoo's own models: base.language.export supplies the translatable terms and base.language.install activates the language. export writes <module>/i18n/<lang>.po (creating the i18n/ folder when missing) and MERGES with the file already there — a written translation is never replaced by what the export says, new terms are added, and terms the module dropped are kept as #~ instead of deleted. status reports what each language covers, check fails while an entry still awaits its text. It does not translate: the export gives the message IDs and a person writes the text. Version-aware (Odoo 16 is the boundary, the same one that removed ir.translation). |
odoo_import |
Preparation of a CSV/XLS/XLSX import through Odoo's OWN importer (base_import), never through a parser of this plugin: prepare uploads the authorised file with the web session and its own approval, preview reports what Odoo reads (sheets, headers, a bounded sample, the importable fields), map records a decision per column, plan turns it into an apply batch — and odoo_functional approves and executes it like any other, so this tool never applies an import on its own. A JSONP answer is parsed as data (never executed), the session cookie never leaves the plugin, and a version outside the verified families is refused with what to investigate. |
odoo_functional |
The batch executor of the functional path: plan (validate and store a batch fail-closed), approve (native human approval bound to the spec, design, plan and batch hashes), apply (execute it one operation at a time, persisting each state before and after the call, and record it as applied only after reading back the state the operation declared — a call that answers but leaves the records untouched is a failure, not a success), inspect (read-only discovery under its own scope), status, reconcile (decide an outcome that came back as unknown), verify (evidence per acceptance criterion) and compensate (build the undo batch from the journal). The declared environment gates the run and production additionally needs a declared backup; while a batch runs, every other mutation path is denied. |
🎛️ Delegation mode
The pipeline starts by asking how much of it to delegate — recorded once per
project with odoo_setup mode=autonomy decision=...:
| Mode | Who answers the gates | How it ends |
|---|---|---|
| Supervised (default) | you, for each gated phase | you approve, or the run parks |
| Autonomous | a human-proxy agent (resources/roles/human-proxy.md) that emits only a fail-closed line-start APPROVED or NEEDS_REVISION |
create_goal runs unattended rounds until DONE or BLOCKED |
[!TIP] In autonomous mode the brakes stay armed:
stop.md, the iteration ceiling and the diagnosis ladder all remain, andBLOCKEDis the only way the run pages a human. Connection grants are not covered by the switch — a human still authorizes the instance once.
🧠 Context engineering layers
| Layer | Component |
|---|---|
| identity | resources/roles/*.md — architect, developer, qa, consultant, human-proxy, security-reviewer, documentation roles, each with its limits. Both bundled skills declare resources/ as their resourceBase, so every roles/… and references/… path resolves from the base the host hands the model |
| odoo_connection | odoo-client.ts — JSON-RPC auth, execute_kw, session minting |
| executors | odoo_module, odoo_execute, odoo_validate, odoo_errors |
| schemas | staged templates with required sections; transition() rejects a phase whose deliverable lacks them |
| knowledge | version-pinned Odoo pattern skills (delegated, verified by the skill) |
| skills | skills/*/SKILL.md — the 5-phase development flow and the functional flow, auto-registered with the host on apply(). The body is the ROUTE (phases, owners, gates, exits); the per-phase WORK lives in resources/roles/ and resources/references/, loaded on demand. Measured budgets keep the selected load from creeping back |
| logbook | kb.json — decisions, discarded options, blockers; read before proposing |
| audit | .sdd/audit.jsonl — sanitized append-only tool-activity log, written by a global tools/result listener (not just the Odoo tools) |
| rollback | .sdd/checkpoints/<id>/ — manifest + file snapshot + data journal, restorable per spec |
| security | odoo_security_scan rules + the security-reviewer role + the mandatory security interview in CLARIFY |
| test | tests/smoke.mjs — instance-free invariant suite (state machine, security, policy guard, RPC shapes, root/specs layout, real-Cordis host contract) + tests/client.mjs — browser bundle contract and settings-panel render |
🛡️ Safety, rollback and traceability
The pipeline assumes the agent will eventually be wrong, so every mutation path has a way back and a way to prove what happened.
Credentials are not consent. Before any tool opens a socket towards the instance, a HUMAN must have approved that exact target.
odoo_setup mode=authorizeasks through the host's native approval seam and only theallowed-onceoutcome stores a receipt in.sdd/grants.json(0600, gitignored). The receipt is bound to a fingerprint ofurl + db + username, so changing any of them invalidates it;mode=revokedrops it. Without a live receipt, no client is handed out at all, so a configured.envcannot be used silently. In AUTONOMOUS mode there are no answerers, so the run reportsNOT AUTHORIZEDand parks — which is the point.The model cannot relax its own policy. Changing the allowlist or the policy guards (
odoo_config mode=set) and switching delegation mode (odoo_setup mode=autonomy) each require a native approval.No change without a spec — or your explicit waiver. Whatever the request ("now also change X" in the same chat), a change needs a spec in a writing phase (
WRITE_CODE,VERIFY,FIX_LOOP,APPLY_CONFIG) or a waiver you approved for THAT session. The gate covers both surfaces: instance mutations (odoo_execute,odoo_module,odoo_import prepare) and source edits through the host'swrite/edittools. Writing the spec documents themselves (specs/,.sdd/) is never blocked — otherwise the way out would be blocked too. A small change is a small spec:mode=bug, one acceptance criterion, no design interview. If you would rather not specify it,sdd_phase operation=waive detail="..."records your words, needs your approval, and covers this session only: the next chat starts under the policy again, and every change made under it still lands in the KB as a decision.requireSpecForChanges: falseis the human off-switch. Honest hole: the guard sees host tool calls, so a change made throughbash(a heredoc,sed -i) is not gated — the pipeline is the path, not a jail.Where the question is asked (
specPolicyScope, default"odoo"). A spec requirement is only meaningful where there is Odoo work, and the gate used to check one thing only: whether the path fell inside the session's root. So a plugin installed at profile level blocked writes in folders that have nothing to do with Odoo — notes, a pipeline of your own, anything. With"odoo", a file edit is gated only where the directory shows a sign of Odoo work:.sdd/holding real plugin state (config, specs, checkpoints, a waiver) — not a.sdd/containing onlyaudit.jsonl, which the audit listener writes for every tool call and which therefore means "something ran here", not "this is an Odoo project";__manifest__.pyin the root — the directory IS a module;__manifest__.pyone level down — the directory holds modules;- an
odoo_*tool already used against that root in this session.
An instance mutation is always gated: it is an
odoo_*call, which is itself the evidence.stop.mdstill halts everything, adopted or not, because it is an emergency stop and not a scope question."everywhere"restores the old blanket behaviour, and the refusal now names the signal that armed it rather than leaving you to guess.Three ways out, and all three work. (1) a small spec for this change, (2) continue the spec that already covers it, (3) ask the developer to waive —
sdd_phase operation=waive detail="...". The third used to require aspec_id, which made it unreachable in the one situation it exists for: a directory with no specs, where you would have to create one to say you did not want one. A waiver is per project and per session, andspec_idis now optional for it (and forwaive revoke=true) while every other operation still demands it.Checkpoint before mutating. With
requireCheckpointBeforeMutationon (default), instance mutations are denied untilsdd_checkpoint createhas snapshotted the active spec — and denied outright beforeWRITE_CODE. The rule coversodoo_execute(CRUD and business actions),odoo_module install|upgrade,odoo_import use=prepareandodoo_functional operation=apply, which used to be missing from that classification. Snapshots skip symlinks (lstat) and never copy.envor key material.Fail-closed guard. An internal guard failure denies with a visible reason instead of allowing the call through. The guard is also a MOUNT requirement while either policy is armed: a host that exposes no
tools.guard(or whose guard cannot be installed) makes the plugin refuse to mount with the reason and the way out, instead of staying up and looking protected.The canonical phase decides. Authorization reads the phase from
specs/<id>/state.json; the pointer
…
Links
More in this category
loopx-project/loopx#dsh-loopx-plugin★ 6398
LoopX, a provider-neutral, local-first state kernel and control plane for long-horizon agents: keeps Goal, Todo, gate, evidence, quota, recovery, and handoff state above DeepSeek Harness, while the plugin bootstraps the CLI and skills, admits bounded same-session continuation, and adds a loopback GoalBar for the exact bound loop.
Q00/ouroboros#integrations/dsh-plugin★ 6260
Config-only bundle that mounts Ouroboros through the DSH MCP client, exposing 36 interview, Seed, execution, evaluation, and evolution workflow tools in DSH.
chuspeeism/dashi-taskboard#deepseek-harness★ 3388
Embeds the active installed Codex Taskboard runtime in the DeepSeek Harness sidebar, using its launcher runtime descriptor instead of a fixed port.
NanmiCoder/dsh-agent-teams★ 2368
AgentTeams multi-agent teams.
EthanYoQ/AI-Novel-Writer#dsh-ai-novel-writer★ 1753
Installs a dedicated AI novel-writing preset and workbench: revisioned local project assets, a compact side drawer, and native approval-gated single-file changes.
Miaotofu01/Study-Mate★ 1144
Your AI learning companion: helping you plan your studies, understand concepts, and build projects to master a subject.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.