Skill Tracer
Skills › gstack › gstack › types.ts

types.ts

153 lines · scripts/resolvers/types.ts at 3f79d81

1import { ALL_HOST_CONFIGS } from '../../hosts/index';
2
3/**
4 * Host type — derived from host configs in hosts/*.ts.
5 * Adding a new host: create hosts/myhost.ts + add to hosts/index.ts.
6 * Do NOT hardcode host names here.
7 */
8export type Host = (typeof ALL_HOST_CONFIGS)[number]['name'];
9
10export interface HostPaths {
11 skillRoot: string;
12 localSkillRoot: string;
13 binDir: string;
14 browseDir: string;
15 designDir: string;
16 makePdfDir: string;
19/**
20 * Make a host path safe to interpolate INSIDE DOUBLE QUOTES in generated bash.
21 *
22 * Tilde-based hosts (Claude, factory) resolve to paths like
23 * `~/.claude/skills/gstack/bin`. Bash only performs tilde expansion when the
24 * `~` is UNQUOTED, so `"~/.claude/..."` is a literal relative path that never
25 * resolves. A `[ -x "~/..." ]` test is therefore always false and a
26 * `"~/..." --flag` invocation always fails — the surrounding block silently
27 * becomes dead code rather than erroring.
28 *
29 * Env-var hosts already use `$GSTACK_BIN`, which expands correctly when
30 * quoted, so they pass through untouched.
31 *
32 * Use this ONLY where the path lands inside double quotes. Unquoted
33 * interpolations (`${ctx.paths.binDir}/gstack-slug`) expand fine as-is and are
34 * left alone so generated docs keep the more readable `~`.
35 */
36export function quoteSafePath(hostPath: string): string {
37 return hostPath.startsWith('~/') ? `$HOME/${hostPath.slice(2)}` : hostPath;
40/**
41 * HOST_PATHS — derived from host configs.
42 * Each config's globalRoot/localSkillRoot determines the path structure.
43 * Non-Claude hosts use $GSTACK_ROOT env vars (set by preamble).
44 */
45function buildHostPaths(): Record<string, HostPaths> {
46 const paths: Record<string, HostPaths> = {};
47 for (const config of ALL_HOST_CONFIGS) {
48 if (config.usesEnvVars) {
49 paths[config.name] = {
50 skillRoot: '$GSTACK_ROOT',
51 localSkillRoot: config.localSkillRoot,
52 binDir: '$GSTACK_BIN',
53 browseDir: '$GSTACK_BROWSE',
54 designDir: '$GSTACK_DESIGN',
55 makePdfDir: '$GSTACK_MAKE_PDF',
56 };
57 } else {
58 const root = `~/${config.globalRoot}`;
59 paths[config.name] = {
60 skillRoot: root,
61 localSkillRoot: config.localSkillRoot,
62 binDir: `${root}/bin`,
63 browseDir: `${root}/browse/dist`,
64 designDir: `${root}/design/dist`,
65 makePdfDir: `${root}/make-pdf/dist`,
66 };
67 }
68 }
69 return paths;
72export const HOST_PATHS: Record<string, HostPaths> = buildHostPaths();
74/**
75 * Render a HostPaths binary dir as a shell-expandable absolute path.
76 * Claude-style dirs are `~`-rooted (e.g. `~/.claude/skills/gstack/browse/dist`)
77 * and expand via `$HOME`; env-var hosts already carry an absolute `$GSTACK_*`
78 * value, so they pass through untouched — prepending `$HOME` would double it.
79 */
80export function toShellPath(dir: string): string {
81 return dir.startsWith('~') ? `$HOME${dir.slice(1)}` : dir;
84import type { Model } from '../models';
85export type { Model } from '../models';
87export interface TemplateContext {
88 skillName: string;
89 tmplPath: string;
90 benefitsFrom?: string[];
91 host: Host;
92 paths: HostPaths;
93 preambleTier?: number; // 1-4, controls which preamble sections are included
94 model?: Model; // model family for behavioral overlay. Omitted/undefined → no overlay.
95 interactive?: boolean; // true → emit plan-mode handshake in preamble. Generator-only, not written to SKILL.md.
96 /**
97 * Build-time compression mode. Defaults to 'default'.
98 *
99 * - 'default': full preamble prose ships as today (writing style, completeness,
100 * confusion protocol, context health are all present).
101 * - 'terse': writing-style + completeness + confusion-protocol + context-health
102 * sections are compressed to a one-line pointer at gen time. Saves ~2.6 KB
103 * per tier-2+ skill (measured 2,611 B on the v1.71 render — re-measure with
104 * a terse vs default build diff if the preamble changes shape again).
105 * Opt-in via `--explain-level=terse` build flag for
106 * users who want shipped skills to match their runtime preference and
107 * avoid the per-session terse-mode prose.
108 *
109 * Default builds keep the runtime-conditional behavior intact (Writing Style
110 * section says "skip entirely if EXPLAIN_LEVEL: terse appears in preamble echo").
111 * Terse builds make the compression structural — bytes never ship in the first place.
112 */
113 explainLevel?: 'default' | 'terse';
114 /**
115 * Install-context render contract (docs/ADDING_A_HOST.md): absolute install
116 * root a per-install render serves (`gen-skill-docs --install-root`).
117 * null/undefined = the host's default root (committed render, unchanged bytes).
118 */
119 installRoot?: string | null;
120 /**
121 * Plan-mode preamble block. Defaults to true (block ships).
122 * false (opt-in via `--plan-mode=off`): the "## Plan Mode Safe Operations" +
123 * "## Skill Invocation During Plan Mode" section is dropped from the
124 * preamble. "PLAN MODE EXCEPTION — ALWAYS RUN" markers inside skill bodies
125 * still apply.
126 */
127 planMode?: boolean;
128 /**
129 * Module loadout filter (modules/loadouts.json, gen-skill-docs --loadout/--modules).
130 * Undefined = the `full` loadout: every module renders and committed bytes are unchanged.
131 */
132 modules?: ModuleFilter;
135/** A resolved loadout: which module ids it leaves out, and which ids can never be left out. */
136export interface ModuleFilter {
137 /** Loadout name, or `adhoc` for a --modules spec. */
138 name: string;
139 excluded: ReadonlySet<string>;
140 pinned: ReadonlySet<string>;
141 /** Unpinned preamble pieces render as if the skill's preamble-tier were at most this. */
142 preambleTierMax?: number;
145/** Resolver function signature. args is populated for parameterized placeholders like {{INVOKE_SKILL:name}}. */
146export type ResolverFn = (ctx: TemplateContext, args?: string[]) => string;
148// NOTE: a gated-resolver mechanism (ResolverEntry { resolve, appliesTo } +
149// unwrapResolver) lived here, fully built and tested — and never used by a
150// single one of the 65 registry entries. Per-skill gating happens either at
151// the template level ({{NAME}} is already conditional) or, where it truly
152// exists, via explicit ctx.skillName branches inside resolvers. Deleted
153// rather than kept as speculative API.