@cat-factory/app 0.162.2 → 0.164.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.
Files changed (34) hide show
  1. package/README.md +4 -1
  2. package/app/components/board/RecurringPipelineModal.vue +9 -22
  3. package/app/components/focus/BlockFocusView.vue +25 -19
  4. package/app/components/layout/CommandBar.vue +8 -0
  5. package/app/components/layout/IntegrationsHub.vue +7 -0
  6. package/app/components/panels/InspectorPanel.vue +13 -0
  7. package/app/components/pipeline/PipelinePicker.vue +38 -16
  8. package/app/components/pipeline/PipelinePreview.vue +65 -23
  9. package/app/components/tasks/BugHuntModal.vue +361 -0
  10. package/app/components/tasks/ContextIssuePicker.logic.spec.ts +89 -0
  11. package/app/components/tasks/ContextIssuePicker.logic.ts +74 -0
  12. package/app/components/tasks/ContextIssuePicker.vue +71 -21
  13. package/app/components/tasks/TaskImportModal.vue +2 -4
  14. package/app/composables/api/bugHunt.ts +40 -0
  15. package/app/composables/useApi.ts +2 -0
  16. package/app/pages/index.vue +2 -0
  17. package/app/stores/bugHunt.spec.ts +103 -0
  18. package/app/stores/bugHunt.ts +148 -0
  19. package/app/stores/ui/modals.ts +14 -0
  20. package/app/types/bugHunt.ts +21 -0
  21. package/app/types/domain.ts +1 -0
  22. package/app/utils/pipeline.spec.ts +41 -1
  23. package/app/utils/pipeline.ts +9 -0
  24. package/i18n/locales/de.json +61 -5
  25. package/i18n/locales/en.json +64 -5
  26. package/i18n/locales/es.json +61 -5
  27. package/i18n/locales/fr.json +61 -5
  28. package/i18n/locales/he.json +61 -5
  29. package/i18n/locales/it.json +61 -5
  30. package/i18n/locales/ja.json +61 -5
  31. package/i18n/locales/pl.json +61 -5
  32. package/i18n/locales/tr.json +61 -5
  33. package/i18n/locales/uk.json +61 -5
  34. package/package.json +2 -2
@@ -0,0 +1,40 @@
1
+ import {
2
+ adoptBugHuntCandidateContract,
3
+ listTrackerBoardsContract,
4
+ runBugHuntContract,
5
+ } from '@cat-factory/contracts'
6
+ import type { RunBugHuntInput, TaskSourceKind } from '~/types/domain'
7
+ import type { ApiContext } from './context'
8
+
9
+ /** Bug hunt: list a tracker's boards, rank a board's open unassigned bugs, adopt one. */
10
+ export function bugHuntApi({ send, sendWith, ws, pwHeaders }: ApiContext) {
11
+ return {
12
+ // The boards a hunt can run against (Jira projects / Linear teams / GitHub repos). Errors
13
+ // when the source can't enumerate them, which the modal turns into a free-text board field.
14
+ listTrackerBoards: (workspaceId: string, source: TaskSourceKind) =>
15
+ send(listTrackerBoardsContract, { pathPrefix: ws(workspaceId), pathParams: { source } }),
16
+
17
+ // Scan the board for open, unassigned bugs and rank them by impact vs complexity. A live
18
+ // external call plus a model call, so it can take a while — the modal shows progress.
19
+ runBugHunt: (workspaceId: string, source: TaskSourceKind, body: RunBugHuntInput) =>
20
+ send(runBugHuntContract, {
21
+ pathPrefix: ws(workspaceId),
22
+ pathParams: { source },
23
+ body,
24
+ }),
25
+
26
+ // Adopt the confirmed candidate as a bug task and start its run. Carries the personal
27
+ // password header when the pipeline resolves an individual-usage model, like every start.
28
+ adoptBugHuntCandidate: (
29
+ workspaceId: string,
30
+ source: TaskSourceKind,
31
+ body: { externalId: string; containerId: string; pipelineId?: string },
32
+ password?: string,
33
+ ) =>
34
+ sendWith(pwHeaders(password), adoptBugHuntCandidateContract, {
35
+ pathPrefix: ws(workspaceId),
36
+ pathParams: { source },
37
+ body,
38
+ }),
39
+ }
40
+ }
@@ -44,6 +44,7 @@ import { reviewsApi } from './api/reviews'
44
44
  import { slackApi } from './api/slack'
