strata-agent-memory 0.2.2__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.
- {strata_agent_memory-0.2.2/strata_agent_memory.egg-info → strata_agent_memory-0.2.4}/PKG-INFO +2 -1
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/README.md +1 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/__init__.py +1 -1
- strata_agent_memory-0.2.4/agent_memory/approvals.py +460 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/audit.py +113 -7
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/cli.py +554 -42
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/compiler.py +194 -11
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/contracts.py +266 -77
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/evidence.py +601 -31
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/export.py +2 -1
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/hooks.py +72 -20
- strata_agent_memory-0.2.4/agent_memory/instances.py +134 -0
- strata_agent_memory-0.2.4/agent_memory/kit.py +256 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/ledger.py +470 -69
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/mcp_server.py +55 -4
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/pathmap.py +78 -5
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/recheck.py +3 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/relay/adapters.py +104 -19
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/relay/cli.py +90 -16
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/relay/client.py +439 -25
- strata_agent_memory-0.2.4/agent_memory/relay/core.py +1531 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/reliability.py +4 -4
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/remote.py +155 -17
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/render.py +114 -22
- strata_agent_memory-0.2.4/agent_memory/roster.py +277 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/serve.py +258 -18
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/signing.py +14 -4
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/trust.py +15 -7
- strata_agent_memory-0.2.4/deploy/mac/README.md +284 -0
- strata_agent_memory-0.2.4/deploy/mac/agent-keys.sh +279 -0
- strata_agent_memory-0.2.4/deploy/mac/agent-session.sh +398 -0
- strata_agent_memory-0.2.4/deploy/mac/backup-tokens.sh +136 -0
- strata_agent_memory-0.2.4/deploy/mac/install-hooks-mac.sh +301 -0
- strata_agent_memory-0.2.4/deploy/mac/install-relay-driver.sh +367 -0
- strata_agent_memory-0.2.4/deploy/mac/install-replica-driver.sh +575 -0
- strata_agent_memory-0.2.4/deploy/mac/install-runner-driver.sh +224 -0
- strata_agent_memory-0.2.4/deploy/mac/instances.example.json +27 -0
- strata_agent_memory-0.2.4/deploy/mac/onboard-agent.sh +601 -0
- strata_agent_memory-0.2.4/deploy/mac/relay-agent.sh +358 -0
- strata_agent_memory-0.2.4/deploy/mac/seed-board.sh +256 -0
- strata_agent_memory-0.2.4/deploy/mac/seed-mail-context.sh +212 -0
- strata_agent_memory-0.2.4/deploy/mac/seed-topics.sh +182 -0
- strata_agent_memory-0.2.4/deploy/mac/upgrade-driver.sh +319 -0
- strata_agent_memory-0.2.4/deploy/plugins/.claude-plugin/marketplace.json +12 -0
- strata_agent_memory-0.2.4/deploy/plugins/README.md +112 -0
- strata_agent_memory-0.2.4/deploy/plugins/claude-code/.claude-plugin/plugin.json +8 -0
- strata_agent_memory-0.2.4/deploy/plugins/claude-code/bin/agent-memory-approvals +235 -0
- strata_agent_memory-0.2.4/deploy/plugins/claude-code/hooks/hooks.json +16 -0
- strata_agent_memory-0.2.4/deploy/plugins/claude-code/rules/approvals.md +6 -0
- strata_agent_memory-0.2.4/deploy/plugins/claude-code/skills/approvals/SKILL.md +23 -0
- strata_agent_memory-0.2.4/deploy/plugins/claude-code/skills/approve/SKILL.md +25 -0
- strata_agent_memory-0.2.4/deploy/plugins/claude-code/skills/reject/SKILL.md +25 -0
- strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-answer/SKILL.md +22 -0
- strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-answer/agents/openai.yaml +6 -0
- strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-approvals/SKILL.md +20 -0
- strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-approvals/agents/openai.yaml +6 -0
- strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-approvals/rules/approvals.md +6 -0
- strata_agent_memory-0.2.4/deploy/plugins/codex/skills/agent-memory-approvals/scripts/agent-memory-approvals +235 -0
- strata_agent_memory-0.2.4/deploy/relay/README.md +68 -0
- strata_agent_memory-0.2.4/deploy/relay/agent-memory-relay@.service +46 -0
- strata_agent_memory-0.2.4/deploy/relay/install-relay-mac.sh +429 -0
- strata_agent_memory-0.2.4/deploy/relay/install-relay.sh +592 -0
- strata_agent_memory-0.2.4/deploy/relay/macos/com.strata.agent-memory.relay.plist.template +72 -0
- strata_agent_memory-0.2.4/deploy/relay/relay-launch.sh +98 -0
- strata_agent_memory-0.2.4/pyproject.toml +76 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4/strata_agent_memory.egg-info}/PKG-INFO +2 -1
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/strata_agent_memory.egg-info/SOURCES.txt +47 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_audit.py +118 -1
- strata_agent_memory-0.2.4/tests/test_backlog_check.py +221 -0
- strata_agent_memory-0.2.4/tests/test_changelog_fragments.py +138 -0
- strata_agent_memory-0.2.4/tests/test_cli.py +526 -0
- strata_agent_memory-0.2.4/tests/test_compiler.py +187 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_compiler_render.py +397 -2
- strata_agent_memory-0.2.4/tests/test_config.py +296 -0
- strata_agent_memory-0.2.4/tests/test_contracts.py +1047 -0
- strata_agent_memory-0.2.4/tests/test_deploy_mac.py +3294 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_deploy_relay.py +86 -2
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_deploy_tokens.py +52 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_evidence.py +407 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_hooks.py +192 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_issue_bridge.py +1 -2
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_ledger.py +90 -2
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_packaging.py +42 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_pathmap.py +78 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_phase2.py +83 -2
- strata_agent_memory-0.2.4/tests/test_plugins.py +437 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_records.py +39 -0
- strata_agent_memory-0.2.4/tests/test_relay_adapters.py +316 -0
- strata_agent_memory-0.2.4/tests/test_relay_client.py +2742 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_relay_core.py +673 -9
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_remote.py +644 -14
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_review_fixes.py +18 -13
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_serve.py +557 -21
- strata_agent_memory-0.2.4/tests/test_trust.py +826 -0
- strata_agent_memory-0.2.2/agent_memory/relay/core.py +0 -821
- strata_agent_memory-0.2.2/pyproject.toml +0 -47
- strata_agent_memory-0.2.2/tests/test_cli.py +0 -223
- strata_agent_memory-0.2.2/tests/test_contracts.py +0 -511
- strata_agent_memory-0.2.2/tests/test_relay_client.py +0 -1420
- strata_agent_memory-0.2.2/tests/test_trust.py +0 -405
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/LICENSE +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/__main__.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/compact.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/identity.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/memorizer.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/records.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/relay/__init__.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/agent_memory/stats.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/setup.cfg +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/strata_agent_memory.egg-info/dependency_links.txt +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/strata_agent_memory.egg-info/entry_points.txt +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/strata_agent_memory.egg-info/requires.txt +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/strata_agent_memory.egg-info/top_level.txt +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_compact.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_deploy_replica.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_identity.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_ledger_index.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_memorizer.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_recheck.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_redaction.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_relay_codex.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_reliability.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_remote_signing.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_review2_fixes.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_review3_fixes.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_scale.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_signing.py +0 -0
- {strata_agent_memory-0.2.2 → strata_agent_memory-0.2.4}/tests/test_stats.py +0 -0
{strata_agent_memory-0.2.2/strata_agent_memory.egg-info → strata_agent_memory-0.2.4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: strata-agent-memory
|
|
3
|
-
Version: 0.2.
|
|
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.
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""The human-only acts on a ledger, one decision each (docs/PROTOCOL.md section 10).
|
|
2
|
+
|
|
3
|
+
Agents verify and a PM closes (section 4.1). What is left for a person is small and
|
|
4
|
+
should cost a person one read and one word each: ``pending`` lists it, and ``answer``
|
|
5
|
+
files the person's record for it. An item is one of
|
|
6
|
+
|
|
7
|
+
close a submitted contract that is verified by derivation and that no PM may
|
|
8
|
+
close (``pm_may_close`` off, or ``config.pm`` names nobody). A gate.
|
|
9
|
+
pm-close a close a PM filed. Information, not a gate: the close stands; the
|
|
10
|
+
person acknowledges it, or objects in a note.
|
|
11
|
+
retraction a note tagged ``approval:retraction`` asking that a claim be withdrawn:
|
|
12
|
+
the claim is the note's ``derived_from`` (or the first claim id its
|
|
13
|
+
statement names), the reason is the note.
|
|
14
|
+
reopen a note tagged ``approval:reopen --contract <id>``: back to ``accepted``,
|
|
15
|
+
or to the status a ``status:<s>`` tag names.
|
|
16
|
+
reject a note tagged ``approval:reject --contract <id>``: the work will not be
|
|
17
|
+
done.
|
|
18
|
+
constraint a note tagged ``approval:constraint``: its statement becomes a
|
|
19
|
+
human-filed constraint.
|
|
20
|
+
needs-human a relay's breaker stopped a thread (section 11); the person's answer is
|
|
21
|
+
a human record on that thread, which is what releases it.
|
|
22
|
+
|
|
23
|
+
``approval_mode`` decides who is asked for what. human (the default): all of the above.
|
|
24
|
+
auto: the agents close among themselves, so only constraints and retractions -- the acts
|
|
25
|
+
that change what every reader is told is true -- reach a person, and a stopped thread,
|
|
26
|
+
which is an alarm rather than an approval.
|
|
27
|
+
|
|
28
|
+
A person's answer carries the tags ``approval:approved`` or ``approval:rejected`` and
|
|
29
|
+
``answers:<item id>``, and an item with such an answer from a human is no longer
|
|
30
|
+
pending. Every item carries five lines -- the title and the PR, what was done, the
|
|
31
|
+
evidence (receipt, who re-ran it, the result), the review verdict with its record id, the
|
|
32
|
+
risk -- and the expected answer: approve, reject or hold.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import re
|
|
37
|
+
from typing import Iterable, List, Optional
|
|
38
|
+
|
|
39
|
+
from . import contracts
|
|
40
|
+
from .relay.core import agent_name
|
|
41
|
+
from .records import ValidationError, new_record, scope_for_target, sort_key
|
|
42
|
+
|
|
43
|
+
GATE_KINDS = ("close", "retraction", "reopen", "reject", "constraint", "needs-human")
|
|
44
|
+
PROPOSAL_KINDS = ("retraction", "reopen", "reject", "constraint")
|
|
45
|
+
AUTO_KINDS = ("retraction", "constraint")
|
|
46
|
+
APPROVED, REJECTED = "approval:approved", "approval:rejected"
|
|
47
|
+
PR_URL = re.compile(r"https://github\.com/[\w.-]+/[\w.-]+/pull/(\d+)")
|
|
48
|
+
PR_REF = re.compile(r"\bPR\s*#?(\d+)\b", re.I)
|
|
49
|
+
REOPEN_STATUSES = ("proposed", "accepted", "in_progress")
|
|
50
|
+
LINE = 160 # a summary line a person reads at a glance
|
|
51
|
+
# Every summary is text an agent wrote, shown to the person whose answer it asks for: C0
|
|
52
|
+
# and C1 controls (ESC, CSI) would let it move the cursor and rewrite what the person
|
|
53
|
+
# reads, and bidirectional overrides would reorder it. They become spaces.
|
|
54
|
+
CONTROL = re.compile("[\x00-\x1f\x7f-\x9f\u200b-\u200f\u202a-\u202e\u2066-\u2069]")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _plain(text) -> str:
|
|
58
|
+
return " ".join(CONTROL.sub(" ", str(text or "")).split())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _short(text: Optional[str], limit: int = LINE) -> str:
|
|
62
|
+
text = _plain(text)
|
|
63
|
+
return text if len(text) <= limit else text[:limit - 1].rstrip() + "…"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _first_sentence(text: Optional[str]) -> str:
|
|
67
|
+
text = _plain(text)
|
|
68
|
+
match = re.search(r"[.;:](\s|$)", text)
|
|
69
|
+
return _short(text[:match.start() + 1] if match and match.start() > 20 else text)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _tags(rec: dict) -> List[str]:
|
|
73
|
+
return [t for t in (rec.get("tags") or []) if isinstance(t, str)]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _human(rec: dict) -> bool:
|
|
77
|
+
return (rec.get("author") or {}).get("kind") == "human"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _who(author: dict) -> str:
|
|
81
|
+
return f"{author.get('harness') or author.get('kind') or '?'}/{author.get('user') or '?'}"
|
|
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
|
+
|
|
95
|
+
def _answered(ledger) -> tuple:
|
|
96
|
+
"""({item id: the newest human answer's ts}, {thread: the newest human record's ts}).
|
|
97
|
+
|
|
98
|
+
An answer is explicit: a human record tagged ``answers:<id>``; an agent's record
|
|
99
|
+
carrying the tag answers nothing. A person who replied on a stopped thread some other
|
|
100
|
+
way has answered it too -- that reply is what released it (section 11) -- so the
|
|
101
|
+
newest human ``thread:`` record per thread is kept as well. Nothing else counts: a
|
|
102
|
+
human's unrelated update on a contract is not an answer to whether to close it.
|
|
103
|
+
"""
|
|
104
|
+
ids, threads = {}, {}
|
|
105
|
+
for rec in ledger.all():
|
|
106
|
+
if not _human(rec):
|
|
107
|
+
continue
|
|
108
|
+
ts = rec.get("ts") or ""
|
|
109
|
+
for tag in _tags(rec):
|
|
110
|
+
if tag.startswith("answers:"):
|
|
111
|
+
key = tag[len("answers:"):]
|
|
112
|
+
ids[key] = max(ids.get(key, ""), ts)
|
|
113
|
+
elif tag.startswith("thread:"):
|
|
114
|
+
thread = tag[len("thread:"):]
|
|
115
|
+
threads[thread] = max(threads.get(thread, ""), ts)
|
|
116
|
+
return ids, threads
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _evidence_moved(ledger, contract_id: str, basis: dict) -> str:
|
|
120
|
+
"""The newest ts among the records a close rests on: the latest acceptance run, the
|
|
121
|
+
standing outcome claims, their verifications, the contract's transitions. A person's
|
|
122
|
+
answer older than this was an answer to other evidence, so the item is asked again."""
|
|
123
|
+
stamps = [u.get("ts") or "" for u in ledger.contract_updates(contract_id)]
|
|
124
|
+
for cid in basis.get("outcomes") or []:
|
|
125
|
+
stamps.append((ledger.get(cid) or {}).get("ts") or "")
|
|
126
|
+
stamps += [v.get("ts") or "" for v in ledger.verifications_for(cid)]
|
|
127
|
+
return max(stamps, default="")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# --------------------------------------------------------------------------- the lines
|
|
131
|
+
|
|
132
|
+
def _pr(ledger, contract_id: Optional[str]) -> Optional[str]:
|
|
133
|
+
"""The newest PR a contract's own records name: its updates' notes, then its claims."""
|
|
134
|
+
if not contract_id:
|
|
135
|
+
return None
|
|
136
|
+
texts = [(u["ts"], u["body"].get("note") or "") for u in ledger.contract_updates(contract_id)]
|
|
137
|
+
texts += [(c["ts"], c["body"].get("statement") or "") for c in ledger.claims_for_contract(contract_id)]
|
|
138
|
+
for _, text in sorted(texts, reverse=True):
|
|
139
|
+
url = PR_URL.search(text)
|
|
140
|
+
if url:
|
|
141
|
+
return f"PR #{url.group(1)} ({url.group(0)})"
|
|
142
|
+
ref = PR_REF.search(text)
|
|
143
|
+
if ref:
|
|
144
|
+
return f"PR #{ref.group(1)}"
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _contract_notes(ledger, contract_id: Optional[str], tag: str) -> List[dict]:
|
|
149
|
+
"""Claims about a contract carrying ``tag`` (or a ``tag:...`` form), newest first."""
|
|
150
|
+
if not contract_id:
|
|
151
|
+
return []
|
|
152
|
+
found = [c for c in ledger.claims_for_contract(contract_id)
|
|
153
|
+
if any(t == tag or t.startswith(tag + ":") for t in _tags(c))
|
|
154
|
+
and not ledger.retractions_for(c["id"])]
|
|
155
|
+
return sorted(found, key=sort_key, reverse=True)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _review(ledger, contract_id: Optional[str]) -> dict:
|
|
159
|
+
"""The newest review note on the contract: tag ``review``, verdict from a ``verdict:``
|
|
160
|
+
tag (approve, approve-with-nits, changes, reject) or the review's first words."""
|
|
161
|
+
notes = _contract_notes(ledger, contract_id, "review")
|
|
162
|
+
if not notes:
|
|
163
|
+
return {"verdict": None, "id": None, "line": "review: none on file"}
|
|
164
|
+
note = notes[0]
|
|
165
|
+
verdict = next((t[len("verdict:"):] for t in _tags(note) if t.startswith("verdict:")), None)
|
|
166
|
+
shown = verdict or _short(note["body"].get("statement"), 60)
|
|
167
|
+
return {"verdict": verdict, "id": note["id"],
|
|
168
|
+
"line": f"review: {shown} ({note['id']}, {_who(note['author'])})"}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _risk(ledger, contract_id: Optional[str]) -> dict:
|
|
172
|
+
"""The newest open risk or finding filed against the contract."""
|
|
173
|
+
notes = _contract_notes(ledger, contract_id, "risk") + _contract_notes(ledger, contract_id, "finding")
|
|
174
|
+
if not notes:
|
|
175
|
+
return {"id": None, "line": "risk: none recorded"}
|
|
176
|
+
note = sorted(notes, key=sort_key, reverse=True)[0]
|
|
177
|
+
return {"id": note["id"], "line": f"risk: {_short(note['body'].get('statement'), LINE - 20)} ({note['id']})"}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _evidence(ledger, basis: Optional[dict]) -> str:
|
|
181
|
+
"""Receipt, who re-ran it, the result -- for each outcome the contract rests on."""
|
|
182
|
+
if not basis or not basis.get("outcomes"):
|
|
183
|
+
return "evidence: none on file"
|
|
184
|
+
parts = []
|
|
185
|
+
for cid in basis["outcomes"]:
|
|
186
|
+
claim = ledger.get(cid) or {}
|
|
187
|
+
receipts = [e for e in (claim.get("body") or {}).get("evidence") or [] if isinstance(e, str)]
|
|
188
|
+
checks = [ledger.get(v) for v in basis["verifications"].get(cid, [])]
|
|
189
|
+
checks = [v for v in checks if v]
|
|
190
|
+
if checks:
|
|
191
|
+
v = checks[-1]
|
|
192
|
+
results = [r.get("outcome") for r in (v.get("body") or {}).get("results") or [] if isinstance(r, dict)]
|
|
193
|
+
parts.append(f"{', '.join(receipts) or cid} re-run by {_who(v['author'])} ({v['id']}): "
|
|
194
|
+
f"{(v.get('body') or {}).get('verdict')}{', ' + ', '.join(results) if results else ''}")
|
|
195
|
+
else:
|
|
196
|
+
parts.append(f"{', '.join(receipts) or cid}: not re-checked by another identity")
|
|
197
|
+
if basis.get("run"):
|
|
198
|
+
parts.append(f"acceptance {basis['run']}: " + ("passed" if basis.get("ok") else _short(basis.get("reason"), 80)))
|
|
199
|
+
return _short("evidence: " + "; ".join(parts), LINE * 2)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _expected_for_close(review: dict, risk: dict) -> str:
|
|
203
|
+
if review["verdict"] in ("changes", "reject", "request-changes", "blocked"):
|
|
204
|
+
return "hold"
|
|
205
|
+
if review["id"] is None or risk["id"] is not None:
|
|
206
|
+
return "hold"
|
|
207
|
+
return "approve"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _item(ledger, kind: str, rid: str, contract_id: Optional[str], title: str, done: str, evidence: str,
|
|
211
|
+
review: dict, risk: dict, expected: str, gate: bool, **extra) -> dict:
|
|
212
|
+
pr = _pr(ledger, contract_id)
|
|
213
|
+
# every field an agent wrote is cleaned here, in the item GET /approvals, memory_approvals
|
|
214
|
+
# and --json hand on, not only in the summary a person reads
|
|
215
|
+
title, done, evidence = _plain(title), _plain(done), _plain(evidence)
|
|
216
|
+
review_line, risk_line = _plain(review["line"]), _plain(risk["line"])
|
|
217
|
+
extra = {key: (_plain(value) if isinstance(value, str) else value) for key, value in extra.items()}
|
|
218
|
+
lines = [f"{title} -- {pr or 'no PR named'}", done, evidence, review_line, risk_line]
|
|
219
|
+
return {"id": rid, "kind": kind, "gate": gate, "contract": contract_id, "title": title, "pr": pr,
|
|
220
|
+
"done": done, "evidence": evidence, "review": review_line, "review_id": review["id"],
|
|
221
|
+
"risk": risk_line, "risk_id": risk["id"], "expected": expected,
|
|
222
|
+
"summary": [_short(line, LINE * 2) for line in lines], **extra}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# --------------------------------------------------------------------------- the list
|
|
226
|
+
|
|
227
|
+
def pending(ledger, assessment=None, reader: Optional[str] = None) -> dict:
|
|
228
|
+
"""What waits for a person on this ledger, with the policy that put it there.
|
|
229
|
+
|
|
230
|
+
``assessment`` is the trust assessment to read tiers from (computed once when
|
|
231
|
+
omitted). ``reader`` is the user asking: a user-scoped proposal is listed only to
|
|
232
|
+
its owner, as every other user-scoped record is.
|
|
233
|
+
"""
|
|
234
|
+
config = ledger.config
|
|
235
|
+
mode = contracts.approval_mode(config)
|
|
236
|
+
tiers = assessment if assessment is not None else contracts._Lazy(ledger)
|
|
237
|
+
answered, released = _answered(ledger)
|
|
238
|
+
items: List[dict] = []
|
|
239
|
+
|
|
240
|
+
def visible(rec: dict) -> bool:
|
|
241
|
+
scope = rec.get("scope") or {}
|
|
242
|
+
return scope.get("level") != "user" or (reader is not None and scope.get("user") == reader)
|
|
243
|
+
|
|
244
|
+
no_pm = not contracts.pm_may_close(config) or not contracts.pm_users(config)
|
|
245
|
+
for rec in sorted(ledger.by_kind("contract"), key=sort_key):
|
|
246
|
+
cid = rec["id"]
|
|
247
|
+
filed = contracts.filed_status(ledger, cid)
|
|
248
|
+
title = _short(rec["body"].get("title"), 100)
|
|
249
|
+
if mode == "human" and filed in ("submitted", "verified") and no_pm:
|
|
250
|
+
# a `verified` filed before the rule is history the old machine judged: its
|
|
251
|
+
# close is still a person's, and the lines show what it rests on today. A
|
|
252
|
+
# person who declined it is asked again once the evidence moves.
|
|
253
|
+
basis = contracts.verification_basis(ledger, cid, tiers)
|
|
254
|
+
fresh = cid not in answered or answered[cid] < _evidence_moved(ledger, cid, basis)
|
|
255
|
+
if fresh and (basis["ok"] or filed == "verified"):
|
|
256
|
+
outcome = (ledger.get(basis["outcomes"][0]) if basis["outcomes"] else None) or {}
|
|
257
|
+
review, risk = _review(ledger, cid), _risk(ledger, cid)
|
|
258
|
+
items.append(_item(ledger, "close", cid, cid, title,
|
|
259
|
+
_first_sentence((outcome.get("body") or {}).get("statement")),
|
|
260
|
+
_evidence(ledger, basis), review, risk,
|
|
261
|
+
_expected_for_close(review, risk), gate=True))
|
|
262
|
+
if mode == "human" and filed == "closed":
|
|
263
|
+
close = contracts.transitions(ledger, cid)[-1]
|
|
264
|
+
# a PM's close, listed so a person sees it; an agent that closed before there
|
|
265
|
+
# was a PM rule is history, not something to acknowledge now
|
|
266
|
+
if contracts.is_pm(config, close["author"]) and close["id"] not in answered:
|
|
267
|
+
review, risk = _review(ledger, cid), _risk(ledger, cid)
|
|
268
|
+
basis = contracts.verification_basis(ledger, cid, tiers)
|
|
269
|
+
items.append(_item(ledger, "pm-close", close["id"], cid, title,
|
|
270
|
+
_short(f"closed by {_who(close['author'])}: {close['body'].get('note') or ''}"),
|
|
271
|
+
_evidence(ledger, basis), review, risk, "approve", gate=False))
|
|
272
|
+
|
|
273
|
+
kinds = AUTO_KINDS if mode == "auto" else PROPOSAL_KINDS
|
|
274
|
+
for note in sorted(ledger.by_kind("claim"), key=sort_key):
|
|
275
|
+
if note["id"] in answered or not visible(note) or ledger.retractions_for(note["id"]):
|
|
276
|
+
continue
|
|
277
|
+
tags = _tags(note)
|
|
278
|
+
if "needs-human" in tags: # an alarm in either mode: auto removes approvals, not alarms
|
|
279
|
+
thread = next((t[len("thread:"):] for t in tags if t.startswith("thread:")), None)
|
|
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 {})
|
|
286
|
+
items.append(_item(ledger, "needs-human", note["id"], (note.get("body") or {}).get("contract"),
|
|
287
|
+
f"thread {thread}", _short(note["body"].get("statement")),
|
|
288
|
+
f"evidence: the last message on it is {', '.join(note.get('derived_from') or []) or '?'}",
|
|
289
|
+
{"verdict": None, "id": None, "line": "review: none (a breaker, not work)"},
|
|
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))
|
|
295
|
+
continue
|
|
296
|
+
kind = next((t[len("approval:"):] for t in tags if t.startswith("approval:")), None)
|
|
297
|
+
if kind not in kinds:
|
|
298
|
+
continue
|
|
299
|
+
item = _proposal(ledger, note, kind, tiers)
|
|
300
|
+
if item is not None:
|
|
301
|
+
items.append(item)
|
|
302
|
+
return {"approval_mode": mode, "pm_may_close": contracts.pm_may_close(config),
|
|
303
|
+
"pm": contracts.pm_users(config), "items": items,
|
|
304
|
+
"waiting": sum(1 for i in items if i["gate"]), "information": sum(1 for i in items if not i["gate"])}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _target_claim(ledger, note: dict) -> Optional[dict]:
|
|
308
|
+
"""The one claim a retraction proposal names: its derived_from when that names any
|
|
309
|
+
record, else the claim ids in its statement -- and only when exactly one claim is
|
|
310
|
+
named. A note that names two would otherwise have the person retract whichever came
|
|
311
|
+
first (review, story 1); it waits, saying why, until it names one."""
|
|
312
|
+
named = [rid for rid in (note.get("derived_from") or []) if rid != note["id"]]
|
|
313
|
+
if not named:
|
|
314
|
+
named = [rid for rid in contracts.CLAIM_ID.findall(note["body"].get("statement") or "") if rid != note["id"]]
|
|
315
|
+
named = list(dict.fromkeys(named))
|
|
316
|
+
if len(named) != 1:
|
|
317
|
+
return None
|
|
318
|
+
rec = ledger.get(named[0])
|
|
319
|
+
return rec if rec is not None and rec.get("kind") == "claim" else None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _proposal(ledger, note: dict, kind: str, tiers) -> Optional[dict]:
|
|
323
|
+
"""A PM's note asking for a human-only act, as an item -- or None once the act has
|
|
324
|
+
happened by some other road (the claim already withdrawn, the contract already there)."""
|
|
325
|
+
body = note.get("body") or {}
|
|
326
|
+
cid = body.get("contract")
|
|
327
|
+
title = _short((ledger.get(cid) or {}).get("body", {}).get("title"), 100) if cid else "(no contract)"
|
|
328
|
+
review, risk = _review(ledger, cid), _risk(ledger, cid)
|
|
329
|
+
asked = f"{_who(note['author'])} asks: {_short(body.get('statement'), LINE - 30)}"
|
|
330
|
+
if kind == "retraction":
|
|
331
|
+
target = _target_claim(ledger, note)
|
|
332
|
+
if target is None:
|
|
333
|
+
return _item(ledger, kind, note["id"], cid, title if cid else "retract (no single claim named)", asked,
|
|
334
|
+
"evidence: the note names no single claim to retract; name exactly one in derived_from",
|
|
335
|
+
review, risk, "reject", gate=True, target=None)
|
|
336
|
+
if ledger.retractions_for(target["id"]):
|
|
337
|
+
return None
|
|
338
|
+
tier = tiers.get(target["id"], {}).get("tier", "?")
|
|
339
|
+
evidence = f"evidence: {target['id']} by {_who(target['author'])} is {tier}"
|
|
340
|
+
return _item(ledger, kind, note["id"], cid, title if cid else f"retract {target['id']}", asked, evidence,
|
|
341
|
+
review, risk, "approve", gate=True, target=target["id"])
|
|
342
|
+
if kind in ("reopen", "reject"):
|
|
343
|
+
if not cid or ledger.get(cid) is None:
|
|
344
|
+
return None
|
|
345
|
+
filed = contracts.filed_status(ledger, cid)
|
|
346
|
+
if kind == "reject" and filed == "rejected":
|
|
347
|
+
return None
|
|
348
|
+
if kind == "reopen" and filed not in contracts.TERMINAL_STATUSES:
|
|
349
|
+
return None
|
|
350
|
+
status = "rejected" if kind == "reject" else next(
|
|
351
|
+
(t[len("status:"):] for t in _tags(note) if t.startswith("status:")
|
|
352
|
+
and t[len("status:"):] in REOPEN_STATUSES), "accepted")
|
|
353
|
+
return _item(ledger, kind, note["id"], cid, title, asked,
|
|
354
|
+
f"evidence: the contract is {filed}; this moves it to {status}",
|
|
355
|
+
review, risk, "approve", gate=True, to_status=status)
|
|
356
|
+
if kind == "constraint":
|
|
357
|
+
# the whole statement is what becomes the constraint, so the whole of it is shown:
|
|
358
|
+
# a summary cut at 160 characters would let a tail nobody read be approved
|
|
359
|
+
return _item(ledger, kind, note["id"], cid, title if cid else "a constraint for every reader", asked,
|
|
360
|
+
"evidence: a constraint is what a person says always applies; nothing to re-run",
|
|
361
|
+
review, risk, "approve", gate=True, text=_plain(body.get("statement")))
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# --------------------------------------------------------------------------- the answer
|
|
366
|
+
|
|
367
|
+
def answer(ledger, author: dict, scope: dict, item_id: str, verdict: str = "approve",
|
|
368
|
+
note: Optional[str] = None, assessment=None) -> List[dict]:
|
|
369
|
+
"""File a person's answer to one pending item; returns the records filed.
|
|
370
|
+
|
|
371
|
+
Refused for anything but a human identity: an agent may explain and recommend, and
|
|
372
|
+
the record this files is the one thing it may not. ``reject`` needs a note (the reason
|
|
373
|
+
is the record); ``approve`` takes one. Raises ValidationError with the refusal, or
|
|
374
|
+
KeyError when nothing is pending under that id.
|
|
375
|
+
"""
|
|
376
|
+
if author.get("kind") != "human":
|
|
377
|
+
raise ValidationError(
|
|
378
|
+
f"approving is a person's act: this identity is an agent's ({_who(author)}); file the answer under "
|
|
379
|
+
f"a human token (agent-memory approve --as-human locally, or a human's token over --remote)")
|
|
380
|
+
if verdict not in ("approve", "reject"):
|
|
381
|
+
raise ValidationError("an answer is approve or reject; hold is filing nothing")
|
|
382
|
+
note = (note or "").strip() or None
|
|
383
|
+
if verdict == "reject" and not note:
|
|
384
|
+
raise ValidationError("a rejection needs a note: the reason is the record")
|
|
385
|
+
# one assessment for the list and the close's basis: each would otherwise compute its own
|
|
386
|
+
assessment = assessment if assessment is not None else contracts._Lazy(ledger)
|
|
387
|
+
view = pending(ledger, assessment)
|
|
388
|
+
item = next((i for i in view["items"] if i["id"] == item_id), None)
|
|
389
|
+
if item is None:
|
|
390
|
+
raise KeyError(f"nothing pending under {item_id} (agent-memory approvals lists what is)")
|
|
391
|
+
tags = [APPROVED if verdict == "approve" else REJECTED, f"answers:{item_id}"]
|
|
392
|
+
said = f": {note}" if note else ""
|
|
393
|
+
if verdict == "reject" or item["kind"] == "pm-close":
|
|
394
|
+
return [_answer_note(ledger, author, scope, item, verdict, note, tags)]
|
|
395
|
+
kind = item["kind"]
|
|
396
|
+
if kind == "close":
|
|
397
|
+
basis = contracts.verification_basis(ledger, item["contract"], assessment)
|
|
398
|
+
relied = [f"{c} re-checked in {', '.join(basis['verifications'].get(c, [])) or '-'}" for c in basis["outcomes"]]
|
|
399
|
+
return [contracts.update(ledger, author, scope, item["contract"], "closed",
|
|
400
|
+
note=f"approved: {'; '.join(relied)}{said}", tags=tags)]
|
|
401
|
+
if kind in ("reopen", "reject"):
|
|
402
|
+
proposal = ledger.get(item_id)
|
|
403
|
+
reason = note or (proposal or {}).get("body", {}).get("statement")
|
|
404
|
+
return [contracts.update(ledger, author, scope, item["contract"], item["to_status"],
|
|
405
|
+
note=f"approved {item_id}: {_short(reason, 400)}", tags=tags)]
|
|
406
|
+
if kind == "retraction":
|
|
407
|
+
if not item.get("target"):
|
|
408
|
+
raise ValidationError(f"{item_id} names no single claim to retract: reject it, and ask for a note "
|
|
409
|
+
f"that names exactly one in derived_from")
|
|
410
|
+
proposal = ledger.get(item_id) or {}
|
|
411
|
+
target = ledger.get(item["target"])
|
|
412
|
+
reason = f"approved {item_id}: {proposal.get('body', {}).get('statement')}{said}"
|
|
413
|
+
rec = new_record("retraction", author, scope_for_target(target, scope),
|
|
414
|
+
{"claim": target["id"], "reason": _short(reason, 1900)},
|
|
415
|
+
derived_from=[target["id"]], tags=tags)
|
|
416
|
+
return [ledger.append(rec)]
|
|
417
|
+
if kind == "constraint":
|
|
418
|
+
proposal = ledger.get(item_id) or {}
|
|
419
|
+
body = {"type": "constraint", "statement": proposal["body"]["statement"]}
|
|
420
|
+
if proposal["body"].get("contract"):
|
|
421
|
+
body["contract"] = proposal["body"]["contract"]
|
|
422
|
+
return [ledger.append(new_record("claim", author, scope, body, derived_from=[item_id], tags=tags))]
|
|
423
|
+
if kind == "needs-human":
|
|
424
|
+
tags.append(f"thread:{item['thread']}")
|
|
425
|
+
body = {"type": "note", "statement": f"released thread {item['thread']}{said or ': read and answered'}"}
|
|
426
|
+
return [ledger.append(new_record("claim", author, scope, body, derived_from=[item_id], tags=tags))]
|
|
427
|
+
raise ValidationError(f"no answer is defined for a {kind} item")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _answer_note(ledger, author: dict, scope: dict, item: dict, verdict: str, note: Optional[str],
|
|
431
|
+
tags: Iterable[str]) -> dict:
|
|
432
|
+
"""The record for an acknowledgement or a rejection: a note, on the contract when the
|
|
433
|
+
item has one, deriving from the item when the item is a claim (rule 10)."""
|
|
434
|
+
word = "acknowledged" if verdict == "approve" else "declined"
|
|
435
|
+
body = {"type": "note", "statement": _short(f"{word} {item['kind']} {item['id']}{': ' + note if note else ''}", 1900)}
|
|
436
|
+
if item.get("contract"):
|
|
437
|
+
body["contract"] = item["contract"]
|
|
438
|
+
target = ledger.get(item["id"])
|
|
439
|
+
derived = [item["id"]] if target is not None and target.get("kind") == "claim" else []
|
|
440
|
+
return ledger.append(new_record("claim", author, scope, body, derived_from=derived, tags=list(tags)))
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def render(view: dict) -> str:
|
|
444
|
+
"""The text a person reads: a header, then five lines and the expected answer per item."""
|
|
445
|
+
lines = [f"approvals: approval_mode {view['approval_mode']}, pm_may_close "
|
|
446
|
+
f"{'on' if view['pm_may_close'] else 'off'}, PM {', '.join(view['pm']) or 'none'}; "
|
|
447
|
+
f"{view['waiting']} waiting, {view['information']} for information"]
|
|
448
|
+
for item in view["items"]:
|
|
449
|
+
lines.append("")
|
|
450
|
+
lines.append(f"[{item['kind']}] {item['id']}" + ("" if item["gate"] else " (information: the close stands)"))
|
|
451
|
+
for n, text in enumerate(item["summary"], 1):
|
|
452
|
+
lines.append(f" {n} {text}")
|
|
453
|
+
if item.get("text"):
|
|
454
|
+
lines.append(f" the constraint, in full: {item['text']}")
|
|
455
|
+
lines.append(f" -> {item['expected']}: agent-memory approve {item['id']} | "
|
|
456
|
+
f"agent-memory reject {item['id']} --note \"...\"")
|
|
457
|
+
if not view["items"]:
|
|
458
|
+
lines.append("nothing waits for a person")
|
|
459
|
+
# the view may come from a replica (GET /approvals): the text is cleaned here as well
|
|
460
|
+
return "\n".join(CONTROL.sub(" ", line) for line in lines)
|