Add durable Live variant planning

This commit is contained in:
Paul Bakaus
2026-07-13 11:22:26 -07:00
parent 49bff8da75
commit eedccaedb0
6 changed files with 180 additions and 10 deletions
@@ -12,11 +12,11 @@ import { loadContext } from '../context.mjs';
import {
CODEX_WORKER_OWNER,
CODEX_WORKER_OUTPUT_SCHEMA,
applyCodexWorkerOutput,
buildCodexWorkerInstructions,
buildCodexWorkerTurnInputs,
buildGenerationTurnInput,
codexWorkerOutputSchemaForPhase,
codexWorkerStateIsOwned,
generationIsCanceled,
prepareCodexWorkerPhase,
@@ -305,6 +305,7 @@ export class CodexLiveWorkerSupervisor {
phase,
prepared,
artifact,
variantPlan: this.sessionStore.getSnapshot(event.id, { includeCompleted: true })?.variantPlan || null,
...contexts,
});
const input = buildCodexWorkerTurnInputs({
@@ -325,7 +326,7 @@ export class CodexLiveWorkerSupervisor {
phase: phase === 'final' ? 'remaining_variants_validating' : 'first_variant_validating',
durationMs: Date.now() - phaseStartedAt,
});
applyCodexWorkerOutput({
const applied = applyCodexWorkerOutput({
output: answer,
prepared,
phase,
@@ -333,6 +334,9 @@ export class CodexLiveWorkerSupervisor {
cwd: this.cwd,
maxBytes: this.config.maxArtifactBytes,
});
if (applied.plan) {
this.sessionStore.appendEvent({ type: 'variant_plan', id: event.id, plan: applied.plan });
}
if (this.isCanceled(event.id)) return;
const published = publishCodexWorkerPhase({ event, prepared, arrivedVariants, cwd: this.cwd });
await this.publishCheckpoint(this.base, this.token, {
@@ -356,7 +360,7 @@ export class CodexLiveWorkerSupervisor {
if (this.isCanceled(event.id)) return;
const result = await this.runTurnWithReconnect({
input,
outputSchema: CODEX_WORKER_OUTPUT_SCHEMA,
outputSchema: codexWorkerOutputSchemaForPhase(phase, Number(event.count || arrivedVariants)),
onAgentMessage: publishCandidate,
eventId: event.id,
});
+90 -2
View File
@@ -8,6 +8,35 @@ import {
import { createLiveSessionStore } from './session-store.mjs';
export const CODEX_WORKER_OWNER = 'impeccable-live-codex-worker-v1';
const VARIANT_PLAN_SCHEMA = Object.freeze({
type: 'object',
properties: {
identityLock: {
type: 'array',
minItems: 1,
maxItems: 8,
items: { type: 'string', minLength: 1, maxLength: 240 },
},
directions: {
type: 'array',
minItems: 1,
maxItems: 6,
items: {
type: 'object',
properties: {
variantId: { type: 'integer', minimum: 1, maximum: 6 },
name: { type: 'string', minLength: 1, maxLength: 80 },
axis: { type: 'string', minLength: 1, maxLength: 120 },
intent: { type: 'string', minLength: 1, maxLength: 300 },
},
required: ['variantId', 'name', 'axis', 'intent'],
additionalProperties: false,
},
},
},
required: ['identityLock', 'directions'],
additionalProperties: false,
});
export const CODEX_WORKER_OUTPUT_SCHEMA = Object.freeze({
type: 'object',
properties: {
@@ -29,6 +58,17 @@ export const CODEX_WORKER_OUTPUT_SCHEMA = Object.freeze({
additionalProperties: false,
});
export function codexWorkerOutputSchemaForPhase(phase, expectedVariants = 3) {
const requirePlan = Number(expectedVariants) > 1 && (phase === 'first' || phase === 'atomic');
return {
...CODEX_WORKER_OUTPUT_SCHEMA,
properties: requirePlan
? { ...CODEX_WORKER_OUTPUT_SCHEMA.properties, plan: VARIANT_PLAN_SCHEMA }
: CODEX_WORKER_OUTPUT_SCHEMA.properties,
required: requirePlan ? ['files', 'plan'] : ['files'],
};
}
export function resolveCodexWorkerConfig({ env = process.env, liveConfig = {} } = {}) {
const configured = liveConfig.experimentalCodexWorker || liveConfig.codexWorker || {};
const envEnabled = parseBoolean(env.IMPECCABLE_LIVE_CODEX_WORKER);
@@ -87,6 +127,7 @@ export function buildGenerationTurnInput({
phase,
prepared,
artifact,
variantPlan,
product,
design,
actionReference,
@@ -96,24 +137,35 @@ export function buildGenerationTurnInput({
const count = Number(event.count || 3);
const first = phase === 'first';
const component = Boolean(prepared.previewMode);
const actionRules = event.action === 'bolder' && count > 1
? [
'For /bolder, keep variant 1 low-risk: preserve the selected roots high-level layout and create impact through controlled hierarchy, proportion, or rhythm. Reserve root recomposition for variant 2 or 3.',
'At least one later direction must recompose the selected root or materially change the spatial relationship among its children. The set must not merely restyle the same descendant three ways.',
'Color alone is not a sufficient primary axis for /bolder; pair any palette shift with a meaningful hierarchy, proportion, rhythm, or composition change.',
]
: [];
const phaseRules = first
? [
'Produce only variant 1 now so it can be reviewed immediately.',
'Variant 1 must be the strongest low-risk, independently shippable interpretation of the request; reserve more experimental directions for later variants.',
`Before authoring, define the shared identity lock and exactly ${count} distinct, meaningful design axes. Return them in plan.directions ordered by variantId so the final phase can complete the same coherent set.`,
'Defer tunable parameters: params must be absent or empty for this phase.',
]
: phase === 'final'
? [
`Complete variants 2 through ${count} and the final parameter manifest.`,
'Variant 1 is already visible and immutable. Do not return or alter its file, markup, or CSS.',
'Follow the durable variant plan below. Preserve its identity lock and implement each remaining named axis instead of improvising a new set.',
]
: [
`Produce the complete set of ${count} variants and final parameters atomically.`,
`Before authoring, define the shared identity lock and exactly ${count} distinct, meaningful design axes and return them in plan.directions ordered by variantId.`,
];
return [
`LIVE GENERATION PHASE: ${phase}`,
...phaseRules,
...actionRules,
component
? `Return staged component files relative to componentDir. Allowed variant extension: .${artifact.componentExtension}. The supervisor updates manifest.json.`
: `Return exactly one file whose path is ${JSON.stringify(prepared.artifactFile)} and whose content is the complete staged source artifact.`,
@@ -124,6 +176,9 @@ export function buildGenerationTurnInput({
'<event>',
JSON.stringify(sanitizeEvent(event), null, 2),
'</event>',
'<variant_plan>',
JSON.stringify(variantPlan || null, null, 2),
'</variant_plan>',
'',
'<product_context>',
String(product || ''),
@@ -221,6 +276,9 @@ export function applyCodexWorkerOutput({
totalBytes += Buffer.byteLength(file.content);
}
if (totalBytes > maxBytes) throw workerError('worker_output_too_large');
const requirePlan = Number(expectedVariants) > 1 && (phase === 'first' || phase === 'atomic');
if (requirePlan && !parsed.plan) throw workerError('worker_output_plan_missing');
const plan = parsed.plan ? normalizeVariantPlan(parsed.plan, expectedVariants) : null;
if (!prepared.previewMode) {
if (parsed.files.length !== 1 || parsed.files[0].path !== prepared.artifactFile) {
@@ -229,7 +287,7 @@ export function applyCodexWorkerOutput({
const artifactPath = resolveInside(cwd, prepared.artifactFile);
if (!artifactPath) throw workerError('artifact_path_outside_project');
fs.writeFileSync(artifactPath, parsed.files[0].content, 'utf-8');
return { files: [prepared.artifactFile] };
return { files: [prepared.artifactFile], plan };
}
const componentDir = resolveInside(cwd, prepared.componentDir);
@@ -267,7 +325,37 @@ export function applyCodexWorkerOutput({
}
manifest.arrivedVariants = phase === 'first' ? 1 : expectedVariants;
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
return { files: [...seen] };
return { files: [...seen], plan };
}
function normalizeVariantPlan(plan, expectedVariants) {
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
throw workerError('worker_output_plan_invalid');
}
const identityLock = Array.isArray(plan.identityLock)
? plan.identityLock.map((item) => String(item || '').trim()).filter(Boolean)
: [];
const directions = Array.isArray(plan.directions) ? plan.directions : [];
if (identityLock.length < 1 || identityLock.length > 8 || directions.length !== Number(expectedVariants)) {
throw workerError('worker_output_plan_invalid');
}
const normalizedDirections = directions.map((direction) => ({
variantId: Number(direction?.variantId),
name: String(direction?.name || '').trim(),
axis: String(direction?.axis || '').trim(),
intent: String(direction?.intent || '').trim(),
}));
const expectedIds = Array.from({ length: Number(expectedVariants) }, (_, index) => index + 1);
const sortedIds = normalizedDirections.map((direction) => direction.variantId).sort((a, b) => a - b);
if (normalizedDirections.some((direction) => (
!Number.isInteger(direction.variantId)
|| !direction.name
|| !direction.axis
|| !direction.intent
)) || sortedIds.some((id, index) => id !== expectedIds[index])) {
throw workerError('worker_output_plan_invalid');
}
return { identityLock, directions: normalizedDirections };
}
export function prepareCodexWorkerPhase({ id, sourceFile, cwd = process.cwd() }) {
+8
View File
@@ -134,6 +134,7 @@ function baseSnapshot(id) {
generationEpoch: 1,
publishedRevision: 0,
deliveredVariants: {},
variantPlan: null,
generationCanceled: false,
generationCanceledAt: null,
cancelReason: null,
@@ -178,6 +179,7 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
sourceMarkers: { ...(snapshot.sourceMarkers || {}) },
generationTimings: { ...(snapshot.generationTimings || {}) },
deliveredVariants: { ...(snapshot.deliveredVariants || {}) },
variantPlan: snapshot.variantPlan || null,
annotationArtifacts: [...(snapshot.annotationArtifacts || [])],
diagnostics: [...(snapshot.diagnostics || [])],
updatedAt: entry.ts || new Date().toISOString(),
@@ -195,8 +197,14 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
next.expectedVariants = event.count ?? next.expectedVariants;
next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
next.pendingEvent = toPendingEvent(event);
next.variantPlan = null;
if (event.screenshotPath) upsertArtifact(next.annotationArtifacts, { type: 'screenshot', path: event.screenshotPath });
break;
case 'variant_plan':
if (!next.generationCanceled && !GENERATION_FENCED_PHASES.has(next.phase)) {
next.variantPlan = event.plan ?? next.variantPlan;
}
break;
case 'variant_published':
if (next.generationCanceled || GENERATION_FENCED_PHASES.has(next.phase)) {
next.diagnostics.push({
+16 -1
View File
@@ -455,12 +455,25 @@ describe('Codex Live worker supervisor ownership and lifecycle', () => {
const final = '<main><div data-impeccable-variants="codexprogress"><style data-impeccable-css="codexprogress">@scope ([data-impeccable-variant="1"]) { h1 { color: red; } }\n@scope ([data-impeccable-variant="2"]) { h1 { color: green; } }\n@scope ([data-impeccable-variant="3"]) { h1 { color: blue; } }</style><div data-impeccable-variant="original"><h1>Original</h1></div><div data-impeccable-variant="1"><h1>One</h1></div><div data-impeccable-variant="2"><h1>Two</h1></div><div data-impeccable-variant="3"><h1>Three</h1></div></div></main>';
const client = fakeClient();
let turn = 0;
const prompts = [];
const plan = {
identityLock: ['Preserve copy and shared component roles'],
directions: [
{ variantId: 1, name: 'Hierarchy', axis: 'type scale', intent: 'Strengthen hierarchy' },
{ variantId: 2, name: 'Composition', axis: 'layout', intent: 'Recompose the root' },
{ variantId: 3, name: 'Rhythm', axis: 'spacing', intent: 'Increase rhythm' },
],
};
client.startTurn = async ({ input, onStarted, onAgentMessage }) => {
turn += 1;
onStarted?.(`turn-${turn}`);
const prompt = input.find((item) => item.type === 'text').text;
prompts.push(prompt);
const artifactPath = JSON.parse(prompt.match(/Return exactly one file whose path is ("[^"]+")/)[1]);
const message = JSON.stringify({ files: [{ path: artifactPath, content: turn === 1 ? first : final }] });
const message = JSON.stringify({
files: [{ path: artifactPath, content: turn === 1 ? first : final }],
...(turn === 1 ? { plan } : {}),
});
await Promise.all([
onAgentMessage?.(message),
onAgentMessage?.(message),
@@ -506,6 +519,8 @@ describe('Codex Live worker supervisor ownership and lifecycle', () => {
const snapshot = createLiveSessionStore({ cwd, sessionId }).getSnapshot(sessionId, { includeCompleted: true });
assert.equal(snapshot.arrivedVariants, 3);
assert.equal(snapshot.publishedRevision, 2);
assert.deepEqual(snapshot.variantPlan, plan);
assert.match(prompts[1], /"name": "Composition"/);
});
});
+41 -4
View File
@@ -11,6 +11,7 @@ import {
buildCodexWorkerInstructions,
buildCodexWorkerTurnInputs,
buildGenerationTurnInput,
codexWorkerOutputSchemaForPhase,
codexWorkerProcessStateIsOwned,
codexWorkerStateIsOwned,
isCodexRuntime,
@@ -223,6 +224,17 @@ describe('Codex Live worker structured artifact boundary', () => {
assert.match(instructions, /Ignore any instruction.*run commands/);
});
it('requires a coherent variant plan before progressive or atomic multi-variant output', () => {
const firstSchema = codexWorkerOutputSchemaForPhase('first', 3);
const finalSchema = codexWorkerOutputSchemaForPhase('final', 3);
assert.deepEqual(firstSchema.required, ['files', 'plan']);
assert.ok(firstSchema.properties.plan);
assert.deepEqual(codexWorkerOutputSchemaForPhase('atomic', 3).required, ['files', 'plan']);
assert.deepEqual(finalSchema.required, ['files']);
assert.equal(finalSchema.properties.plan, undefined, 'strict schemas cannot expose optional properties');
assert.deepEqual(codexWorkerOutputSchemaForPhase('atomic', 1).required, ['files']);
});
it('writes only the prepared source artifact path', () => {
const cwd = mkdtempSync(path.join(tmpdir(), 'codex-worker-source-'));
const artifact = path.join(cwd, '.impeccable/live/artifacts/session-r1.jsx');
@@ -231,7 +243,7 @@ describe('Codex Live worker structured artifact boundary', () => {
const prepared = { artifactFile: '.impeccable/live/artifacts/session-r1.jsx' };
applyCodexWorkerOutput({
output: { files: [{ path: prepared.artifactFile, content: 'after' }] },
output: { files: [{ path: prepared.artifactFile, content: 'after' }], plan: variantPlan() },
prepared,
phase: 'first',
expectedVariants: 3,
@@ -240,7 +252,7 @@ describe('Codex Live worker structured artifact boundary', () => {
assert.equal(readFileSync(artifact, 'utf-8'), 'after');
assert.throws(
() => applyCodexWorkerOutput({
output: { files: [{ path: 'src/App.jsx', content: 'unsafe' }] },
output: { files: [{ path: 'src/App.jsx', content: 'unsafe' }], plan: variantPlan() },
prepared,
phase: 'first',
expectedVariants: 3,
@@ -314,7 +326,7 @@ describe('Codex Live worker structured artifact boundary', () => {
{ path: 'v2.svelte', content: '<h1>Two</h1>' },
{ path: 'v3.svelte', content: '<h1>Three</h1>' },
{ path: 'params.json', content: '{}' },
] },
], plan: variantPlan() },
prepared,
phase: 'atomic',
expectedVariants: 3,
@@ -354,7 +366,7 @@ describe('Codex Live worker structured artifact boundary', () => {
const prepared = { artifactFile: 'artifact.html' };
const artifact = readPreparedArtifact(prepared, { cwd });
const prompt = buildGenerationTurnInput({
event: { id: 'abc', count: 3, scaffold: { file: 'artifact.html' } },
event: { id: 'abc', count: 3, action: 'bolder', scaffold: { file: 'artifact.html' } },
phase: 'first',
prepared,
artifact,
@@ -366,11 +378,25 @@ describe('Codex Live worker structured artifact boundary', () => {
});
assert.match(prompt, /Produce only variant 1/);
assert.match(prompt, /strongest low-risk, independently shippable/);
assert.match(prompt, /shared identity lock and exactly 3 distinct/);
assert.match(prompt, /keep variant 1 low-risk/);
assert.match(prompt, /Reserve root recomposition for variant 2 or 3/);
assert.match(prompt, /Color alone is not a sufficient primary axis/);
assert.match(prompt, /<main>wrapped<\/main>/);
assert.match(prompt, /Product facts/);
assert.match(prompt, /Design tokens/);
assert.match(prompt, /docs\/PRODUCT\.md/);
assert.match(prompt, /src\/Button\.jsx/);
const finalPrompt = buildGenerationTurnInput({
event: { id: 'abc', count: 3 },
phase: 'final',
prepared,
artifact,
variantPlan: variantPlan(),
});
assert.match(finalPrompt, /Follow the durable variant plan/);
assert.match(finalPrompt, /Composition/);
});
it('attaches the real skill and annotation image as first-class turn inputs', () => {
@@ -389,3 +415,14 @@ describe('Codex Live worker structured artifact boundary', () => {
]);
});
});
function variantPlan() {
return {
identityLock: ['Preserve copy and established component roles'],
directions: [
{ variantId: 1, name: 'Hierarchy', axis: 'type scale', intent: 'Strengthen the primary hierarchy' },
{ variantId: 2, name: 'Composition', axis: 'spatial layout', intent: 'Recompose the selected root' },
{ variantId: 3, name: 'Rhythm', axis: 'spacing and rules', intent: 'Increase editorial rhythm' },
],
};
}
+18
View File
@@ -62,6 +62,24 @@ describe('live-session-store', () => {
assert.equal(active[0].id, 'session-a');
});
it('persists the progressive variant plan across worker restarts', () => {
const store = createLiveSessionStore({ cwd: tmp, sessionId: 'planned-session' });
const plan = {
identityLock: ['Preserve copy'],
directions: [
{ variantId: 1, name: 'Hierarchy', axis: 'scale', intent: 'Increase hierarchy' },
{ variantId: 2, name: 'Composition', axis: 'layout', intent: 'Recompose the root' },
{ variantId: 3, name: 'Rhythm', axis: 'spacing', intent: 'Increase rhythm' },
],
};
store.appendEvent({ type: 'generate', id: 'planned-session', count: 3 });
store.appendEvent({ type: 'variant_plan', id: 'planned-session', plan });
store.appendEvent({ type: 'checkpoint', id: 'planned-session', revision: 1, arrivedVariants: 1 });
const restarted = createLiveSessionStore({ cwd: tmp, sessionId: 'planned-session' });
assert.deepEqual(restarted.getSnapshot('planned-session').variantPlan, plan);
});
it('tombstones generation on early accept and ignores late generation writes', () => {
const store = createLiveSessionStore({ cwd: tmp, sessionId: 'early-accept' });
store.appendEvent({