@cat-factory/app 0.224.0 → 0.224.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@ import BinaryOutputReport from '~/components/binaryOutput/BinaryOutputReport.vue
13
13
  import EnvironmentStatusPanel from '~/components/environments/EnvironmentStatusPanel.vue'
14
14
  import FrontendBindingsResolved from '~/components/panels/inspector/FrontendBindingsResolved.vue'
15
15
  import { UI_TESTER_AGENT_KIND } from '@cat-factory/contracts'
16
+ import type { GateApprovalRefusal } from '@cat-factory/contracts'
16
17
  import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
17
18
  import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
18
19
  import StepExecutionHistory from '~/components/board/StepExecutionHistory.vue'
@@ -227,6 +228,10 @@ const {
227
228
  draftProposal,
228
229
  rejectArmed,
229
230
  canRequestChanges,
231
+ quorum: gateQuorum,
232
+ viewerHasApproved,
233
+ approvalWouldClearGate,
234
+ refusal: gateRefusal,
230
235
  onProseClick,
231
236
  addDraftComment,
232
237
  cancelDraft,
@@ -241,6 +246,25 @@ const {
241
246
  reject,
242
247
  } = approval
243
248
 
249
+ /**
250
+ * Why the gate refuses this viewer, worded for them. An exhaustive Record over the shared refusal
251
+ * vocabulary with LITERAL keys, so the typed-message-key check sees them and a new refusal reason
252
+ * fails the typecheck instead of rendering as a blank line under a disabled button.
253
+ */
254
+ const GATE_REFUSAL_KEYS: Record<GateApprovalRefusal, string> = {
255
+ not_a_gate_approver: 'panels.stepDetail.notAnApprover',
256
+ gate_approver_identity_required: 'panels.stepDetail.approverIdentityRequired',
257
+ }
258
+
259
+ /**
260
+ * Whether "approve with corrections" is offered RIGHT NOW. Two independent reasons withhold it,
261
+ * and they are kept apart because their remedies differ: an output that is a rendering can never
262
+ * be edited here, while an unmet quorum only means not yet: this viewer's approval is not the
263
+ * one that clears the gate, and an edit under it would move the artifact beneath the approvals
264
+ * already recorded. Each states itself below rather than the button quietly vanishing.
265
+ */
266
+ const proposalEditableNow = computed(() => proposalEditable.value && approvalWouldClearGate.value)
267
+
244
268
  const resolvingCap = ref(false)
245
269
  async function resolveCompanionCap(choice: IterationCapChoice) {
246
270
  if (!ctx.value || !approvalId.value || resolvingCap.value) return
@@ -692,6 +716,30 @@ async function copyOutput() {
692
716
  <p class="mt-1 text-[12px] text-slate-400">
693
717
  {{ editing ? t('panels.stepDetail.editHint') : t('panels.stepDetail.reviewHint') }}
694
718
  </p>
719
+ <!-- The gate's configured POLICY, when it has one. Both lines exist because an
720
+ approve on such a gate legitimately may not advance the run: without the tally, a
721
+ correctly-recorded approval is indistinguishable from a call that failed, and
722
+ without the refusal a person would press a button the server answers 403. -->
723
+ <p
724
+ v-if="gateQuorum"
725
+ class="mt-1 text-[12px] text-amber-300/90"
726
+ data-testid="gate-quorum"
727
+ >
728
+ {{
729
+ t('panels.stepDetail.quorumProgress', {
730
+ recorded: gateQuorum.recorded,
731
+ required: gateQuorum.required,
732
+ })
733
+ }}
734
+ <span v-if="viewerHasApproved">{{ t('panels.stepDetail.quorumYours') }}</span>
735
+ </p>
736
+ <p
737
+ v-if="gateRefusal"
738
+ class="mt-1 text-[12px] text-slate-400"
739
+ data-testid="gate-not-approver"
740
+ >
741
+ {{ t(GATE_REFUSAL_KEYS[gateRefusal]) }}
742
+ </p>
695
743
  </div>
696
744
 
697
745
  <div class="flex-1 space-y-3 overflow-auto overscroll-contain px-4 py-3">
@@ -820,27 +868,36 @@ async function copyOutput() {
820
868
  size="sm"
821
869
  icon="i-lucide-check"
822
870
  block
823
- :disabled="rejectArmed"
871
+ :disabled="rejectArmed || !!gateRefusal"
824
872
  :loading="submitting"
825
873
  @click="approve"
826
874
  >
827
875
  {{ t('panels.stepDetail.approveAndProceed') }}
828
876
  </UButton>
829
877
  <UButton
830
- v-if="proposalEditable"
878
+ v-if="proposalEditableNow"
831
879
  color="primary"
832
880
  variant="soft"
833
881
  size="sm"
834
882
  icon="i-lucide-pencil"
835
883
  block
836
- :disabled="rejectArmed || submitting"
884
+ :disabled="rejectArmed || submitting || !!gateRefusal"
837
885
  @click="startEditing"
838
886
  >
839
887
  {{ t('panels.stepDetail.approveWithCorrections') }}
840
888
  </UButton>
841
- <p v-else class="text-[10px] text-slate-500" data-testid="step-rendered-output-note">
889
+ <p
890
+ v-else-if="!proposalEditable"
891
+ class="text-[10px] text-slate-500"
892
+ data-testid="step-rendered-output-note"
893
+ >
842
894
  {{ t('panels.stepDetail.renderedOutputNote') }}
843
895
  </p>
896
+ <!-- Withheld only until the quorum is one approval away, so it says so rather than
897
+ leaving a reviewer to wonder where the affordance went. -->
898
+ <p v-else class="text-[10px] text-slate-500" data-testid="step-quorum-edit-locked">
899
+ {{ t('panels.stepDetail.quorumEditLocked') }}
900
+ </p>
844
901
 
845
902
  <!-- destructive: a two-step inline confirm instead of a native dialog -->
846
903
  <div
@@ -881,7 +938,7 @@ async function copyOutput() {
881
938
  icon="i-lucide-rotate-ccw"
882
939
  class="flex-1"
883
940
  data-testid="step-request-changes"
884
- :disabled="!canRequestChanges"
941
+ :disabled="!canRequestChanges || !!gateRefusal"
885
942
  :loading="submitting"
886
943
  @click="requestChanges"
887
944
  >
@@ -893,7 +950,7 @@ async function copyOutput() {
893
950
  size="sm"
894
951
  icon="i-lucide-ban"
895
952
  class="flex-1"
896
- :disabled="submitting"
953
+ :disabled="submitting || !!gateRefusal"
897
954
  @click="armReject"
898
955
  >
899
956
  {{ t('panels.stepDetail.reject') }}
@@ -0,0 +1,187 @@
1
+ <script setup lang="ts">
2
+ // One step's GATE configuration in the pipeline builder: who may clear its human approval gate,
3
+ // how many of them must, and the parameters of the registered gate the step's kind runs.
4
+ //
5
+ // Two halves with deliberately different sources, rendered together because they are one question
6
+ // to the author ("how does this checkpoint behave?"):
7
+ //
8
+ // - the approval policy is PLATFORM-typed (`StepGateConfig.approvers` / `minApprovals`), so it
9
+ // gets purpose-built controls and the shared rules from `@cat-factory/contracts` decide what
10
+ // counts as configured;
11
+ // - the gate's own parameters are DECLARED BY THE GATE (`gateConfigForms` on the snapshot) and
12
+ // rendered through the shared `DescriptorFields`, so a deployment's gate gets an authoring
13
+ // form from its registration alone and this component learns nothing about it.
14
+ //
15
+ // Extracted rather than inlined into `PipelineBuilder.vue` for the size rule (split along a seam,
16
+ // never grow the ratchet) and because the approver controls need a roster load this component can
17
+ // own on its own terms.
18
+ import { computed, ref, watch } from 'vue'
19
+ import { MAX_GATE_APPROVALS, requiredGateApprovals } from '@cat-factory/contracts'
20
+ import type { DescriptorFieldValues, StepGateConfig, WorkspaceRole } from '~/types/domain'
21
+ import DescriptorFields from '~/components/common/DescriptorFields.vue'
22
+
23
+ const props = defineProps<{
24
+ /** The step's index in the draft, for the store patch. */
25
+ index: number
26
+ /** Whether the step carries a human approval gate (the approval half renders only then). */
27
+ gated: boolean
28
+ /** The step's agent kind, for looking up its registered gate's declared parameters. */
29
+ kind: string
30
+ }>()
31
+
32
+ const { t } = useI18n()
33
+ const pipelines = usePipelinesStore()
34
+ const workspace = useWorkspaceStore()
35
+ const members = useWorkspaceMembersStore()
36
+
37
+ const config = computed<StepGateConfig>(() => pipelines.draftGateConfig(props.index) ?? {})
38
+
39
+ /**
40
+ * The roles a gate may name. `viewer` is deliberately absent: the workspace RBAC write floor
41
+ * refuses a viewer's resolution before the policy is ever consulted, so offering it would let an
42
+ * author configure an approver who can never approve.
43
+ */
44
+ const APPROVER_ROLES: readonly WorkspaceRole[] = ['admin', 'member']
45
+
46
+ const ROLE_LABEL_KEYS: Record<'admin' | 'member', string> = {
47
+ admin: 'pipeline.gateConfig.roleAdmin',
48
+ member: 'pipeline.gateConfig.roleMember',
49
+ }
50
+
51
+ const requiredApprovals = computed(() => requiredGateApprovals(config.value))
52
+
53
+ // The roster backs the named-approver picker. Loaded lazily the first time this panel renders (it
54
+ // is not on the board snapshot) and only when the caller has a workspace — every resolved role may
55
+ // read it, so this is not an admin-only affordance.
56
+ const rosterLoaded = ref(false)
57
+ watch(
58
+ () => [props.gated, workspace.workspaceId] as const,
59
+ async ([gated, workspaceId]) => {
60
+ if (!gated || !workspaceId || rosterLoaded.value) return
61
+ rosterLoaded.value = true
62
+ await members.load(workspaceId)
63
+ },
64
+ { immediate: true },
65
+ )
66
+
67
+ const memberOptions = computed(() =>
68
+ members.members.map((m) => ({ value: m.userId, label: m.name || m.email || m.userId })),
69
+ )
70
+
71
+ function toggleRole(role: WorkspaceRole, on: boolean) {
72
+ const roles = new Set<WorkspaceRole>(config.value.approvers?.roles ?? [])
73
+ if (on) roles.add(role)
74
+ else roles.delete(role)
75
+ patchApprovers({ roles: [...roles] })
76
+ }
77
+
78
+ function setNamedApprovers(userIds: string[]) {
79
+ patchApprovers({ userIds })
80
+ }
81
+
82
+ /**
83
+ * Write one axis of the approver policy back, keeping the other. Empty arrays are dropped rather
84
+ * than stored: `{ roles: [] }` names nobody, and a policy that names nobody would refuse every
85
+ * approver and park the run forever — so "no rule" has to persist as an ABSENT rule.
86
+ */
87
+ function patchApprovers(patch: { roles?: WorkspaceRole[]; userIds?: string[] }) {
88
+ const merged = { ...config.value.approvers, ...patch }
89
+ const approvers: NonNullable<StepGateConfig['approvers']> = {}
90
+ if (merged.roles?.length) approvers.roles = merged.roles
91
+ if (merged.userIds?.length) approvers.userIds = merged.userIds
92
+ pipelines.patchDraftGateConfig(props.index, {
93
+ approvers: Object.keys(approvers).length ? approvers : undefined,
94
+ })
95
+ }
96
+
97
+ function setRequiredApprovals(raw: string) {
98
+ const parsed = Number.parseInt(raw, 10)
99
+ const bounded = Number.isFinite(parsed) ? Math.min(Math.max(parsed, 1), MAX_GATE_APPROVALS) : 1
100
+ // The store drops a value of 1 (the default), so an author who types the default back persists
101
+ // nothing — the same normalization every other per-step option follows.
102
+ pipelines.patchDraftGateConfig(props.index, { minApprovals: bounded })
103
+ }
104
+
105
+ /** The parameters the registered gate for this step's kind declares, if any. */
106
+ const gateFields = computed(
107
+ () => pipelines.gateConfigForms.find((form) => form.kind === props.kind)?.fields,
108
+ )
109
+
110
+ const gateFieldValues = computed<DescriptorFieldValues>({
111
+ get: () => config.value.fields ?? {},
112
+ set: (fields) => pipelines.patchDraftGateConfig(props.index, { fields }),
113
+ })
114
+ </script>
115
+
116
+ <template>
117
+ <div v-if="gated || gateFields?.length" class="ms-6 space-y-2" data-testid="gate-config">
118
+ <div v-if="gated" class="space-y-2 rounded-md border border-amber-800/40 bg-amber-950/10 p-2">
119
+ <div class="flex flex-wrap items-center gap-2 text-[10px]">
120
+ <span class="text-slate-500">{{ t('pipeline.gateConfig.approversLabel') }}</span>
121
+ <label
122
+ v-for="role in APPROVER_ROLES"
123
+ :key="role"
124
+ class="flex items-center gap-1 text-slate-400"
125
+ >
126
+ <input
127
+ type="checkbox"
128
+ :checked="config.approvers?.roles?.includes(role) ?? false"
129
+ :data-testid="`gate-approver-role-${role}`"
130
+ @change="toggleRole(role, ($event.target as HTMLInputElement).checked)"
131
+ />
132
+ {{ t(ROLE_LABEL_KEYS[role as 'admin' | 'member']) }}
133
+ </label>
134
+ </div>
135
+
136
+ <div class="flex flex-wrap items-center gap-2 text-[10px]">
137
+ <span class="text-slate-500">{{ t('pipeline.gateConfig.namedApproversLabel') }}</span>
138
+ <USelectMenu
139
+ class="w-64"
140
+ multiple
141
+ size="xs"
142
+ value-key="value"
143
+ :items="memberOptions"
144
+ :model-value="config.approvers?.userIds ?? []"
145
+ :placeholder="t('pipeline.gateConfig.namedApproversPlaceholder')"
146
+ data-testid="gate-named-approvers"
147
+ @update:model-value="setNamedApprovers(($event ?? []) as string[])"
148
+ />
149
+ </div>
150
+
151
+ <div class="flex flex-wrap items-center gap-2 text-[10px]">
152
+ <label class="text-slate-500" :title="t('pipeline.gateConfig.requiredApprovalsHint')">
153
+ {{ t('pipeline.gateConfig.requiredApprovalsLabel') }}
154
+ </label>
155
+ <input
156
+ :value="requiredApprovals"
157
+ type="number"
158
+ min="1"
159
+ :max="MAX_GATE_APPROVALS"
160
+ step="1"
161
+ class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
162
+ data-testid="gate-required-approvals"
163
+ @change="setRequiredApprovals(($event.target as HTMLInputElement).value)"
164
+ />
165
+ <span class="text-slate-500">{{ t('pipeline.gateConfig.requiredApprovalsHint') }}</span>
166
+ </div>
167
+
168
+ <p v-if="!config.approvers" class="text-[10px] text-slate-500">
169
+ {{ t('pipeline.gateConfig.anyoneHint') }}
170
+ </p>
171
+ </div>
172
+
173
+ <!-- The registered gate's OWN parameters, rendered from what it declared. Labels and help are
174
+ the gate's own English (the descriptor-form convention); only the heading is i18n. -->
175
+ <div
176
+ v-if="gateFields?.length"
177
+ class="space-y-2 rounded-md border border-slate-800 bg-slate-900/40 p-2"
178
+ >
179
+ <p class="text-[10px] text-slate-500">{{ t('pipeline.gateConfig.gateParametersLabel') }}</p>
180
+ <DescriptorFields
181
+ v-model="gateFieldValues"
182
+ :fields="gateFields"
183
+ testid-prefix="gate-parameter"
184
+ />
185
+ </div>
186
+ </div>
187
+ </template>
@@ -6,6 +6,7 @@ import AgentPalette from '~/components/palettes/AgentPalette.vue'
6
6
  import AgentKindIcon from '~/components/pipeline/AgentKindIcon.vue'
7
7
  import AgentPromptEditor from '~/components/pipeline/AgentPromptEditor.vue'
8
8
  import EstimateThresholdFields from '~/components/pipeline/EstimateThresholdFields.vue'
9
+ import GateConfigFields from '~/components/pipeline/GateConfigFields.vue'
9
10
  import OutputBudgetInput from '~/components/pipeline/OutputBudgetInput.vue'
10
11
  import BinaryOutputStepPicker from '~/components/pipeline/BinaryOutputStepPicker.vue'
11
12
  import { ESTIMATE_AXES, ESTIMATE_AXIS_FIELD, type EstimateAxis } from '~/utils/estimateGating'
@@ -164,6 +165,23 @@ function showVariantPicker(index: number, kind: AgentKind): boolean {
164
165
  return showOverrideField(uiMode.isAdvanced, pipelines.draftAgentVariantId(index) ?? null)
165
166
  }
166
167
 
168
+ /**
169
+ * Whether to offer this step's GATE configuration (approvers + required approvals, and any
170
+ * parameters its registered gate declares). There has to be something to configure: either the
171
+ * step carries a human approval gate, or its kind has a registered gate that declares parameters.
172
+ *
173
+ * Then it is an OVERRIDE like the variant picker above — advanced-only until a value is actually
174
+ * set, and visible in both tiers from then on. That second half is load-bearing here rather than
175
+ * cosmetic: the builder saves the whole step-options bag, so a configured policy invisible to a
176
+ * basic-mode editor would be a policy they silently save over.
177
+ */
178
+ function showGateConfig(index: number, kind: AgentKind): boolean {
179
+ const gated = pipelines.draftGates[index] === true
180
+ const declaresFields = pipelines.gateConfigForms.some((form) => form.kind === kind)
181
+ if (!gated && !declaresFields) return false
182
+ return showOverrideField(uiMode.isAdvanced, pipelines.draftGateConfig(index) ?? null)
183
+ }
184
+
167
185
  /**
168
186
  * The variants registered for a step's kind as USelect items, with an explicit "shipped prompt"
169
187
  * entry so clearing the pick is a choice in the same list rather than a separate affordance.
@@ -843,6 +861,19 @@ async function clone(p: Pipeline) {
843
861
  />
844
862
  </div>
845
863
 
864
+ <!-- Gate configuration: who may clear this step's approval gate and how many of
865
+ them, plus the parameters its registered gate declares. An OVERRIDE of the
866
+ defaults (one approval from anyone entitled to write, the gate's shipped knobs),
867
+ so it is advanced-only until a step actually configures something — at which point
868
+ it must stay visible in both tiers, or a member editing the pipeline would save
869
+ over a policy they were never shown. -->
870
+ <GateConfigFields
871
+ v-if="showGateConfig(unit.index, unit.kind)"
872
+ :index="unit.index"
873
+ :gated="pipelines.draftGates[unit.index] === true"
874
+ :kind="unit.kind"
875
+ />
876
+
846
877
  <!-- Binary-output picker: a generator kind's step is parametrized by the
847
878
  foundational STORAGE service its artifacts go through (`stepOptions.binaryOutput`)
848
879
  plus any services consulted for the generation's scope. Required, not an
@@ -1,6 +1,9 @@
1
1
  import { ref, computed, nextTick } from 'vue'
2
+ import { refuseGateResolution, UNATTRIBUTED_GATE_ACTOR } from '@cat-factory/contracts'
3
+ import type { GateActor, GateApprovalRefusal } from '@cat-factory/contracts'
2
4
  import type { PipelineStep } from '~/types/execution'
3
5
  import { useProseComments } from '~/composables/useProseComments'
6
+ import { useWorkspaceAccess } from '~/composables/useWorkspaceAccess'
4
7
 
5
8
  /**
6
9
  * The GitHub-style approval/review state machine for a pending gate step. When the
@@ -23,6 +26,8 @@ export function useStepApproval(opts: {
23
26
  close: () => void
24
27
  }) {
25
28
  const execution = useExecutionStore()
29
+ const auth = useAuthStore()
30
+ const access = useWorkspaceAccess()
26
31
 
27
32
  const feedback = ref('')
28
33
  const submitting = ref(false)
@@ -60,12 +65,71 @@ export function useStepApproval(opts: {
60
65
  () => !!feedback.value.trim() || reviewComments.value.length > 0,
61
66
  )
62
67
 
68
+ // ---- The gate's own POLICY, as the pipeline step configured it -------------------------
69
+ //
70
+ // Read from the SAME `@cat-factory/contracts` rules the engine enforces, never a local
71
+ // reimplementation: a button enabled by a second copy of the rule is a request the server
72
+ // refuses, and the person pressing it has no way to tell which of the two is right.
73
+
74
+ const approval = computed(() => opts.step()?.approval ?? null)
75
+
76
+ /**
77
+ * The gate's quorum, or null when it needs the usual single approval. Non-null is what makes
78
+ * the rail say "1 of 2 approvals" — otherwise an approve that correctly leaves the run parked
79
+ * looks exactly like one that failed.
80
+ */
81
+ const quorum = computed(() => {
82
+ const required = approval.value?.requiredApprovals ?? 1
83
+ if (required <= 1) return null
84
+ return { required, recorded: approval.value?.approvals?.length ?? 0 }
85
+ })
86
+
87
+ /** Whether the viewer's own approval is already counted (so the rail can say so). */
88
+ const viewerHasApproved = computed(() => {
89
+ const userId = auth.user?.id
90
+ return !!userId && !!approval.value?.approvals?.some((a) => a.actorId === userId)
91
+ })
92
+
93
+ /**
94
+ * Whether the viewer's approval would be the one that CLEARS the gate. Always true without a
95
+ * quorum; under one it folds the viewer in the way the server does, so a re-approval by someone
96
+ * already counted does not read as a new vote.
97
+ *
98
+ * This is what decides whether "approve with corrections" is offered: a quorum votes on ONE
99
+ * artifact, so an edit that does not clear the gate would rewrite the proposal under the people
100
+ * already counted toward it and the ones still to come. The server refuses that
101
+ * (`proposal_not_editable_until_quorum`); hiding the affordance is what stops a reviewer typing
102
+ * a correction into a dead end, the same disposition as `outputIsRendered`.
103
+ */
104
+ const approvalWouldClearGate = computed(() => {
105
+ const q = quorum.value
106
+ if (!q) return true
107
+ return (viewerHasApproved.value ? q.recorded : q.recorded + 1) >= q.required
108
+ })
109
+
110
+ /**
111
+ * Why the viewer may not resolve this gate, or null when they may. Drives the disabled state of
112
+ * all three verbs, since the policy governs every resolution and not just approve.
113
+ *
114
+ * With auth off there is no signed-in user, and the actor is `unattributed` — exactly what the
115
+ * server will decide with, so the rail refuses ahead of it rather than offering a button whose
116
+ * request comes back 403.
117
+ */
118
+ const refusal = computed<GateApprovalRefusal | null>(() => {
119
+ if (!approval.value) return null
120
+ const user = auth.user
121
+ const actor: GateActor = user
122
+ ? { id: user.id, kind: 'user', role: access.role.value }
123
+ : { id: UNATTRIBUTED_GATE_ACTOR, kind: 'unattributed', role: access.role.value }
124
+ return refuseGateResolution(approval.value.approverPolicy, actor)
125
+ })
126
+
63
127
  // Plain approve: accept the agent's proposal verbatim and advance. Every action below
64
128
  // closes the overlay ONLY when the command actually ran — a server refusal (surfaced as
65
129
  // a toast by the store) or a cancelled credential prompt keeps the review open.
66
130
  async function approve() {
67
131
  const id = opts.approvalId()
68
- if (!opts.instanceId() || !id || submitting.value) return
132
+ if (!opts.instanceId() || !id || submitting.value || refusal.value) return
69
133
  submitting.value = true
70
134
  try {
71
135
  if (await execution.approveStep(opts.instanceId()!, id)) opts.close()
@@ -88,7 +152,9 @@ export function useStepApproval(opts: {
88
152
  }
89
153
  async function approveWithEdits() {
90
154
  const id = opts.approvalId()
91
- if (!opts.instanceId() || !id || submitting.value) return
155
+ if (!opts.instanceId() || !id || submitting.value || refusal.value) return
156
+ // The server refuses an edit that does not clear the gate; never send one.
157
+ if (!approvalWouldClearGate.value) return
92
158
  submitting.value = true
93
159
  try {
94
160
  if (await execution.approveStep(opts.instanceId()!, id, draftProposal.value)) opts.close()
@@ -99,6 +165,7 @@ export function useStepApproval(opts: {
99
165
  async function requestChanges() {
100
166
  const id = opts.approvalId()
101
167
  if (!opts.instanceId() || !id || submitting.value || !canRequestChanges.value) return
168
+ if (refusal.value) return
102
169
  submitting.value = true
103
170
  try {
104
171
  const ok = await execution.requestStepChanges(opts.instanceId()!, id, {
@@ -118,7 +185,7 @@ export function useStepApproval(opts: {
118
185
  }
119
186
  async function reject() {
120
187
  const id = opts.approvalId()
121
- if (!opts.instanceId() || !id || submitting.value) return
188
+ if (!opts.instanceId() || !id || submitting.value || refusal.value) return
122
189
  submitting.value = true
123
190
  try {
124
191
  if (await execution.rejectStep(opts.instanceId()!, id, feedback.value.trim() || undefined)) {
@@ -159,6 +226,10 @@ export function useStepApproval(opts: {
159
226
  draftProposal,
160
227
  rejectArmed,
161
228
  canRequestChanges,
229
+ quorum,
230
+ viewerHasApproved,
231
+ approvalWouldClearGate,
232
+ refusal,
162
233
  syncHighlights,
163
234
  onProseClick,
164
235
  addDraftComment,
@@ -2,6 +2,7 @@ import { computed } from 'vue'
2
2
  import type { AgentKind, Pipeline } from '~/types/domain'
3
3
  import { companionForProducer } from '~/utils/catalog'
4
4
  import type { PipelinesContext } from './context'
5
+ import { createPipelineGateConfigActions } from './draftGateConfig'
5
6
  import { createPipelineStepConfigActions } from './draftStepConfig'
6
7
 
7
8
  /**
@@ -192,6 +193,7 @@ export function createPipelineDraftActions(ctx: PipelinesContext) {
192
193
 
193
194
  return {
194
195
  ...createPipelineStepConfigActions(ctx),
196
+ ...createPipelineGateConfigActions(ctx),
195
197
  addToDraft,
196
198
  removeFromDraft,
197
199
  moveInDraft,
@@ -0,0 +1,55 @@
1
+ import type { StepGateConfig, StepOptions } from '@cat-factory/contracts'
2
+ import { hasApproverPolicy } from '@cat-factory/contracts'
3
+ import type { PipelinesContext } from './context'
4
+
5
+ /**
6
+ * The pipeline-builder draft's per-step GATE configuration: who may clear a step's human approval
7
+ * gate, how many of them must, and the parameters the step's registered gate declares.
8
+ *
9
+ * Its own module rather than another pair of accessors on `./draftStepConfig`, which owns the flat
10
+ * per-step options (a skill id, a variant id, a token ceiling — each one field, read and written
11
+ * whole). Gate config is the one nested value there: it MERGES a patch across two independently
12
+ * edited halves and normalizes at three levels on the way out, which is a different enough shape to
13
+ * be worth reading on its own. (It also put that file over the per-function line budget, which is
14
+ * the ratchet doing its job rather than a number to raise.)
15
+ */
16
+ export function createPipelineGateConfigActions(ctx: PipelinesContext) {
17
+ const { draftStepOptions } = ctx
18
+
19
+ /**
20
+ * The gate configuration on the draft step at `index`, or undefined when the step takes every
21
+ * default (one approval from anyone entitled to write, and the gate's shipped knobs).
22
+ */
23
+ function draftGateConfig(index: number): StepGateConfig | undefined {
24
+ return draftStepOptions.value[index]?.gateConfig
25
+ }
26
+
27
+ /**
28
+ * Merge a PATCH into the draft step's gate configuration. A patch rather than a whole-value set
29
+ * because the builder edits the two halves through different controls: a whole-value write from
30
+ * the approver fields would drop the gate's own parameters, and vice versa.
31
+ *
32
+ * Normalizes downward at every level: a field set back to its default is deleted, an emptied
33
+ * `gateConfig` is dropped, and an emptied options bag becomes `null`. So a step returned to the
34
+ * defaults persists nothing at all — the same rule the other per-step options follow, and what
35
+ * keeps an all-default pipeline from growing a `step_options` array it does not need.
36
+ *
37
+ * An empty approver policy is dropped rather than stored, and that one is not just tidiness:
38
+ * `{ roles: [] }` names nobody, and a policy naming nobody would refuse every approver and park
39
+ * the run forever. "No rule" has to persist as an ABSENT rule.
40
+ */
41
+ function patchDraftGateConfig(index: number, patch: Partial<StepGateConfig>) {
42
+ const gateConfig: StepGateConfig = { ...draftGateConfig(index), ...patch }
43
+ if (!hasApproverPolicy(gateConfig.approvers)) delete gateConfig.approvers
44
+ if (gateConfig.minApprovals !== undefined && gateConfig.minApprovals <= 1) {
45
+ delete gateConfig.minApprovals
46
+ }
47
+ if (gateConfig.fields && Object.keys(gateConfig.fields).length === 0) delete gateConfig.fields
48
+ const next: StepOptions = { ...draftStepOptions.value[index] }
49
+ if (Object.keys(gateConfig).length) next.gateConfig = gateConfig
50
+ else delete next.gateConfig
51
+ draftStepOptions.value[index] = Object.keys(next).length ? next : null
52
+ }
53
+
54
+ return { draftGateConfig, patchDraftGateConfig }
55
+ }
@@ -7,7 +7,8 @@ import { defaultConsensusConfig, type PipelinesContext } from './context'
7
7
  * consensus-GROUP tier set), the human approval gate, the estimate gate on a companion step, the
8
8
  * follow-up and test-QC companions, the per-step enable flag, and the `StepOptions` bag
9
9
  * (requirements auto-recommendation, the picked skill, the picked agent-kind variant, the
10
- * per-step output-token ceiling, the binary-output storage/context selection).
10
+ * per-step output-token ceiling, the binary-output storage/context selection). The step's GATE
11
+ * configuration is the sibling `./draftGateConfig`, which explains why it is not here.
11
12
  *
12
13
  * Split out of `./draftActions`, which owns the draft's STRUCTURE (insert / remove / reorder /
13
14
  * units). Every function here reads and writes one of the parallel per-step arrays at an index and
@@ -1,7 +1,7 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { ref } from 'vue'
3
3
  import type { Pipeline } from '~/types/domain'
4
- import type { PipelinePurpose, RetiredPipelineWire } from '@cat-factory/contracts'
4
+ import type { GateConfigForm, PipelinePurpose, RetiredPipelineWire } from '@cat-factory/contracts'
5
5
  import { useUpsertList } from '~/composables/useUpsertList'
6
6
  import { createDraftStepState, type PipelinesContext } from '~/stores/pipelines/context'
7
7
  import { createPipelineDraftActions } from '~/stores/pipelines/draftActions'
@@ -39,6 +39,13 @@ export const usePipelinesStore = defineStore('pipelines', () => {
39
39
  * `usePipelineHealth`). Disjoint from {@link catalogVersions} by construction.
40
40
  */
41
41
  const retiredPipelines = ref<RetiredPipelineWire[]>([])
42
+ /**
43
+ * The per-step parameters each registered GATE declares, projected from the deployment's gate
44
+ * registry onto the board snapshot. The builder renders a gated step's own config form from
45
+ * these, so what it can save is exactly what run admission validates. Empty on a deployment
46
+ * whose gates declare nothing.
47
+ */
48
+ const gateConfigForms = ref<GateConfigForm[]>([])
42
49
 
43
50
  // The per-step, index-aligned draft arrays (kept in lockstep — see `createDraftStepState`).
44
51
  const {
@@ -83,6 +90,16 @@ export const usePipelinesStore = defineStore('pipelines', () => {
83
90
  retiredPipelines.value = retired ?? []
84
91
  }
85
92
 
93
+ /**
94
+ * Replace the registered gates' declared config forms from a snapshot. Applied even when EMPTY,
95
+ * for the reason `retired` is: carrying the previous board's forward would offer a form for a
96
+ * parameter this deployment's gates do not declare, and a value saved through it would then be
97
+ * refused at save by the very registry that never declared it.
98
+ */
99
+ function hydrateGateConfigForms(forms: GateConfigForm[]) {
100
+ gateConfigForms.value = forms
101
+ }
102
+
86
103
  function getPipeline(id: string) {
87
104
  return pipelines.value.find((p) => p.id === id)
88
105
  }
@@ -117,6 +134,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
117
134
  pipelines,
118
135
  catalogVersions,
119
136
  retiredPipelines,
137
+ gateConfigForms,
120
138
  draft,
121
139
  draftGates,
122
140
  draftEnabled,
@@ -132,6 +150,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
132
150
  draftDescription,
133
151
  editingId,
134
152
  hydrate,
153
+ hydrateGateConfigForms,
135
154
  getPipeline,
136
155
  ...draftActions,
137
156
  ...persistence,
@@ -114,6 +114,9 @@ export function applySnapshotToStores(snapshot: WorkspaceSnapshot, boardSince?:
114
114
  snapshot.binaryGeneratorsUnavailable === true,
115
115
  )
116
116
  useTaskTypesStore().hydrateCapabilities(capabilities)
117
+ // The per-step parameters each registered gate declares, so a gated step's config form in the
118
+ // builder comes from the gate's own registration rather than a form hard-coded per gate.
119
+ usePipelinesStore().hydrateGateConfigForms(snapshot.gateConfigForms ?? [])
117
120
  // The account's repo-sourced Claude Skills catalog (shared across its workspaces), so the
118
121
  // pipeline builder's per-step skill picker has its options. A straight replace.
119
122
  useSkillsStore().hydrate(snapshot.skills ?? [])
@@ -73,6 +73,11 @@ export type {
73
73
  DescriptorField,
74
74
  DescriptorFieldValue,
75
75
  DescriptorFieldValues,
76
+ // Per-step GATE configuration: who may resolve a human approval gate, how many of them, and
77
+ // the parameters the step's registered gate declares (`contracts/src/gate-config.ts`).
78
+ GateApproverPolicy,
79
+ GateConfigForm,
80
+ StepGateConfig,
76
81
  Pipeline,
77
82
  PipelinePurpose,
78
83
  SpendStatus,
@@ -1985,7 +1985,12 @@
1985
1985
  "notRun": "nicht ausgeführt",
1986
1986
  "attempts": "Versuch {attempts} von {maxAttempts}",
1987
1987
  "omittedTestPaths": "{count} deklarierte Testdatei wurde vor dem Nachweis verworfen, daher wurde der Stand vor dem Fix aus einer unvollständigen Reproduktion aufgebaut. | {count} deklarierte Testdateien wurden vor dem Nachweis verworfen, daher wurde der Stand vor dem Fix aus einer unvollständigen Reproduktion aufgebaut."
1988
- }
1988
+ },
1989
+ "quorumProgress": "{recorded} von {required} Freigaben",
1990
+ "quorumYours": "(deine ist gezählt)",
1991
+ "quorumEditLocked": "Korrekturen sind möglich, sobald deine Freigabe dieses Gate auflöst. Bis dahin unverändert freigeben oder Änderungen anfordern.",
1992
+ "notAnApprover": "Dieses Gate benennt, wer es auflösen darf, und du gehörst nicht dazu.",
1993
+ "approverIdentityRequired": "Dieses Gate benennt, wer es auflösen darf; eine dieser Personen muss dafür angemeldet sein."
1989
1994
  },
1990
1995
  "inspector": {
1991
1996
  "frameStatus": {
@@ -4129,6 +4134,17 @@
4129
4134
  "hint": "Der Höchstumfang einer einzelnen Antwort dieses Agenten. Leer lassen heißt vom nächsthöheren Level erben, nicht das Limit aufheben. Der Wert eines Pipeline-Schritts sticht die Vorgabe des Workspace.",
4130
4135
  "inherits": "Übernommen",
4131
4136
  "inheritsValue": "Übernommen ({tokens})"
4137
+ },
4138
+ "gateConfig": {
4139
+ "approversLabel": "Wer freigeben darf",
4140
+ "roleAdmin": "Administratoren",
4141
+ "roleMember": "Mitglieder",
4142
+ "namedApproversLabel": "Benannte Freigebende",
4143
+ "namedApproversPlaceholder": "Alle mit Schreibrecht",
4144
+ "requiredApprovalsLabel": "Erforderliche Freigaben",
4145
+ "requiredApprovalsHint": "Jeweils von einer anderen Person, bevor der Lauf fortgesetzt wird.",
4146
+ "anyoneHint": "Alle, die dieses Board bearbeiten dürfen, können diesen Schritt freigeben.",
4147
+ "gateParametersLabel": "Gate-Einstellungen"
4132
4148
  }
4133
4149
  },
4134
4150
  "agentPrompt": {
@@ -1544,7 +1544,12 @@
1544
1544
  "attempts": "Attempt {attempts} of {maxAttempts}",
1545
1545
  "omittedTestPaths": "{count} declared test file was dropped before the proof ran, so the pre-fix tree was rebuilt from an incomplete reproduction. | {count} declared test files were dropped before the proof ran, so the pre-fix tree was rebuilt from an incomplete reproduction.",
1546
1546
  "@omittedTestPaths": "Plural form required. A dropped file can leave the pre-fix tree without the reproduction, which makes the check pass there and wrongly reads as 'the test does not capture the defect'."
1547
- }
1547
+ },
1548
+ "quorumProgress": "{recorded} of {required} approvals",
1549
+ "quorumYours": "(yours counted)",
1550
+ "quorumEditLocked": "Corrections can be typed once yours is the approval that clears this gate. Until then, approve as-is or request changes.",
1551
+ "notAnApprover": "This gate names who may resolve it, and you are not one of them.",
1552
+ "approverIdentityRequired": "This gate names who may resolve it, so it has to be resolved by one of those people signed in."
1548
1553
  },
1549
1554
  "inspector": {
1550
1555
  "frameStatus": {
@@ -4643,6 +4648,17 @@
4643
4648
  "hint": "The most a single reply from this agent may run to. Leave it empty to inherit the next level up rather than to lift the limit. A pipeline step's own value wins over the workspace default.",
4644
4649
  "inherits": "Inherited",
4645
4650
  "inheritsValue": "Inherited ({tokens})"
4651
+ },
4652
+ "gateConfig": {
4653
+ "approversLabel": "Who may approve",
4654
+ "roleAdmin": "Admins",
4655
+ "roleMember": "Members",
4656
+ "namedApproversLabel": "Named approvers",
4657
+ "namedApproversPlaceholder": "Anyone entitled to write",
4658
+ "requiredApprovalsLabel": "Approvals required",
4659
+ "requiredApprovalsHint": "Each from a different person before the run continues.",
4660
+ "anyoneHint": "Anyone who can edit this board may approve this step.",
4661
+ "gateParametersLabel": "Gate settings"
4646
4662
  }
4647
4663
  },
4648
4664
  "agentPrompt": {
@@ -1455,7 +1455,12 @@
1455
1455
  "notRun": "no ejecutado",
1456
1456
  "attempts": "Intento {attempts} de {maxAttempts}",
1457
1457
  "omittedTestPaths": "Se descartó {count} archivo de prueba declarado antes de ejecutar la prueba, por lo que el árbol previo a la corrección se reconstruyó a partir de una reproducción incompleta. | Se descartaron {count} archivos de prueba declarados antes de ejecutar la prueba, por lo que el árbol previo a la corrección se reconstruyó a partir de una reproducción incompleta."
1458
- }
1458
+ },
1459
+ "quorumProgress": "{recorded} de {required} aprobaciones",
1460
+ "quorumYours": "(la tuya está contada)",
1461
+ "quorumEditLocked": "Podrás escribir correcciones cuando tu aprobación sea la que libere esta puerta. Hasta entonces, aprueba tal cual o solicita cambios.",
1462
+ "notAnApprover": "Esta compuerta designa quién puede resolverla y tú no estás en la lista.",
1463
+ "approverIdentityRequired": "Esta compuerta designa quién puede resolverla, así que debe resolverla una de esas personas con la sesión iniciada."
1459
1464
  },
1460
1465
  "inspector": {
1461
1466
  "frameStatus": {
@@ -4510,6 +4515,17 @@
4510
4515
  "hint": "El maximo al que puede llegar una sola respuesta de este agente. Dejarlo vacio hereda del nivel superior; no elimina el limite. El valor propio de un paso del pipeline prevalece sobre el predeterminado del espacio de trabajo.",
4511
4516
  "inherits": "Heredado",
4512
4517
  "inheritsValue": "Heredado ({tokens})"
4518
+ },
4519
+ "gateConfig": {
4520
+ "approversLabel": "Quién puede aprobar",
4521
+ "roleAdmin": "Administradores",
4522
+ "roleMember": "Miembros",
4523
+ "namedApproversLabel": "Aprobadores designados",
4524
+ "namedApproversPlaceholder": "Cualquiera con permiso de escritura",
4525
+ "requiredApprovalsLabel": "Aprobaciones necesarias",
4526
+ "requiredApprovalsHint": "Cada una de una persona distinta antes de que la ejecución continúe.",
4527
+ "anyoneHint": "Cualquiera que pueda editar este tablero puede aprobar este paso.",
4528
+ "gateParametersLabel": "Ajustes de la compuerta"
4513
4529
  }
4514
4530
  },
4515
4531
  "agentPrompt": {
@@ -1455,7 +1455,12 @@
1455
1455
  "notRun": "non exécuté",
1456
1456
  "attempts": "Tentative {attempts} sur {maxAttempts}",
1457
1457
  "omittedTestPaths": "{count} fichier de test déclaré a été écarté avant l’exécution de la preuve, l’arbre d’avant le correctif a donc été reconstruit à partir d’une reproduction incomplète. | {count} fichiers de test déclarés ont été écartés avant l’exécution de la preuve, l’arbre d’avant le correctif a donc été reconstruit à partir d’une reproduction incomplète."
1458
- }
1458
+ },
1459
+ "quorumProgress": "{recorded} approbations sur {required}",
1460
+ "quorumYours": "(la vôtre est comptée)",
1461
+ "quorumEditLocked": "Les corrections seront possibles quand votre approbation sera celle qui libère ce point de contrôle. D'ici là, approuvez tel quel ou demandez des modifications.",
1462
+ "notAnApprover": "Ce contrôle désigne qui peut le lever, et vous n'en faites pas partie.",
1463
+ "approverIdentityRequired": "Ce contrôle désigne qui peut le lever : l'une de ces personnes doit être connectée pour le faire."
1459
1464
  },
1460
1465
  "inspector": {
1461
1466
  "frameStatus": {
@@ -4510,6 +4515,17 @@
4510
4515
  "hint": "La longueur maximale d'une seule reponse de cet agent. Laisser vide herite du niveau superieur, cela ne leve pas la limite. La valeur propre a une etape du pipeline l'emporte sur la valeur par defaut de l'espace de travail.",
4511
4516
  "inherits": "Hérité",
4512
4517
  "inheritsValue": "Hérité ({tokens})"
4518
+ },
4519
+ "gateConfig": {
4520
+ "approversLabel": "Qui peut approuver",
4521
+ "roleAdmin": "Administrateurs",
4522
+ "roleMember": "Membres",
4523
+ "namedApproversLabel": "Approbateurs désignés",
4524
+ "namedApproversPlaceholder": "Toute personne autorisée à écrire",
4525
+ "requiredApprovalsLabel": "Approbations requises",
4526
+ "requiredApprovalsHint": "Chacune d'une personne différente avant que l'exécution reprenne.",
4527
+ "anyoneHint": "Toute personne pouvant modifier ce tableau peut approuver cette étape.",
4528
+ "gateParametersLabel": "Paramètres du contrôle"
4513
4529
  }
4514
4530
  },
4515
4531
  "agentPrompt": {
@@ -1455,7 +1455,12 @@
1455
1455
  "notRun": "לא הורץ",
1456
1456
  "attempts": "ניסיון {attempts} מתוך {maxAttempts}",
1457
1457
  "omittedTestPaths": "קובץ בדיקה אחד שהוצהר הושמט לפני הרצת ההוכחה, ולכן העץ שלפני התיקון נבנה משחזור חלקי. | שני קובצי בדיקה שהוצהרו הושמטו לפני הרצת ההוכחה, ולכן העץ שלפני התיקון נבנה משחזור חלקי. | {count} קובצי בדיקה שהוצהרו הושמטו לפני הרצת ההוכחה, ולכן העץ שלפני התיקון נבנה משחזור חלקי."
1458
- }
1458
+ },
1459
+ "quorumProgress": "{recorded} מתוך {required} אישורים",
1460
+ "quorumYours": "(שלך נספר)",
1461
+ "quorumEditLocked": "אפשר להקליד תיקונים כאשר האישור שלך יהיה זה שפותח את השער. עד אז, אשר כמות שהוא או בקש שינויים.",
1462
+ "notAnApprover": "השער הזה מגדיר מי רשאי לפתור אותו, ואינך אחד מהם.",
1463
+ "approverIdentityRequired": "השער הזה מגדיר מי רשאי לפתור אותו, ולכן אחד מאותם אנשים חייב להיות מחובר."
1459
1464
  },
1460
1465
  "inspector": {
1461
1466
  "frameStatus": {
@@ -4510,6 +4515,17 @@
4510
4515
  "hint": "המקסימום לתשובה בודדת של הסוכן הזה. השארה ריקה יורשת מהרמה שמעל, ואינה מבטלת את המגבלה. ערך של שלב בצינור גובר על ברירת המחדל של סביבת העבודה.",
4511
4516
  "inherits": "בירושה",
4512
4517
  "inheritsValue": "בירושה ({tokens})"
4518
+ },
4519
+ "gateConfig": {
4520
+ "approversLabel": "מי רשאי לאשר",
4521
+ "roleAdmin": "מנהלים",
4522
+ "roleMember": "חברים",
4523
+ "namedApproversLabel": "מאשרים ייעודיים",
4524
+ "namedApproversPlaceholder": "כל מי שרשאי לערוך",
4525
+ "requiredApprovalsLabel": "אישורים נדרשים",
4526
+ "requiredApprovalsHint": "כל אחד מאדם אחר, לפני שההרצה ממשיכה.",
4527
+ "anyoneHint": "כל מי שיכול לערוך את הלוח הזה רשאי לאשר את השלב.",
4528
+ "gateParametersLabel": "הגדרות השער"
4513
4529
  }
4514
4530
  },
4515
4531
  "agentPrompt": {
@@ -1985,7 +1985,12 @@
1985
1985
  "notRun": "non eseguito",
1986
1986
  "attempts": "Tentativo {attempts} di {maxAttempts}",
1987
1987
  "omittedTestPaths": "{count} file di test dichiarato è stato scartato prima dell’esecuzione della prova, quindi l’albero precedente alla correzione è stato ricostruito da una riproduzione incompleta. | {count} file di test dichiarati sono stati scartati prima dell’esecuzione della prova, quindi l’albero precedente alla correzione è stato ricostruito da una riproduzione incompleta."
1988
- }
1988
+ },
1989
+ "quorumProgress": "{recorded} di {required} approvazioni",
1990
+ "quorumYours": "(la tua è conteggiata)",
1991
+ "quorumEditLocked": "Le correzioni si potranno scrivere quando la tua approvazione sarà quella che sblocca questo gate. Fino ad allora, approva così com'è o richiedi modifiche.",
1992
+ "notAnApprover": "Questo gate indica chi può risolverlo e tu non sei tra loro.",
1993
+ "approverIdentityRequired": "Questo gate indica chi può risolverlo, quindi deve farlo una di quelle persone dopo aver effettuato l’accesso."
1989
1994
  },
1990
1995
  "inspector": {
1991
1996
  "frameStatus": {
@@ -4129,6 +4134,17 @@
4129
4134
  "hint": "Il massimo a cui puo arrivare una singola risposta di questo agente. Lasciarlo vuoto eredita dal livello superiore, non toglie il limite. Il valore di uno step della pipeline prevale sul predefinito dello spazio di lavoro.",
4130
4135
  "inherits": "Ereditato",
4131
4136
  "inheritsValue": "Ereditato ({tokens})"
4137
+ },
4138
+ "gateConfig": {
4139
+ "approversLabel": "Chi può approvare",
4140
+ "roleAdmin": "Amministratori",
4141
+ "roleMember": "Membri",
4142
+ "namedApproversLabel": "Approvatori designati",
4143
+ "namedApproversPlaceholder": "Chiunque abbia permessi di scrittura",
4144
+ "requiredApprovalsLabel": "Approvazioni richieste",
4145
+ "requiredApprovalsHint": "Ciascuna da una persona diversa prima che l'esecuzione prosegua.",
4146
+ "anyoneHint": "Chiunque possa modificare questa board può approvare questo passo.",
4147
+ "gateParametersLabel": "Impostazioni del gate"
4132
4148
  }
4133
4149
  },
4134
4150
  "agentPrompt": {
@@ -1455,7 +1455,12 @@
1455
1455
  "notRun": "未実行",
1456
1456
  "attempts": "{maxAttempts} 回中 {attempts} 回目の試行",
1457
1457
  "omittedTestPaths": "宣言されたテストファイル {count} 件が証跡の実行前に除外されたため、修正前のツリーは不完全な再現から再構築されました。 | 宣言されたテストファイル {count} 件が証跡の実行前に除外されたため、修正前のツリーは不完全な再現から再構築されました。"
1458
- }
1458
+ },
1459
+ "quorumProgress": "承認 {recorded} / {required}",
1460
+ "quorumYours": "(あなたの承認は集計済み)",
1461
+ "quorumEditLocked": "このゲートを解除する承認があなたのものになった時点で、修正を入力できます。それまでは、そのまま承認するか変更を依頼してください。",
1462
+ "notAnApprover": "このゲートは解除できる人を指定しており、あなたは含まれていません。",
1463
+ "approverIdentityRequired": "このゲートは解除できる人を指定しているため、その人がサインインして解除する必要があります。"
1459
1464
  },
1460
1465
  "inspector": {
1461
1466
  "frameStatus": {
@@ -4510,6 +4515,17 @@
4510
4515
  "hint": "このエージェントの1回の応答の上限です。空欄にすると上位の設定を継承します。上限が無くなるわけではありません。パイプラインのステップに設定した値はワークスペースの既定値より優先されます。",
4511
4516
  "inherits": "継承",
4512
4517
  "inheritsValue": "継承({tokens})"
4518
+ },
4519
+ "gateConfig": {
4520
+ "approversLabel": "承認できる人",
4521
+ "roleAdmin": "管理者",
4522
+ "roleMember": "メンバー",
4523
+ "namedApproversLabel": "指名した承認者",
4524
+ "namedApproversPlaceholder": "書き込み権限のある全員",
4525
+ "requiredApprovalsLabel": "必要な承認数",
4526
+ "requiredApprovalsHint": "実行を再開する前に、それぞれ別の人からの承認が必要です。",
4527
+ "anyoneHint": "このボードを編集できる人なら誰でもこのステップを承認できます。",
4528
+ "gateParametersLabel": "ゲートの設定"
4513
4529
  }
4514
4530
  },
4515
4531
  "agentPrompt": {
@@ -1455,7 +1455,12 @@
1455
1455
  "notRun": "nie uruchomiono",
1456
1456
  "attempts": "Próba {attempts} z {maxAttempts}",
1457
1457
  "omittedTestPaths": "Przed uruchomieniem dowodu pominięto {count} zadeklarowany plik testowy, więc drzewo sprzed poprawki odtworzono z niepełnej reprodukcji. | Przed uruchomieniem dowodu pominięto {count} zadeklarowane pliki testowe, więc drzewo sprzed poprawki odtworzono z niepełnej reprodukcji. | Przed uruchomieniem dowodu pominięto {count} zadeklarowanych plików testowych, więc drzewo sprzed poprawki odtworzono z niepełnej reprodukcji."
1458
- }
1458
+ },
1459
+ "quorumProgress": "{recorded} z {required} zatwierdzeń",
1460
+ "quorumYours": "(twoje jest policzone)",
1461
+ "quorumEditLocked": "Poprawki będzie można wpisać, gdy Twoja akceptacja odblokuje tę bramkę. Do tego czasu zatwierdź bez zmian lub poproś o zmiany.",
1462
+ "notAnApprover": "Ta bramka wskazuje, kto może ją rozstrzygnąć, a ciebie na tej liście nie ma.",
1463
+ "approverIdentityRequired": "Ta bramka wskazuje, kto może ją rozstrzygnąć, więc musi to zrobić jedna z tych osób po zalogowaniu."
1459
1464
  },
1460
1465
  "inspector": {
1461
1466
  "frameStatus": {
@@ -4510,6 +4515,17 @@
4510
4515
  "hint": "Maksymalna długość pojedynczej odpowiedzi tego agenta. Puste pole dziedziczy wartość z wyższego poziomu, a nie znosi limitu. Wartość ustawiona na kroku pipeline'u ma pierwszeństwo przed domyślną wartością obszaru roboczego.",
4511
4516
  "inherits": "Odziedziczone",
4512
4517
  "inheritsValue": "Odziedziczone ({tokens})"
4518
+ },
4519
+ "gateConfig": {
4520
+ "approversLabel": "Kto może zatwierdzić",
4521
+ "roleAdmin": "Administratorzy",
4522
+ "roleMember": "Członkowie",
4523
+ "namedApproversLabel": "Wskazane osoby zatwierdzające",
4524
+ "namedApproversPlaceholder": "Każdy z prawem zapisu",
4525
+ "requiredApprovalsLabel": "Wymagane zatwierdzenia",
4526
+ "requiredApprovalsHint": "Każde od innej osoby, zanim przebieg ruszy dalej.",
4527
+ "anyoneHint": "Każdy, kto może edytować tę tablicę, może zatwierdzić ten krok.",
4528
+ "gateParametersLabel": "Ustawienia bramki"
4513
4529
  }
4514
4530
  },
4515
4531
  "agentPrompt": {
@@ -1455,7 +1455,12 @@
1455
1455
  "notRun": "çalıştırılmadı",
1456
1456
  "attempts": "{maxAttempts} denemeden {attempts}. deneme",
1457
1457
  "omittedTestPaths": "Kanıt çalıştırılmadan önce bildirilen {count} test dosyası atıldı; bu yüzden düzeltme öncesi ağaç eksik bir yeniden üretimden kuruldu. | Kanıt çalıştırılmadan önce bildirilen {count} test dosyası atıldı; bu yüzden düzeltme öncesi ağaç eksik bir yeniden üretimden kuruldu."
1458
- }
1458
+ },
1459
+ "quorumProgress": "{required} onaydan {recorded} tanesi",
1460
+ "quorumYours": "(seninki sayıldı)",
1461
+ "quorumEditLocked": "Bu geçidi açan onay sizinki olduğunda düzeltme yazabilirsiniz. O ana kadar olduğu gibi onaylayın ya da değişiklik isteyin.",
1462
+ "notAnApprover": "Bu kapı kimlerin karar verebileceğini belirtiyor ve sen bunlardan biri değilsin.",
1463
+ "approverIdentityRequired": "Bu kapı kimlerin karar verebileceğini belirtiyor; bu kişilerden biri oturum açarak karar vermeli."
1459
1464
  },
1460
1465
  "inspector": {
1461
1466
  "frameStatus": {
@@ -4510,6 +4515,17 @@
4510
4515
  "hint": "Bu ajanın tek bir yanıtının ulaşabileceği üst sınır. Boş bırakmak sınırı kaldırmaz, bir üst seviyeden devralır. Pipeline adımının kendi değeri çalışma alanı varsayılanını geçersiz kılar.",
4511
4516
  "inherits": "Devralındı",
4512
4517
  "inheritsValue": "Devralındı ({tokens})"
4518
+ },
4519
+ "gateConfig": {
4520
+ "approversLabel": "Kimler onaylayabilir",
4521
+ "roleAdmin": "Yöneticiler",
4522
+ "roleMember": "Üyeler",
4523
+ "namedApproversLabel": "Adı geçen onaylayıcılar",
4524
+ "namedApproversPlaceholder": "Yazma yetkisi olan herkes",
4525
+ "requiredApprovalsLabel": "Gereken onay sayısı",
4526
+ "requiredApprovalsHint": "Çalışma devam etmeden önce her biri farklı bir kişiden.",
4527
+ "anyoneHint": "Bu panoyu düzenleyebilen herkes bu adımı onaylayabilir.",
4528
+ "gateParametersLabel": "Kapı ayarları"
4513
4529
  }
4514
4530
  },
4515
4531
  "agentPrompt": {
@@ -1455,7 +1455,12 @@
1455
1455
  "notRun": "не запускалося",
1456
1456
  "attempts": "Спроба {attempts} з {maxAttempts}",
1457
1457
  "omittedTestPaths": "Перед запуском доказу пропущено {count} оголошений тестовий файл, тож дерево до виправлення відтворено з неповної репродукції. | Перед запуском доказу пропущено {count} оголошені тестові файли, тож дерево до виправлення відтворено з неповної репродукції. | Перед запуском доказу пропущено {count} оголошених тестових файлів, тож дерево до виправлення відтворено з неповної репродукції."
1458
- }
1458
+ },
1459
+ "quorumProgress": "{recorded} із {required} схвалень",
1460
+ "quorumYours": "(твоє враховано)",
1461
+ "quorumEditLocked": "Виправлення можна буде ввести, коли саме твоє схвалення відкриє цей шлюз. До того часу схвали як є або попроси зміни.",
1462
+ "notAnApprover": "Цей шлюз називає, хто може його розв’язати, і тебе серед них немає.",
1463
+ "approverIdentityRequired": "Цей шлюз називає, хто може його розв’язати, тож це має зробити одна з тих людей, увійшовши в систему."
1459
1464
  },
1460
1465
  "inspector": {
1461
1466
  "frameStatus": {
@@ -4510,6 +4515,17 @@
4510
4515
  "hint": "Максимум для однієї відповіді цього агента. Порожнє поле успадковує значення з вищого рівня, а не знімає обмеження. Значення кроку пайплайна має перевагу над типовим значенням робочого простору.",
4511
4516
  "inherits": "Успадковано",
4512
4517
  "inheritsValue": "Успадковано ({tokens})"
4518
+ },
4519
+ "gateConfig": {
4520
+ "approversLabel": "Хто може схвалити",
4521
+ "roleAdmin": "Адміністратори",
4522
+ "roleMember": "Учасники",
4523
+ "namedApproversLabel": "Названі схвалювачі",
4524
+ "namedApproversPlaceholder": "Будь-хто з правом запису",
4525
+ "requiredApprovalsLabel": "Потрібно схвалень",
4526
+ "requiredApprovalsHint": "Кожне від іншої людини, перш ніж запуск продовжиться.",
4527
+ "anyoneHint": "Будь-хто, хто може редагувати цю дошку, може схвалити цей крок.",
4528
+ "gateParametersLabel": "Налаштування шлюзу"
4513
4529
  }
4514
4530
  },
4515
4531
  "agentPrompt": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.224.0",
3
+ "version": "0.224.1",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,7 +40,7 @@
40
40
  "valibot": "^1.4.2",
41
41
  "vue": "3.5.40",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.240.0"
43
+ "@cat-factory/contracts": "0.241.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",