@cat-factory/app 0.257.0 → 0.258.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/binaryOutput/BinaryOutputReport.vue +42 -0
- package/app/components/board/AddTaskModal.vue +38 -12
- package/app/components/board/RecurringPipelineModal.vue +24 -12
- package/app/components/board/nodes/TaskCard.vue +34 -7
- package/app/components/panels/InspectorPanel.vue +39 -0
- package/app/components/pipeline/BinaryOutputStepPicker.logic.spec.ts +29 -0
- package/app/components/pipeline/BinaryOutputStepPicker.logic.ts +23 -0
- package/app/components/pipeline/BinaryOutputStepPicker.vue +109 -0
- package/app/components/pipeline/PipelineBuilder.vue +44 -0
- package/app/components/pipeline/PipelinePreview.vue +20 -1
- package/app/components/pipeline/PipelineProgress.vue +13 -0
- package/app/composables/api/execution.ts +19 -0
- package/app/composables/usePipelineHealth.spec.ts +42 -5
- package/app/composables/usePipelineHealth.ts +109 -48
- package/app/modular/agent-kinds.ts +5 -0
- package/app/stores/environmentWizard/context.ts +0 -2
- package/app/stores/environmentWizard/flow.ts +11 -6
- package/app/stores/environmentWizard.ts +12 -11
- package/app/stores/execution/commands.ts +26 -1
- package/app/stores/pipelines/draftActions.ts +2 -0
- package/app/stores/pipelines/draftStepConfig.ts +4 -161
- package/app/stores/pipelines/draftStepOptions.ts +204 -0
- package/app/types/domain.ts +6 -0
- package/app/utils/agentPalette.spec.ts +26 -0
- package/app/utils/agentPalette.ts +9 -4
- package/app/utils/binaryOutput.spec.ts +122 -1
- package/app/utils/binaryOutput.ts +101 -0
- package/app/utils/catalog.spec.ts +24 -0
- package/app/utils/catalog.ts +21 -4
- package/app/utils/pipeline.spec.ts +35 -3
- package/app/utils/pipeline.ts +54 -3
- package/app/utils/pipelineRender.spec.ts +78 -2
- package/app/utils/pipelineRender.ts +43 -0
- package/i18n/locales/de.json +29 -1
- package/i18n/locales/en.json +29 -1
- package/i18n/locales/es.json +29 -1
- package/i18n/locales/fr.json +29 -1
- package/i18n/locales/he.json +29 -1
- package/i18n/locales/it.json +29 -1
- package/i18n/locales/ja.json +29 -1
- package/i18n/locales/pl.json +29 -1
- package/i18n/locales/tr.json +29 -1
- package/i18n/locales/uk.json +29 -1
- 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 {
|
|
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 (
|
|
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
|
|
98
|
-
const
|
|
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: '
|
|
110
|
+
{ id: 'pl_audit', name: 'Audit and fix' },
|
|
111
111
|
)
|
|
112
|
-
const { hasIssues, invalid, outdated } = scan([
|
|
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
|
-
*
|
|
84
|
-
*
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 ===
|
|
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,
|