fix(rules): planning reads list rules by id and title instead of restating them (#4081)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 53s
CI & Build / TypeScript typecheck (push) Successful in 1m3s
CI & Build / Python tests (push) Failing after 1m4s
CI & Build / Build & push image (push) Skipped

start_planning on project 2 replied with 92,645 characters, 65k of them
applicable_rules. Milestone 414 made a project's listing every global rule
tagged to an area it works in (before, the rules of subscribed rulebooks, and
project 2 subscribed to none), and the non-brief rules_payload sent each as a
full rule_brief. get_milestone, get_project and get_task carried the same.

Every rules_payload form now lists: id, title, the topic a global rule sits in,
and `via` for a co_surfaces partner. get_rule reads one in full, and retrieval
delivers them in full when work matches — the reasoning #4045 applied to the
handshake. A test pins 81 full-length rules under 6k characters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-15 13:26:48 -04:00
co-authored by Claude Opus 5
parent 0bf7406f42
commit ac01eee040
2 changed files with 49 additions and 14 deletions
+24 -14
View File
@@ -1112,36 +1112,46 @@ def rules_payload(
(`plugin_context`) — computes a marker and shows nobody anything, and
counting it would put rules in the denominator that no agent ever saw.
EVERY FORM LISTS, NONE RESTATES. Rules reach a session in full by
retrieval, so these payloads say which constraints exist — id and title,
the topic a global rule sits in, `via` for a co_surfaces partner — and
get_rule reads one. Planning reads carried the full rule_brief until a
project's listing grew to every global rule tagged to its areas
(milestone 414) and start_planning replied with 92k characters (#4081),
the shape #4045 had just removed from the handshake.
`brief` is the session handshake's form (#4045): the project's own rules
as id and title, nothing else. Rules reach a session in full by
retrieval, so the handshake lists which of the project's constraints exist
rather than restating them; get_rule reads one. Only what is shown is
recorded as surfaced.
only. Only what is shown is recorded as surfaced.
"""
project_rules = [_rule_line(r) for r in applicable.get("project_rules", [])]
if brief:
project_rules = [
{"id": r["id"], "title": r["title"]}
for r in applicable.get("project_rules", [])
]
record_rule_surfaced(
user_id=user_id, rule_ids=[r["id"] for r in project_rules], source=source,
)
return {"project_rules": project_rules}
rules = [_rule_line(r) for r in applicable.get("rules", [])]
record_rule_surfaced(
user_id=user_id,
rule_ids=(
[r["id"] for r in applicable.get("rules", [])]
+ [r["id"] for r in applicable.get("project_rules", [])]
),
rule_ids=[r["id"] for r in rules] + [r["id"] for r in project_rules],
source=source,
)
return {
"applicable_rules": applicable["rules"],
"applicable_rules": rules,
"applicable_rules_truncated": applicable["truncated"],
"project_rules": applicable.get("project_rules", []),
"project_rules": project_rules,
}
def _rule_line(brief: dict) -> dict:
"""One rule as a listing names it: enough to recognise it and fetch it.
Keys a row does not carry are left out rather than sent empty (#2483)."""
line = {"id": brief["id"], "title": brief["title"]}
for key in ("topic_title", "via"):
if brief.get(key):
line[key] = brief[key]
return line
# ── The staleness marker (milestone 323 step 5) ────────────────────────
#
# WHAT THIS CAN AND CANNOT SEE. An etag catches a rule that MOVED after a