Make the generate lane snappy: self-contained playbook, fast-path poll instructions

The maintainer's field run took five and a half minutes from the prompt
to variants on screen. Two baseline runs on the same repo reproduced it
(356 s mean): 68 KB of skill text read before the first variant (a 36 KB
live.md among it), six to ten tool calls spent finding the dev URL and
the selector, 9 to 10 KB of variants carrying tune knobs, and a document
read plus a detect pass after the accept.

generate.md is now the whole contract for the lane and never sends the
agent to live.md, craft-floor.md, or the action reference on the happy
path; the floors are inlined. The engine carries the rest: a generate
started by live-generate is journaled and queued with origin "agent",
and its poll instructions hand out the fast path (identity from the
event's computed styles and custom properties, the action's three
dimensions, no knobs unless asked, one edit, reply done) instead of the
interactive planning pointer. `impeccable live --allow-missing-context`
boots without PRODUCT.md or DESIGN.md, naming what is missing, so the
lane never falls into the init interview; the boot also reports devUrl,
the origin whose page carries the injected tag, so the agent opens the
page instead of reading terminals. Accept is a bake and live-complete is
its verification: no detect pass, no document read.

Three trimmed runs (one without any context files) averaged 179 s from
prompt to variants, 21 tool calls and 106k tokens against the baseline's
356 s, 30 tool calls and 144k tokens; the accept bake went from 67 s to
41 s. Method and numbers: tmp/questionaire/plan41-field-tests/SNAPPY-REPORT.md
in the maintainer's checkout.