45
45
  import { specApi } from './api/spec'
46
46
  import { tasksApi } from './api/tasks'
47
+ import { bugHuntApi } from './api/bugHunt'
47
48
  import { testSecretsApi } from './api/testSecrets'
48
49
  import { userSecretsApi } from './api/userSecrets'
49
50
  import { userSettingsApi } from './api/userSettings'
@@ -119,6 +120,7 @@ export function useApi() {
119
120
  ...executionApi(ctx),
120
121
  ...documentsApi(ctx),
121
122
  ...tasksApi(ctx),
123
+ ...bugHuntApi(ctx),
122
124
  ...reviewsApi(ctx),
123
125
  ...followUpsApi(ctx),
124
126
  ...forkDecisionApi(ctx),
@@ -45,6 +45,7 @@ const TaskSourceConnectModal = defineAsyncComponent(
45
45
  () => import('~/components/tasks/TaskSourceConnectModal.vue'),
46
46
  )
47
47
  const TaskImportModal = defineAsyncComponent(() => import('~/components/tasks/TaskImportModal.vue'))
48
+ const BugHuntModal = defineAsyncComponent(() => import('~/components/tasks/BugHuntModal.vue'))
48
49
  const RecurringPipelineModal = defineAsyncComponent(
49
50
  () => import('~/components/board/RecurringPipelineModal.vue'),
50
51
  )
@@ -398,6 +399,7 @@ watch(
398
399
  first open (its own chunk) rather than bloating the initial bundle. -->
399
400
  <TaskSourceConnectModal v-if="ui.taskConnect" />
400
401
  <TaskImportModal v-if="ui.taskImport" />
402
+ <BugHuntModal v-if="ui.bugHunt" />
401
403
  <RecurringPipelineModal v-if="ui.addRecurringFrameId" />
402
404
  <ObservabilityPanel v-if="ui.observabilityInstanceId" />
403
405
  <OperatorDashboardPanel v-if="ui.operatorDashboardOpen" />
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { useBugHuntStore } from '~/stores/bugHunt'
3
+ import { useWorkspaceStore } from '~/stores/workspace'
4
+ import { ApiError } from '~/composables/api/errors'
5
+ import type { TaskSourceKind, TrackerBoardsView } from '~/types/domain'
6
+
7
+ // What the board picker does with a FAILED board read. Only one failure means "this tracker
8
+ // cannot enumerate boards, so type one in"; every other failure has to stay visible as an error,
9
+ // or a tracker outage silently wears the same clothes as an unsupported provider.
10
+
11
+ function apiError(statusCode: number, code: string, details?: Record<string, unknown>): ApiError {
12
+ return new ApiError(statusCode, { error: { code, message: 'nope', details } })
13
+ }
14
+
15
+ /** The unsupported-source refusal `BugHuntService.listBoards` raises. */
16
+ const boardsUnsupported = () =>
17
+ Promise.reject(apiError(400, 'validation', { reason: 'boards_unsupported' }))
18
+
19
+ /**
20
+ * Stub `useApi` ONCE, behind a handler the test can swap. The store resolves `useApi()` at setup,
21
+ * so re-stubbing after `useBugHuntStore()` would leave it holding the first stub for ever.
22
+ */
23
+ function stubApi(): {
24
+ store: ReturnType<typeof useBugHuntStore>
25
+ serve: (fn: () => Promise<unknown>) => void
26
+ } {
27
+ let handler: () => Promise<unknown> = () => Promise.resolve({ source: 'jira', boards: [] })
28
+ vi.stubGlobal('useApi', () => ({
29
+ listTrackerBoards: (_ws: string, _source: TaskSourceKind) =>
30
+ handler() as Promise<TrackerBoardsView>,
31
+ }))
32
+ return {
33
+ store: useBugHuntStore(),
34
+ serve: (fn) => {
35
+ handler = fn
36
+ },
37
+ }
38
+ }
39
+
40
+ describe('bug hunt store — board listing failures', () => {
41
+ beforeEach(() => {
42
+ useWorkspaceStore().workspaceId = 'ws1'
43
+ })
44
+
45
+ it('keeps the backend reason so the picker can offer free text for an unsupported source', async () => {
46
+ const { store, serve } = stubApi()
47
+ serve(boardsUnsupported)
48
+
49
+ await store.loadBoards('jira')
50
+
51
+ expect(store.boards).toEqual([])
52
+ expect(store.boardsErrorReason).toBe('boards_unsupported')
53
+ expect(store.boardsError).toBeTruthy()
54
+ })
55
+
56
+ it('records NO reason for a failure that is simply an unreachable tracker', async () => {
57
+ const { store, serve } = stubApi()
58
+ serve(() => Promise.reject(apiError(502, 'upstream')))
59
+
60
+ await store.loadBoards('jira')
61
+
62
+ // The modal keys its free-text fallback on the reason, so a null one keeps this an error.
63
+ expect(store.boardsErrorReason).toBeNull()
64
+ expect(store.boardsError).toBeTruthy()
65
+ })
66
+
67
+ it('clears a previous failure when a later source lists its boards fine', async () => {
68
+ const { store, serve } = stubApi()
69
+ serve(boardsUnsupported)
70
+ await store.loadBoards('jira')
71
+
72
+ serve(() =>
73
+ Promise.resolve({ source: 'linear', boards: [{ id: 't1', name: 'Eng', key: 'ENG' }] }),
74
+ )
75
+ await store.loadBoards('linear')
76
+
77
+ expect(store.boardsError).toBeNull()
78
+ expect(store.boardsErrorReason).toBeNull()
79
+ expect(store.boards.map((b) => b.id)).toEqual(['t1'])
80
+ })
81
+
82
+ it('a source switch mid-flight never lands the older tracker failure on the newer one', async () => {
83
+ const { store, serve } = stubApi()
84
+ let rejectJira!: (e: unknown) => void
85
+ serve(
86
+ () =>
87
+ new Promise((_res, rej) => {
88
+ rejectJira = rej
89
+ }),
90
+ )
91
+ const inFlight = store.loadBoards('jira')
92
+
93
+ serve(() => Promise.resolve({ source: 'linear', boards: [] }))
94
+ await store.loadBoards('linear')
95
+ rejectJira(apiError(400, 'validation', { reason: 'boards_unsupported' }))
96
+ await inFlight
97
+
98
+ // The slower tracker's refusal must not flip the newer tracker's picker to free text.
99
+ expect(store.boardsSource).toBe('linear')
100
+ expect(store.boardsErrorReason).toBeNull()
101
+ expect(store.boardsError).toBeNull()
102
+ })
103
+ })
@@ -0,0 +1,148 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type { BugHuntResult, RunBugHuntInput, TaskSourceKind, TrackerBoard } from '~/types/domain'
4
+ import { apiErrorEnvelope } from '~/composables/api/errors'
5
+ import { useWorkspaceStore } from '~/stores/workspace'
6
+ import { usePersonalSubscriptionsStore } from '~/stores/personalSubscriptions'
7
+
8
+ /** The backend's `error.details.reason` code, when it sent one (see `useApi`'s error envelope). */
9
+ function errorReasonOf(error: unknown): string | null {
10
+ const details = apiErrorEnvelope(error)?.details
11
+ if (!details || typeof details !== 'object') return null
12
+ const reason = (details as Record<string, unknown>).reason
13
+ return typeof reason === 'string' ? reason : null
14
+ }
15
+
16
+ /**
17
+ * Bug-hunt state: the boards of the tracker being browsed, the last hunt's ranked candidates,
18
+ * and the actions behind the three steps (list boards → run the hunt → adopt one candidate).
19
+ *
20
+ * Nothing here is persisted server-side — a hunt is a live read plus a ranking, so the store
21
+ * holds the whole feature's state and a page reload starts a fresh hunt. That is deliberate:
22
+ * a stale ranking of a board that has since moved on is worse than no ranking.
23
+ */
24
+ export const useBugHuntStore = defineStore('bugHunt', () => {
25
+ const api = useApi()
26
+ const workspace = useWorkspaceStore()
27
+
28
+ /** Boards of the last source `loadBoards` was called for, keyed so a source switch re-fetches. */
29
+ const boards = ref<TrackerBoard[]>([])
30
+ const boardsSource = ref<TaskSourceKind | null>(null)
31
+ const boardsLoading = ref(false)
32
+ /**
33
+ * Why board listing failed, if it did. Kept rather than swallowed: a source whose provider
34
+ * can't enumerate boards is a normal, actionable state (type the board in yourself), and an
35
+ * empty picker alone doesn't say that.
36
+ */
37
+ const boardsError = ref<string | null>(null)
38
+ /**
39
+ * The backend's machine-readable reason for that failure. Only `boards_unsupported` means
40
+ * "this tracker cannot enumerate boards, so type one in"; every other failure (an unreachable
41
+ * site, an expired token) is a real error and must be shown as one — offering a free-text
42
+ * field there would just move the same failure to the next click.
43
+ */
44
+ const boardsErrorReason = ref<string | null>(null)
45
+
46
+ const result = ref<BugHuntResult | null>(null)
47
+ const hunting = ref(false)
48
+ const huntError = ref<string | null>(null)
49
+ /** The candidate currently being adopted, so only its own row shows a spinner. */
50
+ const adopting = ref<string | null>(null)
51
+
52
+ const candidates = computed(() => result.value?.candidates ?? [])
53
+ const hasResult = computed(() => result.value !== null)
54
+
55
+ /** Load the boards a hunt can run against for one source. */
56
+ async function loadBoards(source: TaskSourceKind): Promise<void> {
57
+ boardsLoading.value = true
58
+ boardsError.value = null
59
+ boardsErrorReason.value = null
60
+ boardsSource.value = source
61
+ try {
62
+ const view = await api.listTrackerBoards(workspace.requireId(), source)
63
+ // A source switch mid-flight would otherwise land the slower response over the newer
64
+ // one, leaving the picker showing another tracker's boards.
65
+ if (boardsSource.value !== source) return
66
+ boards.value = view.boards
67
+ } catch (e) {
68
+ if (boardsSource.value !== source) return
69
+ boards.value = []
70
+ boardsError.value = e instanceof Error ? e.message : String(e)
71
+ boardsErrorReason.value = errorReasonOf(e)
72
+ } finally {
73
+ boardsLoading.value = false
74
+ }
75
+ }
76
+
77
+ /** Run a hunt and keep its ranked result. Returns false when the scan itself failed. */
78
+ async function hunt(source: TaskSourceKind, input: RunBugHuntInput): Promise<boolean> {
79
+ hunting.value = true
80
+ huntError.value = null
81
+ try {
82
+ result.value = await api.runBugHunt(workspace.requireId(), source, input)
83
+ return true
84
+ } catch (e) {
85
+ result.value = null
86
+ huntError.value = e instanceof Error ? e.message : String(e)
87
+ return false
88
+ } finally {
89
+ hunting.value = false
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Adopt a candidate as a bug task and start its run. Rides the personal password like every
95
+ * other run start, so an individual-usage model prompts here rather than failing the start.
96
+ * Returns the new block's id on success, null when the user cancelled the credential prompt.
97
+ */
98
+ async function adopt(
99
+ source: TaskSourceKind,
100
+ externalId: string,
101
+ containerId: string,
102
+ pipelineId?: string,
103
+ ): Promise<string | null> {
104
+ const personal = usePersonalSubscriptionsStore()
105
+ adopting.value = externalId
106
+ try {
107
+ let blockId: string | null = null
108
+ const ok = await personal.withCredential(async (password) => {
109
+ const adopted = await api.adoptBugHuntCandidate(
110
+ workspace.requireId(),
111
+ source,
112
+ { externalId, containerId, ...(pipelineId ? { pipelineId } : {}) },
113
+ password,
114
+ )
115
+ blockId = adopted.block.id
116
+ await workspace.refresh()
117
+ })
118
+ return ok ? blockId : null
119
+ } finally {
120
+ adopting.value = null
121
+ }
122
+ }
123
+
124
+ /** Drop the last hunt (on close, or when the source/board changes under it). */
125
+ function reset(): void {
126
+ result.value = null
127
+ huntError.value = null
128
+ adopting.value = null
129
+ }
130
+
131
+ return {
132
+ boards,
133
+ boardsSource,
134
+ boardsLoading,
135
+ boardsError,
136
+ boardsErrorReason,
137
+ result,
138
+ candidates,
139
+ hasResult,
140
+ hunting,
141
+ huntError,
142
+ adopting,
143
+ loadBoards,
144
+ hunt,
145
+ adopt,
146
+ reset,
147
+ }
148
+ })
@@ -180,6 +180,10 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
180
180
  // the create-in target AND scopes the issue search to the frame's linked repo.
181
181
  // Null → the unscoped "import an issue" surface (workspace-wide search).
182
182
  const taskImport = ref<{ source: TaskSourceKind | null; containerId: string | null } | null>(null)
183
+ // Bug hunt: pick a tracker + one of its boards, rank its open unassigned bugs, adopt one.
184
+ // `containerId` (a service frame or module) preselects where an adopted bug lands; null →
185
+ // opened standalone, and the modal offers every container on the board.
186
+ const bugHunt = ref<{ source: TaskSourceKind | null; containerId: string | null } | null>(null)
183
187
 
184
188
  // Add-task modal: the container (service frame or module) a new task is being
185
189
  // added to, or null when closed. The user types the title + description; nothing
@@ -255,6 +259,13 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
255
259
  function closeTaskImport() {
256
260
  taskImport.value = null
257
261
  }
262
+ function openBugHunt(source: TaskSourceKind | null = null, containerId: string | null = null) {
263
+ resetHubReturn()
264
+ bugHunt.value = { source, containerId }
265
+ }
266
+ function closeBugHunt() {
267
+ bugHunt.value = null
268
+ }
258
269
  function openAddTask(containerId: string, prefill: AddTaskPrefill | null = null) {
259
270
  addTaskPrefill.value = prefill
260
271
  addTaskContainerId.value = containerId
@@ -289,6 +300,7 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
289
300
  spawnPreview,
290
301
  taskConnect,
291
302
  taskImport,
303
+ bugHunt,
292
304
  addTaskContainerId,
293
305
  addTaskPrefill,
294
306
  reviewFrictionContext,
@@ -306,6 +318,8 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
306
318
  closeTaskConnect,
307
319
  openTaskImport,
308
320
  closeTaskImport,
321
+ openBugHunt,
322
+ closeBugHunt,
309
323
  openAddTask,
310
324
  closeAddTask,
311
325
  openReviewFriction,
@@ -0,0 +1,21 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Bug hunt: pick a connected tracker + one of its boards, get its open and
3
+ // unassigned bugs ranked by impact against implementation complexity, then adopt
4
+ // one onto the board and run the bug-fix pipeline on it.
5
+ //
6
+ // The interactive dual of the recurring `bug-intake` step. All wire shapes are
7
+ // sourced from @cat-factory/contracts (single source of truth).
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export type {
11
+ TrackerBoard,
12
+ TrackerBoardsView,
13
+ BugCandidate,
14
+ BugHuntAnalysis,
15
+ BugHuntAnalysisStatus,
16
+ BugHuntCandidate,
17
+ BugHuntConfidence,
18
+ BugHuntResult,
19
+ RunBugHuntInput,
20
+ AdoptBugHuntCandidateInput,
21
+ } from '@cat-factory/contracts'
@@ -170,6 +170,7 @@ export type * from './fragments'
170
170
  export type * from './skills'
171
171
  export type * from './documents'
172
172
  export type * from './tasks'
173
+ export type * from './bugHunt'
173
174
  export type * from './bootstrap'
174
175
  export type * from './envConfigRepair'
175
176
  export type * from './github'
@@ -1,13 +1,53 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { pipelineAllowedForTaskType, purposeAllowsAgentCategory } from '@cat-factory/contracts'
3
3
  import type { Block, Pipeline } from '~/types/domain'
4
- import { pipelineAllowedForManualStart } from '~/utils/pipeline'
4
+ import {
5
+ pipelineAllowedForManualStart,
6
+ pipelineDisplaySteps,
7
+ pipelineGateCount,
8
+ } from '~/utils/pipeline'
5
9
 
6
10
  // A minimal pipeline: only the fields the launch/task-type filters read matter here.
7
11
  function pipeline(over: Partial<Pipeline> = {}): Pipeline {
8
12
  return { id: 'pl_x', name: 'X', agentKinds: ['coder'], ...over } as Pipeline
9
13
  }
10
14
 
15
+ // The data behind every picker's preview pane: the ordered steps a run will actually execute.
16
+ describe('pipelineDisplaySteps', () => {
17
+ it('keeps the authored order and flags the gated steps', () => {
18
+ const p = pipeline({
19
+ agentKinds: ['task-estimator', 'coder', 'reviewer'],
20
+ gates: [false, true, false],
21
+ })
22
+ expect(pipelineDisplaySteps(p)).toEqual([
23
+ { kind: 'task-estimator', gated: false },
24
+ { kind: 'coder', gated: true },
25
+ { kind: 'reviewer', gated: false },
26
+ ])
27
+ })
28
+
29
+ it('drops steps disabled by default — they never run, so listing them would misdescribe it', () => {
30
+ // The short `enabled` array also pins "no entry ⇒ enabled": `tester` has none and stays.
31
+ const p = pipeline({ agentKinds: ['architect', 'coder', 'tester'], enabled: [false, true] })
32
+ expect(pipelineDisplaySteps(p).map((s) => s.kind)).toEqual(['coder', 'tester'])
33
+ })
34
+ })
35
+
36
+ describe('pipelineGateCount', () => {
37
+ it('counts only the gates that a run can actually stop at', () => {
38
+ expect(pipelineGateCount(pipeline({ agentKinds: ['coder'] }))).toBe(0)
39
+ expect(
40
+ pipelineGateCount(pipeline({ agentKinds: ['coder', 'merger'], gates: [true, true] })),
41
+ ).toBe(2)
42
+ // A gate declared on a disabled step gates nothing: the step is skipped at run.
43
+ expect(
44
+ pipelineGateCount(
45
+ pipeline({ agentKinds: ['coder', 'merger'], gates: [true, true], enabled: [true, false] }),
46
+ ),
47
+ ).toBe(1)
48
+ })
49
+ })
50
+
11
51
  describe('pipelineAllowedForTaskType', () => {
12
52
  it('a document task offers ONLY document-purpose pipelines', () => {
13
53
  expect(pipelineAllowedForTaskType(pipeline({ purpose: 'document' }), 'document')).toBe(true)
@@ -29,6 +29,15 @@ export function pipelineDisplaySteps(pipeline: Pipeline): PipelineDisplayStep[]
29
29
  .map(({ kind, gated }) => ({ kind, gated }))
30
30
  }
31
31
 
32
+ /**
33
+ * How many times a run of `pipeline` will stop for a human. Counted off the DISPLAYED steps, not
34
+ * `pipeline.gates`, because a gate declared on a step that is disabled by default gates nothing —
35
+ * that step never runs, so promising a stop there would misstate what the pipeline does.
36
+ */
37
+ export function pipelineGateCount(pipeline: Pipeline): number {
38
+ return pipelineDisplaySteps(pipeline).filter((s) => s.gated).length
39
+ }
40
+
32
41
  // Re-exported so a picker can import the task-type gate from the same module as the
33
42
  // launch/frame gates it composes with (the classifier itself lives in `@cat-factory/contracts`).
34
43
  export { pipelineAllowedForTaskType }
@@ -1650,7 +1650,8 @@
1650
1650
  "title": "Diesen Dienst archivieren?",
1651
1651
  "body": "„{name}“ und seine Aufgaben werden vom Board ausgeblendet. Du kannst den Dienst jederzeit wiederherstellen."
1652
1652
  },
1653
- "runBlocked": "Blockiert durch eine unerledigte Abhängigkeit: {names} | Blockiert durch {count} unerledigte Abhängigkeiten: {names}"
1653
+ "runBlocked": "Blockiert durch eine unerledigte Abhängigkeit: {names} | Blockiert durch {count} unerledigte Abhängigkeiten: {names}",
1654
+ "huntBugs": "Fehler jagen"
1654
1655
  }
1655
1656
  },
1656
1657
  "layout": {
@@ -1989,7 +1990,8 @@
1989
1990
  "accountSettings": "Konto-Einstellungen",
1990
1991
  "localModels": "Meine lokalen Runner",
1991
1992
  "sandbox": "Sandbox öffnen",
1992
- "shortcuts": "Tastenkürzel"
1993
+ "shortcuts": "Tastenkürzel",
1994
+ "bugHunt": "Fehlerjagd"
1993
1995
  },
1994
1996
  "keywords": {
1995
1997
  "newPipeline": "pipeline agents chain",
@@ -2009,7 +2011,8 @@
2009
2011
  "accountSettings": "account team members roles invitations email api keys fragment best practice library context organization personal",
2010
2012
  "localModels": "local model runner ollama lm studio llamacpp vllm endpoint",
2011
2013
  "sandbox": "sandbox prompt model test experiment judge fixture benchmark evaluate",
2012
- "shortcuts": "keyboard shortcuts keys hotkeys cheatsheet help"
2014
+ "shortcuts": "keyboard shortcuts keys hotkeys cheatsheet help",
2015
+ "bugHunt": "fehler bug jagd triage backlog tracker nicht zugewiesen"
2013
2016
  }
2014
2017
  },
2015
2018
  "shortcuts": {
@@ -2102,6 +2105,10 @@
2102
2105
  },
2103
2106
  "documentTemplates": {
2104
2107
  "label": "Vorlagen & Beispiele"
2108
+ },
2109
+ "bugHunt": {
2110
+ "label": "Fehlerjagd",
2111
+ "description": "Offene, nicht zugewiesene Fehler eines Boards bewerten und einen zum Beheben auswählen"
2105
2112
  }
2106
2113
  }
2107
2114
  },
@@ -3238,7 +3245,11 @@
3238
3245
  "attachByReference": "{ref} per Referenz anhängen",
3239
3246
  "noMatches": "Keine passenden Issues.",
3240
3247
  "emptySearchable": "Nach Titel suchen oder ein importiertes Issue auswählen.",
3241
- "emptyRefOnly": "Fügen Sie eine Issue-URL oder einen -Schlüssel ein, um es anzuhängen."
3248
+ "emptyRefOnly": "Fügen Sie eine Issue-URL oder einen -Schlüssel ein, um es anzuhängen.",
3249
+ "sourceLabel": "Quelle",
3250
+ "noSource": "Quelle wählen",
3251
+ "connectSource": "{label} verbinden",
3252
+ "enableSource": "{label} aktivieren"
3242
3253
  },
3243
3254
  "contextIssues": {
3244
3255
  "title": "Kontext-Issues",
@@ -3300,6 +3311,49 @@
3300
3311
  "oauthOr": "oder mit einem API-Schlüssel verbinden"
3301
3312
  }
3302
3313
  },
