diff --git a/scripts/lib/transformers/factory.js b/scripts/lib/transformers/factory.js index 6b92a6aa2..eb2fa4e02 100644 --- a/scripts/lib/transformers/factory.js +++ b/scripts/lib/transformers/factory.js @@ -13,6 +13,12 @@ import { import { SKILL_CATEGORIES, CATEGORY_ORDER } from '../skill-categories.js'; import { hooksJsonFor } from './hooks.js'; +// Preamble prepended to every generated degraded-mode fallback reference file. +// These files are single-sourced from skill/agents/ so a harness with no +// subagent capability runs each role inline from the same specialized text. +const DEGRADED_PREAMBLE = ` +This harness has no subagent capability, so you are running this role inline. Step fully out of the work you just finished, adopt only this file's instructions for the pass, and disclose the substitution in one line when you report. Where the text below addresses a parent agent, you are both parties: produce the full output contract first, then act on it yourself.`; + /** * Map from frontmatter field name to extraction spec. * @@ -258,6 +264,28 @@ export function createTransformer(config) { } } + // Generate degraded-mode fallback reference files from the shipped + // subagent definitions. Single-sourced from skill/agents/ so a harness + // with no subagent capability runs each role inline from the same + // specialized text. Role name = agent name minus the `impeccable-` + // prefix. These pass through the same provider-block compilation and + // placeholder replacement as ordinary reference files, so blocks + // and {{placeholders}} resolve identically. + if (skill.agents && skill.agents.length > 0) { + const degradedDir = path.join(skillDir, 'reference', 'degraded'); + ensureDir(degradedDir); + for (const agent of skill.agents) { + const role = agent.name.replace(/^impeccable-/, ''); + let body = compileProviderBlocks(agent.body, providerTags); + body = replacePlaceholders(body, placeholderKey, [], allSkillNames); + body = stripRuleMarkers(body); + body = body.replace(/\{\{scripts_path\}\}/g, scriptsPath); + const content = `${DEGRADED_PREAMBLE}\n\n${body.replace(/^\s+/, '')}`; + writeFile(path.join(degradedDir, `${role}.md`), content); + refCount++; + } + } + // Copy script files if (skill.scripts && skill.scripts.length > 0) { const scriptsOutDir = path.join(skillDir, 'scripts'); diff --git a/skill/reference/new-work.md b/skill/reference/new-work.md index 01d637722..d27c7d43d 100644 --- a/skill/reference/new-work.md +++ b/skill/reference/new-work.md @@ -102,6 +102,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and stop; the reviewer ran so that you do not re-open your own hunt. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and stop; the reviewer ran so that you do not re-open your own hunt. Do not run a second detector. -Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. +Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/skill/reference/visualize.md b/skill/reference/visualize.md index 5c7adc2fd..5cf78a476 100644 --- a/skill/reference/visualize.md +++ b/skill/reference/visualize.md @@ -33,6 +33,6 @@ Treat the comp as a north star, not something to trace. Do not rasterize core UI ## Produce only the assets the build needs -When clean raster ingredients are required and the harness runs subagents, use the shipped asset producer, `impeccable-asset-producer` (`impeccable_asset_producer` in codex): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Otherwise produce the minimum required assets in the current thread with whatever generation exists, the native tool or generate-image.mjs. +When clean raster ingredients are required and the harness runs subagents, use the shipped asset producer, `impeccable-asset-producer` (`impeccable_asset_producer` in codex): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Otherwise produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists, the native tool or generate-image.mjs. Return to [new-work.md](new-work.md) for the direction contract, implementation, and the finishing pass. diff --git a/tests/build.test.js b/tests/build.test.js index 819421a12..265fe9da4 100644 --- a/tests/build.test.js +++ b/tests/build.test.js @@ -386,3 +386,83 @@ describe('bundled skill scripts are self-contained', () => { expect(broken).toEqual([]); }); }); + +describe('degraded-mode fallback reference generation', () => { + const ROOT = process.cwd(); + const DEGRADED_TEST_DIR = path.join(ROOT, 'test-tmp-degraded'); + const DIST = path.join(DEGRADED_TEST_DIR, 'dist'); + + const readDegraded = (provider, configDir, role) => + fs.readFileSync( + path.join(DIST, provider, configDir, 'skills', 'impeccable', 'reference', 'degraded', `${role}.md`), + 'utf-8' + ); + + beforeEach(() => { + if (fs.existsSync(DEGRADED_TEST_DIR)) fs.rmSync(DEGRADED_TEST_DIR, { recursive: true, force: true }); + fs.mkdirSync(DEGRADED_TEST_DIR, { recursive: true }); + const { skills } = utils.readSourceFiles(ROOT); + transformers.transformClaudeCode(skills, DIST); + transformers.transformCodex(skills, DIST); + }); + + afterEach(() => { + if (fs.existsSync(DEGRADED_TEST_DIR)) fs.rmSync(DEGRADED_TEST_DIR, { recursive: true, force: true }); + }); + + test('a build emits reference/degraded/.md for every agent, prefix-stripped', () => { + const dir = path.join(DIST, 'codex', '.codex', 'skills', 'impeccable', 'reference', 'degraded'); + const files = fs.readdirSync(dir).sort(); + expect(files).toEqual([ + 'asset-producer.md', + 'documenter.md', + 'finish-reviewer.md', + 'manual-edit-applier.md', + ]); + }); + + test('finish-reviewer fallback opens with the preamble and carries a distinctive body phrase', () => { + const content = readDegraded('codex', '.codex', 'finish-reviewer'); + expect(content.startsWith('')) + .toBe(true); + expect(content).toContain('This harness has no subagent capability, so you are running this role inline.'); + // Distinctive phrase from the agent body proves the source body was inlined. + expect(content).toContain('material_fixes'); + }); + + test('generated fallbacks pass through provider-block compilation (codex keeps its block, others strip it)', () => { + // Standalone provider blocks are the shape compileProviderBlocks compiles. + // A synthetic agent proves the degraded path runs the same compilation as + // ordinary reference files, with the right provider tags per target. + const synthetic = { + name: 'impeccable', + description: 'synthetic', + body: 'Synthetic skill body.', + agents: [ + { + name: 'impeccable-synthetic', + body: 'Shared body line.\n\n\nCODEX_ONLY_MARKER for the codex target.\n\n\nMore shared body.', + }, + ], + }; + const synthDist = path.join(DEGRADED_TEST_DIR, 'synth'); + transformers.transformCodex([synthetic], synthDist); + transformers.transformClaudeCode([synthetic], synthDist); + const read = (provider, configDir) => + fs.readFileSync( + path.join(synthDist, provider, configDir, 'skills', 'impeccable', 'reference', 'degraded', 'synthetic.md'), + 'utf-8' + ); + const codex = read('codex', '.codex'); + const claude = read('claude-code', '.claude'); + expect(codex).toContain('CODEX_ONLY_MARKER'); + expect(claude).not.toContain('CODEX_ONLY_MARKER'); + // Both still carry the preamble and the shared body. + expect(codex.startsWith('