@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.
- package/app/components/board/CreateInitiativeModal.vue +113 -0
- package/app/components/board/nodes/BlockNode.vue +21 -1
- package/app/components/board/nodes/InitiativeCard.vue +107 -0
- package/app/components/initiative/InitiativeTrackerWindow.vue +279 -0
- package/app/components/panels/InspectorPanel.vue +5 -0
- package/app/components/panels/StepResultViewHost.vue +5 -0
- package/app/components/panels/inspector/InitiativeInspector.vue +78 -0
- package/app/composables/api/initiative.ts +28 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/useBlockQueries.ts +22 -3
- package/app/composables/useWorkspaceStream.ts +5 -0
- package/app/pages/index.vue +1 -0
- package/app/stores/board.ts +20 -6
- package/app/stores/initiative.ts +98 -0
- package/app/stores/ui.ts +18 -0
- package/app/stores/workspace.ts +3 -0
- package/app/types/domain.ts +1 -0
- package/app/types/initiative.ts +18 -0
- package/app/utils/catalog.ts +24 -0
- package/app/utils/initiative.ts +57 -0
- package/i18n/locales/en.json +67 -1
- package/i18n/locales/es.json +67 -1
- package/i18n/locales/fr.json +67 -1
- package/i18n/locales/he.json +67 -1
- package/i18n/locales/ja.json +67 -1
- package/i18n/locales/pl.json +67 -1
- package/i18n/locales/tr.json +67 -1
- package/i18n/locales/uk.json +67 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
package/app/pages/index.vue
CHANGED
package/app/stores/board.ts
CHANGED
|
@@ -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
|
-
/**
|
|
26
|
-
|
|
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
|
|
259
|
-
// that link — snapshot the originals so a failed delete restores them
|
|
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)) ||
|
|
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) => ({
|
|
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,
|
package/app/stores/workspace.ts
CHANGED
|
@@ -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
|
package/app/types/domain.ts
CHANGED
|
@@ -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'
|
package/app/utils/catalog.ts
CHANGED
|
@@ -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
|
+
}
|
package/i18n/locales/en.json
CHANGED
|
@@ -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
|
}
|