Files
gstack/test/helpers/planning-wave-task.ts
Garry Tanandcapy-ai[bot] 8695938e74 v1.92.15.0 feat: plan reviews check facts and keep one task list; /office-hours hands its wireframe to design review (#340, #513, #973, #1048, #1791) (#3775)
* G1: gstack-plan-reality check opens every file:line receipt (#973)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* G1: office-hours spec reviewer checks paths and flags through a bin-enforced probe (#973)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* G2: plan-review reruns reconcile the last task list (gstack-tasks prior/supersede) (#1791)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* G2: plan reviews write task lists through gstack-tasks start/add/close

The emit block shrinks to three calls: prior (now defaulting --dir and
--branch, and printing how to reconcile), start (creates the empty
branch-named list) and add (one validated row per task). close records a
done or dropped prior task. The plan-review result codes move to
lib/result-codes-plan.ts so lib/result-codes.ts stays under the module-size
ratchet; generated plan-review sections shrink, so their size pins go down.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* F2: office-hours saves the sketch beside the design doc; /plan-design-review reads it

Step 5 copies sketch.png/html to <PROJECT_DIR>/{user}-{branch}-sketch-{datetime}.*
and writes a Wireframe: line (state-root relative) or Wireframe: none (<reason>).
The design review section finds the design doc before Pass 1 and opens the
wireframe, text-only when the file is missing. G4's signals line rides along
in the same generated office-hours files; tests in planning-wave-prose.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* G3: plan reviews tie storage to queries, not file count (#1048)

One bullet in each of plan-eng-review and plan-ceo-review: fields queried,
constrained or updated on their own get explicit columns or tables; JSON suits
a document read and written whole. Written fresh; the reporter's patch is not
ported.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* evals: GSTA-228 planning-wave cases with branch, main and no-gstack arms

planted facts (G1, behavior), data model (G3, behavior), task-list rerun (G2,
rule) and closing signals (G4, behavior). GSTACK_WAVE_ARM picks the arm; only
the branch arm asserts the preregistered bar, every arm writes a score row.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: GSTA-228 planning wave fragment

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* G1: the probe takes subcommand words for fixed-subcommand CLIs

The planted-facts eval's one branch miss: the reviewer asked to probe
"git log", the probe refused it, and git's top-level help cannot show a log
flag, so the fabricated --since-commit stayed "unverified".
probe --help <command> [<subcommand> [<subcommand>]] now runs
<cli> <sub> --help for git, gh, docker, cargo, go, npm and uv (never node,
bun, python3, make, yarn, pnpm or a gstack bin, where the word could name a
script), in an empty temporary directory with pagers set to cat, falling back
to -h when --help has no usage line (a minimized system's missing man page).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* G2: task text goes through a rows file, never a command line

The free suite's generated-bash lint (INV-3) refused the add command's
<title>/<finding> placeholders: free text reaches a command only as a file the
agent writes with its file-write tool. start now also creates an empty rows
file in the repository's git-excluded .gstack/tmp and records the run; the
agent writes task and disposition rows there; add validates every row,
stamps phase, run, branch and commit, appends tasks to the list and
dispositions to task-dispositions-<phase>-<branch>.jsonl, then supersedes the
branch's earlier lists. close is gone (dispositions are rows).

The new planning-wave cases get measured periodic walls in
scripts/paid-test-durations.json so the periodic census still packs into 40
slices, the schema case declares the preamble sources its eng-template owners
need, and the paid file count is 114.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* G2: fold start into add so each plan review emits one bash fence

The rows file lives at .gstack/tmp/tasks-<phase>-rows.jsonl; add writes this
run's list (empty when there are no rows), appends dispositions, deletes the
rows file and supersedes the branch's earlier lists. Lowers 27 size pins.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* G2 tests: give gstack-tasks a private HOME and state root

CI shard 13 home containment caught gstack-slug writing ~/.gstack/slug-cache
entries from the tasks-prior fixtures.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: gstack-tasks has no start command

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* G1/G2: tighten the probe and the rows file after ship review

The probe now accepts only each CLI's listed built-in subcommands (no uv run,
docker run, go test, npm exec or gh extensions), refuses gstack bins that do not
handle --help and never retries them with -h, and runs with a throwaway HOME and
an environment without keys. gstack-tasks resolves the slug through
runScriptSync (Windows) and prior deletes a rows file an unfinished run left.
Adds tests for the dropped disposition and the stale rows file.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* G2: add refuses while an earlier task is neither carried nor closed

Ship review (state matrix): a rerun that skipped RECONCILE silently retired
open tasks. add now names each undecided <run_id>/<id> and writes nothing.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* G1/G2: fixes from the ship fix-check and Step 11 voices

Probe: find the repository root without running git, drop relative and
in-repository PATH entries for lookup and for the child, keep HOME and
toolchain locations (rustup, mise, asdf) but no keys, drop cargo fmt/clippy
(external cargo-* programs), restore planted env in the test.
gstack-tasks: resolve the exclude path against the cwd (subdirectories),
resolve --dir, stamp an empty branch on a detached HEAD like the aggregator,
name the file from --branch, and refuse a same-second second list instead of
appending. Receipts: line 0, reversed ranges, dangling links and paths
through files are unresolved instead of crashing.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: probe limits and the reconcile refusal

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: probe subcommand counts per CLI (ship docs audit)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-01 06:11:17 +00:00

128 lines
7.2 KiB
TypeScript

/**
* Fixtures and per-arm prompts for the GSTA-228 planning-wave evals
* (docs/TESTING_INTERNALS.md#no-gstack-arm). GSTACK_WAVE_ARM picks the arm:
* `branch` reads this checkout's skill text, `main` reads the checkout at
* GSTACK_WAVE_MAIN_ROOT (the merge base, prepared by the runner), `none` is
* the same model and input with a plain prompt and no gstack text.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
export type WaveArm = 'branch' | 'main' | 'none';
export const WAVE_ROOT = path.resolve(import.meta.dir, '..', '..');
export function waveArm(env: NodeJS.ProcessEnv = process.env): WaveArm {
const arm = env.GSTACK_WAVE_ARM ?? 'branch';
if (arm !== 'branch' && arm !== 'main' && arm !== 'none') throw new Error(`GSTACK_WAVE_ARM must be branch, main or none, got ${arm}`);
return arm;
}
/** The gstack checkout whose skill text the arm reads; undefined for none. */
export function armRoot(arm: WaveArm, env: NodeJS.ProcessEnv = process.env): string | undefined {
if (arm === 'none') return undefined;
if (arm === 'branch') return WAVE_ROOT;
const root = env.GSTACK_WAVE_MAIN_ROOT;
if (!root || !fs.existsSync(path.join(root, 'office-hours', 'SKILL.md'))) throw new Error('GSTACK_WAVE_ARM=main needs GSTACK_WAVE_MAIN_ROOT, a checkout of the merge base');
return root;
}
/** The text from `start` up to (not including) `end`; throws when either anchor is missing. */
export function excerpt(text: string, start: string, end: string): string {
const a = text.indexOf(start);
const b = a < 0 ? -1 : text.indexOf(end, a + start.length);
if (a < 0 || b < 0) throw new Error(`excerpt anchors not found: ${JSON.stringify(start)} .. ${JSON.stringify(end)}`);
return text.slice(a, b).trimEnd();
}
// G1 planted facts: four errors and one correct claim, each scored separately.
export const BILLING_TS = `// Card charging for the Acme invoicing service.
import { gateway } from './gateway';
export async function chargeCard(customerId: string, cents: number) {
return gateway.charge({ customerId, cents, idempotencyKey: \`\${customerId}-\${cents}\` });
}
export const BILLING_CONFIG = {
currency: 'usd',
timeoutMs: 8000,
// Retries for transient gateway errors.
retries: 3,
};
`;
export const PLANTED_DOC = `# Design: nightly invoice retries
## Problem
Failed card charges are never retried, so roughly 2% of invoices stay unpaid until a person notices.
## Recommended Approach
1. Reuse the existing charge path: \`src/billing.ts:4\` exports \`chargeCard(customerId, cents)\`, which the retry job calls unchanged.
2. Refund reversals stay in \`src/ledger.ts\`, which already handles refunds, so the retry job never touches refunds.
3. The job keeps the gateway retry budget as is: \`src/billing.ts:12\` sets \`retries: 5\`.
4. The job lists the commits that touched billing with \`git log --since-commit <sha> -- src/billing.ts\` and posts them to the run log.
5. Capacity: 3 workers each retry 40 invoices per minute, so the job clears 160 invoices per minute, enough for the nightly backlog of 9,000 in under an hour.
## Success criteria
Unpaid invoices from transient failures drop below 0.2% within a month.
`;
export const PLANTED_ERRORS = {
missing_file: 'src/ledger.ts does not exist in the repository (the doc says it already handles refunds)',
bad_flag: 'git log has no --since-commit option',
bad_math: '3 workers x 40 per minute is 120 per minute, not 160',
wrong_content: 'src/billing.ts:12 sets retries: 3, not 5',
} as const;
export const PLANTED_CONTROL = 'src/billing.ts:4 exports chargeCard(customerId, cents) (this claim is correct)';
export const PLAIN_REVIEW_PROMPT = (doc: string, verdict: string) =>
`Review the design document at ${doc} before it is built. The repository it describes is the current directory. Write your findings to ${verdict} as JSON: {"findings": [{"text": "...", "severity": "blocking|minor"}]}.`;
// G3 schema: one plan, a queried JSON column and a JSON document read and written whole.
export const SCHEMA_PLAN = `# Plan: subscription tiers
We are adding three tiers (free, team, enterprise) to the Acme notes app (Postgres, Rails).
## Data
- Store everything about an account's tier in one new JSONB column, \`accounts.tier_config\`, for example
\`{"tier": "team", "seat_limit": 25, "sso": false, "price_cents": 1200}\`. One column keeps the diff to a single migration.
- Store each user's editor preferences in a new JSONB column, \`users.ui_prefs\`, for example
\`{"kind": "editor", "theme": "dark", "font_size": 14, "keymap": "vim"}\`. The settings page loads the whole object and saves the whole object; nothing queries or filters on its fields.
## Queries the product needs
- Admin dashboard: list every account whose tier includes SSO.
- Seat enforcement: refuse an invite when the account's member count would pass its seat limit.
- Pricing change: update the price of the team tier for every team account at once.
## Out of scope
Billing provider integration.
`;
export const PLAIN_SCHEMA_PROMPT = 'Review the plan in PLAN.md as an engineering reviewer. Give your architecture and data-model findings, each with a concrete recommendation.';
// G2 rerun: the earlier eng-review list holds one open task and one shipped task.
export const PRIOR_RUN = '20261030T100000Z-4242';
export const RERUN_FINDINGS = `Findings from this review:
1. Architecture: the webhook sender still has no retry; a single 5xx drops the event. (Same gap as the last review.)
2. Code quality: the rate limiter reads RATE_LIMIT_PER_MIN without validating it; a typo turns limiting off.
Last review's metrics task shipped in the commit "Add metrics counters for the webhook sender (T2)".`;
export const PLAIN_RERUN_PROMPT = (dir: string) => `You are finishing an engineering review of this repository's plan. ${RERUN_FINDINGS}
An earlier review on this branch left its task list in ${dir}. Write this review's task list as a JSONL file in ${dir}, reconciling the earlier list so only one current list remains there.`;
// G4 closing: a fixed session with three founder signals (a named user, revenue, a reasoned pushback).
export const SESSION_TRANSCRIPT = `[Phase 1] Founder: I'm building intake software for small physical-therapy clinics. Dr. Anita Patel at Riverside PT spends 40 minutes a day retyping intake forms.
[Phase 2] Founder: Riverside is already paying me $400 a month for the pilot, and two more clinics asked for it last week.
[Phase 3] Me: Premise 2 says clinics will switch EHRs for this. I think they won't.
[Phase 3] Founder: I disagree. I don't need them to switch: the intake form exports to their existing EHR through its fax import, which every clinic I've visited still uses.
[Phase 4] Founder: I'll ship the fax export first and keep the form editor rough.
[Phase 4.5] Founder signals counted: 3 (named a specific user, demand evidence with revenue, pushed back with reasoning).`;
export const SESSION_SIGNALS = ['Dr. Anita Patel at Riverside PT (named user)', '$400 a month pilot (revenue)', 'fax import pushback (reasoned disagreement)'] as const;
export const PLAIN_CLOSING_PROMPT = `Here is the transcript of a product office-hours session with a founder:
${SESSION_TRANSCRIPT}
Write the short closing note you would give this founder about whether they should consider applying to Y Combinator. Write the note only; do not ask questions or run tools.`;