feat: design taste engine with persistent schema

Adds a cross-session taste profile that learns from design-shotgun
approval/rejection decisions. Biases future design-consultation and
design-shotgun proposals toward the user's demonstrated preferences.

Codex review caught that the plan had "taste engine" as a vague goal
without schema, decay, migration, or placeholder insertion points. This
commit ships the full spec.

Schema v1 at ~/.gstack/projects/$SLUG/taste-profile.json:
- version, updated_at
- dimensions: fonts, colors, layouts, aesthetics — each with approved[]
  and rejected[] preference lists
- sessions: last 50 (FIFO truncation), each with ts/action/variant/reason
- Preference: { value, confidence, approved_count, rejected_count, last_seen }
- Confidence: Laplace-smoothed approved/(total+1)
- Decay: 5% per week of inactivity, computed at read time (not write)

Changes:
- bin/gstack-taste-update: new CLI. Subcommands approved/rejected/show/
  migrate. Parses reason string for dimension signals (e.g.,
  "fonts: Geist; colors: slate; aesthetics: minimal"). Emits taste-drift
  NOTE when a new signal contradicts a strong opposing signal. Legacy
  approved.json aggregates migrate to v1 on next write.
- scripts/resolvers/design.ts: new generateTasteProfile() resolver.
  Produces the prose that skills see: how to read the profile, how to
  factor into proposals, conflict handling, schema migration.
- scripts/resolvers/index.ts: register TASTE_PROFILE and a BIN_DIR
  resolver (returns ctx.paths.binDir, used by templates that shell out
  to gstack-* binaries).
- design-consultation/SKILL.md.tmpl: insert {{TASTE_PROFILE}} placeholder
  in Phase 1 right after the memorable-thing forcing question so the
  Phase 3 proposal can factor in learned preferences.
- design-shotgun/SKILL.md.tmpl: taste memory section now reads
  taste-profile.json via {{TASTE_PROFILE}}, falls back to per-session
  approved.json (legacy). Approval flow documented to call
  gstack-taste-update after user picks/rejects a variant.

Known gap: v1 extracts dimension signals from a reason string passed
by the caller ("fonts: X; colors: Y"). Future v2 can read EXIF or an
accompanying manifest written by design-shotgun alongside each variant
for automatic dimension extraction without needing the reason argument.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-17 06:10:54 +08:00
parent c1d6fd8706
commit 9e95a9dc50
7 changed files with 466 additions and 13 deletions

293
bin/gstack-taste-update Executable file
View File

