@cat-factory/app 0.6.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 (189) 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 +143 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AddTaskModal.vue +444 -0
  10. package/app/components/board/AgentFailureCard.vue +97 -0
  11. package/app/components/board/AgentStopButton.vue +61 -0
  12. package/app/components/board/BoardCanvas.vue +183 -0
  13. package/app/components/board/ContextPicker.vue +367 -0
  14. package/app/components/board/RecurringPipelineModal.vue +219 -0
  15. package/app/components/board/TaskDependencyEdges.vue +132 -0
  16. package/app/components/board/nodes/AgentChip.vue +59 -0
  17. package/app/components/board/nodes/BlockNode.vue +433 -0
  18. package/app/components/board/nodes/DecisionBadge.vue +27 -0
  19. package/app/components/board/nodes/DraggableTask.vue +48 -0
  20. package/app/components/board/nodes/ModuleFrame.vue +97 -0
  21. package/app/components/board/nodes/TaskCard.vue +359 -0
  22. package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
  23. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  24. package/app/components/clarity/ClarityReviewWindow.vue +611 -0
  25. package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
  26. package/app/components/documents/DocumentImportModal.vue +161 -0
  27. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  28. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  29. package/app/components/documents/TaskContextDocs.vue +83 -0
  30. package/app/components/focus/BlockFocusView.vue +171 -0
  31. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  32. package/app/components/gates/GateResultView.vue +282 -0
  33. package/app/components/github/AddServiceFromRepoModal.vue +354 -0
  34. package/app/components/github/GitHubConnect.vue +183 -0
  35. package/app/components/github/GitHubOnboarding.vue +45 -0
  36. package/app/components/github/GitHubPanel.vue +584 -0
  37. package/app/components/github/RepoTreeBrowser.vue +171 -0
  38. package/app/components/layout/AccountTeamSettings.vue +237 -0
  39. package/app/components/layout/BoardSwitcher.vue +280 -0
  40. package/app/components/layout/BoardToolbar.vue +156 -0
  41. package/app/components/layout/CommandBar.vue +336 -0
  42. package/app/components/layout/GitHubPatBanner.vue +73 -0
  43. package/app/components/layout/NotificationsInbox.vue +175 -0
  44. package/app/components/layout/SideBar.vue +314 -0
  45. package/app/components/layout/SpendWarningBanner.vue +107 -0
  46. package/app/components/observability/StepMetricsBar.vue +102 -0
  47. package/app/components/palettes/AgentPalette.vue +86 -0
  48. package/app/components/panels/AgentStepDetail.vue +737 -0
  49. package/app/components/panels/DecisionModal.vue +71 -0
  50. package/app/components/panels/InspectorPanel.vue +465 -0
  51. package/app/components/panels/ObservabilityPanel.vue +351 -0
  52. package/app/components/panels/StepMetadataCard.vue +253 -0
  53. package/app/components/panels/StepRestartControl.vue +90 -0
  54. package/app/components/panels/StepResultViewHost.vue +40 -0
  55. package/app/components/panels/StepTestReport.vue +84 -0
  56. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  57. package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
  58. package/app/components/panels/inspector/ServiceFragments.vue +82 -0
  59. package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
  60. package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
  61. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  62. package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
  63. package/app/components/panels/inspector/TaskExecution.vue +364 -0
  64. package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
  65. package/app/components/panels/inspector/TaskStructure.vue +96 -0
  66. package/app/components/pipeline/AgentKindIcon.vue +30 -0
  67. package/app/components/pipeline/IterationCapPrompt.vue +70 -0
  68. package/app/components/pipeline/PipelineBuilder.vue +817 -0
  69. package/app/components/pipeline/PipelineProgress.vue +484 -0
  70. package/app/components/providers/ApiKeysSection.vue +273 -0
  71. package/app/components/providers/PersonalCredentialModal.vue +128 -0
  72. package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
  73. package/app/components/providers/VendorCredentialsModal.vue +197 -0
  74. package/app/components/recurring/RecurrenceEditor.vue +124 -0
  75. package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
  76. package/app/components/settings/DatadogPanel.vue +213 -0
  77. package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
  78. package/app/components/settings/MergeThresholdsPanel.vue +378 -0
  79. package/app/components/settings/ModelDefaultsPanel.vue +250 -0
  80. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
  81. package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
  82. package/app/components/slack/SlackPanel.vue +299 -0
  83. package/app/components/tasks/TaskContextIssues.vue +88 -0
  84. package/app/components/tasks/TaskImportModal.vue +207 -0
  85. package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
  86. package/app/components/testing/TestReportWindow.vue +404 -0
  87. package/app/composables/api/accounts.ts +81 -0
  88. package/app/composables/api/auth.ts +45 -0
  89. package/app/composables/api/board.ts +101 -0
  90. package/app/composables/api/bootstrap.ts +62 -0
  91. package/app/composables/api/context.ts +25 -0
  92. package/app/composables/api/documents.ts +74 -0
  93. package/app/composables/api/execution.ts +127 -0
  94. package/app/composables/api/fragments.ts +71 -0
  95. package/app/composables/api/github.ts +131 -0
  96. package/app/composables/api/models.ts +127 -0
  97. package/app/composables/api/notifications.ts +23 -0
  98. package/app/composables/api/presets.ts +29 -0
  99. package/app/composables/api/recurring.ts +68 -0
  100. package/app/composables/api/releaseHealth.ts +43 -0
  101. package/app/composables/api/reviews.ts +146 -0
  102. package/app/composables/api/slack.ts +54 -0
  103. package/app/composables/api/tasks.ts +72 -0
  104. package/app/composables/api/workspaces.ts +36 -0
  105. package/app/composables/useApi.ts +89 -0
  106. package/app/composables/useBlockDrag.ts +90 -0
  107. package/app/composables/useBlockQueries.ts +154 -0
  108. package/app/composables/useBoardFlow.ts +11 -0
  109. package/app/composables/useContextLinking.ts +65 -0
  110. package/app/composables/useDepLabels.ts +26 -0
  111. package/app/composables/useFrameResize.ts +54 -0
  112. package/app/composables/useResultView.ts +48 -0
  113. package/app/composables/useReviewStage.ts +40 -0
  114. package/app/composables/useSemanticZoom.ts +31 -0
  115. package/app/composables/useStepApproval.ts +233 -0
  116. package/app/composables/useStepProse.ts +78 -0
  117. package/app/composables/useStepTimer.ts +63 -0
  118. package/app/composables/useTaskExpansion.ts +92 -0
  119. package/app/composables/useWorkspaceStream.ts +155 -0
  120. package/app/docs/architecture.md +31 -0
  121. package/app/pages/index.vue +141 -0
  122. package/app/stores/accounts.ts +152 -0
  123. package/app/stores/agentConfig.ts +35 -0
  124. package/app/stores/agentRuns.ts +122 -0
  125. package/app/stores/agents.ts +40 -0
  126. package/app/stores/apiKeys.ts +108 -0
  127. package/app/stores/auth.ts +166 -0
  128. package/app/stores/board.spec.ts +205 -0
  129. package/app/stores/board.ts +286 -0
  130. package/app/stores/bootstrap.ts +97 -0
  131. package/app/stores/clarity.ts +196 -0
  132. package/app/stores/consensus.ts +60 -0
  133. package/app/stores/documents.ts +176 -0
  134. package/app/stores/execution.ts +273 -0
  135. package/app/stores/fragmentLibrary.ts +147 -0
  136. package/app/stores/fragments.ts +40 -0
  137. package/app/stores/github.ts +305 -0
  138. package/app/stores/localModels.ts +51 -0
  139. package/app/stores/mergePresets.ts +58 -0
  140. package/app/stores/modelDefaults.ts +76 -0
  141. package/app/stores/models.ts +134 -0
  142. package/app/stores/notifications.ts +70 -0
  143. package/app/stores/observability.ts +144 -0
  144. package/app/stores/personalSubscriptions.ts +215 -0
  145. package/app/stores/pipelines.ts +327 -0
  146. package/app/stores/recurringPipelines.ts +112 -0
  147. package/app/stores/releaseHealth.ts +75 -0
  148. package/app/stores/requirements.spec.ts +94 -0
  149. package/app/stores/requirements.ts +208 -0
  150. package/app/stores/serviceFragmentDefaults.ts +29 -0
  151. package/app/stores/services.ts +87 -0
  152. package/app/stores/slack.ts +142 -0
  153. package/app/stores/taskExpansion.ts +36 -0
  154. package/app/stores/tasks.spec.ts +71 -0
  155. package/app/stores/tasks.ts +176 -0
  156. package/app/stores/tracker.ts +27 -0
  157. package/app/stores/ui.ts +434 -0
  158. package/app/stores/vendorCredentials.ts +54 -0
  159. package/app/stores/workspace.ts +215 -0
  160. package/app/stores/workspaceSettings.ts +36 -0
  161. package/app/types/accounts.ts +77 -0
  162. package/app/types/bootstrap.ts +83 -0
  163. package/app/types/clarity.ts +59 -0
  164. package/app/types/consensus.ts +91 -0
  165. package/app/types/documents.ts +104 -0
  166. package/app/types/domain.ts +495 -0
  167. package/app/types/execution.ts +383 -0
  168. package/app/types/fragments.ts +72 -0
  169. package/app/types/github.ts +173 -0
  170. package/app/types/localModels.ts +73 -0
  171. package/app/types/merge.ts +71 -0
  172. package/app/types/models.ts +157 -0
  173. package/app/types/notifications.ts +74 -0
  174. package/app/types/recurring.ts +69 -0
  175. package/app/types/releaseHealth.ts +31 -0
  176. package/app/types/requirements.ts +61 -0
  177. package/app/types/services.ts +27 -0
  178. package/app/types/slack.ts +57 -0
  179. package/app/types/tasks.ts +82 -0
  180. package/app/types/tracker.ts +18 -0
  181. package/app/utils/agentOutput.spec.ts +128 -0
  182. package/app/utils/agentOutput.ts +173 -0
  183. package/app/utils/catalog.spec.ts +112 -0
  184. package/app/utils/catalog.ts +455 -0
  185. package/app/utils/dnd.ts +29 -0
  186. package/app/utils/observability.ts +52 -0
  187. package/app/utils/pipelineRender.ts +151 -0
  188. package/nuxt.config.ts +55 -0
  189. package/package.json +45 -0
