@cat-factory/app 0.31.0 → 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.
@@ -0,0 +1,210 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type {
4
+ BrainstormSession,
5
+ BrainstormStage,
6
+ ResolveBrainstormExceededChoice,
7
+ ReviewItemStatus,
8
+ } from '~/types/brainstorm'
9
+ import { useWorkspaceStore } from '~/stores/workspace'
10
+
11
+ /**
12
+ * Brainstorm (structured-dialogue) state. On the pipeline path a brainstorm runs as an opt-in
13
+ * gate step: the run parks while the human picks / steers / dismisses the proposed options, then
14
+ * asks to incorporate. Incorporation + the re-run run ASYNCHRONOUSLY in the durable driver — the
15
+ * call returns at once (status `incorporating`) and the user goes back to the board; they are
16
+ * summoned again (a notification) only if the re-run yields options or hits the cap. The store is
17
+ * patched both from call responses and from live `brainstorm` stream events (see `upsert`).
18
+ *
19
+ * A block may have one live session per STAGE (`requirements` / `architecture`), so the cache is
20
+ * keyed by `${blockId}:${stage}`. `available` mirrors the backend's opt-in gate (a 503 hides the
21
+ * UI). Per-workspace; nothing is persisted client-side.
22
+ */
23
+ export const useBrainstormStore = defineStore('brainstorm', () => {
24
+ const api = useApi()
25
+ const workspace = useWorkspaceStore()
26
+
27
+ const key = (blockId: string, stage: BrainstormStage) => `${blockId}:${stage}`
28
+
29
+ /** null = unknown (not probed), true/false = feature on/off. */
30
+ const available = ref<boolean | null>(null)
31
+ /** The current session per `${blockId}:${stage}` (null = fetched, none exists). */
32
+ const sessions = ref<Record<string, BrainstormSession | null>>({})
33
+ /** `${blockId}:${stage}` keys whose agent is currently running (run / re-run). */
34
+ const running = ref<Set<string>>(new Set())
35
+ /** Session ids currently incorporating their picks. */
36
+ const incorporating = ref<Set<string>>(new Set())
37
+ /** `${blockId}:${stage}` keys whose current session is being fetched (the initial `load`). */
38
+ const loadingByKey = ref<Set<string>>(new Set())
39
+ const inFlight = new Map<string, Promise<void>>()
40
+
41
+ function sessionFor(blockId: string, stage: BrainstormStage): BrainstormSession | null {
42
+ return sessions.value[key(blockId, stage)] ?? null
43
+ }
44
+ /** The async background stage a session is in, or null (so the board can show "working"). */
45
+ function backgroundStage(
46
+ blockId: string,
47
+ stage: BrainstormStage,
48
+ ): 'incorporating' | 'reviewing' | null {
49
+ const status = sessions.value[key(blockId, stage)]?.status
50
+ return status === 'incorporating' || status === 'reviewing' ? status : null
51
+ }
52
+ function isRunning(blockId: string, stage: BrainstormStage): boolean {
53
+ return running.value.has(key(blockId, stage))
54
+ }
55
+ function isLoading(blockId: string, stage: BrainstormStage): boolean {
56
+ return loadingByKey.value.has(key(blockId, stage))
57
+ }
58
+ function isIncorporating(sessionId: string): boolean {
59
+ return incorporating.value.has(sessionId)
60
+ }
61
+
62
+ /** Options still needing a human (status `open`). */
63
+ function openCount(session: BrainstormSession): number {
64
+ return session.items.filter((i) => i.status === 'open').length
65
+ }
66
+ /** Options the human chose (a reply recorded), which the companion folds in. */
67
+ function answeredCount(session: BrainstormSession): number {
68
+ return session.items.filter((i) => i.status === 'answered' || i.status === 'resolved').length
69
+ }
70
+ /** Every option is settled (chosen or dismissed) — none still open. */
71
+ function allSettled(session: BrainstormSession): boolean {
72
+ return openCount(session) === 0
73
+ }
74
+ /** Incorporation is possible: all options settled AND at least one was chosen. */
75
+ function canIncorporate(session: BrainstormSession): boolean {
76
+ return allSettled(session) && answeredCount(session) > 0
77
+ }
78
+ /** Proceed (skip the companion) is possible: all options settled but none chosen. */
79
+ function canProceed(session: BrainstormSession): boolean {
80
+ return allSettled(session) && answeredCount(session) === 0
81
+ }
82
+
83
+ function store(session: BrainstormSession) {
84
+ sessions.value = { ...sessions.value, [key(session.blockId, session.stage)]: session }
85
+ }
86
+
87
+ function withFlag(set: typeof running, k: string, on: boolean) {
88
+ const next = new Set(set.value)
89
+ if (on) next.add(k)
90
+ else next.delete(k)
91
+ set.value = next
92
+ }
93
+
94
+ /** Fetch the current session for a block + stage (probing the feature's availability). */
95
+ async function load(blockId: string, stage: BrainstormStage) {
96
+ if (!workspace.workspaceId) return
97
+ const k = key(blockId, stage)
98
+ const pending = inFlight.get(k)
99
+ if (pending) return pending
100
+ const promise = (async () => {
101
+ withFlag(loadingByKey, k, true)
102
+ try {
103
+ const session = await api.getBrainstorm(workspace.requireId(), blockId, stage)
104
+ available.value = true
105
+ sessions.value = { ...sessions.value, [k]: session }
106
+ } catch {
107
+ available.value = false
108
+ } finally {
109
+ withFlag(loadingByKey, k, false)
110
+ inFlight.delete(k)
111
+ }
112
+ })()
113
+ inFlight.set(k, promise)
114
+ return promise
115
+ }
116
+
117
+ /** Record a human's choice on one option. */
118
+ async function reply(session: BrainstormSession, itemId: string, text: string) {
119
+ store(await api.replyBrainstormItem(workspace.requireId(), session.id, itemId, text))
120
+ }
121
+
122
+ /** Set an option's status (dismiss / reopen). */
123
+ async function setItemStatus(
124
+ session: BrainstormSession,
125
+ itemId: string,
126
+ status: ReviewItemStatus,
127
+ ) {
128
+ store(await api.setBrainstormItemStatus(workspace.requireId(), session.id, itemId, status))
129
+ }
130
+
131
+ /**
132
+ * Ask the driver to incorporate the picks ASYNCHRONOUSLY. Optional `feedback` is the "do it
133
+ * differently" direction when redoing a merge. Returns at once with the `incorporating`
134
+ * session (the fold + re-run happen in the background).
135
+ */
136
+ async function incorporate(session: BrainstormSession, feedback?: string) {
137
+ withFlag(incorporating, session.id, true)
138
+ try {
139
+ const updated = await api.incorporateBrainstorm(
140
+ workspace.requireId(),
141
+ session.blockId,
142
+ session.stage,
143
+ feedback,
144
+ )
145
+ store(updated)
146
+ return updated
147
+ } finally {
148
+ withFlag(incorporating, session.id, false)
149
+ }
150
+ }
151
+
152
+ /** Re-run the brainstorm against the converged direction (one more pass; may converge/advance). */
153
+ async function reReview(blockId: string, stage: BrainstormStage): Promise<BrainstormSession> {
154
+ withFlag(running, key(blockId, stage), true)
155
+ try {
156
+ const updated = await api.reReviewBrainstorm(workspace.requireId(), blockId, stage)
157
+ store(updated)
158
+ return updated
159
+ } finally {
160
+ withFlag(running, key(blockId, stage), false)
161
+ }
162
+ }
163
+
164
+ /** Proceed: settle the brainstorm and advance the parked run. */
165
+ async function proceed(blockId: string, stage: BrainstormStage): Promise<BrainstormSession> {
166
+ const updated = await api.proceedBrainstorm(workspace.requireId(), blockId, stage)
167
+ store(updated)
168
+ return updated
169
+ }
170
+
171
+ /** Resolve a capped session: extra-round / proceed / stop-reset. */
172
+ async function resolveExceeded(
173
+ blockId: string,
174
+ stage: BrainstormStage,
175
+ choice: ResolveBrainstormExceededChoice,
176
+ ): Promise<BrainstormSession> {
177
+ const updated = await api.resolveBrainstormExceeded(
178
+ workspace.requireId(),
179
+ blockId,
180
+ stage,
181
+ choice,
182
+ )
183
+ store(updated)
184
+ return updated
185
+ }
186
+
187
+ return {
188
+ available,
189
+ sessions,
190
+ sessionFor,
191
+ backgroundStage,
192
+ isRunning,
193
+ isLoading,
194
+ isIncorporating,
195
+ openCount,
196
+ answeredCount,
197
+ allSettled,
198
+ canIncorporate,
199
+ canProceed,
200
+ load,
201
+ reply,
202
+ setItemStatus,
203
+ incorporate,
204
+ reReview,
205
+ proceed,
206
+ resolveExceeded,
207
+ // Patch the cache from a live `brainstorm` stream event.
208
+ upsert: store,
209
+ }
210
+ })
@@ -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
+ })
@@ -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)
@@ -233,7 +237,20 @@ export const useUiStore = defineStore('ui', () => {
233
237
  ? agentKindMeta(step.agentKind).resultView
234
238
  : undefined
235
239
  if (view && instance) {
236
- 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
+ }
237
254
  return
238
255
  }
239
256
  stepDetail.value = { instanceId, stepIndex }
@@ -456,10 +473,41 @@ export const useUiStore = defineStore('ui', () => {
456
473
  function openClarityReview(blockId: string) {
457
474
  resultView.value = { view: 'clarity-review', blockId, instanceId: null, stepIndex: null }
458
475
  }
476
+ function openBrainstorm(blockId: string, stage: 'requirements' | 'architecture') {
477
+ resultView.value = { view: 'brainstorm', blockId, instanceId: null, stepIndex: null, stage }
478
+ }
459
479
  // Open the service-spec window for a service frame (the inspector's "View Requirements").
460
480
  function openServiceSpec(blockId: string) {
461
481
  resultView.value = { view: 'service-spec', blockId, instanceId: null, stepIndex: null }
462
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
+ }
463
511
  function closeResultView() {
464
512
  resultView.value = null
465
513
  }
@@ -594,7 +642,9 @@ export const useUiStore = defineStore('ui', () => {
594
642
  resetAiOnboarding,
595
643
  openRequirementReview,
596
644
  openClarityReview,
645
+ openBrainstorm,
597
646
  openServiceSpec,
647
+ openFollowUps,
598
648
  closeRequirementReview,
599
649
  openStepDetail,
600
650
  closeStepDetail,
@@ -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'
@@ -390,6 +396,12 @@ export interface Pipeline {
390
396
  * always run. Used to make a companion conditional on the task estimate.
391
397
  */
392
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)[]
393
405
  /** Free-form organizational labels for the library (filter/search). */
394
406
  labels?: string[]
395
407
  /** True when archived: kept but hidden from the default library view. */
@@ -583,6 +595,7 @@ export type WorkspaceEvent =
583
595
  | { type: 'requirements'; review: RequirementReview; at: number }
584
596
  | { type: 'consensus'; session: ConsensusSession; at: number }
585
597
  | { type: 'clarity'; review: ClarityReview; at: number }
598
+ | { type: 'brainstorm'; session: BrainstormSession; at: number }
586
599
  | { type: 'kaizen'; grading: KaizenGrading; at: number }
587
600
 
588
601
  /** Level-of-detail buckets driven by the canvas zoom level. Shallow → deep:
@@ -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',