@@ -0,0 +1,293 @@
#!/usr/bin/env bun
// gstack-taste-update — update the persistent taste profile at
// ~/.gstack/projects/$SLUG/taste-profile.json
//
// Usage:
// gstack-taste-update approved <variant-path> [--reason "<why>"]
// gstack-taste-update rejected <variant-path> [--reason "<why>"]
// gstack-taste-update show — print current profile summary
// gstack-taste-update migrate — upgrade legacy approved.json to v1
//
// Schema v1 at ~/.gstack/projects/$SLUG/taste-profile.json:
//
// {
// "version": 1,
// "updated_at": "<ISO 8601>",
// "dimensions": {
// "fonts": { "approved": [...], "rejected": [...] },
// "colors": { "approved": [...], "rejected": [...] },
// "layouts": { "approved": [...], "rejected": [...] },
// "aesthetics": { "approved": [...], "rejected": [...] }
// },
// "sessions": [ // last 50 only — truncated via decay
// { "ts": "<ISO>", "action": "approved"|"rejected", "variant": "<path>", "reason": "<optional>" }
// ]
// }
//
// Each Preference entry:
// { value: string, confidence: number (0-1), approved_count, rejected_count, last_seen }
//
// Confidence is computed with Laplace smoothing + 5% weekly decay at read time.
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
const STATE_DIR = process.env.GSTACK_STATE_DIR || path.join(process.env.HOME || '/', '.gstack');
const SCHEMA_VERSION = 1;
const SESSION_CAP = 50;
const DECAY_PER_WEEK = 0.05;
type Dimension = 'fonts' | 'colors' | 'layouts' | 'aesthetics';
const DIMENSIONS: Dimension[] = ['fonts', 'colors', 'layouts', 'aesthetics'];
interface Preference {
value: string;
confidence: number;
approved_count: number;
rejected_count: number;
last_seen: string;
}
interface SessionRecord {
ts: string;
action: 'approved' | 'rejected';
variant: string;
reason?: string;
}
interface TasteProfile {
version: number;
updated_at: string;
dimensions: Record<Dimension, { approved: Preference[]; rejected: Preference[] }>;
sessions: SessionRecord[];
}
function getSlug(): string {
try {
const output = execSync('git rev-parse --show-toplevel', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
return path.basename(output);
} catch {
return 'unknown';
}
}
function profilePath(slug: string): string {
return path.join(STATE_DIR, 'projects', slug, 'taste-profile.json');
}
function emptyProfile(): TasteProfile {
return {
version: SCHEMA_VERSION,
updated_at: new Date().toISOString(),
dimensions: {
fonts: { approved: [], rejected: [] },
colors: { approved: [], rejected: [] },
layouts: { approved: [], rejected: [] },
aesthetics: { approved: [], rejected: [] },
},
sessions: [],
};
}
function load(slug: string): TasteProfile {
const p = profilePath(slug);
if (!fs.existsSync(p)) return emptyProfile();
try {
const raw = JSON.parse(fs.readFileSync(p, 'utf-8'));
if (!raw.version || raw.version < SCHEMA_VERSION) {
return migrate(raw);
}
return raw as TasteProfile;
} catch (err) {
console.error(`WARN: could not parse ${p}:`, (err as Error).message);
return emptyProfile();
}
}
function save(slug: string, profile: TasteProfile): void {
const p = profilePath(slug);
fs.mkdirSync(path.dirname(p), { recursive: true });
profile.updated_at = new Date().toISOString();
fs.writeFileSync(p, JSON.stringify(profile, null, 2) + '\n');
}
/**
* Migrate a legacy profile (no version or version < SCHEMA_VERSION) into the
* current schema, preserving data where possible. Legacy approved.json aggregates
* get normalized into empty-but-valid v1 profiles so the next write populates them.
*/
function migrate(legacy: unknown): TasteProfile {
const fresh = emptyProfile();
if (legacy && typeof legacy === 'object') {
const anyLegacy = legacy as Record<string, unknown>;
// Preserve sessions if present
if (Array.isArray(anyLegacy.sessions)) {
fresh.sessions = anyLegacy.sessions.slice(-SESSION_CAP) as SessionRecord[];
}
// Preserve dimensions if present and well-formed
if (anyLegacy.dimensions && typeof anyLegacy.dimensions === 'object') {
for (const dim of DIMENSIONS) {
const src = (anyLegacy.dimensions as Record<string, unknown>)[dim];
if (src && typeof src === 'object') {
const ss = src as Record<string, unknown>;
if (Array.isArray(ss.approved)) fresh.dimensions[dim].approved = ss.approved as Preference[];
if (Array.isArray(ss.rejected)) fresh.dimensions[dim].rejected = ss.rejected as Preference[];
}
}
}
}
return fresh;
}
/**
* Apply 5% per-week decay to confidence values at read/show time.
* Returns a copy; does NOT mutate or persist the input.
*/
function applyDecay(profile: TasteProfile): TasteProfile {
const now = Date.now();
const decayed = JSON.parse(JSON.stringify(profile)) as TasteProfile;
for (const dim of DIMENSIONS) {
for (const bucket of ['approved', 'rejected'] as const) {
for (const pref of decayed.dimensions[dim][bucket]) {
const lastSeen = new Date(pref.last_seen).getTime();
const weeks = Math.max(0, (now - lastSeen) / (7 * 24 * 60 * 60 * 1000));
pref.confidence = Math.max(0, pref.confidence * Math.pow(1 - DECAY_PER_WEEK, weeks));
}
}
}
return decayed;
}
/**
* Extract dimension values from a variant description. V1 keeps this simple:
* the variant is a path/name like "variant-A" — we can't extract real design
* tokens without the mockup's metadata. Callers should pass a reason string
* that mentions fonts/colors/layouts/aesthetics. If the reason is missing,
* the session is recorded but dimensions don't get updated.
*
* Future v2: parse the variant PNG's EXIF, or read an accompanying manifest
* that design-shotgun writes next to each variant.
*/
function extractSignals(reason?: string): Partial<Record<Dimension, string[]>> {
if (!reason) return {};
const out: Partial<Record<Dimension, string[]>> = {};
// naive pattern: "fonts: X, Y; colors: Z" — split by dimension label
const labelRe = /(fonts|colors|layouts|aesthetics):\s*([^;]+)/gi;
let m: RegExpExecArray | null;
while ((m = labelRe.exec(reason)) !== null) {
const dim = m[1].toLowerCase() as Dimension;
const values = m[2].split(',').map(s => s.trim()).filter(Boolean);
out[dim] = values;
}
return out;
}
function bumpPref(list: Preference[], value: string, opposite: Preference[], action: 'approved' | 'rejected'): Preference[] {
const now = new Date().toISOString();
let entry = list.find(p => p.value.toLowerCase() === value.toLowerCase());
if (!entry) {
entry = { value, confidence: 0, approved_count: 0, rejected_count: 0, last_seen: now };
list.push(entry);
}
if (action === 'approved') {
entry.approved_count += 1;
} else {
entry.rejected_count += 1;
}
entry.last_seen = now;
// Laplace-smoothed confidence
const total = entry.approved_count + entry.rejected_count;
entry.confidence = entry.approved_count / (total + 1);
// Flag conflict if the opposite bucket has a strong entry for this value
const opp = opposite.find(p => p.value.toLowerCase() === value.toLowerCase());
if (opp && opp.approved_count + opp.rejected_count >= 3 && opp.confidence >= 0.6) {
console.error(`NOTE: taste drift — "${value}" previously ${action === 'approved' ? 'rejected' : 'approved'} with confidence ${opp.confidence.toFixed(2)}. Keep both signals; aggregate confidence will rebalance.`);
}
return list;
}
function cmdUpdate(action: 'approved' | 'rejected', variant: string, reason?: string): void {
const slug = getSlug();
const profile = load(slug);
const signals = extractSignals(reason);
for (const dim of DIMENSIONS) {
const values = signals[dim];
if (!values) continue;
const bucket = profile.dimensions[dim][action];
const opposite = profile.dimensions[dim][action === 'approved' ? 'rejected' : 'approved'];
for (const v of values) bumpPref(bucket, v, opposite, action);
}
// Always record the session even if no dimensions were extracted
profile.sessions.push({ ts: new Date().toISOString(), action, variant, reason });
// Truncate sessions to last SESSION_CAP entries (FIFO)
if (profile.sessions.length > SESSION_CAP) {
profile.sessions = profile.sessions.slice(-SESSION_CAP);
}
save(slug, profile);
console.log(`${action}: ${variant} → ${profilePath(slug)}`);
}
function cmdShow(): void {
const slug = getSlug();
const profile = applyDecay(load(slug));
console.log(`taste-profile.json (slug: ${slug}, sessions: ${profile.sessions.length})`);
for (const dim of DIMENSIONS) {
const top = [...profile.dimensions[dim].approved]
.sort((a, b) => b.confidence * b.approved_count - a.confidence * a.approved_count)
.slice(0, 3);
const topRej = [...profile.dimensions[dim].rejected]
.sort((a, b) => b.confidence * b.rejected_count - a.confidence * a.rejected_count)
.slice(0, 3);
if (top.length || topRej.length) {
console.log(`\n[${dim}]`);
if (top.length) {
console.log(' approved (decayed):');
for (const p of top) console.log(` ${p.value} — conf ${p.confidence.toFixed(2)} (+${p.approved_count}/-${p.rejected_count})`);
}
if (topRej.length) {
console.log(' rejected:');
for (const p of topRej) console.log(` ${p.value} — conf ${p.confidence.toFixed(2)} (+${p.approved_count}/-${p.rejected_count})`);
}
}
}
}
function cmdMigrate(): void {
const slug = getSlug();
const profile = load(slug);
save(slug, profile);
console.log(`migrated taste profile to v${SCHEMA_VERSION} at ${profilePath(slug)}`);
}
// ─── CLI entry ────────────────────────────────────────────────
const args = process.argv.slice(2);
const cmd = args[0];
switch (cmd) {
case 'approved':
case 'rejected': {
const variant = args[1];
if (!variant) {
console.error(`Usage: gstack-taste-update ${cmd} <variant-path> [--reason "<why>"]`);
process.exit(1);
}
const reasonIdx = args.indexOf('--reason');
const reason = reasonIdx >= 0 ? args[reasonIdx + 1] : undefined;
cmdUpdate(cmd as 'approved' | 'rejected', variant, reason);
break;
}
case 'show':
cmdShow();
break;
case 'migrate':
cmdMigrate();
break;
default:
console.error('Usage: gstack-taste-update {approved|rejected|show|migrate} [args]');
process.exit(1);
}

View File

@@ -859,6 +859,54 @@ a posture ("for builders, not managers"). Write it down. Every subsequent design
decision should serve this memorable thing. Design that tries to be memorable for decision should serve this memorable thing. Design that tries to be memorable for
everything is memorable for nothing. everything is memorable for nothing.
### Taste profile (if this user has prior sessions)
Read the persistent taste profile if it exists:
```bash
_TASTE_PROFILE=~/.gstack/projects/$SLUG/taste-profile.json
if [ -f "$_TASTE_PROFILE" ]; then
# Schema v1: { dimensions: { fonts, colors, layouts, aesthetics }, sessions: [] }
# Each dimension has approved[] and rejected[] entries with
# { value, confidence, approved_count, rejected_count, last_seen }
# Confidence decays 5% per week of inactivity — computed at read time.
cat "$_TASTE_PROFILE" 2>/dev/null | head -200
echo "TASTE_PROFILE_FOUND"
else
echo "NO_TASTE_PROFILE"
fi
```
**If TASTE_PROFILE_FOUND:** Summarize the strongest signals (top 3 approved entries
per dimension by confidence * approved_count). Include them in the design brief:
"Based on \${SESSION_COUNT} prior sessions, this user's taste leans toward:
fonts [top-3], colors [top-3], layouts [top-3], aesthetics [top-3]. Bias
generation toward these unless the user explicitly requests a different direction.
Also avoid their strong rejections: [top-3 rejected per dimension]."
**If NO_TASTE_PROFILE:** Fall through to per-session approved.json files (legacy).
**Conflict handling:** If the current user request contradicts a strong persistent
signal (e.g., "make it playful" when taste profile strongly prefers minimal), flag
it: "Note: your taste profile strongly prefers minimal. You're asking for playful
this time — I'll proceed, but want me to update the taste profile, or treat this
as a one-off?"
**Decay:** Confidence scores decay 5% per week. A font approved 6 months ago with
10 approvals has less weight than one approved last week. The decay calculation
happens at read time, not write time, so the file only grows on change.
**Schema migration:** If the file has no `version` field or `version: 0`, it's
the legacy approved.json aggregate — `~/.claude/skills/gstack/bin/gstack-taste-update`
will migrate it to schema v1 on the next write.
If a taste profile exists for this project, factor it into your Phase 3 proposal.
The profile reflects what the user has actually approved in prior sessions — treat
it as a demonstrated preference, not a constraint. You may still deliberately
depart from it if the product direction demands something different; when you do,
say so explicitly and connect the departure to the memorable-thing answer above.
--- ---
## Phase 2: Research (only if user said yes) ## Phase 2: Research (only if user said yes)

View File

@@ -102,6 +102,16 @@ a posture ("for builders, not managers"). Write it down. Every subsequent design
decision should serve this memorable thing. Design that tries to be memorable for decision should serve this memorable thing. Design that tries to be memorable for
everything is memorable for nothing. everything is memorable for nothing.
### Taste profile (if this user has prior sessions)
{{TASTE_PROFILE}}
If a taste profile exists for this project, factor it into your Phase 3 proposal.
The profile reflects what the user has actually approved in prior sessions — treat
it as a demonstrated preference, not a constraint. You may still deliberately
depart from it if the product direction demands something different; when you do,
say so explicitly and connect the departure to the memorable-thing answer above.
--- ---
## Phase 2: Research (only if user said yes) ## Phase 2: Research (only if user said yes)

View File

@@ -785,7 +785,52 @@ Two rounds max of context gathering, then proceed with what you have and note as
## Step 2: Taste Memory ## Step 2: Taste Memory
Read prior approved designs to bias generation toward the user's demonstrated taste: Read both the persistent taste profile (cross-session) AND the per-session approved
designs to bias generation toward the user's demonstrated taste.
**Persistent taste profile (v1 schema at `~/.gstack/projects/$SLUG/taste-profile.json`):**
Read the persistent taste profile if it exists:
```bash
_TASTE_PROFILE=~/.gstack/projects/$SLUG/taste-profile.json
if [ -f "$_TASTE_PROFILE" ]; then
# Schema v1: { dimensions: { fonts, colors, layouts, aesthetics }, sessions: [] }
# Each dimension has approved[] and rejected[] entries with
# { value, confidence, approved_count, rejected_count, last_seen }
# Confidence decays 5% per week of inactivity — computed at read time.
cat "$_TASTE_PROFILE" 2>/dev/null | head -200
echo "TASTE_PROFILE_FOUND"
else
echo "NO_TASTE_PROFILE"
fi
```
**If TASTE_PROFILE_FOUND:** Summarize the strongest signals (top 3 approved entries
per dimension by confidence * approved_count). Include them in the design brief:
"Based on \${SESSION_COUNT} prior sessions, this user's taste leans toward:
fonts [top-3], colors [top-3], layouts [top-3], aesthetics [top-3]. Bias
generation toward these unless the user explicitly requests a different direction.
Also avoid their strong rejections: [top-3 rejected per dimension]."
**If NO_TASTE_PROFILE:** Fall through to per-session approved.json files (legacy).
**Conflict handling:** If the current user request contradicts a strong persistent
signal (e.g., "make it playful" when taste profile strongly prefers minimal), flag
it: "Note: your taste profile strongly prefers minimal. You're asking for playful
this time — I'll proceed, but want me to update the taste profile, or treat this
as a one-off?"
**Decay:** Confidence scores decay 5% per week. A font approved 6 months ago with
10 approvals has less weight than one approved last week. The decay calculation
happens at read time, not write time, so the file only grows on change.
**Schema migration:** If the file has no `version` field or `version: 0`, it's
the legacy approved.json aggregate — `~/.claude/skills/gstack/bin/gstack-taste-update`
will migrate it to schema v1 on the next write.
**Per-session approved.json files (legacy, still supported):**
```bash ```bash
setopt +o nomatch 2>/dev/null || true setopt +o nomatch 2>/dev/null || true
@@ -793,14 +838,17 @@ _TASTE=$(find ~/.gstack/projects/$SLUG/designs/ -name "approved.json" -maxdepth
``` ```
If prior sessions exist, read each `approved.json` and extract patterns from the If prior sessions exist, read each `approved.json` and extract patterns from the
approved variants. Include a taste summary in the design brief: approved variants. Merge these into the taste-profile.json-derived signal — if the
profile already says "user prefers Geist font" (from aggregated history), the
"The user previously approved designs with these characteristics: [high contrast, approved.json files add the specific recent approval context.
generous whitespace, modern sans-serif typography, etc.]. Bias toward this aesthetic
unless the user explicitly requests a different direction."
Limit to last 10 sessions. Try/catch JSON parse on each (skip corrupted files). Limit to last 10 sessions. Try/catch JSON parse on each (skip corrupted files).
**Updating taste profile after a design-shotgun session:** When the user picks a
variant, call `~/.claude/skills/gstack/bin/gstack-taste-update approved <variant-path>`. When they
explicitly reject a variant, call `~/.claude/skills/gstack/bin/gstack-taste-update rejected <variant-path>`.
The CLI handles schema migration from approved.json, decay, and conflict flagging.
## Step 3: Generate Variants ## Step 3: Generate Variants
Set up the output directory: Set up the output directory:

View File

@@ -116,7 +116,14 @@ Two rounds max of context gathering, then proceed with what you have and note as
## Step 2: Taste Memory ## Step 2: Taste Memory
Read prior approved designs to bias generation toward the user's demonstrated taste: Read both the persistent taste profile (cross-session) AND the per-session approved
designs to bias generation toward the user's demonstrated taste.
**Persistent taste profile (v1 schema at `~/.gstack/projects/$SLUG/taste-profile.json`):**
{{TASTE_PROFILE}}
**Per-session approved.json files (legacy, still supported):**
```bash ```bash
setopt +o nomatch 2>/dev/null || true setopt +o nomatch 2>/dev/null || true
@@ -124,14 +131,17 @@ _TASTE=$(find ~/.gstack/projects/$SLUG/designs/ -name "approved.json" -maxdepth
``` ```
If prior sessions exist, read each `approved.json` and extract patterns from the If prior sessions exist, read each `approved.json` and extract patterns from the
approved variants. Include a taste summary in the design brief: approved variants. Merge these into the taste-profile.json-derived signal — if the
profile already says "user prefers Geist font" (from aggregated history), the
"The user previously approved designs with these characteristics: [high contrast, approved.json files add the specific recent approval context.
generous whitespace, modern sans-serif typography, etc.]. Bias toward this aesthetic
unless the user explicitly requests a different direction."
Limit to last 10 sessions. Try/catch JSON parse on each (skip corrupted files). Limit to last 10 sessions. Try/catch JSON parse on each (skip corrupted files).
**Updating taste profile after a design-shotgun session:** When the user picks a
variant, call `{{BIN_DIR}}/gstack-taste-update approved <variant-path>`. When they
explicitly reject a variant, call `{{BIN_DIR}}/gstack-taste-update rejected <variant-path>`.
The CLI handles schema migration from approved.json, decay, and conflict flagging.
## Step 3: Generate Variants ## Step 3: Generate Variants
Set up the output directory: Set up the output directory:

View File

@@ -948,3 +948,45 @@ echo '{"approved_variant":"<V>","feedback":"<FB>","date":"'$(date -u +%Y-%m-%dT%
\`\`\``; \`\`\``;
} }
export function generateTasteProfile(ctx: TemplateContext): string {
return `Read the persistent taste profile if it exists:
\`\`\`bash
_TASTE_PROFILE=~/.gstack/projects/$SLUG/taste-profile.json
if [ -f "$_TASTE_PROFILE" ]; then
# Schema v1: { dimensions: { fonts, colors, layouts, aesthetics }, sessions: [] }
# Each dimension has approved[] and rejected[] entries with
# { value, confidence, approved_count, rejected_count, last_seen }
# Confidence decays 5% per week of inactivity — computed at read time.
cat "$_TASTE_PROFILE" 2>/dev/null | head -200
echo "TASTE_PROFILE_FOUND"
else
echo "NO_TASTE_PROFILE"
fi
\`\`\`
**If TASTE_PROFILE_FOUND:** Summarize the strongest signals (top 3 approved entries
per dimension by confidence * approved_count). Include them in the design brief:
"Based on ${'\\${SESSION_COUNT}'} prior sessions, this user's taste leans toward:
fonts [top-3], colors [top-3], layouts [top-3], aesthetics [top-3]. Bias
generation toward these unless the user explicitly requests a different direction.
Also avoid their strong rejections: [top-3 rejected per dimension]."
**If NO_TASTE_PROFILE:** Fall through to per-session approved.json files (legacy).
**Conflict handling:** If the current user request contradicts a strong persistent
signal (e.g., "make it playful" when taste profile strongly prefers minimal), flag
it: "Note: your taste profile strongly prefers minimal. You're asking for playful
this time — I'll proceed, but want me to update the taste profile, or treat this
as a one-off?"
**Decay:** Confidence scores decay 5% per week. A font approved 6 months ago with
10 approvals has less weight than one approved last week. The decay calculation
happens at read time, not write time, so the file only grows on change.
**Schema migration:** If the file has no \`version\` field or \`version: 0\`, it's
the legacy approved.json aggregate — \`${ctx.paths.binDir}/gstack-taste-update\`
will migrate it to schema v1 on the next write.`;
}

