Exclusive

Run Contract <mv-run-contract>

A run contract for long AI tasks that becomes the live dashboard of the same job: one card, two lives.

How it works

A run contract for long AI tasks that becomes the live dashboard of the same job: one card, two lives. Before: objective, time window drawn as a band on a time axis, adjustable spending cap, done criteria, delegation boundary (allowed ✓ / not allowed ⊘) and open questions to settle before launch; you sign by holding the button while a handwritten signature is inked under your finger, and it retracts if you let go early. After: the card morphs in place (FLIP), a "now" marker advances through the band and stretches it on overrun, the cap becomes a burning fuse that pauses for approval ("Allow +$5" / "Stop"), and checkpoints appear as knots with human-readable summaries of the decisions made and "Resume from here".

CategoryFeedback
TypeWeb Component (<mv-run-contract>)
Statusstable
KitAI you can check
Keywordsexclusive, ai, agent, long-running, slow-ai, contract, consent, budget, cost-cap, eta, checkpoint, breadcrumbs, progress, operator, dashboard, hold-to-confirm, signature, human-in-the-loop

When to use

  • A long AI task needs agreed terms before launch: objective, time window, spending cap and allowed actions
  • The same card should become a live dashboard with elapsed time, budget burn, pauses and checkpoints
  • A run must pause for approval when it hits its budget and allow resuming from a checkpoint

Avoid when

  • The task is short and a simple progress indicator is enough → use Progress instead
  • Many agents run in parallel and need a shared overview → use Agent Lenses instead
  • A plain hold-to-confirm action is needed without contract terms → use Hold Button instead

Install

