Bring Live progressive delivery and the generator subagent to Claude Code

Almost none of this branch's Live work was actually Codex-specific. The publisher,
the fences, the source locks and the browser's partial-arrival UI are plain node
and DOM with zero provider references, and the progressive E2E already passes on
five frameworks driven by a non-Codex agent. The Codex-only part was policy prose
and one frontmatter line, so Claude Code shipped the progressive browser UI it
could never trigger.

Progressive delivery, Codex and Claude Code:
- Add a `live-progressive` capability tag and opt codex, agents, and claude-code
  in. A provider block takes one tag, so naming harnesses would have meant
  duplicating the recipe per tag; a capability reads better than a provider list
  anyway. Cursor and everyone else keep the atomic path until their poll loop is
  known not to stall on the extra publish calls.
- Claude Code publishes variant 1 as soon as it validates rather than waiting to
  write the whole trio in one edit. Nothing about the arrival path needed
  changing: the publisher writes, framework HMR pushes, and the browser's
  MutationObserver counts variants. The parent conversation was never in that
  path, which is why Claude Code's lack of subagent progress streaming does not
  matter here.

Generator subagent:
- Drop `providers: codex` from impeccable-live-generator. The build already maps
  its frontmatter correctly for Claude Code, and impeccable-manual-edit-applier
  has shipped to .claude/agents/ this way all along.
- The reason differs per harness, so the reference says so: Codex delegates to
  unblock a foreground poll, Claude Code delegates to keep a long session's
  screenshots and variant CSS out of the main context. Follows the existing
  manual-edit-applier convention: both agent names, and an inline fallback when
  native subagents are unavailable.

Fixes found on the way:
- The two publish commands hardcoded `.agents/skills/impeccable/scripts/` while
  the other thirteen commands in live.md use {{scripts_path}}. Correct only for
  the Codex repo-skills bundle; it would have pointed Claude Code at a directory
  its install never creates. The shipped .codex variant was already internally
  inconsistent. Now covered by a test.
- `--agent=codex` resolved to the canned fake agent, because the flag parsed as
  `x === 'llm' ? 'llm' : 'fake'`. The private evals Live runner passes exactly
  that, so a real-harness run would have scored deterministic stub variants and
  reported them as Codex output. Unknown values for --agent, --scenario and
  --delivery now fail loudly.
- live-reference tests now compile with each provider's real providerTags instead
  of hand-written lists, so a providers.js misconfiguration fails in tests rather
  than shipping.

Verified: progressive E2E green on vite8-react-plain against a real Vite server
and Chromium; every provider variant's publish and poll paths now agree; Cursor
and Gemini still compile to atomic only.

