Trim dead code and tighten the branch to surgical scope

- palette-picker.js: drop the superseded APP/DOCS/INDEX copy tables, the
  disabled font-rail hoist machinery, LOREM.caption, and an orphan marker
- design-context.css + dcx-hierarchy.css: drop the demo-era specimen,
  radius, and spacing-bar rule groups nothing renders
- index.astro: drop an unreferenced boards const
- de-export module-internal names (store, bindings, portability,
  visual-cues); remove picker-server's unused doc-session kill switch and
  document --doc in its help
- palette.mjs: realpath the entry guard so symlinked installs still run
  the CLI (same guard as visual-cues.mjs)
- revert serve-question.mjs copy ride-along; drop the plan/ gitignore
  entry; dedupe a build-picker comment; file design-context with the
  system category entries; alphabetize IMPECCABLE_SUB_COMMANDS
- new-work.md: drop a same-screen restatement of the comp/world ranking
- CLAUDE.md: correct the picker gate baseline to the measured 606 and
  explain the per-element multiplication; the trims removed the two
  dead-block font findings
- DESIGN-CONTEXT-ARCHITECTURE.md: list portability.mjs in the code layout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abdul Wahab
2026-09-01 10:08:22 +05:00
co-authored by Claude Fable 5
parent 6c2a79891c
commit 065bd4f3ca
17 changed files with 32 additions and 226 deletions
-3
View File
@@ -138,9 +138,6 @@ tmp/
# (scripts/generate-og-image.js); this dir is throwaway and safe to delete.
.og-build/
# Working plans for agent-executed features (browser questionnaire etc.).
# Local planning scratch between the planning agent and executing agents.
plan/
# Intermediate and generated output for the picker build. Release builds sync
# the generated picker into the tracked provider harness directories.
+3 -3
View File
@@ -240,18 +240,18 @@ node cli/bin/cli.js detect skill/scripts/picker/
**Name the directory, not `index.html`.** The built page only links its stylesheet and its script, and the rules that read CSS or JS text run per file, so scanning the page alone never reads a line of either. That is how three `design-system-color` findings vanished with nothing fixed: the mode-tile demo palette moved out of an inline `style` attribute and into `picker.css`, and the gate went quiet while the values stayed put. Naming a bundle by path is not a fix either, because Vite content-hashes the filenames and a stale path silently narrows the scan back to the page.
Baseline: **54 findings**, 29 on `index.html`, 24 on the bundled stylesheet, and 1 on the bundled script. The groups below are accepted with a reason on record, so hold 54 rather than treating it as a backlog:
Baseline: **606 findings**: 556 on `index.html`, 47 on the bundled stylesheet, and 3 on the bundled script. The headline is dominated by per-element multiplication, not by distinct problems: the jsdom pass reports once per matching element, so one deliberate declaration can carry hundreds of hits, and a single one does: `picker.css`'s artboard morph, which eases `margin` and `padding` while the wireframe previews an answer, accounts for 527 of the `index.html` findings on its own (the morph is the preview mechanic; easing geometry on a miniature board is the point, not thrash). The distinct accepted groups below are the real ledger, each with a reason on record, so hold 606 rather than treating it as a backlog:
- 18 `cramped-padding` on the palette strips, nine for each of the two: the editable one on screen 02 and the reading copy under screen 03's choices. Each strip reports its group, its four items, and its four swatches. A swatch is a color that has to reach its own edges, so `.picker-band` carries no padding and places its hex readout and drag grip by inset instead, which the rule has no way to see. Adding padding here would put a frame around every color the visitor picked.
- 3 `design-system-color` for the mode-tile demo palette (`#195856`, `#5f7c7a`, `#3a958b`). They are examples standing in for the palette the visitor is about to pick, which is the carve-out DESIGN.md's OKLCH-Only Rule already names. The argument is at the declaration in `picker/styles/picker.css`; read it before touching them.
- 8 `design-system-font-size` from `picker/styles/picker.css`, seven of them already waived there in `.impeccable/config.json`. They reappear under the built path only because that waiver is scoped to the source path. The eighth is `1.05rem` on `.picker-finish-lede`, the review copy on the last screen, which sits on the questionnaire's own kit-token ramp for the same reason the other seven do.
- 2 `border-accent-on-rounded` on `.ks-bento` in the vendored `kinpaku-kit.css`, which carries no `border-radius` at all. The rule wants a radius on the same line, and minification puts the whole stylesheet on one line, so every line-scoped regex rule reads this bundle with no line context. Check a snippet against the source before acting on it.
- 13 from the design context document, the review UI the picker hands off to after the last question. Eleven are `picker/styles/design-context.css`: 5 `design-system-font-size` and 1 `design-system-radius` (`22px`) for the ported prototype's own type ramp and shape vocabulary, 2 `design-system-color` (`#7ba98f`, `#c26d5a`) for the tray status dots, 2 `design-system-font` naming the two faces the document sets itself in, and 1 `layout-transition` on the tile expander, which animates `width` and `height` because the tile morphing into a panel is the geometry it is animating. One is `picker/scripts/design-context.js`, whose Google Fonts URL is built from whichever family the run chose, so the rule sees a template where it wants a declared face. The last is `all-caps-body` on the doc masthead's mono note. The user locked that UI; both files carry a waiver in `.impeccable/config.json` saying so, and like the picker.css group those waivers are source-scoped, so the values resurface under the built path.
- 11 from the design context document, the review UI the picker hands off to after the last question. Nine are `picker/styles/design-context.css`: 5 `design-system-font-size` and 1 `design-system-radius` (`22px`) for the ported prototype's own type ramp and shape vocabulary, 2 `design-system-color` (`#7ba98f`, `#c26d5a`) for the tray status dots, and 1 `layout-transition` on the tile expander, which animates `width` and `height` because the tile morphing into a panel is the geometry it is animating. One is `picker/scripts/design-context.js`, whose Google Fonts URL is built from whichever family the run chose, so the rule sees a template where it wants a declared face. The last is `all-caps-body` on the doc masthead's mono note. The user locked that UI; both files carry a waiver in `.impeccable/config.json` saying so, and like the picker.css group those waivers are source-scoped, so the values resurface under the built path.
- 1 `undersized-ui-text` on the `Change` legend in the document's request-scope control, at 10.88px. Same slot and same argument as the micro-labels below.
The last 9 are the page findings the questionnaire has carried since those screens were built: 3 `cramped-padding` on the option panel and the icon sheet, both edge-to-edge grids whose rows hold their own padding, and 6 mono readouts and micro-labels under the text-size and tracking floors. They are the group to revisit first if the number is ever worth lowering.
**When the number moves, find out which file moved it before editing this section.** The baseline read 39 until the design context document landed, and all 15 findings that arrived with it are named above: 13 in its own two files, plus the finish-screen lede and the `Change` legend that ship in the picker's files but belong to that feature. The five other workstreams in the same batch added none. Building the tree at the previous commit in a `git worktree` and scanning both builds is how that was established, and it beats reasoning about which change is responsible.
**When the number moves, find out which file moved it before editing this section.** The distinct-group ledger read 39 until the design context document landed, and 15 findings arrived with it: 13 in its own two files (two later left with a dead type-specimen block), plus the finish-screen lede and the `Change` legend that ship in the picker's files but belong to that feature. The five other workstreams in the same batch added none. Building the tree at the previous commit in a `git worktree` and scanning both builds is how that was established, and it beats reasoning about which change is responsible.
For line numbers during triage, `node cli/bin/cli.js detect picker/styles/` scans the unminified sources. It is not the gate: the source scan's style-context heuristic skips custom-property declarations, so it misses the palette entirely. A served URL is worse than either, because the browser pass only measures the screen in front of it and the picker hides all but the active one.
+1
View File
@@ -77,6 +77,7 @@ skill/scripts/design-context/
store.mjs the only code that knows store paths or writes store files
bindings.mjs the editable-field registry for the document
session-routes.mjs HTTP handlers for the document edit session
portability.mjs the export bundle format and its import validation
skill/scripts/
picker-server.mjs static serving, boot contract, submit, autosave, spawn
-7
View File
@@ -62,13 +62,6 @@ const iconRows = iconPacks.packs.map((pack) => ({
// screen per surface. Screen 06 takes a class because its scenes are written per
// board; the rest ask the same question of every board and only need to know
// which anatomy is under them.
const boards = [
{ surface: 'persuade' },
{ surface: 'operate' },
{ surface: 'read' },
{ surface: 'experience' },
];
const questions = [
{
id: '06',
+6 -104
View File
@@ -50,7 +50,6 @@ const LOREM = {
'Excepteur sint occaecat',
'Non proident, sunt in culpa',
],
caption: 'Lorem ipsum dolor sit amet, consectetur adipiscing.',
};
/* Every word slot on the font boards, in lorem. A pair is judged on glyphs, and
@@ -105,45 +104,6 @@ const LOREM_INDEX = {
be words the agent writes and nobody ever reads. */
const GALLERY_CARDS = 3;
/* The words an interface supplies rather than the product: a tool's own rail,
the headings over its columns, the figures under them, and the two rows of a
settings panel. Kept here beside LOREM and for the same reason. What this
board has to prove is that the pair draws lining numerals that hold a column
and a semibold label that stays inside one, and both are properties of the
face. A column is also the tightest slot on any of the boards, so its words
cannot be left to whatever the run happens to be selling. */
const APP = {
rail: ['Overview', 'Reports', 'Settings'],
columns: ['Item', 'Status', 'Amount'],
figures: ['1,284', '98.2%', '41'],
amounts: ['$12,400', '$3,860', '$9,215'],
panel: ['Preferences', 'Last 30 days'],
switches: ['Email digest', 'Compact rows'],
chartTitle: 'Volume by channel',
/* One word each, because the label under a bar has the bar's own width and
nothing more: a category that wraps or truncates here is a fault in the
drawing rather than a report on the pair. The handset takes the first
three, which is why the widest of them comes early. */
lanes: ['Direct', 'Search', 'Social', 'Email', 'Other'],
};
/* The same argument as APP, for the surface where the words belong to the
document rather than to the product. The rail lists sections of one page and
the crumb says where that page sits, neither of which the manifest's nav can
stand in for without the board reading as the same four words twice. */
const DOCS = {
rail: ['Getting started', 'Install', 'Configuration', 'API reference'],
crumb: 'Docs / Getting started',
note: 'Note',
};
/* Same again for the index. The carousel's stops name parts of a body of work,
so the footer's links cannot stand in for them: the two lists sit a band
apart on the same board and would read as one list printed twice. */
const INDEX = {
stops: ['Selected', 'Archive', 'Studio', 'Contact'],
};
/* Deliberately Latin faces with English copy, same as the constraint on
dealt pairs in visual-cues.md Step 6. TODO: lift both together when the
picker's previews learn non-Latin scripts and per-language font subsets. */
@@ -599,7 +559,6 @@ fontOptions.onchange = ({ target }) => {
if (!target.matches('input[name="font-pair"]')) return;
const pair = fontManifest.pairs.find(({ id }) => id === target.value);
if (pair) syncFontPair(pair);
requestHoist();
};
/* Scroll by whole rows so an option never ends up half in frame, and disable
@@ -652,32 +611,14 @@ function restWhenIdle(list, rest) {
};
}
/* The chosen pair takes the top of the rail, so the answer is the first thing
the list shows and the rest keep their dealt order underneath it.
The move is not made at the moment of choosing, and that is the whole
design. Reordering under a live cursor drags the row the user just clicked
out from under the pointer and parks a different pair where the next click
is already aimed. Reordering on a radio group's arrow keys is worse: every
press both moves focus and commits, so a list that re-sorts per press
re-sorts between presses and the group cannot be crossed at all.
So the rail reorders only while nobody is working it. A selection sets the
request; the pointer leaving, or focus leaving, spends it. Arriving on the
screen spends it too, which is the backstop if a settle is ever missed. */
const typeRail = fontOptions.closest('.picker-type-rail');
const RAIL_NAV_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', ' ']);
let hoistPending = false;
let pointerInRail = false;
let keyboardInRail = false;
/* The chosen pair takes the top of the rail only at quiet moments: arriving
on the screen, or a dialog closing over it. Reordering live on selection
was removed, because it drags the row the user just clicked out from under
the pointer and, on a radio group's arrow keys, re-sorts the list between
presses; between arrivals the rail keeps the order the pairs were dealt in
and the checked state alone says which one is chosen. */
let hoistFlash;
/* :hover covers the one case the pointer events miss: the cursor already
resting where the rail appears, which fires no enter of its own. */
const railBusy = () => pointerInRail
|| typeRail.matches(':hover')
|| (keyboardInRail && typeRail.contains(document.activeElement));
/* Nothing is under the cursor when the rows move, so the move itself needs no
transition to be readable. What it needs is somewhere for the eye to land
after it: the row that just took the top comes up bright and settles to its
@@ -694,7 +635,6 @@ function flashHoisted(node) {
}
function applyHoist({ force = false } = {}) {
hoistPending = false;
const chosen = fontOptions.querySelector('input[name="font-pair"]:checked')?.closest('.picker-type-option');
if (!chosen) return;
const wanted = [chosen, ...pairOrder.filter((node) => node !== chosen)];
@@ -716,42 +656,6 @@ function applyHoist({ force = false } = {}) {
if (moved) flashHoisted(chosen);
}
/* Disabled: the reorder pulled the row the user had just clicked away from
where they left it, which reads as the list moving on its own. The rail now
keeps the order the pairs were dealt in and the checked state alone says
which one is chosen. */
function requestHoist() {
hoistPending = false;
}
function settleHoist() {
if (hoistPending && !railBusy()) applyHoist();
}
typeRail.addEventListener('pointerenter', () => {
pointerInRail = true;
});
// A frame of slack so :hover has resolved before the guard reads it.
typeRail.addEventListener('pointerleave', () => {
pointerInRail = false;
requestAnimationFrame(settleHoist);
});
typeRail.addEventListener('pointerdown', () => {
keyboardInRail = false;
});
typeRail.addEventListener('keydown', ({ key }) => {
if (RAIL_NAV_KEYS.has(key)) keyboardInRail = true;
});
typeRail.addEventListener('focusout', ({ relatedTarget }) => {
if (typeRail.contains(relatedTarget)) return;
keyboardInRail = false;
settleHoist();
});
/* Type scale.
The numbers are the real ones: step n is 16px * ratio^n, and a Golden Ratio
@@ -3971,7 +3875,6 @@ function ensurePremiumMotion() {
premiumMotionInstalled = true;
installPremiumMotion();
}
/* premium-motion-end */
/* Custom fonts. A URL is carried through as-is; an uploaded face is handed to
@@ -4867,7 +4770,6 @@ document.addEventListener('picker:screenchange', (event) => {
// Arriving is the quietest moment there is, so the rail settles here even
// if it is already in order: the chosen pair is the row you land on.
if (event.detail.screen === '04') {
keyboardInRail = false;
applyHoist({ force: true });
}
// A hidden screen has no animations to rewind, so the scene starts on the
-1
View File
@@ -45,7 +45,6 @@
.dcx-detail-article .dcx-surface-goal,
.dcx-detail-article .dcx-swatch-meta > p,
.dcx-detail-article .dcx-scale-head,
.dcx-detail-article .dcx-specimen-body,
.dcx-detail-article .dcx-pick-desc,
.dcx-detail-article .dcx-asset-caption > p,
.dcx-detail-article .dcx-prose {
-86
View File
@@ -974,91 +974,6 @@ body.dcx-open { overflow: hidden; background: linear-gradient(180deg, var(--ks-l
color: var(--ks-text-faint);
}
/* Type specimen (Typography) — set in the project's real faces. */
.dcx-specimen {
display: grid;
gap: 18px;
padding: clamp(24px, 3.4vw, 40px);
border: 1px solid var(--ks-rule);
border-radius: 10px;
background: var(--panel-bg);
}
.dcx-specimen-display {
margin: 0;
font-family: "Shippori Mincho", "Hiragino Mincho ProN", "Yu Mincho", Georgia, serif;
font-weight: 400;
font-size: clamp(2.2rem, 4.6vw, 3.6rem);
line-height: 1.15;
color: var(--ks-champagne);
text-wrap: balance;
}
.dcx-specimen-body {
margin: 0;
max-width: 62ch;
font-family: "Source Sans 3", system-ui, sans-serif;
font-size: 1rem;
line-height: 1.6;
color: var(--ks-text-muted);
}
.dcx-specimen-label {
margin: 0;
font-family: "Source Sans 3", system-ui, sans-serif;
font-size: 0.875rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ks-text-faint);
}
/* Radius samples (Material). */
.dcx-radius-row { display: flex; flex-wrap: wrap; gap: 18px; }
.dcx-radius-sample { display: grid; gap: 8px; justify-items: center; }
.dcx-radius-box {
width: 76px;
height: 76px;
border: 1.5px solid var(--accent-line);
background: var(--accent-wash);
}
.dcx-radius-caption {
font-family: var(--ks-mono);
font-size: 0.68rem;
color: var(--ks-text-muted);
}
/* Spacing scale bars (Material). */
.dcx-space-rows { display: grid; gap: 8px; }
.dcx-space-row {
display: grid;
grid-template-columns: 74px minmax(0, 1fr) 84px;
align-items: center;
gap: 14px;
}
.dcx-space-name,
.dcx-space-value {
font-family: var(--ks-mono);
font-size: 0.7rem;
color: var(--ks-text-muted);
}
.dcx-space-value { text-align: right; color: var(--ks-text-faint); }
.dcx-space-bar {
height: 12px;
width: var(--bar, 20%);
min-width: 6px;
border-radius: 3px;
background: var(--accent-wash);
border: 1px solid var(--accent-line);
}
/* Empty state (Iconography). */
.dcx-empty {
padding: clamp(26px, 4vw, 40px);
@@ -1181,7 +1096,6 @@ body.dcx-open { overflow: hidden; background: linear-gradient(180deg, var(--ks-l
gap: 4px;
}
.dcx-space-row { grid-template-columns: 58px minmax(0, 1fr) 70px; }
}
/* ============================================================
-2
View File
@@ -11,8 +11,6 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const buildDir = path.join(root, 'build-picker');
const outputDir = path.join(root, 'skill/scripts/picker');
// Static assets vendored under picker/assets/ (the site now lives in the
// private impeccable-site repo, so the picker carries its own copies).
// Static assets vendored under picker/assets/ (the site now lives in the
// private impeccable-site repo, so the picker carries its own copies). These
// are referenced from markup at runtime, so Vite never sees them.
const assetsSource = path.join(root, 'picker/assets');
+1 -1
View File
@@ -12,7 +12,6 @@ export const SKILL_CATEGORIES = {
impeccable: 'create',
shape: 'create',
// EVALUATE - review and assess
'design-context': 'system',
critique: 'evaluate',
audit: 'evaluate',
// REFINE - improve existing design
@@ -37,6 +36,7 @@ export const SKILL_CATEGORIES = {
init: 'system',
document: 'system',
extract: 'system',
'design-context': 'system',
live: 'system',
};
+3 -3
View File
@@ -645,9 +645,9 @@ const EXCLUDED_FROM_SUGGESTIONS = new Set([
// These are the commands that audit/critique/etc. reference when suggesting next steps.
const IMPECCABLE_SUB_COMMANDS = [
'adapt', 'animate', 'audit', 'bolder', 'clarify', 'colorize',
'critique',
'design-context', 'delight', 'distill', 'document', 'harden', 'layout',
'onboard', 'optimize', 'overdrive', 'polish', 'quieter', 'shape', 'typeset',
'critique', 'delight', 'design-context', 'distill', 'document', 'harden',
'layout', 'onboard', 'optimize', 'overdrive', 'polish', 'quieter', 'shape',
'typeset',
];
export function replacePlaceholders(content, provider, commandNames = [], allSkillNames = []) {
+1 -1
View File
@@ -14,7 +14,7 @@
* reader is the prose around it.
*/
export const BINDINGS = {
const BINDINGS = {
'palette.primary': { file: 'answers', path: 'palette-primary', kind: 'color', downstream: 'design-md' },
'palette.secondary': { file: 'answers', path: 'palette-secondary', kind: 'color', downstream: 'design-md' },
'palette.tertiary': { file: 'answers', path: 'palette-tertiary', kind: 'color', downstream: 'design-md' },
+4 -4
View File
@@ -24,8 +24,8 @@ import {
SCHEMA_VERSION,
} from './store.mjs';
export const BUNDLE_KIND = 'impeccable-design-context';
export const BUNDLE_SCHEMA = 1;
const BUNDLE_KIND = 'impeccable-design-context';
const BUNDLE_SCHEMA = 1;
const MAX_FILE_BYTES = 1024 * 1024;
const MAX_BUNDLE_BYTES = 20 * 1024 * 1024;
@@ -88,7 +88,7 @@ async function collectFiles(cwd, { includeAssets = true } = {}) {
return { files, skipped };
}
export async function buildBundle(cwd, { includeAssets = true, now = new Date() } = {}) {
async function buildBundle(cwd, { includeAssets = true, now = new Date() } = {}) {
const target = paths(cwd);
const answers = await readAnswers(cwd);
if (!answers) throw new Error('No design interview found. Run /impeccable document to create one.');
@@ -154,7 +154,7 @@ function perSurfaceTable(answers, surfaces) {
}
/** One document a reader, or another tool, can follow without this toolchain. */
export function renderMarkdown(bundle) {
function renderMarkdown(bundle) {
const context = bundle.context?.context || {};
const answers = bundle.answers || {};
const name = bundle.product?.name || 'This product';
+2 -3
View File
@@ -22,8 +22,8 @@ import fs from 'node:fs';
import { readFile, mkdir, rename, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
export const STORE_DIR = '.impeccable/design-context';
export const WORKSPACE_DIR = '.impeccable/visual-cues';
const STORE_DIR = '.impeccable/design-context';
const WORKSPACE_DIR = '.impeccable/visual-cues';
/* The shape of context.json. Bump only when the shape changes, never for a release. */
export const SCHEMA_VERSION = 1;
@@ -45,7 +45,6 @@ export function paths(cwd = process.cwd()) {
journalJsonl: path.join(runtime, 'journal.jsonl'),
draftJson: path.join(runtime, 'draft.json'),
exportsDir: path.join(store, 'exports'),
workspaceDir: path.resolve(cwd, WORKSPACE_DIR),
cuesJson: path.resolve(cwd, WORKSPACE_DIR, 'cues.json'),
fontsManifestJson: path.resolve(cwd, WORKSPACE_DIR, 'fonts.json'),
};
+6 -1
View File
@@ -27,6 +27,8 @@
*/
import crypto from 'node:crypto';
import { realpathSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
// Seeds are inlined (129 entries, hand-curated via a tinder review of
@@ -500,7 +502,10 @@ function hueWord(H) {
// no side effects.
export { SEEDS };
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
// argv[1] must be realpath'd: a skill installed via symlink makes argv[1]
// the symlink path, which never equality-matches import.meta.url's realpath,
// so the CLI would silently never run (same guard as visual-cues.mjs).
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(resolve(process.argv[1]))).href) {
const args = parseArgs(process.argv.slice(2));
const seed = pickSeed(SEEDS, args);
const [L, C, H] = seed.oklch;
+1 -3
View File
@@ -60,6 +60,7 @@ Options:
--cues-dir PATH Visual cues directory (default: .impeccable/visual-cues)
--timeout MINUTES Exit 2 if nothing submits (default: 60)
--fresh Start blank, ignoring any previous answers or draft
--doc Reopen the design context document; no questionnaire
--help Show this help
Output:
@@ -422,9 +423,6 @@ async function copyChosenCue(answers) {
}
async function spawnDocSession() {
/* The read-only path is otherwise unreachable from a test, and a document
that renders without an edit session is a real state worth exercising. */
if (process.env.IMPECCABLE_DOC_SESSION_DISABLE === '1') return null;
try {
const docPort = await findOpenPort(port + 1);
const docToken = randomUUID();
+1 -1
View File
@@ -375,7 +375,7 @@ if (hasFlag('start')) {
const state = JSON.parse(fs.readFileSync(stateFile(key), 'utf8'));
console.log(`QUESTION URL: ${state.url}`);
console.log(`QUESTION KEY: ${key}`);
console.log('Open the URL for the user now: on Cursor with browser_navigate (the in-IDE browser), on another harness with its in-app browser tool, otherwise the system opener (macOS `open`, Linux `xdg-open`), otherwise show the URL.');
console.log('Open the URL for the user now: in-app browser when the harness has one, otherwise the system opener (macOS `open`, Linux `xdg-open`), otherwise show the URL.');
console.log(`Then collect the answer with: node ${fileURLToPath(import.meta.url)} --wait --key ${key}`);
process.exit(0);
}
+3 -3
View File
@@ -40,7 +40,7 @@ function paeth(a, b, c) {
return c;
}
export function decodePng(buf) {
function decodePng(buf) {
if (!buf.subarray(0, 8).equals(PNG_SIG)) throw new Error('not a PNG file');
// Walk the chunk stream: each chunk is [4-byte length][4-byte type][data][4-byte crc].
// IHDR carries the header fields; IDAT is the (possibly multi-chunk)
@@ -306,8 +306,8 @@ function main() {
}
// Only auto-run when invoked directly (`node visual-cues.mjs ...`), not
// when another module imports its exports (decodePng, etc.), e.g. from a
// test file. import.meta.url is Node's realpath of the entry file, so
// when another module imports it. import.meta.url is Node's realpath of
// the entry file, so
// argv[1] must be realpath'd too, not just path.resolve'd: a skill
// installed via symlink (the standard `skills link`/install path) makes
// argv[1] the symlink path, which never equality-matches the resolved