From 1ba75a820e57d77e1de6b42426ed6db98633c772 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 22 Apr 2026 09:32:31 -0700 Subject: [PATCH] fix(skill): 3 review-bot findings from EAC PR 1. cleanup-deprecated: strip `i-` prefix before fingerprint lookup so `i-harden` / `i-optimize` classify correctly (regression from the prefixed-naming migration). 2. build: substitute `{{scripts_path}}` in reference/*.md the same way it's substituted in SKILL.md. Previously the placeholder survived unresolved in built reference files, so any reference that told the agent to run a scripts path emitted a literal `{{scripts_path}}` to the shell. 3. live-poll: drop the `undici` import. Node's built-in fetch enforces a 300s headers timeout that can't be lowered per-request, so we now cap each poll slice at 270s and loop internally until a real event or the caller's total timeout. Removes the hard `ERR_MODULE_NOT_FOUND` failure when undici isn't transitively hoisted. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skills/impeccable/reference/document.md | 4 +- .agents/skills/impeccable/reference/live.md | 12 ++-- .agents/skills/impeccable/reference/teach.md | 4 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .../skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- .../skills/impeccable/reference/document.md | 4 +- .claude/skills/impeccable/reference/live.md | 12 ++-- .claude/skills/impeccable/reference/teach.md | 4 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .../skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- .../skills/impeccable/reference/document.md | 4 +- .cursor/skills/impeccable/reference/live.md | 12 ++-- .cursor/skills/impeccable/reference/teach.md | 4 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .../skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- .../skills/impeccable/reference/document.md | 4 +- .gemini/skills/impeccable/reference/live.md | 12 ++-- .gemini/skills/impeccable/reference/teach.md | 4 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .../skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- .../skills/impeccable/reference/document.md | 4 +- .github/skills/impeccable/reference/live.md | 12 ++-- .github/skills/impeccable/reference/teach.md | 4 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .../skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- .kiro/skills/impeccable/reference/document.md | 4 +- .kiro/skills/impeccable/reference/live.md | 12 ++-- .kiro/skills/impeccable/reference/teach.md | 4 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .kiro/skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- .../skills/impeccable/reference/document.md | 4 +- .opencode/skills/impeccable/reference/live.md | 12 ++-- .../skills/impeccable/reference/teach.md | 4 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .../skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- .pi/skills/impeccable/reference/document.md | 4 +- .pi/skills/impeccable/reference/live.md | 12 ++-- .pi/skills/impeccable/reference/teach.md | 4 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .pi/skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- .../skills/impeccable/reference/document.md | 4 +- .rovodev/skills/impeccable/reference/live.md | 12 ++-- .rovodev/skills/impeccable/reference/teach.md | 4 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .../skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- .../skills/impeccable/reference/document.md | 4 +- .trae-cn/skills/impeccable/reference/live.md | 12 ++-- .trae-cn/skills/impeccable/reference/teach.md | 4 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .../skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- .trae/skills/impeccable/reference/document.md | 4 +- .trae/skills/impeccable/reference/live.md | 12 ++-- .trae/skills/impeccable/reference/teach.md | 4 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .trae/skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- scripts/lib/transformers/factory.js | 3 +- .../impeccable/scripts/cleanup-deprecated.mjs | 5 +- .../skills/impeccable/scripts/live-poll.mjs | 60 ++++++++++++------- 58 files changed, 616 insertions(+), 387 deletions(-) diff --git a/.agents/skills/impeccable/reference/document.md b/.agents/skills/impeccable/reference/document.md index 1be6c9404..9ba4ee17a 100644 --- a/.agents/skills/impeccable/reference/document.md +++ b/.agents/skills/impeccable/reference/document.md @@ -277,7 +277,7 @@ Do not reword. The panel shows these as secondary collapsible context; the same 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `DESIGN.json` was also written alongside — the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. +4. **Refresh the session cache.** Run `node .agents/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. ## Seed mode @@ -342,7 +342,7 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `$impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. +3. Run `node .agents/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. ## Style guidelines diff --git a/.agents/skills/impeccable/reference/live.md b/.agents/skills/impeccable/reference/live.md index 6c0faa78a..957ea2f0e 100644 --- a/.agents/skills/impeccable/reference/live.md +++ b/.agents/skills/impeccable/reference/live.md @@ -25,7 +25,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi ## Start ```bash -node {{scripts_path}}/live.mjs +node .agents/skills/impeccable/scripts/live.mjs ``` Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `$impeccable document` for the matching DESIGN.md. @@ -38,7 +38,7 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ``` LOOP: - node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= + node .agents/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP @@ -71,7 +71,7 @@ Reading annotations precisely: ### 2. Wrap the element ```bash -node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" +node .agents/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" ``` Flag mapping — keep them separate, don't collapse into `--query`: @@ -181,7 +181,7 @@ One edit, all variants — the browser's MutationObserver picks everything up in ### 7. Signal done ```bash -node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH +node .agents/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH ``` `RELATIVE_PATH` is relative to project root (`public/index.html`, `src/App.tsx`, etc.) — the browser fetches source directly if the dev server lacks HMR. @@ -272,7 +272,7 @@ When the poll returns `exit`, proceed to cleanup. If the poll is still running a ## Cleanup ```bash -node {{scripts_path}}/live-server.mjs stop +node .agents/skills/impeccable/scripts/live-server.mjs stop ``` Stops the HTTP server and runs `live-inject.mjs --remove` to strip `localhost:…/live.js` from the HTML entry. To stop the server but keep the inject tag (for a quick restart), use `stop --keep-inject`. `config.json` persists for future sessions. @@ -321,7 +321,7 @@ If `config.cspChecked === true`, skip this entire section. You already asked thi Otherwise, run the detection helper: ```bash -node {{scripts_path}}/detect-csp.mjs +node .agents/skills/impeccable/scripts/detect-csp.mjs ``` Output: `{ shape, signals }` where `shape` is one of `append-arrays`, `append-string`, `middleware`, `meta-tag`, or `null`. The shape is named by *patch mechanism*, so one template covers many frameworks. diff --git a/.agents/skills/impeccable/reference/teach.md b/.agents/skills/impeccable/reference/teach.md index 8e862584e..7a829abc5 100644 --- a/.agents/skills/impeccable/reference/teach.md +++ b/.agents/skills/impeccable/reference/teach.md @@ -12,7 +12,7 @@ Every other impeccable command reads these files before doing any work. Run the shared loader first so you know what already exists: ```bash -node {{scripts_path}}/load-context.mjs +node .agents/skills/impeccable/scripts/load-context.mjs ``` The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. @@ -130,7 +130,7 @@ Summarize: - The 3-5 strategic principles from PRODUCT.md that will guide future work - If DESIGN.md is pending, remind the user how to generate it later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .agents/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. If teach was invoked as a blocker by another impeccable command (e.g. the user ran `$impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. diff --git a/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs b/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/.agents/skills/impeccable/scripts/live-poll.mjs b/.agents/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/.agents/skills/impeccable/scripts/live-poll.mjs +++ b/.agents/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/.claude/skills/impeccable/reference/document.md b/.claude/skills/impeccable/reference/document.md index bccc18d77..74f35efc5 100644 --- a/.claude/skills/impeccable/reference/document.md +++ b/.claude/skills/impeccable/reference/document.md @@ -277,7 +277,7 @@ Do not reword. The panel shows these as secondary collapsible context; the same 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `DESIGN.json` was also written alongside — the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. +4. **Refresh the session cache.** Run `node .claude/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. ## Seed mode @@ -342,7 +342,7 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. +3. Run `node .claude/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. ## Style guidelines diff --git a/.claude/skills/impeccable/reference/live.md b/.claude/skills/impeccable/reference/live.md index 89d633161..bee92fbf4 100644 --- a/.claude/skills/impeccable/reference/live.md +++ b/.claude/skills/impeccable/reference/live.md @@ -25,7 +25,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi ## Start ```bash -node {{scripts_path}}/live.mjs +node .claude/skills/impeccable/scripts/live.mjs ``` Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. @@ -38,7 +38,7 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ``` LOOP: - node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= + node .claude/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP @@ -71,7 +71,7 @@ Reading annotations precisely: ### 2. Wrap the element ```bash -node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" +node .claude/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" ``` Flag mapping — keep them separate, don't collapse into `--query`: @@ -181,7 +181,7 @@ One edit, all variants — the browser's MutationObserver picks everything up in ### 7. Signal done ```bash -node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH +node .claude/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH ``` `RELATIVE_PATH` is relative to project root (`public/index.html`, `src/App.tsx`, etc.) — the browser fetches source directly if the dev server lacks HMR. @@ -272,7 +272,7 @@ When the poll returns `exit`, proceed to cleanup. If the poll is still running a ## Cleanup ```bash -node {{scripts_path}}/live-server.mjs stop +node .claude/skills/impeccable/scripts/live-server.mjs stop ``` Stops the HTTP server and runs `live-inject.mjs --remove` to strip `localhost:…/live.js` from the HTML entry. To stop the server but keep the inject tag (for a quick restart), use `stop --keep-inject`. `config.json` persists for future sessions. @@ -321,7 +321,7 @@ If `config.cspChecked === true`, skip this entire section. You already asked thi Otherwise, run the detection helper: ```bash -node {{scripts_path}}/detect-csp.mjs +node .claude/skills/impeccable/scripts/detect-csp.mjs ``` Output: `{ shape, signals }` where `shape` is one of `append-arrays`, `append-string`, `middleware`, `meta-tag`, or `null`. The shape is named by *patch mechanism*, so one template covers many frameworks. diff --git a/.claude/skills/impeccable/reference/teach.md b/.claude/skills/impeccable/reference/teach.md index 95d1e29ea..cca736ba5 100644 --- a/.claude/skills/impeccable/reference/teach.md +++ b/.claude/skills/impeccable/reference/teach.md @@ -12,7 +12,7 @@ Every other impeccable command reads these files before doing any work. Run the shared loader first so you know what already exists: ```bash -node {{scripts_path}}/load-context.mjs +node .claude/skills/impeccable/scripts/load-context.mjs ``` The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. @@ -130,7 +130,7 @@ Summarize: - The 3-5 strategic principles from PRODUCT.md that will guide future work - If DESIGN.md is pending, remind the user how to generate it later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .claude/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. diff --git a/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs b/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/.claude/skills/impeccable/scripts/live-poll.mjs b/.claude/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/.claude/skills/impeccable/scripts/live-poll.mjs +++ b/.claude/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/.cursor/skills/impeccable/reference/document.md b/.cursor/skills/impeccable/reference/document.md index 49cabda0c..244a1927f 100644 --- a/.cursor/skills/impeccable/reference/document.md +++ b/.cursor/skills/impeccable/reference/document.md @@ -277,7 +277,7 @@ Do not reword. The panel shows these as secondary collapsible context; the same 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `DESIGN.json` was also written alongside — the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. +4. **Refresh the session cache.** Run `node .cursor/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. ## Seed mode @@ -342,7 +342,7 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. +3. Run `node .cursor/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. ## Style guidelines diff --git a/.cursor/skills/impeccable/reference/live.md b/.cursor/skills/impeccable/reference/live.md index 89d633161..7aaab09fb 100644 --- a/.cursor/skills/impeccable/reference/live.md +++ b/.cursor/skills/impeccable/reference/live.md @@ -25,7 +25,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi ## Start ```bash -node {{scripts_path}}/live.mjs +node .cursor/skills/impeccable/scripts/live.mjs ``` Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. @@ -38,7 +38,7 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ``` LOOP: - node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= + node .cursor/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP @@ -71,7 +71,7 @@ Reading annotations precisely: ### 2. Wrap the element ```bash -node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" +node .cursor/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" ``` Flag mapping — keep them separate, don't collapse into `--query`: @@ -181,7 +181,7 @@ One edit, all variants — the browser's MutationObserver picks everything up in ### 7. Signal done ```bash -node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH +node .cursor/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH ``` `RELATIVE_PATH` is relative to project root (`public/index.html`, `src/App.tsx`, etc.) — the browser fetches source directly if the dev server lacks HMR. @@ -272,7 +272,7 @@ When the poll returns `exit`, proceed to cleanup. If the poll is still running a ## Cleanup ```bash -node {{scripts_path}}/live-server.mjs stop +node .cursor/skills/impeccable/scripts/live-server.mjs stop ``` Stops the HTTP server and runs `live-inject.mjs --remove` to strip `localhost:…/live.js` from the HTML entry. To stop the server but keep the inject tag (for a quick restart), use `stop --keep-inject`. `config.json` persists for future sessions. @@ -321,7 +321,7 @@ If `config.cspChecked === true`, skip this entire section. You already asked thi Otherwise, run the detection helper: ```bash -node {{scripts_path}}/detect-csp.mjs +node .cursor/skills/impeccable/scripts/detect-csp.mjs ``` Output: `{ shape, signals }` where `shape` is one of `append-arrays`, `append-string`, `middleware`, `meta-tag`, or `null`. The shape is named by *patch mechanism*, so one template covers many frameworks. diff --git a/.cursor/skills/impeccable/reference/teach.md b/.cursor/skills/impeccable/reference/teach.md index 87cc872f5..87392dd1c 100644 --- a/.cursor/skills/impeccable/reference/teach.md +++ b/.cursor/skills/impeccable/reference/teach.md @@ -12,7 +12,7 @@ Every other impeccable command reads these files before doing any work. Run the shared loader first so you know what already exists: ```bash -node {{scripts_path}}/load-context.mjs +node .cursor/skills/impeccable/scripts/load-context.mjs ``` The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. @@ -130,7 +130,7 @@ Summarize: - The 3-5 strategic principles from PRODUCT.md that will guide future work - If DESIGN.md is pending, remind the user how to generate it later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .cursor/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. diff --git a/.cursor/skills/impeccable/scripts/cleanup-deprecated.mjs b/.cursor/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/.cursor/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.cursor/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/.cursor/skills/impeccable/scripts/live-poll.mjs b/.cursor/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/.cursor/skills/impeccable/scripts/live-poll.mjs +++ b/.cursor/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/.gemini/skills/impeccable/reference/document.md b/.gemini/skills/impeccable/reference/document.md index 49cabda0c..6f74e2aaa 100644 --- a/.gemini/skills/impeccable/reference/document.md +++ b/.gemini/skills/impeccable/reference/document.md @@ -277,7 +277,7 @@ Do not reword. The panel shows these as secondary collapsible context; the same 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `DESIGN.json` was also written alongside — the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. +4. **Refresh the session cache.** Run `node .gemini/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. ## Seed mode @@ -342,7 +342,7 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. +3. Run `node .gemini/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. ## Style guidelines diff --git a/.gemini/skills/impeccable/reference/live.md b/.gemini/skills/impeccable/reference/live.md index 89d633161..9e371813f 100644 --- a/.gemini/skills/impeccable/reference/live.md +++ b/.gemini/skills/impeccable/reference/live.md @@ -25,7 +25,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi ## Start ```bash -node {{scripts_path}}/live.mjs +node .gemini/skills/impeccable/scripts/live.mjs ``` Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. @@ -38,7 +38,7 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ``` LOOP: - node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= + node .gemini/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP @@ -71,7 +71,7 @@ Reading annotations precisely: ### 2. Wrap the element ```bash -node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" +node .gemini/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" ``` Flag mapping — keep them separate, don't collapse into `--query`: @@ -181,7 +181,7 @@ One edit, all variants — the browser's MutationObserver picks everything up in ### 7. Signal done ```bash -node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH +node .gemini/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH ``` `RELATIVE_PATH` is relative to project root (`public/index.html`, `src/App.tsx`, etc.) — the browser fetches source directly if the dev server lacks HMR. @@ -272,7 +272,7 @@ When the poll returns `exit`, proceed to cleanup. If the poll is still running a ## Cleanup ```bash -node {{scripts_path}}/live-server.mjs stop +node .gemini/skills/impeccable/scripts/live-server.mjs stop ``` Stops the HTTP server and runs `live-inject.mjs --remove` to strip `localhost:…/live.js` from the HTML entry. To stop the server but keep the inject tag (for a quick restart), use `stop --keep-inject`. `config.json` persists for future sessions. @@ -321,7 +321,7 @@ If `config.cspChecked === true`, skip this entire section. You already asked thi Otherwise, run the detection helper: ```bash -node {{scripts_path}}/detect-csp.mjs +node .gemini/skills/impeccable/scripts/detect-csp.mjs ``` Output: `{ shape, signals }` where `shape` is one of `append-arrays`, `append-string`, `middleware`, `meta-tag`, or `null`. The shape is named by *patch mechanism*, so one template covers many frameworks. diff --git a/.gemini/skills/impeccable/reference/teach.md b/.gemini/skills/impeccable/reference/teach.md index d244ee1b7..a899c9aee 100644 --- a/.gemini/skills/impeccable/reference/teach.md +++ b/.gemini/skills/impeccable/reference/teach.md @@ -12,7 +12,7 @@ Every other impeccable command reads these files before doing any work. Run the shared loader first so you know what already exists: ```bash -node {{scripts_path}}/load-context.mjs +node .gemini/skills/impeccable/scripts/load-context.mjs ``` The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. @@ -130,7 +130,7 @@ Summarize: - The 3-5 strategic principles from PRODUCT.md that will guide future work - If DESIGN.md is pending, remind the user how to generate it later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .gemini/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. diff --git a/.gemini/skills/impeccable/scripts/cleanup-deprecated.mjs b/.gemini/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/.gemini/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.gemini/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/.gemini/skills/impeccable/scripts/live-poll.mjs b/.gemini/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/.gemini/skills/impeccable/scripts/live-poll.mjs +++ b/.gemini/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/.github/skills/impeccable/reference/document.md b/.github/skills/impeccable/reference/document.md index 49cabda0c..361ccfba3 100644 --- a/.github/skills/impeccable/reference/document.md +++ b/.github/skills/impeccable/reference/document.md @@ -277,7 +277,7 @@ Do not reword. The panel shows these as secondary collapsible context; the same 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `DESIGN.json` was also written alongside — the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. +4. **Refresh the session cache.** Run `node .github/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. ## Seed mode @@ -342,7 +342,7 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. +3. Run `node .github/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. ## Style guidelines diff --git a/.github/skills/impeccable/reference/live.md b/.github/skills/impeccable/reference/live.md index 89d633161..800b3bdff 100644 --- a/.github/skills/impeccable/reference/live.md +++ b/.github/skills/impeccable/reference/live.md @@ -25,7 +25,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi ## Start ```bash -node {{scripts_path}}/live.mjs +node .github/skills/impeccable/scripts/live.mjs ``` Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. @@ -38,7 +38,7 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ``` LOOP: - node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= + node .github/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP @@ -71,7 +71,7 @@ Reading annotations precisely: ### 2. Wrap the element ```bash -node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" +node .github/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" ``` Flag mapping — keep them separate, don't collapse into `--query`: @@ -181,7 +181,7 @@ One edit, all variants — the browser's MutationObserver picks everything up in ### 7. Signal done ```bash -node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH +node .github/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH ``` `RELATIVE_PATH` is relative to project root (`public/index.html`, `src/App.tsx`, etc.) — the browser fetches source directly if the dev server lacks HMR. @@ -272,7 +272,7 @@ When the poll returns `exit`, proceed to cleanup. If the poll is still running a ## Cleanup ```bash -node {{scripts_path}}/live-server.mjs stop +node .github/skills/impeccable/scripts/live-server.mjs stop ``` Stops the HTTP server and runs `live-inject.mjs --remove` to strip `localhost:…/live.js` from the HTML entry. To stop the server but keep the inject tag (for a quick restart), use `stop --keep-inject`. `config.json` persists for future sessions. @@ -321,7 +321,7 @@ If `config.cspChecked === true`, skip this entire section. You already asked thi Otherwise, run the detection helper: ```bash -node {{scripts_path}}/detect-csp.mjs +node .github/skills/impeccable/scripts/detect-csp.mjs ``` Output: `{ shape, signals }` where `shape` is one of `append-arrays`, `append-string`, `middleware`, `meta-tag`, or `null`. The shape is named by *patch mechanism*, so one template covers many frameworks. diff --git a/.github/skills/impeccable/reference/teach.md b/.github/skills/impeccable/reference/teach.md index 5dd8eeda9..d27276c8c 100644 --- a/.github/skills/impeccable/reference/teach.md +++ b/.github/skills/impeccable/reference/teach.md @@ -12,7 +12,7 @@ Every other impeccable command reads these files before doing any work. Run the shared loader first so you know what already exists: ```bash -node {{scripts_path}}/load-context.mjs +node .github/skills/impeccable/scripts/load-context.mjs ``` The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. @@ -130,7 +130,7 @@ Summarize: - The 3-5 strategic principles from PRODUCT.md that will guide future work - If DESIGN.md is pending, remind the user how to generate it later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .github/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. diff --git a/.github/skills/impeccable/scripts/cleanup-deprecated.mjs b/.github/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/.github/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.github/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/.github/skills/impeccable/scripts/live-poll.mjs b/.github/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/.github/skills/impeccable/scripts/live-poll.mjs +++ b/.github/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/.kiro/skills/impeccable/reference/document.md b/.kiro/skills/impeccable/reference/document.md index 49cabda0c..14058d733 100644 --- a/.kiro/skills/impeccable/reference/document.md +++ b/.kiro/skills/impeccable/reference/document.md @@ -277,7 +277,7 @@ Do not reword. The panel shows these as secondary collapsible context; the same 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `DESIGN.json` was also written alongside — the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. +4. **Refresh the session cache.** Run `node .kiro/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. ## Seed mode @@ -342,7 +342,7 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. +3. Run `node .kiro/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. ## Style guidelines diff --git a/.kiro/skills/impeccable/reference/live.md b/.kiro/skills/impeccable/reference/live.md index 89d633161..71127201c 100644 --- a/.kiro/skills/impeccable/reference/live.md +++ b/.kiro/skills/impeccable/reference/live.md @@ -25,7 +25,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi ## Start ```bash -node {{scripts_path}}/live.mjs +node .kiro/skills/impeccable/scripts/live.mjs ``` Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. @@ -38,7 +38,7 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ``` LOOP: - node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= + node .kiro/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP @@ -71,7 +71,7 @@ Reading annotations precisely: ### 2. Wrap the element ```bash -node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" +node .kiro/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" ``` Flag mapping — keep them separate, don't collapse into `--query`: @@ -181,7 +181,7 @@ One edit, all variants — the browser's MutationObserver picks everything up in ### 7. Signal done ```bash -node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH +node .kiro/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH ``` `RELATIVE_PATH` is relative to project root (`public/index.html`, `src/App.tsx`, etc.) — the browser fetches source directly if the dev server lacks HMR. @@ -272,7 +272,7 @@ When the poll returns `exit`, proceed to cleanup. If the poll is still running a ## Cleanup ```bash -node {{scripts_path}}/live-server.mjs stop +node .kiro/skills/impeccable/scripts/live-server.mjs stop ``` Stops the HTTP server and runs `live-inject.mjs --remove` to strip `localhost:…/live.js` from the HTML entry. To stop the server but keep the inject tag (for a quick restart), use `stop --keep-inject`. `config.json` persists for future sessions. @@ -321,7 +321,7 @@ If `config.cspChecked === true`, skip this entire section. You already asked thi Otherwise, run the detection helper: ```bash -node {{scripts_path}}/detect-csp.mjs +node .kiro/skills/impeccable/scripts/detect-csp.mjs ``` Output: `{ shape, signals }` where `shape` is one of `append-arrays`, `append-string`, `middleware`, `meta-tag`, or `null`. The shape is named by *patch mechanism*, so one template covers many frameworks. diff --git a/.kiro/skills/impeccable/reference/teach.md b/.kiro/skills/impeccable/reference/teach.md index 8f055d57b..21c6f7639 100644 --- a/.kiro/skills/impeccable/reference/teach.md +++ b/.kiro/skills/impeccable/reference/teach.md @@ -12,7 +12,7 @@ Every other impeccable command reads these files before doing any work. Run the shared loader first so you know what already exists: ```bash -node {{scripts_path}}/load-context.mjs +node .kiro/skills/impeccable/scripts/load-context.mjs ``` The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. @@ -130,7 +130,7 @@ Summarize: - The 3-5 strategic principles from PRODUCT.md that will guide future work - If DESIGN.md is pending, remind the user how to generate it later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .kiro/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. diff --git a/.kiro/skills/impeccable/scripts/cleanup-deprecated.mjs b/.kiro/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/.kiro/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.kiro/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/.kiro/skills/impeccable/scripts/live-poll.mjs b/.kiro/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/.kiro/skills/impeccable/scripts/live-poll.mjs +++ b/.kiro/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/.opencode/skills/impeccable/reference/document.md b/.opencode/skills/impeccable/reference/document.md index 67770119c..045b40fa7 100644 --- a/.opencode/skills/impeccable/reference/document.md +++ b/.opencode/skills/impeccable/reference/document.md @@ -277,7 +277,7 @@ Do not reword. The panel shows these as secondary collapsible context; the same 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `DESIGN.json` was also written alongside — the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. +4. **Refresh the session cache.** Run `node .opencode/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. ## Seed mode @@ -342,7 +342,7 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. +3. Run `node .opencode/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. ## Style guidelines diff --git a/.opencode/skills/impeccable/reference/live.md b/.opencode/skills/impeccable/reference/live.md index 89d633161..8573a128f 100644 --- a/.opencode/skills/impeccable/reference/live.md +++ b/.opencode/skills/impeccable/reference/live.md @@ -25,7 +25,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi ## Start ```bash -node {{scripts_path}}/live.mjs +node .opencode/skills/impeccable/scripts/live.mjs ``` Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. @@ -38,7 +38,7 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ``` LOOP: - node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= + node .opencode/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP @@ -71,7 +71,7 @@ Reading annotations precisely: ### 2. Wrap the element ```bash -node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" +node .opencode/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" ``` Flag mapping — keep them separate, don't collapse into `--query`: @@ -181,7 +181,7 @@ One edit, all variants — the browser's MutationObserver picks everything up in ### 7. Signal done ```bash -node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH +node .opencode/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH ``` `RELATIVE_PATH` is relative to project root (`public/index.html`, `src/App.tsx`, etc.) — the browser fetches source directly if the dev server lacks HMR. @@ -272,7 +272,7 @@ When the poll returns `exit`, proceed to cleanup. If the poll is still running a ## Cleanup ```bash -node {{scripts_path}}/live-server.mjs stop +node .opencode/skills/impeccable/scripts/live-server.mjs stop ``` Stops the HTTP server and runs `live-inject.mjs --remove` to strip `localhost:…/live.js` from the HTML entry. To stop the server but keep the inject tag (for a quick restart), use `stop --keep-inject`. `config.json` persists for future sessions. @@ -321,7 +321,7 @@ If `config.cspChecked === true`, skip this entire section. You already asked thi Otherwise, run the detection helper: ```bash -node {{scripts_path}}/detect-csp.mjs +node .opencode/skills/impeccable/scripts/detect-csp.mjs ``` Output: `{ shape, signals }` where `shape` is one of `append-arrays`, `append-string`, `middleware`, `meta-tag`, or `null`. The shape is named by *patch mechanism*, so one template covers many frameworks. diff --git a/.opencode/skills/impeccable/reference/teach.md b/.opencode/skills/impeccable/reference/teach.md index 63ce22562..0d8be53f6 100644 --- a/.opencode/skills/impeccable/reference/teach.md +++ b/.opencode/skills/impeccable/reference/teach.md @@ -12,7 +12,7 @@ Every other impeccable command reads these files before doing any work. Run the shared loader first so you know what already exists: ```bash -node {{scripts_path}}/load-context.mjs +node .opencode/skills/impeccable/scripts/load-context.mjs ``` The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. @@ -130,7 +130,7 @@ Summarize: - The 3-5 strategic principles from PRODUCT.md that will guide future work - If DESIGN.md is pending, remind the user how to generate it later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .opencode/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. diff --git a/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs b/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/.opencode/skills/impeccable/scripts/live-poll.mjs b/.opencode/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/.opencode/skills/impeccable/scripts/live-poll.mjs +++ b/.opencode/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/.pi/skills/impeccable/reference/document.md b/.pi/skills/impeccable/reference/document.md index 49cabda0c..01ec2d140 100644 --- a/.pi/skills/impeccable/reference/document.md +++ b/.pi/skills/impeccable/reference/document.md @@ -277,7 +277,7 @@ Do not reword. The panel shows these as secondary collapsible context; the same 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `DESIGN.json` was also written alongside — the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. +4. **Refresh the session cache.** Run `node .pi/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. ## Seed mode @@ -342,7 +342,7 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. +3. Run `node .pi/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. ## Style guidelines diff --git a/.pi/skills/impeccable/reference/live.md b/.pi/skills/impeccable/reference/live.md index 89d633161..2d83fcdbc 100644 --- a/.pi/skills/impeccable/reference/live.md +++ b/.pi/skills/impeccable/reference/live.md @@ -25,7 +25,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi ## Start ```bash -node {{scripts_path}}/live.mjs +node .pi/skills/impeccable/scripts/live.mjs ``` Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. @@ -38,7 +38,7 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ``` LOOP: - node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= + node .pi/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP @@ -71,7 +71,7 @@ Reading annotations precisely: ### 2. Wrap the element ```bash -node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" +node .pi/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" ``` Flag mapping — keep them separate, don't collapse into `--query`: @@ -181,7 +181,7 @@ One edit, all variants — the browser's MutationObserver picks everything up in ### 7. Signal done ```bash -node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH +node .pi/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH ``` `RELATIVE_PATH` is relative to project root (`public/index.html`, `src/App.tsx`, etc.) — the browser fetches source directly if the dev server lacks HMR. @@ -272,7 +272,7 @@ When the poll returns `exit`, proceed to cleanup. If the poll is still running a ## Cleanup ```bash -node {{scripts_path}}/live-server.mjs stop +node .pi/skills/impeccable/scripts/live-server.mjs stop ``` Stops the HTTP server and runs `live-inject.mjs --remove` to strip `localhost:…/live.js` from the HTML entry. To stop the server but keep the inject tag (for a quick restart), use `stop --keep-inject`. `config.json` persists for future sessions. @@ -321,7 +321,7 @@ If `config.cspChecked === true`, skip this entire section. You already asked thi Otherwise, run the detection helper: ```bash -node {{scripts_path}}/detect-csp.mjs +node .pi/skills/impeccable/scripts/detect-csp.mjs ``` Output: `{ shape, signals }` where `shape` is one of `append-arrays`, `append-string`, `middleware`, `meta-tag`, or `null`. The shape is named by *patch mechanism*, so one template covers many frameworks. diff --git a/.pi/skills/impeccable/reference/teach.md b/.pi/skills/impeccable/reference/teach.md index d3ea1b0ae..2b569abbf 100644 --- a/.pi/skills/impeccable/reference/teach.md +++ b/.pi/skills/impeccable/reference/teach.md @@ -12,7 +12,7 @@ Every other impeccable command reads these files before doing any work. Run the shared loader first so you know what already exists: ```bash -node {{scripts_path}}/load-context.mjs +node .pi/skills/impeccable/scripts/load-context.mjs ``` The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. @@ -130,7 +130,7 @@ Summarize: - The 3-5 strategic principles from PRODUCT.md that will guide future work - If DESIGN.md is pending, remind the user how to generate it later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .pi/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. diff --git a/.pi/skills/impeccable/scripts/cleanup-deprecated.mjs b/.pi/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/.pi/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.pi/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/.pi/skills/impeccable/scripts/live-poll.mjs b/.pi/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/.pi/skills/impeccable/scripts/live-poll.mjs +++ b/.pi/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/.rovodev/skills/impeccable/reference/document.md b/.rovodev/skills/impeccable/reference/document.md index 49cabda0c..e66dc4b64 100644 --- a/.rovodev/skills/impeccable/reference/document.md +++ b/.rovodev/skills/impeccable/reference/document.md @@ -277,7 +277,7 @@ Do not reword. The panel shows these as secondary collapsible context; the same 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `DESIGN.json` was also written alongside — the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. +4. **Refresh the session cache.** Run `node .rovodev/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. ## Seed mode @@ -342,7 +342,7 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. +3. Run `node .rovodev/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. ## Style guidelines diff --git a/.rovodev/skills/impeccable/reference/live.md b/.rovodev/skills/impeccable/reference/live.md index 89d633161..b84bf390d 100644 --- a/.rovodev/skills/impeccable/reference/live.md +++ b/.rovodev/skills/impeccable/reference/live.md @@ -25,7 +25,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi ## Start ```bash -node {{scripts_path}}/live.mjs +node .rovodev/skills/impeccable/scripts/live.mjs ``` Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. @@ -38,7 +38,7 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ``` LOOP: - node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= + node .rovodev/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP @@ -71,7 +71,7 @@ Reading annotations precisely: ### 2. Wrap the element ```bash -node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" +node .rovodev/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" ``` Flag mapping — keep them separate, don't collapse into `--query`: @@ -181,7 +181,7 @@ One edit, all variants — the browser's MutationObserver picks everything up in ### 7. Signal done ```bash -node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH +node .rovodev/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH ``` `RELATIVE_PATH` is relative to project root (`public/index.html`, `src/App.tsx`, etc.) — the browser fetches source directly if the dev server lacks HMR. @@ -272,7 +272,7 @@ When the poll returns `exit`, proceed to cleanup. If the poll is still running a ## Cleanup ```bash -node {{scripts_path}}/live-server.mjs stop +node .rovodev/skills/impeccable/scripts/live-server.mjs stop ``` Stops the HTTP server and runs `live-inject.mjs --remove` to strip `localhost:…/live.js` from the HTML entry. To stop the server but keep the inject tag (for a quick restart), use `stop --keep-inject`. `config.json` persists for future sessions. @@ -321,7 +321,7 @@ If `config.cspChecked === true`, skip this entire section. You already asked thi Otherwise, run the detection helper: ```bash -node {{scripts_path}}/detect-csp.mjs +node .rovodev/skills/impeccable/scripts/detect-csp.mjs ``` Output: `{ shape, signals }` where `shape` is one of `append-arrays`, `append-string`, `middleware`, `meta-tag`, or `null`. The shape is named by *patch mechanism*, so one template covers many frameworks. diff --git a/.rovodev/skills/impeccable/reference/teach.md b/.rovodev/skills/impeccable/reference/teach.md index d3ea1b0ae..e699f2e9e 100644 --- a/.rovodev/skills/impeccable/reference/teach.md +++ b/.rovodev/skills/impeccable/reference/teach.md @@ -12,7 +12,7 @@ Every other impeccable command reads these files before doing any work. Run the shared loader first so you know what already exists: ```bash -node {{scripts_path}}/load-context.mjs +node .rovodev/skills/impeccable/scripts/load-context.mjs ``` The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. @@ -130,7 +130,7 @@ Summarize: - The 3-5 strategic principles from PRODUCT.md that will guide future work - If DESIGN.md is pending, remind the user how to generate it later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .rovodev/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. diff --git a/.rovodev/skills/impeccable/scripts/cleanup-deprecated.mjs b/.rovodev/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/.rovodev/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.rovodev/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/.rovodev/skills/impeccable/scripts/live-poll.mjs b/.rovodev/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/.rovodev/skills/impeccable/scripts/live-poll.mjs +++ b/.rovodev/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/.trae-cn/skills/impeccable/reference/document.md b/.trae-cn/skills/impeccable/reference/document.md index 49cabda0c..5e25e7129 100644 --- a/.trae-cn/skills/impeccable/reference/document.md +++ b/.trae-cn/skills/impeccable/reference/document.md @@ -277,7 +277,7 @@ Do not reword. The panel shows these as secondary collapsible context; the same 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `DESIGN.json` was also written alongside — the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. +4. **Refresh the session cache.** Run `node .trae-cn/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. ## Seed mode @@ -342,7 +342,7 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. +3. Run `node .trae-cn/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. ## Style guidelines diff --git a/.trae-cn/skills/impeccable/reference/live.md b/.trae-cn/skills/impeccable/reference/live.md index 89d633161..501f878bd 100644 --- a/.trae-cn/skills/impeccable/reference/live.md +++ b/.trae-cn/skills/impeccable/reference/live.md @@ -25,7 +25,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi ## Start ```bash -node {{scripts_path}}/live.mjs +node .trae-cn/skills/impeccable/scripts/live.mjs ``` Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. @@ -38,7 +38,7 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ``` LOOP: - node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= + node .trae-cn/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP @@ -71,7 +71,7 @@ Reading annotations precisely: ### 2. Wrap the element ```bash -node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" +node .trae-cn/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" ``` Flag mapping — keep them separate, don't collapse into `--query`: @@ -181,7 +181,7 @@ One edit, all variants — the browser's MutationObserver picks everything up in ### 7. Signal done ```bash -node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH +node .trae-cn/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH ``` `RELATIVE_PATH` is relative to project root (`public/index.html`, `src/App.tsx`, etc.) — the browser fetches source directly if the dev server lacks HMR. @@ -272,7 +272,7 @@ When the poll returns `exit`, proceed to cleanup. If the poll is still running a ## Cleanup ```bash -node {{scripts_path}}/live-server.mjs stop +node .trae-cn/skills/impeccable/scripts/live-server.mjs stop ``` Stops the HTTP server and runs `live-inject.mjs --remove` to strip `localhost:…/live.js` from the HTML entry. To stop the server but keep the inject tag (for a quick restart), use `stop --keep-inject`. `config.json` persists for future sessions. @@ -321,7 +321,7 @@ If `config.cspChecked === true`, skip this entire section. You already asked thi Otherwise, run the detection helper: ```bash -node {{scripts_path}}/detect-csp.mjs +node .trae-cn/skills/impeccable/scripts/detect-csp.mjs ``` Output: `{ shape, signals }` where `shape` is one of `append-arrays`, `append-string`, `middleware`, `meta-tag`, or `null`. The shape is named by *patch mechanism*, so one template covers many frameworks. diff --git a/.trae-cn/skills/impeccable/reference/teach.md b/.trae-cn/skills/impeccable/reference/teach.md index 364bd3c37..e69f4ab5e 100644 --- a/.trae-cn/skills/impeccable/reference/teach.md +++ b/.trae-cn/skills/impeccable/reference/teach.md @@ -12,7 +12,7 @@ Every other impeccable command reads these files before doing any work. Run the shared loader first so you know what already exists: ```bash -node {{scripts_path}}/load-context.mjs +node .trae-cn/skills/impeccable/scripts/load-context.mjs ``` The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. @@ -130,7 +130,7 @@ Summarize: - The 3-5 strategic principles from PRODUCT.md that will guide future work - If DESIGN.md is pending, remind the user how to generate it later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .trae-cn/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. diff --git a/.trae-cn/skills/impeccable/scripts/cleanup-deprecated.mjs b/.trae-cn/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/.trae-cn/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.trae-cn/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/.trae-cn/skills/impeccable/scripts/live-poll.mjs b/.trae-cn/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/.trae-cn/skills/impeccable/scripts/live-poll.mjs +++ b/.trae-cn/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/.trae/skills/impeccable/reference/document.md b/.trae/skills/impeccable/reference/document.md index 49cabda0c..6b0b3b224 100644 --- a/.trae/skills/impeccable/reference/document.md +++ b/.trae/skills/impeccable/reference/document.md @@ -277,7 +277,7 @@ Do not reword. The panel shows these as secondary collapsible context; the same 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `DESIGN.json` was also written alongside — the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. +4. **Refresh the session cache.** Run `node .trae/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. ## Seed mode @@ -342,7 +342,7 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. +3. Run `node .trae/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. ## Style guidelines diff --git a/.trae/skills/impeccable/reference/live.md b/.trae/skills/impeccable/reference/live.md index 89d633161..3fcb44a6d 100644 --- a/.trae/skills/impeccable/reference/live.md +++ b/.trae/skills/impeccable/reference/live.md @@ -25,7 +25,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi ## Start ```bash -node {{scripts_path}}/live.mjs +node .trae/skills/impeccable/scripts/live.mjs ``` Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation — **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. @@ -38,7 +38,7 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ``` LOOP: - node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= + node .trae/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP @@ -71,7 +71,7 @@ Reading annotations precisely: ### 2. Wrap the element ```bash -node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" +node .trae/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" ``` Flag mapping — keep them separate, don't collapse into `--query`: @@ -181,7 +181,7 @@ One edit, all variants — the browser's MutationObserver picks everything up in ### 7. Signal done ```bash -node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH +node .trae/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH ``` `RELATIVE_PATH` is relative to project root (`public/index.html`, `src/App.tsx`, etc.) — the browser fetches source directly if the dev server lacks HMR. @@ -272,7 +272,7 @@ When the poll returns `exit`, proceed to cleanup. If the poll is still running a ## Cleanup ```bash -node {{scripts_path}}/live-server.mjs stop +node .trae/skills/impeccable/scripts/live-server.mjs stop ``` Stops the HTTP server and runs `live-inject.mjs --remove` to strip `localhost:…/live.js` from the HTML entry. To stop the server but keep the inject tag (for a quick restart), use `stop --keep-inject`. `config.json` persists for future sessions. @@ -321,7 +321,7 @@ If `config.cspChecked === true`, skip this entire section. You already asked thi Otherwise, run the detection helper: ```bash -node {{scripts_path}}/detect-csp.mjs +node .trae/skills/impeccable/scripts/detect-csp.mjs ``` Output: `{ shape, signals }` where `shape` is one of `append-arrays`, `append-string`, `middleware`, `meta-tag`, or `null`. The shape is named by *patch mechanism*, so one template covers many frameworks. diff --git a/.trae/skills/impeccable/reference/teach.md b/.trae/skills/impeccable/reference/teach.md index 364bd3c37..edc81f37a 100644 --- a/.trae/skills/impeccable/reference/teach.md +++ b/.trae/skills/impeccable/reference/teach.md @@ -12,7 +12,7 @@ Every other impeccable command reads these files before doing any work. Run the shared loader first so you know what already exists: ```bash -node {{scripts_path}}/load-context.mjs +node .trae/skills/impeccable/scripts/load-context.mjs ``` The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. @@ -130,7 +130,7 @@ Summarize: - The 3-5 strategic principles from PRODUCT.md that will guide future work - If DESIGN.md is pending, remind the user how to generate it later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .trae/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. diff --git a/.trae/skills/impeccable/scripts/cleanup-deprecated.mjs b/.trae/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/.trae/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.trae/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/.trae/skills/impeccable/scripts/live-poll.mjs b/.trae/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/.trae/skills/impeccable/scripts/live-poll.mjs +++ b/.trae/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/scripts/lib/transformers/factory.js b/scripts/lib/transformers/factory.js index f94576d95..5507b0ea0 100644 --- a/scripts/lib/transformers/factory.js +++ b/scripts/lib/transformers/factory.js @@ -153,7 +153,8 @@ export function createTransformer(config) { const refDir = path.join(skillDir, 'reference'); ensureDir(refDir); for (const ref of skill.references) { - const refContent = replacePlaceholders(ref.content, placeholderKey, [], allSkillNames); + let refContent = replacePlaceholders(ref.content, placeholderKey, [], allSkillNames); + refContent = refContent.replace(/\{\{scripts_path\}\}/g, scriptsPath); writeFile(path.join(refDir, `${ref.name}.md`), refContent); refCount++; } diff --git a/source/skills/impeccable/scripts/cleanup-deprecated.mjs b/source/skills/impeccable/scripts/cleanup-deprecated.mjs index e8149a5ab..6aee4710a 100644 --- a/source/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/source/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -127,7 +127,10 @@ export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { // 2. Word-level content heuristic. if (/impeccable/i.test(content)) return true; // 3. Per-skill fingerprint for old skills that never mentioned the pack. - const fingerprint = skillName && SKILL_FINGERPRINTS[skillName]; + // Strip the i- prefix so both `harden` and `i-harden` resolve to the + // same fingerprint entry. + const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; + const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; if (fingerprint && content.includes(fingerprint)) return true; return false; } diff --git a/source/skills/impeccable/scripts/live-poll.mjs b/source/skills/impeccable/scripts/live-poll.mjs index 3c8088c23..03d05baec 100644 --- a/source/skills/impeccable/scripts/live-poll.mjs +++ b/source/skills/impeccable/scripts/live-poll.mjs @@ -13,13 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -import { Agent, setGlobalDispatcher } from 'undici'; -// Disable undici's default 300s headersTimeout so long-polls can sit open -// indefinitely (until a browser event or the server's own timeout fires). -// Without this, fetch() throws a bare "fetch failed" at 5 minutes even -// though the server would have happily kept the connection alive. -setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 })); +// Node's built-in fetch (undici under the hood) enforces a 300s headers +// timeout that can't be lowered per-request. We cap each request below +// that ceiling and loop in `pollOnce` to synthesize a long poll without +// depending on the standalone undici package. +const PER_REQUEST_TIMEOUT_MS = 270_000; const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json'); @@ -100,28 +99,45 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min; undici's default - // 5-min headers-timeout is disabled at import time so this can sit open - // indefinitely without fetch errors. + // Poll mode: block until browser event. Default 10 min. Node's built-in + // fetch enforces a 300s headers timeout, so we loop in slices under that + // ceiling and keep re-polling until we get a real event or the user's + // total timeout runs out. const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const deadline = Date.now() + totalTimeout; + let event; try { - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`); + while (true) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + event = { type: 'timeout' }; + break; + } + const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); + if (res.status === 401) { + console.error('Authentication failed. The server token may have changed.'); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + + if (!res.ok) { + console.error(`Poll failed: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const next = await res.json(); + // Server-side timeout means no browser event arrived in this slice. + // Loop and re-poll until we get a real event or we hit the user's + // total deadline. + if (next?.type === 'timeout' && Date.now() < deadline) continue; + event = next; + break; } - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const event = await res.json(); - // Auto-handle accept/discard via deterministic script if (event.type === 'accept' || event.type === 'discard') { const __dirname = path.dirname(fileURLToPath(import.meta.url));