Merge remote-tracking branch 'origin/garrytan/opus55-wip-A' into garrytan/opus-5-5-prompt-audit

This commit is contained in:
garrytan committed 2026-10-03 00:46:21 +00:00
commit 52064e6883
13 files changed
+126 -74

No files matched your search

+27 -22
View File
@@ -92,9 +92,10 @@ shard processes, serial within each, packed by recorded per-file durations
when `scripts/free-test-durations.json` exists — refresh occasionally with
`bun run test:free --record-durations`; strict-output classification per
shard: a shard without bun's terminal summary line FAILS — silent truncation
cannot report green). The former trailing serial tree-mutating shard is
gone: `TREE_MUTATING` is empty (gen-skill-docs has a main() guard and
`--out-dir` renders every host, so tests render into mkdtemps — see
cannot report green). `TREE_MUTATING` lists the files that still run in their
own trailing serial shard (today only `test/bootstrap-retention.test.ts`, for
host-wide procfs visibility); gen-skill-docs tests no longer need it (main()
guard, and `--out-dir` renders every host into mkdtemps — see
docs/TESTING_INTERNALS.md). Never type bare `bun test` for the suite: it
walks the whole repo, loading paid eval files and missing the strict
classifier.
@@ -121,7 +122,18 @@ engine's own tests run everywhere.
New or changed tests follow the [test value bar](docs/test-value-bar.md): each one
protects behavior a real regression would break, and contract tests (SKILL.md
goldens, prompt bytes) stay. The bar's source is `scripts/resolvers/test-value.ts`.
goldens, and prompt bytes that are a contract as defined below) stay. The bar's
source is `scripts/resolvers/test-value.ts`.
In this repo, prompt bytes are a contract only when software reads them (a
parser, hook, grader or another skill consumes the exact text) or a recorded
eval shows the wording matters. Tests on skill templates and generated SKILL.md
pin structure, step order, routing tables, machine-read markers and
safety-critical lines; check safety lines case-insensitively, on meaning rather
than capitals. Leave behavior to E2E cases and judges. Don't pin emphasis,
capitalization, issue numbers, or a sentence a behavioral check already covers,
and when a rewrite changes a pinned sentence, replace the pin with a structural
or meaning-level check instead of pinning the new sentence.
Projects tune `/ship`'s coverage gate with optional CLAUDE.md `## Test Coverage`
keys, all absent by default: `Minimum:`, `Target:`, `Generation cap:` (default 5),
`Base control:` (`auto` or `off`), `Base control budget:` (seconds, default 90) and
@@ -188,7 +200,8 @@ or as a reference doc, (3) only compress carefully-tuned prose as a last resort
cuts to the coverage audit, review army, or voice directive have real quality cost.
A second, harder ceiling guards the DISCOVERY surface: `test/catalog-budget.test.ts`
caps the aggregate frontmatter `name` + `description` across all skills at 1,171
caps the aggregate frontmatter `name` + `description` across all skills at
`CATALOG_BUDGET_TOKEN_EQUIVALENTS` (1,194 today; each new skill ratchets it)
token-equivalents (260-byte per-skill sub-cap), counted through the shared census
in `test/helpers/skill-census.ts`. This one is enforced, not a warning — every
host loads the full catalog every session, so growth here taxes every
@@ -246,8 +259,9 @@ Rules:
## Writing style (V1)
Default output from every tier-≥2 skill follows the Writing Style section in
`scripts/resolvers/preamble.ts`: jargon glossed on first use (curated list in
`scripts/jargon-list.json`, baked at gen-skill-docs time), questions framed in
`scripts/resolvers/preamble/generate-writing-style.ts`: jargon glossed on first
use (curated list in `scripts/jargon-list.json`, which the skill Reads at runtime
on the first jargon term), questions framed in
outcome terms ("what breaks for your users if...") not implementation terms,
short sentences, decisions close with user impact. Power users who want the
tighter V0 prose set `gstack-config set explain_level terse` (binary switch,
@@ -702,19 +716,6 @@ regenerated SKILL.md shifts prompt context.
"Pre-existing" without receipts is a lazy claim. Prove it or don't say it.
## Long-running tasks: don't give up
When running evals, E2E tests, or any long-running background task, **poll until
completion**. Use `sleep 180 && echo "ready"` + `TaskOutput` in a loop every 3
minutes. Never switch to blocking mode and give up when the poll times out. Never
say "I'll be notified when it completes" and stop checking — keep the loop going
until the task finishes or the user tells you to stop.
The full E2E suite can take 30-45 minutes. That's 10-15 polling cycles. Do all of
them. Report progress at each check (which tests passed, which are running, any
failures so far). The user wants to see the run complete, not a promise that
you'll check later.
## Running evals as an agent: always detach (SIGTERM-proof)
When **you (an agent/harness)** launch a long eval/benchmark run, run it through
@@ -737,7 +738,8 @@ the run can also die to idle-sleep. `gstack-detach` fixes both: a fresh session
(stray `claude`/`codex` grandchildren included), a per-shard
`GSTACK_EVAL_DIR=<evalDir>/shards/<slug>/` honored by the `EvalCollector`
constructor, and an aggregate that separates failed vs timed-out vs
never-started shards — the detach timeouts (47340s gate / 67380s periodic;
never-started shards — the detach timeouts (the `--timeout` values on
package.json's `eval:bg:gate` / `eval:bg:periodic`;
floor enforced against the live shard census by
test/eval-detach-timeout-floor.test.ts)
are sized against worst-case shard wall clock. `EVALS_JOBS` sets the shard
@@ -750,7 +752,10 @@ the run can also die to idle-sleep. `gstack-detach` fixes both: a fresh session
- Then **poll the printed logfile** with a death-aware watcher: break on the
guaranteed `### gstack-detach EXIT=<code> ###` sentinel (success AND failure are
both marked, so silence is never mistaken for success). The detached run survives
even if your watcher gets reaped, so re-checking the log always works.
even if your watcher gets reaped, so re-checking the log always works. Keep
checking until the sentinel appears or the user tells you to stop; a long run is
expected, and a promise to check later is not a result. At each check, report
which tests passed, which are still running, and any failures so far.
- Why the lock: a shared dev box with several Conductor worktrees will rate-limit
the model API if two eval suites run at once (15-way concurrency each), which
mass-times-out E2E tests. The lock makes the second run WAIT, not collide.
+3 -2
View File
@@ -1,8 +1,9 @@
# gstack Builder Ethos
These are the principles that shape how gstack thinks, recommends, and builds.
They are injected into every workflow skill's preamble automatically. They
reflect what we believe about building software in 2026.
Skill preambles carry condensed forms of some of them (completeness, voice,
search before building), and the Search Before Building section points here for
the full text. They reflect what we believe about building software in 2026.
---
+7 -5
View File
@@ -40,14 +40,16 @@ This is the gstack router. Its one job is to send the request to the right skill
Best-effort, record which way you routed (never block on it). Set `ROUTE_OUTCOME` to
`browse` (sent to /browse), `routed` (sent to another skill), or `direct` (answered
directly, no skill matched):
directly, no skill matched), and replace `SESSION_ID` with the value the skill-start
output echoed:
```bash
~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type route --skill gstack --outcome ROUTE_OUTCOME --session-id "$_SESSION_ID" 2>/dev/null || true
~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type route --skill gstack --outcome ROUTE_OUTCOME --session-id "SESSION_ID" 2>/dev/null || true
```
If `PROACTIVE` is `false`: do NOT proactively invoke or suggest other gstack skills during
this session. Only run skills the user explicitly invokes. This preference persists across
sessions via `gstack-config`.
If `PROACTIVE` is `false`: do not proactively invoke or suggest other gstack skills during
this session, including by asking whether to run one ("want me to run /X?"). Only run
skills the user explicitly invokes. This preference persists across sessions via
`gstack-config`.
If `PROACTIVE` is `true` (default): **invoke the Skill tool** when the user's request
matches a skill's purpose. Do NOT answer directly when a skill exists for the task.
+3 -2
View File
@@ -6,5 +6,6 @@ turns out to be unnecessary, mark it skipped with a one-line reason.
non-trivial new features), briefly state your approach before executing. This lets
the user course-correct cheaply instead of mid-flight.
**Dedicated tools over Bash.** Prefer Read, Edit, Write, Glob, Grep over shell
equivalents (cat, sed, find, grep). The dedicated tools are cheaper and clearer.
**Dedicated tools over Bash.** Prefer the host's dedicated file tools (Read, Edit,
Write, and its search tools when it has them) over shell equivalents (cat, sed,
find, grep). The dedicated tools are cheaper and clearer.
+20 -5
View File
@@ -34,14 +34,30 @@ export type Model = (typeof ALL_MODEL_NAMES)[number];
/**
* Resolve a model argument from CLI input to a known Model family.
*
* Input is trimmed and lowercased, and one trailing `[...]` context marker
* (for example `claude-sonnet-5[1m]`) is stripped before matching.
*
* Precedence rules:
* 1. Exact match against ALL_MODEL_NAMES → return as-is. This is the ONLY
* path that selects `gpt-5.6-sol` — Sol is intentionally exact-only.
* It is also how to force a model-pinned profile: `--model opus-4-7`,
* `--model sonnet-5`, and so on.
* 2. Family heuristics for common variants:
* - `gpt-5.4-mini`, `gpt-5.4-turbo`, `gpt-5.4-*` → `gpt-5.4`
* - `gpt-*` (anything else GPT, including other 5.6 variants) → `gpt`
* - `o3`, `o4`, `o4-mini`, `o1`, `o1-mini`, `o1-pro` → `o-series`
* - `claude-*` (sonnet, opus, haiku, any version) → `claude`
* - Model-pinned Claude families (Opus 4.7, Opus 4.8, Sonnet 5) use an
* allowlist: the bare ID (`claude-<family>-<major>`), the bare ID plus
* an 8-digit date snapshot (`-YYYYMMDD`), and the bare ID plus `-latest`
* keep the pinned profile. Any other suffix, including a point release
* with or without a date (`claude-opus-4-7-1`, `claude-sonnet-5-5`,
* `claude-sonnet-5-5-20261001`), falls to `claude`: a newer model should
* not read nudges written for an older one.
* - Fable is the exception: any `claude-fable-5-*` ID keeps `fable-5`,
* because `claude-fable-5-1` is CLAUDE_FRONTIER_EVAL_MODEL
* (lib/eval-model.ts) and moving it would shift every eval baseline.
* The exception ends when CLAUDE_FRONTIER_EVAL_MODEL changes.
* - Any other `claude-*` (including `claude-opus-5-5`) → `claude`
* - `gemini-*` (2.5-pro, flash, etc.) → `gemini`
* 3. Unknown input → returns null (caller decides: error, or fall back).
*
@@ -50,7 +66,7 @@ export type Model = (typeof ALL_MODEL_NAMES)[number];
* normalizes CLI input to a family name.
*/
export function resolveModel(input: string): Model | null {
const s = input.trim();
const s = input.trim().toLowerCase().replace(/\[[^\]]*\]$/, '').trim();
if (!s) return null;
// Exact match first
@@ -67,10 +83,9 @@ export function resolveModel(input: string): Model | null {
if (/^gpt-5\.4(-|$)/.test(s)) return 'gpt-5.4';
if (/^gpt(-|$)/.test(s)) return 'gpt';
if (/^o[0-9]+(-|$)/.test(s)) return 'o-series';
if (/^claude-opus-4-7(-|$)/.test(s)) return 'opus-4-7';
const pinned = /^claude-(opus-4-7|opus-4-8|sonnet-5)(-\d{8}|-latest)?$/.exec(s);
if (pinned) return pinned[1] as Model;
if (/^claude-fable-5(-|$)/.test(s)) return 'fable-5';
if (/^claude-opus-4-8(-|$)/.test(s)) return 'opus-4-8';
if (/^claude-sonnet-5(-|$)/.test(s)) return 'sonnet-5';
if (/^claude(-|$)/.test(s)) return 'claude';
if (/^gemini(-|$)/.test(s)) return 'gemini';
@@ -48,13 +48,12 @@ Escalate after 3 failed attempts, uncertain security-sensitive changes, or scope
## Operational Self-Improvement
Before completing, review the session for durable learnings and log each one —
this step ALWAYS runs, it is not conditional on something feeling noteworthy
(#2402: 43 of 44 learnings came from explicit /learn because "if you
discovered" read as optional). A durable learning is a project quirk, command
fix, pitfall, or pattern that would save 5+ minutes in a future session. If
the review genuinely surfaces none, state "No durable learnings this session"
in your completion summary — an explicit empty result, not a skipped step.
Before completing, review the session for durable learnings and log each one.
The review runs every time, not only when something felt noteworthy. A durable
learning is a project quirk, command fix, pitfall, or pattern that would save
5+ minutes in a future session. If the review genuinely surfaces none, state
"No durable learnings this session" in your completion summary — an explicit
empty result, not a skipped step.
\`\`\`bash
${ctx.paths.binDir}/gstack-learnings-log '{"skill":"SKILL_NAME","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":N,"source":"observed"}'
@@ -4,7 +4,7 @@ export function generateContextHealth(ctx?: TemplateContext): string {
if (ctx?.explainLevel === 'terse') return '';
return `## Context Health (soft directive)
During long-running skill sessions, periodically write a brief \`[PROGRESS]\` summary: done, next, surprises.
During long-running skill sessions, when you finish a phase or change direction, tell the user in a sentence or two what is done, what is next, and anything surprising.
If you are looping on the same diagnostic, same file, or failed fix variants, STOP and reassess. Consider escalation or /context-save. Progress summaries must NEVER mutate git state.`;
}
@@ -31,34 +31,18 @@ Check \`REPO_MODE\` from the preamble output.
**If REPO_MODE is \`solo\`:**
Use AskUserQuestion:
> These test failures appear pre-existing (not caused by your branch changes):
>
> [list each failure with file:line and brief error description]
>
> Since this is a solo repo, you're the only one who will fix these.
>
> RECOMMENDATION: Choose A — fix now while the context is fresh. Completeness: 9/10.
> A) Investigate and fix now (human: ~2-4h / CC: ~15min) — Completeness: 10/10
> B) Add as P0 TODO — fix after this branch lands — Completeness: 7/10
> C) Skip — I know about this, ship anyway — Completeness: 3/10
Ask with AskUserQuestion in the AskUserQuestion Format. List each failure with file:line and a brief error description, say the failures appear pre-existing (not caused by this branch), and say that in a solo repo nobody else will fix them. Options, with A recommended because the context is fresh:
- A) Investigate and fix now (human: ~2-4h / CC: ~15min) — Completeness 10/10
- B) Add as P0 TODO — fix after this branch lands — Completeness 7/10
- C) Skip — I know about this, ship anyway — Completeness 3/10
**If REPO_MODE is \`collaborative\` or \`unknown\`:**
Use AskUserQuestion:
> These test failures appear pre-existing (not caused by your branch changes):
>
> [list each failure with file:line and brief error description]
>
> This is a collaborative repo — these may be someone else's responsibility.
>
> RECOMMENDATION: Choose B — assign it to whoever broke it so the right person fixes it. Completeness: 9/10.
> A) Investigate and fix now anyway — Completeness: 10/10
> B) Blame + assign GitHub issue to the author — Completeness: 9/10
> C) Add as P0 TODO — Completeness: 7/10
> D) Skip — ship anyway — Completeness: 3/10
Ask with AskUserQuestion in the AskUserQuestion Format. List each failure the same way, and say that in a collaborative repo these may be someone else's responsibility. Options, with B recommended so the person who broke it fixes it:
- A) Investigate and fix now anyway — Completeness 10/10
- B) Blame + assign GitHub issue to the author — Completeness 9/10
- C) Add as P0 TODO — Completeness 7/10
- D) Skip — ship anyway — Completeness 3/10
### Step T4: Execute the chosen action
@@ -8,7 +8,7 @@ import type { TemplateContext } from '../types';
* the fence prose scopes and the model follows.
*/
export function generateUpgradeCheck(ctx: TemplateContext): string {
return `If \`PROACTIVE\` is \`"false"\`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
return `If \`PROACTIVE\` is \`false\`, do not auto-invoke or suggest skills, including by asking whether to run one. Only run skills the user explicitly invokes.
If \`SKILL_PREFIX\` is \`"true"\`, suggest/invoke \`/gstack-*\` names. Disk paths stay \`${ctx.paths.skillRoot}/[skill-name]/SKILL.md\`.`;
}
@@ -31,6 +31,6 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
- User-turn override wins: if the current message asks for terse / no explanations / just the answer, skip this section.
- Terse mode (EXPLAIN_LEVEL: terse): no glosses, no outcome-framing layer, shorter responses.
Curated jargon list lives at \`${jargonPath}\` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the \`terms\` array as the canonical list. The list is repo-owned and may grow between releases.
Curated jargon list lives at \`${jargonPath}\`. On the first jargon term you encounter this session, Read that file once; treat the \`terms\` array as the canonical list. The list is repo-owned and may grow between releases.
`;
}
+2 -2
View File
@@ -640,7 +640,7 @@ describe('gen-skill-docs', () => {
// Should not have Edit, Glob, or Grep in allowed-tools.
// Scope to frontmatter (between the first two --- lines) — the body can
// legitimately mention these tool names in prose (e.g., Claude model
// overlay says "prefer Read, Edit, Write, Glob, Grep over Bash").
// overlay names the host's dedicated file and search tools).
const fmMatch = qaOnlyContent.match(/^---\n([\s\S]*?)\n---/);
expect(fmMatch).not.toBeNull();
const frontmatter = fmMatch![1];
@@ -4485,7 +4485,7 @@ describe('plan-mode-info resolver (handshake-replacement)', () => {
'utf-8',
);
const planModeIdx = content.indexOf(PLAN_MODE_INFO_MARKER);
const upgradeIdx = content.indexOf('If `PROACTIVE` is `"false"`');
const upgradeIdx = content.search(/If `PROACTIVE` is `"?false"?`/i);
expect(planModeIdx).toBeGreaterThan(0);
expect(upgradeIdx).toBeGreaterThan(0);
expect(planModeIdx).toBeLessThan(upgradeIdx);
+45
View File
@@ -10,6 +10,7 @@ import type { TemplateContext } from '../scripts/resolvers/types';
import { HOST_PATHS } from '../scripts/resolvers/types';
import { generateModelOverlay } from '../scripts/resolvers/model-overlay';
import { resolveModel } from '../scripts/models';
import { CLAUDE_FRONTIER_EVAL_MODEL } from '../lib/eval-model';
import { readOverlay } from '../scripts/resolvers/model-overlay';
import { generateCompletenessSection } from '../scripts/resolvers/preamble/generate-completeness-section';
import { generateSetupCommand } from '../scripts/resolvers/utility';
@@ -368,3 +369,47 @@ describe('Sonnet 5 overlay — family nudges', () => {
});
});
});
describe('model-routing-claude', () => {
test.each([
['claude-opus-4-7', 'opus-4-7'],
['claude-opus-4-7-20260416', 'opus-4-7'],
['claude-opus-4-7-latest', 'opus-4-7'],
['claude-opus-4-7-1', 'claude'],
['claude-opus-4-7-1-20260601', 'claude'],
['claude-opus-4-8', 'opus-4-8'],
['claude-opus-4-8-20260701', 'opus-4-8'],
['claude-opus-4-8-latest', 'opus-4-8'],
['claude-opus-4-8-1', 'claude'],
['claude-sonnet-5', 'sonnet-5'],
['claude-sonnet-5-20260115', 'sonnet-5'],
['claude-sonnet-5-latest', 'sonnet-5'],
['claude-sonnet-5-5', 'claude'],
['claude-sonnet-5-5-20261001', 'claude'],
['claude-sonnet-5-10', 'claude'],
['claude-sonnet-5-latest-20260101', 'claude'],
['claude-opus-5-5', 'claude'],
['claude-fable-5', 'fable-5'],
['claude-fable-5-1', 'fable-5'],
])('%s routes to %s', (input, expected) => {
expect(resolveModel(input)).toBe(expected);
});
test('input is lowercased and one trailing [...] marker is stripped', () => {
expect(resolveModel('CLAUDE-SONNET-5')).toBe('sonnet-5');
expect(resolveModel('claude-sonnet-5[1m]')).toBe('sonnet-5');
expect(resolveModel('claude-opus-5-5[1m]')).toBe('claude');
});
test('family names force the pinned profile', () => {
expect(resolveModel('opus-4-7')).toBe('opus-4-7');
expect(resolveModel('opus-4-8')).toBe('opus-4-8');
expect(resolveModel('sonnet-5')).toBe('sonnet-5');
});
test('Fable prefix exception holds only while the frontier eval model is a Fable 5 point release', () => {
if (resolveModel('claude-fable-5-99') === 'fable-5') {
expect(CLAUDE_FRONTIER_EVAL_MODEL.startsWith('claude-fable-5-')).toBe(true);
}
});
});
+1 -1
View File
@@ -100,7 +100,7 @@ describe('terse build — per-resolver behavior', () => {
test('default: emits full section', () => {
const out = generateContextHealth(makeCtx('default'));
expect(out).toContain('## Context Health');
expect(out).toContain('PROGRESS');
expect(out.replace(/^## Context Health.*$/m, '').trim().length).toBeGreaterThan(80);
});
test('terse: returns empty string', () => {