feat(grok-build): load carved sections on demand (token ceiling)

Grok ship and other carved skills no longer inline full section bodies into
SKILL.md. Like Claude, they emit STOP-Read pointers to package-local
sections/ files under ~/.grok/skills/gstack-*/sections/.

Drops gstack-ship always-loaded size from ~45k tokens / 179KB to ~20k / 80KB
(under the 160KB soft ceiling). Codex and other hosts keep monolith inline.
This commit is contained in:
Nehr 2026-07-13 01:35:40 +07:00
parent 1480052e77
commit c8132daf79
4 changed files with 211 additions and 1866 deletions

View File

@ -18,6 +18,7 @@ import * as path from 'path';
import type { Host, TemplateContext } from './resolvers/types';
import { HOST_PATHS, unwrapResolver } from './resolvers/types';
import { RESOLVERS } from './resolvers/index';
import { hostUsesSectionPointers } from './resolvers/sections';
import { externalSkillName, extractHookSafetyProse as _extractHookSafetyProse, extractNameAndDescription as _extractNameAndDescription, condenseOpenAIShortDescription as _condenseOpenAIShortDescription, generateOpenAIYaml as _generateOpenAIYaml } from './resolvers/codex-helpers';
import { generatePlanCompletionAuditShip, generatePlanCompletionAuditReview, generatePlanVerificationExec } from './resolvers/review';
import { ALL_HOST_CONFIGS, ALL_HOST_NAMES, resolveHostArg, getHostConfig } from '../hosts/index';
@ -1024,14 +1025,15 @@ for (const currentHost of hostsToRun) {
}
}
// ─── Section generation (v2 plan T9, Claude-first carve) ───
// On-demand sections/*.md for carved skills. Generated for CLAUDE ONLY:
// every other host inlines section content via the {{SECTION:id}} resolver
// (keeping the full monolith skill), so they need no section files and we
// sidestep host-portable section paths until that plumbing lands. No-op for
// any skill without a sections/ dir. Mirrors the SKILL.md DRY_RUN handling so
// sections participate in the freshness gate.
for (const sec of currentHost === 'claude' ? discoverSectionTemplates(ROOT) : []) {
// ─── Section generation (v2 plan T9 carve) ───
// On-demand sections/*.md for carved skills.
// Pointer hosts (Claude, Grok Build): emit section files next to the skill
// package; {{SECTION:id}} is a STOP-Read pointer (keeps the skeleton small).
// Inline hosts (Codex, Factory, …): {{SECTION:id}} inlines content into the
// monolith SKILL.md — no separate section files.
// No-op for any skill without a sections/ dir. Mirrors the SKILL.md DRY_RUN
// handling so sections participate in the freshness gate.
for (const sec of hostUsesSectionPointers(currentHost) ? discoverSectionTemplates(ROOT) : []) {
if (currentHostConfig.generation.includeSkills?.length &&
!currentHostConfig.generation.includeSkills.includes(sec.skillDir)) continue;
if (currentHostConfig.generation.skipSkills?.length &&

View File

@ -1,25 +1,28 @@
/**
* Section resolvers (v2 plan T9, Claude-first carve).
* Section resolvers (v2 plan T9 carve).
*
* A carved skill keeps its prose-heavy steps in `<skill>/sections/<id>.md`, read
* on demand. The SAME template ships to every host, so these resolvers make the
* carve host-aware:
*
* - On CLAUDE: {{SECTION:id}} emits a STOP-Read pointer to the generated section
* file (the skeleton), and the section .md is generated + installed separately.
* - On every OTHER host: {{SECTION:id}} INLINES the section template's content,
* so external hosts keep the full monolith ship skill (no section files, no
* host-portable-path problem). Inlined content keeps its own {{RESOLVER}}
* tokens, which the generator's multi-pass resolve expands.
* - On CLAUDE: SECTION:id emits a STOP-Read pointer to the generated section
* file under the nested monorepo install ({skillRoot}/{skill}/sections/).
* - On GROK-BUILD: same pointer mode, but paths use the flat Grok package layout
* (~/.grok/skills/gstack-{skill}/sections/). Section files are generated into
* each package's sections/ dir and ride along with package install.
* - On every OTHER host: SECTION placeholders INLINE the section template content,
* so those hosts keep the full monolith skill (no section files, no
* host-portable-path problem). Inlined content keeps its own resolver tokens,
* which the generator's multi-pass resolve expands.
*
* {{SECTION_INDEX:skill}} renders the situationsection table from the PASSIVE
* manifest on Claude (empty on other hosts they have no sections). The manifest
* SECTION_INDEX renders the situation-to-section table from the PASSIVE
* manifest on pointer hosts (empty when sections are inlined). The manifest
* is the single source of id/file/title/trigger text (CM2; v2_PLAN.md:663).
*/
import * as fs from 'fs';
import * as path from 'path';
import type { ResolverFn, TemplateContext } from './types';
import type { Host, ResolverFn, TemplateContext } from './types';
const ROOT = path.resolve(import.meta.dir, '..', '..');
@ -34,6 +37,21 @@ interface SectionManifest {
sections: SectionEntry[];
}
/** Hosts that load carved sections on demand (not monolith-inline). */
export function hostUsesSectionPointers(host: Host | string): boolean {
return host === 'claude' || host === 'grok-build';
}
/**
* External package dir name for flat hosts (gstack-ship, gstack-upgrade, ).
* Mirrors gen-skill-docs externalSkillName for skill dirs.
*/
export function externalSkillPackageName(skillName: string): string {
if (skillName === '.' || skillName === '' || skillName === 'gstack') return 'gstack';
if (skillName.startsWith('gstack-')) return skillName;
return `gstack-${skillName}`;
}
function loadManifest(skill: string): SectionManifest {
const p = path.join(ROOT, skill, 'sections', 'manifest.json');
const raw = fs.readFileSync(p, 'utf-8');
@ -49,35 +67,56 @@ function findSection(skill: string, id: string): SectionEntry {
}
/**
* {{SECTION:id}} pointer on Claude, inline on other hosts.
* Claude path uses the stable gstack-root install (`{skillRoot}/{skill}/sections/`),
* which always exists, instead of a naked relative path (Codex outside-voice #7).
* Absolute-style path the agent should Read for a section file.
* Claude: nested monorepo install under skillRoot.
* Grok: flat package next to the thin runtime root.
*/
export function sectionPointerPath(
host: Host | string,
skillName: string,
sectionFile: string,
skillRoot: string,
): string {
if (host === 'grok-build') {
const pkg = externalSkillPackageName(skillName);
return `~/.grok/skills/${pkg}/sections/${sectionFile}`;
}
// Claude (and any future nested-install pointer host)
return `${skillRoot}/${skillName}/sections/${sectionFile}`;
}
function stopReadDirective(sectionPath: string, trigger: string): string {
return [
`> **STOP.** Before ${trigger}, Read \`${sectionPath}\` and execute it`,
`> in full. Do not work from memory — that section is the source of truth for this step.`,
].join('\n');
}
/**
* SECTION:id pointer on Claude/Grok, inline on other hosts.
*/
export const SECTION: ResolverFn = (ctx: TemplateContext, args?: string[]): string => {
const id = args?.[0];
if (!id) throw new Error('{{SECTION:id}} requires a section id');
const entry = findSection(ctx.skillName, id);
if (ctx.host === 'claude') {
const sectionPath = `${ctx.paths.skillRoot}/${ctx.skillName}/sections/${entry.file}`;
return [
`> **STOP.** Before ${entry.trigger}, Read \`${sectionPath}\` and execute it`,
`> in full. Do not work from memory — that section is the source of truth for this step.`,
].join('\n');
if (hostUsesSectionPointers(ctx.host)) {
const sectionPath = sectionPointerPath(ctx.host, ctx.skillName, entry.file, ctx.paths.skillRoot);
return stopReadDirective(sectionPath, entry.trigger);
}
// Non-Claude hosts inline the section template content (monolith preserved).
// Non-pointer hosts inline the section template content (monolith preserved).
// Inner {{RESOLVER}} tokens are expanded by the generator's multi-pass resolve.
const tmplPath = path.join(ROOT, ctx.skillName, 'sections', `${entry.file}.tmpl`);
return fs.readFileSync(tmplPath, 'utf-8').trimEnd();
};
/**
* {{SECTION_INDEX:skill}} situationsection table from the passive manifest.
* Claude only; other hosts inline everything so an index would be noise.
* SECTION_INDEX situation-to-section table from the passive manifest.
* Pointer hosts only; inline hosts have no separate section files.
*/
export const SECTION_INDEX: ResolverFn = (ctx: TemplateContext, args?: string[]): string => {
if (ctx.host !== 'claude') return '';
if (!hostUsesSectionPointers(ctx.host)) return '';
const skill = args?.[0] ?? ctx.skillName;
const manifest = loadManifest(skill);
const lines: string[] = [
@ -90,7 +129,11 @@ export const SECTION_INDEX: ResolverFn = (ctx: TemplateContext, args?: string[])
'|------|-------------------|',
];
for (const s of manifest.sections) {
lines.push(`| ${s.trigger} | \`sections/${s.file}\` |`);
const sectionPath = sectionPointerPath(ctx.host, skill, s.file, ctx.paths.skillRoot);
// Table shows the resolvable path (Grok: full ~/.grok/...; Claude: short sections/)
const display =
ctx.host === 'grok-build' ? `\`${sectionPath}\`` : `\`sections/${s.file}\``;
lines.push(`| ${s.trigger} | ${display} |`);
}
return lines.join('\n');
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
/**
* Grok Build section-pointer mode (Option A token-ceiling fix).
*
* Carved skills must emit STOP-Read pointers + separate section files under
* .grok/skills/gstack-<skill>/sections/, not a monolith that exceeds the
* gen-skill-docs soft ceiling (~160KB / ~40k tokens).
*/
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import {
hostUsesSectionPointers,
externalSkillPackageName,
sectionPointerPath,
SECTION,
SECTION_INDEX,
} from '../scripts/resolvers/sections';
import type { TemplateContext } from '../scripts/resolvers/types';
import { HOST_PATHS } from '../scripts/resolvers/types';
const ROOT = path.resolve(import.meta.dir, '..');
function shipCtx(host: 'claude' | 'grok-build' | 'codex'): TemplateContext {
return {
skillName: 'ship',
tmplPath: path.join(ROOT, 'ship', 'SKILL.md.tmpl'),
host,
paths: HOST_PATHS[host],
};
}
describe('hostUsesSectionPointers', () => {
test('claude and grok-build use pointers; codex inlines', () => {
expect(hostUsesSectionPointers('claude')).toBe(true);
expect(hostUsesSectionPointers('grok-build')).toBe(true);
expect(hostUsesSectionPointers('codex')).toBe(false);
expect(hostUsesSectionPointers('factory')).toBe(false);
});
});
describe('sectionPointerPath', () => {
test('grok uses flat package under ~/.grok/skills', () => {
expect(sectionPointerPath('grok-build', 'ship', 'tests.md', '$GSTACK_ROOT')).toBe(
'~/.grok/skills/gstack-ship/sections/tests.md',
);
expect(externalSkillPackageName('ship')).toBe('gstack-ship');
expect(externalSkillPackageName('gstack-upgrade')).toBe('gstack-upgrade');
});
test('claude uses nested skillRoot layout', () => {
expect(sectionPointerPath('claude', 'ship', 'tests.md', '~/.claude/skills/gstack')).toBe(
'~/.claude/skills/gstack/ship/sections/tests.md',
);
});
});
describe('SECTION / SECTION_INDEX resolvers', () => {
test('grok ship SECTION emits STOP-Read to gstack-ship package path', () => {
const out = SECTION(shipCtx('grok-build'), ['tests']);
expect(out).toContain('**STOP.**');
expect(out).toContain('~/.grok/skills/gstack-ship/sections/tests.md');
expect(out).not.toContain('## Test Framework Bootstrap'); // not inlined
});
test('codex ship SECTION still inlines section body', () => {
const out = SECTION(shipCtx('codex'), ['tests']);
expect(out).not.toContain('**STOP.**');
// tests.md.tmpl has substantive body (not a pointer)
expect(out.length).toBeGreaterThan(200);
});
test('grok SECTION_INDEX lists full paths; codex SECTION_INDEX is empty', () => {
const grok = SECTION_INDEX(shipCtx('grok-build'), ['ship']);
expect(grok).toContain('## Section index');
expect(grok).toContain('~/.grok/skills/gstack-ship/sections/review-army.md');
expect(SECTION_INDEX(shipCtx('codex'), ['ship'])).toBe('');
});
});
describe('generated grok ship package (when present)', () => {
const shipPkg = path.join(ROOT, '.grok', 'skills', 'gstack-ship');
const skillMd = path.join(shipPkg, 'SKILL.md');
const sectionsDir = path.join(shipPkg, 'sections');
test('gstack-ship SKILL.md is under soft token ceiling when generated', () => {
if (!fs.existsSync(skillMd)) return; // gen not run in this env
const bytes = fs.statSync(skillMd).size;
// Soft ceiling in gen-skill-docs: 160_000 bytes (~40k tokens)
expect(bytes).toBeLessThan(160_000);
});
test('gstack-ship has carved section files + STOP pointers when generated', () => {
if (!fs.existsSync(skillMd)) return;
const body = fs.readFileSync(skillMd, 'utf-8');
expect(body).toContain('**STOP.**');
expect(body).toContain('~/.grok/skills/gstack-ship/sections/');
// Heavy steps must not be inlined into the skeleton
expect(body).not.toMatch(/## Step 9\.1: Review Army/);
expect(fs.existsSync(path.join(sectionsDir, 'review-army.md'))).toBe(true);
expect(fs.existsSync(path.join(sectionsDir, 'adversarial.md'))).toBe(true);
expect(fs.existsSync(path.join(sectionsDir, 'tests.md'))).toBe(true);
});
});