@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,110 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Execution model: a pipeline of agents running against a single block.
3
+ // Mirrors the `@cat-factory/contracts` execution schemas.
4
+ // ---------------------------------------------------------------------------
5
+
6
+ import type { AgentKind } from './domain'
7
+
8
+ /** Runtime state of a single agent within a running execution. */
9
+ export type AgentState =
10
+ | 'pending' // not started
11
+ | 'working' // actively (visually) working
12
+ | 'waiting_decision' // paused, needs a human decision
13
+ | 'done' // finished
14
+
15
+ /** A decision an agent surfaces mid-step that a human must resolve. */
16
+ export interface Decision {
17
+ id: string
18
+ question: string
19
+ options: string[]
20
+ chosen: string | null
21
+ }
22
+
23
+ /** One entry of a running step's todo list — its label and current status. */
24
+ export interface StepSubtaskItem {
25
+ label: string
26
+ status: 'pending' | 'in_progress' | 'completed'
27
+ }
28
+
29
+ /** Live subtask counts a running step reports from the agent's own todo list. */
30
+ export interface StepSubtasks {
31
+ completed: number
32
+ inProgress: number
33
+ total: number
34
+ /** The individual todo entries, so a zoomed-in card can show the actual list. */
35
+ items?: StepSubtaskItem[]
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Shared "agent run" failure model. Both flows that run an agent in a container
40
+ // — a task pipeline `execution` and a repo `bootstrap` — surface failures with
41
+ // this shape, so the board renders one failure banner + retry for either. Mirrors
42
+ // `agentFailureSchema` in `@cat-factory/contracts`.
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /** The agent flows that produce a container-backed "agent run". */
46
+ export type AgentRunKind = 'bootstrap' | 'execution'
47
+
48
+ /** How an agent run faulted, so the board can classify it (and hint at a retry).
49
+ * The union spans both flows; a given flow only ever produces a subset. */
50
+ export type AgentFailureKind =
51
+ | 'preflight'
52
+ | 'dispatch'
53
+ | 'evicted'
54
+ | 'timeout'
55
+ | 'agent'
56
+ | 'job_failed'
57
+ | 'decision_timeout'
58
+ | 'unknown'
59
+
60
+ /** Structured diagnostics captured when an agent run fails. */
61
+ export interface AgentFailure {
62
+ kind: AgentFailureKind
63
+ /** Human-readable summary (mirrors the run's one-line `error`). */
64
+ message: string
65
+ /** Extended detail when available (the harness's reason, an HTTP body, …). */
66
+ detail: string | null
67
+ /** Where to look next (e.g. "check the container logs for this job id"). */
68
+ hint: string | null
69
+ /** Epoch ms the failure was recorded. */
70
+ occurredAt: number
71
+ /** Last subtask counts seen before the failure, for context (null if none). */
72
+ lastSubtasks: StepSubtasks | null
73
+ }
74
+
75
+ /** One agent's slot in a running pipeline. */
76
+ export interface PipelineStep {
77
+ agentKind: AgentKind
78
+ state: AgentState
79
+ /** 0..1 progress of this individual step */
80
+ progress: number
81
+ /** live "N/M done" subtask counts while an async (container) step runs */
82
+ subtasks?: StepSubtasks
83
+ /** present + unresolved => the step (and block) is blocked */
84
+ decision: Decision | null
85
+ /** text the agent produced for this step (when LLM execution is enabled). */
86
+ output?: string
87
+ /** identifier of the model that produced `output`, for transparency. */
88
+ model?: string
89
+ /** prompt-fragment library ids folded into this step (manual ∪ selector pick). */
90
+ selectedFragmentIds?: string[]
91
+ }
92
+
93
+ /** A pipeline instance running against one block. */
94
+ export interface ExecutionInstance {
95
+ id: string
96
+ blockId: string
97
+ pipelineId: string
98
+ pipelineName: string
99
+ steps: PipelineStep[]
100
+ /** index into steps of the currently active step */
101
+ currentStep: number
102
+ /**
103
+ * 'paused' = halted by the spend safeguard until the budget frees up;
104
+ * 'failed' = the run faulted (see `failure`) — surfaces the shared failure
105
+ * banner + retry, instead of the old success-looking `pr_ready` lie.
106
+ */
107
+ status: 'running' | 'blocked' | 'done' | 'paused' | 'failed'
108
+ /** Structured failure diagnostics when `status` is `failed`; null otherwise. */
109
+ failure?: AgentFailure | null
110
+ }
@@ -0,0 +1,72 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Prompt-fragment library (ADR 0006). Mirrors the `@cat-factory/contracts`
3
+ // fragment-library schemas: the managed, tenant-scoped catalog a workspace
4
+ // resolves (built-in ∪ account ∪ workspace), plus the repo sources that feed it.
5
+ // ---------------------------------------------------------------------------
6
+
7
+ import type { AgentKind, BlockType } from './domain'
8
+ import type { PromptFragment } from './models'
9
+
10
+ /** Which scope owns a managed fragment / source. */
11
+ export type FragmentOwnerKind = 'account' | 'workspace'
12
+
13
+ /** The tier a resolved fragment originates from after override-by-id. */
14
+ export type FragmentTier = 'builtin' | 'account' | 'workspace'
15
+
16
+ /** Inputs for creating a hand-authored fragment at a tier. */
17
+ export interface CreatePromptFragmentInput {
18
+ id?: string
19
+ title: string
20
+ category?: string
21
+ summary: string
22
+ body: string
23
+ tags?: string[]
24
+ appliesTo?: { blockTypes?: BlockType[]; agentKinds?: AgentKind[] }
25
+ version?: string
26
+ }
27
+
28
+ /** Partial patch for editing a fragment at a tier. */
29
+ export type UpdatePromptFragmentInput = Partial<CreatePromptFragmentInput>
30
+
31
+ /** A fragment after the three tiers are merged for a workspace. */
32
+ export interface ResolvedFragment extends PromptFragment {
33
+ tier: FragmentTier
34
+ }
35
+
36
+ /** A repo directory linked as a source of Markdown guideline files. */
37
+ export interface FragmentSource {
38
+ id: string
39
+ ownerKind: FragmentOwnerKind
40
+ ownerId: string
41
+ repoOwner: string
42
+ repoName: string
43
+ gitRef: string
44
+ dirPath: string
45
+ lastSyncedSha: string | null
46
+ lastSyncedAt: number | null
47
+ createdAt: number
48
+ }
49
+
50
+ /** Inputs for linking a repo directory as a fragment source. */
51
+ export interface LinkFragmentSourceInput {
52
+ repoOwner: string
53
+ repoName: string
54
+ gitRef?: string
55
+ dirPath?: string
56
+ }
57
+
58
+ /** Outcome of resyncing a source. */
59
+ export interface FragmentSyncResult {
60
+ upserted: number
61
+ tombstoned: number
62
+ unchanged: number
63
+ lastSyncedSha: string | null
64
+ }
65
+
66
+ /** Cheap "check for changes" result (no writes); powers the resync badge. */
67
+ export interface FragmentSourceStatus {
68
+ changed: boolean
69
+ changedCount: number
70
+ lastSyncedSha: string | null
71
+ remoteSha: string | null
72
+ }
@@ -0,0 +1,153 @@
1
+ // ---------------------------------------------------------------------------
2
+ // GitHub integration. The backend's `GitHubModule` projects GitHub data
3
+ // (repos/branches, pull requests/issues) into D1 and serves it fast and
4
+ // rate-limit-free, alongside connect/resync/write endpoints. These mirror the
5
+ // `@cat-factory/contracts` GitHub schemas so responses drop straight into the
6
+ // Pinia store, just as the document-source types do for Confluence/Notion.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /** A workspace's GitHub App installation, as exposed to clients (no token). */
10
+ export interface GitHubConnection {
11
+ installationId: number
12
+ accountLogin: string
13
+ targetType: 'Organization' | 'User'
14
+ connectedAt: number
15
+ /**
16
+ * Whether cat-factory can create repos under this account itself (privileged
17
+ * App tier). When true, the bootstrap UI drops the manual "create on GitHub"
18
+ * step. Defaults to false for older backends that don't send it.
19
+ */
20
+ canCreateRepos?: boolean
21
+ }
22
+
23
+ /**
24
+ * A discoverable App installation for the connect picker. `connected` says
25
+ * whether it's already bound: to THIS workspace, to ANOTHER (so connecting would
26
+ * be rejected), or to NONE (free to connect).
27
+ */
28
+ export interface GitHubInstallationOption {
29
+ installationId: number
30
+ accountLogin: string
31
+ targetType: 'Organization' | 'User'
32
+ accountAvatarUrl: string | null
33
+ connected: 'this' | 'other' | 'none'
34
+ }
35
+
36
+ /** A repository the integration tracks for a workspace. */
37
+ export interface GitHubRepo {
38
+ githubId: number
39
+ installationId: number
40
+ owner: string
41
+ name: string
42
+ defaultBranch: string | null
43
+ private: boolean
44
+ /** Optional link to a board block this repo backs. */
45
+ blockId: string | null
46
+ syncedAt: number
47
+ }
48
+
49
+ export interface GitHubBranch {
50
+ repoGithubId: number
51
+ name: string
52
+ headSha: string
53
+ protected: boolean
54
+ syncedAt: number
55
+ }
56
+
57
+ /**
58
+ * A repo the connected installation can access, annotated with whether the
59
+ * current workspace links it. Drives the per-workspace repo picker — repos are
60
+ * linked explicitly per board, since the installation is shared across an
61
+ * account's workspaces.
62
+ */
63
+ export interface GitHubAvailableRepo {
64
+ githubId: number
65
+ owner: string
66
+ name: string
67
+ defaultBranch: string | null
68
+ private: boolean
69
+ linked: boolean
70
+ }
71
+
72
+ export type GitHubPullRequestState = 'open' | 'closed'
73
+
74
+ export interface GitHubPullRequest {
75
+ repoGithubId: number
76
+ number: number
77
+ githubId: number
78
+ title: string
79
+ state: GitHubPullRequestState
80
+ headRef: string | null
81
+ baseRef: string | null
82
+ headSha: string | null
83
+ merged: boolean
84
+ author: string | null
85
+ updatedAt: number | null
86
+ syncedAt: number
87
+ }
88
+
89
+ export type GitHubIssueState = 'open' | 'closed'
90
+
91
+ export interface GitHubIssue {
92
+ repoGithubId: number
93
+ number: number
94
+ githubId: number
95
+ title: string
96
+ state: GitHubIssueState
97
+ author: string | null
98
+ labels: string[]
99
+ updatedAt: number | null
100
+ syncedAt: number
101
+ }
102
+
103
+ // ---- request inputs -------------------------------------------------------
104
+
105
+ /** Trigger a resync. Defaults to an incremental resync of all tracked repos. */
106
+ export interface ResyncRequest {
107
+ /** Limit the resync to a single repo (by its GitHub numeric id). */
108
+ repoGithubId?: number
109
+ /** Run a full backfill (durable Workflow) instead of an incremental pass. */
110
+ full?: boolean
111
+ }
112
+
113
+ export interface CreateBranchInput {
114
+ name: string
115
+ fromSha: string
116
+ }
117
+
118
+ /** Body for programmatic repo creation (privileged App tier). */
119
+ export interface CreateRepoRequest {
120
+ /** A single GitHub name segment — no "owner/" prefix. */
121
+ name: string
122
+ private?: boolean
123
+ description?: string
124
+ }
125
+
126
+ /** The freshly-created repository returned by the create-repo endpoint. */
127
+ export interface CreatedRepo {
128
+ githubId: number
129
+ owner: string
130
+ name: string
131
+ defaultBranch: string | null
132
+ private: boolean
133
+ }
134
+
135
+ export interface CommitFilesInput {
136
+ branch: string
137
+ message: string
138
+ files: { path: string; content: string }[]
139
+ /** Parent commit to build on; defaults to the branch tip. */
140
+ baseSha?: string
141
+ }
142
+
143
+ export interface OpenPullRequestInput {
144
+ title: string
145
+ head: string
146
+ base: string
147
+ body?: string
148
+ draft?: boolean
149
+ }
150
+
151
+ export interface MergePullRequestInput {
152
+ method?: 'merge' | 'squash' | 'rebase'
153
+ }
@@ -0,0 +1,48 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Model selection & best-practice prompt fragments. Mirrors the
3
+ // `@cat-factory/contracts` schemas served read-only by the backend.
4
+ // ---------------------------------------------------------------------------
5
+
6
+ import type { AgentKind, BlockType } from './domain'
7
+
8
+ /**
9
+ * A selectable LLM model, resolved to the flavour actually in use for this
10
+ * deployment (served by `GET /models`). `flavor` is `direct` when the model's
11
+ * own provider key is configured, else `cloudflare`. Mirrors `ModelOption` in
12
+ * `@cat-factory/contracts`.
13
+ */
14
+ export interface ModelOption {
15
+ id: string
16
+ label: string
17
+ description: string
18
+ flavor: 'cloudflare' | 'direct'
19
+ providerLabel: string
20
+ provider: string
21
+ model: string
22
+ }
23
+
24
+ /**
25
+ * A curated best-practice "prompt fragment" served read-only by the backend
26
+ * (`GET /prompt-fragments`). Users pick which apply to a block; the backend folds
27
+ * the selected fragments' bodies into the agent system prompt at run time.
28
+ */
29
+ export interface PromptFragment {
30
+ id: string
31
+ version: string
32
+ title: string
33
+ category: string
34
+ summary: string
35
+ body: string
36
+ appliesTo?: {
37
+ blockTypes?: BlockType[]
38
+ agentKinds?: AgentKind[]
39
+ }
40
+ /** Free-form tags the relevance selector uses (managed/sourced fragments only). */
41
+ tags?: string[]
42
+ /** Provenance when sourced from a repo; absent for built-in/hand-authored. */
43
+ source?: {
44
+ sourceId: string
45
+ path: string
46
+ sha: string
47
+ }
48
+ }
@@ -0,0 +1,38 @@
1
+ // Requirements-review wire types. Mirror of `@cat-factory/contracts`'
2
+ // requirements.ts, kept in sync by hand like the rest of `~/types/*` (the SPA
3
+ // does not import the backend package directly).
4
+ //
5
+ // A stateless reviewer agent inspects a block's collected requirements and
6
+ // raises questions / gaps / clarifications; a human answers or dismisses each;
7
+ // then the agent folds the answers back into the block's requirements.
8
+
9
+ export type ReviewItemCategory = 'gap' | 'clarification' | 'assumption' | 'risk' | 'question'
10
+
11
+ export type ReviewItemSeverity = 'low' | 'medium' | 'high'
12
+
13
+ export type ReviewItemStatus = 'open' | 'answered' | 'resolved' | 'dismissed'
14
+
15
+ export interface RequirementReviewItem {
16
+ id: string
17
+ category: ReviewItemCategory
18
+ severity: ReviewItemSeverity
19
+ title: string
20
+ detail: string
21
+ status: ReviewItemStatus
22
+ reply: string | null
23
+ createdAt: number
24
+ updatedAt: number
25
+ }
26
+
27
+ export type RequirementReviewStatus = 'ready' | 'incorporated'
28
+
29
+ export interface RequirementReview {
30
+ id: string
31
+ blockId: string
32
+ status: RequirementReviewStatus
33
+ items: RequirementReviewItem[]
34
+ model: string | null
35
+ incorporatedRequirements: string | null
36
+ createdAt: number
37
+ updatedAt: number
38
+ }
@@ -0,0 +1,36 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Acceptance test scenarios.
3
+ //
4
+ // A feature-scoped, black-box acceptance scenario in Given / When / Then form.
5
+ // These are what the `acceptance` agent produces from a block's requirements
6
+ // (description + linked PRDs), and what the `playwright` agent turns into
7
+ // end-to-end tests. The board is server-owned, but — like the agent palette —
8
+ // scenarios are an editable, client-side prototype surface, persisted locally so
9
+ // a user can author and refine the set for a feature before wiring up real runs.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /** Where a scenario came from: drafted by the acceptance agent or hand-written. */
13
+ export type ScenarioSource = 'generated' | 'manual'
14
+
15
+ /** Review state of a scenario in its feature's current set. */
16
+ export type ScenarioStatus = 'draft' | 'approved'
17
+
18
+ /** A single acceptance scenario for a feature. */
19
+ export interface AcceptanceScenario {
20
+ id: string
21
+ /** The feature (matching `Block.features`) this scenario verifies. */
22
+ feature: string
23
+ /** Short, human title — also the name of the generated Playwright test. */
24
+ title: string
25
+ /** Preconditions (one clause per entry). */
26
+ given: string[]
27
+ /** The user action under test (kept to a single clear action). */
28
+ when: string[]
29
+ /** Observable, asserted outcomes. */
30
+ then: string[]
31
+ status: ScenarioStatus
32
+ source: ScenarioSource
33
+ /** True once a Playwright test has been generated for this scenario. */
34
+ hasPlaywrightTest: boolean
35
+ createdAt: number
36
+ }
@@ -0,0 +1,67 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Task-source integration. Individual issues imported from external task
3
+ // trackers (Jira, …) can be attached to a board task as agent context. These
4
+ // mirror the `@cat-factory/contracts` task schemas; the abstraction is
5
+ // source-agnostic, keyed by `source`. Unlike document sources there is no
6
+ // plan/spawn — an issue is linked for context, never expanded into structure.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import type { CredentialField } from './documents'
10
+
11
+ /** The external task trackers cat-factory can link to. */
12
+ export type TaskSourceKind = 'jira'
13
+
14
+ export type { CredentialField }
15
+
16
+ /** A source's self-description: drives the generic connect + import UI. */
17
+ export interface TaskSourceDescriptor {
18
+ source: TaskSourceKind
19
+ label: string
20
+ /** Lucide icon name for the source. */
21
+ icon: string
22
+ credentialFields: CredentialField[]
23
+ refLabel: string
24
+ refPlaceholder: string
25
+ }
26
+
27
+ /** A workspace's connection to a task source (never carries credentials). */
28
+ export interface TaskConnection {
29
+ source: TaskSourceKind
30
+ /** Human-friendly label for what we're connected to (site URL). */
31
+ label: string
32
+ /** When the connection was established (epoch ms). */
33
+ connectedAt: number
34
+ }
35
+
36
+ /** A single comment on an issue, with its body as lightweight Markdown. */
37
+ export interface TaskComment {
38
+ author: string
39
+ createdAt: string
40
+ body: string
41
+ }
42
+
43
+ /** An issue imported from a source into the workspace, as a structured record. */
44
+ export interface SourceTask {
45
+ source: TaskSourceKind
46
+ /** The source's canonical key for the issue (e.g. `PROJ-123`). */
47
+ externalId: string
48
+ title: string
49
+ url: string
50
+ /** Workflow status name, e.g. `In Progress`. */
51
+ status: string
52
+ /** Issue type name, e.g. `Bug`. */
53
+ type: string
54
+ /** Assignee display name, or null when unassigned. */
55
+ assignee: string | null
56
+ /** Priority name, or null when none. */
57
+ priority: string | null
58
+ labels: string[]
59
+ /** Issue description as lightweight Markdown. */
60
+ description: string
61
+ comments: TaskComment[]
62
+ /** Short plain-text preview of the issue. */
63
+ excerpt: string
64
+ /** The board block this issue is attached to as context, if any. */
65
+ linkedBlockId: string | null
66
+ syncedAt: number
67
+ }
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import type { AgentKind, BlockStatus, BlockType } from '~/types/domain'
3
+ import {
4
+ AGENT_ARCHETYPES,
5
+ AGENT_BY_KIND,
6
+ BLOCK_TYPE_META,
7
+ STATUS_META,
8
+ DEFAULT_CONFIDENCE_THRESHOLD,
9
+ uid,
10
+ } from '~/utils/catalog'
11
+
12
+ const AGENT_KINDS: AgentKind[] = [
13
+ 'architect',
14
+ 'researcher',
15
+ 'coder',
16
+ 'tester',
17
+ 'reviewer',
18
+ 'documenter',
19
+ 'integrator',
20
+ 'acceptance',
21
+ 'playwright',
22
+ 'mocker',
23
+ 'business-documenter',
24
+ 'business-reviewer',
25
+ ]
26
+ const BLOCK_TYPES: BlockType[] = [
27
+ 'frontend',
28
+ 'service',
29
+ 'api',
30
+ 'database',
31
+ 'queue',
32
+ 'integration',
33
+ 'external',
34
+ ]
35
+ const BLOCK_STATUSES: BlockStatus[] = [
36
+ 'planned',
37
+ 'ready',
38
+ 'in_progress',
39
+ 'blocked',
40
+ 'pr_ready',
41
+ 'done',
42
+ ]
43
+
44
+ describe('catalog', () => {
45
+ it('indexes every archetype by its kind', () => {
46
+ expect(Object.keys(AGENT_BY_KIND).sort()).toEqual([...AGENT_KINDS].sort())
47
+ for (const a of AGENT_ARCHETYPES) {
48
+ expect(AGENT_BY_KIND[a.kind]).toBe(a)
49
+ }
50
+ })
51
+
52
+ it('provides metadata for every block type', () => {
53
+ for (const t of BLOCK_TYPES) {
54
+ expect(BLOCK_TYPE_META[t]).toMatchObject({
55
+ label: expect.any(String),
56
+ icon: expect.any(String),
57
+ accent: expect.any(String),
58
+ })
59
+ }
60
+ })
61
+
62
+ it('provides metadata for every block status', () => {
63
+ for (const s of BLOCK_STATUSES) {
64
+ expect(STATUS_META[s]).toMatchObject({
65
+ label: expect.any(String),
66
+ color: expect.any(String),
67
+ chip: expect.any(String),
68
+ icon: expect.any(String),
69
+ })
70
+ }
71
+ })
72
+
73
+ it('keeps the default confidence threshold within 0..1', () => {
74
+ expect(DEFAULT_CONFIDENCE_THRESHOLD).toBeGreaterThan(0)
75
+ expect(DEFAULT_CONFIDENCE_THRESHOLD).toBeLessThanOrEqual(1)
76
+ })
77
+
78
+ it('uid produces prefixed, unique-ish ids', () => {
79
+ expect(uid('blk')).toMatch(/^blk_[a-z0-9]+$/)
80
+ expect(uid('blk')).not.toBe(uid('blk'))
81
+ })
82
+ })