@cat-factory/app 0.146.2 → 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.
@@ -1,14 +1,15 @@
1
1
  <script setup lang="ts">
2
- // Shared best-practice prompt-fragment picker: a category-grouped "add" dropdown (with links out
3
- // to the fragment library) plus the currently-selected fragments as removable badges.
2
+ // Shared best-practice prompt-fragment picker: a category-grouped MULTI-SELECT popover (with links
3
+ // out to the fragment library) plus the currently-selected fragments as removable badges.
4
+ // Selecting a row TOGGLES it and leaves the list open, so several fragments can be picked (and
5
+ // unpicked) in one visit; a dedicated "Done" button closes the panel.
4
6
  // Presentational + `v-model`-driven — the caller owns WHERE the selection lives (a task's
5
7
  // `fragmentIds`, a service's `serviceFragmentIds`, or a not-yet-created task's local draft) and
6
8
  // binds the id list. Authored once and reused by the create-task form and the task/service
7
9
  // inspectors so the three pickers can't drift. Ids the catalog no longer resolves still render
8
10
  // (labelled by their raw id) so they stay visible and removable.
9
- import type { DropdownMenuItem } from '@nuxt/ui'
10
11
  import type { PromptFragment } from '~/types/domain'
11
- import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
12
+ import { buildFragmentCategoryGroups } from '~/utils/fragmentPicker'
12
13
 
13
14
  const props = withDefaults(
14
15
  defineProps<{
@@ -30,6 +31,8 @@ const ui = useUiStore()
30
31
  const accounts = useAccountsStore()
31
32
  const { t } = useI18n()
32
33
 
34
+ const open = ref(false)
35
+
33
36
  // The catalog is per-board and invalidated on a workspace switch; (re)load it lazily — a no-op
34
37
  // while current.
35
38
  onMounted(() => fragments.ensureLoaded())
@@ -38,39 +41,18 @@ const selectedFragments = computed(() =>
38
41
  props.modelValue.map((id) => fragments.getFragment(id) ?? { id, title: id, summary: '' }),
39
42
  )
40
43
 
41
- // A trailing group that jumps from "attach a fragment" to authoring/editing the library itself
42
- // (board tier always; account tier when accounts are enabled). Open to every member.
43
- const manageItems = computed<DropdownMenuItem[]>(() => {
44
- const items: DropdownMenuItem[] = [
45
- {
46
- label: t('inspector.fragments.manageBoard'),
47
- icon: 'i-lucide-book-marked',
48
- onSelect: () => ui.openFragmentLibrary(),
49
- },
50
- ]
51
- if (accounts.enabled) {
52
- items.push({
53
- label: t('inspector.fragments.manageAccount'),
54
- icon: 'i-lucide-users',
55
- onSelect: () => ui.openAccountSettings('fragments'),
56
- })
57
- }
58
- return items
59
- })
44
+ const selectedSet = computed(() => new Set(props.modelValue))
60
45
 
61
- // Picker menu: pool fragments not already selected, grouped into labelled per-category sections,
62
- // with the management links appended as the final group.
63
- const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
64
- const selected = new Set(props.modelValue)
65
- return [
66
- ...buildFragmentPickerGroups(props.pool, (id) => selected.has(id), addFragment),
67
- manageItems.value,
68
- ]
69
- })
46
+ // Pool fragments bucketed into labelled per-category sections. Selected rows stay in the list so
47
+ // they can be unpicked in place (a check marks the current selection).
48
+ const categoryGroups = computed(() => buildFragmentCategoryGroups(props.pool))
70
49
 
71
- function addFragment(id: string) {
72
- if (props.modelValue.includes(id)) return
73
- emit('update:modelValue', [...props.modelValue, id])
50
+ function toggleFragment(id: string) {
51
+ if (props.modelValue.includes(id)) {
52
+ removeFragment(id)
53
+ } else {
54
+ emit('update:modelValue', [...props.modelValue, id])
55
+ }
74
56
  }
75
57
 
76
58
  function removeFragment(id: string) {
@@ -79,6 +61,16 @@ function removeFragment(id: string) {
79
61
  props.modelValue.filter((x) => x !== id),
80
62
  )
81
63
  }
64
+
65
+ function manageBoard() {
66
+ open.value = false
67
+ ui.openFragmentLibrary()
68
+ }
69
+
70
+ function manageAccount() {
71
+ open.value = false
72
+ ui.openAccountSettings('fragments')
73
+ }
82
74
  </script>
83
75
 
84
76
  <template>
@@ -88,7 +80,7 @@ function removeFragment(id: string) {
88
80
  {{ label }}
89
81
  </span>
90
82
  <span v-else />
91
- <UDropdownMenu :items="fragmentMenu">
83
+ <UPopover v-model:open="open" :content="{ align: 'end' }">
92
84
  <UButton
93
85
  size="xs"
94
86
  variant="ghost"
@@ -97,7 +89,79 @@ function removeFragment(id: string) {
97
89
  trailing-icon="i-lucide-chevron-down"
98
90
  data-testid="fragment-add"
99
91
  />
100
- </UDropdownMenu>
92
+
93
+ <template #content>
94
+ <div
95
+ class="flex max-h-[24rem] w-[min(22rem,92vw)] flex-col"
96
+ data-testid="fragment-picker-panel"
97
+ >
98
+ <div class="min-h-0 flex-1 overflow-y-auto p-1">
99
+ <template v-if="categoryGroups.length">
100
+ <div v-for="group in categoryGroups" :key="group.category">
101
+ <p
102
+ class="px-2 pb-0.5 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500"
103
+ >
104
+ {{ group.category }}
105
+ </p>
106
+ <button
107
+ v-for="f in group.fragments"
108
+ :key="f.id"
109
+ type="button"
110
+ class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-start text-sm hover:bg-slate-800/60"
111
+ :class="selectedSet.has(f.id) ? 'text-slate-100' : 'text-slate-300'"
112
+ :title="f.summary"
113
+ :data-testid="`fragment-option-${f.id}`"
114
+ :aria-pressed="selectedSet.has(f.id)"
115
+ @click="toggleFragment(f.id)"
116
+ >
117
+ <UIcon
118
+ :name="selectedSet.has(f.id) ? 'i-lucide-check' : 'i-lucide-plus'"
119
+ class="h-4 w-4 shrink-0"
120
+ :class="selectedSet.has(f.id) ? 'text-primary-400' : 'text-slate-500'"
121
+ />
122
+ <span class="flex-1 truncate">{{ f.title }}</span>
123
+ </button>
124
+ </div>
125
+ </template>
126
+ <p v-else class="px-2 py-3 text-[12px] text-slate-500">
127
+ {{ t('inspector.fragments.pickerEmpty') }}
128
+ </p>
129
+
130
+ <div class="mt-1 border-t border-slate-800 pt-1">
131
+ <button
132
+ type="button"
133
+ class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-start text-sm text-slate-300 hover:bg-slate-800/60"
134
+ @click="manageBoard"
135
+ >
136
+ <UIcon name="i-lucide-book-marked" class="h-4 w-4 shrink-0 text-slate-400" />
137
+ <span class="flex-1 truncate">{{ t('inspector.fragments.manageBoard') }}</span>
138
+ </button>
139
+ <button
140
+ v-if="accounts.enabled"
141
+ type="button"
142
+ class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-start text-sm text-slate-300 hover:bg-slate-800/60"
143
+ @click="manageAccount"
144
+ >
145
+ <UIcon name="i-lucide-users" class="h-4 w-4 shrink-0 text-slate-400" />
146
+ <span class="flex-1 truncate">{{ t('inspector.fragments.manageAccount') }}</span>
147
+ </button>
148
+ </div>
149
+ </div>
150
+
151
+ <div class="flex justify-end border-t border-slate-800 p-1.5">
152
+ <UButton
153
+ size="xs"
154
+ color="neutral"
155
+ variant="soft"
156
+ data-testid="fragment-picker-done"
157
+ @click="open = false"
158
+ >
159
+ {{ t('inspector.fragments.done') }}
160
+ </UButton>
161
+ </div>
162
+ </div>
163
+ </template>
164
+ </UPopover>
101
165
  </div>
102
166
  <div v-if="selectedFragments.length" class="flex flex-wrap gap-1">
103
167
  <UBadge
@@ -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(() => selected.value.size > 0)
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, [...selected.value], action).catch(() => {})
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: selected.size }) }}
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
- <h4 class="min-w-0 flex-1 text-[13px] font-medium text-slate-100">
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 class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300">
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 { getPrReviewContract, resolvePrReviewContract } from '@cat-factory/contracts'
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
  }
