mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-06 13:45:35 +02:00
30fe6bb11c
* fix(plan-eng-review): tighten STOP gates with anti-rationalization clause
Five sites in SKILL.md.tmpl uplift to the office-hours b512be71 pattern:
the four review-section gates (Architecture, Code Quality, Test, Performance)
plus the Step 0 complexity-check trigger. Adds tool_use reminder ("call the
tool directly"), names blocked next steps explicitly, anti-rationalization
clause naming the precise failure mode (loading the schema via ToolSearch
and writing the recommendation as chat prose).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(test/helpers): initialPlanContent + wrote_findings_before_asking + shared report-at-bottom assertion
Three additions to claude-pty-runner.ts:
1. runPlanSkillObservation gains initialPlanContent?: string. Pre-pumps a
user message containing the seeded plan before invoking the skill, with
a 3s gap so the message renders before the slash command. claude has no
--plan-file flag (verified via claude --help), so message-pump is the
route. Lets STOP-gate regression tests force complexity findings.
2. ClassifyResult gains wrote_findings_before_asking with companion
strictPlanWrites?: boolean opt on classifyVisible. Fires when a Write/
Edit to .claude/plans/* precedes any AskUserQuestion render in the
session window. Default off — preserves zero-findings → write plan →
plan_ready as legitimate for unseeded smokes. Six new unit tests cover
before/after-AUQ ordering, permission-dialog edge case, strict-off path.
3. assertReportAtBottomIfPlanWritten(obs) shared helper. Wraps the existing
assertReviewReportAtBottom(content) and gates on obs.planFile (artifact
existing), so the assertion fires under both 'asked' and 'plan_ready'
when a plan was actually written.
Also: runPlanSkillObservation now captures obs.planFile on every classifier
outcome, not just 'plan_ready'. Catches the case where the skill wrote a
plan partway through then paused on a question.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: wire assertReportAtBottomIfPlanWritten into 4 plan-mode E2E tests + add seeded-plan STOP-gate case
Every test case in skill-e2e-plan-{eng,ceo,design,devex}-plan-mode.test.ts
that produces a plan file now asserts ## GSTACK REVIEW REPORT is the last
## section. The {{PLAN_FILE_REVIEW_REPORT}} resolver mandated this contract;
nothing tested it until now.
Plan-eng additionally gains a third test case: STOP gate fires when seeded
plan forces Step 0 findings. Combines the new initialPlanContent runner
option with --disallowedTools AskUserQuestion to force the Conductor
MCP-variant path through mcp__*__AskUserQuestion. Asserts outcome NOT in
{wrote_findings_before_asking, auto_decided, silent_write, exited, timeout}
and that plan_ready outcomes carry a ## Decisions section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(touchfiles): delete duplicate plan-design-review-plan-mode keys
Verified duplicates in test/helpers/touchfiles.ts:
- E2E_TOUCHFILES had plan-design-review-plan-mode at line 94 (full deps)
AND line 243 (smaller deps); JS object literals: later wins.
- E2E_TIERS had it at line 399 ('gate') AND line 524 ('periodic'); same
later-wins rule.
Effective tier was 'periodic', not 'gate'. Three of four plan-mode siblings
ran on every PR; design ran weekly only.
Delete the line-243 and line-524 duplicates. Keep line 94 (full deps) and
line 399 ('gate'). Also extend the four plan-mode-test entries to include
scripts/resolvers/review.ts so changes to {{PLAN_FILE_REVIEW_REPORT}}
trigger all four siblings in bun run eval:select.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version and changelog (v1.26.2.0)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: tighten CHANGELOG voice for v1.26.2.0
Move contributor-flavored bullet (runPlanSkillObservation seeding) into
For contributors. Drop branch-internal narrative (Codex review pass,
plan iteration tracking) per CHANGELOG-for-users style.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
6.6 KiB
TypeScript
140 lines
6.6 KiB
TypeScript
/**
|
|
* plan-ceo-review plan-mode smoke (gate, paid, real-PTY).
|
|
*
|
|
* Asserts: when /plan-ceo-review is invoked in plan mode, the FIRST terminal
|
|
* outcome is 'asked' — a skill-question numbered list. Permission dialogs
|
|
* (which also render numbered lists) are filtered out by `runPlanSkillObservation`
|
|
* via its `isPermissionDialogVisible(visible.slice(-1500))` short-circuit.
|
|
*
|
|
* Reaching 'plan_ready' first IS the regression we want to catch: the agent
|
|
* skipped Step 0 entirely and went straight to ExitPlanMode. The original
|
|
* failure had the assistant read a diff, write a plan with two issues, and
|
|
* call ExitPlanMode without ever firing AskUserQuestion — the user had to
|
|
* manually call out the missing per-issue questions.
|
|
*
|
|
* Why this skill is special: unlike plan-eng-review / plan-design-review /
|
|
* plan-devex-review (whose smokes accept either 'asked' or 'plan_ready'),
|
|
* plan-ceo-review's template mandates Step 0A premise challenge (3 baked-in
|
|
* questions) AND Step 0F mode selection BEFORE any plan write. There is no
|
|
* legitimate path to plan_ready that does not first emit a skill-question
|
|
* numbered prompt.
|
|
*
|
|
* Env passthrough: passes `QUESTION_TUNING=false` and `EXPLAIN_LEVEL=default`
|
|
* via the runner's env option. Today these are advisory — `gstack-config`
|
|
* reads `~/.gstack/config.yaml`, not env vars, so a contributor with
|
|
* `question_tuning: true` set in their YAML config can still see AUTO_DECIDE
|
|
* masking. The env passthrough is wired so a future gstack-config change to
|
|
* honor env overrides will make this test hermetic without further edits.
|
|
* Tracked as a post-merge follow-up.
|
|
*
|
|
* FAIL conditions: 'plan_ready' first, silent Write/Edit before any prompt,
|
|
* claude crash, timeout.
|
|
*
|
|
* See test/helpers/claude-pty-runner.ts for runner internals.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import {
|
|
runPlanSkillObservation,
|
|
planFileHasDecisionsSection,
|
|
assertReportAtBottomIfPlanWritten,
|
|
} from './helpers/claude-pty-runner';
|
|
|
|
const shouldRun = !!process.env.EVALS && process.env.EVALS_TIER === 'gate';
|
|
const describeE2E = shouldRun ? describe : describe.skip;
|
|
|
|
describeE2E('plan-ceo-review plan-mode smoke (gate)', () => {
|
|
test('first terminal outcome is asked (Step 0 fires before any plan write)', async () => {
|
|
const obs = await runPlanSkillObservation({
|
|
skillName: 'plan-ceo-review',
|
|
inPlanMode: true,
|
|
timeoutMs: 300_000,
|
|
env: { QUESTION_TUNING: 'false', EXPLAIN_LEVEL: 'default' },
|
|
});
|
|
|
|
if (obs.outcome !== 'asked') {
|
|
const diagnosis =
|
|
obs.outcome === 'plan_ready'
|
|
? `'plan_ready' first means the agent skipped Step 0 entirely and went straight to ExitPlanMode without asking.`
|
|
: obs.outcome === 'timeout'
|
|
? `Timeout means the agent neither asked nor completed within the budget — likely hung mid-question or stuck on a permission dialog.`
|
|
: obs.outcome === 'silent_write'
|
|
? `Silent Write/Edit fired to an unsanctioned path before any AskUserQuestion — also a Step 0 skip.`
|
|
: `Outcome '${obs.outcome}' is unexpected; investigate the evidence below.`;
|
|
throw new Error(
|
|
`plan-ceo-review smoke FAILED: outcome=${obs.outcome}\n` +
|
|
`${diagnosis}\n` +
|
|
`Expected 'asked'. See plan-ceo-review/SKILL.md.tmpl: the Step 0 STOP rules ` +
|
|
`and the "One issue = one AskUserQuestion call" rule under "CRITICAL RULE — ` +
|
|
`How to ask questions".\n` +
|
|
`summary: ${obs.summary}\n` +
|
|
`elapsed: ${obs.elapsedMs}ms\n` +
|
|
`--- evidence (last 2KB visible) ---\n${obs.evidence}`,
|
|
);
|
|
}
|
|
assertReportAtBottomIfPlanWritten(obs);
|
|
}, 360_000);
|
|
|
|
// v1.21+ regression: Conductor launches Claude Code with
|
|
// `--disallowedTools AskUserQuestion --permission-mode default` (verified
|
|
// via `ps` on the live Conductor claude process). Native AskUserQuestion
|
|
// is removed from the model's tool registry; without fallback guidance
|
|
// the model can't ask and silently proceeds.
|
|
//
|
|
// The fix (Tool resolution preamble) accepts two surface paths under
|
|
// --disallowedTools:
|
|
// - 'asked' — model emits a numbered-option prompt as prose (with
|
|
// the same D<N> + Pros/cons format as a real AUQ)
|
|
// - 'plan_ready' — model writes the question into the plan file as a
|
|
// "## Decisions to confirm" section + ExitPlanMode;
|
|
// the native plan-mode "Ready to execute?" surfaces
|
|
// it through the TTY confirmation
|
|
//
|
|
// Both let the user see the decision. Failure signals are
|
|
// silent_write/exited/timeout (model never surfaced the question) and
|
|
// 'auto_decided' (the AUTO_DECIDE preamble fired without a /plan-tune
|
|
// opt-in — caught explicitly).
|
|
test('AskUserQuestion surfaces when --disallowedTools AskUserQuestion is set', async () => {
|
|
const obs = await runPlanSkillObservation({
|
|
skillName: 'plan-ceo-review',
|
|
inPlanMode: true,
|
|
extraArgs: ['--disallowedTools', 'AskUserQuestion'],
|
|
timeoutMs: 300_000,
|
|
});
|
|
|
|
if (
|
|
obs.outcome === 'auto_decided' ||
|
|
obs.outcome === 'silent_write' ||
|
|
obs.outcome === 'exited' ||
|
|
obs.outcome === 'timeout'
|
|
) {
|
|
throw new Error(
|
|
`plan-ceo-review AskUserQuestion-blocked regression: outcome=${obs.outcome}\n` +
|
|
`summary: ${obs.summary}\n` +
|
|
`elapsed: ${obs.elapsedMs}ms\n` +
|
|
`--- evidence (last 2KB visible) ---\n${obs.evidence}`,
|
|
);
|
|
}
|
|
// plan_ready under --disallowedTools is only a pass when the model used
|
|
// the plan-file fallback (wrote a `## Decisions to confirm` section).
|
|
// Without that section, plan_ready means the model silently skipped Step 0
|
|
// and went straight to ExitPlanMode — the regression we're catching.
|
|
if (obs.outcome === 'plan_ready') {
|
|
if (!obs.planFile) {
|
|
throw new Error(
|
|
`plan-ceo-review AskUserQuestion-blocked regression: outcome=plan_ready but no plan file path detected in TTY output. Cannot verify the model used the fallback flow.\n` +
|
|
`--- evidence (last 2KB visible) ---\n${obs.evidence}`,
|
|
);
|
|
}
|
|
if (!planFileHasDecisionsSection(obs.planFile)) {
|
|
throw new Error(
|
|
`plan-ceo-review AskUserQuestion-blocked regression: model wrote ${obs.planFile} without a "## Decisions" section. Step 0 was silently skipped.\n` +
|
|
`--- evidence (last 2KB visible) ---\n${obs.evidence}`,
|
|
);
|
|
}
|
|
}
|
|
expect(['asked', 'plan_ready']).toContain(obs.outcome);
|
|
assertReportAtBottomIfPlanWritten(obs);
|
|
}, 360_000);
|
|
});
|