Skip to content
TuffTasks, docs and chat for teams and AI agents

Interactive Answers

Clause Playbook

An agent's negotiation playbook for one clause — market spread as dot columns, walk-away, fallback and preferred zones on a two-handle track, and the settings it acts on.

1 variantMIT

Preview

§ 9.2Liability cap

Negotiation playbook · Corvane MSA

No edits

Comparing your playbook with 2,140 signed MSAs…

Market1 dot ≈ 9 MSAs
p45 of market
Liability cap thresholds
  • Walk-away<6
  • Fallback6–12
  • Preferred12+
Data-breach super-capMultiple of the general cap
2×
Escalate toWho the agent pings before it concedes

Live in the playbook

Installation

Copies the component into components/ui and installs its dependencies and theme tokens.

pnpm dlx shadcn@latest add https://tuffui.com/r/clause-playbook.json

Usage

Pass your own data and wire the callbacks to your agent runtime.

import {
  ClausePlaybook,
  marketPercentile,
  type ClauseThresholds,
} from "@/components/ui/clause-playbook";

export function LiabilityCapAnswer({ proposal }: { proposal: AgentProposal }) {
  const playbook = usePlaybook("liability-cap");
  const [thresholds, setThresholds] = useState<ClauseThresholds>(proposal.thresholds);
  const [values, setValues] = useState(proposal.values); // { escalate: "legal" }

  return (
    <ClausePlaybook
      number="9.2"
      clause="Liability cap"
      unit="mo"
      domain={[0, 24]}
      // Signed contracts bucketed by cap: [{ value: 12, count: 260 }, …]
      distribution={playbook.market}
      // The live playbook is the baseline; the agent's proposal counts as edits.
      defaultThresholds={playbook.thresholds}
      thresholds={thresholds}
      onThresholdsChange={setThresholds}
      settings={[
        { kind: "stepper", id: "superCap", label: "Data-breach super-cap",
          defaultValue: playbook.superCap, min: 1, max: 5, format: (v) => `${v}×` },
        { kind: "select", id: "escalate", label: "Escalate to", defaultValue: playbook.escalate,
          options: [
            { value: "legal", label: "Legal", hint: "always" },
            { value: "finance", label: "Finance", hint: `below ${thresholds.preferred} mo` },
          ] },
        { kind: "toggle", id: "autoAccept", label: "Auto-accept within fallback",
          description: (t) => `Agent signs off ${t.fallback}–${t.preferred} mo`,
          defaultValue: playbook.autoAccept },
      ]}
      values={values}
      onValuesChange={setValues}
      note={`Agent suggests ${thresholds.preferred} mo · p${marketPercentile(playbook.market, thresholds.preferred)} of market`}
      sampleNoun="MSAs"
      onApply={(t, v) => savePlaybook("liability-cap", { thresholds: t, ...v })}
      onDiscard={() => proposal.dismiss()}
    />
  );
}

Props

PropTypeDefaultDescription
clause*string—Clause name in the header, e.g. "Liability cap". Also names the handles for screen readers.
numberstring—Clause number shown as "§ 9.2" before the name.
subtitleReactNode—Line under the title, e.g. the playbook and the contract.
iconIconScalesPhosphor icon in the header tile.
distribution*ClauseBucket[]—Market data, one bucket per value. Each bucket is a stack of dots; when stacks get tall one dot stands for several contracts, noted above the chart. Dots below the fallback handle are dim, the fallback zone neutral, the preferred zone accent.
domain[number, number][0, 24]Ends of the scale.
stepnumber1Handles snap to this step. Arrow keys move one step.
largeStepnumberan eighth of the domainShift+arrow and Page Up / Page Down move this far. Home and End jump to the ends.
ticksnumber[]five even ticksAxis values. A tick next to a handle gives way to the handle's own value, which is emphasized.
unitstring—Unit after values, e.g. "mo". Shown on the last tick and in screen-reader text.
format(value: number) => string—Formats a value without the unit.
thresholdsClauseThresholds—Controlled handles. Below `fallback` is walk-away, from `preferred` up is preferred. Changes made from outside, like an agent's proposal, glide into place.
defaultThresholdsClauseThresholds—Starting handles when uncontrolled, and the baseline edits are counted against. Pass the live playbook here and the proposal as `thresholds`.
onThresholdsChange(thresholds: ClauseThresholds) => void—Called while a handle is dragged or moved with the keyboard.
minGapnumberstepSmallest distance between the two handles.
zoneLabels{ walkAway?; fallback?; preferred? }"Walk-away", "Fallback", "Preferred"Names of the three zones in the legend and on the handles.
settingsClauseSetting[][]Rows under the chart: a stepper, a select with hints, or a toggle. A row's `defaultValue` is its baseline for the edit count.
valuesRecord<string, ClauseValue>—Controlled setting values by id. Ids left out use their `defaultValue`.
onValuesChange(values, changedId?) => void—Called with every setting value when one changes. `changedId` is empty on Discard.
noteReactNode—The agent's line above the chart, e.g. what it suggests and why. A new string slides in over the old one and is announced.
sampleNounstring"contracts"What the market counts, plural, for "1 dot ≈ 22 MSAs".
onApply(thresholds, values) => void—Shows Apply, enabled while there are edits. Current values become the new baseline, so the count goes back to zero.
onDiscard() => void—Shows Discard. Handles and settings go back to the baseline first.
classNamestring—Extra classes for the root element.
type ClauseThresholds = {
  fallback: number;   // walk-away below this
  preferred: number;  // preferred from this up
};

type ClauseBucket = { value: number; count: number };

type ClauseValue = number | string | boolean;

type ClauseSetting = {
  id: string;
  label: string;
  description?: ReactNode | ((thresholds: ClauseThresholds) => ReactNode);
} & (
  | { kind: "stepper"; defaultValue: number; min?: number; max?: number;
      step?: number; format?: (value: number) => string }
  | { kind: "select"; defaultValue: string; options: ClauseSelectOption[] }
  | { kind: "toggle"; defaultValue: boolean }
);

type ClauseSelectOption = { value: string; label: string; hint?: string };

// Share of the market below a value, ties counted half: 49 → "p49".
function marketPercentile(distribution: ClauseBucket[], value: number): number;