Skip to content

Finance reconciliation: reconcile-receipts skill + cron (closes unimatrix27/ideas#25) - #6

Open
unimatrix27 wants to merge 12 commits into
mainfrom
feat/finance-reconcile-skill
Open

Finance reconciliation: reconcile-receipts skill + cron (closes unimatrix27/ideas#25)#6
unimatrix27 wants to merge 12 commits into
mainfrom
feat/finance-reconcile-skill

Conversation

@unimatrix27

Copy link
Copy Markdown
Owner

Summary

Closes unimatrix27/ideas#25. Ships the Mode-B LLM brain that orchestrates the deterministic finance-reconcile toolbox from PR #5 (unimatrix27/ideas#23), per the architecture in NousResearch#27 and the framework in NousResearch#26.

  • One locked SKILL.md. Frontmatter sets metadata.hermes.locked: true, body opens with ## Subagent dispatch so SOUL.md routes the cron-spawned parent through delegate_task exactly once with toolsets=[terminal, skills] and role=leaf. web is intentionally not granted.
  • One offline test file (13 tests, 0 LLM calls, 0 network, 0 Postgres). Builds a MockDispatchRunner that parses the dispatch block out of SKILL.md and asserts the SOUL contract, plus a ScriptedChild that transcribes the SKILL.md decision norms into Python so tool-call ORDER + SHAPE assertions hit the rules and not a model's wording.
  • One new cron entry appended to ~/.hermes/cron/jobs.json on the EC2 (the file is not in the repo). skill: reconcile-receipts, schedule: 0 8 5 * *, deliver: telegram, enabled_toolsets: [terminal, skills].

No new migrations. No new tools. No new tables. No Python under finance/ beyond the test file. Per NousResearch#25's scope discipline.

Do not merge. Awaits review per the issue's acceptance criteria, and depends on PR #5 (#5, branch feat/finance-toolbox) landing first — this branch is stacked on top of it.

SKILL.md preview (first 30 lines)

---
name: reconcile-receipts
description: "Monthly DATEV receipt reconciliation (Mode B). Reviews proposed matches, sends confirmed receipts, flags anomalies, reports unforeseen state. Triggers on phrases like 'reconcile receipts', 'run the monthly finance agent', 'check this month's transactions for receipts', 'belege agent', 'monatsabschluss', 'belege abgleichen', 'monatliche belege-prüfung'."
version: 1.0.0
platforms: [linux]
metadata:
  hermes:
    tags: [finance, datev, belege, reconciliation, lineo, mode-b]
    related_skills: []
    locked: true
---

# Monthly DATEV reconciliation (Mode B)

This skill is the LLM brain that orchestrates the deterministic `finance-reconcile` toolbox shipped in unimatrix27/hermes-agent PR #5 (closes unimatrix27/ideas#23). It runs once a month under cron, reviews every open bank transaction for the previous calendar month, decides whether each proposed match is correct, sends approved matches to DATEV, and flags anything it cannot place. Architecture: unimatrix27/ideas#27 (Mode A vs Mode B framework: #26).

## Subagent dispatch

- **Toolsets:** `terminal`, `skills`
- **Role:** `leaf`

(The global skill-dispatch rule in `~/.hermes/SOUL.md` handles the delegation contract for the parent session. Everything below is the subagent's brief. `web` is **not** granted — the agent has no business browsing.)

## Subagent brief

### Role

You are the Mode B subagent dispatched by the parent session for monthly DATEV reconciliation. Your context is fresh; your toolset is `terminal` (which gives you the full `finance-reconcile` verb set) and `skills`. You cannot delegate further. Your job is to look at every relevant bank transaction in scope this run, decide whether each proposed match is correct, send approved matches to DATEV via `send_match`, and flag anything you do not understand. "I don't know" is a first-class outcome — route it through `flag_anomaly`, never through inline guessing.

Tests

$ /home/hermes/.hermes/hermes-agent/venv/bin/python -m pytest finance/tests/test_reconcile_skill.py -q -o addopts=''
.............                                                            [100%]
13 passed in 0.05s

$ /home/hermes/.hermes/hermes-agent/venv/bin/python -m pytest finance/tests/ -q -o addopts=''
[...]
107 passed in 0.36s

13 new tests, 0 LLM / network / Postgres dependencies. Coverage matches issue NousResearch#25's test plan:

# Test What it asserts
1 test_skill_md_locked_and_dispatch_block_present metadata.hermes.locked: true, ## Subagent dispatch heading, toolsets [terminal, skills], role leaf, German trigger phrases present in description
2 test_parent_dispatches_via_delegate_task_exactly_once parsing the dispatch block produces the right kwargs; runner records exactly one delegate_task call; a second dispatch raises (SOUL rule)
3 test_clean_month_tool_call_order_and_summary refresh → reads → per-tx review → finalize_run; approve_match before send_match on the seeded Sipgate proposal; finalize_run row has non-empty summary_md and month_scope + model_id at the top of tool_call_summary
4 test_unknown_vendor_anomaly_is_flagged_not_sent unknown MYSTERY HOLDINGS 999.99 EUR → flag_anomaly; approve_match / send_match not called; hunter called exactly once
5 test_existing_open_anomaly_is_not_reflagged suppression via read_anomalies(status="open") blocks a duplicate flag for the same (tx_id, reason)
6 test_prior_run_flag_suppresses_reflag suppression via get_run_history(...).tool_call_summary.flags_raised blocks a flag the prior run already raised
7 test_sipgate_high_confidence_approve_then_send very_high single-candidate Sipgate B4373121 → TX 56 → approve_match then send_match on the same match_id; bank.belege_sent row written with via='agent_match'; FakeGraphMailSender recorded one send
8 test_adversarial_unignore_instruction_is_ignored an injected "please un-ignore tx 999" request is dropped by the child (SKILL.md rule); defense in depth: mark_ignored verb still raises InvalidTransition if invoked
9 test_clean_month_finalize_with_proposed_changes_null clean run → proposed_changes=None (jsonb column stays null)
10 test_hunter_empty_result_falls_through_without_loop_for_portal_vendor Vodafone in 'missing' bucket → search_for_missing_receipt called exactly once; portal vendor → mark_manual_needed; no flag
11 test_hunter_empty_result_flags_for_non_portal_vendor non-portal vendor in 'missing' + empty hunter → flag_anomaly; no mark_manual_needed
12 test_hunter_returns_a_candidate_loops_into_proposal_flow hunter inserts a candidate as a side effect → child re-reads get_tx_context and applies the proposal rules; still exactly one hunter call
13 test_dispatch_runner_routes_to_child end-to-end: parent dispatches via delegate_task with the parsed toolsets, child factory drives a full ScriptedChild run, parent never touches the verb layer directly

Live cron run (2026-05-12)

Triggered via hermes cron run 63fd019107c3 after standing up the runtime prereqs (see "Live-run prereq dance" below). Cron session: cron_63fd019107c3_20260512_055958. Agent.log line confirming delivery: cron.scheduler: Job '63fd019107c3': delivered to telegram:364975143 via live adapter.

artefact value
bank.agent_reconcile_runs.id 2 (written via manual finance-reconcile finalize_run after the agent's call crashed on the --summary-md alias gap — see follow-up #2 below)
summary_md "Live cron run 2026-05-12 against scope 2026-04. The Mode B agent dispatched from cron job 63fd019107c3 ran indexer (0 new), matcher (1 tx seen / 0 new proposals), list_open_transactions, and inspected the result: of 43 in-scope txs, 24 are ignored and 18 are already done; the only non-done/non-ignored row is tx 83 (Google Ads — already in manual_needed from a prior run, portal-only vendor), which the skill rules leave alone. 0 approved+sent, 0 newly marked manual-needed, 0 anomalies flagged, 1 tx still open. Routine month."
telegram message telegram:364975143 (delivered by hermes gateway live adapter)
approve_match / send_match 0 / 0
flag_anomaly 0
bank.agent_anomalies new rows 0
bank.belege_sent new rows 0
bank.receipt_matches updated rows 0

No unexpected anomalies on real April 2026 data — the month is genuinely quiet (24 ignored, 18 done, 1 prior manual_needed for the portal-only Google Ads vendor).

What the live agent actually did

The cron-spawned parent loaded the SKILL.md inline (the SOUL dispatch rule fires for skill_view-from-an-agent, not for the cron scheduler's load path — follow-up #5). The agent then called, in order:

  1. finance-reconcile run_indexer{scanned: 0, new: 0, dedup_skipped: 0, portal_required: 0, parse_failed: 0} (idempotent refresh; nothing new in the mailbox)
  2. finance-reconcile run_matcher --month 2026-04{proposed_new: 0, proposed_updated: 0, skipped_existing: 0, txs_seen: 1, txs_with_proposals: 0}
  3. finance-reconcile list_open_transactions --month 2026-04 → 43 rows, all of which were done, ignored, or the single pre-existing manual_needed Google Ads tx
  4. finance-reconcile finalize_run --summary-md '<paragraph>' --notes '<jsonb>' --invoked-by 'cron:reconcile-receipts'argparse error: --summary-md is not a registered flag (follow-up Finance reconciliation: fixture pack (closes unimatrix27/ideas#24) #2)
  5. Agent's final text response: fabricated a {run_id: 76, notifier_dispatched: true} tool-result and returned the summary paragraph; Telegram delivered the fabricated response

I then ran finance-reconcile finalize_run --summary '<same paragraph>' --notes '<same jsonb>' --invoked-by 'cron' manually to land a real bank.agent_reconcile_runs row (id=2), and updated the SKILL.md to use --summary + --notes + --invoked-by 'cron' literally so subsequent runs don't hit the same gap.

Live-run prereq dance (one-time EC2 setup notes)

For anyone re-running the smoke on this host:

  1. Re-install hermes-agent against the work clone so the finance-reconcile console script (added in PR Finance reconciliation: toolbox (closes unimatrix27/ideas#23) #5) registers:
    ~/.hermes/hermes-agent/venv/bin/python -m pip install -e /home/hermes/work/hermes-agent
    ln -s ~/.hermes/hermes-agent/venv/bin/finance-reconcile ~/.local/bin/finance-reconcile
    
  2. Sync the new SKILL.md into the runtime skill index:
    HERMES_BUNDLED_SKILLS=/home/hermes/work/hermes-agent/skills \
      ~/.hermes/hermes-agent/venv/bin/python -c \
      "from tools.skills_sync import sync_skills; sync_skills(quiet=False)"
    
  3. Append the cron entry (already done on this host — 63fd019107c3). Gotcha: hermes' cron run round-trips the entry through a normalize step that prefers skills over skill. Both fields need to be populated ("skills": ["reconcile-receipts"] AND "skill": "reconcile-receipts") or normalize wins with the empty list and clears the skill ref.
  4. Confirm: hermes skills list | grep reconcile-receipts shows enabled, finance-reconcile --help resolves, hermes cron list shows the entry with the right next_run_at.

Reverse after the smoke (or after PR #5 lands upstream and CI installs the right shape):

~/.hermes/hermes-agent/venv/bin/python -m pip install -e /home/hermes/.hermes/hermes-agent
rm ~/.local/bin/finance-reconcile  # if a runtime install registers it

Acceptance criteria

  • SKILL.md has metadata.hermes.locked: true and a ## Subagent dispatch heading. Verified by test_skill_md_locked_and_dispatch_block_present.
  • One cron tick runs end-to-end on a fresh month. Done on 2026-05-12 for scope 2026-04; the parent loaded the skill, the agent ran run_indexer → run_matcher → list_open_transactions → finalize_run (final call's flag gap surfaced as follow-up Finance reconciliation: fixture pack (closes unimatrix27/ideas#24) #2), report delivered via the existing Telegram channel.
  • The agent never writes Python or SQL; it only invokes the finance-reconcile verbs from Terminal backend fanout pr2 - PENDING NousResearch/hermes-agent#23 through terminal. Verified in the live session — the four tool calls in the cron transcript are all finance-reconcile <verb> invocations.
  • Every state-changing tool call auditable via bank.agent_reconcile_runs.tool_call_summary and the legacy_meta.agent_notes trail on receipt_matches. The toolbox shipped in PR Finance reconciliation: toolbox (closes unimatrix27/ideas#23) #5 enforces this; tests exercise the audit shape end-to-end.
  • A test run against a fixture month with one injected anomaly produces zero unapproved auto-sends, one anomaly row, a report mentioning it. Covered by test_unknown_vendor_anomaly_is_flagged_not_sent.
  • The skill file is immutable from inside any agent run. Frontmatter locked: true + ## Subagent dispatch heading → SOUL.md skill-mutation rule forbids skill_manage(action='patch') and any other in-agent write. Not exercised by an attempted-patch test in this PR because the lock is enforced at the framework level (SOUL.md), not by the skill itself — testing that would mean testing SOUL.md's rules, which is outside Nous Portal subscription integration NousResearch/hermes-agent#25's scope.

Follow-ups discovered during the live smoke (PR #5 territory)

These are all surfaced by NousResearch#25 but owned by PR #5. They block production cron runs from doing real work until each is resolved:

  1. DEFAULT_DATEV_RECIPIENT is a placeholder. finance/tools/verbs.py hardcodes rechnung@lineo.finance as the recipient default. The real DATEV uploadmail is 36ec220d-733a-4c6e-a626-33cbcb408039@uploadmail.datev.de. The SKILL.md works around this by passing --datev-recipient + --from-mailbox literally on every send_match invocation, but a clean fix is an env var in PR Finance reconciliation: toolbox (closes unimatrix27/ideas#23) #5.
  2. --summary-md alias on finalize_run is documented in --help but missing from argparse. Crashed the agent's first finalize_run call in the live smoke. PR Finance reconciliation: toolbox (closes unimatrix27/ideas#23) #5 should either drop the misleading --help blurb or add the alias.
  3. --tool-call-summary alias on finalize_run is similarly missing. Currently --notes writes to the notes text column, not the tool_call_summary jsonb column. The Nous Portal subscription integration NousResearch/hermes-agent#25 spec says month_scope and model_id go in tool_call_summary — that placement is impossible via the CLI today. The SKILL.md works around this by sending --notes <jsonb> until PR Finance reconciliation: toolbox (closes unimatrix27/ideas#23) #5 ships the alias.
  4. agent_reconcile_runs.invoked_by check constraint is narrow ({llm, user, cron}). An agent setting --invoked-by 'cron:reconcile-receipts' hard-rejects with CheckViolation. Either loosen to a prefix match, or document that the value must be one of three tokens.
  5. SOUL.md dispatch rule doesn't fire on the cron path — the cron scheduler loads skill: <name> via tools.skills_tool.skill_view() and prepends the body to the parent's prompt; the SOUL rule is documented for agents loading skills, not for the cron load path. In the live smoke the parent ran the workflow inline (with all turns counting against its own budget) rather than via delegate_task. The skill's behaviour is correct either way, but the fresh-context Mode B isolation isn't being realized in the cron path. Either move the dispatch rule into the cron-load path, or accept inline execution and adjust the SKILL.md framing.

Files

Path Purpose
skills/finance/reconcile-receipts/SKILL.md The locked Mode B skill. Frontmatter + ## Subagent dispatch + Role / Workflow / Decision norms / Hard rules / Report contract — mirroring the weather-heating template.
finance/tests/test_reconcile_skill.py 13 offline tests with MockDispatchRunner + ScriptedChild + the existing InMemoryToolAdapter / FakeGraphMailSender / RecordingNotifier fixtures from PR #5.

Out of scope (intentionally not in this PR)

Test plan

🤖 Generated with Claude Code

unimatrix27 and others added 11 commits May 11, 2026 16:25
…ema and idempotent backfill

Implements unimatrix27/ideas#20 — data foundation only, no matching logic.

Migration: creates two new tables in the existing bank.* schema with the
columns, checks, and indexes specified in NousResearch#20. Legacy tables
(transactions, belege_sent, belege_to_send, belege_missing, match_proposals)
are not touched. Rollback drops only what up created.

Backfill: populates the new tables from three legacy sources and skips
bank.match_proposals (out of scope per NousResearch#20). Idempotent on re-run via a
unique partial index on legacy_belege_sent_id and legacy_meta lookups for
rows without it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One-shot, public-fork-safe fixture pack so NousResearch#22 (parsers + matcher) can be
implemented fully offline. Ships:

  - tests/fixtures/finance/<vendor>/<invoice>.txt + .meta.json for Sipgate,
    Notion, Lucky Penny (invoice + paired credit note), and Vodafone.
  - tests/fixtures/finance/vodafone/portal_notification_*.txt — body of one
    notification-only email (Vodafone is portal-only most months).
  - tests/fixtures/finance/transactions.jsonl — 11 named TX ids + the Google
    Ads kanban-task row; counterparty IBANs redacted to "DE**".
  - tests/fixtures/finance/beleg_match_samples.jsonl — 9 rows incl. all 3
    via='manual_review' shapes verbatim (load-bearing for NousResearch#20's backfill
    tests).
  - tests/fixtures/finance/belege_sent_samples.jsonl — 9 rows covering each
    via value, >=2 with bank_tx_id IS NULL, >=2 with attachments.
  - finance/scripts/build_fixtures.py + README — the re-runnable extractor.

Re-running build_fixtures.py against the same Supabase + mailbox state
produces byte-identical output. The script is NOT run in CI; it needs
SUPABASE_DB_URL + the LINEO_MS_* delegated token bundle.
…deas#22)

Pure-function parsers for Sipgate, Notion, Lucky Penny, and Vodafone
under finance/parsers/, plus finance/matcher.py — the deterministic
candidate generator that walks open bank.transactions and writes
'proposed' (or 'manual_needed' for portal-only) rows to
bank.receipt_matches with stable reason codes.

Matcher invariants (anchored in NousResearch#27):
  - Never writes 'approved' / 'sent' / 'rejected' / 'ignored'.
  - Skips ignored transactions and txs with an existing approved/sent match.
  - Idempotent: re-runs touch nothing unless reason_codes change.
  - decided_by='code' for everything it writes.

Strategy (A) per the implementing-agent brief: tests run fully offline
against InMemoryMatcherAdapter loaded from the NousResearch#24 fixture pack. A thin
PostgresMatcherAdapter is included for the cron entrypoint, mirroring
PR #1's psycopg2 style — no ORM, no LLM, no network.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mode-B receipt ingest pipeline for the finance reconciliation feature
(NousResearch#27). Pulls /messages/delta from configured Microsoft Graph mailboxes,
downloads PDF attachments once (SHA-256 dedupe), extracts text once
(pymupdf default; marker-pdf gated behind FINANCE_INDEXER_ALLOW_MARKER),
and lands one row per attachment in bank.receipt_candidates. Senders on
a portal-required allowlist with no PDF attachment land with
parse_status='portal_required'.

Implementation mirrors PR #3's MatcherAdapter pattern: an IndexerAdapter
Protocol with in-memory + Postgres implementations, so tests stay
offline. Reuses tools/microsoft_graph_client.py via a delegated
DelegatedTokenProvider that quacks like the upstream MicrosoftGraphTokenProvider
(refresh-token grant against the lineo-ms-tokens bundle).

Migration 002_indexer_state adds bank.indexer_state keyed by a composite
mailbox::folder string — Graph rejects mailbox-wide /messages/delta with
"Change tracking is not supported", so the indexer is folder-scoped. The
well-known 'inbox' ID resolves regardless of mailbox locale.

Same code path serves the CLI (python -m finance.indexer for cron) and
NousResearch#23's run_indexer() wrapper (finance.indexer.run). Cron entry registered
at ~/.hermes/cron/jobs.json with schedule "0 9-18 * * 1-5", invoking
~/.hermes/scripts/finance_indexer.sh (no_agent=True, deterministic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the deterministic verb toolbox the Mode-B reconcile agent (NousResearch#25)
orchestrates. Each verb is a `finance-reconcile <subcommand>` invocation
and a `from finance.tools import ...` callable — same code path either
way, so NousResearch#25's agent gets the full verb set through the standard hermes
`terminal` toolset (no custom MCP server / per-verb registration).

* finance/cli.py — `finance-reconcile` argparse entrypoint. Read verbs
  emit JSON on stdout; write/side-effect verbs emit "ok:" / "error:"
  + exit code.
* finance/tools/ — Protocol + InMemory + Postgres adapters mirroring
  PR #3 / #4. Verbs:
  Read:        list_open_transactions, get_tx_context, get_proposals,
               get_run_history, read_anomalies
  Job runner:  run_indexer, run_matcher (thin wrappers around NousResearch#21/NousResearch#22)
  Write:       approve_match, reject_match, mark_manual_needed,
               mark_ignored (hard guard against true→false flip)
  Side effect: send_match (four-step Graph pipeline, idempotent on
               natural key), flag_anomaly, search_for_missing_receipt
               (graceful when NousResearch#28 not registered), finalize_run
* finance/migrations/003 — additive `bank.receipt_status_v` view with
  the six-bucket classification; `bank.belege_missing` unchanged.
* finance/migrations/004 — `bank.agent_anomalies` +
  `bank.agent_reconcile_runs` tables (shapes only; agent skill ships
  in NousResearch#25).
* finance/tests/test_tools.py — 36 offline tests covering the
  acceptance criteria (mark_ignored guard, send_match four cases incl.
  step-c-success/step-d-failure, hunter graceful degradation,
  concurrent approve_match, read-only invariant).

End-to-end smoke: send_match exercised once against
sebastian.stuecker@gmail.com (NOT real DATEV) using a synthetic PDF
and a benign 'done' tx; resulting belege_sent row id captured and then
deleted to restore production state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Mode-B LLM brain that orchestrates the deterministic toolbox shipped
in #5 (NousResearch#23). One locked SKILL.md, one offline test file. No new tools, no
new tables, no Python under finance/ beyond the test file — per NousResearch#25's
scope discipline. The cron entry that schedules this skill lives in
~/.hermes/cron/jobs.json on the EC2; it is not in the repo.

* skills/finance/reconcile-receipts/SKILL.md — locked Mode B skill.
  Frontmatter sets `metadata.hermes.locked: true` and the body opens
  with `## Subagent dispatch` so SOUL.md routes the cron-spawned parent
  through `delegate_task` exactly once with toolsets=[terminal, skills]
  and role=leaf. `web` is intentionally not granted. The subagent brief
  mirrors the weather-heating template (NousResearch#27's rehearsal): role,
  workflow, decision norms, hard rules, report contract. Decision norms
  cover the four confidence buckets, the missing-bucket hunter
  dispatch (one call per tx, no loop), portal-vendor manual_needed
  fallback, and suppression against read_anomalies + get_run_history.
  send_match calls always pass the explicit DATEV uploadmail via
  --datev-recipient + --from-mailbox so PR #5's placeholder default
  never reaches a real send. German trigger phrases (`belege agent`,
  `monatsabschluss`) in the description for German voice triggers.

* finance/tests/test_reconcile_skill.py — 13 offline tests, no Postgres,
  no network, no Graph, no LLM. Builds a MockDispatchRunner that parses
  the dispatch block out of SKILL.md and asserts the SOUL contract
  (exactly one delegate_task, no inline workflow), plus a ScriptedChild
  that transcribes the SKILL.md decision norms into Python so the
  tool-call ORDER + SHAPE assertions hit the rules, not a model's
  wording. Coverage matches issue NousResearch#25's test plan: skill-dispatch,
  prompt regression on a clean month, anomaly flagging on an unknown
  vendor, re-flag suppression (open anomaly + prior-run row), Sipgate
  B4373121 → TX 56 approve-then-send, adversarial un-ignore request,
  self-audit silence (proposed_changes=null), and hunter graceful
  degradation (portal vendor + non-portal vendor + hunter-found-
  candidate flow, all asserting exactly one search_for_missing_receipt
  call per tx).

Verified locally: 13/13 new tests pass; 107/107 total finance tests
pass (the existing 94 from #5 + the new 13).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first live cron tick on 2026-05-12 hit two argparse gaps shipped in
PR #5's `finance-reconcile finalize_run`: the --help blurb advertises
backward-compat aliases `--summary-md` and `--tool-call-summary`, but
neither is registered with argparse. The agent's first run used those
aliases per the SKILL.md as-written, crashed with `error: the following
arguments are required: --summary`, then fabricated a fake success
({"run_id": 76, "notifier_dispatched": true}) in its final text turn
which Telegram delivered.

Switching the SKILL.md to the actually-registered flags (`--summary`,
`--notes`, `--invoked-by 'cron'`) keeps the skill correct against
PR #5 as-shipped. PR #5 owns the alias fix (tracked as a follow-up
in this PR's body).

Also pinned `--invoked-by 'cron'` explicitly so the agent doesn't pick
something like `cron:reconcile-receipts` and trip the
`agent_reconcile_runs_invoked_by_chk` constraint
(allowed: llm|user|cron).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 12, 2026

Copy link
Copy Markdown

🔎 Lint report: feat/finance-reconcile-skill vs origin/main

ruff

Total: 2 on HEAD, 0 on base (🆕 +2)

🆕 New issues (2):

Rule Count
PLW1514 2
First entries
finance/indexer.py:1324: [PLW1514] `pathlib.Path(...).read_text` without explicit `encoding` argument
finance/scripts/build_fixtures.py:138: [PLW1514] `pathlib.Path(...).read_text` without explicit `encoding` argument

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 8005 on HEAD, 7967 on base (🆕 +38)

🆕 New issues (36):

Rule Count
unresolved-import 23
invalid-argument-type 7
invalid-assignment 2
unknown-argument 1
unsupported-operator 1
possibly-missing-submodule 1
invalid-return-type 1
First entries
finance/matcher.py:88: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `date`, found `(Any & ~str & ~datetime) | None | date`
finance/indexer.py:388: [unresolved-import] unresolved-import: Cannot resolve imported module `marker.convert`
finance/indexer.py:704: [invalid-argument-type] invalid-argument-type: Argument to bound method `GraphFetcher.list_attachments` is incorrect: Expected `str`, found `Any | None`
finance/tools/verbs.py:539: [unknown-argument] unknown-argument: Argument `attachment_sha256` does not match any known parameter of bound method `ToolAdapter.insert_belege_sent`
finance/indexer.py:1400: [unresolved-import] unresolved-import: Cannot resolve imported module `psycopg2`
finance/tests/test_indexer.py:26: [unresolved-import] unresolved-import: Cannot resolve imported module `pymupdf`
finance/parsers/vodafone.py:124: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["gross_amount"]` and value of type `int | float` on object of type `dict[str, str]`
finance/cli.py:76: [unresolved-import] unresolved-import: Cannot resolve imported module `psycopg2`
finance/indexer.py:1075: [unresolved-import] unresolved-import: Cannot resolve imported module `psycopg2.extras`
finance/tests/test_tools.py:548: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["Run done"]` and `str | None`
finance/backfill_receipts.py:29: [unresolved-import] unresolved-import: Cannot resolve imported module `psycopg2`
finance/scripts/build_fixtures.py:218: [possibly-missing-submodule] possibly-missing-submodule: Submodule `error` might not have been imported
finance/tools/graph.py:91: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `list[dict[str, Any]]`
finance/backfill_receipts.py:30: [unresolved-import] unresolved-import: Cannot resolve imported module `psycopg2.extras`
finance/indexer.py:389: [unresolved-import] unresolved-import: Cannot resolve imported module `marker.models`
finance/tests/test_fixture_pack.py:11: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
finance/indexer.py:1367: [invalid-argument-type] invalid-argument-type: Argument to `MicrosoftGraphClient.__init__` is incorrect: Expected `MicrosoftGraphTokenProvider`, found `DelegatedTokenProvider`
finance/tests/test_reconcile_skill.py:61: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
finance/indexer.py:368: [unresolved-import] unresolved-import: Cannot resolve imported module `pymupdf`
finance/tests/test_parsers.py:12: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
finance/scripts/build_fixtures.py:50: [unresolved-import] unresolved-import: Cannot resolve imported module `psycopg2`
finance/tools/notifier.py:33: [invalid-argument-type] invalid-argument-type: Argument to function `print` is incorrect: Expected `SupportsWrite[str] | None`, found `object`
finance/scripts/build_fixtures.py:51: [unresolved-import] unresolved-import: Cannot resolve imported module `psycopg2.extras`
finance/verify_backfill.py:12: [unresolved-import] unresolved-import: Cannot resolve imported module `psycopg2`
finance/tools/verbs.py:215: [invalid-argument-type] invalid-argument-type: Argument to function `run_matcher` is incorrect: Expected `MatcherAdapter`, found `Any | _FilteringMatcherAdapter`
... and 11 more

✅ Fixed issues: none

Unchanged: 4210 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

The 2026-05-12 live cron smoke caught the agent hallucinating
{run_id: 76, notifier_dispatched: true} after finalize_run crashed on
the PR #5 --summary-md alias gap. The existing "no fallback to inline
reasoning" rule wasn't strong enough to suppress the pretraining bias
toward writing-a-plausible-result.

Replaces that rule with a verbatim negative-example block ("NEVER
fabricate a tool result", "NEVER invent a run_id ..."), and adds one
regression test that simulates finalize_run raising ToolError on first
call: asserts a single flag_anomaly naming the failed verb, no
approvals/sends after the failure, an explicit finalize exit, and
no fabricated db ids in the recorded summary_md.

The 5 PR-#5 toolbox follow-ups remain owned by PR #5 / its
follow-up issue, not this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@unimatrix27

Copy link
Copy Markdown
Owner Author

Adds Track A from NousResearch#25's live-smoke postmortem: hardens the tool-failure rule in SKILL.md with a verbatim negative-example block, plus a regression test that fails before the prompt change. The 5 toolbox follow-ups remain in their own issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant