mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
* feat(design-html): craft section and free craft check, adapted from Elaya design skills (GSTA-92) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(GSTA-92): restore the full CHANGELOG under the Unreleased entry; drop a $-digit literal from craft.md; count only craft-check executions in design-html-slop-gate Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(design-shotgun,design-html): carry the chosen archetype into approved.json; persist accepted craft exceptions in finalized.json (GSTA-92 phase 2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(design-review,plan-design-review): craft audit carved on every host, Persuade plan checks, craft tells as catalog entries (GSTA-92 phase 2) Size pins and caps raised to measured sizes only: design-review (13 hosts), plan-design-review (12), design-consultation (9), design-consultation maxSizeRatio, and the new design-review parity guard. Garry 2026-10-25 "Do it" (GSTA-92). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * refactor(design-catalog): craft tells are checker-only entries; trim design-review and plan-design-review growth to the planned size (GSTA-92) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(touchfiles): design-html-slop-gate selects on the craft check it runs (GSTA-92) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(touchfiles): design-review and plan-design-review cases select on the craft resolver; slop gate on the catalog (GSTA-92) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(placeholders): allow the craft audit page path placeholder (GSTA-92) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (GSTA-92) A malformed or missing --accepted exceptions file now gets the full craft check with nothing accepted, after a warning, still exiting 2. Before, it printed nothing, and the craft section treats a missing CRAFT_RESULT as 'check unavailable', so one bad finalized.json switched the gate off (found by both adversarial passes). The header comment and the craft section now document the accepted-count suffix on CRAFT_RESULT. Tests cover duplicate carried-forward exception ids and the slop-gate touchfiles selection. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: scope --accepted to the same page; record current counts (GSTA-92) The craft section passed --accepted whenever a finalized.json sat beside the page, so a same-day Start fresh build inherited the old page's exceptions. It now passes them only when evolving or rerunning a page it finalized, records the count the check reports now, and writes [] when nothing was accepted instead of the template's placeholder entry. A test pins exit 2 for a clean page with a broken exceptions file. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: note the archetype handoff in the shotgun and design-html loops (GSTA-92) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: craft audit capture, exceptions and trigger from /ship review (GSTA-92) - craft-audit: no CRAFT_RESULT line means unavailable (exit 2 after a malformed --accepted record still printed findings); --accepted only for a /design-html finalized page, never for needs-proof or lorem; never add [Needs proof] markers to a live page; Aside capture via pg.evaluate; the saved raw DOM is deleted in the checking block and never reported - manifest trigger covers an explicit craft audit (skeleton -6 bytes; pins lowered to measured size, union ratio back under its cap) - autoFixEntries reads RENDERED; DESIGN_CRAFT_RULE needs exactly one lead - tests: checkerOnly contract, resolver throw paths, Persuade checks in both skills, resolver touchfile selection, exception state cells - CHANGELOG: host counts and the audit's new rules Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: craft audit live-page limits, claim handling and guards (GSTA-92) - craft-audit: linked CSS/JS is unchecked on a saved page (judge by eye); a mount-node shell is framework source; --accepted takes that screen's finalized.json or none; on a live page a claim without proof is filed and asked about, never marked or cut; the saved page is removed whatever the exit code; shorter fix-order and Tailwind prose keeps the union under cap - tests: exact-lead red cases, autoFixEntries in the checkerOnly union, the audit's outcome/exception/session-data clauses; carve guard checks the session-data rule and records measured sizes (skeleton cap 137_767) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: craft check skips mount-node shells and script bodies; audit capture order (GSTA-92) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * revert: craft checker back to bed645a's reviewed behavior; textless mount-node shell judged by eye (GSTA-92) cycle-cap exception (GSTA-92 lane, 2026-10-25): reverted 216987e's checker half to bed645a's reviewed behavior; byte-neutral prose reword; no new logic Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: craft audit judges any mount-node shell by eye; pin the clause (GSTA-92) second cycle-cap exception: GStack Minder's decision under Garry's 'do it' on phase 2 (GSTA-92, 2026-10-25): one deleted word plus one test Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: craft resolver placeholders, carved-on-every-host list, design-craft resolver (GSTA-92 /ship docs audit) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes after main merge: claim rule covers repo files, craft temp var, guard tests (GSTA-92) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: craft audit files failed Persuade checks as deferred High, judges source tells by eye; stderr pin (GSTA-92) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(design-review): never cut a claim a rule or printed fix says to drop Pass R3 (native adversarial, Medium): the CRAFT_CHECK fix text for round-number and needs-proof says to drop or remove the claim, which could lead Phase 8 to delete a real claim. The audit sentence now covers both a rule and the printed fix: defer the claim and ask for the proof. Byte-neutral (80 -> 80); design-review/SKILL.md 137,767, union 145,037, unchanged. The test pin moves with the sentence; no carve-guard mustContain held this sentence. third cycle-cap exception (Captain, 2026-10-26): zero-byte reword closing the content-deletion path; test pin and carve-guard mustContain moved Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * design-craft: reconcile ids with the design lint (layout-transition is static-tier; legacy transition-all exceptions map to it) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
151 lines
8.5 KiB
TypeScript
151 lines
8.5 KiB
TypeScript
/**
|
|
* C4 (#2777): ship, plan-ceo-review, office-hours and design-review are carved on every host, and no
|
|
* generated SKILL.md exceeds 160,000 UTF-8 bytes on any host or model overlay
|
|
* setup can select. Carved sections on external hosts are pointed at relative
|
|
* to the installed skill directory: `$GSTACK_ROOT/<skill>/sections/...` does
|
|
* not exist in external runtime roots (Codex's holds bin, lib, browse, review).
|
|
*/
|
|
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
import { spawnSync } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { ALL_HOST_CONFIGS } from '../hosts';
|
|
import { ALL_MODEL_NAMES } from '../scripts/models';
|
|
import { runGeneration } from '../scripts/gen-skill-docs';
|
|
import { sectionPath, SKILL_BYTE_CEILING, usesLazySections } from '../scripts/resolvers/sections';
|
|
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
|
|
import { prepareMethodology } from '../bin/gstack-autoplan-snapshot';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const EXTERNAL = ALL_HOST_CONFIGS.filter(c => c.name !== 'claude');
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-external-carve-'));
|
|
const context = (host: string, skillName: string): TemplateContext => ({ host, skillName, tmplPath: '', paths: HOST_PATHS[host] });
|
|
const skillDir = (host: (typeof ALL_HOST_CONFIGS)[number], out: string, skill: string) =>
|
|
host.name === 'claude' ? path.join(out, skill) : path.join(out, host.hostSubdir, 'skills', `gstack-${skill}`);
|
|
|
|
function skillFiles(out: string): string[] {
|
|
const files: string[] = [];
|
|
const walk = (dir: string) => {
|
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (e.isDirectory()) { if (e.name !== 'node_modules') walk(path.join(dir, e.name)); }
|
|
else if (e.name === 'SKILL.md') files.push(path.join(dir, e.name));
|
|
}
|
|
};
|
|
walk(out);
|
|
return files;
|
|
}
|
|
|
|
const renders: Record<string, string> = {};
|
|
beforeAll(async () => {
|
|
renders.all = path.join(tmp, 'all');
|
|
const all = await runGeneration({ host: 'all', outputRoot: renders.all });
|
|
expect(all.diagnostics.filter(d => d.kind === 'error' || d.kind === 'warning').map(d => d.message)).toEqual([]);
|
|
for (const model of ALL_MODEL_NAMES) {
|
|
renders[model] = path.join(tmp, `codex-${model}`);
|
|
const r = await runGeneration({ host: 'codex', model, outputRoot: renders[model] });
|
|
expect(r.diagnostics.filter(d => d.kind === 'error').map(d => d.message)).toEqual([]);
|
|
}
|
|
}, 120_000);
|
|
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
|
|
describe('C4: external section pointers', () => {
|
|
for (const config of EXTERNAL) {
|
|
test(`${config.name}: ship and plan-ceo-review point relative to the installed skill directory`, () => {
|
|
expect(sectionPath(context(config.name, 'ship'), 'ship', 'tests'))
|
|
.toBe('`sections/tests.md` relative to the installed `gstack-ship` SKILL.md directory');
|
|
expect(sectionPath(context(config.name, 'plan-ceo-review'), 'plan-ceo-review', 'review-sections'))
|
|
.toBe('`sections/review-sections.md` relative to the installed `gstack-plan-ceo-review` SKILL.md directory');
|
|
expect(sectionPath(context(config.name, 'ship'), 'ship', 'tests')).not.toContain('$GSTACK_ROOT');
|
|
});
|
|
}
|
|
|
|
test('claude keeps its global-root pointer (bytes unchanged)', () => {
|
|
expect(sectionPath(context('claude', 'ship'), 'ship', 'tests')).toBe('`~/.claude/skills/gstack/ship/sections/tests.md`');
|
|
});
|
|
});
|
|
|
|
describe('C4: ship, plan-ceo-review, office-hours and design-review are carved on every external host', () => {
|
|
for (const config of EXTERNAL) {
|
|
test(`${config.name}: every pointer in the carved skill resolves to a generated section file`, () => {
|
|
for (const skill of ['ship', 'plan-ceo-review', 'office-hours', 'design-review']) {
|
|
expect(usesLazySections(config.name, skill)).toBe(true);
|
|
const dir = skillDir(config, renders.all, skill);
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, skill, 'sections', 'manifest.json'), 'utf8')) as { sections: Array<{ file: string }> };
|
|
const text = [fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf8'),
|
|
...manifest.sections.map(s => fs.readFileSync(path.join(dir, 'sections', s.file), 'utf8'))].join('\n');
|
|
expect(text).not.toMatch(new RegExp(`\\$GSTACK_ROOT/${skill}/sections/`));
|
|
const pointers = [...text.matchAll(new RegExp(`\`sections/([\\w.-]+)\` relative to the installed \`gstack-${skill}\` SKILL\\.md directory`, 'g'))].map(m => m[1]);
|
|
expect(pointers.length).toBeGreaterThan(0);
|
|
for (const file of pointers) expect(fs.existsSync(path.join(dir, 'sections', file)), `${skill}/sections/${file}`).toBe(true);
|
|
expect(fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf8')).toContain('## Section index');
|
|
}
|
|
});
|
|
}
|
|
|
|
test('no external render points at <root>/<skill>/sections/ (runtime roots have no section trees)', () => {
|
|
const offenders: string[] = [];
|
|
for (const config of EXTERNAL) {
|
|
for (const file of skillFiles(path.join(renders.all, config.hostSubdir))) {
|
|
const hits = fs.readFileSync(file, 'utf8').match(/(?:\$GSTACK_ROOT|~\/[\w./-]*skills\/gstack)\/[a-z0-9-]+\/sections\/[\w.-]+/g) ?? [];
|
|
offenders.push(...hits.map(hit => `${path.relative(renders.all, file)}: ${hit}`));
|
|
}
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
const codex = ALL_HOST_CONFIGS.find(c => c.name === 'codex')!;
|
|
expect(fs.readFileSync(path.join(skillDir(codex, renders.all, 'plan-eng-review'), 'SKILL.md'), 'utf8'))
|
|
.toContain('Read the `review-sections` section inlined in this SKILL.md');
|
|
const html = fs.readFileSync(path.join(skillDir(codex, renders.all, 'design-html'), 'SKILL.md'), 'utf8');
|
|
expect(html).not.toContain('detector-install-offer.md');
|
|
expect(html).toContain('the user has never answered this. Ask now');
|
|
});
|
|
|
|
test('codex: the autoplan methodology loader accepts the carved plan-ceo-review install', () => {
|
|
const codex = ALL_HOST_CONFIGS.find(c => c.name === 'codex')!;
|
|
const restore = path.join(tmp, 'restore.md');
|
|
fs.writeFileSync(restore, '# plan\n');
|
|
const prepared = prepareMethodology('ceo', path.join(skillDir(codex, renders.all, 'plan-ceo-review'), 'SKILL.md'), restore);
|
|
const methodology = fs.readFileSync(prepared.methodologyPath, 'utf8');
|
|
expect(methodology).toContain('sections/review-sections.md');
|
|
expect(methodology).toContain('## Review Sections');
|
|
});
|
|
});
|
|
|
|
describe('C4: UTF-8 byte gate (every host, every Codex model overlay)', () => {
|
|
test(`no generated SKILL.md exceeds ${SKILL_BYTE_CEILING} bytes`, () => {
|
|
const over: string[] = [];
|
|
let checked = 0;
|
|
for (const [name, out] of Object.entries(renders)) {
|
|
for (const file of skillFiles(out)) {
|
|
checked++;
|
|
const bytes = fs.statSync(file).size;
|
|
if (bytes > SKILL_BYTE_CEILING) over.push(`${name}: ${path.relative(out, file)} is ${bytes} bytes`);
|
|
}
|
|
}
|
|
expect(checked).toBeGreaterThan(500);
|
|
expect(over).toEqual([]);
|
|
});
|
|
|
|
test('the generator fails an oversized render in repo mode and only warns under --install-root', () => {
|
|
const fixture = path.join(tmp, 'oversized');
|
|
fs.mkdirSync(fixture);
|
|
for (const entry of fs.readdirSync(ROOT)) {
|
|
if (['scripts', 'retro', '.git'].includes(entry)) continue;
|
|
fs.symlinkSync(path.join(ROOT, entry), path.join(fixture, entry));
|
|
}
|
|
fs.cpSync(path.join(ROOT, 'scripts'), path.join(fixture, 'scripts'), { recursive: true });
|
|
fs.cpSync(path.join(ROOT, 'retro'), path.join(fixture, 'retro'), { recursive: true });
|
|
const tmpl = path.join(fixture, 'retro', 'SKILL.md.tmpl');
|
|
fs.appendFileSync(tmpl, `\n${`${'Padding for the oversized-render fixture. '.repeat(20)}\n`.repeat(250)}`);
|
|
const gen = (...args: string[]) => spawnSync(process.execPath, ['scripts/gen-skill-docs.ts', '--host', 'codex', ...args],
|
|
{ cwd: fixture, encoding: 'utf8', timeout: 60_000 });
|
|
const repo = gen('--out-dir', path.join(fixture, 'out-repo'));
|
|
expect(repo.status).toBe(1);
|
|
expect(repo.stderr).toMatch(/codex\/gstack-retro\/SKILL\.md is \d+ bytes, over the 160,000-byte limit\. Fix: carve sections with usesLazySections\(\) for this skill\./);
|
|
const install = gen('--out-dir', path.join(fixture, 'out-install'), '--install-root', '/opt/gstack');
|
|
expect(install.status, install.stderr).toBe(0);
|
|
expect(install.stderr).toContain('over the 160,000-byte limit');
|
|
expect(fs.existsSync(path.join(fixture, 'out-install', '.agents', 'skills', 'gstack-retro', 'SKILL.md'))).toBe(true);
|
|
});
|
|
});
|