Commit Graph
424 Commits
Author SHA1 Message Date
Paul BakausandGitHub 19ed691810 Merge pull request #583 from pbakaus/fix/build-path-consent-in-init
Ask the build path as its own question, and record only a real answer
2026-08-14 00:11:50 -04:00
Paul BakausandGitHub 43f418a565 Merge pull request #582 from pbakaus/fix/build-path-flip-inspiration-stack
Flipping to comp demotes the inspiration instead of stacking a second slot
2026-08-14 00:11:29 -04:00
Paul BakausandClaude Opus 5 ae56d719af Name the image-gen sources and the unanswered default
Two review findings on #583.

"context.mjs reports it" undersold what counts: that directive only fires on an
OPENAI_API_KEY, so a harness with a native image tool and no key generates
images while the boot output says nothing. Read literally, the step would skip
the question exactly where the toggle belongs. It now names both sources and
says a silent boot is not evidence.

The unanswered branch said to state which path the session takes without saying
which one it is. Left implicit, an agent picks its own, which is the failure
this step exists to stop. It now names comp-first, the default new-work applies
when nothing is recorded.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 00:06:17 -04:00
Paul BakausandClaude Opus 5 1f0169d126 Clear the render clock when a poll generation goes stale
Follow-up to the generation stamp: both probe callbacks returned on a stale
generation without clearing the elapsed-time interval. tryLoad clears it on
re-entry, but a probe that finishes stale schedules no re-entry, so flipping
back while one was in flight left the interval ticking for the rest of the
page's life. Both exits now clear it, which is what the isConnected guard above
them already did.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 00:01:21 -04:00
Paul BakausandClaude Opus 5 f2f1cdb0bb Resolve every zoom target in one handler, and stamp each poll generation
Three review findings on #582, all real and all mine.

Delegating `.pip` and `.media` to `document` separately meant they could not
stop each other: stopPropagation ends bubbling, not siblings on the same
target. Clicking the corner inspiration opened the inspiration and then the
media handler replaced it with the comp, so the corner was unusable on exactly
the cards this PR set out to fix. All three targets now resolve in one
delegated listener in priority order, corner before chip before slot, and a
chip that is not expand keeps its own click instead of falling through.

Flip-back restored the face without clearing what the pending state had added,
so a slot that reached stand-in came back carrying "comp pending" beside a
fresh label, and one whose art had failed came back still marked unavailable.
Restore now clears both, and a slot with no art to restore returns to the
honest "artwork unavailable" treatment rather than being labeled inspiration.

Converting in place means the same node is reused across flip cycles, and the
old poll closure outlived its cycle: a probe from the first flip could settle
the second one, stripping the new shimmer and stopping the live poll while the
comp stayed hidden. Each run now carries a generation stamp that flip-back
bumps, and both probe callbacks bail when it moves.

The test covers the corner click against the landed comp, and I confirmed it
fails when the priority ordering is removed.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 23:56:51 -04:00
Paul BakausandClaude Opus 5 83e8b4645c Ask the build path as its own question, and record only a real answer
A live Codex session folded the build path into the stack question as a
trailing recommendation ("I recommend static and code-first"), never said what
either name means, treated its own recommendation as the user's answer, and
wrote `buildPath: code` as a standing default. The user disagreed and flipped
the board to comp-first, but a flip binds one session, so the unasked default
stayed on disk to steer every later round.

Step 5 said to "ask once ... stated as the trade it is", which the run
violated, but the step left the shortcut open: it sits after the interview
ends, it never says a recommendation is not an answer, and unlike the stack
question it offers no way to end without a value, so an agent holding no answer
writes one anyway.

Now: it is its own question, never a clause inside another; the trade is stated
in the question the user reads, because the two names mean nothing on first
contact; only the user's own choice is written; and an unanswered question
records nothing and says so. Unset is a working state, since the page toggle
governs the session and new-work's one-time offer still captures the answer at
the first flip.

The same run also wrote "Code-first build path" into PRODUCT.md's `## Stack`,
so the step now says the config is the only place this lives: a copy in product
truth outlives the setting and steers rounds nobody can trace back to it.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 23:44:38 -04:00
Paul BakausandClaude Opus 5 b7960ecde3 Keep the scaffolder test inside its own workspace
Two review findings on #581, both fair.

The scratch app symlinked the whole of the repo's node_modules, so the
scaffolder's output directory, `node_modules/.impeccable-live`, resolved to the
REPO's copy. Variants were written there and survived `afterEach`, which only
removed the temp dir; the next case reused the session id, and the scaffolder
keeps existing variant files, so a case could parse a previous case's source
against a fresh manifest. Now only `svelte` is linked, into a node_modules the
workspace owns, and each case gets its own session id. Svelte's own
dependencies still resolve, because node follows the link to its real path
before looking for them.

The comment also pointed at a `PROPS_SCRIPT_SHAPES` symbol that does not exist
in the test file. Dropped the name and kept the file reference.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 23:38:35 -04:00
Paul BakausandClaude Opus 5 ec189f4536 Flipping to comp demotes the inspiration instead of stacking a second slot
Two defects in the same few lines, both from `enterComp` hand-building a media
slot after the deal instead of reaching the shape a comp-first render serves.

A code-led card carrying catalog art shows that art as its face. Flipping to
comp inserted a fresh shimmer slot above the body and left the face alone, so
the card rendered the inspiration full-bleed with the rendering comp stacked
under it: two images of equal weight, which is the one thing the corner
treatment exists to prevent. The flip now converts that slot in place, moving
the art into the `figure.pip` and dropping the face label, and flipping back
restores it, so a round-trip leaves the card as it was dealt.

The slot it built also carried no chips, and the zoom handlers were bound per
element at load, so a comp that streamed in after a flip could not be opened at
all: no expand affordance, and no click handler on the art. The three lightbox
handlers are now delegated, which is what makes any later-built slot work, and
a converted slot keeps the chips it already had. Polling learned to stop on a
slot that stays in the DOM but loses its pending state, which only happens now
that a flip back can restore rather than remove.

The existing toggle test covered a wireframe card, where the schematic is
hidden and a fresh slot inserted; that branch was fine, which is why this went
unseen. The new test drives the art-carrying card and fails on the stacking
assertion without this change.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 23:32:02 -04:00
Paul BakausandClaude Opus 5 5961269cb5 Stop emitting a JSDoc cast into every Svelte variant (fixes #580)
Live mode scaffolds each Svelte variant with a props script that annotated the
declaration:

    /** @type {{ title: string; }} */
    let { title } = $props();

A JSDoc `@type` written directly before a value is also JSDoc's cast syntax,
and esrap 2.3.3, the printer Svelte emits JS through, moves that annotation
onto the template's own declaration:

    var /** @type {{ title: string; }} */ (h1) = root();

`var (h1) = ...` does not parse. The .svelte source is valid, the compile
succeeds, and the failure lands in the browser's dynamic import as "Unexpected
token '('": the variant never mounts and the session shows nothing. `@typedef`
carries the same shape without being a cast, so both builders emit that.

This is not test-only. Every Svelte variant we generate carried the construct,
so live mode was broken for any user whose install resolved esrap 2.3.3.
Svelte declares `esrap: ^2.2.12`, so a fresh install takes it; this repo's
lockfile pins 2.3.0, which is why unit tests stayed green while the fixture,
which installs into a temp dir, did not.

Two reasons the existing pre-publish guard could not have caught it, now
recorded next to it:

  - `compileCheckVariants` compiles with `generate: false`, so there is no
    emitted JS to inspect.
  - `loadSvelteCompiler` resolves the compiler through createRequire, which
    Svelte's export map routes to a prebuilt CJS build. A dev server imports
    `src/compiler`, and only that path runs the app's installed printer. The
    guard was checking a different compiler than the browser runs.

The new suite therefore imports the compiler as ESM and asserts the emitted
JavaScript parses, rather than pinning the comment style: a future printer that
mangles some other construct fails it too. The first draft used createRequire
and reported green against the exact input that breaks in a browser, which is
the mistake worth not repeating.

Verified against svelte 5.56.9 with esrap 2.3.3. Full live-e2e sweep green,
26 fixtures.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 23:18:29 -04:00
Paul BakausandClaude Opus 5 d6c2442dbe Say why the flip is session-only, not just that it is
The BUILD_PATH_DEFAULT line ended on a bare absolute: a flip "is never written
back to the config". True wherever the line appears, since it is emitted only
when a default is already recorded, but the sentence does not carry its own
scope and has now been read twice as a rule that overrides new-work's one-time
offer. That is the same failure the previous commit fixed in serve-question,
where an unscoped "never write it" did override the offer.

The directive now states the condition it depends on and names where the
exception lives, so a reader who meets the line without the surrounding code
cannot draw the wrong rule from it.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:52:44 -04:00
Paul BakausandClaude Opus 5 816ffe92d0 Surface the build-path finding in doctor, and keep cwd out of the lookup
Round two of review findings, all four valid.

`doctor` builds its own finding list and never called `checkBuildPathUnset`,
so `config-build-path-unset` could not appear in the report even though
doctor.md documents it. That is also the only path left once stalenessCheck is
off, which is exactly when someone is looking for it.

The lookup chain included `process.cwd()`, which lets an ambient invoking
directory decide another project's workflow: run from workspace A with
--target resolving onto workspace B, and B inherited A's buildPath ahead of
the repository default. The chain is now the resolved project then the repo
root, matching `checkBuildPathUnset` exactly; cwd stands in only when no
project resolved at all.

Two prose contradictions, both mine. new-work said to write the value "when
the user says yes" and then to "record the answer either way", which reads as
persist-on-yes-only and leaves the decline to be asked again next session. It
now says the write always happens and the answer picks the value. The README
still pointed existing projects at re-running init, which is the problem this
PR exists to solve; it now names the toggle as the migration path.

The workspace-isolation test earned a correction of its own: the first version
passed a relative --target, which resolves against the caller's cwd and puts
projectRoot back on the calling workspace, so it asserted nothing.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:41:32 -04:00
Paul BakausandClaude Opus 5 c0e7f2d778 Read the repo-root build path, and stop overstating what a flip forbids
Two findings from Greptile on #579, both about the same key seen from
different roots.

`appendBuildPathDirective` searched projectRoot and cwd but never repoRoot,
while `checkBuildPathUnset` reads both. In a monorepo that committed the
preference once at the root, the two disagreed in the worst direction: the
staleness finding stayed silent because a value existed, and the directive
never named it, so nothing on screen explained why the recorded default was
not being honored. Roots are now ordered nearest first, workspace over repo
root, with regression tests for both the fallback and the override.

The ANSWER line for a flipped path said "never write it to settings". The
page indeed never writes it, but the sentence read as a rule and applied
itself to new-work's one-time offer, which exists for exactly the case a flip
creates: a project with no recorded default, asked once after the round
closes. It now states what the page does and names the exception.

The same report's first issue also named context.mjs, and that part does not
hold: its directive is emitted only when a value is already recorded, which is
precisely when session-only is the correct instruction. Left as is.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:20:19 -04:00
Paul BakausandClaude Opus 5 07663f5fbd Stop the update directive from spelling out a command it forbids
Two defects the skill-behavior baseline had recorded as failing on main.

`UPDATE_AVAILABLE` told the agent to ask once, then said "If they agree, run
`npx impeccable update`", then said to continue without waiting. Nothing gated
the run on an answer, and the same sentence removed the wait that could have
produced one, so the command read as the next step and sonnet took it. The
offer stays; the command leaves the turn. Running it mid-session rewrites the
files the session is reading and only takes effect next session, so there is
nothing to gain by running it now, and the directive says that rather than
relying on the model to infer it. Failed 3 of 3 before, passes 3 of 3 after.

Scenario 15 was a broken fixture, not a routing defect. The iOS workspace held
PRODUCT.md and nothing else, so `audit the app in this workspace` named an app
that was not there: sonnet spent its step budget hunting for it, including a
`find /` across the filesystem, and read no reference file at all. The
assertion reported "loaded audit.md instead of the variant" when the truth was
"loaded neither". One SwiftUI screen makes the request answerable, and the
scenario then passes on unmodified main, which is the evidence that the skill
text was never at fault. This is the convention MINIMAL_LANDING_HTML already
established for the web scenarios; the native fixture never received it.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:04:53 -04:00
Paul BakausandClaude Opus 5 c489335799 Build path becomes a config key existing projects can actually reach
The build-path preference shipped as a question only `init` asks, written to
a file only `init` writes. Nothing routes an initialized project back through
init, so every existing project took the comp-first default without anyone
choosing it, and the only recourse was a footer toggle that binds one session.

Neither the setting nor the round that preceded it ever reached a release
(skill-v4.0.4 has no `buildPath`, no `comp-led`, no `.impeccable/settings.json`),
so the PRODUCT.md standing-commitment fallback describes an era that never
existed publicly. It is deleted rather than honored: told a field might exist,
models go hunting for it and preserve it.

- `buildPath` moves from `.impeccable/settings.json` into the unified
  `.impeccable/config.json`, which already has a known-keys registry, doctor
  coverage, and a gitignored `config.local.json` override. Whether a machine
  has an image tool is a property of that machine, so the local file wins.
- new-work captures the answer from behavior instead of an interview: a toggle
  flip on a project recording nothing asks once, after the round closes,
  whether to keep it. The answer is written either way, because a declined
  offer nothing writes down is an offer the next session makes again.
- Two findings: `config-invalid-build-path` (an unread value rides the default
  rather than the opposite path) and `config-build-path-unset`, gated on a
  product record plus evidence of direction work so polish-and-audit projects
  never hear about a setting they do not use.
- init treats a recorded value as a confirmed answer, resolving its conflict
  with Step 1's "do not reopen confirmed fields".
- The setting was undocumented in the README and doctor.md. Both now cover it.

Also records a measured skill-behavior baseline. Three cells fail on unmodified
main (scenarios 9 and 15, `initialized natural build`), verified against a clean
worktree; the suite README now says so, so the next person does not spend the
hour attributing them to their own branch.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 21:52:17 -04:00
Paul BakausandGitHub 710aa57637 Merge pull request #576 from pbakaus/fix/ask-instruction-message-boundary
Make critique's report and close actually land
2026-08-13 17:12:52 -04:00
Paul BakausandGitHub 504b8f2a22 Merge pull request #571 from pbakaus/codex/issue-565-sketch-timeout
Fix stalled missing decision comps
2026-08-13 17:06:43 -04:00
Paul BakausandClaude Opus 5 121602079c Deliver the report as its own step; retune the lineup
Two failures the trace test found were structural, not model quirks.

critique.md described the report's format and then went straight to writing a
temp file, with no step saying to output the report. gemini-3.6-flash and luna
both responded by bundling heredoc, snapshot write, trend read, and cleanup into
one bash call and stopping, leaving a perfect archive nobody had read. A
"Deliver the Report" step now precedes persistence, and persistence describes
itself as a copy of what was already sent. gemini-3.6-flash failed three
consecutive runs before this and its failures afterward all show the report
reaching chat.

The close is also step 6 of the persistence list rather than a section after it,
since the same shape is what fixed delivery.

Lineup: gpt-5.6-luna and deepseek-v4-flash out, gpt-5.6-terra in at
reasoningEffort high (IMPECCABLE_SKILL_BEHAVIOR_EFFORT overrides), gemini 3.5 to
3.6. Provider options resolve from the model object inside the harness so the 21
runTurn call sites are untouched. Verified the effort actually reaches the API
rather than being silently dropped.

The Gemini bump was not cosmetic: 3.5-flash passed critique closes twice and
3.6-flash then failed three times against identical text. A version bump inside
one family changed the outcome, so the README now treats cross-version carryover
as unmeasured.

Known floor, recorded: critique closes is flaky on gemini-3.6-flash, 1 run in 3.
Two structural attempts moved it from consistently failing to intermittently
passing and then stopped paying. claude-sonnet-5 and gpt-5.6-terra are clean.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:03:19 -04:00
Paul BakausandGitHub 7d907bbb14 Merge pull request #572 from pbakaus/codex/unify-svelte-accept-flow
Simplify Svelte accept orchestration
2026-08-13 17:00:57 -04:00
Paul BakausandClaude Opus 5 ebc63f071a Fix critique's close on the right mechanism
The earlier fix in this branch was built on a wrong diagnosis. It assumed a
structured question hides any prose sharing its message, so it split report and
question across two turns. A controlled check showed prose before a question
renders fine; what hides a report is emitting it AFTER the question. The split
therefore fixed nothing and introduced a worse failure: a turn that ends on the
report is a turn that ends, and the questions never arrived at all.

Persistence returns to main's ordering, byte for byte, and the boundary prose is
gone. What replaces it is a position rule: the question is the last thing in the
response.

The trace test added here found two failures beyond the reported one. Critique
can fail to land in three ways, and they are now all asserted:

  1. Question emitted before the report, hiding it behind the picker.
  2. No close at all: no questions and no skip line, so polish inherits nothing.
  3. Report authored into the persistence heredoc and never written to chat,
     leaving a perfect snapshot and a user who sees nothing.

Mode 3 predates this branch entirely. Persistence step 1 now says the temp file
is an archive copy, not delivery.

The Codex final-question gate is promoted out of its <codex> fence, where it was
stripped for three of four providers, and the skip branch is now a countable
threshold (fewer than 3 Priority Issues) rather than a judgment call.

Known floor, recorded in the suite README: gpt-5.6-luna passes 1 run in 6 and
deepseek-v4-flash is flaky. claude-sonnet-5 and gemini-3.5-flash are consistent.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:14:44 -04:00
Paul BakausandClaude Opus 5 d4e1b0902f Enforce the ask_instruction sentence-initial contract
Review on #576 caught document.md:71 splicing {{ask_instruction}} after
"then", which is the same defect this branch set out to fix. Rendered for
Codex it produced "Show the user the existing file, then STOP and use Codex's
structured user-input/question tool...". The line now starts a new sentence.

The comment added to PROVIDER_PLACEHOLDERS asserted the contract without
enforcing it, which is exactly how four reference files shipped the splice in
the first place. validateAskInstructionSites() in build.js now checks every
call site and fails the build on a mid-sentence interpolation, and the comment
points at the gate instead of asking authors to remember.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 13:24:29 -04:00
Paul BakausandClaude Opus 5 0e5c6cbe17 Keep critique's report out of the question's message
The critique report and the AskUserQuestion call shipped in one assistant
message, so the report stayed hidden until the user answered the picker and
the command read as if it had never run.

Reorder critique's persistence steps so the temp-file cleanup runs after the
report and trend line are sent. That cleanup now ends the message carrying the
report, leaving the questions to open a fresh one. Both critique.md and
overdrive.md state the constraint and why it exists, so the ordering is not an
unexplained sequence a model can optimize away. Overdrive additionally moves
its direction descriptions inside the question options, where the user is
actually reading them.

Also fix the ask_instruction splices. The placeholder is a complete sentence,
but five call sites spliced it mid-sentence and shipped text like "stop and
STOP and call the AskUserQuestion tool to clarify. before expanding it". Every
call site is now sentence-initial and the twelve lowercase provider values are
capitalized to match, with a comment in utils.js pinning the contract.

Record a workflow-contract baseline for the current model lineup. The two
failures seen while validating this change are pre-existing: bolder refinement
fails on deepseek-v4-flash identically with bolder.md reverted to HEAD, and
redesign replaces DESIGN is flaky on assertions driven by new-work.md, which
this change does not touch.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 01:06:33 -04:00
Paul BakausandClaude Fable 5 9e8b9bc389 Build-path toggle aligns with the headline row
Same line as the round's title rather than the brand mark: the control
reads as part of the round it configures, and the brand row stays clean.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 23:29:14 -04:00
Paul BakausandClaude Fable 5 9b055c82d7 Build-path toggle rides the brand row, top right
Top-left is the brand and reading-entry corner and mode controls belong top
right; on the brand row the toggle also costs no vertical space, so the
headline keeps its position.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 23:26:39 -04:00
Paul BakausandClaude Fable 5 76b9aaf021 Build-path toggle moves to the header; a code-to-comp flip confirms first
The toggle sits top-left under the brand instead of in the footer bar, and
flipping to comp-first now opens a confirm dialog before anything renders,
since the flip starts billed, minutes-long generation; flipping back stays
free and immediate. The dialog lives at the document root so it never loses
the stacking fight with the deck. The schema blob also states harder that
toggle: true may only be offered when image generation exists.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 23:23:34 -04:00
Paul BakausandClaude Fable 5 ea90b23bc8 Merge main: build-path setting lands under the evidence-gated fallback
The retired execution-contract round and the buildPath payload keep
main's text; the decision-page fallback keeps this branch's gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 17:44:37 -04:00
Paul BakausandClaude Fable 5 64dd60a78a Build path becomes a setting plus a page toggle; the followup contract round retires
- serve-question: payload buildPath { value, toggle } renders a footer
  segmented control (comp first / code first) with the trade stated in one
  line; the default comes from settings, a flip binds that session only.
  Code-led rounds treat declared comp paths as flip reserves: wireframes
  render, a flip to comp shimmers the slots and surfaces once through
  --wait as BUILD PATH FLIPPED so the agent starts generating mid-round;
  the flip back is free and a landed comp stays. The ANSWER carries
  buildPath and buildPathFlipped with a session-only directive.
- init Step 5 asks the preference once (only when image generation exists)
  and writes .impeccable/settings.json; context.mjs surfaces the recorded
  default every session; PRODUCT.md standing commitments stay honored as
  the fallback.
- new-work retires the two-card execution-contract round: no round asks a
  workflow preference. followup stays as the generic same-table mechanism.
- e2e: new toggle test (14/14 with the wireframe test).

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 16:56:39 -04:00
Paul BakausandClaude Fable 5 6129744410 Scope the fallback to exit code 2 from starting the script
Bugbot's finding: exit 2 is overloaded, and at --wait it means the
question server died, so the unscoped rule would drop a live visual
round onto the text channel after a transient daemon loss. The gate now
names the serving invocation, which also settles Copilot's exit-code
ambiguity, and the display clause reads grammatically.

AI-assisted (Claude Fable 5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 16:43:39 -04:00
Paul BakausandClaude Fable 5 e2421aff43 The decision page's fallback is earned by exit code, never predicted
Across every recorded gpt-5.6-sol session, serve-question.mjs was never
invoked once: the rule's prose list of fallback environments (headless,
CI, an eval worker, a remote shell) let the model match itself against
the list and take the structured question tool without running the
script, while claude-opus-5 on the identical harness runs the script
every time and the page works. The direction is then chosen with no
imagery on the table, the catalog challengers are weighed without their
art, and the session's own safest candidate wins: measured end to end
on the eval harness, this is where bland output enters.

The environment list is deleted; the script's own exit 2 is now the
only key to the fallback, and the script already prints the rationale
and the override at runtime to exactly the sessions that hit it. Same
gate on the comp round's approval point: inline image rendering earns
the in-harness path, and a text-only surface is not display.

One adversarial review pass; its two word-level findings are applied.

AI-assisted (Claude Fable 5), prepared for maintainer review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 16:37:03 -04:00
Paul BakausandClaude Fable 5 ac4c3200db Surface rounds deal three structures, draw wireframes, and anchor comps on a reference screenshot
- concept-seed --scope surface deals three grounded-list indices (dice-picked,
  primary leads) instead of one: a single card is not a choice, and the
  no-lineup rule stays direction-only, where it was written for worlds
- serve-question renders a new per-card wireframe field as a layout schematic
  in the media slot: the code-led channel's visualization, no image
  generation needed, no card back, no salience weight
- generate-image gains --ref (repeatable): routes through the edits endpoint
  with input images, so an established world's comp inherits identity from a
  captured screenshot of a real page instead of a prose paraphrase; tested
  against impeccable.style, where the reference-anchored comp reproduced the
  live site's chrome and the prose-only comp drifted
- new-work rung two rewritten around the dealt hand: lock-in is the
  approval, a locked comp builds comp-led and discharges the visualize.md
  three-option round, a locked wireframe builds code-led; visualize.md
  records the exemption and the reference-image discipline, including the
  reference-leak caveat (chrome carries, the reference page's content
  does not)

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 16:28:15 -04:00
Paul BakausandGitHub e36833ce21 Merge pull request #521 from digitallamb/pr/hermes-provider
Add Hermes Agent as a supported provider
2026-08-12 16:07:43 -04:00
Paul BakausandGitHub 6c837bd7d4 Merge pull request #562 from pbakaus/codex/issue-561-critique-signals
Fix critique routing snapshot metrics
2026-08-12 15:57:19 -04:00
Paul BakausandClaude Fable 5 f66eace20d Decision page: plain-language raises, IMPECCABLE'S PICK, sticky footer, short-viewport fit
- The raise block drops the side-tab left border for a quiet patina panel,
  and drops the poker jargon: "Improved by Impeccable's worlds" with
  per-line "From <world>" donors, on single raises too; tooltip, aria, and
  screen-reader copy follow
- The pick-card kicker convention renames MY PICK to IMPECCABLE'S PICK at
  every definition site, so users stop reading "my" as themselves
- The footer (steer, registers, canon exit) is a sticky full-bleed bar on
  wide viewports, sharing one --page-inset with the content column; portrait
  keeps it in flow where the deck scrolls internally
- Short landscape viewports compact the headline and narrow the cards so a
  full round fits 1440x800

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 15:26:34 -04:00
Paul Bakaus 3a26dcb809 Keep decision body order in fallback
Prepared and verified with AI assistance under maintainer authorization.
2026-08-12 15:25:04 -04:00
Paul Bakaus 37be3fa36b Fix stalled missing decision comps
Restated on current upstream main after the comp-field migration. Prepared and verified with AI assistance under maintainer authorization.
2026-08-12 15:09:27 -04:00
Paul BakausandClaude Fable 5 53a6947653 Close the unnamed-focal-moment gap; unstutter the inverse-failure lead
Cursor's finding was real: gating the density check on a named focal
moment let a busy comp pass whenever the direction named none, which is
the common case on the lane that produced the busy comps. The second leg
reuses the bullet's own distinction: several regions performing the
concept at once is the same shout; regions doing their jobs are not.

AI-assisted (Claude Fable 5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 14:29:59 -04:00
Paul Bakaus f1b7111503 Simplify Svelte accept orchestration
Unify Svelte component accept and discard around one operation dispatch, source lock, error path, and result emission while preserving their existing CLI contracts. Add direct CLI characterization coverage for both operations.\n\nAI-assisted implementation under pbakaus's scheduled-refactor authorization.
2026-08-12 14:15:14 -04:00
Paul BakausandClaude Fable 5 cb305fdca1 Two adversarial reviews later, the discipline says half as much
Two independent skeptic passes over the added prose, one hunting
oversteer and example bias, one hunting mode and platform damage. What
they killed, and why:

- The absolute 'never a medium' rule contradicted the file's own
  imagery-stance fixity two paragraphs up and stripped legitimate guards
  (an illustration-committed world, a native app screen warding off
  stock-photo drift). A medium ban now belongs to the committed imagery
  stance, never to caution, and the rule appears once per reader context
  instead of five times corpus-wide.
- The quoted incident string and the four-example subject list taught
  the model the exact framings they existed to prevent. Gone; the
  abstract rule plus the point-at-the-subject check carry it.
- 'A first-time visitor learns what this is, why it matters, and what
  to do' was Persuade anatomy imposed on all four modes. The guard is
  now mode-neutral: a quieted region keeps its information and stops
  performing.
- 'Calm is what Operate and Read surfaces are for' contradicted
  operate.md's density affordance. Deleted; modes stay defined in one
  place.
- The focal-moment count now presupposes nothing: it fires only where
  the direction names a focal moment, and only on same-scale rivalry,
  so an even, calm field stops reading as a failure.
- The decision-comp clause and the mode bullet no longer restate what
  they can reference.

Net: the prose additions drop from roughly 480 words to under 200, with
no quoted strings and no example lists.

AI-assisted (Claude Fable 5), prepared for maintainer review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 17:53:10 -04:00
Paul BakausandClaude Fable 5 e91c273361 The density check cuts competition, never content
Proven necessary by its own demo: the first re-render of the declined
moto-forum comp satisfied subject-present and one-dominant-move by
deleting the value proposition, leaving a members' index that told a
first-time visitor nothing about what this is or why to care. Paul
caught it. Quieting a region means it stops performing, not that it
leaves; empty is quieter, not calmer.

AI-assisted (Claude Fable 5), prepared for maintainer review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 17:25:04 -04:00
Paul BakausandClaude Fable 5 e867487d55 Ban fabrications, never media: counter the exclusion-list reflex everywhere prompts are authored
The declined moto-forum comp's prompt read 'no gradients, no rounded SaaS
cards, no photography, no fake member counts, no badges, no testimonials':
the reflex that rightly bans invented claims swallowed the one medium the
subject lives in, and that is exactly how a motorcycle forum got comped
with no motorcycles. The lektor prompt's 'no AI imagery', written by an
image model, is the same fingerprint.

One counterweight, phrased once per authoring surface: the comp
discipline's subject-presence check (which the decision comps already
bind), the asset producer's own prompt rules (a standalone agent that
never reads visualize.md), and new-work's author-assets law (the path a
code-led build takes without the comp round). Truth binds claims, not
demonstrations; a photo of the subject doing its job is a demonstration.

