@scemoon/cdh 1.0.4 → 1.0.6

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 (269) hide show
  1. package/ai-dlc-skill/SKILL.md +4 -2
  2. package/ai-dlc-skill/agents/master.md +6 -0
  3. package/ai-dlc-skill/agents/plan-agent.md +2 -2
  4. package/ai-dlc-skill/agents/understand-agent.md +4 -4
  5. package/ai-dlc-skill/agents/verify-agent.md +1 -1
  6. package/ai-dlc-skill/architecture/project-structure.md +10 -10
  7. package/ai-dlc-skill/brownfield/README.md +2 -2
  8. package/ai-dlc-skill/brownfield/generate-context.sh +3 -3
  9. package/ai-dlc-skill/brownfield/scripts/extract-api.sh +5 -5
  10. package/ai-dlc-skill/components/backend.md +1 -1
  11. package/ai-dlc-skill/components/web.md +2 -2
  12. package/ai-dlc-skill/contracts/README.md +3 -3
  13. package/ai-dlc-skill/cross-tool/cline/export.sh +1 -1
  14. package/ai-dlc-skill/cross-tool/copilot/export.sh +1 -1
  15. package/ai-dlc-skill/cross-tool/onecode/export-skills.sh +7 -3
  16. package/ai-dlc-skill/cross-tool/onecode/export.sh +14 -8
  17. package/ai-dlc-skill/phases/deliver/lifecycle.md +6 -6
  18. package/ai-dlc-skill/phases/deliver/prompt.md +2 -2
  19. package/ai-dlc-skill/phases/deliver/rules.md +1 -1
  20. package/ai-dlc-skill/phases/plan/lifecycle.md +11 -11
  21. package/ai-dlc-skill/phases/plan/prompt.md +6 -6
  22. package/ai-dlc-skill/phases/plan/rules.md +1 -1
  23. package/ai-dlc-skill/phases/understand/entry.md +1 -1
  24. package/ai-dlc-skill/phases/understand/lifecycle.md +7 -7
  25. package/ai-dlc-skill/phases/understand/prompt.md +8 -8
  26. package/ai-dlc-skill/phases/understand/rules.md +5 -5
  27. package/ai-dlc-skill/phases/verify/lifecycle.md +24 -24
  28. package/ai-dlc-skill/phases/verify/prompt.md +8 -8
  29. package/ai-dlc-skill/phases/verify/rules.md +9 -9
  30. package/ai-dlc-skill/providers/README.md +2 -2
  31. package/ai-dlc-skill/providers/aliyun/preview.yaml +2 -2
  32. package/ai-dlc-skill/providers/tcb/preview.yaml +2 -2
  33. package/ai-dlc-skill/requirements.md +7 -7
  34. package/ai-dlc-skill/skill.yaml +4 -4
  35. package/ai-dlc-skill/templates/integration/CHANGELOG.md +2 -2
  36. package/ai-dlc-skill/templates/integration/contract-diff.md +7 -7
  37. package/ai-dlc-skill/templates/integration/contract-spec.md +7 -7
  38. package/ai-dlc-skill/templates/integration/runtime-contract.yaml +1 -1
  39. package/ai-dlc-skill/templates/project/README.md +18 -19
  40. package/ai-dlc-skill/templates/project/template.md +21 -21
  41. package/ai-dlc-skill/walkthrough/collect.sh +1 -1
  42. package/ai-dlc-skill/walkthrough/template.md +2 -2
  43. package/ai-dlc-skill/workflows/ai-dlc.yaml +12 -12
  44. package/ai-dlc-skill/workflows/deployment.yaml +4 -4
  45. package/cdh/__pycache__/cdh_mcp_injector.cpython-314.pyc +0 -0
  46. package/cdh/__pycache__/cdh_mcp_loader.cpython-314.pyc +0 -0
  47. package/cdh/__pycache__/cdh_mcp_manager.cpython-314.pyc +0 -0
  48. package/cdh/__pycache__/cdh_session_aggregator.cpython-314.pyc +0 -0
  49. package/cdh/__pycache__/cdh_skill_loader.cpython-314.pyc +0 -0
  50. package/cdh/__pycache__/cdh_skill_manager.cpython-314.pyc +0 -0
  51. package/cdh/__pycache__/cli.cpython-313.pyc +0 -0
  52. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  53. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  54. package/cdh/cdh_mcp_injector.py +115 -0
  55. package/cdh/cdh_mcp_loader.py +67 -0
  56. package/cdh/cdh_mcp_manager.py +73 -0
  57. package/cdh/cdh_session_aggregator.py +258 -0
  58. package/cdh/cdh_skill_manager.py +105 -0
  59. package/cdh/cli.py +64 -4
  60. package/cdh/scaffold.py +192 -41
  61. package/onecode/__init__.py +1 -1
  62. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  63. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  64. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  65. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  66. package/onecode/__pycache__/migrate.cpython-314.pyc +0 -0
  67. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  68. package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
  69. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  70. package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
  71. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  72. package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
  73. package/onecode/agent/__pycache__/project_doc.cpython-314.pyc +0 -0
  74. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  75. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  76. package/onecode/agent/agents/types.py +47 -164
  77. package/onecode/agent/attachments.py +0 -1
  78. package/onecode/agent/cdh_loader.py +17 -25
  79. package/onecode/agent/context.py +8 -3
  80. package/onecode/agent/engine.py +835 -220
  81. package/onecode/agent/hooks.py +1 -1
  82. package/onecode/agent/onecode_agent_acp.py +460 -47
  83. package/onecode/agent/onecode_agent_acp.py.bak +2447 -0
  84. package/onecode/agent/permissions.py +0 -1
  85. package/onecode/agent/project_doc.py +19 -0
  86. package/onecode/agent/session.py +17 -3
  87. package/onecode/agent/snapshot.py +3 -4
  88. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  89. package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
  90. package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
  91. package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
  92. package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
  93. package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
  94. package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
  95. package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
  96. package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
  97. package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
  98. package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
  99. package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
  100. package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
  101. package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
  102. package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
  103. package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
  104. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  105. package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
  106. package/onecode/agent/tools/agent_tools.py +4 -6
  107. package/onecode/agent/tools/apply_patch_tool.py +2 -8
  108. package/onecode/agent/tools/bash_tool.py +32 -12
  109. package/onecode/agent/tools/communication_tools.py +4 -5
  110. package/onecode/agent/tools/config_tool.py +3 -3
  111. package/onecode/agent/tools/cron_tools.py +4 -5
  112. package/onecode/agent/tools/file_ops.py +37 -3
  113. package/onecode/agent/tools/file_tools.py +10 -11
  114. package/onecode/agent/tools/git_tools.py +2 -2
  115. package/onecode/agent/tools/lsp_tools.py +2 -2
  116. package/onecode/agent/tools/mcp_tools.py +3 -5
  117. package/onecode/agent/tools/permission_handler.py +1 -1
  118. package/onecode/agent/tools/protocol.py +1 -1
  119. package/onecode/agent/tools/registry.py +69 -2
  120. package/onecode/agent/tools/sandbox.py +110 -16
  121. package/onecode/agent/tools/skill_tools.py +2 -2
  122. package/onecode/agent/tools/todo_tools.py +38 -19
  123. package/onecode/agent/tools/web_tools.py +6 -7
  124. package/onecode/builtin_skills/agent-browser/SKILL.md +212 -0
  125. package/onecode/builtin_skills/agent-browser/skill.yaml +8 -0
  126. package/onecode/builtin_skills/skill-creator/SKILL.md +106 -0
  127. package/onecode/builtin_skills/skill-creator/skill.yaml +8 -0
  128. package/onecode/cli.py +186 -6
  129. package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
  130. package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
  131. package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
  132. package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
  133. package/onecode/codebase/chunker.py +0 -5
  134. package/onecode/codebase/indexer.py +14 -1
  135. package/onecode/codebase/retriever.py +13 -2
  136. package/onecode/codebase/storage.py +5 -4
  137. package/onecode/commands.py +81 -6
  138. package/onecode/config.py +41 -12
  139. package/onecode/config_screen.py +7 -3
  140. package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
  141. package/onecode/mcp/manager.py +2 -3
  142. package/onecode/memory/__init__.py +7 -53
  143. package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
  144. package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
  145. package/onecode/memory/__pycache__/offload.cpython-314.pyc +0 -0
  146. package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
  147. package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
  148. package/onecode/memory/backend.py +13 -39
  149. package/onecode/memory/pyramid.py +2 -71
  150. package/onecode/memory/recall.py +38 -3
  151. package/onecode/migrate.py +122 -0
  152. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  153. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  154. package/onecode/models/messages.py +58 -0
  155. package/onecode/models/provider.py +20 -2
  156. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  157. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  158. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  159. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  160. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  161. package/onecode/models/providers/anthropic_provider.py +59 -12
  162. package/onecode/models/providers/glm_provider.py +40 -0
  163. package/onecode/models/providers/minimax_provider.py +3 -0
  164. package/onecode/models/providers/minimaxi_provider.py +3 -0
  165. package/onecode/models/providers/ollama_provider.py +3 -0
  166. package/onecode/server/app.py +1 -1
  167. package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
  168. package/onecode/skills/__pycache__/bootstrap.cpython-314.pyc +0 -0
  169. package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
  170. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  171. package/onecode/skills/argument_substitution.py +0 -1
  172. package/onecode/skills/bootstrap.py +124 -0
  173. package/onecode/skills/loader.py +35 -53
  174. package/onecode/skills/manager.py +2 -2
  175. package/onecode/storage/activity.py +3 -4
  176. package/onecode/storage/session.py +3 -4
  177. package/onecode/tasks/manager.py +23 -10
  178. package/onecode/trace/tracer.py +2 -3
  179. package/package.json +1 -1
  180. package/pyproject.toml +5 -1
  181. package/run.js +68 -14
  182. package/tui/__pycache__/about.cpython-314.pyc +0 -0
  183. package/tui/__pycache__/app.cpython-313.pyc +0 -0
  184. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  185. package/tui/__pycache__/danger.cpython-314.pyc +0 -0
  186. package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
  187. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  188. package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
  189. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  190. package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
  191. package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
  192. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  193. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  194. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  195. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  196. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  197. package/tui/acp/agent.py +125 -58
  198. package/tui/acp/api.py +12 -0
  199. package/tui/acp/messages.py +3 -1
  200. package/tui/acp/prompt.py +0 -57
  201. package/tui/acp/protocol.py +6 -0
  202. package/tui/app.py +11 -33
  203. package/tui/messages.py +2 -0
  204. package/tui/path_complete.py +1 -1
  205. package/tui/paths.py +16 -0
  206. package/tui/screens/__pycache__/agent_modal.cpython-314.pyc +0 -0
  207. package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
  208. package/tui/screens/__pycache__/diff_screen.cpython-314.pyc +0 -0
  209. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  210. package/tui/screens/__pycache__/main.cpython-313.pyc +0 -0
  211. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  212. package/tui/screens/__pycache__/permissions.cpython-314.pyc +0 -0
  213. package/tui/screens/__pycache__/projects_app.cpython-314.pyc +0 -0
  214. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  215. package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
  216. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  217. package/tui/screens/__pycache__/tool_content_screen.cpython-314.pyc +0 -0
  218. package/tui/screens/component_picker.py +35 -15
  219. package/tui/screens/diff_screen.py +84 -0
  220. package/tui/screens/log.py +25 -25
  221. package/tui/screens/main.py +48 -26
  222. package/tui/screens/projects.tcss +5 -1
  223. package/tui/screens/projects_app.py +6 -6
  224. package/tui/screens/projects_screen.py +19 -39
  225. package/tui/screens/sessions.py +3 -5
  226. package/tui/session_tracker.py +3 -0
  227. package/tui/settings_schema.py +1 -1
  228. package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
  229. package/tui/visuals/columns.py +1 -2
  230. package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
  231. package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
  232. package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
  233. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  234. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  235. package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
  236. package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
  237. package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
  238. package/tui/widgets/__pycache__/load_more.cpython-314.pyc +0 -0
  239. package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
  240. package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
  241. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  242. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  243. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  244. package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
  245. package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
  246. package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
  247. package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
  248. package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
  249. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  250. package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
  251. package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
  252. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  253. package/tui/widgets/agent_response.py +0 -1
  254. package/tui/widgets/agent_thought.py +0 -1
  255. package/tui/widgets/conversation.py +157 -14
  256. package/tui/widgets/diff_view.py +10 -0
  257. package/tui/widgets/load_more.py +24 -0
  258. package/tui/widgets/modified_files.py +86 -12
  259. package/tui/widgets/plan.py +1 -1
  260. package/tui/widgets/project_grid_select.py +9 -2
  261. package/tui/widgets/session_grid_select.py +6 -0
  262. package/tui/widgets/side_bar.py +10 -0
  263. package/tui/widgets/subagent.py +92 -58
  264. package/tui/widgets/subagent_screen.py +638 -115
  265. package/tui/widgets/terminal.py +1 -2
  266. package/tui/widgets/terminal_tool.py +11 -2
  267. package/tui/widgets/tool_call.py +35 -2
  268. package/onecode/memory/offload.py +0 -114
  269. package/onecode/memory/symbolic.py +0 -164
