Skip to content

release: 1.3.3 — compositions-table pagination/virtualization + Autopilot anti-confabulation - #30

Merged
braghettos merged 119 commits into
mainfrom
release/1.3.3
Jul 23, 2026
Merged

release: 1.3.3 — compositions-table pagination/virtualization + Autopilot anti-confabulation#30
braghettos merged 119 commits into
mainfrom
release/1.3.3

Conversation

@braghettos

Copy link
Copy Markdown
Collaborator

Cut as tag 1.3.3 (CI builds ghcr.io/braghettos/krateo-frontend:1.3.3). Merged to main so the fixes don't regress in later releases (the 1.3.2 lesson).

Pagination/virtualization/compositions no longer wedges at ~60k rows: server-side window slice (perPage=50) + antd6 virtual Table; controlled pager when the widget emits pagination.total. (compositions-table CR template slices on .slice — portal-chart PR separate.)

Autopilot anti-confabulation — every-turn grounding guardrail + truthful pageStatus/large/loadState so page-load questions are answered from page context, never blamed on an unrelated crashlooping pod. Reconciled with main's PORTAL_HOUSE_RULES + value/loading/stale collector (nothing regressed).

Also carries the ?ask=<prompt> deep-link. Gates: build ✓, lint ✓, vitest 192/192, tsc 0 errors.

🤖 Generated with Claude Code

