@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.name,
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
- return mergePresets.presets.find((p) => p.id === mergePresetId.value)?.name ?? 'Workspace default'
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.name,
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
 
@@ -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
@@ -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.23.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",