mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
v1.91.45.0 feat: configurable model tiers and advisory freshness (#3073)
* feat: add configurable model tiers and advisory freshness * fix: disclose role selections before Claude review execution * fix: reject malformed model policies and keep model pins portable * fix: separate readiness diagnostics and bound version checks * fix: preserve actionable model failures and validate review inputs before probes * fix: retry released probe locks and retain late-rejection repair context * feat: finalize configurable model tiers and verified release repairs * fix: preserve strict model policy reads on Windows * fix: keep fixture probes read-only and clarify pending CEO decisions * fix: verify native terminal closure before bounded descriptor settlement
This commit is contained in:
1 parent
52eec18c63
commit
92cfd07a79
96 files changed
+6539
-411
No files matched your search
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 4,
|
||||
"patterns": [
|
||||
"test/helpers/agent-sdk-runner.ts",
|
||||
"test/helpers/codex-session-runner.ts",
|
||||
@@ -10,44 +10,44 @@
|
||||
"test/helpers/llm-judge.ts"
|
||||
],
|
||||
"files": {
|
||||
"scripts/lib/paid-cases.ts": {"blob":"9bacd52446192a7ecf3dfaae901ea443cf9c9ae2","since":2},
|
||||
"scripts/lib/paid-plan.ts": {"blob":"45721ce128a112666a0caed6c13c66637b74ca54","since":2},
|
||||
"scripts/lib/paid-report.ts": {"blob":"3fda96a8b1f7f3d55797934847353f254cc4d054","since":2},
|
||||
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":2},
|
||||
"scripts/lib/shard-engine.ts": {"blob":"3cd0b1aaa84018b00bf9ea0ce48093f3c8492f66","since":2},
|
||||
"scripts/test-paid-shards.ts": {"blob":"8ddee5f11ed7856876135520dd8025ab989e2db9","since":2},
|
||||
"scripts/test-pr-profile.ts": {"blob":"88ecbce2b71c1e388c4dbca3f7013c71013bf8d3","since":2,"nonBehavioral":"test-pr-profile classifies the dispatch-only measure-journal-read workflow as a free-lane path; selection of paid cases and every verdict are unchanged"},
|
||||
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":2},
|
||||
"test/helpers/agent-sdk-runner.ts": {"blob":"210bba92d241a90df578b2c7fc943ac5755d0025","since":2},
|
||||
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":2},
|
||||
"test/helpers/claude-pty-runner.ts": {"blob":"20cc5894532873aa6145fc6617fdeec8cef94a2d","since":2},
|
||||
"test/helpers/codex-session-runner.ts": {"blob":"4eea35931a35a0afb006363402e3d4191e379737","since":2},
|
||||
"test/helpers/e2e-gate.ts": {"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":2},
|
||||
"test/helpers/e2e-helpers.ts": {"blob":"2d4a2a53063cfc1f003db366d2c861ab1e379e6e","since":2},
|
||||
"test/helpers/eval-budgets.ts": {"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":2},
|
||||
"test/helpers/eval-store.ts": {"blob":"609c6d099108ff2696eb014b47aa318a618dd2af","since":2},
|
||||
"test/helpers/hermetic-env.ts": {"blob":"6473f78285384f4420f48cbe5928658919078e0b","since":2},
|
||||
"test/helpers/llm-judge.ts": {"blob":"54e3306b494fb5d7f132ccdaaae5c680465028f9","since":2},
|
||||
"test/helpers/paid-test-set.ts": {"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":2},
|
||||
"test/helpers/pty-screen.ts": {"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":2},
|
||||
"test/helpers/pty/auq.ts": {"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":2},
|
||||
"test/helpers/pty/binary.ts": {"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":2},
|
||||
"test/helpers/pty/boundaries.ts": {"blob":"53eb76e57c0558c00254366d6334a1c833126575","since":2},
|
||||
"test/helpers/pty/classify.ts": {"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":2},
|
||||
"test/helpers/pty/fake-session.ts": {"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":2},
|
||||
"test/helpers/pty/judge.ts": {"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":2},
|
||||
"test/helpers/pty/launch.ts": {"blob":"4ef0bcb7b5e7c35d2aac0cbffd7f8a303440b6fe","since":2},
|
||||
"test/helpers/pty/plan-native.ts": {"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":2},
|
||||
"test/helpers/pty/runners/counting.ts": {"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":2},
|
||||
"test/helpers/pty/runners/floor.ts": {"blob":"465f01641fb3a18ae1c9d2613a9c2b26f8fb503e","since":2},
|
||||
"test/helpers/pty/runners/observation.ts": {"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":2},
|
||||
"test/helpers/pty/screen.ts": {"blob":"ee005f36ffa48617e6832ccbeb35e8e96873df9b","since":2},
|
||||
"test/helpers/pty/session.ts": {"blob":"58533cea9d81cc87bb3b499bdfe0db8e3a8709c5","since":2},
|
||||
"test/helpers/session-drain-policy.ts": {"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":2},
|
||||
"test/helpers/session-ledger.ts": {"blob":"6e3c9e837e46f98addba2d12eaebff802da320bb","since":2},
|
||||
"test/helpers/session-runner.ts": {"blob":"d40eb29e8b8fdc0675b511693e4a90823b5963fc","since":2},
|
||||
"test/helpers/skill-fixture.ts": {"blob":"b188064594eb4201a54a80ba1bc6f28e31d7f49e","since":2},
|
||||
"test/helpers/test-selection.ts": {"blob":"51655f89f66be7a5ec487e2abfe4744300ebd5f6","since":2},
|
||||
"test/helpers/touchfiles.ts": {"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":2}
|
||||
"scripts/lib/paid-cases.ts": {"blob":"9bacd52446192a7ecf3dfaae901ea443cf9c9ae2","since":4},
|
||||
"scripts/lib/paid-plan.ts": {"blob":"45721ce128a112666a0caed6c13c66637b74ca54","since":4},
|
||||
"scripts/lib/paid-report.ts": {"blob":"3fda96a8b1f7f3d55797934847353f254cc4d054","since":4},
|
||||
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":4},
|
||||
"scripts/lib/shard-engine.ts": {"blob":"3cd0b1aaa84018b00bf9ea0ce48093f3c8492f66","since":4},
|
||||
"scripts/test-paid-shards.ts": {"blob":"8ddee5f11ed7856876135520dd8025ab989e2db9","since":4},
|
||||
"scripts/test-pr-profile.ts": {"blob":"54363c3d3aad9743b9e79219da7ec8a0f4ab0832","since":4},
|
||||
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":4},
|
||||
"test/helpers/agent-sdk-runner.ts": {"blob":"210bba92d241a90df578b2c7fc943ac5755d0025","since":4},
|
||||
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":4},
|
||||
"test/helpers/claude-pty-runner.ts": {"blob":"20cc5894532873aa6145fc6617fdeec8cef94a2d","since":4},
|
||||
"test/helpers/codex-session-runner.ts": {"blob":"4eea35931a35a0afb006363402e3d4191e379737","since":4},
|
||||
"test/helpers/e2e-gate.ts": {"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":4},
|
||||
"test/helpers/e2e-helpers.ts": {"blob":"2d4a2a53063cfc1f003db366d2c861ab1e379e6e","since":4},
|
||||
"test/helpers/eval-budgets.ts": {"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":4},
|
||||
"test/helpers/eval-store.ts": {"blob":"609c6d099108ff2696eb014b47aa318a618dd2af","since":4},
|
||||
"test/helpers/hermetic-env.ts": {"blob":"6473f78285384f4420f48cbe5928658919078e0b","since":4},
|
||||
"test/helpers/llm-judge.ts": {"blob":"54e3306b494fb5d7f132ccdaaae5c680465028f9","since":4},
|
||||
"test/helpers/paid-test-set.ts": {"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":4},
|
||||
"test/helpers/pty-screen.ts": {"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":4},
|
||||
"test/helpers/pty/auq.ts": {"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":4},
|
||||
"test/helpers/pty/binary.ts": {"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":4},
|
||||
"test/helpers/pty/boundaries.ts": {"blob":"53eb76e57c0558c00254366d6334a1c833126575","since":4},
|
||||
"test/helpers/pty/classify.ts": {"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":4},
|
||||
"test/helpers/pty/fake-session.ts": {"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":4},
|
||||
"test/helpers/pty/judge.ts": {"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":4},
|
||||
"test/helpers/pty/launch.ts": {"blob":"ca874247a5b6bf6076d3cd34da01af9c8cc292b7","since":4},
|
||||
"test/helpers/pty/plan-native.ts": {"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":4},
|
||||
"test/helpers/pty/runners/counting.ts": {"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":4},
|
||||
"test/helpers/pty/runners/floor.ts": {"blob":"6faa013d55901f59ae7b7d1ef6f3a9ee6c9a3034","since":4},
|
||||
"test/helpers/pty/runners/observation.ts": {"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":4},
|
||||
"test/helpers/pty/screen.ts": {"blob":"ee005f36ffa48617e6832ccbeb35e8e96873df9b","since":4},
|
||||
"test/helpers/pty/session.ts": {"blob":"58533cea9d81cc87bb3b499bdfe0db8e3a8709c5","since":4},
|
||||
"test/helpers/session-drain-policy.ts": {"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":4},
|
||||
"test/helpers/session-ledger.ts": {"blob":"6e3c9e837e46f98addba2d12eaebff802da320bb","since":4},
|
||||
"test/helpers/session-runner.ts": {"blob":"d40eb29e8b8fdc0675b511693e4a90823b5963fc","since":4},
|
||||
"test/helpers/skill-fixture.ts": {"blob":"b188064594eb4201a54a80ba1bc6f28e31d7f49e","since":4},
|
||||
"test/helpers/test-selection.ts": {"blob":"51655f89f66be7a5ec487e2abfe4744300ebd5f6","since":4},
|
||||
"test/helpers/touchfiles.ts": {"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":4}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { readFileSync, writeFileSync, appendFileSync } from 'node:fs';
|
||||
import { collectFreshnessSources, type Fetcher } from '../lib/model-policy-freshness-sources';
|
||||
import { assessFreshness, freshnessCatalogIdentity, renderFreshnessReport, type FreshnessObservation } from '../lib/model-policy-freshness';
|
||||
import { freshnessGitHub } from '../lib/model-policy-freshness-github';
|
||||
import { publishFreshness, type FreshnessGitHub } from '../lib/model-policy-freshness-publication';
|
||||
|
||||
export async function runModelPolicyFreshness(options: {
|
||||
env?: NodeJS.ProcessEnv; fetcher?: Fetcher; github?: FreshnessGitHub; now?: () => string; publish?: boolean;
|
||||
}) {
|
||||
const env = options.env ?? process.env;
|
||||
const now = options.now ?? (() => new Date().toISOString());
|
||||
const startedAt = now();
|
||||
const catalog = freshnessCatalogIdentity(readFileSync(new URL('../lib/model-catalog.ts', import.meta.url), 'utf8'));
|
||||
const sources = await collectFreshnessSources(options.fetcher);
|
||||
const observation: FreshnessObservation = {
|
||||
catalog,
|
||||
checkedAt: now(), run: { id: env.GITHUB_RUN_ID ?? '0', attempt: Number(env.GITHUB_RUN_ATTEMPT ?? '1'), startedAt, headSha: env.GITHUB_SHA ?? '0'.repeat(40) }, sources,
|
||||
};
|
||||
let report = assessFreshness(observation);
|
||||
let publication;
|
||||
if (options.publish) {
|
||||
if (!env.GITHUB_REF) throw new Error('publication-ref-missing');
|
||||
if (env.GITHUB_REF === 'refs/heads/main' && (!env.GITHUB_REPOSITORY || !env.GH_TOKEN || !env.GITHUB_RUN_ID || !env.GITHUB_SHA)) throw new Error('github-publication-prerequisites-missing');
|
||||
try {
|
||||
const github = options.github ?? (env.GITHUB_REF === 'refs/heads/main' ? freshnessGitHub(env.GITHUB_REPOSITORY!, env.GH_TOKEN!, options.fetcher) : null);
|
||||
publication = github ? await publishFreshness(observation, github, env.GITHUB_REF) : { action: 'report-only' as const };
|
||||
if ('report' in publication && publication.report) report = publication.report;
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error && /^[a-z0-9: /.-]{1,160}$/i.test(error.message) ? error.message : 'publication-unavailable';
|
||||
publication = { action: 'failed' as const, error: reason };
|
||||
report = { ...report, status: 'unknown/source-unavailable', complete: false, errors: [...report.errors, `publication:${reason}`] };
|
||||
}
|
||||
}
|
||||
return { schemaVersion: 1, observation, report, publication };
|
||||
}
|
||||
|
||||
async function main(): Promise<number> {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.some(arg => !['--publish', '--help'].includes(arg))) { console.error('Usage: bun scripts/model-policy-freshness.ts [--publish]'); return 2; }
|
||||
if (args.includes('--help')) {
|
||||
console.log('Check four official public model recommendation/lifecycle sources without paid calls. Default: report-only. --publish requires main/default-branch identity and GITHUB_TOKEN-backed GH_TOKEN; no catalog or pin writes. JSON goes to stdout. Optional MODEL_FRESHNESS_REPORT writes the same artifact; GITHUB_STEP_SUMMARY receives the human report.');
|
||||
return 0;
|
||||
}
|
||||
const result = await runModelPolicyFreshness({ publish: args.includes('--publish') });
|
||||
const output = `${JSON.stringify(result, null, 2)}\n`;
|
||||
if (process.env.MODEL_FRESHNESS_REPORT) writeFileSync(process.env.MODEL_FRESHNESS_REPORT, output, { mode: 0o600 });
|
||||
if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${renderFreshnessReport(result.report)}\n`);
|
||||
console.log(output.trimEnd());
|
||||
if (result.publication?.action.startsWith('rejected-') || result.publication?.action === 'failed') return 1;
|
||||
return result.report.status === 'unknown/source-unavailable' || result.report.status === 'stale' ? 1 : 0;
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
main().then(code => { process.exitCode = code; }).catch(() => {
|
||||
console.error('Model-policy freshness or report delivery failed. Confirm the tracking issue state; do not infer freshness from this run. Check the workflow logs, official sources, issue permissions/state, and docs/troubleshooting.md#model-policy-freshness.');
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import * as path from 'node:path';
|
||||
import { discoverTemplates, includesSkill } from './discover-skills';
|
||||
import { externalSkillName, extractNameAndDescription } from './external-skill-names';
|
||||
import { getHostConfig } from '../hosts';
|
||||
import { CLAUDE_CODE_RUNTIME_FILES } from '../lib/claude-code-migration';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const value = (flag: string): string => {
|
||||
@@ -155,12 +156,11 @@ for (const name of hostEntries.filter(name => name.startsWith('gstack-') && !sou
|
||||
|
||||
const old = path.join(namespace, 'gstack-claude');
|
||||
const nextSkill = path.join(namespace, 'gstack-claude-code');
|
||||
const renameFiles = ['bin/gstack-claude-code', 'lib/claude-code.ts', 'lib/claude-code-windows-job.ts', 'lib/claude-bin.ts', 'lib/outside-review-result.ts'];
|
||||
const runtimeSkill = path.join(runtime, 'SKILL.md');
|
||||
const runtimeBanner = !fs.existsSync(runtimeSkill) || fs.readFileSync(runtimeSkill, 'utf8').includes('<!-- AUTO-GENERATED from');
|
||||
const rename = owned(old) && (!exists(nextSkill) || owned(nextSkill))
|
||||
&& (!exists(runtime)?.isSymbolicLink() || linkIsOurs(runtime))
|
||||
&& runtimeBanner && renameFiles.every(rel => {
|
||||
&& runtimeBanner && CLAUDE_CODE_RUNTIME_FILES.every(rel => {
|
||||
const parent = path.join(runtime, path.dirname(rel));
|
||||
return exists(path.join(source, rel)) && (!exists(parent)?.isSymbolicLink() || linkIsOurs(parent));
|
||||
});
|
||||
@@ -172,7 +172,7 @@ if (rename) {
|
||||
for (const rel of ['SKILL.md', 'agents/openai.yaml']) checkAtomicCopy(path.join(next, rel), physicalNamespace, 'rename skill write', true);
|
||||
}
|
||||
if (exists(runtime) && physical(runtime) !== source) {
|
||||
for (const rel of renameFiles) {
|
||||
for (const rel of CLAUDE_CODE_RUNTIME_FILES) {
|
||||
const dest = path.join(runtime, rel);
|
||||
if (exists(dest) && physical(dest) === physical(path.join(source, rel))) continue;
|
||||
checkAtomicCopy(dest, physicalNamespace, 'rename runtime write');
|
||||
|
||||
@@ -4,6 +4,8 @@ import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { ALL_MODEL_NAMES, resolveModel, type Model } from './models';
|
||||
import { ModelPolicyError, resolvePlanReviewModel, selectionRepair, type ModelSelection } from '../lib/model-policy';
|
||||
import { emitModelPolicyNotice } from '../lib/model-policy-notice';
|
||||
|
||||
export interface CodexGenerationModelResolution {
|
||||
model: Model;
|
||||
@@ -181,6 +183,45 @@ export function resolveCodexRuntimeModel(opts: {
|
||||
return { kind: opts.kind, model: CODEX_DEFAULT_MODEL, source: `gstack default (no model in ${stripControl(configPath)})` };
|
||||
}
|
||||
|
||||
export interface CodexPlanReviewSelection extends CodexRuntimeModelSelection {
|
||||
selection: ModelSelection;
|
||||
/** Source-aware repair for this record, never a lower-priority setting. */
|
||||
repair: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Role-bearing Codex selection: the plan-review policy ranks request, env,
|
||||
* tier config and catalog. Host mode keeps the legacy native resolver,
|
||||
* labelled as delegated so its origin stays honest.
|
||||
*/
|
||||
export function resolveCodexPlanReviewModel(opts: {
|
||||
kind: CodexInvocationKind;
|
||||
explicit?: string;
|
||||
env?: Record<string, string | undefined>;
|
||||
codexHome?: string;
|
||||
home?: string;
|
||||
/** Invocation context forwarded to the shared role resolver. */
|
||||
cwd?: string;
|
||||
}): CodexPlanReviewSelection {
|
||||
const env = opts.env ?? process.env;
|
||||
const selection = resolvePlanReviewModel({ provider: 'openai', requestedModel: opts.explicit || undefined, env, cwd: opts.cwd });
|
||||
const repair = stripControl(selectionRepair(selection).join('; '));
|
||||
if (selection.status === 'selected') {
|
||||
const model = selection.requestedModel;
|
||||
if (!CODEX_RUNTIME_MODEL_PATTERN.test(model)) {
|
||||
throw new Error(`Invalid Codex model '${sanitize(model)}' from ${sanitize(selection.source.label)}. ${repair}`);
|
||||
}
|
||||
return { kind: opts.kind, model, source: stripControl(selection.source.label), selection, repair };
|
||||
}
|
||||
const native = resolveCodexRuntimeModel({ kind: opts.kind, env, codexHome: opts.codexHome, home: opts.home });
|
||||
return { ...native, source: stripControl(`${selection.source.label}; ${native.source}`), selection, repair: repair || REPAIR };
|
||||
}
|
||||
|
||||
function policyFailure(error: unknown): string {
|
||||
if (!(error instanceof ModelPolicyError)) return (error as Error).message;
|
||||
return [`${error.problem} ${error.cause}`.trim(), ...error.repair.map(step => `Repair: ${step}`), `Docs: ${error.docs}`].join('\n');
|
||||
}
|
||||
|
||||
function readArg(name: string): string | undefined {
|
||||
const exact = process.argv.indexOf(name);
|
||||
if (exact >= 0) return process.argv[exact + 1];
|
||||
@@ -193,7 +234,15 @@ if (import.meta.main) {
|
||||
try {
|
||||
const runtime = readArg('--runtime');
|
||||
if (runtime !== undefined) {
|
||||
if (runtime !== 'exec' && runtime !== 'review') throw new Error('Usage: --runtime exec|review [--explicit <model>]');
|
||||
if (runtime !== 'exec' && runtime !== 'review') throw new Error('Usage: --runtime exec|review [--explicit <model>] [--role plan-review]');
|
||||
const role = readArg('--role');
|
||||
if (role !== undefined) {
|
||||
if (role !== 'plan-review') throw new Error('Usage: --role plan-review');
|
||||
const record = resolveCodexPlanReviewModel({ kind: runtime, explicit: readArg('--explicit'), cwd: readArg('--cwd') });
|
||||
emitModelPolicyNotice(record.selection);
|
||||
process.stdout.write(`${record.model}\t${record.source}\t${record.selection.tier}\t${record.repair}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
const selection = resolveCodexRuntimeModel({ kind: runtime, explicit: readArg('--explicit') });
|
||||
process.stdout.write(`${selection.model}\t${selection.source}\n`);
|
||||
process.exit(0);
|
||||
@@ -209,7 +258,7 @@ if (import.meta.main) {
|
||||
// so a hostile CODEX_HOME cannot smuggle tabs/newlines into the TSV contract.
|
||||
process.stdout.write(`${result.model}\t${stripControl(result.source)}\n`);
|
||||
} catch (error) {
|
||||
process.stderr.write(`${(error as Error).message}\n`);
|
||||
process.stderr.write(`${policyFailure(error)}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -57,9 +57,11 @@ export const OPENAI_LITMUS_CHECKS = [
|
||||
export const CODEX_WEB_SEARCH_FLAG = `-c 'web_search="cached"'`;
|
||||
|
||||
/**
|
||||
* Default model for gstack-owned Codex invocations when nothing else chooses.
|
||||
* Historical no-role Codex default and the pinned eval ruler some tests import.
|
||||
* It does not follow the role catalog in lib/model-catalog.ts: plan-review
|
||||
* calls resolve their model through lib/model-policy.ts at invocation time.
|
||||
*
|
||||
* The runtime model is resolved per invocation kind by
|
||||
* A no-role runtime model is resolved per invocation kind by
|
||||
* `gstack-codex-probe select-model exec|review` (backed by
|
||||
* resolveCodexRuntimeModel in scripts/resolve-codex-generation-model.ts):
|
||||
* explicit request (`--model`), then GSTACK_CODEX_MODEL, then Codex config.toml
|
||||
@@ -90,10 +92,18 @@ export const CODEX_PROBE_PATH = '~/.claude/skills/gstack/bin/gstack-codex-probe'
|
||||
* `_CODEX_SEL` and `_CODEX_SANDBOX_MODE` for CODEX_MODEL_CONFIG_FLAG and
|
||||
* CODEX_SANDBOX_REF. The probe is executed, never sourced, so no shell state
|
||||
* crosses from the helper and the calling shell does not matter.
|
||||
*
|
||||
* With `role`, `role-ready` supplies the plan-review record instead: one
|
||||
* selection, auth, sandbox and model probe. It and the block's later
|
||||
* `run-with-timeout <budgetSecs>` dispatch share one exported deadline, so
|
||||
* readiness time is charged to the review instead of extending it. `optional`
|
||||
* (manual /codex) keeps select-model unless the agent sets `_CODEX_ROLE`.
|
||||
*/
|
||||
export function codexSelect(kind: 'exec' | 'review', probe: string = CODEX_PROBE_PATH): string {
|
||||
export function codexSelect(kind: 'exec' | 'review', probe: string = CODEX_PROBE_PATH, role?: { budgetSecs: number; optional?: boolean }): string {
|
||||
const select = `_CODEX_OUT=$("$_CODEX_PROBE" select-model ${kind}) || exit 1`;
|
||||
const ready = `export _CODEX_DEADLINE=$(($(date +%s)+${role?.budgetSecs})); _CODEX_OUT=$("$_CODEX_PROBE" role-ready ${kind}) || exit $?`;
|
||||
return `_CODEX_PROBE=${probe}
|
||||
_CODEX_OUT=$("$_CODEX_PROBE" select-model ${kind}) || exit 1
|
||||
${!role ? select : role.optional ? `_CODEX_ROLE=''\nif [ -n "$_CODEX_ROLE" ]; then ${ready}\nelse ${select}; fi` : ready}
|
||||
_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p')
|
||||
_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p')`;
|
||||
}
|
||||
@@ -144,7 +154,7 @@ On any error: continue — ${feature} is informational, not a gate.`;
|
||||
* - `codex-only` (diff adversarial): disabled gates only the Codex passes; the
|
||||
* free Claude adversarial subagent still runs.
|
||||
*/
|
||||
export function codexPreflight(opts: { modeVar?: string; disabledBehavior: 'skip-all' | 'codex-only'; nativeReview?: boolean }): string {
|
||||
export function codexPreflight(opts: { modeVar?: string; disabledBehavior: 'skip-all' | 'codex-only'; nativeReview?: boolean; role?: boolean }): string {
|
||||
const m = opts.modeVar ?? '_CODEX_MODE';
|
||||
const disabledLine = opts.disabledBehavior === 'codex-only'
|
||||
? 'Skip the Codex passes only; the Claude adversarial subagent below STILL runs (it is free and fast). Print: "Codex passes skipped (codex_reviews disabled) — running Claude adversarial only."'
|
||||
@@ -168,10 +178,12 @@ elif [ -n "$_gstack_helper_error" ]; then
|
||||
${m}="helper_unavailable"; echo "$_gstack_helper_error"
|
||||
elif ! "$_CODEX_PROBE" check-auth >/dev/null 2>&1; then
|
||||
${m}="not_authed"; "$_CODEX_PROBE" log-event codex_auth_failed 2>/dev/null || true
|
||||
else
|
||||
# Free sandbox check before the paid probe; probe exit 2 = the CLI cannot run.
|
||||
_CODEX_MP=0; _CODEX_PS=""
|
||||
"$_CODEX_PROBE" check-sandbox || _CODEX_MP=3
|
||||
else${opts.role ? '' : `
|
||||
# Free sandbox check before the paid probe; probe exit 2 = the CLI cannot run.`}
|
||||
_CODEX_MP=0${opts.role ? '' : '; _CODEX_PS=""'}
|
||||
"$_CODEX_PROBE" check-sandbox || _CODEX_MP=3${opts.role ? `
|
||||
if [ "$_CODEX_MP" -eq 3 ]; then
|
||||
${m}="sandbox_unavailable"` : `
|
||||
for _CODEX_KIND in exec${opts.nativeReview ? ' review' : ''}; do
|
||||
[ "$_CODEX_MP" -eq 0 ] || break
|
||||
_CODEX_PO=$("$_CODEX_PROBE" probe-model $_CODEX_KIND); _CODEX_MP=$?; printf '%s\\n' "$_CODEX_PO"
|
||||
@@ -188,7 +200,7 @@ else
|
||||
elif [ "$_CODEX_PS" = inconclusive ]; then
|
||||
${m}="unverified"
|
||||
elif [ "$_CODEX_PS" = rate_limited ]; then
|
||||
${m}="unverified (rate_limited)"
|
||||
${m}="unverified (rate_limited)"`}
|
||||
else
|
||||
${m}="ready"; "$_CODEX_PROBE" check-version || true
|
||||
fi
|
||||
@@ -203,10 +215,11 @@ 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 (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.
|
||||
- **\`model_unusable\`** — ${opts.role ? 'the role invocation rejected policy, auth or model selection. Relay its reason and source-specific Repair/HINT lines, not a lower-priority setting. `AUTH_FAILED` needs `codex login`; never substitute a model.' : "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}${opts.role ? '' : ' The ~10s round trip is cached for 1h.'}
|
||||
- **\`quota_exhausted\`** — Codex usage limit: relay the probe's lines verbatim (reset time, retry); no more Codex calls this run. ${nativeRoute}
|
||||
- **\`sandbox_unavailable\`** — Codex's sandbox cannot start here (containers without user namespaces); the probe printed the reason and fix. No paid call ran; outside coverage is unavailable. ${nativeRoute}
|
||||
- **\`ready\`** or **\`unverified\`** — run the Codex pass below. \`unverified\` means the model check timed out or, with \`(rate_limited)\`, hit a 429; say so, and let the pass's own verdict decide.`;
|
||||
- **\`ready\`** or **\`unverified\`** — run the Codex pass below. \`unverified\` means the model check timed out or, with \`(rate_limited)\`, hit a 429; say so, and let the pass's own verdict decide.${opts.role ? `
|
||||
Plan-review readiness probes the [policy](https://github.com/garrytan/gstack/blob/main/docs/model-policy.md) model. Relay its diagnostics; never infer quota from the review block's exit status.` : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -841,7 +841,7 @@ printf 'DESIGN_BRIEF=%s\\n' "$_DESIGN_BRIEF"
|
||||
Write the product brief to that path; remember its absolute path across fresh Bash calls. Neither voice inherits context: give both the same brief. Include its complete contents in the outside prompt file for Codex, along with the design-direction request below; substitute its shell-quoted absolute path for the literal <prepared-prompt-file> in the invocation. Keep your draft direction out of both prompts; give the native Agent its absolute path (the product brief's path, not the Codex prompt file). Never paste brief text into shell source.` : ''}
|
||||
|
||||
**Check ${outsideVoiceFor(ctx).label} availability:**
|
||||
${outsideVoicePreflight(ctx, { disabledBehavior: 'opt-in', acceptedOnly: isDesignConsultation })}
|
||||
${outsideVoicePreflight(ctx, { disabledBehavior: 'opt-in', acceptedOnly: isDesignConsultation, ...(isPlanDesignReview ? { role: 'plan-review' as const } : {}) })}
|
||||
|
||||
${isDesignConsultation ? 'Non-ready CLI: retain its repair notice and use only the native voice. The invocation deliberately rechecks the harness before spawning; native success never replaces external coverage.' : 'Declined: skip both voices. Non-ready: retain the repair notice, use only the native voice, and record `outside_status: unavailable` even if it succeeds. The invocation rechecks the harness before spawning.'}
|
||||
|
||||
@@ -853,7 +853,7 @@ Prompt (include the actual plan/product/frontend source context, not only file p
|
||||
|
||||
"${codexPrompt}"
|
||||
|
||||
${outsideVoiceInvocation(ctx, { timeoutMs: 300000, reasoningEffort, ...(isDesignConsultation ? { purpose: 'design-direction' as const } : {}) })}
|
||||
${outsideVoiceInvocation(ctx, { timeoutMs: 300000, reasoningEffort, ...(isDesignConsultation ? { purpose: 'design-direction' as const } : {}), ...(isPlanDesignReview ? { role: 'plan-review' as const } : {}) })}
|
||||
|
||||
2. **${outsideVoiceFor(ctx).nativeLabel} design subagent** (Agent tool, ${FOREGROUND_IF_AVAILABLE}; await its result. ${BACKGROUND_RECOVERY}):
|
||||
"${subagentPrompt}"
|
||||
|
||||
@@ -27,6 +27,7 @@ import { generateReviewDashboard, generatePlanFileReviewReport } from './review-
|
||||
import { generatePlanReviewApprovalCheck, generateExitPlanModeGate, generatePlanCompletionAuditShip, generatePlanCompletionGateShip, generatePlanCompletionAuditReview, generatePlanVerificationExec } from './plan-gates';
|
||||
import { generateAntiShortcutClause, generateSpecReviewLoop, generateBenefitsFrom } from './spec-review';
|
||||
import { generateCodexSecondOpinion, generateAdversarialStep, generateCodexPlanReview, generateCodexDocReview } from './outside-voice-steps';
|
||||
import { generateImplementationModelHandoff } from './plan-review';
|
||||
import { generateScopeDrift, generateCrossReviewDedup, generateSharedCodeReuse } from './review-scope';
|
||||
import { generateSlugEval, generateSlugSetup, generateBaseBranchDetect, generateDeployBootstrap, generateQAMethodology, generateCoAuthorTrailer, generateChangelogWorkflow, generateCodexWebSearchFlag, generateCodexModelConfigFlag, generateCodexReviewModelConfigFlag, generateClaudeModelFlag, generateSetupCommand } from './utility';
|
||||
import { generateLearningsSearch, generateLearningsLog } from './learnings';
|
||||
@@ -57,15 +58,15 @@ export const RESOLVERS: Record<string, ResolverFn> = {
|
||||
NATIVE_LABEL: (ctx) => outsideVoiceFor(ctx).nativeLabel,
|
||||
OUTSIDE_PROVIDER: (ctx) => outsideVoiceFor(ctx).id,
|
||||
HOST_ID: (ctx) => ctx.host,
|
||||
OUTSIDE_PREFLIGHT: (ctx, args) => outsideVoicePreflight(ctx, { disabledBehavior: args?.[0] === 'opt-in' ? 'opt-in' : 'codex-only' }),
|
||||
OUTSIDE_INVOCATION: (ctx, args) => outsideVoiceInvocation(ctx, { timeoutMs: args?.[0] === 'spec' ? 120000 : 540000, gate: args?.[0] === 'spec' ? 'spec' : 'review', reasoningEffort: args?.[0] === 'spec' ? 'medium' : 'high' }),
|
||||
OUTSIDE_PREFLIGHT: (ctx, args) => outsideVoicePreflight(ctx, { disabledBehavior: args?.[0] === 'opt-in' ? 'opt-in' : 'codex-only', ...(args?.[0] === 'autoplan' || args?.[1] === 'plan-review' ? { role: 'plan-review' as const } : {}) }),
|
||||
OUTSIDE_INVOCATION: (ctx, args) => outsideVoiceInvocation(ctx, { timeoutMs: args?.[0] === 'spec' ? 120000 : 540000, gate: args?.[0] === 'spec' ? 'spec' : 'review', reasoningEffort: args?.[0] === 'spec' ? 'medium' : 'high', ...(args?.[0] === 'autoplan' || args?.[0] === 'spec' ? { role: 'plan-review' as const } : {}) }),
|
||||
OUTSIDE_PROVENANCE: (ctx, args) => outsideVoiceProvenance(ctx, args?.[0] ?? ctx.skillName),
|
||||
SLUG_EVAL: generateSlugEval,
|
||||
SLUG_SETUP: generateSlugSetup,
|
||||
CODEX_WEB_SEARCH_FLAG: generateCodexWebSearchFlag,
|
||||
CODEX_MODEL_CONFIG_FLAG: generateCodexModelConfigFlag,
|
||||
CODEX_REVIEW_MODEL_CONFIG_FLAG: generateCodexReviewModelConfigFlag,
|
||||
CODEX_SELECT: (_ctx, args) => codexSelect(args?.[0] === 'review' ? 'review' : 'exec'),
|
||||
CODEX_SELECT: (_ctx, args) => codexSelect(args?.[0] === 'review' ? 'review' : 'exec', undefined, args?.[1] ? { budgetSecs: Number(args[1]), optional: true } : undefined),
|
||||
CLAUDE_MODEL_FLAG: generateClaudeModelFlag,
|
||||
REDACT_INVOCATION_BLOCK: generateRedactInvocationBlock,
|
||||
FREE_TEXT_FILE: generateFreeTextFile,
|
||||
@@ -126,6 +127,7 @@ export const RESOLVERS: Record<string, ResolverFn> = {
|
||||
SCOPE_DRIFT: generateScopeDrift,
|
||||
DEPLOY_BOOTSTRAP: generateDeployBootstrap,
|
||||
CODEX_PLAN_REVIEW: generateCodexPlanReview,
|
||||
IMPLEMENTATION_MODEL_HANDOFF: generateImplementationModelHandoff,
|
||||
CODEX_DOC_REVIEW: generateCodexDocReview,
|
||||
PLAN_COMPLETION_AUDIT_SHIP: generatePlanCompletionAuditShip,
|
||||
PLAN_COMPLETION_GATE_SHIP: generatePlanCompletionGateShip,
|
||||
|
||||
@@ -416,7 +416,7 @@ completed output. Use private temporary paths, with no background jobs.` : `Run
|
||||
Finish a failed attempt's termination before fallback; consume only its completed
|
||||
output. No background jobs or shared temporary paths.`}
|
||||
|
||||
${outsideVoiceInvocation(ctx, { timeoutMs: 300000 })}
|
||||
${outsideVoiceInvocation(ctx, { timeoutMs: 300000, role: 'plan-review' })}
|
||||
|
||||
Present the full output verbatim:
|
||||
|
||||
@@ -457,7 +457,7 @@ Immediately before dispatching, check the preflight result again. On
|
||||
do not dispatch. Otherwise, use this fallback for missing/broken CLI, failed
|
||||
authentication/model selection, a failed preflight${needsApprovalReadiness ? ' (including harness mismatch)' : ''}, or a failed outside invocation.
|
||||
The disabled branch never reaches this fallback.
|
||||
${needsApprovalReadiness ? '' : `On \`CODEX_MODE: ${outsideVoiceFor(ctx).id === 'codex' ? 'under_codex' : 'under_current_harness'}\`, report the setup repair and
|
||||
${needsApprovalReadiness ? '' : `On \`CODEX_MODE: under_current_harness\`, report the setup repair and
|
||||
\`outside_status: unavailable\`, run no outside CLI, and use the native subagent below.
|
||||
A native result never supplies outside coverage.`}`}`;
|
||||
}
|
||||
@@ -633,7 +633,7 @@ review. The user turns this off only by asking explicitly
|
||||
|
||||
**Preflight — decide whether and how the outside voice runs:**
|
||||
|
||||
${outsideVoicePreflight(ctx, { disabledBehavior: 'skip-all' })}
|
||||
${outsideVoicePreflight(ctx, { disabledBehavior: 'skip-all', role: 'plan-review' })}
|
||||
|
||||
${codexPlanOutcomeRouting(ctx, ceo, needsApprovalReadiness)}
|
||||
|
||||
|
||||
@@ -44,10 +44,13 @@ if { ${own}; }; then
|
||||
fi`;
|
||||
}
|
||||
|
||||
export function outsideVoicePreflight(ctx: TemplateContext, opts: { disabledBehavior: 'skip-all' | 'codex-only' | 'opt-in'; acceptedOnly?: boolean; nativeReview?: boolean }): string {
|
||||
/** Caller-owned workflow role. Only enumerated plan-review generators pass it; everything else stays no-role. */
|
||||
export type OutsideRole = 'plan-review';
|
||||
|
||||
export function outsideVoicePreflight(ctx: TemplateContext, opts: { disabledBehavior: 'skip-all' | 'codex-only' | 'opt-in'; acceptedOnly?: boolean; nativeReview?: boolean; role?: OutsideRole }): string {
|
||||
const v = outsideVoiceFor(ctx);
|
||||
if (v.id === 'codex' && opts.disabledBehavior !== 'opt-in') {
|
||||
let preflight = outsideVoiceLabels(ctx, codexPreflight({ disabledBehavior: opts.disabledBehavior, nativeReview: opts.nativeReview }))
|
||||
if (v.id === 'codex' && (opts.disabledBehavior === 'codex-only' || (opts.disabledBehavior === 'skip-all' && !opts.role))) {
|
||||
let preflight = outsideVoiceLabels(ctx, codexPreflight({ disabledBehavior: opts.disabledBehavior, nativeReview: opts.nativeReview, role: !!opts.role }))
|
||||
.replace('```bash\n', `\`\`\`bash\n${runtimeRootPrelude(ctx)}\n`);
|
||||
if (['plan-eng-review', 'plan-ceo-review'].includes(ctx.skillName)) {
|
||||
preflight = preflight.replace("follow the workflow's native-review instructions below",
|
||||
@@ -81,7 +84,7 @@ if [ "$_OUTSIDE_CFG" = disabled ]; then
|
||||
${ctx.skillName === 'ship' && opts.disabledBehavior === 'opt-in' ? `Ship attempts this optional design check automatically when frontend review applies.
|
||||
The enabled value above carries that choice. No additional opt-in is needed.
|
||||
Step 11 keeps its separate outside-review switch.
|
||||
\`CODEX_MODE\` reports provider availability, not user consent; here the provider is **${v.label}**.` : `The historical \`CODEX_MODE\` variable describes **${v.label}** availability here.`} Authentication and configured model validity are checked by the actual invocation, without overriding either. Missing/broken CLI: install or repair ${v.label}; authentication failure: run \`${v.id === 'codex' ? 'codex login' : 'claude auth login'}\`. ${opts.disabledBehavior === 'skip-all' ? 'Disabled ends this entire extra review step, including the native fallback; record outside_status: disabled and continue after the section. Disabled is not an unavailable provider and never triggers a replacement reviewer.' : opts.disabledBehavior === 'codex-only' ? 'Disabled skips only the outside CLI; retain the native pass.' : ctx.skillName === 'ship' ? '' : 'Honor this caller’s existing opt-in/skip choice.'} ${opts.disabledBehavior === 'skip-all' ? 'Provider failure is missing outside coverage; follow the caller’s existing fallback only when reviews are enabled.' : opts.disabledBehavior === 'codex-only' ? 'Non-ready means missing outside coverage. Keep the required native pass without duplicating it.' : 'Any non-ready outcome is missing outside coverage; follow the caller’s existing fallback.'} Never substitute another external provider.`;
|
||||
\`CODEX_MODE\` reports provider availability, not user consent; here the provider is **${v.label}**.` : `The historical \`CODEX_MODE\` variable describes **${v.label}** availability here.`} ${opts.role ? 'The invocation checks auth and the [policy](https://github.com/garrytan/gstack/blob/main/docs/model-policy.md) plan-review model.' : 'Authentication and configured model validity are checked by the actual invocation, without overriding either.'} Missing/broken CLI: install or repair ${v.label}; authentication failure: run \`${v.id === 'codex' ? 'codex login' : 'claude auth login'}\`. ${opts.disabledBehavior === 'skip-all' ? 'Disabled ends this entire extra review step, including the native fallback; record outside_status: disabled and continue after the section. Disabled is not an unavailable provider and never triggers a replacement reviewer.' : opts.disabledBehavior === 'codex-only' ? 'Disabled skips only the outside CLI; retain the native pass.' : ctx.skillName === 'ship' ? '' : 'Honor this caller’s existing opt-in/skip choice.'} ${opts.disabledBehavior === 'skip-all' ? 'Provider failure is missing outside coverage; follow the caller’s existing fallback only when reviews are enabled.' : opts.disabledBehavior === 'codex-only' ? 'Non-ready means missing outside coverage. Keep the required native pass without duplicating it.' : 'Any non-ready outcome is missing outside coverage; follow the caller’s existing fallback.'} Never substitute another external provider.`;
|
||||
}
|
||||
|
||||
export interface OutsideCommandOptions {
|
||||
@@ -97,6 +100,8 @@ export interface OutsideCommandOptions {
|
||||
/** Creative proposals retain the recommendation gate with task-specific wording. */
|
||||
purpose?: 'design-direction';
|
||||
nativeAlreadyRequired?: boolean;
|
||||
/** Resolve the plan-review policy once in the invocation shell and bind it through probe and dispatch. */
|
||||
role?: OutsideRole;
|
||||
}
|
||||
|
||||
/** One self-contained shell body. No shell functions/variables survive between blocks. */
|
||||
@@ -108,20 +113,22 @@ export function outsideVoiceCommand(ctx: TemplateContext, opts: OutsideCommandOp
|
||||
const codex = opts.structuredBase
|
||||
? `codex review --base ${sh(opts.structuredBase)} -c "sandbox_mode=\\"${CODEX_SANDBOX_REF}\\"" ${CODEX_REVIEW_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="${opts.reasoningEffort ?? 'high'}"' ${CODEX_WEB_SEARCH_FLAG} < /dev/null`
|
||||
: `codex exec - -C "$_REPO_ROOT" -s "${CODEX_SANDBOX_REF}" ${CODEX_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="${opts.reasoningEffort ?? 'high'}"' ${CODEX_WEB_SEARCH_FLAG} --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT"`;
|
||||
const kind = opts.structuredBase ? 'review' : 'exec';
|
||||
const invocation = v.id === 'codex'
|
||||
? `${codexSelect(opts.structuredBase ? 'review' : 'exec', `"${bin}/gstack-codex-probe"`)}
|
||||
"$_CODEX_PROBE" check-sandbox || exit 1
|
||||
? `${opts.role
|
||||
? codexSelect(kind, `"${bin}/gstack-codex-probe"`, { budgetSecs: Math.ceil(opts.timeoutMs / 1000) })
|
||||
: `${codexSelect(kind, `"${bin}/gstack-codex-probe"`)}
|
||||
"$_CODEX_PROBE" check-sandbox || exit 1`}
|
||||
"$_CODEX_PROBE" show-first-use-notice
|
||||
_OUTSIDE_EXIT=0
|
||||
"$_CODEX_PROBE" run-with-timeout ${Math.ceil(opts.timeoutMs / 1000)} ${codex} >"$_OUTSIDE_TMP/${opts.structuredBase ? 'text' : 'events'}" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$?
|
||||
${opts.structuredBase ? 'cat "$_OUTSIDE_TMP/text"' : 'cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events"'}`
|
||||
: `_OUTSIDE_EXIT=0
|
||||
"${bin}/gstack-claude-code" --cwd "$_REPO_ROOT" --access ${opts.access ?? 'none'} --timeout-ms ${opts.timeoutMs} <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/result.json" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$?
|
||||
: >"$_OUTSIDE_TMP/stderr" || exit 1
|
||||
"${bin}/gstack-claude-code" --cwd "$_REPO_ROOT" --access ${opts.access ?? 'none'} --timeout-ms ${opts.timeoutMs}${opts.role ? ` --role ${opts.role}` : ''} <"$_OUTSIDE_INPUT" >"$_OUTSIDE_TMP/result.json" || _OUTSIDE_EXIT=$?
|
||||
# Preserve session/usage/modelUsage from this JSON; multiple models have no invented primary.
|
||||
cat "$_OUTSIDE_TMP/result.json" || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; }
|
||||
if [ "$_OUTSIDE_EXIT" -eq 0 ]; then
|
||||
bun -e 'const r=await Bun.file(process.argv[1]).json(); if(r.status!=="completed" || typeof r.result!=="string" || !r.result.trim()) process.exit(1); await Bun.write(process.argv[2],r.result)' "$_OUTSIDE_TMP/result.json" "$_OUTSIDE_TMP/text" || _OUTSIDE_EXIT=1
|
||||
fi`;
|
||||
bun -e 'const r=await Bun.file(process.argv[1]).json(); await Bun.write(process.argv[3],typeof r.stderr==="string"?r.stderr:""); if(r.status!=="completed" || typeof r.result!=="string" || !r.result.trim()) process.exit(1); await Bun.write(process.argv[2],r.result)' "$_OUTSIDE_TMP/result.json" "$_OUTSIDE_TMP/text" "$_OUTSIDE_TMP/stderr" || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; }`;
|
||||
return `${outsideVoiceGuard(ctx)}
|
||||
${runtimeRootPrelude(ctx)}
|
||||
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo 'ERROR: not in a git repo' >&2; exit 1; }
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/** Plan-approval handoff: recommend the runtime-resolved implementation model, never switch to it. */
|
||||
import { toShellPath, type TemplateContext } from './types';
|
||||
|
||||
export function generateImplementationModelHandoff(ctx: TemplateContext): string {
|
||||
const provider = ctx.host === 'claude' ? ' --provider anthropic' : ctx.host === 'codex' ? ' --provider openai' : '';
|
||||
return `**Implementation model:** relay model/source from \`"${toShellPath(ctx.paths.binDir)}/gstack-models" resolve --role implementation${provider}\`${provider ? '' : ' (one per provider)'}. gstack cannot change this session. Recommend only; no spawn or config edits unless asked. On error, relay its repair, not a model. [Policy setup](https://github.com/garrytan/gstack/blob/main/docs/model-policy.md).`;
|
||||
}
|
||||
@@ -228,12 +228,12 @@ ${setup ? 'Read `sections/browser-setup.md` in full unless already completed;\n'
|
||||
|
||||
export function generateQAReviewPreflight(ctx: TemplateContext): string {
|
||||
sectionPath(ctx, 'qa', 'exploratory');
|
||||
return `> **STOP.** Before any probe, including plan checks, complete the ordered scope/method Reads below and await them. Templates cannot replace them.
|
||||
return `> **STOP.** Before any probe, including plan checks, complete the ordered scope/method Reads below in earlier responses. Never batch a probe (capture included) with its prerequisite Read; templates cannot replace Reads.
|
||||
${ctx.skillName === 'review' ? 'Step 4 is read-only: defer charters, setup and probes to Step 4.7.\n' : ''}
|
||||
{{QA_RESOURCE:exploratory}}
|
||||
Reading exploratory.md does not complete them: when it returns, Read the scope section and selected surface methods it lists, in order, and await them.
|
||||
After exploratory returns, Read scope then selected methods in order; it is only the entrypoint.
|
||||
|
||||
Resolve QA's \`sections/...\` and \`templates/...\` paths from that installed QA SKILL.md directory, not the caller or product directory.`;
|
||||
QA's \`sections/...\` and \`templates/...\` paths resolve from installed QA SKILL.md, not the caller or product directory.`;
|
||||
}
|
||||
|
||||
export function generateQAReview(ctx: TemplateContext): string {
|
||||
|
||||
@@ -226,6 +226,7 @@ export const FREE_ONLY_PR_FILES = [
|
||||
'.github/workflows/dependency-review.yml',
|
||||
'.github/workflows/free-tests.yml',
|
||||
'.github/workflows/make-pdf-gate.yml',
|
||||
'.github/workflows/model-policy-freshness.yml',
|
||||
'.github/workflows/measure-journal-read.yml',
|
||||
'.github/workflows/native-qualification.yml',
|
||||
'.github/workflows/osv-scanner.yml',
|
||||
|
||||
Reference in new issue
Block a user