@cat-factory/app 0.158.3 → 0.159.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/judge/JudgeResultView.vue +300 -0
- package/app/components/layout/NotificationsInbox.vue +15 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +13 -0
- package/app/components/slack/SlackPanel.vue +1 -0
- package/app/composables/api/judge.ts +28 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/usePipelineErrorToast.ts +9 -0
- package/app/modular/result-views.ts +4 -0
- package/app/stores/judge.ts +83 -0
- package/app/types/execution.ts +7 -0
- package/i18n/locales/de.json +43 -2
- package/i18n/locales/en.json +46 -2
- package/i18n/locales/es.json +43 -2
- package/i18n/locales/fr.json +43 -2
- package/i18n/locales/he.json +43 -2
- package/i18n/locales/it.json +43 -2
- package/i18n/locales/ja.json +43 -2
- package/i18n/locales/pl.json +43 -2
- package/i18n/locales/tr.json +43 -2
- package/i18n/locales/uk.json +43 -2
- package/package.json +2 -2
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Judge window — the dedicated surface for a JUDGE step (the fourth step-taxonomy bucket),
|
|
3
|
+
// opened via the universal result-view host (the same seam the gate window and the test report
|
|
4
|
+
// use). ONE window serves EVERY registered judge: it reads the rubric name, the score, the
|
|
5
|
+
// per-task threshold and the findings straight off `step.judge`, so a deployment that registers
|
|
6
|
+
// a rubric gets a real result window with no frontend code at all.
|
|
7
|
+
//
|
|
8
|
+
// When the verdict parked the run it also owns the decision: proceed anyway, bounce the work
|
|
9
|
+
// back to the producing step for rework (with optional extra guidance), or stop the run.
|
|
10
|
+
import { computed, ref } from 'vue'
|
|
11
|
+
import { agentKindMeta } from '~/utils/catalog'
|
|
12
|
+
import type { JudgeFinding, JudgeStepState } from '~/types/execution'
|
|
13
|
+
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
14
|
+
import CopyButton from '~/components/common/CopyButton.vue'
|
|
15
|
+
|
|
16
|
+
const board = useBoardStore()
|
|
17
|
+
const execution = useExecutionStore()
|
|
18
|
+
const judgeStore = useJudgeStore()
|
|
19
|
+
const { t, n } = useI18n()
|
|
20
|
+
const access = useWorkspaceAccess()
|
|
21
|
+
|
|
22
|
+
// Synchronous window: it reads its state straight off the execution step, so there is nothing
|
|
23
|
+
// to fetch on open. `ResultWindowShell` owns Escape (and focus trap + scroll lock + stacking).
|
|
24
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('judge')
|
|
25
|
+
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
26
|
+
|
|
27
|
+
const instance = computed(() =>
|
|
28
|
+
instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
|
|
29
|
+
)
|
|
30
|
+
const step = computed(() => {
|
|
31
|
+
if (instance.value === null || stepIndex.value === null) return null
|
|
32
|
+
return instance.value.steps[stepIndex.value] ?? null
|
|
33
|
+
})
|
|
34
|
+
const judge = computed<JudgeStepState | null>(() => step.value?.judge ?? null)
|
|
35
|
+
const meta = computed(() => agentKindMeta(step.value?.agentKind ?? 'judge'))
|
|
36
|
+
|
|
37
|
+
const rubricName = computed(() => judge.value?.rubricName ?? meta.value.label)
|
|
38
|
+
const headerTitle = computed(
|
|
39
|
+
() => `${rubricName.value}${block.value ? ` — ${block.value.title}` : ''}`,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const verdict = computed(() => judge.value?.verdict ?? null)
|
|
43
|
+
const score = computed(() => verdict.value?.score ?? null)
|
|
44
|
+
const threshold = computed(() => judge.value?.threshold ?? null)
|
|
45
|
+
const awaiting = computed(() => judge.value?.status === 'awaiting_decision')
|
|
46
|
+
const rounds = computed(() => [...(judge.value?.rounds ?? [])].reverse())
|
|
47
|
+
|
|
48
|
+
// Severity-ordered, worst first — the same order the rework brief hands the producer, so the
|
|
49
|
+
// window and the bounced agent agree on what matters.
|
|
50
|
+
const SEVERITY_RANK: Record<JudgeFinding['severity'], number> = {
|
|
51
|
+
critical: 3,
|
|
52
|
+
high: 2,
|
|
53
|
+
medium: 1,
|
|
54
|
+
low: 0,
|
|
55
|
+
}
|
|
56
|
+
const findings = computed(() =>
|
|
57
|
+
[...(verdict.value?.findings ?? [])].sort(
|
|
58
|
+
(a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity],
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
// Exhaustive maps keyed off the wire unions, so adding a status/severity/disposition upstream
|
|
63
|
+
// fails the typecheck here rather than leaking a raw key at runtime (the tier-2 drift guard).
|
|
64
|
+
const STATUS_META = computed<
|
|
65
|
+
Record<
|
|
66
|
+
NonNullable<JudgeStepState['status']>,
|
|
67
|
+
{ label: string; badge: 'success' | 'warning' | 'error' | 'neutral' }
|
|
68
|
+
>
|
|
69
|
+
>(() => ({
|
|
70
|
+
evaluating: { label: t('judge.status.evaluating'), badge: 'neutral' },
|
|
71
|
+
awaiting_decision: { label: t('judge.status.awaitingDecision'), badge: 'warning' },
|
|
72
|
+
bouncing: { label: t('judge.status.bouncing'), badge: 'warning' },
|
|
73
|
+
passed: { label: t('judge.status.passed'), badge: 'success' },
|
|
74
|
+
failed: { label: t('judge.status.failed'), badge: 'error' },
|
|
75
|
+
skipped: { label: t('judge.status.skipped'), badge: 'neutral' },
|
|
76
|
+
}))
|
|
77
|
+
|
|
78
|
+
const SEVERITY_LABELS = computed<Record<JudgeFinding['severity'], string>>(() => ({
|
|
79
|
+
low: t('judge.severity.low'),
|
|
80
|
+
medium: t('judge.severity.medium'),
|
|
81
|
+
high: t('judge.severity.high'),
|
|
82
|
+
critical: t('judge.severity.critical'),
|
|
83
|
+
}))
|
|
84
|
+
|
|
85
|
+
const SEVERITY_CLASSES: Record<JudgeFinding['severity'], string> = {
|
|
86
|
+
low: 'border-slate-700 text-slate-300',
|
|
87
|
+
medium: 'border-amber-500/40 text-amber-300',
|
|
88
|
+
high: 'border-orange-500/40 text-orange-300',
|
|
89
|
+
critical: 'border-rose-500/40 text-rose-300',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const DISPOSITION_LABELS = computed<
|
|
93
|
+
Record<NonNullable<NonNullable<JudgeStepState['disposition']>>, string>
|
|
94
|
+
>(() => ({
|
|
95
|
+
pass: t('judge.disposition.pass'),
|
|
96
|
+
park: t('judge.disposition.park'),
|
|
97
|
+
bounce: t('judge.disposition.bounce'),
|
|
98
|
+
fail: t('judge.disposition.fail'),
|
|
99
|
+
}))
|
|
100
|
+
|
|
101
|
+
const feedback = ref('')
|
|
102
|
+
const busy = computed(() => judgeStore.resolving)
|
|
103
|
+
const canAct = computed(() => awaiting.value && access.canExecuteRuns.value && !busy.value)
|
|
104
|
+
|
|
105
|
+
async function act(choice: 'proceed' | 'bounce' | 'stop') {
|
|
106
|
+
const id = instanceId.value
|
|
107
|
+
if (!id) return
|
|
108
|
+
await judgeStore.resolve(id, choice, feedback.value.trim() || undefined)
|
|
109
|
+
feedback.value = ''
|
|
110
|
+
}
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<template>
|
|
114
|
+
<ResultWindowShell
|
|
115
|
+
:open="open"
|
|
116
|
+
:icon="meta.icon"
|
|
117
|
+
icon-class="bg-amber-500/15 text-amber-300"
|
|
118
|
+
:title="headerTitle"
|
|
119
|
+
:subtitle="t('judge.subtitle')"
|
|
120
|
+
:step-ref="{ instanceId, stepIndex }"
|
|
121
|
+
width="3xl"
|
|
122
|
+
@close="close"
|
|
123
|
+
>
|
|
124
|
+
<template #header-extras>
|
|
125
|
+
<UBadge
|
|
126
|
+
v-if="judge"
|
|
127
|
+
:color="STATUS_META[judge.status].badge"
|
|
128
|
+
variant="subtle"
|
|
129
|
+
size="sm"
|
|
130
|
+
data-testid="judge-status"
|
|
131
|
+
>
|
|
132
|
+
{{ STATUS_META[judge.status].label }}
|
|
133
|
+
</UBadge>
|
|
134
|
+
</template>
|
|
135
|
+
|
|
136
|
+
<div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
|
|
137
|
+
<div
|
|
138
|
+
v-if="!judge"
|
|
139
|
+
class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
|
|
140
|
+
>
|
|
141
|
+
<UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
|
|
142
|
+
<p class="text-sm">{{ t('judge.empty') }}</p>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<template v-else>
|
|
146
|
+
<!-- The score against the task's threshold — the whole verdict in one line. -->
|
|
147
|
+
<div
|
|
148
|
+
class="flex flex-wrap items-center gap-x-3 gap-y-1 rounded-lg border border-slate-800 bg-slate-950/40 px-3 py-2.5"
|
|
149
|
+
data-testid="judge-score"
|
|
150
|
+
>
|
|
151
|
+
<span class="text-[13px] font-semibold text-slate-100">
|
|
152
|
+
{{ score === null ? t('judge.notScored') : n(score, 'percent') }}
|
|
153
|
+
</span>
|
|
154
|
+
<span v-if="threshold !== null" class="text-[12px] text-slate-400">
|
|
155
|
+
{{ t('judge.threshold', { threshold: n(threshold, 'percent') }) }}
|
|
156
|
+
</span>
|
|
157
|
+
<span v-if="judge.disposition" class="text-[12px] text-slate-400">
|
|
158
|
+
· {{ DISPOSITION_LABELS[judge.disposition] }}
|
|
159
|
+
</span>
|
|
160
|
+
<span v-if="judge.rubricOverridden" class="text-[11px] text-violet-300">
|
|
161
|
+
· {{ t('judge.rubricOverridden') }}
|
|
162
|
+
</span>
|
|
163
|
+
<span v-if="(judge.maxBounces ?? 0) > 0" class="text-[11px] text-slate-500">
|
|
164
|
+
·
|
|
165
|
+
{{ t('judge.reworkRounds', { spent: judge.bounces ?? 0, budget: judge.maxBounces }) }}
|
|
166
|
+
</span>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<!-- Why the judge did nothing, when it did nothing. A skipped judge must never read
|
|
170
|
+
like a clean pass. -->
|
|
171
|
+
<p
|
|
172
|
+
v-if="judge.note"
|
|
173
|
+
class="mt-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2 text-[12px] leading-relaxed text-slate-400"
|
|
174
|
+
>
|
|
175
|
+
{{ judge.note }}
|
|
176
|
+
</p>
|
|
177
|
+
|
|
178
|
+
<div v-if="verdict?.summary" class="relative mt-3">
|
|
179
|
+
<CopyButton :text="verdict.summary" class="absolute end-1 top-1" />
|
|
180
|
+
<p class="whitespace-pre-wrap pe-8 text-[13px] leading-relaxed text-slate-200">
|
|
181
|
+
{{ verdict.summary }}
|
|
182
|
+
</p>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<section v-if="findings.length" class="mt-4">
|
|
186
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
187
|
+
{{ t('judge.findingsHeading') }}
|
|
188
|
+
</h3>
|
|
189
|
+
<ul class="flex flex-col gap-2">
|
|
190
|
+
<li
|
|
191
|
+
v-for="(finding, i) in findings"
|
|
192
|
+
:key="`${finding.title}-${i}`"
|
|
193
|
+
class="rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
|
|
194
|
+
data-testid="judge-finding"
|
|
195
|
+
>
|
|
196
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
197
|
+
<span
|
|
198
|
+
class="rounded border px-1.5 py-0.5 text-[10px] uppercase tracking-wide"
|
|
199
|
+
:class="SEVERITY_CLASSES[finding.severity]"
|
|
200
|
+
>
|
|
201
|
+
{{ SEVERITY_LABELS[finding.severity] }}
|
|
202
|
+
</span>
|
|
203
|
+
<span class="text-[13px] font-medium text-slate-100">{{ finding.title }}</span>
|
|
204
|
+
<code v-if="finding.where" class="text-[11px] text-slate-500">{{
|
|
205
|
+
finding.where
|
|
206
|
+
}}</code>
|
|
207
|
+
</div>
|
|
208
|
+
<p
|
|
209
|
+
v-if="finding.detail"
|
|
210
|
+
class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-400"
|
|
211
|
+
>
|
|
212
|
+
{{ finding.detail }}
|
|
213
|
+
</p>
|
|
214
|
+
</li>
|
|
215
|
+
</ul>
|
|
216
|
+
</section>
|
|
217
|
+
|
|
218
|
+
<!-- The decision. Only shown while the run is actually parked on this verdict. -->
|
|
219
|
+
<section v-if="awaiting" class="mt-5" data-testid="judge-decision">
|
|
220
|
+
<h3 class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
221
|
+
{{ t('judge.decisionHeading') }}
|
|
222
|
+
</h3>
|
|
223
|
+
<p class="mb-2 text-[11px] leading-relaxed text-slate-500">
|
|
224
|
+
{{ t('judge.decisionDescription') }}
|
|
225
|
+
</p>
|
|
226
|
+
<textarea
|
|
227
|
+
v-model="feedback"
|
|
228
|
+
rows="3"
|
|
229
|
+
:disabled="busy"
|
|
230
|
+
:placeholder="t('judge.feedbackPlaceholder')"
|
|
231
|
+
class="w-full resize-y rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/60"
|
|
232
|
+
/>
|
|
233
|
+
<p v-if="judgeStore.error" class="mt-2 text-[12px] text-rose-300">
|
|
234
|
+
{{ judgeStore.error }}
|
|
235
|
+
</p>
|
|
236
|
+
<div class="mt-2 flex flex-wrap justify-end gap-2">
|
|
237
|
+
<UButton
|
|
238
|
+
size="sm"
|
|
239
|
+
color="neutral"
|
|
240
|
+
variant="ghost"
|
|
241
|
+
icon="i-lucide-octagon-x"
|
|
242
|
+
:loading="busy"
|
|
243
|
+
:disabled="!canAct"
|
|
244
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
245
|
+
data-testid="judge-stop"
|
|
246
|
+
@click="act('stop')"
|
|
247
|
+
>
|
|
248
|
+
{{ t('judge.stop') }}
|
|
249
|
+
</UButton>
|
|
250
|
+
<UButton
|
|
251
|
+
size="sm"
|
|
252
|
+
color="warning"
|
|
253
|
+
icon="i-lucide-undo-2"
|
|
254
|
+
:loading="busy"
|
|
255
|
+
:disabled="!canAct"
|
|
256
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
257
|
+
data-testid="judge-bounce"
|
|
258
|
+
@click="act('bounce')"
|
|
259
|
+
>
|
|
260
|
+
{{ t('judge.bounce') }}
|
|
261
|
+
</UButton>
|
|
262
|
+
<UButton
|
|
263
|
+
size="sm"
|
|
264
|
+
color="primary"
|
|
265
|
+
icon="i-lucide-circle-check"
|
|
266
|
+
:loading="busy"
|
|
267
|
+
:disabled="!canAct"
|
|
268
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
269
|
+
data-testid="judge-proceed"
|
|
270
|
+
@click="act('proceed')"
|
|
271
|
+
>
|
|
272
|
+
{{ t('judge.proceed') }}
|
|
273
|
+
</UButton>
|
|
274
|
+
</div>
|
|
275
|
+
</section>
|
|
276
|
+
|
|
277
|
+
<!-- The round history: a looping judge must not be a black box (the gate-attempt rule). -->
|
|
278
|
+
<section v-if="rounds.length > 1" class="mt-5">
|
|
279
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
280
|
+
{{ t('judge.roundsHeading') }}
|
|
281
|
+
</h3>
|
|
282
|
+
<ul class="flex flex-col gap-1.5">
|
|
283
|
+
<li
|
|
284
|
+
v-for="round in rounds"
|
|
285
|
+
:key="round.round"
|
|
286
|
+
class="flex flex-wrap items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-1.5 text-[12px] text-slate-300"
|
|
287
|
+
>
|
|
288
|
+
<span class="text-slate-500">{{ t('judge.round', { round: round.round }) }}</span>
|
|
289
|
+
<span class="font-medium text-slate-100">{{
|
|
290
|
+
n(round.verdict.score, 'percent')
|
|
291
|
+
}}</span>
|
|
292
|
+
<span class="text-slate-400">{{ DISPOSITION_LABELS[round.disposition] }}</span>
|
|
293
|
+
<code v-if="round.model" class="text-[11px] text-slate-500">{{ round.model }}</code>
|
|
294
|
+
</li>
|
|
295
|
+
</ul>
|
|
296
|
+
</section>
|
|
297
|
+
</template>
|
|
298
|
+
</div>
|
|
299
|
+
</ResultWindowShell>
|
|
300
|
+
</template>
|
|
@@ -71,6 +71,7 @@ const META: Record<Notification['type'], { icon: string; color: Accent }> = {
|
|
|
71
71
|
// the title opens the fork-decision window (see `reveal`); "act" just marks it read (the
|
|
72
72
|
// choice is made in that window — pick a fork / enter a custom approach — not here).
|
|
73
73
|
fork_decision_pending: { icon: 'i-lucide-git-fork', color: 'warning' },
|
|
74
|
+
judge_review: { icon: 'i-lucide-scale', color: 'warning' },
|
|
74
75
|
// The PR reviewer surfaced findings to triage. Clicking the title opens the PR-review window
|
|
75
76
|
// (see `reveal`); "act" just marks it read (findings are selected in that window, not here).
|
|
76
77
|
pr_review_ready: { icon: 'i-lucide-clipboard-check', color: 'primary' },
|
|
@@ -107,6 +108,7 @@ const ACTION_KEYS: Record<Notification['type'], string> = {
|
|
|
107
108
|
human_review: 'layout.notifications.action.human_review',
|
|
108
109
|
followup_pending: 'layout.notifications.action.followup_pending',
|
|
109
110
|
fork_decision_pending: 'layout.notifications.action.fork_decision_pending',
|
|
111
|
+
judge_review: 'layout.notifications.action.judge_review',
|
|
110
112
|
pr_review_ready: 'layout.notifications.action.pr_review_ready',
|
|
111
113
|
initiative: 'layout.notifications.action.initiative',
|
|
112
114
|
platform_health: 'layout.notifications.action.platform_health',
|
|
@@ -232,6 +234,7 @@ function reveal(n: Notification) {
|
|
|
232
234
|
else if (n.type === 'human_review') revealHumanReview(n)
|
|
233
235
|
else if (n.type === 'followup_pending') revealFollowUps(n)
|
|
234
236
|
else if (n.type === 'fork_decision_pending') revealForkDecision(n)
|
|
237
|
+
else if (n.type === 'judge_review') revealJudge(n)
|
|
235
238
|
else if (n.type === 'pr_review_ready') revealPrReview(n)
|
|
236
239
|
else if (n.type === 'initiative') ui.openInitiativeTracker(n.blockId)
|
|
237
240
|
else ui.select(n.blockId)
|
|
@@ -267,6 +270,18 @@ function revealForkDecision(n: Notification) {
|
|
|
267
270
|
else if (n.blockId) ui.select(n.blockId)
|
|
268
271
|
}
|
|
269
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Open the judge window for a run parked on a rubric verdict: find the step carrying the parked
|
|
275
|
+
* verdict and open it through the universal step dispatch (a registered judge declares the
|
|
276
|
+
* `judge` result view). Falls back to focusing the block when the run isn't loaded.
|
|
277
|
+
*/
|
|
278
|
+
function revealJudge(n: Notification) {
|
|
279
|
+
const instance = n.executionId ? execution.getInstance(n.executionId) : undefined
|
|
280
|
+
const idx = instance?.steps.findIndex((s) => s.judge?.status === 'awaiting_decision') ?? -1
|
|
281
|
+
if (instance && idx >= 0) ui.openStepDetail(instance.id, idx)
|
|
282
|
+
else if (n.blockId) ui.select(n.blockId)
|
|
283
|
+
}
|
|
284
|
+
|
|
270
285
|
/**
|
|
271
286
|
* Open the PR deep-review window for a run parked awaiting a finding selection.
|
|
272
287
|
* Falls back to focusing the block when the run isn't loaded.
|
|
@@ -297,6 +297,7 @@ const ENV_TEST_CONFLICT_KEYS: Record<Extract<ConflictReason, `env_test_${string}
|
|
|
297
297
|
env_test_infraless: 'errors.conflict.title.env_test_infraless',
|
|
298
298
|
env_test_not_provisionable: 'errors.conflict.title.env_test_not_provisionable',
|
|
299
299
|
env_test_no_vcs: 'errors.conflict.title.env_test_no_vcs',
|
|
300
|
+
env_test_connection_failed: 'errors.conflict.title.env_test_connection_failed',
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
function buildEnvTestError(e: unknown): EnvTestError {
|
|
@@ -317,6 +318,18 @@ function buildEnvTestError(e: unknown): EnvTestError {
|
|
|
317
318
|
configurable: true,
|
|
318
319
|
}
|
|
319
320
|
}
|
|
321
|
+
// The handler resolved but its live connection probe failed. The provider's OWN message is the
|
|
322
|
+
// actionable part ("Kargo project 'X' was not found"), so wrap it in localized prose rather than
|
|
323
|
+
// replacing it with a generic sentence — and offer the same jump, since the fix is in the
|
|
324
|
+
// handler's connection config.
|
|
325
|
+
if (reason === 'env_test_connection_failed' && parsed?.message) {
|
|
326
|
+
return {
|
|
327
|
+
text: t('errors.conflict.description.env_test_connection_failed_detail', {
|
|
328
|
+
detail: parsed.message,
|
|
329
|
+
}),
|
|
330
|
+
configurable: true,
|
|
331
|
+
}
|
|
332
|
+
}
|
|
320
333
|
const key =
|
|
321
334
|
reason && reason in ENV_TEST_CONFLICT_KEYS
|
|
322
335
|
? ENV_TEST_CONFLICT_KEYS[reason as keyof typeof ENV_TEST_CONFLICT_KEYS]
|
|
@@ -68,6 +68,7 @@ const routes = reactive<Record<NotificationType, SlackRoute>>({
|
|
|
68
68
|
human_review: { enabled: false, channel: '' },
|
|
69
69
|
followup_pending: { enabled: false, channel: '' },
|
|
70
70
|
fork_decision_pending: { enabled: false, channel: '' },
|
|
71
|
+
judge_review: { enabled: false, channel: '' },
|
|
71
72
|
pr_review_ready: { enabled: false, channel: '' },
|
|
72
73
|
initiative: { enabled: false, channel: '' },
|
|
73
74
|
platform_health: { enabled: false, channel: '' },
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getJudgeDecisionContract, resolveJudgeContract } from '@cat-factory/contracts'
|
|
2
|
+
import type { ApiContext } from './context'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JUDGE steps (the fourth step-taxonomy bucket): a rubric scores the run's work and, when the
|
|
6
|
+
* verdict misses the task's threshold, the run parks. These endpoints read the verdict and
|
|
7
|
+
* resolve the park — proceed anyway, bounce the work back to the producing step for rework, or
|
|
8
|
+
* stop the run. The read returns null when no step carries judge state.
|
|
9
|
+
*/
|
|
10
|
+
export function judgeApi({ send, ws }: ApiContext) {
|
|
11
|
+
return {
|
|
12
|
+
// The live judge state for a run (null when no step carries one).
|
|
13
|
+
getJudgeState: (workspaceId: string, executionId: string) =>
|
|
14
|
+
send(getJudgeDecisionContract, { pathPrefix: ws(workspaceId), pathParams: { executionId } }),
|
|
15
|
+
|
|
16
|
+
// Resolve a parked verdict: proceed / bounce for rework / stop the run.
|
|
17
|
+
resolveJudge: (
|
|
18
|
+
workspaceId: string,
|
|
19
|
+
executionId: string,
|
|
20
|
+
body: { choice: 'proceed' | 'bounce' | 'stop'; feedback?: string | null },
|
|
21
|
+
) =>
|
|
22
|
+
send(resolveJudgeContract, {
|
|
23
|
+
pathPrefix: ws(workspaceId),
|
|
24
|
+
pathParams: { executionId },
|
|
25
|
+
body,
|
|
26
|
+
}),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -10,6 +10,7 @@ import { documentsApi } from './api/documents'
|
|
|
10
10
|
import { executionApi } from './api/execution'
|
|
11
11
|
import { followUpsApi } from './api/followUps'
|
|
12
12
|
import { forkDecisionApi } from './api/forkDecision'
|
|
13
|
+
import { judgeApi } from './api/judge'
|
|
13
14
|
import { prReviewApi } from './api/prReview'
|
|
14
15
|
import { fragmentsApi } from './api/fragments'
|
|
15
16
|
import { skillsApi } from './api/skills'
|
|
@@ -119,6 +120,7 @@ export function useApi() {
|
|
|
119
120
|
...reviewsApi(ctx),
|
|
120
121
|
...followUpsApi(ctx),
|
|
121
122
|
...forkDecisionApi(ctx),
|
|
123
|
+
...judgeApi(ctx),
|
|
122
124
|
...prReviewApi(ctx),
|
|
123
125
|
...humanTestApi(ctx),
|
|
124
126
|
...visualConfirmApi(ctx),
|
|
@@ -166,6 +166,15 @@ const CONFLICT_INFO: Record<Exclude<ConflictReason, BespokeConflictReason>, Conf
|
|
|
166
166
|
run: (ui) => ui.openGitHub(),
|
|
167
167
|
},
|
|
168
168
|
},
|
|
169
|
+
env_test_connection_failed: {
|
|
170
|
+
titleKey: 'errors.conflict.title.env_test_connection_failed',
|
|
171
|
+
descriptionKey: 'errors.conflict.description.env_test_connection_failed',
|
|
172
|
+
action: {
|
|
173
|
+
labelKey: 'errors.conflict.action.configureInfrastructure',
|
|
174
|
+
icon: 'i-lucide-settings',
|
|
175
|
+
run: (ui) => ui.openProviderConnection('environment'),
|
|
176
|
+
},
|
|
177
|
+
},
|
|
169
178
|
// Opt-in review-debt friction. In the normal task-create flow AddTaskModal intercepts these
|
|
170
179
|
// 409s and opens the friction dialog (which can retry with an acknowledgement), so these entries
|
|
171
180
|
// are the last-resort toast fallback for any OTHER caller — a generic, param-free title +
|
|
@@ -19,6 +19,7 @@ import InitiativeTrackerWindow from '~/components/initiative/InitiativeTrackerWi
|
|
|
19
19
|
import InitiativePlanningWindow from '~/components/initiative/InitiativePlanningWindow.vue'
|
|
20
20
|
import DocInterviewWindow from '~/components/docs/DocInterviewWindow.vue'
|
|
21
21
|
import RalphLoopResultView from '~/components/ralph/RalphLoopResultView.vue'
|
|
22
|
+
import JudgeResultView from '~/components/judge/JudgeResultView.vue'
|
|
22
23
|
import type { ResultViewContribution } from './slots'
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -83,6 +84,9 @@ const BUILT_IN_RESULT_VIEWS: Record<ResultViewId, Component> = {
|
|
|
83
84
|
'doc-interview': DocInterviewWindow,
|
|
84
85
|
// The Ralph loop: the retry-until-done iteration history + the validation command + its output.
|
|
85
86
|
'ralph-loop': RalphLoopResultView,
|
|
87
|
+
// Shared by EVERY registered judge (the fourth step-taxonomy bucket): the rubric verdict's
|
|
88
|
+
// score vs the task threshold, its findings, the round history, and the park's decision.
|
|
89
|
+
judge: JudgeResultView,
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
/**
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { JudgeStepState } from '~/types/execution'
|
|
4
|
+
import { useApi } from '~/composables/useApi'
|
|
5
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
6
|
+
import { useExecutionStore } from '~/stores/execution'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The JUDGE action surface (the fourth step-taxonomy bucket). The live verdict lives on the
|
|
10
|
+
* run's judge step (`step.judge`) and is kept fresh by the execution stream, so the window reads
|
|
11
|
+
* it straight off the execution store — this store only wraps the `resolve` action (and a
|
|
12
|
+
* warm-up `load`), tracks the in-flight state so the window can disable its controls, and
|
|
13
|
+
* reflects the returned state back onto the execution store so the UI updates immediately even
|
|
14
|
+
* before the stream echoes the change. Keyed by executionId, mirroring the fork-decision store.
|
|
15
|
+
*/
|
|
16
|
+
export const useJudgeStore = defineStore('judge', () => {
|
|
17
|
+
const api = useApi()
|
|
18
|
+
const workspace = useWorkspaceStore()
|
|
19
|
+
const execution = useExecutionStore()
|
|
20
|
+
|
|
21
|
+
/** True while a resolve call is in flight (drives the buttons' spinner / disabled state). */
|
|
22
|
+
const resolving = ref(false)
|
|
23
|
+
/** The last error message from an action, surfaced inline; cleared on the next action. */
|
|
24
|
+
const error = ref<string | null>(null)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reflect an authoritative judge state onto the run's judge step. A pipeline may place more
|
|
28
|
+
* than one judge, so target the step this verdict is about rather than the first one holding
|
|
29
|
+
* judge state: prefer the step still awaiting a decision, then the current step, and only then
|
|
30
|
+
* fall back to the first step carrying judge state. The stream corrects any mismatch; this
|
|
31
|
+
* keeps the immediate optimistic echo on the right step.
|
|
32
|
+
*/
|
|
33
|
+
function reflect(executionId: string, state: JudgeStepState | null): void {
|
|
34
|
+
if (!state) return
|
|
35
|
+
const instance = execution.getInstance(executionId)
|
|
36
|
+
if (!instance) return
|
|
37
|
+
const current = instance.steps[instance.currentStep]
|
|
38
|
+
const step =
|
|
39
|
+
instance.steps.find((s) => s.judge?.status === 'awaiting_decision') ??
|
|
40
|
+
(current?.judge ? current : undefined) ??
|
|
41
|
+
instance.steps.find((s) => s.judge)
|
|
42
|
+
if (step) step.judge = state
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Warm the live state from the GET (the stream also keeps it fresh). Best-effort. */
|
|
46
|
+
async function load(executionId: string): Promise<void> {
|
|
47
|
+
error.value = null
|
|
48
|
+
try {
|
|
49
|
+
const state = await api.getJudgeState(workspace.requireId(), executionId)
|
|
50
|
+
reflect(executionId, state as JudgeStepState | null)
|
|
51
|
+
} catch (e) {
|
|
52
|
+
error.value = e instanceof Error ? e.message : 'Failed to load'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the parked verdict: `proceed` advances the run despite the score, `bounce` sends the
|
|
58
|
+
* work back to the producing step with the findings (plus any extra guidance) as rework, and
|
|
59
|
+
* `stop` fails the run.
|
|
60
|
+
*/
|
|
61
|
+
async function resolve(
|
|
62
|
+
executionId: string,
|
|
63
|
+
choice: 'proceed' | 'bounce' | 'stop',
|
|
64
|
+
feedback?: string,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
error.value = null
|
|
67
|
+
resolving.value = true
|
|
68
|
+
try {
|
|
69
|
+
const state = await api.resolveJudge(workspace.requireId(), executionId, {
|
|
70
|
+
choice,
|
|
71
|
+
...(feedback ? { feedback } : {}),
|
|
72
|
+
})
|
|
73
|
+
reflect(executionId, state as JudgeStepState)
|
|
74
|
+
} catch (e) {
|
|
75
|
+
error.value = e instanceof Error ? e.message : 'Failed to resolve'
|
|
76
|
+
throw e
|
|
77
|
+
} finally {
|
|
78
|
+
resolving.value = false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { resolving, error, load, resolve }
|
|
83
|
+
})
|
package/app/types/execution.ts
CHANGED
|
@@ -43,6 +43,13 @@ export type {
|
|
|
43
43
|
ForkDecisionStatus,
|
|
44
44
|
ForkChoice,
|
|
45
45
|
ForkDecisionStepState,
|
|
46
|
+
JudgeVerdict,
|
|
47
|
+
JudgeFinding,
|
|
48
|
+
JudgeFindingSeverity,
|
|
49
|
+
JudgeDisposition,
|
|
50
|
+
JudgeStatus,
|
|
51
|
+
JudgeRound,
|
|
52
|
+
JudgeStepState,
|
|
46
53
|
PrReviewStepState,
|
|
47
54
|
PrReviewFinding,
|
|
48
55
|
PrReviewFindingChallenge,
|
package/i18n/locales/de.json
CHANGED
|
@@ -1831,6 +1831,7 @@
|
|
|
1831
1831
|
"initiative": "Als gelesen markieren",
|
|
1832
1832
|
"markRead": "Als gelesen markieren",
|
|
1833
1833
|
"fork_decision_pending": "Als gelesen markieren",
|
|
1834
|
+
"judge_review": "Als gelesen markieren",
|
|
1834
1835
|
"pr_review_ready": "Als gelesen markieren",
|
|
1835
1836
|
"budget_paused": "Als gelesen markieren",
|
|
1836
1837
|
"key_drift": "Veraltete Zugangsdaten entfernen",
|
|
@@ -4162,7 +4163,8 @@
|
|
|
4162
4163
|
"env_test_not_a_frame": "Kein Dienst",
|
|
4163
4164
|
"env_test_infraless": "Nichts zu testen",
|
|
4164
4165
|
"env_test_not_provisionable": "Umgebungs-Handler nicht konfiguriert",
|
|
4165
|
-
"env_test_no_vcs": "Git-Anbieter nicht verbunden"
|
|
4166
|
+
"env_test_no_vcs": "Git-Anbieter nicht verbunden",
|
|
4167
|
+
"env_test_connection_failed": "Umgebungsverbindung fehlgeschlagen"
|
|
4166
4168
|
},
|
|
4167
4169
|
"description": {
|
|
4168
4170
|
"dependencies_unmet": "Diese Aufgabe hängt von anderen ab, die noch nicht abgeschlossen sind. Schließe sie ab oder gib sie frei und starte dann erneut.",
|
|
@@ -4184,7 +4186,9 @@
|
|
|
4184
4186
|
"env_test_not_provisionable": "Dieser Service hat einen Bereitstellungstyp, aber es ist noch kein Workspace-Handler dafür verfügbar, sodass die Bereitstellung nicht laufen kann. Konfiguriere einen Umgebungs-Handler.",
|
|
4185
4187
|
"env_test_not_provisionable_no_handler": "Für den Bereitstellungstyp dieses Service ist kein Workspace-Umgebungs-Handler konfiguriert. Registriere in Infrastruktur → Testumgebungen einen — für einen benutzerdefinierten Anbieter einen Custom (remote-custom)-Handler, dessen akzeptierte manifest id mit der übereinstimmt, die dieser Service festlegt.",
|
|
4186
4188
|
"env_test_not_provisionable_type_mismatch": "Mehr als ein Umgebungs-Handler könnte auf den Bereitstellungstyp dieses Service passen. Lege für diesen Service eine manifest id fest, sodass genau einer aufgelöst wird, oder entferne den überlappenden Handler in Infrastruktur → Testumgebungen.",
|
|
4187
|
-
"env_test_no_vcs": "Der Selbsttest benötigt einen Git-Anbieter, um seinen Wegwerf-Branch zu erstellen und zu löschen, aber dieser Workspace ist mit keinem verbunden."
|
|
4189
|
+
"env_test_no_vcs": "Der Selbsttest benötigt einen Git-Anbieter, um seinen Wegwerf-Branch zu erstellen und zu löschen, aber dieser Workspace ist mit keinem verbunden.",
|
|
4190
|
+
"env_test_connection_failed": "Der Umgebungs-Handler dieses Dienstes hat seinen Verbindungstest nicht bestanden. Prüfen Sie Endpunkt, Anmeldedaten und Projekteinstellungen und testen Sie die Verbindung erneut.",
|
|
4191
|
+
"env_test_connection_failed_detail": "Der Umgebungs-Handler dieses Dienstes hat seinen Verbindungstest nicht bestanden: {detail}. Prüfen Sie Endpunkt, Anmeldedaten und Projekteinstellungen und testen Sie die Verbindung erneut."
|
|
4188
4192
|
},
|
|
4189
4193
|
"action": {
|
|
4190
4194
|
"connectGitHub": "GitHub verbinden",
|
|
@@ -4373,6 +4377,43 @@
|
|
|
4373
4377
|
"footer": "Ein Gate führt eine programmatische Vorprüfung aus und startet den {helper} nur, wenn sie fehlschlägt; eine grüne Prüfung fährt fort, ohne dass etwas gestartet wird."
|
|
4374
4378
|
}
|
|
4375
4379
|
},
|
|
4380
|
+
"judge": {
|
|
4381
|
+
"subtitle": "Bewertet die Arbeit dieses Laufs anhand eines Bewertungsrasters und fährt fort, fragt nach oder schickt sie zurück",
|
|
4382
|
+
"empty": "Dieser Schritt hat noch kein Urteil erzeugt.",
|
|
4383
|
+
"notScored": "Nicht bewertet",
|
|
4384
|
+
"threshold": "Schwellenwert {threshold}",
|
|
4385
|
+
"rubricOverridden": "Raster des Arbeitsbereichs",
|
|
4386
|
+
"reworkRounds": "Überarbeitung {spent}/{budget}",
|
|
4387
|
+
"findingsHeading": "Was das Bewertungsraster beanstandet hat",
|
|
4388
|
+
"roundsHeading": "Prüfrunden",
|
|
4389
|
+
"round": "Runde {round}",
|
|
4390
|
+
"decisionHeading": "Ihre Entscheidung",
|
|
4391
|
+
"decisionDescription": "Trotzdem fortfahren, die Arbeit zur Überarbeitung zurückschicken oder den Lauf stoppen.",
|
|
4392
|
+
"feedbackPlaceholder": "Optionale Hinweise, die beim Zurückschicken mitgegeben werden…",
|
|
4393
|
+
"proceed": "Trotzdem fortfahren",
|
|
4394
|
+
"bounce": "Zur Überarbeitung zurückschicken",
|
|
4395
|
+
"stop": "Lauf stoppen",
|
|
4396
|
+
"status": {
|
|
4397
|
+
"evaluating": "Wird geprüft",
|
|
4398
|
+
"awaitingDecision": "Entscheidung nötig",
|
|
4399
|
+
"bouncing": "Wird überarbeitet",
|
|
4400
|
+
"passed": "Bestanden",
|
|
4401
|
+
"failed": "Fehlgeschlagen",
|
|
4402
|
+
"skipped": "Übersprungen"
|
|
4403
|
+
},
|
|
4404
|
+
"severity": {
|
|
4405
|
+
"low": "Niedrig",
|
|
4406
|
+
"medium": "Mittel",
|
|
4407
|
+
"high": "Hoch",
|
|
4408
|
+
"critical": "Kritisch"
|
|
4409
|
+
},
|
|
4410
|
+
"disposition": {
|
|
4411
|
+
"pass": "Bestanden",
|
|
4412
|
+
"park": "Nachfrage an einen Menschen",
|
|
4413
|
+
"bounce": "Zur Überarbeitung zurückgeschickt",
|
|
4414
|
+
"fail": "Lauf fehlgeschlagen"
|
|
4415
|
+
}
|
|
4416
|
+
},
|
|
4376
4417
|
"consensus": {
|
|
4377
4418
|
"titlePrefix": "Konsens",
|
|
4378
4419
|
"participantCount": "keine Teilnehmer | ein Teilnehmer | {count} Teilnehmer",
|