trinity-agent 0.7.1__tar.gz → 0.7.2__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 (156) hide show
  1. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/PKG-INFO +8 -4
  2. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/README.en.md +8 -3
  3. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/README.md +7 -3
  4. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/checkpoint.md +2 -2
  5. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/workflow-v0.7.0-guide.md +2 -1
  6. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/pyproject.toml +1 -1
  7. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/__init__.py +1 -1
  8. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/app.py +1 -0
  9. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/prompt.py +25 -2
  10. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/session.py +131 -0
  11. trinity_agent-0.7.2/src/trinity/workflow/persistence.py +207 -0
  12. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tui_prompt.py +23 -0
  13. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tui_session.py +42 -0
  14. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_workflow_persistence.py +38 -0
  15. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/uv.lock +1 -1
  16. trinity_agent-0.7.1/src/trinity/workflow/persistence.py +0 -86
  17. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/.github/workflows/publish-pypi.yml +0 -0
  18. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/.gitignore +0 -0
  19. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/LICENSE +0 -0
  20. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/phase-6-plan.md +0 -0
  21. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-phase-10-interactive-redesign.md +0 -0
  22. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-prompt-compression.md +0 -0
  23. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-token-analytics.md +0 -0
  24. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-token-optimization-phase7b.md +0 -0
  25. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-tui-overhaul.md +0 -0
  26. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-02-v0.6.9-interactive-error-analysis.md +0 -0
  27. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-03-phase10-remaining.md +0 -0
  28. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-03-v0.7.0-workflow-engine-redesign.md +0 -0
  29. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-04-isolated-provider-bootstrap.md +0 -0
  30. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/plans/2026-06-04-v0.7.0-follow-up-implementation-candidates.md +0 -0
  31. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/reference-architecture.md +0 -0
  32. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/2026-06-04-isolated-provider-bootstrap.md +0 -0
  33. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-1-T.md +0 -0
  34. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-10-T.md +0 -0
  35. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-2-T.md +0 -0
  36. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-3-T.md +0 -0
  37. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-4-T.md +0 -0
  38. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-5-T.md +0 -0
  39. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-6-T.md +0 -0
  40. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-7-T.md +0 -0
  41. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/phase-9-T.md +0 -0
  42. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/v0.7.0-workflow-engine.md +0 -0
  43. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/test-results/v070-smoke-checklist.md +0 -0
  44. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/docs/troubleshooting-provider-readiness.md +0 -0
  45. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/__main__.py +0 -0
  46. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/__init__.py +0 -0
  47. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/base.py +0 -0
  48. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/claude_agent.py +0 -0
  49. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/codex_agent.py +0 -0
  50. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/factory.py +0 -0
  51. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/gemini_agent.py +0 -0
  52. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/agents/response_cleaner.py +0 -0
  53. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/cli.py +0 -0
  54. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/__init__.py +0 -0
  55. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/base.py +0 -0
  56. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/hook.py +0 -0
  57. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/idle.py +0 -0
  58. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/marker.py +0 -0
  59. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/completion/prompt.py +0 -0
  60. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/config.py +0 -0
  61. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/__init__.py +0 -0
  62. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/analytics.py +0 -0
  63. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/budget.py +0 -0
  64. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/compressor.py +0 -0
  65. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/monitor.py +0 -0
  66. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/rotator.py +0 -0
  67. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/context/shared.py +0 -0
  68. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/deliberation/__init__.py +0 -0
  69. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/deliberation/consensus.py +0 -0
  70. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/deliberation/distributor.py +0 -0
  71. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/deliberation/protocol.py +0 -0
  72. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/error_handler.py +0 -0
  73. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/health/__init__.py +0 -0
  74. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/health/checker.py +0 -0
  75. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/i18n.py +0 -0
  76. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/logging.py +0 -0
  77. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/models.py +0 -0
  78. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/orchestrator.py +0 -0
  79. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/providers/__init__.py +0 -0
  80. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/providers/bootstrap.py +0 -0
  81. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/providers/readiness.py +0 -0
  82. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/retry.py +0 -0
  83. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/setup/__init__.py +0 -0
  84. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/setup/detector.py +0 -0
  85. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/setup/wizard.py +0 -0
  86. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tmux/__init__.py +0 -0
  87. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tmux/layout.py +0 -0
  88. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tmux/pane.py +0 -0
  89. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tmux/session.py +0 -0
  90. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/__init__.py +0 -0
  91. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/events.py +0 -0
  92. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/tui/theme.py +0 -0
  93. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/__init__.py +0 -0
  94. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/decomposer.py +0 -0
  95. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/engine.py +0 -0
  96. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/execution.py +0 -0
  97. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/ledger.py +0 -0
  98. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/lifecycle.py +0 -0
  99. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/models.py +0 -0
  100. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/review.py +0 -0
  101. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workflow/structured.py +0 -0
  102. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workspace/__init__.py +0 -0
  103. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workspace/isolation.py +0 -0
  104. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/src/trinity/workspace/managed_home.py +0 -0
  105. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/templates/trinity.config.example +0 -0
  106. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/conftest.py +0 -0
  107. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_agent_factory.py +0 -0
  108. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_analytics.py +0 -0
  109. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_blueprint_decomposer.py +0 -0
  110. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_budget.py +0 -0
  111. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_claude_agent.py +0 -0
  112. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_cli.py +0 -0
  113. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_cli_detector.py +0 -0
  114. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_cli_v2.py +0 -0
  115. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_codex_agent.py +0 -0
  116. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_completion.py +0 -0
  117. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_compressor.py +0 -0
  118. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_config.py +0 -0
  119. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_consensus_v2.py +0 -0
  120. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_context_monitor.py +0 -0
  121. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_deliberation.py +0 -0
  122. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_e2e.py +0 -0
  123. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_error_handling.py +0 -0
  124. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_execution_protocol.py +0 -0
  125. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_gemini_agent.py +0 -0
  126. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_health_checker.py +0 -0
  127. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_i18n.py +0 -0
  128. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_interactive_claude.py +0 -0
  129. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_lifecycle_guard.py +0 -0
  130. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_logging.py +0 -0
  131. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_managed_home.py +0 -0
  132. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_models.py +0 -0
  133. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_multi_provider.py +0 -0
  134. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_orchestrator.py +0 -0
  135. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_peer_review.py +0 -0
  136. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_protocol.py +0 -0
  137. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_protocol_compression.py +0 -0
  138. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_protocol_v2.py +0 -0
  139. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_provider_bootstrap.py +0 -0
  140. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_provider_readiness.py +0 -0
  141. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_response_cleaner.py +0 -0
  142. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_response_contract.py +0 -0
  143. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_retry.py +0 -0
  144. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_rotator.py +0 -0
  145. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_session_handoff.py +0 -0
  146. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_session_rotator.py +0 -0
  147. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_setup_wizard.py +0 -0
  148. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_shared_context.py +0 -0
  149. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_shared_ledger.py +0 -0
  150. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_structured_consensus.py +0 -0
  151. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tmux.py +0 -0
  152. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tmux_integration.py +0 -0
  153. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tmux_layout.py +0 -0
  154. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_tui.py +0 -0
  155. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_workflow_engine.py +0 -0
  156. {trinity_agent-0.7.1 → trinity_agent-0.7.2}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trinity-agent
