Files
gstack/scripts/resolvers/types.ts
Garry Tanandcapy-ai[bot] f4f16e83e0 v1.91.305.0 feat: render gstack per model with modules left out, measured before any model gets less (GSTA-140 PR 1a) (#3554)
* feat(modules): module catalog, per-model loadouts manifest and gstack-modules render (GSTA-140 PR 1a)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(modules): cover bin/gstack-modules and the loadouts schema; allow modules/ in pregate strays

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(modules): gen:skill-docs refuses an exclusion render without --out-dir; render tests set up their own arm

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(modules): skill exclusions honor pinned sections and section references; own-property loadout lookups; llms.txt follows the selection; ownership rechecked before swap

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: bump version and changelog (v1.91.296.0)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: loadout render cost and the modules/ directory in PROJECT_STRUCTURE

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: unstamp #3554 (slot calls stamp); CHANGELOG entry back under [Unreleased]

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-29 04:22:41 +00:00

154 lines
6.1 KiB
TypeScript

import { ALL_HOST_CONFIGS } from '../../hosts/index';
/**
* Host type — derived from host configs in hosts/*.ts.
* Adding a new host: create hosts/myhost.ts + add to hosts/index.ts.
* Do NOT hardcode host names here.
*/
export type Host = (typeof ALL_HOST_CONFIGS)[number]['name'];
export interface HostPaths {
skillRoot: string;
localSkillRoot: string;
binDir: string;
browseDir: string;
designDir: string;
makePdfDir: string;
}
/**
* Make a host path safe to interpolate INSIDE DOUBLE QUOTES in generated bash.
*
* Tilde-based hosts (Claude, factory) resolve to paths like
* `~/.claude/skills/gstack/bin`. Bash only performs tilde expansion when the
* `~` is UNQUOTED, so `"~/.claude/..."` is a literal relative path that never
* resolves. A `[ -x "~/..." ]` test is therefore always false and a
* `"~/..." --flag` invocation always fails — the surrounding block silently
* becomes dead code rather than erroring.
*
* Env-var hosts already use `$GSTACK_BIN`, which expands correctly when
* quoted, so they pass through untouched.
*
* Use this ONLY where the path lands inside double quotes. Unquoted
* interpolations (`${ctx.paths.binDir}/gstack-slug`) expand fine as-is and are
* left alone so generated docs keep the more readable `~`.
*/
export function quoteSafePath(hostPath: string): string {
return hostPath.startsWith('~/') ? `$HOME/${hostPath.slice(2)}` : hostPath;
}
/**
* HOST_PATHS — derived from host configs.
* Each config's globalRoot/localSkillRoot determines the path structure.
* Non-Claude hosts use $GSTACK_ROOT env vars (set by preamble).
*/
function buildHostPaths(): Record<string, HostPaths> {
const paths: Record<string, HostPaths> = {};
for (const config of ALL_HOST_CONFIGS) {
if (config.usesEnvVars) {
paths[config.name] = {
skillRoot: '$GSTACK_ROOT',
localSkillRoot: config.localSkillRoot,
binDir: '$GSTACK_BIN',
browseDir: '$GSTACK_BROWSE',
designDir: '$GSTACK_DESIGN',
makePdfDir: '$GSTACK_MAKE_PDF',
};
} else {
const root = `~/${config.globalRoot}`;
paths[config.name] = {
skillRoot: root,
localSkillRoot: config.localSkillRoot,
binDir: `${root}/bin`,
browseDir: `${root}/browse/dist`,
designDir: `${root}/design/dist`,
makePdfDir: `${root}/make-pdf/dist`,
};
}
}
return paths;
}
export const HOST_PATHS: Record<string, HostPaths> = buildHostPaths();
/**
* Render a HostPaths binary dir as a shell-expandable absolute path.
* Claude-style dirs are `~`-rooted (e.g. `~/.claude/skills/gstack/browse/dist`)
* and expand via `$HOME`; env-var hosts already carry an absolute `$GSTACK_*`
* value, so they pass through untouched — prepending `$HOME` would double it.
*/
export function toShellPath(dir: string): string {
return dir.startsWith('~') ? `$HOME${dir.slice(1)}` : dir;
}
import type { Model } from '../models';
export type { Model } from '../models';
export interface TemplateContext {
skillName: string;
tmplPath: string;
benefitsFrom?: string[];
host: Host;
paths: HostPaths;
preambleTier?: number; // 1-4, controls which preamble sections are included
model?: Model; // model family for behavioral overlay. Omitted/undefined → no overlay.
interactive?: boolean; // true → emit plan-mode handshake in preamble. Generator-only, not written to SKILL.md.
/**
* Build-time compression mode. Defaults to 'default'.
*
* - 'default': full preamble prose ships as today (writing style, completeness,
* confusion protocol, context health are all present).
* - 'terse': writing-style + completeness + confusion-protocol + context-health
* sections are compressed to a one-line pointer at gen time. Saves ~2.6 KB
* per tier-2+ skill (measured 2,611 B on the v1.71 render — re-measure with
* a terse vs default build diff if the preamble changes shape again).
* Opt-in via `--explain-level=terse` build flag for
* users who want shipped skills to match their runtime preference and
* avoid the per-session terse-mode prose.
*
* Default builds keep the runtime-conditional behavior intact (Writing Style
* section says "skip entirely if EXPLAIN_LEVEL: terse appears in preamble echo").
* Terse builds make the compression structural — bytes never ship in the first place.
*/
explainLevel?: 'default' | 'terse';
/**
* Install-context render contract (docs/ADDING_A_HOST.md): absolute install
* root a per-install render serves (`gen-skill-docs --install-root`).
* null/undefined = the host's default root (committed render, unchanged bytes).
*/
installRoot?: string | null;
/**
* Plan-mode preamble block. Defaults to true (block ships).
* false (opt-in via `--plan-mode=off`): the "## Plan Mode Safe Operations" +
* "## Skill Invocation During Plan Mode" section is dropped from the
* preamble. "PLAN MODE EXCEPTION — ALWAYS RUN" markers inside skill bodies
* still apply.
*/
planMode?: boolean;
/**
* Module loadout filter (modules/loadouts.json, gen-skill-docs --loadout/--modules).
* Undefined = the `full` loadout: every module renders and committed bytes are unchanged.
*/
modules?: ModuleFilter;
}
/** A resolved loadout: which module ids it leaves out, and which ids can never be left out. */
export interface ModuleFilter {
/** Loadout name, or `adhoc` for a --modules spec. */
name: string;
excluded: ReadonlySet<string>;
pinned: ReadonlySet<string>;
/** Unpinned preamble pieces render as if the skill's preamble-tier were at most this. */
preambleTierMax?: number;
}
/** Resolver function signature. args is populated for parameterized placeholders like {{INVOKE_SKILL:name}}. */
export type ResolverFn = (ctx: TemplateContext, args?: string[]) => string;
// NOTE: a gated-resolver mechanism (ResolverEntry { resolve, appliesTo } +
// unwrapResolver) lived here, fully built and tested — and never used by a
// single one of the 65 registry entries. Per-skill gating happens either at
// the template level ({{NAME}} is already conditional) or, where it truly
// exists, via explicit ctx.skillName branches inside resolvers. Deleted
// rather than kept as speculative API.