@cat-factory/app 0.35.0 → 0.37.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 (87) hide show
  1. package/app/components/auth/UserMenu.vue +11 -1
  2. package/app/components/brainstorm/BrainstormWindow.vue +2 -1
  3. package/app/components/clarity/ClarityReviewWindow.vue +2 -1
  4. package/app/components/gates/GateResultView.vue +107 -12
  5. package/app/components/layout/IntegrationBackTitle.vue +12 -7
  6. package/app/components/layout/IntegrationsHub.vue +191 -43
  7. package/app/components/layout/NotificationsInbox.vue +16 -0
  8. package/app/components/layout/PersonalSetupModal.vue +141 -0
  9. package/app/components/pipeline/PipelineBuilder.vue +1 -1
  10. package/app/components/providers/VendorCredentialsModal.vue +7 -2
  11. package/app/components/slack/SlackPanel.vue +1 -0
  12. package/app/composables/api/accounts.ts +36 -51
  13. package/app/composables/api/auth.ts +20 -19
  14. package/app/composables/api/board.ts +60 -40
  15. package/app/composables/api/bootstrap.ts +25 -22
  16. package/app/composables/api/client.ts +102 -0
  17. package/app/composables/api/context.ts +25 -6
  18. package/app/composables/api/documents.ts +36 -34
  19. package/app/composables/api/execution.ts +65 -48
  20. package/app/composables/api/followUps.ts +26 -26
  21. package/app/composables/api/fragments.ts +47 -34
  22. package/app/composables/api/github.ts +65 -45
  23. package/app/composables/api/humanReview.ts +19 -0
  24. package/app/composables/api/humanTest.ts +15 -11
  25. package/app/composables/api/kaizen.ts +8 -6
  26. package/app/composables/api/localSettings.ts +5 -4
  27. package/app/composables/api/models.ts +58 -51
  28. package/app/composables/api/notifications.ts +13 -7
  29. package/app/composables/api/presets.ts +34 -28
  30. package/app/composables/api/providerConnections.ts +68 -26
  31. package/app/composables/api/recurring.ts +40 -30
  32. package/app/composables/api/releaseHealth.ts +28 -26
  33. package/app/composables/api/reviews.ts +136 -114
  34. package/app/composables/api/sandbox.ts +52 -34
  35. package/app/composables/api/slack.ts +22 -25
  36. package/app/composables/api/spec.ts +3 -3
  37. package/app/composables/api/tasks.ts +42 -41
  38. package/app/composables/api/userSecrets.ts +12 -17
  39. package/app/composables/api/workspaces.ts +21 -15
  40. package/app/composables/useApi.ts +11 -1
  41. package/app/composables/useIntegrationBack.ts +9 -3
  42. package/app/pages/index.vue +2 -0
  43. package/app/stores/auth.ts +2 -1
  44. package/app/stores/board.ts +2 -1
  45. package/app/stores/brainstorm.ts +2 -2
  46. package/app/stores/clarity.ts +6 -2
  47. package/app/stores/execution.ts +3 -2
  48. package/app/stores/github.ts +1 -2
  49. package/app/stores/humanReview.ts +41 -0
  50. package/app/stores/mergePresets.ts +2 -6
  51. package/app/stores/pipelines.ts +1 -1
  52. package/app/stores/recurringPipelines.ts +2 -7
  53. package/app/stores/sandbox.ts +1 -2
  54. package/app/stores/ui.ts +62 -19
  55. package/app/types/accountSettings.ts +11 -36
  56. package/app/types/accounts.ts +16 -71
  57. package/app/types/bootstrap.ts +13 -75
  58. package/app/types/brainstorm.ts +13 -38
  59. package/app/types/clarity.ts +12 -43
  60. package/app/types/consensus.ts +16 -89
  61. package/app/types/documents.ts +19 -94
  62. package/app/types/domain.ts +54 -582
  63. package/app/types/execution.ts +48 -499
  64. package/app/types/fragments.ts +15 -83
  65. package/app/types/github.ts +25 -161
  66. package/app/types/incidentEnrichment.ts +10 -25
  67. package/app/types/localModels.ts +11 -61
  68. package/app/types/localSettings.ts +9 -26
  69. package/app/types/merge.ts +10 -68
  70. package/app/types/model-presets.ts +7 -28
  71. package/app/types/models.ts +16 -164
  72. package/app/types/notifications.ts +18 -76
  73. package/app/types/openrouter.ts +8 -34
  74. package/app/types/providerConnections.ts +21 -41
  75. package/app/types/provisioningLogs.ts +9 -29
  76. package/app/types/recurring.ts +10 -63
  77. package/app/types/releaseHealth.ts +15 -39
  78. package/app/types/requirements.ts +14 -84
  79. package/app/types/sandbox.ts +45 -161
  80. package/app/types/services.ts +3 -22
  81. package/app/types/slack.ts +10 -47
  82. package/app/types/spec.ts +15 -68
  83. package/app/types/tasks.ts +15 -111
  84. package/app/types/tracker.ts +9 -24
  85. package/app/types/userSecrets.ts +12 -47
  86. package/app/utils/catalog.ts +12 -0
  87. package/package.json +9 -2