3314
+ "bugHunt": {
3315
+ "title": "Fehlerjagd",
3316
+ "intro": "Durchsuche ein Tracker-Board nach offenen, nicht zugewiesenen Fehlern und bewerte sie nach Auswirkung im Verhältnis zum geschätzten Aufwand. Wähle einen aus, und er wird zu einer Aufgabe, die die Fehlerbehebungs-Pipeline durchläuft.",
3317
+ "connectFirst": "Verbinde oder aktiviere zuerst eine Aufgabenquelle.",
3318
+ "connectSource": "{label} verbinden",
3319
+ "needFrameFirst": "Füge zuerst einen Service-Rahmen zum Board hinzu, damit ein übernommener Fehler irgendwo landen kann.",
3320
+ "tracker": "Tracker",
3321
+ "board": "Board",
3322
+ "pickBoard": "Board auswählen",
3323
+ "boardPlaceholder": "Projektschlüssel, Team-ID oder owner/repo",
3324
+ "boardsFailed": "Boards konnten nicht geladen werden: {reason}",
3325
+ "issueType": "Vorgangstyp",
3326
+ "issueTypeHelp": "Standard ist bug. Wird von Trackern ohne Vorgangstypen ignoriert.",
3327
+ "labels": "Labels",
3328
+ "labelsHelp": "Durch Komma getrennt. Alle müssen vorhanden sein.",
3329
+ "adoptInto": "Ausgewählten Fehler hinzufügen zu",
3330
+ "run": "Jagen",
3331
+ "running": "Board wird gelesen und die Funde werden bewertet…",
3332
+ "huntFailed": "Die Jagd ist fehlgeschlagen",
3333
+ "notAssessed": "Nicht bewertet",
3334
+ "recommended": "Empfohlen",
3335
+ "adopt": "Diesen wählen",
3336
+ "adopted": "{id} übernommen",
3337
+ "adoptedRunning": "Die Fehlerbehebungs-Pipeline läuft dafür.",
3338
+ "adoptFailed": "Dieser Fehler konnte nicht übernommen werden",
3339
+ "noCandidates": "Auf diesem Board gab es keine offenen, nicht zugewiesenen Fehler.",
3340
+ "ratings": "Auswirkung {impact}/5, Aufwand {complexity}/5, Konfidenz {confidence}",
3341
+ "viaModel": "Bewertet von {model}.",
3342
+ "truncated": "Es wurden nur die ersten {count} passenden Fehler geprüft; dieses Board enthält mehr.",
3343
+ "comments": "{count} Kommentar | {count} Kommentare",
3344
+ "confidence": {
3345
+ "high": "hoch",
3346
+ "medium": "mittel",
3347
+ "low": "niedrig"
3348
+ },
3349
+ "status": {
3350
+ "ranked": "Sortiert nach Auswirkung im Verhältnis zum geschätzten Aufwand.",
3351
+ "unavailable": "Es ist kein Bewertungsmodell konfiguriert, daher sind diese unbewertet.",
3352
+ "failed": "Die Bewertung konnte nicht abgeschlossen werden, daher sind diese unbewertet.",
3353
+ "over_budget": "Dieser Arbeitsbereich hat sein Ausgabenbudget überschritten, daher wurden diese nicht bewertet.",
3354
+ "empty": "Nichts zu bewerten."
3355
+ }
3356
+ },
3303
3357
  "pipeline": {
3304
3358
  "iterationCap": {
3305
3359
  "extraRound": "Noch eine Runde",
@@ -3308,10 +3362,12 @@
3308
3362
  },
3309
3363
  "preview": {
3310
3364
  "stepCount": "{count} Schritt | {count} Schritte",
3365
+ "gateCount": "{count} Freigabe | {count} Freigaben",
3311
3366
  "gated": "Manuelle Freigabe nach diesem Schritt"
3312
3367
  },
3313
3368
  "picker": {
3314
- "noneHint": "Keine Standard-Pipeline. Du wählst beim Ausführen der Aufgabe eine aus."
3369
+ "noneHint": "Keine Standard-Pipeline. Du wählst beim Ausführen der Aufgabe eine aus.",
3370
+ "hoverHint": "Fahre über eine Pipeline, um die Schritte zu sehen, die sie ausführt."
3315
3371
  },
3316
3372
  "builder": {
3317
3373
  "title": "Pipeline-Builder",