@cat-factory/app 0.257.0 → 0.258.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.
Files changed (44) hide show
  1. package/app/components/binaryOutput/BinaryOutputReport.vue +42 -0
  2. package/app/components/board/AddTaskModal.vue +38 -12
  3. package/app/components/board/RecurringPipelineModal.vue +24 -12
  4. package/app/components/board/nodes/TaskCard.vue +34 -7
  5. package/app/components/panels/InspectorPanel.vue +39 -0
  6. package/app/components/pipeline/BinaryOutputStepPicker.logic.spec.ts +29 -0
  7. package/app/components/pipeline/BinaryOutputStepPicker.logic.ts +23 -0
  8. package/app/components/pipeline/BinaryOutputStepPicker.vue +109 -0
  9. package/app/components/pipeline/PipelineBuilder.vue +44 -0
  10. package/app/components/pipeline/PipelinePreview.vue +20 -1
  11. package/app/components/pipeline/PipelineProgress.vue +13 -0
  12. package/app/composables/api/execution.ts +19 -0
  13. package/app/composables/usePipelineHealth.spec.ts +42 -5
  14. package/app/composables/usePipelineHealth.ts +109 -48
  15. package/app/modular/agent-kinds.ts +5 -0
  16. package/app/stores/environmentWizard/context.ts +0 -2
  17. package/app/stores/environmentWizard/flow.ts +11 -6
  18. package/app/stores/environmentWizard.ts +12 -11
  19. package/app/stores/execution/commands.ts +26 -1
  20. package/app/stores/pipelines/draftActions.ts +2 -0
  21. package/app/stores/pipelines/draftStepConfig.ts +4 -161
  22. package/app/stores/pipelines/draftStepOptions.ts +204 -0
  23. package/app/types/domain.ts +6 -0
  24. package/app/utils/agentPalette.spec.ts +26 -0
  25. package/app/utils/agentPalette.ts +9 -4
  26. package/app/utils/binaryOutput.spec.ts +122 -1
  27. package/app/utils/binaryOutput.ts +101 -0
  28. package/app/utils/catalog.spec.ts +24 -0
  29. package/app/utils/catalog.ts +21 -4
  30. package/app/utils/pipeline.spec.ts +35 -3
  31. package/app/utils/pipeline.ts +54 -3
  32. package/app/utils/pipelineRender.spec.ts +78 -2
  33. package/app/utils/pipelineRender.ts +43 -0
  34. package/i18n/locales/de.json +29 -1
  35. package/i18n/locales/en.json +29 -1
  36. package/i18n/locales/es.json +29 -1
  37. package/i18n/locales/fr.json +29 -1
  38. package/i18n/locales/he.json +29 -1
  39. package/i18n/locales/it.json +29 -1
  40. package/i18n/locales/ja.json +29 -1
  41. package/i18n/locales/pl.json +29 -1
  42. package/i18n/locales/tr.json +29 -1
  43. package/i18n/locales/uk.json +29 -1
  44. package/package.json +2 -2
@@ -11,7 +11,12 @@
11
11
  import { computed } from 'vue'
12
12
  import type { Pipeline } from '~/types/domain'
13
13
  import { agentKindMeta } from '~/utils/catalog'
14
- import { pipelineDisplaySteps, pipelineGateCount } from '~/utils/pipeline'
14
+ import {
15
+ CONDITION_MARKERS,
16
+ pipelineConditionalCount,
17
+ pipelineDisplaySteps,
18
+ pipelineGateCount,
19
+ } from '~/utils/pipeline'
15
20
  import AgentKindIcon from '~/components/pipeline/AgentKindIcon.vue'
16
21
 
17
22
  const props = defineProps<{ pipeline: Pipeline }>()
@@ -19,6 +24,7 @@ const { t } = useI18n()
19
24
 
20
25
  const steps = computed(() => pipelineDisplaySteps(props.pipeline))
21
26
  const gateCount = computed(() => pipelineGateCount(props.pipeline))
27
+ const conditionalCount = computed(() => pipelineConditionalCount(props.pipeline))
22
28
 
23
29
  /** What the agent at this step does — the same catalog prose the palette and step tooltips use. */
24
30
  function stepDescription(kind: string): string {
@@ -50,6 +56,12 @@ function stepDescription(kind: string): string {
50
56
  <UIcon name="i-lucide-shield-check" class="h-3 w-3" />
51
57
  {{ t('pipeline.preview.gateCount', { count: gateCount }, gateCount) }}
52
58
  </span>
59
+ <!-- Conditional steps change what a run of this pipeline actually does from task to task,
60
+ which is exactly what a preview read BEFORE picking has to say out loud. -->
61
+ <span v-if="conditionalCount" class="inline-flex items-center gap-1 text-sky-500">
62
+ <UIcon name="i-lucide-git-branch" class="h-3 w-3" />
63
+ {{ t('pipeline.preview.conditionalCount', { count: conditionalCount }, conditionalCount) }}
64
+ </span>
53
65
  </div>
54
66
 
55
67
  <!-- The ordered steps. The number column doubles as the flow connector (a rule drawn between
@@ -79,6 +91,13 @@ function stepDescription(kind: string): string {
79
91
  class="h-3 w-3 shrink-0 text-amber-400"
80
92
  :title="t('pipeline.preview.gated')"
81
93
  />
94
+ <UIcon
95
+ v-for="c in s.conditions"
96
+ :key="c"
97
+ :name="CONDITION_MARKERS[c].icon"
98
+ class="h-3 w-3 shrink-0 text-sky-400"
99
+ :title="t(CONDITION_MARKERS[c].key)"
100
+ />
82
101
  </div>
83
102
  <!-- Clamped: the catalog prose runs long for some kinds, and <AgentKindIcon> already
84
103
  carries the full text in its hover tooltip. -->
@@ -12,6 +12,7 @@ import {
12
12
  containerPhaseLabel,
13
13
  dedicatedParkView,
14
14
  REDIRECT_PARK_PRESENTATION,
15
+ stepSkipReasonKey,
15
16
  } from '~/utils/pipelineRender'
16
17
  import { prReviewPhase } from '~/utils/prReviewProgress'
17
18
  import StepMetricsBar from '~/components/observability/StepMetricsBar.vue'
@@ -559,6 +560,18 @@ const ITEM_ICON: Record<string, string> = {
559
560
  {{ t('pipeline.progress.clickToRead') }}
560
561
  </p>
561
562
 
563
+ <!-- Why a skipped step did not run. A skipped step finishes `done` with no output, so
564
+ without this line it is indistinguishable from one that ran and said nothing —
565
+ which reads as a tester that silently did its job. -->
566
+ <p
567
+ v-if="stepSkipReasonKey(s)"
568
+ class="mt-2 flex items-center gap-1 text-[11px] text-slate-500"
569
+ data-testid="step-skip-reason"
570
+ >
571
+ <UIcon name="i-lucide-skip-forward" class="h-3 w-3 shrink-0" />
572
+ {{ t(stepSkipReasonKey(s)!) }}
573
+ </p>
574
+
562
575
  <!-- Conditionally-run companion (today the Tester's fixer): a distinct
563
576
  sub-node marked possible / running / completed / skipped. -->
564
577
  <div
@@ -15,6 +15,7 @@ import {
15
15
  resolveStepExceededContract,
16
16
  restartExecutionContract,
17
17
  resumeSpendContract,
18
+ startAgentKindExecutionContract,
18
19
  startExecutionContract,
19
20
  } from '@cat-factory/contracts'
20
21
  import type { RequestStepChangesInput, RunMode } from '@cat-factory/contracts'
@@ -41,6 +42,24 @@ export function executionApi({ send, sendWith, ws, pwHeaders }: ApiContext) {
41
42
  body,
42
43
  }),
43
44
 
45
+ /**
46
+ * Start ONE agent kind against a block — a run with no pipeline behind it (the service
47
+ * frame's "Map service" action, the environment wizard's deep analysis). Gated on the
48
+ * personal password exactly as a pipeline start is: the kind leases a personal subscription
49
+ * the same way a pipeline step does.
50
+ */
51
+ startAgentKindExecution: (
52
+ workspaceId: string,
53
+ blockId: string,
54
+ agentKind: string,
55
+ password?: string,
56
+ ) =>
57
+ sendWith(pwHeaders(password), startAgentKindExecutionContract, {
58
+ pathPrefix: ws(workspaceId),
59
+ pathParams: { blockId },
60
+ body: { agentKind },
61
+ }),
62
+
44
63
  cancelExecution: (workspaceId: string, blockId: string) =>
45
64
  send(cancelExecutionContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
46
65
 
@@ -7,7 +7,7 @@ import { usePipelineHealth } from '~/composables/usePipelineHealth'
7
7
  /**
8
8
  * Guards the startup pipeline-health advisory against the failure that bit the first cut: a
9
9
  * legitimate built-in agent kind missing from the frontend catalog made `isKnownAgentKind`
10
- * return false, so a stock seeded pipeline (`pl_tech_debt`, which uses `analysis` + `tracker`)
10
+ * return false, so a stock seeded pipeline (one using `analysis` + `tracker`)
11
11
  * was reported "invalid" in every workspace with a Reseed action that could never fix it.
12
12
  *
13
13
  * The kind lists below mirror the canonical built-ins in
@@ -94,8 +94,8 @@ describe('isKnownAgentKind', () => {
94
94
  })
95
95
 
96
96
  describe('usePipelineHealth', () => {
97
- it('does not flag the stock tech-debt built-in (analysis + tracker) as invalid', () => {
98
- const techDebt = builtin(
97
+ it('does not flag an audit pipeline (analysis + tracker) as invalid', () => {
98
+ const audit = builtin(
99
99
  [
100
100
  'analysis',
101
101
  'tracker',
@@ -107,9 +107,9 @@ describe('usePipelineHealth', () => {
107
107
  'ci',
108
108
  'merger',
109
109
  ],
110
- { id: 'pl_tech_debt', name: 'Tech debt' },
110
+ { id: 'pl_audit', name: 'Audit and fix' },
111
111
  )
112
- const { hasIssues, invalid, outdated } = scan([techDebt])
112
+ const { hasIssues, invalid, outdated } = scan([audit])
113
113
  expect(hasIssues.value).toBe(false)
114
114
  expect(invalid.value).toHaveLength(0)
115
115
  expect(outdated.value).toHaveLength(0)
@@ -164,6 +164,43 @@ describe('usePipelineHealth', () => {
164
164
  expect(invalid.value[0]!.problems.some((p) => p.type === 'shape')).toBe(true)
165
165
  })
166
166
 
167
+ // The RUN CONDITION is the second skip axis, and the advisory has to mirror it for the same
168
+ // reason it mirrors the estimate gate: a rule the engine enforces at save that this scan calls
169
+ // healthy leaves the author to discover it as a 422.
170
+ it('accepts a run condition on a kind the shared gatable set allows', () => {
171
+ const conditional = builtin(['coder', 'reviewer', 'tester-ui'], {
172
+ stepOptions: [null, null, { condition: { serviceScope: 'frontend' } }],
173
+ })
174
+ expect(scan([conditional]).hasIssues.value).toBe(false)
175
+ })
176
+
177
+ it('flags a run condition on a kind the run structurally needs (merger)', () => {
178
+ const conditionalMerger = builtin(['coder', 'merger'], {
179
+ stepOptions: [null, { condition: { serviceScope: 'frontend' } }],
180
+ })
181
+ const { invalid } = scan([conditionalMerger])
182
+ expect(invalid.value).toHaveLength(1)
183
+ expect(invalid.value[0]!.problems.some((p) => p.type === 'shape')).toBe(true)
184
+ })
185
+
186
+ it('flags a step carrying BOTH a human approval gate and a run condition (shape)', () => {
187
+ const both = builtin(['coder', 'tester-ui'], {
188
+ gates: [false, true],
189
+ stepOptions: [null, { condition: { serviceScope: 'frontend' } }],
190
+ })
191
+ const { invalid } = scan([both])
192
+ expect(invalid.value).toHaveLength(1)
193
+ expect(invalid.value[0]!.problems.some((p) => p.type === 'shape')).toBe(true)
194
+ })
195
+
196
+ it('accepts a run condition BESIDE an estimate gate: the axes compose', () => {
197
+ const both = builtin(['task-estimator', 'coder', 'tester-ui'], {
198
+ gating: [null, null, { enabled: true, minComplexity: 0.4, onMissingEstimate: 'run' }],
199
+ stepOptions: [null, null, { condition: { serviceScope: 'frontend' } }],
200
+ })
201
+ expect(scan([both]).hasIssues.value).toBe(false)
202
+ })
203
+
167
204
  it('flags a step carrying BOTH a human approval gate and an estimate gate (shape)', () => {
168
205
  const both = builtin(['task-estimator', 'architect'], {
169
206
  gates: [false, true],
@@ -80,24 +80,11 @@ function companionTargets(companion: string): string[] {
80
80
  const isEnabledAt = (p: Pipeline, i: number) => p.enabled?.[i] !== false
81
81
 
82
82
  /**
83
- * Client-side mirror of the backend `validatePipelineShape` (companion adjacency + estimate
84
- * gating, over the ENABLED subset), collecting the first problem instead of throwing. Returns a
85
- * human message, or null when the shape is valid. Kept in step with
86
- * `backend/packages/orchestration/src/modules/pipelines/pipelineShape.ts`.
87
- *
88
- * A rule here must be keyed off vocabulary SHARED with that module (`@cat-factory/contracts`)
89
- * wherever one exists, never re-stated locally — see the gating note below for what a drifted copy
90
- * costs. Adding a rule to `assertValidGating` without adding it here is the milder half of the same
91
- * drift: a pipeline the engine refuses at save that this advisory calls healthy.
83
+ * Companion adjacency: an enabled companion's nearest preceding ENABLED step must be a producer it
84
+ * can review. Mirrors `assertValidCompanionPlacement`.
92
85
  */
93
- function shapeProblem(p: Pipeline): string | null {
86
+ function companionProblem(p: Pipeline): string | null {
94
87
  const kinds = p.agentKinds
95
- // No enabled steps ⇒ nothing would run.
96
- if (kinds.length === 0 || !kinds.some((_, i) => isEnabledAt(p, i))) {
97
- return 'No enabled steps — the pipeline has nothing to run.'
98
- }
99
- // Companion adjacency: an enabled companion's nearest preceding enabled step must be a
100
- // producer it can review.
101
88
  for (let i = 0; i < kinds.length; i++) {
102
89
  const kind = kinds[i]
103
90
  if (!kind || !isProducerCompanion(kind) || !isEnabledAt(p, i)) continue
@@ -113,44 +100,118 @@ function shapeProblem(p: Pipeline): string | null {
113
100
  return `Companion '${kind}' must run immediately after an enabled step it can review (${targets.join(', ')}).`
114
101
  }
115
102
  }
116
- // Estimate gating: an enabled gated step must be a GATABLE kind, must not also carry a human
117
- // approval gate, must set ≥1 threshold, and must have an enabled task-estimator earlier in the
118
- // chain. Gatability reads the SHARED `BUILTIN_GATABLE_KINDS` rather than a local rule, because
119
- // this advisory auto-opens a modal over the board: a copy of the rule that drifts behind the
120
- // engine's does not merely warn wrongly, it calls a pipeline the product SHIPS invalid and leaves
121
- // the board unusable. A DEPLOYMENT-registered kind can override gatability for itself through the
122
- // agent-kind registry, which the SPA cannot see, so the two are not perfectly symmetric: such a
123
- // kind is reported here and accepted by the engine. That is the safe direction of the asymmetry —
124
- // a dismissible advisory rather than a refused save and the only one available without shipping
125
- // the registry to the browser.
103
+ return null
104
+ }
105
+
106
+ /**
107
+ * The rule both SKIP AXES share: a step that may be absent from a run must be a kind whose result
108
+ * later steps read as context, and must not also carry a human approval gate (a skip may leave a
109
+ * checkpoint un-reached, never cancel one the author asked for). Returns the problem, or null.
110
+ *
111
+ * Shared by {@link gatingProblem} and {@link conditionProblem} rather than written twice, because
112
+ * the reason is identical and only the axis's name differs — which is exactly how the two would
113
+ * drift apart. `axis` supplies the naming, mirroring `assertValidGating` /
114
+ * `assertValidRunConditions`.
115
+ *
116
+ * Gatability reads the SHARED `BUILTIN_GATABLE_KINDS` rather than a local rule, because this
117
+ * advisory auto-opens a modal over the board: a copy of the rule that drifts behind the engine's
118
+ * does not merely warn wrongly, it calls a pipeline the product SHIPS invalid and leaves the board
119
+ * unusable. A DEPLOYMENT-registered kind can override gatability for itself through the agent-kind
120
+ * registry, which the SPA cannot see, so the two are not perfectly symmetric: such a kind is
121
+ * reported here and accepted by the engine. That is the safe direction of the asymmetry — a
122
+ * dismissible advisory rather than a refused save — and the only one available without shipping the
123
+ * registry to the browser.
124
+ */
125
+ function skipAxisProblem(
126
+ p: Pipeline,
127
+ i: number,
128
+ axis: {
129
+ notGatable: (kind: string | undefined) => string
130
+ withHumanGate: (kind: string) => string
131
+ },
132
+ ): string | null {
133
+ const kind = p.agentKinds[i]
134
+ if (!kind || !isBuiltinGatableKind(kind)) return axis.notGatable(kind)
135
+ if (p.gates?.[i] === true) return axis.withHumanGate(kind)
136
+ return null
137
+ }
138
+
139
+ /**
140
+ * Estimate gating: the shared skip-axis rules, plus the two specific to an estimate — at least one
141
+ * axis threshold (with none the step would ALWAYS skip) and an enabled task-estimator earlier in
142
+ * the chain (or the gate has nothing to consult). Mirrors `assertValidGating`.
143
+ */
144
+ function gatingProblem(p: Pipeline): string | null {
126
145
  const gating = p.gating
127
- if (gating) {
128
- for (let i = 0; i < kinds.length; i++) {
129
- const g = gating[i] as StepGating | null | undefined
130
- if (!g?.enabled || !isEnabledAt(p, i)) continue
131
- const kind = kinds[i]
132
- if (!kind || !isBuiltinGatableKind(kind)) {
133
- return `Step '${kind}' may not be estimate-gated — its output is required by the rest of the run. Only a step whose result later steps read as context (a design, a review, an extra verification pass) may be skipped on the estimate.`
134
- }
135
- // A human approval gate and an estimate gate on the same step contradict: the estimate may
136
- // ADD a human checkpoint but never CANCEL a pause the pipeline author asked for.
137
- if (p.gates?.[i] === true) {
138
- return `Step '${kind}' carries a human approval gate, so it cannot also be estimate-gated — the estimate may add a human checkpoint but never remove one.`
139
- }
140
- if (g.minComplexity === undefined && g.minRisk === undefined && g.minImpact === undefined) {
141
- return `Step '${kind}' is estimate-gated but sets no threshold (complexity / risk / impact).`
142
- }
143
- const hasEstimator = kinds
144
- .slice(0, i)
145
- .some((k, j) => k === TASK_ESTIMATOR_KIND && isEnabledAt(p, j))
146
- if (!hasEstimator) {
147
- return `Step '${kind}' is gated on the estimate but no enabled '${TASK_ESTIMATOR_KIND}' runs before it.`
148
- }
146
+ if (!gating) return null
147
+ const kinds = p.agentKinds
148
+ for (let i = 0; i < kinds.length; i++) {
149
+ const g = gating[i] as StepGating | null | undefined
150
+ if (!g?.enabled || !isEnabledAt(p, i)) continue
151
+ const shared = skipAxisProblem(p, i, {
152
+ notGatable: (kind) =>
153
+ `Step '${kind}' may not be estimate-gated — its output is required by the rest of the run. Only a step whose result later steps read as context (a design, a review, an extra verification pass) may be skipped on the estimate.`,
154
+ withHumanGate: (kind) =>
155
+ `Step '${kind}' carries a human approval gate, so it cannot also be estimate-gated — the estimate may add a human checkpoint but never remove one.`,
156
+ })
157
+ if (shared) return shared
158
+ const kind = kinds[i]
159
+ if (g.minComplexity === undefined && g.minRisk === undefined && g.minImpact === undefined) {
160
+ return `Step '${kind}' is estimate-gated but sets no threshold (complexity / risk / impact).`
161
+ }
162
+ const hasEstimator = kinds
163
+ .slice(0, i)
164
+ .some((k, j) => k === TASK_ESTIMATOR_KIND && isEnabledAt(p, j))
165
+ if (!hasEstimator) {
166
+ return `Step '${kind}' is gated on the estimate but no enabled '${TASK_ESTIMATOR_KIND}' runs before it.`
149
167
  }
150
168
  }
151
169
  return null
152
170
  }
153
171
 
172
+ /**
173
+ * Run conditions: the SECOND skip axis, held to the shared rules and nothing more. A skip is a skip
174
+ * whichever axis caused it, so a condition on a non-gatable kind drops something the run needs.
175
+ * Mirrors `assertValidRunConditions`. A condition BESIDE an estimate gate is deliberately fine.
176
+ */
177
+ function conditionProblem(p: Pipeline): string | null {
178
+ const stepOptions = p.stepOptions
179
+ if (!stepOptions) return null
180
+ for (let i = 0; i < p.agentKinds.length; i++) {
181
+ if (!stepOptions[i]?.condition || !isEnabledAt(p, i)) continue
182
+ const problem = skipAxisProblem(p, i, {
183
+ notGatable: (kind) =>
184
+ `Step '${kind}' may not carry a run condition — its output is required by the rest of the run, so a run outside the condition's scope would silently finish without it.`,
185
+ withHumanGate: (kind) =>
186
+ `Step '${kind}' carries a human approval gate, so it cannot also carry a run condition — a condition may leave a checkpoint un-reached but never remove one.`,
187
+ })
188
+ if (problem) return problem
189
+ }
190
+ return null
191
+ }
192
+
193
+ /**
194
+ * Client-side mirror of the backend `validatePipelineShape` (companion adjacency + both skip axes,
195
+ * over the ENABLED subset), collecting the first problem instead of throwing. Returns a human
196
+ * message, or null when the shape is valid. Kept in step with
197
+ * `backend/packages/orchestration/src/modules/pipelines/pipelineShape.ts`.
198
+ *
199
+ * One delegate per rule, in the order the backend checks them, so adding the next rule is a
200
+ * function beside these rather than another branch inside one that already carries three.
201
+ *
202
+ * A rule here must be keyed off vocabulary SHARED with that module (`@cat-factory/contracts`)
203
+ * wherever one exists, never re-stated locally — see {@link skipAxisProblem} for what a drifted
204
+ * copy costs. Adding a rule to `validatePipelineShape` without adding it here is the milder half of
205
+ * the same drift: a pipeline the engine refuses at save that this advisory calls healthy.
206
+ */
207
+ function shapeProblem(p: Pipeline): string | null {
208
+ // No enabled steps ⇒ nothing would run.
209
+ if (p.agentKinds.length === 0 || !p.agentKinds.some((_, i) => isEnabledAt(p, i))) {
210
+ return 'No enabled steps — the pipeline has nothing to run.'
211
+ }
212
+ return companionProblem(p) ?? gatingProblem(p) ?? conditionProblem(p)
213
+ }
214
+
154
215
  /**
155
216
  * Detect pipelines in an unhealthy state for the startup advisory: those referencing an unknown
156
217
  * agent kind or with an invalid shape (offer to delete a custom one / reseed a built-in), built-ins
@@ -35,6 +35,11 @@ export function customKindToArchetype(kind: CustomAgentKind): AgentArchetype {
35
35
  // projection would fork the rule the moment the default changes.
36
36
  ...(p.tier ? { tier: p.tier } : {}),
37
37
  ...(p.resultView ? { resultView: p.resultView } : {}),
38
+ // The kind is the platform's to dispatch, not a block anyone places. Carried onto the
39
+ // archetype rather than dropped at the projection because the catalog is also the READ MODEL
40
+ // every run view resolves a step's label and icon through: filtering it out here would leave
41
+ // the wizard's own analyst run rendering as an unknown kind.
42
+ ...(p.internal ? { internal: true } : {}),
38
43
  // Not part of `presentation` on the wire — it is a fact about how the kind RUNS, projected
39
44
  // beside `container` — so it is lifted from the entry itself. Carried onto the archetype
40
45
  // because the pipeline builder resolves a step's meta through `agentKindMeta`, not through
@@ -9,7 +9,6 @@ import type { useBoardStore } from '~/stores/board'
9
9
  import type { useExecutionStore } from '~/stores/execution'
10
10
  import type { useGitHubStore } from '~/stores/github'
11
11
  import type { useInfraConfigStore } from '~/stores/infraConfig'
12
- import type { usePipelinesStore } from '~/stores/pipelines'
13
12
  import type { usePreflightsStore } from '~/stores/preflights'
14
13
 
15
14
  /**
@@ -47,7 +46,6 @@ export interface WizardContext {
47
46
  trialStarted: Ref<boolean>
48
47
  // ---- derived the actions read ----
49
48
  repoContext: ComputedRef<{ githubId: number; directory?: string | null } | undefined>
50
- analysisPipeline: ComputedRef<ReturnType<ReturnType<typeof usePipelinesStore>['getPipeline']>>
51
49
  merged: ComputedRef<MergedRecipeDraft | null>
52
50
  }
53
51
 
@@ -1,3 +1,4 @@
1
+ import { ENVIRONMENT_ANALYST_AGENT_KIND } from '@cat-factory/contracts'
1
2
  import type { WizardContext } from './context'
2
3
  import { cloneRecipe } from './context'
3
4
 
@@ -34,7 +35,6 @@ export function createFlowActions(ctx: WizardContext) {
34
35
  trialError,
35
36
  trialStarted,
36
37
  repoContext,
37
- analysisPipeline,
38
38
  merged,
39
39
  } = ctx
40
40
 
@@ -117,18 +117,23 @@ export function createFlowActions(ctx: WizardContext) {
117
117
  if (id) void detect()
118
118
  }
119
119
 
120
- /** Fire the analyst-only pipeline against the frame (mirrors how bootstrap runs pl_blueprint). */
120
+ /**
121
+ * Run the analyst agent against the frame — a SINGLE-KIND run, the same seam the board's
122
+ * "Map service" action uses. `startAgentKind` reports a refusal by returning false (it has
123
+ * already surfaced the reason as a toast), so both halves of "it did not start" land on the
124
+ * wizard's own error state rather than only the thrown one.
125
+ */
121
126
  async function startAnalysis() {
122
127
  const id = frameId.value
123
- const pipeline = analysisPipeline.value
124
- if (!id || !pipeline) {
128
+ if (!id) {
125
129
  analysisError.value = true
126
130
  return
127
131
  }
128
132
  analysisError.value = false
129
133
  try {
130
- await execution.start(id, pipeline)
131
- analysisRequested.value = true
134
+ const started = await execution.startAgentKind(id, ENVIRONMENT_ANALYST_AGENT_KIND)
135
+ if (started) analysisRequested.value = true
136
+ else analysisError.value = true
132
137
  } catch {
133
138
  analysisError.value = true
134
139
  }
@@ -7,6 +7,8 @@ import {
7
7
  type PreflightResult,
8
8
  type ProvisioningRecommendation,
9
9
  type StackRecipe,
10
+ ENVIRONMENT_ANALYST_AGENT_KIND,
11
+ adHocPipelineIdFor,
10
12
  analystRecipeDraftSchema,
11
13
  mergeAnalystRecipeDraft,
12
14
  } from '@cat-factory/contracts'
@@ -15,7 +17,6 @@ import { useBoardStore } from '~/stores/board'
15
17
  import { useExecutionStore } from '~/stores/execution'
16
18
  import { useGitHubStore } from '~/stores/github'
17
19
  import { useInfraConfigStore } from '~/stores/infraConfig'
18
- import { usePipelinesStore } from '~/stores/pipelines'
19
20
  import { usePreflightsStore } from '~/stores/preflights'
20
21
  import { useServicesStore } from '~/stores/services'
21
22
  import type { WizardContext } from '~/stores/environmentWizard/context'
@@ -43,10 +44,11 @@ import { createSaveActions } from '~/stores/environmentWizard/save'
43
44
  // recipe / save) that close over the shared reactive {@link WizardContext} assembled here — a
44
45
  // size-only extraction following the `board` store idiom, behaviour is unchanged.
45
46
 
46
- /** The seeded analyst-only pipeline the "run deep analysis" trigger starts against the frame. */
47
- const ANALYSIS_PIPELINE_ID = 'pl_environment_analysis'
48
- /** The analyst agent kind whose `result.custom` carries the drafted recipe. */
49
- const ANALYST_AGENT_KIND = 'environment-analyst'
47
+ // The "run deep analysis" trigger starts the analyst agent as a SINGLE-KIND run — one step, no
48
+ // pipeline and reads the drafted recipe off that step's `result.custom`. Both the kind and the
49
+ // id its run reports come from the shared contract, so the wizard cannot go looking for a run
50
+ // under a name the backend stopped using.
51
+ const ANALYSIS_PIPELINE_ID = adHocPipelineIdFor(ENVIRONMENT_ANALYST_AGENT_KIND)
50
52
 
51
53
  /** The analyst run's lifecycle as the wizard surfaces it. */
52
54
  export type AnalysisStatus = 'idle' | 'running' | 'ready' | 'failed'
@@ -57,7 +59,6 @@ export const useEnvironmentWizardStore = defineStore('environmentWizard', () =>
57
59
  const services = useServicesStore()
58
60
  const infra = useInfraConfigStore()
59
61
  const execution = useExecutionStore()
60
- const pipelines = usePipelinesStore()
61
62
  const preflights = usePreflightsStore()
62
63
 
63
64
  // ---- Target frame -------------------------------------------------------
@@ -124,9 +125,10 @@ export const useEnvironmentWizardStore = defineStore('environmentWizard', () =>
124
125
 
125
126
  const hasRepo = computed(() => repoContext.value !== undefined)
126
127
 
127
- /** The seeded analyst pipeline, when present in the workspace (else deep analysis is unavailable). */
128
- const analysisPipeline = computed(() => pipelines.getPipeline(ANALYSIS_PIPELINE_ID))
129
- const canAnalyze = computed(() => hasRepo.value && analysisPipeline.value !== undefined)
128
+ // Deep analysis needs only a repo to read: the agent is started by KIND, so there is no
129
+ // catalog row for the workspace to be missing (which is what the old `pl_environment_analysis`
130
+ // lookup guarded against).
131
+ const canAnalyze = computed(() => hasRepo.value)
130
132
 
131
133
  /** The analyst run for this frame (newest matching instance), read live from the execution store.
132
134
  * Filters the full instance list (not the collapsing `getByBlock`, which returns a single run per
@@ -153,7 +155,7 @@ export const useEnvironmentWizardStore = defineStore('environmentWizard', () =>
153
155
  const analystDraft = computed<AnalystRecipeDraft | null>(() => {
154
156
  const run = analystRun.value
155
157
  if (!run) return null
156
- const analystStep = run.steps.find((s) => s.agentKind === ANALYST_AGENT_KIND)
158
+ const analystStep = run.steps.find((s) => s.agentKind === ENVIRONMENT_ANALYST_AGENT_KIND)
157
159
  if (!analystStep || analystStep.state !== 'done' || analystStep.custom === undefined)
158
160
  return null
159
161
  const parsed = v.safeParse(analystRecipeDraftSchema, analystStep.custom)
@@ -207,7 +209,6 @@ export const useEnvironmentWizardStore = defineStore('environmentWizard', () =>
207
209
  trialError,
208
210
  trialStarted,
209
211
  repoContext,
210
- analysisPipeline,
211
212
  merged,
212
213
  }
213
214
  const flow = createFlowActions(context)
@@ -41,10 +41,15 @@ export function createExecutionCommands(ctx: ExecutionCommandContext) {
41
41
  * and nothing merges. It is a request, not a decision — the task's merge preset can sandbox a
42
42
  * role's runs whatever they asked for, so what the run got is read back off the run's own
43
43
  * `mode`, never assumed from what was sent here.
44
+ *
45
+ * Takes the id + name it actually sends and reports, not a whole {@link Pipeline}: a caller may
46
+ * legitimately hold neither, because a task can be pinned to an INTERNAL pipeline that the
47
+ * library withholds from every picker. Demanding the row there would force the caller to either
48
+ * fabricate one or silently start something else.
44
49
  */
45
50
  async function start(
46
51
  blockId: string,
47
- pipeline: Pipeline,
52
+ pipeline: Pick<Pipeline, 'id' | 'name'>,
48
53
  options?: { mode?: RunMode },
49
54
  ): Promise<boolean> {
50
55
  const ws = useWorkspaceStore()
@@ -71,6 +76,25 @@ export function createExecutionCommands(ctx: ExecutionCommandContext) {
71
76
  }
72
77
  }
73
78
 
79
+ /**
80
+ * Start ONE agent kind against a block. The single-kind counterpart of {@link start}: same
81
+ * credential gate, same snapshot refresh, same false-on-refusal contract — only the thing being
82
+ * started is an agent rather than a pipeline.
83
+ */
84
+ async function startAgentKind(blockId: string, agentKind: string): Promise<boolean> {
85
+ const ws = useWorkspaceStore()
86
+ const personal = usePersonalSubscriptionsStore()
87
+ try {
88
+ return await personal.withCredential(async (password) => {
89
+ await api.startAgentKindExecution(ws.requireId(), blockId, agentKind, password)
90
+ await ws.refresh()
91
+ })
92
+ } catch (e) {
93
+ runErrors.present(e, 'errors.action.startFailed')
94
+ return false
95
+ }
96
+ }
97
+
74
98
  // Interacting with a running individual-usage run (resolve/approve/request-changes) advances
75
99
  // + re-dispatches the run, so the server re-mints its short-TTL activation from the personal
76
100
  // password first. It rides the cached password transparently, and — like start/retry — is
@@ -237,6 +261,7 @@ export function createExecutionCommands(ctx: ExecutionCommandContext) {
237
261
 
238
262
  return {
239
263
  start,
264
+ startAgentKind,
240
265
  resolveDecision,
241
266
  approveStep,
242
267
  requestStepChanges,
@@ -4,6 +4,7 @@ import { companionForProducer } from '~/utils/catalog'
4
4
  import type { PipelinesContext } from './context'
5
5
  import { createPipelineGateConfigActions } from './draftGateConfig'
6
6
  import { createPipelineStepConfigActions } from './draftStepConfig'
7
+ import { createPipelineStepOptionActions } from './draftStepOptions'
7
8
 
8
9
  /**
9
10
  * The pipeline-builder draft's STRUCTURE: inserting/removing/reordering steps, the companion
@@ -193,6 +194,7 @@ export function createPipelineDraftActions(ctx: PipelinesContext) {
193
194
 
194
195
  return {
195
196
  ...createPipelineStepConfigActions(ctx),
197
+ ...createPipelineStepOptionActions(ctx),
196
198
  ...createPipelineGateConfigActions(ctx),
197
199
  addToDraft,
198
200
  removeFromDraft,