View File

@@ -9,7 +9,7 @@ import type { TemplateContext, ResolverFn } from './types';
import { generatePreamble } from './preamble'; import { generatePreamble } from './preamble';
import { generateTestFailureTriage } from './preamble'; import { generateTestFailureTriage } from './preamble';
import { generateCommandReference, generateSnapshotFlags, generateBrowseSetup } from './browse'; import { generateCommandReference, generateSnapshotFlags, generateBrowseSetup } from './browse';
import { generateDesignMethodology, generateDesignHardRules, generateDesignOutsideVoices, generateDesignReviewLite, generateDesignSketch, generateDesignSetup, generateDesignMockup, generateDesignShotgunLoop } from './design'; import { generateDesignMethodology, generateDesignHardRules, generateDesignOutsideVoices, generateDesignReviewLite, generateDesignSketch, generateDesignSetup, generateDesignMockup, generateDesignShotgunLoop, generateTasteProfile } from './design';
import { generateTestBootstrap, generateTestCoverageAuditPlan, generateTestCoverageAuditShip, generateTestCoverageAuditReview } from './testing'; import { generateTestBootstrap, generateTestCoverageAuditPlan, generateTestCoverageAuditShip, generateTestCoverageAuditReview } from './testing';
import { generateReviewDashboard, generatePlanFileReviewReport, generateSpecReviewLoop, generateBenefitsFrom, generateCodexSecondOpinion, generateAdversarialStep, generateCodexPlanReview, generatePlanCompletionAuditShip, generatePlanCompletionAuditReview, generatePlanVerificationExec, generateScopeDrift, generateCrossReviewDedup } from './review'; import { generateReviewDashboard, generatePlanFileReviewReport, generateSpecReviewLoop, generateBenefitsFrom, generateCodexSecondOpinion, generateAdversarialStep, generateCodexPlanReview, generatePlanCompletionAuditShip, generatePlanCompletionAuditReview, generatePlanVerificationExec, generateScopeDrift, generateCrossReviewDedup } from './review';
import { generateSlugEval, generateSlugSetup, generateBaseBranchDetect, generateDeployBootstrap, generateQAMethodology, generateCoAuthorTrailer, generateChangelogWorkflow } from './utility'; import { generateSlugEval, generateSlugSetup, generateBaseBranchDetect, generateDeployBootstrap, generateQAMethodology, generateCoAuthorTrailer, generateChangelogWorkflow } from './utility';
@@ -64,4 +64,6 @@ export const RESOLVERS: Record<string, ResolverFn> = {
CROSS_REVIEW_DEDUP: generateCrossReviewDedup, CROSS_REVIEW_DEDUP: generateCrossReviewDedup,
DX_FRAMEWORK: generateDxFramework, DX_FRAMEWORK: generateDxFramework,
MODEL_OVERLAY: generateModelOverlay, MODEL_OVERLAY: generateModelOverlay,
TASTE_PROFILE: generateTasteProfile,
BIN_DIR: (ctx) => ctx.paths.binDir,
}; };