strata-agent-memory 0.2.3__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. {strata_agent_memory-0.2.3/strata_agent_memory.egg-info → strata_agent_memory-0.2.4}/PKG-INFO +2 -1
  2. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/README.md +1 -0
  3. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/__init__.py +1 -1
  4. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/approvals.py +22 -2
  5. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/audit.py +112 -6
  6. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/cli.py +266 -29
  7. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/compiler.py +169 -8
  8. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/contracts.py +2 -1
  9. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/evidence.py +558 -29
  10. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/export.py +2 -1
  11. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/hooks.py +21 -2
  12. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/instances.py +4 -4
  13. strata_agent_memory-0.2.4/agent_memory/kit.py +256 -0
  14. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/ledger.py +374 -66
  15. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/mcp_server.py +18 -3
  16. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/pathmap.py +78 -5
  17. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/relay/adapters.py +95 -17
  18. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/relay/cli.py +90 -16
  19. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/relay/client.py +167 -28
  20. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/relay/core.py +222 -33
  21. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/remote.py +47 -12
  22. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/render.py +97 -21
  23. strata_agent_memory-0.2.4/agent_memory/roster.py +277 -0
  24. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/serve.py +56 -8
  25. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/README.md +94 -0
  26. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/onboard-agent.sh +5 -3
  27. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/relay-agent.sh +9 -14
  28. strata_agent_memory-0.2.4/deploy/plugins/.claude-plugin/marketplace.json +12 -0
  29. strata_agent_memory-0.2.4/deploy/plugins/README.md +112 -0
  30. strata_agent_memory-0.2.4/deploy/plugins/claude-code/.claude-plugin/plugin.json +8 -0
  31. strata_agent_memory-0.2.4/deploy/plugins/claude-code/bin/agent-memory-approvals +235 -0
  32. strata_agent_memory-0.2.4/deploy/plugins/claude-code/hooks/hooks.json +16 -0
  33. strata_agent_memory-0.2.4/deploy/plugins/claude-code/rules/approvals.md +6 -0
  34. strata_agent_memory-0.2.4/deploy/plugins/claude-code/skills/approvals/SKILL.md +23 -0
  35. strata_agent_memory-0.2.4/deploy/plugins/claude-code/skills/approve/SKILL.md +25 -0
  36. strata_agent_memory-0.2.4/deploy/plugins/claude-code/skills/reject/SKILL.md +25 -0
  37. strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-answer/SKILL.md +22 -0
  38. strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-answer/agents/openai.yaml +6 -0
  39. strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-approvals/SKILL.md +20 -0
  40. strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-approvals/agents/openai.yaml +6 -0
  41. strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-approvals/rules/approvals.md +6 -0
  42. strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-approvals/scripts/agent-memory-approvals +235 -0
  43. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/relay/install-relay-mac.sh +40 -5
  44. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/relay/install-relay.sh +6 -3
  45. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/relay/macos/com.strata.agent-memory.relay.plist.template +5 -0
  46. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/pyproject.toml +16 -2
  47. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4/strata_agent_memory.egg-info}/PKG-INFO +2 -1
  48. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/strata_agent_memory.egg-info/SOURCES.txt +21 -0
  49. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_audit.py +116 -0
  50. strata_agent_memory-0.2.4/tests/test_changelog_fragments.py +138 -0
  51. strata_agent_memory-0.2.4/tests/test_cli.py +526 -0
  52. strata_agent_memory-0.2.4/tests/test_compiler.py +187 -0
  53. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_compiler_render.py +208 -2
  54. strata_agent_memory-0.2.4/tests/test_config.py +296 -0
  55. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_contracts.py +27 -0
  56. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_deploy_mac.py +343 -4
  57. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_deploy_relay.py +86 -2
  58. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_deploy_tokens.py +52 -0
  59. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_evidence.py +364 -0
  60. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_hooks.py +33 -0
  61. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_packaging.py +17 -0
  62. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_pathmap.py +78 -0
  63. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_phase2.py +43 -1
  64. strata_agent_memory-0.2.4/tests/test_plugins.py +437 -0
  65. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_records.py +39 -0
  66. strata_agent_memory-0.2.4/tests/test_relay_adapters.py +316 -0
  67. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_relay_client.py +479 -13
  68. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_relay_core.py +264 -1
  69. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_remote.py +197 -0
  70. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_serve.py +83 -0
  71. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_trust.py +378 -0
  72. strata_agent_memory-0.2.3/agent_memory/kit.py +0 -94
  73. strata_agent_memory-0.2.3/tests/test_cli.py +0 -285
  74. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/LICENSE +0 -0
  75. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/__main__.py +0 -0
  76. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/compact.py +0 -0
  77. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/identity.py +0 -0
  78. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/memorizer.py +0 -0
  79. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/recheck.py +0 -0
  80. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/records.py +0 -0
  81. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/relay/__init__.py +0 -0
  82. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/reliability.py +0 -0
  83. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/signing.py +0 -0
  84. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/stats.py +0 -0
  85. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/agent_memory/trust.py +0 -0
  86. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/agent-keys.sh +0 -0
  87. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/agent-session.sh +0 -0
  88. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/backup-tokens.sh +0 -0
  89. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/install-hooks-mac.sh +0 -0
  90. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/install-relay-driver.sh +0 -0
  91. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/install-replica-driver.sh +0 -0
  92. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/install-runner-driver.sh +0 -0
  93. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/instances.example.json +0 -0
  94. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/seed-board.sh +0 -0
  95. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/seed-mail-context.sh +0 -0
  96. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/seed-topics.sh +0 -0
  97. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/mac/upgrade-driver.sh +0 -0
  98. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/relay/README.md +0 -0
  99. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/relay/agent-memory-relay@.service +0 -0
  100. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/deploy/relay/relay-launch.sh +0 -0
  101. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/setup.cfg +0 -0
  102. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/strata_agent_memory.egg-info/dependency_links.txt +0 -0
  103. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/strata_agent_memory.egg-info/entry_points.txt +0 -0
  104. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/strata_agent_memory.egg-info/requires.txt +0 -0
  105. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/strata_agent_memory.egg-info/top_level.txt +0 -0
  106. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_backlog_check.py +0 -0
  107. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_compact.py +0 -0
  108. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_deploy_replica.py +0 -0
  109. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_identity.py +0 -0
  110. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_issue_bridge.py +0 -0
  111. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_ledger.py +0 -0
  112. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_ledger_index.py +0 -0
  113. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_memorizer.py +0 -0
  114. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_recheck.py +0 -0
  115. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_redaction.py +0 -0
  116. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_relay_codex.py +0 -0
  117. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_reliability.py +0 -0
  118. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_remote_signing.py +0 -0
  119. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_review2_fixes.py +0 -0
  120. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_review3_fixes.py +0 -0
  121. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_review_fixes.py +0 -0
  122. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_scale.py +0 -0
  123. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_signing.py +0 -0
  124. {strata_agent_memory-0.2.3 → strata_agent_memory-0.2.4}/tests/test_stats.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strata-agent-memory
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Portable, auditable, cross-vendor memory for LLM agents: evidence-bound claims, trust tiers, and compiled context views.
5
5
  Author: Strata Intelligence
6
6
  License-Expression: Apache-2.0
@@ -201,6 +201,7 @@ tests/ pytest suite; tests/sit/phase3/ is the CLI-driven system test
201
201
  - [docs/RESEARCH-SYNTHESIS.md](docs/RESEARCH-SYNTHESIS.md): the nine sources, what was taken from each, and what none of them had.
202
202
  - [docs/INTEGRATION.md](docs/INTEGRATION.md): setup per harness, the local verifier lane, the contract loop.
203
203
  - [docs/COMMS-BUS-PLAN.md](docs/COMMS-BUS-PLAN.md): how agents talk through the ledger without spending tokens while they wait, and [docs/RUNBOOK-RELAY.md](docs/RUNBOOK-RELAY.md) to run a relay per agent.
204
+ - [docs/HARNESS-MATRIX.md](docs/HARNESS-MATRIX.md): the M3 harness reach matrix: MCP, hooks, wake paths, compile injection and relay fitness for every harness surface, from vendor documentation (nothing run).
204
205
  - [docs/LANDSCAPE.md](docs/LANDSCAPE.md): the competing systems, their verified licenses, the standards receipts align with, and what none of them do.
205
206
  - [docs/UAT-PLAN.md](docs/UAT-PLAN.md): the one-week acceptance test across Claude Code, Codex, Antigravity and Grok.
206
207
  - [docs/UAT-SELF.md](docs/UAT-SELF.md): the same test for one person with two subscriptions, Claude Code and Codex CLI, with `scripts/uat/` to set it up and to run the cross-lineage verification.
