@cat-factory/app 0.270.2 → 0.270.3

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 CHANGED
@@ -348,8 +348,12 @@ picking what each of them runs on are halves of the same job.
348
348
  The builder's palette narrows on two axes, and both controls sit on one row above the catalog
349
349
  (`PipelinePurposeSelect` above `AgentTierSelect`), each with its own "n hidden" hint so neither
350
350
  narrowing reads as an empty catalog. The tier says how deep to look; the **purpose** says what
351
- the pipeline is for (`build` / `document` / `review` / `research` / `planning`), and the palette
352
- drops the categories that purpose has no use for. It reaches past the palette: the saved-pipeline
351
+ the pipeline is for (`build` / `bugfix` / `document` / `review` / `research` / `planning`), and the
352
+ palette drops the categories that purpose has no use for. `bugfix` is the one pair that shares a
353
+ row with another member: it ships code exactly as `build` does, and differs only in being offered
354
+ to a `bug` task and withheld from a `feature` one, because a preset that investigates a defect
355
+ report and writes a failing reproduction test has neither input on a feature. It reaches past the
356
+ palette: the saved-pipeline
353
357
  library in the builder's third column lists the pipelines built for the purpose being edited, so
354
358
  one dial narrows both ends of the slideover. The purpose is not a view preference either: it is
355
359
  saved on the pipeline and also decides which task pickers offer it, which is why the control
