mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-22 17:49:57 +02:00
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:
+33
-17
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user