mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 07:36:50 +03:00
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 <noreply@anthropic.com>
This commit is contained in:
committed by
Abdul Wahab
co-authored by
Claude Fable 5
parent
c36e37808e
commit
695d1bd515
@@ -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 <project-root-relative path you wrote> --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);
|
||||
|
||||
@@ -35,8 +35,8 @@ pub fn finding(id: &str, artifact: &str, path: Option<String>, 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"];
|
||||
|
||||
@@ -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, Value>) -> 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<String, Value>, 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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,12 +194,12 @@ fn instructions_for(result: &Map<String, Value>, self_cmd: &str) -> Option<Strin
|
||||
let reply = format!("{} live-poll --reply {} done --file <project-root-relative path you wrote> --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
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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":<msg>}`, messages verbatim): `agent_target: selector is required`, `agent_target: selector too long` (>1000 chars), `agent_target: invalid action (valid: <VISUAL_ACTIONS joined ', '>)`, `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 <i
|
||||
- JSX: everything above wrapped in `<indent><div data-impeccable-carbonize="ID" style={{ display: "contents" }}>` … `</div>` with body indented 2 more, `<style …>{\`` / `\`}</style>`, `{/* … */}` comments, `style={{ display: 'contents' }}` on the variant div.
|
||||
Result `{handled:true, file: rel, carbonize:boolean, todo?:'REQUIRED before next poll: carbonize cleanup in <file>. 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:<reason>`) 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 (`<PricingGrid>`, `<Card.Root>`: 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` → `<anchor> .x`, `:scope:hover > .x` → `<anchor>:hover > .x`, bare `:scope` → `<anchor>`; 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 `<style>` block when it has one, else the same search. The rules are **appended** under `/* impeccable generate <id>: accepted variant N */` (existing rules are never rewritten; a same-selector rule later in the cascade overrides declaration by declaration). The source is verified clean (`verifyAcceptedSource`) before anything is written; the stylesheet is written first, then the source with the variant unwrapped at the wrapper's indentation. Result `{handled:true, file, carbonize:false, baked:true, variant:'N', css:{file: rel|null (null = the page's own <style>), rules, anchor}, verify:{clean, findings}}`; the poll's completion for it is `complete`, so the session ends without `live-complete`. After accept with `--page-url`, buffered manual-edit ops whose original/new text appears as an exact text segment in the replaced original block are dropped from `pending-manual-edits.json`.
|
||||
**Mechanical bake** (`bake.rs`): only on `--bake` (never by default and never on `--no-bake`; the generate lane's accept carbonizes exactly like plain live's, so the agent integrates the accepted variant per live.md), a knob-free HTML/JSX accept is made permanent instead of leaving the carbonize block. Refused (falls back to the carbonize block, with `bakeSkipped:<reason>`) 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 (`<PricingGrid>`, `<Card.Root>`: 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` → `<anchor> .x`, `:scope:hover > .x` → `<anchor>:hover > .x`, bare `:scope` → `<anchor>`; 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 `<style>` block when it has one, else the same search. The rules are **appended** under `/* impeccable generate <id>: accepted variant N */` (existing rules are never rewritten; a same-selector rule later in the cascade overrides declaration by declaration). The source is verified clean (`verifyAcceptedSource`) before anything is written; the stylesheet is written first, then the source with the variant unwrapped at the wrapper's indentation. Result `{handled:true, file, carbonize:false, baked:true, variant:'N', css:{file: rel|null (null = the page's own <style>), rules, anchor}, verify:{clean, findings}}`; the poll's completion for it is `complete`, so the session ends without `live-complete`. After accept with `--page-url`, buffered manual-edit ops whose original/new text appears as an exact text segment in the replaced original block are dropped from `pending-manual-edits.json`.
|
||||
|
||||
Receipt: on any `handled!==false` result write `accept-receipts/<id>.json` = `{id, operation:'accept'|'discard', variantId:'N'|null, result, completedAt}` (tmp+rename). Re-run with same op/variant → prior `result` + `{handled:true, alreadyApplied:true}`; different → `{handled:false, mode:'error', error:'accept_receipt_conflict', priorOperation, priorVariantId}`.
|
||||
|
||||
@@ -1804,7 +1804,7 @@ Conventions: every script's "run directly" guard is `process.argv[1]` ending wit
|
||||
- **Invoked from**: `skill/reference/generate.md` (the `generate` command), as the lane's one start command after the agent opened the page in its harness's own browser: `impeccable live-generate --target src/App.jsx --dev-url http://127.0.0.1:5173/ --selector ".pricing-grid" --action bolder --count 3 --boot --wait-for-browser 60000` (`--open` only on a harness with no browser tool; without `--dev-url` and without a wait, the verdict is `browser_needed` and the agent comes back with the page open).
|
||||
- **Args**: `--selector <css>` (required), `--text <snippet>`, `--index <n>` (1-based), `--action <name>` (default `impeccable`), `--count <n>` (default 3, 1-8), `--prompt <text>`, `--dry-run`, `--wait-for-browser <ms>`, `--no-live-bar` (body `hideLiveBar:true`: the helper sets its lifetime-wide `hideLiveBar` preference, broadcasts `{type:'live_bar', hidden:true}` to every connected overlay before the target goes out, and answers `hideLiveBar:true` on every later `connected` frame; the overlay hides its global bar accordingly and skips its "No PRODUCT.md found" connect notice, the variant controls still show, and only the helper stopping ends it), `--target <path>` (consumed by `enterLiveRoot`, and the boot's --target under `--boot`), `--boot` (run `live --allow-missing-context --dev-url --no-live-bar [--target]` in-process from the caller's cwd first, reusing a running helper; implies `hideLiveBar:true` on the target; the boot's `devUrl, pageFiles, projectRoot, targetPath, liveBarHidden, contextMissing, contextNote, hasProduct, product, productPath, hasDesign, design, designPath, hasSurfaceBrief, surfaceBrief, surfaceBriefPath` ride along as `boot`; a refusing boot is printed as its own payload plus `ok:false`, `bootError:<its error>` and `_instructions`, exit 1; an unparseable boot → `boot_failed` (+`exitCode`, `detail`)), `--open` (ignored, with `openIgnored:'harness browser'` on the verdict and an `_instructions` prefix saying so, when the provider id is `cursor` or `claude-code` and neither `IMPECCABLE_BROWSER` nor the config's `browser` names a browser: a harness with its own browser never gets a second window from this verb, and the generic `BROWSER` variable is not that choice; otherwise, when `/status` reports no connected client: the dev URL is the boot's `devUrl` or a fresh `dev_url::probe`; none → `no_dev_server`, exit 1; else the URL is opened without waiting for the browser (`IMPECCABLE_BROWSER`, then `browser` in `.impeccable/config.local.json` / `.impeccable/config.json` at the app root, then `BROWSER`, then `open` / `xdg-open` / `cmd /c start`; a value with a path separator runs as a program with the URL as its argument, on macOS any other value is `open -a <name>`), recorded as `opened:{url, via}`, and `--wait-for-browser` defaults to 60000; a launch failure → `browser_open_failed` (+`url`, `detail`)), `--allow-missing-context` (accepted and ignored), `--dev-url [<url>]` (bare: ignored, the boot's own flag; with a value: the dev server the caller already knows, put first in the boot's probe list (`IMPECCABLE_DEV_URL_CANDIDATES` still wins when set) and in this verb's own probe, and reported as `devUrl` with `devUrlVerified:false` when no probe confirmed the tag on it), `--help`. A flag without a value → stdout `{"ok":false,"error":"missing_flag_value","flag":"--x"}`, exit 1.
|
||||
- **Env**: `IMPECCABLE_SELF` (how the boot and poll verbs are spelled in `_instructions`).
|
||||
- **Behavior**: `enterLiveRoot`; local verdicts first, each pretty-printed JSON on stdout with `_instructions`, exit 1: `selector_required`, `invalid_action` (+`action`, `validActions`), `invalid_count` (+`count`), `invalid_index` (+`index`), `invalid_wait` (+`wait`); no `server.json` (or one without port/token) → `server_not_running`. With `--wait-for-browser`, `GET /status` once a second until `connectedClients > 0` or the budget ends (`no_browser_connected` + `waitedMs`); while it waits, the dev URL it knows (the one it opened, else the boot's `devUrl`, else the probe or the caller's hint) gets a TCP connect every third tick, and two misses in a row end the wait early with `dev_server_gone` (+`devUrl`, `waitedMs`, `harness`) whose `_instructions` name the harness's way to start the dev script and rerun with `--dev-url`; an unanswered `/status` → `server_unreachable`. Then `POST /agent-target` with `{token, selector, action, count, text?, index?, prompt?, dryRun?}` under a 20 s client cap: a transport timeout → `request_timeout` (+`detail`, browser_timeout instructions), any other transport failure → `server_unreachable` (+`detail`); a non-2xx answer → `{ok:false, error:<body.error or http_<status>>, ...body}`; an unparseable body → `bad_server_response` (+`status`). Before the target goes out, with no page connected (`/status` `connectedClients` 0), no `--open`, no `--wait-for-browser`, and no `--dry-run`: `browser_needed` (`devUrl` from the boot's probe, else a probe led by the hint, else the hint unverified; `devUrlVerified`; `harness`, the provider id) or `no_dev_server` when no URL is known, exit 1. Its `_instructions` name the harness's own browser and never a second one: `cursor` → `browser_navigate` (reuse the tab on that origin); `claude-code` → the Browser pane (`navigate` the tab already on that origin, `tabs_context`, `preview_start` with the URL when the pane is closed); `codex` → rerun with `--open` or give the user the URL; others → the harness browser tool, else `--open` or the user; then rerun with `--wait-for-browser 60000`. `no_dev_server` names where this harness starts a server (`claude-code`: `preview_start` or the dev script; others: the dev script in a background terminal) and asks for `--dev-url <url>` on the rerun. Every verdict from the boot on carries `harness`. A started session (`ok:true`, not a dry run) then collects its own generate event: `GET /poll?types=generate&id=<sessionId>` in ≤5 s slices for up to 20 s, leased exactly as a poll leases it (the preflight scaffold runs on lease), printed as `event` with locally generated `_instructions` (the fast path), or `event:null` when it did not arrive. A 2xx answer is printed as received plus `boot`/`opened` when those ran, plus `_instructions` for `ok` (dry run; started session with `event`, pointing at the edit and at `live-poll --reply <id> done --file <path> --then-poll`; started session without it, pointing at `live-poll` first), `no_dev_server`, `browser_open_failed`, `no_browser_connected` (a variant when `opened` is present), `browser_timeout`, `invalid_selector`, `no_match` (wording depends on `rawMatchCount`), `ambiguous`, `index_out_of_range`, `busy`, `go_failed`, `server_stopping`; exit 0 when `ok:true`, else 1. `_instructions` are regenerated locally from the verdict, never taken from the wire.
|
||||
- **Behavior**: `enterLiveRoot`; local verdicts first, each pretty-printed JSON on stdout with `_instructions`, exit 1: `selector_required`, `invalid_action` (+`action`, `validActions`), `invalid_count` (+`count`), `invalid_index` (+`index`), `invalid_wait` (+`wait`); no `server.json` (or one without port/token) → `server_not_running`. With `--wait-for-browser`, `GET /status` once a second until `connectedClients > 0` or the budget ends (`no_browser_connected` + `waitedMs`); while it waits, the dev URL it knows (the one it opened, else the boot's `devUrl`, else the probe or the caller's hint) gets a TCP connect every third tick, and two misses in a row end the wait early with `dev_server_gone` (+`devUrl`, `waitedMs`, `harness`) whose `_instructions` name the harness's way to start the dev script and rerun with `--dev-url`; an unanswered `/status` → `server_unreachable`. Then `POST /agent-target` with `{token, selector, action, count, text?, index?, prompt?, dryRun?}` under a 20 s client cap: a transport timeout → `request_timeout` (+`detail`, browser_timeout instructions), any other transport failure → `server_unreachable` (+`detail`); a non-2xx answer → `{ok:false, error:<body.error or http_<status>>, ...body}`; an unparseable body → `bad_server_response` (+`status`). Before the target goes out, with no page connected (`/status` `connectedClients` 0), no `--open`, no `--wait-for-browser`, and no `--dry-run`: `browser_needed` (`devUrl` from the boot's probe, else a probe led by the hint, else the hint unverified; `devUrlVerified`; `harness`, the provider id) or `no_dev_server` when no URL is known, exit 1. Its `_instructions` name the harness's own browser and never a second one: `cursor` → `browser_navigate` (reuse the tab on that origin); `claude-code` → the Browser pane (`navigate` the tab already on that origin, `tabs_context`, `preview_start` with the URL when the pane is closed); `codex` → rerun with `--open` or give the user the URL; others → the harness browser tool, else `--open` or the user; then rerun with `--wait-for-browser 60000`. `no_dev_server` names where this harness starts a server (`claude-code`: `preview_start` or the dev script; others: the dev script in a background terminal) and asks for `--dev-url <url>` on the rerun. Every verdict from the boot on carries `harness`. A started session (`ok:true`, not a dry run) then collects its own generate event: `GET /poll?types=generate&id=<sessionId>` in ≤5 s slices for up to 20 s, leased exactly as a poll leases it (the preflight scaffold runs on lease), printed as `event` with locally generated `_instructions` (the same planning steps `live-poll` would print for a user's Go), or `event:null` when it did not arrive. A 2xx answer is printed as received plus `boot`/`opened` when those ran, plus `_instructions` for `ok` (dry run; started session with `event`, pointing at the edit and at `live-poll --reply <id> done --file <path> --then-poll`; started session without it, pointing at `live-poll` first), `no_dev_server`, `browser_open_failed`, `no_browser_connected` (a variant when `opened` is present), `browser_timeout`, `invalid_selector`, `no_match` (wording depends on `rawMatchCount`), `ambiguous`, `index_out_of_range`, `busy`, `go_failed`, `server_stopping`; exit 0 when `ok:true`, else 1. `_instructions` are regenerated locally from the verdict, never taken from the wire.
|
||||
- **Tests**: `tests/oracle/cases/live-generate.mjs` (local verdicts, no-browser), `tests/live-agent-target.test.mjs` (protocol matrix against the binary), `crates/cli/tests/agent_target.rs`, `tests/live-e2e.test.mjs` (`agentTargetScenario`).
|
||||
|
||||
#### `live-commit-manual-edits.mjs` -> `impeccable commit-manual-edits`
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ Core principles:
|
||||
|
||||
## Setup
|
||||
|
||||
1. Run `<skill-base-dir>/scripts/impeccable context` once per session, where `<skill-base-dir>` is the directory that contains this SKILL.md (the skill folder, not a plugin root two levels above it); keep cwd at the user's project. That base directory resolves every `{{scripts_path}}/impeccable <verb>` command in this skill and its references, and `{{scripts_path}}` is the fallback only when the runtime reports no base directory. On a Windows shell without `sh`, call `{{scripts_path}}/impeccable.cmd` instead. The launcher runs a self-contained binary that ships next to it or is downloaded once on first run; no Node or other runtime is required. Pass a named source file or route as `--target <path>`. It loads PRODUCT.md, DESIGN.md, the matching surface brief, and native-platform guidance when applicable; follow its directives and do not rerun it. The one exception is `generate`: skip this step for it, its own start command loads the same files. <!-- rule:skill-setup-context -->
|
||||
1. Run `<skill-base-dir>/scripts/impeccable context` once per session, where `<skill-base-dir>` is the directory that contains this SKILL.md (the skill folder, not a plugin root two levels above it); keep cwd at the user's project. That base directory resolves every `{{scripts_path}}/impeccable <verb>` command in this skill and its references, and `{{scripts_path}}` is the fallback only when the runtime reports no base directory. On a Windows shell without `sh`, call `{{scripts_path}}/impeccable.cmd` instead. The launcher runs a self-contained binary that ships next to it or is downloaded once on first run; no Node or other runtime is required. Pass a named source file or route as `--target <path>`. It loads PRODUCT.md, DESIGN.md, the matching surface brief, and native-platform guidance when applicable; follow its directives and do not rerun it. <!-- rule:skill-setup-context -->
|
||||
2. Load the request's playbook: its Commands-table reference for an explicit/implied sub-command, or [reference/new-work.md](reference/new-work.md) for a new surface or replacement visual world. Inspect target and incumbent visual truth before editing. When the app cannot run, start with committed visual-regression goldens or screenshot fixtures; verify target and freshness against current tokens, CSS, components, or assets, resolve conflicts, and compare theme/variant captures. <!-- rule:skill-setup-command-ref --> <!-- rule:skill-setup-read-project -->
|
||||
3. After resolving analysis and direction, read [reference/craft-floor.md](reference/craft-floor.md) immediately before any UI edit, including small refinements. It carries the quality floor, the absolute bans, and the reflexes no detector catches. Do not load it for planning-only work. <!-- rule:skill-craft-floor-load -->
|
||||
|
||||
|
||||
+11
-39
@@ -1,16 +1,15 @@
|
||||
> **Additional context needed**: only the target element, when the request does not name one that resolves uniquely on the page.
|
||||
|
||||
Generate is the fast lane into live mode: the user names an element, a direction, and a count in one sentence, and within a minute they are cycling through variants in their browser. One command boots the helper, hands the element to the overlay in the page your harness already shows (it scrolls to it, selects it, and fires the same Go a click fires) and returns the generate event; one edit writes the variants; one call replies and waits for the user's choice, which the helper bakes into source itself. This file is the whole contract for that lane; read [live.md](live.md) only for a situation Step 3 names as outside the lane.
|
||||
Generate is the fast lane into live mode: the user names an element, a direction, and a count in one sentence, and within a minute they are cycling through variants in their browser. One command boots the helper, hands the element to the overlay in the page your harness already shows (it scrolls to it, selects it, and fires the same Go a click fires) and returns the generate event; one edit writes the variants; one call replies and waits for the user's choice, which the helper bakes into source itself. This file owns the lane's plumbing; from the event onward the design work is [live.md](live.md)'s, unchanged, so read it in full now if you have not this session.
|
||||
|
||||
**Web only.** Live mode's browser overlay has no native equivalent; on `ios` / `android` / `adaptive` projects, decline this command and offer `bolder` or `quieter` on the source instead.
|
||||
|
||||
Speed is the product here. Every tool call before the variants land is a second the user spends staring at a selected element. The lane below is three impeccable commands and one edit around the page your harness already shows; anything beyond it needs a reason from the output in front of you. **Skip Setup step 1 for this command**: do not run `impeccable context`; the boot inside Step 2 loads PRODUCT.md, DESIGN.md, and the surface brief itself. This lane also replaces Setup step 3 for the preview edit: the floors craft-floor.md guards are written into Step 3, so do not open craft-floor.md, and read the action's reference only when Step 3 says so.
|
||||
The plumbing is where the lane saves time: one command starts the session around the page your harness already shows, one call replies and waits, and nothing here is a browser you have to babysit. The design work is not where it saves time. Setup runs as for any command (`impeccable context`, this reference, craft-floor.md before the edit), and the variants are planned, written, and accepted exactly the way a live session plans, writes, and accepts them.
|
||||
|
||||
Four prohibitions cover the known ways this command goes wrong:
|
||||
Three prohibitions cover the known ways this command goes wrong:
|
||||
|
||||
- **Never run init or document, and never ask for PRODUCT.md or DESIGN.md.** When they exist, the start command prints them under `boot` and you use them. When they do not, it says so (`contextMissing`, `contextNote`) and you extract the identity from the event (Step 3). A missing file is never a reason to interview the user inside this command; offer `init` in one line after the session ends.
|
||||
- **Never hand-write a variants wrapper or invent a session id.** Only the browser mints session ids (8 hex characters, at Go). A missing event is fixed by rerunning Step 2, never with a direct source edit.
|
||||
- **Never declare parameter knobs** (`data-impeccable-params`). A variant is a finished design to choose from; the helper bakes the accepted one mechanically, and knobs would block that. Tunable knobs are plain `live`'s job.
|
||||
- **Do not act on hook findings while live markers are in the file**, and do not restyle variants to appease them; the accept verifies the file once the variant is permanent.
|
||||
|
||||
## Step 1: Parse the request
|
||||
@@ -55,10 +54,10 @@ Run it in the foreground in Cursor and Claude Code (it returns within the wait);
|
||||
- `--target`: the file that renders the element when the request or the project makes it obvious; skip it otherwise.
|
||||
- `--dev-url`: the origin from 1; omit it and the boot probes.
|
||||
- `--selector`: a unique class first, then a landmark tag plus class, an id last (every variant mounts a copy of the element, so an id repeats in the DOM). **The request names a repeated component in plural** ("the pricing cards"): target the container that holds the set, so one scoped stylesheet restyles every instance. One read of the source file that renders the element is allowed when the selector is not obvious; `--dry-run` resolves and reports without starting anything when it is not certain.
|
||||
- `--boot`: runs the lane's boot (context loaded, missing files tolerated, dev URL found, bottom bar hidden for the helper's lifetime) and reuses a helper that is already running. Its result rides along as `boot`.
|
||||
- `--boot`: runs the lane's boot (PRODUCT.md and DESIGN.md loaded again for the helper, missing files tolerated, dev URL found, bottom bar hidden for the helper's lifetime) and reuses a helper that is already running. Its result rides along as `boot`.
|
||||
- Also available: `--prompt`, `--text` (keep only matches whose visible text contains a snippet), `--index` (1-based pick among matches).
|
||||
|
||||
Read the output in this order: `boot.product` / `boot.design` / `boot.surfaceBrief` (or `boot.contextMissing` with `boot.contextNote`: the page is the source of truth, per the note), then `event`, the generate event for `sessionId`, with `_instructions` that carry the whole plan. Every verdict carries `_instructions`, and they win over your recollection of this file; the ones whose move is a decision of yours:
|
||||
Read the output in this order: `boot` (or `boot.contextMissing` with `boot.contextNote`: the page is the source of truth, per the note), then `event`, the generate event for `sessionId`, with the same `_instructions` a user's Go gets. Every verdict carries `_instructions`, and they win over your recollection of this file; the ones whose move is a decision of yours:
|
||||
|
||||
- **`ambiguous`**: the candidates are listed; target their common container, or rerun with `--text "<visible text>"` or `--index <n>`.
|
||||
- **`dev_server_gone`**: the dev server stopped answering while the command waited for the page (on Cursor, a server another chat started dies with that chat). Start it the way the verdict says, then rerun with `--dev-url <url>`.
|
||||
@@ -70,50 +69,23 @@ Done when the output shows `ok: true`, a `sessionId`, and an `event`, reached wi
|
||||
|
||||
## Step 3: Generate
|
||||
|
||||
The event's `_instructions` are the plan: the fast path names the identity sources (the event's `element.computedStyles`, `cssCustomProperties`, and `parentContext`, plus `boot`), the three dimensions your variants vary for this action, and the exact splice. Do it in ONE edit and reply. Concretely:
|
||||
The event is a standard `generate` event: the picked element's context, a preflighted scaffold, and `_instructions` naming the action's reference, the planning section, and the exact splice. Handle it exactly per live.md's **Handle generate**, which owns everything from the identity lock to the done reply: read the action's reference and craft-floor.md as it says, plan per section 4 (identity first, then mode, then three different primary axes, then the squint test), declare knobs per section 7, and deliver per section 6 (a complete replacement of the element per variant, the preview CSS plus every variant in one edit at the scaffold's splice). The lane changes nothing about what a variant may be: the moves a live session would make on this element (a promoted tier, a restructured set, a reordered card, a different surface) are open here too. Never screenshot the page; the overlay preview is the review channel until accept.
|
||||
|
||||
1. **Identity, one sentence, from the event.** Real colors, faces, corners, borders, shadows, and the layout topology on screen. `boot.design` wins when it is present, and it is a boundary, not a mood board: its tokens and named rules hold in every variant. Amplify inside them (a system that forbids fills, shadows, tints, or unequal columns gets its boldest allowed move on that axis, not the forbidden one), and leave the tokens an axis does not need exactly as written (radius, border, padding, the one bold weight). Leaving the system is the user's decision to make afterwards, never a variant's. Never read PRODUCT.md, DESIGN.md, live.md, or craft-floor.md for this; never screenshot the page.
|
||||
2. **The action's reference is optional.** Read `reference/<action>.md` only when the prompt or the element makes the direction unclear; the `_instructions` already carry the action's three dimensions.
|
||||
3. **Write the splice.** The event's `scaffold` tells you where: `sourceWritten: false` hands you `wrapperBlock` and the source range to replace (`replaceStartLine` to `replaceEndLine`); a written wrapper hands you `file` and `insertLine`. Either way, one edit lands the preview CSS plus all variants:
|
||||
|
||||
```html
|
||||
<!-- Variants: insert below this line -->
|
||||
<style data-impeccable-css="SESSION_ID">
|
||||
@scope ([data-impeccable-variant="1"]) { :scope > .pricing { ... } }
|
||||
@scope ([data-impeccable-variant="2"]) { :scope > .pricing { ... } }
|
||||
@scope ([data-impeccable-variant="3"]) { :scope > .pricing { ... } }
|
||||
</style>
|
||||
<div data-impeccable-variant="1"><!-- full element, variant 1 --></div>
|
||||
<div data-impeccable-variant="2" style="display: none"><!-- variant 2 --></div>
|
||||
<div data-impeccable-variant="3" style="display: none"><!-- variant 3 --></div>
|
||||
```
|
||||
|
||||
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 `<style>` content in a template literal, use `className=` and `style={{ display: 'none' }}`, keep `data-impeccable-*` attributes as plain strings.
|
||||
4. **No knobs**, per the prohibition above: `data-impeccable-params`, `data-p-*` selectors, or `var(--p-*)` values turn the mechanical accept into a manual one.
|
||||
5. **Floors, by construction**: body text contrast 4.5:1 or better, no text under 12px, controls at least 40px tall, focus states kept. Do not verify beyond that; the overlay preview is the review channel until accept.
|
||||
6. **Reply and wait in one call**, with the file you wrote:
|
||||
**Reply and wait in one call**, with the file you wrote:
|
||||
|
||||
```bash
|
||||
{{scripts_path}}/impeccable live-poll --reply EVENT_ID done --file src/App.jsx --then-poll
|
||||
```
|
||||
|
||||
This replies done (the browser mounts the variants) and then blocks until the user's choice arrives, so run it the way your harness runs a long poll: **Cursor** in a background terminal with notify on `"type":"(accept|discard|variant_mount_failed|exit)"`; **Claude Code** as a background task; **Codex** in a yielded foreground exec session. Never pass a short `--timeout=`. If the edit fails after the browser flipped to GENERATING, `--reply EVENT_ID error "Short reason"` (without `--then-poll`) so the bar resets.
|
||||
This replies done (the browser mounts the variants) and then blocks until the user's choice arrives, so run it the way your harness runs a long wait: **Claude Code** in the foreground with your tool's longest timeout (600000 ms), so you are paused until the choice arrives; **Codex** in a yielded foreground exec; **Cursor** in a background terminal with notify on `"type":"(accept|discard|variant_mount_failed|exit)"`. Never pass a short `--timeout=`. While it runs there is nothing else to do: never sleep and never poll its output on a timer; a harness that backgrounds it wakes you when it returns. `{"type":"timeout"}` means the user has not chosen yet: run `live-poll` again and keep waiting. If the edit fails after the browser flipped to GENERATING, `--reply EVENT_ID error "Short reason"` (without `--then-poll`) so the bar resets.
|
||||
|
||||
Then tell the user, in one line, where their variants are: *"Three [bolder] variants are live on [the pricing cards]: cycle with the floating bar's arrows and Accept the keeper."*
|
||||
Then tell the user, in one line, where their variants are: *"Three [bolder] variants are live on [the pricing cards]: cycle with the floating bar's arrows, adjust the Tune knobs, and Accept the keeper."*
|
||||
|
||||
Outside the lane, read the matching live.md section before acting: `scaffold.previewMode: "svelte-component"` (Svelte previews are edited as components, and their accept was always mechanical), `mode: "insert"`, `variant_mount_failed`, `steer`, `manual_edit_apply`, and any `fallback: "agent-driven"` wrap error.
|
||||
Outside the replace path, read the matching live.md section before acting: `scaffold.previewMode: "svelte-component"` (Svelte previews are edited as components, and their accept is mechanical), `mode: "insert"`, `variant_mount_failed`, `steer`, `manual_edit_apply`, and any `fallback: "agent-driven"` wrap error.
|
||||
|
||||
## Step 4: Accept and close
|
||||
|
||||
The call from Step 3 returns the user's choice. **`discard`**: nothing to do. **`accept` with `_acceptResult.baked: true`**: the helper already made the variant permanent (its rules were rewritten to real selectors and appended to `_acceptResult.css.file`, or to the page's own `<style>` block; the wrapper is gone; the session is complete): nothing to do, and no `live-complete`. Do not re-read or restyle the file.
|
||||
|
||||
**`accept` with `carbonize: true`** (the bake was not mechanical; `bakeSkipped` says why): finish it by hand in one pass over `_acceptResult.file` and the stylesheet that already owns the element's styling: move the accepted variant's rules into that stylesheet with real selectors (`@scope ([data-impeccable-variant="N"]) { :scope > .x }` becomes `.pricing > .x`), unwrap the element and drop every `data-impeccable-*` attribute, delete the inline `<style>` block and both `impeccable-carbonize` markers, then `{{scripts_path}}/impeccable live-complete --id SESSION_ID` and confirm `phase: "completed"`. Reads before that bake: `_acceptResult.file` and the stylesheet, nothing else.
|
||||
The call from Step 3 returns the user's choice. **`discard`**: nothing to do. **`accept`**: `_acceptResult.carbonize: true` is the normal case, and the cleanup is live.md's **Required after accept**, unchanged: move the accepted variant's rules into the stylesheet that already owns the element with real selectors, bake the chosen knob values in, unwrap the element and drop every `data-impeccable-*` attribute, delete the inline `<style>` block and both `impeccable-carbonize` markers, then `{{scripts_path}}/impeccable live-complete --id SESSION_ID` and confirm `phase: "completed"`. (`baked: true` appears only when the accept was run with `--bake`; then the helper already made the variant permanent and no `live-complete` is owed.)
|
||||
|
||||
Close without being asked, the moment the choice is handled:
|
||||
|
||||
|
||||
@@ -2695,9 +2695,7 @@
|
||||
// then becomes interactive as soon as this variant exposes controls.
|
||||
const visParams = parseVariantParams(getVisibleVariantEl());
|
||||
const hasParams = visParams.length > 0;
|
||||
// A generate-lane session declares no knobs, so it never shows the
|
||||
// pending chip; a user's session keeps it exactly as before.
|
||||
const paramsPending = !hasParams && sessionOrigin !== 'agent' && (parameterGenerationState === 'pending' || parameterGenerationState === 'loading');
|
||||
const paramsPending = !hasParams && (parameterGenerationState === 'pending' || parameterGenerationState === 'loading');
|
||||
if (hasParams || paramsPending) {
|
||||
const tune = el('button', {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
||||
|
||||
@@ -846,15 +846,15 @@ describe('live-browser source contracts', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('never shows a pending Tune chip for a session the generate verb started', () => {
|
||||
// The generate lane declares no knobs, so the chip that spins between
|
||||
// the variants mounting and the done reply is noise there; a user's Go
|
||||
// keeps the chip exactly as before (origin null).
|
||||
it('shows the pending Tune chip for a lane session exactly as for any other', () => {
|
||||
// A Go the generate verb fired plans and declares knobs like a user's Go,
|
||||
// so the chip that spins between the variants mounting and the done
|
||||
// reply is gated on the parameter state and nothing else.
|
||||
assert.equal((SOURCE.match(/parameterGenerationState = 'pending';\s*sessionOrigin = agentTargetForGo \? 'agent' : null;/g) || []).length, 2, 'every Go records who fired it');
|
||||
assert.match(
|
||||
SOURCE,
|
||||
/const paramsPending = !hasParams && sessionOrigin !== 'agent' && \(parameterGenerationState === 'pending' \|\| parameterGenerationState === 'loading'\);/,
|
||||
'the pending chip is gated on the origin and nothing else changed',
|
||||
/const paramsPending = !hasParams && \(parameterGenerationState === 'pending' \|\| parameterGenerationState === 'loading'\);/,
|
||||
'the pending chip is not gated on the origin',
|
||||
);
|
||||
assert.match(SOURCE, /origin: sessionOrigin \|\| undefined,/, 'the origin is saved with the session');
|
||||
assert.match(SOURCE, /sessionOrigin = saved\.origin === 'agent' \? 'agent' : null;/, 'and restored across a reload');
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "Impeccable doctor: <WS>\n\nworth saying (2):\n config-unknown-keys [.impeccable/config.local.json]\n .impeccable/config.local.json has top-level key(s) nothing reads: `nope`. Recognized keys are `hook`, `detector`, `updateCheck`, `stalenessCheck`, `projectRoots`, `buildPath`, `$schema`, `version`.\n → Report the exact keys to the user. A near-miss of a real key is a setting that has never applied.\n config-invalid-build-path [.impeccable/config.local.json]\n .impeccable/config.local.json sets `buildPath` to \"maybe\", which nothing reads. The values are `comp` and `code`.\n → Report the value. An unread `buildPath` does not fall back to the other path; it falls back to the default, so a project meaning `code` has been building comp-led.\n\n",
|
||||
"stdout": "Impeccable doctor: <WS>\n\nworth saying (2):\n config-unknown-keys [.impeccable/config.local.json]\n .impeccable/config.local.json has top-level key(s) nothing reads: `nope`. Recognized keys are `hook`, `detector`, `updateCheck`, `stalenessCheck`, `projectRoots`, `buildPath`, `browser`, `$schema`, `version`.\n → Report the exact keys to the user. A near-miss of a real key is a setting that has never applied.\n config-invalid-build-path [.impeccable/config.local.json]\n .impeccable/config.local.json sets `buildPath` to \"maybe\", which nothing reads. The values are `comp` and `code`.\n → Report the value. An unread `buildPath` does not fall back to the other path; it falls back to the default, so a project meaning `code` has been building comp-led.\n\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "Impeccable doctor: <WS>\n\nneeds a command (1):\n product-schema-legacy [PRODUCT.md]\n PRODUCT.md has no schema stamp and none of the sections the current record adds (Positioning, Operating Context, Evidence on Hand, Product Principles), so it predates this version of the product record.\n → Offer `init`, which preserves confirmed answers and fills the gaps by interview. Do not rewrite the file from inference.\n\nworth saying (8):\n product-deprecated-register [PRODUCT.md]\n PRODUCT.md still carries a `## Register` section. v4 replaced the brand/product register axis with the four visitor modes (Persuade, Operate, Read, Experience), which are chosen per surface and persisted in that surface's brief. Nothing reads `## Register` any more.\n → Treat `## Register` as absent for every decision this session. Offer to delete the section; do not let its value influence the work either way.\n design-md-coverage [DESIGN.md]\n DESIGN.md has no components section. Agents generating new screens get no normative guidance for those, and the live design panel renders generic approximations in their place.\n → Ask whether the section never applied or was never written. `document` fills it from the code if the project has the answer in its CSS.\n config-unknown-keys [.impeccable/config.json]\n .impeccable/config.json has top-level key(s) nothing reads: `theme`. Recognized keys are `hook`, `detector`, `updateCheck`, `stalenessCheck`, `projectRoots`, `buildPath`, `$schema`, `version`.\n → Report the exact keys to the user. A near-miss of a real key is a setting that has never applied.\n config-invalid-build-path [.impeccable/config.json]\n .impeccable/config.json sets `buildPath` to \"fast\", which nothing reads. The values are `comp` and `code`.\n → Report the value. An unread `buildPath` does not fall back to the other path; it falls back to the default, so a project meaning `code` has been building comp-led.\n config-unknown-detector-keys [.impeccable/config.json]\n .impeccable/config.json has `detector` key(s) nothing reads: `mode`. Recognized keys are `ignoreRules`, `ignoreFiles`, `ignoreValues`, `designSystem`, `extensions`.\n → Report the exact keys. `ignoreRule` for `ignoreRules` is the common one, and it silences nothing.\n detector-ignore-rules-unknown [.impeccable/config.json]\n .impeccable/config.json ignores rule id(s) the detector does not have: `not-a-real-rule`. Either the rule was renamed or removed, or the id was mistyped and has never suppressed anything.\n → Report the exact ids. Removing them is safe; keeping a dead ignore hides that the rule is gone.\n detector-ignore-files-missing [.impeccable/config.json]\n .impeccable/config.json ignores file path(s) that no longer exist: `src/vendor/missing.css`.\n → Ask whether the file moved (repoint the entry) or was deleted (drop it). A stale entry silently stops covering the file that replaced it.\n surface-brief-orphaned [.impeccable/surfaces/src-old-astro.md]\n 1 persisted surface brief(s) name a primary target that no longer exists: .impeccable/surfaces/src-old-astro.md → src/old.astro.\n → Ask whether the surface moved (repoint the brief) or was removed (delete the brief). Until then the brief is authority for a file that is gone.\n\nautomatic (1):\n legacy-live-state [.impeccable-live.json]\n Live-mode state sits in retired location(s): `.impeccable-live.json`. Current live mode writes under `.impeccable/live/`.\n → These are read only through backward-compatible fallbacks and are safe to delete once no live session is running. No user decision is needed.\n\nApplied nothing.\nLeft alone:\n legacy-live-state: delete by hand once no live session is running\n",
|
||||
"stdout": "Impeccable doctor: <WS>\n\nneeds a command (1):\n product-schema-legacy [PRODUCT.md]\n PRODUCT.md has no schema stamp and none of the sections the current record adds (Positioning, Operating Context, Evidence on Hand, Product Principles), so it predates this version of the product record.\n → Offer `init`, which preserves confirmed answers and fills the gaps by interview. Do not rewrite the file from inference.\n\nworth saying (8):\n product-deprecated-register [PRODUCT.md]\n PRODUCT.md still carries a `## Register` section. v4 replaced the brand/product register axis with the four visitor modes (Persuade, Operate, Read, Experience), which are chosen per surface and persisted in that surface's brief. Nothing reads `## Register` any more.\n → Treat `## Register` as absent for every decision this session. Offer to delete the section; do not let its value influence the work either way.\n design-md-coverage [DESIGN.md]\n DESIGN.md has no components section. Agents generating new screens get no normative guidance for those, and the live design panel renders generic approximations in their place.\n → Ask whether the section never applied or was never written. `document` fills it from the code if the project has the answer in its CSS.\n config-unknown-keys [.impeccable/config.json]\n .impeccable/config.json has top-level key(s) nothing reads: `theme`. Recognized keys are `hook`, `detector`, `updateCheck`, `stalenessCheck`, `projectRoots`, `buildPath`, `browser`, `$schema`, `version`.\n → Report the exact keys to the user. A near-miss of a real key is a setting that has never applied.\n config-invalid-build-path [.impeccable/config.json]\n .impeccable/config.json sets `buildPath` to \"fast\", which nothing reads. The values are `comp` and `code`.\n → Report the value. An unread `buildPath` does not fall back to the other path; it falls back to the default, so a project meaning `code` has been building comp-led.\n config-unknown-detector-keys [.impeccable/config.json]\n .impeccable/config.json has `detector` key(s) nothing reads: `mode`. Recognized keys are `ignoreRules`, `ignoreFiles`, `ignoreValues`, `designSystem`, `extensions`.\n → Report the exact keys. `ignoreRule` for `ignoreRules` is the common one, and it silences nothing.\n detector-ignore-rules-unknown [.impeccable/config.json]\n .impeccable/config.json ignores rule id(s) the detector does not have: `not-a-real-rule`. Either the rule was renamed or removed, or the id was mistyped and has never suppressed anything.\n → Report the exact ids. Removing them is safe; keeping a dead ignore hides that the rule is gone.\n detector-ignore-files-missing [.impeccable/config.json]\n .impeccable/config.json ignores file path(s) that no longer exist: `src/vendor/missing.css`.\n → Ask whether the file moved (repoint the entry) or was deleted (drop it). A stale entry silently stops covering the file that replaced it.\n surface-brief-orphaned [.impeccable/surfaces/src-old-astro.md]\n 1 persisted surface brief(s) name a primary target that no longer exists: .impeccable/surfaces/src-old-astro.md → src/old.astro.\n → Ask whether the surface moved (repoint the brief) or was removed (delete the brief). Until then the brief is authority for a file that is gone.\n\nautomatic (1):\n legacy-live-state [.impeccable-live.json]\n Live-mode state sits in retired location(s): `.impeccable-live.json`. Current live mode writes under `.impeccable/live/`.\n → These are read only through backward-compatible fallbacks and are safe to delete once no live session is running. No user decision is needed.\n\nApplied nothing.\nLeft alone:\n legacy-live-state: delete by hand once no live session is running\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "Impeccable doctor: <WS>\n\nneeds a command (2):\n product-schema-legacy [PRODUCT.md]\n PRODUCT.md has no schema stamp and none of the sections the current record adds (Positioning, Operating Context, Evidence on Hand, Product Principles), so it predates this version of the product record.\n → Offer `init`, which preserves confirmed answers and fills the gaps by interview. Do not rewrite the file from inference.\n design-sidecar-schema-outdated [DESIGN.json]\n DESIGN.json is schemaVersion 1; the current sidecar is 2. Token primitives moved to the DESIGN.md frontmatter, so the old shape carries values that are now read from two places.\n → Offer `document` to regenerate the sidecar. It reads the existing DESIGN.md, so no interview is needed.\n\nworth saying (9):\n product-deprecated-register [PRODUCT.md]\n PRODUCT.md still carries a `## Register` section. v4 replaced the brand/product register axis with the four visitor modes (Persuade, Operate, Read, Experience), which are chosen per surface and persisted in that surface's brief. Nothing reads `## Register` any more.\n → Treat `## Register` as absent for every decision this session. Offer to delete the section; do not let its value influence the work either way.\n design-sidecar-stale [DESIGN.json]\n DESIGN.md was edited after DESIGN.json was generated, so the sidecar's ramps, shadows, motion tokens, and component snippets may contradict it.\n → Offer `document` to refresh the sidecar, preserving DESIGN.md.\n design-md-coverage [DESIGN.md]\n DESIGN.md has no components section. Agents generating new screens get no normative guidance for those, and the live design panel renders generic approximations in their place.\n → Ask whether the section never applied or was never written. `document` fills it from the code if the project has the answer in its CSS.\n config-unknown-keys [.impeccable/config.json]\n .impeccable/config.json has top-level key(s) nothing reads: `theme`. Recognized keys are `hook`, `detector`, `updateCheck`, `stalenessCheck`, `projectRoots`, `buildPath`, `$schema`, `version`.\n → Report the exact keys to the user. A near-miss of a real key is a setting that has never applied.\n config-invalid-build-path [.impeccable/config.json]\n .impeccable/config.json sets `buildPath` to \"fast\", which nothing reads. The values are `comp` and `code`.\n → Report the value. An unread `buildPath` does not fall back to the other path; it falls back to the default, so a project meaning `code` has been building comp-led.\n config-unknown-detector-keys [.impeccable/config.json]\n .impeccable/config.json has `detector` key(s) nothing reads: `mode`. Recognized keys are `ignoreRules`, `ignoreFiles`, `ignoreValues`, `designSystem`, `extensions`.\n → Report the exact keys. `ignoreRule` for `ignoreRules` is the common one, and it silences nothing.\n detector-ignore-rules-unknown [.impeccable/config.json]\n .impeccable/config.json ignores rule id(s) the detector does not have: `not-a-real-rule`. Either the rule was renamed or removed, or the id was mistyped and has never suppressed anything.\n → Report the exact ids. Removing them is safe; keeping a dead ignore hides that the rule is gone.\n detector-ignore-files-missing [.impeccable/config.json]\n .impeccable/config.json ignores file path(s) that no longer exist: `src/vendor/missing.css`.\n → Ask whether the file moved (repoint the entry) or was deleted (drop it). A stale entry silently stops covering the file that replaced it.\n surface-brief-orphaned [.impeccable/surfaces/src-old-astro.md]\n 1 persisted surface brief(s) name a primary target that no longer exists: .impeccable/surfaces/src-old-astro.md → src/old.astro.\n → Ask whether the surface moved (repoint the brief) or was removed (delete the brief). Until then the brief is authority for a file that is gone.\n\nautomatic (2):\n design-sidecar-legacy-path [DESIGN.json]\n The design sidecar sits at DESIGN.json, a location kept only for backward compatibility.\n → Move it to .impeccable/design.json the next time the sidecar is written. No user decision is needed.\n legacy-live-state [.impeccable-live.json]\n Live-mode state sits in retired location(s): `.impeccable-live.json`. Current live mode writes under `.impeccable/live/`.\n → These are read only through backward-compatible fallbacks and are safe to delete once no live session is running. No user decision is needed.\n\nApplied:\n Moved DESIGN.json to .impeccable/design.json.\nLeft alone:\n legacy-live-state: delete by hand once no live session is running\n",
|
||||
"stdout": "Impeccable doctor: <WS>\n\nneeds a command (2):\n product-schema-legacy [PRODUCT.md]\n PRODUCT.md has no schema stamp and none of the sections the current record adds (Positioning, Operating Context, Evidence on Hand, Product Principles), so it predates this version of the product record.\n → Offer `init`, which preserves confirmed answers and fills the gaps by interview. Do not rewrite the file from inference.\n design-sidecar-schema-outdated [DESIGN.json]\n DESIGN.json is schemaVersion 1; the current sidecar is 2. Token primitives moved to the DESIGN.md frontmatter, so the old shape carries values that are now read from two places.\n → Offer `document` to regenerate the sidecar. It reads the existing DESIGN.md, so no interview is needed.\n\nworth saying (9):\n product-deprecated-register [PRODUCT.md]\n PRODUCT.md still carries a `## Register` section. v4 replaced the brand/product register axis with the four visitor modes (Persuade, Operate, Read, Experience), which are chosen per surface and persisted in that surface's brief. Nothing reads `## Register` any more.\n → Treat `## Register` as absent for every decision this session. Offer to delete the section; do not let its value influence the work either way.\n design-sidecar-stale [DESIGN.json]\n DESIGN.md was edited after DESIGN.json was generated, so the sidecar's ramps, shadows, motion tokens, and component snippets may contradict it.\n → Offer `document` to refresh the sidecar, preserving DESIGN.md.\n design-md-coverage [DESIGN.md]\n DESIGN.md has no components section. Agents generating new screens get no normative guidance for those, and the live design panel renders generic approximations in their place.\n → Ask whether the section never applied or was never written. `document` fills it from the code if the project has the answer in its CSS.\n config-unknown-keys [.impeccable/config.json]\n .impeccable/config.json has top-level key(s) nothing reads: `theme`. Recognized keys are `hook`, `detector`, `updateCheck`, `stalenessCheck`, `projectRoots`, `buildPath`, `browser`, `$schema`, `version`.\n → Report the exact keys to the user. A near-miss of a real key is a setting that has never applied.\n config-invalid-build-path [.impeccable/config.json]\n .impeccable/config.json sets `buildPath` to \"fast\", which nothing reads. The values are `comp` and `code`.\n → Report the value. An unread `buildPath` does not fall back to the other path; it falls back to the default, so a project meaning `code` has been building comp-led.\n config-unknown-detector-keys [.impeccable/config.json]\n .impeccable/config.json has `detector` key(s) nothing reads: `mode`. Recognized keys are `ignoreRules`, `ignoreFiles`, `ignoreValues`, `designSystem`, `extensions`.\n → Report the exact keys. `ignoreRule` for `ignoreRules` is the common one, and it silences nothing.\n detector-ignore-rules-unknown [.impeccable/config.json]\n .impeccable/config.json ignores rule id(s) the detector does not have: `not-a-real-rule`. Either the rule was renamed or removed, or the id was mistyped and has never suppressed anything.\n → Report the exact ids. Removing them is safe; keeping a dead ignore hides that the rule is gone.\n detector-ignore-files-missing [.impeccable/config.json]\n .impeccable/config.json ignores file path(s) that no longer exist: `src/vendor/missing.css`.\n → Ask whether the file moved (repoint the entry) or was deleted (drop it). A stale entry silently stops covering the file that replaced it.\n surface-brief-orphaned [.impeccable/surfaces/src-old-astro.md]\n 1 persisted surface brief(s) name a primary target that no longer exists: .impeccable/surfaces/src-old-astro.md → src/old.astro.\n → Ask whether the surface moved (repoint the brief) or was removed (delete the brief). Until then the brief is authority for a file that is gone.\n\nautomatic (2):\n design-sidecar-legacy-path [DESIGN.json]\n The design sidecar sits at DESIGN.json, a location kept only for backward compatibility.\n → Move it to .impeccable/design.json the next time the sidecar is written. No user decision is needed.\n legacy-live-state [.impeccable-live.json]\n Live-mode state sits in retired location(s): `.impeccable-live.json`. Current live mode writes under `.impeccable/live/`.\n → These are read only through backward-compatible fallbacks and are safe to delete once no live session is running. No user decision is needed.\n\nApplied:\n Moved DESIGN.json to .impeccable/design.json.\nLeft alone:\n legacy-live-state: delete by hand once no live session is running\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "Impeccable doctor: <WS>\n\nneeds a command (2):\n product-schema-legacy [PRODUCT.md]\n PRODUCT.md has no schema stamp and none of the sections the current record adds (Positioning, Operating Context, Evidence on Hand, Product Principles), so it predates this version of the product record.\n → Offer `init`, which preserves confirmed answers and fills the gaps by interview. Do not rewrite the file from inference.\n design-sidecar-schema-outdated [DESIGN.json]\n DESIGN.json is schemaVersion 1; the current sidecar is 2. Token primitives moved to the DESIGN.md frontmatter, so the old shape carries values that are now read from two places.\n → Offer `document` to regenerate the sidecar. It reads the existing DESIGN.md, so no interview is needed.\n\nworth saying (9):\n product-deprecated-register [PRODUCT.md]\n PRODUCT.md still carries a `## Register` section. v4 replaced the brand/product register axis with the four visitor modes (Persuade, Operate, Read, Experience), which are chosen per surface and persisted in that surface's brief. Nothing reads `## Register` any more.\n → Treat `## Register` as absent for every decision this session. Offer to delete the section; do not let its value influence the work either way.\n design-sidecar-stale [DESIGN.json]\n DESIGN.md was edited after DESIGN.json was generated, so the sidecar's ramps, shadows, motion tokens, and component snippets may contradict it.\n → Offer `document` to refresh the sidecar, preserving DESIGN.md.\n design-md-coverage [DESIGN.md]\n DESIGN.md has no components section. Agents generating new screens get no normative guidance for those, and the live design panel renders generic approximations in their place.\n → Ask whether the section never applied or was never written. `document` fills it from the code if the project has the answer in its CSS.\n config-unknown-keys [.impeccable/config.json]\n .impeccable/config.json has top-level key(s) nothing reads: `theme`. Recognized keys are `hook`, `detector`, `updateCheck`, `stalenessCheck`, `projectRoots`, `buildPath`, `$schema`, `version`.\n → Report the exact keys to the user. A near-miss of a real key is a setting that has never applied.\n config-invalid-build-path [.impeccable/config.json]\n .impeccable/config.json sets `buildPath` to \"fast\", which nothing reads. The values are `comp` and `code`.\n → Report the value. An unread `buildPath` does not fall back to the other path; it falls back to the default, so a project meaning `code` has been building comp-led.\n config-unknown-detector-keys [.impeccable/config.json]\n .impeccable/config.json has `detector` key(s) nothing reads: `mode`. Recognized keys are `ignoreRules`, `ignoreFiles`, `ignoreValues`, `designSystem`, `extensions`.\n → Report the exact keys. `ignoreRule` for `ignoreRules` is the common one, and it silences nothing.\n detector-ignore-rules-unknown [.impeccable/config.json]\n .impeccable/config.json ignores rule id(s) the detector does not have: `not-a-real-rule`. Either the rule was renamed or removed, or the id was mistyped and has never suppressed anything.\n → Report the exact ids. Removing them is safe; keeping a dead ignore hides that the rule is gone.\n detector-ignore-files-missing [.impeccable/config.json]\n .impeccable/config.json ignores file path(s) that no longer exist: `src/vendor/missing.css`.\n → Ask whether the file moved (repoint the entry) or was deleted (drop it). A stale entry silently stops covering the file that replaced it.\n surface-brief-orphaned [.impeccable/surfaces/src-old-astro.md]\n 1 persisted surface brief(s) name a primary target that no longer exists: .impeccable/surfaces/src-old-astro.md → src/old.astro.\n → Ask whether the surface moved (repoint the brief) or was removed (delete the brief). Until then the brief is authority for a file that is gone.\n\nautomatic (2):\n design-sidecar-legacy-path [DESIGN.json]\n The design sidecar sits at DESIGN.json, a location kept only for backward compatibility.\n → Move it to .impeccable/design.json the next time the sidecar is written. No user decision is needed.\n legacy-live-state [.impeccable-live.json]\n Live-mode state sits in retired location(s): `.impeccable-live.json`. Current live mode writes under `.impeccable/live/`.\n → These are read only through backward-compatible fallbacks and are safe to delete once no live session is running. No user decision is needed.\n\nRun `<IMPECCABLE> doctor --fix` to apply the automatic migrations, or `/impeccable doctor` to work through all of them.\n",
|
||||
"stdout": "Impeccable doctor: <WS>\n\nneeds a command (2):\n product-schema-legacy [PRODUCT.md]\n PRODUCT.md has no schema stamp and none of the sections the current record adds (Positioning, Operating Context, Evidence on Hand, Product Principles), so it predates this version of the product record.\n → Offer `init`, which preserves confirmed answers and fills the gaps by interview. Do not rewrite the file from inference.\n design-sidecar-schema-outdated [DESIGN.json]\n DESIGN.json is schemaVersion 1; the current sidecar is 2. Token primitives moved to the DESIGN.md frontmatter, so the old shape carries values that are now read from two places.\n → Offer `document` to regenerate the sidecar. It reads the existing DESIGN.md, so no interview is needed.\n\nworth saying (9):\n product-deprecated-register [PRODUCT.md]\n PRODUCT.md still carries a `## Register` section. v4 replaced the brand/product register axis with the four visitor modes (Persuade, Operate, Read, Experience), which are chosen per surface and persisted in that surface's brief. Nothing reads `## Register` any more.\n → Treat `## Register` as absent for every decision this session. Offer to delete the section; do not let its value influence the work either way.\n design-sidecar-stale [DESIGN.json]\n DESIGN.md was edited after DESIGN.json was generated, so the sidecar's ramps, shadows, motion tokens, and component snippets may contradict it.\n → Offer `document` to refresh the sidecar, preserving DESIGN.md.\n design-md-coverage [DESIGN.md]\n DESIGN.md has no components section. Agents generating new screens get no normative guidance for those, and the live design panel renders generic approximations in their place.\n → Ask whether the section never applied or was never written. `document` fills it from the code if the project has the answer in its CSS.\n config-unknown-keys [.impeccable/config.json]\n .impeccable/config.json has top-level key(s) nothing reads: `theme`. Recognized keys are `hook`, `detector`, `updateCheck`, `stalenessCheck`, `projectRoots`, `buildPath`, `browser`, `$schema`, `version`.\n → Report the exact keys to the user. A near-miss of a real key is a setting that has never applied.\n config-invalid-build-path [.impeccable/config.json]\n .impeccable/config.json sets `buildPath` to \"fast\", which nothing reads. The values are `comp` and `code`.\n → Report the value. An unread `buildPath` does not fall back to the other path; it falls back to the default, so a project meaning `code` has been building comp-led.\n config-unknown-detector-keys [.impeccable/config.json]\n .impeccable/config.json has `detector` key(s) nothing reads: `mode`. Recognized keys are `ignoreRules`, `ignoreFiles`, `ignoreValues`, `designSystem`, `extensions`.\n → Report the exact keys. `ignoreRule` for `ignoreRules` is the common one, and it silences nothing.\n detector-ignore-rules-unknown [.impeccable/config.json]\n .impeccable/config.json ignores rule id(s) the detector does not have: `not-a-real-rule`. Either the rule was renamed or removed, or the id was mistyped and has never suppressed anything.\n → Report the exact ids. Removing them is safe; keeping a dead ignore hides that the rule is gone.\n detector-ignore-files-missing [.impeccable/config.json]\n .impeccable/config.json ignores file path(s) that no longer exist: `src/vendor/missing.css`.\n → Ask whether the file moved (repoint the entry) or was deleted (drop it). A stale entry silently stops covering the file that replaced it.\n surface-brief-orphaned [.impeccable/surfaces/src-old-astro.md]\n 1 persisted surface brief(s) name a primary target that no longer exists: .impeccable/surfaces/src-old-astro.md → src/old.astro.\n → Ask whether the surface moved (repoint the brief) or was removed (delete the brief). Until then the brief is authority for a file that is gone.\n\nautomatic (2):\n design-sidecar-legacy-path [DESIGN.json]\n The design sidecar sits at DESIGN.json, a location kept only for backward compatibility.\n → Move it to .impeccable/design.json the next time the sidecar is written. No user decision is needed.\n legacy-live-state [.impeccable-live.json]\n Live-mode state sits in retired location(s): `.impeccable-live.json`. Current live mode writes under `.impeccable/live/`.\n → These are read only through backward-compatible fallbacks and are safe to delete once no live session is running. No user decision is needed.\n\nRun `<IMPECCABLE> doctor --fix` to apply the automatic migrations, or `/impeccable doctor` to work through all of them.\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "{\n \"projectRoot\": \"<WS>\",\n \"repoRoot\": \"<WS>\",\n \"isMonorepo\": false,\n \"productPath\": \"PRODUCT.md\",\n \"designPath\": \"DESIGN.md\",\n \"platform\": null,\n \"ruleRegistryAvailable\": true,\n \"findings\": [\n {\n \"id\": \"product-deprecated-register\",\n \"artifact\": \"PRODUCT.md\",\n \"path\": \"PRODUCT.md\",\n \"severity\": \"mention\",\n \"summary\": \"PRODUCT.md still carries a `## Register` section. v4 replaced the brand/product register axis with the four visitor modes (Persuade, Operate, Read, Experience), which are chosen per surface and persisted in that surface's brief. Nothing reads `## Register` any more.\",\n \"fix\": \"Treat `## Register` as absent for every decision this session. Offer to delete the section; do not let its value influence the work either way.\"\n },\n {\n \"id\": \"product-schema-legacy\",\n \"artifact\": \"PRODUCT.md\",\n \"path\": \"PRODUCT.md\",\n \"severity\": \"route\",\n \"summary\": \"PRODUCT.md has no schema stamp and none of the sections the current record adds (Positioning, Operating Context, Evidence on Hand, Product Principles), so it predates this version of the product record.\",\n \"fix\": \"Offer `init`, which preserves confirmed answers and fills the gaps by interview. Do not rewrite the file from inference.\"\n },\n {\n \"id\": \"design-sidecar-schema-outdated\",\n \"artifact\": \"design.json\",\n \"path\": \".impeccable/design.json\",\n \"severity\": \"route\",\n \"summary\": \".impeccable/design.json is schemaVersion 1; the current sidecar is 2. Token primitives moved to the DESIGN.md frontmatter, so the old shape carries values that are now read from two places.\",\n \"fix\": \"Offer `document` to regenerate the sidecar. It reads the existing DESIGN.md, so no interview is needed.\"\n },\n {\n \"id\": \"design-md-coverage\",\n \"artifact\": \"DESIGN.md\",\n \"path\": \"DESIGN.md\",\n \"severity\": \"mention\",\n \"summary\": \"DESIGN.md has no colors, typography, components section. Agents generating new screens get no normative guidance for those, and the live design panel renders generic approximations in their place.\",\n \"fix\": \"Ask whether the section never applied or was never written. `document` fills it from the code if the project has the answer in its CSS.\"\n },\n {\n \"id\": \"config-unknown-keys\",\n \"artifact\": \"config.json\",\n \"path\": \".impeccable/config.json\",\n \"severity\": \"mention\",\n \"summary\": \".impeccable/config.json has top-level key(s) nothing reads: `unknownSetting`. Recognized keys are `hook`, `detector`, `updateCheck`, `stalenessCheck`, `projectRoots`, `buildPath`, `$schema`, `version`.\",\n \"fix\": \"Report the exact keys to the user. A near-miss of a real key is a setting that has never applied.\"\n }\n ],\n \"workspaces\": []\n}\n",
|
||||
"stdout": "{\n \"projectRoot\": \"<WS>\",\n \"repoRoot\": \"<WS>\",\n \"isMonorepo\": false,\n \"productPath\": \"PRODUCT.md\",\n \"designPath\": \"DESIGN.md\",\n \"platform\": null,\n \"ruleRegistryAvailable\": true,\n \"findings\": [\n {\n \"id\": \"product-deprecated-register\",\n \"artifact\": \"PRODUCT.md\",\n \"path\": \"PRODUCT.md\",\n \"severity\": \"mention\",\n \"summary\": \"PRODUCT.md still carries a `## Register` section. v4 replaced the brand/product register axis with the four visitor modes (Persuade, Operate, Read, Experience), which are chosen per surface and persisted in that surface's brief. Nothing reads `## Register` any more.\",\n \"fix\": \"Treat `## Register` as absent for every decision this session. Offer to delete the section; do not let its value influence the work either way.\"\n },\n {\n \"id\": \"product-schema-legacy\",\n \"artifact\": \"PRODUCT.md\",\n \"path\": \"PRODUCT.md\",\n \"severity\": \"route\",\n \"summary\": \"PRODUCT.md has no schema stamp and none of the sections the current record adds (Positioning, Operating Context, Evidence on Hand, Product Principles), so it predates this version of the product record.\",\n \"fix\": \"Offer `init`, which preserves confirmed answers and fills the gaps by interview. Do not rewrite the file from inference.\"\n },\n {\n \"id\": \"design-sidecar-schema-outdated\",\n \"artifact\": \"design.json\",\n \"path\": \".impeccable/design.json\",\n \"severity\": \"route\",\n \"summary\": \".impeccable/design.json is schemaVersion 1; the current sidecar is 2. Token primitives moved to the DESIGN.md frontmatter, so the old shape carries values that are now read from two places.\",\n \"fix\": \"Offer `document` to regenerate the sidecar. It reads the existing DESIGN.md, so no interview is needed.\"\n },\n {\n \"id\": \"design-md-coverage\",\n \"artifact\": \"DESIGN.md\",\n \"path\": \"DESIGN.md\",\n \"severity\": \"mention\",\n \"summary\": \"DESIGN.md has no colors, typography, components section. Agents generating new screens get no normative guidance for those, and the live design panel renders generic approximations in their place.\",\n \"fix\": \"Ask whether the section never applied or was never written. `document` fills it from the code if the project has the answer in its CSS.\"\n },\n {\n \"id\": \"config-unknown-keys\",\n \"artifact\": \"config.json\",\n \"path\": \".impeccable/config.json\",\n \"severity\": \"mention\",\n \"summary\": \".impeccable/config.json has top-level key(s) nothing reads: `unknownSetting`. Recognized keys are `hook`, `detector`, `updateCheck`, `stalenessCheck`, `projectRoots`, `buildPath`, `browser`, `$schema`, `version`.\",\n \"fix\": \"Report the exact keys to the user. A near-miss of a real key is a setting that has never applied.\"\n }\n ],\n \"workspaces\": []\n}\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "Usage: impeccable live-accept [options]\n\nDeterministic accept/discard for live variant sessions.\n\nModes:\n --discard Remove variants, restore original\n --variant N Accept variant N, discard the rest\n\nRequired:\n --id SESSION_ID Session ID of the variant wrapper\n\nOptions:\n --page-url URL Current browser page URL; scopes staged copy-edit cleanup\n --bake Bake a knob-free HTML/JSX accept mechanically (rules to the\n owning stylesheet, wrapper unwrapped) instead of leaving\n the carbonize block; the default for sessions the\n generate verb started (origin \"agent\")\n --no-bake Never bake; always leave the carbonize block\n --defer-source-write\n Deprecated compatibility flag. Svelte component accepts\n now write the real source immediately.\n\nOutput (JSON):\n { handled, file, carbonize, baked?, css?, bakeSkipped? }\n",
|
||||
"stdout": "Usage: impeccable live-accept [options]\n\nDeterministic accept/discard for live variant sessions.\n\nModes:\n --discard Remove variants, restore original\n --variant N Accept variant N, discard the rest\n\nRequired:\n --id SESSION_ID Session ID of the variant wrapper\n\nOptions:\n --page-url URL Current browser page URL; scopes staged copy-edit cleanup\n --bake Bake a knob-free HTML/JSX accept mechanically (rules to the\n owning stylesheet, wrapper unwrapped) instead of leaving\n the carbonize block; opt-in, never the default\n --no-bake Never bake; always leave the carbonize block\n --defer-source-write\n Deprecated compatibility flag. Svelte component accepts\n now write the real source immediately.\n\nOutput (JSON):\n { handled, file, carbonize, baked?, css?, bakeSkipped? }\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
|
||||
Reference in New Issue
Block a user