From eebfb7c2ce65c21d8682c0e2de2c2865b68de7bd Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sat, 5 Sep 2026 10:47:32 -0700 Subject: [PATCH] Release: CLI 4.0.2 and engine 0.1.1 Ship signed skill-bundle verification, fix annotated-session checkpoint ordering, and pin all published engine platform packages. Validated with Rust, Node, browser, and provider-backed end-to-end tests. AI assistance: prepared and validated with Codex under Paul Bakaus direction. --- Cargo.lock | 32 +++++++------- Cargo.toml | 2 +- ENGINE_VERSION | 2 +- bun.lock | 20 ++++++--- package.json | 12 +++--- skill/scripts/VERSION | 2 +- skill/scripts/live-browser.js | 10 +++-- .../vite8-sveltekit-stateful/fixture.json | 6 +++ tests/live-browser-source.test.mjs | 43 ++++++++++++++++++- tests/live-e2e-llm-agent.test.mjs | 14 ++++++ tests/live-e2e.test.mjs | 16 ++++--- tests/live-e2e/agents/llm-agent.mjs | 19 +++++++- tests/live-svelte-adapter-deepseek.test.mjs | 18 +++----- 13 files changed, 143 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 525e7b489..c844692f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index dcca399c8..7902fb211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/ENGINE_VERSION b/ENGINE_VERSION index 6e8bf73aa..17e51c385 100644 --- a/ENGINE_VERSION +++ b/ENGINE_VERSION @@ -1 +1 @@ -0.1.0 +0.1.1 diff --git a/bun.lock b/bun.lock index ac2a8bcbf..0c640563e 100644 --- a/bun.lock +++ b/bun.lock @@ -19,11 +19,11 @@ "zod": "^4.3.6", }, "optionalDependencies": { - "@impeccable/cli-darwin-arm64": "0.1.0", - "@impeccable/cli-darwin-x64": "0.1.0", - "@impeccable/cli-linux-arm64": "0.1.0", - "@impeccable/cli-linux-x64": "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-arm64": "0.1.1", + "@impeccable/cli-linux-x64": "0.1.1", + "@impeccable/cli-windows-x64": "0.1.1", }, }, }, @@ -72,6 +72,16 @@ "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + "@impeccable/cli-darwin-arm64": ["@impeccable/cli-darwin-arm64@0.1.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "impeccable-darwin-arm64": "bin/impeccable" } }, "sha512-1/DZYaiZqDoNwpXpyoG4gRQpgLZ4YDCMADGzA5wYmNiG279b3KPt2RLYUww6NF2hxoMxdVQUAGzdc5062KZKHg=="], + + "@impeccable/cli-darwin-x64": ["@impeccable/cli-darwin-x64@0.1.1", "", { "os": "darwin", "cpu": "x64", "bin": { "impeccable-darwin-x64": "bin/impeccable" } }, "sha512-/itjFZEHPcz1RQDBx3+2aeTebQ4pCD30VKAJ3Zst5KxHteJQOW3hoiTFE7jAOXg3vG2D/qAaEh/7Eo+xzEawew=="], + + "@impeccable/cli-linux-arm64": ["@impeccable/cli-linux-arm64@0.1.1", "", { "os": "linux", "cpu": "arm64", "bin": { "impeccable-linux-arm64": "bin/impeccable" } }, "sha512-uGJ2DNVq3NzH8+RlTlyn1XWpAsNS1bv6kix7PsAnzCa0aBiOHaYSUkPXbqDVUTDy9X4oEOsZYuzbgoY+otkoMQ=="], + + "@impeccable/cli-linux-x64": ["@impeccable/cli-linux-x64@0.1.1", "", { "os": "linux", "cpu": "x64", "bin": { "impeccable-linux-x64": "bin/impeccable" } }, "sha512-wPul+V7w9g0MZAgFmJJvEOXpy8AYt4htmLQJIx7XvakSp8CS+PYBjEFqmWSW41NlrNAaifzDKdUe6rzeRli97A=="], + + "@impeccable/cli-windows-x64": ["@impeccable/cli-windows-x64@0.1.1", "", { "os": "win32", "cpu": "x64", "bin": { "impeccable-windows-x64": "bin/impeccable.exe" } }, "sha512-dqcQ8VQFschjA1iFzhKvO44UEzsbQQGHAZW3MmsxJbqo/uSDENIL8WA5l4K8Q9wqjf4Kbc/H1hqo6MjOuQw85A=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], diff --git a/package.json b/package.json index 2ed7e66b2..e69627040 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/skill/scripts/VERSION b/skill/scripts/VERSION index 6e8bf73aa..17e51c385 100644 --- a/skill/scripts/VERSION +++ b/skill/scripts/VERSION @@ -1 +1 @@ -0.1.0 +0.1.1 diff --git a/skill/scripts/live-browser.js b/skill/scripts/live-browser.js index 277c60e2c..ac6f18586 100644 --- a/skill/scripts/live-browser.js +++ b/skill/scripts/live-browser.js @@ -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'); } } diff --git a/tests/framework-fixtures/vite8-sveltekit-stateful/fixture.json b/tests/framework-fixtures/vite8-sveltekit-stateful/fixture.json index 945c90668..a2f8fa6ba 100644 --- a/tests/framework-fixtures/vite8-sveltekit-stateful/fixture.json +++ b/tests/framework-fixtures/vite8-sveltekit-stateful/fixture.json @@ -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, diff --git a/tests/live-browser-source.test.mjs b/tests/live-browser-source.test.mjs index 1148a5542..6f4f440c6 100644 --- a/tests/live-browser-source.test.mjs +++ b/tests/live-browser-source.test.mjs @@ -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', ); }); diff --git a/tests/live-e2e-llm-agent.test.mjs b/tests/live-e2e-llm-agent.test.mjs index 193178d89..9f06a9c38 100644 --- a/tests/live-e2e-llm-agent.test.mjs +++ b/tests/live-e2e-llm-agent.test.mjs @@ -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({}, {}); diff --git a/tests/live-e2e.test.mjs b/tests/live-e2e.test.mjs index e03d5cf85..f4224c4b7 100644 --- a/tests/live-e2e.test.mjs +++ b/tests/live-e2e.test.mjs @@ -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); diff --git a/tests/live-e2e/agents/llm-agent.mjs b/tests/live-e2e/agents/llm-agent.mjs index 4128ba924..7368c23e9 100644 --- a/tests/live-e2e/agents/llm-agent.mjs +++ b/tests/live-e2e/agents/llm-agent.mjs @@ -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; diff --git a/tests/live-svelte-adapter-deepseek.test.mjs b/tests/live-svelte-adapter-deepseek.test.mjs index af37031ed..7ec030a17 100644 --- a/tests/live-svelte-adapter-deepseek.test.mjs +++ b/tests/live-svelte-adapter-deepseek.test.mjs @@ -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 }) {