@cat-factory/app 0.182.0 → 0.182.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.
package/README.md CHANGED
@@ -177,9 +177,13 @@ Two placements are load-bearing enough to state, because putting a new one in th
177
177
  wrong place is invisible until a user cannot find it:
178
178
 
179
179
  - **The sidebar section is a claim about what the destination IS.** `models` is the
180
- engines, `integrations` the optional systems, `infrastructure` where agent
181
- containers and test environments run, `configuration` workspace/account settings.
182
- `nav-contributions.spec.ts` pins the section order and each section's membership.
180
+ model layer — the engines, the per-agent model choice, and the surfaces that
181
+ evaluate a prompt+agent+model combination (Sandbox, Kaizen); `integrations` the
182
+ optional EXTERNAL systems; `infrastructure` where agent containers and test
183
+ environments run; `configuration` workspace/account settings. A surface that
184
+ connects to nothing does not belong in `integrations` however configuration-shaped
185
+ it feels. `nav-contributions.spec.ts` pins the section order and each section's
186
+ membership.
183
187
  - **A flow that edits ONE entity's config is a section of the window that owns that
184
188
  config, not a sibling nav entry.** The guided Docker Compose environment setup
185
189
  (`ComposeEnvironmentSetupSection.vue` → `EnvironmentSetupWizard.vue`) lives inside
@@ -83,12 +83,22 @@ const selected = computed(() => ui.selectedBlockId === props.id)
83
83
  // kept (gated off) so the prior behaviour is one edit away if we want chips back.
84
84
  const showExpanded = computed(() => true)
85
85
 
86
- // Surface a pending decision from this frame OR any of its tasks (O(tasks) map
86
+ // Every child whose parked run the frame badge speaks for: its tasks AND its initiative
87
+ // blocks. An initiative is a frame child like a module and runs an ordinary pipeline (its
88
+ // planner parks on a real approval gate), so leaving it out made a whole class of parked run
89
+ // invisible at frame level — the badge read "nothing needs you" while a plan sat waiting.
90
+ const attentionIds = computed(() => {
91
+ const ids = new Set(taskIds.value)
92
+ for (const i of initiativeBlocks.value) ids.add(i.id)
93
+ return ids
94
+ })
95
+
96
+ // Surface a pending decision from this frame OR any of its children (O(children) map
87
97
  // lookups, not a scan of every open decision per frame).