@@ -0,0 +1,68 @@
1
+ import type { Service, WorkspaceMount } from '~/types/domain'
2
+ import type {
3
+ CreateScheduleInput,
4
+ PipelineSchedule,
5
+ ScheduleRun,
6
+ UpdateScheduleInput,
7
+ } from '~/types/recurring'
8
+ import type { ApiContext, Position } from './context'
9
+
10
+ /** Recurring (scheduled) pipelines + the in-org shared-service mount catalog. */
11
+ export function recurringApi({ http, ws }: ApiContext) {
12
+ return {
13
+ // ---- recurring pipelines (scheduled runs against a service) -----------
14
+ listRecurringPipelines: (workspaceId: string) =>
15
+ http<PipelineSchedule[]>(`${ws(workspaceId)}/recurring-pipelines`),
16
+
17
+ createRecurringPipeline: (workspaceId: string, body: CreateScheduleInput) =>
18
+ http<PipelineSchedule>(`${ws(workspaceId)}/recurring-pipelines`, { method: 'POST', body }),
19
+
20
+ updateRecurringPipeline: (workspaceId: string, id: string, body: UpdateScheduleInput) =>
21
+ http<PipelineSchedule>(`${ws(workspaceId)}/recurring-pipelines/${encodeURIComponent(id)}`, {
22
+ method: 'PATCH',
23
+ body,
24
+ }),
25
+
26
+ deleteRecurringPipeline: (workspaceId: string, id: string) =>
27
+ http(`${ws(workspaceId)}/recurring-pipelines/${encodeURIComponent(id)}`, {
28
+ method: 'DELETE',
29
+ }),
30
+
31
+ listScheduleRuns: (workspaceId: string, id: string) =>
32
+ http<ScheduleRun[]>(`${ws(workspaceId)}/recurring-pipelines/${encodeURIComponent(id)}/runs`),
33
+
34
+ runScheduleNow: (workspaceId: string, id: string) =>
35
+ http<PipelineSchedule>(
36
+ `${ws(workspaceId)}/recurring-pipelines/${encodeURIComponent(id)}/run-now`,
37
+ { method: 'POST' },
38
+ ),
39
+
40
+ // ---- in-org shared services (mount/unmount + org catalog) -------------
41
+ // The services this workspace mounts, and the org catalog it can mount from. A 503
42
+ // means the feature isn't wired (the store hides its UI on any error here).
43
+ listServiceMounts: (workspaceId: string) =>
44
+ http<WorkspaceMount[]>(`${ws(workspaceId)}/services`),
45
+
46
+ listServiceCatalog: (workspaceId: string) =>
47
+ http<Service[]>(`${ws(workspaceId)}/services/catalog`),
48
+
49
+ mountService: (workspaceId: string, serviceId: string, body: { position?: Position } = {}) =>
50
+ http<WorkspaceMount>(`${ws(workspaceId)}/services/${encodeURIComponent(serviceId)}`, {
51
+ method: 'POST',
52
+ body,
53
+ }),
54
+
55
+ unmountService: (workspaceId: string, serviceId: string) =>
56
+ http(`${ws(workspaceId)}/services/${encodeURIComponent(serviceId)}`, { method: 'DELETE' }),
57
+
58
+ updateMountLayout: (
59
+ workspaceId: string,
60
+ serviceId: string,
61
+ body: { position?: Position; size?: { w: number; h: number } | null },
62
+ ) =>
63
+ http<WorkspaceMount>(`${ws(workspaceId)}/services/${encodeURIComponent(serviceId)}/layout`, {
64
+ method: 'PATCH',
65
+ body,
66
+ }),
67
+ }
68
+ }
@@ -0,0 +1,43 @@
1
+ import type {
2
+ DatadogConnectionView,
3
+ ReleaseHealthConfig,
4
+ UpsertDatadogConnectionInput,
5
+ UpsertReleaseHealthConfigInput,
6
+ } from '~/types/releaseHealth'
7
+ import type { ApiContext } from './context'
8
+
9
+ /** Datadog post-release-health: the connection + per-block monitor/SLO mapping. */
10
+ export function releaseHealthApi({ http, ws }: ApiContext) {
11
+ return {
12
+ // ---- Datadog post-release-health settings -----------------------------
13
+ getDatadogConnection: (workspaceId: string) =>
14
+ http<DatadogConnectionView>(`${ws(workspaceId)}/datadog/connection`),
15
+
16
+ setDatadogConnection: (workspaceId: string, body: UpsertDatadogConnectionInput) =>
17
+ http<DatadogConnectionView>(`${ws(workspaceId)}/datadog/connection`, {
18
+ method: 'PUT',
19
+ body,
20
+ }),
21
+
22
+ deleteDatadogConnection: (workspaceId: string) =>
23
+ http(`${ws(workspaceId)}/datadog/connection`, { method: 'DELETE' }),
24
+
25
+ listReleaseHealthConfigs: (workspaceId: string) =>
26
+ http<ReleaseHealthConfig[]>(`${ws(workspaceId)}/release-health-configs`),
27
+
28
+ upsertReleaseHealthConfig: (
29
+ workspaceId: string,
30
+ blockId: string,
31
+ body: UpsertReleaseHealthConfigInput,
32
+ ) =>
33
+ http<ReleaseHealthConfig>(
34
+ `${ws(workspaceId)}/release-health-configs/${encodeURIComponent(blockId)}`,
35
+ { method: 'PUT', body },
36
+ ),
37
+
38
+ deleteReleaseHealthConfig: (workspaceId: string, blockId: string) =>
39
+ http(`${ws(workspaceId)}/release-health-configs/${encodeURIComponent(blockId)}`, {
40
+ method: 'DELETE',
41
+ }),
42
+ }
43
+ }
@@ -0,0 +1,146 @@
1
+ import type { ClarityReview, ResolveClarityExceededChoice } from '~/types/clarity'
2
+ import type { ConsensusSession } from '~/types/consensus'
3
+ import type {
4
+ RequirementReview,
5
+ ResolveRequirementsExceededChoice,
6
+ ReviewItemStatus,
7
+ } from '~/types/requirements'
8
+ import type { ApiContext } from './context'
9
+
10
+ /**
11
+ * The two iterative gate reviewers (requirements + clarity) and the consensus
12
+ * session read. Each reviewer follows the same answer → incorporate → re-review
13
+ * → proceed/resolve-exceeded loop.
14
+ */
15
+ export function reviewsApi({ http, ws }: ApiContext) {
16
+ return {
17
+ // ---- requirements review (stateless reviewer agent) ------------------
18
+ // The current review for a block (null when none has been run). A 503 means
19
+ // the feature is unconfigured (the panel hides on any error here).
20
+ getRequirementReview: (workspaceId: string, blockId: string) =>
21
+ http<RequirementReview | null>(
22
+ `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review`,
23
+ ),
24
+
25
+ // The latest consensus session for a block (`{ session: null }` when none / consensus
26
+ // off). The live transcript also arrives via the `consensus` stream event.
27
+ getConsensusSession: (workspaceId: string, blockId: string) =>
28
+ http<{ session: ConsensusSession | null }>(
29
+ `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/consensus-session`,
30
+ ),
31
+
32
+ replyRequirementItem: (workspaceId: string, reviewId: string, itemId: string, reply: string) =>
33
+ http<RequirementReview>(
34
+ `${ws(workspaceId)}/requirement-reviews/${encodeURIComponent(reviewId)}/items/${encodeURIComponent(itemId)}/reply`,
35
+ { method: 'POST', body: { reply } },
36
+ ),
37
+
38
+ setRequirementItemStatus: (
39
+ workspaceId: string,
40
+ reviewId: string,
41
+ itemId: string,
42
+ status: ReviewItemStatus,
43
+ ) =>
44
+ http<RequirementReview>(
45
+ `${ws(workspaceId)}/requirement-reviews/${encodeURIComponent(reviewId)}/items/${encodeURIComponent(itemId)}`,
46
+ { method: 'PATCH', body: { status } },
47
+ ),
48
+
49
+ // Incorporate the answers ASYNCHRONOUSLY (every finding must be answered or dismissed).
50
+ // The durable driver folds them and re-reviews in the background. Optional `feedback` is
51
+ // the "do it differently" lever when redoing a merge. Returns the `incorporating` review
52
+ // at once; a notification calls the user back only if the re-review needs input.
53
+ incorporateRequirements: (workspaceId: string, blockId: string, feedback?: string) =>
54
+ http<RequirementReview>(
55
+ `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review/incorporate`,
56
+ { method: 'POST', body: feedback ? { feedback } : {} },
57
+ ),
58
+
59
+ // Re-review the incorporated document (one more reviewer pass). On convergence the
60
+ // parked run advances; otherwise the response carries the next cycle / cap state.
61
+ reReviewRequirements: (workspaceId: string, blockId: string) =>
62
+ http<RequirementReview>(
63
+ `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review/re-review`,
64
+ { method: 'POST' },
65
+ ),
66
+
67
+ // Proceed: settle the requirements and advance the parked run (all findings dismissed).
68
+ proceedRequirements: (workspaceId: string, blockId: string) =>
69
+ http<RequirementReview>(
70
+ `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review/proceed`,
71
+ { method: 'POST' },
72
+ ),
73
+
74
+ // Resolve a review that hit its iteration cap: extra-round / proceed / stop-reset.
75
+ resolveRequirementsExceeded: (
76
+ workspaceId: string,
77
+ blockId: string,
78
+ choice: ResolveRequirementsExceededChoice,
79
+ ) =>
80
+ http<RequirementReview>(
81
+ `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review/resolve-exceeded`,
82
+ { method: 'POST', body: { choice } },
83
+ ),
84
+
85
+ // ---- clarity review (bug-report triage reviewer agent) ---------------
86
+ // The current review for a block (null when none has been run). A 503 means
87
+ // the feature is unconfigured (the panel hides on any error here).
88
+ getClarityReview: (workspaceId: string, blockId: string) =>
89
+ http<ClarityReview | null>(
90
+ `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/clarity-review`,
91
+ ),
92
+
93
+ replyClarityItem: (workspaceId: string, reviewId: string, itemId: string, reply: string) =>
94
+ http<ClarityReview>(
95
+ `${ws(workspaceId)}/clarity-reviews/${encodeURIComponent(reviewId)}/items/${encodeURIComponent(itemId)}/reply`,
96
+ { method: 'POST', body: { reply } },
97
+ ),
98
+
99
+ setClarityItemStatus: (
100
+ workspaceId: string,
101
+ reviewId: string,
102
+ itemId: string,
103
+ status: ReviewItemStatus,
104
+ ) =>
105
+ http<ClarityReview>(
106
+ `${ws(workspaceId)}/clarity-reviews/${encodeURIComponent(reviewId)}/items/${encodeURIComponent(itemId)}`,
107
+ { method: 'PATCH', body: { status } },
108
+ ),
109
+
110
+ // Incorporate the answers ASYNCHRONOUSLY (every finding must be answered or dismissed).
111
+ // The durable driver folds them and re-reviews in the background. Optional `feedback` is
112
+ // the "do it differently" lever when redoing a merge. Returns the `incorporating` review
113
+ // at once; a notification calls the user back only if the re-review needs input.
114
+ incorporateClarity: (workspaceId: string, blockId: string, feedback?: string) =>
115
+ http<ClarityReview>(
116
+ `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/clarity-review/incorporate`,
117
+ { method: 'POST', body: feedback ? { feedback } : {} },
118
+ ),
119
+
120
+ // Re-review the clarified report (one more reviewer pass). On convergence the parked run
121
+ // advances; otherwise the response carries the next cycle / cap state.
122
+ reReviewClarity: (workspaceId: string, blockId: string) =>
123
+ http<ClarityReview>(
124
+ `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/clarity-review/re-review`,
125
+ { method: 'POST' },
126
+ ),
127
+
128
+ // Proceed: settle the clarity review and advance the parked run (all findings dismissed).
129
+ proceedClarity: (workspaceId: string, blockId: string) =>
130
+ http<ClarityReview>(
131
+ `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/clarity-review/proceed`,
132
+ { method: 'POST' },
133
+ ),
134
+
135
+ // Resolve a review that hit its iteration cap: extra-round / proceed / stop-reset.
136
+ resolveClarityExceeded: (
137
+ workspaceId: string,
138
+ blockId: string,
139
+ choice: ResolveClarityExceededChoice,
140
+ ) =>
141
+ http<ClarityReview>(
142
+ `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/clarity-review/resolve-exceeded`,
143
+ { method: 'POST', body: { choice } },
144
+ ),
145
+ }
146
+ }
@@ -0,0 +1,54 @@
1
+ import type {
2
+ SlackChannel,
3
+ SlackConnection,
4
+ SlackMemberMappingEntry,
5
+ SlackNotificationSettings,
6
+ } from '~/types/slack'
7
+ import type { ApiContext } from './context'
8
+
9
+ /** Slack integration: per-account connection, per-workspace routing + member map. */
10
+ export function slackApi({ http, ws }: ApiContext) {
11
+ return {
12
+ // ---- slack integration (extra notification transport) -----------------
13
+ // Per-account connection (manual bot-token paste + the OAuth "Add to Slack"
14
+ // URL), per-workspace routing, and the per-account member map. A 503 from
15
+ // `getSlackConnection` means the integration is off (the store hides its UI).
16
+ getSlackConnection: (workspaceId: string) =>
17
+ http<{ connection: SlackConnection | null; oauthEnabled: boolean }>(
18
+ `${ws(workspaceId)}/slack/connection`,
19
+ ),
20
+
21
+ getSlackInstallUrl: (workspaceId: string) =>
22
+ http<{ url: string }>(`${ws(workspaceId)}/slack/install-url`),
23
+
24
+ connectSlack: (workspaceId: string, token: string) =>
25
+ http<SlackConnection>(`${ws(workspaceId)}/slack/connect`, {
26
+ method: 'POST',
27
+ body: { token },
28
+ }),
29
+
30
+ disconnectSlack: (workspaceId: string) =>
31
+ http(`${ws(workspaceId)}/slack/connection`, { method: 'DELETE' }),
32
+
33
+ listSlackChannels: (workspaceId: string) =>
34
+ http<{ channels: SlackChannel[] }>(`${ws(workspaceId)}/slack/channels`),
35
+
36
+ getSlackSettings: (workspaceId: string) =>
37
+ http<SlackNotificationSettings>(`${ws(workspaceId)}/slack/settings`),
38
+
39
+ updateSlackSettings: (
40
+ workspaceId: string,
41
+ body: { routes: SlackNotificationSettings['routes']; mentionsEnabled: boolean },
42
+ ) =>
43
+ http<SlackNotificationSettings>(`${ws(workspaceId)}/slack/settings`, { method: 'PUT', body }),
44
+
45
+ getSlackMemberMapping: (workspaceId: string) =>
46
+ http<{ entries: SlackMemberMappingEntry[] }>(`${ws(workspaceId)}/slack/member-mapping`),
47
+
48
+ updateSlackMemberMapping: (workspaceId: string, entries: SlackMemberMappingEntry[]) =>
49
+ http<{ entries: SlackMemberMappingEntry[] }>(`${ws(workspaceId)}/slack/member-mapping`, {
50
+ method: 'PUT',
51
+ body: { entries },
52
+ }),
53
+ }
54
+ }
@@ -0,0 +1,72 @@
1
+ import type {
2
+ Block,
3
+ SourceTask,
4
+ TaskConnection,
5
+ TaskSearchResult,
6
+ TaskSourceDescriptor,
7
+ TaskSourceKind,
8
+ } from '~/types/domain'
9
+ import type { PutTrackerSettingsInput, TrackerSettings } from '~/types/tracker'
10
+ import type { ApiContext } from './context'
11
+
12
+ /** Task sources (Jira, …): connect/import/search/link + the workspace tracker selection. */
13
+ export function tasksApi({ http, ws }: ApiContext) {
14
+ return {
15
+ // ---- task sources (Jira, …) ------------------------------------------
16
+ // The configured trackers + their connect/import metadata. A 503 means the
17
+ // integration is off (the store hides its UI on any error here).
18
+ listTaskSources: (workspaceId: string) =>
19
+ http<{ sources: TaskSourceDescriptor[] }>(`${ws(workspaceId)}/task-sources`),
20
+
21
+ listTaskConnections: (workspaceId: string) =>
22
+ http<{ connections: TaskConnection[] }>(`${ws(workspaceId)}/task-sources/connections`),
23
+
24
+ connectTaskSource: (
25
+ workspaceId: string,
26
+ source: TaskSourceKind,
27
+ credentials: Record<string, string>,
28
+ ) =>
29
+ http<TaskConnection>(`${ws(workspaceId)}/task-sources/${source}/connect`, {
30
+ method: 'POST',
31
+ body: { credentials },
32
+ }),
33
+
34
+ disconnectTaskSource: (workspaceId: string, source: TaskSourceKind) =>
35
+ http(`${ws(workspaceId)}/task-sources/${source}/connection`, { method: 'DELETE' }),
36
+
37
+ listTasks: (workspaceId: string) => http<SourceTask[]>(`${ws(workspaceId)}/tasks`),
38
+
39
+ importTask: (workspaceId: string, source: TaskSourceKind, body: { ref: string }) =>
40
+ http<SourceTask>(`${ws(workspaceId)}/task-sources/${source}/import`, {
41
+ method: 'POST',
42
+ body,
43
+ }),
44
+
45
+ searchTaskSource: (workspaceId: string, source: TaskSourceKind, query: string) =>
46
+ http<{ results: TaskSearchResult[] }>(`${ws(workspaceId)}/task-sources/${source}/search`, {
47
+ method: 'POST',
48
+ body: { query },
49
+ }),
50
+
51
+ linkTask: (
52
+ workspaceId: string,
53
+ body: { source: TaskSourceKind; externalId: string; blockId: string },
54
+ ) => http<SourceTask>(`${ws(workspaceId)}/tasks/link`, { method: 'POST', body }),
55
+
56
+ createTaskFromIssue: (
57
+ workspaceId: string,
58
+ body: { source: TaskSourceKind; externalId: string; containerId: string },
59
+ ) =>
60
+ http<{ block: Block; task: SourceTask }>(`${ws(workspaceId)}/tasks/create-block`, {
61
+ method: 'POST',
62
+ body,
63
+ }),
64
+
65
+ // ---- issue-tracker selection (workspace-level) ------------------------
66
+ getTrackerSettings: (workspaceId: string) =>
67
+ http<TrackerSettings>(`${ws(workspaceId)}/tracker-settings`),
68
+
69
+ putTrackerSettings: (workspaceId: string, body: PutTrackerSettingsInput) =>
70
+ http<TrackerSettings>(`${ws(workspaceId)}/tracker-settings`, { method: 'PUT', body }),
71
+ }
72
+ }
@@ -0,0 +1,36 @@
1
+ import type {
2
+ UpdateWorkspaceSettingsInput,
3
+ Workspace,
4
+ WorkspaceSettings,
5
+ WorkspaceSnapshot,
6
+ } from '~/types/domain'
7
+ import type { ApiContext } from './context'
8
+
9
+ /** Workspace CRUD + the full snapshot read. */
10
+ export function workspacesApi({ http, ws }: ApiContext) {
11
+ return {
12
+ // ---- workspaces -------------------------------------------------------
13
+ listWorkspaces: () => http<Workspace[]>('/workspaces'),
14
+
15
+ createWorkspace: (
16
+ body: { name?: string; description?: string; seed?: boolean; accountId?: string } = {},
17
+ ) => http<WorkspaceSnapshot>('/workspaces', { method: 'POST', body }),
18
+
19
+ getWorkspace: (workspaceId: string) => http<WorkspaceSnapshot>(ws(workspaceId)),
20
+
21
+ updateWorkspace: (workspaceId: string, body: { name?: string; description?: string | null }) =>
22
+ http<Workspace>(ws(workspaceId), { method: 'PATCH', body }),
23
+
24
+ renameWorkspace: (workspaceId: string, name: string) =>
25
+ http<Workspace>(ws(workspaceId), { method: 'PATCH', body: { name } }),
26
+
27
+ deleteWorkspace: (workspaceId: string) => http(ws(workspaceId), { method: 'DELETE' }),
28
+
29
+ // ---- workspace runtime settings (human-wait escalation + per-service task limit) --
30
+ getWorkspaceSettings: (workspaceId: string) =>
31
+ http<WorkspaceSettings>(`${ws(workspaceId)}/settings`),
32
+
33
+ updateWorkspaceSettings: (workspaceId: string, body: UpdateWorkspaceSettingsInput) =>
34
+ http<WorkspaceSettings>(`${ws(workspaceId)}/settings`, { method: 'PUT', body }),
35
+ }
36
+ }
@@ -0,0 +1,89 @@
1
+ import type { FragmentOwnerKind } from '~/types/domain'
2
+ import type { ApiContext } from './api/context'
3
+ import { accountsApi } from './api/accounts'
4
+ import { authApi } from './api/auth'
5
+ import { bootstrapApi } from './api/bootstrap'
6
+ import { boardApi } from './api/board'
7
+ import { documentsApi } from './api/documents'
8
+ import { executionApi } from './api/execution'
9
+ import { fragmentsApi } from './api/fragments'
10
+ import { githubApi } from './api/github'
11
+ import { modelsApi } from './api/models'
12
+ import { notificationsApi } from './api/notifications'
13
+ import { presetsApi } from './api/presets'
14
+ import { recurringApi } from './api/recurring'
15
+ import { releaseHealthApi } from './api/releaseHealth'
16
+ import { reviewsApi } from './api/reviews'
17
+ import { slackApi } from './api/slack'
18
+ import { tasksApi } from './api/tasks'
19
+ import { workspacesApi } from './api/workspaces'
20
+
21
+ /**
22
+ * Thin typed client over the cat-factory backend (a Hono worker). Every method
23
+ * maps to one REST endpoint; the request/response shapes mirror
24
+ * `@cat-factory/contracts`, so responses drop straight into the Pinia stores.
25
+ *
26
+ * The endpoints are grouped by domain into the `./api/*` factory modules; this
27
+ * function builds the shared {@link ApiContext} (the authed `$fetch` client +
28
+ * path/header helpers) and spreads every group into a single flat client, so
29
+ * call sites stay `useApi().someMethod(...)`.
30
+ *
31
+ * The base URL comes from runtime config (`NUXT_PUBLIC_API_BASE`), defaulting to
32
+ * the local wrangler dev server — see `nuxt.config.ts`.
33
+ */
34
+ export function useApi() {
35
+ const apiBase = useRuntimeConfig().public.apiBase
36
+ const http = $fetch.create({
37
+ baseURL: apiBase,
38
+ // Attach the session token (when signed in) so the backend's auth gate lets
39
+ // the request through. Read lazily from the store so a fresh token applies
40
+ // without rebuilding the client.
41
+ onRequest({ options }) {
42
+ const token = useAuthStore().token
43
+ if (!token) return
44
+ const headers = new Headers(options.headers)
45
+ headers.set('Authorization', `Bearer ${token}`)
46
+ options.headers = headers
47
+ },
48
+ // A 401 means our token lapsed or was revoked — drop it so the UI re-gates.
49
+ onResponseError({ response }) {
50
+ if (response?.status === 401) useAuthStore().handleUnauthorized()
51
+ },
52
+ })
53
+
54
+ // The personal-subscription unlock password (individual-usage vendors) rides as an
55
+ // ambient request header — like the bearer token — so it never lands in a request
56
+ // body/wire-contract payload. Mirrors PERSONAL_PASSWORD_HEADER in @cat-factory/contracts.
57
+ const pwHeaders = (password?: string): Record<string, string> | undefined =>
58
+ password ? { 'X-Personal-Password': password } : undefined
59
+
60
+ const ws = (workspaceId: string) => `/workspaces/${encodeURIComponent(workspaceId)}`
61
+ // Prompt-fragment library routes exist at both tiers; resolve the prefix from
62
+ // the owner scope (ADR 0006 §8).
63
+ const scope = (kind: FragmentOwnerKind, id: string) =>
64
+ kind === 'account'
65
+ ? `/accounts/${encodeURIComponent(id)}`
66
+ : `/workspaces/${encodeURIComponent(id)}`
67
+
68
+ const ctx: ApiContext = { http, ws, scope, pwHeaders }
69
+
70
+ return {
71
+ ...authApi(ctx),
72
+ ...fragmentsApi(ctx),
73
+ ...modelsApi(ctx),
74
+ ...accountsApi(ctx),
75
+ ...workspacesApi(ctx),
76
+ ...boardApi(ctx),
77
+ ...executionApi(ctx),
78
+ ...documentsApi(ctx),
79
+ ...tasksApi(ctx),
80
+ ...reviewsApi(ctx),
81
+ ...notificationsApi(ctx),
82
+ ...presetsApi(ctx),
83
+ ...releaseHealthApi(ctx),
84
+ ...recurringApi(ctx),
85
+ ...githubApi(ctx),
86
+ ...slackApi(ctx),
87
+ ...bootstrapApi(ctx),
88
+ }
89
+ }
@@ -0,0 +1,90 @@
1
+ import { ref } from 'vue'
2
+ import type { Block } from '~/types/domain'
3
+
4
+ /**
5
+ * Pointer-driven dragging for blocks positioned inside a container's 2D canvas
6
+ * (tasks inside services/modules, modules inside services). Movement is divided
7
+ * by the board zoom so the block tracks the cursor. When `reparent` is set, the
8
+ * drop point is hit-tested against `[data-drop-zone]` ancestors so a task can be
9
+ * dragged from a service into a module (or back out).
10
+ */
11
+ export function useBlockDrag() {
12
+ const board = useBoardStore()
13
+ const ui = useUiStore()
14
+ const draggingId = ref<string | null>(null)
15
+
16
+ function startDrag(
17
+ block: Block,
18
+ e: PointerEvent,
19
+ opts: { reparent?: boolean; clamp?: boolean } = {},
20
+ ) {
21
+ if (e.button !== 0) return
22
+ e.preventDefault()
23
+ e.stopPropagation()
24
+ const startX = e.clientX
25
+ const startY = e.clientY
26
+ const orig = { ...block.position }
27
+ // Container-local blocks (tasks/modules) are clamped to their parent's origin;
28
+ // frames live in free-floating flow space, so they opt out via `clamp: false`.
29
+ const clamp = opts.clamp ?? true
30
+ draggingId.value = block.id
31
+ // Position is only previewed locally while dragging and persisted once on
32
+ // release. Writing every move raced — a late, out-of-order response could land
33
+ // a stale position last and make the block jump after the user let go.
34
+ let moved = false
35
+ let last = orig
36
+
37
+ const onMove = (ev: PointerEvent) => {
38
+ const z = ui.zoom || 1
39
+ const nx = orig.x + (ev.clientX - startX) / z
40
+ const ny = orig.y + (ev.clientY - startY) / z
41
+ moved = true
42
+ last = { x: clamp ? Math.max(0, nx) : nx, y: clamp ? Math.max(0, ny) : ny }
43
+ board.previewMove(block.id, last)
44
+ }
45
+ const onUp = (ev: PointerEvent) => {
46
+ window.removeEventListener('pointermove', onMove)
47
+ window.removeEventListener('pointerup', onUp)
48
+ if (moved) {
49
+ // A successful reparent persists the move itself; otherwise commit the final
50
+ // position in place. Either way it's a single write, not one per frame. Run
51
+ // the hit-test BEFORE clearing draggingId so the dragged element is still
52
+ // marked non-interactive (see DraggableTask) and the zone beneath resolves.
53
+ const reparented = opts.reparent && reparentAt(block, ev.clientX, ev.clientY)
54
+ if (!reparented) void board.moveBlock(block.id, last)
55
+ }
56
+ draggingId.value = null
57
+ }
58
+ window.addEventListener('pointermove', onMove)
59
+ window.addEventListener('pointerup', onUp)
60
+ }
61
+
62
+ /** Returns true when the block was dropped into a *different* container. */
63
+ function reparentAt(block: Block, clientX: number, clientY: number): boolean {
64
+ const el = document.querySelector(`[data-block-id="${block.id}"]`) as HTMLElement | null
65
+ if (!el) return false
66
+ // The dragged block is already non-interactive while dragging (DraggableTask
67
+ // drops pointer-events on the whole wrapper, handle included); belt-and-braces,
68
+ // also neutralise this node so elementFromPoint resolves the zone beneath it.
69
+ const prev = el.style.pointerEvents
70
+ el.style.pointerEvents = 'none'
71
+ const under = document.elementFromPoint(clientX, clientY) as HTMLElement | null
72
+ const zoneEl = under?.closest('[data-drop-zone]') as HTMLElement | null
73
+ el.style.pointerEvents = prev
74
+ if (!zoneEl) return false
75
+
76
+ const newParent = zoneEl.getAttribute('data-drop-zone')!
77
+ if (newParent === block.parentId) return false // same container — caller commits position
78
+
79
+ const z = ui.zoom || 1
80
+ const zr = zoneEl.getBoundingClientRect()
81
+ const er = el.getBoundingClientRect()
82
+ void board.reparentBlock(block.id, newParent, {
83
+ x: Math.max(0, (er.left - zr.left) / z),
84
+ y: Math.max(0, (er.top - zr.top) / z),
85
+ })
86
+ return true
87
+ }
88
+
89
+ return { draggingId, startDrag }
90
+ }