@cat-factory/app 1.0.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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +18 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AgentFailureCard.vue +97 -0
  10. package/app/components/board/AgentStopButton.vue +61 -0
  11. package/app/components/board/BoardCanvas.vue +146 -0
  12. package/app/components/board/TaskDependencyEdges.vue +132 -0
  13. package/app/components/board/nodes/AgentChip.vue +59 -0
  14. package/app/components/board/nodes/BlockNode.vue +347 -0
  15. package/app/components/board/nodes/DecisionBadge.vue +21 -0
  16. package/app/components/board/nodes/DraggableTask.vue +69 -0
  17. package/app/components/board/nodes/ModuleFrame.vue +70 -0
  18. package/app/components/board/nodes/TaskCard.vue +237 -0
  19. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  20. package/app/components/documents/DocumentImportModal.vue +161 -0
  21. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  22. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  23. package/app/components/documents/TaskContextDocs.vue +83 -0
  24. package/app/components/focus/BlockFocusView.vue +161 -0
  25. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  26. package/app/components/github/GitHubConnect.vue +183 -0
  27. package/app/components/github/GitHubPanel.vue +584 -0
  28. package/app/components/layout/BoardSwitcher.vue +202 -0
  29. package/app/components/layout/BoardToolbar.vue +109 -0
  30. package/app/components/layout/SideBar.vue +193 -0
  31. package/app/components/layout/SpendWarningBanner.vue +107 -0
  32. package/app/components/palettes/AgentPalette.vue +33 -0
  33. package/app/components/palettes/BlockPalette.vue +41 -0
  34. package/app/components/palettes/PipelinePalette.vue +74 -0
  35. package/app/components/panels/DecisionModal.vue +71 -0
  36. package/app/components/panels/InspectorPanel.vue +296 -0
  37. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  38. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  39. package/app/components/panels/inspector/TaskExecution.vue +175 -0
  40. package/app/components/panels/inspector/TaskModelSettings.vue +128 -0
  41. package/app/components/panels/inspector/TaskStructure.vue +139 -0
  42. package/app/components/pipeline/PipelineBuilder.vue +227 -0
  43. package/app/components/pipeline/PipelineProgress.vue +246 -0
  44. package/app/components/requirements/RequirementReviewModal.vue +328 -0
  45. package/app/components/scenarios/FeatureScenarios.vue +162 -0
  46. package/app/components/scenarios/ScenarioCard.vue +109 -0
  47. package/app/components/tasks/TaskContextIssues.vue +88 -0
  48. package/app/components/tasks/TaskImportModal.vue +140 -0
  49. package/app/components/tasks/TaskSourceConnectModal.vue +122 -0
  50. package/app/composables/useApi.ts +535 -0
  51. package/app/composables/useBlockDrag.ts +75 -0
  52. package/app/composables/useBlockQueries.ts +136 -0
  53. package/app/composables/useBoardFlow.ts +11 -0
  54. package/app/composables/useDepLabels.ts +26 -0
  55. package/app/composables/useSemanticZoom.ts +16 -0
  56. package/app/composables/useWorkspaceStream.ts +125 -0
  57. package/app/docs/architecture.md +31 -0
  58. package/app/pages/index.vue +80 -0
  59. package/app/stores/accounts.ts +64 -0
  60. package/app/stores/agentRuns.ts +117 -0
  61. package/app/stores/agents.ts +40 -0
  62. package/app/stores/auth.ts +97 -0
  63. package/app/stores/board.spec.ts +197 -0
  64. package/app/stores/board.ts +147 -0
  65. package/app/stores/bootstrap.ts +97 -0
  66. package/app/stores/documents.ts +165 -0
  67. package/app/stores/execution.ts +115 -0
  68. package/app/stores/fragmentLibrary.ts +147 -0
  69. package/app/stores/fragments.ts +40 -0
  70. package/app/stores/github.ts +291 -0
  71. package/app/stores/models.ts +48 -0
  72. package/app/stores/pipelines.ts +77 -0
  73. package/app/stores/requirements.ts +133 -0
  74. package/app/stores/scenarios.spec.ts +82 -0
  75. package/app/stores/scenarios.ts +196 -0
  76. package/app/stores/tasks.spec.ts +71 -0
  77. package/app/stores/tasks.ts +149 -0
  78. package/app/stores/ui.ts +204 -0
  79. package/app/stores/workspace.ts +201 -0
  80. package/app/types/accounts.ts +38 -0
  81. package/app/types/bootstrap.ts +83 -0
  82. package/app/types/documents.ts +92 -0
  83. package/app/types/domain.ts +216 -0
  84. package/app/types/execution.ts +110 -0
  85. package/app/types/fragments.ts +72 -0
  86. package/app/types/github.ts +153 -0
  87. package/app/types/models.ts +48 -0
  88. package/app/types/requirements.ts +38 -0
  89. package/app/types/scenarios.ts +36 -0
  90. package/app/types/tasks.ts +67 -0
  91. package/app/utils/catalog.spec.ts +82 -0
  92. package/app/utils/catalog.ts +185 -0
  93. package/app/utils/dnd.ts +29 -0
  94. package/nuxt.config.ts +43 -0
  95. package/package.json +43 -0
