mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 07:36:50 +03:00
Keep the generate lane's boot extras behind flags
The speed pass had every `impeccable live` boot probe for the dev server and print three new keys (devUrl, contextMissing, contextNote), which moved an oracle golden and cost a plain live session a probe it never asked for. The lane's extras are opt-in now: `--dev-url` runs the probe and reports devUrl; `--allow-missing-context` reports the context keys. Without either flag the boot's work and payload are byte-identical to before, which the restored golden and a new boot test pin. generate.md passes both flags; the contract doc says so. 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
1220f26d08
commit
f29636a3d5
@@ -265,13 +265,17 @@ 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.
|
||||
// The generate lane's two opt-ins. Both are silent unless asked for, so
|
||||
// a plain boot's payload is unchanged: `--dev-url` probes which dev
|
||||
// server is serving this app right now (the page carrying our tag) and
|
||||
// reports `devUrl`; `--allow-missing-context` reports `contextMissing`
|
||||
// and a `contextNote` for the files it let the boot proceed without.
|
||||
let want_dev_url = args.iter().any(|a| a == "--dev-url");
|
||||
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() {
|
||||
let dev_url = if !want_dev_url || token_for_probe.is_empty() {
|
||||
None
|
||||
} else {
|
||||
crate::dev_url::probe(
|
||||
@@ -287,13 +291,10 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
|
||||
missing.join(" and ")
|
||||
))
|
||||
};
|
||||
let payload = json!({
|
||||
let mut 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,
|
||||
@@ -312,6 +313,15 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
|
||||
"surfaceBriefPath": surface_brief_path,
|
||||
"_instructions": boot_instructions(&self_cmd),
|
||||
});
|
||||
if let Some(obj) = payload.as_object_mut() {
|
||||
if want_dev_url {
|
||||
obj.insert("devUrl".into(), dev_url.map(Value::String).unwrap_or(Value::Null));
|
||||
}
|
||||
if allow_missing_context {
|
||||
obj.insert("contextMissing".into(), json!(missing));
|
||||
obj.insert("contextNote".into(), context_note);
|
||||
}
|
||||
}
|
||||
println(io, &json_pretty(&payload));
|
||||
0
|
||||
}
|
||||
|
||||
@@ -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}`. 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.
|
||||
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, and only then the success payload gains `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, 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.
|
||||
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). With `--dev-url` (and only then) the payload also carries `devUrl`, 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. Without either flag the payload and the boot's work are exactly as before: no probe runs and neither key appears.
|
||||
- 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`
|
||||
|
||||
@@ -38,10 +38,10 @@ Done when you hold an action from the vocabulary, a count from 1 to 8, and the e
|
||||
|
||||
## Step 2: Boot and open the page
|
||||
|
||||
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.
|
||||
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 both flags: `--allow-missing-context` lets the boot proceed when PRODUCT.md or DESIGN.md is absent (it changes nothing when both exist), and `--dev-url` asks the boot to find the dev server. Neither touches a plain `live` session.
|
||||
|
||||
```bash
|
||||
{{scripts_path}}/impeccable live --target src/App.jsx --allow-missing-context
|
||||
{{scripts_path}}/impeccable live --target src/App.jsx --allow-missing-context --dev-url
|
||||
```
|
||||
|
||||
Read three fields of the output and nothing else:
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('live boot fast lane', { skip: ENGINE_BIN ? false : ENGINE_MISSING_MESS
|
||||
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}` });
|
||||
const booted = await run(tmp, ['live', '--allow-missing-context', '--dev-url'], { 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/);
|
||||
@@ -73,14 +73,22 @@ describe('live boot fast lane', { skip: ENGINE_BIN ? false : ENGINE_MISSING_MESS
|
||||
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 () => {
|
||||
it('with both files present the flags report devUrl null and an empty contextMissing', 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/' });
|
||||
const booted = await run(tmp, ['live', '--allow-missing-context', '--dev-url'], { 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']);
|
||||
});
|
||||
|
||||
it('a plain live boot is untouched by the lane: no probe, no new keys', async () => {
|
||||
const booted = await run(tmp, ['live'], { IMPECCABLE_DEV_URL_CANDIDATES: devUrl });
|
||||
assert.equal(booted.ok, true, JSON.stringify(booted));
|
||||
assert.ok(!('devUrl' in booted), 'devUrl only appears with --dev-url');
|
||||
assert.ok(!('contextMissing' in booted) && !('contextNote' in booted), 'context keys only appear with --allow-missing-context');
|
||||
await run(tmp, ['live-server', 'stop']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null
|
||||
|
||||
Reference in New Issue
Block a user