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) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-22 09:32:31 -07:00
co-authored by Claude Opus 4.7
parent 99494348bf
commit 1ba75a820e
58 changed files with 616 additions and 387 deletions
@@ -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
+6 -6
View File
@@ -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.
+2 -2
View File
@@ -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.
@@ -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;
}
+38 -22
View File
@@ -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));
@@ -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
+6 -6
View File
@@ -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.
+2 -2
View File
@@ -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.
@@ -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;
}
+38 -22
View File
@@ -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));
@@ -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
+6 -6
View File
@@ -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.
+2 -2
View File
@@ -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.
@@ -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;
}
+38 -22
View File
@@ -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));
@@ -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
+6 -6
View File
@@ -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.
+2 -2
View File
@@ -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.
@@ -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;
}
+38 -22
View File
@@ -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));
@@ -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
+6 -6
View File
@@ -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.
+2 -2
View File
@@ -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.
@@ -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;
}
+38 -22
View File
@@ -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));
@@ -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
+6 -6
View File
@@ -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.
+2 -2
View File
@@ -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.
@@ -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;
}
+38 -22
View File
@@ -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));
@@ -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
@@ -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.
@@ -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.
@@ -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;
}
@@ -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));
+2 -2
View File
@@ -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
+6 -6
View File
@@ -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.
+2 -2
View File
@@ -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.
@@ -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;
}
+38 -22
View File
@@ -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));
@@ -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
+6 -6
View File
@@ -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.
@@ -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.
@@ -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;
}
@@ -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));
@@ -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
+6 -6
View File
@@ -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.
@@ -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.
@@ -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;
}
@@ -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));
@@ -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
+6 -6
View File
@@ -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.
+2 -2
View File
@@ -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.
@@ -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;
}
+38 -22
View File
@@ -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));
+2 -1
View File
@@ -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++;
}
@@ -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;
}
+38 -22
View File
@@ -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));