@cat-factory/app 0.87.5 → 0.88.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.
@@ -5,7 +5,8 @@
5
5
  // task block inside the frame that the schedule re-runs. When the Tech-debt
6
6
  // pipeline is picked, the workspace issue-tracker choice is surfaced inline (it is
7
7
  // where that pipeline files its ticket) and saved alongside.
8
- import type { Recurrence, ScheduleTemplate } from '~/types/recurring'
8
+ import type { IssueIntakeConfig, Recurrence, ScheduleTemplate } from '~/types/recurring'
9
+ import type { TaskSourceKind } from '~/types/domain'
9
10
  import { pipelineAllowedForSchedule } from '~/utils/pipeline'
10
11
 
11
12
  const ui = useUiStore()
@@ -13,6 +14,7 @@ const board = useBoardStore()
13
14
  const pipelines = usePipelinesStore()
14
15
  const recurring = useRecurringPipelinesStore()
15
16
  const tracker = useTrackerStore()
17
+ const tasks = useTasksStore()
16
18
  const toast = useToast()
17
19
  const { t } = useI18n()
18
20
 
@@ -41,6 +43,17 @@ const trackerKind = ref<'github' | 'jira' | 'linear' | null>(null)
41
43
  const jiraProjectKey = ref('')
42
44
  const linearTeamId = ref('')
43
45
 
