| name | sailor-mandate-planner |
| description | Station 3 entry — turn a complete strategy spec into a mandate plan, routing each action to a shared permission template or to bespoke authoring. Use when the user says "build the mandate", "which template do I need", "turn my strategy into permissions", "register permissions", "what bounds does my agent need" — and structurally whenever .sail/strategy.md is complete and mandate work begins. |
sailor-mandate-planner — route the strategy into enforced bounds (Station 3)
This skill is a gate and a router, deliberately thin. The template registry lives in sailor-templates, the bespoke method in sailor-mandates, the per-category routing rows in sailor-strategy's references, and the cross-category possibility map — this file tells you when to cross the gate, WHERE the route for each action actually comes from, and which simulation path proves it.
Gate (fail-closed)
Read .sail/strategy.md first. AGENTS.md station 3: "Gate: complete .sail/strategy.md". If the file is missing, any completeness dimension is not concrete, its JSON block lacks "confirmedByUser": true, or its "version" is not 3 — do not proceed. Hand back to sailor-strategy and return once the gate holds — a pre-version: 3 file either has no resolved actions[] to plan from (version < 2) or has never had the per-action exitPath question asked (version: 2). The completeness checklist lives there, not here.
Routing method
-
Enumerate the actions from actions[] — it's already the resolved list, don't re-derive it. The spec's JSON actions array is one entry per distinct action the strategy implies — every swap leg, deposit, borrow (including the collateral-supply leg, which is itself a deposit, and the repay/unwind leg if the strategy has an exit condition), transfer, and withdrawal — each already carrying its resolved tokenIn/tokenOut, venue, pool (swap actions), caps, and riskBounds. This is the producer→consumer contract with Station 2: what you configure a permission FROM is what's already in the action's caps/riskBounds, not a re-elicitation — if a value looks missing, that's a Station 2 gap, not something to infer here.
Check every action on that list against sailor-mandates/references/approvals.md — not swap alone, and at enumeration time, not as an afterthought — a deposit, collateral-supply leg, or repay leg needs the same scrutiny as a swap, and a route.type: "bespoke" action is not exempt just because it has no spoke skill to remind you (see sailor-templates's "Beyond the templates"). approvals.md is the single source on which actions need approve coverage and how; do not re-derive the logic here.
-
Each action's route field is the input — verify it, don't re-derive from scratch. Station 2 (sailor-strategy Act 2) already recorded it in action.route: {type, name}. For each action:
route.type: "template" — verify it actually holds: the named template's selector set genuinely matches this call shape (check the spoke's "What it enforces" table), and its capability limits (chain, venue family, oracle requirement) actually fit the action's chain/venue/pool. Holds → proceed. Doesn't → the spec was wrong; resolve it per the fallback below and say so to the user.
route.type: "bespoke" — verify a shared template genuinely can't express it (check sailor-templates — sailor mandate templates --json / node .agents/skills/sailor-templates/catalog.mjs --chain <id> — before accepting "bespoke" at face value; a spec can overreach here too).
- Missing
route, or version < 3 (an older spec, or a gap) — resolve it now, using the same aids Station 2 uses: the matching category reference's routing rows when the action's category fits, or the possibility map when none does — and say plainly that you resolved it here, not at Station 2.
- Mixing templates and bespoke in one mandate is normal — a plan is N template rows + M bespoke rows, not a binary choice.
-
Present the mandate plan to the user before any deployment. One row per action: the action → its permission (template name, or "bespoke: ") → the bounds it will carry (straight from that action's caps and riskBounds — the same resolved values the user already reviewed at Station 2 exit, now feeding the permission config, not re-typed) → its simulation route (below). Include the registration-fee disclosure — fee mechanics live in sailor-mandates (Registration fee section).
Every bespoke row carries this disclosure: what it wants isn't a standard template, but it's completely expressible — the coding agent authors a permission that bounds exactly this, in contracts/, through sailor-mandates's authoring gates. The kernel evaluates it on every dispatch but does not verify its logic does what it claims: the operator reviews it and signs it (the protocol's own Known Limitations). What bespoke work against this specific venue actually entails was already disclosed at Station 2 exit (sailor-strategy Act 3) — don't repeat it, but if authoring is about to start against an exotic or multi-generation venue, re-surface sailor-mandates/references/dark-reverts.md now. If the plan has no bespoke rows, say nothing about Solidity.
Toolchain precheck (only when any row is bespoke): before presenting the plan as approvable, verify Foundry is installed — run forge --version. If it is missing, do not present an approvable plan; route the user to the Foundry install one-liner in sailor-mandates ("Prerequisite — Foundry"), then rerun the check.
Price-band tolerance check (swap rows only) — before presenting the plan as approvable. For every swap action, whatever route it took, check the tolerance you're about to configure — SwapPermission's maxSlippageBps or SwapPermissionNoOracle's per-pair toleranceBps — against the action's own resolved pool.feeTier (already in the strategy artifact; Uniswap V3 fee units are hundredths of a bip — divide by 100 for bps) and riskBounds.maxSlippageBps (the agent's own slippage). If tolerance ≤ fee + slippage, the band will silently reject every legitimate trade — say so to the user with the actual numbers before registering: "At <tolerance> bps tolerance against a <fee>-bps pool fee and <slippage> bps of agent slippage, this permission will deny every trade — every buy will fail, silently. Set it to at least <fee + slippage + ~150> bps instead." This is a recommendation, not a refusal — the user may keep a tight value knowingly; they must never hit this UNKNOWINGLY. Full reasoning and the formula: sailor-template-swap-no-oracle (the "⚠️ Tolerance vs. pool fee" section).
Position-exit check (accumulate-direction swap, deposit, and borrow rows only) — before presenting the plan as approvable. For every position-opening action, read its exitPath. If it's missing, or managedBy: "none-declined" with no record of the user actually being asked, say so plainly, naming the action: "<action id> accumulates a position with no recorded exit choice — was this discussed at Station 2? 'I'll exit manually' is a complete answer if that's what the user wants; it just needs to be on record." This reconciles what Station 2 should already have captured — it is not a re-interrogation, and it never blocks: if exitPath is present and coherent (agent with a real paired action, or owner/none-declined explicitly recorded), say nothing.
Position-exit sizing rule (managedBy: "agent" rows only). Size the exit leg's cap to the position's projected ceiling, not the entry trade: per-period amount × the accumulation horizon (a stated planning horizon — confirm one with the user if exitCondition is "runs until revoked") × a growth headroom multiplier for anything that isn't a stablecoin pair. For a swap-based exit reusing the entry's SwapPermission/SwapPermissionNoOracle registration, remember maxAmountPerTx is ONE value shared across every pair on that registration (confirmed in the frozen source) — there is no way to give entry and exit different caps on one registration, so size the single shared cap to whichever leg needs more, which is almost always the exit ceiling; this never forces bigger entry trades, a cap only ever bounds a maximum. The honest default, most of the time: size it generously, or effectively unbounded (type(uint256).max) — the price floor (maxSlippageBps/toleranceBps — the price-band tolerance check just above) is the real protection against a bad fill, independent of cap size; a tight exit cap buys no extra safety here, only a forced reconfigure at the worst moment. For a bespoke vault-redeem or repay exit, the same principle applies to whatever cap the bespoke contract encodes (authoring-patterns.md carries the matching authoring gotcha). If the exit is a share-token sweep via WithdrawPermission (see yield.md), say so explicitly: the cap is then share-denominated, and headroom must cover both continued accumulation AND a lower future share price, not just position growth. Disclose the trade-off plainly: an undersized cap's fallback is a configure()/reconfigure — an owner signature required at exactly the moment the user is trying to exit, often urgently; sizing generously upfront avoids that, at no cost to safety since the price floor (or, for vault/borrow exits, the venue's own accounting) is what actually protects the trade.
Simulation routing — every permission, both paths, one standard
Every permission in the plan needs a simulation path decided HERE, at planning time — not discovered mid-flow:
- Template rows → the parametric probe script,
scripts/probe-mandate.mjs (reuse-flow step 5). It derives the lean probe set from the same config blob you configure with. Two rows carry a wrinkle the script already handles: BorrowPermission needs --protocol <aave|morpho|compound>; ApproveAndCallBatchPermission is probed as evaluateBatch batch arrays, not sailor mandate simulate. Detail lives in reuse-flow.md — don't restate it here.
- Bespoke rows → agent-derived probes, following the probe pattern in
sailor-mandates (one bound the contract encodes → one must-fail probe just past it, plus one representative must-pass inside every bound). This is the structurally-only option — a freshly authored contract has no schema for a script to read.
The one standard, on either path: a simulation has passed only when its must-fail probes are PROVEN TO REJECT. A run that only exercises the happy path is not a passed simulation, whether the permission is a template or bespoke — the must-fail proofs are the point (they're what "fail-closed" means in practice, not just in the whitepaper). The exit verifier below checks this, not just that simulate ran.
Ordering rules (enforced here, specified elsewhere)
- Shared singletons: register ≠ configure. Follow
sailor-templates's reuse flow exactly — register, configure, then simulate ONCE (the single safety gate, reuse-flow step 5); a registered-but-unconfigured template denies everything. The exit-verifier below checks that this one simulation passed — it does not call for a second run.
- Bespoke permissions: AGENTS.md invariant 1 — "Deploy → simulate → register. Registration is authorization; nothing is authorized before its bounds are proven, including proven to reject what they must reject."
- A mixed plan composes both orderings independently, per row — a template row's register→configure→simulate and a bespoke row's deploy→simulate→register don't block or reorder each other; sequence each row by its own kind, not by a single mandate-wide order.
- Every plan must answer the approve-coverage question before it is final — not only for swap, for every action that needs one (step 1 above). Read
sailor-mandates/references/approvals.md and pick the execution model as part of the plan, not after it.
Handoff
Once every row in the plan is registered, configured, and simulate-verified, sign the mandate — once, for the whole plan, not per row:
sailor mandate sign
This is the closing act of Station 3, not maintenance: it reviews the full set of permissions now tracked for the SMA (reconciled against on-chain truth), discloses any outstanding registration fee, takes one confirmation from the user, and writes .sail/mandate.json — the file sailor run hard-requires (absent it, run/run --once refuses with "Run sailor mandate sign first"). Run it after the last row's simulation passes, never before — signing reviews what's already registered, it doesn't register anything new on its own (though it will register any tracked-but-unregistered permission it finds, disclosing the fee first).
State the position-exit story, one line per position-opening action, before this signature. Never invent an answer here — this is a summary of what Station 2 recorded and the position-exit check above already reconciled, not a new determination:
<action id>: exit is agent-managed via <exit action id>, capped at <exit cap, human units>.
<action id>: exit is owner-managed — you'll unwind manually via the Safe (sailor-operate's sovereign exit) or the dashboard, whenever you choose.
Exit verifier: every permission in the plan registered, configured, and simulate-verified — passing its must-pass samples AND correctly rejecting its must-fail samples (the standard above — must-fail proven, not just run) — AND the mandate signed (.sail/mandate.json written by sailor mandate sign). Next: Station 4 — the agent-build skill. Dispatch mechanics reference in the interim: sailor-transactions.