     1→import { describe, test, expect } from 'bun:test';
     2→import { validateSkill, extractRemoteSlugPatterns, extractWeightsFromTable } from './helpers/skill-parser';
     3→import { ALL_COMMANDS, COMMAND_DESCRIPTIONS, READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from '../browse/src/commands';
     4→import { SNAPSHOT_FLAGS } from '../browse/src/snapshot';
     5→import * as fs from 'fs';
     6→import * as path from 'path';
     7→
     8→const ROOT = path.resolve(import.meta.dir, '..');
     9→
    10→describe('SKILL.md command validation', () => {
    11→  test('all $B commands in SKILL.md are valid browse commands', () => {
    12→    const result = validateSkill(path.join(ROOT, 'SKILL.md'));
    13→    expect(result.invalid).toHaveLength(0);
    14→    expect(result.valid.length).toBeGreaterThan(0);
    15→  });
    16→
    17→  test('all snapshot flags in SKILL.md are valid', () => {
    18→    const result = validateSkill(path.join(ROOT, 'SKILL.md'));
    19→    expect(result.snapshotFlagErrors).toHaveLength(0);
    20→  });
    21→
    22→  test('all $B commands in browse/SKILL.md are valid browse commands', () => {
    23→    const result = validateSkill(path.join(ROOT, 'browse', 'SKILL.md'));
    24→    expect(result.invalid).toHaveLength(0);
    25→    expect(result.valid.length).toBeGreaterThan(0);
    26→  });
    27→
    28→  test('all snapshot flags in browse/SKILL.md are valid', () => {
    29→    const result = validateSkill(path.join(ROOT, 'browse', 'SKILL.md'));
    30→    expect(result.snapshotFlagErrors).toHaveLength(0);
    31→  });
    32→
    33→  test('all $B commands in qa/SKILL.md are valid browse commands', () => {
    34→    const qaSkill = path.join(ROOT, 'qa', 'SKILL.md');
    35→    if (!fs.existsSync(qaSkill)) return; // skip if missing
    36→    const result = validateSkill(qaSkill);
    37→    expect(result.invalid).toHaveLength(0);
    38→  });
    39→
    40→  test('all snapshot flags in qa/SKILL.md are valid', () => {
    41→    const qaSkill = path.join(ROOT, 'qa', 'SKILL.md');
    42→    if (!fs.existsSync(qaSkill)) return;
    43→    const result = validateSkill(qaSkill);
    44→    expect(result.snapshotFlagErrors).toHaveLength(0);
    45→  });
    46→
    47→  test('all $B commands in qa-only/SKILL.md are valid browse commands', () => {
    48→    const qaOnlySkill = path.join(ROOT, 'qa-only', 'SKILL.md');
    49→    if (!fs.existsSync(qaOnlySkill)) return;
    50→    const result = validateSkill(qaOnlySkill);
    51→    expect(result.invalid).toHaveLength(0);
    52→  });
    53→
    54→  test('all snapshot flags in qa-only/SKILL.md are valid', () => {
    55→    const qaOnlySkill = path.join(ROOT, 'qa-only', 'SKILL.md');
    56→    if (!fs.existsSync(qaOnlySkill)) return;
    57→    const result = validateSkill(qaOnlySkill);
    58→    expect(result.snapshotFlagErrors).toHaveLength(0);
    59→  });
    60→
    61→  test('all $B commands in plan-design-review/SKILL.md are valid browse commands', () => {
    62→    const skill = path.join(ROOT, 'plan-design-review', 'SKILL.md');
    63→    if (!fs.existsSync(skill)) return;
    64→    const result = validateSkill(skill);
    65→    expect(result.invalid).toHaveLength(0);
    66→  });
    67→
    68→  test('all snapshot flags in plan-design-review/SKILL.md are valid', () => {
    69→    const skill = path.join(ROOT, 'plan-design-review', 'SKILL.md');
    70→    if (!fs.existsSync(skill)) return;
    71→    const result = validateSkill(skill);
    72→    expect(result.snapshotFlagErrors).toHaveLength(0);
    73→  });
    74→
    75→  test('all $B commands in design-review/SKILL.md are valid browse commands', () => {
    76→    const skill = path.join(ROOT, 'design-review', 'SKILL.md');
    77→    if (!fs.existsSync(skill)) return;
    78→    const result = validateSkill(skill);
    79→    expect(result.invalid).toHaveLength(0);
    80→  });
    81→
    82→  test('all snapshot flags in design-review/SKILL.md are valid', () => {
    83→    const skill = path.join(ROOT, 'design-review', 'SKILL.md');
    84→    if (!fs.existsSync(skill)) return;
    85→    const result = validateSkill(skill);
    86→    expect(result.snapshotFlagErrors).toHaveLength(0);
    87→  });
    88→
    89→  test('all $B commands in design-consultation/SKILL.md are valid browse commands', () => {
    90→    const skill = path.join(ROOT, 'design-consultation', 'SKILL.md');
    91→    if (!fs.existsSync(skill)) return;
    92→    const result = validateSkill(skill);
    93→    expect(result.invalid).toHaveLength(0);
    94→  });
    95→
    96→  test('all snapshot flags in design-consultation/SKILL.md are valid', () => {
    97→    const skill = path.join(ROOT, 'design-consultation', 'SKILL.md');
    98→    if (!fs.existsSync(skill)) return;
    99→    const result = validateSkill(skill);
   100→    expect(result.snapshotFlagErrors).toHaveLength(0);
   101→  });
   102→});
   103→
   104→describe('Command registry consistency', () => {
   105→  test('COMMAND_DESCRIPTIONS covers all commands in sets', () => {
   106→    const allCmds = new Set([...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS]);
   107→    const descKeys = new Set(Object.keys(COMMAND_DESCRIPTIONS));
   108→    for (const cmd of allCmds) {
   109→      expect(descKeys.has(cmd)).toBe(true);
   110→    }
   111→  });
   112→
   113→  test('COMMAND_DESCRIPTIONS has no extra commands not in sets', () => {
   114→    const allCmds = new Set([...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS]);
   115→    for (const key of Object.keys(COMMAND_DESCRIPTIONS)) {
   116→      expect(allCmds.has(key)).toBe(true);
   117→    }
   118→  });
   119→
   120→  test('ALL_COMMANDS matches union of all sets', () => {
   121→    const union = new Set([...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS]);
   122→    expect(ALL_COMMANDS.size).toBe(union.size);
   123→    for (const cmd of union) {
   124→      expect(ALL_COMMANDS.has(cmd)).toBe(true);
   125→    }
   126→  });
   127→
   128→  test('SNAPSHOT_FLAGS option keys are valid SnapshotOptions fields', () => {
   129→    const validKeys = new Set([
   130→      'interactive', 'compact', 'depth', 'selector',
   131→      'diff', 'annotate', 'outputPath', 'cursorInteractive',
   132→    ]);
   133→    for (const flag of SNAPSHOT_FLAGS) {
   134→      expect(validKeys.has(flag.optionKey)).toBe(true);
   135→    }
   136→  });
   137→});
   138→
   139→describe('Usage string consistency', () => {
   140→  // Normalize a usage string to its structural skeleton for comparison.
   141→  // Replaces <param-names> with <>, [optional] with [], strips parenthetical hints.
   142→  // This catches format mismatches (e.g., <name>:<value> vs <name> <value>)
   143→  // without tripping on abbreviation differences (e.g., <sel> vs <selector>).
   144→  function skeleton(usage: string): string {
   145→    return usage
   146→      .replace(/\(.*?\)/g, '')        // strip parenthetical hints like (e.g., Enter, Tab)
   147→      .replace(/<[^>]*>/g, '<>')      // normalize <param-name> → <>
   148→      .replace(/\[[^\]]*\]/g, '[]')   // normalize [optional] → []
   149→      .replace(/\s+/g, ' ')           // collapse whitespace
   150→      .trim();
   151→  }
   152→
   153→  // Cross-check Usage: patterns in implementation against COMMAND_DESCRIPTIONS
   154→  test('implementation Usage: structural format matches COMMAND_DESCRIPTIONS', () => {
   155→    const implFiles = [
   156→      path.join(ROOT, 'browse', 'src', 'write-commands.ts'),
   157→      path.join(ROOT, 'browse', 'src', 'read-commands.ts'),
   158→      path.join(ROOT, 'browse', 'src', 'meta-commands.ts'),
   159→    ];
   160→
   161→    // Extract "Usage: browse <pattern>" from throw new Error(...) calls
   162→    const usagePattern = /throw new Error\(['"`]Usage:\s*browse\s+(.+?)['"`]\)/g;
   163→    const implUsages = new Map<string, string>();
   164→
   165→    for (const file of implFiles) {
   166→      const content = fs.readFileSync(file, 'utf-8');
   167→      let match;
   168→      while ((match = usagePattern.exec(content)) !== null) {
   169→        const usage = match[1].split('\\n')[0].trim();
   170→        const cmd = usage.split(/\s/)[0];
   171→        implUsages.set(cmd, usage);
   172→      }
   173→    }
   174→
   175→    // Compare structural skeletons
   176→    const mismatches: string[] = [];
   177→    for (const [cmd, implUsage] of implUsages) {
   178→      const desc = COMMAND_DESCRIPTIONS[cmd];
   179→      if (!desc) continue;
   180→      if (!desc.usage) continue;
   181→      const descSkel = skeleton(desc.usage);
   182→      const implSkel = skeleton(implUsage);
   183→      if (descSkel !== implSkel) {
   184→        mismatches.push(`${cmd}: docs "${desc.usage}" (${descSkel}) vs impl "${implUsage}" (${implSkel})`);
   185→      }
   186→    }
   187→
   188→    expect(mismatches).toEqual([]);
   189→  });
   190→});
   191→
   192→describe('Generated SKILL.md freshness', () => {
   193→  test('no unresolved {{placeholders}} in generated SKILL.md', () => {
   194→    const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
   195→    const unresolved = content.match(/\{\{\w+\}\}/g);
   196→    expect(unresolved).toBeNull();
   197→  });
   198→
   199→  test('no unresolved {{placeholders}} in generated browse/SKILL.md', () => {
   200→    const content = fs.readFileSync(path.join(ROOT, 'browse', 'SKILL.md'), 'utf-8');
   201→    const unresolved = content.match(/\{\{\w+\}\}/g);
   202→    expect(unresolved).toBeNull();
   203→  });
   204→
   205→  test('generated SKILL.md has AUTO-GENERATED header', () => {
   206→    const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
   207→    expect(content).toContain('AUTO-GENERATED');
   208→  });
   209→});
   210→
   211→// --- Update check preamble validation ---
   212→
   213→describe('Update check preamble', () => {
   214→  const skillsWithUpdateCheck = [
   215→    'SKILL.md', 'browse/SKILL.md', 'qa/SKILL.md',
   216→    'qa-only/SKILL.md',
   217→    'setup-browser-cookies/SKILL.md',
   218→    'ship/SKILL.md', 'review/SKILL.md',
   219→    'plan-ceo-review/SKILL.md', 'plan-eng-review/SKILL.md',
   220→    'retro/SKILL.md',
   221→    'plan-design-review/SKILL.md',
   222→    'design-review/SKILL.md',
   223→    'design-consultation/SKILL.md',
   224→    'document-release/SKILL.md',
   225→  ];
   226→
   227→  for (const skill of skillsWithUpdateCheck) {
   228→    test(`${skill} update check line ends with || true`, () => {
   229→      const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
   230→      // The second line of the bash block must end with || true
   231→      // to avoid exit code 1 when _UPD is empty (up to date)
   232→      const match = content.match(/\[ -n "\$_UPD" \].*$/m);
   233→      expect(match).not.toBeNull();
   234→      expect(match![0]).toContain('|| true');
   235→    });
   236→  }
   237→
   238→  test('all skills with update check are generated from .tmpl', () => {
   239→    for (const skill of skillsWithUpdateCheck) {
   240→      const tmplPath = path.join(ROOT, skill + '.tmpl');
   241→      expect(fs.existsSync(tmplPath)).toBe(true);
   242→    }
   243→  });
   244→
   245→  test('update check bash block exits 0 when up to date', () => {
   246→    // Simulate the exact preamble command from SKILL.md
   247→    const result = Bun.spawnSync(['bash', '-c',
   248→      '_UPD=$(echo "" || true); [ -n "$_UPD" ] && echo "$_UPD" || true'
   249→    ], { stdout: 'pipe', stderr: 'pipe' });
   250→    expect(result.exitCode).toBe(0);
   251→  });
   252→
   253→  test('update check bash block exits 0 when upgrade available', () => {
   254→    const result = Bun.spawnSync(['bash', '-c',
   255→      '_UPD=$(echo "UPGRADE_AVAILABLE 0.3.3 0.4.0" || true); [ -n "$_UPD" ] && echo "$_UPD" || true'
   256→    ], { stdout: 'pipe', stderr: 'pipe' });
   257→    expect(result.exitCode).toBe(0);
   258→    expect(result.stdout.toString().trim()).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
   259→  });
   260→});
   261→
   262→// --- Part 7: Cross-skill path consistency (A1) ---
   263→
   264→describe('Cross-skill path consistency', () => {
   265→  test('REMOTE_SLUG derivation pattern is identical across files that use it', () => {
   266→    const patterns = extractRemoteSlugPatterns(ROOT, ['qa', 'review']);
   267→    const allPatterns: string[] = [];
   268→
   269→    for (const [, filePatterns] of patterns) {
   270→      allPatterns.push(...filePatterns);
   271→    }
   272→
   273→    // Should find at least 2 occurrences (qa/SKILL.md + review/greptile-triage.md)
   274→    expect(allPatterns.length).toBeGreaterThanOrEqual(2);
   275→
   276→    // All occurrences must be character-for-character identical
   277→    const unique = new Set(allPatterns);
   278→    if (unique.size > 1) {
   279→      const variants = Array.from(unique);
   280→      throw new Error(
   281→        `REMOTE_SLUG pattern differs across files:\n` +
   282→        variants.map((v, i) => `  ${i + 1}: ${v}`).join('\n')
   283→      );
   284→    }
   285→  });
   286→
   287→  test('all greptile-history write references specify both per-project and global paths', () => {
   288→    const filesToCheck = [
   289→      'review/SKILL.md',
   290→      'ship/SKILL.md',
   291→      'review/greptile-triage.md',
   292→    ];
   293→
   294→    for (const file of filesToCheck) {
   295→      const filePath = path.join(ROOT, file);
   296→      if (!fs.existsSync(filePath)) continue;
   297→      const content = fs.readFileSync(filePath, 'utf-8');
   298→
   299→      const hasBoth = (content.includes('per-project') && content.includes('global')) ||
   300→        (content.includes('$REMOTE_SLUG/greptile-history') && content.includes('~/.gstack/greptile-history'));
   301→
   302→      expect(hasBoth).toBe(true);
   303→    }
   304→  });
   305→
   306→  test('greptile-triage.md contains both project and global history paths', () => {
   307→    const content = fs.readFileSync(path.join(ROOT, 'review', 'greptile-triage.md'), 'utf-8');
   308→    expect(content).toContain('$REMOTE_SLUG/greptile-history.md');
   309→    expect(content).toContain('~/.gstack/greptile-history.md');
   310→  });
   311→
   312→  test('retro/SKILL.md reads global greptile-history (not per-project)', () => {
   313→    const content = fs.readFileSync(path.join(ROOT, 'retro', 'SKILL.md'), 'utf-8');
   314→    expect(content).toContain('~/.gstack/greptile-history.md');
   315→    // Should NOT reference per-project path for reads
   316→    expect(content).not.toContain('$REMOTE_SLUG/greptile-history.md');
   317→  });
   318→});
   319→
   320→// --- Part 7: QA skill structure validation (A2) ---
   321→
   322→describe('QA skill structure validation', () => {
   323→  const qaContent = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   324→
   325→  test('qa/SKILL.md has all 11 phases', () => {
   326→    const phases = [
   327→      'Phase 1', 'Initialize',
   328→      'Phase 2', 'Authenticate',
   329→      'Phase 3', 'Orient',
   330→      'Phase 4', 'Explore',
   331→      'Phase 5', 'Document',
   332→      'Phase 6', 'Wrap Up',
   333→      'Phase 7', 'Triage',
   334→      'Phase 8', 'Fix Loop',
   335→      'Phase 9', 'Final QA',
   336→      'Phase 10', 'Report',
   337→      'Phase 11', 'TODOS',
   338→    ];
   339→    for (const phase of phases) {
   340→      expect(qaContent).toContain(phase);
   341→    }
   342→  });
   343→
   344→  test('has all four QA modes defined', () => {
   345→    const modes = [
   346→      'Diff-aware',
   347→      'Full',
   348→      'Quick',
   349→      'Regression',
   350→    ];
   351→    for (const mode of modes) {
   352→      expect(qaContent).toContain(mode);
   353→    }
   354→
   355→    // Mode triggers/flags
   356→    expect(qaContent).toContain('--quick');
   357→    expect(qaContent).toContain('--regression');
   358→  });
   359→
   360→  test('has all three tiers defined', () => {
   361→    const tiers = ['Quick', 'Standard', 'Exhaustive'];
   362→    for (const tier of tiers) {
   363→      expect(qaContent).toContain(tier);
   364→    }
   365→  });
   366→
   367→  test('health score weights sum to 100%', () => {
   368→    const weights = extractWeightsFromTable(qaContent);
   369→    expect(weights.size).toBeGreaterThan(0);
   370→
   371→    let sum = 0;
   372→    for (const pct of weights.values()) {
   373→      sum += pct;
   374→    }
   375→    expect(sum).toBe(100);
   376→  });
   377→
   378→  test('health score has all 8 categories', () => {
   379→    const weights = extractWeightsFromTable(qaContent);
   380→    const expectedCategories = [
   381→      'Console', 'Links', 'Visual', 'Functional',
   382→      'UX', 'Performance', 'Content', 'Accessibility',
   383→    ];
   384→    for (const cat of expectedCategories) {
   385→      expect(weights.has(cat)).toBe(true);
   386→    }
   387→    expect(weights.size).toBe(8);
   388→  });
   389→
   390→  test('has four mode definitions (Diff-aware/Full/Quick/Regression)', () => {
   391→    expect(qaContent).toContain('### Diff-aware');
   392→    expect(qaContent).toContain('### Full');
   393→    expect(qaContent).toContain('### Quick');
   394→    expect(qaContent).toContain('### Regression');
   395→  });
   396→
   397→  test('output structure references report directory layout', () => {
   398→    expect(qaContent).toContain('qa-report-');
   399→    expect(qaContent).toContain('baseline.json');
   400→    expect(qaContent).toContain('screenshots/');
   401→    expect(qaContent).toContain('.gstack/qa-reports/');
   402→  });
   403→});
   404→
   405→// --- Part 7: Greptile history format consistency (A3) ---
   406→
   407→describe('Greptile history format consistency', () => {
   408→  test('greptile-triage.md defines the canonical history format', () => {
   409→    const content = fs.readFileSync(path.join(ROOT, 'review', 'greptile-triage.md'), 'utf-8');
   410→    expect(content).toContain('<YYYY-MM-DD>');
   411→    expect(content).toContain('<owner/repo>');
   412→    expect(content).toContain('<type');
   413→    expect(content).toContain('<file-pattern>');
   414→    expect(content).toContain('<category>');
   415→  });
   416→
   417→  test('review/SKILL.md and ship/SKILL.md both reference greptile-triage.md for write details', () => {
   418→    const reviewContent = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
   419→    const shipContent = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   420→
   421→    expect(reviewContent.toLowerCase()).toContain('greptile-triage.md');
   422→    expect(shipContent.toLowerCase()).toContain('greptile-triage.md');
   423→  });
   424→
   425→  test('greptile-triage.md defines all 9 valid categories', () => {
   426→    const content = fs.readFileSync(path.join(ROOT, 'review', 'greptile-triage.md'), 'utf-8');
   427→    const categories = [
   428→      'race-condition', 'null-check', 'error-handling', 'style',
   429→      'type-safety', 'security', 'performance', 'correctness', 'other',
   430→    ];
   431→    for (const cat of categories) {
   432→      expect(content).toContain(cat);
   433→    }
   434→  });
   435→});
   436→
   437→// --- Hardcoded branch name detection in templates ---
   438→
   439→describe('No hardcoded branch names in SKILL templates', () => {
   440→  const tmplFiles = [
   441→    'ship/SKILL.md.tmpl',
   442→    'review/SKILL.md.tmpl',
   443→    'qa/SKILL.md.tmpl',
   444→    'plan-ceo-review/SKILL.md.tmpl',
   445→    'retro/SKILL.md.tmpl',
   446→    'document-release/SKILL.md.tmpl',
   447→    'plan-eng-review/SKILL.md.tmpl',
   448→    'plan-design-review/SKILL.md.tmpl',
   449→  ];
   450→
   451→  // Patterns that indicate hardcoded 'main' in git commands
   452→  const gitMainPatterns = [
   453→    /\bgit\s+diff\s+(?:origin\/)?main\b/,
   454→    /\bgit\s+log\s+(?:origin\/)?main\b/,
   455→    /\bgit\s+fetch\s+origin\s+main\b/,
   456→    /\bgit\s+merge\s+origin\/main\b/,
   457→    /\borigin\/main\b/,
   458→  ];
   459→
   460→  // Lines that are allowed to mention 'main' (fallback logic, prose)
   461→  const allowlist = [
   462→    /fall\s*back\s+to\s+`main`/i,
   463→    /fall\s*back\s+to\s+`?main`?/i,
   464→    /typically\s+`?main`?/i,
   465→    /If\s+on\s+`main`/i,  // old pattern — should not exist
   466→  ];
   467→
   468→  for (const tmplFile of tmplFiles) {
   469→    test(`${tmplFile} has no hardcoded 'main' in git commands`, () => {
   470→      const filePath = path.join(ROOT, tmplFile);
   471→      if (!fs.existsSync(filePath)) return;
   472→      const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
   473→      const violations: string[] = [];
   474→
   475→      for (let i = 0; i < lines.length; i++) {
   476→        const line = lines[i];
   477→        const isAllowlisted = allowlist.some(p => p.test(line));
   478→        if (isAllowlisted) continue;
   479→
   480→        for (const pattern of gitMainPatterns) {
   481→          if (pattern.test(line)) {
   482→            violations.push(`Line ${i + 1}: ${line.trim()}`);
   483→            break;
   484→          }
   485→        }
   486→      }
   487→
   488→      if (violations.length > 0) {
   489→        throw new Error(
   490→          `${tmplFile} has hardcoded 'main' in git commands:\n` +
   491→          violations.map(v => `  ${v}`).join('\n')
   492→        );
   493→      }
   494→    });
   495→  }
   496→});
   497→
   498→// --- Part 7b: TODOS-format.md reference consistency ---
   499→
   500→describe('TODOS-format.md reference consistency', () => {
   501→  test('review/TODOS-format.md exists and defines canonical format', () => {
   502→    const content = fs.readFileSync(path.join(ROOT, 'review', 'TODOS-format.md'), 'utf-8');
   503→    expect(content).toContain('**What:**');
   504→    expect(content).toContain('**Why:**');
   505→    expect(content).toContain('**Priority:**');
   506→    expect(content).toContain('**Effort:**');
   507→    expect(content).toContain('## Completed');
   508→  });
   509→
   510→  test('skills that write TODOs reference TODOS-format.md', () => {
   511→    const shipContent = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   512→    const ceoPlanContent = fs.readFileSync(path.join(ROOT, 'plan-ceo-review', 'SKILL.md'), 'utf-8');
   513→    const engPlanContent = fs.readFileSync(path.join(ROOT, 'plan-eng-review', 'SKILL.md'), 'utf-8');
   514→
   515→    expect(shipContent).toContain('TODOS-format.md');
   516→    expect(ceoPlanContent).toContain('TODOS-format.md');
   517→    expect(engPlanContent).toContain('TODOS-format.md');
   518→  });
   519→});
   520→
   521→// --- v0.4.1 feature coverage: RECOMMENDATION format, session awareness, enum completeness ---
   522→
   523→describe('v0.4.1 preamble features', () => {
   524→  const skillsWithPreamble = [
   525→    'SKILL.md', 'browse/SKILL.md', 'qa/SKILL.md',
   526→    'qa-only/SKILL.md',
   527→    'setup-browser-cookies/SKILL.md',
   528→    'ship/SKILL.md', 'review/SKILL.md',
   529→    'plan-ceo-review/SKILL.md', 'plan-eng-review/SKILL.md',
   530→    'retro/SKILL.md',
   531→    'plan-design-review/SKILL.md',
   532→    'design-review/SKILL.md',
   533→    'design-consultation/SKILL.md',
   534→    'document-release/SKILL.md',
   535→  ];
   536→
   537→  for (const skill of skillsWithPreamble) {
   538→    test(`${skill} contains RECOMMENDATION format`, () => {
   539→      const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
   540→      expect(content).toContain('RECOMMENDATION: Choose');
   541→      expect(content).toContain('AskUserQuestion');
   542→    });
   543→
   544→    test(`${skill} contains session awareness`, () => {
   545→      const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
   546→      expect(content).toContain('_SESSIONS');
   547→      expect(content).toContain('RECOMMENDATION');
   548→    });
   549→  }
   550→});
   551→
   552→// --- Contributor mode preamble structure validation ---
   553→
   554→describe('Contributor mode preamble structure', () => {
   555→  const skillsWithPreamble = [
   556→    'SKILL.md', 'browse/SKILL.md', 'qa/SKILL.md',
   557→    'qa-only/SKILL.md',
   558→    'setup-browser-cookies/SKILL.md',
   559→    'ship/SKILL.md', 'review/SKILL.md',
   560→    'plan-ceo-review/SKILL.md', 'plan-eng-review/SKILL.md',
   561→    'retro/SKILL.md',
   562→    'plan-design-review/SKILL.md',
   563→    'design-review/SKILL.md',
   564→    'design-consultation/SKILL.md',
   565→    'document-release/SKILL.md',
   566→  ];
   567→
   568→  for (const skill of skillsWithPreamble) {
   569→    test(`${skill} has 0-10 rating in contributor mode`, () => {
   570→      const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
   571→      expect(content).toContain('0 to 10');
   572→      expect(content).toContain('My rating');
   573→    });
   574→
   575→    test(`${skill} has calibration example`, () => {
   576→      const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
   577→      expect(content).toContain('Calibration');
   578→      expect(content).toContain('the bar');
   579→    });
   580→
   581→    test(`${skill} has "what would make this a 10" field`, () => {
   582→      const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
   583→      expect(content).toContain('What would make this a 10');
   584→    });
   585→
   586→    test(`${skill} uses periodic reflection (not per-command)`, () => {
   587→      const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
   588→      expect(content).toContain('workflow step');
   589→      expect(content).not.toContain('After you use gstack-provided CLIs');
   590→    });
   591→  }
   592→});
   593→
   594→describe('Enum & Value Completeness in review checklist', () => {
   595→  const checklist = fs.readFileSync(path.join(ROOT, 'review', 'checklist.md'), 'utf-8');
   596→
   597→  test('checklist has Enum & Value Completeness section', () => {
   598→    expect(checklist).toContain('Enum & Value Completeness');
   599→  });
   600→
   601→  test('Enum & Value Completeness is classified as CRITICAL', () => {
   602→    // It should appear under Pass 1 — CRITICAL, not Pass 2
   603→    const pass1Start = checklist.indexOf('### Pass 1');
   604→    const pass2Start = checklist.indexOf('### Pass 2');
   605→    const enumStart = checklist.indexOf('Enum & Value Completeness');
   606→    expect(enumStart).toBeGreaterThan(pass1Start);
   607→    expect(enumStart).toBeLessThan(pass2Start);
   608→  });
   609→
   610→  test('Enum & Value Completeness mentions tracing through consumers', () => {
   611→    expect(checklist).toContain('Trace it through every consumer');
   612→    expect(checklist).toContain('case');
   613→    expect(checklist).toContain('allowlist');
   614→  });
   615→
   616→  test('Enum & Value Completeness is in the severity classification as CRITICAL', () => {
   617→    const gateSection = checklist.slice(checklist.indexOf('## Severity Classification'));
   618→    // The ASCII art has CRITICAL on the left and INFORMATIONAL on the right
   619→    // Enum & Value Completeness should appear on a line with the CRITICAL tree (├─ or └─)
   620→    const enumLine = gateSection.split('\n').find(l => l.includes('Enum & Value Completeness'));
   621→    expect(enumLine).toBeDefined();
   622→    // It's on the left (CRITICAL) side — starts with ├─ or └─
   623→    expect(enumLine!.trimStart().startsWith('├─') || enumLine!.trimStart().startsWith('└─')).toBe(true);
   624→  });
   625→
   626→  test('Fix-First Heuristic exists in checklist and is referenced by review + ship', () => {
   627→    expect(checklist).toContain('## Fix-First Heuristic');
   628→    expect(checklist).toContain('AUTO-FIX');
   629→    expect(checklist).toContain('ASK');
   630→
   631→    const reviewSkill = fs.readFileSync(path.join(ROOT, 'review/SKILL.md'), 'utf-8');
   632→    const shipSkill = fs.readFileSync(path.join(ROOT, 'ship/SKILL.md'), 'utf-8');
   633→    expect(reviewSkill).toContain('AUTO-FIX');
   634→    expect(reviewSkill).toContain('[AUTO-FIXED]');
   635→    expect(shipSkill).toContain('AUTO-FIX');
   636→    expect(shipSkill).toContain('[AUTO-FIXED]');
   637→  });
   638→});
   639→
   640→// --- Completeness Principle spot-check ---
   641→
   642→describe('Completeness Principle in generated SKILL.md files', () => {
   643→  const skillsWithPreamble = [
   644→    'SKILL.md', 'browse/SKILL.md', 'qa/SKILL.md',
   645→    'qa-only/SKILL.md',
   646→    'setup-browser-cookies/SKILL.md',
   647→    'ship/SKILL.md', 'review/SKILL.md',
   648→    'plan-ceo-review/SKILL.md', 'plan-eng-review/SKILL.md',
   649→    'retro/SKILL.md',
   650→    'plan-design-review/SKILL.md',
   651→    'design-review/SKILL.md',
   652→    'design-consultation/SKILL.md',
   653→    'document-release/SKILL.md',
   654→  ];
   655→
   656→  for (const skill of skillsWithPreamble) {
   657→    test(`${skill} contains Completeness Principle section`, () => {
   658→      const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
   659→      expect(content).toContain('Completeness Principle');
   660→      expect(content).toContain('Boil the Lake');
   661→    });
   662→  }
   663→
   664→  test('Completeness Principle includes compression table', () => {
   665→    const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
   666→    expect(content).toContain('CC+gstack');
   667→    expect(content).toContain('Compression');
   668→  });
   669→
   670→  test('Completeness Principle includes anti-patterns', () => {
   671→    const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
   672→    expect(content).toContain('BAD:');
   673→    expect(content).toContain('Anti-patterns');
   674→  });
   675→});
   676→
   677→// --- Part 7: Planted-bug fixture validation (A4) ---
   678→
   679→describe('Planted-bug fixture validation', () => {
   680→  test('qa-eval ground truth has exactly 5 planted bugs', () => {
   681→    const groundTruth = JSON.parse(
   682→      fs.readFileSync(path.join(ROOT, 'test', 'fixtures', 'qa-eval-ground-truth.json'), 'utf-8')
   683→    );
   684→    expect(groundTruth.bugs).toHaveLength(5);
   685→    expect(groundTruth.total_bugs).toBe(5);
   686→  });
   687→
   688→  test('qa-eval-spa ground truth has exactly 5 planted bugs', () => {
   689→    const groundTruth = JSON.parse(
   690→      fs.readFileSync(path.join(ROOT, 'test', 'fixtures', 'qa-eval-spa-ground-truth.json'), 'utf-8')
   691→    );
   692→    expect(groundTruth.bugs).toHaveLength(5);
   693→    expect(groundTruth.total_bugs).toBe(5);
   694→  });
   695→
   696→  test('qa-eval-checkout ground truth has exactly 5 planted bugs', () => {
   697→    const groundTruth = JSON.parse(
   698→      fs.readFileSync(path.join(ROOT, 'test', 'fixtures', 'qa-eval-checkout-ground-truth.json'), 'utf-8')
   699→    );
   700→    expect(groundTruth.bugs).toHaveLength(5);
   701→    expect(groundTruth.total_bugs).toBe(5);
   702→  });
   703→
   704→  test('qa-eval.html contains the planted bugs', () => {
   705→    const html = fs.readFileSync(path.join(ROOT, 'browse', 'test', 'fixtures', 'qa-eval.html'), 'utf-8');
   706→    // BUG 1: broken link
   707→    expect(html).toContain('/nonexistent-404-page');
   708→    // BUG 2: disabled submit
   709→    expect(html).toContain('disabled');
   710→    // BUG 3: overflow
   711→    expect(html).toContain('overflow: hidden');
   712→    // BUG 4: missing alt
   713→    expect(html).toMatch(/<img[^>]*src="\/logo\.png"[^>]*>/);
   714→    expect(html).not.toMatch(/<img[^>]*src="\/logo\.png"[^>]*alt=/);
   715→    // BUG 5: console error
   716→    expect(html).toContain("Cannot read properties of undefined");
   717→  });
   718→
   719→  test('review-eval-vuln.rb contains expected vulnerability patterns', () => {
   720→    const content = fs.readFileSync(path.join(ROOT, 'test', 'fixtures', 'review-eval-vuln.rb'), 'utf-8');
   721→    expect(content).toContain('params[:id]');
   722→    expect(content).toContain('update_column');
   723→  });
   724→});
   725→
   726→// --- CEO review mode validation ---
   727→
   728→describe('CEO review mode validation', () => {
   729→  const content = fs.readFileSync(path.join(ROOT, 'plan-ceo-review', 'SKILL.md'), 'utf-8');
   730→
   731→  test('has all four CEO review modes defined', () => {
   732→    const modes = ['SCOPE EXPANSION', 'SELECTIVE EXPANSION', 'HOLD SCOPE', 'SCOPE REDUCTION'];
   733→    for (const mode of modes) {
   734→      expect(content).toContain(mode);
   735→    }
   736→  });
   737→
   738→  test('has CEO plan persistence step', () => {
   739→    expect(content).toContain('ceo-plans');
   740→    expect(content).toContain('status: ACTIVE');
   741→  });
   742→
   743→  test('has docs/designs promotion section', () => {
   744→    expect(content).toContain('docs/designs');
   745→    expect(content).toContain('PROMOTED');
   746→  });
   747→
   748→  test('mode quick reference has four columns', () => {
   749→    expect(content).toContain('EXPANSION');
   750→    expect(content).toContain('SELECTIVE');
   751→    expect(content).toContain('HOLD SCOPE');
   752→    expect(content).toContain('REDUCTION');
   753→  });
   754→});
   755→
   756→// --- gstack-slug helper ---
   757→
   758→describe('gstack-slug', () => {
   759→  const SLUG_BIN = path.join(ROOT, 'bin', 'gstack-slug');
   760→
   761→  test('binary exists and is executable', () => {
   762→    expect(fs.existsSync(SLUG_BIN)).toBe(true);
   763→    const stat = fs.statSync(SLUG_BIN);
   764→    expect(stat.mode & 0o111).toBeGreaterThan(0);
   765→  });
   766→
   767→  test('outputs SLUG and BRANCH lines in a git repo', () => {
   768→    const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
   769→    expect(result.exitCode).toBe(0);
   770→    const output = result.stdout.toString();
   771→    expect(output).toContain('SLUG=');
   772→    expect(output).toContain('BRANCH=');
   773→  });
   774→
   775→  test('SLUG does not contain forward slashes', () => {
   776→    const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
   777→    const slug = result.stdout.toString().match(/SLUG=(.*)/)?.[1] ?? '';
   778→    expect(slug).not.toContain('/');
   779→    expect(slug.length).toBeGreaterThan(0);
   780→  });
   781→
   782→  test('BRANCH does not contain forward slashes', () => {
   783→    const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
   784→    const branch = result.stdout.toString().match(/BRANCH=(.*)/)?.[1] ?? '';
   785→    expect(branch).not.toContain('/');
   786→    expect(branch.length).toBeGreaterThan(0);
   787→  });
   788→
   789→  test('output is eval-compatible (KEY=VALUE format)', () => {
   790→    const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
   791→    const lines = result.stdout.toString().trim().split('\n');
   792→    expect(lines.length).toBe(2);
   793→    expect(lines[0]).toMatch(/^SLUG=.+/);
   794→    expect(lines[1]).toMatch(/^BRANCH=.+/);
   795→  });
   796→});
   797→
   798→// --- Test Bootstrap validation ---
   799→
   800→describe('Test Bootstrap ({{TEST_BOOTSTRAP}}) integration', () => {
   801→  test('TEST_BOOTSTRAP resolver produces valid content', () => {
   802→    const qaContent = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   803→    expect(qaContent).toContain('Test Framework Bootstrap');
   804→    expect(qaContent).toContain('RUNTIME:ruby');
   805→    expect(qaContent).toContain('RUNTIME:node');
   806→    expect(qaContent).toContain('RUNTIME:python');
   807→    expect(qaContent).toContain('no-test-bootstrap');
   808→    expect(qaContent).toContain('BOOTSTRAP_DECLINED');
   809→  });
   810→
   811→  test('TEST_BOOTSTRAP appears in qa/SKILL.md', () => {
   812→    const content = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   813→    expect(content).toContain('Test Framework Bootstrap');
   814→    expect(content).toContain('TESTING.md');
   815→    expect(content).toContain('CLAUDE.md');
   816→  });
   817→
   818→  test('TEST_BOOTSTRAP appears in ship/SKILL.md', () => {
   819→    const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   820→    expect(content).toContain('Test Framework Bootstrap');
   821→    expect(content).toContain('Step 2.5');
   822→  });
   823→
   824→  test('TEST_BOOTSTRAP appears in design-review/SKILL.md', () => {
   825→    const content = fs.readFileSync(path.join(ROOT, 'design-review', 'SKILL.md'), 'utf-8');
   826→    expect(content).toContain('Test Framework Bootstrap');
   827→  });
   828→
   829→  test('TEST_BOOTSTRAP does NOT appear in qa-only/SKILL.md', () => {
   830→    const content = fs.readFileSync(path.join(ROOT, 'qa-only', 'SKILL.md'), 'utf-8');
   831→    expect(content).not.toContain('Test Framework Bootstrap');
   832→    // But should have the recommendation note
   833→    expect(content).toContain('No test framework detected');
   834→    expect(content).toContain('Run `/qa` to bootstrap');
   835→  });
   836→
   837→  test('bootstrap includes framework knowledge table', () => {
   838→    const content = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   839→    expect(content).toContain('vitest');
   840→    expect(content).toContain('minitest');
   841→    expect(content).toContain('pytest');
   842→    expect(content).toContain('cargo test');
   843→    expect(content).toContain('phpunit');
   844→    expect(content).toContain('ExUnit');
   845→  });
   846→
   847→  test('bootstrap includes CI/CD pipeline generation', () => {
   848→    const content = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   849→    expect(content).toContain('.github/workflows/test.yml');
   850→    expect(content).toContain('GitHub Actions');
   851→  });
   852→
   853→  test('bootstrap includes first real tests step', () => {
   854→    const content = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   855→    expect(content).toContain('First real tests');
   856→    expect(content).toContain('git log --since=30.days');
   857→    expect(content).toContain('Prioritize by risk');
   858→  });
   859→
   860→  test('bootstrap includes vibe coding philosophy', () => {
   861→    const content = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   862→    expect(content).toContain('vibe coding');
   863→    expect(content).toContain('100% test coverage');
   864→  });
   865→
   866→  test('WebSearch is in allowed-tools for qa, ship, design-review', () => {
   867→    const qa = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   868→    const ship = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   869→    const qaDesign = fs.readFileSync(path.join(ROOT, 'design-review', 'SKILL.md'), 'utf-8');
   870→    expect(qa).toContain('WebSearch');
   871→    expect(ship).toContain('WebSearch');
   872→    expect(qaDesign).toContain('WebSearch');
   873→  });
   874→});
   875→
   876→// --- Phase 8e.5 regression test validation ---
   877→
   878→describe('Phase 8e.5 regression test generation', () => {
   879→  test('qa/SKILL.md contains Phase 8e.5', () => {
   880→    const content = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   881→    expect(content).toContain('8e.5. Regression Test');
   882→    expect(content).toContain('test(qa): regression test');
   883→    expect(content).toContain('WTF-likelihood exclusion');
   884→  });
   885→
   886→  test('qa/SKILL.md Rule 13 is amended for regression tests', () => {
   887→    const content = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   888→    expect(content).toContain('Only modify tests when generating regression tests in Phase 8e.5');
   889→    expect(content).not.toContain('Never modify tests or CI configuration');
   890→  });
   891→
   892→  test('design-review has CSS-aware Phase 8e.5 variant', () => {
   893→    const content = fs.readFileSync(path.join(ROOT, 'design-review', 'SKILL.md'), 'utf-8');
   894→    expect(content).toContain('8e.5. Regression Test (design-review variant)');
   895→    expect(content).toContain('CSS-only');
   896→    expect(content).toContain('test(design): regression test');
   897→  });
   898→
   899→  test('regression test includes full attribution comment format', () => {
   900→    const content = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   901→    expect(content).toContain('// Regression: ISSUE-NNN');
   902→    expect(content).toContain('// Found by /qa on');
   903→    expect(content).toContain('// Report: .gstack/qa-reports/');
   904→  });
   905→
   906→  test('regression test uses auto-incrementing names', () => {
   907→    const content = fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
   908→    expect(content).toContain('auto-incrementing');
   909→    expect(content).toContain('max number + 1');
   910→  });
   911→});
   912→
   913→// --- Step 3.4 coverage audit validation ---
   914→
   915→describe('Step 3.4 test coverage audit', () => {
   916→  test('ship/SKILL.md contains Step 3.4', () => {
   917→    const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   918→    expect(content).toContain('Step 3.4: Test Coverage Audit');
   919→    expect(content).toContain('CODE PATH COVERAGE');
   920→  });
   921→
   922→  test('Step 3.4 includes quality scoring rubric', () => {
   923→    const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   924→    expect(content).toContain('★★★');
   925→    expect(content).toContain('★★');
   926→    expect(content).toContain('edge cases AND error paths');
   927→    expect(content).toContain('happy path only');
   928→  });
   929→
   930→  test('Step 3.4 includes before/after test count', () => {
   931→    const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   932→    expect(content).toContain('Count test files before');
   933→    expect(content).toContain('Count test files after');
   934→  });
   935→
   936→  test('ship PR body includes Test Coverage section', () => {
   937→    const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   938→    expect(content).toContain('## Test Coverage');
   939→  });
   940→
   941→  test('ship rules include test generation rule', () => {
   942→    const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   943→    expect(content).toContain('Step 3.4 generates coverage tests');
   944→    expect(content).toContain('Never commit failing tests');
   945→  });
   946→
   947→  test('Step 3.4 includes vibe coding philosophy', () => {
   948→    const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   949→    expect(content).toContain('vibe coding becomes yolo coding');
   950→  });
   951→
   952→  test('Step 3.4 traces actual codepaths, not just syntax', () => {
   953→    const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   954→    expect(content).toContain('Trace every codepath');
   955→    expect(content).toContain('Trace data flow');
   956→    expect(content).toContain('Diagram the execution');
   957→  });
   958→
   959→  test('Step 3.4 maps user flows and interaction edge cases', () => {
   960→    const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   961→    expect(content).toContain('Map user flows');
   962→    expect(content).toContain('Interaction edge cases');
   963→    expect(content).toContain('Double-click');
   964→    expect(content).toContain('Navigate away');
   965→    expect(content).toContain('Error states the user can see');
   966→    expect(content).toContain('Empty/zero/boundary states');
   967→  });
   968→
   969→  test('Step 3.4 diagram includes USER FLOW COVERAGE section', () => {
   970→    const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
   971→    expect(content).toContain('USER FLOW COVERAGE');
   972→    expect(content).toContain('Code paths:');
   973→    expect(content).toContain('User flows:');
   974→  });
   975→});
   976→
   977→// --- Retro test health validation ---
   978→
   979→describe('Retro test health tracking', () => {
   980→  test('retro/SKILL.md has test health data gathering commands', () => {
   981→    const content = fs.readFileSync(path.join(ROOT, 'retro', 'SKILL.md'), 'utf-8');
   982→    expect(content).toContain('# 10. Test file count');
   983→    expect(content).toContain('# 11. Regression test commits');
   984→    expect(content).toContain('# 12. Test files changed');
   985→  });
   986→
   987→  test('retro/SKILL.md has Test Health metrics row', () => {
   988→    const content = fs.readFileSync(path.join(ROOT, 'retro', 'SKILL.md'), 'utf-8');
   989→    expect(content).toContain('Test Health');
   990→    expect(content).toContain('regression tests');
   991→  });
   992→
   993→  test('retro/SKILL.md has Test Health narrative section', () => {
   994→    const content = fs.readFileSync(path.join(ROOT, 'retro', 'SKILL.md'), 'utf-8');
   995→    expect(content).toContain('### Test Health');
   996→    expect(content).toContain('Total test files');
   997→    expect(content).toContain('vibe coding safe');
   998→  });
   999→
  1000→  test('retro JSON schema includes test_health field', () => {
  1001→    const content = fs.readFileSync(path.join(ROOT, 'retro', 'SKILL.md'), 'utf-8');
  1002→    expect(content).toContain('test_health');
  1003→    expect(content).toContain('total_test_files');
  1004→    expect(content).toContain('regression_test_commits');
  1005→  });
  1006→});
  1007→
  1008→// --- QA report template regression tests section ---
  1009→
  1010→describe('QA report template', () => {
  1011→  test('qa-report-template.md has Regression Tests section', () => {
  1012→    const content = fs.readFileSync(path.join(ROOT, 'qa', 'templates', 'qa-report-template.md'), 'utf-8');
  1013→    expect(content).toContain('## Regression Tests');
  1014→    expect(content).toContain('committed / deferred / skipped');
  1015→    expect(content).toContain('### Deferred Tests');
  1016→    expect(content).toContain('**Precondition:**');
  1017→  });
  1018→});
  1019→


bash "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags-shell.sh" "post:observe" "skills/continuous-learning-v2/hooks/observe.sh" "standard,strict"


     1→/**
     2→ * LLM-as-a-Judge evals for generated SKILL.md quality.
     3→ *
     4→ * Uses the Anthropic API directly (not Agent SDK) to evaluate whether
     5→ * generated command docs are clear, complete, and actionable for an AI agent.
     6→ *
     7→ * Requires: ANTHROPIC_API_KEY env var (or EVALS=1 with key already set)
     8→ * Run: EVALS=1 bun run test:eval
     9→ *
    10→ * Cost: ~$0.05-0.15 per run (sonnet)
    11→ */
    12→
    13→import { describe, test, expect, afterAll } from 'bun:test';
    14→import Anthropic from '@anthropic-ai/sdk';
    15→import * as fs from 'fs';
    16→import * as path from 'path';
    17→import { callJudge, judge } from './helpers/llm-judge';
    18→import type { JudgeScore } from './helpers/llm-judge';
    19→import { EvalCollector } from './helpers/eval-store';
    20→import { selectTests, detectBaseBranch, getChangedFiles, LLM_JUDGE_TOUCHFILES, GLOBAL_TOUCHFILES } from './helpers/touchfiles';
    21→
    22→const ROOT = path.resolve(import.meta.dir, '..');
    23→// Run when EVALS=1 is set (requires ANTHROPIC_API_KEY in env)
    24→const evalsEnabled = !!process.env.EVALS;
    25→const describeEval = evalsEnabled ? describe : describe.skip;
    26→
    27→// Eval result collector
    28→const evalCollector = evalsEnabled ? new EvalCollector('llm-judge') : null;
    29→
    30→// --- Diff-based test selection ---
    31→let selectedTests: string[] | null = null;
    32→
    33→if (evalsEnabled && !process.env.EVALS_ALL) {
    34→  const baseBranch = process.env.EVALS_BASE
    35→    || detectBaseBranch(ROOT)
    36→    || 'main';
    37→  const changedFiles = getChangedFiles(baseBranch, ROOT);
    38→
    39→  if (changedFiles.length > 0) {
    40→    const selection = selectTests(changedFiles, LLM_JUDGE_TOUCHFILES, GLOBAL_TOUCHFILES);
    41→    selectedTests = selection.selected;
    42→    process.stderr.write(`\nLLM-judge selection (${selection.reason}): ${selection.selected.length}/${Object.keys(LLM_JUDGE_TOUCHFILES).length} tests\n`);
    43→    if (selection.skipped.length > 0) {
    44→      process.stderr.write(`  Skipped: ${selection.skipped.join(', ')}\n`);
    45→    }
    46→    process.stderr.write('\n');
    47→  }
    48→}
    49→
    50→/** Wrap a describe block to skip if none of its tests are selected. */
    51→function describeIfSelected(name: string, testNames: string[], fn: () => void) {
    52→  const anySelected = selectedTests === null || testNames.some(t => selectedTests!.includes(t));
    53→  (anySelected ? describeEval : describe.skip)(name, fn);
    54→}
    55→
    56→/** Skip an individual test if not selected (for multi-test describe blocks). */
    57→function testIfSelected(testName: string, fn: () => Promise<void>, timeout: number) {
    58→  const shouldRun = selectedTests === null || selectedTests.includes(testName);
    59→  (shouldRun ? test : test.skip)(testName, fn, timeout);
    60→}
    61→
    62→describeIfSelected('LLM-as-judge quality evals', [
    63→  'command reference table', 'snapshot flags reference',
    64→  'browse/SKILL.md reference', 'setup block', 'regression vs baseline',
    65→], () => {
    66→  testIfSelected('command reference table', async () => {
    67→    const t0 = Date.now();
    68→    const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
    69→    const start = content.indexOf('## Command Reference');
    70→    const end = content.indexOf('## Tips');
    71→    const section = content.slice(start, end);
    72→
    73→    const scores = await judge('command reference table', section);
    74→    console.log('Command reference scores:', JSON.stringify(scores, null, 2));
    75→
    76→    evalCollector?.addTest({
    77→      name: 'command reference table',
    78→      suite: 'LLM-as-judge quality evals',
    79→      tier: 'llm-judge',
    80→      passed: scores.clarity >= 4 && scores.completeness >= 4 && scores.actionability >= 4,
    81→      duration_ms: Date.now() - t0,
    82→      cost_usd: 0.02,
    83→      judge_scores: { clarity: scores.clarity, completeness: scores.completeness, actionability: scores.actionability },
    84→      judge_reasoning: scores.reasoning,
    85→    });
    86→
    87→    expect(scores.clarity).toBeGreaterThanOrEqual(4);
    88→    expect(scores.completeness).toBeGreaterThanOrEqual(4);
    89→    expect(scores.actionability).toBeGreaterThanOrEqual(4);
    90→  }, 30_000);
    91→
    92→  testIfSelected('snapshot flags reference', async () => {
    93→    const t0 = Date.now();
    94→    const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
    95→    const start = content.indexOf('## Snapshot System');
    96→    const end = content.indexOf('## Command Reference');
    97→    const section = content.slice(start, end);
    98→
    99→    const scores = await judge('snapshot flags reference', section);
   100→    console.log('Snapshot flags scores:', JSON.stringify(scores, null, 2));
   101→
   102→    evalCollector?.addTest({
   103→      name: 'snapshot flags reference',
   104→      suite: 'LLM-as-judge quality evals',
   105→      tier: 'llm-judge',
   106→      passed: scores.clarity >= 4 && scores.completeness >= 4 && scores.actionability >= 4,
   107→      duration_ms: Date.now() - t0,
   108→      cost_usd: 0.02,
   109→      judge_scores: { clarity: scores.clarity, completeness: scores.completeness, actionability: scores.actionability },
   110→      judge_reasoning: scores.reasoning,
   111→    });
   112→
   113→    expect(scores.clarity).toBeGreaterThanOrEqual(4);
   114→    expect(scores.completeness).toBeGreaterThanOrEqual(4);
   115→    expect(scores.actionability).toBeGreaterThanOrEqual(4);
   116→  }, 30_000);
   117→
   118→  testIfSelected('browse/SKILL.md reference', async () => {
   119→    const t0 = Date.now();
   120→    const content = fs.readFileSync(path.join(ROOT, 'browse', 'SKILL.md'), 'utf-8');
   121→    const start = content.indexOf('## Snapshot Flags');
   122→    const section = content.slice(start);
   123→
   124→    const scores = await judge('browse skill reference (flags + commands)', section);
   125→    console.log('Browse SKILL.md scores:', JSON.stringify(scores, null, 2));
   126→
   127→    evalCollector?.addTest({
   128→      name: 'browse/SKILL.md reference',
   129→      suite: 'LLM-as-judge quality evals',
   130→      tier: 'llm-judge',
   131→      passed: scores.clarity >= 4 && scores.completeness >= 4 && scores.actionability >= 4,
   132→      duration_ms: Date.now() - t0,
   133→      cost_usd: 0.02,
   134→      judge_scores: { clarity: scores.clarity, completeness: scores.completeness, actionability: scores.actionability },
   135→      judge_reasoning: scores.reasoning,
   136→    });
   137→
   138→    expect(scores.clarity).toBeGreaterThanOrEqual(4);
   139→    expect(scores.completeness).toBeGreaterThanOrEqual(4);
   140→    expect(scores.actionability).toBeGreaterThanOrEqual(4);
   141→  }, 30_000);
   142→
   143→  testIfSelected('setup block', async () => {
   144→    const t0 = Date.now();
   145→    const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
   146→    const setupStart = content.indexOf('## SETUP');
   147→    const setupEnd = content.indexOf('## IMPORTANT');
   148→    const section = content.slice(setupStart, setupEnd);
   149→
   150→    const scores = await judge('setup/binary discovery instructions', section);
   151→    console.log('Setup block scores:', JSON.stringify(scores, null, 2));
   152→
   153→    evalCollector?.addTest({
   154→      name: 'setup block',
   155→      suite: 'LLM-as-judge quality evals',
   156→      tier: 'llm-judge',
   157→      passed: scores.actionability >= 3 && scores.clarity >= 3,
   158→      duration_ms: Date.now() - t0,
   159→      cost_usd: 0.02,
   160→      judge_scores: { clarity: scores.clarity, completeness: scores.completeness, actionability: scores.actionability },
   161→      judge_reasoning: scores.reasoning,
   162→    });
   163→
   164→    // Setup block is intentionally minimal (binary discovery only).
   165→    // SKILL_DIR is inferred from context, so judge sometimes scores 3.
   166→    expect(scores.actionability).toBeGreaterThanOrEqual(3);
   167→    expect(scores.clarity).toBeGreaterThanOrEqual(3);
   168→  }, 30_000);
   169→
   170→  testIfSelected('regression vs baseline', async () => {
   171→    const t0 = Date.now();
   172→    const generated = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
   173→    const genStart = generated.indexOf('## Command Reference');
   174→    const genEnd = generated.indexOf('## Tips');
   175→    const genSection = generated.slice(genStart, genEnd);
   176→
   177→    const baseline = `## Command Reference
   178→
   179→### Navigation
   180→| Command | Description |
   181→|---------|-------------|
   182→| \`goto <url>\` | Navigate to URL |
   183→| \`back\` / \`forward\` | History navigation |
   184→| \`reload\` | Reload page |
   185→| \`url\` | Print current URL |
   186→
   187→### Interaction
   188→| Command | Description |
   189→|---------|-------------|
   190→| \`click <sel>\` | Click element |
   191→| \`fill <sel> <val>\` | Fill input |
   192→| \`select <sel> <val>\` | Select dropdown |
   193→| \`hover <sel>\` | Hover element |
   194→| \`type <text>\` | Type into focused element |
   195→| \`press <key>\` | Press key (Enter, Tab, Escape) |
   196→| \`scroll [sel]\` | Scroll element into view |
   197→| \`wait <sel>\` | Wait for element (max 10s) |
   198→| \`wait --networkidle\` | Wait for network to be idle |
   199→| \`wait --load\` | Wait for page load event |
   200→


bash "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags-shell.sh" "pre:observe" "skills/continuous-learning-v2/hooks/observe.sh" "standard,strict"


     1→import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
     2→import { runSkillTest } from './helpers/session-runner';
     3→import type { SkillTestResult } from './helpers/session-runner';
     4→import { outcomeJudge, callJudge } from './helpers/llm-judge';
     5→import { EvalCollector, judgePassed } from './helpers/eval-store';
     6→import type { EvalTestEntry } from './helpers/eval-store';
     7→import { startTestServer } from '../browse/test/test-server';
     8→import { selectTests, detectBaseBranch, getChangedFiles, E2E_TOUCHFILES, GLOBAL_TOUCHFILES } from './helpers/touchfiles';
     9→import { spawnSync } from 'child_process';
    10→import * as fs from 'fs';
    11→import * as path from 'path';
    12→import * as os from 'os';
    13→
    14→const ROOT = path.resolve(import.meta.dir, '..');
    15→
    16→// Skip unless EVALS=1. Session runner strips CLAUDE* env vars to avoid nested session issues.
    17→//
    18→// BLAME PROTOCOL: When an eval fails, do NOT claim "pre-existing" or "not related
    19→// to our changes" without proof. Run the same eval on main to verify. These tests
    20→// have invisible couplings — preamble text, SKILL.md content, and timing all affect
    21→// agent behavior. See CLAUDE.md "E2E eval failure blame protocol" for details.
    22→const evalsEnabled = !!process.env.EVALS;
    23→const describeE2E = evalsEnabled ? describe : describe.skip;
    24→
    25→// --- Diff-based test selection ---
    26→// When EVALS_ALL is not set, only run tests whose touchfiles were modified.
    27→// Set EVALS_ALL=1 to force all tests. Set EVALS_BASE to override base branch.
    28→let selectedTests: string[] | null = null; // null = run all
    29→
    30→if (evalsEnabled && !process.env.EVALS_ALL) {
    31→  const baseBranch = process.env.EVALS_BASE
    32→    || detectBaseBranch(ROOT)
    33→    || 'main';
    34→  const changedFiles = getChangedFiles(baseBranch, ROOT);
    35→
    36→  if (changedFiles.length > 0) {
    37→    const selection = selectTests(changedFiles, E2E_TOUCHFILES, GLOBAL_TOUCHFILES);
    38→    selectedTests = selection.selected;
    39→    process.stderr.write(`\nE2E selection (${selection.reason}): ${selection.selected.length}/${Object.keys(E2E_TOUCHFILES).length} tests\n`);
    40→    if (selection.skipped.length > 0) {
    41→      process.stderr.write(`  Skipped: ${selection.skipped.join(', ')}\n`);
    42→    }
    43→    process.stderr.write('\n');
    44→  }
    45→  // If changedFiles is empty (e.g., on main branch), selectedTests stays null → run all
    46→}
    47→
    48→/** Wrap a describe block to skip entirely if none of its tests are selected. */
    49→function describeIfSelected(name: string, testNames: string[], fn: () => void) {
    50→  const anySelected = selectedTests === null || testNames.some(t => selectedTests!.includes(t));
    51→  (anySelected ? describeE2E : describe.skip)(name, fn);
    52→}
    53→
    54→/** Skip an individual test if not selected (for multi-test describe blocks). */
    55→function testIfSelected(testName: string, fn: () => Promise<void>, timeout: number) {
    56→  const shouldRun = selectedTests === null || selectedTests.includes(testName);
    57→  (shouldRun ? test : test.skip)(testName, fn, timeout);
    58→}
    59→
    60→// Eval result collector — accumulates test results, writes to ~/.gstack-dev/evals/ on finalize
    61→const evalCollector = evalsEnabled ? new EvalCollector('e2e') : null;
    62→
    63→// Unique run ID for this E2E session — used for heartbeat + per-run log directory
    64→const runId = new Date().toISOString().replace(/[:.]/g, '').replace('T', '-').slice(0, 15);
    65→
    66→/** DRY helper to record an E2E test result into the eval collector. */
    67→function recordE2E(name: string, suite: string, result: SkillTestResult, extra?: Partial<EvalTestEntry>) {
    68→  // Derive last tool call from transcript for machine-readable diagnostics
    69→  const lastTool = result.toolCalls.length > 0
    70→    ? `${result.toolCalls[result.toolCalls.length - 1].tool}(${JSON.stringify(result.toolCalls[result.toolCalls.length - 1].input).slice(0, 60)})`
    71→    : undefined;
    72→
    73→  evalCollector?.addTest({
    74→    name, suite, tier: 'e2e',
    75→    passed: result.exitReason === 'success' && result.browseErrors.length === 0,
    76→    duration_ms: result.duration,
    77→    cost_usd: result.costEstimate.estimatedCost,
    78→    transcript: result.transcript,
    79→    output: result.output?.slice(0, 2000),
    80→    turns_used: result.costEstimate.turnsUsed,
    81→    browse_errors: result.browseErrors,
    82→    exit_reason: result.exitReason,
    83→    timeout_at_turn: result.exitReason === 'timeout' ? result.costEstimate.turnsUsed : undefined,
    84→    last_tool_call: lastTool,
    85→    ...extra,
    86→  });
    87→}
    88→
    89→let testServer: ReturnType<typeof startTestServer>;
    90→let tmpDir: string;
    91→const browseBin = path.resolve(ROOT, 'browse', 'dist', 'browse');
    92→
    93→/**
    94→ * Copy a directory tree recursively (files only, follows structure).
    95→ */
    96→function copyDirSync(src: string, dest: string) {
    97→  fs.mkdirSync(dest, { recursive: true });
    98→  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
    99→    const srcPath = path.join(src, entry.name);
   100→    const destPath = path.join(dest, entry.name);
   101→    if (entry.isDirectory()) {
   102→      copyDirSync(srcPath, destPath);
   103→    } else {
   104→      fs.copyFileSync(srcPath, destPath);
   105→    }
   106→  }
   107→}
   108→
   109→/**
   110→ * Set up browse shims (binary symlink, find-browse, remote-slug) in a tmpDir.
   111→ */
   112→function setupBrowseShims(dir: string) {
   113→  // Symlink browse binary
   114→  const binDir = path.join(dir, 'browse', 'dist');
   115→  fs.mkdirSync(binDir, { recursive: true });
   116→  if (fs.existsSync(browseBin)) {
   117→    fs.symlinkSync(browseBin, path.join(binDir, 'browse'));
   118→  }
   119→
   120→  // find-browse shim
   121→  const findBrowseDir = path.join(dir, 'browse', 'bin');
   122→  fs.mkdirSync(findBrowseDir, { recursive: true });
   123→  fs.writeFileSync(
   124→    path.join(findBrowseDir, 'find-browse'),
   125→    `#!/bin/bash\necho "${browseBin}"\n`,
   126→    { mode: 0o755 },
   127→  );
   128→
   129→  // remote-slug shim (returns test-project)
   130→  fs.writeFileSync(
   131→    path.join(findBrowseDir, 'remote-slug'),
   132→    `#!/bin/bash\necho "test-project"\n`,
   133→    { mode: 0o755 },
   134→  );
   135→}
   136→
   137→/**
   138→ * Print cost summary after an E2E test.
   139→ */
   140→function logCost(label: string, result: { costEstimate: { turnsUsed: number; estimatedTokens: number; estimatedCost: number }; duration: number }) {
   141→  const { turnsUsed, estimatedTokens, estimatedCost } = result.costEstimate;
   142→  const durationSec = Math.round(result.duration / 1000);
   143→  console.log(`${label}: $${estimatedCost.toFixed(2)} (${turnsUsed} turns, ${(estimatedTokens / 1000).toFixed(1)}k tokens, ${durationSec}s)`);
   144→}
   145→
   146→/**
   147→ * Dump diagnostic info on planted-bug outcome failure (decision 1C).
   148→ */
   149→function dumpOutcomeDiagnostic(dir: string, label: string, report: string, judgeResult: any) {
   150→  try {
   151→    const transcriptDir = path.join(dir, '.gstack', 'test-transcripts');
   152→    fs.mkdirSync(transcriptDir, { recursive: true });
   153→    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
   154→    fs.writeFileSync(
   155→      path.join(transcriptDir, `${label}-outcome-${timestamp}.json`),
   156→      JSON.stringify({ label, report, judgeResult }, null, 2),
   157→    );
   158→  } catch { /* non-fatal */ }
   159→}
   160→
   161→// Fail fast if Anthropic API is unreachable — don't burn through 13 tests getting ConnectionRefused
   162→if (evalsEnabled) {
   163→  const check = spawnSync('sh', ['-c', 'echo "ping" | claude -p --max-turns 1 --output-format stream-json --verbose --dangerously-skip-permissions'], {
   164→    stdio: 'pipe', timeout: 30_000,
   165→  });
   166→  const output = check.stdout?.toString() || '';
   167→  if (output.includes('ConnectionRefused') || output.includes('Unable to connect')) {
   168→    throw new Error('Anthropic API unreachable — aborting E2E suite. Fix connectivity and retry.');
   169→  }
   170→}
   171→
   172→describeIfSelected('Skill E2E tests', [
   173→  'browse-basic', 'browse-snapshot', 'skillmd-setup-discovery',
   174→  'skillmd-no-local-binary', 'skillmd-outside-git', 'contributor-mode', 'session-awareness',
   175→], () => {
   176→  beforeAll(() => {
   177→    testServer = startTestServer();
   178→    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-'));
   179→    setupBrowseShims(tmpDir);
   180→  });
   181→
   182→  afterAll(() => {
   183→    testServer?.server?.stop();
   184→    try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
   185→  });
   186→
   187→  testIfSelected('browse-basic', async () => {
   188→    const result = await runSkillTest({
   189→      prompt: `You have a browse binary at ${browseBin}. Assign it to B variable and run these commands in sequence:
   190→1. $B goto ${testServer.url}
   191→2. $B snapshot -i
   192→3. $B text
   193→4. $B screenshot /tmp/skill-e2e-test.png
   194→Report the results of each command.`,
   195→      workingDirectory: tmpDir,
   196→      maxTurns: 10,
   197→      timeout: 60_000,
   198→      testName: 'browse-basic',
   199→      runId,
   200→    });


Now let me read the scripts for gen-skill-docs and skill-check:


     1→#!/usr/bin/env bun
     2→/**
     3→ * Generate SKILL.md files from .tmpl templates.
     4→ *
     5→ * Pipeline:
     6→ *   read .tmpl → find {{PLACEHOLDERS}} → resolve from source → format → write .md
     7→ *
     8→ * Supports --dry-run: generate to memory, exit 1 if different from committed file.
     9→ * Used by skill:check and CI freshness checks.
    10→ */
    11→
    12→import { COMMAND_DESCRIPTIONS } from '../browse/src/commands';
    13→import { SNAPSHOT_FLAGS } from '../browse/src/snapshot';
    14→import * as fs from 'fs';
    15→import * as path from 'path';
    16→
    17→const ROOT = path.resolve(import.meta.dir, '..');
    18→const DRY_RUN = process.argv.includes('--dry-run');
    19→
    20→// ─── Placeholder Resolvers ──────────────────────────────────
    21→
    22→function generateCommandReference(): string {
    23→  // Group commands by category
    24→  const groups = new Map<string, Array<{ command: string; description: string; usage?: string }>>();
    25→  for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) {
    26→    const list = groups.get(meta.category) || [];
    27→    list.push({ command: cmd, description: meta.description, usage: meta.usage });
    28→    groups.set(meta.category, list);
    29→  }
    30→
    31→  // Category display order
    32→  const categoryOrder = [
    33→    'Navigation', 'Reading', 'Interaction', 'Inspection',
    34→    'Visual', 'Snapshot', 'Meta', 'Tabs', 'Server',
    35→  ];
    36→
    37→  const sections: string[] = [];
    38→  for (const category of categoryOrder) {
    39→    const commands = groups.get(category);
    40→    if (!commands || commands.length === 0) continue;
    41→
    42→    // Sort alphabetically within category
    43→    commands.sort((a, b) => a.command.localeCompare(b.command));
    44→
    45→    sections.push(`### ${category}`);
    46→    sections.push('| Command | Description |');
    47→    sections.push('|---------|-------------|');
    48→    for (const cmd of commands) {
    49→      const display = cmd.usage ? `\`${cmd.usage}\`` : `\`${cmd.command}\``;
    50→      sections.push(`| ${display} | ${cmd.description} |`);
    51→    }
    52→    sections.push('');
    53→  }
    54→
    55→  return sections.join('\n').trimEnd();
    56→}
    57→
    58→function generateSnapshotFlags(): string {
    59→  const lines: string[] = [
    60→    'The snapshot is your primary tool for understanding and interacting with pages.',
    61→    '',
    62→    '```',
    63→  ];
    64→
    65→  for (const flag of SNAPSHOT_FLAGS) {
    66→    const label = flag.valueHint ? `${flag.short} ${flag.valueHint}` : flag.short;
    67→    lines.push(`${label.padEnd(10)}${flag.long.padEnd(24)}${flag.description}`);
    68→  }
    69→
    70→  lines.push('```');
    71→  lines.push('');
    72→  lines.push('All flags can be combined freely. `-o` only applies when `-a` is also used.');
    73→  lines.push('Example: `$B snapshot -i -a -C -o /tmp/annotated.png`');
    74→  lines.push('');
    75→  lines.push('**Ref numbering:** @e refs are assigned sequentially (@e1, @e2, ...) in tree order.');
    76→  lines.push('@c refs from `-C` are numbered separately (@c1, @c2, ...).');
    77→  lines.push('');
    78→  lines.push('After snapshot, use @refs as selectors in any command:');
    79→  lines.push('```bash');
    80→  lines.push('$B click @e3       $B fill @e4 "value"     $B hover @e1');
    81→  lines.push('$B html @e2        $B css @e5 "color"      $B attrs @e6');
    82→  lines.push('$B click @c1       # cursor-interactive ref (from -C)');
    83→  lines.push('```');
    84→  lines.push('');
    85→  lines.push('**Output format:** indented accessibility tree with @ref IDs, one element per line.');
    86→  lines.push('```');
    87→  lines.push('  @e1 [heading] "Welcome" [level=1]');
    88→  lines.push('  @e2 [textbox] "Email"');
    89→  lines.push('  @e3 [button] "Submit"');
    90→  lines.push('```');
    91→  lines.push('');
    92→  lines.push('Refs are invalidated on navigation — run `snapshot` again after `goto`.');
    93→
    94→  return lines.join('\n');
    95→}
    96→
    97→function generatePreamble(): string {
    98→  return `## Preamble (run first)
    99→
   100→\`\`\`bash
   101→_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
   102→[ -n "$_UPD" ] && echo "$_UPD" || true
   103→mkdir -p ~/.gstack/sessions
   104→touch ~/.gstack/sessions/"$PPID"
   105→_SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr -d ' ')
   106→find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
   107→_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
   108→_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
   109→echo "BRANCH: $_BRANCH"
   110→_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
   111→echo "LAKE_INTRO: $_LAKE_SEEN"
   112→\`\`\`
   113→
   114→If output shows \`UPGRADE_AVAILABLE <old> <new>\`: read \`~/.claude/skills/gstack/gstack-upgrade/SKILL.md\` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If \`JUST_UPGRADED <from> <to>\`: tell user "Running gstack v{to} (just updated!)" and continue.
   115→
   116→If \`LAKE_INTRO\` is \`no\`: Before continuing, introduce the Completeness Principle.
   117→Tell the user: "gstack follows the **Boil the Lake** principle — always do the complete
   118→thing when AI makes the marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean"
   119→Then offer to open the essay in their default browser:
   120→
   121→\`\`\`bash
   122→open https://garryslist.org/posts/boil-the-ocean
   123→touch ~/.gstack/.completeness-intro-seen
   124→\`\`\`
   125→
   126→Only run \`open\` if the user says yes. Always run \`touch\` to mark as seen. This only happens once.
   127→
   128→## AskUserQuestion Format
   129→
   130→**ALWAYS follow this structure for every AskUserQuestion call:**
   131→1. **Re-ground:** State the project, the current branch (use the \`_BRANCH\` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
   132→2. **Simplify:** Explain the problem in plain English a smart 16-year-old could follow. No raw function names, no internal jargon, no implementation details. Use concrete examples and analogies. Say what it DOES, not what it's called.
   133→3. **Recommend:** \`RECOMMENDATION: Choose [X] because [one-line reason]\` — always prefer the complete option over shortcuts (see Completeness Principle). Include \`Completeness: X/10\` for each option. Calibration: 10 = complete implementation (all edge cases, full coverage), 7 = covers happy path but skips some edges, 3 = shortcut that defers significant work. If both options are 8+, pick the higher; if one is ≤5, flag it.
   134→4. **Options:** Lettered options: \`A) ... B) ... C) ...\` — when an option involves effort, show both scales: \`(human: ~X / CC: ~Y)\`
   135→
   136→Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
   137→
   138→Per-skill instructions may add additional formatting rules on top of this baseline.
   139→
   140→## Completeness Principle — Boil the Lake
   141→
   142→AI-assisted coding makes the marginal cost of completeness near-zero. When you present options:
   143→
   144→- If Option A is the complete implementation (full parity, all edge cases, 100% coverage) and Option B is a shortcut that saves modest effort — **always recommend A**. The delta between 80 lines and 150 lines is meaningless with CC+gstack. "Good enough" is the wrong instinct when "complete" costs minutes more.
   145→- **Lake vs. ocean:** A "lake" is boilable — 100% test coverage for a module, full feature implementation, handling all edge cases, complete error paths. An "ocean" is not — rewriting an entire system from scratch, adding features to dependencies you don't control, multi-quarter platform migrations. Recommend boiling lakes. Flag oceans as out of scope.
   146→- **When estimating effort**, always show both scales: human team time and CC+gstack time. The compression ratio varies by task type — use this reference:
   147→
   148→| Task type | Human team | CC+gstack | Compression |
   149→|-----------|-----------|-----------|-------------|
   150→| Boilerplate / scaffolding | 2 days | 15 min | ~100x |
   151→| Test writing | 1 day | 15 min | ~50x |
   152→| Feature implementation | 1 week | 30 min | ~30x |
   153→| Bug fix + regression test | 4 hours | 15 min | ~20x |
   154→| Architecture / design | 2 days | 4 hours | ~5x |
   155→| Research / exploration | 1 day | 3 hours | ~3x |
   156→
   157→- This principle applies to test coverage, error handling, documentation, edge cases, and feature completeness. Don't skip the last 10% to "save time" — with AI, that 10% costs seconds.
   158→
   159→**Anti-patterns — DON'T do this:**
   160→- BAD: "Choose B — it covers 90% of the value with less code." (If A is only 70 lines more, choose A.)
   161→- BAD: "We can skip edge case handling to save time." (Edge case handling costs minutes with CC.)
   162→- BAD: "Let's defer test coverage to a follow-up PR." (Tests are the cheapest lake to boil.)
   163→- BAD: Quoting only human-team effort: "This would take 2 weeks." (Say: "2 weeks human / ~1 hour CC.")
   164→
   165→## Contributor Mode
   166→
   167→If \`_CONTRIB\` is \`true\`: you are in **contributor mode**. You're a gstack user who also helps make it better.
   168→
   169→**At the end of each major workflow step** (not after every single command), reflect on the gstack tooling you used. Rate your experience 0 to 10. If it wasn't a 10, think about why. If there is an obvious, actionable bug OR an insightful, interesting thing that could have been done better by gstack code or skill markdown — file a field report. Maybe our contributor will help make us better!
   170→
   171→**Calibration — this is the bar:** For example, \`$B js "await fetch(...)"\` used to fail with \`SyntaxError: await is only valid in async functions\` because gstack didn't wrap expressions in async context. Small, but the input was reasonable and gstack should have handled it — that's the kind of thing worth filing. Things less consequential than this, ignore.
   172→
   173→**NOT worth filing:** user's app bugs, network errors to user's URL, auth failures on user's site, user's own JS logic bugs.
   174→
   175→**To file:** write \`~/.gstack/contributor-logs/{slug}.md\` with **all sections below** (do not truncate — include every section through the Date/Version footer):
   176→
   177→\`\`\`
   178→# {Title}
   179→
   180→Hey gstack team — ran into this while using /{skill-name}:
   181→
   182→**What I was trying to do:** {what the user/agent was attempting}
   183→**What happened instead:** {what actually happened}
   184→**My rating:** {0-10} — {one sentence on why it wasn't a 10}
   185→
   186→## Steps to reproduce
   187→1. {step}
   188→
   189→## Raw output
   190→\`\`\`
   191→{paste the actual error or unexpected output here}
   192→\`\`\`
   193→
   194→## What would make this a 10
   195→{one sentence: what gstack should have done differently}
   196→
   197→**Date:** {YYYY-MM-DD} | **Version:** {gstack version} | **Skill:** /{skill}
   198→\`\`\`
   199→
   200→Slug: lowercase, hyphens, max 60 chars (e.g. \`browse-js-no-await\`). Skip if file already exists. Max 3 reports per session. File inline and continue — don't stop the workflow. Tell user: "Filed gstack field report: {title}"`;
   201→}
   202→
   203→function generateBrowseSetup(): string {
   204→  return `## SETUP (run this check BEFORE any browse command)
   205→
   206→\`\`\`bash
   207→_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
   208→B=""
   209→[ -n "$_ROOT" ] && [ -x "$_ROOT/.claude/skills/gstack/browse/dist/browse" ] && B="$_ROOT/.claude/skills/gstack/browse/dist/browse"
   210→[ -z "$B" ] && B=~/.claude/skills/gstack/browse/dist/browse
   211→if [ -x "$B" ]; then
   212→  echo "READY: $B"
   213→else
   214→  echo "NEEDS_SETUP"
   215→fi
   216→\`\`\`
   217→
   218→If \`NEEDS_SETUP\`:
   219→1. Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait.
   220→2. Run: \`cd <SKILL_DIR> && ./setup\`
   221→3. If \`bun\` is not installed: \`curl -fsSL https://bun.sh/install | bash\``;
   222→}
   223→
   224→function generateBaseBranchDetect(): string {
   225→  return `## Step 0: Detect base branch
   226→
   227→Determine which branch this PR targets. Use the result as "the base branch" in all subsequent steps.
   228→
   229→1. Check if a PR already exists for this branch:
   230→   \`gh pr view --json baseRefName -q .baseRefName\`
   231→   If this succeeds, use the printed branch name as the base branch.
   232→
   233→2. If no PR exists (command fails), detect the repo's default branch:
   234→   \`gh repo view --json defaultBranchRef -q .defaultBranchRef.name\`
   235→
   236→3. If both commands fail, fall back to \`main\`.
   237→
   238→Print the detected base branch name. In every subsequent \`git diff\`, \`git log\`,
   239→\`git fetch\`, \`git merge\`, and \`gh pr create\` command, substitute the detected
   240→branch name wherever the instructions say "the base branch."
   241→
   242→---`;
   243→}
   244→
   245→function generateQAMethodology(): string {
   246→  return `## Modes
   247→
   248→### Diff-aware (automatic when on a feature branch with no URL)
   249→
   250→This is the **primary mode** for developers verifying their work. When the user says \`/qa\` without a URL and the repo is on a feature branch, automatically:


     1→#!/usr/bin/env bun
     2→/**
     3→ * skill:check — Health summary for all SKILL.md files.
     4→ *
     5→ * Reports:
     6→ *   - Command validation (valid/invalid/snapshot errors)
     7→ *   - Template coverage (which SKILL.md files have .tmpl sources)
     8→ *   - Freshness check (generated files match committed files)
     9→ */
    10→
    11→import { validateSkill } from '../test/helpers/skill-parser';
    12→import * as fs from 'fs';
    13→import * as path from 'path';
    14→import { execSync } from 'child_process';
    15→
    16→const ROOT = path.resolve(import.meta.dir, '..');
    17→
    18→// Find all SKILL.md files
    19→const SKILL_FILES = [
    20→  'SKILL.md',
    21→  'browse/SKILL.md',
    22→  'qa/SKILL.md',
    23→  'qa-only/SKILL.md',
    24→  'ship/SKILL.md',
    25→  'review/SKILL.md',
    26→  'retro/SKILL.md',
    27→  'plan-ceo-review/SKILL.md',
    28→  'plan-eng-review/SKILL.md',
    29→  'setup-browser-cookies/SKILL.md',
    30→  'plan-design-review/SKILL.md',
    31→  'design-review/SKILL.md',
    32→  'gstack-upgrade/SKILL.md',
    33→  'document-release/SKILL.md',
    34→].filter(f => fs.existsSync(path.join(ROOT, f)));
    35→
    36→let hasErrors = false;
    37→
    38→// ─── Skills ─────────────────────────────────────────────────
    39→
    40→console.log('  Skills:');
    41→for (const file of SKILL_FILES) {
    42→  const fullPath = path.join(ROOT, file);
    43→  const result = validateSkill(fullPath);
    44→
    45→  if (result.warnings.length > 0) {
    46→    console.log(`  \u26a0\ufe0f  ${file.padEnd(30)} — ${result.warnings.join(', ')}`);
    47→    continue;
    48→  }
    49→
    50→  const totalValid = result.valid.length;
    51→  const totalInvalid = result.invalid.length;
    52→  const totalSnapErrors = result.snapshotFlagErrors.length;
    53→
    54→  if (totalInvalid > 0 || totalSnapErrors > 0) {
    55→    hasErrors = true;
    56→    console.log(`  \u274c ${file.padEnd(30)} — ${totalValid} valid, ${totalInvalid} invalid, ${totalSnapErrors} snapshot errors`);
    57→    for (const inv of result.invalid) {
    58→      console.log(`      line ${inv.line}: unknown command '${inv.command}'`);
    59→    }
    60→    for (const se of result.snapshotFlagErrors) {
    61→      console.log(`      line ${se.command.line}: ${se.error}`);
    62→    }
    63→  } else {
    64→    console.log(`  \u2705 ${file.padEnd(30)} — ${totalValid} commands, all valid`);
    65→  }
    66→}
    67→
    68→// ─── Templates ──────────────────────────────────────────────
    69→
    70→console.log('\n  Templates:');
    71→const TEMPLATES = [
    72→  { tmpl: 'SKILL.md.tmpl', output: 'SKILL.md' },
    73→  { tmpl: 'browse/SKILL.md.tmpl', output: 'browse/SKILL.md' },
    74→];
    75→
    76→for (const { tmpl, output } of TEMPLATES) {
    77→  const tmplPath = path.join(ROOT, tmpl);
    78→  const outPath = path.join(ROOT, output);
    79→  if (!fs.existsSync(tmplPath)) {
    80→    console.log(`  \u26a0\ufe0f  ${output.padEnd(30)} — no template`);
    81→    continue;
    82→  }
    83→  if (!fs.existsSync(outPath)) {
    84→    hasErrors = true;
    85→    console.log(`  \u274c ${output.padEnd(30)} — generated file missing! Run: bun run gen:skill-docs`);
    86→    continue;
    87→  }
    88→  console.log(`  \u2705 ${tmpl.padEnd(30)} \u2192 ${output}`);
    89→}
    90→
    91→// Skills without templates
    92→for (const file of SKILL_FILES) {
    93→  const tmplPath = path.join(ROOT, file + '.tmpl');
    94→  if (!fs.existsSync(tmplPath) && !TEMPLATES.some(t => t.output === file)) {
    95→    console.log(`  \u26a0\ufe0f  ${file.padEnd(30)} — no template (OK if no $B commands)`);
    96→  }
    97→}
    98→
    99→// ─── Freshness ──────────────────────────────────────────────
   100→
   101→console.log('\n  Freshness:');
   102→try {
   103→  execSync('bun run scripts/gen-skill-docs.ts --dry-run', { cwd: ROOT, stdio: 'pipe' });
   104→  console.log('  \u2705 All generated files are fresh');
   105→} catch (err: any) {
   106→  hasErrors = true;
   107→  const output = err.stdout?.toString() || '';
   108→  console.log('  \u274c Generated files are stale:');
   109→  for (const line of output.split('\n').filter((l: string) => l.startsWith('STALE'))) {
   110→    console.log(`      ${line}`);
   111→  }
   112→  console.log('      Run: bun run gen:skill-docs');
   113→}
   114→
   115→console.log('');
   116→process.exit(hasErrors ? 1 : 0);
   117→


Now let me examine the browse directory structure and key files:


/root/projects/gits/gstack/browse/src/browser-manager.ts
/root/projects/gits/gstack/browse/src/buffers.ts
/root/projects/gits/gstack/browse/src/cli.ts
/root/projects/gits/gstack/browse/src/commands.ts
/root/projects/gits/gstack/browse/src/config.ts
/root/projects/gits/gstack/browse/src/cookie-import-browser.ts
/root/projects/gits/gstack/browse/src/cookie-picker-routes.ts
/root/projects/gits/gstack/browse/src/cookie-picker-ui.ts
/root/projects/gits/gstack/browse/src/find-browse.ts
/root/projects/gits/gstack/browse/src/meta-commands.ts
/root/projects/gits/gstack/browse/src/read-commands.ts
/root/projects/gits/gstack/browse/src/server.ts
/root/projects/gits/gstack/browse/src/snapshot.ts
/root/projects/gits/gstack/browse/src/write-commands.ts


     1→/**
     2→ * Command registry — single source of truth for all browse commands.
     3→ *
     4→ * Dependency graph:
     5→ *   commands.ts ──▶ server.ts (runtime dispatch)
     6→ *                ──▶ gen-skill-docs.ts (doc generation)
     7→ *                ──▶ skill-parser.ts (validation)
     8→ *                ──▶ skill-check.ts (health reporting)
     9→ *
    10→ * Zero side effects. Safe to import from build scripts and tests.
    11→ */
    12→
    13→export const READ_COMMANDS = new Set([
    14→  'text', 'html', 'links', 'forms', 'accessibility',
    15→  'js', 'eval', 'css', 'attrs',
    16→  'console', 'network', 'cookies', 'storage', 'perf',
    17→  'dialog', 'is',
    18→]);
    19→
    20→export const WRITE_COMMANDS = new Set([
    21→  'goto', 'back', 'forward', 'reload',
    22→  'click', 'fill', 'select', 'hover', 'type', 'press', 'scroll', 'wait',
    23→  'viewport', 'cookie', 'cookie-import', 'cookie-import-browser', 'header', 'useragent',
    24→  'upload', 'dialog-accept', 'dialog-dismiss',
    25→]);
    26→
    27→export const META_COMMANDS = new Set([
    28→  'tabs', 'tab', 'newtab', 'closetab',
    29→  'status', 'stop', 'restart',
    30→  'screenshot', 'pdf', 'responsive',
    31→  'chain', 'diff',
    32→  'url', 'snapshot',
    33→]);
    34→
    35→export const ALL_COMMANDS = new Set([...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS]);
    36→
    37→export const COMMAND_DESCRIPTIONS: Record<string, { category: string; description: string; usage?: string }> = {
    38→  // Navigation
    39→  'goto':    { category: 'Navigation', description: 'Navigate to URL', usage: 'goto <url>' },
    40→  'back':    { category: 'Navigation', description: 'History back' },
    41→  'forward': { category: 'Navigation', description: 'History forward' },
    42→  'reload':  { category: 'Navigation', description: 'Reload page' },
    43→  'url':     { category: 'Navigation', description: 'Print current URL' },
    44→  // Reading
    45→  'text':    { category: 'Reading', description: 'Cleaned page text' },
    46→  'html':    { category: 'Reading', description: 'innerHTML of selector (throws if not found), or full page HTML if no selector given', usage: 'html [selector]' },
    47→  'links':   { category: 'Reading', description: 'All links as "text → href"' },
    48→  'forms':   { category: 'Reading', description: 'Form fields as JSON' },
    49→  'accessibility': { category: 'Reading', description: 'Full ARIA tree' },
    50→  // Inspection
    51→  'js':      { category: 'Inspection', description: 'Run JavaScript expression and return result as string', usage: 'js <expr>' },
    52→  'eval':    { category: 'Inspection', description: 'Run JavaScript from file and return result as string (path must be under /tmp or cwd)', usage: 'eval <file>' },
    53→  'css':     { category: 'Inspection', description: 'Computed CSS value', usage: 'css <sel> <prop>' },
    54→  'attrs':   { category: 'Inspection', description: 'Element attributes as JSON', usage: 'attrs <sel|@ref>' },
    55→  'is':      { category: 'Inspection', description: 'State check (visible/hidden/enabled/disabled/checked/editable/focused)', usage: 'is <prop> <sel>' },
    56→  'console': { category: 'Inspection', description: 'Console messages (--errors filters to error/warning)', usage: 'console [--clear|--errors]' },
    57→  'network': { category: 'Inspection', description: 'Network requests', usage: 'network [--clear]' },
    58→  'dialog':  { category: 'Inspection', description: 'Dialog messages', usage: 'dialog [--clear]' },
    59→  'cookies': { category: 'Inspection', description: 'All cookies as JSON' },
    60→  'storage': { category: 'Inspection', description: 'Read all localStorage + sessionStorage as JSON, or set <key> <value> to write localStorage', usage: 'storage [set k v]' },
    61→  'perf':    { category: 'Inspection', description: 'Page load timings' },
    62→  // Interaction
    63→  'click':   { category: 'Interaction', description: 'Click element', usage: 'click <sel>' },
    64→  'fill':    { category: 'Interaction', description: 'Fill input', usage: 'fill <sel> <val>' },
    65→  'select':  { category: 'Interaction', description: 'Select dropdown option by value, label, or visible text', usage: 'select <sel> <val>' },
    66→  'hover':   { category: 'Interaction', description: 'Hover element', usage: 'hover <sel>' },
    67→  'type':    { category: 'Interaction', description: 'Type into focused element', usage: 'type <text>' },
    68→  'press':   { category: 'Interaction', description: 'Press key — Enter, Tab, Escape, ArrowUp/Down/Left/Right, Backspace, Delete, Home, End, PageUp, PageDown, or modifiers like Shift+Enter', usage: 'press <key>' },
    69→  'scroll':  { category: 'Interaction', description: 'Scroll element into view, or scroll to page bottom if no selector', usage: 'scroll [sel]' },
    70→  'wait':    { category: 'Interaction', description: 'Wait for element, network idle, or page load (timeout: 15s)', usage: 'wait <sel|--networkidle|--load>' },
    71→  'upload':  { category: 'Interaction', description: 'Upload file(s)', usage: 'upload <sel> <file> [file2...]' },
    72→  'viewport':{ category: 'Interaction', description: 'Set viewport size', usage: 'viewport <WxH>' },
    73→  'cookie':  { category: 'Interaction', description: 'Set cookie on current page domain', usage: 'cookie <name>=<value>' },
    74→  'cookie-import': { category: 'Interaction', description: 'Import cookies from JSON file', usage: 'cookie-import <json>' },
    75→  'cookie-import-browser': { category: 'Interaction', description: 'Import cookies from Comet, Chrome, Arc, Brave, or Edge (opens picker, or use --domain for direct import)', usage: 'cookie-import-browser [browser] [--domain d]' },
    76→  'header':  { category: 'Interaction', description: 'Set custom request header (colon-separated, sensitive values auto-redacted)', usage: 'header <name>:<value>' },
    77→  'useragent': { category: 'Interaction', description: 'Set user agent', usage: 'useragent <string>' },
    78→  'dialog-accept': { category: 'Interaction', description: 'Auto-accept next alert/confirm/prompt. Optional text is sent as the prompt response', usage: 'dialog-accept [text]' },
    79→  'dialog-dismiss': { category: 'Interaction', description: 'Auto-dismiss next dialog' },
    80→  // Visual
    81→  'screenshot': { category: 'Visual', description: 'Save screenshot (supports element crop via CSS/@ref, --clip region, --viewport)', usage: 'screenshot [--viewport] [--clip x,y,w,h] [selector|@ref] [path]' },
    82→  'pdf':     { category: 'Visual', description: 'Save as PDF', usage: 'pdf [path]' },
    83→  'responsive': { category: 'Visual', description: 'Screenshots at mobile (375x812), tablet (768x1024), desktop (1280x720). Saves as {prefix}-mobile.png etc.', usage: 'responsive [prefix]' },
    84→  'diff':    { category: 'Visual', description: 'Text diff between pages', usage: 'diff <url1> <url2>' },
    85→  // Tabs
    86→  'tabs':    { category: 'Tabs', description: 'List open tabs' },
    87→  'tab':     { category: 'Tabs', description: 'Switch to tab', usage: 'tab <id>' },
    88→  'newtab':  { category: 'Tabs', description: 'Open new tab', usage: 'newtab [url]' },
    89→  'closetab':{ category: 'Tabs', description: 'Close tab', usage: 'closetab [id]' },
    90→  // Server
    91→  'status':  { category: 'Server', description: 'Health check' },
    92→  'stop':    { category: 'Server', description: 'Shutdown server' },
    93→  'restart': { category: 'Server', description: 'Restart server' },
    94→  // Meta
    95→  'snapshot':{ category: 'Snapshot', description: 'Accessibility tree with @e refs for element selection. Flags: -i interactive only, -c compact, -d N depth limit, -s sel scope, -D diff vs previous, -a annotated screenshot, -o path output, -C cursor-interactive @c refs', usage: 'snapshot [flags]' },
    96→  'chain':   { category: 'Meta', description: 'Run commands from JSON stdin. Format: [["cmd","arg1",...],...]' },
    97→};
    98→
    99→// Load-time validation: descriptions must cover exactly the command sets
   100→const allCmds = new Set([...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS]);
   101→const descKeys = new Set(Object.keys(COMMAND_DESCRIPTIONS));
   102→for (const cmd of allCmds) {
   103→  if (!descKeys.has(cmd)) throw new Error(`COMMAND_DESCRIPTIONS missing entry for: ${cmd}`);
   104→}
   105→for (const key of descKeys) {
   106→  if (!allCmds.has(key)) throw new Error(`COMMAND_DESCRIPTIONS has unknown command: ${key}`);
   107→}
   108→