@cat-factory/app 0.23.0 → 0.24.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.
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import type { CreateTaskType, TaskSourceKind, TaskTypeFields } from '~/types/domain'
|
|
15
15
|
import ContextDocumentPicker from '~/components/documents/ContextDocumentPicker.vue'
|
|
16
16
|
import ContextIssuePicker from '~/components/tasks/ContextIssuePicker.vue'
|
|
17
|
+
import { mergePresetOptionLabel, mergePresetThresholds } from '~/utils/mergePreset'
|
|
17
18
|
|
|
18
19
|
const ui = useUiStore()
|
|
19
20
|
const board = useBoardStore()
|
|
@@ -41,6 +42,9 @@ const container = computed(() =>
|
|
|
41
42
|
const title = ref('')
|
|
42
43
|
const description = ref('')
|
|
43
44
|
const saving = ref(false)
|
|
45
|
+
// Whether the user marks this as a purely technical task up front (a refactor /
|
|
46
|
+
// non-functional change). Left off ⇒ the engine infers it from the spec phase.
|
|
47
|
+
const technical = ref(false)
|
|
44
48
|
|
|
45
49
|
// The kind of task being created. `recurring` is special: it is created through the
|
|
46
50
|
// recurring-pipeline schedule flow (a schedule on the service frame), so picking it
|
|
@@ -104,13 +108,13 @@ const presetMenu = computed(() => [
|
|
|
104
108
|
[
|
|
105
109
|
{
|
|
106
110
|
label: mergePresets.defaultPreset
|
|
107
|
-
? `Default (${mergePresets.defaultPreset.name})`
|
|
111
|
+
? `Default (${mergePresets.defaultPreset.name}) — ${mergePresetThresholds(mergePresets.defaultPreset)}`
|
|
108
112
|
: 'Workspace default',
|
|
109
113
|
icon: 'i-lucide-rotate-ccw',
|
|
110
114
|
onSelect: () => (mergePresetId.value = ''),
|
|
111
115
|
},
|
|
112
116
|
...mergePresets.presets.map((p) => ({
|
|
113
|
-
label: p
|
|
117
|
+
label: mergePresetOptionLabel(p),
|
|
114
118
|
icon: 'i-lucide-git-merge',
|
|
115
119
|
onSelect: () => (mergePresetId.value = p.id),
|
|
116
120
|
})),
|
|
@@ -119,10 +123,11 @@ const presetMenu = computed(() => [
|
|
|
119
123
|
const selectedPresetLabel = computed(() => {
|
|
120
124
|
if (!mergePresetId.value) {
|
|
121
125
|
return mergePresets.defaultPreset
|
|
122
|
-
? `Default (${mergePresets.defaultPreset.name})`
|
|
126
|
+
? `Default (${mergePresets.defaultPreset.name}) — ${mergePresetThresholds(mergePresets.defaultPreset)}`
|
|
123
127
|
: 'Workspace default'
|
|
124
128
|
}
|
|
125
|
-
|
|
129
|
+
const picked = mergePresets.presets.find((p) => p.id === mergePresetId.value)
|
|
130
|
+
return picked ? mergePresetOptionLabel(picked) : 'Workspace default'
|
|
126
131
|
})
|
|
127
132
|
|
|
128
133
|
// Model preset: which model each agent runs on. Empty = workspace default preset.
|
|
@@ -274,6 +279,7 @@ watch(open, (isOpen) => {
|
|
|
274
279
|
description.value = ''
|
|
275
280
|
saving.value = false
|
|
276
281
|
taskType.value = 'feature'
|
|
282
|
+
technical.value = false
|
|
277
283
|
severity.value = ''
|
|
278
284
|
stepsToReproduce.value = ''
|
|
279
285
|
timeboxHours.value = undefined
|
|
@@ -337,6 +343,7 @@ async function add() {
|
|
|
337
343
|
...(Object.keys(agentConfigValues.value).length
|
|
338
344
|
? { agentConfig: agentConfigValues.value }
|
|
339
345
|
: {}),
|
|
346
|
+
...(technical.value ? { technical: true } : {}),
|
|
340
347
|
})
|
|
341
348
|
if (block) {
|
|
342
349
|
const failed = await linkPending(block.id, pendingContext.value)
|
|
@@ -446,6 +453,19 @@ async function add() {
|
|
|
446
453
|
/>
|
|
447
454
|
</UFormField>
|
|
448
455
|
|
|
456
|
+
<UCheckbox v-model="technical" name="technical">
|
|
457
|
+
<template #label>
|
|
458
|
+
<span class="text-sm text-slate-200">Technical task</span>
|
|
459
|
+
</template>
|
|
460
|
+
<template #description>
|
|
461
|
+
<span class="text-[11px] text-slate-500">
|
|
462
|
+
A refactor / non-functional / internal change. The implementer treats the task
|
|
463
|
+
definition as primary and the spec as a regression reference; leave off to let the
|
|
464
|
+
spec phase decide.
|
|
465
|
+
</span>
|
|
466
|
+
</template>
|
|
467
|
+
</UCheckbox>
|
|
468
|
+
|
|
449
469
|
<!-- Per-type fields. -->
|
|
450
470
|
<div v-if="taskType === 'bug'" class="grid grid-cols-2 gap-3">
|
|
451
471
|
<UFormField label="Severity">
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, onMounted } from 'vue'
|
|
3
3
|
import type { Block } from '~/types/domain'
|
|
4
4
|
import type { WritebackOverride } from '~/types/tracker'
|
|
5
|
+
import { mergePresetOptionLabel, mergePresetThresholds } from '~/utils/mergePreset'
|
|
5
6
|
|
|
6
7
|
const props = defineProps<{ block: Block }>()
|
|
7
8
|
|
|
@@ -61,13 +62,13 @@ const presetMenu = computed(() => [
|
|
|
61
62
|
[
|
|
62
63
|
{
|
|
63
64
|
label: mergePresets.defaultPreset
|
|
64
|
-
? `Default (${mergePresets.defaultPreset.name})`
|
|
65
|
+
? `Default (${mergePresets.defaultPreset.name}) — ${mergePresetThresholds(mergePresets.defaultPreset)}`
|
|
65
66
|
: 'Workspace default',
|
|
66
67
|
icon: 'i-lucide-rotate-ccw',
|
|
67
68
|
onSelect: () => setPreset(''),
|
|
68
69
|
},
|
|
69
70
|
...mergePresets.presets.map((p) => ({
|
|
70
|
-
label: p
|
|
71
|
+
label: mergePresetOptionLabel(p),
|
|
71
72
|
icon: 'i-lucide-git-merge',
|
|
72
73
|
onSelect: () => setPreset(p.id),
|
|
73
74
|
})),
|
|
@@ -172,6 +173,31 @@ const commentOnPrOpenLabel = computed(() =>
|
|
|
172
173
|
const resolveOnMergeLabel = computed(() =>
|
|
173
174
|
writebackLabel(props.block.trackerResolveOnMerge, tracker.settings.writebackResolveOnMerge),
|
|
174
175
|
)
|
|
176
|
+
|
|
177
|
+
// ---- technical label (tri-state) -------------------------------------------
|
|
178
|
+
// Whether this is a purely technical task (the implementer then treats the task
|
|
179
|
+
// definition as primary and the spec as a regression reference). Tri-state: Unset lets
|
|
180
|
+
// the engine infer it from the spec phase; Technical / Business are authoritative human
|
|
181
|
+
// choices the engine never overrides. `null` clears back to Unset.
|
|
182
|
+
function setTechnical(value: boolean | null) {
|
|
183
|
+
board.updateBlock(props.block.id, { technical: value })
|
|
184
|
+
}
|
|
185
|
+
const technicalMenu = [
|
|
186
|
+
[
|
|
187
|
+
{
|
|
188
|
+
label: 'Unset (auto-detect)',
|
|
189
|
+
icon: 'i-lucide-rotate-ccw',
|
|
190
|
+
onSelect: () => setTechnical(null),
|
|
191
|
+
},
|
|
192
|
+
{ label: 'Technical', icon: 'i-lucide-wrench', onSelect: () => setTechnical(true) },
|
|
193
|
+
{ label: 'Business', icon: 'i-lucide-briefcase', onSelect: () => setTechnical(false) },
|
|
194
|
+
],
|
|
195
|
+
]
|
|
196
|
+
const technicalLabel = computed(() => {
|
|
197
|
+
if (props.block.technical === true) return 'Technical'
|
|
198
|
+
if (props.block.technical === false) return 'Business'
|
|
199
|
+
return 'Unset (auto-detect)'
|
|
200
|
+
})
|
|
175
201
|
</script>
|
|
176
202
|
|
|
177
203
|
<template>
|
|
@@ -301,6 +327,38 @@ const resolveOnMergeLabel = computed(() =>
|
|
|
301
327
|
</p>
|
|
302
328
|
</div>
|
|
303
329
|
|
|
330
|
+
<!-- technical label (tri-state) -->
|
|
331
|
+
<div>
|
|
332
|
+
<div class="mb-1 flex items-center justify-between">
|
|
333
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
334
|
+
Task kind
|
|
335
|
+
</span>
|
|
336
|
+
<UDropdownMenu :items="technicalMenu">
|
|
337
|
+
<UButton
|
|
338
|
+
size="xs"
|
|
339
|
+
variant="ghost"
|
|
340
|
+
color="neutral"
|
|
341
|
+
icon="i-lucide-wrench"
|
|
342
|
+
trailing-icon="i-lucide-chevron-down"
|
|
343
|
+
>
|
|
344
|
+
{{ technicalLabel }}
|
|
345
|
+
</UButton>
|
|
346
|
+
</UDropdownMenu>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="text-[11px] text-slate-500">
|
|
349
|
+
<template v-if="block.technical === true">
|
|
350
|
+
Technical — the implementer treats the task definition as primary and the spec as a
|
|
351
|
+
regression reference; the spec-writer may produce no business specs.
|
|
352
|
+
</template>
|
|
353
|
+
<template v-else-if="block.technical === false">
|
|
354
|
+
Business — the specification leads, as usual.
|
|
355
|
+
</template>
|
|
356
|
+
<template v-else>
|
|
357
|
+
Auto-detect — inferred from the spec phase. Set it explicitly to override.
|
|
358
|
+
</template>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
304
362
|
<!-- issue-tracker writeback overrides -->
|
|
305
363
|
<div>
|
|
306
364
|
<div class="mb-1 flex items-center justify-between">
|
|
@@ -42,6 +42,7 @@ export function boardApi({ http, ws }: ApiContext) {
|
|
|
42
42
|
modelPresetId?: string
|
|
43
43
|
pipelineId?: string
|
|
44
44
|
agentConfig?: Record<string, string>
|
|
45
|
+
technical?: boolean
|
|
45
46
|
},
|
|
46
47
|
) => http<Block>(`${ws(workspaceId)}/blocks/${blockId}/tasks`, { method: 'POST', body }),
|
|
47
48
|
|
package/app/stores/board.ts
CHANGED
|
@@ -80,6 +80,7 @@ export const useBoardStore = defineStore('board', () => {
|
|
|
80
80
|
modelPresetId?: string
|
|
81
81
|
pipelineId?: string
|
|
82
82
|
agentConfig?: Record<string, string>
|
|
83
|
+
technical?: boolean
|
|
83
84
|
},
|
|
84
85
|
): Promise<Block | undefined> {
|
|
85
86
|
if (!getBlock(containerId)) return
|
|
@@ -92,6 +93,7 @@ export const useBoardStore = defineStore('board', () => {
|
|
|
92
93
|
...(options?.modelPresetId ? { modelPresetId: options.modelPresetId } : {}),
|
|
93
94
|
...(options?.pipelineId ? { pipelineId: options.pipelineId } : {}),
|
|
94
95
|
...(options?.agentConfig ? { agentConfig: options.agentConfig } : {}),
|
|
96
|
+
...(options?.technical ? { technical: true } : {}),
|
|
95
97
|
})
|
|
96
98
|
upsert(block)
|
|
97
99
|
return block
|
package/app/types/domain.ts
CHANGED
|
@@ -132,6 +132,15 @@ export interface Block {
|
|
|
132
132
|
taskType?: TaskType
|
|
133
133
|
/** task-only: small per-type form fields (bug severity, spike timebox, …). */
|
|
134
134
|
taskTypeFields?: TaskTypeFields | null
|
|
135
|
+
/**
|
|
136
|
+
* task-only: TECHNICAL label. `true` ⇒ a refactor / non-functional / internal change
|
|
137
|
+
* (the implementer treats the task definition as primary, specs as a regression
|
|
138
|
+
* reference; the spec-writer may produce no business specs). `false` ⇒ a business task.
|
|
139
|
+
* `null`/absent ⇒ not yet determined — the engine may infer it from the spec phase. A
|
|
140
|
+
* human-set value is authoritative and never overridden; the inspector toggle is
|
|
141
|
+
* tri-state (unset / technical / business) and sends `null` for "unset".
|
|
142
|
+
*/
|
|
143
|
+
technical?: boolean | null
|
|
135
144
|
/** ids of best-practice prompt fragments folded into this block's agent prompts. */
|
|
136
145
|
fragmentIds?: string[]
|
|
137
146
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MergeThresholdPreset } from '~/types/merge'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A compact one-line summary of a merge preset's auto-merge ceilings + CI-fix budget,
|
|
5
|
+
* suitable for a dropdown option label so the user sees each preset's actual thresholds
|
|
6
|
+
* (not just its name) while choosing one. Percentages are the stored 0..1 ratios
|
|
7
|
+
* rendered as whole percents.
|
|
8
|
+
*/
|
|
9
|
+
export function mergePresetThresholds(p: MergeThresholdPreset): string {
|
|
10
|
+
const pct = (n: number) => `${Math.round(n * 100)}%`
|
|
11
|
+
return `cx ≤${pct(p.maxComplexity)} · risk ≤${pct(p.maxRisk)} · impact ≤${pct(
|
|
12
|
+
p.maxImpact,
|
|
13
|
+
)} · ${p.ciMaxAttempts} CI fixes`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The preset name followed by its thresholds, for a single-line dropdown option. */
|
|
17
|
+
export function mergePresetOptionLabel(p: MergeThresholdPreset): string {
|
|
18
|
+
return `${p.name} — ${mergePresetThresholds(p)}`
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|