@cat-factory/app 0.109.4 → 0.110.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 (36) hide show
  1. package/app/components/board/AddTaskModal.vue +15 -15
  2. package/app/components/panels/inspector/TaskRunSettings.vue +12 -12
  3. package/app/components/pipeline/PipelineBuilder.vue +22 -0
  4. package/app/components/requirements/RequirementsReviewWindow.vue +300 -203
  5. package/app/components/settings/{MergePresetHealthModal.vue → RiskPolicyHealthModal.vue} +19 -19
  6. package/app/components/settings/{MergeThresholdsPanel.vue → RiskPolicyPanel.vue} +46 -46
  7. package/app/components/settings/UsageSettings.vue +142 -0
  8. package/app/components/settings/WorkspaceSettingsPanel.vue +14 -2
  9. package/app/composables/api/board.ts +1 -1
  10. package/app/composables/api/execution.ts +5 -0
  11. package/app/composables/api/presets.ts +18 -18
  12. package/app/composables/api/reviews.ts +7 -6
  13. package/app/composables/{useMergePresetHealth.ts → useRiskPolicyHealth.ts} +10 -10
  14. package/app/pages/index.vue +9 -9
  15. package/app/stores/board.ts +2 -2
  16. package/app/stores/pipelines.ts +41 -1
  17. package/app/stores/requirements.ts +9 -13
  18. package/app/stores/{mergePresets.ts → riskPolicies.ts} +13 -13
  19. package/app/stores/ui.ts +17 -17
  20. package/app/stores/usage.ts +60 -0
  21. package/app/stores/workspace.spec.ts +1 -1
  22. package/app/stores/workspace.ts +4 -4
  23. package/app/types/merge.ts +3 -3
  24. package/app/types/requirements.ts +1 -0
  25. package/app/utils/{mergePreset.ts → riskPolicy.ts} +4 -4
  26. package/i18n/locales/de.json +52 -27
  27. package/i18n/locales/en.json +52 -27
  28. package/i18n/locales/es.json +52 -27
  29. package/i18n/locales/fr.json +53 -28
  30. package/i18n/locales/he.json +53 -28
  31. package/i18n/locales/it.json +53 -28
  32. package/i18n/locales/ja.json +52 -27
  33. package/i18n/locales/pl.json +53 -28
  34. package/i18n/locales/tr.json +52 -27
  35. package/i18n/locales/uk.json +53 -28
  36. package/package.json +2 -2
@@ -1,49 +1,49 @@
1
1
  import {
2
- createMergePresetContract,
2
+ createRiskPolicyContract,
3
3
  createModelPresetContract,
4
- deleteMergePresetContract,
4
+ deleteRiskPolicyContract,
5
5
  deleteModelPresetContract,
6
- listMergePresetsContract,
6
+ listRiskPoliciesContract,
7
7
  listModelPresetsContract,
8
- reseedMergePresetContract,
8
+ reseedRiskPolicyContract,
9
9
  reseedModelPresetContract,
10
- updateMergePresetContract,
10
+ updateRiskPolicyContract,
11
11
  updateModelPresetContract,
12
12
  } from '@cat-factory/contracts'
13
- import type { UpdateMergePresetInput } from '~/types/merge'
13
+ import type { UpdateRiskPolicyInput } from '~/types/merge'
14
14
  import type { CreateModelPresetInput, UpdateModelPresetInput } from '~/types/model-presets'
15
15
  import type { SendParams } from './client'
16
16
  import type { ApiContext } from './context'
17
17
 
18
18
  // The merge-preset create body is typed from the contract's INPUT shape so the
19
19
  // valibot-defaulted fields (release/grace windows, isDefault) stay optional for callers
20
- // (the exported `CreateMergePresetInput` is the post-default OUTPUT shape).
21
- type CreateMergePresetBody = NonNullable<SendParams<typeof createMergePresetContract>['body']>
20
+ // (the exported `CreateRiskPolicyInput` is the post-default OUTPUT shape).
21
+ type CreateRiskPolicyBody = NonNullable<SendParams<typeof createRiskPolicyContract>['body']>
22
22
 
23
23
  /** The per-workspace preset libraries: merge-threshold policy + model->agent mapping. */
