Files
gstack/test/helpers/e2e-hosts.ts
T
Garry Tanandcapy-ai[bot] ef99eb5a55 feat: retire /investigate after a no-gain eval (GST-1717) (#4110)
* feat: retire /investigate after a no-gain eval (GST-1717)

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

* fix(skill-start): core routing offer drops every non-core rule line (GST-1717)

The retired /investigate rule became 'Bugs/errors → fix them directly', which no
longer matches '→ invoke /', so proactive=core leaked it into the offered section.
Match any '- … → ' rule line.

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

* revert(review): keep review/checklist.md at its no-loss-pinned bytes (GST-1717)

The pin names replay GSTA-224 H1; a reviewer checklist edit merges only after a
passing no-loss replay (crew rule 15). The /investigate flake line stays until a
replayed repin.

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

* test: repair free-suite fallout from retiring /investigate (GST-1717)

CI run 42340956269 at 83a8a9a: drop fixtures, ledger rows, allowlist entries and
counts for the retired skill and its deleted paid files; record the harness
change as non-behavioral; the test-of-test detector accepts path.join(ROOT, 'bin');
uncovered-prompt fixtures use a tracked prompt, since an untracked path now
reads as a deleted one.

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

* fix(relink): back up a retired skill's real SKILL.md before cleaning it (GST-1717)

With the skill's source gone, the customized-file check never ran, so a
banner-carrying local copy of investigate/SKILL.md was deleted instead of moved
to the backup root (red-team review, pass 1).

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

* fix(qm): restore the review checklist's flake-line rewrite (GST-1717)

review/checklist.md keeps its pinned /investigate line, so the QM edition needs
its replaceText rule back; qm/ stays fresh against the generator.

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

* docs(changelog): name the setup flag break, the relink backup and the pinned checklist line (GST-1717)

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

* fix(setup,relink): back up a retired skill's real SKILL.md instead of deleting it (GST-1717)

Scoped fix round under grant a-360dd62bd46a (outside-voice blocking finding and
adversarial #1, #4). setup's prune compares a copied host entry with the render
before removing that render, and backs up a copy that differs or whose render is
gone; relink no longer takes the marker's rm -rf for a skill whose source is
gone, so a real SKILL.md is backed up and the user's files stay. setup reports a
failed remove-source of the retired gate hooks on stderr.

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

* fix(pr-profile): a changed prompt counts as deleted only when it is gone from disk (GST-1717)

An untracked prompt file on disk still needs prompt validation (targeted review
finding); harness change recorded as non-behavioral.

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

* docs(changelog): exact backup wording, the hook-removal warning and GST-1744 (GST-1717)

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

* fix(setup): one backup folder per host for a retired skill's copies (GST-1717)

Under grant a-c95576b8b1f9 (fix-check#1): every copy host moved its
gstack-investigate/SKILL.md to the same backup path, so a later host's copy
overwrote an earlier host's edits. A sandboxed ./setup --host factory run showed
gen-skill-docs removes the retired render before the prune, so every real copy
is backed up; each host now gets its own folder.

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

* docs(changelog): say exactly what setup backs up for a retired skill (GST-1717)

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

* docs(changelog): end the two colon sentences prose-lint flagged (GST-1717)

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

* test(retired-skills): name plan-eng-review once behind a pregate-literal waiver (GST-1717)

pregate's literals check read the GSTACK_RETIRED_SKILLS change as an old
value still in the tests. plan-eng-review is still retired; investigate was
appended. The fixture names it once, with the waiver.

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-07 06:52:04 +00:00

128 lines
6.0 KiB
TypeScript

/**
* Agent host selection for paid E2E cases (GSTA-49 TM2). EVALS_HOST picks the
* agent every runSkillTest session runs on: `claude` (default) or `codex`
* (codex exec, test/helpers/codex-host-runner.ts). E2E_HOSTS is the per-case
* host metadata over every gate case (classification in the GSTA-49 TM2
* scoping): `both` runs on either host with the shared adapter; every other
* value is the reason the case is Claude-only. On the Codex host a Claude-only
* case is SKIPPED with its reason, planned as excluded and deselected by the
* report with zero credit, never counted as failed. A case without metadata
* (every non-gate case) is Claude-only.
*/
export type EvalHost = 'claude' | 'codex';
export const EVAL_HOSTS: readonly EvalHost[] = ['claude', 'codex'];
export function resolveEvalHost(env: NodeJS.ProcessEnv = process.env): EvalHost {
const raw = (env.EVALS_HOST ?? '').trim();
if (!raw) return 'claude';
if ((EVAL_HOSTS as readonly string[]).includes(raw)) return raw as EvalHost;
throw new Error(`EVALS_HOST must be one of ${EVAL_HOSTS.join(', ')}; got "${raw}"`);
}
export const CLAUDE_ONLY_REASONS = {
pty: 'drives the interactive Claude Code TUI (plan mode, native AskUserQuestion) through the PTY harness',
sdk: 'uses the Claude Agent SDK (canUseTool answers and PreToolUse hooks)',
subagents: 'asserts Claude Agent/Task subagent events',
'skill-tool': 'depends on the Claude Skill tool load and registration',
hermetic: 'a claude -p hermetic canary; the Codex host needs its own canary twin',
'self-host': 'the Claude host calling /codex; on the Codex host /codex is the agent itself',
rework: 'parses Claude stream-json tool events or prescribes native Claude tools; not ported to the Codex host',
'host-neutral': 'runs no model session, so a second host adds nothing',
} as const;
export type ClaudeOnlyReason = keyof typeof CLAUDE_ONLY_REASONS;
export type HostSupport = 'both' | ClaudeOnlyReason;
/** Adapter-only cases: final output, files, git state, Bash commands, server logs or browseErrors. */
const ADAPTER_ONLY: readonly string[] = [
'benchmark-workflow',
'browse-basic',
'browse-snapshot',
'canary-workflow',
'context-recovery-artifacts',
'context-restore-loads-latest',
'context-save-writes-file',
'cso-diff-mode',
'design-review-detector-shim',
'design-review-detector-shim-dom',
'design-review-plugin-handoff',
'diagram-triplet',
'document-release',
'gstack-upgrade-happy-path',
'land-and-deploy-first-run',
'land-and-deploy-review-gate',
'land-and-deploy-workflow',
'learnings-show',
'operational-learning',
'plan-design-review-no-ui-scope',
'plan-eng-coverage-audit',
'plan-review-report',
'plan-tune-inspect',
'qa-bootstrap',
'qa-quick',
'retro-base-branch',
'review-army-delivery-audit',
'review-army-json-findings',
'review-army-migration-safety',
'review-army-quality-score',
'review-base-branch',
'review-coverage-audit',
'review-enum-completeness',
'review-sql-injection',
'review-test-value',
'safety-pair-agent-block',
'safety-ship-stale-evidence',
'setup-deploy-workflow',
'ship-base-branch',
'ship-coverage-audit',
'ship-coverage-value',
'ship-local-workflow',
'ship-triage',
'skillmd-no-local-binary',
'skillmd-outside-git',
'skillmd-setup-discovery',
'test-audit-report-only',
'tpa-absent-darwin',
'tpa-absent-linux',
'tpa-apple-ban',
'tpa-broken',
'tpa-present'
];
const CLAUDE_ONLY: Readonly<Record<ClaudeOnlyReason, readonly string[]>> = {
pty: ['autoplan-guard-pty', 'office-hours-auto-mode', 'plan-ceo-finding-floor', 'plan-ceo-review-plan-mode',
'plan-design-with-ui-scope', 'plan-devex-finding-floor', 'plan-devex-review-plan-mode', 'plan-mode-no-op'],
sdk: ['auq-format-gate',
'shared-libs-review-index-flags', 'shared-libs-review-lifecycle', 'shared-libs-review-path-eligibility',
'shared-libs-review-prior-coverage', 'shared-libs-review-revalidation', 'ship-frozen-scope-convergence', 'ship-local-hook-preservation',
'ship-managed-hook-refresh', 'ship-skipped-queued-finding', 'ship-unmanaged-hook-consent'],
subagents: ['docsync-spawned', 'review-army-perf-n-plus-one'],
'skill-tool': ['router-failure-triage-resolves',
'router-failure-verified-cause', 'skillify-approval-reject', 'skillify-happy-path', 'skillify-provenance-refusal'],
hermetic: ['hermetic-canary', 'hermetic-sentinel'],
'self-host': ['safety-codex-consult-embed'],
rework: ['qa-functional-cli-fix', 'qa-functional-cli-report', 'qa-functional-webhook-fix', 'qa-functional-webhook-report',
'qa-only-no-fix', 'review-dashboard-via', 'review-exploratory-small-cli', 'review-gate-integrity',
'shared-libs-read-only', 'shared-libs-unsupported-git', 'ship-docsync-completion', 'ship-docsync-current',
'ship-docsync-failure', 'ship-docsync-late-result', 'ship-docsync-launch-failure', 'ship-docsync-missing-asset',
'ship-docsync-missing-marker', 'ship-docsync-recovery', 'ship-docsync-stale-after', 'ship-docsync-stale-before',
'ship-docsync-store', 'ship-docsync-timeout-unsettled', 'ship-exploratory-late-input', 'ship-exploratory-plan-checks',
'ship-exploratory-small-cli', 'ship-exploratory-unavailable'],
'host-neutral': ['timeline-event-flow'],
};
export const E2E_HOSTS: Readonly<Record<string, HostSupport>> = Object.freeze(Object.fromEntries([
...ADAPTER_ONLY.map(id => [id, 'both'] as const),
...(Object.entries(CLAUDE_ONLY) as Array<[ClaudeOnlyReason, readonly string[]]>).flatMap(([reason, ids]) => ids.map(id => [id, reason] as const)),
]));
/** Why a case does not run on `host`, or null when it does. */
export function hostSkipReason(id: string, host: EvalHost, hosts: Readonly<Record<string, HostSupport>> = E2E_HOSTS): string | null {
if (host === 'claude') return null;
const support = hosts[id];
if (support === 'both') return null;
if (support === undefined) return `Claude-only on the ${host} host: no Codex host metadata (E2E_HOSTS covers the gate cases)`;
return `Claude-only on the ${host} host: ${CLAUDE_ONLY_REASONS[support]}`;
}