3
- Version: 0.7.1
3
+ Version: 0.7.2
4
4
  Summary: Three minds, one context — Multi-agent AI orchestrator for Claude Code, Codex, and Gemini CLI.
5
5
  Project-URL: Homepage, https://github.com/hongdangmoo49/Trinity
6
6
  Project-URL: Repository, https://github.com/hongdangmoo49/Trinity
@@ -150,7 +150,7 @@ Trinity가 백그라운드에서 다음 단계를 자동으로 수행합니다:
150
150
  Trinity는 **Rich 라이브러리 기반의 미려한 터미널 UI(TUI)**를 제공하여, 에이전트 간의 실시간 토론 과정을 시각적으로 보여줍니다.
151
151
 
152
152
  ```
153
- 🧠 Trinity v0.7.1 — 세 개의 두뇌, 하나의 컨텍스트
153
+ 🧠 Trinity v0.7.2 — 세 개의 두뇌, 하나의 컨텍스트
154
154
 
155
155
  🏗️ claude ✅ ⚙️ codex ✅ 🔍 gemini ✅
156
156
 
@@ -228,9 +228,13 @@ Trinity는 **Rich 라이브러리 기반의 미려한 터미널 UI(TUI)**를 제
228
228
  | `/agent <이름> on\|off` | 특정 에이전트를 즉시 활성화하거나 비활성화 |
229
229
  | `/history` | 이전 라운드의 토론 히스토리 요약 조회 |
230
230
  | `/save` | 현재 토론 세션의 전체 결과를 파일로 영구 저장 |
231
+ | `/resume [N\|latest\|ID]` | 저장된 workflow 세션을 선택해 재개 |
231
232
  | `/help` | 사용 가능한 인라인 명령어 도움말 표시 |
232
233
  | `/quit` | Trinity 종료 및 백그라운드 리소스 정리 |
233
234
 
235
+ TUI는 기본적으로 새 workflow 세션으로 시작한다. 이전 active workflow는
236
+ `.trinity/workflow/history/`에 보존되며 `/resume`으로 명시적으로 재개한다.
237
+
234
238
  ---
235
239
 
236
240
  ## ⚙️ 설정
@@ -408,8 +412,8 @@ uv publish --token <PYPI_TOKEN>
408
412
 
409
413
  | 지표 | 수치 |
410
414
  | :--- | :--- |
411
- | **버전** | 0.7.1 |
412
- | **테스트** | 909개 테스트 통과 |
415
+ | **버전** | 0.7.2 |
416
+ | **테스트** | 915개 테스트 통과 |
413
417
  | **커버리지** | 약 87% |
414
418
  | **소스 파일** | 50여 개 |
415
419
  | **주요 의존성 라이브러리** | `click`, `rich`, `prompt_toolkit`, `tomli` |
@@ -124,7 +124,7 @@ That's it. Trinity will:
124
124
  Trinity features a **Rich-based terminal UI** with real-time deliberation visualization.
125
125
 
126
126
  ```
127
- 🧠 Trinity v0.7.1 — Three minds, one context
127
+ 🧠 Trinity v0.7.2 — Three minds, one context
128
128
 
129
129
  🏗️ claude ✅ ⚙️ codex ✅ 🔍 gemini ✅
130
130
 
@@ -202,9 +202,14 @@ Inside the interactive TUI (`trinity` with no args):
202
202
  | `/agent <name> on\|off` | Enable/disable an agent |
203
203
  | `/history` | Show deliberation history |
204
204
  | `/save` | Save session results to file |
205
+ | `/resume [N\|latest\|ID]` | Select and resume a saved workflow session |
205
206
  | `/help` | Show help |
206
207
  | `/quit` | Exit Trinity |
207
208
 
209
+ The TUI starts with a new workflow session by default. The previous active
210
+ workflow is preserved in `.trinity/workflow/history/` and can be resumed
211
+ explicitly with `/resume`.
212
+
208
213
  ---
209
214
 
210
215
  ## ⚙️ Configuration
@@ -382,8 +387,8 @@ uv publish --token <PYPI_TOKEN>
382
387
 
383
388
  | Metric | Value |
384
389
  | :--- | :--- |
385
- | **Version** | 0.7.1 |
386
- | **Tests** | 909 passed |
390
+ | **Version** | 0.7.2 |
391
+ | **Tests** | 915 passed |
387
392
  | **Coverage** | ~87% |
388
393
  | **Source files** | 50+ |
389
394
  | **Dependencies** | `click`, `rich`, `prompt_toolkit`, `tomli` |
@@ -122,7 +122,7 @@ Trinity가 백그라운드에서 다음 단계를 자동으로 수행합니다:
122
122
  Trinity는 **Rich 라이브러리 기반의 미려한 터미널 UI(TUI)**를 제공하여, 에이전트 간의 실시간 토론 과정을 시각적으로 보여줍니다.
123
123
 