@@ -177,6 +177,7 @@ tests/ pytest suite; tests/sit/phase3/ is the CLI-driven system test
177
177
  - [docs/RESEARCH-SYNTHESIS.md](docs/RESEARCH-SYNTHESIS.md): the nine sources, what was taken from each, and what none of them had.
178
178
  - [docs/INTEGRATION.md](docs/INTEGRATION.md): setup per harness, the local verifier lane, the contract loop.
179
179
  - [docs/COMMS-BUS-PLAN.md](docs/COMMS-BUS-PLAN.md): how agents talk through the ledger without spending tokens while they wait, and [docs/RUNBOOK-RELAY.md](docs/RUNBOOK-RELAY.md) to run a relay per agent.
180
+ - [docs/HARNESS-MATRIX.md](docs/HARNESS-MATRIX.md): the M3 harness reach matrix: MCP, hooks, wake paths, compile injection and relay fitness for every harness surface, from vendor documentation (nothing run).
180
181
  - [docs/LANDSCAPE.md](docs/LANDSCAPE.md): the competing systems, their verified licenses, the standards receipts align with, and what none of them do.
181
182
  - [docs/UAT-PLAN.md](docs/UAT-PLAN.md): the one-week acceptance test across Claude Code, Codex, Antigravity and Grok.
182
183
  - [docs/UAT-SELF.md](docs/UAT-SELF.md): the same test for one person with two subscriptions, Claude Code and Codex CLI, with `scripts/uat/` to set it up and to run the cross-lineage verification.
@@ -9,4 +9,4 @@ see, what did it claim, and can the proof be re-run.
9
9
  The core is dependency-free (Python 3.10+ standard library only).
10
10
  """
11
11
 
12
- __version__ = "0.2.3"
12
+ __version__ = "0.2.4"
@@ -37,6 +37,7 @@ import re
37
37
  from typing import Iterable, List, Optional
38
38
 
39
39
  from . import contracts
40
+ from .relay.core import agent_name
40
41
  from .records import ValidationError, new_record, scope_for_target, sort_key
41
42
 
42
43
  GATE_KINDS = ("close", "retraction", "reopen", "reject", "constraint", "needs-human")
@@ -80,6 +81,17 @@ def _who(author: dict) -> str:
80
81
  return f"{author.get('harness') or author.get('kind') or '?'}/{author.get('user') or '?'}"
81
82
 
82
83
 
84
+ def _on_thread(ledger, thread: str, author: dict) -> bool:
85
+ """Whether a note's author is on the thread its tag names: the root's author or one of
86
+ its addressees, as the relay that stopped the thread is."""
87
+ root = ledger.get(thread)
88
+ if not isinstance(root, dict):
89
+ return False
90
+ names = {agent_name(root.get("author") or {})}
91
+ names.update(t[len("to:"):] for t in _tags(root) if t.startswith("to:"))
92
+ return agent_name(author) in names
93
+
94
+
83
95
  def _answered(ledger) -> tuple:
84
96
  """({item id: the newest human answer's ts}, {thread: the newest human record's ts}).
85
97
 
@@ -266,12 +278,20 @@ def pending(ledger, assessment=None, reader: Optional[str] = None) -> dict:
266
278
  if "needs-human" in tags: # an alarm in either mode: auto removes approvals, not alarms
267
279
  thread = next((t[len("thread:"):] for t in tags if t.startswith("thread:")), None)
268
280
  if thread and released.get(thread, "") <= (note.get("ts") or ""):
281
+ # a relay's note on a stop something released before the note was due (a
282
+ # contract update, section 11) is news for a person, and waits on nobody --
283
+ # when the relay that stopped is the one saying so: any token can file a note,
284
+ # and one tagged released by a stranger to the thread is still a stop
285
+ gone = "released" in tags and _on_thread(ledger, thread, note.get("author") or {})
269
286
  items.append(_item(ledger, "needs-human", note["id"], (note.get("body") or {}).get("contract"),
270
287
  f"thread {thread}", _short(note["body"].get("statement")),
271
288
  f"evidence: the last message on it is {', '.join(note.get('derived_from') or []) or '?'}",
272
289
  {"verdict": None, "id": None, "line": "review: none (a breaker, not work)"},
273
- {"id": None, "line": "risk: the thread stays stopped until a person answers on it"},
274
- "approve", gate=True, thread=thread))
290
+ {"id": None, "line": ("risk: none -- the thread was released before this note, "
291
+ "and what it held goes to the agent with the release")
292
+ if gone else
293
+ "risk: the thread stays stopped until a person answers on it"},
294
+ "approve", gate=not gone, thread=thread))
275
295
  continue
