From 695d1bd515abc2bf4c647d57c62ceef3552d4af9 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Date: Mon, 14 Sep 2026 15:42:43 +0500 Subject: [PATCH] generate: plan, tune, and accept exactly like live The lane's variants were tamer than the ones a live session makes on the same element: its poll instructions replaced live.md's planning method with a cheat sheet, its reference forbade knobs, told the agent to copy the markup verbatim and to treat DESIGN.md as a hard boundary, and its accept appended anchored overrides instead of integrating the design. Measured on the same page with Opus, live runs promoted a tier, broke the grid, and declared knobs; lane runs restyled three equal boxes. Now a Go the generate verb fires gets the same _instructions as a user's Go (the action's reference, section 4 planning, knobs per section 7), generate.md hands the design work to live.md's Handle generate and its Required after accept, Setup runs as for any command, the Tune chip behaves as in any session, and the mechanical bake is opt-in (--bake) instead of the lane's default. The start verdict points at live.md, and `browser` (the config key the opener reads) is a recognized key. Goldens re-recorded for the accept help and the recognized-keys line. Written with AI assistance (Claude). Co-Authored-By: Claude Fable 5 --- crates/cli/tests/agent_target.rs | 6 +- crates/context/src/staleness.rs | 4 +- crates/live/src/instructions.rs | 65 +++---------------- crates/live/src/live_accept.rs | 54 +++++++-------- crates/live/src/live_generate.rs | 4 +- docs/CLI-CONTRACT.md | 8 +-- skill/SKILL.src.md | 2 +- skill/reference/generate.md | 50 ++++---------- skill/scripts/live-browser.js | 4 +- tests/live-browser-source.test.mjs | 12 ++-- tests/oracle/golden/context-legacy.json | 2 +- .../golden/context-staleness-cache-env.json | 2 +- .../golden/context-staleness-throttle.json | 2 +- .../doctor-config-local-and-shared.json | 2 +- .../oracle/golden/doctor-legacy-fix-json.json | 2 +- .../doctor-legacy-fix-no-overwrite.json | 2 +- .../golden/doctor-legacy-fix-twice.json | 6 +- tests/oracle/golden/doctor-legacy-fix.json | 2 +- tests/oracle/golden/doctor-legacy-json.json | 2 +- tests/oracle/golden/doctor-legacy-text.json | 2 +- .../golden/doctor-order-boot-and-deep.json | 2 +- tests/oracle/golden/live-accept-help.json | 2 +- 22 files changed, 76 insertions(+), 161 deletions(-) diff --git a/crates/cli/tests/agent_target.rs b/crates/cli/tests/agent_target.rs index 558f84d2d..82b66fce9 100644 --- a/crates/cli/tests/agent_target.rs +++ b/crates/cli/tests/agent_target.rs @@ -754,11 +754,13 @@ fn live_generate_collects_its_own_generate_event_and_reply_then_poll_returns_the assert_eq!(code, 0, "{verdict}\n{stderr}"); assert_eq!(verdict["ok"], serde_json::json!(true), "{verdict}"); assert_eq!(verdict["sessionId"], serde_json::json!("c0ffee11")); - // B: the session's generate event rides along, leased, with the fast path. + // B: the session's generate event rides along, leased, with the same + // planning steps a user's Go gets. assert_eq!(verdict["event"]["type"], serde_json::json!("generate"), "{verdict}"); assert_eq!(verdict["event"]["id"], serde_json::json!("c0ffee11")); assert_eq!(verdict["event"]["origin"], serde_json::json!("agent")); - assert!(verdict["event"]["_instructions"].as_str().unwrap().contains("Fast path"), "{verdict}"); + let plan = verdict["event"]["_instructions"].as_str().unwrap(); + assert!(plan.contains("read reference/bolder.md before planning") && plan.contains("live.md section 4"), "{verdict}"); assert!(verdict["_instructions"].as_str().unwrap().contains("--reply c0ffee11 done --file --then-poll"), "{verdict}"); // Leased: a plain poll finds nothing else to hand out. let (_, polled) = http(s.port, "GET", &format!("/poll?token={}&timeout=300", s.token), None); diff --git a/crates/context/src/staleness.rs b/crates/context/src/staleness.rs index 4f7d9b92b..a04b73516 100644 --- a/crates/context/src/staleness.rs +++ b/crates/context/src/staleness.rs @@ -35,8 +35,8 @@ pub fn finding(id: &str, artifact: &str, path: Option, severity: &'stati Finding { id: id.to_string(), artifact: artifact.to_string(), path, severity, summary, fix } } -const KNOWN_CONFIG_KEYS: [&str; 8] = - ["hook", "detector", "updateCheck", "stalenessCheck", "projectRoots", "buildPath", "$schema", "version"]; +const KNOWN_CONFIG_KEYS: [&str; 9] = + ["hook", "detector", "updateCheck", "stalenessCheck", "projectRoots", "buildPath", "browser", "$schema", "version"]; const BUILD_PATH_VALUES: [&str; 2] = ["comp", "code"]; const DIRECTION_WORK_PATHS: [&str; 2] = [".impeccable/surfaces", ".impeccable/mocks/decision"]; const KNOWN_DETECTOR_KEYS: [&str; 5] = ["ignoreRules", "ignoreFiles", "ignoreValues", "designSystem", "extensions"]; diff --git a/crates/live/src/instructions.rs b/crates/live/src/instructions.rs index 58657d3b3..d1dd1561a 100644 --- a/crates/live/src/instructions.rs +++ b/crates/live/src/instructions.rs @@ -25,47 +25,6 @@ const PLAN_POINTER: &str = "Plan per live.md section 4: extract the identity loc /// The three dimensions an agent-initiated generate varies for each action: /// one per variant, so the trio reads as the same brand at three angles. -fn action_axes(action: &str) -> &'static str { - match action { - "bolder" => "scale (bigger type and tighter hierarchy) / saturation (commit the accent color) / structure (a stronger composition)", - "quieter" => "color (pull the accent back) / ornament (fewer decorations) / spacing (more air, softer edges)", - "distill" => "visual noise / redundant content / nested structure, one class of excess removed per variant", - "polish" => "rhythm / hierarchy / micro-details", - "typeset" => "a different pairing AND scale ratio per variant, within the available faces", - "colorize" => "a different hue family per variant, with its own chroma and contrast strategy", - "layout" => "three different structural arrangements, not spacing tweaks", - "adapt" => "mobile-first / tablet / desktop-or-print", - "animate" => "cascade stagger / clip wipe / scale-and-focus", - "delight" => "micro-interaction / typographic surprise / illustrated accent", - "overdrive" => "a different convention broken per variant: scale / structure / motion", - _ => "hierarchy / color strategy / density", - } -} - -/// What the poll tells the handler of a generate the agent itself started -/// (`origin: "agent"`): the user asked for variants to choose from, fast. -fn fast_path_instructions(event: &Map) -> String { - let action = event - .get("action") - .and_then(Value::as_str) - .filter(|a| !a.is_empty()) - .unwrap_or("impeccable"); - let count = js_str(event.get("count")); - let prompt = event - .get("freeformPrompt") - .and_then(Value::as_str) - .filter(|p| !p.trim().is_empty()) - .map(|p| format!(" The user's prompt narrows every variant: \"{}\".", slice16(p, 200))) - .unwrap_or_default(); - format!( - "Fast path (the user asked for {count} \"{action}\" variants to choose from, and is watching): do not read live.md, craft-floor.md, PRODUCT.md, or DESIGN.md now; the boot already handed you any design context, and this event carries element.computedStyles, element.cssCustomProperties, and element.parentContext. Lock the identity in ONE sentence from those (real colors, faces, corners, borders, shadows), then write {count} variants that each amplify a DIFFERENT dimension for {action}: {axes}. Keep the copy verbatim; no new fonts or hues beyond what the page already uses unless the prompt asks. When the boot printed a DESIGN.md, its tokens and named rules bound every variant: amplify inside them, never against them (a system that forbids fills, shadows, tints, or unequal columns gets its boldest allowed move on that axis instead, and tokens the axis does not need, such as radius, border, padding, and the number of bold weights, stay exactly as written); leaving the system is the user's call, not a variant. No parameter knobs (no data-impeccable-params): this lane bakes the accepted variant mechanically, and knobs belong to plain live. Floors: body text contrast 4.5:1 or better, no text under 12px, controls at least 40px tall, focus states kept.{prompt}", - count = count, - action = action, - axes = action_axes(action), - prompt = prompt - ) -} - fn reply_cmd(self_cmd: &str, id: &str, rest: &str) -> String { format!("{} --reply {} {}", poll_cmd(self_cmd), id, rest) } @@ -243,20 +202,17 @@ fn generate_instructions(event: &Map, self_cmd: &str) -> String { tag )); } + // A Go the generate verb fired (`origin: "agent"`) plans exactly like a + // user's Go: the same reference, the same section 4 method, the same + // knob budget. The lane is a different way in, not a different design. let action = event.get("action").filter(|a| truthy(Some(a))); - let agent_initiated = event.get("origin").and_then(Value::as_str) == Some("agent"); - if agent_initiated { - steps.push(fast_path_instructions(event)); - } match action { - Some(_) if agent_initiated => {} Some(a) if a.as_str() != Some("impeccable") => steps.push(format!( "Action is \"{}\": read reference/{}.md before planning; its MUST params are non-negotiable. {}", js_str(Some(a)), js_str(Some(a)), PLAN_POINTER )), - _ if agent_initiated => {} _ => steps.push(format!( "Freeform action: work from SKILL.md rules plus craft-floor.md; no sub-command file. {}", PLAN_POINTER @@ -468,15 +424,12 @@ mod tests { } #[test] - fn an_agent_initiated_generate_gets_the_fast_path_not_the_planning_ceremony() { - let text = generate_instructions(&generate_event(Some("agent")), "impeccable"); - assert!(text.contains("Fast path"), "{text}"); - assert!(text.contains("scale (bigger type"), "{text}"); - assert!(text.contains("No parameter knobs"), "{text}"); - assert!(!text.contains("live.md section 4"), "{text}"); - assert!(!text.contains("read reference/bolder.md"), "{text}"); + fn an_agent_initiated_generate_plans_exactly_like_a_users() { + let lane = generate_instructions(&generate_event(Some("agent")), "impeccable"); let user = generate_instructions(&generate_event(None), "impeccable"); - assert!(user.contains("live.md section 4"), "{user}"); - assert!(!user.contains("Fast path"), "{user}"); + assert_eq!(lane, user, "the lane is a different way in, not a different design"); + assert!(lane.contains("read reference/bolder.md before planning"), "{lane}"); + assert!(lane.contains("live.md section 4"), "{lane}"); + assert!(lane.contains("parameter knobs per section 7"), "{lane}"); } } diff --git a/crates/live/src/live_accept.rs b/crates/live/src/live_accept.rs index 62b595da6..79c7f0e43 100644 --- a/crates/live/src/live_accept.rs +++ b/crates/live/src/live_accept.rs @@ -5,7 +5,6 @@ use crate::paths::{live_dir, safe_session_id}; use crate::pending_edits::{read_buffer, write_buffer}; use crate::roots::enter_live_root; -use crate::session::create_live_session_store; use crate::source_lock::with_source_lock; use crate::source_search::{find_source_file, is_generated_file, resolve_live_template_extensions}; use crate::svelte_component::{ @@ -36,8 +35,7 @@ Options: --page-url URL Current browser page URL; scopes staged copy-edit cleanup --bake Bake a knob-free HTML/JSX accept mechanically (rules to the owning stylesheet, wrapper unwrapped) instead of leaving - the carbonize block; the default for sessions the - generate verb started (origin \"agent\") + the carbonize block; opt-in, never the default --no-bake Never bake; always leave the carbonize block --defer-source-write Deprecated compatibility flag. Svelte component accepts @@ -407,18 +405,12 @@ fn accept_cli(args: &[String], io: &mut Io) -> i32 { } } } else { - // A session the generate verb started is baked mechanically unless - // told otherwise; anything else only on --bake. Plain live keeps - // its carbonize block. - let agent_origin = !no_bake - && !bake_flag - && create_live_session_store(&cwd, &env, Some(&id)) - .get_snapshot(&id, true) - .ok() - .flatten() - .and_then(|s| s.get("origin").and_then(|o| o.as_str()).map(|o| o == "agent")) - .unwrap_or(false); - let bake = if !no_bake && (bake_flag || agent_origin) { + // Only --bake asks for the mechanical bake. Every session, the + // generate lane's included, keeps the carbonize block by default: + // the agent integrates the accepted variant the way live.md says, + // which is what makes the result read as designed rather than + // appended. + let bake = if !no_bake && bake_flag { Some(BakeRequest { cwd: cwd.clone(), session_id: id.clone() }) } else { None @@ -1319,11 +1311,10 @@ mod bake_tests { } #[test] - fn an_agent_started_session_bakes_by_default_and_plain_live_does_not() { - let dir = project("origin"); - let cwd = dir.to_string_lossy().into_owned(); + fn no_session_bakes_without_the_flag_the_generate_lane_included() { let env: Env = std::env::vars().collect(); // Plain live: no origin, no flag -> the carbonize block, as before. + let dir = project("origin"); let plain = accept(&dir, &["--id", SESSION, "--variant", "2"]); assert_eq!(plain["carbonize"], json!(true), "{plain}"); assert!(plain.get("baked").is_none(), "{plain}"); @@ -1331,26 +1322,25 @@ mod bake_tests { assert!(jsx.contains("impeccable-carbonize-start"), "{jsx}"); assert!(!std::fs::read_to_string(dir.join("src/styles.css")).unwrap().contains("32px")); - // The generate verb's session: the journal says origin agent. + // The generate verb's session (the journal says origin agent) carbonizes + // the same way: the agent integrates the accepted variant per live.md. let dir2 = project("origin2"); let cwd2 = dir2.to_string_lossy().into_owned(); - let store = create_live_session_store(&cwd2, &env, Some(SESSION)); - store + crate::session::create_live_session_store(&cwd2, &env, Some(SESSION)) .append_event(&json!({ "type": "generate", "id": SESSION, "origin": "agent", "count": 3, "pageUrl": "/", "action": "bolder" })) .unwrap(); - let baked = accept(&dir2, &["--id", SESSION, "--variant", "2"]); - assert_eq!(baked["baked"], json!(true), "{baked}"); - assert!(!std::fs::read_to_string(dir2.join("src/App.jsx")).unwrap().contains("data-impeccable")); - // --no-bake wins over the origin. + let lane = accept(&dir2, &["--id", SESSION, "--variant", "2"]); + assert_eq!(lane["carbonize"], json!(true), "{lane}"); + assert!(lane.get("baked").is_none(), "{lane}"); + assert!(std::fs::read_to_string(dir2.join("src/App.jsx")).unwrap().contains("impeccable-carbonize-start")); + // --bake is the only way in, and --no-bake still wins over it. let dir3 = project("origin3"); - let cwd3 = dir3.to_string_lossy().into_owned(); - create_live_session_store(&cwd3, &env, Some(SESSION)) - .append_event(&json!({ "type": "generate", "id": SESSION, "origin": "agent", "count": 3, "pageUrl": "/", "action": "bolder" })) - .unwrap(); - let kept = accept(&dir3, &["--id", SESSION, "--variant", "2", "--no-bake"]); + let baked = accept(&dir3, &["--id", SESSION, "--variant", "2", "--bake"]); + assert_eq!(baked["baked"], json!(true), "{baked}"); + let dir4 = project("origin4"); + let kept = accept(&dir4, &["--id", SESSION, "--variant", "2", "--bake", "--no-bake"]); assert_eq!(kept["carbonize"], json!(true), "{kept}"); - let _ = (cwd, cwd2, cwd3); - for d in [dir, dir2, dir3] { + for d in [dir, dir2, dir3, dir4] { let _ = std::fs::remove_dir_all(&d); } } diff --git a/crates/live/src/live_generate.rs b/crates/live/src/live_generate.rs index 49bfe7fff..b3aa4880f 100644 --- a/crates/live/src/live_generate.rs +++ b/crates/live/src/live_generate.rs @@ -194,12 +194,12 @@ fn instructions_for(result: &Map, self_cmd: &str) -> Option --then-poll", self_cmd, s("sessionId")); if result.get("event").map(|e| e.is_object()).unwrap_or(false) { return Some(format!( - "Session {} started: the browser scrolled to the target and fired Go (action \"{}\", count {}). Its generate event is in this output as `event`, already leased: follow event._instructions (identity from the event, ONE edit, no knobs). When the edit is written, reply and wait for the user's choice in one call: {}. The accept it returns is baked into source mechanically (_acceptResult.baked) and completes the session; then stop the helper.", + "Session {} started: the browser scrolled to the target and fired Go (action \"{}\", count {}). Its generate event is in this output as `event`, already leased: handle it exactly per live.md's Handle generate, as event._instructions say (the action's reference, section 4 planning, knobs per section 7, all variants in ONE edit at the scaffold's splice). When the edit is written, reply and wait for the user's choice in one call: {}. The accept it returns carbonizes like plain live's: finish live.md's Required after accept, run live-complete, then stop the helper.", s("sessionId"), s("action"), n("count"), reply )); } return Some(format!( - "Session {} started: the browser scrolled to the target and fired Go (action \"{}\", count {}). Its generate event had not arrived yet: run {} live-poll to collect it (its _instructions carry the fast path: identity from the event, ONE edit, no knobs), then reply and wait for the accept in one call: {}.", + "Session {} started: the browser scrolled to the target and fired Go (action \"{}\", count {}). Its generate event had not arrived yet: run {} live-poll to collect it and handle it exactly per live.md's Handle generate, as its _instructions say; then reply and wait for the accept in one call: {}.", s("sessionId"), s("action"), n("count"), self_cmd, reply )); } diff --git a/docs/CLI-CONTRACT.md b/docs/CLI-CONTRACT.md index 7a9c00880..75f62f8a9 100644 --- a/docs/CLI-CONTRACT.md +++ b/docs/CLI-CONTRACT.md @@ -1480,7 +1480,7 @@ Binds `127.0.0.1:PORT`. CORS: if request has `Origin` and (origin is loopback ht | `POST /manual-edit-discard?token=&pageUrl=` | 401 | see 10 | | `POST /manual-edit` | | 410 `{"error":"/manual-edit is removed; use /manual-edit-stash and /manual-edit-commit for staged copy edits."}` | | `POST /agent-target` | body JSON `token` mismatch → 401 `{"error":"Unauthorized"}`; invalid JSON → 400 `{"error":"Invalid JSON"}` | Agent-initiated targeting (the `generate` command). Validation (400 `{"error":}`, messages verbatim): `agent_target: selector is required`, `agent_target: selector too long` (>1000 chars), `agent_target: invalid action (valid: )`, `agent_target: count must be 1-8`, `agent_target: text must be a string of at most 500 chars`, `agent_target: index must be a positive integer (1-based)`, `agent_target: prompt must be a string of at most 2000 chars`, `agent_target: dryRun must be a boolean` / `agent_target: hideLiveBar must be a boolean`. No SSE client → 200 `{ok:false, error:'no_browser_connected'}`. Otherwise mint an 8-hex `targetId`, broadcast `agent_target` (see 6.2), and **hold the response** until `/agent-target-result` resolves it, every connected overlay has declined (busy roll call, see `/agent-target-claim`), or `IMPECCABLE_AGENT_TARGET_TIMEOUT_MS` (default 15000) elapses: busy verdict `{ok:false, error:'busy', state, reason}` from the first report when any report exists, else `{ok:false, error:'browser_timeout', timeoutMs}`. The held reply is 200 `{targetId, ...result}`; shutdown resolves every held request with `{ok:false, error:'server_stopping'}`. | -| `POST /agent-target-result` | 401 / 400 Invalid JSON | `targetId` and `clientId` (non-empty strings) required else 400 (`agent_target_result: missing targetId` / `agent_target_result: missing clientId`); while the target is pending, only its lease holder's `clientId` may answer: another overlay gets 409 `{"error":"agent_target_result: not the holder", reason:'not_holder'|'unclaimed', targetId}` and the request stays pending. Otherwise `targetId` (non-empty string) required else 400 `{"error":"agent_target_result: missing targetId"}`; the remaining body fields (minus `token`) resolve the held request; 200 `{ok:true, delivered:boolean}` (`delivered:false` when nothing awaits that id). A `generate` event on `POST /events` may carry `agentTarget: {targetId, result}`: once the event is accepted, the server resolves that pending target with `result` (the envelope is stripped before journaling and never reaches the poller), so a page that dies between Go and its result cannot leave the request pending for a second Go elsewhere; whichever of the event and the result post lands first answers. An accepted generate event carrying the envelope is journaled and queued with `origin: "agent"`, and `live-poll` renders that event's `_instructions` as the fast path (identity from the event's `element.computedStyles` / `cssCustomProperties` / `parentContext`, the action's three dimensions, no parameter knobs unless the prompt asks, one edit, reply done) instead of the interactive planning pointer and the action-reference read. The envelope also carries `clientId`: a generate event from a page that is not the pending target's holder (another page holds the lease, or held it last, or nobody claimed it; a lapsed lease still belongs to the page that held it last until a rescuer claims), or naming a target that was already answered, with a different session or with none (a timeout or a failure verdict the CLI has reported), or that the helper neither holds nor remembers (never issued by it, or evicted from its bounded record of answered targets), is refused with 409 `{"error":"agent_target_already_served", targetId, sessionId?}` and journals nothing, and the overlay drops that local session; the holder's own Go and the answering session's own event are welcome. | +| `POST /agent-target-result` | 401 / 400 Invalid JSON | `targetId` and `clientId` (non-empty strings) required else 400 (`agent_target_result: missing targetId` / `agent_target_result: missing clientId`); while the target is pending, only its lease holder's `clientId` may answer: another overlay gets 409 `{"error":"agent_target_result: not the holder", reason:'not_holder'|'unclaimed', targetId}` and the request stays pending. Otherwise `targetId` (non-empty string) required else 400 `{"error":"agent_target_result: missing targetId"}`; the remaining body fields (minus `token`) resolve the held request; 200 `{ok:true, delivered:boolean}` (`delivered:false` when nothing awaits that id). A `generate` event on `POST /events` may carry `agentTarget: {targetId, result}`: once the event is accepted, the server resolves that pending target with `result` (the envelope is stripped before journaling and never reaches the poller), so a page that dies between Go and its result cannot leave the request pending for a second Go elsewhere; whichever of the event and the result post lands first answers. An accepted generate event carrying the envelope is journaled and queued with `origin: "agent"` (the overlay restores its lane chrome from it); its `_instructions` are the same planning steps a user's Go gets. The envelope also carries `clientId`: a generate event from a page that is not the pending target's holder (another page holds the lease, or held it last, or nobody claimed it; a lapsed lease still belongs to the page that held it last until a rescuer claims), or naming a target that was already answered, with a different session or with none (a timeout or a failure verdict the CLI has reported), or that the helper neither holds nor remembers (never issued by it, or evicted from its bounded record of answered targets), is refused with 409 `{"error":"agent_target_already_served", targetId, sessionId?}` and journals nothing, and the overlay drops that local session; the holder's own Go and the answering session's own event are welcome. | | `POST /live-bar` | 401 / 400 Invalid JSON | `hidden` (boolean) required else 400 `{"error":"live_bar: hidden must be a boolean"}`. Sets the helper-wide bar preference; on a change broadcasts `{type:'live_bar', hidden}` to every SSE client. `GET /status` and the SSE `connected` frame carry it as `hideLiveBar`. Answers `{ok:true, hidden}`. | | `POST /agent-target-claim` | 401 / 400 Invalid JSON | `targetId` and `clientId` (non-empty strings) required else 400 `{"error":"agent_target_claim: missing targetId or clientId"}`. Roll call plus a first-wins lease, so exactly one overlay acts on a broadcast target. Unknown or resolved target → `{ok:true, granted:false, pending:false}` (ends a rescuer's retry loop). `eligible !== true` → record `{state, reason, result?}` under `clientId` (replacing an earlier report; `result` is the overlay's resolution verdict when `reason` is `no_match`, i.e. its page cannot resolve the selector), release the lease if this client holds it, answer `{ok:true, granted:false, pending}` (`pending` false once the request resolved, so a declining overlay knows whether to keep watching for a change of its word), then complete the roll call when no owner holds the lease and reports ≥ connected overlays. Verdict precedence: a report whose `reason` is not `no_match` (a tab that could serve later) → `{ok:false, error:'busy', state, reason}` at once; when every report is `no_match` the roll call stays open for `IMPECCABLE_AGENT_TARGET_RESOLVE_GRACE_MS` (default 3000) after each overlay's first such report (a late reporter extends the grace by the full window; a page whose element mounts late keeps re-checking while its decline answers `pending:true`, an eligible claim drops its stale report, and the overlay declines rather than posting a result when the element is gone after its claim), then answers the first report's `result` (e.g. `no_match` with `rawMatchCount`, `invalid_selector`); the timeout uses the same precedence when any report exists. `eligible === true` → drop this client's earlier report; `granted` when no owner, the same owner (renew), or the lease lapsed (`IMPECCABLE_AGENT_TARGET_CLAIM_LEASE_MS`, default 3000); answer `{ok:true, granted, pending:true}`. | | anything else | | 404 `Not found` | @@ -1575,7 +1575,7 @@ Journal line: `{"seq":N,"id":"…","type":"…","ts":"ISO","event":{…full even Base snapshot: `{id, phase:'new', pageUrl:null, sourceFile:null, previewFile:null, previewMode:null, expectedVariants:0, arrivedVariants:0, visibleVariant:null, paramValues:{}, pendingEventSeq:null, pendingEvent:null, deliveryLease:null, checkpointRevision:0, browserCheckpointRevision:0, publicationCheckpointRevision:0, activeOwner:null, sourceMarkers:{}, fallbackMode:null, generationPhase:null, generationCompletedAt:null, generationTimings:{}, variantPlan:null, generationCanceled:false, generationCanceledAt:null, cancelReason:null, annotationArtifacts:[], mountedVariants:[], mountFailures:[], renderState:null, diagnostics:[], updatedAt:null}` (+ `detectorWaivers`, `message` when set). Reducer per event type (`updatedAt = entry.ts`): -- `generate`: phase `generate_requested`; `origin` (the server stamps `"agent"` on a Go the generate verb fired; accept reads it back to decide the mechanical bake); pageUrl; expectedVariants=count; pendingEventSeq=seq; pendingEvent=event; variantPlan=null; mounted/mountFailures cleared, renderState null; screenshotPath → push `{type:'screenshot', path}` artifact. +- `generate`: phase `generate_requested`; `origin` (the server stamps `"agent"` on a Go the generate verb fired; the overlay restores its lane chrome from it); pageUrl; expectedVariants=count; pendingEventSeq=seq; pendingEvent=event; variantPlan=null; mounted/mountFailures cleared, renderState null; screenshotPath → push `{type:'screenshot', path}` artifact. - `variant_plan` (unless canceled/fenced): variantPlan=plan. `detector_waivers`: append waivers. - `agent_phase`: generationPhase=phase; `generationTimings[phase]={at, durationMs}`. - `variants_ready`|`agent_done`: if canceled/fenced and not (agent_done carbonize in `accept_requested`) → diagnostic `late_generation_event_ignored`; else phase = `carbonize_required` if carbonize else `variants_ready`; generationCompletedAt; sourceFile=event.sourceFile??event.file; previewFile/previewMode; arrivedVariants = event.arrivedVariants ?? expected; clear pending; carbonize → diagnostic `carbonize_cleanup_required`; renderState derived (`mounted` if any mounted, `failed` if failures only, `pending` if completed, else null). @@ -1686,7 +1686,7 @@ Order in `live-accept.mjs`: receipt check → find `impeccable-variants-start
` … `
` with body indented 2 more, ``, `{/* … */}` comments, `style={{ display: 'contents' }}` on the variant div. Result `{handled:true, file: rel, carbonize:boolean, todo?:'REQUIRED before next poll: carbonize cleanup in . See reference/live.md "Required after accept".', bakeSkipped?}`. Discard: replace range with deindented original → `{handled:true, file, carbonize:false}`. -**Mechanical bake** (`bake.rs`): for a session whose snapshot carries `origin:'agent'` (a Go fired by `live-generate`) or on `--bake` (never on `--no-bake`; plain live sessions are untouched), a knob-free HTML/JSX accept is made permanent instead of leaving the carbonize block. Refused (falls back to the carbonize block, with `bakeSkipped:`) when: `--param-values` is non-empty; the accepted variant carries `data-impeccable-*` or `data-p-*` inside it; the preview CSS uses `var(--p-*)`, `data-p-*`, or `data-impeccable-params`; the variant's root is a component (``, ``: what it renders is unknown, and its `className` or `id` prop may never reach that element) or has neither an id nor a static class (`className={expr}`); a `:scope` cannot be rewritten (sibling combinators, `:scope` not at the front, nested `@scope`); the accepted variant declares no rule; or no destination stylesheet exists. The rewrite: the accepted `@scope ([data-impeccable-variant="N"])` block is flattened and every selector re-anchored on the root tag's selector (`#id`, else `tag.class.class`): `:scope > .x` → `.x`, `:scope .x` → ` .x`, `:scope:hover > .x` → `:hover > .x`, bare `:scope` → ``; Astro's `[data-impeccable-variant="N"] > .x` prefix the same way; nested `@media`/`@supports` inside the block keep their prelude; top-level `@keyframes`/`@font-face` are kept, other variants' blocks dropped. Destination: for `.jsx`/`.tsx` the `.css` file under the app root (skipping node_modules/.git/.impeccable/dist/build/coverage/framework caches, depth ≤ 6, `.min.css` and generated or git-ignored files excluded) with the most rules naming the anchor's id or classes, else the only `.css` file; for other files the page's own last ` -
-
-
-``` - - Rules that keep the browser mounting what you wrote, and the accept baking it: - - each variant div holds exactly one top-level element, same tag as the original, its id or class kept so the bake can anchor its selectors, copy verbatim; - - first variant visible, the rest `display: none`; - - every `:scope` rule steps into a descendant (`:scope > .card`; a bare `:scope` or a sibling combinator on it breaks the bake); - - the variant's own markup carries no `data-impeccable-*` attributes; - - the event's `cssAuthoring` wins over the sketch above for `styleTag` and selector strategy; - - **JSX / TSX**: wrap the `