@cat-factory/app 0.23.1 → 0.25.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/AddTaskModal.vue +18 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +141 -5
- package/app/components/panels/inspector/TaskRunSettings.vue +57 -0
- package/app/composables/api/board.ts +1 -0
- package/app/composables/api/fragments.ts +15 -0
- package/app/stores/board.ts +2 -0
- package/app/stores/fragmentLibrary.ts +25 -0
- package/app/types/domain.ts +9 -0
- package/app/types/fragments.ts +16 -0
- package/app/types/models.ts +11 -0
- package/package.json +1 -1
|
@@ -42,6 +42,9 @@ const container = computed(() =>
|
|
|
42
42
|
const title = ref('')
|
|
43
43
|
const description = ref('')
|
|
44
44
|
const saving = ref(false)
|
|
45
|
+
// Whether the user marks this as a purely technical task up front (a refactor /
|
|
46
|
+
// non-functional change). Left off ⇒ the engine infers it from the spec phase.
|
|
47
|
+
const technical = ref(false)
|
|
45
48
|
|
|
46
49
|
// The kind of task being created. `recurring` is special: it is created through the
|
|
47
50
|
// recurring-pipeline schedule flow (a schedule on the service frame), so picking it
|
|
@@ -276,6 +279,7 @@ watch(open, (isOpen) => {
|
|
|
276
279
|
description.value = ''
|
|
277
280
|
saving.value = false
|
|
278
281
|
taskType.value = 'feature'
|
|
282
|
+
technical.value = false
|
|
279
283
|
severity.value = ''
|
|
280
284
|
stepsToReproduce.value = ''
|
|
281
285
|
timeboxHours.value = undefined
|
|
@@ -339,6 +343,7 @@ async function add() {
|
|
|
339
343
|
...(Object.keys(agentConfigValues.value).length
|
|
340
344
|
? { agentConfig: agentConfigValues.value }
|
|
341
345
|
: {}),
|
|
346
|
+
...(technical.value ? { technical: true } : {}),
|
|
342
347
|
})
|
|
343
348
|
if (block) {
|
|
344
349
|
const failed = await linkPending(block.id, pendingContext.value)
|
|
@@ -448,6 +453,19 @@ async function add() {
|
|
|
448
453
|
/>
|
|
449
454
|
</UFormField>
|
|
450
455
|
|
|
456
|
+
<UCheckbox v-model="technical" name="technical">
|
|
457
|
+
<template #label>
|
|
458
|
+
<span class="text-sm text-slate-200">Technical task</span>
|
|
459
|
+
</template>
|
|
460
|
+
<template #description>
|
|
461
|
+
<span class="text-[11px] text-slate-500">
|
|
462
|
+
A refactor / non-functional / internal change. The implementer treats the task
|
|
463
|
+
definition as primary and the spec as a regression reference; leave off to let the
|
|
464
|
+
spec phase decide.
|
|
465
|
+
</span>
|
|
466
|
+
</template>
|
|
467
|
+
</UCheckbox>
|
|
468
|
+
|
|
451
469
|
<!-- Per-type fields. -->
|
|
452
470
|
<div v-if="taskType === 'bug'" class="grid grid-cols-2 gap-3">
|
|
453
471
|
<UFormField label="Severity">
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// + resync), and review the merged catalog (built-in ∪ account ∪ workspace) an
|
|
5
5
|
// agent is selected from per run. Workspace-tier focused; the resolved view shows
|
|
6
6
|
// every tier so the inheritance is visible.
|
|
7
|
-
import type { ResolvedFragment } from '~/types/domain'
|
|
7
|
+
import type { DocumentSourceKind, ResolvedFragment } from '~/types/domain'
|
|
8
8
|
|
|
9
9
|
const ui = useUiStore()
|
|
10
10
|
const library = useFragmentLibraryStore()
|
|
11
|
+
const documents = useDocumentsStore()
|
|
11
12
|
const toast = useToast()
|
|
12
13
|
|
|
13
14
|
const open = computed({
|
|
@@ -18,10 +19,13 @@ const open = computed({
|
|
|
18
19
|
})
|
|
19
20
|
|
|
20
21
|
watch(open, (isOpen) => {
|
|
21
|
-
if (isOpen)
|
|
22
|
+
if (isOpen) {
|
|
23
|
+
void library.probe()
|
|
24
|
+
void documents.probe()
|
|
25
|
+
}
|
|
22
26
|
})
|
|
23
27
|
|
|
24
|
-
type Tab = 'catalog' | 'authored' | 'sources'
|
|
28
|
+
type Tab = 'catalog' | 'authored' | 'documents' | 'sources'
|
|
25
29
|
const tab = ref<Tab>('catalog')
|
|
26
30
|
|
|
27
31
|
const tierLabel: Record<ResolvedFragment['tier'], string> = {
|
|
@@ -80,6 +84,42 @@ async function removeFragment(id: string) {
|
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
|
|
87
|
+
// ---- document-backed (living) fragments -----------------------------------
|
|
88
|
+
// Link a Confluence/Notion page or GitHub file as a fragment that is re-resolved
|
|
89
|
+
// from the source at run time (a living source of truth, not a frozen snapshot).
|
|
90
|
+
const docDraft = ref({ source: '' as DocumentSourceKind | '', ref: '', tags: '' })
|
|
91
|
+
const docDraftValid = computed(() => docDraft.value.source && docDraft.value.ref.trim())
|
|
92
|
+
|
|
93
|
+
/** The board's existing document-backed fragments (workspace tier). */
|
|
94
|
+
const documentFragments = computed(() => library.fragments.filter((f) => f.documentRef))
|
|
95
|
+
|
|
96
|
+
async function linkDocumentFragment() {
|
|
97
|
+
if (!docDraftValid.value) return
|
|
98
|
+
try {
|
|
99
|
+
await library.createDocumentFragment({
|
|
100
|
+
source: docDraft.value.source as DocumentSourceKind,
|
|
101
|
+
ref: docDraft.value.ref.trim(),
|
|
102
|
+
tags: docDraft.value.tags
|
|
103
|
+
.split(',')
|
|
104
|
+
.map((t) => t.trim())
|
|
105
|
+
.filter(Boolean),
|
|
106
|
+
})
|
|
107
|
+
docDraft.value = { source: '', ref: '', tags: '' }
|
|
108
|
+
toast.add({ title: 'Document linked as a living fragment', icon: 'i-lucide-link' })
|
|
109
|
+
} catch (e) {
|
|
110
|
+
notifyError('Could not link document', e)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function refreshFragment(id: string) {
|
|
115
|
+
try {
|
|
116
|
+
await library.refreshDocumentFragment(id)
|
|
117
|
+
toast.add({ title: 'Fragment re-resolved from source', icon: 'i-lucide-refresh-cw' })
|
|
118
|
+
} catch (e) {
|
|
119
|
+
notifyError('Could not refresh fragment', e)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
83
123
|
// ---- repo sources ----------------------------------------------------------
|
|
84
124
|
const sourceDraft = ref({ repoOwner: '', repoName: '', dirPath: '', gitRef: '' })
|
|
85
125
|
const sourceValid = computed(
|
|
@@ -150,7 +190,7 @@ async function unlinkSource(id: string) {
|
|
|
150
190
|
|
|
151
191
|
<div class="flex gap-2">
|
|
152
192
|
<UButton
|
|
153
|
-
v-for="t in ['catalog', 'authored', 'sources'] as Tab[]"
|
|
193
|
+
v-for="t in ['catalog', 'authored', 'documents', 'sources'] as Tab[]"
|
|
154
194
|
:key="t"
|
|
155
195
|
:color="tab === t ? 'primary' : 'neutral'"
|
|
156
196
|
:variant="tab === t ? 'solid' : 'ghost'"
|
|
@@ -162,7 +202,9 @@ async function unlinkSource(id: string) {
|
|
|
162
202
|
? 'Resolved catalog'
|
|
163
203
|
: t === 'authored'
|
|
164
204
|
? 'This board'
|
|
165
|
-
:
|
|
205
|
+
: t === 'documents'
|
|
206
|
+
? 'Documents'
|
|
207
|
+
: 'Repo sources'
|
|
166
208
|
}}
|
|
167
209
|
</UButton>
|
|
168
210
|
</div>
|
|
@@ -183,6 +225,15 @@ async function unlinkSource(id: string) {
|
|
|
183
225
|
<UBadge size="xs" :color="tierColor[f.tier]" variant="subtle">
|
|
184
226
|
{{ tierLabel[f.tier] }}
|
|
185
227
|
</UBadge>
|
|
228
|
+
<UBadge
|
|
229
|
+
v-if="f.documentRef"
|
|
230
|
+
size="xs"
|
|
231
|
+
color="success"
|
|
232
|
+
variant="subtle"
|
|
233
|
+
icon="i-lucide-radio"
|
|
234
|
+
>
|
|
235
|
+
Live · {{ f.documentRef.source }}
|
|
236
|
+
</UBadge>
|
|
186
237
|
<span class="ml-auto font-mono text-[11px] text-slate-500">{{ f.id }}</span>
|
|
187
238
|
</div>
|
|
188
239
|
<p class="mt-1 text-sm text-slate-400">{{ f.summary }}</p>
|
|
@@ -249,6 +300,91 @@ async function unlinkSource(id: string) {
|
|
|
249
300
|
</div>
|
|
250
301
|
</div>
|
|
251
302
|
|
|
303
|
+
<!-- Document-backed (living) fragments -->
|
|
304
|
+
<div v-else-if="tab === 'documents'" class="flex flex-col gap-3">
|
|
305
|
+
<p class="text-xs text-slate-500">
|
|
306
|
+
Link a Confluence/Notion page or a GitHub file as a best-practice fragment. Its guidance
|
|
307
|
+
is re-resolved from the source at run time — edit the doc and the next agent run follows
|
|
308
|
+
the new version (no re-import).
|
|
309
|
+
</p>
|
|
310
|
+
|
|
311
|
+
<div
|
|
312
|
+
v-for="f in documentFragments"
|
|
313
|
+
:key="f.id"
|
|
314
|
+
class="flex items-start gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
315
|
+
>
|
|
316
|
+
<UIcon name="i-lucide-radio" class="mt-0.5 h-4 w-4 text-emerald-400" />
|
|
317
|
+
<div class="min-w-0">
|
|
318
|
+
<div class="flex items-center gap-2">
|
|
319
|
+
<span class="font-medium text-slate-100">{{ f.title }}</span>
|
|
320
|
+
<UBadge size="xs" color="success" variant="subtle">
|
|
321
|
+
{{ f.documentRef?.source }}
|
|
322
|
+
</UBadge>
|
|
323
|
+
</div>
|
|
324
|
+
<p class="text-sm text-slate-400">{{ f.summary }}</p>
|
|
325
|
+
<p v-if="f.resolvedAt" class="text-[11px] text-slate-500">
|
|
326
|
+
last resolved {{ new Date(f.resolvedAt).toLocaleString() }}
|
|
327
|
+
</p>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="ml-auto flex gap-1">
|
|
330
|
+
<UButton
|
|
331
|
+
icon="i-lucide-refresh-cw"
|
|
332
|
+
size="xs"
|
|
333
|
+
variant="ghost"
|
|
334
|
+
:loading="library.loading"
|
|
335
|
+
title="Re-resolve from source now"
|
|
336
|
+
@click="refreshFragment(f.id)"
|
|
337
|
+
/>
|
|
338
|
+
<UButton
|
|
339
|
+
icon="i-lucide-trash-2"
|
|
340
|
+
size="xs"
|
|
341
|
+
color="error"
|
|
342
|
+
variant="ghost"
|
|
343
|
+
@click="removeFragment(f.id)"
|
|
344
|
+
/>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
<p v-if="!documentFragments.length" class="text-sm text-slate-500">
|
|
348
|
+
No document-backed fragments yet. Link one below.
|
|
349
|
+
</p>
|
|
350
|
+
|
|
351
|
+
<div class="rounded-md border border-slate-800 p-3">
|
|
352
|
+
<p class="mb-2 text-sm font-medium">Link a document</p>
|
|
353
|
+
<div v-if="!documents.connectedSources.length" class="text-sm text-slate-500">
|
|
354
|
+
Connect a document source (Confluence, Notion or GitHub) under Integrations first.
|
|
355
|
+
</div>
|
|
356
|
+
<div v-else class="flex flex-col gap-2">
|
|
357
|
+
<div class="flex flex-wrap gap-2">
|
|
358
|
+
<UButton
|
|
359
|
+
v-for="s in documents.connectedSources"
|
|
360
|
+
:key="s.source"
|
|
361
|
+
size="xs"
|
|
362
|
+
:color="docDraft.source === s.source ? 'primary' : 'neutral'"
|
|
363
|
+
:variant="docDraft.source === s.source ? 'solid' : 'outline'"
|
|
364
|
+
@click="docDraft.source = s.source"
|
|
365
|
+
>
|
|
366
|
+
{{ s.label }}
|
|
367
|
+
</UButton>
|
|
368
|
+
</div>
|
|
369
|
+
<UInput
|
|
370
|
+
v-model="docDraft.ref"
|
|
371
|
+
placeholder="Page id or URL (e.g. a Confluence/Notion page or GitHub file URL)"
|
|
372
|
+
/>
|
|
373
|
+
<UInput v-model="docDraft.tags" placeholder="Tags, comma-separated (optional)" />
|
|
374
|
+
<UButton
|
|
375
|
+
icon="i-lucide-link"
|
|
376
|
+
size="sm"
|
|
377
|
+
:disabled="!docDraftValid"
|
|
378
|
+
:loading="library.loading"
|
|
379
|
+
class="self-start"
|
|
380
|
+
@click="linkDocumentFragment"
|
|
381
|
+
>
|
|
382
|
+
Link as living fragment
|
|
383
|
+
</UButton>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
252
388
|
<!-- Repo sources -->
|
|
253
389
|
<div v-else class="flex flex-col gap-3">
|
|
254
390
|
<div
|
|
@@ -173,6 +173,31 @@ const commentOnPrOpenLabel = computed(() =>
|
|
|
173
173
|
const resolveOnMergeLabel = computed(() =>
|
|
174
174
|
writebackLabel(props.block.trackerResolveOnMerge, tracker.settings.writebackResolveOnMerge),
|
|
175
175
|
)
|
|
176
|
+
|
|
177
|
+
// ---- technical label (tri-state) -------------------------------------------
|
|
178
|
+
// Whether this is a purely technical task (the implementer then treats the task
|
|
179
|
+
// definition as primary and the spec as a regression reference). Tri-state: Unset lets
|
|
180
|
+
// the engine infer it from the spec phase; Technical / Business are authoritative human
|
|
181
|
+
// choices the engine never overrides. `null` clears back to Unset.
|
|
182
|
+
function setTechnical(value: boolean | null) {
|
|
183
|
+
board.updateBlock(props.block.id, { technical: value })
|
|
184
|
+
}
|
|
185
|
+
const technicalMenu = [
|
|
186
|
+
[
|
|
187
|
+
{
|
|
188
|
+
label: 'Unset (auto-detect)',
|
|
189
|
+
icon: 'i-lucide-rotate-ccw',
|
|
190
|
+
onSelect: () => setTechnical(null),
|
|
191
|
+
},
|
|
192
|
+
{ label: 'Technical', icon: 'i-lucide-wrench', onSelect: () => setTechnical(true) },
|
|
193
|
+
{ label: 'Business', icon: 'i-lucide-briefcase', onSelect: () => setTechnical(false) },
|
|
194
|
+
],
|
|
195
|
+
]
|
|
196
|
+
const technicalLabel = computed(() => {
|
|
197
|
+
if (props.block.technical === true) return 'Technical'
|
|
198
|
+
if (props.block.technical === false) return 'Business'
|
|
199
|
+
return 'Unset (auto-detect)'
|
|
200
|
+
})
|
|
176
201
|
</script>
|
|
177
202
|
|
|
178
203
|
<template>
|
|
@@ -302,6 +327,38 @@ const resolveOnMergeLabel = computed(() =>
|
|
|
302
327
|
</p>
|
|
303
328
|
</div>
|
|
304
329
|
|
|
330
|
+
<!-- technical label (tri-state) -->
|
|
331
|
+
<div>
|
|
332
|
+
<div class="mb-1 flex items-center justify-between">
|
|
333
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
334
|
+
Task kind
|
|
335
|
+
</span>
|
|
336
|
+
<UDropdownMenu :items="technicalMenu">
|
|
337
|
+
<UButton
|
|
338
|
+
size="xs"
|
|
339
|
+
variant="ghost"
|
|
340
|
+
color="neutral"
|
|
341
|
+
icon="i-lucide-wrench"
|
|
342
|
+
trailing-icon="i-lucide-chevron-down"
|
|
343
|
+
>
|
|
344
|
+
{{ technicalLabel }}
|
|
345
|
+
</UButton>
|
|
346
|
+
</UDropdownMenu>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="text-[11px] text-slate-500">
|
|
349
|
+
<template v-if="block.technical === true">
|
|
350
|
+
Technical — the implementer treats the task definition as primary and the spec as a
|
|
351
|
+
regression reference; the spec-writer may produce no business specs.
|
|
352
|
+
</template>
|
|
353
|
+
<template v-else-if="block.technical === false">
|
|
354
|
+
Business — the specification leads, as usual.
|
|
355
|
+
</template>
|
|
356
|
+
<template v-else>
|
|
357
|
+
Auto-detect — inferred from the spec phase. Set it explicitly to override.
|
|
358
|
+
</template>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
305
362
|
<!-- issue-tracker writeback overrides -->
|
|
306
363
|
<div>
|
|
307
364
|
<div class="mb-1 flex items-center justify-between">
|
|
@@ -42,6 +42,7 @@ export function boardApi({ http, ws }: ApiContext) {
|
|
|
42
42
|
modelPresetId?: string
|
|
43
43
|
pipelineId?: string
|
|
44
44
|
agentConfig?: Record<string, string>
|
|
45
|
+
technical?: boolean
|
|
45
46
|
},
|
|
46
47
|
) => http<Block>(`${ws(workspaceId)}/blocks/${blockId}/tasks`, { method: 'POST', body }),
|
|
47
48
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
CreateDocumentFragmentInput,
|
|
2
3
|
CreatePromptFragmentInput,
|
|
3
4
|
FragmentOwnerKind,
|
|
4
5
|
FragmentSource,
|
|
@@ -45,6 +46,20 @@ export function fragmentsApi({ http, ws, scope }: ApiContext) {
|
|
|
45
46
|
method: 'DELETE',
|
|
46
47
|
}),
|
|
47
48
|
|
|
49
|
+
// Link an external document (Confluence/Notion/GitHub) as a living fragment.
|
|
50
|
+
createDocumentFragment: (
|
|
51
|
+
kind: FragmentOwnerKind,
|
|
52
|
+
id: string,
|
|
53
|
+
body: CreateDocumentFragmentInput,
|
|
54
|
+
) => http<PromptFragment>(`${scope(kind, id)}/document-fragments`, { method: 'POST', body }),
|
|
55
|
+
|
|
56
|
+
// Force an immediate live re-resolve of a document-backed fragment.
|
|
57
|
+
refreshFragment: (kind: FragmentOwnerKind, id: string, fragmentId: string) =>
|
|
58
|
+
http<PromptFragment>(
|
|
59
|
+
`${scope(kind, id)}/prompt-fragments/${encodeURIComponent(fragmentId)}/refresh`,
|
|
60
|
+
{ method: 'POST' },
|
|
61
|
+
),
|
|
62
|
+
|
|
48
63
|
// Repo sources of guideline Markdown.
|
|
49
64
|
listFragmentSources: (kind: FragmentOwnerKind, id: string) =>
|
|
50
65
|
http<FragmentSource[]>(`${scope(kind, id)}/fragment-sources`),
|
package/app/stores/board.ts
CHANGED
|
@@ -80,6 +80,7 @@ export const useBoardStore = defineStore('board', () => {
|
|
|
80
80
|
modelPresetId?: string
|
|
81
81
|
pipelineId?: string
|
|
82
82
|
agentConfig?: Record<string, string>
|
|
83
|
+
technical?: boolean
|
|
83
84
|
},
|
|
84
85
|
): Promise<Block | undefined> {
|
|
85
86
|
if (!getBlock(containerId)) return
|
|
@@ -92,6 +93,7 @@ export const useBoardStore = defineStore('board', () => {
|
|
|
92
93
|
...(options?.modelPresetId ? { modelPresetId: options.modelPresetId } : {}),
|
|
93
94
|
...(options?.pipelineId ? { pipelineId: options.pipelineId } : {}),
|
|
94
95
|
...(options?.agentConfig ? { agentConfig: options.agentConfig } : {}),
|
|
96
|
+
...(options?.technical ? { technical: true } : {}),
|
|
95
97
|
})
|
|
96
98
|
upsert(block)
|
|
97
99
|
return block
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
3
|
import type {
|
|
4
|
+
CreateDocumentFragmentInput,
|
|
4
5
|
CreatePromptFragmentInput,
|
|
5
6
|
FragmentSource,
|
|
6
7
|
LinkFragmentSourceInput,
|
|
@@ -77,6 +78,28 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
77
78
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/** Link an external document as a living (dynamically-resolved) fragment. */
|
|
82
|
+
async function createDocumentFragment(input: CreateDocumentFragmentInput) {
|
|
83
|
+
loading.value = true
|
|
84
|
+
try {
|
|
85
|
+
await api.createDocumentFragment('workspace', workspace.requireId(), input)
|
|
86
|
+
await Promise.all([reloadTier(), refreshResolved()])
|
|
87
|
+
} finally {
|
|
88
|
+
loading.value = false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Force an immediate live re-resolve of a document-backed fragment. */
|
|
93
|
+
async function refreshDocumentFragment(fragmentId: string) {
|
|
94
|
+
loading.value = true
|
|
95
|
+
try {
|
|
96
|
+
await api.refreshFragment('workspace', workspace.requireId(), fragmentId)
|
|
97
|
+
await Promise.all([reloadTier(), refreshResolved()])
|
|
98
|
+
} finally {
|
|
99
|
+
loading.value = false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
80
103
|
/** Tombstone a fragment at the workspace tier (suppresses an inherited one). */
|
|
81
104
|
async function remove(fragmentId: string) {
|
|
82
105
|
await api.deleteFragment('workspace', workspace.requireId(), fragmentId)
|
|
@@ -137,6 +160,8 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
137
160
|
probe,
|
|
138
161
|
refreshResolved,
|
|
139
162
|
create,
|
|
163
|
+
createDocumentFragment,
|
|
164
|
+
refreshDocumentFragment,
|
|
140
165
|
update,
|
|
141
166
|
remove,
|
|
142
167
|
linkSource,
|
package/app/types/domain.ts
CHANGED
|
@@ -132,6 +132,15 @@ export interface Block {
|
|
|
132
132
|
taskType?: TaskType
|
|
133
133
|
/** task-only: small per-type form fields (bug severity, spike timebox, …). */
|
|
134
134
|
taskTypeFields?: TaskTypeFields | null
|
|
135
|
+
/**
|
|
136
|
+
* task-only: TECHNICAL label. `true` ⇒ a refactor / non-functional / internal change
|
|
137
|
+
* (the implementer treats the task definition as primary, specs as a regression
|
|
138
|
+
* reference; the spec-writer may produce no business specs). `false` ⇒ a business task.
|
|
139
|
+
* `null`/absent ⇒ not yet determined — the engine may infer it from the spec phase. A
|
|
140
|
+
* human-set value is authoritative and never overridden; the inspector toggle is
|
|
141
|
+
* tri-state (unset / technical / business) and sends `null` for "unset".
|
|
142
|
+
*/
|
|
143
|
+
technical?: boolean | null
|
|
135
144
|
/** ids of best-practice prompt fragments folded into this block's agent prompts. */
|
|
136
145
|
fragmentIds?: string[]
|
|
137
146
|
/**
|
package/app/types/fragments.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
7
|
import type { AgentKind, BlockType } from './domain'
|
|
8
|
+
import type { DocumentSourceKind } from './documents'
|
|
8
9
|
import type { PromptFragment } from './models'
|
|
9
10
|
|
|
10
11
|
/** Which scope owns a managed fragment / source. */
|
|
@@ -28,6 +29,21 @@ export interface CreatePromptFragmentInput {
|
|
|
28
29
|
/** Partial patch for editing a fragment at a tier. */
|
|
29
30
|
export type UpdatePromptFragmentInput = Partial<CreatePromptFragmentInput>
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Inputs for linking an external document (Confluence/Notion page or GitHub file)
|
|
34
|
+
* as a LIVING fragment at a tier. Title/summary/body are derived from the fetched
|
|
35
|
+
* document, not supplied here. `viaWorkspaceId` is only needed at the account tier.
|
|
36
|
+
*/
|
|
37
|
+
export interface CreateDocumentFragmentInput {
|
|
38
|
+
source: DocumentSourceKind
|
|
39
|
+
ref: string
|
|
40
|
+
id?: string
|
|
41
|
+
category?: string
|
|
42
|
+
tags?: string[]
|
|
43
|
+
appliesTo?: { blockTypes?: BlockType[]; agentKinds?: AgentKind[] }
|
|
44
|
+
viaWorkspaceId?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
/** A fragment after the three tiers are merged for a workspace. */
|
|
32
48
|
export interface ResolvedFragment extends PromptFragment {
|
|
33
49
|
tier: FragmentTier
|
package/app/types/models.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
|
|
6
6
|
import type { AgentKind, BlockType } from './domain'
|
|
7
|
+
import type { DocumentSourceKind } from './documents'
|
|
7
8
|
|
|
8
9
|
/** Subscription vendors whose pooled tokens drive the Claude Code / Codex harnesses. */
|
|
9
10
|
export type SubscriptionVendor = 'claude' | 'codex' | 'glm' | 'kimi' | 'deepseek'
|
|
@@ -154,4 +155,14 @@ export interface PromptFragment {
|
|
|
154
155
|
path: string
|
|
155
156
|
sha: string
|
|
156
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Provenance when the body is a LIVING external document (Confluence/Notion/
|
|
160
|
+
* GitHub). The body is re-resolved from the source at run time, not frozen.
|
|
161
|
+
*/
|
|
162
|
+
documentRef?: {
|
|
163
|
+
source: DocumentSourceKind
|
|
164
|
+
externalId: string
|
|
165
|
+
}
|
|
166
|
+
/** When the document-backed body was last resolved from the source (epoch ms). */
|
|
167
|
+
resolvedAt?: number
|
|
157
168
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|