@@ -1,15 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
3
  import uuid
5
4
  from datetime import datetime, timezone
6
5
  from pathlib import Path
7
6
  from typing import Optional
8
7
 
9
- from sqlalchemy import Column, String, Text, DateTime, JSON, create_engine
8
+ from sqlalchemy import Column, String, DateTime, JSON, create_engine
10
9
  from sqlalchemy.orm import declarative_base, sessionmaker
11
10
 
12
- from onecode.config import CLOUD_DEV_HARNESS_DIR
11
+ from onecode.config import ONECODE_DIR
13
12
 
14
13
  Base = declarative_base()
15
14
 
@@ -28,7 +27,7 @@ class ActivityRecord(Base):
28
27
  class ActivityRecorder:
29
28
  def __init__(self, db_path: Optional[Path] = None):
30
29
  if db_path is None:
31
- db_path = CLOUD_DEV_HARNESS_DIR / "sessions" / "cdh.db"
30
+ db_path = ONECODE_DIR / "sessions" / "cdh.db"
32
31
  db_path.parent.mkdir(parents=True, exist_ok=True)
33
32
  self.engine = create_engine(f"sqlite:///{db_path}")
34
33
  Base.metadata.create_all(self.engine)
@@ -9,14 +9,13 @@ from typing import Optional
9
9
  from sqlalchemy import (
10
10
  Column,
11
11
  String,
12
- Text,
13
12
  DateTime,
14
13
  JSON,
15
14
  create_engine,
16
15
  )
