Skip to content

DO NOT MERGE — merge train: 2743,2752,2753,2619,2746 - #2765

Closed
vybe wants to merge 27 commits into
devfrom
train/20260914-0901
Closed

vybe wants to merge 27 commits into
devfrom
train/20260914-0901

Conversation

@vybe

@vybe vybe commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Integration surface for #2743, #2752, #2753, #2619, #2746. Never merged; members merge individually once green.

🤖 Generated with Claude Code

https://claude.ai/code/session_012tCskBCnoydX8YAWFmm8TN

dolho and others added 27 commits September 8, 2026 15:21
An agent that uses its canvas the way ent#438 intends accumulates dozens: one
per report, per topic, per run. The Workspace could only ever ADD to that pile
— the client-portal surface had no delete at all, the only ordering was "newest
updated", and nothing bounded the table.

Two decisions, both by operator ruling 2026-09-08, recorded because each had a
plausible alternative:

**Deleting is owner-or-admin.** The answer ent#548 gives for files — the owner
deletes the shared artifact. This NARROWS the platform DELETE route, which
accepted any user with agent access; safe because no UI called it, so no
workflow depended on the wider gate. A canvas is one shared surface with no
per-user copy, so a non-owner has no "hide it from my list" middle ground: per
AC #2 they see no control at all rather than one that 403s. Agents keep
clearing their own (`clear_canvas`, the #918 self-gate).

**The bound is a per-agent CAP, not a retention window.** ent#438 recorded "no
retention window" because the composite key bounds rows per canvas — but
`canvas_id` is agent-chosen, so the COUNT was unbounded; the axis was missed,
not decided. `CANVAS_MAX_PER_AGENT` (100, env-tunable) is checked inside
`upsert_canvas`'s insert branch, in the same transaction as the INSERT, so it
is not a check-then-act race. Updating an existing canvas is NEVER refused — a
cap that froze updates would punish exactly the agent that reuses ids — and the
refusal is a named 409 telling the agent to retire one, never an eviction:
deleting a person's surfaces on a timer is the #1638 failure direction.

Both surfaces resolve permission through `db.can_user_share_agent`, the same
predicate `assert_agent_owner` uses, so Agent Detail and the Workspace cannot
disagree about who owns an agent. The Workspace learns it from
`PortalAgentCard.can_manage_canvases` — the portal's only capability channel
(#2128), since a portal principal cannot read `/api/settings/feature-flags` —
and it fails closed.

`pinned` (dual-track: `agent_canvases_pinned` + Alembic 0058, NOT NULL DEFAULT
0, no backfill) is written only by the human pin route and is deliberately
absent from every agent-facing tool: `audience` is the agent's decision about
who may read, `pinned` is the reader's about what they see first, and an agent
that could pin itself to the top would defeat the ordering. A pin survives the
agent rewriting the canvas.

Living with many is `CanvasPanel.vue`, shared by both surfaces (one rendering
layer, per ent#475): search once the list passes six, a height-bounded strip so
a long list does not cost the rail its other tabs, and a Manage mode giving
each row its age, stale mark, pin and delete. Decidable rules are pure in
`canvasUtils.js` — vitest runs `environment: 'node'` with no mount harness, so
a rule inside the SFC is one no test can reach.

Bulk delete is a POST, not a body-carrying DELETE (bodies on DELETE are
permitted-but-unreliable and this one is not optional), declared above the
parameterized routes on both routers (Invariant #4), and it reports the ids
that EXISTED rather than the ids requested so "3 of 5 removed" is sayable.

Three pre-existing guards failed and each was right to: `empty_canvas` was
missing the new field (a real bug in this change, fixed), the self-gate guard
needed to learn the new gate's name, and the positional-read guard needed its
synthetic row extended — that one exists precisely because `_row_to_summary`
reads by index.

Tests: 20 new backend cases (cap refusal + update-at-cap, pin ordering and
survival, bulk scoping, the permission matrix on both surfaces, route ordering,
dual-track migration parity, and that the MCP tools cannot pin) and ~24 vitest
cases for the pure rules. Verified against a real database: the cap refuses the
4th of 3, updates still succeed at the cap, a pin outranks recency and survives
a rewrite, and bulk delete returns only the ids that existed.

Related to Abilityai/trinity-enterprise#553

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdxGmvuKuqaWJZ6pKUgaUJ
…ment the lifecycle

Closes the three acceptance criteria the first commit left open:

* AC #1 asked for the deletion to be audited and only the BULK route was —
  the single-canvas route is the one a person actually clicks. Logged only
  when something was removed, since the route is idempotent and a repeat
  click would otherwise fill the trail with events where nothing happened.
* AC #8: deleting the DEFAULT canvas is allowed, comes back empty on the
  next write, and frees a slot against the cap. `main` is the id both the
  MCP tools and the voice panel fall back to, so it is the one most likely
  to be deleted by accident and the one whose deletion must strand nobody.
* AC #9: the user doc gains a "Removing canvases" section stating the
  permission rule, the cap, and that nothing is ever deleted to make room.

Also records a latent pre-existing mismatch found while testing:
`empty_canvas` returns None timestamps while `models.Canvas` requires
strings, so `Canvas(**empty_canvas(...))` raises. Not live — its only
caller declares no `response_model` — but adding one there would turn the
voice teardown poll into a 500. Left as a comment where the next person to
reach for that will meet it, rather than fixed out of scope.

Related to Abilityai/trinity-enterprise#553

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdxGmvuKuqaWJZ6pKUgaUJ
…he test imports it

Two ent#553 tests passed in isolation and failed in a full-suite run:
`test_the_cap_refuses_a_new_canvas_by_name` and
`test_deleting_the_default_canvas_frees_a_slot_against_the_cap`.

Order-dependence, not a defect in the feature. They patched
`db.canvas.CANVAS_MAX_PER_AGENT` via a fresh `import db.canvas`. Some earlier
test in the suite evicts that module from `sys.modules`, so the fresh import
hands back a NEW module object while the live `db._canvas_ops` is still an
instance of the OLD class — whose `upsert_canvas` reads the OLD module's
globals. The patch lands somewhere nothing consults, the cap stays at its
default of 100, and the "refuses the 4th of 3" assertions fail.

`_set_cap` patches the bound method's own `__globals__`, which is whichever
module dict the running code actually closes over — correct whether or not an
eviction happened, so it does not depend on knowing which test pollutes.

Same failure and same fix as #2589, where the identical shape bit
`mark_stale_activities_failed`. Worth noting the class: a monkeypatch on a
module attribute is only as good as the assumption that the live object came
from that module object, and in a suite that evicts modules that assumption is
not free.

The feature is unchanged — this touches only the test file.

Related to Abilityai/trinity-enterprise#553

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdxGmvuKuqaWJZ6pKUgaUJ
…eachable

Two review items from #2619.

**Alembic head fork (#2068 class).** `0058_agent_canvases_pinned` shared
`down_revision = 0057` with #2608's `0058_portal_file_dismissals`, which has
since landed on `dev` — two heads, and `alembic upgrade head` resolves its
single target before applying anything, so EVERY revision merged since the
fork stops arriving, not just one. Re-parented onto
`0058_portal_file_dismissals` and renumbered to `0059` so the prefix keeps
being a usable ordering cue; the id is not applied anywhere yet, so the
rename costs nothing. `check_alembic_heads.py` reports 1 head.

**`CANVAS_MAX_PER_AGENT` was inert (#1039 class).** The refusal message names
the number, but the variable was read only from `os.getenv` in `models.py` and
appeared in no compose file — so an operator following the refusal's own advice
would raise a lever that never reaches the container. Wired into
`docker-compose.yml`, `.prod.yml` and `.hosted.yml` (the last two launch
standalone, no base merge / no `env_file`) plus `.env.example`.

Related to #2619

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd71qsYbFodvofba8P69eP
…ecycle

# Conflicts:
#	src/backend/client_portal/service.py
#	src/backend/models.py
#	src/backend/routers/canvas.py
#	src/backend/services/canvas_service.py
…t#553

The raw-colour ratchet became enforceable on dev while this branch was
open (#2605/#2609), and the merge brings it here: this PR's delete/pin/
search chrome takes `components/canvas/CanvasPanel.vue` from 25 to 46
`raw_gray`, so `tests/unit/rawColorRatchet.spec.js` fails the frontend
build.

That growth is the honest kind. The design-system contract SPELLS the
neutral ink ladder as `gray-N` — surfaces gray-50/100/800/900, borders
gray-200/300/700/800, ink gray-300/400/500/600 — and there is no
semantic token for a neutral, which is exactly why the spec's own
comment says gray is ratcheted but never held to zero for new files.
The rule it does hold new code to is `raw_nongray`, and this file stays
at **0**.

Re-frozen in its OWN commit with the increase named in the baseline's
`refrozen` block, which is what the ratchet's error message asks for —
not absorbed silently into the feature diff. The entry is hand-edited
rather than regenerated so #2605's provenance block survives; no other
file's ceiling moves (verified: nothing grew, nothing is stale, no
un-baselined file carries `raw_nongray`).

Related to #553

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd71qsYbFodvofba8P69eP
… on ownership (ent#553)

Three review findings, all in the same direction — the backend was right and
the user-facing half did not arrive — plus the two smaller ones.

1. **The stated bound now reaches the user.** `canvasLimit` was a `ref(0)`
   nothing ever assigned, so `canvasHeadroom(n, 0)` returned `{label: null}` and
   the early warning could not render at any count. The ceiling rides
   `GET /api/settings/feature-flags` as `canvas_max_per_agent` — the established
   home for a value the browser needs to render a surface, and where
   `platform_default_model` / `install_source` already set the precedent for a
   non-boolean. Not a new route (Invariant #13 would owe three surfaces for one
   integer) and not an envelope around the canvas list (the MCP tool and the
   Workspace both read it as a bare array). It is a CONSTANT, not per-agent
   state, and the client already holds the count. `0` still means "not told" and
   still renders nothing, so an older backend is unchanged.

2. **The Workspace canvas writes are audited.** The three portal routes recorded
   nothing while their operator twins have logged since they shipped, and
   `docs/user-docs/agents/agent-canvas.md` tells users deletion is audited — so
   the claim was false for exactly the client-facing surface. `_audit_canvas_change`
   is the shared helper; the actor is `actor_email` (the documented #848
   inline-auth path) rather than a fabricated `User`, which is honest because
   `_require_canvas_manager` is platform-only and owner-or-admin, so a real
   Trinity user is always behind it. Ids and counts only (G-04). The three
   routes become `async def` to await it, matching their operator twins, which
   already call the same sync db functions from an async handler.

   Pinning is audited too, on BOTH surfaces — the operator route was the one
   recording nothing. A pin decides which canvas an entire roster sees first, so
   it is an administrative act on a shared surface, not a per-viewer preference.

3. **`canManage` comes from the parent.** It was hardcoded `true` on the
   argument that the server decides. It does — but a merely-shared user was then
   shown Manage → Delete / Pin and got a 403, which is the failing-control
   problem `can_manage_canvases` exists to prevent on the Workspace. Agent
   Detail passes `agent.can_share`, the same predicate `ReportsPanel` five lines
   above already reads and the same one `_gate_human_removal` enforces. The prop
   defaults FALSE, so a caller that forgets it hides an affordance rather than
   offering one that refuses.

4. **`get_agent_card` resolves `can_manage_canvases`.** It omitted it, so the
   same owner read `true` in the sidebar and `false` on the agent's own page —
   the disagreement #2160's own docstring says that function exists to prevent.

5. **An agent genuinely cannot pin its own canvas now.** The user doc said so;
   `_gate_human_removal` allowed it (right for delete — an agent tidying up
   after itself — and wrong for pin), and "no MCP tool exposes it" is a property
   of the client, not of the route. `_gate_pin` is humans-only, which makes the
   documented sentence true rather than aspirational.

Tests: the audit guard now walks the portal routes as well as `routers.canvas`
(it only ever inspected the latter, which is why three unaudited routes passed
it), plus pin-audit parity, the humans-only pin gate beside the still-permitted
agent self-delete, the feature-flags constant being the same object the refusal
is raised from, the agent-card/roster agreement, and four frontend wiring cases.

1025 backend / 2538 frontend tests green.

Related to Abilityai/trinity-enterprise#553

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSjVEjay9ztC1oDXXkh9uN
… (ent#553)

Found re-reviewing my own audit fix. Adding the rows was right; the attribution
was wrong, and a row that lands under the wrong actor is worse than the missing
row it replaced — nothing fails, so the wrong answer is believed.

`_audit_canvas_change` passed `actor_email` only. But
`platform_audit_service._resolve_actor` derives `actor_type` from
`actor_user` / `actor_agent_name` / `mcp_scope` / `mcp_key_id` and never from
the email, so an email-only call falls through to its last branch:

    _resolve_actor(None, None, None, None) -> ("system", "trinity-system", None)

So every Workspace canvas delete and pin was recorded as `actor_type="system"`,
`actor_id="trinity-system"` — a named operator's action attributed to the
platform, invisible to any `actor_type=user` query and to the audit UI's
per-actor filter. Verified against the real resolver, not by reading the call.

The `actor_email`-only path I cited (#848 inline auth) is right where the caller
genuinely has no `users` row. That is not this route: `_require_canvas_manager`
is platform-only and resolves through `db.can_user_share_agent`, so a row exists
by construction. It now resolves that row and passes `actor_user`, producing the
same `("user", <id>, <email>)` shape the operator twin has always written —
which is the point, since auditing the two surfaces differently buys little more
than auditing one of them.

Best-effort by construction: the action has already happened, so a lookup that
raises or misses must not drop the row. It falls back to the email-only call
with a WARNING, since a miss would mean the gate admitted someone the user table
does not know.

Tests: the regression is pinned against the REAL `_resolve_actor` (both the
shape the fix must not return to and the shape it produces now), plus a source
guard that the helper resolves a row, passes `actor_user`, keeps the email as a
fallback and cannot raise. Removing `actor_user=` reds it.

31 passed on the ent#553 file; 953 across canvas / portal / audit.

Related to Abilityai/trinity-enterprise#553

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSjVEjay9ztC1oDXXkh9uN
Resolve src/frontend/raw-color-baseline.json: keep dev's #2662 notes, totals
patched to the merged tree's real values (per-file entries unchanged on both
sides; scanner + ratchet test verified).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015owqMKD5QDjzZrUTF2Joht
The two cap tests imported the class from `db.canvas` while `_set_cap` already
patches the cap through `upsert_canvas.__globals__` — because an earlier test
can evict and re-import the module. The same eviction gives the test a
different class object than the one the live code raises, and `pytest.raises`
then reports the correct refusal as an unexpected exception. Seen once in a
full local run after the dev merge (both tests pass in isolation and under
CI's three seeds); resolve the class from the same globals the cap comes from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015owqMKD5QDjzZrUTF2Joht
…atch (ent#553 review)

`CanvasPanel.vue` gated the chip strip on `visible.length > 1 || manage`,
where `visible` is the FILTERED list. Searching down to exactly one canvas
hid the strip while the previously selected canvas stayed on screen, and the
auto-select watcher — keyed off the unfiltered `props.canvases` — never
selected the match. Proven by execution: 7 canvases, query "Topic 3" → strip
false, no-match message false. The one canvas the user just searched for was
unreachable.

Fix:
- `canvasSelectorVisible({visible, manage, query})` — with a query, any hit
  shows the strip; without one, a single canvas is no choice (unchanged).
- `canvasAutoSelect(visible, selectedId, query)` — while a query is active
  the selection follows the matches; no-op with no query or when the current
  selection already matches.
- `CanvasPanel.vue` consumes both: `v-if="selectorVisible"` and a watcher on
  `[visible, query]`.

Tests:
- `canvasUtils.spec.js`: the two pure rules.
- `canvasPanelSelectorGate.spec.js`: slices the `selectorVisible` computed out
  of the SFC and RUNS it against the ejection's numbers; pins that the
  template reads the computed, not a re-derived length test, and that the
  watcher calls `canvasAutoSelect`.
- `test_ent553_canvas_lifecycle.py`: the second review ask — the per-agent
  cap reaches the wire as a 409 through the real router → service → db
  chain (only the Redis rate limiter stubbed), names the remedy, and the same
  PUT against an existing id stays an update.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015owqMKD5QDjzZrUTF2Joht
message_router.py defined _sanitize_filename twice at module scope. The
first definition is a one-line delegator to services.upload_service.
sanitize_filename; the second is the full hardened implementation added
for Issue #487. Python binds the later definition, so the delegator was
dead code (pyflakes F811: redefinition of unused _sanitize_filename).

Removing it also drops the now-unused sanitize_filename import. No
behavior change: the Issue #487 implementation was already the one that
ran.
…t#553 review)

`query` has exactly one writer — the search input's `v-model` — and that
input was `v-if="showSearch"` with `showSearch = ordered.length > 6`.
Seven canvases, type "Topic 3", delete the one match: six canvases, the
box unmounts, `visible` still filters on the stale query, the strip
collapses, and the panel says *No canvas matches "Topic 3"* with no
control left to clear it. Every remaining canvas is unreachable via the
chips until navigation. Also reachable with no operator action: the
agent's own `clear_canvas` plus a rail refresh while a query is typed.

The rule is pure — `canvasSearchVisible(count, threshold, query)` — and
keeps the box while a query is active regardless of the count: the typed
intent survives the shrink, and the no-match line keeps the one control
that clears it. Resetting `query` when the box would flip off was the
other option and was rejected: it erases a search the user was mid-way
through because a sibling canvas went away.

The gate spec that pinned the previous ejection drove `visible`/`query`
in isolation from `showSearch`, which is why it could not see this one.
It now slices the real `showSearch` computed out of the SFC and RUNS it
against the ejection's own numbers (7 → 6 with "Topic 3" typed → box
stays; 6 with no query → box gone), and pins that the input is gated on
that computed and is the sole writer of `query`. Mutation-checked:
reverting the gate to the old length test reds three cases.

Four mechanical items from the same review ride along:

- requirements/core-agent.md: the ent#438 "deliberately no retention
  window: bounded by construction" line now says why that reasoning was
  wrong (rows are bounded per canvas, the count was not) and what bounds
  it instead; FR-18..FR-22 record delete / bulk / cap / pin / search,
  which had no requirements entries at all.
- raw-color-baseline.json: the `_ent553_note` naming CanvasPanel.vue's
  25 → 46 raw_gray was added in 2794388 and dropped by the dev merge
  aa248f7; re-added so the growth is named in the file.
- routers/canvas.py `# mcp:` header now says pin and bulk-delete are
  unexposed on purpose, and why — Invariant #13's deliberate-vs-forgotten
  signal.
- feature-flows/agent-canvas.md: the two search-state rules and the
  defect class they close.

Verified: vitest 2696 passed (121 files); canvas backend suites 101
passed; raw-colour ratchet and loading-gate ratchet unchanged.

Related to Abilityai/trinity-enterprise#553

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RS8FBA7oEtAAv6GamZKong
…Q 334→431

The user docs were last regenerated for v0.9.0 (2026-08-17). 331 commits
landed since; this pass reads every feature flow, router and view touched
in that window and brings the 138-page tree back to the code.

Rewritten around the current product: the Workspace (chat tabs + pinned
Main, the composer with typeahead/model tiers/files/voice, the rail, voice
mode, canvas vocabulary), subscriptions (5h/7d headroom, pressure, weekly-
limit warning, mid-turn failover), hosted install (pull-only compose,
TRINITY_IMAGE_TAG, DigitalOcean 1-Click, compose-file pairing, unless-
stopped, /mcp via nginx, a full .env reference per compose), asks and
deliverables, durable loops, schedule delivery to a Workspace user, rooms
in OSS core, Library assign/unassign, fan_out receipts and polling, MCP key
scopes, platform settings encrypted at rest, telemetry second cut,
guardrail hooks under managed settings, Codex API-key auth, Telegram HTML
rendering, the .gitignore push sweep, the dashboard widget allowlist, and
the abilities agent-dev 1.16.0 skills.

Surfaces that no longer exist are removed rather than left described:
the Dashboard "Tag Clouds" / "Activity Feed" sections, the browser
Terminal tab (hidden since March — agent-terminal.md now documents SSH),
the Agents page, the network graph, the Chat-tab voice overlay, the
per-agent /workspace page, the OAuth "click the provider button" flow,
and an "Operations → Restart All" control that never had a button.

FAQ: every page refreshed against the updated feature docs, 97 questions
added, the index regenerated from headings. README index, UI tour,
overview, and the video library (v0.9.0 review was missing) updated.

Verified: 0 broken relative links, no issue numbers or codenames outside
the historical dev-announcements archive, public-safety greps clean, and
the CI enterprise-docs guard pattern returns nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CvKzfEstE5mGZURBUcdGb
…ack under its own cap

/sync-feature-flows over the last five dev commits (#2638, #2703, #2571,
two left the feature's *home* doc stale while the change was documented
elsewhere.

fan-out.md (FANOUT-001 home, untouched since April) — #2670 only landed
in mcp-orchestration.md, so the doc that owns routers/fan_out.py still
said "POST only". Now documents GET /api/agents/{name}/fan-out/{fan_out_id},
build_fan_out_batch_status status derivation, the on_started hook,
FanOutBatchTask/FanOutBatchStatus, get_fan_out_executions and its
dual-scope reason, the bounded client.ts::fanOut() + get_fan_out_result
MCP tool (receipt rationale summarized, pointer to mcp-orchestration.md).
Drift repaired while there: db/schedules.py:NNN paths -> the #1481
db/schedules/ package, migration ordinal #30 -> #33 (verified against
MIGRATIONS), tool registration via addAllTools, X-MCP-Key-* headers
documented as inert per #2389.

subscription-auto-switch.md — the #2638 prose was complete but its three
catalog tables were not reconciled: Files (subscription_headroom_service,
execution_envelope, client_portal/service, the #2638 test; the toggle
lives in SubscriptionsPanel.vue + stores/subscriptions.js, not
views/Settings.vue), System Setting (subscription_api_key_fallback),
API Endpoints (GET/PUT /settings/api-key-fallback).

feature-flows.md — 547 -> 444 lines. Recent Updates trimmed 137 -> 20
rows, matching its own "newest ~20" header (#1360); one-line rows added
for #2638, #2703, #2670, which had none. 17 flow docs had no Documented
Flows category row — subscription-auto-switch.md among them, reachable
only via a June Recent Updates row the trim would have removed — so
every one of the 190 flow docs now has a category row. Skill Injection
row refreshed for delivery-on-assign (#2703). Three links that were
already broken in HEAD (AUDIT-001-execution-origin-tracking, skills-crud,
mcp-skill-tools) are left as-is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01168enK9QL4DNuVSN6taw2E
…ecycle

Conflict: src/frontend/raw-color-baseline.json (the `totals` block only).

The two sides moved different counters for unrelated reasons, so the
resolution takes both rather than choosing a side:

- dev (#2718) fixed a scanner false positive — a `#` followed by hex
  digits in rendered copy (issue references like `(#526)`) was read as a
  colour — dropping hardcoded_colors 456 -> 447 across 8 files. Nothing
  on this branch touches those files.
- this branch adds the canvas delete/pin/search chrome, which raises
  semantic_tokens 4020 -> 4030.

Merged totals are therefore hardcoded_colors 447 (dev's) and
semantic_tokens 4030 (this branch's); raw_nongray, raw_gray and
files_with_violations were identical on both sides and merged cleanly.

`totals` is informational — the ratchet compares PER-FILE counts — but it
is kept honest anyway. Verified with the gate itself:
`npx vitest run tests/unit/rawColorRatchet.spec.js` → 14 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q19uRCksdn4DiRAJ55rfpZ
… — mechanical, per the merge-train note on the PR

Blends the two Fable 5.1 rename conflicts (scheduling.md step 4, chat-and-sessions.md model FAQ) keeping both sides' additions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tCskBCnoydX8YAWFmm8TN
… lines (#2746) — mechanical, per the merge-train note on the PR

test_2110_widget_type_parity forbids backticking a fictional widget type on any line that says "widget type(s)", even in a sentence denying it exists. Unbackticks the three names at trinity-plugin.md:191 and faq/advanced-features.md:59; content unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tCskBCnoydX8YAWFmm8TN
# Conflicts:
#	docs/user-docs/agents/agent-canvas.md
@vybe vybe added the ui PR touches the frontend UI — triggers Playwright e2e tests label Sep 14, 2026
@github-actions

Copy link
Copy Markdown

🚧 Alembic head check could not run — this PR conflicts with dev.

git merge-tree reported conflicts, so there is no merged tree to check. GitHub cannot compute refs/pull/N/merge in this state either, which is why a conflicting PR shows no checks at all.

Merge dev into this branch and push. The head check re-runs automatically on the next push to dev touching src/backend/migrations/versions/**.

Advisory — this check does not block merge. · head_sha: de887dbbd8f29e1471f5aa00d282cef56c96275b · run

@vybe

vybe commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Train complete: #2743, #2752, #2753, #2619, #2746 merged individually. Closing the integration surface.

@vybe vybe closed this Sep 14, 2026
@vybe
vybe deleted the train/20260914-0901 branch September 14, 2026 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui PR touches the frontend UI — triggers Playwright e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants