Files
accounted/extensions/general/mcp-server/recommended-tools.ts
T
MattssonandClaude Fable 5 a0ca692fed feat(invoices): quarterly, half-yearly and yearly recurring invoice schedules (#1438)
* fix(mcp): offer the link tool in the uncategorized-transactions VAT blocker

The gnubok_vat_close_check blocker hint only named categorize/auto-match,
both of which create new bookkeeping. For a transaction whose
affarshandelse is already booked on an existing verifikat, following the
hint would double-book, so agents dead-ended the case into "contact
support" (2026-08-06 support mail from Orto Engineering). The hint now
also names gnubok_link_transaction_to_journal_entry, is extracted as an
exported constant pinned by a test, and the tool joins the
categorize_month recommended loadout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(invoices): quarterly, half-yearly and yearly recurring schedules

User request: recurring invoice schedules only supported monthly cadence.
Adds interval_months (SMALLINT 1-12, default 1) to
recurring_invoice_schedules; the UI offers manadsvis/kvartalsvis/
halvarsvis/arsvis presets while API and MCP accept any 1-12.

The cron advances next_run_date by whole intervals from the due date, and
the new rollNextRunDateForward() helper rolls missed or edited interval
schedules on their own month grid so a quarterly Jan/Apr/Jul/Oct schedule
missed in an outage rolls Jan 15 to Apr 15, never Feb 15. Monthly
(interval 1) keeps its existing today-anchored recompute semantics
unchanged. Changing the interval alone never touches next_run_date: the
new cadence applies from the next run, so an edit can never pull a send
earlier.

Existing rows default to 1 and behave byte-identically. The MCP slice of
this feature (interval_months on the three recurring-schedule tools in
server.ts) was committed in d2600907f alongside the VAT-blocker hint fix
by a parallel session sharing this worktree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(invoices): address PR #1438 review findings

CodeRabbit round 1, all three findings:
- MCP descriptions now state the full accepted interval range (any integer
  1-12) instead of enumerating only the 1/3/6/12 presets, and qualify that
  changing ONLY interval_months leaves next_run_date untouched.
- assertValidCadence rejects fractional day_of_month.
- rollNextRunDateForward rejects calendar-invalid anchors that pass the
  shape regex (2026-13-05, 2026-02-31), with regression tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 15:43:54 +02:00

152 lines
5.8 KiB
TypeScript

/**
* Recommended tool loadouts per workflow, surfaced by gnubok_get_agent_briefing
* as `recommended_tools`.
*
* Why: client harnesses with deferred tool loading (Claude Code ToolSearch,
* claude.ai connector search) otherwise burn 4-6 round-trips discovering tools
* cluster by cluster before any work happens. Each loadout names the exact
* registry tools a workflow needs, ordered by typical call sequence, so a
* harness that supports batch selection (ToolSearch select:a,b,c) loads the
* whole cluster in ONE call, and connector agents stop guessing search
* keywords.
*
* Drift protection (same spirit as deriveToolMeta): the loadouts are validated
* against the real tool registry and the workflow-skill registry via
* assertRecommendedLoadoutsValid(), called at module init in server.ts right
* after the tools array is defined. A loadout naming a tool or skill that does
* not exist fails module load, and therefore every test that imports the
* server. __tests__/agent-briefing.test.ts additionally pins the check.
*
* The list is static per issue #1098: the briefing does not currently query
* workflow state (unbooked counts, open periods), so gating inclusion on state
* would mean new reads in the hot bootstrap path. Every loadout is returned
* for every company; applicability is the agent's judgment call.
*/
import { workflowSkills } from './skills'
export interface WorkflowLoadout {
/** Stable snake_case workflow key (e.g. "categorize_month"). */
workflow: string
/** One-line English description of what the workflow accomplishes. */
description: string
/** Workflow-skill slug: pass to gnubok_load_skill for the full playbook. */
skill: string
/** Exact registry tool names, ordered by typical call sequence. */
tools: readonly string[]
}
export const RECOMMENDED_WORKFLOW_LOADOUTS: readonly WorkflowLoadout[] = [
{
workflow: 'categorize_month',
description: 'Categorize and book a month of bank transactions.',
skill: 'bank-reconciliation',
tools: [
'gnubok_list_uncategorized_transactions',
'gnubok_suggest_categories',
'gnubok_categorize_transaction',
'gnubok_match_transaction_to_invoice',
// For transactions whose affärshändelse is already booked on an existing
// verifikat: links without creating new bookkeeping. Categorizing such a
// transaction would double-book it.
'gnubok_link_transaction_to_journal_entry',
// Tagging: check the registry before writing dimensions bags on
// categorize calls (resolve-don't-select needs real codes/names).
'gnubok_list_dimensions',
'gnubok_load_skill',
'gnubok_approve_pending_operation',
],
},
{
workflow: 'close_period',
description: 'Reconcile, document voucher gaps, and lock a fiscal period.',
skill: 'month-end-close',
tools: [
'gnubok_list_fiscal_periods',
'gnubok_list_uncategorized_transactions',
'gnubok_get_reconciliation_status',
'gnubok_list_voucher_gaps',
'gnubok_explain_voucher_gap',
'gnubok_lock_period',
'gnubok_approve_pending_operation',
],
},
{
workflow: 'invoice_run',
description: 'Create and send customer invoices.',
skill: 'invoicing-rules',
tools: [
'gnubok_list_customers',
'gnubok_create_customer',
'gnubok_list_articles',
// Tagging: invoices carry default_dimensions + per-item bags; check the
// registry before setting them on gnubok_create_invoice.
'gnubok_list_dimensions',
'gnubok_create_invoice',
'gnubok_send_invoice',
'gnubok_mark_invoice_as_sent',
'gnubok_approve_pending_operation',
],
},
{
workflow: 'vat_declaration',
description: 'Compute, review, and file the VAT declaration.',
skill: 'quarterly-vat-review',
tools: [
'gnubok_get_vat_report',
'gnubok_vat_close_check',
'gnubok_get_general_ledger',
'gnubok_vat_declaration_validate',
'gnubok_vat_declaration_submit',
'gnubok_vat_declaration_status',
'gnubok_approve_pending_operation',
],
},
{
workflow: 'payroll_month',
description: 'Run monthly payroll and generate the AGI.',
skill: 'payroll-monthly',
tools: [
'gnubok_list_employees',
'gnubok_create_salary_run',
'gnubok_calculate_salary_run',
'gnubok_get_salary_run',
'gnubok_book_salary_run',
'gnubok_generate_agi',
'gnubok_approve_pending_operation',
],
},
]
/**
* Fails fast when a loadout references a tool or workflow skill that does not
* exist. Called at module init in server.ts (after the tools array is built)
* so any rename/removal in the registry breaks the build and the test suite
* immediately instead of shipping a briefing that recommends phantom tools.
*/
export function assertRecommendedLoadoutsValid(knownToolNames: ReadonlySet<string>): void {
const knownSkillSlugs = new Set(workflowSkills.map((s) => s.slug))
const seenWorkflows = new Set<string>()
for (const loadout of RECOMMENDED_WORKFLOW_LOADOUTS) {
if (seenWorkflows.has(loadout.workflow)) {
throw new Error(
`recommended_tools: duplicate workflow key "${loadout.workflow}" in RECOMMENDED_WORKFLOW_LOADOUTS.`
)
}
seenWorkflows.add(loadout.workflow)
if (!knownSkillSlugs.has(loadout.skill)) {
throw new Error(
`recommended_tools: workflow "${loadout.workflow}" references unknown skill slug "${loadout.skill}". ` +
'Update RECOMMENDED_WORKFLOW_LOADOUTS in recommended-tools.ts.'
)
}
for (const toolName of loadout.tools) {
if (!knownToolNames.has(toolName)) {
throw new Error(
`recommended_tools: workflow "${loadout.workflow}" references unknown tool "${toolName}". ` +
'Update RECOMMENDED_WORKFLOW_LOADOUTS in recommended-tools.ts when renaming or removing tools.'
)
}
}
}
}