Compare commits

...
Author SHA1 Message Date
Paul Bakaus 2564531fd0 Fix: checkpoint annotated sessions after creation
Defer generate_started until the server acknowledges the generate event, including screenshot capture/upload. Guard failed and superseded creation. Cover delayed creation in source-function tests and a slow-upload Svelte browser regression without reload recovery.

Prepared with AI assistance under maintainer direction.
2026-09-05 10:21:47 -07:00
Paul Bakaus 8c880d75f9 Fix: retain bounded DeepSeek planning in release tests
Use explicit low-effort thinking: non-thinking mode was unreliable for the full live specification, while the provider high-effort default exhausted bounded responses. Keep real provider edits and existing assertions.

Prepared with AI assistance under maintainer direction.
2026-09-04 19:33:40 -07:00
Paul Bakaus cc988144a0 Fix: wait for settled Svelte test variants
Reuse the shared mount-aware cycling helper before subsequent clicks and reload. Keep the DeepSeek request limits scoped to that provider so other provider defaults stay unchanged.

Prepared with AI assistance under maintainer direction.
2026-09-04 19:29:46 -07:00
Paul Bakaus dc5c4b2184 Fix: bound DeepSeek release-test requests
Explicitly disable thinking for the bounded JSON edit harness and give steer the same timeout/retry limits as manual edits. Keep real provider calls and all browser assertions intact.

Prepared with AI assistance under maintainer direction.
2026-09-04 19:24:42 -07:00
Paul Bakaus 9a7fae4361 Release: prepare engine 0.1.1 and CLI 4.0.2
Activate the signed-bundle verifier from #734. Publish the engine binaries and platform packages before merging these pins and publishing the CLI. The signed skill 4.2.0 ZIP remains unchanged.

