mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
feat: add configurable model tiers and advisory freshness
This commit is contained in:
1 parent
db745675bd
commit
5ffc050659
78 files changed
+5106
-325
No files matched your search
@@ -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;
|
||||
/** Target repository; its project settings can route to a custom endpoint. */
|
||||
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_select_model exec|review` (bin/gstack-codex-probe, backed by
|
||||
* resolveCodexRuntimeModel in scripts/resolve-codex-generation-model.ts):
|
||||
* explicit request, then GSTACK_CODEX_MODEL, then Codex config.toml (`model`;
|
||||
|
||||
@@ -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, \`run_in_background: false\`; await its result):
|
||||
"${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,8 +58,8 @@ 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,
|
||||
@@ -125,6 +126,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,9 +44,12 @@ 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') {
|
||||
if (v.id === 'codex' && opts.disabledBehavior !== 'opt-in' && !opts.role) {
|
||||
let preflight = outsideVoiceLabels(ctx, codexPreflight({ disabledBehavior: opts.disabledBehavior, nativeReview: opts.nativeReview }))
|
||||
.replace('```bash\n', `\`\`\`bash\n${runtimeRootPrelude(ctx)}\n`);
|
||||
if (['plan-eng-review', 'plan-ceo-review'].includes(ctx.skillName)) {
|
||||
@@ -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 ? 'Authentication and the plan-review model (policy-selected, printed with its source) are checked by the actual invocation.' : '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,15 +113,19 @@ export function outsideVoiceCommand(ctx: TemplateContext, opts: OutsideCommandOp
|
||||
const codex = opts.structuredBase
|
||||
? `codex review --base ${sh(opts.structuredBase)} -c "sandbox_mode=\\"\${_GSTACK_CODEX_SANDBOX:?}\\"" ${CODEX_REVIEW_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="${opts.reasoningEffort ?? 'high'}"' ${CODEX_WEB_SEARCH_FLAG} < /dev/null`
|
||||
: `codex exec - -C "$_REPO_ROOT" -s "\${_GSTACK_CODEX_SANDBOX:?}" ${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 codexSelection = opts.role
|
||||
? `source "${bin}/gstack-codex-probe" && _gstack_codex_role_ready ${kind} "$_REPO_ROOT" || exit $?`
|
||||
: `source "${bin}/gstack-codex-probe" && _gstack_codex_select_model ${kind} || exit 1
|
||||
_gstack_codex_sandbox_preflight >/dev/null || exit 1`;
|
||||
const invocation = v.id === 'codex'
|
||||
? `source "${bin}/gstack-codex-probe" && _gstack_codex_select_model ${opts.structuredBase ? 'review' : 'exec'} || exit 1
|
||||
_gstack_codex_sandbox_preflight >/dev/null || exit 1
|
||||
? `${codexSelection}
|
||||
_gstack_codex_first_use_notice
|
||||
_OUTSIDE_EXIT=0
|
||||
_gstack_codex_timeout_wrapper ${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=$?
|
||||
"${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" 2>"$_OUTSIDE_TMP/stderr" || _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
|
||||
|
||||
@@ -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 the model and source from \`"${toShellPath(ctx.paths.binDir)}/gstack-models" resolve --role implementation${provider}\`${provider ? '' : ' (one per provider)'}. A recommendation only; gstack cannot change this session. Switch nothing, start or spawn nothing, edit no config unless asked. On error relay its repair, not a model.`;
|
||||
}
|
||||
@@ -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/native-qualification.yml',
|
||||
'.github/workflows/osv-scanner.yml',
|
||||
'.github/workflows/platform-qualification.yml',
|
||||
|
||||
Reference in new issue
Block a user