Exclusivebeta

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. 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>)
Statusbeta
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

Avoid when

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/motion.js, core/observe.js, components/run-contract/run-contract.js, components/run-contract/run-contract.css.

Usage

Canonical markup — start from it 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).

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.