[1dae04a7] Video: release 0.25.0 (revision) (#536)

* fix(release): CI wait polls the prod rung; escape the header tooltip apostrophe

get_latest_ci_conclusion defaults to the ladder's head rung, so
wait_for_ci searched slave for a release commit that lives on master
and timed out after 40 minutes with the run already green. The wait
now passes the prod branch explicitly. Also fixes the
react/no-unescaped-entities error that turned master's Panel CI red.

* fix(panel,video): dead dialog triggers behind tooltips; dotted composition ids render

HelpTip nested inside a Dialog/AlertDialog trigger puts the trigger's
click handler on the Tooltip root, which renders no DOM — the agents
Spawn item and the KB Reindex-All / Delete-index confirms were dead.
Tooltips now wrap the triggers. The video renderer accepts interior
single dots in composition ids (release-0.25.0) with '..' still
unrepresentable, and propose_video refuses an unrenderable id at
authoring time.

* fix(dispatch): restart-safe PM review turns

A leaf task in awaiting_pm_review had no periodic pickup: the closure
dispatcher bailed on childless tasks and skipped PR-bearing review
tasks as already-promoted, assuming the submit-time PM session was
still alive — an assumption every restart breaks. Proven live on the
docs-sync leaf after the 0.25.0 redeploy, which also dependency-blocked
its sibling dev task. Childless awaiting_pm_review tasks now flow to
the PM's review turn, and the merge turn respawns its PM when none is
active.

* [1dae04a7] Revise release-0.25.0 composition to 40s scene-based pacing with four feature cards

* [1dae04a7] docs(motion): update release-0.25.0 README section for 40s four-card revision

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>
This commit is contained in:
Renzo F
2026-07-16 17:15:12 +02:00
committed by GitHub
co-authored by Renn F UX/UI Developer 1 UX/UI Documenter
parent 63212899ca
commit 797847e379
16 changed files with 246 additions and 89 deletions
+11 -4
View File
@@ -76,7 +76,14 @@ The smoke test (`release-recap.test.js`) asserts this schema, checks the counts,
`compositions/release-0.25.0/` is a panel-demo kit clip for the RoboCo v0.25.0 release. It builds on the `kit/` register instead of the text-card style, so it has no `theme.css` of its own.
The story is "governance gets a better UI": the CEO types "Governance gets a better UI" into the panel intake, then three shipped feature cards — Env ladder, Collision map, Metrics donut — enter a kanban column and flip from `in progress` to `completed`. A cursor clicks through the cards, a stats overlay flashes "1 release / 25 agents / 1 human", and a toast confirms "v0.25.0 shipped / I approved once. 25 agents did the rest." The clip closes on the "roboco.tech" outro.
The current revision runs **40 seconds** total (up from an earlier 14s cut) so every feature card is fully visible before the next one enters. The story is still "governance gets a better UI": the CEO types "Governance gets a better UI" into the panel intake at 3.6s, then four shipped feature cards enter the kanban column one per scene and flip from `in progress` to `completed`:
1. **Env ladder** — enters at 5.0s, completes at 6.6s ("Dev to prod, one rung at a time.").
2. **Collision map** — enters at 10.0s, completes at 11.6s ("See who touched what before you review.").
3. **Metrics donut** — enters at 15.0s, completes at 16.6s ("90 days of real task flow.").
4. **Notification bell** — enters at 20.0s, completes at 21.6s ("Real read/ack actions on every alert.").
Each card gets roughly five seconds of fully visible time before the next card enters. A cursor clicks the column at 22.0s, the stats overlay shows "1 release / 25 agents / 1 human" from 24.0s to 32.0s, the toast "v0.25.0 shipped / I approved once. 25 agents did the rest." runs from 30.0s to 38.0s, and the "roboco.tech" outro lands at 36.0s and holds through the end.
The composition reuses the same `pk-frame` chrome, `pk-column`/`pk-card`, `pk-pill`, `pk-cursor`, `pk-toast`, and `pk-outro` pieces from `kit/`, plus the typing reveal wired through `props.js`. Each feature card uses the `pk-pill--swap-out` / `pk-pill--swap-in` pattern from `panel-demo` and `release-recap` to replace the `in progress` pill with `completed` on the same beat.
@@ -101,14 +108,14 @@ pnpm test # release-0.25.0.test.js is picked up by vitest
### `captions.json`
Same schema as `release-recap`: one `captions.json` next to the HTML with self-verified X and TikTok captions:
Same schema as `release-recap`: one `captions.json` next to the HTML with self-verified X and TikTok captions. The X caption was updated to include the Notification bell feature and now totals **216 characters**:
```json
{
"composition_id": "release-0.25.0",
"occasion": "release: RoboCo v0.25.0",
"platforms": {
"x": { "caption": "...", "char_count": 170, "limit": 280, "within_limit": true },
"x": { "caption": "...", "char_count": 216, "limit": 280, "within_limit": true },
"tiktok": { "caption": "...", "char_count": 347, "limit": 2200, "within_limit": true }
}
}
@@ -116,4 +123,4 @@ Same schema as `release-recap`: one `captions.json` next to the HTML with self-v
### Smoke-test invariants
`release-0.25.0.test.js` extends the panel-demo register checks: both `vertical.html` (1080×1920) and `square.html` (1080×1080) parse with the HyperFrames params, the kit CSS/JS wiring is present, three feature cards each carry a progress-to-completed pill swap, the cursor and toast appear, the outro shows "roboco.tech", no external scripts are loaded, and no em dashes slip into on-screen copy or captions.
`release-0.25.0.test.js` extends the panel-demo register checks: both `vertical.html` (1080×1920) and `square.html` (1080×1080) parse with `data-duration="40"` and the HyperFrames params, the kit CSS/JS wiring is present, **four** feature cards each carry a progress-to-completed pill swap and include the "Notification bell" text, the cursor and toast appear, the outro shows "roboco.tech", no external scripts are loaded, and no em dashes slip into on-screen copy or captions.
@@ -3,8 +3,8 @@
"occasion": "release: RoboCo v0.25.0",
"platforms": {
"x": {
"caption": "RoboCo v0.25.0 is live. Env ladder from dev to prod. Collision map in every review. Metrics with a 90-day window. One human approved it; 25 agents shipped it. roboco.tech",
"char_count": 170,
"caption": "RoboCo v0.25.0 is live. Env ladder from dev to prod. Collision map in every review. Metrics with a 90-day window. Notification bell with real read/ack actions. One human approved it; 25 agents shipped it. roboco.tech",
"char_count": 216,
"limit": 280,
"within_limit": true
},
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
// HTML-structure smoke test for the RoboCo v0.25.0 release composition.
// Extends the panel-demo register: panel chrome, typing reveal, feature cards
// Extends the panel-demo register: panel chrome, typing reveal, four feature cards
// with progress -> completed pill flips, cursor, stats overlay, toast,
// outro, offline constraint, and the captions.json schema/limits.
@@ -34,7 +34,7 @@ describe("release-0.25.0 composition", () => {
expect(html).toContain('data-width="1080"');
expect(html).toContain(`data-height="${height}"`);
expect(html).toMatch(/data-duration="14"/);
expect(html).toMatch(/data-duration="40"/);
expect(html).toMatch(/data-fps="30"/);
const propsIdx = html.indexOf('<script src="props.js"></script>');
@@ -58,14 +58,15 @@ describe("release-0.25.0 composition", () => {
const cardCount = (html.match(/pk-card clip/g) || []).length;
const progressCount = (html.match(/pk-pill--progress/g) || []).length;
const completedCount = (html.match(/pk-pill--completed/g) || []).length;
expect(cardCount).toBe(3);
expect(progressCount).toBe(3);
expect(completedCount).toBe(3);
expect(cardCount).toBe(4);
expect(progressCount).toBe(4);
expect(completedCount).toBe(4);
expect(html).toContain("v0.25.0");
expect(html).toContain("Env ladder");
expect(html).toContain("Collision map");
expect(html).toContain("Metrics donut");
expect(html).toContain("Notification bell");
expect(html).toContain("pk-cursor");
expect(html).toContain("pk-cursor__ring");
+38 -26
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" data-composition-id="release-0.25.0" data-composition-duration="14" data-fps="30">
<html lang="en" data-composition-id="release-0.25.0" data-composition-duration="40" data-fps="30">
<head>
<meta charset="utf-8" />
<title>RoboCo v0.25.0 Release - square (1080×1080)</title>
@@ -8,7 +8,7 @@
<script src="../../kit/kit.js"></script>
<style>
/* Square cut of the v0.25.0 release clip. Tighter spacing, smaller hero,
and the stats overlay covers the full frame for readability. */
and the stats overlay covers the full frame for readability. Total 40s. */
.r25-hero {
position: absolute;
@@ -128,7 +128,7 @@
z-index: 20;
font-family: var(--pk-font-display);
opacity: 0;
animation: r25-stats-in 0.4s var(--pk-ease) 10.8s forwards, r25-stats-out 0.5s var(--pk-ease) 12.2s forwards;
animation: r25-stats-in 0.4s var(--pk-ease) 24.0s forwards, r25-stats-out 0.5s var(--pk-ease) 32.0s forwards;
}
@keyframes r25-stats-in { to { opacity: 1; } }
.r25-stats__line {
@@ -161,9 +161,9 @@
</style>
</head>
<body>
<div id="stage" data-composition-id="release-0.25.0" data-width="1080" data-height="1080" data-start="0" data-duration="14" data-fps="30">
<div id="stage" data-composition-id="release-0.25.0" data-width="1080" data-height="1080" data-start="0" data-duration="40" data-fps="30">
<div class="r25-hero clip" data-start="0" data-duration="3.2" data-track-index="13">
<div class="r25-hero clip" data-start="0" data-duration="3.2" data-track-index="16">
<div class="r25-hero__markwrap">
<img class="r25-hero__mark" src="../../public/roboco-logo-darkmode.png" alt="RoboCo" />
<div class="r25-hero__ring"></div>
@@ -172,7 +172,7 @@
<div class="r25-hero__tag">governance gets a better UI</div>
</div>
<div class="pk-frame clip" data-start="2.6" data-duration="11.4" data-track-index="1">
<div class="pk-frame clip" data-start="2.6" data-duration="37.4" data-track-index="1">
<aside class="pk-frame__sidebar">
<div class="r25-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
<div class="r25-nav">Overview</div>
@@ -188,7 +188,7 @@
<header class="pk-frame__topbar">
<div class="pk-frame__search">Search tasks, agents...</div>
<div class="r25-user">
<div class="pk-frame__status clip" data-start="2.6" data-duration="11.4" data-track-index="0">
<div class="pk-frame__status clip" data-start="2.6" data-duration="37.4" data-track-index="0">
<span class="pk-frame__statusdot"></span>
<span>Live</span>
</div>
@@ -197,7 +197,7 @@
</header>
<div class="pk-frame__content">
<div class="pk-intake clip" data-start="3.6" data-duration="10.4" data-track-index="2" style="animation-delay: 3.6s;">
<div class="pk-intake clip" data-start="3.6" data-duration="36.4" data-track-index="2" style="animation-delay: 3.6s;">
<div class="pk-intake__label">New task</div>
<div class="pk-intake__field"><span id="typeIntro">Governance gets a better UI</span><span class="pk-caret"></span></div>
</div>
@@ -205,55 +205,67 @@
<div class="pk-column">
<div class="pk-column__header">Shipped in v0.25.0</div>
<div class="pk-card clip" data-start="5.2" data-duration="8.8" data-track-index="3" style="animation-delay: 5.2s;">
<div class="pk-card clip" data-start="5.0" data-duration="35" data-track-index="3" style="animation-delay: 5.0s;">
<div class="pk-card__title">Env ladder</div>
<div class="r25-sub">Dev to prod, one rung at a time.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">project settings</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="5.2" data-duration="1.6" data-track-index="4" style="--pk-pill-enter-delay: 5.2s; --pk-pill-exit-delay: 6.6s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="6.8" data-duration="7.2" data-track-index="5" style="--pk-pill-enter-delay: 6.8s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="5.0" data-duration="1.6" data-track-index="4" style="--pk-pill-enter-delay: 5.0s; --pk-pill-exit-delay: 6.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="6.6" data-duration="33.4" data-track-index="5" style="--pk-pill-enter-delay: 6.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="7" data-duration="7" data-track-index="6" style="animation-delay: 7s;">
<div class="pk-card clip" data-start="10.0" data-duration="30" data-track-index="6" style="animation-delay: 10.0s;">
<div class="pk-card__title">Collision map</div>
<div class="r25-sub">See who touched what before you review.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">task detail</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="7" data-duration="1.6" data-track-index="7" style="--pk-pill-enter-delay: 7s; --pk-pill-exit-delay: 8.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="8.6" data-duration="5.4" data-track-index="8" style="--pk-pill-enter-delay: 8.6s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="10.0" data-duration="1.6" data-track-index="7" style="--pk-pill-enter-delay: 10.0s; --pk-pill-exit-delay: 11.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="11.6" data-duration="28.4" data-track-index="8" style="--pk-pill-enter-delay: 11.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="8.8" data-duration="5.2" data-track-index="9" style="animation-delay: 8.8s;">
<div class="pk-card clip" data-start="15.0" data-duration="25" data-track-index="9" style="animation-delay: 15.0s;">
<div class="pk-card__title">Metrics donut</div>
<div class="r25-sub">90 days of real task flow.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">performance tab</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="8.8" data-duration="1.6" data-track-index="10" style="--pk-pill-enter-delay: 8.8s; --pk-pill-exit-delay: 10.2s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="10.4" data-duration="3.6" data-track-index="11" style="--pk-pill-enter-delay: 10.4s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="15.0" data-duration="1.6" data-track-index="10" style="--pk-pill-enter-delay: 15.0s; --pk-pill-exit-delay: 16.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="16.6" data-duration="23.4" data-track-index="11" style="--pk-pill-enter-delay: 16.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="20.0" data-duration="20" data-track-index="12" style="animation-delay: 20.0s;">
<div class="pk-card__title">Notification bell</div>
<div class="r25-sub">Real read/ack actions on every alert.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">notifications</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="20.0" data-duration="1.6" data-track-index="13" style="--pk-pill-enter-delay: 20.0s; --pk-pill-exit-delay: 21.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="21.6" data-duration="18.4" data-track-index="14" style="--pk-pill-enter-delay: 21.6s;">completed</span>
</div>
</div>
</div>
<div class="pk-cursor clip" data-start="9.4" data-duration="4.6" data-track-index="12" style="--pk-cursor-delay: 9.4s; --pk-cursor-x0: 700px; --pk-cursor-y0: 300px; --pk-cursor-x1: 200px; --pk-cursor-y1: 1110px;">
<div class="pk-cursor clip" data-start="22.0" data-duration="6" data-track-index="15" style="--pk-cursor-delay: 22.0s; --pk-cursor-x0: 700px; --pk-cursor-y0: 300px; --pk-cursor-x1: 200px; --pk-cursor-y1: 700px;">
<div class="pk-cursor__glyph"></div>
<div class="pk-cursor__ring" style="--pk-click-delay: 10.4s;"></div>
<div class="pk-cursor__ring" style="--pk-click-delay: 23.4s;"></div>
</div>
<div class="r25-stats clip" data-start="10.8" data-duration="3.2" data-track-index="14">
<div class="r25-stats__line" style="animation-delay: 10.8s;">1 release</div>
<div class="r25-stats__line r25-stats__line--accent" style="animation-delay: 11.1s;">25 agents</div>
<div class="r25-stats__line" style="animation-delay: 11.4s;">1 human</div>
<div class="r25-stats clip" data-start="24.0" data-duration="8" data-track-index="17">
<div class="r25-stats__line" style="animation-delay: 24.0s;">1 release</div>
<div class="r25-stats__line r25-stats__line--accent" style="animation-delay: 24.3s;">25 agents</div>
<div class="r25-stats__line" style="animation-delay: 24.6s;">1 human</div>
</div>
<div class="pk-toast clip" data-start="11.8" data-duration="2.2" data-track-index="15" style="animation-delay: 11.8s;">
<div class="pk-toast clip" data-start="30.0" data-duration="8" data-track-index="18" style="animation-delay: 30.0s;">
<div class="pk-toast__icon"></div>
<div class="pk-toast__body">
<div class="pk-toast__title" id="toastTitle">v0.25.0 shipped</div>
@@ -261,7 +273,7 @@
</div>
</div>
<div class="pk-outro clip" data-start="12.6" data-duration="1.4" data-track-index="16" style="animation-delay: 12.6s;">
<div class="pk-outro clip" data-start="36.0" data-duration="4" data-track-index="19" style="animation-delay: 36.0s;">
<span class="r25-outro__logo"><img src="../../public/roboco-logo.png" alt="" /></span>
<span>roboco.tech</span>
</div>
@@ -273,7 +285,7 @@
window.__timelines = window.__timelines || {};
window.__timelines["release-0.25.0"] = {
id: "release-0.25.0",
duration: 14,
duration: 40,
fps: 30,
animations: []
};
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" data-composition-id="release-0.25.0" data-composition-duration="14" data-fps="30">
<html lang="en" data-composition-id="release-0.25.0" data-composition-duration="40" data-fps="30">
<head>
<meta charset="utf-8" />
<title>RoboCo v0.25.0 Release - vertical (1080×1920)</title>
@@ -8,8 +8,8 @@
<script src="../../kit/kit.js"></script>
<style>
/* v0.25.0 release clip - panel-demo register. Identity cold open,
panel assembles, three feature cards flip to completed, cursor click,
receipt stats, toast, lockup. */
panel assembles, four feature cards flip to completed one per scene,
cursor click, receipt stats, toast, lockup. Total 40s. */
/* ---- Identity cold open -------------------------------------------- */
.r25-hero {
@@ -149,7 +149,7 @@
animation: pk-enter-rise-scale 0.55s var(--pk-ease) forwards;
}
.r25-stats__line--accent { color: var(--pk-accent); }
.r25-stats { animation: r25-stats-out 0.5s var(--pk-ease) 11.6s forwards; }
.r25-stats { animation: r25-stats-out 0.5s var(--pk-ease) 32.0s forwards; }
@keyframes r25-stats-out { to { opacity: 0; } }
.pk-toast { --pk-toast-bottom: 140px; }
@@ -177,10 +177,10 @@
</style>
</head>
<body>
<div id="stage" data-composition-id="release-0.25.0" data-width="1080" data-height="1920" data-start="0" data-duration="14" data-fps="30">
<div id="stage" data-composition-id="release-0.25.0" data-width="1080" data-height="1920" data-start="0" data-duration="40" data-fps="30">
<!-- Beat 1: identity cold open. -->
<div class="r25-hero clip" data-start="0" data-duration="3.2" data-track-index="13">
<div class="r25-hero clip" data-start="0" data-duration="3.2" data-track-index="16">
<div class="r25-hero__markwrap">
<img class="r25-hero__mark" src="../../public/roboco-logo-darkmode.png" alt="RoboCo" />
<div class="r25-hero__ring"></div>
@@ -190,7 +190,7 @@
</div>
<!-- Beat 2: the panel assembles. -->
<div class="pk-frame clip" data-start="2.6" data-duration="11.4" data-track-index="1">
<div class="pk-frame clip" data-start="2.6" data-duration="37.4" data-track-index="1">
<aside class="pk-frame__sidebar">
<div class="r25-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
<div class="r25-nav">Overview</div>
@@ -206,7 +206,7 @@
<header class="pk-frame__topbar">
<div class="pk-frame__search">Search tasks, agents...</div>
<div class="r25-user">
<div class="pk-frame__status clip" data-start="2.6" data-duration="11.4" data-track-index="0">
<div class="pk-frame__status clip" data-start="2.6" data-duration="37.4" data-track-index="0">
<span class="pk-frame__statusdot"></span>
<span>Live</span>
</div>
@@ -216,67 +216,79 @@
<div class="pk-frame__content">
<!-- Beat 3: the hook types into intake. -->
<div class="pk-intake clip" data-start="3.6" data-duration="10.4" data-track-index="2" style="animation-delay: 3.6s;">
<div class="pk-intake clip" data-start="3.6" data-duration="36.4" data-track-index="2" style="animation-delay: 3.6s;">
<div class="pk-intake__label">New task</div>
<div class="pk-intake__field"><span id="typeIntro">Governance gets a better UI</span><span class="pk-caret"></span></div>
</div>
<!-- Beat 4: three feature cards stack and complete. -->
<!-- Beat 4: four feature cards, one per ~5s scene, each fully visible before the next enters. -->
<div class="pk-column">
<div class="pk-column__header">Shipped in v0.25.0</div>
<div class="pk-card clip" data-start="5.2" data-duration="8.8" data-track-index="3" style="animation-delay: 5.2s;">
<div class="pk-card clip" data-start="5.0" data-duration="35" data-track-index="3" style="animation-delay: 5.0s;">
<div class="pk-card__title">Env ladder</div>
<div class="r25-sub">Dev to prod, one rung at a time.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">project settings</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="5.2" data-duration="1.6" data-track-index="4" style="--pk-pill-enter-delay: 5.2s; --pk-pill-exit-delay: 6.6s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="6.8" data-duration="7.2" data-track-index="5" style="--pk-pill-enter-delay: 6.8s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="5.0" data-duration="1.6" data-track-index="4" style="--pk-pill-enter-delay: 5.0s; --pk-pill-exit-delay: 6.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="6.6" data-duration="33.4" data-track-index="5" style="--pk-pill-enter-delay: 6.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="7" data-duration="7" data-track-index="6" style="animation-delay: 7s;">
<div class="pk-card clip" data-start="10.0" data-duration="30" data-track-index="6" style="animation-delay: 10.0s;">
<div class="pk-card__title">Collision map</div>
<div class="r25-sub">See who touched what before you review.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">task detail</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="7" data-duration="1.6" data-track-index="7" style="--pk-pill-enter-delay: 7s; --pk-pill-exit-delay: 8.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="8.6" data-duration="5.4" data-track-index="8" style="--pk-pill-enter-delay: 8.6s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="10.0" data-duration="1.6" data-track-index="7" style="--pk-pill-enter-delay: 10.0s; --pk-pill-exit-delay: 11.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="11.6" data-duration="28.4" data-track-index="8" style="--pk-pill-enter-delay: 11.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="8.8" data-duration="5.2" data-track-index="9" style="animation-delay: 8.8s;">
<div class="pk-card clip" data-start="15.0" data-duration="25" data-track-index="9" style="animation-delay: 15.0s;">
<div class="pk-card__title">Metrics donut</div>
<div class="r25-sub">90 days of real task flow.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">performance tab</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="8.8" data-duration="1.6" data-track-index="10" style="--pk-pill-enter-delay: 8.8s; --pk-pill-exit-delay: 10.2s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="10.4" data-duration="3.6" data-track-index="11" style="--pk-pill-enter-delay: 10.4s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="15.0" data-duration="1.6" data-track-index="10" style="--pk-pill-enter-delay: 15.0s; --pk-pill-exit-delay: 16.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="16.6" data-duration="23.4" data-track-index="11" style="--pk-pill-enter-delay: 16.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="20.0" data-duration="20" data-track-index="12" style="animation-delay: 20.0s;">
<div class="pk-card__title">Notification bell</div>
<div class="r25-sub">Real read/ack actions on every alert.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">notifications</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="20.0" data-duration="1.6" data-track-index="13" style="--pk-pill-enter-delay: 20.0s; --pk-pill-exit-delay: 21.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="21.6" data-duration="18.4" data-track-index="14" style="--pk-pill-enter-delay: 21.6s;">completed</span>
</div>
</div>
</div>
<!-- Cursor glide to the newest card. -->
<div class="pk-cursor clip" data-start="9.4" data-duration="4.6" data-track-index="12" style="--pk-cursor-delay: 9.4s; --pk-cursor-x0: 720px; --pk-cursor-y0: 300px; --pk-cursor-x1: 200px; --pk-cursor-y1: 1110px;">
<div class="pk-cursor clip" data-start="22.0" data-duration="6" data-track-index="15" style="--pk-cursor-delay: 22.0s; --pk-cursor-x0: 720px; --pk-cursor-y0: 300px; --pk-cursor-x1: 200px; --pk-cursor-y1: 1090px;">
<div class="pk-cursor__glyph"></div>
<div class="pk-cursor__ring" style="--pk-click-delay: 10.4s;"></div>
<div class="pk-cursor__ring" style="--pk-click-delay: 23.4s;"></div>
</div>
<!-- Beat 5: receipt stats. -->
<div class="r25-stats clip" data-start="10.8" data-duration="3.2" data-track-index="14">
<div class="r25-stats__line" style="animation-delay: 10.8s;">1 release</div>
<div class="r25-stats__line r25-stats__line--accent" style="animation-delay: 11.1s;">25 agents</div>
<div class="r25-stats__line" style="animation-delay: 11.4s;">1 human</div>
<div class="r25-stats clip" data-start="24.0" data-duration="8" data-track-index="17">
<div class="r25-stats__line" style="animation-delay: 24.0s;">1 release</div>
<div class="r25-stats__line r25-stats__line--accent" style="animation-delay: 24.3s;">25 agents</div>
<div class="r25-stats__line" style="animation-delay: 24.6s;">1 human</div>
</div>
<!-- Beat 6: confirmation toast, then lockup. -->
<div class="pk-toast clip" data-start="11.8" data-duration="2.2" data-track-index="15" style="animation-delay: 11.8s;">
<div class="pk-toast clip" data-start="30.0" data-duration="8" data-track-index="18" style="animation-delay: 30.0s;">
<div class="pk-toast__icon"></div>
<div class="pk-toast__body">
<div class="pk-toast__title" id="toastTitle">v0.25.0 shipped</div>
@@ -284,7 +296,7 @@
</div>
</div>
<div class="pk-outro clip" data-start="12.6" data-duration="1.4" data-track-index="16" style="animation-delay: 12.6s;">
<div class="pk-outro clip" data-start="36.0" data-duration="4" data-track-index="19" style="animation-delay: 36.0s;">
<span class="r25-outro__logo"><img src="../../public/roboco-logo.png" alt="" /></span>
<span>roboco.tech</span>
</div>
@@ -296,7 +308,7 @@
window.__timelines = window.__timelines || {};
window.__timelines["release-0.25.0"] = {
id: "release-0.25.0",
duration: 14,
duration: 40,
fps: 30,
animations: []
};
@@ -70,18 +70,28 @@ export function SpawnAgentDialog({
setInitialPrompt("");
};
// The tooltip must wrap the DialogTrigger, never sit inside it: with
// HelpTip as DialogTrigger's asChild child, the dialog's click handler is
// cloned onto the Tooltip root (not a DOM element) and silently dropped.
const defaultTrigger = (
<HelpTip label="Start this agent's container, optionally pre-claiming a task" side="left">
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Play className="h-4 w-4 mr-2" />
Spawn
</DropdownMenuItem>
</HelpTip>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Play className="h-4 w-4 mr-2" />
Spawn
</DropdownMenuItem>
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger || defaultTrigger}</DialogTrigger>
{trigger ? (
<DialogTrigger asChild>{trigger}</DialogTrigger>
) : (
<HelpTip
label="Start this agent's container, optionally pre-claiming a task"
side="left"
>
<DialogTrigger asChild>{defaultTrigger}</DialogTrigger>
</HelpTip>
)}
<DialogContent>
<DialogHeader>
<DialogTitle>Spawn {agentName}</DialogTitle>
@@ -555,14 +555,14 @@ function KnowledgeBaseBrowserContent() {
<CardTitle className="text-sm flex items-center justify-between">
<span>Summary</span>
<AlertDialog>
<AlertDialogTrigger asChild>
<HelpTip label="Rebuilds the Documentation index from the repo's docs/ tree — the other categories (journals, conversations, etc.) are populated live by agent activity, not by this button">
<HelpTip label="Rebuilds the Documentation index from the repo's docs/ tree — the other categories (journals, conversations, etc.) are populated live by agent activity, not by this button">
<AlertDialogTrigger asChild>
<Button size="sm" variant="destructive">
<RefreshCw className="h-3 w-3 mr-1" />
Reindex All
</Button>
</HelpTip>
</AlertDialogTrigger>
</AlertDialogTrigger>
</HelpTip>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reindex All Data?</AlertDialogTitle>
@@ -679,8 +679,8 @@ function KnowledgeBaseBrowserContent() {
</Button>
</HelpTip>
<AlertDialog>
<AlertDialogTrigger asChild>
<HelpTip label="Delete this index and all its documents">
<HelpTip label="Delete this index and all its documents">
<AlertDialogTrigger asChild>
<Button
size="sm"
variant="outline"
@@ -689,8 +689,8 @@ function KnowledgeBaseBrowserContent() {
>
<Trash2 className="h-3 w-3" />
</Button>
</HelpTip>
</AlertDialogTrigger>
</AlertDialogTrigger>
</HelpTip>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
+1 -1
View File
@@ -125,7 +125,7 @@ export function Header() {
</div>
</TooltipTrigger>
<TooltipContent>
Signed in as the CEO the panel's single human operator
Signed in as the CEO the panel&apos;s single human operator
</TooltipContent>
</Tooltip>
</div>
+16 -4
View File
@@ -12090,11 +12090,15 @@ Start now: evidence(task_id="{task_id}")
@staticmethod
def _already_promoted_for_closure(task: dict[str, Any]) -> bool:
"""Skip closure respawn when PR+status show task has moved up."""
"""Skip closure respawn when PR+status show task has moved past the PM.
``awaiting_pm_review`` is deliberately NOT in the skip set: it is the
PM's own merge/review turn, and the session that submitted may be gone
(restart, idle-reap) the ``_is_agent_active`` check below already
prevents double-spawning while a PM is genuinely alive."""
return bool(
task.get("pr_number")
and task.get("status")
in ("awaiting_pm_review", "awaiting_ceo_approval", "completed")
and task.get("status") in ("awaiting_ceo_approval", "completed")
)
@staticmethod
@@ -12314,7 +12318,15 @@ Start now: evidence(task_id="{task_id}")
return
descendants = await self._fetch_all_descendants(client, task_id)
if not descendants or not self._all_descendants_terminal(descendants):
# A childless task in awaiting_pm_review IS the PM's review turn —
# its dev→qa→doc stages ran on the task itself. Without this, a leaf
# review stranded by a restart (the submit-time PM session gone) had
# no periodic pickup at all — proven live on the docs-sync leaf
# after the 0.25.0 redeploy.
is_leaf_review = not descendants and task.get("status") == "awaiting_pm_review"
if not is_leaf_review and (
not descendants or not self._all_descendants_terminal(descendants)
):
return
if self._already_promoted_for_closure(task):
return
@@ -342,6 +342,8 @@ _CURATE_VAULT_ROLES: frozenset[str] = frozenset({"auditor"})
# reuses MAX_TWEET_CHARS). No role frozenset here, unlike the sets above:
# propose_video is gated on the caller's TEAM at runtime (_caller_team), not
# role — Role.DEVELOPER doesn't distinguish a ux-dev from a be-dev/fe-dev.
# Kept in lockstep with video-renderer/server.js COMPOSITION_ID_RE.
_COMPOSITION_ID_RE = re.compile(r"^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$")
_VIDEO_PLATFORMS: frozenset[str] = frozenset({"x", "tiktok"})
_MAX_TIKTOK_CAPTION_CHARS = 2200
@@ -1532,6 +1534,20 @@ class ContentActions:
statement count under the xenon/PLR0911 budget)."""
if rej := cls._reject_soup(composition_id, field="composition_id", min_chars=2):
return rej
# Mirror the video-renderer sidecar's charset rule so an unrenderable
# id is refused at authoring time, not at render time days later.
if not _COMPOSITION_ID_RE.fullmatch(composition_id.strip()):
return Envelope.invalid_state(
message=(
f"composition_id {composition_id!r} is not renderable — "
"letters, digits, '_' or '-' with optional interior dots"
),
remediate=(
"rename the composition dir to match (e.g. "
"'release-0-25-0' or 'release-0.25.0') and call "
"propose_video again with that id"
),
)
if rej := cls._reject_caption(
x_caption, field="x_caption", max_chars=MAX_TWEET_CHARS
):
+5 -1
View File
@@ -2202,6 +2202,7 @@ class GitService(BaseService):
*,
workflow: str | None = None,
head_sha: str | None = None,
branch: str | None = None,
) -> dict[str, Any] | None:
"""Latest completed CI (GitHub Actions) run on a project's default branch.
@@ -2229,7 +2230,10 @@ class GitService(BaseService):
git_token = await self._token_for_project(project_slug)
if not git_token:
return None
branch = head_branch(project)
# Default to the head rung (where dev work and the release gate look);
# the release-commit CI wait overrides with the prod rung, where the
# pushed release commit actually lives.
branch = branch or head_branch(project)
query = _CiRunQuery(
project_slug=project_slug,
owner_repo=(owner, repo),
+4 -1
View File
@@ -449,7 +449,10 @@ class _GitReleaseOps:
git = get_git_service(self._session)
for _ in range(_CI_MAX_POLLS):
ci = await git.get_latest_ci_conclusion(
self._slug, workflow=self._ci_workflow, head_sha=commit_sha
self._slug,
workflow=self._ci_workflow,
head_sha=commit_sha,
branch=self._default_branch,
)
if ci and ci.get("head_sha") == commit_sha:
conclusion = (ci.get("conclusion") or "").lower()
@@ -286,3 +286,14 @@ async def test_propose_video_defaults_input_props_to_empty_dict(
draft = markers.get_video_draft(authoring)
assert draft is not None
assert draft["input_props"] == {}
@pytest.mark.asyncio
async def test_propose_video_rejects_unrenderable_composition_id() -> None:
"""An id the renderer's charset rule would 400 is refused at authoring
time, not at render time days later."""
env = await _actions("developer", "ux_ui").propose_video(
agent_id=uuid4(), **_valid_kwargs(composition_id="release 0.25.0!")
)
assert env.error == "invalid_state"
assert "not renderable" in (env.message or "")
@@ -161,3 +161,38 @@ async def test_auto_recover_blocked_swallows_errors() -> None:
# Must not raise.
await orch._auto_recover_blocked_parent(client, "p")
@pytest.mark.asyncio
async def test_childless_awaiting_pm_review_reaches_closure() -> None:
"""A leaf task in awaiting_pm_review (dev->qa->doc ran on the task itself)
is the PM's review turn — it must flow past the descendants gate, or a
restart-stranded review has no periodic pickup at all."""
orch = _ready_orch()
orch._fetch_all_descendants = AsyncMock(return_value=[])
orch._closure_handled_without_pm = AsyncMock(return_value=(True, None))
task = {"id": "t1", "status": "awaiting_pm_review", "team": "frontend"}
await orch._maybe_spawn_pm_closure(MagicMock(), task)
orch._closure_handled_without_pm.assert_awaited_once()
@pytest.mark.asyncio
async def test_childless_in_progress_still_bails() -> None:
"""A childless claimed/in_progress task is a dev's work, not PM closure
material the descendants gate must still bail."""
orch = _ready_orch()
orch._fetch_all_descendants = AsyncMock(return_value=[])
orch._closure_handled_without_pm = AsyncMock(return_value=(True, None))
task = {"id": "t1", "status": "in_progress", "team": "frontend"}
await orch._maybe_spawn_pm_closure(MagicMock(), task)
orch._closure_handled_without_pm.assert_not_awaited()
def test_promoted_skip_excludes_the_pm_merge_turn() -> None:
"""awaiting_pm_review IS the PM's turn — a PR-bearing task there must not
be skipped as already-promoted (the submit-time PM session may be gone)."""
promoted = AgentOrchestrator._already_promoted_for_closure
assert not promoted({"pr_number": 5, "status": "awaiting_pm_review"})
assert promoted({"pr_number": 5, "status": "awaiting_ceo_approval"})
assert promoted({"pr_number": 5, "status": "completed"})
assert not promoted({"pr_number": None, "status": "completed"})
@@ -587,3 +587,34 @@ def test_insert_changelog_entry_without_unreleased_is_unchanged_behavior() -> No
result = re._insert_changelog_entry(existing, entry)
assert result.index("## [0.25.0]") < result.index("## [0.24.0]")
assert "- old" in result and "- new" in result
@pytest.mark.asyncio
async def test_wait_for_ci_polls_the_prod_branch(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""The release commit lives on the prod rung — the CI wait must query that
branch, not the ladder head where get_latest_ci_conclusion defaults."""
seen: dict[str, object] = {}
async def _fake_get_ci(_slug: str, **kwargs: object) -> dict[str, object]:
seen.update(kwargs)
return {"head_sha": "cafebabe", "conclusion": "success"}
monkeypatch.setattr(
"roboco.services.git.get_git_service",
lambda _session: SimpleNamespace(get_latest_ci_conclusion=_fake_get_ci),
)
ctx = _ReleaseContext(
slug="roboco-api",
prod_branch="master",
root=tmp_path,
git_url="x",
git_prefix=[],
ci_workflow="ci.yml",
env_chain=["slave"],
)
ops = _GitReleaseOps(session=MagicMock(), ctx=ctx)
ok = await ops.wait_for_ci("cafebabe")
assert ok is True
assert seen.get("branch") == "master"
+5 -2
View File
@@ -24,7 +24,10 @@ const PORT = Number(process.env.PORT ?? 3001);
// safe path segment so a ".." / "/" / absolute-path value can't escape the
// composition dir (path traversal). The orchestrator only ever sends a real
// composition id, but this is the trust boundary — validate here.
const COMPOSITION_ID_RE = /^[A-Za-z0-9_-]+$/;
// Letters/digits/_/- plus interior single dots (e.g. release-0.25.0).
// No leading dot and no adjacent dots, so '.'/'..' path segments and
// hidden-file names remain unrepresentable — this stays the trust boundary.
const COMPOSITION_ID_RE = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
// motion/ source (no node_modules, no build output) is a few hundred KB in
// practice; this cap is generous headroom, not a tuned budget.
@@ -74,7 +77,7 @@ app.post("/render", renderLimiter, upload.single("source"), async (req, res) =>
) {
res.status(400).json({
error:
"'composition_id' must be a non-empty string of letters, digits, '_' or '-'",
"'composition_id' must be letters, digits, '_' or '-', with optional interior dots",
});
return;
}