@@ -0,0 +1,102 @@
1
+ import type {
2
+ ApiContract,
3
+ ClientRequestParams,
4
+ DefaultStreaming,
5
+ InferNonSseClientResponse,
6
+ SuccessfulHttpStatusCode,
7
+ } from '@toad-contracts/core'
8
+ import {
9
+ type ContractRequestOptions,
10
+ sendByApiContract,
11
+ type WretchInstance,
12
+ } from '@toad-contracts/frontend-http-client'
13
+ import wretch from 'wretch'
14
+
15
+ /**
16
+ * The validated success-response body inferred from a route contract (every REST
17
+ * endpoint here is non-SSE). This is what {@link ApiSend} resolves to.
18
+ */
19
+ export type SuccessBodyOf<T extends ApiContract> = Extract<
20
+ InferNonSseClientResponse<T>,
21
+ { statusCode: SuccessfulHttpStatusCode }
22
+ >['body']
23
+
24
+ /** The request params a contract requires (pathParams/body/queryParams/headers), per the contract. */
25
+ export type SendParams<T extends ApiContract> = ClientRequestParams<
26
+ T,
27
+ DefaultStreaming<T['responsesByStatusCode']>
28
+ > &
29
+ ContractRequestOptions<true>
30
+
31
+ /**
32
+ * A bound, throw-on-error sender: validates the response against the contract and
33
+ * returns the success body, or throws the typed error (an `UnexpectedResponseError`
34
+ * or a declared non-2xx response). Preserves the throwing ergonomics the Pinia
35
+ * stores already expect from the old `$fetch` client.
36
+ */
37
+ export type ApiSend = <T extends ApiContract>(
38
+ contract: T,
39
+ params: SendParams<T>,
40
+ ) => Promise<SuccessBodyOf<T>>
41
+
42
+ /**
43
+ * Build the authed wretch client. Ports the concerns the old `$fetch` client had:
44
+ * base URL from runtime config, a lazily-read bearer token (so a fresh token applies
45
+ * without rebuilding the client), and a 401 → re-gate.
46
+ */
47
+ export function createApiClient(): WretchInstance {
48
+ const apiBase = useRuntimeConfig().public.apiBase
49
+ return wretch(apiBase).middlewares([
50
+ (next) => async (url, opts) => {
51
+ const token = useAuthStore().token
52
+ if (token) {
53
+ opts.headers = {
54
+ ...(opts.headers as Record<string, string> | undefined),
55
+ Authorization: `Bearer ${token}`,
56
+ }
57
+ }
58
+ const response = await next(url, opts)
59
+ // A 401 means our token lapsed or was revoked — drop it so the UI re-gates.
60
+ if (response.status === 401) useAuthStore().handleUnauthorized()
61
+ return response
62
+ },
63
+ ])
64
+ }
65
+
66
+ /**
67
+ * Send a contract request and unwrap to the success body (or throw the typed error).
68
+ * The public signature preserves per-contract inference for callers; inside,
69
+ * sendByApiContract's deeply-conditional result type can't be proven equal to
70
+ * SuccessBodyOf<T> generically, so the success body is asserted at this single boundary.
71
+ */
72
+ export async function sendContract<T extends ApiContract>(
73
+ client: WretchInstance,
74
+ contract: T,
75
+ params: SendParams<T>,
76
+ ): Promise<SuccessBodyOf<T>> {
77
+ const outcome = await sendByApiContract(client, contract, params)
78
+ if (outcome.error) throw outcome.error
79
+ return outcome.result!.body as SuccessBodyOf<T>
80
+ }
81
+
82
+ /** Curry {@link sendContract} over a client into the throw-on-error {@link ApiSend}. */
83
+ export function createSend(client: WretchInstance): ApiSend {
84
+ return (contract, params) => sendContract(client, contract, params)
85
+ }
86
+
87
+ /**
88
+ * Like {@link ApiSend} but augments the request with ambient headers not modelled by the
89
+ * contract (the personal-subscription unlock password, mirroring how the bearer token
90
+ * rides outside the wire body). `undefined` headers send unchanged.
91
+ */
92
+ export type ApiSendWith = <T extends ApiContract>(
93
+ extraHeaders: Record<string, string> | undefined,
94
+ contract: T,
95
+ params: SendParams<T>,
96
+ ) => Promise<SuccessBodyOf<T>>
97
+
98
+ /** Curry {@link sendContract} with per-call ambient headers (see {@link ApiSendWith}). */
99
+ export function createSendWith(client: WretchInstance): ApiSendWith {
100
+ return (extraHeaders, contract, params) =>
101
+ sendContract(extraHeaders ? client.headers(extraHeaders) : client, contract, params)
102
+ }
@@ -1,19 +1,38 @@
1
+ import type { WretchInstance } from '@toad-contracts/frontend-http-client'
1
2
  import type { FragmentOwnerKind } from '~/types/domain'
