@cat-factory/app 0.234.1 → 0.235.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/README.md +28 -13
- package/app/components/pipeline/AgentKindIcon.vue +1 -1
- package/app/components/settings/ModelConfigurationPanel.vue +2 -2
- package/app/composables/useWorkspaceStream.ts +31 -77
- package/app/composables/workspaceStream/applyWorkspaceEvent.spec.ts +150 -0
- package/app/composables/workspaceStream/applyWorkspaceEvent.ts +173 -0
- package/app/docs/consumer-extensions.md +9 -6
- package/app/stores/agents.ts +5 -3
- package/app/utils/catalog.spec.ts +37 -2
- package/app/utils/catalog.ts +56 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
The user-facing app, packaged as a **reusable Nuxt 4 layer**: a single-page app
|
|
4
4
|
that runs entirely in the browser and renders the architecture board, drives agent
|
|
5
5
|
pipelines, and reflects live execution. A deployment consumes it via
|
|
6
|
-
`extends: ['@cat-factory/app']` (see [`deploy/frontend`](
|
|
7
|
-
It talks to the [backend Worker](
|
|
6
|
+
`extends: ['@cat-factory/app']` (see [`deploy/frontend`](https://github.com/kibertoad/cat-factory/tree/main/deploy/frontend)).
|
|
7
|
+
It talks to the [backend Worker](https://github.com/kibertoad/cat-factory/blob/main/backend/README.md) over REST and a single
|
|
8
8
|
WebSocket, sharing wire types from
|
|
9
|
-
[`@cat-factory/contracts`](
|
|
9
|
+
[`@cat-factory/contracts`](https://github.com/kibertoad/cat-factory/tree/main/backend/packages/contracts).
|
|
10
10
|
|
|
11
11
|
The SPA source lives under `app/` (the Nuxt srcDir).
|
|
12
12
|
|
|
@@ -364,7 +364,7 @@ is SUPPRESSED rather than unmounted, because it holds the running tour's resolve
|
|
|
364
364
|
remount would re-resolve it against gates that may have flipped since the tour started.
|
|
365
365
|
|
|
366
366
|
The decisions behind this surface, and why each alternative was rejected, are recorded in
|
|
367
|
-
[ADR 0036](
|
|
367
|
+
[ADR 0036](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/adr/0036-in-app-tutorials.md). This section is the authority on how
|
|
368
368
|
the thing WORKS.
|
|
369
369
|
|
|
370
370
|
A tour is **data, not components**: an ordered list of steps, each pointing at an on-screen
|
|
@@ -552,11 +552,26 @@ The recurring product bug behind most e2e flakes: a stale full-snapshot refresh
|
|
|
552
552
|
live state. The SPA has two delivery shapes and mixing them wrong drops live-added state with NO
|
|
553
553
|
event left to restore it.
|
|
554
554
|
|
|
555
|
-
- **Know how your entity is delivered.**
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
555
|
+
- **Know how your entity is delivered.** Targeted events (`execution`/`bootstrap`/`initiative`)
|
|
556
|
+
carry the entity and `upsert` it, so they don't clobber. A `board` event is delivered EITHER
|
|
557
|
+
way and the backend decides per change: it carries `block` when the change is fully described
|
|
558
|
+
by one (a spawned task, a field edit, a dependency toggle, a move), and carries none when it is
|
|
559
|
+
not (a removal, a reparent, a resize, a blueprint reconcile). A payload-less `board` event still
|
|
560
|
+
means a debounced full `workspace.refresh()`, where `hydrate` REPLACES whole lists. Routing
|
|
561
|
+
lives in `composables/workspaceStream/applyWorkspaceEvent.ts`, whose `switch` carries a `never`
|
|
562
|
+
guard: a new `WorkspaceEvent` member fails the BUILD rather than falling through to nothing. The
|
|
563
|
+
emit-site decision lives in `BoardService.emitBoardChanged`'s doc comment. Prefer a targeted
|
|
564
|
+
upsert for anything that must appear reliably.
|
|
565
|
+
- **Two blocks are refused a payload at the wire, on EVERY event that carries one.** Kernel's
|
|
566
|
+
`deliverableBoardBlock` is the single gate (both facades' `boardChanged` AND `bootstrapChanged`
|
|
567
|
+
assemble through it, via `boardWireEvent`/`bootstrapWireEvent`), so a new emitter cannot
|
|
568
|
+
reintroduce either by forgetting: a service FRAME, whose position and size are a per-board
|
|
569
|
+
`WorkspaceMount` override that one shared payload cannot state correctly on the several boards a
|
|
570
|
+
fan-out reaches; and a headless `internal` anchor block, which `composeBoard` filters out of
|
|
571
|
+
every snapshot and which would therefore render as a card no later read can remove. Both degrade
|
|
572
|
+
to the coarse signal, so nothing is lost but the refresh. A bootstrap's frame is always the first
|
|
573
|
+
case, which is why the `bootstrap` event's job rides live while the frame's own transitions
|
|
574
|
+
arrive as coarse `board` events beside it.
|
|
560
575
|
- **Full refreshes MUST be monotonic.** Two `refresh()` calls can be in flight; a staler one
|
|
561
576
|
resolving later overwrites the newer. `workspace.refresh()` guards this with a sequence. Do not
|
|
562
577
|
reintroduce an unguarded `hydrate(await fetch())`, and apply the guard to any new coalesced
|
|
@@ -583,7 +598,7 @@ event left to restore it.
|
|
|
583
598
|
All user-facing SPA copy goes through `@nuxtjs/i18n`; never hard-code a display string. This
|
|
584
599
|
layer ships the base `en` locale, and a downstream deployment overrides by dropping its own files
|
|
585
600
|
(the per-layer deep-merge is the override seam, consumer wins key by key). Migration status:
|
|
586
|
-
[`docs/internal/localization.md`](
|
|
601
|
+
[`docs/internal/localization.md`](https://github.com/kibertoad/cat-factory/blob/main/docs/internal/localization.md).
|
|
587
602
|
|
|
588
603
|
- `i18n/locales/<locale>.json`: the catalogs (the v9+ `i18n/` convention, NOT `app/locales/`).
|
|
589
604
|
- `i18n/i18n.config.ts`: runtime vue-i18n behaviour only (fallback locale, the plural
|
|
@@ -688,7 +703,7 @@ auto-imported
|
|
|
688
703
|
(`ResultWindowShell`, the `StepRunMeta` run-metadata block, `useResultView`, …), and the
|
|
689
704
|
namespacing / degradation rules are in
|
|
690
705
|
[`app/docs/consumer-extensions.md`](./app/docs/consumer-extensions.md); a full worked
|
|
691
|
-
example ships in [`deploy/frontend`](
|
|
706
|
+
example ships in [`deploy/frontend`](https://github.com/kibertoad/cat-factory/tree/main/deploy/frontend) (the `acme:security` module).
|
|
692
707
|
|
|
693
708
|
## Key UI surfaces
|
|
694
709
|
|
|
@@ -795,5 +810,5 @@ pnpm lint # oxlint + oxfmt --check
|
|
|
795
810
|
```
|
|
796
811
|
|
|
797
812
|
> Building/deploying the static site is covered in the deployment docs: see the
|
|
798
|
-
> [top-level README → Deployment](
|
|
799
|
-
> [`deploy/frontend/README.md`](
|
|
813
|
+
> [top-level README → Deployment](https://github.com/kibertoad/cat-factory/blob/main/README.md#deployment) and
|
|
814
|
+
> [`deploy/frontend/README.md`](https://github.com/kibertoad/cat-factory/blob/main/deploy/frontend/README.md).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The single rendering path for an agent kind's icon (+ optional label) anywhere a
|
|
3
3
|
// pipeline or run lists its steps. Resolves display metadata through
|
|
4
4
|
// `agentKindMeta`, which is total over every kind — palette archetypes, custom
|
|
5
|
-
// agents and the engine's system kinds (ci / merger /
|
|
5
|
+
// agents and the engine's system kinds (ci / merger / conflicts) — so
|
|
6
6
|
// a saved pipeline that contains a system kind can never blow up the renderer.
|
|
7
7
|
import { computed } from 'vue'
|
|
8
8
|
import { agentKindMeta } from '~/utils/catalog'
|
|
@@ -56,8 +56,8 @@ const busy = ref(false)
|
|
|
56
56
|
const filter = ref('')
|
|
57
57
|
|
|
58
58
|
// The palette archetypes PLUS the engine-driven kinds that still run an LLM
|
|
59
|
-
// (
|
|
60
|
-
//
|
|
59
|
+
// (merger, the fixers/resolver). The pure gates run no model, so they stay out —
|
|
60
|
+
// exactly the set the per-agent override list should cover.
|
|
61
61
|
const configurableKinds = computed(() => [...agents.archetypes, ...MODEL_CONFIGURABLE_SYSTEM_KINDS])
|
|
62
62
|
|
|
63
63
|
// Narrowed to the selected agent tier, EXCEPT that a kind the preset being edited already
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { ref, onScopeDispose } from 'vue'
|
|
2
2
|
import type { WorkspaceEvent } from '~/types/domain'
|
|
3
3
|
import { wsOriginFor } from '~/utils/apiOrigin'
|
|
4
|
+
import {
|
|
5
|
+
applyWorkspaceEvent,
|
|
6
|
+
type WorkspaceEventTargets,
|
|
7
|
+
} from '~/composables/workspaceStream/applyWorkspaceEvent'
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Subscribes to the backend's per-workspace WebSocket event stream and keeps the
|
|
@@ -8,11 +12,11 @@ import { wsOriginFor } from '~/utils/apiOrigin'
|
|
|
8
12
|
* once (e.g. on the board page) after the workspace is ready.
|
|
9
13
|
*
|
|
10
14
|
* `execution` events patch the run + its block directly; `bootstrap` events patch
|
|
11
|
-
* a repo-bootstrap run + its service frame (live "bootstrapping…" progress);
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* while disconnected, so the server stays the source of truth
|
|
15
|
-
* self-heals.
|
|
15
|
+
* a repo-bootstrap run + its service frame (live "bootstrapping…" progress); a
|
|
16
|
+
* `board` event patches the block it carries, or triggers a debounced full refresh when it
|
|
17
|
+
* carries none (a removal, a reparent, a service-frame change). On every (re)connect we refresh
|
|
18
|
+
* once to reconcile anything missed while disconnected, so the server stays the source of truth
|
|
19
|
+
* and a dropped socket self-heals. Routing lives in {@link applyWorkspaceEvent}.
|
|
16
20
|
*/
|
|
17
21
|
export function useWorkspaceStream() {
|
|
18
22
|
const workspace = useWorkspaceStore()
|
|
@@ -81,6 +85,27 @@ export function useWorkspaceStream() {
|
|
|
81
85
|
boardDebounce = setTimeout(() => void refreshWithRetry(workspaceId), 300)
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
// The stores this stream feeds, bound once. Routing lives in `applyWorkspaceEvent` so the
|
|
89
|
+
// targeted-vs-coarse decision on a `board` event is unit-testable without a socket.
|
|
90
|
+
const targets: WorkspaceEventTargets = {
|
|
91
|
+
upsertExecution: (instance) => execution.upsert(instance),
|
|
92
|
+
upsertBlock: (block) => board.upsert(block),
|
|
93
|
+
upsertBootstrap: (job) => agentRuns.upsertBootstrap(job),
|
|
94
|
+
upsertEnvConfigRepair: (job) => agentRuns.upsertEnvConfigRepair(job),
|
|
95
|
+
upsertEnvironmentTest: (run) => environmentTest.upsert(run),
|
|
96
|
+
patchInfraSetup: (area, status, detail) => workspace.patchInfraSetup(area, status, detail),
|
|
97
|
+
upsertNotification: (n) => notifications.upsert(n),
|
|
98
|
+
appendLlmCall: (call) => observability.appendCall(call),
|
|
99
|
+
upsertRequirements: (r) => requirements.upsert(r),
|
|
100
|
+
upsertConsensus: (s) => consensus.upsert(s),
|
|
101
|
+
upsertClarity: (r) => clarity.upsert(r),
|
|
102
|
+
upsertBrainstorm: (s) => brainstorm.upsert(s),
|
|
103
|
+
upsertKaizen: (g) => kaizen.upsert(g),
|
|
104
|
+
upsertInitiative: (i) => initiatives.upsert(i),
|
|
105
|
+
upsertDocInterview: (s) => docInterview.upsert(s),
|
|
106
|
+
refreshBoard: () => debouncedBoardRefresh(),
|
|
107
|
+
}
|
|
108
|
+
|
|
84
109
|
function onMessage(raw: string) {
|
|
85
110
|
let event: WorkspaceEvent
|
|
86
111
|
try {
|
|
@@ -88,78 +113,7 @@ export function useWorkspaceStream() {
|
|
|
88
113
|
} catch {
|
|
89
114
|
return
|
|
90
115
|
}
|
|
91
|
-
|
|
92
|
-
// Full instance drives the step-level UI; agentRuns derives its coarse
|
|
93
|
-
// failure/retry summary from the same store, so no extra call is needed.
|
|
94
|
-
execution.upsert(event.instance)
|
|
95
|
-
if (event.block) board.upsert(event.block)
|
|
96
|
-
} else if (event.type === 'board') {
|
|
97
|
-
debouncedBoardRefresh()
|
|
98
|
-
} else if (event.type === 'bootstrap') {
|
|
99
|
-
// Patch the run's live status/subtasks and its provisional/linked frame so
|
|
100
|
-
// the "bootstrapping…" card updates in place (then flips to a ready service
|
|
101
|
-
// or a failed badge) without a full refresh.
|
|
102
|
-
agentRuns.upsertBootstrap(event.job)
|
|
103
|
-
if (event.block) board.upsert(event.block)
|
|
104
|
-
} else if (event.type === 'env-config-repair') {
|
|
105
|
-
// A provider config-repair run advanced — patch its live status/subtasks/outcome so
|
|
106
|
-
// the infrastructure-providers window's "repairing…" indicator updates in place
|
|
107
|
-
// (then flips to ok / residual issues / a failure) without a refetch. No board block.
|
|
108
|
-
agentRuns.upsertEnvConfigRepair(event.job)
|
|
109
|
-
} else if (event.type === 'envTest') {
|
|
110
|
-
// An ephemeral-environment self-test advanced a stage — patch the run so the service
|
|
111
|
-
// inspector's "Test environment creation" control shows the live stage + final
|
|
112
|
-
// outcome in place without a refetch. No board block.
|
|
113
|
-
environmentTest.upsert(event.run)
|
|
114
|
-
} else if (event.type === 'infraSetup') {
|
|
115
|
-
// The reachability watcher found a configured infrastructure area dead (or answering again) —
|
|
116
|
-
// patch that one area so the setup banner appears/clears immediately. A full refresh would
|
|
117
|
-
// pay the whole snapshot aggregate for a one-field delta, and the projection the snapshot
|
|
118
|
-
// recomputes already folds the same recorded state.
|
|
119
|
-
workspace.patchInfraSetup(event.area, event.status, event.detail)
|
|
120
|
-
} else if (event.type === 'notification') {
|
|
121
|
-
// A PR needs a merge decision, a pipeline finished, or CI gave up — patch the
|
|
122
|
-
// inbox + per-block badge in place (resolved ones drop out of the inbox).
|
|
123
|
-
notifications.upsert(event.notification)
|
|
124
|
-
} else if (event.type === 'llmCall') {
|
|
125
|
-
// A container agent just made a model call — fold the compact summary into the
|
|
126
|
-
// observability store so an open "Model activity" panel updates live (and keeps
|
|
127
|
-
// updating even when the durable driver is evicted: the proxy emits these
|
|
128
|
-
// independently of the run's poll loop).
|
|
129
|
-
observability.appendCall(event.call)
|
|
130
|
-
} else if (event.type === 'requirements') {
|
|
131
|
-
// The async incorporate + re-review cycle changed a review's status — patch the cache
|
|
132
|
-
// so an open review window / inspector reflects it live ("incorporating…" → the next
|
|
133
|
-
// cycle / converged). The summons back, when needed, arrives as a `notification`.
|
|
134
|
-
requirements.upsert(event.review)
|
|
135
|
-
} else if (event.type === 'consensus') {
|
|
136
|
-
// A consensus session advanced (a round landed, the synthesis completed, or it
|
|
137
|
-
// failed) — patch the cache so an open Consensus Session window renders the
|
|
138
|
-
// multi-model process live, round by round.
|
|
139
|
-
consensus.upsert(event.session)
|
|
140
|
-
} else if (event.type === 'clarity') {
|
|
141
|
-
// The async incorporate + re-review cycle changed a clarity review's status — patch the
|
|
142
|
-
// cache so an open review window / inspector reflects it live ("incorporating…" → the
|
|
143
|
-
// next cycle / converged). The summons back, when needed, arrives as a `notification`.
|
|
144
|
-
clarity.upsert(event.review)
|
|
145
|
-
} else if (event.type === 'brainstorm') {
|
|
146
|
-
// The async incorporate + re-run cycle changed a brainstorm session's status — patch the
|
|
147
|
-
// cache so an open brainstorm window / inspector reflects it live.
|
|
148
|
-
brainstorm.upsert(event.session)
|
|
149
|
-
} else if (event.type === 'kaizen') {
|
|
150
|
-
// A post-run Kaizen grading was scheduled, started or completed — fold it into the
|
|
151
|
-
// run cache (so an open run window shows scheduled→running→complete live) and the
|
|
152
|
-
// Kaizen screen history. Never surfaced on the board.
|
|
153
|
-
kaizen.upsert(event.grading)
|
|
154
|
-
} else if (event.type === 'initiative') {
|
|
155
|
-
// An initiative changed (created, plan ingested, an item settled) — patch the cache
|
|
156
|
-
// so an open tracker window / the board card reflects the transition live.
|
|
157
|
-
initiatives.upsert(event.initiative)
|
|
158
|
-
} else if (event.type === 'docInterview') {
|
|
159
|
-
// The interactive document interview advanced (a fresh batch of questions, an answer, or
|
|
160
|
-
// convergence) — patch the cache so an open interview window reflects it live.
|
|
161
|
-
docInterview.upsert(event.session)
|
|
162
|
-
}
|
|
116
|
+
applyWorkspaceEvent(event, targets)
|
|
163
117
|
}
|
|
164
118
|
|
|
165
119
|
async function connect() {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import type { Block, WorkspaceEvent } from '~/types/domain'
|
|
3
|
+
import {
|
|
4
|
+
applyWorkspaceEvent,
|
|
5
|
+
type WorkspaceEventTargets,
|
|
6
|
+
} from '~/composables/workspaceStream/applyWorkspaceEvent'
|
|
7
|
+
|
|
8
|
+
function targets(): WorkspaceEventTargets & { calls: string[] } {
|
|
9
|
+
const calls: string[] = []
|
|
10
|
+
const record =
|
|
11
|
+
<T extends unknown[]>(name: string) =>
|
|
12
|
+
(...args: T) => {
|
|
13
|
+
void args
|
|
14
|
+
calls.push(name)
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
calls,
|
|
18
|
+
upsertExecution: record('upsertExecution'),
|
|
19
|
+
upsertBlock: record('upsertBlock'),
|
|
20
|
+
upsertBootstrap: record('upsertBootstrap'),
|
|
21
|
+
upsertEnvConfigRepair: record('upsertEnvConfigRepair'),
|
|
22
|
+
upsertEnvironmentTest: record('upsertEnvironmentTest'),
|
|
23
|
+
patchInfraSetup: record('patchInfraSetup'),
|
|
24
|
+
upsertNotification: record('upsertNotification'),
|
|
25
|
+
appendLlmCall: record('appendLlmCall'),
|
|
26
|
+
upsertRequirements: record('upsertRequirements'),
|
|
27
|
+
upsertConsensus: record('upsertConsensus'),
|
|
28
|
+
upsertClarity: record('upsertClarity'),
|
|
29
|
+
upsertBrainstorm: record('upsertBrainstorm'),
|
|
30
|
+
upsertKaizen: record('upsertKaizen'),
|
|
31
|
+
upsertInitiative: record('upsertInitiative'),
|
|
32
|
+
upsertDocInterview: record('upsertDocInterview'),
|
|
33
|
+
refreshBoard: record('refreshBoard'),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const task = {
|
|
38
|
+
id: 'blk_task',
|
|
39
|
+
title: 'Ship it',
|
|
40
|
+
type: 'service',
|
|
41
|
+
description: '',
|
|
42
|
+
position: { x: 0, y: 0 },
|
|
43
|
+
status: 'planned',
|
|
44
|
+
progress: 0,
|
|
45
|
+
dependsOn: [],
|
|
46
|
+
executionId: null,
|
|
47
|
+
level: 'task',
|
|
48
|
+
parentId: 'blk_frame',
|
|
49
|
+
} as unknown as Block
|
|
50
|
+
|
|
51
|
+
describe('applyWorkspaceEvent: the board branch', () => {
|
|
52
|
+
it('patches the carried block instead of refreshing the whole board', () => {
|
|
53
|
+
const to = targets()
|
|
54
|
+
const event: WorkspaceEvent = { type: 'board', reason: 'block-added', block: task, at: 1 }
|
|
55
|
+
|
|
56
|
+
applyWorkspaceEvent(event, to)
|
|
57
|
+
|
|
58
|
+
// The whole point of the change: a spawned task costs one upsert, not a snapshot fetch that
|
|
59
|
+
// REPLACES every store's list.
|
|
60
|
+
expect(to.calls).toEqual(['upsertBlock'])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('falls back to a full refresh when the change carries no block', () => {
|
|
64
|
+
const to = targets()
|
|
65
|
+
// Everything the publisher withholds a payload for lands here: a removal that cascades, a
|
|
66
|
+
// reparent that moves a subtree, and a service FRAME whose geometry is per-board. None has a
|
|
67
|
+
// single block that states the new shape, so the client must re-read its own projection.
|
|
68
|
+
const event: WorkspaceEvent = { type: 'board', reason: 'block-removed', at: 1 }
|
|
69
|
+
|
|
70
|
+
applyWorkspaceEvent(event, to)
|
|
71
|
+
|
|
72
|
+
expect(to.calls).toEqual(['refreshBoard'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('falls back to a full refresh when the payload is explicitly null', () => {
|
|
76
|
+
const to = targets()
|
|
77
|
+
// The frame shape as the publishers actually emit it: `deliverableBoardBlock` answers `null`
|
|
78
|
+
// rather than omitting the key, and an absent payload and a null one must route identically.
|
|
79
|
+
const event: WorkspaceEvent = { type: 'board', reason: 'block-archived', block: null, at: 1 }
|
|
80
|
+
|
|
81
|
+
applyWorkspaceEvent(event, to)
|
|
82
|
+
|
|
83
|
+
expect(to.calls).toEqual(['refreshBoard'])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('routes a board event through the SAME upsert an execution event uses', () => {
|
|
87
|
+
// Coherence: the monotonic live-upsert stamp that stops a stale refresh clobbering newer
|
|
88
|
+
// state lives in `board.upsert`. A targeted board event that patched `blocks` any other way
|
|
89
|
+
// would silently escape that guard.
|
|
90
|
+
const upsertBlock = vi.fn()
|
|
91
|
+
const to = { ...targets(), upsertBlock }
|
|
92
|
+
|
|
93
|
+
applyWorkspaceEvent({ type: 'board', reason: 'block-updated', block: task, at: 1 }, to)
|
|
94
|
+
applyWorkspaceEvent(
|
|
95
|
+
{
|
|
96
|
+
type: 'execution',
|
|
97
|
+
instance: { id: 'exec_1', blockId: task.id } as never,
|
|
98
|
+
block: task,
|
|
99
|
+
at: 2,
|
|
100
|
+
},
|
|
101
|
+
to,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
expect(upsertBlock).toHaveBeenCalledTimes(2)
|
|
105
|
+
expect(upsertBlock).toHaveBeenNthCalledWith(1, task)
|
|
106
|
+
expect(upsertBlock).toHaveBeenNthCalledWith(2, task)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('applyWorkspaceEvent: the other branches', () => {
|
|
111
|
+
it('keeps every non-board event on its own targeted store call', () => {
|
|
112
|
+
// Each type below is delivered as a patch rather than a board refresh. This table cannot see
|
|
113
|
+
// a NEW event type (an absent member is just absent from a hand-written list). That job
|
|
114
|
+
// belongs to the `never` guard on the switch's `default`, which fails the BUILD instead.
|
|
115
|
+
const cases: [WorkspaceEvent, string][] = [
|
|
116
|
+
[{ type: 'bootstrap', job: {} as never, block: null, at: 1 }, 'upsertBootstrap'],
|
|
117
|
+
[{ type: 'env-config-repair', job: {} as never, at: 1 }, 'upsertEnvConfigRepair'],
|
|
118
|
+
[{ type: 'envTest', run: {} as never, at: 1 }, 'upsertEnvironmentTest'],
|
|
119
|
+
[
|
|
120
|
+
{ type: 'infraSetup', area: 'runnerPool' as never, status: 'ok' as never, at: 1 },
|
|
121
|
+
'patchInfraSetup',
|
|
122
|
+
],
|
|
123
|
+
[{ type: 'notification', notification: {} as never, at: 1 }, 'upsertNotification'],
|
|
124
|
+
[{ type: 'llmCall', call: {} as never, at: 1 }, 'appendLlmCall'],
|
|
125
|
+
[{ type: 'requirements', review: {} as never, at: 1 }, 'upsertRequirements'],
|
|
126
|
+
[{ type: 'consensus', session: {} as never, at: 1 }, 'upsertConsensus'],
|
|
127
|
+
[{ type: 'clarity', review: {} as never, at: 1 }, 'upsertClarity'],
|
|
128
|
+
[{ type: 'brainstorm', session: {} as never, at: 1 }, 'upsertBrainstorm'],
|
|
129
|
+
[{ type: 'kaizen', grading: {} as never, at: 1 }, 'upsertKaizen'],
|
|
130
|
+
[{ type: 'initiative', initiative: {} as never, at: 1 }, 'upsertInitiative'],
|
|
131
|
+
[{ type: 'docInterview', session: {} as never, at: 1 }, 'upsertDocInterview'],
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
for (const [event, expected] of cases) {
|
|
135
|
+
const to = targets()
|
|
136
|
+
applyWorkspaceEvent(event, to)
|
|
137
|
+
expect(to.calls, `${event.type} should route to ${expected}`).toEqual([expected])
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('drops an event type it does not know rather than tearing down the session', () => {
|
|
142
|
+
// A backend one release ahead pushes types this build has never heard of. The socket carries
|
|
143
|
+
// every other event for the whole workspace, so the unknown one is dropped, not thrown on.
|
|
144
|
+
const to = targets()
|
|
145
|
+
|
|
146
|
+
applyWorkspaceEvent({ type: 'a-later-release', at: 1 } as unknown as WorkspaceEvent, to)
|
|
147
|
+
|
|
148
|
+
expect(to.calls).toEqual([])
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Block,
|
|
3
|
+
BootstrapJob,
|
|
4
|
+
EnvConfigRepairJob,
|
|
5
|
+
EnvironmentTestRun,
|
|
6
|
+
ExecutionInstance,
|
|
7
|
+
InfraSetupArea,
|
|
8
|
+
InfraSetupStatus,
|
|
9
|
+
Initiative,
|
|
10
|
+
KaizenGrading,
|
|
11
|
+
LlmCallActivity,
|
|
12
|
+
Notification,
|
|
13
|
+
WorkspaceEvent,
|
|
14
|
+
} from '~/types/domain'
|
|
15
|
+
import type { BrainstormSession } from '~/types/brainstorm'
|
|
16
|
+
import type { ClarityReview } from '~/types/clarity'
|
|
17
|
+
import type { ConsensusSession } from '~/types/consensus'
|
|
18
|
+
import type { DocInterviewSession } from '~/types/doc-interview'
|
|
19
|
+
import type { RequirementReview } from '~/types/requirements'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Everything {@link applyWorkspaceEvent} needs to route one pushed event into the stores, as bound
|
|
23
|
+
* callbacks rather than the stores themselves: it makes the routing (above all the `board` branch's
|
|
24
|
+
* targeted-vs-coarse decision) unit-testable without a Pinia instance or a live socket.
|
|
25
|
+
*
|
|
26
|
+
* Each callback names the DOMAIN type it takes, not the event field it happens to be fed from
|
|
27
|
+
* today. `upsertBlock` is the reason that matters: three branches (`execution`, `board`,
|
|
28
|
+
* `bootstrap`) share it, so typing it off any one of their payloads would silently retype the
|
|
29
|
+
* other two the next time that event's shape moved.
|
|
30
|
+
*/
|
|
31
|
+
export interface WorkspaceEventTargets {
|
|
32
|
+
upsertExecution: (instance: ExecutionInstance) => void
|
|
33
|
+
upsertBlock: (block: Block) => void
|
|
34
|
+
upsertBootstrap: (job: BootstrapJob) => void
|
|
35
|
+
upsertEnvConfigRepair: (job: EnvConfigRepairJob) => void
|
|
36
|
+
upsertEnvironmentTest: (run: EnvironmentTestRun) => void
|
|
37
|
+
patchInfraSetup: (area: InfraSetupArea, status: InfraSetupStatus, detail?: string) => void
|
|
38
|
+
upsertNotification: (n: Notification) => void
|
|
39
|
+
appendLlmCall: (call: LlmCallActivity) => void
|
|
40
|
+
upsertRequirements: (r: RequirementReview) => void
|
|
41
|
+
upsertConsensus: (s: ConsensusSession) => void
|
|
42
|
+
upsertClarity: (r: ClarityReview) => void
|
|
43
|
+
upsertBrainstorm: (s: BrainstormSession) => void
|
|
44
|
+
upsertKaizen: (g: KaizenGrading) => void
|
|
45
|
+
upsertInitiative: (i: Initiative) => void
|
|
46
|
+
upsertDocInterview: (s: DocInterviewSession) => void
|
|
47
|
+
/** Debounced full `workspace.refresh()`: the fallback for a change no payload can state. */
|
|
48
|
+
refreshBoard: () => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Route one pushed workspace event into the stores.
|
|
53
|
+
*
|
|
54
|
+
* The `board` branch is the one with a decision in it. A `board` event used to mean "re-fetch the
|
|
55
|
+
* whole snapshot", which on an active board is a REPLACE-style rehydrate of ~20 stores every ~300ms
|
|
56
|
+
* debounce window, for changes as small as one spawned task. The backend now carries the changed
|
|
57
|
+
* block whenever the change is fully described by it (see the `board` case in
|
|
58
|
+
* `@cat-factory/contracts`' `WorkspaceEvent`), so those patch in place exactly like an `execution`
|
|
59
|
+
* event's block does, through the same `upsert` whose monotonic stamp keeps a later stale refresh
|
|
60
|
+
* from clobbering them.
|
|
61
|
+
*
|
|
62
|
+
* A `board` event with NO block keeps the old behaviour, and that is not a fallback to tidy away:
|
|
63
|
+
* a removal, a reparent, a blueprint reconcile and every service-frame change genuinely need the
|
|
64
|
+
* refresh, because their new shape is not one block's contents.
|
|
65
|
+
*/
|
|
66
|
+
export function applyWorkspaceEvent(event: WorkspaceEvent, to: WorkspaceEventTargets): void {
|
|
67
|
+
switch (event.type) {
|
|
68
|
+
case 'execution':
|
|
69
|
+
// Full instance drives the step-level UI; agentRuns derives its coarse
|
|
70
|
+
// failure/retry summary from the same store, so no extra call is needed.
|
|
71
|
+
to.upsertExecution(event.instance)
|
|
72
|
+
if (event.block) to.upsertBlock(event.block)
|
|
73
|
+
return
|
|
74
|
+
case 'board':
|
|
75
|
+
// Targeted when the change fits in one block, coarse otherwise. Both shapes reach every
|
|
76
|
+
// board that mounts the affected service; only the cost differs.
|
|
77
|
+
if (event.block) to.upsertBlock(event.block)
|
|
78
|
+
else to.refreshBoard()
|
|
79
|
+
return
|
|
80
|
+
case 'bootstrap':
|
|
81
|
+
// Patch the run's live status/subtasks and its provisional/linked frame so
|
|
82
|
+
// the "bootstrapping…" card updates in place (then flips to a ready service
|
|
83
|
+
// or a failed badge) without a full refresh.
|
|
84
|
+
to.upsertBootstrap(event.job)
|
|
85
|
+
if (event.block) to.upsertBlock(event.block)
|
|
86
|
+
return
|
|
87
|
+
case 'env-config-repair':
|
|
88
|
+
// A provider config-repair run advanced: patch its live status/subtasks/outcome so the
|
|
89
|
+
// infrastructure-providers window's "repairing…" indicator updates in place (then flips to
|
|
90
|
+
// ok / residual issues / a failure) without a refetch. No board block.
|
|
91
|
+
to.upsertEnvConfigRepair(event.job)
|
|
92
|
+
return
|
|
93
|
+
case 'envTest':
|
|
94
|
+
// An ephemeral-environment self-test advanced a stage: patch the run so the service
|
|
95
|
+
// inspector's "Test environment creation" control shows the live stage + final outcome in
|
|
96
|
+
// place without a refetch. No board block.
|
|
97
|
+
to.upsertEnvironmentTest(event.run)
|
|
98
|
+
return
|
|
99
|
+
case 'infraSetup':
|
|
100
|
+
// The reachability watcher found a configured infrastructure area dead (or answering again):
|
|
101
|
+
// patch that one area so the setup banner appears/clears immediately. A full refresh would
|
|
102
|
+
// pay the whole snapshot aggregate for a one-field delta, and the projection the snapshot
|
|
103
|
+
// recomputes already folds the same recorded state.
|
|
104
|
+
to.patchInfraSetup(event.area, event.status, event.detail)
|
|
105
|
+
return
|
|
106
|
+
case 'notification':
|
|
107
|
+
// A PR needs a merge decision, a pipeline finished, or CI gave up: patch the
|
|
108
|
+
// inbox + per-block badge in place (resolved ones drop out of the inbox).
|
|
109
|
+
to.upsertNotification(event.notification)
|
|
110
|
+
return
|
|
111
|
+
case 'llmCall':
|
|
112
|
+
// A container agent just made a model call: fold the compact summary into the observability
|
|
113
|
+
// store so an open "Model activity" panel updates live (and keeps updating even when the
|
|
114
|
+
// durable driver is evicted, since the proxy emits these independently of the poll loop).
|
|
115
|
+
to.appendLlmCall(event.call)
|
|
116
|
+
return
|
|
117
|
+
case 'requirements':
|
|
118
|
+
// The async incorporate + re-review cycle changed a review's status: patch the cache so an
|
|
119
|
+
// open review window / inspector reflects it live ("incorporating…" → the next cycle /
|
|
120
|
+
// converged). The summons back, when needed, arrives as a `notification`.
|
|
121
|
+
to.upsertRequirements(event.review)
|
|
122
|
+
return
|
|
123
|
+
case 'consensus':
|
|
124
|
+
// A consensus session advanced (a round landed, the synthesis completed, or it failed):
|
|
125
|
+
// patch the cache so an open Consensus Session window renders the multi-model process live,
|
|
126
|
+
// round by round.
|
|
127
|
+
to.upsertConsensus(event.session)
|
|
128
|
+
return
|
|
129
|
+
case 'clarity':
|
|
130
|
+
// The clarity mirror of `requirements`.
|
|
131
|
+
to.upsertClarity(event.review)
|
|
132
|
+
return
|
|
133
|
+
case 'brainstorm':
|
|
134
|
+
// The async incorporate + re-run cycle changed a brainstorm session's status: patch the
|
|
135
|
+
// cache so an open brainstorm window / inspector reflects it live.
|
|
136
|
+
to.upsertBrainstorm(event.session)
|
|
137
|
+
return
|
|
138
|
+
case 'kaizen':
|
|
139
|
+
// A post-run Kaizen grading was scheduled, started or completed: fold it into the run cache
|
|
140
|
+
// (so an open run window shows scheduled→running→complete live) and the Kaizen screen
|
|
141
|
+
// history. Never surfaced on the board.
|
|
142
|
+
to.upsertKaizen(event.grading)
|
|
143
|
+
return
|
|
144
|
+
case 'initiative':
|
|
145
|
+
// An initiative changed (created, plan ingested, an item settled): patch the cache so an
|
|
146
|
+
// open tracker window / the board card reflects the transition live.
|
|
147
|
+
to.upsertInitiative(event.initiative)
|
|
148
|
+
return
|
|
149
|
+
case 'docInterview':
|
|
150
|
+
// The interactive document interview advanced (a fresh batch of questions, an answer, or
|
|
151
|
+
// convergence): patch the cache so an open interview window reflects it live.
|
|
152
|
+
to.upsertDocInterview(event.session)
|
|
153
|
+
return
|
|
154
|
+
default:
|
|
155
|
+
return dropUnknownEvent(event)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The exhaustiveness guard for the routing above.
|
|
161
|
+
*
|
|
162
|
+
* The COMPILE-TIME half is the point: `never` fails the build the moment `WorkspaceEvent` gains a
|
|
163
|
+
* member with no case, so a new pushed event cannot ship as a branch that silently does nothing.
|
|
164
|
+
* The spec's per-type table cannot do that job: a member absent from a hand-written list is just
|
|
165
|
+
* absent, and the suite stays green.
|
|
166
|
+
*
|
|
167
|
+
* At RUNTIME this deliberately drops the event. A backend one release ahead of the SPA legitimately
|
|
168
|
+
* pushes types this build has never heard of, and the connection carries every other event for the
|
|
169
|
+
* whole workspace: throwing would trade one unknown payload for the entire live session.
|
|
170
|
+
*/
|
|
171
|
+
function dropUnknownEvent(event: never): void {
|
|
172
|
+
void event
|
|
173
|
+
}
|
|
@@ -4,19 +4,19 @@ A deployment that consumes this layer (`extends: ['@cat-factory/app']`) can cont
|
|
|
4
4
|
its own **components** (result windows, navigation entries, inspector panels, agent-kind
|
|
5
5
|
palette data) **without forking the layer**. This is the frontend counterpart of the
|
|
6
6
|
backend's public registries (`registerAgentKind`, `registerGate`; see
|
|
7
|
-
[`backend/docs/custom-agents.md`](
|
|
7
|
+
[`backend/docs/custom-agents.md`](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/custom-agents.md)). The governing
|
|
8
8
|
principle is the same: **zero host edits for a consumer extension**.
|
|
9
9
|
|
|
10
10
|
A worked, end-to-end example ships in the template deployment:
|
|
11
|
-
[`deploy/frontend/app/`](
|
|
11
|
+
[`deploy/frontend/app/`](https://github.com/kibertoad/cat-factory/tree/main/deploy/frontend/app) (the `acme:security` module): the
|
|
12
12
|
frontend analogue of the backend
|
|
13
|
-
[`@cat-factory/example-custom-agent`](
|
|
13
|
+
[`@cat-factory/example-custom-agent`](https://github.com/kibertoad/cat-factory/tree/main/backend/internal/example-custom-agent)
|
|
14
14
|
package. Read this guide alongside it.
|
|
15
15
|
|
|
16
16
|
> This is the **landed** surface (modular-vue adoption slices 1–5). The larger consumer
|
|
17
17
|
> extension programme (custom task types, generic interactive phases, overlays, consumer
|
|
18
18
|
> notification kinds, stream events, and the hardened public export surface) is tracked in
|
|
19
|
-
> [`docs/initiatives/frontend-extension-mechanism.md`](
|
|
19
|
+
> [`docs/initiatives/frontend-extension-mechanism.md`](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/frontend-extension-mechanism.md).
|
|
20
20
|
|
|
21
21
|
## The one seam: `registerAppModule`
|
|
22
22
|
|
|
@@ -219,8 +219,11 @@ suite can address a row and the caption inside it.
|
|
|
219
219
|
Together, `fields` + `defaultFragmentIds` + `defaultPipelineId` are what turns a task type from a
|
|
220
220
|
badge into a **reusable operation**: a canned unit of work an org runs repeatedly with per-case
|
|
221
221
|
input, whose collected values reach every agent's prompt. See
|
|
222
|
-
[`backend/docs/reusable-operations.md`](
|
|
223
|
-
`org:introduce-api` worked example
|
|
222
|
+
[`backend/docs/reusable-operations.md`](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/reusable-operations.md) and the
|
|
223
|
+
[`org:introduce-api` worked example](https://github.com/kibertoad/cat-factory/tree/main/backend/internal/example-custom-agent).
|
|
224
|
+
Both links are absolute on purpose: this page ships inside the `@cat-factory/app` tarball, so a
|
|
225
|
+
relative path out of it resolves only from a checkout, which is the one place its reader is not.
|
|
226
|
+
(`scripts/check-shipped-doc-links.mjs` fails CI on a shipped doc that grows one.)
|
|
224
227
|
|
|
225
228
|
The **same type can be delivered from the backend** instead of code-shipped: register it on the
|
|
226
229
|
deployment's app-owned `TaskTypeRegistry` and it arrives in the workspace snapshot's
|
package/app/stores/agents.ts
CHANGED
|
@@ -69,9 +69,11 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
69
69
|
* mapped to display metadata, de-duplicated, and never shadowing a built-in or
|
|
70
70
|
* system kind. The old `registerCustomKinds` only guarded `AGENT_BY_KIND`; this
|
|
71
71
|
* intentionally ALSO drops any custom kind colliding with a `SYSTEM_AGENT_META`
|
|
72
|
-
* kind (`ci` / `merger` /
|
|
73
|
-
*
|
|
74
|
-
*
|
|
72
|
+
* kind (`ci` / `merger` / gates …), so a snapshot can't override an engine kind's
|
|
73
|
+
* palette entry either — matching `agentKindMeta`'s precedence (built-in → system
|
|
74
|
+
* → custom), where a colliding custom kind would never win anyway. Note the cost of
|
|
75
|
+
* that guard: a SYSTEM_AGENT_META entry silently removes a registered kind from the
|
|
76
|
+
* palette, so the map must stay limited to kinds the engine inserts itself.
|
|
75
77
|
*/
|
|
76
78
|
const customArchetypes = computed<AgentArchetype[]>(() => {
|
|
77
79
|
const seen = new Set<string>()
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
AGENT_ARCHETYPES,
|
|
5
5
|
AGENT_BY_KIND,
|
|
6
6
|
BLOCK_TYPE_META,
|
|
7
|
+
COMPANION_FOR_PRODUCER,
|
|
8
|
+
MODEL_CONFIGURABLE_SYSTEM_KINDS,
|
|
7
9
|
STATUS_META,
|
|
8
10
|
SYSTEM_AGENT_META,
|
|
9
11
|
agentKindMeta,
|
|
@@ -20,9 +22,12 @@ const AGENT_KINDS: AgentKind[] = [
|
|
|
20
22
|
'pr-reviewer',
|
|
21
23
|
'spike',
|
|
22
24
|
'task-estimator',
|
|
25
|
+
'spec-writer',
|
|
26
|
+
'blueprints',
|
|
23
27
|
'architect',
|
|
24
28
|
'researcher',
|
|
25
29
|
'coder',
|
|
30
|
+
'deployer',
|
|
26
31
|
'tester-api',
|
|
27
32
|
'tester-ui',
|
|
28
33
|
'reviewer',
|
|
@@ -80,6 +85,38 @@ describe('catalog', () => {
|
|
|
80
85
|
expect(basic).toEqual(expect.arrayContaining(['architect', 'coder', 'tester-api']))
|
|
81
86
|
})
|
|
82
87
|
|
|
88
|
+
it('never shadows a companion producer as a system kind', () => {
|
|
89
|
+
// A companion is never placed directly: the builder renders it as a toggle on its producer
|
|
90
|
+
// step, so a producer that cannot be placed takes its companion out of the builder with it.
|
|
91
|
+
// A producer reaches the palette either statically (AGENT_ARCHETYPES) or from the backend
|
|
92
|
+
// registry — and an entry in SYSTEM_AGENT_META DROPS the registry's copy (see the agents
|
|
93
|
+
// store's `customArchetypes`), which is how `spec-writer` and its `spec-companion` both
|
|
94
|
+
// became unreachable. The shadow is the half this file owns, so it is the half asserted.
|
|
95
|
+
for (const producer of Object.keys(COMPANION_FOR_PRODUCER)) {
|
|
96
|
+
expect(
|
|
97
|
+
producer in SYSTEM_AGENT_META,
|
|
98
|
+
`${producer} has a companion but is shadowed as a system kind, so neither can be placed`,
|
|
99
|
+
).toBe(false)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('resolves every kind the Model Defaults panel lists beside the palette', () => {
|
|
104
|
+
// The list is spelled as kind strings indexed into SYSTEM_AGENT_META through a non-null
|
|
105
|
+
// assertion, so a kind that MOVES to the palette (or is renamed) leaves an `undefined` in
|
|
106
|
+
// the array rather than a type error, and the panel renders a blank row it cannot pin a
|
|
107
|
+
// model on. Assert the relation the assertion claims.
|
|
108
|
+
for (const entry of MODEL_CONFIGURABLE_SYSTEM_KINDS) {
|
|
109
|
+
expect(entry, 'MODEL_CONFIGURABLE_SYSTEM_KINDS names a kind with no metadata').toBeDefined()
|
|
110
|
+
expect(entry.kind).toEqual(expect.any(String))
|
|
111
|
+
}
|
|
112
|
+
// And nothing is offered twice: a palette archetype is already listed by the panel, so a
|
|
113
|
+
// kind appearing in both would render a duplicate row.
|
|
114
|
+
const palette = new Set(AGENT_ARCHETYPES.map((a) => a.kind))
|
|
115
|
+
for (const entry of MODEL_CONFIGURABLE_SYSTEM_KINDS) {
|
|
116
|
+
expect(palette.has(entry.kind), `${entry.kind} is listed twice in Model Defaults`).toBe(false)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
83
120
|
it('resolves usable metadata for every kind via agentKindMeta', () => {
|
|
84
121
|
// Palette archetypes resolve to their own entry.
|
|
85
122
|
for (const a of AGENT_ARCHETYPES) {
|
|
@@ -88,8 +125,6 @@ describe('catalog', () => {
|
|
|
88
125
|
// Engine system kinds (present in seeded pipelines but not the palette) resolve
|
|
89
126
|
// to their system metadata rather than blowing up an undefined access.
|
|
90
127
|
for (const kind of [
|
|
91
|
-
'spec-writer',
|
|
92
|
-
'blueprints',
|
|
93
128
|
'conflicts',
|
|
94
129
|
'conflict-resolver',
|
|
95
130
|
'ci',
|
package/app/utils/catalog.ts
CHANGED
|
@@ -136,6 +136,21 @@ export const AGENT_ARCHETYPES: AgentArchetype[] = [
|
|
|
136
136
|
'A structured dialogue that explores and finalizes a technical approach from the refined requirements — proposing options with explicit trade-offs and letting you converge, before the architect.',
|
|
137
137
|
resultView: 'brainstorm',
|
|
138
138
|
},
|
|
139
|
+
{
|
|
140
|
+
// Authors the service's in-repo specification from the clarified requirements, so it sits
|
|
141
|
+
// beside the design kinds and ahead of the architect that reads what it wrote. Registered on
|
|
142
|
+
// the backend so it also arrives via the workspace manifest, and modelled statically here for
|
|
143
|
+
// the same reason `pr-reviewer` is: a `pl_bugfix` / `pl_spec` timeline must name the step
|
|
144
|
+
// before the manifest hydrates. Mirrors the backend `presentation` in `spec-blueprints.ts`.
|
|
145
|
+
kind: 'spec-writer',
|
|
146
|
+
tier: 'intermediate',
|
|
147
|
+
label: 'Spec Writer',
|
|
148
|
+
icon: 'i-lucide-clipboard-list',
|
|
149
|
+
color: '#c084fc',
|
|
150
|
+
category: 'design',
|
|
151
|
+
description:
|
|
152
|
+
"Aggregates every task's clarified requirements into the service's in-repo specification (spec.json) with full acceptance-scenario coverage, derived into Gherkin.",
|
|
153
|
+
},
|
|
139
154
|
{
|
|
140
155
|
kind: 'architect',
|
|
141
156
|
tier: 'basic',
|
|
@@ -145,6 +160,18 @@ export const AGENT_ARCHETYPES: AgentArchetype[] = [
|
|
|
145
160
|
category: 'design',
|
|
146
161
|
description: 'Designs the shape of the solution and breaks down the work.',
|
|
147
162
|
},
|
|
163
|
+
{
|
|
164
|
+
// Refreshes the service → modules map the board projects. Statically modelled beside its
|
|
165
|
+
// backend `presentation` for the same reason the Spec Writer is: `pl_blueprint` timelines
|
|
166
|
+
// render before the manifest hydrates.
|
|
167
|
+
kind: 'blueprints',
|
|
168
|
+
tier: 'intermediate',
|
|
169
|
+
label: 'Blueprinter',
|
|
170
|
+
icon: 'i-lucide-map',
|
|
171
|
+
color: '#22d3ee',
|
|
172
|
+
category: 'design',
|
|
173
|
+
description: 'Maps the repository into the service → modules blueprint.',
|
|
174
|
+
},
|
|
148
175
|
{
|
|
149
176
|
kind: 'researcher',
|
|
150
177
|
tier: 'intermediate',
|
|
@@ -181,6 +208,21 @@ export const AGENT_ARCHETYPES: AgentArchetype[] = [
|
|
|
181
208
|
category: 'build',
|
|
182
209
|
description: 'Builds WireMock mocks for external services and wires them into local/CI runs.',
|
|
183
210
|
},
|
|
211
|
+
{
|
|
212
|
+
// Provisions the ephemeral environment the tester / human-test / playwright steps read, which
|
|
213
|
+
// is why it leads the testing group. A palette block for the same reason `disposer` is one:
|
|
214
|
+
// `assertDeployerBeforeConsumer` REFUSES a run whose chain reaches an env consumer with no
|
|
215
|
+
// Deployer in front of it on a deployable service, and a hand-built pipeline that hits that
|
|
216
|
+
// refusal has no reseed to fall back on.
|
|
217
|
+
kind: 'deployer',
|
|
218
|
+
tier: 'intermediate',
|
|
219
|
+
label: 'Deployer',
|
|
220
|
+
icon: 'i-lucide-cloud-upload',
|
|
221
|
+
color: '#34d399',
|
|
222
|
+
category: 'test',
|
|
223
|
+
description:
|
|
224
|
+
'Provisions the ephemeral environment the tester and human-test gate run against (kubernetes / custom services); a no-op for docker-compose / infraless. Place it before the first step that needs the environment.',
|
|
225
|
+
},
|
|
184
226
|
{
|
|
185
227
|
kind: 'tester-api',
|
|
186
228
|
tier: 'basic',
|
|
@@ -491,29 +533,22 @@ export function isTesterKind(kind: string): boolean {
|
|
|
491
533
|
|
|
492
534
|
/**
|
|
493
535
|
* Display metadata for the engine-driven "system" kinds — the gate/automation
|
|
494
|
-
* steps (
|
|
495
|
-
*
|
|
496
|
-
*
|
|
536
|
+
* steps (conflicts gate + resolver, CI gate + fixer, merger) that appear in
|
|
537
|
+
* seeded pipelines and run timelines but are NOT user-addable palette
|
|
538
|
+
* archetypes, so they're intentionally absent from {@link AGENT_ARCHETYPES}
|
|
497
539
|
* / {@link AGENT_BY_KIND}. Looked up through {@link agentKindMeta}.
|
|
540
|
+
*
|
|
541
|
+
* An entry here also SHADOWS the backend's own catalog: the agents store drops any
|
|
542
|
+
* registered kind whose id appears in this map (see `customArchetypes`), so listing a
|
|
543
|
+
* kind that declares `presentation` silently overrides the deployment's decision to
|
|
544
|
+
* offer it. That is how `spec-writer` and `blueprints` stayed out of the palette while
|
|
545
|
+
* both collapse docs promised them as opt-in builder steps, and it took
|
|
546
|
+
* `spec-companion` with them: a companion renders as a toggle on its producer, so a
|
|
547
|
+
* shadowed producer removes both. Add a kind here only when the ENGINE decides the
|
|
548
|
+
* step exists — a gate it inserts, a helper it escalates to — and never when the
|
|
549
|
+
* backend registers it as a palette block.
|
|
498
550
|
*/
|
|
499
551
|
export const SYSTEM_AGENT_META: Record<string, AgentArchetype> = {
|
|
500
|
-
'spec-writer': {
|
|
501
|
-
kind: 'spec-writer',
|
|
502
|
-
tier: 'intermediate',
|
|
503
|
-
label: 'Spec Writer',
|
|
504
|
-
icon: 'i-lucide-clipboard-list',
|
|
505
|
-
color: '#c084fc',
|
|
506
|
-
description:
|
|
507
|
-
"Aggregates every task's clarified requirements into the service's in-repo specification (spec.json) with full acceptance-scenario coverage, derived into Gherkin.",
|
|
508
|
-
},
|
|
509
|
-
blueprints: {
|
|
510
|
-
kind: 'blueprints',
|
|
511
|
-
tier: 'intermediate',
|
|
512
|
-
label: 'Blueprinter',
|
|
513
|
-
icon: 'i-lucide-map',
|
|
514
|
-
color: '#22d3ee',
|
|
515
|
-
description: 'Maps the repository into the service → modules blueprint.',
|
|
516
|
-
},
|
|
517
552
|
// The read-only Challenge Investigator: dispatched off a parked `pr-reviewer` step when a human
|
|
518
553
|
// challenges a finding, it re-examines that ONE finding against the full source and upholds
|
|
519
554
|
// (strengthening) or retracts it. Never a palette block; modelled here purely so it is a
|
|
@@ -529,19 +564,6 @@ export const SYSTEM_AGENT_META: Record<string, AgentArchetype> = {
|
|
|
529
564
|
'Re-examines a single challenged PR-review finding against the full source, then upholds ' +
|
|
530
565
|
'(strengthening it) or retracts it with a justification. Configurable separately from the reviewer.',
|
|
531
566
|
},
|
|
532
|
-
// The single environment provisioner: an operational (non-LLM) step that stands up the ephemeral
|
|
533
|
-
// environment the tester / human-test gate run against for a kubernetes/custom service, and is a
|
|
534
|
-
// fast no-op for docker-compose / infraless. Seeded before the first tester/human-test step in the
|
|
535
|
-
// built-in pipelines, so it needs display metadata (else it renders as a generic gray "Agent").
|
|
536
|
-
deployer: {
|
|
537
|
-
kind: 'deployer',
|
|
538
|
-
tier: 'intermediate',
|
|
539
|
-
label: 'Deployer',
|
|
540
|
-
icon: 'i-lucide-cloud-upload',
|
|
541
|
-
color: '#34d399',
|
|
542
|
-
description:
|
|
543
|
-
'Provisions the ephemeral environment the tester and human-test gate run against (kubernetes / custom services); a no-op for docker-compose / infraless.',
|
|
544
|
-
},
|
|
545
567
|
// The Initiative Planning pipeline's steps. Only runnable on an initiative
|
|
546
568
|
// block (pl_initiative — enforced by the engine), so they are display-metadata
|
|
547
569
|
// system kinds, never palette archetypes. The analyst runs FIRST, ahead of the
|
|
@@ -766,8 +788,6 @@ export const OBSERVABILITY_GATE_ARCHETYPE: AgentArchetype =
|
|
|
766
788
|
*/
|
|
767
789
|
export const MODEL_CONFIGURABLE_SYSTEM_KINDS: AgentArchetype[] = [
|
|
768
790
|
...[
|
|
769
|
-
'spec-writer',
|
|
770
|
-
'blueprints',
|
|
771
791
|
'initiative-planner',
|
|
772
792
|
'conflict-resolver',
|
|
773
793
|
'ci-fixer',
|
|
@@ -798,7 +818,7 @@ const FALLBACK_AGENT_META: Omit<AgentArchetype, 'kind'> = {
|
|
|
798
818
|
* {@link customAgentKindMeta} by the agents store), or an unknown one — ALWAYS
|
|
799
819
|
* returning a usable icon/label/color. This is the single lookup every pipeline
|
|
800
820
|
* / run renderer should use so a kind missing from the archetype map (e.g.
|
|
801
|
-
* `ci`/`merger
|
|
821
|
+
* `ci`/`merger` in a seeded pipeline) can never blow up a component
|
|
802
822
|
* with an undefined access. Reading `customAgentKindMeta` reactively means a
|
|
803
823
|
* component computed re-runs when the custom catalog changes.
|
|
804
824
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.235.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",
|
|
@@ -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.255.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|