oacp-cli 0.2.2__tar.gz → 0.2.3__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 (118) hide show
  1. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/CHANGELOG.md +12 -0
  2. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/PKG-INFO +2 -2
  3. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/README.md +1 -1
  4. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/runtime_capability_matrix.md +28 -16
  5. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/pyproject.toml +1 -1
  6. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/oacp_watch.py +95 -16
  7. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_watch.py +217 -6
  8. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/.github/workflows/ci.yml +0 -0
  9. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/.github/workflows/release.yml +0 -0
  10. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/.gitignore +0 -0
  11. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/CONTRIBUTING.md +0 -0
  12. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/LICENSE +0 -0
  13. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/Makefile +0 -0
  14. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/QUICKSTART.md +0 -0
  15. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/SECURITY.md +0 -0
  16. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/SPEC.md +0 -0
  17. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/adoption.md +0 -0
  18. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/doctor.md +0 -0
  19. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/prompt_caching.md +0 -0
  20. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/setup.md +0 -0
  21. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/unified_skill_spec.md +0 -0
  22. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/versioning.md +0 -0
  23. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/agent_profiles.md +0 -0
  24. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/agent_safety_defaults.md +0 -0
  25. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/credential_scoping.md +0 -0
  26. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/cross_runtime_sync.md +0 -0
  27. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/dispatch_states.yaml +0 -0
  28. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/inbox_outbox.md +0 -0
  29. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/mcp_integration.md +0 -0
  30. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/multi_agent_shared_workspace.md +0 -0
  31. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/org_memory.md +0 -0
  32. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/packet_states.yaml +0 -0
  33. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/review_loop.md +0 -0
  34. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/runtime_capabilities.md +0 -0
  35. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/session_init.md +0 -0
  36. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/skills_manifest.yaml +0 -0
  37. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/task_negotiation.md +0 -0
  38. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/examples/quickstart/README.md +0 -0
  39. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/mcp_servers/oacp_coordinator.py +0 -0
  40. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/oacp/__init__.py +0 -0
  41. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/oacp/cli.py +0 -0
  42. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/_oacp_constants.py +0 -0
  43. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/_oacp_env.py +0 -0
  44. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/add_agent.py +0 -0
  45. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/agent_profile.py +0 -0
  46. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/check_quality_gate.py +0 -0
  47. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/codex_session_init.py +0 -0
  48. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/create_handoff_packet.py +0 -0
  49. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/handoff_schema.py +0 -0
  50. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/init_org_memory.py +0 -0
  51. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/init_packet.sh +0 -0
  52. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/init_project_workspace.py +0 -0
  53. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/init_project_workspace.sh +0 -0
  54. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/memory_archive_common.py +0 -0
  55. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/memory_cli.py +0 -0
  56. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/normalize_findings.py +0 -0
  57. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/oacp_doctor.py +0 -0
  58. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/oacp_inbox.py +0 -0
  59. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/preflight.py +0 -0
  60. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/promote_to_archive.py +0 -0
  61. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/restore_from_archive.py +0 -0
  62. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/send_inbox_message.py +0 -0
  63. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/session_lifecycle_hooks.py +0 -0
  64. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/setup_runtime.py +0 -0
  65. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/update_workspace.sh +0 -0
  66. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/validate_agent_card.py +0 -0
  67. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/validate_message.py +0 -0
  68. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/write_event.py +0 -0
  69. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/agent_card.template.yaml +0 -0
  70. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/agent_profile.template.yaml +0 -0
  71. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/agent_status.template.yaml +0 -0
  72. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/checkpoint.template.md +0 -0
  73. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/claude/agents/role_agent.template.md +0 -0
  74. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/claude/rules/guardrail.template.md +0 -0
  75. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/findings_packet.template.yaml +0 -0
  76. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/github_actions_quality_gate.yaml +0 -0
  77. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/guardrails/coding_standards.template.md +0 -0
  78. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/guardrails/safe_commands.template.md +0 -0
  79. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/guardrails/secrets_rules.template.md +0 -0
  80. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/handoff_packet.template.yaml +0 -0
  81. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/inbox_message.template.yaml +0 -0
  82. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/manual_validation.template.md +0 -0
  83. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/merge_decision.template.md +0 -0
  84. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/org-memory/decisions.md +0 -0
  85. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/org-memory/events/.gitkeep +0 -0
  86. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/org-memory/events/20260317-170120-example-api-convention.md +0 -0
  87. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/org-memory/recent.md +0 -0
  88. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/org-memory/rules.md +0 -0
  89. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/review_packet.template.md +0 -0
  90. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/roles/role_baseline.template.md +0 -0
  91. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/roles/role_definition.template.yaml +0 -0
  92. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/runtime_capabilities.yaml +0 -0
  93. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/skills_manifest.template.yaml +0 -0
  94. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/test_packet.template.md +0 -0
  95. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_add_agent.py +0 -0
  96. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_agent_profile.py +0 -0
  97. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_codex_session_init.py +0 -0
  98. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_create_handoff_packet.py +0 -0
  99. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_github_workflows.py +0 -0
  100. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_handoff_schema.py +0 -0
  101. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_init_project_workspace.py +0 -0
  102. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_memory_archive.py +0 -0
  103. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_cli.py +0 -0
  104. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_constants.py +0 -0
  105. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_coordinator.py +0 -0
  106. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_doctor.py +0 -0
  107. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_env.py +0 -0
  108. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_inbox.py +0 -0
  109. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_preflight.py +0 -0
  110. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_review_loop.py +0 -0
  111. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_send_inbox_message.py +0 -0
  112. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_session_lifecycle_hooks.py +0 -0
  113. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_setup_runtime.py +0 -0
  114. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_update_workspace.py +0 -0
  115. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_validate_agent_card.py +0 -0
  116. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_validate_message.py +0 -0
  117. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_workspace_discovery.py +0 -0
  118. {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_write_event.py +0 -0
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.3] - 2026-04-26
9
+
10
+ ### Changed
11
+
12
+ - `oacp watch` defaults are now notification-friendly: existing inbox messages are no longer replayed on the first scan for a target, and `message_archived` events are suppressed by default. Pass `--since=epoch` to restore replay and `--show-archived` to re-enable archive events. Reduces noise for `Monitor` and `oacp watch` consumers.
13
+
14
+ ### Added
15
+
16
+ - `oacp watch --since=<spec>` — controls the first-run baseline cutoff. Accepts `now` (default), `epoch`, relative durations (`30s`, `5m`, `2h`, `7d`), or ISO 8601 timestamps. Only applies on first run for a target (no state file yet).
17
+ - `oacp watch --show-archived` — opt-in flag to emit `message_archived` events. Useful for observer agents tracking another agent's inbox; disabled by default because the watching agent's own deletes are self-loops.
18
+
8
19
  ## [0.2.2] - 2026-04-17
