strata-agent-memory 0.2.6__tar.gz → 0.2.8__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 (172) hide show
  1. {strata_agent_memory-0.2.6/strata_agent_memory.egg-info → strata_agent_memory-0.2.8}/PKG-INFO +1 -1
  2. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/__init__.py +1 -1
  3. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/cli.py +34 -10
  4. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/compiler.py +107 -29
  5. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/contracts.py +152 -7
  6. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/evidence.py +122 -14
  7. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/lane.py +255 -37
  8. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/ledger.py +6 -0
  9. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/mcp_server.py +19 -9
  10. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/recheck.py +88 -5
  11. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/records.py +20 -2
  12. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/remote.py +70 -6
  13. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/render.py +7 -1
  14. strata_agent_memory-0.2.8/agent_memory/retrieval.py +205 -0
  15. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/serve.py +229 -1
  16. strata_agent_memory-0.2.8/deploy/lib/served-ledger.sh +58 -0
  17. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/README.md +7 -1
  18. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/backup-tokens.sh +4 -2
  19. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/install-replica-driver.sh +104 -71
  20. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/onboard-agent.sh +9 -4
  21. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/seed-board.sh +3 -0
  22. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/seed-topics.sh +3 -0
  23. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/upgrade-driver.sh +34 -7
  24. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/pyproject.toml +17 -3
  25. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8/strata_agent_memory.egg-info}/PKG-INFO +1 -1
  26. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/strata_agent_memory.egg-info/SOURCES.txt +20 -0
  27. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_assessment_67b26eae.py +11 -0
  28. strata_agent_memory-0.2.8/tests/test_binding_refusal_97e36882.py +122 -0
  29. strata_agent_memory-0.2.8/tests/test_build_recheck_image_e4a93fd6.py +51 -0
  30. strata_agent_memory-0.2.8/tests/test_contract_amend_649e0b45.py +354 -0
  31. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_deploy_mac.py +19 -1
  32. strata_agent_memory-0.2.8/tests/test_install_lane_cb430a30.py +335 -0
  33. strata_agent_memory-0.2.8/tests/test_lane_derived_roots_817ab446.py +263 -0
  34. strata_agent_memory-0.2.8/tests/test_lane_nightly_84b22b63.py +203 -0
  35. strata_agent_memory-0.2.8/tests/test_lane_stop_63b7cf8b.py +159 -0
  36. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_ledger.py +1 -0
  37. strata_agent_memory-0.2.8/tests/test_memoryagentbench_60a8abfc.py +374 -0
  38. strata_agent_memory-0.2.8/tests/test_memsyco_7b16e8e4.py +553 -0
  39. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_packaging.py +5 -1
  40. strata_agent_memory-0.2.8/tests/test_proof_exclusions_e4a93fd6.py +80 -0
  41. strata_agent_memory-0.2.8/tests/test_recheck_away_207af145.py +276 -0
  42. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_recheck_sandbox_543e48a7.py +40 -6
  43. strata_agent_memory-0.2.8/tests/test_recheck_sandbox_nits_543e48a7.py +73 -0
  44. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_relay_client.py +1 -1
  45. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_relay_release_gate_b3b3e487.py +2 -2
  46. strata_agent_memory-0.2.8/tests/test_reply_cap_cfbd7681.py +229 -0
  47. strata_agent_memory-0.2.8/tests/test_retrieval_plugin_9e166c2f.py +364 -0
  48. strata_agent_memory-0.2.8/tests/test_rollback_78710ebf.py +510 -0
  49. strata_agent_memory-0.2.8/tests/test_sandbox_image_e4a93fd6.py +320 -0
  50. strata_agent_memory-0.2.8/tests/test_served_ledger_6e8e49f3.py +265 -0
  51. strata_agent_memory-0.2.8/tests/test_tools_clone_4b72f0c1.py +232 -0
  52. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/LICENSE +0 -0
  53. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/README.md +0 -0
  54. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/__main__.py +0 -0
  55. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/approvals.py +0 -0
  56. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/audit.py +0 -0
  57. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/compact.py +0 -0
  58. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/export.py +0 -0
  59. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/hooks.py +0 -0
  60. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/identity.py +0 -0
  61. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/instances.py +0 -0
  62. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/kit.py +0 -0
  63. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/measure.py +0 -0
  64. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/memorizer.py +0 -0
  65. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/pathmap.py +0 -0
  66. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/relay/__init__.py +0 -0
  67. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/relay/adapters.py +0 -0
  68. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/relay/cli.py +0 -0
  69. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/relay/client.py +0 -0
  70. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/relay/core.py +0 -0
  71. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/reliability.py +0 -0
  72. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/roster.py +0 -0
  73. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/signing.py +0 -0
  74. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/stats.py +0 -0
  75. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/supersession.py +0 -0
  76. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/trust.py +0 -0
  77. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/agent_memory/wake.py +0 -0
  78. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/agent-keys.sh +0 -0
  79. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/agent-session.sh +0 -0
  80. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/install-hooks-mac.sh +0 -0
  81. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/install-relay-driver.sh +0 -0
  82. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/install-runner-driver.sh +0 -0
  83. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/instances.example.json +0 -0
  84. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/relay-agent.sh +0 -0
  85. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/mac/seed-mail-context.sh +0 -0
  86. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/.claude-plugin/marketplace.json +0 -0
  87. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/README.md +0 -0
  88. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  89. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/bin/agent-memory-approvals +0 -0
  90. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/bin/agent-memory-wake +0 -0
  91. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/hooks/hooks.json +0 -0
  92. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/monitors/monitors.json +0 -0
  93. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/rules/approvals.md +0 -0
  94. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/skills/approvals/SKILL.md +0 -0
  95. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/skills/approve/SKILL.md +0 -0
  96. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/skills/inbox/SKILL.md +0 -0
  97. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/claude-code/skills/reject/SKILL.md +0 -0
  98. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-answer/SKILL.md +0 -0
  99. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-answer/agents/openai.yaml +0 -0
  100. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-approvals/SKILL.md +0 -0
  101. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-approvals/agents/openai.yaml +0 -0
  102. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-approvals/rules/approvals.md +0 -0
  103. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/plugins/codex/skills/agent-memory-approvals/scripts/agent-memory-approvals +0 -0
  104. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/relay/README.md +0 -0
  105. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/relay/agent-memory-relay@.service +0 -0
  106. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/relay/install-relay-mac.sh +0 -0
  107. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/relay/install-relay.sh +0 -0
  108. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/relay/macos/com.strata.agent-memory.relay.plist.template +0 -0
  109. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/deploy/relay/relay-launch.sh +0 -0
  110. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/setup.cfg +0 -0
  111. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/strata_agent_memory.egg-info/dependency_links.txt +0 -0
  112. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/strata_agent_memory.egg-info/entry_points.txt +0 -0
  113. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/strata_agent_memory.egg-info/requires.txt +0 -0
  114. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/strata_agent_memory.egg-info/top_level.txt +0 -0
  115. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_audit.py +0 -0
  116. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_audit_lane_numbers_ea89ba26.py +0 -0
  117. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_audit_measurement_13190a20.py +0 -0
  118. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_backlog_check.py +0 -0
  119. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_changelog_fragments.py +0 -0
  120. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_changelog_guard_shallow_57a2f78e.py +0 -0
  121. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_ci_docs_only_353ce010.py +0 -0
  122. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_cli.py +0 -0
  123. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_compact.py +0 -0
  124. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_compiler.py +0 -0
  125. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_compiler_render.py +0 -0
  126. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_config.py +0 -0
  127. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_contracts.py +0 -0
  128. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_deploy_relay.py +0 -0
  129. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_deploy_replica.py +0 -0
  130. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_deploy_tokens.py +0 -0
  131. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_evidence.py +0 -0
  132. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_find_root_b4b5019b.py +0 -0
  133. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_hooks.py +0 -0
  134. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_identity.py +0 -0
  135. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_issue_bridge.py +0 -0
  136. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_lane_63b7cf8b.py +0 -0
  137. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_lane_priority_779d51ba.py +0 -0
  138. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_ledger_index.py +0 -0
  139. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_live_wake_c217c336.py +0 -0
  140. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_local_lanes_sibling_1411a2b2.py +0 -0
  141. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_memorizer.py +0 -0
  142. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_outcome_commit_binding_b1de564d.py +0 -0
  143. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_outcome_needs_evidence_b1de564d.py +0 -0
  144. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_pathmap.py +0 -0
  145. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_phase2.py +0 -0
  146. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_plugins.py +0 -0
  147. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_receipt_reports_274e6199.py +0 -0
  148. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_recheck.py +0 -0
  149. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_recheck_remote_8185d2d3.py +0 -0
  150. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_records.py +0 -0
  151. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_redaction.py +0 -0
  152. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_relay_adapters.py +0 -0
  153. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_relay_codex.py +0 -0
  154. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_relay_core.py +0 -0
  155. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_relay_instance_2a2e3887.py +0 -0
  156. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_relay_release_526f5261.py +0 -0
  157. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_reliability.py +0 -0
  158. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_remote.py +0 -0
  159. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_remote_signing.py +0 -0
  160. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_review2_fixes.py +0 -0
  161. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_review3_fixes.py +0 -0
  162. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_review_fixes.py +0 -0
  163. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_scale.py +0 -0
  164. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_serve.py +0 -0
  165. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_signing.py +0 -0
  166. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_sit_fleet_1b24b0b1.py +0 -0
  167. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_sit_fleet_ci_0d9b76a2.py +0 -0
  168. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_sit_var_bindings_e802ca84.py +0 -0
  169. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_stats.py +0 -0
  170. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_supersedes_61416280.py +0 -0
  171. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_trust.py +0 -0
  172. {strata_agent_memory-0.2.6 → strata_agent_memory-0.2.8}/tests/test_uv_lock_env_274e6199.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strata-agent-memory
