Skip to content

feat(workspace): the role card reads the objective ↔ metric join (abilityai/trinity-enterprise#676) - #3132

Merged
vybe merged 10 commits into
devfrom
feature/676-role-card-objective-join
Oct 1, 2026
Merged

vybe merged 10 commits into
devfrom
feature/676-role-card-objective-join

Conversation

@webmixgamer

@webmixgamer webmixgamer commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • The Workspace role card stops computing its own objective ↔ metric join and its own 30-day metric staleness. It calls the one join (objective_join_service.read_objective_join) and serves a field-picking client projection of it. Its old number source (the agent server's /api/metrics) has answered 410 since ent#479, so rows read "— / target · stale" before this change.
  • The card's objective read draws on the same per-agent budget as GET /api/agents/{name}/objectives, spelled once in services/objectives_read_budget.py, behind a per-viewer cap of a third of that limit (20/min at the default). The budget is spent only when the card is about to read objectives.
  • PortalAgentRole.vue renders actual/target by declared type, gap.status as a badge, freshness from the backend's verdict, a sentence per finding code, a named line when the objectives could not be read, and a line under a list that may be incomplete.

Deliberate departures from the issue text

  • The card never answers 429. The issue's acceptance line says "Portal role route returns 429 under the shared key". Decided at plan review: the agent's own get_objectives spends the same bucket and the card also carries the role, the readiness stamp and the owner's flip, so a whole-route 429 would let an agent hide its owner's control. A refused read is a 200 without objectives (objectives_error: objectives_rate_limited) and without a container fan-out. The operator route still answers 429.
  • type and unit are on the projection, beyond the issue's field list, so numbers format through utils/metricFormat.js.
  • summary is not projected; card-level findings cross as finding_codes (codes only), per the ruling at claim time.
  • A placement change was tried and reverted. An ent#527 follow-up decided at the eyeball moved the Role block above "Suggested for you". The PR review found that the block renders only after its role read answers, so it pushed the already-painted suggestions down; it is back below them (f8377831a), and PortalAgentDetails.vue is unchanged against dev.

Changes from the PR review

  • Docs and test seams: the public user guide still said metrics go stale after 30 days; two memory docs contradicted the code; the flow doc overclaimed; one route test patched a seam the route no longer calls; the two-doors tests now pin the limit they do arithmetic on.
  • The budget is spent only when the card is about to read objectives: the router passes a required admit_objectives callable, and the builder calls it after the role file, just before the join.
  • The per-viewer cap is derived (max(1, limit // 3)), so an operator who lowers OBJECTIVES_READ_RATE_LIMIT cannot leave one viewer able to empty it.
  • A partial list says so: objectives_partial plus one line when objectives joined but some objective files were not read.
  • After merging ent#727 (cross-agent metrics) from dev: the card passes no can_view, so the join's fail-closed default holds and a Workspace viewer never sees a number served by another agent. A test pins this against a store that does hold such a grant.

Changes

  • src/backend/client_portal/role_card.py — local join, metric staleness and /api/metrics read removed; portal_metric, portal_objective, objectives_error, finding_codes; objectives_partial; build_role_card takes a required admit_objectives callable.
  • src/backend/services/objectives_read_budget.py (new) — key, limit, window; enforce (429) and admit (bool).
  • src/backend/client_portal/router.py, routers/agent_files.py, client_portal/models.py.
  • src/frontend/src/components/portal/PortalAgentRole.vue.
  • Docs: requirements §5.36 and §50, the two feature flows, architecture/{workspace,backend,api-endpoints}.md, the public user guide (docs/user-docs/sharing-and-access/workspace.md), three learnings fragments, the /cso --diff report.

Test Plan

  • tests/unit/test_ent676_role_card_join.py (40): the projection's key sets as literals, the card through the real join, every named "no objectives" state, the two doors on the real in-process limiter.
  • test_ent666_objective_join.py::test_there_is_exactly_one_stale_rule_and_this_module_imports_it extended to role_card.py.
  • src/frontend/tests/unit/portalAgentRole.spec.js (mounted, 35): every gap, freshness, finding and error state, and the partial-list line.
  • Mutations: 10 on role_card.py / router.py and 7 on the component at first; each review fix carries its own (budget asked at the door, a fixed viewer cap, dropping the partial flag or its line, a can_view on the card's join, the limiter above the self-gate). Every one turned a named test red.
  • Operator eyeball on localhost, light and dark: the seeded fixture rendered every row as expected, the budget tripped on the 21st read and recovered, the copy was approved. The partial-list line added at review: copy approved by the owner; not eyeballed on localhost.
  • /verify-local --skip-agent --skip-unit on 649400ab0: PASS — prod backend image built and import main inside it, isolated stack booted healthy, integration 70 passed / 13 skipped / 2 deselected (the two registry false-fails). The sequential unit stage was skipped; the unit suite was run separately.
  • On the final tree (dev fff19824c merged): 255 unit files touching metrics, objectives, the portal, the limiter or the docs, 7,641 passed. The 5 failures are the order-dependent test_2703_skill_assign_delivery.py set (25 passed alone; this branch touches no skill code). Ten frontend specs and ratchets, 257 passed.

No migrations. No MCP, agent-server or dependency change.

Fixes abilityai/trinity-enterprise#676

🤖 Generated with Claude Code

webmixgamer and others added 3 commits September 30, 2026 14:57
…lityai/trinity-enterprise#676)

The Workspace role card (ent#527) shipped with its own objective ↔ metric
join and a 30-day metric staleness rule over the agent's metrics.json. The
platform's one join (ent#666) and one stale rule (ent#479, 2x cadence)
landed after it, so the join was spelled twice and the card's numbers came
from a source that has answered 410 since ent#479.

- role_card.py drops its join, its metric staleness and the /api/metrics
  read, and calls objective_join_service.read_objective_join with the
  template and client it already holds. is_stale / STALE_AFTER_DAYS remain
  for the role file's review_by only.
- The card serves a field-picking projection: a finding crosses as its
  code, never the operator sentence, an objective file path or the role
  owner. The Workspace holds its own sentence per code.
- No objectives is silent only when it is true. objectives_error names a
  door that died, a slow read, files that would not read, a store failure
  and a refused budget; the role, readiness and the owner's flip survive
  each.
- One budget, two doors: services/objectives_read_budget.py spells the
  agent_objectives_read key, limit and window once. The operator route
  answers 429; the card never does, because the agent can empty the bucket
  through its own get_objectives and the card carries its owner's control.
  A per-viewer cap (20/min per agent) is spent first.
- PortalAgentRole.vue renders actual/target by declared type, gap.status
  as a badge and freshness from the backend's verdict.

Decided at plan review, against the issue's acceptance line "returns 429
under the shared key": a refused read is a 200 without objectives.

Tests: tests/unit/test_ent676_role_card_join.py (the projection's key sets
as literals, the card through the real join, the two doors on the real
in-process limiter), the one-stale-rule test extended to role_card.py, and
the mounted spec for every gap, freshness, finding and error state.
Mutations: 10 on role_card.py / router.py and 7 on the component, each
turning a named test red.

Refs Abilityai/trinity-enterprise#676

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…27 placement follow-up)

Decided at the Abilityai/trinity-enterprise#676 eyeball: in the Info tab
the Role block (role, objectives, relationship, readiness) moves to
directly after the agent's description and before the suggestions. The
reading order is identity, then purpose and state, then actions. The
placement itself came from ent#527 (#2927), so this is its own commit.

The card renders only for a companion with a role, so every other agent's
panel is unchanged.

Test: portalAgentRole.spec.js mounts PortalAgentDetails and asserts the
Role section precedes the suggestions section in the DOM; it was red
before the move.

Refs Abilityai/trinity-enterprise#527, Abilityai/trinity-enterprise#676

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three doc conflicts, each resolved by keeping both sides: dev renamed the
ent#661 'project hub' to 'project view (ent#661 v3)' on lines this branch
also edits (api-endpoints.md, backend.md, lifecycle-observability.md §50.10).
Source files merged cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@webmixgamer webmixgamer added the ui PR touches the frontend UI — triggers Playwright e2e tests label Sep 30, 2026
webmixgamer and others added 7 commits September 30, 2026 20:45
…" (ent#527 placement follow-up)"

This reverts commit 649400a.

The /review of PR #3132 found that the Role block renders nothing until its
role read answers, and that read includes the container fan-out, while
"Suggested for you" paints its skeleton at once. Above the suggestions, a
companion's Role block therefore arrived late and pushed the suggestions
and their buttons down the panel (design-system principles 4 and 30). Below
them, its arrival only moves the sections under it. Decided at review: move
it back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inity-enterprise#676)

- docs/user-docs/sharing-and-access/workspace.md still told Workspace users
  a metric goes stale after 30 days. It now states the 2x cadence rule, "no
  points yet", the target badges, the finding line and the named failed read.
- architecture/backend.md said the role card imports `freshness`; it
  reaches it through the objective join. lifecycle-observability.md §50.6
  sized the budget on "ten role cards polling at 30 s"; the card never polls.
- workspace-role-card.md claimed no objectives is "only ever silent when it
  is true". It now says which failures name themselves, that a file-level
  failure cannot be attributed to a role, and that a role-file error stops
  the card before the objectives are read.
- test_ent666_objectives_route: the 403-before-limiter test patches the seam
  the route calls (`objectives_read_budget.enforce`), not `rate_limiter`
  beneath it. Mutation: moving the limiter above the self-gate turns it red.
- test_ent676: the two-doors fixture pins the limit its arithmetic uses.
- A learnings fragment: the flow sync does not reach docs/user-docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…objectives (Abilityai/trinity-enterprise#676)

PR #3132's review (both independent reviewers) found the portal route
deciding admission before the card was built. Every Info-tab open therefore
spent one viewer token and one shared `agent_objectives_read` token, even
for an agent with no role, a stopped agent or a broken role file, none of
which reads an objective.

The router now passes the same two checks, in the same order, as a
required `admit_objectives` callable. `build_role_card` calls it once, after
the role file has been read and just before the join. The builder stays
HTTP-free (Invariant #1), and there is still no default.

Test: test_a_card_that_reads_no_objectives_spends_no_budget (no role, role
file missing, bad role id, stopped). Mutation: asking at the top of the
builder turns all four red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d limit (Abilityai/trinity-enterprise#676)

PR #3132's review: the per-viewer cap was a fixed 20 while the shared
budget it sits in front of comes from OBJECTIVES_READ_RATE_LIMIT. An
operator who lowered that limit to 20 or less would let one Workspace
viewer empty the budget the operator route and the agent's own
get_objectives draw on, and the "at most a third" comment would be false.

The cap is now derived: max(1, limit // 3), in the budget's window. At the
default 60 it is 20 a minute, so default behaviour is unchanged.

Test: test_the_viewer_cap_is_a_third_of_the_limit_whatever_the_operator_sets
(limit 12: the viewer gets 4, the operator door keeps 8). Mutation: a fixed
20 turns it red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bilityai/trinity-enterprise#676)

PR #3132's review (both independent reviewers): when some objectives
joined but one of the agent's objective files failed to read or parse, the
card showed the objectives it had with no sign that any were missing.
`objectives_error` covered only the zero-objectives case (the claim-time
ruling), and nothing rendered `finding_codes`.

The card now carries `objectives_partial`: set when objectives joined AND
the join reported a file-level failure (objective_unreadable,
objective_invalid, objective_file_skipped) or files beyond the scan bound.
The Role card shows one line under the list: "Some objective files in the
agent's canon couldn't be read, so this list may be incomplete." It says
"may" because a file-level failure cannot be attributed to a role.

Tests: a table over objectives_partial, one unreadable file beside a joined
objective through the real join, and two mounted cases. Mutations: dropping
the assignment and dropping the line each turn a test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…card-objective-join

Conflicts, each resolved by keeping both sides:
- routers/agent_files.py imports: dev's metric_access_service plus this
  branch's objectives_read_budget.
- architecture/api-endpoints.md and backend.md: dev's ent#727 wording with
  this branch's shared-budget and role-card sentences applied to it.

One semantic conflict: ent#727 added a required `request` parameter to
`get_agent_objectives`, which test_ent676's two-doors harness calls
directly; it now passes `request=None` (the parameter only feeds the
cross-read audit, which a stubbed join never triggers).

The role card calls `read_objective_join` without `can_view`, so ent#727's
cross-agent resolution stays closed for it (the join's documented
fail-closed default).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bilityai/trinity-enterprise#676)

ent#727 (merged from dev) lets the objective join read a metric from an
agent the reader holds a grant on, but only when the caller passes a
`can_view` naming who is looking; `None` fails closed. The role card
passes none, so a Workspace viewer, possibly an external client, never sees
a number served by an agent they may not be rostered on.

This pins that through the real join with a store that does hold such a
grant, and proves the test is not vacuous by resolving the same metric
through the join when a viewer is named. Mutation: handing the card's join
`can_view=lambda t: True` turns it red on the resolved value (99.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@webmixgamer

Copy link
Copy Markdown
Contributor Author

/review summary

Full structural review of this branch against dev, run as two independent passes (backend, and frontend + docs) with every finding checked against the lines it cites. Head at review time: 433280f57. Fixes landed through c8466c470.

Critical findings: none. Checked:

  • Auth: the roster gate (uniform 404) runs before either limiter key is built, and both keys use the validated name.
  • Disclosure: the projected field sets are pinned as literals, and operator sentences, objective file paths and owner: role:<id> never cross to the Workspace.
  • Credentials, SQL, enterprise docs: no credential handling or SQL added; the enterprise-docs guard has 0 hits.

Fixed on this PR

Finding Commit
The Role block moved above "Suggested for you" rendered only after its role read answered, so it pushed the already-painted suggestions down (design-system principles 4 and 30). It is back below them f8377831a
The public user guide still said a metric goes stale after 30 days. Two memory docs contradicted the code, and the flow doc overclaimed when an empty objectives block is silent. One route test patched a seam the route no longer calls, and the two-doors tests did arithmetic on an env-tunable limit fc7204318
The objectives budget was spent on every Info-tab open, before the card knew whether it would read anything. It is now spent only after the role file is read, just before the join a3c1fd6d6
The per-viewer cap was a fixed 20 in front of an env-tunable shared limit, so a lowered limit let one viewer empty the budget. It is now max(1, limit // 3), still 20 at the default bcdd0deb8
A partial list (some objective files failed to read) looked complete. objectives_partial now adds one line under the list 65abe5d99
After merging ent#727 (cross-agent metrics): pinned that the card passes no can_view, so a Workspace viewer never sees a number served by another agent c8466c470

Each fix carries a mutation that turned a named test red before the fix went in.

Left as is, low

  • A metric-store outage shows the general "couldn't be read" line rather than a distinct one; this is deliberate, a single catch-all code.
  • The role route itself has no rate limit beyond the objectives budget. That predates this PR, and the fan-out is now bounded. It is registered for follow-up.
  • A failed role read still renders nothing, and "Try again" has no retry control. This predates the PR and is registered for follow-up.
  • The new "no points yet" text uses the file's existing text-gray-400.
  • The compose-file comments on OBJECTIVES_READ_RATE_LIMIT still describe GET /objectives only; .env.example is updated.

Verification

  • Unit: on the final tree, 255 unit files touching metrics, objectives, the portal, the limiter or the docs gave 7,641 passed. The 5 failures are the order-dependent test_2703_skill_assign_delivery.py set; it passes alone and this branch touches no skill code.
  • Frontend: the specs and ratchets pass, 257 tests.
  • CI: green on c8466c470, including regression diff, e2e, journey-smoke, prod-image-smoke, CodeQL and schema-parity.
  • /validate-pr: APPROVE. After merge, set status-in-dev on abilityai/trinity-enterprise#676 by hand, because the closing keyword does not cross repos.

@vybe

vybe commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

merge-train (#3143): on this train. /validate-pr came back READY with no criticals, and the claims in your /review summary checked out.

Non-blocking, for a follow-up:

  • finding_codes (card-level, models.py:646) is served and tested on the backend, but nothing in PortalAgentRole.vue reads it. Either render it or stop serving it.
  • The compose comments at docker-compose.yml:108, docker-compose.prod.yml:169 and docker-compose.hosted.yml:222 still describe the old OBJECTIVES_READ_RATE_LIMIT scope.

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train: batch validated on train/#3143 (all Tier 1 + Tier 2 green)

@vybe
vybe merged commit 2a791a4 into dev Oct 1, 2026
28 of 30 checks passed
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.

2 participants