Skip to content

feat(objectives): guard metrics in the objective join (abilityai/trinity-enterprise#731) - #3423

Merged
vybe merged 1 commit into
devfrom
feature/731-guard-metrics
Oct 9, 2026
Merged

vybe merged 1 commit into
devfrom
feature/731-guard-metrics

Conversation

@obasilakis

@obasilakis obasilakis commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes abilityai/trinity-enterprise#731.

What

An objective may list guard_metrics:: numbers that must not move while the objective's own metrics are pursued. The objective join ignored that key, so the 18 guards in the live canon were never checked or shown.

  • Parse. Each guard is appended to the objective's metrics list with role: "guard" (primaries get role: "primary"). One list means guards are fetched, and served through a grant (ent#727), by the existing code. Guards have their own per-objective cap; a name in both lists keeps the primary.
  • Judge. A guard always resolves to neutral / direction_source: objective, no mismatch, whatever the registry or its own direction: says. It is on_target within tolerance (default exact) or off_target, never behind / ahead. The same metric as a primary of another objective keeps its normal direction there.
  • Count. summary.guards = {total, on_target, off_target, not_computable, stale}. Every other summary bucket counts primaries only, so an objective without guards reports exactly what it did before. One summary factory serves the join and the empty answer.
  • Findings. guard_metrics_invalid (section not a list), guard_target_unset (no numeric target; 11 of the 18 live guards), guard_direction_invalid (a direction other than hold).
  • Workspace role card. PortalRoleMetric.role crosses as the fixed value guard | primary. A guard row shows a neutral "Guard" badge; guard_target_unset reads "Target not set".
  • MCP. get_objectives describes guards in one line. That line replaces a sentence describing a neutral / direction_source: "none" pair the join never returns, which keeps the description inside the 2048-char budget.
  • Docs. Requirements (lifecycle-observability §50.11, findings table, response shape), both feature flows, the agent guide.

Tests

  • tests/unit/test_ent731_guard_metrics.py (45): parse shapes and malformed input, the always-hold rule against every registry/declared pair, never behind/ahead, exact hold without tolerance, a metric that is a guard in one objective and a primary in another, served / stale / retired / undeclared / supporting-only guards, summary separation and empty-path parity, a guard fetched through the whole read_objective_join, and the portal role as a fixed value.
  • Mutation: with the guard branch in resolve_direction removed, 20 of those tests go red.
  • Existing tests updated for the new role key where they pin exact row contents (test_ent527_role_card, test_ent666_objective_join, test_ent676_role_card_join METRIC_KEYS).
  • portalAgentRole.spec.js: Guard badge mounted, and the two new finding sentences.
  • Full backend unit suite: 24669 passed. Frontend unit: 5076 passed with an English locale. MCP: 42 passed, including the description budget.

Security

/cso --diff: 0 findings (docs/security-reports/cso-diff-2026-10-09-ent731-guard-metrics.md). No new endpoint, auth change, read path, dependency or workflow. Guard names reach other agents' numbers only through the existing can_view grant check.

Conflict note

#3271 edits the same summary block in join_objectives; whichever lands second rebases.

🤖 Generated with Claude Code

…ity-enterprise#731)

An objective's top-level `guard_metrics:` list is now read. Each guard joins
the same `metrics` list tagged `role: "guard"`, so it is fetched and served
through a grant like any other row. A guard is always judged as a hold:
on_target within its tolerance or off_target, never behind or ahead,
whatever the registry or its own direction says. The same metric as a
primary of another objective keeps its normal direction there.

- summary.guards counts guards; every other bucket counts primaries only,
  so an objective without guards reports exactly as before
- findings: guard_metrics_invalid, guard_target_unset, guard_direction_invalid
- guards have their own per-objective cap
- Workspace role card: PortalRoleMetric.role (fixed guard|primary), a
  neutral "Guard" badge, "Target not set" for a guard with no target
- MCP get_objectives description names guards and drops a sentence about a
  neutral/none direction pair the join never returns

Mutation: with the guard-hold branch in resolve_direction removed, 20 tests
in tests/unit/test_ent731_guard_metrics.py go red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@obasilakis obasilakis added the ui PR touches the frontend UI — triggers Playwright e2e tests label Oct 9, 2026
@obasilakis
obasilakis requested a review from dolho October 9, 2026 07:54

@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 (2026-10-09): validated at 80825916 — /validate-pr and /review found no criticals; the tests execute the changed path and every new value has a live consumer. Merge with dev (after #3411) is clean. OSS-core placement approved by the operator.

@vybe
vybe merged commit ed59049 into dev Oct 9, 2026
29 of 31 checks passed
dolho added a commit that referenced this pull request Oct 12, 2026
…merge-train review

Conflicts with #3423 (ent#731 guard metrics), resolved by keeping both sides:
`PortalRoleMetric` carries `label` and `role`; the key-set pins list both. The
Guard badge sits on the owned objectives' metric rows, beside the plain-words
line. Supported objectives show headings only, so they show no badge. The
ent#812 "no seat" state keeps its sentence, without the old "Objectives" heading.

Beyond the conflicts (merge-train review, 2026-10-08 / 10-10):
- `tracked_by` is gone (model, projection, Vue branch, spec, requirement). The
  card never resolved `served_by`, so it could not produce the name. Naming
  another agent to an external client is a decision this card does not make.
  It says "Tracked by another agent".
- The fleet-wide `agreed_labels` is replaced by
  `declarations_elsewhere(agent_name, names)`: active declarations by the SAME
  owner's other live agents only. An agent of another owner can no longer put
  text on this owner's client-facing card.
- "Tracked elsewhere" is confirmed against those declarations. The join's
  `declared_elsewhere` is also true when no agent declares the number, so it
  is now only a candidate. A failed lookup makes no claim.
- `findingText` loses the two strings `findingLine` filters out.
- New card text uses the contract's tertiary ink
  (`text-gray-500 dark:text-gray-400`), not `text-gray-400` alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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