276
296
  kind = next((t[len("approval:"):] for t in tags if t.startswith("approval:")), None)
277
297
  if kind not in kinds:
@@ -28,8 +28,10 @@ from typing import Dict, List, Optional
28
28
 
29
29
  from . import evidence as ev
30
30
  from . import reliability as rel
31
+ from . import roster as roster_mod
31
32
  from . import signing
32
33
  from .contracts import status_of
34
+ from .ledger import UNTRUSTED_POLICY
33
35
  from .records import is_redacted, receipt_visible_to
34
36
  from .trust import TIERS, assess, lineage_differs, summarize
35
37
 
@@ -118,6 +120,74 @@ def _capped(items: List[dict]) -> dict:
118
120
  return {"total": len(items), "items": newest[:_SECTION_CAP]}
119
121
 
120
122
 
123
+ # a line break, or any other control character, in a field a record carries: the report is
124
+ # read by people and, through memory_audit, by models, and one row must stay one row
125
+ _BREAKS = re.compile(r"[\x00-\x1f\x7f-\x9f\u2028\u2029]+")
126
+
127
+
128
+ def _one_line(text: str) -> str:
129
+ """A report row flattened to one line: a harness or a user is whatever the filing
130
+ process said it was, and a newline in one would forge a heading in the report."""
131
+ return _BREAKS.sub(" ", str(text))
132
+
133
+
134
+ ROSTER_OUTSIDERS = ("people and the ci lineage are on no agent roster by design and are not listed; a "
135
+ "person's or ci's record stays what it is (docs/PM-BRIEF.md step 4)")
136
+
137
+
138
+ def _roster_sections(ledger, rep: Dict, by_id: Dict[str, dict], reader_user: Optional[str]) -> None:
139
+ """The three questions the roster answers for a corpus (docs/PROTOCOL.md section 12):
140
+ who filed without being on it, who filed after being retired from it, and who
141
+ verified a claim of a project whose roster they are not on.
142
+
143
+ None of them moves a tier or refuses anything: each is the question to ask before the
144
+ lineage sections above are read as facts. A ledger with no roster answers none of
145
+ them, and says so rather than listing everybody.
146
+ """
147
+ config = ledger.config
148
+ roster = roster_mod.entries(config)
149
+ on_roster = roster_mod.by_user(config)
150
+ has_roster = bool(roster)
151
+ # every lookup the loop makes, built once: per record they would rebuild the roster, and
152
+ # an audit is records x roster that way
153
+ retired = {roster_mod.user_key(e.get("user")): e["retired"] for e in roster.values()
154
+ if isinstance(e, dict) and e.get("retired")}
155
+ project_users: Dict[str, set] = {}
156
+ for e in roster.values():
157
+ for project in (e.get("projects") or []) if isinstance(e, dict) else ():
158
+ project_users.setdefault(project, set()).add(roster_mod.user_key(e.get("user")))
159
+ has_projects = bool(project_users)
160
+ off, late, outside = [], [], []
161
+ for rec in ledger.all() if has_roster else ():
162
+ if rec["kind"] not in _IDENTITY_KINDS or not _reader_sees(ledger, rec, by_id, reader_user):
163
+ continue
164
+ a = rec["author"]
165
+ if a.get("kind") == "human" or a.get("harness") == "ci":
166
+ continue
167
+ row = {"id": rec["id"], "kind": rec["kind"], "user": a.get("user"), "harness": a.get("harness"),
168
+ "ts": rec["ts"]}
169
+ key = roster_mod.user_key(a.get("user"))
170
+ if key not in on_roster:
171
+ off.append(row)
172
+ elif key in retired and rec["ts"][:10] > retired[key]: # after the retired date, not on it
173
+ late.append(dict(row, retired=retired[key]))
174
+ if rec["kind"] == "verification": # an off-roster verifier is outside the project's roster too
175
+ claim = ledger.get((rec.get("body") or {}).get("claim"))
176
+ project = ((claim or {}).get("scope") or {}).get("project")
177
+ users = project_users.get(project) # None: the roster says nothing about this project
178
+ if claim is not None and users is not None and key not in users:
179
+ outside.append(dict(row, claim=claim["id"], project=project,
180
+ verdict=(rec.get("body") or {}).get("verdict")))
181
+ rep["records_off_roster"] = _capped(off)
182
+ rep["records_after_retirement"] = _capped(late)
183
+ rep["verifications_outside_project_roster"] = _capped(outside)
184
+ for key in ("records_off_roster", "records_after_retirement", "verifications_outside_project_roster"):
185
+ if not has_roster:
186
+ rep[key]["no_roster"] = True
187
+ if has_roster and not has_projects:
188
+ rep["verifications_outside_project_roster"]["no_projects"] = True
189
+
190
+
121
191
  def report(ledger, recheck: bool = False, cwd: Optional[str] = None, timeout: Optional[float] = 300,
122
192
  path_map=None, reader_user: Optional[str] = None) -> dict:
123
193
  """The audit, for one reader. ``reader_user`` is who is asking: a user-scoped claim is
@@ -165,6 +235,10 @@ def report(ledger, recheck: bool = False, cwd: Optional[str] = None, timeout: Op
165
235
  "signatures": {},
166
236
  "unsigned_by_registered_signers": {},
167
237
  "verifications_by_unregistered_identities": {},
238
+ "roster_problems": list(getattr(ledger, "roster_problems", None) or []),
239
+ "records_off_roster": {},
240
+ "records_after_retirement": {},
241
+ "verifications_outside_project_roster": {},
168
242
  "recheck": [],
169
243
  }
170
244
  # measured history, computed from the ledger alone: counts, intervals, and the word
@@ -173,6 +247,12 @@ def report(ledger, recheck: bool = False, cwd: Optional[str] = None, timeout: Op
173
247
  rep["reliability"] = measured["table"]
174
248
  rep["same_lineage_agreement"] = measured["same_lineage_agreement"]
175
249
  rep["staleness"] = measured["staleness"]
250
+ # roadmap 8.5.1: a project whose pinned sections are over config.compile_pinned_chars:
251
+ # the board shows every reader fewer contracts than are open, and the constraints, never
252
+ # cut, make the view longer than the cap. The audit names both, so a person consolidates
253
+ # the constraints, closes work, or raises the cap
254
+ from .compiler import pinned_over_cap # local: the compiler imports the trust layer too
255
+ rep["pinned_over_cap"] = pinned_over_cap(ledger, assessment, reader_user)
176
256
  rep["judgment_calibration"] = measured["judgment_calibration"]
177
257
  for c in claims:
178
258
  a = c["author"]
@@ -337,6 +417,7 @@ def report(ledger, recheck: bool = False, cwd: Optional[str] = None, timeout: Op
337
417
  # a ledger that never registered a key is not a ledger where everybody is
338
418
  # unregistered: there is no registry to be outside of
339
419
  rep["verifications_by_unregistered_identities"]["no_registry"] = True
420
+ _roster_sections(ledger, rep, by_id, reader_user)
340
421
  if recheck:
341
422
  for c in claims:
342
423
  info = assessment[c["id"]]
@@ -367,9 +448,8 @@ def format_report(rep: dict) -> str:
367
448
  lines.append(f"labels: unsigned by a registered signer={unsigned.get('total', 0)}, "
368
449
  f"verifications by an unregistered identity={unregistered.get('total', 0)}")
369
450
  if not rep.get("trusted", True):
370
- lines.append("repo: UNTRUSTED -- config.json's policy keys (recheck_allow, "
371
- "recheck_allow_shell, redact, path_map, org_path, signers, vendor_map, "
372
- "cross_vendor_required, stale_after_days, reliability, max_clock_skew_seconds) "
451
+ lines.append("repo: UNTRUSTED -- config.json's policy keys "
452
+ f"({', '.join(sorted(UNTRUSTED_POLICY))}) "
373
453
  "were ignored; run `agent-memory trust .` to honour them")
374
454
  if not rep.get("recheck_allow_set"):
375
455
  lines.append("note: config.recheck_allow is not set: the re-check lane is held to the starter "
@@ -383,7 +463,7 @@ def format_report(rep: dict) -> str:
383
463
  lines.append("")
384
464
  lines.append(f"## {title} ({len(rows)})")
385
465
  for r in rows[:50]:
386
- lines.append("- " + fmt(r))
466
+ lines.append("- " + _one_line(fmt(r)))
387
467
  if len(rows) > 50:
388
468
  lines.append(f"- ... {len(rows) - 50} more")
389
469
 
@@ -396,7 +476,7 @@ def format_report(rep: dict) -> str:
396
476
  if note:
397
477
  lines.append(f"- {note}")
398
478
  for r in items:
399
- lines.append("- " + fmt(r))
479
+ lines.append("- " + _one_line(fmt(r)))
400
480
  if sec.get("total", 0) > len(items):
401
481
  lines.append(f"- ... {sec['total'] - len(items)} more")
402
482
 
@@ -423,6 +503,12 @@ def format_report(rep: dict) -> str:
423
503
  section("Taint chains", rep["taint_chains"], lambda r: f"{r['root']} -> " + ", ".join(t['id'] for t in r['tainted']))
424
504
  section("Stale", rep["stale"], lambda r: f"{r['id']} {r['statement']} -- {'; '.join(r['reasons'])}")
425
505
  section("Facts with no evidence (asserted)", rep["asserted_facts"], lambda r: f"{r['id']} [{r['by']}] {r['statement']}")
506
+ section("Pinned sections over config.compile_pinned_chars",
507
+ rep.get("pinned_over_cap", []),
508
+ lambda r: (f"{r['project']}{' (' + r['reader'] + ')' if r.get('reader') else ''}: {r['section']} "
509
+ f"takes {r['chars']} characters against a cap of {r['cap']}; "
510
+ + ("kept whole in every view (constraints are never cut): consolidate them"
511
+ if r.get("kept_whole") else f"left out: {', '.join(r['left_out'])}")))
426
512
  section("Contracts", rep["contracts"], lambda r: f"{r['id']} {r['status']}: {r['title']} ({r['claims']} claims {r['tiers']})")
427
513
  section("Compilation receipts", rep["compilations"],
428
514
  lambda r: f"{r['id']} {r['ts']} {r['role']}/{r['mode']} for {r['reader']}: {r['included']} in, {r['excluded']} out, {r['tokens']} tok")
@@ -454,6 +540,24 @@ def format_report(rep: dict) -> str:
454
540
  f"{r['verdict']} at {r['ts']}",
455
541
  note=("config.signers is empty: this ledger registered no key, so no identity in it "
456
542
  "can be checked" if unregistered.get("no_registry") else None))
543
+ section("Roster entries set aside (fix them in config.json)", rep.get("roster_problems") or [], str)
544
+ no_roster = ("config.agents is empty: this ledger has no roster, so no identity in it is on or off one "
545
+ "(docs/PROTOCOL.md section 12)")
546
+ off = rep.get("records_off_roster") or {}
547
+ capped("Records by identities not on the roster", off,
548
+ lambda r: f"{r['id']} [{r['kind']}] {r['user']} via {r['harness']} at {r['ts']}",
549
+ note=no_roster if off.get("no_roster") else ROSTER_OUTSIDERS)
550
+ late = rep.get("records_after_retirement") or {}
551
+ capped("Records filed after their author retired", late,
552
+ lambda r: f"{r['id']} [{r['kind']}] {r['user']} (retired {r['retired']}) at {r['ts']}",
553
+ note=no_roster if late.get("no_roster") else None)
554
+ outside = rep.get("verifications_outside_project_roster") or {}
555
+ capped("Verifications by an identity outside the claim's project roster", outside,
556
+ lambda r: f"{r['id']} over {r['claim']} (project {r['project']}) [{r['user']} via {r['harness']}] "
557
+ f"{r['verdict']} at {r['ts']}",
558
+ note=(no_roster if outside.get("no_roster") else
559
+ "no roster entry names a project, so no verification is outside one"
560
+ if outside.get("no_projects") else ROSTER_OUTSIDERS))
457
561
  lines.append("")
458
562
  lines.append(f"## Verifier lineage (refutation rates) ({len(rep['lineage_matrix'])})")
459
563
  for key, cell in sorted(rep["lineage_matrix"].items()):
@@ -476,4 +580,6 @@ def format_report(rep: dict) -> str:
476
580
  lambda r: f"{r['claim']} {r['evidence']}: {r['outcome']} -- {r['detail']}"
477
581
  + (" [environment binding: staleness, not refutation]" if r.get("role") == "environment" else "")
478
582
  + (f" [{r['note']}]" if r.get("note") else ""))
479
- return "\n".join(lines) + "\n"
583
+ # every line the report prints stays one line, whatever a record carried into it (the
584
+ # authors line, a harness, a user): one flatten here covers every section, old and new
585
+ return "\n".join(_one_line(line) for line in lines) + "\n"