node scripts/add.mjs run-contract --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["run-contract"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/canvas.js, core/dom.js, core/element.js, core/i18n.js, core/motion.js, core/observe.js, components/run-contract/run-contract.js, components/run-contract/run-contract.css.

Usage

Quick start, the smallest working markup:

<mv-run-contract eta="25-40" budget="12">
  <p slot="objective">Compare 20 product sheets on price, shipping and reviews.</p>
  <ul slot="done"><li>A comparison table across 18 criteria</li></ul>
  <ul slot="allow"><li>Browse public websites</li></ul>
  <ul slot="deny"><li>Send emails</li></ul>
</mv-run-contract>

Canonical markup, to start from and customize with attributes, data-* and CSS variables:

<div id="rc-demo" style="width:min(100%,42rem);margin-inline:auto">
  <style>
    #rc-demo .rc-bar { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; margin-top:.875rem }
    #rc-demo .rc-hint { margin:0; color:var(--mv-fg-muted); font-size:.75rem }
  </style>

  <mv-run-contract id="rc-watch" eta="25-40" budget="12" budget-max="30" extend="5" speed="60">
    <p slot="objective">Competitive watch: 20 product sheets compared on price, shipping and customer reviews.</p>
    <ul slot="done">
      <li>20 product sheets collected from dated public sources</li>
      <li>Comparison table across 18 criteria, prices normalized with tax</li>
      <li>One-page summary with the 3 biggest gaps</li>
    </ul>
    <ul slot="allow">
      <li>Read your files in the “Market watch” folder</li>
      <li>Browse public websites</li>
      <li>Create a results spreadsheet</li>
    </ul>
    <ul slot="deny">
      <li>Send emails</li>
      <li>Create accounts or pay for access</li>
    </ul>
    <p slot="question" data-name="market" data-options="United States, International" data-answer="United States">Which market should we compare?</p>
    <p slot="question" data-name="shipping" data-options="Yes (delivered price), No (list price)">Should prices include shipping?</p>
  </mv-run-contract>

  <div class="rc-bar">
    <p class="rc-hint">Hold <kbd class="mv-kbd">Space</kbd> or the button to sign · accelerated demo: 1 s = 1 min</p>
    <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="rc-replay">Replay demo</button>
  </div>

  <script type="module">
    const rc = document.getElementById("rc-watch");
    await customElements.whenDefined("mv-run-contract");

    // Scripted research agent: breadcrumbs are decisions, not logs.
    const PLAN = [
      { at: 4, label: "Scope set", summary: "20 products kept out of 26 candidates, 6 out-of-range dropped", detail: "Exclusion rule: discontinued products and quote-only listings." },
      { at: 11, label: "Sources vetted", summary: "14 sources kept, 3 dropped (paywalled)", detail: "Subscription comparison sites are outside the delegation: no paid access was created." },
      { at: 20, label: "Sheets extracted", summary: "20 sheets collected, 2 incomplete ones retried", detail: "Two sheets missing shipping costs were completed from the retailers’ terms of sale." },
      { at: 29, label: "Prices normalized", summary: "All prices converted to delivered price with tax, 18 criteria aligned", detail: "Assumption: standard ground shipping within the contiguous United States." },
      { at: 38, label: "Summary drafted", summary: "3 major gaps found, table ready for review", detail: "The draft summary is in “Market watch / 2026-09”." },
    ];
    const END = 43;
    const cost = (m) => (m <= 33 ? m * 0.366 : 12.08 + (m - 33) * 0.2);
    let timer = 0;
    let next = 0;
    let offset = 0; // minutes replayed after a "resume from"
    let extra = 0;  // money already spent before that point

    const tick = () => {
      if (rc.state !== "running") return;
      const m = rc.elapsed;
      const work = m - offset;
      while (next < PLAN.length && work >= PLAN[next].at) rc.checkpoint({ ...PLAN[next++], at: m });
      const done = Math.min(20, Math.floor((work / END) * 20));
      rc.progress({ spent: extra + cost(work), percent: Math.min(0.99, work / END), note: `${done} of 20 sheet${done === 1 ? "" : "s"} compared` });
      if (work >= END) {
        clearInterval(timer);
        rc.complete({ summary: "20 sheets compared across 18 criteria. Report ready: 3 major price gaps, 2 stock-outs to watch." });
      }
    };
    const run = () => { clearInterval(timer); timer = setInterval(tick, 200); };

    rc.addEventListener("mv-start", () => { next = offset = extra = 0; run(); });
    rc.addEventListener("mv-resume-from", (e) => {
      // The agent restarts from the chosen checkpoint: later breadcrumbs are redone from now on.
      const cp = e.detail.checkpoint;
      const i = PLAN.findIndex((p) => p.label === cp.label);
      const workAt = i >= 0 ? PLAN[i].at : 0;
      next = i + 1;
      offset = rc.elapsed - workAt;
      extra = rc.spent - cost(workAt);
      run();
    });
    rc.addEventListener("mv-budget", (e) => console.info("mv-budget", e.detail.type, e.detail.spent.toFixed(2), "/", e.detail.cap));
    document.getElementById("rc-replay").addEventListener("click", () => {
      clearInterval(timer);
      rc.reset();
    });
  </script>
</div>

API

Attributes

NameTypeDefaultDescription
etastringTime window in minutes, e.g. "25-40" (read on mount when the terms property is not used).
budgetnumber10Initial spending cap.
budget-maxnumbercap × 2.5Upper bound of the cap slider.
extendnumber5Amount offered by "Allow +…" when the cap is reached.
currencystringUSDISO 4217 currency (en-US formatting).
speednumber1Simulated minutes per real minute for the internal clock (60: one second = one minute, handy for demos). Irrelevant when the host passes elapsed to progress().
holdnumber (ms)1200How long the button must be held to sign.
warnnumber (0-1)0.8Budget warning threshold (fraction of the cap).
data-state / data-phasereflectedCurrent state (contract | running | paused | complete | failed | stopped) and phase (contract | run), for host styling.

Properties

NameTypeDescription
terms{ objective, eta: { min, max }, budget: { cap, max, min, step, extend, currency, editable }, done: string[], allow: string[], deny: string[], questions: { id, label, options?, answer? }[] }Contract terms (replaces the markup). Reading returns a copy, including a cap the user may have changed. Can be set before define().
statestringCurrent state (read-only).
answers{ [id]: string }Answers to the clarification questions (read-only).
elapsednumber (min)Running time elapsed, pauses excluded (read-only).
spent / checkpointsnumber / object[]Current spend and a copy of the checkpoints (read-only).
stringsPartial<Record<string, string | { one, other }>>Overrides for the default text (en-US); a key with a count ({n}, {m} or {h}) can be a { one, other } plural form. Status: statusContract, statusRunning, statusPaused, statusComplete, statusFailed, statusStopped, kickerContract, kickerRun, pause, resume. Durations: durationUnderMinute, durationMinutes ({m}), durationHoursMinutes ({h}, {m}), durationHours ({h}), justNow, ago ({time}). Terms: untitled (objective fallback), rowEta, rowCap, rowBudget, rowProgress, rowDone, rowScope, allowed, notAllowed, etaRange ({min}, {max}), etaAlert, capLabel (slider label), capNote ({pct}), questionsTitle, reason (plural {n}). Signing: sign, signed, signHint, signNote, sealSigned ({date}, {time}), sealDone (plural {n}), sealAllowed ({n}), sealDenied ({n}). Tracker: lastCheckpoint ({ago}), noCheckpoint, etaDone / etaInterrupted ({time}), etaStarting ({min}, {max}), etaLeft ({elapsed}, {left}), etaRangeLeft ({elapsed}, {min}, {max}), etaOver ({time}), etaWithin, spentOf ({cap}), used ({pct}), capOf ({cap}), fuseLabel, fuseValue ({spent}, {cap}, {pct}), capReached, pastThreshold ({pct}), progressLabel, percent ({pct}), progressValue ({pct}, {note}), checkpoints, noCheckpoints, checkpointLabel ({n}: default checkpoint label), discarded, savedAt ({time}, {spent}), resumeFromHere, knotTitle ({label}, {summary}). Alert: alertCapTitle ({cap}), alertCapText ({spent}), allowMore ({amount}), stop, alertPaused, alertWaiting, alertDone ({time}, {spent}), alertDoneText, alertFailed, resumeLast, alertStopped, alertStoppedText ({reason}, {spent}). Default reasons: pausedByApi, pausedByUser, pausedByCap ({cap}), stoppedByUser, unknownError. Announcements: announceStart ({min}, {max}), announceCheckpoint ({label}, {summary}), announceResumed, announceCapRaised ({cap}), announceDone ({time}, {spent}, {summary}), announceFailed ({message}), announceStopped ({spent}), announceOver ({elapsed}, {min}, {max}), announceResumeFrom ({label}), announcePausedCap ({cap}), announcePaused ({reason}), announceWarning ({pct}, {spent}, {cap}), announceReady. Money and dates keep en-US formatting. App-wide translations: setStrings() from core/i18n.js, under the key "run-contract". Can be set before the element is defined.

Methods

NameDescription
sign()Signs like a completed hold: refused (false) while a question is open; emits mv-sign (cancelable), then calls start().
start()Turns the contract into the tracker and starts the clock (no signature when called directly).
checkpoint({ label, summary, detail?, at? })Adds a checkpoint: a knot on the axis plus a conceptual breadcrumb. at in minutes (default: now).
progress({ spent?, percent?, note?, elapsed? })Updates spend, progress (0-1 or 0-100), a short note ("9 of 20 sheets") and, optionally, the real elapsed time. Crossing the warning threshold or the cap triggers the warning or the pause.
pause(reason) / resume()Host-driven pause (clock stopped) and resume.
extendBudget(amount?)Raises the cap (default: extend) and resumes if the cap had paused the run.
complete(result) / fail(error) / stop(reason?)Ends the run: success (result.summary), failure (error.message, can resume from the last checkpoint) or stop.
reset()Back to the unsigned contract with the original terms and answers (reverse morph).

Events

NameDescription
mv-signContract signed, cancelable. detail: { terms, answers, signedAt }.
mv-startTracker started. detail: { terms, answers, signedAt }.
mv-checkpointCheckpoint added. detail: { index, label, summary, detail, at, spent }.
mv-budgetBudget threshold. detail: { type: "warning" | "cap" | "extend", threshold?, amount?, spent, cap }. On "cap" the run is already paused: the host must suspend the agent.
mv-pause / mv-resume / mv-stopRequested by the user (or by the cap for mv-resume, source: "budget"): the host must act on the agent. Method calls do not emit these events.
mv-resume-from"Resume from here": detail { index, checkpoint }, cancelable. Otherwise later checkpoints are marked discarded and the run restarts.
mv-completeDone. detail: { result, elapsed, spent, checkpoints }.

Content structure

NameDescription
objectiveElement whose text is the objective (one sentence).
done<ul>/<ol> of done criteria (or one element per criterion).
allow / deny<ul> of actions the agent may / may not take.
questionA question to settle before signing; data-options="A, B" for choices (otherwise a text field), data-name for the id, data-answer for a default answer.

CSS classes

NameDescription
mv-run-contract-cardThe generated card (an article labelled by the objective).
mv-run-contract-axis / -band / -over / -nowTime axis: estimate band, hatched extension on overrun, "now" marker.
mv-run-contract-fuse / -emberBudget fuse (role=meter) and its ember.
mv-run-contract-sign / -inkHold-to-sign field and the handwritten stroke.
mv-run-contract-crumbs / -crumbCheckpoint list (conceptual breadcrumbs).
mv-run-contract-alertState panel: paused for approval, done, failed, stopped.

CSS variables

NameDefaultDescription
--mv-run-contract-accentvar(--mv-accent)Action color (slider, latest checkpoint, primary button).
--mv-run-contract-bandvar(--mv-accent)Tint of the estimate band.
--mv-run-contract-emberwarning / danger mixEmber and spark color (artistic effect).
--mv-run-contract-ink-colorvar(--mv-accent-fg)Signature ink.
--mv-run-contract-bgvar(--mv-surface)Card background.

Accessibility

Terms are a real description list (dt/dd); allowed and not-allowed actions are two labelled lists where every line has its icon and the group title carries the label (never color alone). Questions are fieldset/legend groups of native radios. The sign button is held with a pointer or Space/Enter; aria-describedby explains the hold and, while a question is open, why it is blocked (aria-disabled: it stays focusable and says why). A plain click never signs. While running, the budget is a role=meter and progress a role=progressbar, both with plain-language aria-valuetext; the time text gives elapsed time, remaining estimate and overrun (icon + label); checkpoints are a list of aria-expanded buttons revealing details and "Resume from here" (the axis knots are decorative duplicates). A polite live region announces signing, checkpoints, budget thresholds, time overrun, pauses, completion and failure. After signing, focus moves to the Pause button. Reduced motion: no animated morph, no sparks or flicker, markers step instead of gliding; the signature stays because it is the hold gauge. A single frameLoop is suspended offscreen, in hidden tabs and when idle; nothing keeps running once the element is removed. Forced colors: slider, option chips (checked in Highlight), fuse, progress bar and knots use system colors. Known limits: the internal clock stops while the run is paused; the slotted terms (objective, lists, questions) are read once on mount, later changes go through the terms property; "Resume from here" neither rewinds the clock nor refunds spending: the host restarts the agent from that checkpoint.