mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-22 04:40:44 +02:00
* v1.87.5.0 perf: remove idle waits from tests and CI planning * fix: settle split PTY redraws before routing input * docs: record final burst-safe test benchmarks * fix: keep cold-setup snapshot metadata dependency-free * fix: avoid early-reader pipe races in artifact URL parsing * fix: preserve safety matches for multiline command payloads * fix: recognize concurrent CSO publication removal * test: preload the UI design-review target before invocation * docs: record validation blocker fixes * fix: bind plan observer rejection to the invoked command * fix: count only native design decisions in the UI gate * docs: clarify UI-positive eval evidence requirements * test: recognize native UI decisions without weakening finding counts * test: decouple native UI evidence from question punctuation * test: recognize concrete native UI decisions independently of prose format * fix: retain failed eval logs under the hidden CI cache * test: await telemetry completion instead of racing disk writes
28 lines
2.1 KiB
TypeScript
28 lines
2.1 KiB
TypeScript
import type { AskUserQuestionFingerprint } from './claude-pty-runner';
|
|
import { isDesignCountFirstReview } from './design-count-review';
|
|
|
|
export function isDesignUIScopeReview(fp: AskUserQuestionFingerprint): boolean {
|
|
const call = fp.nativeCall;
|
|
if (!call?.answered || call.failed || !Array.isArray(call.unansweredQuestionIndices) ||
|
|
call.unansweredQuestionIndices.length || !call.questions.length ||
|
|
fp.signature !== `${call.sessionId}:${call.toolUseId}`) return false;
|
|
if (call.questions.some(q => q.multiSelect || q.options.length < 2 ||
|
|
new Set(q.options.map(option => option.label)).size !== q.options.length ||
|
|
!q.options.some(option => option.label === call.answers?.[q.question]))) return false;
|
|
if (isDesignCountFirstReview(fp)) return true;
|
|
const workflow = /\b(?:review(?:s|ers?)?|scope|setup|learnings|routing|mockups?|permissions?|codex|claude|outside)\b/i;
|
|
const ui = /\b(?:dashboard|hierarchy|panels?|layout|headers?|buttons?|navigation|notifications?|activity|actions?|spacing|colou?rs?|fonts?|typography|loading|errors?|focus|contrast|keyboard|mobile|responsive|toasts?|modals?|empty)\b/i;
|
|
return call.questions.some(q => {
|
|
if (/^(?:scope|focus|learnings|routing|next steps?|outside(?: design)? voices)$/i.test(q.header.trim())) return false;
|
|
const issue = /^(?:D\d+\s*[—–:-]\s*)?Issue ([1-9]\d*)\s*[:—–-]\s*([^\n]+\?)$/i.exec(q.question.split('\n')[0]!.trim());
|
|
if (!issue || workflow.test(issue[2]!) || !ui.test(issue[2]!) ||
|
|
!q.options.some(option => ui.test(`${option.label} ${option.description ?? ''}`))) return false;
|
|
const context = /^Project\/branch\/task:([^\n]*)/mi.exec(q.question)?.[1] ?? '';
|
|
const namedPlans = context.match(/\b[\w.-]+\.md\b/gi) ?? [];
|
|
if ((namedPlans.length && !namedPlans.some(plan => /^PLAN\.md$/i.test(plan))) ||
|
|
/\b(?:before|prior to)\s+Pass\b/i.test(context)) return false;
|
|
const choice = new RegExp(`^${issue[1]}[A-Z](?:[).:—–-]\\s*|\\s+)\\S`);
|
|
return q.options.every(option => choice.test(option.label) && !workflow.test(option.label));
|
|
});
|
|
}
|