Repository navigation
feat(workspace): the role card reads the objective ↔ metric join (abilityai/trinity-enterprise#676) - #3132
Merged
Conversation
…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>
…" (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>
Contributor
Author
|
| 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_LIMITstill describeGET /objectivesonly;.env.exampleis 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.pyset; it passes alone and this branch touches no skill code. - Frontend: the specs and ratchets pass, 257 tests.
- CI: green on
c8466c470, includingregression diff,e2e,journey-smoke,prod-image-smoke, CodeQL andschema-parity. /validate-pr: APPROVE. After merge, setstatus-in-devon abilityai/trinity-enterprise#676 by hand, because the closing keyword does not cross repos.
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:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
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.GET /api/agents/{name}/objectives, spelled once inservices/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.vuerenders actual/target by declared type,gap.statusas 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
get_objectivesspends 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.typeandunitare on the projection, beyond the issue's field list, so numbers format throughutils/metricFormat.js.summaryis not projected; card-level findings cross asfinding_codes(codes only), per the ruling at claim time.f8377831a), andPortalAgentDetails.vueis unchanged againstdev.Changes from the PR review
admit_objectivescallable, and the builder calls it after the role file, just before the join.max(1, limit // 3)), so an operator who lowersOBJECTIVES_READ_RATE_LIMITcannot leave one viewer able to empty it.objectives_partialplus one line when objectives joined but some objective files were not read.dev: the card passes nocan_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/metricsread removed;portal_metric,portal_objective,objectives_error,finding_codes;objectives_partial;build_role_cardtakes a requiredadmit_objectivescallable.src/backend/services/objectives_read_budget.py(new) — key, limit, window;enforce(429) andadmit(bool).src/backend/client_portal/router.py,routers/agent_files.py,client_portal/models.py.src/frontend/src/components/portal/PortalAgentRole.vue.architecture/{workspace,backend,api-endpoints}.md, the public user guide (docs/user-docs/sharing-and-access/workspace.md), three learnings fragments, the/cso --diffreport.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_itextended torole_card.py.src/frontend/tests/unit/portalAgentRole.spec.js(mounted, 35): every gap, freshness, finding and error state, and the partial-list line.role_card.py/router.pyand 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, acan_viewon the card's join, the limiter above the self-gate). Every one turned a named test red./verify-local --skip-agent --skip-uniton649400ab0: PASS — prod backend image built andimport maininside 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.fff19824cmerged): 255 unit files touching metrics, objectives, the portal, the limiter or the docs, 7,641 passed. The 5 failures are the order-dependenttest_2703_skill_assign_delivery.pyset (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