@cat-factory/app 0.146.3 → 0.147.0
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/prReview/PrReviewWindow.vue +212 -13
- package/app/composables/api/prReview.ts +26 -1
- package/app/stores/prReview.ts +46 -1
- package/app/types/execution.ts +1 -0
- package/app/utils/catalog.ts +17 -0
- package/i18n/locales/de.json +16 -0
- package/i18n/locales/en.json +16 -0
- package/i18n/locales/es.json +16 -0
- package/i18n/locales/fr.json +16 -0
- package/i18n/locales/he.json +16 -0
- package/i18n/locales/it.json +16 -0
- package/i18n/locales/ja.json +16 -0
- package/i18n/locales/pl.json +16 -0
- package/i18n/locales/tr.json +16 -0
- package/i18n/locales/uk.json +16 -0
- package/package.json +2 -2
|
@@ -46,6 +46,10 @@ const step = computed(() => {
|
|
|
46
46
|
const state = computed<PrReviewStepState | null>(() => step.value?.prReview ?? null)
|
|
47
47
|
const status = computed(() => state.value?.status ?? null)
|
|
48
48
|
const awaiting = computed(() => status.value === 'awaiting_selection')
|
|
49
|
+
// A finding is being re-examined by the Challenge Investigator (the whole review is `challenging`
|
|
50
|
+
// while it runs). The findings stay visible — the challenged one shows a spinner — but the
|
|
51
|
+
// selection controls + per-finding actions are disabled until the verdict lands and it re-parks.
|
|
52
|
+
const challenging = computed(() => status.value === 'challenging')
|
|
49
53
|
// The reviewer's live todo list while it works, streamed onto the step. Its entries are the
|
|
50
54
|
// cohesive slices/chunks the agent grouped the diff into (plus a final "aggregate" step), so it
|
|
51
55
|
// surfaces slices-reviewed-so-far progress during the `reviewing` phase — richer than a spinner.
|
|
@@ -101,6 +105,27 @@ const groups = computed(() => {
|
|
|
101
105
|
return out
|
|
102
106
|
})
|
|
103
107
|
|
|
108
|
+
/** A finding the Challenge Investigator RETRACTED — it can no longer be acted on (auto-deselected). */
|
|
109
|
+
function isRetracted(f: PrReviewFinding): boolean {
|
|
110
|
+
return f.challenge?.status === 'retracted'
|
|
111
|
+
}
|
|
112
|
+
/** A finding currently being re-examined by the Challenge Investigator. */
|
|
113
|
+
function isInvestigating(f: PrReviewFinding): boolean {
|
|
114
|
+
return f.challenge?.status === 'investigating'
|
|
115
|
+
}
|
|
116
|
+
/** A finding the investigator UPHELD + strengthened after a challenge (its body actually changed). */
|
|
117
|
+
function isAmended(f: PrReviewFinding): boolean {
|
|
118
|
+
return f.challenge?.status === 'amended'
|
|
119
|
+
}
|
|
120
|
+
/** A finding the investigator UPHELD as written after a challenge (kept, no revision). */
|
|
121
|
+
function isUpheld(f: PrReviewFinding): boolean {
|
|
122
|
+
return f.challenge?.status === 'upheld'
|
|
123
|
+
}
|
|
124
|
+
/** A finding whose challenge investigation FAILED — the finding is kept as-is; re-challenge allowed. */
|
|
125
|
+
function isChallengeFailed(f: PrReviewFinding): boolean {
|
|
126
|
+
return f.challenge?.status === 'failed'
|
|
127
|
+
}
|
|
128
|
+
|
|
104
129
|
// The human's selection — a set of finding ids. Defaults to every finding (the human deselects
|
|
105
130
|
// the noise), so "Finish" without touching anything keeps all. Re-seeded ONLY when the set of
|
|
106
131
|
// finding IDS actually changes — NOT on every execution-stream re-emit (which hands us a fresh
|
|
@@ -111,11 +136,17 @@ const selected = ref<Set<string>>(new Set())
|
|
|
111
136
|
watch(
|
|
112
137
|
findingIdKey,
|
|
113
138
|
() => {
|
|
114
|
-
selected.value = new Set(findings.value.map((f) => f.id))
|
|
139
|
+
selected.value = new Set(findings.value.filter((f) => !isRetracted(f)).map((f) => f.id))
|
|
115
140
|
},
|
|
116
141
|
{ immediate: true },
|
|
117
142
|
)
|
|
118
143
|
|
|
144
|
+
// The effective selection: checked AND not retracted. A retracted finding is never acted on even
|
|
145
|
+
// if its box was checked before the investigator dropped it (it's disabled + unchecked visually).
|
|
146
|
+
const activeSelectedIds = computed(() =>
|
|
147
|
+
findings.value.filter((f) => selected.value.has(f.id) && !isRetracted(f)).map((f) => f.id),
|
|
148
|
+
)
|
|
149
|
+
|
|
119
150
|
function toggle(id: string): void {
|
|
120
151
|
const next = new Set(selected.value)
|
|
121
152
|
if (next.has(id)) next.delete(id)
|
|
@@ -123,7 +154,7 @@ function toggle(id: string): void {
|
|
|
123
154
|
selected.value = next
|
|
124
155
|
}
|
|
125
156
|
function selectAll(): void {
|
|
126
|
-
selected.value = new Set(findings.value.map((f) => f.id))
|
|
157
|
+
selected.value = new Set(findings.value.filter((f) => !isRetracted(f)).map((f) => f.id))
|
|
127
158
|
}
|
|
128
159
|
function clearAll(): void {
|
|
129
160
|
selected.value = new Set()
|
|
@@ -132,13 +163,40 @@ function clearAll(): void {
|
|
|
132
163
|
const canResolve = computed(() => awaiting.value && !prReview.resolving)
|
|
133
164
|
// Fix / Post act on the selection, so they need at least one selected finding; Finish always
|
|
134
165
|
// works (it just records the — possibly empty — curated selection and completes the review).
|
|
135
|
-
const hasSelection = computed(() =>
|
|
166
|
+
const hasSelection = computed(() => activeSelectedIds.value.length > 0)
|
|
136
167
|
|
|
137
168
|
async function onResolve(action: PrReviewResolution): Promise<void> {
|
|
138
169
|
const id = instanceId.value
|
|
139
170
|
if (!id || !canResolve.value) return
|
|
140
171
|
if ((action === 'fix' || action === 'post') && !hasSelection.value) return
|
|
141
|
-
await prReview.resolve(id,
|
|
172
|
+
await prReview.resolve(id, activeSelectedIds.value, action).catch(() => {})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Per-finding CHALLENGE: the open finding's id (its inline concern box is showing) + the drafted
|
|
176
|
+
// concern text. Dispatching moves the whole review to `challenging` until the verdict lands.
|
|
177
|
+
const challengeForId = ref<string | null>(null)
|
|
178
|
+
const challengeText = ref('')
|
|
179
|
+
function openChallenge(id: string): void {
|
|
180
|
+
challengeForId.value = id
|
|
181
|
+
challengeText.value = ''
|
|
182
|
+
}
|
|
183
|
+
function cancelChallenge(): void {
|
|
184
|
+
challengeForId.value = null
|
|
185
|
+
challengeText.value = ''
|
|
186
|
+
}
|
|
187
|
+
async function submitChallenge(id: string): Promise<void> {
|
|
188
|
+
const inst = instanceId.value
|
|
189
|
+
if (!inst || !canResolve.value) return
|
|
190
|
+
const question = challengeText.value.trim()
|
|
191
|
+
challengeForId.value = null
|
|
192
|
+
challengeText.value = ''
|
|
193
|
+
await prReview.challenge(inst, id, question || undefined).catch(() => {})
|
|
194
|
+
}
|
|
195
|
+
async function onDismiss(id: string): Promise<void> {
|
|
196
|
+
const inst = instanceId.value
|
|
197
|
+
if (!inst || !canResolve.value) return
|
|
198
|
+
if (challengeForId.value === id) cancelChallenge()
|
|
199
|
+
await prReview.dismiss(inst, id).catch(() => {})
|
|
142
200
|
}
|
|
143
201
|
</script>
|
|
144
202
|
|
|
@@ -335,10 +393,20 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
|
|
|
335
393
|
</div>
|
|
336
394
|
|
|
337
395
|
<template v-else>
|
|
396
|
+
<!-- A challenge is in flight: the Challenge Investigator is re-examining a finding. -->
|
|
397
|
+
<div
|
|
398
|
+
v-if="challenging"
|
|
399
|
+
data-testid="pr-review-challenging"
|
|
400
|
+
class="mb-3 flex items-center gap-2 rounded-md border border-indigo-500/40 bg-indigo-500/10 px-3 py-2 text-[12px] text-indigo-200"
|
|
401
|
+
>
|
|
402
|
+
<UIcon name="i-lucide-loader-circle" class="h-4 w-4 shrink-0 animate-spin" />
|
|
403
|
+
<span>{{ t('prReview.challenge.investigatingBanner') }}</span>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
338
406
|
<!-- Selection toolbar -->
|
|
339
407
|
<div v-if="awaiting" class="mb-2 flex items-center gap-3 text-[11px] text-slate-400">
|
|
340
408
|
<span data-testid="pr-review-selected-count">
|
|
341
|
-
{{ t('prReview.selectedCount', { count:
|
|
409
|
+
{{ t('prReview.selectedCount', { count: activeSelectedIds.length }) }}
|
|
342
410
|
</span>
|
|
343
411
|
<button class="text-indigo-300 hover:underline" @click="selectAll">
|
|
344
412
|
{{ t('prReview.selectAll') }}
|
|
@@ -361,19 +429,21 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
|
|
|
361
429
|
:key="f.id"
|
|
362
430
|
data-testid="pr-review-finding"
|
|
363
431
|
class="mb-1.5 rounded-xl border px-3 py-2 transition"
|
|
364
|
-
:class="
|
|
365
|
-
awaiting && selected.has(f.id)
|
|
432
|
+
:class="[
|
|
433
|
+
awaiting && selected.has(f.id) && !isRetracted(f)
|
|
366
434
|
? 'border-indigo-500/60 bg-indigo-500/5'
|
|
367
|
-
: 'border-slate-800 bg-slate-900/60'
|
|
368
|
-
|
|
435
|
+
: 'border-slate-800 bg-slate-900/60',
|
|
436
|
+
isRetracted(f) ? 'opacity-60' : '',
|
|
437
|
+
]"
|
|
369
438
|
>
|
|
370
439
|
<div class="flex items-start gap-2">
|
|
371
440
|
<input
|
|
372
|
-
v-if="awaiting"
|
|
441
|
+
v-if="awaiting || challenging"
|
|
373
442
|
type="checkbox"
|
|
374
443
|
class="mt-1 accent-indigo-500"
|
|
375
444
|
data-testid="pr-review-finding-toggle"
|
|
376
|
-
:checked="selected.has(f.id)"
|
|
445
|
+
:checked="selected.has(f.id) && !isRetracted(f)"
|
|
446
|
+
:disabled="!awaiting || isRetracted(f)"
|
|
377
447
|
@change="toggle(f.id)"
|
|
378
448
|
/>
|
|
379
449
|
<div class="min-w-0 flex-1">
|
|
@@ -394,7 +464,47 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
|
|
|
394
464
|
>
|
|
395
465
|
{{ t('prReview.postReport.postedBadge') }}
|
|
396
466
|
</span>
|
|
397
|
-
|
|
467
|
+
<!-- Challenge outcome badges -->
|
|
468
|
+
<span
|
|
469
|
+
v-if="isRetracted(f)"
|
|
470
|
+
data-testid="pr-review-finding-retracted"
|
|
471
|
+
class="rounded bg-rose-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-rose-300 ring-1 ring-rose-500/30"
|
|
472
|
+
>
|
|
473
|
+
{{ t('prReview.challenge.retractedBadge') }}
|
|
474
|
+
</span>
|
|
475
|
+
<span
|
|
476
|
+
v-else-if="isAmended(f)"
|
|
477
|
+
data-testid="pr-review-finding-amended"
|
|
478
|
+
class="rounded bg-sky-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-sky-300 ring-1 ring-sky-500/30"
|
|
479
|
+
>
|
|
480
|
+
{{ t('prReview.challenge.strengthenedBadge') }}
|
|
481
|
+
</span>
|
|
482
|
+
<span
|
|
483
|
+
v-else-if="isUpheld(f)"
|
|
484
|
+
data-testid="pr-review-finding-upheld"
|
|
485
|
+
class="rounded bg-emerald-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-emerald-300 ring-1 ring-emerald-500/30"
|
|
486
|
+
>
|
|
487
|
+
{{ t('prReview.challenge.upheldBadge') }}
|
|
488
|
+
</span>
|
|
489
|
+
<span
|
|
490
|
+
v-else-if="isChallengeFailed(f)"
|
|
491
|
+
data-testid="pr-review-finding-challenge-failed"
|
|
492
|
+
class="rounded bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-amber-300 ring-1 ring-amber-500/30"
|
|
493
|
+
>
|
|
494
|
+
{{ t('prReview.challenge.failedBadge') }}
|
|
495
|
+
</span>
|
|
496
|
+
<span
|
|
497
|
+
v-else-if="isInvestigating(f)"
|
|
498
|
+
data-testid="pr-review-finding-investigating"
|
|
499
|
+
class="flex items-center gap-1 rounded bg-indigo-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-indigo-300 ring-1 ring-indigo-500/30"
|
|
500
|
+
>
|
|
501
|
+
<UIcon name="i-lucide-loader-circle" class="h-3 w-3 animate-spin" />
|
|
502
|
+
{{ t('prReview.challenge.investigatingBadge') }}
|
|
503
|
+
</span>
|
|
504
|
+
<h4
|
|
505
|
+
class="min-w-0 flex-1 text-[13px] font-medium text-slate-100"
|
|
506
|
+
:class="isRetracted(f) ? 'line-through' : ''"
|
|
507
|
+
>
|
|
398
508
|
{{ f.title }}
|
|
399
509
|
</h4>
|
|
400
510
|
</div>
|
|
@@ -404,7 +514,10 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
|
|
|
404
514
|
· {{ t('prReview.line', { line: f.line }) }}</template
|
|
405
515
|
>
|
|
406
516
|
</p>
|
|
407
|
-
<p
|
|
517
|
+
<p
|
|
518
|
+
class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300"
|
|
519
|
+
:class="isRetracted(f) ? 'line-through' : ''"
|
|
520
|
+
>
|
|
408
521
|
{{ f.detail }}
|
|
409
522
|
</p>
|
|
410
523
|
<p
|
|
@@ -414,6 +527,92 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
|
|
|
414
527
|
<span class="text-slate-500">{{ t('prReview.suggestedFix') }}</span>
|
|
415
528
|
{{ f.suggestedFix }}
|
|
416
529
|
</p>
|
|
530
|
+
|
|
531
|
+
<!-- The investigator's justification (why the finding holds up / was retracted),
|
|
532
|
+
or the reason the challenge investigation failed. -->
|
|
533
|
+
<p
|
|
534
|
+
v-if="f.challenge?.justification"
|
|
535
|
+
data-testid="pr-review-finding-justification"
|
|
536
|
+
class="mt-1.5 whitespace-pre-wrap rounded-md px-2 py-1 text-[11px]"
|
|
537
|
+
:class="
|
|
538
|
+
isRetracted(f)
|
|
539
|
+
? 'bg-rose-500/10 text-rose-200'
|
|
540
|
+
: isChallengeFailed(f)
|
|
541
|
+
? 'bg-amber-500/10 text-amber-200'
|
|
542
|
+
: 'bg-sky-500/10 text-sky-200'
|
|
543
|
+
"
|
|
544
|
+
>
|
|
545
|
+
<span class="font-medium">{{
|
|
546
|
+
isChallengeFailed(f)
|
|
547
|
+
? t('prReview.challenge.failedLabel')
|
|
548
|
+
: t('prReview.challenge.verdictLabel')
|
|
549
|
+
}}</span>
|
|
550
|
+
{{ f.challenge.justification }}
|
|
551
|
+
</p>
|
|
552
|
+
|
|
553
|
+
<!-- Per-finding actions: Challenge + Dismiss (only while awaiting a selection). -->
|
|
554
|
+
<div
|
|
555
|
+
v-if="awaiting && !isInvestigating(f)"
|
|
556
|
+
class="mt-1.5 flex items-center gap-3 text-[11px]"
|
|
557
|
+
>
|
|
558
|
+
<button
|
|
559
|
+
v-if="!isRetracted(f)"
|
|
560
|
+
data-testid="pr-review-finding-challenge"
|
|
561
|
+
class="flex items-center gap-1 text-indigo-300 hover:underline disabled:opacity-50"
|
|
562
|
+
:disabled="!canResolve || !access.canExecuteRuns.value"
|
|
563
|
+
@click="openChallenge(f.id)"
|
|
564
|
+
>
|
|
565
|
+
<UIcon name="i-lucide-gavel" class="h-3.5 w-3.5" />
|
|
566
|
+
{{
|
|
567
|
+
f.challenge
|
|
568
|
+
? t('prReview.challenge.reChallenge')
|
|
569
|
+
: t('prReview.challenge.action')
|
|
570
|
+
}}
|
|
571
|
+
</button>
|
|
572
|
+
<button
|
|
573
|
+
data-testid="pr-review-finding-dismiss"
|
|
574
|
+
class="flex items-center gap-1 text-slate-400 hover:text-rose-300 hover:underline disabled:opacity-50"
|
|
575
|
+
:disabled="!canResolve || !access.canExecuteRuns.value"
|
|
576
|
+
@click="onDismiss(f.id)"
|
|
577
|
+
>
|
|
578
|
+
<UIcon name="i-lucide-trash-2" class="h-3.5 w-3.5" />
|
|
579
|
+
{{ t('prReview.challenge.dismiss') }}
|
|
580
|
+
</button>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
<!-- The inline challenge box: an OPTIONAL specific concern for the investigator. -->
|
|
584
|
+
<div
|
|
585
|
+
v-if="challengeForId === f.id"
|
|
586
|
+
data-testid="pr-review-challenge-box"
|
|
587
|
+
class="mt-2 rounded-md border border-indigo-500/40 bg-slate-900/80 p-2"
|
|
588
|
+
>
|
|
589
|
+
<textarea
|
|
590
|
+
v-model="challengeText"
|
|
591
|
+
data-testid="pr-review-challenge-input"
|
|
592
|
+
rows="2"
|
|
593
|
+
:placeholder="t('prReview.challenge.placeholder')"
|
|
594
|
+
class="w-full resize-y rounded border border-slate-700 bg-slate-950/60 px-2 py-1 text-[12px] text-slate-200 outline-none focus:border-indigo-500"
|
|
595
|
+
/>
|
|
596
|
+
<p class="mt-1 text-[10px] text-slate-500">
|
|
597
|
+
{{ t('prReview.challenge.hint') }}
|
|
598
|
+
</p>
|
|
599
|
+
<div class="mt-1.5 flex justify-end gap-2">
|
|
600
|
+
<button
|
|
601
|
+
class="rounded px-2 py-1 text-[11px] text-slate-400 hover:text-slate-200"
|
|
602
|
+
@click="cancelChallenge"
|
|
603
|
+
>
|
|
604
|
+
{{ t('common.cancel') }}
|
|
605
|
+
</button>
|
|
606
|
+
<button
|
|
607
|
+
data-testid="pr-review-challenge-submit"
|
|
608
|
+
class="rounded bg-indigo-500/80 px-2 py-1 text-[11px] font-medium text-white hover:bg-indigo-500 disabled:opacity-50"
|
|
609
|
+
:disabled="!canResolve"
|
|
610
|
+
@click="submitChallenge(f.id)"
|
|
611
|
+
>
|
|
612
|
+
{{ t('prReview.challenge.submit') }}
|
|
613
|
+
</button>
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
417
616
|
</div>
|
|
418
617
|
</div>
|
|
419
618
|
</article>
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
challengePrReviewFindingContract,
|
|
3
|
+
dismissPrReviewFindingContract,
|
|
4
|
+
getPrReviewContract,
|
|
5
|
+
resolvePrReviewContract,
|
|
6
|
+
} from '@cat-factory/contracts'
|
|
2
7
|
import type { ApiContext } from './context'
|
|
3
8
|
|
|
4
9
|
/**
|
|
@@ -26,5 +31,25 @@ export function prReviewApi({ send, ws }: ApiContext) {
|
|
|
26
31
|
pathParams: { executionId },
|
|
27
32
|
body,
|
|
28
33
|
}),
|
|
34
|
+
|
|
35
|
+
// Dismiss a parked finding entirely (drops it + prunes it from the selection).
|
|
36
|
+
dismissPrReviewFinding: (workspaceId: string, executionId: string, findingId: string) =>
|
|
37
|
+
send(dismissPrReviewFindingContract, {
|
|
38
|
+
pathPrefix: ws(workspaceId),
|
|
39
|
+
pathParams: { executionId, findingId },
|
|
40
|
+
}),
|
|
41
|
+
|
|
42
|
+
// Challenge a parked finding — dispatch the Challenge Investigator (optional specific concern).
|
|
43
|
+
challengePrReviewFinding: (
|
|
44
|
+
workspaceId: string,
|
|
45
|
+
executionId: string,
|
|
46
|
+
findingId: string,
|
|
47
|
+
body: { question?: string },
|
|
48
|
+
) =>
|
|
49
|
+
send(challengePrReviewFindingContract, {
|
|
50
|
+
pathPrefix: ws(workspaceId),
|
|
51
|
+
pathParams: { executionId, findingId },
|
|
52
|
+
body,
|
|
53
|
+
}),
|
|
29
54
|
}
|
|
30
55
|
}
|
package/app/stores/prReview.ts
CHANGED
|
@@ -82,5 +82,50 @@ export const usePrReviewStore = defineStore('prReview', () => {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
/** Dismiss a finding entirely: it's removed from the review (and the selection). Stays parked. */
|
|
86
|
+
async function dismiss(executionId: string, findingId: string): Promise<void> {
|
|
87
|
+
error.value = null
|
|
88
|
+
resolving.value = true
|
|
89
|
+
try {
|
|
90
|
+
const state = await api.dismissPrReviewFinding(workspace.requireId(), executionId, findingId)
|
|
91
|
+
reflect(executionId, state as PrReviewStepState)
|
|
92
|
+
} catch (e) {
|
|
93
|
+
error.value = e instanceof Error ? e.message : 'Failed to dismiss finding'
|
|
94
|
+
throw e
|
|
95
|
+
} finally {
|
|
96
|
+
resolving.value = false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Challenge a finding: dispatch the Challenge Investigator with an OPTIONAL specific concern
|
|
102
|
+
* (blank ⇒ the generic "dig deeper + validate" prompt). The review moves to `challenging` while
|
|
103
|
+
* the investigator digs in; its verdict (strengthen / retract the finding) arrives via the stream.
|
|
104
|
+
*/
|
|
105
|
+
async function challenge(
|
|
106
|
+
executionId: string,
|
|
107
|
+
findingId: string,
|
|
108
|
+
question?: string,
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
error.value = null
|
|
111
|
+
resolving.value = true
|
|
112
|
+
try {
|
|
113
|
+
const state = await api.challengePrReviewFinding(
|
|
114
|
+
workspace.requireId(),
|
|
115
|
+
executionId,
|
|
116
|
+
findingId,
|
|
117
|
+
{
|
|
118
|
+
question,
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
reflect(executionId, state as PrReviewStepState)
|
|
122
|
+
} catch (e) {
|
|
123
|
+
error.value = e instanceof Error ? e.message : 'Failed to challenge finding'
|
|
124
|
+
throw e
|
|
125
|
+
} finally {
|
|
126
|
+
resolving.value = false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { resolving, error, load, resolve, dismiss, challenge }
|
|
86
131
|
})
|
package/app/types/execution.ts
CHANGED
package/app/utils/catalog.ts
CHANGED
|
@@ -412,6 +412,20 @@ export const SYSTEM_AGENT_META: Record<string, AgentArchetype> = {
|
|
|
412
412
|
color: '#22d3ee',
|
|
413
413
|
description: 'Maps the repository into the service → modules blueprint.',
|
|
414
414
|
},
|
|
415
|
+
// The read-only Challenge Investigator: dispatched off a parked `pr-reviewer` step when a human
|
|
416
|
+
// challenges a finding, it re-examines that ONE finding against the full source and upholds
|
|
417
|
+
// (strengthening) or retracts it. Never a palette block; modelled here purely so it is a
|
|
418
|
+
// configurable per-kind default in the Model Defaults panel — a workspace can point it at a
|
|
419
|
+
// different (stronger) model than the reviewer. Its output renders in the pr-review window.
|
|
420
|
+
'challenge-investigator': {
|
|
421
|
+
kind: 'challenge-investigator',
|
|
422
|
+
label: 'Challenge Investigator',
|
|
423
|
+
icon: 'i-lucide-gavel',
|
|
424
|
+
color: '#6366f1',
|
|
425
|
+
description:
|
|
426
|
+
'Re-examines a single challenged PR-review finding against the full source, then upholds ' +
|
|
427
|
+
'(strengthening it) or retracts it with a justification. Configurable separately from the reviewer.',
|
|
428
|
+
},
|
|
415
429
|
// The single environment provisioner: an operational (non-LLM) step that stands up the ephemeral
|
|
416
430
|
// environment the tester / human-test gate run against for a kubernetes/custom service, and is a
|
|
417
431
|
// fast no-op for docker-compose / infraless. Seeded before the first tester/human-test step in the
|
|
@@ -634,6 +648,9 @@ export const MODEL_CONFIGURABLE_SYSTEM_KINDS: AgentArchetype[] = [
|
|
|
634
648
|
'fixer',
|
|
635
649
|
'merger',
|
|
636
650
|
'kaizen',
|
|
651
|
+
// The PR-review Challenge Investigator — pinnable to its own (stronger) model, separately
|
|
652
|
+
// from the reviewer that produced the findings.
|
|
653
|
+
'challenge-investigator',
|
|
637
654
|
].map((kind) => SYSTEM_AGENT_META[kind]!),
|
|
638
655
|
// Companions run LLMs but aren't palette-addable (they're producer toggles), so include
|
|
639
656
|
// them here to keep their per-workspace default model pinnable in the Model Defaults panel.
|
package/i18n/locales/de.json
CHANGED
|
@@ -4948,6 +4948,22 @@
|
|
|
4948
4948
|
"chunks": "{completed} von {total} Abschnitten geprüft",
|
|
4949
4949
|
"inProgress": "{count} in Bearbeitung"
|
|
4950
4950
|
},
|
|
4951
|
+
"challenge": {
|
|
4952
|
+
"action": "Anfechten",
|
|
4953
|
+
"reChallenge": "Erneut anfechten",
|
|
4954
|
+
"dismiss": "Verwerfen",
|
|
4955
|
+
"submit": "Untersuchen",
|
|
4956
|
+
"placeholder": "Optional: Was genau ist an diesem Befund falsch oder nicht überzeugend?",
|
|
4957
|
+
"hint": "Leer lassen, damit der Prüfer tiefer gräbt, den Befund begründet und prüft, ob er zutreffend und relevant ist.",
|
|
4958
|
+
"investigatingBanner": "Der Challenge-Investigator prüft einen Befund erneut anhand des Quellcodes…",
|
|
4959
|
+
"investigatingBadge": "Wird untersucht",
|
|
4960
|
+
"strengthenedBadge": "Bestärkt",
|
|
4961
|
+
"upheldBadge": "Bestätigt",
|
|
4962
|
+
"retractedBadge": "Zurückgezogen",
|
|
4963
|
+
"verdictLabel": "Prüfer:",
|
|
4964
|
+
"failedBadge": "Anfechtung fehlgeschlagen",
|
|
4965
|
+
"failedLabel": "Anfechtung fehlgeschlagen:"
|
|
4966
|
+
},
|
|
4951
4967
|
"fixing": {
|
|
4952
4968
|
"title": "Ausgewählte Befunde werden behoben…",
|
|
4953
4969
|
"hint": "Ein Fixer committet Änderungen für die ausgewählten Befunde auf den Pull-Request-Branch."
|
package/i18n/locales/en.json
CHANGED
|
@@ -5077,6 +5077,22 @@
|
|
|
5077
5077
|
"chunks": "{completed} of {total} chunks reviewed",
|
|
5078
5078
|
"inProgress": "{count} in progress"
|
|
5079
5079
|
},
|
|
5080
|
+
"challenge": {
|
|
5081
|
+
"action": "Challenge",
|
|
5082
|
+
"reChallenge": "Challenge again",
|
|
5083
|
+
"dismiss": "Dismiss",
|
|
5084
|
+
"submit": "Investigate",
|
|
5085
|
+
"placeholder": "Optional: what specifically is wrong or unconvincing about this finding?",
|
|
5086
|
+
"hint": "Leave blank to have the investigator dig deeper, justify the finding, and check it's accurate and relevant.",
|
|
5087
|
+
"investigatingBanner": "The Challenge Investigator is re-examining a finding against the source…",
|
|
5088
|
+
"investigatingBadge": "Investigating",
|
|
5089
|
+
"strengthenedBadge": "Strengthened",
|
|
5090
|
+
"upheldBadge": "Upheld",
|
|
5091
|
+
"retractedBadge": "Retracted",
|
|
5092
|
+
"verdictLabel": "Investigator:",
|
|
5093
|
+
"failedBadge": "Challenge failed",
|
|
5094
|
+
"failedLabel": "Challenge failed:"
|
|
5095
|
+
},
|
|
5080
5096
|
"fixing": {
|
|
5081
5097
|
"title": "Fixing the selected findings…",
|
|
5082
5098
|
"hint": "A fixer is committing changes for the selected findings onto the pull request branch."
|
package/i18n/locales/es.json
CHANGED
|
@@ -4936,6 +4936,22 @@
|
|
|
4936
4936
|
"chunks": "{completed} de {total} fragmentos revisados",
|
|
4937
4937
|
"inProgress": "{count} en curso"
|
|
4938
4938
|
},
|
|
4939
|
+
"challenge": {
|
|
4940
|
+
"action": "Cuestionar",
|
|
4941
|
+
"reChallenge": "Cuestionar de nuevo",
|
|
4942
|
+
"dismiss": "Descartar",
|
|
4943
|
+
"submit": "Investigar",
|
|
4944
|
+
"placeholder": "Opcional: ¿qué es exactamente lo incorrecto o poco convincente de este hallazgo?",
|
|
4945
|
+
"hint": "Déjalo en blanco para que el investigador profundice, justifique el hallazgo y compruebe que es preciso y relevante.",
|
|
4946
|
+
"investigatingBanner": "El Investigador de Objeciones está reexaminando un hallazgo frente al código fuente…",
|
|
4947
|
+
"investigatingBadge": "Investigando",
|
|
4948
|
+
"strengthenedBadge": "Reforzado",
|
|
4949
|
+
"upheldBadge": "Confirmado",
|
|
4950
|
+
"retractedBadge": "Retirado",
|
|
4951
|
+
"verdictLabel": "Investigador:",
|
|
4952
|
+
"failedBadge": "Cuestionamiento fallido",
|
|
4953
|
+
"failedLabel": "Cuestionamiento fallido:"
|
|
4954
|
+
},
|
|
4939
4955
|
"fixing": {
|
|
4940
4956
|
"title": "Corrigiendo los hallazgos seleccionados…",
|
|
4941
4957
|
"hint": "Un corrector está confirmando cambios para los hallazgos seleccionados en la rama del pull request."
|
package/i18n/locales/fr.json
CHANGED
|
@@ -4936,6 +4936,22 @@
|
|
|
4936
4936
|
"chunks": "{completed} sur {total} sections examinées",
|
|
4937
4937
|
"inProgress": "{count} en cours"
|
|
4938
4938
|
},
|
|
4939
|
+
"challenge": {
|
|
4940
|
+
"action": "Contester",
|
|
4941
|
+
"reChallenge": "Contester à nouveau",
|
|
4942
|
+
"dismiss": "Écarter",
|
|
4943
|
+
"submit": "Enquêter",
|
|
4944
|
+
"placeholder": "Facultatif : qu'y a-t-il précisément d'incorrect ou de peu convaincant dans ce constat ?",
|
|
4945
|
+
"hint": "Laissez vide pour que l'enquêteur approfondisse, justifie le constat et vérifie qu'il est exact et pertinent.",
|
|
4946
|
+
"investigatingBanner": "L'enquêteur de contestation réexamine un constat par rapport au code source…",
|
|
4947
|
+
"investigatingBadge": "Enquête en cours",
|
|
4948
|
+
"strengthenedBadge": "Renforcé",
|
|
4949
|
+
"upheldBadge": "Confirmé",
|
|
4950
|
+
"retractedBadge": "Retiré",
|
|
4951
|
+
"verdictLabel": "Enquêteur :",
|
|
4952
|
+
"failedBadge": "Échec de la contestation",
|
|
4953
|
+
"failedLabel": "Échec de la contestation :"
|
|
4954
|
+
},
|
|
4939
4955
|
"fixing": {
|
|
4940
4956
|
"title": "Correction des points sélectionnés…",
|
|
4941
4957
|
"hint": "Un correcteur valide des modifications pour les points sélectionnés sur la branche de la pull request."
|
package/i18n/locales/he.json
CHANGED
|
@@ -4947,6 +4947,22 @@
|
|
|
4947
4947
|
"chunks": "{completed} מתוך {total} מקטעים נבדקו",
|
|
4948
4948
|
"inProgress": "{count} בתהליך"
|
|
4949
4949
|
},
|
|
4950
|
+
"challenge": {
|
|
4951
|
+
"action": "ערער",
|
|
4952
|
+
"reChallenge": "ערער שוב",
|
|
4953
|
+
"dismiss": "התעלם",
|
|
4954
|
+
"submit": "חקור",
|
|
4955
|
+
"placeholder": "רשות: מה בדיוק שגוי או לא משכנע בממצא הזה?",
|
|
4956
|
+
"hint": "השאירו ריק כדי שהחוקר יעמיק, ינמק את הממצא ויוודא שהוא מדויק ורלוונטי.",
|
|
4957
|
+
"investigatingBanner": "חוקר הערעורים בוחן מחדש ממצא מול קוד המקור…",
|
|
4958
|
+
"investigatingBadge": "בחקירה",
|
|
4959
|
+
"strengthenedBadge": "חוזק",
|
|
4960
|
+
"upheldBadge": "אושר",
|
|
4961
|
+
"retractedBadge": "בוטל",
|
|
4962
|
+
"verdictLabel": "חוקר:",
|
|
4963
|
+
"failedBadge": "הערעור נכשל",
|
|
4964
|
+
"failedLabel": "הערעור נכשל:"
|
|
4965
|
+
},
|
|
4950
4966
|
"fixing": {
|
|
4951
4967
|
"title": "מתקן את הממצאים שנבחרו…",
|
|
4952
4968
|
"hint": "מתקן מבצע commit לשינויים עבור הממצאים שנבחרו אל ענף בקשת המשיכה."
|
package/i18n/locales/it.json
CHANGED
|
@@ -4948,6 +4948,22 @@
|
|
|
4948
4948
|
"chunks": "{completed} di {total} sezioni esaminate",
|
|
4949
4949
|
"inProgress": "{count} in corso"
|
|
4950
4950
|
},
|
|
4951
|
+
"challenge": {
|
|
4952
|
+
"action": "Contesta",
|
|
4953
|
+
"reChallenge": "Contesta di nuovo",
|
|
4954
|
+
"dismiss": "Scarta",
|
|
4955
|
+
"submit": "Indaga",
|
|
4956
|
+
"placeholder": "Facoltativo: cosa c'è di preciso di sbagliato o poco convincente in questo rilievo?",
|
|
4957
|
+
"hint": "Lascia vuoto per far approfondire l'investigatore, giustificare il rilievo e verificarne accuratezza e pertinenza.",
|
|
4958
|
+
"investigatingBanner": "L'investigatore delle contestazioni sta riesaminando un rilievo rispetto al codice sorgente…",
|
|
4959
|
+
"investigatingBadge": "In esame",
|
|
4960
|
+
"strengthenedBadge": "Rafforzato",
|
|
4961
|
+
"upheldBadge": "Confermato",
|
|
4962
|
+
"retractedBadge": "Ritirato",
|
|
4963
|
+
"verdictLabel": "Investigatore:",
|
|
4964
|
+
"failedBadge": "Contestazione non riuscita",
|
|
4965
|
+
"failedLabel": "Contestazione non riuscita:"
|
|
4966
|
+
},
|
|
4951
4967
|
"fixing": {
|
|
4952
4968
|
"title": "Correzione dei rilievi selezionati…",
|
|
4953
4969
|
"hint": "Un fixer sta effettuando il commit delle modifiche per i rilievi selezionati sul branch della pull request."
|
package/i18n/locales/ja.json
CHANGED
|
@@ -4948,6 +4948,22 @@
|
|
|
4948
4948
|
"chunks": "{total} 個中 {completed} 個のチャンクをレビュー済み",
|
|
4949
4949
|
"inProgress": "{count} 件処理中"
|
|
4950
4950
|
},
|
|
4951
|
+
"challenge": {
|
|
4952
|
+
"action": "異議を唱える",
|
|
4953
|
+
"reChallenge": "再度異議を唱える",
|
|
4954
|
+
"dismiss": "却下",
|
|
4955
|
+
"submit": "調査する",
|
|
4956
|
+
"placeholder": "任意: この指摘のどこが誤り、または納得できないかを記入してください。",
|
|
4957
|
+
"hint": "空欄のままにすると、調査担当がさらに深く掘り下げ、指摘の根拠を示し、正確性と関連性を検証します。",
|
|
4958
|
+
"investigatingBanner": "異議調査担当がソースコードに照らして指摘を再検証しています…",
|
|
4959
|
+
"investigatingBadge": "調査中",
|
|
4960
|
+
"strengthenedBadge": "強化済み",
|
|
4961
|
+
"upheldBadge": "支持",
|
|
4962
|
+
"retractedBadge": "撤回済み",
|
|
4963
|
+
"verdictLabel": "調査担当:",
|
|
4964
|
+
"failedBadge": "異議調査に失敗",
|
|
4965
|
+
"failedLabel": "異議調査に失敗:"
|
|
4966
|
+
},
|
|
4951
4967
|
"fixing": {
|
|
4952
4968
|
"title": "選択した指摘を修正中…",
|
|
4953
4969
|
"hint": "フィクサーが選択した指摘の変更をプルリクエストのブランチにコミットしています。"
|
package/i18n/locales/pl.json
CHANGED
|
@@ -4936,6 +4936,22 @@
|
|
|
4936
4936
|
"chunks": "Sprawdzono {completed} z {total} fragmentów",
|
|
4937
4937
|
"inProgress": "{count} w toku"
|
|
4938
4938
|
},
|
|
4939
|
+
"challenge": {
|
|
4940
|
+
"action": "Zakwestionuj",
|
|
4941
|
+
"reChallenge": "Zakwestionuj ponownie",
|
|
4942
|
+
"dismiss": "Odrzuć",
|
|
4943
|
+
"submit": "Zbadaj",
|
|
4944
|
+
"placeholder": "Opcjonalnie: co dokładnie jest błędne lub nieprzekonujące w tym ustaleniu?",
|
|
4945
|
+
"hint": "Pozostaw puste, aby badacz zgłębił temat, uzasadnił ustalenie i sprawdził, czy jest trafne i istotne.",
|
|
4946
|
+
"investigatingBanner": "Badacz zastrzeżeń ponownie analizuje ustalenie w oparciu o kod źródłowy…",
|
|
4947
|
+
"investigatingBadge": "Badanie",
|
|
4948
|
+
"strengthenedBadge": "Wzmocnione",
|
|
4949
|
+
"upheldBadge": "Podtrzymane",
|
|
4950
|
+
"retractedBadge": "Wycofane",
|
|
4951
|
+
"verdictLabel": "Badacz:",
|
|
4952
|
+
"failedBadge": "Zakwestionowanie nie powiodło się",
|
|
4953
|
+
"failedLabel": "Zakwestionowanie nie powiodło się:"
|
|
4954
|
+
},
|
|
4939
4955
|
"fixing": {
|
|
4940
4956
|
"title": "Naprawianie wybranych uwag…",
|
|
4941
4957
|
"hint": "Fixer zatwierdza zmiany dla wybranych uwag w gałęzi pull requesta."
|
package/i18n/locales/tr.json
CHANGED
|
@@ -4948,6 +4948,22 @@
|
|
|
4948
4948
|
"chunks": "{total} bölümden {completed} tanesi incelendi",
|
|
4949
4949
|
"inProgress": "{count} devam ediyor"
|
|
4950
4950
|
},
|
|
4951
|
+
"challenge": {
|
|
4952
|
+
"action": "İtiraz et",
|
|
4953
|
+
"reChallenge": "Tekrar itiraz et",
|
|
4954
|
+
"dismiss": "Yoksay",
|
|
4955
|
+
"submit": "İncele",
|
|
4956
|
+
"placeholder": "İsteğe bağlı: Bu bulguda tam olarak yanlış veya ikna edici olmayan nedir?",
|
|
4957
|
+
"hint": "Araştırmacının daha derine inmesi, bulguyu gerekçelendirmesi ve doğru ve ilgili olduğunu doğrulaması için boş bırakın.",
|
|
4958
|
+
"investigatingBanner": "İtiraz Araştırmacısı bir bulguyu kaynak koda göre yeniden inceliyor…",
|
|
4959
|
+
"investigatingBadge": "İnceleniyor",
|
|
4960
|
+
"strengthenedBadge": "Güçlendirildi",
|
|
4961
|
+
"upheldBadge": "Onaylandı",
|
|
4962
|
+
"retractedBadge": "Geri çekildi",
|
|
4963
|
+
"verdictLabel": "Araştırmacı:",
|
|
4964
|
+
"failedBadge": "İtiraz başarısız oldu",
|
|
4965
|
+
"failedLabel": "İtiraz başarısız oldu:"
|
|
4966
|
+
},
|
|
4951
4967
|
"fixing": {
|
|
4952
4968
|
"title": "Seçili bulgular düzeltiliyor…",
|
|
4953
4969
|
"hint": "Bir düzeltici, seçili bulgular için değişiklikleri pull request dalına işliyor."
|
package/i18n/locales/uk.json
CHANGED
|
@@ -4936,6 +4936,22 @@
|
|
|
4936
4936
|
"chunks": "Перевірено {completed} з {total} фрагментів",
|
|
4937
4937
|
"inProgress": "{count} у процесі"
|
|
4938
4938
|
},
|
|
4939
|
+
"challenge": {
|
|
4940
|
+
"action": "Оскаржити",
|
|
4941
|
+
"reChallenge": "Оскаржити знову",
|
|
4942
|
+
"dismiss": "Відхилити",
|
|
4943
|
+
"submit": "Дослідити",
|
|
4944
|
+
"placeholder": "Необов'язково: що саме неправильне або непереконливе в цьому зауваженні?",
|
|
4945
|
+
"hint": "Залиште порожнім, щоб дослідник копнув глибше, обґрунтував зауваження та перевірив його точність і доречність.",
|
|
4946
|
+
"investigatingBanner": "Дослідник оскаржень повторно перевіряє зауваження за вихідним кодом…",
|
|
4947
|
+
"investigatingBadge": "Дослідження",
|
|
4948
|
+
"strengthenedBadge": "Посилено",
|
|
4949
|
+
"upheldBadge": "Підтверджено",
|
|
4950
|
+
"retractedBadge": "Відкликано",
|
|
4951
|
+
"verdictLabel": "Дослідник:",
|
|
4952
|
+
"failedBadge": "Оскарження не вдалося",
|
|
4953
|
+
"failedLabel": "Оскарження не вдалося:"
|
|
4954
|
+
},
|
|
4939
4955
|
"fixing": {
|
|
4940
4956
|
"title": "Виправлення вибраних зауважень…",
|
|
4941
4957
|
"hint": "Фіксер комітить зміни для вибраних зауважень у гілку pull request."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.147.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"valibot": "^1.4.2",
|
|
41
41
|
"vue": "3.5.40",
|
|
42
42
|
"wretch": "^3.0.9",
|
|
43
|
-
"@cat-factory/contracts": "0.
|
|
43
|
+
"@cat-factory/contracts": "0.154.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|