17
- from sqlalchemy.orm import declarative_base, Session as SASession, sessionmaker
16
+ from sqlalchemy.orm import declarative_base, sessionmaker
18
17
 
19
- from onecode.config import CLOUD_DEV_HARNESS_DIR
18
+ from onecode.config import ONECODE_DIR
20
19
 
21
20
  Base = declarative_base()
22
21
 
@@ -43,7 +42,7 @@ class SessionRecord(Base):
43
42
  class SessionStore:
44
43
  def __init__(self, db_path: Optional[Path] = None):
45
44
  if db_path is None:
46
- db_path = CLOUD_DEV_HARNESS_DIR / "sessions" / "cdh.db"
45
+ db_path = ONECODE_DIR / "sessions" / "cdh.db"
47
46
  db_path.parent.mkdir(parents=True, exist_ok=True)
48
47
  self.engine = create_engine(f"sqlite:///{db_path}")
49
48
  Base.metadata.create_all(self.engine)
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import logging
5
5
  import uuid
6
- from dataclasses import dataclass, field
6
+ from dataclasses import dataclass
7
7
  from datetime import datetime
8
8
  from pathlib import Path
9
9
  from typing import Any, Callable, Optional
@@ -16,8 +16,6 @@ from onecode.tasks.models import (
16
16
  TaskChecklistState,
17
17
  TaskGateRecord,
18
18
  TaskToolCallSummary,
19
- TodoItem,
20
- TodoStatus,
21
19
  )