24
24
  export function presetsApi({ send, ws }: ApiContext) {
25
25
  return {
26
26
  // ---- merge threshold presets (per-task auto-merge policy library) -----
27
- listMergePresets: (workspaceId: string) =>
28
- send(listMergePresetsContract, { pathPrefix: ws(workspaceId) }),
27
+ listRiskPolicies: (workspaceId: string) =>
28
+ send(listRiskPoliciesContract, { pathPrefix: ws(workspaceId) }),
29
29
 
30
- createMergePreset: (workspaceId: string, body: CreateMergePresetBody) =>
31
- send(createMergePresetContract, { pathPrefix: ws(workspaceId), body }),
30
+ createRiskPolicy: (workspaceId: string, body: CreateRiskPolicyBody) =>
31
+ send(createRiskPolicyContract, { pathPrefix: ws(workspaceId), body }),
32
32
 
33
- updateMergePreset: (workspaceId: string, presetId: string, body: UpdateMergePresetInput) =>
34
- send(updateMergePresetContract, {
33
+ updateRiskPolicy: (workspaceId: string, presetId: string, body: UpdateRiskPolicyInput) =>
34
+ send(updateRiskPolicyContract, {
35
35
  pathPrefix: ws(workspaceId),
36
36
  pathParams: { presetId },
37
37
  body,
38
38
  }),
39
39
 
40
- deleteMergePreset: (workspaceId: string, presetId: string) =>
41
- send(deleteMergePresetContract, { pathPrefix: ws(workspaceId), pathParams: { presetId } }),
40
+ deleteRiskPolicy: (workspaceId: string, presetId: string) =>
41
+ send(deleteRiskPolicyContract, { pathPrefix: ws(workspaceId), pathParams: { presetId } }),
42
42
 
43
43
  // Restore a built-in preset to its current catalog definition (adopt an update, repair a
44
44
  // drifted one, or materialise a new built-in that appeared). Custom presets reject this.
45
- reseedMergePreset: (workspaceId: string, presetId: string) =>
46
- send(reseedMergePresetContract, { pathPrefix: ws(workspaceId), pathParams: { presetId } }),
45
+ reseedRiskPolicy: (workspaceId: string, presetId: string) =>
46
+ send(reseedRiskPolicyContract, { pathPrefix: ws(workspaceId), pathParams: { presetId } }),
47
47
 
48
48
  // ---- model presets (per-task model->agent mapping library) ------------
49
49
  listModelPresets: (workspaceId: string) =>
@@ -27,6 +27,7 @@ import {
27
27
  updateRequirementItemStatusContract,
28
28
  } from '@cat-factory/contracts'
29
29
  import type {
30
+ RequestRecommendationItem,
30
31
  UpdateBrainstormItemStatusInput,
31
32
  UpdateClarityItemStatusInput,
32
33
  } from '@cat-factory/contracts'
@@ -109,19 +110,19 @@ export function reviewsApi({ send, ws }: ApiContext) {
109
110
  body: { choice },
110
111
  }),
111
112
 
112
- // Ask the Requirement Writer to recommend grounded answers for a batch of findings (by
113
- // item id). Returns the review with `pending` placeholder recommendations; they fill in
114
- // (`ready`) asynchronously via the `requirements` stream as the Writer produces each.
113
+ // Ask the Requirement Writer to recommend grounded answers for a batch of findings. Each
114
+ // item carries its finding id plus optional per-finding guidance (the note the human typed
115
+ // before choosing "recommend something"). Returns the review with `pending` placeholder
116
+ // recommendations; they fill in (`ready`) asynchronously via the `requirements` stream.
115
117
  requestRecommendations: (
116
118
  workspaceId: string,
117
119
  blockId: string,
118
- itemIds: string[],
119
- note?: string,
120
+ items: RequestRecommendationItem[],
120
121
  ) =>
121
122
  send(requestRequirementRecommendationsContract, {
122
123
  pathPrefix: ws(workspaceId),
123
124
  pathParams: { blockId },
124
- body: { itemIds, ...(note ? { note } : {}) },
125
+ body: { items },
125
126
  }),
126
127
 
127
128
  // Accept a recommendation (becomes the finding's answer), reject it, or re-request it
@@ -1,12 +1,12 @@
1
1
  import { computed } from 'vue'
2
- import type { MergeThresholdPreset } from '~/types/merge'
3
- import { useMergePresetsStore } from '~/stores/mergePresets'
2
+ import type { RiskPolicy } from '~/types/merge'
3
+ import { useRiskPoliciesStore } from '~/stores/riskPolicies'
4
4
 
5
- export type MergePresetIssueType = 'outdated' | 'new'
5
+ export type RiskPolicyIssueType = 'outdated' | 'new'
6
6
 
7
7
  /** A built-in merge preset that the workspace should reseed (an update, or a new one to add). */
8
- export interface MergePresetIssue {
9
- type: MergePresetIssueType
8
+ export interface RiskPolicyIssue {
9
+ type: RiskPolicyIssueType
10
10
  /** The catalog (built-in) id — what the reseed endpoint is keyed by. */
11
11
  id: string
12
12
  /** The preset name (the stored copy's for `outdated`, the built-in id for a `new` one). */
@@ -18,7 +18,7 @@ export interface MergePresetIssue {
18
18
  }
19
19
 
20
20
  /** A built-in's display name for an issue message (humanise its catalog id as a fallback). */
21
- function builtinName(id: string, stored: MergeThresholdPreset | undefined): string {
21
+ function builtinName(id: string, stored: RiskPolicy | undefined): string {
22
22
  if (stored) return stored.name
23
23
  // `mp_manual_review` -> "Manual review" — only used until the row is reseeded into existence.
24
24
  return id.replace(/^mp_/, '').replace(/_/g, ' ')
@@ -32,11 +32,11 @@ function builtinName(id: string, stored: MergeThresholdPreset | undefined): stri
32
32
  * a stored preset is a built-in iff its id is a catalog key, and a catalog key with no stored
33
33
  * preset is a new built-in.
34
34
  */
35
- export function useMergePresetHealth() {
36
- const store = useMergePresetsStore()
35
+ export function useRiskPolicyHealth() {
36
+ const store = useRiskPoliciesStore()
37
37
 
38
- const issues = computed<MergePresetIssue[]>(() => {
39
- const out: MergePresetIssue[] = []
38
+ const issues = computed<RiskPolicyIssue[]>(() => {
39
+ const out: RiskPolicyIssue[] = []
40
40
  const byId = new Map(store.presets.map((p) => [p.id, p]))
41
41
  for (const [id, catalogVersion] of Object.entries(store.catalogVersions)) {
42
42
  const stored = byId.get(id)
@@ -71,8 +71,8 @@ const PipelineHealthModal = defineAsyncComponent(
71
71
  () => import('~/components/pipeline/PipelineHealthModal.vue'),
72
72
  )
73
73
  // Startup advisory for new / outdated built-in merge presets — same once-per-session pattern.
74
- const MergePresetHealthModal = defineAsyncComponent(
75
- () => import('~/components/settings/MergePresetHealthModal.vue'),
74
+ const RiskPolicyHealthModal = defineAsyncComponent(
75
+ () => import('~/components/settings/RiskPolicyHealthModal.vue'),
76
76
  )
77
77
  // Startup advisory for new / outdated built-in model presets — same once-per-session pattern.
78
78
  const ModelPresetHealthModal = defineAsyncComponent(
@@ -185,12 +185,12 @@ watch(
185
185
  )
186
186
  // Same advisory for built-in merge presets: surface new / outdated ones once per session. Defers
187
187
  // to the pipeline advisory when both fire, so at most one modal auto-opens on a given load.
188
- const { hasIssues: mergePresetIssues } = useMergePresetHealth()
188
+ const { hasIssues: riskPolicyIssues } = useRiskPolicyHealth()
189
189
  watch(
190
- () => [workspace.ready, mergePresetIssues.value, ui.pipelineHealthOpen],
190
+ () => [workspace.ready, riskPolicyIssues.value, ui.pipelineHealthOpen],
191
191
  () => {
192
- if (workspace.ready && mergePresetIssues.value && !ui.pipelineHealthOpen) {
193
- ui.maybeOpenMergePresetHealth()
192
+ if (workspace.ready && riskPolicyIssues.value && !ui.pipelineHealthOpen) {
193
+ ui.maybeOpenRiskPolicyHealth()
194
194
  }
195
195
  },
196
196
  { immediate: true },
@@ -200,13 +200,13 @@ watch(
200
200
  // to the pipeline + merge-preset advisories when they fire, so at most one modal auto-opens.
201
201
  const { hasIssues: modelPresetIssues } = useModelPresetHealth()
202
202
  watch(
203
- () => [workspace.ready, modelPresetIssues.value, ui.pipelineHealthOpen, ui.mergePresetHealthOpen],
203
+ () => [workspace.ready, modelPresetIssues.value, ui.pipelineHealthOpen, ui.riskPolicyHealthOpen],
204
204
  () => {
205
205
  if (
206
206
  workspace.ready &&
207
207
  modelPresetIssues.value &&
208
208
  !ui.pipelineHealthOpen &&
209
- !ui.mergePresetHealthOpen
209
+ !ui.riskPolicyHealthOpen
210
210
  ) {
211
211
  ui.maybeOpenModelPresetHealth()
212
212
  }
@@ -390,7 +390,7 @@ watch(
390
390
  <SlackPanel v-if="ui.slackOpen" />
391
391
  <FragmentLibraryPanel v-if="ui.fragmentLibraryOpen" />
392
392
  <PipelineHealthModal v-if="ui.pipelineHealthOpen" />
393
- <MergePresetHealthModal v-if="ui.mergePresetHealthOpen" />
393
+ <RiskPolicyHealthModal v-if="ui.riskPolicyHealthOpen" />
394
394
  <ModelPresetHealthModal v-if="ui.modelPresetHealthOpen" />
395
395
  <IntegrationsHub v-if="ui.integrationsOpen" />
396
396
  <PersonalSetupModal v-if="ui.personalSetupOpen" />
@@ -169,7 +169,7 @@ export const useBoardStore = defineStore('board', () => {
169
169
  options?: {
170
170
  taskType?: CreateTaskType
171
171
  taskTypeFields?: TaskTypeFields
172
- mergePresetId?: string
172
+ riskPolicyId?: string
173
173
  modelPresetId?: string
174
174
  pipelineId?: string
175
175
  agentConfig?: Record<string, string>
@@ -182,7 +182,7 @@ export const useBoardStore = defineStore('board', () => {
182
182
  description,
183
183
  ...(options?.taskType ? { taskType: options.taskType } : {}),
184
184
  ...(options?.taskTypeFields ? { taskTypeFields: options.taskTypeFields } : {}),
185
- ...(options?.mergePresetId ? { mergePresetId: options.mergePresetId } : {}),
185
+ ...(options?.riskPolicyId ? { riskPolicyId: options.riskPolicyId } : {}),
186
186
  ...(options?.modelPresetId ? { modelPresetId: options.modelPresetId } : {}),
187
187
  ...(options?.pipelineId ? { pipelineId: options.pipelineId } : {}),
188
188
  ...(options?.agentConfig ? { agentConfig: options.agentConfig } : {}),
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
2
2
  import { computed, ref } from 'vue'
3
3
  import type { AgentKind, Pipeline } from '~/types/domain'
4
4
  import type { ConsensusStepConfig, StepGating } from '~/types/consensus'
5
- import type { TesterQualityConfig } from '@cat-factory/contracts'
5
+ import type { StepOptions, TesterQualityConfig } from '@cat-factory/contracts'
6
6
  import { companionForProducer, uid } from '~/utils/catalog'
7
7
  import { useWorkspaceStore } from '~/stores/workspace'
8
8
 
@@ -67,6 +67,13 @@ export const usePipelinesStore = defineStore('pipelines', () => {
67
67
  * entry with `gating` makes it conditional on the task estimate.
68
68
  */
69
69
  const draftTesterQuality = ref<(TesterQualityConfig | null)[]>([])
70
+ /**
71
+ * Per-step options bag, kept index-aligned with `draft`: the extensible home for new per-step
72
+ * parameters (see `StepOptions`). `null`/absent per step ⇒ that step's defaults. Today the only
73
+ * field is `autoRecommend` (requirements-review); by convention we store ONLY deviations from a
74
+ * default, so an entry exists only when a step opts out of something.
75
+ */
76
+ const draftStepOptions = ref<(StepOptions | null)[]>([])
70
77
  /** Organizational labels for the pipeline being assembled/edited. */
71
78
  const draftLabels = ref<string[]>([])
72
79
  const draftName = ref('New pipeline')
@@ -93,6 +100,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
93
100
  draftGating.value.splice(index, 0, null)
94
101
  draftFollowUps.value.splice(index, 0, null)
95
102
  draftTesterQuality.value.splice(index, 0, null)
103
+ draftStepOptions.value.splice(index, 0, null)
96
104
  }
97
105
 
98
106
  function addToDraft(kind: AgentKind) {
@@ -108,6 +116,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
108
116
  draftGating.value.splice(index, 1)
109
117
  draftFollowUps.value.splice(index, 1)
110
118
  draftTesterQuality.value.splice(index, 1)
119
+ draftStepOptions.value.splice(index, 1)
111
120
  }
112
121
 
113
122
  function moveInDraft(from: number, to: number) {
@@ -128,6 +137,8 @@ export const usePipelinesStore = defineStore('pipelines', () => {
128
137
  draftFollowUps.value.splice(to, 0, fu ?? null)
129
138
  const [tq] = draftTesterQuality.value.splice(from, 1)
130
139
  draftTesterQuality.value.splice(to, 0, tq ?? null)
140
+ const [so] = draftStepOptions.value.splice(from, 1)
141
+ draftStepOptions.value.splice(to, 0, so ?? null)
131
142
  }
132
143
 
133
144
  /** Whether the producer step at `index` currently has its companion attached after it. */
@@ -204,6 +215,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
204
215
  draftGating.value = reorder(draftGating.value)
205
216
  draftFollowUps.value = reorder(draftFollowUps.value)
206
217
  draftTesterQuality.value = reorder(draftTesterQuality.value)
218
+ draftStepOptions.value = reorder(draftStepOptions.value)
207
219
  }
208
220
 
209
221
  /** Toggle the consensus mechanism on the draft step at `index` (default config / off). */
@@ -259,6 +271,23 @@ export const usePipelinesStore = defineStore('pipelines', () => {
259
271
  draftEnabled.value[index] = draftEnabled.value[index] === false
260
272
  }
261
273
 
274
+ /** Whether auto-recommendation is on for the draft (requirements-review) step at `index`. */
275
+ function draftAutoRecommendEnabled(index: number): boolean {
276
+ return draftStepOptions.value[index]?.autoRecommend !== false
277
+ }
278
+
279
+ /**
280
+ * Toggle the requirements-review auto-recommendation on the draft step at `index`. It is on by
281
+ * default, so we store ONLY the opt-out (`{ autoRecommend: false }`); toggling back drops the
282
+ * flag. Merges with any other future StepOptions fields rather than clobbering the whole bag.
283
+ */
284
+ function toggleDraftAutoRecommend(index: number) {
285
+ const next: StepOptions = { ...draftStepOptions.value[index] }
286
+ if (draftAutoRecommendEnabled(index)) next.autoRecommend = false
287
+ else delete next.autoRecommend
288
+ draftStepOptions.value[index] = Object.keys(next).length ? next : null
289
+ }
290
+
262
291
  function clearDraft() {
263
292
  draft.value = []
264
293
  draftGates.value = []
@@ -268,6 +297,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
268
297
  draftGating.value = []
269
298
  draftFollowUps.value = []
270
299
  draftTesterQuality.value = []
300
+ draftStepOptions.value = []
271
301
  draftLabels.value = []
272
302
  draftName.value = 'New pipeline'
273
303
  editingId.value = null
@@ -285,6 +315,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
285
315
  draftTesterQuality.value = pipeline.agentKinds.map(
286
316
  (_, i) => pipeline.testerQuality?.[i] ?? null,
287
317
  )
318
+ draftStepOptions.value = pipeline.agentKinds.map((_, i) => pipeline.stepOptions?.[i] ?? null)
288
319
  draftLabels.value = [...(pipeline.labels ?? [])]
289
320
  draftName.value = pipeline.name
290
321
  editingId.value = pipeline.id
@@ -320,6 +351,12 @@ export const usePipelinesStore = defineStore('pipelines', () => {
320
351
  ...(draftTesterQuality.value.some((q) => q?.enabled === false || q?.gating?.enabled)
321
352
  ? { testerQuality: [...draftTesterQuality.value] }
322
353
  : {}),
354
+ // ALWAYS send stepOptions, unlike the legacy per-step arrays above. Those omit-when-default,
355
+ // which means an update can never CLEAR them (an omitted field reads as "keep existing"), so
356
+ // toggling the last opt-out back to its default on a saved pipeline would silently not
357
+ // persist. Sending the aligned array always lets `update` overwrite; the backend normalizes
358
+ // an all-default array away (stores nothing), so this is a no-op on create / all-default.
359
+ stepOptions: draftStepOptions.value.map((o) => o ?? null),
323
360
  // Only send labels when there are any.
324
361
  ...(draftLabels.value.length ? { labels: [...draftLabels.value] } : {}),
325
362
  }
@@ -393,6 +430,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
393
430
  draftGating,
394
431
  draftFollowUps,
395
432
  draftTesterQuality,
433
+ draftStepOptions,
396
434
  draftLabels,
397
435
  draftName,
398
436
  editingId,
@@ -410,6 +448,8 @@ export const usePipelinesStore = defineStore('pipelines', () => {
410
448
  toggleDraftFollowUps,
411
449
  toggleDraftTesterQuality,
412
450
  toggleDraftTesterQualityGating,
451
+ draftAutoRecommendEnabled,
452
+ toggleDraftAutoRecommend,
413
453
  toggleDraftEnabled,
414
454
  toggleDraftConsensus,
415
455
  setDraftConsensus,
@@ -1,6 +1,7 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { ref } from 'vue'
3
3
  import type {
4
+ RequestRecommendationItem,
4
5
  RequirementReview,
5
6
  ResolveRequirementsExceededChoice,
6
7
  ReviewItemStatus,
@@ -208,22 +209,17 @@ export const useRequirementsStore = defineStore('requirements', () => {
208
209
  }
209
210
 
210
211
  /**
211
- * Ask the Requirement Writer to recommend answers for a batch of findings (by item id).
212
- * ASYNCHRONOUS: returns at once with `pending` placeholder recommendations (the Writer runs
213
- * per finding in the durable driver), which fill in (`ready`) via live `requirements` stream
214
- * events; a notification calls the user back when the batch is ready. The board shows the
215
- * `recommending` background stage while any placeholder is pending. Optional `note` steers the
216
- * whole batch.
212
+ * Ask the Requirement Writer to recommend answers for a batch of findings. Each item carries
213
+ * its finding id plus optional per-finding guidance (the note the human typed before choosing
214
+ * "recommend something"). ASYNCHRONOUS: returns at once with `pending` placeholder
215
+ * recommendations (the Writer runs per finding in the durable driver), which fill in (`ready`)
216
+ * via live `requirements` stream events; a notification calls the user back when the batch is
217
+ * ready. The board shows the `recommending` background stage while any placeholder is pending.
217
218
  */
218
- async function requestRecommendations(blockId: string, itemIds: string[], note?: string) {
219
+ async function requestRecommendations(blockId: string, items: RequestRecommendationItem[]) {
219
220
  withFlag(recommending, blockId, true)
220
221
  try {
221
- const updated = await api.requestRecommendations(
222
- workspace.requireId(),
223
- blockId,
224
- itemIds,
225
- note,
226
- )
222
+ const updated = await api.requestRecommendations(workspace.requireId(), blockId, items)
227
223
  if (updated) store(updated)
228
224
  return updated
229
225
  } finally {
@@ -1,6 +1,6 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { computed, ref } from 'vue'
3
- import type { MergeThresholdPreset, UpdateMergePresetInput } from '~/types/domain'
3
+ import type { RiskPolicy, UpdateRiskPolicyInput } from '~/types/domain'
4
4
  import { useWorkspaceStore } from '~/stores/workspace'
5
5
 
6
6
  /**
@@ -9,20 +9,20 @@ import { useWorkspaceStore } from '~/stores/workspace'
9
9
  * resolved preset). Hydrated from the workspace snapshot; managed via a small
10
10
  * settings UI. The backend always keeps at least one default preset.
11
11
  */
12
- export const useMergePresetsStore = defineStore('mergePresets', () => {
12
+ export const useRiskPoliciesStore = defineStore('riskPolicies', () => {
13
13
  const api = useApi()
14
14
 
15
- const presets = ref<MergeThresholdPreset[]>([])
15
+ const presets = ref<RiskPolicy[]>([])
16
16
  /**
17
- * Current built-in catalog versions (`seedMergePresets()`), keyed by preset id, from the
17
+ * Current built-in catalog versions (`seedRiskPolicies()`), keyed by preset id, from the
18
18
  * workspace snapshot. The keys ARE the set of built-in ids: a stored preset whose id is a
19
19
  * key here is a built-in (and is outdated when its `version` is below the catalog value),
20
20
  * and a key with no matching stored preset is a NEW built-in the workspace can add. Drives
21
- * `useMergePresetHealth`.
21
+ * `useRiskPolicyHealth`.
22
22
  */
23
23
  const catalogVersions = ref<Record<string, number>>({})
24
24
 
25
- function hydrate(list: MergeThresholdPreset[], versions?: Record<string, number>) {
25
+ function hydrate(list: RiskPolicy[], versions?: Record<string, number>) {
26
26
  presets.value = [...list].sort((a, b) => a.createdAt - b.createdAt)
27
27
  if (versions) catalogVersions.value = versions
28
28
  }
@@ -31,7 +31,7 @@ export const useMergePresetsStore = defineStore('mergePresets', () => {
31
31
  const defaultPreset = computed(() => presets.value.find((p) => p.isDefault) ?? null)
32
32
 
33
33
  /** Resolve a task's effective preset by id, falling back to the default. */
34
- function resolve(presetId: string | undefined): MergeThresholdPreset | null {
34
+ function resolve(presetId: string | undefined): RiskPolicy | null {
35
35
  if (presetId) {
36
36
  const picked = presets.value.find((p) => p.id === presetId)
37
37
  if (picked) return picked
@@ -39,23 +39,23 @@ export const useMergePresetsStore = defineStore('mergePresets', () => {
39
39
  return defaultPreset.value
40
40
  }
41
41
 
42
- async function create(input: Parameters<typeof api.createMergePreset>[1]) {
42
+ async function create(input: Parameters<typeof api.createRiskPolicy>[1]) {
43
43
  const ws = useWorkspaceStore()
44
- const created = await api.createMergePreset(ws.requireId(), input)
44
+ const created = await api.createRiskPolicy(ws.requireId(), input)
45
45
  await ws.refresh()
46
46
  return created
47
47
  }
48
48
 
49
- async function update(presetId: string, patch: UpdateMergePresetInput) {
49
+ async function update(presetId: string, patch: UpdateRiskPolicyInput) {
50
50
  const ws = useWorkspaceStore()
51
- const updated = await api.updateMergePreset(ws.requireId(), presetId, patch)
51
+ const updated = await api.updateRiskPolicy(ws.requireId(), presetId, patch)
52
52
  await ws.refresh()
53
53
  return updated
54
54
  }
55
55
 
56
56
  async function remove(presetId: string) {
57
57
  const ws = useWorkspaceStore()
58
- await api.deleteMergePreset(ws.requireId(), presetId)
58
+ await api.deleteRiskPolicy(ws.requireId(), presetId)
59
59
  await ws.refresh()
60
60
  }
61
61
 
@@ -66,7 +66,7 @@ export const useMergePresetsStore = defineStore('mergePresets', () => {
66
66
  */
67
67
  async function reseed(presetId: string) {
68
68
  const ws = useWorkspaceStore()
69
- const updated = await api.reseedMergePreset(ws.requireId(), presetId)
69
+ const updated = await api.reseedRiskPolicy(ws.requireId(), presetId)
70
70
  await ws.refresh()
71
71
  return updated
72
72
  }
package/app/stores/ui.ts CHANGED
@@ -40,10 +40,10 @@ export const useUiStore = defineStore('ui', () => {
40
40
  const pipelineHealthOpen = ref(false)
41
41
  const pipelineHealthSeen = ref(false)
42
42
  // Merge-preset health startup advisory: lists built-ins with a newer catalog version (reseed)
43
- // and new built-in presets the workspace can add. `mergePresetHealthSeen` gates auto-open to
43
+ // and new built-in presets the workspace can add. `riskPolicyHealthSeen` gates auto-open to
44
44
  // once per session so it does not re-pop on every snapshot re-hydration (mirrors pipelines).
45
- const mergePresetHealthOpen = ref(false)
46
- const mergePresetHealthSeen = ref(false)
45
+ const riskPolicyHealthOpen = ref(false)
46
+ const riskPolicyHealthSeen = ref(false)
47
47
  // Model-preset health startup advisory: lists built-ins with a newer catalog version (reseed)
48
48
  // and new built-in presets the workspace can add. `modelPresetHealthSeen` gates auto-open to
49
49
  // once per session so it does not re-pop on every snapshot re-hydration (mirrors pipelines).
@@ -313,19 +313,19 @@ export const useUiStore = defineStore('ui', () => {
313
313
  }
314
314
 
315
315
  /** Auto-open the merge-preset health advisory once per session (no-op after it's been shown). */
316
- function maybeOpenMergePresetHealth() {
317
- if (mergePresetHealthSeen.value) return
318
- mergePresetHealthSeen.value = true
319
- mergePresetHealthOpen.value = true
316
+ function maybeOpenRiskPolicyHealth() {
317
+ if (riskPolicyHealthSeen.value) return
318
+ riskPolicyHealthSeen.value = true
319
+ riskPolicyHealthOpen.value = true
320
320
  }
321
321
 
322
- function openMergePresetHealth() {
323
- mergePresetHealthSeen.value = true
324
- mergePresetHealthOpen.value = true
322
+ function openRiskPolicyHealth() {
323
+ riskPolicyHealthSeen.value = true
324
+ riskPolicyHealthOpen.value = true
325
325
  }
326
326
 
327
- function closeMergePresetHealth() {
328
- mergePresetHealthOpen.value = false
327
+ function closeRiskPolicyHealth() {
328
+ riskPolicyHealthOpen.value = false
329
329
  }
330
330
 
331
331
  /** Auto-open the model-preset health advisory once per session (no-op after it's been shown). */
@@ -839,8 +839,8 @@ export const useUiStore = defineStore('ui', () => {
839
839
  builderOpen,
840
840
  pipelineHealthOpen,
841
841
  pipelineHealthSeen,
842
- mergePresetHealthOpen,
843
- mergePresetHealthSeen,
842
+ riskPolicyHealthOpen,
843
+ riskPolicyHealthSeen,
844
844
  modelPresetHealthOpen,
845
845
  modelPresetHealthSeen,
846
846
  decisionContext,
@@ -907,9 +907,9 @@ export const useUiStore = defineStore('ui', () => {
907
907
  maybeOpenPipelineHealth,
908
908
  openPipelineHealth,
909
909
  closePipelineHealth,
910
- maybeOpenMergePresetHealth,
911
- openMergePresetHealth,
912
- closeMergePresetHealth,
910
+ maybeOpenRiskPolicyHealth,
911
+ openRiskPolicyHealth,
912
+ closeRiskPolicyHealth,
913
913
  maybeOpenModelPresetHealth,
914
914
  openModelPresetHealth,
915
915
  closeModelPresetHealth,
@@ -0,0 +1,60 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type { UsageReport } from '@cat-factory/contracts'
4
+
5
+ /**
6
+ * The workspace's token-usage report for the current billing period (the "Usage" settings
7
+ * tab). Loaded on demand from `GET /workspaces/:ws/usage`; covers BOTH metered API/proxy
8
+ * calls and flat-rate subscription harness usage (Claude Code / Codex / GLM / pooled Kimi &
9
+ * DeepSeek). Reporting only — the spend budget still counts only the metered rows.
10
+ */
11
+ export const useUsageStore = defineStore('usage', () => {
12
+ const api = useApi()
13
+ const report = ref<UsageReport | null>(null)
14
+ const loading = ref(false)
15
+ const error = ref<string | null>(null)
16
+
17
+ async function load(ws: string) {
18
+ loading.value = true
19
+ error.value = null
20
+ try {
21
+ report.value = await api.getUsage(ws)
22
+ } catch (e) {
23
+ error.value = e instanceof Error ? e.message : String(e)
24
+ } finally {
25
+ loading.value = false
26
+ }
27
+ }
28
+
29
+ const rows = computed(() => report.value?.rows ?? [])
30
+ const metered = computed(() => rows.value.filter((r) => r.billing === 'metered'))
31
+ const subscription = computed(() => rows.value.filter((r) => r.billing === 'subscription'))
32
+
33
+ /** Summed input/output tokens + cost for a set of rows. */
34
+ function totalOf(list: UsageReport['rows']) {
35
+ return list.reduce(
36
+ (acc, r) => ({
37
+ inputTokens: acc.inputTokens + r.inputTokens,
38
+ outputTokens: acc.outputTokens + r.outputTokens,
39
+ costEstimate: acc.costEstimate + r.costEstimate,
40
+ calls: acc.calls + r.calls,
41
+ }),
42
+ { inputTokens: 0, outputTokens: 0, costEstimate: 0, calls: 0 },
43
+ )
44
+ }
45
+
46
+ const meteredTotal = computed(() => totalOf(metered.value))
47
+ const subscriptionTotal = computed(() => totalOf(subscription.value))
48
+
49
+ return {
50
+ report,
51
+ loading,
52
+ error,
53
+ load,
54
+ rows,
55
+ metered,
56
+ subscription,
57
+ meteredTotal,
58
+ subscriptionTotal,
59
+ }
60
+ })
@@ -19,7 +19,7 @@ const INERT_STORES = [
19
19
  'useFragmentsStore',
20
20
  'useGitHubStore',
21
21
  'useInitiativesStore',
22
- 'useMergePresetsStore',
22
+ 'useRiskPoliciesStore',
23
23
  'useModelPresetsStore',
24
24
  'useNotificationsStore',
25
25
  'usePipelinesStore',
@@ -13,7 +13,7 @@ import { usePipelinesStore } from '~/stores/pipelines'
13
13
  import { useExecutionStore } from '~/stores/execution'
14
14
  import { useAgentRunsStore } from '~/stores/agentRuns'
15
15
  import { useNotificationsStore } from '~/stores/notifications'
16
- import { useMergePresetsStore } from '~/stores/mergePresets'
16
+ import { useRiskPoliciesStore } from '~/stores/riskPolicies'
17
17
  import { useSharedStacksStore } from '~/stores/sharedStacks'
18
18
  import { useWorkspaceSettingsStore } from '~/stores/workspaceSettings'
19
19
  import { useAgentConfigStore } from '~/stores/agentConfig'
@@ -120,9 +120,9 @@ export const useWorkspaceStore = defineStore(
120
120
  useAgentRunsStore().hydrate(snapshot.bootstrapJobs ?? [], snapshot.workspace.id)
121
121
  useAgentRunsStore().hydrateEnvConfigRepair(snapshot.envConfigRepairJobs ?? [])
122
122
  useNotificationsStore().hydrate(snapshot.notifications ?? [])
123
- useMergePresetsStore().hydrate(
124
- snapshot.mergePresets ?? [],
125
- snapshot.mergePresetCatalogVersions,
123
+ useRiskPoliciesStore().hydrate(
124
+ snapshot.riskPolicies ?? [],
125
+ snapshot.riskPolicyCatalogVersions,
126
126
  )
127
127
  useSharedStacksStore().hydrate(snapshot.sharedStacks ?? [])
128
128
  useWorkspaceSettingsStore().hydrate(snapshot.settings)
@@ -7,7 +7,7 @@
7
7
  export type {
8
8
  MergeAssessment,
9
9
  RequirementConcernLevel,
10
- MergeThresholdPreset,
11
- CreateMergePresetInput,
12
- UpdateMergePresetInput,
10
+ RiskPolicy,
11
+ CreateRiskPolicyInput,
12
+ UpdateRiskPolicyInput,
13
13
  } from '@cat-factory/contracts'
@@ -18,4 +18,5 @@ export type {
18
18
  RecommendationStatus,
19
19
  RequirementRecommendation,
20
20
  RequirementReview,
21
+ RequestRecommendationItem,
21
22
  } from '@cat-factory/contracts'