@cat-factory/app 0.81.0 → 0.82.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,78 @@
1
+ <script setup lang="ts">
2
+ // Inspector body for an `initiative`-level block: the entity's status + goal, the
3
+ // "Run planning" control (pinned to the Initiative Planning pipeline — the engine
4
+ // refuses any other on this block), and the tracker window opener. Read-only in
5
+ // this slice; plan/policy editing lands with the execution loop.
6
+ import type { Block, InitiativeStatus } from '~/types/domain'
7
+ import { INITIATIVE_STATUS_LABEL_KEYS, initiativeProgress } from '~/utils/initiative'
8
+
9
+ const props = defineProps<{ block: Block }>()
10
+
11
+ const initiatives = useInitiativesStore()
12
+ const pipelines = usePipelinesStore()
13
+ const execution = useExecutionStore()
14
+ const ui = useUiStore()
15
+ const { t } = useI18n()
16
+
17
+ const initiative = computed(() => initiatives.forBlock(props.block.id))
18
+
19
+ const status = computed<InitiativeStatus>(() => initiative.value?.status ?? 'planning')
20
+
21
+ // The ONLY pipeline runnable on an initiative block (see the engine's runnable guard).
22
+ const planningPipeline = computed(() => pipelines.pipelines.find((p) => p.id === 'pl_initiative'))
23
+ const running = computed(() => !!props.block.executionId)
24
+
25
+ function runPlanning() {
26
+ if (planningPipeline.value) void execution.start(props.block.id, planningPipeline.value)
27
+ }
28
+ function openTracker() {
29
+ ui.openInitiativeTracker(props.block.id)
30
+ }
31
+
32
+ const progress = computed(() => initiativeProgress(initiative.value?.items))
33
+ </script>
34
+
35
+ <template>
36
+ <div class="space-y-3" data-testid="initiative-inspector">
37
+ <div class="flex items-center gap-2">
38
+ <UBadge color="primary" variant="subtle" size="sm">
39
+ {{ t(INITIATIVE_STATUS_LABEL_KEYS[status]) }}
40
+ </UBadge>
41
+ <span v-if="progress" class="text-[11px] text-slate-400">
42
+ {{ t('initiative.card.progress', { done: progress.settled, total: progress.total }) }}
43
+ </span>
44
+ </div>
45
+
46
+ <p v-if="initiative?.goal" class="whitespace-pre-wrap text-[12px] text-slate-300">
47
+ {{ initiative.goal }}
48
+ </p>
49
+
50
+ <div class="flex flex-wrap items-center gap-2">
51
+ <UButton
52
+ data-testid="initiative-run-planning"
53
+ color="primary"
54
+ variant="soft"
55
+ size="sm"
56
+ icon="i-lucide-play"
57
+ :disabled="!planningPipeline || running"
58
+ @click="runPlanning"
59
+ >
60
+ {{ t('initiative.inspector.runPlanning') }}
61
+ </UButton>
62
+ <UButton
63
+ data-testid="initiative-inspector-tracker"
64
+ color="neutral"
65
+ variant="soft"
66
+ size="sm"
67
+ icon="i-lucide-list-checks"
68
+ @click="openTracker"
69
+ >
70
+ {{ t('initiative.card.openTracker') }}
71
+ </UButton>
72
+ </div>
73
+
74
+ <p class="text-[11px] text-slate-500">
75
+ {{ t('initiative.inspector.hint') }}
76
+ </p>
77
+ </div>
78
+ </template>
@@ -0,0 +1,28 @@
1
+ import {
2
+ createInitiativeContract,
3
+ getInitiativeByBlockContract,
4
+ getInitiativeContract,
5
+ listInitiativesContract,
6
+ } from '@cat-factory/contracts'
7
+ import type { ApiContext } from './context'
8
+
9
+ /** Initiatives: the long-running multi-task work containers (create + tracker reads). */
10
+ export function initiativeApi({ send, ws }: ApiContext) {
11
+ return {
12
+ // Create the initiative-level board block AND its empty entity in one call.
13
+ createInitiative: (
14
+ workspaceId: string,
15
+ body: { frameId: string; title: string; description?: string },
16
+ ) => send(createInitiativeContract, { pathPrefix: ws(workspaceId), body }),
17
+
18
+ listInitiatives: (workspaceId: string) =>
19
+ send(listInitiativesContract, { pathPrefix: ws(workspaceId) }),
20
+
21
+ getInitiative: (workspaceId: string, initiativeId: string) =>
22
+ send(getInitiativeContract, { pathPrefix: ws(workspaceId), pathParams: { initiativeId } }),
23
+
24
+ // The tracker window's load path: the initiative anchored to a board block.
25
+ getInitiativeByBlock: (workspaceId: string, blockId: string) =>
26
+ send(getInitiativeByBlockContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
27
+ }
28
+ }
@@ -13,6 +13,7 @@ import { githubApi } from './api/github'
13
13
  import { humanReviewApi } from './api/humanReview'
14
14
  import { humanTestApi } from './api/humanTest'
15
15
  import { infraHandlersApi } from './api/infraHandlers'
16
+ import { initiativeApi } from './api/initiative'
16
17
  import { visualConfirmApi } from './api/visualConfirm'
17
18
  import { kaizenApi } from './api/kaizen'
18
19
  import { localSettingsApi } from './api/localSettings'
@@ -112,6 +113,7 @@ export function useApi() {
112
113
  ...presetsApi(ctx),
113
114
  ...providerConnectionsApi(ctx),
114
115
  ...infraHandlersApi(ctx),
116
+ ...initiativeApi(ctx),
115
117
  ...provisioningLogsApi(ctx),
116
118
  ...releaseHealthApi(ctx),
117
119
  ...packageRegistriesApi(ctx),
@@ -59,6 +59,11 @@ export function useBlockQueries(blocks: Ref<Block[]>) {
59
59
  return childrenOf(serviceId).filter((b) => b.level === 'module')
60
60
  }
61
61
 
62
+ /** Initiative containers inside a service (frame children, like modules). */
63
+ function initiativesOf(serviceId: string) {
64
+ return childrenOf(serviceId).filter((b) => b.level === 'initiative')
65
+ }
66
+
62
67
  /** Tasks anywhere under a container — directly, or nested inside its modules. */
63
68
  function allTasksUnder(containerId: string): Block[] {
64
69
  const direct = tasksOf(containerId)
@@ -124,9 +129,17 @@ export function useBlockQueries(blocks: Ref<Block[]>) {
124
129
  */
125
130
  function frameStatus(frameId: string): BlockStatus {
126
131
  const tasks = allTasksUnder(frameId)
127
- if (tasks.length === 0) return 'planned'
128
- if (tasks.some((t) => t.status === 'blocked')) return 'blocked'
129
- if (tasks.some((t) => t.status === 'in_progress' || t.status === 'pr_ready'))
132
+ // Initiative containers are frame children too: a frame holding only an initiative
133
+ // is NOT empty, and an active (planning/executing block `in_progress`) or blocked
134
+ // initiative drives the frame's activity dot just like a task does.
135
+ const inits = initiativesOf(frameId)
136
+ if (tasks.length === 0 && inits.length === 0) return 'planned'
137
+ if (tasks.some((t) => t.status === 'blocked') || inits.some((i) => i.status === 'blocked'))
138
+ return 'blocked'
139
+ if (
140
+ tasks.some((t) => t.status === 'in_progress' || t.status === 'pr_ready') ||
141
+ inits.some((i) => i.status === 'in_progress')
142
+ )
130
143
  return 'in_progress'
131
144
  return 'ready'
132
145
  }
@@ -153,6 +166,11 @@ export function useBlockQueries(blocks: Ref<Block[]>) {
153
166
  w = Math.max(w, m.position.x + s.w + 12)
154
167
  inner = Math.max(inner, m.position.y + s.h + 12)
155
168
  }
169
+ // Initiative cards render inside the frame's drop zone like tasks (230×~170).
170
+ for (const i of initiativesOf(id)) {
171
+ w = Math.max(w, i.position.x + 230 + 12)
172
+ inner = Math.max(inner, i.position.y + 170 + 12)
173
+ }
156
174
  return { w, h: inner + headerH }
157
175
  }
158
176
 
@@ -180,6 +198,7 @@ export function useBlockQueries(blocks: Ref<Block[]>) {
180
198
  childrenOf,
181
199
  tasksOf,
182
200
  modulesOf,
201
+ initiativesOf,
183
202
  allTasksUnder,
184
203
  serviceOf,
185
204
  unmetDeps,
@@ -25,6 +25,7 @@ export function useWorkspaceStream() {
25
25
  const clarity = useClarityStore()
26
26
  const brainstorm = useBrainstormStore()
27
27
  const kaizen = useKaizenStore()
28
+ const initiatives = useInitiativesStore()
28
29
  const api = useApi()
29
30
  const apiBase = useRuntimeConfig().public.apiBase
30
31
 
@@ -103,6 +104,10 @@ export function useWorkspaceStream() {
103
104
  // run cache (so an open run window shows scheduled→running→complete live) and the
104
105
  // Kaizen screen history. Never surfaced on the board.
105
106
  kaizen.upsert(event.grading)
107
+ } else if (event.type === 'initiative') {
108
+ // An initiative changed (created, plan ingested, an item settled) — patch the cache
109
+ // so an open tracker window / the board card reflects the transition live.
110
+ initiatives.upsert(event.initiative)
106
111
  }
107
112
  }
108
113
 
@@ -332,6 +332,7 @@ watch(
332
332
  <AgentStepDetail />
333
333
  <StepResultViewHost />
334
334
  <AddTaskModal />
335
+ <CreateInitiativeModal />
335
336
  <CommandBar />
336
337
  <PersonalCredentialModal />
337
338
  <ConfirmDialog />
@@ -22,8 +22,11 @@ import { useBlockQueries } from '~/composables/useBlockQueries'
22
22
  interface RemovalSnapshot {
23
23
  /** The removed block + all its descendants, in their original order. */
24
24
  removed: Block[]
25
- /** Survivors whose `dependsOn`/`epicId` lost an edge to a removed block (originals to restore). */
26
- edges: { id: string; dependsOn: string[]; epicId: string | null }[]
25
+ /**
26
+ * Survivors whose `dependsOn`/`epicId`/`initiativeId` lost an edge to a removed block
27
+ * (originals to restore on rollback).
28
+ */
29
+ edges: { id: string; dependsOn: string[]; epicId: string | null; initiativeId: string | null }[]
27
30
  }
28
31
 
29
32
  export const useBoardStore = defineStore('board', () => {
@@ -255,15 +258,23 @@ export const useBoardStore = defineStore('board', () => {
255
258
  }
256
259
  }
257
260
  const removed = blocks.value.filter((b) => doomed.has(b.id))
258
- // Survivors that pointed at a doomed block (dependency edge or epic membership) lose
259
- // that link — snapshot the originals so a failed delete restores them faithfully.
261
+ // Survivors that pointed at a doomed block (dependency edge, epic membership, or initiative
262
+ // membership) lose that link — snapshot the originals so a failed delete restores them
263
+ // faithfully. Mirrors the backend `pruneDanglingEdges` detach.
260
264
  const edges = blocks.value
261
265
  .filter(
262
266
  (b) =>
263
267
  !doomed.has(b.id) &&
264
- (b.dependsOn.some((d) => doomed.has(d)) || (b.epicId != null && doomed.has(b.epicId))),
268
+ (b.dependsOn.some((d) => doomed.has(d)) ||
269
+ (b.epicId != null && doomed.has(b.epicId)) ||
270
+ (b.initiativeId != null && doomed.has(b.initiativeId))),
265
271
  )
266
- .map((b) => ({ id: b.id, dependsOn: [...b.dependsOn], epicId: b.epicId ?? null }))
272
+ .map((b) => ({
273
+ id: b.id,
274
+ dependsOn: [...b.dependsOn],
275
+ epicId: b.epicId ?? null,
276
+ initiativeId: b.initiativeId ?? null,
277
+ }))
267
278
  blocks.value = blocks.value.filter((b) => !doomed.has(b.id))
268
279
  for (const b of blocks.value) {
269
280
  if (b.dependsOn.some((d) => doomed.has(d))) {
@@ -271,6 +282,8 @@ export const useBoardStore = defineStore('board', () => {
271
282
  }
272
283
  // A member of a deleted epic loses its membership (the task itself survives).
273
284
  if (b.epicId != null && doomed.has(b.epicId)) b.epicId = null
285
+ // Likewise a task spawned by a deleted initiative loses its (non-structural) membership.
286
+ if (b.initiativeId != null && doomed.has(b.initiativeId)) b.initiativeId = null
274
287
  }
275
288
  return { removed, edges }
276
289
  }
@@ -283,6 +296,7 @@ export const useBoardStore = defineStore('board', () => {
283
296
  if (b) {
284
297
  b.dependsOn = e.dependsOn
285
298
  b.epicId = e.epicId
299
+ b.initiativeId = e.initiativeId
286
300
  }
287
301
  }
288
302
  }
@@ -0,0 +1,98 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type { Initiative } from '~/types/domain'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+ import { useBoardStore } from '~/stores/board'
6
+
7
+ /**
8
+ * Initiative state — the long-running multi-task work containers, keyed by their
9
+ * anchor BLOCK id (the id everything on the board navigates by). Hydrated from the
10
+ * workspace snapshot (`snapshot.initiatives`) and patched live from `initiative`
11
+ * stream events; `create` calls the API and applies the authoritative entity +
12
+ * block the server returns. `available` mirrors the backend's opt-in module (a 503
13
+ * hides the UI). Per-workspace; nothing is persisted client-side.
14
+ *
15
+ * NOTE: distinct from `useTrackerStore` (the workspace's ISSUE-tracker selection) —
16
+ * "tracker" in initiative-land means the initiative's plan/tracker document.
17
+ */
18
+ export const useInitiativesStore = defineStore('initiatives', () => {
19
+ const api = useApi()
20
+ const workspace = useWorkspaceStore()
21
+
22
+ /** null = unknown (not probed), true/false = feature on/off. */
23
+ const available = ref<boolean | null>(null)
24
+ /** The entities keyed by their anchor block id. */
25
+ const byBlock = ref<Record<string, Initiative>>({})
26
+ /** True while a create call is in flight (the modal's submit spinner). */
27
+ const creating = ref(false)
28
+
29
+ const all = computed(() => Object.values(byBlock.value))
30
+
31
+ function forBlock(blockId: string): Initiative | null {
32
+ return byBlock.value[blockId] ?? null
33
+ }
34
+
35
+ /**
36
+ * Rebuild the cache from a snapshot (the hydrate fan-out). The snapshot is authoritative
37
+ * for EXISTENCE (entities it omits are dropped — they were deleted), but NOT for freshness:
38
+ * a stale snapshot captured before a live `initiative` event must not regress a newer entity
39
+ * already patched into the store. So for a blockId present in both, keep whichever `rev` is
40
+ * higher — the same live-event-vs-resync race guard `upsert` applies, mirroring the fix the
41
+ * repo's flake note describes for `agentRuns.hydrate`.
42
+ */
43
+ function hydrate(next: Initiative[] | undefined) {
44
+ if (next === undefined) return
45
+ available.value = true
46
+ const map: Record<string, Initiative> = {}
47
+ for (const initiative of next) {
48
+ const existing = byBlock.value[initiative.blockId]
49
+ map[initiative.blockId] = existing && existing.rev > initiative.rev ? existing : initiative
50
+ }
51
+ byBlock.value = map
52
+ }
53
+
54
+ /** Patch from a live `initiative` stream event or a call response (newest rev wins). */
55
+ function upsert(initiative: Initiative) {
56
+ const existing = byBlock.value[initiative.blockId]
57
+ if (existing && existing.rev > initiative.rev) return
58
+ byBlock.value = { ...byBlock.value, [initiative.blockId]: initiative }
59
+ }
60
+
61
+ /** Create an initiative under a service frame (block + entity in one call). */
62
+ async function create(frameId: string, input: { title: string; description?: string }) {
63
+ if (!workspace.workspaceId) throw new Error('No active workspace')
64
+ creating.value = true
65
+ try {
66
+ const created = await api.createInitiative(workspace.workspaceId, {
67
+ frameId,
68
+ title: input.title,
69
+ ...(input.description ? { description: input.description } : {}),
70
+ })
71
+ useBoardStore().upsert(created.block)
72
+ upsert(created.initiative)
73
+ return created
74
+ } finally {
75
+ creating.value = false
76
+ }
77
+ }
78
+
79
+ /** Re-fetch one block's initiative (the tracker window's load path). */
80
+ async function load(blockId: string) {
81
+ if (!workspace.workspaceId) return
82
+ try {
83
+ const initiative = await api.getInitiativeByBlock(workspace.workspaceId, blockId)
84
+ available.value = true
85
+ if (initiative) upsert(initiative)
86
+ } catch (error) {
87
+ const status = (error as { status?: number } | null)?.status
88
+ if (status === 503) available.value = false
89
+ else throw error
90
+ }
91
+ }
92
+
93
+ function reset() {
94
+ byBlock.value = {}
95
+ }
96
+
97
+ return { available, byBlock, all, creating, forBlock, hydrate, upsert, create, load, reset }
98
+ })
package/app/stores/ui.ts CHANGED
@@ -86,6 +86,10 @@ export const useUiStore = defineStore('ui', () => {
86
86
  // the frame opens it, scoped to that frame).
87
87
  const addRecurringFrameId = ref<string | null>(null)
88
88
 
89
+ // Create-initiative modal: the service frame a new initiative is being created
90
+ // under, or null when closed (mirrors the add-task flow).
91
+ const createInitiativeFrameId = ref<string | null>(null)
92
+
89
93
  // Repo-bootstrap modal (manage reference architectures + launch a bootstrap).
90
94
  const bootstrapOpen = ref(false)
91
95
 
@@ -424,6 +428,12 @@ export const useUiStore = defineStore('ui', () => {
424
428
  function closeAddRecurring() {
425
429
  addRecurringFrameId.value = null
426
430
  }
431
+ function openCreateInitiative(frameId: string) {
432
+ createInitiativeFrameId.value = frameId
433
+ }
434
+ function closeCreateInitiative() {
435
+ createInitiativeFrameId.value = null
436
+ }
427
437
  function openBootstrap() {
428
438
  bootstrapOpen.value = true
429
439
  }
@@ -714,6 +724,10 @@ export const useUiStore = defineStore('ui', () => {
714
724
  function openServiceSpec(blockId: string) {
715
725
  resultView.value = { view: 'service-spec', blockId, instanceId: null, stepIndex: null }
716
726
  }
727
+ // Open the initiative tracker window for an initiative block (board card / inspector).
728
+ function openInitiativeTracker(blockId: string) {
729
+ resultView.value = { view: 'initiative-tracker', blockId, instanceId: null, stepIndex: null }
730
+ }
717
731
  // Open the Follow-up companion window for a run's Coder step (the blinking chip + the
718
732
  // `followup_pending` notification). Resolves the Coder step index from the run when not
719
733
  // given, so callers that only know the run can still open it.
@@ -783,6 +797,7 @@ export const useUiStore = defineStore('ui', () => {
783
797
  addTaskContainerId,
784
798
  addTaskPrefill,
785
799
  addRecurringFrameId,
800
+ createInitiativeFrameId,
786
801
  bootstrapOpen,
787
802
  addServiceOpen,
788
803
  githubOpen,
@@ -856,6 +871,8 @@ export const useUiStore = defineStore('ui', () => {
856
871
  closeAddTask,
857
872
  openAddRecurring,
858
873
  closeAddRecurring,
874
+ openCreateInitiative,
875
+ closeCreateInitiative,
859
876
  openBootstrap,
860
877
  closeBootstrap,
861
878
  openAddService,
@@ -921,6 +938,7 @@ export const useUiStore = defineStore('ui', () => {
921
938
  openClarityReview,
922
939
  openBrainstorm,
923
940
  openServiceSpec,
941
+ openInitiativeTracker,
924
942
  openFollowUps,
925
943
  closeRequirementReview,
926
944
  openStepDetail,
@@ -13,6 +13,7 @@ import { useAgentConfigStore } from '~/stores/agentConfig'
13
13
  import { useModelPresetsStore } from '~/stores/modelPresets'
14
14
  import { useServiceFragmentDefaultsStore } from '~/stores/serviceFragmentDefaults'
15
15
  import { useRecurringPipelinesStore } from '~/stores/recurringPipelines'
16
+ import { useInitiativesStore } from '~/stores/initiative'
16
17
  import { useServicesStore } from '~/stores/services'
17
18
  import { useAgentsStore } from '~/stores/agents'
18
19
  import { useTrackerStore } from '~/stores/tracker'
@@ -82,6 +83,7 @@ export const useWorkspaceStore = defineStore(
82
83
  useBrainstormStore().reset()
83
84
  useConsensusStore().reset()
84
85
  useGitHubStore().reset()
86
+ useInitiativesStore().reset()
85
87
  // The fragment picker catalog is per-board (the merged tenant catalog), so drop
86
88
  // it too — the next inspector open re-fetches it for the switched-to board rather
87
89
  // than showing the previous board's (or a raw-id placeholder for) fragments.
@@ -109,6 +111,7 @@ export const useWorkspaceStore = defineStore(
109
111
  useModelPresetsStore().hydrate(snapshot.modelPresets ?? [])
110
112
  useServiceFragmentDefaultsStore().hydrate(snapshot.serviceFragmentDefaults?.fragmentIds)
111
113
  useRecurringPipelinesStore().hydrate(snapshot.recurringPipelines ?? [])
114
+ useInitiativesStore().hydrate(snapshot.initiatives)
112
115
  useTrackerStore().hydrate(snapshot.trackerSettings)
113
116
  useServicesStore().hydrate(snapshot.mounts ?? [], snapshot.serviceCatalog ?? [])
114
117
  // Merge the deployment's registered custom agent kinds into the palette catalog so a
@@ -134,3 +134,4 @@ export type * from './merge'
134
134
  export type * from './services'
135
135
  export type * from './recurring'
136
136
  export type * from './tracker'
137
+ export type * from './initiative'
@@ -0,0 +1,18 @@
1
+ // Initiative wire shapes, re-exported from the shared contracts package (the single
2
+ // source of truth across the wire boundary). The SPA imports these through
3
+ // `~/types/domain` like every other domain type.
4
+ export type {
5
+ CreateInitiativeInput,
6
+ Initiative,
7
+ InitiativeDecision,
8
+ InitiativeDeviation,
9
+ InitiativeEstimate,
10
+ InitiativeExecutionPolicy,
11
+ InitiativeFollowUp,
12
+ InitiativeItem,
13
+ InitiativeItemStatus,
14
+ InitiativePhase,
15
+ InitiativePipelineRule,
16
+ InitiativeQa,
17
+ InitiativeStatus,
18
+ } from '@cat-factory/contracts'
@@ -344,6 +344,29 @@ export const SYSTEM_AGENT_META: Record<string, AgentArchetype> = {
344
344
  color: '#22d3ee',
345
345
  description: 'Maps the repository into the service → modules blueprint.',
346
346
  },
347
+ // The Initiative Planning pipeline's two steps. Only runnable on an initiative
348
+ // block (pl_initiative — enforced by the engine), so they are display-metadata
349
+ // system kinds, never palette archetypes.
350
+ 'initiative-planner': {
351
+ kind: 'initiative-planner',
352
+ label: 'Initiative Planner',
353
+ icon: 'i-lucide-milestone',
354
+ color: '#818cf8',
355
+ description:
356
+ "Explores the codebase and drafts the initiative's multi-phase plan (items, estimates, concurrency + pipeline policy) for approval.",
357
+ // Opens the dedicated tracker window (phases / items / policy) instead of the
358
+ // generic prose step-detail panel.
359
+ resultView: 'initiative-tracker',
360
+ },
361
+ 'initiative-committer': {
362
+ kind: 'initiative-committer',
363
+ label: 'Initiative Committer',
364
+ icon: 'i-lucide-git-commit-horizontal',
365
+ color: '#818cf8',
366
+ description:
367
+ 'Persists the approved plan and commits the in-repo tracker (docs/initiatives/<slug>/), arming the execution loop. Runs no model.',
368
+ resultView: 'initiative-tracker',
369
+ },
347
370
  // A read-only repository audit that emits a prioritized findings report. Not a palette
348
371
  // archetype (it is only seeded into the recurring tech-debt pipeline), so it lives here
349
372
  // for run-timeline / saved-pipeline display rather than in AGENT_ARCHETYPES.
@@ -479,6 +502,7 @@ export const MODEL_CONFIGURABLE_SYSTEM_KINDS: AgentArchetype[] = [
479
502
  ...[
480
503
  'spec-writer',
481
504
  'blueprints',
505
+ 'initiative-planner',
482
506
  'conflict-resolver',
483
507
  'ci-fixer',
484
508
  'fixer',
@@ -0,0 +1,57 @@
1
+ import type { InitiativeItem, InitiativeItemStatus, InitiativeStatus } from '~/types/domain'
2
+
3
+ // Shared initiative presentation vocabulary, so the board card, the inspector body and
4
+ // the tracker window render statuses/progress from ONE source. The exhaustive
5
+ // `Record<Enum, string>` maps keep the tier-2 typecheck guard live (a new status without
6
+ // a label/chip fails the build) without triplicating it across the components.
7
+
8
+ /** Initiative lifecycle status → i18n label key. */
9
+ export const INITIATIVE_STATUS_LABEL_KEYS: Record<InitiativeStatus, string> = {
10
+ planning: 'initiative.status.planning',
11
+ awaiting_approval: 'initiative.status.awaiting_approval',
12
+ executing: 'initiative.status.executing',
13
+ paused: 'initiative.status.paused',
14
+ done: 'initiative.status.done',
15
+ cancelled: 'initiative.status.cancelled',
16
+ }
17
+
18
+ /** Initiative lifecycle status → Nuxt UI badge colour. */
19
+ export const INITIATIVE_STATUS_CHIPS: Record<InitiativeStatus, string> = {
20
+ planning: 'neutral',
21
+ awaiting_approval: 'warning',
22
+ executing: 'info',
23
+ paused: 'neutral',
24
+ done: 'success',
25
+ cancelled: 'neutral',
26
+ }
27
+
28
+ /** Tracker item status → i18n label key. */
29
+ export const INITIATIVE_ITEM_STATUS_LABEL_KEYS: Record<InitiativeItemStatus, string> = {
30
+ pending: 'initiative.itemStatus.pending',
31
+ in_progress: 'initiative.itemStatus.in_progress',
32
+ pr_open: 'initiative.itemStatus.pr_open',
33
+ done: 'initiative.itemStatus.done',
34
+ blocked: 'initiative.itemStatus.blocked',
35
+ skipped: 'initiative.itemStatus.skipped',
36
+ }
37
+
38
+ /** Tracker item status → Nuxt UI badge colour. */
39
+ export const INITIATIVE_ITEM_STATUS_CHIPS: Record<InitiativeItemStatus, string> = {
40
+ pending: 'neutral',
41
+ in_progress: 'info',
42
+ pr_open: 'warning',
43
+ done: 'success',
44
+ blocked: 'error',
45
+ skipped: 'neutral',
46
+ }
47
+
48
+ /** Item statuses that count as settled — mirrors the backend terminal-status set. */
49
+ const SETTLED: ReadonlySet<InitiativeItemStatus> = new Set(['done', 'skipped'])
50
+
51
+ /** Completion rollup across an initiative's items, or null when there are none. */
52
+ export function initiativeProgress(
53
+ items: InitiativeItem[] | undefined,
54
+ ): { settled: number; total: number } | null {
55
+ if (!items || items.length === 0) return null
56
+ return { settled: items.filter((i) => SETTLED.has(i.status)).length, total: items.length }
57
+ }
@@ -275,7 +275,8 @@
275
275
  "dragService": "Drag service",
276
276
  "dragTask": "Drag task",
277
277
  "dragToResize": "Drag to resize",
278
- "addFirstTask": "Add the first task"
278
+ "addFirstTask": "Add the first task",
279
+ "createInitiativeTitle": "Create initiative"
279
280
  },
280
281
  "decisionBadge": {
281
282
  "decisionNeeded": "Decision needed",
@@ -4071,5 +4072,70 @@
4071
4072
  "saveArchFailed": "Could not save reference architecture",
4072
4073
  "deleteFailed": "Could not delete"
4073
4074
  }
4075
+ },
4076
+ "initiative": {
4077
+ "create": {
4078
+ "title": "Create initiative",
4079
+ "inFrame": "New initiative in {frame}",
4080
+ "titleField": "Title",
4081
+ "titlePlaceholder": "e.g. Migrate the API to the new auth model",
4082
+ "goalField": "Goal",
4083
+ "goalPlaceholder": "Describe the goal, constraints and rough scope. The planner refines this into a multi-phase plan.",
4084
+ "hint": "Nothing runs yet: after creating, run the Initiative Planning pipeline on the block. It analyses the codebase and drafts the multi-phase plan for your approval.",
4085
+ "submit": "Create initiative",
4086
+ "failedTitle": "Could not create the initiative"
4087
+ },
4088
+ "status": {
4089
+ "planning": "Planning",
4090
+ "awaiting_approval": "Awaiting approval",
4091
+ "executing": "Executing",
4092
+ "paused": "Paused",
4093
+ "done": "Done",
4094
+ "cancelled": "Cancelled"
4095
+ },
4096
+ "itemStatus": {
4097
+ "pending": "Pending",
4098
+ "in_progress": "In progress",
4099
+ "pr_open": "PR open",
4100
+ "done": "Done",
4101
+ "blocked": "Blocked",
4102
+ "skipped": "Skipped"
4103
+ },
4104
+ "card": {
4105
+ "kind": "Initiative",
4106
+ "progress": "{done}/{total} items done",
4107
+ "openTracker": "Open tracker"
4108
+ },
4109
+ "tracker": {
4110
+ "title": "Initiative tracker",
4111
+ "subtitle": "Phases, work items, decisions and progress of this initiative",
4112
+ "empty": "No initiative found for this block.",
4113
+ "goal": "Goal",
4114
+ "constraints": "Constraints",
4115
+ "nonGoals": "Non-goals",
4116
+ "analysis": "Codebase analysis",
4117
+ "noPlan": "No plan yet. Run the Initiative Planning pipeline to draft the multi-phase plan.",
4118
+ "phase": "Phase: {title}",
4119
+ "colItem": "Item",
4120
+ "colStatus": "Status",
4121
+ "colPr": "PR",
4122
+ "dependsOn": "Depends on: {items}",
4123
+ "prLink": "PR",
4124
+ "policy": "Execution policy",
4125
+ "maxConcurrent": "Max concurrent tasks: {count}",
4126
+ "defaultPipeline": "Default pipeline:",
4127
+ "axisComplexity": "complexity >= {value}",
4128
+ "axisRisk": "risk >= {value}",
4129
+ "axisImpact": "impact >= {value}",
4130
+ "axisNever": "never matches (no thresholds)",
4131
+ "decisions": "Decisions",
4132
+ "deviations": "Deviations",
4133
+ "followUps": "Follow-ups",
4134
+ "caveats": "Known caveats"
4135
+ },
4136
+ "inspector": {
4137
+ "runPlanning": "Run planning",
4138
+ "hint": "The planning pipeline explores the codebase, drafts the multi-phase plan for approval, then commits the tracker document to the repository."
4139
+ }
4074
4140
  }
4075
4141
  }