22
20
 
23
21
 
@@ -56,6 +54,8 @@ class TaskManager:
56
54
  self._storage_path = storage_path or Path.home() / ".cdh" / "tasks"
57
55
  self._executor: Optional[TaskExecutor] = None
58
56
  self._worker_task: Optional[asyncio.Task] = None
57
+ self._semaphore = asyncio.Semaphore(5)
58
+ self._shutdown: bool = False
59
59
 
60
60
  def set_executor(self, executor: TaskExecutor) -> None:
61
61
  self._executor = executor
@@ -90,11 +90,14 @@ class TaskManager:
90
90
  self._worker_task = asyncio.create_task(self._worker_loop())
91
91
 
92
92
  async def _worker_loop(self) -> None:
93
- while True:
93
+ while not self._shutdown:
94
94
  try:
95
- task = await self._queue.get()
95
+ task = await asyncio.wait_for(self._queue.get(), timeout=30)
96
96
  if task.id not in self._running:
97
- self._running[task.id] = asyncio.create_task(self._run_task(task))
97
+ async with self._semaphore:
98
+ self._running[task.id] = asyncio.create_task(self._run_task(task))
99
+ except asyncio.TimeoutError:
100
+ continue
98
101
  except asyncio.CancelledError:
99
102
  break
100
103
  except Exception as e:
@@ -112,7 +115,10 @@ class TaskManager:
112
115
  if self._executor:
113
116
  cancel = asyncio.Event()
114
117
  events: asyncio.Queue = asyncio.Queue()
115
- result = await self._executor.execute(execution, events, cancel)
118
+ result = await asyncio.wait_for(
119
+ self._executor.execute(execution, events, cancel),
120
+ timeout=300,
121
+ )
116
122
  task_record.status = TaskStatus.COMPLETED if result.get("success") else TaskStatus.FAILED
117
123
  task_record.error = result.get("error")
118
124
  else:
@@ -147,8 +153,7 @@ class TaskManager:
147
153
  return list(self._tasks.values())
148
154
  return [t for t in self._tasks.values() if t.status == status]
149
155
 
150
- def add_todo_to_task(self, task_id: str, content: str, status: str = "pending") -> Optional["TaskChecklistItem"]:
151
- from onecode.tasks.models import TaskChecklistItem
156
+ def add_todo_to_task(self, task_id: str, content: str, status: str = "pending") -> Optional["TaskChecklistItem"]: # noqa: F821
152
157
  task = self._tasks.get(task_id)
153
158
  if not task:
154
159
  return None
@@ -211,9 +216,17 @@ class TaskManager:
211
216
  return True
212
217
 
213
218
  async def stop(self) -> None:
219
+ self._shutdown = True
214
220
  if self._worker_task:
215
221
  self._worker_task.cancel()
216
222
  try:
217
223
  await self._worker_task
218
224
  except asyncio.CancelledError:
219
- pass
225
+ pass
226
+ if self._running:
227
+ results = await asyncio.gather(
228
+ *self._running.values(), return_exceptions=True,
229
+ )
230
+ for tid, res in zip(list(self._running.keys()), results):
231
+ if isinstance(res, Exception):
232
+ logger.warning("Task %s exited with error: %s", tid, res)
@@ -5,10 +5,9 @@ import time
5
5
  import uuid
6
6
  from contextlib import contextmanager
7
7
  from datetime import datetime, timezone
8
- from pathlib import Path
9
8
  from typing import Any, Optional
10
9
 
11
- from onecode.config import CLOUD_DEV_HARNESS_DIR
10
+ from onecode.config import ONECODE_DIR
12
11
 
13
12
 
14
13
  class Span:
@@ -55,7 +54,7 @@ class Tracer:
55
54
  self.recording: bool = False
56
55
  self.spans: list[Span] = []
57
56
  self.stack: list[str] = []
58
- self.trace_dir = CLOUD_DEV_HARNESS_DIR / "traces"
57
+ self.trace_dir = ONECODE_DIR / "traces"
59
58
  self.trace_dir.mkdir(parents=True, exist_ok=True)
60
59
  self._otlp_endpoint: Optional[str] = None
61
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scemoon/cdh",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Cloud Dev Harness - cloud-native development Agent framework",
5
5
  "keywords": ["ai", "agent", "cloud", "development", "cli", "tui"],
6
6
  "license": "MIT",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cloud-dev-harness"
3
- version = "1.0.0"
3
+ version = "1.0.5"
4
4
  description = "Cloud Dev Harness - cloud-native development Agent framework with A2TUI"
5
5
  requires-python = ">=3.14.2"
6
6
  dependencies = [
@@ -57,6 +57,9 @@ onecode = [
57
57
  "builtin_skills/**/*",
58
58
  ]
59
59
 
60
+ [tool.ruff]
61
+ exclude = ["tui/", "dist/", ".venv/"]
62
+
60
63
  [tool.pytest.ini_options]
61
64
  asyncio_mode = "auto"
62
65
 
@@ -66,5 +69,6 @@ dev = [
66
69
  "pyinstrument>=5.1.1",
67
70
  "pytest>=9.0.3",
68
71
  "pytest-asyncio>=1.4.0",
72
+ "pytest-cov>=6.0.0",
69
73
  "textual-dev>=1.8.0",
70
74
  ]
package/run.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
- const { spawn, execSync } = require('child_process');
2
+ const { spawn, spawnSync, execSync } = require('child_process');
3
3
  const path = require('path');
4
+ const fs = require('fs');
5
+ const os = require('os');
4
6
  const pkg = require('./package.json');
5
7
 
6
8
  function exec(cmd, opts = {}) {
@@ -11,6 +13,12 @@ function exec(cmd, opts = {}) {
11
13
  }
12
14
  }
13
15
 
16
+ function execVerbose(cmd, label) {
17
+ if (label) console.log(`cdh: ${label}`);
18
+ const r = spawnSync(cmd, { shell: true, stdio: 'inherit' });
19
+ return { ok: r.status === 0 };
20
+ }
21
+
14
22
  function checkPython() {
15
23
  const script = 'import sys; v=sys.version_info; print(f"{v.major}.{v.minor}")';
16
24
  const r = exec(`python3 -c "${script}"`);
@@ -32,11 +40,49 @@ function checkCdhInstalled(pythonCmd) {
32
40
  return exec(`${pythonCmd} -m pip show cloud-dev-harness 2>/dev/null`).ok;
33
41
  }
34
42
 
43
+ function uninstallCmd() {
44
+ const cdhDir = path.join(os.homedir(), '.cdh');
45
+ const pythonDir = path.join(cdhDir, 'python');
46
+
47
+ console.log('cdh: Uninstalling Cloud Dev Harness...');
48
+ console.log('');
49
+
50
+ if (fs.existsSync(pythonDir)) {
51
+ console.log(`cdh: Removing Python environment at ${pythonDir}...`);
52
+ fs.rmSync(pythonDir, { recursive: true, force: true });
53
+ }
54
+
55
+ if (fs.existsSync(cdhDir)) {
56
+ console.log(`cdh: Removing global state at ${cdhDir}...`);
57
+ fs.rmSync(cdhDir, { recursive: true, force: true });
58
+ }
59
+
60
+ console.log('');
61
+ console.log('cdh: Cleanup complete. To finish uninstall:');
62
+ console.log('');
63
+ console.log(' 1. Remove the npm package:');
64
+ console.log(' pnpm remove -g @scemoon/cdh');
65
+ console.log(' # or: npm uninstall -g @scemoon/cdh');
66
+ console.log('');
67
+ console.log(' 2. Check your shell config (~/.zshrc, ~/.bashrc, etc.) for:');
68
+ console.log(' export PATH="$HOME/.cdh/python/bin:$PATH"');
69
+ console.log(' Remove this line if present.');
70
+ console.log('');
71
+
72
+ process.exit(0);
73
+ }
74
+
35
75
  function run(pythonModule) {
36
76
  const PKG_DIR = __dirname;
37
- const PYTHON_ENV_DIR = path.join(require('os').homedir(), '.cdh', 'python');
77
+ const PYTHON_ENV_DIR = path.join(os.homedir(), '.cdh', 'python');
38
78
 
39
79
  const args = process.argv.slice(2);
80
+
81
+ if (args[0] === 'uninstall') {
82
+ uninstallCmd();
83
+ return;
84
+ }
85
+
40
86
  if (args.includes('--version') || args.includes('-v')) {
41
87
  console.log(pkg.version);
42
88
  process.exit(0);
@@ -49,13 +95,19 @@ function run(pythonModule) {
49
95
  console.error(`cdh: Python ${py.version || 'not found'}, version 3.14+ is required.`);
50
96
 
51
97
  if (checkUv()) {
52
- console.log('cdh: Creating Python environment with uv...');
53
- exec(`uv venv "${PYTHON_ENV_DIR}"`);
98
+ if (fs.existsSync(PYTHON_ENV_DIR)) {
99
+ console.log('cdh: Python environment exists, reusing...');
100
+ } else {
101
+ const rv = execVerbose(`uv venv "${PYTHON_ENV_DIR}"`, 'Creating Python environment with uv...');
102
+ if (!rv.ok) {
103
+ console.error('cdh: Failed to create Python environment.');
104
+ process.exit(1);
105
+ }
106
+ }
54
107
  const venvPython = path.join(PYTHON_ENV_DIR, 'bin', 'python');
55
- console.log('cdh: Installing cloud-dev-harness...');
56
- const r = exec(`uv pip install "${PKG_DIR}" --python "${venvPython}"`);
57
- if (!r.ok) {
58
- console.error('cdh: Install failed:', r.out);
108
+ const ri = execVerbose(`uv pip install "${PKG_DIR}" --python "${venvPython}"`, 'Installing cloud-dev-harness...');
109
+ if (!ri.ok) {
110
+ console.error('cdh: Install failed.');
59
111
  process.exit(1);
60
112
  }
61
113
  if (isPostinstall) {
@@ -72,12 +124,14 @@ function run(pythonModule) {
72
124
  }
73
125
 
74
126
  if (!checkCdhInstalled(py.pythonCmd)) {
75
- console.log('cdh: Installing cloud-dev-harness...');
76
- const r = checkUv()
77
- ? exec(`uv pip install "${PKG_DIR}" --python "${py.pythonCmd}"`)
78
- : exec(`${py.pythonCmd} -m pip install "${PKG_DIR}"`);
79
- if (!r.ok) {
80
- console.error('cdh: Install failed:', r.out);
127
+ let ri;
128
+ if (checkUv()) {
129
+ ri = execVerbose(`uv pip install "${PKG_DIR}" --python "${py.pythonCmd}"`, 'Installing cloud-dev-harness...');
130
+ } else {
131
+ ri = execVerbose(`${py.pythonCmd} -m pip install "${PKG_DIR}"`, 'Installing cloud-dev-harness...');
132
+ }
133
+ if (!ri.ok) {
134
+ console.error('cdh: Install failed.');
81
135
  process.exit(1);
82
136
  }
83
137
  }
Binary file
package/tui/acp/agent.py CHANGED
@@ -28,7 +28,6 @@ from tui.acp import messages
28
28
  from tui.acp.prompt import build as build_prompt
29
29
  from tui import messages as tui_messages
30
30
  from tui.db import DB
31
- from tui import paths
32
31
  from tui import constants
33
32
  from tui.answer import Answer
34
33
 
@@ -88,6 +87,7 @@ class Agent(AgentBase):
88
87
  self._message_target: MessagePump | None = None
89
88
 
90
89
  self._terminal_count: int = 0
90
+ self.session_load_info: dict = {}
91
91
 
92
92
  @property
93
93
  def command(self) -> str | None:
@@ -122,9 +122,14 @@ class Agent(AgentBase):
122
122
  request: JSONRPC request object.
123
123
 
124
124
  """
125
- assert self._process is not None, "Process should be present here"
125
+ if self._process is None:
126
+ logger.warning(
127
+ "Cannot send request %s: agent process is not running",
128
+ request.body,
129
+ )
130
+ return
126
131
 
127
- logger.debug("[client] %s", request.body)
132
+ logger.info("[client] send: %s", request.body)
128
133
  if (stdin := self._process.stdin) is not None:
129
134
  stdin.write(b"%s\n" % request.body_json)
130
135
 
@@ -542,61 +547,64 @@ class Agent(AgentBase):
542
547
  if (task := asyncio.current_task()) is not None:
543
548
  tasks.discard(task)
544
549
 
545
- while line := await process.stdout.readline():
546
- # This line should contain JSON, which may be:
547
- # A) a JSONRPC request
548
- # B) a JSONRPC response to a previous request
549
- if not line.strip():
550
- continue
551
-
552
- try:
553
- line_str = line.decode("utf-8")
554
- except Exception as error:
555
- logger.error("Unable to decode utf-8 from agent: %s", error)
556
- continue
557
-
558
- logger.debug("[agent] %s", line_str.rstrip())
559
- try:
560
- agent_data: jsonrpc.JSONType = json.loads(line_str)
561
- except Exception as error:
562
- logger.error("Failed to decode JSON from agent: %s", error)
563
- continue
564
-
565
- if isinstance(agent_data, dict):
566
- if "result" in agent_data or "error" in agent_data:
567
- # Wait for pending notification dispatch tasks so all ACP
568
- # messages are queued before the response future resolves.
569
- if tasks:
570
- await asyncio.gather(*tasks, return_exceptions=True)
571
- tasks.clear()
572
- API.process_response(agent_data)
550
+ try:
551
+ while line := await process.stdout.readline():
552
+ # This line should contain JSON, which may be:
553
+ # A) a JSONRPC request
554
+ # B) a JSONRPC response to a previous request
555
+ if not line.strip():
573
556
  continue
574
557
 
575
- elif isinstance(agent_data, list):
576
- if not all(isinstance(datum, dict) for datum in agent_data):
577
- logger.error("Agent sent invalid data: %r", agent_data)
558
+ try:
559
+ line_str = line.decode("utf-8")
560
+ except Exception as error:
561
+ logger.error("Unable to decode utf-8 from agent: %s", error)
578
562
  continue
579
563
 
580
- if not isinstance(agent_data, dict):
581
- logger.error("Invalid JSON from agent: %r", agent_data)
582
- continue
564
+ logger.debug("[agent] %s", line_str.rstrip())
565
+ try:
566
+ agent_data: jsonrpc.JSONType = json.loads(line_str)
567
+ except Exception as error:
568
+ logger.error("Failed to decode JSON from agent: %s", error)
569
+ continue
583
570
 
584
- if not isinstance(agent_data, dict):
585
- logger.error("Invalid JSON from agent: %r", agent_data)
586
- continue
571
+ if isinstance(agent_data, dict):
572
+ if "result" in agent_data or "error" in agent_data:
573
+ # Wait for pending notification dispatch tasks so all ACP
574
+ # messages are queued before the response future resolves.
575
+ if tasks:
576
+ await asyncio.gather(*tasks, return_exceptions=True)
577
+ tasks.clear()
578
+ API.process_response(agent_data)
579
+ continue
580
+
581
+ elif isinstance(agent_data, list):
582
+ if not all(isinstance(datum, dict) for datum in agent_data):
583
+ logger.error("Agent sent invalid data: %r", agent_data)
584
+ continue
585
+
586
+ if not isinstance(agent_data, dict):
587
+ logger.error("Invalid JSON from agent: %r", agent_data)
588
+ continue
587
589
 
588
- # By this point we know it is a JSON RPC call
589
- assert isinstance(agent_data, dict)
590
- tasks.add(asyncio.create_task(call_jsonrpc(agent_data)))
591
- await asyncio.sleep(0)
590
+ if not isinstance(agent_data, dict):
591
+ logger.error("Invalid JSON from agent: %r", agent_data)
592
+ continue
592
593
 
593
- # Cancel all remaining tasks and wait for them to finish
594
- for task in tasks:
595
- task.cancel()
594
+ # By this point we know it is a JSON RPC call
595
+ assert isinstance(agent_data, dict)
596
+ tasks.add(asyncio.create_task(call_jsonrpc(agent_data)))
597
+ await asyncio.sleep(0)
598
+ except Exception as e:
599
+ logger.exception("Agent read loop failed: %s", e)
600
+ finally:
601
+ # Cancel all remaining tasks and wait for them to finish
602
+ for task in tasks:
603
+ task.cancel()
596
604
 
597
- # Wait for all tasks to complete cancellation
598
- if tasks:
599
- await asyncio.gather(*tasks, return_exceptions=True)
605
+ # Wait for all tasks to complete cancellation
606
+ if tasks:
607
+ await asyncio.gather(*tasks, return_exceptions=True)
600
608
 
601
609
  if process.returncode:
602
610
  assert process.stderr is not None
@@ -691,6 +699,7 @@ class Agent(AgentBase):
691
699
  protocol=data["protocol"],
692
700
  meta=data["meta"],
693
701
  )
702
+ self.post_message(tui_messages.SessionUpdate(session_pk=self.session_pk))
694
703
  self._pending_session_data = None
695
704
  await self._save_last_session_to_cdh()
696
705
 
@@ -816,8 +825,19 @@ class Agent(AgentBase):
816
825
  self.post_message(messages.SessionReplay(active=True))
817
826
  try:
818
827
  response = await session_load_response.wait()
828
+ self.session_load_info = {
829
+ "total_messages": response.get("_total_messages", 0),
830
+ "visible_start": response.get("_visible_start", 0),
831
+ }
819
832
  finally:
820
- self.post_message(messages.SessionReplay(active=False))
833
+ self.post_message(messages.SessionReplay(
834
+ active=False,
835
+ total_messages=self.session_load_info.get("total_messages", 0),
836
+ visible_count=self.session_load_info.get("total_messages", 0) - self.session_load_info.get("visible_start", 0),
837
+ ))
838
+
839
+ if error := response.get("error"):
840
+ logger.warning("Session load error: %s", error)
821
841
 
822
842
  if (modes := response.get("modes", None)) is not None:
823
843
  current_mode = modes["currentModeId"]
@@ -845,10 +865,20 @@ class Agent(AgentBase):
845
865
  await self._ensure_db_session()
846
866
 
847
867
  if session_title:
848
- self.post_message(tui_messages.SessionUpdate(name=session_title))
868
+ self.post_message(tui_messages.SessionUpdate(name=session_title, session_pk=self.session_pk))
849
869
 
850
870
  await self._save_last_session_to_cdh()
851
871
 
872
+ async def acp_load_earlier_messages(self, offset: int, limit: int) -> None:
873
+ """Load a range of earlier messages from the current session."""
874
+ assert self.session_id is not None
875
+ with self.request():
876
+ response = api.session_load_earlier(self.session_id, offset, limit)
877
+ try:
878
+ await response.wait()
879
+ except jsonrpc.JSONRPCError:
880
+ pass
881
+
852
882
  async def acp_session_prompt(
853
883
  self, prompt: list[protocol.ContentBlock]
854
884
  ) -> str | None:
@@ -858,6 +888,14 @@ class Agent(AgentBase):
858
888
  The stop reason.
859
889
 
860
890
  """
891
+ if self._process is None:
892
+ self.post_message(
893
+ AgentFail(
894
+ "Agent process is not running",
895
+ "The agent subprocess has terminated. Restart the session to continue.",
896
+ )
897
+ )
898
+ return None
861
899
  with self.request():
862
900
  session_prompt = api.session_prompt(prompt, self.session_id)
863
901
  try:
@@ -908,6 +946,15 @@ class Agent(AgentBase):
908
946
  async def set_mode(self, mode_id: str) -> str | None:
909
947
  return await self.acp_session_set_mode(mode_id)
910
948
 
949
+ async def acp_session_clear_todos(self) -> dict:
950
+ """Clear all todos via the session/clear_todos RPC."""
951
+ if self.session_id is None:
952
+ return {"cleared": False}
953
+ with self.request():
954
+ response = api.session_clear_todos(self.session_id)
955
+ result = await response.wait()
956
+ return result or {"cleared": False}
957
+
911
958
  async def set_session_name(self, name: str) -> None:
912
959
  if self.session_pk is None:
913
960
  return
@@ -919,16 +966,36 @@ class Agent(AgentBase):
919
966
  if session.load():
920
967
  session.name = name
921
968
  session.save()
922
- self.post_message(tui_messages.SessionUpdate(name=name))
969
+ self.post_message(tui_messages.SessionUpdate(name=name, session_pk=self.session_pk))
923
970
 
924
971
  async def acp_session_cancel(self) -> bool:
925
- with self.request():
926
- response = api.session_cancel(self.session_id, {})
972
+ """Send session/cancel notification to the agent subprocess.
973
+
974
+ Writes directly to stdin with drain() to ensure the message is
975
+ actually flushed through the pipe, unlike the Request path which
976
+ is synchronous and cannot drain.
977
+ """
978
+ if not self.session_id:
979
+ logger.info("[client] session/cancel: no session_id, skipping")
980
+ return False
981
+ cancel_msg = {
982
+ "jsonrpc": "2.0",
983
+ "method": "session/cancel",
984
+ "params": {"sessionId": self.session_id, "_meta": {}},
985
+ }
986
+ data = json.dumps(cancel_msg).encode("utf-8") + b"\n"
987
+ logger.info("[client] session/cancel: sending %s", cancel_msg)
988
+ stdin = self._process.stdin if self._process else None
989
+ if stdin is None:
990
+ logger.info("[client] session/cancel: no stdin pipe, skipping")
991
+ return False
992
+ stdin.write(data)
927
993
  try:
928
- await response.wait()
929
- except jsonrpc.APIError:
930
- # No-op if there is nothing to cancel
994
+ await stdin.drain()
995
+ except OSError:
996
+ logger.info("[client] session/cancel: drain failed", exc_info=True)
931
997
  return False
998
+ logger.info("[client] session/cancel: sent successfully")
932
999
  return True
933
1000
 
934
1001
  async def cancel(self) -> bool:
package/tui/acp/api.py CHANGED
@@ -59,3 +59,15 @@ def session_set_mode(sessionId: str, modeId: str) -> protocol.SetSessionModeResp
59
59
  def session_save(sessionId: str) -> dict:
60
60
  """Save the session state."""
61
61
  ...
62
+
63
+
64
+ @API.method(name="session/clear_todos")
65
+ def session_clear_todos(sessionId: str) -> dict:
66
+ """Clear all todos in the current session."""
67
+ ...
68
+
69
+
70
+ @API.method(name="session/load_earlier")
71
+ def session_load_earlier(sessionId: str, offset: int, limit: int) -> dict:
72
+ """Load a range of earlier messages from the current session."""
73
+ ...
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass, field
4
4
 
5
5
  from asyncio import Future
6
- from typing import Any, Mapping, TYPE_CHECKING
6
+ from typing import Mapping, TYPE_CHECKING
7
7
  from textual.message import Message
8
8
 
9
9
  import rich.repr
@@ -38,6 +38,8 @@ class Thinking(AgentMessage):
38
38
  class SessionReplay(AgentMessage):
39
39
  """Marks the start/end of a session replay (loading historical messages)."""
40
40
  active: bool
41
+ total_messages: int = 0
42
+ visible_count: int = 0
41
43
 
42
44
 
43
45
  @dataclass