80a14ddfd2
Closes the MCP dimension gaps found in the 2026-07-28 audit:
- gnubok_bulk_book_inbox_items accepts a shared dimensions bag through
all three layers (tool schema + BulkBookInboxSchema + categorize-core
BulkBookInboxInput), resolve-don't-select with echoed resolutions; the
web inbox bulk-book route and the pending-op executor inherit it via
the shared schema.
- gnubok_create_employee / gnubok_update_employee accept
default_dimensions (names resolve to codes; {} clears on update).
The command layer already persisted the field: only the MCP boundary
blocked it, leaving payroll tagging dashboard-only.
- gnubok_query_journal: dimensions bag filter (jsonb containment via
the GIN index, covers custom dims the legacy project/cost_center
filters cannot) + include_dimensions to return each line's bag.
The wide full-match fetch stays dims-free unless something needs it.
- gnubok_list_invoices / gnubok_list_supplier_invoices return
default_dimensions (agents could set invoice bags but never read
them back).
- Discoverability: create_voucher, categorize_transaction,
correct_entry, update_invoice descriptions now name dimensions;
categorize_month and invoice_run loadouts include
gnubok_list_dimensions. Trimmed new schema prose to stay under the
tools/list payload budget.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
148 lines
5.6 KiB
TypeScript
148 lines
5.6 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',
|
|
// 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.'
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|