mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
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:
co-authored by
Claude Opus 4.7
parent
99494348bf
commit
1ba75a820e
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user