3
- Version: 0.2.6
3
+ Version: 0.2.8
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
@@ -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.6"
12
+ __version__ = "0.2.8"
@@ -606,9 +606,10 @@ def _private_contract_refusal(args) -> Optional[int]:
606
606
 
607
607
 
608
608
  def _acceptance_checks(args) -> List[dict]:
609
- """The acceptance checks a `contract create` asks for, in the order the flags read
610
- them. The remote lane files the same contract as the local one, so both build the
611
- list here: a check that got lost on the way is a contract nobody can prove."""
609
+ """The acceptance checks a `contract create` asks for, or a `contract update` amends the
610
+ list to (ctr_649e0b459a758929), in the order the flags read them. The remote lane files
611
+ the same record as the local one, so both build the list here: a check that got lost on
612
+ the way is a contract nobody can prove."""
612
613
  acceptance = [{"kind": "command", "cmd": c, "expect_exit": 0} for c in args.check or []]
613
614
  acceptance += [{"kind": "human", "statement": s} for s in args.human_check or []]
614
615
  acceptance += [{"kind": "file", "path": f} for f in args.file_check or []]
@@ -637,8 +638,13 @@ def cmd_contract(args) -> int:
637
638
  _print(out)
638
639
  return 0 if signed else 3
639
640
  if args.action == "update":
640
- rec = contracts.update(ledger, author, scope, args.id, args.status, note=args.note, claims=args.claims or (),
641
- assigned_to=getattr(args, "assign", None))
641
+ try:
642
+ rec = contracts.update(ledger, author, scope, args.id, args.status, note=args.note, claims=args.claims or (),
643
+ assigned_to=getattr(args, "assign", None),
644
+ acceptance_checks=_acceptance_checks(args) or None)
645
+ except ValidationError as exc:
646
+ print(f"rejected: {exc}", file=sys.stderr)
647
+ return 3
642
648
  out = {"id": rec["id"], "status": args.status}
643
649
  signed = _sign_after(args, ledger, author, scope, rec, out)
644
650
  _print(out)
@@ -1479,7 +1485,10 @@ def _remote_recheck(args, client) -> int:
1479
1485
  max=args.max, cwd=root, timeout=args.timeout, path_map=path_map,
1480
1486
  dry_run=args.dry_run, min_age_days=args.min_age_days, force=args.force,
1481
1487
  assessment=view.assessment, file=file,
1482
- policy=_sandbox_policy(args, _local_sandbox(view.config, root)))
1488
+ policy=_sandbox_policy(args, _local_sandbox(view.config, root)),
1489
+ # a re-run away from the outcome's bound commit refutes nothing
1490
+ # (the lane's rule, ctr_207af145a59463ed)
1491
+ away_is_error=True)
1483
1492
  except ValueError as exc:
1484
1493
  print(f"recheck: {exc}", file=sys.stderr)
1485
1494
  return 2
@@ -1679,7 +1688,8 @@ def _remote_contract_update(args, client) -> int:
1679
1688
  return 2
1680
1689
  claims = list(args.claims or [])
1681
1690
  body = contracts.update_body(args.id, args.status, note=args.note, claims=claims,
1682
- assigned_to=getattr(args, "assign", None))
1691
+ assigned_to=getattr(args, "assign", None),
1692
+ acceptance_checks=_acceptance_checks(args) or None)
1683
1693
  payload = {"kind": "contract_update", "body": body, "derived_from": [args.id, *claims]}
1684
1694
  rec, _ = client.post_record(_scoped_payload(payload, _remote_scope(args, client)))
1685
1695
  out = {"id": rec["id"], "status": rec["body"]["status"]}
@@ -1704,9 +1714,16 @@ def _remote_contract_accept(args, client) -> int:
1704
1714
  if local is None:
1705
1715
  return 2
1706
1716
  served = _ServedLedger(client, local)
1717
+ # read once, before anything runs: the checks the replica says the contract answers to
1718
+ # now, an amendment's included (the served view holds no contract_updates to find one
1719
+ # in, ctr_649e0b459a758929), and the status the run reports it started under, which
1720
+ # ``contracts.run_acceptance`` reads before its checks run too (PROTOCOL 4.1)
1721
+ started = client.contract_status(args.id)
1722
+ checks = started.get("acceptance")
1707
1723
  results = contracts.run_acceptance(served, args.id, cwd=args.cwd or str(local.root),
1708
1724
  timeout=args.timeout, file=False,
1709
- policy=_sandbox_policy(args, served.config))
1725
+ policy=_sandbox_policy(args, served.config),
1726
+ checks=checks if isinstance(checks, list) else None)
1710
1727
  # asked once and used for the receipts and the run alike: a --scope this client typed
1711
1728
  # has to reach the server the way `claim` and `retract` send it, and a project scope
1712
1729
  # is still the server's own to stamp
@@ -1716,7 +1733,7 @@ def _remote_contract_accept(args, client) -> int:
1716
1733
  if body and body.get("kind") in ("command", "test"):
1717
1734
  rec, _ = client.post_record(_scoped_payload({"kind": "evidence", "body": body}, scope))
1718
1735
  r["receipt"] = rec["id"]
1719
- status = client.contract_status(args.id)["status"] # an acceptance run reports, it does not promote
1736
+ status = started["status"] # an acceptance run reports, it does not promote
1720
1737
  client.post_record(_scoped_payload({"kind": "contract_update", "body": {
1721
1738
  "contract": args.id, "status": status,
1722
1739
  "acceptance": [{"check": r["check"], "outcome": r["outcome"], "detail": r["detail"],
@@ -2349,6 +2366,12 @@ def build_parser() -> argparse.ArgumentParser:
2349
2366
  c.add_argument("--note")
2350
2367
  c.add_argument("--claims", nargs="*")
2351
2368
  c.add_argument("--assign", help="hand the contract to this agent (the board follows the latest assignee)")
2369
+ # an amendment of the acceptance checks: the whole list, as `contract create` takes it
2370
+ # (ctr_649e0b459a758929); refused under the contract's assignee's own key
2371
+ c.add_argument("--check", action="append", help="amend the acceptance: a command expecting exit 0 (repeatable)")
2372
+ c.add_argument("--human-check", action="append", help="amend the acceptance: a human sign-off (repeatable)")
2373
+ c.add_argument("--file-check", action="append", help="amend the acceptance: a file that must exist (repeatable)")
2374
+ c.add_argument("--acceptance-json", help="amend the acceptance: a JSON list of checks, replacing the contract's")
2352
2375
  _add_identity(c)
2353
2376
  _add_signing(c)
2354
2377
  c = cs.add_parser("from-issue", help="create a contract from a GitHub issue (needs the gh CLI)")
@@ -2419,7 +2442,8 @@ def build_parser() -> argparse.ArgumentParser:
2419
2442
  "verifications under the running identity; with --remote, the "
2420
2443
  "replica's claims, re-run here under the replica's re-check policy")
2421
2444
  s.add_argument("--only", choices=list(recheck_mod.ONLY_CHOICES), default="unverified",
2422
- help="unverified (tier evidenced, default), stale, or all (everything but refuted/tainted)")
2445
+ help="unverified (tier evidenced or self-verified, whatever inconclusives it carries; the "
2446
+ "default), stale, or all (everything but refuted/tainted)")
2423
2447
  s.add_argument("--type", action="append",
2424
2448
  help="restrict to these claim types, comma-separated (e.g. outcome,fact)")
2425
2449
  s.add_argument("--max", type=int, help="stop after this many claims")
@@ -14,6 +14,7 @@ Roles and modes follow the fork/isolate rule:
14
14
  from __future__ import annotations
15
15
 
16
16
  import re
17
+ import sys
17
18
  from dataclasses import dataclass, field
18
19
  from datetime import timedelta
19
20
  from typing import Callable, Dict, Iterable, List, Optional, Tuple
@@ -142,6 +143,9 @@ class Compiled:
142
143
  pinned_cap: Optional[int] = None
143
144
  pinned_cut: Dict[str, int] = field(default_factory=dict)
144
145
  pinned_full: Dict[str, int] = field(default_factory=dict)
146
+ # the retrieval plugin that took part (ctr_9e166c2f291b829b): {plugin, share, ids}, and
147
+ # error when it could not run; None when config.retrieval names none
148
+ retrieval: Optional[Dict] = None
145
149
 
146
150
  def included_ids(self) -> List[str]:
147
151
  return [i.id for items in self.sections.values() for i in items]
@@ -590,6 +594,10 @@ def compile_context(
590
594
  else:
591
595
  out.sections["working"].append(item)
592
596
 
597
+ # the retrieval plugin (ctr_9e166c2f291b829b): the ids it finds for the task, of which
598
+ # the reader sees only what this compile's own policy filter let through (candidates)
599
+ retrieval_cfg, retrieved = _retrieval(ledger, task, {item.id: item for item, _ in candidates}, out, assessment)
600
+
593
601
  seen_dnr = set()
594
602
  for it in do_not_retry:
595
603
  key = (it.id, it.statement)
@@ -619,35 +627,72 @@ def compile_context(
619
627
  # costed provisionally here and the difference is charged once it is known
620
628
  provisional = _obligations(role, mode, round_no, bool(out.sections["cross_check"]), bool(contract_id), lens)
621
629
  overhead = overhead_tokens(out, provisional)
622
- used = overhead
623
- kept: Dict[str, List[Item]] = {name: [] for name in SECTION_ORDER}
624
- remaining_by_section = {name: [] for name in SECTION_ORDER}
625
- # pass 1: each section up to its share; pass 2: fill leftovers in priority order
626
- for name in SECTION_ORDER:
627
- cap = int(budget * SECTION_SHARE[name])
628
- # half of the constraints section is kept for its human-filed items, which arrive
629
- # in standing order, so the freshly filed ones compete for the other half only
630
- reserved = int(cap * CONSTRAINT_HUMAN_RESERVE) if name == "constraints" else 0
631
- section_used = unreserved_used = 0
632
- for item in out.sections[name]:
633
- cost = cost_of(item)
634
- reserve_eligible = bool(reserved) and item.is_human
635
- fits_share = reserve_eligible or unreserved_used + cost <= cap - reserved
636
- if section_used + cost <= cap and fits_share and used + cost <= budget:
637
- kept[name].append(item)
638
- section_used += cost
639
- unreserved_used += 0 if reserve_eligible else cost
640
- used += cost
641
- else:
642
- remaining_by_section[name].append(item)
643
- for name in SECTION_ORDER:
644
- for item in remaining_by_section[name]:
645
- cost = cost_of(item)
646
- if used + cost <= budget:
647
- kept[name].append(item)
648
- used += cost
649
- else:
650
- out.excluded.append((item.id, "budget"))
630
+ def allocate(reserve: int, found: List[Item], fixed: Optional[List[Item]] = None):
631
+ """(kept, used, left out for the budget): pass one fills each section up to its
632
+ share, the retrieval plugin's ``found`` fill its section up to ``reserve``, and pass
633
+ two fills leftovers in priority order. ``fixed`` is the constraints section as
634
+ compile alone keeps it: the plugin's share is never taken from what always applies
635
+ (the review of ctr_9e166c2f291b829b)."""
636
+ used = overhead
637
+ kept: Dict[str, List[Item]] = {name: [] for name in SECTION_ORDER}
638
+ remaining_by_section: Dict[str, List[Item]] = {name: [] for name in SECTION_ORDER}
639
+ # pass 1: each section up to its share; pass 2: fill leftovers in priority order
640
+ for name in SECTION_ORDER:
641
+ if name == "constraints" and fixed is not None:
642
+ kept[name] = list(fixed)
643
+ used += sum(cost_of(item) for item in fixed)
644
+ held = {id(item) for item in fixed}
645
+ remaining_by_section[name] = [item for item in out.sections[name] if id(item) not in held]
646
+ continue
647
+ cap = int(budget * SECTION_SHARE[name])
648
+ # half of the constraints section is kept for its human-filed items, which arrive
649
+ # in standing order, so the freshly filed ones compete for the other half only
650
+ reserved = int(cap * CONSTRAINT_HUMAN_RESERVE) if name == "constraints" else 0
651
+ section_used = unreserved_used = 0
652
+ for item in out.sections[name]:
653
+ cost = cost_of(item)
654
+ reserve_eligible = bool(reserved) and item.is_human
655
+ fits_share = reserve_eligible or unreserved_used + cost <= cap - reserved
656
+ if section_used + cost <= cap and fits_share and used + cost <= budget - reserve:
657
+ kept[name].append(item)
658
+ section_used += cost
659
+ unreserved_used += 0 if reserve_eligible else cost
660
+ used += cost
661
+ else:
662
+ remaining_by_section[name].append(item)
663
+ taken = {item.id for items in kept.values() for item in items}
664
+ if found:
665
+ # what the plugin found and pass 1 did not already keep, in the plugin's order,
666
+ # up to its share: a record shown once, in the section that placed it first
667
+ kept["retrieved"], spent = [], 0
668
+ for item in found:
669
+ cost = cost_of(item)
670
+ if item.id not in taken and spent + cost <= reserve and used + cost <= budget:
671
+ kept["retrieved"].append(item)
672
+ taken.add(item.id)
673
+ spent += cost
674
+ used += cost
675
+ left_out: List[str] = []
676
+ for name in SECTION_ORDER:
677
+ for item in remaining_by_section[name]:
678
+ if item.id in taken:
679
+ continue # the retrieved section placed it
680
+ cost = cost_of(item)
681
+ if used + cost <= budget:
682
+ kept[name].append(item)
683
+ used += cost
684
+ else:
685
+ left_out.append(item.id)
686
+ return kept, used, left_out
687
+
688
+ # compile alone; and when the retrieval plugin found something, again with its share
689
+ # held back from every section but the constraints, which keep what compile alone
690
+ # gives them. A plugin that finds nothing changes nothing (BENCHMARK-PLAN 1.2)
691
+ kept, used, left_out = allocate(0, [])
692
+ if retrieved:
693
+ kept, used, left_out = allocate(int(budget * retrieval_cfg["share"]), retrieved, fixed=kept["constraints"])
694
+ out.retrieval["ids"] = [item.id for item in kept["retrieved"]]
695
+ out.excluded += [(rid, "budget") for rid in left_out]
651
696
  out.sections = kept
652
697
  out.used_tokens = used
653
698
 
@@ -668,6 +713,34 @@ def compile_context(
668
713
  return out
669
714
 
670
715
 
716
+ def _retrieval(ledger, task: Optional[str], eligible: Dict[str, "Item"], out: "Compiled",
717
+ assessment: Optional[dict] = None):
718
+ """(the plugin's settings or None, the eligible items it found, best first), and
719
+ ``out.retrieval`` set to what the receipt records. A plugin that cannot run leaves the
720
+ view as compile alone makes it, and says so: on the receipt, and in one stderr line."""
721
+ from . import retrieval as retrieval_mod # local: retrieval imports this module
722
+ try:
723
+ cfg = retrieval_mod.settings(ledger.config)
724
+ except retrieval_mod.RetrievalError as exc:
725
+ out.retrieval = {"plugin": retrieval_mod.configured_name(ledger.config), "share": None, "ids": [],
726
+ "error": str(exc)}
727
+ print(f"agent-memory: the retrieval plugin did not run ({exc}); compiled without it", file=sys.stderr)
728
+ return None, []
729
+ if cfg is None:
730
+ return None, []
731
+ out.retrieval = {"plugin": cfg["plugin"], "share": cfg["share"], "ids": []}
732
+ if not task:
733
+ return cfg, [] # nothing to search by: a view with no task is compile's alone
734
+ try:
735
+ found = retrieval_mod.retrieve(ledger, task, cfg, assessment)
736
+ except retrieval_mod.RetrievalError as exc:
737
+ out.retrieval["error"] = str(exc)
738
+ print(f"agent-memory: the retrieval plugin {cfg['plugin']} did not run ({exc}); compiled without it",
739
+ file=sys.stderr)
740
+ return cfg, []
741
+ return cfg, [eligible[rid] for rid in found if rid in eligible]
742
+
743
+
671
744
  def compiled_from_json(payload: dict) -> Compiled:
672
745
  """Rebuild a Compiled from ``render.render_json``'s payload.
673
746
 
@@ -690,6 +763,7 @@ def compiled_from_json(payload: dict) -> Compiled:
690
763
  # saying it was cut
691
764
  board=[dict(row) for row in payload.get("board") or []],
692
765
  )
766
+ out.retrieval = payload.get("retrieval") if isinstance(payload.get("retrieval"), dict) else None
693
767
  display = payload.get("reader_display")
694
768
  out.reader_display = display if isinstance(display, str) and display else None
695
769
  out.config_notes = [n for n in payload.get("config_notes") or [] if isinstance(n, str)]
@@ -840,6 +914,10 @@ def record_compilation(ledger, compiled: Compiled, rendered: str, render_name: s
840
914
  # which reliability numbers the reader was shown, or null when annotation was off
841
915
  "reliability": compiled.reliability_meta,
842
916
  }
917
+ if compiled.retrieval is not None:
918
+ # which configuration produced the view (ctr_9e166c2f291b829b): the plugin, its
919
+ # share, the ids it added, and why it did not run when it did not
920
+ body["retrieval"] = compiled.retrieval
843
921
  # which repository the view was cut to, and where it was served and asked from: set
844
922
  # only when they are, so a plain view's receipt is what it always was
845
923
  for key, value in (("repo", compiled.repo), ("served", compiled.served or None),
@@ -181,6 +181,116 @@ def _linked_claims(ledger, contract_id: str, extra: Iterable[str] = ()) -> List[
181
181
  return out
182
182
 
183
183
 
184
+ def _assignee_of(body: dict) -> Optional[str]:
185
+ value = (body or {}).get("assigned_to")
186
+ return value.strip().lower() if isinstance(value, str) and value.strip() else None
187
+
188
+
189
+ def assignee_now(ledger, contract_id: str) -> Optional[str]:
190
+ """Who holds the contract: the latest update that names an assignee, else the one it
191
+ was filed with (what ``status_of`` reports as ``assigned_to``)."""
192
+ rec = ledger.get(contract_id)
193
+ held = _assignee_of((rec or {}).get("body") or {})
194
+ for u in sorted(ledger.contract_updates(contract_id), key=sort_key):
195
+ held = _assignee_of(u["body"]) or held
196
+ return held
197
+
198
+
199
+ def _may_amend(config: dict, author: dict) -> bool:
200
+ """A person, or the ledger's PM (config.pm): an agent's standing to amend a bar would
201
+ rest on an assignment any token can file, so no other agent has it."""
202
+ return author.get("kind") == "human" or is_pm(config, author)
203
+
204
+
205
+ def _names_of(author: dict) -> set:
206
+ """The names that mean ``author`` when a contract is assigned: its user id and the local
207
+ part the relay addresses it by, as a reader is matched (``compiler._reader_names``,
208
+ PROTOCOL section 11). Every holder test here goes through it (the review of #148)."""
209
+ from .compiler import _reader_names # local: compiler imports this module
210
+ return _reader_names(author)
211
+
212
+
213
+ def _has_note(body: dict) -> bool:
214
+ note = body.get("note")
215
+ return isinstance(note, str) and bool(note.strip())
216
+
217
+
218
+ def amendment_refusal(ledger, author: dict, contract_id: str, assigned_to: Optional[str] = None,
219
+ note: Optional[str] = None) -> Optional[str]:
220
+ """Why ``author`` may not amend the contract's acceptance checks, or None
221
+ (ctr_649e0b459a758929). The worker never writes their own bar, so only a person or
222
+ the PM amends one. Reassignment is open to any token, so "anybody but the assignee"
223
+ would let the assignee reassign the contract away and amend it in the next update,
224
+ and would leave an unassigned contract to whoever wants it (the review of this
225
+ story). The PM does not amend a contract it holds, or one the same update hands to
226
+ it; a person may, as a person may reopen a closed one: the bar is the person's to set
227
+ (the PE's ruling). An amendment is a decision on the record, so it carries a note.
228
+ A contract names its holder by user id or by local part, and both mean the author."""
229
+ names = _names_of(author)
230
+ if not _may_amend(getattr(ledger, "config", None) or {}, author):
231
+ return (f"{author.get('user') or 'this agent'} may not amend contract {contract_id}'s acceptance checks: "
232
+ f"an amendment is a person's or the PM's (config.pm), since the worker never writes their own bar")
233
+ if not (note or "").strip():
234
+ return (f"an amendment of contract {contract_id}'s acceptance checks is a decision on the record: "
235
+ f"say why in --note")
236
+ if author.get("kind") == "human":
237
+ return None
238
+ holder = assignee_now(ledger, contract_id)
239
+ if holder and holder in names:
240
+ return (f"contract {contract_id} is assigned to {holder}, and the assignee never amends its own "
241
+ f"acceptance checks: the PM or somebody else does")
242
+ handed = _assignee_of({"assigned_to": assigned_to})
243
+ if handed and handed in names:
244
+ return (f"an amendment of contract {contract_id}'s acceptance checks may not assign the contract to its "
245
+ f"own author ({handed})")
246
+ return None
247
+
248
+
249
+ def amendments(ledger, contract_id: str) -> List[dict]:
250
+ """The contract_updates that amend the contract's acceptance checks, oldest first.
251
+
252
+ Refused where it is filed, and not counted where it is read, by the same rules: an
253
+ amendment by anybody but a person or the PM, or with no note, or whose agent author
254
+ held the contract when it was filed (by user id or local part), or which handed it to
255
+ that author, changes nothing (one written by hand, or through a replica that did not
256
+ know the key). The history is folded in ``sort_key`` order, so every machine counts
257
+ the same ones."""
258
+ config = getattr(ledger, "config", None) or {}
259
+ rec = ledger.get(contract_id)
260
+ held = _assignee_of((rec or {}).get("body") or {})
261
+ counted = []
262
+ for u in sorted(ledger.contract_updates(contract_id), key=sort_key):
263
+ body = u.get("body") or {}
264
+ if "acceptance_checks" in body and "acceptance" not in body:
265
+ author = u.get("author") or {}
266
+ names = _names_of(author)
267
+ handed = _assignee_of(body)
268
+ held_by_author = author.get("kind") != "human" and bool(
269
+ (held and held in names) or (handed and handed in names))
270
+ if _may_amend(config, author) and _has_note(body) and not held_by_author:
271
+ counted.append(u)
272
+ held = _assignee_of(body) or held
273
+ return counted
274
+
275
+
276
+ def acceptance_of(ledger, contract_id: str) -> tuple:
277
+ """(the acceptance checks the contract answers to, the amendment that set them or
278
+ None): the latest counted amendment's list, else the one the contract was filed with.
279
+ Every reader and runner of the checks goes through here (ctr_649e0b459a758929)."""
280
+ rec = ledger.get(contract_id)
281
+ if rec is None or rec.get("kind") != "contract":
282
+ raise KeyError(f"no contract {contract_id}")
283
+ if not hasattr(ledger, "contract_updates"):
284
+ # a view that holds no contract_updates cannot see an amendment, and running the list
285
+ # the contract was filed with would be the very slip this reads past: a caller with
286
+ # such a view passes the checks the replica reports (``run_acceptance(checks=)``)
287
+ raise TypeError(f"the acceptance of {contract_id} needs a ledger that holds its contract_updates")
288
+ latest = amendments(ledger, contract_id)
289
+ if latest:
290
+ return latest[-1]["body"]["acceptance_checks"], latest[-1]["id"]
291
+ return rec["body"]["acceptance"], None
292
+
293
+
184
294
  def acceptance_runs(ledger, contract_id: str) -> List[dict]:
185
295
  """Acceptance-run contract_updates for a contract, oldest first."""
186
296
  runs = [u for u in ledger.contract_updates(contract_id) if u["body"].get("acceptance")]
@@ -270,6 +380,13 @@ def verification_basis(ledger, contract_id: str, assessment=None) -> dict:
270
380
  return out
271
381
  latest = runs[-1]
272
382
  out["run"] = latest["id"]
383
+ amended = amendments(ledger, contract_id)
384
+ if amended and sort_key(amended[-1]) > sort_key(latest):
385
+ # the run asked the checks the contract answered to then, not the ones it answers to
386
+ # now (ctr_649e0b459a758929)
387
+ out["reason"] = (f"the acceptance was amended by {amended[-1]['id']} after the latest run; "
388
+ f"run `agent-memory contract accept {contract_id}`")
389
+ return out
273
390
  if not _run_ok(latest):
274
391
  failing = [r.get("check", {}).get("cmd") or r.get("check", {}).get("path") or r.get("check", {}).get("kind")
275
392
  for r in latest["body"]["acceptance"] if r.get("outcome") not in ("match", "skipped")]
@@ -452,6 +569,11 @@ def transition_refusal(ledger, rec: dict, assessment=None) -> Optional[str]:
452
569
  note=note if isinstance(note, str) else None, assessment=assessment)
453
570
  except ValidationError as exc:
454
571
  return str(exc)
572
+ if "acceptance_checks" in body:
573
+ assigned = body.get("assigned_to")
574
+ return amendment_refusal(ledger, rec.get("author") or {}, contract_id,
575
+ assigned if isinstance(assigned, str) else None,
576
+ note=note if isinstance(note, str) else None)
455
577
  return None
456
578
 
457
579
 
@@ -461,6 +583,7 @@ def update_body(
461
583
  note: Optional[str] = None,
462
584
  claims: Iterable[str] = (),
463
585
  assigned_to: Optional[str] = None,
586
+ acceptance_checks: Optional[List[dict]] = None,
464
587
  ) -> dict:
465
588
  """The body of a contract_update that asks for a move, without filing it.
466
589
 
@@ -473,6 +596,8 @@ def update_body(
473
596
  body["note"] = note
474
597
  if assigned_to:
475
598
  body["assigned_to"] = assigned_to
599
+ if acceptance_checks:
600
+ body["acceptance_checks"] = list(acceptance_checks)
476
601
  return body
477
602
 
478
603
 
@@ -486,13 +611,21 @@ def update(
486
611
  claims: Iterable[str] = (),
487
612
  tags: Iterable[str] = (),
488
613
  assigned_to: Optional[str] = None,
614
+ acceptance_checks: Optional[List[dict]] = None,
489
615
  ) -> dict:
490
616
  """File a status transition, enforcing the rules above. ``assigned_to`` hands the
491
617
  contract to another agent: the latest update that names one is who holds it, which
492
- is what the compiled board shows (a PM reassigns with `contract update --assign`)."""
618
+ is what the compiled board shows (a PM reassigns with `contract update --assign`).
619
+ ``acceptance_checks`` replaces the checks the contract answers to from here on
620
+ (``acceptance_of``); the assignee may not file one (``amendment_refusal``)."""
493
621
  _check_transition(ledger, author, contract_id, status, claims, note=note)
622
+ if acceptance_checks:
623
+ refusal = amendment_refusal(ledger, author, contract_id, assigned_to, note=note)
624
+ if refusal:
625
+ raise ValidationError(refusal)
494
626
  rec = new_record("contract_update", author, scope,
495
- update_body(contract_id, status, note=note, claims=claims, assigned_to=assigned_to),
627
+ update_body(contract_id, status, note=note, claims=claims, assigned_to=assigned_to,
628
+ acceptance_checks=acceptance_checks),
496
629
  derived_from=[contract_id, *claims], tags=tags)
497
630
  return ledger.append(rec)
498
631
 
@@ -522,6 +655,8 @@ def status_of(ledger, contract_id: str, assessment: Optional[dict] = None) -> di
522
655
  for u in updates:
523
656
  linked_ids.update(u["body"].get("claims", []))
524
657
  claims = [ledger.get(i) for i in sorted(linked_ids) if ledger.get(i)]
658
+ checks, amended_by = acceptance_of(ledger, contract_id)
659
+ amended_note = next((u["body"].get("note") for u in updates if u["id"] == amended_by), None)
525
660
  out = {
526
661
  "id": contract_id,
527
662
  "title": rec["body"]["title"],
@@ -537,7 +672,11 @@ def status_of(ledger, contract_id: str, assessment: Optional[dict] = None) -> di
537
672
  # thread besides the assignee's (ctr_b3b3e487832b5c94): a relay asks the replica
538
673
  "pm": pm_users(getattr(ledger, "config", None) or {}),
539
674
  "issue": rec["body"].get("issue"),
540
- "acceptance": rec["body"]["acceptance"],
675
+ "acceptance": checks,
676
+ # the amendment the checks come from, or None for the list the contract was filed
677
+ # with, and the note that says why (an amendment is a decision on the record)
678
+ "acceptance_amended_by": amended_by,
679
+ "acceptance_amended_note": amended_note,
541
680
  "updates": [{"ts": u["ts"], "status": u["body"]["status"], "by": u["author"].get("harness"), "note": u["body"].get("note")} for u in updates],
542
681
  "acceptance_runs": [],
543
682
  "last_acceptance_ok": False,
@@ -560,7 +699,7 @@ def status_of(ledger, contract_id: str, assessment: Optional[dict] = None) -> di
560
699
 
561
700
  def run_acceptance(ledger, contract_id: str, cwd: Optional[str] = None, timeout: Optional[float] = 600,
562
701
  author: Optional[dict] = None, scope: Optional[dict] = None, file: bool = True,
563
- policy: Optional[dict] = None) -> List[dict]:
702
+ policy: Optional[dict] = None, checks: Optional[List[dict]] = None) -> List[dict]:
564
703
  """Run each acceptance check and return [{check, outcome, detail, receipt_body, receipt}].
565
704
 
566
705
  With ``file=True`` and an author and scope, the command/test receipts and a
@@ -588,7 +727,12 @@ def run_acceptance(ledger, contract_id: str, cwd: Optional[str] = None, timeout:
588
727
  except ev.SandboxError as exc: # asked for and malformed: no check runs at all
589
728
  sandbox, unsandboxable = None, str(exc)
590
729
  results = []
591
- for check in rec["body"]["acceptance"]:
730
+ # what this runner saw when it started (PROTOCOL 4.1: the status its runner read before
731
+ # the checks ran), as the remote lane reads it, never a status a close filed mid-run
732
+ started = status_now(ledger, contract_id) if file and author and scope else None
733
+ # the checks it answers to now (``acceptance_of``); a caller whose ledger holds no
734
+ # updates (the remote lane's served view) passes the list the replica reports
735
+ for check in (checks if checks is not None else acceptance_of(ledger, contract_id)[0]):
592
736
  kind = check.get("kind")
593
737
  if kind == "command":
594
738
  where = ev.resolved_directory(base / check["cwd"] if check.get("cwd") else base,
@@ -621,7 +765,8 @@ def run_acceptance(ledger, contract_id: str, cwd: Optional[str] = None, timeout:
621
765
  detail = (f"exit {body['exit']} (expected {expect_exit})" + (f"; {body.get('summary')}" if body.get("summary") else "")
622
766
  + ("; timed out, and everything it started was stopped" if body.get("timed_out") else ""))
623
767
  if sandbox is not None:
624
- reason = ev.sandbox_failure(sandbox["image"], body, expect_exit, check["cmd"]) if not ok else None
768
+ reason = (ev.sandbox_failure(sandbox["image"], body, expect_exit, check["cmd"],
769
+ unproven=ev.image_proof(sandbox, where)) if not ok else None)
625
770
  if reason:
626
771
  outcome, detail = "error", f"{reason}; {detail}"
627
772
  detail = f"sandboxed: podman {sandbox['image']}; {detail}"
@@ -646,7 +791,7 @@ def run_acceptance(ledger, contract_id: str, cwd: Optional[str] = None, timeout:
646
791
  body = r.get("receipt_body")
647
792
  if body and body.get("kind") in ("command", "test"):
648
793
  r["receipt"] = ledger.append(new_record("evidence", author, scope, body))["id"]
649
- status = status_now(ledger, contract_id) # what this runner saw: a run reports, it never promotes
794
+ status = started # what this runner saw when it started: a run reports, it never promotes
650
795
  body = {
651
796
  "contract": contract_id,
652
797
  "status": status,