Sync generated provider output

This commit is contained in:
github-actions[bot]
2026-08-14 03:00:43 +00:00
parent 9d723f39df
commit 4f08ff3bfc
128 changed files with 2288 additions and 368 deletions
@@ -434,7 +434,7 @@ export function renderConceptSeed({
lead carrying kicker THE ROLL, with steer and re-roll, and let the user
lock one in; the world is already settled, so this choice is composition.
Visualize every dealt card: with image generation available and a
comp-led default (.impeccable/settings.json buildPath; the page toggle
comp-led default (.impeccable/config.json buildPath; the page toggle
handles the exception), declare a comp per card and generate after
serving, lead first; otherwise author each card's wireframe field (see
serve-question --schema) and the page draws the schematic. Carry the
+57 -15
View File
@@ -1013,14 +1013,22 @@ async function fetchLatestSkillVersion() {
}
}
// Two instructions used to sit in one directive: ask, and "if they agree, run
// it". Nothing gated the second on an answer, and the same sentence said to
// continue without waiting, so a run that could never establish agreement was
// still spelled out as the next command. The offer stays; the command leaves
// this turn entirely, because installing over the skill mid-session changes
// files the session is reading and only takes effect in the next one anyway.
function buildUpdateDirective(localVersion, latestVersion) {
return (
`UPDATE_AVAILABLE: A newer Impeccable skill is available ` +
`(installed v${localVersion}, latest v${latestVersion}). ` +
`Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` +
`Mention it once, in this form: "A newer Impeccable (v${latestVersion}) is available. ` +
`Update now? It runs \`npx impeccable update\`." ` +
`If they agree, run \`npx impeccable update\` (the update applies to the next session, not this one). ` +
`Either way, continue the current task without waiting, and do not raise this again.`
`Do not run \`npx impeccable update\` in this turn, whatever the user answers: it rewrites the skill files ` +
`this session is reading, and the update only takes effect in the next session, so there is nothing to gain now. ` +
`Run it in a later turn, only after the user has asked for it in their own words. ` +
`Continue the current task now without waiting, and do not raise this again.`
);
}
@@ -1142,7 +1150,7 @@ async function cli() {
parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists }));
appendDetectorFallback(parts, ctx);
appendImageGenDirective(parts);
appendBuildPathDirective(parts);
appendBuildPathDirective(parts, ctx);
appendAutonomyCounterDirective(parts);
appendSubagentAuthorizationDirective(parts);
if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) {
@@ -1162,7 +1170,7 @@ async function cli() {
parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists }));
appendDetectorFallback(parts, ctx);
appendImageGenDirective(parts);
appendBuildPathDirective(parts);
appendBuildPathDirective(parts, ctx);
appendAutonomyCounterDirective(parts);
appendSubagentAuthorizationDirective(parts);
if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) {
@@ -1271,17 +1279,51 @@ function automaticHookMode(ctx) {
}
// Build-path preference: a workflow setting (comp-led vs code-led), read
// here so every session starts knowing it without a file hunt. Absence
// stays silent; new-work's own default applies, and the decision page
// toggle can flip the value for a single session.
function appendBuildPathDirective(parts) {
try {
const settings = JSON.parse(fs.readFileSync(path.join(process.cwd(), '.impeccable', 'settings.json'), 'utf8'));
if (settings.buildPath === 'comp' || settings.buildPath === 'code') {
parts.push(`BUILD_PATH_DEFAULT: ${settings.buildPath} (from .impeccable/settings.json). Author direction and surface rounds with this as buildPath.value and toggle: true; a flip on the page binds that session only and is never written back to settings.`);
// Build-path preference: a workflow setting (comp-led vs code-led), read here
// so every session starts knowing it without a file hunt. It rides the unified
// config beside the hook and detector settings, and the gitignored local file
// wins, because whether a machine has an image tool is a property of that
// machine, not of the team's committed default. Absence stays silent;
// new-work's own default applies, and the decision page toggle can flip the
// value for a single session.
function readBuildPathAt(root) {
let value = null;
let source = null;
for (const name of ['config.json', 'config.local.json']) {
const raw = readJson(path.join(root, '.impeccable', name));
if (raw?.buildPath === 'comp' || raw?.buildPath === 'code') {
value = raw.buildPath;
source = `.impeccable/${name}`;
}
} catch { /* no settings file */ }
}
return value ? { value, source } : null;
}
// Roots in precedence order, nearest first: the resolved project decides, and
// the repo root is the fallback a monorepo commits once for every app in it.
// `checkBuildPathUnset` reads exactly these two, and the pair has to match:
// when they disagree the finding goes silent because a value exists while the
// directive never names it, which is the one combination nobody can debug.
//
// The invoking directory is deliberately not in the chain. With `--target`
// selecting another workspace, cwd is the caller's app, not the target's, and
// letting it rank above the repo root hands one workspace another's workflow.
// It stands in only when no project resolved at all.
function appendBuildPathDirective(parts, ctx) {
const roots = [...new Set(
[ctx?.projectRoot || process.cwd(), ctx?.repoRoot].filter(Boolean).map((root) => path.resolve(root)),
)];
for (const root of roots) {
const found = readBuildPathAt(root);
if (!found) continue;
// "Never written back" is scoped by the fact that this directive exists at
// all: it is emitted only where a value is already recorded, which is the
// case where a flip really is session-only. Saying so inline because the
// bare absolute reads as a rule that overrides new-work's one-time offer,
// which is exactly how the same wording misfired in serve-question.
parts.push(`BUILD_PATH_DEFAULT: ${found.value} (from ${found.source}). Author direction and surface rounds with this as buildPath.value and toggle: true; a flip on the page binds that session only and is never written back, because a default is already recorded here. New-work's one-time offer to record a flipped value applies only where no default exists, which is why you are not seeing this line on those projects.`);
return;
}
}
// Image generation availability: harness-native tools always win, but when the
@@ -33,6 +33,7 @@ import {
stampProductSchema,
} from './lib/artifact-schema.mjs';
import {
checkBuildPathUnset,
checkConfig,
checkDesignSidecar,
checkNativePlatformEvidence,
@@ -120,6 +121,7 @@ async function collect(cwd, targetOptions) {
...checkDesignDrift({ designPath: absDesignPath, projectRoot }),
...checkDesignCoverage({ design: ctx.design, designPath: ctx.designPath, parseDesignMd }),
...checkConfig({ projectRoot, repoRoot: ctx.repoRoot }),
...checkBuildPathUnset({ projectRoot, repoRoot: ctx.repoRoot, product: ctx.product }),
...checkDetectorIgnores({ projectRoot, knownRuleIds }),
...checkSurfaceBriefs({ candidates: ctx.surfaceBriefCandidates, projectRoot }),
...checkHookInstallation({
@@ -47,18 +47,33 @@ import {
// Top-level keys any reader honors: `hook` and `detector` subtrees (hook-lib's
// readConfig), `updateCheck` (context.mjs), `projectRoots` (context.mjs's
// monorepo resolution), plus `stalenessCheck` below. `$schema` and `version`
// are allowed as conventional metadata nobody reads.
// monorepo resolution), `buildPath` (context.mjs's build-path directive), plus
// `stalenessCheck` below. `$schema` and `version` are allowed as conventional
// metadata nobody reads.
const KNOWN_CONFIG_KEYS = new Set([
'hook',
'detector',
'updateCheck',
'stalenessCheck',
'projectRoots',
'buildPath',
'$schema',
'version',
]);
// The only two values context.mjs and new-work honor. A near miss reads as a
// working preference and silently rides the opposite path, so it is worth
// reporting rather than coercing.
const BUILD_PATH_VALUES = Object.freeze(['comp', 'code']);
// Evidence that this project does the kind of work `buildPath` governs. A
// project that only ever ran polish or audit has no use for the setting and
// should never be told it exists. Two stats, so Tier 1 can afford it.
const DIRECTION_WORK_PATHS = Object.freeze([
path.join('.impeccable', 'surfaces'),
path.join('.impeccable', 'mocks', 'decision'),
]);
// `detector` is a closed set, so a typo here is worth reporting. `hook` is not
// checked: it carries runtime settings from several writers and the false
// positive rate would outweigh the catch.
@@ -325,6 +340,20 @@ export function checkConfig({ projectRoot, repoRoot }) {
}));
}
if (Object.prototype.hasOwnProperty.call(raw, 'buildPath')
&& !BUILD_PATH_VALUES.includes(raw.buildPath)) {
findings.push(finding({
id: 'config-invalid-build-path',
artifact: 'config.json',
filePath: rel,
severity: 'mention',
summary: `${rel} sets \`buildPath\` to ${JSON.stringify(raw.buildPath)}, which nothing reads. `
+ `The values are ${BUILD_PATH_VALUES.map((value) => `\`${value}\``).join(' and ')}.`,
fix: 'Report the value. An unread `buildPath` does not fall back to the other path; '
+ 'it falls back to the default, so a project meaning `code` has been building comp-led.',
}));
}
const detector = raw.detector;
if (detector && typeof detector === 'object' && !Array.isArray(detector)) {
const unknownDetector = Object.keys(detector).filter((key) => !KNOWN_DETECTOR_KEYS.has(key));
@@ -345,6 +374,47 @@ export function checkConfig({ projectRoot, repoRoot }) {
return findings;
}
/**
* No recorded build-path preference on a project that plainly does visual
* direction work. Not drift in the usual sense: the setting is newer than the
* project, so every project that predates it lands here at once. That is why
* it is gated twice, on a product record and on evidence of the work the
* setting governs, and why it says the choice rather than assuming a harness
* can make it. Image generation is the real precondition and this module
* cannot see it: a harness-native image tool leaves no trace on disk, so the
* finding hands the question to the one reader that knows.
*/
export function checkBuildPathUnset({ projectRoot, repoRoot, product }) {
if (!projectRoot || !product) return [];
const roots = [...new Set([projectRoot, repoRoot].filter(Boolean).map((root) => path.resolve(root)))];
for (const root of roots) {
for (const name of ['config.json', 'config.local.json']) {
const raw = readJson(path.join(root, '.impeccable', name));
// Any declared value ends this, valid or not: an invalid one already has
// its own finding and two reports of one key is noise.
if (raw && Object.prototype.hasOwnProperty.call(raw, 'buildPath')) return [];
}
}
const evidence = DIRECTION_WORK_PATHS.filter((rel) => fs.existsSync(path.join(projectRoot, rel)));
if (!evidence.length) return [];
return [finding({
id: 'config-build-path-unset',
artifact: 'config.json',
filePath: '.impeccable/config.json',
severity: 'mention',
summary: 'This project has run visual direction work but records no `buildPath`, '
+ 'so every direction round takes the comp-first default without anyone having chosen it.',
fix: 'Only when image generation exists in your tool surface, offer the choice once: '
+ '**comp-first** (an image sets the bar before any code; bolder composition, slower) or '
+ '**code-first** (build directly; ambition carried by the direction contract; leaner, faster). '
+ 'Write the answer to `.impeccable/config.json` as `"buildPath": "comp"` or `"buildPath": "code"`, '
+ 'merging with the keys already there. Without image generation there is no choice to record: stay silent.',
})];
}
// ─── Surface briefs ────────────────────────────────────────────────────────
/**
@@ -446,6 +516,7 @@ export function collectBootFindings(ctx, extras = {}) {
projectRoot,
}),
...checkConfig({ projectRoot, repoRoot: ctx.repoRoot }),
...checkBuildPathUnset({ projectRoot, repoRoot: ctx.repoRoot, product: ctx.product }),
...checkSurfaceBriefs({ candidates: ctx.surfaceBriefCandidates, projectRoot }),
...(extras.projectRootPatterns
? checkProjectRoots({
@@ -161,8 +161,12 @@ function printAnswer(raw) {
console.log('FOLLOWUP OPEN: the table stays open and the page is showing a loading hand. Deliver the next round now with --update --key <key> --payload <file>, then collect it with --wait; never leave the page waiting on a round you have not sent.');
}
if (a.buildPath === 'comp' || a.buildPath === 'code') {
// The page never writes the flip itself, but "never write it" overstated
// that into a rule the agent then applied to new-work's one-time offer,
// which exists for exactly this case: a flip on a project that had no
// recorded default is the only moment the preference is ever asked for.
const origin = a.buildPathFlipped
? 'flipped on the page, so it binds this session only; never write it to settings'
? 'flipped on the page, so it binds this session only, and the page never writes it back; the sole exception is new-works one-time offer, on a project that had no recorded default at all, which asks after the round closes and writes the answer to .impeccable/config.json'
: 'the rounds recorded default';
console.log(`BUILD PATH: ${a.buildPath} (${origin}). ${a.buildPath === 'comp'
? 'Comp-led: the chosen cards comp is law; generate it before building when it does not exist yet, and the finish review audits the build against it.'
@@ -199,7 +203,7 @@ if (hasFlag('schema')) {
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' },
steer: true,
}, null, 2));
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdicts call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLES PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challengers system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the cards full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register <value> for the next round; offer the registers on direction rounds, and never pre-select one. buildPath rides the payload as { "value": "comp"|"code", "toggle": true }: the value is the recorded default (.impeccable/settings.json, or a PRODUCT.md standing commitment as fallback) and the toggle renders a footer switch whose flip binds that session only; the ANSWER then carries buildPath plus buildPathFlipped. On a code-led round each card still declares its comp path as a flip reserve: wireframes render, and a flip to comp makes --wait return once with BUILD PATH FLIPPED so you generate the comps into the declared slots while the round stays open; a flip back to code is free, and a comp that already landed stays as the critique reference. The toggle may only be offered when image generation exists: a harness with no image tool and no API key never sets toggle: true, so the choice never renders where comps cannot be made, and code-led simply rides as the untoggleable value. followup: true keeps the table open after a pick for a second round via --update; send the next payload immediately, the page is waiting on it.');
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdicts call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLES PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challengers system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the cards full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register <value> for the next round; offer the registers on direction rounds, and never pre-select one. buildPath rides the payload as { "value": "comp"|"code", "toggle": true }: the value is the recorded default (.impeccable/config.json buildPath, or .impeccable/config.local.json where one machine differs) and the toggle renders a footer switch whose flip binds that session only; the ANSWER then carries buildPath plus buildPathFlipped. On a code-led round each card still declares its comp path as a flip reserve: wireframes render, and a flip to comp makes --wait return once with BUILD PATH FLIPPED so you generate the comps into the declared slots while the round stays open; a flip back to code is free, and a comp that already landed stays as the critique reference. The toggle may only be offered when image generation exists: a harness with no image tool and no API key never sets toggle: true, so the choice never renders where comps cannot be made, and code-led simply rides as the untoggleable value. followup: true keeps the table open after a pick for a second round via --update; send the next payload immediately, the page is waiting on it.');
process.exit(0);
}