Prepared with AI assistance under maintainer direction.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-17 15:43:34 -07:00
co-authored by Claude
parent 79656d1ce8
commit fc620b9620
8 changed files with 134 additions and 33 deletions
+4 -4
View File
@@ -15,7 +15,7 @@ import {
waitForCycling,
waitForHandshake,
} from '../tests/live-e2e/ui.mjs';
import { boolFlag, parseArgs, positiveIntFlag } from './lib/cli-args.mjs';
import { boolFlag, parseArgs, positiveIntFlag, resolveEnum } from './lib/cli-args.mjs';
import {
buildInteractionRun,
assembleSplitProgressiveOutput,
@@ -28,9 +28,9 @@ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const args = parseArgs(process.argv.slice(2));
const fixtureName = String(args.fixture || 'vite8-react-plain');
const iterations = positiveIntFlag(args.iterations, 5);
const agentMode = args.agent === 'llm' ? 'llm' : 'fake';
const scenario = args.scenario === 'annotated' ? 'annotated' : 'plain';
const delivery = args.delivery === 'progressive' ? 'progressive' : 'atomic';
const agentMode = resolveEnum(args.agent, ['fake', 'llm'], 'fake', '--agent');
const scenario = resolveEnum(args.scenario, ['plain', 'annotated'], 'plain', '--scenario');
const delivery = resolveEnum(args.delivery, ['atomic', 'progressive'], 'atomic', '--delivery');
const simulatedTailMs = positiveIntFlag(args.simulatedTailMs, 0);
const quiet = boolFlag(args.quiet);
const outputPath = args.output ? resolve(ROOT, String(args.output)) : null;
+16
View File
@@ -69,3 +69,19 @@ export function positiveIntFlag(value, fallback) {
}
return parsed;
}
/**
* Resolve a flag that must be one of a fixed set.
*
* A silent `x === 'known' ? 'known' : fallback` is the trap this replaces: the
* private evals Live runner passes `--agent=codex`, which fell through to the
* canned fake agent and produced a clean-looking report of a deterministic stub
* labelled as a real harness run. An unrecognized value is a mistake, not a
* request for the default.
*/
export function resolveEnum(value, allowed, fallback, flagName) {
if (value === undefined || value === true) return fallback;
const normalized = String(value).trim().toLowerCase();
if (allowed.includes(normalized)) return normalized;
throw new Error(`${flagName} must be one of ${allowed.join(', ')}; got: ${value}`);
}
+6 -3
View File
@@ -22,7 +22,10 @@ export const PROVIDERS = {
},
'claude-code': {
provider: 'claude-code',
providerTags: ['claude-code', 'claude'],
// live-progressive: Live delivers variant 1 as soon as it validates instead of
// one atomic edit. Claude Code polls in a background task, so the extra
// publish calls do not stall its control lane.
providerTags: ['claude-code', 'claude', 'live-progressive'],
configDir: '.claude',
displayName: 'Claude Code',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'],
@@ -40,7 +43,7 @@ export const PROVIDERS = {
},
codex: {
provider: 'codex',
providerTags: ['codex'],
providerTags: ['codex', 'live-progressive'],
configDir: '.codex',
displayName: 'Codex',
frontmatterFields: [],
@@ -54,7 +57,7 @@ export const PROVIDERS = {
},
agents: {
provider: 'agents',
providerTags: ['agents', 'codex'],
providerTags: ['agents', 'codex', 'live-progressive'],
configDir: '.agents',
displayName: 'Codex Repo Skills',
placeholderProvider: 'codex',
+5
View File
@@ -645,6 +645,11 @@ export const PROVIDER_BLOCK_TAGS = new Set([
'rovo-dev',
'trae',
'trae-cn',
// Capability tags. Not harness names: they mark instructions that belong to a
// shared capability several harnesses opt into. Listing the harnesses instead
// would mean duplicating the block body per provider tag, since a block takes
// one tag. Opt a provider in by adding the tag to its providerTags.
'live-progressive',
]);
/**
+1 -2
View File
@@ -6,7 +6,6 @@ tools: Read, Write, Edit, Bash, Glob, Grep
model: inherit
effort: low
max-turns: 16
providers: codex
nickname-candidates:
- Variant Producer
- Live Composer
@@ -15,7 +14,7 @@ nickname-candidates:
# Impeccable Live Generator
You own one leased Impeccable Live `generate` event. The parent thread owns browser control and the foreground poll loop. Never poll, Accept, Discard, commit, stage, or edit generated provider output.
You own one leased Impeccable Live `generate` event. The parent thread owns browser control and the poll loop. Never poll, Accept, Discard, commit, stage, or edit generated provider output.
## Compact input contract
+11 -11
View File
@@ -17,24 +17,24 @@ Execute in order. No step skipped, no step reordered.
3. Poll loop with the default long timeout (600000 ms). Run `live-poll.mjs` again immediately after every event or `--reply`; Codex runs this one-shot poll in the foreground. Never pass a short `--timeout=`.
The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect.
4. On `generate`: reuse `event.scaffold` when present; read the screenshot if present; load the action's reference; deliver variants using the harness policy below; `--reply done`; poll again. In Codex, delegate the complete event to `impeccable_live_generator` and resume the foreground poll immediately; the generator owns publication and the reply.
4. On `generate`: reuse `event.scaffold` when present; read the screenshot if present; load the action's reference; deliver variants using the harness policy below; `--reply done`; poll again. In Codex and Claude Code, delegate the complete event to `impeccable_live_generator` / `impeccable-live-generator` and resume polling immediately; the generator owns publication and the reply.
5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE.
6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately. Carbonize accepts remain recoverable until the foreground task runs `live-complete.mjs --id EVENT_ID`; finish that cleanup before polling again.
7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart.
8. On `exit`: run the cleanup at the bottom.
Harness policy:
- **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell.
- **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. When `generate` arrives and native subagents are available, delegate the complete event to `impeccable-live-generator` with a compact handoff, then start the next poll immediately while that subagent publishes and replies; if unavailable, generate inline with the same contract. Do not paste this full reference into the handoff. The subagent's context is separate from yours, which is the point: a long session's screenshots, references, and variant CSS stay out of the main conversation. Handle Steer, Accept/Discard, manual Apply, carbonize, and Exit in the main conversation.
- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing).
- **Codex**: run the default one-shot poll in a **yielded foreground exec session**. Do not suffix it with `&`, use `--stream`, or leave Live without an active foreground poll. When `generate` arrives, delegate to the low-effort `impeccable_live_generator` agent with a compact handoff, then immediately start the next foreground poll while that agent publishes and replies. Do not paste this full reference into the handoff. Handle Steer, Accept/Discard, manual Apply, carbonize, and Exit in the main task; after each handler/reply, restart the foreground poll.
- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits.
Generation delivery policy:
- **Default (Claude Code, Cursor, and other harnesses):** keep the established atomic single-edit delivery unless that harness has independently demonstrated that progressive tool calls are faster and reliable. This avoids trading model latency for extra tool-call latency on harnesses with different streaming behavior.
- **Default (Cursor and other harnesses):** keep the established atomic single-edit delivery. Do not switch a harness to progressive until its poll loop is known not to block on the extra publish calls. This avoids trading model latency for extra tool-call latency on harnesses with different streaming behavior.
<codex>
- **Codex progressive override:** deliver progressively through `live-publish.mjs`, never by editing project source directly. Publish variant 1 as soon as it is complete, then publish each additional validated variant (or the largest ready prefix) without waiting for later siblings. Attach parameter CSS/manifests only with the complete set. The browser makes every arrived variant immediately reviewable and acceptable; Accept/Discard durably cancel unfinished revisions.
</codex>
<live-progressive>
- **Progressive delivery (Codex, Claude Code):** deliver progressively through `live-publish.mjs`, never by editing project source directly. Publish variant 1 as soon as it is complete, then publish each additional validated variant (or the largest ready prefix) without waiting for later siblings. Attach parameter CSS/manifests only with the complete set. The browser makes every arrived variant immediately reviewable and acceptable; Accept/Discard durably cancel unfinished revisions. The user reviews the first direction while the rest are still being written, so time-to-first-variant is what matters, not total time.
</live-progressive>
Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences.
@@ -314,14 +314,14 @@ Colocate preview CSS as a `<style>` tag inside the variant wrapper; `<style>` wo
**Atomic default:** write CSS + all variants + parameter manifests in one edit at `insertLine`, preserving the established behavior.
<codex>
**Codex transactional progressive override:**
<live-progressive>
**Transactional progressive delivery (Codex, Claude Code):**
1. Plan all directions and name their parameter axes first so the trio remains coherent.
2. Prepare revision 1 from the scaffolded source:
```bash
node .agents/skills/impeccable/scripts/live-publish.mjs --prepare --id EVENT_ID --file SOURCE_FILE
node {{scripts_path}}/live-publish.mjs --prepare --id EVENT_ID --file SOURCE_FILE
```
The JSON result contains `artifactFile`, `epoch`, and `expectedSourceHash`. For the normal source-wrapper path, the live scaffold is an isolated `source-artifact` preview under `.impeccable/live/previews/`; edit **only `artifactFile`** at `insertLine`: write variant 1 and only the CSS it needs. Do not attach `data-impeccable-params` yet. The true source is only the publisher's hash fence and must remain byte-identical until Accept.
@@ -330,7 +330,7 @@ node .agents/skills/impeccable/scripts/live-publish.mjs --prepare --id EVENT_ID
3. Publish revision 1 with the exact fence values returned by `--prepare`:
```bash
node .agents/skills/impeccable/scripts/live-publish.mjs --id EVENT_ID --epoch EPOCH \
node {{scripts_path}}/live-publish.mjs --id EVENT_ID --epoch EPOCH \
--file SOURCE_FILE --artifact ARTIFACT_FILE --expected-source-hash SOURCE_HASH \
--arrived 1 --expected EVENT_COUNT
```
@@ -339,7 +339,7 @@ node .agents/skills/impeccable/scripts/live-publish.mjs --id EVENT_ID --epoch EP
4. Continue variants 2 through `EVENT_COUNT` from the stored plan. Whenever another direction validates, run `--prepare` again so the revision starts from the immutable published prefix, add the largest ready prefix without changing any published variant or default appearance, and publish it immediately. Attach parameter CSS/manifests only when the complete set is ready, using `--kind params`. On component-preview paths, preserve every already-published `vN.svelte` / `vN.vue` byte-for-byte; publication rejects a revision that silently changes a variant the user may already be reviewing.
5. A params-only pass is recovery-only: use it when durable state says every variant arrived but `paramsPublished` is still false after an interrupted publication.
6. Verify the published preview parses, then `--reply done`. A late reply is diagnostic only after Accept/Discard and cannot move the durable session backward.
</codex>
</live-progressive>
Use the `cssAuthoring` object returned by `live-wrap.mjs` to author the temporary preview CSS. The style opening tag shown below is the common case; replace it with `cssAuthoring.styleTag` when the tool returns a different one. The variant markup shape is otherwise stable:
+27 -1
View File
@@ -7,7 +7,7 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { boolFlag, parseArgs, positiveIntFlag, toCamel } from '../scripts/lib/cli-args.mjs';
import { boolFlag, parseArgs, positiveIntFlag, resolveEnum, toCamel } from '../scripts/lib/cli-args.mjs';
describe('parseArgs', () => {
it('reads space-separated values', () => {
@@ -99,3 +99,29 @@ describe('positiveIntFlag', () => {
}
});
});
describe('resolveEnum', () => {
it('accepts an allowed value, case-insensitively', () => {
assert.equal(resolveEnum('llm', ['fake', 'llm'], 'fake', '--agent'), 'llm');
assert.equal(resolveEnum('LLM', ['fake', 'llm'], 'fake', '--agent'), 'llm');
});
it('falls back when absent or given as a bare flag', () => {
assert.equal(resolveEnum(undefined, ['fake', 'llm'], 'fake', '--agent'), 'fake');
assert.equal(resolveEnum(true, ['fake', 'llm'], 'fake', '--agent'), 'fake');
});
it('throws on an unrecognized value instead of silently using the default', () => {
// The private evals Live runner passes --agent=codex. Falling back to the
// canned fake agent produced a clean report of a deterministic stub labelled
// as a real harness run.
assert.throws(
() => resolveEnum('codex', ['fake', 'llm'], 'fake', '--agent'),
/--agent must be one of fake, llm; got: codex/,
);
assert.throws(
() => resolveEnum('progresive', ['atomic', 'progressive'], 'atomic', '--delivery'),
/--delivery must be one of atomic, progressive/,
);
});
});
+64 -12
View File
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { compileProviderBlocks } from '../scripts/lib/utils.js';
import { PROVIDERS } from '../scripts/lib/transformers/providers.js';
const ROOT = process.cwd();
@@ -68,7 +69,14 @@ describe('live reference authoring contract', () => {
assert.match(liveMd, /Do not paste this full reference into the handoff/);
assert.match(generationAgentMd, /codex-name: impeccable_live_generator/);
assert.match(generationAgentMd, /effort: low/);
assert.match(generationAgentMd, /providers: codex/);
// The generator ships to every harness with an agent format, not just Codex:
// Codex delegates to unblock its foreground poll, Claude Code delegates to
// keep a long session's screenshots and variant CSS out of the main context.
assert.doesNotMatch(
generationAgentMd,
/^providers:/m,
'the live generator must not be gated to one harness',
);
assert.match(generationAgentMd, /Never poll, Accept, Discard/);
assert.match(generationAgentMd, /Publish the first reviewable result/);
assert.match(generationAgentMd, /preserve every already-published variant byte-for-byte/i);
@@ -118,8 +126,11 @@ describe('live reference authoring contract', () => {
it('keeps Codex sandbox guidance Codex-only', () => {
const liveMd = readFileSync(join(ROOT, 'skill/reference/live.md'), 'utf-8');
const codexLiveMd = compileProviderBlocks(liveMd, ['codex']);
const claudeLiveMd = compileProviderBlocks(liveMd, ['claude-code', 'claude']);
// Compile with each provider's real tags rather than hand-written ones, so a
// providers.js misconfiguration fails here instead of shipping.
const compileFor = (provider) => compileProviderBlocks(liveMd, PROVIDERS[provider].providerTags);
const codexLiveMd = compileFor('codex');
const claudeLiveMd = compileFor('claude-code');
assert.match(
codexLiveMd,
@@ -133,7 +144,7 @@ describe('live reference authoring contract', () => {
);
assert.doesNotMatch(
codexLiveMd,
/<\/?codex>/,
/<\/?(codex|live-progressive)>/,
'provider block tags should not leak into compiled Codex live reference',
);
assert.doesNotMatch(
@@ -141,16 +152,57 @@ describe('live reference authoring contract', () => {
/sandbox_permissions: "require_escalated"/,
'Codex-only sandbox guidance should not appear in Claude live reference',
);
assert.match(
codexLiveMd,
/Codex progressive override/,
'Codex live reference should progressively deliver the first reviewable variant',
);
});
it('gives progressive delivery to the harnesses that opt in, and only those', () => {
const liveMd = readFileSync(join(ROOT, 'skill/reference/live.md'), 'utf-8');
const compileFor = (provider) => compileProviderBlocks(liveMd, PROVIDERS[provider].providerTags);
// Codex delegates to unblock a foreground poll; Claude Code polls in a
// background task. Both can publish variant 1 before the trio is finished.
for (const provider of ['codex', 'agents', 'claude-code']) {
const compiled = compileFor(provider);
assert.match(
compiled,
/Transactional progressive delivery/,
`${provider} should get the progressive publish recipe`,
);
assert.match(
compiled,
/Progressive delivery \(Codex, Claude Code\)/,
`${provider} should get the progressive delivery policy`,
);
}
// Everyone else keeps the atomic single-edit path until their poll loop is
// known not to stall on the extra publish calls.
for (const provider of ['cursor', 'gemini']) {
const compiled = compileFor(provider);
assert.doesNotMatch(
compiled,
/Transactional progressive delivery|Progressive delivery \(Codex, Claude Code\)/,
`${provider} has not opted into progressive delivery`,
);
assert.match(compiled, /\*\*Atomic default:\*\*/, `${provider} should keep the atomic path`);
assert.doesNotMatch(
compiled,
/<\/?live-progressive>/,
`capability block tags should not leak into the compiled ${provider} reference`,
);
}
});
it('routes every live-publish command through the per-provider scripts path', () => {
const liveMd = readFileSync(join(ROOT, 'skill/reference/live.md'), 'utf-8');
// The progressive recipe used to hardcode `.agents/skills/...`, which is only
// correct for the Codex repo-skills bundle. Every other harness would have
// been told to run the publisher from a directory its install never creates.
assert.doesNotMatch(
claudeLiveMd,
/Codex progressive override|first-reviewable milestone/,
'Claude live reference should retain the atomic path without Codex-specific delivery instructions',
liveMd,
/node\s+\.[a-z-]+\/skills\/impeccable\/scripts\//,
'live.md must not hardcode a harness config dir; use {{scripts_path}}',
);
assert.match(liveMd, /node \{\{scripts_path\}\}\/live-publish\.mjs --prepare/);
});
it('keeps live preview CSS guidance capability-mode driven', () => {