@cat-factory/app 0.89.0 → 0.91.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.
@@ -86,6 +86,7 @@ const selectedPipelineLabel = computed(
86
86
  const template = computed<ScheduleTemplate>(() => {
87
87
  if (pipelineId.value === 'pl_tech_debt') return 'tech-debt'
88
88
  if (pipelineId.value === 'pl_dep_update') return 'dep-update'
89
+ if (pipelineId.value === 'pl_bug_triage') return 'bug-triage'
89
90
  return 'custom'
90
91
  })
91
92
  const isTechDebt = computed(() => template.value === 'tech-debt')
@@ -35,9 +35,16 @@ const gate = computed<GateStepState | null>(() => step.value?.gate ?? null)
35
35
 
36
36
  const isCi = computed(() => step.value?.agentKind === 'ci')
37
37
  const isHumanReview = computed(() => step.value?.agentKind === 'human-review')
38
+ const isDocQuality = computed(() => step.value?.agentKind === 'doc-quality')
38
39
  const meta = computed(() => agentKindMeta(step.value?.agentKind ?? 'ci'))
39
40
  const helperKind = computed(() =>
40
- isHumanReview.value ? 'fixer' : isCi.value ? 'ci-fixer' : 'conflict-resolver',
41
+ isHumanReview.value
42
+ ? 'fixer'
43
+ : isCi.value
44
+ ? 'ci-fixer'
45
+ : isDocQuality.value
46
+ ? 'doc-fixer'
47
+ : 'conflict-resolver',
41
48
  )
42
49
  const helperMeta = computed(() => agentKindMeta(helperKind.value))
43
50
 
@@ -46,7 +53,9 @@ const subtitle = computed(() =>
46
53
  ? t('gates.subtitle.humanReview')
47
54
  : isCi.value
48
55
  ? t('gates.subtitle.ci')
49
- : t('gates.subtitle.conflicts'),
56
+ : isDocQuality.value
57
+ ? t('gates.subtitle.docQuality')
58
+ : t('gates.subtitle.conflicts'),
50
59
  )
51
60
 
52
61
  // Human-review: approval progress + the freeform "request a fix" control.
@@ -316,6 +325,35 @@ const conflictVerdict = computed(() => {
316
325
  </p>
317
326
  </template>
318
327
 
328
+ <!-- Doc quality: the deterministic structural findings the gate raised -->
329
+ <template v-else-if="isDocQuality">
330
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
331
+ {{ t('gates.docQuality.findings') }}
332
+ </h3>
333
+ <div
334
+ v-if="gate.lastFailureSummary"
335
+ class="relative rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
336
+ >
337
+ <CopyButton :text="gate.lastFailureSummary" class="absolute end-1 top-1" />
338
+ <p class="whitespace-pre-wrap pe-8 text-[12px] leading-relaxed text-slate-300">
339
+ {{ gate.lastFailureSummary }}
340
+ </p>
341
+ </div>
342
+ <p v-else class="text-[13px] leading-relaxed text-slate-300">
343
+ {{ t('gates.docQuality.findingsFallback') }}
344
+ </p>
345
+ <a
346
+ v-if="prUrl"
347
+ :href="prUrl"
348
+ target="_blank"
349
+ rel="noopener"
350
+ class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
351
+ >
352
+ {{ t('gates.docQuality.viewPr') }}
353
+ <UIcon name="i-lucide-external-link" class="h-3 w-3" />
354
+ </a>
355
+ </template>
356
+
319
357
  <!-- Conflicts: verdict + the resolver's account of what it left -->
320
358
  <template v-else>
321
359
  <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
@@ -3,9 +3,11 @@
3
3
  // environment tab's backend-type selector (the other being the BYO HTTP manifest editor).
4
4
  // It builds the discriminated `{ kind: 'kubernetes', kubernetes }` config plus the
5
5
  // `apiToken` secret bundle and emits test/save to the parent tab (which calls the shared
6
- // provider-connections store). The K8s env config differs from the runner K8s config
7
- // (manifest source + URL derivation vs an executor image + namespace), so it has its own
8
- // form rather than reusing KubernetesRunnerForm.
6
+ // provider-connections store). The K8s env config has manifest-source + URL-derivation
7
+ // (discriminated-union) fields a flat descriptor form can't yet express, so unlike the
8
+ // runner backends, which now self-describe via `configTemplate` and render through the
9
+ // generic form — the environment axis keeps this bespoke form until it, too, is descriptor-
10
+ // driven (see docs/initiatives/descriptor-driven-infra-forms.md).
9
11
  import { computed, reactive, ref, watch } from 'vue'
10
12
  import { KUBERNETES_ENV_TOKEN_SECRET_KEY } from '@cat-factory/contracts'
11
13
  import type { ProviderConnection } from '~/types/providerConnections'
@@ -1,16 +1,20 @@
1
1
  <script setup lang="ts">
2
2
  // One tab of the Infrastructure window — the connect surface for a single provider kind
3
3
  // (container agents → runner pool, or test environments → environment provider). Both
4
- // self-describe via a ProviderDescriptor, so this renders either without hard-coding them:
5
- // - a NATIVE provider (ships a `manifestTemplate`) → the friendly flat field form, whose
6
- // values are overlaid back onto the manifest before saving (the single storage path).
4
+ // self-describe via a ProviderDescriptor, so this renders every backend WITHOUT hard-coding
5
+ // which optional kinds (Kubernetes, EKS, a custom kind) exist:
6
+ // - a NATIVE MANIFEST provider (ships a `manifestTemplate`) the flat field form, whose
7
+ // values are overlaid back onto the manifest before saving.
8
+ // - a NATIVE CONFIG backend (ships a `configTemplate` — the runner backends Kubernetes/EKS
9
+ // and any custom native kind) → the SAME flat field form, whose values are overlaid onto
10
+ // the discriminated `{ kind, <payload> }` config the backend described. The SPA never
11
+ // names a backend; it reads the single payload key off the skeleton.
7
12
  // - a MANIFEST-driven provider (no template) → the full JSON manifest editor
8
13
  // (ProviderManifestEditor), which replaces the old "use the API" disclaimer.
9
14
  import { computed, ref, toRaw, watch } from 'vue'
10
- import type { ProviderConnectionKind } from '~/types/providerConnections'
15
+ import type { ProviderConfigField, ProviderConnectionKind } from '~/types/providerConnections'
11
16
  import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
12
17
  import ProviderManifestEditor from '~/components/settings/ProviderManifestEditor.vue'
13
- import KubernetesRunnerForm from '~/components/settings/KubernetesRunnerForm.vue'
14
18
  import KubernetesEnvironmentForm from '~/components/settings/KubernetesEnvironmentForm.vue'
15
19
 
16
20
  const props = defineProps<{
@@ -56,17 +60,28 @@ const testResult = ref<{ ok: boolean; message?: string } | null>(null)
56
60
  const testing = ref(false)
57
61
  const busy = ref(false)
58
62
 
59
- /** A native provider ships a manifest scaffold ⇒ render the friendly flat field form. */
60
- const isNative = computed(() => !!descriptor.value?.manifestTemplate)
63
+ // A native provider renders the friendly flat field form. Two flavours self-describe it:
64
+ // `manifestTemplate` (overlay onto a manifest) and `configTemplate` (overlay onto a
65
+ // discriminated backend config — the Kubernetes/EKS/custom runner backends).
66
+ const isNativeManifest = computed(() => !!descriptor.value?.manifestTemplate)
67
+ const isNativeConfig = computed(() => !!descriptor.value?.configTemplate)
68
+ const isNative = computed(() => isNativeManifest.value || isNativeConfig.value)
61
69
  const secretFieldCount = computed(
62
70
  () => (descriptor.value?.configFields ?? []).filter((f) => f.secret).length,
63
71
  )
64
72
  const hasSecretFields = computed(() => secretFieldCount.value > 0)
65
73
 
66
- // Seed the flat-form draft from the saved manifest so an edit starts from the CURRENT
67
- // non-secret config (baseUrl + providerConfig). Secret fields are never prefilled.
74
+ // Seed the flat-form draft from the CURRENT non-secret config so an edit starts populated.
75
+ // Secret fields are never prefilled. A `configTemplate` backend gets its flat values straight
76
+ // from `descriptor.values`; a `manifestTemplate` backend reads them off the saved manifest
77
+ // (baseUrl + providerConfig). On a fresh Kubernetes connect a `k3s` preset prefills local defaults.
68
78
  function resetDraft() {
69
79
  testResult.value = null
80
+ if (isNativeConfig.value) {
81
+ values.value = { ...descriptor.value?.values }
82
+ applyPresetDefaults()
83
+ return
84
+ }
70
85
  const saved = descriptor.value?.savedManifest
71
86
  const cfg = (saved?.providerConfig as Record<string, unknown> | undefined) ?? {}
72
87
  const next: Record<string, string> = {}
@@ -82,6 +97,24 @@ function resetDraft() {
82
97
  values.value = next
83
98
  }
84
99
 
100
+ // The low-config `k3s` preset (an execution-axis radio the picker synthesises) prefills the
101
+ // local-cluster defaults into the generic Kubernetes runner form. Only on a FRESH connect —
102
+ // never clobbering a stored connection's values. Kept SPA-local because the preset is a UI
103
+ // affordance of the picker, not a backend concept.
104
+ function applyPresetDefaults() {
105
+ if (props.preset !== 'k3s' || connection.value) return
106
+ const seed: Record<string, string> = {
107
+ label: 'Local k3s',
108
+ apiServerUrl: 'https://127.0.0.1:6443',
109
+ namespace: 'cat-factory',
110
+ insecureSkipTlsVerify: 'true',
111
+ }
112
+ if (props.suggestedImage) seed.image = props.suggestedImage
113
+ for (const [k, val] of Object.entries(seed)) {
114
+ if (!(values.value[k] ?? '').trim()) values.value[k] = val
115
+ }
116
+ }
117
+
85
118
  /** A flat-form field is satisfied when filled now, or already stored, or it has a default. */
86
119
  function satisfied(key: string): boolean {
87
120
  const f = descriptor.value?.configFields.find((cf) => cf.key === key)
@@ -124,6 +157,51 @@ function buildManifestPayload(): {
124
157
  return { manifest, secrets, backendKind: props.backendKind }
125
158
  }
126
159
 
160
+ /** Coerce a flat string form value to the JSON type the backend config expects for its field. */
161
+ function coerceFieldValue(field: ProviderConfigField, raw: string): unknown {
162
+ if (field.type === 'number') return Number(raw)
163
+ if (field.type === 'checkbox') return raw === 'true'
164
+ return raw
165
+ }
166
+
167
+ /**
168
+ * Overlay the flat field values onto a NATIVE backend's discriminated `configTemplate`. The
169
+ * skeleton is `{ kind, <payload> }`, so every non-secret field is written to the single
170
+ * non-`kind` payload key (typed via the field's `type`), each secret to the write-only bundle,
171
+ * and a cleared field is dropped so it reverts to absent. Because the template is the STORED
172
+ * config on an edit, advanced API-only keys the flat form never renders are preserved.
173
+ */
174
+ function buildConfigPayload(): {
175
+ config: Record<string, unknown>
176
+ secrets: Record<string, string>
177
+ } | null {
178
+ const template = descriptor.value?.configTemplate
179
+ if (!template) return null
180
+ const config: Record<string, unknown> = structuredClone(toRaw(template))
181
+ const payloadKey = Object.keys(config).find((k) => k !== 'kind')
182
+ if (!payloadKey) return null
183
+ const payload: Record<string, unknown> = {
184
+ ...(config[payloadKey] as Record<string, unknown> | undefined),
185
+ }
186
+ const secrets: Record<string, string> = {}
187
+ for (const f of descriptor.value?.configFields ?? []) {
188
+ const raw = (values.value[f.key] ?? '').trim()
189
+ if (f.secret) {
190
+ if (raw) secrets[f.key] = raw
191
+ continue
192
+ }
193
+ if (!raw) delete payload[f.key]
194
+ else payload[f.key] = coerceFieldValue(f, raw)
195
+ }
196
+ config[payloadKey] = payload
197
+ return { config, secrets }
198
+ }
199
+
200
+ /** The payload for the active native flavour (discriminated config or manifest overlay). */
201
+ function buildFlatPayload() {
202
+ return isNativeConfig.value ? buildConfigPayload() : buildManifestPayload()
203
+ }
204
+
127
205
  function notifyError(title: string, e: unknown) {
128
206
  toast.add({
129
207
  title,
@@ -141,9 +219,9 @@ function toastSaved() {
141
219
  })
142
220
  }
143
221
 
144
- // --- Native flat-form actions -------------------------------------------------------
222
+ // --- Native flat-form actions (both manifest-overlay and config-overlay flavours) ---
145
223
  async function testNative() {
146
- const payload = buildManifestPayload()
224
+ const payload = buildFlatPayload()
147
225
  if (!payload) return
148
226
  testing.value = true
149
227
  testResult.value = null
@@ -159,7 +237,7 @@ async function testNative() {
159
237
  async function saveNative() {
160
238
  busy.value = true
161
239
  try {
162
- const payload = buildManifestPayload()
240
+ const payload = buildFlatPayload()
163
241
  if (payload) await store.register(props.kind, payload)
164
242
  emit('connected')
165
243
  resetDraft()
@@ -332,23 +410,12 @@ function fieldHelp(key: string): string | undefined {
332
410
  }}
333
411
  </div>
334
412
 
335
- <!-- Native Kubernetes runner backend (runner-pool). -->
336
- <KubernetesRunnerForm
337
- v-if="kind === 'runner-pool' && backendKind === 'kubernetes'"
338
- :connection="connection"
339
- :preset="preset"
340
- :suggested-image="suggestedImage"
341
- :supports-test="descriptor.supportsTest"
342
- :testing="testing"
343
- :busy="busy"
344
- :test-result="testResult"
345
- @test="testConfig"
346
- @save="saveConfig"
347
- />
348
-
349
- <!-- Native Kubernetes ephemeral-environment backend (environment). -->
413
+ <!-- Native Kubernetes ephemeral-environment backend (environment). The runner-pool
414
+ Kubernetes/EKS backends now self-describe via `configTemplate` and render through the
415
+ generic flat form below — no per-kind component. The env axis keeps its bespoke form
416
+ until it, too, is descriptor-driven (see docs/initiatives/descriptor-driven-infra-forms.md). -->
350
417
  <KubernetesEnvironmentForm
351
- v-else-if="kind === 'environment' && backendKind === 'kubernetes'"
418
+ v-if="kind === 'environment' && backendKind === 'kubernetes'"
352
419
  :connection="connection"
353
420
  :supports-test="descriptor.supportsTest"
354
421
  :testing="testing"
@@ -396,6 +463,26 @@ function fieldHelp(key: string): string | undefined {
396
463
  :items="(field.options ?? []).map((o) => ({ label: o.label, value: o.value }))"
397
464
  :placeholder="field.default ?? field.placeholder"
398
465
  />
466
+ <USwitch
467
+ v-else-if="field.type === 'checkbox'"
468
+ :model-value="values[field.key] === 'true'"
469
+ @update:model-value="values[field.key] = $event ? 'true' : 'false'"
470
+ />
471
+ <UTextarea
472
+ v-else-if="field.type === 'textarea'"
473
+ v-model="values[field.key]"
474
+ :rows="4"
475
+ class="w-full font-mono"
476
+ :placeholder="field.default ?? field.placeholder"
477
+ />
478
+ <UInput
479
+ v-else-if="field.type === 'number'"
480
+ :model-value="values[field.key] ?? ''"
481
+ type="number"
482
+ class="font-mono"
483
+ :placeholder="field.default ?? field.placeholder"
484
+ @update:model-value="values[field.key] = String($event ?? '')"
485
+ />
399
486
  <UInput
400
487
  v-else
401
488
  v-model="values[field.key]"
@@ -410,6 +410,19 @@ export const SYSTEM_AGENT_META: Record<string, AgentArchetype> = {
410
410
  description:
411
411
  'Files a tracker ticket (GitHub issue / Jira) from the analysis before work starts.',
412
412
  },
413
+ // The inbound dual of `tracker`: a one-shot engine step that PULLS one matching open issue
414
+ // from the schedule's configured tracker board, marks it in-progress with a "taken by
415
+ // cat-factory" comment, and reseeds the recurring block from it. Runs no model; seeded only
416
+ // into the recurring bug-triage pipeline, so it is a display-metadata system kind (like
417
+ // `tracker`), not a palette archetype.
418
+ 'bug-intake': {
419
+ kind: 'bug-intake',
420
+ label: 'Bug Intake',
421
+ icon: 'i-lucide-inbox',
422
+ color: '#fb923c',
423
+ description:
424
+ 'Pulls one matching open issue from the configured tracker board, marks it in-progress, and seeds the run from it.',
425
+ },
413
426
  conflicts: {
414
427
  kind: 'conflicts',
415
428
  label: 'Conflicts Gate',
@@ -437,6 +450,20 @@ export const SYSTEM_AGENT_META: Record<string, AgentArchetype> = {
437
450
  // of the generic prose step-detail panel. Shared with the conflicts gate.
438
451
  resultView: 'gate',
439
452
  },
453
+ // The forward document pipelines' structural gate. Seed-only (no `category`, like ci /
454
+ // conflicts) — it's part of `pl_document(_quick)`, not a standing palette block — but it
455
+ // needs display metadata so timelines/saved pipelines render it. Its helper `doc-fixer`
456
+ // is a registered kind, so it arrives via the workspace snapshot's `customAgentKinds`.
457
+ 'doc-quality': {
458
+ kind: 'doc-quality',
459
+ label: 'Doc Quality Gate',
460
+ icon: 'i-lucide-file-check-2',
461
+ color: '#818cf8',
462
+ description:
463
+ 'Checks the drafted document for required sections, placeholders, links and heading structure, looping the doc fixer on problems.',
464
+ // Opens the dedicated gate window (verdict, attempts, the document findings).
465
+ resultView: 'gate',
466
+ },
440
467
  'ci-fixer': {
441
468
  kind: 'ci-fixer',
442
469
  label: 'CI Fixer',
@@ -3030,7 +3030,8 @@
3030
3030
  "subtitle": {
3031
3031
  "humanReview": "Waits for a human code review on the PR, looping the fixer on comments",
3032
3032
  "ci": "Gates the PR on green CI, looping the CI fixer on failure",
3033
- "conflicts": "Gates the PR on a clean merge, looping the resolver on conflicts"
3033
+ "conflicts": "Gates the PR on a clean merge, looping the resolver on conflicts",
3034
+ "docQuality": "Checks the drafted document's structure, looping the doc fixer on problems"
3034
3035
  },
3035
3036
  "status": {
3036
3037
  "passed": "Passed",
@@ -3071,6 +3072,11 @@
3071
3072
  "mergeability": "Mergeability",
3072
3073
  "viewPr": "View pull request on GitHub"
3073
3074
  },
3075
+ "docQuality": {
3076
+ "findings": "Document issues",
3077
+ "findingsFallback": "The document passed the structural checks.",
3078
+ "viewPr": "View pull request on GitHub"
3079
+ },
3074
3080
  "attemptsHeading": "{helper} attempts",
3075
3081
  "attempt": "Attempt {number}",
3076
3082
  "attemptInstructions": "Handed to {helper}",
@@ -2938,7 +2938,8 @@
2938
2938
  "subtitle": {
2939
2939
  "humanReview": "Espera una revisión de código humana en el PR, repitiendo el corrector ante los comentarios",
2940
2940
  "ci": "Bloquea el PR hasta que la CI esté verde, repitiendo el corrector de CI ante los fallos",
2941
- "conflicts": "Bloquea el PR hasta una fusión limpia, repitiendo el resolutor ante los conflictos"
2941
+ "conflicts": "Bloquea el PR hasta una fusión limpia, repitiendo el resolutor ante los conflictos",
2942
+ "docQuality": "Comprueba la estructura del documento redactado, repitiendo el corrector de documentos ante los problemas"
2942
2943
  },
2943
2944
  "status": {
2944
2945
  "passed": "Aprobado",
@@ -2979,6 +2980,11 @@
2979
2980
  "mergeability": "Fusionabilidad",
2980
2981
  "viewPr": "Ver la pull request en GitHub"
2981
2982
  },
2983
+ "docQuality": {
2984
+ "findings": "Problemas del documento",
2985
+ "findingsFallback": "El documento superó las comprobaciones estructurales.",
2986
+ "viewPr": "Ver la pull request en GitHub"
2987
+ },
2982
2988
  "attemptsHeading": "Intentos de {helper}",
2983
2989
  "attempt": "Intento {number}",
2984
2990
  "attemptInstructions": "Entregado a {helper}",
@@ -2938,7 +2938,8 @@
2938
2938
  "subtitle": {
2939
2939
  "humanReview": "Attend une revue de code humaine sur la PR, en relançant le correcteur à chaque commentaire",
2940
2940
  "ci": "Bloque la PR jusqu'à une CI verte, en relançant le correcteur de CI en cas d'échec",
2941
- "conflicts": "Bloque la PR jusqu'à une fusion propre, en relançant le résolveur en cas de conflits"
2941
+ "conflicts": "Bloque la PR jusqu'à une fusion propre, en relançant le résolveur en cas de conflits",
2942
+ "docQuality": "Vérifie la structure du document rédigé, en relançant le correcteur de documents en cas de problème"
2942
2943
  },
2943
2944
  "status": {
2944
2945
  "passed": "Réussi",
@@ -2979,6 +2980,11 @@
2979
2980
  "mergeability": "Fusionnabilité",
2980
2981
  "viewPr": "Voir la pull request sur GitHub"
2981
2982
  },
2983
+ "docQuality": {
2984
+ "findings": "Problèmes du document",
2985
+ "findingsFallback": "Le document a réussi les vérifications structurelles.",
2986
+ "viewPr": "Voir la pull request sur GitHub"
2987
+ },
2982
2988
  "attemptsHeading": "Tentatives du {helper}",
2983
2989
  "attempt": "Tentative {number}",
2984
2990
  "attemptInstructions": "Transmis à {helper}",
@@ -2943,7 +2943,8 @@
2943
2943
  "subtitle": {
2944
2944
  "humanReview": "ממתין לסקירת קוד אנושית על ה-PR, ומפעיל בלולאה את המתקן על הערות",
2945
2945
  "ci": "מגדר את ה-PR על CI ירוק, ומפעיל בלולאה את מתקן ה-CI בכישלון",
2946
- "conflicts": "מגדר את ה-PR על מיזוג נקי, ומפעיל בלולאה את הפותר בהתנגשויות"
2946
+ "conflicts": "מגדר את ה-PR על מיזוג נקי, ומפעיל בלולאה את הפותר בהתנגשויות",
2947
+ "docQuality": "בודק את מבנה המסמך שנוסח, ומפעיל בלולאה את מתקן המסמכים בבעיות"
2947
2948
  },
2948
2949
  "status": {
2949
2950
  "passed": "עבר",
@@ -2984,6 +2985,11 @@
2984
2985
  "mergeability": "יכולת מיזוג",
2985
2986
  "viewPr": "הצג את בקשת המשיכה ב-GitHub"
2986
2987
  },
2988
+ "docQuality": {
2989
+ "findings": "בעיות במסמך",
2990
+ "findingsFallback": "המסמך עבר את בדיקות המבנה.",
2991
+ "viewPr": "הצג את בקשת המשיכה ב-GitHub"
2992
+ },
2987
2993
  "attemptsHeading": "ניסיונות {helper}",
2988
2994
  "attempt": "ניסיון {number}",
2989
2995
  "attemptInstructions": "נמסר ל־{helper}",
@@ -2947,7 +2947,8 @@
2947
2947
  "subtitle": {
2948
2948
  "humanReview": "PR の人によるコードレビューを待ち、コメントに応じて fixer をループします",
2949
2949
  "ci": "CI がグリーンになるまで PR をゲートし、失敗時に CI fixer をループします",
2950
- "conflicts": "クリーンなマージになるまで PR をゲートし、コンフリクト時に resolver をループします"
2950
+ "conflicts": "クリーンなマージになるまで PR をゲートし、コンフリクト時に resolver をループします",
2951
+ "docQuality": "作成された文書の構造をチェックし、問題があれば doc fixer をループします"
2951
2952
  },
2952
2953
  "status": {
2953
2954
  "passed": "合格",
@@ -2988,6 +2989,11 @@
2988
2989
  "mergeability": "マージ可能性",
2989
2990
  "viewPr": "GitHub でプルリクエストを表示"
2990
2991
  },
2992
+ "docQuality": {
2993
+ "findings": "文書の問題",
2994
+ "findingsFallback": "文書は構造チェックに合格しました。",
2995
+ "viewPr": "GitHub でプルリクエストを表示"
2996
+ },
2991
2997
  "attemptsHeading": "{helper} の試行",
2992
2998
  "attempt": "試行 {number}",
2993
2999
  "attemptInstructions": "{helper} への指示",
@@ -2938,7 +2938,8 @@
2938
2938
  "subtitle": {
2939
2939
  "humanReview": "Czeka na recenzję kodu przez człowieka na PR, ponawiając korektora po komentarzach",
2940
2940
  "ci": "Blokuje PR do zielonego CI, ponawiając korektora CI przy niepowodzeniu",
2941
- "conflicts": "Blokuje PR do czystego scalenia, ponawiając rozwiązywacza przy konfliktach"
2941
+ "conflicts": "Blokuje PR do czystego scalenia, ponawiając rozwiązywacza przy konfliktach",
2942
+ "docQuality": "Sprawdza strukturę przygotowanego dokumentu, ponawiając korektora dokumentów przy problemach"
2942
2943
  },
2943
2944
  "status": {
2944
2945
  "passed": "Zaliczono",
@@ -2979,6 +2980,11 @@
2979
2980
  "mergeability": "Możliwość scalenia",
2980
2981
  "viewPr": "Zobacz pull request na GitHub"
2981
2982
  },
2983
+ "docQuality": {
2984
+ "findings": "Problemy dokumentu",
2985
+ "findingsFallback": "Dokument przeszedł kontrole strukturalne.",
2986
+ "viewPr": "Zobacz pull request na GitHub"
2987
+ },
2982
2988
  "attemptsHeading": "Próby: {helper}",
2983
2989
  "attempt": "Próba {number}",
2984
2990
  "attemptInstructions": "Przekazano do: {helper}",
@@ -2947,7 +2947,8 @@
2947
2947
  "subtitle": {
2948
2948
  "humanReview": "PR üzerinde bir insan kod incelemesini bekler, yorumlar üzerine düzelticiyi döngüye alır",
2949
2949
  "ci": "PR'ı yeşil CI üzerinde kapı altına alır, başarısızlıkta CI düzelticiyi döngüye alır",
2950
- "conflicts": "PR'ı temiz bir birleştirme üzerinde kapı altına alır, çakışmalarda çözücüyü döngüye alır"
2950
+ "conflicts": "PR'ı temiz bir birleştirme üzerinde kapı altına alır, çakışmalarda çözücüyü döngüye alır",
2951
+ "docQuality": "Hazırlanan belgenin yapısını denetler, sorunlarda belge düzelticiyi döngüye alır"
2951
2952
  },
2952
2953
  "status": {
2953
2954
  "passed": "Geçti",
@@ -2988,6 +2989,11 @@
2988
2989
  "mergeability": "Birleştirilebilirlik",
2989
2990
  "viewPr": "Çekme isteğini GitHub'da görüntüle"
2990
2991
  },
2992
+ "docQuality": {
2993
+ "findings": "Belge sorunları",
2994
+ "findingsFallback": "Belge yapısal denetimleri geçti.",
2995
+ "viewPr": "Çekme isteğini GitHub'da görüntüle"
2996
+ },
2991
2997
  "attemptsHeading": "{helper} denemeleri",
2992
2998
  "attempt": "Deneme {number}",
2993
2999
  "attemptInstructions": "{helper}'a iletildi",
@@ -2938,7 +2938,8 @@
2938
2938
  "subtitle": {
2939
2939
  "humanReview": "Чекає на огляд коду людиною в PR, повторюючи виправлювача після коментарів",
2940
2940
  "ci": "Блокує PR до зеленого CI, повторюючи виправлювача CI у разі помилки",
2941
- "conflicts": "Блокує PR до чистого злиття, повторюючи розв'язувача у разі конфліктів"
2941
+ "conflicts": "Блокує PR до чистого злиття, повторюючи розв'язувача у разі конфліктів",
2942
+ "docQuality": "Перевіряє структуру підготовленого документа, повторюючи виправлювача документів у разі проблем"
2942
2943
  },
2943
2944
  "status": {
2944
2945
  "passed": "Пройдено",
@@ -2979,6 +2980,11 @@
2979
2980
  "mergeability": "Можливість злиття",
2980
2981
  "viewPr": "Переглянути pull request на GitHub"
2981
2982
  },
2983
+ "docQuality": {
2984
+ "findings": "Проблеми документа",
2985
+ "findingsFallback": "Документ пройшов структурні перевірки.",
2986
+ "viewPr": "Переглянути pull request на GitHub"
2987
+ },
2982
2988
  "attemptsHeading": "Спроби: {helper}",
2983
2989
  "attempt": "Спроба {number}",
2984
2990
  "attemptInstructions": "Передано: {helper}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.89.0",
3
+ "version": "0.91.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "^3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.97.0"
37
+ "@cat-factory/contracts": "0.98.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",
@@ -1,252 +0,0 @@
1
- <script setup lang="ts">
2
- // The Kubernetes "agent runner backend" connect form — one option of the runner-pool tab's
3
- // backend-type selector (the other being the manifest pool). It builds the discriminated
4
- // `{ kind: 'kubernetes', kubernetes }` config + the `apiToken` secret bundle and emits
5
- // test/save to the parent tab (which calls the shared provider-connections store).
6
- import { computed, reactive, ref, watch } from 'vue'
7
- import { KUBERNETES_RUNNER_TOKEN_SECRET_KEY } from '@cat-factory/contracts'
8
- import type { ProviderConnection } from '~/types/providerConnections'
9
-
10
- const props = defineProps<{
11
- connection: ProviderConnection | null
12
- /** A low-config preset to seed the form with (today: local k3s). */
13
- preset?: 'k3s'
14
- /** The deployment's executor image, used to prefill the k3s preset's image field. */
15
- suggestedImage?: string
16
- supportsTest: boolean
17
- testing: boolean
18
- busy: boolean
19
- testResult: { ok: boolean; message?: string } | null
20
- }>()
21
-
22
- const emit = defineEmits<{
23
- test: [payload: { config: Record<string, unknown>; secrets: Record<string, string> }]
24
- save: [payload: { config: Record<string, unknown>; secrets: Record<string, string> }]
25
- }>()
26
-
27
- const { t } = useI18n()
28
-
29
- const form = reactive({
30
- label: '',
31
- apiServerUrl: '',
32
- namespace: '',
33
- image: '',
34
- imageUi: '',
35
- caCertPem: '',
36
- harnessPort: '',
37
- insecureSkipTlsVerify: false,
38
- })
39
- const apiToken = ref('')
40
-
41
- // A registered k8s connection exposes its non-secret config, so prefill every non-secret
42
- // field from it (never the token — secrets are write-only and re-entered on update). This
43
- // lets an edit change one field without re-typing the whole form.
44
- watch(
45
- () => props.connection,
46
- (c) => {
47
- if (c?.kind !== 'kubernetes') return
48
- form.label = c.label
49
- form.apiServerUrl = c.baseUrl
50
- const k =
51
- c.config && (c.config as { kind?: string }).kind === 'kubernetes'
52
- ? (c.config as { kubernetes: Record<string, unknown> }).kubernetes
53
- : undefined
54
- if (k) {
55
- form.namespace = typeof k.namespace === 'string' ? k.namespace : ''
56
- form.image = typeof k.image === 'string' ? k.image : ''
57
- form.imageUi = typeof k.imageUi === 'string' ? k.imageUi : ''
58
- form.caCertPem = typeof k.caCertPem === 'string' ? k.caCertPem : ''
59
- form.harnessPort = typeof k.harnessPort === 'number' ? String(k.harnessPort) : ''
60
- form.insecureSkipTlsVerify = k.insecureSkipTlsVerify === true
61
- }
62
- },
63
- { immediate: true },
64
- )
65
-
66
- // Low-config k3s preset: seed the local-cluster defaults so the operator only pastes a
67
- // ServiceAccount token (and an image, unless the deployment surfaced one). Only seeds a
68
- // fresh form — never clobbers an existing connection's config on edit.
69
- watch(
70
- () => props.preset,
71
- (preset) => {
72
- if (preset !== 'k3s' || props.connection) return
73
- form.label = 'Local k3s'
74
- form.apiServerUrl = 'https://127.0.0.1:6443'
75
- form.namespace = 'cat-factory'
76
- form.insecureSkipTlsVerify = true
77
- if (props.suggestedImage) form.image = props.suggestedImage
78
- },
79
- { immediate: true },
80
- )
81
-
82
- const canSave = computed(
83
- () =>
84
- !!form.label.trim() &&
85
- !!form.apiServerUrl.trim() &&
86
- !!form.namespace.trim() &&
87
- !!form.image.trim() &&
88
- !!apiToken.value.trim(),
89
- )
90
-
91
- // Why the Connect button is disabled, surfaced as a red hint next to it so a mandatory-field gap
92
- // is visible rather than a dead button. Lists the empty required fields by their on-screen label.
93
- const connectBlockedReason = computed(() => {
94
- if (canSave.value) return ''
95
- const missing: string[] = []
96
- if (!form.label.trim()) missing.push(t('settings.providerConnection.kubernetes.label'))
97
- if (!form.apiServerUrl.trim())
98
- missing.push(t('settings.providerConnection.kubernetes.apiServerUrl'))
99
- if (!form.namespace.trim()) missing.push(t('settings.providerConnection.kubernetes.namespace'))
100
- if (!form.image.trim()) missing.push(t('settings.providerConnection.kubernetes.image'))
101
- if (!apiToken.value.trim()) missing.push(t('settings.providerConnection.kubernetes.apiToken'))
102
- return t('settings.providerConnection.form.missingFields', { fields: missing.join(', ') })
103
- })
104
-
105
- function buildPayload(): { config: Record<string, unknown>; secrets: Record<string, string> } {
106
- const kubernetes: Record<string, unknown> = {
107
- label: form.label.trim(),
108
- apiServerUrl: form.apiServerUrl.trim(),
109
- namespace: form.namespace.trim(),
110
- image: form.image.trim(),
111
- }
112
- if (form.imageUi.trim()) kubernetes.imageUi = form.imageUi.trim()
113
- if (form.caCertPem.trim()) kubernetes.caCertPem = form.caCertPem.trim()
114
- if (form.insecureSkipTlsVerify) kubernetes.insecureSkipTlsVerify = true
115
- const port = Number(form.harnessPort)
116
- if (form.harnessPort.trim() && Number.isFinite(port)) kubernetes.harnessPort = port
117
- return {
118
- config: { kind: 'kubernetes', kubernetes },
119
- secrets: { [KUBERNETES_RUNNER_TOKEN_SECRET_KEY]: apiToken.value.trim() },
120
- }
121
- }
122
- </script>
123
-
124
- <template>
125
- <div class="rounded-lg border border-dashed border-slate-700 p-3 space-y-3">
126
- <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
127
- {{
128
- connection?.kind === 'kubernetes'
129
- ? t('settings.providerConnection.form.updateConfiguration')
130
- : t('settings.providerConnection.form.connect')
131
- }}
132
- </p>
133
-
134
- <UFormField :label="t('settings.providerConnection.kubernetes.label')">
135
- <UInput
136
- v-model="form.label"
137
- :placeholder="t('settings.providerConnection.kubernetes.labelPlaceholder')"
138
- />
139
- </UFormField>
140
-
141
- <UFormField
142
- :label="t('settings.providerConnection.kubernetes.apiServerUrl')"
143
- :help="t('settings.providerConnection.kubernetes.apiServerUrlHelp')"
144
- >
145
- <UInput v-model="form.apiServerUrl" class="font-mono" placeholder="https://10.0.0.1:6443" />
146
- </UFormField>
147
-
148
- <UFormField :label="t('settings.providerConnection.kubernetes.namespace')">
149
- <UInput v-model="form.namespace" class="font-mono" placeholder="cat-factory" />
150
- </UFormField>
151
-
152
- <UFormField
153
- :label="t('settings.providerConnection.kubernetes.image')"
154
- :help="t('settings.providerConnection.kubernetes.imageHelp')"
155
- >
156
- <UInput
157
- v-model="form.image"
158
- class="font-mono"
159
- placeholder="ghcr.io/acme/cat-factory-executor:latest"
160
- />
161
- </UFormField>
162
-
163
- <UFormField
164
- :label="
165
- t('settings.providerConnection.form.optionalLabel', {
166
- label: t('settings.providerConnection.kubernetes.imageUi'),
167
- })
168
- "
169
- >
170
- <UInput v-model="form.imageUi" class="font-mono" />
171
- </UFormField>
172
-
173
- <UFormField
174
- :label="t('settings.providerConnection.kubernetes.apiToken')"
175
- :help="t('settings.providerConnection.kubernetes.apiTokenHelp')"
176
- >
177
- <UInput v-model="apiToken" type="password" class="font-mono" />
178
- </UFormField>
179
-
180
- <UFormField
181
- :label="
182
- t('settings.providerConnection.form.optionalLabel', {
183
- label: t('settings.providerConnection.kubernetes.caCertPem'),
184
- })
185
- "
186
- :help="t('settings.providerConnection.kubernetes.caCertPemHelp')"
187
- >
188
- <UTextarea
189
- v-model="form.caCertPem"
190
- :rows="3"
191
- class="font-mono"
192
- placeholder="-----BEGIN CERTIFICATE-----"
193
- />
194
- </UFormField>
195
-
196
- <UFormField :help="t('settings.providerConnection.kubernetes.insecureSkipTlsVerifyHelp')">
197
- <UCheckbox
198
- v-model="form.insecureSkipTlsVerify"
199
- :label="t('settings.providerConnection.kubernetes.insecureSkipTlsVerify')"
200
- />
201
- </UFormField>
202
-
203
- <UFormField
204
- :label="
205
- t('settings.providerConnection.form.optionalLabel', {
206
- label: t('settings.providerConnection.kubernetes.harnessPort'),
207
- })
208
- "
209
- >
210
- <UInput v-model="form.harnessPort" type="number" class="font-mono" placeholder="8080" />
211
- </UFormField>
212
-
213
- <div v-if="supportsTest" class="flex items-center gap-2">
214
- <UButton
215
- color="neutral"
216
- variant="soft"
217
- size="sm"
218
- icon="i-lucide-plug-zap"
219
- :loading="testing"
220
- :disabled="!canSave"
221
- @click="emit('test', buildPayload())"
222
- >
223
- {{ t('settings.providerConnection.test.button') }}
224
- </UButton>
225
- <span v-if="testResult && testResult.ok" class="text-xs text-emerald-400">
226
- {{ testResult.message ?? t('settings.providerConnection.test.ok') }}
227
- </span>
228
- <span v-else-if="testResult" class="text-xs text-rose-400">
229
- {{ testResult.message ?? t('settings.providerConnection.test.failed') }}
230
- </span>
231
- </div>
232
-
233
- <div class="flex items-center justify-end gap-3">
234
- <p v-if="connectBlockedReason" class="flex-1 text-left text-xs text-rose-400">
235
- {{ connectBlockedReason }}
236
- </p>
237
- <UButton
238
- color="primary"
239
- size="sm"
240
- :loading="busy"
241
- :disabled="!canSave"
242
- @click="emit('save', buildPayload())"
243
- >
244
- {{
245
- connection?.kind === 'kubernetes'
246
- ? t('common.save')
247
- : t('settings.providerConnection.form.connect')
248
- }}
249
- </UButton>
250
- </div>
251
- </div>
252
- </template>