46
+ // Issue-intake config (only relevant when the picked pipeline has a `bug-intake` step). Which
47
+ // tracker board + predicates a recurring bug-triage run pulls its one issue from, per-schedule.
48
+ const intakeSource = ref<TaskSourceKind | null>(null)
49
+ const intakeJiraProjectKey = ref('')
50
+ const intakeLinearTeamId = ref('')
51
+ const intakeGithubRepo = ref('')
52
+ const intakeTitleFragment = ref('')
53
+ const intakeLabels = ref('') // comma-separated in the UI, sent as an array
54
+ const intakeIssueType = ref('')
55
+ const intakeInProgressLabel = ref('')
56
+
44
57
  function defaultRecurrence(): Recurrence {
45
58
  return {
46
59
  intervalHours: 168, // weekly
@@ -77,6 +90,19 @@ const template = computed<ScheduleTemplate>(() => {
77
90
  })
78
91
  const isTechDebt = computed(() => template.value === 'tech-debt')
79
92
 
93
+ // A pipeline whose ENABLED steps include `bug-intake` pulls its work from the tracker board, so
94
+ // the intake config is surfaced + required. Mirrors the backend `pipelineHasEnabledBugIntake`
95
+ // (a disabled step imposes nothing), so the modal doesn't demand config for a step that won't run.
96
+ const isBugIntake = computed(() => {
97
+ const pipeline = selectedPipeline.value
98
+ if (!pipeline) return false
99
+ return pipeline.agentKinds.some(
100
+ (kind, i) => kind === 'bug-intake' && pipeline.enabled?.[i] !== false,
101
+ )
102
+ })
103
+ // Sources that can back intake right now (connected / App-installed AND enabled).
104
+ const intakeSources = computed(() => tasks.offeredSources)
105
+
80
106
  watch(open, (isOpen) => {
81
107
  if (!isOpen) return
82
108
  name.value = ''
@@ -92,9 +118,62 @@ watch(open, (isOpen) => {
92
118
  trackerKind.value = tracker.settings.tracker
93
119
  jiraProjectKey.value = tracker.settings.jiraProjectKey ?? ''
94
120
  linearTeamId.value = tracker.settings.linearTeamId ?? ''
121
+ intakeSource.value = null
122
+ intakeJiraProjectKey.value = ''
123
+ intakeLinearTeamId.value = ''
124
+ intakeGithubRepo.value = ''
125
+ intakeTitleFragment.value = ''
126
+ intakeLabels.value = ''
127
+ intakeIssueType.value = ''
128
+ intakeInProgressLabel.value = ''
129
+ // Load the connected task sources so the intake source picker is populated.
130
+ void tasks.probe()
131
+ })
132
+
133
+ // The board field required for the picked source must be filled before a bug-intake schedule saves.
134
+ const intakeReady = computed(() => {
135
+ if (!isBugIntake.value) return true
136
+ if (intakeSource.value === 'jira') return intakeJiraProjectKey.value.trim().length > 0
137
+ if (intakeSource.value === 'linear') return intakeLinearTeamId.value.trim().length > 0
138
+ if (intakeSource.value === 'github') return intakeGithubRepo.value.trim().length > 0
139
+ return false
95
140
  })
96
141
 
97
- const canAdd = computed(() => name.value.trim().length > 0 && pipelineId.value.length > 0)
142
+ function buildIssueIntake(): IssueIntakeConfig {
143
+ const source = intakeSource.value as TaskSourceKind
144
+ const labels = intakeLabels.value
145
+ .split(',')
146
+ .map((l) => l.trim())
147
+ .filter(Boolean)
148
+ return {
149
+ source,
150
+ board: {
151
+ ...(source === 'jira' && intakeJiraProjectKey.value.trim()
152
+ ? { jiraProjectKey: intakeJiraProjectKey.value.trim() }
153
+ : {}),
154
+ ...(source === 'linear' && intakeLinearTeamId.value.trim()
155
+ ? { linearTeamId: intakeLinearTeamId.value.trim() }
156
+ : {}),
157
+ ...(source === 'github' && intakeGithubRepo.value.trim()
158
+ ? { githubRepo: intakeGithubRepo.value.trim() }
159
+ : {}),
160
+ },
161
+ predicates: {
162
+ ...(intakeTitleFragment.value.trim()
163
+ ? { titleFragment: intakeTitleFragment.value.trim() }
164
+ : {}),
165
+ ...(labels.length ? { labels } : {}),
166
+ ...(intakeIssueType.value.trim() ? { issueType: intakeIssueType.value.trim() } : {}),
167
+ },
168
+ ...(source === 'github' && intakeInProgressLabel.value.trim()
169
+ ? { inProgressLabel: intakeInProgressLabel.value.trim() }
170
+ : {}),
171
+ }
172
+ }
173
+
174
+ const canAdd = computed(
175
+ () => name.value.trim().length > 0 && pipelineId.value.length > 0 && intakeReady.value,
176
+ )
98
177
 
99
178
  async function add() {
100
179
  const frameId = ui.addRecurringFrameId
@@ -119,6 +198,7 @@ async function add() {
119
198
  onDemand: onDemand.value,
120
199
  ...(onDemand.value ? {} : { recurrence: recurrence.value }),
121
200
  ...(description.value.trim() ? { description: description.value.trim() } : {}),
201
+ ...(isBugIntake.value ? { issueIntake: buildIssueIntake() } : {}),
122
202
  })
123
203
  ui.closeAddRecurring()
124
204
  } catch (e) {
@@ -238,6 +318,86 @@ async function add() {
238
318
  </UFormField>
239
319
  </div>
240
320
 
321
+ <div v-if="isBugIntake" class="space-y-3 rounded-lg border border-slate-800 p-3">
322
+ <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
323
+ {{ t('board.recurring.intake') }}
324
+ </p>
325
+ <p class="text-[11px] text-slate-500">
326
+ {{ t('board.recurring.intakeHint') }}
327
+ </p>
328
+ <p v-if="intakeSources.length === 0" class="text-[11px] text-amber-500">
329
+ {{ t('board.recurring.intakeNoSources') }}
330
+ </p>
331
+ <div v-else class="flex flex-wrap gap-1">
332
+ <UButton
333
+ v-for="s in intakeSources"
334
+ :key="s.source"
335
+ size="xs"
336
+ :color="intakeSource === s.source ? 'primary' : 'neutral'"
337
+ :variant="intakeSource === s.source ? 'solid' : 'subtle'"
338
+ :icon="s.icon"
339
+ @click="intakeSource = s.source"
340
+ >
341
+ {{ s.label }}
342
+ </UButton>
343
+ </div>
344
+
345
+ <UFormField
346
+ v-if="intakeSource === 'jira'"
347
+ :label="t('board.recurring.jiraProjectKey')"
348
+ required
349
+ >
350
+ <UInput
351
+ v-model="intakeJiraProjectKey"
352
+ :placeholder="t('board.recurring.jiraProjectKeyPlaceholder')"
353
+ class="w-full"
354
+ />
355
+ </UFormField>
356
+ <UFormField
357
+ v-if="intakeSource === 'linear'"
358
+ :label="t('board.recurring.linearTeamId')"
359
+ required
360
+ >
361
+ <UInput v-model="intakeLinearTeamId" placeholder="team_…" class="w-full" />
362
+ </UFormField>
363
+ <UFormField
364
+ v-if="intakeSource === 'github'"
365
+ :label="t('board.recurring.intakeGithubRepo')"
366
+ required
367
+ >
368
+ <!-- A GitHub repo ref is always the literal `owner/name` path, never localized. -->
369
+ <UInput v-model="intakeGithubRepo" placeholder="owner/name" class="w-full" />
370
+ </UFormField>
371
+
372
+ <template v-if="intakeSource">
373
+ <UFormField :label="t('board.recurring.intakeTitleFragment')">
374
+ <UInput
375
+ v-model="intakeTitleFragment"
376
+ :placeholder="t('board.recurring.intakeTitleFragmentPlaceholder')"
377
+ class="w-full"
378
+ />
379
+ </UFormField>
380
+ <UFormField :label="t('board.recurring.intakeLabels')">
381
+ <UInput
382
+ v-model="intakeLabels"
383
+ :placeholder="t('board.recurring.intakeLabelsPlaceholder')"
384
+ class="w-full"
385
+ />
386
+ </UFormField>
387
+ <UFormField :label="t('board.recurring.intakeIssueType')">
388
+ <!-- A literal issue-type example (tracker vocabulary), kept verbatim across locales. -->
389
+ <UInput v-model="intakeIssueType" placeholder="bug" class="w-full" />
390
+ </UFormField>
391
+ <UFormField
392
+ v-if="intakeSource === 'github'"
393
+ :label="t('board.recurring.intakeInProgressLabel')"
394
+ >
395
+ <!-- A literal label example, kept verbatim across locales. -->
396
+ <UInput v-model="intakeInProgressLabel" placeholder="in-progress" class="w-full" />
397
+ </UFormField>
398
+ </template>
399
+ </div>
400
+
241
401
  <p class="text-[11px] text-slate-500">
242
402
  {{ t('board.recurring.footerHint') }}
243
403
  </p>
@@ -7,9 +7,11 @@
7
7
  // result-view host: from the board card / inspector (`ui.openInitiativeTracker`) or
8
8
  // as the planner step's result view. Live `initiative` stream events patch the
9
9
  // store, so an open window follows the plan as it is ingested and later executed.
10
- import { computed } from 'vue'
11
- import type { InitiativeItem } from '~/types/domain'
10
+ import { computed, reactive, ref } from 'vue'
11
+ import type { InitiativeFollowUp, InitiativeItem } from '~/types/domain'
12
12
  import {
13
+ INITIATIVE_FOLLOWUP_STATUS_CHIPS,
14
+ INITIATIVE_FOLLOWUP_STATUS_LABEL_KEYS,
13
15
  INITIATIVE_ITEM_STATUS_CHIPS,
14
16
  INITIATIVE_ITEM_STATUS_LABEL_KEYS,
15
17
  INITIATIVE_STATUS_LABEL_KEYS,
@@ -19,6 +21,7 @@ import {
19
21
  const board = useBoardStore()
20
22
  const initiatives = useInitiativesStore()
21
23
  const { t } = useI18n()
24
+ const toast = useToast()
22
25
 
23
26
  const { open, blockId, close } = useResultView('initiative-tracker', {
24
27
  onOpen: (id) => void initiatives.load(id),
@@ -52,6 +55,93 @@ function ruleAxes(rule: { minComplexity?: number; minRisk?: number; minImpact?:
52
55
  ].filter((a): a is string => a !== null)
53
56
  return axes.length ? axes.join(' · ') : t('initiative.tracker.axisNever')
54
57
  }
58
+
59
+ // ---- Curation (slice 4): only meaningful while the initiative is still executing ----
60
+ const editable = computed(() => initiative.value?.status === 'executing')
61
+
62
+ /** Report a failed curation call as a toast (a stale-rev CAS conflict, an illegal edit, …). */
63
+ function reportError(error: unknown) {
64
+ const message = error instanceof Error ? error.message : t('initiative.curation.failed')
65
+ toast.add({ title: t('initiative.curation.failed'), description: message, color: 'error' })
66
+ }
67
+
68
+ // Follow-up promotion: an inline per-follow-up form (phase + optional title override).
69
+ const promotingId = ref<string | null>(null)
70
+ const promoteForm = reactive<{ phaseId: string; title: string }>({ phaseId: '', title: '' })
71
+
72
+ function startPromote(followUp: InitiativeFollowUp) {
73
+ const sourcePhase = (initiative.value?.items ?? []).find(
74
+ (i) => i.id === followUp.sourceItemId,
75
+ )?.phaseId
76
+ promoteForm.phaseId = sourcePhase ?? phases.value[0]?.id ?? ''
77
+ promoteForm.title = followUp.title
78
+ promotingId.value = followUp.id
79
+ }
80
+
81
+ async function submitPromote(followUp: InitiativeFollowUp) {
82
+ if (!initiative.value || !promoteForm.phaseId) return
83
+ try {
84
+ await initiatives.promoteFollowUp(initiative.value.id, followUp.id, {
85
+ phaseId: promoteForm.phaseId,
86
+ ...(promoteForm.title.trim() && promoteForm.title.trim() !== followUp.title
87
+ ? { title: promoteForm.title.trim() }
88
+ : {}),
89
+ })
90
+ promotingId.value = null
91
+ } catch (error) {
92
+ reportError(error)
93
+ }
94
+ }
95
+
96
+ async function dismissFollowUp(followUp: InitiativeFollowUp) {
97
+ if (!initiative.value) return
98
+ try {
99
+ await initiatives.dismissFollowUp(initiative.value.id, followUp.id)
100
+ } catch (error) {
101
+ reportError(error)
102
+ }
103
+ }
104
+
105
+ // Item status control: retry a blocked item, or skip a pending/blocked one.
106
+ async function itemAction(item: InitiativeItem, action: 'retry' | 'skip') {
107
+ if (!initiative.value) return
108
+ try {
109
+ await initiatives.updateItem(initiative.value.id, item.id, { action })
110
+ } catch (error) {
111
+ reportError(error)
112
+ }
113
+ }
114
+
115
+ // Policy editing: retune the two scalar knobs (concurrency + default pipeline) while preserving
116
+ // the planner-authored rules. A full rule editor stays out of scope — re-plan to reshape rules.
117
+ const editingPolicy = ref(false)
118
+ const policyForm = reactive<{ maxConcurrent: number; defaultPipelineId: string }>({
119
+ maxConcurrent: 1,
120
+ defaultPipelineId: '',
121
+ })
122
+
123
+ function startEditPolicy() {
124
+ const policy = initiative.value?.policy
125
+ if (!policy) return
126
+ policyForm.maxConcurrent = policy.maxConcurrent
127
+ policyForm.defaultPipelineId = policy.defaultPipelineId
128
+ editingPolicy.value = true
129
+ }
130
+
131
+ async function savePolicy() {
132
+ const policy = initiative.value?.policy
133
+ if (!initiative.value || !policy) return
134
+ try {
135
+ await initiatives.updatePolicy(initiative.value.id, {
136
+ ...policy,
137
+ maxConcurrent: policyForm.maxConcurrent,
138
+ defaultPipelineId: policyForm.defaultPipelineId.trim() || policy.defaultPipelineId,
139
+ })
140
+ editingPolicy.value = false
141
+ } catch (error) {
142
+ reportError(error)
143
+ }
144
+ }
55
145
  </script>
56
146
 
57
147
  <template>
@@ -193,10 +283,34 @@ function ruleAxes(rule: { minComplexity?: number; minRisk?: number; minImpact?:
193
283
  <div v-if="item.note" class="mt-0.5 text-[10px] text-amber-300/80">
194
284
  {{ item.note }}
195
285
  </div>
286
+ <div
287
+ v-if="
288
+ editable && (item.status === 'blocked' || item.status === 'pending')
289
+ "
290
+ class="mt-1 flex gap-1.5"
291
+ >
292
+ <button
293
+ v-if="item.status === 'blocked'"
294
+ class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800 disabled:opacity-50"
295
+ :disabled="initiatives.curating"
296
+ :data-testid="`initiative-item-retry-${item.id}`"
297
+ @click="itemAction(item, 'retry')"
298
+ >
299
+ {{ t('initiative.curation.retry') }}
300
+ </button>
301
+ <button
302
+ class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800 disabled:opacity-50"
303
+ :disabled="initiatives.curating"
304
+ :data-testid="`initiative-item-skip-${item.id}`"
305
+ @click="itemAction(item, 'skip')"
306
+ >
307
+ {{ t('initiative.curation.skip') }}
308
+ </button>
309
+ </div>
196
310
  </td>
197
311
  <td class="px-3 py-2 align-top">
198
312
  <UBadge
199
- :color="INITIATIVE_ITEM_STATUS_CHIPS[item.status] as any"
313
+ :color="INITIATIVE_ITEM_STATUS_CHIPS[item.status]"
200
314
  variant="subtle"
201
315
  size="sm"
202
316
  >
@@ -225,10 +339,20 @@ function ruleAxes(rule: { minComplexity?: number; minRisk?: number; minImpact?:
225
339
 
226
340
  <!-- Execution policy -->
227
341
  <section v-if="initiative.policy" class="mb-4">
228
- <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
229
- {{ t('initiative.tracker.policy') }}
230
- </h3>
231
- <ul class="text-[12px] text-slate-300">
342
+ <div class="mb-1 flex items-center gap-2">
343
+ <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
344
+ {{ t('initiative.tracker.policy') }}
345
+ </h3>
346
+ <button
347
+ v-if="editable && !editingPolicy"
348
+ class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800"
349
+ data-testid="initiative-policy-edit"
350
+ @click="startEditPolicy"
351
+ >
352
+ {{ t('initiative.curation.edit') }}
353
+ </button>
354
+ </div>
355
+ <ul v-if="!editingPolicy" class="text-[12px] text-slate-300">
232
356
  <li>
233
357
  {{
234
358
  t('initiative.tracker.maxConcurrent', {
@@ -245,6 +369,45 @@ function ruleAxes(rule: { minComplexity?: number; minRisk?: number; minImpact?:
245
369
  <code class="text-sky-300">{{ initiative.policy.defaultPipelineId }}</code>
246
370
  </li>
247
371
  </ul>
372
+ <!-- Edit form: the two scalar knobs; planner-authored rules are preserved. -->
373
+ <div v-else class="flex flex-col gap-2 rounded-lg border border-slate-800 p-3">
374
+ <label class="flex items-center gap-2 text-[12px] text-slate-300">
375
+ <span class="w-40">{{ t('initiative.curation.maxConcurrentField') }}</span>
376
+ <input
377
+ v-model.number="policyForm.maxConcurrent"
378
+ type="number"
379
+ min="1"
380
+ max="20"
381
+ class="w-20 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-slate-200"
382
+ data-testid="initiative-policy-max-concurrent"
383
+ />
384
+ </label>
385
+ <label class="flex items-center gap-2 text-[12px] text-slate-300">
386
+ <span class="w-40">{{ t('initiative.curation.defaultPipelineField') }}</span>
387
+ <input
388
+ v-model="policyForm.defaultPipelineId"
389
+ type="text"
390
+ class="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 font-mono text-[11px] text-slate-200"
391
+ data-testid="initiative-policy-default-pipeline"
392
+ />
393
+ </label>
394
+ <div class="flex gap-2">
395
+ <button
396
+ class="rounded bg-indigo-600 px-2 py-1 text-[11px] text-white hover:bg-indigo-500 disabled:opacity-50"
397
+ :disabled="initiatives.curating"
398
+ data-testid="initiative-policy-save"
399
+ @click="savePolicy"
400
+ >
401
+ {{ t('initiative.curation.save') }}
402
+ </button>
403
+ <button
404
+ class="rounded border border-slate-700 px-2 py-1 text-[11px] text-slate-300 hover:bg-slate-800"
405
+ @click="editingPolicy = false"
406
+ >
407
+ {{ t('initiative.curation.cancel') }}
408
+ </button>
409
+ </div>
410
+ </div>
248
411
  </section>
249
412
 
250
413
  <!-- Logs -->
@@ -275,10 +438,85 @@ function ruleAxes(rule: { minComplexity?: number; minRisk?: number; minImpact?:
275
438
  <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
276
439
  {{ t('initiative.tracker.followUps') }}
277
440
  </h3>
278
- <ul class="list-inside list-disc text-[13px] text-slate-300">
279
- <li v-for="f in initiative.followUps" :key="f.id">
280
- <span class="font-medium">{{ f.title }}</span>
281
- <span v-if="f.detail" class="text-slate-400"> — {{ f.detail }}</span>
441
+ <ul class="flex flex-col gap-2 text-[13px] text-slate-300">
442
+ <li
443
+ v-for="f in initiative.followUps"
444
+ :key="f.id"
445
+ class="rounded-lg border border-slate-800 p-2.5"
446
+ :data-testid="`initiative-followup-${f.id}`"
447
+ >
448
+ <div class="flex items-start gap-2">
449
+ <div class="min-w-0 flex-1">
450
+ <span class="font-medium">{{ f.title }}</span>
451
+ <span v-if="f.detail" class="text-slate-400"> — {{ f.detail }}</span>
452
+ </div>
453
+ <UBadge
454
+ :color="INITIATIVE_FOLLOWUP_STATUS_CHIPS[f.status]"
455
+ variant="subtle"
456
+ size="sm"
457
+ >
458
+ {{ t(INITIATIVE_FOLLOWUP_STATUS_LABEL_KEYS[f.status]) }}
459
+ </UBadge>
460
+ </div>
461
+ <!-- Triage actions for an open follow-up (only while executing) -->
462
+ <div v-if="editable && f.status === 'open'" class="mt-2">
463
+ <div v-if="promotingId === f.id" class="flex flex-col gap-2">
464
+ <label class="flex items-center gap-2 text-[12px]">
465
+ <span class="text-slate-400">{{
466
+ t('initiative.curation.phaseField')
467
+ }}</span>
468
+ <select
469
+ v-model="promoteForm.phaseId"
470
+ class="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-slate-200"
471
+ data-testid="initiative-promote-phase"
472
+ >
473
+ <option v-for="p in phases" :key="p.id" :value="p.id">
474
+ {{ p.title }}
475
+ </option>
476
+ </select>
477
+ </label>
478
+ <input
479
+ v-model="promoteForm.title"
480
+ type="text"
481
+ class="rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[12px] text-slate-200"
482
+ :placeholder="t('initiative.curation.itemTitlePlaceholder')"
483
+ data-testid="initiative-promote-title"
484
+ />
485
+ <div class="flex gap-2">
486
+ <button
487
+ class="rounded bg-indigo-600 px-2 py-1 text-[11px] text-white hover:bg-indigo-500 disabled:opacity-50"
488
+ :disabled="initiatives.curating || !promoteForm.phaseId"
489
+ data-testid="initiative-promote-submit"
490
+ @click="submitPromote(f)"
491
+ >
492
+ {{ t('initiative.curation.promoteConfirm') }}
493
+ </button>
494
+ <button
495
+ class="rounded border border-slate-700 px-2 py-1 text-[11px] text-slate-300 hover:bg-slate-800"
496
+ @click="promotingId = null"
497
+ >
498
+ {{ t('initiative.curation.cancel') }}
499
+ </button>
500
+ </div>
501
+ </div>
502
+ <div v-else class="flex gap-1.5">
503
+ <button
504
+ class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800"
505
+ data-testid="initiative-followup-promote"
506
+ @click="startPromote(f)"
507
+ >
508
+ {{ t('initiative.curation.promote') }}
509
+ </button>
510
+ <button
511
+ class="rounded border border-slate-700 px-1.5 py-0.5 text-[10px] text-slate-300 hover:bg-slate-800 disabled:opacity-50"
512
+ :disabled="initiatives.curating"
513
+ data-testid="initiative-followup-dismiss"
514
+ @click="dismissFollowUp(f)"
515
+ >
516
+ {{ t('initiative.curation.dismiss') }}
517
+ </button>
518
+ </div>
519
+ </div>
282
520
  </li>
283
521
  </ul>
284
522
  </section>
@@ -66,6 +66,7 @@ const REASON_KEYS: Record<MergeDecision['reason'], string> = {
66
66
  no_rationale: 'panels.mergerResult.reason.no_rationale',
67
67
  no_assessment: 'panels.mergerResult.reason.no_assessment',
68
68
  merge_failed: 'panels.mergerResult.reason.merge_failed',
69
+ merge_partial: 'panels.mergerResult.reason.merge_partial',
69
70
  }
70
71
  const OUTCOME_KEYS: Record<MergeDecision['outcome'], string> = {
71
72
  auto_merged: 'panels.mergerResult.outcome.auto_merged',
@@ -3,12 +3,21 @@ import {
3
3
  cancelInitiativeContract,
4
4
  continueInitiativePlanningContract,
5
5
  createInitiativeContract,
6
+ dismissInitiativeFollowUpContract,
6
7
  getInitiativeByBlockContract,
7
8
  getInitiativeContract,
8
9
  listInitiativesContract,
9
10
  pauseInitiativeContract,
10
11
  proceedInitiativePlanningContract,
12
+ promoteInitiativeFollowUpContract,
11
13
  resumeInitiativeContract,
14
+ updateInitiativeItemContract,
15
+ updateInitiativePolicyContract,
16
+ } from '@cat-factory/contracts'
17
+ import type {
18
+ InitiativeExecutionPolicy,
19
+ PromoteInitiativeFollowUpInput,
20
+ UpdateInitiativeItemInput,
12
21
  } from '@cat-factory/contracts'
13
22
  import type { ApiContext } from './context'
14
23
 
@@ -66,5 +75,47 @@ export function initiativeApi({ send, ws }: ApiContext) {
66
75
 
67
76
  cancelInitiative: (workspaceId: string, blockId: string) =>
68
77
  send(cancelInitiativeContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
78
+
79
+ // Follow-up triage + item/policy editing (slice 4): keyed by initiative id.
80
+ promoteInitiativeFollowUp: (
81
+ workspaceId: string,
82
+ initiativeId: string,
83
+ followUpId: string,
84
+ body: PromoteInitiativeFollowUpInput,
85
+ ) =>
86
+ send(promoteInitiativeFollowUpContract, {
87
+ pathPrefix: ws(workspaceId),
88
+ pathParams: { initiativeId, followUpId },
89
+ body,
90
+ }),
91
+
92
+ dismissInitiativeFollowUp: (workspaceId: string, initiativeId: string, followUpId: string) =>
93
+ send(dismissInitiativeFollowUpContract, {
94
+ pathPrefix: ws(workspaceId),
95
+ pathParams: { initiativeId, followUpId },
96
+ }),
97
+
98
+ updateInitiativeItem: (
99
+ workspaceId: string,
100
+ initiativeId: string,
101
+ itemId: string,
102
+ body: UpdateInitiativeItemInput,
103
+ ) =>
104
+ send(updateInitiativeItemContract, {
105
+ pathPrefix: ws(workspaceId),
106
+ pathParams: { initiativeId, itemId },
107
+ body,
108
+ }),
109
+
110
+ updateInitiativePolicy: (
111
+ workspaceId: string,
112
+ initiativeId: string,
113
+ body: InitiativeExecutionPolicy,
114
+ ) =>
115
+ send(updateInitiativePolicyContract, {
116
+ pathPrefix: ws(workspaceId),
117
+ pathParams: { initiativeId },
118
+ body,
119
+ }),
69
120
  }
70
121
  }
@@ -1,6 +1,11 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { computed, ref } from 'vue'
3
- import type { Initiative } from '~/types/domain'
3
+ import type {
4
+ Initiative,
5
+ InitiativeExecutionPolicy,
6
+ PromoteInitiativeFollowUpInput,
7
+ UpdateInitiativeItemInput,
8
+ } from '~/types/domain'
4
9
  import { useWorkspaceStore } from '~/stores/workspace'
5
10
  import { useBoardStore } from '~/stores/board'
6
11
 
@@ -154,6 +159,77 @@ export const useInitiativesStore = defineStore('initiatives', () => {
154
159
  }
155
160
  }
156
161
 
162
+ /** True while a curation action (promote/dismiss/edit item/edit policy) is in flight. */
163
+ const curating = ref(false)
164
+
165
+ async function curate<T>(fn: () => Promise<T>): Promise<T> {
166
+ if (!workspace.workspaceId) throw new Error('No active workspace')
167
+ curating.value = true
168
+ try {
169
+ return await fn()
170
+ } finally {
171
+ curating.value = false
172
+ }
173
+ }
174
+
175
+ /** Promote an `open` harvested follow-up into a new pending tracker item. */
176
+ async function promoteFollowUp(
177
+ initiativeId: string,
178
+ followUpId: string,
179
+ input: PromoteInitiativeFollowUpInput,
180
+ ) {
181
+ return curate(async () => {
182
+ const updated = await api.promoteInitiativeFollowUp(
183
+ workspace.workspaceId!,
184
+ initiativeId,
185
+ followUpId,
186
+ input,
187
+ )
188
+ upsert(updated)
189
+ return updated
190
+ })
191
+ }
192
+
193
+ /** Dismiss a harvested follow-up. */
194
+ async function dismissFollowUp(initiativeId: string, followUpId: string) {
195
+ return curate(async () => {
196
+ const updated = await api.dismissInitiativeFollowUp(
197
+ workspace.workspaceId!,
198
+ initiativeId,
199
+ followUpId,
200
+ )
201
+ upsert(updated)
202
+ return updated
203
+ })
204
+ }
205
+
206
+ /** Edit one tracker item and/or drive its status (retry a blocked item / skip it). */
207
+ async function updateItem(
208
+ initiativeId: string,
209
+ itemId: string,
210
+ input: UpdateInitiativeItemInput,
211
+ ) {
212
+ return curate(async () => {
213
+ const updated = await api.updateInitiativeItem(
214
+ workspace.workspaceId!,
215
+ initiativeId,
216
+ itemId,
217
+ input,
218
+ )
219
+ upsert(updated)
220
+ return updated
221
+ })
222
+ }
223
+
224
+ /** Replace the execution policy (concurrency + pipeline rules). */
225
+ async function updatePolicy(initiativeId: string, policy: InitiativeExecutionPolicy) {
226
+ return curate(async () => {
227
+ const updated = await api.updateInitiativePolicy(workspace.workspaceId!, initiativeId, policy)
228
+ upsert(updated)
229
+ return updated
230
+ })
231
+ }
232
+
157
233
  function reset() {
158
234
  byBlock.value = {}
159
235
  }
@@ -165,6 +241,7 @@ export const useInitiativesStore = defineStore('initiatives', () => {
165
241
  creating,
166
242
  resuming,
167
243
  controlling,
244
+ curating,
168
245
  forBlock,
169
246
  hydrate,
170
247
  upsert,
@@ -174,6 +251,10 @@ export const useInitiativesStore = defineStore('initiatives', () => {
174
251
  continuePlanning,
175
252
  proceedPlanning,
176
253
  control,
254
+ promoteFollowUp,
255
+ dismissFollowUp,
256
+ updateItem,
257
+ updatePolicy,
177
258
  reset,
178
259
  }
179
260
  })
@@ -15,4 +15,7 @@ export type {
15
15
  InitiativePipelineRule,
16
16
  InitiativeQa,
17
17
  InitiativeStatus,
18
+ PromoteInitiativeFollowUpInput,
19
+ UpdateInitiativeItemInput,
20
+ UpdateInitiativePolicyInput,
18
21
  } from '@cat-factory/contracts'
@@ -9,6 +9,7 @@
9
9
  export type {
10
10
  ScheduleTemplate,
11
11
  Recurrence,
12
+ IssueIntakeConfig,
12
13
  PipelineSchedule,
13
14
  ScheduleRun,
14
15
  CreateScheduleInput,
@@ -51,16 +51,18 @@ export const AGENT_ARCHETYPES: AgentArchetype[] = [
51
51
  resultView: 'clarity-review',
52
52
  },
53
53
  {
54
- // A read-only `/explore` agent (like the architect), so it's a first-class palette block
55
- // a user can add to any pipeline — not just the `pl_bugfix` preset where it leads. No
56
- // `resultView`: the enriched report is prose, so it uses the generic step-detail panel.
54
+ // A read-only, structured `container-explore` agent, so it's a first-class palette block a
55
+ // user can add to any pipeline — not just the `pl_bugfix` preset where it leads. Its
56
+ // structured triage opens in the shared generic viewer; the clarity gate consumes its
57
+ // `clarity`/`questions` server-side.
57
58
  kind: 'bug-investigator',
58
59
  label: 'Bug Investigator',
59
60
  icon: 'i-lucide-search-code',
60
61
  color: '#38bdf8',
61
62
  category: 'review',
62
63
  description:
63
- 'Read-only codebase investigation that traces the bug to its root cause and produces an enriched report (no code changes).',
64
+ 'Read-only, multi-repo codebase investigation that traces the bug to its root cause and decides whether the report is fixable as-is or needs the reporter to clarify (no code changes).',
65
+ resultView: 'generic-structured',
64
66
  },
65
67
  {
66
68
  kind: 'task-estimator',
@@ -1,10 +1,19 @@
1
- import type { InitiativeItem, InitiativeItemStatus, InitiativeStatus } from '~/types/domain'
1
+ import type {
2
+ InitiativeFollowUp,
3
+ InitiativeItem,
4
+ InitiativeItemStatus,
5
+ InitiativeStatus,
6
+ } from '~/types/domain'
2
7
 
3
8
  // Shared initiative presentation vocabulary, so the board card, the inspector body and
4
9
  // the tracker window render statuses/progress from ONE source. The exhaustive
5
- // `Record<Enum, string>` maps keep the tier-2 typecheck guard live (a new status without
10
+ // `Record<Enum, …>` maps keep the tier-2 typecheck guard live (a new status without
6
11
  // a label/chip fails the build) without triplicating it across the components.
7
12
 
13
+ /** Nuxt UI badge/chip colour names — mirrors `UBadge`'s `color` prop union, so a chip map
14
+ * types its values against it and the `:color` binding needs no cast. */
15
+ type BadgeColor = 'error' | 'info' | 'primary' | 'secondary' | 'success' | 'warning' | 'neutral'
16
+
8
17
  /** Initiative lifecycle status → i18n label key. */
9
18
  export const INITIATIVE_STATUS_LABEL_KEYS: Record<InitiativeStatus, string> = {
10
19
  planning: 'initiative.status.planning',
@@ -16,7 +25,7 @@ export const INITIATIVE_STATUS_LABEL_KEYS: Record<InitiativeStatus, string> = {
16
25
  }
17
26
 
18
27
  /** Initiative lifecycle status → Nuxt UI badge colour. */
19
- export const INITIATIVE_STATUS_CHIPS: Record<InitiativeStatus, string> = {
28
+ export const INITIATIVE_STATUS_CHIPS: Record<InitiativeStatus, BadgeColor> = {
20
29
  planning: 'neutral',
21
30
  awaiting_approval: 'warning',
22
31
  executing: 'info',
@@ -36,7 +45,7 @@ export const INITIATIVE_ITEM_STATUS_LABEL_KEYS: Record<InitiativeItemStatus, str
36
45
  }
37
46
 
38
47
  /** Tracker item status → Nuxt UI badge colour. */
39
- export const INITIATIVE_ITEM_STATUS_CHIPS: Record<InitiativeItemStatus, string> = {
48
+ export const INITIATIVE_ITEM_STATUS_CHIPS: Record<InitiativeItemStatus, BadgeColor> = {
40
49
  pending: 'neutral',
41
50
  in_progress: 'info',
42
51
  pr_open: 'warning',
@@ -45,6 +54,20 @@ export const INITIATIVE_ITEM_STATUS_CHIPS: Record<InitiativeItemStatus, string>
45
54
  skipped: 'neutral',
46
55
  }
47
56
 
57
+ /** Follow-up triage status → i18n label key. Exhaustive so a new status fails the build. */
58
+ export const INITIATIVE_FOLLOWUP_STATUS_LABEL_KEYS: Record<InitiativeFollowUp['status'], string> = {
59
+ open: 'initiative.followUpStatus.open',
60
+ promoted: 'initiative.followUpStatus.promoted',
61
+ dismissed: 'initiative.followUpStatus.dismissed',
62
+ }
63
+
64
+ /** Follow-up triage status → Nuxt UI badge colour. */
65
+ export const INITIATIVE_FOLLOWUP_STATUS_CHIPS: Record<InitiativeFollowUp['status'], BadgeColor> = {
66
+ open: 'warning',
67
+ promoted: 'success',
68
+ dismissed: 'neutral',
69
+ }
70
+
48
71
  /** Item statuses that count as settled — mirrors the backend terminal-status set. */
49
72
  const SETTLED: ReadonlySet<InitiativeItemStatus> = new Set(['done', 'skipped'])
50
73
 
@@ -219,7 +219,17 @@
219
219
  "submit": "Add recurring pipeline",
220
220
  "addFailedTitle": "Could not add recurring pipeline",
221
221
  "onDemand": "On-demand (manual only)",
222
- "onDemandHint": "Runs only when you trigger it, with no schedule. Because you are present each time, its task may use an individual-usage subscription model."
222
+ "onDemandHint": "Runs only when you trigger it, with no schedule. Because you are present each time, its task may use an individual-usage subscription model.",
223
+ "intake": "Issue intake",
224
+ "intakeHint": "Each run picks one matching open issue from the tracker and works it end to end.",
225
+ "intakeNoSources": "Connect a task source first to pull issues from it.",
226
+ "intakeGithubRepo": "Repository",
227
+ "intakeTitleFragment": "Title contains",
228
+ "intakeTitleFragmentPlaceholder": "e.g. crash",
229
+ "intakeLabels": "Labels",
230
+ "intakeLabelsPlaceholder": "comma-separated",
231
+ "intakeIssueType": "Issue type",
232
+ "intakeInProgressLabel": "In-progress label"
223
233
  },
224
234
  "failure": {
225
235
  "containerFailedToStart": "Container failed to start",
@@ -854,7 +864,8 @@
854
864
  "auto_merge_disabled": "The {preset} preset sends every PR to a human, so this one is waiting for review.",
855
865
  "no_rationale": "The merger scored the PR but gave no rationale, so the verdict could not be trusted to auto-merge; the PR is waiting for a human to merge.",
856
866
  "no_assessment": "The merger did not return a parseable assessment, so the PR is waiting for a human to merge.",
857
- "merge_failed": "The scores were within the {preset} thresholds, but the automatic merge could not complete (for example branch protection or a conflict), so the PR is waiting for a human to merge."
867
+ "merge_failed": "The scores were within the {preset} thresholds, but the automatic merge could not complete (for example branch protection or a conflict), so the PR is waiting for a human to merge.",
868
+ "merge_partial": "Some of the task's pull requests merged, but a later one could not, so the change is waiting for a human to finish or revert the multi-repo merge."
858
869
  },
859
870
  "scores": "Scores",
860
871
  "axis": {
@@ -4166,6 +4177,26 @@
4166
4177
  "hint": "Continue lets the planner ask follow-ups; Proceed plans with the answers so far.",
4167
4178
  "proceed": "Proceed to plan",
4168
4179
  "continue": "Continue"
4180
+ },
4181
+ "followUpStatus": {
4182
+ "open": "Open",
4183
+ "promoted": "Promoted",
4184
+ "dismissed": "Dismissed"
4185
+ },
4186
+ "curation": {
4187
+ "promote": "Promote to item",
4188
+ "promoteConfirm": "Create item",
4189
+ "dismiss": "Dismiss",
4190
+ "retry": "Retry",
4191
+ "skip": "Skip",
4192
+ "edit": "Edit",
4193
+ "save": "Save",
4194
+ "cancel": "Cancel",
4195
+ "phaseField": "Phase",
4196
+ "itemTitlePlaceholder": "Item title (defaults to the follow-up's)",
4197
+ "maxConcurrentField": "Max concurrent tasks",
4198
+ "defaultPipelineField": "Default pipeline",
4199
+ "failed": "Could not update the initiative"
4169
4200
  }
4170
4201
  }
4171
4202
  }
@@ -198,7 +198,17 @@
198
198
  "submit": "Añadir pipeline recurrente",
199
199
  "addFailedTitle": "No se pudo añadir la pipeline recurrente",
200
200
  "onDemand": "Bajo demanda (solo manual)",
201
- "onDemandHint": "Se ejecuta solo cuando lo activas, sin programación. Como estás presente cada vez, su tarea puede usar un modelo de suscripción de uso individual."
201
+ "onDemandHint": "Se ejecuta solo cuando lo activas, sin programación. Como estás presente cada vez, su tarea puede usar un modelo de suscripción de uso individual.",
202
+ "intake": "Admisión de incidencias",
203
+ "intakeHint": "Cada ejecución toma una incidencia abierta que coincide del rastreador y la resuelve de principio a fin.",
204
+ "intakeNoSources": "Primero conecta una fuente de tareas para extraer incidencias de ella.",
205
+ "intakeGithubRepo": "Repositorio",
206
+ "intakeTitleFragment": "El título contiene",
207
+ "intakeTitleFragmentPlaceholder": "p. ej. crash",
208
+ "intakeLabels": "Etiquetas",
209
+ "intakeLabelsPlaceholder": "separadas por comas",
210
+ "intakeIssueType": "Tipo de incidencia",
211
+ "intakeInProgressLabel": "Etiqueta de en progreso"
202
212
  },
203
213
  "failure": {
204
214
  "containerFailedToStart": "El contenedor no pudo iniciarse",
@@ -811,7 +821,8 @@
811
821
  "auto_merge_disabled": "El preajuste {preset} envía todos los PR a una persona, así que este espera revisión.",
812
822
  "no_rationale": "El fusionador puntuó el PR pero no dio ninguna justificación, así que no se pudo confiar en el veredicto para fusionar automáticamente; el PR espera a que una persona lo fusione.",
813
823
  "no_assessment": "El fusionador no devolvió una evaluación analizable, por lo que el PR espera a que una persona lo fusione.",
814
- "merge_failed": "Las puntuaciones estaban dentro de los umbrales de {preset}, pero la fusión automática no pudo completarse (por ejemplo, protección de rama o un conflicto), por lo que el PR espera a que una persona lo fusione."
824
+ "merge_failed": "Las puntuaciones estaban dentro de los umbrales de {preset}, pero la fusión automática no pudo completarse (por ejemplo, protección de rama o un conflicto), por lo que el PR espera a que una persona lo fusione.",
825
+ "merge_partial": "Algunas de las solicitudes de incorporación de la tarea se fusionaron, pero una posterior no pudo, por lo que el cambio espera a que una persona termine o revierta la fusión multirrepositorio."
815
826
  },
816
827
  "scores": "Puntuaciones",
817
828
  "axis": {
@@ -4048,6 +4059,26 @@
4048
4059
  "hint": "Continuar permite al planificador hacer mas preguntas; Proceder planifica con las respuestas actuales.",
4049
4060
  "proceed": "Proceder a planificar",
4050
4061
  "continue": "Continuar"
4062
+ },
4063
+ "followUpStatus": {
4064
+ "open": "Abierto",
4065
+ "promoted": "Promovido",
4066
+ "dismissed": "Descartado"
4067
+ },
4068
+ "curation": {
4069
+ "promote": "Promover a elemento",
4070
+ "promoteConfirm": "Crear elemento",
4071
+ "dismiss": "Descartar",
4072
+ "retry": "Reintentar",
4073
+ "skip": "Omitir",
4074
+ "edit": "Editar",
4075
+ "save": "Guardar",
4076
+ "cancel": "Cancelar",
4077
+ "phaseField": "Fase",
4078
+ "itemTitlePlaceholder": "Titulo del elemento (por defecto el del seguimiento)",
4079
+ "maxConcurrentField": "Tareas concurrentes maximas",
4080
+ "defaultPipelineField": "Pipeline por defecto",
4081
+ "failed": "No se pudo actualizar la iniciativa"
4051
4082
  }
4052
4083
  }
4053
4084
  }
@@ -198,7 +198,17 @@
198
198
  "submit": "Ajouter la pipeline récurrente",
199
199
  "addFailedTitle": "Impossible d’ajouter la pipeline récurrente",
200
200
  "onDemand": "À la demande (manuel uniquement)",
201
- "onDemandHint": "Ne s'exécute que lorsque vous le déclenchez, sans planification. Comme vous êtes présent à chaque fois, sa tâche peut utiliser un modèle d'abonnement à usage individuel."
201
+ "onDemandHint": "Ne s'exécute que lorsque vous le déclenchez, sans planification. Comme vous êtes présent à chaque fois, sa tâche peut utiliser un modèle d'abonnement à usage individuel.",
202
+ "intake": "Prise en charge des tickets",
203
+ "intakeHint": "Chaque exécution sélectionne un ticket ouvert correspondant dans le suivi et le traite de bout en bout.",
204
+ "intakeNoSources": "Connectez d'abord une source de tâches pour en extraire des tickets.",
205
+ "intakeGithubRepo": "Dépôt",
206
+ "intakeTitleFragment": "Le titre contient",
207
+ "intakeTitleFragmentPlaceholder": "ex. crash",
208
+ "intakeLabels": "Étiquettes",
209
+ "intakeLabelsPlaceholder": "séparées par des virgules",
210
+ "intakeIssueType": "Type de ticket",
211
+ "intakeInProgressLabel": "Étiquette en cours"
202
212
  },
203
213
  "failure": {
204
214
  "containerFailedToStart": "Le conteneur n’a pas pu démarrer",
@@ -811,7 +821,8 @@
811
821
  "auto_merge_disabled": "Le préréglage {preset} envoie chaque PR à une personne ; celle-ci attend donc une revue.",
812
822
  "no_rationale": "Le fusionneur a évalué la PR mais n'a donné aucune justification, le verdict n'a donc pas pu être approuvé pour une fusion automatique ; la PR attend une fusion par une personne.",
813
823
  "no_assessment": "Le fusionneur n'a pas renvoyé d'évaluation exploitable, la PR attend donc une fusion par une personne.",
814
- "merge_failed": "Les scores étaient dans les seuils de {preset}, mais la fusion automatique n'a pas pu aboutir (par exemple protection de branche ou conflit), la PR attend donc une fusion par une personne."
824
+ "merge_failed": "Les scores étaient dans les seuils de {preset}, mais la fusion automatique n'a pas pu aboutir (par exemple protection de branche ou conflit), la PR attend donc une fusion par une personne.",
825
+ "merge_partial": "Certaines des pull requests de la tâche ont été fusionnées, mais une suivante n'a pas pu l'être, le changement attend donc qu'une personne termine ou annule la fusion multi-dépôt."
815
826
  },
816
827
  "scores": "Scores",
817
828
  "axis": {
@@ -4048,6 +4059,26 @@
4048
4059
  "hint": "Continuer permet au planificateur de poser des questions complementaires ; Proceder planifie avec les reponses actuelles.",
4049
4060
  "proceed": "Proceder a la planification",
4050
4061
  "continue": "Continuer"
4062
+ },
4063
+ "followUpStatus": {
4064
+ "open": "Ouvert",
4065
+ "promoted": "Promu",
4066
+ "dismissed": "Rejete"
4067
+ },
4068
+ "curation": {
4069
+ "promote": "Promouvoir en element",
4070
+ "promoteConfirm": "Creer l'element",
4071
+ "dismiss": "Rejeter",
4072
+ "retry": "Reessayer",
4073
+ "skip": "Ignorer",
4074
+ "edit": "Modifier",
4075
+ "save": "Enregistrer",
4076
+ "cancel": "Annuler",
4077
+ "phaseField": "Phase",
4078
+ "itemTitlePlaceholder": "Titre de l'element (par defaut celui du suivi)",
4079
+ "maxConcurrentField": "Taches simultanees maximales",
4080
+ "defaultPipelineField": "Pipeline par defaut",
4081
+ "failed": "Impossible de mettre a jour l'initiative"
4051
4082
  }
4052
4083
  }
4053
4084
  }
@@ -198,7 +198,17 @@
198
198
  "submit": "הוסף צינור מחזורי",
199
199
  "addFailedTitle": "לא ניתן היה להוסיף צינור מחזורי",
200
200
  "onDemand": "לפי דרישה (ידני בלבד)",
201
- "onDemandHint": "רץ רק כשאתה מפעיל אותו, ללא תזמון. מכיוון שאתה נוכח בכל פעם, המשימה יכולה להשתמש במודל מנוי לשימוש אישי."
201
+ "onDemandHint": "רץ רק כשאתה מפעיל אותו, ללא תזמון. מכיוון שאתה נוכח בכל פעם, המשימה יכולה להשתמש במודל מנוי לשימוש אישי.",
202
+ "intake": "קליטת תקלות",
203
+ "intakeHint": "כל הרצה בוחרת תקלה פתוחה תואמת אחת מהמעקב ומטפלת בה מקצה לקצה.",
204
+ "intakeNoSources": "חבר תחילה מקור משימות כדי למשוך ממנו תקלות.",
205
+ "intakeGithubRepo": "מאגר",
206
+ "intakeTitleFragment": "הכותרת מכילה",
207
+ "intakeTitleFragmentPlaceholder": "למשל crash",
208
+ "intakeLabels": "תוויות",
209
+ "intakeLabelsPlaceholder": "מופרדות בפסיקים",
210
+ "intakeIssueType": "סוג תקלה",
211
+ "intakeInProgressLabel": "תווית בתהליך"
202
212
  },
203
213
  "failure": {
204
214
  "containerFailedToStart": "מכל הקונטיינר נכשל בהפעלה",
@@ -811,7 +821,8 @@
811
821
  "auto_merge_disabled": "הקדם-הגדרה {preset} שולחת כל PR לאדם, ולכן זה ממתין לבדיקה.",
812
822
  "no_rationale": "הממזג נתן ציון ל-PR אך לא סיפק נימוק, ולכן לא ניתן היה לסמוך על ההכרעה למיזוג אוטומטי; ה-PR ממתין למיזוג ידני.",
813
823
  "no_assessment": "הממזג לא החזיר הערכה שניתן לפענח, ולכן ה-PR ממתין למיזוג ידני.",
814
- "merge_failed": "הציונים היו בתוך ספי {preset}, אך המיזוג האוטומטי לא הושלם (למשל הגנת ענף או התנגשות), ולכן ה-PR ממתין למיזוג ידני."
824
+ "merge_failed": "הציונים היו בתוך ספי {preset}, אך המיזוג האוטומטי לא הושלם (למשל הגנת ענף או התנגשות), ולכן ה-PR ממתין למיזוג ידני.",
825
+ "merge_partial": "חלק מבקשות המשיכה של המשימה מוזגו, אך אחת מאוחרת יותר נכשלה, ולכן השינוי ממתין שאדם ישלים או יבטל את המיזוג הרב-מאגרי."
815
826
  },
816
827
  "scores": "ציונים",
817
828
  "axis": {
@@ -4059,6 +4070,26 @@
4059
4070
  "hint": "המשך מאפשר למתכנן לשאול שאלות המשך; עבור לתכנון מתכנן עם התשובות עד כה.",
4060
4071
  "proceed": "עבור לתכנון",
4061
4072
  "continue": "המשך"
4073
+ },
4074
+ "followUpStatus": {
4075
+ "open": "פתוח",
4076
+ "promoted": "קודם",
4077
+ "dismissed": "נדחה"
4078
+ },
4079
+ "curation": {
4080
+ "promote": "קדם לפריט",
4081
+ "promoteConfirm": "צור פריט",
4082
+ "dismiss": "התעלם",
4083
+ "retry": "נסה שוב",
4084
+ "skip": "דלג",
4085
+ "edit": "ערוך",
4086
+ "save": "שמור",
4087
+ "cancel": "בטל",
4088
+ "phaseField": "שלב",
4089
+ "itemTitlePlaceholder": "כותרת הפריט (ברירת מחדל: של המעקב)",
4090
+ "maxConcurrentField": "מקסימום משימות במקביל",
4091
+ "defaultPipelineField": "צינור ברירת מחדל",
4092
+ "failed": "לא ניתן לעדכן את היוזמה"
4062
4093
  }
4063
4094
  }
4064
4095
  }
@@ -198,7 +198,17 @@
198
198
  "submit": "繰り返しパイプラインを追加",
199
199
  "addFailedTitle": "繰り返しパイプラインを追加できませんでした",
200
200
  "onDemand": "オンデマンド(手動のみ)",
201
- "onDemandHint": "スケジュールはなく、手動で実行したときのみ動作します。毎回ユーザーが立ち会うため、タスクは個人利用のサブスクリプションモデルを使用できます。"
201
+ "onDemandHint": "スケジュールはなく、手動で実行したときのみ動作します。毎回ユーザーが立ち会うため、タスクは個人利用のサブスクリプションモデルを使用できます。",
202
+ "intake": "課題の取り込み",
203
+ "intakeHint": "各実行はトラッカーから条件に一致する未解決の課題を1件選び、最後まで対応します。",
204
+ "intakeNoSources": "課題を取り込むには、まずタスクソースを接続してください。",
205
+ "intakeGithubRepo": "リポジトリ",
206
+ "intakeTitleFragment": "タイトルに含む",
207
+ "intakeTitleFragmentPlaceholder": "例: crash",
208
+ "intakeLabels": "ラベル",
209
+ "intakeLabelsPlaceholder": "カンマ区切り",
210
+ "intakeIssueType": "課題タイプ",
211
+ "intakeInProgressLabel": "進行中ラベル"
202
212
  },
203
213
  "failure": {
204
214
  "containerFailedToStart": "コンテナの起動に失敗しました",
@@ -811,7 +821,8 @@
811
821
  "auto_merge_disabled": "{preset} プリセットはすべての PR を人に回すため、この PR はレビュー待ちです。",
812
822
  "no_rationale": "マージ担当は PR を採点しましたが根拠を示さなかったため、自動マージするには判定を信頼できませんでした。PR は人によるマージを待っています。",
813
823
  "no_assessment": "マージ担当が解析可能な評価を返さなかったため、PR は人によるマージを待っています。",
814
- "merge_failed": "スコアは {preset} のしきい値内でしたが、自動マージを完了できなかった(例: ブランチ保護や競合)ため、PR は人によるマージを待っています。"
824
+ "merge_failed": "スコアは {preset} のしきい値内でしたが、自動マージを完了できなかった(例: ブランチ保護や競合)ため、PR は人によるマージを待っています。",
825
+ "merge_partial": "タスクの一部のプルリクエストはマージされましたが、後続の1つが失敗したため、変更は人が複数リポジトリのマージを完了するか元に戻すのを待っています。"
815
826
  },
816
827
  "scores": "スコア",
817
828
  "axis": {
@@ -4060,6 +4071,26 @@
4060
4071
  "hint": "「続行」でプランナーが追加の質問をします。「計画に進む」でこれまでの回答をもとに計画します。",
4061
4072
  "proceed": "計画に進む",
4062
4073
  "continue": "続行"
4074
+ },
4075
+ "followUpStatus": {
4076
+ "open": "未対応",
4077
+ "promoted": "項目化済み",
4078
+ "dismissed": "却下"
4079
+ },
4080
+ "curation": {
4081
+ "promote": "項目に昇格",
4082
+ "promoteConfirm": "項目を作成",
4083
+ "dismiss": "却下",
4084
+ "retry": "再試行",
4085
+ "skip": "スキップ",
4086
+ "edit": "編集",
4087
+ "save": "保存",
4088
+ "cancel": "キャンセル",
4089
+ "phaseField": "フェーズ",
4090
+ "itemTitlePlaceholder": "項目のタイトル(既定はフォローアップのもの)",
4091
+ "maxConcurrentField": "最大同時実行タスク数",
4092
+ "defaultPipelineField": "既定のパイプライン",
4093
+ "failed": "イニシアチブを更新できませんでした"
4063
4094
  }
4064
4095
  }
4065
4096
  }
@@ -198,7 +198,17 @@
198
198
  "submit": "Dodaj cykliczny pipeline",
199
199
  "addFailedTitle": "Nie udało się dodać cyklicznego pipeline’u",
200
200
  "onDemand": "Na żądanie (tylko ręcznie)",
201
- "onDemandHint": "Uruchamia się tylko po ręcznym wyzwoleniu, bez harmonogramu. Ponieważ za każdym razem jesteś obecny, jego zadanie może korzystać z modelu subskrypcji do użytku indywidualnego."
201
+ "onDemandHint": "Uruchamia się tylko po ręcznym wyzwoleniu, bez harmonogramu. Ponieważ za każdym razem jesteś obecny, jego zadanie może korzystać z modelu subskrypcji do użytku indywidualnego.",
202
+ "intake": "Pobieranie zgłoszeń",
203
+ "intakeHint": "Każde uruchomienie wybiera jedno pasujące otwarte zgłoszenie z trackera i realizuje je od początku do końca.",
204
+ "intakeNoSources": "Najpierw połącz źródło zadań, aby pobierać z niego zgłoszenia.",
205
+ "intakeGithubRepo": "Repozytorium",
206
+ "intakeTitleFragment": "Tytuł zawiera",
207
+ "intakeTitleFragmentPlaceholder": "np. crash",
208
+ "intakeLabels": "Etykiety",
209
+ "intakeLabelsPlaceholder": "oddzielone przecinkami",
210
+ "intakeIssueType": "Typ zgłoszenia",
211
+ "intakeInProgressLabel": "Etykieta w toku"
202
212
  },
203
213
  "failure": {
204
214
  "containerFailedToStart": "Nie udało się uruchomić kontenera",
@@ -811,7 +821,8 @@
811
821
  "auto_merge_disabled": "Ustawienie {preset} kieruje każdy PR do człowieka, więc ten czeka na przegląd.",
812
822
  "no_rationale": "Scalający ocenił PR, ale nie podał uzasadnienia, więc werdyktowi nie można było zaufać na tyle, by scalić automatycznie; PR czeka na scalenie przez człowieka.",
813
823
  "no_assessment": "Scalający nie zwrócił możliwej do przetworzenia oceny, więc PR czeka na scalenie przez człowieka.",
814
- "merge_failed": "Oceny mieściły się w progach {preset}, ale automatyczne scalenie nie mogło się powieść (np. ochrona gałęzi lub konflikt), więc PR czeka na scalenie przez człowieka."
824
+ "merge_failed": "Oceny mieściły się w progach {preset}, ale automatyczne scalenie nie mogło się powieść (np. ochrona gałęzi lub konflikt), więc PR czeka na scalenie przez człowieka.",
825
+ "merge_partial": "Część pull requestów zadania została scalona, ale kolejnego nie udało się scalić, więc zmiana czeka, aż człowiek dokończy lub cofnie scalanie wielorepozytoryjne."
815
826
  },
816
827
  "scores": "Oceny",
817
828
  "axis": {
@@ -4048,6 +4059,26 @@
4048
4059
  "hint": "Kontynuuj pozwala planiscie zadac kolejne pytania; Przejdz do planowania planuje na podstawie dotychczasowych odpowiedzi.",
4049
4060
  "proceed": "Przejdz do planowania",
4050
4061
  "continue": "Kontynuuj"
4062
+ },
4063
+ "followUpStatus": {
4064
+ "open": "Otwarte",
4065
+ "promoted": "Awansowane",
4066
+ "dismissed": "Odrzucone"
4067
+ },
4068
+ "curation": {
4069
+ "promote": "Awansuj do elementu",
4070
+ "promoteConfirm": "Utworz element",
4071
+ "dismiss": "Odrzuc",
4072
+ "retry": "Ponow",
4073
+ "skip": "Pomin",
4074
+ "edit": "Edytuj",
4075
+ "save": "Zapisz",
4076
+ "cancel": "Anuluj",
4077
+ "phaseField": "Faza",
4078
+ "itemTitlePlaceholder": "Tytul elementu (domyslnie z zadania nastepczego)",
4079
+ "maxConcurrentField": "Maks. rownoczesnych zadan",
4080
+ "defaultPipelineField": "Domyslny pipeline",
4081
+ "failed": "Nie udalo sie zaktualizowac inicjatywy"
4051
4082
  }
4052
4083
  }
4053
4084
  }
@@ -198,7 +198,17 @@
198
198
  "submit": "Yinelenen pipeline ekle",
199
199
  "addFailedTitle": "Yinelenen pipeline eklenemedi",
200
200
  "onDemand": "İstek üzerine (yalnızca manuel)",
201
- "onDemandHint": "Yalnızca siz tetiklediğinizde çalışır, zamanlama yoktur. Her seferinde siz hazır bulunduğunuz için görevi bireysel kullanımlı bir abonelik modeli kullanabilir."
201
+ "onDemandHint": "Yalnızca siz tetiklediğinizde çalışır, zamanlama yoktur. Her seferinde siz hazır bulunduğunuz için görevi bireysel kullanımlı bir abonelik modeli kullanabilir.",
202
+ "intake": "Sorun alımı",
203
+ "intakeHint": "Her çalıştırma, izleyiciden eşleşen açık bir sorunu seçer ve baştan sona işler.",
204
+ "intakeNoSources": "Sorunları çekmek için önce bir görev kaynağı bağlayın.",
205
+ "intakeGithubRepo": "Depo",
206
+ "intakeTitleFragment": "Başlık şunu içerir",
207
+ "intakeTitleFragmentPlaceholder": "örn. crash",
208
+ "intakeLabels": "Etiketler",
209
+ "intakeLabelsPlaceholder": "virgülle ayrılmış",
210
+ "intakeIssueType": "Sorun türü",
211
+ "intakeInProgressLabel": "Devam ediyor etiketi"
202
212
  },
203
213
  "failure": {
204
214
  "containerFailedToStart": "Konteyner başlatılamadı",
@@ -811,7 +821,8 @@
811
821
  "auto_merge_disabled": "{preset} ön ayarı her PR'yi bir kişiye yönlendirir, bu yüzden bu PR inceleme bekliyor.",
812
822
  "no_rationale": "Birleştirici PR'yi puanladı ancak bir gerekçe vermedi, bu yüzden karara otomatik birleştirme için güvenilemedi; PR bir kişinin birleştirmesini bekliyor.",
813
823
  "no_assessment": "Birleştirici ayrıştırılabilir bir değerlendirme döndürmedi, bu yüzden PR bir kişinin birleştirmesini bekliyor.",
814
- "merge_failed": "Puanlar {preset} eşiklerinin içindeydi ancak otomatik birleştirme tamamlanamadı (örneğin dal koruması veya bir çakışma), bu yüzden PR bir kişinin birleştirmesini bekliyor."
824
+ "merge_failed": "Puanlar {preset} eşiklerinin içindeydi ancak otomatik birleştirme tamamlanamadı (örneğin dal koruması veya bir çakışma), bu yüzden PR bir kişinin birleştirmesini bekliyor.",
825
+ "merge_partial": "Görevin bazı pull request'leri birleştirildi ancak sonraki biri birleştirilemedi, bu yüzden değişiklik bir kişinin çoklu depo birleştirmesini tamamlamasını veya geri almasını bekliyor."
815
826
  },
816
827
  "scores": "Puanlar",
817
828
  "axis": {
@@ -4060,6 +4071,26 @@
4060
4071
  "hint": "Devam et, planlayicinin ek sorular sormasini saglar; Planlamaya gec, mevcut yanitlarla planlar.",
4061
4072
  "proceed": "Planlamaya gec",
4062
4073
  "continue": "Devam et"
4074
+ },
4075
+ "followUpStatus": {
4076
+ "open": "Acik",
4077
+ "promoted": "Yukseltildi",
4078
+ "dismissed": "Yoksayildi"
4079
+ },
4080
+ "curation": {
4081
+ "promote": "Ogeye yukselt",
4082
+ "promoteConfirm": "Oge olustur",
4083
+ "dismiss": "Yoksay",
4084
+ "retry": "Yeniden dene",
4085
+ "skip": "Atla",
4086
+ "edit": "Duzenle",
4087
+ "save": "Kaydet",
4088
+ "cancel": "Iptal",
4089
+ "phaseField": "Asama",
4090
+ "itemTitlePlaceholder": "Oge basligi (varsayilan: takip ogesininki)",
4091
+ "maxConcurrentField": "Maks. es zamanli gorev",
4092
+ "defaultPipelineField": "Varsayilan pipeline",
4093
+ "failed": "Girisim guncellenemedi"
4063
4094
  }
4064
4095
  }
4065
4096
  }
@@ -198,7 +198,17 @@
198
198
  "submit": "Додати періодичний конвеєр",
199
199
  "addFailedTitle": "Не вдалося додати періодичний конвеєр",
200
200
  "onDemand": "За запитом (лише вручну)",
201
- "onDemandHint": "Запускається лише коли ви його активуєте, без розкладу. Оскільки ви присутні щоразу, його завдання може використовувати модель підписки для індивідуального використання."
201
+ "onDemandHint": "Запускається лише коли ви його активуєте, без розкладу. Оскільки ви присутні щоразу, його завдання може використовувати модель підписки для індивідуального використання.",
202
+ "intake": "Приймання завдань",
203
+ "intakeHint": "Кожен запуск вибирає одне відкрите завдання з трекера, що відповідає умовам, і опрацьовує його від початку до кінця.",
204
+ "intakeNoSources": "Спочатку підключіть джерело завдань, щоб отримувати з нього завдання.",
205
+ "intakeGithubRepo": "Репозиторій",
206
+ "intakeTitleFragment": "Заголовок містить",
207
+ "intakeTitleFragmentPlaceholder": "напр. crash",
208
+ "intakeLabels": "Мітки",
209
+ "intakeLabelsPlaceholder": "через кому",
210
+ "intakeIssueType": "Тип завдання",
211
+ "intakeInProgressLabel": "Мітка «в роботі»"
202
212
  },
203
213
  "failure": {
204
214
  "containerFailedToStart": "Не вдалося запустити контейнер",
@@ -811,7 +821,8 @@
811
821
  "auto_merge_disabled": "Пресет {preset} надсилає кожен PR людині, тож цей очікує на перевірку.",
812
822
  "no_rationale": "Модуль злиття оцінив PR, але не надав обґрунтування, тому вердикту не можна було довіряти для автоматичного злиття; PR очікує на злиття людиною.",
813
823
  "no_assessment": "Модуль злиття не повернув придатну для обробки оцінку, тому PR очікує на злиття людиною.",
814
- "merge_failed": "Оцінки були в межах порогів {preset}, але автоматичне злиття не вдалося завершити (наприклад, захист гілки або конфлікт), тому PR очікує на злиття людиною."
824
+ "merge_failed": "Оцінки були в межах порогів {preset}, але автоматичне злиття не вдалося завершити (наприклад, захист гілки або конфлікт), тому PR очікує на злиття людиною.",
825
+ "merge_partial": "Частину пул-реквестів завдання злито, але наступний не вдалося, тому зміна очікує, поки людина завершить або скасує багаторепозиторне злиття."
815
826
  },
816
827
  "scores": "Оцінки",
817
828
  "axis": {
@@ -4048,6 +4059,26 @@
4048
4059
  "hint": "Продовжити дозволяє планувальнику ставити додаткові питання; Перейти до планування планує з наявними відповідями.",
4049
4060
  "proceed": "Перейти до планування",
4050
4061
  "continue": "Продовжити"
4062
+ },
4063
+ "followUpStatus": {
4064
+ "open": "Відкрито",
4065
+ "promoted": "Підвищено",
4066
+ "dismissed": "Відхилено"
4067
+ },
4068
+ "curation": {
4069
+ "promote": "Підвищити до елемента",
4070
+ "promoteConfirm": "Створити елемент",
4071
+ "dismiss": "Відхилити",
4072
+ "retry": "Повторити",
4073
+ "skip": "Пропустити",
4074
+ "edit": "Редагувати",
4075
+ "save": "Зберегти",
4076
+ "cancel": "Скасувати",
4077
+ "phaseField": "Фаза",
4078
+ "itemTitlePlaceholder": "Назва елемента (типово — з подальшого завдання)",
4079
+ "maxConcurrentField": "Макс. одночасних завдань",
4080
+ "defaultPipelineField": "Типовий конвеєр",
4081
+ "failed": "Не вдалося оновити ініціативу"
4051
4082
  }
4052
4083
  }
4053
4084
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.87.5",
3
+ "version": "0.88.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",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "^3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.95.0"
37
+ "@cat-factory/contracts": "0.96.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",