Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 50 additions & 1 deletion client/src/components/cos/tabs/AgentCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {
GitPullRequest,
Sparkles,
RefreshCw,
Copy
Copy,
Target
} from 'lucide-react';
import * as api from '../../../services/api';
import OutputBlocks from '../OutputBlocks';
Expand Down Expand Up @@ -104,6 +105,52 @@ function TranscriptTruncationNotice({ transcript }) {
);
}

// Goal-fidelity verdict (#5994) — did this run build what the task asked for?
// Distinct from the quality reviewers, which never see the request. Rendered
// whatever the verdict: a `ship` is the evidence the gate ran, and its absence
// is what tells the reader the run was never judged against its objective.
//
// `missing` / `unrequested` are model-authored text derived from an untrusted
// diff, so they render as plain list items — never a link, path, or command.
const GOAL_FIDELITY_TONE = {
ship: { border: 'border-port-success/30', text: 'text-port-success', label: 'Delivers the objective' },
'fix-first': { border: 'border-port-warning/30', text: 'text-port-warning', label: 'Delivers it, with gaps' },
rethink: { border: 'border-port-error/40', text: 'text-port-error', label: 'Does not deliver the objective' }
};

function GoalFidelityPanel({ review }) {
const tone = GOAL_FIDELITY_TONE[review?.verdict];
if (!tone) return null;
return (
<div className={`mt-2 bg-port-bg/50 border rounded p-2.5 ${tone.border}`}>
<div className={`text-[11px] flex items-center gap-1 ${tone.text}`}>
<Target size={10} aria-hidden="true" />
Goal fidelity: {tone.label}
<span className="text-gray-500">
({review.verdict}{review.model ? ` · ${review.model}` : ''}{review.diffTruncated ? ' · partial diff' : ''})
</span>
</div>
{review.missing?.length > 0 && (
<div className="mt-1.5 text-xs text-gray-400">
<span className="text-gray-500">Asked for but missing:</span>
<ul className="list-disc list-inside">
{review.missing.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</div>
)}
{review.unrequested?.length > 0 && (
<div className="mt-1.5 text-xs text-gray-400">
<span className="text-gray-500">Not asked for:</span>
<ul className="list-disc list-inside">
{review.unrequested.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</div>
)}
{review.evidence && <p className="mt-1.5 text-xs text-gray-400">{review.evidence}</p>}
</div>
);
}

export default function AgentCard({ agent, onPause, onKill, onDelete, onResume, onRelaunch, completed, paused = false, liveOutput, durations, onFeedbackChange, remote, peerName }) {
const [expanded, setExpanded] = useState(false);
const [now, setNow] = useState(Date.now());
Expand Down Expand Up @@ -869,6 +916,8 @@ export default function AgentCard({ agent, onPause, onKill, onDelete, onResume,
</div>
)}

{agent.result?.goalFidelity && <GoalFidelityPanel review={agent.result.goalFidelity} />}

{completed && (agent.metadata?.taskSummary || agent.metadata?.malwareScan?.reportUrl) && (
<div className="mt-2 bg-port-bg/50 border border-port-border/50 rounded p-2.5">
<div className="text-[11px] text-gray-500 mb-1 flex items-center gap-1">
Expand Down
43 changes: 43 additions & 0 deletions client/src/components/cos/tabs/AgentCard.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,46 @@ describe('AgentCard missing shell explanation', () => {
expect(screen.queryByText('No shell')).not.toBeInTheDocument();
});
});

// #5994: the goal-fidelity verdict — whether the run built what the task asked
// for, which no quality reviewer can answer because none of them see the request.
describe('AgentCard goal fidelity', () => {
const withReview = (goalFidelity) => ({ ...agent, result: { ...agent.result, goalFidelity } });

it('names the missing and unrequested work behind a rethink verdict', () => {
render(
<MemoryRouter>
<AgentCard agent={withReview({
verdict: 'rethink',
missing: ['the retry backoff'],
unrequested: ['an unrelated logging refactor'],
evidence: 'no tests were run',
model: 'example-model',
})} completed />
</MemoryRouter>
);

expect(screen.getByText(/Does not deliver the objective/)).toBeInTheDocument();
expect(screen.getByText('the retry backoff')).toBeInTheDocument();
expect(screen.getByText('an unrelated logging refactor')).toBeInTheDocument();
expect(screen.getByText('no tests were run')).toBeInTheDocument();
});

it('shows a clean ship verdict, and renders nothing at all for a run the gate never judged', () => {
const { unmount } = render(
<MemoryRouter>
<AgentCard agent={withReview({ verdict: 'ship', missing: [], unrequested: [], evidence: '' })} completed />
</MemoryRouter>
);
expect(screen.getByText(/Delivers the objective/)).toBeInTheDocument();
unmount();

render(
<MemoryRouter>
<AgentCard agent={agent} completed />
</MemoryRouter>
);
expect(screen.queryByText(/Goal fidelity/)).not.toBeInTheDocument();
});
});

26 changes: 26 additions & 0 deletions client/src/components/settings/CodeReviewersTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import toast from '../ui/Toast';
import Banner from '../ui/Banner';
import * as api from '../../services/api';
import ReviewerPicker from '../cos/ReviewerPicker';
import GoalFidelityControls from './GoalFidelityControls';
import useReviewerModelOptions from '../../hooks/useReviewerModelOptions';
import { reviewerModelsFromDefaults, reviewerModelsToDefaults, reviewerEffortsFromDefaults, reviewerEffortsToDefaults } from '../../lib/reviewerModels';
import {
Expand Down Expand Up @@ -39,6 +40,7 @@ export default function CodeReviewersTab() {
const [reviewerEfforts, setReviewerEfforts] = useState({});
const [stopMode, setStopMode] = useState(DEFAULT_REVIEW_STOP_MODE);
const [reviewerApplies, setReviewerApplies] = useState(false);
const [goalFidelity, setGoalFidelity] = useState({ enabled: true, backend: null, model: null, effort: null });
const [installed, setInstalled] = useState({});
const modelOptions = useReviewerModelOptions();

Expand All @@ -60,6 +62,14 @@ export default function CodeReviewersTab() {
setReviewerEfforts(reviewerEffortsFromDefaults(defaults));
setStopMode(defaults.stopMode || DEFAULT_REVIEW_STOP_MODE);
setReviewerApplies(defaults.reviewerApplies === true);
// `enabled` defaults ON, so an absent block must read as on — not as a
// stored `false` the next save would then persist.
setGoalFidelity({
enabled: defaults.goalFidelity?.enabled !== false,
backend: defaults.goalFidelity?.backend || null,
model: defaults.goalFidelity?.model || null,
effort: defaults.goalFidelity?.effort || null,
});
setInstalled(defaults.installed && typeof defaults.installed === 'object' && !Array.isArray(defaults.installed) ? defaults.installed : {});
} else {
setLoadError(true);
Expand Down Expand Up @@ -90,6 +100,15 @@ export default function CodeReviewersTab() {
reviewerApplies,
...reviewerModelsToDefaults(reviewerModels),
...reviewerEffortsToDefaults(reviewerEfforts),
// Absent keys are dropped rather than sent as null: the schema treats an
// absent scalar as "inherit", and persisting an explicit null would be a
// pin the resolver can't tell from a deliberate one.
goalFidelity: {
enabled: goalFidelity.enabled,
...(goalFidelity.backend ? { backend: goalFidelity.backend } : {}),
...(goalFidelity.model ? { model: goalFidelity.model } : {}),
...(goalFidelity.effort ? { effort: goalFidelity.effort } : {}),
},
};
const ok = await api.updateSettings({ codeReview: payload }, { silent: true })
.then(() => true)
Expand Down Expand Up @@ -155,6 +174,13 @@ export default function CodeReviewersTab() {
}}
/>

<GoalFidelityControls
value={goalFidelity}
modelOptions={modelOptions}
disabled={saving || loadError}
onChange={setGoalFidelity}
/>

<div className="flex justify-end">
<button
type="button"
Expand Down
47 changes: 47 additions & 0 deletions client/src/components/settings/CodeReviewersTab.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,51 @@ describe('CodeReviewersTab', () => {
expect(api.updateSettings).toHaveBeenCalled();
});
});

// The goal-fidelity gate (#5994) — the second review, which asks whether a
// finished run delivered the objective rather than whether the code is good.
it('round-trips the goal-fidelity gate, and defaults an absent block to on', async () => {
api.getCodeReviewDefaults.mockResolvedValue({
reviewers: ['ollama'],
usernames: [],
optionalReviewers: [],
reviewerMaxRounds: {},
stopMode: 'all',
reviewerApplies: false,
});
api.updateSettings.mockResolvedValue({});

render(<CodeReviewersTab />);
const checkbox = await screen.findByLabelText(/Check finished runs against the task objective/);
// An install that has never saved the block must read as ON — persisting a
// stored `false` here would silently switch off a gate nobody turned off.
expect(checkbox).toBeChecked();

fireEvent.change(screen.getByLabelText('Local model runtime'), { target: { value: 'lmstudio' } });
fireEvent.click(screen.getByRole('button', { name: 'Save defaults' }));

await waitFor(() => expect(api.updateSettings).toHaveBeenCalled());
const [payload] = api.updateSettings.mock.calls[0];
expect(payload.codeReview.goalFidelity).toEqual({ enabled: true, backend: 'lmstudio' });
});

it('sends an explicit off switch, and drops the unset pins rather than persisting empty ones', async () => {
api.getCodeReviewDefaults.mockResolvedValue({
reviewers: ['ollama'],
usernames: [],
optionalReviewers: [],
reviewerMaxRounds: {},
stopMode: 'all',
reviewerApplies: false,
goalFidelity: { enabled: true, backend: null, model: null, effort: null },
});
api.updateSettings.mockResolvedValue({});

render(<CodeReviewersTab />);
fireEvent.click(await screen.findByLabelText(/Check finished runs against the task objective/));
fireEvent.click(screen.getByRole('button', { name: 'Save defaults' }));

await waitFor(() => expect(api.updateSettings).toHaveBeenCalled());
expect(api.updateSettings.mock.calls[0][0].codeReview.goalFidelity).toEqual({ enabled: false });
});
});
109 changes: 109 additions & 0 deletions client/src/components/settings/GoalFidelityControls.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { LOCAL_LLM_EFFORT_LEVELS, LOCAL_LLM_REVIEWERS, reviewerLabel } from '../cos/constants';
import { sanitizeReviewerModelInput } from '../../lib/reviewerPins';

// The goal-fidelity gate's controls (#5994) — the SECOND review, which asks
// whether a finished run's diff delivers the objective the task stated, rather
// than whether it is decent code. It runs server-side at agent completion, so
// its reviewer has to be one PortOS can call itself: the local-LLM backends. The
// CLI reviewers are invoked by the follow-up agent from a prompt and have no
// server-side entry point, which is why they are absent from this picker rather
// than shown disabled.
//
// Every field is optional. Left unset, the gate inherits whichever local-LLM
// reviewer the chain above already runs (and that reviewer's pinned model and
// effort), so the common case is configuring nothing at all.
export default function GoalFidelityControls({ value, modelOptions, disabled = false, onChange }) {
const enabled = value?.enabled !== false;
const backend = value?.backend || '';
const patch = (fields) => onChange({ enabled, backend: value?.backend || null, model: value?.model || null, effort: value?.effort || null, ...fields });

// Only a chosen backend has a catalog to offer. With none chosen the model and
// effort fields are inert on purpose: a pin typed against "whatever the chain
// runs" would follow the chain to a backend that never heard of that id.
const options = backend ? (modelOptions?.optionsByReviewer?.[backend] || []) : [];
const freeText = backend ? modelOptions?.freeText?.[backend] !== false : true;
const fieldsDisabled = disabled || !enabled || !backend;

return (
<div className="border border-port-border/60 rounded-lg p-3 space-y-2.5">
<label className="flex items-start gap-2 cursor-pointer">
<input
type="checkbox"
id="goal-fidelity-enabled"
checked={enabled}
disabled={disabled}
onChange={(e) => patch({ enabled: e.target.checked })}
className="mt-0.5 accent-port-accent"
/>
<span>
<span className="text-sm text-white">Check finished runs against the task objective</span>
<span className="block text-xs text-gray-500">
After a CoS agent run ships, re-read its accumulated diff against what the task actually asked for — what is missing, what was never requested, whether the work was really verified. A <span className="font-mono">rethink</span> verdict records the run as needing attention instead of complete. Runs only when a local model below (or in the chain above) is available, so leaving it on costs nothing until one is.
</span>
</span>
</label>

<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div>
<label htmlFor="goal-fidelity-backend" className="block text-[11px] text-gray-500 mb-1">Local model runtime</label>
<select
id="goal-fidelity-backend"
value={backend}
disabled={disabled || !enabled}
onChange={(e) => patch({ backend: e.target.value || null, model: null, effort: null })}
className="w-full px-2 py-1 text-xs bg-port-bg border border-port-border rounded text-white disabled:opacity-50"
>
<option value="">Same as the reviewer chain</option>
{LOCAL_LLM_REVIEWERS.map((r) => (
<option key={r} value={r}>{reviewerLabel(r)}</option>
))}
</select>
</div>

<div>
<label htmlFor="goal-fidelity-model" className="block text-[11px] text-gray-500 mb-1">Model</label>
{freeText ? (
<input
id="goal-fidelity-model"
type="text"
value={value?.model || ''}
disabled={fieldsDisabled}
placeholder={backend ? 'That runtime’s default' : 'Pick a runtime first'}
onChange={(e) => patch({ model: sanitizeReviewerModelInput(e.target.value) || null })}
className="w-full px-2 py-1 text-xs bg-port-bg border border-port-border rounded text-white disabled:opacity-50 font-mono"
/>
) : (
<select
id="goal-fidelity-model"
value={value?.model || ''}
disabled={fieldsDisabled}
onChange={(e) => patch({ model: e.target.value || null })}
className="w-full px-2 py-1 text-xs bg-port-bg border border-port-border rounded text-white disabled:opacity-50"
>
<option value="">That runtime’s default</option>
{options.map((id) => (
<option key={id} value={id}>{id}</option>
))}
</select>
)}
</div>

<div>
<label htmlFor="goal-fidelity-effort" className="block text-[11px] text-gray-500 mb-1">Reasoning effort</label>
<select
id="goal-fidelity-effort"
value={value?.effort || ''}
disabled={fieldsDisabled}
onChange={(e) => patch({ effort: e.target.value || null })}
className="w-full px-2 py-1 text-xs bg-port-bg border border-port-border rounded text-white disabled:opacity-50"
>
<option value="">Model’s own default</option>
{LOCAL_LLM_EFFORT_LEVELS.map((level) => (
<option key={level} value={level}>{level}</option>
))}
</select>
</div>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion docs/features/product-surfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Submit tasks, manage durable autonomous agents, schedule recurring automations,
| **AI Providers & Model Runner** | `/ai` | Multi-provider configuration supporting CLI agents (Claude Code, Codex, Antigravity, OpenCode), cloud APIs (OpenAI, Anthropic, Gemini, Grok), and local endpoints (Ollama, LM Studio, vLLM, SGLang). | [Claude on Ollama](./claude-ollama.md) |
| **Prompt Manager** | `/prompts` | Reusable prompt template library, variable substitution engine, prompt versioning, and auto-upgrade migrations. | [Prompt Manager](./prompt-manager.md) |
| **Runs & Run Events Ledger** | `/cos/runs`, `/cos/run-events` | Comprehensive ledger of past and in-flight AI runs, lifecycle event replay, and orphaned process recovery. | — |
| **Code Reviewers** | `/settings/code-reviewers` | Configurable multi-reviewer chain (Copilot, Claude, Antigravity, Codex, Grok, Cursor, OpenCode, Kimi, LM Studio, Ollama, MTPLX) with stop conditions, max rounds, per-reviewer model/effort pins, and dispute workflows. | — |
| **Code Reviewers** | `/settings/code-reviewers` | Configurable multi-reviewer chain (Copilot, Claude, Antigravity, Codex, Grok, Cursor, OpenCode, Kimi, LM Studio, Ollama, MTPLX) with stop conditions, max rounds, per-reviewer model/effort pins, dispute workflows, and the goal-fidelity gate that re-reads a finished CoS run's diff against the task's stated objective (`ship` / `fix-first` / `rethink`). | — |

---

Expand Down
Loading