Drop the live generator subagent; fix the artifact decoy that broke accept

The first real Claude Code Live run failed, and the subagent was not the cause.

Root cause: progressive publication stages each revision as
`.impeccable/live/artifacts/<id>-r<n>.<source-ext>`, nothing ever deleted them,
and findSessionFile's walker skipped only node_modules/.git/dist/build. It
searches src, app, pages, ... then `.`; a project whose source is not under one of
those (this repo's own site lives in site/pages/) falls through to the `.` walk,
where dot-directories sort before letters. So accept found the artifact instead of
the real file. Two outcomes, both reproduced: where isGeneratedFile returns true
it declines with mode: 'fallback' (what the run hit, after which the agent
hand-carbonized several hundred lines across three stylesheets, including
unrequested drive-by edits); where it returns false, accept writes the variant
into the throwaway artifact and reports handled: true while real source never
changes.

The E2E suite could not have caught this. Every fixture puts source under `src/`,
which is searched before the `.` walk can reach `.impeccable`. Five framework
fixtures and three progressive scenarios pass because of fixture layout, not
because the path works. I read that as evidence and shouldn't have.

- Never search `.impeccable`: it is Impeccable's own state, never project source.
- Retire a session's staged artifacts on accept/discard, so they cannot outlive
  the session and become a decoy for anything else that walks the tree.
- Regression tests use a site/pages layout with artifacts present. All three fail
  against the previous code.

Generator subagent removed, on both harnesses:
The parent must hand-compress the design system into the handoff, and compression
is lossy. Measured on the real run: a 6,826-char handoff carrying exactly one
token reference, after the parent had itself read kinpaku-tokens.css. The subagent
then spent 3 of its first 9 turns hunting DESIGN.md, gave up, and emitted 0
var(--token) uses and 22 raw oklch literals — violating its own spec's "Never
invent raw colors when tokens exist" — including a 1:1 gold-on-gold contrast bug.
Isolation is not a benefit here; knowing the design system is the job. Generation
stays in the main thread, which already holds the tokens and writes them from the
first byte, so carbonize is a move rather than a translation.

Copy edits keep their subagent: applying a known set of ops to a named file is
self-contained, so an isolated context costs nothing. That is the line.

Progressive delivery stays for Codex and Claude Code, main-thread driven. Claude
Code keeps the full benefit because its poll is a background task. Codex's poll
blocks the foreground, so with no subagent the user sees variant 1 early via HMR
but cannot accept it until the trio finishes; that is the cost of the
simplification and it is worth naming.

Prepared with AI assistance under maintainer direction.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-17 18:56:23 -07:00
co-authored by Claude
parent 1b194d9751
commit c7b67b3832
6 changed files with 129 additions and 75 deletions
+23 -1
View File
@@ -3,7 +3,7 @@ import path from 'node:path';
import { createHash } from 'node:crypto';
import { createLiveSessionStore } from './session-store.mjs';
import { withSourceLockSync } from './source-lock.mjs';
import { getLiveDir } from '../lib/impeccable-paths.mjs';
import { getLiveDir, safeSessionId } from '../lib/impeccable-paths.mjs';
import {
SOURCE_ARTIFACT_PREVIEW_MODE,
findSourceArtifactManifest,
@@ -13,6 +13,28 @@ export function sha256(value) {
return createHash('sha256').update(value).digest('hex');
}
/**
* Delete a session's staged revision artifacts.
*
* Nothing used to remove these, and they are the reason a Live accept could
* resolve to the wrong file: `<id>-r<n>.<source-ext>` carries the session marker,
* so it is a decoy for any marker search that walks the project. live-accept no
* longer searches `.impeccable`, but the artifacts should not outlive the session
* they belong to either. Called on accept and discard.
*/
export function removeGenerationArtifacts(id, cwd = process.cwd()) {
let removed = 0;
try { safeSessionId(id); } catch { return removed; }
const artifactDir = path.join(getLiveDir(cwd), 'artifacts');
let entries;
try { entries = fs.readdirSync(artifactDir); } catch { return removed; }
for (const name of entries) {
if (!name.startsWith(id + '-r')) continue;
try { fs.rmSync(path.join(artifactDir, name), { force: true }); removed += 1; } catch { /* best effort */ }
}
return removed;
}
export function prepareGenerationArtifact({ id, sourceFile, cwd = process.cwd() } = {}) {
if (!id) return failure('missing_session_id');
if (!sourceFile) return failure('missing_file');