@cat-factory/app 0.6.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 (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +143 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AddTaskModal.vue +444 -0
  10. package/app/components/board/AgentFailureCard.vue +97 -0
  11. package/app/components/board/AgentStopButton.vue +61 -0
  12. package/app/components/board/BoardCanvas.vue +183 -0
  13. package/app/components/board/ContextPicker.vue +367 -0
  14. package/app/components/board/RecurringPipelineModal.vue +219 -0
  15. package/app/components/board/TaskDependencyEdges.vue +132 -0
  16. package/app/components/board/nodes/AgentChip.vue +59 -0
  17. package/app/components/board/nodes/BlockNode.vue +433 -0
  18. package/app/components/board/nodes/DecisionBadge.vue +27 -0
  19. package/app/components/board/nodes/DraggableTask.vue +48 -0
  20. package/app/components/board/nodes/ModuleFrame.vue +97 -0
  21. package/app/components/board/nodes/TaskCard.vue +359 -0
  22. package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
  23. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  24. package/app/components/clarity/ClarityReviewWindow.vue +611 -0
  25. package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
  26. package/app/components/documents/DocumentImportModal.vue +161 -0
  27. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  28. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  29. package/app/components/documents/TaskContextDocs.vue +83 -0
  30. package/app/components/focus/BlockFocusView.vue +171 -0
  31. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  32. package/app/components/gates/GateResultView.vue +282 -0
  33. package/app/components/github/AddServiceFromRepoModal.vue +354 -0
  34. package/app/components/github/GitHubConnect.vue +183 -0
  35. package/app/components/github/GitHubOnboarding.vue +45 -0
  36. package/app/components/github/GitHubPanel.vue +584 -0
  37. package/app/components/github/RepoTreeBrowser.vue +171 -0
  38. package/app/components/layout/AccountTeamSettings.vue +237 -0
  39. package/app/components/layout/BoardSwitcher.vue +280 -0
  40. package/app/components/layout/BoardToolbar.vue +156 -0
  41. package/app/components/layout/CommandBar.vue +336 -0
  42. package/app/components/layout/GitHubPatBanner.vue +73 -0
  43. package/app/components/layout/NotificationsInbox.vue +175 -0
  44. package/app/components/layout/SideBar.vue +314 -0
  45. package/app/components/layout/SpendWarningBanner.vue +107 -0
  46. package/app/components/observability/StepMetricsBar.vue +102 -0
  47. package/app/components/palettes/AgentPalette.vue +86 -0
  48. package/app/components/panels/AgentStepDetail.vue +737 -0
  49. package/app/components/panels/DecisionModal.vue +71 -0
  50. package/app/components/panels/InspectorPanel.vue +465 -0
  51. package/app/components/panels/ObservabilityPanel.vue +351 -0
  52. package/app/components/panels/StepMetadataCard.vue +253 -0
  53. package/app/components/panels/StepRestartControl.vue +90 -0
  54. package/app/components/panels/StepResultViewHost.vue +40 -0
  55. package/app/components/panels/StepTestReport.vue +84 -0
  56. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  57. package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
  58. package/app/components/panels/inspector/ServiceFragments.vue +82 -0
  59. package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
  60. package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
  61. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  62. package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
  63. package/app/components/panels/inspector/TaskExecution.vue +364 -0
  64. package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
  65. package/app/components/panels/inspector/TaskStructure.vue +96 -0
  66. package/app/components/pipeline/AgentKindIcon.vue +30 -0
  67. package/app/components/pipeline/IterationCapPrompt.vue +70 -0
  68. package/app/components/pipeline/PipelineBuilder.vue +817 -0
  69. package/app/components/pipeline/PipelineProgress.vue +484 -0
  70. package/app/components/providers/ApiKeysSection.vue +273 -0
  71. package/app/components/providers/PersonalCredentialModal.vue +128 -0
  72. package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
  73. package/app/components/providers/VendorCredentialsModal.vue +197 -0
  74. package/app/components/recurring/RecurrenceEditor.vue +124 -0
  75. package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
  76. package/app/components/settings/DatadogPanel.vue +213 -0
  77. package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
  78. package/app/components/settings/MergeThresholdsPanel.vue +378 -0
  79. package/app/components/settings/ModelDefaultsPanel.vue +250 -0
  80. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
  81. package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
  82. package/app/components/slack/SlackPanel.vue +299 -0
  83. package/app/components/tasks/TaskContextIssues.vue +88 -0
  84. package/app/components/tasks/TaskImportModal.vue +207 -0
  85. package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
  86. package/app/components/testing/TestReportWindow.vue +404 -0
  87. package/app/composables/api/accounts.ts +81 -0
  88. package/app/composables/api/auth.ts +45 -0
  89. package/app/composables/api/board.ts +101 -0
  90. package/app/composables/api/bootstrap.ts +62 -0
  91. package/app/composables/api/context.ts +25 -0
  92. package/app/composables/api/documents.ts +74 -0
  93. package/app/composables/api/execution.ts +127 -0
  94. package/app/composables/api/fragments.ts +71 -0
  95. package/app/composables/api/github.ts +131 -0
  96. package/app/composables/api/models.ts +127 -0
  97. package/app/composables/api/notifications.ts +23 -0
  98. package/app/composables/api/presets.ts +29 -0
  99. package/app/composables/api/recurring.ts +68 -0
  100. package/app/composables/api/releaseHealth.ts +43 -0
  101. package/app/composables/api/reviews.ts +146 -0
  102. package/app/composables/api/slack.ts +54 -0
  103. package/app/composables/api/tasks.ts +72 -0
  104. package/app/composables/api/workspaces.ts +36 -0
  105. package/app/composables/useApi.ts +89 -0
  106. package/app/composables/useBlockDrag.ts +90 -0
  107. package/app/composables/useBlockQueries.ts +154 -0
  108. package/app/composables/useBoardFlow.ts +11 -0
  109. package/app/composables/useContextLinking.ts +65 -0
  110. package/app/composables/useDepLabels.ts +26 -0
  111. package/app/composables/useFrameResize.ts +54 -0
  112. package/app/composables/useResultView.ts +48 -0
  113. package/app/composables/useReviewStage.ts +40 -0
  114. package/app/composables/useSemanticZoom.ts +31 -0
  115. package/app/composables/useStepApproval.ts +233 -0
  116. package/app/composables/useStepProse.ts +78 -0
  117. package/app/composables/useStepTimer.ts +63 -0
  118. package/app/composables/useTaskExpansion.ts +92 -0
  119. package/app/composables/useWorkspaceStream.ts +155 -0
  120. package/app/docs/architecture.md +31 -0
  121. package/app/pages/index.vue +141 -0
  122. package/app/stores/accounts.ts +152 -0
  123. package/app/stores/agentConfig.ts +35 -0
  124. package/app/stores/agentRuns.ts +122 -0
  125. package/app/stores/agents.ts +40 -0
  126. package/app/stores/apiKeys.ts +108 -0
  127. package/app/stores/auth.ts +166 -0
  128. package/app/stores/board.spec.ts +205 -0
  129. package/app/stores/board.ts +286 -0
  130. package/app/stores/bootstrap.ts +97 -0
  131. package/app/stores/clarity.ts +196 -0
  132. package/app/stores/consensus.ts +60 -0
  133. package/app/stores/documents.ts +176 -0
  134. package/app/stores/execution.ts +273 -0
  135. package/app/stores/fragmentLibrary.ts +147 -0
  136. package/app/stores/fragments.ts +40 -0
  137. package/app/stores/github.ts +305 -0
  138. package/app/stores/localModels.ts +51 -0
  139. package/app/stores/mergePresets.ts +58 -0
  140. package/app/stores/modelDefaults.ts +76 -0
  141. package/app/stores/models.ts +134 -0
  142. package/app/stores/notifications.ts +70 -0
  143. package/app/stores/observability.ts +144 -0
  144. package/app/stores/personalSubscriptions.ts +215 -0
  145. package/app/stores/pipelines.ts +327 -0
  146. package/app/stores/recurringPipelines.ts +112 -0
  147. package/app/stores/releaseHealth.ts +75 -0
  148. package/app/stores/requirements.spec.ts +94 -0
  149. package/app/stores/requirements.ts +208 -0
  150. package/app/stores/serviceFragmentDefaults.ts +29 -0
  151. package/app/stores/services.ts +87 -0
  152. package/app/stores/slack.ts +142 -0
  153. package/app/stores/taskExpansion.ts +36 -0
  154. package/app/stores/tasks.spec.ts +71 -0
  155. package/app/stores/tasks.ts +176 -0
  156. package/app/stores/tracker.ts +27 -0
  157. package/app/stores/ui.ts +434 -0
  158. package/app/stores/vendorCredentials.ts +54 -0
  159. package/app/stores/workspace.ts +215 -0
  160. package/app/stores/workspaceSettings.ts +36 -0
  161. package/app/types/accounts.ts +77 -0
  162. package/app/types/bootstrap.ts +83 -0
  163. package/app/types/clarity.ts +59 -0
  164. package/app/types/consensus.ts +91 -0
  165. package/app/types/documents.ts +104 -0
  166. package/app/types/domain.ts +495 -0
  167. package/app/types/execution.ts +383 -0
  168. package/app/types/fragments.ts +72 -0
  169. package/app/types/github.ts +173 -0
  170. package/app/types/localModels.ts +73 -0
  171. package/app/types/merge.ts +71 -0
  172. package/app/types/models.ts +157 -0
  173. package/app/types/notifications.ts +74 -0
  174. package/app/types/recurring.ts +69 -0
  175. package/app/types/releaseHealth.ts +31 -0
  176. package/app/types/requirements.ts +61 -0
  177. package/app/types/services.ts +27 -0
  178. package/app/types/slack.ts +57 -0
  179. package/app/types/tasks.ts +82 -0
  180. package/app/types/tracker.ts +18 -0
  181. package/app/utils/agentOutput.spec.ts +128 -0
  182. package/app/utils/agentOutput.ts +173 -0
  183. package/app/utils/catalog.spec.ts +112 -0
  184. package/app/utils/catalog.ts +455 -0
  185. package/app/utils/dnd.ts +29 -0
  186. package/app/utils/observability.ts +52 -0
  187. package/app/utils/pipelineRender.ts +151 -0
  188. package/nuxt.config.ts +55 -0
  189. package/package.json +45 -0
@@ -0,0 +1,364 @@
1
+ <script setup lang="ts">
2
+ import type { Block } from '~/types/domain'
3
+ import { agentKindMeta } from '~/utils/catalog'
4
+ import { gateCompanionFor, COMPANION_STATE_META, isCompanionKind } from '~/utils/pipelineRender'
5
+ import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
6
+
7
+ const props = defineProps<{ block: Block }>()
8
+
9
+ const execution = useExecutionStore()
10
+ const agentRuns = useAgentRunsStore()
11
+ const ui = useUiStore()
12
+ const models = useModelsStore()
13
+ const reviews = useReviewStage()
14
+
15
+ // The async stage this task's iterative reviewer gate (requirements-review / clarity-review)
16
+ // is mid-cycle in (folding the answers, then re-reviewing), or null. While set, the gate is
17
+ // doing background work and needs NO human, so its "Review" button is replaced by a working
18
+ // indicator.
19
+ const reviewStage = computed(() => reviews.stageForBlock(props.block.id))
20
+ const reviewStageLabel = computed(() =>
21
+ reviewStage.value === 'incorporating'
22
+ ? 'Incorporating…'
23
+ : reviewStage.value === 'reviewing'
24
+ ? 'Re-reviewing…'
25
+ : null,
26
+ )
27
+
28
+ const instance = computed(() => execution.getInstance(props.block.executionId))
29
+ // A failed run is no longer executing: a step left mid-flight must stop showing
30
+ // its live "Spinning up…" phase (the shared failure banner renders below).
31
+ const runFailed = computed(() => instance.value?.status === 'failed')
32
+
33
+ // A failed pipeline run surfaces the shared failure banner + retry — the
34
+ // execution failure surface that the old `pr_ready` flip used to hide.
35
+ const failedRun = computed(() => {
36
+ const run = agentRuns.byBlock[props.block.id]
37
+ return run && run.status === 'failed' ? run : null
38
+ })
39
+
40
+ const pr = computed(() => props.block.pullRequest)
41
+ /** A PR is merged once the block is `done`; otherwise it is open awaiting merge. */
42
+ const prMerged = computed(() => props.block.status === 'done')
43
+ const prLabel = computed(() => {
44
+ const number = pr.value?.number
45
+ return number ? `PR #${number}` : 'Pull request'
46
+ })
47
+
48
+ const stepLabel: Record<string, string> = {
49
+ pending: 'Pending',
50
+ working: 'Working',
51
+ waiting_decision: 'Needs decision',
52
+ done: 'Done',
53
+ }
54
+
55
+ /** A step left mid-flight (`working`) on a failed run gave up — not still working. */
56
+ function stepFailed(s: { state: string }) {
57
+ return runFailed.value && s.state === 'working'
58
+ }
59
+
60
+ /** A gated step parked for approval reads "Needs approval", not "Needs decision". */
61
+ function labelForStep(s: {
62
+ state: string
63
+ agentKind?: string
64
+ approval?: { status: string } | null
65
+ companion?: { exceeded?: boolean } | null
66
+ startingContainer?: boolean
67
+ }) {
68
+ // A step left mid-flight on a failed run reads "Failed", not the misleading "Working".
69
+ if (stepFailed(s)) return 'Failed'
70
+ // A reviewer gate mid-cycle reads its working stage, not "Needs approval".
71
+ if (reviews.isBackground(s.agentKind, props.block.id) && reviewStageLabel.value)
72
+ return reviewStageLabel.value
73
+ // A companion that spent its rework budget needs a decision, not an approval.
74
+ if (s.approval?.status === 'pending' && s.companion?.exceeded) return 'Needs decision'
75
+ if (s.approval?.status === 'pending') return 'Needs approval'
76
+ // A container-backed step whose container is still cold-booting (only while the
77
+ // run is live — a failed run's mid-flight step is no longer spinning up).
78
+ if (s.startingContainer && !runFailed.value) return 'Spinning up…'
79
+ return stepLabel[s.state]
80
+ }
81
+
82
+ function openDecisionFor(decisionId: string) {
83
+ if (instance.value) ui.openDecision(instance.value.id, decisionId)
84
+ }
85
+
86
+ function openApprovalFor(approvalId: string) {
87
+ if (instance.value) ui.openApprovalDetail(instance.value.id, approvalId)
88
+ }
89
+
90
+ // Clicking any agent opens its step-detail overlay — execution metadata (state,
91
+ // timing, model, subtasks) plus the full prose output when the agent produced one.
92
+ function openStep(i: number) {
93
+ if (instance.value) ui.openStepDetail(instance.value.id, i)
94
+ }
95
+
96
+ // Stop the run WITHOUT deleting it: halts the container + driver and records a
97
+ // `cancelled` failure, leaving the run readable + retryable (the block goes
98
+ // `blocked`). The destructive reset (delete the run, return the task to `planned`)
99
+ // is a separate, explicit action.
100
+ const stopping = ref(false)
101
+ async function stopRun() {
102
+ if (!instance.value || stopping.value) return
103
+ stopping.value = true
104
+ try {
105
+ await execution.stop(instance.value.id)
106
+ } finally {
107
+ stopping.value = false
108
+ }
109
+ }
110
+ const resetting = ref(false)
111
+ async function resetRun() {
112
+ if (resetting.value) return
113
+ resetting.value = true
114
+ try {
115
+ await execution.cancel(props.block.id)
116
+ } finally {
117
+ resetting.value = false
118
+ }
119
+ }
120
+ </script>
121
+
122
+ <template>
123
+ <div class="space-y-4">
124
+ <!-- running pipeline -->
125
+ <div v-if="instance">
126
+ <div class="mb-1 flex items-center justify-between">
127
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
128
+ {{ instance.pipelineName }}
129
+ </span>
130
+ <div class="flex items-center gap-1">
131
+ <!-- Stop without deleting: halts the run but keeps it readable + retryable. -->
132
+ <UButton
133
+ icon="i-lucide-square"
134
+ color="warning"
135
+ variant="ghost"
136
+ size="xs"
137
+ :loading="stopping"
138
+ :disabled="resetting"
139
+ title="Stop the run but keep it (readable + retryable)"
140
+ @click="stopRun"
141
+ >
142
+ Stop
143
+ </UButton>
144
+ <!-- Destructive: discard the run and return the task to planned. -->
145
+ <UButton
146
+ icon="i-lucide-trash-2"
147
+ color="error"
148
+ variant="ghost"
149
+ size="xs"
150
+ :loading="resetting"
151
+ :disabled="stopping"
152
+ title="Discard this run and reset the task to planned"
153
+ @click="resetRun"
154
+ >
155
+ Reset
156
+ </UButton>
157
+ </div>
158
+ </div>
159
+ <ul class="space-y-1">
160
+ <li
161
+ v-for="(s, i) in instance.steps"
162
+ :key="i"
163
+ class="rounded-md px-2 py-1"
164
+ :class="i === instance.currentStep ? 'bg-slate-800/70' : ''"
165
+ >
166
+ <div class="flex items-center gap-2">
167
+ <!-- Every agent is clickable: it opens the step-detail overlay (timing,
168
+ model, subtasks + the prose output when there is one). -->
169
+ <button
170
+ type="button"
171
+ class="flex min-w-0 cursor-pointer items-center gap-2 text-left transition hover:text-white"
172
+ :title="s.output ? 'View details & read output' : 'View step details'"
173
+ @click="openStep(i)"
174
+ >
175
+ <UIcon
176
+ :name="agentKindMeta(s.agentKind).icon"
177
+ class="h-4 w-4 shrink-0"
178
+ :style="{ color: agentKindMeta(s.agentKind).color }"
179
+ />
180
+ <span class="truncate text-xs text-slate-200">
181
+ {{ agentKindMeta(s.agentKind).label }}
182
+ </span>
183
+ <span
184
+ v-if="isCompanionKind(s.agentKind)"
185
+ class="shrink-0 rounded bg-slate-700/60 px-1 text-[9px] font-medium uppercase tracking-wide text-slate-300"
186
+ title="Companion of a producer step"
187
+ >
188
+ Companion
189
+ </span>
190
+ <UIcon
191
+ :name="s.output ? 'i-lucide-book-open-text' : 'i-lucide-info'"
192
+ class="h-3.5 w-3.5 shrink-0 text-slate-500"
193
+ />
194
+ </button>
195
+ <span
196
+ v-if="s.subtasks && s.subtasks.total > 0"
197
+ class="ml-auto font-mono text-[10px] tabular-nums text-slate-300"
198
+ :title="
199
+ s.subtasks.inProgress > 0
200
+ ? `${s.subtasks.completed} of ${s.subtasks.total} subtasks done, ${s.subtasks.inProgress} in progress`
201
+ : `${s.subtasks.completed} of ${s.subtasks.total} subtasks done`
202
+ "
203
+ >
204
+ {{ s.subtasks.completed }}/{{ s.subtasks.total }}
205
+ </span>
206
+ <span
207
+ class="inline-flex items-center gap-1 text-[10px]"
208
+ :class="[
209
+ stepFailed(s) ? 'text-rose-400' : 'text-slate-400',
210
+ { 'ml-auto': !s.subtasks },
211
+ ]"
212
+ >
213
+ <UIcon v-if="stepFailed(s)" name="i-lucide-circle-x" class="h-3 w-3 shrink-0" />
214
+ {{ labelForStep(s) }}
215
+ </span>
216
+ <UButton
217
+ v-if="s.decision && !s.decision.chosen"
218
+ color="warning"
219
+ variant="soft"
220
+ size="xs"
221
+ icon="i-lucide-circle-help"
222
+ @click="openDecisionFor(s.decision.id)"
223
+ >
224
+ Resolve
225
+ </UButton>
226
+ <!-- reviewer gate folding/re-reviewing in the background: a working
227
+ indicator, NOT a "Review" gate (the human is summoned only if needed) -->
228
+ <span
229
+ v-else-if="reviews.isBackground(s.agentKind, block.id) && reviewStage"
230
+ class="inline-flex shrink-0 items-center gap-1 text-[10px] text-indigo-300"
231
+ >
232
+ <UIcon name="i-lucide-loader-circle" class="h-3 w-3 animate-spin" />
233
+ {{ reviewStageLabel }}
234
+ </span>
235
+ <!-- A companion that spent its rework budget parks on the iteration-cap
236
+ gate: it needs a 3-way DECISION (one more round / proceed / stop &
237
+ reset), not a plain approval — flag it distinctly so it can't read as
238
+ a normal "Approve". Opens the same detail surface (IterationCapPrompt). -->
239
+ <UButton
240
+ v-else-if="s.approval && s.approval.status === 'pending' && s.companion?.exceeded"
241
+ color="error"
242
+ variant="soft"
243
+ size="xs"
244
+ icon="i-lucide-alert-triangle"
245
+ @click="openApprovalFor(s.approval.id)"
246
+ >
247
+ Decide
248
+ </UButton>
249
+ <UButton
250
+ v-else-if="s.approval && s.approval.status === 'pending'"
251
+ color="warning"
252
+ variant="soft"
253
+ size="xs"
254
+ :icon="
255
+ agentKindMeta(s.agentKind).resultView
256
+ ? 'i-lucide-clipboard-check'
257
+ : 'i-lucide-shield-check'
258
+ "
259
+ @click="openApprovalFor(s.approval.id)"
260
+ >
261
+ {{ agentKindMeta(s.agentKind).resultView ? 'Review' : 'Approve' }}
262
+ </UButton>
263
+ </div>
264
+ <div
265
+ v-if="s.subtasks && s.subtasks.total > 0"
266
+ class="mt-1 ml-6 h-1 overflow-hidden rounded-full bg-slate-700/60"
267
+ >
268
+ <div
269
+ class="h-full rounded-full bg-indigo-400 transition-all duration-500"
270
+ :style="{ width: `${(s.subtasks.completed / s.subtasks.total) * 100}%` }"
271
+ />
272
+ </div>
273
+ <div
274
+ v-if="s.model"
275
+ class="mt-0.5 flex items-center gap-1 pl-6 text-[10px] text-slate-500"
276
+ :title="s.model"
277
+ >
278
+ <UIcon name="i-lucide-cpu" class="h-3 w-3" />
279
+ {{ models.labelForRef(s.model) }}
280
+ </div>
281
+ <!-- Prompt-fragment standards the library selected for this step. -->
282
+ <div
283
+ v-if="s.selectedFragmentIds && s.selectedFragmentIds.length"
284
+ class="mt-0.5 flex flex-wrap items-center gap-1 pl-6 text-[10px] text-slate-500"
285
+ :title="`Best-practice fragments folded into this step: ${s.selectedFragmentIds.join(', ')}`"
286
+ >
287
+ <UIcon name="i-lucide-book-marked" class="h-3 w-3 shrink-0" />
288
+ <span>{{ s.selectedFragmentIds.length }} standard(s) applied</span>
289
+ </div>
290
+ <!-- Conditionally-run companion (the Tester's fixer): possible/running/
291
+ completed/skipped, so it's clear whether a fix pass ran. -->
292
+ <div
293
+ v-if="gateCompanionFor(s, runFailed)"
294
+ class="mt-0.5 flex items-center gap-1.5 pl-6 text-[10px]"
295
+ >
296
+ <UIcon
297
+ :name="agentKindMeta(gateCompanionFor(s, runFailed)!.kind).icon"
298
+ class="h-3 w-3 shrink-0"
299
+ :class="[
300
+ COMPANION_STATE_META[gateCompanionFor(s, runFailed)!.state].text,
301
+ gateCompanionFor(s, runFailed)!.state === 'running' ? 'animate-spin' : '',
302
+ ]"
303
+ />
304
+ <span class="text-slate-400">
305
+ {{ agentKindMeta(gateCompanionFor(s, runFailed)!.kind).label }} (companion)
306
+ </span>
307
+ <span
308
+ class="ml-auto"
309
+ :class="COMPANION_STATE_META[gateCompanionFor(s, runFailed)!.state].text"
310
+ >
311
+ {{ COMPANION_STATE_META[gateCompanionFor(s, runFailed)!.state].label }}
312
+ </span>
313
+ </div>
314
+ </li>
315
+ </ul>
316
+ </div>
317
+
318
+ <!-- failed run: shared failure banner + retry -->
319
+ <AgentFailureCard v-if="failedRun" :run="failedRun" />
320
+
321
+ <!-- Open PR: link straight to it on GitHub -->
322
+ <div v-if="pr" class="space-y-2">
323
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
324
+ Pull request
325
+ </span>
326
+ <UButton
327
+ :to="pr.url"
328
+ target="_blank"
329
+ rel="noopener"
330
+ external
331
+ color="neutral"
332
+ variant="soft"
333
+ size="sm"
334
+ icon="i-lucide-git-pull-request"
335
+ trailing-icon="i-lucide-external-link"
336
+ block
337
+ >
338
+ <span class="flex w-full items-center gap-2">
339
+ {{ prLabel }}
340
+ <UBadge :color="prMerged ? 'success' : 'info'" variant="subtle" size="sm" class="ml-auto">
341
+ {{ prMerged ? 'Merged' : 'Open' }}
342
+ </UBadge>
343
+ </span>
344
+ </UButton>
345
+ <p v-if="pr.branch" class="flex items-center gap-1 truncate text-[10px] text-slate-500">
346
+ <UIcon name="i-lucide-git-branch" class="h-3 w-3 shrink-0" />
347
+ <span class="truncate" :title="pr.branch">{{ pr.branch }}</span>
348
+ </p>
349
+ </div>
350
+
351
+ <!-- PR ready: merge -->
352
+ <UButton
353
+ v-if="block.status === 'pr_ready'"
354
+ color="success"
355
+ variant="solid"
356
+ size="sm"
357
+ icon="i-lucide-git-merge"
358
+ block
359
+ @click="execution.mergePr(block.id)"
360
+ >
361
+ Merge PR
362
+ </UButton>
363
+ </div>
364
+ </template>
@@ -0,0 +1,187 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted } from 'vue'
3
+ import type { Block } from '~/types/domain'
4
+
5
+ const props = defineProps<{ block: Block }>()
6
+
7
+ const board = useBoardStore()
8
+ const mergePresets = useMergePresetsStore()
9
+ const pipelines = usePipelinesStore()
10
+ const accounts = useAccountsStore()
11
+
12
+ // ---- responsible product person --------------------------------------------
13
+ // The account member (a `product` role-holder) accountable for this task; they are
14
+ // notified when requirement review flags findings. Picks from the account roster.
15
+ onMounted(() => {
16
+ const id = accounts.activeAccountId
17
+ if (id && accounts.members.length === 0) void accounts.loadRoster(id).catch(() => {})
18
+ })
19
+ const productMembers = computed(() => accounts.members.filter((m) => m.roles.includes('product')))
20
+ const responsible = computed(() =>
21
+ accounts.members.find((m) => m.userId === props.block.responsibleProductUserId),
22
+ )
23
+ const responsibleLabel = computed(() => {
24
+ const m = responsible.value
25
+ if (!m) return undefined
26
+ return m.name || m.email || m.userId
27
+ })
28
+ const responsibleMenu = computed(() => [
29
+ [
30
+ { label: 'Unassigned', icon: 'i-lucide-user-x', onSelect: () => setResponsible('') },
31
+ ...productMembers.value.map((m) => ({
32
+ label: m.name || m.email || m.userId,
33
+ icon: 'i-lucide-user',
34
+ onSelect: () => setResponsible(m.userId),
35
+ })),
36
+ ],
37
+ ])
38
+ function setResponsible(userId: string) {
39
+ board.updateBlock(props.block.id, { responsibleProductUserId: userId })
40
+ }
41
+
42
+ // ---- merge policy preset ---------------------------------------------------
43
+ // Which merge threshold preset governs this task's auto-merge decision + CI-fixer
44
+ // budget. None selected → the workspace default preset. (The old confidence-based
45
+ // auto-merge threshold is gone; the `merger` step gates on this policy instead.)
46
+ const selectedPreset = computed(() => mergePresets.resolve(props.block.mergePresetId))
47
+ const presetMenu = computed(() => [
48
+ [
49
+ {
50
+ label: mergePresets.defaultPreset
51
+ ? `Default (${mergePresets.defaultPreset.name})`
52
+ : 'Workspace default',
53
+ icon: 'i-lucide-rotate-ccw',
54
+ onSelect: () => setPreset(''),
55
+ },
56
+ ...mergePresets.presets.map((p) => ({
57
+ label: p.name,
58
+ icon: 'i-lucide-git-merge',
59
+ onSelect: () => setPreset(p.id),
60
+ })),
61
+ ],
62
+ ])
63
+ function setPreset(id: string) {
64
+ board.updateBlock(props.block.id, { mergePresetId: id })
65
+ }
66
+
67
+ // ---- pipeline --------------------------------------------------------------
68
+ // The pipeline this task's Run controls default to. None selected → the user picks
69
+ // at run time (the board falls back to the first defined pipeline).
70
+ const selectedPipeline = computed(() =>
71
+ props.block.pipelineId ? pipelines.getPipeline(props.block.pipelineId) : undefined,
72
+ )
73
+ const pipelineMenu = computed(() => [
74
+ [
75
+ {
76
+ label: 'No default',
77
+ icon: 'i-lucide-rotate-ccw',
78
+ onSelect: () => setPipeline(''),
79
+ },
80
+ ...pipelines.pipelines.map((p) => ({
81
+ label: p.name,
82
+ icon: 'i-lucide-workflow',
83
+ onSelect: () => setPipeline(p.id),
84
+ })),
85
+ ],
86
+ ])
87
+ function setPipeline(id: string) {
88
+ board.updateBlock(props.block.id, { pipelineId: id })
89
+ }
90
+ </script>
91
+
92
+ <template>
93
+ <div class="space-y-4">
94
+ <!-- pipeline -->
95
+ <div>
96
+ <div class="mb-1 flex items-center justify-between">
97
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
98
+ Pipeline
99
+ </span>
100
+ <UDropdownMenu :items="pipelineMenu">
101
+ <UButton
102
+ size="xs"
103
+ variant="ghost"
104
+ color="neutral"
105
+ icon="i-lucide-workflow"
106
+ trailing-icon="i-lucide-chevron-down"
107
+ />
108
+ </UDropdownMenu>
109
+ </div>
110
+ <div v-if="selectedPipeline" class="flex items-center gap-1">
111
+ <UBadge
112
+ color="primary"
113
+ variant="subtle"
114
+ size="sm"
115
+ class="cursor-pointer"
116
+ @click="setPipeline('')"
117
+ >
118
+ {{ selectedPipeline.name }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
119
+ </UBadge>
120
+ </div>
121
+ <div v-else class="text-[11px] text-slate-500">
122
+ No default — pick a pipeline when you run this task.
123
+ </div>
124
+ </div>
125
+
126
+ <!-- merge policy preset -->
127
+ <div>
128
+ <div class="mb-1 flex items-center justify-between">
129
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
130
+ Merge policy
131
+ </span>
132
+ <UDropdownMenu :items="presetMenu">
133
+ <UButton
134
+ size="xs"
135
+ variant="ghost"
136
+ color="neutral"
137
+ icon="i-lucide-git-merge"
138
+ trailing-icon="i-lucide-chevron-down"
139
+ />
140
+ </UDropdownMenu>
141
+ </div>
142
+ <div v-if="selectedPreset" class="text-[11px] text-slate-400">
143
+ <span class="text-slate-300">{{ selectedPreset.name }}</span>
144
+ — auto-merge when complexity ≤ {{ Math.round(selectedPreset.maxComplexity * 100) }}%, risk ≤
145
+ {{ Math.round(selectedPreset.maxRisk * 100) }}%, impact ≤
146
+ {{ Math.round(selectedPreset.maxImpact * 100) }}%; up to
147
+ {{ selectedPreset.ciMaxAttempts }} CI-fix attempts.
148
+ <span v-if="!block.mergePresetId" class="text-slate-500">(workspace default)</span>
149
+ </div>
150
+ <div v-else class="text-[11px] text-slate-500">
151
+ No preset configured — the merger raises a review notification for every PR.
152
+ </div>
153
+ </div>
154
+
155
+ <!-- responsible product person -->
156
+ <div>
157
+ <div class="mb-1 flex items-center justify-between">
158
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
159
+ Responsible product
160
+ </span>
161
+ <UDropdownMenu :items="responsibleMenu">
162
+ <UButton
163
+ size="xs"
164
+ variant="ghost"
165
+ color="neutral"
166
+ icon="i-lucide-user"
167
+ trailing-icon="i-lucide-chevron-down"
168
+ />
169
+ </UDropdownMenu>
170
+ </div>
171
+ <div v-if="responsibleLabel" class="flex items-center gap-1">
172
+ <UBadge
173
+ color="primary"
174
+ variant="subtle"
175
+ size="sm"
176
+ class="cursor-pointer"
177
+ @click="setResponsible('')"
178
+ >
179
+ {{ responsibleLabel }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
180
+ </UBadge>
181
+ </div>
182
+ <div v-else class="text-[11px] text-slate-500">
183
+ Unassigned — set a product owner to notify them when requirement review flags this task.
184
+ </div>
185
+ </div>
186
+ </div>
187
+ </template>
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ import type { Block } from '~/types/domain'
3
+
4
+ const props = defineProps<{ block: Block }>()
5
+
6
+ const board = useBoardStore()
7
+ const fragments = useFragmentsStore()
8
+
9
+ // ---- best-practice prompt fragments ----------------------------------------
10
+ // Selected fragments (resolved against the catalog; unknown ids are dropped).
11
+ const selectedFragments = computed(() =>
12
+ (props.block.fragmentIds ?? [])
13
+ .map((id) => fragments.getFragment(id))
14
+ .filter((f): f is NonNullable<typeof f> => !!f),
15
+ )
16
+
17
+ // Picker menu: fragments suitable for this block's type, not already selected,
18
+ // grouped by category so the dropdown reads like the catalog.
19
+ const fragmentMenu = computed(() => {
20
+ const selected = new Set(props.block.fragmentIds ?? [])
21
+ const groups = new Map<string, { label: string; onSelect: () => void }[]>()
22
+ for (const f of fragments.forBlockType(props.block.type)) {
23
+ if (selected.has(f.id)) continue
24
+ const items = groups.get(f.category) ?? []
25
+ items.push({ label: f.title, onSelect: () => addFragment(f.id) })
26
+ groups.set(f.category, items)
27
+ }
28
+ return [...groups.values()]
29
+ })
30
+
31
+ function addFragment(id: string) {
32
+ const list = props.block.fragmentIds ? [...props.block.fragmentIds] : []
33
+ if (!list.includes(id)) list.push(id)
34
+ board.updateBlock(props.block.id, { fragmentIds: list })
35
+ }
36
+
37
+ function removeFragment(id: string) {
38
+ if (!props.block.fragmentIds) return
39
+ board.updateBlock(props.block.id, {
40
+ fragmentIds: props.block.fragmentIds.filter((x) => x !== id),
41
+ })
42
+ }
43
+ </script>
44
+
45
+ <template>
46
+ <div class="space-y-4">
47
+ <!-- module assignment -->
48
+ <div>
49
+ <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
50
+ Module
51
+ </div>
52
+ <UInput
53
+ v-model="block.moduleName"
54
+ size="sm"
55
+ class="w-full"
56
+ placeholder="e.g. Sessions (created on implement if new)"
57
+ icon="i-lucide-package"
58
+ />
59
+ </div>
60
+
61
+ <!-- best practices (prompt fragments) -->
62
+ <div>
63
+ <div class="mb-1 flex items-center justify-between">
64
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
65
+ Best practices
66
+ </span>
67
+ <UDropdownMenu v-if="fragmentMenu.length" :items="fragmentMenu">
68
+ <UButton
69
+ size="xs"
70
+ variant="ghost"
71
+ color="neutral"
72
+ icon="i-lucide-plus"
73
+ trailing-icon="i-lucide-chevron-down"
74
+ />
75
+ </UDropdownMenu>
76
+ </div>
77
+ <div v-if="selectedFragments.length" class="mb-1 flex flex-wrap gap-1">
78
+ <UBadge
79
+ v-for="f in selectedFragments"
80
+ :key="f.id"
81
+ color="primary"
82
+ variant="subtle"
83
+ size="sm"
84
+ class="cursor-pointer"
85
+ :title="f.summary"
86
+ @click="removeFragment(f.id)"
87
+ >
88
+ {{ f.title }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
89
+ </UBadge>
90
+ </div>
91
+ <div v-else class="text-[11px] text-slate-500">
92
+ None — agents follow their default guidance.
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ // The single rendering path for an agent kind's icon (+ optional label) anywhere a
3
+ // pipeline or run lists its steps. Resolves display metadata through
4
+ // `agentKindMeta`, which is total over every kind — palette archetypes, custom
5
+ // agents and the engine's system kinds (ci / merger / blueprints / conflicts) — so
6
+ // a saved pipeline that contains a system kind can never blow up the renderer.
7
+ import { computed } from 'vue'
8
+ import { agentKindMeta } from '~/utils/catalog'
9
+
10
+ const props = withDefaults(
11
+ defineProps<{ kind: string; showLabel?: boolean; iconClass?: string }>(),
12
+ { showLabel: false, iconClass: 'h-4 w-4' },
13
+ )
14
+
15
+ const meta = computed(() => agentKindMeta(props.kind))
16
+
17
+ // Hover tooltip explaining what the agent does. Lead with the label (the icon
18
+ // alone is ambiguous) then the catalog description, so every place that renders
19
+ // an agent step through this single path gets the same explanation on hover.
20
+ const tooltip = computed(() =>
21
+ meta.value.description ? `${meta.value.label} — ${meta.value.description}` : meta.value.label,
22
+ )
23
+ </script>
24
+
25
+ <template>
26
+ <span class="inline-flex items-center gap-2" :title="tooltip">
27
+ <UIcon :name="meta.icon" :class="iconClass" class="shrink-0" :style="{ color: meta.color }" />
28
+ <span v-if="showLabel" class="text-xs text-slate-100">{{ meta.label }}</span>
29
+ </span>
30
+ </template>