@cat-factory/app 0.30.6 → 0.32.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 (32) hide show
  1. package/app/components/brainstorm/BrainstormWindow.vue +617 -0
  2. package/app/components/followUp/FollowUpWindow.vue +257 -0
  3. package/app/components/kaizen/KaizenPanel.vue +208 -0
  4. package/app/components/kaizen/KaizenStepStatus.vue +94 -0
  5. package/app/components/layout/NotificationsInbox.vue +13 -0
  6. package/app/components/layout/SideBar.vue +12 -0
  7. package/app/components/panels/AgentStepDetail.vue +6 -0
  8. package/app/components/panels/StepResultViewHost.vue +7 -0
  9. package/app/components/pipeline/PipelineBuilder.vue +21 -0
  10. package/app/components/pipeline/PipelineProgress.vue +59 -1
  11. package/app/components/settings/WorkspaceSettingsPanel.vue +20 -0
  12. package/app/components/slack/SlackPanel.vue +1 -0
  13. package/app/composables/api/followUps.ts +52 -0
  14. package/app/composables/api/kaizen.ts +16 -0
  15. package/app/composables/api/reviews.ts +68 -0
  16. package/app/composables/useApi.ts +4 -0
  17. package/app/composables/useResultView.ts +3 -1
  18. package/app/composables/useWorkspaceStream.ts +11 -0
  19. package/app/pages/index.vue +2 -0
  20. package/app/stores/brainstorm.ts +210 -0
  21. package/app/stores/followUps.ts +73 -0
  22. package/app/stores/kaizen.ts +101 -0
  23. package/app/stores/pipelines.ts +25 -0
  24. package/app/stores/ui.ts +64 -1
  25. package/app/stores/workspaceSettings.ts +1 -0
  26. package/app/types/brainstorm.ts +55 -0
  27. package/app/types/domain.ts +64 -0
  28. package/app/types/execution.ts +41 -0
  29. package/app/types/notifications.ts +1 -0
  30. package/app/utils/catalog.spec.ts +2 -0
  31. package/app/utils/catalog.ts +68 -3
  32. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import { useApi } from '~/composables/useApi'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+ import { useExecutionStore } from '~/stores/execution'