@@ -380,13 +380,14 @@ const selectedModelPresetLabel = computed(() => {
380
380
  // still browsable.
381
381
  const fragmentPool = computed(() => fragments.forBlockType(frame.value?.type ?? 'service'))
382
382
 
383
- // Hide UI-testing pipelines (`tester-ui` / `visual-confirmation`) when the target frame has no
384
- // UI to exercise they'd be refused server-side (see utils/pipeline + the backend gate). Also
385
- // hide `'recurring'`-only pipelines (a one-off task start of one is refused at run start) and every
386
- // pipeline whose purpose doesn't match the chosen task type (a doc task authors a doc, a review task
387
- // reviews a PR, and a `feature`/`bug` task ships code so it is offered build + research only, not
388
- // the doc/review/planning presets). `blockLevel: 'task'` is passed literally because this modal only
389
- // ever creates a task leaf, which also drops the three planning presets the backend would refuse.
383
+ // Hide UI-testing pipelines when the target frame has no UI for them to reach — a step scoped to
384
+ // a frontend service excuses itself, so this only drops an UNCONDITIONAL one (see utils/pipeline
385
+ // + the backend gate). Also hide `'recurring'`-only pipelines (a one-off task start of one is
386
+ // refused at run start) and every pipeline whose purpose doesn't match the chosen task type (a doc
387
+ // task authors a doc, a review task reviews a PR, a `bug` task ships code and may reach for a
388
+ // bugfix preset, and a `feature` gets that set minus the bugfix ones, which have no defect report
389
+ // to investigate). `blockLevel: 'task'` is passed literally because this modal only ever creates a
390
+ // task leaf, which also drops the three planning presets the backend would refuse.
390
391
  // Re-filters as the chosen task type changes.
391
392
  const selectablePipelines = computed(() =>
392
393
  pipelines.pipelines.filter((p) =>
@@ -416,7 +417,15 @@ const DEFAULT_PIPELINE_FOR_TYPE: Partial<Record<TaskTypeChoice, string>> = {
416
417
  * deliberately does not name), the build rung this interface mode defaults to. Basic mode gets the
417
418
  * fixed Standard build, advanced the Adaptive one; `defaultBuildPipelineId` owns that rule so the
418
419
  * create form and the task card's plain "Start" cannot disagree about it. Empty when the resolved
419
- * preset is not in this workspace's library (an older seed, or a retired rung).
420
+ * preset is not in this workspace's library (an older seed, or a retired rung) or when the picker
421
+ * would not OFFER it for `type`.
422
+ *
423
+ * That second check is why it re-runs the picker's own predicate rather than scanning the whole
424
+ * library: a workspace whose declared interactive default is a bugfix preset would otherwise open a
425
+ * `feature` form on a selection with no matching row, and create the task pinned to a pipeline the
426
+ * same screen says is not allowed for its type. Parameterised by `type` rather than reading
427
+ * `selectablePipelines`, so the answer cannot depend on whether the watcher fires before or after
428
+ * the ref it filters on has settled.
420
429
  *
421
430
  * ONE definition, read by both the type watcher and the open-reset. They used to compute it
422
431
  * separately, the reset consulting `DEFAULT_PIPELINE_FOR_TYPE` alone and falling to `''` for every
@@ -433,7 +442,10 @@ function defaultPipelineIdFor(type: TaskTypeChoice): string {
433
442
  // and the task card's plain Start still cannot disagree (see `declaredDefaultId`).
434
443
  pipelines.declaredDefaultId('interactive') ??
435
444
  defaultBuildPipelineId(uiMode.isAdvanced)
436
- return pipelines.pipelines.some((p) => p.id === preset) ? preset : ''
445
+ const resolved = pipelines.pipelines.find((p) => p.id === preset)
446
+ const offered =
447
+ !!resolved && pipelineAllowedForManualStart(resolved, frame.value, board.blocks, type, 'task')
448
+ return offered ? preset : ''
437
449
  }
438
450
 
439
451
  watch(taskType, (next) => {
@@ -131,7 +131,8 @@ function defaultRecurrence(): Recurrence {
131
131
  }
132
132
  }
133
133
 
134
- // Hide UI-testing pipelines when the frame has no UI to exercise they'd be refused at run start.
134
+ // Hide a pipeline whose UI-testing step would REACH a frame with no UI (one scoped to a frontend
135
+ // service excuses itself) — it would be refused at run start.
135
136
  // Also hide `'one-off'`-only pipelines: attaching one to a schedule is refused server-side.
136
137
  const selectablePipelines = computed(() =>
137
138
  pipelines.pipelines.filter((p) => pipelineAllowedForSchedule(p, frame.value, board.blocks)),
@@ -1,6 +1,6 @@
1
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
2
+ // The pipeline-PURPOSE control: what the pipeline being built exists to do (build, fix a bug,
3
+ // document, review, research, plan). It sits beside `AgentTierSelect` at the top of the agent palette
4
4
  // because the two are the same kind of dial (each narrows the catalog below to what is worth
5
5
  // offering), and a filter the user cannot see is a catalog that looks incomplete for no reason.
6
6
  //
@@ -25,6 +25,7 @@ const emit = defineEmits<{ (e: 'update:purpose', purpose: PipelinePurpose): void
25
25
  // typed-message-keys check covers them, the same shape `AgentTierSelect` uses for its tiers.
26
26
  const PURPOSE_LABELS = computed<Record<PipelinePurpose, string>>(() => ({
27
27
  build: t('pipeline.builder.purposeOption.build'),
28
+ bugfix: t('pipeline.builder.purposeOption.bugfix'),
28
29
  document: t('pipeline.builder.purposeOption.document'),
29
30
  review: t('pipeline.builder.purposeOption.review'),
30
31
  research: t('pipeline.builder.purposeOption.research'),
@@ -148,12 +148,21 @@ const selectedPipeline = computed(() =>
148
148
  // drops the planning presets the engine would refuse. The task-type narrowing already excludes them
149
149
  // for `feature`/`bug`, but NOT for a `spike` / `ralph` / deployment-custom type — and a planning
150
150
  // preset settable as a task's DEFAULT pipeline is a 409 on every later Start.
151
+ // The pipeline this task is ALREADY pinned to is kept in the list even when the narrowing would
152
+ // drop it, the same rule `showOverrideField` states for a hidden override an entity already
153
+ // carries: every one of these gates has been tightened at least once, and a task pinned under the
154
+ // looser one is a legal pre-existing state (a `feature` on a bugfix preset, until the purpose
155
+ // classifier split). Without it the badge below names a pipeline the dropdown does not contain,
156
+ // and the first change to anything else is a one-way door out of a selection the panel is still
157
+ // showing. It is only ever the CURRENT value, so nothing new can be pinned through it.
151
158
  const taskFrame = computed(() => board.serviceOf(props.block))
152
- const selectablePipelines = computed(() =>
153
- pipelines.pipelines.filter((p) =>
159
+ const selectablePipelines = computed(() => {
160
+ const offered = pipelines.pipelines.filter((p) =>
154
161
  pipelineAllowedForManualStart(p, taskFrame.value, board.blocks, props.block.taskType, 'task'),
155
- ),
156
- )
162
+ )
163
+ const pinned = selectedPipeline.value
164
+ return pinned && !offered.some((p) => p.id === pinned.id) ? [pinned, ...offered] : offered
165
+ })
157
166
  function setPipeline(id: string) {
158
167
  board.updateBlock(props.block.id, { pipelineId: id })
159
168
  }
@@ -75,10 +75,10 @@ export const usePipelinesStore = defineStore('pipelines', () => {
75
75
  /** Organizational labels for the pipeline being assembled/edited. */
76
76
  const draftLabels = ref<string[]>([])
77
77
  /**
78
- * The use-case classifier of the pipeline being assembled/edited (`build` / `document` /
79
- * `review` / `research` / `planning`). Drives which task pickers offer the saved pipeline, which
80
- * agent kinds the builder palette shows (a non-`build` purpose hides the Implementation/Testing
81
- * kinds) and which saved pipelines the library lists.
78
+ * The use-case classifier of the pipeline being assembled/edited (`build` / `bugfix` /
79
+ * `document` / `review` / `research` / `planning`). Drives which task pickers offer the saved
80
+ * pipeline, which agent kinds the builder palette shows (a purpose that ships no code hides the
81
+ * Implementation/Testing kinds) and which saved pipelines the library lists.
82
82
  *
83
83
  * Never null: `Pipeline.purpose` is mandatory, so a draft is classified from the moment it
84
84
  * exists. `build` is the default because it is what an unclassified pipeline has always behaved
@@ -6,13 +6,17 @@ import {
6
6
  PIPELINE_PURPOSES,
7
7
  pipelineAllowedForBlockLevel,
8
8
  pipelineAllowedForTaskType,
9
+ pipelineMatchesPurpose,
10
+ pipelineRunsVisualStep,
9
11
  purposeAllowsAgentCategory,
10
12
  purposeSuggestsAgentCategory,
11
13
  purposeSuggestsAgentKind,
14
+ resolveRunServiceScope,
12
15
  } from '@cat-factory/contracts'
13
16
  import type { PipelinePurpose } from '@cat-factory/contracts'
14
17
  import type { Block, Pipeline } from '~/types/domain'
15
18
  import {
19
+ pipelineAllowedForFrame,
16
20
  pipelineAllowedForManualStart,
17
21
  pipelineAllowedForSchedule,
18
22
  pipelineConditionalCount,
@@ -136,6 +140,22 @@ describe('pipelineAllowedForTaskType', () => {
136
140
  }
137
141
  })
138
142
 
143
+ it('offers a bugfix preset to a bug task and withholds it from a feature', () => {
144
+ // "Triage & fix bug" investigates a defect REPORT, triages it with a person and writes a
145
+ // failing reproduction test before anything is fixed. A feature has no report to investigate
146
+ // and nothing red to turn green, so the front half of the run has no input at all. The bug
147
+ // task still gets the whole build ladder beside it — this narrows one preset, not the type.
148
+ const bugfix = pipeline({ purpose: 'bugfix' })
149
+ expect(pipelineAllowedForTaskType(bugfix, 'bug')).toBe(true)
150
+ expect(pipelineAllowedForTaskType(bugfix, 'feature')).toBe(false)
151
+ expect(pipelineAllowedForTaskType(pipeline({ purpose: 'build' }), 'bug')).toBe(true)
152
+ // And it stays out of the task types that demand their own explicit member.
153
+ expect(pipelineAllowedForTaskType(bugfix, 'document')).toBe(false)
154
+ expect(pipelineAllowedForTaskType(bugfix, 'review')).toBe(false)
155
+ // An un-narrowed type (spike, ralph, a deployment's own) is unrestricted as ever.
156
+ expect(pipelineAllowedForTaskType(bugfix, 'spike')).toBe(true)
157
+ })
158
+
139
159
  it('keeps a pipeline whose classifier this build cannot name on a feature / bug task', () => {
140
160
  // The one place this narrowing runs opposite to the document/review one, and it has to. The
141
161
  // value is persisted, so a deployment's own classifier (or one retired since the row was
@@ -189,8 +209,10 @@ describe('pipelineAllowedForBlockLevel (initiative binding)', () => {
189
209
  })
190
210
 
191
211
  describe('purposeAllowsAgentCategory (builder save gate)', () => {
192
- it('a build pipeline, and one whose classifier this build cannot name, may use every category', () => {
193
- for (const purpose of ['build', UNKNOWN_PURPOSE] as const) {
212
+ it('a code-shipping pipeline, and one whose classifier this build cannot name, may use every category', () => {
213
+ // `bugfix` rides with `build` here: the two differ only in the task type they are OFFERED to,
214
+ // and a bug fix designs, implements, tests and merges like any other change.
215
+ for (const purpose of ['build', 'bugfix', UNKNOWN_PURPOSE] as const) {
194
216
  for (const cat of AGENT_CATEGORIES) {
195
217
  expect(purposeAllowsAgentCategory(purpose, cat)).toBe(true)
196
218
  }
@@ -211,8 +233,8 @@ describe('purposeAllowsAgentCategory (builder save gate)', () => {
211
233
  })
212
234
 
213
235
  describe('purposeSuggestsAgentCategory (builder palette filter)', () => {
214
- it('offers the whole catalog to a build pipeline, and to one it cannot name', () => {
215
- for (const purpose of ['build', UNKNOWN_PURPOSE] as const) {
236
+ it('offers the whole catalog to a code-shipping pipeline, and to one it cannot name', () => {
237
+ for (const purpose of ['build', 'bugfix', UNKNOWN_PURPOSE] as const) {
216
238
  for (const cat of AGENT_CATEGORIES) {
217
239
  expect(purposeSuggestsAgentCategory(purpose, cat)).toBe(true)
218
240
  }
@@ -273,6 +295,23 @@ describe('purposeSuggestsAgentKind (what the palette actually filters on)', () =
273
295
  expect(purposeSuggestsAgentCategory('review', 'docs')).toBe(true)
274
296
  })
275
297
 
298
+ it('reads a `build` declaration as covering `bugfix`, but not the other way round', () => {
299
+ // The two purposes are the same WORK, split only so the pickers can withhold a defect-report
300
+ // preset from a feature task. Every kind declaring `build` predates the split, so without
301
+ // this the bugfix palette would open nearly empty — the Bug Investigator included.
302
+ const builder = { category: 'build', purposes: ['build'] } as const
303
+ expect(purposeSuggestsAgentKind('bugfix', builder)).toBe(true)
304
+ expect(purposeSuggestsAgentKind('document', builder)).toBe(false)
305
+ // One-way: naming only `bugfix` claims the defect-report context, not builds in general.
306
+ const bugOnly = { category: 'build', purposes: ['bugfix'] } as const
307
+ expect(purposeSuggestsAgentKind('bugfix', bugOnly)).toBe(true)
308
+ expect(purposeSuggestsAgentKind('build', bugOnly)).toBe(false)
309
+ // ONE relation, not one per surface: the saved-pipeline library reads it through the same
310
+ // helper, so a draft cannot find the build ladder in its palette and not in its library.
311
+ expect(pipelineMatchesPurpose({ purpose: 'build' } as Pipeline, 'bugfix')).toBe(true)
312
+ expect(pipelineMatchesPurpose({ purpose: 'bugfix' } as Pipeline, 'build')).toBe(false)
313
+ })
314
+
276
315
  it('is a declaration, not an exemption from the category', () => {
277
316
  // A kind cannot buy its way back into a purpose its section is not offered to: `docs` is gone
278
317
  // for `planning`, and the palette drops the kind whether or not it named `planning` itself.
@@ -341,6 +380,80 @@ describe('a purpose or category this build does not recognise', () => {
341
380
  })
342
381
  })
343
382
 
383
+ describe('pipelineAllowedForFrame (the visual gate)', () => {
384
+ const serviceFrame = { id: 'blk_svc', level: 'frame', type: 'service' } as Block
385
+ const frontendFrame = { id: 'blk_fe', level: 'frame', type: 'frontend' } as Block
386
+
387
+ // Every build rung ships the CONDITIONAL tester pair: the browser pass declares the frontend
388
+ // scope, the API pass the backend one, so one preset covers both kinds of service.
389
+ const buildRung = pipeline({
390
+ purpose: 'build',
391
+ agentKinds: ['coder', 'tester-api', 'tester-ui', 'merger'],
392
+ stepOptions: [
393
+ null,
394
+ { condition: { serviceScope: 'backend' } },
395
+ { condition: { serviceScope: 'frontend' } },
396
+ null,
397
+ ],
398
+ })
399
+
400
+ it('offers a build rung on a plain backend service — its UI pass is scoped out', () => {
401
+ // The regression: reading "does the pipeline LIST a visual step" hid Standard / Simple /
402
+ // Adaptive / Complex build from the pickers on every non-frontend service, while the engine
403
+ // would have started any of them (it drops the condition-excluded steps before its own gate).
404
+ expect(pipelineAllowedForFrame(buildRung, serviceFrame, [serviceFrame])).toBe(true)
405
+ expect(pipelineAllowedForManualStart(buildRung, serviceFrame, [serviceFrame], 'feature')).toBe(
406
+ true,
407
+ )
408
+ })
409
+
410
+ it('offers it on a frontend frame too, where the UI pass is exactly what runs', () => {
411
+ // Stated as the premise, not just the answer: on this frame the condition ADMITS the browser
412
+ // pass, so the gate is passing on the frame half rather than on the step being scoped out.
413
+ // Without it the case reads identically against the pre-fix implementation, which reached the
414
+ // same `true` down the other branch and asserted nothing about the change.
415
+ expect(pipelineRunsVisualStep(buildRung, resolveRunServiceScope([frontendFrame]))).toBe(true)
416
+ expect(pipelineAllowedForFrame(buildRung, frontendFrame, [frontendFrame])).toBe(true)
417
+ })
418
+
419
+ it('offers everything on a service frame a FRONTEND binds as its backend', () => {
420
+ // The other half of `frameAllowsVisualPipeline`, and the branch with the widest reach: the
421
+ // linked frontend is the UI a change to this service is validated through, so the frame has a
422
+ // UI and even an unconditional visual pipeline may run on it.
423
+ const boundService = { id: 'blk_api', level: 'frame', type: 'service' } as Block
424
+ const binder = {
425
+ id: 'blk_fe',
426
+ level: 'frame',
427
+ type: 'frontend',
428
+ frontendConfig: {
429
+ backendBindings: [
430
+ { envVar: 'API_URL', source: { kind: 'service', serviceBlockId: 'blk_api' } },
431
+ ],
432
+ },
433
+ } as Block
434
+ const blocks = [boundService, binder]
435
+ const visual = pipeline({ purpose: 'build', agentKinds: ['coder', 'visual-confirmation'] })
436
+ expect(pipelineAllowedForFrame(buildRung, boundService, blocks)).toBe(true)
437
+ expect(pipelineAllowedForFrame(visual, boundService, blocks)).toBe(true)
438
+ // ...and it is the BINDING that does it, not the mere presence of a frontend on the board.
439
+ expect(pipelineAllowedForFrame(visual, boundService, [boundService, frontendFrame])).toBe(false)
440
+ })
441
+
442
+ it('still hides a pipeline whose visual step is UNCONDITIONAL on a frame with no UI', () => {
443
+ // The gate the conditional pair did not repeal: a `visual-confirmation` step with nothing to
444
+ // scope it out has no app to show a person, and the engine answers a 409.
445
+ const visual = pipeline({ purpose: 'build', agentKinds: ['coder', 'visual-confirmation'] })
446
+ expect(pipelineAllowedForFrame(visual, serviceFrame, [serviceFrame])).toBe(false)
447
+ expect(pipelineAllowedForFrame(visual, frontendFrame, [frontendFrame])).toBe(true)
448
+ })
449
+
450
+ it('hides a conditional visual pipeline when the frame cannot be resolved', () => {
451
+ // No frame ⇒ no scope to judge the condition against, so the step counts and the frame half
452
+ // refuses. The fail-safe direction, and the same answer the engine gives.
453
+ expect(pipelineAllowedForFrame(buildRung, undefined, [])).toBe(false)
454
+ })
455
+ })
456
+
344
457
  describe('pipelineAllowedForManualStart composes the task-type gate', () => {
345
458
  const noFrame = undefined
346
459
  const blocks: Block[] = []
@@ -3,6 +3,8 @@ import {
3
3
  pipelineAllowedForBlockLevel,
4
4
  pipelineAllowedForTaskType,
5
5
  pipelineHasVisualStep,
6
+ pipelineRunsVisualStep,
7
+ resolveRunServiceScope,
6
8
  } from '@cat-factory/contracts'
7
9
  import type { AgentKind, Block, BlockLevel, Pipeline } from '~/types/domain'
8
10
 
@@ -102,16 +104,40 @@ export { pipelineAllowedForBlockLevel, pipelineAllowedForTaskType }
102
104
  // the gate.
103
105
 
104
106
  /**
105
- * Whether `pipeline` may run on a task under `frame`. A non-visual pipeline is always allowed;
106
- * a visual one only when the frame has a UI (see {@link frameAllowsVisualPipeline}). `blocks` is
107
- * the board's block list, used to find frontend→service links.
107
+ * Whether `pipeline` may run on a task under `frame`. Allowed when the frame has a UI (see
108
+ * {@link frameAllowsVisualPipeline}), and otherwise only when no visual step would REACH a
109
+ * dispatch there ({@link pipelineRunsVisualStep}). `blocks` is the board's block list, used to
110
+ * find frontend→service links.
111
+ *
112
+ * The two halves mirror run admission, which filters the chain through the run conditions and
113
+ * then gates the survivors on the frame. Asking only "does it list a visual step" hid every
114
+ * build rung from every picker on every non-frontend service the moment the ladder adopted the
115
+ * conditional tester pair: each rung lists a `tester-ui` scoped to `frontend`, which a backend
116
+ * task skips.
117
+ *
118
+ * The coarse {@link pipelineHasVisualStep} runs FIRST and is the only work an ordinary pipeline
119
+ * costs: this is called once per row of every pipeline picker, recomputed on each task-type change
120
+ * and each live board push, and the other two halves each walk the whole block list or allocate a
121
+ * scope. A pipeline listing no visual step at all can reach neither answer, so nothing is decided
122
+ * before it that it could decide differently.
123
+ *
124
+ * The scope is derived from the FRAME alone, where the engine also folds in the task's involved
125
+ * services. That cannot disagree here: an involved service is always a non-frontend (only a
126
+ * `service` frame survives `validInvolvedServiceFrames`), so it can only ever re-confirm the
127
+ * `backend` half this frame already sets — and on a frontend frame the gate passes on the frame
128
+ * itself. `runServiceScope.test.ts` pins that filter as the invariant this omission rests on, so
129
+ * widening it fails there rather than here, where the symptom would be a rung this offers and the
130
+ * run-start gate then refuses. An unresolved frame yields an empty scope, which admits every
131
+ * condition and is then refused by the frame half, exactly as the engine refuses it.
108
132
  */
109
133
  export function pipelineAllowedForFrame(
110
134
  pipeline: Pipeline,
111
135
  frame: Block | undefined,
112
136
  blocks: readonly Block[],
113
137
  ): boolean {
114
- return !pipelineHasVisualStep(pipeline) || frameAllowsVisualPipeline(frame, blocks)
138
+ if (!pipelineHasVisualStep(pipeline)) return true
139
+ if (frameAllowsVisualPipeline(frame, blocks)) return true
140
+ return !pipelineRunsVisualStep(pipeline, resolveRunServiceScope(frame ? [frame] : []))
115
141
  }
116
142
 
117
143
  // Launch-availability filters, the surface counterpart to the backend's start-origin gate (a
@@ -126,7 +152,8 @@ export function pipelineAllowedForFrame(
126
152
  * - `'recurring'`-only pipelines the backend would refuse;
127
153
  * - visual pipelines on a frame with no UI;
128
154
  * - pipelines whose `purpose` doesn't fit the given `taskType` (a `document` task offers only
129
- * document pipelines; a `feature`/`bug` task only build + research ones);
155
+ * document pipelines; a `bug` task build + bugfix + research ones, and a `feature` the same
156
+ * minus the bugfix presets, which have no defect report to work from);
130
157
  * - pipelines whose `purpose` doesn't fit the given `blockLevel` (planning pipelines run only on
131
158
  * an initiative block, and an initiative block runs only those).
132
159
  *
@@ -82,6 +82,22 @@ describe('narrowPipelineLibrary', () => {
82
82
  expect(shown.archivedInScope).toBe(1)
83
83
  })
84
84
 
85
+ it('lists a build pipeline in a BUGFIX library, but not the reverse', () => {
86
+ // The one relation `classifierSuits` admits, read here through the same helper the palette
87
+ // reads: a bugfix draft is doing build work, so the whole ladder belongs in front of it, and
88
+ // exact matching would open that library holding the two bugfix presets alone, the same
89
+ // near-empty catalog the palette rule exists to prevent. A build draft is NOT doing defect
90
+ // work, so the bugfix rows stay behind the dial, counted and one click away.
91
+ const lib = [...LIBRARY, row('bugfix-a', 'bugfix')]
92
+ expect(narrowPipelineLibrary(lib, { purpose: 'bugfix' }).offered.map((p) => p.id)).toEqual([
93
+ 'build-a',
94
+ 'bugfix-a',
95
+ ])
96
+ const atBuild = narrowPipelineLibrary(lib, { purpose: 'build' })
97
+ expect(atBuild.offered.map((p) => p.id)).toEqual(['build-a'])
98
+ expect(atBuild.hiddenByPurpose).toBe(4)
99
+ })
100
+
85
101
  it('narrows by neither purpose this build cannot name', () => {
86
102
  // Same disposition as the palette's: an unrecognised value is one this build has nothing to
87
103
  // narrow BY and nothing to narrow it AGAINST, never a reason to guess a current member. A
@@ -4391,6 +4391,7 @@
4391
4391
  "purposeUnrecognized": "Nicht erkannt ({purpose})",
4392
4392
  "purposeOption": {
4393
4393
  "build": "Entwicklung",
4394
+ "bugfix": "Fehlerbehebung",
4394
4395
  "document": "Dokumentation",
4395
4396
  "review": "Review",
4396
4397
  "research": "Recherche",
@@ -4459,7 +4460,7 @@
4459
4460
  "skillNoneAvailable": "Keine Skills verfügbar. Verknüpfe eine Skill-Quelle in den Kontoeinstellungen.",
4460
4461
  "skillMissing": "Dieser Skill ist nicht mehr im Katalog; wähle einen anderen.",
4461
4462
  "skillNeedsPick": "Ein Skill-Schritt braucht einen ausgewählten Skill, bevor du speichern kannst.",
4462
- "purposeStepsConflict": "Dieser Zweck schreibt keinen Code und führt keine Tests aus, doch die Pipeline enthält noch Implementierungs- oder Testschritte. Entfernen Sie diese oder setzen Sie den Zweck auf Entwicklung.",
4463
+ "purposeStepsConflict": "Dieser Zweck schreibt keinen Code und führt keine Tests aus, doch die Pipeline enthält noch Implementierungs- oder Testschritte. Entfernen Sie diese oder wählen Sie einen Zweck, der Code liefert (Entwicklung oder Fehlerbehebung).",
4463
4464
  "promptEditTooltip": "Systemprompt dieses Agenten bearbeiten",
4464
4465
  "promptEditedTooltip": "Der Systemprompt dieses Agenten ist für diesen Arbeitsbereich bearbeitet",
4465
4466
  "consensusGroups": "Konsensgruppen",
@@ -4998,6 +4998,7 @@
4998
4998
  },
4999
4999
  "purposeOption": {
5000
5000
  "build": "Build",
5001
+ "bugfix": "Bug fix",
5001
5002
  "document": "Documentation",
5002
5003
  "review": "Review",
5003
5004
  "research": "Research",
@@ -5069,7 +5070,7 @@
5069
5070
  "skillNoneAvailable": "No skills available. Link a skill source in account settings.",
5070
5071
  "skillMissing": "This skill is no longer in the catalog; pick another.",
5071
5072
  "skillNeedsPick": "A Skill step needs a skill selected before you can save.",
5072
- "purposeStepsConflict": "This purpose writes no code and runs no tests, but the pipeline still has implementation or testing steps. Remove them or switch the purpose to Build.",
5073
+ "purposeStepsConflict": "This purpose writes no code and runs no tests, but the pipeline still has implementation or testing steps. Remove them, or switch to a purpose that ships code (Build or Bug fix).",
5073
5074
  "promptEditTooltip": "Edit this agent's system prompt",
5074
5075
  "promptEditedTooltip": "This agent's system prompt is edited for this workspace",
5075
5076
  "consensusGroups": "Consensus groups",
@@ -4834,6 +4834,7 @@
4834
4834
  "purposeUnrecognized": "No reconocido ({purpose})",
4835
4835
  "purposeOption": {
4836
4836
  "build": "Desarrollo",
4837
+ "bugfix": "Corrección de errores",
4837
4838
  "document": "Documentación",
4838
4839
  "review": "Revisión",
4839
4840
  "research": "Investigación",
@@ -4902,7 +4903,7 @@
4902
4903
  "skillNoneAvailable": "No hay habilidades disponibles. Vincula una fuente de habilidades en la configuración de la cuenta.",
4903
4904
  "skillMissing": "Esta habilidad ya no está en el catálogo; elige otra.",
4904
4905
  "skillNeedsPick": "Un paso Skill necesita una habilidad seleccionada antes de poder guardar.",
4905
- "purposeStepsConflict": "Este propósito no escribe código ni ejecuta pruebas, pero la canalización todavía tiene pasos de implementación o de prueba. Quítalos o cambia el propósito a Desarrollo.",
4906
+ "purposeStepsConflict": "Este propósito no escribe código ni ejecuta pruebas, pero la canalización todavía tiene pasos de implementación o de prueba. Quítalos o cambia a un propósito que entregue código (Desarrollo o Corrección de errores).",
4906
4907
  "promptEditTooltip": "Editar el prompt del sistema de este agente",
4907
4908
  "promptEditedTooltip": "El prompt del sistema de este agente está editado para este espacio de trabajo",
4908
4909
  "consensusGroups": "Grupos de consenso",
@@ -4834,6 +4834,7 @@
4834
4834
  "purposeUnrecognized": "Non reconnu ({purpose})",
4835
4835
  "purposeOption": {
4836
4836
  "build": "Développement",
4837
+ "bugfix": "Correction de bug",
4837
4838
  "document": "Documentation",
4838
4839
  "review": "Revue",
4839
4840
  "research": "Recherche",
@@ -4902,7 +4903,7 @@
4902
4903
  "skillNoneAvailable": "Aucune compétence disponible. Reliez une source de compétences dans les paramètres du compte.",
4903
4904
  "skillMissing": "Cette compétence n'est plus dans le catalogue ; choisissez-en une autre.",
4904
4905
  "skillNeedsPick": "Une étape Skill a besoin d'une compétence sélectionnée avant de pouvoir enregistrer.",
4905
- "purposeStepsConflict": "Cet objectif n'écrit aucun code et n'exécute aucun test, mais le pipeline contient encore des étapes d'implémentation ou de test. Supprimez-les ou définissez l'objectif sur Développement.",
4906
+ "purposeStepsConflict": "Cet objectif n'écrit aucun code et n'exécute aucun test, mais le pipeline contient encore des étapes d'implémentation ou de test. Supprimez-les ou choisissez un objectif qui livre du code (Développement ou Correction de bug).",
4906
4907
  "promptEditTooltip": "Modifier l'invite système de cet agent",
4907
4908
  "promptEditedTooltip": "L'invite système de cet agent est modifiée pour cet espace de travail",
4908
4909
  "consensusGroups": "Groupes de consensus",
@@ -4834,6 +4834,7 @@
4834
4834
  "purposeUnrecognized": "לא מזוהה ({purpose})",
4835
4835
  "purposeOption": {
4836
4836
  "build": "פיתוח",
4837
+ "bugfix": "תיקון באג",
4837
4838
  "document": "תיעוד",
4838
4839
  "review": "סקירה",
4839
4840
  "research": "מחקר",
@@ -4902,7 +4903,7 @@
4902
4903
  "skillNoneAvailable": "אין כישורים זמינים. קישר מקור כישורים בהגדרות החשבון.",
4903
4904
  "skillMissing": "כישור זה כבר אינו בקטלוג; בחר אחר.",
4904
4905
  "skillNeedsPick": "שלב Skill דורש כישור נבחר לפני שתוכל לשמור.",
4905
- "purposeStepsConflict": "מטרה זו אינה כותבת קוד ואינה מריצה בדיקות, אך הצינור עדיין כולל שלבי פיתוח או בדיקה. הסירו אותם או שנו את המטרה לפיתוח.",
4906
+ "purposeStepsConflict": "מטרה זו אינה כותבת קוד ואינה מריצה בדיקות, אך הצינור עדיין כולל שלבי פיתוח או בדיקה. הסירו אותם או בחרו מטרה שמספקת קוד (פיתוח או תיקון באג).",
4906
4907
  "promptEditTooltip": "עריכת הנחיית המערכת של הסוכן הזה",
4907
4908
  "promptEditedTooltip": "הנחיית המערכת של הסוכן הזה נערכה עבור מרחב העבודה הזה",
4908
4909
  "consensusGroups": "קבוצות קונצנזוס",
@@ -4391,6 +4391,7 @@
4391
4391
  "purposeUnrecognized": "Non riconosciuto ({purpose})",
4392
4392
  "purposeOption": {
4393
4393
  "build": "Sviluppo",
4394
+ "bugfix": "Correzione di bug",
4394
4395
  "document": "Documentazione",
4395
4396
  "review": "Revisione",
4396
4397
  "research": "Ricerca",
@@ -4459,7 +4460,7 @@
4459
4460
  "skillNoneAvailable": "Nessuna competenza disponibile. Collega una fonte di competenze nelle impostazioni dell'account.",
4460
4461
  "skillMissing": "Questa competenza non è più nel catalogo; scegline un'altra.",
4461
4462
  "skillNeedsPick": "Un passo Skill richiede una competenza selezionata prima di poter salvare.",
4462
- "purposeStepsConflict": "Questo scopo non scrive codice né esegue test, ma la pipeline contiene ancora passaggi di implementazione o di test. Rimuovili o imposta lo scopo su Sviluppo.",
4463
+ "purposeStepsConflict": "Questo scopo non scrive codice né esegue test, ma la pipeline contiene ancora passaggi di implementazione o di test. Rimuovili oppure scegli uno scopo che rilascia codice (Sviluppo o Correzione di bug).",
4463
4464
  "promptEditTooltip": "Modifica il prompt di sistema di questo agente",
4464
4465
  "promptEditedTooltip": "Il prompt di sistema di questo agente è modificato per questo spazio di lavoro",
4465
4466
  "consensusGroups": "Gruppi di consenso",
@@ -4834,6 +4834,7 @@
4834
4834
  "purposeUnrecognized": "認識できません ({purpose})",
4835
4835
  "purposeOption": {
4836
4836
  "build": "開発",
4837
+ "bugfix": "バグ修正",
4837
4838
  "document": "ドキュメント",
4838
4839
  "review": "レビュー",
4839
4840
  "research": "調査",
@@ -4902,7 +4903,7 @@
4902
4903
  "skillNoneAvailable": "利用できるスキルがありません。アカウント設定でスキルソースをリンクしてください。",
4903
4904
  "skillMissing": "このスキルはカタログに存在しません。別のスキルを選んでください。",
4904
4905
  "skillNeedsPick": "Skill ステップを保存するにはスキルの選択が必要です。",
4905
- "purposeStepsConflict": "この目的はコードを書かず、テストも実行しませんが、パイプラインには実装またはテストのステップが残っています。それらを削除するか、目的を「開発」に切り替えてください。",
4906
+ "purposeStepsConflict": "この目的はコードを書かず、テストも実行しませんが、パイプラインには実装またはテストのステップが残っています。それらを削除するか、コードを提供する目的(「開発」または「バグ修正」)に切り替えてください。",
4906
4907
  "promptEditTooltip": "このエージェントのシステムプロンプトを編集",
4907
4908
  "promptEditedTooltip": "このエージェントのシステムプロンプトは、このワークスペース用に編集されています",
4908
4909
  "consensusGroups": "コンセンサスグループ",
@@ -4834,6 +4834,7 @@
4834
4834
  "purposeUnrecognized": "Nierozpoznany ({purpose})",
4835
4835
  "purposeOption": {
4836
4836
  "build": "Programowanie",
4837
+ "bugfix": "Poprawka błędu",
4837
4838
  "document": "Dokumentacja",
4838
4839
  "review": "Przegląd",
4839
4840
  "research": "Badania",
@@ -4902,7 +4903,7 @@
4902
4903
  "skillNoneAvailable": "Brak dostępnych umiejętności. Połącz źródło umiejętności w ustawieniach konta.",
4903
4904
  "skillMissing": "Tej umiejętności nie ma już w katalogu; wybierz inną.",
4904
4905
  "skillNeedsPick": "Krok Skill wymaga wybranej umiejętności, zanim będzie można zapisać.",
4905
- "purposeStepsConflict": "To przeznaczenie nie tworzy kodu ani nie uruchamia testów, ale potok nadal zawiera kroki programowania lub testowania. Usuń je lub zmień przeznaczenie na Programowanie.",
4906
+ "purposeStepsConflict": "To przeznaczenie nie tworzy kodu ani nie uruchamia testów, ale potok nadal zawiera kroki programowania lub testowania. Usuń je lub zmień przeznaczenie na takie, które dostarcza kod (Programowanie lub Poprawka błędu).",
4906
4907
  "promptEditTooltip": "Edytuj prompt systemowy tego agenta",
4907
4908
  "promptEditedTooltip": "Prompt systemowy tego agenta jest zmieniony dla tego obszaru roboczego",
4908
4909
  "consensusGroups": "Grupy konsensusu",
@@ -4834,6 +4834,7 @@
4834
4834
  "purposeUnrecognized": "Tanınmayan ({purpose})",
4835
4835
  "purposeOption": {
4836
4836
  "build": "Geliştirme",
4837
+ "bugfix": "Hata düzeltme",
4837
4838
  "document": "Dokümantasyon",
4838
4839
  "review": "İnceleme",
4839
4840
  "research": "Araştırma",
@@ -4902,7 +4903,7 @@
4902
4903
  "skillNoneAvailable": "Kullanılabilir beceri yok. Hesap ayarlarında bir beceri kaynağı bağlayın.",
4903
4904
  "skillMissing": "Bu beceri artık katalogda yok; başka birini seçin.",
4904
4905
  "skillNeedsPick": "Bir Skill adımı, kaydedebilmeniz için seçilmiş bir beceri gerektirir.",
4905
- "purposeStepsConflict": "Bu amaç kod yazmaz ve test çalıştırmaz, ancak işlem hattında hâlâ uygulama veya test adımları var. Bunları kaldırın veya amacı Geliştirme olarak değiştirin.",
4906
+ "purposeStepsConflict": "Bu amaç kod yazmaz ve test çalıştırmaz, ancak işlem hattında hâlâ uygulama veya test adımları var. Bunları kaldırın veya kod teslim eden bir amaca geçin (Geliştirme ya da Hata düzeltme).",
4906
4907
  "promptEditTooltip": "Bu ajanın sistem istemini düzenle",
4907
4908
  "promptEditedTooltip": "Bu ajanın sistem istemi bu çalışma alanı için düzenlenmiş",
4908
4909
  "consensusGroups": "Uzlaşı grupları",
@@ -4834,6 +4834,7 @@
4834
4834
  "purposeUnrecognized": "Нерозпізнано ({purpose})",
4835
4835
  "purposeOption": {
4836
4836
  "build": "Розробка",
4837
+ "bugfix": "Виправлення помилки",
4837
4838
  "document": "Документація",
4838
4839
  "review": "Рецензування",
4839
4840
  "research": "Дослідження",
@@ -4902,7 +4903,7 @@
4902
4903
  "skillNoneAvailable": "Немає доступних навичок. Пов'яжіть джерело навичок у налаштуваннях облікового запису.",
4903
4904
  "skillMissing": "Цієї навички більше немає в каталозі; виберіть іншу.",
4904
4905
  "skillNeedsPick": "Крок Skill потребує вибраної навички, перш ніж ви зможете зберегти.",
4905
- "purposeStepsConflict": "Це призначення не пише код і не запускає тести, але конвеєр досі містить кроки програмування або тестування. Видаліть їх або змініть призначення на Розробку.",
4906
+ "purposeStepsConflict": "Це призначення не пише код і не запускає тести, але конвеєр досі містить кроки програмування або тестування. Видаліть їх або змініть призначення на таке, що постачає код (Розробка або Виправлення помилки).",
4906
4907
  "promptEditTooltip": "Редагувати системний промпт цього агента",
4907
4908
  "promptEditedTooltip": "Системний промпт цього агента змінено для цього робочого простору",
4908
4909
  "consensusGroups": "Групи консенсусу",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.270.2",
3
+ "version": "0.270.3",
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.41",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.308.1"
43
+ "@cat-factory/contracts": "0.309.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",