@cat-factory/app 0.6.0 → 0.7.2

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.
@@ -1,87 +1,87 @@
1
- import { defineStore } from 'pinia'
2
- import { computed, ref } from 'vue'
3
- import type { Service, WorkspaceMount } from '~/types/services'
4
- import { useWorkspaceStore } from '~/stores/workspace'
5
-
6
- /**
7
- * In-org shared services. A `Service` is account-owned (a service frame + its subtree + repo)
8
- * and can be mounted onto several teams' boards; a `WorkspaceMount` places one onto THIS
9
- * board with its own frame layout. Hydrated from the workspace snapshot:
10
- * - `mounts` — the services this board mounts (drives the per-board frame layout),
11
- * - `catalog` — the org's services this board can mount from (each with a `mountCount`).
12
- */
13
- export const useServicesStore = defineStore('services', () => {
14
- const api = useApi()
15
-
16
- const mounts = ref<WorkspaceMount[]>([])
17
- const catalog = ref<Service[]>([])
18
-
19
- function hydrate(nextMounts: WorkspaceMount[], nextCatalog: Service[]) {
20
- mounts.value = [...nextMounts]
21
- catalog.value = [...nextCatalog]
22
- }
23
-
24
- /** Mount row keyed by service id. */
25
- const byServiceId = computed<Record<string, WorkspaceMount>>(() => {
26
- const map: Record<string, WorkspaceMount> = {}
27
- for (const m of mounts.value) map[m.serviceId] = m
28
- return map
29
- })
30
-
31
- /** Catalog service keyed by its frame block id (resolve a frame → its service). */
32
- const serviceByFrameBlock = computed<Record<string, Service>>(() => {
33
- const map: Record<string, Service> = {}
34
- for (const s of catalog.value) map[s.frameBlockId] = s
35
- return map
36
- })
37
-
38
- /** Org services NOT yet mounted on this board (the "add existing service" picker's options). */
39
- const mountable = computed<Service[]>(() => {
40
- const mounted = new Set(mounts.value.map((m) => m.serviceId))
41
- return catalog.value.filter((s) => !mounted.has(s.id))
42
- })
43
-
44
- /** A frame is "shared" when its service is mounted on more than one board. */
45
- function isSharedFrame(frameBlockId: string): boolean {
46
- return (serviceByFrameBlock.value[frameBlockId]?.mountCount ?? 0) > 1
47
- }
48
-
49
- async function mount(serviceId: string, position?: { x: number; y: number }) {
50
- const ws = useWorkspaceStore()
51
- const created = await api.mountService(ws.requireId(), serviceId, position ? { position } : {})
52
- await ws.refresh()
53
- return created
54
- }
55
-
56
- async function unmount(serviceId: string) {
57
- const ws = useWorkspaceStore()
58
- await api.unmountService(ws.requireId(), serviceId)
59
- await ws.refresh()
60
- }
61
-
62
- /** Persist a mounted frame's per-board layout (called on frame drag/resize end). */
63
- async function updateLayout(
64
- serviceId: string,
65
- position?: { x: number; y: number },
66
- size?: { w: number; h: number } | null,
67
- ) {
68
- const ws = useWorkspaceStore()
69
- const updated = await api.updateMountLayout(ws.requireId(), serviceId, { position, size })
70
- const local = mounts.value.find((m) => m.serviceId === serviceId)
71
- if (local) Object.assign(local, updated)
72
- return updated
73
- }
74
-
75
- return {
76
- mounts,
77
- catalog,
78
- byServiceId,
79
- serviceByFrameBlock,
80
- mountable,
81
- isSharedFrame,
82
- hydrate,
83
- mount,
84
- unmount,
85
- updateLayout,
86
- }
87
- })
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type { Service, WorkspaceMount } from '~/types/services'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+
6
+ /**
7
+ * In-org shared services. A `Service` is account-owned (a service frame + its subtree + repo)
8
+ * and can be mounted onto several teams' boards; a `WorkspaceMount` places one onto THIS
9
+ * board with its own frame layout. Hydrated from the workspace snapshot:
10
+ * - `mounts` — the services this board mounts (drives the per-board frame layout),
11
+ * - `catalog` — the org's services this board can mount from (each with a `mountCount`).
12
+ */
13
+ export const useServicesStore = defineStore('services', () => {
14
+ const api = useApi()
15
+
16
+ const mounts = ref<WorkspaceMount[]>([])
17
+ const catalog = ref<Service[]>([])
18
+
19
+ function hydrate(nextMounts: WorkspaceMount[], nextCatalog: Service[]) {
20
+ mounts.value = [...nextMounts]
21
+ catalog.value = [...nextCatalog]
22
+ }
23
+
24
+ /** Mount row keyed by service id. */
25
+ const byServiceId = computed<Record<string, WorkspaceMount>>(() => {
26
+ const map: Record<string, WorkspaceMount> = {}
27
+ for (const m of mounts.value) map[m.serviceId] = m
28
+ return map
29
+ })
30
+
31
+ /** Catalog service keyed by its frame block id (resolve a frame → its service). */
32
+ const serviceByFrameBlock = computed<Record<string, Service>>(() => {
33
+ const map: Record<string, Service> = {}
34
+ for (const s of catalog.value) map[s.frameBlockId] = s
35
+ return map
36
+ })
37
+
38
+ /** Org services NOT yet mounted on this board (the "add existing service" picker's options). */
39
+ const mountable = computed<Service[]>(() => {
40
+ const mounted = new Set(mounts.value.map((m) => m.serviceId))
41
+ return catalog.value.filter((s) => !mounted.has(s.id))
42
+ })
43
+
44
+ /** A frame is "shared" when its service is mounted on more than one board. */
45
+ function isSharedFrame(frameBlockId: string): boolean {
46
+ return (serviceByFrameBlock.value[frameBlockId]?.mountCount ?? 0) > 1
47
+ }
48
+
49
+ async function mount(serviceId: string, position?: { x: number; y: number }) {
50
+ const ws = useWorkspaceStore()
51
+ const created = await api.mountService(ws.requireId(), serviceId, position ? { position } : {})
52
+ await ws.refresh()
53
+ return created
54
+ }
55
+
56
+ async function unmount(serviceId: string) {
57
+ const ws = useWorkspaceStore()
58
+ await api.unmountService(ws.requireId(), serviceId)
59
+ await ws.refresh()
60
+ }
61
+
62
+ /** Persist a mounted frame's per-board layout (called on frame drag/resize end). */
63
+ async function updateLayout(
64
+ serviceId: string,
65
+ position?: { x: number; y: number },
66
+ size?: { w: number; h: number } | null,
67
+ ) {
68
+ const ws = useWorkspaceStore()
69
+ const updated = await api.updateMountLayout(ws.requireId(), serviceId, { position, size })
70
+ const local = mounts.value.find((m) => m.serviceId === serviceId)
71
+ if (local) Object.assign(local, updated)
72
+ return updated
73
+ }
74
+
75
+ return {
76
+ mounts,
77
+ catalog,
78
+ byServiceId,
79
+ serviceByFrameBlock,
80
+ mountable,
81
+ isSharedFrame,
82
+ hydrate,
83
+ mount,
84
+ unmount,
85
+ updateLayout,
86
+ }
87
+ })
@@ -1,27 +1,39 @@
1
- import { defineStore } from 'pinia'
2
- import { ref } from 'vue'
3
- import type { PutTrackerSettingsInput, TrackerSettings } from '~/types/tracker'
4
- import { useWorkspaceStore } from '~/stores/workspace'
5
-
6
- /**
7
- * The workspace's issue-tracker selection (GitHub Issues or Jira) — where the
8
- * tech-debt recurring pipeline files its ticket. Hydrated from the snapshot;
9
- * edited inline when configuring a tech-debt recurring pipeline.
10
- */
11
- export const useTrackerStore = defineStore('tracker', () => {
12
- const api = useApi()
13
-
14
- const settings = ref<TrackerSettings>({ tracker: null, jiraProjectKey: null, updatedAt: 0 })
15
-
16
- function hydrate(value: TrackerSettings | undefined) {
17
- settings.value = value ?? { tracker: null, jiraProjectKey: null, updatedAt: 0 }
18
- }
19
-
20
- async function save(input: PutTrackerSettingsInput) {
21
- const ws = useWorkspaceStore()
22
- settings.value = await api.putTrackerSettings(ws.requireId(), input)
23
- return settings.value
24
- }
25
-
26
- return { settings, hydrate, save }
27
- })
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type { PutTrackerSettingsInput, TrackerSettings } from '~/types/tracker'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+
6
+ /**
7
+ * The workspace's issue-tracker selection (GitHub Issues or Jira) — where the
8
+ * tech-debt recurring pipeline files its ticket. Hydrated from the snapshot;
9
+ * edited inline when configuring a tech-debt recurring pipeline.
10
+ */
11
+ export const useTrackerStore = defineStore('tracker', () => {
12
+ const api = useApi()
13
+
14
+ const settings = ref<TrackerSettings>({
15
+ tracker: null,
16
+ jiraProjectKey: null,
17
+ writebackCommentOnPrOpen: false,
18
+ writebackResolveOnMerge: false,
19
+ updatedAt: 0,
20
+ })
21
+
22
+ function hydrate(value: TrackerSettings | undefined) {
23
+ settings.value = value ?? {
24
+ tracker: null,
25
+ jiraProjectKey: null,
26
+ writebackCommentOnPrOpen: false,
27
+ writebackResolveOnMerge: false,
28
+ updatedAt: 0,
29
+ }
30
+ }
31
+
32
+ async function save(input: PutTrackerSettingsInput) {
33
+ const ws = useWorkspaceStore()
34
+ settings.value = await api.putTrackerSettings(ws.requireId(), input)
35
+ return settings.value
36
+ }
37
+
38
+ return { settings, hydrate, save }
39
+ })
package/app/stores/ui.ts CHANGED
@@ -70,6 +70,9 @@ export const useUiStore = defineStore('ui', () => {
70
70
  // Workspace-settings panel: the run-timing escalation threshold + per-service task limit.
71
71
  const workspaceSettingsOpen = ref(false)
72
72
  const datadogOpen = ref(false)
73
+ // Workspace-settings panel: issue-tracker writeback toggles (comment on PR open,
74
+ // close linked issue on merge).
75
+ const issueWritebackOpen = ref(false)
73
76
  const modelDefaultsOpen = ref(false)
74
77
  // Workspace-settings panel: the default service-fragment selection new services inherit.
75
78
  const serviceFragmentDefaultsOpen = ref(false)
@@ -287,6 +290,12 @@ export const useUiStore = defineStore('ui', () => {
287
290
  function closeWorkspaceSettings() {
288
291
  workspaceSettingsOpen.value = false
289
292
  }
293
+ function openIssueWriteback() {
294
+ issueWritebackOpen.value = true
295
+ }
296
+ function closeIssueWriteback() {
297
+ issueWritebackOpen.value = false
298
+ }
290
299
  function openDatadog() {
291
300
  datadogOpen.value = true
292
301
  }
@@ -360,6 +369,7 @@ export const useUiStore = defineStore('ui', () => {
360
369
  fragmentLibraryOpen,
361
370
  commandBarOpen,
362
371
  mergeThresholdsOpen,
372
+ issueWritebackOpen,
363
373
  workspaceSettingsOpen,
364
374
  datadogOpen,
365
375
  modelDefaultsOpen,
@@ -411,6 +421,8 @@ export const useUiStore = defineStore('ui', () => {
411
421
  toggleCommandBar,
412
422
  openMergeThresholds,
413
423
  closeMergeThresholds,
424
+ openIssueWriteback,
425
+ closeIssueWriteback,
414
426
  openWorkspaceSettings,
415
427
  closeWorkspaceSettings,
416
428
  openDatadog,
@@ -1,104 +1,104 @@
1
- // ---------------------------------------------------------------------------
2
- // Document-source integration. Requirements / RFCs / PRDs imported from external
3
- // sources (Confluence, Notion, …) can be expanded into board structure or
4
- // attached to a task as agent context. These mirror the `@cat-factory/contracts`
5
- // document schemas; the abstraction is source-agnostic, keyed by `source`.
6
- // ---------------------------------------------------------------------------
7
-
8
- import type { BlockType } from './domain'
9
-
10
- /** The external document sources cat-factory can link to. */
11
- export type DocumentSourceKind = 'confluence' | 'notion' | 'github'
12
-
13
- /** One credential a provider needs to connect (rendered as a form field). */
14
- export interface CredentialField {
15
- key: string
16
- label: string
17
- help?: string
18
- placeholder?: string
19
- secret?: boolean
20
- }
21
-
22
- /** A source's self-description: drives the generic connect + import UI. */
23
- export interface DocumentSourceDescriptor {
24
- source: DocumentSourceKind
25
- label: string
26
- /** Lucide icon name for the source. */
27
- icon: string
28
- credentialFields: CredentialField[]
29
- refLabel: string
30
- refPlaceholder: string
31
- /** Whether the source supports searching its catalogue by title/content. */
32
- searchable?: boolean
33
- }
34
-
35
- /** A workspace's connection to a document source (never carries credentials). */
36
- export interface DocumentConnection {
37
- source: DocumentSourceKind
38
- /** Human-friendly label for what we're connected to (site URL, workspace name). */
39
- label: string
40
- /** When the connection was established (epoch ms). */
41
- connectedAt: number
42
- }
43
-
44
- /** A page imported from a source into the workspace. */
45
- export interface SourceDocument {
46
- source: DocumentSourceKind
47
- /** The source's stable id for the page. */
48
- externalId: string
49
- title: string
50
- url: string
51
- /** Short plain-text preview of the page body. */
52
- excerpt: string
53
- /** The board block this document is attached to as context, if any. */
54
- linkedBlockId: string | null
55
- syncedAt: number
56
- }
57
-
58
- /** A lean hit from searching a document source's catalogue (not yet imported). */
59
- export interface DocumentSearchResult {
60
- source: DocumentSourceKind
61
- /** The source's stable id for the page (re-usable as an import ref). */
62
- externalId: string
63
- title: string
64
- url: string
65
- /** Short plain-text preview (may be empty). */
66
- excerpt: string
67
- }
68
-
69
- /** A proposed task within a planned frame/module. */
70
- export interface PlanTask {
71
- title: string
72
- description?: string
73
- }
74
-
75
- /** A proposed module grouping tasks within a planned frame. */
76
- export interface PlanModule {
77
- name: string
78
- tasks: PlanTask[]
79
- }
80
-
81
- /** A proposed top-level frame with its modules and loose tasks. */
82
- export interface PlanFrame {
83
- type: BlockType
84
- title: string
85
- description?: string
86
- modules: PlanModule[]
87
- tasks: PlanTask[]
88
- }
89
-
90
- /** A board structure extracted from an imported document. */
91
- export interface DocumentBoardPlan {
92
- source: DocumentSourceKind
93
- externalId: string
94
- /** Whether an LLM produced the plan or the deterministic heading parser did. */
95
- planner: 'llm' | 'headings'
96
- frames: PlanFrame[]
97
- }
98
-
99
- /** Counts of blocks created by spawning a plan onto the board. */
100
- export interface SpawnResult {
101
- frames: number
102
- modules: number
103
- tasks: number
104
- }
1
+ // ---------------------------------------------------------------------------
2
+ // Document-source integration. Requirements / RFCs / PRDs imported from external
3
+ // sources (Confluence, Notion, …) can be expanded into board structure or
4
+ // attached to a task as agent context. These mirror the `@cat-factory/contracts`
5
+ // document schemas; the abstraction is source-agnostic, keyed by `source`.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import type { BlockType } from './domain'
9
+
10
+ /** The external document sources cat-factory can link to. */
11
+ export type DocumentSourceKind = 'confluence' | 'notion' | 'github'
12
+
13
+ /** One credential a provider needs to connect (rendered as a form field). */
14
+ export interface CredentialField {
15
+ key: string
16
+ label: string
17
+ help?: string
18
+ placeholder?: string
19
+ secret?: boolean
20
+ }
21
+
22
+ /** A source's self-description: drives the generic connect + import UI. */
23
+ export interface DocumentSourceDescriptor {
24
+ source: DocumentSourceKind
25
+ label: string
26
+ /** Lucide icon name for the source. */
27
+ icon: string
28
+ credentialFields: CredentialField[]
29
+ refLabel: string
30
+ refPlaceholder: string
31
+ /** Whether the source supports searching its catalogue by title/content. */
32
+ searchable?: boolean
33
+ }
34
+
35
+ /** A workspace's connection to a document source (never carries credentials). */
36
+ export interface DocumentConnection {
37
+ source: DocumentSourceKind
38
+ /** Human-friendly label for what we're connected to (site URL, workspace name). */
39
+ label: string
40
+ /** When the connection was established (epoch ms). */
41
+ connectedAt: number
42
+ }
43
+
44
+ /** A page imported from a source into the workspace. */
45
+ export interface SourceDocument {
46
+ source: DocumentSourceKind
47
+ /** The source's stable id for the page. */
48
+ externalId: string
49
+ title: string
50
+ url: string
51
+ /** Short plain-text preview of the page body. */
52
+ excerpt: string
53
+ /** The board block this document is attached to as context, if any. */
54
+ linkedBlockId: string | null
55
+ syncedAt: number
56
+ }
57
+
58
+ /** A lean hit from searching a document source's catalogue (not yet imported). */
59
+ export interface DocumentSearchResult {
60
+ source: DocumentSourceKind
61
+ /** The source's stable id for the page (re-usable as an import ref). */
62
+ externalId: string
63
+ title: string
64
+ url: string
65
+ /** Short plain-text preview (may be empty). */
66
+ excerpt: string
67
+ }
68
+
69
+ /** A proposed task within a planned frame/module. */
70
+ export interface PlanTask {
71
+ title: string
72
+ description?: string
73
+ }
74
+
75
+ /** A proposed module grouping tasks within a planned frame. */
76
+ export interface PlanModule {
77
+ name: string
78
+ tasks: PlanTask[]
79
+ }
80
+
81
+ /** A proposed top-level frame with its modules and loose tasks. */
82
+ export interface PlanFrame {
83
+ type: BlockType
84
+ title: string
85
+ description?: string
86
+ modules: PlanModule[]
87
+ tasks: PlanTask[]
88
+ }
89
+
90
+ /** A board structure extracted from an imported document. */
91
+ export interface DocumentBoardPlan {
92
+ source: DocumentSourceKind
93
+ externalId: string
94
+ /** Whether an LLM produced the plan or the deterministic heading parser did. */
95
+ planner: 'llm' | 'headings'
96
+ frames: PlanFrame[]
97
+ }
98
+
99
+ /** Counts of blocks created by spawning a plan onto the board. */
100
+ export interface SpawnResult {
101
+ frames: number
102
+ modules: number
103
+ tasks: number
104
+ }
@@ -23,7 +23,7 @@ import type { ClarityReview } from './clarity'
23
23
  import type { MergeThresholdPreset } from './merge'
24
24
  import type { PipelineSchedule } from './recurring'
25
25
  import type { Service, WorkspaceMount } from './services'
26
- import type { TrackerSettings } from './tracker'
26
+ import type { TrackerSettings, WritebackOverride } from './tracker'
27
27
 
28
28
  /** Lifecycle of an architecture building block. */
29
29
  export type BlockStatus =
@@ -145,6 +145,10 @@ export interface Block {
145
145
  * with the `product` role, notified when requirement review flags this task.
146
146
  */
147
147
  responsibleProductUserId?: string | null
148
+ /** task-only: override "comment on the linked issue when a PR opens"; absent/null = inherit workspace. */
149
+ trackerCommentOnPrOpen?: WritebackOverride | null
150
+ /** task-only: override "close the linked issue as resolved on merge"; absent/null = inherit workspace. */
151
+ trackerResolveOnMerge?: WritebackOverride | null
148
152
  }
149
153
 
150
154
  /**
@@ -333,6 +333,22 @@ export interface PipelineStep {
333
333
  export interface GateFailingCheck {
334
334
  name: string
335
335
  conclusion: string | null
336
+ /** GitHub web URL of the check run, so the UI can link to the failed run's logs */
337
+ url?: string | null
338
+ }
339
+
340
+ /** One helper-agent attempt the gate dispatched (mirrors `gateAttemptSchema`). */
341
+ export interface GateAttempt {
342
+ /** 1-based attempt number */
343
+ attempt: number
344
+ /** epoch ms when the helper job finished */
345
+ at: number
346
+ /** how the helper job ended */
347
+ outcome: 'completed' | 'failed'
348
+ /** the PR head commit the helper worked against, when known */
349
+ headSha?: string | null
350
+ /** the helper's own account of what it did / what remains */
351
+ summary?: string | null
336
352
  }
337
353
 
338
354
  /** Live state of a polling gate step (`ci` / `conflicts`); mirrors `gateStepStateSchema`. */
@@ -350,6 +366,8 @@ export interface GateStepState {
350
366
  lastFailureSummary?: string | null
351
367
  /** structured failing checks behind the summary (CI gate only; absent for conflicts) */
352
368
  failingChecks?: GateFailingCheck[] | null
369
+ /** history of the helper-agent attempts this gate dispatched, newest last */
370
+ attemptLog?: GateAttempt[] | null
353
371
  }
354
372
 
355
373
  /** Live state of a `tester` step's Tester→Fixer loop (mirrors `testerStepStateSchema`). */