@cat-factory/app 0.293.1 → 0.295.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/board/AddTaskModal.vue +80 -0
- package/app/components/board/RecurringPipelineModal.vue +34 -20
- package/app/components/bootstrap/BootstrapModal.logic.spec.ts +54 -0
- package/app/components/bootstrap/BootstrapModal.logic.ts +44 -0
- package/app/components/bootstrap/BootstrapModal.vue +155 -16
- package/app/components/bugFishing/BugFishingWindow.vue +524 -0
- package/app/components/focus/BlockFocusView.vue +2 -0
- package/app/components/github/RepoTreeBrowser.vue +89 -6
- package/app/components/layout/NotificationsInbox.vue +15 -0
- package/app/components/panels/ResultWindowDrafts.logic.spec.ts +25 -3
- package/app/components/panels/ResultWindowShell.logic.spec.ts +4 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +46 -1
- package/app/components/slack/SlackPanel.vue +1 -0
- package/app/composables/api/bugFishing.ts +52 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/usePipelineErrorToast.ts +21 -0
- package/app/modular/result-views.ts +6 -0
- package/app/stores/agentRuns.spec.ts +1 -0
- package/app/stores/bugFishing.ts +143 -0
- package/app/stores/ui/resultViews.ts +14 -7
- package/app/stores/ui/runStepOpeners.ts +29 -1
- package/app/stores/workspaceSettings.ts +1 -0
- package/app/types/bootstrap.ts +1 -0
- package/app/types/execution.ts +9 -0
- package/app/utils/catalog.spec.ts +1 -0
- package/app/utils/catalog.ts +25 -0
- package/app/utils/repoPath.spec.ts +49 -0
- package/app/utils/repoPath.ts +28 -0
- package/i18n/locales/de.json +121 -10
- package/i18n/locales/en.json +120 -9
- package/i18n/locales/es.json +121 -10
- package/i18n/locales/fr.json +121 -10
- package/i18n/locales/he.json +121 -10
- package/i18n/locales/it.json +121 -10
- package/i18n/locales/ja.json +121 -10
- package/i18n/locales/pl.json +121 -10
- package/i18n/locales/tr.json +121 -10
- package/i18n/locales/uk.json +121 -10
- package/package.json +2 -2
|
@@ -68,6 +68,10 @@ const META: Record<Notification['type'], { icon: string; color: Accent }> = {
|
|
|
68
68
|
// The PR reviewer surfaced findings to triage. Clicking the title opens the PR-review window
|
|
69
69
|
// (see `reveal`); "act" just marks it read (findings are selected in that window, not here).
|
|
70
70
|
pr_review_ready: { icon: 'i-lucide-clipboard-check', color: 'primary' },
|
|
71
|
+
// A bug-fishing expedition finished every angle and is waiting for its catch to be triaged.
|
|
72
|
+
// Clicking the title opens the expedition window (see `reveal`); "act" just marks it read
|
|
73
|
+
// (findings are marked in that window, and each mark spawns its own fix task, not here).
|
|
74
|
+
bug_fishing_triage: { icon: 'i-lucide-fish', color: 'primary' },
|
|
71
75
|
// The initiative loop needs attention (a blocked task, or completion). Clicking the title
|
|
72
76
|
// opens the initiative tracker window; "act" just marks it read.
|
|
73
77
|
initiative: { icon: 'i-lucide-milestone', color: 'primary' },
|
|
@@ -112,6 +116,7 @@ const ACTION_KEYS: Record<Notification['type'], string> = {
|
|
|
112
116
|
fork_decision_pending: 'layout.notifications.action.fork_decision_pending',
|
|
113
117
|
judge_review: 'layout.notifications.action.judge_review',
|
|
114
118
|
pr_review_ready: 'layout.notifications.action.pr_review_ready',
|
|
119
|
+
bug_fishing_triage: 'layout.notifications.action.bug_fishing_triage',
|
|
115
120
|
initiative: 'layout.notifications.action.initiative',
|
|
116
121
|
platform_health: 'layout.notifications.action.platform_health',
|
|
117
122
|
budget_paused: 'layout.notifications.action.budget_paused',
|
|
@@ -240,6 +245,7 @@ function reveal(n: Notification) {
|
|
|
240
245
|
else if (n.type === 'fork_decision_pending') revealForkDecision(n)
|
|
241
246
|
else if (n.type === 'judge_review') revealJudge(n)
|
|
242
247
|
else if (n.type === 'pr_review_ready') revealPrReview(n)
|
|
248
|
+
else if (n.type === 'bug_fishing_triage') revealBugFishing(n)
|
|
243
249
|
else if (n.type === 'initiative') ui.openInitiativeTracker(n.blockId)
|
|
244
250
|
else ui.select(n.blockId)
|
|
245
251
|
}
|
|
@@ -295,6 +301,15 @@ function revealPrReview(n: Notification) {
|
|
|
295
301
|
else if (n.blockId) ui.select(n.blockId)
|
|
296
302
|
}
|
|
297
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Open the bug-fishing expedition window for a run whose angles have all settled. Falls back to
|
|
306
|
+
* focusing the block when the run is not loaded, exactly like its PR-review sibling.
|
|
307
|
+
*/
|
|
308
|
+
function revealBugFishing(n: Notification) {
|
|
309
|
+
if (n.executionId && execution.getInstance(n.executionId)) ui.openBugFishing(n.executionId)
|
|
310
|
+
else if (n.blockId) ui.select(n.blockId)
|
|
311
|
+
}
|
|
312
|
+
|
|
298
313
|
/**
|
|
299
314
|
* Open the human-testing window for a parked `human-test` gate: find the run's parked
|
|
300
315
|
* human-test step and open it through the universal step dispatch (its archetype declares
|
|
@@ -45,6 +45,10 @@ const WINDOWS: Record<string, { drafts: Disposition; why: string }> = {
|
|
|
45
45
|
drafts: 'confirm',
|
|
46
46
|
why: 'per-item replies + the redo comment; a reply resolves an item and the redo starts a pass',
|
|
47
47
|
},
|
|
48
|
+
'bugFishing/BugFishingWindow.vue': {
|
|
49
|
+
drafts: 'none',
|
|
50
|
+
why: 'every mark and dismiss commits on click; the fix-pipeline picker and the show-triaged toggle only shape what the NEXT click does',
|
|
51
|
+
},
|
|
48
52
|
'clarity/ClarityReviewWindow.vue': {
|
|
49
53
|
drafts: 'flush',
|
|
50
54
|
why: 'per-finding answers, each recorded on its own',
|
|
@@ -112,7 +116,16 @@ const WINDOWS: Record<string, { drafts: Disposition; why: string }> = {
|
|
|
112
116
|
* planning window's `v-model:answer="drafts[q.key]"` sat unnoticed in a 'none' row precisely because
|
|
113
117
|
* it looked enough like the lightbox pair to pass a shape test.
|
|
114
118
|
*/
|
|
115
|
-
const VIEW_STATE_BINDINGS = [
|
|
119
|
+
const VIEW_STATE_BINDINGS = [
|
|
120
|
+
'v-model:open="lightboxOpen"',
|
|
121
|
+
'v-model:index="lightboxIndex"',
|
|
122
|
+
// The bug-fishing window's two controls. Neither holds unsubmitted work: marking and dismissing
|
|
123
|
+
// a finding each commit on click, so the picker only decides which pipeline the NEXT mark runs
|
|
124
|
+
// and the toggle only decides which rows are listed. Losing either on close loses a selection,
|
|
125
|
+
// not something the user wrote.
|
|
126
|
+
'v-model="pipelineOverride"',
|
|
127
|
+
'v-model="showTriaged"',
|
|
128
|
+
]
|
|
116
129
|
|
|
117
130
|
/** Every component that mounts the shell, keyed by its path relative to `app/components`. */
|
|
118
131
|
function findConsumers(): Map<string, string> {
|
|
@@ -167,9 +180,18 @@ function stateBindings(source: string): string[] {
|
|
|
167
180
|
return [...source.matchAll(pattern)].map((match) => match[0])
|
|
168
181
|
}
|
|
169
182
|
|
|
170
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* A native form control, which holds typed input whether or not it carries a `v-model`.
|
|
185
|
+
*
|
|
186
|
+
* Checkboxes and radios are deliberately excluded: they hold a CHOICE, not typed input, so one
|
|
187
|
+
* left unsubmitted is a control the user can re-make in a click rather than something they wrote
|
|
188
|
+
* and would lose. The exclusion costs nothing, because this predicate is only the fallback for a
|
|
189
|
+
* control carrying no `v-model` at all — a toggle anybody reads is bound, and a binding is
|
|
190
|
+
* unknown to {@link VIEW_STATE_BINDINGS} until someone classifies it.
|
|
191
|
+
*/
|
|
171
192
|
function hasNativeControl(source: string): boolean {
|
|
172
|
-
|
|
193
|
+
const controls = source.match(/<(?:input|textarea|select)\b[^>]*>/g) ?? []
|
|
194
|
+
return controls.some((tag) => !/type="(?:checkbox|radio)"/.test(tag))
|
|
173
195
|
}
|
|
174
196
|
|
|
175
197
|
/** What makes this window suspect for a 'none' row, or `null` when nothing does. */
|
|
@@ -35,6 +35,10 @@ const WINDOWS: Record<string, { width: ResultWindowWidth; why: string }> = {
|
|
|
35
35
|
width: 'full',
|
|
36
36
|
why: 'options column + the choose/dismiss action rail',
|
|
37
37
|
},
|
|
38
|
+
'bugFishing/BugFishingWindow.vue': {
|
|
39
|
+
width: 'full',
|
|
40
|
+
why: 'the angle rail + the catch column, whose finding rows lay badges, path and the mark/dismiss actions out beside each other',
|
|
41
|
+
},
|
|
38
42
|
'clarity/ClarityReviewWindow.vue': {
|
|
39
43
|
width: 'full',
|
|
40
44
|
why: 'findings column + the answer/dismiss action rail',
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// by external-tool URL resolvers); present only where any are declared.
|
|
10
10
|
// The latter three are body-only section components rendered in tabs here (no longer
|
|
11
11
|
// standalone modals).
|
|
12
|
-
import { reactive, ref, watch } from 'vue'
|
|
12
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
13
13
|
import { useReactiveSlots } from '@modular-vue/runtime'
|
|
14
14
|
import type { InputGateMode, ReviewFrictionMode, TaskLimitMode } from '~/types/domain'
|
|
15
15
|
import RiskPolicyPanel from '~/components/settings/RiskPolicyPanel.vue'
|
|
@@ -22,12 +22,15 @@ import WorkspaceMembersSettings from '~/components/layout/WorkspaceMembersSettin
|
|
|
22
22
|
import WorkspaceMetadataSettings from '~/components/settings/WorkspaceMetadataSettings.vue'
|
|
23
23
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
24
24
|
import type { AppSlots } from '~/modular/slots'
|
|
25
|
+
import { usePipelinesStore } from '~/stores/pipelines'
|
|
26
|
+
import { pipelineAllowedForTaskType } from '~/utils/pipeline'
|
|
25
27
|
|
|
26
28
|
const { t, te } = useI18n()
|
|
27
29
|
const ui = useUiStore()
|
|
28
30
|
const store = useWorkspaceSettingsStore()
|
|
29
31
|
const workspace = useWorkspaceStore()
|
|
30
32
|
const access = useWorkspaceAccess()
|
|
33
|
+
const pipelines = usePipelinesStore()
|
|
31
34
|
const toast = useToast()
|
|
32
35
|
const { present } = usePipelineErrorToast()
|
|
33
36
|
const slots = useReactiveSlots<AppSlots>()
|
|
@@ -182,6 +185,22 @@ const REVIEW_FRICTION_MODES = computed<{ value: ReviewFrictionMode; label: strin
|
|
|
182
185
|
{ value: 'enforce', label: t('settings.workspaceSettings.reviewFriction.modes.enforce') },
|
|
183
186
|
])
|
|
184
187
|
|
|
188
|
+
/**
|
|
189
|
+
* The pipelines a bug-fishing expedition's spawned fix task may run, plus the "use the built-in
|
|
190
|
+
* bug-fix preset" row that clears the pin.
|
|
191
|
+
*
|
|
192
|
+
* Narrowed by the same predicate the create form uses for a `bug` task, because a spawned fix IS
|
|
193
|
+
* one — a board that could pin a document-authoring preset here would spawn tasks that author a
|
|
194
|
+
* document instead of fixing the defect they were spawned for. The clearing row carries the empty
|
|
195
|
+
* string rather than being absent, so "no board default" is a value someone can choose back to.
|
|
196
|
+
*/
|
|
197
|
+
const fixPipelineOptions = computed<{ value: string; label: string }[]>(() => [
|
|
198
|
+
{ value: '', label: t('settings.workspaceSettings.bugFishing.builtInDefault') },
|
|
199
|
+
...pipelines.pipelines
|
|
200
|
+
.filter((p) => pipelineAllowedForTaskType(p, 'bug'))
|
|
201
|
+
.map((p) => ({ value: p.id, label: p.name })),
|
|
202
|
+
])
|
|
203
|
+
|
|
185
204
|
/** The localized "Max {type} tasks" label for a per-type running-task limit input. */
|
|
186
205
|
function maxTaskTypeLabel(type: LimitTaskType): string {
|
|
187
206
|
const key = TASK_TYPE_KEYS[type]
|
|
@@ -203,6 +222,8 @@ const draft = reactive({
|
|
|
203
222
|
doneLaneRetentionDays: 14 as number,
|
|
204
223
|
kaizenEnabled: true,
|
|
205
224
|
allowInitiatorPat: true,
|
|
225
|
+
// '' means "no board default", which resolves to the built-in bug-fix preset at spawn time.
|
|
226
|
+
bugFishingFixPipelineId: '',
|
|
206
227
|
inputGateMode: 'standard' as InputGateMode,
|
|
207
228
|
reviewFrictionMode: 'off' as ReviewFrictionMode,
|
|
208
229
|
reviewFrictionWarnCount: 3,
|
|
@@ -230,6 +251,7 @@ function hydrate() {
|
|
|
230
251
|
draft.doneLaneRetentionDays = s.doneLaneRetentionDays ?? 14
|
|
231
252
|
draft.kaizenEnabled = s.kaizenEnabled
|
|
232
253
|
draft.allowInitiatorPat = s.allowInitiatorPat
|
|
254
|
+
draft.bugFishingFixPipelineId = s.bugFishingFixPipelineId ?? ''
|
|
233
255
|
draft.inputGateMode = s.inputGateMode
|
|
234
256
|
draft.reviewFrictionMode = s.reviewFrictionMode
|
|
235
257
|
draft.reviewFrictionWarnCount = s.reviewFrictionWarnCount
|
|
@@ -290,6 +312,9 @@ async function save() {
|
|
|
290
312
|
doneLaneRetentionDays: draft.doneLaneRetentionEnabled ? draft.doneLaneRetentionDays : null,
|
|
291
313
|
kaizenEnabled: draft.kaizenEnabled,
|
|
292
314
|
allowInitiatorPat: draft.allowInitiatorPat,
|
|
315
|
+
// The empty string clears the pin back to the built-in preset (the backend trims it to null),
|
|
316
|
+
// which is how every other pinned-pipeline field on the platform is cleared.
|
|
317
|
+
bugFishingFixPipelineId: draft.bugFishingFixPipelineId,
|
|
293
318
|
inputGateMode: draft.inputGateMode,
|
|
294
319
|
reviewFrictionMode: draft.reviewFrictionMode,
|
|
295
320
|
reviewFrictionWarnCount: draft.reviewFrictionWarnCount,
|
|
@@ -630,6 +655,26 @@ async function save() {
|
|
|
630
655
|
</p>
|
|
631
656
|
</section>
|
|
632
657
|
|
|
658
|
+
<!-- Bug-fishing expedition: the pipeline a MARKED finding's spawned fix task runs.
|
|
659
|
+
It is a property of how this team fixes bugs rather than of any one hunt, which
|
|
660
|
+
is why it is a board setting and not a field on the expedition. -->
|
|
661
|
+
<section class="space-y-2">
|
|
662
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
663
|
+
{{ t('settings.workspaceSettings.bugFishing.heading') }}
|
|
664
|
+
</h3>
|
|
665
|
+
<p class="text-[11px] text-slate-400">
|
|
666
|
+
{{ t('settings.workspaceSettings.bugFishing.body') }}
|
|
667
|
+
</p>
|
|
668
|
+
<USelectMenu
|
|
669
|
+
v-model="draft.bugFishingFixPipelineId"
|
|
670
|
+
:items="fixPipelineOptions"
|
|
671
|
+
value-key="value"
|
|
672
|
+
size="sm"
|
|
673
|
+
class="max-w-md"
|
|
674
|
+
data-testid="workspace-settings-bug-fishing-pipeline"
|
|
675
|
+
/>
|
|
676
|
+
</section>
|
|
677
|
+
|
|
633
678
|
<!-- Kaizen agent -->
|
|
634
679
|
<section class="space-y-2">
|
|
635
680
|
<h3 class="text-sm font-semibold text-slate-200">
|
|
@@ -74,6 +74,7 @@ const routes = reactive<Record<NotificationType, SlackRoute>>({
|
|
|
74
74
|
fork_decision_pending: { enabled: false, channel: '' },
|
|
75
75
|
judge_review: { enabled: false, channel: '' },
|
|
76
76
|
pr_review_ready: { enabled: false, channel: '' },
|
|
77
|
+
bug_fishing_triage: { enabled: false, channel: '' },
|
|
77
78
|
initiative: { enabled: false, channel: '' },
|
|
78
79
|
platform_health: { enabled: false, channel: '' },
|
|
79
80
|
budget_paused: { enabled: false, channel: '' },
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addressBugFishingFindingsContract,
|
|
3
|
+
dismissBugFishingFindingContract,
|
|
4
|
+
getBugFishingContract,
|
|
5
|
+
resolveBugFishingContract,
|
|
6
|
+
} from '@cat-factory/contracts'
|
|
7
|
+
import type { ApiContext } from './context'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The bug-fishing expedition: the read-only `bug-fisher` agent reads the service's codebase once
|
|
11
|
+
* per ANGLE and reports what each angle caught, and the run parks once every angle has settled.
|
|
12
|
+
* These endpoints read the live catch, MARK findings (each spawning its own bug-fix task), drop
|
|
13
|
+
* one from triage, and finish a parked expedition. The read returns null when no `bug-fisher`
|
|
14
|
+
* step carries expedition state.
|
|
15
|
+
*/
|
|
16
|
+
export function bugFishingApi({ send, ws }: ApiContext) {
|
|
17
|
+
return {
|
|
18
|
+
// The live expedition state for a run (null when no bug-fisher step carries one).
|
|
19
|
+
getBugFishing: (workspaceId: string, executionId: string) =>
|
|
20
|
+
send(getBugFishingContract, { pathPrefix: ws(workspaceId), pathParams: { executionId } }),
|
|
21
|
+
|
|
22
|
+
// Mark findings to be addressed: one bug-fix task per finding, linked to the expedition.
|
|
23
|
+
// Accepted while later angles are still fishing, which is the point of the phase loop.
|
|
24
|
+
// `pipelineId` overrides the board's default fix pipeline for this batch only.
|
|
25
|
+
addressBugFishingFindings: (
|
|
26
|
+
workspaceId: string,
|
|
27
|
+
executionId: string,
|
|
28
|
+
body: { findingIds: string[]; pipelineId?: string },
|
|
29
|
+
) =>
|
|
30
|
+
send(addressBugFishingFindingsContract, {
|
|
31
|
+
pathPrefix: ws(workspaceId),
|
|
32
|
+
pathParams: { executionId },
|
|
33
|
+
body,
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
// Dismiss a finding: it stays on the record, struck through, and can no longer be marked.
|
|
37
|
+
dismissBugFishingFinding: (workspaceId: string, executionId: string, findingId: string) =>
|
|
38
|
+
send(dismissBugFishingFindingContract, {
|
|
39
|
+
pathPrefix: ws(workspaceId),
|
|
40
|
+
pathParams: { executionId, findingId },
|
|
41
|
+
body: {},
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
// Finish a parked expedition (triage is done); the run advances past the step.
|
|
45
|
+
resolveBugFishing: (workspaceId: string, executionId: string) =>
|
|
46
|
+
send(resolveBugFishingContract, {
|
|
47
|
+
pathPrefix: ws(workspaceId),
|
|
48
|
+
pathParams: { executionId },
|
|
49
|
+
body: {},
|
|
50
|
+
}),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -18,6 +18,7 @@ import { forkDecisionApi } from './api/forkDecision'
|
|
|
18
18
|
import { inputGateApi } from './api/inputGate'
|
|
19
19
|
import { judgeApi } from './api/judge'
|
|
20
20
|
import { prReviewApi } from './api/prReview'
|
|
21
|
+
import { bugFishingApi } from './api/bugFishing'
|
|
21
22
|
import { fragmentsApi } from './api/fragments'
|
|
22
23
|
import { foundationalServicesApi } from './api/foundationalServices'
|
|
23
24
|
import { skillsApi } from './api/skills'
|
|
@@ -138,6 +139,7 @@ export function useApi() {
|
|
|
138
139
|
...inputGateApi(ctx),
|
|
139
140
|
...judgeApi(ctx),
|
|
140
141
|
...prReviewApi(ctx),
|
|
142
|
+
...bugFishingApi(ctx),
|
|
141
143
|
...humanTestApi(ctx),
|
|
142
144
|
...visualConfirmApi(ctx),
|
|
143
145
|
...humanReviewApi(ctx),
|
|
@@ -125,6 +125,27 @@ const CONFLICT_INFO: Record<Exclude<ConflictReason, BespokeConflictReason>, Conf
|
|
|
125
125
|
titleKey: 'errors.conflict.title.kaizen_entry_not_settled',
|
|
126
126
|
descriptionKey: 'errors.conflict.description.kaizen_entry_not_settled',
|
|
127
127
|
},
|
|
128
|
+
// The four bug-fishing-expedition refusals. `no_expedition` / `not_awaiting_triage` are the
|
|
129
|
+
// "wrong run" / "already finished" pair (the window refreshes rather than re-offering the
|
|
130
|
+
// control); `already_addressed` names findings that already have a fix task, so a second
|
|
131
|
+
// request cannot double-spawn; `no_host_frame` is a board-shape problem the operator fixes by
|
|
132
|
+
// putting the expedition under a service.
|
|
133
|
+
no_expedition: {
|
|
134
|
+
titleKey: 'errors.conflict.title.no_expedition',
|
|
135
|
+
descriptionKey: 'errors.conflict.description.no_expedition',
|
|
136
|
+
},
|
|
137
|
+
not_awaiting_triage: {
|
|
138
|
+
titleKey: 'errors.conflict.title.not_awaiting_triage',
|
|
139
|
+
descriptionKey: 'errors.conflict.description.not_awaiting_triage',
|
|
140
|
+
},
|
|
141
|
+
already_addressed: {
|
|
142
|
+
titleKey: 'errors.conflict.title.already_addressed',
|
|
143
|
+
descriptionKey: 'errors.conflict.description.already_addressed',
|
|
144
|
+
},
|
|
145
|
+
no_host_frame: {
|
|
146
|
+
titleKey: 'errors.conflict.title.no_host_frame',
|
|
147
|
+
descriptionKey: 'errors.conflict.description.no_host_frame',
|
|
148
|
+
},
|
|
128
149
|
task_limit_reached: {
|
|
129
150
|
titleKey: 'errors.conflict.title.task_limit_reached',
|
|
130
151
|
descriptionKey: 'errors.conflict.description.task_limit_reached',
|
|
@@ -48,6 +48,9 @@ const ForkDecisionWindow = defineAsyncView(
|
|
|
48
48
|
() => import('~/components/forkDecision/ForkDecisionWindow.vue'),
|
|
49
49
|
)
|
|
50
50
|
const PrReviewWindow = defineAsyncView(() => import('~/components/prReview/PrReviewWindow.vue'))
|
|
51
|
+
const BugFishingWindow = defineAsyncView(
|
|
52
|
+
() => import('~/components/bugFishing/BugFishingWindow.vue'),
|
|
53
|
+
)
|
|
51
54
|
const MergerResultView = defineAsyncView(() => import('~/components/panels/MergerResultView.vue'))
|
|
52
55
|
const InitiativeTrackerWindow = defineAsyncView(
|
|
53
56
|
() => import('~/components/initiative/InitiativeTrackerWindow.vue'),
|
|
@@ -120,6 +123,9 @@ const BUILT_IN_RESULT_VIEWS: Record<ResultViewId, Component> = {
|
|
|
120
123
|
'binary-candidates': BinaryCandidatesWindow,
|
|
121
124
|
// The PR deep-review: the reviewer's sliced, prioritized findings + the human's multi-select.
|
|
122
125
|
'pr-review': PrReviewWindow,
|
|
126
|
+
// The bug-fishing expedition: the per-angle catch, and the human's per-finding triage (each
|
|
127
|
+
// marked finding spawns its own bug-fix task).
|
|
128
|
+
'bug-fishing': BugFishingWindow,
|
|
123
129
|
// The merger's verdict: PR complexity/risk/impact scores + the engine's decision (and why).
|
|
124
130
|
merger: MergerResultView,
|
|
125
131
|
// The initiative tracker: phases, per-item status + PR links, decisions, deviations, caveats.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { BugFishingStepState } from '~/types/execution'
|
|
4
|
+
import { useApi } from '~/composables/useApi'
|
|
5
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
6
|
+
import { useExecutionStore } from '~/stores/execution'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The bug-fishing expedition's action surface. The live state lives on the run's `bug-fisher`
|
|
10
|
+
* step (`step.bugFishing`) and is kept fresh by the execution stream, so the window reads it
|
|
11
|
+
* straight off the execution store — this store only wraps the actions, tracks what is in
|
|
12
|
+
* flight so the window can disable its controls, and reflects the returned state back onto the
|
|
13
|
+
* execution store so the UI updates before the stream echoes the change. Keyed by executionId,
|
|
14
|
+
* mirroring the PR-review store.
|
|
15
|
+
*/
|
|
16
|
+
export const useBugFishingStore = defineStore('bugFishing', () => {
|
|
17
|
+
const api = useApi()
|
|
18
|
+
const workspace = useWorkspaceStore()
|
|
19
|
+
const execution = useExecutionStore()
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The finding ids whose fix task is being spawned right now. A SET rather than one boolean
|
|
23
|
+
* because marking is available while the expedition is still fishing, so a person can mark a
|
|
24
|
+
* second finding while the first is still spawning — and a shared flag would grey out the row
|
|
25
|
+
* they just clicked along with every other one.
|
|
26
|
+
*/
|
|
27
|
+
const spawning = ref<Set<string>>(new Set())
|
|
28
|
+
/** True while the finish call is in flight (drives the Finish button's spinner). */
|
|
29
|
+
const resolving = ref(false)
|
|
30
|
+
/** The last error message from an action, surfaced inline; cleared on the next action. */
|
|
31
|
+
const error = ref<string | null>(null)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Apply an authoritative expedition state to the run's `bug-fisher` step. A pipeline could
|
|
35
|
+
* carry more than one, so target the one this state is about: prefer the step still awaiting
|
|
36
|
+
* triage, then the current step, then the first step carrying expedition state.
|
|
37
|
+
*
|
|
38
|
+
* Only ever called through {@link ExecutionStore.echoAfter}, which drops the echo when the
|
|
39
|
+
* event stream already delivered a newer revision. That guard matters most here: marking a
|
|
40
|
+
* finding is accepted mid-expedition, so an unguarded echo could put a later phase's findings
|
|
41
|
+
* back to the set the mark request happened to see.
|
|
42
|
+
*/
|
|
43
|
+
function assign(
|
|
44
|
+
instance: ReturnType<typeof execution.getInstance> & object,
|
|
45
|
+
state: BugFishingStepState,
|
|
46
|
+
): void {
|
|
47
|
+
const isLive = (s: (typeof instance.steps)[number]) =>
|
|
48
|
+
s.agentKind === 'bug-fisher' && s.bugFishing?.status === 'awaiting_triage'
|
|
49
|
+
const current = instance.steps[instance.currentStep]
|
|
50
|
+
const step =
|
|
51
|
+
instance.steps.find(isLive) ??
|
|
52
|
+
(current?.agentKind === 'bug-fisher' && current.bugFishing ? current : undefined) ??
|
|
53
|
+
instance.steps.find((s) => s.bugFishing)
|
|
54
|
+
if (step) step.bugFishing = state
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Warm the live state from the GET (the stream also keeps it fresh). Best-effort. */
|
|
58
|
+
async function load(executionId: string): Promise<void> {
|
|
59
|
+
error.value = null
|
|
60
|
+
try {
|
|
61
|
+
await execution.echoAfter(
|
|
62
|
+
executionId,
|
|
63
|
+
() => api.getBugFishing(workspace.requireId(), executionId),
|
|
64
|
+
(state, instance) => {
|
|
65
|
+
if (state) assign(instance, state as BugFishingStepState)
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
} catch (e) {
|
|
69
|
+
error.value = e instanceof Error ? e.message : 'Failed to load'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mark findings to be addressed: each spawns its own bug-fix task, linked to the expedition.
|
|
75
|
+
* `pipelineId` overrides the board's default fix pipeline for this batch only.
|
|
76
|
+
*/
|
|
77
|
+
async function address(
|
|
78
|
+
executionId: string,
|
|
79
|
+
findingIds: string[],
|
|
80
|
+
pipelineId?: string,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
error.value = null
|
|
83
|
+
spawning.value = new Set([...spawning.value, ...findingIds])
|
|
84
|
+
try {
|
|
85
|
+
await execution.echoAfter(
|
|
86
|
+
executionId,
|
|
87
|
+
() =>
|
|
88
|
+
api.addressBugFishingFindings(workspace.requireId(), executionId, {
|
|
89
|
+
findingIds,
|
|
90
|
+
...(pipelineId ? { pipelineId } : {}),
|
|
91
|
+
}),
|
|
92
|
+
(state, instance) => assign(instance, state as BugFishingStepState),
|
|
93
|
+
)
|
|
94
|
+
} catch (e) {
|
|
95
|
+
error.value = e instanceof Error ? e.message : 'Failed to create the fix task'
|
|
96
|
+
throw e
|
|
97
|
+
} finally {
|
|
98
|
+
const next = new Set(spawning.value)
|
|
99
|
+
for (const id of findingIds) next.delete(id)
|
|
100
|
+
spawning.value = next
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Dismiss a finding: it stays on the record, struck through, and can no longer be marked. */
|
|
105
|
+
async function dismiss(executionId: string, findingId: string): Promise<void> {
|
|
106
|
+
error.value = null
|
|
107
|
+
spawning.value = new Set([...spawning.value, findingId])
|
|
108
|
+
try {
|
|
109
|
+
await execution.echoAfter(
|
|
110
|
+
executionId,
|
|
111
|
+
() => api.dismissBugFishingFinding(workspace.requireId(), executionId, findingId),
|
|
112
|
+
(state, instance) => assign(instance, state as BugFishingStepState),
|
|
113
|
+
)
|
|
114
|
+
} catch (e) {
|
|
115
|
+
error.value = e instanceof Error ? e.message : 'Failed to dismiss the finding'
|
|
116
|
+
throw e
|
|
117
|
+
} finally {
|
|
118
|
+
const next = new Set(spawning.value)
|
|
119
|
+
next.delete(findingId)
|
|
120
|
+
spawning.value = next
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Finish a parked expedition: triage is done and the run advances past the step. */
|
|
125
|
+
async function resolve(executionId: string): Promise<void> {
|
|
126
|
+
error.value = null
|
|
127
|
+
resolving.value = true
|
|
128
|
+
try {
|
|
129
|
+
await execution.echoAfter(
|
|
130
|
+
executionId,
|
|
131
|
+
() => api.resolveBugFishing(workspace.requireId(), executionId),
|
|
132
|
+
(state, instance) => assign(instance, state as BugFishingStepState),
|
|
133
|
+
)
|
|
134
|
+
} catch (e) {
|
|
135
|
+
error.value = e instanceof Error ? e.message : 'Failed to finish the expedition'
|
|
136
|
+
throw e
|
|
137
|
+
} finally {
|
|
138
|
+
resolving.value = false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { spawning, resolving, error, load, address, dismiss, resolve }
|
|
143
|
+
})
|
|
@@ -164,13 +164,19 @@ export function createUiResultViews() {
|
|
|
164
164
|
// The run-scoped openers (a caller that knows only the RUN, so the step index has to be
|
|
165
165
|
// resolved) live in a sibling module: they share one shape and one hazard, and lifting them out
|
|
166
166
|
// keeps this factory inside its per-function line budget. Their two seams are bound here.
|
|
167
|
-
const {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
const {
|
|
168
|
+
openFollowUps,
|
|
169
|
+
openForkDecision,
|
|
170
|
+
openBinaryCandidates,
|
|
171
|
+
openPrReview,
|
|
172
|
+
openBugFishing,
|
|
173
|
+
openTestEvidence,
|
|
174
|
+
} = createRunStepOpeners({
|
|
175
|
+
dispatchStepView: (instanceId, stepIndex) => dispatchStepView(instanceId, stepIndex),
|
|
176
|
+
setResultView: (view, instance, stepIndex) => {
|
|
177
|
+
resultView.value = { view, blockId: instance.blockId, instanceId: instance.id, stepIndex }
|
|
178
|
+
},
|
|
179
|
+
})
|
|
174
180
|
|
|
175
181
|
function closeResultView() {
|
|
176
182
|
resultView.value = null
|
|
@@ -212,6 +218,7 @@ export function createUiResultViews() {
|
|
|
212
218
|
openForkDecision,
|
|
213
219
|
openBinaryCandidates,
|
|
214
220
|
openPrReview,
|
|
221
|
+
openBugFishing,
|
|
215
222
|
openTestEvidence,
|
|
216
223
|
openOutcome,
|
|
217
224
|
openRunOutcome,
|
|
@@ -136,6 +136,27 @@ export function createRunStepOpeners(deps: RunStepOpenerDeps) {
|
|
|
136
136
|
)
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Open the BUG-FISHING expedition window for a run's `bug-fisher` step (from the
|
|
140
|
+
// `bug_fishing_triage` notification / the step). Resolves the step index from the run when not
|
|
141
|
+
// given, preferring the step parked awaiting triage.
|
|
142
|
+
function openBugFishing(instanceId: string, stepIndex: number | null = null) {
|
|
143
|
+
withStep(
|
|
144
|
+
instanceId,
|
|
145
|
+
stepIndex,
|
|
146
|
+
(instance) => {
|
|
147
|
+
const awaiting = indexOf(
|
|
148
|
+
instance,
|
|
149
|
+
(s) => s.agentKind === 'bug-fisher' && s.bugFishing?.status === 'awaiting_triage',
|
|
150
|
+
)
|
|
151
|
+
if (awaiting >= 0) return awaiting
|
|
152
|
+
const current = instance.steps[instance.currentStep]
|
|
153
|
+
if (current?.agentKind === 'bug-fisher' && current.bugFishing) return instance.currentStep
|
|
154
|
+
return indexOf(instance, (s) => s.agentKind === 'bug-fisher' && !!s.bugFishing)
|
|
155
|
+
},
|
|
156
|
+
(instance, idx) => deps.setResultView('bug-fishing', instance, idx),
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
139
160
|
// Open the Tester's result window for a run, where the screenshots and per-area outcomes it
|
|
140
161
|
// captured are rendered. The entry point is the `test-evidence` deep link the engine puts in
|
|
141
162
|
// every PR verification report's environment-lifecycle section, so the caller only ever knows
|
|
@@ -161,5 +182,12 @@ export function createRunStepOpeners(deps: RunStepOpenerDeps) {
|
|
|
161
182
|
)
|
|
162
183
|
}
|
|
163
184
|
|
|
164
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
openFollowUps,
|
|
187
|
+
openForkDecision,
|
|
188
|
+
openBinaryCandidates,
|
|
189
|
+
openPrReview,
|
|
190
|
+
openBugFishing,
|
|
191
|
+
openTestEvidence,
|
|
192
|
+
}
|
|
165
193
|
}
|
|
@@ -35,6 +35,7 @@ const DEFAULTS: WorkspaceSettings = {
|
|
|
35
35
|
allowInitiatorPat: true,
|
|
36
36
|
// The custom metadata bag: empty until someone fills a declared field in. Never null — an
|
|
37
37
|
// external-tool resolver indexes it (`ctx.metadata.gameId`) with no guard.
|
|
38
|
+
bugFishingFixPipelineId: null,
|
|
38
39
|
metadata: {},
|
|
39
40
|
}
|
|
40
41
|
|
package/app/types/bootstrap.ts
CHANGED
package/app/types/execution.ts
CHANGED
|
@@ -83,6 +83,15 @@ export type {
|
|
|
83
83
|
PrReviewResolution,
|
|
84
84
|
PrReviewPostReport,
|
|
85
85
|
PrReviewPostFailure,
|
|
86
|
+
BugFishingStepState,
|
|
87
|
+
BugFishingStatus,
|
|
88
|
+
BugFishingPhase,
|
|
89
|
+
BugFishingPhaseStatus,
|
|
90
|
+
BugFishingFinding,
|
|
91
|
+
BugFishingSeverity,
|
|
92
|
+
BugFishingFindingKind,
|
|
93
|
+
BugFishingConfidence,
|
|
94
|
+
BugFishingSpawn,
|
|
86
95
|
AgentEffortReport,
|
|
87
96
|
FragmentAdherence,
|
|
88
97
|
FragmentAdherenceItem,
|
package/app/utils/catalog.ts
CHANGED
|
@@ -80,6 +80,25 @@ export const AGENT_ARCHETYPES: AgentArchetype[] = [
|
|
|
80
80
|
'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).',
|
|
81
81
|
resultView: 'generic-structured',
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
// The BUG-FISHING expedition's single step, dispatched once per ANGLE by the engine's phase
|
|
85
|
+
// loop. Registered on the backend so it also arrives via the workspace manifest, but modelled
|
|
86
|
+
// statically here for the reason `pr-reviewer` is: `agentKindMeta('bug-fisher').resultView`
|
|
87
|
+
// has to resolve to the expedition window on every surface, not only once the manifest is
|
|
88
|
+
// hydrated. Mirrors the backend `presentation` in `bug-fisher.ts`.
|
|
89
|
+
kind: 'bug-fisher',
|
|
90
|
+
tier: 'intermediate',
|
|
91
|
+
label: 'Bug Fisher',
|
|
92
|
+
icon: 'i-lucide-fish',
|
|
93
|
+
color: '#0ea5e9',
|
|
94
|
+
category: 'review',
|
|
95
|
+
// Hunts for defects nobody reported: it belongs with the bug work rather than the build
|
|
96
|
+
// ladder, and with review because its product is findings.
|
|
97
|
+
purposes: ['bugfix', 'review'],
|
|
98
|
+
description:
|
|
99
|
+
'Read-only, multi-angle hunt through an existing codebase for genuine logic gaps, latent bugs, footguns and unhandled edge cases — one pass per angle, nothing changed.',
|
|
100
|
+
resultView: 'bug-fishing',
|
|
101
|
+
},
|
|
83
102
|
{
|
|
84
103
|
// A read-only, token-bounded deep review of an EXISTING open pull request (the `pl_review`
|
|
85
104
|
// pipeline's single step). Registered on the backend so it also arrives via the workspace
|
|
@@ -953,6 +972,12 @@ export const TASK_TYPE_META: Record<string, TaskTypeMeta> = {
|
|
|
953
972
|
color: '#f87171',
|
|
954
973
|
labelKey: 'board.addTask.types.bug',
|
|
955
974
|
},
|
|
975
|
+
'bug-fishing': {
|
|
976
|
+
taskType: 'bug-fishing',
|
|
977
|
+
icon: 'i-lucide-fish',
|
|
978
|
+
color: '#0ea5e9',
|
|
979
|
+
labelKey: 'board.addTask.types.bugFishing',
|
|
980
|
+
},
|
|
956
981
|
document: {
|
|
957
982
|
taskType: 'document',
|
|
958
983
|
icon: 'i-lucide-file-text',
|