@cat-factory/app 0.150.1 → 0.151.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.
@@ -28,6 +28,7 @@ import ContextDocumentPicker from '~/components/documents/ContextDocumentPicker.
28
28
  import ContextIssuePicker from '~/components/tasks/ContextIssuePicker.vue'
29
29
  import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
30
30
  import { riskPolicyOptionLabel, riskPolicySummary } from '~/utils/riskPolicy'
31
+ import { parseConflict } from '~/composables/usePipelineErrorToast'
31
32
  import { pipelineAllowedForManualStart } from '~/utils/pipeline'
32
33
 
33
34
  const ui = useUiStore()
@@ -678,8 +679,7 @@ const canAdd = computed(() => {
678
679
  })
679
680
 
680
681
  async function add() {
681
- const containerId = ui.addTaskContainerId
682
- if (!containerId || !canAdd.value) return
682
+ if (!canAdd.value) return
683
683
  // Recurring tasks are created via a schedule on the service frame — hand off to the
684
684
  // existing recurring-pipeline modal (which carries the cadence + prompt).
685
685
  if (isRecurring.value) {
@@ -689,6 +689,17 @@ async function add() {
689
689
  ui.openAddRecurring(frameId)
690
690
  return
691
691
  }
692
+ await submitCreate(false)
693
+ }
694
+
695
+ /**
696
+ * Create the task, optionally acknowledging review-debt friction. A `review_debt_*` 409 opens the
697
+ * friction dialog instead of a bare error toast: the soft `warn` tier's dialog can retry via
698
+ * `submitCreate(true)` (the `onConfirm`), while a hard `blocked` tier only offers "Go review".
699
+ */
700
+ async function submitCreate(acknowledgeReviewDebt: boolean) {
701
+ const containerId = ui.addTaskContainerId
702
+ if (!containerId) return
692
703
  saving.value = true
693
704
  try {
694
705
  const typeFields = buildTypeFields()
@@ -718,6 +729,7 @@ async function add() {
718
729
  // re-seeded from the service. The task owns its fragments from here.
719
730
  fragmentIds: [...fragmentIds.value],
720
731
  ...(technical.value ? { technical: true } : {}),
732
+ ...(acknowledgeReviewDebt ? { acknowledgeReviewDebt: true } : {}),
721
733
  })
722
734
  if (block) {
723
735
  // Surface the SPECIFIC cause of any attachment that couldn't be linked (a GitHub
@@ -725,8 +737,14 @@ async function add() {
725
737
  // one-click "Copy details" for a bug report.
726
738
  presentLinkFailures(await linkPending(block.id, pendingContext.value), block.id)
727
739
  }
740
+ ui.closeReviewFriction()
728
741
  ui.closeAddTask()
729
742
  } catch (e) {
743
+ const conflict = parseConflict(e)
744
+ if (conflict?.reason === 'review_debt_warn' || conflict?.reason === 'review_debt_blocked') {
745
+ openReviewFrictionDialog(conflict)
746
+ return
747
+ }
730
748
  toast.add({
731
749
  title: t('board.addTask.addFailedTitle'),
732
750
  description: e instanceof Error ? e.message : String(e),
@@ -737,6 +755,29 @@ async function add() {
737
755
  saving.value = false
738
756
  }
739
757
  }
758
+
759
+ /** Turn a parsed review-debt friction 409 into the dialog context (see ReviewFrictionDialog.vue). */
760
+ function openReviewFrictionDialog(conflict: NonNullable<ReturnType<typeof parseConflict>>) {
761
+ const details = conflict.details
762
+ const rawDebt = Array.isArray(details.debt) ? details.debt : []
763
+ const debt = rawDebt.map((d) => {
764
+ const row = (d ?? {}) as { blockId?: unknown; title?: unknown; waitingMinutes?: unknown }
765
+ return {
766
+ blockId: typeof row.blockId === 'string' ? row.blockId : '',
767
+ title: typeof row.title === 'string' ? row.title : null,
768
+ waitingMinutes: typeof row.waitingMinutes === 'number' ? row.waitingMinutes : 0,
769
+ }
770
+ })
771
+ const isWarn = conflict.reason === 'review_debt_warn'
772
+ ui.openReviewFriction({
773
+ kind: isWarn ? 'warn' : 'blocked',
774
+ reason:
775
+ details.friction === 'count' || details.friction === 'stuck' ? details.friction : undefined,
776
+ threshold: typeof details.threshold === 'number' ? details.threshold : null,
777
+ debt,
778
+ onConfirm: isWarn ? () => void submitCreate(true) : null,
779
+ })
780
+ }
740
781
  </script>
741
782
 
742
783
  <template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ // A small pre-warn badge shown next to an "Add task" affordance when the workspace's opt-in
3
+ // review-debt friction is at (or past) its threshold, so the friction dialog isn't a surprise.
4
+ // Hidden entirely when friction is off or the queue is under the warn threshold. The server
5
+ // remains the authority — this is a hint. See backend/docs/review-debt-friction.md.
6
+ import { computed } from 'vue'
7
+ import { useReviewDebt } from '~/composables/useReviewDebt'
8
+
9
+ const { active, debtCount, blocked } = useReviewDebt()
10
+ const { t } = useI18n()
11
+
12
+ const color = computed(() => (blocked.value ? 'error' : 'warning'))
13
+ </script>
14
+
15
+ <template>
16
+ <UBadge
17
+ v-if="active"
18
+ :color="color"
19
+ variant="subtle"
20
+ size="sm"
21
+ icon="i-lucide-clock"
22
+ :title="t('errors.reviewFriction.badge', { count: debtCount })"
23
+ >
24
+ {{ debtCount }}
25
+ </UBadge>
26
+ </template>
@@ -0,0 +1,118 @@
1
+ <script setup lang="ts">
2
+ // Opt-in review-debt friction dialog. Shown when a task-create request is refused by the friction
3
+ // gate (a `review_debt_warn` / `review_debt_blocked` 409). It lists exactly which tasks are waiting
4
+ // on human review (worst first), each deep-linking to its block so "go review instead" is one
5
+ // click. The soft `warn` tier offers a secondary "Create anyway" (retries with the acknowledge
6
+ // flag); the hard `blocked` tier does not. Driven by `ui.reviewFrictionContext`, mounted in
7
+ // pages/index.vue. See backend/docs/review-debt-friction.md.
8
+ import { computed } from 'vue'
9
+
10
+ const { t } = useI18n()
11
+ const ui = useUiStore()
12
+
13
+ const ctx = computed(() => ui.reviewFrictionContext)
14
+
15
+ const open = computed({
16
+ get: () => ctx.value !== null,
17
+ set: (v: boolean) => {
18
+ if (!v) ui.closeReviewFriction()
19
+ },
20
+ })
21
+
22
+ const title = computed(() =>
23
+ ctx.value?.kind === 'blocked'
24
+ ? t('errors.reviewFriction.blockedTitle')
25
+ : t('errors.reviewFriction.warnTitle'),
26
+ )
27
+
28
+ const body = computed(() => {
29
+ const c = ctx.value
30
+ if (!c) return ''
31
+ if (c.kind === 'warn') return t('errors.reviewFriction.warnBody', { count: c.debt.length })
32
+ if (c.reason === 'stuck')
33
+ return t('errors.reviewFriction.blockedStuckBody', { minutes: c.threshold ?? 0 })
34
+ return t('errors.reviewFriction.blockedCountBody', {
35
+ count: c.debt.length,
36
+ threshold: c.threshold ?? 0,
37
+ })
38
+ })
39
+
40
+ /** Deep-link to a waiting task's block and dismiss the whole friction flow. */
41
+ function goToBlock(blockId: string) {
42
+ if (blockId) ui.select(blockId)
43
+ ui.closeReviewFriction()
44
+ ui.closeAddTask()
45
+ }
46
+
47
+ function goReview() {
48
+ const first = ctx.value?.debt[0]
49
+ if (first) goToBlock(first.blockId)
50
+ else {
51
+ ui.closeReviewFriction()
52
+ ui.closeAddTask()
53
+ }
54
+ }
55
+
56
+ function createAnyway() {
57
+ ctx.value?.onConfirm?.()
58
+ }
59
+ </script>
60
+
61
+ <template>
62
+ <UModal v-model:open="open" :title="title" :ui="{ content: 'max-w-xl' }">
63
+ <template #body>
64
+ <div v-if="ctx" class="space-y-5">
65
+ <p class="text-sm text-slate-300">{{ body }}</p>
66
+
67
+ <div class="rounded-lg border border-slate-700 bg-slate-900/50 p-2">
68
+ <p class="mb-1.5 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
69
+ {{ t('errors.reviewFriction.waitingHeading') }}
70
+ </p>
71
+ <ul class="space-y-1">
72
+ <li v-for="item in ctx.debt" :key="item.blockId">
73
+ <button
74
+ type="button"
75
+ class="flex w-full items-center justify-between gap-3 rounded-md px-2 py-1.5 text-left text-sm hover:bg-slate-800/60"
76
+ @click="goToBlock(item.blockId)"
77
+ >
78
+ <span class="truncate text-slate-200">
79
+ {{ item.title || t('errors.reviewFriction.untitled') }}
80
+ </span>
81
+ <span class="shrink-0 text-[12px] text-slate-500">
82
+ {{ t('errors.reviewFriction.waiting', { minutes: item.waitingMinutes }) }}
83
+ </span>
84
+ </button>
85
+ </li>
86
+ </ul>
87
+ </div>
88
+
89
+ <div class="flex flex-wrap justify-end gap-2">
90
+ <UButton
91
+ color="neutral"
92
+ variant="ghost"
93
+ size="sm"
94
+ @click="
95
+ () => {
96
+ open = false
97
+ }
98
+ "
99
+ >
100
+ {{ t('errors.reviewFriction.close') }}
101
+ </UButton>
102
+ <UButton
103
+ v-if="ctx.onConfirm"
104
+ color="neutral"
105
+ variant="subtle"
106
+ size="sm"
107
+ @click="createAnyway"
108
+ >
109
+ {{ t('errors.reviewFriction.createAnyway') }}
110
+ </UButton>
111
+ <UButton color="primary" size="sm" icon="i-lucide-list-checks" @click="goReview">
112
+ {{ t('errors.reviewFriction.goReview') }}
113
+ </UButton>
114
+ </div>
115
+ </div>
116
+ </template>
117
+ </UModal>
118
+ </template>
@@ -2,6 +2,7 @@
2
2
  import type { Block } from '~/types/domain'
3
3
  import { STATUS_META } from '~/utils/catalog'
4
4
  import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
5
+ import ReviewDebtBadge from '~/components/board/ReviewDebtBadge.vue'
5
6
 
6
7
  const props = defineProps<{ block: Block }>()
7
8
 
@@ -61,9 +62,12 @@ function addTask() {
61
62
  : t('inspector.container.tasks', { count: tasks.length })
62
63
  }}
63
64
  </span>
64
- <UButton size="xs" variant="soft" color="primary" icon="i-lucide-plus" @click="addTask">
65
- {{ t('inspector.container.addTask') }}
66
- </UButton>
65
+ <div class="flex items-center gap-1.5">
66
+ <ReviewDebtBadge />
67
+ <UButton size="xs" variant="soft" color="primary" icon="i-lucide-plus" @click="addTask">
68
+ {{ t('inspector.container.addTask') }}
69
+ </UButton>
70
+ </div>
67
71
  </div>
68
72
  <ul v-if="tasks.length" class="space-y-1">
69
73
  <li
@@ -8,7 +8,7 @@
8
8
  // The latter three are body-only section components rendered in tabs here (no longer
9
9
  // standalone modals).
10
10
  import { reactive, ref, watch } from 'vue'
11
- import type { TaskLimitMode } from '~/types/domain'
11
+ import type { ReviewFrictionMode, TaskLimitMode } from '~/types/domain'
12
12
  import RiskPolicyPanel from '~/components/settings/RiskPolicyPanel.vue'
13
13
  import IssueTrackerPanel from '~/components/settings/IssueTrackerPanel.vue'
14
14
  import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
@@ -129,6 +129,12 @@ const MODES = computed<{ value: TaskLimitMode; label: string }[]>(() => [
129
129
  { value: 'per_type', label: t('settings.workspaceSettings.taskLimit.modes.per_type') },
130
130
  ])
131
131
 
132
+ const REVIEW_FRICTION_MODES = computed<{ value: ReviewFrictionMode; label: string }[]>(() => [
133
+ { value: 'off', label: t('settings.workspaceSettings.reviewFriction.modes.off') },
134
+ { value: 'warn', label: t('settings.workspaceSettings.reviewFriction.modes.warn') },
135
+ { value: 'enforce', label: t('settings.workspaceSettings.reviewFriction.modes.enforce') },
136
+ ])
137
+
132
138
  /** The localized "Max {type} tasks" label for a per-type running-task limit input. */
133
139
  function maxTaskTypeLabel(type: LimitTaskType): string {
134
140
  const key = TASK_TYPE_KEYS[type]
@@ -145,6 +151,12 @@ const draft = reactive({
145
151
  storeAgentContext: true,
146
152
  artifactRetentionDays: 14,
147
153
  kaizenEnabled: true,
154
+ reviewFrictionMode: 'off' as ReviewFrictionMode,
155
+ reviewFrictionWarnCount: 3,
156
+ reviewFrictionBlockCountEnabled: false,
157
+ reviewFrictionBlockCount: 10 as number,
158
+ reviewFrictionBlockStuckEnabled: false,
159
+ reviewFrictionBlockStuckMinutes: 1440 as number,
148
160
  })
149
161
 
150
162
  function hydrate() {
@@ -157,6 +169,14 @@ function hydrate() {
157
169
  draft.storeAgentContext = s.storeAgentContext
158
170
  draft.artifactRetentionDays = s.artifactRetentionDays
159
171
  draft.kaizenEnabled = s.kaizenEnabled
172
+ draft.reviewFrictionMode = s.reviewFrictionMode
173
+ draft.reviewFrictionWarnCount = s.reviewFrictionWarnCount
174
+ // The hard-block knobs are nullable (null ⇒ that trigger is off); a per-trigger checkbox is
175
+ // enabled from whether a value is stored, defaulting the input to a sensible starting number.
176
+ draft.reviewFrictionBlockCountEnabled = s.reviewFrictionBlockCount != null
177
+ draft.reviewFrictionBlockCount = s.reviewFrictionBlockCount ?? 10
178
+ draft.reviewFrictionBlockStuckEnabled = s.reviewFrictionBlockStuckMinutes != null
179
+ draft.reviewFrictionBlockStuckMinutes = s.reviewFrictionBlockStuckMinutes ?? 1440
160
180
  }
161
181
 
162
182
  // `store.settings` is always replaced wholesale (store hydrate/update reassign the ref),
@@ -166,6 +186,25 @@ watch(() => store.settings, hydrate, { immediate: true })
166
186
  const saving = ref(false)
167
187
 
168
188
  async function save() {
189
+ // The hard-block triggers only apply in `enforce` mode; a disabled trigger sends null.
190
+ const blockCount =
191
+ draft.reviewFrictionMode === 'enforce' && draft.reviewFrictionBlockCountEnabled
192
+ ? draft.reviewFrictionBlockCount
193
+ : null
194
+ const blockStuckMinutes =
195
+ draft.reviewFrictionMode === 'enforce' && draft.reviewFrictionBlockStuckEnabled
196
+ ? draft.reviewFrictionBlockStuckMinutes
197
+ : null
198
+ // Mirror the backend's enforce-mode validation client-side so the user gets an immediate,
199
+ // localized message instead of the raw 422 (enforce needs at least one hard trigger).
200
+ if (draft.reviewFrictionMode === 'enforce' && blockCount == null && blockStuckMinutes == null) {
201
+ toast.add({
202
+ title: t('settings.workspaceSettings.reviewFriction.needsTrigger'),
203
+ icon: 'i-lucide-triangle-alert',
204
+ color: 'warning',
205
+ })
206
+ return
207
+ }
169
208
  saving.value = true
170
209
  try {
171
210
  await store.update({
@@ -185,6 +224,10 @@ async function save() {
185
224
  storeAgentContext: draft.storeAgentContext,
186
225
  artifactRetentionDays: draft.artifactRetentionDays,
187
226
  kaizenEnabled: draft.kaizenEnabled,
227
+ reviewFrictionMode: draft.reviewFrictionMode,
228
+ reviewFrictionWarnCount: draft.reviewFrictionWarnCount,
229
+ reviewFrictionBlockCount: blockCount,
230
+ reviewFrictionBlockStuckMinutes: blockStuckMinutes,
188
231
  })
189
232
  toast.add({
190
233
  title: t('settings.workspaceSettings.toast.saved'),
@@ -288,6 +331,80 @@ async function save() {
288
331
  </div>
289
332
  </section>
290
333
 
334
+ <!-- Review-debt friction on task creation -->
335
+ <section class="space-y-2">
336
+ <h3 class="text-sm font-semibold text-slate-200">
337
+ {{ t('settings.workspaceSettings.reviewFriction.heading') }}
338
+ </h3>
339
+ <p class="text-[11px] text-slate-400">
340
+ {{ t('settings.workspaceSettings.reviewFriction.body') }}
341
+ </p>
342
+ <label class="block w-64">
343
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">{{
344
+ t('settings.workspaceSettings.reviewFriction.mode')
345
+ }}</span>
346
+ <USelect
347
+ v-model="draft.reviewFrictionMode"
348
+ :items="REVIEW_FRICTION_MODES"
349
+ value-key="value"
350
+ size="sm"
351
+ class="w-full"
352
+ />
353
+ </label>
354
+
355
+ <label v-if="draft.reviewFrictionMode !== 'off'" class="block w-48">
356
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
357
+ {{ t('settings.workspaceSettings.reviewFriction.warnCount') }}
358
+ </span>
359
+ <UInput
360
+ v-model.number="draft.reviewFrictionWarnCount"
361
+ type="number"
362
+ :min="1"
363
+ size="sm"
364
+ />
365
+ </label>
366
+
367
+ <div v-if="draft.reviewFrictionMode === 'enforce'" class="space-y-2">
368
+ <p class="text-[11px] text-slate-400">
369
+ {{ t('settings.workspaceSettings.reviewFriction.enforceHint') }}
370
+ </p>
371
+ <label class="flex items-center gap-2">
372
+ <USwitch v-model="draft.reviewFrictionBlockCountEnabled" size="sm" />
373
+ <span class="text-[13px] text-slate-300">{{
374
+ t('settings.workspaceSettings.reviewFriction.blockCountToggle')
375
+ }}</span>
376
+ </label>
377
+ <label v-if="draft.reviewFrictionBlockCountEnabled" class="block w-48">
378
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
379
+ {{ t('settings.workspaceSettings.reviewFriction.blockCount') }}
380
+ </span>
381
+ <UInput
382
+ v-model.number="draft.reviewFrictionBlockCount"
383
+ type="number"
384
+ :min="1"
385
+ size="sm"
386
+ />
387
+ </label>
388
+ <label class="flex items-center gap-2">
389
+ <USwitch v-model="draft.reviewFrictionBlockStuckEnabled" size="sm" />
390
+ <span class="text-[13px] text-slate-300">{{
391
+ t('settings.workspaceSettings.reviewFriction.blockStuckToggle')
392
+ }}</span>
393
+ </label>
394
+ <label v-if="draft.reviewFrictionBlockStuckEnabled" class="block w-48">
395
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
396
+ {{ t('settings.workspaceSettings.reviewFriction.blockStuckMinutes') }}
397
+ </span>
398
+ <UInput
399
+ v-model.number="draft.reviewFrictionBlockStuckMinutes"
400
+ type="number"
401
+ :min="1"
402
+ size="sm"
403
+ />
404
+ </label>
405
+ </div>
406
+ </section>
407
+
291
408
  <!-- Agent observability -->
292
409
  <section class="space-y-2">
293
410
  <h3 class="text-sm font-semibold text-slate-200">
@@ -54,6 +54,7 @@ export function boardApi({ send, ws }: ApiContext) {
54
54
  pipelineId?: string
55
55
  agentConfig?: Record<string, string>
56
56
  technical?: boolean
57
+ acknowledgeReviewDebt?: boolean
57
58
  },
58
59
  ) => send(addTaskContract, { pathPrefix: ws(workspaceId), pathParams: { blockId }, body }),
59
60
 
@@ -166,6 +166,18 @@ const CONFLICT_INFO: Record<Exclude<ConflictReason, BespokeConflictReason>, Conf
166
166
  run: (ui) => ui.openGitHub(),
167
167
  },
168
168
  },
169
+ // Opt-in review-debt friction. In the normal task-create flow AddTaskModal intercepts these
170
+ // 409s and opens the friction dialog (which can retry with an acknowledgement), so these entries
171
+ // are the last-resort toast fallback for any OTHER caller — a generic, param-free title +
172
+ // description reusing the dialog's own `errors.reviewFriction.*` namespace.
173
+ review_debt_warn: {
174
+ titleKey: 'errors.reviewFriction.warnTitle',
175
+ descriptionKey: 'errors.reviewFriction.warnToast',
176
+ },
177
+ review_debt_blocked: {
178
+ titleKey: 'errors.reviewFriction.blockedTitle',
179
+ descriptionKey: 'errors.reviewFriction.blockedToast',
180
+ },
169
181
  }
170
182
 
171
183
  /**
@@ -0,0 +1,23 @@
1
+ import { computed } from 'vue'
2
+ import { assessReviewFriction } from '@cat-factory/contracts'
3
+ import { useNotificationsStore } from '~/stores/notifications'
4
+ import { useWorkspaceSettingsStore } from '~/stores/workspaceSettings'
5
+
6
+ /**
7
+ * The client-side review-debt verdict, computed from the SAME pure `assessReviewFriction` the
8
+ * backend enforces with, over the workspace snapshot's open notifications + settings. Lets the
9
+ * add-task affordances pre-warn (a debt badge) BEFORE a create is attempted, so the friction
10
+ * dialog is rarely a surprise. The server stays the authority — this is a hint only. See
11
+ * backend/docs/review-debt-friction.md.
12
+ */
13
+ export function useReviewDebt() {
14
+ const notifications = useNotificationsStore()
15
+ const settings = useWorkspaceSettingsStore()
16
+ const verdict = computed(() =>
17
+ assessReviewFriction(notifications.open, settings.settings, Date.now()),
18
+ )
19
+ const active = computed(() => verdict.value.kind !== 'ok')
20
+ const debtCount = computed(() => (verdict.value.kind === 'ok' ? 0 : verdict.value.debt.length))
21
+ const blocked = computed(() => verdict.value.kind === 'block')
22
+ return { verdict, active, debtCount, blocked }
23
+ }
@@ -17,6 +17,7 @@ import DecisionModal from '~/components/panels/DecisionModal.vue'
17
17
  import AgentStepDetail from '~/components/panels/AgentStepDetail.vue'
18
18
  import StepResultViewHost from '~/components/panels/StepResultViewHost.vue'
19
19
  import AddTaskModal from '~/components/board/AddTaskModal.vue'
20
+ import ReviewFrictionDialog from '~/components/board/ReviewFrictionDialog.vue'
20
21
  import CreateInitiativeModal from '~/components/board/CreateInitiativeModal.vue'
21
22
  import GitHubOnboarding from '~/components/github/GitHubOnboarding.vue'
22
23
  import CommandBar from '~/components/layout/CommandBar.vue'
@@ -377,6 +378,7 @@ watch(
377
378
  <AgentStepDetail />
378
379
  <StepResultViewHost />
379
380
  <AddTaskModal />
381
+ <ReviewFrictionDialog v-if="ui.reviewFrictionContext" />
380
382
  <CreateInitiativeModal />
381
383
  <CommandBar />
382
384
  <PersonalCredentialModal />
@@ -73,6 +73,9 @@ export function createBoardMutations(ctx: BoardWriteContext) {
73
73
  agentConfig?: Record<string, string>
74
74
  fragmentIds?: string[]
75
75
  technical?: boolean
76
+ // Opt-in review-debt friction: set on the retry after the human confirms the friction
77
+ // dialog, so a soft `review_debt_warn` 409 is tunnelled through (never a hard block).
78
+ acknowledgeReviewDebt?: boolean
76
79
  },
77
80
  ): Promise<Block | undefined> {
78
81
  if (!getBlock(containerId)) return
@@ -90,6 +93,7 @@ export function createBoardMutations(ctx: BoardWriteContext) {
90
93
  // when a caller doesn't manage fragments at all (then the backend seeds from the service).
91
94
  ...(options?.fragmentIds !== undefined ? { fragmentIds: options.fragmentIds } : {}),
92
95
  ...(options?.technical ? { technical: true } : {}),
96
+ ...(options?.acknowledgeReviewDebt ? { acknowledgeReviewDebt: true } : {}),
93
97
  })
94
98
  upsert(block)
95
99
  return block
@@ -25,6 +25,29 @@ export interface K3sSetupPrefill {
25
25
  insecureSkipTlsVerify?: boolean
26
26
  }
27
27
 
28
+ /** One task waiting on human review, as carried on a review-debt friction 409's details. */
29
+ export interface ReviewDebtRow {
30
+ blockId: string
31
+ /** The task's title, joined in server-side; null when it couldn't be resolved. */
32
+ title: string | null
33
+ /** How long the task has been waiting on review, in minutes. */
34
+ waitingMinutes: number
35
+ }
36
+
37
+ /** The context a review-debt friction dialog renders + acts on (from the parsed 409). */
38
+ export interface ReviewFrictionModalContext {
39
+ /** `warn` = soft friction (the human may proceed); `blocked` = hard block (they may not). */
40
+ kind: 'warn' | 'blocked'
41
+ /** Which hard trigger fired (`count` / `stuck`), for the blocked-tier message. */
42
+ reason?: 'count' | 'stuck'
43
+ /** The count / stuck-minutes threshold that fired, for the message. */
44
+ threshold?: number | null
45
+ /** The waiting tasks, worst-first, each deep-linkable to its block. */
46
+ debt: ReviewDebtRow[]
47
+ /** Retry the create with `acknowledgeReviewDebt` — present only for the soft `warn` tier. */
48
+ onConfirm: (() => void) | null
49
+ }
50
+
28
51
  /** Clears both hub came-from markers; injected into the slices whose `open*` handlers reset them. */
29
52
  type ResetHubReturn = () => void
30
53
 
@@ -167,6 +190,14 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
167
190
  // linked context). The user still confirms pipeline / preset before adding.
168
191
  const addTaskPrefill = ref<AddTaskPrefill | null>(null)
169
192
 
193
+ // Review-debt friction dialog: opened when a task-create request is refused by the opt-in
194
+ // friction gate (`review_debt_warn` / `review_debt_blocked` 409). Carries the parsed conflict
195
+ // details the backend supplied (the waiting tasks + the trigger) so the dialog can list exactly
196
+ // what is in review, and — for the soft `warn` tier — an `onConfirm` the "Create anyway" button
197
+ // runs to retry the create with `acknowledgeReviewDebt`. Null when closed. See
198
+ // backend/docs/review-debt-friction.md.
199
+ const reviewFrictionContext = ref<ReviewFrictionModalContext | null>(null)
200
+
170
201
  // Add-recurring-pipeline modal: the service frame a new recurring pipeline is
171
202
  // being added to, or null when closed (mirrors the add-task flow — a button on
172
203
  // the frame opens it, scoped to that frame).
@@ -232,6 +263,12 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
232
263
  addTaskContainerId.value = null
233
264
  addTaskPrefill.value = null
234
265
  }
266
+ function openReviewFriction(ctx: ReviewFrictionModalContext) {
267
+ reviewFrictionContext.value = ctx
268
+ }
269
+ function closeReviewFriction() {
270
+ reviewFrictionContext.value = null
271
+ }
235
272
  function openAddRecurring(frameId: string) {
236
273
  addRecurringFrameId.value = frameId
237
274
  }
@@ -254,6 +291,7 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
254
291
  taskImport,
255
292
  addTaskContainerId,
256
293
  addTaskPrefill,
294
+ reviewFrictionContext,
257
295
  addRecurringFrameId,
258
296
  createInitiativeFrameId,
259
297
  openDocumentConnect,
@@ -270,6 +308,8 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
270
308
  closeTaskImport,
271
309
  openAddTask,
272
310
  closeAddTask,
311
+ openReviewFriction,
312
+ closeReviewFriction,
273
313
  openAddRecurring,
274
314
  closeAddRecurring,
275
315
  openCreateInitiative,
@@ -13,6 +13,10 @@ const DEFAULTS: WorkspaceSettings = {
13
13
  artifactRetentionDays: 14,
14
14
  kaizenEnabled: true,
15
15
  delegateAgentsToRunnerPool: false,
16
+ reviewFrictionMode: 'off',
17
+ reviewFrictionWarnCount: 3,
18
+ reviewFrictionBlockCount: null,
19
+ reviewFrictionBlockStuckMinutes: null,
16
20
  spendCurrency: null,
17
21
  spendMonthlyLimit: null,
18
22
  }
@@ -76,6 +76,7 @@ export type {
76
76
  WorkspaceAccess,
77
77
  WorkspaceMember,
78
78
  TaskLimitMode,
79
+ ReviewFrictionMode,
79
80
  WorkspaceSettings,
80
81
  UpdateWorkspaceSettingsInput,
81
82
  UserSettings,
@@ -738,6 +738,23 @@
738
738
  "review": "Review",
739
739
  "ralph": "Ralph-Schleife"
740
740
  },
741
+ "reviewFriction": {
742
+ "heading": "Review-Rückstau-Bremse",
743
+ "body": "Erschwert das Anlegen neuer Aufgaben, solange fertige Arbeit auf die menschliche Prüfung wartet. Standardmäßig aus. „Warnen“ fügt eine Bestätigung hinzu; „Erzwingen“ verweigert das Anlegen ab einem harten Limit ganz.",
744
+ "mode": "Modus",
745
+ "modes": {
746
+ "off": "Aus",
747
+ "warn": "Warnen (Bestätigung nötig)",
748
+ "enforce": "Erzwingen (harte Sperre)"
749
+ },
750
+ "warnCount": "Warnen ab (Aufgaben in Prüfung)",
751
+ "enforceHint": "„Erzwingen“ verweigert das Anlegen von Aufgaben. Aktiviere unten mindestens einen harten Auslöser.",
752
+ "blockCountToggle": "Sperren, wenn zu viele Aufgaben in Prüfung sind",
753
+ "blockCount": "Sperren ab (Aufgaben in Prüfung)",
754
+ "blockStuckToggle": "Sperren, wenn eine Aufgabe zu lange wartet",
755
+ "blockStuckMinutes": "Sperren nach (Minuten Wartezeit)",
756
+ "needsTrigger": "Der Modus „Erzwingen“ benötigt mindestens einen aktiven harten Auslöser (eine Anzahl oder eine Wartezeit)."
757
+ },
741
758
  "observability": {
742
759
  "heading": "Agenten-Observability",
743
760
  "body": "Speichere den vollständigen Kontext, der jedem Agenten bereitgestellt wird: die zusammengesetzten Prompts, die eingefügten Best-Practice-Fragmente und den vollständigen Inhalt der in seinen Container injizierten Dateien, sodass er später in der Observability-Ansicht inspiziert werden kann. Die Inhalte werden für denselben Zeitraum wie die LLM-Telemetrie pro Aufruf aufbewahrt. Deaktivieren, um die Speicherung zu stoppen (bestehende Snapshots werden durch die Aufbewahrung bereinigt).",
@@ -4060,6 +4077,22 @@
4060
4077
  "mergeFailed": "Zusammenführen fehlgeschlagen",
4061
4078
  "restartFailed": "Neustart fehlgeschlagen"
4062
4079
  },
4080
+ "reviewFriction": {
4081
+ "warnTitle": "Aufgaben warten auf Prüfung",
4082
+ "blockedTitle": "Zu viele Aufgaben warten auf Prüfung",
4083
+ "warnToast": "Einige Aufgaben warten auf die menschliche Prüfung. Prüfe sie, bevor du mehr Arbeit anlegst.",
4084
+ "blockedToast": "Das Anlegen von Aufgaben ist gesperrt, bis der Prüf-Rückstau abgebaut ist.",
4085
+ "warnBody": "{count} Aufgabe(n) warten auf die menschliche Prüfung. Prüfe sie, bevor du mehr Arbeit anlegst.",
4086
+ "blockedCountBody": "Anlegen gesperrt: {count} Aufgabe(n) warten auf die menschliche Prüfung (Limit {threshold}). Baue zuerst den Prüf-Rückstau ab.",
4087
+ "blockedStuckBody": "Anlegen gesperrt: Eine Aufgabe wartet seit über {minutes} Minute(n) auf die menschliche Prüfung. Baue zuerst den Prüf-Rückstau ab.",
4088
+ "waitingHeading": "Warten auf Prüfung",
4089
+ "waiting": "{minutes} Min. Wartezeit",
4090
+ "untitled": "Aufgabe ohne Titel",
4091
+ "close": "Schließen",
4092
+ "createAnyway": "Trotzdem anlegen",
4093
+ "goReview": "Zur Prüfung",
4094
+ "badge": "{count} Aufgabe(n) warten auf Prüfung"
4095
+ },
4063
4096
  "conflict": {
4064
4097
  "title": {
4065
4098
  "dependencies_unmet": "Durch Abhängigkeiten blockiert",
@@ -493,6 +493,22 @@
493
493
  "mergeFailed": "Failed to merge",
494
494
  "restartFailed": "Failed to restart"
495
495
  },
496
+ "reviewFriction": {
497
+ "warnTitle": "Tasks waiting on review",
498
+ "blockedTitle": "Too many tasks waiting on review",
499
+ "warnToast": "Some tasks are waiting on human review. Consider reviewing them before creating more work.",
500
+ "blockedToast": "Task creation is blocked until the review queue is worked down.",
501
+ "warnBody": "{count} task(s) are waiting on human review. Consider reviewing them before creating more work.",
502
+ "blockedCountBody": "Task creation is blocked: {count} task(s) are waiting on human review (limit {threshold}). Work down the review queue first.",
503
+ "blockedStuckBody": "Task creation is blocked: a task has been waiting on human review for over {minutes} minute(s). Work down the review queue first.",
504
+ "waitingHeading": "Waiting on review",
505
+ "waiting": "{minutes} min waiting",
506
+ "untitled": "Untitled task",
507
+ "close": "Close",
508
+ "createAnyway": "Create anyway",
509
+ "goReview": "Go review",
510
+ "badge": "{count} task(s) waiting on review"
511
+ },
496
512
  "conflict": {
497
513
  "title": {
498
514
  "dependencies_unmet": "Blocked by dependencies",
@@ -2757,6 +2773,23 @@
2757
2773
  "review": "review",
2758
2774
  "ralph": "Ralph loop"
2759
2775
  },
2776
+ "reviewFriction": {
2777
+ "heading": "Review-debt friction",
2778
+ "body": "Discourage authoring new tasks while finished work waits on human review. Off by default. Warn adds a confirm-to-proceed step; Enforce also refuses task creation outright past a hard limit.",
2779
+ "mode": "Mode",
2780
+ "modes": {
2781
+ "off": "Off",
2782
+ "warn": "Warn (confirm to proceed)",
2783
+ "enforce": "Enforce (hard block)"
2784
+ },
2785
+ "warnCount": "Warn at (tasks in review)",
2786
+ "enforceHint": "Enforce refuses task creation. Enable at least one hard trigger below.",
2787
+ "blockCountToggle": "Block when too many tasks are in review",
2788
+ "blockCount": "Block at (tasks in review)",
2789
+ "blockStuckToggle": "Block when a task has waited too long",
2790
+ "blockStuckMinutes": "Block after (minutes waiting)",
2791
+ "needsTrigger": "Enforce mode needs at least one hard trigger enabled (a count or a wait time)."
2792
+ },
2760
2793
  "observability": {
2761
2794
  "heading": "Agent observability",
2762
2795
  "body": "Store the complete context provided to each agent: the composed prompts, the best-practice fragments folded in, and the full content of the files injected into its container, so it can be inspected later in the observability view. The bodies are kept for the same window as the per-call LLM telemetry. Turn off to stop storing it (existing snapshots are pruned by retention).",
@@ -457,6 +457,22 @@
457
457
  "mergeFailed": "No se pudo fusionar",
458
458
  "restartFailed": "No se pudo reiniciar"
459
459
  },
460
+ "reviewFriction": {
461
+ "warnTitle": "Tareas esperando revisión",
462
+ "blockedTitle": "Demasiadas tareas esperando revisión",
463
+ "warnToast": "Algunas tareas esperan revisión humana. Considera revisarlas antes de crear más trabajo.",
464
+ "blockedToast": "La creación de tareas está bloqueada hasta reducir la cola de revisión.",
465
+ "warnBody": "{count} tarea(s) esperan revisión humana. Considera revisarlas antes de crear más trabajo.",
466
+ "blockedCountBody": "Creación bloqueada: {count} tarea(s) esperan revisión humana (límite {threshold}). Reduce primero la cola de revisión.",
467
+ "blockedStuckBody": "Creación bloqueada: una tarea lleva más de {minutes} minuto(s) esperando revisión humana. Reduce primero la cola de revisión.",
468
+ "waitingHeading": "Esperando revisión",
469
+ "waiting": "{minutes} min esperando",
470
+ "untitled": "Tarea sin título",
471
+ "close": "Cerrar",
472
+ "createAnyway": "Crear de todos modos",
473
+ "goReview": "Ir a revisar",
474
+ "badge": "{count} tarea(s) esperando revisión"
475
+ },
460
476
  "conflict": {
461
477
  "title": {
462
478
  "dependencies_unmet": "Bloqueado por dependencias",
@@ -2569,6 +2585,23 @@
2569
2585
  "review": "revisión",
2570
2586
  "ralph": "Bucle Ralph"
2571
2587
  },
2588
+ "reviewFriction": {
2589
+ "heading": "Fricción por revisión pendiente",
2590
+ "body": "Desalienta crear tareas nuevas mientras el trabajo terminado espera revisión humana. Desactivado por defecto. «Avisar» añade una confirmación; «Aplicar» rechaza la creación al superar un límite estricto.",
2591
+ "mode": "Modo",
2592
+ "modes": {
2593
+ "off": "Desactivado",
2594
+ "warn": "Avisar (confirmar para continuar)",
2595
+ "enforce": "Aplicar (bloqueo estricto)"
2596
+ },
2597
+ "warnCount": "Avisar a partir de (tareas en revisión)",
2598
+ "enforceHint": "«Aplicar» rechaza la creación de tareas. Activa al menos un disparador estricto abajo.",
2599
+ "blockCountToggle": "Bloquear cuando haya demasiadas tareas en revisión",
2600
+ "blockCount": "Bloquear a partir de (tareas en revisión)",
2601
+ "blockStuckToggle": "Bloquear cuando una tarea lleve esperando demasiado",
2602
+ "blockStuckMinutes": "Bloquear tras (minutos de espera)",
2603
+ "needsTrigger": "El modo «Aplicar» necesita al menos un disparador estricto activo (un recuento o un tiempo de espera)."
2604
+ },
2572
2605
  "observability": {
2573
2606
  "heading": "Observabilidad del agente",
2574
2607
  "body": "Almacena el contexto completo proporcionado a cada agente: los prompts compuestos, los fragmentos de buenas prácticas incorporados y el contenido íntegro de los archivos inyectados en su contenedor, para poder inspeccionarlo después en la vista de observabilidad. Los cuerpos se conservan durante la misma ventana que la telemetría de LLM por llamada. Desactívalo para dejar de almacenarlo (las instantáneas existentes se eliminan por retención).",
@@ -457,6 +457,22 @@
457
457
  "mergeFailed": "Échec de la fusion",
458
458
  "restartFailed": "Échec du redémarrage"
459
459
  },
460
+ "reviewFriction": {
461
+ "warnTitle": "Tâches en attente de revue",
462
+ "blockedTitle": "Trop de tâches en attente de revue",
463
+ "warnToast": "Des tâches attendent une revue humaine. Pensez à les revoir avant de créer plus de travail.",
464
+ "blockedToast": "La création de tâches est bloquée jusqu’à résorption de la file de revue.",
465
+ "warnBody": "{count} tâche(s) attendent une revue humaine. Pensez à les revoir avant de créer plus de travail.",
466
+ "blockedCountBody": "Création bloquée : {count} tâche(s) attendent une revue humaine (limite {threshold}). Résorbez d’abord la file de revue.",
467
+ "blockedStuckBody": "Création bloquée : une tâche attend une revue humaine depuis plus de {minutes} minute(s). Résorbez d’abord la file de revue.",
468
+ "waitingHeading": "En attente de revue",
469
+ "waiting": "{minutes} min d’attente",
470
+ "untitled": "Tâche sans titre",
471
+ "close": "Fermer",
472
+ "createAnyway": "Créer quand même",
473
+ "goReview": "Aller revoir",
474
+ "badge": "{count} tâche(s) en attente de revue"
475
+ },
460
476
  "conflict": {
461
477
  "title": {
462
478
  "dependencies_unmet": "Bloqué par des dépendances",
@@ -2569,6 +2585,23 @@
2569
2585
  "review": "revue",
2570
2586
  "ralph": "Boucle Ralph"
2571
2587
  },
2588
+ "reviewFriction": {
2589
+ "heading": "Friction de dette de revue",
2590
+ "body": "Décourage la création de nouvelles tâches tant que du travail terminé attend une revue humaine. Désactivé par défaut. « Avertir » ajoute une confirmation ; « Imposer » refuse la création au-delà d’une limite stricte.",
2591
+ "mode": "Mode",
2592
+ "modes": {
2593
+ "off": "Désactivé",
2594
+ "warn": "Avertir (confirmer pour continuer)",
2595
+ "enforce": "Imposer (blocage strict)"
2596
+ },
2597
+ "warnCount": "Avertir à partir de (tâches en revue)",
2598
+ "enforceHint": "« Imposer » refuse la création de tâches. Activez au moins un déclencheur strict ci-dessous.",
2599
+ "blockCountToggle": "Bloquer quand trop de tâches sont en revue",
2600
+ "blockCount": "Bloquer à partir de (tâches en revue)",
2601
+ "blockStuckToggle": "Bloquer quand une tâche attend depuis trop longtemps",
2602
+ "blockStuckMinutes": "Bloquer après (minutes d’attente)",
2603
+ "needsTrigger": "Le mode « Imposer » nécessite au moins un déclencheur strict activé (un nombre ou un temps d’attente)."
2604
+ },
2572
2605
  "observability": {
2573
2606
  "heading": "Observabilité de l'agent",
2574
2607
  "body": "Stockez le contexte complet fourni à chaque agent : les prompts composés, les fragments de bonnes pratiques intégrés et le contenu intégral des fichiers injectés dans son conteneur, afin de pouvoir l'inspecter plus tard dans la vue d'observabilité. Les corps sont conservés pendant la même fenêtre que la télémétrie LLM par appel. Désactivez pour cesser de le stocker (les instantanés existants sont supprimés par la rétention).",
@@ -457,6 +457,22 @@
457
457
  "mergeFailed": "המיזוג נכשל",
458
458
  "restartFailed": "ההפעלה מחדש נכשלה"
459
459
  },
460
+ "reviewFriction": {
461
+ "warnTitle": "משימות ממתינות לסקירה",
462
+ "blockedTitle": "יותר מדי משימות ממתינות לסקירה",
463
+ "warnToast": "כמה משימות ממתינות לסקירה אנושית. שקול לסקור אותן לפני יצירת עבודה נוספת.",
464
+ "blockedToast": "יצירת משימות חסומה עד לצמצום תור הסקירה.",
465
+ "warnBody": "{count} משימות ממתינות לסקירה אנושית. שקול לסקור אותן לפני יצירת עבודה נוספת.",
466
+ "blockedCountBody": "היצירה חסומה: {count} משימות ממתינות לסקירה אנושית (מגבלה {threshold}). צמצם תחילה את תור הסקירה.",
467
+ "blockedStuckBody": "היצירה חסומה: משימה ממתינה לסקירה אנושית יותר מ־{minutes} דקות. צמצם תחילה את תור הסקירה.",
468
+ "waitingHeading": "ממתין לסקירה",
469
+ "waiting": "{minutes} דק׳ בהמתנה",
470
+ "untitled": "משימה ללא כותרת",
471
+ "close": "סגור",
472
+ "createAnyway": "צור בכל זאת",
473
+ "goReview": "עבור לסקירה",
474
+ "badge": "{count} משימות ממתינות לסקירה"
475
+ },
460
476
  "conflict": {
461
477
  "title": {
462
478
  "dependencies_unmet": "חסום על ידי תלויות",
@@ -2690,6 +2706,23 @@
2690
2706
  "review": "סקירה",
2691
2707
  "ralph": "לולאת Ralph"
2692
2708
  },
2709
+ "reviewFriction": {
2710
+ "heading": "חיכוך חוב סקירה",
2711
+ "body": "מרתיע מיצירת משימות חדשות בזמן שעבודה שהושלמה ממתינה לסקירה אנושית. כבוי כברירת מחדל. „אזהרה” מוסיפה אישור; „אכיפה” מסרבת ליצירה מעבר לגבול קשיח.",
2712
+ "mode": "מצב",
2713
+ "modes": {
2714
+ "off": "כבוי",
2715
+ "warn": "אזהרה (אישור כדי להמשיך)",
2716
+ "enforce": "אכיפה (חסימה קשיחה)"
2717
+ },
2718
+ "warnCount": "אזהרה החל מ־ (משימות בסקירה)",
2719
+ "enforceHint": "„אכיפה” מסרבת ליצירת משימות. הפעל לפחות טריגר קשיח אחד למטה.",
2720
+ "blockCountToggle": "חסום כשיש יותר מדי משימות בסקירה",
2721
+ "blockCount": "חסום החל מ־ (משימות בסקירה)",
2722
+ "blockStuckToggle": "חסום כשמשימה ממתינה יותר מדי זמן",
2723
+ "blockStuckMinutes": "חסום לאחר (דקות המתנה)",
2724
+ "needsTrigger": "מצב „אכיפה” דורש לפחות טריגר קשיח פעיל אחד (מספר או זמן המתנה)."
2725
+ },
2693
2726
  "observability": {
2694
2727
  "heading": "תצפיתיות סוכנים",
2695
2728
  "body": "אחסן את ההקשר המלא שניתן לכל סוכן: ההנחיות המורכבות, מקטעי שיטות העבודה המומלצות שמשולבים, והתוכן המלא של הקבצים שמוזרקים לקונטיינר שלו, כך שניתן יהיה לבדוק אותו מאוחר יותר בתצוגת התצפיתיות. הגופים נשמרים לאותו חלון כמו טלמטריית ה-LLM לכל קריאה. כבה כדי להפסיק לאחסן אותו (תצלומים קיימים נגזמים לפי תקופת השמירה).",
@@ -738,6 +738,23 @@
738
738
  "review": "revisione",
739
739
  "ralph": "Ciclo Ralph"
740
740
  },
741
+ "reviewFriction": {
742
+ "heading": "Attrito per revisioni in sospeso",
743
+ "body": "Scoraggia la creazione di nuove attività mentre il lavoro completato attende la revisione umana. Disattivato per impostazione predefinita. «Avvisa» aggiunge una conferma; «Imponi» rifiuta la creazione oltre un limite rigido.",
744
+ "mode": "Modalità",
745
+ "modes": {
746
+ "off": "Disattivato",
747
+ "warn": "Avvisa (conferma per procedere)",
748
+ "enforce": "Imponi (blocco rigido)"
749
+ },
750
+ "warnCount": "Avvisa da (attività in revisione)",
751
+ "enforceHint": "«Imponi» rifiuta la creazione di attività. Attiva almeno un trigger rigido qui sotto.",
752
+ "blockCountToggle": "Blocca quando troppe attività sono in revisione",
753
+ "blockCount": "Blocca da (attività in revisione)",
754
+ "blockStuckToggle": "Blocca quando un’attività attende da troppo tempo",
755
+ "blockStuckMinutes": "Blocca dopo (minuti di attesa)",
756
+ "needsTrigger": "La modalità «Imponi» richiede almeno un trigger rigido attivo (un conteggio o un tempo di attesa)."
757
+ },
741
758
  "observability": {
742
759
  "heading": "Osservabilita degli agenti",
743
760
  "body": "Memorizza il contesto completo fornito a ogni agente: i prompt composti, i frammenti di best practice inclusi e il contenuto completo dei file iniettati nel suo container, cosicche possa essere ispezionato in seguito nella vista di osservabilita. I corpi vengono conservati per la stessa finestra della telemetria LLM per chiamata. Disattiva per interrompere la memorizzazione (gli snapshot esistenti vengono eliminati dalla retention).",
@@ -4060,6 +4077,22 @@
4060
4077
  "mergeFailed": "Merge non riuscito",
4061
4078
  "restartFailed": "Riavvio non riuscito"
4062
4079
  },
4080
+ "reviewFriction": {
4081
+ "warnTitle": "Attività in attesa di revisione",
4082
+ "blockedTitle": "Troppe attività in attesa di revisione",
4083
+ "warnToast": "Alcune attività attendono la revisione umana. Valuta di revisionarle prima di creare altro lavoro.",
4084
+ "blockedToast": "La creazione di attività è bloccata finché non si riduce la coda di revisione.",
4085
+ "warnBody": "{count} attività attendono la revisione umana. Valuta di revisionarle prima di creare altro lavoro.",
4086
+ "blockedCountBody": "Creazione bloccata: {count} attività attendono la revisione umana (limite {threshold}). Riduci prima la coda di revisione.",
4087
+ "blockedStuckBody": "Creazione bloccata: un’attività attende la revisione umana da oltre {minutes} minuto/i. Riduci prima la coda di revisione.",
4088
+ "waitingHeading": "In attesa di revisione",
4089
+ "waiting": "{minutes} min di attesa",
4090
+ "untitled": "Attività senza titolo",
4091
+ "close": "Chiudi",
4092
+ "createAnyway": "Crea comunque",
4093
+ "goReview": "Vai a revisionare",
4094
+ "badge": "{count} attività in attesa di revisione"
4095
+ },
4063
4096
  "conflict": {
4064
4097
  "title": {
4065
4098
  "dependencies_unmet": "Bloccato dalle dipendenze",
@@ -457,6 +457,22 @@
457
457
  "mergeFailed": "マージに失敗しました",
458
458
  "restartFailed": "再起動に失敗しました"
459
459
  },
460
+ "reviewFriction": {
461
+ "warnTitle": "レビュー待ちのタスク",
462
+ "blockedTitle": "レビュー待ちのタスクが多すぎます",
463
+ "warnToast": "一部のタスクが人によるレビューを待っています。新しい作業を作成する前にレビューを検討してください。",
464
+ "blockedToast": "レビュー待ち行列が解消されるまで、タスクの作成はブロックされます。",
465
+ "warnBody": "{count} 件のタスクが人によるレビューを待っています。新しい作業を作成する前にレビューを検討してください。",
466
+ "blockedCountBody": "作成がブロックされました:{count} 件のタスクが人によるレビューを待っています(上限 {threshold})。先にレビュー待ち行列を解消してください。",
467
+ "blockedStuckBody": "作成がブロックされました:あるタスクが {minutes} 分以上、人によるレビューを待っています。先にレビュー待ち行列を解消してください。",
468
+ "waitingHeading": "レビュー待ち",
469
+ "waiting": "待機 {minutes} 分",
470
+ "untitled": "無題のタスク",
471
+ "close": "閉じる",
472
+ "createAnyway": "それでも作成",
473
+ "goReview": "レビューへ移動",
474
+ "badge": "{count} 件のタスクがレビュー待ち"
475
+ },
460
476
  "conflict": {
461
477
  "title": {
462
478
  "dependencies_unmet": "依存関係によりブロック中",
@@ -2691,6 +2707,23 @@
2691
2707
  "review": "レビュー",
2692
2708
  "ralph": "Ralph ループ"
2693
2709
  },
2710
+ "reviewFriction": {
2711
+ "heading": "レビュー滞留の摩擦",
2712
+ "body": "完了した作業が人によるレビューを待っている間、新しいタスクの作成を抑制します。既定ではオフ。「警告」は確認ステップを追加し、「強制」は厳格な上限を超えると作成を完全に拒否します。",
2713
+ "mode": "モード",
2714
+ "modes": {
2715
+ "off": "オフ",
2716
+ "warn": "警告(確認して続行)",
2717
+ "enforce": "強制(ハードブロック)"
2718
+ },
2719
+ "warnCount": "警告するしきい値(レビュー中のタスク数)",
2720
+ "enforceHint": "「強制」はタスク作成を拒否します。下で少なくとも1つのハードトリガーを有効にしてください。",
2721
+ "blockCountToggle": "レビュー中のタスクが多すぎる場合にブロック",
2722
+ "blockCount": "ブロックするしきい値(レビュー中のタスク数)",
2723
+ "blockStuckToggle": "タスクの待機が長すぎる場合にブロック",
2724
+ "blockStuckMinutes": "ブロックするまでの時間(待機分数)",
2725
+ "needsTrigger": "「強制」モードには、有効なハードトリガーが少なくとも1つ必要です(件数または待機時間)。"
2726
+ },
2694
2727
  "observability": {
2695
2728
  "heading": "エージェントの可観測性",
2696
2729
  "body": "各エージェントに提供された完全なコンテキストを保存します。構成されたプロンプト、折り込まれたベストプラクティスのフラグメント、コンテナに注入されたファイルの全内容を保存し、後から可観測性ビューで確認できます。本文は呼び出しごとの LLM テレメトリと同じ期間だけ保持されます。オフにすると保存を停止します (既存のスナップショットは保持期限により削除されます)。",
@@ -457,6 +457,22 @@
457
457
  "mergeFailed": "Nie udało się scalić",
458
458
  "restartFailed": "Nie udało się ponownie uruchomić"
459
459
  },
460
+ "reviewFriction": {
461
+ "warnTitle": "Zadania czekające na przegląd",
462
+ "blockedTitle": "Zbyt wiele zadań czeka na przegląd",
463
+ "warnToast": "Niektóre zadania czekają na przegląd przez człowieka. Rozważ ich przejrzenie przed tworzeniem kolejnej pracy.",
464
+ "blockedToast": "Tworzenie zadań jest zablokowane do czasu zmniejszenia kolejki przeglądów.",
465
+ "warnBody": "{count} zadań czeka na przegląd przez człowieka. Rozważ ich przejrzenie przed tworzeniem kolejnej pracy.",
466
+ "blockedCountBody": "Tworzenie zablokowane: {count} zadań czeka na przegląd przez człowieka (limit {threshold}). Najpierw zmniejsz kolejkę przeglądów.",
467
+ "blockedStuckBody": "Tworzenie zablokowane: zadanie czeka na przegląd przez człowieka od ponad {minutes} minut(y). Najpierw zmniejsz kolejkę przeglądów.",
468
+ "waitingHeading": "Oczekujące na przegląd",
469
+ "waiting": "{minutes} min oczekiwania",
470
+ "untitled": "Zadanie bez tytułu",
471
+ "close": "Zamknij",
472
+ "createAnyway": "Utwórz mimo to",
473
+ "goReview": "Przejdź do przeglądu",
474
+ "badge": "{count} zadań czeka na przegląd"
475
+ },
460
476
  "conflict": {
461
477
  "title": {
462
478
  "dependencies_unmet": "Zablokowane przez zależności",
@@ -2569,6 +2585,23 @@
2569
2585
  "review": "przegląd",
2570
2586
  "ralph": "Pętla Ralph"
2571
2587
  },
2588
+ "reviewFriction": {
2589
+ "heading": "Hamulec długu przeglądów",
2590
+ "body": "Zniechęca do tworzenia nowych zadań, gdy ukończona praca czeka na przegląd przez człowieka. Domyślnie wyłączone. „Ostrzegaj” dodaje potwierdzenie; „Wymuszaj” całkowicie odmawia utworzenia po przekroczeniu twardego limitu.",
2591
+ "mode": "Tryb",
2592
+ "modes": {
2593
+ "off": "Wyłączone",
2594
+ "warn": "Ostrzegaj (potwierdź, aby kontynuować)",
2595
+ "enforce": "Wymuszaj (twarda blokada)"
2596
+ },
2597
+ "warnCount": "Ostrzegaj od (zadań w przeglądzie)",
2598
+ "enforceHint": "„Wymuszaj” odmawia tworzenia zadań. Włącz poniżej co najmniej jeden twardy wyzwalacz.",
2599
+ "blockCountToggle": "Blokuj, gdy zbyt wiele zadań jest w przeglądzie",
2600
+ "blockCount": "Blokuj od (zadań w przeglądzie)",
2601
+ "blockStuckToggle": "Blokuj, gdy zadanie czeka zbyt długo",
2602
+ "blockStuckMinutes": "Blokuj po (minutach oczekiwania)",
2603
+ "needsTrigger": "Tryb „Wymuszaj” wymaga włączenia co najmniej jednego twardego wyzwalacza (liczby lub czasu oczekiwania)."
2604
+ },
2572
2605
  "observability": {
2573
2606
  "heading": "Obserwowalność agenta",
2574
2607
  "body": "Przechowuj pełny kontekst przekazany każdemu agentowi: złożone prompty, włączone fragmenty dobrych praktyk oraz pełną treść plików wstrzykniętych do jego kontenera, aby można było je później sprawdzić w widoku obserwowalności. Treści są przechowywane przez to samo okno co telemetria LLM dla poszczególnych wywołań. Wyłącz, aby przestać je przechowywać (istniejące migawki są usuwane przez retencję).",
@@ -457,6 +457,22 @@
457
457
  "mergeFailed": "Birleştirilemedi",
458
458
  "restartFailed": "Yeniden başlatılamadı"
459
459
  },
460
+ "reviewFriction": {
461
+ "warnTitle": "Görevler incelemeyi bekliyor",
462
+ "blockedTitle": "Çok fazla görev incelemeyi bekliyor",
463
+ "warnToast": "Bazı görevler insan incelemesini bekliyor. Daha fazla iş oluşturmadan önce onları incelemeyi düşünün.",
464
+ "blockedToast": "İnceleme kuyruğu azaltılana kadar görev oluşturma engellendi.",
465
+ "warnBody": "{count} görev insan incelemesini bekliyor. Daha fazla iş oluşturmadan önce onları incelemeyi düşünün.",
466
+ "blockedCountBody": "Oluşturma engellendi: {count} görev insan incelemesini bekliyor (sınır {threshold}). Önce inceleme kuyruğunu azaltın.",
467
+ "blockedStuckBody": "Oluşturma engellendi: bir görev {minutes} dakikadan uzun süredir insan incelemesini bekliyor. Önce inceleme kuyruğunu azaltın.",
468
+ "waitingHeading": "İncelemeyi bekliyor",
469
+ "waiting": "{minutes} dk bekliyor",
470
+ "untitled": "Başlıksız görev",
471
+ "close": "Kapat",
472
+ "createAnyway": "Yine de oluştur",
473
+ "goReview": "İncelemeye git",
474
+ "badge": "{count} görev incelemeyi bekliyor"
475
+ },
460
476
  "conflict": {
461
477
  "title": {
462
478
  "dependencies_unmet": "Bağımlılıklar tarafından engellendi",
@@ -2691,6 +2707,23 @@
2691
2707
  "review": "inceleme",
2692
2708
  "ralph": "Ralph döngüsü"
2693
2709
  },
2710
+ "reviewFriction": {
2711
+ "heading": "İnceleme borcu sürtünmesi",
2712
+ "body": "Tamamlanan iş insan incelemesini beklerken yeni görev oluşturmayı caydırır. Varsayılan olarak kapalı. «Uyar» bir onay adımı ekler; «Zorla» katı bir sınırın ötesinde oluşturmayı tümüyle reddeder.",
2713
+ "mode": "Mod",
2714
+ "modes": {
2715
+ "off": "Kapalı",
2716
+ "warn": "Uyar (devam için onayla)",
2717
+ "enforce": "Zorla (katı engel)"
2718
+ },
2719
+ "warnCount": "Şu sayıda uyar (incelemedeki görevler)",
2720
+ "enforceHint": "«Zorla» görev oluşturmayı reddeder. Aşağıda en az bir katı tetikleyici etkinleştirin.",
2721
+ "blockCountToggle": "İncelemede çok fazla görev olduğunda engelle",
2722
+ "blockCount": "Şu sayıda engelle (incelemedeki görevler)",
2723
+ "blockStuckToggle": "Bir görev çok uzun süre beklediğinde engelle",
2724
+ "blockStuckMinutes": "Şu süreden sonra engelle (dakika bekleme)",
2725
+ "needsTrigger": "«Zorla» modu en az bir etkin katı tetikleyici gerektirir (bir sayı veya bir bekleme süresi)."
2726
+ },
2694
2727
  "observability": {
2695
2728
  "heading": "Agent gözlemlenebilirliği",
2696
2729
  "body": "Her agent'a sağlanan tüm bağlamı saklayın: oluşturulan istemler, içine katlanan en iyi uygulama parçaları ve konteynerine enjekte edilen dosyaların tam içeriği; böylece daha sonra gözlemlenebilirlik görünümünde incelenebilir. Gövdeler, çağrı başına LLM telemetrisi ile aynı süre boyunca tutulur. Saklamayı durdurmak için kapatın (mevcut anlık görüntüler saklama ilkesiyle temizlenir).",
@@ -457,6 +457,22 @@
457
457
  "mergeFailed": "Не вдалося злити",
458
458
  "restartFailed": "Не вдалося перезапустити"
459
459
  },
460
+ "reviewFriction": {
461
+ "warnTitle": "Завдання чекають на рев’ю",
462
+ "blockedTitle": "Забагато завдань чекають на рев’ю",
463
+ "warnToast": "Деякі завдання чекають на рев’ю людиною. Розгляньте можливість переглянути їх, перш ніж створювати більше роботи.",
464
+ "blockedToast": "Створення завдань заблоковано, доки не зменшиться черга рев’ю.",
465
+ "warnBody": "{count} завдань(ня) чекають на рев’ю людиною. Розгляньте можливість переглянути їх, перш ніж створювати більше роботи.",
466
+ "blockedCountBody": "Створення заблоковано: {count} завдань(ня) чекають на рев’ю людиною (ліміт {threshold}). Спершу зменшіть чергу рев’ю.",
467
+ "blockedStuckBody": "Створення заблоковано: завдання чекає на рев’ю людиною понад {minutes} хвилин(и). Спершу зменшіть чергу рев’ю.",
468
+ "waitingHeading": "Очікують на рев’ю",
469
+ "waiting": "{minutes} хв очікування",
470
+ "untitled": "Завдання без назви",
471
+ "close": "Закрити",
472
+ "createAnyway": "Усе одно створити",
473
+ "goReview": "Перейти до рев’ю",
474
+ "badge": "{count} завдань(ня) чекають на рев’ю"
475
+ },
460
476
  "conflict": {
461
477
  "title": {
462
478
  "dependencies_unmet": "Заблоковано залежностями",
@@ -2569,6 +2585,23 @@
2569
2585
  "review": "огляд",
2570
2586
  "ralph": "Цикл Ralph"
2571
2587
  },
2588
+ "reviewFriction": {
2589
+ "heading": "Тертя через борг рев’ю",
2590
+ "body": "Ускладнює створення нових завдань, поки завершена робота чекає на рев’ю людиною. Типово вимкнено. «Попереджати» додає підтвердження; «Примусово» повністю відмовляє у створенні після досягнення жорсткого ліміту.",
2591
+ "mode": "Режим",
2592
+ "modes": {
2593
+ "off": "Вимкнено",
2594
+ "warn": "Попереджати (підтвердити, щоб продовжити)",
2595
+ "enforce": "Примусово (жорстке блокування)"
2596
+ },
2597
+ "warnCount": "Попереджати від (завдань на рев’ю)",
2598
+ "enforceHint": "«Примусово» відмовляє у створенні завдань. Увімкніть принаймні один жорсткий тригер нижче.",
2599
+ "blockCountToggle": "Блокувати, коли забагато завдань на рев’ю",
2600
+ "blockCount": "Блокувати від (завдань на рев’ю)",
2601
+ "blockStuckToggle": "Блокувати, коли завдання чекає надто довго",
2602
+ "blockStuckMinutes": "Блокувати після (хвилин очікування)",
2603
+ "needsTrigger": "Режим «Примусово» потребує принаймні одного увімкненого жорсткого тригера (кількості або часу очікування)."
2604
+ },
2572
2605
  "observability": {
2573
2606
  "heading": "Спостережуваність агента",
2574
2607
  "body": "Зберігайте повний контекст, наданий кожному агенту: складені промпти, вбудовані фрагменти найкращих практик і повний вміст файлів, впроваджених у його контейнер, щоб згодом їх можна було переглянути у вікні спостережуваності. Тіла зберігаються протягом того самого вікна, що й телеметрія LLM для кожного виклику. Вимкніть, щоб припинити зберігання (наявні знімки видаляються за політикою зберігання).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.150.1",
3
+ "version": "0.151.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.158.0"
43
+ "@cat-factory/contracts": "0.159.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",