@cat-factory/app 0.72.0 → 0.74.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.
- package/app/components/board/nodes/TaskCard.vue +12 -2
- package/app/components/bootstrap/BootstrapModal.vue +23 -7
- package/app/components/brainstorm/BrainstormWindow.vue +2 -0
- package/app/components/clarity/ClarityReviewWindow.vue +2 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +2 -0
- package/app/components/focus/BlockFocusView.vue +2 -0
- package/app/components/followUp/FollowUpWindow.vue +2 -0
- package/app/components/gates/GateResultView.vue +2 -0
- package/app/components/github/AddServiceFromRepoModal.vue +54 -85
- package/app/components/humanTest/HumanTestWindow.vue +2 -0
- package/app/components/layout/ConnectionStatusBanner.vue +81 -0
- package/app/components/layout/NotificationsInbox.vue +15 -0
- package/app/components/panels/GenericStructuredResultView.vue +2 -0
- package/app/components/panels/InspectorPanel.vue +30 -1
- package/app/components/panels/inspector/TaskExecution.vue +25 -1
- package/app/components/pipeline/PipelineBuilder.vue +110 -7
- package/app/components/requirements/RequirementsReviewWindow.vue +2 -0
- package/app/components/spec/ServiceSpecWindow.vue +2 -0
- package/app/components/testing/TestReportWindow.vue +91 -0
- package/app/composables/api/github.ts +8 -3
- package/app/composables/useKeyboardShortcuts.ts +10 -2
- package/app/pages/index.vue +6 -4
- package/app/stores/board.spec.ts +58 -1
- package/app/stores/board.ts +59 -15
- package/app/stores/brainstorm.spec.ts +35 -0
- package/app/stores/brainstorm.ts +11 -2
- package/app/stores/clarity.spec.ts +33 -0
- package/app/stores/clarity.ts +11 -2
- package/app/stores/execution.spec.ts +71 -13
- package/app/stores/execution.ts +44 -6
- package/app/stores/github.ts +12 -3
- package/app/stores/pipelines.ts +53 -0
- package/app/stores/recurringPipelines.ts +5 -1
- package/app/stores/requirements.spec.ts +21 -0
- package/app/stores/requirements.ts +11 -2
- package/app/stores/workspace.ts +1 -1
- package/app/utils/catalog.ts +9 -0
- package/i18n/locales/en.json +56 -5
- package/i18n/locales/es.json +56 -5
- package/i18n/locales/fr.json +56 -5
- package/i18n/locales/he.json +56 -5
- package/i18n/locales/ja.json +56 -5
- package/i18n/locales/pl.json +56 -5
- package/i18n/locales/tr.json +56 -5
- package/i18n/locales/uk.json +56 -5
- package/package.json +2 -2
package/app/stores/clarity.ts
CHANGED
|
@@ -87,6 +87,16 @@ export const useClarityStore = defineStore('clarity', () => {
|
|
|
87
87
|
reviews.value = { ...reviews.value, [review.blockId]: review }
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** Patch the cache from a live `clarity` stream event (newest wins per block). */
|
|
91
|
+
function upsert(review: ClarityReview) {
|
|
92
|
+
const existing = reviews.value[review.blockId]
|
|
93
|
+
// Keep the freshest by updatedAt (the consensus-store guard): `store()` also runs on
|
|
94
|
+
// API responses, so a slightly-older event racing a just-submitted answer over the
|
|
95
|
+
// separate WS transport must not revert the review the response already delivered.
|
|
96
|
+
if (existing && existing.id === review.id && existing.updatedAt > review.updatedAt) return
|
|
97
|
+
store(review)
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
/** Drop all cached reviews + in-flight state (called on workspace switch). */
|
|
91
101
|
function reset() {
|
|
92
102
|
available.value = null
|
|
@@ -205,7 +215,6 @@ export const useClarityStore = defineStore('clarity', () => {
|
|
|
205
215
|
proceed,
|
|
206
216
|
resolveExceeded,
|
|
207
217
|
reset,
|
|
208
|
-
|
|
209
|
-
upsert: store,
|
|
218
|
+
upsert,
|
|
210
219
|
}
|
|
211
220
|
})
|
|
@@ -18,26 +18,84 @@ describe('execution store gate grouping', () => {
|
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
it('decisionsByBlock groups open (unchosen) decisions by block', () => {
|
|
21
|
-
store.hydrate(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
store.hydrate(
|
|
22
|
+
[
|
|
23
|
+
instance('e1', 'b1', [
|
|
24
|
+
{ agentKind: 'coder', decision: { id: 'd1', chosen: null } },
|
|
25
|
+
{ agentKind: 'coder', decision: { id: 'd2', chosen: 'yes' } }, // chosen ⇒ excluded
|
|
26
|
+
]),
|
|
27
|
+
instance('e2', 'b2', [{ agentKind: 'architect', decision: { id: 'd3', chosen: null } }]),
|
|
28
|
+
],
|
|
29
|
+
'ws1',
|
|
30
|
+
)
|
|
28
31
|
expect(store.decisionsByBlock.get('b1')?.map((d) => d.decision.id)).toEqual(['d1'])
|
|
29
32
|
expect(store.decisionsByBlock.get('b2')?.map((d) => d.decision.id)).toEqual(['d3'])
|
|
30
33
|
expect(store.decisionsByBlock.has('missing')).toBe(false)
|
|
31
34
|
})
|
|
32
35
|
|
|
33
36
|
it('approvalsByBlock groups pending approvals by block', () => {
|
|
34
|
-
store.hydrate(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
store.hydrate(
|
|
38
|
+
[
|
|
39
|
+
instance('e1', 'b1', [
|
|
40
|
+
{ agentKind: 'merger', approval: { id: 'a1', status: 'pending' } },
|
|
41
|
+
{ agentKind: 'merger', approval: { id: 'a2', status: 'approved' } }, // not pending ⇒ excluded
|
|
42
|
+
]),
|
|
43
|
+
],
|
|
44
|
+
'ws1',
|
|
45
|
+
)
|
|
40
46
|
expect(store.approvalsByBlock.get('b1')?.map((a) => a.approval.id)).toEqual(['a1'])
|
|
41
47
|
expect(store.approvalsByBlock.get('b2')).toBeUndefined()
|
|
42
48
|
})
|
|
43
49
|
})
|
|
50
|
+
|
|
51
|
+
/** A run fixture carrying the fields the reconcile guards read (`id`, `rev`, `status`). */
|
|
52
|
+
function run(id: string, rev: number, status: string): ExecutionInstance {
|
|
53
|
+
return { id, blockId: `blk_${id}`, steps: [], status, rev } as unknown as ExecutionInstance
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('execution store snapshot/event reconcile', () => {
|
|
57
|
+
let store: ReturnType<typeof useExecutionStore>
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
store = useExecutionStore()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('a lagging snapshot cannot regress a run a live event already advanced (REGRESS)', () => {
|
|
63
|
+
store.hydrate([run('e1', 3, 'running')], 'ws1')
|
|
64
|
+
// Live event: the run reached a terminal state (rev 4). It emits nothing further.
|
|
65
|
+
store.upsert(run('e1', 4, 'done'))
|
|
66
|
+
// A snapshot read BEFORE the event resolves after it — same run at the older rev.
|
|
67
|
+
store.hydrate([run('e1', 3, 'running')], 'ws1')
|
|
68
|
+
expect(store.getInstance('e1')?.status).toBe('done')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('keeps a live-added run a lagging snapshot never saw (DROP)', () => {
|
|
72
|
+
store.hydrate([run('e1', 1, 'running')], 'ws1')
|
|
73
|
+
store.upsert(run('e2', 1, 'running'))
|
|
74
|
+
store.hydrate([run('e1', 2, 'running')], 'ws1') // stale read: predates e2
|
|
75
|
+
expect(store.getInstance('e2')).toBeTruthy()
|
|
76
|
+
expect(store.getInstance('e1')?.rev).toBe(2)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('a workspace switch replaces the cache outright (no cross-board leak)', () => {
|
|
80
|
+
store.hydrate([run('e1', 1, 'running')], 'ws1')
|
|
81
|
+
store.upsert(run('e2', 1, 'running'))
|
|
82
|
+
store.hydrate([run('e3', 1, 'running')], 'ws2')
|
|
83
|
+
expect(store.getInstance('e1')).toBeUndefined()
|
|
84
|
+
expect(store.getInstance('e2')).toBeUndefined()
|
|
85
|
+
expect(store.getInstance('e3')).toBeTruthy()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('an out-of-order live event cannot regress a newer cached run; same-rev replaces', () => {
|
|
89
|
+
store.upsert(run('e1', 5, 'done'))
|
|
90
|
+
store.upsert(run('e1', 4, 'running')) // stale event → ignored
|
|
91
|
+
expect(store.getInstance('e1')?.status).toBe('done')
|
|
92
|
+
store.upsert(run('e1', 5, 'failed')) // equal rev → latest event wins
|
|
93
|
+
expect(store.getInstance('e1')?.status).toBe('failed')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('treats a missing rev as 0 (legacy rows still hydrate)', () => {
|
|
97
|
+
store.hydrate([{ id: 'e1', blockId: 'b1', steps: [], status: 'running' } as never], 'ws1')
|
|
98
|
+
store.upsert(run('e1', 1, 'done'))
|
|
99
|
+
expect(store.getInstance('e1')?.status).toBe('done')
|
|
100
|
+
})
|
|
101
|
+
})
|
package/app/stores/execution.ts
CHANGED
|
@@ -25,17 +25,55 @@ export const useExecutionStore = defineStore('execution', () => {
|
|
|
25
25
|
// gets identical handling, including the fire-and-forget ones that never caught.
|
|
26
26
|
const runErrors = usePipelineErrorToast()
|
|
27
27
|
const instances = ref<ExecutionInstance[]>([])
|
|
28
|
+
// The workspace whose snapshot last hydrated the cache. Scopes the DROP-preservation
|
|
29
|
+
// below: a board SWITCH replaces the cache outright instead of leaking the previous
|
|
30
|
+
// board's runs (an ExecutionInstance carries no workspaceId of its own).
|
|
31
|
+
let hydratedWorkspaceId: string | null = null
|
|
28
32
|
|
|
29
|
-
/**
|
|
30
|
-
function
|
|
31
|
-
|
|
33
|
+
/** A run's monotonic server revision (bumped on every persisted write; absent = 0). */
|
|
34
|
+
function revOf(e: ExecutionInstance): number {
|
|
35
|
+
return e.rev ?? 0
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Reconcile the cached executions with a server snapshot for `workspaceId`. A snapshot
|
|
40
|
+
* is authoritative EXCEPT where a live `execution` event already advanced (or ADDED) a
|
|
41
|
+
* run past what this (possibly stale) read observed — the same two clobber hazards the
|
|
42
|
+
* `agentRuns` store guards, keyed here on the run's monotonic `rev`:
|
|
43
|
+
* - REGRESS: a run present in BOTH — keep the newer-by-`rev` version, so a lagging
|
|
44
|
+
* refresh (the stream's on-(re)connect resync, the debounced `board`-event refetch)
|
|
45
|
+
* can't revert a just-terminal run to `running`. A terminal run emits nothing
|
|
46
|
+
* further, so a regression here would strand the UI until an unrelated refresh.
|
|
47
|
+
* - DROP: a run a live event just ADDED that the (older) snapshot never saw — keep it
|
|
48
|
+
* rather than silently dropping it.
|
|
49
|
+
*/
|
|
50
|
+
function hydrate(next: ExecutionInstance[], workspaceId: string) {
|
|
51
|
+
const sameWorkspace = hydratedWorkspaceId === workspaceId
|
|
52
|
+
hydratedWorkspaceId = workspaceId
|
|
53
|
+
if (!sameWorkspace) {
|
|
54
|
+
instances.value = next
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
const incomingIds = new Set(next.map((e) => e.id))
|
|
58
|
+
const held = new Map(instances.value.map((e) => [e.id, e]))
|
|
59
|
+
const reconciled = next.map((incoming) => {
|
|
60
|
+
const current = held.get(incoming.id)
|
|
61
|
+
return current && revOf(current) > revOf(incoming) ? current : incoming
|
|
62
|
+
})
|
|
63
|
+
const preserved = [...held.values()].filter((e) => !incomingIds.has(e.id))
|
|
64
|
+
instances.value = [...reconciled, ...preserved]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Insert or replace a single execution instance pushed by the event stream.
|
|
69
|
+
* Monotonic by `rev`: an out-of-order/stale event can't regress a run a newer
|
|
70
|
+
* write already advanced (same guard as {@link hydrate}).
|
|
71
|
+
*/
|
|
35
72
|
function upsert(instance: ExecutionInstance) {
|
|
36
73
|
const i = instances.value.findIndex((e) => e.id === instance.id)
|
|
37
|
-
if (i >= 0)
|
|
38
|
-
|
|
74
|
+
if (i >= 0) {
|
|
75
|
+
if (revOf(instance) >= revOf(instances.value[i]!)) instances.value[i] = instance
|
|
76
|
+
} else instances.value.push(instance)
|
|
39
77
|
}
|
|
40
78
|
|
|
41
79
|
const byId = computed(() => {
|
package/app/stores/github.ts
CHANGED
|
@@ -131,12 +131,21 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
131
131
|
if (connected.value && repos.value.length === 0) await load()
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/**
|
|
135
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Load the repos the installation can access, with this workspace's link state.
|
|
136
|
+
* With a `q` the backend filters `owner/name` server-side (the add-service picker
|
|
137
|
+
* searches instead of prefetching a huge installation); without one it browses all
|
|
138
|
+
* (the repo-link panel). A blank/short `q` clears the list rather than fetching.
|
|
139
|
+
*/
|
|
140
|
+
async function loadAvailableRepos(q?: string) {
|
|
136
141
|
if (!connected.value) return
|
|
142
|
+
if (q !== undefined && q.trim() === '') {
|
|
143
|
+
availableRepos.value = []
|
|
144
|
+
return
|
|
145
|
+
}
|
|
137
146
|
loadingAvailable.value = true
|
|
138
147
|
try {
|
|
139
|
-
availableRepos.value = await api.listGitHubAvailableRepos(workspace.requireId())
|
|
148
|
+
availableRepos.value = await api.listGitHubAvailableRepos(workspace.requireId(), q)
|
|
140
149
|
} finally {
|
|
141
150
|
loadingAvailable.value = false
|
|
142
151
|
}
|
package/app/stores/pipelines.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
3
|
import type { AgentKind, Pipeline } from '~/types/domain'
|
|
4
4
|
import type { ConsensusStepConfig, StepGating } from '~/types/consensus'
|
|
5
|
+
import type { TesterQualityConfig } from '@cat-factory/contracts'
|
|
5
6
|
import { companionForProducer, uid } from '~/utils/catalog'
|
|
6
7
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
7
8
|
|
|
@@ -59,6 +60,13 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
59
60
|
* a `coder` step; `false` disables the companion there (default/true ⇒ enabled).
|
|
60
61
|
*/
|
|
61
62
|
const draftFollowUps = ref<(boolean | null)[]>([])
|
|
63
|
+
/**
|
|
64
|
+
* Per-step test quality-control companion config, kept index-aligned with `draft`. Only
|
|
65
|
+
* meaningful on a Tester step (`tester-api`/`tester-ui`); `null`/absent means "enabled, no
|
|
66
|
+
* gating" (the QC companion is on by default), `{ enabled: false }` disables it, and an
|
|
67
|
+
* entry with `gating` makes it conditional on the task estimate.
|
|
68
|
+
*/
|
|
69
|
+
const draftTesterQuality = ref<(TesterQualityConfig | null)[]>([])
|
|
62
70
|
/** Organizational labels for the pipeline being assembled/edited. */
|
|
63
71
|
const draftLabels = ref<string[]>([])
|
|
64
72
|
const draftName = ref('New pipeline')
|
|
@@ -84,6 +92,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
84
92
|
draftConsensus.value.splice(index, 0, null)
|
|
85
93
|
draftGating.value.splice(index, 0, null)
|
|
86
94
|
draftFollowUps.value.splice(index, 0, null)
|
|
95
|
+
draftTesterQuality.value.splice(index, 0, null)
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
function addToDraft(kind: AgentKind) {
|
|
@@ -98,6 +107,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
98
107
|
draftConsensus.value.splice(index, 1)
|
|
99
108
|
draftGating.value.splice(index, 1)
|
|
100
109
|
draftFollowUps.value.splice(index, 1)
|
|
110
|
+
draftTesterQuality.value.splice(index, 1)
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
function moveInDraft(from: number, to: number) {
|
|
@@ -116,6 +126,8 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
116
126
|
draftGating.value.splice(to, 0, gat ?? null)
|
|
117
127
|
const [fu] = draftFollowUps.value.splice(from, 1)
|
|
118
128
|
draftFollowUps.value.splice(to, 0, fu ?? null)
|
|
129
|
+
const [tq] = draftTesterQuality.value.splice(from, 1)
|
|
130
|
+
draftTesterQuality.value.splice(to, 0, tq ?? null)
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
/** Whether the producer step at `index` currently has its companion attached after it. */
|
|
@@ -191,6 +203,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
191
203
|
draftConsensus.value = reorder(draftConsensus.value)
|
|
192
204
|
draftGating.value = reorder(draftGating.value)
|
|
193
205
|
draftFollowUps.value = reorder(draftFollowUps.value)
|
|
206
|
+
draftTesterQuality.value = reorder(draftTesterQuality.value)
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
/** Toggle the consensus mechanism on the draft step at `index` (default config / off). */
|
|
@@ -214,6 +227,33 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
214
227
|
draftFollowUps.value[index] = draftFollowUps.value[index] === false ? null : false
|
|
215
228
|
}
|
|
216
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Toggle the test quality-control companion on the draft (Tester) step at `index`. The
|
|
232
|
+
* companion is enabled by default (a `null` entry), so the first toggle disables it
|
|
233
|
+
* (`{ enabled: false }`, dropping any gating) and the next restores the default.
|
|
234
|
+
*/
|
|
235
|
+
function toggleDraftTesterQuality(index: number) {
|
|
236
|
+
draftTesterQuality.value[index] =
|
|
237
|
+
draftTesterQuality.value[index]?.enabled === false ? null : { enabled: false }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Toggle estimate gating on/off for the QC companion on the draft (Tester) step at `index`.
|
|
242
|
+
* A no-op while the companion is disabled (nothing to gate). Enabling gating pins the config
|
|
243
|
+
* to `{ enabled: true, gating }` so the thresholds are editable; disabling drops back to the
|
|
244
|
+
* default `null` (enabled, ungated).
|
|
245
|
+
*/
|
|
246
|
+
function toggleDraftTesterQualityGating(index: number) {
|
|
247
|
+
const cur = draftTesterQuality.value[index]
|
|
248
|
+
if (cur?.enabled === false) return
|
|
249
|
+
draftTesterQuality.value[index] = cur?.gating?.enabled
|
|
250
|
+
? null
|
|
251
|
+
: {
|
|
252
|
+
enabled: true,
|
|
253
|
+
gating: { enabled: true, minRisk: 0.5, minImpact: 0.5, onMissingEstimate: 'run' },
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
217
257
|
/** Enable/disable the draft step at `index` without removing it. */
|
|
218
258
|
function toggleDraftEnabled(index: number) {
|
|
219
259
|
draftEnabled.value[index] = draftEnabled.value[index] === false
|
|
@@ -227,6 +267,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
227
267
|
draftConsensus.value = []
|
|
228
268
|
draftGating.value = []
|
|
229
269
|
draftFollowUps.value = []
|
|
270
|
+
draftTesterQuality.value = []
|
|
230
271
|
draftLabels.value = []
|
|
231
272
|
draftName.value = 'New pipeline'
|
|
232
273
|
editingId.value = null
|
|
@@ -241,6 +282,9 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
241
282
|
draftConsensus.value = pipeline.agentKinds.map((_, i) => pipeline.consensus?.[i] ?? null)
|
|
242
283
|
draftGating.value = pipeline.agentKinds.map((_, i) => pipeline.gating?.[i] ?? null)
|
|
243
284
|
draftFollowUps.value = pipeline.agentKinds.map((_, i) => pipeline.followUps?.[i] ?? null)
|
|
285
|
+
draftTesterQuality.value = pipeline.agentKinds.map(
|
|
286
|
+
(_, i) => pipeline.testerQuality?.[i] ?? null,
|
|
287
|
+
)
|
|
244
288
|
draftLabels.value = [...(pipeline.labels ?? [])]
|
|
245
289
|
draftName.value = pipeline.name
|
|
246
290
|
editingId.value = pipeline.id
|
|
@@ -270,6 +314,12 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
270
314
|
...(draftFollowUps.value.some((f) => f === false)
|
|
271
315
|
? { followUps: [...draftFollowUps.value] }
|
|
272
316
|
: {}),
|
|
317
|
+
// Only send testerQuality when at least one Tester step deviates from the default
|
|
318
|
+
// (companion disabled, or an estimate gate configured) — the default (null/enabled,
|
|
319
|
+
// ungated) is not worth persisting.
|
|
320
|
+
...(draftTesterQuality.value.some((q) => q?.enabled === false || q?.gating?.enabled)
|
|
321
|
+
? { testerQuality: [...draftTesterQuality.value] }
|
|
322
|
+
: {}),
|
|
273
323
|
// Only send labels when there are any.
|
|
274
324
|
...(draftLabels.value.length ? { labels: [...draftLabels.value] } : {}),
|
|
275
325
|
}
|
|
@@ -342,6 +392,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
342
392
|
draftConsensus,
|
|
343
393
|
draftGating,
|
|
344
394
|
draftFollowUps,
|
|
395
|
+
draftTesterQuality,
|
|
345
396
|
draftLabels,
|
|
346
397
|
draftName,
|
|
347
398
|
editingId,
|
|
@@ -357,6 +408,8 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
357
408
|
toggleDraftGating,
|
|
358
409
|
toggleDraftGate,
|
|
359
410
|
toggleDraftFollowUps,
|
|
411
|
+
toggleDraftTesterQuality,
|
|
412
|
+
toggleDraftTesterQualityGating,
|
|
360
413
|
toggleDraftEnabled,
|
|
361
414
|
toggleDraftConsensus,
|
|
362
415
|
setDraftConsensus,
|
|
@@ -13,6 +13,10 @@ import { useBoardStore } from '~/stores/board'
|
|
|
13
13
|
export const useRecurringPipelinesStore = defineStore('recurringPipelines', () => {
|
|
14
14
|
const api = useApi()
|
|
15
15
|
const toast = useToast()
|
|
16
|
+
// Resolve translations through the Nuxt app's global i18n instance — a store runs outside a
|
|
17
|
+
// component `setup`, so `useI18n()` is unavailable (see the board store for the same pattern).
|
|
18
|
+
const nuxtApp = useNuxtApp()
|
|
19
|
+
const tr = (key: string): string => (nuxtApp.$i18n as { t: (k: string) => string }).t(key)
|
|
16
20
|
|
|
17
21
|
const schedules = ref<PipelineSchedule[]>([])
|
|
18
22
|
/** Lazily-loaded run history, keyed by schedule id. */
|
|
@@ -68,7 +72,7 @@ export const useRecurringPipelinesStore = defineStore('recurringPipelines', () =
|
|
|
68
72
|
schedules.value = prevSchedules
|
|
69
73
|
if (blockSnap) board.reattach(blockSnap)
|
|
70
74
|
toast.add({
|
|
71
|
-
title: '
|
|
75
|
+
title: tr('board.toast.recurringDeleteFailed'),
|
|
72
76
|
description: e instanceof Error ? e.message : String(e),
|
|
73
77
|
icon: 'i-lucide-triangle-alert',
|
|
74
78
|
color: 'error',
|
|
@@ -92,3 +92,24 @@ describe('requirements store load() loading flag', () => {
|
|
|
92
92
|
expect(calls).toBe(2)
|
|
93
93
|
})
|
|
94
94
|
})
|
|
95
|
+
|
|
96
|
+
describe('requirements store live-event upsert guard', () => {
|
|
97
|
+
it('an out-of-order stream event cannot revert a newer cached review', () => {
|
|
98
|
+
const store = useRequirementsStore()
|
|
99
|
+
// The API response for a just-submitted answer landed first (newer updatedAt)…
|
|
100
|
+
store.upsert(review({ updatedAt: 2000, status: 'merged' }))
|
|
101
|
+
// …then the slightly-older stream event (emitted just before) arrives late.
|
|
102
|
+
store.upsert(review({ updatedAt: 1000, status: 'ready' }))
|
|
103
|
+
expect(store.reviewFor('b1')?.status).toBe('merged')
|
|
104
|
+
// A genuinely newer event still applies.
|
|
105
|
+
store.upsert(review({ updatedAt: 3000, status: 'incorporated' }))
|
|
106
|
+
expect(store.reviewFor('b1')?.status).toBe('incorporated')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('a NEW review (different id) for the block replaces regardless of updatedAt', () => {
|
|
110
|
+
const store = useRequirementsStore()
|
|
111
|
+
store.upsert(review({ updatedAt: 2000 }))
|
|
112
|
+
store.upsert(review({ id: 'rr2', updatedAt: 1000 }))
|
|
113
|
+
expect(store.reviewFor('b1')?.id).toBe('rr2')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -97,6 +97,16 @@ export const useRequirementsStore = defineStore('requirements', () => {
|
|
|
97
97
|
reviews.value = { ...reviews.value, [review.blockId]: review }
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** Patch the cache from a live `requirements` stream event (newest wins per block). */
|
|
101
|
+
function upsert(review: RequirementReview) {
|
|
102
|
+
const existing = reviews.value[review.blockId]
|
|
103
|
+
// Keep the freshest by updatedAt (the consensus-store guard): `store()` also runs on
|
|
104
|
+
// API responses, so a slightly-older event racing a just-submitted answer over the
|
|
105
|
+
// separate WS transport must not revert the review the response already delivered.
|
|
106
|
+
if (existing && existing.id === review.id && existing.updatedAt > review.updatedAt) return
|
|
107
|
+
store(review)
|
|
108
|
+
}
|
|
109
|
+
|
|
100
110
|
/** Drop all cached reviews + in-flight state (called on workspace switch). */
|
|
101
111
|
function reset() {
|
|
102
112
|
available.value = null
|
|
@@ -277,7 +287,6 @@ export const useRequirementsStore = defineStore('requirements', () => {
|
|
|
277
287
|
rejectRecommendation,
|
|
278
288
|
reRequestRecommendation,
|
|
279
289
|
reset,
|
|
280
|
-
|
|
281
|
-
upsert: store,
|
|
290
|
+
upsert,
|
|
282
291
|
}
|
|
283
292
|
})
|
package/app/stores/workspace.ts
CHANGED
|
@@ -91,7 +91,7 @@ export const useWorkspaceStore = defineStore(
|
|
|
91
91
|
else workspaces.value.unshift(snapshot.workspace)
|
|
92
92
|
useBoardStore().hydrate(snapshot.blocks)
|
|
93
93
|
usePipelinesStore().hydrate(snapshot.pipelines, snapshot.pipelineCatalogVersions)
|
|
94
|
-
useExecutionStore().hydrate(snapshot.executions)
|
|
94
|
+
useExecutionStore().hydrate(snapshot.executions, snapshot.workspace.id)
|
|
95
95
|
useAgentRunsStore().hydrate(snapshot.bootstrapJobs ?? [], snapshot.workspace.id)
|
|
96
96
|
useAgentRunsStore().hydrateEnvConfigRepair(snapshot.envConfigRepairJobs ?? [])
|
|
97
97
|
useNotificationsStore().hydrate(snapshot.notifications ?? [])
|
package/app/utils/catalog.ts
CHANGED
|
@@ -312,6 +312,15 @@ export function isConsensusEligibleKind(kind: string): boolean {
|
|
|
312
312
|
return CONSENSUS_ELIGIBLE_KINDS.has(kind)
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Whether an agent kind is one of the Tester gate kinds (API or UI). Mirrors the backend
|
|
317
|
+
* `isTesterKind`; used by the pipeline builder to surface the test quality-control companion
|
|
318
|
+
* toggle only on Tester steps.
|
|
319
|
+
*/
|
|
320
|
+
export function isTesterKind(kind: string): boolean {
|
|
321
|
+
return kind === 'tester-api' || kind === 'tester-ui'
|
|
322
|
+
}
|
|
323
|
+
|
|
315
324
|
/**
|
|
316
325
|
* Display metadata for the engine-driven "system" kinds — the gate/automation
|
|
317
326
|
* steps (blueprint mapper, conflicts gate + resolver, CI gate + fixer, merger)
|
package/i18n/locales/en.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
+
"app": {
|
|
3
|
+
"loading": "Loading…",
|
|
4
|
+
"loadingBoard": "Loading board…",
|
|
5
|
+
"backendUnreachable": "Can't reach the backend",
|
|
6
|
+
"reconnecting": "Reconnecting…"
|
|
7
|
+
},
|
|
2
8
|
"language": {
|
|
3
9
|
"switcher": "Language",
|
|
4
10
|
"warning": {
|
|
@@ -83,6 +89,14 @@
|
|
|
83
89
|
"accountSettings": "Account settings"
|
|
84
90
|
},
|
|
85
91
|
"board": {
|
|
92
|
+
"toast": {
|
|
93
|
+
"updateFailed": "Could not save changes",
|
|
94
|
+
"epicFailed": "Could not change epic",
|
|
95
|
+
"moveFailed": "Could not move",
|
|
96
|
+
"deleteFailed": "Could not delete",
|
|
97
|
+
"linkFailed": "Could not link tasks",
|
|
98
|
+
"recurringDeleteFailed": "Could not delete recurring pipeline"
|
|
99
|
+
},
|
|
86
100
|
"repoTypes": {
|
|
87
101
|
"service": "Service",
|
|
88
102
|
"frontend": "Frontend",
|
|
@@ -291,6 +305,11 @@
|
|
|
291
305
|
"openPrOnGithub": "Open {pr} on GitHub",
|
|
292
306
|
"review": "Review",
|
|
293
307
|
"merge": "Merge",
|
|
308
|
+
"mergeConfirm": {
|
|
309
|
+
"title": "Merge this pull request?",
|
|
310
|
+
"body": "This merges the PR into its base branch and completes the task. This can't be undone.",
|
|
311
|
+
"confirm": "Merge"
|
|
312
|
+
},
|
|
294
313
|
"implemented": "implemented",
|
|
295
314
|
"module": "Module: {name}",
|
|
296
315
|
"buildSteps": "Build steps",
|
|
@@ -645,6 +664,16 @@
|
|
|
645
664
|
"stopTooltip": "Stop the run but keep it (readable and retryable)",
|
|
646
665
|
"reset": "Reset",
|
|
647
666
|
"resetTooltip": "Discard this run and reset the task to planned",
|
|
667
|
+
"resetConfirm": {
|
|
668
|
+
"title": "Discard this run?",
|
|
669
|
+
"body": "This deletes the run and returns the task to planned. This can't be undone.",
|
|
670
|
+
"confirm": "Discard run"
|
|
671
|
+
},
|
|
672
|
+
"mergeConfirm": {
|
|
673
|
+
"title": "Merge this pull request?",
|
|
674
|
+
"body": "This merges the PR into its base branch and completes the task. This can't be undone.",
|
|
675
|
+
"confirm": "Merge"
|
|
676
|
+
},
|
|
648
677
|
"viewDetailsOutput": "View details and read output",
|
|
649
678
|
"viewDetails": "View step details",
|
|
650
679
|
"companion": "Companion",
|
|
@@ -1228,7 +1257,9 @@
|
|
|
1228
1257
|
"dismiss": "Dismiss",
|
|
1229
1258
|
"toast": {
|
|
1230
1259
|
"acted": "Marked as handled",
|
|
1231
|
-
"dismissed": "Dismissed"
|
|
1260
|
+
"dismissed": "Dismissed",
|
|
1261
|
+
"actFailed": "Could not complete that action",
|
|
1262
|
+
"dismissFailed": "Could not dismiss"
|
|
1232
1263
|
},
|
|
1233
1264
|
"action": {
|
|
1234
1265
|
"merge_review": "Merge",
|
|
@@ -2675,6 +2706,10 @@
|
|
|
2675
2706
|
"consensusRevertTooltip": "Consensus enabled. Click to revert to a single agent.",
|
|
2676
2707
|
"followUpEnableTooltip": "Follow-up companion disabled. Click to enable (Coder surfaces loose ends / questions).",
|
|
2677
2708
|
"followUpDisableTooltip": "Follow-up companion enabled. Coder surfaces loose ends / side-tasks / questions; click to disable.",
|
|
2709
|
+
"testerQualityEnableTooltip": "Test quality companion disabled. Click to enable (audits the report for coverage and loops the Tester on gaps).",
|
|
2710
|
+
"testerQualityDisableTooltip": "Test quality companion enabled. Audits the report for coverage before greenlight and loops the Tester on gaps; click to disable.",
|
|
2711
|
+
"testerQualityLabel": "Test quality companion",
|
|
2712
|
+
"testerQualityGateTooltip": "Only run the quality audit when the task estimate clears a threshold (needs a Task Estimator earlier)",
|
|
2678
2713
|
"moveUp": "Move step up",
|
|
2679
2714
|
"moveDown": "Move step down",
|
|
2680
2715
|
"removeStep": "Remove this step from the pipeline",
|
|
@@ -3365,6 +3400,15 @@
|
|
|
3365
3400
|
"failed": "Failed",
|
|
3366
3401
|
"addressed": "Addressing"
|
|
3367
3402
|
},
|
|
3403
|
+
"quality": {
|
|
3404
|
+
"heading": "Coverage review",
|
|
3405
|
+
"reruns": "Quality-driven Tester re-runs",
|
|
3406
|
+
"rerunCount": "{attempts}/{max} re-runs",
|
|
3407
|
+
"exceeded": "Budget spent",
|
|
3408
|
+
"adequate": "Coverage adequate",
|
|
3409
|
+
"inadequate": "Coverage gaps found",
|
|
3410
|
+
"gaps": "Gaps to close"
|
|
3411
|
+
},
|
|
3368
3412
|
"empty": {
|
|
3369
3413
|
"title": "No test report yet.",
|
|
3370
3414
|
"hint": "The report appears once the Tester finishes a pass. While it runs, the step shows live progress on the board."
|
|
@@ -3837,6 +3881,7 @@
|
|
|
3837
3881
|
},
|
|
3838
3882
|
"targetRepo": {
|
|
3839
3883
|
"label": "Target repository name",
|
|
3884
|
+
"namePlaceholder": "payments-service",
|
|
3840
3885
|
"descWithOwner": "Create a fresh repo with this name under {owner}, then bootstrap pushes into it. A prepopulated README, .gitignore or license is fine.",
|
|
3841
3886
|
"descNoOwner": "Create a fresh repo with this name, then bootstrap pushes into it. A prepopulated README, .gitignore or license is fine."
|
|
3842
3887
|
},
|
|
@@ -3856,7 +3901,8 @@
|
|
|
3856
3901
|
},
|
|
3857
3902
|
"description": {
|
|
3858
3903
|
"label": "Description",
|
|
3859
|
-
"help": "Optional one-line summary for the repo."
|
|
3904
|
+
"help": "Optional one-line summary for the repo.",
|
|
3905
|
+
"placeholder": "Handles payment intents and refunds"
|
|
3860
3906
|
},
|
|
3861
3907
|
"instructions": {
|
|
3862
3908
|
"labelReference": "Extra instructions for the bootstrapper",
|
|
@@ -3886,18 +3932,23 @@
|
|
|
3886
3932
|
"add": "Add",
|
|
3887
3933
|
"pickRepo": {
|
|
3888
3934
|
"label": "Pick an existing GitHub repo",
|
|
3889
|
-
"description": "Choose a repo you can access to fill in its owner and name, or enter them manually below."
|
|
3935
|
+
"description": "Choose a repo you can access to fill in its owner and name, or enter them manually below.",
|
|
3936
|
+
"placeholder": "owner/name"
|
|
3890
3937
|
},
|
|
3891
3938
|
"name": {
|
|
3892
3939
|
"label": "Name",
|
|
3893
|
-
"description": "A friendly label for this base."
|
|
3940
|
+
"description": "A friendly label for this base.",
|
|
3941
|
+
"placeholder": "Service Template"
|
|
3894
3942
|
},
|
|
3895
3943
|
"repoOwner": "Repo owner",
|
|
3944
|
+
"repoOwnerPlaceholder": "acme",
|
|
3896
3945
|
"repoName": "Repo name",
|
|
3946
|
+
"repoNamePlaceholder": "service-template",
|
|
3897
3947
|
"descriptionPlaceholder": "Optional summary of this base",
|
|
3898
3948
|
"defaultInstructions": {
|
|
3899
3949
|
"label": "Default bootstrapper instructions",
|
|
3900
|
-
"description": "Prepended to the per-run instructions whenever this base is used."
|
|
3950
|
+
"description": "Prepended to the per-run instructions whenever this base is used.",
|
|
3951
|
+
"placeholder": "e.g. keep the structure; rename packages to match the new service"
|
|
3901
3952
|
}
|
|
3902
3953
|
},
|
|
3903
3954
|
"toast": {
|