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.
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/CHANGELOG.md +12 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/PKG-INFO +2 -2
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/README.md +1 -1
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/runtime_capability_matrix.md +28 -16
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/pyproject.toml +1 -1
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/oacp_watch.py +95 -16
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_watch.py +217 -6
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/.github/workflows/ci.yml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/.github/workflows/release.yml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/.gitignore +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/CONTRIBUTING.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/LICENSE +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/Makefile +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/QUICKSTART.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/SECURITY.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/SPEC.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/adoption.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/doctor.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/prompt_caching.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/setup.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/unified_skill_spec.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/guides/versioning.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/agent_profiles.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/agent_safety_defaults.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/credential_scoping.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/cross_runtime_sync.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/dispatch_states.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/inbox_outbox.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/mcp_integration.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/multi_agent_shared_workspace.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/org_memory.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/packet_states.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/review_loop.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/runtime_capabilities.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/session_init.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/skills_manifest.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/docs/protocol/task_negotiation.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/examples/quickstart/README.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/mcp_servers/oacp_coordinator.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/oacp/__init__.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/oacp/cli.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/_oacp_constants.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/_oacp_env.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/add_agent.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/agent_profile.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/check_quality_gate.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/codex_session_init.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/create_handoff_packet.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/handoff_schema.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/init_org_memory.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/init_packet.sh +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/init_project_workspace.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/init_project_workspace.sh +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/memory_archive_common.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/memory_cli.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/normalize_findings.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/oacp_doctor.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/oacp_inbox.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/preflight.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/promote_to_archive.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/restore_from_archive.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/send_inbox_message.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/session_lifecycle_hooks.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/setup_runtime.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/update_workspace.sh +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/validate_agent_card.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/validate_message.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/scripts/write_event.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/agent_card.template.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/agent_profile.template.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/agent_status.template.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/checkpoint.template.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/claude/agents/role_agent.template.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/claude/rules/guardrail.template.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/findings_packet.template.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/github_actions_quality_gate.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/guardrails/coding_standards.template.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/guardrails/safe_commands.template.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/guardrails/secrets_rules.template.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/handoff_packet.template.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/inbox_message.template.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/manual_validation.template.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/merge_decision.template.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/org-memory/decisions.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/org-memory/events/.gitkeep +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/org-memory/events/20260317-170120-example-api-convention.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/org-memory/recent.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/org-memory/rules.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/review_packet.template.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/roles/role_baseline.template.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/roles/role_definition.template.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/runtime_capabilities.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/skills_manifest.template.yaml +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/templates/test_packet.template.md +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_add_agent.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_agent_profile.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_codex_session_init.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_create_handoff_packet.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_github_workflows.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_handoff_schema.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_init_project_workspace.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_memory_archive.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_cli.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_constants.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_coordinator.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_doctor.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_env.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_oacp_inbox.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_preflight.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_review_loop.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_send_inbox_message.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_session_lifecycle_hooks.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_setup_runtime.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_update_workspace.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_validate_agent_card.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_validate_message.py +0 -0
- {oacp_cli-0.2.2 → oacp_cli-0.2.3}/tests/test_workspace_discovery.py +0 -0
- {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.
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
26
|
-
| Context window | ~1M with Opus 4.7 (auto-compaction extends indefinitely) |
|
|
27
|
-
| Cost model | Token-based, visible in statusline | Not surfaced
|
|
28
|
-
| Sandbox restrictions | Yes — configurable allowlists | Session-dependent
|
|
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.
|
|
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
|
|
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
|
-
|
|
|
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) |
|
|
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 |
|
|
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 |
|
|
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.*
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|