Tests: dev_url probe unit tests, a fast-path instructions unit test, the
origin marker in the protocol suite, and tests/live-boot-fastpath.test.mjs
(flag, contextMissing, devUrl through a stand-in dev server); contract doc
updated.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abdul Wahab
2026-09-15 05:45:49 +05:00
committed by Abdul Wahab
co-authored by Claude Fable 5
parent d579ecb2f2
commit 1220f26d08
13 changed files with 411 additions and 34 deletions
+6
View File
@@ -124,8 +124,14 @@ struct Server {
token: String,
}
/// Server spawns are serialized: seventeen binaries starting at once on a
/// loaded machine have missed even a 30 s pid-file wait, while the tests
/// themselves still run in parallel once their server is up.
static START_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
impl Server {
fn start(tag: &str) -> Server {
let _serialized = START_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let dir = std::env::temp_dir().join(format!("impeccable-agent-target-{}-{}", tag, std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join(".impeccable/live")).unwrap();
+124
View File
@@ -0,0 +1,124 @@
//! Find the dev server that is serving this app right now: the page that
//! carries our injected `live.js?token=<token>` tag is ours, whatever port
//! it answers on. Saves the agent a terminal-reading detour before it can
//! open the page.
use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
/// Ports worth a knock when nothing narrows the search: Vite, Next, Astro,
/// SvelteKit, Nuxt, CRA, Angular, and the usual static servers.
const DEFAULT_PORTS: &[u16] = &[5173, 3000, 4321, 8080, 4173, 3001, 5174, 8000, 4200, 5000, 1234];
/// Candidate origins, in probe order. `IMPECCABLE_DEV_URL_CANDIDATES`
/// (comma-separated) replaces the default list, for tests and unusual hosts.
pub fn candidates(env_override: Option<&str>) -> Vec<String> {
if let Some(list) = env_override {
return list
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|s| s.trim_end_matches('/').to_string() + "/")
.collect();
}
let mut out = Vec::new();
for port in DEFAULT_PORTS {
out.push(format!("http://127.0.0.1:{}/", port));
out.push(format!("http://localhost:{}/", port));
}
out
}
/// The first candidate whose document contains our tag, probed in parallel
/// with short timeouts so a full miss costs well under a second.
pub fn probe(candidates: &[String], token: &str) -> Option<String> {
let needle = format!("live.js?token={}", token);
let hits: Vec<Option<String>> = std::thread::scope(|scope| {
let handles: Vec<_> = candidates
.iter()
.map(|url| {
let needle = needle.clone();
scope.spawn(move || fetch_root(url).filter(|body| body.contains(&needle)).map(|_| url.clone()))
})
.collect();
handles.into_iter().map(|h| h.join().unwrap_or(None)).collect()
});
hits.into_iter().flatten().next()
}
/// A minimal HTTP/1.0 GET of `/`; returns the response body on any 2xx.
fn fetch_root(url: &str) -> Option<String> {
let rest = url.strip_prefix("http://")?;
let host_port = rest.split('/').next()?;
let (host, port) = match host_port.rsplit_once(':') {
Some((h, p)) => (h, p.parse::<u16>().ok()?),
None => (host_port, 80),
};
let addr = (host, port).to_socket_addrs().ok()?.next()?;
let mut stream = TcpStream::connect_timeout(&addr, Duration::from_millis(300)).ok()?;
stream.set_read_timeout(Some(Duration::from_millis(1500))).ok()?;
stream.set_write_timeout(Some(Duration::from_millis(300))).ok()?;
stream
.write_all(format!("GET / HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n\r\n", host_port).as_bytes())
.ok()?;
let mut raw = Vec::new();
let mut buf = [0u8; 8192];
while raw.len() < 512 * 1024 {
match stream.read(&mut buf) {
Ok(0) => break,
Ok(n) => raw.extend_from_slice(&buf[..n]),
Err(_) => break,
}
}
let text = String::from_utf8_lossy(&raw).into_owned();
let status_ok = text
.lines()
.next()
.map(|l| l.split_whitespace().nth(1).map(|c| c.starts_with('2')).unwrap_or(false))
.unwrap_or(false);
if !status_ok {
return None;
}
Some(text.split_once("\r\n\r\n").map(|(_, b)| b.to_string()).unwrap_or(text))
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::TcpListener;
fn serve_once(body: &'static str) -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
std::thread::spawn(move || {
for _ in 0..2 {
if let Ok((mut s, _)) = listener.accept() {
let mut buf = [0u8; 1024];
let _ = s.read(&mut buf);
let _ = s.write_all(
format!("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n{}", body).as_bytes(),
);
}
}
});
format!("http://127.0.0.1:{}/", port)
}
#[test]
fn finds_the_origin_that_serves_our_tag() {
let ours = serve_once("<html><script src=\"http://localhost:8400/live.js?token=abc-123\"></script></html>");
let theirs = serve_once("<html><script src=\"http://localhost:8400/live.js?token=other\"></script></html>");
let dead = "http://127.0.0.1:1/".to_string();
let found = probe(&[dead, theirs.clone(), ours.clone()], "abc-123");
assert_eq!(found.as_deref(), Some(ours.as_str()));
assert_eq!(probe(&[theirs], "abc-123"), None);
}
#[test]
fn env_override_replaces_the_default_list() {
let c = candidates(Some("http://localhost:9999, http://127.0.0.1:7777/"));
assert_eq!(c, vec!["http://localhost:9999/".to_string(), "http://127.0.0.1:7777/".to_string()]);
assert!(candidates(None).iter().any(|u| u == "http://127.0.0.1:5173/"));
}
}
+81
View File
@@ -23,6 +23,49 @@ use serde_json::{Map, Value};
const PLAN_POINTER: &str = "Plan per live.md section 4: extract the identity lock, pick default vs departure mode, commit each variant to a DIFFERENT primary axis, squint-test the trio. Size parameter knobs per section 7 budgets.";
/// 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. No parameter knobs (no data-impeccable-params) unless the prompt asks for something tunable. 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)
}
@@ -201,13 +244,19 @@ fn generate_instructions(event: &Map<String, Value>, self_cmd: &str) -> String {
));
}
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
@@ -385,3 +434,35 @@ fn accept_instructions(event: &Map<String, Value>, self_cmd: &str) -> String {
prefix, file
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn generate_event(origin: Option<&str>) -> Map<String, Value> {
let mut m = Map::new();
m.insert("type".into(), json!("generate"));
m.insert("id".into(), json!("ab12cd34"));
m.insert("action".into(), json!("bolder"));
m.insert("count".into(), json!(3));
m.insert("element".into(), json!({ "tagName": "section", "id": "pricing", "classes": ["pricing"], "textContent": "Simple pricing" }));
if let Some(o) = origin {
m.insert("origin".into(), json!(o));
}
m
}
#[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}");
let user = generate_instructions(&generate_event(None), "impeccable");
assert!(user.contains("live.md section 4"), "{user}");
assert!(!user.contains("Fast path"), "{user}");
}
}
+1
View File
@@ -10,6 +10,7 @@ pub mod browser_assets;
pub mod config;
pub mod copy_edit_agent;
pub mod design_md;
pub mod dev_url;
pub mod event_validation;
pub mod gitignore;
pub mod inject;
+31 -1
View File
@@ -118,7 +118,12 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
if design.is_none() {
missing.push("DESIGN.md");
}
if !missing.is_empty() {
// `--allow-missing-context`: a caller that would rather start from the
// page than from an interview (the generate command) boots anyway; the
// payload names what is missing so the agent extracts the identity
// from the surface instead of running init or document mid-session.
let allow_missing_context = args.iter().any(|a| a == "--allow-missing-context");
if !missing.is_empty() && !allow_missing_context {
let payload = json!({
"ok": false,
"error": "context_missing",
@@ -260,10 +265,35 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
break;
}
let self_cmd = impeccable_context::provider::detect(&env, &cwd).self_cmd;
// 6. Which dev server is serving this app right now (the page carrying
// our tag), so the agent opens it without reading terminals.
let token_for_probe = match server_info.get("token") {
Some(Value::String(s)) => s.clone(),
_ => String::new(),
};
let dev_url = if token_for_probe.is_empty() {
None
} else {
crate::dev_url::probe(
&crate::dev_url::candidates(env.get("IMPECCABLE_DEV_URL_CANDIDATES").map(String::as_str)),
&token_for_probe,
)
};
let context_note = if missing.is_empty() {
Value::Null
} else {
json!(format!(
"Booted without {} (--allow-missing-context). Extract the identity from the picked element's computed styles, CSS custom properties, and sibling styling; do not run init or document during this session, and do not ask for them.",
missing.join(" and ")
))
};
let payload = json!({
"ok": true,
"serverPort": server_info.get("port").cloned().unwrap_or(Value::Null),
"serverToken": server_info.get("token").cloned().unwrap_or(Value::Null),
"devUrl": dev_url,
"contextMissing": missing,
"contextNote": context_note,
"pageFiles": resolved_files,
"liveConfigPath": check_result.get("path").cloned().unwrap_or(Value::Null),
"configDrift": drift,
+1 -1
View File
@@ -120,7 +120,7 @@ fn instructions_for(result: &Map<String, Value>, self_cmd: &str) -> Option<Strin
));
}
return Some(format!(
"Session {} started: the browser scrolled to the target and fired Go (action \"{}\", count {}). Poll now with {} live-poll; the next event for this session is its generate event. Handle it exactly per live.md's Handle generate, then reply done and keep polling.",
"Session {} started: the browser scrolled to the target and fired Go (action \"{}\", count {}). Poll now with {} live-poll; the next event for this session is its generate event, and its _instructions carry the whole fast path (identity from the event, one edit, reply done). Follow them, then keep polling for the accept.",
s("sessionId"), s("action"), n("count"), self_cmd
));
}
+9
View File
@@ -1197,6 +1197,15 @@ fn handle_events_post(
} else {
None
};
if agent_target.is_some() {
// An agent-initiated Go (the generate command): the poll hands its
// handler the fast-path instructions instead of the interactive
// planning ceremony. The marker rides in the journal and the queue.
msg_obj.insert("origin".into(), json!("agent"));
if let Some(o) = msg.as_object_mut() {
o.insert("origin".into(), json!("agent"));
}
}
crate::server_state::strip_poller_owned_event_fields(&mut msg_obj);
let mut st = lock(shared);
if ty == "agent_phase" {
+3 -3
View File
@@ -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`. 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` (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. The envelope also carries `clientId`: a generate event naming a target that another page now holds (a live lease, this page's having lapsed while it captured) or 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 answering session's own event is welcome. |
| `POST /agent-target-result` | 401 / 400 Invalid JSON | `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 naming a target that another page now holds (a live lease, this page's having lapsed while it captured) or 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 answering session's own event is welcome. |
| `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` |
@@ -1727,13 +1727,13 @@ Conventions: every script's "run directly" guard is `process.argv[1]` ending wit
- Flow & outputs (all pretty-printed JSON, 2 spaces, exit 0 unless noted):
1. Workspace monorepo selection (`resolveTargetSelection`, only when no target, cwd is a workspace/monorepo root with discoverable children): `{ok:false, error:'target_selection_required', targetPath:null, projectRoot, repoRoot, targetCandidates:[{name, path, targetExample, …context summary}], hint:'Ask the user which app Impeccable should use, then rerun live from that child app cwd. Use --target <path> only as a fallback or explicit path diagnostic.'}`.
2. `resolveRoots` selection → `{ok:false, error:'target_selection_required', targetCandidates:[{name,path}], hint:'Several apps with a dev-server config exist. Ask the user which one to use, then rerun with --target <path into that app>.'}`.
3. Missing/unreadable/empty PRODUCT.md or DESIGN.md → `{ok:false, error:'context_missing', missing:['PRODUCT.md'?,'DESIGN.md'?], nextCommand:'init'|'document', targetPath, projectRoot, repoRoot, productPath:rel|null, designPath:rel|null}`.
3. Missing/unreadable/empty PRODUCT.md or DESIGN.md → `{ok:false, error:'context_missing', missing:['PRODUCT.md'?,'DESIGN.md'?], nextCommand:'init'|'document', targetPath, projectRoot, repoRoot, productPath:rel|null, designPath:rel|null}`. With `--allow-missing-context` the boot continues instead: the success payload carries `contextMissing:[...]` (the same names; `[]` when nothing is missing) and `contextNote` (a sentence telling the agent to extract the identity from the page and never run init or document mid-session; `null` when nothing is missing), with `hasProduct`/`hasDesign` false and `product`/`design` null for the missing ones.
4. `writeRootsManifest(roots)`.
5. `node live-inject.mjs --check` (cwd appRoot, 15 s): not ok → print that JSON (`{ok:false,error:'config_missing'|'config_invalid',path,message?}` or `{ok:false,error:'check_failed',raw}`) + `targetPath, projectRoot, repoRoot`, exit 0.
6. Reuse server if `server.json` pid alive, else `node live-server.mjs --background`; failure → `{ok:false,error:'server_start_failed'}` exit 1.
7. `node live-inject.mjs --port P --token T`; not ok → `{ok:false,error:'inject_failed',detail:<json|raw>,serverPort}` exit 1.
8. Drift scan: `.html` files under `public, src, app, pages` (skipping ignored dirs/dot-dirs) not in resolved files and not user-excluded → `configDrift = {orphans:[≤20], orphanCount, hint:'N HTML file(s) exist but aren\'t in config.files. Consider adding them, or use a glob pattern like "public/**/*.html".'}` else `null`.
9. Success: `{ok:true, serverPort, serverToken, pageFiles:[…resolved], liveConfigPath, configDrift, targetPath, projectRoot:appRoot, repoRoot, roots:{manifest}, hasProduct:true, product:<text>, productPath:rel, hasDesign:true, design:<text>, designPath:rel, hasSurfaceBrief, surfaceBrief:<text|null>, surfaceBriefPath:rel|null, _instructions:'Open the app URL that serves a pageFiles entry (never serverPort; that is the helper). Then start the poll loop per your harness policy in live.md and re-run node <scripts>/live-poll.mjs immediately after every event or reply. Every event carries _instructions: follow them; they are the authoritative next step with real ids and paths filled in. A poll that is running is a poll you are SERVICING: never announce you are waiting and idle your turn; stay on the exec session until it returns an event, and never end a turn while a poll is outstanding.'}`. Surface brief resolved from `.impeccable/surfaces` under appRoot, contextRoot, repoRoot (first hit).
9. Success: `{ok:true, serverPort, serverToken, devUrl, contextMissing, contextNote, pageFiles:[…resolved], liveConfigPath, configDrift, targetPath, projectRoot:appRoot, repoRoot, roots:{manifest}, hasProduct:true, product:<text>, productPath:rel, hasDesign:true, design:<text>, designPath:rel, hasSurfaceBrief, surfaceBrief:<text|null>, surfaceBriefPath:rel|null, _instructions:'Open the app URL that serves a pageFiles entry (never serverPort; that is the helper). Then start the poll loop per your harness policy in live.md and re-run node <scripts>/live-poll.mjs immediately after every event or reply. Every event carries _instructions: follow them; they are the authoritative next step with real ids and paths filled in. A poll that is running is a poll you are SERVICING: never announce you are waiting and idle your turn; stay on the exec session until it returns an event, and never end a turn while a poll is outstanding.'}`. Surface brief resolved from `.impeccable/surfaces` under appRoot, contextRoot, repoRoot (first hit). `devUrl` is the origin of the dev server serving this app right now, found by fetching `/` on the candidate origins (`http://127.0.0.1:<p>/` and `http://localhost:<p>/` for p in 5173, 3000, 4321, 8080, 4173, 3001, 5174, 8000, 4200, 5000, 1234, probed in parallel with sub-second timeouts; `IMPECCABLE_DEV_URL_CANDIDATES` replaces the list with a comma-separated one) and keeping the first whose document contains the injected `live.js?token=<serverToken>` tag; `null` when none does.
- Tests: `tests/live-target-context.test.mjs`, `tests/live-roots.test.mjs`, `tests/live-e2e.test.mjs` (`session.liveBoot` for `appDir` fixtures), `tests/live-recovery-commands.test.mjs`.
#### `live-server.mjs` -> `impeccable live-server`
+2
View File
@@ -141,6 +141,7 @@ export const SUITES = {
/^tests\/live-e2e\//,
/^tests\/lib\/engine-bin\.mjs$/,
/^tests\/live-agent-target\.test\.mjs$/,
/^tests\/live-boot-fastpath\.test\.mjs$/,
],
commands: [
{
@@ -148,6 +149,7 @@ export const SUITES = {
files: [
'tests/live-reference.test.mjs',
'tests/live-agent-target.test.mjs',
'tests/live-boot-fastpath.test.mjs',
'tests/live-browser-ignores.test.mjs',
'tests/live-browser-source.test.mjs',
'tests/live-e2e-agent-output.test.mjs',
+64 -28
View File
@@ -1,14 +1,17 @@
> **Additional context needed**: only the target element, when the request does not name one that resolves uniquely on the page.
Generate is a programmatic entry into live mode: the user names an element, a direction, and a count in one sentence, and you boot the live session, point the browser at the element, and the overlay scrolls to it, selects it, and fires the same Go a user click fires. Everything downstream is the standard live session. Read [live.md](live.md) in full now if you have not this session; this file is the entry ramp into its contract, and from Step 4 on you are inside it, with one deliberate divergence: Step 5 closes the session on its own once the accept lands, instead of staying open the way `live` does.
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. You boot the helper, open the page, and hand the element to `impeccable live-generate`; the overlay scrolls to it, selects it, and fires the same Go a click fires. This file is the whole contract for that lane. **Do not read [live.md](live.md) for it**: every tool output carries `_instructions` with the next move for that exact situation, and they win over anything you remember. Open live.md only for a situation this file names as outside the lane.
**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.
Three prohibitions cover the known ways this command goes wrong. Each names the tempting move first:
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 five commands and one edit; anything beyond it needs a reason from the output in front of you. This lane also replaces Setup step 3 for the preview edit: the floors craft-floor.md guards are written into Step 4, so do not open craft-floor.md, and read the action's reference only when Step 4 says so.
- The poll shows no generate event yet, and writing variants straight into source feels faster. **Never hand-write a variants wrapper or invent a session id.** Only the browser mints session ids (8 hex characters, at Go), and the server refuses events for any other id; a missing event is fixed in Step 2 or Step 3, never with a direct source edit.
- Handing the user a link to click feels polite. **Open the page yourself** (Step 2); a pasted link usually means no page ever connects.
- The design hook may flag the preview scaffolding you just published. **Do not act on hook findings while live markers are in the file**, and do not restyle variants to appease them; `impeccable live-complete` verifies the file once the accepted variant is permanent. Current hooks stand down on the markers themselves; older installed hooks may still nag.
Four 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 boot prints them and you use them. When they do not, the boot says so and you extract the identity from the page (Step 4). 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 in Step 2 or Step 3, never with a direct source edit.
- **Open the page yourself** (Step 2). A pasted link usually means no page ever connects.
- **Do not act on hook findings while live markers are in the file**, and do not restyle variants to appease them; `impeccable live-complete` verifies the file once the accepted variant is permanent.
## Step 1: Parse the request
@@ -33,60 +36,93 @@ Three parts, all from the user's sentence:
Done when you hold an action from the vocabulary, a count from 1 to 8, and the element description.
## Step 2: Boot live mode and open the page
## Step 2: Boot and open the page
Run the boot exactly as [live.md](live.md)'s Start section describes:
One command. Pass `--target` with the file that renders the element when the request or the project makes it obvious; skip it otherwise. Always pass `--allow-missing-context`: it lets the boot proceed when PRODUCT.md or DESIGN.md is absent and changes nothing when both exist.
```bash
{{scripts_path}}/impeccable live
{{scripts_path}}/impeccable live --target src/App.jsx --allow-missing-context
```
**`config_missing` / `config_invalid`**: follow [live-setup.md](live-setup.md) first.
Read three fields of the output and nothing else:
Then open the app URL that serves a `pageFiles` entry (never `serverPort`; that is the helper, not the app):
- `product` / `design` (or `contextMissing` with a `contextNote`): the design context you have. Present means use it; missing means the page is the source of truth, per the note. Either way, continue.
- `devUrl`: the dev server that is serving this app right now. **Open it**: Cursor `browser_navigate`, any other harness its browser tool. `devUrl: null` means no dev server is serving the page yet: start the project's dev script in a background terminal (`npm run dev` or the framework's equivalent), open the URL it prints, and never kill or restart it afterwards.
- `pageFiles`: the page the helper injected into; the URL that serves it is the one to open (never `serverPort`, that is the helper).
- **Cursor**: `browser_navigate` to the URL now; do not skip it.
- **Any other harness with a browser tool**: open the URL with that tool.
- **No browser tool exists in this harness**: tell the user the exact URL to open, and pass `--wait-for-browser 120000` in Step 3 so the command fires the moment their page connects.
**No browser tool in this harness**: tell the user the exact URL in one line, and pass `--wait-for-browser 120000` in Step 3 so the command fires the moment their page connects.
Done when the boot printed `"ok": true` and a page with the overlay is connected, which Step 3 proves by answering anything other than `no_browser_connected`.
**`config_missing` / `config_invalid`**: follow [live-setup.md](live-setup.md) first, then rerun the boot.
Done when the boot printed `"ok": true` and a page is open. You do not need to read `package.json`, the dev-server config, terminal logs, or the page source to get here.
## Step 3: Target the element
Derive the selector from project source, not from guesswork: an id first, then a unique class, then a landmark tag plus class. **The request names a repeated component in plural** ("the pricing cards"): target the container that holds the set, so scoped CSS restyles every instance at once. **Unsure the selector resolves uniquely**: probe with `--dry-run`; it resolves and reports without starting anything, and it works even mid-session.
One command. Derive the selector from what the user said and what you already know of the project: an id first, then a unique class, then a landmark tag plus class. **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.
```bash
{{scripts_path}}/impeccable live-generate --selector "section.pricing" --action bolder --count 3
{{scripts_path}}/impeccable live-generate --selector "#pricing" --action bolder --count 3
```
Flags: `--selector` (required), `--action`, `--count`, `--prompt`, `--text` (keep only matches whose visible text contains a snippet), `--index` (1-based pick among matches), `--dry-run`, `--wait-for-browser <ms>`.
Every verdict carries `_instructions` with the next move for that exact situation, with real values filled in; follow them over your recollection of this file. Two verdicts deserve naming because their fix sits outside the command:
Every verdict carries `_instructions`; follow them over your recollection of this file. Two deserve naming:
- **`no_browser_connected`**: Step 2's page is not actually open; open it yourself, then rerun.
- **`ambiguous`**: the candidates are listed in the output; target their common container, or rerun with `--text "<visible text>"` or `--index <n>`.
- **`ambiguous`**: the candidates are listed; target their common container, or rerun with `--text "<visible text>"` or `--index <n>`.
Done when the verdict is `ok: true` with a `sessionId`: the browser has scrolled to the element, entered the picked state, and fired Go.
Done when the verdict is `ok: true` with a `sessionId`: the browser has scrolled to the element, selected it, and fired Go.
## Step 4: Generate
Start the poll loop per your harness policy in [live.md](live.md). The queued event for the returned `sessionId` is a standard `generate` event with the picked element's context and a preflighted scaffold; handle it exactly per live.md's Handle generate, which owns everything from planning to the done reply.
Start the poll. Harness policy: **Cursor** runs `{{scripts_path}}/impeccable live-poll` one-shot in a background terminal with notify on `"type":"(generate|accept|discard|variant_mount_failed|exit)"`, handles the event, replies, and restarts the poll; **Claude Code** runs it as a background task; **Codex** runs it one-shot in a yielded foreground exec session and services it; never pass a short `--timeout=`.
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."*
The first event is the `generate` for your `sessionId`, and its `_instructions` are the whole plan: the fast path names the identity sources (the event's `element.computedStyles`, `cssCustomProperties`, and `parentContext`, plus whatever the boot printed), the three dimensions your variants vary for this action, the no-knobs default, and the exact splice. Do it in ONE edit and reply done. Concretely:
**Publishing variants does not end the session.** Keep servicing the poll; accept, discard, and carbonize cleanup follow live.md unchanged, and the helper server stays up through the accept. Done when live.md's contract marks the event you handled complete and the poll is running again.
1. **Identity, one sentence, from the event.** Real colors, faces, corners, borders, shadows, and the layout topology on screen. DESIGN.md wins when the boot printed one. 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:
## Step 5: Close the session
```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>
```
Generate is a one-shot command; this is where it diverges from an open-ended `live` session. Once the accept (or discard) completes, wrap up without being asked: carbonize cleanup is done and `impeccable live-complete` printed `phase: "completed"` (a discard needs no cleanup), so kill your background poll and run live.md's Cleanup:
Rules that keep the browser mounting what you wrote: each variant div holds exactly ONE top-level element, same tag as the original, with the copy verbatim; first variant visible, the rest `display: none`; every `:scope` rule steps into a descendant (`:scope > .card`, never a bare `:scope`); use the `styleTag` and selector strategy from the event's `cssAuthoring` when it differs from the sketch above. **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 parameter knobs** unless the user asked for something tunable. A variant is a finished design to choose from, not a control panel.
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 done** with the file you wrote: `{{scripts_path}}/impeccable live-poll --reply EVENT_ID done --file src/App.jsx`, then poll again. If the edit fails after the browser flipped to GENERATING, `--reply EVENT_ID error "Short reason"` 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."*
Outside the lane, read the matching live.md section before acting: `scaffold.previewMode: "svelte-component"` (Svelte previews are edited as components), `mode: "insert"`, `variant_mount_failed`, `steer`, `manual_edit_apply`, and any `fallback: "agent-driven"` wrap error.
## Step 5: Accept and close
`accept` and `discard` arrive on the poll. The poll script has already run `impeccable live-accept`; the browser is already showing the choice. **Discard**: nothing to do; go to the close below. **Accept with `carbonize: false`**: same. **Accept with `carbonize: true`**: the accepted variant sits in source between `impeccable-carbonize-start/end SESSION_ID` markers with an inline `<style data-impeccable-css>`; make it permanent in one pass over `_acceptResult.file` and the stylesheet that already owns the element's styling:
1. Move the accepted variant's rules into that stylesheet, rewriting `@scope ([data-impeccable-variant="N"]) { :scope > .x }` to the real selectors (`.pricing > .x`).
2. Unwrap: keep the accepted element, delete the variant div (and on JSX the outer `data-impeccable-carbonize` div), drop every `data-impeccable-*` and `data-p-*` attribute.
3. Delete the inline `<style>` block, both markers, and any rules for the other variants.
Then `{{scripts_path}}/impeccable live-complete --id SESSION_ID` and confirm `phase: "completed"`; it refuses with `source_dirty` and findings while any live-mode leftover remains, so fix and rerun. **That command is the verification for this lane**: no `detect` pass, no document or init, no DESIGN.md edits, no reading of `document.md`. Reads before the bake: `_acceptResult.file` and the stylesheet, nothing else.
Close without being asked, the moment the accept or discard is complete:
```bash
{{scripts_path}}/impeccable live-server stop
```
Stopping removes the injected live script, and that removal reloads the page one last time: the user's browser now shows the accepted design with no overlay chrome, still served by their dev server.
Stopping removes the injected script and reloads the page once: the user sees the accepted design with no overlay chrome, still served by their dev server. **Never kill or restart the dev server**, including one you started in Step 2.
- **The user asks for more variants before you wrapped up**: skip the wrap-up, target the next element through the same session (Step 3, with `--dry-run` first when the selector is uncertain), and wrap up after the last accept.
- Restarting the dev server to freshen the page feels like tidying. **Never kill or restart the user's dev server**, including one you started in Step 2. It keeps serving the accepted source after wrap-up; a tab that still looks stale needs one hard refresh, not a new server. A relaunched server also hops to the next free port and strands every open tab on the dead one.
- **The user asks for more variants before you closed**: skip the close, target the next element through the same session (Step 3), and close after the last accept.
- **Interrupted or unsure of the state**: `{{scripts_path}}/impeccable live-status`, then `live-resume`; the journal under `.impeccable/live/sessions/` is canonical.
Done when the helper is stopped, the stop output reported the script tag removed, and the dev site still answers with the accepted design.
Done when the helper is stopped and the dev site still answers with the accepted design.
+2
View File
@@ -769,6 +769,8 @@ describe('POST /agent-target', { skip: ENGINE_BIN ? false : ENGINE_MISSING_MESSA
assert.deepEqual(late, { ok: true, granted: false, pending: false }, 'nothing is left for a rescuer to serve twice');
const journal = readFileSync(join(tmp, '.impeccable/live/sessions/aabbccdd.jsonl'), 'utf-8');
assert.ok(!journal.includes('agentTarget'), 'the envelope never reaches the journal');
const journaled = JSON.parse(journal.split('\n').find((l) => l.includes('"generate"')));
assert.equal((journaled.event || journaled).origin, 'agent', 'an agent-initiated generate is marked so the poll hands out the fast path');
} finally {
tabA.close();
tabB.close();
+86
View File
@@ -0,0 +1,86 @@
/**
* The generate command's fast lane through the live boot: `--allow-missing-context`
* boots a project that has no PRODUCT.md / DESIGN.md (naming what is missing
* instead of refusing), and the boot reports `devUrl`, the dev server that is
* serving the injected page right now, so the agent never reads terminals.
*/
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createServer } from 'node:http';
import { execFile } from 'node:child_process';
import { ENGINE_MISSING_MESSAGE, engineEnv, findEngineBinary } from './lib/engine-bin.mjs';
const ENGINE_BIN = findEngineBinary();
// Async on purpose: the stand-in dev server below lives in this process, so
// a blocking exec would freeze the event loop while the boot probes it.
function run(cwd, args, env = {}) {
return new Promise((resolve) => {
execFile(ENGINE_BIN, args, { cwd, encoding: 'utf-8', env: engineEnv(ENGINE_BIN, env) }, (err, stdout) => {
const text = (stdout || err?.stdout || '').trim();
if (!text) return resolve({ ok: false, error: 'no_output', detail: String(err) });
// `live-server stop` answers in prose ("Stopped live server on port N.").
try { resolve(JSON.parse(text)); } catch { resolve({ ok: !err, raw: text }); }
});
});
}
describe('live boot fast lane', { skip: ENGINE_BIN ? false : ENGINE_MISSING_MESSAGE }, () => {
let tmp;
let server;
let devUrl;
before(async () => {
tmp = mkdtempSync(join(tmpdir(), 'impeccable-boot-fastlane-'));
writeFileSync(join(tmp, 'package.json'), JSON.stringify({ name: 'fastlane', scripts: { dev: 'vite' } }));
writeFileSync(join(tmp, 'vite.config.js'), 'export default {}\n');
writeFileSync(join(tmp, 'index.html'), '<!doctype html><html><body><h1 id="hero">Hero</h1></body></html>\n');
mkdirSync(join(tmp, '.impeccable/live'), { recursive: true });
writeFileSync(join(tmp, '.impeccable/live/config.json'), JSON.stringify({"files": ["index.html"], "insertBefore": "</body>", "commentSyntax": "html"}));
// A stand-in dev server: serves the project's index.html as it is on disk,
// injected tag included, the way Vite would.
server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(readFileSync(join(tmp, 'index.html'), 'utf-8'));
});
await new Promise((r) => server.listen(0, '127.0.0.1', r));
devUrl = `http://127.0.0.1:${server.address().port}/`;
});
after(async () => {
if (existsSync(join(tmp, '.impeccable/live/server.json'))) await run(tmp, ['live-server', 'stop']);
await new Promise((r) => server.close(r));
rmSync(tmp, { recursive: true, force: true });
});
it('refuses without the flag, boots with it, naming what is missing, and finds the dev server', async () => {
const refused = await run(tmp, ['live']);
assert.equal(refused.ok, false);
assert.equal(refused.error, 'context_missing');
assert.deepEqual(refused.missing, ['PRODUCT.md', 'DESIGN.md']);
const booted = await run(tmp, ['live', '--allow-missing-context'], { IMPECCABLE_DEV_URL_CANDIDATES: `http://127.0.0.1:1/, ${devUrl}` });
assert.equal(booted.ok, true, JSON.stringify(booted));
assert.deepEqual(booted.contextMissing, ['PRODUCT.md', 'DESIGN.md']);
assert.match(booted.contextNote, /do not run init or document/);
assert.equal(booted.hasProduct, false);
assert.equal(booted.hasDesign, false);
assert.equal(booted.devUrl, devUrl, 'the origin serving the injected page is reported');
assert.ok(readFileSync(join(tmp, 'index.html'), 'utf-8').includes('live.js?token='), 'the page was injected');
const stopped = await run(tmp, ['live-server', 'stop']);
assert.ok(stopped.ok !== false, JSON.stringify(stopped));
});
it('reports devUrl null when nothing serves the injected page, and no contextMissing when both files exist', async () => {
writeFileSync(join(tmp, 'PRODUCT.md'), '# Product\n\n## Platform\n\nweb\n');
writeFileSync(join(tmp, 'DESIGN.md'), '---\nname: Test\n---\n# Design\n');
const booted = await run(tmp, ['live'], { IMPECCABLE_DEV_URL_CANDIDATES: 'http://127.0.0.1:1/' });
assert.equal(booted.ok, true, JSON.stringify(booted));
assert.deepEqual(booted.contextMissing, []);
assert.equal(booted.contextNote, null);
assert.equal(booted.devUrl, null);
await run(tmp, ['live-server', 'stop']);
});
});
@@ -1,7 +1,7 @@
{
"steps": [
{
"stdout": "{\n \"ok\": true,\n \"serverPort\": <PORT>,\n \"serverToken\": \"<UUID>\",\n \"pageFiles\": [\n \"index.html\",\n \"public/no-body.html\",\n \"public/docs/guide.html\"\n ],\n \"liveConfigPath\": \"<WS>/.impeccable/live/config.json\",\n \"configDrift\": {\n \"orphans\": [\n \"src/cards.html\"\n ],\n \"orphanCount\": 1,\n \"hint\": \"1 HTML file(s) exist but aren't in config.files. Consider adding them, or use a glob pattern like \\\"public/**/*.html\\\".\"\n },\n \"targetPath\": null,\n \"projectRoot\": \"<WS>\",\n \"repoRoot\": \"<WS>\",\n \"roots\": {\n \"version\": 1,\n \"appRoot\": \"<WS>\",\n \"repoRoot\": \"<WS>\",\n \"contextRoot\": \"<WS>\",\n \"sessionRoot\": \"<WS>/.impeccable/live\",\n \"productPath\": \"<WS>/PRODUCT.md\",\n \"designPath\": \"<WS>/DESIGN.md\",\n \"resolvedFrom\": \"cwd\"\n },\n \"hasProduct\": true,\n \"product\": \"<!-- impeccable:product-schema 2 -->\\n# Product\\n\\n## What it is\\nA fixture app used by the oracle harness for live mode.\\n\\n## Platform\\nweb\\n\",\n \"productPath\": \"PRODUCT.md\",\n \"hasDesign\": true,\n \"design\": \"---\\nname: Oracle Live Fixture\\ndescription: A one-page fixture set in plain type on paper-white surfaces.\\ncolors:\\n ink: \\\"#142720\\\"\\n paper: \\\"#ffffff\\\"\\ntypography:\\n body:\\n fontFamily: \\\"system-ui, sans-serif\\\"\\n fontWeight: 400\\n lineHeight: 1.5\\n---\\n\",\n \"designPath\": \"DESIGN.md\",\n \"hasSurfaceBrief\": false,\n \"surfaceBrief\": null,\n \"surfaceBriefPath\": null,\n \"_instructions\": \"Open the app URL that serves a pageFiles entry (never serverPort; that is the helper). Then start the poll loop per your harness policy in live.md and re-run <IMPECCABLE> live-poll immediately after every event or reply. Every event carries _instructions: follow them; they are the authoritative next step with real ids and paths filled in. A poll that is running is a poll you are SERVICING: never announce you are waiting and idle your turn; stay on the exec session until it returns an event, and never end a turn while a poll is outstanding.\"\n}\n",
"stdout": "{\n \"ok\": true,\n \"serverPort\": <PORT>,\n \"serverToken\": \"<UUID>\",\n \"devUrl\": null,\n \"contextMissing\": [],\n \"contextNote\": null,\n \"pageFiles\": [\n \"index.html\",\n \"public/no-body.html\",\n \"public/docs/guide.html\"\n ],\n \"liveConfigPath\": \"<WS>/.impeccable/live/config.json\",\n \"configDrift\": {\n \"orphans\": [\n \"src/cards.html\"\n ],\n \"orphanCount\": 1,\n \"hint\": \"1 HTML file(s) exist but aren't in config.files. Consider adding them, or use a glob pattern like \\\"public/**/*.html\\\".\"\n },\n \"targetPath\": null,\n \"projectRoot\": \"<WS>\",\n \"repoRoot\": \"<WS>\",\n \"roots\": {\n \"version\": 1,\n \"appRoot\": \"<WS>\",\n \"repoRoot\": \"<WS>\",\n \"contextRoot\": \"<WS>\",\n \"sessionRoot\": \"<WS>/.impeccable/live\",\n \"productPath\": \"<WS>/PRODUCT.md\",\n \"designPath\": \"<WS>/DESIGN.md\",\n \"resolvedFrom\": \"cwd\"\n },\n \"hasProduct\": true,\n \"product\": \"<!-- impeccable:product-schema 2 -->\\n# Product\\n\\n## What it is\\nA fixture app used by the oracle harness for live mode.\\n\\n## Platform\\nweb\\n\",\n \"productPath\": \"PRODUCT.md\",\n \"hasDesign\": true,\n \"design\": \"---\\nname: Oracle Live Fixture\\ndescription: A one-page fixture set in plain type on paper-white surfaces.\\ncolors:\\n ink: \\\"#142720\\\"\\n paper: \\\"#ffffff\\\"\\ntypography:\\n body:\\n fontFamily: \\\"system-ui, sans-serif\\\"\\n fontWeight: 400\\n lineHeight: 1.5\\n---\\n\",\n \"designPath\": \"DESIGN.md\",\n \"hasSurfaceBrief\": false,\n \"surfaceBrief\": null,\n \"surfaceBriefPath\": null,\n \"_instructions\": \"Open the app URL that serves a pageFiles entry (never serverPort; that is the helper). Then start the poll loop per your harness policy in live.md and re-run <IMPECCABLE> live-poll immediately after every event or reply. Every event carries _instructions: follow them; they are the authoritative next step with real ids and paths filled in. A poll that is running is a poll you are SERVICING: never announce you are waiting and idle your turn; stay on the exec session until it returns an event, and never end a turn while a poll is outstanding.\"\n}\n",
"stderr": "",
"exit": 0,
"signal": null