flowjet 2.1.2__tar.gz → 2.1.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 (174) hide show
  1. {flowjet-2.1.2 → flowjet-2.1.3}/CHANGELOG.md +43 -0
  2. {flowjet-2.1.2 → flowjet-2.1.3}/PKG-INFO +1 -1
  3. {flowjet-2.1.2 → flowjet-2.1.3}/pyproject.toml +1 -1
  4. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/cli.py +196 -23
  5. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/stream.py +252 -108
  6. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/nano/mapping.py +9 -1
  7. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/events.py +4 -0
  8. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/pool.py +21 -3
  9. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/sandbox/settings.py +7 -2
  10. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/acp/agent.py +11 -1
  11. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/service.py +12 -2
  12. {flowjet-2.1.2 → flowjet-2.1.3}/.gitignore +0 -0
  13. {flowjet-2.1.2 → flowjet-2.1.3}/LICENSE +0 -0
  14. {flowjet-2.1.2 → flowjet-2.1.3}/README.md +0 -0
  15. {flowjet-2.1.2 → flowjet-2.1.3}/config/nano-local.yml +0 -0
  16. {flowjet-2.1.2 → flowjet-2.1.3}/config/nano-sandbox-soothe.yml +0 -0
  17. {flowjet-2.1.2 → flowjet-2.1.3}/config/nano.yml +0 -0
  18. {flowjet-2.1.2 → flowjet-2.1.3}/deploy/README.md +0 -0
  19. {flowjet-2.1.2 → flowjet-2.1.3}/docs/completion-design.md +0 -0
  20. {flowjet-2.1.2 → flowjet-2.1.3}/docs/impl/IG-001-openai-compatible-server.md +0 -0
  21. {flowjet-2.1.2 → flowjet-2.1.3}/docs/impl/IG-002-isolated-thread-pool-runtime.md +0 -0
  22. {flowjet-2.1.2 → flowjet-2.1.3}/docs/impl/IG-003-production-isolation-hardening.md +0 -0
  23. {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/RFC-001-openai-compatible-api.md +0 -0
  24. {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/RFC-002-isolated-thread-pool-runtime.md +0 -0
  25. {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/RFC-003-production-isolation-hardening.md +0 -0
  26. {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/RFC-004-unified-package.md +0 -0
  27. {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/RFC-005-arcbox-tool-sandbox.md +0 -0
  28. {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/rfc-history.md +0 -0
  29. {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/rfc-index.md +0 -0
  30. {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/rfc-namings.md +0 -0
  31. {flowjet-2.1.2 → flowjet-2.1.3}/examples/README.md +0 -0
  32. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/__init__.py +0 -0
  33. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/__main__.py +0 -0
  34. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/README.md +0 -0
  35. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/gws-drive/SKILL.md +0 -0
  36. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/gws-drive-upload/SKILL.md +0 -0
  37. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/.env.example +0 -0
  38. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/README.md +0 -0
  39. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/SKILL.md +0 -0
  40. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/assets/background_library.html +0 -0
  41. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/assets/showcase_template.html +0 -0
  42. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/references/background_styles.md +0 -0
  43. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/references/design_patterns.md +0 -0
  44. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/references/webgl_backgrounds.md +0 -0
  45. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/requirements.txt +0 -0
  46. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/scripts/generate_showcase.py +0 -0
  47. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/scripts/svg_to_png.py +0 -0
  48. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/SKILL.md +0 -0
  49. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/brainstorming/spec-document-reviewer-prompt.md +0 -0
  50. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/coding-plan-template.md +0 -0
  51. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/impl-guide-template.md +0 -0
  52. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/review/pr-review-template.md +0 -0
  53. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/review/review-checklist.md +0 -0
  54. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/specs/rfc-template.md +0 -0
  55. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-architecture-design.md +0 -0
  56. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-conceptual-design.md +0 -0
  57. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-impl-interface-design.md +0 -0
  58. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-drafts-readme.md +0 -0
  59. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-impl-readme.md +0 -0
  60. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-platonic.yml +0 -0
  61. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-history.md +0 -0
  62. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-index.md +0 -0
  63. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-namings.md +0 -0
  64. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-standard.md +0 -0
  65. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/BRAINSTORM/brainstorm.md +0 -0
  66. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-code.md +0 -0
  67. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-create-guide.md +0 -0
  68. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-full.md +0 -0
  69. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-update-guide.md +0 -0
  70. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-validate-guide.md +0 -0
  71. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-plan-modular-specs.md +0 -0
  72. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-architecture.md +0 -0
  73. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-conceptual.md +0 -0
  74. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-impl-interface.md +0 -0
  75. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scaffold.md +0 -0
  76. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scan.md +0 -0
  77. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/REFERENCE.md +0 -0
  78. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/REVIEW/review-spec-compliance.md +0 -0
  79. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-compliance.md +0 -0
  80. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-taxonomy.md +0 -0
  81. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-history.md +0 -0
  82. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-index.md +0 -0
  83. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-namings.md +0 -0
  84. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-refine.md +0 -0
  85. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-validate-consistency.md +0 -0
  86. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-overview.md +0 -0
  87. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-1.md +0 -0
  88. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-2.md +0 -0
  89. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-3.md +0 -0
  90. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/SKILL.md +0 -0
  91. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-deep.json +0 -0
  92. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-first.json +0 -0
  93. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/idea-first.json +0 -0
  94. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/loop.json +0 -0
  95. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/paper-pipeline.json +0 -0
  96. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/rapid.json +0 -0
  97. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/rigor-first.json +0 -0
  98. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/stance-first.json +0 -0
  99. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/systematic-review.json +0 -0
  100. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/verify-loop.json +0 -0
  101. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/references/backends.md +0 -0
  102. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-ars-omr.md +0 -0
  103. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-omr-ars.md +0 -0
  104. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/references/routing.md +0 -0
  105. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/scripts/csljson_to_omr.py +0 -0
  106. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/scripts/papers_index_to_csljson.py +0 -0
  107. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/__init__.py +0 -0
  108. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/__main__.py +0 -0
  109. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/agent.py +0 -0
  110. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/completion/__init__.py +0 -0
  111. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/completion/context.py +0 -0
  112. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/completion/engine.py +0 -0
  113. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/doctor_cmd.py +0 -0
  114. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/doctor_fix_cmd.py +0 -0
  115. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/progress.py +0 -0
  116. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/serve_cmd.py +0 -0
  117. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/setup_cmd.py +0 -0
  118. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/shell/__init__.py +0 -0
  119. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/shell/_fj.zsh +0 -0
  120. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/shell/fj.bash +0 -0
  121. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/threads.py +0 -0
  122. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/tool_stream.py +0 -0
  123. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/__init__.py +0 -0
  124. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/_warnings.py +0 -0
  125. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/__init__.py +0 -0
  126. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/config.py +0 -0
  127. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/factory.py +0 -0
  128. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/skills.py +0 -0
  129. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/wiring.py +0 -0
  130. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/workspace.py +0 -0
  131. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/backend_config.py +0 -0
  132. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/base.py +0 -0
  133. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/nano/__init__.py +0 -0
  134. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/nano/adapter.py +0 -0
  135. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/nano/backend.py +0 -0
  136. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/soothe/__init__.py +0 -0
  137. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/soothe/adapter.py +0 -0
  138. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/soothe/backend.py +0 -0
  139. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/identity.py +0 -0
  140. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/modes.py +0 -0
  141. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/protocol.py +0 -0
  142. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/__init__.py +0 -0
  143. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/direct.py +0 -0
  144. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/fake.py +0 -0
  145. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/__init__.py +0 -0
  146. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/adapter.py +0 -0
  147. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/admission.py +0 -0
  148. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/backend.py +0 -0
  149. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/errors.py +0 -0
  150. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/mode_gate.py +0 -0
  151. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/request.py +0 -0
  152. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/workspace.py +0 -0
  153. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/sandbox/__init__.py +0 -0
  154. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/sandbox/arbox.py +0 -0
  155. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/sandbox/resolver.py +0 -0
  156. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/streaming/__init__.py +0 -0
  157. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/streaming/presentation.py +0 -0
  158. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/streaming/tool_results.py +0 -0
  159. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/streaming/tool_stream.py +0 -0
  160. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/home.py +0 -0
  161. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/__init__.py +0 -0
  162. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/__main__.py +0 -0
  163. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/acp/__init__.py +0 -0
  164. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/acp/routes.py +0 -0
  165. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/config.py +0 -0
  166. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/http/__init__.py +0 -0
  167. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/http/app.py +0 -0
  168. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/http/auth.py +0 -0
  169. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/__init__.py +0 -0
  170. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/errors.py +0 -0
  171. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/projection.py +0 -0
  172. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/routes.py +0 -0
  173. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/schemas.py +0 -0
  174. {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/store.py +0 -0
@@ -10,6 +10,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [2.1.3] — 2026-10-23
14
+
15
+ ### Fixed
16
+
17
+ - **CLI: every failure path now surfaces a message — no more silent exits.**
18
+ The CLI could exit with no output (SIGTERM/SIGHUP, plugin `sys.exit`, broken
19
+ pipes, empty answers) and destroyed the original error stack even with `-v`.
20
+ This closes every identified gap in the error-to-output flow:
21
+
22
+ - `RunFailed` now carries `exc_type` + traceback; `stream_query` raises
23
+ `AgentRunError` so the CLI reports the real exception type and, with `-v`,
24
+ the original in-run stack instead of a re-raise site.
25
+ - SIGTERM/SIGHUP cancel the run gracefully: "interrupted by SIGTERM" with
26
+ conventional `128+signal` exit codes (was: instant silent death).
27
+ - Empty-answer runs warn on stderr ("agent produced no output · N tool calls
28
+ failed · last tool error") and exit 3.
29
+ - Partial narration is flushed to stderr before any failure/timeout/interrupt
30
+ message — a provider drop no longer loses the whole answer.
31
+ - `--timeout SECONDS` cancels a stuck run, reporting the last activity.
32
+ - `FJ_LOG_FILE=<path>` keeps WARNING+ with full tracebacks in a rotating log
33
+ for post-mortem diagnosis (incl. `logger.exception` from plugins).
34
+ - Multi-line errors truncated to one line now hint "run with -v".
35
+ - `BrokenPipeError`: exit 141 with a message; shutdown flush noise silenced
36
+ via `devnull` dup2 (`fj q | head`).
37
+ - Teardown suppresses `CancelledError` so it cannot mask the real result.
38
+ - Server parity: pool `worker_error`, OpenAI-compatible and ACP failures
39
+ populate/render `exc_type` + traceback (ACP shows "Error: <Type>: <msg>").
40
+ - Minor: thread-snapshot check failures are logged; config-load errors
41
+ honor `-v` with a traceback; plugin `sys.exit(0)` still fails loudly.
42
+
43
+ ### Changed
44
+
45
+ - **ArcBox tool sandbox: guest network is now off by default**
46
+ (`sandbox.network` defaults to `false`, was `true`). With `true` the guest is
47
+ NAT'ed through the ArcBox host and the daemon only knows ENABLED|NONE, so
48
+ everything the host can route to — internal network and corporate VPN
49
+ included — was reachable from agent shell commands. With `false` the guest
50
+ boots without a network device (no route, no DNS): shell/code execution can
51
+ reach neither the public internet nor internal services. Web search/crawl is
52
+ unaffected — `web_search`/`fetch_url` are host-placement tools and never run
53
+ in the guest. Opt back in per deployment (`sandbox.network: true` in the
54
+ nano.yml) when the guest really needs egress.
55
+
13
56
  ## [2.1.2] — 2026-10-21
14
57
 
15
58
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flowjet
3
- Version: 2.1.2
3
+ Version: 2.1.3
4
4
  Summary: FlowJet — agent CLI (fj) and server (ACP + OpenAI-compatible HTTP) powered by Soothe
5
5
  Project-URL: Homepage, https://github.com/caesar0301/flowjet
6
6
  Project-URL: Repository, https://github.com/caesar0301/flowjet
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flowjet"
7
- version = "2.1.2"
7
+ version = "2.1.3"
8
8
  description = "FlowJet — agent CLI (fj) and server (ACP + OpenAI-compatible HTTP) powered by Soothe"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -10,7 +10,9 @@ import asyncio
10
10
  import contextlib
11
11
  import logging
12
12
  import os
13
+ import signal
13
14
  import sys
15
+ import traceback
14
16
  from collections.abc import Coroutine
15
17
  from logging.handlers import RotatingFileHandler
16
18
  from pathlib import Path
@@ -21,6 +23,8 @@ from flowjet.cli import __version__
21
23
  FORMAL_CLI = "flowjet"
22
24
  CLI_ALIASES = frozenset({"fj", "fjf", FORMAL_CLI})
23
25
 
26
+ _logger = logging.getLogger(__name__)
27
+
24
28
 
25
29
  # ---------------------------------------------------------------------------
26
30
  # CLI logging — stdlib only, kept here so ``fj -V`` / ``fj -h`` never pay the
@@ -29,18 +33,30 @@ CLI_ALIASES = frozenset({"fj", "fjf", FORMAL_CLI})
29
33
 
30
34
  _BROWSER_USE_SETUP_LOGGING = "BROWSER_USE_SETUP_LOGGING"
31
35
  _FJ_CONSOLE_HANDLER = "fj-console"
36
+ _FJ_FILE_HANDLER = "fj-file"
37
+ _LOG_FILE_ENV = "FJ_LOG_FILE"
32
38
 
33
39
 
34
40
  class _CompactConsoleFormatter(logging.Formatter):
35
- """One-line console records without exception tracebacks."""
41
+ """One-line console records; exceptions summarized on the same line.
42
+
43
+ Full tracebacks would smash the ephemeral progress line, but dropping the
44
+ exception entirely (the old behavior) hid the root cause even with ``-v``.
45
+ The full stack for run failures is available via ``AgentRunError``.
46
+ """
36
47
 
37
48
  def formatException(self, ei: object) -> str: # noqa: N802 - logging API
38
49
  return ""
39
50
 
40
51
  def format(self, record: logging.LogRecord) -> str:
41
- # logger.exception sets exc_info; drop it so format() stays one line.
42
- record.exc_info = None
43
- record.exc_text = None
52
+ # logger.exception sets exc_info; fold "type: message" into the record
53
+ # message so format() stays one line without losing the cause.
54
+ if record.exc_info:
55
+ exc_val = record.exc_info[1]
56
+ if exc_val is not None:
57
+ record.msg = f"{record.getMessage()}: {type(exc_val).__name__}: {exc_val}"
58
+ record.exc_info = None
59
+ record.exc_text = None
44
60
  return super().format(record)
45
61
 
46
62
 
@@ -52,6 +68,8 @@ def configure_cli_logging(*, verbose: bool = False) -> None:
52
68
  lines do not interleave with the agent answer.
53
69
  - Prevent ``lastResort`` traceback dumps from soothe tool failures.
54
70
  - When ``verbose``, show WARNING+ as single-line messages on stderr.
71
+ - When ``FJ_LOG_FILE`` is set, mirror WARNING+ (with full tracebacks) to
72
+ that rotating file for post-mortem diagnosis of silent failures.
55
73
  """
56
74
  os.environ.setdefault(_BROWSER_USE_SETUP_LOGGING, "false")
57
75
  _remove_root_console_handlers()
@@ -68,6 +86,30 @@ def configure_cli_logging(*, verbose: bool = False) -> None:
68
86
  root.addHandler(null)
69
87
  if root.level < logging.WARNING:
70
88
  root.setLevel(logging.WARNING)
89
+ _attach_file_log_handler(root)
90
+
91
+
92
+ def _attach_file_log_handler(root: logging.Logger) -> None:
93
+ """Attach the ``FJ_LOG_FILE`` rotating handler once, if requested.
94
+
95
+ Unlike the compact console formatter, the file keeps full tracebacks, so
96
+ ``logger.exception`` records from soothe/plugins stay diagnosable after a
97
+ crash — including for deaths the CLI itself cannot report (SIGKILL).
98
+ """
99
+ if any(getattr(h, "name", None) == _FJ_FILE_HANDLER for h in root.handlers):
100
+ return
101
+ path = os.environ.get(_LOG_FILE_ENV, "").strip()
102
+ if not path:
103
+ return
104
+ try:
105
+ handler = RotatingFileHandler(path, maxBytes=2_000_000, backupCount=2, encoding="utf-8")
106
+ except OSError:
107
+ _logger.warning("FJ_LOG_FILE is not writable: %s", path)
108
+ return
109
+ handler.set_name(_FJ_FILE_HANDLER)
110
+ handler.setLevel(logging.WARNING)
111
+ handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)-8s [%(name)s] %(message)s"))
112
+ root.addHandler(handler)
71
113
 
72
114
 
73
115
  def silence_after_plugins(*, verbose: bool = False) -> None:
@@ -155,13 +197,18 @@ notes:
155
197
  • Bypass mode (skip all security enforcement) is the CLI default — use -a for guardrails
156
198
  • One query per thread at a time; different threads may run concurrently
157
199
  • With -v, prints thread <id> and the project dir on stderr before the run
200
+ • --timeout N cancels a run after N seconds (0 = no limit)
201
+ • Exit codes: 0 ok · 1 error · 2 usage · 3 no output · 130/143 interrupted
202
+ • Set FJ_LOG_FILE=<path> to keep warnings & errors (with tracebacks) on file
158
203
  """
159
204
 
160
205
 
161
206
  # Boolean short flags that may appear clustered (``-lv`` → ``-l -v``).
162
207
  _BOOL_SHORTS = frozenset({"h", "V", "l", "v", "f", "a"})
163
208
  # Short flags that consume the next argv token as a value.
164
- _VALUE_FLAGS = frozenset({"-c", "--config", "-t", "--thread", "-w", "--workspace", "-n"})
209
+ _VALUE_FLAGS = frozenset(
210
+ {"-c", "--config", "-t", "--thread", "-w", "--workspace", "-n", "--timeout"}
211
+ )
165
212
  _FLAG_ONLY = frozenset(
166
213
  {
167
214
  "-h",
@@ -179,7 +226,7 @@ _FLAG_ONLY = frozenset(
179
226
  "--ask",
180
227
  }
181
228
  )
182
- _EQUALS_PREFIXES = ("--config=", "--thread=", "--workspace=")
229
+ _EQUALS_PREFIXES = ("--config=", "--thread=", "--workspace=", "--timeout=")
183
230
 
184
231
 
185
232
  def _expand_short_cluster(tok: str) -> list[str] | None:
@@ -244,6 +291,7 @@ def validate_arg_composition(args: Any) -> str | None:
244
291
  - ``-n`` requires ``-l``/``--list``
245
292
  - ``-l`` is exclusive with a query, ``-f``, ``-t``, ``-w``, ``--no-stream``, ``--ask``
246
293
  - ``-t``/``--thread`` overrides ``-f``/``--follow`` when both are given
294
+ - ``--timeout`` must be >= 0 (0 = no limit)
247
295
  """
248
296
  listing = bool(getattr(args, "list", False))
249
297
  list_limit = getattr(args, "list_limit", None)
@@ -257,6 +305,10 @@ def validate_arg_composition(args: Any) -> str | None:
257
305
  if list_limit is not None and not listing:
258
306
  return "-n requires -l/--list"
259
307
 
308
+ timeout = getattr(args, "timeout", None)
309
+ if timeout is not None and timeout < 0:
310
+ return "--timeout must be >= 0 (0 = no limit)"
311
+
260
312
  if listing:
261
313
  if query:
262
314
  return "-l/--list does not take a query (got leftover text after options)"
@@ -376,6 +428,14 @@ def _build_parser(prog: str | None = None) -> argparse.ArgumentParser:
376
428
  help="Ask mode: answer the query with no tools (pure Q&A)",
377
429
  )
378
430
 
431
+ run = parser.add_argument_group("run")
432
+ run.add_argument(
433
+ "--timeout",
434
+ metavar="SECONDS",
435
+ type=float,
436
+ help="Cancel the run after N seconds (0 = no limit)",
437
+ )
438
+
379
439
  paths = parser.add_argument_group("paths")
380
440
  paths.add_argument(
381
441
  "-c",
@@ -458,6 +518,8 @@ def _load_config_or_exit(args: argparse.Namespace) -> Any | int:
458
518
  return 1
459
519
  except Exception as exc:
460
520
  sys.stderr.write(f"error: failed to load config: {exc}\n")
521
+ if getattr(args, "verbose", False):
522
+ sys.stderr.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
461
523
  return 1
462
524
 
463
525
 
@@ -517,7 +579,10 @@ async def _detect_pending_interrupt(agent: Any, thread_id: str) -> Any:
517
579
  return None
518
580
  try:
519
581
  snapshot = await get_state({"configurable": {"thread_id": thread_id}})
520
- except Exception:
582
+ except Exception as exc:
583
+ # Not fatal — but explain why a pending question may be missed and a
584
+ # new turn started instead of resuming (visible with -v / FJ_LOG_FILE).
585
+ _logger.warning("thread snapshot check failed; starting a new turn: %s", exc)
521
586
  return None
522
587
  if snapshot is None:
523
588
  return None
@@ -567,6 +632,9 @@ async def run_async(args: argparse.Namespace) -> int:
567
632
  return config
568
633
 
569
634
  workspace = Path(args.workspace).expanduser().resolve() if args.workspace else None
635
+ # ``--timeout 0`` means no limit (same as omitting the flag).
636
+ timeout_raw = getattr(args, "timeout", None)
637
+ timeout_s = float(timeout_raw) if timeout_raw else None
570
638
 
571
639
  try:
572
640
  async with open_sqlite_checkpointer(config) as checkpointer:
@@ -607,19 +675,21 @@ async def run_async(args: argparse.Namespace) -> int:
607
675
  sys.stdout.flush()
608
676
  resume_value = {"answers": [args.query_text]}
609
677
  if args.no_stream:
610
- await invoke_query(
678
+ result = await invoke_query(
611
679
  agent,
612
680
  args.query_text,
613
681
  thread_id=thread_id,
614
682
  resume_value=resume_value,
683
+ timeout_s=timeout_s,
615
684
  )
616
685
  else:
617
- await stream_query(
686
+ result = await stream_query(
618
687
  agent,
619
688
  args.query_text,
620
689
  thread_id=thread_id,
621
690
  show_tool_calls=args.verbose,
622
691
  resume_value=resume_value,
692
+ timeout_s=timeout_s,
623
693
  )
624
694
  except ConcurrentSessionError as exc:
625
695
  sys.stderr.write(f"error: {exc}\n")
@@ -627,6 +697,26 @@ async def run_async(args: argparse.Namespace) -> int:
627
697
  except KeyboardInterrupt:
628
698
  sys.stderr.write("\ninterrupted\n")
629
699
  return 130
700
+ except asyncio.CancelledError:
701
+ sig = _interrupt_signal
702
+ if sig is not None:
703
+ name = signal.Signals(sig).name
704
+ sys.stderr.write(f"\ninterrupted by {name} (signal {sig})\n")
705
+ return 128 + sig
706
+ sys.stderr.write("error: run cancelled\n")
707
+ return 130
708
+ except BrokenPipeError:
709
+ # Our stdout consumer (e.g. ``fj q | head``) closed early. The final
710
+ # flush guard in ``main`` silences the shutdown-time noise.
711
+ sys.stderr.write("error: output pipe closed\n")
712
+ return 141
713
+ except SystemExit as exc:
714
+ # A plugin/tool called sys.exit() mid-run; without this the process
715
+ # would exit with no message at all.
716
+ sys.stderr.write(
717
+ f"error: unexpected exit({exc.code}) raised during the run (plugin/tool called sys.exit)\n"
718
+ )
719
+ return 1
630
720
  except Exception as exc:
631
721
  from flowjet.cli.stream import write_cli_error
632
722
 
@@ -639,6 +729,10 @@ async def run_async(args: argparse.Namespace) -> int:
639
729
  append_history(args.query_text)
640
730
  except Exception:
641
731
  pass
732
+ if not result:
733
+ # Completed but produced no answer text (a warning is already on
734
+ # stderr) — scripts need a distinct, detectable exit code.
735
+ return 3
642
736
  return 0
643
737
 
644
738
 
@@ -646,6 +740,55 @@ async def run_async(args: argparse.Namespace) -> int:
646
740
  # closed anyway.
647
741
  _TEARDOWN_GRACE_S = 3.0
648
742
 
743
+ # Signal (SIGTERM/SIGHUP) that cancelled the current run, if any. Module-level
744
+ # because the loop-installed callback and `run_async` share the main thread.
745
+ _interrupt_signal: int | None = None
746
+
747
+
748
+ def _on_interrupt_signal(sig: int, task: asyncio.Task[int]) -> None:
749
+ # Convert SIGTERM/SIGHUP into a graceful task cancel (recorded for the exit code).
750
+ global _interrupt_signal
751
+ if _interrupt_signal is None:
752
+ _interrupt_signal = int(sig)
753
+ if not task.done():
754
+ task.cancel()
755
+
756
+
757
+ def _install_signal_cancellation(
758
+ loop: asyncio.AbstractEventLoop,
759
+ task: asyncio.Task[int],
760
+ ) -> list[tuple[int, Any]]:
761
+ # Route SIGTERM/SIGHUP through the loop so the run unwinds cleanly.
762
+ #
763
+ # Without this the default disposition kills the process instantly — no
764
+ # cleanup, no message (the "no message and exit" symptom). Platforms
765
+ # without add_signal_handler (Windows) or non-main-thread callers keep
766
+ # the default behaviour.
767
+ installed: list[tuple[int, Any]] = []
768
+ for name in ("SIGTERM", "SIGHUP"):
769
+ sig = getattr(signal, name, None)
770
+ if sig is None:
771
+ continue
772
+ previous = signal.getsignal(sig)
773
+ try:
774
+ loop.add_signal_handler(sig, _on_interrupt_signal, sig, task)
775
+ except (NotImplementedError, RuntimeError, ValueError, OSError):
776
+ continue
777
+ installed.append((int(sig), previous))
778
+ return installed
779
+
780
+
781
+ def _remove_signal_cancellation(
782
+ loop: asyncio.AbstractEventLoop,
783
+ installed: list[tuple[int, Any]],
784
+ ) -> None:
785
+ for sig, previous in installed:
786
+ with contextlib.suppress(Exception, asyncio.CancelledError):
787
+ loop.remove_signal_handler(signal.Signals(sig))
788
+ if previous not in (signal.SIG_DFL, None):
789
+ with contextlib.suppress(Exception, asyncio.CancelledError):
790
+ signal.signal(signal.Signals(sig), previous) # type: ignore[arg-type]
791
+
649
792
 
650
793
  async def _cancel_stray_tasks(loop: asyncio.AbstractEventLoop, grace: float) -> None:
651
794
  """Cancel every task but this one, and close async generators, within ``grace``.
@@ -670,13 +813,24 @@ def run_one_shot(coro: Coroutine[Any, Any, int], *, grace: float = _TEARDOWN_GRA
670
813
  one task that swallows ``CancelledError`` pins the process after the answer
671
814
  has already been printed. ``loop.close()`` is also where libraries install
672
815
  their own cleanup hooks, so it runs whether or not the strays went quietly.
816
+
817
+ SIGTERM/SIGHUP cancel the main task instead of killing the process, so the
818
+ run unwinds with a visible message and a conventional 128+signal exit code.
673
819
  """
820
+ global _interrupt_signal
821
+ _interrupt_signal = None
674
822
  loop = asyncio.new_event_loop()
823
+ installed: list[tuple[int, Any]] = []
675
824
  try:
676
825
  asyncio.set_event_loop(loop)
677
- return loop.run_until_complete(coro)
826
+ task = loop.create_task(coro, name="fj-run")
827
+ installed = _install_signal_cancellation(loop, task)
828
+ return loop.run_until_complete(task)
678
829
  finally:
679
- with contextlib.suppress(Exception):
830
+ _remove_signal_cancellation(loop, installed)
831
+ # CancelledError is a BaseException — a stray re-cancel during teardown
832
+ # must not mask the real result/exception.
833
+ with contextlib.suppress(Exception, asyncio.CancelledError):
680
834
  loop.run_until_complete(_cancel_stray_tasks(loop, grace))
681
835
  asyncio.set_event_loop(None)
682
836
  loop.close()
@@ -702,6 +856,15 @@ def main_follow(argv: list[str] | None = None) -> int:
702
856
  return main(_inject_follow(raw))
703
857
 
704
858
 
859
+ def _handle_broken_pipe() -> int:
860
+ """Silence the shutdown flush noise after our stdout consumer closed early."""
861
+ try:
862
+ os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
863
+ except (OSError, ValueError, AttributeError): # pragma: no cover - platform
864
+ pass
865
+ return 141 # 128 + SIGPIPE
866
+
867
+
705
868
  def main(argv: list[str] | None = None) -> int:
706
869
  # Silence noisy third-party warnings (requests version-mismatch,
707
870
  # LangChain beta-API) before any heavy import pulls them in.
@@ -721,33 +884,43 @@ def main(argv: list[str] | None = None) -> int:
721
884
  if args.command == "setup":
722
885
  from flowjet.cli.setup_cmd import run_setup
723
886
 
724
- return run_setup(getattr(args, "config", None))
725
- if args.command == "doctor":
887
+ result = run_setup(getattr(args, "config", None))
888
+ elif args.command == "doctor":
726
889
  from flowjet.cli.doctor_cmd import run_doctor
727
890
 
728
- return run_doctor(getattr(args, "doctor_argv", []))
729
- if args.command == "doctor-fix":
891
+ result = run_doctor(getattr(args, "doctor_argv", []))
892
+ elif args.command == "doctor-fix":
730
893
  from flowjet.cli.doctor_fix_cmd import run_doctor_fix
731
894
 
732
- return run_doctor_fix(getattr(args, "doctor_fix_argv", []))
733
- if args.command == "__complete":
895
+ result = run_doctor_fix(getattr(args, "doctor_fix_argv", []))
896
+ elif args.command == "__complete":
734
897
  from flowjet.cli.completion import run_complete
735
898
 
736
- return run_complete(getattr(args, "complete_argv", []))
737
- if args.command == "serve":
899
+ result = run_complete(getattr(args, "complete_argv", []))
900
+ elif args.command == "serve":
738
901
  # Lazy: the server extra (fastapi/uvicorn/acp) must never be an
739
902
  # import-time dependency of the CLI.
740
903
  from flowjet.cli.serve_cmd import run_serve
741
904
 
742
- return run_serve(getattr(args, "serve_argv", []))
743
- if args.command == "completion":
905
+ result = run_serve(getattr(args, "serve_argv", []))
906
+ elif args.command == "completion":
744
907
  from flowjet.cli.completion import run_completion_script
745
908
 
746
- return run_completion_script(getattr(args, "completion_argv", []))
747
- return run_one_shot(run_async(args))
909
+ result = run_completion_script(getattr(args, "completion_argv", []))
910
+ else:
911
+ result = run_one_shot(run_async(args))
748
912
  except KeyboardInterrupt:
749
913
  sys.stderr.write("\ninterrupted\n")
750
914
  return 130
915
+ except BrokenPipeError:
916
+ return _handle_broken_pipe()
917
+ try:
918
+ # Flush before returning: a closed consumer would otherwise trigger
919
+ # "Exception ignored" noise during interpreter shutdown.
920
+ sys.stdout.flush()
921
+ except BrokenPipeError:
922
+ return _handle_broken_pipe()
923
+ return result
751
924
 
752
925
 
753
926
  if __name__ == "__main__": # pragma: no cover