@cat-factory/app 0.80.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,113 @@
1
+ <script setup lang="ts">
2
+ // Create a new INITIATIVE under a service frame — the longer-running counterpart
3
+ // to a task. The user names it and writes the rough goal statement; the server
4
+ // materialises the initiative-level board block + its empty tracker entity in one
5
+ // call. Nothing is planned here: the user then runs the Initiative Planning
6
+ // pipeline (pl_initiative) on the block, which analyses the codebase, drafts the
7
+ // multi-phase plan for approval, and commits the in-repo tracker.
8
+ const ui = useUiStore()
9
+ const board = useBoardStore()
10
+ const initiatives = useInitiativesStore()
11
+ const toast = useToast()
12
+ const { t } = useI18n()
13
+
14
+ const open = computed({
15
+ get: () => ui.createInitiativeFrameId !== null,
16
+ set: (v: boolean) => {
17
+ if (!v) ui.closeCreateInitiative()
18
+ },
19
+ })
20
+
21
+ const frame = computed(() =>
22
+ ui.createInitiativeFrameId ? board.getBlock(ui.createInitiativeFrameId) : undefined,
23
+ )
24
+
25
+ const title = ref('')
26
+ const description = ref('')
27
+
28
+ watch(open, (o) => {
29
+ if (o) {
30
+ title.value = ''
31
+ description.value = ''
32
+ }
33
+ })
34
+
35
+ async function create() {
36
+ const frameId = ui.createInitiativeFrameId
37
+ if (!frameId || !title.value.trim() || initiatives.creating) return
38
+ try {
39
+ const { block } = await initiatives.create(frameId, {
40
+ title: title.value.trim(),
41
+ description: description.value.trim() || undefined,
42
+ })
43
+ ui.closeCreateInitiative()
44
+ // Select the fresh block so the inspector offers "Run planning" right away.
45
+ ui.select(block.id)
46
+ } catch (e) {
47
+ toast.add({
48
+ title: t('initiative.create.failedTitle'),
49
+ description: e instanceof Error ? e.message : String(e),
50
+ icon: 'i-lucide-triangle-alert',
51
+ color: 'error',
52
+ })
53
+ }
54
+ }
55
+ </script>
56
+
57
+ <template>
58
+ <UModal v-model:open="open" :title="t('initiative.create.title')">
59
+ <template #body>
60
+ <div class="space-y-4" data-testid="create-initiative-modal">
61
+ <p v-if="frame" class="text-xs text-slate-400">
62
+ <i18n-t keypath="initiative.create.inFrame" tag="span" scope="global">
63
+ <template #frame>
64
+ <span class="font-medium text-slate-200">{{ frame.title }}</span>
65
+ </template>
66
+ </i18n-t>
67
+ </p>
68
+
69
+ <UFormField :label="t('initiative.create.titleField')" required>
70
+ <UInput
71
+ v-model="title"
72
+ data-testid="create-initiative-title"
73
+ :placeholder="t('initiative.create.titlePlaceholder')"
74
+ autofocus
75
+ class="w-full"
76
+ @keydown.enter="create"
77
+ />
78
+ </UFormField>
79
+
80
+ <UFormField :label="t('initiative.create.goalField')">
81
+ <UTextarea
82
+ v-model="description"
83
+ data-testid="create-initiative-goal"
84
+ :rows="4"
85
+ autoresize
86
+ :placeholder="t('initiative.create.goalPlaceholder')"
87
+ class="w-full"
88
+ />
89
+ </UFormField>
90
+
91
+ <p class="text-[11px] text-slate-500">
92
+ {{ t('initiative.create.hint') }}
93
+ </p>
94
+ </div>
95
+ </template>
96
+ <template #footer>
97
+ <div class="flex w-full justify-end gap-2">
98
+ <UButton color="neutral" variant="ghost" @click="open = false">
99
+ {{ t('common.cancel') }}
100
+ </UButton>
101
+ <UButton
102
+ data-testid="create-initiative-submit"
103
+ color="primary"
104
+ :loading="initiatives.creating"
105
+ :disabled="!title.trim()"
106
+ @click="create"
107
+ >
108
+ {{ t('initiative.create.submit') }}
109
+ </UButton>
110
+ </div>
111
+ </template>
112
+ </UModal>
113
+ </template>
@@ -3,6 +3,7 @@ import type { Block, BlockStatus } from '~/types/domain'
3
3
  import { blockTypeMeta, STATUS_META } from '~/utils/catalog'
