@cat-factory/app 0.153.3 → 0.154.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.
@@ -1,7 +1,6 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { computed, ref } from 'vue'
3
3
  import type {
4
- CreateBranchInput,
5
4
  GitHubAvailableRepo,
6
5
  GitHubBranch,
7
6
  GitHubConnection,
@@ -9,18 +8,15 @@ import type {
9
8
  GitHubIssue,
10
9
  GitHubPullRequest,
11
10
  GitHubRepo,
12
- MergePullRequestInput,
13
- OpenPullRequestInput,
14
11
  RepoTreeEntry,
15
- ResyncRequest,
16
12
  } from '~/types/domain'
17
13
  import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
18
14
  import { useUpsertList } from '~/composables/useUpsertList'
19
15
  import { useWorkspaceStore } from '~/stores/workspace'
20
16
  import { useServicesStore } from '~/stores/services'
21
-
22
- /** Stable identity for a pull request in the `pulls` list: repo + PR number. */
23
- const pullKey = (repoGithubId: number, number: number) => `${repoGithubId}:${number}`
17
+ import { pullKey, type GitHubStoreContext } from '~/stores/github/context'
18
+ import { createGitHubConnectionActions } from '~/stores/github/connection'
19
+ import { createGitHubRepoActions } from '~/stores/github/repoActions'
24
20
 
25
21
  /**
26
22
  * GitHub integration state: the workspace's App installation, the projected
@@ -155,170 +151,35 @@ export const useGitHubStore = defineStore('github', () => {
155
151
  if (connected.value && repos.value.length === 0) await load()
156
152
  }
157
153
 
158
- /**
159
- * Load the repos the installation can access, with this workspace's link state.
160
- * With a `q` the backend filters `owner/name` server-side (the add-service picker
161
- * searches instead of prefetching a huge installation); without one it browses all
162
- * (the repo-link panel). A blank/short `q` clears the list rather than fetching.
163
- */
164
- async function loadAvailableRepos(q?: string) {
165
- if (!connected.value) return
166
- if (q !== undefined && q.trim() === '') {
167
- availableRepos.value = []
168
- return
169
- }
170
- loadingAvailable.value = true
171
- try {
172
- availableRepos.value = await api.listGitHubAvailableRepos(workspace.requireId(), q)
173
- } finally {
174
- loadingAvailable.value = false
175
- }
176
- }
177
-
178
- /**
179
- * Search the installation/PAT-accessible repos server-side WITHOUT touching the shared
180
- * `availableRepos`/`loadingAvailable` singleton — it returns the matches to the caller instead.
181
- * This is the reusable form behind {@link useRepoSearch}: two independent pickers (the
182
- * add-service modal and the doc-task reference-repo picker) can search concurrently without
183
- * clobbering each other's results. A blank/short `q` (or no connection) returns `[]`.
184
- */
185
- async function searchAvailableRepos(q: string): Promise<GitHubAvailableRepo[]> {
186
- if (!connected.value || q.trim() === '') return []
187
- return api.listGitHubAvailableRepos(workspace.requireId(), q)
188
- }
189
-
190
- /** Set the exact set of repos this workspace links, then refresh projections. */
191
- async function setLinkedRepos(repoGithubIds: number[]) {
192
- savingRepos.value = true
193
- try {
194
- repos.value = await api.setGitHubLinkedRepos(workspace.requireId(), repoGithubIds)
195
- // Reflect the new link state in the picker and refresh PRs/issues.
196
- const linked = new Set(repoGithubIds)
197
- availableRepos.value = availableRepos.value.map((r) => ({
198
- ...r,
199
- linked: linked.has(r.githubId),
200
- }))
201
- await load()
202
- } finally {
203
- savingRepos.value = false
204
- }
205
- }
206
-
207
- /** Lazily load (and cache) the branches for a single repo. */
208
- async function loadBranches(repoGithubId: number): Promise<GitHubBranch[]> {
209
- const list = await api.listGitHubBranches(workspace.requireId(), repoGithubId)
210
- branches.value = { ...branches.value, [repoGithubId]: list }
211
- return list
212
- }
213
-
214
- /** List one level of a (monorepo) repo's tree, for the service-directory picker. */
215
- function loadRepoTree(repoGithubId: number, path = '') {
216
- return api.listGitHubRepoTree(workspace.requireId(), repoGithubId, path)
217
- }
218
-
219
154
  /** Full file listing per repo (recursive tree), cached by GitHub numeric id. */
220
155
  const repoFiles = ref<Record<number, RepoTreeEntry[]>>({})
221
156
 
222
- /**
223
- * Load (and cache) EVERY file in a repo the whole tree in one recursive read so a
224
- * picker can search files by path entirely client-side (no per-keystroke server call).
225
- * Cached by repo id so re-opening the picker for the same repo is instant; force a
226
- * refetch with `{ reload: true }`. Mirrors the branches cache.
227
- */
228
- async function loadRepoFiles(
229
- repoGithubId: number,
230
- opts: { reload?: boolean } = {},
231
- ): Promise<RepoTreeEntry[]> {
232
- const cached = repoFiles.value[repoGithubId]
233
- if (cached && !opts.reload) return cached
234
- const files = await api.listGitHubRepoFiles(workspace.requireId(), repoGithubId)
235
- repoFiles.value = { ...repoFiles.value, [repoGithubId]: files }
236
- return files
237
- }
238
-
239
- /** The URL a workspace owner visits to install the App against this workspace. */
240
- function getInstallUrl(): Promise<string> {
241
- return api.getGitHubInstallUrl(workspace.requireId()).then((r) => r.url)
242
- }
243
-
244
- /** Discover the App's installations so the user can connect one without typing an id. */
245
- async function loadInstallations() {
246
- loadingInstallations.value = true
247
- try {
248
- const { installations: list } = await api.listGitHubInstallations(workspace.requireId())
249
- installations.value = list
250
- } finally {
251
- loadingInstallations.value = false
252
- }
253
- }
254
-
255
- /** Programmatic bind by installation id (the browser flow uses the redirect). */
256
- async function connect(installationId: number) {
257
- connection.value = await api.connectGitHub(workspace.requireId(), installationId)
258
- available.value = true
259
- await load()
260
- }
261
-
262
- async function disconnect() {
263
- await api.disconnectGitHub(workspace.requireId())
264
- connection.value = null
265
- repos.value = []
266
- availableRepos.value = []
267
- pulls.value = []
268
- issues.value = []
269
- branches.value = {}
270
- }
271
-
272
- /** Trigger a resync, then refresh projections (no-op for queued/backfill). */
273
- async function resync(body: ResyncRequest = {}) {
274
- syncing.value = true
275
- try {
276
- const res = await api.resyncGitHub(workspace.requireId(), body)
277
- await load()
278
- return res
279
- } finally {
280
- syncing.value = false
281
- }
282
- }
283
-
284
- // ---- repo writes ----------------------------------------------------------
285
-
286
- /**
287
- * Create a repository under the connected account (privileged App tier). Only
288
- * meaningful when `canCreateRepos`; the backend 409s otherwise. Returns the
289
- * created repo so the caller can confirm/link it.
290
- */
291
- function createRepo(input: Parameters<typeof api.createGitHubRepo>[1]) {
292
- return api.createGitHubRepo(workspace.requireId(), input)
293
- }
294
-
295
- async function createBranch(repoGithubId: number, input: CreateBranchInput) {
296
- const branch = await api.createGitHubBranch(workspace.requireId(), repoGithubId, input)
297
- const next = branches.value[repoGithubId] ?? []
298
- branches.value = { ...branches.value, [repoGithubId]: [branch, ...next] }
299
- return branch
300
- }
301
-
302
- async function openPullRequest(repoGithubId: number, input: OpenPullRequestInput) {
303
- const pr = await api.openGitHubPullRequest(workspace.requireId(), repoGithubId, input)
304
- upsertPull(pr)
305
- return pr
306
- }
307
-
308
- async function mergePullRequest(
309
- repoGithubId: number,
310
- number: number,
311
- input: MergePullRequestInput = {},
312
- ) {
313
- await api.mergeGitHubPullRequest(workspace.requireId(), repoGithubId, number, input)
314
- // Optimistically reflect the merge until the next sync confirms it.
315
- const existing = getPull(pullKey(repoGithubId, number))
316
- if (existing) upsertPull({ ...existing, state: 'closed', merged: true })
317
- }
318
-
319
- function comment(repoGithubId: number, number: number, body: string) {
320
- return api.commentGitHubIssue(workspace.requireId(), repoGithubId, number, body)
157
+ // The installation lifecycle + the per-repo reads/writes, split into cohesive factories
158
+ // sharing the state above (a size-only extraction mirroring `stores/board/`behaviour is
159
+ // identical to the former in-closure functions).
160
+ const context: GitHubStoreContext = {
161
+ api,
162
+ workspace,
163
+ available,
164
+ connection,
165
+ installations,
166
+ loadingInstallations,
167
+ repos,
168
+ availableRepos,
169
+ loadingAvailable,
170
+ savingRepos,
171
+ pulls,
172
+ upsertPull,
173
+ getPull,
174
+ issues,
175
+ branches,
176
+ repoFiles,
177
+ syncing,
178
+ connected,
179
+ load,
321
180
  }
181
+ const connectionActions = createGitHubConnectionActions(context)
182
+ const repoActions = createGitHubRepoActions(context)
322
183
 
323
184
  /**
324
185
  * Drop the per-workspace projection + connection state (called on workspace switch)
@@ -365,23 +226,9 @@ export const useGitHubStore = defineStore('github', () => {
365
226
  ensureProbed,
366
227
  load,
367
228
  ensureLoaded,
368
- loadAvailableRepos,
369
- searchAvailableRepos,
370
- setLinkedRepos,
371
- loadRepoTree,
372
229
  repoFiles,
373
- loadRepoFiles,
374
- loadBranches,
375
- getInstallUrl,
376
- loadInstallations,
377
- connect,
378
- disconnect,
379
- resync,
380
- createRepo,
381
- createBranch,
382
- openPullRequest,
383
- mergePullRequest,
384
- comment,
230
+ ...connectionActions,
231
+ ...repoActions,
385
232
  reset,
386
233
  }
387
234
  })
@@ -0,0 +1,89 @@
1
+ import { ref } from 'vue'
2
+ import type {
3
+ InitiativeExecutionPolicy,
4
+ PromoteInitiativeFollowUpInput,
5
+ UpdateInitiativeItemInput,
6
+ } from '~/types/domain'
7
+ import type { InitiativeActionContext } from './planning'
8
+
9
+ /**
10
+ * The tracker-curation actions: promoting / dismissing a harvested follow-up, editing one
11
+ * tracker item (or driving its status), and replacing the execution policy. Every one runs
12
+ * through the shared `curate` wrapper so the window has a single in-flight flag.
13
+ */
14
+ export function createInitiativeCurationActions(ctx: InitiativeActionContext) {
15
+ const { api, workspace, upsert } = ctx
16
+
17
+ /** True while a curation action (promote/dismiss/edit item/edit policy) is in flight. */
18
+ const curating = ref(false)
19
+
20
+ async function curate<T>(fn: () => Promise<T>): Promise<T> {
21
+ if (!workspace.workspaceId) throw new Error('No active workspace')
22
+ curating.value = true
23
+ try {
24
+ return await fn()
25
+ } finally {
26
+ curating.value = false
27
+ }
28
+ }
29
+
30
+ /** Promote an `open` harvested follow-up into a new pending tracker item. */
31
+ async function promoteFollowUp(
32
+ initiativeId: string,
33
+ followUpId: string,
34
+ input: PromoteInitiativeFollowUpInput,
35
+ ) {
36
+ return curate(async () => {
37
+ const updated = await api.promoteInitiativeFollowUp(
38
+ workspace.workspaceId!,
39
+ initiativeId,
40
+ followUpId,
41
+ input,
42
+ )
43
+ upsert(updated)
44
+ return updated
45
+ })
46
+ }
47
+
48
+ /** Dismiss a harvested follow-up. */
49
+ async function dismissFollowUp(initiativeId: string, followUpId: string) {
50
+ return curate(async () => {
51
+ const updated = await api.dismissInitiativeFollowUp(
52
+ workspace.workspaceId!,
53
+ initiativeId,
54
+ followUpId,
55
+ )
56
+ upsert(updated)
57
+ return updated
58
+ })
59
+ }
60
+
61
+ /** Edit one tracker item and/or drive its status (retry a blocked item / skip it). */
62
+ async function updateItem(
63
+ initiativeId: string,
64
+ itemId: string,
65
+ input: UpdateInitiativeItemInput,
66
+ ) {
67
+ return curate(async () => {
68
+ const updated = await api.updateInitiativeItem(
69
+ workspace.workspaceId!,
70
+ initiativeId,
71
+ itemId,
72
+ input,
73
+ )
74
+ upsert(updated)
75
+ return updated
76
+ })
77
+ }
78
+
79
+ /** Replace the execution policy (concurrency + pipeline rules). */
80
+ async function updatePolicy(initiativeId: string, policy: InitiativeExecutionPolicy) {
81
+ return curate(async () => {
82
+ const updated = await api.updateInitiativePolicy(workspace.workspaceId!, initiativeId, policy)
83
+ upsert(updated)
84
+ return updated
85
+ })
86
+ }
87
+
88
+ return { curating, promoteFollowUp, dismissFollowUp, updateItem, updatePolicy }
89
+ }
@@ -0,0 +1,121 @@
1
+ import type { Ref } from 'vue'
2
+ import { ref } from 'vue'
3
+ import type { Initiative } from '~/types/domain'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+
6
+ /**
7
+ * Shared reactive state + injected dependencies the initiatives-store action factories close
8
+ * over. Created once in the `initiatives` store setup and threaded into
9
+ * {@link createInitiativePlanningActions} / {@link createInitiativeCurationActions} so the split
10
+ * operations stay behaviourally identical to the original single-closure store — a size-only
11
+ * extraction mirroring `stores/board/` and `stores/pipelines/`, not a new seam.
12
+ */
13
+ export interface InitiativeActionContext {
14
+ api: ReturnType<typeof useApi>
15
+ workspace: ReturnType<typeof useWorkspaceStore>
16
+ /** Patch an entity a call returned into the by-block cache (newest rev wins). */
17
+ upsert: (initiative: Initiative) => void
18
+ }
19
+
20
+ /**
21
+ * The planning-window actions: answering / dismissing the interviewer's questions, asking it to
22
+ * recommend an answer, and the two loop-resuming controls (continue / proceed). Owns the
23
+ * in-flight flags the window renders its spinners from.
24
+ */
25
+ export function createInitiativePlanningActions(ctx: InitiativeActionContext) {
26
+ const { api, workspace, upsert } = ctx
27
+
28
+ /** True while a planning-window action (continue/proceed) is resuming the run. */
29
+ const resuming = ref(false)
30
+ /** Question ids the interviewer is currently drafting a recommendation for (window spinner). */
31
+ const recommending: Ref<Set<string>> = ref(new Set())
32
+
33
+ /** Record the human's answer to one pending interview question (no run resume). */
34
+ async function answerQuestion(blockId: string, questionId: string, answer: string) {
35
+ if (!workspace.workspaceId) throw new Error('No active workspace')
36
+ const updated = await api.answerInitiativeQuestion(
37
+ workspace.workspaceId,
38
+ blockId,
39
+ questionId,
40
+ answer,
41
+ )
42
+ upsert(updated)
43
+ return updated
44
+ }
45
+
46
+ /** Mark a planning question not-relevant (`dismissed`) or reopen it (no run resume). */
47
+ async function setQuestionStatus(
48
+ blockId: string,
49
+ questionId: string,
50
+ status: 'open' | 'dismissed',
51
+ ) {
52
+ if (!workspace.workspaceId) throw new Error('No active workspace')
53
+ const updated = await api.setInitiativeQuestionStatus(
54
+ workspace.workspaceId,
55
+ blockId,
56
+ questionId,
57
+ status,
58
+ )
59
+ upsert(updated)
60
+ return updated
61
+ }
62
+
63
+ /**
64
+ * Ask the interviewer to recommend a suggested answer for one pending question. Runs the
65
+ * interviewer LLM inline server-side; the returned entity carries the suggestion on the question.
66
+ * Tracks the in-flight id so the window can show a per-question spinner.
67
+ */
68
+ async function recommendAnswer(blockId: string, questionId: string) {
69
+ if (!workspace.workspaceId) throw new Error('No active workspace')
70
+ recommending.value = new Set(recommending.value).add(questionId)
71
+ try {
72
+ const updated = await api.recommendInitiativeAnswer(
73
+ workspace.workspaceId,
74
+ blockId,
75
+ questionId,
76
+ )
77
+ upsert(updated)
78
+ return updated
79
+ } finally {
80
+ const next = new Set(recommending.value)
81
+ next.delete(questionId)
82
+ recommending.value = next
83
+ }
84
+ }
85
+
86
+ /** Submit the answers and resume the interview (the interviewer re-runs, may ask more). */
87
+ async function continuePlanning(blockId: string) {
88
+ if (!workspace.workspaceId) throw new Error('No active workspace')
89
+ resuming.value = true
90
+ try {
91
+ const updated = await api.continueInitiativePlanning(workspace.workspaceId, blockId)
92
+ upsert(updated)
93
+ return updated
94
+ } finally {
95
+ resuming.value = false
96
+ }
97
+ }
98
+
99
+ /** Skip remaining questions: the interviewer converges and the run advances. */
100
+ async function proceedPlanning(blockId: string) {
101
+ if (!workspace.workspaceId) throw new Error('No active workspace')
102
+ resuming.value = true
103
+ try {
104
+ const updated = await api.proceedInitiativePlanning(workspace.workspaceId, blockId)
105
+ upsert(updated)
106
+ return updated
107
+ } finally {
108
+ resuming.value = false
109
+ }
110
+ }
111
+
112
+ return {
113
+ resuming,
114
+ recommending,
115
+ answerQuestion,
116
+ setQuestionStatus,
117
+ recommendAnswer,
118
+ continuePlanning,
119
+ proceedPlanning,
120
+ }
121
+ }
@@ -1,16 +1,14 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { computed, ref } from 'vue'
3
- import type {
4
- Initiative,
5
- InitiativeExecutionPolicy,
6
- InitiativePresetDescriptor,
7
- InitiativePresetInputs,
8
- PromoteInitiativeFollowUpInput,
9
- UpdateInitiativeItemInput,
10
- } from '~/types/domain'
3
+ import type { Initiative, InitiativePresetDescriptor, InitiativePresetInputs } from '~/types/domain'
11
4
 
12
5
  import { useWorkspaceStore } from '~/stores/workspace'
13
6
  import { useBoardStore } from '~/stores/board'
7
+ import {
8
+ createInitiativePlanningActions,
9
+ type InitiativeActionContext,
10
+ } from '~/stores/initiative/planning'
11
+ import { createInitiativeCurationActions } from '~/stores/initiative/curation'
14
12
 
15
13
  /** The built-in generic preset id (mirrors kernel's `GENERIC_INITIATIVE_PRESET_ID`). */
16
14
  export const GENERIC_PRESET_ID = 'preset_generic'
@@ -165,91 +163,6 @@ export const useInitiativesStore = defineStore('initiatives', () => {
165
163
  }
166
164
  }
167
165
 
168
- /** True while a planning-window action (continue/proceed) is resuming the run. */
169
- const resuming = ref(false)
170
-
171
- /** Record the human's answer to one pending interview question (no run resume). */
172
- async function answerQuestion(blockId: string, questionId: string, answer: string) {
173
- if (!workspace.workspaceId) throw new Error('No active workspace')
174
- const updated = await api.answerInitiativeQuestion(
175
- workspace.workspaceId,
176
- blockId,
177
- questionId,
178
- answer,
179
- )
180
- upsert(updated)
181
- return updated
182
- }
183
-
184
- /** Question ids the interviewer is currently drafting a recommendation for (window spinner). */
185
- const recommending = ref<Set<string>>(new Set())
186
-
187
- /** Mark a planning question not-relevant (`dismissed`) or reopen it (no run resume). */
188
- async function setQuestionStatus(
189
- blockId: string,
190
- questionId: string,
191
- status: 'open' | 'dismissed',
192
- ) {
193
- if (!workspace.workspaceId) throw new Error('No active workspace')
194
- const updated = await api.setInitiativeQuestionStatus(
195
- workspace.workspaceId,
196
- blockId,
197
- questionId,
198
- status,
199
- )
200
- upsert(updated)
201
- return updated
202
- }
203
-
204
- /**
205
- * Ask the interviewer to recommend a suggested answer for one pending question. Runs the
206
- * interviewer LLM inline server-side; the returned entity carries the suggestion on the question.
207
- * Tracks the in-flight id so the window can show a per-question spinner.
208
- */
209
- async function recommendAnswer(blockId: string, questionId: string) {
210
- if (!workspace.workspaceId) throw new Error('No active workspace')
211
- recommending.value = new Set(recommending.value).add(questionId)
212
- try {
213
- const updated = await api.recommendInitiativeAnswer(
214
- workspace.workspaceId,
215
- blockId,
216
- questionId,
217
- )
218
- upsert(updated)
219
- return updated
220
- } finally {
221
- const next = new Set(recommending.value)
222
- next.delete(questionId)
223
- recommending.value = next
224
- }
225
- }
226
-
227
- /** Submit the answers and resume the interview (the interviewer re-runs, may ask more). */
228
- async function continuePlanning(blockId: string) {
229
- if (!workspace.workspaceId) throw new Error('No active workspace')
230
- resuming.value = true
231
- try {
232
- const updated = await api.continueInitiativePlanning(workspace.workspaceId, blockId)
233
- upsert(updated)
234
- return updated
235
- } finally {
236
- resuming.value = false
237
- }
238
- }
239
-
240
- /** Skip remaining questions: the interviewer converges and the run advances. */
241
- async function proceedPlanning(blockId: string) {
242
- if (!workspace.workspaceId) throw new Error('No active workspace')
243
- resuming.value = true
244
- try {
245
- const updated = await api.proceedInitiativePlanning(workspace.workspaceId, blockId)
246
- upsert(updated)
247
- return updated
248
- } finally {
249
- resuming.value = false
250
- }
251
- }
252
-
253
166
  /** True while a loop control (pause/resume/cancel) is in flight. */
254
167
  const controlling = ref(false)
255
168
 
@@ -272,76 +185,12 @@ export const useInitiativesStore = defineStore('initiatives', () => {
272
185
  }
273
186
  }
274
187
 
275
- /** True while a curation action (promote/dismiss/edit item/edit policy) is in flight. */
276
- const curating = ref(false)
277
-
278
- async function curate<T>(fn: () => Promise<T>): Promise<T> {
279
- if (!workspace.workspaceId) throw new Error('No active workspace')
280
- curating.value = true
281
- try {
282
- return await fn()
283
- } finally {
284
- curating.value = false
285
- }
286
- }
287
-
288
- /** Promote an `open` harvested follow-up into a new pending tracker item. */
289
- async function promoteFollowUp(
290
- initiativeId: string,
291
- followUpId: string,
292
- input: PromoteInitiativeFollowUpInput,
293
- ) {
294
- return curate(async () => {
295
- const updated = await api.promoteInitiativeFollowUp(
296
- workspace.workspaceId!,
297
- initiativeId,
298
- followUpId,
299
- input,
300
- )
301
- upsert(updated)
302
- return updated
303
- })
304
- }
305
-
306
- /** Dismiss a harvested follow-up. */
307
- async function dismissFollowUp(initiativeId: string, followUpId: string) {
308
- return curate(async () => {
309
- const updated = await api.dismissInitiativeFollowUp(
310
- workspace.workspaceId!,
311
- initiativeId,
312
- followUpId,
313
- )
314
- upsert(updated)
315
- return updated
316
- })
317
- }
318
-
319
- /** Edit one tracker item and/or drive its status (retry a blocked item / skip it). */
320
- async function updateItem(
321
- initiativeId: string,
322
- itemId: string,
323
- input: UpdateInitiativeItemInput,
324
- ) {
325
- return curate(async () => {
326
- const updated = await api.updateInitiativeItem(
327
- workspace.workspaceId!,
328
- initiativeId,
329
- itemId,
330
- input,
331
- )
332
- upsert(updated)
333
- return updated
334
- })
335
- }
336
-
337
- /** Replace the execution policy (concurrency + pipeline rules). */
338
- async function updatePolicy(initiativeId: string, policy: InitiativeExecutionPolicy) {
339
- return curate(async () => {
340
- const updated = await api.updateInitiativePolicy(workspace.workspaceId!, initiativeId, policy)
341
- upsert(updated)
342
- return updated
343
- })
344
- }
188
+ // The planning-window + tracker-curation actions, split into cohesive factories sharing the
189
+ // state above (a size-only extraction mirroring `stores/board/` — behaviour is identical to the
190
+ // former in-closure functions). Each owns the in-flight flags its window renders.
191
+ const context: InitiativeActionContext = { api, workspace, upsert }
192
+ const planning = createInitiativePlanningActions(context)
193
+ const curation = createInitiativeCurationActions(context)
345
194
 
346
195
  function reset() {
347
196
  byBlock.value = {}
@@ -353,10 +202,7 @@ export const useInitiativesStore = defineStore('initiatives', () => {
353
202
  presets,
354
203
  all,
355
204
  creating,
356
- resuming,
357
205
  controlling,
358
- curating,
359
- recommending,
360
206
  forBlock,
361
207
  presetById,
362
208
  planningPipelineIdFor,
@@ -366,16 +212,9 @@ export const useInitiativesStore = defineStore('initiatives', () => {
366
212
  create,
367
213
  probePreset,
368
214
  load,
369
- answerQuestion,
370
- setQuestionStatus,
371
- recommendAnswer,
372
- continuePlanning,
373
- proceedPlanning,
374
215
  control,
375
- promoteFollowUp,
376
- dismissFollowUp,
377
- updateItem,
378
- updatePolicy,
216
+ ...planning,
217
+ ...curation,
379
218
  reset,
380
219
  }
381
220
  })