88
98
  const blockDecisions = computed(() => {
89
99
  const byBlock = execution.decisionsByBlock
90
100
  const out = [...(byBlock.get(props.id) ?? [])]
91
- for (const id of taskIds.value) {
101
+ for (const id of attentionIds.value) {
92
102
  const list = byBlock.get(id)
93
103
  if (list) out.push(...list)
94
104
  }
@@ -100,14 +110,14 @@ function openFirstDecision() {
100
110
  if (d) ui.openDecision(d.instanceId, d.decision.id)
101
111
  }
102
112
 
103
- // Surface a pending approval gate from this frame OR any of its tasks — but NOT an
113
+ // Surface a pending approval gate from this frame OR any of its children — but NOT an
104
114
  // iterative reviewer gate (requirements-review / clarity-review) that's mid-cycle
105
115
  // (incorporating / re-reviewing in the driver), which is background work needing no human,
106
116
  // so it stays off the frame's "Approval" badge.
107
117
  const blockApprovals = computed(() => {
108
118
  const byBlock = execution.approvalsByBlock
109
119
  const candidates = [...(byBlock.get(props.id) ?? [])]
110
- for (const id of taskIds.value) {
120
+ for (const id of attentionIds.value) {
111
121
  const list = byBlock.get(id)
112
122
  if (list) candidates.push(...list)
113
123
  }
@@ -5,12 +5,18 @@
5
5
  // initiative's equivalent "Run planning" (and, while parked mid-interview, "Answer
6
6
  // planning questions") lives right here on the board — the same actions the
7
7
  // inspector offers — so starting an initiative isn't hidden behind selecting it.
8
+ // It likewise mirrors a task card's `attention` affordance: a planning run parked
9
+ // on the plan-approval gate (or on an agent-raised decision) offers the button that
10
+ // opens the window resolving it, instead of leaving the card on a spinning "Run
11
+ // planning" whose only route in was the inspector's execution panel.
8
12
  // The tracker button opens the dedicated window directly. Draggable within its
9
13
  // frame like a task card.
10
14
  import type { InitiativeStatus } from '~/types/domain'
11
15
  import { useBlockDrag } from '~/composables/useBlockDrag'
12
16
  import { useInitiativePlanning } from '~/composables/useInitiativePlanning'
13
17
  import {
18
+ INITIATIVE_ATTENTION_ICONS,
19
+ INITIATIVE_ATTENTION_LABEL_KEYS,
14
20
  INITIATIVE_STATUS_CHIPS,
15
21
  INITIATIVE_STATUS_LABEL_KEYS,
16
22
  initiativeProgress,
@@ -40,6 +46,7 @@ const {
40
46
  running,
41
47
  awaitingAnswers,
42
48
  interviewing,
49
+ attention,
43
50
  starting,
44
51
  runPlanning,
45
52
  openPlanning,
@@ -79,7 +86,10 @@ function onHandle(e: PointerEvent) {
79
86
  data-testid="initiative-card"
80
87
  :data-status="status"
81
88
  class="cursor-pointer rounded-b-lg border border-indigo-800/60 bg-indigo-950/40 p-3 transition hover:border-indigo-600"
82
- :class="[selected ? 'ring-2 ring-indigo-400/60' : '', awaitingAnswers ? 'board-pulse' : '']"
89
+ :class="[
90
+ selected ? 'ring-2 ring-indigo-400/60' : '',
91
+ awaitingAnswers || attention ? 'board-pulse' : '',
92
+ ]"
83
93
  @click.stop="select"
84
94
  >
85
95
  <div class="flex items-start justify-between gap-2">
@@ -106,8 +116,23 @@ function onHandle(e: PointerEvent) {
106
116
  </div>
107
117
  </div>
108
118
  <div class="nodrag mt-2 flex flex-wrap items-center gap-1">
119
+ <!-- Parked for a human: the drafted plan awaits approval, or an agent raised a
120
+ decision. Opens the window that can resolve the park (never the generic panel,
121
+ which the server refuses for a park a dedicated window owns). -->
109
122
  <UButton
110
- v-if="awaitingAnswers"
123
+ v-if="attention"
124
+ data-testid="initiative-card-review"
125
+ :data-attention="attention.kind"
126
+ size="xs"
127
+ variant="solid"
128
+ color="warning"
129
+ :icon="INITIATIVE_ATTENTION_ICONS[attention.kind]"
130
+ @click.stop="attention.open()"
131
+ >
132
+ {{ t(INITIATIVE_ATTENTION_LABEL_KEYS[attention.kind]) }}
133
+ </UButton>
134
+ <UButton
135
+ v-else-if="awaitingAnswers"
111
136
  data-testid="initiative-card-answer-planning"
112
137
  size="xs"
113
138
  variant="solid"
@@ -1,14 +1,20 @@
1
1
  <script setup lang="ts">
2
- // The initiative tracker window — the dedicated read-only view of an initiative's
3
- // plan/tracker entity: goal + constraints, the phases with their per-item status +
4
- // PR links, the execution policy, and the decisions / deviations / follow-ups /
5
- // caveats logs. Renders the DB entity (the source of truth) — never the in-repo
6
- // mirror, which may not exist (GitHub-unwired workspaces). Opened via the universal
7
- // result-view host: from the board card / inspector (`ui.openInitiativeTracker`) or
8
- // as the planner step's result view. Live `initiative` stream events patch the
9
- // store, so an open window follows the plan as it is ingested and later executed.
10
- import { computed, reactive, ref } from 'vue'
2
+ // The initiative tracker window — the dedicated view of an initiative's plan/tracker
3
+ // entity: goal + constraints, the phases with their per-item status + PR links, the
4
+ // execution policy, and the decisions / deviations / follow-ups / caveats logs.
5
+ // Renders the DB entity (the source of truth) — never the in-repo mirror, which may
6
+ // not exist (GitHub-unwired workspaces). Opened via the universal result-view host:
7
+ // from the board card / inspector (`ui.openInitiativeTracker`) or as the planner
8
+ // step's result view. Live `initiative` stream events patch the store, so an open
9
+ // window follows the plan as it is ingested and later executed.
10
+ //
11
+ // It also OWNS the planner's plan-approval gate: this window is where the planner
12
+ // step's park routes (its archetype declares this result view), so the approve /
13
+ // request-changes rail has to live here or the gate has no resolving surface at all —
14
+ // which is exactly how an approved-only-over-REST plan gate shipped.
15
+ import { computed, reactive, ref, watch } from 'vue'
11
16
  import type { InitiativeFollowUp, InitiativeItem } from '~/types/domain'
17
+ import { useInitiativePlanning } from '~/composables/useInitiativePlanning'
12
18
  import {
13
19
  INITIATIVE_FOLLOWUP_STATUS_CHIPS,
14
20
  INITIATIVE_FOLLOWUP_STATUS_LABEL_KEYS,
@@ -23,6 +29,8 @@ import StepRunMeta from '~/components/panels/StepRunMeta.vue'
23
29
 
24
30
  const board = useBoardStore()
25
31
  const initiatives = useInitiativesStore()
32
+ const execution = useExecutionStore()
33
+ const access = useWorkspaceAccess()
26
34
  const { t } = useI18n()
27
35
  const toast = useToast()
28
36
 
@@ -92,6 +100,58 @@ async function checkpointControl(action: 'resume' | 'cancel') {
92
100
  }
93
101
  }
94
102
 
103
+ // ---- Plan review: the planner step's human gate, resolved right here -----------------------
104
+ // Derived from the BLOCK (via the shared planning composable), not from this window's own
105
+ // `stepIndex`: the card / inspector open the tracker with no step, and that is the entry point a
106
+ // human parked on the gate actually uses. So the rail appears on every route into the window.
107
+ const { planApproval } = useInitiativePlanning(() => blockId.value ?? '')
108
+
109
+ /** Draft feedback for "request changes" (the planner re-runs with it), and the rail's in-flight
110
+ * state. Reset when a different initiative opens so a draft can't follow the window. */
111
+ const planFeedback = ref('')
112
+ const requestingChanges = ref(false)
113
+ const resolvingPlan = ref(false)
114
+ watch(blockId, () => {
115
+ planFeedback.value = ''
116
+ requestingChanges.value = false
117
+ })
118
+
119
+ const canRequestChanges = computed(() => planFeedback.value.trim().length > 0)
120
+
121
+ /**
122
+ * Accept the drafted plan: the run advances to the committer, which persists the initiative and
123
+ * arms the execution loop. The window deliberately stays OPEN — the rail disappears with the
124
+ * approval (live), and the tracker is where the plan then starts executing.
125
+ */
126
+ async function approvePlan() {
127
+ const parked = planApproval.value
128
+ if (!parked || resolvingPlan.value) return
129
+ resolvingPlan.value = true
130
+ try {
131
+ await execution.approveStep(parked.instanceId, parked.approval.id)
132
+ } finally {
133
+ resolvingPlan.value = false
134
+ }
135
+ }
136
+
137
+ /** Send the plan back to the planner with what to change; it re-plans and parks again. */
138
+ async function submitPlanChanges() {
139
+ const parked = planApproval.value
140
+ if (!parked || resolvingPlan.value || !canRequestChanges.value) return
141
+ resolvingPlan.value = true
142
+ try {
143
+ const ok = await execution.requestStepChanges(parked.instanceId, parked.approval.id, {
144
+ feedback: planFeedback.value.trim(),
145
+ })
146
+ if (ok) {
147
+ planFeedback.value = ''
148
+ requestingChanges.value = false
149
+ }
150
+ } finally {
151
+ resolvingPlan.value = false
152
+ }
153
+ }
154
+
95
155
  const policyRules = computed(() => initiative.value?.policy?.rules ?? [])
96
156
  function ruleAxes(rule: { minComplexity?: number; minRisk?: number; minImpact?: number }): string {
97
157
  const axes = [
@@ -234,6 +294,86 @@ async function savePolicy() {
234
294
  </div>
235
295
 
236
296
  <template v-else>
297
+ <!-- The planner's human gate: the plan below is drafted but NOT yet committed. This
298
+ is the only surface that can resolve it (the generic approval panel is never
299
+ reached — the planner's archetype routes its park to this window), so approve /
300
+ request changes live here, beside the plan they judge. -->
301
+ <section
302
+ v-if="planApproval"
303
+ class="mb-4 rounded-lg border border-amber-500/40 bg-amber-500/10 p-3.5"
304
+ data-testid="initiative-plan-review"
305
+ >
306
+ <div class="flex items-start gap-2.5">
307
+ <UIcon
308
+ name="i-lucide-clipboard-check"
309
+ class="mt-0.5 h-4 w-4 shrink-0 text-amber-300"
310
+ />
311
+ <div class="min-w-0 flex-1">
312
+ <h3 class="text-[13px] font-semibold text-amber-200">
313
+ {{ t('initiative.planReview.title') }}
314
+ </h3>
315
+ <p class="mt-0.5 text-[12px] leading-relaxed text-amber-100/80">
316
+ {{ t('initiative.planReview.body') }}
317
+ </p>
318
+ <div v-if="!requestingChanges" class="mt-2.5 flex flex-wrap gap-2">
319
+ <button
320
+ class="rounded bg-indigo-600 px-2.5 py-1 text-[11px] font-medium text-white hover:bg-indigo-500 disabled:opacity-50"
321
+ :disabled="resolvingPlan || !access.canExecuteRuns.value"
322
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
323
+ data-testid="initiative-plan-approve"
324
+ @click="approvePlan"
325
+ >
326
+ {{ t('initiative.planReview.approve') }}
327
+ </button>
328
+ <button
329
+ class="rounded border border-amber-400/50 px-2.5 py-1 text-[11px] font-medium text-amber-200 hover:bg-amber-500/10 disabled:opacity-50"
330
+ :disabled="resolvingPlan || !access.canExecuteRuns.value"
331
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
332
+ data-testid="initiative-plan-request-changes"
333
+ @click="requestingChanges = true"
334
+ >
335
+ {{ t('initiative.planReview.requestChanges') }}
336
+ </button>
337
+ </div>
338
+ <!-- Request-changes composer: the feedback is what the planner re-plans FROM,
339
+ so it is required — an empty send would re-run the planner with nothing
340
+ to act on and park again on the same plan. -->
341
+ <div v-else class="mt-2.5">
342
+ <UTextarea
343
+ v-model="planFeedback"
344
+ :rows="3"
345
+ autoresize
346
+ size="sm"
347
+ class="w-full"
348
+ data-testid="initiative-plan-feedback"
349
+ :placeholder="t('initiative.planReview.feedbackPlaceholder')"
350
+ />
351
+ <div class="mt-2 flex flex-wrap gap-2">
352
+ <button
353
+ class="rounded bg-amber-500 px-2.5 py-1 text-[11px] font-medium text-slate-950 hover:bg-amber-400 disabled:opacity-50"
354
+ :disabled="
355
+ resolvingPlan || !canRequestChanges || !access.canExecuteRuns.value
356
+ "
357
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
358
+ data-testid="initiative-plan-send-back"
359
+ @click="submitPlanChanges"
360
+ >
361
+ {{ t('initiative.planReview.sendBack') }}
362
+ </button>
363
+ <button
364
+ class="rounded border border-slate-600 px-2.5 py-1 text-[11px] font-medium text-slate-300 hover:bg-slate-800 disabled:opacity-50"
365
+ :disabled="resolvingPlan"
366
+ data-testid="initiative-plan-cancel-changes"
367
+ @click="requestingChanges = false"
368
+ >
369
+ {{ t('common.cancel') }}
370
+ </button>
371
+ </div>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ </section>
376
+
237
377
  <!-- Paused at a phase checkpoint (D2): a completed checkpoint phase is awaiting
238
378
  review before the next phase spawns. Read the phase's artifacts/PRs below,
239
379
  then resume (continue) or cancel (stop) the initiative right here. -->
@@ -6,7 +6,12 @@
6
6
  // with slice 4.
7
7
  import type { Block, InitiativeStatus } from '~/types/domain'
8
8
  import { useInitiativePlanning } from '~/composables/useInitiativePlanning'
9
- import { INITIATIVE_STATUS_LABEL_KEYS, initiativeProgress } from '~/utils/initiative'
9
+ import {
10
+ INITIATIVE_ATTENTION_ICONS,
11
+ INITIATIVE_ATTENTION_LABEL_KEYS,
12
+ INITIATIVE_STATUS_LABEL_KEYS,
13
+ initiativeProgress,
14
+ } from '~/utils/initiative'
10
15
 
11
16
  const props = defineProps<{ block: Block }>()
12
17
 
@@ -24,6 +29,7 @@ const {
24
29
  running,
25
30
  awaitingAnswers,
26
31
  interviewing,
32
+ attention,
27
33
  starting,
28
34
  runPlanning,
29
35
  openPlanning,
@@ -56,8 +62,23 @@ function control(action: 'pause' | 'resume' | 'cancel') {
56
62
  </p>
57
63
 
58
64
  <div class="flex flex-wrap items-center gap-2">
65
+ <!-- Parked for a human (the drafted plan awaits approval, or an agent raised a decision).
66
+ The same affordance the board card carries, resolved from the same composable — the
67
+ run's park must not be reachable only through the execution panel's step list. -->
59
68
  <UButton
60
- v-if="awaitingAnswers"
69
+ v-if="attention"
70
+ data-testid="initiative-review"
71
+ :data-attention="attention.kind"
72
+ color="warning"
73
+ variant="solid"
74
+ size="sm"
75
+ :icon="INITIATIVE_ATTENTION_ICONS[attention.kind]"
76
+ @click="attention.open()"
77
+ >
78
+ {{ t(INITIATIVE_ATTENTION_LABEL_KEYS[attention.kind]) }}
79
+ </UButton>
80
+ <UButton
81
+ v-else-if="awaitingAnswers"
61
82
  data-testid="initiative-answer-planning"
62
83
  color="primary"
63
84
  variant="solid"
@@ -4,12 +4,25 @@ import { useExecutionStore } from '~/stores/execution'
4
4
  import { useInitiativesStore } from '~/stores/initiative'
5
5
  import { usePipelinesStore } from '~/stores/pipelines'
6
6
  import { useUiStore } from '~/stores/ui'
7
+ import { agentKindMeta } from '~/utils/catalog'
8
+ import { selectPlanApproval, type InitiativeAttentionKind } from '~/utils/initiative'
7
9
  import {
8
10
  INITIATIVE_INTERVIEWER_KIND,
9
11
  interviewGatePhase,
10
12
  interviewStepReached,
11
13
  } from '~/utils/interviewGate'
12
14
 
15
+ /**
16
+ * What an initiative's planning run needs from a human right now: which kind of park it is (the
17
+ * card + inspector resolve its icon/label from the shared `INITIATIVE_ATTENTION_*` maps, so the
18
+ * two surfaces word one park identically), and the action that opens the surface which can
19
+ * RESOLVE it — the step's own dedicated window, via `dispatchStepView`.
20
+ */
21
+ export interface InitiativeAttention {
22
+ kind: InitiativeAttentionKind
23
+ open: () => void
24
+ }
25
+
13
26
  /**
14
27
  * Shared planning affordances for an `initiative`-level block, used by BOTH the board card
15
28
  * (`InitiativeCard`) and the inspector (`InitiativeInspector`) so the two surfaces can never drift
@@ -83,6 +96,60 @@ export function useInitiativePlanning(blockId: MaybeRefOrGetter<string>) {
83
96
  () => interviewPhase.value === 'working' || interviewPhase.value === 'preparing',
84
97
  )
85
98
 
99
+ /**
100
+ * The planner's parked plan-approval gate, or undefined. `pl_initiative` gates the planner step
101
+ * (`{ kind: 'initiative-planner', gate: true }`), so a finished planning pass PARKS the run on a
102
+ * pending `step.approval` until a human accepts the plan — the state this composable's other
103
+ * flags deliberately do NOT cover (the interview has converged, and the run is `blocked`, so
104
+ * `awaitingAnswers` and `interviewing` are both false and the card would otherwise sit on a
105
+ * disabled, spinning "Run planning").
106
+ *
107
+ * The interviewer's own park is excluded by the window its step routes to (see
108
+ * {@link selectPlanApproval}), not by the interview phase, so the two affordances can never both
109
+ * claim one park.
110
+ */
111
+ const planApproval = computed(() =>
112
+ selectPlanApproval(
113
+ execution.approvalsByBlock.get(toValue(blockId)) ?? [],
114
+ (kind) => agentKindMeta(kind).resultView,
115
+ ),
116
+ )
117
+
118
+ /** An agent-raised decision on the planning run (the analyst is an ordinary agent step). */
119
+ const pendingDecision = computed(() => execution.decisionsByBlock.get(toValue(blockId))?.[0])
120
+
121
+ /**
122
+ * The single thing a human has to act on, or null — the initiative dual of a task card's
123
+ * `attention`. A decision outranks an approval (a step never holds both; this is just a stable
124
+ * order). Opening always goes through the step-view dispatch, so the park lands in the window
125
+ * that can RESOLVE it (the plan gate → the tracker window's plan-review rail) rather than a
126
+ * generic panel that would refuse it.
127
+ */
128
+ const attention = computed<InitiativeAttention | null>(() => {
129
+ const id = toValue(blockId)
130
+ const decision = pendingDecision.value
131
+ if (decision) {
132
+ return {
133
+ kind: 'decision',
134
+ open: () => {
135
+ ui.select(id)
136
+ ui.openDecision(decision.instanceId, decision.decision.id)
137
+ },
138
+ }
139
+ }
140
+ const approval = planApproval.value
141
+ if (approval) {
142
+ return {
143
+ kind: 'approval',
144
+ open: () => {
145
+ ui.select(id)
146
+ ui.openApprovalDetail(approval.instanceId, approval.approval.id)
147
+ },
148
+ }
149
+ }
150
+ return null
151
+ })
152
+
86
153
  /**
87
154
  * Optimistic start flag: flip true the instant "Run planning" is clicked, before the stream
88
155
  * pushes the block's `executionId` back. Cleared the moment `running` takes over (success) or the
@@ -123,6 +190,9 @@ export function useInitiativePlanning(blockId: MaybeRefOrGetter<string>) {
123
190
  interviewPhase,
124
191
  awaitingAnswers,
125
192
  interviewing,
193
+ planApproval,
194
+ pendingDecision,
195
+ attention,
126
196
  starting,
127
197
  runPlanning,
128
198
  openPlanning,
@@ -285,10 +285,20 @@ describe('nav grouping helpers', () => {
285
285
  'workspaceContext',
286
286
  'configuration',
287
287
  ])
288
- // The engines are their own section, ahead of the optional integrations; `model-config`
289
- // sits beside the providers it picks models from rather than under `configuration`.
288
+ // The model layer is its own section, ahead of the optional integrations: the engines,
289
+ // the per-agent model choice (beside the providers it picks from, rather than under
290
+ // `configuration`), and the two surfaces that evaluate a prompt+agent+model. Sandbox and
291
+ // Kaizen used to sit under `integrations`, which read as a claim they connect to an
292
+ // external system; they don't — they exercise and grade what this section configures.
290
293
  const models = groups.find((g) => g.group === 'models')
291
- expect(models?.items.map((i) => i.id)).toEqual(['model-providers', 'model-config'])
294
+ expect(models?.items.map((i) => i.id)).toEqual([
295
+ 'model-providers',
296
+ 'model-config',
297
+ 'sandbox',
298
+ 'kaizen',
299
+ ])
300
+ const integrations = groups.find((g) => g.group === 'integrations')
301
+ expect(integrations?.items.map((i) => i.id)).toEqual(['integrations-hub'])
292
302
  const configuration = groups.find((g) => g.group === 'configuration')
293
303
  expect(configuration?.items.map((i) => i.id)).toEqual([
294
304
  'workspace-settings',
@@ -30,11 +30,15 @@ export type NavSurface = 'sidebar' | 'command' | 'toolbar'
30
30
  *
31
31
  * `models` and `integrations` are deliberately SEPARATE sections even though a model
32
32
  * provider is technically also an external system we connect to. They answer different
33
- * questions: `models` is the ENGINE the harnesses run on (no provider ⇒ nothing runs at
34
- * all), `integrations` is the optional systems that feed a run context or receive its
35
- * output (source control, trackers, documents, chat, observability) each of which a
36
- * deployment can live without. Folding the providers in among them buried the one
37
- * connection every deployment must make in a list of ones most never touch.
33
+ * questions: `models` is the MODEL LAYER — which engine the harnesses run on (no provider
34
+ * ⇒ nothing runs at all), which model each agent kind uses, and how well a given
35
+ * prompt+agent+model actually performs (`sandbox`, `kaizen`); `integrations` is the
36
+ * optional EXTERNAL SYSTEMS that feed a run context or receive its output (source control,
37
+ * trackers, documents, chat, observability) each of which a deployment can live without.
38
+ * Folding the providers in among them buried the one connection every deployment must make
39
+ * in a list of ones most never touch; parking the two model-quality surfaces there was the
40
+ * same mistake from the other end — neither Sandbox nor Kaizen connects to anything, they
41
+ * evaluate what the `models` section configures.
38
42
  */
39
43
  export type NavSidebarGroup =
40
44
  | 'create'
@@ -268,33 +272,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
268
272
  testId: 'nav-integrations',
269
273
  sidebar: { group: 'integrations', order: 10 },
270
274
  },
271
- {
272
- id: 'sandbox',
273
- labelKey: 'nav.sandbox',
274
- icon: 'i-lucide-flask-conical',
275
- surfaces: S('sidebar', 'command'),
276
- advanced: true,
277
- gate: (g) => g.canManageIntegrations,
278
- action: 'sandbox',
279
- testId: 'nav-sandbox',
280
- sidebar: { group: 'integrations', order: 20 },
281
- command: {
282
- group: 'workspace',
283
- order: 70,
284
- labelKey: 'layout.commandBar.cmd.sandbox',
285
- keywordsKey: 'layout.commandBar.keywords.sandbox',
286
- },
287
- },
288
- {
289
- id: 'kaizen',
290
- labelKey: 'nav.kaizen',
291
- icon: 'i-lucide-sparkles',
292
- surfaces: S('sidebar'),
293
- advanced: true,
294
- action: 'kaizen',
295
- testId: 'nav-kaizen',
296
- sidebar: { group: 'integrations', order: 30 },
297
- },
298
275
  {
299
276
  id: 'infrastructure',
300
277
  labelKey: 'nav.infrastructure',
@@ -370,6 +347,39 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
370
347
  keywordsKey: 'layout.commandBar.keywords.modelConfiguration',
371
348
  },
372
349
  },
350
+ {
351
+ // Trying prompt versions and models against graded fixtures — a model-layer surface,
352
+ // not an integration: it connects to no external system, it exercises the providers
353
+ // and per-agent models the two entries above configure.
354
+ id: 'sandbox',
355
+ labelKey: 'nav.sandbox',
356
+ icon: 'i-lucide-flask-conical',
357
+ surfaces: S('sidebar', 'command'),
358
+ advanced: true,
359
+ gate: (g) => g.canManageIntegrations,
360
+ action: 'sandbox',
361
+ testId: 'nav-sandbox',
362
+ sidebar: { group: 'models', order: 30 },
363
+ command: {
364
+ group: 'workspace',
365
+ order: 70,
366
+ labelKey: 'layout.commandBar.cmd.sandbox',
367
+ keywordsKey: 'layout.commandBar.keywords.sandbox',
368
+ },
369
+ },
370
+ {
371
+ // The same axis after the fact: grading history and verified prompt+agent+model combos.
372
+ // Sandbox asks "which combination should we use", Kaizen answers "how is the one we
373
+ // shipped doing" — both read the model layer, so they sit with it.
374
+ id: 'kaizen',
375
+ labelKey: 'nav.kaizen',
376
+ icon: 'i-lucide-sparkles',
377
+ surfaces: S('sidebar'),
378
+ advanced: true,
379
+ action: 'kaizen',
380
+ testId: 'nav-kaizen',
381
+ sidebar: { group: 'models', order: 40 },
382
+ },
373
383
  {
374
384
  id: 'service-fragment-defaults',
375
385
  labelKey: 'layout.commandBar.cmd.serviceFragmentDefaults',
@@ -1,7 +1,12 @@
1
1
  import { INITIATIVE_ITEM_TERMINAL_STATUSES } from '@cat-factory/contracts'
2
2
  import { describe, it, expect } from 'vitest'
3
3
  import type { InitiativeItem, InitiativePhase, InitiativeQa } from '~/types/domain'
4
- import { isPendingQuestion, orderInterviewQuestions, pendingCheckpointPhase } from './initiative'
4
+ import {
5
+ isPendingQuestion,
6
+ orderInterviewQuestions,
7
+ pendingCheckpointPhase,
8
+ selectPlanApproval,
9
+ } from './initiative'
5
10
 
6
11
  // `pendingCheckpointPhase` mirrors the backend `pendingCheckpoint` (orchestration
7
12
  // `initiative.logic.ts`); these pin the same ordering/edge cases the loop pauses on, so the
@@ -150,3 +155,48 @@ describe('orderInterviewQuestions', () => {
150
155
  expect(orderInterviewQuestions([])).toEqual([])
151
156
  })
152
157
  })
158
+
159
+ // The plan-review park: which of a block's pending approvals the board card / inspector offer as
160
+ // "Review plan", and which one they must leave alone. Both gates on the planning pipeline park on
161
+ // a `step.approval`, so the interviewer's park (owned by the planning window's "Answer planning
162
+ // questions") is the case this selector exists to keep out — offering it here would give one park
163
+ // two differently-worded buttons, and the tracker window it opened could not resolve it.
164
+
165
+ /** A pending approval as `execution.approvalsByBlock` carries it (only the fields read here). */
166
+ const parked = (agentKind: string, id: string) => ({ agentKind, approval: { id } })
167
+
168
+ /** The catalog's result-view resolver, as the composable passes it in. */
169
+ const resultViewOf = (kind: string): string | undefined =>
170
+ kind === 'initiative-interviewer'
171
+ ? 'initiative-planning'
172
+ : kind.startsWith('initiative-')
173
+ ? 'initiative-tracker'
174
+ : undefined
175
+
176
+ describe('selectPlanApproval', () => {
177
+ it('is undefined when nothing is parked', () => {
178
+ expect(selectPlanApproval([], resultViewOf)).toBeUndefined()
179
+ })
180
+
181
+ it('picks the planner gate — the plan awaiting approval', () => {
182
+ const approvals = [parked('initiative-planner', 'ap_1')]
183
+ expect(selectPlanApproval(approvals, resultViewOf)?.approval.id).toBe('ap_1')
184
+ })
185
+
186
+ it('leaves the interviewer park to the planning window', () => {
187
+ const approvals = [parked('initiative-interviewer', 'ap_interview')]
188
+ expect(selectPlanApproval(approvals, resultViewOf)).toBeUndefined()
189
+ })
190
+
191
+ it('finds the plan gate past an interview park (a re-run interviewing again)', () => {
192
+ const approvals = [parked('initiative-interviewer', 'ap_interview'), parked('x', 'ap_plan')]
193
+ expect(selectPlanApproval(approvals, resultViewOf)?.approval.id).toBe('ap_plan')
194
+ })
195
+
196
+ it('offers a gated step of a custom planning pipeline, whatever window it routes to', () => {
197
+ // A kind with no dedicated window at all still parks a human; the affordance opens whatever
198
+ // `dispatchStepView` routes it to (the generic panel), which is exactly what resolves it.
199
+ const approvals = [parked('some-custom-kind', 'ap_custom')]
200
+ expect(selectPlanApproval(approvals, resultViewOf)?.approval.id).toBe('ap_custom')
201
+ })
202
+ })
@@ -59,6 +59,50 @@ export const INITIATIVE_ITEM_STATUS_CHIPS: Record<InitiativeItemStatus, BadgeCol
59
59
  skipped: 'neutral',
60
60
  }
61
61
 
62
+ /**
63
+ * The two ways an initiative's planning run parks for a human: an agent-raised `decision`, or a
64
+ * pending step `approval` (the plan-approval gate the `initiative-planner` step carries). Resolved
65
+ * per block by `useInitiativePlanning().attention`.
66
+ */
67
+ export type InitiativeAttentionKind = 'decision' | 'approval'
68
+
69
+ /** Park kind → i18n label key for the card/inspector button that opens the resolving window. */
70
+ export const INITIATIVE_ATTENTION_LABEL_KEYS: Record<InitiativeAttentionKind, string> = {
71
+ decision: 'initiative.inspector.resolveDecision',
72
+ approval: 'initiative.inspector.reviewPlan',
73
+ }
74
+
75
+ /** Park kind → button icon, so the board card and the inspector can't diverge on one park. */
76
+ export const INITIATIVE_ATTENTION_ICONS: Record<InitiativeAttentionKind, string> = {
77
+ decision: 'i-lucide-circle-help',
78
+ approval: 'i-lucide-clipboard-check',
79
+ }
80
+
81
+ /**
82
+ * The result view the INTERVIEW gate owns. Its park rides the same `step.approval` mechanism as
83
+ * every other gate, so anything offering "there is a plan to review here" has to exclude it: the
84
+ * interview park is already owned by the planning window, behind the differently-worded "Answer
85
+ * planning questions".
86
+ */
87
+ export const INTERVIEW_GATE_RESULT_VIEW = 'initiative-planning'
88
+
89
+ /**
90
+ * The block's parked approval that is a PLAN REVIEW — the planner's human gate (`pl_initiative`
91
+ * declares `{ kind: 'initiative-planner', gate: true }`), or any other gated step of a custom
92
+ * planning pipeline — as opposed to the interviewer's park.
93
+ *
94
+ * Discriminated by the step's own result view, the seam `dispatchStepView` routes on, rather than
95
+ * by an agent-kind list or by the interview phase: the affordance's ACTION is that dispatch, so
96
+ * keying the offer on the same fact guarantees the button opens a window that can resolve what it
97
+ * offered — and that the interview and plan affordances can never both claim one park.
98
+ */
99
+ export function selectPlanApproval<A extends { agentKind: string }>(
100
+ approvals: readonly A[],
101
+ resultViewOf: (agentKind: string) => string | undefined,
102
+ ): A | undefined {
103
+ return approvals.find((a) => resultViewOf(a.agentKind) !== INTERVIEW_GATE_RESULT_VIEW)
104
+ }
105
+
62
106
  /** Follow-up triage status → i18n label key. Exhaustive so a new status fails the build. */
63
107
  export const INITIATIVE_FOLLOWUP_STATUS_LABEL_KEYS: Record<InitiativeFollowUp['status'], string> = {
64
108
  open: 'initiative.followUpStatus.open',
@@ -4282,12 +4282,22 @@
4282
4282
  "inspector": {
4283
4283
  "runPlanning": "Planung ausführen",
4284
4284
  "answerPlanning": "Planungsfragen beantworten",
4285
+ "reviewPlan": "Plan prüfen",
4286
+ "resolveDecision": "Auflösen",
4285
4287
  "planningInProgress": "Planung läuft",
4286
4288
  "pause": "Pausieren",
4287
4289
  "resume": "Fortsetzen",
4288
4290
  "cancel": "Initiative abbrechen",
4289
4291
  "hint": "Die Planning-Pipeline erkundet die Codebasis, entwirft den mehrphasigen Plan zur Freigabe und committet dann das Tracker-Dokument ins Repository."
4290
4292
  },
4293
+ "planReview": {
4294
+ "title": "Dieser Plan wartet auf dich",
4295
+ "body": "Der Planner hat die Phasen und Aufgaben unten entworfen. Gib sie frei, um den Plan zu committen und die Arbeit zu starten, oder schicke den Plan mit deinen Änderungswünschen zurück.",
4296
+ "approve": "Plan freigeben",
4297
+ "requestChanges": "Änderungen anfordern",
4298
+ "feedbackPlaceholder": "Was soll der Planner ändern? Umfang, Reihenfolge der Phasen, fehlende Arbeit, eine Aufgabe, die woanders hingehört …",
4299
+ "sendBack": "An den Planner zurückschicken"
4300
+ },
4291
4301
  "planning": {
4292
4302
  "title": "Die Initiative planen",
4293
4303
  "subtitle": "Beantworten Sie die Fragen des Planers, damit er die Initiative eingrenzen kann",
@@ -5459,12 +5459,22 @@
5459
5459
  "inspector": {
5460
5460
  "runPlanning": "Run planning",
5461
5461
  "answerPlanning": "Answer planning questions",
5462
+ "reviewPlan": "Review plan",
5463
+ "resolveDecision": "Resolve",
5462
5464
  "planningInProgress": "Planning in progress",
5463
5465
  "pause": "Pause",
5464
5466
  "resume": "Resume",
5465
5467
  "cancel": "Cancel initiative",
5466
5468
  "hint": "The planning pipeline explores the codebase, drafts the multi-phase plan for approval, then commits the tracker document to the repository."
5467
5469
  },
5470
+ "planReview": {
5471
+ "title": "This plan is waiting for you",
5472
+ "body": "The planner drafted the phases and items below. Approve them to commit the plan and start the work, or send the plan back with what to change.",
5473
+ "approve": "Approve plan",
5474
+ "requestChanges": "Request changes",
5475
+ "feedbackPlaceholder": "What should the planner change? Scope, phase order, missing work, an item that belongs elsewhere…",
5476
+ "sendBack": "Send back to the planner"
5477
+ },
5468
5478
  "planning": {
5469
5479
  "title": "Plan the initiative",
5470
5480
  "subtitle": "Answer the planner's questions so it can scope the initiative",
@@ -5294,12 +5294,22 @@
5294
5294
  "inspector": {
5295
5295
  "runPlanning": "Ejecutar planificacion",
5296
5296
  "answerPlanning": "Responder preguntas de planificacion",
5297
+ "reviewPlan": "Revisar plan",
5298
+ "resolveDecision": "Resolver",
5297
5299
  "planningInProgress": "Planificacion en curso",
5298
5300
  "pause": "Pausar",
5299
5301
  "resume": "Reanudar",
5300
5302
  "cancel": "Cancelar iniciativa",
5301
5303
  "hint": "El pipeline de planificacion explora el codigo, redacta el plan multifase para su aprobacion y luego confirma el documento de seguimiento en el repositorio."
5302
5304
  },
5305
+ "planReview": {
5306
+ "title": "Este plan te está esperando",
5307
+ "body": "El planificador redactó las fases y los elementos de abajo. Apruébalos para confirmar el plan y empezar el trabajo, o devuelve el plan indicando qué cambiar.",
5308
+ "approve": "Aprobar plan",
5309
+ "requestChanges": "Solicitar cambios",
5310
+ "feedbackPlaceholder": "¿Qué debería cambiar el planificador? Alcance, orden de las fases, trabajo que falta, un elemento que va en otro sitio…",
5311
+ "sendBack": "Devolver al planificador"
5312
+ },
5303
5313
  "planning": {
5304
5314
  "title": "Planificar la iniciativa",
5305
5315
  "subtitle": "Responde las preguntas del planificador para acotar la iniciativa",
@@ -5294,12 +5294,22 @@
5294
5294
  "inspector": {
5295
5295
  "runPlanning": "Lancer la planification",
5296
5296
  "answerPlanning": "Repondre aux questions de planification",
5297
+ "reviewPlan": "Examiner le plan",
5298
+ "resolveDecision": "Résoudre",
5297
5299
  "planningInProgress": "Planification en cours",
5298
5300
  "pause": "Mettre en pause",
5299
5301
  "resume": "Reprendre",
5300
5302
  "cancel": "Annuler l'initiative",
5301
5303
  "hint": "Le pipeline de planification explore le code, redige le plan multiphase pour approbation, puis valide le document de suivi dans le depot."
5302
5304
  },
5305
+ "planReview": {
5306
+ "title": "Ce plan vous attend",
5307
+ "body": "Le planificateur a rédigé les phases et les éléments ci-dessous. Approuvez-les pour valider le plan et lancer le travail, ou renvoyez le plan en indiquant ce qu'il faut changer.",
5308
+ "approve": "Approuver le plan",
5309
+ "requestChanges": "Demander des modifications",
5310
+ "feedbackPlaceholder": "Que doit changer le planificateur ? Périmètre, ordre des phases, travail manquant, un élément qui a sa place ailleurs…",
5311
+ "sendBack": "Renvoyer au planificateur"
5312
+ },
5303
5313
  "planning": {
5304
5314
  "title": "Planifier l'initiative",
5305
5315
  "subtitle": "Repondez aux questions du planificateur pour cadrer l'initiative",
@@ -5305,12 +5305,22 @@
5305
5305
  "inspector": {
5306
5306
  "runPlanning": "הרצת תכנון",
5307
5307
  "answerPlanning": "מענה על שאלות התכנון",
5308
+ "reviewPlan": "בדיקת התוכנית",
5309
+ "resolveDecision": "פתור",
5308
5310
  "planningInProgress": "התכנון מתבצע",
5309
5311
  "pause": "השהה",
5310
5312
  "resume": "המשך",
5311
5313
  "cancel": "ביטול היוזמה",
5312
5314
  "hint": "צינור התכנון חוקר את הקוד, מנסח את התוכנית הרב-שלבית לאישור, ואז שומר את מסמך המעקב במאגר."
5313
5315
  },
5316
+ "planReview": {
5317
+ "title": "התוכנית הזו ממתינה לך",
5318
+ "body": "המתכנן ניסח את השלבים והפריטים שלמטה. אשרו אותם כדי לשמור את התוכנית ולהתחיל בעבודה, או החזירו את התוכנית עם מה שצריך לשנות.",
5319
+ "approve": "אישור התוכנית",
5320
+ "requestChanges": "בקשת שינויים",
5321
+ "feedbackPlaceholder": "מה המתכנן צריך לשנות? היקף, סדר השלבים, עבודה חסרה, פריט ששייך למקום אחר…",
5322
+ "sendBack": "החזרה למתכנן"
5323
+ },
5314
5324
  "planning": {
5315
5325
  "title": "תכנון היוזמה",
5316
5326
  "subtitle": "ענה על שאלות המתכנן כדי למקד את היוזמה",
@@ -4282,12 +4282,22 @@
4282
4282
  "inspector": {
4283
4283
  "runPlanning": "Esegui pianificazione",
4284
4284
  "answerPlanning": "Rispondi alle domande di pianificazione",
4285
+ "reviewPlan": "Rivedi il piano",
4286
+ "resolveDecision": "Risolvi",
4285
4287
  "planningInProgress": "Pianificazione in corso",
4286
4288
  "pause": "Pausa",
4287
4289
  "resume": "Riprendi",
4288
4290
  "cancel": "Annulla iniziativa",
4289
4291
  "hint": "La pipeline di pianificazione esplora il codebase, redige il piano multi-fase per l'approvazione, poi effettua il commit del documento del tracker nel repository."
4290
4292
  },
4293
+ "planReview": {
4294
+ "title": "Questo piano ti sta aspettando",
4295
+ "body": "Il planner ha redatto le fasi e gli elementi qui sotto. Approvali per confermare il piano e avviare il lavoro, oppure rimanda indietro il piano indicando cosa cambiare.",
4296
+ "approve": "Approva il piano",
4297
+ "requestChanges": "Richiedi modifiche",
4298
+ "feedbackPlaceholder": "Cosa deve cambiare il planner? Ambito, ordine delle fasi, lavoro mancante, un elemento che sta altrove…",
4299
+ "sendBack": "Rimanda al planner"
4300
+ },
4291
4301
  "planning": {
4292
4302
  "title": "Pianifica l'iniziativa",
4293
4303
  "subtitle": "Rispondi alle domande del pianificatore affinche possa definire l'ambito dell'iniziativa",
@@ -5306,12 +5306,22 @@
5306
5306
  "inspector": {
5307
5307
  "runPlanning": "計画を実行",
5308
5308
  "answerPlanning": "計画の質問に回答",
5309
+ "reviewPlan": "計画をレビュー",
5310
+ "resolveDecision": "解決",
5309
5311
  "planningInProgress": "計画を実行中",
5310
5312
  "pause": "一時停止",
5311
5313
  "resume": "再開",
5312
5314
  "cancel": "イニシアチブをキャンセル",
5313
5315
  "hint": "計画パイプラインはコードベースを調査し、承認用の複数フェーズ計画を起草し、その後トラッカー文書をリポジトリにコミットします。"
5314
5316
  },
5317
+ "planReview": {
5318
+ "title": "この計画が承認を待っています",
5319
+ "body": "プランナーが以下のフェーズと項目を起草しました。承認すると計画がコミットされ、作業が始まります。変更したい点がある場合は、その内容を添えて計画を差し戻してください。",
5320
+ "approve": "計画を承認",
5321
+ "requestChanges": "変更を依頼",
5322
+ "feedbackPlaceholder": "プランナーに変更してほしい点は何ですか。範囲、フェーズの順序、不足している作業、別の場所に属する項目など…",
5323
+ "sendBack": "プランナーに差し戻す"
5324
+ },
5315
5325
  "planning": {
5316
5326
  "title": "イニシアチブを計画",
5317
5327
  "subtitle": "プランナーの質問に答えてイニシアチブの範囲を定めます",
@@ -5294,12 +5294,22 @@
5294
5294
  "inspector": {
5295
5295
  "runPlanning": "Uruchom planowanie",
5296
5296
  "answerPlanning": "Odpowiedz na pytania planowania",
5297
+ "reviewPlan": "Przejrzyj plan",
5298
+ "resolveDecision": "Rozwiąż",
5297
5299
  "planningInProgress": "Planowanie w toku",
5298
5300
  "pause": "Wstrzymaj",
5299
5301
  "resume": "Wznow",
5300
5302
  "cancel": "Anuluj inicjatywe",
5301
5303
  "hint": "Pipeline planowania bada kod, przygotowuje wielofazowy plan do zatwierdzenia, a nastepnie zapisuje dokument trackera w repozytorium."
5302
5304
  },
5305
+ "planReview": {
5306
+ "title": "Ten plan czeka na Ciebie",
5307
+ "body": "Planer przygotował poniższe fazy i elementy. Zatwierdź je, aby zapisać plan i rozpocząć pracę, albo odeślij plan z informacją, co zmienić.",
5308
+ "approve": "Zatwierdź plan",
5309
+ "requestChanges": "Poproś o zmiany",
5310
+ "feedbackPlaceholder": "Co planer ma zmienić? Zakres, kolejność faz, brakująca praca, element, który pasuje gdzie indziej…",
5311
+ "sendBack": "Odeślij do planera"
5312
+ },
5303
5313
  "planning": {
5304
5314
  "title": "Zaplanuj inicjatywe",
5305
5315
  "subtitle": "Odpowiedz na pytania planisty, aby okreslic zakres inicjatywy",
@@ -5306,12 +5306,22 @@
5306
5306
  "inspector": {
5307
5307
  "runPlanning": "Planlamayi calistir",
5308
5308
  "answerPlanning": "Planlama sorularini yanitla",
5309
+ "reviewPlan": "Planı incele",
5310
+ "resolveDecision": "Çöz",
5309
5311
  "planningInProgress": "Planlama suruyor",
5310
5312
  "pause": "Duraklat",
5311
5313
  "resume": "Devam et",
5312
5314
  "cancel": "Girisimi iptal et",
5313
5315
  "hint": "Planlama hatti kod tabanini inceler, onay icin cok asamali plani hazirlar ve ardindan izleyici belgesini depoya kaydeder."
5314
5316
  },
5317
+ "planReview": {
5318
+ "title": "Bu plan sizi bekliyor",
5319
+ "body": "Planlayıcı aşağıdaki aşamaları ve maddeleri hazırladı. Planı kaydedip işe başlamak için onaylayın ya da neyin değişmesi gerektiğini yazarak planı geri gönderin.",
5320
+ "approve": "Planı onayla",
5321
+ "requestChanges": "Değişiklik iste",
5322
+ "feedbackPlaceholder": "Planlayıcı neyi değiştirmeli? Kapsam, aşama sırası, eksik iş, başka yere ait bir madde…",
5323
+ "sendBack": "Planlayıcıya geri gönder"
5324
+ },
5315
5325
  "planning": {
5316
5326
  "title": "Girisimi planla",
5317
5327
  "subtitle": "Girisimi kapsamlandirmak icin planlayicinin sorularini yanitlayin",
@@ -5294,12 +5294,22 @@
5294
5294
  "inspector": {
5295
5295
  "runPlanning": "Запустити планування",
5296
5296
  "answerPlanning": "Відповісти на питання планування",
5297
+ "reviewPlan": "Переглянути план",
5298
+ "resolveDecision": "Вирішити",
5297
5299
  "planningInProgress": "Планування триває",
5298
5300
  "pause": "Призупинити",
5299
5301
  "resume": "Відновити",
5300
5302
  "cancel": "Скасувати ініціативу",
5301
5303
  "hint": "Пайплайн планування досліджує кодову базу, готує багатофазний план на затвердження, а потім комітить документ трекера до репозиторію."
5302
5304
  },
5305
+ "planReview": {
5306
+ "title": "Цей план чекає на вас",
5307
+ "body": "Планувальник підготував фази та елементи нижче. Затвердіть їх, щоб зафіксувати план і розпочати роботу, або поверніть план із зазначенням, що змінити.",
5308
+ "approve": "Затвердити план",
5309
+ "requestChanges": "Запросити зміни",
5310
+ "feedbackPlaceholder": "Що має змінити планувальник? Обсяг, порядок фаз, пропущена робота, елемент, якому місце деінде…",
5311
+ "sendBack": "Повернути планувальнику"
5312
+ },
5303
5313
  "planning": {
5304
5314
  "title": "Спланувати ініціативу",
5305
5315
  "subtitle": "Дайте відповіді на питання планувальника, щоб окреслити ініціативу",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.182.0",
3
+ "version": "0.182.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",