AI-assisted (Claude Fable 5), prepared for maintainer review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 16:09:21 -04:00
Paul BakausandClaude Fable 5 248a4a699a Retire the sketch era's wire name: the field is comp, sketch is an alias
The deliverable died in #545; the word survived as the decision-page
payload's field name, annotated everywhere it appeared with the same
compatibility apology. The page and the skill text ship together and
payloads are per-session, so the compatibility burden is one input alias,
not a frozen name.

serve-question.mjs: the card field, the answer key, the schema docs, the
--schema example, the help text, and every internal identifier (compSrc,
data-comp, .media.comp-pending, img.comp, comp-note) now say comp; a
payload declaring the legacy sketch key still renders and answers
identically. new-work.md and the asset producer drop their wire-name
parentheticals. The unit suite covers the canonical answer key coming
back from a legacy-key payload; the new-work e2e's declined-card stray
comp stays declared as sketch, which doubles as alias coverage.

AI-assisted (Claude Fable 5), prepared for maintainer review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 15:53:25 -04:00
Paul BakausandClaude Fable 5 62e90a257f Comps are shipped screens: subject present, mode readable, depth over coverage
Factory review evidence (two batches, both lanes): generated comps drift
poster-ward. They render the world's atmosphere at high density, drop the
surface's subject (a motorcycle forum comped with no motorcycles), and stop
reading as screens a product would ship. The existing anti-vignette
self-check catches the fully collapsed case but says nothing about density
or subject presence, and new-work's "committed all the way" reads as a
coverage instruction.

Three sibling self-checks in visualize.md's comp discipline, each phrased
per mode (Persuade/Operate/Read/Experience) and platform-neutral: the
subject appears as the content the regions hold; the mode must be readable
from the image alone; commitment is depth, not coverage, with one dominant
move per viewport. new-work.md's decision-comp rule gains a clause binding
the same checks so the direction round inherits them explicitly.

AI-assisted (Claude Fable 5), prepared for maintainer review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 15:29:51 -04:00
dc4e4a4bd6 Denoise the design hook and let agents self-serve confident ignores (#508)
* Denoise the design hook and let agents self-serve confident ignores (#497)

The directive footer now emits in full once per session (a one-line
reminder after), the DESIGN.md staleness note is mentioned once per
session, rule descriptions dedupe within an emission, and the per-line
ignore suggestion shrinks to the bare rule/value pair. The footer and
hooks.md replace the confirmation-gated ignore policy with a three-way
triage: fix real problems, self-serve the narrowest ignore for confident
false positives or sanctioned exceptions and disclose it (with an honest
--reason), ask when unsure. Self-serve stops at ignore-value, and the
footer now gives a runnable hook-admin.mjs command instead of a slash
command agents cannot execute.

Measured on a seeded lab session replaying 11 hook events: 33,658 to
14,063 chars of agent-visible output (-58%).

AI-assisted (Cursor agent), directed and reviewed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Preserve the policy footer and honor maxChars under constrained budgets

Greptile's runtime check found two pre-existing clamp gaps that matter
more now that the full policy emits once per session: the last-resort
tail slice cut the footer off an over-budget emission, and the DESIGN.md
staleness note was appended after clamping, pushing past maxChars.

The clamp now gives the footer the budget first, clipping the finding
line and downgrading full to short policy when needed. The staleness
note defers, without consuming its session flag, to a later emission
with room.

AI-assisted (Cursor agent), directed and reviewed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Harden the constrained-budget clamp: keep findings, honest flags, guaranteed note

Review follow-ups from Bugbot and Greptile on the clamp fix:

- The clamp retries with the short policy before dropping finding lines
  that fit beside it, and a grouped result that kept only a file header
  no longer counts as a fit.
- The full-footer session flag commits only when the full policy
  actually survived the clamp, so a downgraded emission does not mark
  the session as having seen a policy it never received.
- Render paths reserve room for a pending DESIGN.md staleness note, so
  it is delivered inside the budget on the first emission instead of
  deferring behind full emissions indefinitely.

AI-assisted (Cursor agent), directed and reviewed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Route the Cursor deny cap through the clamp and match the whole footer

Bugbot follow-up: cursorBlockMessage tail-sliced at 4000 chars after
render, which the default 8000-char budget made reachable, and a cut
that spared the footer's opening words still committed the session
flag. The 4000 cap now feeds through the renderer's footer-preserving
clamp, and commitFooterShown matches the complete footer text instead
of a sentinel.

AI-assisted (Cursor agent), directed and reviewed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Reconcile hook.test.mjs expectations with main's per-platform quoting

Three tests fell out of sync when main's quoteCommandArg change (#533,
building on #476) met this branch's footer/hint rework. Test-only
changes; production logic untouched:

- The full-footer test now accepts either close quote after the
  hook-admin.mjs path, since quoteCommandArg single-quotes absolute
  paths on POSIX and double-quotes them on Windows. The short-footer
  guard rejects `node '` and `node "` alike.
- The #476 hostile-value test asserts the new bare
  `ignore-value <rule> '<value>'` hint format. The security property is
  unchanged: the value still passes through quoteCommandArg, so
  $(touch pwned) stays single-quoted and inert.
- The #533 test previously asserted a concrete quoted `--file` path in
  the footer; directiveFooter() now carries only literal placeholders,
  so that surface is gone. The per-platform assertion moves to the
  per-finding ignore hint, the remaining user-visible surface where
  scanned file content flows through quoteCommandArg.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix two Bugbot findings: Cursor prefix budget and footer-cutting tail slice

Both flagged by Cursor Bugbot on PR #508 after the main merge; both real.

1. cursorBlockMessage computed min(maxChars, 4000 - prefix), so a
   configured maxChars at or below the Cursor ceiling never charged the
   BLOCK_PREFIX against the budget: the final deny text could exceed
   maxChars by the prefix length, and appendDesignSystemNoteOnce's size
   check lost exactly the room designNoteReserve had held back. The
   prefix now comes off whichever limit binds. Default-config behavior
   is unchanged (min(8000, 4000) - 60 equals the old 4000 - 60).

2. The note reservation is subtracted after renderTemplate's 500-char
   floor, so the clamp can run below the budget clampLastLine assumed
   safe, and its last-resort path tail-sliced the rendered text, cutting
   the policy footer (the failure mode this PR exists to eliminate) when
   a deep file path met a pending DESIGN.md note. The reservation order
   stays (the staleness-note delivery guarantee at floor budgets depends
   on it); the last resort now drops the finding line and clips the head
   instead, so the footer survives every path. New regression test pins
   it: 6 findings, 100-char path, maxChars 500, reserveChars 134.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude <noreply@anthropic.com>

* Charge the Cursor deny prefix after the renderer's floor, not before

Greptile's runtime check caught the residual from ead0346d: subtracting
BLOCK_PREFIX from the maxChars passed to renderTemplate does nothing at
floor-tier configs, because the renderer re-raises any budget below its
500-char floor. At maxChars 500 with a stale design sidecar, the
prefixed denial landed at 432 chars and appendDesignSystemNoteOnce
could not fit the staleness note inside 500, deferring it (flag
unconsumed) for every equivalent denial in the session.

The prefix now rides in reserveChars, which comes off after the floor,
so it is charged at every config tier and the final prefixed message
plus a pending note closes exactly at the binding limit (499 chars in
the regression scenario). Default-config output is byte-identical:
max(500, min(8000, 4000)) - prefix equals the old min(8000, 4000) -
prefix. New end-to-end Cursor preToolUse test pins the path with a real
stale sidecar at maxChars 500.

AI-assisted (Cursor agent), directed and reviewed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Paul Bakaus <paul.bakaus@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-11 13:42:39 -04:00
Paul Bakaus 357f358050 Reject empty critique metrics
Treat empty and whitespace-only snapshot values as missing so malformed frontmatter cannot reintroduce plausible zeroes.

Prepared with AI assistance under maintainer pbakaus's standing automation authorization.
2026-08-11 13:00:31 -04:00
Paul Bakaus d4aacaccfd Fix critique routing signals
Read the documented critique snapshot keys while preserving legacy aliases, and surface missing metrics as null instead of zero.

Prepared with AI assistance under maintainer pbakaus's standing automation authorization.
2026-08-11 12:50:01 -04:00
Paul BakausandGitHub 7fa695093e Bound copy-edit prompt context (#528)
* Bound copy-edit prompt context

Whitelist and truncate staged operation context before it reaches the local agent prompt.

AI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.

* Harden copy-edit prompt bounds

Bound repair, candidate, and element context consistently and preserve absent source positions as null.\n\nAI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.

* Preserve bounded repair context

Keep repair attempt metadata and nested diagnostics while retaining prompt limits.\n\nAI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.
2026-08-09 16:56:49 -07:00
Paul BakausandGitHub 63fb8a56f9 Fix Claude copy-edit prompt transport (#529)
Pass staged copy-edit prompts over stdin so large batches do not exceed platform argv limits.

AI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.
2026-08-09 16:56:46 -07:00
Paul BakausandGitHub 181212cb2d Map polish's evidence and verify steps per platform (#550)
* Map polish's evidence and verify steps per platform

polish.md was the last command reference verifying through web-only
vocabulary after #546 gave the pipeline its native leg. Three targeted
mappings, following the in-file precedent new-work.md set (the
classify-triage-polish-verify flow itself is platform-neutral, so no
polish.native.md):

- Evidence gathering: desktop and mobile sizes on the web; the shipped
  device classes on simulator, emulator, or hardware on native, per the
  platform reference's Verifying the build section.
- Verify checklist layouts: phone and tablet size classes, both
  orientations where supported, on native.
- Verify checklist "supported browsers": native has none, so the
  analogues are named (runtime warnings, dropped frames, supported OS
  versions).

Assisted-by: Claude Code

* fix: branch the verify checklist web-vs-native explicitly

Copilot follow-up: the parenthetical style could read as both term
sets applying on native. The two bullets now branch explicitly, and
the shared items (console errors, layout shift, latency, image
loading) stay unbranched since they apply everywhere.

Assisted-by: Claude Code

* fix: restore runtime warnings to the native verify branch

greptile follow-up: the explicit-branch restyle dropped the runtime
warnings requirement the parenthetical carried; folding it into
"console errors everywhere" hid it behind web vocabulary. It is back
as its own item in the native branch.

Assisted-by: Claude Code
2026-08-09 15:35:07 -07:00
Paul BakausandGitHub 19786e7a22 Native leg for the verify-and-review pipeline (#546)
* Give the verify-and-review pipeline a native leg

The build-verify-review loop assumed a browser end to end while the
comp side of the system was already platform-aware: new-work.md,
visualize.md, and the asset producer all comp a native app portrait at
its device viewport, and then the verification steps asked for desktop
and mobile browser screenshots of it. Concretely:

- new-work.md step 7 ordered detect.mjs on every hookless build with no
  platform guard. routing.md declares the detector web-only and the
  design hook skips native projects, so a native build was always
  hookless and always ordered to run an HTML rule engine over
  Swift/Kotlin/RN code. The playbook now guards it: web-only, and on
  native the reviewer's floor check is the named slop gate.
- The inspection round and the SKILL.src.md batched-round principle
  named desktop and mobile as the only viewports. Both now map per
  platform: web keeps desktop and mobile; native inspects the shipped
  device classes per OS, captured from the simulator or emulator.
- ios.md and android.md carried no verification guidance at all, so
  nothing told a native run how to produce the screenshots the evidence
  chain depends on. Each gains a Verifying the build section: simctl /
  adb capture commands, dark-appearance and type-scale checks, and the
  simulator-vs-hardware honesty line.
- The finish reviewer judged native builds blind: it never runs
  context.mjs and its packet carried no platform guidance. On native
  the packet now includes the platform reference path(s) and a
  no-detector-ran line, and the reviewer's Input Contract says to judge
  in the platform's conventions.

Assisted-by: Claude Code

* fix: address PR review bot findings

- greptile: carry the capture's device selector through the
  state-changing verification commands (simctl appearance, adb uimode
  and font_scale); unqualified forms fail with several targets attached
- Copilot: align new-work.md's cross-reference with the actual heading
  (Verifying the build)
- Copilot: give the finish reviewer's Input Contract the native
  filename example new-work.md establishes (phone.png / tablet.png,
  suffixed per OS on adaptive)

Assisted-by: Claude Code

* fix: identify simulators by UDID, not display name

greptile follow-up: display names can collide across booted simulators,
so the capture and appearance commands now both key on the UDID from
simctl list devices booted.

Assisted-by: Claude Code
2026-08-09 15:09:55 -07:00
045865918a Held for review: agent placeholder substitution, reviewer recapture contract, base-directory script form (#544)
* Resolve {{scripts_path}} in the agent bodies Codex ships

Three code paths emit an agent body: the degraded fallback reference, the
.toml nested inside the skill for Codex, and the native agent file. Only the
nested .toml skipped placeholder substitution and rule-marker stripping, so
the codex and .agents dists shipped `node {{scripts_path}}/embed-prompt.mjs`
verbatim in the asset producer, and every caller had to substitute the token
itself at load time.

All three now render through renderAgentBody(), and the new regression test
asserts a runnable embed-prompt command on each emitted surface plus a
synthetic agent proving markers and placeholders resolve in the nested .toml.

Prepared by an AI agent (Claude Code) under pbakaus's instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Give the finish reviewer's screenshots one fixed address

The Input Contract asked for "desktop and mobile screenshot paths captured by
the parent" and named none, so each session invented a filename and the
verdict pass went looking for a recapture that was never written there. Two
reviewer passes burned on that in the eval runs.

The parent now captures and recaptures to .impeccable/review/desktop.png and
.impeccable/review/mobile.png, and the reviewer reads those two first,
treating a brief-named path as the fallback for a parent that wrote elsewhere.

Prepared by an AI agent (Claude Code) under pbakaus's instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Lead Setup with the base directory the runtime reports

The rendered claude and codex skills opened with
`node .claude/skills/impeccable/scripts/context.mjs`, a project-relative path
that resolves in this repo and in nothing a user installs: a personal or
plugin install puts the scripts outside the project entirely. The working form
was already in the text, parenthesized, after the one that fails.

Setup now leads with `node <skill-base-dir>/scripts/context.mjs` and says once
that the base directory resolves every scripts-path command in the skill and
its references, leaving the project-relative path as the fallback for runtimes
that report no base directory.

Prepared by an AI agent (Claude Code) under pbakaus's instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Answer the Copilot review: brittle model assertion, missing review dir

Assert that {{model}} resolved rather than that it resolved to "GPT", which
belongs to PROVIDER_PLACEHOLDERS and can change without touching what the test
guards. And have the parent create .impeccable/review/ when the harness does
not, so a fresh project's first capture has somewhere to land.

Prepared by an AI agent (Claude Code) under pbakaus's instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Make the review-screenshot contract directory-based, not web-viewport-named

Two amendments to the recapture contract from review feedback:

1. The canonical location is the directory .impeccable/review/, one file
   per captured viewport; desktop.png and mobile.png are the web case,
   not the contract. Baking web-viewport names into the reviewer's spec
   would have hardened a web assumption into paths that a native
   (ios/android/adaptive) build cannot honestly write.

2. Precedence restored to explicit-beats-convention: paths the calling
   brief names are authoritative when the files exist; the canonical
   directory is where the reviewer looks when the brief names none or a
   named path is missing. This avoids stale canonical files from an
   earlier run silently winning over fresh explicit paths. The observed
   failure (the verdict round inventing a round-stamped filename) stays
   fixed: recapture happens over the same files, and invented filenames
   are still called out as pointing at nothing.

Assisted-by: Claude Code

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 19:26:43 -07:00
490dcfd678 Fix #476: stop using JSON.stringify/double quotes as shell quoting in four exec sites (#533)
* Fix: use argv exec and single-quote escaping for the four #476 shell-injection sites

JSON.stringify and raw double-quote interpolation were used as shell quoting,
but /bin/sh still expands $(...), backticks, and ${} inside double quotes.

- is-generated.mjs / live.mjs runScript: switch execSync string commands to
  execFileSync argv form, which never invokes a shell. Closes the remote path
  where a source file named `$(...)` executes during the live-mode walk.
- skills.mjs hook command + hook-lib.mjs ignore-value suggestion: values that
  must stay shell strings now use POSIX single-quote escaping instead of
  JSON/double quotes. The doctor's hook-token parser learns the single-quoted
  absolute form so it keeps verifying user-level installs.

Adds regression tests for the single-quoted absolute hook form and the
single-quoted ignore-value suggestion. Verified end to end in a browser through
a real live-mode wrap walk against a hostile-named source file.

Prepared with AI assistance (Cursor) under maintainer instruction.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Test: lock in POSIX single-quoting for a $(...) absolute install path (#476)

Follow-up from security review: prove an install path embedding $(...) is
single-quoted in the written hook manifest, not double-quoted.

Prepared with AI assistance (Cursor) under maintainer instruction.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix: quote ignore-command args per platform so Windows cmd.exe keeps spaces (#533)

Greptile flagged that switching quoteCommandArg to POSIX single quotes fixed
$(...) injection on /bin/sh but regressed Windows cmd.exe, where single quotes
are literal, so a --file path containing spaces was split and the ignore scope
was stored malformed.

The suggested command runs on the same machine the hook fired on, so branch on
process.platform (the pattern skills.mjs already uses): single-quote on POSIX
for the #476 fix, and keep the original double-quote escaping on Windows so
that path's behavior is unchanged. Adds a regression test asserting both forms.

Prepared with AI assistance (Cursor) under maintainer instruction.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Test: prove the POSIX hook guard is inert under /bin/sh and Windows keeps double quotes (#533)

Greptile's probe could not reach the generated manifest, leaving the hook
command contract unverified. Convert that into committed proof:

- POSIX: install with a $(touch pwned) absolute path, then actually execute the
  generated guard under /bin/sh from a clean cwd and assert no marker file
  appears and the guard exits 0 (single-quoted substitution stays inert).
- Windows: drive copyProviderHooks as win32 in-process and assert the command
  keeps the double-quoted absolute path (usable when the install path has
  spaces; $(...) is inert on cmd.exe anyway).

Test-only; source quoting is unchanged.

Prepared with AI assistance (Cursor) under maintainer instruction.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 18:43:17 -07:00