mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-16 01:45:29 +02:00
* feat: add a restricted and supervised Claude Code runner Preserve configured authentication and models while enforcing tool access, strict completion JSON, bounded output and process cleanup. Cover argv, failure handling, session metadata and Windows process containment. * feat: route outside reviews by harness and migrate wrapper installs Use Claude Code from Codex and Codex from other supported hosts, with shared invocation rendering, positive gate validation and per-phase provenance. Rename /claude to /claude-code, repair managed shared and copied installations safely, and generate native Kiro skills. Add installed-workflow, failure-injection and live cross-harness regression coverage. * test: recognize CEO mode labels without terminal spacing The paid workflow rendered SCOPEEXPANSION at option 4, but its driver required a literal space. Match the leading mode title without cursor-spacing artifacts and ignore adjacent preview text. Preserve missing-target failures and downstream posture assertions. * test: isolate plan-count fixtures before starting review workflows Seed the complete test plan in a private git repository before launching Claude, so a bare slash command cannot review the live workspace while a delayed fixture message remains queued. Preserve count thresholds, parsers and budgets. Add initial-context and installed-discovery tests, and retain startup/terminal diagnostics on failed evaluations. * test: stabilize review fixtures and Claude eval startup Preserve source boundaries in workflow judge inputs, isolate CEO mode plans, and wait for interactive trust input readiness. Keep startup failure evidence and retain existing models, budgets, and assertions. Co-Authored-By: OpenAI Codex <noreply@openai.com> * test: classify collapsed review modes and isolate seeded findings Keep review questions out of the setup count when terminal cursor positioning removes spaces. State existing webhook safeguards so the five-finding control measures its seeded defects without accidental extra security and concurrency gaps. Preserve question bands and the paired control. Co-Authored-By: OpenAI Codex <noreply@openai.com> * test: isolate browser daemon state across free shards Co-Authored-By: OpenAI Codex <noreply@openai.com> * test: stabilize native review counting and interactive navigation Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: prepare v1.82.0.0 release Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: eliminate browser and process-cleanup test flakes Pin every CI surface to Bun 1.4.0 to avoid extra-stdio finalizers closing reused live sockets. Add an isolated GC/listener regression that fails on Bun 1.3.13, and prevent coordinated rollback to an affected CI runtime. Check renderer cleanup against the render's own staging directory so concurrent renders cannot invalidate the assertion. Make the no-pgrep process-tree walk tolerate disappearing /proc entries, and synchronize its test fixture through child readiness and pipe EOF instead of sleeps. Validation: 9,157 passed, 31 skipped, zero failures across 556 files with retries disabled. Build, all-host generation freshness, and skill checks passed. All three races have failing-before/passing-after regressions. * fix: count completed native review questions in evals * fix: drive review navigation from confirmed native choices * fix: require complete section-loading eval reports * test: isolate telemetry HTTP transport from local assertions * fix: keep review input on the active native question * test: let tunnel revocation daemon choose an available port * test: allocate available ports for pairing and watchdog fixtures * fix: stabilize planning eval navigation and phase reporting * test: isolate installed runtime paths in planning evals * test: stabilize review evidence and concurrent refresh fixtures * fix: resolve design findings before editing the plan * fix: honor and persist disabled outside plan reviews * fix: preserve planning decisions and terminal evidence Load installed host reviews at autoplan phase entry and wait for completed reviewers and saved artifacts. Reuse approved remedies while preserving individual finding decisions. Drive interactive evals from the current terminal viewport, bind native questions across scrolling, and require complete native report evidence. Cover captured stale menus, permission lifecycles, setup classification, and disabled-review tool availability with deterministic regressions. Advance release metadata and the upgrade migration to the unclaimed 1.83.0.0 slot. * fix: drive native review questions and preserve current plans Use the native single-choice keyboard protocol and current terminal viewport, with per-question navigation inside packets and completed-call coverage. Keep permissions, multi-select menus, and Submit controls distinct. Send Autoplan reviewers the amended implementation plan, keep its review record separate, and supply retained application contracts in the chain fixture. Clarify individual DevEx decisions and complete CEO fix options; use one active plan destination for the section-loading report. * fix: preserve complete plan-review decisions * fix: recognize native plan dialogs and reviewer controls * fix: preserve review decisions and phase completion * fix: recognize completed reviews without losing findings * fix: preserve review continuity and native eval completion * test: fix native review completion and eval retry isolation * test: handle native review menus and complete eval fixtures * test: fix native review setup, completion, and isolation failures * test: limit native skill discovery to runtime assets * fix: bind Autoplan reviews to full ordered phase inputs * test: fix planning eval routing, counting, and timeout handling * chore: advance queued release to v1.84.0.0 * fix: preserve complete review inputs and planning decisions * fix: reconcile review approvals and preserve phase obligations * fix: preserve review obligations and unblock eval permissions Carry recorded Autoplan requirements into blind phase inputs, require Eng review approvals before exit, and exercise combined asynchronous flows in CEO reviews. Correct native finding and handoff classification and unblock repeated report edits using scoped request identities. * fix: retain plan requirements and complete native review dialogs * fix: complete native review prompts and retain plan references * fix: preserve review inputs and classify native eval evidence * fix: check competing completion orders in CEO reviews * fix: recognize review decisions and require phase methodology Require the current phase methodology before Autoplan snapshots. Correct substantive decision, closed handoff, and cache-finding classification, and honor the recommended implementation approach in native review dialogs. Add captured-transcript regressions without changing review thresholds, provider models, retries, or deadlines. * test: bind native review decisions and close completed handoffs * fix: complete review dialogs and verify methodology delivery * fix: preserve review evidence and unblock native eval prompts * fix: handle native review question completions * fix: recognize native review narration and controls * fix: count native review decisions and isolate eval fixtures * test: verify seeded review coverage and current artifact permissions * test: isolate model and brain-aware skill renders * fix: repair native workflow evaluation and clarify review steps * fix: stabilize workflow eval evidence and review guidance * test: repair native workflow observation and fixture isolation * fix: recognize completed workflow evidence and owned skill reads * test: repair seeded workflow delivery and completion evidence * test: recognize current review evidence across native forms * test: handle native review variants and permission redraws * fix: honor review preferences and recognize native eval evidence * test: recognize completed review decisions and queued permissions * test: match current review contracts and partial-line edits * test: recognize completed workflow evidence and bounded human waits * fix: preserve review entry gates and native eval interactions * fix: recognize native workflow evidence and preserve review gates * test: recognize current review evidence and preconfigure workflow fixtures * test: recognize completed review findings and scoped artifact permissions * fix: stabilize native workflow review and permission evidence * fix: recognize current review evidence and scoped edit confirmations Clarify Design and engineering review entry instructions and Design scoring. Recognize required legacy coverage and public Autoplan completion recaps. Bind the pending Edit confirmation to its exact file, ordered digest, and one-request approval when a preceding command display remains visible. Keep reviews within their existing size limits and preserve scope gates when extracting workflow fixtures from either supported preamble header. Keep failure outcomes, review thresholds, provider choices, and eval budgets. * fix: recover review workflow progress and eval evidence * fix: recognize valid review evidence and scope selection * test: fix review evidence parsing and repeated artifact prompts * test: recognize valid review decisions and pending native cards * fix(plan-eng-review): keep final navigation consistent with approved tasks * test: recognize valid review evidence and bind legacy diff requests * fix: stabilize review eval evidence and harness repair guidance * docs: update project documentation for v1.85.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * test: fix Windows CI fixtures and credential scan Rebase captured JSON values and filesystem evidence using the appropriate path convention. Compile native fake CLIs on Windows and synchronize pipe holder readiness, with cleanup retained when assertions fail. Assemble synthetic credential fixtures at runtime so the added-line scan keeps enforcing the same gate without flagging its own rejection controls. Discover generated skills directly for the empty-find regression check, avoiding a recursive scan through saved evaluation artifacts and dependencies. * fix: preserve source renders on Windows Compare canonical generator paths using native separators so an output sidecar pointing at the source cannot overwrite its skill or metadata. Keep the regression fixture isolated from the real checkout and expose freshness diagnostics before asserting subprocess status. Detach Windows drain-test pipe holders from the fake provider's automatic child cleanup while preserving the enclosing runner job and its assertions. * fix: clarify outside review fallback and CEO decisions Render one applicable own-harness fallback path and retain native review, disabled policy, and missing-coverage semantics. Align report field names and mode labels, and make the existing per-cut scope approval explicit. Regenerate skill outputs and keep the workflow judge's model, thresholds, and retry policy unchanged. * chore: move release to free version slot (v1.86.0.0) PR #2852 now claims v1.85.0.0. Align the release metadata and rename migration so upgrades from that version still receive it. Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: include engineering review prerequisites and restore branch context * fix: recognize coverage diagrams and clarify design review instructions * fix: preserve file identities and join Windows test processes --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
683 lines
54 KiB
TypeScript
683 lines
54 KiB
TypeScript
import type { AskUserQuestionFingerprint } from './claude-pty-runner';
|
||
import type { NativePlanQuestionCall } from './plan-count-transcript';
|
||
|
||
/** Concrete product decisions, separate from the skill's mandatory Step-0 confirmations. */
|
||
export const DEVEX_COUNT_FILES: Record<string, string> = {
|
||
'README.md': `# EvalKit SDK
|
||
|
||
EvalKit is a Python SDK for ML engineers evaluating LLM responses. The primary
|
||
developer writes Python daily, uses a terminal, and wants a local result before
|
||
connecting the SDK to production CI. The agreed review posture is DX POLISH:
|
||
improve the existing SDK's touchpoints within the beta release scope.
|
||
|
||
## Getting started
|
||
|
||
Install with \`python -m pip install evalkit==2.0.0b1\`,
|
||
then follow the quickstart's command: \`python examples/first_eval.py\`.
|
||
The published package inventory is in docs/package-contents.txt.
|
||
|
||
The chosen first-success experience is an included, copy-paste demo command:
|
||
\`python -m evalkit.demo\`. It evaluates bundled sample responses and prints
|
||
real per-example scores plus an overall score. It needs no hosted playground
|
||
or new interactive UI. Like every first evaluation, it currently waits for the
|
||
mandatory CI check described in docs/current-contracts.md.
|
||
|
||
The bundled demo already works without a developer API key. Its sample evaluation
|
||
uses the shipped mock transport; its mandatory remote CI check uses the included
|
||
sample-project binding. No credentials step precedes this first demo result.
|
||
The keyless demo still waits for that CI check and has no skip or offline bypass.
|
||
|
||
After the demo, developers obtain a key for their first live evaluation at
|
||
https://console.evalkit.example/settings/api-keys: select the project, choose
|
||
Create key, copy the value once, and export EVALKIT_API_KEY in their terminal.
|
||
The page also lists existing keys and provides revoke/rotate controls. The
|
||
bundled demo does not use this key; live evaluations do.
|
||
|
||
Expected completed demo output for the bundled sample responses is documented
|
||
here; the shipped demo prints this per-example and aggregate score format:
|
||
|
||
example 1: score=0.80
|
||
example 2: score=1.00
|
||
overall: score=0.90
|
||
|
||
See docs/api.md for public API and upgrade behavior, and docs/benchmarks.md for
|
||
the completed onboarding study. These documents describe the existing SDK's
|
||
behavior; its runtime is maintained separately from this release-planning repo.
|
||
`,
|
||
'docs/benchmarks.md': `# Completed onboarding study
|
||
|
||
The internal comparison measured Python SDK onboarding with the same developer
|
||
and machine. Peer SDK A took 2 minutes, B took 4 minutes, and C took 3 minutes.
|
||
EvalKit took 6 minutes, including the mandatory 5-minute CI wait. The measurement
|
||
starts before installation and ends at the first real evaluation result.
|
||
|
||
The agreed target is under 2 minutes. The study, target persona, and terminal
|
||
demo delivery vehicle are already approved. Timing instrumentation and the
|
||
post-beta feedback survey exist and will continue unchanged.
|
||
`,
|
||
'docs/current-contracts.md': `# Existing SDK contracts
|
||
|
||
On a developer's first local evaluation, the SDK requires a successful remote
|
||
CI check and blocks for five minutes before returning an evaluation result.
|
||
There is no skip flag or offline first-run path. The beta plan retains this gate.
|
||
|
||
During the required wait, the existing SDK writes a progress line to stderr
|
||
every 30 seconds, such as "Waiting for CI check: 90s elapsed of 300s", and reports
|
||
when the check finishes. Progress does not bypass the check or return evaluation
|
||
results before its required successful completion.
|
||
|
||
Before the countdown, the SDK already prints what the check verifies and where
|
||
to inspect it: "Verifying the sample-project binding with EvalKit CI; inspect
|
||
https://ci.evalkit.example/checks/<check-id>; normally completes within 300s."
|
||
The URL identifies the check without exposing credentials. If it has not
|
||
succeeded at 300s, the SDK reports EVALKIT_CI_TIMEOUT, the check URL, and the
|
||
instruction to inspect that check and retry after CI recovers. Its help link
|
||
explains the check states and recovery steps. Success is still required before
|
||
the first local result; these messages do not change the mandatory wait.
|
||
|
||
Authentication errors behave exactly as documented in docs/api.md. All other
|
||
errors already identify the cause, relevant argument or file, and an actionable
|
||
fix. Errors redact secrets. API timeouts, cancellation, rate limits, and retries
|
||
are bounded and documented; evaluation IDs prevent duplicate submitted jobs.
|
||
|
||
The SDK supports Python 3.10+, macOS, Linux, and Windows without Docker. Its
|
||
type annotations, offline sample data, mock transport, noninteractive CI mode,
|
||
API reference, support contact, changelog, and contributor guide already work.
|
||
Telemetry is opt-in. No new hosted service, language binding, or community
|
||
program is proposed in this release.
|
||
`,
|
||
'docs/api.md': `# Public API retained by the beta plan
|
||
|
||
The two evaluation functions accept positional arguments:
|
||
|
||
- \`run_eval(dataset, evaluator)\`
|
||
- \`run_batch(evaluator, dataset)\`
|
||
|
||
Both argument names describe the same concepts. The reversed positional order
|
||
is intentional in the current draft; neither function requires keyword arguments.
|
||
|
||
For an invalid API key, the SDK raises \`AuthError("request failed")\`.
|
||
There is no authentication error code, explanation of the cause, or instruction
|
||
for replacing the key. The plan retains this message.
|
||
|
||
Version 1 exposes \`Client.evaluate()\`. Version 2 replaces it with
|
||
\`Client.run()\` and removes the old name immediately. No compatibility alias,
|
||
deprecation warning, migration guide, or codemod is provided. Other public APIs
|
||
retain their existing behavior, and the release changelog is otherwise complete.
|
||
`,
|
||
'docs/package-contents.txt': `Published evalkit 2.0.0b1 package inventory:
|
||
evalkit/__init__.py
|
||
evalkit/client.py
|
||
evalkit/demo.py
|
||
evalkit/sample_responses.json
|
||
README.md
|
||
|
||
The quickstart references examples/first_eval.py, but that file is absent from
|
||
both the published package and the release examples archive. The terminal demo
|
||
module and its sample data are included and work as documented.
|
||
`,
|
||
};
|
||
|
||
export function planDevexCountFixture(planPath: string): string {
|
||
return [
|
||
`Please review this plan thoroughly. As you go, write your plan-mode plan to ${planPath} (use Edit/Write to that exact path).`,
|
||
'This is an interactive review; a responder is available for AskUserQuestion.',
|
||
'Present each unresolved decision and wait for its answer.',
|
||
'',
|
||
'# Plan: EvalKit SDK beta release polish',
|
||
'',
|
||
'The primary developer, onboarding benchmark, and terminal demo experience',
|
||
'are settled in README.md and docs/benchmarks.md. Use DX POLISH for the',
|
||
'existing release scope. Review the actual documented contracts and proposed',
|
||
'behavior, including the first-run CI requirement, public function signatures,',
|
||
'authentication error, packaged quickstart, and v1-to-v2 client upgrade.',
|
||
'',
|
||
'The current draft ships the behavior in docs/current-contracts.md and',
|
||
'docs/api.md unchanged, using the package inventory in docs/package-contents.txt.',
|
||
'Recommendations that repair those developer-facing contracts belong in this',
|
||
'plan. Existing working contracts remain the baseline for the review.',
|
||
].join('\n');
|
||
}
|
||
|
||
type QuestionRecord = { header: string; question: string; options?: Array<{ label: string; description?: string }> };
|
||
|
||
function questionRecords(fp: AskUserQuestionFingerprint, answeredOnly = false): QuestionRecord[] {
|
||
if (!fp.nativeCall) return [{ header: '', question: fp.promptSnippet }];
|
||
return fp.nativeCall.questions.filter(q => !answeredOnly
|
||
|| (fp.nativeCall!.answered && Boolean(fp.nativeCall!.answers?.[q.question])));
|
||
}
|
||
|
||
const ADMINISTRATIVE_HEADERS = new Set([
|
||
'design doc', 'prerequisite', 'routing rules', 'routing setup', 'cross-project',
|
||
'target persona', 'developer persona', 'persona selection', 'empathy check',
|
||
'narrative check', 'tthw target', 'competitive benchmark', 'benchmark confirmation',
|
||
'magic delivery', 'review mode', 'fix scope', 'confusion scope',
|
||
]);
|
||
|
||
/** The structured accuracy frame approves an observation, never a proposed repair. */
|
||
function structuredEmpathyAccuracy(header: string, question: string, options: QuestionRecord['options']): boolean {
|
||
if (!/^Empathy$/i.test(header.trim()) || !options || options.length !== 3 || /<gstack-qid/i.test(question)) return false;
|
||
const compact = (text: string) => text.trim().replace(/\s+/g, ' ');
|
||
const clean = (text: string) => compact(text).replace(/\s*\(recommended\)$/i, '');
|
||
// Consume complete descriptions too: an accurate recap cannot conceal an
|
||
// additional approval in the explanation of an option.
|
||
const descriptions = new Map([
|
||
['accurate, proceed', /^✅ Every beat is grounded in a documented contract, not a guess about the runtime\. ✅ Lets the review move to friction-point decisions immediately\. ❌ If the runtime differs from the docs, the scores inherit that gap\.$/i],
|
||
['some of this is wrong', /^✅ You correct specific beats \(for example, the demo may not need an API key\) before scoring\. ✅ Keeps the narrative honest for the implementer who reads it\. ❌ Costs one round-trip before friction-point questions begin\.$/i],
|
||
['way off, actual experience is...', /^✅ Replaces the narrative entirely with your account of the real first run\. ✅ Prevents a review built on a wrong premise\. ❌ Discards the traced path and requires you to describe the flow from scratch\.$/i],
|
||
]);
|
||
const labels = options.map(option => clean(option.label).toLowerCase());
|
||
if (new Set(labels).size !== 3 || options.some((option, i) => !option.description ||
|
||
!descriptions.get(labels[i]!)?.test(compact(option.description)))) return false;
|
||
const parts = question.trim().replace(/^D\s*\d+\s*[—–:-]\s*/i, '').split(/\n\s*\n/);
|
||
if (parts.length !== 3) return false;
|
||
const role = String.raw`(?:(?:ML|backend|frontend|full-stack) )?(?:developer|engineer)`;
|
||
const preamble = new RegExp(String.raw`^Does this first-person narrative match what your ${role} experiences today\? Project/branch/task: [\w-]+ on [\w/-]+, [\w.-]+ SDK beta polish\. ELI10: Before scoring anything, I walk the actual README path as the target developer and describe what they see and feel\. If I have the experience wrong, every score downstream is wrong too, so please correct me here\. Stakes: this narrative becomes the Developer Perspective section the implementer reads\.$`, 'i');
|
||
if (!preamble.test(compact(parts[0]!)) ||
|
||
!/^Stakes if we pick wrong: the review polishes the wrong pain\. Recommendation: A because every step above traces to a specific line in README\.md, docs\/api\.md, docs\/current-contracts\.md, or docs\/package-contents\.txt\. Note: options differ in kind, not coverage [—–-] no completeness score\. Net: proceed on the traced path vs\. correct it before scoring\.$/i.test(compact(parts[2]!))) return false;
|
||
const journey = parts[1]!.split('\n');
|
||
if (!new RegExp(String.raw`^NARRATIVE \(${role}, terminal, wants a local result before CI\):$`, 'i').test(journey.shift() ?? '')) return false;
|
||
// Quoted commands/messages are source evidence. Every unquoted sentence
|
||
// must consume one known observation form; a heading alone cannot turn
|
||
// arbitrary instructions, deontic clauses or imperatives into evidence.
|
||
const sentences = compact(journey.join(' ')).replace(/`[^`]*`|"(?:[^"\\]|\\.)*"|“[^”]*”/g, '[source]').split(/(?<=[.!?])\s+/);
|
||
const observations = [
|
||
/^I open the README\.$/i,
|
||
/^Heading one is \[source\], and the first paragraph describes me exactly, so I keep reading\.$/i,
|
||
/^Under \[source\] I copy \[source\], export [A-Z][A-Z_]+, and run \[source\] as instructed\.$/,
|
||
/^Python says \[source\]\.$/,
|
||
/^I check site-packages: \w+ has \w+\.py, \w+\.py, \w+\.json, no examples folder\.$/i,
|
||
/^(?:\d+|Thirty) seconds lost, some trust lost\.$/i,
|
||
/^The next paragraph mentions \[source\], so I try that\.$/i,
|
||
/^It starts, then stderr prints \[source\]\.$/i,
|
||
/^I wanted a local score on bundled sample data; instead I['’]m waiting (?:\d+|five) minutes on a remote check I never configured, at \d+-second updates, with no flag to skip it\.$/i,
|
||
/^Peer SDK [A-Z] gave me a number in (?:\d+|two) minutes total\.$/i,
|
||
/^I alt-tab\.$/i,
|
||
/^Later the scores appear: \d+(?:\.\d+)?, \d+(?:\.\d+)?, \d+(?:\.\d+)?\.$/i,
|
||
/^Fine\.$/i,
|
||
/^I write my own call: \[source\]\.$/i,
|
||
/^Then I try \[source\] and it fails, because run_batch takes \(evaluator, dataset\)\.$/i,
|
||
/^I paste a typo['’]d key and get \[source\]: no code, no hint that the key is the problem\.$/i,
|
||
/^On my existing v\d+ code, \[source\] is now simply gone with no warning or migration note\.$/i,
|
||
];
|
||
return sentences.length > 0 && sentences.every(sentence => observations.some(pattern => pattern.test(sentence)));
|
||
}
|
||
|
||
/** Confirming a quoted developer journey authorizes understanding, not its repairs. */
|
||
function empathyAccuracyConfirmation(header: string, question: string, options: QuestionRecord['options']): boolean {
|
||
if (!/^(?:Empathy(?: narrative| trace)?|Narrative)$/i.test(header.trim()) ||
|
||
!options || options.length < 2 || options.length > 4) return false;
|
||
const clean = (value: string) => value.trim().replace(/\s*\(recommended\)\s*$/i, '').trim();
|
||
const confirm = (label: string) => /^(?:Accurate|Yes\s*[—–-]\s*accurate)\s*[—–-]\s*proceed(?: with this understanding)?$/i.test(clean(label));
|
||
const correct = (label: string) => /^(?:Part(?:ly|ially) wrong\s*[—–-]\s*let me correct it|Mostly right\s*[—–-]\s*minor corrections|Wrong path\s*[—–-]\s*the actual flow is different|Wrong\s*[—–-]\s*actual experience differs|The experience is different\s*[—–-]\s*let me describe it)$/i.test(clean(label));
|
||
const labels = options.map(option => clean(option.label));
|
||
if (new Set(labels).size !== labels.length || labels.filter(confirm).length !== 1 ||
|
||
!labels.some(correct) || !labels.every(label => confirm(label) || correct(label))) return false;
|
||
// Consume each description completely: an accuracy label must not also
|
||
// approve a remedy hidden in a subsequent sentence or clause.
|
||
const description = /^(?:(?:The (?:narrative|trace) is (?:correct|accurate)\.[ ]*)?Proceed with this understanding(?: for the full DX review)?\.|Some details are off; I['’]ll clarify (?:before we continue|the actual experience)\.|This matches the actual developer experience; use it as the basis for the review\.|The (?:real|actual) (?:getting-started path|flow|experience) differs(?: significantly)? from what was traced\.)$/i;
|
||
if (options.some(option => option.description && !description.test(clean(option.description)))) return false;
|
||
const ids = question.match(/<gstack-qid:[^>]+>/gi) ?? [];
|
||
if (ids.length > 1 || (question.match(/<gstack-qid/gi)?.length ?? 0) !== ids.length) return false;
|
||
const text = question.replace(/\s*<gstack-qid:[^>]+>\s*$/i, '').trim()
|
||
.replace(/^D\s*\d+\s*[—–:-]\s*/i, '');
|
||
const paragraphs = text.split(/\n\s*\n/);
|
||
const opening = paragraphs.shift() ?? '';
|
||
const closing = paragraphs.pop() ?? '';
|
||
if (!/^(?:Empathy (?:narrative|trace): does this match (?:(?:the [\w.-]+ (?:getting-started|onboarding|first-run) )?reality|your actual developer experience)\?|Does (?:this|the) (?:empathy narrative|first-person developer trace) match reality\?)$/i.test(opening) ||
|
||
!/^Does this match (?:reality|the actual experience)\?(?: Where am I wrong\?)?$/i.test(closing)) return false;
|
||
// Only quoted journey evidence and an observational preface may intervene.
|
||
// Additional questions or instructions outside the quote remain decisions.
|
||
const source = String.raw`(?:the docs|[\w-]+(?:[/.][\w-]+)+)`;
|
||
const role = String.raw`(?:(?:Python|JavaScript|TypeScript|Go|Rust|Java|Ruby) )?(?:(?:ML|backend|frontend|full-stack) )?(?:developer|engineer)`;
|
||
// A first-person journey may be delimited with horizontal rules instead
|
||
// of blockquotes. Keep its observation preface and both boundaries exact;
|
||
// an obligation outside that evidence is still a substantive decision.
|
||
const narrated = new RegExp(String.raw`^Here['’]s what I think a ${role} experiences today with [\w.-]+:$`, 'i');
|
||
if (narrated.test(paragraphs[0] ?? '')) {
|
||
const journey = paragraphs.slice(2, -1);
|
||
const observed = /^(?:I (?:find|found|open|read|run|try|install|look|wait|see|notice|receive|got|get|check|search|browse|start|follow)\b|After (?:scanning|reading|checking|searching|browsing)\b[^.!?\n]*\bI (?:find|spot|see|notice)\b)/i;
|
||
const decision = /\b(?:approv\w*|recommend\w*|suggest\w*|propos\w*|authoriz\w*|consent\w*|decid\w*|request\w*)\b|\b(?:should|could|can|may|must|shall|would) (?:we|you|I)\b|\b(?:we|you|I) (?:should|could|must|shall|will|would|need to|want to)\b|\blet['’]s\b|(?:^|[.!?;:]\s+|\b(?:please|also|then|and)\s+)(?:add|fix|package|remove|change|implement|enable|disable|repair|rewrite|apply|replace)\b/i;
|
||
// Every unquoted sentence must still describe an observation. Delimiters
|
||
// cannot turn a new imperative (including an unknown action verb) into
|
||
// quoted evidence. Explicit requests and obligations fail independently
|
||
// of which action they name.
|
||
const obligation = /\b(?:please|must|should|shall|ought|need(?:s)? to|ha(?:ve|s) to|required to)\b/i;
|
||
const sentences = journey.flatMap(part => part
|
||
.replace(/`[^`]*`|"(?:[^"\\]|\\.)*"|“[^”]*”/g, quote =>
|
||
'[source]' + (/[.!?]["”]$/.test(quote) ? quote.at(-2) : ''))
|
||
.split(/(?<=[.!?;])\s+/));
|
||
const observation = /^(?:(?:(?:Fine,|But)\s+)?I (?:find|found|open|read|run|try|install|look|wait|see|notice|receive|got|get|check|search|browse|start|follow|go|sit|lost|burned|don['’]t know)\b|After (?:scanning|reading|checking|searching|browsing)\b[^.!?\n]*\bI (?:find|spot|see|notice)\b|(?:The )?README (?:then says:|pointed me at)\s|First thing I see: install with \[source\]\.?$|Then: (?:set )?\[source\]\.?$|It starts [—–-] nothing happens\.?$|[\w]+ (?:seconds?|minutes?) (?:later: \[source\]|pass)\.?$|Wait, what\?$|A local demo needs a CI check\?$|Is something broken\?$|\[source\]\.?$)/i;
|
||
return paragraphs.length >= 4 && paragraphs[1] === '---' && paragraphs.at(-1) === '---' &&
|
||
journey.every(part => observed.test(part) && !decision.test(part) && !obligation.test(part)) &&
|
||
sentences.every(sentence => observation.test(sentence));
|
||
}
|
||
const goal = String.raw`(?: who just heard about [\w.-]+ and wants to verify it works locally before integrating it into their team['’]s CI pipeline)?`;
|
||
const preface = new RegExp(String.raw`^(?:Here['’]s what I (?:traced|observed) from ${source}(?:, ${source})*(?: and ${source})?\.\s*)?(?:The persona: ${role}${goal}\.)?$`, 'i');
|
||
let quoted = false;
|
||
for (const paragraph of paragraphs) {
|
||
if (paragraph.split('\n').every(line => /^\s*>/.test(line))) { quoted = true; continue; }
|
||
if (quoted || !preface.test(paragraph)) return false;
|
||
}
|
||
return quoted;
|
||
}
|
||
|
||
function administrativeQuestion(header: string, question: string, options: QuestionRecord['options']): boolean {
|
||
// These decisions establish the review's evidence and scope. Mentioning a
|
||
// defect in their recap does not turn a confirmation into a finding.
|
||
if (ADMINISTRATIVE_HEADERS.has(header.toLowerCase().replace(/\s+/g, ' ').trim())) return true;
|
||
if (empathyAccuracyConfirmation(header, question, options)) return true;
|
||
if (structuredEmpathyAccuracy(header, question, options)) return true;
|
||
if (/^empathy(?:\s*\(0B\))?$/i.test(header.trim()) &&
|
||
/^Does (?:this|the) empathy narrative match\b/i.test(question.replace(/^D\s*\d+\s*[—–:-]\s*/i, ''))) {
|
||
const labels = options?.map(option => option.label.trim().replace(/\s*\(recommended\)\s*$/i, '')) ?? [];
|
||
const confirm = (label: string) => /^Yes\s*[—–-]\s*accurate, proceed with this understanding$/i.test(label);
|
||
const correct = (label: string) => /^The experience is different\s*[—–-]\s*let me describe it$/i.test(label) ||
|
||
(/^Partially\s*[—–-]\s*(?:the [^;.!?]+? (?:does|is|has)|it (?:does|is|has)|there (?:is|are))\s+[^;.!?]+$/i.test(label) &&
|
||
!/\b(?:should|must|needs?|shall|will|would|could)\b|(?:[,::]|\b(?:and|then)\b)\s*(?:add|fix|package|remove|change|implement|enable|disable)\b/i.test(label));
|
||
if (labels.filter(confirm).length === 1 && labels.some(correct) && labels.every(label => confirm(label) || correct(label))) return true;
|
||
}
|
||
const narrativeHeader = header.trim().replace(/^D\s*\d+\s*(?:[—–:-]\s*)?/i, '');
|
||
const narrativeQuestion = question.replace(/^D\s*\d+\s*[—–:-]\s*/i, '');
|
||
if (/^Narrative$/i.test(narrativeHeader) &&
|
||
/^Does (?:this|the) first-person developer trace match reality\?/i.test(narrativeQuestion) &&
|
||
!/<gstack-qid/i.test(question)) {
|
||
const labels = options?.map(option => option.label.trim().replace(/\s*\(recommended\)\s*$/i, '')) ?? [];
|
||
const confirm = (label: string) => /^Accurate\s*[—–-]\s*proceed$/i.test(label);
|
||
const correct = (label: string) => /^(?:Mostly right\s*[—–-]\s*minor corrections|Wrong\s*[—–-]\s*actual experience differs)$/i.test(label);
|
||
const repair = /(?:^|[.!?]\s+|\b(?:and|then|also|please|must|should|will|need to|proceed to|continue to)\s+)(?:add|fix|package|remove|change|implement|enable|disable|repair|rewrite)\b/i;
|
||
// The captured trace has only its opening and closing accuracy questions.
|
||
// An additional question asks for another decision, even with accuracy labels.
|
||
const confirmationOnly = /^Does (?:this|the) first-person developer trace match reality\?[^?]*Does this match the actual experience\?\s*$/i.test(narrativeQuestion);
|
||
if (labels.filter(confirm).length === 1 && labels.some(correct) &&
|
||
new Set(labels).size === labels.length && labels.every(label => confirm(label) || correct(label)) &&
|
||
confirmationOnly && !repair.test(narrativeQuestion) &&
|
||
options!.every(option => !repair.test(option.description ?? ''))) return true;
|
||
}
|
||
const id = [...question.matchAll(/<gstack-qid:([^>]+)>/gi)].at(-1)?.[1];
|
||
if (id && /^(?:routing-injection|cross-project-learnings|plan-devex-review-(?:office-hours-preflight|prereq|persona|empathy(?:-check|-narrative)?|tthw-tier|competitive-tier|benchmark-tier|magical-moment|mode|confusion-report))$/i.test(id)) return true;
|
||
return /how deep should this dx review|which (?:dx )?review mode|\b(?:can|shall|should) we (?:continue|proceed|begin)(?: (?:the )?(?:setup|review)| now)?\?\s*$/i.test(question);
|
||
}
|
||
|
||
/** The answered native call proves a decision; its content must identify a concrete problem. */
|
||
function substantiveIssue({ header, question, options }: QuestionRecord): boolean {
|
||
if (administrativeQuestion(header, question, options)) return false;
|
||
const normalized = `${header} ${question}`.replace(/\s+/g, ' ');
|
||
const ciGate = /\b(?:CI|continuous integration)\b/i.test(normalized)
|
||
&& /\b(?:first[- ](?:local[- ])?runs?|first eval(?:uation)?|local eval(?:uation)?|hello world)\b/i.test(normalized)
|
||
&& /\b(?:mandatory|required|blocks?|five[- ]minute|5[- ]min(?:ute)?|wait|gate)\b/i.test(normalized);
|
||
const argumentsReversed = /\brun_eval\b/i.test(normalized) && /\brun_batch\b/i.test(normalized)
|
||
&& /\b(?:revers\w*|inconsisten\w*|swapp\w*|different|order|positional)\b/i.test(normalized);
|
||
const opaqueAuth = /\b(?:AuthError|API[- ]?key|authentication|invalid key)\b/i.test(normalized)
|
||
&& /request failed|\b(?:opaque|generic|unactionable|cryptic)\b|no (?:cause|guidance|fix|explanation|instruction)|doesn.t (?:explain|guide)/i.test(normalized);
|
||
const missingExample = /examples\/first_eval\.py|\b(?:packaged|quickstart|quick-start) example\b/i.test(normalized)
|
||
&& /\b(?:missing|absent|omitted|FileNotFoundError)\b|not (?:included|packaged|shipped)|doesn.t (?:exist|ship)/i.test(normalized);
|
||
const breakingRename = /Client\.evaluate|Client\.run|\bmethod rename\b/i.test(normalized)
|
||
&& /\b(?:breaking|remov\w*|renam\w*)\b/i.test(normalized)
|
||
&& /\b(?:migration|deprecation|compatibility|alias|codemod)\b/i.test(normalized);
|
||
// Expected-output documentation is separate from whether its command
|
||
// exists. Count the actual gap plus offered documentation remedy, not a
|
||
// generic navigation question that merely names output in its options.
|
||
const outputSubject = String.raw`(?:(?:expected|sample|example)(?: demo)?|demo) output`;
|
||
// Consume the complete noun phrase, including a negating determiner,
|
||
// before judging its absence. A nested "demo output" suffix cannot
|
||
// escape "no sample demo output is missing" and become a finding.
|
||
const missingState = [...normalized.matchAll(new RegExp(String.raw`\b(?:(no|not any)\s+)?${outputSubject}\s+(?:(?:is|are|was|were)\s+)?(?:missing|absent|omitted|unspecified)\b`, 'gi'))];
|
||
const missingSubject = [...normalized.matchAll(new RegExp(String.raw`\b(?:(no|not any)\s+)?missing\s+${outputSubject}\b`, 'gi'))];
|
||
const noOutput = new RegExp(String.raw`\bno\s+${outputSubject}\s*(?:[,.;!?]|\b(?:in|from|for|yet)\b)`, 'i');
|
||
const outputGap = missingState.some(match => !match[1]) || missingSubject.some(match => !match[1]) || noOutput.test(normalized);
|
||
const missingOutput = /\b(?:README|quick[- ]?start|documentation)\b/i.test(normalized)
|
||
&& (outputGap || /\b(?:README|quick[- ]?start|documentation)\b[^.!?;]{0,50}\b(?:doesn['’]t|does not)\s+(?:show|include)\b[^.!?;]{0,25}\boutput\b/i.test(normalized))
|
||
&& Boolean(options?.some(option => /^(?:[A-Z][.:)]\s*)?Add\s+(?:to\s+(?:the\s+)?plan:\s*include\s+)?(?:an?\s+)?(?:expected|sample|example)(?:\s+demo)?\s+output\b[^.!?]*\b(?:README|quick[- ]?start|documentation)\b/i.test(option.label)));
|
||
return ciGate || argumentsReversed || opaqueAuth || missingExample || breakingRename || missingOutput;
|
||
}
|
||
|
||
/** A setup heading cannot hide a positively selected repair to the existing behavior. */
|
||
function answeredSetupRepair(fp: AskUserQuestionFingerprint): boolean {
|
||
const call = fp.nativeCall;
|
||
if (!call || call.failed || !call.answered || call.questions.length !== 1 ||
|
||
call.unansweredQuestionIndices?.length || fp.signature !== `${call.sessionId}:${call.toolUseId}`) return false;
|
||
const question = call.questions[0]!;
|
||
if (question.multiSelect) return false;
|
||
const selected = question.options.filter(option => option.label === call.answers?.[question.question]);
|
||
if (selected.length !== 1) return false;
|
||
const ids = [...question.question.matchAll(/<gstack-qid:([a-z0-9-]+)>/gi)];
|
||
if (ids.length !== 1 || (question.question.match(/<gstack-qid/gi)?.length ?? 0) !== 1) return false;
|
||
const id = ids[0]![1]!.toLowerCase();
|
||
const header = question.header.trim().replace(/^D\s*\d+\s*[—–:-]\s*/i, '');
|
||
const text = question.question.replace(/\s+/g, ' ');
|
||
const label = selected[0]!.label.replace(/^[A-Z][.):]\s*/i, '');
|
||
if (id === 'plan-devex-review-tthw-tier' && /^TTHW target$/i.test(header)) {
|
||
return /TTHW|Time-to-Hello-World/i.test(text) && /\bCI\b/i.test(text) &&
|
||
/\b(?:mandatory|blocks?|retains? the CI block)\b/i.test(text) &&
|
||
/(?:^|[—–:]\s*)add\s+(?:an?\s+)?(?:skip flag|--skip-ci|offline(?:[- ]first[- ]run)? path)\b/i.test(label);
|
||
}
|
||
if (id === 'plan-devex-review-tthw-ci-block' && /^TTHW target$/i.test(header)) {
|
||
// A confirmed benchmark does not approve a new CI bypass. This captured
|
||
// menu asserts the broken target and selects an explicit repair.
|
||
const headline = question.question.split('\n')[0]!.replace(/<gstack-qid:[^>]+>/i, '').trim();
|
||
return Array.isArray(call.unansweredQuestionIndices) && call.unansweredQuestionIndices.length === 0 &&
|
||
/^D\s*\d+\s*[—–:-]\s*Journey Stage HELLO WORLD:\s*The \d+[- ]minute mandatory CI block makes the under-\d+[- ]minute TTHW target unreachable\.\s*$/i.test(headline) &&
|
||
/^Add (?:a )?demo-mode CI skip flag(?:\s*\(Recommended\))?$/i.test(label);
|
||
}
|
||
if (id === 'plan-devex-review-magical-moment' && /^Magical moment$/i.test(header)) {
|
||
// The selected option adds progress feedback beyond the already chosen
|
||
// demo vehicle and prior CI-bypass decision. An unselected remedy or
|
||
// a confirmation of that vehicle alone remains setup.
|
||
return /\bdemo\b/i.test(text) && /\bsilently blocks?\b|\bsilent (?:CI )?wait\b/i.test(text) &&
|
||
/(?:^|[—–:]\s*)add\s+[^.!?;]{0,80}\bprogress (?:output|indicator)\b/i.test(label);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** A current first-pass repair can name the broken contract without its file path. */
|
||
function answeredContractRepair(fp: AskUserQuestionFingerprint): boolean {
|
||
const call = fp.nativeCall;
|
||
if (!call?.answered || call.failed || call.questions.length !== 1 ||
|
||
!Array.isArray(call.unansweredQuestionIndices) || call.unansweredQuestionIndices.length || fp.signature !== `${call.sessionId}:${call.toolUseId}`) return false;
|
||
const q = call.questions[0]!;
|
||
if (q.multiSelect || q.options.length < 2 || new Set(q.options.map(o => o.label)).size !== q.options.length ||
|
||
q.options.filter(o => o.label === call.answers?.[q.question]).length !== 1 ||
|
||
administrativeQuestion(q.header, q.question, q.options)) return false;
|
||
const ids = [...q.question.matchAll(/<gstack-qid:([^>]+)>/gi)];
|
||
if (ids.length !== 1 || (q.question.match(/<gstack-qid/gi)?.length ?? 0) !== 1) return false;
|
||
if (/^(?:plan-)?devex-(?:review-)?[a-z0-9-]+$/i.test(ids[0]![1]!) &&
|
||
!/(?:^|-)(?:mode|setup|scope|routing|prerequisite|next-steps?)(?:-|$)/i.test(ids[0]![1]!) &&
|
||
/^TTHW block$/i.test(q.header.trim())) {
|
||
// The retained CI wait contradicts an agreed target; this is an accepted
|
||
// repair decision, not selection or confirmation of the target itself.
|
||
if (call.answered !== true || call.failed !== false ||
|
||
fp.options.length !== q.options.length || !fp.options.every((o, i) =>
|
||
o.index === i + 1 && o.label === q.options[i]!.label)) return false;
|
||
const body = q.question.replace(/\s*<gstack-qid:[^>]+>\s*$/i, '').trim();
|
||
const timing = /^D\s*\d+\s*[—–:-]\s*Pass 1 \(Getting Started\): The agreed <(\d+(?:\.\d+)?) min TTHW target is mathematically impossible with the retained (\d+(?:\.\d+)?)[- ](?:min|minute) CI block\. Which resolution belongs in the plan\?$/i.exec(body);
|
||
if (!timing) return false;
|
||
const [target, wait] = timing.slice(1).map(Number);
|
||
const selected = call.answers![q.question]!.replace(/\s*\(Recommended\)\s*$/i, '').trim();
|
||
return [target, wait].every(n => Number.isFinite(n) && n! > 0) && wait! >= target! &&
|
||
/^(?:Demo-only CI bypass|Add --offline flag to [a-z_$][\w$.-]*|Update TTHW target to reflect reality)$/i.test(selected);
|
||
}
|
||
if (ids[0]![1] === 'devex-demo-ci-bypass') {
|
||
// A demo is a first result too. Require an affirmative measured timing
|
||
// contradiction and a direct bypass decision, not benchmark confirmation.
|
||
if (call.failed !== false || !/^Demo CI gate$/i.test(q.header.trim()) ||
|
||
/(?:^|\n)[ \t]*(?:>|`{3}|~{3}|example:)/im.test(q.question)) return false;
|
||
const headline = /^D\s*\d+\s*[—–:-]\s*[a-z][a-z0-9 -]{0,60} demo command: should it bypass the mandatory CI check to reach the <(\d+(?:\.\d+)?) min TTHW target\?$/i.exec(q.question.split('\n')[0]!.trim());
|
||
const timing = /^ELI10:\s*The agreed onboarding target is under (\d+(?:\.\d+)?) minutes(?: \([^\n)]+\))?\.\s+Today `[^`\n]+` blocks for (\d+(?:\.\d+)?) minutes waiting for a CI check, giving a measured TTHW of (\d+(?:\.\d+)?) minutes(?: [—–-] Red Flag tier vs\. Competitor [A-Z]['’]s \d+(?:\.\d+)? minutes)?\.(?:\s|$)/im.exec(q.question);
|
||
if (!headline || !timing) return false;
|
||
const [target, wait, measured] = timing.slice(1).map(Number);
|
||
return [target, wait, measured].every(n => Number.isFinite(n) && n! > 0) &&
|
||
Number(headline[1]) === target && wait! >= target! && measured! >= wait!;
|
||
}
|
||
if (
|
||
!/^plan-devex-(?:review-)?[a-z0-9-]+$/i.test(ids[0]![1]!) ||
|
||
/(?:^|-)(?:mode|setup|scope|routing|prerequisite|next-steps?)(?:-|$)/i.test(ids[0]![1]!)) return false;
|
||
const body = q.question.replace(/<gstack-qid:[^>]+>/i, '').trim().replace(/\s+/g, ' ');
|
||
if (!/^D\s*\d+\s*[—–:-]\s*Pass\s+1\s*\(Getting Started\):/i.test(body)) return false;
|
||
const statement = body.replace(/^D\s*\d+\s*[—–:-]\s*Pass\s+1\s*\(Getting Started\):\s*/i, '');
|
||
const absentPackageFile = /^(?:The )?(?:README )?quickstart points to a file that doesn['’]t exist in the (?:published )?package\b/i.test(statement) &&
|
||
/\bhow should (?:the plan|we) fix (?:it|this)\?$/i.test(body);
|
||
const conflictingGate = /^(?:The )?plan targets TTHW\b[^.!?]*\bbut retains a mandatory\b[^.!?]*\bCI gate with no skip path\b/i.test(statement) &&
|
||
/\b(?:these are mutually exclusive|these contradict each other)\b/i.test(body) &&
|
||
/\bhow should (?:the plan|we) resolve (?:this|it)\?$/i.test(body);
|
||
// A first-run decision may describe shipment, or compare the measured gate
|
||
// directly with the benchmark. Require the complete affirmative claim and
|
||
// its repair question; setup/quoted/negated recaps still fail above/below.
|
||
const completedNative = call.answered === true && call.failed === false;
|
||
const unshippedQuickstart = completedNative &&
|
||
/^(?:The )?(?:README )?quickstart points to a file that doesn['’]t ship in the (?:published )?package\. Should we fix the quickstart path in the plan\?$/i.test(statement);
|
||
const unreachableBenchmark = completedNative &&
|
||
/^(?:The )?benchmarks set an? <\d+(?:\.\d+)? min TTHW target, but the mandatory \d+(?:\.\d+)?[- ]minute CI gate makes that unreachable\. The plan retains the gate\. How should this plan handle the contradiction\?$/i.test(statement);
|
||
return absentPackageFile || conflictingGate || unshippedQuickstart || unreachableBenchmark;
|
||
}
|
||
|
||
/** An explicitly quoted developer account plus accuracy-only choices adds no repair. */
|
||
function answeredQuotedAccuracy(fp: AskUserQuestionFingerprint): boolean {
|
||
const call = fp.nativeCall;
|
||
if (!call?.sessionId || !call.toolUseId || call.answered !== true || call.failed !== false ||
|
||
call.questions.length !== 1 || fp.signature !== `${call.sessionId}:${call.toolUseId}` ||
|
||
!Array.isArray(call.unansweredQuestionIndices) || call.unansweredQuestionIndices.length ||
|
||
Object.keys(call.answers ?? {}).length !== 1 || !Number.isFinite(Date.parse(call.answeredAt ?? ''))) return false;
|
||
const q = call.questions[0]!;
|
||
if (q.header !== 'Narrative' || q.multiSelect || q.options.length !== 3 || fp.options.length !== 3 ||
|
||
!fp.options.every((o,i) => o.index === i+1 && o.label === q.options[i]!.label) ||
|
||
!q.options.some(o => call.answers?.[q.question] === o.label) || /<gstack-qid/i.test(q.question)) return false;
|
||
const expectedOptions = [
|
||
['This is accurate, proceed', 'Use this narrative as the Developer Perspective section and continue to friction-point decisions.'],
|
||
['Some of this is wrong, let me correct it', 'Tell me which steps differ; I will fold corrections in before scoring.'],
|
||
['This is way off, the actual experience is...', 'Describe the real flow and I will rebuild the narrative from it.'],
|
||
];
|
||
if (!q.options.every((o,i) => o.label.replace(/ \(recommended\)$/i, '') === expectedOptions[i]![0] && o.description === expectedOptions[i]![1])) return false;
|
||
const parts = q.question.replace(/^D\d+\s*[—–-]\s*/, '').split(/\n\s*\n/);
|
||
if (parts.length < 5 || parts[0] !== 'Empathy narrative: does this match what your ML engineer experiences today?' ||
|
||
!/^Project\/branch\/task: [\w/-]+ branch, [\w. -]+ beta polish, tracing the README getting-started path as written\.$/.test(parts[1]!) ||
|
||
parts[2] !== 'Here is what I think your ML engineer experiences today:') return false;
|
||
const quoted = parts.slice(3,-1).join('\n\n');
|
||
// These are source words in an explicitly bounded quotation, not approval
|
||
// of any action they mention. No unquoted paragraph may intervene.
|
||
if (!/^"I [\s\S]+"$/.test(quoted) || (quoted.match(/"/g)?.length ?? 0) !== 2) return false;
|
||
const explanatory = [
|
||
"ELI10: This narrative becomes the 'Developer Perspective' section the implementer reads. If it is wrong, the whole review is calibrated against a fake developer.",
|
||
'Stakes if we pick wrong: we fix friction your developer never hits, or miss the one that actually loses them.',
|
||
'Recommendation: A because every step above quotes a documented contract in README.md, docs/api.md, docs/current-contracts.md, or docs/package-contents.txt rather than a guess.',
|
||
'Note: options differ in kind, not coverage — no completeness score.',
|
||
'A) This is accurate, proceed with this understanding (recommended)',
|
||
'✅ Every friction point is grounded in a specific documented line, not hypothesized',
|
||
'✅ Lets the review move straight to per-friction-point decisions with shared context',
|
||
'❌ If the docs lag the real runtime, a fixed contract could be reviewed as if still broken',
|
||
'B) Some of this is wrong, let me correct it',
|
||
'✅ Corrections get folded into the narrative before any scoring happens',
|
||
'✅ Catches doc-versus-runtime drift the repo cannot show me',
|
||
'❌ Requires you to spell out which steps differ and how',
|
||
'C) This is way off, the actual experience is...',
|
||
'✅ Resets the review against your real onboarding flow',
|
||
'✅ Prevents scoring against contracts that no longer exist',
|
||
'❌ Discards a trace that matches the docs line for line, so the docs would also need fixing',
|
||
'Net: trading trust in the checked-in docs against knowledge only you have about the live SDK.',
|
||
];
|
||
const tail = parts.at(-1)!.split('\n').map(line => line.trim());
|
||
return tail.length === explanatory.length && tail.every((line,i) => line === explanatory[i]);
|
||
}
|
||
|
||
/** A missing release measurement is new work even though its benchmark already exists. */
|
||
function answeredMeasurementGate(fp: AskUserQuestionFingerprint): boolean {
|
||
const call = fp.nativeCall;
|
||
if (!call?.sessionId || !call.toolUseId || call.answered !== true || call.failed !== false ||
|
||
call.questions.length !== 1 || fp.signature !== `${call.sessionId}:${call.toolUseId}` ||
|
||
!Array.isArray(call.unansweredQuestionIndices) || call.unansweredQuestionIndices.length ||
|
||
Object.keys(call.answers ?? {}).length !== 1 || !Number.isFinite(Date.parse(call.answeredAt ?? ''))) return false;
|
||
const q = call.questions[0]!;
|
||
if (q.header !== 'Measurement' || q.multiSelect || q.options.length < 2 || q.options.length > 4 ||
|
||
new Set(q.options.map(o => o.label)).size !== q.options.length || fp.options.length !== q.options.length ||
|
||
!fp.options.every((o,i) => o.index === i+1 && o.label === q.options[i]!.label) ||
|
||
!q.options.some(o => call.answers?.[q.question] === o.label) || /<gstack-qid/i.test(q.question)) return false;
|
||
const title = q.question.split('\n')[0]!.replace(/^D\d+\s*[—–-]\s*/, '');
|
||
return /^Pass \d+ \(DX Measurement\): the < \d+(?:\.\d+)? min target is asserted but never re-measured after the fixes\.$/.test(title) &&
|
||
/^Evidence: [^\n]+\. Nothing in the plan re-runs that same study after D\d+[–-]D\d+ land, so the beta could ship with the target still unmet and nobody would know until the survey\.$/m.test(q.question) &&
|
||
q.options.some(o => /^Fix in plan: re-run study as ship gate, record demo and live TTHW(?: \(recommended\))?$/.test(o.label) &&
|
||
/^Same protocol as docs\/benchmarks\.md on the release candidate; demo TTHW < \d+(?:\.\d+)? min required before tagging\.$/.test(o.description ?? ''));
|
||
}
|
||
|
||
/** A recap can confirm existing approvals, but its text cannot manufacture them. */
|
||
function answeredRoleplayRecap(fp: AskUserQuestionFingerprint, priorCalls: readonly NativePlanQuestionCall[]): boolean {
|
||
const call = fp.nativeCall;
|
||
const completed = (c: NativePlanQuestionCall) => c.answered === true && c.failed === false &&
|
||
Boolean(c.sessionId && c.toolUseId) && c.questions.length === 1 && !c.questions[0]!.multiSelect &&
|
||
Array.isArray(c.unansweredQuestionIndices) && c.unansweredQuestionIndices.length === 0 &&
|
||
Object.keys(c.answers ?? {}).length === 1 && Number.isFinite(Date.parse(c.answeredAt ?? '')) &&
|
||
c.questions[0]!.options.filter(o => o.label === c.answers?.[c.questions[0]!.question]).length === 1;
|
||
if (!call || !completed(call) || fp.signature !== `${call.sessionId}:${call.toolUseId}` ||
|
||
(fp.nativeQuestionIndex !== undefined && fp.nativeQuestionIndex !== 0)) return false;
|
||
const q = call.questions[0]!;
|
||
if (q.header !== 'Roleplay' || q.options.length !== 4 || fp.options.length !== 4 ||
|
||
!fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) ||
|
||
new Set(q.options.map(o => o.label)).size !== 4 || /<gstack-qid/i.test(q.question)) return false;
|
||
const labels = q.options.map(o => o.label.replace(/ \(recommended\)$/i, ''));
|
||
if (labels.join('|') !== 'All of them, fix every confusion point|Let me pick which ones matter|Critical ones only (#1, #2, #5)|This is unrealistic, our developers already know the context' ||
|
||
call.answers?.[q.question] !== q.options[0]!.label) return false;
|
||
const mapping = /^Address #1 through #(\d+), matching the D(\d+)[–-]D(\d+) decisions\.$/.exec(q.options[0]!.description ?? '');
|
||
if (!mapping) return false;
|
||
const [size, first, last] = mapping.slice(1).map(Number);
|
||
if (size !== 5 || last! - first! + 1 !== size || first! < 1 || last! > 1000) return false;
|
||
if (q.options[1]!.description !== 'Tell me which numbers to keep and which to drop.' ||
|
||
q.options[2]!.description !== 'Fix quickstart, CI gate, and upgrade; leave signature order and auth error.' ||
|
||
q.options[3]!.description !== 'Skip the confusion points; keep contracts as drafted.') return false;
|
||
const prior: NativePlanQuestionCall[] = [];
|
||
for (let decision = first!; decision <= last!; decision++) {
|
||
const matches = priorCalls.filter(c => c.sessionId === call.sessionId && completed(c) &&
|
||
c.toolUseId !== call.toolUseId && Date.parse(c.answeredAt!) < Date.parse(call.answeredAt!) &&
|
||
new RegExp(`^D${decision}\\s*[—–-]\\s*`).test(c.questions[0]!.question));
|
||
if (matches.length !== 1 || !/^Fix in plan:/.test(matches[0]!.answers![matches[0]!.questions[0]!.question]!)) return false;
|
||
prior.push(matches[0]!);
|
||
}
|
||
// Each observed confusion point refers to the same already-approved contract.
|
||
// The fixture's five independent defects remain explicit; new measurement,
|
||
// documentation or TODO decisions do not enter this confirmation path.
|
||
const subjects = [/examples\/first_eval\.py/, /\bCI\b/, /\brun_eval\b[\s\S]*\brun_batch\b|\brun_batch\b[\s\S]*\brun_eval\b/, /\bAuthError\b/, /Client\.evaluate\(\)/i];
|
||
if (prior.some((c, i) => !subjects[i]!.test(c.questions[0]!.question))) return false;
|
||
// Sharing a subject or a "fix" prefix is not approval of this remedy. Bind
|
||
// each chosen option and its entire consequence to the contract recapped.
|
||
const approvedRepairs = [
|
||
['Fix in plan: demo-first quickstart + resolve first_eval.py', 'README leads with python -m evalkit.demo; ship or remove first_eval.py; add a packaging check for documented paths.'],
|
||
['Fix in plan: no CI check on mock-transport runs; gate the first live eval instead', 'Demo returns immediately; CI check with existing progress/timeout messaging moves to the first keyed evaluation.'],
|
||
['Fix in plan: align order + keyword-only + clear TypeError', 'run_batch(dataset, evaluator) matching run_eval; keyword-only enforcement; positional misuse raises a TypeError naming the expected call.'],
|
||
['Fix in plan: coded, causal AuthError with fix and redaction', 'Error code, key source, cause, console fix URL, redacted key prefix, help link. Matches the existing error pattern.'],
|
||
['Fix in plan: alias + DeprecationWarning + migration guide + codemod', 'evaluate() delegates to run() with a warning through 2.x betas; changelog and docs/api.md gain a migration section; sed/codemod recipe shipped.'],
|
||
];
|
||
if (prior.some((c, i) => {
|
||
const question = c.questions[0]!;
|
||
const selected = question.options.find(o => o.label === c.answers![question.question])!;
|
||
return selected.label.replace(/ \(recommended\)$/i, '') !== approvedRepairs[i]![0] ||
|
||
selected.description !== approvedRepairs[i]![1];
|
||
})) return false;
|
||
const parts = q.question.replace(/^D\d+\s*[—–-]\s*/, '').split(/\n\s*\n/);
|
||
if (parts.length !== 5 || parts[0] !== 'First-time developer roleplay: which confusion points should the plan address?' ||
|
||
!/^Project\/branch\/task: [\w/-]+ branch, [\w. -]+ beta polish; roleplayed your ML engineer through the README as written\.$/.test(parts[1]!) ||
|
||
parts[2] !== 'I roleplayed as your ML engineer attempting the getting started flow. Here is what confused me, with timestamps:') return false;
|
||
const observed = parts[3]!.split('\n');
|
||
const source = String.raw`[\w./-]+:\d+(?:-\d+)?`;
|
||
const observation = [
|
||
new RegExp(String.raw`^T\+\d+:\d+ +#1 \x60python examples/first_eval\.py\x60 fails: file not in package or archive \(${source}, ${source}\)\. "[^"\n]+"$`),
|
||
new RegExp(String.raw`^T\+\d+:\d+ +#2 Keyless demo starts a remote CI check on a sample-project binding I never created \(${source}, ${source}\)\. "[^"\n]+"$`),
|
||
new RegExp(String.raw`^T\+\d+:\d+ +Scores print\. Works, but \d+ min vs the \d+ min target \(${source}\)\. Impression: slow\.$`),
|
||
new RegExp(String.raw`^T\+\d+:\d+ +#3 run_batch fails inside the evaluator because its argument order is the reverse of run_eval \(${source}\)\. "[^"\n]+"$`),
|
||
new RegExp(String.raw`^T\+\d+:\d+ +#4 \x60AuthError: request failed\x60 on a wrong-project key; I check network and server status first because nothing says "key" \(${source}\)\.$`),
|
||
new RegExp(String.raw`^T\+\d+:\d+ +#5 v1 project upgraded: every client\.evaluate\(\) raises AttributeError; changelog has no migration entry \(${source}\)\. Final state: file an issue or pin v1\.$`),
|
||
];
|
||
if (observed.length !== observation.length || observed.some((line, i) => !observation[i]!.test(line))) return false;
|
||
// Consume the complete decision explanation too. Additional work under a
|
||
// valid heading or in a choice description must remain substantive.
|
||
const range = `D${first}–D${last}`;
|
||
const tail = parts[4]!.replace(new RegExp(`D${first}[–-]D${last}`, 'g'), range).split('\n');
|
||
const expected = [
|
||
'ELI10: Each numbered point is a place a real first-time user stops and asks a question nobody is there to answer. The plan should remove every one it reasonably can.',
|
||
'Stakes if we pick wrong: leave one in and that is the step where the developer\'s session ends; each maps to a contract PLAN.md explicitly asked to be reviewed.',
|
||
`Recommendation: A because all five map one-to-one to the ${range} decisions you already resolved as "fix in plan", so addressing all of them is consistent with those calls.`,
|
||
'Completeness: A=10/10, B=depends on selection, C=6/10, D=1/10',
|
||
'A) All of them, fix every confusion point (recommended)',
|
||
`✅ Consistent with ${range}; every confusion point already has an agreed fix`,
|
||
'✅ Leaves no known dead end in the first 30 minutes of use',
|
||
'❌ Full set of fixes touches README, client.py, demo gate, error class, and changelog (human: ~3 days / CC: ~1.5 hours)',
|
||
'B) Let me pick which ones matter',
|
||
'✅ Lets you drop a point if you know something the docs do not show',
|
||
'✅ Keeps the plan focused on what you consider blocking',
|
||
`❌ Reopens decisions ${range} that were just settled`,
|
||
'C) The critical ones only (#1, #2, #5), skip #3 and #4',
|
||
'✅ Covers the broken quickstart, the TTHW blocker, and the upgrade break',
|
||
'✅ Smaller diff to review',
|
||
'❌ Ships an inconsistent API and an undiagnosable auth error in a DX polish release',
|
||
'D) This is unrealistic, our developers already know the context',
|
||
'✅ Zero work now',
|
||
'✅ Valid if every beta user is internal and already trained',
|
||
'❌ README.md:3-5 describes an external ML engineer meeting the SDK fresh, which contradicts this',
|
||
'Net: trading a known, already-scoped set of fixes against leaving a documented dead end in the first session.',
|
||
];
|
||
return tail.length === expected.length && tail.every((line, i) => line.trim() === expected[i]);
|
||
}
|
||
|
||
/** A batched native call remains one decision; the caller owns call-ID deduplication. */
|
||
export function isDevexReviewIssue(fp: AskUserQuestionFingerprint, priorCalls: readonly NativePlanQuestionCall[] = []): boolean {
|
||
if (answeredQuotedAccuracy(fp) || answeredRoleplayRecap(fp, priorCalls)) return false;
|
||
return answeredMeasurementGate(fp) || answeredSetupRepair(fp) || answeredContractRepair(fp) || answeredKeylessDemoRepair(fp) || answeredDocumentationFollowup(fp) || questionRecords(fp, true).some(substantiveIssue);
|
||
}
|
||
|
||
/** Key acquisition docs and eliminating the demo's key requirement are distinct work. */
|
||
function answeredKeylessDemoRepair(fp: AskUserQuestionFingerprint): boolean {
|
||
const call = fp.nativeCall;
|
||
if (call?.answered !== true || call.failed !== false || call.questions.length !== 1 ||
|
||
!Array.isArray(call.unansweredQuestionIndices) || call.unansweredQuestionIndices.length ||
|
||
fp.signature !== `${call.sessionId}:${call.toolUseId}`) return false;
|
||
const q = call.questions[0]!;
|
||
if (q.multiSelect || q.options.length < 2 || new Set(q.options.map(o => o.label)).size !== q.options.length ||
|
||
fp.options.length !== q.options.length || fp.options.some((o, i) => o.index !== i + 1 || o.label !== q.options[i]!.label) ||
|
||
!/^Golden path$/i.test(q.header.trim()) || /<gstack-qid/i.test(q.question)) return false;
|
||
const selected = q.options.filter(o => o.label === call.answers?.[q.question]);
|
||
if (selected.length !== 1 || !/^Install, demo, then key(?: \(recommended\))?$/i.test(selected[0]!.label) ||
|
||
!/\bDemo path is guaranteed keyless and offline; if the runtime currently insists on a key for the demo, remove that check\b/.test(selected[0]!.description ?? '')) return false;
|
||
const lines = q.question.split('\n');
|
||
return /^D\s*\d+\s*[—–:-]\s*Pass 1 Getting Started \((?:10|[0-9])\/10 today\): should the golden path put the demo BEFORE the API key step\?$/i.test(lines[0]!) &&
|
||
/^ELI10: Today README "Getting started" \(lines \d+-\d+\) reads install, set [A-Z][A-Z_]+, run a missing file\./m.test(q.question);
|
||
}
|
||
|
||
/** New documentation and example obligations are separate from the original repairs. */
|
||
function answeredDocumentationFollowup(fp: AskUserQuestionFingerprint): boolean {
|
||
const call = fp.nativeCall;
|
||
if (!call?.answered || call.failed !== false || call.questions.length !== 1 ||
|
||
!Array.isArray(call.unansweredQuestionIndices) || call.unansweredQuestionIndices.length ||
|
||
fp.signature !== `${call.sessionId}:${call.toolUseId}`) return false;
|
||
const q = call.questions[0]!;
|
||
if (q.multiSelect || q.options.length < 2 || new Set(q.options.map(o => o.label)).size !== q.options.length ||
|
||
administrativeQuestion(q.header, q.question, q.options)) return false;
|
||
const selected = q.options.filter(o => o.label === call.answers?.[q.question]);
|
||
const ids = [...q.question.matchAll(/<gstack-qid:([^>]+)>/gi)];
|
||
if (selected.length !== 1 || ids.length !== 1 || (q.question.match(/<gstack-qid/gi)?.length ?? 0) !== 1) return false;
|
||
const label = selected[0]!.label.trim().replace(/^[A-Z][.):]\s*/i, '').replace(/\s*\(recommended\)$/i, '');
|
||
const headline = q.question.split('\n')[0]!.replace(/<gstack-qid:[^>]+>/i, '').trim();
|
||
if (/^plan-devex-review-todo\d+-migration-guide$/i.test(ids[0]![1]!) && /^TODO[- ]\d+ Migration$/i.test(q.header.trim())) {
|
||
// A written upgrade guide is additional work beyond the accepted runtime
|
||
// compatibility shim. Require that distinct gap and the selected doc task;
|
||
// a recap, hypothetical example or unselected guide cannot supply it.
|
||
const parts = q.question.replace(/\s*<gstack-qid:[^>]+>\s*$/i, '').trim().split(/\n\s*\n/);
|
||
const compact = (text: string | undefined) => (text ?? '').replace(/\s+/g, ' ').trim();
|
||
return call.answered === true && parts.length === 5 &&
|
||
/^D\s*\d+\s*[—–:-]\s*TODO: should the plan include a v\d+→v\d+ written migration guide\?$/i.test(compact(parts[0])) &&
|
||
/^The deprecation shim \(T\d+\) handles the runtime experience: v\d+ callers get a DeprecationWarning naming `[a-z_]\w*\(\)` as the replacement\. But there is currently no written migration guide in docs\/\.$/i.test(compact(parts[1])) &&
|
||
/^A one-page migration guide covers: - What changed \(`[a-z_]\w*\(\)` → `[a-z_]\w*\(\)`\) - What stayed the same \(all other APIs\) - How to find and update callsites \(grep for `[^`\n]+`\) - When the shim is removed \(e\.g\., v\d+(?:\.\d+)?\)$/i.test(compact(parts[2])) &&
|
||
/^Without it, developers upgrading a large codebase need to discover the change at each call site rather than planning the migration upfront\. The changelog has the what; the guide provides the how and the timeline\.$/i.test(compact(parts[3])) &&
|
||
/^Completeness: A=(?:10|[0-9])\/10 \(complete\), B=(?:10|[0-9])\/10 \(runtime-only, no planning\), C=(?:10|[0-9])\/10$/i.test(compact(parts[4])) &&
|
||
/^Add to TODOS\.md [—–-] include migration guide in plan$/i.test(label) &&
|
||
/^Add docs\/migration-v\d+-v\d+\.md as a P[0-3] task\. One page covering the rename, unchanged APIs, grep command to find callsites, and shim removal timeline\. Completeness: (?:10|[0-9])\/10\.$/i.test(compact(selected[0]!.description));
|
||
}
|
||
if (ids[0]![1] === 'devex-api-key-docs' && /^API key docs$/i.test(q.header.trim())) {
|
||
// The earlier auth-error decision changes runtime diagnostics. This one
|
||
// adds the missing acquisition instructions to the README itself.
|
||
return /^D\s*\d+\s*[—–:-]\s*Pass\s+\d+:\s*Documentation\s*[—–:-]\s*README says ['"][^'"]+['"] but never says where to get one\.$/i.test(headline) &&
|
||
/^Add key acquisition link to README$/i.test(label);
|
||
}
|
||
if (ids[0]![1] === 'devex-todo-real-world-examples' && /^TODO examples$/i.test(q.header.trim())) {
|
||
// The quickstart repair supplies one missing file. These additional
|
||
// custom-data examples are an independently accepted follow-up obligation.
|
||
return /^D\s*\d+\s*[—–:-]\s*TODO check:\s*Real-world examples beyond the bundled sample data\?$/i.test(headline) &&
|
||
/^\*\*What:\*\* Add \d+(?:-\d+)? additional examples\/ files showing real use cases\b/m.test(q.question) &&
|
||
/^(?:Add to TODOS\.md for post-beta|Build it now as part of this plan)$/i.test(label);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** Select POLISH only on the recognized mode menu; leave all other answers unchanged. */
|
||
export function devexReviewModePick(fp: AskUserQuestionFingerprint): number | null {
|
||
if (fp.nativeCall && fp.nativeCall.questions.length !== 1) return null;
|
||
const record = questionRecords(fp)[0];
|
||
const text = record ? `${record.header} ${record.question}` : '';
|
||
if (!/<gstack-qid:plan-devex-review-mode>/i.test(text)
|
||
&& !/how\s*deep\s*should\s*this\s*dx\s*review|which\s*(?:dx\s*)?review\s*mode/i.test(text)) return null;
|
||
const modes = fp.options.map(option => ({
|
||
index: option.index,
|
||
mode: /^(?:[A-C][.)])?DX(POLISH|EXPANSION|TRIAGE)(?:$|[^A-Z])/.exec(
|
||
option.label.split(/[│┌\r\n]/, 1)[0]!.replace(/\s+/g, '').toUpperCase(),
|
||
)?.[1],
|
||
}));
|
||
if (!['POLISH', 'EXPANSION', 'TRIAGE'].every(mode => modes.filter(option => option.mode === mode).length === 1)) return null;
|
||
return modes.find(option => option.mode === 'POLISH')!.index;
|
||
}
|