Commit Graph
100 Commits
Author SHA1 Message Date
arcodangeandClaude Fable 5 3840e74dcd feat(write-skill): idempotency keys — manifest replay is a no-op (erp#44)
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
2026-07-18 23:46:05 +02:00
arcodange 66b277abfa Merge pull request 'feat(fleet): multi-runtime harness + erp#63 parity/bench evidence' (#69) from arcodange/harness-portability into main 2026-07-18 19:59:01 +02:00
arcodangeandClaude Fable 5 ceb4321224 chore(fleet): erp#63 evidence — verifier parity + builder bench transcripts
- runs/2026-07-18/: the 8 sha256-pinned verifier transcripts (4 runtimes ×
  2 tests), blind-judging verdicts (2 independent judges per cell, unanimous),
  the erp#56 builder-bench journal + prompt + caps, and the evidence README
  with the parity table.
- run-verifier.sh: mistral runtime drops the tool-filter flag (--enabled-tools
  with a no-match pattern hangs vibe 2.21.0); plain -p with --max-turns 1.

Verdicts: Mistral (vibe -p, mistral-medium-3.5) and Ornith 35B (hermes MLX)
reach verdict parity with the Claude baseline on both tests → admitted to
verifier duty. Qwen2.5-7B-4bit fails both → the honest small-model floor.
Builder bench: erp#56 completed by the Mistral runtime, 0 code corrections,
261 s, acceptance run clean (0 bank-UNKNOWN) → merged as PR #68.

Closes #63 (with the paired factory qa-strategy PR).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-07-18 19:56:55 +02:00
arcodange 9b23ee0d52 Merge pull request 'fix(bank-reco): known-patterns.json — Mistral is annual, not monthly' (#68) from arcodange/known-patterns-fix into main 2026-07-18 19:53:58 +02:00
arcodange 6b576931b6 fix(bank-reco): known-patterns.json — Mistral is annual, not monthly
- 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]>
2026-07-18 18:53:07 +02:00
arcodangeandClaude Fable 5 f2a60817e2 feat(fleet): multi-runtime harness — verifier tests + capped builder shell
The harness layer (builder sessions, cold verifiers, evidence flow) gets a
committable home, per the PRD model-fleet § harness portability and erp#63:

- fleet/harness/verifier/: the two canonical verifier tests (locate-test,
  cold-reader backlog audit) with pinned inputs, verbatim prompts, ground
  truth and pass rules — judged context-free, never self-graded.
- fleet/harness/bin/run-verifier.sh: runs a test against any OpenAI-style
  local endpoint (Ornith/MLX) or vibe -p (Mistral); emits sha256-pinned
  JSON transcripts.
- fleet/harness/bin/vibe-builder.sh: the bounded shell for scoped builders
  and recurring tasks — refuses the trunk (linked-worktree guard), hard
  --max-turns/--max-price caps, full JSON journal per run.
- fleet/README.md layout + AGENTS.md Fleet section updated in the same
  change (same-change freshness rule).

Part of erp#63 (harness portability spike, D2).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-07-18 18:52:53 +02:00
arcodange fe6909a2a6 Merge pull request 'feat(write-skill): client-dossier ops — thirdparty update (allowlisted) + idempotent contacts' (#66) from arcodange/client-dossier-ops into main 2026-07-15 19:51:20 +02:00
arcodangeandClaude Fable 5 35b227eb6d feat(write-skill): client-dossier ops — thirdparty update (allowlisted) + idempotent contacts
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]>
2026-07-15 19:49:13 +02:00
arcodange 6b546e77da Merge pull request 'docs(replay-pack): prod replay executed 2026-07-15 — evidence in README' (#64) from arcodange/replay-done into main 2026-07-15 19:16:08 +02:00
arcodangeandClaude Fable 5 532143737b docs(replay-pack): prod replay executed 2026-07-15 — evidence in README
Fresh provenance 36/36 on re-fetched PDFs, prelude green, manifests A+B
applied (FAC004-007, FAF2026010-013, 6 payments), snapshots bracketed,
bank match since January: 0 UNKNOWN.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-15 19:15:33 +02:00
arcodange 320fb22329 Merge pull request 'feat(fleet): scaffold fleet/ — atom registry, class skeletons, AGENTS.md' (#62) from arcodange/fleet-scaffold into main 2026-07-15 18:41:55 +02:00
arcodangeandClaude Fable 5 96c594e0ce feat(fleet): scaffold the atom registry — classes, contract, worked example, AGENTS.md section
Closes erp#38 deliverables: fleet/ layout, atom.yaml schema documented
in fleet/README.md, 7 class skeletons per the PRD agent-catalog,
invoice-extract as the worked example (contract only — implementation
is erp#40), golden/ + profile/ stubs, AGENTS.md Fleet section with
freshness fixes (fleet/ no longer "not yet landed").

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-15 18:38:08 +02:00
arcodange 3ad2d1df74 Merge pull request 'chore(write-skill): preserve the 2026-07-11 books-regularization replay pack' (#61) from arcodange/replay-pack-preserve into main 2026-07-12 14:05:27 +02:00
arcodangeandClaude Fable 5 ccc8b7912f chore(write-skill): preserve the 2026-07-11 books-regularization replay pack
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]>
2026-07-12 14:05:01 +02:00
arcodange bceeb7dc19 Merge pull request 'docs: AGENTS.md — orientation + backlog discovery for agents' (#58) from arcodange/agents-md-backlog into main 2026-07-11 20:21:18 +02:00
arcodangeandClaude Fable 5 539c11b6de docs: AGENTS.md — closure QA gate held by an independent subagent
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-11 20:19:24 +02:00
arcodangeandClaude Fable 5 3c11a926d5 docs: AGENTS.md — doc-freshness rule (trust order + QA-gated closure pointer)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-11 20:17:16 +02:00
arcodangeandClaude Fable 5 dd7b683944 docs: AGENTS.md — orientation + backlog discovery for agents
Agents landing in this repo had no entry point: no AGENTS.md, and the
backlog (erp#38-57 on 6 dated milestones, gateway#1-2, factory#22)
was only discoverable from the PRD STATUS in the factory repo. This
seeds the repo-root orientation map: where the work comes from (resume
protocol: top unblocked issue of the earliest open milestone, gitea
MCP pointers, owner gotcha for telegram-gateway), the repo map, the
operating rules (trunk/worktrees, read-only prod, sandbox+promote
gate, append-only ledger, anti-hallucination contract pointers).

Advances erp#38 (AGENTS.md seed; the fleet/ scaffold and the full
fleet section remain in #38's scope).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-11 20:07:19 +02:00
arcodange 0d66d6a6dc Merge pull request 'fix(txid): normalize bank tx ids to fit Dolibarr's num_payment varchar(50)' (#37) from arcodange/txid-varchar50-normalize into main
Reviewed-on: #37
2026-07-11 18:05:55 +02:00
arcodange 5f5b6f872a Merge pull request 'fix(provision): grant user lire (251) so the checkpoint armed probe passes; probes distinguish 401/403' (#36) from arcodange/provision-user-lire-probe into main
Reviewed-on: #36
2026-07-11 18:04:49 +02:00
arcodangeandClaude Fable 5 f9d83037b6 fix(txid): normalize bank tx ids to fit Dolibarr's num_payment varchar(50)
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]>
2026-07-11 17:48:35 +02:00
arcodangeandClaude Fable 5 0240c519b1 fix(provision): grant user lire (251) to ai_agent_sandbox so the armed probe passes
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]>
2026-07-11 17:38:30 +02:00
arcodange 3045d50626 Merge pull request 'fix(sandbox-poc): regenerate api_key when the existing one is garbage + note platform backup gaps' (#35) from claude/poc-apikey-regen into main 2026-06-30 22:45:06 +02:00
arcodangeandClaude Opus 4.7 7dcd982448 fix(sandbox-poc): regenerate api_key when the existing one is garbage; note platform backup gaps
After an iso-prod refresh the instance unique-id changes, so an api_key encrypted
with the OLD id can't be decrypted — Dolibarr renders non-UTF-8 bytes in the field.
The POC's generateApiKey reused any non-empty value, so it copied that garbage into
test/.ai_agent_sandbox.key (corrupt key, 401s). Now it reuses ONLY a clean key
(^[A-Za-z0-9_-]{24,}$); otherwise it clears the field and regenerates. So
`checkpoint provision` after a refresh yields a fresh, working key.

Also documents the open PLATFORM follow-ups in ops/backup/README.md (easy to find
when revisiting ERP backups): the orphaned Longhorn `default` recurring-job group
(other cluster volumes have no offsite backup), and verifying the factory
pg_dumpall host cron.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 22:44:40 +02:00
arcodange 27e7cef6f2 Merge pull request 'feat(backup): restore subcommand (db + documents), proven on the sandbox' (#34) from claude/dolibarr-backup-restore into main 2026-06-30 18:05:48 +02:00
arcodangeandClaude Opus 4.7 a8b80f17e4 feat(backup): restore subcommand (db + documents), proven on the sandbox
dolibarr-backup.sh restore --db|--docs <ts> --env <e> --yes — the recovery half of
the dedicated backup. DESTRUCTIVE (gated by --yes + explicit --env): scales the app
to 0, then
- --db: DROP OWNED BY <owner_role> CASCADE + pg_restore --no-owner --role (same
  mechanics as sandbox-lifecycle.sh), from s3://.../erp/<env>/db/<ts>.dump;
- --docs: clears /var/www/documents and untars s3://.../erp/<env>/docs/<ts>.tar.gz;
then scales the app back to 1. OWNER_ROLE per env (erp_role / erp_sandbox_role).
The key is the bare <ts> filename from `list`; --db/--docs selects the subpath.

Proven on the sandbox: backup → mutate MAIN_INFO_SOCIETE_NOM to a sentinel →
restore --db → the value reverted to the backup's ('Arcodange'). (First run caught
a path bug — the fetch missed the db/ subdir — now fixed.)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 18:05:19 +02:00
arcodange 590dbe1f26 Merge pull request 'feat(backup): enable the daily backup CronJob on prod (Vault creds wired)' (#33) from claude/dolibarr-backup-enable into main 2026-06-30 17:42:13 +02:00
arcodangeandClaude Opus 4.7 102a205ff8 feat(backup): enable the daily backup CronJob on prod (Vault creds wired)
tools#5 granted the erp prod Vault policy read on kvv2/data/longhorn/gcs-backup
(applied + verified: 1 changed, 0 destroyed). So the CronJob's VaultStaticSecret
can now resolve the GCS creds.

- backup.enabled: true (prod), vaultS3Path: longhorn/gcs-backup.
- sandbox overlay keeps backup.enabled: false (reproducible; its env policy wasn't
  granted the read).

ArgoCD will deploy the CronJob + ConfigMap + VaultStaticSecret in the erp namespace.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 17:41:39 +02:00
arcodange 223dae227e Merge pull request 'feat(backup): skip-if-unchanged + scheduled CronJob in the chart' (#32) from claude/dolibarr-backup-cronjob into main 2026-06-30 15:53:57 +02:00
arcodangeandClaude Opus 4.7 a3f0586c77 feat(backup): skip-if-unchanged + scheduled CronJob in the chart
Builds on the dedicated backup (erp#31).

Skip-if-unchanged: each half (DB / documents) carries a content fingerprint at
erp/<env>/.fp-{db,docs} and is dumped+uploaded only if it differs from the last
run — a quiet ERP day re-uploads nothing. Fingerprint = durable BUSINESS content
only: DB = count+max(tms) over tms tables EXCEPT volatile churn (llx_const,
llx_user, session/cron); docs EXCLUDE */temp/* (Dolibarr stats cache) — from both
the fingerprint and the tar. Proven live: 1st run uploads both, immediate 2nd run
skips both (uploaded=0).

Automation: the in-container logic moves to chart/files/backup-job.sh (single
source of truth, read by the orchestrator AND the chart). New
chart/templates/backup-cronjob.yaml renders a daily CronJob + ConfigMap +
VaultStaticSecret, gated by backup.enabled (default false). Helm-verified: off by
default (0 CronJobs), on renders correctly, env-aware (PREFIX erp/prod vs
erp/sandbox), script embedded.

Activation (documented): store GCS HMAC creds at kvv2/<backup.vaultS3Path>
(default erp/backup), grant the erp `auth` Vault role read on it (tools change),
set backup.enabled=true. Until then the orchestrator runs on demand.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 15:53:13 +02:00
arcodange e69717c2d9 Merge pull request 'feat(ops): dedicated Dolibarr backup (DB + documents → offsite GCS, 10y retention)' (#31) from claude/dolibarr-backup-strategy into main 2026-06-30 15:33:29 +02:00
arcodangeandClaude Opus 4.7 8ec8fde67e feat(ops): dedicated Dolibarr backup (DB + documents → offsite GCS, 10y retention)
The accounting data + issued documents are legally retained 10 years and warrant a
backup dedicated to Dolibarr. An audit found the generic Longhorn external backup
NEVER covered the erp volume (its Longhorn volume sits in the orphaned `default`
recurring-job group; the only job has groups=[] → serves nothing; lastBackupAt=never).
So /var/www/documents (invoice PDFs, supplier pieces, contracts, ECM) had zero
offsite copy — only in-cluster replicas.

ops/backup/dolibarr-backup.sh (orchestrator) + ops/backup/backup-job.sh (in-container
logic, env-driven, single source of truth):
- pg_dump -Fc of the DB + tar of the documents PVC (RWX, read-only mount) ->
  s3://arcodange-backup/erp/<env>/{db,docs}/<ts>, then tiered prune (daily 30d /
  monthly 12m / yearly 10y).
- prod is READ-only (dump+tar read; writes go only to the backup bucket); the DB is
  read with the env's own dynamic creds; the GCS HMAC secret is copied transiently
  (base64, deleted on exit) and never printed; the whole script ships base64.
- fixes the aws-cli v2.23+ default-checksum incompatibility with GCS/S3-compat
  (SignatureDoesNotMatch) via AWS_*_CHECKSUM_*=when_required.

Proven live: sandbox end-to-end (dump+tar+upload+prune, verified in GCS, cleaned up)
and retention logic unit-tested (1100 daily -> 46 kept). The FIRST real prod backup
was taken (erp/prod/db 1.2 MB + erp/prod/docs 12.5 MB) — closing the gap now.

Automation (recurring CronJob in the chart + a dedicated erp Vault policy for its
own S3 creds) is the documented next step; the orchestrator works today on demand.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 15:32:36 +02:00
arcodange d27b5bfd45 Merge pull request 'feat(skills,cli): dolibarr-sandbox-checkpoint — manage the sandbox iso-prod checkpoint' (#30) from claude/sandbox-checkpoint-cli into main 2026-06-30 07:20:21 +02:00
arcodangeandClaude Opus 4.7 275a59b478 feat(skills,cli): dolibarr-sandbox-checkpoint — manage the sandbox iso-prod checkpoint
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]>
2026-06-30 07:19:59 +02:00
arcodange d31e995acd Merge pull request 'fix(ops): sandbox refresh-from-prod actually restores (pg_restore -U + self-heal pause)' (#29) from claude/sandbox-lifecycle-restore-fix into main 2026-06-30 07:00:03 +02:00
arcodangeandClaude Opus 4.7 434be7488d fix(ops): sandbox refresh-from-prod actually restores now (pg_restore -U + self-heal pause)
refresh-from-prod was structurally broken and silently no-op'd the restore:

1. pg_restore lacked -U, so the postgres image connected as its OS user `root`
   and auth-failed. The failure was swallowed by `|| echo "ignorable warnings"`,
   so the script reported success while the DROP OWNED had already emptied the DB.
   E2's original seed was a manual process, so this path had never really run.
   Fix: pass `-h $PGHOST -U $SB_PGUSER`; don't trust pg_restore's exit code (it
   returns non-zero on the harmless "schema public already exists" notice) — verify
   by counting restored llx_* tables and FAIL the Job if < 250.

2. erp-sandbox is ArgoCD-managed with self-heal ON, which reverts the
   `kubectl scale --replicas=0` within seconds — so the seed ran with Dolibarr
   still connected. Fix: pause self-heal for the duration, re-arm it after; app
   restore + self-heal restoration + secret cleanup are guarded by an EXIT trap so
   an interrupt can't strand the sandbox at replicas=0 / self-heal off.

Validated end-to-end on the live sandbox: 295 llx tables, company=Arcodange,
owner=erp_sandbox_role, self-heal re-armed, pod 1/1. README documents the self-heal
pause and the iso-prod consequence (ai_agent_sandbox is wiped → re-provision).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 06:59:39 +02:00
arcodange 0688e3d7fd Merge pull request 'feat(bank-match): exact tx-id match pass (consumes payment transaction_id)' (#28) from claude/bank-match-exact-txid into main 2026-06-30 00:39:44 +02:00
arcodangeandClaude Opus 4.7 4b17e5f22c feat(bank-match): exact tx-id match pass (consumes payment transaction_id)
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]>
2026-06-30 00:39:21 +02:00
arcodange 01e3eaac5e Merge pull request 'feat(payment): first-class transaction_id when recording a règlement' (#27) from claude/dolibarr-payment-txn-integrate into main 2026-06-30 00:34:32 +02:00
arcodangeandClaude Opus 4.7 b945c8de47 feat(payment): first-class transaction_id when recording a règlement
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]>
2026-06-30 00:34:10 +02:00
arcodange d81bff0ed3 Merge pull request 'feat(payment): return the bank transaction id on règlements (reconciliation link)' (#26) from claude/dolibarr-payment-txn into main 2026-06-30 00:17:59 +02:00
arcodangeandClaude Opus 4.7 e748efd8f0 feat(payment): return the bank transaction id on règlements (reconciliation link)
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]>
2026-06-30 00:17:41 +02:00
arcodange 463e10a417 Merge pull request 'feat(skills,cli): supplier avoirs + banque lire (bank-account discovery)' (#25) from claude/dolibarr-last-bits into main 2026-06-30 00:08:07 +02:00
arcodangeandClaude Opus 4.7 64d2cb4237 feat(skills,cli): supplier avoirs + banque lire (bank-account discovery)
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]>
2026-06-30 00:07:48 +02:00
arcodange 09a2cbab0a Merge pull request 'feat(promote): resolve pre-existing entities by business key (#entity:field=value)' (#24) from claude/dolibarr-promote-lookup into main 2026-06-29 23:58:41 +02:00
arcodangeandClaude Opus 4.7 04985fe15c feat(promote): resolve pre-existing entities by business key (#entity:field=value)
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]>
2026-06-29 23:57:49 +02:00
arcodange 7949ab34f8 Merge pull request 'feat(skills,cli): promote-to-prod replay (ADR-0003 capstone) + supplier payment fix' (#23) from claude/dolibarr-promote into main 2026-06-29 23:49:22 +02:00
arcodangeandClaude Opus 4.7 00d86b47a3 feat(skills,cli): promote-to-prod replay (ADR-0003 capstone) + supplier payment fix
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]>
2026-06-29 23:48:47 +02:00
arcodange e4c67c0108 Merge pull request 'feat(skills,cli): sandbox avoir (credit note) + arcodange sandbox CLI group' (#22) from claude/dolibarr-sandbox-write-cli into main 2026-06-29 21:05:25 +02:00
arcodangeandClaude Opus 4.7 79286650d7 feat(skills,cli): sandbox avoir (credit note) + arcodange sandbox CLI group
- 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]>
2026-06-29 21:04:49 +02:00
arcodange 52f4d02722 Merge pull request 'feat(skills): dolibarr-sandbox-write — host-guarded write skill (V9)' (#21) from claude/dolibarr-sandbox-write into main 2026-06-29 20:50:11 +02:00
arcodangeandClaude Opus 4.7 d2e8b3a3a4 feat(skills): dolibarr-sandbox-write — host-guarded write skill (V9)
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]>
2026-06-29 20:49:31 +02:00
arcodange c269751422 Merge pull request 'fix(test): grant societe client voir (262) so /thirdparties list works' (#20) from claude/sandbox-provision-run into main 2026-06-29 20:17:20 +02:00
arcodangeandClaude Opus 4.7 7619a4f358 fix(test): grant societe client voir (262) so /thirdparties list works
Validating ai_agent_sandbox's key against the sandbox API, /thirdparties
returned 404 (the voir_tous ACL trap) while /invoices, /products,
/supplierinvoices returned 200. The missing right is `societe client voir`
(id 262, "see all thirdparties") — prod's ai_agent has it. Added it to
WRITE_IDS so the list endpoint works; other modules' lists are fine with plain
`lire`.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 20:16:59 +02:00
arcodange 37865c55c5 Merge pull request 'fix(test): persist API key + anchor rights selector + idempotent createUser' (#19) from claude/sandbox-provision-run into main 2026-06-29 14:31:24 +02:00
arcodangeandClaude Opus 4.7 18c5d0ebda fix(test): persist generated API key + anchor rights selector + idempotent createUser
First real run against the sandbox revealed three issues in userSetup.ts:

1. generateApiKey generated the key client-side and read it into the file but
   never submitted the edit form, so Dolibarr never persisted api_key (DB stayed
   NULL → the key could not authenticate). Now it clicks Save after generating.

2. assignRights matched `rights=<id>` as an href substring, so a short id like
   12 (facture creer) also matched rights=121 / rights=1232 and .first() clicked
   the wrong link — facture creer was never granted. Anchored with a trailing
   "&" (rights=<id>&) for an exact match.

3. createUser was not idempotent: a re-run hit the existing login and failed to
   parse a new id. Added findUserId (look up by login via the user list) and
   return the existing id instead of creating a duplicate.

Verified the symptoms live: ai_agent_sandbox (rowid 4) had api_key NULL and was
missing only facture/creer among the 11 intended rights.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 14:30:36 +02:00
arcodange 650594bb70 Merge pull request 'fix(sandbox): Dolibarr dev mode (DOLI_PROD=0) + correct install.lock path' (#18) from claude/sandbox-dev-mode into main 2026-06-29 14:11:18 +02:00
arcodangeandClaude Opus 4.7 416cd807a3 fix(sandbox): run Dolibarr in dev mode (DOLI_PROD=0) + correct install.lock path
After seeding erp-sandbox from prod, the home dashboard rendered a generic
"technical error" banner per box: prod mode ($dolibarr_main_prod=1, the image
default via DOLI_PROD) escalates the seed's minor non-fatal warnings into that
banner. Setting DOLI_PROD=0 for non-prod environments makes Dolibarr render
real errors inline (correct for a rehearsal env) and clears the banners.

config.yaml adds `DOLI_PROD: "0"` only when env != prod, so the prod configmap
is byte-identical (prod keeps the image default DOLI_PROD=1) — verified via
helm template diff. ArgoCD rolls only the sandbox pod.

Also corrects the test/README install.lock path: Dolibarr checks the DATA root
(/var/www/documents, a PVC — persists across restarts), not /var/www/html. And
notes that a prod-seeded sandbox still needs install.lock created (the seed +
documents/mycompany sync don't include it).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 14:09:59 +02:00
arcodange 2154bf319e Merge pull request 'feat(test): split env config — .env (prod) vs .env.sandbox (sandbox)' (#17) from claude/test-env-sandbox-split into main 2026-06-29 11:26:39 +02:00
arcodangeandClaude Opus 4.7 e4a7f99333 feat(test): split env config — .env (prod) vs .env.sandbox (sandbox)
provisionSandbox.ts now loads its own .env.sandbox (via @std/dotenv loadSync)
instead of the shared .env, so prod (main.ts → .env) and sandbox
(provisionSandbox.ts → .env.sandbox) configs don't collide. .gitignore widened
to .env* (keeping .env.example tracked). .env.example rewritten to document the
two-file convention + the per-env kubectl secret sources, including the caveat
that a prod-seeded sandbox uses PROD's admin password.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 11:26:05 +02:00
arcodange 04281f0ab7 Merge pull request 'docs(test): preserve the install.lock step in test/README' (#16) from claude/test-readme-install-lock into main 2026-06-29 07:54:22 +02:00
arcodangeandClaude Opus 4.7 c010099dae docs(test): preserve the install.lock step in test/README
The pre-existing (untracked) test/README documented creating Dolibarr's
install.lock after a fresh install — a non-obvious operational step missing from
the rewritten README. Preserve it (generalized to the per-env namespace/label,
with a note that a prod-seeded instance doesn't need it).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 07:54:01 +02:00
arcodange 4cc5ca39ce Merge pull request 'feat(ops): erp-sandbox iso-prod seed + documents sync tooling (ADR-0003 E2)' (#15) from claude/e2-sandbox-lifecycle into main 2026-06-29 07:43:44 +02:00
arcodange fbc0cc6962 Merge pull request 'feat(test): provision erp-sandbox via Playwright (REST API + write-scoped ai_agent_sandbox user)' (#14) from claude/poc-provision-sandbox into main 2026-06-29 07:43:32 +02:00
arcodangeandClaude Opus 4.7 7264f00ed4 feat(ops): erp-sandbox iso-prod seed + documents sync tooling (ADR-0003 E2)
Productionizes the sandbox state-lifecycle mechanisms validated live against
erp-sandbox. `ops/sandbox/sandbox-lifecycle.sh`:
  - refresh-from-prod: read-only pg_dump of prod erp (default_transaction_read_only)
    -> DROP OWNED BY erp_sandbox_role CASCADE -> pg_restore into erp-sandbox, using
    the sandbox's own membership creds (no DROP/CREATE DATABASE, no CREATEDB, no
    superuser). Dumps the full public schema (so app helper functions + triggers
    come over) and filters the provisioner-owned pgbouncer user_lookup function
    from the restore TOC. Scales the pod to 0 for exclusive access; copies prod
    creds into a transient secret that is deleted on exit.
  - sync-documents: tar-pipe the documents/mycompany tree (company logo + uploads)
    prod -> sandbox, since uploaded files live on the PVC, not the DB.

Prod integrity is structural: prod is read-only during dump; the restore can only
write erp-sandbox (erp_sandbox_role owns only the sandbox DB and cannot drop prod
erp/erp_role); the platform's only prod-capable superuser stays behind the
human-gated postgres.yaml CI and is never used here.

README documents the integrity guarantee, the encryption + PVC fidelity caveats,
the BDD reset loop, and the hardening backlog (dedicated read-only dump role,
golden-cache PVC).

Refs ADR-0003 (factory#19). Chart owner-role fix = erp#13.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 07:42:00 +02:00
arcodangeandClaude Opus 4.7 523f0cf001 feat(test): provision erp-sandbox via Playwright (REST API + write-scoped ai_agent_sandbox user)
Extend the Deno + Playwright UI-automation POC to provision the erp-sandbox
Dolibarr for the AI agent:

- moduleSetup.ts: add enableApiModule(ctx) — toggles the REST API / Web services
  module on /admin/modules.php (kanban). Resilient: tries the fr_FR card label
  "API/Web services REST (serveur)" first, falls back to a /API.*REST|REST.*API/i
  title match if the exact label is absent.
- userSetup.ts (new): createUser (returns the new numeric id), assignRights
  (clicks each addrights link on /user/perms.php, idempotent), generateApiKey
  (triggers Dolibarr's generate control on the user card and reads the value back).
- provisionSandbox.ts (new entrypoint, main.ts untouched): login → enable API →
  create ai_agent_sandbox (non-admin) → grant write rights → generate API key,
  then write the key to test/.ai_agent_sandbox.key (gitignored) instead of
  printing it.
- .gitignore (new), .env.example + README.md: sandbox vars, the
  deno run --allow-all provisionSandbox.ts command, and kubectl one-liners to
  pull DOLI_ADMIN_PASSWORD (secretkv) / DOLI_DB_PASSWORD (vso-db-credentials)
  from the erp-sandbox namespace.

Why UI not SQL: API keys are encrypted with the instance's DOLI_INSTANCE_UNIQUE_ID,
so the key must be generated by the sandbox itself, not INSERTed raw.

deno check passes for provisionSandbox.ts and scripts/admin/userSetup.ts.
NOT run end-to-end: the sandbox Dolibarr is not installed yet (empty DB / fresh
install wizard), so the selectors are best-effort Dolibarr 22 conventions and
must be confirmed on the first real run.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 07:34:21 +02:00
arcodange cb17332314 Merge pull request 'fix(chart): template Postgres owner role in update_ownership.sql for multi-env' (#13) from claude/fix-chart-owner-role into main 2026-06-28 22:30:54 +02:00
arcodangeandClaude Opus 4.7 c0d5f2e144 fix(chart): template Postgres owner role in update_ownership.sql for multi-env
The Dolibarr before-start step `chart/scripts/update_ownership.sql` (embedded
into a ConfigMap by `chart/templates/scripts-config.yaml`) hardcoded the
Postgres owner role `erp_role`. It reassigns ownership of all public-schema
objects to that role after install. For any non-prod environment the owner
role differs — by the multi-env elision rule (ADR-0002/0003) it is snake-case
`<app>_role` for prod and `<app>_<env>_role` for non-prod, so the sandbox owner
role is `erp_sandbox_role`. With the literal `erp_role`, installing Dolibarr in
`erp-sandbox` would reassign sandbox tables to prod's `erp_role`, which (a)
breaks the sandbox runtime (its dynamic DB creds are a member of
`erp_sandbox_role`, not `erp_role`) and (b) breaks the ADR-0003 reset
(`DROP OWNED BY erp_sandbox_role`).

Fix: make the owner role env-aware via a new chart value `db.ownerRole`.
- values.yaml: default `ownerRole: erp_role` (prod).
- values-sandbox.yaml: override `ownerRole: erp_sandbox_role`.
- update_ownership.sql: all `'erp_role'` literals → `'{{ .Values.db.ownerRole }}'`.
- scripts-config.yaml: render that one SQL file through `tpl` so the value is
  substituted (the other script has no template vars and stays on `.Files.Get`).
  The SQL's `$$`, `%I`, `format(...)`, `RAISE NOTICE` are not Go-template syntax,
  so `tpl` only substitutes the added `{{ .Values.db.ownerRole }}`.

Verified: the prod ConfigMap render (values.yaml only) is byte-identical to
origin/main (empty diff, still `erp_role`); the sandbox render
(-f values.yaml -f values-sandbox.yaml) now contains `erp_sandbox_role` and no
bare `erp_role`; `helm lint` passes (no worse than origin/main).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-28 22:29:18 +02:00
arcodange 354b40549d Merge pull request 'feat(multi-env): Phase D3 — erp iac creates erp-sandbox Vault auth + creds + KV' (#12) from claude/phaseD-erp-sandbox-iac into main 2026-06-28 17:30:57 +02:00
arcodangeandClaude Opus 4.7 e2fa327361 feat(multi-env): Phase D3 — erp iac creates erp-sandbox Vault auth + creds + KV
ADR-0002 Phase D, erp-repo layer. iac/main.tf iterates `envs = ["prod",
"sandbox"]` so the app_roles module + the admin-bootstrap resources are
materialised per environment:
  - module.app_roles["sandbox"] → auth/kubernetes/role/erp-sandbox +
    postgres/creds/erp-sandbox (dynamic role GRANTs erp_sandbox_role — the
    snake-case owner role created in factory#17 — and REVOKEs on DATABASE
    erp-sandbox; token_policies ["default","erp-sandbox"] = the policy from
    tools#3).
  - random_password.admin_initial_password["sandbox"], random_uuid.dolibarr_id
    ["sandbox"], and vault_kv_secret_v2.dolibarr_admin_setup["sandbox"]
    (kvv2/erp-sandbox/config) → the sandbox Dolibarr's own admin password +
    encryption id.

State migration via `moved` blocks: the pre-existing single-env resources are
re-keyed into the for_each map under "prod" so introducing for_each does NOT
destroy+recreate them. Critical for random_uuid.dolibarr_id (prevent_destroy =
true — prod encryption id + paid-module binding): a wrong/absent moved would
HARD-FAIL the apply rather than lose it. The module's internal moved
(role -> role[0]) chains with the module re-key. Verified the exact compound
(old-module state → new module count+moved + for_each, one apply) with two
standalone tofu plans: both show "X moved", sandbox created, 0 destroyed.

env=prod renders byte-identical to the single-env baseline (module elision
rule), so the prod erp Vault auth role, dynamic creds, admin secret + KV are
unchanged.

D3 of Phase D. D1 = factory#17 (DB+role, merged). D2 = tools#3 (Vault
policies, merged). D4 (ArgoCD Application) is next.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-28 17:26:46 +02:00
arcodange b4bdbe75df Merge pull request 'chart: Phase C of multi-env evolution — template literals, add sandbox overlay' (#11) from claude/chart-multi-env-prep into main 2026-05-31 23:27:05 +02:00
arcodangeandClaude Opus 4.7 ec4df4719f chart: template hardcoded single-env literals; add values-sandbox.yaml overlay
Phase C of the multi-env evolution discussed in the runbook design thread
(see PR description). Pure refactor — the prod helm template render is
verified byte-identical (10857 bytes both before and after, diff exit 0).

What was hardcoded, now templated:
- chart/templates/vaultauth.yaml          role: erp                       → role: {{ .Values.vault.k8sRole }}
- chart/templates/vaultdynamicsecret.yaml path: creds/erp                 → path: {{ .Values.vault.dynamicPath }}
- chart/templates/vaultsecret.yaml        path: erp/config                → path: {{ .Values.vault.staticPath }}
- chart/templates/config.yaml             DOLI_DB_NAME: erp               → DOLI_DB_NAME: {{ .Values.db.name }}
                                          DOLI_URL_ROOT: https://erp..lab → DOLI_URL_ROOT: 'https://{{ .Values.host }}'

values.yaml gains a documented multi-env coordinate block with prod defaults
(env, instance, host, db.name, vault.k8sRole, vault.dynamicPath, vault.staticPath).
The elision rule (env=prod → no suffix, env=non-prod → "<app>-<env>" suffix)
guarantees the prod render is unchanged.

chart/values-sandbox.yaml is added as the ready-to-use overlay for Phase D.
It is NOT wired into any helm install / ArgoCD app today — the platform side
(factory/postgres/iac tfvars, tools/hashicorp-vault/iac module signature) is
not yet evolved. The file documents the convention so the Phase D commit can
just `helm install -f values.yaml -f values-sandbox.yaml`.

Also fixes .gitea/workflows/vault.yaml CI typo: the vault_step JWT role was
gitea_cicd_webapp (copy-paste from the template repo) instead of
gitea_cicd_erp. Real bug — the erp CI would have failed JWT auth against
Vault. Fix unrelated to multi-env but bundled here because it's small and
touches the same file family.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-31 23:26:20 +02:00
arcodange 444886b91a Merge pull request 'arcodange-email-ingest V8.1: filter calendar invites + newsletter senders' (#10) from claude/arcodange-email-ingest-v81 into main 2026-05-31 15:18:58 +02:00
arcodangeandClaude Opus 4.7 1d38f25c23 arcodange-email-ingest V8.1: filter calendar invites + newsletter senders
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]>
2026-05-31 15:18:31 +02:00
arcodange 794aa18d2a Merge pull request 'add arcodange-email-ingest — Zoho Mail → Dolibarr supplier-invoice drafts' (#9) from claude/arcodange-email-ingest into main 2026-05-31 14:56:58 +02:00
arcodangeandClaude Opus 4.7 c2d8479f5e add arcodange-email-ingest — Zoho Mail → Dolibarr supplier-invoice drafts
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]>
2026-05-31 14:56:15 +02:00
arcodange a1042a483b Merge pull request 'arcodange-bank-reco V7: avoir netting + fk_account context + wire-ref matching' (#8) from claude/arcodange-bank-reco-v7 into main 2026-05-31 14:20:37 +02:00
arcodangeandClaude Opus 4.7 246c7fc5a9 arcodange-bank-reco V7: avoir netting + fk_account context + wire-ref matching
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]>
2026-05-31 14:20:06 +02:00
arcodange 0f5b6bcbad Merge pull request 'arcodange-bank-reco: known-patterns catalog + annotated bank-only buckets' (#7) from claude/arcodange-bank-reco-patterns into main 2026-05-31 14:07:39 +02:00
arcodangeandClaude Opus 4.7 4b6a5f7529 arcodange-bank-reco: add known-patterns.json catalog + bank-match annotation
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]>
2026-05-31 14:06:46 +02:00
arcodange f398003eae Merge pull request 'add arcodange-bank-reco — Qonto + Wise reconciliation against Dolibarr' (#6) from claude/arcodange-bank-reco into main 2026-05-31 13:58:06 +02:00
arcodangeandClaude Opus 4.7 bd90266372 add arcodange-bank-reco — Qonto + Wise reconciliation against Dolibarr
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]>
2026-05-31 13:57:21 +02:00
arcodange 4252a88681 Merge pull request 'add bin/arcodange CLI and dolibarr-tva-summary skill' (#5) from claude/arcodange-cli-tva-summary into main 2026-05-29 11:30:55 +02:00
arcodangeandClaude Opus 4.7 1c0ba8ea75 add bin/arcodange CLI and dolibarr-tva-summary skill
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]>
2026-05-29 11:30:18 +02:00
arcodange 8ba10e6f65 Merge pull request 'add 2 dolibarr-* skills: thirdparty-completeness, tva-deductible' (#4) from claude/dolibarr-thirdparty-tva-deductible into main 2026-05-29 06:15:04 +02:00
arcodangeandClaude Opus 4.7 585b7beb03 add dolibarr-thirdparty-completeness and dolibarr-tva-deductible
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]>
2026-05-29 06:14:28 +02:00
arcodange fe9e8274f1 Merge pull request 'add 3 dolibarr-* skills: tva-reconciliation, recurring-templates, data-snapshot' (#3) from claude/dolibarr-tva-recurring-snapshot into main 2026-05-29 00:10:52 +02:00
arcodangeandClaude Opus 4.7 f19b1d2ef2 add dolibarr-tva-reconciliation, dolibarr-recurring-templates, dolibarr-data-snapshot
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]>
2026-05-29 00:01:06 +02:00
arcodange d34cba3fa0 Merge pull request 'add dolibarr-payments-state skill for cash receipt tracking' (#2) from claude/dolibarr-payments-state into main 2026-05-28 18:53:22 +02:00
arcodangeandClaude Opus 4.7 e7abfd5e22 add dolibarr-payments-state skill for cash receipt tracking
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]>
2026-05-28 18:52:48 +02:00
arcodange 00ddf41f5c Merge pull request 'add dolibarr api skills for read-only inspection' (#1) from claude/dolibarr-skill into main 2026-05-28 18:47:28 +02:00
arcodangeandClaude Opus 4.7 bbfa50c3eb add dolibarr api skills for read-only inspection
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]>
2026-05-28 18:43:39 +02:00
arcodange e90ac2df80 upgrade from 20.0.1 to 22.0.4 2026-03-14 17:33:15 +01:00
arcodange afba67c68c .duckdns.org to internal dns .lab 2026-01-03 18:50:39 +01:00
arcodange 5d41173f8d log fix for table permissions 2025-09-02 16:10:42 +02:00
arcodange 42412d5814 nfs-client storageClass is no longer available 2025-08-27 20:43:49 +02:00
arcodange f4d450c75a work in progress with deno 2025-08-08 17:57:56 +02:00
arcodange 8bdc03a221 gitea workflow triggerable from ui 2025-08-08 16:42:24 +02:00
arcodange 9d4d33ef45 begin setup automation with deno and playwright 2024-11-15 16:08:41 +01:00
arcodange 5c08cc9cfd deploy dolibarr 2024-11-15 16:02:54 +01:00