Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f9c166e
fix(ai): Resolve issue #1892 - Make /use switch the durable PR model …
Aug 14, 2026
4d5856a
feat(ai): Fixed the PR’s `max-lines` lint failure.
Aug 14, 2026
dc6bd57
feat(ai): Updated [test/slashCommandsBlock.test.ts](/tmp/git-processo…
Aug 14, 2026
4718a87
feat(ai): Fixed the full-suite failure by adding the new core resolve…
Aug 14, 2026
a3ab4e9
feat(ai): Implemented both release-blocking race fixes.
Aug 14, 2026
75e7a4d
feat(ai): Fixed the build-check failure without changing runtime beha…
Aug 14, 2026
d0de5b3
Merge remote-tracking branch 'origin/main' into 1892/gpt-5.6-sol-make…
Aug 14, 2026
a9af5ef
feat(ai): Implemented the narrow F1 provenance fix.
Aug 14, 2026
7e7dab0
feat(ai): Implemented F1 and F2 only.
Aug 14, 2026
797b9e1
Merge remote-tracking branch 'origin/main' into 1892/gpt-5.6-sol-make…
Aug 14, 2026
347ac08
feat(ai): Implemented only F3–F6; no commit created.
Aug 14, 2026
a3bbb5c
feat(ai): Implemented only F7–F10.
Aug 14, 2026
1cc7b7c
feat(ai): Implemented the exact rollback fix:
Aug 14, 2026
bea5c83
feat(ai): Implemented only F11 and F12.
Aug 14, 2026
a98b3c5
feat(ai): Implemented F12 narrowly without committing.
Aug 14, 2026
9b153fc
feat(ai): Fixed the full-suite failure in [test/ultrafixJobCurrent.te…
Aug 14, 2026
7e67bd2
feat(ai): Implemented only F13 and F14.
Aug 14, 2026
d50d4cf
feat(ai): Fixed the build-check lint failure in [modelLabelResolution…
Aug 14, 2026
7e0e8ba
feat(ai): Implemented the F13/F14 test-only release gate at exact hea…
Aug 14, 2026
cfff983
feat(ai): Implemented only F15 and F16.
Aug 14, 2026
a88bc90
feat(ai): Implemented only F17–F19.
Aug 14, 2026
a41a41e
feat(ai): Implemented the F17 publication interleaving fix without co…
Aug 14, 2026
fdb39cb
feat(ai): Implemented only F20 and F21.
Aug 14, 2026
5005769
feat(ai): Fixed the sole full-suite failure in [ultrafixLabelTransiti…
Aug 14, 2026
112a3f3
feat(ai): Fixed the two CI lint failures without changing runtime beh…
Aug 14, 2026
b2b2969
Merge remote-tracking branch 'origin/main' into 1892/gpt-5.6-sol-make…
Aug 14, 2026
642e156
feat(ai): Implemented the narrowly scoped durability fix.
Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/docs/features/agents-and-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ The same aliases work in PR comments (the `llm-` prefix is optional; the raw cat

```
/switch claude-opus5 # future follow-ups on this PR use this model
/use codex-gpt56-sol # one follow-up with this model
/use codex-gpt56-sol # switch the PR and run one follow-up with this model
/review claude-opus5 codex-gpt56-sol # independent reviews from two models
```

Expand Down
8 changes: 4 additions & 4 deletions docs/docs/features/pr-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ To **take over an existing PR** for ongoing work (so that natural follow-up comm
| `/fix` | You want to apply a `/review`'s pending suggestions | Yes | [`/fix`](#fix) |
| `/merge` | You want the base branch merged into the PR branch | Maybe, if conflicts need resolution | [`/merge`](#merge) |
| `/switch <model-id>` | You want future PR work to use a different model | No, unless you include follow-up instructions | [`/switch`](#switch) |
| `/use <model-id>` | You want one immediate follow-up run with a temporary model | Yes | [`/use`](#use) |
| `/use <model-id>` | You want to switch models and run an immediate follow-up | Yes | [`/use`](#use) |
| `/ultrafix` | You want an automated review-fix loop | Yes | [`/ultrafix`](#ultrafix) |

## Syntax Rules
Expand Down Expand Up @@ -170,14 +170,14 @@ Without instructions, `/switch` only updates the label and makes no code changes

### `/use`

`/use` runs one immediate follow-up task with a temporary model:
`/use` is the documented switch-and-run command. It first replaces the PR's model label with the selected model's canonical configured label, then queues an immediate follow-up:

```text
/use <model-id>
Please investigate the flaky test failure and update the PR.
```

The PR's model label keeps its current value. Later work returns to the PR's configured model unless you use `/switch` or another `/use`. Like `/switch`, `/use` takes one model argument, and the agent sees only your instructions, without the command syntax.
The label update completes before work is queued, and the selected agent and model are also stored on the job. This keeps batching, restarts, and provider-limit retries on the new provider. If another writer is active, ProPR saves the follow-up for the next run instead of starting a concurrent writer. A delayed provider-limit retry is superseded so the new provider does not wait for the old provider's reset. Like `/switch`, `/use` takes one model argument, and the agent sees only your instructions, without the command syntax.

### Choosing A Model

Expand All @@ -187,7 +187,7 @@ Use routing when:

- A model is better suited to the task
- The current model is stuck
- You want a one-off second opinion
- You want to move the PR to a different provider
- You need to work around provider capacity or rate limits

## Ultrafix And Branch Updates
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/operations/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Then check credentials, branch settings, and agent configuration — the usual c
Recovery runs through the PR conversation:

- Add a clearer follow-up comment with stronger instructions.
- `/switch <model-id>` to change the PR's model going forward, or `/use <model-id>` for a one-off task with a different model.
- `/switch <model-id>` to change only the PR's model label, or `/use <model-id>` to change the durable label and immediately retry the follow-up with that model.
- `/review` then `/fix`, or `/ultrafix` for an automated review-fix loop (remove the `ultrafix` PR label to stop it).
- Re-run with a smaller scope — see [Work Splitting](../features/work-splitting.md).
- Undo a bad commit with `propr task revert owner/repo <pr> <sha> <issue>`, which runs a signed system task (authorized via `SYSTEM_TASK_SECRET`) that resets the branch and force-pushes.
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/tutorials/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Use slash commands only for specific actions:
- `/fix` applies unprocessed AI review comments generated by `/review`.
- `/merge` merges the base branch into the PR branch, attempts automatic conflict resolution, and reports back.
- `/switch <model-id>` changes the PR's model label going forward.
- `/use <model-id>` runs one follow-up task with that model without changing the PR's model.
- `/use <model-id>` switches the PR's durable model label and runs one follow-up task with that model.
- `/ultrafix` runs a review-fix loop. Parameters: `goal=<score>`, `max=<cycles>`, `pause=<seconds>`, `model=<model-id>`, for example `/ultrafix goal=9 max=5`. It waits for CI checks and PR inactivity between cycles. The `ultrafix` PR label is the circuit breaker — remove it to stop the loop.

See [PR Slash Commands](../features/pr-commands.md).
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/config/modelAliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,10 @@ export {
getAllCustomLabels,
resolveCustomLabel,
resolveLlmLabel,
resolveCanonicalModelSelection,
resolveCanonicalModelSelectionFromLabels,
resolveReviewModels,
type CanonicalModelSelection,
ReviewModelResolutionError,
type ReviewAssignment,
} from './modelLabelResolution.js';
103 changes: 97 additions & 6 deletions packages/core/src/config/modelLabelResolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AgentRegistry } from '../agents/AgentRegistry.js';
import type { AgentConfig } from '../agents/types.js';
import { toProprOpenCodeModelId } from '../agents/impl/openCodeModelIds.js';
import { shortHash } from '@propr/shared';
import { buildAgentModelLlmLabel, buildDynamicLlmLabel } from '@propr/shared';
import { ALL_MODELS, MODEL_INFO_MAP, type AgentType } from './modelDefinitions.js';
import {
MODEL_ALIASES,
Expand Down Expand Up @@ -30,6 +31,7 @@ async function resolveCustomLabel(label: string): Promise<LlmLabelResolution | n
const lowerLabel = label.toLowerCase();

for (const agent of agents) {
if (!agent.config.enabled) continue;
// Check modelCustomLabels for this agent
if (agent.config.modelCustomLabels) {
for (const [modelId, customLabel] of Object.entries(agent.config.modelCustomLabels)) {
Expand All @@ -46,6 +48,92 @@ async function resolveCustomLabel(label: string): Promise<LlmLabelResolution | n
return null;
}

export interface CanonicalModelSelection extends LlmLabelResolution {
/** The one GitHub label that durably represents this selection. */
githubLabel: string;
}

/**
* Resolve a slash-command model token to an enabled, configured agent/model and
* the exact GitHub label used by that configuration. This deliberately does
* not invent `llm-${modelId}`: catalog labels, per-model custom labels, agent
* aliases, and dynamic labels all have their own canonical spelling.
*/
async function resolveCanonicalModelSelection(requested: string): Promise<CanonicalModelSelection | null> {
const token = requested.trim();
if (!token) return null;

const registry = AgentRegistry.getInstance();
await registry.ensureInitialized();
const agents = registry.getAllAgents();

// parseSlashCommand strips llm-, while callers outside the parser may pass
// a complete configured label. Try both spellings for custom labels.
const customCandidates = token.toLowerCase().startsWith('llm-')
? [token]
: [token, `llm-${token}`];
let customResolution: LlmLabelResolution | null = null;
let matchedCustomLabel: string | null = null;
for (const candidate of customCandidates) {
customResolution = await resolveCustomLabel(candidate);
if (customResolution) {
matchedCustomLabel = candidate;
break;
}
}

const normalizedToken = token.replace(/^llm-/i, '');
const resolution = customResolution ?? await resolveLlmLabel(normalizedToken, true);
const agent = agents.find(candidate =>
candidate.config.enabled
&& candidate.config.alias.toLowerCase() === resolution.agentAlias.toLowerCase()
);
if (!agent) return null;

const configuredModel = agent.config.supportedModels.find(model =>
model.toLowerCase() === resolution.model.toLowerCase()
);
if (!configuredModel) return null;

const configuredCustomLabel = agent.config.modelCustomLabels?.[configuredModel]
?? Object.entries(agent.config.modelCustomLabels ?? {}).find(
([model]) => model.toLowerCase() === configuredModel.toLowerCase()
)?.[1];
const modelInfo = MODEL_INFO_MAP[configuredModel];
const labelAgentAlias = agent.config.alias === 'default'
? agent.config.type
: agent.config.alias;
const githubLabel = configuredCustomLabel
|| matchedCustomLabel
|| (modelInfo && buildAgentModelLlmLabel(agent.config.type, labelAgentAlias, modelInfo))
|| buildDynamicLlmLabel(agent.config.alias, configuredModel);

return { agentAlias: agent.config.alias, model: configuredModel, githubLabel };
}

/** Resolve the canonical configured selection represented by a PR label set. */
async function resolveCanonicalModelSelectionFromLabels(
labels: Array<string | { name: string }>,
modelLabelPattern = '^llm-(.+)$',
): Promise<CanonicalModelSelection | null> {
const pattern = new RegExp(modelLabelPattern);
const customLabels = new Set((await getAllCustomLabels()).map(label => label.toLowerCase()));
const managedLabels: string[] = [];
for (const label of labels) {
const name = typeof label === 'string' ? label : label.name;
if (customLabels.has(name.toLowerCase()) || name.match(pattern)?.[1]) managedLabels.push(name);
}
// Exclusive label convergence adds the target before removing the old
// label. Do not choose either routing while that transition is visible.
if (managedLabels.length !== 1) return null;

const [managedLabel] = managedLabels;
const requested = customLabels.has(managedLabel.toLowerCase())
? managedLabel
: managedLabel.match(pattern)?.[1] ?? managedLabel;
return resolveCanonicalModelSelection(requested);
}

/**
* Gets all custom labels configured across all models in all agents.
*
Expand Down Expand Up @@ -265,11 +353,14 @@ function resolveKnownModelAliasLabel(label: string, agents: { config: AgentConfi
* @param label - The LLM label without the "llm-" prefix (e.g., "gemini-pro", "claude-opus", "opus")
* @returns Object with agentAlias and model
*/
async function resolveLlmLabel(label: string): Promise<LlmLabelResolution> {
async function resolveLlmLabel(label: string, enabledOnly = false): Promise<LlmLabelResolution> {
const registry = AgentRegistry.getInstance();
await registry.ensureInitialized();

const agents = registry.getAllAgents();
const agents = registry.getAllAgents().filter(agent => !enabledOnly || agent.config.enabled);
const defaultAgentAlias = enabledOnly
? agents[0]?.config.alias ?? 'default'
: registry.getDefaultAgent()?.config.alias ?? 'default';

const supportedModelMatch = resolveBySupportedModelId(label, agents);
if (supportedModelMatch) {
Expand All @@ -282,8 +373,7 @@ async function resolveLlmLabel(label: string): Promise<LlmLabelResolution> {
return explicitLabelMatch;
}
if (label.includes('~')) {
const defaultAgent = registry.getDefaultAgent();
return { agentAlias: defaultAgent?.config.alias || 'default', model: label };
return { agentAlias: defaultAgentAlias, model: label };
}

const lowerLabel = label.toLowerCase();
Expand Down Expand Up @@ -323,8 +413,7 @@ async function resolveLlmLabel(label: string): Promise<LlmLabelResolution> {
return knownAliasMatch;
}

const defaultAgent = registry.getDefaultAgent();
return { agentAlias: defaultAgent?.config.alias || 'default', model: label };
return { agentAlias: defaultAgentAlias, model: label };
}

/**
Expand Down Expand Up @@ -455,5 +544,7 @@ export {
getAllCustomLabels,
resolveCustomLabel,
resolveLlmLabel,
resolveCanonicalModelSelection,
resolveCanonicalModelSelectionFromLabels,
resolveReviewModels,
};
8 changes: 7 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export { formatResetTime, addModelSpecificDelay, parseResetTimeFromMessage, calc
export { filterCommentByAuthor, checkCommentTrigger, checkCommentIgnore } from './utils/commentFilters.js';
export { ensureGitRepository } from './utils/git/gitValidation.js';
export { safeRemoveLabel, safeAddLabel, safeUpdateLabels } from './utils/github/labelOperations.js';
export {
getUnprocessedCommentIdentity,
getUnprocessedCommentRevisionIdentity,
dedupeUnprocessedComments,
restorePendingCommentsIdempotently,
} from './utils/pendingComments.js';
export type { LabelContext, UpdateResults } from './utils/github/labelOperations.js';
export { createLogFiles, generateCompletionComment, redactSecrets } from './utils/github/logFiles.js';
export { formatSubscriptionUsage } from './utils/github/formatSubscriptionUsage.js';
Expand Down Expand Up @@ -67,7 +73,7 @@ export type { PlanIssueDefaultSelection } from './config/planIssueDefaultSelecti
export { getPlanIssueDefaultSelection } from './config/planIssueDefaultSelection.js';
export type { PlanIssueSelectionAgent } from './config/planIssueDefaultSelection.js';
export { resolveConfiguredModel } from './config/configuredModel.js';
export { resolveModelAlias, getDefaultModel, getPreferredModelForAgent, getModelShortName, getModelName, MODEL_ALIASES, MODEL_SHORT_NAMES, resolveLlmLabel, getOpenRouterId, getAgentTypeFromModel, resolveCustomLabel, getAllCustomLabels, findMatchingModel, resolveReviewModels, ReviewModelResolutionError, NoDefaultModelConfiguredError } from './config/modelAliases.js';
export { resolveModelAlias, getDefaultModel, getPreferredModelForAgent, getModelShortName, getModelName, MODEL_ALIASES, MODEL_SHORT_NAMES, resolveLlmLabel, resolveCanonicalModelSelection, resolveCanonicalModelSelectionFromLabels, getOpenRouterId, getAgentTypeFromModel, resolveCustomLabel, getAllCustomLabels, findMatchingModel, resolveReviewModels, ReviewModelResolutionError, NoDefaultModelConfiguredError, type CanonicalModelSelection } from './config/modelAliases.js';
export type { LlmLabelResolution, ReviewAssignment } from './config/modelAliases.js';
export { CLAUDE_MODELS, CODEX_MODELS, ANTIGRAVITY_MODELS, OPENCODE_MODELS, VIBE_MODELS, ALL_MODELS, AGENT_MODELS, AGENT_DISPLAY, AGENT_DISPLAY_ORDER, MODEL_INFO_MAP, AGENT_DEFAULTS, typeBadgeColors } from './config/modelDefinitions.js';
export type { AgentType as ModelAgentType, AgentDisplayInfo, ModelInfo } from './config/modelDefinitions.js';
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/queue/taskQueue.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export interface CommentJobData {
repoOwner: string;
repoName: string;
llm?: string | null;
/** Durable explicit routing selected by /use or /switch. */
agentAlias?: string;
modelName?: string;
modelLabel?: string;
/** Marks the reconstructable delayed job created after a provider limit. */
isRetryFromRateLimit?: boolean;
correlationId: string;
title?: string;
subtitle?: string;
Expand All @@ -65,6 +71,10 @@ export interface CommentJobData {
commandCommentId?: number;
/** Creation time of the GitHub comment that established the queued command context. */
commandCommentCreatedAt?: string;
/** Revision time of the GitHub comment that established the queued command context. */
commandCommentUpdatedAt?: string;
/** Timestamp-and-body identity of the revision that established the queued command context. */
commandCommentRevisionIdentity?: string;
/** GitHub resource type of the comment that established the queued command context. */
commandCommentType?: 'review' | 'issue';
/** Ultrafix-specific settings when commandMode is 'ultrafix' */
Expand All @@ -79,6 +89,10 @@ export interface UnprocessedComment {
id: number;
/** GitHub creation time used to order issue and review comments together. */
createdAt?: string;
/** GitHub revision time used to distinguish edited comment deliveries. */
updatedAt?: string;
/** Stable timestamp-and-body identity used across webhook, queue, and retry storage. */
revisionIdentity?: string;
body: string;
body_html?: string; // HTML with signed image URLs (from accept: application/vnd.github.full+json)
author: string;
Expand All @@ -90,6 +104,10 @@ export interface UnprocessedComment {
requestedModels?: string[];
commandInstructions?: string;
llmOverride?: string | null;
/** Explicit routing carried through pending-comment batching. */
agentAlias?: string;
modelName?: string;
modelLabel?: string;
/** Ultrafix-specific settings when commandMode is 'ultrafix' */
ultrafixMeta?: UltrafixCommandMeta;
}
Expand Down
Loading
Loading