Files
gstack/scripts/resolvers/types.ts
T
Garry Tan 5e4895c90a feat: plan-mode handshake for interactive review skills
Add a preamble-level STOP-Ask handshake that fires when the user invokes any
of the 4 interactive review skills (plan-ceo-review, plan-eng-review,
plan-design-review, plan-devex-review) while their Claude Code session is
in plan mode. Without this gate, plan mode's "this supercedes any other
instructions" system-reminder outranked the skills' interactive STOP gates
and the skills silently wrote plan files without any per-finding AskUserQuestion.

The handshake offers 2 options (exit-and-rerun, cancel) — the original
third "stay and batch" option was dropped after two independent reviewers
flagged it as a silent bypass of the skills' anti-skip rule.

Architecture decisions (CEO+Eng review):
- Preamble-level resolver, not per-template injection (Codex finding #2)
- Position 1 in preamble composition: after bash block (_SESSION_ID live),
  before onboarding AskUserQuestion gates (so fresh-install users see the
  handshake first, not drowned in telemetry/proactive/routing prompts)
- Generator-only `interactive: true` frontmatter flag, following the
  `preamble-tier` precedent (no host-config frontmatter allowlist edits)
- Host-scoped to Claude via `ctx.host === 'claude'` check inside the
  resolver (simpler than `suppressedResolvers` which only gates `{{}}`
  placeholders)
- One-way-door classification in scripts/question-registry.ts for all 4
  skills so question-tuning `never-ask` preferences can't suppress the gate
- Synchronous telemetry write to ~/.gstack/analytics/skill-usage.jsonl on
  handshake fire (captures A-exit and C-cancel outcomes that terminate the
  skill before end-of-run telemetry runs)

Also adds an explicit STOP block to plan-ceo-review Step 0C-bis so the
approach-selection question can't silently skip to mode selection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 23:40:36 -07:00

69 lines
2.2 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;
}
/**
* 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();
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.
}
/** Resolver function signature. args is populated for parameterized placeholders like {{INVOKE_SKILL:name}}. */
export type ResolverFn = (ctx: TemplateContext, args?: string[]) => string;