@raishin/vanguard-frontier-agentic 3.15.0 → 3.17.0

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 (374) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +3 -1
  3. package/.cursor-plugin/plugin.json +3 -1
  4. package/.github/plugin/marketplace.json +1 -1
  5. package/README.md +11 -11
  6. package/agents/AGENTS.md +1 -1
  7. package/agents/README.md +1 -1
  8. package/agents/cross-functional/agentic-security-review-agent/AGENT.md +168 -0
  9. package/agents/cross-functional/agentic-security-review-agent/harnesses/claude-code.agent.md +158 -0
  10. package/agents/cross-functional/agentic-security-review-agent/harnesses/codex.toml +165 -0
  11. package/agents/cross-functional/agentic-security-review-agent/harnesses/copilot.agent.md +163 -0
  12. package/agents/cross-functional/agentic-security-review-agent/harnesses/cursor.agent.md +158 -0
  13. package/agents/cross-functional/agentic-security-review-agent/harnesses/gemini.agent.md +164 -0
  14. package/agents/cross-functional/agentic-security-review-agent/harnesses/kiro-cli.agent.json +5 -0
  15. package/agents/cross-functional/agentic-security-review-agent/harnesses/kiro-ide.agent.md +157 -0
  16. package/agents/cross-functional/agentic-security-review-agent/metadata.json +45 -0
  17. package/agents/cross-functional/agentic-system-architecture-review-agent/AGENT.md +157 -0
  18. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/claude-code.agent.md +147 -0
  19. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/codex.toml +154 -0
  20. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/copilot.agent.md +152 -0
  21. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/cursor.agent.md +147 -0
  22. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/gemini.agent.md +153 -0
  23. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/kiro-cli.agent.json +5 -0
  24. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/kiro-ide.agent.md +146 -0
  25. package/agents/cross-functional/agentic-system-architecture-review-agent/metadata.json +46 -0
  26. package/agents/typescript/typescript-maestro-agent/AGENT.md +1 -1
  27. package/agents/typescript/typescript-maestro-agent/harnesses/claude-code.agent.md +1 -1
  28. package/agents/typescript/typescript-maestro-agent/harnesses/codex.toml +1 -1
  29. package/agents/typescript/typescript-maestro-agent/harnesses/copilot.agent.md +1 -1
  30. package/agents/typescript/typescript-maestro-agent/harnesses/cursor.agent.md +1 -1
  31. package/agents/typescript/typescript-maestro-agent/harnesses/gemini.agent.md +1 -1
  32. package/agents/typescript/typescript-maestro-agent/harnesses/kiro-cli.agent.json +1 -1
  33. package/agents/typescript/typescript-maestro-agent/harnesses/kiro-ide.agent.md +1 -1
  34. package/agents/typescript/typescript-mcp-tool-contract-agent/AGENT.md +1 -1
  35. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/claude-code.agent.md +1 -1
  36. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/copilot.agent.md +1 -1
  37. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/cursor.agent.md +1 -1
  38. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/gemini.agent.md +1 -1
  39. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/kiro-cli.agent.json +1 -1
  40. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/kiro-ide.agent.md +1 -1
  41. package/catalog/AGENTS.md +2 -1
  42. package/catalog/agents.json +71 -0
  43. package/catalog/asset-integrity.json +1639 -92
  44. package/catalog/index.json +3 -2
  45. package/catalog/install-roles.json +24 -3
  46. package/catalog/model-assignments.json +66 -0
  47. package/catalog/mods.json +131 -0
  48. package/catalog/skill-manifest.json +134 -8
  49. package/catalog/skills.json +89 -0
  50. package/mods/README.md +220 -0
  51. package/mods/audit-trail/README.md +70 -0
  52. package/mods/audit-trail/bin/hook.mjs +65 -0
  53. package/mods/audit-trail/claude-code/UNSUPPORTED.md +11 -0
  54. package/mods/audit-trail/claude-code/settings.hooks.json +14 -0
  55. package/mods/audit-trail/codex/hooks.json +14 -0
  56. package/mods/audit-trail/kiro/UNSUPPORTED.md +7 -0
  57. package/mods/audit-trail/kiro/audit-trail-post-tool-use-hook.json +14 -0
  58. package/mods/audit-trail/lib/common.mjs +213 -0
  59. package/mods/audit-trail/lib/core.mjs +50 -0
  60. package/mods/audit-trail/lib/runtime.mjs +140 -0
  61. package/mods/audit-trail/mod.toml +37 -0
  62. package/mods/blast-radius-guard/.claude-plugin/plugin.json +5 -0
  63. package/mods/blast-radius-guard/README.md +53 -0
  64. package/mods/blast-radius-guard/bin/hook.mjs +18 -0
  65. package/mods/blast-radius-guard/claude-code/settings.hooks.json +14 -0
  66. package/mods/blast-radius-guard/codex/UNSUPPORTED.md +7 -0
  67. package/mods/blast-radius-guard/codex/hooks.json +14 -0
  68. package/mods/blast-radius-guard/hooks/hooks.json +5 -0
  69. package/mods/blast-radius-guard/hooks/register.test.ts +97 -0
  70. package/mods/blast-radius-guard/hooks/register.ts +13 -0
  71. package/mods/blast-radius-guard/kiro/UNSUPPORTED.md +7 -0
  72. package/mods/blast-radius-guard/kiro/blast-radius-guard-pre-tool-use-hook.json +14 -0
  73. package/mods/blast-radius-guard/lib/common.mjs +213 -0
  74. package/mods/blast-radius-guard/lib/core.mjs +66 -0
  75. package/mods/blast-radius-guard/lib/patterns.mjs +102 -0
  76. package/mods/blast-radius-guard/lib/runtime.mjs +140 -0
  77. package/mods/blast-radius-guard/mod.toml +34 -0
  78. package/mods/blast-radius-guard/patterns.json +101 -0
  79. package/mods/context-burn/.claude-plugin/plugin.json +5 -0
  80. package/mods/context-burn/README.md +66 -0
  81. package/mods/context-burn/bin/hook.mjs +38 -0
  82. package/mods/context-burn/claude-code/UNSUPPORTED.md +7 -0
  83. package/mods/context-burn/claude-code/settings.hooks.json +24 -0
  84. package/mods/context-burn/codex/UNSUPPORTED.md +7 -0
  85. package/mods/context-burn/codex/hooks.json +24 -0
  86. package/mods/context-burn/hooks/hooks.json +5 -0
  87. package/mods/context-burn/hooks/register.test.ts +151 -0
  88. package/mods/context-burn/hooks/register.ts +35 -0
  89. package/mods/context-burn/kiro/UNSUPPORTED.md +7 -0
  90. package/mods/context-burn/kiro/context-burn-post-tool-use-hook.json +14 -0
  91. package/mods/context-burn/kiro/context-burn-session-start-hook.json +14 -0
  92. package/mods/context-burn/lib/common.mjs +213 -0
  93. package/mods/context-burn/lib/core.mjs +79 -0
  94. package/mods/context-burn/lib/runtime.mjs +140 -0
  95. package/mods/context-burn/mod.toml +37 -0
  96. package/mods/live-guard-gate/.claude-plugin/plugin.json +5 -0
  97. package/mods/live-guard-gate/README.md +43 -0
  98. package/mods/live-guard-gate/bin/hook.mjs +12 -0
  99. package/mods/live-guard-gate/claude-code/settings.hooks.json +14 -0
  100. package/mods/live-guard-gate/codex/UNSUPPORTED.md +7 -0
  101. package/mods/live-guard-gate/codex/hooks.json +14 -0
  102. package/mods/live-guard-gate/hooks/hooks.json +5 -0
  103. package/mods/live-guard-gate/hooks/register.test.ts +59 -0
  104. package/mods/live-guard-gate/hooks/register.ts +11 -0
  105. package/mods/live-guard-gate/kiro/UNSUPPORTED.md +7 -0
  106. package/mods/live-guard-gate/kiro/live-guard-gate-pre-tool-use-hook.json +14 -0
  107. package/mods/live-guard-gate/lib/common.mjs +213 -0
  108. package/mods/live-guard-gate/lib/core.mjs +24 -0
  109. package/mods/live-guard-gate/lib/live-agents.mjs +65 -0
  110. package/mods/live-guard-gate/lib/patterns.mjs +523 -0
  111. package/mods/live-guard-gate/lib/runtime.mjs +140 -0
  112. package/mods/live-guard-gate/mod.toml +34 -0
  113. package/mods/live-guard-gate/patterns.json +522 -0
  114. package/mods/secret-redactor/.claude-plugin/plugin.json +5 -0
  115. package/mods/secret-redactor/README.md +86 -0
  116. package/mods/secret-redactor/bin/hook.mjs +35 -0
  117. package/mods/secret-redactor/claude-code/UNSUPPORTED.md +7 -0
  118. package/mods/secret-redactor/claude-code/settings.hooks.json +24 -0
  119. package/mods/secret-redactor/codex/UNSUPPORTED.md +11 -0
  120. package/mods/secret-redactor/codex/hooks.json +14 -0
  121. package/mods/secret-redactor/hooks/hooks.json +5 -0
  122. package/mods/secret-redactor/hooks/register.test.ts +150 -0
  123. package/mods/secret-redactor/hooks/register.ts +19 -0
  124. package/mods/secret-redactor/kiro/UNSUPPORTED.md +11 -0
  125. package/mods/secret-redactor/kiro/secret-redactor-pre-tool-use-hook.json +14 -0
  126. package/mods/secret-redactor/lib/common.mjs +213 -0
  127. package/mods/secret-redactor/lib/core.mjs +61 -0
  128. package/mods/secret-redactor/lib/patterns.mjs +75 -0
  129. package/mods/secret-redactor/lib/runtime.mjs +140 -0
  130. package/mods/secret-redactor/mod.toml +43 -0
  131. package/mods/secret-redactor/patterns.json +74 -0
  132. package/package.json +9 -4
  133. package/plugins/vanguard-frontier-agentic/.codex-plugin/plugin.json +1 -1
  134. package/powers/vanguard-generic/POWER.md +6 -6
  135. package/powers/vanguard-kubernetes/POWER.md +1 -1
  136. package/powers/vanguard-multi-cloud/POWER.md +1 -1
  137. package/schemas/agentic-handoff.schema.json +443 -0
  138. package/scripts/generate-kiro-powers.mjs +17 -6
  139. package/scripts/generate-mods.mjs +784 -0
  140. package/scripts/mods/common.mjs +213 -0
  141. package/scripts/mods/runtime.mjs +140 -0
  142. package/scripts/typescript_data/agents/00-typescript-maestro-agent.json +2 -2
  143. package/scripts/typescript_data/agents/11-typescript-mcp-tool-contract-agent.json +4 -4
  144. package/skills/cross-functional/agentic-board-protocol/SKILL.md +85 -0
  145. package/skills/cross-functional/agentic-board-protocol/metadata.json +28 -0
  146. package/skills/cross-functional/agentic-board-protocol/references/decision-record.md +134 -0
  147. package/skills/cross-functional/agentic-board-protocol/references/handoff-envelope.md +208 -0
  148. package/skills/cross-functional/agentic-board-protocol/references/risk-tier-crosswalk.md +88 -0
  149. package/skills/cross-functional/agentic-board-protocol/references/stop-conditions.md +122 -0
  150. package/skills/cross-functional/agentic-board-protocol/references/value-case.md +159 -0
  151. package/skills/cross-functional/agentic-security-review/SKILL.md +78 -0
  152. package/skills/cross-functional/agentic-security-review/metadata.json +30 -0
  153. package/skills/cross-functional/agentic-security-review/references/identity-and-delegated-authorization.md +144 -0
  154. package/skills/cross-functional/agentic-security-review/references/supply-chain-and-provenance.md +111 -0
  155. package/skills/cross-functional/agentic-security-review/references/threat-model-owasp-asi.md +55 -0
  156. package/skills/cross-functional/agentic-security-review/references/tools-memory-and-data-reach.md +125 -0
  157. package/skills/cross-functional/agentic-security-review/references/workflow-and-output.md +210 -0
  158. package/skills/cross-functional/agentic-system-architecture-review/SKILL.md +74 -0
  159. package/skills/cross-functional/agentic-system-architecture-review/metadata.json +31 -0
  160. package/skills/cross-functional/agentic-system-architecture-review/references/delegation-and-handoff-review.md +107 -0
  161. package/skills/cross-functional/agentic-system-architecture-review/references/pattern-selection.md +81 -0
  162. package/skills/cross-functional/agentic-system-architecture-review/references/reliability-and-containment.md +86 -0
  163. package/skills/cross-functional/agentic-system-architecture-review/references/state-memory-and-durability.md +102 -0
  164. package/skills/cross-functional/agentic-system-architecture-review/references/workflow-and-output.md +135 -0
  165. package/skills/typescript/typescript-maestro/SKILL.md +1 -1
  166. package/skills/typescript/typescript-maestro/references/routing-taxonomy.md +1 -1
  167. package/skills/typescript/typescript-mcp-tool-contract/SKILL.md +1 -1
  168. package/tests/agentic-board-suite/_build_fixtures.py +1751 -0
  169. package/tests/agentic-board-suite/_build_harnesses.py +123 -0
  170. package/tests/agentic-board-suite/agentic_board_rules.py +857 -0
  171. package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--challenge-names-a-later-envelope.json +244 -0
  172. package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--dangling-challenge-beside-a-request.json +410 -0
  173. package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--dangling-proposal.json +187 -0
  174. package/tests/agentic-board-suite/fixtures/invalid/CH-ROUND-COUNTED--round-two-declared-as-one.json +481 -0
  175. package/tests/agentic-board-suite/fixtures/invalid/CH-ROUND-COUNTED--self-declared-round.json +187 -0
  176. package/tests/agentic-board-suite/fixtures/invalid/DR-ALTERNATIVES--no-alternatives.json +340 -0
  177. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-left-behind-by-proposer-swap.json +532 -0
  178. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-left-unattached.json +480 -0
  179. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-on-root-left-unattached.json +410 -0
  180. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--late-challenge-before-record.json +410 -0
  181. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--superseded-request-missed-earlier-challenge.json +565 -0
  182. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-PRECEDES--withdrawal-attached-before-it-was-sent.json +483 -0
  183. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-PRECEDES--withdrawal-descends-from-the-record.json +547 -0
  184. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r1-flag-without-challenge.json +349 -0
  185. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r2-without-challenge.json +339 -0
  186. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r3-no-flags-without-challenge.json +339 -0
  187. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-RESOLVES--dangling-challenge.json +347 -0
  188. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-TARGETS-PROPOSAL--challenge-of-requester-message.json +415 -0
  189. package/tests/agentic-board-suite/fixtures/invalid/DR-CLASS-FROM-ROOT--root-declares-no-class.json +345 -0
  190. package/tests/agentic-board-suite/fixtures/invalid/DR-CLASS-FROM-ROOT--root-declares-other-class.json +346 -0
  191. package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--designated-challenger-only-on-the-framing.json +346 -0
  192. package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--investment-without-finance.json +436 -0
  193. package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--stopped-designated-challenge.json +348 -0
  194. package/tests/agentic-board-suite/fixtures/invalid/DR-HARD-GATE--conditional-over-hard-gate.json +357 -0
  195. package/tests/agentic-board-suite/fixtures/invalid/DR-HARD-GATE--unresolved-hard-gate.json +357 -0
  196. package/tests/agentic-board-suite/fixtures/invalid/DR-NO-APPROVE-OVER-DISSENT--approve-over-a-stopped-challenge.json +413 -0
  197. package/tests/agentic-board-suite/fixtures/invalid/DR-NO-APPROVE-OVER-DISSENT--approve-over-dissent.json +346 -0
  198. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--re-ask-without-supersedes.json +426 -0
  199. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--request-superseded-twice.json +581 -0
  200. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-challenge.json +427 -0
  201. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-dangling-id.json +427 -0
  202. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-later-request.json +500 -0
  203. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--superseding-request-from-another-proposer.json +621 -0
  204. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersession-cycle.json +501 -0
  205. package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--agent-opens-decision-path.json +480 -0
  206. package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--owner-is-the-requester.json +273 -0
  207. package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--request-to-later-sorting-non-owner.json +273 -0
  208. package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--request-to-non-owner.json +273 -0
  209. package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--root-names-no-owner.json +341 -0
  210. package/tests/agentic-board-suite/fixtures/invalid/DR-PROPOSER-IS-SENDER--challenger-assembles-request.json +337 -0
  211. package/tests/agentic-board-suite/fixtures/invalid/DR-PROPOSER-NOT-CHALLENGER--self-challenge.json +411 -0
  212. package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--concurrent-higher-tier-envelope.json +403 -0
  213. package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--r5-decided-through-lower-tier-request.json +433 -0
  214. package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--request-drops-materiality-flags.json +479 -0
  215. package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--risk-raised-after-request-before-decision.json +403 -0
  216. package/tests/agentic-board-suite/fixtures/invalid/DR-TO-PRINCIPAL--request-to-agent.json +272 -0
  217. package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--dissent-dropped.json +341 -0
  218. package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--stopped-challenge-recorded-withdrawn.json +408 -0
  219. package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--unresolved-without-dissent.json +341 -0
  220. package/tests/agentic-board-suite/fixtures/invalid/DR-VALUE-CASE-REQUIRED--investment-without-value-case.json +353 -0
  221. package/tests/agentic-board-suite/fixtures/invalid/DRC-AFTER-REQUEST--decided-before-request.json +408 -0
  222. package/tests/agentic-board-suite/fixtures/invalid/DRC-AFTER-REQUEST--request-descends-from-its-record.json +408 -0
  223. package/tests/agentic-board-suite/fixtures/invalid/DRC-APPROVAL-STATE--state-contradicts-outcome.json +346 -0
  224. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-approves-a-redesign.json +433 -0
  225. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-contradicts-outcome.json +433 -0
  226. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-does-not-descend-from-its-record.json +499 -0
  227. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-is-the-parent-of-its-record.json +500 -0
  228. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-names-another-approver.json +438 -0
  229. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-nobody-asked-for.json +421 -0
  230. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-sent-before-the-record.json +499 -0
  231. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--decision-in-force-not-co-signed.json +591 -0
  232. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--later-sorting-second-approver-never-signs.json +359 -0
  233. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--named-second-approver-never-signs.json +359 -0
  234. package/tests/agentic-board-suite/fixtures/invalid/DRC-COMPLETED--record-left-dispatched.json +346 -0
  235. package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-ADDRESSEE--decided-by-non-addressee.json +408 -0
  236. package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-ADDRESSEE--earlier-sorting-non-addressee-decides.json +408 -0
  237. package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-SENDER--relayed-decision.json +408 -0
  238. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--override-without-acceptance.json +356 -0
  239. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--same-round-withdrawal.json +423 -0
  240. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--stopped-over-cap-withdrawal.json +623 -0
  241. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--unrelated-withdrawal-does-not-clear-gate.json +482 -0
  242. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--withdrawal-names-an-undelivered-envelope.json +482 -0
  243. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--withdrawal-on-another-proposers-revision.json +600 -0
  244. package/tests/agentic-board-suite/fixtures/invalid/DRC-NOT-REQUESTER--second-approver-is-requester.json +433 -0
  245. package/tests/agentic-board-suite/fixtures/invalid/DRC-ONE-RECORD--contradicting-records.json +416 -0
  246. package/tests/agentic-board-suite/fixtures/invalid/DRC-ONE-RECORD--duplicate-co-signature.json +507 -0
  247. package/tests/agentic-board-suite/fixtures/invalid/DRC-PRINCIPAL-SOURCE--agent-writes-decision.json +345 -0
  248. package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--points-at-challenge.json +346 -0
  249. package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--record-names-a-missing-request.json +346 -0
  250. package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--request-edited-after-decision.json +346 -0
  251. package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--r5-request-decided-on-lower-tier-branch.json +418 -0
  252. package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--r5-single-approver.json +354 -0
  253. package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--second-approver-is-decider.json +359 -0
  254. package/tests/agentic-board-suite/fixtures/invalid/HO-AGENT-NO-APPROVAL--agent-claims-approval.json +132 -0
  255. package/tests/agentic-board-suite/fixtures/invalid/HO-APPROVAL-ONLY-ON-RECORD--principal-delegate-claims-approval.json +132 -0
  256. package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-ACTIONS--child-widens-actions.json +345 -0
  257. package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-BUDGET--child-raises-limit.json +132 -0
  258. package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-BUDGET--currency-changed.json +132 -0
  259. package/tests/agentic-board-suite/fixtures/invalid/HO-BLOCK-AS-CHALLENGE--block-sent-as-return.json +184 -0
  260. package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-CUSTODY--parent-sender-continues-unstopped.json +187 -0
  261. package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-CUSTODY--reply-from-outside-chain.json +132 -0
  262. package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-PARENT--hop-skips.json +132 -0
  263. package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-TIME--sent-before-parent.json +132 -0
  264. package/tests/agentic-board-suite/fixtures/invalid/HO-DESTINATION-TIER--delegate-to-live-guard.json +132 -0
  265. package/tests/agentic-board-suite/fixtures/invalid/HO-DESTINATION-TIER--delegate-to-untiered-agent.json +132 -0
  266. package/tests/agentic-board-suite/fixtures/invalid/HO-ESCALATE-TO-PRINCIPAL--escalate-to-agent.json +131 -0
  267. package/tests/agentic-board-suite/fixtures/invalid/HO-ESCALATE-TO-PRINCIPAL--escalated-delegate-to-agent.json +132 -0
  268. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-CLASS--classification-lowered.json +346 -0
  269. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-DEADLINE--deadline-extended.json +132 -0
  270. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-MATERIALITY--flag-dropped.json +434 -0
  271. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-MAX-HOPS--max-hops-raised.json +132 -0
  272. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-PROHIBITIONS--prohibition-dropped.json +131 -0
  273. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-SPENT--one-counter-decreases.json +346 -0
  274. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-SPENT--spent-counter-reset.json +346 -0
  275. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-TIER--tier-lowered.json +346 -0
  276. package/tests/agentic-board-suite/fixtures/invalid/HO-NO-EXECUTION--deploy-authorized.json +133 -0
  277. package/tests/agentic-board-suite/fixtures/invalid/HO-NO-SELF-ADDRESS--challenge-to-self.json +187 -0
  278. package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-CORRELATION--two-correlations.json +132 -0
  279. package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-ROOT--second-root-beside-a-decision.json +410 -0
  280. package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-ROOT--two-roots.json +190 -0
  281. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-CHALLENGE--challenge-without-payload.json +180 -0
  282. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-DECISION-RECORD--record-without-payload.json +331 -0
  283. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-DECISION-REQUEST--request-without-payload.json +324 -0
  284. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-ESCALATE--escalate-without-payload.json +128 -0
  285. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-EXCLUSIVE--return-with-escalation.json +136 -0
  286. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-RETURN--return-without-verdict.json +124 -0
  287. package/tests/agentic-board-suite/fixtures/invalid/HO-REASON-HAS-STATUS--reason-on-completed.json +134 -0
  288. package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-on-inference-only.json +132 -0
  289. package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-over-blocking-gap.json +137 -0
  290. package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-with-no-evidence.json +127 -0
  291. package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-TO-SENDER--return-skips-the-delegator.json +185 -0
  292. package/tests/agentic-board-suite/fixtures/invalid/HO-ROOT-PARENT--root-with-parent.json +132 -0
  293. package/tests/agentic-board-suite/fixtures/invalid/HO-SCHEMA--unknown-credential-field.json +133 -0
  294. package/tests/agentic-board-suite/fixtures/invalid/HO-STOP-NOTE--conflict-without-note.json +133 -0
  295. package/tests/agentic-board-suite/fixtures/invalid/HO-STOPPED-HAS-REASON--stopped-without-reason.json +132 -0
  296. package/tests/agentic-board-suite/fixtures/invalid/HO-UNIQUE-TASK-ID--duplicate-task-id.json +198 -0
  297. package/tests/agentic-board-suite/fixtures/invalid/STOP-BUDGET--budget-undeclared.json +123 -0
  298. package/tests/agentic-board-suite/fixtures/invalid/STOP-BUDGET--return-overspent.json +132 -0
  299. package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--revisions-carried-in-rebuttal-challenges.json +461 -0
  300. package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--third-round.json +454 -0
  301. package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--third-version-by-return-naming-the-original.json +461 -0
  302. package/tests/agentic-board-suite/fixtures/invalid/STOP-DEADLINE--sent-after-deadline.json +132 -0
  303. package/tests/agentic-board-suite/fixtures/invalid/STOP-DEPTH--depth-undeclared.json +182 -0
  304. package/tests/agentic-board-suite/fixtures/invalid/STOP-INSTRUCTION-BAIT--claimed-approval-dispatched.json +132 -0
  305. package/tests/agentic-board-suite/fixtures/invalid/STOP-LIVE-GUARD--mutation-dispatched.json +130 -0
  306. package/tests/agentic-board-suite/fixtures/invalid/STOP-MISSING-EVIDENCE--blocking-gap-dispatched.json +137 -0
  307. package/tests/agentic-board-suite/fixtures/invalid/STOP-MISSING-EVIDENCE--decision-request-blocking-gap.json +351 -0
  308. package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--exchanges-on-the-original-envelope.json +381 -0
  309. package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--loop-undeclared.json +182 -0
  310. package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--revisit-to-challenger-of-another-party.json +256 -0
  311. package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--second-revisit-to-challenger.json +540 -0
  312. package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--challenge-answers-stopped-delegate.json +247 -0
  313. package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--decision-on-escalated-request.json +353 -0
  314. package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--decision-on-stopped-request.json +422 -0
  315. package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--reply-to-stopped-delegate.json +244 -0
  316. package/tests/agentic-board-suite/fixtures/invalid/VC-BENEFIT-CEILING--benefit-above-baseline.json +436 -0
  317. package/tests/agentic-board-suite/fixtures/invalid/VC-COST-SIGN--negative-risk-loss.json +436 -0
  318. package/tests/agentic-board-suite/fixtures/invalid/VC-COST-SIGN--risk-loss-low-minus-one.json +436 -0
  319. package/tests/agentic-board-suite/fixtures/invalid/VC-COUNTERFACTUALS--single-agent-counterfactual-missing.json +427 -0
  320. package/tests/agentic-board-suite/fixtures/invalid/VC-COUNTERFACTUALS--workflow-counterfactual-missing.json +427 -0
  321. package/tests/agentic-board-suite/fixtures/invalid/VC-NAV-ARITHMETIC--nav-off-by-one.json +436 -0
  322. package/tests/agentic-board-suite/fixtures/invalid/VC-ORDER--inverted-range.json +436 -0
  323. package/tests/agentic-board-suite/fixtures/invalid/VC-OVERSIGHT--free-oversight.json +436 -0
  324. package/tests/agentic-board-suite/fixtures/invalid/VC-PROMOTION--measured-but-not-incremental.json +436 -0
  325. package/tests/agentic-board-suite/fixtures/invalid/VC-PROMOTION--promoted-on-estimate.json +436 -0
  326. package/tests/agentic-board-suite/fixtures/invalid/VC-RECOMMEND-INCREMENTAL--recommend-non-incremental.json +436 -0
  327. package/tests/agentic-board-suite/fixtures/invalid/VC-RUNS-BOUND--more-successes-than-runs.json +436 -0
  328. package/tests/agentic-board-suite/fixtures/invalid/VC-SYNTHETIC--synthetic-claims-measured.json +436 -0
  329. package/tests/agentic-board-suite/fixtures/invalid/VC-UNIT-COST--no-successes-nonzero-cost.json +436 -0
  330. package/tests/agentic-board-suite/fixtures/invalid/VC-UNIT-COST--unit-cost-mismatch.json +436 -0
  331. package/tests/agentic-board-suite/fixtures/valid/v01-r0-single-review.json +129 -0
  332. package/tests/agentic-board-suite/fixtures/valid/v01x-block-return-on-inference.json +125 -0
  333. package/tests/agentic-board-suite/fixtures/valid/v01x-block-return-to-principal.json +125 -0
  334. package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-documentation-evidence.json +129 -0
  335. package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-mixed-evidence.json +133 -0
  336. package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-nonblocking-gap.json +134 -0
  337. package/tests/agentic-board-suite/fixtures/valid/v02-r2-architecture-dissent.json +343 -0
  338. package/tests/agentic-board-suite/fixtures/valid/v02x-challenge-after-decision.json +407 -0
  339. package/tests/agentic-board-suite/fixtures/valid/v02x-challenge-after-record-same-second.json +464 -0
  340. package/tests/agentic-board-suite/fixtures/valid/v02x-redispatch-after-cured-stop.json +534 -0
  341. package/tests/agentic-board-suite/fixtures/valid/v02x-security-proposal-returned-to-its-delegator.json +407 -0
  342. package/tests/agentic-board-suite/fixtures/valid/v03-investment-loses-to-workflow.json +433 -0
  343. package/tests/agentic-board-suite/fixtures/valid/v04-r3-hard-gate-risk-acceptance.json +354 -0
  344. package/tests/agentic-board-suite/fixtures/valid/v04x-live-hard-gate-recommend-escalate.json +354 -0
  345. package/tests/agentic-board-suite/fixtures/valid/v04x-live-hard-gate-recommend-reject.json +354 -0
  346. package/tests/agentic-board-suite/fixtures/valid/v05-live-guard-escalation.json +129 -0
  347. package/tests/agentic-board-suite/fixtures/valid/v06-r5-dual-approval.json +430 -0
  348. package/tests/agentic-board-suite/fixtures/valid/v06x-co-sign-same-second.json +430 -0
  349. package/tests/agentic-board-suite/fixtures/valid/v06x-re-asked-r5-decision-co-signed-again.json +662 -0
  350. package/tests/agentic-board-suite/fixtures/valid/v06x-unsigned-approve-superseded-by-co-signed-reject.json +588 -0
  351. package/tests/agentic-board-suite/fixtures/valid/v07-depth-and-routing-loop.json +180 -0
  352. package/tests/agentic-board-suite/fixtures/valid/v08-budget-exhausted.json +186 -0
  353. package/tests/agentic-board-suite/fixtures/valid/v09-instruction-bait.json +131 -0
  354. package/tests/agentic-board-suite/fixtures/valid/v09x-requester-restates-after-bait.json +251 -0
  355. package/tests/agentic-board-suite/fixtures/valid/v10-r1-optional-challenge.json +184 -0
  356. package/tests/agentic-board-suite/fixtures/valid/v10x-report-up-after-challenge.json +246 -0
  357. package/tests/agentic-board-suite/fixtures/valid/v11-two-rounds-hard-gate-withdrawn.json +478 -0
  358. package/tests/agentic-board-suite/fixtures/valid/v11x-designated-challenge-first-answer-over-cap.json +598 -0
  359. package/tests/agentic-board-suite/fixtures/valid/v11x-interleaved-challenger-round-two.json +604 -0
  360. package/tests/agentic-board-suite/fixtures/valid/v11x-proposal-handed-to-the-challenger-capped.json +860 -0
  361. package/tests/agentic-board-suite/fixtures/valid/v11x-round-three-escalation-path.json +747 -0
  362. package/tests/agentic-board-suite/fixtures/valid/v11x-same-second-round-two.json +478 -0
  363. package/tests/agentic-board-suite/fixtures/valid/v11x-two-findings-one-round.json +549 -0
  364. package/tests/agentic-board-suite/fixtures/valid/v12-superseded-request.json +497 -0
  365. package/tests/agentic-board-suite/fixtures/valid/v12x-challenge-after-the-first-decision.json +566 -0
  366. package/tests/agentic-board-suite/fixtures/valid/v12x-late-challenge-cured-by-supersede.json +566 -0
  367. package/tests/agentic-board-suite/fixtures/valid/v12x-supersede-in-the-same-second.json +497 -0
  368. package/tests/agentic-board-suite/test_agentic_board.py +502 -0
  369. package/tests/fixtures/README.md +1 -1
  370. package/tests/fuzz-properties.test.mjs +254 -0
  371. package/tests/mods.test.mjs +768 -0
  372. package/tests/validate-agent-tool-tiers.py +7 -5
  373. package/tests/validate-asset-integrity.py +1 -0
  374. package/tests/validate-mods.py +821 -0
