@cat-factory/app 0.253.2 → 0.254.1
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/README.md +49 -0
- package/app/components/palettes/AgentPalette.vue +38 -33
- package/app/components/palettes/PipelinePurposeSelect.vue +80 -0
- package/app/components/pipeline/PipelineBuilder.vue +12 -38
- package/app/utils/agentPalette.spec.ts +145 -0
- package/app/utils/agentPalette.ts +89 -0
- package/app/utils/agentTier.spec.ts +1 -17
- package/app/utils/agentTier.ts +0 -8
- package/app/utils/pipeline.spec.ts +85 -4
- package/i18n/locales/de.json +3 -1
- package/i18n/locales/en.json +9 -1
- package/i18n/locales/es.json +3 -1
- package/i18n/locales/fr.json +3 -1
- package/i18n/locales/he.json +3 -1
- package/i18n/locales/it.json +3 -1
- package/i18n/locales/ja.json +3 -1
- package/i18n/locales/pl.json +3 -1
- package/i18n/locales/tr.json +3 -1
- package/i18n/locales/uk.json +3 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -265,6 +265,55 @@ picking what each of them runs on are halves of the same job.
|
|
|
265
265
|
whatever the tier: the same rule `showOverrideField` states for a single field: a row the
|
|
266
266
|
user can neither read nor clear is worse than a longer list.
|
|
267
267
|
|
|
268
|
+
### The palette's second dial: the pipeline's purpose
|
|
269
|
+
|
|
270
|
+
The builder's palette narrows on two axes, and both controls sit on one row above the catalog
|
|
271
|
+
(`PipelinePurposeSelect` above `AgentTierSelect`), each with its own "n hidden" hint so neither
|
|
272
|
+
narrowing reads as an empty catalog. The tier says how deep to look; the **purpose** says what
|
|
273
|
+
the pipeline is for (`build` / `document` / `review` / `research` / `planning`), and the palette
|
|
274
|
+
drops the categories that purpose has no use for. The purpose is not a view preference: it is
|
|
275
|
+
saved on the pipeline and also decides which task pickers offer it, which is why the control
|
|
276
|
+
writes through to the draft while the tier writes to its own store.
|
|
277
|
+
|
|
278
|
+
Purpose is filtered by two predicates in `@cat-factory/contracts`, and the difference between
|
|
279
|
+
them is the point:
|
|
280
|
+
|
|
281
|
+
- `purposeSuggestsAgentCategory` is **relevance**: what the palette OFFERS. Opinionated (a
|
|
282
|
+
review pipeline designs nothing; a planning pipeline has no pull request to gate), because a
|
|
283
|
+
wrong guess costs one purpose switch.
|
|
284
|
+
- `purposeAllowsAgentCategory` is **compatibility**: what the builder will SAVE. It states only
|
|
285
|
+
what is contradictory (a pipeline that writes no code carrying an implementation step) and
|
|
286
|
+
drives the draft's conflict warning.
|
|
287
|
+
|
|
288
|
+
Relevance is a subset of compatibility, asserted over the whole grid in `pipeline.spec.ts`. Keep
|
|
289
|
+
it that way: the palette may hide what the save gate tolerates, so tightening the relevance table
|
|
290
|
+
never turns a stored pipeline into one its own editor refuses, but offering a kind the save gate
|
|
291
|
+
then rejects would be a dead end with the refusal arriving after the work.
|
|
292
|
+
|
|
293
|
+
**Each hint counts what relaxing THAT dial alone would reveal**, which is why the reduction is one
|
|
294
|
+
function (`utils/agentPalette.ts`) rather than two chained filters at the call site. Chaining them
|
|
295
|
+
and subtracting the lengths gives the second dial an honest count and hands the first one the whole
|
|
296
|
+
rest of the catalog: at the default `basic` tier a `planning` pipeline claimed thirteen kinds hidden
|
|
297
|
+
for its purpose when switching back to Build revealed three, the other ten being tier-hidden either
|
|
298
|
+
way. So a kind BOTH dials hide is counted by neither, correctly, and a new dial measures itself
|
|
299
|
+
against what the others already admit rather than against the raw catalog.
|
|
300
|
+
|
|
301
|
+
**A purpose or category this build does not recognise narrows nothing.** Both are closed
|
|
302
|
+
vocabularies and both are persisted, so a reader is total against the type and partial against the
|
|
303
|
+
data: a `Pipeline.purpose` outlives the build that wrote it, and a `presentation.category` arrives
|
|
304
|
+
in the snapshot from a kind a deployment registered. Narrow with the schema-derived
|
|
305
|
+
`isPipelinePurpose` / `isAgentCategory` before indexing anything by one, never with an optional
|
|
306
|
+
call, so adding a member still fails the build. The two predicates read the unknown value through
|
|
307
|
+
one helper because they have to agree about it: one narrowing by a purpose the other no longer
|
|
308
|
+
recognises is exactly the subset violation above. On the control itself an unrecognised purpose is
|
|
309
|
+
NAMED and quoted back rather than left to render blank, which would read as a pipeline nobody
|
|
310
|
+
classified while the saved row says otherwise.
|
|
311
|
+
|
|
312
|
+
Offering such a kind is only half of keeping it: `groupAgentPalette` puts whatever no section
|
|
313
|
+
CLAIMED into the trailing custom bucket, derived from the sections rather than from an absent
|
|
314
|
+
`category`. A kind whose category has no section matches neither test, so filtering on the absent
|
|
315
|
+
one alone deleted it from a palette its own save gate accepts.
|
|
316
|
+
|
|
268
317
|
## In-app tutorial tours
|
|
269
318
|
|
|
270
319
|
On first launch (once the board is up and no other startup advisory is open) the app asks
|
|
@@ -1,53 +1,46 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import { useLocalStorage } from '@vueuse/core'
|
|
4
|
-
import { purposeAllowsAgentCategory } from '@cat-factory/contracts'
|
|
5
4
|
import type { AgentKind, PipelinePurpose } from '~/types/domain'
|
|
6
5
|
import AgentTierSelect from '~/components/palettes/AgentTierSelect.vue'
|
|
7
|
-
import
|
|
6
|
+
import PipelinePurposeSelect from '~/components/palettes/PipelinePurposeSelect.vue'
|
|
7
|
+
import { groupAgentPalette, narrowAgentPalette } from '~/utils/agentPalette'
|
|
8
8
|
import { AGENT_CATEGORIES, OBSERVABILITY_GATE_ARCHETYPE } from '~/utils/catalog'
|
|
9
9
|
|
|
10
10
|
const { t } = useI18n()
|
|
11
11
|
const agents = useAgentsStore()
|
|
12
12
|
const agentTier = useAgentTierStore()
|
|
13
13
|
const releaseHealth = useReleaseHealthStore()
|
|
14
|
-
defineEmits<{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
defineEmits<{
|
|
15
|
+
(e: 'add', kind: AgentKind): void
|
|
16
|
+
(e: 'update:purpose', purpose: PipelinePurpose): void
|
|
17
|
+
}>()
|
|
18
|
+
// The purpose of the pipeline being built, edited right here by the control above the catalog.
|
|
19
|
+
// It narrows the palette to the categories that purpose has any use for (a review pipeline
|
|
20
|
+
// designs nothing and builds nothing, see `purposeSuggestsAgentCategory`); `null` shows all.
|
|
18
21
|
const props = defineProps<{ purpose?: PipelinePurpose | null }>()
|
|
19
22
|
|
|
20
23
|
// The post-release-health gate is only meaningful — and only accepted by the backend —
|
|
21
24
|
// with an observability integration connected, so it appears in the palette ONLY then.
|
|
22
|
-
const
|
|
23
|
-
|
|
25
|
+
const connected = computed(() =>
|
|
26
|
+
releaseHealth.connection.connected
|
|
24
27
|
? [...agents.archetypes, OBSERVABILITY_GATE_ARCHETYPE]
|
|
25
|
-
: agents.archetypes
|
|
26
|
-
|
|
27
|
-
// has no category to gate, so it always shows).
|
|
28
|
-
return all.filter((a) => !a.category || purposeAllowsAgentCategory(props.purpose, a.category))
|
|
29
|
-
})
|
|
28
|
+
: agents.archetypes,
|
|
29
|
+
)
|
|
30
30
|
|
|
31
|
-
// Then narrow
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
const palette = computed(() =>
|
|
36
|
-
const hiddenByTier = computed(() => offered.value.length - palette.value.length)
|
|
31
|
+
// Then narrow on the two dials above the catalog, each hint counting what relaxing THAT dial
|
|
32
|
+
// alone would reveal. `narrowAgentPalette` owns that rule so it is unit-testable, and so neither
|
|
33
|
+
// count can be quietly re-derived here as a subtraction that measures the wrong population.
|
|
34
|
+
const narrowed = computed(() => narrowAgentPalette(connected.value, props.purpose, agentTier.tier))
|
|
35
|
+
const palette = computed(() => narrowed.value.offered)
|
|
37
36
|
|
|
38
|
-
// Group the palette into the ordered catalog categories, plus a trailing "Custom" bucket
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}))
|
|
46
|
-
const custom = palette.value.filter((a) => !a.category)
|
|
47
|
-
if (custom.length)
|
|
48
|
-
ordered.push({ id: 'custom', label: t('palette.customAgents'), agents: custom })
|
|
49
|
-
return ordered.filter((g) => g.agents.length)
|
|
50
|
-
})
|
|
37
|
+
// Group the palette into the ordered catalog categories, plus a trailing "Custom" bucket for
|
|
38
|
+
// whatever none of them claimed. `groupAgentPalette` owns the placement rule for the same reason
|
|
39
|
+
// `narrowAgentPalette` owns the counting one: it decides what is VISIBLE, and the version living
|
|
40
|
+
// here as an inline computed dropped a kind whose category had no section outright.
|
|
41
|
+
const groups = computed(() =>
|
|
42
|
+
groupAgentPalette(palette.value, AGENT_CATEGORIES, t('palette.customAgents')),
|
|
43
|
+
)
|
|
51
44
|
|
|
52
45
|
// Persist which category sections are collapsed across builder opens.
|
|
53
46
|
const collapsed = useLocalStorage<string[]>('cf.pipelineBuilder.collapsedAgentCategories', [])
|
|
@@ -64,7 +57,19 @@ function toggle(id: string) {
|
|
|
64
57
|
<template>
|
|
65
58
|
<div class="space-y-2">
|
|
66
59
|
<p class="px-1 text-[11px] text-slate-500">{{ t('palette.hint') }}</p>
|
|
67
|
-
|
|
60
|
+
<!-- The two catalog dials, one above the other: what this pipeline is for, and how deep into
|
|
61
|
+
the agent catalog to look. Both narrow the sections below, and each states its own count.
|
|
62
|
+
Stacked rather than side by side because the palette column is a third of the slideover:
|
|
63
|
+
two half-width buttons truncate to "Purpose: Doc…", which is the one thing a filter
|
|
64
|
+
control may not do. -->
|
|
65
|
+
<div class="space-y-2">
|
|
66
|
+
<PipelinePurposeSelect
|
|
67
|
+
:purpose="props.purpose"
|
|
68
|
+
:hidden-count="narrowed.hiddenByPurpose"
|
|
69
|
+
@update:purpose="$emit('update:purpose', $event)"
|
|
70
|
+
/>
|
|
71
|
+
<AgentTierSelect :hidden-count="narrowed.hiddenByTier" />
|
|
72
|
+
</div>
|
|
68
73
|
<div class="space-y-2">
|
|
69
74
|
<section v-for="g in groups" :key="g.id">
|
|
70
75
|
<button
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The pipeline-PURPOSE control: what the pipeline being built exists to do (build, document,
|
|
3
|
+
// review, research, plan). It sits beside `AgentTierSelect` at the top of the agent palette
|
|
4
|
+
// because the two are the same kind of dial (each narrows the catalog below to what is worth
|
|
5
|
+
// offering), and a filter the user cannot see is a catalog that looks incomplete for no reason.
|
|
6
|
+
//
|
|
7
|
+
// Unlike the tier, this is not a view preference: it is saved on the pipeline and also decides
|
|
8
|
+
// which task pickers offer it (`pipelineAllowedForTaskType`). So the control writes through to
|
|
9
|
+
// the draft rather than to a store of its own.
|
|
10
|
+
import { computed } from 'vue'
|
|
11
|
+
import { isPipelinePurpose, PIPELINE_PURPOSES, type PipelinePurpose } from '@cat-factory/contracts'
|
|
12
|
+
|
|
13
|
+
const { t } = useI18n()
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
/** The draft's purpose. `null` = unclassified, which narrows nothing. */
|
|
17
|
+
purpose?: PipelinePurpose | null
|
|
18
|
+
/** How many agent kinds the current purpose hides. Renders the "n hidden" hint when > 0. */
|
|
19
|
+
hiddenCount?: number
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{ (e: 'update:purpose', purpose: PipelinePurpose): void }>()
|
|
23
|
+
|
|
24
|
+
// One STATIC literal `t()` key per purpose (not a key assembled from the loop variable), so the
|
|
25
|
+
// typed-message-keys check covers them, the same shape `AgentTierSelect` uses for its tiers.
|
|
26
|
+
const PURPOSE_LABELS = computed<Record<PipelinePurpose, string>>(() => ({
|
|
27
|
+
build: t('pipeline.builder.purposeOption.build'),
|
|
28
|
+
document: t('pipeline.builder.purposeOption.document'),
|
|
29
|
+
review: t('pipeline.builder.purposeOption.review'),
|
|
30
|
+
research: t('pipeline.builder.purposeOption.research'),
|
|
31
|
+
planning: t('pipeline.builder.purposeOption.planning'),
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
// The button text: the chosen purpose, or the placeholder while the draft carries none. An
|
|
35
|
+
// unclassified draft reads as an unmade choice rather than as a purpose called "none", because
|
|
36
|
+
// that is what it is: nothing is filtered until one is picked.
|
|
37
|
+
//
|
|
38
|
+
// A stored purpose this build has no label for is NAMED as unrecognised and quoted back, not
|
|
39
|
+
// folded into the placeholder and not left to render as an empty string after the colon. It is
|
|
40
|
+
// the one state the user cannot diagnose from the control: the palette is unfiltered (see
|
|
41
|
+
// `purposeSuggestsAgentCategory`), so a blank label would read as a pipeline nobody classified
|
|
42
|
+
// while the saved row says otherwise. The menu still lists every current member, so naming the
|
|
43
|
+
// value is also the way out of it.
|
|
44
|
+
const current = computed(() => {
|
|
45
|
+
const purpose = props.purpose
|
|
46
|
+
if (!purpose) return t('pipeline.builder.purposePlaceholder')
|
|
47
|
+
if (!isPipelinePurpose(purpose)) return t('pipeline.builder.purposeUnrecognized', { purpose })
|
|
48
|
+
return PURPOSE_LABELS.value[purpose]
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const items = computed(() => [
|
|
52
|
+
PIPELINE_PURPOSES.map((purpose) => ({
|
|
53
|
+
label: PURPOSE_LABELS.value[purpose],
|
|
54
|
+
icon: purpose === props.purpose ? 'i-lucide-check' : 'i-lucide-target',
|
|
55
|
+
onSelect: () => emit('update:purpose', purpose),
|
|
56
|
+
})),
|
|
57
|
+
])
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<div class="space-y-1">
|
|
62
|
+
<UDropdownMenu :items="items" :ui="{ content: 'z-[60] max-w-72' }">
|
|
63
|
+
<UButton
|
|
64
|
+
size="xs"
|
|
65
|
+
color="neutral"
|
|
66
|
+
variant="soft"
|
|
67
|
+
icon="i-lucide-target"
|
|
68
|
+
trailing-icon="i-lucide-chevron-down"
|
|
69
|
+
class="w-full justify-between"
|
|
70
|
+
:title="t('pipeline.builder.purposeLabel')"
|
|
71
|
+
data-testid="pipeline-purpose-select"
|
|
72
|
+
>
|
|
73
|
+
<span class="truncate"> {{ t('pipeline.builder.purposeLabel') }}: {{ current }} </span>
|
|
74
|
+
</UButton>
|
|
75
|
+
</UDropdownMenu>
|
|
76
|
+
<p v-if="props.hiddenCount" class="px-1 text-[10px] text-slate-500">
|
|
77
|
+
{{ t('palette.purposeHidden', { count: props.hiddenCount }, props.hiddenCount) }}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import { purposeAllowsAgentCategory } from '@cat-factory/contracts'
|
|
4
|
-
import type { AgentKind, Pipeline
|
|
4
|
+
import type { AgentKind, Pipeline } from '~/types/domain'
|
|
5
5
|
import AgentPalette from '~/components/palettes/AgentPalette.vue'
|
|
6
6
|
import AgentKindIcon from '~/components/pipeline/AgentKindIcon.vue'
|
|
7
7
|
import AgentPromptEditor from '~/components/pipeline/AgentPromptEditor.vue'
|
|
@@ -30,18 +30,6 @@ const CONSENSUS_STRATEGIES = computed<{ value: ConsensusStrategy; label: string
|
|
|
30
30
|
{ value: 'ranked-voting', label: t('pipeline.builder.strategyOption.ranked-voting') },
|
|
31
31
|
])
|
|
32
32
|
|
|
33
|
-
// The use-case classifier options for the pipeline (its `purpose`). Static literal `t()` keys, one
|
|
34
|
-
// per `PIPELINE_PURPOSES` member, so the typed-message-keys check sees them (a runtime-built key
|
|
35
|
-
// wouldn't be checkable). A non-`build` purpose hides the Implementation/Testing agent kinds in
|
|
36
|
-
// the palette below (see `AgentPalette`), and drives which task pickers offer the saved pipeline.
|
|
37
|
-
const PURPOSE_OPTIONS = computed<{ value: PipelinePurpose; label: string }[]>(() => [
|
|
38
|
-
{ value: 'build', label: t('pipeline.builder.purposeOption.build') },
|
|
39
|
-
{ value: 'document', label: t('pipeline.builder.purposeOption.document') },
|
|
40
|
-
{ value: 'review', label: t('pipeline.builder.purposeOption.review') },
|
|
41
|
-
{ value: 'research', label: t('pipeline.builder.purposeOption.research') },
|
|
42
|
-
{ value: 'planning', label: t('pipeline.builder.purposeOption.planning') },
|
|
43
|
-
])
|
|
44
|
-
|
|
45
33
|
/** Add a blank participant to the draft step's consensus config. */
|
|
46
34
|
function addParticipant(i: number) {
|
|
47
35
|
const cfg = pipelines.draftConsensus[i]
|
|
@@ -208,12 +196,16 @@ const skillStepNeedsPick = computed(() =>
|
|
|
208
196
|
),
|
|
209
197
|
)
|
|
210
198
|
|
|
211
|
-
// Steps whose agent category the chosen purpose
|
|
212
|
-
// runs no tests, so the Implementation/Testing categories are disallowed
|
|
213
|
-
// `purposeAllowsAgentCategory`).
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
199
|
+
// Steps whose agent category the chosen purpose CONTRADICTS (a non-`build` purpose writes no code
|
|
200
|
+
// and runs no tests, so the Implementation/Testing categories are disallowed, see
|
|
201
|
+
// `purposeAllowsAgentCategory`). Only reachable by switching an existing draft to a non-`build`
|
|
202
|
+
// purpose AFTER such steps were added, since the palette offers neither. The backend has no
|
|
203
|
+
// kind→category map to gate on, so the builder is the enforcement point: save is blocked until the
|
|
204
|
+
// offending steps are removed (or the purpose set back to Build).
|
|
205
|
+
//
|
|
206
|
+
// Deliberately the compatibility predicate, not the palette's narrower relevance one: a purpose
|
|
207
|
+
// that merely stops SUGGESTING a category must not turn a pipeline somebody already built into
|
|
208
|
+
// one they cannot save.
|
|
217
209
|
const stepsDisallowedByPurpose = computed(() =>
|
|
218
210
|
pipelines.draft.filter((kind) => {
|
|
219
211
|
const category = agentKindMeta(kind).category
|
|
@@ -492,7 +484,7 @@ async function clone(p: Pipeline) {
|
|
|
492
484
|
</UButton>
|
|
493
485
|
</div>
|
|
494
486
|
<div class="flex-1 pe-1 lg:min-h-0 lg:overflow-y-auto">
|
|
495
|
-
<AgentPalette :purpose="pipelines.draftPurpose" @add="add" />
|
|
487
|
+
<AgentPalette v-model:purpose="pipelines.draftPurpose" @add="add" />
|
|
496
488
|
</div>
|
|
497
489
|
</div>
|
|
498
490
|
|
|
@@ -524,24 +516,6 @@ async function clone(p: Pipeline) {
|
|
|
524
516
|
data-testid="pipeline-builder-name"
|
|
525
517
|
/>
|
|
526
518
|
|
|
527
|
-
<!-- Purpose: the pipeline's use-case classifier. Drives which task pickers offer it (a
|
|
528
|
-
document task offers only `document` pipelines) and narrows the palette below (a
|
|
529
|
-
non-build purpose hides the Implementation/Testing kinds). -->
|
|
530
|
-
<div class="mb-2 flex items-center gap-2">
|
|
531
|
-
<label class="shrink-0 text-[11px] font-medium text-slate-400">
|
|
532
|
-
{{ t('pipeline.builder.purposeLabel') }}
|
|
533
|
-
</label>
|
|
534
|
-
<USelect
|
|
535
|
-
:model-value="pipelines.draftPurpose ?? undefined"
|
|
536
|
-
:items="PURPOSE_OPTIONS"
|
|
537
|
-
value-key="value"
|
|
538
|
-
size="sm"
|
|
539
|
-
class="min-w-40"
|
|
540
|
-
:placeholder="t('pipeline.builder.purposePlaceholder')"
|
|
541
|
-
@update:model-value="pipelines.draftPurpose = $event"
|
|
542
|
-
/>
|
|
543
|
-
</div>
|
|
544
|
-
|
|
545
519
|
<!-- Description: the prose summary shown next to the step list in the pipeline pickers. -->
|
|
546
520
|
<UTextarea
|
|
547
521
|
v-model="pipelines.draftDescription"
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { AgentArchetype } from '~/types/domain'
|
|
3
|
+
import { groupAgentPalette, narrowAgentPalette } from '~/utils/agentPalette'
|
|
4
|
+
|
|
5
|
+
const archetype = (
|
|
6
|
+
kind: string,
|
|
7
|
+
category?: AgentArchetype['category'],
|
|
8
|
+
tier?: AgentArchetype['tier'],
|
|
9
|
+
): AgentArchetype => ({
|
|
10
|
+
kind: kind as AgentArchetype['kind'],
|
|
11
|
+
label: kind,
|
|
12
|
+
icon: 'i-lucide-bot',
|
|
13
|
+
color: '#fff',
|
|
14
|
+
description: kind,
|
|
15
|
+
...(category ? { category } : {}),
|
|
16
|
+
...(tier ? { tier } : {}),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// Spread across both dials on purpose: every combination of relevant/irrelevant to a `planning`
|
|
20
|
+
// pipeline (which keeps `review` + `design` and drops `build` / `test` / `docs` / `gates`) and
|
|
21
|
+
// visible/hidden at the `basic` tier, so the two counts cannot both be right by coincidence.
|
|
22
|
+
const CATALOG: AgentArchetype[] = [
|
|
23
|
+
archetype('coder', 'build', 'basic'), // irrelevant, in tier
|
|
24
|
+
archetype('tester', 'test', 'advanced'), // irrelevant AND out of tier
|
|
25
|
+
archetype('architect', 'design', 'basic'), // relevant, in tier
|
|
26
|
+
archetype('researcher', 'design', 'advanced'), // relevant, out of tier
|
|
27
|
+
archetype('documenter', 'docs', 'basic'), // irrelevant, in tier
|
|
28
|
+
// A deployment-registered kind declaring neither: no category to judge (always relevant) and an
|
|
29
|
+
// absent tier, which defaults to `intermediate` and so is out of the basic tier.
|
|
30
|
+
archetype('acme-auditor'),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
describe('narrowAgentPalette', () => {
|
|
34
|
+
it('narrows nothing for an unclassified pipeline at the widest tier', () => {
|
|
35
|
+
const { offered, hiddenByPurpose, hiddenByTier } = narrowAgentPalette(CATALOG, null, 'advanced')
|
|
36
|
+
expect(offered.map((a) => a.kind)).toEqual(CATALOG.map((a) => a.kind))
|
|
37
|
+
expect(hiddenByPurpose).toBe(0)
|
|
38
|
+
expect(hiddenByTier).toBe(0)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('accumulates the tiers when the purpose narrows nothing', () => {
|
|
42
|
+
expect(narrowAgentPalette(CATALOG, null, 'basic').offered.map((a) => a.kind)).toEqual([
|
|
43
|
+
'coder',
|
|
44
|
+
'architect',
|
|
45
|
+
'documenter',
|
|
46
|
+
])
|
|
47
|
+
expect(narrowAgentPalette(CATALOG, null, 'intermediate').offered.map((a) => a.kind)).toEqual([
|
|
48
|
+
'coder',
|
|
49
|
+
'architect',
|
|
50
|
+
'documenter',
|
|
51
|
+
// An undeclared tier defaults to intermediate, so it appears here.
|
|
52
|
+
'acme-auditor',
|
|
53
|
+
])
|
|
54
|
+
expect(narrowAgentPalette(CATALOG, null, 'basic').hiddenByTier).toBe(3)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('counts each dial against what the OTHER already admits', () => {
|
|
58
|
+
// The regression this shape exists for: measuring the purpose count over the whole catalog
|
|
59
|
+
// reported 3 here (coder, tester, documenter) while switching back to Build at this tier
|
|
60
|
+
// reveals only 2: `tester` is tier-hidden either way, so naming it under the purpose control
|
|
61
|
+
// sends the reader to a dial that cannot produce it.
|
|
62
|
+
const { offered, hiddenByPurpose, hiddenByTier } = narrowAgentPalette(
|
|
63
|
+
CATALOG,
|
|
64
|
+
'planning',
|
|
65
|
+
'basic',
|
|
66
|
+
)
|
|
67
|
+
expect(offered.map((a) => a.kind)).toEqual(['architect'])
|
|
68
|
+
expect(hiddenByPurpose).toBe(2)
|
|
69
|
+
expect(hiddenByTier).toBe(2)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('states the full purpose narrowing once the tier stops hiding anything', () => {
|
|
73
|
+
const { offered, hiddenByPurpose, hiddenByTier } = narrowAgentPalette(
|
|
74
|
+
CATALOG,
|
|
75
|
+
'planning',
|
|
76
|
+
'advanced',
|
|
77
|
+
)
|
|
78
|
+
// The uncategorized kind has nothing for the purpose dial to judge, so it stays offered.
|
|
79
|
+
expect(offered.map((a) => a.kind)).toEqual(['architect', 'researcher', 'acme-auditor'])
|
|
80
|
+
expect(hiddenByPurpose).toBe(3)
|
|
81
|
+
expect(hiddenByTier).toBe(0)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('leaves a kind both dials hide out of both counts', () => {
|
|
85
|
+
// Each count promises "relax THIS dial alone and you get n more", and relaxing either alone
|
|
86
|
+
// would not reveal `tester`. So the four buckets partition the catalog with it in none of the
|
|
87
|
+
// three the palette names, which is the property that keeps the two hints from double-counting.
|
|
88
|
+
const { offered, hiddenByPurpose, hiddenByTier } = narrowAgentPalette(
|
|
89
|
+
CATALOG,
|
|
90
|
+
'planning',
|
|
91
|
+
'basic',
|
|
92
|
+
)
|
|
93
|
+
const hiddenByBoth = CATALOG.length - offered.length - hiddenByPurpose - hiddenByTier
|
|
94
|
+
expect(hiddenByBoth).toBe(1)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('keeps a purpose this build does not recognise from narrowing anything', () => {
|
|
98
|
+
// A stored `purpose` from a build that shipped a member this one has not (or has retired):
|
|
99
|
+
// unknown is not a licence to guess, so the palette offers what the tier admits and says so.
|
|
100
|
+
const purpose = 'acme-migration' as never
|
|
101
|
+
const { offered, hiddenByPurpose } = narrowAgentPalette(CATALOG, purpose, 'advanced')
|
|
102
|
+
expect(offered.map((a) => a.kind)).toEqual(CATALOG.map((a) => a.kind))
|
|
103
|
+
expect(hiddenByPurpose).toBe(0)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// The rendered sections, mirroring `utils/catalog.ts` shape (id + display label).
|
|
108
|
+
const SECTIONS = [
|
|
109
|
+
{ id: 'design', label: 'Design & research' },
|
|
110
|
+
{ id: 'build', label: 'Implementation' },
|
|
111
|
+
{ id: 'test', label: 'Testing' },
|
|
112
|
+
{ id: 'docs', label: 'Documentation' },
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
describe('groupAgentPalette', () => {
|
|
116
|
+
it('fills the sections in their given order and drops the empty ones', () => {
|
|
117
|
+
const groups = groupAgentPalette(CATALOG, SECTIONS, 'Custom agents')
|
|
118
|
+
expect(groups.map((g) => [g.id, g.agents.map((a) => a.kind)])).toEqual([
|
|
119
|
+
['design', ['architect', 'researcher']],
|
|
120
|
+
['build', ['coder']],
|
|
121
|
+
['test', ['tester']],
|
|
122
|
+
['docs', ['documenter']],
|
|
123
|
+
// The uncategorized kind, in the trailing bucket. `review` and `gates` have no members
|
|
124
|
+
// here, so neither section is rendered at all.
|
|
125
|
+
['custom', ['acme-auditor']],
|
|
126
|
+
])
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('files a kind whose category has no section under custom rather than deleting it', () => {
|
|
130
|
+
// The regression: every section filter misses it AND so does a bare `!a.category`, so it
|
|
131
|
+
// vanished from a palette whose save gate accepts it. Reachable from both sides (a
|
|
132
|
+
// deployment-registered kind naming a category this build retired, and this list drifting
|
|
133
|
+
// behind the schema), which is why the leftover bucket is derived from the sections.
|
|
134
|
+
const registered = archetype('acme-auditor', 'observability' as never, 'basic')
|
|
135
|
+
const groups = groupAgentPalette([registered], SECTIONS, 'Custom agents')
|
|
136
|
+
expect(groups).toEqual([{ id: 'custom', label: 'Custom agents', agents: [registered] }])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('renders no custom section when every kind was claimed', () => {
|
|
140
|
+
const claimed = CATALOG.filter((a) => a.category)
|
|
141
|
+
const groups = groupAgentPalette(claimed, SECTIONS, 'Custom agents')
|
|
142
|
+
expect(groups.map((g) => g.id)).not.toContain('custom')
|
|
143
|
+
expect(groups.flatMap((g) => g.agents)).toHaveLength(claimed.length)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AgentTier,
|
|
3
|
+
agentTierVisibleAt,
|
|
4
|
+
type PipelinePurpose,
|
|
5
|
+
purposeSuggestsAgentCategory,
|
|
6
|
+
} from '@cat-factory/contracts'
|
|
7
|
+
import type { AgentArchetype } from '~/types/domain'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The pipeline builder's palette narrows the agent catalog on TWO dials, and the pair is the
|
|
11
|
+
* reason this reduction is one function rather than two chained filters at the call site.
|
|
12
|
+
*
|
|
13
|
+
* The dials are independent (the pipeline's PURPOSE says which categories the work has any use
|
|
14
|
+
* for, the agent TIER says how deep into the catalog to look), but their hints are not, because
|
|
15
|
+
* each hint is an invitation to reach for that dial. Chaining the filters and subtracting the
|
|
16
|
+
* lengths gives the second dial an honest count and the first one the whole rest of the catalog,
|
|
17
|
+
* so at the default `basic` tier a `planning` pipeline reported thirteen kinds hidden for its
|
|
18
|
+
* purpose when switching back to Build revealed three: the other ten were tier-hidden either way.
|
|
19
|
+
* That is exactly the "chasing a control that cannot help them" the ordering was meant to avoid,
|
|
20
|
+
* just pointed at the other dial.
|
|
21
|
+
*
|
|
22
|
+
* So each count is measured against what the OTHER dial already admits, which makes both of them
|
|
23
|
+
* the same promise: relax THIS dial alone and you get n more. A kind both dials hide is counted
|
|
24
|
+
* by neither, and correctly: relaxing either one alone would not reveal it.
|
|
25
|
+
*/
|
|
26
|
+
export interface NarrowedAgentPalette<T> {
|
|
27
|
+
/** The archetypes both dials admit: what the palette renders, in input order. */
|
|
28
|
+
offered: T[]
|
|
29
|
+
/** How many more the CURRENT tier would show if the purpose narrowed nothing. */
|
|
30
|
+
hiddenByPurpose: number
|
|
31
|
+
/** How many more the CURRENT purpose would show at the widest tier. */
|
|
32
|
+
hiddenByTier: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reduce `archetypes` to what the palette offers at `purpose` + `tier`, with each dial's hint
|
|
37
|
+
* count (see {@link NarrowedAgentPalette} for what the counts promise).
|
|
38
|
+
*
|
|
39
|
+
* An archetype carrying no `category` has nothing for the purpose dial to judge, so it is always
|
|
40
|
+
* relevant; an absent `tier` is `DEFAULT_AGENT_TIER`. Both are how a deployment-registered kind
|
|
41
|
+
* that declares neither behaves, and neither is a reason to drop it from the catalog.
|
|
42
|
+
*/
|
|
43
|
+
export function narrowAgentPalette<T extends Pick<AgentArchetype, 'tier' | 'category'>>(
|
|
44
|
+
archetypes: readonly T[],
|
|
45
|
+
purpose: PipelinePurpose | null | undefined,
|
|
46
|
+
tier: AgentTier,
|
|
47
|
+
): NarrowedAgentPalette<T> {
|
|
48
|
+
const relevant = (a: T) => !a.category || purposeSuggestsAgentCategory(purpose, a.category)
|
|
49
|
+
const inTier = (a: T) => agentTierVisibleAt(a.tier, tier)
|
|
50
|
+
return {
|
|
51
|
+
offered: archetypes.filter((a) => relevant(a) && inTier(a)),
|
|
52
|
+
hiddenByPurpose: archetypes.filter((a) => inTier(a) && !relevant(a)).length,
|
|
53
|
+
hiddenByTier: archetypes.filter((a) => relevant(a) && !inTier(a)).length,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One rendered palette section: an ordered category, or the trailing custom bucket. */
|
|
58
|
+
export interface AgentPaletteGroup<T> {
|
|
59
|
+
id: string
|
|
60
|
+
label: string
|
|
61
|
+
agents: T[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Group what the palette offers into `sections` in their given order, with everything left over
|
|
66
|
+
* in a trailing bucket under `customLabel`. Empty sections are dropped.
|
|
67
|
+
*
|
|
68
|
+
* The leftover bucket is what no section CLAIMED, not what carries no `category`, and the two are
|
|
69
|
+
* different populations: an archetype whose category has no section here belongs to neither, so
|
|
70
|
+
* testing for the absent one alone silently DELETED it from the palette. A `presentation.category`
|
|
71
|
+
* comes from a kind a DEPLOYMENT registered and `sections` is the SPA's own mirror of the schema,
|
|
72
|
+
* so the gap opens from either side, and the save gate accepts such a kind either way. Showing it
|
|
73
|
+
* under "custom" says what is true: nothing here knows where to file it.
|
|
74
|
+
*/
|
|
75
|
+
export function groupAgentPalette<T extends Pick<AgentArchetype, 'category'>>(
|
|
76
|
+
offered: readonly T[],
|
|
77
|
+
sections: readonly { id: string; label: string }[],
|
|
78
|
+
customLabel: string,
|
|
79
|
+
): AgentPaletteGroup<T>[] {
|
|
80
|
+
const claimed = new Set<string>(sections.map((s) => s.id))
|
|
81
|
+
const groups: AgentPaletteGroup<T>[] = sections.map((s) => ({
|
|
82
|
+
id: s.id,
|
|
83
|
+
label: s.label,
|
|
84
|
+
agents: offered.filter((a) => a.category === s.id),
|
|
85
|
+
}))
|
|
86
|
+
const custom = offered.filter((a) => !a.category || !claimed.has(a.category))
|
|
87
|
+
if (custom.length) groups.push({ id: 'custom', label: customLabel, agents: custom })
|
|
88
|
+
return groups.filter((g) => g.agents.length)
|
|
89
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import type { AgentArchetype } from '~/types/domain'
|
|
3
|
-
import {
|
|
3
|
+
import { filterByAgentTierKeeping, isAgentTier } from '~/utils/agentTier'
|
|
4
4
|
|
|
5
5
|
const archetype = (kind: string, tier?: AgentArchetype['tier']): AgentArchetype => ({
|
|
6
6
|
kind: kind as AgentArchetype['kind'],
|
|
@@ -19,22 +19,6 @@ const CATALOG: AgentArchetype[] = [
|
|
|
19
19
|
archetype('acme-auditor'),
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
-
describe('filterByAgentTier', () => {
|
|
23
|
-
it('shows only the basic kinds at the default level', () => {
|
|
24
|
-
expect(filterByAgentTier(CATALOG, 'basic').map((a) => a.kind)).toEqual(['coder'])
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('accumulates the levels, with advanced showing the whole catalog', () => {
|
|
28
|
-
expect(filterByAgentTier(CATALOG, 'intermediate').map((a) => a.kind)).toEqual([
|
|
29
|
-
'coder',
|
|
30
|
-
'researcher',
|
|
31
|
-
// An undeclared tier defaults to intermediate, so it appears here.
|
|
32
|
-
'acme-auditor',
|
|
33
|
-
])
|
|
34
|
-
expect(filterByAgentTier(CATALOG, 'advanced')).toHaveLength(CATALOG.length)
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
|
|
38
22
|
describe('filterByAgentTierKeeping', () => {
|
|
39
23
|
it('keeps a pinned kind the tier would otherwise hide, in catalog order', () => {
|
|
40
24
|
const kept = filterByAgentTierKeeping(CATALOG, 'basic', (a) => a.kind === 'mocker')
|
package/app/utils/agentTier.ts
CHANGED
|
@@ -25,14 +25,6 @@ export function isAgentTier(value: unknown): value is AgentTier {
|
|
|
25
25
|
return typeof value === 'string' && (AGENT_TIERS as readonly string[]).includes(value)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/** Keep only the archetypes visible at `level` (cumulative — see `agentTierVisibleAt`). */
|
|
29
|
-
export function filterByAgentTier<T extends Pick<AgentArchetype, 'tier'>>(
|
|
30
|
-
archetypes: readonly T[],
|
|
31
|
-
level: AgentTier,
|
|
32
|
-
): T[] {
|
|
33
|
-
return archetypes.filter((a) => agentTierVisibleAt(a.tier, level))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
28
|
/**
|
|
37
29
|
* Keep the archetypes visible at `level`, PLUS any the caller marks as pinned — the model
|
|
38
30
|
* preset editor's kinds that already carry an override. An entity can hold a setting written
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
+
agentCategorySchema,
|
|
4
|
+
isAgentCategory,
|
|
5
|
+
isPipelinePurpose,
|
|
6
|
+
PIPELINE_PURPOSES,
|
|
3
7
|
pipelineAllowedForBlockLevel,
|
|
4
8
|
pipelineAllowedForTaskType,
|
|
5
9
|
purposeAllowsAgentCategory,
|
|
10
|
+
purposeSuggestsAgentCategory,
|
|
6
11
|
} from '@cat-factory/contracts'
|
|
7
12
|
import type { Block, Pipeline } from '~/types/domain'
|
|
8
13
|
import {
|
|
@@ -12,6 +17,17 @@ import {
|
|
|
12
17
|
pipelineGateCount,
|
|
13
18
|
} from '~/utils/pipeline'
|
|
14
19
|
|
|
20
|
+
// The palette categories, read off the schema the predicates themselves are typed against, so a
|
|
21
|
+
// new category joins these sweeps instead of quietly going unasserted behind a hand-listed tuple.
|
|
22
|
+
const AGENT_CATEGORIES = agentCategorySchema.options
|
|
23
|
+
|
|
24
|
+
// A `purpose` typed as a member and only DECLARED to be one: `Pipeline.purpose` is persisted and
|
|
25
|
+
// no boundary re-checks it against the union this build compiled, so a browser on a cached bundle
|
|
26
|
+
// sees a member shipped after it and a retired member goes on living in saved rows. Same story for
|
|
27
|
+
// a `presentation.category`, which arrives in the snapshot from a deployment-registered kind.
|
|
28
|
+
const UNKNOWN_PURPOSE = 'acme-migration' as never
|
|
29
|
+
const UNKNOWN_CATEGORY = 'observability' as never
|
|
30
|
+
|
|
15
31
|
// A minimal pipeline: only the fields the launch/task-type filters read matter here.
|
|
16
32
|
function pipeline(over: Partial<Pipeline> = {}): Pipeline {
|
|
17
33
|
return { id: 'pl_x', name: 'X', agentKinds: ['coder'], ...over } as Pipeline
|
|
@@ -134,20 +150,21 @@ describe('pipelineAllowedForBlockLevel (initiative binding)', () => {
|
|
|
134
150
|
})
|
|
135
151
|
})
|
|
136
152
|
|
|
137
|
-
describe('purposeAllowsAgentCategory (builder
|
|
153
|
+
describe('purposeAllowsAgentCategory (builder save gate)', () => {
|
|
138
154
|
it('a build (or unclassified) pipeline may use every category', () => {
|
|
139
155
|
for (const purpose of ['build', null, undefined] as const) {
|
|
140
|
-
for (const cat of
|
|
156
|
+
for (const cat of AGENT_CATEGORIES) {
|
|
141
157
|
expect(purposeAllowsAgentCategory(purpose, cat)).toBe(true)
|
|
142
158
|
}
|
|
143
159
|
}
|
|
144
160
|
})
|
|
145
161
|
|
|
146
|
-
it('a non-build pipeline
|
|
162
|
+
it('a non-build pipeline refuses the Implementation (build) and Testing (test) categories', () => {
|
|
147
163
|
for (const purpose of ['document', 'review', 'research', 'planning'] as const) {
|
|
148
164
|
expect(purposeAllowsAgentCategory(purpose, 'build')).toBe(false)
|
|
149
165
|
expect(purposeAllowsAgentCategory(purpose, 'test')).toBe(false)
|
|
150
|
-
//
|
|
166
|
+
// Everything else stays SAVEABLE even where the palette stops offering it (below), so a
|
|
167
|
+
// stored pipeline never becomes unsaveable because the relevance table gained an opinion.
|
|
151
168
|
expect(purposeAllowsAgentCategory(purpose, 'docs')).toBe(true)
|
|
152
169
|
expect(purposeAllowsAgentCategory(purpose, 'review')).toBe(true)
|
|
153
170
|
expect(purposeAllowsAgentCategory(purpose, 'gates')).toBe(true)
|
|
@@ -155,6 +172,70 @@ describe('purposeAllowsAgentCategory (builder palette gate)', () => {
|
|
|
155
172
|
})
|
|
156
173
|
})
|
|
157
174
|
|
|
175
|
+
describe('purposeSuggestsAgentCategory (builder palette filter)', () => {
|
|
176
|
+
it('offers the whole catalog to a build pipeline and to an unclassified one', () => {
|
|
177
|
+
for (const purpose of ['build', null, undefined] as const) {
|
|
178
|
+
for (const cat of AGENT_CATEGORIES) {
|
|
179
|
+
expect(purposeSuggestsAgentCategory(purpose, cat)).toBe(true)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('narrows each purpose to the categories it has a use for', () => {
|
|
185
|
+
// A PR review designs nothing and builds nothing; the plan an initiative pipeline produces
|
|
186
|
+
// is its own in-repo tracker, with no pull request to gate and no repo docs to write.
|
|
187
|
+
expect(purposeSuggestsAgentCategory('review', 'design')).toBe(false)
|
|
188
|
+
expect(purposeSuggestsAgentCategory('review', 'review')).toBe(true)
|
|
189
|
+
expect(purposeSuggestsAgentCategory('planning', 'gates')).toBe(false)
|
|
190
|
+
expect(purposeSuggestsAgentCategory('planning', 'docs')).toBe(false)
|
|
191
|
+
expect(purposeSuggestsAgentCategory('planning', 'design')).toBe(true)
|
|
192
|
+
// Authoring a document and running a spike are researched and reviewed like any change.
|
|
193
|
+
for (const purpose of ['document', 'research'] as const) {
|
|
194
|
+
expect(purposeSuggestsAgentCategory(purpose, 'design')).toBe(true)
|
|
195
|
+
expect(purposeSuggestsAgentCategory(purpose, 'docs')).toBe(true)
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('never offers what the save gate would refuse', () => {
|
|
200
|
+
// The invariant that keeps the two tables honest, over the whole grid rather than the cells
|
|
201
|
+
// that happen to differ today: relevance may hide more than compatibility, never less, or
|
|
202
|
+
// the palette would offer a kind whose step then blocks the save. The unknown purpose rides
|
|
203
|
+
// the same sweep because it is the case where the two could most easily read the same value
|
|
204
|
+
// in opposite directions, one narrowing by it and the other not.
|
|
205
|
+
for (const purpose of [...PIPELINE_PURPOSES, UNKNOWN_PURPOSE]) {
|
|
206
|
+
for (const cat of AGENT_CATEGORIES) {
|
|
207
|
+
if (purposeSuggestsAgentCategory(purpose, cat)) {
|
|
208
|
+
expect(purposeAllowsAgentCategory(purpose, cat)).toBe(true)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('a purpose or category this build does not recognise', () => {
|
|
216
|
+
it('is recognised as unknown rather than trusted', () => {
|
|
217
|
+
for (const purpose of PIPELINE_PURPOSES) expect(isPipelinePurpose(purpose)).toBe(true)
|
|
218
|
+
for (const cat of AGENT_CATEGORIES) expect(isAgentCategory(cat)).toBe(true)
|
|
219
|
+
expect(isPipelinePurpose('acme-migration')).toBe(false)
|
|
220
|
+
expect(isPipelinePurpose('')).toBe(false)
|
|
221
|
+
expect(isAgentCategory('observability')).toBe(false)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('narrows nothing, in the palette or the save gate', () => {
|
|
225
|
+
// Both predicates index a table by these values, so before the guards an unknown purpose threw
|
|
226
|
+
// inside the palette's computed and white-screened the builder, while an unknown category read
|
|
227
|
+
// as `undefined` and silently dropped a registered kind the save gate would have accepted.
|
|
228
|
+
// Unknown means this build has nothing to narrow by, which is exactly what absent already means.
|
|
229
|
+
for (const cat of AGENT_CATEGORIES) {
|
|
230
|
+
expect(purposeSuggestsAgentCategory(UNKNOWN_PURPOSE, cat)).toBe(true)
|
|
231
|
+
expect(purposeAllowsAgentCategory(UNKNOWN_PURPOSE, cat)).toBe(true)
|
|
232
|
+
}
|
|
233
|
+
for (const purpose of PIPELINE_PURPOSES) {
|
|
234
|
+
expect(purposeSuggestsAgentCategory(purpose, UNKNOWN_CATEGORY)).toBe(true)
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
158
239
|
describe('pipelineAllowedForManualStart composes the task-type gate', () => {
|
|
159
240
|
const noFrame = undefined
|
|
160
241
|
const blocks: Block[] = []
|
package/i18n/locales/de.json
CHANGED
|
@@ -4214,6 +4214,7 @@
|
|
|
4214
4214
|
"rounds": "Runden",
|
|
4215
4215
|
"purposeLabel": "Zweck",
|
|
4216
4216
|
"purposePlaceholder": "Zweck auswählen",
|
|
4217
|
+
"purposeUnrecognized": "Nicht erkannt ({purpose})",
|
|
4217
4218
|
"purposeOption": {
|
|
4218
4219
|
"build": "Entwicklung",
|
|
4219
4220
|
"document": "Dokumentation",
|
|
@@ -6734,7 +6735,8 @@
|
|
|
6734
6735
|
},
|
|
6735
6736
|
"palette": {
|
|
6736
6737
|
"hint": "Klicke auf einen Agenten, um ihn an die Pipeline anzuhängen.",
|
|
6737
|
-
"customAgents": "Benutzerdefinierte Agenten"
|
|
6738
|
+
"customAgents": "Benutzerdefinierte Agenten",
|
|
6739
|
+
"purposeHidden": "{count} Agent ist für diesen Zweck ausgeblendet. | {count} Agenten sind für diesen Zweck ausgeblendet."
|
|
6738
6740
|
},
|
|
6739
6741
|
"forkDecision": {
|
|
6740
6742
|
"title": "Implementierungsansatz wählen",
|
package/i18n/locales/en.json
CHANGED
|
@@ -4796,6 +4796,10 @@
|
|
|
4796
4796
|
"rounds": "Rounds",
|
|
4797
4797
|
"purposeLabel": "Purpose",
|
|
4798
4798
|
"purposePlaceholder": "Select a purpose",
|
|
4799
|
+
"purposeUnrecognized": "Unrecognized ({purpose})",
|
|
4800
|
+
"@purposeUnrecognized": {
|
|
4801
|
+
"description": "Shown on the pipeline-purpose control when the pipeline carries a purpose this build has no label for (a member added by a newer build, or one since retired). The raw stored value is quoted back so it can be recognised and re-picked."
|
|
4802
|
+
},
|
|
4799
4803
|
"purposeOption": {
|
|
4800
4804
|
"build": "Build",
|
|
4801
4805
|
"document": "Documentation",
|
|
@@ -5129,7 +5133,11 @@
|
|
|
5129
5133
|
},
|
|
5130
5134
|
"palette": {
|
|
5131
5135
|
"hint": "Click an agent to append it to the pipeline.",
|
|
5132
|
-
"customAgents": "Custom agents"
|
|
5136
|
+
"customAgents": "Custom agents",
|
|
5137
|
+
"purposeHidden": "{count} agent hidden for this purpose. | {count} agents hidden for this purpose.",
|
|
5138
|
+
"@purposeHidden": {
|
|
5139
|
+
"description": "Shown under the pipeline-purpose control in the agent palette: how many agents are not offered because they are irrelevant to the pipeline's purpose (e.g. a coder in a documentation pipeline)."
|
|
5140
|
+
}
|
|
5133
5141
|
},
|
|
5134
5142
|
"gates": {
|
|
5135
5143
|
"subtitle": {
|
package/i18n/locales/es.json
CHANGED
|
@@ -4653,6 +4653,7 @@
|
|
|
4653
4653
|
"rounds": "Rondas",
|
|
4654
4654
|
"purposeLabel": "Propósito",
|
|
4655
4655
|
"purposePlaceholder": "Seleccionar un propósito",
|
|
4656
|
+
"purposeUnrecognized": "No reconocido ({purpose})",
|
|
4656
4657
|
"purposeOption": {
|
|
4657
4658
|
"build": "Desarrollo",
|
|
4658
4659
|
"document": "Documentación",
|
|
@@ -4903,7 +4904,8 @@
|
|
|
4903
4904
|
},
|
|
4904
4905
|
"palette": {
|
|
4905
4906
|
"hint": "Haz clic en un agente para añadirlo al pipeline.",
|
|
4906
|
-
"customAgents": "Agentes personalizados"
|
|
4907
|
+
"customAgents": "Agentes personalizados",
|
|
4908
|
+
"purposeHidden": "{count} agente oculto para este propósito. | {count} agentes ocultos para este propósito."
|
|
4907
4909
|
},
|
|
4908
4910
|
"gates": {
|
|
4909
4911
|
"subtitle": {
|
package/i18n/locales/fr.json
CHANGED
|
@@ -4653,6 +4653,7 @@
|
|
|
4653
4653
|
"rounds": "Tours",
|
|
4654
4654
|
"purposeLabel": "Objectif",
|
|
4655
4655
|
"purposePlaceholder": "Sélectionner un objectif",
|
|
4656
|
+
"purposeUnrecognized": "Non reconnu ({purpose})",
|
|
4656
4657
|
"purposeOption": {
|
|
4657
4658
|
"build": "Développement",
|
|
4658
4659
|
"document": "Documentation",
|
|
@@ -4903,7 +4904,8 @@
|
|
|
4903
4904
|
},
|
|
4904
4905
|
"palette": {
|
|
4905
4906
|
"hint": "Cliquez sur un agent pour l'ajouter au pipeline.",
|
|
4906
|
-
"customAgents": "Agents personnalisés"
|
|
4907
|
+
"customAgents": "Agents personnalisés",
|
|
4908
|
+
"purposeHidden": "{count} agent masqué pour cet objectif. | {count} agents masqués pour cet objectif."
|
|
4907
4909
|
},
|
|
4908
4910
|
"gates": {
|
|
4909
4911
|
"subtitle": {
|
package/i18n/locales/he.json
CHANGED
|
@@ -4653,6 +4653,7 @@
|
|
|
4653
4653
|
"rounds": "סבבים",
|
|
4654
4654
|
"purposeLabel": "מטרה",
|
|
4655
4655
|
"purposePlaceholder": "בחר מטרה",
|
|
4656
|
+
"purposeUnrecognized": "לא מזוהה ({purpose})",
|
|
4656
4657
|
"purposeOption": {
|
|
4657
4658
|
"build": "פיתוח",
|
|
4658
4659
|
"document": "תיעוד",
|
|
@@ -4903,7 +4904,8 @@
|
|
|
4903
4904
|
},
|
|
4904
4905
|
"palette": {
|
|
4905
4906
|
"hint": "לחץ על סוכן כדי להוסיף אותו לצינור.",
|
|
4906
|
-
"customAgents": "סוכנים מותאמים אישית"
|
|
4907
|
+
"customAgents": "סוכנים מותאמים אישית",
|
|
4908
|
+
"purposeHidden": "סוכן אחד מוסתר עבור מטרה זו. | שני סוכנים מוסתרים עבור מטרה זו. | {count} סוכנים מוסתרים עבור מטרה זו."
|
|
4907
4909
|
},
|
|
4908
4910
|
"gates": {
|
|
4909
4911
|
"subtitle": {
|
package/i18n/locales/it.json
CHANGED
|
@@ -4214,6 +4214,7 @@
|
|
|
4214
4214
|
"rounds": "Giri",
|
|
4215
4215
|
"purposeLabel": "Scopo",
|
|
4216
4216
|
"purposePlaceholder": "Seleziona uno scopo",
|
|
4217
|
+
"purposeUnrecognized": "Non riconosciuto ({purpose})",
|
|
4217
4218
|
"purposeOption": {
|
|
4218
4219
|
"build": "Sviluppo",
|
|
4219
4220
|
"document": "Documentazione",
|
|
@@ -6734,7 +6735,8 @@
|
|
|
6734
6735
|
},
|
|
6735
6736
|
"palette": {
|
|
6736
6737
|
"hint": "Clicca su un agente per aggiungerlo alla pipeline.",
|
|
6737
|
-
"customAgents": "Agenti personalizzati"
|
|
6738
|
+
"customAgents": "Agenti personalizzati",
|
|
6739
|
+
"purposeHidden": "{count} agente nascosto per questo scopo. | {count} agenti nascosti per questo scopo."
|
|
6738
6740
|
},
|
|
6739
6741
|
"forkDecision": {
|
|
6740
6742
|
"title": "Scegli un approccio di implementazione",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -4653,6 +4653,7 @@
|
|
|
4653
4653
|
"rounds": "ラウンド",
|
|
4654
4654
|
"purposeLabel": "目的",
|
|
4655
4655
|
"purposePlaceholder": "目的を選択",
|
|
4656
|
+
"purposeUnrecognized": "認識できません ({purpose})",
|
|
4656
4657
|
"purposeOption": {
|
|
4657
4658
|
"build": "開発",
|
|
4658
4659
|
"document": "ドキュメント",
|
|
@@ -4903,7 +4904,8 @@
|
|
|
4903
4904
|
},
|
|
4904
4905
|
"palette": {
|
|
4905
4906
|
"hint": "エージェントをクリックするとパイプラインに追加されます。",
|
|
4906
|
-
"customAgents": "カスタムエージェント"
|
|
4907
|
+
"customAgents": "カスタムエージェント",
|
|
4908
|
+
"purposeHidden": "この目的では {count} 件のエージェントが非表示です。"
|
|
4907
4909
|
},
|
|
4908
4910
|
"gates": {
|
|
4909
4911
|
"subtitle": {
|
package/i18n/locales/pl.json
CHANGED
|
@@ -4653,6 +4653,7 @@
|
|
|
4653
4653
|
"rounds": "Rundy",
|
|
4654
4654
|
"purposeLabel": "Cel",
|
|
4655
4655
|
"purposePlaceholder": "Wybierz cel",
|
|
4656
|
+
"purposeUnrecognized": "Nierozpoznany ({purpose})",
|
|
4656
4657
|
"purposeOption": {
|
|
4657
4658
|
"build": "Programowanie",
|
|
4658
4659
|
"document": "Dokumentacja",
|
|
@@ -4903,7 +4904,8 @@
|
|
|
4903
4904
|
},
|
|
4904
4905
|
"palette": {
|
|
4905
4906
|
"hint": "Kliknij agenta, aby dodać go do pipeline'u.",
|
|
4906
|
-
"customAgents": "Agenci niestandardowi"
|
|
4907
|
+
"customAgents": "Agenci niestandardowi",
|
|
4908
|
+
"purposeHidden": "{count} agent ukryty dla tego celu. | {count} agenci ukryci dla tego celu. | {count} agentów ukrytych dla tego celu."
|
|
4907
4909
|
},
|
|
4908
4910
|
"gates": {
|
|
4909
4911
|
"subtitle": {
|
package/i18n/locales/tr.json
CHANGED
|
@@ -4653,6 +4653,7 @@
|
|
|
4653
4653
|
"rounds": "Tur",
|
|
4654
4654
|
"purposeLabel": "Amaç",
|
|
4655
4655
|
"purposePlaceholder": "Bir amaç seçin",
|
|
4656
|
+
"purposeUnrecognized": "Tanınmayan ({purpose})",
|
|
4656
4657
|
"purposeOption": {
|
|
4657
4658
|
"build": "Geliştirme",
|
|
4658
4659
|
"document": "Dokümantasyon",
|
|
@@ -4903,7 +4904,8 @@
|
|
|
4903
4904
|
},
|
|
4904
4905
|
"palette": {
|
|
4905
4906
|
"hint": "Bir agent'ı pipeline'a eklemek için tıklayın.",
|
|
4906
|
-
"customAgents": "Özel agent'lar"
|
|
4907
|
+
"customAgents": "Özel agent'lar",
|
|
4908
|
+
"purposeHidden": "Bu amaç için {count} ajan gizli."
|
|
4907
4909
|
},
|
|
4908
4910
|
"gates": {
|
|
4909
4911
|
"subtitle": {
|
package/i18n/locales/uk.json
CHANGED
|
@@ -4653,6 +4653,7 @@
|
|
|
4653
4653
|
"rounds": "Раунди",
|
|
4654
4654
|
"purposeLabel": "Призначення",
|
|
4655
4655
|
"purposePlaceholder": "Виберіть призначення",
|
|
4656
|
+
"purposeUnrecognized": "Нерозпізнано ({purpose})",
|
|
4656
4657
|
"purposeOption": {
|
|
4657
4658
|
"build": "Розробка",
|
|
4658
4659
|
"document": "Документація",
|
|
@@ -4903,7 +4904,8 @@
|
|
|
4903
4904
|
},
|
|
4904
4905
|
"palette": {
|
|
4905
4906
|
"hint": "Натисніть агента, щоб додати його до пайплайну.",
|
|
4906
|
-
"customAgents": "Власні агенти"
|
|
4907
|
+
"customAgents": "Власні агенти",
|
|
4908
|
+
"purposeHidden": "{count} агента приховано для цього призначення. | {count} агентів приховано для цього призначення. | {count} агентів приховано для цього призначення."
|
|
4907
4909
|
},
|
|
4908
4910
|
"gates": {
|
|
4909
4911
|
"subtitle": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.254.1",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"valibot": "^1.4.2",
|
|
41
41
|
"vue": "3.5.40",
|
|
42
42
|
"wretch": "^3.0.9",
|
|
43
|
-
"@cat-factory/contracts": "0.
|
|
43
|
+
"@cat-factory/contracts": "0.278.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|