braghettos and others added 30 commits July 7, 2026 00:12
AutopilotProvider reads `?ask=<prompt>` from the URL once per visit → opens the rail
and sends the prompt (guarded so a refresh doesn't re-ask), then strips the param.
Lets any widget start an Autopilot turn via a plain `navigate` action (no new action
type) — e.g. an observability alert's "Troubleshoot with Autopilot" button navigates
to `/observability?ask=<troubleshooting prompt>`. The context collector already carries
the page's telemetry, so the analysis is grounded in what's on screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bounded server-side WINDOW slice (perPage=50) + antd6 virtual table so the
/compositions page stops wedging at ~60k rows. useWidgetQuery requests
page/perPage when a resource opts in; WidgetRenderer maps tables→50; Table
enables virtual + a controlled server pager only when the widget emits
pagination.total (the 6 non-slicing tables keep prior behavior).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every-turn grounding guardrail (page-load/render questions answered from page
context only, never blamed on unrelated cluster-workload health) + collector
now reports per-widget loadState + pageStatus (heavy for the 60k table), so
Autopilot names the real cause instead of an unrelated crashloop pod.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ilot anti-confabulation

Merge feat/autopilot-ask-deeplink into main. Table: server-side window pagination (perPage=50) + antd6 virtual table so /compositions no longer wedges at ~60k rows; controlled pager when the widget emits pagination.total. Autopilot: every-turn anti-confabulation grounding guardrail + truthful pageStatus/large/loadState (page-load questions answered from page context, never blamed on unrelated cluster-workload health), reconciled with main's PORTAL_HOUSE_RULES + value/loading/stale collector. Also carries the ?ask deep-link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fieldHeader + SchemaForm section labels rendered the raw values.schema.json KEY
(azs, cidr) instead of the human title the blueprint author wrote. Prefer node.title
when present, fall back to key — a novice now sees 'Availability zones', not 'azs'.
Fixes every blueprint + install create-form at once (UX audit, onboarding dim).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…freshness

Delivery-plan Wave 0 + Wave 1 (parallel build, combined gates green: tsc + 239 tests + build):
- W0-1 Autopilot verb registry: declarative {name,sideEffect,argSchema,apply} REGISTRY
  replaces the inline READONLY_VERBS Set + verb if-ladder in actionBridge.ts; deny-by-default
  is now a data property (unknown verb OR sideEffect:'write' -> denied). previewBlueprint/
  previewPage read-only verbs added as one-line entries (the Wave-4 builders' preview hook).
  runAction forced-confirm path kept distinct + gated.
- W0-5 expose resolved-resource identity: WidgetInventoryEntry gains resource{gvr,name,ns,uid}
  from status.resourcesRefs (the day-2 grounding the ops copilot needs), passing the redactor
  unchanged.
- Freshness & session honesty (Wave 1): FreshnessBadge (LIVE/Refreshing/Stale·Nm) via react-query
  dataUpdatedAt/isStale/isFetching, a distinct calm widget-TIMEOUT state, and an in-place
  session-resume prompt (/login?next=<route>) instead of a hard 401 wipe. Fixes UX-audit perf/44.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… wirings

Delivery-plan Wave 0 W0-2 + Wave 1 follow-ups (combined gates green: tsc + 271 tests + build):
- W0-2 HITL blast-radius gate: EVERY mutating verb (POST/PUT/PATCH/DELETE) is now ALWAYS
  human-gated in the single write chokepoint (useHandleActions runRest) — was opt-in via
  requireConfirmation (a real bypass). BlastRadiusConfirm shows verb+GVR+cluster/ns+object-count
  +diff; DELETE gets a danger button; decline returns before the fetch (no write). Covers Form
  submit, row actions, and Autopilot runAction (one chokepoint). buildBlastRadius is a pure,
  React-free kernel with full unit coverage.
- Freshness follow-ups: isWidgetArmed() exported from refreshSse (liveArmed now reflects a real
  open /refreshes channel, not a proxy); useWidgetQuery returns timedOut (WidgetRenderer drives
  the calm timeout state off it); Login consumes ?next= (open-redirect-safe resolveNextPath) so
  the 1.3.5 session-resume returns users to where they were.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iation control

Wave 1 Day-2 ops, part A (pre-defined actions). Teaches the copilot: when a failing
composition can be fixed by a control ALREADY on the page — Resume a paused one, Sync a
stuck one, Update an out-of-date one — proactively propose that ONE runAction instead of
only describing the problem. The user confirms it in the W0-2 blast-radius dialog (verb +
resource + ns/cluster + diff), so it's human-gated + safe. Prefer least-disruptive; never
propose Delete as a fix unless asked. Capabilities + house-rule 9. (Loop already gated by
W0-2/1.3.6; this makes it proactive.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng verb

Wave 1 Day-2 ops, part B. A new patchField portal-action lets the copilot change ONE
spec field of the composition on screen (the 'change a param to remediate' case), routed
through the SAME W0-2 blast-radius gate (human confirms the diff). Two safety layers:
- isPatchAllowed scoping kernel (pure, unit-tested): composition kinds ONLY (group ends
  composition.krateo.io) + a single simple spec.<key> ONLY — rejects arbitrary resources,
  metadata/status/deletion, nested paths, wildcards.
- dispatched as a PATCH rest action via handleAction -> runRest isMutatingVerb -> W0-2
  BlastRadiusConfirm (never bypasses ctx.confirm; server-side /call RBAC still applies).
Capability + house-rule 10 (patch only a visible spec field of the on-screen composition).
(Also folds in the refreshSse/useWidgetQuery test files missed from the 1.3.6 add.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hips (Wave 1)

Wave-1 Onboarding copilot — frontend pieces (server-driven; the onboarding
empty-state CR ships in krateo-portal-chart).

- Listy: new `hideWhenEmpty` — when the RA emits no items, render nothing
  (null) instead of the antd Empty "No data" box. This turns a Listy into a
  server-driven conditional section: the onboarding empty-state's RA emits
  intent tiles only when the user owns zero compositions, so the whole card
  appears/vanishes purely with the data (no client "we decided you're new" flag).
- AutopilotRail: curated starter prompts in the empty rail ("Show me around",
  "How do I create my first resource?", "What's on this page?") so a
  zero-knowledge user has an obvious first move before the first turn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y for exceptions

A FreshnessBadge overlaid on EVERY widget in the steady "Live" state is noise, not
signal: when everything says "Live", the badge conveys nothing and just clutters a
dense dashboard (40 widgets → 40 pills). Freshness is only worth surfacing for the
states a user should notice — "Refreshing…" (transient) and "Stale" (actionable, with
the ↻ refresh affordance).

WidgetRenderer now derives the freshness tone and renders the widget with NO overlay
when tone === 'live'; the overlay appears only for refreshing/stale. deriveFreshnessState
is unchanged (still unit-tested); this gates the render, not the state machine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the "Live badge on every widget" fix: after suppressing the steady
Live pill, widgets that aren't SSE-armed fell through to a text "Updated just now"
pill (and some "Stale"), which is still noise. Now:

- The overlay renders ONLY when a widget is actively refreshing OR genuinely stale
  (isStale). Live / just-updated / not-armed states show NOTHING.
- The indicator is a small color-coded DOT (no text): amber pulsing = refreshing,
  muted graphite = stale. The full label ("Stale · 2m ago") rides in the hover
  tooltip; the stale dot is itself the click-to-refresh control.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, N /call dispatches (W0-4 replacement, zero snowplow changes)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror of the A6 identityContext injection (same array-of-string shape, same
optionality) EXCEPT no enum — extras key names are author-open (a Select's
queryParam is author-configurable). Without this schema field the apiserver
silently prunes the portal chart's 104 spec.keyExtras declarations and snowplow
F6's fold-nothing cache keying reads them as empty. See snowplow
docs/f6-portal-handoff-2026-07-13.md §1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ew hardening)

buildSetOpPath interpolates name/namespace into the apiserver URL; a '/'
would re-target a subresource (foo/status) and '?#' would smuggle query
tricks past the confirm. Reject non-DNS-1123 segments in the scoping
kernel — mirrors patchField's rejection of path characters.
…gated portal write, tagged human vs agent

Every resolved portal write (success OR failure) now fire-and-forgets ONE
immutable AuditRecord CR (audit.krateo.io/v1alpha1, namespaced — CRD ships
separately in the portal chart), carrying the SAME blast radius the human
confirmed at the W0-2/W0-4 gate (reused verbatim, never rebuilt), the origin
tag, the outcome, and requestedAt/resolvedAt.

- src/hooks/provenance.ts: buildAuditRecord (CR body, generateName 'ar-',
  ns = the write's target namespace) + emitAuditRecord (same fetch shape
  runRest uses; STRICTLY best-effort — 404/403/network all swallowed with one
  console.debug, never blocks or fails the primary write) + recordProvenance
  (flag-gated, void fire-and-forget entry point).
- Origin plumbing: optional WriteOrigin {actor, agentSessionId?, prompt?}
  threaded through handleAction/dispatchAction (ActionRuntime) and
  handleActionSet/runRestSet; absent = {actor:'human'}. The Autopilot bridge
  binds actor:'agent' + the provider's sessionId + lastUserText into every
  dispatch it drives (runAction / patchField / applyResourceSet) — patchField
  and applyResourceSet modules stay origin-agnostic (bound via deps).
- Emission points: runRest emits ONE record after the write resolves (ok,
  HTTP failure, or network throw → status 0); a declined confirm emits
  NOTHING. runRestSet emits ONE record per SET (count = ops.length, summary
  lists the ordered ops), never per op.
- Feature flag: config.json api.PROVENANCE_ENABLED (default OFF — clusters
  without the CRD see zero new traffic), surfaced as ctx.provenanceEnabled.
- Tests: 16 new (actor shapes, best-effort swallowing, success/failure/
  network outcomes, declined-confirm silence, one-per-set, flag OFF); all
  332 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Phase 2)

kagent Agent CRs with mcpServer.requireApproval pause execution on write
tool calls; the rail is the A2A client that must decide. Grounded in the
kagent source (v0.10.0-beta3 checkout; same wire shapes as 0.9.9):

- PAUSE: task status flips to input-required (final=true) with one
  adk_request_confirmation DataPart per paused call (metadata
  adk_|kagent_ type=function_call + is_long_running=true; data.args
  carries originalFunctionCall {name,args,id} + toolConfirmation
  payload.subagent_name) — go/adk/pkg/a2a/executor.go + hitl.go,
  kagent-core _hitl_utils.py.
- RESUME: a user message on the SAME taskId/contextId whose first part
  is the {decision_type: approve|reject[, rejection_reason]} DataPart
  (hitl.go ExtractDecisionFromMessage; kagent UI sendApprovalDecision).

approval.ts        pure protocol module: parseApprovalPause (both
                   metadata prefixes), buildDecisionMessage,
                   formatArgumentsPreview (manifest surfaced verbatim,
                   capped), deny-by-default governor (5-min single-shot
                   settle/expire state machine).
transport.ts       tracks the stream's taskId, emits require_approval
                   before done on input-required, adds respondToApproval
                   (shared streamJsonRpc runner); echo stub gains a fake
                   k8s_apply_manifest pause (apply/delete/scale/hitl
                   trigger) so the card is exercisable offline.
Provider           pendingApproval state + governor ref; approve/deny
                   resume into a NEW assistant bubble stamped with a
                   decision chip; DENY-BY-DEFAULT on dismiss, new
                   thread, and 5-minute timeout.
Rail               calm approval card (BlastRadiusConfirm language:
                   amber APPROVAL chip, tool + owning agent facts, mono
                   args/manifest block, Approve amber / Deny quiet,
                   deny-by-default note).

Provenance: assessed and deliberately SKIPPED — recordProvenance needs
a BlastRadius (verb+GVR+namespace) the approval doesn't carry; deriving
one would mean client-side manifest parsing (fabrication risk, against
the server-side-computation rule) and emitAuditRecord drops namespace-
less records anyway. Documented at the dispatch site.

Tests: 16 pure-logic cases (kagent-shape fixtures for parsing, the
documented response shapes, preview capping, governor transitions).
Gates: tsc --noEmit clean, eslint touched clean, vitest 348/348, build ok.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h /call query shape (snowplow has no raw /apis route)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… via the applySet fabric (W3-1)

A rest action may set fanOutPath: <field> naming an ARRAY field in the
submitted values. The submit expands into ONE ordered write per element:
for each write the array field is replaced by the single element BEFORE
payloadToOverride jq interpolation, so chart expressions like
${ .json.clusters } resolve per-op (metadata.name suffix, per-spoke
spec.deploy.targetRef.name). The set rides runRestSet — ONE aggregated
W0-2 blast-radius confirm, sequential stop-on-first-error dispatch,
per-item results, ONE W0-3 set AuditRecord. onEventNavigateTo is a
config error on fan-out actions (single-uid semantics).

Also: a Form schema array field with items.enum now renders a
fixed-options multi-select (the fleet target-clusters picker).

fanOutPath is added to the canonical actions fragment and propagated
verbatim to the 4 action-bearing widget schemas (Button/Card/Form/Listy);
types + CRDs regenerated. 365/365 tests (+5 fan-out), lint, build green.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: 26d903c4-733b-4152-9291-78178c980b68
feat(actions): fanOutPath — one Form submit fans out N ordered writes (W3-1)
…reviewPage / previewRestDef)

Three new deny-by-default READ-ONLY entries in the W0-1 verb registry, auto-applied
like navigate — the mandatory preview gate the Wave-4 builders' writes sit behind.
They mutate NOTHING and render into a new shared preview Drawer.

- previewBlueprint {chart:{url,version?,repo?}, values?}: POSTs {chart, values} to
  ${RENDER_API_BASE_URL}/render (new optional config key — the helm-render service
  transport seam, designed ahead of its deployment) and lists the rendered child
  objects (kind/name/namespace headline + collapsible YAML). No renderBaseUrl
  configured -> a graceful 'preview unavailable' chip, ZERO network. A render
  {error} response is shown AS the preview content — a bad chart is data.
- previewPage {widgets:[<widget CR objects>]}: ZERO network — an honest SOURCE
  preview drawer (one collapsible YAML per proposed CR, clearly captioned). NOT a
  live render, deliberately: WidgetRenderer requires a served widgetEndpoint,
  container widgets resolve children through further served endpoints, and drafts
  only carry an unresolved spec (snowplow compiles spec->status server-side) — a
  client-side 'render' would either fetch or fake it. Live draft rendering is a
  documented follow-up.
- previewRestDef {restDefinition:<CR draft>}: pure client-side parsing — the draft
  YAML + a structured summary of the mapped verbs/paths (action · METHOD path),
  kind/group, identifiers. No network, no crdgen (v1 = structured source preview).

New modules (repo max-lines rule): previewBus.ts (pure drawer-open seam, mirrors the
widgets/Drawer CustomEvent overlay pattern), previewBridge.ts (pure arg guards +
render transport + payload builders), previewSurface.tsx/.module.css (the drawer,
mounted once by AutopilotProvider). previewHandlers.ts rewritten from the W0-1
navigate placeholders to the real contracts; malformed args are DENIED (null),
never a crash. VerbDeps grows an optional renderBaseUrl passed by the bridge from
config.api.RENDER_API_BASE_URL.

Tests: previewBridge.test.ts (15) + previewHandlers.test.ts (10) + updated
verbRegistry.test.ts — registry membership + sideEffect:read, malformed-arg denial,
no-renderBaseUrl -> unavailable chip with no fetch, happy-path render with fetch
mocked, {error}-as-content, unreachable-service resolution, RestDefinition
verb/path extraction from a fixture, previewPage zero-network. Full suite 387
passed; lint, tsc --noEmit, vite build all green.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: 26d903c4-733b-4152-9291-78178c980b68
feat(autopilot): Wave-4 read-only preview verbs (previewBlueprint / previewPage / previewRestDef)
…pplySet fabric (W3-2 FE-1)

A rest action may set ops: [{resourceRefId, payload, payloadToOverride}] —
an ordered list of DISTINCT writes applied as ONE gated set. Each entry
resolves its OWN resource ref (verb + path + payload base) and builds its
OWN payload via the existing buildPayload, all interpolating ${ .json.* }
against the SAME submitted values — so one Form submit can create a Role
AND its RoleBinding (different plurals, different payload shapes; later a
ConfigMap + RestDefinition for the KOG builder). This composes N distinct
payload templates, complementing fanOutPath (which fans ONE template over
an array); the two are mutually exclusive (config error).

The set rides runRestSet: ONE aggregated W0-2 blast-radius confirm
(decline = nothing dispatched), sequential stop-on-first-error dispatch,
per-item results, ONE W0-3 set AuditRecord. Per-op /call paths get each
payload's own metadata name/namespace (updateNameNamespace); full success
honors onSuccessNavigateTo (interpolated from the submitted values) +
closeDrawer. Config errors (unknown/non-mutating op ref, ops×fanOutPath,
ops×onEventNavigateTo) toast and dispatch NOTHING. The action's top-level
resourceRefId/payload are IGNORED when ops is present (documented in the
schema).

ops is added to the canonical actions fragment and propagated verbatim to
the 4 action-bearing widget schemas (Button/Card/Form/Listy); types + CRDs
regenerated (42/42 clean; Button/Card/Form/Listy CRDs changed). Tests live
in runRestOps.test.ts (max-lines budget, the runRestSet.test.ts precedent):
388/388 (+6 ops), lint, build, validate-schemas green.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: 26d903c4-733b-4152-9291-78178c980b68
…nput (#33)

Root cause: WidgetRenderer's freshness overlay wrapped the rendered widget
in a <div> ONLY while stale/refreshing, so every live-refresh/event refetch
flipped the subtree's root element type (Suspense <-> div). React reconciles
that as unmount+remount, resetting the whole Form to the freshly-refetched
initialValues — on a busy cluster (refetch storms) forms were nearly
unfillable, and the remount also explains the observed antd-store /
rc-select display divergence (stale form-instance handles).

Fixes:
- WidgetRenderer: render the freshness wrapper <div> unconditionally and
  toggle only the badge inside it — the element tree is now structure-stable
  across stale/refetch flips, so widget-local state (form fields) survives.
- Form: apply the Autopilot agent draft once per draft, keyed by the
  provider's nonce — a refetch recomputes safeAgentDraft's identity and
  previously re-applied the stale draft over the user's newer edits.
- Form: while the form is fully pristine (form.isFieldsTouched() false),
  re-seed freshly-fetched initialValues via setFields({touched:false}) so an
  idle form keeps tracking server state; after ANY user touch, refetched
  initialValues are never applied again — user input wins, while fresh
  schema/enum options still flow in through the normal re-render.

Tests (new WidgetRenderer.formStability.test.tsx, jsdom): typed Input and
multi-select choices survive refetch cycles/storms (store-level probe via
submit), fresh enum options still arrive, pristine re-seed semantics, and
the draft nonce guard. Adds jsdom + @testing-library/react devDeps for
component-level tests.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: 26d903c4-733b-4152-9291-78178c980b68
fix(Form): survive widget refetches without wiping in-progress user input (#33)
feat(actions): ops[] multi-op rest action — N distinct writes per submit (W3-2 FE-1)
…nt substitution)

A pasted OpenAPI document is held CLIENT-SIDE (512 KiB UTF-8 cap — the JSON-escaped
ConfigMap payload stays under snowplow's verified 1 MiB /call body cap) and goes
user → cluster VERBATIM: agent publish proposals carry only the exact
{"$oasAttachment": true} token as the ConfigMap data value, and the frontend
substitutes the held bytes at publish-payload compile time — the document never
round-trips the model and never enters the collected page context.

Pure module (store factory + detection + substitution); unit tests cover the cap
(UTF-8 bytes, boundary, over-cap URL-path hint), conservative OpenAPI detection,
verbatim/exact-token/pure substitution, and the absent-attachment refusal.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: 26d903c4-733b-4152-9291-78178c980b68
…w-drawer wiring

Pure mapper/validation module mirroring the LIVE restdefinitions.ogen.krateo.io
v1alpha1 CRD: required fields (oasPath/resourceGroup/resource.kind/verbsDescription),
the action + method enums, findby-only identifiersMatchPolicy/pagination,
requestFieldMapping exactly-one-of inPath|inQuery|inBody + inCustomResource,
configurationFields shape, DNS-1123 metadata, and the two oasPath forms —
configmap://<ns>/<name>/<key> OR http(s)://… (everything else rejected).

restDefImmutabilityWarnings surfaces every CEL-immutable field the draft sets
(kind, resourceGroup, identifiers, additionalStatusFields, excludedSpecFields,
configurationFields — a wrong first publish means delete + recreate), and
buildKogPublishOps compiles the URL-first 1-op / paste 2-op (ConfigMap with the
$oasAttachment token, then RestDefinition) applyResourceSet publish plans; built
ops pass the set kernel (isApplySetAllowed) by construction.

Wired into previewRestDef: buildRestDefPreviewPayload now carries problems
(validation errors) + warnings (immutability), rendered as error/warning Alerts
in the preview drawer — the decide-before-publish surface.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: 26d903c4-733b-4152-9291-78178c980b68
braghettos and others added 27 commits July 21, 2026 13:46
…live drawer

Opening the drawer the instant the sandbox POSTs land races snowplow's informer
ingest: the root Flex's serve can resolve only PART of its children into
status.resourcesRefs, react-query caches that partial serve, and the page then
renders partially forever — the silent "only the first widget shows" preview
(forensics: applies 200, then GETs fired for one child only; the same flex
served all children when curled minutes later).

Add the A.2.35 warm-up gate: after a successful sandbox apply, poll the root's
REAL serve (Bearer fetch on the snowplow base) until every child id the root
declares is resolved, bounded at 20s (then open anyway — the renderer's own
error/Retry surface is the fallback). Gate is skipped when no snowplow base URL
is configured, so v1/source flows and existing tests are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(its INIT)

rootDraftTargetOf picked "the first non-RESTAction draft" as the page root —
so whenever the agent listed a child first (a heading Paragraph, a Statistic),
the drawer mounted that single widget as the whole preview: the silent
one-widget page behind every "where is the table / piechart" report.

Mirror the production model: the portal shell boots from its configured INIT
endpoint; a page preview boots from the page's OWN entry — the page-<slug>
root Flex (the identity the publish gate already requires). Draft order is
never used to infer the entry. No page-* root Flex in the set → there is no
page to mount: honest source fallback with an actionable caption ("author the
root Flex named page-<slug> and preview again") that the agent can act on.
The A.2.35 warm-up gate now also polls the real page root.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er/repo/base

"Publishes to krateo-portal-chart · PR into main" was hardcoded three ways (the
orchestrator fence, the op-builder default, the drawer header). Make the
destination the human's: before ANY git-write set is assembled, a proper form
(owner / repository / base branch) asks where the files should be committed —
the fence coords and builder defaults are only PREFILLS, and the last confirmed
choice prefills the next ask. Cancel denies the publish (nothing assembled).

Wiring mirrors the preview drawer's global-overlay pattern: one PublishTargetFormHost
mounted by the provider + a promise-based request seam the publish branches await.
Headless-safe: with no mounted host (unit tests) the ask resolves to its prefills,
keeping those flows byte-identical. The drawer's "Publishes to" header now says the
destination is confirmed at publish.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… in the same reply

The iterate instruction ("iterate with a NEW previewPage after every correction")
had no fence-strength language, so the model narrated changes without emitting:
"I've added the new Pie Chart" while the drawer stayed on the old 5-draft page —
AND the specialist had really modified the RESTAction underneath, so the stale
live preview errored minutes later (the errored-widgets ending). Same
narrate-instead-of-emit class the publish fence rule already stamps out.

Add the ITERATION FENCE: any change to the page or its data source ⇒ the very
next reply MUST carry a new previewPage fence with the FULL updated CR set;
describing the change without the fence is a BUG (nothing updates, and the open
preview breaks against the modified RA). Never claim updated without the fence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…no decay)

The 1.3.63 fence lived only in the turn-1 PORTAL BUILDER protocol, which decays
across the A2A thread — by the iteration turn the model was back to narrating
("I've added the new Pie Chart") without emitting previewPage, leaving the live
preview stale against the really-modified RESTAction. The user must NOT have to
ask for the fence in chat — the product owns this behavior.

Move a compact ITERATION FENCE line into PORTAL_BUILDER_ROUTING_DIRECTIVE,
which the provider deterministically injects on EVERY turn of the builder
route: any change to the previewed page or its data source in a reply requires
the full previewPage fence in that SAME reply; a described-but-unfenced change
is a BUG. Same per-turn placement pattern as the house-rules recap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An ajv-rejected previewPage now self-corrects without the user asking:
- previewBus holds the last preview's validation verdicts (previewProblems)
- collect() surfaces them in the page-context envelope every turn
- the every-turn routing directive gains PREVIEW SELF-CORRECTION (fix the
  exact errors + re-emit the full fence, re-delegating when appropriate)
- AutopilotProvider fires ONE hidden recovery turn on a 'preview blocked'
  chip (same 1/user-turn trampoline cap as tool-not-found/narrated-publish)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The what-am-I-publishing question, answered in all three places:
- the blueprint preview's files tab is now 'Chart files' (per-payload filesLabel)
- the drawer caption says it: Helm chart / tree the PR commits / dry-run Source
- the publishes-to strip + the destination form say what ships after the merge
  (a versioned OCI Helm chart)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e image

The deployed portal footer read 'build 0.0.0 · dev': the tag never reached the
docker build. The workflow now passes VERSION (tag) + APP_BUILD (sha); the
Dockerfile exports APP_BUILD for vite's git-less build, which prefers it over
the rev-parse fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The KOG publish had no narrated-publish backstop: gemini approves the previewed
RestDefinition in prose, emits no applyResourceSet fence, and the confirm never
opens (2/2 recorded stalls). Same recovery as the blueprint/page paths:
- previewGate keeps the LAST previewed draft (lastDraft) and builds the exact
  re-prompt (buildKogPublishNudge: 2-op $oasAttachment paste case / 1-op URL)
- the provider fires ONE hidden recovery turn when an approval lands with a
  previewed RestDefinition and no fence

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live 422: the model previews the FULL RestDefinition but re-composes an
ABBREVIATED CR in the publish op — dropping spec.resource.verbsDescription
(Required) — so op 2 fails AFTER the ConfigMap already applied. Same invariant
as the blueprint/page paths, now for KOG: published bytes == previewed bytes.
compilePublishOps receives ops rebased onto previewGate.lastDraft() (draft
fills omissions; the op's oasPath/metadata win; only matching kind+group ops
are touched). The human confirms the hydrated, REAL payload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live take: the model previewed a RestDefinition MISSING verbsDescription — the
drawer flagged it ('Validation errors — publishing this draft would be
rejected') but nothing drove a correction, and the 1.3.69 hydration faithfully
preserved the broken draft into a 422. Now:
- previewBridge surfaces restdef verdicts on previewProblems (the collector
  puts them in the model's page context)
- a problems-carrying previewRestDef yields the page-path's 'preview blocked'
  chip → the existing preview-validation trampoline fires unchanged
- the nudge + every-turn directive cover both preview verbs (re-emit the SAME
  verb, complete CR including spec.resource.verbsDescription)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…blish gate

Live take on 1.3.70: the self-correction recovery turn JUMPED TO PUBLISH over a
verbless draft — the confirm opened because recordPreview had armed the gate on
the invalid preview. Now:
- previewGate.recordPreview refuses drafts with validation problems (only a
  CLEAN preview unlocks publishing; lastDraft holds only clean drafts, so the
  1.3.69 hydration can never rebase onto a broken one)
- the self-correction nudge + directive explicitly forbid publish/applyResourceSet
  while previewProblems is present

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…emit

Four consecutive live takes previewed drafts failing client validation
('spec.oasPath is required' + 'verbsDescription requires at least one entry'):
the prompt's preview spec said oasPath '<see below>' and never insisted the
preview be complete, so the model treated both as publish-time concerns. The
spec now REQUIRES oasPath in the preview (URL verbatim, or the paste-case
configmap://krateo-system/<kind-lower>-oas/openapi.yaml convention) and at
least one {action,method,path} verbsDescription entry, with an inline example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… JSON

The SET confirm ('Confirm N writes') showed each op as a 140-char JSON sliver;
the scalar diffs showed pretty-printed JSON. The human confirms Kubernetes
OBJECTS — render them the way Kubernetes writes them: full-body YAML, syntax-
highlighted with the preview drawer's highlighter family, scroll-bounded per op
so a large payload (a pasted OpenAPI ConfigMap) stays confirmable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ard-coded white

UX-audit P1: 'CONSOLE', the username and the build line were hard-coded white
(contrast 1.00-1.09 on the light sider — invisible). Derive from --text-color
so both themes keep the intended muted treatment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…16)

/kog-builder read KOG-BUILDER above a page titled API Builder. The
section crumb now resolves its label from menuRoutes (the same chart
nav CR that feeds the sidebar, so they cannot drift); unlabelled routes
fall back to the slug. Deeper segments stay verbatim on purpose — they
are Kubernetes resource names.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…) — UX-audit #21

The Marketplace grid delivers ~400 catalog cards as one unpaginated wall.
Listy now mirrors the antd List `pagination` prop as a minimal serializable
subset: `widgetData.pagination` = { pageSize (required int), position?
(top|bottom|both) }. Presence enables client-side paging of the delivered
dataSource; absent keeps the antd default (no pagination), so every existing
list renders exactly as before.

Server-side facet/search filters (?extras -> RA jq) shrink the array BEFORE
it reaches the widget, so paging composes with them: antd clamps the current
page into the filtered range (verified by test), and hideOnSinglePage keeps
the pager off single-page results (chrome is exception-only).

NEW WIDGET CRD FIELD: Listy `spec.widgetData.pagination` (object; required
integer pageSize + optional position enum). The CRD artifact is generated,
not tracked (scripts/krateoctl-output is gitignored — the release-tag CI
syncs CRDs to krateo-frontend-chart); regenerated locally via
`npm run generate-crds` to confirm the field survives krateoctl unchanged
(no union/typed-map normalization involved).

Gates: eslint (touched files) clean; vitest 685/685 incl. 4 new pagination
tests; vite build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…disabled Autopilot (UX-19)

The composition-detail Diagnose button (and any Troubleshoot-with-Autopilot
entry point) deep-links with ?ask=<prompt>, which AutopilotProvider's seed
effect consumed only when Autopilot is enabled. On a portal without
AUTOPILOT_API_BASE_URL the click silently no-oped: the page navigated, the
param sat in the URL, and nothing explained why no assistant appeared.

The ?ask= handling now lives in one hook (useAskDeepLink, extracted from the
provider — which sat exactly at the max-lines cap):
- enabled: unchanged one-turn-per-visit seed (open rail, send, strip param)
- disabled: an antd warning notification — "Autopilot is not enabled on this
  portal — the Diagnose assistant is unavailable. Ask your administrator to
  enable it." — fired once per arrival (stable key dedupes double-fires),
  then the param is stripped (replace, not push) so refreshes don't re-announce
- navigation itself is untouched in both paths

Adds unit tests for the disabled path (notice + strip + no seed + unrelated
params preserved) and the enabled seed path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every Table column gets an antd sorter automatically — zero widget-schema/
CRD changes. The column type is sniffed from the RAW dataSource values at
its valueKey (bounded sample): all-numeric -> numeric compare + right-align;
kubectl-style ages (the jq 'def rel' format: 45s/3m/8h/5d/1w/2mo/1y/now) ->
parsed-seconds compare + right-align; ISO-8601 -> Date compare; everything
else -> localeCompare. Tag cells sort by their label text; bar by percent;
conditions by condition types. Missing values sort last in BOTH directions
(pre-negated for antd's descend). No default sortOrder — the server's jq
order stays until the user clicks a header (asc -> desc -> off).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…audit #22)

Typing in the ⌘K palette now shows the top 8 global-search hits inline
(debounced 300ms, cached per term via react-query) instead of a blind
Enter-only flow. Selection via ↑/↓ + Enter or click navigates straight
to the hit's link; plain Enter keeps the existing /search?q= behavior,
which is also the silent fallback on any typeahead fetch failure.

The endpoint is resolved data-driven from the /search route in
menuRoutes (the same resourceRef→endpoint precedence WidgetPage uses,
today the listies/search-results ref), with a one-hop follow through a
container's resourcesRefs for the flexes/page-search convention shape.
The call forwards extras {"q": term} — identical to the /search page's
contract (the widget's keyExtras: [q] partitions snowplow's L1 by term).

No widget CRD schema change: pure shell chrome + existing snowplow data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The KOG (RestDefinition/API) builder previously published via a direct
2-op cluster write (POST configmaps + POST restdefinitions), so the
generated kind landed live the instant the user confirmed. This makes it
consistent with the blueprint and page builders: the model emits ONE
scalar `publishRestDef` verb and the HOST fans it into a git-write PR set
(gitrefs + repocontents + pullrequests) targeting the KOG repo.

- new kogPublish.ts: buildKogPublishAsPrOps() commits
  apis/<kind>/restdefinition.yaml (+ configmaps/<kind>-oas.yaml in the
  paste case, carrying the OpenAPI document inline). resolveKogPublishDraft
  discriminates URL vs paste from the draft oasPath and refuses a
  paste-case publish with no held document. The OAS "held-in-portal"
  guarantee is preserved: the held bytes are embedded at publish-compile
  time; the model never reproduces them. The committed RestDefinition's
  oasPath is rewritten to the git-shipped ConfigMap for consistency.
- publishTargetForm: PublishTargetRequest.kind now includes 'restdef',
  with a krateo-oas default repo and a PR-based blurb.
- AutopilotProvider: a `publishRestDef` finalize branch wired like
  publishBlueprint/publishPage (destination form -> KOG preview gate ->
  authorship stamp -> blast-radius confirm). compilePublishOps/
  compileKogPublishOps/heldDraftIdentity/recordPagePreview extracted to
  publishCompile.ts (keeps the provider under max-lines).
- prompts (actionBridge) + the narrated-publish nudge (previewGate) now
  emit `publishRestDef` instead of the old direct-write applyResourceSet.
- kogMapping.buildKogPublishOps retained as a documented defense-in-depth
  fallback (the applyResourceSet restdefinitions path still gates/works).

Tests: kogPublish.test.ts (17) covers URL + paste ops, verbatim OAS
embedding, oasPath rewrite, resolver discrimination, kernel + defaults/
overrides; publishTargetForm.test.ts covers the kind extension. Whole
vitest suite green (747), eslint clean on touched files, build passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…#21)

A rowNavigateTo of exactly {key} means the server precomputed a complete
per-row route (composition rows -> /compositions/.., others -> /resources/..);
use it verbatim without slash-encoding. Multi-segment templates still encode
each interpolated value. +3 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(Card): remove FooterItem loading box that popped on load (#55 #2) (#61)

Each Card footer item wrapped a WidgetRenderer and, while its child loaded,
applied an .itemLoading class capping it to 100x100 (flex:1) — then snapped to
the natural size once loaded, a visible pop/jump. WidgetRenderer already renders
its own sized skeleton (isPending → WidgetLoading), so the extra capped box was
redundant. Drop the isLoading state + onLoadingChange wiring + the .itemLoading
rule; the item now keeps one stable box across load → loaded.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5aeddbe)

* fix(Form): keep the field label above its control in all layouts (#54 §0.4, #57 #5) (#62)

Each schema field's label is a stacked name + wrapping description, designed to
sit ABOVE the control. Under a Form layout='horizontal', antd renders the label
beside the control, so a long description crowds/overlaps the input. Pin each
field's labelCol/wrapperCol to span 24 so the header always stacks above the
control — a no-op for the common vertical/inline forms (already stacked), the fix
for horizontal ones. Public antd API, no CSS-internals dependency.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 10b152d)

* fix(widgets): visual/schema defects across Row, Table, Tabs, Profile (#59)

Consolidated zero-behavior-risk fixes from the #54/#55/#57 evaluations:

- Row (schema): the alignment default is intentionally 'stretch' (equal-height
  sibling cards when the rail narrows content, per Row.tsx:33), but the schema +
  generated type documented 'middle' — a contract lie cited by #54 §0.6a, #55 #3/#4,
  and #57 pitfall #1. Correct the description to match the code (keep the code default).
- Table (Table.tsx + schema): add the missing 'middle' fallback (size ?? 'middle'),
  mirroring Button — an omitted size no longer falls through to antd's raw, undocumented
  default (#54 §0.5, #57 #4). Document it in the schema description.
- Tabs (module.css): replace the off-scale magic 12.5px tab-label size with the
  --font-size-xxs token (#54 §0.7).
- Profile (page css + tsx): the .avatar/.fullname/.email rules were dead (nested under
  .profile>.container>.content, which the page never renders) and the avatar Col had no
  md span so columns didn't sum to 24 at md+ (#55 #6a/#6b). Flatten the CSS to the real
  DOM (tokenized; the old 36px/24px were off-scale) and give the Cols md={8}/md={16}.

Generated *.type.d.ts JSDoc mirrored by hand to match the schema edits.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit af2fb5b)

* feat(Row): responsive per-item spans (xs/sm/md/lg/xl/xxl) (#55 #3/#4) (#64)

Row items only had a fixed numeric span, so a row can't adapt when its container
narrows — the shared root cause of #55 #3 (dashboard rows look wrong) and #4
(blueprint detail renders wrong with the Autopilot rail open, which shrinks the
content column). Add optional antd breakpoint overrides to each Row item, mirroring
the Col widget's existing xs/sm/md/lg/xl/xxl. Purely additive: items without them
keep the base __TEXT	__DATA	__OBJC	others	dec	hex span, so every existing CR is unchanged. Generated type mirrored.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 2328ecf)

* feat(autopilot): add a Stop control to abort an in-flight turn (#55 #8) (#60)

The rail could start a streaming turn but never cancel it — abort existed
(transport returns controller.abort(), stored in abortRef) but was only wired
to unmount + newThread, and the composer merely disabled Send while streaming.

Adds a provider stop(): aborts the stream, resets streaming, and settles the
in-flight assistant bubble (drops the caret, keeps the partial answer, leaves the
session/contextId intact so a follow-up continues the same thread). The composer
now swaps Send → a Stop button while streaming (new stroke-style StopIcon).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit d9275c6)

* fix(Drawer): don't let the Filters drawer cover the Autopilot rail (#55 #1) (#63)

The singleton Filters/detail Drawer portals to document.body and, when open, its
fixed inset:0 root covers the whole viewport — including the docked 384px Autopilot
rail (an in-flow flex column). The provider now publishes the rail width as a :root
CSS var (--autopilot-rail-width, 0 when closed/disabled), and the Drawer insets its
right edge by it, so the mask + panel cover the content column and leave the rail
visible + interactive — matching the 'rail is never overlaid' contract.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 333fe43)

* fix(Shell): move Autopilot toggle beside the user menu (#56 1.1/1.4) (#66)

Autopilot is a distinct surface, not a page utility, but its header toggle sat
second in the search/notifications/theme cluster. Move it to sit immediately left
of the user menu, separated by a thin divider, so it reads as its own affordance.
Also document that SparkIcon's solid fill is a deliberate brand-glyph exception
(#56 1.4) rather than restyling the Autopilot identity mark.

The colour / Signal-Yellow half (#56 1.2/1.3) is intentionally NOT here — it targets
Brand v2 tokens that don't exist on main yet (blocked on #49).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit e8e1a19)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…isplay:contents) (#68)

The live-refresh freshness wrapper rendered unconditionally with `width: 100%`, so
two widgets in a horizontal Flex each shrank to an equal 1/N column (flex-shrink:1),
defeating the parent CR's `justify`. E.g. the dashboard stat "of total" caption and
the Compositions "Create composition" button both sat mid-row instead of hugging
left / pinning right — the CRs are correct (`justify: flex-start` / `space-between`),
the wrapper collapsed them.

Keep the wrapper <div> UNCONDITIONAL (stable subtree root — preserves the form
remount-invariant, #33) but make it `display: contents` (layout-transparent) until a
badge actually shows; only then does it become a real positioning box
(`.freshnessWrapActive`) to anchor the absolute overlay. In the common (no-badge)
case the wrapper no longer participates in layout, so horizontal widget rows honor
their CR `justify` again; vertical/full-width stacks are unaffected.

Verified: WidgetRenderer tests (incl. the remount-invariant / formStability) pass;
build green; WidgetRenderer.tsx lints clean.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@braghettos
braghettos merged commit f05c7b7 into main Jul 23, 2026
2 checks passed
braghettos added a commit that referenced this pull request Aug 3, 2026
…2) (#30)

Render api.SNOWPLOW_IDENTITY_INJECTION: true into the frontend config.json. Capability fact
authored by the chart from the bundled snowplow version (>= 1.7.0), NOT an operator knob:
when true the frontend (A5) stops volunteering login identity in ?extras= because snowplow
injects it server-side, restoring per-widget L1 cache sharing. Absent/false = byte-identical
legacy behavior, so old frontend/snowplow are unaffected.

Done the generator's way: the flag lives in values.yaml config: (boolean, capability-fact
rationale as a comment); values.schema.json is a DERIVED artifact regenerated by
scripts/gen-config-schema.py (the installer applies schema defaults, so the schema is the
source of truth). Taught the generator to infer the JSON-Schema type from the value
(bool -> boolean, checked before int since bool subclasses int), so the flag renders
type:boolean/default:true and the ConfigMap template emits a real JSON boolean. The regen
also picks up the previously-missing OTEL_COLLECTOR_URL, clearing the schema-vs-values drift
that was reddening frontend-chart PRs (drift gate `gen + git diff --exit-code` now clean).

Ships together with snowplow 1.7.0, the widget identityContext CRD declarations, and the
greeting client-render. Ref: snowplow docs/A6-chart-enablement-spec-2026-07-07.md §1.2 / §4.1.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
braghettos added a commit that referenced this pull request Aug 3, 2026
…6 migration) (#32)

The flag merged (PR #30) as a boolean, which is a HARD blocker: the krateo installer's config
plumbing (chart-inspector) emits only strings, so a boolean field both rejects every installer
value AND denies the krateofrontends v1-1-8 -> v1-2-0 composition migration ("expected boolean,
got string") — wedging frontend 1.2.0 in an infinite retry (found live on krateo-installer-test).

Option A (decided): keep the field string-typed. Zero runtime change — the frontend already reads
injectIdentity = !config.api.SNOWPLOW_IDENTITY_INJECTION (JS truthiness), which coerces strings:
""/absent -> inject ON (legacy rollout hold-off, safe default), "true" -> inject OFF. String ""
also fixes the boolean's wrong default:true (inject-OFF as the shipped default).

Generator-driven, not a hand-patch: values.yaml ships "" (hold-off), and gen-config-schema.py pins
this key to type:string via TYPE_OVERRIDES so the schema can't drift back to boolean. Ship as
frontend-chart 1.2.1; the installer re-pins frontend -> 1.2.1 and the string->string migration
succeeds (Synced=True). The flag flip to "true" stays the LAST rollout step (A6 §6).

Ref: snowplow docs/frontend-1.2.0-config-type-conflict-spec-2026-07-07.md §4.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants