mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-08 17:58:13 +01:00
Merge main 1ccab0d into gsta258-l1-release-gate (rule 19 grant, GSTA-36)
Resolve scripts/harness-version.json against #3903: take main's manifest and carry this branch's test-pr-profile.ts pin into scripts/harness-pins/scripts/test-pr-profile.ts.json. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
commit
c0d33a2195
133 files changed
+1414
-240
No files matched your search
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
|
||||
/**
|
||||
* Keep HARNESS_VERSION honest (CEO-27/ENG-3/DX-6). scripts/harness-version.json
|
||||
* pins the blob of every shared-harness file (GLOBAL_TOUCHFILES plus its
|
||||
* `patterns`). test/harness-version.test.ts fails when a harness file changes
|
||||
* holds the version and the harness `patterns`; scripts/harness-pins/<path>.json
|
||||
* pins the blob of every shared-harness file (GLOBAL_TOUCHFILES plus those
|
||||
* patterns). test/harness-version.test.ts fails when a harness file changes
|
||||
* without a decision, and this script records the decision:
|
||||
*
|
||||
* --bump "<reason>" the change alters how cases behave: version + 1,
|
||||
@@ -12,9 +13,10 @@
|
||||
* compatible and no series resets
|
||||
* --check print drift and the fix; exit 1 on drift
|
||||
*
|
||||
* Entries are one line per file, so two PRs touching different harness files
|
||||
* merge cleanly. When both bump, or both touch one file, git conflicts on the
|
||||
* manifest: take the higher version, then rerun this script.
|
||||
* Each pin is its own file (GST-1084), so two PRs touching different harness
|
||||
* files never edit the same file and merge cleanly. When both bump, or both
|
||||
* touch one file, git conflicts on the manifest or that pin: take the higher
|
||||
* version, then rerun this script.
|
||||
*/
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
@@ -23,14 +25,35 @@ import { harnessFiles, treeEntries, type TreeEntry } from './eval-trial-series';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
export const MANIFEST = 'scripts/harness-version.json';
|
||||
export const PINS_DIR = 'scripts/harness-pins';
|
||||
export const DOC_ANCHOR = 'docs/TESTING_INTERNALS.md#harness-version';
|
||||
|
||||
export interface ManifestEntry { blob: string; since: number; nonBehavioral?: string }
|
||||
export interface HarnessManifest { version: number; patterns: string[]; files: Record<string, ManifestEntry> }
|
||||
export interface Drift { changed: string[]; added: string[]; removed: string[] }
|
||||
|
||||
const pinPath = (root: string, file: string) => path.join(root, PINS_DIR, `${file}.json`);
|
||||
|
||||
/** The pin files under root's PINS_DIR, keyed by pinned path; any file that is not <path>.json is an error. */
|
||||
function readPins(root: string): Record<string, ManifestEntry> {
|
||||
const dir = path.join(root, PINS_DIR);
|
||||
const files: Record<string, ManifestEntry> = {};
|
||||
for (const rel of (fs.readdirSync(dir, { recursive: true }) as string[]).map(f => f.split(path.sep).join('/')).sort()) {
|
||||
if (fs.statSync(path.join(dir, rel)).isDirectory()) continue;
|
||||
if (!rel.endsWith('.json')) throw new Error(`${PINS_DIR}/${rel}: every harness pin is <pinned path>.json`);
|
||||
try {
|
||||
files[rel.slice(0, -'.json'.length)] = JSON.parse(fs.readFileSync(path.join(dir, rel), 'utf8'));
|
||||
} catch (e) {
|
||||
throw new Error(`${PINS_DIR}/${rel}: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
export function readManifest(root = ROOT): HarnessManifest {
|
||||
return JSON.parse(fs.readFileSync(path.join(root, MANIFEST), 'utf8'));
|
||||
const head = JSON.parse(fs.readFileSync(path.join(root, MANIFEST), 'utf8'));
|
||||
if (Object.keys(head).sort().join() !== 'patterns,version') throw new Error(`${MANIFEST} holds only version and patterns; pins live in ${PINS_DIR}/<path>.json`);
|
||||
return { version: head.version, patterns: head.patterns, files: readPins(root) };
|
||||
}
|
||||
|
||||
/** The tracked harness files with blobs of their working-tree bytes (an unstaged edit counts; a deleted file drops out). */
|
||||
@@ -43,11 +66,11 @@ export function workingHarnessFiles(root: string, patterns: readonly string[]):
|
||||
return tracked.map((entry, index) => ({ file: entry.file, blob: blobs[index]! }));
|
||||
}
|
||||
|
||||
/** Harness files whose blob differs from, is missing from, or no longer matches an entry in the manifest. */
|
||||
/** Harness files whose blob differs from, is missing from, or no longer matches an entry in the manifest; a pin ahead of the manifest version (a bump that failed partway) counts as changed. */
|
||||
export function harnessDrift(manifest: HarnessManifest, files: TreeEntry[]): Drift {
|
||||
const current = new Map(files.map(entry => [entry.file, entry.blob]));
|
||||
return {
|
||||
changed: files.filter(entry => manifest.files[entry.file] && manifest.files[entry.file]!.blob !== entry.blob).map(entry => entry.file).sort(),
|
||||
changed: files.filter(entry => manifest.files[entry.file] && (manifest.files[entry.file]!.blob !== entry.blob || manifest.files[entry.file]!.since > manifest.version)).map(entry => entry.file).sort(),
|
||||
added: files.filter(entry => !manifest.files[entry.file]).map(entry => entry.file).sort(),
|
||||
removed: Object.keys(manifest.files).filter(file => !current.has(file)).sort(),
|
||||
};
|
||||
@@ -64,7 +87,7 @@ export function formatDrift(drift: Drift, version: number): string {
|
||||
'Fix (pick one):',
|
||||
' bun run scripts/bump-harness-version.ts --non-behavioral "<why no verdict can change>" # refactor/comment/logging: no series reset',
|
||||
' bun run scripts/bump-harness-version.ts --bump "<what behavior changed>" # starts a new pass-rate series for every case',
|
||||
`Merge conflict in ${MANIFEST}: take the higher version, then rerun the script. See ${DOC_ANCHOR}.`,
|
||||
`Merge conflict in ${MANIFEST} or ${PINS_DIR}/: take the higher version, then rerun the script. See ${DOC_ANCHOR}.`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -82,11 +105,16 @@ export function decide(manifest: HarnessManifest, files: TreeEntry[], decision:
|
||||
: manifest.files[entry.file]!])) };
|
||||
}
|
||||
|
||||
/** Stable, one-entry-per-line JSON so concurrent edits to different files merge cleanly. */
|
||||
export function serializeManifest(manifest: HarnessManifest): string {
|
||||
const files = Object.keys(manifest.files).sort().map(file => ` ${JSON.stringify(file)}: ${JSON.stringify(manifest.files[file])}`);
|
||||
return `{\n "version": ${manifest.version},\n "patterns": [\n${manifest.patterns.map(pattern => ` ${JSON.stringify(pattern)}`).join(',\n')}\n ],\n`
|
||||
+ ` "files": {\n${files.join(',\n')}\n }\n}\n`;
|
||||
/** Write one pin file per harness file (rewriting only pins whose bytes change, deleting dropped ones), then the manifest head, so a failed pin write never moves the version. */
|
||||
export function writeManifest(root: string, manifest: HarnessManifest): void {
|
||||
for (const file of Object.keys(readPins(root))) if (!manifest.files[file]) fs.rmSync(pinPath(root, file));
|
||||
for (const [file, entry] of Object.entries(manifest.files)) {
|
||||
const target = pinPath(root, file), text = `${JSON.stringify(entry)}\n`;
|
||||
if (fs.existsSync(target) && fs.readFileSync(target, 'utf8') === text) continue;
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, text);
|
||||
}
|
||||
fs.writeFileSync(path.join(root, MANIFEST), `${JSON.stringify({ version: manifest.version, patterns: manifest.patterns }, null, 2)}\n`);
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
@@ -106,7 +134,7 @@ if (import.meta.main) {
|
||||
}
|
||||
if (mode === '--non-behavioral' && !hasDrift(drift)) { console.log('no harness drift: nothing to record'); process.exit(0); }
|
||||
const next = decide(manifest, files, mode === '--bump' ? 'bump' : 'non-behavioral', argv[1] ?? '');
|
||||
fs.writeFileSync(path.join(ROOT, MANIFEST), serializeManifest(next));
|
||||
writeManifest(ROOT, next);
|
||||
console.log(mode === '--bump'
|
||||
? `HARNESS_VERSION ${manifest.version} -> ${next.version}: every case starts a new pass-rate series (reason: ${argv[1]})`
|
||||
: `recorded ${drift.changed.length + drift.added.length} file(s) as non-behavioral at HARNESS_VERSION ${next.version}${drift.removed.length ? `, dropped ${drift.removed.length} removed file(s)` : ''}`);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"f862cf5241580c7593b9ee62eacf5ef046248d26","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"e8ce3cb98474dded38c5128a631d283b0ca35b63","since":6,"nonBehavioral":"F1 rule-16 flaky rerun: report aggregation, rerun planning and the gate slice exit code; no case's execution, trial outcome, trial record or panel verdict changes, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"e0d18da4ea02ac443835a68992abeea2b47f22fc","since":6,"nonBehavioral":"the headline adds only streamed session cost; a billed session's cost is already in its collector total; cache writes without a 5m key fall back to the total minus 1h; cost fields only"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"dc714d7d897aed185c0ec3d2c92ad22d954039e5","since":6,"nonBehavioral":"createShardSandbox drops the Capy launch markers (GSTACK_SESSION_KIND, GSTACK_CAPY, GSTACK_EPHEMERAL, GSTACK_INSTALL_VERDICT) from shard environments; CI never sets them, so no case's input or verdict changes there, and on a Capy machine fixtures now see the session kind they set themselves instead of the installer's"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"5439d0a8f6e1569fa7c924af20d67c30c81d5d30","since":6,"nonBehavioral":"credential rejections are classed infra instead of assertion: no case's pass or fail changes; only failed trials whose provider rejected the run's key move class, so no rerun spends on a rejected key"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"89a688ffeb7f000e7341062e45bf93e36a0119ce","since":6,"nonBehavioral":"test-pr-profile.ts lists .github/workflows/release-gate.yml and .github/workflows/windows-scoped-tests.yml as free-lane only; neither workflow runs a paid case through this list, so no case selection or verdict can change"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"d66dac458b8c13250973e550edf8087d4044cd0b","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets. Also: runAgentSdkTest now validates systemPrompt before starting; every live caller already passed the claude_code preset, so the prompt the model sees is unchanged and no verdict can change"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"ce7a5eaa66e502a052ea9c53348dd2ab25db6148","since":6,"nonBehavioral":"Removes the plan-design 0D focus picker from the shared harness; floor.ts gains an optional pickSetupChoice hook that no other caller passes, so no other case's input or verdict can change. plan-design-finding-floor, the only case whose seed raises that menu, passes the picker from its owned fixture test/fixtures/design-floor-focus-picker.ts, so its owned bytes start its own new series."}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"bff225017e235f0be6ece29f04971ac04b95b544","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"c040ebbfa815b8cce19ef80c6133bd5a5474149a","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"91996ff2bc303ee29f4e6f1705d1468b2536b32e","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"4085ad18cd99698f675deadad2a76a796427b693","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"cc072895461dc52ea317d1beff72121342420d30","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"ff32d03f32e4af76c6c7314a79cd1b35263ed0dd","since":6,"nonBehavioral":"llm-judge.ts adds a pass-through byte counter (bytesReceived, lastByteMs) to the aborted-judge diagnostic; the request, response bytes, deadline and scoring are unchanged. GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading so the review E2E cases extract the same section (now the skill's own reworded bytes); llm-judge.ts changes one bullet of the cross-skill consistency judge's intended architecture to name review-bot-history.md beside the older greptile-history.md name, so its calibration corpus (greptile-history inputs) stays valid. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own series"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"c09b69611fb22ddf093adb2baa83434494e5f8fa","since":6,"nonBehavioral":"Widens the two plan-eng-review boundary predicates' qid regex to also accept the optional ;signal=<key> hint suffix (#3442). Only a question carrying that suffix can change verdict, and only the changed question-tuning resolver asks for it; every case that runs a skill with that resolver owns its generated SKILL.md, which this change also edits, so those cases already start their own series. No other case's input or verdict can change."}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"4f0731ef39bc3a6a7c5c592ce98690a24cb1ed7f","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"a9938cb98f285474ebc4860cd61012504a36466e","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"1384f765e48e511d5f843df2660b2ec70bbcb6b2","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"8fd1749a62f24dc37ae186c8a21c2165f7f7645e","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"08f7ccbdbd9d71a5dc86edd2f8fced423a78ad9a","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"c99baaaa0a77a5a72384493fd8a85a4253ecb05d","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"c73b2596d9eb5377530a6bf1d284e0b6ea1556a0","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"ce7609eacc7a8265a4a211358d1b4fb49ea85bc5","since":6,"nonBehavioral":"GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading ('Check for review bot comments') so the review E2E cases extract the same section, now the skill's own reworded bytes; llm-judge.ts updates only the cross-skill consistency judge's intended architecture to the renamed review-bot-history.md and its record helper. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own new series"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"a8d21e3817c3f35a1d06af325c92fde7d420e377","since":6,"nonBehavioral":"readSkillEffective also appends a variant's added ### subsections (office-hours' Open-ended questions); readSkillStartup, readSharedSections and readSkillEffective return bytes identical to the pinned helper for every tier 2+ render without added subsections (735 across all hosts) and for office-hours' SKILL.md before this PR, so office-hours' fixture input changes only because its prompt changed in this PR, not because the helper did"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"e1b54ad5e3d3eadd8027de1b44e57554d6d99a41","since":6,"nonBehavioral":"selection only: fragment exemption narrowed to .md files directly in the fragments dir; changed-file lists include a rename's old path; no case prompt, fixture, judge or verdict logic changes (GST-400)"}
|
||||
@@ -0,0 +1 @@
|
||||
{"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":6}
|
||||
@@ -9,49 +9,5 @@
|
||||
"test/helpers/pty-screen.ts",
|
||||
"test/helpers/e2e-gate.ts",
|
||||
"test/helpers/llm-judge.ts"
|
||||
],
|
||||
"files": {
|
||||
"scripts/lib/paid-cases.ts": {"blob":"f862cf5241580c7593b9ee62eacf5ef046248d26","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"},
|
||||
"scripts/lib/paid-plan.ts": {"blob":"e8ce3cb98474dded38c5128a631d283b0ca35b63","since":6,"nonBehavioral":"F1 rule-16 flaky rerun: report aggregation, rerun planning and the gate slice exit code; no case's execution, trial outcome, trial record or panel verdict changes, so no pass-rate series resets"},
|
||||
"scripts/lib/paid-report.ts": {"blob":"e0d18da4ea02ac443835a68992abeea2b47f22fc","since":6,"nonBehavioral":"the headline adds only streamed session cost; a billed session's cost is already in its collector total; cache writes without a 5m key fall back to the total minus 1h; cost fields only"},
|
||||
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":6},
|
||||
"scripts/lib/shard-engine.ts": {"blob":"dc714d7d897aed185c0ec3d2c92ad22d954039e5","since":6,"nonBehavioral":"createShardSandbox drops the Capy launch markers (GSTACK_SESSION_KIND, GSTACK_CAPY, GSTACK_EPHEMERAL, GSTACK_INSTALL_VERDICT) from shard environments; CI never sets them, so no case's input or verdict changes there, and on a Capy machine fixtures now see the session kind they set themselves instead of the installer's"},
|
||||
"scripts/test-paid-shards.ts": {"blob":"5439d0a8f6e1569fa7c924af20d67c30c81d5d30","since":6,"nonBehavioral":"credential rejections are classed infra instead of assertion: no case's pass or fail changes; only failed trials whose provider rejected the run's key move class, so no rerun spends on a rejected key"},
|
||||
"scripts/test-pr-profile.ts": {"blob":"89a688ffeb7f000e7341062e45bf93e36a0119ce","since":6,"nonBehavioral":"test-pr-profile.ts lists .github/workflows/release-gate.yml and .github/workflows/windows-scoped-tests.yml as free-lane only; neither workflow runs a paid case through this list, so no case selection or verdict can change"},
|
||||
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":6},
|
||||
"test/helpers/agent-sdk-runner.ts": {"blob":"d66dac458b8c13250973e550edf8087d4044cd0b","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets. Also: runAgentSdkTest now validates systemPrompt before starting; every live caller already passed the claude_code preset, so the prompt the model sees is unchanged and no verdict can change"},
|
||||
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":6},
|
||||
"test/helpers/claude-pty-runner.ts": {"blob":"ce7a5eaa66e502a052ea9c53348dd2ab25db6148","since":6,"nonBehavioral":"Removes the plan-design 0D focus picker from the shared harness; floor.ts gains an optional pickSetupChoice hook that no other caller passes, so no other case's input or verdict can change. plan-design-finding-floor, the only case whose seed raises that menu, passes the picker from its owned fixture test/fixtures/design-floor-focus-picker.ts, so its owned bytes start its own new series."},
|
||||
"test/helpers/codex-host-runner.ts": {"blob":"bff225017e235f0be6ece29f04971ac04b95b544","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"},
|
||||
"test/helpers/codex-session-runner.ts": {"blob":"c040ebbfa815b8cce19ef80c6133bd5a5474149a","since":6,"nonBehavioral":"Claude-host verdicts are unchanged: the Codex host (EVALS_HOST=codex, codex-host-runner) runs only when selected and records its own per-model series; the Codex runner refactor (execCodex extraction, onSpawn hook) keeps its events and verdicts and adds token cost accounting; host fields on the plan/report are absent for claude"},
|
||||
"test/helpers/e2e-gate.ts": {"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":6},
|
||||
"test/helpers/e2e-helpers.ts": {"blob":"91996ff2bc303ee29f4e6f1705d1468b2536b32e","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"},
|
||||
"test/helpers/eval-budgets.ts": {"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":6},
|
||||
"test/helpers/eval-store.ts": {"blob":"4085ad18cd99698f675deadad2a76a796427b693","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"},
|
||||
"test/helpers/hermetic-env.ts": {"blob":"cc072895461dc52ea317d1beff72121342420d30","since":6},
|
||||
"test/helpers/llm-judge.ts": {"blob":"ff32d03f32e4af76c6c7314a79cd1b35263ed0dd","since":6,"nonBehavioral":"llm-judge.ts adds a pass-through byte counter (bytesReceived, lastByteMs) to the aborted-judge diagnostic; the request, response bytes, deadline and scoring are unchanged. GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading so the review E2E cases extract the same section (now the skill's own reworded bytes); llm-judge.ts changes one bullet of the cross-skill consistency judge's intended architecture to name review-bot-history.md beside the older greptile-history.md name, so its calibration corpus (greptile-history inputs) stays valid. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own series"},
|
||||
"test/helpers/paid-test-set.ts": {"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":6},
|
||||
"test/helpers/pty-screen.ts": {"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":6},
|
||||
"test/helpers/pty/auq.ts": {"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":6},
|
||||
"test/helpers/pty/binary.ts": {"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":6},
|
||||
"test/helpers/pty/boundaries.ts": {"blob":"c09b69611fb22ddf093adb2baa83434494e5f8fa","since":6,"nonBehavioral":"Widens the two plan-eng-review boundary predicates' qid regex to also accept the optional ;signal=<key> hint suffix (#3442). Only a question carrying that suffix can change verdict, and only the changed question-tuning resolver asks for it; every case that runs a skill with that resolver owns its generated SKILL.md, which this change also edits, so those cases already start their own series. No other case's input or verdict can change."},
|
||||
"test/helpers/pty/classify.ts": {"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":6},
|
||||
"test/helpers/pty/fake-session.ts": {"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":6},
|
||||
"test/helpers/pty/judge.ts": {"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":6},
|
||||
"test/helpers/pty/launch.ts": {"blob":"4f0731ef39bc3a6a7c5c592ce98690a24cb1ed7f","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
|
||||
"test/helpers/pty/plan-native.ts": {"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":6},
|
||||
"test/helpers/pty/runners/counting.ts": {"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":6},
|
||||
"test/helpers/pty/runners/floor.ts": {"blob":"a9938cb98f285474ebc4860cd61012504a36466e","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"},
|
||||
"test/helpers/pty/runners/observation.ts": {"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":6},
|
||||
"test/helpers/pty/screen.ts": {"blob":"1384f765e48e511d5f843df2660b2ec70bbcb6b2","since":6,"nonBehavioral":"The PTY floor and observation runners end a plan-mode turn at an idle prompt (the plan-mode footer without its running 'esc to interrupt' segment, after a 10 s settle) instead of waiting out the deadline. The floor runner exits as api_error only after the idle screen with its error panel held still for 10 s and every tick's question and assessment steps found nothing to credit (the transcript is re-read each tick), so a native or prose question already on screen is credited as before, even when its transcript line lands after the screen draws done (test/helpers/claude-pty-runner.runners.unit.test.ts, the prose-question-before-panel case). With no qualifying question an idle turn stays idle, so a trial that failed at the deadline as observer_timeout now fails earlier as api_error. An observation returns what the deadline would have returned. A running footer, extra footer text and a cut footer still wait for the deadline. No pass becomes a fail or a fail a pass. The session ledger may now record failure_cause api_error or refusal instead of observer_timeout or session_timeout for a plan-mode session whose last idle turn shows a panel; failure_class, the gate red class and pass rates are unchanged, so no pass-rate series resets"},
|
||||
"test/helpers/pty/session.ts": {"blob":"8fd1749a62f24dc37ae186c8a21c2165f7f7645e","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
|
||||
"test/helpers/pty/transcript-cost.ts": {"blob":"08f7ccbdbd9d71a5dc86edd2f8fced423a78ad9a","since":6,"nonBehavioral":"PTY transcript pricing finds the session by the caller's --session-id or the one new transcript for the launch cwd; the launcher adds no argument; native AUQ capture opts out; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
|
||||
"test/helpers/session-drain-policy.ts": {"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":6},
|
||||
"test/helpers/session-ledger.ts": {"blob":"c99baaaa0a77a5a72384493fd8a85a4253ecb05d","since":6,"nonBehavioral":"PTY sessions price their ledger row (streamed_cost_usd) from the session transcript found by the caller's --session-id or the one new transcript for the launch cwd; ledger cost fields only, no prompt, argv, answer, timeout or verdict changes"},
|
||||
"test/helpers/session-runner.ts": {"blob":"c73b2596d9eb5377530a6bf1d284e0b6ea1556a0","since":6,"nonBehavioral":"Cost attribution only (GSTA-150 PR 2): a display-label record carries cost_case_id from runSkillTest's testName and the report binds its cost by it while the failed record and failure class still come from the named records (test/paid-report-cost.test.ts pins both); an aborted Agent SDK session logs its streamed usage as streamed_cost_usd, used only when a case has no record; a listed model-free case with no record or session is a known $0. No case's execution, outcome, failure class, rerun eligibility or panel verdict changes, so no pass-rate series resets"},
|
||||
"test/helpers/skill-fixture.ts": {"blob":"ce7609eacc7a8265a4a211358d1b4fb49ea85bc5","since":6,"nonBehavioral":"GSTA-224 B2 (#3122): skill-fixture.ts follows review's renamed Step 2.5 heading ('Check for review bot comments') so the review E2E cases extract the same section, now the skill's own reworded bytes; llm-judge.ts updates only the cross-skill consistency judge's intended architecture to the renamed review-bot-history.md and its record helper. No other case's input or verdict can change; the cross-skill judge's judged files changed in this PR, so its owned inputs start its own new series"},
|
||||
"test/helpers/skill-startup.ts": {"blob":"a8d21e3817c3f35a1d06af325c92fde7d420e377","since":6,"nonBehavioral":"readSkillEffective also appends a variant's added ### subsections (office-hours' Open-ended questions); readSkillStartup, readSharedSections and readSkillEffective return bytes identical to the pinned helper for every tier 2+ render without added subsections (735 across all hosts) and for office-hours' SKILL.md before this PR, so office-hours' fixture input changes only because its prompt changed in this PR, not because the helper did"},
|
||||
"test/helpers/test-selection.ts": {"blob":"e1b54ad5e3d3eadd8027de1b44e57554d6d99a41","since":6,"nonBehavioral":"selection only: fragment exemption narrowed to .md files directly in the fragments dir; changed-file lists include a rename's old path; no case prompt, fixture, judge or verdict logic changes (GST-400)"},
|
||||
"test/helpers/touchfiles.ts": {"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":6}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* {{CONTRACT_DONE_MEANS}}: a skill's `contract:` frontmatter block (GST-1088, frozen v1
|
||||
* schema) rendered as a short "Done means" list at the top of the body, so the
|
||||
* outputs the model reads as required and the block the Stop check reads cannot
|
||||
* drift. parseSkillContract is the schema; test/skill-descriptions.test.ts runs it
|
||||
* over every template, so a malformed block fails CI instead of failing open.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { TemplateContext } from './types';
|
||||
import { getHostConfig } from '../../hosts/index';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..', '..');
|
||||
const code = (s: string) => '\u0060' + s + '\u0060';
|
||||
|
||||
export type ContractItem =
|
||||
| { id: string; kind: 'report_labels'; heading: string; labels: string[] }
|
||||
| { id: string; kind: 'check_after_last_edit' }
|
||||
| { id: string; kind: 'selected_specialists_dispatched'; marker: string }
|
||||
| { id: string; kind: 'agent_dispatch'; match: string };
|
||||
|
||||
export interface SkillContract {
|
||||
version: 1;
|
||||
items: ContractItem[];
|
||||
}
|
||||
|
||||
const KIND_FIELDS: Record<ContractItem['kind'], string[]> = {
|
||||
report_labels: ['heading', 'labels'],
|
||||
check_after_last_edit: [],
|
||||
selected_specialists_dispatched: ['marker'],
|
||||
agent_dispatch: ['match'],
|
||||
};
|
||||
|
||||
// The resolver and section that render the step each kind checks today. A host that suppresses the resolver, or a loadout that excludes the section, never shows that step, so its items are left out of the list there.
|
||||
const BACKED_BY: Partial<Record<ContractItem['kind'], { resolver: string; section: string }>> = {
|
||||
selected_specialists_dispatched: { resolver: 'REVIEW_ARMY', section: 'review-army' },
|
||||
agent_dispatch: { resolver: 'ADVERSARIAL_STEP', section: 'adversarial' },
|
||||
};
|
||||
|
||||
const PLACEHOLDER = /^\[.*\]$|^<.*>$|^\.\.\.$|^(todo|tbd|n\/a)$/i;
|
||||
|
||||
function fail(where: string, problem: string): never {
|
||||
throw new Error('CONTRACT_INVALID ' + where + ': ' + problem + '. Fix: edit ' + where + ', then bun run gen:skill-docs');
|
||||
}
|
||||
|
||||
const isMap = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||
const isText = (v: unknown): v is string => typeof v === 'string' && v.trim() !== '';
|
||||
|
||||
/** Validates a parsed `contract:` value against the frozen v1 schema; where names the template in errors. */
|
||||
export function parseSkillContract(value: unknown, where: string): SkillContract {
|
||||
if (!isMap(value)) fail(where, 'contract must be a mapping { version: 1, items: [...] }');
|
||||
for (const key of Object.keys(value)) {
|
||||
if (key !== 'version' && key !== 'items') fail(where, 'contract has unknown key "' + key + '"');
|
||||
}
|
||||
if (value.version !== 1) fail(where, 'contract version must be 1, got ' + JSON.stringify(value.version));
|
||||
if (!Array.isArray(value.items) || value.items.length === 0) fail(where, 'contract items must be a non-empty list');
|
||||
const seen = new Set<string>();
|
||||
const items = value.items.map((raw: unknown, i: number) => {
|
||||
const at = 'items[' + i + ']';
|
||||
if (!isMap(raw)) fail(where, at + ' must be a mapping');
|
||||
const kind = raw.kind as ContractItem['kind'];
|
||||
if (typeof raw.kind !== 'string' || !Object.hasOwn(KIND_FIELDS, kind)) {
|
||||
fail(where, at + ' kind ' + JSON.stringify(raw.kind) + ' is not one of ' + Object.keys(KIND_FIELDS).join(', '));
|
||||
}
|
||||
if (typeof raw.id !== 'string' || !/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(raw.id)) {
|
||||
fail(where, at + ' id must be lowercase kebab-case, got ' + JSON.stringify(raw.id));
|
||||
}
|
||||
if (seen.has(raw.id)) fail(where, at + ' duplicate id "' + raw.id + '"');
|
||||
seen.add(raw.id);
|
||||
for (const key of Object.keys(raw)) {
|
||||
if (key !== 'id' && key !== 'kind' && !KIND_FIELDS[kind].includes(key)) fail(where, at + ' (' + kind + ') has unknown key "' + key + '"');
|
||||
}
|
||||
for (const field of KIND_FIELDS[kind]) {
|
||||
if (field !== 'labels' && !isText(raw[field])) fail(where, at + ' ' + field + ' must be a non-empty string');
|
||||
}
|
||||
if (kind === 'report_labels') {
|
||||
if (!Array.isArray(raw.labels) || raw.labels.length === 0) fail(where, at + ' labels must be a non-empty list');
|
||||
for (const label of raw.labels as unknown[]) {
|
||||
if (!isText(label)) fail(where, at + ' labels must be non-empty strings');
|
||||
if (PLACEHOLDER.test(label.trim())) fail(where, at + ' label ' + JSON.stringify(label) + ' is a placeholder');
|
||||
}
|
||||
}
|
||||
return raw as ContractItem;
|
||||
});
|
||||
return { version: 1, items };
|
||||
}
|
||||
|
||||
/** The template's contract, or null without one; required makes a missing block an error. */
|
||||
export function contractFromTemplate(tmplContent: string, where: string, opts: { required?: boolean } = {}): SkillContract | null {
|
||||
const block = tmplContent.replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
||||
if (!block || !/^contract:/m.test(block[1])) {
|
||||
if (opts.required) fail(where, '{{CONTRACT_DONE_MEANS}} needs a contract block in the frontmatter');
|
||||
return null;
|
||||
}
|
||||
let frontmatter: unknown;
|
||||
try {
|
||||
frontmatter = Bun.YAML.parse(block[1]);
|
||||
} catch (error) {
|
||||
fail(where, 'frontmatter is not valid YAML (' + (error instanceof Error ? error.message : String(error)) + ')');
|
||||
}
|
||||
return parseSkillContract(isMap(frontmatter) ? frontmatter.contract : undefined, where);
|
||||
}
|
||||
|
||||
const list = (xs: string[]) => xs.length < 2 ? xs.join('') : xs.slice(0, -1).join(', ') + ' and ' + xs[xs.length - 1];
|
||||
|
||||
function plainWords(item: ContractItem): string {
|
||||
switch (item.kind) {
|
||||
case 'report_labels': return 'a ' + code(item.heading) + ' with ' + list(item.labels) + ' filled in (' + code('none: <why>') + ' where the skill\'s own stop rule leaves one empty)';
|
||||
case 'check_after_last_edit': return 'a test or repro command run after your last edit';
|
||||
case 'selected_specialists_dispatched': return 'the specialist selection printed (' + code(item.marker) + ' ...), then one Agent dispatch per specialist it names (none when a small diff skips them)';
|
||||
case 'agent_dispatch': return 'an Agent dispatch for the ' + item.id.replace(/-/g, ' ') + ', its prompt including "' + item.match + '"';
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether bin/gstack-skill-contract ships. The PR that adds it flips this, regenerates and re-pins; test/skill-descriptions.test.ts fails while the two disagree. */
|
||||
export const CONTRACT_CHECKER_SHIPPED = true;
|
||||
|
||||
/** The list; checker adds the check and waive commands, which only a tree that ships the checker may mention. */
|
||||
export function renderDoneMeans(contract: SkillContract, checker = CONTRACT_CHECKER_SHIPPED): string {
|
||||
if (contract.items.length === 0) return '';
|
||||
return [
|
||||
'## Done means',
|
||||
'',
|
||||
'Finish only when each of these exists (contract v1):',
|
||||
'',
|
||||
...contract.items.map(item => '- ' + code(item.id) + ': ' + plainWords(item) + '.'),
|
||||
...(checker ? ['', code('gstack-skill-contract check') + ' reads them from the transcript (docs/skill-routing-and-contracts.md). ' +
|
||||
'For an item that truly does not apply, run ' + code('gstack-skill-contract waive <item> --reason "<why>"') + ' (this session only; it counts as a miss).'] : []),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function generateContractDoneMeans(ctx: TemplateContext): string {
|
||||
const where = path.relative(ROOT, ctx.tmplPath).split(path.sep).join('/');
|
||||
const contract = contractFromTemplate(fs.readFileSync(ctx.tmplPath, 'utf-8'), where, { required: true })!;
|
||||
const suppressed = new Set(getHostConfig(ctx.host).suppressedResolvers ?? []);
|
||||
const shown = (item: ContractItem) => {
|
||||
const backing = BACKED_BY[item.kind];
|
||||
return !backing || (!suppressed.has(backing.resolver) && !ctx.modules?.excluded.has('section:' + ctx.skillName + '/' + backing.section));
|
||||
};
|
||||
return renderDoneMeans({ ...contract, items: contract.items.filter(shown) });
|
||||
}
|
||||
@@ -58,6 +58,7 @@ import { generateShipDriverStops } from './ship-driver';
|
||||
import { generateQAScope, generateQAExploratory, generateQAFunctional, generateQAResource, generateQAReview, generateQAReviewPreflight, generateQAMethodReads } from './qa';
|
||||
import { generateVoiceTriggerRouting } from './voice-triggers';
|
||||
import { generateTranscriptDiscovery, generateTranscriptSources } from './transcripts';
|
||||
import { generateContractDoneMeans } from './contract-done-means';
|
||||
|
||||
export const RESOLVERS: Record<string, ResolverFn> = {
|
||||
AUTOPLAN_PUBLICATION_HOOK: generateAutoplanPublicationHook,
|
||||
@@ -197,4 +198,5 @@ export const RESOLVERS: Record<string, ResolverFn> = {
|
||||
SECTION_INDEX,
|
||||
TRANSCRIPT_SOURCES: generateTranscriptSources,
|
||||
TRANSCRIPT_DISCOVERY: generateTranscriptDiscovery,
|
||||
CONTRACT_DONE_MEANS: generateContractDoneMeans,
|
||||
};
|
||||
Reference in new issue
Block a user