6
+
7
+ /**
8
+ * The Follow-up companion action surface. The live item state lives on the run's Coder step
9
+ * (`step.followUps`) and is kept fresh by the execution stream, so the window reads items
10
+ * straight off the execution store — this store only wraps the decide actions (file / send
11
+ * back / answer / dismiss) and tracks which item is mid-action so the window can disable its
12
+ * buttons. The returned state is also pushed back into the execution store so the UI updates
13
+ * immediately even before the stream echoes the change.
14
+ */
15
+ export const useFollowUpsStore = defineStore('followUps', () => {
16
+ const api = useApi()
17
+ const workspace = useWorkspaceStore()
18
+ const execution = useExecutionStore()
19
+
20
+ /** Item ids with an action in flight (drives per-row spinners / disabled buttons). */
21
+ const acting = ref<Set<string>>(new Set())
22
+ /** The last error message from an action, surfaced inline; cleared on the next action. */
23
+ const error = ref<string | null>(null)
24
+
25
+ function isActing(itemId: string): boolean {
26
+ return acting.value.has(itemId)
27
+ }
28
+
29
+ function mark(itemId: string, on: boolean) {
30
+ const next = new Set(acting.value)
31
+ if (on) next.add(itemId)
32
+ else next.delete(itemId)
33
+ acting.value = next
34
+ }
35
+
36
+ /** Run one decide action, reflecting the returned state onto the run's Coder step. */
37
+ async function act(
38
+ executionId: string,
39
+ itemId: string,
40
+ call: (ws: string) => Promise<unknown>,
41
+ ): Promise<void> {
42
+ error.value = null
43
+ mark(itemId, true)
44
+ try {
45
+ const state = await call(workspace.requireId())
46
+ // Reflect the authoritative state immediately (the stream will also echo it).
47
+ const instance = execution.getInstance(executionId)
48
+ const step = instance?.steps.find((s) => s.followUps?.enabled)
49
+ if (step && state && typeof state === 'object') {
50
+ step.followUps = state as typeof step.followUps
51
+ }
52
+ } catch (e) {
53
+ error.value = e instanceof Error ? e.message : 'Action failed'
54
+ throw e
55
+ } finally {
56
+ mark(itemId, false)
57
+ }
58
+ }
59
+
60
+ const fileItem = (executionId: string, itemId: string) =>
61
+ act(executionId, itemId, (ws) => api.fileFollowUp(ws, executionId, itemId))
62
+
63
+ const queueItem = (executionId: string, itemId: string) =>
64
+ act(executionId, itemId, (ws) => api.queueFollowUp(ws, executionId, itemId))
65
+
66
+ const answerItem = (executionId: string, itemId: string, answer: string) =>
67
+ act(executionId, itemId, (ws) => api.answerFollowUp(ws, executionId, itemId, answer))
68
+
69
+ const dismissItem = (executionId: string, itemId: string) =>
70
+ act(executionId, itemId, (ws) => api.dismissFollowUp(ws, executionId, itemId))
71
+
72
+ return { acting, error, isActing, fileItem, queueItem, answerItem, dismissItem }
73
+ })
@@ -0,0 +1,101 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type { KaizenGrading, KaizenVerifiedCombo } from '~/types/domain'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+
6
+ /**
7
+ * Kaizen state: per-run gradings (for the run-window status surface) and the
8
+ * workspace-wide history + verified-combo library (for the Kaizen screen). Gradings
9
+ * arrive both via lazy loads and live over the workspace stream (`upsert`). Never
10
+ * surfaced on the board — only inside run details + the dedicated screen.
11
+ */
12
+ export const useKaizenStore = defineStore('kaizen', () => {
13
+ const api = useApi()
14
+
15
+ /** Gradings keyed by run (execution) id, for the run window. */
16
+ const byExecution = ref<Record<string, KaizenGrading[]>>({})
17
+ /** Recent grading history for the Kaizen screen. */
18
+ const history = ref<KaizenGrading[]>([])
19
+ /** The verified-combo library for the Kaizen screen. */
20
+ const verified = ref<KaizenVerifiedCombo[]>([])
21
+ const loadingOverview = ref(false)
22
+ const loadingExecution = ref<Set<string>>(new Set())
23
+ /** 503 ⇒ the Kaizen feature isn't configured on this deployment. */
24
+ const available = ref<boolean | null>(null)
25
+
26
+ function gradingsFor(executionId: string): KaizenGrading[] {
27
+ return byExecution.value[executionId] ?? []
28
+ }
29
+
30
+ /** The grading for a specific step of a run, if any. */
31
+ function gradingForStep(executionId: string, stepIndex: number): KaizenGrading | null {
32
+ return gradingsFor(executionId).find((g) => g.stepIndex === stepIndex) ?? null
33
+ }
34
+
35
+ async function loadOverview() {
36
+ const ws = useWorkspaceStore()
37
+ loadingOverview.value = true
38
+ try {
39
+ const overview = await api.getKaizenOverview(ws.requireId())
40
+ history.value = overview.gradings
41
+ verified.value = overview.verified
42
+ available.value = true
43
+ } catch (e) {
44
+ if ((e as { statusCode?: number; status?: number })?.statusCode === 503)
45
+ available.value = false
46
+ else throw e
47
+ } finally {
48
+ loadingOverview.value = false
49
+ }
50
+ }
51
+
52
+ async function loadForExecution(executionId: string) {
53
+ const ws = useWorkspaceStore()
54
+ loadingExecution.value = new Set(loadingExecution.value).add(executionId)
55
+ try {
56
+ const { gradings } = await api.getKaizenForExecution(ws.requireId(), executionId)
57
+ byExecution.value = { ...byExecution.value, [executionId]: gradings }
58
+ available.value = true
59
+ } catch (e) {
60
+ if ((e as { statusCode?: number; status?: number })?.statusCode === 503)
61
+ available.value = false
62
+ else throw e
63
+ } finally {
64
+ const next = new Set(loadingExecution.value)
65
+ next.delete(executionId)
66
+ loadingExecution.value = next
67
+ }
68
+ }
69
+
70
+ /** Fold a grading pushed over the stream into both the run cache and the screen history. */
71
+ function upsert(grading: KaizenGrading) {
72
+ const current = byExecution.value[grading.executionId] ?? []
73
+ const replaced = current.some((g) => g.id === grading.id)
74
+ const nextRun = replaced
75
+ ? current.map((g) => (g.id === grading.id ? grading : g))
76
+ : [...current, grading]
77
+ byExecution.value = { ...byExecution.value, [grading.executionId]: nextRun }
78
+ // Keep the screen history live too (newest first), if it's been loaded.
79
+ const inHistory = history.value.some((g) => g.id === grading.id)
80
+ if (inHistory) history.value = history.value.map((g) => (g.id === grading.id ? grading : g))
81
+ else history.value = [grading, ...history.value]
82
+ }
83
+
84
+ const isLoadingExecution = (executionId: string) => loadingExecution.value.has(executionId)
85
+ const verifiedCount = computed(() => verified.value.filter((c) => c.verified).length)
86
+
87
+ return {
88
+ byExecution,
89
+ history,
90
+ verified,
91
+ available,
92
+ loadingOverview,
93
+ verifiedCount,
94
+ gradingsFor,
95
+ gradingForStep,
96
+ loadOverview,
97
+ loadForExecution,
98
+ upsert,
99
+ isLoadingExecution,
100
+ }
101
+ })
@@ -48,6 +48,11 @@ export const usePipelinesStore = defineStore('pipelines', () => {
48
48
  const draftConsensus = ref<(ConsensusStepConfig | null)[]>([])
49
49
  /** Per-step estimate gating, kept index-aligned with `draft` (null ⇒ always run). */
50
50
  const draftGating = ref<(StepGating | null)[]>([])
51
+ /**
52
+ * Per-step Follow-up companion toggle, kept index-aligned with `draft`. Only meaningful on
53
+ * a `coder` step; `false` disables the companion there (default/true ⇒ enabled).
54
+ */
55
+ const draftFollowUps = ref<(boolean | null)[]>([])
51
56
  /** Organizational labels for the pipeline being assembled/edited. */
52
57
  const draftLabels = ref<string[]>([])
53
58
  const draftName = ref('New pipeline')
@@ -71,6 +76,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
71
76
  draftThresholds.value.splice(index, 0, null)
72
77
  draftConsensus.value.splice(index, 0, null)
73
78
  draftGating.value.splice(index, 0, null)
79
+ draftFollowUps.value.splice(index, 0, null)
74
80
  }
75
81
 
76
82
  function addToDraft(kind: AgentKind) {
@@ -84,6 +90,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
84
90
  draftThresholds.value.splice(index, 1)
85
91
  draftConsensus.value.splice(index, 1)
86
92
  draftGating.value.splice(index, 1)
93
+ draftFollowUps.value.splice(index, 1)
87
94
  }
88
95
 
89
96
  function moveInDraft(from: number, to: number) {
@@ -100,6 +107,8 @@ export const usePipelinesStore = defineStore('pipelines', () => {
100
107
  draftConsensus.value.splice(to, 0, cons ?? null)
101
108
  const [gat] = draftGating.value.splice(from, 1)
102
109
  draftGating.value.splice(to, 0, gat ?? null)
110
+ const [fu] = draftFollowUps.value.splice(from, 1)
111
+ draftFollowUps.value.splice(to, 0, fu ?? null)
103
112
  }
104
113
 
105
114
  /** Whether the producer step at `index` currently has its companion attached after it. */
@@ -174,6 +183,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
174
183
  draftThresholds.value = reorder(draftThresholds.value)
175
184
  draftConsensus.value = reorder(draftConsensus.value)
176
185
  draftGating.value = reorder(draftGating.value)
186
+ draftFollowUps.value = reorder(draftFollowUps.value)
177
187
  }
178
188
 
179
189
  /** Toggle the consensus mechanism on the draft step at `index` (default config / off). */
@@ -191,6 +201,12 @@ export const usePipelinesStore = defineStore('pipelines', () => {
191
201
  draftGates.value[index] = !draftGates.value[index]
192
202
  }
193
203
 
204
+ /** Toggle the Follow-up companion on the draft (coder) step at `index` (default on → off). */
205
+ function toggleDraftFollowUps(index: number) {
206
+ // Default (null/true) is enabled, so the first toggle disables it (false); toggle back to null.
207
+ draftFollowUps.value[index] = draftFollowUps.value[index] === false ? null : false
208
+ }
209
+
194
210
  /** Enable/disable the draft step at `index` without removing it. */
195
211
  function toggleDraftEnabled(index: number) {
196
212
  draftEnabled.value[index] = draftEnabled.value[index] === false
@@ -203,6 +219,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
203
219
  draftThresholds.value = []
204
220
  draftConsensus.value = []
205
221
  draftGating.value = []
222
+ draftFollowUps.value = []
206
223
  draftLabels.value = []
207
224
  draftName.value = 'New pipeline'
208
225
  editingId.value = null
@@ -216,6 +233,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
216
233
  draftThresholds.value = pipeline.agentKinds.map((_, i) => pipeline.thresholds?.[i] ?? null)
217
234
  draftConsensus.value = pipeline.agentKinds.map((_, i) => pipeline.consensus?.[i] ?? null)
218
235
  draftGating.value = pipeline.agentKinds.map((_, i) => pipeline.gating?.[i] ?? null)
236
+ draftFollowUps.value = pipeline.agentKinds.map((_, i) => pipeline.followUps?.[i] ?? null)
219
237
  draftLabels.value = [...(pipeline.labels ?? [])]
220
238
  draftName.value = pipeline.name
221
239
  editingId.value = pipeline.id
@@ -240,6 +258,11 @@ export const usePipelinesStore = defineStore('pipelines', () => {
240
258
  : {}),
241
259
  // Only send gating when at least one step has gating enabled.
242
260
  ...(draftGating.value.some((g) => g?.enabled) ? { gating: [...draftGating.value] } : {}),
261
+ // Only send followUps when at least one step disables it (default is on, so only the
262
+ // explicit `false` opt-outs are worth persisting).
263
+ ...(draftFollowUps.value.some((f) => f === false)
264
+ ? { followUps: [...draftFollowUps.value] }
265
+ : {}),
243
266
  // Only send labels when there are any.
244
267
  ...(draftLabels.value.length ? { labels: [...draftLabels.value] } : {}),
245
268
  }
@@ -297,6 +320,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
297
320
  draftThresholds,
298
321
  draftConsensus,
299
322
  draftGating,
323
+ draftFollowUps,
300
324
  draftLabels,
301
325
  draftName,
302
326
  editingId,
@@ -311,6 +335,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
311
335
  toggleCompanion,
312
336
  toggleDraftGating,
313
337
  toggleDraftGate,
338
+ toggleDraftFollowUps,
314
339
  toggleDraftEnabled,
315
340
  toggleDraftConsensus,
316
341
  setDraftConsensus,
package/app/stores/ui.ts CHANGED
@@ -135,6 +135,10 @@ export const useUiStore = defineStore('ui', () => {
135
135
  blockId: string
136
136
  instanceId: string | null
137
137
  stepIndex: number | null
138
+ // The brainstorm dialogue stage, set only when `view === 'brainstorm'` (its two agent
139
+ // kinds share one window). Derived from the step's agent kind on the pipeline path, or
140
+ // passed explicitly on an off-path open.
141
+ stage?: 'requirements' | 'architecture'
138
142
  } | null>(null)
139
143
 
140
144
  // Agent step-detail overlay: which pipeline step (a run instance + step index)
@@ -149,6 +153,10 @@ export const useUiStore = defineStore('ui', () => {
149
153
  // per-call detail from the observability store on open.
150
154
  const observabilityInstanceId = ref<string | null>(null)
151
155
 
156
+ // The Kaizen screen (grading history + verified-combo library), a full-panel overlay
157
+ // opened from the sidebar. Distinct from the per-run grading status shown in run details.
158
+ const kaizenScreenOpen = ref(false)
159
+
152
160
  /** Current canvas zoom (driven by Vue Flow viewport). */
153
161
  const zoom = ref(1)
154
162
 
@@ -229,7 +237,20 @@ export const useUiStore = defineStore('ui', () => {
229
237
  ? agentKindMeta(step.agentKind).resultView
230
238
  : undefined
231
239
  if (view && instance) {
232
- resultView.value = { view, blockId: instance.blockId, instanceId, stepIndex }
240
+ // The brainstorm window is shared by both stages; carry which one from the step's kind.
241
+ const stage =
242
+ view === 'brainstorm'
243
+ ? step?.agentKind === 'architecture-brainstorm'
244
+ ? 'architecture'
245
+ : 'requirements'
246
+ : undefined
247
+ resultView.value = {
248
+ view,
249
+ blockId: instance.blockId,
250
+ instanceId,
251
+ stepIndex,
252
+ ...(stage ? { stage } : {}),
253
+ }
233
254
  return
234
255
  }
235
256
  stepDetail.value = { instanceId, stepIndex }
@@ -452,10 +473,41 @@ export const useUiStore = defineStore('ui', () => {
452
473
  function openClarityReview(blockId: string) {
453
474
  resultView.value = { view: 'clarity-review', blockId, instanceId: null, stepIndex: null }
454
475
  }
476
+ function openBrainstorm(blockId: string, stage: 'requirements' | 'architecture') {
477
+ resultView.value = { view: 'brainstorm', blockId, instanceId: null, stepIndex: null, stage }
478
+ }
455
479
  // Open the service-spec window for a service frame (the inspector's "View Requirements").
456
480
  function openServiceSpec(blockId: string) {
457
481
  resultView.value = { view: 'service-spec', blockId, instanceId: null, stepIndex: null }
458
482
  }
483
+ // Open the Follow-up companion window for a run's Coder step (the blinking chip + the
484
+ // `followup_pending` notification). Resolves the Coder step index from the run when not
485
+ // given, so callers that only know the run can still open it.
486
+ function openFollowUps(instanceId: string, stepIndex: number | null = null) {
487
+ const execution = useExecutionStore()
488
+ const instance = execution.getInstance(instanceId)
489
+ if (!instance) return
490
+ // A pipeline may carry more than one follow-up-enabled Coder step, so don't blindly pick
491
+ // the first when no index is given: prefer the step that still has undecided items (the
492
+ // one the run is parked on), else the current step, else the first enabled one.
493
+ const resolveIdx = () => {
494
+ const pending = instance.steps.findIndex(
495
+ (s) => s.followUps?.enabled && s.followUps.items.some((i) => i.status === 'pending'),
496
+ )
497
+ if (pending >= 0) return pending
498
+ const current = instance.steps[instance.currentStep]
499
+ if (current?.followUps?.enabled) return instance.currentStep
500
+ return instance.steps.findIndex((s) => s.followUps?.enabled)
501
+ }
502
+ const idx = stepIndex ?? resolveIdx()
503
+ if (idx < 0) return
504
+ resultView.value = {
505
+ view: 'follow-ups',
506
+ blockId: instance.blockId,
507
+ instanceId,
508
+ stepIndex: idx,
509
+ }
510
+ }
459
511
  function closeResultView() {
460
512
  resultView.value = null
461
513
  }
@@ -473,6 +525,12 @@ export const useUiStore = defineStore('ui', () => {
473
525
  function closeObservability() {
474
526
  observabilityInstanceId.value = null
475
527
  }
528
+ function openKaizen() {
529
+ kaizenScreenOpen.value = true
530
+ }
531
+ function closeKaizen() {
532
+ kaizenScreenOpen.value = false
533
+ }
476
534
 
477
535
  return {
478
536
  selectedBlockId,
@@ -513,6 +571,7 @@ export const useUiStore = defineStore('ui', () => {
513
571
  closeResultView,
514
572
  stepDetail,
515
573
  observabilityInstanceId,
574
+ kaizenScreenOpen,
516
575
  zoom,
517
576
  lod,
518
577
  expandedFrames,
@@ -583,11 +642,15 @@ export const useUiStore = defineStore('ui', () => {
583
642
  resetAiOnboarding,
584
643
  openRequirementReview,
585
644
  openClarityReview,
645
+ openBrainstorm,
586
646
  openServiceSpec,
647
+ openFollowUps,
587
648
  closeRequirementReview,
588
649
  openStepDetail,
589
650
  closeStepDetail,
590
651
  openObservability,
591
652
  closeObservability,
653
+ openKaizen,
654
+ closeKaizen,
592
655
  }
593
656
  })
@@ -10,6 +10,7 @@ const DEFAULTS: WorkspaceSettings = {
10
10
  taskLimitShared: null,
11
11
  taskLimitPerType: null,
12
12
  storeAgentContext: true,
13
+ kaizenEnabled: true,
13
14
  spendCurrency: null,
14
15
  spendMonthlyLimit: null,
15
16
  }
@@ -0,0 +1,55 @@
1
+ // Brainstorm (structured-dialogue) wire types. Mirror of `@cat-factory/contracts`'
2
+ // brainstorm.ts, kept in sync by hand like the rest of `~/types/*` (the SPA does not import
3
+ // the backend package directly).
4
+ //
5
+ // A brainstorm agent runs a structured dialogue: it PROPOSES options with explicit
6
+ // trade-offs (raised as review items), a human picks / steers / dismisses, and the picks are
7
+ // folded into ONE converged direction. There are two stages (`requirements`, `architecture`)
8
+ // served by one engine; a block may have one live session per stage.
9
+ //
10
+ // Structurally identical to a requirements review (the items share the same shape), so the
11
+ // per-item types are reused from `~/types/requirements`; only the `stage` discriminator and
12
+ // the converged document (`convergedDirection`) differ.
13
+
14
+ import type {
15
+ RequirementReviewItem,
16
+ ReviewItemCategory,
17
+ ReviewItemSeverity,
18
+ ReviewItemStatus,
19
+ } from '~/types/requirements'
20
+
21
+ export type { ReviewItemCategory, ReviewItemSeverity, ReviewItemStatus }
22
+
23
+ /** Which dialogue a brainstorm session drives. */
24
+ export type BrainstormStage = 'requirements' | 'architecture'
25
+
26
+ /** A brainstorm option is the same shape as a requirements-review item. */
27
+ export type BrainstormItem = RequirementReviewItem
28
+
29
+ /** Lifecycle of a brainstorm session — identical to the requirements review lifecycle. */
30
+ export type BrainstormStatus =
31
+ | 'ready'
32
+ | 'incorporating'
33
+ | 'reviewing'
34
+ | 'merged'
35
+ | 'exceeded'
36
+ | 'incorporated'
37
+
38
+ /** How a human resolves a session that hit its iteration cap. */
39
+ export type ResolveBrainstormExceededChoice = 'extra-round' | 'proceed' | 'stop-reset'
40
+
41
+ export interface BrainstormSession {
42
+ id: string
43
+ blockId: string
44
+ stage: BrainstormStage
45
+ status: BrainstormStatus
46
+ items: BrainstormItem[]
47
+ model: string | null
48
+ convergedDirection: string | null
49
+ /** Agent passes run so far (initial pass is 1; each re-run adds one). */
50
+ iteration: number
51
+ /** The agent-pass budget (from the task's merge preset; an extra round bumps it). */
52
+ maxIterations: number
53
+ createdAt: number
54
+ updatedAt: number
55
+ }
@@ -20,6 +20,7 @@ import type { Notification } from './notifications'
20
20
  import type { RequirementReview } from './requirements'
21
21
  import type { ConsensusSession, ConsensusStepConfig, StepGating, TaskEstimate } from './consensus'
22
22
  import type { ClarityReview } from './clarity'
23
+ import type { BrainstormSession } from './brainstorm'
23
24
  import type { MergeThresholdPreset } from './merge'
24
25
  import type { ModelPreset } from './model-presets'
25
26
  import type { PipelineSchedule } from './recurring'
@@ -256,6 +257,11 @@ export interface TestReport {
256
257
  /** The kinds of agents available in the agent palette. */
257
258
  export type AgentKind =
258
259
  | 'requirements-review'
260
+ // Brainstorm (structured-dialogue) gates: propose options with trade-offs and let the human
261
+ // converge. `requirements-brainstorm` runs before the requirements review; `architecture-
262
+ // brainstorm` before the architect. Both open the shared brainstorm window.
263
+ | 'requirements-brainstorm'
264
+ | 'architecture-brainstorm'
259
265
  | 'architect'
260
266
  | 'researcher'
261
267
  | 'coder'
@@ -309,6 +315,9 @@ export type AgentKind =
309
315
  // validate the change in a live URL, dispatching the Tester's `fixer` (from findings) or
310
316
  // the `conflict-resolver` (on a conflicting pull-main) on demand. Opens its own window.
311
317
  | 'human-test'
318
+ // The Kaizen agent: post-run grader (NOT a pipeline step / palette archetype). Surfaced
319
+ // only in Model Configuration (its model is pinnable like any agent) and run details.
320
+ | 'kaizen'
312
321
 
313
322
  /** A draggable agent definition shown in the agent palette. */
314
323
  /** Palette grouping for the agent archetypes (collapsible sections in the builder). */
@@ -387,6 +396,12 @@ export interface Pipeline {
387
396
  * always run. Used to make a companion conditional on the task estimate.
388
397
  */
389
398
  gating?: (StepGating | null)[]
399
+ /**
400
+ * Per-step Follow-up companion toggle, parallel to `agentKinds`: governs whether a `coder`
401
+ * step runs the future-looking Follow-up companion. `followUps[i] === false` disables it;
402
+ * `null`/`true`/absent ⇒ enabled (a Coder step gets it by default). Ignored on non-coder steps.
403
+ */
404
+ followUps?: (boolean | null)[]
390
405
  /** Free-form organizational labels for the library (filter/search). */
391
406
  labels?: string[]
392
407
  /** True when archived: kept but hidden from the default library view. */
@@ -495,6 +510,8 @@ export interface WorkspaceSettings {
495
510
  taskLimitPerType: Partial<Record<CreateTaskType, number>> | null
496
511
  /** Whether to store the complete provided-context snapshot for each container agent. */
497
512
  storeAgentContext: boolean
513
+ /** Whether the Kaizen agent grades agent steps after each run. On by default. */
514
+ kaizenEnabled: boolean
498
515
  /** Spend budget currency (ISO 4217). Null ⇒ the built-in default (`EUR`). */
499
516
  spendCurrency: string | null
500
517
  /** Monthly spend budget in `spendCurrency`. Null ⇒ the built-in default. */
@@ -508,6 +525,7 @@ export interface UpdateWorkspaceSettingsInput {
508
525
  taskLimitShared?: number | null
509
526
  taskLimitPerType?: Partial<Record<CreateTaskType, number>> | null
510
527
  storeAgentContext?: boolean
528
+ kaizenEnabled?: boolean
511
529
  spendCurrency?: string | null
512
530
  spendMonthlyLimit?: number | null
513
531
  }
@@ -520,6 +538,50 @@ export interface ServiceFragmentDefaults {
520
538
  fragmentIds: string[]
521
539
  }
522
540
 
541
+ /** Lifecycle of a Kaizen grading. Mirrors `@cat-factory/contracts`. */
542
+ export type KaizenGradingStatus = 'scheduled' | 'running' | 'complete' | 'failed'
543
+
544
+ /**
545
+ * A Kaizen grading of one completed agent step (how smooth/efficient the interaction
546
+ * was, 1..5, plus recommendations). Mirrors `@cat-factory/contracts`.
547
+ */
548
+ export interface KaizenGrading {
549
+ id: string
550
+ executionId: string
551
+ blockId: string
552
+ stepIndex: number
553
+ agentKind: string
554
+ model: string
555
+ promptVersion: number
556
+ comboKey: string
557
+ status: KaizenGradingStatus
558
+ grade: number | null
559
+ summary: string
560
+ recommendations: string[]
561
+ graderModel: string | null
562
+ error: string | null
563
+ createdAt: number
564
+ updatedAt: number
565
+ }
566
+
567
+ /** A `(promptVersion, agentKind, model)` combo's verification progress. */
568
+ export interface KaizenVerifiedCombo {
569
+ comboKey: string
570
+ agentKind: string
571
+ model: string
572
+ promptVersion: number
573
+ consecutiveHighGrades: number
574
+ verified: boolean
575
+ verifiedAt: number | null
576
+ updatedAt: number
577
+ }
578
+
579
+ /** The Kaizen screen payload: recent grading history + the verified-combo library. */
580
+ export interface KaizenOverview {
581
+ gradings: KaizenGrading[]
582
+ verified: KaizenVerifiedCombo[]
583
+ }
584
+
523
585
  /**
524
586
  * Real-time events pushed over the workspace WebSocket stream (see
525
587
  * `useWorkspaceStream`). Mirrors `WorkspaceEvent` in `@cat-factory/contracts`.
@@ -533,6 +595,8 @@ export type WorkspaceEvent =
533
595
  | { type: 'requirements'; review: RequirementReview; at: number }
534
596
  | { type: 'consensus'; session: ConsensusSession; at: number }
535
597
  | { type: 'clarity'; review: ClarityReview; at: number }
598
+ | { type: 'brainstorm'; session: BrainstormSession; at: number }
599
+ | { type: 'kaizen'; grading: KaizenGrading; at: number }
536
600
 
537
601
  /** Level-of-detail buckets driven by the canvas zoom level. Shallow → deep:
538
602
  * `far`/`mid`/`close` govern a service frame (chip → card → opened with tasks);
@@ -382,6 +382,47 @@ export interface PipelineStep {
382
382
  * Mirrors `runEnvironmentSchema`.
383
383
  */
384
384
  environment?: RunEnvironment | null
385
+ /**
386
+ * Live Follow-up companion state when this (coder) step has the future-looking companion
387
+ * enabled: the forward-looking items the Coder streamed (loose ends / side-tasks /
388
+ * questions) and the send-back loop budget. The chip blinks while any item is `pending`;
389
+ * the gate holds the pipeline until every item is decided. Mirrors `followUpsStepStateSchema`.
390
+ */
391
+ followUps?: FollowUpsStepState | null
392
+ }
393
+
394
+ /** What a streamed item is: a forward-looking follow-up or a clarifying question. */
395
+ export type FollowUpItemKind = 'follow_up' | 'question'
396
+
397
+ /** Lifecycle of a single follow-up / question item (mirrors `followUpItemStatusSchema`). */
398
+ export type FollowUpItemStatus = 'pending' | 'filed' | 'queued' | 'answered' | 'dismissed'
399
+
400
+ /** One forward-looking item the Coder surfaced (mirrors `followUpItemSchema`). */
401
+ export interface FollowUpItem {
402
+ id: string
403
+ kind: FollowUpItemKind
404
+ title: string
405
+ detail: string
406
+ suggestedAction?: string | null
407
+ status: FollowUpItemStatus
408
+ /** The human's answer to a `question` item, or null while unanswered / not a question. */
409
+ answer?: string | null
410
+ /** Canonical external id of the filed ticket (e.g. "owner/repo#123"), when `filed`. */
411
+ ticketExternalId?: string | null
412
+ /** URL of the filed ticket, when `filed`. */
413
+ ticketUrl?: string | null
414
+ /** True once a queued / answered item was folded into a Coder loop-back. */
415
+ sentToCoder?: boolean
416
+ createdAt: number
417
+ updatedAt: number
418
+ }
419
+
420
+ /** Live Follow-up companion state on the Coder step (mirrors `followUpsStepStateSchema`). */
421
+ export interface FollowUpsStepState {
422
+ enabled: boolean
423
+ items: FollowUpItem[]
424
+ loops?: number
425
+ maxLoops?: number
385
426
  }
386
427
 
387
428
  /** One failing CI check the gate's precheck saw (mirrors `gateFailingCheckSchema`). */
@@ -15,6 +15,7 @@ export type NotificationType =
15
15
  | 'release_regression'
16
16
  | 'decision_required'
17
17
  | 'human_test_ready'
18
+ | 'followup_pending'
18
19
  export type NotificationStatus = 'open' | 'acted' | 'dismissed'
19
20
 
20
21
  /** The on-call agent's recommendation on a `release_regression`. */
@@ -14,6 +14,8 @@ import {
14
14
  const AGENT_KINDS: AgentKind[] = [
15
15
  'requirements-review',
16
16
  'clarity-review',
17
+ 'requirements-brainstorm',
18
+ 'architecture-brainstorm',
17
19
  'bug-investigator',
18
20
  'task-estimator',
19
21
  'architect',