AI-assisted release preparation by Codex at Paul Bakaus’s request.
2026-09-04 18:25:37 -07:00
12 changed files with 128 additions and 48 deletions
Generated
+16 -16
View File
@@ -509,7 +509,7 @@ dependencies = [
[[package]]
name = "impeccable"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"base64",
"impeccable-browser",
@@ -528,7 +528,7 @@ dependencies = [
[[package]]
name = "impeccable-browser"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"base64",
"impeccable-core",
@@ -542,7 +542,7 @@ dependencies = [
[[package]]
name = "impeccable-bundle"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"base64",
"impeccable-core",
@@ -551,14 +551,14 @@ dependencies = [
[[package]]
name = "impeccable-common"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"libc",
]
[[package]]
name = "impeccable-comp"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"image",
"once_cell",
@@ -570,7 +570,7 @@ dependencies = [
[[package]]
name = "impeccable-comp-verbs"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"impeccable-common",
"impeccable-comp",
@@ -583,7 +583,7 @@ dependencies = [
[[package]]
name = "impeccable-context"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"flate2",
"impeccable-common",
@@ -600,7 +600,7 @@ dependencies = [
[[package]]
name = "impeccable-core"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"impeccable-core",
"impeccable-foundation",
@@ -612,7 +612,7 @@ dependencies = [
[[package]]
name = "impeccable-detect"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"impeccable-common",
"impeccable-core",
@@ -624,7 +624,7 @@ dependencies = [
[[package]]
name = "impeccable-foundation"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"cssparser",
"once_cell",
@@ -637,7 +637,7 @@ dependencies = [
[[package]]
name = "impeccable-hook"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"impeccable-common",
"impeccable-context",
@@ -651,7 +651,7 @@ dependencies = [
[[package]]
name = "impeccable-html"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"cssparser",
"ego-tree",
@@ -672,7 +672,7 @@ dependencies = [
[[package]]
name = "impeccable-live"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"getrandom 0.2.17",
"impeccable-common",
@@ -690,7 +690,7 @@ dependencies = [
[[package]]
name = "impeccable-skills"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"impeccable-common",
"impeccable-context",
@@ -709,7 +709,7 @@ dependencies = [
[[package]]
name = "impeccable-wasm"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"impeccable-core",
"impeccable-detect",
@@ -1681,7 +1681,7 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "xtask"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"impeccable-bundle",
]
+1 -1
View File
@@ -6,7 +6,7 @@ resolver = "2"
members = ["crates/*"]
[workspace.package]
version = "0.1.0"
version = "0.1.1"
edition = "2021"
license = "Apache-2.0"
publish = false
+1 -1
View File
@@ -1 +1 @@
0.1.0
0.1.1
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "impeccable",
"version": "4.0.1",
"version": "4.0.2",
"author": "Paul Bakaus",
"description": "Design skills, commands, and anti-pattern detection for AI coding agents",
"keywords": [
@@ -71,11 +71,11 @@
"check:engine-release": "node scripts/check-engine-release.mjs"
},
"optionalDependencies": {
"@impeccable/cli-darwin-arm64": "0.1.0",
"@impeccable/cli-darwin-x64": "0.1.0",
"@impeccable/cli-linux-x64": "0.1.0",
"@impeccable/cli-linux-arm64": "0.1.0",
"@impeccable/cli-windows-x64": "0.1.0"
"@impeccable/cli-darwin-arm64": "0.1.1",
"@impeccable/cli-darwin-x64": "0.1.1",
"@impeccable/cli-linux-x64": "0.1.1",
"@impeccable/cli-linux-arm64": "0.1.1",
"@impeccable/cli-windows-x64": "0.1.1"
},
"devDependencies": {
"@ai-sdk/anthropic": "^4.0.7",
+1 -1
View File
@@ -1 +1 @@
0.1.0
0.1.1
+6 -4
View File
@@ -7834,7 +7834,6 @@
if (editBadgeEl && editBadgeEl.style.display !== 'none') renderEditBadge('idle-disabled');
showBar('generating');
saveSession();
sendCheckpoint('generate_started');
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
@@ -7916,7 +7915,6 @@
showBar('generating');
startScrollTracking();
saveSession();
sendCheckpoint('generate_started');
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
@@ -8238,7 +8236,8 @@
// rasterization from delaying the fetch itself.
if (!hasAnnotations) {
basePayload.clientSentAt = Date.now();
await sendEvent(basePayload);
const created = await sendEvent(basePayload);
if (created?.ok && currentSessionId === basePayload.id) sendCheckpoint('generate_started');
}
let screenshotPath;
@@ -8279,7 +8278,10 @@
// is semantic input. Plain requests were already dispatched above.
if (hasAnnotations) {
basePayload.clientSentAt = Date.now();
sendEvent(screenshotPath ? { ...basePayload, screenshotPath } : basePayload);
const created = await sendEvent(screenshotPath ? { ...basePayload, screenshotPath } : basePayload);
// Capture/upload can take seconds. Progress before this acknowledgment
// refers to an unknown session and would clear our own active work.
if (created?.ok && currentSessionId === basePayload.id) sendCheckpoint('generate_started');
}
}
@@ -57,6 +57,12 @@
"readyPattern": "Local:\\s+https?://[^:]+:(\\d+)",
"readyTimeoutMs": 120000,
"steer": false,
"liveChrome": {
"annotations": {
"selector": "h1.hero-title",
"uploadDelayMs": 300
}
},
"pickSelector": "ul.expense-list",
"pickPosition": {
"x": 10,
+42 -1
View File
@@ -2,12 +2,53 @@ import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { runInNewContext } from 'node:vm';
const SOURCE = readFileSync(join(process.cwd(), 'skill/scripts/live-browser.js'), 'utf-8');
const PENDING_DOCK_POSITION_SOURCE = SOURCE.match(/function positionPendingDock\(\) \{[\s\S]*?\n \}/)?.[0] || '';
const CAPTURE_AND_EMIT_SOURCE = SOURCE.match(/async function captureAndEmit\([\s\S]*?\n \}/)?.[0] || '';
describe('live-browser source contracts', () => {
it('does not checkpoint a generation before captureAndEmit creates its session', () => {
for (const name of ['handleGo', 'handleInsertCreate']) {
const body = SOURCE.match(new RegExp(`function ${name}\\(\\) \\{[\\s\\S]*?\\n \\}`))?.[0];
assert.ok(body);
assert.doesNotMatch(body, /sendCheckpoint\('generate_started'\)/);
}
});
for (const annotated of [false, true]) {
for (const outcome of ['created', 'failed', 'superseded']) {
it(`${annotated ? 'annotated' : 'plain'} generation checkpoints only its acknowledged current session (${outcome})`, async () => {
const capture = Promise.withResolvers();
const creation = Promise.withResolvers();
const events = [];
const context = {
currentSessionId: 'session-a', state: 'GENERATING', PORT: 1234, TOKEN: 'test',
console, Date,
captureElementToBlob: () => capture.promise,
showShaderOverlay() {},
fetch: async () => ({ ok: true, json: async () => ({ path: '/annotation.png' }) }),
sendEvent: async (payload) => { events.push(payload.type); return creation.promise; },
sendCheckpoint: (reason) => events.push(reason),
};
const emit = runInNewContext(`(${CAPTURE_AND_EMIT_SOURCE})`, context);
const pending = emit({}, { type: 'generate', id: 'session-a' }, {
comments: annotated ? [{ text: 'change title' }] : [], strokes: [],
}, {});
await new Promise(resolve => setImmediate(resolve));
assert.deepEqual(events, annotated ? [] : ['generate']);
capture.resolve({ blob: {}, paper: 'white' });
await new Promise(resolve => setImmediate(resolve));
assert.deepEqual(events, ['generate'], 'capture/upload must not checkpoint before creation is acknowledged');
if (outcome === 'superseded') context.currentSessionId = 'session-b';
creation.resolve(outcome === 'failed' ? null : { ok: true });
await pending;
assert.deepEqual(events, outcome === 'created' ? ['generate', 'generate_started'] : ['generate']);
});
}
}
it('reports foreground poll connectivity without a background worker dependency', () => {
assert.match(
SOURCE,
@@ -29,7 +70,7 @@ describe('live-browser source contracts', () => {
);
assert.match(
CAPTURE_AND_EMIT_SOURCE,
/if \(hasAnnotations\) \{[\s\S]*?basePayload\.clientSentAt = Date\.now\(\);\s*sendEvent\(screenshotPath \? \{ \.\.\.basePayload, screenshotPath \} : basePayload\);\s*\}/,
/if \(hasAnnotations\) \{[\s\S]*?basePayload\.clientSentAt = Date\.now\(\);\s*const created = await sendEvent\(screenshotPath \? \{ \.\.\.basePayload, screenshotPath \} : basePayload\);/,
'annotated generation should dispatch exactly after capture and upload resolve',
);
});
+14
View File
@@ -7,6 +7,7 @@ import {
MANUAL_EDIT_SYSTEM_INSTRUCTIONS,
VARIANT_SYSTEM_INSTRUCTIONS,
createLlmAgent,
llmRequestSettings,
parseManualEditResponse,
parseVariantResponse,
progressiveVariantGuidance,
@@ -19,6 +20,19 @@ import {
validateVariantVisibleCopy,
} from './live-e2e/agents/llm-agent.mjs';
describe('live-e2e LLM request settings', () => {
it('explicitly selects low-effort DeepSeek thinking for bounded JSON edit requests', () => {
assert.deepEqual(llmRequestSettings('deepseek'), {
thinking: { type: 'enabled' }, output_config: { effort: 'low' },
});
});
it('leaves other providers unchanged', () => {
assert.deepEqual(llmRequestSettings('anthropic'), {});
assert.deepEqual(llmRequestSettings('openai'), {});
});
});
describe('live-e2e LLM agent provider config', () => {
it('defaults to OpenAI gpt-5.6-terra at medium reasoning effort', () => {
const config = resolveLlmAgentConfig({}, {});
+10 -6
View File
@@ -1283,17 +1283,21 @@ for (const { name, fixture } of fixtures) {
const pickSelector = annotation.selector || fixture.runtime.pickSelector || 'h1.hero-title';
try {
await waitForHandshake(page);
if (annotation.uploadDelayMs) {
await page.route('**/annotation?*', async (route) => {
await new Promise(resolve => setTimeout(resolve, annotation.uploadDelayMs));
await route.continue();
});
}
if (fixture.runtime.preActions) await runPreActions(page, fixture.runtime.preActions);
await pickElement(page, pickSelector, { resetPickMode: true });
await drawAnnotationPinAndStroke(page, {
comment: annotation.comment || 'Make this selected element easier to scan',
});
await clickGo(page);
await waitForCyclingRobust(page, 3, {
agentMode,
preActions: fixture.runtime.preActions,
log: (m) => t.diagnostic(m),
});
// A reload would mask a checkpoint-before-creation race by adopting
// the session again. Annotated generation must complete in this tab.
await waitForCycling(page, 3, { timeout: agentMode === 'llm' ? 180_000 : 30_000 });
const generateEvent = recordedGenerateEvents.at(-1);
await assertAnnotationUploadEvent(generateEvent);
@@ -1302,7 +1306,7 @@ for (const { name, fixture } of fixtures) {
const sourceFile = await locateSessionFile(session.appRoot);
const svelteComponentTarget = svelteComponentTargetFor(sourceFile);
await clickNext(page);
await cycleToVariant(page, 2, 3);
assert.equal(await getVisibleVariant(page), 2, 'variant 2 visible after annotated generate');
await clickAccept(page, { expectedVariant: 2 });
await waitForBarHidden(page);
+18 -1
View File
@@ -248,6 +248,17 @@ function resolveProvider(opts, env) {
return 'openai';
}
export function llmRequestSettings(provider) {
// DeepSeek defaults to high-effort thinking, which can consume the entire
// bounded response before emitting the JSON these edit tests exercise.
// Low effort retains planning for the full live spec without inheriting
// the provider's high-effort default.
// https://api-docs.deepseek.com/guides/thinking_mode/
return provider === 'deepseek'
? { thinking: { type: 'enabled' }, output_config: { effort: 'low' } }
: {};
}
/**
* Anthropic-SDK-shaped shim over the `ai` SDK for OpenAI models, so the
* three text-only call sites in this file stay provider-agnostic. system
@@ -331,6 +342,7 @@ export async function createLlmAgent(opts = {}) {
try {
response = await client.messages.create(
{
...llmRequestSettings(provider),
model,
temperature: 0,
max_tokens: 16000,
@@ -476,6 +488,7 @@ export async function createLlmAgent(opts = {}) {
try {
response = await client.messages.create(
{
...llmRequestSettings(provider),
model,
temperature: 0,
max_tokens: 16000,
@@ -605,11 +618,15 @@ export async function createLlmAgent(opts = {}) {
].join('\n');
const response = await client.messages.create({
...llmRequestSettings(provider),
model,
max_tokens: 4096,
system: systemBlocks(STEER_SYSTEM_INSTRUCTIONS),
messages: [{ role: 'user', content: userMessage }],
});
}, provider === 'deepseek' ? {
maxRetries: LLM_REQUEST_MAX_RETRIES,
timeout: MANUAL_EDIT_REQUEST_TIMEOUT_MS,
} : {});
const cacheRead = response.usage?.cache_read_input_tokens ?? 0;
const inputTokens = response.usage?.input_tokens ?? 0;
+7 -11
View File
@@ -26,8 +26,7 @@ import {
clickEditCopy,
clickExitLiveMode,
clickGo,
clickNext,
clickPrev,
cycleToVariant,
clickSaveEdit,
drawAnnotationPinAndStroke,
editTextLeaf,
@@ -40,6 +39,7 @@ import {
waitForBarHidden,
waitForCycling,
waitForHandshake,
waitForVariantSettled,
} from './live-e2e/ui.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -281,7 +281,7 @@ async function runAnnotationGenerateFlow({ page, tmp, evidence }) {
const generateEvent = latestJournalEvent(tmp, (event) => event.type === 'generate' && event.screenshotPath);
await assertAnnotationUploadEvent(generateEvent);
assert.ok(existsSync(generateEvent.screenshotPath), 'annotation screenshot file exists');
await clickNext(page);
await cycleTo(page, 2);
await assertVariantCounter(page, 2, 3);
await evidence.capture('annotation-cycle');
await clickDiscard(page);
@@ -514,14 +514,10 @@ async function clickPendingTrash(page) {
}
async function cycleTo(page, target) {
for (let i = 0; i < 6; i++) {
const visible = await getVisibleVariant(page);
if (visible === target) return;
if (visible == null) await page.waitForTimeout(250);
else if (visible < target) await clickNext(page);
else await clickPrev(page);
}
assert.equal(await getVisibleVariant(page), target, `variant ${target} visible`);
// Component imports finish after the counter changes. Do not send the next
// click (or reload) while the previous variant is still mounting.
await cycleToVariant(page, target, 3);
await waitForVariantSettled(page, target, 3);
}
async function waitForVisibleCycling(page, count, { timeout }) {