Repository navigation
Releases
v0.20.0 — mounts only on DSH 0.2.2, narrated video, and an audit ledger that stays small
Requirements
- DSH ≥ 0.2.2-rc.1 — enforced through
engines.dsh, which the host reads
while it ignorespeerDependencies. The host declares Node ≥ 24.9.0, so Node
24 is the practical floor even though the plugin's own code needs ≥ 20. @deepseek-ai/dsh-tools≥ 0.2.2-rc.1 (peer, ships with the host).- Optional:
playwright-core1.63.0 (pinned) forodoo_manual/odoo_video
capture, plusffmpegand a TTS command for the video track. Both are
detected at run time and reported with an actionable remedy, never a stack
trace.
Highlights
odoo_video— plan → voice → capture → assemble, with SRT and VTT
captions always written. The assembly is verified against a real ffmpeg on
Linux and on Windows, not against a stub.odoo_validatereads the project's own rules —<project>/.pylintrc
decides the required manifest keys and authors, and the module is checked for
the OCA metadata (license,website,README.rst,
static/description/index.html) plus two view conventions that break OCA CI:
position="replace"withoutpriority, andt-esc. Aviews/*.xmlthat the
manifest never declares is an error.odoo_i18nwrites the comment the OCA checker requires — exported entries
carry#. module: <name>, andoperation=checkfails on any entry that
lacks it.- The audit ledger stays small — file bodies are recorded as
{bytes, sha256}digests instead of full contents, the ledger rotates at
16 MB keeping one generation, andauditAllToolsnow defaults tofalse
(plugin tools and every policy denial are always recorded). - Every tool call has a title card, and the read-only RPCs are marked
concurrency-safe, so a batch of reads no longer serialises. sdd_handoffbelongs to its spec — it lists only that spec's
checkpoints and summarises the mutation allowlist instead of dumping it.
Fixes
- Stored paths in the persisted reports are POSIX on every OS: a Windows run of
the real assembly used to reportvideo\f1.mp4where the contract — and the
docs — sayvideo/f1.mp4. npm rebuildno longer fails on an installed copy: the tarball ships the
install hook and its build inputs, and the hook itself is a no-op whenlib/
is prebuilt and no compiler is present.- The plugin typechecks against the host it actually runs on (the local
typecheck had been green against a leftover 0.1.x tree). - Three troubleshooting rows added to the READMEs (EN/ES/ZH):
pnpm was not found,Unsupported engine, andERR_MODULE_NOT_FOUND … scripts/prepare.mjs
duringnpm rebuild.
Packaging and CI
- The tarball ships
scripts/prepare.mjsandtsconfig.json, so a consumer's
npm rebuildruns;filesis asserted by a packaging suite. - One declared host floor, enforced by tests:
engines.dsh,host:depsand the
READMEs must state the same version, and the floor is what CI installs in the
host-contractjob. - Matrix green on ubuntu-latest and windows-latest × Node 20, 22 and 24, plus
a Windows job that assembles a real video with Chocolatey's ffmpeg. - Failures are readable without a token: the suites emit annotations with the
tail of the log, because a job log needs credentials and a check run does not.
Install
npm i -g pnpm # if `dsh plugin` says it is missing
dsh plugin --profile web add dsh-odoo-sdd@0.20.0v0.18.0 — Interactive approvals (localized asks + preview-first gates)
Interactive approvals
- Localized approval asks: every native approval now carries a
displayReasonwith English, Spanish and Chinese variants, so the question arrives in the developer's language. The audit trail keeps the classic Englishreasonbyte-for-byte, and hosts withoutdisplayReasonsupport keep working exactly as before. - Spanish approval card:
client.jsregisters Spanish as a first-class language pack (addLanguage), so the card itself renders "Esperando aprobación / Rechazar / Permitir una vez" instead of English-only.
Preview-first gates (approve what you have seen)
- New
sdd_phase operation=preview: returns the current gate's documents inline —spec.mdfor READ_SPEC,architecture.md+test-plan.mdfor ARCHITECTURE (400-line cap per document) — and stamps their sha256 hashes into the spec state. - A gated
advancenow refuses to ask the human without a fresh preview stamp. Editing any gate document after the preview invalidates the stamp on purpose: the developer must be shown what they approve. The stamp clears when the gate is crossed, so the next gate needs its own preview. - Content gates (required headings) run before the approval ask, so a broken draft reports the missing section instead of the missing preview.
Other changes
- Skill
odoo-sdd-workflow: documents the preview-first flow (preview → present to the developer → advance). - Tests: coverage for the preview stamp lifecycle, the Spanish language pack registration, and localized approval reasons across
odoo_functional/odoo_import.
Full changelog: v0.17.0...v0.18.0
v0.17.0 — A manual that asks who it is for, and covers every field, button and group
odoo_manual turns a running Odoo instance into a narrated user manual. This
release adds the interview behind it and covering rules that make "document
everything" checkable, after the 0.15.0 manual engine was never published.
What's new
The manual interviews the reader before writing. canon records who the
manual is for: the user it documents (as_user), whether that user is an
administrator, the modules it covers, and the access groups it walks. The
storyboard and the rendered manual open with a "Who this is for" section, and
lint reports INTERVIEW-MISSING until that is answered.
Every field, every button, every group is coverage surface. The canon now
stores a CanonModelDoc per model: fields with type, required, relation,
selection and the groups that see them; the buttons of the form and of the list
view; the statusbar states; and the access rows per group. lint reports
FIELD-COVERAGE, BUTTON-COVERAGE, WORKFLOW-COVERAGE and GROUP-COVERAGE as
errors while any of them is undocumented, with docs.skip and docs.groupsWhy
as the documented escapes. A model the server refuses to read is reported as
MODEL-UNREADABLE instead of silently disappearing.
The generated guide is a starting point, not the manual. plan now merges
instead of overwriting: a second pass keeps the prose already written per field
and per button, the recovery notes, the audiences, the captures and the flows
that no longer come from the canon.
Every menu the module owns gets a flow. canon with module reads the
module's menus, actions and models, including menus that open a native action;
plan adds one opening flow per menu and one walk per access group.
The render follows the coverage. The rendered manual has a table per model
(field, label, type, required, relation/selection, groups, what it is for), a
table of buttons with their states and confirmation, the workflow with the
states the form allows, and one section per access group.
Fixes
- The configuration no longer crashes
odoo_manual. A settings value that
the host resolves to a descriptor object reachednode:pathand every
operation died withThe "path" argument must be of type string. Every
configuration field is now resolved to its declared type before use, the
specs path goes through the same resolver as the rest of the tools, and a
malformed folder is answered as a blocking message that names the key. - The Settings panel persists
specPolicyScope. - OCA README fragments start at H2, and a capture that names a screenshot
that does not exist is reported as an error. - The Chinese locale speaks Chinese.
- Windows in CI. Three separate defects: the test suites reached
lib/
through a path instead of a file URL, a folder assertion assumed a temp dir
outside the home, and importing a Playwright recording decided "absolute" by
looking at a leading/, so aC:\...path was read as relative. - A failing suite is diagnosable from the run. CI now runs one step per
suite and publishes the failing tail to the annotations of the check run,
which are readable without a token even when the job log is not. - Never write a credential file that git would still see, and keep the
READMEs advertising the version the package declares.
Upgrading from 0.14.0
0.15.0 and 0.16.0 were never published to npm, so 0.17.0 supersedes 0.14.0
directly.
- Requires Node 20 or newer and DSH 0.2.0-rc.1 or newer.
playwright-core1.63.0 is an optional dependency, needed only bycapture
andexplore; the plugin installs and runs without it.- New settings:
manualsMode(specs,docsormodule, defaultspecs) and
manualsDir(defaultdocs/manuals). - A manual written by hand before 0.17.0 has no
canon.json, so the new
coverage rules do not apply untilcanonis run again. - The plugin now registers 18 tools.
Verified
npm test green (manual 214 checks, capture 18), npm run test:package 116 of
116, and CI green on all eight jobs, including Windows with Node 20, 22 and 24.
Full changelog: v0.14.0...v0.17.0
v0.14.0 — A bug fix starts with a test that fails, and requirements get a stable identity
dsh-odoo-sdd on npm is 0.11.2. The versions after it — 0.11.3, 0.12.0,
0.12.1 and 0.13.0 — were tagged and never published, so this release carries
all of them plus 0.14.0. Installing the package today gets you none of it.
A bug fix now starts with a test that fails (0.14.0)
RED before GREEN, enforced. In mode=bug, sdd_phase advance refuses to
leave WRITE_CODE for VERIFY until test-plan.md records the reproduction
that failed first — either red (evidence: <command> -> <what failed>) or, once
fixed, pass (red first: …). A bare pass is not evidence: written after the
fact it is indistinguishable from a test that never failed. sdd_phase status
reports the state, and succeed still accepts only pass, so a red row cannot
close a criterion. mode=create is untouched — there is no defect to reproduce,
and a ritual for every criterion would only teach everyone to write the row
without doing the work.
New tool: odoo_tests (the plugin now exposes 17 tools), usable on its own
with no spec, phase or instance:
check— what an install log cannot show. Atests/*.pythat
tests/__init__.pynever imports (Odoo loads only what is imported, so the
test never runs), a class that is not an Odoo test case, atest_*that
asserts nothing or asserts something trivially true, an exception the test
swallows,time.sleep, explicit skips,cr.commit()inside a test (it breaks
the per-test rollback),SavepointCaseon 17+, anHttpCasewithout
start_tour. Assertion density is reported as information, not as a threshold.plan— maps every acceptance criterion of the spec to the test that mentions
it, by AC id or by theREQ-<AREA>-NNid of its requirement, and lists the
criteria no test covers.scaffold— writestests/test_<module>_sdd.pywith one RED stub per
uncovered criterion (the body raisesNotImplementedError, never a vacuous
pass), create-only and idempotent, adding the import so the file actually
loads.
The guidance ships with the plugin. resources/references/odoo-tests.md
covers the case classes per version, tagging and --test-tags, the red-green
cycle, what makes a test trustworthy, and where coverage thresholds belong (as
guidance — the plugin gates on requirements, not on a line percentage). The
workflow skill and the developer/QA role files point at it, so nothing here
depends on a skill installed outside this package.
Coverage stopped ignoring tests/. A REQ-… annotation written only in a
test used to be reported as unimplemented; it now reads test-only, which is
what it is: the behaviour is exercised, but no line of the module points at the
requirement.
Requirements get a stable identity (0.13.0)
- Every top-level requirement in
spec.mdgets a stableREQ-<AREA>-NNid
(section slug + ordinal), a fingerprint, and an entry in
specs/<id>/req-baseline.json. - DRIFT. Rewording a requirement under an existing id — or deleting it —
makessdd_phase advancerefuse, with the ids named. Accepting the change is
an explicit act:sdd_phase operation=baseline, recorded as a KB decision.
Newly written requirements fold into the baseline on their own, so a later
reword of one of them is caught too. Replacing template scaffolding
(- [ ] AC1: ...) is not "a requirement was removed". - Bidirectional coverage.
odoo_validate module_dir=… spec_dir=…reports
requirements with no implementation and orphaned annotations whose id the
spec no longer declares. Floors by modality:must/shallblock at 1.0,
shouldis reported at 0.8,mayis informational — a narrow gate that is
never waived beats a broad one that always is. 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, plus a precision-by-consumer
table in the generated spec templates.
The spec policy stops following you around (0.12.0)
specPolicyScope (default odoo) gates a file edit only where the directory
shows Odoo work — a .sdd/ holding real state, a __manifest__.py, or an
odoo_* tool already used against that root. Instance mutations are always
gated, stop.md still halts everything, "everywhere" restores the old
behaviour, and the refusal names the signal that armed it. waive also works
with no spec at all, which is the one situation it exists for.
Packaging and CI
The byte budgets now measure the document instead of the checkout: Windows
checks this repository out with CRLF, and the same guide measured 15,912 bytes
on Linux and 16,124 on Windows under a 16,000 limit. actions/checkout and
actions/setup-node moved to v7 for the Node 20 runtime deprecation; the matrix
stays Linux + Windows on Node 20/22/24.
Install
dsh plugin --profile <profile> add dsh-odoo-sdd@0.14.0
# or
npm install dsh-odoo-sdd # published with a provenance attestationv0.11.2 — Every session knows which database it writes to, and modules are translated with Odoo's own models
Nine releases since 0.7.2, which is the last version published to npm. Installing
dsh-odoo-sdd today gets you 0.7.2 — none of this. Publishing this release closes
that gap.
Three changes of substance
The active environment belongs to the SESSION, not the project. Two chats on one
project can work against two databases at once without moving each other's target.
A session that has not chosen is now ASKED rather than given a default: the first
Odoo operation refuses and returns the question, listing every target with its
non-secret identity. Before this, a session that never chose inherited the project's
last-used target — so another session switching environments redirected a working
one silently, and its tool answers still said OK. One question per session buys
that away.
Translations through Odoo's own models. odoo_i18n gets the translatable terms
from base.language.export and activates the language with base.language.install.
It writes <module>/i18n/<lang>.po, creating the i18n/ folder when missing, and
MERGES with the file already there: a translation a person wrote is never replaced
by what the export says, new terms are appended, and terms the module dropped are
kept as #~ instead of deleted. A file that does not parse is refused rather than
overwritten. The languages are decided per spec, in CLARIFY
(translations=["es_VE","es_PA"]), the same way the edition already was.
JSON-2 as the default transport, with automatic fallback to classic JSON-RPC.
Version-aware throughout: Odoo 16 is the single boundary — base.language.install
takes lang through 15 and lang_ids from 16, and ir.translation exists through
15 and is gone from 16, where the i18n/*.po files are the source.
Three defects only a real server could show
The suite was green with 835 checks. Running against a live Odoo 19 found three
things no stub was going to produce:
- A 401 from JSON-2 broke every model call. The target's credential was an
account password, and JSON-2 authenticates withAuthorization: Beareronly. The
client treated the 401 as a final refusal — "a refusal is never re-sent over the
other transport" — so every call failed on the defaultauto, while the classic
transport served that same credential perfectly. The rule was too broad: a 401
means the request NEVER RAN, so re-sending it cannot double-apply anything, unlike
a 403 where the call was processed and refused. A 401 now marks JSON-2 unusable
for that client, records WHY, and falls through. - The export needs its own timeout.
act_getfilewalks the registry, the views
and the field definitions. Exporting every module measured ~27 s — right at the
client's 30 s default, so a slightly loaded server aborted it. - An empty
Plural-Formsis a missing one. Odoo emitsPlural-Forms:with
nothing after it, and the merge checked only forundefined— so every generated
file came out without the plural rule that decides whether a plural string renders
correctly at all.
Also since 0.7.2
- API keys as a first-class credential, and Odoo's JSON-2 API.
- Per-session environment pointers (
.sdd/instances/active-<sessionId>.json). odoo_setup mode=authorize name=<instance>authorizes a target WITHOUT activating
it, which removes the 12-hour grant deadlock;revoke name=Xdrops just that one.odoo_setupnow declares every fieldmode=instanceactually returns. It was
additionalProperties: falseand the listing fields had been added to another
tool's schema by mistake, so every instance answer reported a schema error while
the effect still happened.
v0.11.2 — The READMEs were describing behaviour the code no longer had
Documentation release. The tool table was current; three other things were not,
and one of them was worse than missing.
The transport section documented the bug
It promised the client "never re-sends a refused call over the other
transport". That was true of the version that broke every model call against a
password target, and false from the moment a 401 started falling through. A
README describing the defect you just fixed is worse than a silent one: it tells
the reader the opposite of what the code does.
It now names the three fallback cases — the route is missing, the server predates
it, or the credential cannot be used there — explains WHY a 401 is not a refusal
(the request never ran, so serving it over the other transport cannot
double-apply anything), and keeps the guarantee precise: a call the server
actually PROCESSED and refused is never re-sent.
The translations decision was half-documented
odoo_i18n was in the tool table, but the languages are chosen in CLARIFY and
recorded on the SPEC. The README documented the tool and none of the decision
that drives it. Added the call, the fact that omitting it is a valid answer (not
a fail-closed gate), the gettext codes, the acceptance criterion it creates, and
the status line that reports it.
Also
communityRepoPath and enterpriseRepoPath were configurable and undocumented
in all three READMEs; odooApi was missing from the Chinese one.
Guarded, and verified to fail
Six checks per README now hold this: the credential fallback, the per-spec
translations call, and four config keys — 142 checks in the README suite, up
from 124. They were verified to FAIL on drift rather than merely passing: the
README was sabotaged, the guard caught it, and it was restored.
This is the first release since 0.7.2, which is what npm still serves.
Installing dsh-odoo-sdd today gets you 0.7.2 and none of the nine releases
below. Publishing this release closes that gap.
Install
dsh plugin add dsh-odoo-sdd
16 tools, 835 checks in the smoke suite. Verified against a live Odoo 19 instance,
including a real 290-term export and a round-trip that preserved hand-written
translations across a re-export.
0.7.2 — the active spec belongs to the session, and the edition to the spec
The Settings panel came back
The "Odoo SDD" section vanished after the host moved to 0.2.0-rc.2. That version
removed both halves of the settings contract this plugin used (settingsScope
on the client, installSection on the server), and the section was registered
inside the callback of an inject for a service that no longer existed — so it
stopped registering with no error anywhere.
The panel now registers through configForms + whileServed, and the host
derives the form from the plugin's own Config schema. Two further layers had to
follow, both able to empty the panel on their own: the form only keeps fields
marked .volatile(), and that method does not exist before schemastery 3.18.4 —
an older copy made the module throw at import, taking the whole plugin with it.
engines.dsh is now >=0.2.0-rc.1 and the schemastery peer >=3.18.4.
The Odoo edition belongs to the spec
licensed was plugin configuration that nothing read: the real decision is
state.licensed, answered per spec in CLARIFY. A workspace can hold EE and CE
projects at once, so a workspace-wide value was wrong for every project but one.
The key is gone from the schema, the resolved config, the handoff and the panel;
odoo_config still accepts it and reports it as obsolete rather than obeying
it silently.
Several Odoo targets per project
One .env per project meant overwriting it to switch instances. Targets now live
in .sdd/instances/<name>.env with one active, managed by
odoo_setup mode=instance list|show|use|add|remove. Grants were already per
target, so two instances on one server are two authorizations; use requires a
live grant, and several instances with none active is refused instead of
resolved by order.
The active spec belongs to the session
One .sdd/active.json per project meant every session rewrote the same pointer,
and the guard read it as authorization — so the last session to write decided
which spec the others were authorizing against. Observed live while building
this: the guard refused an edit citing a spec the session had never opened.
Pointers are now per session (.sdd/active/<sessionId>.json). A session's own
pointer always wins — including when it says "no spec"; the project file survives
as the last spec, adopted as a hint only when fresh and naming an existing
spec. adoptProjectPointerHint: false removes even that. Checkpoint snapshots
stay shared (they are the project's tree); which checkpoint is your rollback
target is per session, and dropping one clears the reference everywhere.
Install
dsh plugin --profile web add dsh-odoo-sdd
Full suite green: 708 smoke checks, 43 client, plus functional, import, README and
a 99/99 packaging contract.
v0.5.0 — the role files stop being "personas"
dsh-odoo-sdd 0.5.0
This release carries 0.4.0 plus two corrections. The 0.4.0 tag was never
cut, so the eight behaviour fixes below ship here for the first time: nothing
new was added in either version — every fix wires up a guarantee the plugin
already declared (the RPC error classification, the journal, the native
approval seam, the phase graph).
Part 1 — the eight defects (the 0.4.0 content)
Effects of an RPC call
executeKw()dropped the error class#rpc()had computed, so
isIndeterminateFor()always receivedundefined: a mutation whose answer
never arrived was reported as a plain server error, and the retry of
something that may already have committed looked safe. The class travels now.- CRUD was excluded from the indeterminate rule on the ad-hoc surface
(isBusiness &&), leavingcreate/write/unlinkretryable. - The journal entry was written after the postcondition check, so a failed
proof erased the created id and the pre-image of a mutation the server had
already accepted — nothing left to compensate. The receipt is taken first;
effect: "accepted"now separates "the server took it" from "the state was
proven", andcompensateincludes the first without calling it verified.
Limits of a functional batch
inspectdecided "is this mutating?" by asking the CRUD-only set, so a
kind: "method"batch executed a business action under the discovery
path. It is read-only now, andapplyrefuses a batch that changes nothing.- Business and import operations skipped
confirm_destructive, the production
backup and the identity requirement; a mutating operation must now declare a
postcondition or a stable identity, because one with neither can never be
reconciled. - The approval was validated only on entry and the spec-level
stop.mdwas
never read by the loop: both are re-checked between operations. reconciledecided from "the record exists" — which is what an update always
looks like. It reads the declared effect instead, and an outcome that cannot
be attributed stays undecidable, closed only by an explicit operator decision.- A batch whose operation was left unresolved could simply be sent again. Now
it is refused until the run is settled: reconcile first, then plan the
remainder.
Authorization, state and closing
- Phase gates accepted an
approval_markerthe model wrote itself, with zero
native approval requests. The host is asked now; the marker survives only as
the documented backstop for hosts with no approval seam. odoo_functionalwas missing from the mutating-call classification, so its
mutations bypassed the checkpoint and spec policies it announces.- The failure streak was cleared on the way into VERIFY, so three consecutive
failures were unreachable through real transitions; the iteration ceiling
counted phase hops instead of verification attempts; the guard authorized
against.sdd/active.jsoninstead of the spec's ownstate.json, so a spec
that had goneBLOCKEDkept authorizing changes. - A host whose
tools.guardis absent or fails to install stayed mounted
announcing a policy it could not enforce: it refuses to mount now. PASSEDrequires a row for every AC the spec declares (an omitted
criterion was invisible), is refused outside VERIFY/FIX_LOOP, records the
fingerprint of the evidence it used, andDONErefuses a verdict an edit made
stale.
Session isolation, context and routes
- Grants, the functional audit, the documentation language, the import file
authorisation and the sanitizer resolved the fallback root because they were
called without the execution context: two open projects could read each
other's. - RPC answers were cut at 4,000 characters and still said
OK. A cut answer is
now marked as partial, with the real size and the way to ask for less — never
by re-sending a mutation. - Both skills declared no usable resource base, so all nine
agents/…and
reference paths resolved only because the process cwd happened to be the
repository root.resources/is the single base now, every reference is
resolved from it inside the packed tarball, and the technical guide body went
from 32,495 to ~13,000 bytes by moving per-phase detail into the files that
own those phases. The contradictions between instructions were removed
instead of covered.
Part 2 — the two corrections on top of 0.4.0
Packaging: packed paths are compared separator-insensitively. The
resource-resolution check compared path.relative() output against the file
list npm pack --json reports. On Windows the first returns backslashes while
the second uses forward slashes even there, so every reference looked unshipped
and the packaging contract failed on both Windows jobs — and only there, which
is why Linux stayed green.
Vocabulary: the files stop being "personas". The package carried the term in
60 places: as the name of the folder holding the seven role files, as the way
the guide and the READMEs described them, and inside three source docstrings and
two hints the tools print back to the model. Every other directory, file, field,
config key and identifier is English, so the term stood out as the only
Spanish-derived vocabulary in the package and read as a mistake rather than a
deliberate loanword. resources/personas/ is resources/roles/ now, and the
prose says "role" — which is what each file already titled its first section
(## Role). This is a breaking path change.
Genuine uses are untouched: personal data in the English references, and in
the Spanish README persona where it means a human being and personalizados —
that is the language, not terminology.
Verification
npm run typecheck, npm test (smoke, client, readme, functional, import) and
npm run test:package (99 checks) pass, plus a mount check against the real
host packages (dsh/dsh-tools 0.1.7-rc.2): 15 tools registered, 1 policy
guard installed, both skills resolving their 14 references from resources/.
Breaking changes
- A batch with an unresolved operation is no longer re-applied.
inspectrefuses anything that mutates.- A phase gate asks the human; a declared marker alone no longer advances on a
host that offers the approval API. - The plugin refuses to mount without
tools.guardwhile either policy is armed. - Package layout:
agents/and the skill-localreferences/moved to
resources/personas/in 0.4.0 and toresources/roles/here. - A mutating batch operation must declare a postcondition or a stable identity.
Install
dsh plugin --profile <name> add dsh-odoo-sdd@0.5.00.4.0 — the guarantees it announced, actually enforced
dsh-odoo-sdd 0.4.0
Eight defects, each one a guarantee the plugin declared but never wired.
Nothing new was added: the fixes reuse the classification, the journal, the
native approval seam and the phase graph that were already there.
Effects of an RPC call
executeKw()dropped the error class#rpc()had computed, so
isIndeterminateFor()always receivedundefined: a mutation whose answer
never arrived was reported as a plain server error, and the retry of
something that may already have committed looked safe. The class travels now.- CRUD was excluded from the indeterminate rule on the ad-hoc surface
(isBusiness &&), leavingcreate/write/unlinkretryable. - The journal entry was written after the postcondition check, so a failed
proof erased the created id and the pre-image of a mutation the server had
already accepted — nothing left to compensate. The receipt is taken first;
effect: "accepted"now separates "the server took it" from "the state was
proven", andcompensateincludes the first without calling it verified.
Limits of a functional batch
inspectdecided "is this mutating?" by asking the CRUD-only set, so a
kind: "method"batch executed a business action under the discovery
path. It is read-only now, andapplyrefuses a batch that changes nothing.- Business and import operations skipped
confirm_destructive, the production
backup and the identity requirement; a mutating operation must now declare a
postcondition or a stable identity, because one with neither can never be
reconciled. - The approval was validated only on entry and the spec-level
stop.mdwas
never read by the loop: both are re-checked between operations. reconciledecided from "the record exists" — which is what an update always
looks like. It reads the declared effect instead, and an outcome that cannot
be attributed stays undecidable, closed only by an explicit operator decision.- A batch whose operation was left unresolved could simply be sent again. Now
it is refused until the run is settled: reconcile first, then plan the
remainder.
Authorization, state and closing
- Phase gates accepted an
approval_markerthe model wrote itself, with zero
native approval requests. The host is asked now; the marker survives only as
the documented backstop for hosts with no approval seam. odoo_functionalwas missing from the mutating-call classification, so its
mutations bypassed the checkpoint and spec policies it announces.- The failure streak was cleared on the way into VERIFY, so three consecutive
failures were unreachable through real transitions; the iteration ceiling
counted phase hops instead of verification attempts; the guard authorized
against.sdd/active.jsoninstead of the spec's ownstate.json, so a spec
that had goneBLOCKEDkept authorizing changes. - A host whose
tools.guardis absent or fails to install stayed mounted
announcing a policy it could not enforce: it refuses to mount now. PASSEDrequires a row for every AC the spec declares (an omitted
criterion was invisible), is refused outside VERIFY/FIX_LOOP, records the
fingerprint of the evidence it used, andDONErefuses a verdict an edit made
stale.
Session isolation, context and routes
- Grants, the functional audit, the documentation language, the import file
authorisation and the sanitizer resolved the fallback root because they were
called without the execution context: two open projects could read each
other's. - RPC answers were cut at 4,000 characters and still said
OK. A cut answer is
now marked as partial, with the real size and the way to ask for less — never
by re-sending a mutation. - Both skills declared no usable resource base, so all nine
agents/…and
reference paths resolved only because the process cwd happened to be the
repository root.resources/is the single base now, every reference is
resolved from it inside the packed tarball, and the technical guide body went
from 32,495 to ~13,000 bytes by moving per-phase detail into the personas that
own those phases. The contradictions between instructions were removed instead
of covered.
Verification
npm run typecheck, npm test (smoke, client, readme, functional, import) and
npm run test:package (99 checks) pass, plus the real-cordis mount check on
host 0.1.7-rc.2: 15 tools, 1 policy guard, both skills resolving from
resources/.
Breaking changes
- A batch with an unresolved operation is no longer re-applied.
inspectrefuses anything that mutates.- A phase gate asks the human; a declared marker alone no longer advances on a
host that offers the approval API. - The plugin refuses to mount without
tools.guardwhile either policy is armed. - Package layout:
agents/and the skill-localreferences/moved to
resources/personas/andresources/references/.
0.3.0 — the RPC can call any public method, behind one confirmation
The ad-hoc surface was CRUD-only: the method enum of the schema cut a business
action before the handler ever ran, so an agent analysing a module could read
create_invoices in the source and still not call it. That wall answered the
wrong question. What this plugin has is a replay problem — it cannot undo
what a business action does — and a replay problem is answered by a confirmation,
not by refusing the call.
Added
- Any public method of any model runs on the RPC, with
confirm_destructive=trueand no allowlist:action_*,button_*,do_*,
whatever the model exposes to do something. The list exists only where it
earns its keep (the auditable batch path). - The public read API, complete:
search,name_get,name_search,
default_get,exists,check_access_rights,check_access_rulejoin
search_read/read/search_count/read_group/fields_get. A read needs no
confirmation and no list. - A state guard and a state proof on the ad-hoc surface too, in the same
shape a batch declares and evaluated by the same helper:precondition
(checked before — if it fails, nothing is sent) andpostcondition
(checked after — if it fails, the answer says the call WAS sent and the
instance may be half-changed). argsandkwargsfor the method's own signature: the recordset goes as
args[0](Odoo dispatches on it), positionals follow inargs, keywords in
kwargs. The read/CRUD parameters (values,domain,fields…) are
refused here instead of being silently ignored.INDETERMINATEfor a business action lost mid-flight: a transport or
protocol failure is not a failure, it is an unknown outcome, and reporting it as
a failure is what invites a retry of something that already happened. One rule,
shared with the batch executor.
Changed
- The
methodenum is gone. The classification that the batch executor
already used now decides both surfaces, from one module: read, CRUD, business. - The guard treats a business action as a mutation — checkpoint policy, change
policy and phase rule apply to it like any other write. A read is not a
mutation, and a name the RPC cannot call is left to the tool's own (more
precise) refusal. - A business action is never journaled and never auto-compensated, and the
result says so: "NOT journaled, so no undo exists for this call", plus a warning
when it ran without a precondition (no starting state confirmed) or without a
postcondition (OKthen only means the server accepted the call). - The batch remains the auditable path — allowlisted pair, per-run approval,
required guard and proof — and the docs now say when each one is the right tool
(references/business-methods.mdhas the comparison table).
The wall that stays is Odoo's
A private method (_name) cannot be called over RPC at all, in any supported
version: execute_kw dispatches through get_public_method
(odoo/service/model.py), which answers
AccessError: Private methods (such as 'model._name') cannot be called remotely.
for a leading underscore, for init, for methods decorated @api.private (17+)
and for the internal attribute names; an inexistent method answers
AttributeError. Refusing it locally turns a guaranteed server traceback into an
instruction — read the model in the source (the native/custom repositories are
reported by odoo_config mode=read) and call the public entry point that wraps it:
sale.order._create_invoices is private, while create_invoices() on the
sale.advance.payment.inv wizard is public.
Deliberate asymmetry, so nobody is surprised: the model allowlist
(executeAllowlist) still governs plain create/write/unlink; a business
action's effect is not limited by it, and its authorization is your
confirm_destructive plus, when you declare them, the state guard and the proof.
Install
dsh plugin --profile web add dsh-odoo-sdd # or pin @0.3.00.2.0 — a change after the run needs its own spec, or your waiver
A request that arrives after the pipeline closed — "now also change X", in the
same chat — used to be edited straight into the project. The pipeline authorized
ONE spec and stopped authorizing it at DONE, but nothing enforced that: the
guard covered the instance and left the source files open. 0.2.0 closes it, and
closes it generically: nothing here knows about any particular Odoo model or
method. A small modification is a change like any other, so it gets its own
(small) spec — or an explicit waiver from you.
Added
- A change needs a spec in a writing phase, or your waiver. The guard now
answers that question first, on both surfaces: instance mutations
(odoo_executecreate/write/unlink,odoo_module install|upgrade,
odoo_import prepare) and project file edits through the host'swrite/edit
tools. The spec documents (specs/) and the plugin's own state (.sdd/) are
never blocked — otherwise the way out would be blocked too, and anything
outside the project is none of the plugin's business. - The refusal names the three ways out, so it is a turn, not a wall: open a
small spec for THIS change (mode=bug: one acceptance criterion, no design
interview), continue the active spec if the change is one of its criteria, or
ask the developer for a waiver. sdd_phase operation=waive. The developer's "leave it to your judgement",
kept verbatim, behind native approval, in.sdd/waiver.json. It covers this
session only — the next chat starts under the policy again — and it is not a
silent exception: the decision lands in the KB, the checkpoint policy stays
armed (a waiver answers the spec question, never the rollback one), and
handoff.mdgets a## Waivers (work done without a spec)section, so the
closing document cannot read as if every change had a spec.revoke: true
drops it and needs no approval: tightening a policy is always allowed.requireSpecForChanges(defaulttrue) — the human off-switch, the same
shape as every other policy: Settings panel (ES/EN),odoo_config mode=set,
cordis.patch.yml, status line, and it is reported by the handoff.sdd_phase statustells you where you stand: the policy in effect, the
recorded waiver and whose session it belongs to, and — on aDONEspec — that
the next request needs a new spec.- The intake rule, written where it is read: the workflow skill and the developer
persona now say to take the intake decision (own spec, active spec, or waiver)
before touching a file, and never to route around a refusal — no writing
throughbash, no flipping the policy, no editing.sdd/active.json.
Changed
DONEstops authorizing changes. It stays the honest verdict of what was
verified; a follow-up is its own spec.- A waived session is not blocked by the phase rule either — the two answers
that open the change gate (policy off, waiver for this session) also open the
mutation phase check. The checkpoint requirement is its own policy and stays
armed.
Upgrading from 0.1.x
The default now gates changes, which is a behaviour change for anyone already
using the plugin — that is why this is a minor bump and not a patch. With
requireSpecForChanges on (default), an edit made outside a spec in a writing
phase is refused until you open a small mode=bug spec or approve a waiver for
the session. Setting requireSpecForChanges: false in the panel restores the old
behaviour, per project.
Honest limits, both documented in the READMEs: bash can write files and is
not gated (the host exposes no after-the-fact inspection of what a shell
command wrote — the policy makes the pipeline the path of least resistance and
the exception visible, it is not a sandbox), and odoo_docs keeps its
instance-free, spec-free path because it only writes documentation files.
Install
dsh plugin --profile web add dsh-odoo-sdd # or pin @0.2.0