@cat-factory/app 0.227.0 → 0.228.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -181,6 +181,15 @@ hoc where it can be avoided:
181
181
  control, editable, as soon as any value it edits is set (`false` included: a tri-state
182
182
  `false` is a choice, not absence), so basic mode can never conceal a setting a run will
183
183
  actually use.
184
+ - **The tier may also change which of two routes to the same thing LEADS**, and that is not a
185
+ hidden capability: a `pr_ready` task card offers the outcome summary in both tiers and drops
186
+ the raw pull-request chip in basic, because the card the button opens carries that same link
187
+ at the top. What basic mode may never do is remove the only route (the rule above); ordering
188
+ two routes by which one a tier's reader wants first is what the tier is for. **Write the
189
+ condition as that INVARIANT, not as `isAdvanced` alone** (`TaskCard`'s `showPrChip`: keep the
190
+ chip wherever the outcome card is not offered), because the surface that carries the hidden
191
+ half is itself conditional, and two predicates that must agree by coincidence eventually do
192
+ not: the day the leading route hides, `isAdvanced` alone takes the last route with it.
184
193
 
185
194
  ## Agent tiers (basic / intermediate / advanced)
186
195
 
@@ -700,6 +709,22 @@ example ships in [`deploy/frontend`](../../deploy/frontend) (the `acme:security`
700
709
  additionally leads with `TaskReviewTarget`, linking the pull request it reviews:
701
710
  distinct from the execution panel's link to the PR a run PRODUCED, which a review
702
711
  task never has.
712
+ - **Outcome summary** (`components/outcome`): the run-keyed result window, and what
713
+ the board card and the inspector open to "read the result". It renders the reduction
714
+ in `utils/runOutcome.ts` (requirement coverage joined to the service spec, the
715
+ tester's verdict, the captured views, the recorded checks) and keeps the pull request
716
+ at the top, so the diff stays one click away rather than being the starting point.
717
+ The only result window keyed by a RUN rather than a step: it opens with
718
+ `stepIndex: null` and composes from the whole instance, which is also why it passes
719
+ no `stepRef` to the shell (there is no step to restart from). Available in both
720
+ interface tiers; what `basic` changes is which affordance leads (see
721
+ [interface modes](#interface-modes-basic--advanced)).
722
+ Two rules bind anything added to it. Every entry point gates on `hasOutcomeToShow`
723
+ from the same reduction, so the card is never offered onto sections that all read
724
+ "nothing here". And the window is BLOCK-keyed with the run riding along: a block
725
+ naming a run the store never hydrated is a distinct fact from a task that never ran
726
+ (`RunUnavailableGap`), so a new section reports that case rather than composing from
727
+ the empty step list, which would read as a pipeline that produced nothing.
703
728
  - **Pipeline builder** (`components/pipeline`): assemble/edit agent chains and
704
729
  watch `PipelineProgress`. `PipelinePicker` (+ its `PipelinePreview` pane) is the
705
730
  single way a pipeline is chosen anywhere (add-task, run settings, the recurring
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { Block } from '~/types/domain'
3
3
  import { STATUS_META, MODULE_META, taskTypeMeta } from '~/utils/catalog'
4
+ import { composeRunOutcome, hasOutcomeToShow } from '~/utils/runOutcome'
4
5
  import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
5
6
  import TaskPipelineMini from './TaskPipelineMini.vue'
6
7
 
@@ -72,6 +73,40 @@ const prLabel = computed(() =>
72
73
  pr.value?.number ? t('board.task.prNumber', { number: pr.value.number }) : t('board.task.pr'),
73
74
  )
74
75
 
76
+ /**
77
+ * Reading the result starts at the OUTCOME summary (what changed in product terms, with the
78
+ * captured evidence), and the pull request is one click inside it. In BASIC mode that replaces
79
+ * the card's raw PR chip: the diff is still exactly as reachable, through a surface that says
80
+ * what the diff is about first. Advanced mode keeps both, since a reader who wants the diff
81
+ * directly is the reader that tier is for.
82
+ *
83
+ * Offered only where there is something to read, asked of the SAME reduction the window renders
84
+ * and the inspector's button gates on: a card that offered "read the result" on a task whose
85
+ * every section says "nothing here" would teach people the surface is empty. A task marked done
86
+ * by hand, with no pull request and no run, is that task.
87
+ */
88
+ const uiMode = useUiModeStore()
89
+ const outcomeReadable = computed(() => {
90
+ const block = task.value
91
+ if (!block) return false
92
+ return hasOutcomeToShow(
93
+ composeRunOutcome({ block, instance: execution.getInstance(block.executionId) ?? null }),
94
+ )
95
+ })
96
+ /**
97
+ * The card's raw pull-request chip, which basic mode drops in favour of the outcome card
98
+ * carrying the same link at the top. Written as the INVARIANT ("the diff never stops being
99
+ * reachable from this card") rather than as `isAdvanced` alone, so the tier can only ever
100
+ * reorder two routes and never remove the last one: where the outcome card is not offered,
101
+ * the chip stays in both tiers.
102
+ */
103
+ const showPrChip = computed(
104
+ () => Boolean(pr.value) && (uiMode.isAdvanced || !outcomeReadable.value),
105
+ )
106
+ function openOutcome() {
107
+ ui.openOutcome(props.taskId, task.value?.executionId ?? null)
108
+ }
109
+
75
110
  // This task's current agent run (if any). A failed run must surface the shared
76
111
  // failure banner + retry — NOT a stuck progress bar — so the card never looks
77
112
  // like it's still working after the run has terminated.
@@ -402,8 +437,20 @@ function selectTask() {
402
437
 
403
438
  <template v-if="task.status === 'pr_ready'">
404
439
  <UButton
405
- v-if="pr"
406
- :to="pr.url"
440
+ v-if="outcomeReadable"
441
+ color="primary"
442
+ variant="soft"
443
+ size="xs"
444
+ icon="i-lucide-clipboard-check"
445
+ :title="t('board.task.readOutcomeHint')"
446
+ data-testid="task-open-outcome"
447
+ @click.stop="openOutcome"
448
+ >
449
+ {{ t('board.task.readOutcome') }}
450
+ </UButton>
451
+ <UButton
452
+ v-if="showPrChip"
453
+ :to="pr?.url"
407
454
  target="_blank"
408
455
  rel="noopener"
409
456
  external
@@ -436,12 +483,25 @@ function selectTask() {
436
483
  </UButton>
437
484
  </template>
438
485
 
439
- <span
440
- v-else-if="task.status === 'done'"
441
- class="inline-flex items-center gap-1 text-[9px] text-emerald-400"
442
- >
443
- <UIcon name="i-lucide-check-check" class="h-3 w-3" /> {{ t('board.task.implemented') }}
444
- </span>
486
+ <!-- A merged task is the one people come back to READ, so its result stays openable
487
+ rather than collapsing to a tick the moment it lands. -->
488
+ <template v-else-if="task.status === 'done'">
489
+ <span class="inline-flex items-center gap-1 text-[9px] text-emerald-400">
490
+ <UIcon name="i-lucide-check-check" class="h-3 w-3" /> {{ t('board.task.implemented') }}
491
+ </span>
492
+ <UButton
493
+ v-if="outcomeReadable"
494
+ color="neutral"
495
+ variant="ghost"
496
+ size="xs"
497
+ icon="i-lucide-clipboard-check"
498
+ :title="t('board.task.readOutcomeHint')"
499
+ data-testid="task-open-outcome"
500
+ @click.stop="openOutcome"
501
+ >
502
+ {{ t('board.task.readOutcome') }}
503
+ </UButton>
504
+ </template>
445
505
  </div>
446
506
 
447
507
  <!-- structural metadata: assigned module -->