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".
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
Name
Type
Default
Description
eta
string
Time window in minutes, e.g. "25-40" (read on mount when the terms property is not used).
budget
number
10
Initial spending cap.
budget-max
number
cap × 2.5
Upper bound of the cap slider.
extend
number
5
Amount offered by "Allow +…" when the cap is reached.
currency
string
USD
ISO 4217 currency (en-US formatting).
speed
number
1
Simulated minutes per real minute for the internal clock (60: one second = one minute, handy for demos). Irrelevant when the host passes elapsed to progress().
hold
number (ms)
1200
How long the button must be held to sign.
warn
number (0–1)
0.8
Budget warning threshold (fraction of the cap).
data-state / data-phase
reflected
Current state (contract | running | paused | complete | failed | stopped) and phase (contract | run), for host styling.
Contract terms (replaces the markup). Reading returns a copy, including a cap the user may have changed. Can be set before define().
state
string
Current state (read-only).
answers
{ [id]: string }
Answers to the clarification questions (read-only).
elapsed
number (min)
Running time elapsed, pauses excluded (read-only).
spent / checkpoints
number / object[]
Current spend and a copy of the checkpoints (read-only).
Methods
Name
Description
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).
Budget 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-stop
Requested 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.
Element 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.
question
A 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
Name
Description
mv-run-contract-card
The generated card (an article labelled by the objective).
mv-run-contract-axis / -band / -over / -now
Time axis: estimate band, hatched extension on overrun, "now" marker.
mv-run-contract-fuse / -ember
Budget fuse (role=meter) and its ember.
mv-run-contract-sign / -ink
Hold-to-sign field and the handwritten stroke.
mv-run-contract-crumbs / -crumb
Checkpoint list (conceptual breadcrumbs).
mv-run-contract-alert
State panel: paused for approval, done, failed, stopped.
CSS variables
Name
Default
Description
--mv-run-contract-accent
var(--mv-accent)
Action color (slider, latest checkpoint, primary button).
--mv-run-contract-band
var(--mv-accent)
Tint of the estimate band.
--mv-run-contract-ember
warning / danger mix
Ember and spark color (artistic effect).
--mv-run-contract-ink-color
var(--mv-accent-fg)
Signature ink.
--mv-run-contract-bg
var(--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.