@@ -0,0 +1,201 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type { SpendStatus, Workspace, WorkspaceSnapshot } from '~/types/domain'
4
+ import { useAccountsStore } from '~/stores/accounts'
5
+ import { useBoardStore } from '~/stores/board'
6
+ import { usePipelinesStore } from '~/stores/pipelines'
7
+ import { useExecutionStore } from '~/stores/execution'
8
+ import { useAgentRunsStore } from '~/stores/agentRuns'
9
+
10
+ /**
11
+ * Owns the active workspace and bootstraps the app against the backend. On load
12
+ * it resolves the user's accounts, lists the boards in the active account, opens
13
+ * the persisted one (or the first, or a fresh seeded board), and hydrates the
14
+ * board / pipelines / execution stores from its snapshot.
15
+ *
16
+ * Boards are scoped to an account: switching account re-scopes the board list,
17
+ * and new boards are stamped with the active account so a team can keep org
18
+ * boards separate from personal ones. Only the active workspace id is persisted —
19
+ * all board data lives on the server.
20
+ */
21
+ export const useWorkspaceStore = defineStore(
22
+ 'workspace',
23
+ () => {
24
+ const api = useApi()
25
+
26
+ /** Active workspace id (persisted so a reload reopens the same board). */
27
+ const workspaceId = ref<string | null>(null)
28
+ /** Every board visible to the user, across the accounts they belong to. */
29
+ const workspaces = ref<Workspace[]>([])
30
+ /** True once the initial snapshot has been loaded and stores hydrated. */
31
+ const ready = ref(false)
32
+ /** Set when bootstrap fails so the UI can show a retry. */
33
+ const error = ref<string | null>(null)
34
+ /** Latest spend-safeguard status from the server (null until first load). */
35
+ const spend = ref<SpendStatus | null>(null)
36
+
37
+ /** The boards belonging to the active account (all boards when auth is off). */
38
+ const accountWorkspaces = computed(() => {
39
+ const accounts = useAccountsStore()
40
+ if (!accounts.enabled || !accounts.activeAccountId) return workspaces.value
41
+ return workspaces.value.filter((w) => w.accountId === accounts.activeAccountId)
42
+ })
43
+
44
+ /** The active board's row (for the switcher label). */
45
+ const activeWorkspace = computed(
46
+ () => workspaces.value.find((w) => w.id === workspaceId.value) ?? null,
47
+ )
48
+
49
+ /** Push a snapshot into the data stores. */
50
+ function hydrate(snapshot: WorkspaceSnapshot) {
51
+ workspaceId.value = snapshot.workspace.id
52
+ spend.value = snapshot.spend ?? null
53
+ // Keep the board list in step (e.g. a freshly created board, or a rename).
54
+ const i = workspaces.value.findIndex((w) => w.id === snapshot.workspace.id)
55
+ if (i >= 0) workspaces.value[i] = snapshot.workspace
56
+ else workspaces.value.unshift(snapshot.workspace)
57
+ useBoardStore().hydrate(snapshot.blocks)
58
+ usePipelinesStore().hydrate(snapshot.pipelines)
59
+ useExecutionStore().hydrate(snapshot.executions)
60
+ useAgentRunsStore().hydrate(snapshot.bootstrapJobs ?? [])
61
+ }
62
+
63
+ /** Resolve accounts + boards, then open the right board for the active account. */
64
+ async function init() {
65
+ ready.value = false
66
+ error.value = null
67
+ try {
68
+ // Accounts are an auth concept — empty in dev, which leaves boards unscoped.
69
+ await useAccountsStore()
70
+ .load()
71
+ .catch(() => {})
72
+ workspaces.value = await api.listWorkspaces()
73
+ await resolveActiveBoard()
74
+ ready.value = true
75
+ } catch (e) {
76
+ error.value = e instanceof Error ? e.message : 'Failed to reach the backend.'
77
+ }
78
+ }
79
+
80
+ /** Open the persisted board (aligning the active account to it), else pick/create one. */
81
+ async function resolveActiveBoard() {
82
+ const accounts = useAccountsStore()
83
+ if (workspaceId.value) {
84
+ const existing = workspaces.value.find((w) => w.id === workspaceId.value)
85
+ if (existing) {
86
+ if (accounts.enabled && existing.accountId) accounts.activeAccountId = existing.accountId
87
+ hydrate(await api.getWorkspace(existing.id))
88
+ return
89
+ }
90
+ // Persisted board is gone (deleted, or now another tenant's) — fall through.
91
+ workspaceId.value = null
92
+ }
93
+ const first = accountWorkspaces.value[0]
94
+ if (first) {
95
+ hydrate(await api.getWorkspace(first.id))
96
+ } else {
97
+ hydrate(
98
+ await api.createWorkspace({
99
+ seed: true,
100
+ accountId: accounts.activeAccountId ?? undefined,
101
+ }),
102
+ )
103
+ }
104
+ }
105
+
106
+ /** Switch to another board (within reach of the active account). */
107
+ async function switchTo(id: string) {
108
+ if (id === workspaceId.value) return
109
+ hydrate(await api.getWorkspace(id))
110
+ }
111
+
112
+ /** Switch the active account, then open one of its boards (creating one if needed). */
113
+ async function selectAccount(id: string) {
114
+ const accounts = useAccountsStore()
115
+ if (id === accounts.activeAccountId) return
116
+ accounts.switchTo(id)
117
+ workspaceId.value = null
118
+ await resolveActiveBoard()
119
+ }
120
+
121
+ /** Create a new board in the active account and open it. */
122
+ async function create(name?: string) {
123
+ const accounts = useAccountsStore()
124
+ const snapshot = await api.createWorkspace({
125
+ seed: true,
126
+ name,
127
+ accountId: accounts.activeAccountId ?? undefined,
128
+ })
129
+ hydrate(snapshot)
130
+ return snapshot.workspace
131
+ }
132
+
133
+ /** Rename a board. */
134
+ async function rename(id: string, name: string) {
135
+ const updated = await api.renameWorkspace(id, name)
136
+ const i = workspaces.value.findIndex((w) => w.id === id)
137
+ if (i >= 0) workspaces.value[i] = updated
138
+ return updated
139
+ }
140
+
141
+ /** Delete a board; if it was active, fall back to another in the account. */
142
+ async function remove(id: string) {
143
+ await api.deleteWorkspace(id)
144
+ workspaces.value = workspaces.value.filter((w) => w.id !== id)
145
+ if (workspaceId.value === id) {
146
+ workspaceId.value = null
147
+ await resolveActiveBoard()
148
+ }
149
+ }
150
+
151
+ /** Re-fetch the snapshot and re-hydrate (after mutations and on stream (re)connect). */
152
+ async function refresh() {
153
+ if (!workspaceId.value) return
154
+ hydrate(await api.getWorkspace(workspaceId.value))
155
+ }
156
+
157
+ /** Discard the current workspace and start a fresh, seeded one in this account. */
158
+ async function reset() {
159
+ const prev = workspaceId.value
160
+ workspaceId.value = null
161
+ await create()
162
+ if (prev) {
163
+ workspaces.value = workspaces.value.filter((w) => w.id !== prev)
164
+ await api.deleteWorkspace(prev).catch(() => {})
165
+ }
166
+ }
167
+
168
+ /** The active workspace id, or throw if the app isn't bootstrapped yet. */
169
+ function requireId(): string {
170
+ if (!workspaceId.value) throw new Error('No active workspace')
171
+ return workspaceId.value
172
+ }
173
+
174
+ /** Resume runs paused by the spend safeguard, then refresh the snapshot. */
175
+ async function resumeSpend() {
176
+ await api.resumeSpend(requireId())
177
+ await refresh()
178
+ }
179
+
180
+ return {
181
+ workspaceId,
182
+ workspaces,
183
+ accountWorkspaces,
184
+ activeWorkspace,
185
+ ready,
186
+ error,
187
+ spend,
188
+ init,
189
+ switchTo,
190
+ selectAccount,
191
+ create,
192
+ rename,
193
+ remove,
194
+ refresh,
195
+ reset,
196
+ requireId,
197
+ resumeSpend,
198
+ }
199
+ },
200
+ { persist: { pick: ['workspaceId'] } },
201
+ )
@@ -0,0 +1,38 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Account tenancy. An account owns workspaces (boards): either a single user's
3
+ // `personal` account or an `org` shared by many engineers. Memberships map users
4
+ // to accounts with a role. Mirrors the `@cat-factory/contracts` account schemas
5
+ // so responses drop straight into the Pinia store.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ export type AccountType = 'personal' | 'org'
9
+ export type AccountRole = 'owner' | 'member'
10
+
11
+ /** An account, annotated with the signed-in caller's role in it. */
12
+ export interface Account {
13
+ id: string
14
+ type: AccountType
15
+ name: string
16
+ githubAccountLogin: string | null
17
+ createdAt: number
18
+ /** The caller's role in this account (`null` in the auth-disabled path). */
19
+ role: AccountRole | null
20
+ }
21
+
22
+ /** A member of an account. */
23
+ export interface AccountMember {
24
+ accountId: string
25
+ userId: number
26
+ role: AccountRole
27
+ createdAt: number
28
+ }
29
+
30
+ export interface CreateAccountInput {
31
+ name: string
32
+ githubAccountLogin?: string
33
+ }
34
+
35
+ export interface AddMemberInput {
36
+ userId: number
37
+ role?: AccountRole
38
+ }
@@ -0,0 +1,83 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Repo-bootstrap domain types. Mirrors the `@cat-factory/contracts` bootstrap
3
+ // schemas so backend payloads drop straight into the Pinia store.
4
+ //
5
+ // A "reference architecture" is a managed base repo (an opinionated starter the
6
+ // org wants new services to follow); the "bootstrap repo" task creates a new repo
7
+ // from one and runs a bootstrapper agent in a container to adapt it.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ import type { AgentFailure, AgentFailureKind, StepSubtasks } from './execution'
11
+
12
+ /** A managed base repository new repos are bootstrapped from. */
13
+ export interface ReferenceArchitecture {
14
+ id: string
15
+ workspaceId: string
16
+ name: string
17
+ description: string
18
+ repoOwner: string
19
+ repoName: string
20
+ defaultInstructions: string
21
+ createdAt: number
22
+ updatedAt: number
23
+ }
24
+
25
+ /** Body to register a reference architecture. */
26
+ export interface CreateReferenceArchitectureInput {
27
+ name: string
28
+ description?: string
29
+ repoOwner: string
30
+ repoName: string
31
+ defaultInstructions?: string
32
+ }
33
+
34
+ /** Body to patch a reference architecture (only supplied fields change). */
35
+ export type UpdateReferenceArchitectureInput = Partial<CreateReferenceArchitectureInput>
36
+
37
+ /** Lifecycle of a single "bootstrap repo" run. */
38
+ export type BootstrapStatus = 'pending' | 'running' | 'succeeded' | 'failed'
39
+
40
+ /**
41
+ * A bootstrap run's failure is now the shared {@link AgentFailure} (same shape
42
+ * execution runs use), so the board renders one failure banner + retry for any
43
+ * agent. These aliases stay for back-compat / documentation of the bootstrap
44
+ * subset; `bootstrapFailureKindSchema` on the backend stays narrow.
45
+ */
46
+ export type BootstrapFailureKind = AgentFailureKind
47
+
48
+ /** Structured failure diagnostics captured when a bootstrap run fails. */
49
+ export type BootstrapFailure = AgentFailure
50
+
51
+ /** One "bootstrap repo" run with its outcome. */
52
+ export interface BootstrapJob {
53
+ id: string
54
+ workspaceId: string
55
+ /** Reference architecture the run was based on, or null for a from-scratch run. */
56
+ referenceArchitectureId: string | null
57
+ /** Denormalized reference architecture name, or null for a from-scratch run. */
58
+ referenceArchitectureName: string | null
59
+ repoName: string
60
+ repoOwner: string | null
61
+ repoUrl: string | null
62
+ instructions: string
63
+ status: BootstrapStatus
64
+ /** The board service frame this run materialises, or null if none was created. */
65
+ blockId: string | null
66
+ /** Live subtask counts from the bootstrapper agent, or null until it reports. */
67
+ subtasks: StepSubtasks | null
68
+ error: string | null
69
+ /** Structured failure diagnostics when `status` is `failed`; null otherwise. */
70
+ failure: BootstrapFailure | null
71
+ createdAt: number
72
+ updatedAt: number
73
+ }
74
+
75
+ /** Body to kick off a bootstrap run. Omit `referenceArchitectureId` to bootstrap
76
+ * from a freeform prompt alone (then `instructions` must be non-empty). */
77
+ export interface BootstrapRepoInput {
78
+ referenceArchitectureId?: string | null
79
+ repoName: string
80
+ description?: string
81
+ private?: boolean
82
+ instructions?: string
83
+ }
@@ -0,0 +1,92 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Document-source integration. Requirements / RFCs / PRDs imported from external
3
+ // sources (Confluence, Notion, …) can be expanded into board structure or
4
+ // attached to a task as agent context. These mirror the `@cat-factory/contracts`
5
+ // document schemas; the abstraction is source-agnostic, keyed by `source`.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import type { BlockType } from './domain'
9
+
10
+ /** The external document sources cat-factory can link to. */
11
+ export type DocumentSourceKind = 'confluence' | 'notion'
12
+
13
+ /** One credential a provider needs to connect (rendered as a form field). */
14
+ export interface CredentialField {
15
+ key: string
16
+ label: string
17
+ help?: string
18
+ placeholder?: string
19
+ secret?: boolean
20
+ }
21
+
22
+ /** A source's self-description: drives the generic connect + import UI. */
23
+ export interface DocumentSourceDescriptor {
24
+ source: DocumentSourceKind
25
+ label: string
26
+ /** Lucide icon name for the source. */
27
+ icon: string
28
+ credentialFields: CredentialField[]
29
+ refLabel: string
30
+ refPlaceholder: string
31
+ }
32
+
33
+ /** A workspace's connection to a document source (never carries credentials). */
34
+ export interface DocumentConnection {
35
+ source: DocumentSourceKind
36
+ /** Human-friendly label for what we're connected to (site URL, workspace name). */
37
+ label: string
38
+ /** When the connection was established (epoch ms). */
39
+ connectedAt: number
40
+ }
41
+
42
+ /** A page imported from a source into the workspace. */
43
+ export interface SourceDocument {
44
+ source: DocumentSourceKind
45
+ /** The source's stable id for the page. */
46
+ externalId: string
47
+ title: string
48
+ url: string
49
+ /** Short plain-text preview of the page body. */
50
+ excerpt: string
51
+ /** The board block this document is attached to as context, if any. */
52
+ linkedBlockId: string | null
53
+ syncedAt: number
54
+ }
55
+
56
+ /** A proposed task within a planned frame/module. */
57
+ export interface PlanTask {
58
+ title: string
59
+ description?: string
60
+ features?: string[]
61
+ }
62
+
63
+ /** A proposed module grouping tasks within a planned frame. */
64
+ export interface PlanModule {
65
+ name: string
66
+ tasks: PlanTask[]
67
+ }
68
+
69
+ /** A proposed top-level frame with its modules and loose tasks. */
70
+ export interface PlanFrame {
71
+ type: BlockType
72
+ title: string
73
+ description?: string
74
+ modules: PlanModule[]
75
+ tasks: PlanTask[]
76
+ }
77
+
78
+ /** A board structure extracted from an imported document. */
79
+ export interface DocumentBoardPlan {
80
+ source: DocumentSourceKind
81
+ externalId: string
82
+ /** Whether an LLM produced the plan or the deterministic heading parser did. */
83
+ planner: 'llm' | 'headings'
84
+ frames: PlanFrame[]
85
+ }
86
+
87
+ /** Counts of blocks created by spawning a plan onto the board. */
88
+ export interface SpawnResult {
89
+ frames: number
90
+ modules: number
91
+ tasks: number
92
+ }
@@ -0,0 +1,216 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Domain model for the Agent Architecture Board.
3
+ //
4
+ // These shapes mirror the `@cat-factory/contracts` wire schemas exactly, so a
5
+ // payload returned by the backend drops straight into the Pinia stores without
6
+ // translation. The board and its agent pipelines are owned by the backend; the
7
+ // frontend renders and mutates that state over the REST API.
8
+ //
9
+ // This module holds the core board vocabulary. Adjacent concerns live in
10
+ // sibling modules and are re-exported below so `~/types/domain` stays the single
11
+ // import surface:
12
+ // - execution model → ./execution
13
+ // - models/fragments → ./models
14
+ // - document sources → ./documents
15
+ // ---------------------------------------------------------------------------
16
+
17
+ import type { ExecutionInstance } from './execution'
18
+ import type { BootstrapJob } from './bootstrap'
19
+
20
+ /** Lifecycle of an architecture building block. */
21
+ export type BlockStatus =
22
+ | 'planned' // sketched, no dependencies satisfied yet
23
+ | 'ready' // dependencies done, can be implemented
24
+ | 'in_progress' // a pipeline is (visually) running against it
25
+ | 'blocked' // a pipeline step is waiting on a human decision
26
+ | 'pr_ready' // pipeline finished — a PR is open and awaiting merge
27
+ | 'done' // PR merged, implementation complete
28
+
29
+ /** Kind of architecture building block (drives icon + accent). */
30
+ export type BlockType =
31
+ | 'frontend'
32
+ | 'service'
33
+ | 'api'
34
+ | 'database'
35
+ | 'queue'
36
+ | 'integration'
37
+ | 'external'
38
+ | 'environment'
39
+
40
+ /**
41
+ * Where a block sits in the granularity hierarchy. Both `frame` and `module`
42
+ * are containers ("frames") that hold draggable tasks:
43
+ * - `frame` a top-level Service; the only level rendered as a board node
44
+ * - `module` a sub-frame inside a service; created when a task assigned to a
45
+ * not-yet-existing module is implemented (tasks can also be dragged in)
46
+ * - `task` a draggable unit of work living inside a service or a module
47
+ */
48
+ export type BlockLevel = 'frame' | 'module' | 'task'
49
+
50
+ /** A building block dropped on the board. */
51
+ export interface Block {
52
+ id: string
53
+ title: string
54
+ type: BlockType
55
+ description: string
56
+ /** position relative to the parent container (service or module). */
57
+ position: { x: number; y: number }
58
+ status: BlockStatus
59
+ /** 0..1 implementation progress, derived from the running execution. */
60
+ progress: number
61
+ /** ids of tasks that must be implemented before this one (drives arrows). */
62
+ dependsOn: string[]
63
+ /** id of the ExecutionInstance currently running against this block, if any. */
64
+ executionId: string | null
65
+ /** granularity level; absent on legacy/persisted data means `frame`. */
66
+ level: BlockLevel
67
+ /** parent container: service or module for a task, service for a module. */
68
+ parentId: string | null
69
+ /** task-only: 0..1 confidence produced when the pipeline finishes. */
70
+ confidence?: number
71
+ /** task-only: auto-merge the PR when confidence ≥ this threshold (0..1). */
72
+ confidenceThreshold?: number
73
+ /** task-only: the module this task belongs to (created on implement if absent). */
74
+ moduleName?: string
75
+ /** task-only: the features this task implements (definition metadata). */
76
+ features?: string[]
77
+ /** ids of best-practice prompt fragments folded into this block's agent prompts. */
78
+ fragmentIds?: string[]
79
+ /** id of the model (from MODEL_CATALOG) to run this block's agents with; absent = default. */
80
+ modelId?: string
81
+ /** where this block's acceptance / Playwright tests run; absent = no preference. */
82
+ testTarget?: TestTarget
83
+ /** the PR the block's implementer agent opened for its work; absent = none yet. */
84
+ pullRequest?: PullRequestRef
85
+ }
86
+
87
+ /**
88
+ * A lightweight link from a block to the pull request its implementer agent
89
+ * opened. Just enough to display the PR on the board and navigate to it; mirrors
90
+ * `PullRequestRef` in `@cat-factory/contracts`.
91
+ */
92
+ export interface PullRequestRef {
93
+ /** The PR's web URL, opened when the user clicks through from the board. */
94
+ url: string
95
+ /** The PR number within the repo, shown as `#<number>` when known. */
96
+ number?: number
97
+ /** The head branch the agent pushed its work to, when known. */
98
+ branch?: string
99
+ }
100
+
101
+ /**
102
+ * Where a block's acceptance / Playwright tests run:
103
+ * - `github_actions` in the project's CI, against a service spun up in the run
104
+ * - `ephemeral_env` against the provisioned ephemeral environment for the run
105
+ */
106
+ export type TestTarget = 'github_actions' | 'ephemeral_env'
107
+
108
+ /** The kinds of agents available in the agent palette. */
109
+ export type AgentKind =
110
+ | 'architect'
111
+ | 'researcher'
112
+ | 'coder'
113
+ | 'tester'
114
+ | 'reviewer'
115
+ | 'documenter'
116
+ | 'integrator'
117
+ | 'acceptance'
118
+ | 'playwright'
119
+ | 'mocker'
120
+ | 'business-documenter'
121
+ | 'business-reviewer'
122
+
123
+ /** A draggable agent definition shown in the agent palette. */
124
+ export interface AgentArchetype {
125
+ kind: AgentKind
126
+ label: string
127
+ /** iconify name (lucide) */
128
+ icon: string
129
+ /** tailwind-ish accent token used across chips / borders */
130
+ color: string
131
+ description: string
132
+ }
133
+
134
+ /** A reusable, linear sequence of agents. */
135
+ export interface Pipeline {
136
+ id: string
137
+ name: string
138
+ /** ordered agent kinds — the chain executes left to right */
139
+ agentKinds: AgentKind[]
140
+ }
141
+
142
+ /**
143
+ * Spend-safeguard status for the current billing period (a calendar month).
144
+ * Token usage is priced into a single currency and gated by a budget; once
145
+ * `exceeded`, runs are paused and the frontend shows a large warning.
146
+ */
147
+ export interface SpendStatus {
148
+ /** Start of the current billing period (epoch ms). */
149
+ periodStart: number
150
+ inputTokens: number
151
+ outputTokens: number
152
+ /** Estimated spend this period, in `currency`. */
153
+ costSpent: number
154
+ /** Configured budget for one period, in `currency`. */
155
+ costLimit: number
156
+ /** ISO 4217 currency (e.g. 'EUR'). */
157
+ currency: string
158
+ /** True once the budget is reached: execution is paused. */
159
+ exceeded: boolean
160
+ }
161
+
162
+ /** A board/project container owned by the backend. */
163
+ export interface Workspace {
164
+ id: string
165
+ name: string
166
+ createdAt: number
167
+ /** The account this board belongs to, or null for a legacy/unscoped board. */
168
+ accountId: string | null
169
+ }
170
+
171
+ /** Full server-side state of a workspace, returned on load and after resets. */
172
+ export interface WorkspaceSnapshot {
173
+ workspace: Workspace
174
+ blocks: Block[]
175
+ pipelines: Pipeline[]
176
+ executions: ExecutionInstance[]
177
+ /** Bootstrap runs (the unified `agent_runs` bootstrap rows), so the board can
178
+ * render a bootstrap's live progress / failure + retry on load. Absent on
179
+ * older servers. */
180
+ bootstrapJobs?: BootstrapJob[]
181
+ /** Current spend-safeguard status; absent on older servers. */
182
+ spend?: SpendStatus
183
+ }
184
+
185
+ /**
186
+ * Real-time events pushed over the workspace WebSocket stream (see
187
+ * `useWorkspaceStream`). Mirrors `WorkspaceEvent` in `@cat-factory/contracts`.
188
+ */
189
+ export type WorkspaceEvent =
190
+ | { type: 'execution'; instance: ExecutionInstance; block: Block | null; at: number }
191
+ | { type: 'board'; reason: string; at: number }
192
+ | { type: 'bootstrap'; job: BootstrapJob; block: Block | null; at: number }
193
+
194
+ /** Level-of-detail buckets driven by the canvas zoom level. */
195
+ export type LodLevel = 'far' | 'mid' | 'close'
196
+
197
+ /** The signed-in GitHub user, as returned by the backend's /auth/me. */
198
+ export interface AuthUser {
199
+ /** GitHub user id (stable across renames). */
200
+ id: number
201
+ login: string
202
+ name: string | null
203
+ avatarUrl: string | null
204
+ }
205
+
206
+ // Re-export the adjacent domain modules so `~/types/domain` remains the single
207
+ // import surface for the whole frontend.
208
+ export type * from './execution'
209
+ export type * from './models'
210
+ export type * from './fragments'
211
+ export type * from './documents'
212
+ export type * from './tasks'
213
+ export type * from './scenarios'
214
+ export type * from './bootstrap'
215
+ export type * from './github'
216
+ export type * from './accounts'