124
124
  ```
125
- 🧠 Trinity v0.7.1 — 세 개의 두뇌, 하나의 컨텍스트
125
+ 🧠 Trinity v0.7.2 — 세 개의 두뇌, 하나의 컨텍스트
126
126
 
127
127
  🏗️ claude ✅ ⚙️ codex ✅ 🔍 gemini ✅
128
128
 
@@ -200,9 +200,13 @@ Trinity는 **Rich 라이브러리 기반의 미려한 터미널 UI(TUI)**를 제
200
200
  | `/agent <이름> on\|off` | 특정 에이전트를 즉시 활성화하거나 비활성화 |
201
201
  | `/history` | 이전 라운드의 토론 히스토리 요약 조회 |
202
202
  | `/save` | 현재 토론 세션의 전체 결과를 파일로 영구 저장 |
203
+ | `/resume [N\|latest\|ID]` | 저장된 workflow 세션을 선택해 재개 |
203
204
  | `/help` | 사용 가능한 인라인 명령어 도움말 표시 |
204
205
  | `/quit` | Trinity 종료 및 백그라운드 리소스 정리 |
205
206
 
207
+ TUI는 기본적으로 새 workflow 세션으로 시작한다. 이전 active workflow는
208
+ `.trinity/workflow/history/`에 보존되며 `/resume`으로 명시적으로 재개한다.
209
+
206
210
  ---
207
211
 
208
212
  ## ⚙️ 설정
@@ -380,8 +384,8 @@ uv publish --token <PYPI_TOKEN>
380
384
 
381
385
  | 지표 | 수치 |
382
386
  | :--- | :--- |
383
- | **버전** | 0.7.1 |
384
- | **테스트** | 909개 테스트 통과 |
387
+ | **버전** | 0.7.2 |
388
+ | **테스트** | 915개 테스트 통과 |
385
389
  | **커버리지** | 약 87% |
386
390
  | **소스 파일** | 50여 개 |
387
391
  | **주요 의존성 라이브러리** | `click`, `rich`, `prompt_toolkit`, `tomli` |
@@ -35,8 +35,8 @@
35
35
 
36
36
  ### 검증 기준선
37
37
 
38
- - 패키지/CLI 버전: `0.7.1`
39
- - `uv run pytest -q` → 909 passed, 1 warning
38
+ - 패키지/CLI 버전: `0.7.2`
39
+ - `uv run pytest -q` → 915 passed, 1 warning
40
40
  - 변경 파일 대상 ruff check 통과
41
41
  - 실제 WSL/tmux/provider smoke는 릴리스 전 별도 수행 필요
42
42
 
@@ -29,7 +29,7 @@ trinity
29
29
 
30
30
  새 목표를 입력하면 Trinity는 다음 순서로 진행한다.
31
31
 
32
- 1. active agent와 workflow session을 만든다.
32
+ 1. workflow session을 만든다. 이전 active workflow는 history로 보존된다.
33
33
  2. provider readiness를 확인한다.
34
34
  3. round 기반 deliberation을 실행한다.
35
35
  4. structured consensus가 나오면 blueprint를 저장한다.
@@ -47,6 +47,7 @@ trinity
47
47
  | `/decisions` | 사용자 또는 agent가 남긴 decision ledger를 표시한다. |
48
48
  | `/packages` | blueprint에서 생성된 work package와 실행 상태를 표시한다. |
49
49
  | `/subtasks` | parent agent가 보고한 provider-internal subtask/tool 사용 결과를 표시한다. |
50
+ | `/resume [N\|latest\|ID]` | `.trinity/workflow/history/`에 저장된 workflow를 선택해 재개한다. |
50
51
 
51
52
  ## 4. Workflow State
52
53
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "trinity-agent"
7
- version = "0.7.1"
7
+ version = "0.7.2"
8
8
  description = "Three minds, one context — Multi-agent AI orchestrator for Claude Code, Codex, and Gemini CLI."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """Trinity — Three minds, one context."""
2
2
 
3
- __version__ = "0.7.1"
3
+ __version__ = "0.7.2"
@@ -725,6 +725,7 @@ class TrinityTUI:
725
725
  " [cyan]/decisions[/cyan] — Show recorded workflow decisions\n"
726
726
  " [cyan]/packages[/cyan] — Show workflow work packages\n"
727
727
  " [cyan]/subtasks[/cyan] — Show delegated subtask reports\n"
728
+ " [cyan]/resume [n|latest|id][/cyan] — Resume a saved workflow session\n"
728
729
  " [cyan]/help[/cyan] — Show this help\n"
729
730
  " [cyan]/quit[/cyan] — Exit Trinity\n"
730
731
  )
@@ -15,7 +15,8 @@ from pathlib import Path
15
15
 
16
16
  from prompt_toolkit import PromptSession
17
17
  from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
18
- from prompt_toolkit.completion import WordCompleter
18
+ from prompt_toolkit.completion import Completer, Completion, CompleteEvent
19
+ from prompt_toolkit.document import Document
19
20
  from prompt_toolkit.history import FileHistory
20
21
  from prompt_toolkit.input import DummyInput
21
22
  from prompt_toolkit.output import DummyOutput
@@ -44,6 +45,7 @@ TRINITY_COMMANDS = [
44
45
  "/decisions",
45
46
  "/packages",
46
47
  "/subtasks",
48
+ "/resume",
47
49
  "/help",
48
50
  "/quit",
49
51
  ]
@@ -55,6 +57,27 @@ TRINITY_STYLE = Style.from_dict({
55
57
  })
56
58
 
57
59
 
60
+ class SlashCommandCompleter(Completer):
61
+ """Complete Trinity commands only when the input begins with slash."""
62
+
63
+ def __init__(self, commands: list[str]):
64
+ self.commands = commands
65
+
66
+ def get_completions(
67
+ self,
68
+ document: Document,
69
+ complete_event: CompleteEvent,
70
+ ):
71
+ text = document.text_before_cursor
72
+ if not text.startswith("/") or any(char.isspace() for char in text):
73
+ return
74
+
75
+ normalized = text.lower()
76
+ for command in self.commands:
77
+ if command.lower().startswith(normalized):
78
+ yield Completion(command, start_position=-len(text))
79
+
80
+
58
81
  class TrinityPromptSession:
59
82
  """prompt_toolkit-backed input session with history and completion.
60
83
 
@@ -78,7 +101,7 @@ class TrinityPromptSession:
78
101
  history=FileHistory(history_path) if history_path else None,
79
102
  auto_suggest=AutoSuggestFromHistory(),
80
103
  multiline=False,
81
- completer=WordCompleter(TRINITY_COMMANDS, ignore_case=True),
104
+ completer=SlashCommandCompleter(TRINITY_COMMANDS),
82
105
  style=TRINITY_STYLE,
83
106
  )
84
107
  try:
@@ -15,6 +15,7 @@ import json
15
15
  import logging
16
16
  import shlex
17
17
  import sys
18
+ import time
18
19
  from typing import TYPE_CHECKING
19
20
 
20
21
  from rich.console import Console
@@ -32,6 +33,7 @@ from trinity.workflow import (
32
33
  ExecutionResult,
33
34
  WorkflowEngine,
34
35
  WorkflowInputAction,
36
+ WorkflowPersistence,
35
37
  WorkflowState,
36
38
  )
37
39
 
@@ -62,6 +64,8 @@ class InteractiveSession:
62
64
  self.running = False
63
65
  self._history_file = config.effective_state_dir / "history" / "session_history.json"
64
66
  self._prompt_session = TrinityPromptSession(config.effective_state_dir)
67
+ self.workflow_persistence = WorkflowPersistence(config.effective_state_dir)
68
+ self._startup_archive = self.workflow_persistence.archive_active_session()
65
69
  self.workflow = WorkflowEngine(config.effective_state_dir)
66
70
  self.tui.set_workflow_session(self.workflow.session)
67
71
 
@@ -97,6 +101,11 @@ class InteractiveSession:
97
101
  def _show_welcome(self) -> None:
98
102
  """Show welcome message."""
99
103
  self.console.print(self.tui.get_welcome_text())
104
+ if self._startup_archive:
105
+ self.console.print(
106
+ "[dim]Previous workflow saved to history. "
107
+ "Use /resume to restore it.[/dim]"
108
+ )
100
109
  self.console.print(
101
110
  "[dim]Type a question to start deliberation, or /help for commands.[/dim]\n"
102
111
  )
@@ -156,6 +165,8 @@ class InteractiveSession:
156
165
  self._cmd_packages()
157
166
  elif cmd == "subtasks":
158
167
  self._cmd_subtasks()
168
+ elif cmd == "resume":
169
+ self._cmd_resume(args)
159
170
  else:
160
171
  self.console.print(
161
172
  f"[yellow]Unknown command: /{cmd}. "
@@ -542,6 +553,126 @@ class InteractiveSession:
542
553
 
543
554
  self.console.print(table)
544
555
 
556
+ def _cmd_resume(self, args: list[str]) -> None:
557
+ """Resume an archived workflow session.
558
+
559
+ Usage:
560
+ /resume
561
+ /resume latest
562
+ /resume <index|workflow-id>
563
+ """
564
+ archives = self.workflow_persistence.list_archives()
565
+ if not archives:
566
+ self.console.print("[dim]No saved workflow sessions to resume.[/dim]")
567
+ return
568
+
569
+ selector = args[0] if args else ""
570
+ if not selector:
571
+ if sys.stdin.isatty() and sys.stdout.isatty():
572
+ labels = [
573
+ self._archive_label(archive)
574
+ for archive in archives
575
+ ]
576
+ selected = self._prompt_session.select_option(
577
+ title="Resume Workflow",
578
+ question="Select a saved workflow session.",
579
+ options=labels,
580
+ )
581
+ if selected is None:
582
+ self.console.print("[dim]Resume cancelled.[/dim]")
583
+ return
584
+ selector = selected
585
+ else:
586
+ self._print_resume_table(archives)
587
+ self.console.print("[dim]Usage: /resume <index|latest|workflow-id>[/dim]")
588
+ return
589
+
590
+ archive = self._resolve_archive(archives, selector)
591
+ if archive is None:
592
+ self.console.print(f"[yellow]No matching workflow session: {selector}[/yellow]")
593
+ return
594
+
595
+ archived_current = self.workflow_persistence.archive_active_session()
596
+ self.workflow_persistence.restore_archive(archive)
597
+ self.workflow = WorkflowEngine(self.config.effective_state_dir)
598
+ self.tui.set_workflow_session(self.workflow.session)
599
+
600
+ if archived_current:
601
+ self.console.print(
602
+ f"[dim]Current workflow saved as {archived_current.session.id}.[/dim]"
603
+ )
604
+ self.console.print(
605
+ f"[green]Resumed workflow {self.workflow.session.id}.[/green]"
606
+ )
607
+ self._cmd_workflow()
608
+
609
+ def _print_resume_table(self, archives) -> None:
610
+ """Print archived workflow sessions for manual selection."""
611
+ from rich.table import Table
612
+
613
+ table = Table(title="Saved Workflow Sessions")
614
+ table.add_column("#", justify="right", style="cyan")
615
+ table.add_column("ID")
616
+ table.add_column("State")
617
+ table.add_column("Updated")
618
+ table.add_column("Goal")
619
+
620
+ for index, archive in enumerate(archives, 1):
621
+ session = archive.session
622
+ table.add_row(
623
+ str(index),
624
+ session.id,
625
+ session.state.value,
626
+ self._format_timestamp(session.updated_at),
627
+ self._short_goal(session.goal),
628
+ )
629
+
630
+ self.console.print(table)
631
+
632
+ def _resolve_archive(self, archives, selector: str):
633
+ """Resolve a resume selector to one archive."""
634
+ normalized = selector.strip().lower()
635
+ if normalized in {"latest", "last", "newest"}:
636
+ return archives[0]
637
+ if normalized.isdigit():
638
+ index = int(normalized) - 1
639
+ if 0 <= index < len(archives):
640
+ return archives[index]
641
+ return None
642
+ return next(
643
+ (
644
+ archive
645
+ for archive in archives
646
+ if archive.session.id.lower() == normalized
647
+ ),
648
+ None,
649
+ )
650
+
651
+ def _archive_label(self, archive) -> str:
652
+ """Build a concise label for interactive resume selection."""
653
+ session = archive.session
654
+ return (
655
+ f"{session.id} · {session.state.value} · "
656
+ f"{self._format_timestamp(session.updated_at)} · "
657
+ f"{self._short_goal(session.goal)}"
658
+ )
659
+
660
+ @staticmethod
661
+ def _format_timestamp(timestamp: float) -> str:
662
+ """Format a persisted Unix timestamp for the resume list."""
663
+ try:
664
+ return time.strftime("%Y-%m-%d %H:%M", time.localtime(timestamp))
665
+ except (OSError, ValueError):
666
+ return "unknown"
667
+
668
+ @staticmethod
669
+ def _short_goal(goal: str, limit: int = 60) -> str:
670
+ """Return a compact single-line workflow goal."""
671
+ text = " ".join((goal or "(none)").split())
672
+ if len(text) <= limit:
673
+ return text
674
+ return text[: limit - 3] + "..."
675
+
545
676
  # ─── Deliberation ──────────────────────────────────────────────────
546
677
 
547
678
  def _handle_user_text(self, text: str) -> None:
@@ -0,0 +1,207 @@
1
+ """Workflow session persistence for Trinity v0.7.0."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import shutil
8
+ import time
9
+ from collections.abc import Mapping
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from trinity.workflow.models import WorkflowSession
15
+ from trinity.workflow.models import WorkflowState
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class WorkflowArchive:
22
+ """A saved workflow session available for explicit resume."""
23
+
24
+ session: WorkflowSession
25
+ path: Path
26
+ events_path: Path | None = None
27
+
28
+
29
+ class WorkflowPersistence:
30
+ """Serialize workflow sessions and append-only event logs."""
31
+
32
+ def __init__(
33
+ self,
34
+ state_dir: Path,
35
+ *,
36
+ state_file: Path | None = None,
37
+ events_file: Path | None = None,
38
+ ) -> None:
39
+ workflow_dir = state_dir / "workflow"
40
+ self.session_path = state_file or workflow_dir / "session.json"
41
+ self.events_path = events_file or workflow_dir / "events.jsonl"
42
+
43
+ @property
44
+ def workflow_dir(self) -> Path:
45
+ """Return the directory containing workflow persistence files."""
46
+ return self.session_path.parent
47
+
48
+ @property
49
+ def history_dir(self) -> Path:
50
+ """Return the directory containing archived workflow sessions."""
51
+ return self.workflow_dir / "history"
52
+
53
+ def save(self, session: WorkflowSession) -> None:
54
+ """Write the session to disk as JSON."""
55
+ self.session_path.parent.mkdir(parents=True, exist_ok=True)
56
+ self.session_path.write_text(
57
+ json.dumps(session.to_dict(), indent=2, ensure_ascii=False),
58
+ encoding="utf-8",
59
+ )
60
+ logger.debug("Workflow session saved: %s", session.id)
61
+
62
+ def load(self) -> WorkflowSession | None:
63
+ """Load a persisted session, returning None when unavailable or invalid."""
64
+ if not self.session_path.exists():
65
+ return None
66
+ try:
67
+ data = json.loads(self.session_path.read_text(encoding="utf-8"))
68
+ if not isinstance(data, dict):
69
+ return None
70
+ return WorkflowSession.from_dict(data)
71
+ except (json.JSONDecodeError, OSError, ValueError):
72
+ logger.exception("Failed to load workflow session from %s", self.session_path)
73
+ return None
74
+
75
+ def append_event(self, event: Mapping[str, Any]) -> None:
76
+ """Append one event dictionary to the JSONL event log."""
77
+ self.events_path.parent.mkdir(parents=True, exist_ok=True)
78
+ with self.events_path.open("a", encoding="utf-8") as fh:
79
+ fh.write(json.dumps(dict(event), ensure_ascii=False) + "\n")
80
+
81
+ def load_events(self) -> list[dict[str, Any]]:
82
+ """Read event dictionaries from the JSONL event log."""
83
+ if not self.events_path.exists():
84
+ return []
85
+
86
+ events: list[dict[str, Any]] = []
87
+ try:
88
+ with self.events_path.open("r", encoding="utf-8") as fh:
89
+ for line in fh:
90
+ stripped = line.strip()
91
+ if not stripped:
92
+ continue
93
+ event = json.loads(stripped)
94
+ if isinstance(event, dict):
95
+ events.append(event)
96
+ except (json.JSONDecodeError, OSError):
97
+ logger.exception("Failed to load workflow events from %s", self.events_path)
98
+ return events
99
+
100
+ def clear(self) -> None:
101
+ """Remove persisted workflow files."""
102
+ for path in (self.session_path, self.events_path):
103
+ if path.exists():
104
+ path.unlink()
105
+
106
+ def archive_active_session(self, *, force: bool = False) -> WorkflowArchive | None:
107
+ """Move the current active session into workflow history.
108
+
109
+ Returns the archive metadata, or None when there is no meaningful active
110
+ session to preserve.
111
+ """
112
+ session = self.load()
113
+ if session is None:
114
+ self.clear()
115
+ return None
116
+ if not force and not self._has_meaningful_session(session):
117
+ self.clear()
118
+ return None
119
+
120
+ self.history_dir.mkdir(parents=True, exist_ok=True)
121
+ timestamp = int(time.time() * 1000)
122
+ session_id = self._safe_name(session.id or "workflow")
123
+ archive_path = self.history_dir / f"{timestamp}-{session_id}.json"
124
+ archive_events_path = self.history_dir / f"{timestamp}-{session_id}.events.jsonl"
125
+
126
+ archive_path.write_text(
127
+ json.dumps(session.to_dict(), indent=2, ensure_ascii=False),
128
+ encoding="utf-8",
129
+ )
130
+ events_path: Path | None = None
131
+ if self.events_path.exists():
132
+ shutil.copyfile(self.events_path, archive_events_path)
133
+ events_path = archive_events_path
134
+
135
+ self.clear()
136
+ return WorkflowArchive(session=session, path=archive_path, events_path=events_path)
137
+
138
+ def list_archives(self) -> list[WorkflowArchive]:
139
+ """Return archived workflow sessions, newest first."""
140
+ if not self.history_dir.exists():
141
+ return []
142
+
143
+ archives: list[WorkflowArchive] = []
144
+ for path in self.history_dir.glob("*.json"):
145
+ if path.name.endswith(".events.jsonl"):
146
+ continue
147
+ try:
148
+ data = json.loads(path.read_text(encoding="utf-8"))
149
+ if not isinstance(data, dict):
150
+ continue
151
+ session = WorkflowSession.from_dict(data)
152
+ except (json.JSONDecodeError, OSError, ValueError):
153
+ logger.exception("Failed to load workflow archive from %s", path)
154
+ continue
155
+
156
+ events_path = path.with_suffix(".events.jsonl")
157
+ archives.append(
158
+ WorkflowArchive(
159
+ session=session,
160
+ path=path,
161
+ events_path=events_path if events_path.exists() else None,
162
+ )
163
+ )
164
+
165
+ return sorted(
166
+ archives,
167
+ key=lambda archive: archive.session.updated_at,
168
+ reverse=True,
169
+ )
170
+
171
+ def restore_archive(self, archive: WorkflowArchive) -> WorkflowSession:
172
+ """Restore an archive into the active workflow files."""
173
+ self.session_path.parent.mkdir(parents=True, exist_ok=True)
174
+ shutil.copyfile(archive.path, self.session_path)
175
+ if archive.events_path and archive.events_path.exists():
176
+ shutil.copyfile(archive.events_path, self.events_path)
177
+ elif self.events_path.exists():
178
+ self.events_path.unlink()
179
+ return archive.session
180
+
181
+ @staticmethod
182
+ def _has_meaningful_session(session: WorkflowSession) -> bool:
183
+ """Return whether a session should be preserved in history."""
184
+ return any(
185
+ (
186
+ bool(session.goal.strip()),
187
+ session.state != WorkflowState.IDLE,
188
+ bool(session.active_agents),
189
+ bool(session.pending_questions),
190
+ bool(session.decisions),
191
+ bool(session.work_packages),
192
+ bool(session.execution_results),
193
+ bool(session.subtask_results),
194
+ bool(session.review_packages),
195
+ )
196
+ )
197
+
198
+ @staticmethod
199
+ def _safe_name(value: str) -> str:
200
+ """Return a filesystem-safe identifier fragment."""
201
+ cleaned = []
202
+ for char in value:
203
+ if char.isalnum() or char in {"-", "_"}:
204
+ cleaned.append(char)
205
+ else:
206
+ cleaned.append("-")
207
+ return "".join(cleaned).strip("-") or "workflow"
@@ -2,6 +2,9 @@
2
2
 
3
3
  from unittest.mock import MagicMock, patch
4
4
 
5
+ from prompt_toolkit.completion import CompleteEvent
6
+ from prompt_toolkit.document import Document
7
+
5
8
  from trinity.tui.prompt import TRINITY_COMMANDS, TrinityPromptSession
6
9
 
7
10
 
@@ -46,6 +49,7 @@ class TestCommandCompletion:
46
49
  "/decisions",
47
50
  "/packages",
48
51
  "/subtasks",
52
+ "/resume",
49
53
  "/help",
50
54
  "/quit",
51
55
  }
@@ -56,6 +60,25 @@ class TestCommandCompletion:
56
60
  session = TrinityPromptSession(tmp_path)
57
61
  assert session.session.completer is not None
58
62
 
63
+ def test_completion_only_appears_for_slash_prefix(self, tmp_path):
64
+ session = TrinityPromptSession(tmp_path)
65
+ completer = session.session.completer
66
+
67
+ assert completer is not None
68
+ slash_matches = list(
69
+ completer.get_completions(Document("/sta"), CompleteEvent())
70
+ )
71
+ space_matches = list(
72
+ completer.get_completions(Document(" "), CompleteEvent())
73
+ )
74
+ text_matches = list(
75
+ completer.get_completions(Document("hello /sta"), CompleteEvent())
76
+ )
77
+
78
+ assert any(match.text == "/status" for match in slash_matches)
79
+ assert space_matches == []
80
+ assert text_matches == []
81
+
59
82
 
60
83
  class TestGetInput:
61
84
  """get_input delegates to prompt_toolkit."""