@cat-factory/app 0.111.3 → 0.113.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.
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { AgentState, ExecutionInstance } from '~/types/domain'
3
3
  import type { PipelineStep } from '~/types/execution'
4
- import { agentKindMeta, FOLLOW_UP_COMPANION_META } from '~/utils/catalog'
4
+ import { agentKindMeta, FOLLOW_UP_COMPANION_META, FORK_DECISION_META } from '~/utils/catalog'
5
5
  import {
6
6
  subtaskIconClass,
7
7
  gateCompanionFor,
@@ -68,6 +68,12 @@ function followUpLabel(step: PipelineStep): string {
68
68
  : t('pipeline.progress.followUp.allDecided')
69
69
  }
70
70
 
71
+ /** The active fork-decision phase status on a coder step (proposing / awaiting a choice). */
72
+ function forkPhase(step: PipelineStep): 'proposing' | 'awaiting_choice' | null {
73
+ const status = step.forkDecision?.status
74
+ return status === 'proposing' || status === 'awaiting_choice' ? status : null
75
+ }
76
+
71
77
  // --- restart from a step -----------------------------------------------------
72
78
  // Re-run the pipeline from a chosen step onward: the server resets that step +
73
79
  // every later step's iteration counters and re-drives a fresh run, keeping the
@@ -543,6 +549,40 @@ const ITEM_ICON: Record<string, string> = {
543
549
  </span>
544
550
  </button>
545
551
 
552
+ <!-- Implementation-fork decision phase (Coder step): a spinner while the proposer
553
+ surfaces approaches, then a clickable chip to choose one. -->
554
+ <button
555
+ v-if="forkPhase(s)"
556
+ type="button"
557
+ class="mt-3 flex w-full items-center gap-2 rounded-lg border border-dashed px-2.5 py-1.5 text-start transition hover:border-violet-400/60"
558
+ :class="
559
+ forkPhase(s) === 'awaiting_choice'
560
+ ? 'border-violet-500/50 bg-violet-500/10 followup-blink'
561
+ : 'border-slate-700/70 bg-slate-900/40'
562
+ "
563
+ :disabled="forkPhase(s) === 'proposing'"
564
+ @click="ui.openForkDecision(instance.id, i)"
565
+ >
566
+ <span
567
+ class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-violet-500/40 bg-violet-500/15"
568
+ >
569
+ <UIcon
570
+ :name="
571
+ forkPhase(s) === 'proposing' ? 'i-lucide-loader-circle' : FORK_DECISION_META.icon
572
+ "
573
+ class="h-3 w-3 text-violet-300"
574
+ :class="forkPhase(s) === 'proposing' ? 'animate-spin' : ''"
575
+ />
576
+ </span>
577
+ <span class="min-w-0 flex-1 truncate text-[12px] text-slate-300">
578
+ {{
579
+ forkPhase(s) === 'proposing'
580
+ ? t('pipeline.progress.forkDecision.proposing')
581
+ : t('pipeline.progress.forkDecision.choose')
582
+ }}
583
+ </span>
584
+ </button>
585
+
546
586
  <!-- reviewer gate folding/re-reviewing in the background: a working indicator,
547
587
  NOT a "Review & approve" gate (the human is summoned only if needed) -->
548
588
  <div
@@ -6,6 +6,7 @@
6
6
  // default; it cannot be deleted or un-defaulted (the backend enforces this too).
7
7
  import { computed, reactive, ref, watch } from 'vue'
8
8
  import type { RiskPolicy, RequirementConcernLevel } from '~/types/merge'
9
+ import type { StepGating } from '@cat-factory/contracts'
9
10
 
10
11
  const { t } = useI18n()
11
12
 
@@ -42,9 +43,32 @@ interface Draft {
42
43
  maxRequirementIterations: number
43
44
  maxRequirementConcernAllowed: RequirementConcernLevel
44
45
  autoMergeEnabled: boolean
46
+ // Implementation-fork decision gating (edited 0..100, stored 0..1); disabled ⇒ off in `auto`.
47
+ forkEnabled: boolean
48
+ forkMinComplexity: number
49
+ forkMinRisk: number
50
+ forkMinImpact: number
51
+ forkOnMissing: 'run' | 'skip'
45
52
  }
46
53
  const drafts = reactive<Record<string, Draft>>({})
47
54
 
55
+ // On-missing-estimate options for the fork gating group (fail toward asking / skipping).
56
+ const ON_MISSING_OPTIONS = computed<{ value: 'run' | 'skip'; label: string }[]>(() => [
57
+ { value: 'run', label: t('settings.riskPolicy.forkDecision.onMissing.run') },
58
+ { value: 'skip', label: t('settings.riskPolicy.forkDecision.onMissing.skip') },
59
+ ])
60
+
61
+ /** Build the `StepGating` payload for the fork-decision gate from a draft (or null when off). */
62
+ function forkGating(d: Draft): StepGating {
63
+ return {
64
+ enabled: d.forkEnabled,
65
+ minComplexity: d.forkMinComplexity / 100,
66
+ minRisk: d.forkMinRisk / 100,
67
+ minImpact: d.forkMinImpact / 100,
68
+ onMissingEstimate: d.forkOnMissing,
69
+ }
70
+ }
71
+
48
72
  function toDraft(p: RiskPolicy): Draft {
49
73
  return {
50
74
  name: p.name,
@@ -55,6 +79,11 @@ function toDraft(p: RiskPolicy): Draft {
55
79
  maxRequirementIterations: p.maxRequirementIterations,
56
80
  maxRequirementConcernAllowed: p.maxRequirementConcernAllowed,
57
81
  autoMergeEnabled: p.autoMergeEnabled,
82
+ forkEnabled: p.forkDecision?.enabled ?? false,
83
+ forkMinComplexity: Math.round((p.forkDecision?.minComplexity ?? 0.5) * 100),
84
+ forkMinRisk: Math.round((p.forkDecision?.minRisk ?? 0.4) * 100),
85
+ forkMinImpact: Math.round((p.forkDecision?.minImpact ?? 0.4) * 100),
86
+ forkOnMissing: p.forkDecision?.onMissingEstimate ?? 'run',
58
87
  }
59
88
  }
60
89
 
@@ -92,6 +121,7 @@ async function save(p: RiskPolicy) {
92
121
  maxRequirementIterations: d.maxRequirementIterations,
93
122
  maxRequirementConcernAllowed: d.maxRequirementConcernAllowed,
94
123
  autoMergeEnabled: d.autoMergeEnabled,
124
+ forkDecision: forkGating(d),
95
125
  })
96
126
  toast.add({
97
127
  title: t('settings.riskPolicy.toast.saved'),
@@ -146,6 +176,11 @@ const draft = reactive<Draft>({
146
176
  maxRequirementIterations: 6,
147
177
  maxRequirementConcernAllowed: 'none',
148
178
  autoMergeEnabled: true,
179
+ forkEnabled: false,
180
+ forkMinComplexity: 50,
181
+ forkMinRisk: 40,
182
+ forkMinImpact: 40,
183
+ forkOnMissing: 'run',
149
184
  })
150
185
 
151
186
  async function create() {
@@ -161,6 +196,7 @@ async function create() {
161
196
  maxRequirementIterations: draft.maxRequirementIterations,
162
197
  maxRequirementConcernAllowed: draft.maxRequirementConcernAllowed,
163
198
  autoMergeEnabled: draft.autoMergeEnabled,
199
+ forkDecision: forkGating(draft),
164
200
  })
165
201
  draft.name = ''
166
202
  draft.autoMergeEnabled = true
@@ -305,6 +341,61 @@ async function create() {
305
341
  </label>
306
342
  </div>
307
343
 
344
+ <!-- Implementation-fork decision gate: propose materially different approaches before the
345
+ Coder writes code (in `auto` tri-state, gated on the task estimate). -->
346
+ <div class="mt-3 rounded-md border border-slate-800 bg-slate-900/40 p-3">
347
+ <USwitch
348
+ v-model="drafts[p.id]!.forkEnabled"
349
+ size="sm"
350
+ :label="t('settings.riskPolicy.forkDecision.label')"
351
+ :description="t('settings.riskPolicy.forkDecision.hint')"
352
+ />
353
+ <div v-if="drafts[p.id]!.forkEnabled" class="mt-3 grid grid-cols-2 gap-3 sm:grid-cols-4">
354
+ <label class="block">
355
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
356
+ {{ t('settings.riskPolicy.forkDecision.minComplexity') }}
357
+ </span>
358
+ <UInput
359
+ v-model.number="drafts[p.id]!.forkMinComplexity"
360
+ type="number"
361
+ size="sm"
362
+ :min="0"
363
+ :max="100"
364
+ />
365
+ </label>
366
+ <label class="block">
367
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
368
+ {{ t('settings.riskPolicy.forkDecision.minRisk') }}
369
+ </span>
370
+ <UInput
371
+ v-model.number="drafts[p.id]!.forkMinRisk"
372
+ type="number"
373
+ size="sm"
374
+ :min="0"
375
+ :max="100"
376
+ />
377
+ </label>
378
+ <label class="block">
379
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
380
+ {{ t('settings.riskPolicy.forkDecision.minImpact') }}
381
+ </span>
382
+ <UInput
383
+ v-model.number="drafts[p.id]!.forkMinImpact"
384
+ type="number"
385
+ size="sm"
386
+ :min="0"
387
+ :max="100"
388
+ />
389
+ </label>
390
+ <label class="block">
391
+ <span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
392
+ {{ t('settings.riskPolicy.forkDecision.onMissingLabel') }}
393
+ </span>
394
+ <USelect v-model="drafts[p.id]!.forkOnMissing" :items="ON_MISSING_OPTIONS" size="sm" />
395
+ </label>
396
+ </div>
397
+ </div>
398
+
308
399
  <div class="mt-3 flex items-center justify-between gap-3">
309
400
  <USwitch
310
401
  v-model="drafts[p.id]!.autoMergeEnabled"
@@ -403,6 +494,11 @@ async function create() {
403
494
  size="sm"
404
495
  :label="t('settings.riskPolicy.field.autoMerge')"
405
496
  />
497
+ <USwitch
498
+ v-model="draft.forkEnabled"
499
+ size="sm"
500
+ :label="t('settings.riskPolicy.forkDecision.label')"
501
+ />
406
502
  <UButton
407
503
  color="primary"
408
504
  size="sm"
@@ -56,6 +56,7 @@ const routes = reactive<Record<NotificationType, SlackRoute>>({
56
56
  visual_confirmation_ready: { enabled: false, channel: '' },
57
57
  human_review: { enabled: false, channel: '' },
58
58
  followup_pending: { enabled: false, channel: '' },
59
+ fork_decision_pending: { enabled: false, channel: '' },
59
60
  initiative: { enabled: false, channel: '' },
60
61
  })
61
62
  const mentionsEnabled = ref(false)
@@ -0,0 +1,29 @@
1
+ import { chooseForkContract, getForkDecisionContract } from '@cat-factory/contracts'
2
+ import type { ApiContext } from './context'
3
+
4
+ /**
5
+ * The implementation-fork decision phase: before the Coder writes code the read-only
6
+ * proposer surfaces materially different approaches on the run's coder step and the run
7
+ * parks. These endpoints read the surfaced approaches and record the human's choice (a
8
+ * proposed fork or their own free-text approach); choosing re-runs the Coder with the chosen
9
+ * approach folded in. The read returns null when no coder step carries fork state.
10
+ */
11
+ export function forkDecisionApi({ send, ws }: ApiContext) {
12
+ return {
13
+ // The live fork-decision state for a run (null when no coder step carries one).
14
+ getForkDecision: (workspaceId: string, executionId: string) =>
15
+ send(getForkDecisionContract, { pathPrefix: ws(workspaceId), pathParams: { executionId } }),
16
+
17
+ // Choose an implementation approach — a proposed fork id or a custom approach (+ note).
18
+ chooseFork: (
19
+ workspaceId: string,
20
+ executionId: string,
21
+ body: { forkId?: string | null; custom?: string | null; note?: string | null },
22
+ ) =>
23
+ send(chooseForkContract, {
24
+ pathPrefix: ws(workspaceId),
25
+ pathParams: { executionId },
26
+ body,
27
+ }),
28
+ }
29
+ }
@@ -8,6 +8,7 @@ import { boardApi } from './api/board'
8
8
  import { documentsApi } from './api/documents'
9
9
  import { executionApi } from './api/execution'
10
10
  import { followUpsApi } from './api/followUps'
11
+ import { forkDecisionApi } from './api/forkDecision'
11
12
  import { fragmentsApi } from './api/fragments'
12
13
  import { githubApi } from './api/github'
13
14
  import { humanReviewApi } from './api/humanReview'
@@ -108,6 +109,7 @@ export function useApi() {
108
109
  ...tasksApi(ctx),
109
110
  ...reviewsApi(ctx),
110
111
  ...followUpsApi(ctx),
112
+ ...forkDecisionApi(ctx),
111
113
  ...humanTestApi(ctx),
112
114
  ...visualConfirmApi(ctx),
113
115
  ...humanReviewApi(ctx),
@@ -0,0 +1,84 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type { ForkDecisionStepState } from '~/types/execution'
4
+ import { useApi } from '~/composables/useApi'
5
+ import { useWorkspaceStore } from '~/stores/workspace'
6
+ import { useExecutionStore } from '~/stores/execution'
7
+
8
+ /**
9
+ * The implementation-fork decision action surface. The live fork state lives on the run's
10
+ * Coder step (`step.forkDecision`) and is kept fresh by the execution stream, so the window
11
+ * reads it straight off the execution store — this store only wraps the `choose` action (and
12
+ * a warm-up `load`), tracks the in-flight state so the window can disable its controls, and
13
+ * reflects the returned state back onto the execution store so the UI updates immediately even
14
+ * before the stream echoes the change. Keyed by executionId, mirroring the follow-ups store.
15
+ */
16
+ export const useForkDecisionStore = defineStore('forkDecision', () => {
17
+ const api = useApi()
18
+ const workspace = useWorkspaceStore()
19
+ const execution = useExecutionStore()
20
+
21
+ /** True while a choose call is in flight (drives the Choose button spinner / disabled state). */
22
+ const choosing = ref(false)
23
+ /** The last error message from an action, surfaced inline; cleared on the next action. */
24
+ const error = ref<string | null>(null)
25
+
26
+ /**
27
+ * Reflect an authoritative fork-decision state onto the run's Coder step. A pipeline may
28
+ * carry more than one `coder` step, so target the step this decision is about rather than
29
+ * the first one that happens to hold fork state: prefer the step that is still live
30
+ * (proposing / awaiting the choice / answering), then the current step, and only then fall
31
+ * back to the first step carrying fork state. The stream corrects any mismatch, but this
32
+ * keeps the immediate optimistic echo on the right step.
33
+ */
34
+ function reflect(executionId: string, state: ForkDecisionStepState | null): void {
35
+ if (!state) return
36
+ const instance = execution.getInstance(executionId)
37
+ if (!instance) return
38
+ const isLive = (s: (typeof instance.steps)[number]) =>
39
+ s.agentKind === 'coder' &&
40
+ (s.forkDecision?.status === 'awaiting_choice' ||
41
+ s.forkDecision?.status === 'answering' ||
42
+ s.forkDecision?.status === 'proposing')
43
+ const current = instance.steps[instance.currentStep]
44
+ const step =
45
+ instance.steps.find(isLive) ??
46
+ (current?.agentKind === 'coder' && current.forkDecision ? current : undefined) ??
47
+ instance.steps.find((s) => s.forkDecision)
48
+ if (step) step.forkDecision = state
49
+ }
50
+
51
+ /** Warm the live state from the GET (the stream also keeps it fresh). Best-effort. */
52
+ async function load(executionId: string): Promise<void> {
53
+ error.value = null
54
+ try {
55
+ const state = await api.getForkDecision(workspace.requireId(), executionId)
56
+ reflect(executionId, state as ForkDecisionStepState | null)
57
+ } catch (e) {
58
+ error.value = e instanceof Error ? e.message : 'Failed to load'
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Choose an implementation approach: a proposed fork id OR a custom free-text approach
64
+ * (with an optional steering note). The Coder then re-runs with the choice folded in.
65
+ */
66
+ async function choose(
67
+ executionId: string,
68
+ choice: { forkId?: string; custom?: string; note?: string },
69
+ ): Promise<void> {
70
+ error.value = null
71
+ choosing.value = true
72
+ try {
73
+ const state = await api.chooseFork(workspace.requireId(), executionId, choice)
74
+ reflect(executionId, state as ForkDecisionStepState)
75
+ } catch (e) {
76
+ error.value = e instanceof Error ? e.message : 'Failed to choose'
77
+ throw e
78
+ } finally {
79
+ choosing.value = false
80
+ }
81
+ }
82
+
83
+ return { choosing, error, load, choose }
84
+ })
package/app/stores/ui.ts CHANGED
@@ -809,6 +809,31 @@ export const useUiStore = defineStore('ui', () => {
809
809
  stepIndex: idx,
810
810
  }
811
811
  }
812
+ // Open the implementation-fork decision window for a run's coder step (from the inspector /
813
+ // pipeline chip / `fork_decision_pending` notification). Resolves the coder step index from
814
+ // the run when not given, preferring the step parked awaiting a choice.
815
+ function openForkDecision(instanceId: string, stepIndex: number | null = null) {
816
+ const execution = useExecutionStore()
817
+ const instance = execution.getInstance(instanceId)
818
+ if (!instance) return
819
+ const resolveIdx = () => {
820
+ const awaiting = instance.steps.findIndex(
821
+ (s) => s.agentKind === 'coder' && s.forkDecision?.status === 'awaiting_choice',
822
+ )
823
+ if (awaiting >= 0) return awaiting
824
+ const current = instance.steps[instance.currentStep]
825
+ if (current?.agentKind === 'coder' && current.forkDecision) return instance.currentStep
826
+ return instance.steps.findIndex((s) => s.agentKind === 'coder' && s.forkDecision)
827
+ }
828
+ const idx = stepIndex ?? resolveIdx()
829
+ if (idx < 0) return
830
+ resultView.value = {
831
+ view: 'fork-decision',
832
+ blockId: instance.blockId,
833
+ instanceId,
834
+ stepIndex: idx,
835
+ }
836
+ }
812
837
  function closeResultView() {
813
838
  resultView.value = null
814
839
  }
@@ -1006,6 +1031,7 @@ export const useUiStore = defineStore('ui', () => {
1006
1031
  openInitiativeTracker,
1007
1032
  openInitiativePlanning,
1008
1033
  openFollowUps,
1034
+ openForkDecision,
1009
1035
  closeRequirementReview,
1010
1036
  openStepDetail,
1011
1037
  closeStepDetail,
@@ -29,6 +29,7 @@ export type {
29
29
  Block,
30
30
  PullRequestRef,
31
31
  ReferenceRepo,
32
+ AprioriBranch,
32
33
  CloudProvider,
33
34
  InstanceSize,
34
35
  ProvisionType,
@@ -33,6 +33,11 @@ export type {
33
33
  FollowUpItemStatus,
34
34
  FollowUpItem,
35
35
  FollowUpsStepState,
36
+ ForkOption,
37
+ ForkChatMessage,
38
+ ForkDecisionStatus,
39
+ ForkChoice,
40
+ ForkDecisionStepState,
36
41
  GateFailingCheck,
37
42
  GateAttempt,
38
43
  GateStepState,
@@ -708,6 +708,17 @@ export const FOLLOW_UP_COMPANION_META = {
708
708
  color: '#f472b6',
709
709
  }
710
710
 
711
+ /**
712
+ * Display metadata for the implementation-fork decision phase on a Coder step (a per-step
713
+ * phase, not an agent kind of its own — the read-only `fork-proposer` is never a palette
714
+ * block). Drives the fork-decision window header + the pipeline phase chip.
715
+ */
716
+ export const FORK_DECISION_META = {
717
+ label: 'Implementation-fork decision',
718
+ icon: 'i-lucide-git-fork',
719
+ color: '#a78bfa',
720
+ }
721
+
711
722
  /**
712
723
  * Whether a Coder step has the Follow-up companion enabled, given the pipeline's per-step
713
724
  * `followUps` toggle at index `i`. Enabled by default on a `coder` step (only `false`
@@ -438,6 +438,18 @@
438
438
  "createFailed": "Richtlinie konnte nicht erstellt werden",
439
439
  "defaultFailed": "Standard konnte nicht festgelegt werden",
440
440
  "deleteFailed": "Richtlinie konnte nicht gelöscht werden"
441
+ },
442
+ "forkDecision": {
443
+ "label": "Implementierungs-Weichenentscheidung",
444
+ "hint": "Im Auto-Modus grundlegend verschiedene Ansätze vorschlagen (und für eine Wahl pausieren), wenn die Aufgabenschätzung einen Schwellenwert erreicht.",
445
+ "minComplexity": "Min. Komplexität",
446
+ "minRisk": "Min. Risiko",
447
+ "minImpact": "Min. Auswirkung",
448
+ "onMissingLabel": "Keine Schätzung",
449
+ "onMissing": {
450
+ "run": "Trotzdem vorschlagen",
451
+ "skip": "Überspringen"
452
+ }
441
453
  }
442
454
  },
443
455
  "observabilityConnection": {
@@ -1161,7 +1173,8 @@
1161
1173
  "companionOf": "{label} (Begleiter)",
1162
1174
  "merged": "Gemergt",
1163
1175
  "open": "Öffnen",
1164
- "mergePr": "PR mergen"
1176
+ "mergePr": "PR mergen",
1177
+ "chooseApproach": "Ansatz wählen"
1165
1178
  },
1166
1179
  "structure": {
1167
1180
  "title": "Struktur",
@@ -1227,6 +1240,20 @@
1227
1240
  "involvedServicesEmpty": "Keine verbundenen Services. Verbinde Services am Service-Frame, um sie hier auszuwählen.",
1228
1241
  "involvedServiceStale": "Nicht mehr mit dem Service dieser Aufgabe verbunden; wird bei der nächsten Änderung entfernt."
1229
1242
  },
1243
+ "aprioriBranches": {
1244
+ "title": "Vorhandene Branches",
1245
+ "mode": {
1246
+ "reference": "Referenz",
1247
+ "working": "Arbeitsbranch"
1248
+ },
1249
+ "remove": "{branch} entfernen",
1250
+ "searchPlaceholder": "Vorhandenen Branch hinzufügen…",
1251
+ "connectFirst": "Verbinde GitHub, um vorhandene Branches hinzuzufügen.",
1252
+ "protectedWarning": "{branch} ist ein geschützter Branch. Pushes aus dem Lauf werden möglicherweise abgelehnt.",
1253
+ "multiRepoHint": "Der Arbeitsmodus ist nicht verfügbar, solange diese Aufgabe mehr als einen Service umfasst.",
1254
+ "frozenHint": "Der Arbeitsbranch ist gesperrt, da für diese Aufgabe bereits ein Pull Request existiert.",
1255
+ "hint": "Übergib dieser Aufgabe vorhandene Branches ihres Repositorys. Ein Referenzbranch ist schreibgeschützter Kontext, den die Agenten einsehen dürfen; im einzelnen Arbeitsbranch baut der Lauf weiter, statt einen neuen Branch anzulegen."
1256
+ },
1230
1257
  "referenceRepos": {
1231
1258
  "title": "Referenz-Repositories",
1232
1259
  "remove": "{repo} entfernen",
@@ -1649,7 +1676,8 @@
1649
1676
  "human_review": "Als gelesen markieren",
1650
1677
  "followup_pending": "Als gelesen markieren",
1651
1678
  "initiative": "Als gelesen markieren",
1652
- "markRead": "Als gelesen markieren"
1679
+ "markRead": "Als gelesen markieren",
1680
+ "fork_decision_pending": "Als gelesen markieren"
1653
1681
  }
1654
1682
  },
1655
1683
  "aiProvidersBanner": {
@@ -2979,7 +3007,11 @@
2979
3007
  "subtasksInProgress": "· {count} in Arbeit",
2980
3008
  "clickToRead": "Klicken, um die Ausgabe dieses Agenten zu lesen",
2981
3009
  "reviewApprove": "Vorschlag von {agent} prüfen & freigeben",
2982
- "resolve": "Klären: {question}"
3010
+ "resolve": "Klären: {question}",
3011
+ "forkDecision": {
3012
+ "proposing": "Ansätze werden vorgeschlagen…",
3013
+ "choose": "Ansatz wählen"
3014
+ }
2983
3015
  },
2984
3016
  "health": {
2985
3017
  "title": "Pipeline-Zustand",
@@ -4551,5 +4583,34 @@
4551
4583
  "palette": {
4552
4584
  "hint": "Klicke auf einen Agenten, um ihn an die Pipeline anzuhängen.",
4553
4585
  "customAgents": "Benutzerdefinierte Agenten"
4586
+ },
4587
+ "forkDecision": {
4588
+ "title": "Implementierungsansatz wählen",
4589
+ "titleWithBlock": "Ansatz für {title} wählen",
4590
+ "subtitle": "Grundlegend verschiedene Wege, diese Aufgabe umzusetzen, bevor Code geschrieben wird.",
4591
+ "seam": "Betroffene Stelle:",
4592
+ "recommended": "Empfohlen",
4593
+ "riskNotes": "Risiko:",
4594
+ "noteLabel": "Hinweis (optional)",
4595
+ "notePlaceholder": "Was die Umsetzung berücksichtigen soll…",
4596
+ "choose": "Diesen Ansatz verwenden",
4597
+ "proposing": {
4598
+ "title": "Ansätze werden ermittelt…",
4599
+ "hint": "Der Code wird gelesen, um die grundlegend verschiedenen Umsetzungswege zu finden."
4600
+ },
4601
+ "singlePath": {
4602
+ "title": "Ein klarer Ansatz"
4603
+ },
4604
+ "chosen": {
4605
+ "title": "Ansatz gewählt",
4606
+ "note": "Hinweis: {note}"
4607
+ },
4608
+ "custom": {
4609
+ "title": "Eigenen Ansatz eingeben",
4610
+ "placeholder": "Beschreibe, wie dies umgesetzt werden soll…"
4611
+ },
4612
+ "empty": {
4613
+ "title": "Nichts zu entscheiden"
4614
+ }
4554
4615
  }
4555
4616
  }
@@ -911,7 +911,8 @@
911
911
  "companionOf": "{label} (companion)",
912
912
  "merged": "Merged",
913
913
  "open": "Open",
914
- "mergePr": "Merge PR"
914
+ "mergePr": "Merge PR",
915
+ "chooseApproach": "Choose approach"
915
916
  },
916
917
  "structure": {
917
918
  "title": "Structure",
@@ -977,6 +978,20 @@
977
978
  "involvedServicesEmpty": "No connected services. Connect services on the service frame to select them here.",
978
979
  "involvedServiceStale": "No longer connected to this task's service; it is dropped on the next change."
979
980
  },
981
+ "aprioriBranches": {
982
+ "title": "Existing branches",
983
+ "mode": {
984
+ "reference": "Reference",
985
+ "working": "Working"
986
+ },
987
+ "remove": "Remove {branch}",
988
+ "searchPlaceholder": "Add an existing branch…",
989
+ "connectFirst": "Connect GitHub to add existing branches.",
990
+ "protectedWarning": "{branch} is a protected branch. Pushes from the run may be rejected.",
991
+ "multiRepoHint": "Working mode is unavailable while this task involves more than one service.",
992
+ "frozenHint": "The working branch is locked because a pull request already exists for this task.",
993
+ "hint": "Hand this task pre-existing branches of its repository. A reference branch is read-only context the agents may inspect; the single working branch is where the run keeps building instead of a new branch."
994
+ },
980
995
  "referenceRepos": {
981
996
  "title": "Reference repositories",
982
997
  "remove": "Remove {repo}",
@@ -1539,7 +1554,8 @@
1539
1554
  "human_review": "Mark read",
1540
1555
  "followup_pending": "Mark read",
1541
1556
  "initiative": "Mark read",
1542
- "markRead": "Mark read"
1557
+ "markRead": "Mark read",
1558
+ "fork_decision_pending": "Mark read"
1543
1559
  }
1544
1560
  },
1545
1561
  "aiProvidersBanner": {
@@ -2213,6 +2229,18 @@
2213
2229
  "createFailed": "Could not create policy",
2214
2230
  "defaultFailed": "Could not set default",
2215
2231
  "deleteFailed": "Could not delete policy"
2232
+ },
2233
+ "forkDecision": {
2234
+ "label": "Implementation-fork decision",
2235
+ "hint": "In auto mode, propose materially different approaches (and pause for a choice) when the task estimate meets a threshold.",
2236
+ "minComplexity": "Min complexity",
2237
+ "minRisk": "Min risk",
2238
+ "minImpact": "Min impact",
2239
+ "onMissingLabel": "No estimate",
2240
+ "onMissing": {
2241
+ "run": "Propose anyway",
2242
+ "skip": "Skip"
2243
+ }
2216
2244
  }
2217
2245
  },
2218
2246
  "observabilityConnection": {
@@ -3291,7 +3319,11 @@
3291
3319
  "subtasksInProgress": "· {count} in progress",
3292
3320
  "clickToRead": "Click to read this agent's output",
3293
3321
  "reviewApprove": "Review & approve {agent}'s proposal",
3294
- "resolve": "Resolve: {question}"
3322
+ "resolve": "Resolve: {question}",
3323
+ "forkDecision": {
3324
+ "proposing": "Proposing approaches…",
3325
+ "choose": "Choose an approach"
3326
+ }
3295
3327
  },
3296
3328
  "health": {
3297
3329
  "title": "Pipeline health",
@@ -4668,5 +4700,34 @@
4668
4700
  "hint": "Optionally provision this environment now to test the recipe.",
4669
4701
  "run": "Trial provision"
4670
4702
  }
4703
+ },
4704
+ "forkDecision": {
4705
+ "title": "Choose an implementation approach",
4706
+ "titleWithBlock": "Choose an approach for {title}",
4707
+ "subtitle": "Materially different ways to implement this task, before any code is written.",
4708
+ "seam": "Where it lands:",
4709
+ "recommended": "Recommended",
4710
+ "riskNotes": "Risk:",
4711
+ "noteLabel": "Steering note (optional)",
4712
+ "notePlaceholder": "Anything the implementer should keep in mind…",
4713
+ "choose": "Use this approach",
4714
+ "proposing": {
4715
+ "title": "Surfacing approaches…",
4716
+ "hint": "Reading the code to find the materially different ways to build this."
4717
+ },
4718
+ "singlePath": {
4719
+ "title": "One clear approach"
4720
+ },
4721
+ "chosen": {
4722
+ "title": "Approach chosen",
4723
+ "note": "Note: {note}"
4724
+ },
4725
+ "custom": {
4726
+ "title": "Enter your own approach",
4727
+ "placeholder": "Describe how you want this implemented…"
4728
+ },
4729
+ "empty": {
4730
+ "title": "Nothing to decide"
4731
+ }
4671
4732
  }
4672
4733
  }