@@ -82,5 +82,50 @@ export const usePrReviewStore = defineStore('prReview', () => {
82
82
  }
83
83
  }
84
84
 
85
- return { resolving, error, load, resolve }
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
  })
@@ -45,6 +45,7 @@ export type {
45
45
  ForkDecisionStepState,
46
46
  PrReviewStepState,
47
47
  PrReviewFinding,
48
+ PrReviewFindingChallenge,
48
49
  PrReviewSlice,
49
50
  PrReviewSeverity,
50
51
  PrReviewCategory,
@@ -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.
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import type { PromptFragment } from '~/types/domain'
3
- import { buildFragmentPickerGroups } from './fragmentPicker'
3
+ import { buildFragmentCategoryGroups, buildFragmentPickerGroups } from './fragmentPicker'
4
4
 
5
5
  const frag = (id: string, category: string, title = id): PromptFragment =>
6
6
  ({ id, version: '1.0.0', title, category, summary: '', body: '' }) as PromptFragment
@@ -58,3 +58,24 @@ describe('buildFragmentPickerGroups', () => {
58
58
  expect(picked).toEqual(['node.best-practices'])
59
59
  })
60
60
  })
61
+
62
+ describe('buildFragmentCategoryGroups', () => {
63
+ it('buckets every fragment by category in first-appearance order, keeping pool order within', () => {
64
+ const groups = buildFragmentCategoryGroups(pool)
65
+ expect(groups.map((g) => g.category)).toEqual(['Node', 'Writing style'])
66
+ expect(groups[0]!.fragments.map((f) => f.id)).toEqual([
67
+ 'node.best-practices',
68
+ 'node.performance',
69
+ ])
70
+ expect(groups[1]!.fragments.map((f) => f.id)).toEqual([
71
+ 'style.anti-llmisms',
72
+ 'style.concise-actionable',
73
+ ])
74
+ })
75
+
76
+ it('keeps selected fragments in their bucket (multi-select toggles in place, never hides)', () => {
77
+ // Unlike the dropdown builder, the category grouping does no selection filtering.
78
+ const groups = buildFragmentCategoryGroups(pool)
79
+ expect(groups.flatMap((g) => g.fragments)).toHaveLength(pool.length)
80
+ })
81
+ })
@@ -1,6 +1,28 @@
1
1
  import type { DropdownMenuItem } from '@nuxt/ui'
2
2
  import type { PromptFragment } from '~/types/domain'
3
3
 
