refactor(ship): carve into skeleton + on-demand sections (Claude) (T9)

ship/SKILL.md drops 167KB → 68.7KB (~59% of the always-loaded skill) by moving
8 prose-heavy steps into ship/sections/*.md, read on demand:
tests, test-coverage, plan-completion, review-army, greptile, adversarial,
changelog, pr-body. Step 12's version logic now calls the tested
gstack-version-bump CLI instead of inline bash.

Claude-first (S2): {{SECTION:id}} emits a STOP-Read pointer on Claude (skeleton +
generated section files) and INLINES the content on every other host, so external
hosts keep the full monolith — verified factory at 162KB with no sections dir.
{{SECTION_INDEX:ship}} renders the situation→section table from the PASSIVE
manifest (CM2 / v2_PLAN.md:663); required-reads live only in test fixtures.
Multi-pass resolve expands inlined sections' own resolvers.

Parity: ship invariant flipped to sectioned (union content checks + maxSkeletonBytes
asserts the shrink). Carve-fallout fixed across gen-skill-docs/skill-validation/
golden/plan-completion/#1539/size-budget tests via skeleton+sections union reads.
Free suite green except the pre-existing investigate parity drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-30 10:11:00 -07:00
parent d9749f7375
commit 261d57a8e3
31 changed files with 2814 additions and 4739 deletions
+33 -17
View File
@@ -563,17 +563,31 @@ function resolvePlaceholders(
relTmplPath: string,
): string {
const suppressed = new Set(hostConfig.suppressedResolvers || []);
const content = tmplContent.replace(/\{\{(\w+(?::[^}]+)?)\}\}/g, (_match, fullKey) => {
const parts = fullKey.split(':');
const resolverName = parts[0];
const args = parts.slice(1);
if (suppressed.has(resolverName)) return '';
const entry = RESOLVERS[resolverName];
if (!entry) throw new Error(`Unknown placeholder {{${resolverName}}} in ${relTmplPath}`);
const { resolve, appliesTo } = unwrapResolver(entry);
if (appliesTo && !appliesTo(ctx)) return '';
return args.length > 0 ? resolve(ctx, args) : resolve(ctx);
});
const onePass = (input: string): string =>
input.replace(/\{\{(\w+(?::[^}]+)?)\}\}/g, (_match, fullKey) => {
const parts = fullKey.split(':');
const resolverName = parts[0];
const args = parts.slice(1);
if (suppressed.has(resolverName)) return '';
const entry = RESOLVERS[resolverName];
if (!entry) throw new Error(`Unknown placeholder {{${resolverName}}} in ${relTmplPath}`);
const { resolve, appliesTo } = unwrapResolver(entry);
if (appliesTo && !appliesTo(ctx)) return '';
return args.length > 0 ? resolve(ctx, args) : resolve(ctx);
});
// Multi-pass: a resolver may emit content that itself contains {{TOKENS}} — the
// {{SECTION:id}} resolver inlines a section template (with its own resolvers)
// for non-Claude hosts. .replace() doesn't re-scan inserted text, so loop until
// the output stabilizes. Bounded to avoid an infinite loop if a resolver ever
// emits its own placeholder; 6 passes is far more nesting than any skill needs.
let content = tmplContent;
for (let pass = 0; pass < 6; pass++) {
const next = onePass(content);
if (next === content) break;
content = next;
}
const remaining = content.match(/\{\{(\w+(?::[^}]+)?)\}\}/g);
if (remaining) {
throw new Error(`Unresolved placeholders in ${relTmplPath}: ${remaining.join(', ')}`);
@@ -878,12 +892,14 @@ for (const currentHost of hostsToRun) {
}
}
// ─── Section generation (v2 plan T9) ───────────────────────
// On-demand sections/*.md for carved skills. No-op for any skill without a
// sections/ dir, so this is inert until a skill is carved. Mirrors the
// SKILL.md include/skip host filters (keyed on the owning skill dir) and the
// DRY_RUN freshness handling, so sections participate in the freshness gate.
for (const sec of discoverSectionTemplates(ROOT)) {
// ─── 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) : []) {
if (currentHostConfig.generation.includeSkills?.length &&
!currentHostConfig.generation.includeSkills.includes(sec.skillDir)) continue;
if (currentHostConfig.generation.skipSkills?.length &&