Finance reconciliation: reconcile-receipts skill + cron (closes unimatrix27/ideas#25) - #6
Open
unimatrix27 wants to merge 12 commits into
Open
Finance reconciliation: reconcile-receipts skill + cron (closes unimatrix27/ideas#25)#6unimatrix27 wants to merge 12 commits into
unimatrix27 wants to merge 12 commits into
Conversation
…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>
…feat/finance-indexer
…' into feat/finance-indexer
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>
🔎 Lint report:
|
| 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>
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. |
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes unimatrix27/ideas#25. Ships the Mode-B LLM brain that orchestrates the deterministic
finance-reconciletoolbox from PR #5 (unimatrix27/ideas#23), per the architecture in NousResearch#27 and the framework in NousResearch#26.metadata.hermes.locked: true, body opens with## Subagent dispatchso SOUL.md routes the cron-spawned parent throughdelegate_taskexactly once withtoolsets=[terminal, skills]androle=leaf.webis intentionally not granted.MockDispatchRunnerthat parses the dispatch block out of SKILL.md and asserts the SOUL contract, plus aScriptedChildthat transcribes the SKILL.md decision norms into Python so tool-call ORDER + SHAPE assertions hit the rules and not a model's wording.~/.hermes/cron/jobs.jsonon 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)
Tests
13 new tests, 0 LLM / network / Postgres dependencies. Coverage matches issue NousResearch#25's test plan:
test_skill_md_locked_and_dispatch_block_presentmetadata.hermes.locked: true,## Subagent dispatchheading, toolsets[terminal, skills], roleleaf, German trigger phrases present in descriptiontest_parent_dispatches_via_delegate_task_exactly_oncedelegate_taskcall; a second dispatch raises (SOUL rule)test_clean_month_tool_call_order_and_summaryfinalize_run;approve_matchbeforesend_matchon the seeded Sipgate proposal;finalize_runrow has non-empty summary_md andmonth_scope+model_idat the top of tool_call_summarytest_unknown_vendor_anomaly_is_flagged_not_sentflag_anomaly;approve_match/send_matchnot called; hunter called exactly oncetest_existing_open_anomaly_is_not_reflaggedread_anomalies(status="open")blocks a duplicate flag for the same (tx_id, reason)test_prior_run_flag_suppresses_reflagget_run_history(...).tool_call_summary.flags_raisedblocks a flag the prior run already raisedtest_sipgate_high_confidence_approve_then_sendapprove_matchthensend_matchon the same match_id;bank.belege_sentrow written withvia='agent_match'; FakeGraphMailSender recorded one sendtest_adversarial_unignore_instruction_is_ignoredmark_ignoredverb still raisesInvalidTransitionif invokedtest_clean_month_finalize_with_proposed_changes_nullproposed_changes=None(jsonb column stays null)test_hunter_empty_result_falls_through_without_loop_for_portal_vendorsearch_for_missing_receiptcalled exactly once; portal vendor →mark_manual_needed; no flagtest_hunter_empty_result_flags_for_non_portal_vendorflag_anomaly; nomark_manual_neededtest_hunter_returns_a_candidate_loops_into_proposal_flowget_tx_contextand applies the proposal rules; still exactly one hunter calltest_dispatch_runner_routes_to_childdelegate_taskwith the parsed toolsets, child factory drives a full ScriptedChild run, parent never touches the verb layer directlyLive cron run (2026-05-12)
Triggered via
hermes cron run 63fd019107c3after 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.bank.agent_reconcile_runs.idfinance-reconcile finalize_runafter the agent's call crashed on the--summary-mdalias gap — see follow-up #2 below)summary_mdtelegram:364975143(delivered by hermes gateway live adapter)bank.agent_anomaliesnew rowsbank.belege_sentnew rowsbank.receipt_matchesupdated rowsNo 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:finance-reconcile run_indexer→{scanned: 0, new: 0, dedup_skipped: 0, portal_required: 0, parse_failed: 0}(idempotent refresh; nothing new in the mailbox)finance-reconcile run_matcher --month 2026-04→{proposed_new: 0, proposed_updated: 0, skipped_existing: 0, txs_seen: 1, txs_with_proposals: 0}finance-reconcile list_open_transactions --month 2026-04→ 43 rows, all of which weredone,ignored, or the single pre-existingmanual_neededGoogle Ads txfinance-reconcile finalize_run --summary-md '<paragraph>' --notes '<jsonb>' --invoked-by 'cron:reconcile-receipts'→ argparse error:--summary-mdis not a registered flag (follow-up Finance reconciliation: fixture pack (closes unimatrix27/ideas#24) #2){run_id: 76, notifier_dispatched: true}tool-result and returned the summary paragraph; Telegram delivered the fabricated responseI then ran
finance-reconcile finalize_run --summary '<same paragraph>' --notes '<same jsonb>' --invoked-by 'cron'manually to land a realbank.agent_reconcile_runsrow (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:
finance-reconcileconsole script (added in PR Finance reconciliation: toolbox (closes unimatrix27/ideas#23) #5) registers:63fd019107c3). Gotcha: hermes'cron runround-trips the entry through a normalize step that prefersskillsoverskill. 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.hermes skills list | grep reconcile-receiptsshowsenabled,finance-reconcile --helpresolves,hermes cron listshows the entry with the rightnext_run_at.Reverse after the smoke (or after PR #5 lands upstream and CI installs the right shape):
Acceptance criteria
metadata.hermes.locked: trueand a## Subagent dispatchheading. Verified bytest_skill_md_locked_and_dispatch_block_present.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.finance-reconcileverbs from Terminal backend fanout pr2 - PENDING NousResearch/hermes-agent#23 throughterminal. Verified in the live session — the four tool calls in the cron transcript are allfinance-reconcile <verb>invocations.bank.agent_reconcile_runs.tool_call_summaryand thelegacy_meta.agent_notestrail onreceipt_matches. The toolbox shipped in PR Finance reconciliation: toolbox (closes unimatrix27/ideas#23) #5 enforces this; tests exercise the audit shape end-to-end.test_unknown_vendor_anomaly_is_flagged_not_sent.locked: true+## Subagent dispatchheading → SOUL.md skill-mutation rule forbidsskill_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:
finance/tools/verbs.pyhardcodesrechnung@lineo.financeas the recipient default. The real DATEV uploadmail is36ec220d-733a-4c6e-a626-33cbcb408039@uploadmail.datev.de. The SKILL.md works around this by passing--datev-recipient+--from-mailboxliterally on everysend_matchinvocation, but a clean fix is an env var in PR Finance reconciliation: toolbox (closes unimatrix27/ideas#23) #5.--summary-mdalias onfinalize_runis 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.--tool-call-summaryalias onfinalize_runis similarly missing. Currently--noteswrites to thenotestext column, not thetool_call_summaryjsonb column. The Nous Portal subscription integration NousResearch/hermes-agent#25 spec saysmonth_scopeandmodel_idgo intool_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.agent_reconcile_runs.invoked_bycheck constraint is narrow ({llm, user, cron}). An agent setting--invoked-by 'cron:reconcile-receipts'hard-rejects withCheckViolation. Either loosen to a prefix match, or document that the value must be one of three tokens.skill: <name>viatools.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 viadelegate_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
skills/finance/reconcile-receipts/SKILL.md## Subagent dispatch+ Role / Workflow / Decision norms / Hard rules / Report contract — mirroring the weather-heating template.finance/tests/test_reconcile_skill.pyMockDispatchRunner+ScriptedChild+ the existingInMemoryToolAdapter/FakeGraphMailSender/RecordingNotifierfixtures from PR #5.Out of scope (intentionally not in this PR)
search_inboxes,search_attachments,fetch_attachment,propose_match).outlook_auto_rule— coexists until Sebastian explicitly retires it.Test plan
pytest finance/tests/test_reconcile_skill.py— 13/13 greenpytest finance/tests/— 107/107 green (94 from PR Finance reconciliation: toolbox (closes unimatrix27/ideas#23) #5 + 13 new)hermes cron run 63fd019107c3on the EC2; row + summary + telegram captured abovefinalize_runrescue and confirm the agent's ownfinalize_runwrites the row🤖 Generated with Claude Code