@@ -0,0 +1,857 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic rule engine for agentic-board handoff records.
3
+
4
+ A *record set* is one JSON document ``{"messages": [envelope, ...]}`` whose envelopes share
5
+ one ``correlation_id``. Each envelope must satisfy ``schemas/agentic-handoff.schema.json``
6
+ (shape only); every cross-field and cross-message rule lives here, one predicate per rule
7
+ id, so a defect fails with exactly the id that names it.
8
+
9
+ Decisions come from committed data only: no wall clock, no network, no randomness. Time
10
+ rules compare the envelopes' own ``sent_at`` / ``deadline`` fields (UTC ``Z`` strings, so
11
+ string order is time order).
12
+
13
+ What this does NOT do: authenticate senders, verify that a principal is who it claims to
14
+ be, or enforce anything at runtime. A record that passes is well-formed and internally
15
+ consistent; it is not proof that any approval happened. That is the deploying runtime's job.
16
+
17
+ Schema patterns are evaluated with ECMA-262 semantics, as JSON Schema 2020-12 requires: a
18
+ pattern's final ``$`` matches only at the end of the string. Python's ``re`` lets ``$`` match
19
+ before a trailing newline, which would let ``"subject:...\n"`` pass the schema and then compare
20
+ unequal to the canonical subject in every identity rule.
21
+
22
+ CLI (prints one rule id per line, exit 1 on any violation; each file is checked on its own):
23
+ python3 tests/agentic-board-suite/agentic_board_rules.py <record-set.json> [...]
24
+ Digest helper for a principal binding a decision-record (prints request_digest, exit 0):
25
+ python3 tests/agentic-board-suite/agentic_board_rules.py --digest <request-envelope.json>
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import hashlib
30
+ from collections import Counter
31
+ import json
32
+ import re
33
+ import sys
34
+ from pathlib import Path
35
+
36
+ import jsonschema
37
+
38
+ ROOT = Path(__file__).resolve().parents[2]
39
+ SCHEMA_PATH = ROOT / "schemas" / "agentic-handoff.schema.json"
40
+ CATALOG_PATH = ROOT / "catalog" / "agents.json"
41
+
42
+ ARCH = "agentic-system-architecture-review-agent"
43
+ SEC = "agentic-security-review-agent"
44
+ FIN = "finance-capital-allocation-advisor-agent"
45
+
46
+ BOARD_ACTIONS = frozenset({"read", "analyze", "recommend", "challenge", "escalate"})
47
+ TIER_ORDER = ("R0", "R1", "R2", "R3", "R4", "R5")
48
+ CLASS_ORDER = ("public", "internal", "confidential", "restricted")
49
+ MAX_CHALLENGE_ROUNDS = 2
50
+ MAX_DEPTH = 32 # a schema-valid envelope nests about six deep; deeper input is rejected before validation
51
+ REQUIRED_COUNTERFACTUALS = frozenset({"existing-process", "conventional-automation", "deterministic-workflow", "single-agent"})
52
+ COST_RANGES = ("annual_run_cost", "annual_oversight_cost", "annualized_implementation_cost", "expected_risk_loss")
53
+ # Catalog execution tiers an agent may hold to receive board work (committed data, so deterministic).
54
+ DISPATCHABLE_TIERS = frozenset({"static-review", "read-only-runtime"})
55
+ PROMOTED_STATES = frozenset({"limited-production", "production"})
56
+ BUDGET_DIMENSIONS = ("tool_calls", "tokens", "cost_minor")
57
+ PAYLOAD_BY_TYPE = {
58
+ "delegate": None,
59
+ "return": "verdict",
60
+ "challenge": "challenge",
61
+ "escalate": "escalation",
62
+ "decision-request": "decision_request",
63
+ "decision-record": "decision_record",
64
+ }
65
+ PAYLOAD_KEYS = frozenset(k for k in PAYLOAD_BY_TYPE.values() if k)
66
+
67
+ # Designated challenger per decision class: (primary, fallback when the primary proposed).
68
+ DESIGNATED_CHALLENGERS = {
69
+ "architecture": (SEC, ARCH),
70
+ "security": (ARCH, SEC),
71
+ "automation-investment": (FIN, FIN),
72
+ }
73
+
74
+ # Canary for mutation intent in a delegate's objective. Objectives must be phrased as review
75
+ # questions ("Assess whether ..."); one that opens with a mutation verb is a request to act,
76
+ # which this static board never dispatches. This is a DENYLIST that runs only in this offline
77
+ # gate: it fails OPEN on phrasings it does not list ("Can you deploy ...", "We need to delete
78
+ # ..."), and it can false-stop a noun-led title. It is not runtime enforcement and not a
79
+ # semantic intent classifier; agents apply a semantic stop rule, never this regex.
80
+ _LEAD = r"^\s*(?:(?:urgent(?:ly)?|asap|now|please|immediately|today|tonight|just|go\s+ahead\s+and|quickly)\b[\s:,!.-]*)*"
81
+ _MUTATION_VERBS = (
82
+ r"deploy|roll\s?out|release|promote|ship|delete|destroy|purge|drop|truncate|wipe|grant|revoke|"
83
+ r"assign|pay|refund|transfer|wire|post|book|send|e-?mail|notify|publish|approve|execute|run|apply|"
84
+ r"merge|push|rotate|disable|enable|terminate|shut\s?down|restart|scale|migrate|provision|issue|sign|"
85
+ r"create|update|modify|change|remove|install|uninstall|invoke|trigger|submit|cancel|close"
86
+ )
87
+ LIVE_GUARD_INTENT = re.compile(_LEAD + r"(?:" + _MUTATION_VERBS + r")\b", re.IGNORECASE)
88
+ # Canary for instruction-override bait in an objective (OWASP ASI01 Agent Goal Hijack object).
89
+ # Same caveat: a denylist with low recall that fails open; tests pin its known misses.
90
+ INSTRUCTION_BAIT = re.compile(
91
+ r"\b(?:ignore|disregard|override|bypass|forget)\s+(?:all\s+|any\s+|the\s+|your\s+)?"
92
+ r"(?:previous\s+|prior\s+|above\s+|earlier\s+)?(?:rules|instructions|guardrails|policies|gates|do-not-do\s+list)\b"
93
+ r"|\b(?:already\s+approved|pre-?approved|approval\s+(?:was\s+)?(?:already\s+)?(?:given|granted))\b",
94
+ re.IGNORECASE,
95
+ )
96
+
97
+ RULES: dict[str, str] = {
98
+ # --- single envelope -------------------------------------------------------------------
99
+ "HO-SCHEMA": "Envelope matches schemas/agentic-handoff.schema.json (shape only).",
100
+ "HO-ROOT-PARENT": "parent_task_id is null if and only if hop_count is 0.",
101
+ "HO-NO-SELF-ADDRESS": "source and destination are different parties.",
102
+ "HO-NO-EXECUTION": "authorized_actions is a subset of read/analyze/recommend/challenge/escalate.",
103
+ "HO-AGENT-NO-APPROVAL": "An agent-sourced envelope carries approval_state not-required or pending-human-review.",
104
+ "HO-APPROVAL-ONLY-ON-RECORD": "A principal-sourced envelope other than a decision-record carries approval_state not-required or pending-human-review.",
105
+ "HO-ESCALATE-TO-PRINCIPAL": "An escalate envelope, and any envelope with status escalated, is addressed to a principal.",
106
+ "HO-PAYLOAD-RETURN": "A return envelope carries a verdict.",
107
+ "HO-PAYLOAD-CHALLENGE": "A challenge envelope carries a challenge.",
108
+ "HO-PAYLOAD-ESCALATE": "An escalate envelope carries an escalation.",
109
+ "HO-PAYLOAD-DECISION-REQUEST": "A decision-request envelope carries a decision_request.",
110
+ "HO-PAYLOAD-DECISION-RECORD": "A decision-record envelope carries a decision_record.",
111
+ "HO-PAYLOAD-EXCLUSIVE": "An envelope carries no payload belonging to another message_type.",
112
+ "HO-STOPPED-HAS-REASON": "status stopped requires at least one stop reason.",
113
+ "HO-REASON-HAS-STATUS": "Any stop reason requires status stopped or escalated.",
114
+ "HO-STOP-NOTE": "Declaring conflicting-instructions requires a stop_note.",
115
+ "HO-BLOCK-AS-CHALLENGE": "A return addressed to an agent never carries verdict block: an agent's adverse finding on another agent's work travels as a challenge.",
116
+ "HO-RETURN-EVIDENCE": "A return whose verdict is not block cites at least one evidence_ref not labelled inference and lists no blocking missing evidence.",
117
+ # --- record set ---------------------------------------------------------------------------
118
+ "HO-UNIQUE-TASK-ID": "task_id is unique within the record set.",
119
+ "HO-ONE-CORRELATION": "All envelopes in the record set share one correlation_id.",
120
+ "HO-ONE-ROOT": "Exactly one envelope in the record set has hop_count 0.",
121
+ # --- chain: parent linkage, attenuation, monotonicity --------------------------------------
122
+ "HO-CHAIN-PARENT": "A non-root envelope's parent is present and hop_count equals the parent's plus one.",
123
+ "HO-CHAIN-TIME": "An envelope is not sent before its parent.",
124
+ "HO-CHAIN-CUSTODY": "An envelope is sent by its parent's destination, or by its parent's sender when the parent was stopped.",
125
+ "HO-RETURN-TO-SENDER": "A return goes back up its chain: to its parent's sender, or to the sender of an earlier delegate in its chain that was addressed to the return's sender.",
126
+ "HO-ATTEN-ACTIONS": "authorized_actions never widen from parent to child.",
127
+ "HO-ATTEN-BUDGET": "Budget limits never increase from parent to child, and the currency never changes.",
128
+ "HO-MONO-TIER": "subject_risk_tier never decreases from parent to child.",
129
+ "HO-MONO-CLASS": "data_classification never decreases from parent to child.",
130
+ "HO-MONO-MATERIALITY": "materiality_flags never lose a flag from parent to child.",
131
+ "HO-MONO-PROHIBITIONS": "do_not_do_list never loses an item from parent to child.",
132
+ "HO-MONO-DEADLINE": "deadline never extends from parent to child.",
133
+ "HO-MONO-MAX-HOPS": "max_hops never increases from parent to child.",
134
+ "HO-MONO-SPENT": "Budget spent counters are cumulative: they never decrease from parent to child.",
135
+ "HO-DESTINATION-TIER": "Every agent destination is a catalog agent whose execution_tier is static-review or read-only-runtime.",
136
+ # --- computed stop conditions -------------------------------------------------------------
137
+ "STOP-DEPTH": "A delegate at hop_count >= max_hops declares delegation-depth-exceeded and is stopped/escalated.",
138
+ "STOP-DEADLINE": "An envelope sent after its deadline declares deadline-passed and is stopped/escalated.",
139
+ "STOP-BUDGET": "Exhausted budget (delegate: spent >= limit; other: spent > limit) declares budget-exhausted and is stopped/escalated.",
140
+ "STOP-MISSING-EVIDENCE": "A delegate or decision-request with blocking missing evidence declares missing-evidence and is stopped/escalated.",
141
+ "STOP-LIVE-GUARD": "A delegate whose objective opens with a mutation verb declares live-guard-intent and is stopped/escalated.",
142
+ "STOP-INSTRUCTION-BAIT": "An objective carrying instruction-override or claimed-approval bait declares conflicting-instructions and is stopped/escalated.",
143
+ "STOP-ROUTING-LOOP": "An agent-sourced delegate to an agent already delegated to earlier in its own chain (a stopped delegate did no work and does not count) declares routing-loop and is stopped/escalated, unless it is the one revisit: the destination was delegated to exactly once before in that chain and has challenged the sender's work there.",
144
+ "STOP-CHALLENGE-ROUNDS": "A challenge beyond round 2 declares challenge-rounds-exceeded and is stopped/escalated.",
145
+ "STOP-TERMINAL": "Every child of a stopped envelope is an escalate (which HO-ESCALATE-TO-PRINCIPAL sends to a principal), and no decision-record binds a decision-request that declares a stop reason.",
146
+ # --- challenges and decision requests -----------------------------------------------------
147
+ "CH-PROPOSAL-LINK": "A challenge's proposal_task_id resolves to an envelope in the record set that precedes the challenge: a challenge answers something already sent.",
148
+ "CH-ROUND-COUNTED": "A challenge's round equals the number of delegates, returns and challenges (not stopped) that the proposer of its target had sent to the challenger before it, at least 1. Rounds bound the exchange; they never clear a finding.",
149
+ "DR-TO-PRINCIPAL": "A decision-request is addressed to a principal; agents never decide.",
150
+ "DR-PROPOSER-IS-SENDER": "A decision-request is sent by the agent it names as proposer_agent (no attributing a proposal to another agent).",
151
+ "DR-CHALLENGE-TARGETS-PROPOSAL": "Every attached challenge that is not a HARD gate points at an envelope sent by the proposer; a HARD gate is attached wherever it was raised.",
152
+ "DR-CHALLENGE-RESOLVES": "Every challenge_task_ids entry resolves to a challenge envelope in the record set.",
153
+ "DR-CHALLENGE-PRECEDES": "No attached challenge was sent after the decision-request or descends from it.",
154
+ "DR-CHALLENGE-COMPLETE": "Every challenge on the proposer's envelopes, and every HARD-gate challenge anywhere in the correlation, that precedes the request is attached; for the request in force, so is every such challenge that is not causally after every decision-record binding it, the co-signing record included.",
155
+ "DR-RISK-CARRIED": "A decision-request's subject_risk_tier is at least, and its materiality_flags include, those of every envelope in the correlation not causally after it; for the request in force, not causally after every decision-record binding it (those records aside). The decision carries the highest risk declared before it, wherever it was declared.",
156
+ "DR-ONE-REQUEST": "A correlation has one decision-request in force: any other supersedes a decision-request from the same proposer sent no later than itself, no request is superseded twice, and supersession has no cycle.",
157
+ "DR-OWNER-FROM-ROOT": "A correlation carrying a decision-request is opened by a principal whose root names a decision_owner other than itself, and every decision-request is addressed to that owner.",
158
+ "DR-CLASS-FROM-ROOT": "A decision-request's decision_class equals the decision_class declared on the root envelope.",
159
+ "DR-CHALLENGE-REQUIRED": "At R2+, with any materiality flag, or for automation-investment, at least one challenge is attached.",
160
+ "DR-DESIGNATED-CHALLENGER": "When a challenge is required, the designated challenger for the decision class sent a challenge of the proposer's own envelopes that completed, or that stopped only because the round cap was reached.",
161
+ "DR-PROPOSER-NOT-CHALLENGER": "No attached challenge was raised by the proposer.",
162
+ "DR-ALTERNATIVES": "When a challenge is required, at least one materially different alternative is recorded.",
163
+ "DR-UNRESOLVED-DISSENT": "Every attached challenge that is open (outcome sustained or unresolved, or stopped with a stop reason whatever its outcome) has a dissent entry; no agent, the challenger included, clears it by a later withdrawal.",
164
+ "DR-NO-APPROVE-OVER-DISSENT": "With any attached challenge open (sustained, unresolved or stopped), recommended_outcome is not approve.",
165
+ "DR-HARD-GATE": "With any attached HARD-gate challenge open (sustained, unresolved or stopped), recommended_outcome is reject, redesign, or escalate.",
166
+ "DR-VALUE-CASE-REQUIRED": "An automation-investment decision-request carries a value_case.",
167
+ # --- decision records (principal-sourced) -------------------------------------------------
168
+ "DRC-PRINCIPAL-SOURCE": "A decision-record is sent by a principal.",
169
+ "DRC-DECIDER-IS-SENDER": "decision_record.decided_by is the record's own sender.",
170
+ "DRC-REQUEST-BINDING": "request_task_id resolves to a decision-request and request_digest is the sha256 of its canonical JSON.",
171
+ "DRC-NOT-REQUESTER": "Neither the decider nor a named second approver is the principal who opened the correlation.",
172
+ "DRC-DECIDER-IS-ADDRESSEE": "A decider's record is decided by the principal the decision-request was addressed to.",
173
+ "DRC-AFTER-REQUEST": "A decision-record is not sent before the decision-request it binds, and that request does not descend from it.",
174
+ "DRC-ONE-RECORD": "A decision-request has at most one decider's record, and each decider's record at most one co-signing record.",
175
+ "DRC-COMPLETED": "A decision-record has status completed.",
176
+ "DRC-SECOND-APPROVER": "A decider's record sent at R5, or binding an R5 decision-request, names a second approver distinct from the decider.",
177
+ "DRC-CO-SIGNED": "The decider's record on the decision-request in force, when it names a second approver, is co-signed by that principal's own decision-record.",
178
+ "DRC-CO-SIGN-BINDING": "A co-signing record names a decider's record on the same request with the same digest and outcome, comes from that record's second approver, descends from it in the parent chain, and names no second approver itself.",
179
+ "DRC-HARD-GATE-ACCEPTANCE": "Approving a request with any attached HARD-gate challenge open (sustained, unresolved or stopped) requires risk_acceptance_ref; only a principal lifts a HARD gate.",
180
+ "DRC-APPROVAL-STATE": "approval_state is approved for approve/conditional-approve and rejected for reject/redesign.",
181
+ # --- value case ---------------------------------------------------------------------------
182
+ "VC-ORDER": "Every money range satisfies low <= point <= high.",
183
+ "VC-COST-SIGN": "No cost range (run, oversight, implementation, risk loss) is negative.",
184
+ "VC-NAV-ARITHMETIC": "net_annual_value equals benefit minus run, oversight, implementation and risk loss under interval arithmetic.",
185
+ "VC-BENEFIT-CEILING": "verified_annual_benefit.high does not exceed baseline labor/error cost plus baseline loss/leakage.",
186
+ "VC-OVERSIGHT": "A non-zero intervention or exception rate requires annual_oversight_cost.low > 0.",
187
+ "VC-RUNS-BOUND": "successful_runs does not exceed runs.",
188
+ "VC-UNIT-COST": "cost_per_successful_task_minor equals floor(annual_run_cost.point / successful_runs), or 0 with no successful runs.",
189
+ "VC-COUNTERFACTUALS": "Counterfactuals include existing-process, conventional-automation, deterministic-workflow and single-agent.",
190
+ "VC-PROMOTION": "limited-production/production requires measured confidence and net_annual_value.low above every counterfactual's low.",
191
+ "VC-SYNTHETIC": "A synthetic record never claims measured confidence or promotion beyond pilot.",
192
+ "VC-RECOMMEND-INCREMENTAL": "Recommending approve/conditional-approve on an investment requires net_annual_value.point above every counterfactual's point.",
193
+ }
194
+
195
+ _validator = None
196
+ _tiers: dict | None = None
197
+ _ecma_cache: dict[str, re.Pattern] = {}
198
+
199
+
200
+ def _ecma_pattern(validator, pattern, instance, schema):
201
+ """JSON Schema ``pattern`` with ECMA-262 semantics: a final ``$`` matches only at the end."""
202
+ if not validator.is_type(instance, "string"):
203
+ return
204
+ compiled = _ecma_cache.get(pattern)
205
+ if compiled is None:
206
+ src = pattern[:-1] + r"\Z" if pattern.endswith("$") and not pattern.endswith(r"\$") else pattern
207
+ compiled = _ecma_cache[pattern] = re.compile(src, re.ASCII)
208
+ if not compiled.search(instance):
209
+ yield jsonschema.ValidationError(f"{instance!r} does not match {pattern!r}")
210
+
211
+
212
+ def _schema_validator():
213
+ global _validator
214
+ if _validator is None:
215
+ schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
216
+ strict = jsonschema.validators.extend(jsonschema.Draft202012Validator, {"pattern": _ecma_pattern})
217
+ _validator = strict(schema)
218
+ return _validator
219
+
220
+
221
+ def _catalog_tiers() -> dict:
222
+ global _tiers
223
+ if _tiers is None:
224
+ _tiers = {a["id"]: a.get("execution_tier") for a in json.loads(CATALOG_PATH.read_text(encoding="utf-8"))}
225
+ return _tiers
226
+
227
+
228
+ def _encodable(env) -> bool:
229
+ """A lone surrogate survives json.loads but cannot be digested; treat it as a shape error."""
230
+ try:
231
+ json.dumps(env, ensure_ascii=False).encode("utf-8")
232
+ except (UnicodeEncodeError, TypeError, ValueError):
233
+ return False
234
+ return True
235
+
236
+
237
+ def _depth(value) -> int:
238
+ """Nesting depth, computed without recursion so hostile input cannot exhaust the stack."""
239
+ deepest, stack = 0, [(value, 1)]
240
+ while stack:
241
+ cur, d = stack.pop()
242
+ if isinstance(cur, (dict, list)):
243
+ deepest = max(deepest, d)
244
+ if d <= MAX_DEPTH:
245
+ stack.extend((v, d + 1) for v in (cur.values() if isinstance(cur, dict) else cur))
246
+ return deepest
247
+
248
+
249
+ def canonical_digest(envelope: dict) -> str:
250
+ """sha256 over canonical JSON: sorted keys, compact separators, UTF-8."""
251
+ blob = json.dumps(envelope, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
252
+ return "sha256:" + hashlib.sha256(blob).hexdigest()
253
+
254
+
255
+ def _party_key(party: dict) -> tuple:
256
+ return ("agent", party.get("id")) if party.get("kind") == "agent" else ("principal", party.get("subject"))
257
+
258
+
259
+ def _tier(t: str) -> int:
260
+ return TIER_ORDER.index(t)
261
+
262
+
263
+ def _cls(c: str) -> int:
264
+ return CLASS_ORDER.index(c)
265
+
266
+
267
+ def _requires_challenge(env: dict, req: dict) -> bool:
268
+ return (
269
+ _tier(env["subject_risk_tier"]) >= _tier("R2")
270
+ or bool(env["materiality_flags"])
271
+ or req["decision_class"] == "automation-investment"
272
+ )
273
+
274
+
275
+ def _budget_exhausted(env: dict) -> bool:
276
+ b = env["budget"]
277
+ if env["message_type"] == "delegate":
278
+ return any(b[f"spent_{d}"] >= b[f"limit_{d}"] for d in BUDGET_DIMENSIONS)
279
+ return any(b[f"spent_{d}"] > b[f"limit_{d}"] for d in BUDGET_DIMENSIONS)
280
+
281
+
282
+ def _stopped(env: dict, reason: str) -> bool:
283
+ return reason in env["stop_reasons"] and env["status"] in ("stopped", "escalated")
284
+
285
+
286
+ def _money_ranges(vc: dict):
287
+ for key in ("verified_annual_benefit", "annual_run_cost", "annual_oversight_cost",
288
+ "annualized_implementation_cost", "expected_risk_loss", "net_annual_value"):
289
+ yield vc[key]
290
+ for cf in vc["counterfactuals"]:
291
+ yield cf["net_annual_value"]
292
+
293
+
294
+ def _check_value_case(env: dict, vc: dict, recommended: str | None) -> set[str]:
295
+ out: set[str] = set()
296
+ if any(not (r["low"] <= r["point"] <= r["high"]) for r in _money_ranges(vc)):
297
+ out.add("VC-ORDER")
298
+ costs = COST_RANGES
299
+ if any(min(vc[c]["low"], vc[c]["point"], vc[c]["high"]) < 0 for c in costs):
300
+ out.add("VC-COST-SIGN")
301
+ ben, nav = vc["verified_annual_benefit"], vc["net_annual_value"]
302
+ exp_low = ben["low"] - sum(vc[c]["high"] for c in costs)
303
+ exp_point = ben["point"] - sum(vc[c]["point"] for c in costs)
304
+ exp_high = ben["high"] - sum(vc[c]["low"] for c in costs)
305
+ if (nav["low"], nav["point"], nav["high"]) != (exp_low, exp_point, exp_high):
306
+ out.add("VC-NAV-ARITHMETIC")
307
+ if ben["high"] > vc["baseline_labor_error_cost"] + vc["baseline_loss_leakage"]:
308
+ out.add("VC-BENEFIT-CEILING")
309
+ if (vc["intervention_rate_bp"] > 0 or vc["exception_rate_bp"] > 0) and vc["annual_oversight_cost"]["low"] <= 0:
310
+ out.add("VC-OVERSIGHT")
311
+ if vc["successful_runs"] > vc["runs"]:
312
+ out.add("VC-RUNS-BOUND")
313
+ expected_unit = vc["annual_run_cost"]["point"] // vc["successful_runs"] if vc["successful_runs"] > 0 else 0
314
+ if vc["cost_per_successful_task_minor"] != expected_unit:
315
+ out.add("VC-UNIT-COST")
316
+ kinds = {cf["kind"] for cf in vc["counterfactuals"]}
317
+ if not REQUIRED_COUNTERFACTUALS <= kinds:
318
+ out.add("VC-COUNTERFACTUALS")
319
+ cf_lows = [cf["net_annual_value"]["low"] for cf in vc["counterfactuals"]]
320
+ cf_points = [cf["net_annual_value"]["point"] for cf in vc["counterfactuals"]]
321
+ if vc["promotion_state"] in PROMOTED_STATES and (
322
+ vc["confidence"] != "measured" or (cf_lows and nav["low"] <= max(cf_lows))
323
+ ):
324
+ out.add("VC-PROMOTION")
325
+ if env["synthetic"] and (vc["confidence"] == "measured" or vc["promotion_state"] in PROMOTED_STATES):
326
+ out.add("VC-SYNTHETIC")
327
+ if recommended in ("approve", "conditional-approve") and cf_points and nav["point"] <= max(cf_points):
328
+ out.add("VC-RECOMMEND-INCREMENTAL")
329
+ return out
330
+
331
+
332
+ def _check_envelope(env: dict) -> set[str]:
333
+ """Rules that read one schema-valid envelope."""
334
+ out: set[str] = set()
335
+ mtype, src, dst = env["message_type"], env["source"], env["destination"]
336
+ if (env["parent_task_id"] is None) != (env["hop_count"] == 0):
337
+ out.add("HO-ROOT-PARENT")
338
+ if _party_key(src) == _party_key(dst):
339
+ out.add("HO-NO-SELF-ADDRESS")
340
+ if not set(env["authorized_actions"]) <= BOARD_ACTIONS:
341
+ out.add("HO-NO-EXECUTION")
342
+ if src["kind"] == "agent" and env["approval_state"] not in ("not-required", "pending-human-review"):
343
+ out.add("HO-AGENT-NO-APPROVAL")
344
+ if (src["kind"] == "principal" and mtype != "decision-record"
345
+ and env["approval_state"] not in ("not-required", "pending-human-review")):
346
+ out.add("HO-APPROVAL-ONLY-ON-RECORD")
347
+ if (mtype == "escalate" or env["status"] == "escalated") and dst["kind"] != "principal":
348
+ out.add("HO-ESCALATE-TO-PRINCIPAL")
349
+ if mtype == "return" and "verdict" in env:
350
+ if dst["kind"] == "agent" and env["verdict"]["outcome"] == "block":
351
+ out.add("HO-BLOCK-AS-CHALLENGE")
352
+ if env["verdict"]["outcome"] != "block" and (
353
+ not any(r["label"] != "inference" for r in env["evidence_refs"]) or any(g["blocking"] for g in env["missing_evidence"])
354
+ ):
355
+ out.add("HO-RETURN-EVIDENCE")
356
+ if dst["kind"] == "agent" and _catalog_tiers().get(dst["id"]) not in DISPATCHABLE_TIERS:
357
+ out.add("HO-DESTINATION-TIER")
358
+ own = PAYLOAD_BY_TYPE[mtype]
359
+ for t, key in PAYLOAD_BY_TYPE.items():
360
+ if key and t == mtype and key not in env:
361
+ out.add("HO-PAYLOAD-" + t.upper())
362
+ if any(k in env for k in PAYLOAD_KEYS - ({own} if own else set())):
363
+ out.add("HO-PAYLOAD-EXCLUSIVE")
364
+ if env["status"] == "stopped" and not env["stop_reasons"]:
365
+ out.add("HO-STOPPED-HAS-REASON")
366
+ if env["stop_reasons"] and env["status"] not in ("stopped", "escalated"):
367
+ out.add("HO-REASON-HAS-STATUS")
368
+ if "conflicting-instructions" in env["stop_reasons"] and "stop_note" not in env:
369
+ out.add("HO-STOP-NOTE")
370
+ # computed stop conditions
371
+ if mtype == "delegate" and env["hop_count"] >= env["max_hops"] and not _stopped(env, "delegation-depth-exceeded"):
372
+ out.add("STOP-DEPTH")
373
+ if env["sent_at"] > env["deadline"] and not _stopped(env, "deadline-passed"):
374
+ out.add("STOP-DEADLINE")
375
+ if _budget_exhausted(env) and not _stopped(env, "budget-exhausted"):
376
+ out.add("STOP-BUDGET")
377
+ if (mtype in ("delegate", "decision-request") and any(m["blocking"] for m in env["missing_evidence"])
378
+ and not _stopped(env, "missing-evidence")):
379
+ out.add("STOP-MISSING-EVIDENCE")
380
+ if mtype == "delegate" and LIVE_GUARD_INTENT.search(env["objective"]) and not _stopped(env, "live-guard-intent"):
381
+ out.add("STOP-LIVE-GUARD")
382
+ if INSTRUCTION_BAIT.search(env["objective"]) and not _stopped(env, "conflicting-instructions"):
383
+ out.add("STOP-INSTRUCTION-BAIT")
384
+ if (mtype == "challenge" and "challenge" in env and env["challenge"]["round"] > MAX_CHALLENGE_ROUNDS
385
+ and not _stopped(env, "challenge-rounds-exceeded")):
386
+ out.add("STOP-CHALLENGE-ROUNDS")
387
+ if mtype == "decision-request" and "decision_request" in env:
388
+ req = env["decision_request"]
389
+ if dst["kind"] != "principal":
390
+ out.add("DR-TO-PRINCIPAL")
391
+ if src["kind"] != "agent" or src["id"] != req["proposer_agent"]:
392
+ out.add("DR-PROPOSER-IS-SENDER")
393
+ if req["decision_class"] == "automation-investment" and "value_case" not in req:
394
+ out.add("DR-VALUE-CASE-REQUIRED")
395
+ if _requires_challenge(env, req) and not req["alternatives"]:
396
+ out.add("DR-ALTERNATIVES")
397
+ if "value_case" in req:
398
+ out |= _check_value_case(env, req["value_case"], req["recommended_outcome"])
399
+ if mtype == "decision-record" and "decision_record" in env:
400
+ rec = env["decision_record"]
401
+ if src["kind"] != "principal":
402
+ out.add("DRC-PRINCIPAL-SOURCE")
403
+ else:
404
+ if rec["decided_by"]["subject"] != src["subject"]:
405
+ out.add("DRC-DECIDER-IS-SENDER")
406
+ approved = rec["outcome"] in ("approve", "conditional-approve")
407
+ if env["approval_state"] != ("approved" if approved else "rejected"):
408
+ out.add("DRC-APPROVAL-STATE")
409
+ if env["status"] != "completed":
410
+ out.add("DRC-COMPLETED")
411
+ if env["subject_risk_tier"] == "R5" and "co_signs_task_id" not in rec and (
412
+ "second_approver" not in rec or rec["second_approver"]["subject"] == rec["decided_by"]["subject"]
413
+ ):
414
+ out.add("DRC-SECOND-APPROVER")
415
+ return out
416
+
417
+
418
+ def _ancestors(env: dict, by_id: dict) -> list[dict]:
419
+ chain, seen = [], set()
420
+ cur = by_id.get(env["parent_task_id"]) if env["parent_task_id"] else None
421
+ while cur is not None and cur["task_id"] not in seen:
422
+ seen.add(cur["task_id"])
423
+ chain.append(cur)
424
+ cur = by_id.get(cur["parent_task_id"]) if cur["parent_task_id"] else None
425
+ return chain
426
+
427
+
428
+ class _Order:
429
+ """Causal order over one record set: causal edges first, whole-second sent_at otherwise.
430
+
431
+ The edges are parent links plus bindings: a decision-record comes after the request it binds,
432
+ and a co-signing record after the record it co-signs. a precedes b when a reaches b through
433
+ those edges, or a was sent in an earlier second. Two envelopes sent in the same second with
434
+ no path between them are unordered: no rule may treat either one as later, so a task_id
435
+ string never decides which round came last.
436
+ """
437
+
438
+ def __init__(self, by_id: dict):
439
+ self.by_id = by_id
440
+ self._anc: dict[str, frozenset] = {}
441
+
442
+ def _preds(self, env: dict) -> list[str]:
443
+ preds = [env["parent_task_id"]] if env["parent_task_id"] else []
444
+ rec = env.get("decision_record") if env["message_type"] == "decision-record" else None
445
+ if isinstance(rec, dict):
446
+ preds.append(rec["request_task_id"])
447
+ if "co_signs_task_id" in rec:
448
+ preds.append(rec["co_signs_task_id"])
449
+ return [t for t in preds if t in self.by_id]
450
+
451
+ def ancestors(self, env: dict) -> frozenset:
452
+ key = env["task_id"]
453
+ if key not in self._anc:
454
+ seen: set[str] = set()
455
+ todo = self._preds(env)
456
+ while todo: # iterative and cycle-safe: a malformed record set cannot recurse or loop
457
+ t = todo.pop()
458
+ if t not in seen:
459
+ seen.add(t)
460
+ todo.extend(self._preds(self.by_id[t]))
461
+ self._anc[key] = frozenset(seen)
462
+ return self._anc[key]
463
+
464
+ def precedes(self, a: dict, b: dict) -> bool:
465
+ return a["task_id"] in self.ancestors(b) or a["sent_at"] < b["sent_at"]
466
+
467
+
468
+ def _check_chain(env: dict, parent: dict) -> set[str]:
469
+ out: set[str] = set()
470
+ if env["sent_at"] < parent["sent_at"]:
471
+ out.add("HO-CHAIN-TIME")
472
+ sender = _party_key(env["source"])
473
+ if sender != _party_key(parent["destination"]) and not (
474
+ parent["status"] == "stopped" and sender == _party_key(parent["source"])
475
+ ):
476
+ out.add("HO-CHAIN-CUSTODY")
477
+ if not set(env["authorized_actions"]) <= set(parent["authorized_actions"]):
478
+ out.add("HO-ATTEN-ACTIONS")
479
+ cb, pb = env["budget"], parent["budget"]
480
+ if cb["currency"] != pb["currency"] or any(cb[f"limit_{d}"] > pb[f"limit_{d}"] for d in BUDGET_DIMENSIONS):
481
+ out.add("HO-ATTEN-BUDGET")
482
+ if _tier(env["subject_risk_tier"]) < _tier(parent["subject_risk_tier"]):
483
+ out.add("HO-MONO-TIER")
484
+ if _cls(env["data_classification"]) < _cls(parent["data_classification"]):
485
+ out.add("HO-MONO-CLASS")
486
+ if not set(parent["materiality_flags"]) <= set(env["materiality_flags"]):
487
+ out.add("HO-MONO-MATERIALITY")
488
+ if not set(parent["do_not_do_list"]) <= set(env["do_not_do_list"]):
489
+ out.add("HO-MONO-PROHIBITIONS")
490
+ if env["deadline"] > parent["deadline"]:
491
+ out.add("HO-MONO-DEADLINE")
492
+ if env["max_hops"] > parent["max_hops"]:
493
+ out.add("HO-MONO-MAX-HOPS")
494
+ if any(cb[f"spent_{d}"] < pb[f"spent_{d}"] for d in BUDGET_DIMENSIONS):
495
+ out.add("HO-MONO-SPENT")
496
+ if parent["status"] == "stopped" and env["message_type"] != "escalate":
497
+ out.add("STOP-TERMINAL")
498
+ return out
499
+
500
+
501
+ def _counted_rounds(challenges: list[dict], by_id: dict, order: _Order) -> dict[str, int]:
502
+ """A round is one exchange received: the delegates, returns and challenges (not stopped) that the
503
+ proposer of a challenge's target had sent to the challenger before the challenge (at least 1).
504
+
505
+ The count is per pair of parties across the correlation, whatever role each held, so sub-questions
506
+ and a change of proposer use up rounds too. The challenger cannot raise its own round by naming
507
+ other envelopes, its own, ones it never received or another proposer's. Rounds bound the exchange
508
+ and clear nothing: an answer stopped at the cap still counts as the designated challenge and stays
509
+ open, so the principal decides.
510
+ """
511
+ delivered: dict[tuple, list[dict]] = {}
512
+ for e in by_id.values():
513
+ if e["message_type"] in ("delegate", "return", "challenge") and e["status"] != "stopped": # a stopped one was never delivered
514
+ delivered.setdefault((_party_key(e["source"]), _party_key(e["destination"])), []).append(e)
515
+ rounds: dict[str, int] = {}
516
+ for c in challenges:
517
+ target = by_id.get(c["challenge"]["proposal_task_id"])
518
+ if target is not None:
519
+ received = delivered.get((_party_key(target["source"]), _party_key(c["source"])), [])
520
+ rounds[c["task_id"]] = max(1, sum(1 for e in received if order.precedes(e, c)))
521
+ return rounds
522
+
523
+
524
+ def _routing_loop(env: dict, by_id: dict) -> bool:
525
+ """An agent re-delegating to an agent already in its chain, beyond the one bounded revisit."""
526
+ if env["message_type"] != "delegate" or env["destination"]["kind"] != "agent" or env["source"]["kind"] != "agent":
527
+ return False # a principal re-scoping the work is a human decision, not an agent loop
528
+ ancestors = _ancestors(env, by_id)
529
+ dest = env["destination"]["id"]
530
+ # A stopped delegate did no work, so after a principal cures the stop the agent may dispatch again.
531
+ earlier = [a for a in ancestors if a["message_type"] == "delegate" and a["destination"].get("id") == dest
532
+ and a["status"] != "stopped"]
533
+ if not earlier:
534
+ return False
535
+ # Round 2: the proposer takes its revised proposal back to a challenger that challenged its
536
+ # work. Counting delegates, not the targets a challenger names, allows that revisit exactly
537
+ # once (MAX_CHALLENGE_ROUNDS exchanges in all), never repeatedly.
538
+ challenged_sender = any(
539
+ a["message_type"] == "challenge" and "challenge" in a and _party_key(a["source"]) == ("agent", dest)
540
+ and a["challenge"]["proposal_task_id"] in by_id
541
+ and _party_key(by_id[a["challenge"]["proposal_task_id"]]["source"]) == _party_key(env["source"])
542
+ for a in ancestors)
543
+ return not (challenged_sender and len(earlier) < MAX_CHALLENGE_ROUNDS)
544
+
545
+
546
+ def check_record_set(doc) -> list[str]:
547
+ """Return the sorted list of violated rule ids for one record set."""
548
+ messages = doc.get("messages") if isinstance(doc, dict) else None
549
+ if not isinstance(messages, list) or not messages:
550
+ return ["HO-SCHEMA"]
551
+ validator = _schema_validator()
552
+ violations: set[str] = set()
553
+ valid: list[dict] = []
554
+ for env in messages:
555
+ if not isinstance(env, dict) or _depth(env) > MAX_DEPTH or not _encodable(env) or list(validator.iter_errors(env)):
556
+ violations.add("HO-SCHEMA")
557
+ else:
558
+ valid.append(env)
559
+ if not valid:
560
+ return sorted(violations)
561
+
562
+ ids = [e["task_id"] for e in valid]
563
+ if len(ids) != len(set(ids)):
564
+ violations.add("HO-UNIQUE-TASK-ID")
565
+ if len({e["correlation_id"] for e in valid}) != 1:
566
+ violations.add("HO-ONE-CORRELATION")
567
+ roots = [e for e in valid if e["hop_count"] == 0]
568
+ if len(roots) != 1:
569
+ violations.add("HO-ONE-ROOT")
570
+ by_id = {e["task_id"]: e for e in valid}
571
+ root = roots[0] if len(roots) == 1 else None
572
+ order = _Order(by_id)
573
+
574
+ challenges = [e for e in valid if e["message_type"] == "challenge" and "challenge" in e]
575
+ requests = [e for e in valid if e["message_type"] == "decision-request" and "decision_request" in e]
576
+ records = [e for e in valid if e["message_type"] == "decision-record" and "decision_record" in e]
577
+ rounds = _counted_rounds(challenges, by_id, order)
578
+ superseded = {r["decision_request"]["supersedes_task_id"] for r in requests if "supersedes_task_id" in r["decision_request"]}
579
+ ctx = {"by_id": by_id, "order": order, "superseded": superseded, "challenges": challenges,
580
+ "records": records, "root": root, "live": {},
581
+ "cosigners": {(r["decision_record"]["co_signs_task_id"], r["decision_record"]["decided_by"]["subject"])
582
+ for r in records if "co_signs_task_id" in r["decision_record"]}}
583
+
584
+ for env in valid:
585
+ violations |= _check_envelope(env)
586
+ parent = by_id.get(env["parent_task_id"]) if env["parent_task_id"] else None
587
+ if env["hop_count"] > 0:
588
+ if parent is None or env["hop_count"] != parent["hop_count"] + 1:
589
+ violations.add("HO-CHAIN-PARENT")
590
+ if parent is not None:
591
+ violations |= _check_chain(env, parent)
592
+ if _routing_loop(env, by_id) and not _stopped(env, "routing-loop"):
593
+ violations.add("STOP-ROUTING-LOOP")
594
+ if env["message_type"] == "return" and parent is not None:
595
+ # Back up the chain: to the parent's sender, or to whoever delegated to this sender earlier.
596
+ askers = {_party_key(parent["source"])} | {
597
+ _party_key(a["source"]) for a in _ancestors(env, by_id)
598
+ if a["message_type"] == "delegate" and _party_key(a["destination"]) == _party_key(env["source"])}
599
+ if _party_key(env["destination"]) not in askers:
600
+ violations.add("HO-RETURN-TO-SENDER")
601
+ if env["message_type"] == "challenge" and "challenge" in env:
602
+ target = by_id.get(env["challenge"]["proposal_task_id"])
603
+ if target is None or not order.precedes(target, env):
604
+ violations.add("CH-PROPOSAL-LINK")
605
+ elif env["challenge"]["round"] != rounds[env["task_id"]]:
606
+ violations.add("CH-ROUND-COUNTED")
607
+ if env["message_type"] == "decision-request" and "decision_request" in env:
608
+ violations |= _check_decision_request_links(env, ctx)
609
+ if root is not None and root.get("decision_class") != env["decision_request"]["decision_class"]:
610
+ violations.add("DR-CLASS-FROM-ROOT")
611
+ if root is not None and (
612
+ root["source"]["kind"] != "principal" or "decision_owner" not in root
613
+ or root["decision_owner"]["subject"] == root["source"]["subject"]
614
+ or (env["destination"]["kind"] == "principal" # an agent addressee is DR-TO-PRINCIPAL's
615
+ and env["destination"]["subject"] != root["decision_owner"]["subject"])
616
+ ):
617
+ violations.add("DR-OWNER-FROM-ROOT")
618
+ if env["message_type"] == "decision-record" and "decision_record" in env:
619
+ violations |= _check_decision_record_links(env, ctx)
620
+
621
+ violations |= _check_supersession(requests, by_id, order)
622
+
623
+ # One decider's record per request, one co-signature per decider's record.
624
+ primaries = Counter(r["decision_record"]["request_task_id"] for r in records if "co_signs_task_id" not in r["decision_record"])
625
+ cosigns = Counter(r["decision_record"]["co_signs_task_id"] for r in records if "co_signs_task_id" in r["decision_record"])
626
+ if any(n > 1 for n in primaries.values()) or any(n > 1 for n in cosigns.values()):
627
+ violations.add("DRC-ONE-RECORD")
628
+ return sorted(violations)
629
+
630
+
631
+ def _check_supersession(requests: list[dict], by_id: dict, order: _Order) -> set[str]:
632
+ """One decision-request in force: supersession forms one chain back to the original request."""
633
+ if len([r for r in requests if "supersedes_task_id" not in r["decision_request"]]) > 1:
634
+ return {"DR-ONE-REQUEST"}
635
+ targets = [r["decision_request"]["supersedes_task_id"] for r in requests if "supersedes_task_id" in r["decision_request"]]
636
+ if len(targets) != len(set(targets)):
637
+ return {"DR-ONE-REQUEST"}
638
+ for r in requests:
639
+ if "supersedes_task_id" not in r["decision_request"]:
640
+ continue
641
+ old = by_id.get(r["decision_request"]["supersedes_task_id"])
642
+ if (old is None or old is r or "decision_request" not in old or old["message_type"] != "decision-request"
643
+ or order.precedes(r, old) # a request supersedes only one sent no later than itself
644
+ or old["decision_request"]["proposer_agent"] != r["decision_request"]["proposer_agent"]):
645
+ return {"DR-ONE-REQUEST"}
646
+ cur = r # walk back to the original request; a walk longer than the request count is a cycle
647
+ for _ in range(len(requests)):
648
+ nxt = by_id.get(cur["decision_request"]["supersedes_task_id"])
649
+ if nxt is None or "decision_request" not in nxt or "supersedes_task_id" not in nxt["decision_request"]:
650
+ break
651
+ cur = nxt
652
+ else:
653
+ return {"DR-ONE-REQUEST"}
654
+ return set()
655
+
656
+
657
+ def _attached_challenges(req: dict, by_id: dict) -> list[dict]:
658
+ return [
659
+ by_id[t] for t in req["challenge_task_ids"]
660
+ if t in by_id and by_id[t]["message_type"] == "challenge" and "challenge" in by_id[t]
661
+ ]
662
+
663
+
664
+ def _open(ch: dict) -> bool:
665
+ # A stopped challenge resolved nothing, whatever outcome it records; only a principal resolves it.
666
+ return ch["challenge"]["outcome"] in ("sustained", "unresolved") or bool(ch["stop_reasons"])
667
+
668
+
669
+ def _live(request: dict, ctx: dict) -> list[dict]:
670
+ """Attached challenges that are open: outcome sustained or unresolved, or stopped.
671
+
672
+ No agent clears a challenge, not even its own challenger: a later withdrawal is recorded beside
673
+ the finding, never instead of it. Dissent is preserved, and a HARD gate stays live until a
674
+ principal's decision-record carries risk_acceptance_ref. Every earlier definition of a "later
675
+ round" that let a challenger clear its own finding had a carrier an adversary controlled, so the
676
+ clearing path is gone and rounds only bound how many exchanges happen.
677
+ """
678
+ key = request["task_id"]
679
+ if key not in ctx["live"]:
680
+ ctx["live"][key] = [c for c in _attached_challenges(request["decision_request"], ctx["by_id"]) if _open(c)]
681
+ return ctx["live"][key]
682
+
683
+
684
+ def _on_proposer(ch: dict, proposer: str, by_id: dict) -> bool:
685
+ target = by_id.get(ch["challenge"]["proposal_task_id"])
686
+ return target is not None and target["source"]["kind"] == "agent" and target["source"]["id"] == proposer
687
+
688
+
689
+ def _check_decision_request_links(env: dict, ctx: dict) -> set[str]:
690
+ out: set[str] = set()
691
+ by_id, order = ctx["by_id"], ctx["order"]
692
+ req = env["decision_request"]
693
+ attached = _attached_challenges(req, by_id)
694
+ if any(order.precedes(env, c) for c in attached):
695
+ out.add("DR-CHALLENGE-PRECEDES")
696
+ # Completeness: the principal sees every challenge raised on the proposal before the request.
697
+ # The request in force is also held to every challenge up to its decision, so a HARD gate cannot
698
+ # be dropped by leaving it unattached or by deciding before it arrives; a superseded request is
699
+ # judged as of its own sending, so re-asking with the late challenge attached cures it.
700
+ # A HARD gate is owed wherever it was raised, so a change of proposer cannot leave one behind.
701
+ if env["task_id"] in ctx["superseded"]:
702
+ owed = [c for c in ctx["challenges"] if order.precedes(c, env)]
703
+ else:
704
+ points = [env] + [r for r in ctx["records"] if r["decision_record"]["request_task_id"] == env["task_id"]]
705
+ owed = [c for c in ctx["challenges"] if not all(order.precedes(x, c) for x in points)]
706
+ if any((c["challenge"]["hard_gate"] or _on_proposer(c, req["proposer_agent"], by_id))
707
+ and c["task_id"] not in req["challenge_task_ids"] for c in owed):
708
+ out.add("DR-CHALLENGE-COMPLETE")
709
+ # The decision carries the highest risk declared before it anywhere in the correlation (a same-second
710
+ # envelope counts), so no branch, re-ask or re-tiering can ask a lower-stakes question than the one raised.
711
+ binding = [r for r in ctx["records"] if r["decision_record"]["request_task_id"] == env["task_id"]]
712
+ points = [env] if env["task_id"] in ctx["superseded"] else [env] + binding
713
+ carried = [e for e in by_id.values() if e is not env and not any(e is r for r in binding)
714
+ and not all(order.precedes(x, e) for x in points)]
715
+ if any(_tier(e["subject_risk_tier"]) > _tier(env["subject_risk_tier"])
716
+ or not set(e["materiality_flags"]) <= set(env["materiality_flags"]) for e in carried):
717
+ out.add("DR-RISK-CARRIED")
718
+ if len(attached) != len(req["challenge_task_ids"]):
719
+ out.add("DR-CHALLENGE-RESOLVES")
720
+ if _requires_challenge(env, req) and not req["challenge_task_ids"]:
721
+ out.add("DR-CHALLENGE-REQUIRED")
722
+ elif _requires_challenge(env, req):
723
+ primary, fallback = DESIGNATED_CHALLENGERS[req["decision_class"]]
724
+ designated = fallback if primary == req["proposer_agent"] else primary
725
+ challengers = {c["source"].get("id") for c in attached
726
+ if c["source"]["kind"] == "agent" and _on_proposer(c, req["proposer_agent"], by_id)
727
+ and (c["status"] == "completed" or c["stop_reasons"] == ["challenge-rounds-exceeded"])}
728
+ if designated == req["proposer_agent"] or designated not in challengers:
729
+ out.add("DR-DESIGNATED-CHALLENGER")
730
+ if any(c["source"].get("id") == req["proposer_agent"] for c in attached):
731
+ out.add("DR-PROPOSER-NOT-CHALLENGER")
732
+ for c in attached:
733
+ target = by_id.get(c["challenge"]["proposal_task_id"])
734
+ if (target is not None and not c["challenge"]["hard_gate"]
735
+ and not (target["source"]["kind"] == "agent" and target["source"]["id"] == req["proposer_agent"])):
736
+ out.add("DR-CHALLENGE-TARGETS-PROPOSAL")
737
+ live = _live(env, ctx)
738
+ dissent_ids = {d["challenge_task_id"] for d in req["dissent"]}
739
+ if any(c["task_id"] not in dissent_ids for c in live):
740
+ out.add("DR-UNRESOLVED-DISSENT")
741
+ if live and req["recommended_outcome"] == "approve":
742
+ out.add("DR-NO-APPROVE-OVER-DISSENT")
743
+ if any(c["challenge"]["hard_gate"] for c in live) and req["recommended_outcome"] not in (
744
+ "reject", "redesign", "escalate"
745
+ ):
746
+ out.add("DR-HARD-GATE")
747
+ return out
748
+
749
+
750
+ def _check_decision_record_links(env: dict, ctx: dict) -> set[str]:
751
+ out: set[str] = set()
752
+ by_id, root = ctx["by_id"], ctx["root"]
753
+ rec = env["decision_record"]
754
+ cosign = "co_signs_task_id" in rec
755
+ request = by_id.get(rec["request_task_id"])
756
+ if (request is None or request["message_type"] != "decision-request"
757
+ or rec["request_digest"] != canonical_digest(request)):
758
+ out.add("DRC-REQUEST-BINDING")
759
+ if root is not None and root["source"]["kind"] == "principal" and root["source"]["subject"] in (
760
+ rec["decided_by"]["subject"], rec.get("second_approver", {}).get("subject")
761
+ ):
762
+ out.add("DRC-NOT-REQUESTER")
763
+ if request is not None:
764
+ if env["sent_at"] < request["sent_at"] or env["task_id"] in ctx["order"].ancestors(request):
765
+ out.add("DRC-AFTER-REQUEST")
766
+ if request["stop_reasons"]: # stopped or escalated: a request that met a stop condition is not decidable
767
+ out.add("STOP-TERMINAL")
768
+ if request is not None and "decision_request" in request and not cosign:
769
+ dst = request["destination"]
770
+ if dst["kind"] == "principal" and rec["decided_by"]["subject"] != dst["subject"]:
771
+ out.add("DRC-DECIDER-IS-ADDRESSEE")
772
+ if request["subject_risk_tier"] == "R5" and (
773
+ "second_approver" not in rec or rec["second_approver"]["subject"] == rec["decided_by"]["subject"]
774
+ ):
775
+ out.add("DRC-SECOND-APPROVER")
776
+ hard_open = any(c["challenge"]["hard_gate"] for c in _live(request, ctx))
777
+ if hard_open and rec["outcome"] in ("approve", "conditional-approve") and "risk_acceptance_ref" not in rec:
778
+ out.add("DRC-HARD-GATE-ACCEPTANCE")
779
+ if cosign:
780
+ target = by_id.get(rec["co_signs_task_id"])
781
+ trec = target.get("decision_record") if target is not None and target["message_type"] == "decision-record" else None
782
+ if (trec is None or "co_signs_task_id" in trec or "second_approver" in rec
783
+ or (trec["request_task_id"], trec["request_digest"], trec["outcome"])
784
+ != (rec["request_task_id"], rec["request_digest"], rec["outcome"])
785
+ or trec.get("second_approver", {}).get("subject") != rec["decided_by"]["subject"]
786
+ or env["sent_at"] < target["sent_at"] or all(a is not target for a in _ancestors(env, by_id))):
787
+ out.add("DRC-CO-SIGN-BINDING")
788
+ elif ("second_approver" in rec and rec["second_approver"]["subject"] != rec["decided_by"]["subject"]
789
+ and rec["request_task_id"] not in ctx["superseded"] # a superseded request's record is void, not owed a co-sign
790
+ and (env["task_id"], rec["second_approver"]["subject"]) not in ctx["cosigners"]):
791
+ out.add("DRC-CO-SIGNED")
792
+ return out
793
+
794
+
795
+ def _reject_duplicate_keys(pairs: list) -> dict:
796
+ keys = [k for k, _ in pairs]
797
+ if len(keys) != len(set(keys)):
798
+ raise ValueError("duplicate object key")
799
+ return dict(pairs)
800
+
801
+
802
+ def _reject_constant(name: str):
803
+ raise ValueError(f"{name} is not JSON")
804
+
805
+
806
+ def load_json(path: str):
807
+ """Strict load: duplicate keys, NaN and Infinity are errors (each would make the digest ambiguous).
808
+
809
+ Raises ValueError (including JSONDecodeError), UnicodeDecodeError, RecursionError or OSError.
810
+ """
811
+ return json.loads(Path(path).read_text(encoding="utf-8"), object_pairs_hook=_reject_duplicate_keys,
812
+ parse_constant=_reject_constant)
813
+
814
+
815
+ MALFORMED = (ValueError, UnicodeDecodeError, RecursionError)
816
+
817
+
818
+ def main(argv: list[str]) -> int:
819
+ if not argv or argv == ["--digest"]:
820
+ print(__doc__.strip().splitlines()[-3])
821
+ print(__doc__.strip().splitlines()[-1])
822
+ return 2
823
+ if argv[0] == "--digest":
824
+ for path in argv[1:]:
825
+ try:
826
+ envelope = load_json(path)
827
+ except OSError as exc:
828
+ print(f"{path}: cannot read ({exc.strerror or exc})", file=sys.stderr)
829
+ return 2
830
+ except MALFORMED:
831
+ print(f"{path}: not strict JSON", file=sys.stderr)
832
+ return 2
833
+ if (not isinstance(envelope, dict) or envelope.get("message_type") != "decision-request"
834
+ or _depth(envelope) > MAX_DEPTH or not _encodable(envelope)
835
+ or list(_schema_validator().iter_errors(envelope))):
836
+ print(f"{path}: not a schema-valid decision-request envelope", file=sys.stderr)
837
+ return 2
838
+ print(canonical_digest(envelope))
839
+ return 0
840
+ failed, unreadable = False, False
841
+ for path in argv:
842
+ try:
843
+ rules = check_record_set(load_json(path))
844
+ except OSError as exc:
845
+ print(f"{path}: cannot read ({exc.strerror or exc})", file=sys.stderr)
846
+ unreadable = True
847
+ continue
848
+ except MALFORMED:
849
+ rules = ["HO-SCHEMA"]
850
+ for rule in rules:
851
+ failed = True
852
+ print(f"{path}: {rule}")
853
+ return 2 if unreadable else 1 if failed else 0
854
+
855
+
856
+ if __name__ == "__main__":
857
+ sys.exit(main(sys.argv[1:]))