L'article 289 impose une numérotation chronologique et continue aux factures que
la société ÉMET. Il ne dit rien de la référence de classement que Dolibarr
attribue à celles qu'elle REÇOIT : le numéro qui fait foi pour ces dernières est
celui du fournisseur, porté par ref_supplier, et la séquence FAF suit l'ordre
d'ENREGISTREMENT — c'est sa construction normale, pas un défaut.
Appliquée aux deux registres, la garde produisait un faux positif systématique.
La production porte DÉJÀ cinq ruptures dans la séquence FAF — FAF2026003 daté du
4 janvier suit FAF2026002 daté du 9 — et ZÉRO dans la séquence FAC. Or adc-008
prescrit d'enregistrer une facture fournisseur à la date du document : le cas
ordinaire se heurtait donc au refus et exigeait ARCO_ALLOW_BACKDATE.
Une garde qu'on outrepasse par routine ne garde plus rien. Elle avait déjà été
outrepassée deux fois en une journée sur les factures Anthropic, et le même faux
positif avait été produit par le juge pré-gate sur le change-set d'indemnité —
signe que l'erreur était dans la règle, pas dans son application.
Vérifié sur les deux registres :
facture FOURNISSEUR antidatée au 01/03 -> créée
facture CLIENT antidatée au 01/03 -> REFUSÉE (FAC008 au 23/07)
La garde protège désormais là où la loi s'applique, et cesse d'obstruer là où
elle ne s'applique pas.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Dolibarr n'expose AUCUNE API REST pour les charges sociales (/taxes,
/socialcontributions, /chargesociales répondent tous « API not found »). Le
module est actif, seule l'API manque : le pipeline de promotion, qui parle REST,
ne peut pas porter cette opération. D'où un script UI, gardé par guard.ts.
La sandbox a joué son rôle : quatre doublons y ont été créés pendant la
découverte, sans conséquence, et les quatre pièges du formulaire sont désormais
documentés au lieu d'être redécouverts.
- recordSocialCharge.ts : idempotent (cherche la charge avant de créer),
vérifie par LECTURE de la liste, type TNS par défaut, --dry-run.
- RUNBOOK_charges_sociales.md : écrit pour être suivi par un agent moins
performant ou un harness limité — la commande, les garanties, les quatre
pièges, et ce que le script ne garantit PAS (ni juge, ni artefact de gate).
Les quatre pièges, tous rencontrés :
1. La date visible est décorative : le backend ne lit que les champs CACHÉS
echday/echmonth/echyear. Remplir le champ texte crée l'enregistrement avec
une période aberrante (20/06/2000 observé) au lieu d'échouer.
2. Le bouton de soumission n'a pas d'attribut name — le cibler par value.
3. L'URL après soumission ne porte pas d'id : vérifier par l'URL fait conclure
à un échec sur une création réussie. C'est ce qui a produit les doublons.
4. Les milliers portent une espace insécable (« 1 215,00 ») : une comparaison
littérale casse au-delà de 999 € et l'idempotence saute en silence.
Correction comptable : 645x → 646 dans known-patterns.json. Les cotisations d'un
gérant TNS sont des cotisations personnelles du dirigeant (646), pas des
cotisations patronales sur salaires (645) — Arcodange n'a aucun salarié.
Appliqué en production : les trois échéances URSSAF 2026, toutes IMPAYÉES.
Reste à vérifier dans le dictionnaire Dolibarr que le type « indépendants »
porte bien le code comptable 646.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
L'extraction de champs vivait dans un heredoc à l'intérieur d'email-inspect.sh :
impossible à exécuter isolément, donc jamais mesurée, donc fausse sans que
personne puisse le voir. Sur la facture Darnis F1048 elle renvoyait le numéro de
TVA d'Arcodange comme référence de facture, et aucune date.
- extract_fields.py : l'extraction sort du shell et devient un module.
- test_extract.py : régression contre les 16 factures hand-vérifiées de
fleet/golden/invoice-extract/. Score par champ, et une valeur FAUSSE pèse plus
qu'une valeur absente — un humain recopie ce qui s'affiche.
Valeurs fausses : 4 → 0. Exactitude ref 62,5 → 75 %, date 62,5 → 75 %,
HT 68,8 → 75 %, TTC 81,2 → 93,8 %.
Cinq bugs réels, dont trois invisibles sans test :
- « Nº » sur les factures françaises est U+00BA (ordinal masculin), pas le signe
degré. La classe [°o] le rate, le motif principal échoue, et le repli attrape
le premier jeton ref-shaped du document — très souvent un numéro de TVA.
- Le filtre anti-TVA rejetait « FR73261832 », qui est la vraie référence OVH : un
numéro FR fait exactement 11 caractères après le préfixe.
- « Montant total (HT) » était lu comme un TTC.
- Une référence coupée par la colonne (« 06-01-26- » / « payment-366753 ») était
renvoyée amputée : le recollage doit précéder le scan, sinon la queue seule est
trouvée en premier.
- Un `\b` après `€` ne peut jamais matcher en fin de ligne (€ n'est pas un
caractère de mot) — la TVA n'était jamais extraite.
adc-008 : une facture fournisseur s'enregistre à SA date, même future, tant que
l'exercice (année civile) ne bascule pas. Le document fait foi ; altérer sa date
ferait diverger l'écriture de sa pièce justificative (CGI art. 289 VII).
Registre validé : 8 règles, 8 ADC, 0 erreur.
scopes.ts : 1232 (factures fournisseur) ajouté à prod-write — oubli initial,
révélé par un 403 en production sur F1048. Le pipeline s'est arrêté sans écrire.
Appliqué en production via le pipeline gated : FAF2026014 (Darnis F1048),
218,50 HT + 43,70 TVA = 262,20 TTC, validée, non réglée.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
Completes the role model on the sandbox side, and closes the third failure of
the 2026-07/08 sessions: a refresh wiped hand-granted rights and nothing
recorded them, so the sandbox silently lost capabilities nobody had written down.
- Provisioned `ai_agent_sandbox_read` (36 rights) and
`ai_agent_sandbox_sandbox_write` (44 rights) from test/scopes.ts.
Verified functionally on the live sandbox: the reader reads and gets
403 on invoice creation; the writer creates a draft and gets 403 on DELETE.
- checkpoint-provision.sh now re-creates both scoped agents after every
refresh, so their rights come from code rather than from someone's memory.
Failure to provision a scope warns instead of aborting the whole checkpoint.
- checkpoint-relink-env.sh points the write skill at the scoped writer key,
falling back to the legacy single-user key so an older checkout still works.
The write skill now operates as ai_agent_sandbox_sandbox_write (id 6).
Smoke-tested end to end after the credential swap: the promote pipeline
rehearses on the sandbox under the scoped writer, and `apply` still refuses
without a recorded human gate.
The redundant `ai_agent_prod_prod_write` login is documented as deliberate:
renaming a provisioned production credential means creating a second privileged
user and repointing the promote flow — churn for cosmetics.
Left behind in the sandbox: draft invoice id=19, a scope probe. It cannot be
deleted (no scope grants DELETE, which is the point) and the next refresh
reclaims it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
Two guards, both from real incidents in the same session.
1. invoice-create.sh — chronology (CGI art. 289). Dolibarr assigns the number at
validation, in creation order, so issuing a document dated BEFORE the last one
already issued gives a higher number to an earlier date. The July plan walked
straight into it: the M3 deferred part is due 2026-10-23 and must be issued at
D-60 (24/08) to stay under the L.441-10 I ceiling, while the M4 fixed part is
dated 23/08 — issue them in the wrong order and the numbering breaks. The
guard reads the last issued document of the same kind and refuses an earlier
date, with ARCO_ALLOW_BACKDATE as a loud, documented override.
Verified: refuses a 01/07 invoice against FAC008 (23/07), accepts 23/08.
2. test/scripts/guard.ts — production opt-in. The sandbox-only guard had no way
to express a deliberate production run, so any prod work meant bypassing it
entirely (which is how guards die). Production now requires BOTH
ARCO_ALLOW_PRODUCTION=<exact host> and
ARCO_PROD_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD, and prints a banner. Nothing
reaches prod by inheriting an ambient variable.
Also fixes a misleading "(sandbox verified)" log that printed even on prod.
grantAgentRight.ts joins the repo (it was never committed) and gains --revoke,
so a temporarily elevated right can be handed back — used today to attach a
payment in production and revoked immediately after.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
document-attach.sh uploads a source piece (the supplier's own PDF) onto an
invoice's GED via POST /documents/upload — idempotent by (object, filename,
sha256): before any POST the object's GED is listed and a same-named entry is
downloaded back and sha256-compared. Identical → deduped no-op; different
content → ABORT (refuse-never-repair, overwriteifexists always 0, never
Dolibarr's overwrite flag). Read-back after upload: re-list + download +
sha256-verify. Module-relative download paths are derived from the listing's
fullname (supplier invoices carry an id-derived get_exdir prefix like
9/2/FAF2026013/…, so reconstruction would be wrong).
Promote integration: new `attach` op in promote-plan/promote-apply (OP_SCRIPT),
object_id resolvable via @ref and #supplierinvoice lookups; a relative `file`
resolves against the manifest's directory (replay packs carry pdfs/ beside the
manifest, gitignored — README documents the books@ re-fetch message ids).
promote-plan prints each file's sha256 (or a loud MISSING) at review time.
CLI: `arcodange sandbox attach`.
Proof: offline case 12 in tests/run-tests.sh (upload body, dedupe, conflict
abort, field refusal, manifest-relative resolution via stubbed /documents);
live: manifest-C-ged-attach.json applied twice on the sandbox — run 1 four
created, run 2 four deduped, one GED file per FAF2026010-013, stored sha256s
equal to the re-fetched sources; tests/replay-idempotency.sh extended with an
attach op (4 created → 4 deduped, ged_files count unchanged) and a live
same-name/different-bytes abort verified.
Closes erp#43
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
Learning #4 of the 2026-07-11 rehearsal: manifest B failed mid-run and could
not be re-applied — op 1 (the DARNIS invoice) had already run and a replay
would have duplicated it. Every write op now dedupes BEFORE any POST:
- thirdparty-create.sh: by exact name (promote '#thirdparty:name=' semantics);
ambiguous (2+) aborts; an existing fiche missing the requested role aborts
(refuse-never-repair). Emits {"id", "deduped"} instead of a bare id.
- invoice-create.sh: supplier kind by (socid, ref_supplier) — same key with a
different total aborts as a conflict; customer kind (or supplier without
ref_supplier) by (socid, date, total_ttc ±0.02, line fingerprint) with descs
HTML-unescaped. Credit notes are never candidates. A deduped DRAFT with
validate:true is validated on replay, so an interrupted run converges.
- payment-record.sh: by (invoice, amount, normalized transaction_id), composing
with the erp#37 varchar(50) normalization on BOTH sides so historical
long-form nums still match; same tx + different amount aborts; without a tx
id there is no dedupe key (warned). Dedupe answers id:null (the payments list
exposes no paiement rowid) + the existing bank line.
- All three refuse to POST blind when the dedupe lookup fails with anything but
the documented empty-list 404 (the voir_tous trap would otherwise mint dupes).
- promote-apply.sh: marks each op created / deduped=true inline and totals them
in the summary — an all-deduped second run is visible proof of a no-op.
- promote-plan.sh: advertises each op's dedupe key (and flags tx=MISSING as
'a replay WILL double-pay').
Proof:
- tests/run-tests.sh: 5 new offline cases (11 total) — dedupe hits POST
nothing, conflicts/ambiguity abort pre-POST, long-form history dedupes,
draft convergence validates; stub extended to serve the new lookups with the
live-observed empty behaviors ([] for invoices/payments, 404 for tiers).
- tests/replay-idempotency.sh (new, live): double-applies a self-contained
manifest on the sandbox — run 1 '3 created' (rows 1/1/1), run 2 '3 deduped'
with row counts unchanged and the stored num in erp#37 short form.
- The historic manifest-B now replays on the sandbox as 5/5 deduped, zero new
rows — the exact replay Learning #4 declared impossible.
SKILL.md updated in the same change (per-op dedupe keys, replay-safety section,
gotchas); the 2026-07-11 runbook's Learning #4 carries a dated resolution
addendum.
Closes erp#44.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
- MISTRAL.AI: update note to reflect annual subscription (Le Chat Pro - Annual)
from invoice MSTRL-API-814045-001, 2026-04-02, 143.90 HT / 172.68 TTC.
Next expected ~2027-04.
- CLAUDE.AI: document payment rail moved to personal card (fk_account=3,
API-invisible) for May/June; reference issue #57.
Both AI subscriptions are now recorded supplier invoices (post-replay).
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <[email protected]>
Part of erp#65 (phase 1). Ledger grammar "thirdparty complete" gets its
op: allowlisted non-ledger fields, per-field diff read-back. Contacts
are born idempotent (dedupe by email then name). Promote ops wired both
targets, offline stub tests, SKILL.md workflows, KM dossier manifest
(unsigned-contract truth fix + EIN-to-collect note).
Co-Authored-By: Claude Fable 5 <[email protected]>
The pack (manifests, prelude, runbook, verify-provenance.py PoC) lived
only in an ephemeral session scratchpad while erp#41/#42/#43/#44 now
reference it as fixtures and the prod replay is still pending. 36/36
provenance checks were green at rehearsal time; PDFs are re-fetchable
via arcodange-email-ingest (documented in the pack README).
Co-Authored-By: Claude Fable 5 <[email protected]>
Qonto transaction ids run ~67 chars (<org>-<n>-<n>-transaction-<uuid>) but
Dolibarr stores num_payment in varchar(50) (llx_paiement.num_paiement,
llx_paiementfourn.num_paiement) — POSTing a payment with the raw id fails
HTTP 400 "value too long for type character varying(50)". Parade proven live
on the sandbox (2026-07-11): store the UUID suffix (globally unique, ~37
chars). Wise ids (short numerics) are unaffected.
Writer side — payment-record.sh strips everything through "transaction-"
before POST, announces the normalization on stderr, REFUSES (never truncates)
ids still >50 chars after normalization, and emits the normalized num in the
output JSON.
Reader side — bank-match.sh PASS 0 (exact tx-id, erp#28) now compares BOTH
sides in raw AND canonical short form: Qonto feed ids are carried long+short,
payment nums are normalized on compare — so nums stored short (the varchar(50)
form) and historical long-form nums both keep matching. Wise ids untouched.
Proven offline (no credentials, no network, no sandbox/prod writes):
- arcodange-bank-reco/tests/run-tests.sh — new bank-match --fixtures offline
mode: long feed id ↔ short num, long ↔ long (back-compat), Wise numeric,
each Δ+19d outside the ±7d window so only PASS 0 can pair them (exit 0,
3×[tx-id]); plus the empty-num negative (exit 1, 0 matched).
- dolibarr-sandbox-write/tests/run-tests.sh — payment-record via a stubbed
dol-write.sh (DOL_WRITE hook): long→short in POST body + output JSON, Wise
untouched, >50-after-normalization refused BEFORE any POST, citing
varchar(50).
Both SKILL.md document the canonical short form + the varchar(50) constraint.
Co-Authored-By: Claude Fable 5 <[email protected]>
The checkpoint status/relink-env armed probe calls GET /users/info, which
requires Dolibarr right 251 (user->user->lire). WRITE_IDS didn't include it,
so a freshly provisioned agent answered 403 on the probe — reported NOT armed
— while its key actually authenticates (GET /thirdparties -> 200). Right 251
was granted live in SQL on the sandbox (fk_user=4) today; this persists it in
WRITE_IDS so every future provision grants it.
Also teach both probes to tell the failure modes apart instead of one opaque
message: 401 = key rejected (stale/instance-encrypted -> re-provision),
403 = key OK but right 251 missing (-> grant it / re-provision), 200 = armed.
Docs updated accordingly (checkpoint SKILL.md probe outcomes, sandbox-write
SKILL.md gotcha, test/README.md rights table synced to WRITE_IDS incl. 262/111).
Co-Authored-By: Claude Fable 5 <[email protected]>
A skill + CLI group to drive the ADR-0003 sandbox lifecycle, instead of the manual
kubectl/deno/.env dance:
arcodange sandbox checkpoint status # liveness + is the write agent armed?
arcodange sandbox checkpoint refresh --yes # re-seed iso-prod (DESTRUCTIVE, gated)
arcodange sandbox checkpoint provision # re-create ai_agent_sandbox (Playwright) + relink
arcodange sandbox checkpoint relink-env # rewrite write skill .env from the key + verify
- refresh wraps ops/sandbox/sandbox-lifecycle.sh; requires --yes (it wipes the agent
too, since iso-prod overwrites llx_user). --db-only skips the documents sync.
- provision runs test/provisionSandbox.ts (you do the admin login — PROD creds,
iso-prod) then auto-relinks; relink-env writes .env mode 600 and verifies via
GET /users/info.
- scripts resolve the repo root from ARCO_ROOT (set by bin/arcodange) or their own
path, so they work via the CLI or standalone.
Tested: status reports armed/not-armed correctly; refresh refuses without --yes
(exit 3); relink-env errors with no key (exit 1); help/usage wired.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The consumer side of erp#26/#27: now that a règlement stores its originating bank
transaction id (transaction_id -> llx_bank.num_chq), bank-match uses it.
New PASS 0 (exact), highest priority, before wire-ref and amt+date:
- carry each feed movement's own id (Qonto transaction id; Wise activity + transfer
resource id) as feed_ids, and each Dolibarr payment's num.
- match when a payment's num equals a feed id. Tagged [tx-id].
- DATE-WINDOW-INDEPENDENT — the id is proof, so it pairs movements whose bank
settlement and Dolibarr saisie are weeks apart (which amt+date would miss).
Pass 0 runs before the ref index is built, so its matches are excluded from the
later passes (no double-match).
Fixture-proven: a payment dated 15d off the bank movement (outside the ±7d window)
matches via [tx-id] when num carries the Qonto id, and correctly does NOT match
when num is empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Make the originating bank transaction id a first-class input on payment-record.sh
so every règlement is tied to the real bank movement at write time.
- `transaction_id` is the canonical field (the Qonto/Wise feed tx id); `num` stays
as a back-compat alias. It's stored on the payment's bank line (llx_bank.num_chq),
the reconciliation key.
- Recording WITHOUT a transaction_id prints a stderr warning (still posts, but won't
auto-reconcile) — nudges the agent to always carry it.
- Output normalises to {id, bank_transaction_id, transaction_id}.
- Promote: manifests' payment ops carry transaction_id; promote-plan shows it
(tx=… or tx=MISSING).
Proven live: customer + supplier record with transaction_id; the `num` alias maps
to the same field; the no-tx warning fires; promote plan/apply carry it through.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
A payment only returned its paiement id, which isn't what bank reconciliation
keys on. payment-record.sh now emits {id, bank_transaction_id, num}:
- bank_transaction_id = the Dolibarr bank line (llx_bank.fk_bank_line) the payment
created, resolved via GET /{invoices|supplierinvoices}/{id}/payments (correlated
by num, else the most recent line). Works for customer and supplier.
- num stores the originating bank tx id (Qonto/Wise) and lands on that bank line's
num_chq — so arcodange-bank-reco can match a règlement to a statement line by id
instead of fuzzy amount/date. Both ends captured at write time.
Proven live: customer {id:13,bank_transaction_id:35,num:QONTO-TX-1234},
supplier {id:16,bank_transaction_id:36,num:WISE-TX-5678}; llx_bank rows 35/36
carry the refs in num_chq. promote-apply still extracts .id unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The two V9 follow-ups, both proven live on the sandbox.
- creditnote-create.sh: `kind:"supplier"` makes an avoir fournisseur on
/supplierinvoices (type=2 + fk_facture_source, carries ref_supplier); default
customer path unchanged. Proven: customer AVC002 (-240) + supplier AVF2026001
(-144, ref_supplier carried, linked to source, validated).
- bank-accounts.sh + `arcodange sandbox accounts`: list bank accounts (id/label/
bank) so a payment can pick its account_id. Needs `banque lire` (rights 111),
now added to the provisioner's WRITE_IDS so fresh runs include it; the existing
ai_agent_sandbox user was granted it live. GET /bankaccounts now returns the 3
accounts (QONTO, WISE EURO, Compte Courant Asso).
- SKILL.md: supplier-avoir example + accounts helper + updated banque-lire note.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes the last promote gap: a manifest can now reference records it does NOT
create. A value like "#thirdparty:name=KissMetrics" (or :code=CL0007) is looked
up on the TARGET at apply time and resolved to that target's id — so the same
manifest is portable (sandbox id on --target sandbox, prod id on --target prod).
promote-apply.sh: resolve() gains a "#" branch + a lookup() helper that queries
the target via the GET wrapper with sqlfilters. Supports thirdparty
(name/code/supplier_code) and invoice/supplierinvoice (ref/ref_supplier). A
lookup matching nothing OR more than one record ABORTS the run — it never
guesses, so it cannot write to the wrong entity.
Proven live: "#thirdparty:name=ACME Conseil" resolved to the existing client and
invoiced it; a not-found code and an ambiguous (2-match) name both aborted with
exit 1. Combined with @refs, arbitrary self-contained-or-referential change-sets
now replay cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The human-gated path that carries a reviewed sandbox change to prod.
- promote-plan.sh: render a manifest (JSON array of write ops with symbolic @refs
instead of ids — portable sandbox->prod) as a human-readable change-set.
- promote-apply.sh <manifest> --target sandbox|prod: replay it, resolving each
@ref to the id actually created during the run (dependent ops wire up). sandbox
rehearses via dol-write.sh; prod via dol-prod-write.sh.
- dol-prod-write.sh: the ONLY prod-write path. Prod key read from the ENVIRONMENT
only (DOLIBARR_PROD_WRITE_KEY, never a stored .env); every write refused unless
ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD.
- create scripts take a DOL_WRITE override so promote-apply reuses them per target.
- bin/arcodange: `promote {plan|apply}` group + example manifest.
- payment-record.sh: fixed supplier payments (payment_mode_id + closepaidinvoices).
Proven live: plan renders; apply --target sandbox replays a 3-op chain with refs
resolved (@tp1->id, invoice socid=@tp1, payment invoice=@inv1); --target prod
without the confirm flag is REFUSED before sending. Supplier payment now works
end-to-end via the script.
Limitation (documented): manifests reference entities they create (@ref);
pre-existing prod entities need business-key resolution (follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- dolibarr-sandbox-write/scripts/creditnote-create.sh: create a customer avoir
(credit note) — a customer invoice type=2 referencing source_invoice
(fk_facture_source); amounts negative, validates to an AVC… ref. Proven live.
- bin/arcodange: new `sandbox` command group wiring the write scripts —
`arcodange sandbox {thirdparty|invoice|payment|creditnote|write}` (JSON on
stdin). Header + usage updated to note the CLI now does host-guarded sandbox
writes (still read-only on prod).
- SKILL.md: avoir workflow + CLI notes.
Verified end-to-end through the CLI: thirdparty -> invoice (FAC…) -> avoir
(AVC…, total_ttc -240, fk_facture_source set); host-guard intact via the CLI.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The write-capable companion to the read-only dolibarr* skills, scoped to the
erp-sandbox. Lets an AI agent rehearse bookkeeping writes against a copy of prod
(ADR-0003) before a human promotes the reviewed change to prod.
- scripts/dol-write.sh: write wrapper that REFUSES any host that is not
erp-sandbox.arcodange.lab (the structural prod-safety guarantee) using the
ai_agent_sandbox key from a gitignored .env.
- scripts/thirdparty-create.sh: create client/supplier fiches; codes auto-assign
via the elephant mask (code="-1").
- scripts/invoice-create.sh: customer (/invoices) or supplier (/supplierinvoices)
invoices with product/service lines + ref_supplier, optional validate.
- scripts/payment-record.sh: record a règlement (VIR/CB/CHQ/LIQ); customer pays
full + marks paid, supplier needs an amount.
- SKILL.md (safety model + workflows + the human-gated promote flow), .env.example,
example input.
Proven end-to-end live against the sandbox: client -> invoice (service+product
lines, HT 1100 / TTC 1320) -> validate -> payment (paid); supplier -> supplier
invoice (ref_supplier carried) -> validate. Host guard verified to refuse a prod
URL before sending.
Avoirs (credit notes) and bin/arcodange CLI wiring are planned follow-ups.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
email-list.sh gains two hard-exclusion filters (applied before the
candidate test, regardless of attachments):
- EXCLUDE_PATTERN matches subjects starting with Invitation: / Updated
invitation: / Canceled event: / Accepted: / Declined: / Tentative: /
Maybe: (after stripping Re:/Fwd:/Tr: prefixes). Filters Google Calendar
events that always carry an .ics attachment.
- EXCLUDE_SENDER matches updates.<domain>, noreply@*calendar, news@,
newsletter@. Filters newsletter blast traffic.
Effect on --all-folders --candidates-only baseline: 27 noisy → 12
actionable (calendar invites + the staying-ahead.ai newsletter blast
removed). Real supplier docs intact: Darnis F1042 in /Notification, 3 Free
Mobile factures in /Inbox/abonnements, Mistral + Anthropic in /Inbox/books.
The originally-planned --mark-ingested feature is deferred to V8.2:
flag-set requires the Zoho OAuth scope ZohoMail.messages.UPDATE which our
read-only refresh_token doesn't have. Documented in SKILL.md: once the
user opts in to the wider scope, --mark-ingested becomes a one-line flag
on email-inspect.sh and is_candidate() learns to skip flag_info messages.
Captured the new --all-folders baseline at examples/email-list-all-folders.txt.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
V8 — first inbound-side skill. Closes the loop from "bill arrives by email"
to "ready to enter in Dolibarr UI". Read-only at every layer.
What ships:
- arcodange-email-ingest/scripts/zoho-curl.sh OAuth wrapper with token cache
(50 min TTL, mode 600) — avoids
hitting Zoho OAuth rate limit on
every invocation.
- arcodange-email-ingest/scripts/email-list.sh List candidates in /Inbox/books
(where the books@ alias auto-
routes mail). --candidates-only
filter on supplier patterns or
attachments. --all-folders to
scan everything.
- arcodange-email-ingest/scripts/email-inspect.sh Pull message + attachments,
pdftotext on each PDF, heuristic
extract (supplier, ref, dates,
totals, VAT rate), emit Dolibarr
supplier-invoice draft JSON.
Architecture choice — Zoho API (not IMAP):
- [email protected] is an alias of [email protected] → one OAuth
refresh_token covers everything.
- Gmail folded in via forwarding ([email protected] → books@) — no Google
API setup, no app-passwords, no second OAuth flow.
- Token-based auth, no SCA rabbit hole.
V8.0 baseline (in /Inbox/books):
- 3 candidates: Mistral AI facture, Anthropic Stripe receipt (Fwd Gmail),
INPI payment receipt (Fwd Gmail).
- Heuristic extraction is best-effort: works on amounts/refs for some
templates, misses others (Mistral PDF format, Stripe receipt layout).
- --save-pdf <DIR> lets the operator grab the PDFs for manual entry when
the heuristic falls short.
Rate-limit pitfall documented: Zoho OAuth refresh has an aggressive throttle
("too many requests continuously"). The cache file at $TMPDIR/zoho-access-$USER
(mode 600, 50 min TTL) prevents this; on 401 the wrapper auto-refreshes once
and retries.
V8.1+ ideas in SKILL.md out-of-scope:
- mark ingested emails (IMAP flag or Zoho label)
- body text extraction (inline-HTML invoices)
- per-template parsers or LLM-based extraction
- IMAP fallback for non-Zoho mailboxes
CLI: bin/arcodange email {list|inspect|curl} integrated.
Base updates: dolibarr/SKILL.md cross-link, dolibarr/README.md env schema
extended with ZOHO_CLIENT_ID/SECRET/REFRESH_TOKEN/DC.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Three improvements that reduce the V6.1 exit-1 signal from 10 to 1 on
the current Arcodange baseline. Every bucket now has a single, clear
purpose; the only entry counted as a failure is a genuine action item.
A. fk_account context on dolibarr-only
- Fetches /bankaccounts and tags each dolibarr-only with the account
ref + label (e.g. "CCA1 (G.RADUREAU Compte Courant Asso)").
- Splits dolibarr-only into "on API-tracked accounts" (QON*/WIS* — real
gaps) vs "not in API scope" (CCA1 / personal — expected gaps).
- Personal-account entries no longer count toward the failure verdict.
B. Avoir-cycle netting
- Pairs AVC entries of -X on socid S with FAC entries of +X on the
same socid within ±5d.
- Both surface in a dedicated AVOIR-NETTED bucket and are excluded from
dolibarr-only, since the bank only sees the net of the cycle.
- Resolves the V6.1 noise where AVC001-CL0001001 + FAC001-CL00001
appeared as fake gaps for a 510€ cancel-and-reissue dance.
C. Wire-reference strong matching (--enrich flag, opt-in)
- When --enrich is passed, bank-match.sh fetches /v1/transfers/{id}
per Wise TRANSFER and reads the wire `reference` field.
- References containing a FAC\d+(CL\d+)? pattern strong-match against
the corresponding Dolibarr customer invoice (annotated [wire-ref]
vs the loose [amt+date] kind).
- Verified on FAC002 5100€: KM's wire memo "FOR INVOICE FAC002CL0001002"
gives an unambiguous match independent of date drift.
Baseline (Jan-May 2026, --enrich on):
6 matched · 1 internal · 2 avoir-netted · 7 bank-known · 1 bank-UNKNOWN
0 dol-only-API · 7 dol-only-personal
→ exit-1 count = 1 (just the +2147€ KM Wise 2026-05-29 to record).
The CLI (bin/arcodange) gains --enrich on the match subcommand. The
SKILL.md has a new "V7 bucket structure" section explaining the seven
buckets and a before/after table showing the signal/noise improvement.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
V6.1 follow-up to the bank-reco V6 ship. Splits the BANK-ONLY bucket into
"known patterns" (intentional gaps, documented and classified) vs
"unknown" (real action items).
What the catalog covers today:
- FOUREZ Quentin → capital_deposit (apport en capital 1000 € initial,
notaire FOUREZ centralisateur du dépôt). Maps to Dolibarr account 1013.
- URSSAF → social_charges (account 645100)
- MISTRAL.AI, CLAUDE.AI → ai_subscription (account 6262)
- Wise *Plan, qonto_fee → bank_fee (account 627)
- BALANCE_DEPOSIT / FEATURE_CHARGE on Wise → internal_topup (self-funding
pair, often nets to zero)
Effect on the V6 baseline (Jan-May 2026):
- Before catalog: 8 BANK-ONLY mixed entries (noise + signal)
- After catalog: 7 known + 1 UNKNOWN (just the +2147 € KM Wise payment
2026-05-29 that genuinely needs a Dolibarr entry)
The catalog is JSON (not YAML — stdlib only, no dependency). Schema
documented in SKILL.md. Pattern matches case-insensitive regex against
both bank label AND operation type. Optional filters: bank, side,
amount_min, amount_max.
Exit code now reflects only the UNKNOWN bank-only and dolibarr-only
counts — the verdict is no longer noisy because of intentional gaps.
Edit known-patterns.json as new recurring patterns emerge.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
V6 — the first cross-system skill (under arcodange-* not dolibarr-*).
Closes the loop between what Dolibarr says (ERP-internal) and what the
bank actually saw.
What ships:
- arcodange-bank-reco/scripts/bank-curl.sh unified read-only wrapper for Qonto + Wise
- arcodange-bank-reco/scripts/bank-probe.sh auth + discovery (org slug, profile id, balances)
- arcodange-bank-reco/scripts/qonto-transactions Qonto txn lister with pagination + filters
- arcodange-bank-reco/scripts/wise-transactions Wise activity lister with --enrich for wire refs
- arcodange-bank-reco/scripts/bank-match.sh 3-bucket reconciliation (matched/bank-only/dol-only)
with internal Wise↔Qonto consolidation detection
- arcodange-bank-reco/scripts/bank-balance.sh live balances + Dolibarr cumulative-by-fk_account
The headline bank-curl.sh is SCA-aware (Wise RSA dance) even though we
don't end up using it: the EU statement endpoint is region-blocked
("Funding transfers and retrieving balance statements via API are not
supported except for accounts based in the US, Canada, Australia, New
Zealand, Singapore, and Malaysia" per Wise docs). The wrapper supports
SCA so when/if Wise opens it, we're ready.
The pivot that unblocked Wise incoming: /v1/profiles/{pid}/activities
(documented at https://docs.wise.com/api-reference/activity/activitylist.md)
returns ALL movements in a unified HTML-tagged feed, no SCA required.
Parsing strips the HTML and recovers structured amount/sign/currency.
CLI integration:
- bin/arcodange bank {probe,qonto-transactions,wise-transactions,match,balance,curl}
- dolibarr/SKILL.md catalogue + Pointers updated
- dolibarr/README.md env schema extended with QONTO_*, WISE_*
Live baseline findings to raise with the cohort review (captured in
examples/bank-match-2026-01-to-05.txt):
- Wise 2026-05-29 +2147 EUR Kissmetrics NOT YET in Dolibarr
- Qonto bank-only: MISTRAL.AI 172.68, CLAUDE.AI 180, URSSAF 493, FOUREZ +1000
- 6 movements matched cleanly across Jan-May 2026
- Wise→Qonto 5000 EUR consolidation on 2026-03-13 auto-detected as internal
- Live balance: Qonto 4191.54 + Wise 5308.25 = 9499.79 EUR
V7 candidates noted in SKILL.md out-of-scope: reference-based matching
via the Wise --enrich wire refs (FOR INVOICE FAC***), multi-row Dolibarr
sub-payment aggregation, smarter avoir cycle handling.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Two changes that go together: now operators can run every read-only
workflow without going through Claude. The skills (SKILL.md files)
remain the source of behaviour documentation and Claude triggers;
bin/arcodange is the human-facing entry point.
bin/arcodange:
- Bash dispatcher at the project root. Subcommands per domain:
tva {collect, collect-detail, deductible, deductible-detail, summary},
invoice {list, audit}, thirdparty {audit, audit-all},
payments {state, timeline, by-month},
templates {list, inspect},
snapshot, whoami, ping, curl, help.
- Locates the project root via `git rev-parse` so it works from any
CWD (including from a worktree).
- Per-subcommand `help` text. Unknown commands exit 2 with a hint.
- Reuses the existing per-skill scripts under .claude/skills/<name>/
scripts/ via `exec` (zero behaviour drift, full credit to the
existing tested code).
dolibarr-tva-summary:
- Composes dolibarr-tva-reconciliation (TVA collectée customer-side)
and dolibarr-tva-deductible (TVA déductible supplier-side) into a
single CA3-ready monthly summary with per-month net verdict
(TVA à reverser / crédit de TVA / équilibre) and a cumulative line.
- Live baseline: Arcodange en crédit de TVA de 223.22 € cumulé
(0 € collectée 259-1° CGI vs 223.22 € déductible).
- Exposed as `arcodange tva summary [--year|--since|--until]`.
Each existing skill's SKILL.md gets a one-line "CLI shortcut" near
the top so the human path is discoverable from any skill page.
The project root README.md gets a CLI section as the primary
operator entry point.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
V4 bundle — two more sibling skills, both read-only, both depending
on the dolibarr base skill.
dolibarr-thirdparty-completeness:
- audit-thirdparty.sh <socid>: country-aware completeness audit for
any thirdparty (FR: SIREN + SIRET + tva_intra; EU non-FR: tva_intra;
extra-EU: national tax id). Generalizes the V1 KM-hardcoded script.
- audit-all-thirdparties.sh: loops over /thirdparties and surfaces a
compact table of gaps. --clients-only / --suppliers-only flags.
- Live baseline finds 5/10 thirdparties with mandatory gaps:
KissMetrics (US tax id), Wise Europe SA (BE tva_intra), Medialex
(FR SIRET + tva_intra), Qonto (SIRET), Infogreffe (SIRET).
dolibarr-tva-deductible:
- deductible-by-month.sh: TVA déductible aggregated per period × rate.
- deductible-line-detail.sh: per supplier-invoice line with country-
based CA3 bucket assignment (ligne 20 for 20 % FR, ligne 19 for
reduced rates, ligne 17+24 for intra-UE autoliquidation).
- Live baseline: 223.22 € total TVA déductible across 13 lines.
Wise Europe SA correctly identified as intra-UE autoliquidation;
La Poste correctly identified as FR exempt (timbres).
- Mirrors dolibarr-tva-reconciliation on the supplier side. Together
they give the two numbers a CA3 monthly declaration needs.
Also extends dolibarr/SKILL.md endpoint catalogue with /supplierinvoices
(noting the 403 on the /lines sub-endpoint — inline lines on the detail
endpoint make this a non-issue). dolibarr/README.md gains two new
permission checkboxes for Factures fournisseurs.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
V3 bundle — three sibling skills under .claude/skills/, all read-only,
all depending on the dolibarr base skill.
dolibarr-tva-reconciliation:
- tva-by-month.sh: HT + TVA grouped by (year-month × tva_tx), ready
for CA3 / CA12 transcription.
- tva-line-detail.sh: per-line audit trail with country-based bucket
assignment (A1 domestic / A4 intra-UE autoliquidation / E2 export
hors UE). Documents the French TVA mental model.
- Today every Arcodange line is E2 (KissMetrics, US, autoliquidation
259-1° CGI). The skill scales for the day a French B2B is invoiced.
dolibarr-recurring-templates:
- list-templates.sh: probes /invoices/templates/{id} since there's no
list endpoint. Stops after 5 consecutive empty responses.
- inspect-template.sh: full audit per template, with health checks.
- Surfaces that the "Kiss Metrics Invoice" template has frequency=0
and nb_gen_done=0 — it is NOT auto-firing. Every KM invoice today
was manually duplicated. Cohort-review implication: the deferred
9-month cycle depends on Gabriel clicking "Generate" each month,
not on a Dolibarr cron.
dolibarr-data-snapshot:
- snapshot.sh: bundles every read endpoint the dolibarr-* family uses
into one JSON with a content_hash (sha256 of data only, excluding
timestamp — so identical state hashes identically across runs).
- Use cases: cohort evidence packs, drift detection, archival before
a known-risky UI change.
- V1 baseline summary captured at examples/snapshot-summary.txt
(the ~246 KB snapshot file itself is intentionally not committed).
Also extends dolibarr/SKILL.md endpoint catalogue with
/invoices/templates/{id} (and its no-list-endpoint quirk + the
id-null sentinel for missing ids), plus links to the three new
sibling skills.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
V2 in the dolibarr-* family. Three workflows:
- km-payment-state.sh: per-invoice reconciliation (TTC vs sum of
payments) with OK / PARTIAL / UNPAID / OVERPAID classification.
More honest than the `paye` boolean for deferred-cycle agreements.
- km-payment-timeline.sh: all KM payments sorted by date with
cumulative balance — the foundation for cohort-review deferred
9-month-cycle tracking (actual cash receipts vs contractual schedule).
- payments-by-month.sh: monthly aggregation, KM-scoped by default
or --all-clients for accounting basis.
Also updates dolibarr/SKILL.md endpoint catalogue with
/invoices/{id}/payments (note the date-as-string vs unix-epoch quirk)
and /bankaccounts, plus captures the corresponding examples.
V1 baseline of live data: KM is fully reconciled across 5 invoices
(1 avoir + 4 regular), 8160 € total cash receipts spread Feb/Mar/Apr 2026,
all on WISE EURO (BE).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
First two of an expected family of dolibarr-* skills:
- dolibarr/: platform reference — DOLAPIKEY auth, the voir_tous ACL
trap, endpoint catalogue, the dol-curl.sh wrapper, .env credentials
layout (gitignored, mode 600). Every future workflow skill depends
on this one.
- dolibarr-invoice-audit/: first workflow — list KissMetrics invoices,
audit one invoice end-to-end (JSON facts + PDF mandatory-mention
checklist against the French legal corpus), audit the KissMetrics
thirdparty record.
Live captures in examples/ include real audit findings to surface
to the Arcodange × KissMetrics cohort review: PDFs are missing
capital social, L.441-10 penalties, 40 € indemnity, L.123-22 / R.123-237;
KissMetrics thirdparty has no EIN (idprof1..6 all empty);
static/config/company.json holds placeholder values and a wrong
forme juridique (claims SAS, the real Dolibarr is SARL).
.gitignore hardened with *.credentials, secrets/, *.key, and an
explicit .claude/skills/**/.env pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>