9
20
 
10
21
  ### Added
@@ -110,6 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
110
121
  - Checkout step in github-release workflow job (#19)
111
122
  - Pre-release audit fixes: SHA-pinned actions, dangling doc refs (#15, #16)
112
123
 
124
+ [0.2.3]: https://github.com/kiloloop/oacp/compare/v0.2.2...v0.2.3
113
125
  [0.2.2]: https://github.com/kiloloop/oacp/compare/v0.2.1...v0.2.2
114
126
  [0.2.1]: https://github.com/kiloloop/oacp/compare/v0.2.0...v0.2.1
115
127
  [0.2.0]: https://github.com/kiloloop/oacp/compare/v0.1.9...v0.2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oacp-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Open Agent Coordination Protocol CLI for file-based multi-agent workflows
5
5
  Project-URL: Homepage, https://github.com/kiloloop/oacp
6
6
  Project-URL: Repository, https://github.com/kiloloop/oacp
@@ -165,7 +165,7 @@ uv tool install .
165
165
 
166
166
  **`oacp send`**: `--in-reply-to`, `--expires`, `--body-file`, `--channel`, `--dry-run`, `--json`, `--quiet`
167
167
 
168
- **`oacp watch`**: `--agent`, repeatable `--project`, `--all-projects`, `--json`
168
+ **`oacp watch`**: `--agent`, repeatable `--project`, `--all-projects`, `--json`, `--since` (default `now`), `--show-archived`
169
169
 
170
170
  **`oacp doctor`**: `--fix` (auto-fix safe issues), `--json`, `-o/--output`
171
171
 
@@ -139,7 +139,7 @@ uv tool install .
139
139
 
140
140
  **`oacp send`**: `--in-reply-to`, `--expires`, `--body-file`, `--channel`, `--dry-run`, `--json`, `--quiet`
141
141
 
142
- **`oacp watch`**: `--agent`, repeatable `--project`, `--all-projects`, `--json`
142
+ **`oacp watch`**: `--agent`, repeatable `--project`, `--all-projects`, `--json`, `--since` (default `now`), `--show-archived`
143
143
 
144
144
  **`oacp doctor`**: `--fix` (auto-fix safe issues), `--json`, `-o/--output`
145
145
 
@@ -1,10 +1,12 @@
1
1
  # Cross-Runtime Parity Matrix
2
2
 
3
- **Date**: 2026-04-17
3
+ **Date**: 2026-04-24
4
4
 
5
5
  This is a capability comparison across 3 agent runtimes (Claude Code, Codex, Gemini), compiled from each runtime's self-report and current runtime changelogs. Use this as a reference when deciding which agent to assign for a given task.
6
6
 
7
- Codex was last checked against the official Codex changelog entries for app update `26.415` (2026-04-16) and `Codex CLI 0.121.0` (2026-04-15).
7
+ Claude was last checked against Claude Code `v2.1.118`.
8
+
9
+ Codex was last checked against app update `26.415`, CLI `0.123.0`, and OpenAI's GPT-5.5 launch note from 2026-04-23.
8
10
 
9
11
  ---
10
12
 
@@ -15,31 +17,32 @@ Codex was last checked against the official Codex changelog entries for app upda
15
17
  | Spawn background tasks | Yes — Task tool + Bash `run_in_background` | Yes — shell background processes | Yes — `run_command` async mode |
16
18
  | Spawn subagents | Yes — typed agents (Explore, Plan, general-purpose, code-reviewer, etc.) | Yes — native `spawn_agent` lifecycle with `default`, `explorer`, and `worker` agents | Partial — `browser_subagent` only |
17
19
  | Parallel agent teams | Yes — TeamCreate, task lists, SendMessage, broadcast | Partial — parallel spawned agents are supported, but there is no team/task-list primitive | No — parallel tool calls but no independent agent instances |
18
- | MCP tools | Yes — extensible via MCP servers | Yes — MCP/plugin support depends on configured servers and marketplaces | Yes — MCP server support |
20
+ | MCP tools | Yes — extensible via MCP servers | Yes — MCP/plugin support with `/mcp verbose` diagnostics; configuration-dependent | Yes — MCP server support |
19
21
  | Web search | Yes — native WebSearch tool | Yes — web search/fetch tools | Yes — native `search_web` tool |
20
22
  | Browser interaction | Partial — WebFetch (read-only, HTML→markdown) | Partial — web tools plus early in-app browser for local/public pages without sign-in | Yes — full browser control (click, type, navigate, screenshot, video) |
21
- | File system access | Sandboxed — configurable read/write allowlists | Policy-dependent per session | Full — unrestricted |
23
+ | File system access | Sandboxed — configurable read/write allowlists | Policy-dependent per session; sandbox profiles can include deny-read rules | Full — unrestricted |
22
24
  | Git operations | Yes — via Bash (may need sandbox configuration) | Yes — native | Yes — via shell |
23
25
  | GitHub CLI (gh) | Yes — via Bash (may need sandbox configuration) | Yes — authenticated | Yes — native |
24
26
  | Session memory | Strong — auto-loaded MEMORY.md + optional MCP memory | Partial — app memories where available plus OACP file memory; app memories are not protocol SSOT | Partial — Knowledge Items (not directly writable), conversation logs |
25
- | Interactive mode | Yes — CLI chat with permissions, plan mode | Yes — desktop app | Yes — chat with task UI, artifacts |
26
- | Context window | ~1M with Opus 4.7 (auto-compaction extends indefinitely) | Not directly exposed; practically finite | ~1M tokens |
27
- | Cost model | Token-based, visible in statusline | Not surfaced in runtime | Token-based |
28
- | Sandbox restrictions | Yes — configurable allowlists | Session-dependent | None — full system access |
27
+ | Interactive mode | Yes — CLI chat with permissions, plan mode | Yes — desktop app and CLI/TUI, including Plan Mode and side conversations | Yes — chat with task UI, artifacts |
28
+ | Context window | ~1M with Opus 4.7 (auto-compaction extends indefinitely) | Model-dependent; GPT-5.5 in Codex is documented at 400K, with no auto-compaction guarantee | ~1M tokens |
29
+ | Cost model | Token-based, visible in statusline | Not surfaced per session; GPT-5.5 Fast mode trades 2.5x cost for 1.5x token generation speed | Token-based |
30
+ | Sandbox restrictions | Yes — configurable allowlists | Session-dependent; supports deny-read policies, isolated `codex exec`, and remote sandbox requirements | None — full system access |
29
31
 
30
32
  ---
31
33
 
32
- ## 2. Unique Capabilities (Only One Runtime Has)
34
+ ## 2. Distinctive Capabilities
33
35
 
34
36
  | Capability | Runtime | Details |
35
37
  | -------------------------------- | ------------- | ---------------------------------------------------------------------------- |
36
38
  | Typed subagent orchestration | Claude | Multiple agent types with scoped tools and model selection |
37
39
  | Team coordination primitive | Claude | TeamCreate + task lists + assignment + broadcast + shutdown |
38
- | Plan mode | Claude | Structured explore → plan → approve → implement workflow |
40
+ | Plan mode | Claude, Codex | Claude has structured explore → plan → approve → implement; Codex CLI can move from planning into fresh-context implementation |
39
41
  | Auto-compaction | Claude | Context auto-compresses, enabling unlimited session length |
40
42
  | Cross-session semantic search | Claude | MCP-based searchable memory (optional) |
41
43
  | Browser automation (full) | Gemini | Click, type, navigate, screenshot, WebP video recording |
42
- | Image generation | Gemini | Native `generate_image` tool |
44
+ | GPT-5.5 model availability | Codex | Available in Codex for Plus, Pro, Business, Enterprise, Edu, and Go plans with a 400K context window |
45
+ | Image generation | Codex, Gemini | Codex CLI image generation is enabled by default; Gemini has native `generate_image` |
43
46
  | URL content reading (no browser) | Gemini | `read_url_content` fetches HTML→markdown or PDF directly |
44
47
  | Code outline navigation | Gemini | `view_file_outline`, `view_code_item` for structured exploration |
45
48
  | PTY / terminal stdin | Codex, Gemini | Codex: native PTY; Gemini: `send_command_input` (Claude lacks stdin support) |
@@ -68,9 +71,9 @@ Scope: skills shipped in [`kiloloop/oacp-skills`](https://github.com/kiloloop/oa
68
71
 
69
72
  | Dimension | Claude | Codex | Gemini |
70
73
  | --------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
71
- | Best at | Orchestration, multi-agent teams, persistent memory, plan-then-execute | Terminal-native execution, fast iterative patching, app-assisted PR/artifact review, protocol discipline | Web research, browser automation, visual verification, large context |
72
- | Ideal task type | Team coordination, complex multi-file refactors, long-running sessions | Shell-heavy workflows, targeted file edits, deterministic scripts, PR follow-up, artifact review | External research, UI testing, document review, MCP integrations |
73
- | Cost profile | Flexible (haiku subagents for cheap tasks, opus for complex) | Not directly visible | Token-based, web search has additional costs |
74
+ | Best at | Orchestration, multi-agent teams, persistent memory, plan-then-execute | Terminal-native execution, GPT-5.5 agentic coding, fast iterative patching, plan-to-implementation handoff, app-assisted PR/artifact review, protocol discipline | Web research, browser automation, visual verification, large context |
75
+ | Ideal task type | Team coordination, complex multi-file refactors, long-running sessions | Shell-heavy workflows, long-horizon coding, targeted file edits, deterministic scripts, PR follow-up, artifact review, CLI planning passes | External research, UI testing, document review, MCP integrations |
76
+ | Cost profile | Flexible (haiku subagents for cheap tasks, opus for complex) | Per-session cost not visible; GPT-5.5 is described as more token-efficient than GPT-5.4 for Codex tasks | Token-based, web search has additional costs |
74
77
 
75
78
  ---
76
79
 
@@ -80,11 +83,11 @@ Scope: skills shipped in [`kiloloop/oacp-skills`](https://github.com/kiloloop/oa
80
83
  | ----------------------------- | ----------------------------- | -------------------------------------------------------------- | ---------------------- |
81
84
  | No subagents | — | Yes | Partial (browser only) |
82
85
  | No browser automation | Yes (read-only) | Partial (in-app browser is not full automation and excludes sign-in flows) | — |
83
- | No image generation | Yes | Yes | — |
86
+ | No image generation | Yes | | — |
84
87
  | No persistent writable memory | — | Partial (app memories are not a replacement for OACP durable memory) | Yes |
85
88
  | Sandbox friction | Yes (configurable) | Session-dependent | — |
86
89
  | No team primitive | — | Yes | Yes |
87
- | Context limits | Auto-compaction mitigates | Yes (no compaction) | Large but finite |
90
+ | Context limits | Auto-compaction mitigates | Model-dependent; GPT-5.5 in Codex is 400K, but there is no documented auto-compaction behavior | Large but finite |
88
91
  | No terminal stdin | Yes | — | — |
89
92
  | Cost not surfaced | — | Yes | — |
90
93
 
@@ -110,6 +113,7 @@ These are the highest-impact gaps where one runtime's limitation blocks effectiv
110
113
  | Dimension | Claude | Codex | Gemini |
111
114
  | ---------------------------- | -------------------------------- | ------------------------------------------ | --------------------------------------------- |
112
115
  | Max parallel tool calls | ~10+ | Yes (parallel independent calls) | ~10 (practical) |
116
+ | Side conversations | No | Yes (`/side` in CLI/TUI) | No |
113
117
  | Hooks system | Yes (pre/post tool call hooks) | No | No |
114
118
  | Automation scheduling | Yes (`CronCreate`, `ScheduleWakeup`, `/loop`, `/schedule` skills) | Yes (desktop app thread automations wake a thread on a schedule, with user request) | No |
115
119
  | Notebook editing | Yes (NotebookEdit tool) | No | No |
@@ -117,6 +121,8 @@ These are the highest-impact gaps where one runtime's limitation blocks effectiv
117
121
  | Image reading (multimodal) | Yes | Yes (desktop app local image/view support) | Yes |
118
122
  | Artifact system | No | Yes (sidebar preview for generated files) | Yes (task.md, implementation plans) |
119
123
  | Video recording | No | No | Yes (WebP via browser) |
124
+ | Image generation | No | Yes (enabled by default in CLI) | Yes |
125
+ | MCP diagnostics | Partial | Yes (`/mcp verbose`) | Partial |
120
126
  | Multi-file editing primitive | Edit tool (one file at a time) | `apply_patch` (one file) | `multi_replace_file_content` (non-contiguous) |
121
127
  | Workflow file format | SKILL.md with YAML frontmatter | SKILL.md with YAML frontmatter | Markdown with YAML frontmatter |
122
128
  | Policy visibility at runtime | Partial (sandbox config visible) | Yes (session policy in system context) | Yes (`SafeToAutoRun` flags) |
@@ -124,4 +130,10 @@ These are the highest-impact gaps where one runtime's limitation blocks effectiv
124
130
 
125
131
  ---
126
132
 
133
+ ## 8. Source Notes
134
+
135
+ - GPT-5.5 Codex availability, 400K context, Fast mode, token-efficiency, and API timing come from OpenAI's 2026-04-23 release note: <https://openai.com/index/introducing-gpt-5-5/>.
136
+
137
+ ---
138
+
127
139
  *Each runtime should update only its own column. Discrepancies should be resolved by the runtime owner.*
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oacp-cli"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Open Agent Coordination Protocol CLI for file-based multi-agent workflows"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -18,7 +18,9 @@ from __future__ import annotations
18
18
  import argparse
19
19
  import json
20
20
  import sys
21
+ import time
21
22
  from dataclasses import dataclass
23
+ from datetime import datetime, timezone
22
24
  from pathlib import Path
23
25
  from typing import Any, Dict, Iterable, List, Optional, Sequence
24
26
 
@@ -30,6 +32,37 @@ except ImportError: # pragma: no cover
30
32
 
31
33
  STATE_VERSION = 1
32
34
 
35
+ _DURATION_UNITS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
36
+
37
+
38
+ def _parse_since(spec: str, *, now: float) -> float:
39
+ """Parse a --since spec to an epoch timestamp.
40
+
41
+ Accepts: 'now', 'epoch', duration like '30s' / '5m' / '2h' / '7d',
42
+ or an ISO 8601 timestamp (trailing 'Z' allowed; naive timestamps treated as UTC).
43
+ """
44
+ if spec == "now":
45
+ return now
46
+ if spec == "epoch":
47
+ return 0.0
48
+ if (
49
+ len(spec) >= 2
50
+ and spec[-1] in _DURATION_UNITS
51
+ and spec[:-1].isdigit()
52
+ ):
53
+ return now - int(spec[:-1]) * _DURATION_UNITS[spec[-1]]
54
+ try:
55
+ normalized = spec[:-1] + "+00:00" if spec.endswith("Z") else spec
56
+ dt = datetime.fromisoformat(normalized)
57
+ except ValueError as exc:
58
+ raise argparse.ArgumentTypeError(
59
+ f"--since: cannot parse {spec!r} "
60
+ "(use 'now', 'epoch', a duration like '30s'/'5m'/'2h'/'7d', or an ISO 8601 timestamp)"
61
+ ) from exc
62
+ if dt.tzinfo is None:
63
+ dt = dt.replace(tzinfo=timezone.utc)
64
+ return dt.timestamp()
65
+
33
66
 
34
67
  @dataclass(frozen=True)
35
68
  class WatchTarget:
@@ -222,9 +255,10 @@ def _resolve_targets(
222
255
  return targets, errors
223
256
 
224
257
 
225
- def _load_state(state_file: Path) -> Dict[str, Any]:
258
+ def _load_state(state_file: Path) -> tuple[Dict[str, Any], bool]:
259
+ """Return (state, existed). `existed` is False for first-run targets."""
226
260
  if not state_file.is_file():
227
- return {"version": STATE_VERSION, "messages": {}}
261
+ return {"version": STATE_VERSION, "messages": {}}, False
228
262
  data = json.loads(state_file.read_text(encoding="utf-8"))
229
263
  if not isinstance(data, dict):
230
264
  raise ValueError("state file root must be a JSON object")
@@ -237,7 +271,10 @@ def _load_state(state_file: Path) -> Dict[str, Any]:
237
271
  normalized[str(file_name)] = {
238
272
  key: str(value) for key, value in metadata.items()
239
273
  }
240
- return {"version": int(data.get("version", STATE_VERSION)), "messages": normalized}
274
+ return (
275
+ {"version": int(data.get("version", STATE_VERSION)), "messages": normalized},
276
+ True,
277
+ )
241
278
 
242
279
 
243
280
  def _write_state(state_file: Path, payload: Dict[str, Any]) -> None:
@@ -252,9 +289,10 @@ def _write_state(state_file: Path, payload: Dict[str, Any]) -> None:
252
289
 
253
290
  def _scan_target(
254
291
  target: WatchTarget,
255
- ) -> tuple[Dict[str, Dict[str, str]], List[Dict[str, Any]]]:
292
+ ) -> tuple[Dict[str, Dict[str, str]], Dict[str, float], List[Dict[str, Any]]]:
256
293
  errors: List[Dict[str, Any]] = []
257
294
  current_messages: Dict[str, Dict[str, str]] = {}
295
+ mtimes: Dict[str, float] = {}
258
296
  for path in sorted(
259
297
  candidate
260
298
  for candidate in target.inbox_dir.iterdir()
@@ -262,6 +300,7 @@ def _scan_target(
262
300
  ):
263
301
  try:
264
302
  metadata = _message_metadata(target.project, target.agent, path)
303
+ mtime = path.stat().st_mtime
265
304
  except Exception as exc:
266
305
  errors.append(
267
306
  _error_event(
@@ -280,13 +319,16 @@ def _scan_target(
280
319
  "subject": metadata["subject"],
281
320
  "priority": metadata["priority"],
282
321
  }
283
- return current_messages, errors
322
+ mtimes[path.name] = mtime
323
+ return current_messages, mtimes, errors
284
324
 
285
325
 
286
326
  def _build_delta_events(
287
327
  target: WatchTarget,
288
328
  previous_messages: Dict[str, Dict[str, str]],
289
329
  current_messages: Dict[str, Dict[str, str]],
330
+ *,
331
+ show_archived: bool,
290
332
  ) -> List[Dict[str, Any]]:
291
333
  events: List[Dict[str, Any]] = []
292
334
  for file_name in sorted(current_messages.keys() - previous_messages.keys()):
@@ -297,14 +339,15 @@ def _build_delta_events(
297
339
  }
298
340
  event.update(current_messages[file_name])
299
341
  events.append(event)
300
- for file_name in sorted(previous_messages.keys() - current_messages.keys()):
301
- event = {
302
- "event": "message_archived",
303
- "project": target.project,
304
- "agent": target.agent,
305
- }
306
- event.update(previous_messages[file_name])
307
- events.append(event)
342
+ if show_archived:
343
+ for file_name in sorted(previous_messages.keys() - current_messages.keys()):
344
+ event = {
345
+ "event": "message_archived",
346
+ "project": target.project,
347
+ "agent": target.agent,
348
+ }
349
+ event.update(previous_messages[file_name])
350
+ events.append(event)
308
351
  return events
309
352
 
310
353
 
@@ -330,8 +373,31 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
330
373
  parser.add_argument("--agent", required=True, help="Agent inbox name to watch")
331
374
  parser.add_argument("--oacp-dir", default=None, help="Override OACP home directory")
332
375
  parser.add_argument("--json", action="store_true", dest="json_output", help="Emit JSON Lines")
376
+ parser.add_argument(
377
+ "--since",
378
+ default="now",
379
+ help=(
380
+ "Cutoff for first-run baseline: 'now' (default) suppresses replay of "
381
+ "existing inbox messages; 'epoch' replays everything; '30s'/'5m'/'2h'/'7d' "
382
+ "for relative windows; ISO 8601 ('2026-04-26T00:00:00Z') for absolute. "
383
+ "Only applies on first run for a target (no state file yet)."
384
+ ),
385
+ )
386
+ parser.add_argument(
387
+ "--show-archived",
388
+ action="store_true",
389
+ help=(
390
+ "Emit 'message_archived' events when messages disappear from the inbox. "
391
+ "Off by default — when the watching agent is also the inbox owner, "
392
+ "these events are self-loops (the agent already knows it deleted them)."
393
+ ),
394
+ )
333
395
 
334
396
  args = parser.parse_args(list(argv) if argv is not None else None)
397
+ try:
398
+ since_epoch = _parse_since(args.since, now=time.time())
399
+ except argparse.ArgumentTypeError as exc:
400
+ parser.error(str(exc))
335
401
  oacp_root = _resolve_oacp_home(args.oacp_dir)
336
402
 
337
403
  targets, target_errors = _resolve_targets(
@@ -352,7 +418,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
352
418
 
353
419
  for target in targets:
354
420
  try:
355
- previous_state = _load_state(target.state_file)
421
+ previous_state, state_existed = _load_state(target.state_file)
356
422
  except Exception as exc:
357
423
  had_errors = True
358
424
  _emit_error(
@@ -365,13 +431,26 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
365
431
  json_output=args.json_output,
366
432
  )
367
433
  continue
368
- current_messages, scan_errors = _scan_target(target)
434
+ current_messages, mtimes, scan_errors = _scan_target(target)
369
435
  if scan_errors:
370
436
  had_errors = True
371
437
  for event in scan_errors:
372
438
  _emit_error(event, json_output=args.json_output)
373
439
  previous_messages = previous_state["messages"]
374
- target_events = _build_delta_events(target, previous_messages, current_messages)
440
+ if not state_existed:
441
+ # First run for this target: seed baseline with messages whose mtime <= since.
442
+ # NEW_MESSAGE events fire only for files newer than the cutoff.
443
+ previous_messages = {
444
+ file_name: current_messages[file_name]
445
+ for file_name in current_messages
446
+ if mtimes.get(file_name, 0.0) <= since_epoch
447
+ }
448
+ target_events = _build_delta_events(
449
+ target,
450
+ previous_messages,
451
+ current_messages,
452
+ show_archived=args.show_archived,
453
+ )
375
454
  payload = {
376
455
  "version": STATE_VERSION,
377
456
  "project": target.project,
@@ -6,8 +6,10 @@ from __future__ import annotations
6
6
 
7
7
  import io
8
8
  import json
9
+ import os
9
10
  import sys
10
11
  import tempfile
12
+ import time
11
13
  import unittest
12
14
  from pathlib import Path
13
15
 
@@ -64,7 +66,7 @@ class TestOacpWatch(unittest.TestCase):
64
66
  sys.stderr = old_stderr
65
67
  return code, stdout, stderr
66
68
 
67
- def test_first_run_emits_existing_messages(self) -> None:
69
+ def test_default_first_run_suppresses_existing_messages(self) -> None:
68
70
  with tempfile.TemporaryDirectory() as tmpdir:
69
71
  root = Path(tmpdir)
70
72
  _write_message(
@@ -82,6 +84,36 @@ class TestOacpWatch(unittest.TestCase):
82
84
  )
83
85
  self.assertEqual(code, 0)
84
86
  self.assertEqual(stderr, "")
87
+ self.assertEqual(stdout, "")
88
+
89
+ def test_first_run_with_since_epoch_emits_existing_messages(self) -> None:
90
+ with tempfile.TemporaryDirectory() as tmpdir:
91
+ root = Path(tmpdir)
92
+ _write_message(
93
+ root,
94
+ "demo",
95
+ "codex",
96
+ "20260411050000_iris_task_request_abcd.yaml",
97
+ sender="iris",
98
+ msg_type="task_request",
99
+ priority="P2",
100
+ subject="First",
101
+ )
102
+ code, stdout, stderr = self._run(
103
+ [
104
+ "--agent",
105
+ "codex",
106
+ "--project",
107
+ "demo",
108
+ "--oacp-dir",
109
+ tmpdir,
110
+ "--since",
111
+ "epoch",
112
+ "--json",
113
+ ]
114
+ )
115
+ self.assertEqual(code, 0)
116
+ self.assertEqual(stderr, "")
85
117
  events = [json.loads(line) for line in stdout.splitlines() if line.strip()]
86
118
  self.assertEqual(len(events), 1)
87
119
  self.assertEqual(events[0]["event"], "new_message")
@@ -147,7 +179,7 @@ class TestOacpWatch(unittest.TestCase):
147
179
  self.assertEqual(events[0]["subject"], "Second")
148
180
  self.assertEqual(events[0]["priority"], "P1")
149
181
 
150
- def test_removed_message_emits_message_archived(self) -> None:
182
+ def test_removed_message_emits_message_archived_when_show_archived(self) -> None:
151
183
  with tempfile.TemporaryDirectory() as tmpdir:
152
184
  root = Path(tmpdir)
153
185
  msg_path = _write_message(
@@ -160,10 +192,32 @@ class TestOacpWatch(unittest.TestCase):
160
192
  priority="P2",
161
193
  subject="First",
162
194
  )
163
- self._run(["--agent", "codex", "--project", "demo", "--oacp-dir", tmpdir, "--json"])
195
+ self._run(
196
+ [
197
+ "--agent",
198
+ "codex",
199
+ "--project",
200
+ "demo",
201
+ "--oacp-dir",
202
+ tmpdir,
203
+ "--since",
204
+ "epoch",
205
+ "--show-archived",
206
+ "--json",
207
+ ]
208
+ )
164
209
  msg_path.unlink()
165
210
  code, stdout, stderr = self._run(
166
- ["--agent", "codex", "--project", "demo", "--oacp-dir", tmpdir, "--json"]
211
+ [
212
+ "--agent",
213
+ "codex",
214
+ "--project",
215
+ "demo",
216
+ "--oacp-dir",
217
+ tmpdir,
218
+ "--show-archived",
219
+ "--json",
220
+ ]
167
221
  )
168
222
  self.assertEqual(code, 0)
169
223
  self.assertEqual(stderr, "")
@@ -172,6 +226,41 @@ class TestOacpWatch(unittest.TestCase):
172
226
  self.assertEqual(events[0]["event"], "message_archived")
173
227
  self.assertEqual(events[0]["subject"], "First")
174
228
 
229
+ def test_removed_message_suppressed_by_default(self) -> None:
230
+ with tempfile.TemporaryDirectory() as tmpdir:
231
+ root = Path(tmpdir)
232
+ msg_path = _write_message(
233
+ root,
234
+ "demo",
235
+ "codex",
236
+ "20260411050000_iris_task_request_abcd.yaml",
237
+ sender="iris",
238
+ msg_type="task_request",
239
+ priority="P2",
240
+ subject="First",
241
+ )
242
+ # Seed baseline so the message is tracked in state.
243
+ self._run(
244
+ [
245
+ "--agent",
246
+ "codex",
247
+ "--project",
248
+ "demo",
249
+ "--oacp-dir",
250
+ tmpdir,
251
+ "--since",
252
+ "epoch",
253
+ "--json",
254
+ ]
255
+ )
256
+ msg_path.unlink()
257
+ code, stdout, stderr = self._run(
258
+ ["--agent", "codex", "--project", "demo", "--oacp-dir", tmpdir, "--json"]
259
+ )
260
+ self.assertEqual(code, 0)
261
+ self.assertEqual(stderr, "")
262
+ self.assertEqual(stdout, "")
263
+
175
264
  def test_repeatable_project_keeps_argument_order(self) -> None:
176
265
  with tempfile.TemporaryDirectory() as tmpdir:
177
266
  root = Path(tmpdir)
@@ -205,6 +294,8 @@ class TestOacpWatch(unittest.TestCase):
205
294
  "alpha",
206
295
  "--oacp-dir",
207
296
  tmpdir,
297
+ "--since",
298
+ "epoch",
208
299
  "--json",
209
300
  ]
210
301
  )
@@ -237,7 +328,16 @@ class TestOacpWatch(unittest.TestCase):
237
328
  subject="Alpha",
238
329
  )
239
330
  code, stdout, stderr = self._run(
240
- ["--agent", "codex", "--all-projects", "--oacp-dir", tmpdir, "--json"]
331
+ [
332
+ "--agent",
333
+ "codex",
334
+ "--all-projects",
335
+ "--oacp-dir",
336
+ tmpdir,
337
+ "--since",
338
+ "epoch",
339
+ "--json",
340
+ ]
241
341
  )
242
342
  self.assertEqual(code, 0)
243
343
  self.assertEqual(stderr, "")
@@ -277,7 +377,17 @@ class TestOacpWatch(unittest.TestCase):
277
377
  (inbox_dir / "broken.yaml").write_text("subject: [unterminated\n", encoding="utf-8")
278
378
 
279
379
  first_code, first_stdout, first_stderr = self._run(
280
- ["--agent", "codex", "--project", "demo", "--oacp-dir", tmpdir, "--json"]
380
+ [
381
+ "--agent",
382
+ "codex",
383
+ "--project",
384
+ "demo",
385
+ "--oacp-dir",
386
+ tmpdir,
387
+ "--since",
388
+ "epoch",
389
+ "--json",
390
+ ]
281
391
  )
282
392
  second_code, second_stdout, second_stderr = self._run(
283
393
  ["--agent", "codex", "--project", "demo", "--oacp-dir", tmpdir, "--json"]
@@ -330,6 +440,8 @@ class TestOacpWatch(unittest.TestCase):
330
440
  "missing",
331
441
  "--oacp-dir",
332
442
  tmpdir,
443
+ "--since",
444
+ "epoch",
333
445
  "--json",
334
446
  ]
335
447
  )
@@ -341,5 +453,104 @@ class TestOacpWatch(unittest.TestCase):
341
453
  self.assertEqual(events[1]["project"], "valid")
342
454
 
343
455
 
456
+ def test_since_relative_duration_only_emits_new_files(self) -> None:
457
+ with tempfile.TemporaryDirectory() as tmpdir:
458
+ root = Path(tmpdir)
459
+ old_path = _write_message(
460
+ root,
461
+ "demo",
462
+ "codex",
463
+ "20260411050000_iris_task_request_old.yaml",
464
+ sender="iris",
465
+ msg_type="task_request",
466
+ priority="P2",
467
+ subject="Old",
468
+ )
469
+ # Backdate the old message to 1 hour ago.
470
+ old_time = time.time() - 3600
471
+ os.utime(old_path, (old_time, old_time))
472
+
473
+ _write_message(
474
+ root,
475
+ "demo",
476
+ "codex",
477
+ "20260411060000_iris_task_request_new.yaml",
478
+ sender="iris",
479
+ msg_type="task_request",
480
+ priority="P1",
481
+ subject="New",
482
+ )
483
+
484
+ code, stdout, stderr = self._run(
485
+ [
486
+ "--agent",
487
+ "codex",
488
+ "--project",
489
+ "demo",
490
+ "--oacp-dir",
491
+ tmpdir,
492
+ "--since",
493
+ "5m",
494
+ "--json",
495
+ ]
496
+ )
497
+ self.assertEqual(code, 0)
498
+ self.assertEqual(stderr, "")
499
+ events = [json.loads(line) for line in stdout.splitlines() if line.strip()]
500
+ self.assertEqual(len(events), 1)
501
+ self.assertEqual(events[0]["event"], "new_message")
502
+ self.assertEqual(events[0]["subject"], "New")
503
+
504
+ def test_since_iso_timestamp_with_z_suffix(self) -> None:
505
+ with tempfile.TemporaryDirectory() as tmpdir:
506
+ root = Path(tmpdir)
507
+ _write_message(
508
+ root,
509
+ "demo",
510
+ "codex",
511
+ "20260411050000_iris_task_request_abcd.yaml",
512
+ sender="iris",
513
+ msg_type="task_request",
514
+ priority="P2",
515
+ subject="First",
516
+ )
517
+ code, stdout, stderr = self._run(
518
+ [
519
+ "--agent",
520
+ "codex",
521
+ "--project",
522
+ "demo",
523
+ "--oacp-dir",
524
+ tmpdir,
525
+ "--since",
526
+ "1970-01-01T00:00:00Z",
527
+ "--json",
528
+ ]
529
+ )
530
+ self.assertEqual(code, 0)
531
+ self.assertEqual(stderr, "")
532
+ events = [json.loads(line) for line in stdout.splitlines() if line.strip()]
533
+ self.assertEqual(len(events), 1)
534
+ self.assertEqual(events[0]["subject"], "First")
535
+
536
+ def test_since_invalid_spec_exits_with_usage_error(self) -> None:
537
+ with tempfile.TemporaryDirectory() as tmpdir:
538
+ with self.assertRaises(SystemExit) as ctx:
539
+ self._run(
540
+ [
541
+ "--agent",
542
+ "codex",
543
+ "--project",
544
+ "demo",
545
+ "--oacp-dir",
546
+ tmpdir,
547
+ "--since",
548
+ "not-a-valid-spec",
549
+ "--json",
550
+ ]
551
+ )
552
+ self.assertEqual(ctx.exception.code, 2)
553
+
554
+
344
555
  if __name__ == "__main__":
345
556
  unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes