Skill Tracer
Skills › gstack › gstack › utility.ts

utility.ts

402 lines · scripts/resolvers/utility.ts at 3f79d81

1import type { TemplateContext } from './types';
2import { CODEX_MODEL_CONFIG_FLAG, CODEX_REVIEW_MODEL_CONFIG_FLAG, CODEX_WEB_SEARCH_FLAG } from './constants';
3import { CLAUDE_CAPTURE_EVAL_MODEL } from '../../lib/eval-model';
4
5/**
6 * {{CODEX_WEB_SEARCH_FLAG}} — the non-deprecated codex web-search flag
7 * (#2525). Templates with inline codex invocations reference this token so
8 * the flag has exactly one source of truth (scripts/resolvers/constants.ts).
9 */
10export function generateCodexWebSearchFlag(_ctx: TemplateContext): string {
11 return CODEX_WEB_SEARCH_FLAG;
14/**
15 * {{CODEX_MODEL_CONFIG_FLAG}} — the default frontier Codex model override.
16 * Users can override it with GSTACK_CODEX_MODEL, or replace it with a
17 * request-specific `-c model="..."` when the skill input names a model.
18 */
19export function generateCodexModelConfigFlag(_ctx: TemplateContext): string {
20 return CODEX_MODEL_CONFIG_FLAG;
23export function generateCodexReviewModelConfigFlag(_ctx: TemplateContext): string {
24 return CODEX_REVIEW_MODEL_CONFIG_FLAG;
27export function generateClaudeModelFlag(_ctx: TemplateContext): string {
28 return `--model "\${GSTACK_CLAUDE_MODEL:-${CLAUDE_CAPTURE_EVAL_MODEL}}"`;
31export function generateSlugEval(ctx: TemplateContext): string {
32 return `SLUG=$(${ctx.paths.binDir}/gstack-slug --get SLUG 2>/dev/null)`;
35export function generateSlugSetup(ctx: TemplateContext): string {
36 return `GSTACK_STATE_ROOT=$(${ctx.paths.binDir}/gstack-paths --get GSTACK_STATE_ROOT); : "\${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
37SLUG=$(${ctx.paths.binDir}/gstack-slug --get SLUG 2>/dev/null) && mkdir -p "$GSTACK_STATE_ROOT/projects/$SLUG" && echo "PROJECT_DIR: $GSTACK_STATE_ROOT/projects/$SLUG"`;
40export function generateBaseBranchDetect(_ctx: TemplateContext): string {
41 return `## Step 0: Detect platform and base branch
43First, detect the git hosting platform from the remote URL:
45\`\`\`bash
46git remote get-url origin 2>/dev/null
47\`\`\`
49- If the URL contains "github.com" → platform is **GitHub**
50- If the URL contains "gitlab" → platform is **GitLab**
51- Otherwise, check CLI availability:
52 - \`gh auth status 2>/dev/null\` succeeds → platform is **GitHub** (covers GitHub Enterprise)
53 - \`glab auth status 2>/dev/null\` succeeds → platform is **GitLab** (covers self-hosted)
54 - Neither → **unknown** (use git-native commands only)
56Determine which branch this PR/MR targets, or the repo's default branch if no
57PR/MR exists. Use the result as "the base branch" in all subsequent steps.
59**If GitHub:**
601. \`gh pr view --json baseRefName -q .baseRefName\` — if succeeds, use it
612. \`gh repo view --json defaultBranchRef -q .defaultBranchRef.name\` — if succeeds, use it
63**If GitLab:**
641. \`glab mr view -F json 2>/dev/null\` and extract the \`target_branch\` field — if succeeds, use it
652. \`glab repo view -F json 2>/dev/null\` and extract the \`default_branch\` field — if succeeds, use it
67**Git-native fallback (if unknown platform, or CLI commands fail):**
681. \`git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||'\`
692. If that fails: \`git rev-parse --verify origin/main 2>/dev/null\` → use \`main\`
703. If that fails: \`git rev-parse --verify origin/master 2>/dev/null\` → use \`master\`
72If all fail, fall back to \`main\`.
74Print the detected base branch name. In every subsequent \`git diff\`, \`git log\`,
75\`git fetch\`, \`git merge\`, and PR/MR creation command, substitute the detected
76branch name wherever the instructions say "the base branch" or \`<default>\`.
78---`;
81export function generateDeployBootstrap(_ctx: TemplateContext): string {
82 return `\`\`\`bash
83# Check for persisted deploy config in CLAUDE.md
84DEPLOY_CONFIG=$(grep -A 20 "## Deploy Configuration" CLAUDE.md 2>/dev/null || echo "NO_CONFIG")
85echo "$DEPLOY_CONFIG"
87# If config exists, parse it
88if [ "$DEPLOY_CONFIG" != "NO_CONFIG" ]; then
89 # Cut at the FIRST ": ", not the last. A greedy 's/.*: *//' ate the scheme of
90 # any URL: "Production URL: https://x.com" became "//x.com", because the last
91 # ":" belongs to "https:".
92 PROD_URL=$(echo "$DEPLOY_CONFIG" | grep -i "production.*url" | head -1 | sed 's/^[^:]*: *//')
93 PLATFORM=$(echo "$DEPLOY_CONFIG" | grep -i "platform" | head -1 | sed 's/^[^:]*: *//')
94 echo "PERSISTED_PLATFORM:$PLATFORM"
95 echo "PERSISTED_URL:$PROD_URL"
96fi
98# Auto-detect platform from config files
99[ -f fly.toml ] && echo "PLATFORM:fly"
100[ -f render.yaml ] && echo "PLATFORM:render"
101([ -f vercel.json ] || [ -d .vercel ]) && echo "PLATFORM:vercel"
102[ -f netlify.toml ] && echo "PLATFORM:netlify"
103[ -f Procfile ] && echo "PLATFORM:heroku"
104([ -f railway.json ] || [ -f railway.toml ]) && echo "PLATFORM:railway"
106# Detect deploy workflows
107for f in $(find .github/workflows -maxdepth 1 \\( -name '*.yml' -o -name '*.yaml' \\) 2>/dev/null); do
108 [ -f "$f" ] && grep -qiE "deploy|release|production|cd" "$f" 2>/dev/null && echo "DEPLOY_WORKFLOW:$f"
109 [ -f "$f" ] && grep -qiE "staging" "$f" 2>/dev/null && echo "STAGING_WORKFLOW:$f"
110done
111\`\`\`
113If \`PERSISTED_PLATFORM\` and \`PERSISTED_URL\` were found in CLAUDE.md, use them directly
114and skip manual detection. If no persisted config exists, use the auto-detected platform
115to guide deploy verification. If nothing is detected, ask the user via AskUserQuestion
116in the decision tree below.
118If you want to persist deploy settings for future runs, suggest the user run \`/setup-deploy\`.`;
121export function generateQAMethodology(_ctx: TemplateContext): string {
122 return `# Browser QA methodology
124Run only for selected browser surfaces. Map diffs with source before probes; discovery stays black-box, diagnosis caller-owned.
126The shared exploratory loop owns execution order, not these technique phases. Its
127checkpoint rule covers every probe after the baseline, including orientation, links,
128exact replay and additional evidence. Never batch across checkpoints.
130## Modes
132For /qa and /qa-only, choose Full, Quick or Regression. Resolve conflicting depth flags
133by asking before probes. /review and /ship keep their caller's smoke and plan bounds.
134Diff-aware selects scope, not another pass. Time caps include checkpoints and evidence.
135At exhaustion, stop probing and report unfinished coverage, never skip checkpoints.
137### Diff-aware (automatic when on a feature branch with no URL)
139Substitute the detected base for \`main\`:
141\`\`\`bash
142git diff main...HEAD --name-only
143git log main..HEAD --oneline
144\`\`\`
146Map changed controllers/routes/views/components/models/services/styles to pages. Check commits/PR intent; add related TODO bugs to the test plan. Open static pages directly. For browser-surface API probes:
148\`\`\`bash
149aside repl '
150const pg = await openTab("<base-url>");
151const r = await fetch("<base-url>/api/...", { method: "GET" });
152console.log("API_STATUS=" + r.status);
153console.log("API_BODY_START"); console.log((await r.text()).slice(0, 4000)); console.log("API_BODY_END");
154await closeTab(pg); console.log("GSTACK_STEP_OK");
156\`\`\`
158After selecting and isolating a browser surface, find a local app if its URL is missing:
160\`\`\`bash
161for p in 3000 4000 8080; do curl -sI --max-time 3 "http://localhost:$p" >/dev/null 2>&1 && echo "Found app on :$p"; done
162\`\`\`
164Use the supplied URL or first responder/staging/preview; ask if none. Test changed/adjacent pages and flows. Flag new bugs absent from TODOS.md in the Phase 6 report.
166**No identifiable pages:** use Quick plus discovered interactions, even for backend/config/infrastructure changes.
168### Full (default with a URL)
169Visit every reachable page (5-15 minutes). Score health; document 5-10 evidenced issues, never invent any.
171### Quick (\`--quick\`)
1723 minutes: homepage + top 5 navigation targets. Check loads/console/broken links; score per Health Score Rubric; skip detailed issues/checklist, never the shared loop's gates.
174### Regression (\`--regression <baseline>\`)
175Run Full; append fixed/new issues and score delta. Preserve the supplied prior baseline.
176A missing or unreadable baseline is a missing prerequisite: it blocks the comparison, not the Full run.
178## Workflow
180### Phase 1: Initialize
182Reuse the caller's BROWSER SETUP and owned artifact paths: Aside READY, otherwise \`$B\`
183(\`NEEDS_ASIDE\`/\`ASIDE_NOT_RUNNING\`). Complete only missing setup within caller
184authority. Clamp the shared loop's deadline guard to the caller's running deadline.
186### Phase 2: Authenticate (if needed)
188Follow BROWSER SETUP's **Browser access decision** for /setup-browser-cookies or \`$B handoff\`/\`$B resume\`. Rerun after user sign-in/2FA/OTP/CAPTCHA. Never handle credentials or expose cookies/tokens/localStorage.
190### Phase 3: Orient
192Establish the successful baseline before challenges. Observe the page or interaction's
193expected result/state, not merely a successful load.
195**Read/flow:** set \`flow = true\` and replace action/wait for interactions. Keep ONE script; tabs close at its end.
197\`\`\`bash
198aside repl '
199const flow = false;
200const HOOK = \`(() => { window.__gstackErrs = window.__gstackErrs || []; const oe = console.error; console.error = (...a) => { window.__gstackErrs.push(a.map(String).join(" ")); oe.apply(console, a); }; window.addEventListener("error", e => window.__gstackErrs.push("uncaught: " + e.message)); window.addEventListener("unhandledrejection", e => window.__gstackErrs.push("unhandledrejection: " + (e.reason && e.reason.message || e.reason))); })()\`;
201const pg = await openTab("about:blank");
202await pg._sendToTarget("Page.addScriptToEvaluateOnNewDocument", { source: HOOK });
203await pg.goto("<target-url>");
204console.log((await snapshot(pg, { interactive: true })).tree);
205await pg.screenshot({ path: flow ? "issue-001-step-1.jpg" : "initial.jpg", type: "jpeg", quality: 60, fullPage: !flow });
206if (flow) {
207 await pg.locator("e12").click();
208 await sleep(500);
209 console.log("DIFF_START"); console.log((await snapshot(pg)).diff); console.log("DIFF_END");
210 await pg.screenshot({ path: "issue-001-result.jpg", type: "jpeg", quality: 60 });
212console.log("URL=" + pg.url());
213console.log("CONSOLE_ERRORS=" + JSON.stringify(await pg.evaluate(() => window.__gstackErrs)));
214console.log("TEXT_START"); console.log((await pg.evaluate(() => document.body.innerText)).slice(0, 20000)); console.log("TEXT_END");
215console.log("ASIDE_DIR=" + pwd);
216await closeTab(pg); console.log("GSTACK_STEP_OK");
218\`\`\`
220EVERY screenshot: \`cp "<ASIDE_DIR>/initial.jpg" "$REPORT_DIR/screenshots/initial.jpg"\` (substitute names), then Read it. Never delete reports/screenshots.
222**Links:** same-origin safe paths; HEAD only locally (requests carry cookies).
224\`\`\`bash
225aside repl '
226const pg = await openTab("<target-url>");
227const links = await pg.evaluate(() => [...new Set([...document.querySelectorAll("a[href]")].map(a => a.href))].filter(h => new URL(h).origin === location.origin && !/logout|signout|delete|remove|cancel|unsubscribe/i.test(h)));
228const local = await pg.evaluate(() => /^(localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|::1|\\[::1\\])$|\\.(localhost|test)$/.test(location.hostname));
229for (const l of links) { if (!local) { console.log("LINK ?", l); continue; } const r = await fetch(l, { method: "HEAD" }).catch(e => ({ status: "ERR " + e.message })); console.log("LINK", r.status, l); }
230await closeTab(pg); console.log("GSTACK_STEP_OK");
232\`\`\`
234\`LINK\` 4xx/5xx or \`ERR\` is broken; \`LINK ?\` is unverified. Snapshot SPA buttons/menus missing from links.
236Framework: \`__next\`/\`_next/data\` = Next.js; \`csrf-token\` = Rails; \`wp-content\` = WordPress; no-reload navigation = SPA.
238### Phase 4: Explore
240Select the next candidate from the preceding result. For each page, use the read script with \`page-<name>.jpg\`. Check layout, controls, empty/invalid/edge-case forms, navigation and empty/loading/error/overflow states per \`qa/references/issue-taxonomy.md\`. Prioritize core flows over secondary pages; Quick skips this checklist. For mobile:
242\`\`\`bash
243aside repl '
244const pg = await openTab("<page-url>");
245await pg._sendToTarget("Emulation.setDeviceMetricsOverride", { width: 375, height: 812, deviceScaleFactor: 2, mobile: true });
246await sleep(300);
247await pg.screenshot({ path: "page-mobile.jpg", type: "jpeg", quality: 60, fullPage: true });
248await pg._sendToTarget("Emulation.clearDeviceMetricsOverride", {});
249console.log("ASIDE_DIR=" + pwd); await closeTab(pg); console.log("GSTACK_STEP_OK");
251\`\`\`
253### Phase 5: Document
255Confirm each issue by retrying once under the shared loop's exact-replay rule, then
256minimize and report screenshot evidence immediately. A timeout before replay finishes leaves
257confirmation incomplete. Later timeouts leave confirmed defects intact but evidence
258or minimization unfinished.
260**Interactive:** Phase 3, \`flow = true\`. Alternatives: \`pg.fill("#email", "qa@example.com")\`, \`pg.getByRole("button", { name: "Save" }).click()\`, \`pg.waitForSelector("#done")\`, \`pg.waitForURL(/dashboard/)\`. Link before/after screenshots in repro steps.
262**Static** (copy/layout/images): one annotated screenshot and description.
264\`\`\`bash
265aside repl '
266const pg = await openTab("<page-url>");
267const a = await annotatedScreenshot(pg);
268await fs.writeFile(path.join(pwd, "issue-002.png"), Buffer.from(a.base64Image, "base64"));
269console.log("ASIDE_DIR=" + pwd); await closeTab(pg); console.log("GSTACK_STEP_OK");
271\`\`\`
273### Phase 6: Wrap Up
275Format retained evidence without new probes, using \`templates/qa-report-template.md\`
276from this host's installed QA directory and the caller's artifact/mixed-report rules.
278Report score, Top 3 Things to Fix by severity, console health, severity counts, date, duration, page/screenshot counts and framework. Save \`baseline.json\`: \`date\` (YYYY-MM-DD), \`url\`, \`healthScore\`, \`issues\` (\`id\`, \`title\`, \`severity\`, \`category\`), \`categoryScores\`. Regression: fixed = prior only, new = current only.
280## Health Score Rubric
282Compute each category score (0-100), then take the weighted average.
284### Counting
285- Deduplicate the same root cause across pages. Use one primary category, first applicable: Links (navigation), Accessibility (access barriers), Functional (behavior), Performance (speed), Visual (layout), Content (copy), UX (friction), Console (remaining errors). No double deductions.
286- Exclude **untested** categories; label partial scores **provisional** with coverage. None tested: "not scored". Compare only identical coverage.
288### Console (weight: 15%)
289Deduplicate reproducible errors/exceptions by message+source across pages. Exclude warnings, info, and defects scored elsewhere.
290- 0 errors → 100
291- 1-3 errors → 70
292- 4-10 errors → 40
293- 11+ errors → 10
295### Links (weight: 10%)
296Count unique broken destinations, including client-side routes: repeatable 4xx/5xx, missing routes/anchors, or timeouts. Exclude expected auth redirects and resource/API requests.
297- 0 broken → 100
298- Each broken link → -15 (minimum 0)
300### Per-Category Scoring (Visual, Functional, UX, Content, Performance, Accessibility)
301Start at 100; deduct per finding:
302- Critical issue → -25
303- High issue → -15
304- Medium issue → -8
305- Low issue → -3
306Floor: 0.
308Use the highest applicable severity; record impact/workaround:
309- **Critical:** data loss, security/privacy exposure, or core app unusable for all users.
310- **High:** core/major task blocked without a workaround.
311- **Medium:** task impaired but a workaround exists.
312- **Low:** cosmetic/copy/friction issue without lost task completion.
313Console/Links use counts instead.
315### Weights
316| Category | Weight |
317|----------|--------|
318| Console | 15% |
319| Links | 10% |
320| Visual | 10% |
321| Functional | 20% |
322| UX | 15% |
323| Performance | 10% |
324| Content | 5% |
325| Accessibility | 15% |
327### Final Score
328Use decimal weights (15% = 0.15): \`score = Σ (category_score × weight) / Σ tested weights\`. Round only the final score to the nearest integer (0.5 rounds up).
330---
332## Framework-Specific Guidance
334- **Next.js:** hydration errors (\`Hydration failed\`, \`Text content did not match\`), \`_next/data\` 404s, link-click routing (not just \`goto\`), dynamic-content CLS.
335- **Rails:** dev N+1 warnings, form CSRF, Turbo/Stimulus transitions, flash appearance/dismissal.
336- **WordPress:** plugin JS conflicts, signed-in admin bar, \`/wp-json/\`, mixed content.
337- **SPA:** snapshot navigation, stale state on return, back/forward history, console signs of leaks after extended use.
339## Important Rules
341**Never read source code during browser discovery.** Use realistic end-to-end flows; check console after every interaction. For missing click targets, use annotated labels, then ref/CSS clicks.
343Use \`[REDACTED]\` for credentials. Follow BROWSER SETUP safety/sentinel rules: one AskUserQuestion listing non-LOCAL mutations per run, BEFORE acting. LOOK is not ACT.
345**Never refuse to use the browser for a selected browser surface**, even backend-only app changes. Tests/curl cannot replace it. API/CLI/job/worker/webhook targets do not select it.`;
348export function generateCoAuthorTrailer(ctx: TemplateContext): string {
349 const { getHostConfig } = require('../../hosts/index');
350 const hostConfig = getHostConfig(ctx.host);
351 return hostConfig.coAuthorTrailer || 'Co-Authored-By: Claude <noreply@anthropic.com>';
354export function generateSetupCommand(ctx: TemplateContext): string {
355 // Every non-claude host must reinstall ITSELF on upgrade — bare `./setup`
356 // defaults to the claude host and would leave the invoking host stale.
357 return ctx.host === 'claude' ? './setup' : `./setup --host ${ctx.host}`;
360export function generateChangelogWorkflow(ctx: TemplateContext): string {
361 return `## Step 13: CHANGELOG (auto-generate)
3631. Read \`CHANGELOG.md\` header to know the format.
3652. **First, enumerate every commit on the branch:**
366 \`\`\`bash
367 git log ${ctx.skillName === 'ship' ? 'origin/<base>' : '<base>'}..HEAD --oneline
368 \`\`\`
369 Copy the full list. Count the commits. You will use this as a checklist.
3713. **Read the full diff** to understand what each commit actually changed:
372 \`\`\`bash
373 git diff ${ctx.skillName === 'ship' ? 'origin/<base>' : '<base>...HEAD'}
374 \`\`\`
3764. **Group commits by theme** before writing anything. Common themes:
377 - New features / capabilities
378 - Performance improvements
379 - Bug fixes
380 - Dead code removal / cleanup
381 - Infrastructure / tooling / tests
382 - Refactoring
3845. **Write the CHANGELOG entry** covering ALL groups:
385 - If existing CHANGELOG entries on the branch already cover some commits, replace them with one unified entry for the new version
386 - Categorize changes into applicable sections:
387 - \`### Added\` — new features
388 - \`### Changed\` — changes to existing functionality
389 - \`### Fixed\` — bug fixes
390 - \`### Removed\` — removed features
391 - Write concise, descriptive bullet points
392 - ${ctx.skillName === 'ship' ? 'Insert after the observed file header, before the first release entry, dated today' : 'Insert after the file header (line 5), dated today'}
393 - Format: \`## [X.Y.Z.W] - YYYY-MM-DD\`
394 - **Voice:** Lead with what the user can now **do** that they couldn't before. Use plain language, not implementation details. Never mention TODOS.md, internal tracking, or contributor-facing details.
3966. **Cross-check:** Compare your CHANGELOG entry against the commit list from step 2.
397 Every user-facing change in that list must be represented; add any that is missing.
398 Commits with no user-facing effect (merges, version bumps, fixes to work introduced
399 earlier on this branch) need no bullet. The entry must reflect every user-facing theme.
401**Do NOT ask the user to describe changes.** Infer from the diff and commit history.`;