4
4
  import DecisionBadge from './DecisionBadge.vue'
5
5
  import DraggableTask from './DraggableTask.vue'
6
+ import InitiativeCard from './InitiativeCard.vue'
6
7
  import ModuleFrame from './ModuleFrame.vue'
7
8
  import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
8
9
  import AgentStopButton from '~/components/board/AgentStopButton.vue'
@@ -36,10 +37,13 @@ const typeMeta = computed(() => (block.value ? blockTypeMeta(block.value.type) :
36
37
  // ---- this service's children (tasks + modules) -----------------------------
37
38
  const directTasks = computed(() => board.tasksOf(props.id))
38
39
  const modules = computed(() => board.modulesOf(props.id))
40
+ const initiativeBlocks = computed(() => board.initiativesOf(props.id))
39
41
  const allTasks = computed(() => board.allTasksUnder(props.id))
40
42
  const taskIds = computed(() => new Set(allTasks.value.map((t) => t.id)))
41
43
  const taskCount = computed(() => allTasks.value.length)
42
- const hasTasks = computed(() => taskCount.value > 0 || modules.value.length > 0)
44
+ const hasTasks = computed(
45
+ () => taskCount.value > 0 || modules.value.length > 0 || initiativeBlocks.value.length > 0,
46
+ )
43
47
  // Single pass over the tasks for both rollups (vs. one filter each).
44
48
  const taskStats = computed(() => {
45
49
  let merged = 0
@@ -156,6 +160,11 @@ function addRecurring() {
156
160
  ui.openAddRecurring(props.id)
157
161
  }
158
162
 
163
+ function createInitiative() {
164
+ ui.expandFrame(props.id)
165
+ ui.openCreateInitiative(props.id)
166
+ }
167
+
159
168
  // A task needs merging → green pulse; a task needs a decision → amber pulse.
160
169
  const pulseClass = computed(() => {
161
170
  if (frameStatus.value === 'blocked') return 'board-pulse'
@@ -462,6 +471,16 @@ const ITEM_ICON: Record<string, string> = {
462
471
  :title="t('board.frame.addRecurringTitle')"
463
472
  @click.stop="addRecurring"
464
473
  />
474
+ <UButton
475
+ class="nodrag"
476
+ data-testid="frame-add-initiative"
477
+ :size="isTouch ? 'sm' : 'xs'"
478
+ variant="ghost"
479
+ color="neutral"
480
+ icon="i-lucide-milestone"
481
+ :title="t('board.frame.createInitiativeTitle')"
482
+ @click.stop="createInitiative"
483
+ />
465
484
  <UButton
466
485
  class="nodrag"
467
486
  :size="isTouch ? 'sm' : 'xs'"
@@ -494,6 +513,7 @@ const ITEM_ICON: Record<string, string> = {
494
513
  :style="{ width: canvas.w + 'px', height: canvas.h + 'px' }"
495
514
  >
496
515
  <ModuleFrame v-for="m in modules" :key="m.id" :module-id="m.id" />
516
+ <InitiativeCard v-for="i in initiativeBlocks" :key="i.id" :block-id="i.id" />
497
517
  <DraggableTask v-for="t in directTasks" :key="t.id" :task-id="t.id" />
498
518
  <button
499
519
  v-if="!hasTasks"
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ // The board card for an `initiative`-level block (a frame child, like a module):
3
+ // title, the initiative's lifecycle status, and — once a plan is ingested — the
4
+ // item-completion progress. Clicking selects the block (the inspector offers
5
+ // "Run planning" / "Open tracker"); the tracker button opens the dedicated
6
+ // window directly. Draggable within its frame like a task card.
7
+ import type { InitiativeStatus } from '~/types/domain'
8
+ import { useBlockDrag } from '~/composables/useBlockDrag'
9
+ import {
10
+ INITIATIVE_STATUS_CHIPS,
11
+ INITIATIVE_STATUS_LABEL_KEYS,
12
+ initiativeProgress,
13
+ } from '~/utils/initiative'
14
+
15
+ const props = defineProps<{ blockId: string }>()
16
+ const board = useBoardStore()
17
+ const initiatives = useInitiativesStore()
18
+ const ui = useUiStore()
19
+ const { t } = useI18n()
20
+ const { draggingId, startDrag } = useBlockDrag()
21
+
22
+ const block = computed(() => board.getBlock(props.blockId))
23
+ const initiative = computed(() => initiatives.forBlock(props.blockId))
24
+
25
+ const status = computed<InitiativeStatus>(() => initiative.value?.status ?? 'planning')
26
+ const statusLabel = computed(() => t(INITIATIVE_STATUS_LABEL_KEYS[status.value]))
27
+
28
+ const progress = computed(() => initiativeProgress(initiative.value?.items))
29
+
30
+ const selected = computed(() => ui.selectedBlockId === props.blockId)
31
+
32
+ function select() {
33
+ ui.select(props.blockId)
34
+ }
35
+ function openTracker() {
36
+ ui.select(props.blockId)
37
+ ui.openInitiativeTracker(props.blockId)
38
+ }
39
+ function onHandle(e: PointerEvent) {
40
+ if (block.value) startDrag(block.value, e)
41
+ }
42
+ </script>
43
+
44
+ <template>
45
+ <div
46
+ v-if="block"
47
+ class="absolute w-[230px]"
48
+ :style="{
49
+ left: block.position.x + 'px',
50
+ top: block.position.y + 'px',
51
+ zIndex: draggingId === blockId ? 60 : 10,
52
+ pointerEvents: draggingId === blockId ? 'none' : undefined,
53
+ }"
54
+ >
55
+ <div
56
+ class="nodrag nopan flex cursor-grab touch-none items-center justify-center rounded-t-lg border border-b-0 border-indigo-800/60 bg-indigo-950/60 py-px active:cursor-grabbing pointer-coarse:py-2"
57
+ :title="t('board.frame.dragTask')"
58
+ @pointerdown="onHandle"
59
+ >
60
+ <UIcon
61
+ name="i-lucide-grip-horizontal"
62
+ class="h-3 w-3 text-indigo-400/60 pointer-coarse:h-5 pointer-coarse:w-5"
63
+ />
64
+ </div>
65
+ <div
66
+ data-testid="initiative-card"
67
+ class="cursor-pointer rounded-b-lg border border-indigo-800/60 bg-indigo-950/40 p-3 transition hover:border-indigo-600"
68
+ :class="selected ? 'ring-2 ring-indigo-400/60' : ''"
69
+ @click.stop="select"
70
+ >
71
+ <div class="flex items-start justify-between gap-2">
72
+ <div class="flex items-center gap-2">
73
+ <UIcon name="i-lucide-milestone" class="h-4 w-4 shrink-0 text-indigo-400" />
74
+ <div class="text-xs font-semibold text-white">{{ block.title }}</div>
75
+ </div>
76
+ <UBadge :color="INITIATIVE_STATUS_CHIPS[status] as any" variant="subtle" size="sm">
77
+ {{ statusLabel }}
78
+ </UBadge>
79
+ </div>
80
+ <div class="mt-1 text-[10px] uppercase tracking-wide text-indigo-300/70">
81
+ {{ t('initiative.card.kind') }}
82
+ </div>
83
+ <div v-if="progress" class="mt-2 space-y-1">
84
+ <div class="h-1.5 overflow-hidden rounded bg-slate-800">
85
+ <div
86
+ class="h-full rounded bg-indigo-400"
87
+ :style="{ width: `${Math.round((progress.settled / progress.total) * 100)}%` }"
88
+ />
89
+ </div>
90
+ <div class="text-[10px] text-slate-400">
91
+ {{ t('initiative.card.progress', { done: progress.settled, total: progress.total }) }}
92
+ </div>
93
+ </div>
94
+ <UButton
95
+ class="nodrag mt-2"
96
+ data-testid="initiative-open-tracker"
97
+ size="xs"
98
+ variant="soft"
99
+ color="primary"
100
+ icon="i-lucide-list-checks"
101
+ @click.stop="openTracker"
102
+ >
103
+ {{ t('initiative.card.openTracker') }}
104
+ </UButton>
105
+ </div>
106
+ </div>
107
+ </template>
@@ -0,0 +1,279 @@
1
+ <script setup lang="ts">
2
+ // The initiative tracker window — the dedicated read-only view of an initiative's
3
+ // plan/tracker entity: goal + constraints, the phases with their per-item status +
4
+ // PR links, the execution policy, and the decisions / deviations / follow-ups /
5
+ // caveats logs. Renders the DB entity (the source of truth) — never the in-repo
6
+ // mirror, which may not exist (GitHub-unwired workspaces). Opened via the universal
7
+ // result-view host: from the board card / inspector (`ui.openInitiativeTracker`) or
8
+ // as the planner step's result view. Live `initiative` stream events patch the
9
+ // store, so an open window follows the plan as it is ingested and later executed.
10
+ import { computed } from 'vue'
11
+ import type { InitiativeItem } from '~/types/domain'
12
+ import {
13
+ INITIATIVE_ITEM_STATUS_CHIPS,
14
+ INITIATIVE_ITEM_STATUS_LABEL_KEYS,
15
+ INITIATIVE_STATUS_LABEL_KEYS,
16
+ } from '~/utils/initiative'
17
+
18
+ const board = useBoardStore()
19
+ const initiatives = useInitiativesStore()
20
+ const { t } = useI18n()
21
+
22
+ const { open, blockId, close } = useResultView('initiative-tracker', {
23
+ onOpen: (id) => void initiatives.load(id),
24
+ })
25
+
26
+ const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
27
+ const initiative = computed(() => (blockId.value ? initiatives.forBlock(blockId.value) : null))
28
+
29
+ const phases = computed(() => initiative.value?.phases ?? [])
30
+ function itemsOf(phaseId: string): InitiativeItem[] {
31
+ return (initiative.value?.items ?? []).filter((i) => i.phaseId === phaseId)
32
+ }
33
+
34
+ const policyRules = computed(() => initiative.value?.policy?.rules ?? [])
35
+ function ruleAxes(rule: { minComplexity?: number; minRisk?: number; minImpact?: number }): string {
36
+ const axes = [
37
+ rule.minComplexity !== undefined
38
+ ? t('initiative.tracker.axisComplexity', { value: rule.minComplexity })
39
+ : null,
40
+ rule.minRisk !== undefined ? t('initiative.tracker.axisRisk', { value: rule.minRisk }) : null,
41
+ rule.minImpact !== undefined
42
+ ? t('initiative.tracker.axisImpact', { value: rule.minImpact })
43
+ : null,
44
+ ].filter((a): a is string => a !== null)
45
+ return axes.length ? axes.join(' · ') : t('initiative.tracker.axisNever')
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <Teleport to="body">
51
+ <div
52
+ v-if="open"
53
+ class="fixed inset-0 z-50 flex max-h-[100dvh] items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
54
+ @click.self="close"
55
+ >
56
+ <div
57
+ class="m-4 flex w-full max-w-4xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
58
+ role="dialog"
59
+ aria-modal="true"
60
+ data-testid="initiative-tracker-window"
61
+ >
62
+ <!-- Header -->
63
+ <header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
64
+ <span
65
+ class="flex h-8 w-8 items-center justify-center rounded-lg bg-indigo-500/15 text-indigo-300"
66
+ >
67
+ <UIcon name="i-lucide-milestone" class="h-4 w-4" />
68
+ </span>
69
+ <div class="min-w-0 flex-1">
70
+ <h2 class="truncate text-sm font-semibold text-slate-100">
71
+ {{ initiative?.title ?? block?.title ?? t('initiative.tracker.title') }}
72
+ </h2>
73
+ <p class="truncate text-[11px] text-slate-400">
74
+ {{ t('initiative.tracker.subtitle') }}
75
+ </p>
76
+ </div>
77
+ <UBadge v-if="initiative" color="primary" variant="subtle" size="sm">
78
+ {{ t(INITIATIVE_STATUS_LABEL_KEYS[initiative.status]) }}
79
+ </UBadge>
80
+ <button
81
+ class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
82
+ @click="close"
83
+ >
84
+ <UIcon name="i-lucide-x" class="h-4 w-4" />
85
+ </button>
86
+ </header>
87
+
88
+ <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
89
+ <!-- No entity yet (module unwired / still creating) -->
90
+ <div
91
+ v-if="!initiative"
92
+ class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
93
+ >
94
+ <UIcon name="i-lucide-milestone" class="h-8 w-8 opacity-40" />
95
+ <p class="text-sm">{{ t('initiative.tracker.empty') }}</p>
96
+ </div>
97
+
98
+ <template v-else>
99
+ <!-- Goal & constraints -->
100
+ <section v-if="initiative.goal" class="mb-4">
101
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
102
+ {{ t('initiative.tracker.goal') }}
103
+ </h3>
104
+ <p class="whitespace-pre-wrap text-[13px] leading-relaxed text-slate-300">
105
+ {{ initiative.goal }}
106
+ </p>
107
+ </section>
108
+ <section v-if="initiative.constraints?.length" class="mb-4">
109
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
110
+ {{ t('initiative.tracker.constraints') }}
111
+ </h3>
112
+ <ul class="list-inside list-disc text-[13px] text-slate-300">
113
+ <li v-for="(c, i) in initiative.constraints" :key="i">{{ c }}</li>
114
+ </ul>
115
+ </section>
116
+ <section v-if="initiative.nonGoals?.length" class="mb-4">
117
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
118
+ {{ t('initiative.tracker.nonGoals') }}
119
+ </h3>
120
+ <ul class="list-inside list-disc text-[13px] text-slate-300">
121
+ <li v-for="(g, i) in initiative.nonGoals" :key="i">{{ g }}</li>
122
+ </ul>
123
+ </section>
124
+ <section v-if="initiative.analysisSummary" class="mb-4">
125
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
126
+ {{ t('initiative.tracker.analysis') }}
127
+ </h3>
128
+ <p class="whitespace-pre-wrap text-[13px] leading-relaxed text-slate-300">
129
+ {{ initiative.analysisSummary }}
130
+ </p>
131
+ </section>
132
+
133
+ <!-- Awaiting planning -->
134
+ <div
135
+ v-if="phases.length === 0"
136
+ class="mb-4 rounded-lg border border-dashed border-slate-700 p-4 text-center text-[12px] text-slate-400"
137
+ >
138
+ {{ t('initiative.tracker.noPlan') }}
139
+ </div>
140
+
141
+ <!-- Phases + items -->
142
+ <section v-for="phase in phases" :key="phase.id" class="mb-5">
143
+ <h3 class="mb-1 text-sm font-semibold text-slate-200">
144
+ {{ t('initiative.tracker.phase', { title: phase.title }) }}
145
+ </h3>
146
+ <p v-if="phase.goal" class="mb-2 text-[12px] text-slate-400">{{ phase.goal }}</p>
147
+ <div class="overflow-x-auto rounded-lg border border-slate-800">
148
+ <table class="w-full text-[12px]">
149
+ <thead>
150
+ <tr class="border-b border-slate-800 text-left text-slate-500">
151
+ <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colItem') }}</th>
152
+ <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colStatus') }}</th>
153
+ <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colPr') }}</th>
154
+ </tr>
155
+ </thead>
156
+ <tbody>
157
+ <tr
158
+ v-for="item in itemsOf(phase.id)"
159
+ :key="item.id"
160
+ class="border-b border-slate-800/60 last:border-0"
161
+ >
162
+ <td class="px-3 py-2 align-top">
163
+ <div class="font-medium text-slate-200">{{ item.title }}</div>
164
+ <div
165
+ v-if="item.dependsOn?.length"
166
+ class="mt-0.5 text-[10px] text-slate-500"
167
+ >
168
+ {{
169
+ t('initiative.tracker.dependsOn', {
170
+ items: item.dependsOn.join(', '),
171
+ })
172
+ }}
173
+ </div>
174
+ <div v-if="item.note" class="mt-0.5 text-[10px] text-amber-300/80">
175
+ {{ item.note }}
176
+ </div>
177
+ </td>
178
+ <td class="px-3 py-2 align-top">
179
+ <UBadge
180
+ :color="INITIATIVE_ITEM_STATUS_CHIPS[item.status] as any"
181
+ variant="subtle"
182
+ size="sm"
183
+ >
184
+ {{ t(INITIATIVE_ITEM_STATUS_LABEL_KEYS[item.status]) }}
185
+ </UBadge>
186
+ </td>
187
+ <td class="px-3 py-2 align-top">
188
+ <a
189
+ v-if="item.pr"
190
+ :href="item.pr.url"
191
+ target="_blank"
192
+ rel="noopener"
193
+ class="text-sky-400 hover:underline"
194
+ >
195
+ {{
196
+ item.pr.number ? `#${item.pr.number}` : t('initiative.tracker.prLink')
197
+ }}
198
+ </a>
199
+ <span v-else class="text-slate-600">—</span>
200
+ </td>
201
+ </tr>
202
+ </tbody>
203
+ </table>
204
+ </div>
205
+ </section>
206
+
207
+ <!-- Execution policy -->
208
+ <section v-if="initiative.policy" class="mb-4">
209
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
210
+ {{ t('initiative.tracker.policy') }}
211
+ </h3>
212
+ <ul class="text-[12px] text-slate-300">
213
+ <li>
214
+ {{
215
+ t('initiative.tracker.maxConcurrent', {
216
+ count: initiative.policy.maxConcurrent,
217
+ })
218
+ }}
219
+ </li>
220
+ <li v-for="(rule, i) in policyRules" :key="i">
221
+ <code class="text-sky-300">{{ rule.pipelineId }}</code>
222
+ · {{ ruleAxes(rule) }}
223
+ </li>
224
+ <li>
225
+ {{ t('initiative.tracker.defaultPipeline') }}
226
+ <code class="text-sky-300">{{ initiative.policy.defaultPipelineId }}</code>
227
+ </li>
228
+ </ul>
229
+ </section>
230
+
231
+ <!-- Logs -->
232
+ <section v-if="initiative.decisions?.length" class="mb-4">
233
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
234
+ {{ t('initiative.tracker.decisions') }}
235
+ </h3>
236
+ <ul class="list-inside list-disc text-[13px] text-slate-300">
237
+ <li v-for="d in initiative.decisions" :key="d.id">
238
+ <span class="font-medium">{{ d.title }}</span>
239
+ <span v-if="d.detail" class="text-slate-400"> — {{ d.detail }}</span>
240
+ </li>
241
+ </ul>
242
+ </section>
243
+ <section v-if="initiative.deviations?.length" class="mb-4">
244
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
245
+ {{ t('initiative.tracker.deviations') }}
246
+ </h3>
247
+ <ul class="list-inside list-disc text-[13px] text-slate-300">
248
+ <li v-for="d in initiative.deviations" :key="d.id">
249
+ <code v-if="d.itemId" class="text-slate-400">{{ d.itemId }}</code>
250
+ {{ d.description }}
251
+ <span v-if="d.resolution" class="text-slate-400"> → {{ d.resolution }}</span>
252
+ </li>
253
+ </ul>
254
+ </section>
255
+ <section v-if="initiative.followUps?.length" class="mb-4">
256
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
257
+ {{ t('initiative.tracker.followUps') }}
258
+ </h3>
259
+ <ul class="list-inside list-disc text-[13px] text-slate-300">
260
+ <li v-for="f in initiative.followUps" :key="f.id">
261
+ <span class="font-medium">{{ f.title }}</span>
262
+ <span v-if="f.detail" class="text-slate-400"> — {{ f.detail }}</span>
263
+ </li>
264
+ </ul>
265
+ </section>
266
+ <section v-if="initiative.caveats?.length" class="mb-4">
267
+ <h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
268
+ {{ t('initiative.tracker.caveats') }}
269
+ </h3>
270
+ <ul class="list-inside list-disc text-[13px] text-slate-300">
271
+ <li v-for="(c, i) in initiative.caveats" :key="i">{{ c }}</li>
272
+ </ul>
273
+ </section>
274
+ </template>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ </Teleport>
279
+ </template>
@@ -17,6 +17,7 @@ import TaskRunSettings from '~/components/panels/inspector/TaskRunSettings.vue'
17
17
  import TaskExecution from '~/components/panels/inspector/TaskExecution.vue'
18
18
  import TaskEstimateBadge from '~/components/panels/inspector/TaskEstimateBadge.vue'
19
19
  import EpicChildren from '~/components/panels/inspector/EpicChildren.vue'
20
+ import InitiativeInspector from '~/components/panels/inspector/InitiativeInspector.vue'
20
21
  import RecurringScheduleSettings from '~/components/panels/inspector/RecurringScheduleSettings.vue'
21
22
  import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
22
23
  import AgentStopButton from '~/components/board/AgentStopButton.vue'
@@ -73,6 +74,7 @@ watch(
73
74
  const isContainer = computed(() => level.value === 'frame' || level.value === 'module')
74
75
  const isTask = computed(() => level.value === 'task')
75
76
  const isEpic = computed(() => level.value === 'epic')
77
+ const isInitiative = computed(() => level.value === 'initiative')
76
78
 
77
79
  const instance = computed(() => execution.getInstance(block.value?.executionId))
78
80
  const typeMeta = computed(() => (block.value ? blockTypeMeta(block.value.type) : null))
@@ -509,6 +511,9 @@ const showOriginalDescription = ref(false)
509
511
  <!-- epic: the full tree of member tasks, grouped by service → module -->
510
512
  <EpicChildren v-else-if="isEpic" :key="`epic-${block.id}`" :block="block" />
511
513
 
514
+ <!-- initiative: status + goal, run-planning + tracker controls -->
515
+ <InitiativeInspector v-else-if="isInitiative" :block="block" />
516
+
512
517
  <!-- actions -->
513
518
  <div class="flex items-center gap-2">
514
519
  <UDropdownMenu v-if="isTask" :items="runMenu">
@@ -23,6 +23,7 @@ import GenericStructuredResultView from '~/components/panels/GenericStructuredRe
23
23
  import ServiceSpecWindow from '~/components/spec/ServiceSpecWindow.vue'
24
24
  import FollowUpWindow from '~/components/followUp/FollowUpWindow.vue'
25
25
  import MergerResultView from '~/components/panels/MergerResultView.vue'
26
+ import InitiativeTrackerWindow from '~/components/initiative/InitiativeTrackerWindow.vue'
26
27
 
27
28
  const ui = useUiStore()
28
29
 
@@ -52,6 +53,10 @@ const STEP_RESULT_VIEWS: Record<string, Component> = {
52
53
  // The merger's verdict: the PR's complexity/risk/impact scores + the engine's auto-merge
53
54
  // or awaiting-review decision (and why), instead of the agent's raw JSON.
54
55
  merger: MergerResultView,
56
+ // The initiative tracker: phases, per-item status + PR links, decisions, deviations,
57
+ // caveats. Opened from the initiative card / inspector (`ui.openInitiativeTracker`) and
58
+ // as the planner step's result view.
59
+ 'initiative-tracker': InitiativeTrackerWindow,
55
60
  }
56
61
 
57
62
  const active = computed<Component | null>(() => {