3
+ import type { ApiSend, ApiSendWith } from './client'
2
4
 
3
5
  /** The authed `$fetch` instance type — Nuxt's augmented client, as returned by `$fetch.create`. */
4
6
  export type ApiHttp = ReturnType<typeof $fetch.create>
5
7
 
6
8
  /**
7
9
  * Shared plumbing handed to every grouped API module. `useApi()` builds one of
8
- * these (the authed `$fetch` client + the path/header helpers) and passes it to
9
- * each `*Api(ctx)` factory; the factories return the endpoint methods that
10
- * `useApi()` spreads into its single flat client object. Splitting the client
11
- * this way keeps call sites unchanged (`useApi().someMethod(...)`) while the
12
- * ~100 endpoints live in cohesive per-domain files.
10
+ * these (the authed wretch client, the contract `send` helper + the path/header
11
+ * helpers) and passes it to each `*Api(ctx)` factory; the factories return the
12
+ * endpoint methods that `useApi()` spreads into its single flat client object.
13
+ * Splitting the client this way keeps call sites unchanged
14
+ * (`useApi().someMethod(...)`) while the ~100 endpoints live in cohesive
15
+ * per-domain files.
13
16
  */
14
17
  export interface ApiContext {
15
- /** The authed `$fetch` instance (bearer token + 401 handling pre-wired). */
18
+ /**
19
+ * The authed `$fetch` instance (bearer token + 401 handling pre-wired).
20
+ * Transitional: API groups not yet migrated to contract `send` still use it.
21
+ */
16
22
  http: ApiHttp
23
+ /** The authed wretch client (bearer token + 401 handling pre-wired). */
24
+ client: WretchInstance
25
+ /**
26
+ * Contract sender: validates the response against the route contract and returns
27
+ * the success body, or throws the typed error. The single source of truth for
28
+ * path + method + request + response is the contract in `@cat-factory/contracts`.
29
+ */
30
+ send: ApiSend
31
+ /**
32
+ * Like {@link send} but attaches ambient headers the contract doesn't model — today the
33
+ * personal-subscription unlock password on gated run calls (individual-usage vendors).
34
+ */
35
+ sendWith: ApiSendWith
17
36
  /** `/workspaces/:id` path prefix (id encoded). */
18
37
  ws: (workspaceId: string) => string
19
38
  /** Prompt-fragment library prefix, resolved from the owner scope (ADR 0006 §8). */
@@ -1,58 +1,64 @@
1
- import type {
2
- DocumentBoardPlan,
3
- DocumentConnection,
4
- DocumentSearchResult,
5
- DocumentSourceDescriptor,
6
- DocumentSourceKind,
7
- SourceDocument,
8
- SpawnResult,
9
- } from '~/types/domain'
1
+ import {
2
+ connectDocumentSourceContract,
3
+ disconnectDocumentSourceContract,
4
+ importDocumentContract,
5
+ linkDocumentContract,
6
+ listDocumentConnectionsContract,
7
+ listDocumentsContract,
8
+ listDocumentSourcesContract,
9
+ planDocumentContract,
10
+ searchDocumentsContract,
11
+ spawnDocumentContract,
12
+ } from '@cat-factory/contracts'
13
+ import type { DocumentSourceKind } from '~/types/domain'
10
14
  import type { ApiContext } from './context'
11
15
 
12
16
  /** Document sources (Confluence, Notion, …): connect, import, search, board-spawn. */
13
- export function documentsApi({ http, ws }: ApiContext) {
17
+ export function documentsApi({ send, ws }: ApiContext) {
14
18
  return {
15
19
  // ---- document sources (Confluence, Notion, …) -------------------------
16
20
  // The configured sources + their connect/import metadata. A 503 means the
17
21
  // integration is off (the store hides its UI on any error here).
18
22
  listDocumentSources: (workspaceId: string) =>
19
- http<{ sources: DocumentSourceDescriptor[] }>(`${ws(workspaceId)}/document-sources`),
23
+ send(listDocumentSourcesContract, { pathPrefix: ws(workspaceId) }),
20
24
 
21
25
  listDocumentConnections: (workspaceId: string) =>
22
- http<{ connections: DocumentConnection[] }>(
23
- `${ws(workspaceId)}/document-sources/connections`,
24
- ),
26
+ send(listDocumentConnectionsContract, { pathPrefix: ws(workspaceId) }),
25
27
 
26
28
  connectDocumentSource: (
27
29
  workspaceId: string,
28
30
  source: DocumentSourceKind,
29
31
  credentials: Record<string, string>,
30
32
  ) =>
31
- http<DocumentConnection>(`${ws(workspaceId)}/document-sources/${source}/connect`, {
32
- method: 'POST',
33
+ send(connectDocumentSourceContract, {
34
+ pathPrefix: ws(workspaceId),
35
+ pathParams: { source },
33
36
  body: { credentials },
34
37
  }),
35
38
 
36
39
  disconnectDocumentSource: (workspaceId: string, source: DocumentSourceKind) =>
37
- http(`${ws(workspaceId)}/document-sources/${source}/connection`, { method: 'DELETE' }),
40
+ send(disconnectDocumentSourceContract, {
41
+ pathPrefix: ws(workspaceId),
42
+ pathParams: { source },
43
+ }),
38
44
 
39
- listDocuments: (workspaceId: string) => http<SourceDocument[]>(`${ws(workspaceId)}/documents`),
45
+ listDocuments: (workspaceId: string) =>
46
+ send(listDocumentsContract, { pathPrefix: ws(workspaceId) }),
40
47
 
41
48
  importDocument: (workspaceId: string, source: DocumentSourceKind, body: { ref: string }) =>
42
- http<SourceDocument>(`${ws(workspaceId)}/document-sources/${source}/import`, {
43
- method: 'POST',
44
- body,
45
- }),
49
+ send(importDocumentContract, { pathPrefix: ws(workspaceId), pathParams: { source }, body }),
46
50
 
47
51
  searchDocumentSource: (workspaceId: string, source: DocumentSourceKind, query: string) =>
48
- http<{ results: DocumentSearchResult[] }>(
49
- `${ws(workspaceId)}/document-sources/${source}/search`,
50
- { method: 'POST', body: { query } },
51
- ),
52
+ send(searchDocumentsContract, {
53
+ pathPrefix: ws(workspaceId),
54
+ pathParams: { source },
55
+ body: { query },
56
+ }),
52
57
 
53
58
  planDocument: (workspaceId: string, source: DocumentSourceKind, externalId: string) =>
54
- http<DocumentBoardPlan>(`${ws(workspaceId)}/document-sources/${source}/plan`, {
55
- method: 'POST',
59
+ send(planDocumentContract, {
60
+ pathPrefix: ws(workspaceId),
61
+ pathParams: { source },
56
62
  body: { externalId },
57
63
  }),
58
64
 
@@ -60,15 +66,11 @@ export function documentsApi({ http, ws }: ApiContext) {
60
66
  workspaceId: string,
61
67
  source: DocumentSourceKind,
62
68
  body: { externalId: string; frameId?: string },
63
- ) =>
64
- http<{ plan: DocumentBoardPlan; result: SpawnResult }>(
65
- `${ws(workspaceId)}/document-sources/${source}/spawn`,
66
- { method: 'POST', body },
67
- ),
69
+ ) => send(spawnDocumentContract, { pathPrefix: ws(workspaceId), pathParams: { source }, body }),
68
70
 
69
71
  linkDocument: (
70
72
  workspaceId: string,
71
73
  body: { source: DocumentSourceKind; externalId: string; blockId: string },
72
- ) => http<SourceDocument>(`${ws(workspaceId)}/documents/link`, { method: 'POST', body }),
74
+ ) => send(linkDocumentContract, { pathPrefix: ws(workspaceId), body }),
73
75
  }
74
76
  }
@@ -1,15 +1,24 @@
1
- import type { Block, ExecutionInstance } from '~/types/domain'
2
- import type {
3
- AgentContextSnapshot,
4
- IterationCapChoice,
5
- LlmCallMetric,
6
- LlmMetricsExport,
7
- ReviewComment,
8
- } from '~/types/execution'
1
+ import {
2
+ approveStepContract,
3
+ cancelExecutionContract,
4
+ exportExecutionLlmMetricsContract,
5
+ getExecutionAgentContextContract,
6
+ getExecutionLlmMetricsContract,
7
+ mergeBlockContract,
8
+ rejectStepContract,
9
+ requestStepChangesContract,
10
+ resolveDecisionContract,
11
+ resolveStepExceededContract,
12
+ restartExecutionContract,
13
+ resumeSpendContract,
14
+ startExecutionContract,
15
+ } from '@cat-factory/contracts'
16
+ import type { RequestStepChangesInput } from '@cat-factory/contracts'
17
+ import type { IterationCapChoice } from '~/types/execution'
9
18
  import type { ApiContext } from './context'
10
19
 
11
20
  /** Run lifecycle (start/cancel/decisions/approvals/restart) + LLM metrics + spend. */
12
- export function executionApi({ http, ws, pwHeaders }: ApiContext) {
21
+ export function executionApi({ send, sendWith, ws, pwHeaders }: ApiContext) {
13
22
  return {
14
23
  // ---- executions -------------------------------------------------------
15
24
  startExecution: (
@@ -18,17 +27,17 @@ export function executionApi({ http, ws, pwHeaders }: ApiContext) {
18
27
  body: { pipelineId: string },
19
28
  password?: string,
20
29
  ) =>
21
- http<ExecutionInstance>(`${ws(workspaceId)}/blocks/${blockId}/executions`, {
22
- method: 'POST',
30
+ sendWith(pwHeaders(password), startExecutionContract, {
31
+ pathPrefix: ws(workspaceId),
32
+ pathParams: { blockId },
23
33
  body,
24
- headers: pwHeaders(password),
25
34
  }),
26
35
 
27
36
  cancelExecution: (workspaceId: string, blockId: string) =>
28
- http<Block>(`${ws(workspaceId)}/blocks/${blockId}/executions`, { method: 'DELETE' }),
37
+ send(cancelExecutionContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
29
38
 
30
39
  mergeBlock: (workspaceId: string, blockId: string) =>
31
- http<Block>(`${ws(workspaceId)}/blocks/${blockId}/merge`, { method: 'POST' }),
40
+ send(mergeBlockContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
32
41
 
33
42
  resolveDecision: (
34
43
  workspaceId: string,
@@ -37,10 +46,11 @@ export function executionApi({ http, ws, pwHeaders }: ApiContext) {
37
46
  body: { choice: string },
38
47
  password?: string,
39
48
  ) =>
40
- http<ExecutionInstance>(
41
- `${ws(workspaceId)}/executions/${executionId}/decisions/${decisionId}`,
42
- { method: 'POST', body, headers: pwHeaders(password) },
43
- ),
49
+ sendWith(pwHeaders(password), resolveDecisionContract, {
50
+ pathPrefix: ws(workspaceId),
51
+ pathParams: { executionId, decisionId },
52
+ body,
53
+ }),
44
54
 
45
55
  approveStep: (
46
56
  workspaceId: string,
@@ -49,22 +59,24 @@ export function executionApi({ http, ws, pwHeaders }: ApiContext) {
49
59
  body: { proposal?: string },
50
60
  password?: string,
51
61
  ) =>
52
- http<ExecutionInstance>(
53
- `${ws(workspaceId)}/executions/${executionId}/steps/${approvalId}/approve`,
54
- { method: 'POST', body, headers: pwHeaders(password) },
55
- ),
62
+ sendWith(pwHeaders(password), approveStepContract, {
63
+ pathPrefix: ws(workspaceId),
64
+ pathParams: { executionId, approvalId },
65
+ body,
66
+ }),
56
67
 
57
68
  requestStepChanges: (
58
69
  workspaceId: string,
59
70
  executionId: string,
60
71
  approvalId: string,
61
- body: { feedback?: string; comments?: ReviewComment[] },
72
+ body: RequestStepChangesInput,
62
73
  password?: string,
63
74
  ) =>
64
- http<ExecutionInstance>(
65
- `${ws(workspaceId)}/executions/${executionId}/steps/${approvalId}/request-changes`,
66
- { method: 'POST', body, headers: pwHeaders(password) },
67
- ),
75
+ sendWith(pwHeaders(password), requestStepChangesContract, {
76
+ pathPrefix: ws(workspaceId),
77
+ pathParams: { executionId, approvalId },
78
+ body,
79
+ }),
68
80
 
69
81
  rejectStep: (
70
82
  workspaceId: string,
@@ -72,10 +84,11 @@ export function executionApi({ http, ws, pwHeaders }: ApiContext) {
72
84
  approvalId: string,
73
85
  body: { reason?: string },
74
86
  ) =>
75
- http<ExecutionInstance>(
76
- `${ws(workspaceId)}/executions/${executionId}/steps/${approvalId}/reject`,
77
- { method: 'POST', body },
78
- ),
87
+ send(rejectStepContract, {
88
+ pathPrefix: ws(workspaceId),
89
+ pathParams: { executionId, approvalId },
90
+ body,
91
+ }),
79
92
 
80
93
  // Resolve a companion step parked at its rework cap: one more round / proceed /
81
94
  // stop & reset (the companion analogue of resolveRequirementsExceeded).
@@ -86,10 +99,11 @@ export function executionApi({ http, ws, pwHeaders }: ApiContext) {
86
99
  body: { choice: IterationCapChoice },
87
100
  password?: string,
88
101
  ) =>
89
- http<ExecutionInstance>(
90
- `${ws(workspaceId)}/executions/${executionId}/steps/${approvalId}/resolve-exceeded`,
91
- { method: 'POST', body, headers: pwHeaders(password) },
92
- ),
102
+ sendWith(pwHeaders(password), resolveStepExceededContract, {
103
+ pathPrefix: ws(workspaceId),
104
+ pathParams: { executionId, approvalId },
105
+ body,
106
+ }),
93
107
 
94
108
  // Restart a run from a chosen step: re-run from `fromStepIndex` onward (resetting
95
109
  // that step + later steps' iteration counters) while keeping the earlier steps'
@@ -101,35 +115,38 @@ export function executionApi({ http, ws, pwHeaders }: ApiContext) {
101
115
  fromStepIndex: number,
102
116
  password?: string,
103
117
  ) =>
104
- http<ExecutionInstance>(`${ws(workspaceId)}/executions/${executionId}/restart`, {
105
- method: 'POST',
118
+ sendWith(pwHeaders(password), restartExecutionContract, {
119
+ pathPrefix: ws(workspaceId),
120
+ pathParams: { executionId },
106
121
  body: { fromStepIndex },
107
- headers: pwHeaders(password),
108
122
  }),
109
123
 
110
124
  // ---- LLM observability (per-run model-call metrics) -------------------
111
125
  // The full per-call detail behind the board's step rollups. Empty when the
112
126
  // observability sink is not wired.
113
127
  getLlmMetrics: (workspaceId: string, executionId: string) =>
114
- http<{ executionId: string; calls: LlmCallMetric[] }>(
115
- `${ws(workspaceId)}/executions/${encodeURIComponent(executionId)}/llm-metrics`,
116
- ),
128
+ send(getExecutionLlmMetricsContract, {
129
+ pathPrefix: ws(workspaceId),
130
+ pathParams: { executionId },
131
+ }),
117
132
 
118
133
  // The LLM-friendly export bundle (totals + per-agent insights + every call).
119
134
  exportLlmMetrics: (workspaceId: string, executionId: string) =>
120
- http<LlmMetricsExport>(
121
- `${ws(workspaceId)}/executions/${encodeURIComponent(executionId)}/llm-metrics/export`,
122
- ),
135
+ send(exportExecutionLlmMetricsContract, {
136
+ pathPrefix: ws(workspaceId),
137
+ pathParams: { executionId },
138
+ }),
123
139
 
124
140
  // The complete provided context per container-agent dispatch (composed prompts,
125
141
  // folded-in fragments, injected files). Empty when not wired / storing is off.
126
142
  getAgentContext: (workspaceId: string, executionId: string) =>
127
- http<{ executionId: string; snapshots: AgentContextSnapshot[] }>(
128
- `${ws(workspaceId)}/executions/${encodeURIComponent(executionId)}/agent-context`,
129
- ),
143
+ send(getExecutionAgentContextContract, {
144
+ pathPrefix: ws(workspaceId),
145
+ pathParams: { executionId },
146
+ }),
130
147
 
131
148
  // ---- spend safeguard --------------------------------------------------
132
149
  resumeSpend: (workspaceId: string) =>
133
- http<ExecutionInstance[]>(`${ws(workspaceId)}/spend/resume`, { method: 'POST' }),
150
+ send(resumeSpendContract, { pathPrefix: ws(workspaceId) }),
134
151
  }
135
152
  }
@@ -1,4 +1,10 @@
1
- import type { FollowUpsStepState } from '~/types/execution'
1
+ import {
2
+ answerFollowUpContract,
3
+ dismissFollowUpContract,
4
+ fileFollowUpContract,
5
+ getFollowUpsContract,
6
+ queueFollowUpContract,
7
+ } from '@cat-factory/contracts'
2
8
  import type { ApiContext } from './context'
3
9
 
4
10
  /**
@@ -8,45 +14,39 @@ import type { ApiContext } from './context'
8
14
  * the last item is decided, the backend drives the run forward (loop the Coder for the
9
15
  * queued / answered items, else advance).
10
16
  */
11
- export function followUpsApi({ http, ws }: ApiContext) {
12
- const base = (workspaceId: string, executionId: string) =>
13
- `${ws(workspaceId)}/executions/${encodeURIComponent(executionId)}/follow-ups`
14
-
17
+ export function followUpsApi({ send, ws }: ApiContext) {
15
18
  return {
16
19
  // The live follow-up state for a run (null when the companion is off / nothing surfaced).
17
20
  getFollowUps: (workspaceId: string, executionId: string) =>
18
- http<FollowUpsStepState | null>(base(workspaceId, executionId)),
21
+ send(getFollowUpsContract, { pathPrefix: ws(workspaceId), pathParams: { executionId } }),
19
22
 
20
23
  // File a follow-up as a tracker issue (GitHub Issues / Jira).
21
24
  fileFollowUp: (workspaceId: string, executionId: string, itemId: string) =>
22
- http<FollowUpsStepState>(
23
- `${base(workspaceId, executionId)}/${encodeURIComponent(itemId)}/file`,
24
- {
25
- method: 'POST',
26
- },
27
- ),
25
+ send(fileFollowUpContract, {
26
+ pathPrefix: ws(workspaceId),
27
+ pathParams: { executionId, itemId },
28
+ }),
28
29
 
29
30
  // Send a follow-up back to the Coder (queued for its next pass).
30
31
  queueFollowUp: (workspaceId: string, executionId: string, itemId: string) =>
31
- http<FollowUpsStepState>(
32
- `${base(workspaceId, executionId)}/${encodeURIComponent(itemId)}/queue`,
33
- {
34
- method: 'POST',
35
- },
36
- ),
32
+ send(queueFollowUpContract, {
33
+ pathPrefix: ws(workspaceId),
34
+ pathParams: { executionId, itemId },
35
+ }),
37
36
 
38
37
  // Answer a question item (folded into the Coder's next pass).
39
38
  answerFollowUp: (workspaceId: string, executionId: string, itemId: string, answer: string) =>
40
- http<FollowUpsStepState>(
41
- `${base(workspaceId, executionId)}/${encodeURIComponent(itemId)}/answer`,
42
- { method: 'POST', body: { answer } },
43
- ),
39
+ send(answerFollowUpContract, {
40
+ pathPrefix: ws(workspaceId),
41
+ pathParams: { executionId, itemId },
42
+ body: { answer },
43
+ }),
44
44
 
45
45
  // Dismiss a follow-up / question item without acting on it.
46
46
  dismissFollowUp: (workspaceId: string, executionId: string, itemId: string) =>
47
- http<FollowUpsStepState>(
48
- `${base(workspaceId, executionId)}/${encodeURIComponent(itemId)}/dismiss`,
49
- { method: 'POST' },
50
- ),
47
+ send(dismissFollowUpContract, {
48
+ pathPrefix: ws(workspaceId),
49
+ pathParams: { executionId, itemId },
50
+ }),
51
51
  }
52
52
  }
@@ -1,34 +1,44 @@
1
+ import {
2
+ createDocumentFragmentContract,
3
+ createPromptFragmentContract,
4
+ deletePromptFragmentContract,
5
+ fragmentSourceStatusContract,
6
+ linkFragmentSourceContract,
7
+ listFragmentCatalogContract,
8
+ listFragmentSourcesContract,
9
+ listPromptFragmentsContract,
10
+ refreshPromptFragmentContract,
11
+ resolvedFragmentsContract,
12
+ syncFragmentSourceContract,
13
+ unlinkFragmentSourceContract,
14
+ updatePromptFragmentContract,
15
+ } from '@cat-factory/contracts'
1
16
  import type {
2
17
  CreateDocumentFragmentInput,
3
18
  CreatePromptFragmentInput,
4
19
  FragmentOwnerKind,
5
- FragmentSource,
6
- FragmentSourceStatus,
7
- FragmentSyncResult,
8
20
  LinkFragmentSourceInput,
9
- PromptFragment,
10
- ResolvedFragment,
11
21
  UpdatePromptFragmentInput,
12
22
  } from '~/types/domain'
13
23
  import type { ApiContext } from './context'
14
24
 
15
25
  /** Best-practice prompt-fragment catalog + the managed, tenant-scoped library. */
16
- export function fragmentsApi({ http, ws, scope }: ApiContext) {
26
+ export function fragmentsApi({ send, ws, scope }: ApiContext) {
17
27
  return {
18
28
  // ---- prompt fragments (best-practice catalog) -------------------------
19
- getPromptFragments: () => http<PromptFragment[]>('/prompt-fragments'),
29
+ getPromptFragments: () => send(listFragmentCatalogContract, {}),
20
30
 
21
31
  // ---- prompt-fragment library (managed, tenant-scoped; ADR 0006) -------
22
32
  // The merged catalog an agent actually sees for a board (builtin∪account∪ws).
23
33
  getResolvedFragments: (workspaceId: string) =>
24
- http<ResolvedFragment[]>(`${ws(workspaceId)}/prompt-fragments/resolved`),
34
+ send(resolvedFragmentsContract, { pathPrefix: ws(workspaceId) }),
25
35
 
26
36
  // Per-tier management (scope = account or workspace).
27
37
  listFragments: (kind: FragmentOwnerKind, id: string) =>
28
- http<PromptFragment[]>(`${scope(kind, id)}/prompt-fragments`),
38
+ send(listPromptFragmentsContract, { pathPrefix: scope(kind, id) }),
29
39
 
30
40
  createFragment: (kind: FragmentOwnerKind, id: string, body: CreatePromptFragmentInput) =>
31
- http<PromptFragment>(`${scope(kind, id)}/prompt-fragments`, { method: 'POST', body }),
41
+ send(createPromptFragmentContract, { pathPrefix: scope(kind, id), body }),
32
42
 
33
43
  updateFragment: (
34
44
  kind: FragmentOwnerKind,
@@ -36,14 +46,16 @@ export function fragmentsApi({ http, ws, scope }: ApiContext) {
36
46
  fragmentId: string,
37
47
  body: UpdatePromptFragmentInput,
38
48
  ) =>
39
- http<PromptFragment>(
40
- `${scope(kind, id)}/prompt-fragments/${encodeURIComponent(fragmentId)}`,
41
- { method: 'PATCH', body },
42
- ),
49
+ send(updatePromptFragmentContract, {
50
+ pathPrefix: scope(kind, id),
51
+ pathParams: { fragmentId },
52
+ body,
53
+ }),
43
54
 
44
55
  deleteFragment: (kind: FragmentOwnerKind, id: string, fragmentId: string) =>
45
- http(`${scope(kind, id)}/prompt-fragments/${encodeURIComponent(fragmentId)}`, {
46
- method: 'DELETE',
56
+ send(deletePromptFragmentContract, {
57
+ pathPrefix: scope(kind, id),
58
+ pathParams: { fragmentId },
47
59
  }),
48
60
 
49
61
  // Link an external document (Confluence/Notion/GitHub) as a living fragment.
@@ -51,7 +63,7 @@ export function fragmentsApi({ http, ws, scope }: ApiContext) {
51
63
  kind: FragmentOwnerKind,
52
64
  id: string,
53
65
  body: CreateDocumentFragmentInput,
54
- ) => http<PromptFragment>(`${scope(kind, id)}/document-fragments`, { method: 'POST', body }),
66
+ ) => send(createDocumentFragmentContract, { pathPrefix: scope(kind, id), body }),
55
67
 
56
68
  // Force an immediate live re-resolve of a document-backed fragment. At the
57
69
  // account scope the backend needs a `viaWorkspaceId` (the workspace whose
@@ -62,34 +74,35 @@ export function fragmentsApi({ http, ws, scope }: ApiContext) {
62
74
  fragmentId: string,
63
75
  viaWorkspaceId?: string,
64
76
  ) =>
65
- http<PromptFragment>(
66
- `${scope(kind, id)}/prompt-fragments/${encodeURIComponent(fragmentId)}/refresh${
67
- viaWorkspaceId ? `?viaWorkspaceId=${encodeURIComponent(viaWorkspaceId)}` : ''
68
- }`,
69
- { method: 'POST' },
70
- ),
77
+ send(refreshPromptFragmentContract, {
78
+ pathPrefix: scope(kind, id),
79
+ pathParams: { fragmentId },
80
+ queryParams: { viaWorkspaceId },
81
+ }),
71
82
 
72
83
  // Repo sources of guideline Markdown.
73
84
  listFragmentSources: (kind: FragmentOwnerKind, id: string) =>
74
- http<FragmentSource[]>(`${scope(kind, id)}/fragment-sources`),
85
+ send(listFragmentSourcesContract, { pathPrefix: scope(kind, id) }),
75
86
 
76
87
  linkFragmentSource: (kind: FragmentOwnerKind, id: string, body: LinkFragmentSourceInput) =>
77
- http<FragmentSource>(`${scope(kind, id)}/fragment-sources`, { method: 'POST', body }),
88
+ send(linkFragmentSourceContract, { pathPrefix: scope(kind, id), body }),
78
89
 
79
90
  unlinkFragmentSource: (kind: FragmentOwnerKind, id: string, sourceId: string) =>
80
- http(`${scope(kind, id)}/fragment-sources/${encodeURIComponent(sourceId)}`, {
81
- method: 'DELETE',
91
+ send(unlinkFragmentSourceContract, {
92
+ pathPrefix: scope(kind, id),
93
+ pathParams: { id: sourceId },
82
94
  }),
83
95
 
84
96
  fragmentSourceStatus: (kind: FragmentOwnerKind, id: string, sourceId: string) =>
85
- http<FragmentSourceStatus>(
86
- `${scope(kind, id)}/fragment-sources/${encodeURIComponent(sourceId)}/status`,
87
- ),
97
+ send(fragmentSourceStatusContract, {
98
+ pathPrefix: scope(kind, id),
99
+ pathParams: { id: sourceId },
100
+ }),
88
101
 
89
102
  syncFragmentSource: (kind: FragmentOwnerKind, id: string, sourceId: string) =>
90
- http<FragmentSyncResult>(
91
- `${scope(kind, id)}/fragment-sources/${encodeURIComponent(sourceId)}/sync`,
92
- { method: 'POST' },
93
- ),
103
+ send(syncFragmentSourceContract, {
104
+ pathPrefix: scope(kind, id),
105
+ pathParams: { id: sourceId },
106
+ }),
94
107
  }
95
108
  }