Merge remote-tracking branch 'origin/garrytan/opus55fu-wip-H2' into garrytan/opus55-followups

This commit is contained in:
garrytan committed 2026-10-03 21:36:47 +00:00
commit 86c07461a3
25 files changed
+1262 -35

No files matched your search

+76
View File
@@ -101,3 +101,79 @@ export function validateModel(input: string): string | null {
if ((ALL_MODEL_NAMES as readonly string[]).includes(input)) return null;
return `'${input}' is not a known model. Use ${ALL_MODEL_NAMES.join(', ')}.`;
}
/**
* Overlays `./setup --claude-model <id>` may select for the Claude host. The
* list is the Claude subset of ALL_MODEL_NAMES; a new Claude overlay joins
* both lists.
*/
export const CLAUDE_OVERLAY_MODELS = ['claude', 'opus-4-7', 'opus-4-8', 'sonnet-5', 'fable-5'] as const;
export type ClaudeOverlay = (typeof CLAUDE_OVERLAY_MODELS)[number];
export interface ClaudeOverlayError {
kind: 'alias' | 'unknown' | 'non-claude';
problem: string;
cause: string;
fix: string;
}
export type ClaudeOverlayResult =
| { overlay: ClaudeOverlay; generic: boolean }
| { error: ClaudeOverlayError };
const CLAUDE_CODE_ALIASES = ['opus', 'sonnet', 'haiku', 'default'];
/**
* Map a `--claude-model` value to the overlay setup renders. `resolveModel`
* does the family matching (bracketed context suffixes, dated and `-latest`
* snapshots); this adds the Claude-only allowlist and the errors setup prints.
* `generic` is true when a specific model ID has no overlay of its own and
* uses the generic `claude` overlay (for example `claude-opus-5-5`).
*/
export function resolveClaudeOverlay(id: string): ClaudeOverlayResult {
const s = id.trim().toLowerCase().replace(/\[[^\]]*\]$/, '').trim();
const problem = `${JSON.stringify(id)} is not a Claude model ID that gstack can map to a skill overlay.`;
if (CLAUDE_CODE_ALIASES.includes(s)) {
return { error: {
kind: 'alias',
problem,
cause: `"${s}" is a Claude Code alias whose model changes over time, so it names no fixed overlay.`,
fix: 'Pass a full model ID, for example ./setup --claude-model claude-opus-4-8, or ./setup --claude-model claude for the generic overlay.',
} };
}
const family = resolveModel(s);
if (!family) {
return { error: {
kind: 'unknown',
problem,
cause: 'It does not match any model family gstack knows.',
fix: 'Pass a Claude model ID such as claude-opus-4-8 or claude-sonnet-5, or claude for the generic overlay.',
} };
}
if (!(CLAUDE_OVERLAY_MODELS as readonly string[]).includes(family)) {
return { error: {
kind: 'non-claude',
problem,
cause: `It belongs to the ${family} family, and --claude-model selects Claude overlays only.`,
fix: `For Codex skills use ./setup --host codex --model ${id}. For Claude, pass a Claude model ID such as claude-opus-4-8.`,
} };
}
return { overlay: family as ClaudeOverlay, generic: family === 'claude' && s !== 'claude' };
}
// `bun scripts/models.ts claude-overlay <id>` — setup's bridge to the table
// above. Prints "<overlay>\t<generic 0|1>" or the error lines on stderr (exit 1).
if (import.meta.main) {
const [command, id] = process.argv.slice(2);
if (command !== 'claude-overlay' || id === undefined) {
console.error('Usage: bun scripts/models.ts claude-overlay <claude-model-id>');
process.exit(2);
}
const result = resolveClaudeOverlay(id);
if ('error' in result) {
console.error(`${result.error.problem}\n${result.error.cause}\n${result.error.fix}`);
process.exit(1);
}
console.log(`${result.overlay}\t${result.generic ? 1 : 0}`);
}
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bun
/**
* Prompt-audit manifest: the files a contributor feeds to Anthropic's
* `/claude-api prompt-audit` (a Claude Code skill, run in your own Claude Code
* session) at each frontier-model release, cut into slices small enough for
* one audit pass each. Procedure: CONTRIBUTING.md "Prompt audit at each
* frontier-model release".
*
* Slices, in order: shared instructions every skill reads (CLAUDE.md, model
* overlays, preamble resolvers), the other resolvers, the skill templates
* (a skill's SKILL.md.tmpl and sections stay in one slice), the code that
* builds model requests, and the tests that pin skill wording (they read a
* template, skill or resolver and assert a literal of 40+ characters).
*
* Usage: bun run audit:manifest [--json] [--budget-kb <n>]
*/
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
const ROOT = resolve(import.meta.dir, '..');
const DEFAULT_BUDGET_KB = 200;
export interface AuditSlice { id: string; group: string; files: string[]; bytes: number }
function walk(dir: string, keep: (rel: string) => boolean): string[] {
const out: string[] = [];
const visit = (d: string) => {
for (const entry of readdirSync(d, { withFileTypes: true })) {
const p = join(d, entry.name);
const rel = relative(ROOT, p);
if (entry.isDirectory()) {
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') visit(p);
} else if (keep(rel)) {
out.push(rel);
}
}
};
visit(dir);
return out.sort();
}
const size = (rel: string) => statSync(join(ROOT, rel)).size;
const read = (rel: string) => readFileSync(join(ROOT, rel), 'utf8');
/** Pack units (each a list of files that must stay together) into slices under the budget. */
function pack(group: string, units: string[][], budget: number): Omit<AuditSlice, 'id'>[] {
const slices: Omit<AuditSlice, 'id'>[] = [];
let current: Omit<AuditSlice, 'id'> | null = null;
for (const unit of units) {
const bytes = unit.reduce((sum, f) => sum + size(f), 0);
if (!current || (current.bytes > 0 && current.bytes + bytes > budget)) {
current = { group, files: [], bytes: 0 };
slices.push(current);
}
current.files.push(...unit);
current.bytes += bytes;
}
return slices;
}
export function buildManifest(budgetKb = DEFAULT_BUDGET_KB): AuditSlice[] {
const budget = budgetKb * 1024;
const preamble = walk(join(ROOT, 'scripts/resolvers/preamble'), rel => rel.endsWith('.ts'));
const shared = ['CLAUDE.md', ...walk(join(ROOT, 'model-overlays'), rel => rel.endsWith('.md')), ...preamble];
const resolvers = walk(join(ROOT, 'scripts/resolvers'), rel => rel.endsWith('.ts') && !preamble.includes(rel));
const templates = walk(ROOT, rel => rel.endsWith('.tmpl') && !rel.startsWith('test/'));
const bySkill = new Map<string, string[]>();
for (const rel of templates) {
const skill = rel.includes('/') ? rel.split('/')[0] : '(root)';
bySkill.set(skill, [...(bySkill.get(skill) ?? []), rel]);
}
const requestCode = [...walk(join(ROOT, 'lib'), rel => rel.endsWith('.ts')),
...walk(join(ROOT, 'design/src'), rel => rel.endsWith('.ts')),
...walk(join(ROOT, 'test/helpers'), rel => rel.endsWith('.ts'))]
.filter(rel => /api\.anthropic\.com|api\.openai\.com|\/v1\/messages|messages\.create|responses\.create/.test(read(rel)));
const pinningTests = walk(join(ROOT, 'test'), rel => /^test\/[^/]+\.test\.ts$/.test(rel))
.filter(rel => {
const src = read(rel);
return /SKILL\.md|\.md\.tmpl|scripts\/resolvers|model-overlays/.test(src) && /\.(toContain|toMatch)\((['"`])[^'"`]{40,}/.test(src);
});
const slices = [
...pack('shared instructions', [shared], Infinity),
...pack('resolvers', resolvers.map(f => [f]), budget),
...pack('skill templates', [...bySkill.values()], budget),
...pack('model-request code', requestCode.map(f => [f]), budget),
...pack('tests that pin skill wording', pinningTests.map(f => [f]), budget),
];
return slices.map((slice, i) => ({ id: `s${String(i + 1).padStart(2, '0')}`, ...slice }));
}
if (import.meta.main) {
const args = process.argv.slice(2);
const budgetArg = args.indexOf('--budget-kb');
const budgetKb = budgetArg >= 0 ? Number(args[budgetArg + 1]) : DEFAULT_BUDGET_KB;
if (!Number.isFinite(budgetKb) || budgetKb <= 0) {
console.error('--budget-kb needs a positive number of kilobytes');
process.exit(2);
}
const manifest = buildManifest(budgetKb);
if (args.includes('--json')) {
console.log(JSON.stringify(manifest, null, 2));
} else {
const files = manifest.reduce((n, s) => n + s.files.length, 0);
console.log(`Prompt-audit manifest: ${manifest.length} slices, ${files} files. Run /claude-api prompt-audit in Claude Code on one slice at a time (CONTRIBUTING.md).`);
for (const slice of manifest) {
console.log(`\n## ${slice.id} ${slice.group} (${slice.files.length} files, ${Math.round(slice.bytes / 1024)} KB)`);
for (const file of slice.files) console.log(file);
}
}
}
+1 -1
View File
@@ -165,7 +165,7 @@ Branch on the echoed \`CODEX_MODE\`:
- **\`under_codex\`** — stale artifact selected its own harness. Print: "Codex outside review unavailable: harness mismatch; no outside process started. Missing coverage. Repair: setup --host codex." Skip the outside invocation and follow the workflow's native-review instructions below. Conflicting inherited harness markers are not grounds to guess another provider.
- **\`not_authed\`** — installed but no credentials. Print: "Codex not authenticated; outside coverage unavailable. Run \`codex login\` or set \`$CODEX_API_KEY\`." ${nativeRoute}
- **\`broken_install\`** — the CLI is on PATH but cannot execute (spawn ENOENT, non-executable binary, missing vendor payload). Print: "Codex is installed but its binary cannot run — Codex passes skipped. Reinstall: \`npm install -g @openai/codex\`." Relay the probe's HINT lines. ${nativeRoute}
- **\`model_unusable\`** — the selected model (see \`CODEX_MODEL:\`) is invalid or unavailable to the account (#2477: HTTP 400 on every call). Relay the probe's HINT lines and the fix (\`GSTACK_CODEX_MODEL=<supported-model>\` or config.toml \`model\`); never substitute a model. ${nativeRoute} The ~10s round trip is cached for 1h; timeouts fail open to \`ready\`.
- **\`model_unusable\`** — the selected model (see \`CODEX_MODEL:\`) is invalid or unavailable to the account (HTTP 400 on every call). Relay the probe's HINT lines and the fix (\`GSTACK_CODEX_MODEL=<supported-model>\` or config.toml \`model\`); never substitute a model. ${nativeRoute} The ~10s round trip is cached for 1h; timeouts fail open to \`ready\`.
- **\`ready\`** — run the Codex pass below.`;
}
+1 -1
View File
@@ -133,7 +133,7 @@ if command -v jq >/dev/null 2>&1; then
# .commit must be bound BEFORE piping to the split commit array: a
# pipe rebinds jq's context, so a bare .commit after it indexes the
# ARRAY with a string, every line errors into 2>/dev/null, and the
# aggregate is empty forever — the #2018 zero-tasks bug.
# aggregate is empty forever.
jq -c --arg branch "$BRANCH" --arg commits "$COMMITS_RECENT" \\
'.commit as $c | select(.branch == $branch and ($commits | split("|") | index($c) != null))' \\
"$f" 2>/dev/null >> "$ALL_JSONL" || true