4
+ /** One category bucket: its heading plus the pool fragments that fall under it, in pool order. */
5
+ export interface FragmentCategoryGroup {
6
+ category: string
7
+ fragments: PromptFragment[]
8
+ }
9
+
10
+ /**
11
+ * Bucket the pool fragments by `category`, preserving first-appearance category order and
12
+ * per-category pool order. Unlike {@link buildFragmentPickerGroups} this keeps ALL fragments
13
+ * (selected included) and returns the raw fragments, so a multi-select surface can render each
14
+ * as a toggle that stays visible whether or not it is currently picked.
15
+ */
16
+ export function buildFragmentCategoryGroups(pool: PromptFragment[]): FragmentCategoryGroup[] {
17
+ const groups = new Map<string, PromptFragment[]>()
18
+ for (const f of pool) {
19
+ const items = groups.get(f.category) ?? []
20
+ items.push(f)
21
+ groups.set(f.category, items)
22
+ }
23
+ return [...groups.entries()].map(([category, fragments]) => ({ category, fragments }))
24
+ }
25
+
4
26
  /**
5
27
  * Build the category-grouped groups for a fragment "add" dropdown: the pool fragments not
6
28
  * already selected, bucketed by `category`, each bucket prefixed with a non-interactive
@@ -18,15 +40,12 @@ export function buildFragmentPickerGroups(
18
40
  isSelected: (id: string) => boolean,
19
41
  onSelect: (id: string) => void,
20
42
  ): DropdownMenuItem[][] {
21
- const groups = new Map<string, DropdownMenuItem[]>()
22
- for (const f of pool) {
23
- if (isSelected(f.id)) continue
24
- const items = groups.get(f.category) ?? []
25
- items.push({ label: f.title, onSelect: () => onSelect(f.id) })
26
- groups.set(f.category, items)
27
- }
28
- return [...groups.entries()].map(([category, items]): DropdownMenuItem[] => [
29
- { type: 'label', label: category },
30
- ...items,
31
- ])
43
+ return buildFragmentCategoryGroups(pool)
44
+ .map(({ category, fragments }): DropdownMenuItem[] => {
45
+ const items = fragments
46
+ .filter((f) => !isSelected(f.id))
47
+ .map((f): DropdownMenuItem => ({ label: f.title, onSelect: () => onSelect(f.id) }))
48
+ return items.length ? [{ type: 'label', label: category }, ...items] : []
49
+ })
50
+ .filter((group) => group.length > 0)
32
51
  }
@@ -931,7 +931,9 @@
931
931
  "serviceHint": "Programmierstandards für den gesamten Service. Der Inhalt jedes ausgewählten Fragments wird dem Prompt jedes code-bewussten Agents hinzugefügt, der an Aufgaben unter diesem Service arbeitet.",
932
932
  "serviceEmpty": "Keine. Code-bewusste Agents dieses Service folgen ihren Standardvorgaben.",
933
933
  "manageBoard": "Fragment-Bibliothek dieses Boards verwalten…",
934
- "manageAccount": "Konto-Fragmente verwalten…"
934
+ "manageAccount": "Konto-Fragmente verwalten…",
935
+ "done": "Fertig",
936
+ "pickerEmpty": "Keine Fragmente verfügbar."
935
937
  },
936
938
  "frontendConfig": {
937
939
  "title": "Frontend",
@@ -4946,6 +4948,22 @@
4946
4948
  "chunks": "{completed} von {total} Abschnitten geprüft",
4947
4949
  "inProgress": "{count} in Bearbeitung"
4948
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
+ },
4949
4967
  "fixing": {
4950
4968
  "title": "Ausgewählte Befunde werden behoben…",
4951
4969
  "hint": "Ein Fixer committet Änderungen für die ausgewählten Befunde auf den Pull-Request-Branch."
@@ -706,7 +706,9 @@
706
706
  "serviceHint": "Programming standards for the whole service. The content of each selected fragment is added to the prompt of every code-aware agent working on tasks under this service.",
707
707
  "serviceEmpty": "None. Code-aware agents on this service follow their default guidance.",
708
708
  "manageBoard": "Manage this board's fragment library…",
709
- "manageAccount": "Manage account fragments…"
709
+ "manageAccount": "Manage account fragments…",
710
+ "done": "Done",
711
+ "pickerEmpty": "No fragments available."
710
712
  },
711
713
  "frontendConfig": {
712
714
  "title": "Frontend",
@@ -5075,6 +5077,22 @@
5075
5077
  "chunks": "{completed} of {total} chunks reviewed",
5076
5078
  "inProgress": "{count} in progress"
5077
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
+ },
5078
5096
  "fixing": {
5079
5097
  "title": "Fixing the selected findings…",
5080
5098
  "hint": "A fixer is committing changes for the selected findings onto the pull request branch."
@@ -649,7 +649,9 @@
649
649
  "serviceHint": "Estándares de programación para todo el servicio. El contenido de cada fragmento seleccionado se añade al prompt de cada agente que conoce el código y trabaja en tareas de este servicio.",
650
650
  "serviceEmpty": "Ninguna. Los agentes que conocen el código en este servicio siguen su guía predeterminada.",
651
651
  "manageBoard": "Gestionar la biblioteca de fragmentos de este tablero…",
652
- "manageAccount": "Gestionar los fragmentos de la cuenta…"
652
+ "manageAccount": "Gestionar los fragmentos de la cuenta…",
653
+ "done": "Listo",
654
+ "pickerEmpty": "No hay fragmentos disponibles."
653
655
  },
654
656
  "frontendConfig": {
655
657
  "title": "Frontend",
@@ -4934,6 +4936,22 @@
4934
4936
  "chunks": "{completed} de {total} fragmentos revisados",
4935
4937
  "inProgress": "{count} en curso"
4936
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
+ },
4937
4955
  "fixing": {
4938
4956
  "title": "Corrigiendo los hallazgos seleccionados…",
4939
4957
  "hint": "Un corrector está confirmando cambios para los hallazgos seleccionados en la rama del pull request."
@@ -649,7 +649,9 @@
649
649
  "serviceHint": "Normes de programmation pour tout le service. Le contenu de chaque fragment sélectionné est ajouté au prompt de chaque agent conscient du code travaillant sur les tâches de ce service.",
650
650
  "serviceEmpty": "Aucune. Les agents conscients du code sur ce service suivent leurs consignes par défaut.",
651
651
  "manageBoard": "Gérer la bibliothèque de fragments de ce tableau…",
652
- "manageAccount": "Gérer les fragments du compte…"
652
+ "manageAccount": "Gérer les fragments du compte…",
653
+ "done": "Terminé",
654
+ "pickerEmpty": "Aucun fragment disponible."
653
655
  },
654
656
  "frontendConfig": {
655
657
  "title": "Frontend",
@@ -4934,6 +4936,22 @@
4934
4936
  "chunks": "{completed} sur {total} sections examinées",
4935
4937
  "inProgress": "{count} en cours"
4936
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
+ },
4937
4955
  "fixing": {
4938
4956
  "title": "Correction des points sélectionnés…",
4939
4957
  "hint": "Un correcteur valide des modifications pour les points sélectionnés sur la branche de la pull request."
@@ -649,7 +649,9 @@
649
649
  "serviceHint": "תקני תכנות לכל השירות. התוכן של כל פרגמנט שנבחר מתווסף לפרומפט של כל סוכן מודע-קוד שעובד על משימות תחת שירות זה.",
650
650
  "serviceEmpty": "אין. סוכנים מודעי-קוד בשירות זה פועלים לפי ההנחיות המוגדרות כברירת מחדל.",
651
651
  "manageBoard": "נהל את ספריית הקטעים של לוח זה…",
652
- "manageAccount": "נהל קטעי חשבון…"
652
+ "manageAccount": "נהל קטעי חשבון…",
653
+ "done": "סיום",
654
+ "pickerEmpty": "אין קטעים זמינים."
653
655
  },
654
656
  "frontendConfig": {
655
657
  "title": "Frontend",
@@ -4945,6 +4947,22 @@
4945
4947
  "chunks": "{completed} מתוך {total} מקטעים נבדקו",
4946
4948
  "inProgress": "{count} בתהליך"
4947
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
+ },
4948
4966
  "fixing": {
4949
4967
  "title": "מתקן את הממצאים שנבחרו…",
4950
4968
  "hint": "מתקן מבצע commit לשינויים עבור הממצאים שנבחרו אל ענף בקשת המשיכה."
@@ -931,7 +931,9 @@
931
931
  "serviceHint": "Standard di programmazione per l'intero servizio. Il contenuto di ogni frammento selezionato viene aggiunto al prompt di ogni agente code-aware che lavora sulle attivita' di questo servizio.",
932
932
  "serviceEmpty": "Nessuno. Gli agenti code-aware su questo servizio seguono le loro linee guida predefinite.",
933
933
  "manageBoard": "Gestisci la libreria di frammenti di questa board…",
934
- "manageAccount": "Gestisci i frammenti dell'account…"
934
+ "manageAccount": "Gestisci i frammenti dell'account…",
935
+ "done": "Fatto",
936
+ "pickerEmpty": "Nessun frammento disponibile."
935
937
  },
936
938
  "frontendConfig": {
937
939
  "title": "Frontend",
@@ -4946,6 +4948,22 @@
4946
4948
  "chunks": "{completed} di {total} sezioni esaminate",
4947
4949
  "inProgress": "{count} in corso"
4948
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
+ },
4949
4967
  "fixing": {
4950
4968
  "title": "Correzione dei rilievi selezionati…",
4951
4969
  "hint": "Un fixer sta effettuando il commit delle modifiche per i rilievi selezionati sul branch della pull request."
@@ -649,7 +649,9 @@
649
649
  "serviceHint": "サービス全体のプログラミング標準です。選択した各フラグメントの内容は、このサービスのタスクに取り組むコード対応エージェントすべてのプロンプトに追加されます。",
650
650
  "serviceEmpty": "なし。このサービスのコード対応エージェントはデフォルトのガイダンスに従います。",
651
651
  "manageBoard": "このボードのフラグメントライブラリを管理…",
652
- "manageAccount": "アカウントのフラグメントを管理…"
652
+ "manageAccount": "アカウントのフラグメントを管理…",
653
+ "done": "完了",
654
+ "pickerEmpty": "利用可能なフラグメントがありません。"
653
655
  },
654
656
  "frontendConfig": {
655
657
  "title": "フロントエンド",
@@ -4946,6 +4948,22 @@
4946
4948
  "chunks": "{total} 個中 {completed} 個のチャンクをレビュー済み",
4947
4949
  "inProgress": "{count} 件処理中"
4948
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
+ },
4949
4967
  "fixing": {
4950
4968
  "title": "選択した指摘を修正中…",
4951
4969
  "hint": "フィクサーが選択した指摘の変更をプルリクエストのブランチにコミットしています。"
@@ -649,7 +649,9 @@
649
649
  "serviceHint": "Standardy programowania dla całej usługi. Treść każdego wybranego fragmentu jest dodawana do promptu każdego agenta świadomego kodu pracującego nad zadaniami tej usługi.",
650
650
  "serviceEmpty": "Brak. Agenci świadomi kodu w tej usłudze stosują swoje domyślne wytyczne.",
651
651
  "manageBoard": "Zarządzaj biblioteką fragmentów tej tablicy…",
652
- "manageAccount": "Zarządzaj fragmentami konta…"
652
+ "manageAccount": "Zarządzaj fragmentami konta…",
653
+ "done": "Gotowe",
654
+ "pickerEmpty": "Brak dostępnych fragmentów."
653
655
  },
654
656
  "frontendConfig": {
655
657
  "title": "Frontend",
@@ -4934,6 +4936,22 @@
4934
4936
  "chunks": "Sprawdzono {completed} z {total} fragmentów",
4935
4937
  "inProgress": "{count} w toku"
4936
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
+ },
4937
4955
  "fixing": {
4938
4956
  "title": "Naprawianie wybranych uwag…",
4939
4957
  "hint": "Fixer zatwierdza zmiany dla wybranych uwag w gałęzi pull requesta."
@@ -649,7 +649,9 @@
649
649
  "serviceHint": "Tüm servis için programlama standartları. Seçilen her parçanın içeriği, bu servisin görevlerinde çalışan kod farkında her ajanın prompt'una eklenir.",
650
650
  "serviceEmpty": "Yok. Bu servisteki kod farkında ajanlar varsayılan yönergelerini izler.",
651
651
  "manageBoard": "Bu pano'nun fragman kütüphanesini yönet…",
652
- "manageAccount": "Hesap fragmanlarını yönet…"
652
+ "manageAccount": "Hesap fragmanlarını yönet…",
653
+ "done": "Tamam",
654
+ "pickerEmpty": "Kullanılabilir parça yok."
653
655
  },
654
656
  "frontendConfig": {
655
657
  "title": "Frontend",
@@ -4946,6 +4948,22 @@
4946
4948
  "chunks": "{total} bölümden {completed} tanesi incelendi",
4947
4949
  "inProgress": "{count} devam ediyor"
4948
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
+ },
4949
4967
  "fixing": {
4950
4968
  "title": "Seçili bulgular düzeltiliyor…",
4951
4969
  "hint": "Bir düzeltici, seçili bulgular için değişiklikleri pull request dalına işliyor."
@@ -649,7 +649,9 @@
649
649
  "serviceHint": "Стандарти програмування для всього сервісу. Вміст кожного вибраного фрагмента додається до промпту кожного агента, що працює з кодом завдань цього сервісу.",
650
650
  "serviceEmpty": "Немає. Агенти, що працюють з кодом цього сервісу, дотримуються типових інструкцій.",
651
651
  "manageBoard": "Керувати бібліотекою фрагментів цієї дошки…",
652
- "manageAccount": "Керувати фрагментами облікового запису…"
652
+ "manageAccount": "Керувати фрагментами облікового запису…",
653
+ "done": "Готово",
654
+ "pickerEmpty": "Немає доступних фрагментів."
653
655
  },
654
656
  "frontendConfig": {
655
657
  "title": "Frontend",
@@ -4934,6 +4936,22 @@
4934
4936
  "chunks": "Перевірено {completed} з {total} фрагментів",
4935
4937
  "inProgress": "{count} у процесі"
4936
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
+ },
4937
4955
  "fixing": {
4938
4956
  "title": "Виправлення вибраних зауважень…",
4939
4957
  "hint": "Фіксер комітить зміни для вибраних зауважень у гілку pull request."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.146.2",
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.153.0"
43
+ "@cat-factory/contracts": "0.154.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",